"use strict"; const { minify: terserMinify } = require("terser"); /** @typedef {import("source-map").RawSourceMap} RawSourceMap */ /** @typedef {import("./index.js").ExtractCommentsOptions} ExtractCommentsOptions */ /** @typedef {import("./index.js").CustomMinifyFunction} CustomMinifyFunction */ /** @typedef {import("./index.js").MinifyOptions} MinifyOptions */ /** @typedef {import("terser").MinifyOptions} TerserMinifyOptions */ /** @typedef {import("terser").MinifyOutput} MinifyOutput */ /** @typedef {import("terser").FormatOptions} FormatOptions */ /** @typedef {import("terser").MangleOptions} MangleOptions */ /** @typedef {import("./index.js").ExtractCommentsFunction} ExtractCommentsFunction */ /** @typedef {import("./index.js").ExtractCommentsCondition} ExtractCommentsCondition */ /** * @typedef {Object} InternalMinifyOptions * @property {string} name * @property {string} input * @property {RawSourceMap | undefined} inputSourceMap * @property {ExtractCommentsOptions} extractComments * @property {CustomMinifyFunction | undefined} minify * @property {MinifyOptions} minifyOptions */ /** * @typedef {Array} ExtractedComments */ /** * @typedef {Promise} InternalMinifyResult */ /** * @typedef {TerserMinifyOptions & { sourceMap: undefined } & ({ output: FormatOptions & { beautify: boolean } } | { format: FormatOptions & { beautify: boolean } })} NormalizedTerserMinifyOptions */ /** * @param {TerserMinifyOptions} [terserOptions={}] * @returns {NormalizedTerserMinifyOptions} */ function buildTerserOptions(terserOptions = {}) { return { ...terserOptions, mangle: terserOptions.mangle == null ? true : typeof terserOptions.mangle === "boolean" ? terserOptions.mangle : { ...terserOptions.mangle }, // Ignoring sourceMap from options // eslint-disable-next-line no-undefined sourceMap: undefined, // the `output` option is deprecated ...(terserOptions.format ? { format: { beautify: false, ...terserOptions.format } } : { output: { beautify: false, ...terserOptions.output } }) }; } /** * @param {any} value * @returns {boolean} */ function isObject(value) { const type = typeof value; return value != null && (type === "object" || type === "function"); } /** * @param {ExtractCommentsOptions} extractComments * @param {NormalizedTerserMinifyOptions} terserOptions * @param {ExtractedComments} extractedComments * @returns {ExtractCommentsFunction} */ function buildComments(extractComments, terserOptions, extractedComments) { /** @type {{ [index: string]: ExtractCommentsCondition }} */ const condition = {}; let comments; if (terserOptions.format) { ({ comments } = terserOptions.format); } else if (terserOptions.output) { ({ comments } = terserOptions.output); } condition.preserve = typeof comments !== "undefined" ? comments : false; if (typeof extractComments === "boolean" && extractComments) { condition.extract = "some"; } else if (typeof extractComments === "string" || extractComments instanceof RegExp) { condition.extract = extractComments; } else if (typeof extractComments === "function") { condition.extract = extractComments; } else if (extractComments && isObject(extractComments)) { condition.extract = typeof extractComments.condition === "boolean" && extractComments.condition ? "some" : typeof extractComments.condition !== "undefined" ? extractComments.condition : "some"; } else { // No extract // Preserve using "commentsOpts" or "some" condition.preserve = typeof comments !== "undefined" ? comments : "some"; condition.extract = false; } // Ensure that both conditions are functions ["preserve", "extract"].forEach(key => { /** @type {undefined | string} */ let regexStr; /** @type {undefined | RegExp} */ let regex; switch (typeof condition[key]) { case "boolean": condition[key] = condition[key] ? () => true : () => false; break; case "function": break; case "string": if (condition[key] === "all") { condition[key] = () => true; break; } if (condition[key] === "some") { condition[key] = /** @type {ExtractCommentsFunction} */ (astNode, comment) => (comment.type === "comment2" || comment.type === "comment1") && /@preserve|@lic|@cc_on|^\**!/i.test(comment.value); break; } regexStr = /** @type {string} */ condition[key]; condition[key] = /** @type {ExtractCommentsFunction} */ (astNode, comment) => new RegExp( /** @type {string} */ regexStr).test(comment.value); break; default: regex = /** @type {RegExp} */ condition[key]; condition[key] = /** @type {ExtractCommentsFunction} */ (astNode, comment) => /** @type {RegExp} */ regex.test(comment.value); } }); // Redefine the comments function to extract and preserve // comments according to the two conditions return (astNode, comment) => { if ( /** @type {{ extract: ExtractCommentsFunction }} */ condition.extract(astNode, comment)) { const commentText = comment.type === "comment2" ? `/*${comment.value}*/` : `//${comment.value}`; // Don't include duplicate comments if (!extractedComments.includes(commentText)) { extractedComments.push(commentText); } } return ( /** @type {{ preserve: ExtractCommentsFunction }} */ condition.preserve(astNode, comment) ); }; } /** * @param {InternalMinifyOptions} options * @returns {InternalMinifyResult} */ async function minify(options) { const { name, input, inputSourceMap, minify: minifyFn, minifyOptions } = options; if (minifyFn) { return minifyFn({ [name]: input }, inputSourceMap, minifyOptions); } // Copy terser options const terserOptions = buildTerserOptions(minifyOptions); // Let terser generate a SourceMap if (inputSourceMap) { // @ts-ignore terserOptions.sourceMap = { asObject: true }; } /** @type {ExtractedComments} */ const extractedComments = []; const { extractComments } = options; if (terserOptions.output) { terserOptions.output.comments = buildComments(extractComments, terserOptions, extractedComments); } else if (terserOptions.format) { terserOptions.format.comments = buildComments(extractComments, terserOptions, extractedComments); } const result = await terserMinify({ [name]: input }, terserOptions); return { ...result, extractedComments }; } /** * @param {string} options * @returns {InternalMinifyResult} */ function transform(options) { // 'use strict' => this === undefined (Clean Scope) // Safer for possible security issues, albeit not critical at all here // eslint-disable-next-line no-param-reassign const evaluatedOptions = /** @type {InternalMinifyOptions} */ // eslint-disable-next-line no-new-func new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`)(exports, require, module, __filename, __dirname); return minify(evaluatedOptions); } module.exports.minify = minify; module.exports.transform = transform;