lazy-result.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. 'use strict'
  2. let { isClean, my } = require('./symbols')
  3. let MapGenerator = require('./map-generator')
  4. let stringify = require('./stringify')
  5. let Container = require('./container')
  6. let Document = require('./document')
  7. let warnOnce = require('./warn-once')
  8. let Result = require('./result')
  9. let parse = require('./parse')
  10. let Root = require('./root')
  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. node[isClean] = false
  90. if (node.nodes) node.nodes.forEach(i => cleanMarks(i))
  91. return node
  92. }
  93. let postcss = {}
  94. class LazyResult {
  95. constructor(processor, css, opts) {
  96. this.stringified = false
  97. this.processed = false
  98. let root
  99. if (
  100. typeof css === 'object' &&
  101. css !== null &&
  102. (css.type === 'root' || css.type === 'document')
  103. ) {
  104. root = cleanMarks(css)
  105. } else if (css instanceof LazyResult || css instanceof Result) {
  106. root = cleanMarks(css.root)
  107. if (css.map) {
  108. if (typeof opts.map === 'undefined') opts.map = {}
  109. if (!opts.map.inline) opts.map.inline = false
  110. opts.map.prev = css.map
  111. }
  112. } else {
  113. let parser = parse
  114. if (opts.syntax) parser = opts.syntax.parse
  115. if (opts.parser) parser = opts.parser
  116. if (parser.parse) parser = parser.parse
  117. try {
  118. root = parser(css, opts)
  119. } catch (error) {
  120. this.processed = true
  121. this.error = error
  122. }
  123. if (root && !root[my]) {
  124. /* c8 ignore next 2 */
  125. Container.rebuild(root)
  126. }
  127. }
  128. this.result = new Result(processor, root, opts)
  129. this.helpers = { ...postcss, postcss, result: this.result }
  130. this.plugins = this.processor.plugins.map(plugin => {
  131. if (typeof plugin === 'object' && plugin.prepare) {
  132. return { ...plugin, ...plugin.prepare(this.result) }
  133. } else {
  134. return plugin
  135. }
  136. })
  137. }
  138. async() {
  139. if (this.error) return Promise.reject(this.error)
  140. if (this.processed) return Promise.resolve(this.result)
  141. if (!this.processing) {
  142. this.processing = this.runAsync()
  143. }
  144. return this.processing
  145. }
  146. catch(onRejected) {
  147. return this.async().catch(onRejected)
  148. }
  149. get content() {
  150. return this.stringify().content
  151. }
  152. get css() {
  153. return this.stringify().css
  154. }
  155. finally(onFinally) {
  156. return this.async().then(onFinally, onFinally)
  157. }
  158. getAsyncError() {
  159. throw new Error('Use process(css).then(cb) to work with async plugins')
  160. }
  161. handleError(error, node) {
  162. let plugin = this.result.lastPlugin
  163. try {
  164. if (node) node.addToError(error)
  165. this.error = error
  166. if (error.name === 'CssSyntaxError' && !error.plugin) {
  167. error.plugin = plugin.postcssPlugin
  168. error.setMessage()
  169. } else if (plugin.postcssVersion) {
  170. if (process.env.NODE_ENV !== 'production') {
  171. let pluginName = plugin.postcssPlugin
  172. let pluginVer = plugin.postcssVersion
  173. let runtimeVer = this.result.processor.version
  174. let a = pluginVer.split('.')
  175. let b = runtimeVer.split('.')
  176. if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) {
  177. // eslint-disable-next-line no-console
  178. console.error(
  179. 'Unknown error from PostCSS plugin. Your current PostCSS ' +
  180. 'version is ' +
  181. runtimeVer +
  182. ', but ' +
  183. pluginName +
  184. ' uses ' +
  185. pluginVer +
  186. '. Perhaps this is the source of the error below.'
  187. )
  188. }
  189. }
  190. }
  191. } catch (err) {
  192. /* c8 ignore next 3 */
  193. // eslint-disable-next-line no-console
  194. if (console && console.error) console.error(err)
  195. }
  196. return error
  197. }
  198. get map() {
  199. return this.stringify().map
  200. }
  201. get messages() {
  202. return this.sync().messages
  203. }
  204. get opts() {
  205. return this.result.opts
  206. }
  207. prepareVisitors() {
  208. this.listeners = {}
  209. let add = (plugin, type, cb) => {
  210. if (!this.listeners[type]) this.listeners[type] = []
  211. this.listeners[type].push([plugin, cb])
  212. }
  213. for (let plugin of this.plugins) {
  214. if (typeof plugin === 'object') {
  215. for (let event in plugin) {
  216. if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) {
  217. throw new Error(
  218. `Unknown event ${event} in ${plugin.postcssPlugin}. ` +
  219. `Try to update PostCSS (${this.processor.version} now).`
  220. )
  221. }
  222. if (!NOT_VISITORS[event]) {
  223. if (typeof plugin[event] === 'object') {
  224. for (let filter in plugin[event]) {
  225. if (filter === '*') {
  226. add(plugin, event, plugin[event][filter])
  227. } else {
  228. add(
  229. plugin,
  230. event + '-' + filter.toLowerCase(),
  231. plugin[event][filter]
  232. )
  233. }
  234. }
  235. } else if (typeof plugin[event] === 'function') {
  236. add(plugin, event, plugin[event])
  237. }
  238. }
  239. }
  240. }
  241. }
  242. this.hasListener = Object.keys(this.listeners).length > 0
  243. }
  244. get processor() {
  245. return this.result.processor
  246. }
  247. get root() {
  248. return this.sync().root
  249. }
  250. async runAsync() {
  251. this.plugin = 0
  252. for (let i = 0; i < this.plugins.length; i++) {
  253. let plugin = this.plugins[i]
  254. let promise = this.runOnRoot(plugin)
  255. if (isPromise(promise)) {
  256. try {
  257. await promise
  258. } catch (error) {
  259. throw this.handleError(error)
  260. }
  261. }
  262. }
  263. this.prepareVisitors()
  264. if (this.hasListener) {
  265. let root = this.result.root
  266. while (!root[isClean]) {
  267. root[isClean] = true
  268. let stack = [toStack(root)]
  269. while (stack.length > 0) {
  270. let promise = this.visitTick(stack)
  271. if (isPromise(promise)) {
  272. try {
  273. await promise
  274. } catch (e) {
  275. let node = stack[stack.length - 1].node
  276. throw this.handleError(e, node)
  277. }
  278. }
  279. }
  280. }
  281. if (this.listeners.OnceExit) {
  282. for (let [plugin, visitor] of this.listeners.OnceExit) {
  283. this.result.lastPlugin = plugin
  284. try {
  285. if (root.type === 'document') {
  286. let roots = root.nodes.map(subRoot =>
  287. visitor(subRoot, this.helpers)
  288. )
  289. await Promise.all(roots)
  290. } else {
  291. await visitor(root, this.helpers)
  292. }
  293. } catch (e) {
  294. throw this.handleError(e)
  295. }
  296. }
  297. }
  298. }
  299. this.processed = true
  300. return this.stringify()
  301. }
  302. runOnRoot(plugin) {
  303. this.result.lastPlugin = plugin
  304. try {
  305. if (typeof plugin === 'object' && plugin.Once) {
  306. if (this.result.root.type === 'document') {
  307. let roots = this.result.root.nodes.map(root =>
  308. plugin.Once(root, this.helpers)
  309. )
  310. if (isPromise(roots[0])) {
  311. return Promise.all(roots)
  312. }
  313. return roots
  314. }
  315. return plugin.Once(this.result.root, this.helpers)
  316. } else if (typeof plugin === 'function') {
  317. return plugin(this.result.root, this.result)
  318. }
  319. } catch (error) {
  320. throw this.handleError(error)
  321. }
  322. }
  323. stringify() {
  324. if (this.error) throw this.error
  325. if (this.stringified) return this.result
  326. this.stringified = true
  327. this.sync()
  328. let opts = this.result.opts
  329. let str = stringify
  330. if (opts.syntax) str = opts.syntax.stringify
  331. if (opts.stringifier) str = opts.stringifier
  332. if (str.stringify) str = str.stringify
  333. let map = new MapGenerator(str, this.result.root, this.result.opts)
  334. let data = map.generate()
  335. this.result.css = data[0]
  336. this.result.map = data[1]
  337. return this.result
  338. }
  339. get [Symbol.toStringTag]() {
  340. return 'LazyResult'
  341. }
  342. sync() {
  343. if (this.error) throw this.error
  344. if (this.processed) return this.result
  345. this.processed = true
  346. if (this.processing) {
  347. throw this.getAsyncError()
  348. }
  349. for (let plugin of this.plugins) {
  350. let promise = this.runOnRoot(plugin)
  351. if (isPromise(promise)) {
  352. throw this.getAsyncError()
  353. }
  354. }
  355. this.prepareVisitors()
  356. if (this.hasListener) {
  357. let root = this.result.root
  358. while (!root[isClean]) {
  359. root[isClean] = true
  360. this.walkSync(root)
  361. }
  362. if (this.listeners.OnceExit) {
  363. if (root.type === 'document') {
  364. for (let subRoot of root.nodes) {
  365. this.visitSync(this.listeners.OnceExit, subRoot)
  366. }
  367. } else {
  368. this.visitSync(this.listeners.OnceExit, root)
  369. }
  370. }
  371. }
  372. return this.result
  373. }
  374. then(onFulfilled, onRejected) {
  375. if (process.env.NODE_ENV !== 'production') {
  376. if (!('from' in this.opts)) {
  377. warnOnce(
  378. 'Without `from` option PostCSS could generate wrong source map ' +
  379. 'and will not find Browserslist config. Set it to CSS file path ' +
  380. 'or to `undefined` to prevent this warning.'
  381. )
  382. }
  383. }
  384. return this.async().then(onFulfilled, onRejected)
  385. }
  386. toString() {
  387. return this.css
  388. }
  389. visitSync(visitors, node) {
  390. for (let [plugin, visitor] of visitors) {
  391. this.result.lastPlugin = plugin
  392. let promise
  393. try {
  394. promise = visitor(node, this.helpers)
  395. } catch (e) {
  396. throw this.handleError(e, node.proxyOf)
  397. }
  398. if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
  399. return true
  400. }
  401. if (isPromise(promise)) {
  402. throw this.getAsyncError()
  403. }
  404. }
  405. }
  406. visitTick(stack) {
  407. let visit = stack[stack.length - 1]
  408. let { node, visitors } = visit
  409. if (node.type !== 'root' && node.type !== 'document' && !node.parent) {
  410. stack.pop()
  411. return
  412. }
  413. if (visitors.length > 0 && visit.visitorIndex < visitors.length) {
  414. let [plugin, visitor] = visitors[visit.visitorIndex]
  415. visit.visitorIndex += 1
  416. if (visit.visitorIndex === visitors.length) {
  417. visit.visitors = []
  418. visit.visitorIndex = 0
  419. }
  420. this.result.lastPlugin = plugin
  421. try {
  422. return visitor(node.toProxy(), this.helpers)
  423. } catch (e) {
  424. throw this.handleError(e, node)
  425. }
  426. }
  427. if (visit.iterator !== 0) {
  428. let iterator = visit.iterator
  429. let child
  430. while ((child = node.nodes[node.indexes[iterator]])) {
  431. node.indexes[iterator] += 1
  432. if (!child[isClean]) {
  433. child[isClean] = true
  434. stack.push(toStack(child))
  435. return
  436. }
  437. }
  438. visit.iterator = 0
  439. delete node.indexes[iterator]
  440. }
  441. let events = visit.events
  442. while (visit.eventIndex < events.length) {
  443. let event = events[visit.eventIndex]
  444. visit.eventIndex += 1
  445. if (event === CHILDREN) {
  446. if (node.nodes && node.nodes.length) {
  447. node[isClean] = true
  448. visit.iterator = node.getIterator()
  449. }
  450. return
  451. } else if (this.listeners[event]) {
  452. visit.visitors = this.listeners[event]
  453. return
  454. }
  455. }
  456. stack.pop()
  457. }
  458. walkSync(node) {
  459. node[isClean] = true
  460. let events = getEvents(node)
  461. for (let event of events) {
  462. if (event === CHILDREN) {
  463. if (node.nodes) {
  464. node.each(child => {
  465. if (!child[isClean]) this.walkSync(child)
  466. })
  467. }
  468. } else {
  469. let visitors = this.listeners[event]
  470. if (visitors) {
  471. if (this.visitSync(visitors, node.toProxy())) return
  472. }
  473. }
  474. }
  475. }
  476. warnings() {
  477. return this.sync().warnings()
  478. }
  479. }
  480. LazyResult.registerPostcss = dependant => {
  481. postcss = dependant
  482. }
  483. module.exports = LazyResult
  484. LazyResult.default = LazyResult
  485. Root.registerLazyResult(LazyResult)
  486. Document.registerLazyResult(LazyResult)