index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. const postcss = require('postcss')
  2. const topologicalSort = require('./topologicalSort')
  3. const declWhitelist = ['composes']
  4. const declFilter = new RegExp(`^(${declWhitelist.join('|')})$`)
  5. const matchImports = /^(.+?)\s+from\s+(?:"([^"]+)"|'([^']+)'|(global))$/
  6. const icssImport = /^:import\((?:"([^"]+)"|'([^']+)')\)/
  7. const VISITED_MARKER = 1
  8. function createParentName(rule, root) {
  9. return `__${root.index(rule.parent)}_${rule.selector}`
  10. }
  11. function serializeImports(imports) {
  12. return imports.map(importPath => '`' + importPath + '`').join(', ')
  13. }
  14. /**
  15. * :import('G') {}
  16. *
  17. * Rule
  18. * composes: ... from 'A'
  19. * composes: ... from 'B'
  20. * Rule
  21. * composes: ... from 'A'
  22. * composes: ... from 'A'
  23. * composes: ... from 'C'
  24. *
  25. * Results in:
  26. *
  27. * graph: {
  28. * G: [],
  29. * A: [],
  30. * B: ['A'],
  31. * C: ['A'],
  32. * }
  33. */
  34. function addImportToGraph(importId, parentId, graph, visited) {
  35. const siblingsId = parentId + '_' + 'siblings'
  36. const visitedId = parentId + '_' + importId
  37. if (visited[visitedId] !== VISITED_MARKER) {
  38. if (!Array.isArray(visited[siblingsId])) visited[siblingsId] = []
  39. const siblings = visited[siblingsId]
  40. if (Array.isArray(graph[importId]))
  41. graph[importId] = graph[importId].concat(siblings)
  42. else graph[importId] = siblings.slice()
  43. visited[visitedId] = VISITED_MARKER
  44. siblings.push(importId)
  45. }
  46. }
  47. module.exports = postcss.plugin('modules-extract-imports', function(
  48. options = {}
  49. ) {
  50. const failOnWrongOrder = options.failOnWrongOrder
  51. return css => {
  52. const graph = {}
  53. const visited = {}
  54. const existingImports = {}
  55. const importDecls = {}
  56. const imports = {}
  57. let importIndex = 0
  58. const createImportedName = typeof options.createImportedName !== 'function'
  59. ? (importName /*, path*/) =>
  60. `i__imported_${importName.replace(/\W/g, '_')}_${importIndex++}`
  61. : options.createImportedName
  62. // Check the existing imports order and save refs
  63. css.walkRules(rule => {
  64. const matches = icssImport.exec(rule.selector)
  65. if (matches) {
  66. const [, /*match*/ doubleQuotePath, singleQuotePath] = matches
  67. const importPath = doubleQuotePath || singleQuotePath
  68. addImportToGraph(importPath, 'root', graph, visited)
  69. existingImports[importPath] = rule
  70. }
  71. })
  72. // Find any declaration that supports imports
  73. css.walkDecls(declFilter, decl => {
  74. let matches = decl.value.match(matchImports)
  75. let tmpSymbols
  76. if (matches) {
  77. let [
  78. ,
  79. /*match*/ symbols,
  80. doubleQuotePath,
  81. singleQuotePath,
  82. global
  83. ] = matches
  84. if (global) {
  85. // Composing globals simply means changing these classes to wrap them in global(name)
  86. tmpSymbols = symbols.split(/\s+/).map(s => `global(${s})`)
  87. } else {
  88. const importPath = doubleQuotePath || singleQuotePath
  89. const parentRule = createParentName(decl.parent, css)
  90. addImportToGraph(importPath, parentRule, graph, visited)
  91. importDecls[importPath] = decl
  92. imports[importPath] = imports[importPath] || {}
  93. tmpSymbols = symbols.split(/\s+/).map(s => {
  94. if (!imports[importPath][s]) {
  95. imports[importPath][s] = createImportedName(s, importPath)
  96. }
  97. return imports[importPath][s]
  98. })
  99. }
  100. decl.value = tmpSymbols.join(' ')
  101. }
  102. })
  103. const importsOrder = topologicalSort(graph, failOnWrongOrder)
  104. if (importsOrder instanceof Error) {
  105. const importPath = importsOrder.nodes.find(importPath =>
  106. importDecls.hasOwnProperty(importPath)
  107. )
  108. const decl = importDecls[importPath]
  109. const errMsg =
  110. 'Failed to resolve order of composed modules ' +
  111. serializeImports(importsOrder.nodes) +
  112. '.'
  113. throw decl.error(errMsg, {
  114. plugin: 'modules-extract-imports',
  115. word: 'composes'
  116. })
  117. }
  118. let lastImportRule
  119. importsOrder.forEach(path => {
  120. const importedSymbols = imports[path]
  121. let rule = existingImports[path]
  122. if (!rule && importedSymbols) {
  123. rule = postcss.rule({
  124. selector: `:import("${path}")`,
  125. raws: { after: '\n' }
  126. })
  127. if (lastImportRule) css.insertAfter(lastImportRule, rule)
  128. else css.prepend(rule)
  129. }
  130. lastImportRule = rule
  131. if (!importedSymbols) return
  132. Object.keys(importedSymbols).forEach(importedSymbol => {
  133. rule.append(
  134. postcss.decl({
  135. value: importedSymbol,
  136. prop: importedSymbols[importedSymbol],
  137. raws: { before: '\n ' }
  138. })
  139. )
  140. })
  141. })
  142. }
  143. })