diff --git a/index.js b/index.js index d32cf967..0687f65c 100755 --- a/index.js +++ b/index.js @@ -2,9 +2,6 @@ // builtin tooling const path = require("path") -// external tooling -const postcss = require("postcss") - // internal tooling const joinMedia = require("./lib/join-media") const resolveId = require("./lib/resolve-id") @@ -35,234 +32,253 @@ function AtImport(options) { options.path = options.path.map(p => path.resolve(options.root, p)) - return function(styles, result) { - 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.before - 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" - } - }) -} + return { + postcssPlugin: "postcss-import", + Once(styles, { result, atRule }) { + const state = { + importedFiles: {}, + hashFiles: {}, + } -function applyMedia(bundle) { - bundle.forEach(stmt => { - if (!stmt.media.length) 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.nodes - const parent = nodes[0].parent - const mediaNode = postcss.atRule({ - name: "media", - params: stmt.media.join(", "), - source: parent.source, - }) + if (styles.source && styles.source.input && styles.source.input.file) { + state.importedFiles[styles.source.input.file] = {} + } - parent.insertBefore(nodes[0], mediaNode) + if (options.plugins && !Array.isArray(options.plugins)) { + throw new Error("plugins option must be an array") + } - // remove nodes - nodes.forEach(node => { - node.parent = undefined + return parseStyles(result, styles, options, state, []).then(bundle => { + applyRaws(bundle) + applyMedia(bundle) + applyStyles(bundle, styles) }) - // better output - nodes[0].raws.before = nodes[0].raws.before || "\n" + function applyRaws(bundle) { + bundle.forEach((stmt, index) => { + if (index === 0) return - // wrap new rules with media query - mediaNode.append(nodes) + if (stmt.parent) { + const before = stmt.parent.node.raws.before + 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" + } + }) + } - stmt.type = "media" - stmt.node = mediaNode - delete stmt.nodes - } - }) -} + function applyMedia(bundle) { + bundle.forEach(stmt => { + if (!stmt.media.length) 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.nodes + const parent = nodes[0].parent + const mediaNode = atRule({ + name: "media", + params: stmt.media.join(", "), + source: parent.source, + }) -function applyStyles(bundle, styles) { - styles.nodes = [] - - // Strip additional statements. - bundle.forEach(stmt => { - if (stmt.type === "import") { - stmt.node.parent = undefined - styles.append(stmt.node) - } else if (stmt.type === "media") { - stmt.node.parent = undefined - styles.append(stmt.node) - } else if (stmt.type === "nodes") { - stmt.nodes.forEach(node => { - node.parent = undefined - styles.append(node) - }) - } - }) -} + parent.insertBefore(nodes[0], mediaNode) -function parseStyles(result, styles, options, state, media) { - const statements = parseStatements(result, styles) + // remove nodes + nodes.forEach(node => { + node.parent = undefined + }) - 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 || []) + // better output + nodes[0].raws.before = nodes[0].raws.before || "\n" - // skip protocol base uri (protocol://url) or protocol-relative - if (stmt.type !== "import" || /^(?:[a-z]+:)?\/\//i.test(stmt.uri)) { - return - } + // wrap new rules with media query + mediaNode.append(nodes) - if (options.filter && !options.filter(stmt.uri)) { - // rejected by filter - return + stmt.type = "media" + stmt.node = mediaNode + delete stmt.nodes } - - return resolveImportId(result, stmt, options, state) - }) - }, Promise.resolve()) - }) - .then(() => { - const imports = [] - const bundle = [] - - // squash statements and their children - statements.forEach(stmt => { - if (stmt.type === "import") { - if (stmt.children) { - stmt.children.forEach((child, index) => { - if (child.type === "import") imports.push(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 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: file, - parent: sourceFile, - }) - }) + } - return Promise.all( - resolved.map(file => { - return loadImportContent(result, stmt, file, options, state) + function applyStyles(bundle, styles) { + styles.nodes = [] + + // Strip additional statements. + bundle.forEach(stmt => { + if (stmt.type === "import") { + stmt.node.parent = undefined + styles.append(stmt.node) + } else if (stmt.type === "media") { + stmt.node.parent = undefined + styles.append(stmt.node) + } else if (stmt.type === "nodes") { + stmt.nodes.forEach(node => { + node.parent = undefined + styles.append(node) + }) + } }) - ) - }) - .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 = stmt.media - 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 - } + function parseStyles(result, styles, options, state, media) { + 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 || []) + + // 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(() => { + const imports = [] + const bundle = [] + + // squash statements and their children + statements.forEach(stmt => { + if (stmt.type === "import") { + if (stmt.children) { + stmt.children.forEach((child, index) => { + if (child.type === "import") imports.push(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 Promise.resolve(options.load(filename, options)).then(content => { - if (content.trim() === "") { - result.warn(`${filename} is empty`, { node: atRule }) - return - } + return imports.concat(bundle) + }) + } - // skip previous imported files not containing @import rules - if (state.hashFiles[content] && state.hashFiles[content][media]) return + 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: file, + parent: sourceFile, + }) + }) - return processContent(result, content, filename, options).then( - importedResult => { - const styles = importedResult.root - result.messages = result.messages.concat(importedResult.messages) + 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 = stmt.media 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 + // 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 } - // recursion: import @import from imported file - return parseStyles(result, styles, options, state, media) + 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).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) + } + ) + } + ) } - ) - }) + }, + } } -module.exports = postcss.plugin("postcss-import", AtImport) +AtImport.postcss = true + +module.exports = AtImport diff --git a/package.json b/package.json index 1ad0a695..177c61ff 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "node": ">=10.0.0" }, "dependencies": { - "postcss": "^7.0.1", "postcss-value-parser": "^3.2.3", "read-cache": "^1.0.0", "resolve": "^1.1.7" @@ -32,10 +31,14 @@ "eslint-config-i-am-meticulous": "^11.0.0", "eslint-plugin-import": "^2.17.1", "eslint-plugin-prettier": "^3.0.0", + "postcss": "^8.0.0", "postcss-scss": "^2.0.0", "prettier": "~1.19.1", "sugarss": "^2.0.0" }, + "peerDependencies": { + "postcss": "^8.0.0" + }, "scripts": { "ci": "eslint . && ava", "lint": "eslint . --fix", diff --git a/test/import.js b/test/import.js index ad36328c..56c6250c 100644 --- a/test/import.js +++ b/test/import.js @@ -65,7 +65,12 @@ test("should contain a correct sourcemap", t => { .then(result => { t.is( result.map.toString(), - readFileSync("test/sourcemap/out.css.map", "utf8").trim() + readFileSync( + process.platform === "win32" + ? "test/sourcemap/out.css.win.map" + : "test/sourcemap/out.css.map", + "utf8" + ).trim() ) }) }) diff --git a/test/sourcemap/out.css.win.map b/test/sourcemap/out.css.win.map new file mode 100644 index 00000000..fa8890c4 --- /dev/null +++ b/test/sourcemap/out.css.win.map @@ -0,0 +1 @@ +{"version":3,"sources":["test/sourcemap/imported.css","test/sourcemap/in.css"],"names":[],"mappings":"AAAA;EACE,gBAAgB;AAClB;;ACAA;EACE,UAAU;AACZ","file":"test\\sourcemap\\in.css","sourcesContent":["html {\n background: blue;\n}\n","@import \"imported.css\";\n\nbody {\n color: red;\n}\n"]}