Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/worker-implementation-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"minimizer-webpack-plugin": minor
---

Take a minimizer as a module path or `{ path, export }`, which a worker requires rather than rebuilding it from its source.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,32 @@ default one, and the tables in
[webpack's own minimizers](#webpacks-own-minimizers) for `cssMinify` and
`htmlMinify`.

`implementation` may also name a **module path** rather than hold the function
itself — a string, or `{ path, export }` for a named export, the way
`sass-loader` takes its `implementation`. A worker then `require`s the
minimizer, which is what the default terser is named by; one written as a
function is handed over as source for the worker to rebuild with `new
Function`:

```js
new MinimizerPlugin({
minify: {
// Or the string on its own, where the module exports the function itself.
implementation: {
path: require.resolve("./my-minifier"),
export: "minify",
},
},
});
```

The saving is per task, so it needs every minimizer of that task to be nameable:
one written as a function anywhere in the list leaves the whole task on the
source path, since that function has no other way across. So does a function
anywhere in what the task carries — an `extractComments` callback, or a
minimizer's own options holding one — because a required minimizer is handed
the payload as it is, and a structured clone throws on a function.

`filter(name, info)` states which assets this minimizer is offered — return
`false` to decline one, and anything else (`undefined` included) to accept. It
answers for a `filter` property on the minimizer function itself, which is what
Expand Down
174 changes: 174 additions & 0 deletions src/implementation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
/** @typedef {import("./index.js").CustomOptions} CustomOptions */
/** @typedef {import("./index.js").MinimizeFunctionHelpers} MinimizeFunctionHelpers */
/** @typedef {import("./index.js").ImplementationModuleRef} ImplementationModuleRef */
/**
* @typedef {import("./index.js").BasicMinimizerImplementation<CustomOptions> & MinimizeFunctionHelpers} MinimizerFn
*/

/**
* @param {unknown} implementation a minify function, module path, or path ref
* @returns {ImplementationModuleRef | undefined} how to `require` it in a worker
*/
function getImplementationModuleRef(implementation) {
if (typeof implementation === "string") {
return { path: implementation };
}

if (
implementation &&
typeof implementation === "object" &&
typeof (/** @type {ImplementationModuleRef} */ (implementation).path) ===
"string"
) {
const ref = /** @type {ImplementationModuleRef} */ (implementation);

return typeof ref.export === "string" && ref.export.length > 0
? { path: ref.path, export: ref.export }
: { path: ref.path };
}

return undefined;
}

/**
* @param {unknown} implementation a minify function, module path, or path ref
* @returns {MinimizerFn} the minify function
*/
function loadImplementation(implementation) {
if (typeof implementation === "function") {
return /** @type {MinimizerFn} */ (implementation);
}

const ref = getImplementationModuleRef(implementation);

if (!ref) {
throw new TypeError(
"Invalid minimizer implementation: expected a function, module path string, or { path, export }",
);
}

const mod = require(ref.path);

const loaded =
typeof ref.export === "string"
? mod[ref.export]
: typeof mod === "function"
? mod
: mod && mod.default;

if (typeof loaded !== "function") {
throw new TypeError(
typeof ref.export === "string"
? `Minimizer export "${ref.export}" is not a function in ${ref.path}`
: `Minimizer module does not export a function: ${ref.path}`,
);
}

return /** @type {MinimizerFn} */ (loaded);
}

/**
* Whether a value holds a function anywhere inside it. A worker reached by
* module path is handed the payload as it is, and a structured clone throws on
* one rather than dropping it.
* @param {unknown} value what a worker would be handed
* @param {Set<unknown>=} seen values already walked
* @returns {boolean} true when a function is in there
*/
function holdsFunction(value, seen = new Set()) {
if (typeof value === "function") {
return true;
}

if (!value || typeof value !== "object" || seen.has(value)) {
return false;
}

seen.add(value);

// A structured clone carries a `Map` or a `Set` but not what it holds, and
// neither answers to `Object.values`.
if (value instanceof Map) {
return [...value].some(
([key, one]) => holdsFunction(key, seen) || holdsFunction(one, seen),
);
}

if (value instanceof Set) {
return [...value].some((one) => holdsFunction(one, seen));
}

return Object.values(/** @type {Record<string, unknown>} */ (value)).some(
(one) => holdsFunction(one, seen),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* True when every `minimizer.implementation` is a module path (`string` or
* `{ path, export }`). Inline minify functions keep `transform`. When
* `embedded` is present, *every* configured implementation must be a path —
* a single inline function in the embedded set forces `transform` for the
* whole asset task, even if that asset's own matched minimizers are paths.
* @template T
* @param {import("./index.js").InternalOptions<T>} options options
* @returns {boolean} whether `worker.minify` can run without `transform`
*/
function canMinifyByPath(options) {
/**
* @param {unknown} implementation implementation
* @returns {boolean} true when a module path is known
*/
const hasPath = (implementation) =>
Boolean(getImplementationModuleRef(implementation));

const minimizers = Array.isArray(options.minimizer.implementation)
? options.minimizer.implementation
: [options.minimizer.implementation];

if (!minimizers.every(hasPath)) {
return false;
}

// `extractComments` and a minimizer's own options both take functions, and
// those only ever reached a worker as source.
if (
holdsFunction(options.extractComments) ||
holdsFunction(options.minimizer.options)
) {
return false;
}

if (!options.embedded) {
return true;
}

if (holdsFunction(options.embedded.options)) {
return false;
}

const embedded = Array.isArray(options.embedded.implementation)
? options.embedded.implementation
: [options.embedded.implementation];

return embedded.every(hasPath);
}

/**
* The file `loadImplementation` would `require`, which is what tells two
* references apart: a bare specifier and a file of that name are not one module.
* @param {unknown} implementation a minify function, module path, or path ref
* @returns {string | undefined} its resolved module, or nothing where no module is named
*/
function resolveImplementationModule(implementation) {
const ref = getImplementationModuleRef(implementation);

return ref ? require.resolve(ref.path) : undefined;
}

module.exports = {
canMinifyByPath,
getImplementationModuleRef,
loadImplementation,
resolveImplementationModule,
};
Loading
Loading