lazy-result.js 13 KB

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