Skip to content
Closed
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/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
149 changes: 149 additions & 0 deletions src/implementation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/** @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);

return Object.values(/** @type {Record<string, unknown>} */ (value)).some(
(one) => holdsFunction(one, seen),
);
}

/**
* 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);
}

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