diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4501830
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,26 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10.27.0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+
+ - run: pnpm install --frozen-lockfile
+
+ - run: pnpm run lint
+
+ - run: pnpm test
diff --git a/.gitignore b/.gitignore
index e51b913..009c263 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,10 @@ build/
build-standalone-report/
storybook-build/
+dist/
+
+tests/fixtures-output/
+
# Covers JetBrains IDE PyCharm
.idea/
diff --git a/WARP.md b/WARP.md
new file mode 100644
index 0000000..a8c8242
--- /dev/null
+++ b/WARP.md
@@ -0,0 +1,131 @@
+# WARP.md
+
+This file provides guidance to WARP (warp.dev) when working with code in this repository.
+
+## Commands
+
+### Environment and setup
+
+- Use Node.js version **18 or newer** (see `package.json` `engines.node`).
+- Install dependencies with your preferred package manager, for example:
+ - `pnpm install`
+
+### Build and type-check
+
+- `pnpm run build` — compile TypeScript in `src/` to CommonJS JavaScript in `dist/` via `tsc` and `tsconfig.json`.
+- `pnpm run clean` — remove the `dist/` directory before a fresh build.
+
+### Linting and formatting
+
+- `pnpm run lint` — run ESLint across the project.
+- `pnpm run format` — format the codebase with Prettier.
+
+### Tests
+
+- `pnpm test` — run the Jest test suite (Node test environment, tests under `tests/**/*.test.ts`).
+- Run a single test file (example using the existing test file):
+ - `pnpm jest tests/css-layering-plugin.integration.test.ts`
+ - or `pnpm test -- tests/css-layering-plugin.integration.test.ts`
+
+## Architecture overview
+
+### High-level structure
+
+This repository implements a **webpack plugin and loader** that wrap CSS (and Sass) modules in named CSS cascade layers, driven by glob patterns. The project is written in TypeScript and compiled to JavaScript in `dist/` for publishing.
+
+Key pieces:
+- `src/index.ts` — main webpack plugin implementation (`CSSLayeringPlugin`).
+- `src/loader.ts` — webpack loader that wraps matched stylesheets in `@layer` blocks.
+- `tests/css-layering-plugin.integration.test.ts` — Jest integration tests covering the plugin's HTML injection behavior using real webpack compilations.
+- `tests/css-layering-plugin.css.integration.test.ts` — Jest integration tests covering CSS transformation with various layer configurations.
+- `jest.config.js` — Jest configuration.
+- `tsconfig.json` — TypeScript compiler configuration targeting `dist/`.
+
+### CSSLayeringPlugin (`src/index.ts`)
+
+The `CSSLayeringPlugin` class is the primary integration point with webpack and `html-webpack-plugin`:
+
+- **Options and validation**
+ - Accepts an `Options` object with:
+ - `layers: Layer[]` (required, shared with the loader's `Layer` type).
+ - `nonce?: string` — optional CSP nonce for injected ``,
+ );
+ });
+
+ it("injects link tag with default publicPath when not provided", async () => {
+ const outputName = "link-default-public-path";
+
+ const compiler = createCompiler("basic", outputName, {
+ layers,
+ injectOrderAs: "link",
+ });
+
+ await runCompiler(compiler);
+
+ const { outDir } = getPaths("basic", outputName);
+ const html = await fs.readFile(path.join(outDir, "index.html"), "utf8");
+
+ expect(html).toContain(
+ ``,
+ );
+ });
+
+ it("does not inject order or emit asset when injectOrderAs='none'", async () => {
+ const outputName = "none";
+ const compiler = createCompiler("basic", outputName, {
+ layers,
+ injectOrderAs: "none",
+ });
+
+ const stats = await runCompiler(compiler);
+
+ const { outDir } = getPaths("basic", outputName);
+ const html = await fs.readFile(path.join(outDir, "index.html"), "utf8");
+
+ expect(html).not.toContain("@layer base, components;");
+ expect(html).not.toContain(LAYER_ASSET_PATH);
+
+ // The layer asset should not be present in the compilation when injectOrderAs is 'none'
+ expect(stats.compilation.assets[LAYER_ASSET_PATH]).toBeUndefined();
+ });
+});
diff --git a/tests/fixtures/basic/entry.ts b/tests/fixtures/basic/entry.ts
new file mode 100644
index 0000000..6caa6c8
--- /dev/null
+++ b/tests/fixtures/basic/entry.ts
@@ -0,0 +1,3 @@
+export function main() {
+ // Intentionally empty: we just need an entry for HtmlWebpackPlugin
+}
diff --git a/tests/fixtures/basic/index.html b/tests/fixtures/basic/index.html
new file mode 100644
index 0000000..3a97938
--- /dev/null
+++ b/tests/fixtures/basic/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Layering Plugin Test
+
+
+
+
+
diff --git a/tests/fixtures/css-basic/entry.ts b/tests/fixtures/css-basic/entry.ts
new file mode 100644
index 0000000..ba6c61a
--- /dev/null
+++ b/tests/fixtures/css-basic/entry.ts
@@ -0,0 +1,5 @@
+import "./styles.css";
+
+export function main() {
+ // Entry is only here to make webpack process styles.css
+}
diff --git a/tests/fixtures/css-basic/expected/styles.css b/tests/fixtures/css-basic/expected/styles.css
new file mode 100644
index 0000000..a857cc1
--- /dev/null
+++ b/tests/fixtures/css-basic/expected/styles.css
@@ -0,0 +1,10 @@
+@use "sass:color";
+@use "my:module";
+@layer components {
+ .button {
+ color: red;
+}
+body {
+ margin: 0;
+}
+}
diff --git a/tests/fixtures/css-basic/index.html b/tests/fixtures/css-basic/index.html
new file mode 100644
index 0000000..e9e4423
--- /dev/null
+++ b/tests/fixtures/css-basic/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Layering Plugin CSS Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-basic/styles.css b/tests/fixtures/css-basic/styles.css
new file mode 100644
index 0000000..c5dc67f
--- /dev/null
+++ b/tests/fixtures/css-basic/styles.css
@@ -0,0 +1,8 @@
+@use "sass:color";
+.button {
+ color: red;
+}
+@use "my:module";
+body {
+ margin: 0;
+}
diff --git a/tests/fixtures/css-exclude/entry.ts b/tests/fixtures/css-exclude/entry.ts
new file mode 100644
index 0000000..6601398
--- /dev/null
+++ b/tests/fixtures/css-exclude/entry.ts
@@ -0,0 +1,6 @@
+import "./styles.css";
+import "./styles.ignore.css";
+
+export function main() {
+ // Entry is only here to make webpack process CSS files
+}
diff --git a/tests/fixtures/css-exclude/expected/styles.css b/tests/fixtures/css-exclude/expected/styles.css
new file mode 100644
index 0000000..645d2d0
--- /dev/null
+++ b/tests/fixtures/css-exclude/expected/styles.css
@@ -0,0 +1,5 @@
+@layer components {
+ .included {
+ color: green;
+ }
+}
diff --git a/tests/fixtures/css-exclude/expected/styles.ignore.css b/tests/fixtures/css-exclude/expected/styles.ignore.css
new file mode 100644
index 0000000..dcc3389
--- /dev/null
+++ b/tests/fixtures/css-exclude/expected/styles.ignore.css
@@ -0,0 +1,3 @@
+.ignored {
+ color: blue;
+}
diff --git a/tests/fixtures/css-exclude/index.html b/tests/fixtures/css-exclude/index.html
new file mode 100644
index 0000000..324cb2e
--- /dev/null
+++ b/tests/fixtures/css-exclude/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Exclude Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-exclude/styles.css b/tests/fixtures/css-exclude/styles.css
new file mode 100644
index 0000000..e70e8cf
--- /dev/null
+++ b/tests/fixtures/css-exclude/styles.css
@@ -0,0 +1,3 @@
+.included {
+ color: green;
+}
diff --git a/tests/fixtures/css-exclude/styles.ignore.css b/tests/fixtures/css-exclude/styles.ignore.css
new file mode 100644
index 0000000..dcc3389
--- /dev/null
+++ b/tests/fixtures/css-exclude/styles.ignore.css
@@ -0,0 +1,3 @@
+.ignored {
+ color: blue;
+}
diff --git a/tests/fixtures/css-multi-file/entry.ts b/tests/fixtures/css-multi-file/entry.ts
new file mode 100644
index 0000000..21a0e78
--- /dev/null
+++ b/tests/fixtures/css-multi-file/entry.ts
@@ -0,0 +1,6 @@
+import "./one.css";
+import "./two.css";
+
+export function main() {
+ // Entry is only here to make webpack process both CSS files
+}
diff --git a/tests/fixtures/css-multi-file/expected/one.css b/tests/fixtures/css-multi-file/expected/one.css
new file mode 100644
index 0000000..d7a180a
--- /dev/null
+++ b/tests/fixtures/css-multi-file/expected/one.css
@@ -0,0 +1,5 @@
+@layer shared {
+ .one {
+ color: red;
+ }
+}
diff --git a/tests/fixtures/css-multi-file/expected/two.css b/tests/fixtures/css-multi-file/expected/two.css
new file mode 100644
index 0000000..5e71372
--- /dev/null
+++ b/tests/fixtures/css-multi-file/expected/two.css
@@ -0,0 +1,5 @@
+@layer shared {
+ .two {
+ color: blue;
+ }
+}
diff --git a/tests/fixtures/css-multi-file/index.html b/tests/fixtures/css-multi-file/index.html
new file mode 100644
index 0000000..029b694
--- /dev/null
+++ b/tests/fixtures/css-multi-file/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Multi-File Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-multi-file/one.css b/tests/fixtures/css-multi-file/one.css
new file mode 100644
index 0000000..7ee3e9d
--- /dev/null
+++ b/tests/fixtures/css-multi-file/one.css
@@ -0,0 +1,3 @@
+.one {
+ color: red;
+}
diff --git a/tests/fixtures/css-multi-file/two.css b/tests/fixtures/css-multi-file/two.css
new file mode 100644
index 0000000..68036db
--- /dev/null
+++ b/tests/fixtures/css-multi-file/two.css
@@ -0,0 +1,3 @@
+.two {
+ color: blue;
+}
diff --git a/tests/fixtures/css-multi-layers/components.css b/tests/fixtures/css-multi-layers/components.css
new file mode 100644
index 0000000..1b837da
--- /dev/null
+++ b/tests/fixtures/css-multi-layers/components.css
@@ -0,0 +1,3 @@
+.button {
+ padding: 4px;
+}
diff --git a/tests/fixtures/css-multi-layers/entry.ts b/tests/fixtures/css-multi-layers/entry.ts
new file mode 100644
index 0000000..fea83a4
--- /dev/null
+++ b/tests/fixtures/css-multi-layers/entry.ts
@@ -0,0 +1,6 @@
+import "./reset.css";
+import "./components.css";
+
+export function main() {
+ // Entry is only here to make webpack process CSS files
+}
diff --git a/tests/fixtures/css-multi-layers/expected/components.css b/tests/fixtures/css-multi-layers/expected/components.css
new file mode 100644
index 0000000..06349e7
--- /dev/null
+++ b/tests/fixtures/css-multi-layers/expected/components.css
@@ -0,0 +1,5 @@
+@layer components {
+ .button {
+ padding: 4px;
+ }
+}
diff --git a/tests/fixtures/css-multi-layers/expected/reset.css b/tests/fixtures/css-multi-layers/expected/reset.css
new file mode 100644
index 0000000..aadbdb3
--- /dev/null
+++ b/tests/fixtures/css-multi-layers/expected/reset.css
@@ -0,0 +1,5 @@
+@layer base {
+ body {
+ margin: 0;
+ }
+}
diff --git a/tests/fixtures/css-multi-layers/index.html b/tests/fixtures/css-multi-layers/index.html
new file mode 100644
index 0000000..4b6a288
--- /dev/null
+++ b/tests/fixtures/css-multi-layers/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Multi-layers Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-multi-layers/reset.css b/tests/fixtures/css-multi-layers/reset.css
new file mode 100644
index 0000000..293d3b1
--- /dev/null
+++ b/tests/fixtures/css-multi-layers/reset.css
@@ -0,0 +1,3 @@
+body {
+ margin: 0;
+}
diff --git a/tests/fixtures/css-multi-match/entry.ts b/tests/fixtures/css-multi-match/entry.ts
new file mode 100644
index 0000000..ba6c61a
--- /dev/null
+++ b/tests/fixtures/css-multi-match/entry.ts
@@ -0,0 +1,5 @@
+import "./styles.css";
+
+export function main() {
+ // Entry is only here to make webpack process styles.css
+}
diff --git a/tests/fixtures/css-multi-match/expected/styles.css b/tests/fixtures/css-multi-match/expected/styles.css
new file mode 100644
index 0000000..5dbed40
--- /dev/null
+++ b/tests/fixtures/css-multi-match/expected/styles.css
@@ -0,0 +1,5 @@
+@layer base {
+ .multi {
+ color: red;
+ }
+}
diff --git a/tests/fixtures/css-multi-match/index.html b/tests/fixtures/css-multi-match/index.html
new file mode 100644
index 0000000..e8d35d1
--- /dev/null
+++ b/tests/fixtures/css-multi-match/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Multi-Match Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-multi-match/styles.css b/tests/fixtures/css-multi-match/styles.css
new file mode 100644
index 0000000..aee5a09
--- /dev/null
+++ b/tests/fixtures/css-multi-match/styles.css
@@ -0,0 +1,3 @@
+.multi {
+ color: red;
+}
diff --git a/tests/fixtures/css-no-match/entry.ts b/tests/fixtures/css-no-match/entry.ts
new file mode 100644
index 0000000..9f08f14
--- /dev/null
+++ b/tests/fixtures/css-no-match/entry.ts
@@ -0,0 +1,5 @@
+import "./plain.css";
+
+export function main() {
+ // Entry is only here to make webpack process CSS files
+}
diff --git a/tests/fixtures/css-no-match/expected/plain.css b/tests/fixtures/css-no-match/expected/plain.css
new file mode 100644
index 0000000..c7f1aec
--- /dev/null
+++ b/tests/fixtures/css-no-match/expected/plain.css
@@ -0,0 +1,3 @@
+.plain {
+ border: 1px solid black;
+}
diff --git a/tests/fixtures/css-no-match/index.html b/tests/fixtures/css-no-match/index.html
new file mode 100644
index 0000000..a8ac8f4
--- /dev/null
+++ b/tests/fixtures/css-no-match/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS No Match Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-no-match/plain.css b/tests/fixtures/css-no-match/plain.css
new file mode 100644
index 0000000..c7f1aec
--- /dev/null
+++ b/tests/fixtures/css-no-match/plain.css
@@ -0,0 +1,3 @@
+.plain {
+ border: 1px solid black;
+}
diff --git a/tests/fixtures/css-preexisting-layer/entry.ts b/tests/fixtures/css-preexisting-layer/entry.ts
new file mode 100644
index 0000000..2978a22
--- /dev/null
+++ b/tests/fixtures/css-preexisting-layer/entry.ts
@@ -0,0 +1,5 @@
+import "./styles.css";
+
+export function main() {
+ // Entry is only here to make webpack process CSS files
+}
diff --git a/tests/fixtures/css-preexisting-layer/expected/styles.css b/tests/fixtures/css-preexisting-layer/expected/styles.css
new file mode 100644
index 0000000..9c0d055
--- /dev/null
+++ b/tests/fixtures/css-preexisting-layer/expected/styles.css
@@ -0,0 +1,5 @@
+@layer components {
+ .content {
+ font-size: 14px;
+ }
+}
diff --git a/tests/fixtures/css-preexisting-layer/index.html b/tests/fixtures/css-preexisting-layer/index.html
new file mode 100644
index 0000000..65ce561
--- /dev/null
+++ b/tests/fixtures/css-preexisting-layer/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Preexisting Layer Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-preexisting-layer/styles.css b/tests/fixtures/css-preexisting-layer/styles.css
new file mode 100644
index 0000000..60ba41a
--- /dev/null
+++ b/tests/fixtures/css-preexisting-layer/styles.css
@@ -0,0 +1,3 @@
+.content {
+ font-size: 14px;
+}
diff --git a/tests/fixtures/css-scss-complex/entry.ts b/tests/fixtures/css-scss-complex/entry.ts
new file mode 100644
index 0000000..cb2bbd3
--- /dev/null
+++ b/tests/fixtures/css-scss-complex/entry.ts
@@ -0,0 +1,5 @@
+import "./styles-complex.scss";
+
+export function main() {
+ // Entry is only here to make webpack process complex SCSS file
+}
diff --git a/tests/fixtures/css-scss-complex/expected/styles-complex.scss b/tests/fixtures/css-scss-complex/expected/styles-complex.scss
new file mode 100644
index 0000000..b91e416
--- /dev/null
+++ b/tests/fixtures/css-scss-complex/expected/styles-complex.scss
@@ -0,0 +1,50 @@
+@use "sass:color";
+@use "sass:math";
+@layer components {
+ // Global comment about the theme
+
+ $primary: #0d6efd;
+ $secondary: #6c757d;
+
+ %button-base {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ }
+
+ .button {
+ @extend %button-base;
+
+ &--primary {
+ $primary-dark: color.adjust($primary, $lightness: -5%);
+
+ color: $primary-dark;
+
+ &:hover {
+ color: color.scale($primary-dark, $lightness: 5%);
+ }
+ }
+
+ &--secondary {
+ color: $secondary;
+ }
+ }
+
+ .container {
+ $columns: 3;
+
+ width: math.div(100%, $columns);
+
+ @media (min-width: 768px) {
+ max-width: 720px;
+
+ .inner {
+ padding: 1rem;
+
+ &__title {
+ font-weight: bold;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/css-scss-complex/index.html b/tests/fixtures/css-scss-complex/index.html
new file mode 100644
index 0000000..12cca06
--- /dev/null
+++ b/tests/fixtures/css-scss-complex/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS SCSS Complex Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-scss-complex/styles-complex.scss b/tests/fixtures/css-scss-complex/styles-complex.scss
new file mode 100644
index 0000000..07e57da
--- /dev/null
+++ b/tests/fixtures/css-scss-complex/styles-complex.scss
@@ -0,0 +1,49 @@
+// Global comment about the theme
+@use "sass:color";
+
+$primary: #0d6efd;
+$secondary: #6c757d;
+
+%button-base {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+}
+
+.button {
+ @extend %button-base;
+
+ &--primary {
+ $primary-dark: color.adjust($primary, $lightness: -5%);
+
+ color: $primary-dark;
+
+ &:hover {
+ color: color.scale($primary-dark, $lightness: 5%);
+ }
+ }
+
+ &--secondary {
+ color: $secondary;
+ }
+}
+
+@use "sass:math";
+
+.container {
+ $columns: 3;
+
+ width: math.div(100%, $columns);
+
+ @media (min-width: 768px) {
+ max-width: 720px;
+
+ .inner {
+ padding: 1rem;
+
+ &__title {
+ font-weight: bold;
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/css-scss/entry.ts b/tests/fixtures/css-scss/entry.ts
new file mode 100644
index 0000000..5432925
--- /dev/null
+++ b/tests/fixtures/css-scss/entry.ts
@@ -0,0 +1,5 @@
+import "./styles.scss";
+
+export function main() {
+ // Entry is only here to make webpack process SCSS files
+}
diff --git a/tests/fixtures/css-scss/expected/styles.scss b/tests/fixtures/css-scss/expected/styles.scss
new file mode 100644
index 0000000..4e3cc60
--- /dev/null
+++ b/tests/fixtures/css-scss/expected/styles.scss
@@ -0,0 +1,6 @@
+@use "sass:color";
+@layer components {
+ .button {
+ color: red;
+ }
+}
diff --git a/tests/fixtures/css-scss/index.html b/tests/fixtures/css-scss/index.html
new file mode 100644
index 0000000..a082202
--- /dev/null
+++ b/tests/fixtures/css-scss/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS SCSS Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-scss/styles.scss b/tests/fixtures/css-scss/styles.scss
new file mode 100644
index 0000000..8d62828
--- /dev/null
+++ b/tests/fixtures/css-scss/styles.scss
@@ -0,0 +1,4 @@
+@use "sass:color";
+.button {
+ color: red;
+}
diff --git a/tests/fixtures/css-use-ordering/entry.ts b/tests/fixtures/css-use-ordering/entry.ts
new file mode 100644
index 0000000..2978a22
--- /dev/null
+++ b/tests/fixtures/css-use-ordering/entry.ts
@@ -0,0 +1,5 @@
+import "./styles.css";
+
+export function main() {
+ // Entry is only here to make webpack process CSS files
+}
diff --git a/tests/fixtures/css-use-ordering/expected/styles.css b/tests/fixtures/css-use-ordering/expected/styles.css
new file mode 100644
index 0000000..457744f
--- /dev/null
+++ b/tests/fixtures/css-use-ordering/expected/styles.css
@@ -0,0 +1,12 @@
+@use "sass:color";
+@use "my:module";
+@layer components {
+ /* top comment */
+ .main {
+ color: red;
+ }
+ // mid comment
+ .footer {
+ margin: 0;
+ }
+}
diff --git a/tests/fixtures/css-use-ordering/index.html b/tests/fixtures/css-use-ordering/index.html
new file mode 100644
index 0000000..bd59824
--- /dev/null
+++ b/tests/fixtures/css-use-ordering/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ CSS Use Ordering Fixture
+
+
+
+
+
diff --git a/tests/fixtures/css-use-ordering/styles.css b/tests/fixtures/css-use-ordering/styles.css
new file mode 100644
index 0000000..8470713
--- /dev/null
+++ b/tests/fixtures/css-use-ordering/styles.css
@@ -0,0 +1,10 @@
+/* top comment */
+@use "sass:color";
+.main {
+ color: red;
+}
+// mid comment
+@use "my:module";
+.footer {
+ margin: 0;
+}
diff --git a/tests/helpers/webpack.ts b/tests/helpers/webpack.ts
new file mode 100644
index 0000000..24d418d
--- /dev/null
+++ b/tests/helpers/webpack.ts
@@ -0,0 +1,90 @@
+import path from "node:path";
+import webpack, {
+ type Compiler,
+ type Configuration,
+ type Stats,
+} from "webpack";
+import HtmlWebpackPlugin from "html-webpack-plugin";
+import { CSSLayeringPlugin } from "../../dist";
+
+type PluginOptions = ConstructorParameters[0];
+
+export function getPaths(fixtureName: string, outputName: string) {
+ const fixturesDir = path.resolve(__dirname, "../fixtures", fixtureName);
+ const outDir = path.resolve(
+ __dirname,
+ "../fixtures-output",
+ `${fixtureName}-${outputName}`,
+ );
+
+ return { fixturesDir, outDir };
+}
+
+export function createCompiler(
+ fixtureName: string,
+ outputName: string,
+ pluginOptions: PluginOptions,
+ extraConfig: Configuration = {},
+): Compiler {
+ const { fixturesDir, outDir } = getPaths(fixtureName, outputName);
+
+ const baseConfig: Configuration = {
+ mode: "production",
+ devtool: false,
+ context: fixturesDir,
+ entry: path.join(fixturesDir, "entry.ts"),
+ output: {
+ path: outDir,
+ filename: "bundle.js",
+ publicPath: "",
+ },
+ module: {
+ rules: [],
+ },
+ plugins: [
+ new CSSLayeringPlugin(pluginOptions),
+ new HtmlWebpackPlugin({
+ template: path.join(fixturesDir, "index.html"),
+ filename: "index.html",
+ }),
+ ],
+ };
+
+ return webpack({ ...baseConfig, ...extraConfig });
+}
+
+export function runCompiler(compiler: Compiler): Promise {
+ return new Promise((resolve, reject) => {
+ compiler.run((err, stats) => {
+ compiler.close(() => {});
+
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ if (!stats) {
+ reject(new Error("No stats returned"));
+ return;
+ }
+
+ if (stats.hasErrors()) {
+ reject(new Error(stats.toString("errors-only")));
+ return;
+ }
+
+ resolve(stats);
+ });
+ });
+}
+
+export function getAssetSource(
+ stats: Stats,
+ filename: string,
+): string | undefined {
+ const asset = stats.compilation.assets[filename];
+ if (!asset) return undefined;
+
+ const source = asset.source();
+ return typeof source === "string" ? source : source.toString();
+}
diff --git a/tsconfig.test.json b/tsconfig.test.json
new file mode 100644
index 0000000..172dae2
--- /dev/null
+++ b/tsconfig.test.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "esModuleInterop": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "types": ["node", "jest"]
+ },
+ "include": ["tests/**/*"]
+}