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