UNPKG

9.05 kBJavaScriptView Raw
1'use strict'
2
3let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js')
4let { dirname, resolve, relative, sep } = require('path')
5let { pathToFileURL } = require('url')
6
7let Input = require('./input')
8
9let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator)
10let pathAvailable = Boolean(dirname && resolve && relative && sep)
11
12class MapGenerator {
13 constructor(stringify, root, opts, cssString) {
14 this.stringify = stringify
15 this.mapOpts = opts.map || {}
16 this.root = root
17 this.opts = opts
18 this.css = cssString
19 this.usesFileUrls = !this.mapOpts.from && this.mapOpts.absolute
20 }
21
22 isMap() {
23 if (typeof this.opts.map !== 'undefined') {
24 return !!this.opts.map
25 }
26 return this.previous().length > 0
27 }
28
29 previous() {
30 if (!this.previousMaps) {
31 this.previousMaps = []
32 if (this.root) {
33 this.root.walk(node => {
34 if (node.source && node.source.input.map) {
35 let map = node.source.input.map
36 if (!this.previousMaps.includes(map)) {
37 this.previousMaps.push(map)
38 }
39 }
40 })
41 } else {
42 let input = new Input(this.css, this.opts)
43 if (input.map) this.previousMaps.push(input.map)
44 }
45 }
46
47 return this.previousMaps
48 }
49
50 isInline() {
51 if (typeof this.mapOpts.inline !== 'undefined') {
52 return this.mapOpts.inline
53 }
54
55 let annotation = this.mapOpts.annotation
56 if (typeof annotation !== 'undefined' && annotation !== true) {
57 return false
58 }
59
60 if (this.previous().length) {
61 return this.previous().some(i => i.inline)
62 }
63 return true
64 }
65
66 isSourcesContent() {
67 if (typeof this.mapOpts.sourcesContent !== 'undefined') {
68 return this.mapOpts.sourcesContent
69 }
70 if (this.previous().length) {
71 return this.previous().some(i => i.withContent())
72 }
73 return true
74 }
75
76 clearAnnotation() {
77 if (this.mapOpts.annotation === false) return
78
79 if (this.root) {
80 let node
81 for (let i = this.root.nodes.length - 1; i >= 0; i--) {
82 node = this.root.nodes[i]
83 if (node.type !== 'comment') continue
84 if (node.text.indexOf('# sourceMappingURL=') === 0) {
85 this.root.removeChild(i)
86 }
87 }
88 } else if (this.css) {
89 this.css = this.css.replace(/(\n)?\/\*#[\S\s]*?\*\/$/gm, '')
90 }
91 }
92
93 setSourcesContent() {
94 let already = {}
95 if (this.root) {
96 this.root.walk(node => {
97 if (node.source) {
98 let from = node.source.input.from
99 if (from && !already[from]) {
100 already[from] = true
101 let fromUrl = this.usesFileUrls
102 ? this.toFileUrl(from)
103 : this.toUrl(this.path(from))
104 this.map.setSourceContent(fromUrl, node.source.input.css)
105 }
106 }
107 })
108 } else if (this.css) {
109 let from = this.opts.from
110 ? this.toUrl(this.path(this.opts.from))
111 : '<no source>'
112 this.map.setSourceContent(from, this.css)
113 }
114 }
115
116 applyPrevMaps() {
117 for (let prev of this.previous()) {
118 let from = this.toUrl(this.path(prev.file))
119 let root = prev.root || dirname(prev.file)
120 let map
121
122 if (this.mapOpts.sourcesContent === false) {
123 map = new SourceMapConsumer(prev.text)
124 if (map.sourcesContent) {
125 map.sourcesContent = map.sourcesContent.map(() => null)
126 }
127 } else {
128 map = prev.consumer()
129 }
130
131 this.map.applySourceMap(map, from, this.toUrl(this.path(root)))
132 }
133 }
134
135 isAnnotation() {
136 if (this.isInline()) {
137 return true
138 }
139 if (typeof this.mapOpts.annotation !== 'undefined') {
140 return this.mapOpts.annotation
141 }
142 if (this.previous().length) {
143 return this.previous().some(i => i.annotation)
144 }
145 return true
146 }
147
148 toBase64(str) {
149 if (Buffer) {
150 return Buffer.from(str).toString('base64')
151 } else {
152 return window.btoa(unescape(encodeURIComponent(str)))
153 }
154 }
155
156 addAnnotation() {
157 let content
158
159 if (this.isInline()) {
160 content =
161 'data:application/json;base64,' + this.toBase64(this.map.toString())
162 } else if (typeof this.mapOpts.annotation === 'string') {
163 content = this.mapOpts.annotation
164 } else if (typeof this.mapOpts.annotation === 'function') {
165 content = this.mapOpts.annotation(this.opts.to, this.root)
166 } else {
167 content = this.outputFile() + '.map'
168 }
169 let eol = '\n'
170 if (this.css.includes('\r\n')) eol = '\r\n'
171
172 this.css += eol + '/*# sourceMappingURL=' + content + ' */'
173 }
174
175 outputFile() {
176 if (this.opts.to) {
177 return this.path(this.opts.to)
178 } else if (this.opts.from) {
179 return this.path(this.opts.from)
180 } else {
181 return 'to.css'
182 }
183 }
184
185 generateMap() {
186 if (this.root) {
187 this.generateString()
188 } else if (this.previous().length === 1) {
189 let prev = this.previous()[0].consumer()
190 prev.file = this.outputFile()
191 this.map = SourceMapGenerator.fromSourceMap(prev)
192 } else {
193 this.map = new SourceMapGenerator({ file: this.outputFile() })
194 this.map.addMapping({
195 source: this.opts.from
196 ? this.toUrl(this.path(this.opts.from))
197 : '<no source>',
198 generated: { line: 1, column: 0 },
199 original: { line: 1, column: 0 }
200 })
201 }
202
203 if (this.isSourcesContent()) this.setSourcesContent()
204 if (this.root && this.previous().length > 0) this.applyPrevMaps()
205 if (this.isAnnotation()) this.addAnnotation()
206
207 if (this.isInline()) {
208 return [this.css]
209 } else {
210 return [this.css, this.map]
211 }
212 }
213
214 path(file) {
215 if (file.indexOf('<') === 0) return file
216 if (/^\w+:\/\//.test(file)) return file
217 if (this.mapOpts.absolute) return file
218
219 let from = this.opts.to ? dirname(this.opts.to) : '.'
220
221 if (typeof this.mapOpts.annotation === 'string') {
222 from = dirname(resolve(from, this.mapOpts.annotation))
223 }
224
225 file = relative(from, file)
226 return file
227 }
228
229 toUrl(path) {
230 if (sep === '\\') {
231 path = path.replace(/\\/g, '/')
232 }
233 return encodeURI(path).replace(/[#?]/g, encodeURIComponent)
234 }
235
236 toFileUrl(path) {
237 if (pathToFileURL) {
238 return pathToFileURL(path).toString()
239 } else {
240 throw new Error(
241 '`map.absolute` option is not available in this PostCSS build'
242 )
243 }
244 }
245
246 sourcePath(node) {
247 if (this.mapOpts.from) {
248 return this.toUrl(this.mapOpts.from)
249 } else if (this.usesFileUrls) {
250 return this.toFileUrl(node.source.input.from)
251 } else {
252 return this.toUrl(this.path(node.source.input.from))
253 }
254 }
255
256 generateString() {
257 this.css = ''
258 this.map = new SourceMapGenerator({ file: this.outputFile() })
259
260 let line = 1
261 let column = 1
262
263 let noSource = '<no source>'
264 let mapping = {
265 source: '',
266 generated: { line: 0, column: 0 },
267 original: { line: 0, column: 0 }
268 }
269
270 let lines, last
271 this.stringify(this.root, (str, node, type) => {
272 this.css += str
273
274 if (node && type !== 'end') {
275 mapping.generated.line = line
276 mapping.generated.column = column - 1
277 if (node.source && node.source.start) {
278 mapping.source = this.sourcePath(node)
279 mapping.original.line = node.source.start.line
280 mapping.original.column = node.source.start.column - 1
281 this.map.addMapping(mapping)
282 } else {
283 mapping.source = noSource
284 mapping.original.line = 1
285 mapping.original.column = 0
286 this.map.addMapping(mapping)
287 }
288 }
289
290 lines = str.match(/\n/g)
291 if (lines) {
292 line += lines.length
293 last = str.lastIndexOf('\n')
294 column = str.length - last
295 } else {
296 column += str.length
297 }
298
299 if (node && type !== 'start') {
300 let p = node.parent || { raws: {} }
301 let childless =
302 node.type === 'decl' || (node.type === 'atrule' && !node.nodes)
303 if (!childless || node !== p.last || p.raws.semicolon) {
304 if (node.source && node.source.end) {
305 mapping.source = this.sourcePath(node)
306 mapping.original.line = node.source.end.line
307 mapping.original.column = node.source.end.column - 1
308 mapping.generated.line = line
309 mapping.generated.column = column - 2
310 this.map.addMapping(mapping)
311 } else {
312 mapping.source = noSource
313 mapping.original.line = 1
314 mapping.original.column = 0
315 mapping.generated.line = line
316 mapping.generated.column = column - 1
317 this.map.addMapping(mapping)
318 }
319 }
320 }
321 })
322 }
323
324 generate() {
325 this.clearAnnotation()
326 if (pathAvailable && sourceMapAvailable && this.isMap()) {
327 return this.generateMap()
328 } else {
329 let result = ''
330 this.stringify(this.root, i => {
331 result += i
332 })
333 return [result]
334 }
335 }
336}
337
338module.exports = MapGenerator