html-minifier-next 4.11.0 → 4.12.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 CHANGED
@@ -236,34 +236,34 @@ How does HTML Minifier Next compare to other minifiers? (All with the most aggre
236
236
  | Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next)<br>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[![npm last update](https://img.shields.io/npm/last-update/html-minifier-terser)](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[![npm last update](https://img.shields.io/npm/last-update/htmlnano)](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[![npm last update](https://img.shields.io/npm/last-update/@swc/html)](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[![npm last update](https://img.shields.io/npm/last-update/@minify-html/node)](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[![npm last update](https://img.shields.io/npm/last-update/minimize)](https://socket.dev/npm/package/minimize) | [html­com­pressor.­com](https://htmlcompressor.com/) |
237
237
  | --- | --- | --- | --- | --- | --- | --- | --- | --- |
238
238
  | [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
239
- | [Apple](https://www.apple.com/) | 260 | **203** | 204 | 231 | 235 | 236 | 238 | 238 |
240
- | [BBC](https://www.bbc.co.uk/) | 720 | **655** | 665 | 677 | 677 | 678 | 714 | n/a |
239
+ | [Apple](https://www.apple.com/) | 266 | **207** | **207** | 236 | 239 | 240 | 242 | 243 |
240
+ | [BBC](https://www.bbc.co.uk/) | 739 | **675** | 685 | 695 | 695 | 697 | 733 | n/a |
241
241
  | [CERN](https://home.cern/) | 156 | **87** | **87** | 95 | 95 | 95 | 97 | 100 |
242
- | [CSS-Tricks](https://css-tricks.com/) | 163 | 122 | **120** | 128 | 143 | 144 | 149 | 145 |
242
+ | [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 127 | 142 | 142 | 147 | 144 |
243
243
  | [ECMAScript](https://tc39.es/ecma262/) | 7238 | **6341** | **6341** | 6561 | 6444 | 6567 | 6614 | n/a |
244
244
  | [EDRi](https://edri.org/) | 80 | **59** | 60 | 70 | 70 | 71 | 75 | 73 |
245
- | [EFF](https://www.eff.org/) | 56 | **48** | **48** | 50 | 49 | 49 | 51 | 51 |
245
+ | [EFF](https://www.eff.org/) | 55 | **47** | **47** | 50 | 48 | 49 | 50 | 50 |
246
246
  | [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | **30** | 32 | 32 | 32 | 32 | 32 |
247
- | [FAZ](https://www.faz.net/aktuell/) | 1562 | 1455 | 1460 | **1400** | 1487 | 1498 | 1509 | n/a |
247
+ | [FAZ](https://www.faz.net/aktuell/) | 1543 | 1436 | 1441 | **1383** | 1468 | 1479 | 1490 | n/a |
248
248
  | [French Tech](https://lafrenchtech.gouv.fr/) | 152 | **121** | 122 | 125 | 125 | 125 | 132 | 126 |
249
- | [Frontend Dogma](https://frontenddogma.com/) | 222 | **213** | 215 | 236 | 221 | 222 | 241 | 222 |
249
+ | [Frontend Dogma](https://frontenddogma.com/) | 223 | **213** | 215 | 236 | 221 | 223 | 241 | 222 |
250
250
  | [Google](https://www.google.com/) | 18 | **17** | **17** | **17** | **17** | **17** | 18 | 18 |
251
- | [Ground News](https://ground.news/) | 2339 | **2052** | 2056 | 2151 | 2179 | 2182 | 2326 | n/a |
251
+ | [Ground News](https://ground.news/) | 1437 | **1222** | 1225 | 1320 | 1346 | 1353 | 1424 | n/a |
252
252
  | [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 149 |
253
- | [Igalia](https://www.igalia.com/) | 50 | **33** | **33** | 36 | 36 | 36 | 37 | 36 |
254
- | [Leanpub](https://leanpub.com/) | 1204 | **997** | **997** | 1004 | 1003 | 1001 | 1198 | n/a |
253
+ | [Igalia](https://www.igalia.com/) | 50 | **34** | **34** | 36 | 36 | 36 | 37 | 37 |
254
+ | [Leanpub](https://leanpub.com/) | 1288 | **1092** | **1092** | 1099 | 1097 | 1094 | 1283 | n/a |
255
255
  | [Mastodon](https://mastodon.social/explore) | 37 | **27** | **27** | 32 | 34 | 35 | 35 | 35 |
256
256
  | [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** | 196 | 202 | 200 | 200 | 202 | 203 |
258
- | [Nielsen Norman Group](https://www.nngroup.com/) | 84 | 71 | 71 | **53** | 71 | 73 | 74 | 73 |
257
+ | [Middle East Eye](https://www.middleeasteye.net/) | 223 | **196** | **196** | 203 | 201 | 200 | 202 | 203 |
258
+ | [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 74 | 74 | **55** | 74 | 75 | 77 | 76 |
259
259
  | [SitePoint](https://www.sitepoint.com/) | 485 | **354** | **354** | 425 | 459 | 464 | 481 | n/a |
260
260
  | [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 38 | 39 | 39 |
261
- | [TPGi](https://www.tpgi.com/) | 175 | **159** | 161 | 160 | 164 | 165 | 172 | 171 |
262
- | [United Nations](https://www.un.org/en/) | 151 | **112** | 114 | 121 | 125 | 124 | 130 | 123 |
263
- | [W3C](https://www.w3.org/) | 50 | **36** | **36** | 38 | 38 | 38 | 40 | 38 |
264
- | **Average processing time** | | 302 ms (26/26) | 356 ms (26/26) | 178 ms (26/26) | 59 ms (26/26) | **16 ms (26/26)** | 329 ms (26/26) | 1468 ms (20/26) |
261
+ | [TPGi](https://www.tpgi.com/) | 175 | **160** | 161 | **160** | 164 | 166 | 172 | 172 |
262
+ | [United Nations](https://www.un.org/en/) | 150 | **112** | 113 | 120 | 124 | 124 | 129 | 122 |
263
+ | [W3C](https://www.w3.org/) | 50 | **35** | 36 | 38 | 38 | 38 | 40 | 38 |
264
+ | **Average processing time** | | 306 ms (26/26) | 364 ms (26/26) | 162 ms (26/26) | 55 ms (26/26) | **16 ms (26/26)** | 316 ms (26/26) | 1439 ms (20/26) |
265
265
 
266
- (Last updated: Dec 16, 2025)
266
+ (Last updated: Dec 17, 2025)
267
267
  <!-- End auto-generated -->
268
268
 
269
269
  ## Examples
package/cli.js CHANGED
@@ -28,10 +28,12 @@
28
28
  import fs from 'fs';
29
29
  import path from 'path';
30
30
  import { pathToFileURL } from 'url';
31
+ import os from 'os';
31
32
  import { createRequire } from 'module';
32
33
  import { camelCase, paramCase } from 'change-case';
33
34
  import { Command } from 'commander';
34
- import { minify } from './src/htmlminifier.js';
35
+ // Lazy-load HMN to reduce CLI cold-start overhead
36
+ // import { minify } from './src/htmlminifier.js';
35
37
  import { getPreset, getPresetNames } from './src/presets.js';
36
38
 
37
39
  const require = createRequire(import.meta.url);
@@ -182,6 +184,15 @@ program.option('-o --output <file>', 'Specify output file (reads from file argum
182
184
  program.option('-v --verbose', 'Show detailed processing information');
183
185
  program.option('-d --dry', 'Dry run: process and report statistics without writing output');
184
186
 
187
+ // Lazy import wrapper for HMN
188
+ let minifyFnPromise;
189
+ async function getMinify() {
190
+ if (!minifyFnPromise) {
191
+ minifyFnPromise = import('./src/htmlminifier.js').then(m => m.minify);
192
+ }
193
+ return minifyFnPromise;
194
+ }
195
+
185
196
  function readFile(file) {
186
197
  try {
187
198
  return fs.readFileSync(file, { encoding: 'utf8' });
@@ -270,13 +281,30 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
270
281
  (async () => {
271
282
  let content;
272
283
  let filesProvided = false;
284
+ let capturedFiles = [];
273
285
  await program.arguments('[files...]').action(function (files) {
274
- content = files.map(readFile).join('');
286
+ capturedFiles = files;
275
287
  filesProvided = files.length > 0;
288
+ // Defer reading files until after we check for consumed filenames
276
289
  }).parseAsync(process.argv);
277
290
 
278
291
  const programOptions = program.opts();
279
292
 
293
+ // Check if any `parseJSON` options consumed a filename as their value
294
+ // If so, treat the option as boolean true and add the filename back to the files list
295
+ const jsonOptionKeys = ['minifyCss', 'minifyJs', 'minifyUrls'];
296
+ for (const key of jsonOptionKeys) {
297
+ const value = programOptions[key];
298
+ if (typeof value === 'string' && /\.(html?|php|xml|svg|xhtml|jsx|tsx|vue|ejs|hbs|mustache|twig)$/i.test(value)) {
299
+ // The option consumed a filename - inject it back
300
+ programOptions[key] = true;
301
+ capturedFiles.push(value);
302
+ filesProvided = true;
303
+ }
304
+ }
305
+
306
+ // Defer reading files—multi-file mode will process per-file later
307
+
280
308
  // Load and normalize config if `--config-file` was specified
281
309
  if (programOptions.configFile) {
282
310
  config = await loadConfigFromPath(programOptions.configFile);
@@ -344,6 +372,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
344
372
 
345
373
  let minified;
346
374
  try {
375
+ const minify = await getMinify();
347
376
  minified = await minify(data, createOptions());
348
377
  } catch (err) {
349
378
  fatal('Minification error on ' + inputFile + '\n' + err.message);
@@ -472,6 +501,54 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
472
501
  process.stderr.write('\r\x1b[K'); // Clear the line
473
502
  }
474
503
 
504
+ // Utility: concurrency runner
505
+ async function runWithConcurrency(items, limit, worker) {
506
+ const results = new Array(items.length);
507
+ let next = 0;
508
+ let active = 0;
509
+ return new Promise((resolve, reject) => {
510
+ const launch = () => {
511
+ while (active < limit && next < items.length) {
512
+ const current = next++;
513
+ active++;
514
+ Promise.resolve(worker(items[current], current))
515
+ .then((res) => {
516
+ results[current] = res;
517
+ active--;
518
+ launch();
519
+ })
520
+ .catch(reject);
521
+ }
522
+ if (next >= items.length && active === 0) {
523
+ resolve(results);
524
+ }
525
+ };
526
+ launch();
527
+ });
528
+ }
529
+
530
+ async function collectFiles(dir, extensions, skipRootAbs, ignorePatterns, baseDir) {
531
+ const out = [];
532
+ const entries = await fs.promises.readdir(dir).catch(() => []);
533
+ for (const name of entries) {
534
+ const filePath = path.join(dir, name);
535
+ if (skipRootAbs) {
536
+ const real = await fs.promises.realpath(filePath).catch(() => undefined);
537
+ if (real && (real === skipRootAbs || real.startsWith(skipRootAbs + path.sep))) continue;
538
+ }
539
+ const lst = await fs.promises.lstat(filePath).catch(() => null);
540
+ if (!lst || lst.isSymbolicLink()) continue;
541
+ if (lst.isDirectory()) {
542
+ if (shouldIgnoreDirectory(filePath, ignorePatterns, baseDir)) continue;
543
+ const sub = await collectFiles(filePath, extensions, skipRootAbs, ignorePatterns, baseDir);
544
+ out.push(...sub);
545
+ } else if (shouldProcessFile(name, extensions)) {
546
+ out.push(filePath);
547
+ }
548
+ }
549
+ return out;
550
+ }
551
+
475
552
  async function processDirectory(inputDir, outputDir, extensions, isDryRun = false, isVerbose = false, skipRootAbs, progress = null, ignorePatterns = [], baseDir = null) {
476
553
  // If first call provided a string, normalize once; otherwise assume pre-parsed array
477
554
  if (typeof extensions === 'string') {
@@ -483,61 +560,27 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
483
560
  baseDir = inputDir;
484
561
  }
485
562
 
486
- const files = await fs.promises.readdir(inputDir).catch(err => {
487
- fatal('Cannot read directory ' + inputDir + '\n' + err.message);
488
- });
489
-
490
- const allStats = [];
491
-
492
- for (const file of files) {
493
- const inputFile = path.join(inputDir, file);
494
- const outputFile = path.join(outputDir, file);
495
-
496
- // Skip anything inside the output root to avoid reprocessing
497
- if (skipRootAbs) {
498
- const real = await fs.promises.realpath(inputFile).catch(() => undefined);
499
- if (real && (real === skipRootAbs || real.startsWith(skipRootAbs + path.sep))) {
500
- continue;
501
- }
502
- }
503
-
504
- const lst = await fs.promises.lstat(inputFile).catch(err => {
505
- fatal('Cannot read ' + inputFile + '\n' + err.message);
506
- });
507
-
508
- if (lst.isSymbolicLink()) {
509
- continue;
563
+ // Collect all files first for bounded parallel processing
564
+ const list = await collectFiles(inputDir, extensions, skipRootAbs, ignorePatterns, baseDir);
565
+ const allStats = new Array(list.length);
566
+ const concurrency = Math.max(1, Math.min(os.cpus().length || 4, 8));
567
+ await runWithConcurrency(list, concurrency, async (inputFile, idx) => {
568
+ const rel = path.relative(inputDir, inputFile);
569
+ const outFile = path.join(outputDir, rel);
570
+ const outDir = path.dirname(outFile);
571
+ if (!isDryRun) {
572
+ await fs.promises.mkdir(outDir, { recursive: true }).catch(err => {
573
+ fatal('Cannot create directory ' + outDir + '\n' + err.message);
574
+ });
510
575
  }
511
-
512
- if (lst.isDirectory()) {
513
- // Skip ignored directories
514
- if (shouldIgnoreDirectory(inputFile, ignorePatterns, baseDir)) {
515
- continue;
516
- }
517
- const dirStats = await processDirectory(inputFile, outputFile, extensions, isDryRun, isVerbose, skipRootAbs, progress, ignorePatterns, baseDir);
518
- if (dirStats) {
519
- allStats.push(...dirStats);
520
- }
521
- } else if (shouldProcessFile(file, extensions)) {
522
- if (!isDryRun) {
523
- await fs.promises.mkdir(outputDir, { recursive: true }).catch(err => {
524
- fatal('Cannot create directory ' + outputDir + '\n' + err.message);
525
- });
526
- }
527
- const fileStats = await processFile(inputFile, outputFile, isDryRun, isVerbose);
528
- if (fileStats) {
529
- allStats.push(fileStats);
530
- }
531
-
532
- // Update progress after processing
533
- if (progress) {
534
- progress.current++;
535
- updateProgress(progress.current, progress.total);
536
- }
576
+ const stats = await processFile(inputFile, outFile, isDryRun, isVerbose);
577
+ allStats[idx] = stats;
578
+ if (progress) {
579
+ progress.current++;
580
+ updateProgress(progress.current, progress.total);
537
581
  }
538
- }
539
-
540
- return allStats;
582
+ });
583
+ return allStats.filter(Boolean);
541
584
  }
542
585
 
543
586
  const writeMinify = async () => {
@@ -551,6 +594,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
551
594
  let minified;
552
595
 
553
596
  try {
597
+ const minify = await getMinify();
554
598
  minified = await minify(content, minifierOptions);
555
599
  } catch (err) {
556
600
  fatal('Minification error:\n' + err.message);
@@ -576,17 +620,12 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
576
620
  }
577
621
 
578
622
  if (programOptions.output) {
579
- await fs.promises.mkdir(path.dirname(programOptions.output), { recursive: true }).catch((e) => {
580
- fatal('Cannot create directory ' + path.dirname(programOptions.output) + '\n' + e.message);
581
- });
582
- await new Promise((resolve, reject) => {
583
- const fileStream = fs.createWriteStream(programOptions.output)
584
- .on('error', reject)
585
- .on('finish', resolve);
586
- fileStream.end(minified);
587
- }).catch((e) => {
588
- fatal('Cannot write ' + programOptions.output + '\n' + e.message);
589
- });
623
+ try {
624
+ await fs.promises.mkdir(path.dirname(programOptions.output), { recursive: true });
625
+ await fs.promises.writeFile(programOptions.output, minified, { encoding: 'utf8' });
626
+ } catch (err) {
627
+ fatal('Cannot write ' + programOptions.output + '\n' + err.message);
628
+ }
590
629
  return;
591
630
  }
592
631
 
@@ -646,6 +685,16 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
646
685
  // Parse ignore patterns
647
686
  const ignorePatterns = parseIgnorePatterns(resolvedIgnoreDir);
648
687
 
688
+ // Validate that the input directory exists and is readable
689
+ try {
690
+ const stat = await fs.promises.stat(inputDir);
691
+ if (!stat.isDirectory()) {
692
+ fatal(inputDir + ' is not a directory');
693
+ }
694
+ } catch (err) {
695
+ fatal('Cannot read directory ' + inputDir + '\n' + err.message);
696
+ }
697
+
649
698
  // Resolve base directory for consistent path comparisons
650
699
  const inputDirResolved = await fs.promises.realpath(inputDir).catch(() => inputDir);
651
700
 
@@ -688,12 +737,72 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
688
737
  }
689
738
  })();
690
739
  } else if (filesProvided) { // Minifying one or more files specified on the CMD line
691
- writeMinify();
740
+ // Process each file independently, then concatenate outputs to preserve current behavior
741
+ const minifierOptions = createOptions();
742
+ // Show config info if verbose/dry
743
+ if (programOptions.verbose || programOptions.dry) {
744
+ getActiveOptionsDisplay(minifierOptions);
745
+ }
746
+
747
+ const concurrency = Math.max(1, Math.min(os.cpus().length || 4, 8));
748
+ const inputs = capturedFiles.slice();
749
+
750
+ // Read originals and minify in parallel with bounded concurrency
751
+ const originals = new Array(inputs.length);
752
+ const outputs = new Array(inputs.length);
753
+
754
+ await runWithConcurrency(inputs, concurrency, async (file, idx) => {
755
+ const data = await fs.promises.readFile(file, 'utf8').catch(err => fatal('Cannot read ' + file + '\n' + err.message));
756
+ const minify = await getMinify();
757
+ let out;
758
+ try {
759
+ out = await minify(data, minifierOptions);
760
+ } catch (err) {
761
+ fatal('Minification error on ' + file + '\n' + err.message);
762
+ }
763
+ originals[idx] = data;
764
+ outputs[idx] = out;
765
+ });
766
+
767
+ const originalCombined = originals.join('');
768
+ const minifiedCombined = outputs.join('');
769
+
770
+ const stats = calculateStats(originalCombined, minifiedCombined);
771
+
772
+ if (programOptions.dry) {
773
+ const inputSource = capturedFiles.join(', ');
774
+ const outputDest = programOptions.output || 'STDOUT';
775
+ console.error(`[DRY RUN] Would minify: ${inputSource} → ${outputDest}`);
776
+ console.error(` Original: ${stats.originalSize.toLocaleString()} bytes`);
777
+ console.error(` Minified: ${stats.minifiedSize.toLocaleString()} bytes`);
778
+ console.error(` Saved: ${stats.sign}${Math.abs(stats.saved).toLocaleString()} bytes (${stats.percentage}%)`);
779
+ process.exit(0);
780
+ }
781
+
782
+ if (programOptions.verbose) {
783
+ const inputSource = capturedFiles.join(', ');
784
+ console.error(` ✓ ${inputSource}: ${stats.originalSize.toLocaleString()} → ${stats.minifiedSize.toLocaleString()} bytes (${stats.sign}${Math.abs(stats.saved).toLocaleString()}, ${stats.percentage}%)`);
785
+ }
786
+
787
+ if (programOptions.output) {
788
+ try {
789
+ await fs.promises.mkdir(path.dirname(programOptions.output), { recursive: true });
790
+ await fs.promises.writeFile(programOptions.output, minifiedCombined, 'utf8');
791
+ } catch (err) {
792
+ fatal('Cannot write ' + programOptions.output + '\n' + err.message);
793
+ }
794
+ } else {
795
+ process.stdout.write(minifiedCombined);
796
+ }
797
+ process.exit(0);
692
798
  } else { // Minifying input coming from STDIN
693
799
  content = '';
694
800
  process.stdin.setEncoding('utf8');
695
801
  process.stdin.on('data', function (data) {
696
802
  content += data;
697
- }).on('end', writeMinify);
803
+ }).on('end', async function() {
804
+ await writeMinify();
805
+ process.exit(0);
806
+ });
698
807
  }
699
808
  })();