You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			551 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			551 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											9 months ago
										 | 'use strict' | ||
|  | 
 | ||
|  | let { isClean, my } = require('./symbols') | ||
|  | let MapGenerator = require('./map-generator') | ||
|  | let stringify = require('./stringify') | ||
|  | let Container = require('./container') | ||
|  | let Document = require('./document') | ||
|  | let warnOnce = require('./warn-once') | ||
|  | let Result = require('./result') | ||
|  | let parse = require('./parse') | ||
|  | let Root = require('./root') | ||
|  | 
 | ||
|  | const TYPE_TO_CLASS_NAME = { | ||
|  |   atrule: 'AtRule', | ||
|  |   comment: 'Comment', | ||
|  |   decl: 'Declaration', | ||
|  |   document: 'Document', | ||
|  |   root: 'Root', | ||
|  |   rule: 'Rule' | ||
|  | } | ||
|  | 
 | ||
|  | const PLUGIN_PROPS = { | ||
|  |   AtRule: true, | ||
|  |   AtRuleExit: true, | ||
|  |   Comment: true, | ||
|  |   CommentExit: true, | ||
|  |   Declaration: true, | ||
|  |   DeclarationExit: true, | ||
|  |   Document: true, | ||
|  |   DocumentExit: true, | ||
|  |   Once: true, | ||
|  |   OnceExit: true, | ||
|  |   postcssPlugin: true, | ||
|  |   prepare: true, | ||
|  |   Root: true, | ||
|  |   RootExit: true, | ||
|  |   Rule: true, | ||
|  |   RuleExit: true | ||
|  | } | ||
|  | 
 | ||
|  | const NOT_VISITORS = { | ||
|  |   Once: true, | ||
|  |   postcssPlugin: true, | ||
|  |   prepare: true | ||
|  | } | ||
|  | 
 | ||
|  | const CHILDREN = 0 | ||
|  | 
 | ||
|  | function isPromise(obj) { | ||
|  |   return typeof obj === 'object' && typeof obj.then === 'function' | ||
|  | } | ||
|  | 
 | ||
|  | function getEvents(node) { | ||
|  |   let key = false | ||
|  |   let type = TYPE_TO_CLASS_NAME[node.type] | ||
|  |   if (node.type === 'decl') { | ||
|  |     key = node.prop.toLowerCase() | ||
|  |   } else if (node.type === 'atrule') { | ||
|  |     key = node.name.toLowerCase() | ||
|  |   } | ||
|  | 
 | ||
|  |   if (key && node.append) { | ||
|  |     return [ | ||
|  |       type, | ||
|  |       type + '-' + key, | ||
|  |       CHILDREN, | ||
|  |       type + 'Exit', | ||
|  |       type + 'Exit-' + key | ||
|  |     ] | ||
|  |   } else if (key) { | ||
|  |     return [type, type + '-' + key, type + 'Exit', type + 'Exit-' + key] | ||
|  |   } else if (node.append) { | ||
|  |     return [type, CHILDREN, type + 'Exit'] | ||
|  |   } else { | ||
|  |     return [type, type + 'Exit'] | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function toStack(node) { | ||
|  |   let events | ||
|  |   if (node.type === 'document') { | ||
|  |     events = ['Document', CHILDREN, 'DocumentExit'] | ||
|  |   } else if (node.type === 'root') { | ||
|  |     events = ['Root', CHILDREN, 'RootExit'] | ||
|  |   } else { | ||
|  |     events = getEvents(node) | ||
|  |   } | ||
|  | 
 | ||
|  |   return { | ||
|  |     eventIndex: 0, | ||
|  |     events, | ||
|  |     iterator: 0, | ||
|  |     node, | ||
|  |     visitorIndex: 0, | ||
|  |     visitors: [] | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function cleanMarks(node) { | ||
|  |   node[isClean] = false | ||
|  |   if (node.nodes) node.nodes.forEach(i => cleanMarks(i)) | ||
|  |   return node | ||
|  | } | ||
|  | 
 | ||
|  | let postcss = {} | ||
|  | 
 | ||
|  | class LazyResult { | ||
|  |   constructor(processor, css, opts) { | ||
|  |     this.stringified = false | ||
|  |     this.processed = false | ||
|  | 
 | ||
|  |     let root | ||
|  |     if ( | ||
|  |       typeof css === 'object' && | ||
|  |       css !== null && | ||
|  |       (css.type === 'root' || css.type === 'document') | ||
|  |     ) { | ||
|  |       root = cleanMarks(css) | ||
|  |     } else if (css instanceof LazyResult || css instanceof Result) { | ||
|  |       root = cleanMarks(css.root) | ||
|  |       if (css.map) { | ||
|  |         if (typeof opts.map === 'undefined') opts.map = {} | ||
|  |         if (!opts.map.inline) opts.map.inline = false | ||
|  |         opts.map.prev = css.map | ||
|  |       } | ||
|  |     } else { | ||
|  |       let parser = parse | ||
|  |       if (opts.syntax) parser = opts.syntax.parse | ||
|  |       if (opts.parser) parser = opts.parser | ||
|  |       if (parser.parse) parser = parser.parse | ||
|  | 
 | ||
|  |       try { | ||
|  |         root = parser(css, opts) | ||
|  |       } catch (error) { | ||
|  |         this.processed = true | ||
|  |         this.error = error | ||
|  |       } | ||
|  | 
 | ||
|  |       if (root && !root[my]) { | ||
|  |         /* c8 ignore next 2 */ | ||
|  |         Container.rebuild(root) | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this.result = new Result(processor, root, opts) | ||
|  |     this.helpers = { ...postcss, postcss, result: this.result } | ||
|  |     this.plugins = this.processor.plugins.map(plugin => { | ||
|  |       if (typeof plugin === 'object' && plugin.prepare) { | ||
|  |         return { ...plugin, ...plugin.prepare(this.result) } | ||
|  |       } else { | ||
|  |         return plugin | ||
|  |       } | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   async() { | ||
|  |     if (this.error) return Promise.reject(this.error) | ||
|  |     if (this.processed) return Promise.resolve(this.result) | ||
|  |     if (!this.processing) { | ||
|  |       this.processing = this.runAsync() | ||
|  |     } | ||
|  |     return this.processing | ||
|  |   } | ||
|  | 
 | ||
|  |   catch(onRejected) { | ||
|  |     return this.async().catch(onRejected) | ||
|  |   } | ||
|  | 
 | ||
|  |   finally(onFinally) { | ||
|  |     return this.async().then(onFinally, onFinally) | ||
|  |   } | ||
|  | 
 | ||
|  |   getAsyncError() { | ||
|  |     throw new Error('Use process(css).then(cb) to work with async plugins') | ||
|  |   } | ||
|  | 
 | ||
|  |   handleError(error, node) { | ||
|  |     let plugin = this.result.lastPlugin | ||
|  |     try { | ||
|  |       if (node) node.addToError(error) | ||
|  |       this.error = error | ||
|  |       if (error.name === 'CssSyntaxError' && !error.plugin) { | ||
|  |         error.plugin = plugin.postcssPlugin | ||
|  |         error.setMessage() | ||
|  |       } else if (plugin.postcssVersion) { | ||
|  |         if (process.env.NODE_ENV !== 'production') { | ||
|  |           let pluginName = plugin.postcssPlugin | ||
|  |           let pluginVer = plugin.postcssVersion | ||
|  |           let runtimeVer = this.result.processor.version | ||
|  |           let a = pluginVer.split('.') | ||
|  |           let b = runtimeVer.split('.') | ||
|  | 
 | ||
|  |           if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) { | ||
|  |             // eslint-disable-next-line no-console
 | ||
|  |             console.error( | ||
|  |               'Unknown error from PostCSS plugin. Your current PostCSS ' + | ||
|  |                 'version is ' + | ||
|  |                 runtimeVer + | ||
|  |                 ', but ' + | ||
|  |                 pluginName + | ||
|  |                 ' uses ' + | ||
|  |                 pluginVer + | ||
|  |                 '. Perhaps this is the source of the error below.' | ||
|  |             ) | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } catch (err) { | ||
|  |       /* c8 ignore next 3 */ | ||
|  |       // eslint-disable-next-line no-console
 | ||
|  |       if (console && console.error) console.error(err) | ||
|  |     } | ||
|  |     return error | ||
|  |   } | ||
|  | 
 | ||
|  |   prepareVisitors() { | ||
|  |     this.listeners = {} | ||
|  |     let add = (plugin, type, cb) => { | ||
|  |       if (!this.listeners[type]) this.listeners[type] = [] | ||
|  |       this.listeners[type].push([plugin, cb]) | ||
|  |     } | ||
|  |     for (let plugin of this.plugins) { | ||
|  |       if (typeof plugin === 'object') { | ||
|  |         for (let event in plugin) { | ||
|  |           if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) { | ||
|  |             throw new Error( | ||
|  |               `Unknown event ${event} in ${plugin.postcssPlugin}. ` + | ||
|  |                 `Try to update PostCSS (${this.processor.version} now).` | ||
|  |             ) | ||
|  |           } | ||
|  |           if (!NOT_VISITORS[event]) { | ||
|  |             if (typeof plugin[event] === 'object') { | ||
|  |               for (let filter in plugin[event]) { | ||
|  |                 if (filter === '*') { | ||
|  |                   add(plugin, event, plugin[event][filter]) | ||
|  |                 } else { | ||
|  |                   add( | ||
|  |                     plugin, | ||
|  |                     event + '-' + filter.toLowerCase(), | ||
|  |                     plugin[event][filter] | ||
|  |                   ) | ||
|  |                 } | ||
|  |               } | ||
|  |             } else if (typeof plugin[event] === 'function') { | ||
|  |               add(plugin, event, plugin[event]) | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |     this.hasListener = Object.keys(this.listeners).length > 0 | ||
|  |   } | ||
|  | 
 | ||
|  |   async runAsync() { | ||
|  |     this.plugin = 0 | ||
|  |     for (let i = 0; i < this.plugins.length; i++) { | ||
|  |       let plugin = this.plugins[i] | ||
|  |       let promise = this.runOnRoot(plugin) | ||
|  |       if (isPromise(promise)) { | ||
|  |         try { | ||
|  |           await promise | ||
|  |         } catch (error) { | ||
|  |           throw this.handleError(error) | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this.prepareVisitors() | ||
|  |     if (this.hasListener) { | ||
|  |       let root = this.result.root | ||
|  |       while (!root[isClean]) { | ||
|  |         root[isClean] = true | ||
|  |         let stack = [toStack(root)] | ||
|  |         while (stack.length > 0) { | ||
|  |           let promise = this.visitTick(stack) | ||
|  |           if (isPromise(promise)) { | ||
|  |             try { | ||
|  |               await promise | ||
|  |             } catch (e) { | ||
|  |               let node = stack[stack.length - 1].node | ||
|  |               throw this.handleError(e, node) | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       if (this.listeners.OnceExit) { | ||
|  |         for (let [plugin, visitor] of this.listeners.OnceExit) { | ||
|  |           this.result.lastPlugin = plugin | ||
|  |           try { | ||
|  |             if (root.type === 'document') { | ||
|  |               let roots = root.nodes.map(subRoot => | ||
|  |                 visitor(subRoot, this.helpers) | ||
|  |               ) | ||
|  | 
 | ||
|  |               await Promise.all(roots) | ||
|  |             } else { | ||
|  |               await visitor(root, this.helpers) | ||
|  |             } | ||
|  |           } catch (e) { | ||
|  |             throw this.handleError(e) | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this.processed = true | ||
|  |     return this.stringify() | ||
|  |   } | ||
|  | 
 | ||
|  |   runOnRoot(plugin) { | ||
|  |     this.result.lastPlugin = plugin | ||
|  |     try { | ||
|  |       if (typeof plugin === 'object' && plugin.Once) { | ||
|  |         if (this.result.root.type === 'document') { | ||
|  |           let roots = this.result.root.nodes.map(root => | ||
|  |             plugin.Once(root, this.helpers) | ||
|  |           ) | ||
|  | 
 | ||
|  |           if (isPromise(roots[0])) { | ||
|  |             return Promise.all(roots) | ||
|  |           } | ||
|  | 
 | ||
|  |           return roots | ||
|  |         } | ||
|  | 
 | ||
|  |         return plugin.Once(this.result.root, this.helpers) | ||
|  |       } else if (typeof plugin === 'function') { | ||
|  |         return plugin(this.result.root, this.result) | ||
|  |       } | ||
|  |     } catch (error) { | ||
|  |       throw this.handleError(error) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   stringify() { | ||
|  |     if (this.error) throw this.error | ||
|  |     if (this.stringified) return this.result | ||
|  |     this.stringified = true | ||
|  | 
 | ||
|  |     this.sync() | ||
|  | 
 | ||
|  |     let opts = this.result.opts | ||
|  |     let str = stringify | ||
|  |     if (opts.syntax) str = opts.syntax.stringify | ||
|  |     if (opts.stringifier) str = opts.stringifier | ||
|  |     if (str.stringify) str = str.stringify | ||
|  | 
 | ||
|  |     let map = new MapGenerator(str, this.result.root, this.result.opts) | ||
|  |     let data = map.generate() | ||
|  |     this.result.css = data[0] | ||
|  |     this.result.map = data[1] | ||
|  | 
 | ||
|  |     return this.result | ||
|  |   } | ||
|  | 
 | ||
|  |   sync() { | ||
|  |     if (this.error) throw this.error | ||
|  |     if (this.processed) return this.result | ||
|  |     this.processed = true | ||
|  | 
 | ||
|  |     if (this.processing) { | ||
|  |       throw this.getAsyncError() | ||
|  |     } | ||
|  | 
 | ||
|  |     for (let plugin of this.plugins) { | ||
|  |       let promise = this.runOnRoot(plugin) | ||
|  |       if (isPromise(promise)) { | ||
|  |         throw this.getAsyncError() | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this.prepareVisitors() | ||
|  |     if (this.hasListener) { | ||
|  |       let root = this.result.root | ||
|  |       while (!root[isClean]) { | ||
|  |         root[isClean] = true | ||
|  |         this.walkSync(root) | ||
|  |       } | ||
|  |       if (this.listeners.OnceExit) { | ||
|  |         if (root.type === 'document') { | ||
|  |           for (let subRoot of root.nodes) { | ||
|  |             this.visitSync(this.listeners.OnceExit, subRoot) | ||
|  |           } | ||
|  |         } else { | ||
|  |           this.visitSync(this.listeners.OnceExit, root) | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     return this.result | ||
|  |   } | ||
|  | 
 | ||
|  |   then(onFulfilled, onRejected) { | ||
|  |     if (process.env.NODE_ENV !== 'production') { | ||
|  |       if (!('from' in this.opts)) { | ||
|  |         warnOnce( | ||
|  |           'Without `from` option PostCSS could generate wrong source map ' + | ||
|  |             'and will not find Browserslist config. Set it to CSS file path ' + | ||
|  |             'or to `undefined` to prevent this warning.' | ||
|  |         ) | ||
|  |       } | ||
|  |     } | ||
|  |     return this.async().then(onFulfilled, onRejected) | ||
|  |   } | ||
|  | 
 | ||
|  |   toString() { | ||
|  |     return this.css | ||
|  |   } | ||
|  | 
 | ||
|  |   visitSync(visitors, node) { | ||
|  |     for (let [plugin, visitor] of visitors) { | ||
|  |       this.result.lastPlugin = plugin | ||
|  |       let promise | ||
|  |       try { | ||
|  |         promise = visitor(node, this.helpers) | ||
|  |       } catch (e) { | ||
|  |         throw this.handleError(e, node.proxyOf) | ||
|  |       } | ||
|  |       if (node.type !== 'root' && node.type !== 'document' && !node.parent) { | ||
|  |         return true | ||
|  |       } | ||
|  |       if (isPromise(promise)) { | ||
|  |         throw this.getAsyncError() | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   visitTick(stack) { | ||
|  |     let visit = stack[stack.length - 1] | ||
|  |     let { node, visitors } = visit | ||
|  | 
 | ||
|  |     if (node.type !== 'root' && node.type !== 'document' && !node.parent) { | ||
|  |       stack.pop() | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     if (visitors.length > 0 && visit.visitorIndex < visitors.length) { | ||
|  |       let [plugin, visitor] = visitors[visit.visitorIndex] | ||
|  |       visit.visitorIndex += 1 | ||
|  |       if (visit.visitorIndex === visitors.length) { | ||
|  |         visit.visitors = [] | ||
|  |         visit.visitorIndex = 0 | ||
|  |       } | ||
|  |       this.result.lastPlugin = plugin | ||
|  |       try { | ||
|  |         return visitor(node.toProxy(), this.helpers) | ||
|  |       } catch (e) { | ||
|  |         throw this.handleError(e, node) | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (visit.iterator !== 0) { | ||
|  |       let iterator = visit.iterator | ||
|  |       let child | ||
|  |       while ((child = node.nodes[node.indexes[iterator]])) { | ||
|  |         node.indexes[iterator] += 1 | ||
|  |         if (!child[isClean]) { | ||
|  |           child[isClean] = true | ||
|  |           stack.push(toStack(child)) | ||
|  |           return | ||
|  |         } | ||
|  |       } | ||
|  |       visit.iterator = 0 | ||
|  |       delete node.indexes[iterator] | ||
|  |     } | ||
|  | 
 | ||
|  |     let events = visit.events | ||
|  |     while (visit.eventIndex < events.length) { | ||
|  |       let event = events[visit.eventIndex] | ||
|  |       visit.eventIndex += 1 | ||
|  |       if (event === CHILDREN) { | ||
|  |         if (node.nodes && node.nodes.length) { | ||
|  |           node[isClean] = true | ||
|  |           visit.iterator = node.getIterator() | ||
|  |         } | ||
|  |         return | ||
|  |       } else if (this.listeners[event]) { | ||
|  |         visit.visitors = this.listeners[event] | ||
|  |         return | ||
|  |       } | ||
|  |     } | ||
|  |     stack.pop() | ||
|  |   } | ||
|  | 
 | ||
|  |   walkSync(node) { | ||
|  |     node[isClean] = true | ||
|  |     let events = getEvents(node) | ||
|  |     for (let event of events) { | ||
|  |       if (event === CHILDREN) { | ||
|  |         if (node.nodes) { | ||
|  |           node.each(child => { | ||
|  |             if (!child[isClean]) this.walkSync(child) | ||
|  |           }) | ||
|  |         } | ||
|  |       } else { | ||
|  |         let visitors = this.listeners[event] | ||
|  |         if (visitors) { | ||
|  |           if (this.visitSync(visitors, node.toProxy())) return | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   warnings() { | ||
|  |     return this.sync().warnings() | ||
|  |   } | ||
|  | 
 | ||
|  |   get content() { | ||
|  |     return this.stringify().content | ||
|  |   } | ||
|  | 
 | ||
|  |   get css() { | ||
|  |     return this.stringify().css | ||
|  |   } | ||
|  | 
 | ||
|  |   get map() { | ||
|  |     return this.stringify().map | ||
|  |   } | ||
|  | 
 | ||
|  |   get messages() { | ||
|  |     return this.sync().messages | ||
|  |   } | ||
|  | 
 | ||
|  |   get opts() { | ||
|  |     return this.result.opts | ||
|  |   } | ||
|  | 
 | ||
|  |   get processor() { | ||
|  |     return this.result.processor | ||
|  |   } | ||
|  | 
 | ||
|  |   get root() { | ||
|  |     return this.sync().root | ||
|  |   } | ||
|  | 
 | ||
|  |   get [Symbol.toStringTag]() { | ||
|  |     return 'LazyResult' | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | LazyResult.registerPostcss = dependant => { | ||
|  |   postcss = dependant | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = LazyResult | ||
|  | LazyResult.default = LazyResult | ||
|  | 
 | ||
|  | Root.registerLazyResult(LazyResult) | ||
|  | Document.registerLazyResult(LazyResult) |