diff --git a/package-lock.json b/package-lock.json index d573543..593fc41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "jest-worker": "^27.0.6", - "p-limit": "^3.1.0", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", "source-map": "^0.6.1", @@ -10597,6 +10596,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -13000,6 +13000,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "engines": { "node": ">=10" }, @@ -20822,6 +20823,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -22621,7 +22623,8 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/package.json b/package.json index 3932da9..aa6e5be 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ }, "dependencies": { "jest-worker": "^27.0.6", - "p-limit": "^3.1.0", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", "source-map": "^0.6.1", diff --git a/src/index.js b/src/index.js index 9da3807..6d3ab99 100644 --- a/src/index.js +++ b/src/index.js @@ -4,10 +4,10 @@ import * as os from "os"; import { SourceMapConsumer } from "source-map"; import { validate } from "schema-utils"; import serialize from "serialize-javascript"; -import pLimit from "p-limit"; import { Worker } from "jest-worker"; import { + throttleAll, terserMinify, uglifyJsMinify, swcMinify, @@ -328,7 +328,7 @@ class TerserPlugin { async optimize(compiler, compilation, assets, optimizeOptions) { const cache = compilation.getCache("TerserWebpackPlugin"); let numberOfAssets = 0; - const assetsForJob = await Promise.all( + const assetsForMinify = await Promise.all( Object.keys(assets) .filter((name) => { const { info } = /** @type {Asset} */ (compilation.getAsset(name)); @@ -371,6 +371,10 @@ class TerserPlugin { }) ); + if (assetsForMinify.length === 0) { + return; + } + /** @type {undefined | (() => MinimizerWorker)} */ let getWorker; /** @type {undefined | MinimizerWorker} */ @@ -416,11 +420,6 @@ class TerserPlugin { }; } - const limit = pLimit( - getWorker && numberOfAssets > 0 - ? /** @type {number} */ (numberOfWorkers) - : Infinity - ); const { SourceMapSource, ConcatSource, RawSource } = compiler.webpack.sources; @@ -429,280 +428,279 @@ class TerserPlugin { const allExtractedComments = new Map(); const scheduledTasks = []; - for (const asset of assetsForJob) { - scheduledTasks.push( - limit(async () => { - const { name, inputSource, info, cacheItem } = asset; - let { output } = asset; - - if (!output) { - let input; - /** @type {RawSourceMap | undefined} */ - let inputSourceMap; + for (const asset of assetsForMinify) { + scheduledTasks.push(async () => { + const { name, inputSource, info, cacheItem } = asset; + let { output } = asset; - const { source: sourceFromInputSource, map } = - inputSource.sourceAndMap(); + if (!output) { + let input; + /** @type {RawSourceMap | undefined} */ + let inputSourceMap; - input = sourceFromInputSource; + const { source: sourceFromInputSource, map } = + inputSource.sourceAndMap(); - if (map) { - if (TerserPlugin.isSourceMap(map)) { - inputSourceMap = /** @type {RawSourceMap} */ (map); - } else { - inputSourceMap = /** @type {RawSourceMap} */ (map); + input = sourceFromInputSource; - compilation.warnings.push( - /** @type {WebpackError} */ - (new Error(`${name} contains invalid source map`)) - ); - } - } + if (map) { + if (TerserPlugin.isSourceMap(map)) { + inputSourceMap = /** @type {RawSourceMap} */ (map); + } else { + inputSourceMap = /** @type {RawSourceMap} */ (map); - if (Buffer.isBuffer(input)) { - input = input.toString(); + compilation.warnings.push( + /** @type {WebpackError} */ + (new Error(`${name} contains invalid source map`)) + ); } + } - const options = { - name, - input, - inputSourceMap, - minimizer: { - implementation: this.options.minimizer.implementation, - // @ts-ignore https://github.com/Microsoft/TypeScript/issues/10727 - options: { ...this.options.minimizer.options }, - }, - extractComments: this.options.extractComments, - }; - - if (typeof options.minimizer.options.module === "undefined") { - if (typeof info.javascriptModule !== "undefined") { - options.minimizer.options.module = info.javascriptModule; - } else if (/\.mjs(\?.*)?$/i.test(name)) { - options.minimizer.options.module = true; - } else if (/\.cjs(\?.*)?$/i.test(name)) { - options.minimizer.options.module = false; - } - } + if (Buffer.isBuffer(input)) { + input = input.toString(); + } - if (typeof options.minimizer.options.ecma === "undefined") { - options.minimizer.options.ecma = TerserPlugin.getEcmaVersion( - compiler.options.output.environment || {} - ); + const options = { + name, + input, + inputSourceMap, + minimizer: { + implementation: this.options.minimizer.implementation, + // @ts-ignore https://github.com/Microsoft/TypeScript/issues/10727 + options: { ...this.options.minimizer.options }, + }, + extractComments: this.options.extractComments, + }; + + if (typeof options.minimizer.options.module === "undefined") { + if (typeof info.javascriptModule !== "undefined") { + options.minimizer.options.module = info.javascriptModule; + } else if (/\.mjs(\?.*)?$/i.test(name)) { + options.minimizer.options.module = true; + } else if (/\.cjs(\?.*)?$/i.test(name)) { + options.minimizer.options.module = false; } + } - try { - output = await (getWorker - ? getWorker().transform(serialize(options)) - : minimize(options)); - } catch (error) { - const hasSourceMap = - inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap); + if (typeof options.minimizer.options.ecma === "undefined") { + options.minimizer.options.ecma = TerserPlugin.getEcmaVersion( + compiler.options.output.environment || {} + ); + } - compilation.errors.push( - /** @type {WebpackError} */ - ( - TerserPlugin.buildError( - error, - name, - hasSourceMap - ? new SourceMapConsumer( - /** @type {RawSourceMap} */ (inputSourceMap) - ) - : // eslint-disable-next-line no-undefined - undefined, - // eslint-disable-next-line no-undefined - hasSourceMap ? compilation.requestShortener : undefined - ) + try { + output = await (getWorker + ? getWorker().transform(serialize(options)) + : minimize(options)); + } catch (error) { + const hasSourceMap = + inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap); + + compilation.errors.push( + /** @type {WebpackError} */ + ( + TerserPlugin.buildError( + error, + name, + hasSourceMap + ? new SourceMapConsumer( + /** @type {RawSourceMap} */ (inputSourceMap) + ) + : // eslint-disable-next-line no-undefined + undefined, + // eslint-disable-next-line no-undefined + hasSourceMap ? compilation.requestShortener : undefined ) - ); + ) + ); - return; - } + return; + } - if (typeof output.code === "undefined") { - compilation.errors.push( - /** @type {WebpackError} */ - ( - new Error( - `${name} from Terser plugin\nMinimizer doesn't return result` - ) + if (typeof output.code === "undefined") { + compilation.errors.push( + /** @type {WebpackError} */ + ( + new Error( + `${name} from Terser plugin\nMinimizer doesn't return result` ) - ); + ) + ); - return; - } + return; + } - if (output.warnings && output.warnings.length > 0) { - output.warnings = output.warnings.map( - /** - * @param {Error | string} item - */ - (item) => TerserPlugin.buildWarning(item, name) - ); - } + if (output.warnings && output.warnings.length > 0) { + output.warnings = output.warnings.map( + /** + * @param {Error | string} item + */ + (item) => TerserPlugin.buildWarning(item, name) + ); + } - if (output.errors && output.errors.length > 0) { - const hasSourceMap = - inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap); - - output.errors = output.errors.map( - /** - * @param {Error | string} item - */ - (item) => - TerserPlugin.buildError( - item, - name, - hasSourceMap - ? new SourceMapConsumer( - /** @type {RawSourceMap} */ (inputSourceMap) - ) - : // eslint-disable-next-line no-undefined - undefined, - // eslint-disable-next-line no-undefined - hasSourceMap ? compilation.requestShortener : undefined - ) - ); - } + if (output.errors && output.errors.length > 0) { + const hasSourceMap = + inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap); + + output.errors = output.errors.map( + /** + * @param {Error | string} item + */ + (item) => + TerserPlugin.buildError( + item, + name, + hasSourceMap + ? new SourceMapConsumer( + /** @type {RawSourceMap} */ (inputSourceMap) + ) + : // eslint-disable-next-line no-undefined + undefined, + // eslint-disable-next-line no-undefined + hasSourceMap ? compilation.requestShortener : undefined + ) + ); + } - let shebang; + let shebang; - if ( - /** @type {ExtractCommentsObject} */ - (this.options.extractComments).banner !== false && - output.extractedComments && - output.extractedComments.length > 0 && - output.code.startsWith("#!") - ) { - const firstNewlinePosition = output.code.indexOf("\n"); + if ( + /** @type {ExtractCommentsObject} */ + (this.options.extractComments).banner !== false && + output.extractedComments && + output.extractedComments.length > 0 && + output.code.startsWith("#!") + ) { + const firstNewlinePosition = output.code.indexOf("\n"); - shebang = output.code.substring(0, firstNewlinePosition); - output.code = output.code.substring(firstNewlinePosition + 1); - } + shebang = output.code.substring(0, firstNewlinePosition); + output.code = output.code.substring(firstNewlinePosition + 1); + } - if (output.map) { - output.source = new SourceMapSource( - output.code, - name, - output.map, - input, - /** @type {RawSourceMap} */ (inputSourceMap), - true - ); - } else { - output.source = new RawSource(output.code); - } + if (output.map) { + output.source = new SourceMapSource( + output.code, + name, + output.map, + input, + /** @type {RawSourceMap} */ (inputSourceMap), + true + ); + } else { + output.source = new RawSource(output.code); + } - if ( - output.extractedComments && - output.extractedComments.length > 0 - ) { - const commentsFilename = - /** @type {ExtractCommentsObject} */ - (this.options.extractComments).filename || - "[file].LICENSE.txt[query]"; + if (output.extractedComments && output.extractedComments.length > 0) { + const commentsFilename = + /** @type {ExtractCommentsObject} */ + (this.options.extractComments).filename || + "[file].LICENSE.txt[query]"; - let query = ""; - let filename = name; + let query = ""; + let filename = name; - const querySplit = filename.indexOf("?"); + const querySplit = filename.indexOf("?"); - if (querySplit >= 0) { - query = filename.substr(querySplit); - filename = filename.substr(0, querySplit); - } + if (querySplit >= 0) { + query = filename.substr(querySplit); + filename = filename.substr(0, querySplit); + } - const lastSlashIndex = filename.lastIndexOf("/"); - const basename = - lastSlashIndex === -1 - ? filename - : filename.substr(lastSlashIndex + 1); - const data = { filename, basename, query }; + const lastSlashIndex = filename.lastIndexOf("/"); + const basename = + lastSlashIndex === -1 + ? filename + : filename.substr(lastSlashIndex + 1); + const data = { filename, basename, query }; - output.commentsFilename = compilation.getPath( - commentsFilename, - data - ); + output.commentsFilename = compilation.getPath( + commentsFilename, + data + ); - let banner; + let banner; - // Add a banner to the original file - if ( + // Add a banner to the original file + if ( + /** @type {ExtractCommentsObject} */ + (this.options.extractComments).banner !== false + ) { + banner = /** @type {ExtractCommentsObject} */ - (this.options.extractComments).banner !== false - ) { - banner = - /** @type {ExtractCommentsObject} */ - (this.options.extractComments).banner || - `For license information please see ${path - .relative(path.dirname(name), output.commentsFilename) - .replace(/\\/g, "/")}`; - - if (typeof banner === "function") { - banner = banner(output.commentsFilename); - } - - if (banner) { - output.source = new ConcatSource( - shebang ? `${shebang}\n` : "", - `/*! ${banner} */\n`, - output.source - ); - } - } + (this.options.extractComments).banner || + `For license information please see ${path + .relative(path.dirname(name), output.commentsFilename) + .replace(/\\/g, "/")}`; - const extractedCommentsString = output.extractedComments - .sort() - .join("\n\n"); + if (typeof banner === "function") { + banner = banner(output.commentsFilename); + } - output.extractedCommentsSource = new RawSource( - `${extractedCommentsString}\n` - ); + if (banner) { + output.source = new ConcatSource( + shebang ? `${shebang}\n` : "", + `/*! ${banner} */\n`, + output.source + ); + } } - await cacheItem.storePromise({ - source: output.source, - errors: output.errors, - warnings: output.warnings, - commentsFilename: output.commentsFilename, - extractedCommentsSource: output.extractedCommentsSource, - }); + const extractedCommentsString = output.extractedComments + .sort() + .join("\n\n"); + + output.extractedCommentsSource = new RawSource( + `${extractedCommentsString}\n` + ); } - if (output.warnings && output.warnings.length > 0) { - for (const warning of output.warnings) { - compilation.warnings.push(/** @type {WebpackError} */ (warning)); - } + await cacheItem.storePromise({ + source: output.source, + errors: output.errors, + warnings: output.warnings, + commentsFilename: output.commentsFilename, + extractedCommentsSource: output.extractedCommentsSource, + }); + } + + if (output.warnings && output.warnings.length > 0) { + for (const warning of output.warnings) { + compilation.warnings.push(/** @type {WebpackError} */ (warning)); } + } - if (output.errors && output.errors.length > 0) { - for (const error of output.errors) { - compilation.errors.push(/** @type {WebpackError} */ (error)); - } + if (output.errors && output.errors.length > 0) { + for (const error of output.errors) { + compilation.errors.push(/** @type {WebpackError} */ (error)); } + } - /** @type {Record} */ - const newInfo = { minimized: true }; - const { source, extractedCommentsSource } = output; + /** @type {Record} */ + const newInfo = { minimized: true }; + const { source, extractedCommentsSource } = output; - // Write extracted comments to commentsFilename - if (extractedCommentsSource) { - const { commentsFilename } = output; + // Write extracted comments to commentsFilename + if (extractedCommentsSource) { + const { commentsFilename } = output; - newInfo.related = { license: commentsFilename }; + newInfo.related = { license: commentsFilename }; - allExtractedComments.set(name, { - extractedCommentsSource, - commentsFilename, - }); - } + allExtractedComments.set(name, { + extractedCommentsSource, + commentsFilename, + }); + } - compilation.updateAsset(name, source, newInfo); - }) - ); + compilation.updateAsset(name, source, newInfo); + }); } - await Promise.all(scheduledTasks); + const limit = + getWorker && numberOfAssets > 0 + ? /** @type {number} */ (numberOfWorkers) + : scheduledTasks.length; + await throttleAll(limit, scheduledTasks); if (initializedWorker) { await initializedWorker.end(); diff --git a/src/utils.js b/src/utils.js index 541ae8c..eac434b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -17,6 +17,69 @@ * @typedef {Array} ExtractedComments */ +const notSettled = Symbol(`not-settled`); + +/** + * @template T + * @typedef {() => Promise} Task + */ + +/** + * Run tasks with limited concurency. + * @template T + * @param {number} limit - Limit of tasks that run at once. + * @param {Task[]} tasks - List of tasks to run. + * @returns {Promise} A promise that fulfills to an array of the results + */ +function throttleAll(limit, tasks) { + if (!Number.isInteger(limit) || limit < 1) { + throw new TypeError( + `Expected \`limit\` to be a finite number > 0, got \`${limit}\` (${typeof limit})` + ); + } + + if ( + !Array.isArray(tasks) || + !tasks.every((task) => typeof task === `function`) + ) { + throw new TypeError( + `Expected \`tasks\` to be a list of functions returning a promise` + ); + } + + return new Promise((resolve, reject) => { + const result = Array(tasks.length).fill(notSettled); + + const entries = tasks.entries(); + + const next = () => { + const { done, value } = entries.next(); + + if (done) { + const isLast = !result.includes(notSettled); + + if (isLast) resolve(/** @type{T[]} **/ (result)); + + return; + } + + const [index, task] = value; + + /** + * @param {T} x + */ + const onFulfilled = (x) => { + result[index] = x; + next(); + }; + + task().then(onFulfilled, reject); + }; + + Array(limit).fill(0).forEach(next); + }); +} + /* istanbul ignore next */ /** * @param {Input} input @@ -627,4 +690,4 @@ esbuildMinify.getMinimizerVersion = () => { return packageJson && packageJson.version; }; -export { terserMinify, uglifyJsMinify, swcMinify, esbuildMinify }; +export { throttleAll, terserMinify, uglifyJsMinify, swcMinify, esbuildMinify }; diff --git a/types/utils.d.ts b/types/utils.d.ts index db5e3cc..f32b428 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -1,3 +1,4 @@ +export type Task = () => Promise; export type RawSourceMap = import("source-map").RawSourceMap; export type TerserFormatOptions = import("terser").FormatOptions; export type TerserOptions = import("terser").MinifyOptions; @@ -15,22 +16,18 @@ export type CustomOptions = { [key: string]: any; }; export type ExtractedComments = Array; -/** @typedef {import("source-map").RawSourceMap} RawSourceMap */ -/** @typedef {import("terser").FormatOptions} TerserFormatOptions */ -/** @typedef {import("terser").MinifyOptions} TerserOptions */ -/** @typedef {import("terser").ECMA} TerserECMA */ -/** @typedef {import("./index.js").ExtractCommentsOptions} ExtractCommentsOptions */ -/** @typedef {import("./index.js").ExtractCommentsFunction} ExtractCommentsFunction */ -/** @typedef {import("./index.js").ExtractCommentsCondition} ExtractCommentsCondition */ -/** @typedef {import("./index.js").Input} Input */ -/** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ -/** @typedef {import("./index.js").PredefinedOptions} PredefinedOptions */ /** - * @typedef {{ [key: string]: any }} CustomOptions + * @template T + * @typedef {() => Promise} Task */ /** - * @typedef {Array} ExtractedComments + * Run tasks with limited concurency. + * @template T + * @param {number} limit - Limit of tasks that run at once. + * @param {Task[]} tasks - List of tasks to run. + * @returns {Promise} A promise that fulfills to an array of the results */ +export function throttleAll(limit: number, tasks: Task[]): Promise; /** * @param {Input} input * @param {RawSourceMap | undefined} sourceMap