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