Skip to content

Commit 03143d3

Browse files
authored
feat: support extractComments in swcMinify (#665)
1 parent 783427c commit 03143d3

14 files changed

Lines changed: 1294 additions & 755 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,9 @@ module.exports = {
762762

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

package-lock.json

Lines changed: 775 additions & 600 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"@babel/preset-env": "^7.24.7",
7373
"@commitlint/cli": "^17.7.1",
7474
"@commitlint/config-conventional": "^17.7.0",
75-
"@swc/core": "^1.3.102",
75+
"@swc/core": "^1.15.30",
7676
"@types/node": "^24.2.1",
7777
"@types/serialize-javascript": "^5.0.2",
7878
"@types/uglify-js": "^3.17.5",

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const {
3131

3232
// eslint-disable-next-line jsdoc/reject-any-type
3333
/** @typedef {any} EXPECTED_ANY */
34+
// eslint-disable-next-line jsdoc/require-property
35+
/** @typedef {object} EXPECTED_OBJECT */
3436

3537
/**
3638
* @callback ExtractCommentsFunction

src/utils.js

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
66
/** @typedef {import("./index.js").CustomOptions} CustomOptions */
77
/** @typedef {import("./index.js").RawSourceMap} RawSourceMap */
8+
/** @typedef {import("./index.js").EXPECTED_OBJECT} EXPECTED_OBJECT */
89

910
/**
1011
* @template T
@@ -77,10 +78,9 @@ async function terserMinify(
7778
minimizerOptions,
7879
extractComments,
7980
) {
80-
// eslint-disable-next-line jsdoc/no-restricted-syntax
8181
/**
8282
* @param {unknown} value value
83-
* @returns {value is object} true when value is object or function
83+
* @returns {value is EXPECTED_OBJECT} true when value is object or function
8484
*/
8585
const isObject = (value) => {
8686
const type = typeof value;
@@ -333,10 +333,9 @@ async function uglifyJsMinify(
333333
minimizerOptions,
334334
extractComments,
335335
) {
336-
// eslint-disable-next-line jsdoc/no-restricted-syntax
337336
/**
338337
* @param {unknown} value value
339-
* @returns {value is object} true when value is object or function
338+
* @returns {boolean} true when value is object or function
340339
*/
341340
const isObject = (value) => {
342341
const type = typeof value;
@@ -555,12 +554,112 @@ uglifyJsMinify.supportsWorkerThreads = () => true;
555554
* @param {Input} input input
556555
* @param {RawSourceMap=} sourceMap source map
557556
* @param {CustomOptions=} minimizerOptions options
557+
* @param {ExtractCommentsOptions=} extractComments extract comments option
558558
* @returns {Promise<MinimizedResult>} minimized result
559559
*/
560-
async function swcMinify(input, sourceMap, minimizerOptions) {
560+
async function swcMinify(input, sourceMap, minimizerOptions, extractComments) {
561+
/**
562+
* @param {unknown} value value
563+
* @returns {boolean} true when value is object or function
564+
*/
565+
const isObject = (value) => {
566+
const type = typeof value;
567+
568+
// eslint-disable-next-line no-eq-null, eqeqeq
569+
return value != null && (type === "object" || type === "function");
570+
};
571+
572+
/**
573+
* @param {unknown} extractCommentsOptions extract comments option
574+
* @returns {Error} error for unsupported extract comments option
575+
*/
576+
const createExtractCommentsError = (extractCommentsOptions) =>
577+
new Error(
578+
`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}.`,
579+
);
580+
581+
/**
582+
* @param {unknown} extractCommentsOptions extract comments option
583+
* @returns {{ extractComments: false | true | "some" | "all" | { regex: string }, useDefaultPreserveComments: boolean }} normalized swc extract comments options
584+
*/
585+
const normalizeExtractComments = (extractCommentsOptions) => {
586+
if (typeof extractCommentsOptions === "boolean") {
587+
return {
588+
extractComments: extractCommentsOptions,
589+
useDefaultPreserveComments: !extractCommentsOptions,
590+
};
591+
}
592+
593+
if (typeof extractCommentsOptions === "string") {
594+
return {
595+
extractComments:
596+
extractCommentsOptions === "some" || extractCommentsOptions === "all"
597+
? extractCommentsOptions
598+
: { regex: extractCommentsOptions },
599+
useDefaultPreserveComments: false,
600+
};
601+
}
602+
603+
if (extractCommentsOptions instanceof RegExp) {
604+
if (extractCommentsOptions.flags) {
605+
throw createExtractCommentsError(extractCommentsOptions);
606+
}
607+
608+
return {
609+
extractComments: { regex: extractCommentsOptions.source },
610+
useDefaultPreserveComments: false,
611+
};
612+
}
613+
614+
if (typeof extractCommentsOptions === "function") {
615+
throw createExtractCommentsError(extractCommentsOptions);
616+
}
617+
618+
if (extractCommentsOptions && isObject(extractCommentsOptions)) {
619+
const { condition = "some" } =
620+
/** @type {{ condition?: unknown }} */
621+
(extractCommentsOptions);
622+
623+
if (typeof condition === "boolean") {
624+
return {
625+
extractComments: condition ? "some" : false,
626+
useDefaultPreserveComments: false,
627+
};
628+
}
629+
630+
if (typeof condition === "string") {
631+
return {
632+
extractComments:
633+
condition === "some" || condition === "all"
634+
? condition
635+
: { regex: condition },
636+
useDefaultPreserveComments: false,
637+
};
638+
}
639+
640+
if (condition instanceof RegExp) {
641+
if (condition.flags) {
642+
throw createExtractCommentsError(condition);
643+
}
644+
645+
return {
646+
extractComments: { regex: condition.source },
647+
useDefaultPreserveComments: false,
648+
};
649+
}
650+
651+
throw createExtractCommentsError(condition);
652+
}
653+
654+
return {
655+
extractComments: false,
656+
useDefaultPreserveComments: false,
657+
};
658+
};
659+
561660
/**
562661
* @param {PredefinedOptions<import("@swc/core").JsMinifyOptions> & import("@swc/core").JsMinifyOptions=} swcOptions swc options
563-
* @returns {import("@swc/core").JsMinifyOptions & { sourceMap: undefined | boolean } & { compress: import("@swc/core").TerserCompressOptions }} built swc options
662+
* @returns {import("@swc/core").JsMinifyOptions & { extractComments?: false | true | "some" | "all" | { regex: string } } & { sourceMap: undefined | boolean } & { compress: import("@swc/core").TerserCompressOptions }} built swc options
564663
*/
565664
const buildSwcOptions = (swcOptions = {}) =>
566665
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
@@ -579,6 +678,7 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
579678
: typeof swcOptions.mangle === "boolean"
580679
? swcOptions.mangle
581680
: { ...swcOptions.mangle },
681+
format: { ...swcOptions.format },
582682
// ecma: swcOptions.ecma,
583683
// keep_classnames: swcOptions.keep_classnames,
584684
// keep_fnames: swcOptions.keep_fnames,
@@ -599,12 +699,29 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
599699

600700
// Copy `swc` options
601701
const swcOptions = buildSwcOptions(minimizerOptions);
702+
const normalizedExtractComments = normalizeExtractComments(extractComments);
703+
704+
if (!swcOptions.format) {
705+
swcOptions.format = {};
706+
}
602707

603708
// Let `swc` generate a SourceMap
604709
if (sourceMap) {
605710
swcOptions.sourceMap = true;
606711
}
607712

713+
if (
714+
normalizedExtractComments.useDefaultPreserveComments &&
715+
typeof swcOptions.format.comments === "undefined"
716+
) {
717+
swcOptions.format.comments = "some";
718+
}
719+
720+
if (normalizedExtractComments.extractComments !== false) {
721+
/** @type {import("@swc/core").JsMinifyOptions & { extractComments?: false | true | "some" | "all" | { regex: string } }} */
722+
(swcOptions).extractComments = normalizedExtractComments.extractComments;
723+
}
724+
608725
if (swcOptions.compress) {
609726
// More optimizations
610727
if (typeof swcOptions.compress.ecma === "undefined") {
@@ -621,7 +738,9 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
621738
}
622739

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

626745
let map;
627746

@@ -637,6 +756,7 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
637756
return {
638757
code: result.code,
639758
map,
759+
extractedComments: result.extractedComments || [],
640760
};
641761
}
642762

test/TerserPlugin.test.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525

2626
jest.setTimeout(30000);
2727

28+
const terserPluginName = "TerserPlugin";
29+
2830
expect.addSnapshotSerializer({
2931
test: (value) => {
3032
// For string that are valid JSON
@@ -153,13 +155,19 @@ describe("TerserPlugin", () => {
153155
},
154156
]);
155157

156-
const emptyPluginCount = countPlugins(multiCompiler.compilers[0]);
157-
const expectedPluginCount = countPlugins(multiCompiler.compilers[1]);
158+
const emptyPluginCount = countPlugins(
159+
multiCompiler.compilers[0],
160+
terserPluginName,
161+
);
162+
const expectedPluginCount = countPlugins(
163+
multiCompiler.compilers[1],
164+
terserPluginName,
165+
);
158166

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

161169
for (const compiler of multiCompiler.compilers.slice(2)) {
162-
const pluginCount = countPlugins(compiler);
170+
const pluginCount = countPlugins(compiler, terserPluginName);
163171

164172
expect(pluginCount).not.toEqual(emptyPluginCount);
165173
expect(pluginCount).toEqual(expectedPluginCount);
@@ -262,13 +270,19 @@ describe("TerserPlugin", () => {
262270
},
263271
]);
264272

265-
const emptyPluginCount = countPlugins(multiCompiler.compilers[0]);
266-
const expectedPluginCount = countPlugins(multiCompiler.compilers[1]);
273+
const emptyPluginCount = countPlugins(
274+
multiCompiler.compilers[0],
275+
terserPluginName,
276+
);
277+
const expectedPluginCount = countPlugins(
278+
multiCompiler.compilers[1],
279+
terserPluginName,
280+
);
267281

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

270284
for (const compiler of multiCompiler.compilers.slice(2)) {
271-
const pluginCount = countPlugins(compiler);
285+
const pluginCount = countPlugins(compiler, terserPluginName);
272286

273287
expect(pluginCount).not.toEqual(emptyPluginCount);
274288
expect(pluginCount).toEqual(expectedPluginCount);

0 commit comments

Comments
 (0)