diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ac064b2..111b2c6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x, 24.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc9d28..4c2d1b3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 16.1.1 / 2025-06-17 + +- Fix incorrect cascade layer order when some resources can not be inlined ([#567](https://github.com/postcss/postcss-import/issues/567), [#574](https://github.com/postcss/postcss-import/pull/574)) + # 16.1.0 / 2024-03-20 - Allow bundling URLs with fragments (useful for Vite users) ([#560](https://github.com/postcss/postcss-import/issues/560), [#561](https://github.com/postcss/postcss-import/pull/561)) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0398af2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,27 @@ +"use strict" + +const problems = require("eslint-config-problems") +const prettier = require("eslint-plugin-prettier") +const globals = require("globals") + +module.exports = [ + problems, + { + languageOptions: { + globals: { + ...globals.node, + }, + sourceType: "commonjs", + }, + plugins: { prettier }, + rules: { + "prettier/prettier": [ + "error", + { + semi: false, + arrowParens: "avoid", + }, + ], + }, + }, +] diff --git a/lib/apply-conditions.js b/lib/apply-conditions.js index 76a8475..2005c6b 100644 --- a/lib/apply-conditions.js +++ b/lib/apply-conditions.js @@ -3,12 +3,38 @@ const base64EncodedConditionalImport = require("./base64-encoded-import") module.exports = function applyConditions(bundle, atRule) { - bundle.forEach(stmt => { + const firstImportStatementIndex = bundle.findIndex( + stmt => stmt.type === "import", + ) + const lastImportStatementIndex = bundle.findLastIndex( + stmt => stmt.type === "import", + ) + + bundle.forEach((stmt, index) => { + if (stmt.type === "charset" || stmt.type === "warning") { + return + } + if ( - stmt.type === "charset" || - stmt.type === "warning" || - !stmt.conditions?.length + stmt.type === "layer" && + ((index < lastImportStatementIndex && stmt.conditions?.length) || + (index > firstImportStatementIndex && index < lastImportStatementIndex)) ) { + stmt.type = "import" + stmt.node = stmt.node.clone({ + name: "import", + params: base64EncodedConditionalImport( + `'data:text/css;base64,${Buffer.from(stmt.node.toString()).toString( + "base64", + )}'`, + stmt.conditions, + ), + }) + + return + } + + if (!stmt.conditions?.length) { return } @@ -20,8 +46,15 @@ module.exports = function applyConditions(bundle, atRule) { return } - const { nodes } = stmt - const { parent } = nodes[0] + let nodes + let parent + if (stmt.type === "layer") { + nodes = [stmt.node] + parent = stmt.node.parent + } else { + nodes = stmt.nodes + parent = nodes[0].parent + } const atRules = [] diff --git a/lib/apply-styles.js b/lib/apply-styles.js index ca961b1..a92e7ff 100644 --- a/lib/apply-styles.js +++ b/lib/apply-styles.js @@ -5,7 +5,7 @@ module.exports = function applyStyles(bundle, styles) { // Strip additional statements. bundle.forEach(stmt => { - if (["charset", "import"].includes(stmt.type)) { + if (["charset", "import", "layer"].includes(stmt.type)) { stmt.node.parent = undefined styles.append(stmt.node) } else if (stmt.type === "nodes") { diff --git a/lib/base64-encoded-import.js b/lib/base64-encoded-import.js index a928c62..5e49779 100644 --- a/lib/base64-encoded-import.js +++ b/lib/base64-encoded-import.js @@ -8,6 +8,8 @@ const formatImportPrelude = require("./format-import-prelude") // To achieve this we create a list of base64 encoded imports, where each import contains a stylesheet with another import. // Each import can define a single group of conditions and a single cascade layer. module.exports = function base64EncodedConditionalImport(prelude, conditions) { + if (!conditions?.length) return prelude + conditions.reverse() const first = conditions.pop() let params = `${prelude} ${formatImportPrelude( diff --git a/lib/parse-statements.js b/lib/parse-statements.js index 788193a..3df0bad 100644 --- a/lib/parse-statements.js +++ b/lib/parse-statements.js @@ -9,6 +9,7 @@ const { stringify } = valueParser module.exports = function parseStatements(result, styles, conditions, from) { const statements = [] let nodes = [] + let encounteredNonImportNodes = false styles.each(node => { let stmt @@ -17,6 +18,14 @@ module.exports = function parseStatements(result, styles, conditions, from) { stmt = parseImport(result, node, conditions, from) else if (node.name === "charset") stmt = parseCharset(result, node, conditions, from) + else if ( + node.name === "layer" && + !encounteredNonImportNodes && + !node.nodes + ) + stmt = parseLayer(result, node, conditions, from) + } else if (node.type !== "comment") { + encounteredNonImportNodes = true } if (stmt) { @@ -233,3 +242,12 @@ function parseImport(result, atRule, conditions, from) { return stmt } + +function parseLayer(result, atRule, conditions, from) { + return { + type: "layer", + node: atRule, + conditions: [...conditions], + from, + } +} diff --git a/lib/parse-styles.js b/lib/parse-styles.js index 9dd43d7..766de9c 100644 --- a/lib/parse-styles.js +++ b/lib/parse-styles.js @@ -33,7 +33,7 @@ async function parseStyles( } let charset - const imports = [] + const beforeBundle = [] const bundle = [] function handleCharset(stmt) { @@ -56,19 +56,24 @@ async function parseStyles( else if (stmt.type === "import") { if (stmt.children) { stmt.children.forEach((child, index) => { - if (child.type === "import") imports.push(child) + if (child.type === "import") beforeBundle.push(child) + else if (child.type === "layer") beforeBundle.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 beforeBundle.push(stmt) + } else if (stmt.type === "layer") { + beforeBundle.push(stmt) } else if (stmt.type === "nodes") { bundle.push(stmt) } }) - return charset ? [charset, ...imports.concat(bundle)] : imports.concat(bundle) + return charset + ? [charset, ...beforeBundle.concat(bundle)] + : beforeBundle.concat(bundle) } async function resolveImportId(result, stmt, options, state, postcss) { diff --git a/package.json b/package.json index 2b5bde2..09b5f2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "postcss-import", - "version": "16.1.0", + "version": "16.1.1", "description": "PostCSS plugin to import CSS files", "keywords": [ "css", @@ -27,14 +27,15 @@ }, "devDependencies": { "ava": "^6.0.0", - "c8": "^9.0.0", - "eslint": "^8.27.0", - "eslint-config-problems": "^8.0.0", + "c8": "^10.0.0", + "eslint": "^9.28.0", + "eslint-config-problems": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "globals": "^16.2.0", "postcss": "^8.0.0", "postcss-scss": "^4.0.0", - "prettier": "~3.2.0", - "sugarss": "^4.0.0" + "prettier": "~3.5.0", + "sugarss": "^5.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -44,23 +45,5 @@ "lint": "eslint . --fix", "pretest": "npm run lint", "test": "c8 ava" - }, - "eslintConfig": { - "extends": "eslint-config-problems", - "env": { - "node": true - }, - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "semi": false, - "arrowParens": "avoid" - } - ] - } } } diff --git a/renovate.json b/renovate.json index a513283..98372bf 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,6 @@ { "extends": [ - "config:base", + "config:recommended", ":preserveSemverRanges", ":label(deps)" ] diff --git a/test/fixtures/imports/layer-followed-by-ignore.css b/test/fixtures/imports/layer-followed-by-ignore.css new file mode 100644 index 0000000..13f9646 --- /dev/null +++ b/test/fixtures/imports/layer-followed-by-ignore.css @@ -0,0 +1,4 @@ +/* a comment */ + +@layer layer-alpha; +@import "http://css"; diff --git a/test/fixtures/imports/layer-only.css b/test/fixtures/imports/layer-only.css new file mode 100644 index 0000000..9dc1acc --- /dev/null +++ b/test/fixtures/imports/layer-only.css @@ -0,0 +1 @@ +@layer layer-beta; diff --git a/test/fixtures/layer-followed-by-ignore-with-conditions.css b/test/fixtures/layer-followed-by-ignore-with-conditions.css new file mode 100644 index 0000000..e5cf2e5 --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-with-conditions.css @@ -0,0 +1,2 @@ +@import "layer-only.css" print; +@import "layer-followed-by-ignore.css" screen; diff --git a/test/fixtures/layer-followed-by-ignore-with-conditions.expected.css b/test/fixtures/layer-followed-by-ignore-with-conditions.expected.css new file mode 100644 index 0000000..3d1a219 --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-with-conditions.expected.css @@ -0,0 +1,6 @@ +@import 'data:text/css;base64,QGxheWVyIGxheWVyLWJldGE=' print; +@import 'data:text/css;base64,QGxheWVyIGxheWVyLWFscGhh' screen; +@import "http://css" screen; +@media screen{ +/* a comment */ +} diff --git a/test/fixtures/layer-followed-by-ignore-without-conditions.css b/test/fixtures/layer-followed-by-ignore-without-conditions.css new file mode 100644 index 0000000..6b48f60 --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-without-conditions.css @@ -0,0 +1,3 @@ +@import "http://css-a"; +@import url("layer-only.css"); +@import "http://css-b"; diff --git a/test/fixtures/layer-followed-by-ignore-without-conditions.expected.css b/test/fixtures/layer-followed-by-ignore-without-conditions.expected.css new file mode 100644 index 0000000..6e8818a --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-without-conditions.expected.css @@ -0,0 +1,3 @@ +@import "http://css-a"; +@import 'data:text/css;base64,QGxheWVyIGxheWVyLWJldGE='; +@import "http://css-b"; diff --git a/test/fixtures/layer-followed-by-ignore.css b/test/fixtures/layer-followed-by-ignore.css new file mode 100644 index 0000000..1c4c793 --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore.css @@ -0,0 +1,2 @@ +@layer layer-alpha; +@import "http://css"; diff --git a/test/fixtures/layer-followed-by-ignore.expected.css b/test/fixtures/layer-followed-by-ignore.expected.css new file mode 100644 index 0000000..1c4c793 --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore.expected.css @@ -0,0 +1,2 @@ +@layer layer-alpha; +@import "http://css"; diff --git a/test/fixtures/layer-statement-with-conditions.css b/test/fixtures/layer-statement-with-conditions.css new file mode 100644 index 0000000..2256df4 --- /dev/null +++ b/test/fixtures/layer-statement-with-conditions.css @@ -0,0 +1 @@ +@import "layer-only.css" print; diff --git a/test/fixtures/layer-statement-with-conditions.expected.css b/test/fixtures/layer-statement-with-conditions.expected.css new file mode 100644 index 0000000..9f57df6 --- /dev/null +++ b/test/fixtures/layer-statement-with-conditions.expected.css @@ -0,0 +1,3 @@ +@media print{ +@layer layer-beta +} diff --git a/test/layer.js b/test/layer.js index c80cc32..52236a6 100644 --- a/test/layer.js +++ b/test/layer.js @@ -33,3 +33,27 @@ test( checkFixture, "layer-duplicate-anonymous-imports-skip", ) + +test( + "should correctly handle layer statements followed by ignored imports", + checkFixture, + "layer-followed-by-ignore", +) + +test( + "should correctly handle layer statements followed by ignored imports in conditional imports", + checkFixture, + "layer-followed-by-ignore-with-conditions", +) + +test( + "should correctly handle layer statements followed by ignored imports in unconditional imports", + checkFixture, + "layer-followed-by-ignore-without-conditions", +) + +test( + "should correctly handle layer statements in conditional imports", + checkFixture, + "layer-statement-with-conditions", +)