UNPKG

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