html-minifier-next 4.13.0 → 4.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -14
- package/cli.js +1 -1
- package/dist/htmlminifier.cjs +94 -26
- package/dist/htmlminifier.esm.bundle.js +94 -26
- package/dist/types/htmlminifier.d.ts +10 -3
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +3 -3
- package/dist/types/lib/options.d.ts.map +1 -1
- package/package.json +10 -1
- package/src/htmlminifier.js +23 -3
- package/src/lib/options.js +71 -23
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
161
161
|
| `maxInputLength`<br>`--max-input-length` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
|
|
162
162
|
| `maxLineLength`<br>`--max-line-length` | Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points | |
|
|
163
163
|
| `minifyCSS`<br>`--minify-css` | Minify CSS in `style` elements and `style` attributes (uses [Lightning CSS](https://lightningcss.dev/)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
|
|
164
|
-
| `minifyJS`<br>`--minify-js` | Minify JavaScript in `script` elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
|
|
164
|
+
| `minifyJS`<br>`--minify-js` | Minify JavaScript in `script` elements and event attributes (uses [Terser](https://github.com/terser/terser) or [SWC](https://swc.rs/)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
|
|
165
165
|
| `minifyURLs`<br>`--minify-urls` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `String`, `Object`, `Function(text)`, `async Function(text)`) |
|
|
166
166
|
| `noNewlinesBeforeTagClose`<br>`--no-newlines-before-tag-close` | Never add a newline before a tag that closes an element | `false` |
|
|
167
167
|
| `partialMarkup`<br>`--partial-markup` | Treat input as a partial HTML fragment, preserving stray end tags (closing tags without opening tags) and preventing auto-closing of unclosed tags at end of input | `false` |
|
|
@@ -189,7 +189,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
189
189
|
|
|
190
190
|
Minifier options like `sortAttributes` and `sortClassName` won’t impact the plain‑text size of the output. However, using these options for more consistent ordering improves the compression ratio for gzip and Brotli used over HTTP.
|
|
191
191
|
|
|
192
|
-
### CSS minification
|
|
192
|
+
### CSS minification
|
|
193
193
|
|
|
194
194
|
When `minifyCSS` is set to `true`, HTML Minifier Next uses [Lightning CSS](https://lightningcss.dev/) to minify CSS in `<style>` elements and `style` attributes. Lightning CSS provides excellent minification by default.
|
|
195
195
|
|
|
@@ -228,40 +228,98 @@ const result = await minify(html, {
|
|
|
228
228
|
});
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
+
### JavaScript minification
|
|
232
|
+
|
|
233
|
+
When `minifyJS` is set to `true`, HTML Minifier Next uses [Terser](https://github.com/terser/terser) by default to minify JavaScript in `<script>` elements and event attributes.
|
|
234
|
+
|
|
235
|
+
You can choose between different JS minifiers using the `engine` field:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
const result = await minify(html, {
|
|
239
|
+
minifyJS: {
|
|
240
|
+
engine: 'swc', // Use swc for faster minification
|
|
241
|
+
// SWC-specific options here
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Available engines:**
|
|
247
|
+
|
|
248
|
+
* `terser` (default): The standard JavaScript minifier with excellent compression
|
|
249
|
+
* [`swc`](https://swc.rs/): Rust-based minifier that’s significantly faster than Terser (requires separate installation)
|
|
250
|
+
|
|
251
|
+
**To use swc**, install it as a dependency:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
npm i @swc/core
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Important:** Inline event handlers (e.g., `onclick="return false"`) always use Terser regardless of the `engine` setting, as SWC doesn’t support bare return statements. This is handled automatically—you don’t need to do anything special.
|
|
258
|
+
|
|
259
|
+
You can pass engine-specific configuration options:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
// Using Terser with custom options
|
|
263
|
+
const result = await minify(html, {
|
|
264
|
+
minifyJS: {
|
|
265
|
+
compress: {
|
|
266
|
+
drop_console: true // Remove console.log statements
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Using SWC for faster minification
|
|
272
|
+
const result = await minify(html, {
|
|
273
|
+
minifyJS: {
|
|
274
|
+
engine: 'swc'
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
For advanced usage, you can also pass a function:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
const result = await minify(html, {
|
|
283
|
+
minifyJS: function(text, inline) {
|
|
284
|
+
// `text`: JavaScript string to minify
|
|
285
|
+
// `inline`: `true` for event handlers (e.g., `onclick`), `false` for `<script>` elements
|
|
286
|
+
return yourCustomMinifier(text);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
231
291
|
## Minification comparison
|
|
232
292
|
|
|
233
293
|
How does HTML Minifier Next compare to other minifiers? (All with the most aggressive settings—though without [hyper-optimization](https://meiert.com/blog/the-ways-of-writing-html/#toc-hyper-optimized)—and against some large documents.—Minimize does not minify CSS or JS.)
|
|
234
294
|
|
|
235
295
|
<!-- Auto-generated benchmarks, don’t edit -->
|
|
236
|
-
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next)<br>[](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[](https://socket.dev/npm/package/minimize) | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
296
|
+
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next) ([config](https://github.com/j9t/html-minifier-next/blob/main/benchmarks/html-minifier.json))<br>[](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[](https://socket.dev/npm/package/minimize) | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
237
297
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
238
|
-
| [A List Apart](https://alistapart.com/) | 59 | **
|
|
298
|
+
| [A List Apart](https://alistapart.com/) | 59 | **50** | **50** | 51 | 52 | 51 | 54 | 52 |
|
|
239
299
|
| [Apple](https://www.apple.com/) | 266 | **207** | **207** | 236 | 239 | 240 | 242 | 243 |
|
|
240
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
300
|
+
| [BBC](https://www.bbc.co.uk/) | 701 | **636** | 646 | 658 | 659 | 660 | 695 | n/a |
|
|
241
301
|
| [CERN](https://home.cern/) | 152 | 85 | **84** | 91 | 91 | 91 | 93 | 96 |
|
|
242
302
|
| [CSS-Tricks](https://css-tricks.com/) | 162 | 121 | **120** | 127 | 143 | 143 | 148 | 145 |
|
|
243
303
|
| [ECMAScript](https://tc39.es/ecma262/) | 7250 | **6352** | **6352** | 6573 | 6455 | 6578 | 6626 | n/a |
|
|
244
304
|
| [EDRi](https://edri.org/) | 80 | **59** | 60 | 70 | 70 | 71 | 75 | 73 |
|
|
245
|
-
| [EFF](https://www.eff.org/) | 55 | **
|
|
305
|
+
| [EFF](https://www.eff.org/) | 55 | **45** | 46 | 49 | 48 | 48 | 50 | 50 |
|
|
246
306
|
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
247
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
307
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1603 | 1494 | 1499 | **1437** | 1526 | 1538 | 1549 | n/a |
|
|
248
308
|
| [French Tech](https://lafrenchtech.gouv.fr/) | 152 | **122** | **122** | 126 | 125 | 125 | 132 | 127 |
|
|
249
|
-
| [Frontend Dogma](https://frontenddogma.com/) | 224 | **214** | 216 | 237 | 222 | 224 |
|
|
250
|
-
| [Google](https://www.google.com/) | 18 | **
|
|
251
|
-
| [Ground News](https://ground.news/) |
|
|
309
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 224 | **214** | 216 | 237 | 222 | 224 | 243 | 223 |
|
|
310
|
+
| [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 | 17 | 18 | 18 |
|
|
311
|
+
| [Ground News](https://ground.news/) | 1713 | **1476** | 1477 | 1577 | 1602 | 1607 | 1699 | n/a |
|
|
252
312
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 149 |
|
|
253
313
|
| [Igalia](https://www.igalia.com/) | 50 | **34** | **34** | 36 | 36 | 36 | 37 | 37 |
|
|
254
|
-
| [Leanpub](https://leanpub.com/) | 1551 | **1301** | **1301** | 1308 | 1306 | 1303 | 1545 | n/a |
|
|
255
314
|
| [Mastodon](https://mastodon.social/explore) | 37 | **28** | **28** | 32 | 35 | 35 | 36 | 36 |
|
|
256
315
|
| [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | **62** | 64 | 65 | 65 | 68 | 68 |
|
|
257
316
|
| [Middle East Eye](https://www.middleeasteye.net/) | 222 | **195** | **195** | 202 | 200 | 200 | 202 | 203 |
|
|
258
317
|
| [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 74 | 74 | **55** | 74 | 75 | 77 | 76 |
|
|
259
318
|
| [SitePoint](https://www.sitepoint.com/) | 501 | **370** | **370** | 442 | 475 | 480 | 498 | n/a |
|
|
260
319
|
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 39 | 39 | 39 |
|
|
261
|
-
| [TPGi](https://www.tpgi.com/) | 175 | **
|
|
262
|
-
| [United Nations](https://www.un.org/en/) | 152 | **113** | 114 | 121 | 125 | 125 | 131 | 124 |
|
|
320
|
+
| [TPGi](https://www.tpgi.com/) | 175 | **159** | 161 | 160 | 164 | 166 | 172 | 172 |
|
|
263
321
|
| [W3C](https://www.w3.org/) | 50 | **36** | **36** | 39 | 38 | 38 | 41 | 39 |
|
|
264
|
-
| **Average processing time** | |
|
|
322
|
+
| **Average processing time** | | 256 ms (24/24) | 352 ms (24/24) | 167 ms (24/24) | 54 ms (24/24) | **15 ms (24/24)** | 339 ms (24/24) | 3288 ms (19/24) |
|
|
265
323
|
|
|
266
324
|
(Last updated: Dec 20, 2025)
|
|
267
325
|
<!-- End auto-generated -->
|
package/cli.js
CHANGED
|
@@ -141,7 +141,7 @@ const mainOptions = {
|
|
|
141
141
|
maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseValidInt('maxInputLength')],
|
|
142
142
|
maxLineLength: ['Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points', parseValidInt('maxLineLength')],
|
|
143
143
|
minifyCSS: ['Minify CSS in “style” elements and “style” attributes (uses Lightning CSS)', parseJSON],
|
|
144
|
-
minifyJS: ['Minify JavaScript in “script” elements and event attributes (uses Terser)', parseJSON],
|
|
144
|
+
minifyJS: ['Minify JavaScript in “script” elements and event attributes (uses Terser or SWC; pass “{"engine": "swc"}” for SWC)', parseJSON],
|
|
145
145
|
minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],
|
|
146
146
|
noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
|
|
147
147
|
partialMarkup: 'Treat input as a partial HTML fragment, preserving stray end tags and unclosed tags',
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -1259,11 +1259,12 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1259
1259
|
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
1260
1260
|
* @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
|
|
1261
1261
|
* @param {Function} deps.getTerser - Function to lazily load terser
|
|
1262
|
+
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
1262
1263
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
1263
1264
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
1264
1265
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
1265
1266
|
*/
|
|
1266
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
1267
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
1267
1268
|
const options = {
|
|
1268
1269
|
name: function (name) {
|
|
1269
1270
|
return name.toLowerCase();
|
|
@@ -1383,47 +1384,94 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCac
|
|
|
1383
1384
|
return;
|
|
1384
1385
|
}
|
|
1385
1386
|
|
|
1386
|
-
|
|
1387
|
+
// Parse configuration
|
|
1388
|
+
const config = typeof option === 'object' ? option : {};
|
|
1389
|
+
const engine = (config.engine || 'terser').toLowerCase();
|
|
1387
1390
|
|
|
1391
|
+
// Validate engine
|
|
1392
|
+
const supportedEngines = ['terser', 'swc'];
|
|
1393
|
+
if (!supportedEngines.includes(engine)) {
|
|
1394
|
+
throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Extract engine-specific options (excluding `engine` field itself)
|
|
1398
|
+
const engineOptions = { ...config };
|
|
1399
|
+
delete engineOptions.engine;
|
|
1400
|
+
|
|
1401
|
+
// Terser options (needed for inline JS and when engine is `terser`)
|
|
1402
|
+
const terserOptions = engine === 'terser' ? engineOptions : {};
|
|
1388
1403
|
terserOptions.parse = {
|
|
1389
1404
|
...terserOptions.parse,
|
|
1390
1405
|
bare_returns: false
|
|
1391
1406
|
};
|
|
1392
1407
|
|
|
1408
|
+
// SWC options (when engine is `swc`)
|
|
1409
|
+
const swcOptions = engine === 'swc' ? engineOptions : {};
|
|
1410
|
+
|
|
1411
|
+
// Pre-compute option signatures once for performance (avoid repeated stringification)
|
|
1412
|
+
const terserSig = stableStringify({
|
|
1413
|
+
...terserOptions,
|
|
1414
|
+
cont: !!options.continueOnMinifyError
|
|
1415
|
+
});
|
|
1416
|
+
const swcSig = stableStringify({
|
|
1417
|
+
...swcOptions,
|
|
1418
|
+
cont: !!options.continueOnMinifyError
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1393
1421
|
options.minifyJS = async function (text, inline) {
|
|
1394
1422
|
const start = text.match(/^\s*<!--.*/);
|
|
1395
1423
|
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
1396
1424
|
|
|
1397
|
-
|
|
1425
|
+
// Fast path: Avoid invoking minifier for empty/whitespace-only content
|
|
1426
|
+
if (!code || !code.trim()) {
|
|
1427
|
+
return '';
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Hybrid strategy: Always use Terser for inline JS (needs bare returns support)
|
|
1431
|
+
// Use user’s chosen engine for script blocks
|
|
1432
|
+
const useEngine = inline ? 'terser' : engine;
|
|
1398
1433
|
|
|
1399
1434
|
let jsKey;
|
|
1400
1435
|
try {
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
mangle: terserOptions.mangle,
|
|
1409
|
-
ecma: terserOptions.ecma,
|
|
1410
|
-
toplevel: terserOptions.toplevel,
|
|
1411
|
-
module: terserOptions.module,
|
|
1412
|
-
keep_fnames: terserOptions.keep_fnames,
|
|
1413
|
-
format: terserOptions.format,
|
|
1414
|
-
cont: !!options.continueOnMinifyError,
|
|
1415
|
-
});
|
|
1416
|
-
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
1417
|
-
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
1436
|
+
// Select pre-computed signature based on engine
|
|
1437
|
+
const optsSig = useEngine === 'terser' ? terserSig : swcSig;
|
|
1438
|
+
|
|
1439
|
+
// For large inputs, use length and content fingerprint to prevent collisions
|
|
1440
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|'))
|
|
1441
|
+
+ (inline ? '1' : '0') + '|' + useEngine + '|' + optsSig;
|
|
1442
|
+
|
|
1418
1443
|
const cached = jsMinifyCache.get(jsKey);
|
|
1419
1444
|
if (cached) {
|
|
1420
1445
|
return await cached;
|
|
1421
1446
|
}
|
|
1447
|
+
|
|
1422
1448
|
const inFlight = (async () => {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1449
|
+
// Dispatch to appropriate minifier
|
|
1450
|
+
if (useEngine === 'terser') {
|
|
1451
|
+
// Create a copy to avoid mutating shared `terserOptions` (race condition)
|
|
1452
|
+
const terserCallOptions = {
|
|
1453
|
+
...terserOptions,
|
|
1454
|
+
parse: {
|
|
1455
|
+
...terserOptions.parse,
|
|
1456
|
+
bare_returns: inline
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
const terser = await getTerser();
|
|
1460
|
+
const result = await terser(code, terserCallOptions);
|
|
1461
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
1462
|
+
} else if (useEngine === 'swc') {
|
|
1463
|
+
const swc = await getSwc();
|
|
1464
|
+
// `swc.minify()` takes compress and mangle directly as options
|
|
1465
|
+
const result = await swc.minify(code, {
|
|
1466
|
+
compress: true,
|
|
1467
|
+
mangle: true,
|
|
1468
|
+
...swcOptions, // User options override defaults
|
|
1469
|
+
});
|
|
1470
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
1471
|
+
}
|
|
1472
|
+
throw new Error(`Unknown JS minifier engine: ${useEngine}`);
|
|
1426
1473
|
})();
|
|
1474
|
+
|
|
1427
1475
|
jsMinifyCache.set(jsKey, inFlight);
|
|
1428
1476
|
const resolved = await inFlight;
|
|
1429
1477
|
jsMinifyCache.set(jsKey, resolved);
|
|
@@ -2177,6 +2225,21 @@ async function getTerser() {
|
|
|
2177
2225
|
return terserPromise;
|
|
2178
2226
|
}
|
|
2179
2227
|
|
|
2228
|
+
let swcPromise;
|
|
2229
|
+
async function getSwc() {
|
|
2230
|
+
if (!swcPromise) {
|
|
2231
|
+
swcPromise = import('@swc/core')
|
|
2232
|
+
.then(m => m.default || m)
|
|
2233
|
+
.catch(() => {
|
|
2234
|
+
throw new Error(
|
|
2235
|
+
'The swc minifier requires @swc/core to be installed.\n' +
|
|
2236
|
+
'Install it with: npm install @swc/core'
|
|
2237
|
+
);
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
return swcPromise;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2180
2243
|
// Minification caches
|
|
2181
2244
|
|
|
2182
2245
|
const cssMinifyCache = new LRU(200);
|
|
@@ -2371,10 +2434,14 @@ const jsMinifyCache = new LRU(200);
|
|
|
2371
2434
|
*
|
|
2372
2435
|
* Default: `false`
|
|
2373
2436
|
*
|
|
2374
|
-
* @prop {boolean | import("terser").MinifyOptions | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
|
|
2437
|
+
* @prop {boolean | import("terser").MinifyOptions | {engine?: 'terser' | 'swc', [key: string]: any} | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
|
|
2375
2438
|
* When true, enables JS minification for `<script>` contents and
|
|
2376
|
-
* event handler attributes. If an object is provided, it
|
|
2377
|
-
*
|
|
2439
|
+
* event handler attributes. If an object is provided, it can include:
|
|
2440
|
+
* - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
|
|
2441
|
+
* Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
|
|
2442
|
+
* regardless of engine setting, as swc doesn’t support bare return statements.
|
|
2443
|
+
* - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
|
|
2444
|
+
* SWC options if `engine: 'swc'`).
|
|
2378
2445
|
* If a function is provided, it will be used to perform
|
|
2379
2446
|
* custom JS minification. If disabled, JS is not minified.
|
|
2380
2447
|
*
|
|
@@ -3444,6 +3511,7 @@ const minify = async function (value, options) {
|
|
|
3444
3511
|
options = processOptions(options || {}, {
|
|
3445
3512
|
getLightningCSS,
|
|
3446
3513
|
getTerser,
|
|
3514
|
+
getSwc,
|
|
3447
3515
|
cssMinifyCache,
|
|
3448
3516
|
jsMinifyCache
|
|
3449
3517
|
});
|
|
@@ -6401,11 +6401,12 @@ function shouldMinifyInnerHTML(options) {
|
|
|
6401
6401
|
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
6402
6402
|
* @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
|
|
6403
6403
|
* @param {Function} deps.getTerser - Function to lazily load terser
|
|
6404
|
+
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
6404
6405
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
6405
6406
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
6406
6407
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
6407
6408
|
*/
|
|
6408
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
6409
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
6409
6410
|
const options = {
|
|
6410
6411
|
name: function (name) {
|
|
6411
6412
|
return name.toLowerCase();
|
|
@@ -6525,47 +6526,94 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCac
|
|
|
6525
6526
|
return;
|
|
6526
6527
|
}
|
|
6527
6528
|
|
|
6528
|
-
|
|
6529
|
+
// Parse configuration
|
|
6530
|
+
const config = typeof option === 'object' ? option : {};
|
|
6531
|
+
const engine = (config.engine || 'terser').toLowerCase();
|
|
6529
6532
|
|
|
6533
|
+
// Validate engine
|
|
6534
|
+
const supportedEngines = ['terser', 'swc'];
|
|
6535
|
+
if (!supportedEngines.includes(engine)) {
|
|
6536
|
+
throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
|
|
6537
|
+
}
|
|
6538
|
+
|
|
6539
|
+
// Extract engine-specific options (excluding `engine` field itself)
|
|
6540
|
+
const engineOptions = { ...config };
|
|
6541
|
+
delete engineOptions.engine;
|
|
6542
|
+
|
|
6543
|
+
// Terser options (needed for inline JS and when engine is `terser`)
|
|
6544
|
+
const terserOptions = engine === 'terser' ? engineOptions : {};
|
|
6530
6545
|
terserOptions.parse = {
|
|
6531
6546
|
...terserOptions.parse,
|
|
6532
6547
|
bare_returns: false
|
|
6533
6548
|
};
|
|
6534
6549
|
|
|
6550
|
+
// SWC options (when engine is `swc`)
|
|
6551
|
+
const swcOptions = engine === 'swc' ? engineOptions : {};
|
|
6552
|
+
|
|
6553
|
+
// Pre-compute option signatures once for performance (avoid repeated stringification)
|
|
6554
|
+
const terserSig = stableStringify({
|
|
6555
|
+
...terserOptions,
|
|
6556
|
+
cont: !!options.continueOnMinifyError
|
|
6557
|
+
});
|
|
6558
|
+
const swcSig = stableStringify({
|
|
6559
|
+
...swcOptions,
|
|
6560
|
+
cont: !!options.continueOnMinifyError
|
|
6561
|
+
});
|
|
6562
|
+
|
|
6535
6563
|
options.minifyJS = async function (text, inline) {
|
|
6536
6564
|
const start = text.match(/^\s*<!--.*/);
|
|
6537
6565
|
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
6538
6566
|
|
|
6539
|
-
|
|
6567
|
+
// Fast path: Avoid invoking minifier for empty/whitespace-only content
|
|
6568
|
+
if (!code || !code.trim()) {
|
|
6569
|
+
return '';
|
|
6570
|
+
}
|
|
6571
|
+
|
|
6572
|
+
// Hybrid strategy: Always use Terser for inline JS (needs bare returns support)
|
|
6573
|
+
// Use user’s chosen engine for script blocks
|
|
6574
|
+
const useEngine = inline ? 'terser' : engine;
|
|
6540
6575
|
|
|
6541
6576
|
let jsKey;
|
|
6542
6577
|
try {
|
|
6543
|
-
//
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
mangle: terserOptions.mangle,
|
|
6551
|
-
ecma: terserOptions.ecma,
|
|
6552
|
-
toplevel: terserOptions.toplevel,
|
|
6553
|
-
module: terserOptions.module,
|
|
6554
|
-
keep_fnames: terserOptions.keep_fnames,
|
|
6555
|
-
format: terserOptions.format,
|
|
6556
|
-
cont: !!options.continueOnMinifyError,
|
|
6557
|
-
});
|
|
6558
|
-
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
6559
|
-
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
6578
|
+
// Select pre-computed signature based on engine
|
|
6579
|
+
const optsSig = useEngine === 'terser' ? terserSig : swcSig;
|
|
6580
|
+
|
|
6581
|
+
// For large inputs, use length and content fingerprint to prevent collisions
|
|
6582
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|'))
|
|
6583
|
+
+ (inline ? '1' : '0') + '|' + useEngine + '|' + optsSig;
|
|
6584
|
+
|
|
6560
6585
|
const cached = jsMinifyCache.get(jsKey);
|
|
6561
6586
|
if (cached) {
|
|
6562
6587
|
return await cached;
|
|
6563
6588
|
}
|
|
6589
|
+
|
|
6564
6590
|
const inFlight = (async () => {
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6591
|
+
// Dispatch to appropriate minifier
|
|
6592
|
+
if (useEngine === 'terser') {
|
|
6593
|
+
// Create a copy to avoid mutating shared `terserOptions` (race condition)
|
|
6594
|
+
const terserCallOptions = {
|
|
6595
|
+
...terserOptions,
|
|
6596
|
+
parse: {
|
|
6597
|
+
...terserOptions.parse,
|
|
6598
|
+
bare_returns: inline
|
|
6599
|
+
}
|
|
6600
|
+
};
|
|
6601
|
+
const terser = await getTerser();
|
|
6602
|
+
const result = await terser(code, terserCallOptions);
|
|
6603
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
6604
|
+
} else if (useEngine === 'swc') {
|
|
6605
|
+
const swc = await getSwc();
|
|
6606
|
+
// `swc.minify()` takes compress and mangle directly as options
|
|
6607
|
+
const result = await swc.minify(code, {
|
|
6608
|
+
compress: true,
|
|
6609
|
+
mangle: true,
|
|
6610
|
+
...swcOptions, // User options override defaults
|
|
6611
|
+
});
|
|
6612
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
6613
|
+
}
|
|
6614
|
+
throw new Error(`Unknown JS minifier engine: ${useEngine}`);
|
|
6568
6615
|
})();
|
|
6616
|
+
|
|
6569
6617
|
jsMinifyCache.set(jsKey, inFlight);
|
|
6570
6618
|
const resolved = await inFlight;
|
|
6571
6619
|
jsMinifyCache.set(jsKey, resolved);
|
|
@@ -7319,6 +7367,21 @@ async function getTerser() {
|
|
|
7319
7367
|
return terserPromise;
|
|
7320
7368
|
}
|
|
7321
7369
|
|
|
7370
|
+
let swcPromise;
|
|
7371
|
+
async function getSwc() {
|
|
7372
|
+
if (!swcPromise) {
|
|
7373
|
+
swcPromise = import('@swc/core')
|
|
7374
|
+
.then(m => m.default || m)
|
|
7375
|
+
.catch(() => {
|
|
7376
|
+
throw new Error(
|
|
7377
|
+
'The swc minifier requires @swc/core to be installed.\n' +
|
|
7378
|
+
'Install it with: npm install @swc/core'
|
|
7379
|
+
);
|
|
7380
|
+
});
|
|
7381
|
+
}
|
|
7382
|
+
return swcPromise;
|
|
7383
|
+
}
|
|
7384
|
+
|
|
7322
7385
|
// Minification caches
|
|
7323
7386
|
|
|
7324
7387
|
const cssMinifyCache = new LRU(200);
|
|
@@ -7513,10 +7576,14 @@ const jsMinifyCache = new LRU(200);
|
|
|
7513
7576
|
*
|
|
7514
7577
|
* Default: `false`
|
|
7515
7578
|
*
|
|
7516
|
-
* @prop {boolean | import("terser").MinifyOptions | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
|
|
7579
|
+
* @prop {boolean | import("terser").MinifyOptions | {engine?: 'terser' | 'swc', [key: string]: any} | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
|
|
7517
7580
|
* When true, enables JS minification for `<script>` contents and
|
|
7518
|
-
* event handler attributes. If an object is provided, it
|
|
7519
|
-
*
|
|
7581
|
+
* event handler attributes. If an object is provided, it can include:
|
|
7582
|
+
* - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
|
|
7583
|
+
* Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
|
|
7584
|
+
* regardless of engine setting, as swc doesn’t support bare return statements.
|
|
7585
|
+
* - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
|
|
7586
|
+
* SWC options if `engine: 'swc'`).
|
|
7520
7587
|
* If a function is provided, it will be used to perform
|
|
7521
7588
|
* custom JS minification. If disabled, JS is not minified.
|
|
7522
7589
|
*
|
|
@@ -8586,6 +8653,7 @@ const minify$1 = async function (value, options) {
|
|
|
8586
8653
|
options = processOptions(options || {}, {
|
|
8587
8654
|
getLightningCSS,
|
|
8588
8655
|
getTerser,
|
|
8656
|
+
getSwc,
|
|
8589
8657
|
cssMinifyCache,
|
|
8590
8658
|
jsMinifyCache
|
|
8591
8659
|
});
|
|
@@ -220,14 +220,21 @@ export type MinifierOptions = {
|
|
|
220
220
|
minifyCSS?: boolean | Partial<import("lightningcss").TransformOptions<import("lightningcss").CustomAtRules>> | ((text: string, type?: string) => Promise<string> | string);
|
|
221
221
|
/**
|
|
222
222
|
* When true, enables JS minification for `<script>` contents and
|
|
223
|
-
* event handler attributes. If an object is provided, it
|
|
224
|
-
*
|
|
223
|
+
* event handler attributes. If an object is provided, it can include:
|
|
224
|
+
* - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
|
|
225
|
+
* Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
|
|
226
|
+
* regardless of engine setting, as swc doesn’t support bare return statements.
|
|
227
|
+
* - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
|
|
228
|
+
* SWC options if `engine: 'swc'`).
|
|
225
229
|
* If a function is provided, it will be used to perform
|
|
226
230
|
* custom JS minification. If disabled, JS is not minified.
|
|
227
231
|
*
|
|
228
232
|
* Default: `false`
|
|
229
233
|
*/
|
|
230
|
-
minifyJS?: boolean | import("terser").MinifyOptions |
|
|
234
|
+
minifyJS?: boolean | import("terser").MinifyOptions | {
|
|
235
|
+
engine?: "terser" | "swc";
|
|
236
|
+
[key: string]: any;
|
|
237
|
+
} | ((text: string, inline?: boolean) => Promise<string> | string);
|
|
231
238
|
/**
|
|
232
239
|
* When true, enables URL rewriting/minification. If an object is provided,
|
|
233
240
|
* it is passed to [relateurl](https://www.npmjs.com/package/relateurl)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
|
|
@@ -4,14 +4,14 @@ export function shouldMinifyInnerHTML(options: any): boolean;
|
|
|
4
4
|
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
5
5
|
* @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
|
|
6
6
|
* @param {Function} deps.getTerser - Function to lazily load terser
|
|
7
|
+
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
7
8
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
8
9
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
9
10
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
10
11
|
*/
|
|
11
|
-
export function processOptions(inputOptions: Partial<MinifierOptions>, { getLightningCSS, getTerser, cssMinifyCache, jsMinifyCache }?: {
|
|
12
|
+
export function processOptions(inputOptions: Partial<MinifierOptions>, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache }?: {
|
|
12
13
|
getLightningCSS: Function;
|
|
13
14
|
getTerser: Function;
|
|
14
|
-
|
|
15
|
-
jsMinifyCache: LRU;
|
|
15
|
+
getSwc: Function;
|
|
16
16
|
}): MinifierOptions;
|
|
17
17
|
//# sourceMappingURL=options.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAUA,6DASC;AAID
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAUA,6DASC;AAID;;;;;;;;;GASG;AACH,6CATW,OAAO,CAAC,eAAe,CAAC,0EAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAG9C,eAAe,CAqQ3B"}
|
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@rollup/plugin-json": "^6.1.0",
|
|
22
22
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
23
23
|
"@rollup/plugin-terser": "^0.4.4",
|
|
24
|
+
"@swc/core": "^1.15.7",
|
|
24
25
|
"eslint": "^9.39.1",
|
|
25
26
|
"rollup": "^4.53.3",
|
|
26
27
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
@@ -69,6 +70,14 @@
|
|
|
69
70
|
"module": "./src/htmlminifier.js",
|
|
70
71
|
"types": "./dist/types/htmlminifier.d.ts",
|
|
71
72
|
"name": "html-minifier-next",
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@swc/core": "^1.15.7"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@swc/core": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
72
81
|
"repository": "https://github.com/j9t/html-minifier-next.git",
|
|
73
82
|
"scripts": {
|
|
74
83
|
"prebuild": "node --eval='require(`fs`).rmSync(`dist`,{recursive:true,force:true})'",
|
|
@@ -84,5 +93,5 @@
|
|
|
84
93
|
"test:watch": "node --test --watch tests/*.spec.js"
|
|
85
94
|
},
|
|
86
95
|
"type": "module",
|
|
87
|
-
"version": "4.
|
|
96
|
+
"version": "4.14.0"
|
|
88
97
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -74,6 +74,21 @@ async function getTerser() {
|
|
|
74
74
|
return terserPromise;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
let swcPromise;
|
|
78
|
+
async function getSwc() {
|
|
79
|
+
if (!swcPromise) {
|
|
80
|
+
swcPromise = import('@swc/core')
|
|
81
|
+
.then(m => m.default || m)
|
|
82
|
+
.catch(() => {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'The swc minifier requires @swc/core to be installed.\n' +
|
|
85
|
+
'Install it with: npm install @swc/core'
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return swcPromise;
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
// Minification caches
|
|
78
93
|
|
|
79
94
|
const cssMinifyCache = new LRU(200);
|
|
@@ -268,10 +283,14 @@ const jsMinifyCache = new LRU(200);
|
|
|
268
283
|
*
|
|
269
284
|
* Default: `false`
|
|
270
285
|
*
|
|
271
|
-
* @prop {boolean | import("terser").MinifyOptions | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
|
|
286
|
+
* @prop {boolean | import("terser").MinifyOptions | {engine?: 'terser' | 'swc', [key: string]: any} | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
|
|
272
287
|
* When true, enables JS minification for `<script>` contents and
|
|
273
|
-
* event handler attributes. If an object is provided, it
|
|
274
|
-
*
|
|
288
|
+
* event handler attributes. If an object is provided, it can include:
|
|
289
|
+
* - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
|
|
290
|
+
* Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
|
|
291
|
+
* regardless of engine setting, as swc doesn’t support bare return statements.
|
|
292
|
+
* - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
|
|
293
|
+
* SWC options if `engine: 'swc'`).
|
|
275
294
|
* If a function is provided, it will be used to perform
|
|
276
295
|
* custom JS minification. If disabled, JS is not minified.
|
|
277
296
|
*
|
|
@@ -1341,6 +1360,7 @@ export const minify = async function (value, options) {
|
|
|
1341
1360
|
options = processOptions(options || {}, {
|
|
1342
1361
|
getLightningCSS,
|
|
1343
1362
|
getTerser,
|
|
1363
|
+
getSwc,
|
|
1344
1364
|
cssMinifyCache,
|
|
1345
1365
|
jsMinifyCache
|
|
1346
1366
|
});
|
package/src/lib/options.js
CHANGED
|
@@ -26,11 +26,12 @@ function shouldMinifyInnerHTML(options) {
|
|
|
26
26
|
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
27
27
|
* @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
|
|
28
28
|
* @param {Function} deps.getTerser - Function to lazily load terser
|
|
29
|
+
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
29
30
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
30
31
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
31
32
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
32
33
|
*/
|
|
33
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
34
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
34
35
|
const options = {
|
|
35
36
|
name: function (name) {
|
|
36
37
|
return name.toLowerCase();
|
|
@@ -150,47 +151,94 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCac
|
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
// Parse configuration
|
|
155
|
+
const config = typeof option === 'object' ? option : {};
|
|
156
|
+
const engine = (config.engine || 'terser').toLowerCase();
|
|
154
157
|
|
|
158
|
+
// Validate engine
|
|
159
|
+
const supportedEngines = ['terser', 'swc'];
|
|
160
|
+
if (!supportedEngines.includes(engine)) {
|
|
161
|
+
throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Extract engine-specific options (excluding `engine` field itself)
|
|
165
|
+
const engineOptions = { ...config };
|
|
166
|
+
delete engineOptions.engine;
|
|
167
|
+
|
|
168
|
+
// Terser options (needed for inline JS and when engine is `terser`)
|
|
169
|
+
const terserOptions = engine === 'terser' ? engineOptions : {};
|
|
155
170
|
terserOptions.parse = {
|
|
156
171
|
...terserOptions.parse,
|
|
157
172
|
bare_returns: false
|
|
158
173
|
};
|
|
159
174
|
|
|
175
|
+
// SWC options (when engine is `swc`)
|
|
176
|
+
const swcOptions = engine === 'swc' ? engineOptions : {};
|
|
177
|
+
|
|
178
|
+
// Pre-compute option signatures once for performance (avoid repeated stringification)
|
|
179
|
+
const terserSig = stableStringify({
|
|
180
|
+
...terserOptions,
|
|
181
|
+
cont: !!options.continueOnMinifyError
|
|
182
|
+
});
|
|
183
|
+
const swcSig = stableStringify({
|
|
184
|
+
...swcOptions,
|
|
185
|
+
cont: !!options.continueOnMinifyError
|
|
186
|
+
});
|
|
187
|
+
|
|
160
188
|
options.minifyJS = async function (text, inline) {
|
|
161
189
|
const start = text.match(/^\s*<!--.*/);
|
|
162
190
|
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
163
191
|
|
|
164
|
-
|
|
192
|
+
// Fast path: Avoid invoking minifier for empty/whitespace-only content
|
|
193
|
+
if (!code || !code.trim()) {
|
|
194
|
+
return '';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Hybrid strategy: Always use Terser for inline JS (needs bare returns support)
|
|
198
|
+
// Use user’s chosen engine for script blocks
|
|
199
|
+
const useEngine = inline ? 'terser' : engine;
|
|
165
200
|
|
|
166
201
|
let jsKey;
|
|
167
202
|
try {
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
mangle: terserOptions.mangle,
|
|
176
|
-
ecma: terserOptions.ecma,
|
|
177
|
-
toplevel: terserOptions.toplevel,
|
|
178
|
-
module: terserOptions.module,
|
|
179
|
-
keep_fnames: terserOptions.keep_fnames,
|
|
180
|
-
format: terserOptions.format,
|
|
181
|
-
cont: !!options.continueOnMinifyError,
|
|
182
|
-
});
|
|
183
|
-
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
184
|
-
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
203
|
+
// Select pre-computed signature based on engine
|
|
204
|
+
const optsSig = useEngine === 'terser' ? terserSig : swcSig;
|
|
205
|
+
|
|
206
|
+
// For large inputs, use length and content fingerprint to prevent collisions
|
|
207
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|'))
|
|
208
|
+
+ (inline ? '1' : '0') + '|' + useEngine + '|' + optsSig;
|
|
209
|
+
|
|
185
210
|
const cached = jsMinifyCache.get(jsKey);
|
|
186
211
|
if (cached) {
|
|
187
212
|
return await cached;
|
|
188
213
|
}
|
|
214
|
+
|
|
189
215
|
const inFlight = (async () => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
// Dispatch to appropriate minifier
|
|
217
|
+
if (useEngine === 'terser') {
|
|
218
|
+
// Create a copy to avoid mutating shared `terserOptions` (race condition)
|
|
219
|
+
const terserCallOptions = {
|
|
220
|
+
...terserOptions,
|
|
221
|
+
parse: {
|
|
222
|
+
...terserOptions.parse,
|
|
223
|
+
bare_returns: inline
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const terser = await getTerser();
|
|
227
|
+
const result = await terser(code, terserCallOptions);
|
|
228
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
229
|
+
} else if (useEngine === 'swc') {
|
|
230
|
+
const swc = await getSwc();
|
|
231
|
+
// `swc.minify()` takes compress and mangle directly as options
|
|
232
|
+
const result = await swc.minify(code, {
|
|
233
|
+
compress: true,
|
|
234
|
+
mangle: true,
|
|
235
|
+
...swcOptions, // User options override defaults
|
|
236
|
+
});
|
|
237
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`Unknown JS minifier engine: ${useEngine}`);
|
|
193
240
|
})();
|
|
241
|
+
|
|
194
242
|
jsMinifyCache.set(jsKey, inFlight);
|
|
195
243
|
const resolved = await inFlight;
|
|
196
244
|
jsMinifyCache.set(jsKey, resolved);
|