html-minifier-next 4.13.0 → 4.14.1

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,42 +228,102 @@ 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
298
  | [A List Apart](https://alistapart.com/) | 59 | **49** | 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 |
241
- | [CERN](https://home.cern/) | 152 | 85 | **84** | 91 | 91 | 91 | 93 | 96 |
300
+ | [BBC](https://www.bbc.co.uk/) | 730 | **663** | 673 | 685 | 686 | 687 | 724 | n/a |
301
+ | [CERN](https://home.cern/) | 152 | **83** | 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/) | 1562 | 1455 | 1460 | **1401** | 1487 | 1498 | 1509 | 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 | 224 |
310
+ | [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 | 17 | 18 | 18 |
311
+ | [Ground News](https://ground.news/) | 2344 | **2062** | 2064 | 2159 | 2184 | 2188 | 2331 | 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 |
314
+ | [Leanpub](https://leanpub.com/) | 222 | 192 | **190** | 207 | 207 | 207 | 217 | 219 |
255
315
  | [Mastodon](https://mastodon.social/explore) | 37 | **28** | **28** | 32 | 35 | 35 | 36 | 36 |
256
316
  | [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | **62** | 64 | 65 | 65 | 68 | 68 |
257
- | [Middle East Eye](https://www.middleeasteye.net/) | 222 | **195** | **195** | 202 | 200 | 200 | 202 | 203 |
317
+ | [Middle East Eye](https://www.middleeasteye.net/) | 222 | **195** | **195** | 202 | 200 | 200 | 202 | 202 |
258
318
  | [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 74 | 74 | **55** | 74 | 75 | 77 | 76 |
259
319
  | [SitePoint](https://www.sitepoint.com/) | 501 | **370** | **370** | 442 | 475 | 480 | 498 | n/a |
260
320
  | [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 |
321
+ | [TPGi](https://www.tpgi.com/) | 175 | **159** | 161 | 160 | 164 | 166 | 172 | 172 |
262
322
  | [United Nations](https://www.un.org/en/) | 152 | **113** | 114 | 121 | 125 | 125 | 131 | 124 |
263
323
  | [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) |
324
+ | **Average processing time** | | 264 ms (26/26) | 365 ms (26/26) | 162 ms (26/26) | 56 ms (26/26) | **16 ms (26/26)** | 323 ms (26/26) | 1373 ms (21/26) |
265
325
 
266
- (Last updated: Dec 20, 2025)
326
+ (Last updated: Dec 21, 2025)
267
327
  <!-- End auto-generated -->
268
328
 
269
329
  ## Examples
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',
@@ -928,10 +928,13 @@ const tagDefaults = {
928
928
  colorspace: 'limited-srgb',
929
929
  type: 'text'
930
930
  },
931
+ link: { media: 'all' },
931
932
  marquee: {
932
933
  behavior: 'scroll',
933
934
  direction: 'left'
934
935
  },
936
+ meta: { media: 'all' },
937
+ source: { media: 'all' },
935
938
  style: { media: 'all' },
936
939
  textarea: { wrap: 'soft' },
937
940
  track: { kind: 'subtitles' }
@@ -1259,11 +1262,12 @@ function shouldMinifyInnerHTML(options) {
1259
1262
  * @param {Object} deps - Dependencies from htmlminifier.js
1260
1263
  * @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
1261
1264
  * @param {Function} deps.getTerser - Function to lazily load terser
1265
+ * @param {Function} deps.getSwc - Function to lazily load @swc/core
1262
1266
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
1263
1267
  * @param {LRU} deps.jsMinifyCache - JS minification cache
1264
1268
  * @returns {MinifierOptions} Normalized options with defaults applied
1265
1269
  */
1266
- const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCache, jsMinifyCache } = {}) => {
1270
+ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
1267
1271
  const options = {
1268
1272
  name: function (name) {
1269
1273
  return name.toLowerCase();
@@ -1383,47 +1387,94 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCac
1383
1387
  return;
1384
1388
  }
1385
1389
 
1386
- const terserOptions = typeof option === 'object' ? option : {};
1390
+ // Parse configuration
1391
+ const config = typeof option === 'object' ? option : {};
1392
+ const engine = (config.engine || 'terser').toLowerCase();
1387
1393
 
1394
+ // Validate engine
1395
+ const supportedEngines = ['terser', 'swc'];
1396
+ if (!supportedEngines.includes(engine)) {
1397
+ throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
1398
+ }
1399
+
1400
+ // Extract engine-specific options (excluding `engine` field itself)
1401
+ const engineOptions = { ...config };
1402
+ delete engineOptions.engine;
1403
+
1404
+ // Terser options (needed for inline JS and when engine is `terser`)
1405
+ const terserOptions = engine === 'terser' ? engineOptions : {};
1388
1406
  terserOptions.parse = {
1389
1407
  ...terserOptions.parse,
1390
1408
  bare_returns: false
1391
1409
  };
1392
1410
 
1411
+ // SWC options (when engine is `swc`)
1412
+ const swcOptions = engine === 'swc' ? engineOptions : {};
1413
+
1414
+ // Pre-compute option signatures once for performance (avoid repeated stringification)
1415
+ const terserSig = stableStringify({
1416
+ ...terserOptions,
1417
+ cont: !!options.continueOnMinifyError
1418
+ });
1419
+ const swcSig = stableStringify({
1420
+ ...swcOptions,
1421
+ cont: !!options.continueOnMinifyError
1422
+ });
1423
+
1393
1424
  options.minifyJS = async function (text, inline) {
1394
1425
  const start = text.match(/^\s*<!--.*/);
1395
1426
  const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
1396
1427
 
1397
- terserOptions.parse.bare_returns = inline;
1428
+ // Fast path: Avoid invoking minifier for empty/whitespace-only content
1429
+ if (!code || !code.trim()) {
1430
+ return '';
1431
+ }
1432
+
1433
+ // Hybrid strategy: Always use Terser for inline JS (needs bare returns support)
1434
+ // Use user’s chosen engine for script blocks
1435
+ const useEngine = inline ? 'terser' : engine;
1398
1436
 
1399
1437
  let jsKey;
1400
1438
  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;
1439
+ // Select pre-computed signature based on engine
1440
+ const optsSig = useEngine === 'terser' ? terserSig : swcSig;
1441
+
1442
+ // For large inputs, use length and content fingerprint to prevent collisions
1443
+ jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|'))
1444
+ + (inline ? '1' : '0') + '|' + useEngine + '|' + optsSig;
1445
+
1418
1446
  const cached = jsMinifyCache.get(jsKey);
1419
1447
  if (cached) {
1420
1448
  return await cached;
1421
1449
  }
1450
+
1422
1451
  const inFlight = (async () => {
1423
- const terser = await getTerser();
1424
- const result = await terser(code, terserOptions);
1425
- return result.code.replace(RE_TRAILING_SEMICOLON, '');
1452
+ // Dispatch to appropriate minifier
1453
+ if (useEngine === 'terser') {
1454
+ // Create a copy to avoid mutating shared `terserOptions` (race condition)
1455
+ const terserCallOptions = {
1456
+ ...terserOptions,
1457
+ parse: {
1458
+ ...terserOptions.parse,
1459
+ bare_returns: inline
1460
+ }
1461
+ };
1462
+ const terser = await getTerser();
1463
+ const result = await terser(code, terserCallOptions);
1464
+ return result.code.replace(RE_TRAILING_SEMICOLON, '');
1465
+ } else if (useEngine === 'swc') {
1466
+ const swc = await getSwc();
1467
+ // `swc.minify()` takes compress and mangle directly as options
1468
+ const result = await swc.minify(code, {
1469
+ compress: true,
1470
+ mangle: true,
1471
+ ...swcOptions, // User options override defaults
1472
+ });
1473
+ return result.code.replace(RE_TRAILING_SEMICOLON, '');
1474
+ }
1475
+ throw new Error(`Unknown JS minifier engine: ${useEngine}`);
1426
1476
  })();
1477
+
1427
1478
  jsMinifyCache.set(jsKey, inFlight);
1428
1479
  const resolved = await inFlight;
1429
1480
  jsMinifyCache.set(jsKey, resolved);
@@ -1734,6 +1785,11 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
1734
1785
  }
1735
1786
  try {
1736
1787
  attrValue = await options.minifyCSS(attrValue, 'inline');
1788
+ // After minification, check if CSS consists entirely of invalid properties (no values)
1789
+ // E.g., `color:` or `margin:;padding:` should be treated as empty
1790
+ if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
1791
+ attrValue = '';
1792
+ }
1737
1793
  } catch (err) {
1738
1794
  if (!options.continueOnMinifyError) {
1739
1795
  throw err;
@@ -1782,6 +1838,11 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
1782
1838
  attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
1783
1839
  } else if (isMediaQuery(tag, attrs, attrName)) {
1784
1840
  attrValue = trimWhitespace(attrValue);
1841
+ // Only minify actual media queries (those with features in parentheses)
1842
+ // Skip simple media types like `all`, `screen`, `print` which are already minimal
1843
+ if (!/[()]/.test(attrValue)) {
1844
+ return attrValue;
1845
+ }
1785
1846
  try {
1786
1847
  return await options.minifyCSS(attrValue, 'media');
1787
1848
  } catch (err) {
@@ -2177,6 +2238,21 @@ async function getTerser() {
2177
2238
  return terserPromise;
2178
2239
  }
2179
2240
 
2241
+ let swcPromise;
2242
+ async function getSwc() {
2243
+ if (!swcPromise) {
2244
+ swcPromise = import('@swc/core')
2245
+ .then(m => m.default || m)
2246
+ .catch(() => {
2247
+ throw new Error(
2248
+ 'The swc minifier requires @swc/core to be installed.\n' +
2249
+ 'Install it with: npm install @swc/core'
2250
+ );
2251
+ });
2252
+ }
2253
+ return swcPromise;
2254
+ }
2255
+
2180
2256
  // Minification caches
2181
2257
 
2182
2258
  const cssMinifyCache = new LRU(200);
@@ -2371,10 +2447,14 @@ const jsMinifyCache = new LRU(200);
2371
2447
  *
2372
2448
  * Default: `false`
2373
2449
  *
2374
- * @prop {boolean | import("terser").MinifyOptions | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
2450
+ * @prop {boolean | import("terser").MinifyOptions | {engine?: 'terser' | 'swc', [key: string]: any} | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
2375
2451
  * 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.
2452
+ * event handler attributes. If an object is provided, it can include:
2453
+ * - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
2454
+ * Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
2455
+ * regardless of engine setting, as swc doesn’t support bare return statements.
2456
+ * - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
2457
+ * SWC options if `engine: 'swc'`).
2378
2458
  * If a function is provided, it will be used to perform
2379
2459
  * custom JS minification. If disabled, JS is not minified.
2380
2460
  *
@@ -3444,6 +3524,7 @@ const minify = async function (value, options) {
3444
3524
  options = processOptions(options || {}, {
3445
3525
  getLightningCSS,
3446
3526
  getTerser,
3527
+ getSwc,
3447
3528
  cssMinifyCache,
3448
3529
  jsMinifyCache
3449
3530
  });
@@ -3540,10 +3540,13 @@ const tagDefaults = {
3540
3540
  colorspace: 'limited-srgb',
3541
3541
  type: 'text'
3542
3542
  },
3543
+ link: { media: 'all' },
3543
3544
  marquee: {
3544
3545
  behavior: 'scroll',
3545
3546
  direction: 'left'
3546
3547
  },
3548
+ meta: { media: 'all' },
3549
+ source: { media: 'all' },
3547
3550
  style: { media: 'all' },
3548
3551
  textarea: { wrap: 'soft' },
3549
3552
  track: { kind: 'subtitles' }
@@ -6401,11 +6404,12 @@ function shouldMinifyInnerHTML(options) {
6401
6404
  * @param {Object} deps - Dependencies from htmlminifier.js
6402
6405
  * @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
6403
6406
  * @param {Function} deps.getTerser - Function to lazily load terser
6407
+ * @param {Function} deps.getSwc - Function to lazily load @swc/core
6404
6408
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
6405
6409
  * @param {LRU} deps.jsMinifyCache - JS minification cache
6406
6410
  * @returns {MinifierOptions} Normalized options with defaults applied
6407
6411
  */
6408
- const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCache, jsMinifyCache } = {}) => {
6412
+ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
6409
6413
  const options = {
6410
6414
  name: function (name) {
6411
6415
  return name.toLowerCase();
@@ -6525,47 +6529,94 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, cssMinifyCac
6525
6529
  return;
6526
6530
  }
6527
6531
 
6528
- const terserOptions = typeof option === 'object' ? option : {};
6532
+ // Parse configuration
6533
+ const config = typeof option === 'object' ? option : {};
6534
+ const engine = (config.engine || 'terser').toLowerCase();
6529
6535
 
6536
+ // Validate engine
6537
+ const supportedEngines = ['terser', 'swc'];
6538
+ if (!supportedEngines.includes(engine)) {
6539
+ throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
6540
+ }
6541
+
6542
+ // Extract engine-specific options (excluding `engine` field itself)
6543
+ const engineOptions = { ...config };
6544
+ delete engineOptions.engine;
6545
+
6546
+ // Terser options (needed for inline JS and when engine is `terser`)
6547
+ const terserOptions = engine === 'terser' ? engineOptions : {};
6530
6548
  terserOptions.parse = {
6531
6549
  ...terserOptions.parse,
6532
6550
  bare_returns: false
6533
6551
  };
6534
6552
 
6553
+ // SWC options (when engine is `swc`)
6554
+ const swcOptions = engine === 'swc' ? engineOptions : {};
6555
+
6556
+ // Pre-compute option signatures once for performance (avoid repeated stringification)
6557
+ const terserSig = stableStringify({
6558
+ ...terserOptions,
6559
+ cont: !!options.continueOnMinifyError
6560
+ });
6561
+ const swcSig = stableStringify({
6562
+ ...swcOptions,
6563
+ cont: !!options.continueOnMinifyError
6564
+ });
6565
+
6535
6566
  options.minifyJS = async function (text, inline) {
6536
6567
  const start = text.match(/^\s*<!--.*/);
6537
6568
  const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
6538
6569
 
6539
- terserOptions.parse.bare_returns = inline;
6570
+ // Fast path: Avoid invoking minifier for empty/whitespace-only content
6571
+ if (!code || !code.trim()) {
6572
+ return '';
6573
+ }
6574
+
6575
+ // Hybrid strategy: Always use Terser for inline JS (needs bare returns support)
6576
+ // Use user’s chosen engine for script blocks
6577
+ const useEngine = inline ? 'terser' : engine;
6540
6578
 
6541
6579
  let jsKey;
6542
6580
  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;
6581
+ // Select pre-computed signature based on engine
6582
+ const optsSig = useEngine === 'terser' ? terserSig : swcSig;
6583
+
6584
+ // For large inputs, use length and content fingerprint to prevent collisions
6585
+ jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|'))
6586
+ + (inline ? '1' : '0') + '|' + useEngine + '|' + optsSig;
6587
+
6560
6588
  const cached = jsMinifyCache.get(jsKey);
6561
6589
  if (cached) {
6562
6590
  return await cached;
6563
6591
  }
6592
+
6564
6593
  const inFlight = (async () => {
6565
- const terser = await getTerser();
6566
- const result = await terser(code, terserOptions);
6567
- return result.code.replace(RE_TRAILING_SEMICOLON, '');
6594
+ // Dispatch to appropriate minifier
6595
+ if (useEngine === 'terser') {
6596
+ // Create a copy to avoid mutating shared `terserOptions` (race condition)
6597
+ const terserCallOptions = {
6598
+ ...terserOptions,
6599
+ parse: {
6600
+ ...terserOptions.parse,
6601
+ bare_returns: inline
6602
+ }
6603
+ };
6604
+ const terser = await getTerser();
6605
+ const result = await terser(code, terserCallOptions);
6606
+ return result.code.replace(RE_TRAILING_SEMICOLON, '');
6607
+ } else if (useEngine === 'swc') {
6608
+ const swc = await getSwc();
6609
+ // `swc.minify()` takes compress and mangle directly as options
6610
+ const result = await swc.minify(code, {
6611
+ compress: true,
6612
+ mangle: true,
6613
+ ...swcOptions, // User options override defaults
6614
+ });
6615
+ return result.code.replace(RE_TRAILING_SEMICOLON, '');
6616
+ }
6617
+ throw new Error(`Unknown JS minifier engine: ${useEngine}`);
6568
6618
  })();
6619
+
6569
6620
  jsMinifyCache.set(jsKey, inFlight);
6570
6621
  const resolved = await inFlight;
6571
6622
  jsMinifyCache.set(jsKey, resolved);
@@ -6876,6 +6927,11 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
6876
6927
  }
6877
6928
  try {
6878
6929
  attrValue = await options.minifyCSS(attrValue, 'inline');
6930
+ // After minification, check if CSS consists entirely of invalid properties (no values)
6931
+ // E.g., `color:` or `margin:;padding:` should be treated as empty
6932
+ if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
6933
+ attrValue = '';
6934
+ }
6879
6935
  } catch (err) {
6880
6936
  if (!options.continueOnMinifyError) {
6881
6937
  throw err;
@@ -6924,6 +6980,11 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
6924
6980
  attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
6925
6981
  } else if (isMediaQuery(tag, attrs, attrName)) {
6926
6982
  attrValue = trimWhitespace(attrValue);
6983
+ // Only minify actual media queries (those with features in parentheses)
6984
+ // Skip simple media types like `all`, `screen`, `print` which are already minimal
6985
+ if (!/[()]/.test(attrValue)) {
6986
+ return attrValue;
6987
+ }
6927
6988
  try {
6928
6989
  return await options.minifyCSS(attrValue, 'media');
6929
6990
  } catch (err) {
@@ -7319,6 +7380,21 @@ async function getTerser() {
7319
7380
  return terserPromise;
7320
7381
  }
7321
7382
 
7383
+ let swcPromise;
7384
+ async function getSwc() {
7385
+ if (!swcPromise) {
7386
+ swcPromise = import('@swc/core')
7387
+ .then(m => m.default || m)
7388
+ .catch(() => {
7389
+ throw new Error(
7390
+ 'The swc minifier requires @swc/core to be installed.\n' +
7391
+ 'Install it with: npm install @swc/core'
7392
+ );
7393
+ });
7394
+ }
7395
+ return swcPromise;
7396
+ }
7397
+
7322
7398
  // Minification caches
7323
7399
 
7324
7400
  const cssMinifyCache = new LRU(200);
@@ -7513,10 +7589,14 @@ const jsMinifyCache = new LRU(200);
7513
7589
  *
7514
7590
  * Default: `false`
7515
7591
  *
7516
- * @prop {boolean | import("terser").MinifyOptions | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
7592
+ * @prop {boolean | import("terser").MinifyOptions | {engine?: 'terser' | 'swc', [key: string]: any} | ((text: string, inline?: boolean) => Promise<string> | string)} [minifyJS]
7517
7593
  * 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.
7594
+ * event handler attributes. If an object is provided, it can include:
7595
+ * - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
7596
+ * Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
7597
+ * regardless of engine setting, as swc doesn’t support bare return statements.
7598
+ * - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
7599
+ * SWC options if `engine: 'swc'`).
7520
7600
  * If a function is provided, it will be used to perform
7521
7601
  * custom JS minification. If disabled, JS is not minified.
7522
7602
  *
@@ -8586,6 +8666,7 @@ const minify$1 = async function (value, options) {
8586
8666
  options = processOptions(options || {}, {
8587
8667
  getLightningCSS,
8588
8668
  getTerser,
8669
+ getSwc,
8589
8670
  cssMinifyCache,
8590
8671
  jsMinifyCache
8591
8672
  });
@@ -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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAsBA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAED,mGAqBC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,4DAWC;AAED,2EAEC;AAED,qEAaC;AAED,wEAUC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAID,0IAqHC;AAsBD;;;;GAsCC;AAED,6GA4EC"}
1
+ {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAsBA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAED,mGAqBC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,4DAWC;AAED,2EAEC;AAED,qEAaC;AAED,wEAUC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAID,0IA+HC;AAsBD;;;;GAsCC;AAED,6GA4EC"}
@@ -41,12 +41,24 @@ export namespace tagDefaults {
41
41
  let type_1: string;
42
42
  export { type_1 as type };
43
43
  }
44
+ namespace link {
45
+ let media: string;
46
+ }
44
47
  namespace marquee {
45
48
  let behavior: string;
46
49
  let direction: string;
47
50
  }
51
+ namespace meta {
52
+ let media_1: string;
53
+ export { media_1 as media };
54
+ }
55
+ namespace source {
56
+ let media_2: string;
57
+ export { media_2 as media };
58
+ }
48
59
  namespace style {
49
- let media: string;
60
+ let media_3: string;
61
+ export { media_3 as media };
50
62
  }
51
63
  namespace textarea {
52
64
  let wrap: string;
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"AAEA,iCAAoC;AACpC,+BAAkC;AAClC,oCAA2C;AAC3C,2CAAmD;AACnD,wCAA8C;AAC9C,4CAAkD;AAClD,4CAA2C;AAC3C,4CAA0D;AAC1D,2CAA8C;AAC9C,+CAA0D;AAC1D,2CAAmC;AACnC,mCAA4C;AAK5C,+DAAgb;AAGhb,+DAA6O;AAG7O,yDAAmF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCnF,qDAQG;AAEH,+CAEG;AAcH,0CAUG;AApBH,0CAAwhB;AAExhB,yCAAkD;AAIlD,qCAA8C;AAuB9C,4CAAiF;AAEjF,0CAAoM;AAEpM,qCAAwF;AAExF,0CAA8C;AAE9C,qCAA6S;AAE7S,sCAAsF;AAEtF,6CAA8D;AAE9D,gDAAqD;AAErD,oCAAkD;AAElD,2CAAqD;AAErD,2CAA8D;AAE9D,mCAAuC;AAEvC,uCAAuD;AAEvD,sCAA8C;AAE9C,oCAA2D;AAE3D,uCAA8C;AAE9C,mCAA+wC;AAI/wC,sCAEsD;AAItD,6CAAwD"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"AAEA,iCAAoC;AACpC,+BAAkC;AAClC,oCAA2C;AAC3C,2CAAmD;AACnD,wCAA8C;AAC9C,4CAAkD;AAClD,4CAA2C;AAC3C,4CAA0D;AAC1D,2CAA8C;AAC9C,+CAA0D;AAC1D,2CAAmC;AACnC,mCAA4C;AAK5C,+DAAgb;AAGhb,+DAA6O;AAG7O,yDAAmF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CnF,qDAQG;AAEH,+CAEG;AAcH,0CAUG;AApBH,0CAAwhB;AAExhB,yCAAkD;AAIlD,qCAA8C;AAuB9C,4CAAiF;AAEjF,0CAAoM;AAEpM,qCAAwF;AAExF,0CAA8C;AAE9C,qCAA6S;AAE7S,sCAAsF;AAEtF,6CAA8D;AAE9D,gDAAqD;AAErD,oCAAkD;AAElD,2CAAqD;AAErD,2CAA8D;AAE9D,mCAAuC;AAEvC,uCAAuD;AAEvD,sCAA8C;AAE9C,oCAA2D;AAE3D,uCAA8C;AAE9C,mCAA+wC;AAI/wC,sCAEsD;AAItD,6CAAwD"}
@@ -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.1"
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
  });
@@ -272,6 +272,11 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
272
272
  }
273
273
  try {
274
274
  attrValue = await options.minifyCSS(attrValue, 'inline');
275
+ // After minification, check if CSS consists entirely of invalid properties (no values)
276
+ // E.g., `color:` or `margin:;padding:` should be treated as empty
277
+ if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
278
+ attrValue = '';
279
+ }
275
280
  } catch (err) {
276
281
  if (!options.continueOnMinifyError) {
277
282
  throw err;
@@ -320,6 +325,11 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
320
325
  attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
321
326
  } else if (isMediaQuery(tag, attrs, attrName)) {
322
327
  attrValue = trimWhitespace(attrValue);
328
+ // Only minify actual media queries (those with features in parentheses)
329
+ // Skip simple media types like `all`, `screen`, `print` which are already minimal
330
+ if (!/[()]/.test(attrValue)) {
331
+ return attrValue;
332
+ }
323
333
  try {
324
334
  return await options.minifyCSS(attrValue, 'media');
325
335
  } catch (err) {
@@ -48,10 +48,13 @@ const tagDefaults = {
48
48
  colorspace: 'limited-srgb',
49
49
  type: 'text'
50
50
  },
51
+ link: { media: 'all' },
51
52
  marquee: {
52
53
  behavior: 'scroll',
53
54
  direction: 'left'
54
55
  },
56
+ meta: { media: 'all' },
57
+ source: { media: 'all' },
55
58
  style: { media: 'all' },
56
59
  textarea: { wrap: 'soft' },
57
60
  track: { kind: 'subtitles' }
@@ -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);