Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,9 @@ module.exports = {

> **Warning**
>
> The `extractComments` option is not supported, and all comments will be removed by default. This will be fixed in future
> `extractComments` is supported with `@swc/core >= 1.15.30`.
> Only serializable extract conditions are supported: booleans, `"some"`, `"all"`, string patterns, `RegExp` values without flags, or object conditions that resolve to those forms.
> Function conditions and flagged regular expressions are not supported.

**webpack.config.js**

Expand Down
1,375 changes: 775 additions & 600 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"@babel/preset-env": "^7.24.7",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@swc/core": "^1.3.102",
"@swc/core": "^1.15.30",
"@types/node": "^24.2.1",
"@types/serialize-javascript": "^5.0.2",
"@types/uglify-js": "^3.17.5",
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const {

// eslint-disable-next-line jsdoc/reject-any-type
/** @typedef {any} EXPECTED_ANY */
// eslint-disable-next-line jsdoc/require-property
/** @typedef {object} EXPECTED_OBJECT */

/**
* @callback ExtractCommentsFunction
Expand Down
134 changes: 127 additions & 7 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/** @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 */

/**
* @template T
Expand Down Expand Up @@ -77,10 +78,9 @@ async function terserMinify(
minimizerOptions,
extractComments,
) {
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @param {unknown} value value
* @returns {value is object} true when value is object or function
* @returns {value is EXPECTED_OBJECT} true when value is object or function
*/
const isObject = (value) => {
const type = typeof value;
Expand Down Expand Up @@ -333,10 +333,9 @@ async function uglifyJsMinify(
minimizerOptions,
extractComments,
) {
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @param {unknown} value value
* @returns {value is object} true when value is object or function
* @returns {boolean} true when value is object or function
*/
const isObject = (value) => {
const type = typeof value;
Expand Down Expand Up @@ -555,12 +554,112 @@ uglifyJsMinify.supportsWorkerThreads = () => true;
* @param {Input} input input
* @param {RawSourceMap=} sourceMap source map
* @param {CustomOptions=} minimizerOptions options
* @param {ExtractCommentsOptions=} extractComments extract comments option
* @returns {Promise<MinimizedResult>} minimized result
*/
async function swcMinify(input, sourceMap, minimizerOptions) {
async function swcMinify(input, sourceMap, minimizerOptions, extractComments) {
/**
* @param {unknown} value value
* @returns {boolean} true when value is object or function
*/
const isObject = (value) => {
const type = typeof value;

// eslint-disable-next-line no-eq-null, eqeqeq
return value != null && (type === "object" || type === "function");
};

/**
* @param {unknown} extractCommentsOptions extract comments option
* @returns {Error} error for unsupported extract comments option
*/
const createExtractCommentsError = (extractCommentsOptions) =>
new Error(
`The 'extractComments' option for 'swcMinify' only supports booleans, "some", "all", string patterns, RegExp values without flags, or object conditions that resolve to those forms. Received: ${extractCommentsOptions instanceof RegExp ? extractCommentsOptions.toString() : typeof extractCommentsOptions}.`,
);

/**
* @param {unknown} extractCommentsOptions extract comments option
* @returns {{ extractComments: false | true | "some" | "all" | { regex: string }, useDefaultPreserveComments: boolean }} normalized swc extract comments options
*/
const normalizeExtractComments = (extractCommentsOptions) => {
if (typeof extractCommentsOptions === "boolean") {
return {
extractComments: extractCommentsOptions,
useDefaultPreserveComments: !extractCommentsOptions,
};
}

if (typeof extractCommentsOptions === "string") {
return {
extractComments:
extractCommentsOptions === "some" || extractCommentsOptions === "all"
? extractCommentsOptions
: { regex: extractCommentsOptions },
useDefaultPreserveComments: false,
};
}

if (extractCommentsOptions instanceof RegExp) {
if (extractCommentsOptions.flags) {
throw createExtractCommentsError(extractCommentsOptions);
}

return {
extractComments: { regex: extractCommentsOptions.source },
useDefaultPreserveComments: false,
};
}

if (typeof extractCommentsOptions === "function") {
throw createExtractCommentsError(extractCommentsOptions);
}

if (extractCommentsOptions && isObject(extractCommentsOptions)) {
const { condition = "some" } =
/** @type {{ condition?: unknown }} */
(extractCommentsOptions);

if (typeof condition === "boolean") {
return {
extractComments: condition ? "some" : false,
useDefaultPreserveComments: false,
};
}

if (typeof condition === "string") {
return {
extractComments:
condition === "some" || condition === "all"
? condition
: { regex: condition },
useDefaultPreserveComments: false,
};
}

if (condition instanceof RegExp) {
if (condition.flags) {
throw createExtractCommentsError(condition);
}

return {
extractComments: { regex: condition.source },
useDefaultPreserveComments: false,
};
}

throw createExtractCommentsError(condition);
}

return {
extractComments: false,
useDefaultPreserveComments: false,
};
};

/**
* @param {PredefinedOptions<import("@swc/core").JsMinifyOptions> & import("@swc/core").JsMinifyOptions=} swcOptions swc options
* @returns {import("@swc/core").JsMinifyOptions & { sourceMap: undefined | boolean } & { compress: import("@swc/core").TerserCompressOptions }} built 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 = {}) =>
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
Expand All @@ -579,6 +678,7 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
: typeof swcOptions.mangle === "boolean"
? swcOptions.mangle
: { ...swcOptions.mangle },
format: { ...swcOptions.format },
// ecma: swcOptions.ecma,
// keep_classnames: swcOptions.keep_classnames,
// keep_fnames: swcOptions.keep_fnames,
Expand All @@ -599,12 +699,29 @@ async function swcMinify(input, sourceMap, minimizerOptions) {

// Copy `swc` options
const swcOptions = buildSwcOptions(minimizerOptions);
const normalizedExtractComments = normalizeExtractComments(extractComments);

if (!swcOptions.format) {
swcOptions.format = {};
}

// Let `swc` generate a SourceMap
if (sourceMap) {
swcOptions.sourceMap = true;
}

if (
normalizedExtractComments.useDefaultPreserveComments &&
typeof swcOptions.format.comments === "undefined"
) {
swcOptions.format.comments = "some";
}

if (normalizedExtractComments.extractComments !== false) {
/** @type {import("@swc/core").JsMinifyOptions & { extractComments?: false | true | "some" | "all" | { regex: string } }} */
(swcOptions).extractComments = normalizedExtractComments.extractComments;
}

if (swcOptions.compress) {
// More optimizations
if (typeof swcOptions.compress.ecma === "undefined") {
Expand All @@ -621,7 +738,9 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
}

const [[filename, code]] = Object.entries(input);
const result = await swc.minify(code, swcOptions);
const result =
/** @type {import("@swc/core").Output & { extractedComments?: string[] }} */
(await swc.minify(code, swcOptions));

let map;

Expand All @@ -637,6 +756,7 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
return {
code: result.code,
map,
extractedComments: result.extractedComments || [],
};
}

Expand Down
26 changes: 20 additions & 6 deletions test/TerserPlugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {

jest.setTimeout(30000);

const terserPluginName = "TerserPlugin";

expect.addSnapshotSerializer({
test: (value) => {
// For string that are valid JSON
Expand Down Expand Up @@ -153,13 +155,19 @@ describe("TerserPlugin", () => {
},
]);

const emptyPluginCount = countPlugins(multiCompiler.compilers[0]);
const expectedPluginCount = countPlugins(multiCompiler.compilers[1]);
const emptyPluginCount = countPlugins(
multiCompiler.compilers[0],
terserPluginName,
);
const expectedPluginCount = countPlugins(
multiCompiler.compilers[1],
terserPluginName,
);

expect(emptyPluginCount).not.toEqual(expectedPluginCount);

for (const compiler of multiCompiler.compilers.slice(2)) {
const pluginCount = countPlugins(compiler);
const pluginCount = countPlugins(compiler, terserPluginName);

expect(pluginCount).not.toEqual(emptyPluginCount);
expect(pluginCount).toEqual(expectedPluginCount);
Expand Down Expand Up @@ -262,13 +270,19 @@ describe("TerserPlugin", () => {
},
]);

const emptyPluginCount = countPlugins(multiCompiler.compilers[0]);
const expectedPluginCount = countPlugins(multiCompiler.compilers[1]);
const emptyPluginCount = countPlugins(
multiCompiler.compilers[0],
terserPluginName,
);
const expectedPluginCount = countPlugins(
multiCompiler.compilers[1],
terserPluginName,
);

expect(emptyPluginCount).not.toEqual(expectedPluginCount);

for (const compiler of multiCompiler.compilers.slice(2)) {
const pluginCount = countPlugins(compiler);
const pluginCount = countPlugins(compiler, terserPluginName);

expect(pluginCount).not.toEqual(emptyPluginCount);
expect(pluginCount).toEqual(expectedPluginCount);
Expand Down
Loading
Loading