index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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 matches = declaration.value.match(matchImports);
  72. if (!matches) {
  73. return;
  74. }
  75. let tmpSymbols;
  76. let [
  77. ,
  78. /*match*/ symbols,
  79. doubleQuotePath,
  80. singleQuotePath,
  81. global,
  82. ] = matches;
  83. if (global) {
  84. // Composing globals simply means changing these classes to wrap them in global(name)
  85. tmpSymbols = symbols.split(/\s+/).map((s) => `global(${s})`);
  86. } else {
  87. const importPath = doubleQuotePath || singleQuotePath;
  88. let parent = declaration.parent;
  89. let parentIndexes = "";
  90. while (parent.type !== "root") {
  91. parentIndexes =
  92. parent.parent.index(parent) + "_" + parentIndexes;
  93. parent = parent.parent;
  94. }
  95. const { selector } = declaration.parent;
  96. const parentRule = `_${parentIndexes}${selector}`;
  97. addImportToGraph(importPath, parentRule, graph, visited);
  98. importDecls[importPath] = declaration;
  99. imports[importPath] = imports[importPath] || {};
  100. tmpSymbols = symbols.split(/\s+/).map((s) => {
  101. if (!imports[importPath][s]) {
  102. imports[importPath][s] = createImportedName(s, importPath);
  103. }
  104. return imports[importPath][s];
  105. });
  106. }
  107. declaration.value = tmpSymbols.join(" ");
  108. });
  109. const importsOrder = topologicalSort(graph, failOnWrongOrder);
  110. if (importsOrder instanceof Error) {
  111. const importPath = importsOrder.nodes.find((importPath) =>
  112. // eslint-disable-next-line no-prototype-builtins
  113. importDecls.hasOwnProperty(importPath)
  114. );
  115. const decl = importDecls[importPath];
  116. throw decl.error(
  117. "Failed to resolve order of composed modules " +
  118. importsOrder.nodes
  119. .map((importPath) => "`" + importPath + "`")
  120. .join(", ") +
  121. ".",
  122. {
  123. plugin: "postcss-modules-extract-imports",
  124. word: "composes",
  125. }
  126. );
  127. }
  128. let lastImportRule;
  129. importsOrder.forEach((path) => {
  130. const importedSymbols = imports[path];
  131. let rule = existingImports[path];
  132. if (!rule && importedSymbols) {
  133. rule = postcss.rule({
  134. selector: `:import("${path}")`,
  135. raws: { after: "\n" },
  136. });
  137. if (lastImportRule) {
  138. root.insertAfter(lastImportRule, rule);
  139. } else {
  140. root.prepend(rule);
  141. }
  142. }
  143. lastImportRule = rule;
  144. if (!importedSymbols) {
  145. return;
  146. }
  147. Object.keys(importedSymbols).forEach((importedSymbol) => {
  148. rule.append(
  149. postcss.decl({
  150. value: importedSymbol,
  151. prop: importedSymbols[importedSymbol],
  152. raws: { before: "\n " },
  153. })
  154. );
  155. });
  156. });
  157. },
  158. };
  159. },
  160. };
  161. };
  162. module.exports.postcss = true;