html-minifier-next 3.0.0 → 3.1.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 +19 -3
  2. package/cli.js +143 -52
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -34,10 +34,10 @@ html-minifier-next --collapse-whitespace --remove-comments --minify-js true --in
34
34
  **Process specific file extensions:**
35
35
 
36
36
  ```bash
37
- # Process only HTML files (CLI method)
37
+ # Process only HTML files
38
38
  html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html
39
39
 
40
- # Process multiple file extensions (CLI method)
40
+ # Process multiple file extensions
41
41
  html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --file-ext=html,htm,php
42
42
 
43
43
  # Using configuration file that sets `fileExt` (e.g., `"fileExt": "html,htm"`)
@@ -49,6 +49,21 @@ html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
49
49
  # Consider restricting with “--file-ext” to avoid touching binaries (e.g., images, archives).
50
50
  ```
51
51
 
52
+ **Dry run mode (preview outcome without writing files):**
53
+
54
+ ```bash
55
+ # Preview with output file
56
+ html-minifier-next input.html -o output.html --dry --collapse-whitespace
57
+
58
+ # Preview directory processing with statistics per file and total
59
+ html-minifier-next --input-dir=src --output-dir=dist --dry --collapse-whitespace
60
+ # Output: [DRY RUN] Would process directory: src → dist
61
+ # index.html: 1,234 → 892 bytes (-342, 27.7%)
62
+ # about.html: 2,100 → 1,654 bytes (-446, 21.2%)
63
+ # ---
64
+ # Total: 3,334 → 2,546 bytes (-788, 23.6%)
65
+ ```
66
+
52
67
  ### CLI options
53
68
 
54
69
  Use `html-minifier-next --help` to check all available options:
@@ -58,8 +73,9 @@ Use `html-minifier-next --help` to check all available options:
58
73
  | `--input-dir <dir>` | Specify an input directory | `--input-dir=src` |
59
74
  | `--output-dir <dir>` | Specify an output directory | `--output-dir=dist` |
60
75
  | `--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"` |
61
- | `-o --output <file>` | Specify output file (single file mode) | `-o minified.html` |
76
+ | `-o --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` |
62
77
  | `-c --config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
78
+ | `-d --dry` | Dry run: Process and report statistics without writing output | `html-minifier-next input.html --dry --collapse-whitespace` |
63
79
 
64
80
  ### Configuration file
65
81
 
package/cli.js CHANGED
@@ -44,6 +44,14 @@ function fatal(message) {
44
44
  process.exit(1);
45
45
  }
46
46
 
47
+ // Handle broken pipe (e.g., when piping to `head`)
48
+ process.stdout.on('error', (err) => {
49
+ if (err && err.code === 'EPIPE') {
50
+ process.exit(0);
51
+ }
52
+ fatal('STDOUT error\n' + (err && err.message ? err.message : String(err)));
53
+ });
54
+
47
55
  /**
48
56
  * JSON does not support regexes, so, e.g., JSON.parse() will not create
49
57
  * a RegExp from the JSON value `[ "/matchString/" ]`, which is
@@ -60,7 +68,7 @@ function fatal(message) {
60
68
  * search string, the user would need to enclose the expression in a
61
69
  * second set of slashes:
62
70
  *
63
- * --customAttrSrround "[\"//matchString//\"]"
71
+ * --customAttrSurround "[\"//matchString//\"]"
64
72
  */
65
73
  function parseRegExp(value) {
66
74
  if (value) {
@@ -154,7 +162,8 @@ mainOptionKeys.forEach(function (key) {
154
162
  program.option('--' + paramCase(key), option);
155
163
  }
156
164
  });
157
- program.option('-o --output <file>', 'Specify output file (if not specified STDOUT will be used for output)');
165
+ program.option('-o --output <file>', 'Specify output file (reads from file arguments or STDIN; outputs to STDOUT if not specified)');
166
+ program.option('-d --dry', 'Dry run: process and report statistics without writing output');
158
167
 
159
168
  function readFile(file) {
160
169
  try {
@@ -220,32 +229,35 @@ function createOptions() {
220
229
  return options;
221
230
  }
222
231
 
223
- function mkdir(outputDir, callback) {
224
- fs.mkdir(outputDir, { recursive: true }, function (err) {
225
- if (err) {
226
- fatal('Cannot create directory ' + outputDir + '\n' + err.message);
227
- }
228
- callback();
232
+ async function processFile(inputFile, outputFile, isDryRun = false) {
233
+ const data = await fs.promises.readFile(inputFile, { encoding: 'utf8' }).catch(err => {
234
+ fatal('Cannot read ' + inputFile + '\n' + err.message);
229
235
  });
230
- }
231
236
 
232
- function processFile(inputFile, outputFile) {
233
- fs.readFile(inputFile, { encoding: 'utf8' }, async function (err, data) {
234
- if (err) {
235
- fatal('Cannot read ' + inputFile + '\n' + err.message);
236
- }
237
- let minified;
238
- try {
239
- minified = await minify(data, createOptions());
240
- } catch (e) {
241
- fatal('Minification error on ' + inputFile + '\n' + e.message);
242
- }
243
- fs.writeFile(outputFile, minified, { encoding: 'utf8' }, function (err) {
244
- if (err) {
245
- fatal('Cannot write ' + outputFile + '\n' + err.message);
246
- }
247
- });
237
+ let minified;
238
+ try {
239
+ minified = await minify(data, createOptions());
240
+ } catch (e) {
241
+ fatal('Minification error on ' + inputFile + '\n' + e.message);
242
+ }
243
+
244
+ if (isDryRun) {
245
+ const originalSize = Buffer.byteLength(data, 'utf8');
246
+ const minifiedSize = Buffer.byteLength(minified, 'utf8');
247
+ const saved = originalSize - minifiedSize;
248
+ const sign = saved >= 0 ? '-' : '+';
249
+ const percentage = originalSize ? ((Math.abs(saved) / originalSize) * 100).toFixed(1) : '0.0';
250
+
251
+ console.error(` ${path.relative(process.cwd(), inputFile)}: ${originalSize.toLocaleString()} → ${minifiedSize.toLocaleString()} bytes (${sign}${Math.abs(saved).toLocaleString()}, ${percentage}%)`);
252
+
253
+ return { originalSize, minifiedSize, saved };
254
+ }
255
+
256
+ await fs.promises.writeFile(outputFile, minified, { encoding: 'utf8' }).catch(err => {
257
+ fatal('Cannot write ' + outputFile + '\n' + err.message);
248
258
  });
259
+
260
+ return null;
249
261
  }
250
262
 
251
263
  function parseFileExtensions(fileExt) {
@@ -266,34 +278,57 @@ function shouldProcessFile(filename, fileExtensions) {
266
278
  return fileExtensions.includes(fileExt);
267
279
  }
268
280
 
269
- function processDirectory(inputDir, outputDir, extensions) {
281
+ async function processDirectory(inputDir, outputDir, extensions, isDryRun = false, skipRootAbs) {
270
282
  // If first call provided a string, normalize once; otherwise assume pre-parsed array
271
283
  if (typeof extensions === 'string') {
272
284
  extensions = parseFileExtensions(extensions);
273
285
  }
274
286
 
275
- fs.readdir(inputDir, function (err, files) {
276
- if (err) {
277
- fatal('Cannot read directory ' + inputDir + '\n' + err.message);
287
+ const files = await fs.promises.readdir(inputDir).catch(err => {
288
+ fatal('Cannot read directory ' + inputDir + '\n' + err.message);
289
+ });
290
+
291
+ const allStats = [];
292
+
293
+ for (const file of files) {
294
+ const inputFile = path.join(inputDir, file);
295
+ const outputFile = path.join(outputDir, file);
296
+
297
+ // Skip anything inside the output root to avoid reprocessing
298
+ if (skipRootAbs) {
299
+ const real = await fs.promises.realpath(inputFile).catch(() => undefined);
300
+ if (real && (real === skipRootAbs || real.startsWith(skipRootAbs + path.sep))) {
301
+ continue;
302
+ }
278
303
  }
279
304
 
280
- files.forEach(function (file) {
281
- const inputFile = path.join(inputDir, file);
282
- const outputFile = path.join(outputDir, file);
283
-
284
- fs.stat(inputFile, function (err, stat) {
285
- if (err) {
286
- fatal('Cannot read ' + inputFile + '\n' + err.message);
287
- } else if (stat.isDirectory()) {
288
- processDirectory(inputFile, outputFile, extensions);
289
- } else if (shouldProcessFile(file, extensions)) {
290
- mkdir(outputDir, function () {
291
- processFile(inputFile, outputFile);
292
- });
293
- }
294
- });
305
+ const lst = await fs.promises.lstat(inputFile).catch(err => {
306
+ fatal('Cannot read ' + inputFile + '\n' + err.message);
295
307
  });
296
- });
308
+
309
+ if (lst.isSymbolicLink()) {
310
+ continue;
311
+ }
312
+
313
+ if (lst.isDirectory()) {
314
+ const dirStats = await processDirectory(inputFile, outputFile, extensions, isDryRun, skipRootAbs);
315
+ if (dirStats) {
316
+ allStats.push(...dirStats);
317
+ }
318
+ } else if (shouldProcessFile(file, extensions)) {
319
+ if (!isDryRun) {
320
+ await fs.promises.mkdir(outputDir, { recursive: true }).catch(err => {
321
+ fatal('Cannot create directory ' + outputDir + '\n' + err.message);
322
+ });
323
+ }
324
+ const fileStats = await processFile(inputFile, outputFile, isDryRun);
325
+ if (fileStats) {
326
+ allStats.push(fileStats);
327
+ }
328
+ }
329
+ }
330
+
331
+ return allStats;
297
332
  }
298
333
 
299
334
  const writeMinify = async () => {
@@ -306,16 +341,39 @@ const writeMinify = async () => {
306
341
  fatal('Minification error:\n' + e.message);
307
342
  }
308
343
 
309
- let stream = process.stdout;
344
+ if (programOptions.dry) {
345
+ const originalSize = Buffer.byteLength(content, 'utf8');
346
+ const minifiedSize = Buffer.byteLength(minified, 'utf8');
347
+ const saved = originalSize - minifiedSize;
348
+ const sign = saved >= 0 ? '-' : '+';
349
+ const percentage = originalSize ? ((Math.abs(saved) / originalSize) * 100).toFixed(1) : '0.0';
350
+
351
+ const inputSource = program.args.length > 0 ? program.args.join(', ') : 'STDIN';
352
+ const outputDest = programOptions.output || 'STDOUT';
353
+
354
+ console.error(`[DRY RUN] Would minify: ${inputSource} → ${outputDest}`);
355
+ console.error(` Original: ${originalSize.toLocaleString()} bytes`);
356
+ console.error(` Minified: ${minifiedSize.toLocaleString()} bytes`);
357
+ console.error(` Saved: ${sign}${Math.abs(saved).toLocaleString()} bytes (${percentage}%)`);
358
+ return;
359
+ }
310
360
 
311
361
  if (programOptions.output) {
312
- stream = fs.createWriteStream(programOptions.output)
313
- .on('error', (e) => {
314
- fatal('Cannot write ' + programOptions.output + '\n' + e.message);
315
- });
362
+ await fs.promises.mkdir(path.dirname(programOptions.output), { recursive: true }).catch((e) => {
363
+ fatal('Cannot create directory ' + path.dirname(programOptions.output) + '\n' + e.message);
364
+ });
365
+ await new Promise((resolve, reject) => {
366
+ const fileStream = fs.createWriteStream(programOptions.output)
367
+ .on('error', reject)
368
+ .on('finish', resolve);
369
+ fileStream.end(minified);
370
+ }).catch((e) => {
371
+ fatal('Cannot write ' + programOptions.output + '\n' + e.message);
372
+ });
373
+ return;
316
374
  }
317
375
 
318
- stream.write(minified);
376
+ process.stdout.write(minified);
319
377
  };
320
378
 
321
379
  const { inputDir, outputDir, fileExt } = programOptions;
@@ -330,7 +388,40 @@ if (inputDir || outputDir) {
330
388
  } else if (!outputDir) {
331
389
  fatal('You need to specify where to write the output files with the option --output-dir');
332
390
  }
333
- processDirectory(inputDir, outputDir, resolvedFileExt);
391
+
392
+ (async () => {
393
+ // Prevent traversing into the output directory when it is inside the input directory
394
+ let inputReal;
395
+ let outputReal;
396
+ inputReal = await fs.promises.realpath(inputDir).catch(() => undefined);
397
+ try {
398
+ outputReal = await fs.promises.realpath(outputDir);
399
+ } catch {
400
+ outputReal = path.resolve(outputDir);
401
+ }
402
+ let skipRootAbs;
403
+ if (inputReal && outputReal && (outputReal === inputReal || outputReal.startsWith(inputReal + path.sep))) {
404
+ // Instead of aborting, skip traversing into the output directory
405
+ skipRootAbs = outputReal;
406
+ }
407
+
408
+ if (programOptions.dry) {
409
+ console.error(`[DRY RUN] Would process directory: ${inputDir} → ${outputDir}`);
410
+ }
411
+
412
+ const stats = await processDirectory(inputDir, outputDir, resolvedFileExt, programOptions.dry, skipRootAbs);
413
+
414
+ if (programOptions.dry) {
415
+ const totalOriginal = stats.reduce((sum, s) => sum + s.originalSize, 0);
416
+ const totalMinified = stats.reduce((sum, s) => sum + s.minifiedSize, 0);
417
+ const totalSaved = totalOriginal - totalMinified;
418
+ const sign = totalSaved >= 0 ? '-' : '+';
419
+ const totalPercentage = totalOriginal ? ((Math.abs(totalSaved) / totalOriginal) * 100).toFixed(1) : '0.0';
420
+
421
+ console.error('---');
422
+ console.error(`Total: ${totalOriginal.toLocaleString()} → ${totalMinified.toLocaleString()} bytes (${sign}${Math.abs(totalSaved).toLocaleString()}, ${totalPercentage}%)`);
423
+ }
424
+ })();
334
425
  } else if (content) { // Minifying one or more files specified on the CMD line
335
426
  writeMinify();
336
427
  } else { // Minifying input coming from STDIN
package/package.json CHANGED
@@ -15,15 +15,15 @@
15
15
  "description": "Highly configurable, well-tested, JavaScript-based HTML minifier",
16
16
  "devDependencies": {
17
17
  "@commitlint/cli": "^20.1.0",
18
- "@eslint/js": "^9.36.0",
19
- "@rollup/plugin-commonjs": "^28.0.6",
18
+ "@eslint/js": "^9.37.0",
19
+ "@rollup/plugin-commonjs": "^28.0.8",
20
20
  "@rollup/plugin-json": "^6.1.0",
21
- "@rollup/plugin-node-resolve": "^16.0.2",
21
+ "@rollup/plugin-node-resolve": "^16.0.3",
22
22
  "@rollup/plugin-terser": "^0.4.4",
23
- "eslint": "^9.36.0",
23
+ "eslint": "^9.37.0",
24
24
  "rollup": "^4.52.4",
25
25
  "rollup-plugin-polyfill-node": "^0.13.0",
26
- "vite": "^7.1.9"
26
+ "vite": "^7.1.12"
27
27
  },
28
28
  "exports": {
29
29
  ".": {
@@ -78,5 +78,5 @@
78
78
  "test:watch": "node --test --watch tests/*.spec.js"
79
79
  },
80
80
  "type": "module",
81
- "version": "3.0.0"
81
+ "version": "3.1.0"
82
82
  }