UNPKG

13.6 kBJavaScriptView Raw
1'use strict'
2
3let { isClean, my } = require('./symbols')
4let MapGenerator = require('./map-generator')
5let stringify = require('./stringify')
6let Container = require('./container')
7let Document = require('./document')
8let warnOnce = require('./warn-once')
9let Result = require('./result')
10let parse = require('./parse')
11let Root = require('./root')
12
13const TYPE_TO_CLASS_NAME = {
14 document: 'Document',
15 root: 'Root',
16 atrule: 'AtRule',
17 rule: 'Rule',
18 decl: 'Declaration',
19 comment: 'Comment'
20}
21
22const 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
41const NOT_VISITORS = {
42 postcssPlugin: true,
43 prepare: true,
44 Once: true
45}
46
47const CHILDREN = 0
48
49function isPromise(obj) {
50 return typeof obj === 'object' && typeof obj.then === 'function'
51}
52
53function 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
79function 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
99function cleanMarks(node) {
100 node[isClean] = false
101 if (node.nodes) node.nodes.forEach(i => cleanMarks(i))
102 return node
103}
104
105let postcss = {}
106
107class 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 /* c8 ignore next 2 */
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 // eslint-disable-next-line no-console
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 /* c8 ignore next 3 */
383 // eslint-disable-next-line no-console
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
542LazyResult.registerPostcss = dependant => {
543 postcss = dependant
544}
545
546module.exports = LazyResult
547LazyResult.default = LazyResult
548
549Root.registerLazyResult(LazyResult)
550Document.registerLazyResult(LazyResult)