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.
- package/README.md +19 -3
- package/cli.js +143 -52
- 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
|
|
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
|
|
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 (
|
|
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
|
-
* --
|
|
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 (
|
|
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
|
|
224
|
-
fs.
|
|
225
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
19
|
-
"@rollup/plugin-commonjs": "^28.0.
|
|
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.
|
|
21
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
22
22
|
"@rollup/plugin-terser": "^0.4.4",
|
|
23
|
-
"eslint": "^9.
|
|
23
|
+
"eslint": "^9.37.0",
|
|
24
24
|
"rollup": "^4.52.4",
|
|
25
25
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
26
|
-
"vite": "^7.1.
|
|
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.
|
|
81
|
+
"version": "3.1.0"
|
|
82
82
|
}
|