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 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 with Lightning CSS
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>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-terser)](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[![npm last update](https://img.shields.io/npm/last-update/htmlnano)](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[![npm last update](https://img.shields.io/npm/last-update/@swc/html)](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[![npm last update](https://img.shields.io/npm/last-update/@minify-html/node)](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[![npm last update](https://img.shields.io/npm/last-update/minimize)](https://socket.dev/npm/package/minimize) | [html­com­pressor.­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>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-terser)](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[![npm last update](https://img.shields.io/npm/last-update/htmlnano)](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[![npm last update](https://img.shields.io/npm/last-update/@swc/html)](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[![npm last update](https://img.shields.io/npm/last-update/@minify-html/node)](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[![npm last update](https://img.shields.io/npm/last-update/minimize)](https://socket.dev/npm/package/minimize) | [html­com­pressor.­com](https://htmlcompressor.com/) |
237
297
  | --- | --- | --- | --- | --- | --- | --- | --- | --- |
238
- | [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
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/) | 740 | **672** | 682 | 694 | 695 | 696 | 733 | n/a |
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 | **47** | **47** | 49 | 48 | 49 | 50 | 50 |
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/) | 1595 | 1486 | 1491 | **1429** | 1518 | 1530 | 1541 | n/a |
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 | 242 | 223 |
250
- | [Google](https://www.google.com/) | 18 | **17** | **17** | **17** | **17** | **17** | 18 | 18 |
251
- | [Ground News](https://ground.news/) | 1682 | **1444** | 1447 | 1548 | 1573 | 1578 | 1669 | n/a |
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 | **160** | 161 | **160** | 164 | 166 | 172 | 172 |
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** | | 305 ms (26/26) | 361 ms (26/26) | 175 ms (26/26) | 57 ms (26/26) | **17 ms (26/26)** | 313 ms (26/26) | 1442 ms (20/26) |
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',
@@ -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
- const terserOptions = typeof option === 'object' ? option : {};
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
- terserOptions.parse.bare_returns = inline;
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
- // Fast path: Avoid invoking Terser for empty/whitespace-only content
1402
- if (!code || !code.trim()) {
1403
- return '';
1404
- }
1405
- // Cache key: content, inline, options signature (subset)
1406
- const terserSig = stableStringify({
1407
- compress: terserOptions.compress,
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
- const terser = await getTerser();
1424
- const result = await terser(code, terserOptions);
1425
- return result.code.replace(RE_TRAILING_SEMICOLON, '');
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 is passed to
2377
- * [terser](https://www.npmjs.com/package/terser) as minify options.
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
- const terserOptions = typeof option === 'object' ? option : {};
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
- terserOptions.parse.bare_returns = inline;
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
- // Fast path: Avoid invoking Terser for empty/whitespace-only content
6544
- if (!code || !code.trim()) {
6545
- return '';
6546
- }
6547
- // Cache key: content, inline, options signature (subset)
6548
- const terserSig = stableStringify({
6549
- compress: terserOptions.compress,
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
- const terser = await getTerser();
6566
- const result = await terser(code, terserOptions);
6567
- return result.code.replace(RE_TRAILING_SEMICOLON, '');
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 is passed to
7519
- * [terser](https://www.npmjs.com/package/terser) as minify options.
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 is passed to
224
- * [terser](https://www.npmjs.com/package/terser) as minify options.
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 | ((text: string, inline?: boolean) => Promise<string> | string);
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":"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"}
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
- cssMinifyCache: LRU;
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;;;;;;;;GAQG;AACH,6CARW,OAAO,CAAC,eAAe,CAAC,kEAEhC;IAAuB,eAAe;IACf,SAAS;IACd,cAAc,EAAxB,GAAG;IACO,aAAa,EAAvB,GAAG;CACX,GAAU,eAAe,CAsN3B"}
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.13.0"
96
+ "version": "4.14.0"
88
97
  }
@@ -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 is passed to
274
- * [terser](https://www.npmjs.com/package/terser) as minify options.
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
  });
@@ -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
- const terserOptions = typeof option === 'object' ? option : {};
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
- terserOptions.parse.bare_returns = inline;
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
- // Fast path: Avoid invoking Terser for empty/whitespace-only content
169
- if (!code || !code.trim()) {
170
- return '';
171
- }
172
- // Cache key: content, inline, options signature (subset)
173
- const terserSig = stableStringify({
174
- compress: terserOptions.compress,
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
- const terser = await getTerser();
191
- const result = await terser(code, terserOptions);
192
- return result.code.replace(RE_TRAILING_SEMICOLON, '');
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);