diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e6dd3..bc57632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Behavioural changes in TerserJS are listed [here](https://github.com/terser/terser/blob/master/CHANGELOG.md). ## Unreleased +## 1.2.4 (7 October 2024) +- update TerserJS to [5.34.1] + ## 1.2.3 (11 June 2024) - update TerserJS to [5.31.1] diff --git a/lib/terser.js b/lib/terser.js index 217e6b5..245e7ea 100644 --- a/lib/terser.js +++ b/lib/terser.js @@ -7571,6 +7571,19 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }); }, + ImportExpression: function(M) { + return new AST_Call({ + start: my_start_token(M), + end: my_end_token(M), + expression: from_moz({ + type: "Identifier", + name: "import" + }), + optional: false, + args: [from_moz(M.source)] + }); + }, + ExportAllDeclaration: function(M) { var foreign_name = M.exported == null ? new AST_SymbolExportForeign({ name: "*" }) : @@ -7638,6 +7651,11 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { args.value = { source, flags }; return new AST_RegExp(args); } + const bi = typeof M.value === "bigint" ? M.value.toString() : M.bigint; + if (typeof bi === "string") { + args.value = bi; + return new AST_BigInt(args); + } if (val === null) return new AST_Null(args); switch (typeof val) { case "string": @@ -7705,14 +7723,6 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }); }, - BigIntLiteral(M) { - return new AST_BigInt({ - start : my_start_token(M), - end : my_end_token(M), - value : M.value - }); - }, - EmptyStatement: function(M) { return new AST_EmptyStatement({ start: my_start_token(M), @@ -8185,6 +8195,14 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }; }); def_to_moz(AST_Call, function To_Moz_CallExpression(M) { + if (M.expression instanceof AST_SymbolRef && M.expression.name === "import") { + const [source] = M.args.map(to_moz); + return { + type: "ImportExpression", + source, + }; + } + return { type: "CallExpression", callee: to_moz(M.expression), @@ -8730,8 +8748,13 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) { }); def_to_moz(AST_BigInt, M => ({ - type: "BigIntLiteral", - value: M.value + type: "Literal", + // value cannot be represented natively + // see: https://github.com/estree/estree/blob/master/es2020.md#bigintliteral + value: null, + // `M.value` is a string that may be a hex number representation. + // but "bigint" property should have only decimal digits + bigint: typeof BigInt === "function" ? BigInt(M.value).toString() : M.value, })); AST_Boolean.DEFMETHOD("to_mozilla_ast", AST_Constant.prototype.to_mozilla_ast); @@ -10276,7 +10299,9 @@ function OutputStream(options) { AST_Arrow.DEFMETHOD("_do_print", function(output) { var self = this; var parent = output.parent(); - var needs_parens = (parent instanceof AST_Binary && !(parent instanceof AST_Assign)) || + var needs_parens = (parent instanceof AST_Binary && + !(parent instanceof AST_Assign) && + !(parent instanceof AST_DefaultAssign)) || parent instanceof AST_Unary || (parent instanceof AST_Call && self === parent.expression); if (needs_parens) { output.print("("); } @@ -11649,7 +11674,7 @@ function redefined_catch_def(def) { } } -AST_Scope.DEFMETHOD("figure_out_scope", function(options, { parent_scope = null, toplevel = this } = {}) { +AST_Scope.DEFMETHOD("figure_out_scope", function(options, { parent_scope = undefined, toplevel = this } = {}) { options = defaults(options, { cache: null, ie8: false, @@ -11973,7 +11998,7 @@ AST_Scope.DEFMETHOD("add_child_scope", function (scope) { scope.parent_scope = this; // Propagate to this.uses_arguments from arrow functions - if ((scope instanceof AST_Arrow) && !this.uses_arguments) { + if ((scope instanceof AST_Arrow) && (this instanceof AST_Lambda && !this.uses_arguments)) { this.uses_arguments = walk(scope, node => { if ( node instanceof AST_SymbolRef @@ -13040,6 +13065,23 @@ function make_sequence(orig, expressions) { }); } +function make_empty_function(self) { + return make_node(AST_Function, self, { + uses_arguments: false, + argnames: [], + body: [], + is_generator: false, + async: false, + variables: new Map(), + uses_with: false, + uses_eval: false, + parent_scope: null, + enclosed: [], + cname: 0, + block_scope: undefined, + }); +} + function make_node_from_constant(val, orig) { switch (typeof val) { case "string": @@ -13266,9 +13308,9 @@ function is_reachable(scope_node, defs) { } /** Check if a ref refers to the name of a function/class it's defined within */ -function is_recursive_ref(compressor, def) { +function is_recursive_ref(tw, def) { var node; - for (var i = 0; node = compressor.parent(i); i++) { + for (var i = 0; node = tw.parent(i); i++) { if (node instanceof AST_Lambda || node instanceof AST_Class) { var name = node.name; if (name && name.definition() === def) { @@ -13834,7 +13876,7 @@ function is_nullish(node, compressor) { return any(this.definitions, compressor); }); def_has_side_effects(AST_VarDef, function() { - return this.value; + return this.value != null; }); def_has_side_effects(AST_TemplateSegment, return_false); def_has_side_effects(AST_TemplateString, function(compressor) { @@ -14393,6 +14435,54 @@ function is_modified(compressor, tw, node, value, level, immutable) { } } +/** + * Check if a node may be used by the expression it's in + * void (0, 1, {node}, 2) -> false + * console.log(0, {node}) -> true + */ +function is_used_in_expression(tw) { + for (let p = -1, node, parent; node = tw.parent(p), parent = tw.parent(p + 1); p++) { + if (parent instanceof AST_Sequence) { + const nth_expression = parent.expressions.indexOf(node); + if (nth_expression !== parent.expressions.length - 1) { + // Detect (0, x.noThis)() constructs + const grandparent = tw.parent(p + 2); + if ( + parent.expressions.length > 2 + || parent.expressions.length === 1 + || !requires_sequence_to_maintain_binding(grandparent, parent, parent.expressions[1]) + ) { + return false; + } + return true; + } else { + continue; + } + } + if (parent instanceof AST_Unary) { + const op = parent.operator; + if (op === "void") { + return false; + } + if (op === "typeof" || op === "+" || op === "-" || op === "!" || op === "~") { + continue; + } + } + if ( + parent instanceof AST_SimpleStatement + || parent instanceof AST_LabeledStatement + ) { + return false; + } + if (parent instanceof AST_Scope) { + return false; + } + return true; + } + + return true; +} + /*********************************************************************** A JavaScript tokenizer / parser / beautifier / compressor. @@ -14577,14 +14667,25 @@ def_eval(AST_Object, function (compressor, depth) { var non_converting_unary = makePredicate("! typeof void"); def_eval(AST_UnaryPrefix, function (compressor, depth) { var e = this.expression; - // Function would be evaluated to an array and so typeof would - // incorrectly return 'object'. Hence making is a special case. if (compressor.option("typeofs") - && this.operator == "typeof" - && (e instanceof AST_Lambda + && this.operator == "typeof") { + // Function would be evaluated to an array and so typeof would + // incorrectly return 'object'. Hence making is a special case. + if (e instanceof AST_Lambda || e instanceof AST_SymbolRef - && e.fixed_value() instanceof AST_Lambda)) { - return typeof function () { }; + && e.fixed_value() instanceof AST_Lambda) { + return typeof function () { }; + } + if ( + (e instanceof AST_Object + || e instanceof AST_Array + || (e instanceof AST_SymbolRef + && (e.fixed_value() instanceof AST_Object + || e.fixed_value() instanceof AST_Array))) + && !e.has_side_effects(compressor) + ) { + return typeof {}; + } } if (!non_converting_unary.has(this.operator)) depth++; @@ -15272,7 +15373,6 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { } var var_defs_by_id = new Map(); var initializations = new Map(); - var self_referential_classes = new Set(); // pass 1: find out which symbols are directly used in // this scope (not in nested scopes). @@ -15287,8 +15387,11 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { } if (node === self) return; if (node instanceof AST_Class && node.has_side_effects(compressor)) { - if (node.is_self_referential()) self_referential_classes.add(node); - node.visit_nondeferred_class_parts(tw); + if (node.is_self_referential()) { + descend(); + } else { + node.visit_nondeferred_class_parts(tw); + } } if (node instanceof AST_Defun || node instanceof AST_DefClass) { var node_def = node.name.definition(); @@ -15352,9 +15455,6 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { init.walk(tw); }); }); - self_referential_classes.forEach(function (cls) { - cls.walk(tw); - }); // pass 3: we should drop declarations not in_use var tt = new TreeTransformer( function before(node, descend, in_list) { @@ -15390,7 +15490,13 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { if (!in_use_ids.has(def.id) || def.orig.length > 1) node.name = null; } if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { - var trim = !compressor.option("keep_fargs"); + var trim = + !compressor.option("keep_fargs") + // Is this an IIFE that won't refer to its name? + || parent instanceof AST_Call + && parent.expression === node + && !node.pinned() + && (!node.name || node.name.unreferenced()); for (var a = node.argnames, i = a.length; --i >= 0;) { var sym = a[i]; if (sym instanceof AST_Expansion) { @@ -15631,45 +15737,6 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { } }); -/** - * Check if a node may be used by the expression it's in - * void (0, 1, {node}, 2) -> false - * console.log(0, {node}) -> true - */ -function is_used_in_expression(tw) { - for (let p = -1, node, parent; node = tw.parent(p), parent = tw.parent(p + 1); p++) { - if (parent instanceof AST_Sequence) { - const nth_expression = parent.expressions.indexOf(node); - if (nth_expression !== parent.expressions.length - 1) { - // Detect (0, x.noThis)() constructs - const grandparent = tw.parent(p + 2); - if ( - parent.expressions.length > 2 - || parent.expressions.length === 1 - || !requires_sequence_to_maintain_binding(grandparent, parent, parent.expressions[1]) - ) { - return false; - } - return true; - } else { - continue; - } - } - if (parent instanceof AST_Unary) { - const op = parent.operator; - if (op === "void") { - return false; - } - if (op === "typeof" || op === "+" || op === "-" || op === "!" || op === "~") { - continue; - } - } - return true; - } - - return true; -} - /*********************************************************************** A JavaScript tokenizer / parser / beautifier / compressor. @@ -16106,28 +16173,50 @@ function mark_lambda(tw, descend, compressor) { * // use defined_after * } * - * This function is called on the parent to handle this issue. + * Or even indirectly: + * + * B(); + * var defined_after = true; + * function A() { + * // use defined_after + * } + * function B() { + * A(); + * } + * + * Access a variable before declaration will either throw a ReferenceError + * (if the variable is declared with `let` or `const`), + * or get an `undefined` (if the variable is declared with `var`). + * + * If the variable is inlined into the function, the behavior will change. + * + * This function is called on the parent to disallow inlining of such variables, */ function handle_defined_after_hoist(parent) { const defuns = []; walk(parent, node => { if (node === parent) return; - if (node instanceof AST_Defun) defuns.push(node); + if (node instanceof AST_Defun) { + defuns.push(node); + return true; + } if ( node instanceof AST_Scope || node instanceof AST_SimpleStatement ) return true; }); + // `defun` id to array of `defun` it uses + const defun_dependencies_map = new Map(); + // `defun` id to array of enclosing `def` that are used by the function + const dependencies_map = new Map(); + // all symbol ids that will be tracked for read/write const symbols_of_interest = new Set(); const defuns_of_interest = new Set(); - const potential_conflicts = []; for (const defun of defuns) { const fname_def = defun.name.definition(); - const found_self_ref_in_other_defuns = defuns.some( - d => d !== defun && d.enclosed.indexOf(fname_def) !== -1 - ); + const enclosing_defs = []; for (const def of defun.enclosed) { if ( @@ -16138,93 +16227,107 @@ function handle_defined_after_hoist(parent) { continue; } - // defun is hoisted, so always safe + symbols_of_interest.add(def.id); + + // found a reference to another function if ( def.assignments === 0 && def.orig.length === 1 && def.orig[0] instanceof AST_SymbolDefun ) { - continue; - } + defuns_of_interest.add(def.id); + symbols_of_interest.add(def.id); + + defuns_of_interest.add(fname_def.id); + symbols_of_interest.add(fname_def.id); + + if (!defun_dependencies_map.has(fname_def.id)) { + defun_dependencies_map.set(fname_def.id, []); + } + defun_dependencies_map.get(fname_def.id).push(def.id); - if (found_self_ref_in_other_defuns) { - def.fixed = false; continue; } - // for the slower checks below this loop - potential_conflicts.push({ defun, def, fname_def }); - symbols_of_interest.add(def.id); + enclosing_defs.push(def); + } + + if (enclosing_defs.length) { + dependencies_map.set(fname_def.id, enclosing_defs); + defuns_of_interest.add(fname_def.id); symbols_of_interest.add(fname_def.id); - defuns_of_interest.add(defun); } } - // linearize all symbols, and locate defs that are read after the defun - if (potential_conflicts.length) { - // All "symbols of interest", that is, defuns or defs, that we found. - // These are placed in order so we can check which is after which. - const found_symbols = []; - // Indices of `found_symbols` which are writes - const found_symbol_writes = new Set(); - // Defun ranges are recorded because we don't care if a function uses the def internally - const defun_ranges = new Map(); + // No defuns use outside constants + if (!dependencies_map.size) { + return; + } - let tw; - parent.walk((tw = new TreeWalker((node, descend) => { - if (node instanceof AST_Defun && defuns_of_interest.has(node)) { - const start = found_symbols.length; - descend(); - const end = found_symbols.length; + // Increment to count "symbols of interest" (defuns or defs) that we found. + // These are tracked in AST order so we can check which is after which. + let symbol_index = 1; + // Map a defun ID to its first read (a `symbol_index`) + const defun_first_read_map = new Map(); + // Map a symbol ID to its last write (a `symbol_index`) + const symbol_last_write_map = new Map(); - defun_ranges.set(node, { start, end }); - return true; - } - // if we found a defun on the list, mark IN_DEFUN=id and descend + walk_parent(parent, (node, walk_info) => { + if (node instanceof AST_Symbol && node.thedef) { + const id = node.definition().id; - if (node instanceof AST_Symbol && node.thedef) { - const id = node.definition().id; - if (symbols_of_interest.has(id)) { - if (node instanceof AST_SymbolDeclaration || is_lhs(node, tw)) { - found_symbol_writes.add(found_symbols.length); - } - found_symbols.push(id); + symbol_index++; + + // Track last-writes to symbols + if (symbols_of_interest.has(id)) { + if (node instanceof AST_SymbolDeclaration || is_lhs(node, walk_info.parent())) { + symbol_last_write_map.set(id, symbol_index); } } - }))); - for (const { def, defun, fname_def } of potential_conflicts) { - const defun_range = defun_ranges.get(defun); + // Track first-reads of defuns (refined later) + if (defuns_of_interest.has(id)) { + if (!defun_first_read_map.has(id) && !is_recursive_ref(walk_info, id)) { + defun_first_read_map.set(id, symbol_index); + } + } + } + }); - // find the index in `found_symbols`, with some special rules: - const find = (sym_id, starting_at = 0, must_be_write = false) => { - let index = starting_at; + // Refine `defun_first_read_map` to be as high as possible + for (const [defun, defun_first_read] of defun_first_read_map) { + // Update all depdencies of `defun` + const queue = new Set(defun_dependencies_map.get(defun)); + for (const enclosed_defun of queue) { + let enclosed_defun_first_read = defun_first_read_map.get(enclosed_defun); + if (enclosed_defun_first_read != null && enclosed_defun_first_read < defun_first_read) { + continue; + } - for (;;) { - index = found_symbols.indexOf(sym_id, index); + defun_first_read_map.set(enclosed_defun, defun_first_read); - if (index === -1) { - break; - } else if (index >= defun_range.start && index < defun_range.end) { - index = defun_range.end; - continue; - } else if (must_be_write && !found_symbol_writes.has(index)) { - index++; - continue; - } else { - break; - } - } + for (const enclosed_enclosed_defun of defun_dependencies_map.get(enclosed_defun) || []) { + queue.add(enclosed_enclosed_defun); + } + } + } - return index; - }; + // ensure write-then-read order, otherwise clear `fixed` + // This is safe because last-writes (found_symbol_writes) are assumed to be as late as possible, and first-reads (defun_first_read_map) are assumed to be as early as possible. + for (const [defun, defs] of dependencies_map) { + const defun_first_read = defun_first_read_map.get(defun); + if (defun_first_read === undefined) { + continue; + } - const read_defun_at = find(fname_def.id); - const wrote_def_at = find(def.id, read_defun_at + 1, true); + for (const def of defs) { + if (def.fixed === false) { + continue; + } - const wrote_def_after_reading_defun = read_defun_at != -1 && wrote_def_at != -1 && wrote_def_at > read_defun_at; + let def_last_write = symbol_last_write_map.get(def.id) || 0; - if (wrote_def_after_reading_defun) { + if (defun_first_read < def_last_write) { def.fixed = false; } } @@ -18858,9 +18961,8 @@ def_optimize(AST_Node, function(self) { }); AST_Toplevel.DEFMETHOD("drop_console", function(options) { - var isArray = Array.isArray(options); - - return this.transform(new TreeTransformer(function(self) { + const isArray = Array.isArray(options); + const tt = new TreeTransformer(function(self) { if (self.TYPE !== "Call") { return; } @@ -18871,18 +18973,35 @@ AST_Toplevel.DEFMETHOD("drop_console", function(options) { return; } - if (isArray && options.indexOf(exp.property) === -1) { + if (isArray && !options.includes(exp.property)) { return; } var name = exp.expression; + var depth = 2; while (name.expression) { name = name.expression; + depth++; } + if (is_undeclared_ref(name) && name.name == "console") { - return make_node(AST_Undefined, self); + if ( + depth === 3 + && !["call", "apply"].includes(exp.property) + && is_used_in_expression(tt) + ) { + // a (used) call to Function.prototype methods (eg: console.log.bind(console)) + // but not .call and .apply which would also return undefined. + exp.expression = make_empty_function(self); + set_flag(exp.expression, SQUEEZED); + self.args = []; + } else { + return make_node(AST_Undefined, self); + } } - })); + }); + + return this.transform(tt); }); AST_Node.DEFMETHOD("equivalent_to", function(node) { @@ -19581,7 +19700,9 @@ def_optimize(AST_Switch, function(self, compressor) { eliminate_branch(branch, body[body.length - 1]); continue; } - if (exp instanceof AST_Node) exp = branch.expression.tail_node().evaluate(compressor); + if (exp instanceof AST_Node && !exp.has_side_effects(compressor)) { + exp = branch.expression.tail_node().evaluate(compressor); + } if (exp === value) { exact_match = branch; if (default_branch) { @@ -19763,12 +19884,9 @@ def_optimize(AST_Switch, function(self, compressor) { break DEFAULT; } - let sideEffect = body.find(branch => { - return ( - branch !== default_or_exact - && branch.expression.has_side_effects(compressor) - ); - }); + let sideEffect = body.find( + branch => branch !== default_or_exact && branch.expression.has_side_effects(compressor) + ); // If no cases cause a side-effect, we can eliminate the switch entirely. if (!sideEffect) { return make_node(AST_BlockStatement, self, { @@ -19876,9 +19994,10 @@ def_optimize(AST_Switch, function(self, compressor) { right: branch.expression, }), body: consequent, - alternative: null - }) - ].concat(always) + alternative: null, + }), + always, + ], }).optimize(compressor); } return self; @@ -19901,13 +20020,12 @@ def_optimize(AST_Switch, function(self, compressor) { let pblock = make_node(AST_BlockStatement, prev, { body: pbody }); return bblock.equivalent_to(pblock); } - function statement(expression) { - return make_node(AST_SimpleStatement, expression, { - body: expression - }); + function statement(body) { + return make_node(AST_SimpleStatement, body, { body }); } function has_nested_break(root) { let has_break = false; + let tw = new TreeWalker(node => { if (has_break) return true; if (node instanceof AST_Lambda) return true; @@ -20260,10 +20378,7 @@ def_optimize(AST_Call, function(self, compressor) { && is_undeclared_ref(exp) && exp.name == "Function") { // new Function() => function(){} - if (self.args.length == 0) return make_node(AST_Function, self, { - argnames: [], - body: [] - }).optimize(compressor); + if (self.args.length == 0) return make_empty_function(self).optimize(compressor); if (self.args.every((x) => x instanceof AST_String)) { // quite a corner-case, but we can handle it: // https://github.com/mishoo/UglifyJS2/issues/203 @@ -22023,10 +22138,7 @@ def_optimize(AST_Dot, function(self, compressor) { }); break; case "Function": - self.expression = make_node(AST_Function, self.expression, { - argnames: [], - body: [] - }); + self.expression = make_empty_function(self.expression); break; case "Number": self.expression = make_node(AST_Number, self.expression, { @@ -30779,6 +30891,7 @@ var domprops = [ "uint32", "uint8", "uint8Clamped", + "unadjustedMovement", "unclippedDepth", "unconfigure", "undefined", @@ -31570,7 +31683,9 @@ function mangle_properties(ast, options, annotated_props = find_annotated_props( } else if (node instanceof AST_ObjectProperty) { // setter, getter, method or class field if (!keep_quoted || !node.quote) { - node.key.name = mangle(node.key.name); + if (!node.computed_key()) { + node.key.name = mangle(node.key.name); + } } } else if (node instanceof AST_Dot) { if (!keep_quoted || !node.quote) { @@ -31947,7 +32062,7 @@ function* minify_sync_or_async(files, options, _fs_module) { if (node.block_scope) { node.block_scope.variables = undefined; node.block_scope.enclosed = undefined; - node.parent_scope = undefined; + node.block_scope.parent_scope = undefined; } }); } diff --git a/lib/terser/version.rb b/lib/terser/version.rb index fbeccda..ce7d3b2 100644 --- a/lib/terser/version.rb +++ b/lib/terser/version.rb @@ -2,5 +2,5 @@ class Terser # Current version of Terser. - VERSION = "1.2.3" + VERSION = "1.2.4" end diff --git a/vendor/terser b/vendor/terser index 57ea592..d3eac90 160000 --- a/vendor/terser +++ b/vendor/terser @@ -1 +1 @@ -Subproject commit 57ea5921f9910a0818a5f68190801a66193c1b41 +Subproject commit d3eac90c7573c80431109b8c1cc15de5b7cca6aa