From 12233cad2ee69125fd7ff797d115f31c84c42e26 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 12:05:57 +0000 Subject: [PATCH] docs: use the built-in CSS support of webpack in examples Rewrite the `README.md` examples to use webpack's built-in CSS support (the `css/auto` module type plus `experiments.css`) instead of chaining the loader with `css-loader` and `style-loader`, add sections about `css-loader`/`style-loader` chaining and CSS modules, and update the production/extracting notes accordingly. Cover the same setup with tests: a new `getCssCompiler` helper builds with `experiments.css` enabled and the new `test/builtin-css.test.js` checks the emitted CSS, `@import`, `url()`, CSS modules, source maps and errors. The test runner now only picks up `test/*.test.js`, so JavaScript fixtures are not treated as test files. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01R5LK1NnJYsgthMpP5iqZ3C --- .changeset/builtin-css-examples.md | 5 + README.md | 221 +++++++++++++------- package.json | 4 +- test/__snapshots__/builtin-css.test.js.snap | 73 +++++++ test/builtin-css.test.js | 93 ++++++++ test/fixtures/css-modules/index.js | 3 + test/fixtures/css-modules/style.module.styl | 7 + test/helpers/getCssCompiler.js | 66 ++++++ test/helpers/index.js | 1 + 9 files changed, 399 insertions(+), 74 deletions(-) create mode 100644 .changeset/builtin-css-examples.md create mode 100644 test/__snapshots__/builtin-css.test.js.snap create mode 100644 test/builtin-css.test.js create mode 100644 test/fixtures/css-modules/index.js create mode 100644 test/fixtures/css-modules/style.module.styl create mode 100644 test/helpers/getCssCompiler.js diff --git a/.changeset/builtin-css-examples.md b/.changeset/builtin-css-examples.md new file mode 100644 index 00000000..5805e363 --- /dev/null +++ b/.changeset/builtin-css-examples.md @@ -0,0 +1,5 @@ +--- +"stylus-loader": patch +--- + +Updated the examples in the documentation to use the built-in CSS support of webpack (i.e. the `css/auto` module type and `experiments.css`) instead of chaining the loader with `css-loader` and `style-loader`. Chaining with `css-loader` and `style-loader` is still supported and documented in the "Using `css-loader` and `style-loader`" section. diff --git a/README.md b/README.md index 27b5973d..e2b096d2 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,27 @@ module.exports = { module: { rules: [ { - test: /\.styl$/, - loader: "stylus-loader", // compiles Styl to CSS + test: /\.styl$/i, + // Uses the built-in CSS support of webpack, i.e. `.module.styl` files + // are treated as CSS modules, other files are treated as plain CSS + type: "css/auto", + // Compiles Stylus to CSS + use: ["stylus-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 @@ -101,14 +114,9 @@ module.exports = { module: { rules: [ { - test: /\.styl$/, + test: /\.styl$/i, + type: "css/auto", use: [ - { - loader: "style-loader", - }, - { - loader: "css-loader", - }, { loader: "stylus-loader", options: { @@ -199,6 +207,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -211,10 +222,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -239,6 +249,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -258,14 +271,8 @@ module.exports = { rules: [ { test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - { - loader: "css-loader", - options: { - sourceMap: true, - }, - }, { loader: "stylus-loader", options: { @@ -276,6 +283,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -301,10 +311,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/i, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -315,6 +324,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -351,10 +363,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -365,6 +376,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -377,10 +391,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -401,6 +414,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -411,10 +427,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -435,6 +450,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -458,10 +476,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/i, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -472,6 +489,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -484,10 +504,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/i, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -498,6 +517,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -505,7 +527,7 @@ module.exports = { ### Normal Usage -Chain `stylus-loader` with the [`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** @@ -514,16 +536,42 @@ module.exports = { module: { rules: [ { - test: /\.styl$/, + test: /\.styl$/i, + type: "css/auto", // Handles the generated CSS using the built-in CSS support of webpack + use: ["stylus-loader"], // Compiles Stylus to CSS + }, + ], + }, + experiments: { + css: true, + }, +}; +``` + +The `css/auto` module type treats `*.module.styl` 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. + +### Using `css-loader` and `style-loader` + +The built-in CSS support of webpack is not mandatory, you can still chain the `stylus-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** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.styl$/i, use: [ { - loader: "style-loader", // creates style nodes from JS strings + loader: "style-loader", // Creates style nodes from JS strings }, { - loader: "css-loader", // translates CSS into CommonJS + loader: "css-loader", // Translates CSS into CommonJS }, { - loader: "stylus-loader", // compiles Stylus to CSS + loader: "stylus-loader", // Compiles Stylus to CSS }, ], }, @@ -532,6 +580,8 @@ module.exports = { }; ``` +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 To enable sourcemaps for CSS, you'll need to pass the `sourceMap` property in the loader's options. @@ -545,15 +595,9 @@ module.exports = { module: { rules: [ { - test: /\.styl$/, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - { - loader: "css-loader", - options: { - sourceMap: true, - }, - }, { loader: "stylus-loader", options: { @@ -564,6 +608,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -576,16 +623,11 @@ module.exports = { module: { rules: [ { - test: /\.styl$/, + test: /\.styl$/i, + type: "css/auto", use: [ { - loader: "style-loader", // creates style nodes from JS strings - }, - { - loader: "css-loader", // translates CSS into CommonJS - }, - { - loader: "stylus-loader", // compiles Stylus to CSS + loader: "stylus-loader", options: { stylusOptions: { use: [require("nib")()], @@ -597,6 +639,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -625,10 +670,9 @@ module.exports = { module: { rules: [ { - test: /\.styl$/, + test: /\.styl$/i, + type: "css/auto", use: [ - "style-loader", - "css-loader", { loader: "stylus-loader", options: { @@ -642,12 +686,18 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` ### In production -Usually, it's recommended to extract the style sheets into a dedicated CSS file in production using the [MiniCssExtractPlugin](https://github.com/webpack/mini-css-extract-plugin). This way your styles are not dependent on JavaScript. +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 CSS file in production using the [MiniCssExtractPlugin](https://github.com/webpack/mini-css-extract-plugin). This way your styles are not dependent on JavaScript. ### webpack resolver @@ -687,14 +737,9 @@ module.exports = { module: { rules: [ { - test: /\.styl/, + test: /\.styl$/i, + type: "css/auto", use: [ - { - loader: "style-loader", - }, - { - loader: "css-loader", - }, { loader: "stylus-loader", options: { @@ -707,6 +752,9 @@ module.exports = { }, ], }, + experiments: { + css: true, + }, }; ``` @@ -716,10 +764,39 @@ 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 + +With the built-in CSS support of webpack, `*.module.styl` 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`: + +**webpack.config.js** -- [`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) +```js +module.exports = { + module: { + rules: [ + { + test: /\.styl$/i, + type: "css/auto", + use: ["stylus-loader"], + }, + ], + }, + experiments: { + css: true, + }, +}; +``` + +**index.js** + +```js +import * as styles from "./style.module.styl"; + +document.body.className = styles.box; +``` ## Contributing diff --git a/package.json b/package.json index d45f1787..1bb302c9 100644 --- a/package.json +++ b/package.json @@ -50,9 +50,9 @@ "fix": "npm run fix:code && npm run fmt", "fix:code": "npm run lint:code -- --fix", "pretest": "npm run lint", - "test": "npm run test:base", + "test": "npm run test:base -- \"test/*.test.js\"", "test:base": "node --import ./test/setup-snapshots.js --test --test-force-exit --test-reporter=dot --test-reporter-destination=stdout --experimental-test-module-mocks", - "test:coverage": "npm run test:base -- --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage.lcov --test-coverage-include=\"src/**/*.js\"", + "test:coverage": "npm run test:base -- --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage.lcov --test-coverage-include=\"src/**/*.js\" \"test/*.test.js\"", "prepare": "husky && npm run build", "version": "changeset version", "release": "npm run build && changeset publish" diff --git a/test/__snapshots__/builtin-css.test.js.snap b/test/__snapshots__/builtin-css.test.js.snap new file mode 100644 index 00000000..256ae0c7 --- /dev/null +++ b/test/__snapshots__/builtin-css.test.js.snap @@ -0,0 +1,73 @@ +exports[`built-in CSS support of webpack > should generate source maps 1`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should generate source maps 2`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should throw an error 1`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should throw an error 2`] = ` +[ + "ModuleBuildError: Module build failed (from \`replaced original path\`):\\n/test/fixtures/broken.styl:1:8", + "ModuleError: Module Error (from \`replaced original path\`):\\nexpected \\"indent\\", got \\";\\"" +] +`; + +exports[`built-in CSS support of webpack > should work 1`] = ` +"/*!************************!*\\\\\\n !*** css ./basic.styl ***!\\n \\\\************************/\\nbody {\\n font: 12px Helvetica, Arial, sans-serif;\\n}\\na.button {\\n -webkit-border-radius: 5px;\\n -moz-border-radius: 5px;\\n border-radius: 5px;\\n}\\n\\n" +`; + +exports[`built-in CSS support of webpack > should work 2`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should work 3`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should work with CSS modules 1`] = ` +{ + "nav": "stylusLoaderExport-css-modules_style_module_styl-nav", + "title": "stylusLoaderExport-css-modules_style_module_styl-title" +} +`; + +exports[`built-in CSS support of webpack > should work with CSS modules 2`] = ` +"/*!*******************************************!*\\\\\\n !*** css ./css-modules/style.module.styl ***!\\n \\\\*******************************************/\\n.stylusLoaderExport-css-modules_style_module_styl-nav {\\n color: #eee;\\n}\\n.stylusLoaderExport-css-modules_style_module_styl-title {\\n font-weight: bold;\\n}\\n\\n" +`; + +exports[`built-in CSS support of webpack > should work with CSS modules 3`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should work with CSS modules 4`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should work with \"@import\" 1`] = ` +"/*!******************************!*\\\\\\n !*** css ./import-styl.styl ***!\\n \\\\******************************/\\n.imported-stylus {\\n border: 5px;\\n}\\n\\n" +`; + +exports[`built-in CSS support of webpack > should work with \"@import\" 2`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should work with \"@import\" 3`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should work with \"url()\" 1`] = ` +"/*!***********************!*\\\\\\n !*** css ./urls.styl ***!\\n \\\\***********************/\\nbody {\\n background: url(img.png);\\n}\\n\\n" +`; + +exports[`built-in CSS support of webpack > should work with \"url()\" 2`] = ` +[] +`; + +exports[`built-in CSS support of webpack > should work with \"url()\" 3`] = ` +[] +`; diff --git a/test/builtin-css.test.js b/test/builtin-css.test.js new file mode 100644 index 00000000..46900b0b --- /dev/null +++ b/test/builtin-css.test.js @@ -0,0 +1,93 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; + +import { + compile, + getCodeFromBundle, + getCssCompiler, + getErrors, + getWarnings, + readAsset, +} from "./helpers/index.js"; + +describe("built-in CSS support of webpack", () => { + it("should work", async (t) => { + const testId = "./basic.styl"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + + t.assert.snapshot(readAsset("main.bundle.css", compiler, stats)); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it('should work with "@import"', async (t) => { + const testId = "./import-styl.styl"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + + t.assert.snapshot(readAsset("main.bundle.css", compiler, stats)); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it('should work with "url()"', async (t) => { + const testId = "./urls.styl"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + + // The built-in CSS support of webpack handles `url()` as an asset module + assert.strictEqual("img.png" in stats.compilation.assets, true); + + t.assert.snapshot(readAsset("main.bundle.css", compiler, stats)); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should work with CSS modules", async (t) => { + const testId = "./css-modules/index.js"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + // Exported CSS modules class names + t.assert.snapshot({ ...codeFromBundle }); + t.assert.snapshot(readAsset("main.bundle.css", compiler, stats)); + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should generate source maps", async (t) => { + const testId = "./source-map.styl"; + const compiler = getCssCompiler( + testId, + { sourceMap: true, stylusOptions: { paths: ["test/fixtures/paths"] } }, + { devtool: "source-map" }, + ); + const stats = await compile(compiler); + const css = readAsset("main.bundle.css", compiler, stats); + const map = JSON.parse(readAsset("main.bundle.css.map", compiler, stats)); + + assert.strictEqual(css.includes("sourceMappingURL"), true); + assert.deepStrictEqual( + map.sources.map((source) => source.replaceAll("\\", "/")).toSorted(), + [ + "webpack://stylusLoaderExport/./basic.styl", + "webpack://stylusLoaderExport/./paths/in-paths.styl", + "webpack://stylusLoaderExport/./source-map.styl", + ], + ); + + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); + + it("should throw an error", async (t) => { + const testId = "./broken.styl"; + const compiler = getCssCompiler(testId); + const stats = await compile(compiler); + + t.assert.snapshot(getWarnings(stats)); + t.assert.snapshot(getErrors(stats)); + }); +}); diff --git a/test/fixtures/css-modules/index.js b/test/fixtures/css-modules/index.js new file mode 100644 index 00000000..87b7a0d1 --- /dev/null +++ b/test/fixtures/css-modules/index.js @@ -0,0 +1,3 @@ +import * as styles from "./style.module.styl"; + +export default styles; diff --git a/test/fixtures/css-modules/style.module.styl b/test/fixtures/css-modules/style.module.styl new file mode 100644 index 00000000..1cd86123 --- /dev/null +++ b/test/fixtures/css-modules/style.module.styl @@ -0,0 +1,7 @@ +nav-color = #eee + +.nav + color nav-color + +.title + font-weight bold diff --git a/test/helpers/getCssCompiler.js b/test/helpers/getCssCompiler.js new file mode 100644 index 00000000..f4e53943 --- /dev/null +++ b/test/helpers/getCssCompiler.js @@ -0,0 +1,66 @@ +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 handles the generated CSS using the built-in CSS + * support of webpack (i.e. `experiments.css` and the `css/auto` module type), + * like the examples in the `README.md`. + * @param {string} fixture fixture + * @param {object=} loaderOptions loader options + * @param {object=} config webpack config + * @returns {import("webpack").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].bundle.css", + cssChunkFilename: "[name].chunk.css", + assetModuleFilename: "[name][ext]", + library: "stylusLoaderExport", + }, + module: { + rules: [ + { + test: /\.styl$/i, + // Handles the generated CSS using the built-in CSS support of webpack + type: "css/auto", + use: [ + { + loader: path.resolve(__dirname, "../../src/index.js"), + options: loaderOptions || {}, + }, + ], + }, + ], + }, + experiments: { + // Enables the built-in CSS support of webpack + css: true, + }, + plugins: [], + resolve: { + extensions: [".js", ".css", ".styl"], + }, + ...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 5e6886a5..9c69a924 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 getCodeFromStylus } from "./getCodeFromStylus.js"; export { default as getWarnings } from "./getWarnings.js"; export { default as getErrors } from "./getErrors.js";