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 +75 -15
- package/cli.js +1 -1
- package/dist/htmlminifier.cjs +107 -26
- package/dist/htmlminifier.esm.bundle.js +107 -26
- package/dist/types/htmlminifier.d.ts +10 -3
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +13 -1
- package/dist/types/lib/constants.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/attributes.js +10 -0
- package/src/lib/constants.js +3 -0
- 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,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>[](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
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/) |
|
|
241
|
-
| [CERN](https://home.cern/) | 152 |
|
|
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 | **
|
|
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/) | 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 |
|
|
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 | 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/) |
|
|
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 |
|
|
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 | **
|
|
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** | |
|
|
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
|
|
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',
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
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
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
|
2377
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
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
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
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
|
|
7519
|
-
*
|
|
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
|
|
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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"}
|
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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.1"
|
|
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/attributes.js
CHANGED
|
@@ -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) {
|
package/src/lib/constants.js
CHANGED
|
@@ -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' }
|
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);
|