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