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 | atrule: 'AtRule',
|
15 | comment: 'Comment',
|
16 | decl: 'Declaration',
|
17 | document: 'Document',
|
18 | root: 'Root',
|
19 | rule: 'Rule'
|
20 | }
|
21 |
|
22 | const PLUGIN_PROPS = {
|
23 | AtRule: true,
|
24 | AtRuleExit: true,
|
25 | Comment: true,
|
26 | CommentExit: true,
|
27 | Declaration: true,
|
28 | DeclarationExit: true,
|
29 | Document: true,
|
30 | DocumentExit: true,
|
31 | Once: true,
|
32 | OnceExit: true,
|
33 | postcssPlugin: true,
|
34 | prepare: true,
|
35 | Root: true,
|
36 | RootExit: true,
|
37 | Rule: true,
|
38 | RuleExit: true
|
39 | }
|
40 |
|
41 | const NOT_VISITORS = {
|
42 | Once: true,
|
43 | postcssPlugin: true,
|
44 | prepare: 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 | eventIndex: 0,
|
91 | events,
|
92 | iterator: 0,
|
93 | node,
|
94 | visitorIndex: 0,
|
95 | visitors: []
|
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, postcss, result: this.result }
|
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 | async() {
|
157 | if (this.error) return Promise.reject(this.error)
|
158 | if (this.processed) return Promise.resolve(this.result)
|
159 | if (!this.processing) {
|
160 | this.processing = this.runAsync()
|
161 | }
|
162 | return this.processing
|
163 | }
|
164 |
|
165 | catch(onRejected) {
|
166 | return this.async().catch(onRejected)
|
167 | }
|
168 |
|
169 | get content() {
|
170 | return this.stringify().content
|
171 | }
|
172 |
|
173 | get css() {
|
174 | return this.stringify().css
|
175 | }
|
176 |
|
177 | finally(onFinally) {
|
178 | return this.async().then(onFinally, onFinally)
|
179 | }
|
180 |
|
181 | getAsyncError() {
|
182 | throw new Error('Use process(css).then(cb) to work with async plugins')
|
183 | }
|
184 |
|
185 | handleError(error, node) {
|
186 | let plugin = this.result.lastPlugin
|
187 | try {
|
188 | if (node) node.addToError(error)
|
189 | this.error = error
|
190 | if (error.name === 'CssSyntaxError' && !error.plugin) {
|
191 | error.plugin = plugin.postcssPlugin
|
192 | error.setMessage()
|
193 | } else if (plugin.postcssVersion) {
|
194 | if (process.env.NODE_ENV !== 'production') {
|
195 | let pluginName = plugin.postcssPlugin
|
196 | let pluginVer = plugin.postcssVersion
|
197 | let runtimeVer = this.result.processor.version
|
198 | let a = pluginVer.split('.')
|
199 | let b = runtimeVer.split('.')
|
200 |
|
201 | if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) {
|
202 |
|
203 | console.error(
|
204 | 'Unknown error from PostCSS plugin. Your current PostCSS ' +
|
205 | 'version is ' +
|
206 | runtimeVer +
|
207 | ', but ' +
|
208 | pluginName +
|
209 | ' uses ' +
|
210 | pluginVer +
|
211 | '. Perhaps this is the source of the error below.'
|
212 | )
|
213 | }
|
214 | }
|
215 | }
|
216 | } catch (err) {
|
217 |
|
218 |
|
219 | if (console && console.error) console.error(err)
|
220 | }
|
221 | return error
|
222 | }
|
223 |
|
224 | get map() {
|
225 | return this.stringify().map
|
226 | }
|
227 |
|
228 | get messages() {
|
229 | return this.sync().messages
|
230 | }
|
231 |
|
232 | get opts() {
|
233 | return this.result.opts
|
234 | }
|
235 |
|
236 | prepareVisitors() {
|
237 | this.listeners = {}
|
238 | let add = (plugin, type, cb) => {
|
239 | if (!this.listeners[type]) this.listeners[type] = []
|
240 | this.listeners[type].push([plugin, cb])
|
241 | }
|
242 | for (let plugin of this.plugins) {
|
243 | if (typeof plugin === 'object') {
|
244 | for (let event in plugin) {
|
245 | if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) {
|
246 | throw new Error(
|
247 | `Unknown event ${event} in ${plugin.postcssPlugin}. ` +
|
248 | `Try to update PostCSS (${this.processor.version} now).`
|
249 | )
|
250 | }
|
251 | if (!NOT_VISITORS[event]) {
|
252 | if (typeof plugin[event] === 'object') {
|
253 | for (let filter in plugin[event]) {
|
254 | if (filter === '*') {
|
255 | add(plugin, event, plugin[event][filter])
|
256 | } else {
|
257 | add(
|
258 | plugin,
|
259 | event + '-' + filter.toLowerCase(),
|
260 | plugin[event][filter]
|
261 | )
|
262 | }
|
263 | }
|
264 | } else if (typeof plugin[event] === 'function') {
|
265 | add(plugin, event, plugin[event])
|
266 | }
|
267 | }
|
268 | }
|
269 | }
|
270 | }
|
271 | this.hasListener = Object.keys(this.listeners).length > 0
|
272 | }
|
273 |
|
274 | get processor() {
|
275 | return this.result.processor
|
276 | }
|
277 |
|
278 | get root() {
|
279 | return this.sync().root
|
280 | }
|
281 |
|
282 | async runAsync() {
|
283 | this.plugin = 0
|
284 | for (let i = 0; i < this.plugins.length; i++) {
|
285 | let plugin = this.plugins[i]
|
286 | let promise = this.runOnRoot(plugin)
|
287 | if (isPromise(promise)) {
|
288 | try {
|
289 | await promise
|
290 | } catch (error) {
|
291 | throw this.handleError(error)
|
292 | }
|
293 | }
|
294 | }
|
295 |
|
296 | this.prepareVisitors()
|
297 | if (this.hasListener) {
|
298 | let root = this.result.root
|
299 | while (!root[isClean]) {
|
300 | root[isClean] = true
|
301 | let stack = [toStack(root)]
|
302 | while (stack.length > 0) {
|
303 | let promise = this.visitTick(stack)
|
304 | if (isPromise(promise)) {
|
305 | try {
|
306 | await promise
|
307 | } catch (e) {
|
308 | let node = stack[stack.length - 1].node
|
309 | throw this.handleError(e, node)
|
310 | }
|
311 | }
|
312 | }
|
313 | }
|
314 |
|
315 | if (this.listeners.OnceExit) {
|
316 | for (let [plugin, visitor] of this.listeners.OnceExit) {
|
317 | this.result.lastPlugin = plugin
|
318 | try {
|
319 | if (root.type === 'document') {
|
320 | let roots = root.nodes.map(subRoot =>
|
321 | visitor(subRoot, this.helpers)
|
322 | )
|
323 |
|
324 | await Promise.all(roots)
|
325 | } else {
|
326 | await visitor(root, this.helpers)
|
327 | }
|
328 | } catch (e) {
|
329 | throw this.handleError(e)
|
330 | }
|
331 | }
|
332 | }
|
333 | }
|
334 |
|
335 | this.processed = true
|
336 | return this.stringify()
|
337 | }
|
338 |
|
339 | runOnRoot(plugin) {
|
340 | this.result.lastPlugin = plugin
|
341 | try {
|
342 | if (typeof plugin === 'object' && plugin.Once) {
|
343 | if (this.result.root.type === 'document') {
|
344 | let roots = this.result.root.nodes.map(root =>
|
345 | plugin.Once(root, this.helpers)
|
346 | )
|
347 |
|
348 | if (isPromise(roots[0])) {
|
349 | return Promise.all(roots)
|
350 | }
|
351 |
|
352 | return roots
|
353 | }
|
354 |
|
355 | return plugin.Once(this.result.root, this.helpers)
|
356 | } else if (typeof plugin === 'function') {
|
357 | return plugin(this.result.root, this.result)
|
358 | }
|
359 | } catch (error) {
|
360 | throw this.handleError(error)
|
361 | }
|
362 | }
|
363 |
|
364 | stringify() {
|
365 | if (this.error) throw this.error
|
366 | if (this.stringified) return this.result
|
367 | this.stringified = true
|
368 |
|
369 | this.sync()
|
370 |
|
371 | let opts = this.result.opts
|
372 | let str = stringify
|
373 | if (opts.syntax) str = opts.syntax.stringify
|
374 | if (opts.stringifier) str = opts.stringifier
|
375 | if (str.stringify) str = str.stringify
|
376 |
|
377 | let map = new MapGenerator(str, this.result.root, this.result.opts)
|
378 | let data = map.generate()
|
379 | this.result.css = data[0]
|
380 | this.result.map = data[1]
|
381 |
|
382 | return this.result
|
383 | }
|
384 |
|
385 | get [Symbol.toStringTag]() {
|
386 | return 'LazyResult'
|
387 | }
|
388 |
|
389 | sync() {
|
390 | if (this.error) throw this.error
|
391 | if (this.processed) return this.result
|
392 | this.processed = true
|
393 |
|
394 | if (this.processing) {
|
395 | throw this.getAsyncError()
|
396 | }
|
397 |
|
398 | for (let plugin of this.plugins) {
|
399 | let promise = this.runOnRoot(plugin)
|
400 | if (isPromise(promise)) {
|
401 | throw this.getAsyncError()
|
402 | }
|
403 | }
|
404 |
|
405 | this.prepareVisitors()
|
406 | if (this.hasListener) {
|
407 | let root = this.result.root
|
408 | while (!root[isClean]) {
|
409 | root[isClean] = true
|
410 | this.walkSync(root)
|
411 | }
|
412 | if (this.listeners.OnceExit) {
|
413 | if (root.type === 'document') {
|
414 | for (let subRoot of root.nodes) {
|
415 | this.visitSync(this.listeners.OnceExit, subRoot)
|
416 | }
|
417 | } else {
|
418 | this.visitSync(this.listeners.OnceExit, root)
|
419 | }
|
420 | }
|
421 | }
|
422 |
|
423 | return this.result
|
424 | }
|
425 |
|
426 | then(onFulfilled, onRejected) {
|
427 | if (process.env.NODE_ENV !== 'production') {
|
428 | if (!('from' in this.opts)) {
|
429 | warnOnce(
|
430 | 'Without `from` option PostCSS could generate wrong source map ' +
|
431 | 'and will not find Browserslist config. Set it to CSS file path ' +
|
432 | 'or to `undefined` to prevent this warning.'
|
433 | )
|
434 | }
|
435 | }
|
436 | return this.async().then(onFulfilled, onRejected)
|
437 | }
|
438 |
|
439 | toString() {
|
440 | return this.css
|
441 | }
|
442 |
|
443 | visitSync(visitors, node) {
|
444 | for (let [plugin, visitor] of visitors) {
|
445 | this.result.lastPlugin = plugin
|
446 | let promise
|
447 | try {
|
448 | promise = visitor(node, this.helpers)
|
449 | } catch (e) {
|
450 | throw this.handleError(e, node.proxyOf)
|
451 | }
|
452 | if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
|
453 | return true
|
454 | }
|
455 | if (isPromise(promise)) {
|
456 | throw this.getAsyncError()
|
457 | }
|
458 | }
|
459 | }
|
460 |
|
461 | visitTick(stack) {
|
462 | let visit = stack[stack.length - 1]
|
463 | let { node, visitors } = visit
|
464 |
|
465 | if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
|
466 | stack.pop()
|
467 | return
|
468 | }
|
469 |
|
470 | if (visitors.length > 0 && visit.visitorIndex < visitors.length) {
|
471 | let [plugin, visitor] = visitors[visit.visitorIndex]
|
472 | visit.visitorIndex += 1
|
473 | if (visit.visitorIndex === visitors.length) {
|
474 | visit.visitors = []
|
475 | visit.visitorIndex = 0
|
476 | }
|
477 | this.result.lastPlugin = plugin
|
478 | try {
|
479 | return visitor(node.toProxy(), this.helpers)
|
480 | } catch (e) {
|
481 | throw this.handleError(e, node)
|
482 | }
|
483 | }
|
484 |
|
485 | if (visit.iterator !== 0) {
|
486 | let iterator = visit.iterator
|
487 | let child
|
488 | while ((child = node.nodes[node.indexes[iterator]])) {
|
489 | node.indexes[iterator] += 1
|
490 | if (!child[isClean]) {
|
491 | child[isClean] = true
|
492 | stack.push(toStack(child))
|
493 | return
|
494 | }
|
495 | }
|
496 | visit.iterator = 0
|
497 | delete node.indexes[iterator]
|
498 | }
|
499 |
|
500 | let events = visit.events
|
501 | while (visit.eventIndex < events.length) {
|
502 | let event = events[visit.eventIndex]
|
503 | visit.eventIndex += 1
|
504 | if (event === CHILDREN) {
|
505 | if (node.nodes && node.nodes.length) {
|
506 | node[isClean] = true
|
507 | visit.iterator = node.getIterator()
|
508 | }
|
509 | return
|
510 | } else if (this.listeners[event]) {
|
511 | visit.visitors = this.listeners[event]
|
512 | return
|
513 | }
|
514 | }
|
515 | stack.pop()
|
516 | }
|
517 |
|
518 | walkSync(node) {
|
519 | node[isClean] = true
|
520 | let events = getEvents(node)
|
521 | for (let event of events) {
|
522 | if (event === CHILDREN) {
|
523 | if (node.nodes) {
|
524 | node.each(child => {
|
525 | if (!child[isClean]) this.walkSync(child)
|
526 | })
|
527 | }
|
528 | } else {
|
529 | let visitors = this.listeners[event]
|
530 | if (visitors) {
|
531 | if (this.visitSync(visitors, node.toProxy())) return
|
532 | }
|
533 | }
|
534 | }
|
535 | }
|
536 |
|
537 | warnings() {
|
538 | return this.sync().warnings()
|
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)
|