UNPKG

8.76 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 ? this.toUrl(this.opts.from) : '<no source>',
195 generated: { line: 1, column: 0 },
196 original: { line: 1, column: 0 }
197 })
198 }
199
200 if (this.isSourcesContent()) this.setSourcesContent()
201 if (this.root && this.previous().length > 0) this.applyPrevMaps()
202 if (this.isAnnotation()) this.addAnnotation()
203
204 if (this.isInline()) {
205 return [this.css]
206 } else {
207 return [this.css, this.map]
208 }
209 }
210
211 path(file) {
212 if (file.indexOf('<') === 0) return file
213 if (/^\w+:\/\//.test(file)) return file
214 if (this.mapOpts.absolute) return file
215
216 let from = this.opts.to ? dirname(this.opts.to) : '.'
217
218 if (typeof this.mapOpts.annotation === 'string') {
219 from = dirname(resolve(from, this.mapOpts.annotation))
220 }
221
222 file = relative(from, file)
223 return file
224 }
225
226 toUrl(path) {
227 if (sep === '\\') {
228 path = path.replace(/\\/g, '/')
229 }
230 return encodeURI(path).replace(/[#?]/g, encodeURIComponent)
231 }
232
233 sourcePath(node) {
234 if (this.mapOpts.from) {
235 return this.toUrl(this.mapOpts.from)
236 } else if (this.mapOpts.absolute) {
237 if (pathToFileURL) {
238 return pathToFileURL(node.source.input.from).toString()
239 } else {
240 throw new Error(
241 '`map.absolute` option is not available in this PostCSS build'
242 )
243 }
244 } else {
245 return this.toUrl(this.path(node.source.input.from))
246 }
247 }
248
249 generateString() {
250 this.css = ''
251 this.map = new SourceMapGenerator({ file: this.outputFile() })
252
253 let line = 1
254 let column = 1
255
256 let noSource = '<no source>'
257 let mapping = {
258 source: '',
259 generated: { line: 0, column: 0 },
260 original: { line: 0, column: 0 }
261 }
262
263 let lines, last
264 this.stringify(this.root, (str, node, type) => {
265 this.css += str
266
267 if (node && type !== 'end') {
268 mapping.generated.line = line
269 mapping.generated.column = column - 1
270 if (node.source && node.source.start) {
271 mapping.source = this.sourcePath(node)
272 mapping.original.line = node.source.start.line
273 mapping.original.column = node.source.start.column - 1
274 this.map.addMapping(mapping)
275 } else {
276 mapping.source = noSource
277 mapping.original.line = 1
278 mapping.original.column = 0
279 this.map.addMapping(mapping)
280 }
281 }
282
283 lines = str.match(/\n/g)
284 if (lines) {
285 line += lines.length
286 last = str.lastIndexOf('\n')
287 column = str.length - last
288 } else {
289 column += str.length
290 }
291
292 if (node && type !== 'start') {
293 let p = node.parent || { raws: {} }
294 if (node.type !== 'decl' || node !== p.last || p.raws.semicolon) {
295 if (node.source && node.source.end) {
296 mapping.source = this.sourcePath(node)
297 mapping.original.line = node.source.end.line
298 mapping.original.column = node.source.end.column - 1
299 mapping.generated.line = line
300 mapping.generated.column = column - 2
301 this.map.addMapping(mapping)
302 } else {
303 mapping.source = noSource
304 mapping.original.line = 1
305 mapping.original.column = 0
306 mapping.generated.line = line
307 mapping.generated.column = column - 1
308 this.map.addMapping(mapping)
309 }
310 }
311 }
312 })
313 }
314
315 generate() {
316 this.clearAnnotation()
317 if (pathAvailable && sourceMapAvailable && this.isMap()) {
318 return this.generateMap()
319 } else {
320 let result = ''
321 this.stringify(this.root, i => {
322 result += i
323 })
324 return [result]
325 }
326 }
327}
328
329module.exports = MapGenerator