html-minifier-next 4.1.1 → 4.2.2

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  HTML Minifier Next (HMN) is a highly **configurable, well-tested, JavaScript-based HTML minifier**.
6
6
 
7
- The project has been based on [Terser’s html-minifier-terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy Zaytsev’s html-minifier](https://github.com/kangax/html-minifier) (HMN offers additional features, but is compatible with both). It was set up because as of 2025, both html-minifier-terser and html-minifier have been unmaintained for some time. As the project seems maintainable [to me, [Jens](https://meiert.com/)]—even more so with community support—, it’s being updated and documented further in this place.
7
+ The project has been based on [HTML Minifier Terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy Zaytsev’s HTML Minifier](https://github.com/kangax/html-minifier) (HMN offers additional features, but is compatible with both). It was set up because as of 2025, both HTML Minifier Terser and HTML Minifier have been unmaintained for some time. As the project seems maintainable [to me, [Jens](https://meiert.com/)]—even more so with community support—, it’s being updated and documented further in this place.
8
8
 
9
9
  ## Installation
10
10
 
package/cli.js CHANGED
@@ -180,8 +180,8 @@ program.option('-d --dry', 'Dry run: process and report statistics without writi
180
180
  function readFile(file) {
181
181
  try {
182
182
  return fs.readFileSync(file, { encoding: 'utf8' });
183
- } catch (e) {
184
- fatal('Cannot read ' + file + '\n' + e.message);
183
+ } catch (err) {
184
+ fatal('Cannot read ' + file + '\n' + err.message);
185
185
  }
186
186
  }
187
187
 
@@ -196,7 +196,7 @@ async function loadConfigFromPath(configPath) {
196
196
  // Try JSON first
197
197
  try {
198
198
  return JSON.parse(data);
199
- } catch (je) {
199
+ } catch (jsonErr) {
200
200
  const abs = path.resolve(configPath);
201
201
 
202
202
  // Try CJS require
@@ -204,13 +204,13 @@ async function loadConfigFromPath(configPath) {
204
204
  const result = require(abs);
205
205
  // Handle ESM interop: if `require()` loads an ESM file, it may return `{__esModule: true, default: …}`
206
206
  return (result && result.__esModule && result.default) ? result.default : result;
207
- } catch (ne) {
207
+ } catch (cjsErr) {
208
208
  // Try ESM import
209
209
  try {
210
210
  const mod = await import(pathToFileURL(abs).href);
211
211
  return mod.default || mod;
212
- } catch (ee) {
213
- fatal('Cannot read the specified config file.\nAs JSON: ' + je.message + '\nAs CJS: ' + ne.message + '\nAs ESM: ' + ee.message);
212
+ } catch (esmErr) {
213
+ fatal('Cannot read the specified config file.\nAs JSON: ' + jsonErr.message + '\nAs CJS: ' + cjsErr.message + '\nAs ESM: ' + esmErr.message);
214
214
  }
215
215
  }
216
216
  }
@@ -307,8 +307,8 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
307
307
  let minified;
308
308
  try {
309
309
  minified = await minify(data, createOptions());
310
- } catch (e) {
311
- fatal('Minification error on ' + inputFile + '\n' + e.message);
310
+ } catch (err) {
311
+ fatal('Minification error on ' + inputFile + '\n' + err.message);
312
312
  }
313
313
 
314
314
  const stats = calculateStats(data, minified);
@@ -466,8 +466,8 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
466
466
 
467
467
  try {
468
468
  minified = await minify(content, minifierOptions);
469
- } catch (e) {
470
- fatal('Minification error:\n' + e.message);
469
+ } catch (err) {
470
+ fatal('Minification error:\n' + err.message);
471
471
  }
472
472
 
473
473
  const stats = calculateStats(content, minified);
@@ -1328,8 +1328,8 @@ const processOptions = (inputOptions) => {
1328
1328
  try {
1329
1329
  const result = await terser.minify(code, terserOptions);
1330
1330
  return result.code.replace(/;$/, '');
1331
- } catch (error) {
1332
- options.log(error);
1331
+ } catch (err) {
1332
+ options.log(err);
1333
1333
  return text;
1334
1334
  }
1335
1335
  };
@@ -22332,8 +22332,8 @@ function remove_initializers(var_statement) {
22332
22332
  return decls.length ? make_node(AST_Var, var_statement, { definitions: decls }) : null;
22333
22333
  }
22334
22334
 
22335
- /** Called on code which we know is unreachable, to keep elements that affect outside of it. */
22336
- function trim_unreachable_code(compressor, stat, target) {
22335
+ /** Called on code which won't be executed but has an effect outside of itself: `var`, `function` statements, `export`, `import`. */
22336
+ function extract_from_unreachable_code(compressor, stat, target) {
22337
22337
  walk(stat, node => {
22338
22338
  if (node instanceof AST_Var) {
22339
22339
  const no_initializers = remove_initializers(node);
@@ -22358,7 +22358,8 @@ function trim_unreachable_code(compressor, stat, target) {
22358
22358
  target.push(node);
22359
22359
  return true;
22360
22360
  }
22361
- if (node instanceof AST_Scope) {
22361
+ if (node instanceof AST_Scope || node instanceof AST_Class) {
22362
+ // Do not go into nested scopes
22362
22363
  return true;
22363
22364
  }
22364
22365
  });
@@ -23382,7 +23383,7 @@ function tighten_body(statements, compressor) {
23382
23383
  CHANGED = n != len;
23383
23384
  if (has_quit)
23384
23385
  has_quit.forEach(function (stat) {
23385
- trim_unreachable_code(compressor, stat, statements);
23386
+ extract_from_unreachable_code(compressor, stat, statements);
23386
23387
  });
23387
23388
  }
23388
23389
 
@@ -24565,6 +24566,11 @@ class Compressor extends TreeWalker {
24565
24566
  }
24566
24567
  }
24567
24568
 
24569
+ /** True if compressor.self()'s result will be turned into a 32-bit integer.
24570
+ * ex:
24571
+ * ~{expr}
24572
+ * (1, 2, {expr}) | 0
24573
+ **/
24568
24574
  in_32_bit_context(other_operand_must_be_number) {
24569
24575
  if (!this.option("evaluate")) return false;
24570
24576
  var self = this.self();
@@ -24582,9 +24588,10 @@ class Compressor extends TreeWalker {
24582
24588
  if (
24583
24589
  p instanceof AST_Binary
24584
24590
  && (
24585
- p.operator == "&&"
24586
- || p.operator == "||"
24587
- || p.operator == "??"
24591
+ // Don't talk about p.left. Can change branch taken
24592
+ p.operator == "&&" && p.right === self
24593
+ || p.operator == "||" && p.right === self
24594
+ || p.operator == "??" && p.right === self
24588
24595
  )
24589
24596
  || p instanceof AST_Conditional && p.condition !== self
24590
24597
  || p.tail_node() === self
@@ -25187,7 +25194,7 @@ function if_break_in_loop(self, compressor) {
25187
25194
  body: self.condition
25188
25195
  }));
25189
25196
  }
25190
- trim_unreachable_code(compressor, self.body, body);
25197
+ extract_from_unreachable_code(compressor, self.body, body);
25191
25198
  return make_node(AST_BlockStatement, self, {
25192
25199
  body: body
25193
25200
  });
@@ -25258,7 +25265,7 @@ def_optimize(AST_For, function(self, compressor) {
25258
25265
  if (cond instanceof AST_Node) cond = self.condition.tail_node().evaluate(compressor);
25259
25266
  if (!cond) {
25260
25267
  var body = [];
25261
- trim_unreachable_code(compressor, self.body, body);
25268
+ extract_from_unreachable_code(compressor, self.body, body);
25262
25269
  if (self.init instanceof AST_Statement) {
25263
25270
  body.push(self.init);
25264
25271
  } else if (self.init) {
@@ -25294,7 +25301,7 @@ def_optimize(AST_If, function(self, compressor) {
25294
25301
  if (cond instanceof AST_Node) cond = self.condition.tail_node().evaluate(compressor);
25295
25302
  if (!cond) {
25296
25303
  var body = [];
25297
- trim_unreachable_code(compressor, self.body, body);
25304
+ extract_from_unreachable_code(compressor, self.body, body);
25298
25305
  body.push(make_node(AST_SimpleStatement, self.condition, {
25299
25306
  body: self.condition
25300
25307
  }));
@@ -25307,7 +25314,7 @@ def_optimize(AST_If, function(self, compressor) {
25307
25314
  }));
25308
25315
  body.push(self.body);
25309
25316
  if (self.alternative) {
25310
- trim_unreachable_code(compressor, self.alternative, body);
25317
+ extract_from_unreachable_code(compressor, self.alternative, body);
25311
25318
  }
25312
25319
  return make_node(AST_BlockStatement, self, { body: body }).optimize(compressor);
25313
25320
  }
@@ -25435,6 +25442,9 @@ def_optimize(AST_Switch, function(self, compressor) {
25435
25442
  var body = [];
25436
25443
  var default_branch;
25437
25444
  var exact_match;
25445
+ // - compress self.body into `body`
25446
+ // - find and deduplicate default branch
25447
+ // - find the exact match (`case 1234` inside `switch(1234)`)
25438
25448
  for (var i = 0, len = self.body.length; i < len && !exact_match; i++) {
25439
25449
  branch = self.body[i];
25440
25450
  if (branch instanceof AST_Default) {
@@ -25464,6 +25474,7 @@ def_optimize(AST_Switch, function(self, compressor) {
25464
25474
  }
25465
25475
  body.push(branch);
25466
25476
  }
25477
+ // i < len if we found an exact_match. eliminate the rest
25467
25478
  while (i < len) eliminate_branch(self.body[i++], body[body.length - 1]);
25468
25479
  self.body = body;
25469
25480
 
@@ -25535,7 +25546,7 @@ def_optimize(AST_Switch, function(self, compressor) {
25535
25546
  let i = body.length - 1;
25536
25547
  for (; i >= 0; i--) {
25537
25548
  let bbody = body[i].body;
25538
- if (is_break(bbody[bbody.length - 1], compressor)) bbody.pop();
25549
+ while (is_break(bbody[bbody.length - 1], compressor)) bbody.pop();
25539
25550
  if (!is_inert_body(body[i])) break;
25540
25551
  }
25541
25552
  // i now points to the index of a branch that contains a body. By incrementing, it's
@@ -25551,9 +25562,9 @@ def_optimize(AST_Switch, function(self, compressor) {
25551
25562
  let branch = body[j];
25552
25563
  if (branch === default_or_exact) {
25553
25564
  default_or_exact = null;
25554
- body.pop();
25565
+ eliminate_branch(body.pop());
25555
25566
  } else if (!branch.expression.has_side_effects(compressor)) {
25556
- body.pop();
25567
+ eliminate_branch(body.pop());
25557
25568
  } else {
25558
25569
  break;
25559
25570
  }
@@ -25667,15 +25678,16 @@ def_optimize(AST_Switch, function(self, compressor) {
25667
25678
  // and there's a side-effect somewhere. Just let the below paths take care of it.
25668
25679
  }
25669
25680
 
25681
+ // Reintegrate `decl` (var statements)
25670
25682
  if (body.length > 0) {
25671
25683
  body[0].body = decl.concat(body[0].body);
25672
25684
  }
25673
-
25674
25685
  if (body.length == 0) {
25675
25686
  return make_node(AST_BlockStatement, self, {
25676
25687
  body: decl.concat(statement(self.expression))
25677
25688
  }).optimize(compressor);
25678
25689
  }
25690
+
25679
25691
  if (body.length == 1 && !has_nested_break(self)) {
25680
25692
  // This is the last case body, and we've already pruned any breaks, so it's
25681
25693
  // safe to hoist.
@@ -25755,7 +25767,7 @@ def_optimize(AST_Switch, function(self, compressor) {
25755
25767
  if (prev && !aborts(prev)) {
25756
25768
  prev.body = prev.body.concat(branch.body);
25757
25769
  } else {
25758
- trim_unreachable_code(compressor, branch, decl);
25770
+ extract_from_unreachable_code(compressor, branch, decl);
25759
25771
  }
25760
25772
  }
25761
25773
  function branches_equivalent(branch, prev, insertBreak) {
@@ -25809,7 +25821,7 @@ def_optimize(AST_Try, function(self, compressor) {
25809
25821
  if (compressor.option("dead_code") && self.body.body.every(is_empty)) {
25810
25822
  var body = [];
25811
25823
  if (self.bcatch) {
25812
- trim_unreachable_code(compressor, self.bcatch, body);
25824
+ extract_from_unreachable_code(compressor, self.bcatch, body);
25813
25825
  }
25814
25826
  if (self.bfinally) body.push(...self.bfinally.body);
25815
25827
  return make_node(AST_BlockStatement, self, {
@@ -40369,8 +40381,8 @@ const processOptions = (inputOptions) => {
40369
40381
  try {
40370
40382
  const result = await minify$1(code, terserOptions);
40371
40383
  return result.code.replace(/;$/, '');
40372
- } catch (error) {
40373
- options.log(error);
40384
+ } catch (err) {
40385
+ options.log(err);
40374
40386
  return text;
40375
40387
  }
40376
40388
  };
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "entities": "^7.0.0",
12
12
  "lightningcss": "^1.28.2",
13
13
  "relateurl": "^0.2.7",
14
- "terser": "^5.44.0"
14
+ "terser": "^5.44.1"
15
15
  },
16
16
  "description": "Highly configurable, well-tested, JavaScript-based HTML minifier",
17
17
  "devDependencies": {
@@ -84,5 +84,5 @@
84
84
  "test:watch": "node --test --watch tests/*.spec.js"
85
85
  },
86
86
  "type": "module",
87
- "version": "4.1.1"
87
+ "version": "4.2.2"
88
88
  }
@@ -798,8 +798,8 @@ const processOptions = (inputOptions) => {
798
798
  try {
799
799
  const result = await terser(code, terserOptions);
800
800
  return result.code.replace(/;$/, '');
801
- } catch (error) {
802
- options.log(error);
801
+ } catch (err) {
802
+ options.log(err);
803
803
  return text;
804
804
  }
805
805
  };