From b31e0a3a1bc3b506933f21e3aec04c5cd94c5c05 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 29 Aug 2026 12:33:24 +0000 Subject: [PATCH 1/2] docs: use the built-in CSS support of webpack in examples Replace `style-loader`/`css-loader`/`mini-css-extract-plugin` in the documentation examples with `experiments.css` and the `css/auto` module type, and cover this setup with tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Y38YLRaBZZKSrV2dRiFpi2 --- README.md | 179 +++++++++++++-------- test/__snapshots__/builtInCss.test.js.snap | 112 +++++++++++++ test/builtInCss.test.js | 124 ++++++++++++++ test/fixtures/builtin-css/index.js | 1 + test/fixtures/builtin-css/jss.js | 1 + test/fixtures/builtin-css/modules.js | 3 + test/fixtures/builtin-css/sss.js | 1 + test/fixtures/builtin-css/style.module.css | 7 + test/fixtures/esparser/runManual.mjs | 6 +- test/helpers/getCssCompiler.js | 55 +++++++ test/helpers/index.js | 1 + 11 files changed, 423 insertions(+), 67 deletions(-) create mode 100644 test/__snapshots__/builtInCss.test.js.snap create mode 100644 test/builtInCss.test.js create mode 100644 test/fixtures/builtin-css/index.js create mode 100644 test/fixtures/builtin-css/jss.js create mode 100644 test/fixtures/builtin-css/modules.js create mode 100644 test/fixtures/builtin-css/sss.js create mode 100644 test/fixtures/builtin-css/style.module.css create mode 100644 test/helpers/getCssCompiler.js diff --git a/README.md b/README.md index a27a5b95..e013c000 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ Then add the loader to your `webpack` configuration. For example: > In the following configuration the plugin [`postcss-preset-env`](https://github.com/csstools/postcss-preset-env) is used, which is not installed by default. +> The examples below use the [built-in CSS support](https://webpack.js.org/configuration/experiments/#experimentscss) of webpack (available in webpack `>= 5.87.0`), so no `css-loader` and `style-loader` are required. +> If you prefer to handle CSS using [`css-loader`](https://github.com/webpack/css-loader) and [`style-loader`](https://github.com/webpack/style-loader), keep them in the list of loaders and use `postcss-loader` **before** them. + **file.js** ```js @@ -74,13 +77,17 @@ import css from "file.css"; ```js module.exports = { + experiments: { + // Enable the built-in CSS support of webpack + css: true, + }, module: { rules: [ { test: /\.css$/i, + // `css/auto` treats `*.module.css` files as CSS modules and all other files as regular CSS + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "postcss-loader", options: { @@ -126,11 +133,15 @@ The loader **automatically** searches for configuration files. ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/i, - use: ["style-loader", "css-loader", "postcss-loader"], + type: "css/auto", + use: ["postcss-loader"], }, ], }, @@ -163,13 +174,15 @@ If you use JS styles the [`postcss-js`](https://github.com/postcss/postcss-js) p ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.style.js$/, + type: "css/auto", use: [ - "style-loader", - { loader: "css-loader" }, { loader: "postcss-loader", options: { @@ -513,23 +526,23 @@ Config lookup starts from `path.dirname(file)` and walks the file tree upwards u ``` After setting up your `postcss.config.js`, add `postcss-loader` to your `webpack.config.js`. -You can use it standalone or in conjunction with `css-loader` (recommended). +You can use it standalone or in conjunction with the built-in CSS support of webpack (recommended). -Use `postcss-loader` **before** `css-loader` and `style-loader`, but **after** other preprocessor loaders like e.g `sass|less|stylus-loader`, if you use any (since [webpack loaders evaluate right to left/bottom to top](https://webpack.js.org/concepts/loaders/#configuration)). +Use `postcss-loader` **after** other preprocessor loaders like e.g `sass|less|stylus-loader`, if you use any (since [webpack loaders evaluate right to left/bottom to top](https://webpack.js.org/concepts/loaders/#configuration)). **webpack.config.js** (**recommended**) ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/, - use: [ - "style-loader", - { loader: "css-loader", options: { importLoaders: 1 } }, - "postcss-loader", - ], + type: "css/auto", + use: ["postcss-loader"], }, ], }, @@ -599,13 +612,15 @@ All values enable source map generation except `eval` and `false` value. ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { - test: /\.css$/i, + test: /\.s[ac]ss$/i, + type: "css/auto", use: [ - { loader: "style-loader" }, - { loader: "css-loader", options: { sourceMap: true } }, { loader: "postcss-loader", options: { sourceMap: true } }, { loader: "sass-loader", options: { sourceMap: true } }, ], @@ -622,16 +637,15 @@ Alternative setup: ```js module.exports = { devtool: "source-map", + experiments: { + css: true, + }, module: { rules: [ { - test: /\.css$/i, - use: [ - { loader: "style-loader" }, - { loader: "css-loader" }, - { loader: "postcss-loader" }, - { loader: "sass-loader" }, - ], + test: /\.s[ac]ss$/i, + type: "css/auto", + use: [{ loader: "postcss-loader" }, { loader: "sass-loader" }], }, ], }, @@ -660,13 +674,15 @@ The special `implementation` option determines which implementation of PostCSS t ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { - test: /\.css$/i, + test: /\.s[ac]ss$/i, + type: "css/auto", use: [ - { loader: "style-loader" }, - { loader: "css-loader" }, { loader: "postcss-loader", options: { implementation: require("postcss") }, @@ -685,13 +701,15 @@ module.exports = { ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { - test: /\.css$/i, + test: /\.s[ac]ss$/i, + type: "css/auto", use: [ - { loader: "style-loader" }, - { loader: "css-loader" }, { loader: "postcss-loader", options: { implementation: require.resolve("postcss") }, @@ -722,13 +740,15 @@ Using [`SugarSS`](https://github.com/postcss/sugarss) syntax. ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.sss$/i, + type: "css/auto", use: [ - "style-loader", - { loader: "css-loader", options: { importLoaders: 1 } }, { loader: "postcss-loader", options: { postcssOptions: { parser: "sugarss" } }, @@ -754,13 +774,15 @@ Automatically add vendor prefixes to CSS rules using [`autoprefixer`](https://gi ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/i, + type: "css/auto", use: [ - "style-loader", - { loader: "css-loader", options: { importLoaders: 1 } }, { loader: "postcss-loader", options: { @@ -799,13 +821,15 @@ npm install --save-dev postcss-preset-env ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/i, + type: "css/auto", use: [ - "style-loader", - { loader: "css-loader", options: { importLoaders: 1 } }, { loader: "postcss-loader", options: { @@ -830,27 +854,45 @@ module.exports = { ### CSS Modules -> What are `CSS Modules`? Please [read here](https://github.com/webpack/css-loader#modules). +> What are `CSS Modules`? Please [read here](https://github.com/css-modules/css-modules). No additional options required on the `postcss-loader` side to support CSS Modules. -To make them work properly, either add the `css-loader`’s `importLoaders` option. +With the built-in CSS support of webpack use the `css/auto` module type - all `*.module.css` files are treated as CSS modules, other files are treated as regular CSS. **webpack.config.js** ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/i, - use: [ - "style-loader", - { - loader: "css-loader", - options: { modules: true, importLoaders: 1 }, - }, - "postcss-loader", - ], + type: "css/auto", + use: ["postcss-loader"], + }, + ], + }, +}; +``` + +Use the `css/module` module type to treat **all** matched files as CSS modules, regardless of their name. + +**webpack.config.js** + +```js +module.exports = { + experiments: { + css: true, + }, + module: { + rules: [ + { + test: /\.css$/i, + type: "css/module", + use: ["postcss-loader"], }, ], }, @@ -873,13 +915,15 @@ If you want to process styles written in JavaScript, use the [`postcss-js`](http ```js module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.style.js$/, + type: "css/auto", use: [ - "style-loader", - { loader: "css-loader", options: { importLoaders: 2 } }, { loader: "postcss-loader", options: { @@ -914,38 +958,35 @@ export default { ### Extract CSS -To extract CSS into separate files, use [`mini-css-extract-plugin`](https://github.com/webpack/mini-css-extract-plugin). +The built-in CSS support of webpack extracts CSS into separate files out of the box, no plugin is required. +Use [`output.cssFilename`](https://webpack.js.org/configuration/output/#outputcssfilename) and [`output.cssChunkFilename`](https://webpack.js.org/configuration/output/#outputcsschunkfilename) to control the names of the generated files. **webpack.config.js** ```js const isProductionMode = process.env.NODE_ENV === "production"; -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); - module.exports = { mode: isProductionMode ? "production" : "development", + experiments: { + css: true, + }, + output: { + cssFilename: isProductionMode ? "[name].[contenthash].css" : "[name].css", + }, module: { rules: [ { test: /\.css$/, - use: [ - isProductionMode ? MiniCssExtractPlugin.loader : "style-loader", - "css-loader", - "postcss-loader", - ], + type: "css/auto", + use: ["postcss-loader"], }, ], }, - plugins: [ - new MiniCssExtractPlugin({ - filename: isProductionMode ? "[name].[contenthash].css" : "[name].css", - }), - ], }; ``` -> 💡 Use this setup to extract and cache CSS in production while keeping fast style injection during development. +> 💡 Use this setup to extract and cache CSS in production while keeping fast rebuilds during development. ### Emit assets @@ -974,13 +1015,15 @@ const postcssCustomPlugin = (opts = {}) => ({ }); module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [postcssCustomPlugin()] } }, @@ -1023,13 +1066,15 @@ const postcssCustomPlugin = (opts = {}) => ({ }); module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [postcssCustomPlugin()] } }, @@ -1051,13 +1096,15 @@ module.exports = { const path = require("node:path"); module.exports = { + experiments: { + css: true, + }, module: { rules: [ { test: /\.css$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "postcss-loader", options: { diff --git a/test/__snapshots__/builtInCss.test.js.snap b/test/__snapshots__/builtInCss.test.js.snap new file mode 100644 index 00000000..7854ef38 --- /dev/null +++ b/test/__snapshots__/builtInCss.test.js.snap @@ -0,0 +1,112 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`built-in CSS support of webpack should emit an error on invalid syntax: errors 1`] = ` +[ + "ModuleBuildError: Module build failed (from \`replaced original path\`): + +SyntaxError + +(1:3) /test/fixtures/css/style.css Unnecessary curly bracket + +> 1 | a { + | ^ + 2 | color: black; + 3 | } +", +] +`; + +exports[`built-in CSS support of webpack should emit an error on invalid syntax: warnings 1`] = `[]`; + +exports[`built-in CSS support of webpack should generate source maps: errors 1`] = `[]`; + +exports[`built-in CSS support of webpack should generate source maps: warnings 1`] = `[]`; + +exports[`built-in CSS support of webpack should work with CSS modules: errors 1`] = `[]`; + +exports[`built-in CSS support of webpack should work with CSS modules: warnings 1`] = `[]`; + +exports[`built-in CSS support of webpack should work with SugarSS: css 1`] = ` +"/*!***************************!*\\ + !*** css ./sss/style.sss ***! + \\***************************/ +a { + color: black +} + +" +`; + +exports[`built-in CSS support of webpack should work with SugarSS: errors 1`] = `[]`; + +exports[`built-in CSS support of webpack should work with SugarSS: warnings 1`] = `[]`; + +exports[`built-in CSS support of webpack should work with the "execute" option and CSS-in-JS: css 1`] = ` +"/*!*************************************!*\\ + !*** css ./jss/postcss-js/style.js ***! + \\*************************************/ +a { + color: yellow +} +" +`; + +exports[`built-in CSS support of webpack should work with the "execute" option and CSS-in-JS: errors 1`] = `[]`; + +exports[`built-in CSS support of webpack should work with the "execute" option and CSS-in-JS: warnings 1`] = `[]`; + +exports[`built-in CSS support of webpack should work: css 1`] = ` +"/*!***************************!*\\ + !*** css ./css/style.css ***! + \\***************************/ +a { + color: black; +} + +a { + color: red; +} + +a { + color: green; +} + +a { + color: blue; +} + +.class { + -x-border-color: blue blue *; + -x-color: * #fafafa; +} + +.class-foo { + -z-border-color: blue blue *; + -z-color: * #fafafa; +} + +.phone_title { + width: 500px; + } + +@media (max-width: 500px) { + +.phone_title { + width: auto + } + } + +body.is_dark .phone_title { + color: white; + } + +.phone img { + display: block; + } + +" +`; + +exports[`built-in CSS support of webpack should work: errors 1`] = `[]`; + +exports[`built-in CSS support of webpack should work: warnings 1`] = `[]`; diff --git a/test/builtInCss.test.js b/test/builtInCss.test.js new file mode 100644 index 00000000..d5f24820 --- /dev/null +++ b/test/builtInCss.test.js @@ -0,0 +1,124 @@ +import path from "node:path"; + +import { + compile, + getCssCompiler, + getErrors, + getWarnings, + readAsset, +} from "./helpers/index"; + +describe("built-in CSS support of webpack", () => { + it("should work", async () => { + const compiler = getCssCompiler("./builtin-css/index.js", { + postcssOptions: { + plugins: ["postcss-nested"], + }, + }); + const stats = await compile(compiler); + + expect(readAsset("main.css", compiler, stats)).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it("should work with CSS modules", async () => { + const compiler = getCssCompiler("./builtin-css/modules.js", { + postcssOptions: { + plugins: ["postcss-nested"], + }, + }); + const stats = await compile(compiler); + const css = readAsset("main.css", compiler, stats); + + // The class name is generated by webpack, only check that it was renamed + expect(css).not.toContain(".link {"); + expect(css).toContain("color: black"); + expect(css).toContain("color: red"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it("should work with SugarSS", async () => { + const compiler = getCssCompiler("./builtin-css/sss.js", { + postcssOptions: { + parser: "sugarss", + hideNothingWarning: true, + }, + }); + const stats = await compile(compiler); + + expect(readAsset("main.css", compiler, stats)).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it('should work with the "execute" option and CSS-in-JS', async () => { + const compiler = getCssCompiler( + "./builtin-css/jss.js", + {}, + { + module: { + rules: [ + { + test: /style\.js$/i, + type: "css/auto", + use: [ + { + loader: path.resolve(__dirname, "../src"), + options: { + postcssOptions: { + parser: "postcss-js", + }, + execute: true, + }, + }, + ], + }, + ], + }, + }, + ); + const stats = await compile(compiler); + + expect(readAsset("main.css", compiler, stats)).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it("should generate source maps", async () => { + const compiler = getCssCompiler( + "./builtin-css/index.js", + { + sourceMap: true, + postcssOptions: { + plugins: ["postcss-nested"], + }, + }, + { + devtool: "source-map", + }, + ); + const stats = await compile(compiler); + const sourceMap = JSON.parse(readAsset("main.css.map", compiler, stats)); + + expect( + sourceMap.sources.some((source) => source.endsWith("css/style.css")), + ).toBe(true); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it("should emit an error on invalid syntax", async () => { + const compiler = getCssCompiler("./builtin-css/index.js", { + postcssOptions: { + hideNothingWarning: true, + parser: "sugarss", + }, + }); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); +}); diff --git a/test/fixtures/builtin-css/index.js b/test/fixtures/builtin-css/index.js new file mode 100644 index 00000000..bb8e98ec --- /dev/null +++ b/test/fixtures/builtin-css/index.js @@ -0,0 +1 @@ +import "../css/style.css"; diff --git a/test/fixtures/builtin-css/jss.js b/test/fixtures/builtin-css/jss.js new file mode 100644 index 00000000..d9d73ed9 --- /dev/null +++ b/test/fixtures/builtin-css/jss.js @@ -0,0 +1 @@ +import "../jss/postcss-js/style.js"; diff --git a/test/fixtures/builtin-css/modules.js b/test/fixtures/builtin-css/modules.js new file mode 100644 index 00000000..de55cd05 --- /dev/null +++ b/test/fixtures/builtin-css/modules.js @@ -0,0 +1,3 @@ +import { link } from "./style.module.css"; + +export default link; diff --git a/test/fixtures/builtin-css/sss.js b/test/fixtures/builtin-css/sss.js new file mode 100644 index 00000000..04c2949d --- /dev/null +++ b/test/fixtures/builtin-css/sss.js @@ -0,0 +1 @@ +import "../sss/style.sss"; diff --git a/test/fixtures/builtin-css/style.module.css b/test/fixtures/builtin-css/style.module.css new file mode 100644 index 00000000..2ebaa8b6 --- /dev/null +++ b/test/fixtures/builtin-css/style.module.css @@ -0,0 +1,7 @@ +.link { + color: black; + + &:hover { + color: red; + } +} diff --git a/test/fixtures/esparser/runManual.mjs b/test/fixtures/esparser/runManual.mjs index 2fc433fd..de705466 100644 --- a/test/fixtures/esparser/runManual.mjs +++ b/test/fixtures/esparser/runManual.mjs @@ -10,18 +10,22 @@ const compiler = webpack({ devtool: false, context: path.resolve(rootDir, "../fixtures"), entry: path.resolve(rootDir, "../fixtures", "./sss/index.js"), + experiments: { + css: true, + }, output: { path: path.resolve(rootDir, "../outputs"), filename: "[name].bundle.js", chunkFilename: "[name].chunk.js", + cssFilename: "[name].css", publicPath: "/webpack/public/path/", }, module: { rules: [ { test: /\.(css|sss)$/i, + type: "css/auto", use: [ - 'css-loader', { loader: path.resolve(rootDir, "../../dist"), options: { diff --git a/test/helpers/getCssCompiler.js b/test/helpers/getCssCompiler.js new file mode 100644 index 00000000..a756e559 --- /dev/null +++ b/test/helpers/getCssCompiler.js @@ -0,0 +1,55 @@ +import path from "node:path"; + +import { Volume, createFsFromVolume } from "memfs"; +import webpack from "webpack"; + +// Compiles a fixture using the built-in CSS support of webpack +// (i.e. `experiments.css` and the `css/auto` module type), so no +// `css-loader`/`style-loader` are involved. +export default (fixture, loaderOptions = {}, config = {}) => { + const fullConfig = { + mode: "development", + devtool: config.devtool || false, + context: path.resolve(__dirname, "../fixtures"), + entry: path.resolve(__dirname, "../fixtures", fixture), + experiments: { + css: true, + }, + output: { + path: path.resolve(__dirname, "../outputs"), + filename: "[name].bundle.js", + chunkFilename: "[name].chunk.js", + cssFilename: "[name].css", + cssChunkFilename: "[name].chunk.css", + publicPath: "/webpack/public/path/", + }, + module: { + rules: [ + { + test: /\.(css|sss)$/i, + type: "css/auto", + use: [ + { + loader: path.resolve(__dirname, "../../src"), + options: loaderOptions || {}, + }, + ], + }, + ], + }, + plugins: [], + ...config, + }; + + const compiler = webpack(fullConfig); + + if (!config.outputFileSystem) { + const outputFileSystem = createFsFromVolume(new Volume()); + + outputFileSystem.join = path.join.bind(path); + + compiler.outputFileSystem = outputFileSystem; + } + + return compiler; +}; diff --git a/test/helpers/index.js b/test/helpers/index.js index 99fca117..3ed328a2 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -4,6 +4,7 @@ export { default as normalizeErrors } from "./normalizeErrors"; export { default as execute } from "./execute"; export { default as getCompiler } from "./getCompiler"; export { default as getCodeFromBundle } from "./getCodeFromBundle"; +export { default as getCssCompiler } from "./getCssCompiler"; export { default as getExecutedCode } from "./getExecutedCode"; export { default as getErrors } from "./getErrors"; export { default as readAsset } from "./readAsset"; From 29fd6fe176f7f242323f5c08ba0091829908ca51 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 29 Aug 2026 13:05:27 +0000 Subject: [PATCH 2/2] chore(deps): bump js-yaml to 4.3.2 in the lockfile `npm run security` (`npm audit --omit=dev`) fails on the nested `js-yaml` of `cosmiconfig` (GHSA-h67p-54hq-rp68, GHSA-52cp-r559-cp3m, GHSA-5p4m-2wfm-xmqj). Lockfile-only bump from 4.1.1 to 4.3.2. --- package-lock.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eaecf374..0e2d67d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8396,9 +8396,19 @@ "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1"