From 9c21eb8622922340ee1dd063d6ba21cb271339b9 Mon Sep 17 00:00:00 2001 From: sheo13666q Date: Thu, 24 Sep 2026 10:02:28 +0000 Subject: [PATCH 1/4] fix: minify assets whose names carry a fragment webpack keeps a `#fragment` in the asset name (`[hash][ext][query][fragment]` for asset modules, or a `#` in `output.filename`) and strips it only when writing. `test` already fell back to the bare name, but every built-in `filter`, the default `test`, the `.mjs`/`.cjs` detection and `extensionOf` accepted a query only, so such assets were skipped. Read the extension from before the first `?` or `#`. Claude-Session: https://claude.ai/code/session_01EpeANYvMKyZwSDTRVE5LoG --- .changeset/fragment-asset-names.md | 5 ++ src/index.js | 8 +-- src/utils.js | 28 +++++------ test/css-minify-option.test.js | 37 ++++++++++++++ test/fixtures/fragment-assets.js | 3 ++ test/helpers/readAsset.js | 2 +- test/test-option.test.js | 79 ++++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 .changeset/fragment-asset-names.md create mode 100644 test/fixtures/fragment-assets.js diff --git a/.changeset/fragment-asset-names.md b/.changeset/fragment-asset-names.md new file mode 100644 index 00000000..82ee727d --- /dev/null +++ b/.changeset/fragment-asset-names.md @@ -0,0 +1,5 @@ +--- +"minimizer-webpack-plugin": patch +--- + +Minify assets whose names carry a `#fragment`, such as `[name].js#[contenthash]` or an asset module named `[hash][ext][query][fragment]`. diff --git a/src/index.js b/src/index.js index 80da7302..12fc3d0c 100644 --- a/src/index.js +++ b/src/index.js @@ -445,7 +445,9 @@ class MinimizerPlugin { }) ); const test = - typeof declaredTest !== "undefined" ? declaredTest : /\.[cm]?js(\?.*)?$/i; + typeof declaredTest !== "undefined" + ? declaredTest + : /^[^?#]*\.[cm]?js(?:[?#].*)?$/i; // `terserOptions` is a deprecated alias of `minimizerOptions`; prefer the // new name when both are provided. @@ -987,9 +989,9 @@ class MinimizerPlugin { if (typeof info.javascriptModule !== "undefined") { options.module = info.javascriptModule; - } else if (/\.mjs(\?.*)?$/i.test(name)) { + } else if (/^[^?#]*\.mjs(?:[?#].*)?$/i.test(name)) { options.module = true; - } else if (/\.cjs(\?.*)?$/i.test(name)) { + } else if (/^[^?#]*\.cjs(?:[?#].*)?$/i.test(name)) { options.module = false; } diff --git a/src/utils.js b/src/utils.js index d334627e..f70ea1ff 100644 --- a/src/utils.js +++ b/src/utils.js @@ -118,13 +118,14 @@ function getMinimizerOptionsAt(minimizerOptions, index) { : minimizerOptions; } -const JS_FILE_RE = /\.[cm]?js(\?.*)?$/i; -const JSON_FILE_RE = /\.json(\?.*)?$/i; -const HTML_FILE_RE = /\.html?(\?.*)?$/i; -const CSS_FILE_RE = /\.css(\?.*)?$/i; -const SVG_FILE_RE = /\.svg(\?.*)?$/i; +const JS_FILE_RE = /^[^?#]*\.[cm]?js(?:[?#].*)?$/i; +const JSON_FILE_RE = /^[^?#]*\.json(?:[?#].*)?$/i; +const HTML_FILE_RE = /^[^?#]*\.html?(?:[?#].*)?$/i; +const CSS_FILE_RE = /^[^?#]*\.css(?:[?#].*)?$/i; +const SVG_FILE_RE = /^[^?#]*\.svg(?:[?#].*)?$/i; // What `imageminMinify` is offered; its plugins decide what they act on. -const IMAGE_FILE_RE = /\.(?:avif|gif|jpe?g|jxl|png|svg|tiff?|webp)(\?.*)?$/i; +const IMAGE_FILE_RE = + /^[^?#]*\.(?:avif|gif|jpe?g|jxl|png|svg|tiff?|webp)(?:[?#].*)?$/i; /** @type {undefined | ((specifier: string) => Promise)} */ let dynamicImport; @@ -2079,21 +2080,16 @@ swcMinifyCss.getTypes = () => ["css"]; swcMinifyCss.filter = (name) => CSS_FILE_RE.test(name); /** - * The extension a name carries, lowercased and without the dot or any query. + * The extension a name carries, lowercased and without the dot, query or fragment. * @param {string} name asset name * @returns {string} the extension, or "" when it has none */ function extensionOf(name) { - const withoutQuery = name.replace(/\?.*$/, ""); - const dotIndex = withoutQuery.lastIndexOf("."); - const slashIndex = Math.max( - withoutQuery.lastIndexOf("/"), - withoutQuery.lastIndexOf("\\"), - ); + const bare = name.replace(/[?#].*$/, ""); + const dotIndex = bare.lastIndexOf("."); + const slashIndex = Math.max(bare.lastIndexOf("/"), bare.lastIndexOf("\\")); - return dotIndex > slashIndex - ? withoutQuery.slice(dotIndex + 1).toLowerCase() - : ""; + return dotIndex > slashIndex ? bare.slice(dotIndex + 1).toLowerCase() : ""; } /** diff --git a/test/css-minify-option.test.js b/test/css-minify-option.test.js index 76619de7..2934d062 100644 --- a/test/css-minify-option.test.js +++ b/test/css-minify-option.test.js @@ -373,4 +373,41 @@ describe("css minify option", () => { expect(getErrors(stats)).toMatchSnapshot("errors"); expect(getWarnings(stats)).toMatchSnapshot("warnings"); }); + + it("should minify CSS and HTML assets whose names carry a fragment", async () => { + const compiler = getCompiler({ + entry: path.resolve(__dirname, "./fixtures/fragment-assets.js"), + output: { + path: path.resolve(__dirname, "./dist"), + filename: "[name].js", + assetModuleFilename: "[name][ext][query][fragment]", + }, + }); + + new MinimizerPlugin({ + test: /\.(?:css|html)$/i, + minify: [ + MinimizerPlugin.cssnanoMinify, + MinimizerPlugin.htmlMinifierTerser, + ], + minimizerOptions: [ + {}, + { collapseWhitespace: true, removeComments: true }, + ], + }).apply(compiler); + + const stats = await compile(compiler); + const assets = readsAssets(compiler, stats); + + for (const name of ["file.css#dark", "file.html#top"]) { + expect(stats.compilation.getAsset(name).info.minimized).toBe(true); + } + + expect(assets["file.css#dark"]).toBe( + ".foo{color:red;background:blue}.bar{margin:10px;padding:10px}", + ); + expect(assets["file.html#top"]).not.toMatch(/\n|