Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-regexp-options-in-process-workers.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 45 additions & 1 deletion src/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>=} 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<string, unknown>} */ (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
Expand All @@ -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<T>} 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
Expand All @@ -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;
Expand All @@ -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
Expand Down
24 changes: 23 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -846,6 +863,7 @@ class MinimizerPlugin {
typeof fn.supportsWorkerThreads === "undefined" ||
fn.supportsWorkerThreads() !== false,
);
const workerThreadsAvailable = canUseWorkerThreads();
const needCreateWorker =
optimizeOptions.availableNumberOfCores > 0 &&
workerCapable.includes(true);
Expand Down Expand Up @@ -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);
}

Expand Down
28 changes: 28 additions & 0 deletions test/implementation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
21 changes: 19 additions & 2 deletions test/parallel-option.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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 () => {
Expand Down
6 changes: 6 additions & 0 deletions types/implementation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>} options options
* @param {{ enableWorkerThreads?: boolean }=} capabilities worker capabilities
* @returns {boolean} whether `worker.minify` can run without `transform`
*/
export function canMinifyByPath<T>(
options: import("./index.js").InternalOptions<T>,
capabilities?:
| {
enableWorkerThreads?: boolean;
}
| undefined,
): boolean;
/** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
/** @typedef {import("./index.js").CustomOptions} CustomOptions */
Expand Down
Loading