From 156cfed953df866a0b80dd07e07c18056185e72f Mon Sep 17 00:00:00 2001 From: Sophie Alpert Date: Mon, 6 Oct 2025 14:26:56 -0700 Subject: [PATCH] improve keepNames behavior for profiling tools In my esbuild app with keepNames: true, minify: false I noticed that for declarations `let foo = function() {}` the function shows in V8 profiles as anonymous. This is due to the transformation to `let foo = __name(function() {}, 'foo')` which prevents the NamedEvaluation on the anonymous function which typically happens natively (eg: https://tc39.es/ecma262/#sec-variable-statement-runtime-semantics-evaluation for VariableDeclaration). Evidently V8 keeps an internal idea of the function name distinct from the .name property. To work around this, we transform these instead to `let foo = __firstValue({ foo: function() {} })` which takes advantage of the name assignment in https://tc39.es/ecma262/#sec-runtime-semantics-propertydefinitionevaluation. With this fix, these functions show names correctly in the Chrome profiler. --- .../snapshots/snapshots_default.txt | 184 +++++++++++------- .../snapshots/snapshots_glob.txt | 64 +++--- .../snapshots/snapshots_splitting.txt | 26 +-- .../bundler_tests/snapshots/snapshots_ts.txt | 12 +- internal/js_parser/js_parser.go | 27 ++- internal/runtime/runtime.go | 1 + scripts/js-api-tests.js | 4 +- 7 files changed, 197 insertions(+), 121 deletions(-) diff --git a/internal/bundler_tests/snapshots/snapshots_default.txt b/internal/bundler_tests/snapshots/snapshots_default.txt index ad869cd413a..ccc7c6ef0d1 100644 --- a/internal/bundler_tests/snapshots/snapshots_default.txt +++ b/internal/bundler_tests/snapshots/snapshots_default.txt @@ -2749,102 +2749,154 @@ function foo(fn2 = function() { }) { } __name(foo, "foo"); -var fn = /* @__PURE__ */ __name(function() { -}, "fn"); -var obj = { "f n": /* @__PURE__ */ __name(function() { -}, "f n") }; +var fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}); +var obj = { "f n": /* @__PURE__ */ __firstValue({ + "f n": function() { + } +}) }; class Foo0 { static { __name(this, "Foo0"); } - "f n" = /* @__PURE__ */ __name(function() { - }, "f n"); + "f n" = /* @__PURE__ */ __firstValue({ + "f n": function() { + } + }); } class Foo1 { static { __name(this, "Foo1"); } - static "f n" = /* @__PURE__ */ __name(function() { - }, "f n"); + static "f n" = /* @__PURE__ */ __firstValue({ + "f n": function() { + } + }); } class Foo2 { static { __name(this, "Foo2"); } - accessor "f n" = /* @__PURE__ */ __name(function() { - }, "f n"); + accessor "f n" = /* @__PURE__ */ __firstValue({ + "f n": function() { + } + }); } class Foo3 { static { __name(this, "Foo3"); } - static accessor "f n" = /* @__PURE__ */ __name(function() { - }, "f n"); + static accessor "f n" = /* @__PURE__ */ __firstValue({ + "f n": function() { + } + }); } class Foo4 { static { __name(this, "Foo4"); } - #fn = /* @__PURE__ */ __name(function() { - }, "#fn"); + #fn = /* @__PURE__ */ __firstValue({ + "#fn": function() { + } + }); } class Foo5 { static { __name(this, "Foo5"); } - static #fn = /* @__PURE__ */ __name(function() { - }, "#fn"); + static #fn = /* @__PURE__ */ __firstValue({ + "#fn": function() { + } + }); } class Foo6 { static { __name(this, "Foo6"); } - accessor #fn = /* @__PURE__ */ __name(function() { - }, "#fn"); + accessor #fn = /* @__PURE__ */ __firstValue({ + "#fn": function() { + } + }); } class Foo7 { static { __name(this, "Foo7"); } - static accessor #fn = /* @__PURE__ */ __name(function() { - }, "#fn"); -} -fn = /* @__PURE__ */ __name(function() { -}, "fn"); -fn ||= /* @__PURE__ */ __name(function() { -}, "fn"); -fn &&= /* @__PURE__ */ __name(function() { -}, "fn"); -fn ??= /* @__PURE__ */ __name(function() { -}, "fn"); -var [fn = /* @__PURE__ */ __name(function() { -}, "fn")] = []; -var { fn = /* @__PURE__ */ __name(function() { -}, "fn") } = {}; -for (var [fn = /* @__PURE__ */ __name(function() { -}, "fn")] = []; ; ) ; -for (var { fn = /* @__PURE__ */ __name(function() { -}, "fn") } = {}; ; ) ; -for (var [fn = /* @__PURE__ */ __name(function() { -}, "fn")] in obj) ; -for (var { fn = /* @__PURE__ */ __name(function() { -}, "fn") } in obj) ; -for (var [fn = /* @__PURE__ */ __name(function() { -}, "fn")] of obj) ; -for (var { fn = /* @__PURE__ */ __name(function() { -}, "fn") } of obj) ; -function foo([fn2 = /* @__PURE__ */ __name(function() { -}, "fn")]) { + static accessor #fn = /* @__PURE__ */ __firstValue({ + "#fn": function() { + } + }); +} +fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}); +fn ||= /* @__PURE__ */ __firstValue({ + fn: function() { + } +}); +fn &&= /* @__PURE__ */ __firstValue({ + fn: function() { + } +}); +fn ??= /* @__PURE__ */ __firstValue({ + fn: function() { + } +}); +var [fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +})] = []; +var { fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}) } = {}; +for (var [fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +})] = []; ; ) ; +for (var { fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}) } = {}; ; ) ; +for (var [fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +})] in obj) ; +for (var { fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}) } in obj) ; +for (var [fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +})] of obj) ; +for (var { fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}) } of obj) ; +function foo([fn2 = /* @__PURE__ */ __firstValue({ + fn: function() { + } +})]) { } __name(foo, "foo"); -function foo({ fn: fn2 = /* @__PURE__ */ __name(function() { -}, "fn") }) { +function foo({ fn: fn2 = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}) }) { } __name(foo, "foo"); -[fn = /* @__PURE__ */ __name(function() { -}, "fn")] = []; -({ fn = /* @__PURE__ */ __name(function() { -}, "fn") } = {}); +[fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +})] = []; +({ fn = /* @__PURE__ */ __firstValue({ + fn: function() { + } +}) } = {}); ---------- /out/do-not-keep.js ---------- class Foo0 { @@ -3082,8 +3134,10 @@ function fnStmtKeep() { } __name(fnStmtKeep, "fnStmtKeep"); x = fnStmtKeep; -var fnExprKeep = /* @__PURE__ */ __name(function() { -}, "keep"); +var fnExprKeep = /* @__PURE__ */ __firstValue({ + keep: function() { + } +}); x = fnExprKeep; var clsStmtKeep = class { static { @@ -4670,7 +4724,7 @@ copy import { __commonJS, __require -} from "./chunk-MQN2VSL5.js"; +} from "./chunk-Q5ZK3UMZ.js"; // project/cjs.js var require_cjs = __commonJS({ @@ -4701,15 +4755,15 @@ console.log( e, __require("extern-cjs"), require_cjs(), - import("./dynamic-Q2DWDUFV.js") + import("./dynamic-4NEUC7FD.js") ); var exported; export { exported }; ----------- /out/dynamic-Q2DWDUFV.js ---------- -import "./chunk-MQN2VSL5.js"; +---------- /out/dynamic-4NEUC7FD.js ---------- +import "./chunk-Q5ZK3UMZ.js"; // project/dynamic.js var dynamic_default = 5; @@ -4717,7 +4771,7 @@ export { dynamic_default as default }; ----------- /out/chunk-MQN2VSL5.js ---------- +---------- /out/chunk-Q5ZK3UMZ.js ---------- export { __require, __commonJS @@ -4874,7 +4928,7 @@ d { "out/entry.js": { "imports": [ { - "path": "out/chunk-MQN2VSL5.js", + "path": "out/chunk-Q5ZK3UMZ.js", "kind": "import-statement" }, { @@ -4896,7 +4950,7 @@ d { "external": true }, { - "path": "out/dynamic-Q2DWDUFV.js", + "path": "out/dynamic-4NEUC7FD.js", "kind": "dynamic-import" } ], @@ -4923,10 +4977,10 @@ d { }, "bytes": 642 }, - "out/dynamic-Q2DWDUFV.js": { + "out/dynamic-4NEUC7FD.js": { "imports": [ { - "path": "out/chunk-MQN2VSL5.js", + "path": "out/chunk-Q5ZK3UMZ.js", "kind": "import-statement" } ], @@ -4941,7 +4995,7 @@ d { }, "bytes": 119 }, - "out/chunk-MQN2VSL5.js": { + "out/chunk-Q5ZK3UMZ.js": { "imports": [], "exports": [ "__commonJS", diff --git a/internal/bundler_tests/snapshots/snapshots_glob.txt b/internal/bundler_tests/snapshots/snapshots_glob.txt index 59b248d7fa9..25a83ff6d56 100644 --- a/internal/bundler_tests/snapshots/snapshots_glob.txt +++ b/internal/bundler_tests/snapshots/snapshots_glob.txt @@ -59,13 +59,13 @@ TestGlobBasicSplitting ---------- /out/entry.js ---------- import { require_a -} from "./chunk-KO426RN2.js"; +} from "./chunk-UP4LJV56.js"; import { require_b -} from "./chunk-SGVK3D4Q.js"; +} from "./chunk-UKYXN6A7.js"; import { __glob -} from "./chunk-WCFE7E2E.js"; +} from "./chunk-TIP5PUT2.js"; // require("./src/**/*") in entry.js var globRequire_src = __glob({ @@ -75,8 +75,8 @@ var globRequire_src = __glob({ // import("./src/**/*") in entry.js var globImport_src = __glob({ - "./src/a.js": () => import("./a-7QA47R6Z.js"), - "./src/b.js": () => import("./b-KY4MVCQS.js") + "./src/a.js": () => import("./a-ZVIGZMPD.js"), + "./src/b.js": () => import("./b-SYBKXDGG.js") }); // entry.js @@ -92,17 +92,17 @@ console.log({ } }); ----------- /out/a-7QA47R6Z.js ---------- +---------- /out/a-ZVIGZMPD.js ---------- import { require_a -} from "./chunk-KO426RN2.js"; -import "./chunk-WCFE7E2E.js"; +} from "./chunk-UP4LJV56.js"; +import "./chunk-TIP5PUT2.js"; export default require_a(); ----------- /out/chunk-KO426RN2.js ---------- +---------- /out/chunk-UP4LJV56.js ---------- import { __commonJS -} from "./chunk-WCFE7E2E.js"; +} from "./chunk-TIP5PUT2.js"; // src/a.js var require_a = __commonJS({ @@ -115,17 +115,17 @@ export { require_a }; ----------- /out/b-KY4MVCQS.js ---------- +---------- /out/b-SYBKXDGG.js ---------- import { require_b -} from "./chunk-SGVK3D4Q.js"; -import "./chunk-WCFE7E2E.js"; +} from "./chunk-UKYXN6A7.js"; +import "./chunk-TIP5PUT2.js"; export default require_b(); ----------- /out/chunk-SGVK3D4Q.js ---------- +---------- /out/chunk-UKYXN6A7.js ---------- import { __commonJS -} from "./chunk-WCFE7E2E.js"; +} from "./chunk-TIP5PUT2.js"; // src/b.js var require_b = __commonJS({ @@ -138,7 +138,7 @@ export { require_b }; ----------- /out/chunk-WCFE7E2E.js ---------- +---------- /out/chunk-TIP5PUT2.js ---------- export { __glob, __commonJS @@ -321,13 +321,13 @@ TestTSGlobBasicSplitting ---------- /out/entry.js ---------- import { require_a -} from "./chunk-YMCIDKCT.js"; +} from "./chunk-CL725R6V.js"; import { require_b -} from "./chunk-2BST4PYI.js"; +} from "./chunk-Z5A77AT4.js"; import { __glob -} from "./chunk-WCFE7E2E.js"; +} from "./chunk-TIP5PUT2.js"; // require("./src/**/*") in entry.ts var globRequire_src = __glob({ @@ -337,8 +337,8 @@ var globRequire_src = __glob({ // import("./src/**/*") in entry.ts var globImport_src = __glob({ - "./src/a.ts": () => import("./a-YXM4MR7E.js"), - "./src/b.ts": () => import("./b-IPMBSSGN.js") + "./src/a.ts": () => import("./a-Z4SVGQEW.js"), + "./src/b.ts": () => import("./b-CHH3HMN4.js") }); // entry.ts @@ -354,17 +354,17 @@ console.log({ } }); ----------- /out/a-YXM4MR7E.js ---------- +---------- /out/a-Z4SVGQEW.js ---------- import { require_a -} from "./chunk-YMCIDKCT.js"; -import "./chunk-WCFE7E2E.js"; +} from "./chunk-CL725R6V.js"; +import "./chunk-TIP5PUT2.js"; export default require_a(); ----------- /out/chunk-YMCIDKCT.js ---------- +---------- /out/chunk-CL725R6V.js ---------- import { __commonJS -} from "./chunk-WCFE7E2E.js"; +} from "./chunk-TIP5PUT2.js"; // src/a.ts var require_a = __commonJS({ @@ -377,17 +377,17 @@ export { require_a }; ----------- /out/b-IPMBSSGN.js ---------- +---------- /out/b-CHH3HMN4.js ---------- import { require_b -} from "./chunk-2BST4PYI.js"; -import "./chunk-WCFE7E2E.js"; +} from "./chunk-Z5A77AT4.js"; +import "./chunk-TIP5PUT2.js"; export default require_b(); ----------- /out/chunk-2BST4PYI.js ---------- +---------- /out/chunk-Z5A77AT4.js ---------- import { __commonJS -} from "./chunk-WCFE7E2E.js"; +} from "./chunk-TIP5PUT2.js"; // src/b.ts var require_b = __commonJS({ @@ -400,7 +400,7 @@ export { require_b }; ----------- /out/chunk-WCFE7E2E.js ---------- +---------- /out/chunk-TIP5PUT2.js ---------- export { __glob, __commonJS diff --git a/internal/bundler_tests/snapshots/snapshots_splitting.txt b/internal/bundler_tests/snapshots/snapshots_splitting.txt index 071fb909084..0dced403556 100644 --- a/internal/bundler_tests/snapshots/snapshots_splitting.txt +++ b/internal/bundler_tests/snapshots/snapshots_splitting.txt @@ -258,19 +258,19 @@ TestSplittingDynamicAndNotDynamicCommonJSIntoES6 import { __toESM, require_foo -} from "./chunk-X3UWZZCR.js"; +} from "./chunk-3FOKVS53.js"; // entry.js var import_foo = __toESM(require_foo()); -import("./foo-BJYZ44Z3.js").then(({ default: { bar: b } }) => console.log(import_foo.bar, b)); +import("./foo-4JANTOJX.js").then(({ default: { bar: b } }) => console.log(import_foo.bar, b)); ----------- /out/foo-BJYZ44Z3.js ---------- +---------- /out/foo-4JANTOJX.js ---------- import { require_foo -} from "./chunk-X3UWZZCR.js"; +} from "./chunk-3FOKVS53.js"; export default require_foo(); ----------- /out/chunk-X3UWZZCR.js ---------- +---------- /out/chunk-3FOKVS53.js ---------- // foo.js var require_foo = __commonJS({ "foo.js"(exports) { @@ -313,9 +313,9 @@ export { TestSplittingDynamicCommonJSIntoES6 ---------- /out/entry.js ---------- // entry.js -import("./foo-X6C7FV5C.js").then(({ default: { bar } }) => console.log(bar)); +import("./foo-FQOU3LFF.js").then(({ default: { bar } }) => console.log(bar)); ----------- /out/foo-X6C7FV5C.js ---------- +---------- /out/foo-FQOU3LFF.js ---------- // foo.js var require_foo = __commonJS({ "foo.js"(exports) { @@ -370,7 +370,7 @@ TestSplittingHybridESMAndCJSIssue617 import { foo, init_a -} from "./chunk-PDZFCFBH.js"; +} from "./chunk-OM5ZJZXR.js"; init_a(); export { foo @@ -381,7 +381,7 @@ import { __toCommonJS, a_exports, init_a -} from "./chunk-PDZFCFBH.js"; +} from "./chunk-OM5ZJZXR.js"; // b.js var bar = (init_a(), __toCommonJS(a_exports)); @@ -389,7 +389,7 @@ export { bar }; ----------- /out/chunk-PDZFCFBH.js ---------- +---------- /out/chunk-OM5ZJZXR.js ---------- // a.js var a_exports = {}; __export(a_exports, { @@ -540,7 +540,7 @@ TestSplittingSharedCommonJSIntoES6 ---------- /out/a.js ---------- import { require_shared -} from "./chunk-JQJBVS2P.js"; +} from "./chunk-H4QIU6J7.js"; // a.js var { foo } = require_shared(); @@ -549,13 +549,13 @@ console.log(foo); ---------- /out/b.js ---------- import { require_shared -} from "./chunk-JQJBVS2P.js"; +} from "./chunk-H4QIU6J7.js"; // b.js var { foo } = require_shared(); console.log(foo); ----------- /out/chunk-JQJBVS2P.js ---------- +---------- /out/chunk-H4QIU6J7.js ---------- // shared.js var require_shared = __commonJS({ "shared.js"(exports) { diff --git a/internal/bundler_tests/snapshots/snapshots_ts.txt b/internal/bundler_tests/snapshots/snapshots_ts.txt index b0cb8b80876..9a871a8480a 100644 --- a/internal/bundler_tests/snapshots/snapshots_ts.txt +++ b/internal/bundler_tests/snapshots/snapshots_ts.txt @@ -1684,8 +1684,10 @@ TestTSNamespaceKeepNames // entry.ts var ns; ((ns2) => { - ns2.foo = /* @__PURE__ */ __name(() => { - }, "foo"); + ns2.foo = /* @__PURE__ */ __firstValue({ + foo: () => { + } + }); function bar() { } ns2.bar = bar; @@ -1704,8 +1706,10 @@ TestTSNamespaceKeepNamesTargetES2015 // entry.ts var ns; ((ns2) => { - ns2.foo = /* @__PURE__ */ __name(() => { - }, "foo"); + ns2.foo = /* @__PURE__ */ __firstValue({ + foo: () => { + } + }); function bar() { } ns2.bar = bar; diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 4cc2e15f271..1f1d69e4351 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -10264,13 +10264,30 @@ func (p *parser) mangleIf(stmts []js_ast.Stmt, loc logger.Loc, s *js_ast.SIf) [] } func (p *parser) keepExprSymbolName(value js_ast.Expr, name string) js_ast.Expr { - value = p.callRuntime(value.Loc, "__name", []js_ast.Expr{value, + // For anonymous functions, transform to __firstValue({ foo: function() {} }) which allows native + // ECMAScript function name inference to apply (while still preventing the name from being removed + // by UglifyJS/Terser) + isAnonymousFnOrArrow := false + switch e := value.Data.(type) { + case *js_ast.EFunction: + isAnonymousFnOrArrow = (e.Fn.Name == nil) + case *js_ast.EArrow: + isAnonymousFnOrArrow = true + } + if isAnonymousFnOrArrow { + key := js_ast.Expr{Loc: value.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name)}} + obj := js_ast.Expr{Loc: value.Loc, Data: &js_ast.EObject{Properties: []js_ast.Property{{Key: key, ValueOrNil: value}}}} + call := p.callRuntime(value.Loc, "__firstValue", []js_ast.Expr{obj}) + call.Data.(*js_ast.ECall).CanBeUnwrappedIfUnused = true + return call + } + + // Otherwise, fall back to the __name helper + call := p.callRuntime(value.Loc, "__name", []js_ast.Expr{value, {Loc: value.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name)}}, }) - - // Make sure tree shaking removes this if the function is never used - value.Data.(*js_ast.ECall).CanBeUnwrappedIfUnused = true - return value + call.Data.(*js_ast.ECall).CanBeUnwrappedIfUnused = true + return call } func (p *parser) keepClassOrFnSymbolName(loc logger.Loc, expr js_ast.Expr, name string) js_ast.Stmt { diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 8b4b2c1ae39..d67d248d4d6 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -113,6 +113,7 @@ func Source(unsupportedJSFeatures compat.JSFeature) logger.Source { // Update the "name" property on the function or class for "--keep-names" export var __name = (target, value) => __defProp(target, 'name', { value, configurable: true }) + export var __firstValue = (obj) => { for (var k in obj) { if (__hasOwnProp.call(obj, k)) return obj[k]; } } // This fallback "require" function exists so that "typeof require" can // naturally be "function" even in non-CommonJS environments since esbuild diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index f8dfc49e299..cebf56f5716 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -1007,8 +1007,8 @@ export { const names2 = result2.outputFiles.map(x => path.basename(x.path)).sort() // Check that the public path is included in chunk hashes but not asset hashes - assert.deepStrictEqual(names1, ['data-BYATPJRB.bin', 'in-6QN3TZ3A.js']) - assert.deepStrictEqual(names2, ['data-BYATPJRB.bin', 'in-EJERHMG4.js']) + assert.deepStrictEqual(names1, ['data-BYATPJRB.bin', 'in-JK6YP3RC.js']) + assert.deepStrictEqual(names2, ['data-BYATPJRB.bin', 'in-4QE264NT.js']) }, async fileLoaderPublicPath({ esbuild, testDir }) {