lazy-result.js 12 KB

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