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..01bb0ea9 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 */ @@ -846,6 +863,7 @@ class MinimizerPlugin { typeof fn.supportsWorkerThreads === "undefined" || fn.supportsWorkerThreads() !== false, ); + const workerThreadsAvailable = canUseWorkerThreads(); const needCreateWorker = optimizeOptions.availableNumberOfCores > 0 && workerCapable.includes(true); @@ -908,7 +926,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: enableWorkerThreads && workerThreadsAvailable, + }) + ) { 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({ diff --git a/test/parallel-option.test.js b/test/parallel-option.test.js index 8396e554..1c33e395 100644 --- a/test/parallel-option.test.js +++ b/test/parallel-option.test.js @@ -40,6 +40,18 @@ 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; + } +})(); + +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) => @@ -231,8 +243,13 @@ describe("parallel option", () => { await compile(compiler); - expect(workerMinify).toHaveBeenCalled(); - expect(workerTransform).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 () => { 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 */