From f33f4c4fa4d4b2b2745cb940b22eab6165fe9d24 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 15:17:23 +0000 Subject: [PATCH] docs: use the built-in CSS support of webpack in examples Rewrite the README examples to rely on `experiments.css` and the `css/auto` module type instead of chaining `style-loader` and `css-loader`, and keep the loader chain setup documented in a dedicated section for anyone who still needs it. Add tests covering the loader with the built-in CSS support of webpack: plain CSS, CSS modules, `url()` handling, source maps, `lessOptions`, `additionalData` and error reporting. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017wwBzDKPnXaC6vFomCtu42 --- .cspell.json | 3 +- README.md | 190 +++++++++++++------ test/__snapshots__/built-in-css.test.js.snap | 73 +++++++ test/built-in-css.test.js | 115 +++++++++++ test/fixtures/built-in-css/basic.less | 10 + test/fixtures/built-in-css/imported.less | 3 + test/fixtures/built-in-css/style.module.less | 10 + test/fixtures/built-in-css/variables.less | 3 + test/helpers/getCssCompiler.js | 60 ++++++ test/helpers/index.js | 1 + 10 files changed, 410 insertions(+), 58 deletions(-) create mode 100644 test/__snapshots__/built-in-css.test.js.snap create mode 100644 test/built-in-css.test.js create mode 100644 test/fixtures/built-in-css/basic.less create mode 100644 test/fixtures/built-in-css/imported.less create mode 100644 test/fixtures/built-in-css/style.module.less create mode 100644 test/fixtures/built-in-css/variables.less create mode 100644 test/helpers/getCssCompiler.js diff --git a/.cspell.json b/.cspell.json index 782aa24f..dfbea4b5 100644 --- a/.cspell.json +++ b/.cspell.json @@ -10,7 +10,8 @@ "iscolor", "klona", "FOUC", - "sourcemaps" + "sourcemaps", + "pathinfo" ], "ignorePaths": [ diff --git a/README.md b/README.md index 16f1324a..a2e42f8e 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,26 @@ module.exports = { rules: [ { test: /\.less$/i, - use: [ - // compiles Less to CSS - "style-loader", - "css-loader", - "less-loader", - ], + // Uses the built-in CSS support of webpack, i.e. `.module.less` files + // are treated as CSS modules, other files are treated as plain CSS + type: "css/auto", + // Compiles Less to CSS + use: ["less-loader"], }, ], }, + experiments: { + // Enables the built-in CSS support of webpack + css: true, + }, }; ``` +> [!NOTE] +> +> The built-in CSS support of webpack requires `experiments.css` to be enabled. +> Alternatively you can still chain the loader with [`css-loader`](https://github.com/webpack/css-loader) and [`style-loader`](https://github.com/webpack/style-loader) (or the [`mini-css-extract-plugin`](https://github.com/webpack/mini-css-extract-plugin)), see [Using `css-loader` and `style-loader`](#using-css-loader-and-style-loader). + Finally, run `webpack` using the method you normally use (e.g., via CLI or an npm script). ## Options @@ -96,13 +104,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - { - loader: "style-loader", - }, - { - loader: "css-loader", - }, { loader: "less-loader", options: { @@ -115,6 +118,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -128,9 +134,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -155,6 +160,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -185,9 +193,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -198,6 +205,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -211,9 +221,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -234,6 +243,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -245,9 +257,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -268,6 +279,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -292,14 +306,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - { - loader: "css-loader", - options: { - sourceMap: true, - }, - }, { loader: "less-loader", options: { @@ -310,6 +318,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -335,9 +346,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -348,6 +358,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -377,9 +390,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -390,6 +402,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -405,9 +420,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -418,6 +432,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -451,9 +468,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "less-loader", options: { @@ -464,6 +480,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -471,7 +490,35 @@ module.exports = { ### Normal usage -Chain the `less-loader` with [`css-loader`](https://github.com/webpack/css-loader) and [`style-loader`](https://github.com/webpack/style-loader) to immediately apply all styles to the DOM. +Set the module `type` to `css/auto` and enable `experiments.css` to let webpack handle the generated CSS with its built-in CSS support, without any extra loaders or plugins. + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.less$/i, + type: "css/auto", // Handles the generated CSS using the built-in CSS support of webpack + use: ["less-loader"], // Compiles Less to CSS + }, + ], + }, + experiments: { + css: true, + }, +}; +``` + +The `css/auto` module type treats `*.module.less` files as [CSS modules](#css-modules) and any other file as plain CSS. +Use `type: "css"` to always treat the file as plain CSS, or `type: "css/module"` to always treat it as a CSS module. + +Unfortunately, Less doesn't map all options 1-by-1 to camelCase. When in doubt, [check their executable](https://github.com/less/less.js/blob/3.x/bin/lessc) and search for the dash-case option. + +### Using `css-loader` and `style-loader` + +The built-in CSS support of webpack is not mandatory, you can still chain the `less-loader` with [`css-loader`](https://github.com/webpack/css-loader) and [`style-loader`](https://github.com/webpack/style-loader) to immediately apply all styles to the DOM. **webpack.config.js** @@ -498,7 +545,7 @@ module.exports = { }; ``` -Unfortunately, Less doesn't map all options 1-by-1 to camelCase. When in doubt, [check their executable](https://github.com/less/less.js/blob/3.x/bin/lessc) and search for the dash-case option. +Note that in this case the `type` and `experiments.css` options should not be set for this rule, and options like `sourceMap` have to be enabled for the `css-loader` too. ### Source maps @@ -513,14 +560,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - "style-loader", - { - loader: "css-loader", - options: { - sourceMap: true, - }, - }, { loader: "less-loader", options: { @@ -531,6 +572,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -538,7 +582,10 @@ If you want to edit the original Less files inside Chrome, [there's a good blog ### In production -Usually, it's recommended to extract the style sheets into a dedicated file in production using the [MiniCssExtractPlugin](https://github.com/webpack/mini-css-extract-plugin). This way your styles are not dependent on JavaScript, improving performance and cacheability. +The built-in CSS support of webpack always extracts style sheets into dedicated files, so your styles are not dependent on JavaScript, which improves performance and cacheability. +The name of the generated files can be configured using the [`output.cssFilename`](https://webpack.js.org/configuration/output/#outputcssfilename) and [`output.cssChunkFilename`](https://webpack.js.org/configuration/output/#outputcsschunkfilename) options. + +When you chain the loader with `css-loader` and `style-loader` instead, it's recommended to extract the style sheets into a dedicated file in production using the [MiniCssExtractPlugin](https://github.com/webpack/mini-css-extract-plugin). ### Imports @@ -568,10 +615,14 @@ module.exports = { rules: [ { test: /\.less$/i, - use: ["style-loader", "css-loader", "less-loader"], + type: "css/auto", + use: ["less-loader"], }, ], }, + experiments: { + css: true, + }, resolve: { byDependency: { // More options can be found here https://webpack.js.org/configuration/resolve/ @@ -595,13 +646,8 @@ module.exports = { rules: [ { test: /\.less$/i, + type: "css/auto", use: [ - { - loader: "style-loader", - }, - { - loader: "css-loader", - }, { loader: "less-loader", options: { @@ -614,6 +660,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -668,14 +717,41 @@ Bundling CSS with webpack has some nice advantages like referencing images and f In production, on the other hand, it's not a good idea to apply your style sheets depending on JS execution. Rendering may be delayed or even a [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) might be visible. Thus it's often still better to have them as separate files in your final production build. -There are two possibilities to extract a style sheet from the bundle: +The built-in CSS support of webpack does this out of the box: every entry point and chunk gets its own style sheet, no extra plugin required. +When you chain the loader with the `css-loader` instead, use the [`MiniCssExtractPlugin`](https://github.com/webpack/mini-css-extract-plugin) to extract a style sheet from the bundle. + +### CSS modules -- [`extract-loader`](https://github.com/peerigon/extract-loader) (simpler, but specialized on the css-loader's output) -- [`MiniCssExtractPlugin`](https://github.com/webpack/mini-css-extract-plugin) (more complex, but works in all use-cases) +With the built-in CSS support of webpack, `*.module.less` files are treated as [CSS modules](https://github.com/css-modules/css-modules) when the module `type` is `css/auto`, and all files are treated as CSS modules when the module `type` is `css/module`: -### CSS modules gotcha +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.less$/i, + type: "css/auto", + use: ["less-loader"], + }, + ], + }, + experiments: { + css: true, + }, +}; +``` + +**index.js** + +```js +import * as styles from "./style.module.less"; + +document.body.className = styles.box; +``` -There is a known problem when using Less with [CSS modules](https://github.com/css-modules/css-modules) regarding relative file paths in `url(...)` statements. +There is a known problem when using Less with CSS modules regarding relative file paths in `url(...)` statements. [See this issue for an explanation](https://github.com/webpack/less-loader/issues/109#issuecomment-253797335). ## Contributing diff --git a/test/__snapshots__/built-in-css.test.js.snap b/test/__snapshots__/built-in-css.test.js.snap new file mode 100644 index 00000000..c49db5ec --- /dev/null +++ b/test/__snapshots__/built-in-css.test.js.snap @@ -0,0 +1,73 @@ +exports[`built-in CSS support > should emit an error on a broken file 1`] = ` +[] +`; + +exports[`built-in CSS support > should emit an error on a broken file 2`] = ` +[ + "ModuleBuildError: Module build failed (from \`replaced original path\`):\\n" +] +`; + +exports[`built-in CSS support > should emit assets from \`url()\` 1`] = ` +[] +`; + +exports[`built-in CSS support > should emit assets from \`url()\` 2`] = ` +[] +`; + +exports[`built-in CSS support > should generate source maps 1`] = ` +[] +`; + +exports[`built-in CSS support > should generate source maps 2`] = ` +[] +`; + +exports[`built-in CSS support > should work 1`] = ` +".imported {\\n color: hotpink;\\n}\\n.modules-dir-some-module {\\n color: hotpink;\\n}\\n.box {\\n color: #fe33ac;\\n border-color: #fdcdea;\\n background: url(circle.svg);\\n}\\n\\n" +`; + +exports[`built-in CSS support > should work 2`] = ` +[] +`; + +exports[`built-in CSS support > should work 3`] = ` +[] +`; + +exports[`built-in CSS support > should work with CSS modules 1`] = ` +".built-in-css_style_module_less-box {\\n color: #fe33ac;\\n}\\n.built-in-css_style_module_less-nested {\\n color: #fe33ac;\\n padding: 10px;\\n}\\n\\n" +`; + +exports[`built-in CSS support > should work with CSS modules 2`] = ` +[] +`; + +exports[`built-in CSS support > should work with CSS modules 3`] = ` +[] +`; + +exports[`built-in CSS support > should work with the \`additionalData\` option 1`] = ` +".variables {\\n color: coral;\\n}\\n\\n" +`; + +exports[`built-in CSS support > should work with the \`additionalData\` option 2`] = ` +[] +`; + +exports[`built-in CSS support > should work with the \`additionalData\` option 3`] = ` +[] +`; + +exports[`built-in CSS support > should work with the \`lessOptions\` option 1`] = ` +".variables {\\n color: hotpink;\\n}\\n\\n" +`; + +exports[`built-in CSS support > should work with the \`lessOptions\` option 2`] = ` +[] +`; + +exports[`built-in CSS support > should work with the \`lessOptions\` option 3`] = ` +[] +`; diff --git a/test/built-in-css.test.js b/test/built-in-css.test.js new file mode 100644 index 00000000..66a54136 --- /dev/null +++ b/test/built-in-css.test.js @@ -0,0 +1,115 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; + +import { + compile, + getCssCompiler, + getErrors, + getWarnings, + readAsset, + readsAssets, +} from "./helpers/index.js"; + +describe("built-in CSS support", { timeout: 30000 }, () => { + it("should work", async (t) => { + const testId = "./built-in-css/basic.less"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + + t.assert.snapshot(readAsset("main.css", compiler, stats)); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should emit assets from `url()`", async (t) => { + const testId = "./built-in-css/basic.less"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + const assets = readsAssets(compiler, stats); + + assert.strictEqual( + Object.keys(assets).includes("circle.svg"), + true, + "Expected `circle.svg` to be emitted", + ); + assert.match(assets["main.css"], /url\(circle\.svg\)/); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should work with CSS modules", async (t) => { + const testId = "./built-in-css/style.module.less"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + const css = readAsset("main.css", compiler, stats); + + assert.doesNotMatch( + css, + /^\.box\b/m, + "Expected local class names to be renamed", + ); + assert.match(css, /^\.[\w-]+-box\b/m); + assert.match(css, /^\.[\w-]+-nested\b/m); + t.assert.snapshot(css); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should work with the `lessOptions` option", async (t) => { + const testId = "./built-in-css/variables.less"; + const compiler = getCssCompiler(testId, { + lessOptions: { globalVars: { color: "hotpink" } }, + }); + const stats = await compile(compiler); + + t.assert.snapshot(readAsset("main.css", compiler, stats)); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should work with the `additionalData` option", async (t) => { + const testId = "./built-in-css/variables.less"; + const compiler = getCssCompiler(testId, { + additionalData: "@color: coral;", + }); + const stats = await compile(compiler); + + t.assert.snapshot(readAsset("main.css", compiler, stats)); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should generate source maps", async (t) => { + const testId = "./built-in-css/basic.less"; + const compiler = getCssCompiler( + testId, + { sourceMap: true }, + { devtool: "source-map" }, + ); + const stats = await compile(compiler); + const assets = readsAssets(compiler, stats); + const map = JSON.parse(assets["main.css.map"]); + + assert.strictEqual( + map.sources.some((source) => source.endsWith("built-in-css/basic.less")), + true, + "Expected the source map to reference the original Less file", + ); + assert.strictEqual( + map.sourcesContent.some((content) => content.includes("@base: #f938ab;")), + true, + "Expected the source map to contain the original Less source", + ); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should emit an error on a broken file", async (t) => { + const testId = "./error.less"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); +}); diff --git a/test/fixtures/built-in-css/basic.less b/test/fixtures/built-in-css/basic.less new file mode 100644 index 00000000..bc50eac5 --- /dev/null +++ b/test/fixtures/built-in-css/basic.less @@ -0,0 +1,10 @@ +@import "./imported.less"; +@import "some/module"; + +@base: #f938ab; + +.box { + color: saturate(@base, 5%); + border-color: lighten(@base, 30%); + background: url("../resources/circle.svg"); +} diff --git a/test/fixtures/built-in-css/imported.less b/test/fixtures/built-in-css/imported.less new file mode 100644 index 00000000..2794b308 --- /dev/null +++ b/test/fixtures/built-in-css/imported.less @@ -0,0 +1,3 @@ +.imported { + color: hotpink; +} diff --git a/test/fixtures/built-in-css/style.module.less b/test/fixtures/built-in-css/style.module.less new file mode 100644 index 00000000..406879d1 --- /dev/null +++ b/test/fixtures/built-in-css/style.module.less @@ -0,0 +1,10 @@ +@base: #f938ab; + +.box { + color: saturate(@base, 5%); +} + +.nested { + .box(); + padding: 10px; +} diff --git a/test/fixtures/built-in-css/variables.less b/test/fixtures/built-in-css/variables.less new file mode 100644 index 00000000..ec8d6fba --- /dev/null +++ b/test/fixtures/built-in-css/variables.less @@ -0,0 +1,3 @@ +.variables { + color: @color; +} diff --git a/test/helpers/getCssCompiler.js b/test/helpers/getCssCompiler.js new file mode 100644 index 00000000..f20d2e93 --- /dev/null +++ b/test/helpers/getCssCompiler.js @@ -0,0 +1,60 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { Volume, createFsFromVolume } from "memfs"; +import webpack from "webpack"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Creates a compiler which relies on the built-in CSS support of webpack + * (i.e. `experiments.css`) instead of `css-loader`/`style-loader`. + * @param {string} fixture fixture + * @param {object} loaderOptions loader options + * @param {object} config webpack config + * @returns {Compiler} compiler + */ +export default (fixture, loaderOptions = {}, config = {}) => { + const fullConfig = { + mode: "development", + devtool: config.devtool || false, + context: path.resolve(__dirname, "../fixtures"), + entry: path.resolve(__dirname, "../fixtures", fixture), + output: { + path: path.resolve(__dirname, "../outputs"), + filename: "[name].bundle.js", + chunkFilename: "[name].chunk.js", + cssFilename: "[name].css", + cssChunkFilename: "[name].chunk.css", + assetModuleFilename: "[name][ext]", + pathinfo: false, + }, + experiments: { + css: true, + }, + module: { + rules: [ + { + test: /\.less$/i, + type: "css/auto", + use: [ + { + loader: path.resolve(__dirname, "../../src/index.js"), + options: loaderOptions || {}, + }, + ], + }, + ], + }, + plugins: [], + ...config, + }; + + const compiler = webpack(fullConfig); + + if (!config.outputFileSystem) { + compiler.outputFileSystem = createFsFromVolume(new Volume()); + } + + return compiler; +}; diff --git a/test/helpers/index.js b/test/helpers/index.js index d3df30f2..c5c58f3a 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -2,6 +2,7 @@ export { default as compile } from "./compile.js"; export { default as getCodeFromBundle } from "./getCodeFromBundle.js"; export { default as execute } from "./execute.js"; export { default as getCompiler } from "./getCompiler.js"; +export { default as getCssCompiler } from "./getCssCompiler.js"; export { default as getCodeFromLess } from "./getCodeFromLess.js"; export { default as getWarnings } from "./getWarnings.js"; export { default as getErrors } from "./getErrors.js";