UNPKG

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