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.
Files changed (3) hide show
  1. package/README.md +41 -24
  2. package/cli.js +75 -8
  3. 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, comprehensive) | `--preset=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>[![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/) |
231
234
  | --- | --- | --- | --- | --- | --- | --- | --- | --- |
232
235
  | [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
233
- | [Apple](https://www.apple.com/) | 258 | **202** | **202** | 230 | 233 | 234 | 236 | 237 |
234
- | [BBC](https://www.bbc.co.uk/) | 633 | **575** | 585 | 594 | 594 | 595 | 628 | n/a |
235
- | [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 126 | 141 | 141 | 147 | 143 |
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/) | 55 | **47** | **47** | 50 | 48 | 49 | 50 | 50 |
238
- | [FAZ](https://www.faz.net/aktuell/) | 1573 | 1465 | 1470 | **1411** | 1498 | 1509 | 1520 | n/a |
239
- | [Frontend Dogma](https://frontenddogma.com/) | 222 | **213** | **213** | 234 | 219 | 221 | 239 | 220 |
240
- | [Google](https://www.google.com/) | 76 | **71** | 72 | n/a | 72 | 73 | 76 | 75 |
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/) | 1457 | **1225** | **1225** | 1232 | 1231 | 1226 | 1451 | n/a |
244
- | [Mastodon](https://mastodon.social/explore) | 35 | **26** | **26** | 30 | 33 | 33 | 34 | 34 |
245
- | [MDN](https://developer.mozilla.org/en-US/) | 107 | **62** | **62** | 64 | 64 | 65 | 67 | 68 |
246
- | [Middle East Eye](https://www.middleeasteye.net/) | 222 | **195** | **195** | 202 | 200 | 200 | 202 | 203 |
247
- | [Nielsen Norman Group](https://www.nngroup.com/) | 84 | 71 | 71 | **53** | 71 | 72 | 74 | 73 |
248
- | [SitePoint](https://www.sitepoint.com/) | 495 | **353** | **353** | 431 | 469 | 473 | 491 | n/a |
249
- | [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 37 | 38 | 39 | 39 |
250
- | [TPGi](https://www.tpgi.com/) | 99 | **79** | **79** | 84 | 84 | 84 | 88 | 86 |
251
- | [United Nations](https://www.un.org/en/) | 154 | **115** | 116 | 123 | 127 | 127 | 133 | 126 |
252
- | [W3C](https://www.w3.org/) | 50 | **36** | **36** | 38 | 38 | 38 | 40 | 39 |
253
- | **Average processing time** | | 352 ms (21/21) | 401 ms (21/21) | 175 ms (20/21) | 56 ms (21/21) | **17 ms (21/21)** | 395 ms (21/21) | 1481 ms (16/21) |
254
-
255
- (Last updated: Dec 10, 2025)
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., "html" or "html,htm,php"');
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
- async function countFiles(dir, extensions, skipRootAbs) {
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
- count += await countFiles(filePath, extensions, skipRootAbs);
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
- const dirStats = await processDirectory(inputFile, outputFile, extensions, isDryRun, isVerbose, skipRootAbs, progress);
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
@@ -84,5 +84,5 @@
84
84
  "test:watch": "node --test --watch tests/*.spec.js"
85
85
  },
86
86
  "type": "module",
87
- "version": "4.8.3"
87
+ "version": "4.9.0"
88
88
  }