lazy-result.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. 'use strict'
  2. let Container = require('./container')
  3. let Document = require('./document')
  4. let MapGenerator = require('./map-generator')
  5. let parse = require('./parse')
  6. let Result = require('./result')
  7. let Root = require('./root')
  8. let stringify = require('./stringify')
  9. let { isClean, my } = require('./symbols')
  10. let warnOnce = require('./warn-once')
  11. const TYPE_TO_CLASS_NAME = {
  12. atrule: 'AtRule',
  13. comment: 'Comment',
  14. decl: 'Declaration',
  15. document: 'Document',
  16. root: 'Root',
  17. rule: 'Rule'
  18. }
  19. const PLUGIN_PROPS = {
  20. AtRule: true,
  21. AtRuleExit: true,
  22. Comment: true,
  23. CommentExit: true,
  24. Declaration: true,
  25. DeclarationExit: true,
  26. Document: true,
  27. DocumentExit: true,
  28. Once: true,
  29. OnceExit: true,
  30. postcssPlugin: true,
  31. prepare: true,
  32. Root: true,
  33. RootExit: true,
  34. Rule: true,
  35. RuleExit: true
  36. }
  37. const NOT_VISITORS = {
  38. Once: true,
  39. postcssPlugin: true,
  40. prepare: true
  41. }
  42. const CHILDREN = 0
  43. function isPromise(obj) {
  44. return typeof obj === 'object' && typeof obj.then === 'function'
  45. }
  46. function getEvents(node) {
  47. let key = false
  48. let type = TYPE_TO_CLASS_NAME[node.type]
  49. if (node.type === 'decl') {
  50. key = node.prop.toLowerCase()
  51. } else if (node.type === 'atrule') {
  52. key = node.name.toLowerCase()
  53. }
  54. if (key && node.append) {
  55. return [
  56. type,
  57. type + '-' + key,
  58. CHILDREN,
  59. type + 'Exit',
  60. type + 'Exit-' + key
  61. ]
  62. } else if (key) {
  63. return [type, type + '-' + key, type + 'Exit', type + 'Exit-' + key]
  64. } else if (node.append) {
  65. return [type, CHILDREN, type + 'Exit']
  66. } else {
  67. return [type, type + 'Exit']
  68. }
  69. }
  70. function toStack(node) {
  71. let events
  72. if (node.type === 'document') {
  73. events = ['Document', CHILDREN, 'DocumentExit']
  74. } else if (node.type === 'root') {
  75. events = ['Root', CHILDREN, 'RootExit']
  76. } else {
  77. events = getEvents(node)
  78. }
  79. return {
  80. eventIndex: 0,
  81. events,
  82. iterator: 0,
  83. node,
  84. visitorIndex: 0,
  85. visitors: []
  86. }
  87. }
  88. function cleanMarks(node) {
  89. let stack = [node]
  90. while (stack.length > 0) {
  91. let next = stack.pop()
  92. next[isClean] = false
  93. if (next.nodes) {
  94. for (let i of next.nodes) stack.push(i)
  95. }
  96. }
  97. return node
  98. }
  99. let postcss = {}
  100. class LazyResult {
  101. get content() {
  102. return this.stringify().content
  103. }
  104. get css() {
  105. return this.stringify().css
  106. }
  107. get map() {
  108. return this.stringify().map
  109. }
  110. get messages() {
  111. return this.sync().messages
  112. }
  113. get opts() {
  114. return this.result.opts
  115. }
  116. get processor() {
  117. return this.result.processor
  118. }
  119. get root() {
  120. return this.sync().root
  121. }
  122. get [Symbol.toStringTag]() {
  123. return 'LazyResult'
  124. }
  125. constructor(processor, css, opts) {
  126. this.stringified = false
  127. this.processed = false
  128. let root
  129. if (
  130. typeof css === 'object' &&
  131. css !== null &&
  132. (css.type === 'root' || css.type === 'document')
  133. ) {
  134. root = cleanMarks(css)
  135. } else if (css instanceof LazyResult || css instanceof Result) {
  136. root = cleanMarks(css.root)
  137. if (css.map) {
  138. if (typeof opts.map === 'undefined') opts.map = {}
  139. if (!opts.map.inline) opts.map.inline = false
  140. opts.map.prev = css.map
  141. }
  142. } else {
  143. let parser = parse
  144. if (opts.syntax) parser = opts.syntax.parse
  145. if (opts.parser) parser = opts.parser
  146. if (parser.parse) parser = parser.parse
  147. try {
  148. root = parser(css, opts)
  149. } catch (error) {
  150. this.processed = true
  151. this.error = error
  152. }
  153. if (root && !root[my]) {
  154. /* c8 ignore next 2 */
  155. Container.rebuild(root)
  156. }
  157. }
  158. this.result = new Result(processor, root, opts)
  159. this.helpers = { ...postcss, postcss, result: this.result }
  160. this.plugins = this.processor.plugins.map(plugin => {
  161. if (typeof plugin === 'object' && plugin.prepare) {
  162. return { ...plugin, ...plugin.prepare(this.result) }
  163. } else {
  164. return plugin
  165. }
  166. })
  167. }
  168. async() {
  169. if (this.error) return Promise.reject(this.error)
  170. if (this.processed) return Promise.resolve(this.result)
  171. if (!this.processing) {
  172. this.processing = this.runAsync()
  173. }
  174. return this.processing
  175. }
  176. catch(onRejected) {
  177. return this.async().catch(onRejected)
  178. }
  179. finally(onFinally) {
  180. return this.async().then(onFinally, onFinally)
  181. }
  182. getAsyncError() {
  183. throw new Error('Use process(css).then(cb) to work with async plugins')
  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. if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) {
  201. // eslint-disable-next-line no-console
  202. console.error(
  203. 'Unknown error from PostCSS plugin. Your current PostCSS ' +
  204. 'version is ' +
  205. runtimeVer +
  206. ', but ' +
  207. pluginName +
  208. ' uses ' +
  209. pluginVer +
  210. '. Perhaps this is the source of the error below.'
  211. )
  212. }
  213. }
  214. }
  215. } catch (err) {
  216. /* c8 ignore next 3 */
  217. // eslint-disable-next-line no-console
  218. if (console && console.error) console.error(err)
  219. }
  220. return error
  221. }
  222. prepareVisitors() {
  223. this.listeners = {}
  224. let add = (plugin, type, cb) => {
  225. if (!this.listeners[type]) this.listeners[type] = []
  226. this.listeners[type].push([plugin, cb])
  227. }
  228. for (let plugin of this.plugins) {
  229. if (typeof plugin === 'object') {
  230. for (let event in plugin) {
  231. if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) {
  232. throw new Error(
  233. `Unknown event ${event} in ${plugin.postcssPlugin}. ` +
  234. `Try to update PostCSS (${this.processor.version} now).`
  235. )
  236. }
  237. if (!NOT_VISITORS[event]) {
  238. if (typeof plugin[event] === 'object') {
  239. for (let filter in plugin[event]) {
  240. if (filter === '*') {
  241. add(plugin, event, plugin[event][filter])
  242. } else {
  243. add(
  244. plugin,
  245. event + '-' + filter.toLowerCase(),
  246. plugin[event][filter]
  247. )
  248. }
  249. }
  250. } else if (typeof plugin[event] === 'function') {
  251. add(plugin, event, plugin[event])
  252. }
  253. }
  254. }
  255. }
  256. }
  257. this.hasListener = Object.keys(this.listeners).length > 0
  258. }
  259. async runAsync() {
  260. this.plugin = 0
  261. for (let i = 0; i < this.plugins.length; i++) {
  262. let plugin = this.plugins[i]
  263. let promise = this.runOnRoot(plugin)
  264. if (isPromise(promise)) {
  265. try {
  266. await promise
  267. } catch (error) {
  268. throw this.handleError(error)
  269. }
  270. }
  271. }
  272. this.prepareVisitors()
  273. if (this.hasListener) {
  274. let root = this.result.root
  275. while (!root[isClean]) {
  276. root[isClean] = true
  277. let stack = [toStack(root)]
  278. while (stack.length > 0) {
  279. let promise = this.visitTick(stack)
  280. if (isPromise(promise)) {
  281. try {
  282. await promise
  283. } catch (e) {
  284. let node = stack[stack.length - 1].node
  285. throw this.handleError(e, node)
  286. }
  287. }
  288. }
  289. }
  290. if (this.listeners.OnceExit) {
  291. for (let [plugin, visitor] of this.listeners.OnceExit) {
  292. this.result.lastPlugin = plugin
  293. try {
  294. if (root.type === 'document') {
  295. let roots = root.nodes.map(subRoot =>
  296. visitor(subRoot, this.helpers)
  297. )
  298. await Promise.all(roots)
  299. } else {
  300. await visitor(root, this.helpers)
  301. }
  302. } catch (e) {
  303. throw this.handleError(e)
  304. }
  305. }
  306. }
  307. }
  308. this.processed = true
  309. return this.stringify()
  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. if (isPromise(roots[0])) {
  320. return Promise.all(roots)
  321. }
  322. return roots
  323. }
  324. return plugin.Once(this.result.root, this.helpers)
  325. } else if (typeof plugin === 'function') {
  326. return plugin(this.result.root, this.result)
  327. }
  328. } catch (error) {
  329. throw this.handleError(error)
  330. }
  331. }
  332. stringify() {
  333. if (this.error) throw this.error
  334. if (this.stringified) return this.result
  335. this.stringified = true
  336. this.sync()
  337. let opts = this.result.opts
  338. let str = stringify
  339. if (opts.syntax) str = opts.syntax.stringify
  340. if (opts.stringifier) str = opts.stringifier
  341. if (str.stringify) str = str.stringify
  342. let rootSource = this.result.root.source
  343. if (
  344. opts.map === undefined &&
  345. !(rootSource && rootSource.input && rootSource.input.map)
  346. ) {
  347. let result = ''
  348. str(this.result.root, i => {
  349. result += i
  350. })
  351. this.result.css = result
  352. return this.result
  353. }
  354. let map = new MapGenerator(str, this.result.root, this.result.opts)
  355. let data = map.generate()
  356. this.result.css = data[0]
  357. this.result.map = data[1]
  358. return this.result
  359. }
  360. sync() {
  361. if (this.error) throw this.error
  362. if (this.processed) return this.result
  363. this.processed = true
  364. if (this.processing) {
  365. throw this.getAsyncError()
  366. }
  367. for (let plugin of this.plugins) {
  368. let promise = this.runOnRoot(plugin)
  369. if (isPromise(promise)) {
  370. throw this.getAsyncError()
  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. return this.result
  391. }
  392. then(onFulfilled, onRejected) {
  393. if (process.env.NODE_ENV !== 'production') {
  394. if (!('from' in this.opts)) {
  395. warnOnce(
  396. 'Without `from` option PostCSS could generate wrong source map ' +
  397. 'and will not find Browserslist config. Set it to CSS file path ' +
  398. 'or to `undefined` to prevent this warning.'
  399. )
  400. }
  401. }
  402. return this.async().then(onFulfilled, onRejected)
  403. }
  404. toString() {
  405. return this.css
  406. }
  407. visitSync(visitors, node) {
  408. for (let [plugin, visitor] of visitors) {
  409. this.result.lastPlugin = plugin
  410. let promise
  411. try {
  412. promise = visitor(node, this.helpers)
  413. } catch (e) {
  414. throw this.handleError(e, node.proxyOf)
  415. }
  416. if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
  417. return true
  418. }
  419. if (isPromise(promise)) {
  420. throw this.getAsyncError()
  421. }
  422. }
  423. }
  424. visitTick(stack) {
  425. let visit = stack[stack.length - 1]
  426. let { node, visitors } = visit
  427. if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
  428. stack.pop()
  429. return
  430. }
  431. if (visitors.length > 0 && visit.visitorIndex < visitors.length) {
  432. let [plugin, visitor] = visitors[visit.visitorIndex]
  433. visit.visitorIndex += 1
  434. if (visit.visitorIndex === visitors.length) {
  435. visit.visitors = []
  436. visit.visitorIndex = 0
  437. }
  438. this.result.lastPlugin = plugin
  439. try {
  440. return visitor(node.toProxy(), this.helpers)
  441. } catch (e) {
  442. throw this.handleError(e, node)
  443. }
  444. }
  445. if (visit.iterator !== 0) {
  446. let iterator = visit.iterator
  447. // Advance past the child we just finished visiting. Like
  448. // `Container#each`, the index is incremented only after a child has
  449. // been fully processed, so a node inserted right after the current
  450. // child is not skipped by the `existIndex < index` adjustment in
  451. // `Container#insertAfter()` (which would fire exit events too early).
  452. if (visit.descending) {
  453. visit.descending = false
  454. node.indexes[iterator] += 1
  455. }
  456. let child
  457. while ((child = node.nodes[node.indexes[iterator]])) {
  458. if (!child[isClean]) {
  459. child[isClean] = true
  460. visit.descending = true
  461. stack.push(toStack(child))
  462. return
  463. }
  464. node.indexes[iterator] += 1
  465. }
  466. visit.iterator = 0
  467. delete node.indexes[iterator]
  468. }
  469. let events = visit.events
  470. while (visit.eventIndex < events.length) {
  471. let event = events[visit.eventIndex]
  472. visit.eventIndex += 1
  473. if (event === CHILDREN) {
  474. if (node.nodes && node.nodes.length) {
  475. node[isClean] = true
  476. visit.iterator = node.getIterator()
  477. }
  478. return
  479. } else if (this.listeners[event]) {
  480. visit.visitors = this.listeners[event]
  481. return
  482. }
  483. }
  484. stack.pop()
  485. }
  486. walkSync(node) {
  487. // An explicit stack like in async `visitTick()` to survive deeply
  488. // nested trees. Unlike `visitTick()`, nodes are marked clean only
  489. // on entering, so a node dirtied by its own visitors is revisited
  490. // on the next pass.
  491. node[isClean] = true
  492. let stack = [{ eventIndex: 0, events: getEvents(node), iterator: 0, node }]
  493. while (stack.length > 0) {
  494. let visit = stack[stack.length - 1]
  495. let visitNode = visit.node
  496. if (visit.iterator !== 0) {
  497. let iterator = visit.iterator
  498. // Advance past the child we just finished visiting. Like
  499. // `Container#each`, the index is incremented only after a child has
  500. // been fully processed. Incrementing before (as this loop used to)
  501. // makes a node inserted right after the current child get skipped by
  502. // the `existIndex < index` adjustment in `Container#insertAfter()`,
  503. // which fires exit events before those new siblings are visited.
  504. if (visit.descending) {
  505. visit.descending = false
  506. visitNode.indexes[iterator] += 1
  507. }
  508. let child
  509. let descended = false
  510. while ((child = visitNode.nodes[visitNode.indexes[iterator]])) {
  511. if (!child[isClean]) {
  512. child[isClean] = true
  513. visit.descending = true
  514. stack.push({
  515. eventIndex: 0,
  516. events: getEvents(child),
  517. iterator: 0,
  518. node: child
  519. })
  520. descended = true
  521. break
  522. }
  523. visitNode.indexes[iterator] += 1
  524. }
  525. if (descended) continue
  526. visit.iterator = 0
  527. delete visitNode.indexes[iterator]
  528. }
  529. if (visit.eventIndex < visit.events.length) {
  530. let event = visit.events[visit.eventIndex]
  531. visit.eventIndex += 1
  532. if (event === CHILDREN) {
  533. if (visitNode.nodes && visitNode.nodes.length) {
  534. visit.iterator = visitNode.getIterator()
  535. }
  536. } else {
  537. let visitors = this.listeners[event]
  538. if (visitors) {
  539. if (this.visitSync(visitors, visitNode.toProxy())) stack.pop()
  540. }
  541. }
  542. continue
  543. }
  544. stack.pop()
  545. }
  546. }
  547. warnings() {
  548. return this.sync().warnings()
  549. }
  550. }
  551. LazyResult.registerPostcss = dependant => {
  552. postcss = dependant
  553. }
  554. module.exports = LazyResult
  555. LazyResult.default = LazyResult
  556. Root.registerLazyResult(LazyResult)
  557. Document.registerLazyResult(LazyResult)