1 | 'use strict'
|
2 |
|
3 | let { isClean, my } = require('./symbols')
|
4 | let MapGenerator = require('./map-generator')
|
5 | let stringify = require('./stringify')
|
6 | let Container = require('./container')
|
7 | let Document = require('./document')
|
8 | let warnOnce = require('./warn-once')
|
9 | let Result = require('./result')
|
10 | let parse = require('./parse')
|
11 | let Root = require('./root')
|
12 |
|
13 | const TYPE_TO_CLASS_NAME = {
|
14 | document: 'Document',
|
15 | root: 'Root',
|
16 | atrule: 'AtRule',
|
17 | rule: 'Rule',
|
18 | decl: 'Declaration',
|
19 | comment: 'Comment'
|
20 | }
|
21 |
|
22 | const PLUGIN_PROPS = {
|
23 | postcssPlugin: true,
|
24 | prepare: true,
|
25 | Once: true,
|
26 | Document: true,
|
27 | Root: true,
|
28 | Declaration: true,
|
29 | Rule: true,
|
30 | AtRule: true,
|
31 | Comment: true,
|
32 | DeclarationExit: true,
|
33 | RuleExit: true,
|
34 | AtRuleExit: true,
|
35 | CommentExit: true,
|
36 | RootExit: true,
|
37 | DocumentExit: true,
|
38 | OnceExit: true
|
39 | }
|
40 |
|
41 | const NOT_VISITORS = {
|
42 | postcssPlugin: true,
|
43 | prepare: true,
|
44 | Once: true
|
45 | }
|
46 |
|
47 | const CHILDREN = 0
|
48 |
|
49 | function isPromise(obj) {
|
50 | return typeof obj === 'object' && typeof obj.then === 'function'
|
51 | }
|
52 |
|
53 | function getEvents(node) {
|
54 | let key = false
|
55 | let type = TYPE_TO_CLASS_NAME[node.type]
|
56 | if (node.type === 'decl') {
|
57 | key = node.prop.toLowerCase()
|
58 | } else if (node.type === 'atrule') {
|
59 | key = node.name.toLowerCase()
|
60 | }
|
61 |
|
62 | if (key && node.append) {
|
63 | return [
|
64 | type,
|
65 | type + '-' + key,
|
66 | CHILDREN,
|
67 | type + 'Exit',
|
68 | type + 'Exit-' + key
|
69 | ]
|
70 | } else if (key) {
|
71 | return [type, type + '-' + key, type + 'Exit', type + 'Exit-' + key]
|
72 | } else if (node.append) {
|
73 | return [type, CHILDREN, type + 'Exit']
|
74 | } else {
|
75 | return [type, type + 'Exit']
|
76 | }
|
77 | }
|
78 |
|
79 | function toStack(node) {
|
80 | let events
|
81 | if (node.type === 'document') {
|
82 | events = ['Document', CHILDREN, 'DocumentExit']
|
83 | } else if (node.type === 'root') {
|
84 | events = ['Root', CHILDREN, 'RootExit']
|
85 | } else {
|
86 | events = getEvents(node)
|
87 | }
|
88 |
|
89 | return {
|
90 | node,
|
91 | events,
|
92 | eventIndex: 0,
|
93 | visitors: [],
|
94 | visitorIndex: 0,
|
95 | iterator: 0
|
96 | }
|
97 | }
|
98 |
|
99 | function cleanMarks(node) {
|
100 | node[isClean] = false
|
101 | if (node.nodes) node.nodes.forEach(i => cleanMarks(i))
|
102 | return node
|
103 | }
|
104 |
|
105 | let postcss = {}
|
106 |
|
107 | class LazyResult {
|
108 | constructor(processor, css, opts) {
|
109 | this.stringified = false
|
110 | this.processed = false
|
111 |
|
112 | let root
|
113 | if (
|
114 | typeof css === 'object' &&
|
115 | css !== null &&
|
116 | (css.type === 'root' || css.type === 'document')
|
117 | ) {
|
118 | root = cleanMarks(css)
|
119 | } else if (css instanceof LazyResult || css instanceof Result) {
|
120 | root = cleanMarks(css.root)
|
121 | if (css.map) {
|
122 | if (typeof opts.map === 'undefined') opts.map = {}
|
123 | if (!opts.map.inline) opts.map.inline = false
|
124 | opts.map.prev = css.map
|
125 | }
|
126 | } else {
|
127 | let parser = parse
|
128 | if (opts.syntax) parser = opts.syntax.parse
|
129 | if (opts.parser) parser = opts.parser
|
130 | if (parser.parse) parser = parser.parse
|
131 |
|
132 | try {
|
133 | root = parser(css, opts)
|
134 | } catch (error) {
|
135 | this.processed = true
|
136 | this.error = error
|
137 | }
|
138 |
|
139 | if (root && !root[my]) {
|
140 |
|
141 | Container.rebuild(root)
|
142 | }
|
143 | }
|
144 |
|
145 | this.result = new Result(processor, root, opts)
|
146 | this.helpers = { ...postcss, result: this.result, postcss }
|
147 | this.plugins = this.processor.plugins.map(plugin => {
|
148 | if (typeof plugin === 'object' && plugin.prepare) {
|
149 | return { ...plugin, ...plugin.prepare(this.result) }
|
150 | } else {
|
151 | return plugin
|
152 | }
|
153 | })
|
154 | }
|
155 |
|
156 | get [Symbol.toStringTag]() {
|
157 | return 'LazyResult'
|
158 | }
|
159 |
|
160 | get processor() {
|
161 | return this.result.processor
|
162 | }
|
163 |
|
164 | get opts() {
|
165 | return this.result.opts
|
166 | }
|
167 |
|
168 | get css() {
|
169 | return this.stringify().css
|
170 | }
|
171 |
|
172 | get content() {
|
173 | return this.stringify().content
|
174 | }
|
175 |
|
176 | get map() {
|
177 | return this.stringify().map
|
178 | }
|
179 |
|
180 | get root() {
|
181 | return this.sync().root
|
182 | }
|
183 |
|
184 | get messages() {
|
185 | return this.sync().messages
|
186 | }
|
187 |
|
188 | warnings() {
|
189 | return this.sync().warnings()
|
190 | }
|
191 |
|
192 | toString() {
|
193 | return this.css
|
194 | }
|
195 |
|
196 | then(onFulfilled, onRejected) {
|
197 | if (process.env.NODE_ENV !== 'production') {
|
198 | if (!('from' in this.opts)) {
|
199 | warnOnce(
|
200 | 'Without `from` option PostCSS could generate wrong source map ' +
|
201 | 'and will not find Browserslist config. Set it to CSS file path ' +
|
202 | 'or to `undefined` to prevent this warning.'
|
203 | )
|
204 | }
|
205 | }
|
206 | return this.async().then(onFulfilled, onRejected)
|
207 | }
|
208 |
|
209 | catch(onRejected) {
|
210 | return this.async().catch(onRejected)
|
211 | }
|
212 |
|
213 | finally(onFinally) {
|
214 | return this.async().then(onFinally, onFinally)
|
215 | }
|
216 |
|
217 | async() {
|
218 | if (this.error) return Promise.reject(this.error)
|
219 | if (this.processed) return Promise.resolve(this.result)
|
220 | if (!this.processing) {
|
221 | this.processing = this.runAsync()
|
222 | }
|
223 | return this.processing
|
224 | }
|
225 |
|
226 | sync() {
|
227 | if (this.error) throw this.error
|
228 | if (this.processed) return this.result
|
229 | this.processed = true
|
230 |
|
231 | if (this.processing) {
|
232 | throw this.getAsyncError()
|
233 | }
|
234 |
|
235 | for (let plugin of this.plugins) {
|
236 | let promise = this.runOnRoot(plugin)
|
237 | if (isPromise(promise)) {
|
238 | throw this.getAsyncError()
|
239 | }
|
240 | }
|
241 |
|
242 | this.prepareVisitors()
|
243 | if (this.hasListener) {
|
244 | let root = this.result.root
|
245 | while (!root[isClean]) {
|
246 | root[isClean] = true
|
247 | this.walkSync(root)
|
248 | }
|
249 | if (this.listeners.OnceExit) {
|
250 | if (root.type === 'document') {
|
251 | for (let subRoot of root.nodes) {
|
252 | this.visitSync(this.listeners.OnceExit, subRoot)
|
253 | }
|
254 | } else {
|
255 | this.visitSync(this.listeners.OnceExit, root)
|
256 | }
|
257 | }
|
258 | }
|
259 |
|
260 | return this.result
|
261 | }
|
262 |
|
263 | stringify() {
|
264 | if (this.error) throw this.error
|
265 | if (this.stringified) return this.result
|
266 | this.stringified = true
|
267 |
|
268 | this.sync()
|
269 |
|
270 | let opts = this.result.opts
|
271 | let str = stringify
|
272 | if (opts.syntax) str = opts.syntax.stringify
|
273 | if (opts.stringifier) str = opts.stringifier
|
274 | if (str.stringify) str = str.stringify
|
275 |
|
276 | let map = new MapGenerator(str, this.result.root, this.result.opts)
|
277 | let data = map.generate()
|
278 | this.result.css = data[0]
|
279 | this.result.map = data[1]
|
280 |
|
281 | return this.result
|
282 | }
|
283 |
|
284 | walkSync(node) {
|
285 | node[isClean] = true
|
286 | let events = getEvents(node)
|
287 | for (let event of events) {
|
288 | if (event === CHILDREN) {
|
289 | if (node.nodes) {
|
290 | node.each(child => {
|
291 | if (!child[isClean]) this.walkSync(child)
|
292 | })
|
293 | }
|
294 | } else {
|
295 | let visitors = this.listeners[event]
|
296 | if (visitors) {
|
297 | if (this.visitSync(visitors, node.toProxy())) return
|
298 | }
|
299 | }
|
300 | }
|
301 | }
|
302 |
|
303 | visitSync(visitors, node) {
|
304 | for (let [plugin, visitor] of visitors) {
|
305 | this.result.lastPlugin = plugin
|
306 | let promise
|
307 | try {
|
308 | promise = visitor(node, this.helpers)
|
309 | } catch (e) {
|
310 | throw this.handleError(e, node.proxyOf)
|
311 | }
|
312 | if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
|
313 | return true
|
314 | }
|
315 | if (isPromise(promise)) {
|
316 | throw this.getAsyncError()
|
317 | }
|
318 | }
|
319 | }
|
320 |
|
321 | runOnRoot(plugin) {
|
322 | this.result.lastPlugin = plugin
|
323 | try {
|
324 | if (typeof plugin === 'object' && plugin.Once) {
|
325 | if (this.result.root.type === 'document') {
|
326 | let roots = this.result.root.nodes.map(root =>
|
327 | plugin.Once(root, this.helpers)
|
328 | )
|
329 |
|
330 | if (isPromise(roots[0])) {
|
331 | return Promise.all(roots)
|
332 | }
|
333 |
|
334 | return roots
|
335 | }
|
336 |
|
337 | return plugin.Once(this.result.root, this.helpers)
|
338 | } else if (typeof plugin === 'function') {
|
339 | return plugin(this.result.root, this.result)
|
340 | }
|
341 | } catch (error) {
|
342 | throw this.handleError(error)
|
343 | }
|
344 | }
|
345 |
|
346 | getAsyncError() {
|
347 | throw new Error('Use process(css).then(cb) to work with async plugins')
|
348 | }
|
349 |
|
350 | handleError(error, node) {
|
351 | let plugin = this.result.lastPlugin
|
352 | try {
|
353 | if (node) node.addToError(error)
|
354 | this.error = error
|
355 | if (error.name === 'CssSyntaxError' && !error.plugin) {
|
356 | error.plugin = plugin.postcssPlugin
|
357 | error.setMessage()
|
358 | } else if (plugin.postcssVersion) {
|
359 | if (process.env.NODE_ENV !== 'production') {
|
360 | let pluginName = plugin.postcssPlugin
|
361 | let pluginVer = plugin.postcssVersion
|
362 | let runtimeVer = this.result.processor.version
|
363 | let a = pluginVer.split('.')
|
364 | let b = runtimeVer.split('.')
|
365 |
|
366 | if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) {
|
367 |
|
368 | console.error(
|
369 | 'Unknown error from PostCSS plugin. Your current PostCSS ' +
|
370 | 'version is ' +
|
371 | runtimeVer +
|
372 | ', but ' +
|
373 | pluginName +
|
374 | ' uses ' +
|
375 | pluginVer +
|
376 | '. Perhaps this is the source of the error below.'
|
377 | )
|
378 | }
|
379 | }
|
380 | }
|
381 | } catch (err) {
|
382 |
|
383 |
|
384 | if (console && console.error) console.error(err)
|
385 | }
|
386 | return error
|
387 | }
|
388 |
|
389 | async runAsync() {
|
390 | this.plugin = 0
|
391 | for (let i = 0; i < this.plugins.length; i++) {
|
392 | let plugin = this.plugins[i]
|
393 | let promise = this.runOnRoot(plugin)
|
394 | if (isPromise(promise)) {
|
395 | try {
|
396 | await promise
|
397 | } catch (error) {
|
398 | throw this.handleError(error)
|
399 | }
|
400 | }
|
401 | }
|
402 |
|
403 | this.prepareVisitors()
|
404 | if (this.hasListener) {
|
405 | let root = this.result.root
|
406 | while (!root[isClean]) {
|
407 | root[isClean] = true
|
408 | let stack = [toStack(root)]
|
409 | while (stack.length > 0) {
|
410 | let promise = this.visitTick(stack)
|
411 | if (isPromise(promise)) {
|
412 | try {
|
413 | await promise
|
414 | } catch (e) {
|
415 | let node = stack[stack.length - 1].node
|
416 | throw this.handleError(e, node)
|
417 | }
|
418 | }
|
419 | }
|
420 | }
|
421 |
|
422 | if (this.listeners.OnceExit) {
|
423 | for (let [plugin, visitor] of this.listeners.OnceExit) {
|
424 | this.result.lastPlugin = plugin
|
425 | try {
|
426 | if (root.type === 'document') {
|
427 | let roots = root.nodes.map(subRoot =>
|
428 | visitor(subRoot, this.helpers)
|
429 | )
|
430 |
|
431 | await Promise.all(roots)
|
432 | } else {
|
433 | await visitor(root, this.helpers)
|
434 | }
|
435 | } catch (e) {
|
436 | throw this.handleError(e)
|
437 | }
|
438 | }
|
439 | }
|
440 | }
|
441 |
|
442 | this.processed = true
|
443 | return this.stringify()
|
444 | }
|
445 |
|
446 | prepareVisitors() {
|
447 | this.listeners = {}
|
448 | let add = (plugin, type, cb) => {
|
449 | if (!this.listeners[type]) this.listeners[type] = []
|
450 | this.listeners[type].push([plugin, cb])
|
451 | }
|
452 | for (let plugin of this.plugins) {
|
453 | if (typeof plugin === 'object') {
|
454 | for (let event in plugin) {
|
455 | if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) {
|
456 | throw new Error(
|
457 | `Unknown event ${event} in ${plugin.postcssPlugin}. ` +
|
458 | `Try to update PostCSS (${this.processor.version} now).`
|
459 | )
|
460 | }
|
461 | if (!NOT_VISITORS[event]) {
|
462 | if (typeof plugin[event] === 'object') {
|
463 | for (let filter in plugin[event]) {
|
464 | if (filter === '*') {
|
465 | add(plugin, event, plugin[event][filter])
|
466 | } else {
|
467 | add(
|
468 | plugin,
|
469 | event + '-' + filter.toLowerCase(),
|
470 | plugin[event][filter]
|
471 | )
|
472 | }
|
473 | }
|
474 | } else if (typeof plugin[event] === 'function') {
|
475 | add(plugin, event, plugin[event])
|
476 | }
|
477 | }
|
478 | }
|
479 | }
|
480 | }
|
481 | this.hasListener = Object.keys(this.listeners).length > 0
|
482 | }
|
483 |
|
484 | visitTick(stack) {
|
485 | let visit = stack[stack.length - 1]
|
486 | let { node, visitors } = visit
|
487 |
|
488 | if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
|
489 | stack.pop()
|
490 | return
|
491 | }
|
492 |
|
493 | if (visitors.length > 0 && visit.visitorIndex < visitors.length) {
|
494 | let [plugin, visitor] = visitors[visit.visitorIndex]
|
495 | visit.visitorIndex += 1
|
496 | if (visit.visitorIndex === visitors.length) {
|
497 | visit.visitors = []
|
498 | visit.visitorIndex = 0
|
499 | }
|
500 | this.result.lastPlugin = plugin
|
501 | try {
|
502 | return visitor(node.toProxy(), this.helpers)
|
503 | } catch (e) {
|
504 | throw this.handleError(e, node)
|
505 | }
|
506 | }
|
507 |
|
508 | if (visit.iterator !== 0) {
|
509 | let iterator = visit.iterator
|
510 | let child
|
511 | while ((child = node.nodes[node.indexes[iterator]])) {
|
512 | node.indexes[iterator] += 1
|
513 | if (!child[isClean]) {
|
514 | child[isClean] = true
|
515 | stack.push(toStack(child))
|
516 | return
|
517 | }
|
518 | }
|
519 | visit.iterator = 0
|
520 | delete node.indexes[iterator]
|
521 | }
|
522 |
|
523 | let events = visit.events
|
524 | while (visit.eventIndex < events.length) {
|
525 | let event = events[visit.eventIndex]
|
526 | visit.eventIndex += 1
|
527 | if (event === CHILDREN) {
|
528 | if (node.nodes && node.nodes.length) {
|
529 | node[isClean] = true
|
530 | visit.iterator = node.getIterator()
|
531 | }
|
532 | return
|
533 | } else if (this.listeners[event]) {
|
534 | visit.visitors = this.listeners[event]
|
535 | return
|
536 | }
|
537 | }
|
538 | stack.pop()
|
539 | }
|
540 | }
|
541 |
|
542 | LazyResult.registerPostcss = dependant => {
|
543 | postcss = dependant
|
544 | }
|
545 |
|
546 | module.exports = LazyResult
|
547 | LazyResult.default = LazyResult
|
548 |
|
549 | Root.registerLazyResult(LazyResult)
|
550 | Document.registerLazyResult(LazyResult)
|