index.js 5.8 KB

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