html-minifier-next 7.5.3 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -24
- package/cli.js +178 -55
- package/dist/types/htmlminifier.d.ts +24 -4
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +2 -2
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +3 -2
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +1 -1
- package/dist/types/lib/fragments.d.ts +46 -0
- package/dist/types/lib/fragments.d.ts.map +1 -0
- package/dist/types/lib/option-definitions.d.ts +21 -194
- package/dist/types/lib/option-definitions.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +33 -5
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/unused-css.d.ts +43 -0
- package/dist/types/lib/unused-css.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +26 -1
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +2 -2
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/html-minifier-next.schema.json +11 -8
- package/package.json +2 -2
- package/src/htmlminifier.js +86 -88
- package/src/htmlparser.js +4 -4
- package/src/lib/attributes.js +65 -8
- package/src/lib/constants.js +8 -8
- package/src/lib/elements.js +3 -3
- package/src/lib/fragments.js +274 -0
- package/src/lib/option-definitions.js +19 -7
- package/src/lib/options.js +151 -28
- package/src/lib/unused-css.js +377 -0
- package/src/lib/utils.js +330 -2
- package/src/tokenchain.js +37 -30
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Use `npx html-minifier-next --help` to check all available options:
|
|
|
41
41
|
| `--file-ext <extensions>`, `-f <extensions>` | Specify file extension(s) to process (comma-separated, overrides config file setting); defaults to `html,htm,shtml,shtm`; use `*` for all files | `--file-ext=html,php`, `--file-ext='*'` |
|
|
42
42
|
| `--preset <name>`, `-p <name>` | Use a preset configuration (conservative or comprehensive) | `--preset=conservative` |
|
|
43
43
|
| `--config-file <file>`, `-c <file>` | Use a configuration file (defaults to html-minifier-next.config.json in the working directory, if present) | `--config-file=path/to/config.json` |
|
|
44
|
-
| `--verbose`, `-v` | Show detailed processing information (active options, file statistics) | `npx html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
|
|
44
|
+
| `--verbose`, `-v` | Show detailed processing information (active options, file statistics, and minifier warnings) | `npx html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
|
|
45
45
|
| `--dry`, `-d` | Dry run: Process and report statistics without writing output | `npx html-minifier-next input.html --dry --collapse-whitespace` |
|
|
46
46
|
|
|
47
47
|
### Configuration file
|
|
@@ -69,7 +69,7 @@ For editor support (validation, autocomplete, and inline documentation) in JSON
|
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
(If HMN is installed locally, you can also use the path
|
|
72
|
+
(If HMN is installed locally, you can also use the path ./node_modules/html-minifier-next/html-minifier-next.schema.json instead of the URL.)
|
|
73
73
|
|
|
74
74
|
**JavaScript module configuration example** (requires `"type": "module"` in the project’s package.json, or use a .mjs extension):
|
|
75
75
|
|
|
@@ -91,7 +91,7 @@ const result = await minify('<p title="example" id="moo">foo</p>', {
|
|
|
91
91
|
removeAttributeQuotes: true,
|
|
92
92
|
removeOptionalTags: true
|
|
93
93
|
});
|
|
94
|
-
console.log(result); //
|
|
94
|
+
console.log(result); // `<p title=example id=moo>foo`
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
See [the original blog post](https://perfectionkills.com/experimenting-with-html-minifier/) for details of [how it works](https://perfectionkills.com/experimenting-with-html-minifier/#how_it_works), [descriptions of most options](https://perfectionkills.com/experimenting-with-html-minifier/#options), [testing results](https://perfectionkills.com/experimenting-with-html-minifier/#field_testing), and [conclusions](https://perfectionkills.com/experimenting-with-html-minifier/#cost_and_benefits).
|
|
@@ -144,7 +144,6 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
144
144
|
| `customAttrCollapse`<br>`--custom-attr-collapse` | Regex that specifies custom attribute to strip newlines from (e.g., `/ng-class/`) | `undefined` |
|
|
145
145
|
| `customAttrSurround`<br>`--custom-attr-surround` | Array of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
|
|
146
146
|
| `customEventAttributes`<br>`--custom-event-attributes` | Array of regexes that allow to support custom event attributes for `minifyJS` (e.g., `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
|
|
147
|
-
| `customFragmentQuantifierLimit`<br>`--custom-fragment-quantifier-limit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
|
|
148
147
|
| `decodeEntities`<br>`--decode-entities` | Use direct Unicode characters whenever possible | `false` |
|
|
149
148
|
| `ignoreCustomComments`<br>`--ignore-custom-comments` | Array of regexes that allow to ignore matching comments | `[ /^!/, /^\s*#/ ]` |
|
|
150
149
|
| `ignoreCustomFragments`<br>`--ignore-custom-fragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
|
|
@@ -173,8 +172,10 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
173
172
|
| `removeOptionalTags`<br>`--remove-optional-tags` | [Remove optional tags](https://perfectionkills.com/experimenting-with-html-minifier/#remove_optional_tags) | `false` |
|
|
174
173
|
| `removeRedundantAttributes`<br>`--remove-redundant-attributes` | [Remove attributes when value matches default](https://meiert.com/blog/optional-html/#toc-attribute-values) | `false` |
|
|
175
174
|
| `removeTagWhitespace`<br>`--remove-tag-whitespace` | Remove space between attributes whenever possible; **note that this will result in invalid HTML** | `false` |
|
|
175
|
+
| `removeUnusedCSS`<br>`--remove-unused-css` | [Remove unused CSS rules](#unused-css-removal) from `style` elements; requires `minifyCSS`; **note that this can change how a document renders** | `false` (could be `true`, `{ safelist, scripts }`) |
|
|
176
176
|
| `sortAttributes`<br>`--sort-attributes` | [Sort attributes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
177
177
|
| `sortClassNames`<br>`--sort-class-names` | [Sort style classes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
178
|
+
| `strictCustomFragments`<br>`--strict-custom-fragments` | [Reject `ignoreCustomFragments` patterns that risk catastrophic backtracking](#redos-protection) (rather than warning about them) | `false` |
|
|
178
179
|
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around custom fragments (`ignoreCustomFragments`) | `false` |
|
|
179
180
|
| `useShortDoctype`<br>`--use-short-doctype` | [Replaces the doctype with the short HTML doctype](https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype) | `false` |
|
|
180
181
|
|
|
@@ -186,7 +187,7 @@ A few options take functions and are therefore only available programmatically,
|
|
|
186
187
|
| --- | --- | --- |
|
|
187
188
|
| `canCollapseWhitespace` | `Function(tag, attrs, defaultFn)` that determines whether whitespace inside an element can be collapsed—override to protect additional elements, delegating to `defaultFn` for the rest | Built-in handling (protects `pre`, `textarea`, etc.) |
|
|
188
189
|
| `canTrimWhitespace` | `Function(tag, attrs, defaultFn)` that determines whether leading and trailing whitespace around an element may be trimmed | Built-in handling |
|
|
189
|
-
| `log` | `Function(message)` called with warnings and errors, including minification errors swallowed by `continueOnMinifyError` (e.g., pass `console.error` to surface them) | No-op (errors are silent) |
|
|
190
|
+
| `log` | `Function(message)` called with warnings and errors, including minification errors swallowed by `continueOnMinifyError` (e.g., pass `console.error` to surface them); the CLI wires this up under `--verbose` and `--dry` | No-op (errors are silent) |
|
|
190
191
|
|
|
191
192
|
### Sorting attributes and style classes
|
|
192
193
|
|
|
@@ -216,7 +217,7 @@ Available Lightning CSS options when passed as an object:
|
|
|
216
217
|
|
|
217
218
|
* `targets`: Browser targets for vendor prefix optimization (e.g., `{ chrome: 95, firefox: 90 }`).
|
|
218
219
|
* `unusedSymbols`: Array of class names, IDs, keyframe names, and CSS variables to remove.
|
|
219
|
-
* `errorRecovery`: Boolean to skip invalid rules instead of throwing errors. This is disabled by default in Lightning CSS, but enabled in HMN when the `continueOnMinifyError` option is set to `true` (the default). Explicitly setting `errorRecovery` in `minifyCSS` options will override this automatic behavior.
|
|
220
|
+
* `errorRecovery`: Boolean to skip invalid rules instead of throwing errors. This is disabled by default in Lightning CSS, but enabled in HMN when the `continueOnMinifyError` option is set to `true` (the default). Explicitly setting `errorRecovery` in `minifyCSS` options will override this automatic behavior. What Lightning CSS takes issue with is reported through [the `log` hook](#api-only-options)—it drops some of it (`@property` with an invalid `syntax`) and passes the rest through (an unknown at-rule), so that a dropped rule does not go unnoticed. Every document is reported on separately.
|
|
220
221
|
* `sourceMap`: Boolean to generate source maps.
|
|
221
222
|
|
|
222
223
|
For advanced usage, you can also pass a function:
|
|
@@ -231,6 +232,44 @@ const result = await minify(html, {
|
|
|
231
232
|
});
|
|
232
233
|
```
|
|
233
234
|
|
|
235
|
+
### Unused CSS removal
|
|
236
|
+
|
|
237
|
+
`removeUnusedCSS` removes rules from `style` elements whose class or ID selectors the document doesn’t reference. It requires `minifyCSS`, because the removal runs through Lightning CSS—passing `minifyCSS` a function of your own replaces that step, so the removal does not apply, either. Both cases are reported through [the `log` hook](#api-only-options). It does not touch `style` or `media` attributes.
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
const result = await minify(html, {
|
|
241
|
+
minifyCSS: true,
|
|
242
|
+
removeUnusedCSS: true
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Symbols are considered used when they appear
|
|
247
|
+
|
|
248
|
+
* in a `class` or `id` attribute,
|
|
249
|
+
* in an attribute that references an ID (`for`, `headers`, `list`, `popovertarget`, `aria-controls`, and similar),
|
|
250
|
+
* in a same-document fragment URL, as `href="#main"`, `<use href="#icon">`, or `usemap="#map"`, and in a `url(#gradient)` reference from any attribute,
|
|
251
|
+
* anywhere in a `data-*` attribute value, or
|
|
252
|
+
* anywhere inside an inline `script` element, unless `scripts` is set to `false`.
|
|
253
|
+
|
|
254
|
+
Names carrying characters that end a CSS identifier—`md:flex`, `w-1/2`, `p-[3px]`—are matched as whole tokens, so utility-CSS class names survive whether they come from markup, a `data-*` value, or a string in an inline script.
|
|
255
|
+
|
|
256
|
+
**Class names that only appear in external scripts cannot be detected.** A minifier sees one document, not the DOM that scripts later build from it, so a class added by bundle.js looks exactly like a class nobody uses. List those under `safelist`, as strings or regular expressions:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
const result = await minify(html, {
|
|
260
|
+
minifyCSS: true,
|
|
261
|
+
removeUnusedCSS: {
|
|
262
|
+
safelist: ['is-open', /^js-/],
|
|
263
|
+
// Set to `false` to also drop rules only referenced from inline scripts
|
|
264
|
+
scripts: true
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Names used by `@keyframes` and `@counter-style` rules are not removed, even when no element carries them as a class or ID, since those at-rules are referenced from CSS rather than from markup.
|
|
270
|
+
|
|
271
|
+
Values that cannot be honored—a `safelist` that isn’t an array, an entry that is neither a string nor a regular expression, a misspelled key—are reported through the `log` hook.
|
|
272
|
+
|
|
234
273
|
### JavaScript minification
|
|
235
274
|
|
|
236
275
|
When `minifyJS` is set to `true`, HTML Minifier Next uses [Terser](https://terser.org/) by default to minify JavaScript in `<script>` elements and event attributes.
|
|
@@ -500,7 +539,7 @@ SVG and MathML elements are automatically recognized as foreign elements, and wh
|
|
|
500
539
|
|
|
501
540
|
### Working with invalid or partial markup
|
|
502
541
|
|
|
503
|
-
By default,
|
|
542
|
+
By default, HMN parses markup into a complete tree structure, then modifies it (removing anything that was specified for removal, ignoring anything that was specified to be ignored, etc.), then creates markup from that tree and returns it.
|
|
504
543
|
|
|
505
544
|
_Input markup (e.g., `<p id="">foo`) → Internal representation of markup in a form of tree (e.g., `{ tag: "p", attr: "id", children: ["foo"] }`) → Transformation of internal representation (e.g., removal of `id` attribute) → Output of resulting markup (e.g., `<p>foo</p>`)_
|
|
506
545
|
|
|
@@ -512,36 +551,35 @@ To validate complete HTML markup, use [the W3C validator](https://validator.w3.o
|
|
|
512
551
|
|
|
513
552
|
### ReDoS protection
|
|
514
553
|
|
|
515
|
-
|
|
554
|
+
You can use `ignoreCustomFragments` to hand HTML Minifier Next a regular expression to run against your documents. This is also where a regular expression denial of service (ReDoS) could originate:
|
|
516
555
|
|
|
517
|
-
*
|
|
556
|
+
* Matching without backtracking: A pattern that wraps an any-character or negated-class body in literal delimiters—`<%[\s\S]*?%>` or `\{\{[^}]*?\}\}`, and every other shape below—is matched by scanning for those delimiters in linear time, with no regular expression involved. Patterns of other shapes run as regular expressions, one per pattern, so each keeps its own flags.
|
|
518
557
|
|
|
519
|
-
*
|
|
558
|
+
* Pattern detection: HMN warns about the shapes that backtrack catastrophically—an unlimited quantifier over a group that itself contains a quantifier that can vary (`(a+)+`, `(a?)+`) or alternation (`(a|b)*`), and the same atom repeated unboundedly twice in a row (`.*.*`). A fixed count does not vary, so `(?:a{4})+` passes. These are also shapes a linear scan cannot stand in for. `strictCustomFragments` refuses them with an error instead, which is worth enabling where the patterns or the input are not entirely under your control. A pattern longer than 10,000 characters or nested more than 50 groups deep is judged risky without being analyzed further, so that reading the pattern cannot itself become the expensive step.
|
|
520
559
|
|
|
521
|
-
*
|
|
560
|
+
* Input length limits: The `maxInputLength` option allows you to set a maximum input size to prevent processing of excessively large inputs that could cause performance issues.
|
|
522
561
|
|
|
523
|
-
**Important:**
|
|
562
|
+
**Important:** A single unlimited quantifier is not one of those shapes: `[\s\S]*?` running up to a literal terminator matches in linear time, and it is how HMN’s defaults are written. Bounds are still worth adding where you know the maximum length of a fragment, since they cap how far a failing match can scan.
|
|
524
563
|
|
|
525
564
|
#### Custom fragment examples
|
|
526
565
|
|
|
527
|
-
**Safe patterns
|
|
566
|
+
**Safe patterns:**
|
|
528
567
|
|
|
529
568
|
```js
|
|
530
569
|
ignoreCustomFragments: [
|
|
531
|
-
/<%[\s\S]
|
|
532
|
-
/<\?php[\s\S]{0,5000}?\?>/, // PHP with bounds
|
|
570
|
+
/<%[\s\S]*?%>/, // Lazy scan up to a literal terminator
|
|
571
|
+
/<\?php[\s\S]{0,5000}?\?>/, // PHP with explicit bounds
|
|
533
572
|
/\{\{[^}]{0,500}\}\}/ // Handlebars without nested braces
|
|
534
573
|
]
|
|
535
574
|
```
|
|
536
575
|
|
|
537
|
-
**
|
|
576
|
+
**Unsafe patterns** (these trigger warnings):
|
|
538
577
|
|
|
539
578
|
```js
|
|
540
579
|
ignoreCustomFragments: [
|
|
541
|
-
/<%
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
/(script|style)[\s\S]*?/ // Multiple unlimited quantifiers
|
|
580
|
+
/<%(\s|\S)*?%>/, // Unlimited quantifier over an alternating group
|
|
581
|
+
/\{\{([^}]+)+\}\}/, // Nested unlimited quantifiers
|
|
582
|
+
/<!--[\s\S]*[\s\S]*-->/ // The same atom repeated unboundedly twice
|
|
545
583
|
]
|
|
546
584
|
```
|
|
547
585
|
|
|
@@ -561,8 +599,6 @@ ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
|
561
599
|
ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
562
600
|
```
|
|
563
601
|
|
|
564
|
-
**Important:** When using custom `ignoreCustomFragments`, the minifier automatically applies bounded quantifiers to prevent ReDoS attacks, but you can also write safer patterns yourself using explicit bounds.
|
|
565
|
-
|
|
566
602
|
##### Escaping patterns in different contexts
|
|
567
603
|
|
|
568
604
|
The escaping requirements for `ignoreCustomFragments` patterns differ depending on how you’re using HMN:
|
|
@@ -643,11 +679,11 @@ Parameters:
|
|
|
643
679
|
|
|
644
680
|
* No argument: Runs and, if a baseline exists, shows size and time deltas
|
|
645
681
|
* `--save`: Saves the run as the baseline (e.g., on `main` before switching to a branch)
|
|
646
|
-
* `--core`: Disables the external minifiers (CSS, JS, SVG, URLs) to isolate HMN’s
|
|
682
|
+
* `--core`: Disables the external minifiers (CSS, JS, SVG, URLs) to isolate HMN’s processing time
|
|
647
683
|
* `--iterations=N`: Sets the number of timed iterations (default 5; the median is reported)
|
|
648
684
|
* `--config=PATH`: Uses an alternative options file (default html-minifier-next.config.json)
|
|
649
685
|
|
|
650
|
-
To compare branches (A/B run), execute `npm run benchmark -- --save` on `main`, then `npm run benchmark` on the branch to see the deltas. Add `--core` on both ends when measuring changes to HMN
|
|
686
|
+
To compare branches (A/B run), execute `npm run benchmark -- --save` on `main`, then `npm run benchmark` on the branch to see the deltas. Add `--core` on both ends when measuring changes to HMN rather than bundled minifiers.
|
|
651
687
|
|
|
652
688
|
#### Profiling
|
|
653
689
|
|