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..b4726702 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; } @@ -1108,11 +1110,21 @@ class MinimizerPlugin { let query = ""; let filename = name; - const querySplit = filename.indexOf("?"); + // A fragment is no part of the file on disk, so neither `filename`, + // `query` nor a name built from them may carry one. + const suffixSplit = filename.search(/[?#]/); - if (querySplit >= 0) { - query = filename.slice(querySplit); - filename = filename.slice(0, querySplit); + if (suffixSplit >= 0) { + if (filename[suffixSplit] === "?") { + const fragmentSplit = filename.indexOf("#", suffixSplit); + + query = filename.slice( + suffixSplit, + fragmentSplit >= 0 ? fragmentSplit : undefined, + ); + } + + filename = filename.slice(0, suffixSplit); } const lastSlashIndex = filename.lastIndexOf("/"); 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|