|
|
"use strict" // builtin tooling
const path = require("path")
// internal tooling
const joinMedia = require("./lib/join-media") const joinLayer = require("./lib/join-layer") const resolveId = require("./lib/resolve-id") const loadContent = require("./lib/load-content") const processContent = require("./lib/process-content") const parseStatements = require("./lib/parse-statements")
function AtImport(options) { options = { root: process.cwd(), path: [], skipDuplicates: true, resolve: resolveId, load: loadContent, plugins: [], addModulesDirectories: [], ...options, }
options.root = path.resolve(options.root)
// convert string to an array of a single element
if (typeof options.path === "string") options.path = [options.path]
if (!Array.isArray(options.path)) options.path = []
options.path = options.path.map(p => path.resolve(options.root, p))
return { postcssPlugin: "postcss-import", Once(styles, { result, atRule, postcss }) { const state = { importedFiles: {}, hashFiles: {}, }
if (styles.source && styles.source.input && styles.source.input.file) { state.importedFiles[styles.source.input.file] = {} }
if (options.plugins && !Array.isArray(options.plugins)) { throw new Error("plugins option must be an array") }
return parseStyles(result, styles, options, state, [], []).then( bundle => { applyRaws(bundle) applyMedia(bundle) applyStyles(bundle, styles) } )
function applyRaws(bundle) { bundle.forEach((stmt, index) => { if (index === 0) return
if (stmt.parent) { const { before } = stmt.parent.node.raws if (stmt.type === "nodes") stmt.nodes[0].raws.before = before else stmt.node.raws.before = before } else if (stmt.type === "nodes") { stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n" } }) }
function applyMedia(bundle) { bundle.forEach(stmt => { if ( (!stmt.media.length && !stmt.layer.length) || stmt.type === "charset" ) { return }
if (stmt.type === "import") { stmt.node.params = `${stmt.fullUri} ${stmt.media.join(", ")}` } else if (stmt.type === "media") { stmt.node.params = stmt.media.join(", ") } else { const { nodes } = stmt const { parent } = nodes[0]
let outerAtRule let innerAtRule if (stmt.media.length && stmt.layer.length) { const mediaNode = atRule({ name: "media", params: stmt.media.join(", "), source: parent.source, })
const layerNode = atRule({ name: "layer", params: stmt.layer.filter(layer => layer !== "").join("."), source: parent.source, })
mediaNode.append(layerNode) innerAtRule = layerNode outerAtRule = mediaNode } else if (stmt.media.length) { const mediaNode = atRule({ name: "media", params: stmt.media.join(", "), source: parent.source, })
innerAtRule = mediaNode outerAtRule = mediaNode } else if (stmt.layer.length) { const layerNode = atRule({ name: "layer", params: stmt.layer.filter(layer => layer !== "").join("."), source: parent.source, })
innerAtRule = layerNode outerAtRule = layerNode }
parent.insertBefore(nodes[0], outerAtRule)
// remove nodes
nodes.forEach(node => { node.parent = undefined })
// better output
nodes[0].raws.before = nodes[0].raws.before || "\n"
// wrap new rules with media query and/or layer at rule
innerAtRule.append(nodes)
stmt.type = "media" stmt.node = outerAtRule delete stmt.nodes } }) }
function applyStyles(bundle, styles) { styles.nodes = []
// Strip additional statements.
bundle.forEach(stmt => { if (["charset", "import", "media"].includes(stmt.type)) { stmt.node.parent = undefined styles.append(stmt.node) } else if (stmt.type === "nodes") { stmt.nodes.forEach(node => { node.parent = undefined styles.append(node) }) } }) }
function parseStyles(result, styles, options, state, media, layer) { const statements = parseStatements(result, styles)
return Promise.resolve(statements) .then(stmts => { // process each statement in series
return stmts.reduce((promise, stmt) => { return promise.then(() => { stmt.media = joinMedia(media, stmt.media || []) stmt.layer = joinLayer(layer, stmt.layer || [])
// skip protocol base uri (protocol://url) or protocol-relative
if ( stmt.type !== "import" || /^(?:[a-z]+:)?\/\//i.test(stmt.uri) ) { return }
if (options.filter && !options.filter(stmt.uri)) { // rejected by filter
return }
return resolveImportId(result, stmt, options, state) }) }, Promise.resolve()) }) .then(() => { let charset const imports = [] const bundle = []
function handleCharset(stmt) { if (!charset) charset = stmt // charsets aren't case-sensitive, so convert to lower case to compare
else if ( stmt.node.params.toLowerCase() !== charset.node.params.toLowerCase() ) { throw new Error( `Incompatable @charset statements:
${stmt.node.params} specified in ${stmt.node.source.input.file} ${charset.node.params} specified in ${charset.node.source.input.file}`
) } }
// squash statements and their children
statements.forEach(stmt => { if (stmt.type === "charset") handleCharset(stmt) else if (stmt.type === "import") { if (stmt.children) { stmt.children.forEach((child, index) => { if (child.type === "import") imports.push(child) else if (child.type === "charset") handleCharset(child) else bundle.push(child) // For better output
if (index === 0) child.parent = stmt }) } else imports.push(stmt) } else if (stmt.type === "media" || stmt.type === "nodes") { bundle.push(stmt) } })
return charset ? [charset, ...imports.concat(bundle)] : imports.concat(bundle) }) }
function resolveImportId(result, stmt, options, state) { const atRule = stmt.node let sourceFile if (atRule.source && atRule.source.input && atRule.source.input.file) { sourceFile = atRule.source.input.file } const base = sourceFile ? path.dirname(atRule.source.input.file) : options.root
return Promise.resolve(options.resolve(stmt.uri, base, options)) .then(paths => { if (!Array.isArray(paths)) paths = [paths] // Ensure that each path is absolute:
return Promise.all( paths.map(file => { return !path.isAbsolute(file) ? resolveId(file, base, options) : file }) ) }) .then(resolved => { // Add dependency messages:
resolved.forEach(file => { result.messages.push({ type: "dependency", plugin: "postcss-import", file, parent: sourceFile, }) })
return Promise.all( resolved.map(file => { return loadImportContent(result, stmt, file, options, state) }) ) }) .then(result => { // Merge loaded statements
stmt.children = result.reduce((result, statements) => { return statements ? result.concat(statements) : result }, []) }) }
function loadImportContent(result, stmt, filename, options, state) { const atRule = stmt.node const { media, layer } = stmt if (options.skipDuplicates) { // skip files already imported at the same scope
if ( state.importedFiles[filename] && state.importedFiles[filename][media] ) { return }
// save imported files to skip them next time
if (!state.importedFiles[filename]) state.importedFiles[filename] = {} state.importedFiles[filename][media] = true }
return Promise.resolve(options.load(filename, options)).then( content => { if (content.trim() === "") { result.warn(`${filename} is empty`, { node: atRule }) return }
// skip previous imported files not containing @import rules
if (state.hashFiles[content] && state.hashFiles[content][media]) return
return processContent( result, content, filename, options, postcss ).then(importedResult => { const styles = importedResult.root result.messages = result.messages.concat(importedResult.messages)
if (options.skipDuplicates) { const hasImport = styles.some(child => { return child.type === "atrule" && child.name === "import" }) if (!hasImport) { // save hash files to skip them next time
if (!state.hashFiles[content]) state.hashFiles[content] = {} state.hashFiles[content][media] = true } }
// recursion: import @import from imported file
return parseStyles(result, styles, options, state, media, layer) }) } ) } }, } }
AtImport.postcss = true
module.exports = AtImport
|