mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Add typeof Bun DCE support for --target=bun
- Handle typeof Bun !== 'undefined' and typeof Bun === 'undefined' patterns
- Support both direct identifier (typeof Bun) and dot expressions (typeof globalThis.Bun)
- Works with strict (===, !==) and loose (==, !=) equality operators
- Handles reverse comparisons ('undefined' === typeof Bun)
- Dead code elimination now works for all common Bun detection patterns
Test coverage includes:
- Direct Bun identifier checks
- globalThis.Bun property checks
- process.versions.bun property checks
- typeof Bun patterns (all variations)
All patterns properly eliminate dead code while preserving runtime values.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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": `
|
||||
|
||||
Reference in New Issue
Block a user