You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

550 lines
13 KiB

  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. get content() {
  96. return this.stringify().content
  97. }
  98. get css() {
  99. return this.stringify().css
  100. }
  101. get map() {
  102. return this.stringify().map
  103. }
  104. get messages() {
  105. return this.sync().messages
  106. }
  107. get opts() {
  108. return this.result.opts
  109. }
  110. get processor() {
  111. return this.result.processor
  112. }
  113. get root() {
  114. return this.sync().root
  115. }
  116. get [Symbol.toStringTag]() {
  117. return 'LazyResult'
  118. }
  119. constructor(processor, css, opts) {
  120. this.stringified = false
  121. this.processed = false
  122. let root
  123. if (
  124. typeof css === 'object' &&
  125. css !== null &&
  126. (css.type === 'root' || css.type === 'document')
  127. ) {
  128. root = cleanMarks(css)
  129. } else if (css instanceof LazyResult || css instanceof Result) {
  130. root = cleanMarks(css.root)
  131. if (css.map) {
  132. if (typeof opts.map === 'undefined') opts.map = {}
  133. if (!opts.map.inline) opts.map.inline = false
  134. opts.map.prev = css.map
  135. }
  136. } else {
  137. let parser = parse
  138. if (opts.syntax) parser = opts.syntax.parse
  139. if (opts.parser) parser = opts.parser
  140. if (parser.parse) parser = parser.parse
  141. try {
  142. root = parser(css, opts)
  143. } catch (error) {
  144. this.processed = true
  145. this.error = error
  146. }
  147. if (root && !root[my]) {
  148. /* c8 ignore next 2 */
  149. Container.rebuild(root)
  150. }
  151. }
  152. this.result = new Result(processor, root, opts)
  153. this.helpers = { ...postcss, postcss, result: this.result }
  154. this.plugins = this.processor.plugins.map(plugin => {
  155. if (typeof plugin === 'object' && plugin.prepare) {
  156. return { ...plugin, ...plugin.prepare(this.result) }
  157. } else {
  158. return plugin
  159. }
  160. })
  161. }
  162. async() {
  163. if (this.error) return Promise.reject(this.error)
  164. if (this.processed) return Promise.resolve(this.result)
  165. if (!this.processing) {
  166. this.processing = this.runAsync()
  167. }
  168. return this.processing
  169. }
  170. catch(onRejected) {
  171. return this.async().catch(onRejected)
  172. }
  173. finally(onFinally) {
  174. return this.async().then(onFinally, onFinally)
  175. }
  176. getAsyncError() {
  177. throw new Error('Use process(css).then(cb) to work with async plugins')
  178. }
  179. handleError(error, node) {
  180. let plugin = this.result.lastPlugin
  181. try {
  182. if (node) node.addToError(error)
  183. this.error = error
  184. if (error.name === 'CssSyntaxError' && !error.plugin) {
  185. error.plugin = plugin.postcssPlugin
  186. error.setMessage()
  187. } else if (plugin.postcssVersion) {
  188. if (process.env.NODE_ENV !== 'production') {
  189. let pluginName = plugin.postcssPlugin
  190. let pluginVer = plugin.postcssVersion
  191. let runtimeVer = this.result.processor.version
  192. let a = pluginVer.split('.')
  193. let b = runtimeVer.split('.')
  194. if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) {
  195. // eslint-disable-next-line no-console
  196. console.error(
  197. 'Unknown error from PostCSS plugin. Your current PostCSS ' +
  198. 'version is ' +
  199. runtimeVer +
  200. ', but ' +
  201. pluginName +
  202. ' uses ' +
  203. pluginVer +
  204. '. Perhaps this is the source of the error below.'
  205. )
  206. }
  207. }
  208. }
  209. } catch (err) {
  210. /* c8 ignore next 3 */
  211. // eslint-disable-next-line no-console
  212. if (console && console.error) console.error(err)
  213. }
  214. return error
  215. }
  216. prepareVisitors() {
  217. this.listeners = {}
  218. let add = (plugin, type, cb) => {
  219. if (!this.listeners[type]) this.listeners[type] = []
  220. this.listeners[type].push([plugin, cb])
  221. }
  222. for (let plugin of this.plugins) {
  223. if (typeof plugin === 'object') {
  224. for (let event in plugin) {
  225. if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) {
  226. throw new Error(
  227. `Unknown event ${event} in ${plugin.postcssPlugin}. ` +
  228. `Try to update PostCSS (${this.processor.version} now).`
  229. )
  230. }
  231. if (!NOT_VISITORS[event]) {
  232. if (typeof plugin[event] === 'object') {
  233. for (let filter in plugin[event]) {
  234. if (filter === '*') {
  235. add(plugin, event, plugin[event][filter])
  236. } else {
  237. add(
  238. plugin,
  239. event + '-' + filter.toLowerCase(),
  240. plugin[event][filter]
  241. )
  242. }
  243. }
  244. } else if (typeof plugin[event] === 'function') {
  245. add(plugin, event, plugin[event])
  246. }
  247. }
  248. }
  249. }
  250. }
  251. this.hasListener = Object.keys(this.listeners).length > 0
  252. }
  253. async runAsync() {
  254. this.plugin = 0
  255. for (let i = 0; i < this.plugins.length; i++) {
  256. let plugin = this.plugins[i]
  257. let promise = this.runOnRoot(plugin)
  258. if (isPromise(promise)) {
  259. try {
  260. await promise
  261. } catch (error) {
  262. throw this.handleError(error)
  263. }
  264. }
  265. }
  266. this.prepareVisitors()
  267. if (this.hasListener) {
  268. let root = this.result.root
  269. while (!root[isClean]) {
  270. root[isClean] = true
  271. let stack = [toStack(root)]
  272. while (stack.length > 0) {
  273. let promise = this.visitTick(stack)
  274. if (isPromise(promise)) {
  275. try {
  276. await promise
  277. } catch (e) {
  278. let node = stack[stack.length - 1].node
  279. throw this.handleError(e, node)
  280. }
  281. }
  282. }
  283. }
  284. if (this.listeners.OnceExit) {
  285. for (let [plugin, visitor] of this.listeners.OnceExit) {
  286. this.result.lastPlugin = plugin
  287. try {
  288. if (root.type === 'document') {
  289. let roots = root.nodes.map(subRoot =>
  290. visitor(subRoot, this.helpers)
  291. )
  292. await Promise.all(roots)
  293. } else {
  294. await visitor(root, this.helpers)
  295. }
  296. } catch (e) {
  297. throw this.handleError(e)
  298. }
  299. }
  300. }
  301. }
  302. this.processed = true
  303. return this.stringify()
  304. }
  305. runOnRoot(plugin) {
  306. this.result.lastPlugin = plugin
  307. try {
  308. if (typeof plugin === 'object' && plugin.Once) {
  309. if (this.result.root.type === 'document') {
  310. let roots = this.result.root.nodes.map(root =>
  311. plugin.Once(root, this.helpers)
  312. )
  313. if (isPromise(roots[0])) {
  314. return Promise.all(roots)
  315. }
  316. return roots
  317. }
  318. return plugin.Once(this.result.root, this.helpers)
  319. } else if (typeof plugin === 'function') {
  320. return plugin(this.result.root, this.result)
  321. }
  322. } catch (error) {
  323. throw this.handleError(error)
  324. }
  325. }
  326. stringify() {
  327. if (this.error) throw this.error
  328. if (this.stringified) return this.result
  329. this.stringified = true
  330. this.sync()
  331. let opts = this.result.opts
  332. let str = stringify
  333. if (opts.syntax) str = opts.syntax.stringify
  334. if (opts.stringifier) str = opts.stringifier
  335. if (str.stringify) str = str.stringify
  336. let map = new MapGenerator(str, this.result.root, this.result.opts)
  337. let data = map.generate()
  338. this.result.css = data[0]
  339. this.result.map = data[1]
  340. return this.result
  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)