diff --git a/README.md b/README.md index b8267fb..12899c0 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ module.exports = { Type: ```ts -type minify = ( +type minifyFn = ( input: Record, sourceMap: import("@jridgewell/trace-mapping").SourceMapInput | undefined, minifyOptions: { @@ -277,6 +277,8 @@ type minify = ( warnings?: (string | Error)[] | undefined; extractedComments?: string[] | undefined; }>; + +type minify = minifyFn | minifyFn[]; ``` Default: `TerserPlugin.terserMinify` @@ -285,10 +287,14 @@ Allows you to override the default minify function. By default plugin uses [terser](https://github.com/terser/terser) package. Useful for using and testing unpublished versions or forks. +An array of functions can also be provided to chain multiple minimizers — the output of each minimizer is fed as input to the next. When an array is used, the [`terserOptions`](#terseroptions) option may also be an array (index-paired with `minify`) or a single object that is reused for every minimizer. + > **Warning** > > **Always use `require` inside `minify` function when `parallel` option enabled**. +#### `function` + **webpack.config.js** ```js @@ -337,6 +343,36 @@ module.exports = { }; ``` +#### `array` + +If an array of functions is passed to the `minify` option, the output of each +minimizer is fed as input to the next one. The `terserOptions` option can be +either an array of option objects (index-paired with `minify`) or a single +object that will be shared by all minimizers. Warnings, errors and extracted +comments from all minimizers are merged together. + +**webpack.config.js** + +```js +module.exports = { + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + minify: [TerserPlugin.terserMinify, TerserPlugin.swcMinify], + // `terserOptions` can be an array of options, one per `minify` entry + terserOptions: [ + // Options for `TerserPlugin.terserMinify` + { mangle: false }, + // Options for `TerserPlugin.swcMinify` + {}, + ], + }), + ], + }, +}; +``` + ### `terserOptions` Type: @@ -360,12 +396,19 @@ interface terserOptions { sourceMap?: boolean | SourceMapOptions; toplevel?: boolean; } + +type options = terserOptions | terserOptions[]; ``` Default: [default](https://github.com/terser/terser#minify-options) Terser [options](https://github.com/terser/terser#minify-options). +When the [`minify`](#minify) option is an array of minimizers, `terserOptions` +can also be an array. Each element is passed to the minimizer at the same +index in the `minify` array. If a single object is provided instead, it is +reused for every minimizer. + **webpack.config.js** ```js diff --git a/src/index.js b/src/index.js index 6352670..dd32b72 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ const { minify } = require("./minify"); const schema = require("./options.json"); const { esbuildMinify, + getEcmaVersion, jsonMinify, memoize, swcMinify, @@ -18,7 +19,6 @@ const { /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */ /** @typedef {import("webpack").Compiler} Compiler */ /** @typedef {import("webpack").Compilation} Compilation */ -/** @typedef {import("webpack").Configuration} Configuration */ /** @typedef {import("webpack").Asset} Asset */ /** @typedef {import("webpack").AssetInfo} AssetInfo */ /** @typedef {import("webpack").TemplatePath} TemplatePath */ @@ -96,14 +96,7 @@ const { /** * @template T - * @typedef {object} PredefinedOptions - * @property {T extends { module?: infer P } ? P : boolean | string=} module true when code is a EC module, otherwise false - * @property {T extends { ecma?: infer P } ? P : number | string=} ecma ecma version - */ - -/** - * @template T - * @typedef {PredefinedOptions & InferDefaultType} MinimizerOptions + * @typedef {T extends EXPECTED_ANY[] ? { [P in keyof T]?: T[P] & InferDefaultType } : T & InferDefaultType} MinimizerOptions */ /** @@ -125,7 +118,7 @@ const { /** * @template T - * @typedef {BasicMinimizerImplementation & MinimizeFunctionHelpers} MinimizerImplementation + * @typedef {T extends EXPECTED_ANY[] ? { [P in keyof T]: BasicMinimizerImplementation & MinimizeFunctionHelpers } : BasicMinimizerImplementation & MinimizeFunctionHelpers} MinimizerImplementation */ /** @@ -136,6 +129,8 @@ const { * @property {RawSourceMap | undefined} inputSourceMap input source map * @property {ExtractCommentsOptions | undefined} extractComments extract comments option * @property {{ implementation: MinimizerImplementation, options: MinimizerOptions }} minimizer minimizer + * @property {boolean=} module true when code is a EC module, otherwise false + * @property {number | string=} ecma ecma version */ /** @@ -185,7 +180,9 @@ class TerserPlugin { // TODO handle json and etc in the next major release // TODO make `minimizer` option instead `minify` and `terserOptions` in the next major release, also rename `terserMinify` to `terserMinimize` const { - minify = /** @type {MinimizerImplementation} */ (terserMinify), + minify = /** @type {MinimizerImplementation} */ ( + /** @type {unknown} */ (terserMinify) + ), terserOptions = /** @type {MinimizerOptions} */ ({}), test = /\.[cm]?js(\?.*)?$/i, extractComments = true, @@ -412,13 +409,17 @@ class TerserPlugin { /** @type {undefined | number} */ let numberOfWorkers; + const implementations = Array.isArray(this.options.minimizer.implementation) + ? this.options.minimizer.implementation + : [this.options.minimizer.implementation]; + const needCreateWorker = optimizeOptions.availableNumberOfCores > 0 && - (typeof this.options.minimizer.implementation.supportsWorker === - "undefined" || - (typeof this.options.minimizer.implementation.supportsWorker === - "function" && - this.options.minimizer.implementation.supportsWorker())); + implementations.every( + (impl) => + typeof impl.supportsWorker === "undefined" || + (typeof impl.supportsWorker === "function" && impl.supportsWorker()), + ); if (needCreateWorker) { // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory @@ -439,12 +440,11 @@ class TerserPlugin { ( new Worker(require.resolve("./minify"), { numWorkers: numberOfWorkers, - enableWorkerThreads: - typeof this.options.minimizer.implementation - .supportsWorkerThreads !== "undefined" - ? this.options.minimizer.implementation.supportsWorkerThreads() !== - false - : true, + enableWorkerThreads: implementations.every( + (impl) => + typeof impl.supportsWorkerThreads === "undefined" || + impl.supportsWorkerThreads() !== false, + ), }) ); @@ -502,6 +502,12 @@ class TerserPlugin { input = input.toString(); } + const clonedMinimizerOptions = Array.isArray( + this.options.minimizer.options, + ) + ? this.options.minimizer.options.map((item) => ({ ...item })) + : { .../** @type {T} */ (this.options.minimizer.options) }; + /** * @type {InternalOptions} */ @@ -511,34 +517,22 @@ class TerserPlugin { inputSourceMap, minimizer: { implementation: this.options.minimizer.implementation, - options: { ...this.options.minimizer.options }, + options: + /** @type {MinimizerOptions} */ + (clonedMinimizerOptions), }, extractComments: this.options.extractComments, }; - if (typeof options.minimizer.options.module === "undefined") { - if (typeof info.javascriptModule !== "undefined") { - options.minimizer.options.module = - /** @type {PredefinedOptions["module"]} */ - (info.javascriptModule); - } else if (/\.mjs(\?.*)?$/i.test(name)) { - options.minimizer.options.module = - /** @type {PredefinedOptions["module"]} */ (true); - } else if (/\.cjs(\?.*)?$/i.test(name)) { - options.minimizer.options.module = - /** @type {PredefinedOptions["module"]} */ (false); - } + if (typeof info.javascriptModule !== "undefined") { + options.module = info.javascriptModule; + } else if (/\.mjs(\?.*)?$/i.test(name)) { + options.module = true; + } else if (/\.cjs(\?.*)?$/i.test(name)) { + options.module = false; } - if (typeof options.minimizer.options.ecma === "undefined") { - options.minimizer.options.ecma = - /** @type {PredefinedOptions["ecma"]} */ - ( - TerserPlugin.getEcmaVersion( - compiler.options.output.environment || {}, - ) - ); - } + options.ecma = getEcmaVersion(compiler.options.output.environment); try { output = await (getWorker @@ -610,10 +604,11 @@ class TerserPlugin { ); } - // Custom functions can return `undefined` or `null` - if (typeof output.code !== "undefined" && output.code !== null) { - let shebang; + let shebang; + // Custom functions can return `undefined` or `null` when the + // minimizer only produced warnings, errors or extracted comments + if (typeof output.code !== "undefined" && output.code !== null) { if ( /** @type {ExtractCommentsObject} */ (this.options.extractComments).banner !== false && @@ -642,73 +637,69 @@ class TerserPlugin { } 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]"; - - let query = ""; - let filename = name; + if (output.extractedComments && output.extractedComments.length > 0) { + const commentsFilename = + /** @type {ExtractCommentsObject} */ + (this.options.extractComments).filename || + "[file].LICENSE.txt[query]"; - const querySplit = filename.indexOf("?"); + let query = ""; + let filename = name; - if (querySplit >= 0) { - query = filename.slice(querySplit); - filename = filename.slice(0, querySplit); - } + const querySplit = filename.indexOf("?"); - const lastSlashIndex = filename.lastIndexOf("/"); - const basename = - lastSlashIndex === -1 - ? filename - : filename.slice(lastSlashIndex + 1); - const data = { filename, basename, query }; + if (querySplit >= 0) { + query = filename.slice(querySplit); + filename = filename.slice(0, querySplit); + } - output.commentsFilename = compilation.getPath( - commentsFilename, - data, - ); + const lastSlashIndex = filename.lastIndexOf("/"); + const basename = + lastSlashIndex === -1 + ? filename + : filename.slice(lastSlashIndex + 1); + const data = { filename, basename, query }; - let banner; + output.commentsFilename = compilation.getPath( + commentsFilename, + data, + ); - // Add a banner to the original file - if ( + // Banner only applies when we have a new source to prepend to + if ( + output.source && + /** @type {ExtractCommentsObject} */ + (this.options.extractComments).banner !== false + ) { + let 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, + ); + } } + + const extractedCommentsString = output.extractedComments + .sort() + .join("\n\n"); + + output.extractedCommentsSource = new RawSource( + `${extractedCommentsString}\n`, + ); } await cacheItem.storePromise({ @@ -732,27 +723,29 @@ class TerserPlugin { } } + // Emit extracted comments file even if the main asset was not + // rewritten (some minimizers only produce comments / warnings / errors) + if (output.extractedCommentsSource) { + allExtractedComments.set(name, { + extractedCommentsSource: output.extractedCommentsSource, + commentsFilename: /** @type {string} */ (output.commentsFilename), + }); + } + if (!output.source) { return; } /** @type {AssetInfo} */ const newInfo = { minimized: true }; - const { source, extractedCommentsSource } = output; - // Write extracted comments to commentsFilename - if (extractedCommentsSource) { - const { commentsFilename } = output; - - newInfo.related = { license: commentsFilename }; - - allExtractedComments.set(name, { - extractedCommentsSource, - commentsFilename, - }); + if (output.extractedCommentsSource) { + newInfo.related = { + license: /** @type {string} */ (output.commentsFilename), + }; } - compilation.updateAsset(name, source, newInfo); + compilation.updateAsset(name, output.source, newInfo); }); } @@ -832,31 +825,6 @@ class TerserPlugin { ); } - /** - * @private - * @param {NonNullable["environment"]>} environment environment - * @returns {number} ecma version - */ - static getEcmaVersion(environment) { - // ES 6th - if ( - environment.arrowFunction || - environment.const || - environment.destructuring || - environment.forOf || - environment.module - ) { - return 2015; - } - - // ES 11th - if (environment.bigIntLiteral || environment.dynamicImport) { - return 2020; - } - - return 5; - } - /** * @param {Compiler} compiler compiler * @returns {void} @@ -872,13 +840,21 @@ class TerserPlugin { compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks( compilation, ); + /** + * @param {BasicMinimizerImplementation & MinimizeFunctionHelpers} impl implementation + * @returns {string} minimizer version or "0.0.0" + */ + const getVersion = (impl) => + typeof impl.getMinimizerVersion !== "undefined" + ? impl.getMinimizerVersion() || "0.0.0" + : "0.0.0"; const data = getSerializeJavascript()({ - minimizer: - typeof this.options.minimizer.implementation.getMinimizerVersion !== - "undefined" - ? this.options.minimizer.implementation.getMinimizerVersion() || - "0.0.0" - : "0.0.0", + minimizer: Array.isArray(this.options.minimizer.implementation) + ? this.options.minimizer.implementation.map(getVersion) + : getVersion( + /** @type {BasicMinimizerImplementation & MinimizeFunctionHelpers} */ + (this.options.minimizer.implementation), + ), options: this.options.minimizer.options, }); diff --git a/src/minify.js b/src/minify.js index bd342f6..ccda648 100644 --- a/src/minify.js +++ b/src/minify.js @@ -1,5 +1,10 @@ /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ /** @typedef {import("./index.js").CustomOptions} CustomOptions */ +/** @typedef {import("./index.js").RawSourceMap} RawSourceMap */ +/** + * @template T + * @typedef {import("./index.js").MinimizerOptions} MinimizerOptions + */ /** * @template T @@ -7,15 +12,79 @@ * @returns {Promise} minified result */ async function minify(options) { - const { name, input, inputSourceMap, extractComments } = options; + const { name, input, inputSourceMap, extractComments, module, ecma } = + options; const { implementation, options: minimizerOptions } = options.minimizer; + const implementations = Array.isArray(implementation) + ? implementation + : [implementation]; + + /** @type {string | undefined} */ + let lastCode; + /** @type {RawSourceMap | undefined} */ + let lastMap; + /** @type {(Error | string)[]} */ + const warnings = []; + /** @type {(Error | string)[]} */ + const errors = []; + /** @type {string[]} */ + const extractedComments = []; + + for (let i = 0; i < implementations.length; i++) { + const currentImplementation = + /** @type {import("./index.js").BasicMinimizerImplementation & import("./index.js").MinimizeFunctionHelpers} */ + (implementations[i]); + const currentOptions = + /** @type {import("./index.js").MinimizerOptions} */ + ( + Array.isArray(minimizerOptions) + ? minimizerOptions[i] || {} + : minimizerOptions || {} + ); + const currentInput = typeof lastCode === "string" ? lastCode : input; + const currentMap = typeof lastCode === "string" ? lastMap : inputSourceMap; + + /** @type {MinimizerOptions} */ + (currentOptions).module = + /** @type {MinimizerOptions} */ + (currentOptions).module || module; + /** @type {MinimizerOptions} */ + (currentOptions).ecma = + /** @type {MinimizerOptions} */ + (currentOptions).ecma || ecma; + + const result = await currentImplementation( + { [name]: currentInput }, + currentMap, + currentOptions, + extractComments, + ); + + if (result.warnings && result.warnings.length > 0) { + warnings.push(...result.warnings); + } + + if (result.errors && result.errors.length > 0) { + errors.push(...result.errors); + } + + if (result.extractedComments && result.extractedComments.length > 0) { + extractedComments.push(...result.extractedComments); + } + + if (typeof result.code === "string") { + lastCode = result.code; + lastMap = result.map; + } + } - return implementation( - { [name]: input }, - inputSourceMap, - minimizerOptions, - extractComments, - ); + return { + code: lastCode, + map: lastMap, + warnings, + errors, + extractedComments, + }; } /** diff --git a/src/options.json b/src/options.json index ec63be9..3cf743c 100644 --- a/src/options.json +++ b/src/options.json @@ -67,8 +67,20 @@ "terserOptions": { "description": "Options for `terser` (by default) or custom `minify` function.", "link": "https://github.com/webpack/terser-webpack-plugin#terseroptions", - "additionalProperties": true, - "type": "object" + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "array", + "minItems": 1, + "items": { + "additionalProperties": true, + "type": "object" + } + } + ] }, "extractComments": { "description": "Whether comments shall be extracted to a separate file.", @@ -158,7 +170,18 @@ "minify": { "description": "Allows you to override default minify function.", "link": "https://github.com/webpack/terser-webpack-plugin#number", - "instanceof": "Function" + "anyOf": [ + { + "instanceof": "Function" + }, + { + "type": "array", + "minItems": 1, + "items": { + "instanceof": "Function" + } + } + ] } } } diff --git a/src/utils.js b/src/utils.js index 7852e54..be71eea 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,13 +8,48 @@ /** @typedef {import("./index.js").EXPECTED_OBJECT} EXPECTED_OBJECT */ /** - * @template T - * @typedef {import("./index.js").PredefinedOptions} PredefinedOptions + * @typedef {string[]} ExtractedComments */ /** - * @typedef {string[]} ExtractedComments + * Map a webpack `output.environment` configuration to the highest + * ECMAScript version that the target is known to support. Returns `5` + * when no ES2015+ features are flagged. + * @param {NonNullable["environment"]>} environment environment + * @returns {number} ecma version (5, 2015, 2017 or 2020) */ +function getEcmaVersion(environment) { + // ES2020 (11th edition) + if ( + environment.bigIntLiteral || + environment.dynamicImport || + environment.dynamicImportInWorker || + environment.globalThis || + environment.optionalChaining + ) { + return 2020; + } + + // ES2017 (8th edition) + if (environment.asyncFunction) { + return 2017; + } + + // ES2015 (6th edition) + if ( + environment.arrowFunction || + environment.const || + environment.destructuring || + environment.forOf || + environment.methodShorthand || + environment.module || + environment.templateLiteral + ) { + return 2015; + } + + return 5; +} const notSettled = Symbol("not-settled"); @@ -206,7 +241,7 @@ async function terserMinify( }; /** - * @param {PredefinedOptions & import("terser").MinifyOptions=} terserOptions terser options + * @param {import("terser").MinifyOptions=} terserOptions terser options * @returns {import("terser").MinifyOptions & { sourceMap: import("terser").SourceMapOptions | undefined } & { compress: import("terser").CompressOptions } & ({ output: import("terser").FormatOptions & { beautify: boolean } } | { format: import("terser").FormatOptions & { beautify: boolean } })} built terser options */ const buildTerserOptions = (terserOptions = {}) => @@ -453,7 +488,7 @@ async function uglifyJsMinify( }; /** - * @param {PredefinedOptions & import("uglify-js").MinifyOptions=} uglifyJsOptions uglify-js options + * @param {import("uglify-js").MinifyOptions & { ecma?: number | string }=} uglifyJsOptions uglify-js options * @returns {import("uglify-js").MinifyOptions & { sourceMap: boolean | import("uglify-js").SourceMapOptions | undefined } & { output: import("uglify-js").OutputOptions & { beautify: boolean } }} uglify-js options */ const buildUglifyJsOptions = (uglifyJsOptions = {}) => { @@ -658,7 +693,7 @@ async function swcMinify(input, sourceMap, minimizerOptions, extractComments) { }; /** - * @param {PredefinedOptions & import("@swc/core").JsMinifyOptions=} swcOptions swc options + * @param {import("@swc/core").JsMinifyOptions=} swcOptions swc options * @returns {import("@swc/core").JsMinifyOptions & { extractComments?: false | true | "some" | "all" | { regex: string } } & { sourceMap: undefined | boolean } & { compress: import("@swc/core").TerserCompressOptions }} built swc options */ const buildSwcOptions = (swcOptions = {}) => @@ -789,7 +824,7 @@ swcMinify.supportsWorkerThreads = () => false; */ async function esbuildMinify(input, sourceMap, minimizerOptions) { /** - * @param {PredefinedOptions & import("esbuild").TransformOptions=} esbuildOptions esbuild options + * @param {import("esbuild").TransformOptions & { ecma?: string | number, module?: boolean }=} esbuildOptions esbuild options * @returns {import("esbuild").TransformOptions} built esbuild options */ const buildEsbuildOptions = (esbuildOptions = {}) => { @@ -952,6 +987,7 @@ function memoize(fn) { module.exports = { esbuildMinify, + getEcmaVersion, jsonMinify, memoize, swcMinify, diff --git a/test/__snapshots__/minify-option.test.js.snap b/test/__snapshots__/minify-option.test.js.snap index 6e92b0b..e0031e1 100644 --- a/test/__snapshots__/minify-option.test.js.snap +++ b/test/__snapshots__/minify-option.test.js.snap @@ -1,9 +1,60 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`minify option should output errors and warning: errors 1`] = ` +exports[`minify option should carry the last good code forward when a step in the array returns no code: assets 1`] = ` +Object { + "main.js": "(()=>{\\"use strict\\";console.log(\\"HERE\\")})(); +/* Appended after skipped middle step */", +} +`; + +exports[`minify option should carry the last good code forward when a step in the array returns no code: errors 1`] = `Array []`; + +exports[`minify option should carry the last good code forward when a step in the array returns no code: warnings 1`] = ` +Array [ + "Warning: middle step did nothing", +] +`; + +exports[`minify option should error when the minimizer returns only extracted comments (no code): errors 1`] = ` +Array [ + "Error: main.js from Terser plugin +Minimizer doesn't return result", +] +`; + +exports[`minify option should error when the minimizer returns only extracted comments (no code): warnings 1`] = `Array []`; + +exports[`minify option should error when the minimizer returns only warnings (no code): errors 1`] = ` Array [ "Error: main.js from Terser plugin Minimizer doesn't return result", +] +`; + +exports[`minify option should error when the minimizer returns only warnings (no code): warnings 1`] = ` +Array [ + "Warning: just a warning, no code", +] +`; + +exports[`minify option should merge warnings and errors from all minimizers in an array: errors 1`] = ` +Array [ + "Error: main.js from Terser plugin +error from first", + "Error: main.js from Terser plugin +error from second", +] +`; + +exports[`minify option should merge warnings and errors from all minimizers in an array: warnings 1`] = ` +Array [ + "Warning: warning from first", + "Warning: warning from second", +] +`; + +exports[`minify option should output errors and warning: errors 1`] = ` +Array [ "Error: main.js from Terser plugin error", ] @@ -220,7 +271,7 @@ Object { /** @license Copyright 2112 Moon. **/ ", "main.js": "/*! For license information please see main.js.LICENSE.txt */ -(()=>{var e,t,r,o={855(e,t,r){r.e(203).then(r.t.bind(r,203,23)),e.exports=Math.random()}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={exports:{}};return o[e](r,r.exports,a),r.exports}a.m=o,c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,a.t=function(e,t){if(1&t&&(e=this(e)),8&t||\\"object\\"==typeof e&&e&&(4&t&&e.__esModule||16&t&&\\"function\\"==typeof e.then))return e;var r=Object.create(null);a.r(r);var o={};i=i||[null,c({}),c([]),c(c)];for(var n=2&t&&e;(\\"object\\"==typeof n||\\"function\\"==typeof n)&&!~i.indexOf(n);n=c(n))Object.getOwnPropertyNames(n).forEach(t=>o[t]=()=>e[t]);return o.default=()=>e,a.d(r,o),r},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>\\"\\"+e+\\".\\"+e+\\".js\\",a.g=function(){if(\\"object\\"==typeof globalThis)return globalThis;try{return this||Function(\\"return this\\")()}catch(e){if(\\"object\\"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),u={},a.l=(e,t,r,o)=>{if(u[e])return void u[e].push(t);if(void 0!==r)for(var n,i,c=document.getElementsByTagName(\\"script\\"),p=0;p{n.onerror=n.onload=null,clearTimeout(f);var o=u[e];if(delete u[e],n.parentNode&&n.parentNode.removeChild(n),o&&o.forEach(e=>e(r)),t)return t(r)},f=setTimeout(s.bind(null,void 0,{type:\\"timeout\\",target:n}),12e4);n.onerror=s.bind(null,n.onerror),n.onload=s.bind(null,n.onload),i&&document.head.appendChild(n)},a.r=e=>{\\"u\\">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},a.g.importScripts&&(p=a.g.location+\\"\\");var i,c,u,p,l=a.g.document;if(!p&&l&&(l.currentScript&&\\"SCRIPT\\"===l.currentScript.tagName.toUpperCase()&&(p=l.currentScript.src),!p)){var s=l.getElementsByTagName(\\"script\\");if(s.length)for(var f=s.length-1;f>-1&&(!p||!/^http(s?):/.test(p));)p=s[f--].src}if(!p)throw Error(\\"Automatic publicPath is not supported in this browser\\");a.p=p=p.replace(/^blob:/,\\"\\").replace(/#.*$/,\\"\\").replace(/\\\\?.*$/,\\"\\").replace(/\\\\/[^\\\\/]+$/,\\"/\\"),e={792:0},a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),c=Error();a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&(\\"load\\"===r.type?\\"missing\\":r.type),i=r&&r.target&&r.target.src;c.message=\\"Loading chunk \\"+t+\` failed. +(()=>{var e,t,r,o={855(e,t,r){r.e(203).then(r.t.bind(r,203,23)),e.exports=Math.random()}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={exports:{}};return o[e](r,r.exports,a),r.exports}a.m=o,c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,a.t=function(e,t){if(1&t&&(e=this(e)),8&t||\\"object\\"==typeof e&&e&&(4&t&&e.__esModule||16&t&&\\"function\\"==typeof e.then))return e;var r=Object.create(null);a.r(r);var o={};i=i||[null,c({}),c([]),c(c)];for(var n=2&t&&e;(\\"object\\"==typeof n||\\"function\\"==typeof n)&&!~i.indexOf(n);n=c(n))Object.getOwnPropertyNames(n).forEach(t=>o[t]=()=>e[t]);return o.default=()=>e,a.d(r,o),r},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>\\"\\"+e+\\".\\"+e+\\".js\\",a.g=function(){if(\\"object\\"==typeof globalThis)return globalThis;try{return this||Function(\\"return this\\")()}catch{if(\\"object\\"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),u={},a.l=(e,t,r,o)=>{if(u[e])return void u[e].push(t);if(void 0!==r)for(var n,i,c=document.getElementsByTagName(\\"script\\"),p=0;p{n.onerror=n.onload=null,clearTimeout(f);var o=u[e];if(delete u[e],n.parentNode&&n.parentNode.removeChild(n),o&&o.forEach(e=>e(r)),t)return t(r)},f=setTimeout(s.bind(null,void 0,{type:\\"timeout\\",target:n}),12e4);n.onerror=s.bind(null,n.onerror),n.onload=s.bind(null,n.onload),i&&document.head.appendChild(n)},a.r=e=>{\\"u\\">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},a.g.importScripts&&(p=a.g.location+\\"\\");var i,c,u,p,l=a.g.document;if(!p&&l&&(l.currentScript&&\\"SCRIPT\\"===l.currentScript.tagName.toUpperCase()&&(p=l.currentScript.src),!p)){var s=l.getElementsByTagName(\\"script\\");if(s.length)for(var f=s.length-1;f>-1&&(!p||!/^http(s?):/.test(p));)p=s[f--].src}if(!p)throw Error(\\"Automatic publicPath is not supported in this browser\\");a.p=p=p.replace(/^blob:/,\\"\\").replace(/#.*$/,\\"\\").replace(/\\\\?.*$/,\\"\\").replace(/\\\\/[^\\\\/]+$/,\\"/\\"),e={792:0},a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),c=Error();a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&(\\"load\\"===r.type?\\"missing\\":r.type),i=r&&r.target&&r.target.src;c.message=\\"Loading chunk \\"+t+\` failed. (\`+n+\\": \\"+i+\\")\\",c.name=\\"ChunkLoadError\\",c.type=n,c.request=i,o[1](c)}},\\"chunk-\\"+t,t)}},t=(t,r)=>{var o,n,[i,c,u]=r,p=0;if(i.some(t=>0!==e[t])){for(o in c)a.o(c,o)&&(a.m[o]=c[o]);u&&u(a)}for(t&&t(r);p{var e,t,r,o={855(e,t,r){r.e(203).then(r.t.bind(r,203,23)),e.exports=Math.random()}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={exports:{}};return o[e](r,r.exports,a),r.exports}a.m=o,c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,a.t=function(e,t){if(1&t&&(e=this(e)),8&t||\\"object\\"==typeof e&&e&&(4&t&&e.__esModule||16&t&&\\"function\\"==typeof e.then))return e;var r=Object.create(null);a.r(r);var o={};i=i||[null,c({}),c([]),c(c)];for(var n=2&t&&e;(\\"object\\"==typeof n||\\"function\\"==typeof n)&&!~i.indexOf(n);n=c(n))Object.getOwnPropertyNames(n).forEach(t=>o[t]=()=>e[t]);return o.default=()=>e,a.d(r,o),r},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>\\"\\"+e+\\".\\"+e+\\".js\\",a.g=function(){if(\\"object\\"==typeof globalThis)return globalThis;try{return this||Function(\\"return this\\")()}catch(e){if(\\"object\\"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),u={},a.l=(e,t,r,o)=>{if(u[e])return void u[e].push(t);if(void 0!==r)for(var n,i,c=document.getElementsByTagName(\\"script\\"),p=0;p{n.onerror=n.onload=null,clearTimeout(f);var o=u[e];if(delete u[e],n.parentNode&&n.parentNode.removeChild(n),o&&o.forEach(e=>e(r)),t)return t(r)},f=setTimeout(s.bind(null,void 0,{type:\\"timeout\\",target:n}),12e4);n.onerror=s.bind(null,n.onerror),n.onload=s.bind(null,n.onload),i&&document.head.appendChild(n)},a.r=e=>{\\"u\\">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},a.g.importScripts&&(p=a.g.location+\\"\\");var i,c,u,p,l=a.g.document;if(!p&&l&&(l.currentScript&&\\"SCRIPT\\"===l.currentScript.tagName.toUpperCase()&&(p=l.currentScript.src),!p)){var s=l.getElementsByTagName(\\"script\\");if(s.length)for(var f=s.length-1;f>-1&&(!p||!/^http(s?):/.test(p));)p=s[f--].src}if(!p)throw Error(\\"Automatic publicPath is not supported in this browser\\");a.p=p=p.replace(/^blob:/,\\"\\").replace(/#.*$/,\\"\\").replace(/\\\\?.*$/,\\"\\").replace(/\\\\/[^\\\\/]+$/,\\"/\\"),e={792:0},a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),c=Error();a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&(\\"load\\"===r.type?\\"missing\\":r.type),i=r&&r.target&&r.target.src;c.message=\\"Loading chunk \\"+t+\` failed. +(()=>{var e,t,r,o={855(e,t,r){r.e(203).then(r.t.bind(r,203,23)),e.exports=Math.random()}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={exports:{}};return o[e](r,r.exports,a),r.exports}a.m=o,c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,a.t=function(e,t){if(1&t&&(e=this(e)),8&t||\\"object\\"==typeof e&&e&&(4&t&&e.__esModule||16&t&&\\"function\\"==typeof e.then))return e;var r=Object.create(null);a.r(r);var o={};i=i||[null,c({}),c([]),c(c)];for(var n=2&t&&e;(\\"object\\"==typeof n||\\"function\\"==typeof n)&&!~i.indexOf(n);n=c(n))Object.getOwnPropertyNames(n).forEach(t=>o[t]=()=>e[t]);return o.default=()=>e,a.d(r,o),r},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>\\"\\"+e+\\".\\"+e+\\".js\\",a.g=function(){if(\\"object\\"==typeof globalThis)return globalThis;try{return this||Function(\\"return this\\")()}catch{if(\\"object\\"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),u={},a.l=(e,t,r,o)=>{if(u[e])return void u[e].push(t);if(void 0!==r)for(var n,i,c=document.getElementsByTagName(\\"script\\"),p=0;p{n.onerror=n.onload=null,clearTimeout(f);var o=u[e];if(delete u[e],n.parentNode&&n.parentNode.removeChild(n),o&&o.forEach(e=>e(r)),t)return t(r)},f=setTimeout(s.bind(null,void 0,{type:\\"timeout\\",target:n}),12e4);n.onerror=s.bind(null,n.onerror),n.onload=s.bind(null,n.onload),i&&document.head.appendChild(n)},a.r=e=>{\\"u\\">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},a.g.importScripts&&(p=a.g.location+\\"\\");var i,c,u,p,l=a.g.document;if(!p&&l&&(l.currentScript&&\\"SCRIPT\\"===l.currentScript.tagName.toUpperCase()&&(p=l.currentScript.src),!p)){var s=l.getElementsByTagName(\\"script\\");if(s.length)for(var f=s.length-1;f>-1&&(!p||!/^http(s?):/.test(p));)p=s[f--].src}if(!p)throw Error(\\"Automatic publicPath is not supported in this browser\\");a.p=p=p.replace(/^blob:/,\\"\\").replace(/#.*$/,\\"\\").replace(/\\\\?.*$/,\\"\\").replace(/\\\\/[^\\\\/]+$/,\\"/\\"),e={792:0},a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),c=Error();a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&(\\"load\\"===r.type?\\"missing\\":r.type),i=r&&r.target&&r.target.src;c.message=\\"Loading chunk \\"+t+\` failed. (\`+n+\\": \\"+i+\\")\\",c.name=\\"ChunkLoadError\\",c.type=n,c.request=i,o[1](c)}},\\"chunk-\\"+t,t)}},t=(t,r)=>{var o,n,[i,c,u]=r,p=0;if(i.some(t=>0!==e[t])){for(o in c)a.o(c,o)&&(a.m[o]=c[o]);u&&u(a)}for(t&&t(r);p{var e,t,r,o={855(e,t,r){r.e(203).then(r.t.bind(r,203,23)),e.exports=Math.random()}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={exports:{}};return o[e](r,r.exports,a),r.exports}a.m=o,c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,a.t=function(e,t){if(1&t&&(e=this(e)),8&t||\\"object\\"==typeof e&&e&&(4&t&&e.__esModule||16&t&&\\"function\\"==typeof e.then))return e;var r=Object.create(null);a.r(r);var o={};i=i||[null,c({}),c([]),c(c)];for(var n=2&t&&e;(\\"object\\"==typeof n||\\"function\\"==typeof n)&&!~i.indexOf(n);n=c(n))Object.getOwnPropertyNames(n).forEach(t=>o[t]=()=>e[t]);return o.default=()=>e,a.d(r,o),r},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>\\"\\"+e+\\".\\"+e+\\".js\\",a.g=function(){if(\\"object\\"==typeof globalThis)return globalThis;try{return this||Function(\\"return this\\")()}catch(e){if(\\"object\\"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),u={},a.l=(e,t,r,o)=>{if(u[e])return void u[e].push(t);if(void 0!==r)for(var n,i,c=document.getElementsByTagName(\\"script\\"),p=0;p{n.onerror=n.onload=null,clearTimeout(f);var o=u[e];if(delete u[e],n.parentNode&&n.parentNode.removeChild(n),o&&o.forEach(e=>e(r)),t)return t(r)},f=setTimeout(s.bind(null,void 0,{type:\\"timeout\\",target:n}),12e4);n.onerror=s.bind(null,n.onerror),n.onload=s.bind(null,n.onload),i&&document.head.appendChild(n)},a.r=e=>{\\"u\\">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},a.g.importScripts&&(p=a.g.location+\\"\\");var i,c,u,p,l=a.g.document;if(!p&&l&&(l.currentScript&&\\"SCRIPT\\"===l.currentScript.tagName.toUpperCase()&&(p=l.currentScript.src),!p)){var s=l.getElementsByTagName(\\"script\\");if(s.length)for(var f=s.length-1;f>-1&&(!p||!/^http(s?):/.test(p));)p=s[f--].src}if(!p)throw Error(\\"Automatic publicPath is not supported in this browser\\");a.p=p=p.replace(/^blob:/,\\"\\").replace(/#.*$/,\\"\\").replace(/\\\\?.*$/,\\"\\").replace(/\\\\/[^\\\\/]+$/,\\"/\\"),e={792:0},a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),c=Error();a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&(\\"load\\"===r.type?\\"missing\\":r.type),i=r&&r.target&&r.target.src;c.message=\\"Loading chunk \\"+t+\` failed. + "main.js": "(()=>{var e,t,r,o={855(e,t,r){r.e(203).then(r.t.bind(r,203,23)),e.exports=Math.random()}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={exports:{}};return o[e](r,r.exports,a),r.exports}a.m=o,c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,a.t=function(e,t){if(1&t&&(e=this(e)),8&t||\\"object\\"==typeof e&&e&&(4&t&&e.__esModule||16&t&&\\"function\\"==typeof e.then))return e;var r=Object.create(null);a.r(r);var o={};i=i||[null,c({}),c([]),c(c)];for(var n=2&t&&e;(\\"object\\"==typeof n||\\"function\\"==typeof n)&&!~i.indexOf(n);n=c(n))Object.getOwnPropertyNames(n).forEach(t=>o[t]=()=>e[t]);return o.default=()=>e,a.d(r,o),r},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>\\"\\"+e+\\".\\"+e+\\".js\\",a.g=function(){if(\\"object\\"==typeof globalThis)return globalThis;try{return this||Function(\\"return this\\")()}catch{if(\\"object\\"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),u={},a.l=(e,t,r,o)=>{if(u[e])return void u[e].push(t);if(void 0!==r)for(var n,i,c=document.getElementsByTagName(\\"script\\"),p=0;p{n.onerror=n.onload=null,clearTimeout(f);var o=u[e];if(delete u[e],n.parentNode&&n.parentNode.removeChild(n),o&&o.forEach(e=>e(r)),t)return t(r)},f=setTimeout(s.bind(null,void 0,{type:\\"timeout\\",target:n}),12e4);n.onerror=s.bind(null,n.onerror),n.onload=s.bind(null,n.onload),i&&document.head.appendChild(n)},a.r=e=>{\\"u\\">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\\"Module\\"}),Object.defineProperty(e,\\"__esModule\\",{value:!0})},a.g.importScripts&&(p=a.g.location+\\"\\");var i,c,u,p,l=a.g.document;if(!p&&l&&(l.currentScript&&\\"SCRIPT\\"===l.currentScript.tagName.toUpperCase()&&(p=l.currentScript.src),!p)){var s=l.getElementsByTagName(\\"script\\");if(s.length)for(var f=s.length-1;f>-1&&(!p||!/^http(s?):/.test(p));)p=s[f--].src}if(!p)throw Error(\\"Automatic publicPath is not supported in this browser\\");a.p=p=p.replace(/^blob:/,\\"\\").replace(/#.*$/,\\"\\").replace(/\\\\?.*$/,\\"\\").replace(/\\\\/[^\\\\/]+$/,\\"/\\"),e={792:0},a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),c=Error();a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&(\\"load\\"===r.type?\\"missing\\":r.type),i=r&&r.target&&r.target.src;c.message=\\"Loading chunk \\"+t+\` failed. (\`+n+\\": \\"+i+\\")\\",c.name=\\"ChunkLoadError\\",c.type=n,c.request=i,o[1](c)}},\\"chunk-\\"+t,t)}},t=(t,r)=>{var o,n,[i,c,u]=r,p=0;if(i.some(t=>0!==e[t])){for(o in c)a.o(c,o)&&(a.m[o]=c[o]);u&&u(a)}for(t&&t(r);p{\\"use strict\\";console.log(\\"HERE\\")})();", +} +`; + +exports[`minify option should work when \`minify\` and \`terserOptions\` are both arrays: errors 1`] = `Array []`; + +exports[`minify option should work when \`minify\` and \`terserOptions\` are both arrays: warnings 1`] = `Array []`; + +exports[`minify option should work when \`minify\` is an array of functions: assets 1`] = ` +Object { + "main.js": "(()=>{\\"use strict\\";console.log(\\"HERE\\")})(); +/* Appended by second minimizer */", +} +`; + +exports[`minify option should work when \`minify\` is an array of functions: errors 1`] = `Array []`; + +exports[`minify option should work when \`minify\` is an array of functions: warnings 1`] = `Array []`; + exports[`minify option should work when the "parallel" option is "false": assets 1`] = ` Object { "main.js": "(()=>{\\"use strict\\";console.log(\\"HERE\\")})();", diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index b08ce89..33da749 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -120,20 +120,40 @@ exports[`validation validate 8`] = ` exports[`validation validate 9`] = ` "Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. - - options.minify should be an instance of function. - -> Allows you to override default minify function. - -> Read more at https://github.com/webpack/terser-webpack-plugin#number" + - options.minify should be a non-empty array." `; exports[`validation validate 10`] = ` "Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. - - options.terserOptions should be an object: - object { … } - -> Options for \`terser\` (by default) or custom \`minify\` function. - -> Read more at https://github.com/webpack/terser-webpack-plugin#terseroptions" + - options.minify should be one of these: + function | [function, ...] (should not have fewer than 1 item) + -> Allows you to override default minify function. + -> Read more at https://github.com/webpack/terser-webpack-plugin#number + Details: + * options.minify should be an instance of function. + * options.minify should be an array: + [function, ...] (should not have fewer than 1 item)" `; exports[`validation validate 11`] = ` +"Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. + - options.terserOptions should be a non-empty array." +`; + +exports[`validation validate 12`] = ` +"Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. + - options.terserOptions should be one of these: + object { … } | [object { … }, ...] (should not have fewer than 1 item) + -> Options for \`terser\` (by default) or custom \`minify\` function. + -> Read more at https://github.com/webpack/terser-webpack-plugin#terseroptions + Details: + * options.terserOptions should be an object: + object { … } + * options.terserOptions should be an array: + [object { … }, ...] (should not have fewer than 1 item)" +`; + +exports[`validation validate 13`] = ` "Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. - options.extractComments should be one of these: boolean | non-empty string | RegExp | function | object { condition?, filename?, banner? } @@ -151,7 +171,7 @@ exports[`validation validate 11`] = ` * options.extractComments.condition should be an instance of function." `; -exports[`validation validate 12`] = ` +exports[`validation validate 14`] = ` "Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. - options.extractComments should be one of these: boolean | non-empty string | RegExp | function | object { condition?, filename?, banner? } @@ -167,7 +187,7 @@ exports[`validation validate 12`] = ` * options.extractComments.filename should be an instance of function." `; -exports[`validation validate 13`] = ` +exports[`validation validate 15`] = ` "Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. - options.extractComments should be one of these: boolean | non-empty string | RegExp | function | object { condition?, filename?, banner? } @@ -184,13 +204,13 @@ exports[`validation validate 13`] = ` * options.extractComments.banner should be an instance of function." `; -exports[`validation validate 14`] = ` +exports[`validation validate 16`] = ` "Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. - options.extractComments has an unknown property 'unknown'. These properties are valid: object { condition?, filename?, banner? }" `; -exports[`validation validate 15`] = ` +exports[`validation validate 17`] = ` "Invalid options object. Terser Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: object { test?, include?, exclude?, terserOptions?, extractComments?, parallel?, minify? }" diff --git a/test/__snapshots__/worker.test.js.snap b/test/__snapshots__/worker.test.js.snap index 44d1c79..ff7a66d 100644 --- a/test/__snapshots__/worker.test.js.snap +++ b/test/__snapshots__/worker.test.js.snap @@ -6,179 +6,221 @@ Object { // Comment /* duplicate */ /* duplicate */", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when minimizerOptions.compress.comments is boolean 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when minimizerOptions.compress.comments is object 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when minimizerOptions.extractComments is number 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when minimizerOptions.mangle is "null" 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when minimizerOptions.mangle is boolean 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when minimizerOptions.mangle is object 1`] = ` Object { "code": "var a=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when minimizerOptions.output.comments is string: some 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is "all" value 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [ "/* hello */", "// Comment", "/* duplicate */", ], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is "false" 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is "some" value 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is "true" 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is Function 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [ "/* hello */", "// Comment", "/* duplicate */", ], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is Object with "all" value 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [ "/* hello */", "// Comment", "/* duplicate */", ], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is Object with "some" value 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is Object with "true" value 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is RegExp 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [ "/* hello */", ], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.extractComments is empty Object 1`] = ` Object { "code": "var foo=1;", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot when options.output.comments is "some" 1`] = ` -"var foo = 1;/* hello */ -// Comment -/* duplicate */ -/* duplicate */" +Object { + "code": undefined, + "errors": Array [], + "extractedComments": Array [], + "map": undefined, + "warnings": Array [], +} `; exports[`worker should match snapshot with extract option set to a single file 1`] = ` Object { "code": "/******/function hello(o){console.log(o)}", + "errors": Array [], "extractedComments": Array [], "map": undefined, + "warnings": Array [], } `; exports[`worker should match snapshot with options.inputSourceMap 1`] = ` Object { "code": "function foo(f){if(f)return bar()}", + "errors": Array [], "extractedComments": Array [], "map": Object { "ignoreList": Array [], @@ -193,5 +235,6 @@ Object { ], "version": 3, }, + "warnings": Array [], } `; diff --git a/test/getEcmaVersion.test.js b/test/getEcmaVersion.test.js new file mode 100644 index 0000000..703ac08 --- /dev/null +++ b/test/getEcmaVersion.test.js @@ -0,0 +1,65 @@ +import { getEcmaVersion } from "../src/utils"; + +describe("getEcmaVersion", () => { + it("returns 5 when no ES feature flags are set", () => { + expect(getEcmaVersion({})).toBe(5); + expect(getEcmaVersion({ document: true })).toBe(5); + expect(getEcmaVersion({ nodePrefixForCoreModules: true })).toBe(5); + }); + + it("returns 2015 for ES2015 feature flags", () => { + expect(getEcmaVersion({ arrowFunction: true })).toBe(2015); + expect(getEcmaVersion({ const: true })).toBe(2015); + expect(getEcmaVersion({ destructuring: true })).toBe(2015); + expect(getEcmaVersion({ forOf: true })).toBe(2015); + expect(getEcmaVersion({ methodShorthand: true })).toBe(2015); + expect(getEcmaVersion({ module: true })).toBe(2015); + expect(getEcmaVersion({ templateLiteral: true })).toBe(2015); + }); + + it("returns 2017 for ES2017 feature flags", () => { + expect(getEcmaVersion({ asyncFunction: true })).toBe(2017); + }); + + it("returns 2020 for ES2020 feature flags", () => { + expect(getEcmaVersion({ bigIntLiteral: true })).toBe(2020); + expect(getEcmaVersion({ dynamicImport: true })).toBe(2020); + expect(getEcmaVersion({ dynamicImportInWorker: true })).toBe(2020); + expect(getEcmaVersion({ globalThis: true })).toBe(2020); + expect(getEcmaVersion({ optionalChaining: true })).toBe(2020); + }); + + it("returns the highest matching version when several flags are set", () => { + expect( + getEcmaVersion({ + arrowFunction: true, + asyncFunction: true, + bigIntLiteral: true, + }), + ).toBe(2020); + + expect( + getEcmaVersion({ + arrowFunction: true, + asyncFunction: true, + }), + ).toBe(2017); + + expect( + getEcmaVersion({ + arrowFunction: true, + const: true, + }), + ).toBe(2015); + }); + + it("ignores non-ES feature flags", () => { + expect( + getEcmaVersion({ + document: true, + nodePrefixForCoreModules: true, + arrowFunction: true, + }), + ).toBe(2015); + }); +}); diff --git a/test/minify-option.test.js b/test/minify-option.test.js index 244dbd0..345e4c3 100644 --- a/test/minify-option.test.js +++ b/test/minify-option.test.js @@ -162,6 +162,7 @@ describe("minify option", () => { new TerserPlugin({ minify: () => ({ + code: "1", errors: ["error"], warnings: ["warning"], }), @@ -945,4 +946,154 @@ describe("minify option", () => { expect(getErrors(stats)).toMatchSnapshot("errors"); expect(getWarnings(stats)).toMatchSnapshot("warnings"); }); + + it("should work when `minify` is an array of functions", async () => { + const compiler = getCompiler({ + entry: path.resolve(__dirname, "./fixtures/minify/es6.js"), + output: { + path: path.resolve(__dirname, "./dist-terser"), + filename: "[name].js", + chunkFilename: "[id].[name].js", + }, + }); + + new TerserPlugin({ + minify: [ + (file, sourceMap, minimizerOptions) => + require("terser").minify(file, minimizerOptions), + async (file) => { + const [code] = Object.values(file); + + return { code: `${code}\n/* Appended by second minimizer */` }; + }, + ], + terserOptions: { + mangle: false, + }, + }).apply(compiler); + + const stats = await compile(compiler); + + expect(readsAssets(compiler, stats)).toMatchSnapshot("assets"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + }); + + it("should work when `minify` and `terserOptions` are both arrays", async () => { + const compiler = getCompiler({ + entry: path.resolve(__dirname, "./fixtures/minify/es6.js"), + output: { + path: path.resolve(__dirname, "./dist-terser"), + filename: "[name].js", + chunkFilename: "[id].[name].js", + }, + }); + + new TerserPlugin({ + minify: [ + (file, sourceMap, minimizerOptions) => + require("terser").minify(file, minimizerOptions), + (file, sourceMap, minimizerOptions) => + require("terser").minify(file, minimizerOptions), + ], + terserOptions: [ + { mangle: false }, + { mangle: true, compress: { passes: 2 } }, + ], + }).apply(compiler); + + const stats = await compile(compiler); + + expect(readsAssets(compiler, stats)).toMatchSnapshot("assets"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + }); + + it("should merge warnings and errors from all minimizers in an array", async () => { + const compiler = getCompiler({ + entry: path.resolve(__dirname, "./fixtures/minify/es6.js"), + }); + + new TerserPlugin({ + parallel: false, + minify: [ + async (file) => ({ + code: Object.values(file)[0], + warnings: ["warning from first"], + errors: ["error from first"], + }), + async (file) => ({ + code: Object.values(file)[0], + warnings: ["warning from second"], + errors: ["error from second"], + }), + ], + }).apply(compiler); + + const stats = await compile(compiler); + + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + }); + + it("should error when the minimizer returns only warnings (no code)", async () => { + const compiler = getCompiler({ + entry: path.resolve(__dirname, "./fixtures/minify/es6.js"), + }); + + new TerserPlugin({ + parallel: false, + minify: async () => ({ warnings: ["just a warning, no code"] }), + }).apply(compiler); + + const stats = await compile(compiler); + + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + }); + + it("should error when the minimizer returns only extracted comments (no code)", async () => { + const compiler = getCompiler({ + entry: path.resolve(__dirname, "./fixtures/minify/es6.js"), + }); + + new TerserPlugin({ + parallel: false, + minify: async () => ({ + extractedComments: ["/*! @license from no-code minimizer */"], + }), + }).apply(compiler); + + const stats = await compile(compiler); + + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + }); + + it("should carry the last good code forward when a step in the array returns no code", async () => { + const compiler = getCompiler({ + entry: path.resolve(__dirname, "./fixtures/minify/es6.js"), + }); + + new TerserPlugin({ + parallel: false, + minify: [ + (file, sourceMap, minimizerOptions) => + require("terser").minify(file, minimizerOptions), + // Middle step returns only a warning - next step must get the previous code + async () => ({ warnings: ["middle step did nothing"] }), + async (file) => { + const [code] = Object.values(file); + + return { code: `${code}\n/* Appended after skipped middle step */` }; + }, + ], + }).apply(compiler); + + const stats = await compile(compiler); + + expect(readsAssets(compiler, stats)).toMatchSnapshot("assets"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + }); }); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index a7dbfb2..2fb4081 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -123,6 +123,20 @@ describe("validation", () => { new TerserPlugin({ minify() {} }); }).not.toThrow(); + expect(() => { + new TerserPlugin({ minify: [() => ({ code: "" })] }); + }).not.toThrow(); + + expect(() => { + new TerserPlugin({ + minify: [() => ({ code: "" }), () => ({ code: "" })], + }); + }).not.toThrow(); + + expect(() => { + new TerserPlugin({ minify: [] }); + }).toThrowErrorMatchingSnapshot(); + expect(() => { new TerserPlugin({ minify: true }); }).toThrowErrorMatchingSnapshot(); @@ -131,6 +145,18 @@ describe("validation", () => { new TerserPlugin({ terserOptions: {} }); }).not.toThrow(); + expect(() => { + new TerserPlugin({ terserOptions: [{}] }); + }).not.toThrow(); + + expect(() => { + new TerserPlugin({ terserOptions: [{}, {}] }); + }).not.toThrow(); + + expect(() => { + new TerserPlugin({ terserOptions: [] }); + }).toThrowErrorMatchingSnapshot(); + expect(() => { new TerserPlugin({ terserOptions: null }); }).toThrowErrorMatchingSnapshot(); diff --git a/types/index.d.ts b/types/index.d.ts index 4bd45a3..9b0fc60 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -31,12 +31,6 @@ declare class TerserPlugin { * @returns {number} number of cores for parallelism */ private static getAvailableNumberOfCores; - /** - * @private - * @param {NonNullable["environment"]>} environment environment - * @returns {number} ecma version - */ - private static getEcmaVersion; /** * @param {BasePluginOptions & DefinedDefaultMinimizerAndOptions=} options options */ @@ -75,7 +69,6 @@ declare namespace TerserPlugin { Schema, Compiler, Compilation, - Configuration, Asset, AssetInfo, TemplatePath, @@ -97,7 +90,6 @@ declare namespace TerserPlugin { Input, CustomOptions, InferDefaultType, - PredefinedOptions, MinimizerOptions, BasicMinimizerImplementation, MinimizeFunctionHelpers, @@ -118,7 +110,6 @@ import { jsonMinify } from "./utils"; type Schema = import("schema-utils/declarations/validate").Schema; type Compiler = import("webpack").Compiler; type Compilation = import("webpack").Compilation; -type Configuration = import("webpack").Configuration; type Asset = import("webpack").Asset; type AssetInfo = import("webpack").AssetInfo; type TemplatePath = import("webpack").TemplatePath; @@ -216,29 +207,9 @@ type CustomOptions = { [key: string]: EXPECTED_ANY; }; type InferDefaultType = T extends infer U ? U : CustomOptions; -type PredefinedOptions = { - /** - * true when code is a EC module, otherwise false - */ - module?: - | (T extends { - module?: infer P; - } - ? P - : boolean | string) - | undefined; - /** - * ecma version - */ - ecma?: - | (T extends { - ecma?: infer P; - } - ? P - : number | string) - | undefined; -}; -type MinimizerOptions = PredefinedOptions & InferDefaultType; +type MinimizerOptions = T extends EXPECTED_ANY[] + ? { [P in keyof T]?: T[P] & InferDefaultType } + : T & InferDefaultType; type BasicMinimizerImplementation = ( input: Input, sourceMap: RawSourceMap | undefined, @@ -259,8 +230,12 @@ type MinimizeFunctionHelpers = { */ supportsWorker?: (() => boolean | undefined) | undefined; }; -type MinimizerImplementation = BasicMinimizerImplementation & - MinimizeFunctionHelpers; +type MinimizerImplementation = T extends EXPECTED_ANY[] + ? { + [P in keyof T]: BasicMinimizerImplementation & + MinimizeFunctionHelpers; + } + : BasicMinimizerImplementation & MinimizeFunctionHelpers; type InternalOptions = { /** * name @@ -285,6 +260,14 @@ type InternalOptions = { implementation: MinimizerImplementation; options: MinimizerOptions; }; + /** + * true when code is a EC module, otherwise false + */ + module?: boolean | undefined; + /** + * ecma version + */ + ecma?: (number | string) | undefined; }; type MinimizerWorker = JestWorker & { transform: (options: string) => Promise; diff --git a/types/minify.d.ts b/types/minify.d.ts index c574460..dac0078 100644 --- a/types/minify.d.ts +++ b/types/minify.d.ts @@ -1,7 +1,14 @@ export type MinimizedResult = import("./index.js").MinimizedResult; export type CustomOptions = import("./index.js").CustomOptions; +export type RawSourceMap = import("./index.js").RawSourceMap; +export type MinimizerOptions = import("./index.js").MinimizerOptions; /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ /** @typedef {import("./index.js").CustomOptions} CustomOptions */ +/** @typedef {import("./index.js").RawSourceMap} RawSourceMap */ +/** + * @template T + * @typedef {import("./index.js").MinimizerOptions} MinimizerOptions + */ /** * @template T * @param {import("./index.js").InternalOptions} options options diff --git a/types/utils.d.ts b/types/utils.d.ts index 0865452..7a78f04 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -1,5 +1,3 @@ -export type Task = () => Promise; -export type FunctionReturning = () => T; export type ExtractCommentsOptions = import("./index.js").ExtractCommentsOptions; export type ExtractCommentsFunction = @@ -11,8 +9,9 @@ export type MinimizedResult = import("./index.js").MinimizedResult; export type CustomOptions = import("./index.js").CustomOptions; export type RawSourceMap = import("./index.js").RawSourceMap; export type EXPECTED_OBJECT = import("./index.js").EXPECTED_OBJECT; -export type PredefinedOptions = import("./index.js").PredefinedOptions; export type ExtractedComments = string[]; +export type Task = () => Promise; +export type FunctionReturning = () => T; /** * @param {Input} input input * @param {RawSourceMap=} sourceMap source map @@ -34,6 +33,29 @@ export namespace esbuildMinify { */ function supportsWorkerThreads(): boolean | undefined; } +/** @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").CustomOptions} CustomOptions */ +/** @typedef {import("./index.js").RawSourceMap} RawSourceMap */ +/** @typedef {import("./index.js").EXPECTED_OBJECT} EXPECTED_OBJECT */ +/** + * @typedef {string[]} ExtractedComments + */ +/** + * Map a webpack `output.environment` configuration to the highest + * ECMAScript version that the target is known to support. Returns `5` + * when no ES2015+ features are flagged. + * @param {NonNullable["environment"]>} environment environment + * @returns {number} ecma version (5, 2015, 2017 or 2020) + */ +export function getEcmaVersion( + environment: NonNullable< + NonNullable["environment"] + >, +): number; /** * @param {Input} input input * @param {RawSourceMap=} sourceMap source map