Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
33 changes: 24 additions & 9 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -34,8 +40,8 @@ export const OPTIONS_SCHEMA: JSONSchema7 = {
};

export type Layer = {
path?: string;
exclude?: string;
path?: string | string[];
exclude?: string | string[];
name: string;
};

Expand All @@ -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);
}
}
Expand Down
104 changes: 53 additions & 51 deletions tests/css-layering-plugin.css.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,116 +8,115 @@ 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
{
name: "single layer applied to multiple files",
fixture: "css-multi-file",
outputName: "css",
cssFile: "one.css",
cssFiles: ["one.css", "two.css"],
layers: [{ name: "shared", path: "**/*.css" }],
},
{
fixture: "css-multi-file",
name: "array path patterns",
fixture: "css-array-path",
outputName: "css",
cssFile: "two.css",
layers: [{ name: "shared", path: "**/*.css" }],
cssFiles: ["button.css", "input.scss"],
layers: [
{ name: "components", path: ["**/button.css", "**/input.scss"] },
],
},
{
name: "array exclude patterns",
fixture: "css-array-exclude",
outputName: "css",
cssFiles: ["app.css", "app.test.css", "app.spec.css"],
layers: [
{
name: "components",
path: "**/*.css",
exclude: ["**/*.test.css", "**/*.spec.css"],
},
],
},
] 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,
Expand All @@ -141,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));
}
});
}
});
4 changes: 4 additions & 0 deletions tests/fixtures/css-array-exclude/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.app {
width: 100%;
height: 100vh;
}
3 changes: 3 additions & 0 deletions tests/fixtures/css-array-exclude/app.spec.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.spec {
color: orange;
}
3 changes: 3 additions & 0 deletions tests/fixtures/css-array-exclude/app.test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.test {
color: green;
}
7 changes: 7 additions & 0 deletions tests/fixtures/css-array-exclude/entry.ts
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions tests/fixtures/css-array-exclude/expected/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@layer components {
.app {
width: 100%;
height: 100vh;
}
}
3 changes: 3 additions & 0 deletions tests/fixtures/css-array-exclude/expected/app.spec.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.spec {
color: orange;
}
3 changes: 3 additions & 0 deletions tests/fixtures/css-array-exclude/expected/app.test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.test {
color: green;
}
10 changes: 10 additions & 0 deletions tests/fixtures/css-array-exclude/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Webpack App</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
4 changes: 4 additions & 0 deletions tests/fixtures/css-array-path/button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.button {
padding: 10px;
background: blue;
}
6 changes: 6 additions & 0 deletions tests/fixtures/css-array-path/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import "./button.css";
import "./input.scss";

export function main() {
// Entry is only here to make webpack process CSS files
}
6 changes: 6 additions & 0 deletions tests/fixtures/css-array-path/expected/button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@layer components {
.button {
padding: 10px;
background: blue;
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/css-array-path/expected/input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@layer components {
.input {
border: 1px solid gray;
padding: 5px;
}
}
10 changes: 10 additions & 0 deletions tests/fixtures/css-array-path/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Webpack App</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
4 changes: 4 additions & 0 deletions tests/fixtures/css-array-path/input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.input {
border: 1px solid gray;
padding: 5px;
}