html-minifier-next 4.8.3 → 4.9.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 +41 -24
- package/cli.js +75 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,11 +29,12 @@ Use `html-minifier-next --help` to check all available options:
|
|
|
29
29
|
| Option | Description | Example |
|
|
30
30
|
| --- | --- | --- |
|
|
31
31
|
| `--input-dir <dir>` | Specify an input directory (best restricted with `--file-ext`) | `--input-dir=src` |
|
|
32
|
+
| `--ignore-dir <patterns>` | Exclude directories—relative to input directory—from processing (comma-separated, overrides config file setting) | `--ignore-dir=libs`, `--ignore-dir=libs,vendor,node_modules` |
|
|
32
33
|
| `--output-dir <dir>` | Specify an output directory | `--output-dir=dist` |
|
|
33
|
-
| `--file-ext <extensions>` | Specify file extension(s) to process (overrides config file setting) | `--file-ext=html`, `--file-ext=html,htm,php`, `--file-ext="html, htm, php"` |
|
|
34
|
+
| `--file-ext <extensions>` | Specify file extension(s) to process (comma-separated, overrides config file setting) | `--file-ext=html`, `--file-ext=html,htm,php`, `--file-ext="html, htm, php"` |
|
|
34
35
|
| `-o <file>`, `--output <file>` | Specify output file (reads from file arguments or STDIN) | File to file: `html-minifier-next input.html -o output.html`<br>Pipe to file: `cat input.html \| html-minifier-next -o output.html`<br>File to STDOUT: `html-minifier-next input.html` |
|
|
35
36
|
| `-c <file>`, `--config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
|
|
36
|
-
| `--preset <name>` | Use a preset configuration (conservative
|
|
37
|
+
| `--preset <name>` | Use a preset configuration (conservative or comprehensive) | `--preset=conservative` |
|
|
37
38
|
| `-v`, `--verbose` | Show detailed processing information (active options, file statistics) | `html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
|
|
38
39
|
| `-d`, `--dry` | Dry run: Process and report statistics without writing output | `html-minifier-next input.html --dry --collapse-whitespace` |
|
|
39
40
|
| `-V`, `--version` | Output the version number | `html-minifier-next --version` |
|
|
@@ -48,7 +49,8 @@ You can also use a configuration file to specify options. The file can be either
|
|
|
48
49
|
{
|
|
49
50
|
"collapseWhitespace": true,
|
|
50
51
|
"removeComments": true,
|
|
51
|
-
"fileExt": "html,htm"
|
|
52
|
+
"fileExt": "html,htm",
|
|
53
|
+
"ignoreDir": "libs,vendor"
|
|
52
54
|
}
|
|
53
55
|
```
|
|
54
56
|
|
|
@@ -58,7 +60,8 @@ You can also use a configuration file to specify options. The file can be either
|
|
|
58
60
|
module.exports = {
|
|
59
61
|
collapseWhitespace: true,
|
|
60
62
|
removeComments: true,
|
|
61
|
-
fileExt: "html,htm"
|
|
63
|
+
fileExt: "html,htm",
|
|
64
|
+
ignoreDir: ["libs", "vendor"]
|
|
62
65
|
};
|
|
63
66
|
```
|
|
64
67
|
|
|
@@ -230,29 +233,30 @@ How does HTML Minifier Next compare to other minifiers? (All with the most aggre
|
|
|
230
233
|
| 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/) |
|
|
231
234
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
232
235
|
| [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
|
|
233
|
-
| [Apple](https://www.apple.com/) |
|
|
234
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
235
|
-
| [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 126 |
|
|
236
|
+
| [Apple](https://www.apple.com/) | 260 | **204** | **204** | 232 | 235 | 236 | 238 | 239 |
|
|
237
|
+
| [BBC](https://www.bbc.co.uk/) | 803 | **683** | 693 | 746 | 757 | 760 | 796 | n/a |
|
|
238
|
+
| [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 126 | 142 | 142 | 147 | 143 |
|
|
236
239
|
| [ECMAScript](https://tc39.es/ecma262/) | 7238 | **6341** | **6341** | 6561 | 6444 | 6567 | 6614 | n/a |
|
|
237
|
-
| [EFF](https://www.eff.org/) |
|
|
238
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
239
|
-
| [Frontend Dogma](https://frontenddogma.com/) | 222 | **
|
|
240
|
-
| [Google](https://www.google.com/) |
|
|
240
|
+
| [EFF](https://www.eff.org/) | 56 | **47** | 48 | 50 | 49 | 49 | 51 | 51 |
|
|
241
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1602 | 1492 | 1497 | **1435** | 1525 | 1537 | 1548 | n/a |
|
|
242
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 222 | **212** | 213 | 234 | 219 | 221 | 240 | 221 |
|
|
243
|
+
| [Google](https://www.google.com/) | 18 | **17** | **17** | **17** | **17** | **17** | 18 | 18 |
|
|
244
|
+
| [Ground News](https://ground.news/) | 1576 | **1353** | 1356 | 1450 | 1473 | 1478 | 1563 | n/a |
|
|
241
245
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 149 |
|
|
242
246
|
| [Igalia](https://www.igalia.com/) | 49 | **33** | **33** | 35 | 35 | 35 | 36 | 36 |
|
|
243
|
-
| [Leanpub](https://leanpub.com/) |
|
|
244
|
-
| [Mastodon](https://mastodon.social/explore) |
|
|
245
|
-
| [MDN](https://developer.mozilla.org/en-US/) | 107 | **
|
|
246
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
247
|
-
| [Nielsen Norman Group](https://www.nngroup.com/) | 84 |
|
|
248
|
-
| [SitePoint](https://www.sitepoint.com/) |
|
|
249
|
-
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** |
|
|
250
|
-
| [TPGi](https://www.tpgi.com/) |
|
|
251
|
-
| [United Nations](https://www.un.org/en/) |
|
|
252
|
-
| [W3C](https://www.w3.org/) | 50 | **
|
|
253
|
-
| **Average processing time** | |
|
|
254
|
-
|
|
255
|
-
(Last updated: Dec
|
|
247
|
+
| [Leanpub](https://leanpub.com/) | 2036 | **1755** | **1755** | 1762 | 1761 | 1759 | 2031 | n/a |
|
|
248
|
+
| [Mastodon](https://mastodon.social/explore) | 36 | **27** | **27** | 31 | 34 | 34 | 35 | 35 |
|
|
249
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 107 | **61** | **61** | 63 | 63 | 64 | 66 | 67 |
|
|
250
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **195** | 196 | 203 | 201 | 200 | 202 | 203 |
|
|
251
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 84 | 72 | 72 | **53** | 72 | 73 | 74 | 73 |
|
|
252
|
+
| [SitePoint](https://www.sitepoint.com/) | 487 | **346** | **346** | 424 | 461 | 466 | 484 | n/a |
|
|
253
|
+
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 38 | 39 | 39 |
|
|
254
|
+
| [TPGi](https://www.tpgi.com/) | 175 | **160** | 162 | **160** | 165 | 166 | 172 | 172 |
|
|
255
|
+
| [United Nations](https://www.un.org/en/) | 150 | **112** | 113 | 120 | 124 | 124 | 129 | 122 |
|
|
256
|
+
| [W3C](https://www.w3.org/) | 50 | **35** | 36 | 38 | 38 | 38 | 40 | 38 |
|
|
257
|
+
| **Average processing time** | | 338 ms (22/22) | 385 ms (22/22) | 191 ms (22/22) | 70 ms (22/22) | **18 ms (22/22)** | 365 ms (22/22) | 1403 ms (16/22) |
|
|
258
|
+
|
|
259
|
+
(Last updated: Dec 12, 2025)
|
|
256
260
|
<!-- End auto-generated -->
|
|
257
261
|
|
|
258
262
|
## Examples
|
|
@@ -283,6 +287,19 @@ html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
|
|
|
283
287
|
# Consider restricting with `--file-ext` to avoid touching binaries (e.g., images, archives)
|
|
284
288
|
```
|
|
285
289
|
|
|
290
|
+
**Exclude directories from processing:**
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# Ignore a single directory
|
|
294
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs
|
|
295
|
+
|
|
296
|
+
# Ignore multiple directories
|
|
297
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs,vendor,node_modules
|
|
298
|
+
|
|
299
|
+
# Ignore by relative path (only ignores src/static/libs, not other “libs” directories)
|
|
300
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=static/libs
|
|
301
|
+
```
|
|
302
|
+
|
|
286
303
|
**Dry run mode (preview outcome without writing files):**
|
|
287
304
|
|
|
288
305
|
```bash
|
package/cli.js
CHANGED
|
@@ -247,6 +247,14 @@ function normalizeConfig(config) {
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// Handle `ignoreDir` in config file
|
|
251
|
+
if ('ignoreDir' in normalized) {
|
|
252
|
+
// Support both string (`libs,vendor`) and array (`["libs", "vendor"]`) formats
|
|
253
|
+
if (Array.isArray(normalized.ignoreDir)) {
|
|
254
|
+
normalized.ignoreDir = normalized.ignoreDir.join(',');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
250
258
|
return normalized;
|
|
251
259
|
}
|
|
252
260
|
|
|
@@ -254,8 +262,9 @@ let config = {};
|
|
|
254
262
|
program.option('-c --config-file <file>', 'Use config file');
|
|
255
263
|
program.option('--preset <name>', `Use a preset configuration (${getPresetNames().join(', ')})`);
|
|
256
264
|
program.option('--input-dir <dir>', 'Specify an input directory');
|
|
265
|
+
program.option('--ignore-dir <patterns>', 'Exclude directories—relative to input directory—from processing (comma-separated), e.g., “libs” or “libs,vendor,node_modules”');
|
|
257
266
|
program.option('--output-dir <dir>', 'Specify an output directory');
|
|
258
|
-
program.option('--file-ext <extensions>', 'Specify file extension(s) to process (comma-separated), e.g.,
|
|
267
|
+
program.option('--file-ext <extensions>', 'Specify file extension(s) to process (comma-separated), e.g., “html” or “html,htm,php”');
|
|
259
268
|
|
|
260
269
|
(async () => {
|
|
261
270
|
let content;
|
|
@@ -375,7 +384,42 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
375
384
|
return fileExtensions.includes(fileExt);
|
|
376
385
|
}
|
|
377
386
|
|
|
378
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Parse comma-separated ignore patterns into an array
|
|
389
|
+
* @param {string} patterns - Comma-separated directory patterns (e.g., "libs,vendor")
|
|
390
|
+
* @returns {string[]} Array of trimmed pattern strings with normalized separators
|
|
391
|
+
*/
|
|
392
|
+
function parseIgnorePatterns(patterns) {
|
|
393
|
+
if (!patterns) return [];
|
|
394
|
+
return patterns
|
|
395
|
+
.split(',')
|
|
396
|
+
.map(p => p.trim().replace(/\\/g, '/').replace(/\/+$/, ''))
|
|
397
|
+
.filter(p => p.length > 0);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check if a directory should be ignored based on ignore patterns
|
|
402
|
+
* Supports matching by directory name or relative path
|
|
403
|
+
* @param {string} dirPath - Absolute path to the directory
|
|
404
|
+
* @param {string[]} ignorePatterns - Array of patterns to match against (with forward slashes)
|
|
405
|
+
* @param {string} baseDir - Base directory for relative path calculation
|
|
406
|
+
* @returns {boolean} True if directory should be ignored
|
|
407
|
+
*/
|
|
408
|
+
function shouldIgnoreDirectory(dirPath, ignorePatterns, baseDir) {
|
|
409
|
+
if (!ignorePatterns || ignorePatterns.length === 0) return false;
|
|
410
|
+
|
|
411
|
+
// Normalize to forward slashes for cross-platform comparison
|
|
412
|
+
const relativePath = path.relative(baseDir, dirPath).replace(/\\/g, '/');
|
|
413
|
+
const dirName = path.basename(dirPath);
|
|
414
|
+
|
|
415
|
+
return ignorePatterns.some(pattern => {
|
|
416
|
+
// Support both exact directory names and relative paths
|
|
417
|
+
return dirName === pattern || relativePath === pattern ||
|
|
418
|
+
relativePath.startsWith(pattern + '/');
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function countFiles(dir, extensions, skipRootAbs, ignorePatterns, baseDir) {
|
|
379
423
|
let count = 0;
|
|
380
424
|
|
|
381
425
|
const files = await fs.promises.readdir(dir).catch(() => []);
|
|
@@ -397,7 +441,11 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
397
441
|
}
|
|
398
442
|
|
|
399
443
|
if (lst.isDirectory()) {
|
|
400
|
-
|
|
444
|
+
// Skip ignored directories
|
|
445
|
+
if (shouldIgnoreDirectory(filePath, ignorePatterns, baseDir)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
count += await countFiles(filePath, extensions, skipRootAbs, ignorePatterns, baseDir);
|
|
401
449
|
} else if (shouldProcessFile(file, extensions)) {
|
|
402
450
|
count++;
|
|
403
451
|
}
|
|
@@ -423,12 +471,17 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
423
471
|
process.stderr.write('\r\x1b[K'); // Clear the line
|
|
424
472
|
}
|
|
425
473
|
|
|
426
|
-
async function processDirectory(inputDir, outputDir, extensions, isDryRun = false, isVerbose = false, skipRootAbs, progress = null) {
|
|
474
|
+
async function processDirectory(inputDir, outputDir, extensions, isDryRun = false, isVerbose = false, skipRootAbs, progress = null, ignorePatterns = [], baseDir = null) {
|
|
427
475
|
// If first call provided a string, normalize once; otherwise assume pre-parsed array
|
|
428
476
|
if (typeof extensions === 'string') {
|
|
429
477
|
extensions = parseFileExtensions(extensions);
|
|
430
478
|
}
|
|
431
479
|
|
|
480
|
+
// Set `baseDir` on first call
|
|
481
|
+
if (baseDir === null) {
|
|
482
|
+
baseDir = inputDir;
|
|
483
|
+
}
|
|
484
|
+
|
|
432
485
|
const files = await fs.promises.readdir(inputDir).catch(err => {
|
|
433
486
|
fatal('Cannot read directory ' + inputDir + '\n' + err.message);
|
|
434
487
|
});
|
|
@@ -456,7 +509,11 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
456
509
|
}
|
|
457
510
|
|
|
458
511
|
if (lst.isDirectory()) {
|
|
459
|
-
|
|
512
|
+
// Skip ignored directories
|
|
513
|
+
if (shouldIgnoreDirectory(inputFile, ignorePatterns, baseDir)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const dirStats = await processDirectory(inputFile, outputFile, extensions, isDryRun, isVerbose, skipRootAbs, progress, ignorePatterns, baseDir);
|
|
460
517
|
if (dirStats) {
|
|
461
518
|
allStats.push(...dirStats);
|
|
462
519
|
}
|
|
@@ -535,12 +592,16 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
535
592
|
process.stdout.write(minified);
|
|
536
593
|
};
|
|
537
594
|
|
|
538
|
-
const { inputDir, outputDir, fileExt } = programOptions;
|
|
595
|
+
const { inputDir, outputDir, fileExt, ignoreDir } = programOptions;
|
|
539
596
|
|
|
540
597
|
// Resolve file extensions: CLI argument takes priority over config file, even if empty string
|
|
541
598
|
const hasCliFileExt = program.getOptionValueSource('fileExt') === 'cli';
|
|
542
599
|
const resolvedFileExt = hasCliFileExt ? fileExt : config.fileExt;
|
|
543
600
|
|
|
601
|
+
// Resolve ignore patterns: CLI argument takes priority over config file
|
|
602
|
+
const hasCliIgnoreDir = program.getOptionValueSource('ignoreDir') === 'cli';
|
|
603
|
+
const resolvedIgnoreDir = hasCliIgnoreDir ? ignoreDir : config.ignoreDir;
|
|
604
|
+
|
|
544
605
|
if (inputDir || outputDir) {
|
|
545
606
|
if (!inputDir) {
|
|
546
607
|
fatal('The option `output-dir` needs to be used with the option `input-dir`—if you are working with a single file, use `-o`');
|
|
@@ -581,6 +642,12 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
581
642
|
const showProgress = process.stderr.isTTY && !isVerbose;
|
|
582
643
|
let progress = null;
|
|
583
644
|
|
|
645
|
+
// Parse ignore patterns
|
|
646
|
+
const ignorePatterns = parseIgnorePatterns(resolvedIgnoreDir);
|
|
647
|
+
|
|
648
|
+
// Resolve base directory for consistent path comparisons
|
|
649
|
+
const inputDirResolved = await fs.promises.realpath(inputDir).catch(() => inputDir);
|
|
650
|
+
|
|
584
651
|
if (showProgress) {
|
|
585
652
|
// Start with indeterminate progress, count in background
|
|
586
653
|
progress = {current: 0, total: null};
|
|
@@ -591,7 +658,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
591
658
|
// then see the updated value once `countFiles` resolves,
|
|
592
659
|
// transitioning the indicator from indeterminate to determinate progress without race conditions.
|
|
593
660
|
const extensions = typeof resolvedFileExt === 'string' ? parseFileExtensions(resolvedFileExt) : resolvedFileExt;
|
|
594
|
-
countFiles(inputDir, extensions, skipRootAbs).then(total => {
|
|
661
|
+
countFiles(inputDir, extensions, skipRootAbs, ignorePatterns, inputDirResolved).then(total => {
|
|
595
662
|
if (progress) {
|
|
596
663
|
progress.total = total;
|
|
597
664
|
}
|
|
@@ -600,7 +667,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
600
667
|
});
|
|
601
668
|
}
|
|
602
669
|
|
|
603
|
-
const stats = await processDirectory(inputDir, outputDir, resolvedFileExt, programOptions.dry, isVerbose, skipRootAbs, progress);
|
|
670
|
+
const stats = await processDirectory(inputDir, outputDir, resolvedFileExt, programOptions.dry, isVerbose, skipRootAbs, progress, ignorePatterns, inputDirResolved);
|
|
604
671
|
|
|
605
672
|
// Show completion message and clear progress indicator
|
|
606
673
|
if (progress) {
|
package/package.json
CHANGED