const postcss = require('postcss')
|
const topologicalSort = require('./topologicalSort')
|
|
const declWhitelist = ['composes']
|
const declFilter = new RegExp(`^(${declWhitelist.join('|')})$`)
|
const matchImports = /^(.+?)\s+from\s+(?:"([^"]+)"|'([^']+)'|(global))$/
|
const icssImport = /^:import\((?:"([^"]+)"|'([^']+)')\)/
|
|
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)
|
}
|
}
|
|
module.exports = 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)
|
let tmpSymbols
|
|
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})`)
|
} else {
|
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(' ')
|
}
|
})
|
|
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 && importedSymbols) {
|
rule = postcss.rule({
|
selector: `:import("${path}")`,
|
raws: { after: '\n' }
|
})
|
|
if (lastImportRule) css.insertAfter(lastImportRule, rule)
|
else css.prepend(rule)
|
}
|
|
lastImportRule = rule
|
|
if (!importedSymbols) return
|
|
Object.keys(importedSymbols).forEach(importedSymbol => {
|
rule.append(
|
postcss.decl({
|
value: importedSymbol,
|
prop: importedSymbols[importedSymbol],
|
raws: { before: '\n ' }
|
})
|
)
|
})
|
})
|
}
|
})
|