-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: predictable imports order (#138)
* predictable imports order * test cases * added visited graph to handle copies * fallback for nodejs 4 * testcase for duplicates * graph description * version 1.2.0
- Loading branch information
1 parent
c2c40a2
commit 7f4028c
Showing
12 changed files
with
416 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
node_modules | ||
.DS_Store | ||
coverage | ||
lib | ||
node_modules | ||
yarn.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,169 @@ | ||
import postcss from 'postcss'; | ||
import topologicalSort from './topologicalSort'; | ||
|
||
const declWhitelist = ['composes'], | ||
declFilter = new RegExp( `^(${declWhitelist.join( '|' )})$` ), | ||
matchImports = /^(.+?)\s+from\s+(?:"([^"]+)"|'([^']+)'|(global))$/, | ||
icssImport = /^:import\((?:"([^"]+)"|'([^']+)')\)/; | ||
const declWhitelist = ['composes']; | ||
const declFilter = new RegExp( `^(${declWhitelist.join( '|' )})$` ); | ||
const matchImports = /^(.+?)\s+from\s+(?:"([^"]+)"|'([^']+)'|(global))$/; | ||
const icssImport = /^:import\((?:"([^"]+)"|'([^']+)')\)/; | ||
|
||
const processor = postcss.plugin( 'modules-extract-imports', function ( options ) { | ||
return ( css ) => { | ||
let imports = {}, | ||
importIndex = 0, | ||
createImportedName = options && options.createImportedName || (( importName/*, path*/ ) => `i__imported_${importName.replace( /\W/g, '_' )}_${importIndex++}`); | ||
const VISITED_MARKER = 1; | ||
|
||
function createParentName(rule, root) { | ||
return `__${root.index(rule.parent)}_${rule.selector}`; | ||
} | ||
|
||
function serializeImports(imports) { | ||
return imports.map(importPath => '`' + importPath + '`').join(', '); | ||
} | ||
|
||
/** | ||
* :import('G') {} | ||
* | ||
* Rule | ||
* composes: ... from 'A' | ||
* composes: ... from 'B' | ||
* Rule | ||
* composes: ... from 'A' | ||
* composes: ... from 'A' | ||
* composes: ... from 'C' | ||
* | ||
* Results in: | ||
* | ||
* graph: { | ||
* G: [], | ||
* A: [], | ||
* B: ['A'], | ||
* C: ['A'], | ||
* } | ||
*/ | ||
function addImportToGraph(importId, parentId, graph, visited) { | ||
const siblingsId = parentId + '_' + 'siblings'; | ||
const visitedId = parentId + '_' + importId; | ||
|
||
if (visited[visitedId] !== VISITED_MARKER) { | ||
if (!Array.isArray(visited[siblingsId])) visited[siblingsId] = []; | ||
|
||
const siblings = visited[siblingsId]; | ||
|
||
if (Array.isArray(graph[importId])) | ||
graph[importId] = graph[importId].concat(siblings); | ||
else | ||
graph[importId] = siblings.slice(); | ||
|
||
visited[visitedId] = VISITED_MARKER; | ||
siblings.push(importId); | ||
} | ||
} | ||
|
||
const processor = postcss.plugin('modules-extract-imports', function (options = {}) { | ||
const failOnWrongOrder = options.failOnWrongOrder; | ||
|
||
return css => { | ||
const graph = {}; | ||
const visited = {}; | ||
|
||
const existingImports = {}; | ||
const importDecls = {}; | ||
const imports = {}; | ||
|
||
let importIndex = 0; | ||
|
||
const createImportedName = typeof options.createImportedName !== 'function' | ||
? (importName/*, path*/) => `i__imported_${importName.replace(/\W/g, '_')}_${importIndex++}` | ||
: options.createImportedName; | ||
|
||
// Check the existing imports order and save refs | ||
css.walkRules(rule => { | ||
const matches = icssImport.exec(rule.selector); | ||
|
||
if (matches) { | ||
const [/*match*/, doubleQuotePath, singleQuotePath] = matches; | ||
const importPath = doubleQuotePath || singleQuotePath; | ||
|
||
addImportToGraph(importPath, 'root', graph, visited); | ||
|
||
existingImports[importPath] = rule; | ||
} | ||
}); | ||
|
||
// Find any declaration that supports imports | ||
css.walkDecls( declFilter, ( decl ) => { | ||
let matches = decl.value.match( matchImports ); | ||
css.walkDecls(declFilter, decl => { | ||
let matches = decl.value.match(matchImports); | ||
let tmpSymbols; | ||
if ( matches ) { | ||
|
||
if (matches) { | ||
let [/*match*/, symbols, doubleQuotePath, singleQuotePath, global] = matches; | ||
|
||
if (global) { | ||
// Composing globals simply means changing these classes to wrap them in global(name) | ||
tmpSymbols = symbols.split(/\s+/).map(s => `global(${s})`) | ||
tmpSymbols = symbols.split(/\s+/).map(s => `global(${s})`); | ||
} else { | ||
let path = doubleQuotePath || singleQuotePath; | ||
imports[path] = imports[path] || {}; | ||
tmpSymbols = symbols.split(/\s+/) | ||
.map(s => { | ||
if (!imports[path][s]) { | ||
imports[path][s] = createImportedName(s, path); | ||
} | ||
return imports[path][s]; | ||
}); | ||
const importPath = doubleQuotePath || singleQuotePath; | ||
const parentRule = createParentName(decl.parent, css); | ||
|
||
addImportToGraph(importPath, parentRule, graph, visited); | ||
|
||
importDecls[importPath] = decl; | ||
imports[importPath] = imports[importPath] || {}; | ||
|
||
tmpSymbols = symbols.split(/\s+/).map(s => { | ||
if (!imports[importPath][s]) { | ||
imports[importPath][s] = createImportedName(s, importPath); | ||
} | ||
|
||
return imports[importPath][s]; | ||
}); | ||
} | ||
decl.value = tmpSymbols.join( ' ' ); | ||
} | ||
} ); | ||
|
||
// If we've found any imports, insert or append :import rules | ||
let existingImports = {}; | ||
css.walkRules(rule => { | ||
let matches = icssImport.exec(rule.selector); | ||
if (matches) { | ||
let [/*match*/, doubleQuotePath, singleQuotePath] = matches; | ||
existingImports[doubleQuotePath || singleQuotePath] = rule; | ||
decl.value = tmpSymbols.join(' '); | ||
} | ||
}); | ||
|
||
Object.keys( imports ).reverse().forEach( path => { | ||
const importsOrder = topologicalSort(graph, failOnWrongOrder); | ||
|
||
if (importsOrder instanceof Error) { | ||
const importPath = importsOrder.nodes.find(importPath => importDecls.hasOwnProperty(importPath)); | ||
const decl = importDecls[importPath]; | ||
|
||
const errMsg = 'Failed to resolve order of composed modules ' + serializeImports(importsOrder.nodes) + '.'; | ||
|
||
throw decl.error(errMsg, { | ||
plugin: 'modules-extract-imports', | ||
word: 'composes', | ||
}); | ||
} | ||
|
||
let lastImportRule; | ||
importsOrder.forEach(path => { | ||
const importedSymbols = imports[path]; | ||
let rule = existingImports[path]; | ||
if (!rule) { | ||
rule = postcss.rule( { | ||
|
||
if (!rule && importedSymbols) { | ||
rule = postcss.rule({ | ||
selector: `:import("${path}")`, | ||
raws: { after: "\n" } | ||
} ); | ||
css.prepend( rule ); | ||
raws: {after: '\n'}, | ||
}); | ||
|
||
if (lastImportRule) | ||
css.insertAfter(lastImportRule, rule); | ||
else | ||
css.prepend(rule); | ||
} | ||
Object.keys( imports[path] ).forEach( importedSymbol => { | ||
rule.append(postcss.decl( { | ||
|
||
lastImportRule = rule; | ||
|
||
if (!importedSymbols) return; | ||
|
||
Object.keys(importedSymbols).forEach(importedSymbol => { | ||
rule.append(postcss.decl({ | ||
value: importedSymbol, | ||
prop: imports[path][importedSymbol], | ||
raws: { before: "\n " } | ||
} ) ); | ||
} ); | ||
} ); | ||
prop: importedSymbols[importedSymbol], | ||
raws: {before: '\n '}, | ||
})); | ||
}); | ||
}); | ||
}; | ||
} ); | ||
}); | ||
|
||
export default processor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
const PERMANENT_MARKER = 2; | ||
const TEMPORARY_MARKER = 1; | ||
|
||
function createError(node, graph) { | ||
const er = new Error('Nondeterministic import\'s order'); | ||
|
||
const related = graph[node]; | ||
const relatedNode = related.find(relatedNode => graph[relatedNode].indexOf(node) > -1); | ||
|
||
er.nodes = [node, relatedNode]; | ||
|
||
return er; | ||
} | ||
|
||
function walkGraph(node, graph, state, result, strict) { | ||
if (state[node] === PERMANENT_MARKER) return; | ||
if (state[node] === TEMPORARY_MARKER) { | ||
if (strict) return createError(node, graph); | ||
return; | ||
} | ||
|
||
state[node] = TEMPORARY_MARKER; | ||
|
||
const children = graph[node]; | ||
const length = children.length; | ||
|
||
for (let i = 0; i < length; ++i) { | ||
const er = walkGraph(children[i], graph, state, result, strict); | ||
if (er instanceof Error) return er; | ||
} | ||
|
||
state[node] = PERMANENT_MARKER; | ||
|
||
result.push(node); | ||
} | ||
|
||
export default function topologicalSort(graph, strict) { | ||
const result = []; | ||
const state = {}; | ||
|
||
const nodes = Object.keys(graph); | ||
const length = nodes.length; | ||
|
||
for (let i = 0; i < length; ++i) { | ||
const er = walkGraph(nodes[i], graph, state, result, strict); | ||
if (er instanceof Error) return er; | ||
} | ||
|
||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
'use strict'; | ||
|
||
const assert = require('assert'); | ||
const postcss = require('postcss'); | ||
const processor = require('../'); | ||
|
||
describe('check-import-order', () => { | ||
let pipeline; | ||
|
||
beforeEach(() => { | ||
pipeline = postcss([ | ||
processor({failOnWrongOrder: true}), | ||
]); | ||
}); | ||
|
||
it('should throw an exception', () => { | ||
const input = ` | ||
.aa { | ||
composes: b from './b.css'; | ||
composes: c from './c.css'; | ||
} | ||
.bb { | ||
composes: c from './c.css'; | ||
composes: b from './b.css'; | ||
} | ||
`; | ||
|
||
assert.throws(() => pipeline.process(input).css, | ||
/Failed to resolve order of composed modules/); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
:import("./b.css") { | ||
i__imported_b_1: b; | ||
} | ||
|
||
:import("./c.css") { | ||
i__imported_c_0: c; | ||
} | ||
|
||
.a { | ||
composes: i__imported_c_0; | ||
color: #bebebe; | ||
} | ||
|
||
.b { | ||
/* `b` should be after `c` */ | ||
composes: i__imported_b_1; | ||
composes: i__imported_c_0; | ||
color: #aaa; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
.a { | ||
composes: c from "./c.css"; | ||
color: #bebebe; | ||
} | ||
|
||
.b { | ||
/* `b` should be after `c` */ | ||
composes: b from "./b.css"; | ||
composes: c from "./c.css"; | ||
color: #aaa; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
:import("./aa.css") { | ||
i__imported_a_0: a; | ||
} | ||
|
||
:import("./bb.css") { | ||
i__imported_b_1: b; | ||
} | ||
|
||
:import("./cc.css") { | ||
smthing: somevalue; | ||
i__imported_c_2: c; | ||
} | ||
|
||
.a { | ||
composes: i__imported_a_0; | ||
composes: i__imported_b_1; | ||
composes: i__imported_c_2; | ||
composes: i__imported_a_0; | ||
composes: i__imported_c_2; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
:import("./cc.css") { | ||
smthing: somevalue; | ||
} | ||
|
||
.a { | ||
composes: a from './aa.css'; | ||
composes: b from './bb.css'; | ||
composes: c from './cc.css'; | ||
composes: a from './aa.css'; | ||
composes: c from './cc.css'; | ||
} |
Oops, something went wrong.