From 09e1cd836fcac6e2f2655bd0689ffa5e2a27c83b Mon Sep 17 00:00:00 2001 From: Kresimir Buric Date: Wed, 25 Feb 2026 08:36:07 +0100 Subject: [PATCH 1/2] feat: support array patterns for path and exclude options - Allow path and exclude options to accept string arrays in addition to single strings - Files matching any path pattern will be wrapped in the layer - Files matching any exclude pattern will be excluded from wrapping - Update JSON schema to support both string and array formats - Add comprehensive test coverage for array patterns - Update README with type definitions and examples Co-Authored-By: Oz --- README.md | 22 ++++++++ src/loader.ts | 33 ++++++++---- ...ss-layering-plugin.css.integration.test.ts | 54 +++++++++++++++++++ tests/fixtures/css-array-exclude/app.css | 4 ++ tests/fixtures/css-array-exclude/app.spec.css | 3 ++ tests/fixtures/css-array-exclude/app.test.css | 3 ++ tests/fixtures/css-array-exclude/entry.ts | 7 +++ .../css-array-exclude/expected/app.css | 6 +++ .../css-array-exclude/expected/app.spec.css | 3 ++ .../css-array-exclude/expected/app.test.css | 3 ++ tests/fixtures/css-array-exclude/index.html | 10 ++++ tests/fixtures/css-array-path/button.css | 4 ++ tests/fixtures/css-array-path/entry.ts | 6 +++ .../css-array-path/expected/button.css | 6 +++ .../css-array-path/expected/input.scss | 6 +++ tests/fixtures/css-array-path/index.html | 10 ++++ tests/fixtures/css-array-path/input.scss | 4 ++ 17 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/css-array-exclude/app.css create mode 100644 tests/fixtures/css-array-exclude/app.spec.css create mode 100644 tests/fixtures/css-array-exclude/app.test.css create mode 100644 tests/fixtures/css-array-exclude/entry.ts create mode 100644 tests/fixtures/css-array-exclude/expected/app.css create mode 100644 tests/fixtures/css-array-exclude/expected/app.spec.css create mode 100644 tests/fixtures/css-array-exclude/expected/app.test.css create mode 100644 tests/fixtures/css-array-exclude/index.html create mode 100644 tests/fixtures/css-array-path/button.css create mode 100644 tests/fixtures/css-array-path/entry.ts create mode 100644 tests/fixtures/css-array-path/expected/button.css create mode 100644 tests/fixtures/css-array-path/expected/input.scss create mode 100644 tests/fixtures/css-array-path/index.html create mode 100644 tests/fixtures/css-array-path/input.scss diff --git a/README.md b/README.md index 8f32b5a..a4a1ac1 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,22 @@ module.exports = { ## Options +### `layers` + +```ts +type Layer = { + path?: string | string[]; + exclude?: string | string[]; + name: string; +}; +``` + +An array of layer configurations. Each layer can have: + +- `name` (required): The name of the CSS cascade layer +- `path` (optional): A glob pattern or array of glob patterns to match files that should be wrapped in this layer. If omitted, the layer will only appear in the layer order declaration. +- `exclude` (optional): A glob pattern or array of glob patterns to exclude files from being wrapped in this layer. + ### `injectOrderAs` ```ts @@ -87,6 +103,12 @@ module.exports = { exclude: "**/notification.module.scss", name: "ui-shared", }, + { + // Multiple patterns can be provided as arrays + path: ["**/src/**/*.css", "**/lib/**/*.scss"], + exclude: ["**/*.test.css", "**/*.spec.scss"], + name: "utilities", + }, ], injectOrderAs: "link", publicPath: "/static/css/layers.css", diff --git a/src/loader.ts b/src/loader.ts index 2b48280..d109da6 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -16,12 +16,18 @@ export const OPTIONS_SCHEMA: JSONSchema7 = { description: "All files that are matched with this value using minimatch package will be wrapped with this layer." + "If undefined layer will only be included in layer order declaration (can be used for preexisting layers).", - type: "string", + oneOf: [ + { type: "string" }, + { type: "array", items: { type: "string" } }, + ], }, exclude: { description: "All files matched with this value using minimatch package will be excluded from layer wrapping.", - type: "string", + oneOf: [ + { type: "string" }, + { type: "array", items: { type: "string" } }, + ], }, name: { description: "Name of layer", @@ -34,8 +40,8 @@ export const OPTIONS_SCHEMA: JSONSchema7 = { }; export type Layer = { - path?: string; - exclude?: string; + path?: string | string[]; + exclude?: string | string[]; name: string; }; @@ -47,11 +53,20 @@ function loader(this: LoaderContext<{ layers: Layer[] }>, source: string) { for (const layer of layers) { const { path, name, exclude } = layer; - if ( - path && - minimatch(this.resourcePath, path) && - (exclude === undefined || !minimatch(this.resourcePath, exclude)) - ) { + + const pathMatches = path + ? Array.isArray(path) + ? path.some((pattern) => minimatch(this.resourcePath, pattern)) + : minimatch(this.resourcePath, path) + : false; + + const isExcluded = exclude + ? Array.isArray(exclude) + ? exclude.some((pattern) => minimatch(this.resourcePath, pattern)) + : minimatch(this.resourcePath, exclude) + : false; + + if (pathMatches && !isExcluded) { return wrapSourceInLayer(source, name); } } diff --git a/tests/css-layering-plugin.css.integration.test.ts b/tests/css-layering-plugin.css.integration.test.ts index 0d98bb0..3c23f2b 100644 --- a/tests/css-layering-plugin.css.integration.test.ts +++ b/tests/css-layering-plugin.css.integration.test.ts @@ -112,6 +112,60 @@ describe("CSSLayeringPlugin CSS transformation integration", () => { cssFile: "two.css", layers: [{ name: "shared", path: "**/*.css" }], }, + // Array path patterns: multiple patterns should all be matched + { + fixture: "css-array-path", + outputName: "css", + cssFile: "button.css", + layers: [ + { name: "components", path: ["**/button.css", "**/input.scss"] }, + ], + }, + { + fixture: "css-array-path", + outputName: "css", + cssFile: "input.scss", + layers: [ + { name: "components", path: ["**/button.css", "**/input.scss"] }, + ], + }, + // Array exclude patterns: files matching any exclude pattern should not be wrapped + { + fixture: "css-array-exclude", + outputName: "css", + cssFile: "app.css", + layers: [ + { + name: "components", + path: "**/*.css", + exclude: ["**/*.test.css", "**/*.spec.css"], + }, + ], + }, + { + fixture: "css-array-exclude", + outputName: "css", + cssFile: "app.test.css", + layers: [ + { + name: "components", + path: "**/*.css", + exclude: ["**/*.test.css", "**/*.spec.css"], + }, + ], + }, + { + fixture: "css-array-exclude", + outputName: "css", + cssFile: "app.spec.css", + layers: [ + { + name: "components", + path: "**/*.css", + exclude: ["**/*.test.css", "**/*.spec.css"], + }, + ], + }, ] as const; for (const testCase of cases) { diff --git a/tests/fixtures/css-array-exclude/app.css b/tests/fixtures/css-array-exclude/app.css new file mode 100644 index 0000000..11d21f8 --- /dev/null +++ b/tests/fixtures/css-array-exclude/app.css @@ -0,0 +1,4 @@ +.app { + width: 100%; + height: 100vh; +} diff --git a/tests/fixtures/css-array-exclude/app.spec.css b/tests/fixtures/css-array-exclude/app.spec.css new file mode 100644 index 0000000..210dfdf --- /dev/null +++ b/tests/fixtures/css-array-exclude/app.spec.css @@ -0,0 +1,3 @@ +.spec { + color: orange; +} diff --git a/tests/fixtures/css-array-exclude/app.test.css b/tests/fixtures/css-array-exclude/app.test.css new file mode 100644 index 0000000..e71fd38 --- /dev/null +++ b/tests/fixtures/css-array-exclude/app.test.css @@ -0,0 +1,3 @@ +.test { + color: green; +} diff --git a/tests/fixtures/css-array-exclude/entry.ts b/tests/fixtures/css-array-exclude/entry.ts new file mode 100644 index 0000000..6759c42 --- /dev/null +++ b/tests/fixtures/css-array-exclude/entry.ts @@ -0,0 +1,7 @@ +import "./app.css"; +import "./app.test.css"; +import "./app.spec.css"; + +export function main() { + // Entry is only here to make webpack process CSS files +} diff --git a/tests/fixtures/css-array-exclude/expected/app.css b/tests/fixtures/css-array-exclude/expected/app.css new file mode 100644 index 0000000..48245c4 --- /dev/null +++ b/tests/fixtures/css-array-exclude/expected/app.css @@ -0,0 +1,6 @@ +@layer components { + .app { + width: 100%; + height: 100vh; +} +} diff --git a/tests/fixtures/css-array-exclude/expected/app.spec.css b/tests/fixtures/css-array-exclude/expected/app.spec.css new file mode 100644 index 0000000..210dfdf --- /dev/null +++ b/tests/fixtures/css-array-exclude/expected/app.spec.css @@ -0,0 +1,3 @@ +.spec { + color: orange; +} diff --git a/tests/fixtures/css-array-exclude/expected/app.test.css b/tests/fixtures/css-array-exclude/expected/app.test.css new file mode 100644 index 0000000..e71fd38 --- /dev/null +++ b/tests/fixtures/css-array-exclude/expected/app.test.css @@ -0,0 +1,3 @@ +.test { + color: green; +} diff --git a/tests/fixtures/css-array-exclude/index.html b/tests/fixtures/css-array-exclude/index.html new file mode 100644 index 0000000..afb85bd --- /dev/null +++ b/tests/fixtures/css-array-exclude/index.html @@ -0,0 +1,10 @@ + + + + + Webpack App + + +

Hello world!

+ + diff --git a/tests/fixtures/css-array-path/button.css b/tests/fixtures/css-array-path/button.css new file mode 100644 index 0000000..f1a0cf6 --- /dev/null +++ b/tests/fixtures/css-array-path/button.css @@ -0,0 +1,4 @@ +.button { + padding: 10px; + background: blue; +} diff --git a/tests/fixtures/css-array-path/entry.ts b/tests/fixtures/css-array-path/entry.ts new file mode 100644 index 0000000..5b797c1 --- /dev/null +++ b/tests/fixtures/css-array-path/entry.ts @@ -0,0 +1,6 @@ +import "./button.css"; +import "./input.scss"; + +export function main() { + // Entry is only here to make webpack process CSS files +} diff --git a/tests/fixtures/css-array-path/expected/button.css b/tests/fixtures/css-array-path/expected/button.css new file mode 100644 index 0000000..29af3f2 --- /dev/null +++ b/tests/fixtures/css-array-path/expected/button.css @@ -0,0 +1,6 @@ +@layer components { + .button { + padding: 10px; + background: blue; +} +} diff --git a/tests/fixtures/css-array-path/expected/input.scss b/tests/fixtures/css-array-path/expected/input.scss new file mode 100644 index 0000000..3013ed7 --- /dev/null +++ b/tests/fixtures/css-array-path/expected/input.scss @@ -0,0 +1,6 @@ +@layer components { + .input { + border: 1px solid gray; + padding: 5px; +} +} diff --git a/tests/fixtures/css-array-path/index.html b/tests/fixtures/css-array-path/index.html new file mode 100644 index 0000000..afb85bd --- /dev/null +++ b/tests/fixtures/css-array-path/index.html @@ -0,0 +1,10 @@ + + + + + Webpack App + + +

Hello world!

+ + diff --git a/tests/fixtures/css-array-path/input.scss b/tests/fixtures/css-array-path/input.scss new file mode 100644 index 0000000..8761539 --- /dev/null +++ b/tests/fixtures/css-array-path/input.scss @@ -0,0 +1,4 @@ +.input { + border: 1px solid gray; + padding: 5px; +} From fe32dd2b783a9c9c23ff7c2c0a7101b11d18d88e Mon Sep 17 00:00:00 2001 From: Kresimir Buric Date: Fri, 27 Feb 2026 11:09:06 +0100 Subject: [PATCH 2/2] Refactor tests --- ...ss-layering-plugin.css.integration.test.ts | 128 ++++++------------ 1 file changed, 38 insertions(+), 90 deletions(-) diff --git a/tests/css-layering-plugin.css.integration.test.ts b/tests/css-layering-plugin.css.integration.test.ts index 3c23f2b..46fc699 100644 --- a/tests/css-layering-plugin.css.integration.test.ts +++ b/tests/css-layering-plugin.css.integration.test.ts @@ -8,156 +8,101 @@ function normalizeCss(css: string): string { describe("CSSLayeringPlugin CSS transformation integration", () => { const cases = [ - // Basic case: single CSS file matched by path { + name: "basic CSS file wrapping", fixture: "css-basic", outputName: "css", - cssFile: "styles.css", + cssFiles: ["styles.css"], layers: [{ name: "components", path: "**/styles.css" }], }, - // Exclude behavior: one file wrapped, one left untouched { + name: "exclude pattern", fixture: "css-exclude", outputName: "css", - cssFile: "styles.css", + cssFiles: ["styles.css", "styles.ignore.css"], layers: [ { name: "components", path: "**/*.css", exclude: "**/*.ignore.css" }, ], }, { - fixture: "css-exclude", - outputName: "css", - cssFile: "styles.ignore.css", - layers: [ - { name: "components", path: "**/*.css", exclude: "**/*.ignore.css" }, - ], - }, - // Multiple layers with different paths - { - fixture: "css-multi-layers", - outputName: "css", - cssFile: "reset.css", - layers: [ - { name: "base", path: "**/reset.css" }, - { name: "components", path: "**/components.css" }, - ], - }, - { + name: "multiple layers with different paths", fixture: "css-multi-layers", outputName: "css", - cssFile: "components.css", + cssFiles: ["reset.css", "components.css"], layers: [ { name: "base", path: "**/reset.css" }, { name: "components", path: "**/components.css" }, ], }, - // Layers without path should not be used for wrapping { + name: "preexisting layers without path", fixture: "css-preexisting-layer", outputName: "css", - cssFile: "styles.css", + cssFiles: ["styles.css"], layers: [ { name: "preexisting" }, { name: "components", path: "**/styles.css" }, ], }, - // Non-matching path pattern should leave CSS unchanged { + name: "non-matching path pattern", fixture: "css-no-match", outputName: "css", - cssFile: "plain.css", + cssFiles: ["plain.css"], layers: [{ name: "components", path: "**/styles.css" }], }, - // Use ordering and comments: @use lines hoisted, others wrapped { + name: "@use line hoisting", fixture: "css-use-ordering", outputName: "css", - cssFile: "styles.css", + cssFiles: ["styles.css"], layers: [{ name: "components", path: "**/styles.css" }], }, - // SCSS file handling { + name: "SCSS file handling", fixture: "css-scss", outputName: "css", - cssFile: "styles.scss", + cssFiles: ["styles.scss"], layers: [{ name: "components", path: "**/*.scss" }], }, - // Complex SCSS file with nesting, extends, variables, media queries { + name: "complex SCSS with nesting and variables", fixture: "css-scss-complex", outputName: "css", - cssFile: "styles-complex.scss", + cssFiles: ["styles-complex.scss"], layers: [{ name: "components", path: "**/styles-complex.scss" }], }, - // Multiple layers affecting the same file: first matching layer should be used { + name: "first matching layer wins", fixture: "css-multi-match", outputName: "css", - cssFile: "styles.css", + cssFiles: ["styles.css"], layers: [ { name: "base", path: "**/styles.css" }, { name: "components", path: "**/*.css" }, ], }, - // Single layer applied to multiple files via glob - { - fixture: "css-multi-file", - outputName: "css", - cssFile: "one.css", - layers: [{ name: "shared", path: "**/*.css" }], - }, { + name: "single layer applied to multiple files", fixture: "css-multi-file", outputName: "css", - cssFile: "two.css", + cssFiles: ["one.css", "two.css"], layers: [{ name: "shared", path: "**/*.css" }], }, - // Array path patterns: multiple patterns should all be matched { + name: "array path patterns", fixture: "css-array-path", outputName: "css", - cssFile: "button.css", + cssFiles: ["button.css", "input.scss"], layers: [ { name: "components", path: ["**/button.css", "**/input.scss"] }, ], }, { - fixture: "css-array-path", - outputName: "css", - cssFile: "input.scss", - layers: [ - { name: "components", path: ["**/button.css", "**/input.scss"] }, - ], - }, - // Array exclude patterns: files matching any exclude pattern should not be wrapped - { + name: "array exclude patterns", fixture: "css-array-exclude", outputName: "css", - cssFile: "app.css", - layers: [ - { - name: "components", - path: "**/*.css", - exclude: ["**/*.test.css", "**/*.spec.css"], - }, - ], - }, - { - fixture: "css-array-exclude", - outputName: "css", - cssFile: "app.test.css", - layers: [ - { - name: "components", - path: "**/*.css", - exclude: ["**/*.test.css", "**/*.spec.css"], - }, - ], - }, - { - fixture: "css-array-exclude", - outputName: "css", - cssFile: "app.spec.css", + cssFiles: ["app.css", "app.test.css", "app.spec.css"], layers: [ { name: "components", @@ -169,9 +114,9 @@ describe("CSSLayeringPlugin CSS transformation integration", () => { ] as const; for (const testCase of cases) { - const { fixture, outputName, cssFile, layers } = testCase; + const { name, fixture, outputName, cssFiles, layers } = testCase; - it(`wraps CSS for fixture ${fixture} with configured layers`, async () => { + it(name, async () => { const compiler = createCompiler( fixture, outputName, @@ -195,16 +140,19 @@ describe("CSSLayeringPlugin CSS transformation integration", () => { const { outDir, fixturesDir } = getPaths(fixture, outputName); - const actualCss = await fs.readFile( - path.join(outDir, "css", cssFile), - "utf8", - ); - const expectedCss = await fs.readFile( - path.join(fixturesDir, "expected", cssFile), - "utf8", - ); + // Check all CSS files for this fixture + for (const cssFile of cssFiles) { + const actualCss = await fs.readFile( + path.join(outDir, "css", cssFile), + "utf8", + ); + const expectedCss = await fs.readFile( + path.join(fixturesDir, "expected", cssFile), + "utf8", + ); - expect(normalizeCss(actualCss)).toBe(normalizeCss(expectedCss)); + expect(normalizeCss(actualCss)).toBe(normalizeCss(expectedCss)); + } }); } });