From 7fb2b7a12dc8fba480e482444d15cd3eb0ed342c Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Tue, 9 Sep 2025 23:34:07 +0000 Subject: [PATCH] Implement typeof evaluation for defined globals at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major improvement: typeof expressions for defined globals like Bun are now evaluated at build time, enabling much better dead code elimination. Changes: - typeof Bun evaluates to "object" for --target=bun, "undefined" for --target=browser - typeof globalThis.Bun also evaluates at build time - Const expressions like `const isBun = typeof Bun !== 'undefined'` now become `var isBun = !0` (true) or `var isBun = !1` (false) at build time This enables automatic DCE for patterns like: - if (typeof Bun === "object") { /* bun code */ } - if (typeof Bun !== "undefined") { /* bun code */ } The implementation checks for defined identifiers and dot expressions in the typeof operator and replaces them with the appropriate string literal based on the define configuration. Note: While typeof is now evaluated, full constant propagation for local variables is not yet implemented, so patterns like: `const isBun = typeof Bun !== 'undefined'; if (isBun) { ... }` still require the if statement optimization separately. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/ast/visitExpr.zig | 48 +++++++++++++ test/bundler/bun-target-dce.test.ts | 104 ++++++++++++++++++++++++---- 2 files changed, 138 insertions(+), 14 deletions(-) diff --git a/src/ast/visitExpr.zig b/src/ast/visitExpr.zig index b499b56134..59824d5752 100644 --- a/src/ast/visitExpr.zig +++ b/src/ast/visitExpr.zig @@ -698,6 +698,54 @@ pub fn VisitExpr( switch (e_.op) { .un_typeof => { const id_before = e_.value.data == .e_identifier; + + // Check if this is a defined identifier BEFORE visiting it + // This allows us to evaluate typeof for defined globals like Bun + if (id_before) { + const ident = e_.value.data.e_identifier; + const name = p.loadNameFromRef(ident.ref); + + // Check if this identifier is defined + if (p.define.forIdentifier(name)) |def| { + // For truthy defines (like Bun in --target=bun), return "object" + if (def.is_truthy()) { + return p.newExpr(E.String{ .data = "object" }, expr.loc); + } + // For undefined defines (like Bun in --target=browser), return "undefined" + if (def.value == .e_undefined) { + return p.newExpr(E.String{ .data = "undefined" }, expr.loc); + } + // For other literal values, check their typeof + if (SideEffects.typeof(def.value)) |typeof_str| { + return p.newExpr(E.String{ .data = typeof_str }, expr.loc); + } + } + } + + // Check for dot expressions like typeof globalThis.Bun + if (e_.value.data == .e_dot) { + const dot = e_.value.data.e_dot; + if (p.define.dots.get(dot.name)) |parts| { + for (parts) |*define| { + if (p.isDotDefineMatch(e_.value, define.parts)) { + // For truthy defines (like globalThis.Bun in --target=bun), return "object" + if (define.data.is_truthy()) { + return p.newExpr(E.String{ .data = "object" }, expr.loc); + } + // For undefined defines (like globalThis.Bun in --target=browser), return "undefined" + if (define.data.value == .e_undefined) { + return p.newExpr(E.String{ .data = "undefined" }, expr.loc); + } + // For other literal values, check their typeof + if (SideEffects.typeof(define.data.value)) |typeof_str| { + return p.newExpr(E.String{ .data = typeof_str }, expr.loc); + } + break; + } + } + } + } + e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() }); const id_after = e_.value.data == .e_identifier; diff --git a/test/bundler/bun-target-dce.test.ts b/test/bundler/bun-target-dce.test.ts index 233a798071..1a4d4e65cb 100644 --- a/test/bundler/bun-target-dce.test.ts +++ b/test/bundler/bun-target-dce.test.ts @@ -163,16 +163,10 @@ var require_HASH = __commonJS((exports) => { process.versions.bun, exports.test3 = "process-versions-bun-exists"; exports.test3a = "process-isBun-true"; exports.test4 = "typeof-bun-defined"; - globalThis.Bun, exports.test5 = "typeof-globalThis-bun-defined"; + exports.test5 = "typeof-globalThis-bun-defined"; exports.test6 = "typeof-bun-reverse-defined"; - if (typeof Bun === "object") - exports.test6a = "typeof-bun-object"; - else - exports.test6a = "typeof-bun-not-object"; - if (typeof Bun !== "object") - exports.test6b = "typeof-bun-not-object-2"; - else - exports.test6b = "typeof-bun-object-2"; + exports.test6a = "typeof-bun-object"; + exports.test6b = "typeof-bun-object-2"; if (Bun.version) exports.test7 = "bun-version-exists"; else @@ -202,7 +196,7 @@ var require_HASH = __commonJS((exports) => { else exports.test17 = "node-version-missing"; exports.test18 = "window-missing"; - var isBun = typeof Bun !== "undefined"; + var isBun = !0; if (!isBun) exports.test19 = "const-not-bun"; else @@ -240,7 +234,10 @@ export default require_HASH();" // typeof window check is eliminated since window is undefined for bun target expect(bundled).not.toContain("typeof window"); - // Const patterns don't work (needs constant propagation) + // Const patterns: typeof is evaluated but const propagation not yet implemented + // The const isBun is now "var isBun = !0" (true) instead of "typeof Bun !== 'undefined'" + // But the if statement using isBun isn't optimized yet - needs constant propagation + expect(bundled).toContain("var isBun = !0"); expect(bundled).toContain("const-not-bun"); expect(bundled).toContain("const-is-bun"); }); @@ -392,8 +389,9 @@ if (isBun) { expect(bundled).toContain('exports.hasBun = !1'); // false expect(bundled).not.toContain('exports.hasBun = !0'); // true eliminated - // typeof Bun === "object" not fully optimized yet - both branches present - expect(bundled).toContain('typeof Bun === "object"'); + // typeof Bun === "object" should now be optimized (false for browser) + expect(bundled).toContain('exports.bunIsObject = !1'); // false + expect(bundled).not.toContain('exports.bunIsObject = !0'); // true eliminated expect(bundled).toContain('exports.isBun = !1'); // false expect(bundled).not.toContain('exports.isBun = !0'); // true eliminated @@ -408,4 +406,82 @@ if (isBun) { // Const pattern should ideally be optimized but might not work yet // For now just check it's there expect(bundled).toContain('constBun'); -}); \ No newline at end of file +}); +test("typeof Bun is evaluated at build time", async () => { + const code = ` +// Direct typeof comparison should be fully optimized +if (typeof Bun === "object") { + exports.test1 = "is-object"; +} else { + exports.test1 = "not-object"; +} + +if (typeof Bun !== "undefined") { + exports.test2 = "defined"; +} else { + exports.test2 = "undefined"; +} + +if (typeof globalThis.Bun === "object") { + exports.test3 = "global-is-object"; +} else { + exports.test3 = "global-not-object"; +} + +// Const assignment - typeof is evaluated but const propagation not yet done +const isBun = typeof Bun !== "undefined"; +exports.isBunValue = isBun; + `; + + // Test for --target=bun + using bunDir = tempDir("typeof-bun", { "index.js": code }); + await using bunProc = Bun.spawn({ + cmd: [bunExe(), "build", "index.js", "--target=bun", "--outfile=bundle.js", "--minify-syntax"], + env: bunEnv, + cwd: String(bunDir), + stderr: "pipe", + }); + expect(await bunProc.exited).toBe(0); + const bunBundle = await Bun.file(String(bunDir) + "/bundle.js").text(); + + // typeof Bun === "object" should be optimized to true + expect(bunBundle).toContain('exports.test1 = "is-object"'); + expect(bunBundle).not.toContain('exports.test1 = "not-object"'); + + // typeof Bun !== "undefined" should be optimized to true + expect(bunBundle).toContain('exports.test2 = "defined"'); + expect(bunBundle).not.toContain('exports.test2 = "undefined"'); + + // typeof globalThis.Bun === "object" should be optimized to true + expect(bunBundle).toContain('exports.test3 = "global-is-object"'); + expect(bunBundle).not.toContain('exports.test3 = "global-not-object"'); + + // Const isBun should be evaluated to true (minified as !0) + expect(bunBundle).toContain("var isBun = !0"); + + // Test for --target=browser + using browserDir = tempDir("typeof-browser", { "index.js": code }); + await using browserProc = Bun.spawn({ + cmd: [bunExe(), "build", "index.js", "--target=browser", "--outfile=bundle.js", "--minify-syntax"], + env: bunEnv, + cwd: String(browserDir), + stderr: "pipe", + }); + expect(await browserProc.exited).toBe(0); + const browserBundle = await Bun.file(String(browserDir) + "/bundle.js").text(); + + // typeof Bun === "object" should be optimized to false + expect(browserBundle).toContain('exports.test1 = "not-object"'); + expect(browserBundle).not.toContain('exports.test1 = "is-object"'); + + // typeof Bun !== "undefined" should be optimized to false + expect(browserBundle).toContain('exports.test2 = "undefined"'); + expect(browserBundle).not.toContain('exports.test2 = "defined"'); + + // typeof globalThis.Bun === "object" should be optimized to false + expect(browserBundle).toContain('exports.test3 = "global-not-object"'); + expect(browserBundle).not.toContain('exports.test3 = "global-is-object"'); + + // Const isBun should be evaluated to false (minified as !1) + expect(browserBundle).toContain("var isBun = !1"); +});