Skip to content

Commit c43d001

Browse files
committed
feat: support extractComments in swcMinify
1 parent 783427c commit c43d001

5 files changed

Lines changed: 502 additions & 18 deletions

File tree

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 when `@swc/core` provides the `extractComments` minify option.
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

src/utils.js

Lines changed: 180 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
/** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
66
/** @typedef {import("./index.js").CustomOptions} CustomOptions */
77
/** @typedef {import("./index.js").RawSourceMap} RawSourceMap */
8+
/** @typedef {import("@swc/core").JsMinifyOptions & { extractComments?: false | true | "some" | "all" | { regex: string } }} SwcMinifyOptionsWithExtractComments */
9+
/** @typedef {import("@swc/core").Output & { extractedComments?: string[] }} SwcMinifyOutput */
810

911
/**
1012
* @template T
@@ -77,10 +79,9 @@ async function terserMinify(
7779
minimizerOptions,
7880
extractComments,
7981
) {
80-
// eslint-disable-next-line jsdoc/no-restricted-syntax
8182
/**
8283
* @param {unknown} value value
83-
* @returns {value is object} true when value is object or function
84+
* @returns {boolean} true when value is object or function
8485
*/
8586
const isObject = (value) => {
8687
const type = typeof value;
@@ -333,10 +334,9 @@ async function uglifyJsMinify(
333334
minimizerOptions,
334335
extractComments,
335336
) {
336-
// eslint-disable-next-line jsdoc/no-restricted-syntax
337337
/**
338338
* @param {unknown} value value
339-
* @returns {value is object} true when value is object or function
339+
* @returns {boolean} true when value is object or function
340340
*/
341341
const isObject = (value) => {
342342
const type = typeof value;
@@ -555,12 +555,145 @@ uglifyJsMinify.supportsWorkerThreads = () => true;
555555
* @param {Input} input input
556556
* @param {RawSourceMap=} sourceMap source map
557557
* @param {CustomOptions=} minimizerOptions options
558+
* @param {ExtractCommentsOptions=} extractComments extract comments option
558559
* @returns {Promise<MinimizedResult>} minimized result
559560
*/
560-
async function swcMinify(input, sourceMap, minimizerOptions) {
561+
async function swcMinify(input, sourceMap, minimizerOptions, extractComments) {
562+
/**
563+
* @param {unknown} value value
564+
* @returns {boolean} true when value is object or function
565+
*/
566+
const isObject = (value) => {
567+
const type = typeof value;
568+
569+
// eslint-disable-next-line no-eq-null, eqeqeq
570+
return value != null && (type === "object" || type === "function");
571+
};
572+
573+
/**
574+
* @param {unknown} extractCommentsOptions extract comments option
575+
* @returns {Error} error for unsupported extract comments option
576+
*/
577+
const createExtractCommentsError = (extractCommentsOptions) =>
578+
new Error(
579+
`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}.`,
580+
);
581+
582+
/**
583+
* @param {unknown} extractCommentsOptions extract comments option
584+
* @returns {{ extractComments: false | true | "some" | "all" | { regex: string }, useDefaultPreserveComments: boolean }} normalized swc extract comments options
585+
*/
586+
const normalizeExtractComments = (extractCommentsOptions) => {
587+
if (typeof extractCommentsOptions === "boolean") {
588+
return {
589+
extractComments: extractCommentsOptions,
590+
useDefaultPreserveComments: !extractCommentsOptions,
591+
};
592+
}
593+
594+
if (typeof extractCommentsOptions === "string") {
595+
return {
596+
extractComments:
597+
extractCommentsOptions === "some" || extractCommentsOptions === "all"
598+
? extractCommentsOptions
599+
: { regex: extractCommentsOptions },
600+
useDefaultPreserveComments: false,
601+
};
602+
}
603+
604+
if (extractCommentsOptions instanceof RegExp) {
605+
if (extractCommentsOptions.flags) {
606+
throw createExtractCommentsError(extractCommentsOptions);
607+
}
608+
609+
return {
610+
extractComments: { regex: extractCommentsOptions.source },
611+
useDefaultPreserveComments: false,
612+
};
613+
}
614+
615+
if (typeof extractCommentsOptions === "function") {
616+
throw createExtractCommentsError(extractCommentsOptions);
617+
}
618+
619+
if (extractCommentsOptions && isObject(extractCommentsOptions)) {
620+
const { condition = "some" } = /** @type {{ condition?: unknown }} */ (
621+
extractCommentsOptions
622+
);
623+
624+
if (typeof condition === "boolean") {
625+
return {
626+
extractComments: condition ? "some" : false,
627+
useDefaultPreserveComments: false,
628+
};
629+
}
630+
631+
if (typeof condition === "string") {
632+
return {
633+
extractComments:
634+
condition === "some" || condition === "all"
635+
? condition
636+
: { regex: condition },
637+
useDefaultPreserveComments: false,
638+
};
639+
}
640+
641+
if (condition instanceof RegExp) {
642+
if (condition.flags) {
643+
throw createExtractCommentsError(condition);
644+
}
645+
646+
return {
647+
extractComments: { regex: condition.source },
648+
useDefaultPreserveComments: false,
649+
};
650+
}
651+
652+
throw createExtractCommentsError(condition);
653+
}
654+
655+
return {
656+
extractComments: false,
657+
useDefaultPreserveComments: false,
658+
};
659+
};
660+
661+
/**
662+
* @param {typeof import("@swc/core")} swc swc module
663+
* @returns {boolean} true when the installed swc build supports comment extraction
664+
*/
665+
const hasExtractCommentsSupport = (swc) => {
666+
try {
667+
if (typeof swc.minifySync !== "function") {
668+
return false;
669+
}
670+
671+
const result = /** @type {SwcMinifyOutput} */ (
672+
swc.minifySync(
673+
"/*! swc */var foo = 1;",
674+
/** @type {SwcMinifyOptionsWithExtractComments} */ ({
675+
compress: false,
676+
mangle: false,
677+
format: {
678+
comments: false,
679+
},
680+
extractComments: true,
681+
}),
682+
)
683+
);
684+
685+
return (
686+
Array.isArray(result.extractedComments) &&
687+
result.extractedComments[0] === "/*! swc */"
688+
);
689+
} catch (_error) {
690+
return false;
691+
}
692+
};
693+
561694
/**
562695
* @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
696+
* @returns {SwcMinifyOptionsWithExtractComments & { sourceMap: undefined | boolean } & { compress: import("@swc/core").TerserCompressOptions }} built swc options
564697
*/
565698
const buildSwcOptions = (swcOptions = {}) =>
566699
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
@@ -579,6 +712,7 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
579712
: typeof swcOptions.mangle === "boolean"
580713
? swcOptions.mangle
581714
: { ...swcOptions.mangle },
715+
format: { ...swcOptions.format },
582716
// ecma: swcOptions.ecma,
583717
// keep_classnames: swcOptions.keep_classnames,
584718
// keep_fnames: swcOptions.keep_fnames,
@@ -599,12 +733,48 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
599733

600734
// Copy `swc` options
601735
const swcOptions = buildSwcOptions(minimizerOptions);
736+
const normalizedExtractComments = normalizeExtractComments(extractComments);
737+
738+
if (!swcOptions.format) {
739+
swcOptions.format = {};
740+
}
602741

603742
// Let `swc` generate a SourceMap
604743
if (sourceMap) {
605744
swcOptions.sourceMap = true;
606745
}
607746

747+
if (
748+
normalizedExtractComments.useDefaultPreserveComments &&
749+
typeof swcOptions.format.comments === "undefined"
750+
) {
751+
swcOptions.format.comments = "some";
752+
}
753+
754+
if (normalizedExtractComments.extractComments !== false) {
755+
if (!hasExtractCommentsSupport(swc)) {
756+
let swcVersion;
757+
758+
try {
759+
({ version: swcVersion } = require("@swc/core/package.json"));
760+
} catch (_error) {
761+
// Ignore
762+
}
763+
764+
return {
765+
errors: [
766+
new Error(
767+
`The 'extractComments' option for 'swcMinify' requires an @swc/core build with comment extraction support${swcVersion ? ` (found ${swcVersion})` : ""}. Please upgrade @swc/core.`,
768+
),
769+
],
770+
};
771+
}
772+
773+
/** @type {SwcMinifyOptionsWithExtractComments} */ (
774+
swcOptions
775+
).extractComments = normalizedExtractComments.extractComments;
776+
}
777+
608778
if (swcOptions.compress) {
609779
// More optimizations
610780
if (typeof swcOptions.compress.ecma === "undefined") {
@@ -621,7 +791,9 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
621791
}
622792

623793
const [[filename, code]] = Object.entries(input);
624-
const result = await swc.minify(code, swcOptions);
794+
const result = /** @type {SwcMinifyOutput} */ (
795+
await swc.minify(code, swcOptions)
796+
);
625797

626798
let map;
627799

@@ -637,6 +809,7 @@ async function swcMinify(input, sourceMap, minimizerOptions) {
637809
return {
638810
code: result.code,
639811
map,
812+
extractedComments: result.extractedComments || [],
640813
};
641814
}
642815

0 commit comments

Comments
 (0)