UNPKG

7.53 kBJavaScriptView Raw
1"use strict";
2
3const {
4 minify: terserMinify
5} = require('terser');
6/** @typedef {import("source-map").RawSourceMap} RawSourceMap */
7
8/** @typedef {import("./index.js").ExtractCommentsOptions} ExtractCommentsOptions */
9
10/** @typedef {import("./index.js").CustomMinifyFunction} CustomMinifyFunction */
11
12/** @typedef {import("./index.js").MinifyOptions} MinifyOptions */
13
14/** @typedef {import("terser").MinifyOptions} TerserMinifyOptions */
15
16/** @typedef {import("terser").MinifyOutput} MinifyOutput */
17
18/** @typedef {import("terser").FormatOptions} FormatOptions */
19
20/** @typedef {import("terser").MangleOptions} MangleOptions */
21
22/** @typedef {import("./index.js").ExtractCommentsFunction} ExtractCommentsFunction */
23
24/** @typedef {import("./index.js").ExtractCommentsCondition} ExtractCommentsCondition */
25
26/**
27 * @typedef {Object} InternalMinifyOptions
28 * @property {string} name
29 * @property {string} input
30 * @property {RawSourceMap} inputSourceMap
31 * @property {ExtractCommentsOptions} extractComments
32 * @property {CustomMinifyFunction} minify
33 * @property {MinifyOptions} minifyOptions
34 */
35
36/**
37 * @typedef {Array<string>} ExtractedComments
38 */
39
40/**
41 * @typedef {Promise<MinifyOutput & { extractedComments?: ExtractedComments}>} InternalMinifyResult
42 */
43
44/**
45 * @typedef {TerserMinifyOptions & { sourceMap: undefined } & ({ output: FormatOptions & { beautify: boolean } } | { format: FormatOptions & { beautify: boolean } })} NormalizedTerserMinifyOptions
46 */
47
48/**
49 * @param {TerserMinifyOptions} [terserOptions={}]
50 * @returns {NormalizedTerserMinifyOptions}
51 */
52
53
54function buildTerserOptions(terserOptions = {}) {
55 return { ...terserOptions,
56 mangle: terserOptions.mangle == null ? true : typeof terserOptions.mangle === 'boolean' ? terserOptions.mangle : { ...terserOptions.mangle
57 },
58 // Ignoring sourceMap from options
59 // eslint-disable-next-line no-undefined
60 sourceMap: undefined,
61 // the `output` option is deprecated
62 ...(terserOptions.format ? {
63 format: {
64 beautify: false,
65 ...terserOptions.format
66 }
67 } : {
68 output: {
69 beautify: false,
70 ...terserOptions.output
71 }
72 })
73 };
74}
75/**
76 * @param {any} value
77 * @returns {boolean}
78 */
79
80
81function isObject(value) {
82 const type = typeof value;
83 return value != null && (type === 'object' || type === 'function');
84}
85/**
86 * @param {ExtractCommentsOptions} extractComments
87 * @param {NormalizedTerserMinifyOptions} terserOptions
88 * @param {ExtractedComments} extractedComments
89 * @returns {ExtractCommentsFunction}
90 */
91
92
93function buildComments(extractComments, terserOptions, extractedComments) {
94 /** @type {{ [index: string]: ExtractCommentsCondition }} */
95 const condition = {};
96 let comments;
97
98 if (terserOptions.format) {
99 ({
100 comments
101 } = terserOptions.format);
102 } else if (terserOptions.output) {
103 ({
104 comments
105 } = terserOptions.output);
106 }
107
108 condition.preserve = typeof comments !== 'undefined' ? comments : false;
109
110 if (typeof extractComments === 'boolean' && extractComments) {
111 condition.extract = 'some';
112 } else if (typeof extractComments === 'string' || extractComments instanceof RegExp) {
113 condition.extract = extractComments;
114 } else if (typeof extractComments === 'function') {
115 condition.extract = extractComments;
116 } else if (extractComments && isObject(extractComments)) {
117 condition.extract = typeof extractComments.condition === 'boolean' && extractComments.condition ? 'some' : typeof extractComments.condition !== 'undefined' ? extractComments.condition : 'some';
118 } else {
119 // No extract
120 // Preserve using "commentsOpts" or "some"
121 condition.preserve = typeof comments !== 'undefined' ? comments : 'some';
122 condition.extract = false;
123 } // Ensure that both conditions are functions
124
125
126 ['preserve', 'extract'].forEach(key => {
127 /** @type {undefined | string} */
128 let regexStr;
129 /** @type {undefined | RegExp} */
130
131 let regex;
132
133 switch (typeof condition[key]) {
134 case 'boolean':
135 condition[key] = condition[key] ? () => true : () => false;
136 break;
137
138 case 'function':
139 break;
140
141 case 'string':
142 if (condition[key] === 'all') {
143 condition[key] = () => true;
144
145 break;
146 }
147
148 if (condition[key] === 'some') {
149 condition[key] =
150 /** @type {ExtractCommentsFunction} */
151 (astNode, comment) => {
152 return (comment.type === 'comment2' || comment.type === 'comment1') && /@preserve|@lic|@cc_on|^\**!/i.test(comment.value);
153 };
154
155 break;
156 }
157
158 regexStr =
159 /** @type {string} */
160 condition[key];
161
162 condition[key] =
163 /** @type {ExtractCommentsFunction} */
164 (astNode, comment) => {
165 return new RegExp(
166 /** @type {string} */
167 regexStr).test(comment.value);
168 };
169
170 break;
171
172 default:
173 regex =
174 /** @type {RegExp} */
175 condition[key];
176
177 condition[key] =
178 /** @type {ExtractCommentsFunction} */
179 (astNode, comment) =>
180 /** @type {RegExp} */
181 regex.test(comment.value);
182
183 }
184 }); // Redefine the comments function to extract and preserve
185 // comments according to the two conditions
186
187 return (astNode, comment) => {
188 if (
189 /** @type {{ extract: ExtractCommentsFunction }} */
190 condition.extract(astNode, comment)) {
191 const commentText = comment.type === 'comment2' ? `/*${comment.value}*/` : `//${comment.value}`; // Don't include duplicate comments
192
193 if (!extractedComments.includes(commentText)) {
194 extractedComments.push(commentText);
195 }
196 }
197
198 return (
199 /** @type {{ preserve: ExtractCommentsFunction }} */
200 condition.preserve(astNode, comment)
201 );
202 };
203}
204/**
205 * @param {InternalMinifyOptions} options
206 * @returns {InternalMinifyResult}
207 */
208
209
210async function minify(options) {
211 const {
212 name,
213 input,
214 inputSourceMap,
215 minify: minifyFn,
216 minifyOptions
217 } = options;
218
219 if (minifyFn) {
220 return minifyFn({
221 [name]: input
222 }, inputSourceMap, minifyOptions);
223 } // Copy terser options
224
225
226 const terserOptions = buildTerserOptions(minifyOptions); // Let terser generate a SourceMap
227
228 if (inputSourceMap) {
229 // @ts-ignore
230 terserOptions.sourceMap = {
231 asObject: true
232 };
233 }
234 /** @type {ExtractedComments} */
235
236
237 const extractedComments = [];
238 const {
239 extractComments
240 } = options;
241
242 if (terserOptions.output) {
243 terserOptions.output.comments = buildComments(extractComments, terserOptions, extractedComments);
244 } else if (terserOptions.format) {
245 terserOptions.format.comments = buildComments(extractComments, terserOptions, extractedComments);
246 }
247
248 const result = await terserMinify({
249 [name]: input
250 }, terserOptions);
251 return { ...result,
252 extractedComments
253 };
254}
255/**
256 * @param {string} options
257 * @returns {InternalMinifyResult}
258 */
259
260
261function transform(options) {
262 // 'use strict' => this === undefined (Clean Scope)
263 // Safer for possible security issues, albeit not critical at all here
264 // eslint-disable-next-line no-param-reassign
265 const evaluatedOptions =
266 /** @type {InternalMinifyOptions} */
267 // eslint-disable-next-line no-new-func
268 new Function('exports', 'require', 'module', '__filename', '__dirname', `'use strict'\nreturn ${options}`)(exports, require, module, __filename, __dirname);
269 return minify(evaluatedOptions);
270}
271
272module.exports.minify = minify;
273module.exports.transform = transform;
\No newline at end of file