From 0ab8b3342c3e9d791347df2c048dcdb17b79748c Mon Sep 17 00:00:00 2001 From: xiaoxiaojx <23253540+xiaoxiaojx@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:58:32 +0800 Subject: [PATCH 1/4] fix(worker): preserve regexp options for process workers --- .../fix-regexp-options-in-process-workers.md | 5 ++ src/implementation.js | 46 ++++++++++++++++++- src/index.js | 37 ++++++++++++--- test/implementation.test.js | 28 +++++++++++ 4 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-regexp-options-in-process-workers.md diff --git a/.changeset/fix-regexp-options-in-process-workers.md b/.changeset/fix-regexp-options-in-process-workers.md new file mode 100644 index 00000000..e7028e3f --- /dev/null +++ b/.changeset/fix-regexp-options-in-process-workers.md @@ -0,0 +1,5 @@ +--- +"minimizer-webpack-plugin": patch +--- + +Fallback to the serialized worker path when regular expression options are used with process workers that cannot preserve them. diff --git a/src/implementation.js b/src/implementation.js index ad8d731e..a4c36c5f 100644 --- a/src/implementation.js +++ b/src/implementation.js @@ -104,6 +104,36 @@ function holdsFunction(value, seen = new Set()) { ); } +/** + * Whether a value holds a regular expression anywhere inside it. + * Child-process workers on runtimes without worker threads use the default + * process IPC serializer, which does not preserve regular expressions. + * @param {unknown} value what a worker would be handed + * @param {Set=} seen values already walked + * @returns {boolean} true when a regular expression is in there + */ +function holdsRegExp(value, seen = new Set()) { + if (value instanceof RegExp) { + return true; + } + if (!value || typeof value !== "object" || seen.has(value)) { + return false; + } + seen.add(value); + + if (value instanceof Map) { + return [...value].some( + ([key, one]) => holdsRegExp(key, seen) || holdsRegExp(one, seen), + ); + } + if (value instanceof Set) { + return [...value].some((one) => holdsRegExp(one, seen)); + } + return Object.values(/** @type {Record} */ (value)).some( + (one) => holdsRegExp(one, seen), + ); +} + /** * True when every `minimizer.implementation` is a module path (`string` or * `{ path, export }`). Inline minify functions keep `transform`. When @@ -112,9 +142,10 @@ function holdsFunction(value, seen = new Set()) { * whole asset task, even if that asset's own matched minimizers are paths. * @template T * @param {import("./index.js").InternalOptions} options options + * @param {{ enableWorkerThreads?: boolean }=} capabilities worker capabilities * @returns {boolean} whether `worker.minify` can run without `transform` */ -function canMinifyByPath(options) { +function canMinifyByPath(options, capabilities = {}) { /** * @param {unknown} implementation implementation * @returns {boolean} true when a module path is known @@ -138,6 +169,13 @@ function canMinifyByPath(options) { ) { return false; } + if ( + capabilities.enableWorkerThreads === false && + (holdsRegExp(options.extractComments) || + holdsRegExp(options.minimizer.options)) + ) { + return false; + } if (!options.embedded) { return true; @@ -146,6 +184,12 @@ function canMinifyByPath(options) { if (holdsFunction(options.embedded.options)) { return false; } + if ( + capabilities.enableWorkerThreads === false && + holdsRegExp(options.embedded.options) + ) { + return false; + } const embedded = Array.isArray(options.embedded.implementation) ? options.embedded.implementation diff --git a/src/index.js b/src/index.js index b4726702..dadf3546 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,23 @@ const { uglifyJsMinify, } = require("./utils"); +/** @type {boolean | undefined} */ +let _canUseWorkerThreads; + +const canUseWorkerThreads = () => { + if (_canUseWorkerThreads !== undefined) { + return _canUseWorkerThreads; + } + try { + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- feature detection for Node 10 + require("worker_threads"); + _canUseWorkerThreads = true; + } catch (_error) { + _canUseWorkerThreads = false; + } + return _canUseWorkerThreads; +}; + /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */ /** @typedef {import("webpack").Compiler} Compiler */ /** @typedef {import("webpack").Compilation} Compilation */ @@ -840,12 +857,14 @@ class MinimizerPlugin { ({ fn }) => typeof fn.supportsBinary === "function" && fn.supportsBinary(), ); - const enableWorkerThreads = minimizerSlots.every( - ({ fn }, i) => - !workerCapable[i] || - typeof fn.supportsWorkerThreads === "undefined" || - fn.supportsWorkerThreads() !== false, - ); + const enableWorkerThreads = + canUseWorkerThreads() && + minimizerSlots.every( + ({ fn }, i) => + !workerCapable[i] || + typeof fn.supportsWorkerThreads === "undefined" || + fn.supportsWorkerThreads() !== false, + ); const needCreateWorker = optimizeOptions.availableNumberOfCores > 0 && workerCapable.includes(true); @@ -908,7 +927,11 @@ class MinimizerPlugin { // module paths — including every entry on `embedded`, not just the // asset's matched subset. A mixed path + inline-function config keeps // the whole asset on `transform`. - if (canMinifyByPath(options)) { + if ( + canMinifyByPath(options, { + enableWorkerThreads, + }) + ) { return getWorker().minify(options); } diff --git a/test/implementation.test.js b/test/implementation.test.js index e8ce00f2..a5e4327b 100644 --- a/test/implementation.test.js +++ b/test/implementation.test.js @@ -164,6 +164,34 @@ describe("canMinifyByPath", () => { ).toBe(true); }); + it("should reject a RegExp option when the worker cannot transfer it", () => { + expect( + canMinifyByPath( + { + minimizer: { + implementation: pathImpl, + options: { comments: /license/i }, + }, + }, + { enableWorkerThreads: false }, + ), + ).toBe(false); + }); + + it("should allow a RegExp option when the worker can transfer it", () => { + expect( + canMinifyByPath( + { + minimizer: { + implementation: pathImpl, + options: { comments: /license/i }, + }, + }, + { enableWorkerThreads: true }, + ), + ).toBe(true); + }); + it("should allow embedded when every implementation is a path", () => { expect( canMinifyByPath({ From f256eefbec89078ec9879f2a1c9e4a7276ef6a44 Mon Sep 17 00:00:00 2001 From: xiaoxiaojx <23253540+xiaoxiaojx@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:15:05 +0800 Subject: [PATCH 2/4] fix(worker): preserve worker thread option semantics --- src/index.js | 17 ++++++++--------- types/implementation.d.ts | 6 ++++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index dadf3546..01bb0ea9 100644 --- a/src/index.js +++ b/src/index.js @@ -857,14 +857,13 @@ class MinimizerPlugin { ({ fn }) => typeof fn.supportsBinary === "function" && fn.supportsBinary(), ); - const enableWorkerThreads = - canUseWorkerThreads() && - minimizerSlots.every( - ({ fn }, i) => - !workerCapable[i] || - typeof fn.supportsWorkerThreads === "undefined" || - fn.supportsWorkerThreads() !== false, - ); + const enableWorkerThreads = minimizerSlots.every( + ({ fn }, i) => + !workerCapable[i] || + typeof fn.supportsWorkerThreads === "undefined" || + fn.supportsWorkerThreads() !== false, + ); + const workerThreadsAvailable = canUseWorkerThreads(); const needCreateWorker = optimizeOptions.availableNumberOfCores > 0 && workerCapable.includes(true); @@ -929,7 +928,7 @@ class MinimizerPlugin { // the whole asset on `transform`. if ( canMinifyByPath(options, { - enableWorkerThreads, + enableWorkerThreads: enableWorkerThreads && workerThreadsAvailable, }) ) { return getWorker().minify(options); diff --git a/types/implementation.d.ts b/types/implementation.d.ts index b2499287..0696d75c 100644 --- a/types/implementation.d.ts +++ b/types/implementation.d.ts @@ -15,10 +15,16 @@ export type MinimizerFn = * whole asset task, even if that asset's own matched minimizers are paths. * @template T * @param {import("./index.js").InternalOptions} options options + * @param {{ enableWorkerThreads?: boolean }=} capabilities worker capabilities * @returns {boolean} whether `worker.minify` can run without `transform` */ export function canMinifyByPath( options: import("./index.js").InternalOptions, + capabilities?: + | { + enableWorkerThreads?: boolean; + } + | undefined, ): boolean; /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ /** @typedef {import("./index.js").CustomOptions} CustomOptions */ From 381e0c054c992063c1542c7a5284d1eb6d981a63 Mon Sep 17 00:00:00 2001 From: xiaoxiaojx <23253540+xiaoxiaojx@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:24:31 +0800 Subject: [PATCH 3/4] test(worker): cover regexp fallback without worker threads --- test/parallel-option.test.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/parallel-option.test.js b/test/parallel-option.test.js index 8396e554..1893549d 100644 --- a/test/parallel-option.test.js +++ b/test/parallel-option.test.js @@ -40,6 +40,15 @@ const ENABLE_WORKER_THREADS = ? process.env.ENABLE_WORKER_THREADS === "true" : true; +const WORKER_THREADS_AVAILABLE = (() => { + try { + require("worker_threads"); + return true; + } catch (_error) { + return false; + } +})(); + jest.mock("jest-worker", () => ({ Worker: jest.fn().mockImplementation((workerPath) => ({ transform: (workerTransform = jest.fn((data) => @@ -235,6 +244,20 @@ describe("parallel option", () => { expect(workerTransform).not.toHaveBeenCalled(); }); + it("should use transform for a RegExp when worker threads are unavailable", async () => { + if (WORKER_THREADS_AVAILABLE) return; + + new MinimizerPlugin({ + parallel: true, + extractComments: /license/i, + }).apply(compiler); + + await compile(compiler); + + expect(workerTransform).toHaveBeenCalled(); + expect(workerMinify).not.toHaveBeenCalled(); + }); + it('should match snapshot for the "false" value', async () => { new MinimizerPlugin({ parallel: false }).apply(compiler); From c8c8df5569aa19d59cbec711b46c84596f432867 Mon Sep 17 00:00:00 2001 From: xiaoxiaojx <784487301@qq.com> Date: Fri, 25 Sep 2026 19:45:59 +0800 Subject: [PATCH 4/4] test(worker): cover regexp fallback on node 10 --- test/parallel-option.test.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/test/parallel-option.test.js b/test/parallel-option.test.js index 1893549d..1c33e395 100644 --- a/test/parallel-option.test.js +++ b/test/parallel-option.test.js @@ -49,6 +49,9 @@ const WORKER_THREADS_AVAILABLE = (() => { } })(); +const CAN_USE_WORKER_THREADS = + ENABLE_WORKER_THREADS && WORKER_THREADS_AVAILABLE; + jest.mock("jest-worker", () => ({ Worker: jest.fn().mockImplementation((workerPath) => ({ transform: (workerTransform = jest.fn((data) => @@ -240,22 +243,13 @@ describe("parallel option", () => { await compile(compiler); - expect(workerMinify).toHaveBeenCalled(); - expect(workerTransform).not.toHaveBeenCalled(); - }); - - it("should use transform for a RegExp when worker threads are unavailable", async () => { - if (WORKER_THREADS_AVAILABLE) return; - - new MinimizerPlugin({ - parallel: true, - extractComments: /license/i, - }).apply(compiler); - - await compile(compiler); - - expect(workerTransform).toHaveBeenCalled(); - expect(workerMinify).not.toHaveBeenCalled(); + if (CAN_USE_WORKER_THREADS) { + expect(workerMinify).toHaveBeenCalled(); + expect(workerTransform).not.toHaveBeenCalled(); + } else { + expect(workerTransform).toHaveBeenCalled(); + expect(workerMinify).not.toHaveBeenCalled(); + } }); it('should match snapshot for the "false" value', async () => {