diff --git a/src/ast/SideEffects.zig b/src/ast/SideEffects.zig index ee046a5348..87152bebbc 100644 --- a/src/ast/SideEffects.zig +++ b/src/ast/SideEffects.zig @@ -764,6 +764,110 @@ pub const SideEffects = enum(u1) { } } } + + // Check for typeof Bun patterns in binary expressions + if (exp == .e_binary) { + const e_ = exp.e_binary; + switch (e_.op) { + .bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne => { + // Check for typeof Bun !== "undefined" or typeof Bun === "undefined" + if (e_.left.data == .e_unary) { + const unary = e_.left.data.e_unary; + if (unary.op == .un_typeof) { + // Handle typeof identifier (e.g., typeof Bun) + if (unary.value.data == .e_identifier) { + const ident = unary.value.data.e_identifier; + if (p.define.forIdentifier(p.loadNameFromRef(ident.ref))) |define| { + if (define.is_truthy()) { + // Bun is truthy, so typeof Bun is NOT "undefined" + if (e_.right.data == .e_string) { + const str = e_.right.data.e_string; + if (str.eqlComptime("undefined")) { + // typeof Bun === "undefined" -> false + // typeof Bun !== "undefined" -> true + const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne; + return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects }; + } + } + } + } + } + // Handle typeof dot expression (e.g., typeof globalThis.Bun) + if (unary.value.data == .e_dot) { + const dot = unary.value.data.e_dot; + if (p.define.dots.get(dot.name)) |parts| { + for (parts) |*define| { + if (p.isDotDefineMatch(unary.value, define.parts)) { + if (define.data.is_truthy()) { + // globalThis.Bun is truthy, so typeof globalThis.Bun is NOT "undefined" + if (e_.right.data == .e_string) { + const str = e_.right.data.e_string; + if (str.eqlComptime("undefined")) { + // typeof globalThis.Bun === "undefined" -> false + // typeof globalThis.Bun !== "undefined" -> true + const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne; + return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects }; + } + } + } + break; + } + } + } + } + } + } + // Also check the reverse: "undefined" === typeof Bun + if (e_.right.data == .e_unary) { + const unary = e_.right.data.e_unary; + if (unary.op == .un_typeof) { + // Handle typeof identifier (e.g., typeof Bun) + if (unary.value.data == .e_identifier) { + const ident = unary.value.data.e_identifier; + if (p.define.forIdentifier(p.loadNameFromRef(ident.ref))) |define| { + if (define.is_truthy()) { + // Bun is truthy, so typeof Bun is NOT "undefined" + if (e_.left.data == .e_string) { + const str = e_.left.data.e_string; + if (str.eqlComptime("undefined")) { + // "undefined" === typeof Bun -> false + // "undefined" !== typeof Bun -> true + const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne; + return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects }; + } + } + } + } + } + // Handle typeof dot expression (e.g., typeof globalThis.Bun) + if (unary.value.data == .e_dot) { + const dot = unary.value.data.e_dot; + if (p.define.dots.get(dot.name)) |parts| { + for (parts) |*define| { + if (p.isDotDefineMatch(unary.value, define.parts)) { + if (define.data.is_truthy()) { + // globalThis.Bun is truthy, so typeof globalThis.Bun is NOT "undefined" + if (e_.left.data == .e_string) { + const str = e_.left.data.e_string; + if (str.eqlComptime("undefined")) { + // "undefined" === typeof globalThis.Bun -> false + // "undefined" !== typeof globalThis.Bun -> true + const is_not_equal = e_.op == .bin_strict_ne or e_.op == .bin_loose_ne; + return Result{ .ok = true, .value = is_not_equal, .side_effects = .could_have_side_effects }; + } + } + } + break; + } + } + } + } + } + } + }, + else => {}, + } + } return toBooleanWithoutDCECheck(exp); } diff --git a/test/bundler/bun-target-dead-code-elimination.test.ts b/test/bundler/bun-target-dead-code-elimination.test.ts index 614ee441a7..0677998247 100644 --- a/test/bundler/bun-target-dead-code-elimination.test.ts +++ b/test/bundler/bun-target-dead-code-elimination.test.ts @@ -187,6 +187,73 @@ test("compare dead code elimination: --target=bun vs --target=node", async () => expect(nodeBundle).toContain("process.versions.node"); }); +test("dead code elimination for typeof Bun checks with --target=bun", async () => { + using dir = tempDir("bun-typeof-dce", { + "index.js": ` + // Test typeof Bun checks + if (typeof Bun !== "undefined") { + exports.test1 = "bun-typeof"; + } else { + exports.test1 = "not-bun-typeof"; + require("fs").writeFileSync("typeof-fail.txt", "should not exist"); + } + + // Test typeof globalThis.Bun checks + if (typeof globalThis.Bun !== "undefined") { + exports.test2 = "bun-typeof-global"; + } else { + exports.test2 = "not-bun-typeof-global"; + require("fs").writeFileSync("typeof-global-fail.txt", "should not exist"); + } + + // Test reverse order + if ("undefined" === typeof Bun) { + exports.test3 = "not-bun-reverse"; + require("fs").writeFileSync("reverse-fail.txt", "should not exist"); + } else { + exports.test3 = "bun-reverse"; + } + + // Test != instead of !== + if (typeof Bun != "undefined") { + exports.test4 = "bun-loose-ne"; + } else { + exports.test4 = "not-bun-loose-ne"; + } + `, + }); + + // Build with --target=bun + await using bundleProc = Bun.spawn({ + cmd: [bunExe(), "build", "index.js", "--target=bun", "--outfile=bundle.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const bundleCode = await bundleProc.exited; + expect(bundleCode).toBe(0); + + const bundled = await Bun.file(String(dir) + "/bundle.js").text(); + + // All "not-bun" branches should be eliminated + expect(bundled).not.toContain("not-bun-typeof"); + expect(bundled).not.toContain("not-bun-typeof-global"); + expect(bundled).not.toContain("not-bun-reverse"); + expect(bundled).not.toContain("not-bun-loose-ne"); + + // None of the fail files should be referenced + expect(bundled).not.toContain("typeof-fail.txt"); + expect(bundled).not.toContain("typeof-global-fail.txt"); + expect(bundled).not.toContain("reverse-fail.txt"); + + // The "bun" branches should remain + expect(bundled).toContain("bun-typeof"); + expect(bundled).toContain("bun-typeof-global"); + expect(bundled).toContain("bun-reverse"); + expect(bundled).toContain("bun-loose-ne"); +}); + test("--target=bun does not hardcode runtime values", async () => { using dir = tempDir("bun-no-hardcode", { "index.js": `