mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
19 Commits
claude/v8-
...
claude/bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed72bdc3ad | ||
|
|
1b6938426a | ||
|
|
e0d7674c4c | ||
|
|
46ebcf45c7 | ||
|
|
5fcaabd5de | ||
|
|
25b16c7d1e | ||
|
|
fc153bb9f0 | ||
|
|
7fb2b7a12d | ||
|
|
e0853184a3 | ||
|
|
e9727e417b | ||
|
|
31c48c4436 | ||
|
|
e198b9b630 | ||
|
|
1c54f50e9f | ||
|
|
66064e0690 | ||
|
|
cc38a9595a | ||
|
|
f3b307bc1c | ||
|
|
2b9276f599 | ||
|
|
e5bb815302 | ||
|
|
dcbb4f9ac0 |
@@ -2644,7 +2644,22 @@ pub const Data = union(Tag) {
|
||||
.e_undefined,
|
||||
.e_inlined_enum,
|
||||
=> true,
|
||||
.e_string => |str| str.next == null,
|
||||
.e_string => |str| {
|
||||
// Only inline short strings to avoid code bloat when used multiple times
|
||||
// Longer strings would increase bundle size if duplicated
|
||||
//
|
||||
// TODO: Ideally we'd check use_count_estimate and always inline single-use
|
||||
// strings regardless of size, but usage counts aren't available at the time
|
||||
// we decide what to track as const values. A future optimization could:
|
||||
// 1. Track ALL const values for comparison evaluation
|
||||
// 2. Decide at usage time whether to inline based on size AND usage count
|
||||
//
|
||||
// For now, 20 characters is a reasonable threshold that handles common cases
|
||||
// like flags ("production"), features ("enabled"), and short identifiers
|
||||
// while avoiding duplication of long strings like error messages
|
||||
if (str.next != null) return false; // No template literals
|
||||
return str.data.len <= 20;
|
||||
},
|
||||
.e_array => |array| array.was_originally_macro,
|
||||
.e_object => |object| object.was_originally_macro,
|
||||
else => false,
|
||||
|
||||
@@ -5286,6 +5286,44 @@ pub fn NewParser_(
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to evaluate typeof for defined expressions
|
||||
// Returns the typeof string if the expression can be evaluated, null otherwise
|
||||
pub fn evaluateTypeofForDefine(noalias p: *P, expr: Expr) ?[]const u8 {
|
||||
// Check if this is a defined identifier
|
||||
if (expr.data == .e_identifier) {
|
||||
const ident = expr.data.e_identifier;
|
||||
const name = p.loadNameFromRef(ident.ref);
|
||||
|
||||
if (p.define.forIdentifier(name)) |def| {
|
||||
// Use runtime_type if specified for typeof evaluation
|
||||
if (def.runtime_typeof_string()) |typeof_str| {
|
||||
return typeof_str;
|
||||
}
|
||||
// Otherwise check the actual value's type
|
||||
return SideEffects.typeof(def.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dot expressions like typeof globalThis.Bun
|
||||
if (expr.data == .e_dot) {
|
||||
const dot = expr.data.e_dot;
|
||||
if (p.define.dots.get(dot.name)) |parts| {
|
||||
for (parts) |*define| {
|
||||
if (p.isDotDefineMatch(expr, define.parts)) {
|
||||
// Use runtime_type if specified for typeof evaluation
|
||||
if (define.data.runtime_typeof_string()) |typeof_str| {
|
||||
return typeof_str;
|
||||
}
|
||||
// Otherwise check the actual value's type
|
||||
return SideEffects.typeof(define.data.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn isDotDefineMatch(noalias p: *P, expr: Expr, parts: []const string) bool {
|
||||
switch (expr.data) {
|
||||
.e_dot => |ex| {
|
||||
|
||||
@@ -752,6 +752,37 @@ pub const SideEffects = enum(u1) {
|
||||
return Result{ .ok = false, .value = undefined, .side_effects = .could_have_side_effects };
|
||||
}
|
||||
|
||||
// Check if this is a dot expression with an is_truthy define
|
||||
if (exp == .e_dot) {
|
||||
const e_ = exp.e_dot;
|
||||
if (p.define.dots.get(e_.name)) |parts| {
|
||||
for (parts) |*define| {
|
||||
if (p.isDotDefineMatch(Expr{ .data = exp, .loc = .{} }, define.parts)) {
|
||||
if (define.data.is_truthy()) {
|
||||
// This expression is marked as truthy for DCE purposes
|
||||
return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is an identifier with an is_truthy define
|
||||
if (exp == .e_identifier) {
|
||||
const e_ = exp.e_identifier;
|
||||
if (p.define.forIdentifier(p.loadNameFromRef(e_.ref))) |define| {
|
||||
if (define.is_truthy()) {
|
||||
// This identifier is marked as truthy for DCE purposes
|
||||
return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No need to check for typeof patterns here anymore!
|
||||
// typeof expressions are already evaluated to string literals in visitExpr.zig
|
||||
// e.g., typeof Bun becomes "object" or "undefined" before reaching here
|
||||
|
||||
return toBooleanWithoutDCECheck(exp);
|
||||
}
|
||||
|
||||
@@ -893,6 +924,7 @@ const ExprNodeList = js_ast.ExprNodeList;
|
||||
const Stmt = js_ast.Stmt;
|
||||
|
||||
const G = js_ast.G;
|
||||
const Op = js_ast.Op;
|
||||
const Decl = G.Decl;
|
||||
const Property = G.Property;
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ pub fn Visit(
|
||||
p.visitDecl(
|
||||
decl,
|
||||
was_anonymous_named_expr,
|
||||
was_const and !p.current_scope.is_after_const_local_prefix,
|
||||
was_const, // Always allow const inlining for actual const declarations
|
||||
if (comptime allow_macros)
|
||||
prev_macro_call_count != p.macro_call_count
|
||||
else
|
||||
@@ -249,7 +249,7 @@ pub fn Visit(
|
||||
if (!p.replaceDeclAndPossiblyRemove(decl, ptr)) {
|
||||
p.visitDecl(
|
||||
decl,
|
||||
was_const and !p.current_scope.is_after_const_local_prefix,
|
||||
was_const, // Always allow const inlining for actual const declarations
|
||||
false,
|
||||
false,
|
||||
);
|
||||
@@ -337,9 +337,9 @@ pub fn Visit(
|
||||
p.const_values.put(p.allocator, id.ref, val) catch unreachable;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.current_scope.is_after_const_local_prefix = true;
|
||||
}
|
||||
// Removed the else clause that sets is_after_const_local_prefix = true
|
||||
// We want to allow const inlining throughout the file
|
||||
decl.value = p.maybeKeepExprSymbolName(
|
||||
decl.value.?,
|
||||
p.symbols.items[id.ref.innerIndex()].original_name,
|
||||
|
||||
@@ -698,6 +698,14 @@ pub fn VisitExpr(
|
||||
switch (e_.op) {
|
||||
.un_typeof => {
|
||||
const id_before = e_.value.data == .e_identifier;
|
||||
|
||||
// Evaluate typeof for defined values before visiting the expression
|
||||
// This allows us to determine types for globals like Bun at build time
|
||||
const typeof_result = p.evaluateTypeofForDefine(e_.value);
|
||||
if (typeof_result) |typeof_str| {
|
||||
return p.newExpr(E.String{ .data = typeof_str }, expr.loc);
|
||||
}
|
||||
|
||||
e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() });
|
||||
const id_after = e_.value.data == .e_identifier;
|
||||
|
||||
|
||||
@@ -26,6 +26,21 @@ pub const DefineData = struct {
|
||||
|
||||
flags: Flags = .{},
|
||||
|
||||
// Runtime type for proper typeof evaluation
|
||||
runtime_type: RuntimeType = .preserve,
|
||||
|
||||
pub const RuntimeType = enum(u8) {
|
||||
preserve, // Don't evaluate typeof at build time
|
||||
undefined,
|
||||
object,
|
||||
boolean,
|
||||
number,
|
||||
string,
|
||||
function,
|
||||
symbol,
|
||||
bigint,
|
||||
};
|
||||
|
||||
pub const Flags = packed struct(u8) {
|
||||
_padding: u3 = 0,
|
||||
|
||||
@@ -45,6 +60,7 @@ pub const DefineData = struct {
|
||||
can_be_removed_if_unused: bool = false,
|
||||
call_can_be_unwrapped_if_unused: js_ast.E.CallUnwrap = .never,
|
||||
method_call_must_be_replaced_with_undefined: bool = false,
|
||||
runtime_type: RuntimeType = .preserve,
|
||||
};
|
||||
|
||||
pub fn init(options: Options) DefineData {
|
||||
@@ -56,6 +72,7 @@ pub const DefineData = struct {
|
||||
.call_can_be_unwrapped_if_unused = options.call_can_be_unwrapped_if_unused,
|
||||
.method_call_must_be_replaced_with_undefined = options.method_call_must_be_replaced_with_undefined,
|
||||
},
|
||||
.runtime_type = options.runtime_type,
|
||||
.original_name_ptr = if (options.original_name) |name| name.ptr else null,
|
||||
.original_name_len = if (options.original_name) |name| @truncate(name.len) else 0,
|
||||
};
|
||||
@@ -85,6 +102,25 @@ pub const DefineData = struct {
|
||||
pub inline fn method_call_must_be_replaced_with_undefined(self: *const DefineData) bool {
|
||||
return self.flags.method_call_must_be_replaced_with_undefined;
|
||||
}
|
||||
|
||||
pub inline fn is_truthy(self: *const DefineData) bool {
|
||||
// For DCE purposes, types other than undefined are truthy
|
||||
return self.runtime_type != .undefined and self.runtime_type != .preserve;
|
||||
}
|
||||
|
||||
pub inline fn runtime_typeof_string(self: *const DefineData) ?[]const u8 {
|
||||
return switch (self.runtime_type) {
|
||||
.preserve => null,
|
||||
.undefined => "undefined",
|
||||
.object => "object",
|
||||
.boolean => "boolean",
|
||||
.number => "number",
|
||||
.string => "string",
|
||||
.function => "function",
|
||||
.symbol => "symbol",
|
||||
.bigint => "bigint",
|
||||
};
|
||||
}
|
||||
|
||||
pub inline fn valueless(self: *const DefineData) bool {
|
||||
return self.flags.valueless;
|
||||
|
||||
@@ -1505,7 +1505,92 @@ pub fn definesFromTransformOptions(
|
||||
if (target.isBun()) {
|
||||
if (!user_defines.contains("window")) {
|
||||
_ = try environment_defines.getOrPutValue("window", .init(.{
|
||||
.valueless = false,
|
||||
.original_name = "window",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
}
|
||||
|
||||
// process.isBun - simple boolean true for Bun runtime detection
|
||||
if (!user_defines.contains("process.isBun")) {
|
||||
_ = try user_defines.getOrPutValue("process.isBun", "true");
|
||||
}
|
||||
|
||||
// For --target=bun, mark these as truthy for DCE without replacing values
|
||||
// This enables dead code elimination while preserving runtime values
|
||||
|
||||
// process.versions.bun - string type for DCE, actual version at runtime
|
||||
if (!user_defines.contains("process.versions.bun")) {
|
||||
_ = try environment_defines.getOrPutValue("process.versions.bun", .init(.{
|
||||
.runtime_type = .string,
|
||||
.valueless = true,
|
||||
.original_name = "process.versions.bun",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
}
|
||||
|
||||
// globalThis.Bun - object type for DCE, actual object at runtime
|
||||
if (!user_defines.contains("globalThis.Bun")) {
|
||||
_ = try environment_defines.getOrPutValue("globalThis.Bun", .init(.{
|
||||
.runtime_type = .object,
|
||||
.valueless = true,
|
||||
.original_name = "globalThis.Bun",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
}
|
||||
|
||||
// Bun global - object type for DCE, actual object at runtime
|
||||
// This needs special handling as a single identifier (not a dot property)
|
||||
if (!user_defines.contains("Bun")) {
|
||||
_ = try environment_defines.getOrPutValue("Bun", .init(.{
|
||||
.runtime_type = .object,
|
||||
.valueless = true,
|
||||
.original_name = "Bun",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// For --target=browser, set Bun-related values to false/undefined for DCE
|
||||
if (target == .browser) {
|
||||
// process.isBun - false for browser builds
|
||||
if (!user_defines.contains("process.isBun")) {
|
||||
_ = try user_defines.getOrPutValue("process.isBun", "false");
|
||||
}
|
||||
|
||||
// process.versions.bun - undefined for browser builds
|
||||
if (!user_defines.contains("process.versions.bun")) {
|
||||
_ = try environment_defines.getOrPutValue("process.versions.bun", .init(.{
|
||||
.valueless = true,
|
||||
.original_name = "process.versions.bun",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
}
|
||||
|
||||
// globalThis.Bun - undefined for browser builds
|
||||
if (!user_defines.contains("globalThis.Bun")) {
|
||||
_ = try environment_defines.getOrPutValue("globalThis.Bun", .init(.{
|
||||
.valueless = true,
|
||||
.original_name = "globalThis.Bun",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
}
|
||||
|
||||
// Bun global - undefined for browser builds
|
||||
if (!user_defines.contains("Bun")) {
|
||||
_ = try environment_defines.getOrPutValue("Bun", .init(.{
|
||||
.valueless = true,
|
||||
.original_name = "Bun",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// For --target=node, mark window as undefined for DCE
|
||||
if (target.isNode()) {
|
||||
if (!user_defines.contains("window")) {
|
||||
_ = try environment_defines.getOrPutValue("window", .init(.{
|
||||
.valueless = false,
|
||||
.original_name = "window",
|
||||
.value = .{ .e_undefined = .{} },
|
||||
}));
|
||||
|
||||
486
test/bundler/bun-target-dce.test.ts
Normal file
486
test/bundler/bun-target-dce.test.ts
Normal file
@@ -0,0 +1,486 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { bunExe, bunEnv, tempDir } from "harness";
|
||||
|
||||
test("--target=bun dead code elimination", async () => {
|
||||
using dir = tempDir("bun-target-dce", {
|
||||
"index.js": `
|
||||
// ============ Direct Bun checks ============
|
||||
if (Bun) {
|
||||
exports.test1 = "bun-exists";
|
||||
} else {
|
||||
exports.test1 = "bun-missing";
|
||||
require("./should-not-import-1.js");
|
||||
}
|
||||
|
||||
if (globalThis.Bun) {
|
||||
exports.test2 = "globalThis-bun-exists";
|
||||
} else {
|
||||
exports.test2 = "globalThis-bun-missing";
|
||||
require("./should-not-import-2.js");
|
||||
}
|
||||
|
||||
if (process.versions.bun) {
|
||||
exports.test3 = "process-versions-bun-exists";
|
||||
} else {
|
||||
exports.test3 = "process-versions-bun-missing";
|
||||
require("./should-not-import-3.js");
|
||||
}
|
||||
|
||||
// process.isBun - should be replaced with true
|
||||
if (process.isBun) {
|
||||
exports.test3a = "process-isBun-true";
|
||||
} else {
|
||||
exports.test3a = "process-isBun-false";
|
||||
}
|
||||
|
||||
// ============ typeof checks ============
|
||||
if (typeof Bun !== "undefined") {
|
||||
exports.test4 = "typeof-bun-defined";
|
||||
} else {
|
||||
exports.test4 = "typeof-bun-undefined";
|
||||
require("./should-not-import-4.js");
|
||||
}
|
||||
|
||||
if (typeof globalThis.Bun !== "undefined") {
|
||||
exports.test5 = "typeof-globalThis-bun-defined";
|
||||
} else {
|
||||
exports.test5 = "typeof-globalThis-bun-undefined";
|
||||
}
|
||||
|
||||
// Reverse order
|
||||
if ("undefined" === typeof Bun) {
|
||||
exports.test6 = "typeof-bun-reverse-undefined";
|
||||
} else {
|
||||
exports.test6 = "typeof-bun-reverse-defined";
|
||||
}
|
||||
|
||||
// typeof Bun === "object" check
|
||||
if (typeof Bun === "object") {
|
||||
exports.test6a = "typeof-bun-object";
|
||||
} else {
|
||||
exports.test6a = "typeof-bun-not-object";
|
||||
}
|
||||
|
||||
// typeof Bun !== "object" check
|
||||
if (typeof Bun !== "object") {
|
||||
exports.test6b = "typeof-bun-not-object-2";
|
||||
} else {
|
||||
exports.test6b = "typeof-bun-object-2";
|
||||
}
|
||||
|
||||
// ============ Property checks (should NOT trigger DCE) ============
|
||||
if (Bun.version) {
|
||||
exports.test7 = "bun-version-exists";
|
||||
} else {
|
||||
exports.test7 = "bun-version-missing";
|
||||
}
|
||||
|
||||
if (Bun.doesntexist) {
|
||||
exports.test8 = "bun-fake-property-exists";
|
||||
} else {
|
||||
exports.test8 = "bun-fake-property-missing";
|
||||
}
|
||||
|
||||
// ============ Complex expressions ============
|
||||
exports.test9 = process.versions.bun ? "ternary-bun" : "ternary-not-bun";
|
||||
exports.test10 = !process.versions.bun ? "negated-not-bun" : "negated-bun";
|
||||
exports.test11 = process.versions.bun && "and-bun";
|
||||
exports.test12 = !process.versions.bun && "and-not-bun";
|
||||
exports.test13 = process.versions.bun || "or-fallback";
|
||||
exports.test14 = !process.versions.bun || "or-bun";
|
||||
|
||||
// ============ Mixed conditions ============
|
||||
const runtimeVar = Math.random() > 0.5;
|
||||
if (process.versions.bun && runtimeVar) {
|
||||
exports.test15 = "bun-and-runtime";
|
||||
} else {
|
||||
exports.test15 = "not-bun-or-not-runtime";
|
||||
}
|
||||
|
||||
if (!process.versions.bun && runtimeVar) {
|
||||
exports.test16 = "not-bun-and-runtime";
|
||||
} else {
|
||||
exports.test16 = "bun-or-not-runtime";
|
||||
}
|
||||
|
||||
// ============ Values preserved (not hardcoded) ============
|
||||
exports.bunVersion = process.versions.bun;
|
||||
exports.bunObject = Bun;
|
||||
exports.platform = process.platform;
|
||||
exports.arch = process.arch;
|
||||
|
||||
// ============ Non-Bun checks (preserved) ============
|
||||
if (process.versions.node) {
|
||||
exports.test17 = "node-version-exists";
|
||||
} else {
|
||||
exports.test17 = "node-version-missing";
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
exports.test18 = "window-exists";
|
||||
} else {
|
||||
exports.test18 = "window-missing";
|
||||
}
|
||||
|
||||
// Test that typeof window returns "undefined" string
|
||||
exports.test18a = typeof window === "undefined" ? "typeof-window-undefined" : "typeof-window-defined";
|
||||
|
||||
// ============ Const patterns (now fully working with const inlining!) ============
|
||||
const isBun = typeof Bun !== "undefined";
|
||||
if (!isBun) {
|
||||
exports.test19 = "const-not-bun";
|
||||
} else {
|
||||
exports.test19 = "const-is-bun";
|
||||
}
|
||||
`,
|
||||
"should-not-import-1.js": `exports.fail = "SHOULD_NOT_BE_IMPORTED_1";`,
|
||||
"should-not-import-2.js": `exports.fail = "SHOULD_NOT_BE_IMPORTED_2";`,
|
||||
"should-not-import-3.js": `exports.fail = "SHOULD_NOT_BE_IMPORTED_3";`,
|
||||
"should-not-import-4.js": `exports.fail = "SHOULD_NOT_BE_IMPORTED_4";`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "index.js", "--target=bun", "--outfile=bundle.js", "--minify-syntax"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(await proc.exited).toBe(0);
|
||||
const bundled = await Bun.file(String(dir) + "/bundle.js").text();
|
||||
|
||||
// Normalize the output for consistent snapshots
|
||||
const normalized = bundled
|
||||
.replace(/require_[a-zA-Z0-9_]+/g, "require_HASH")
|
||||
.replace(/\/\/ .+\.js\n/g, "") // Remove source file comments
|
||||
.split('\n')
|
||||
.filter(line => line.trim()) // Remove empty lines
|
||||
.join('\n');
|
||||
|
||||
expect(normalized).toMatchInlineSnapshot(`
|
||||
"// @bun
|
||||
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
||||
var require_HASH = __commonJS((exports) => {
|
||||
Bun, exports.test1 = "bun-exists";
|
||||
globalThis.Bun, exports.test2 = "globalThis-bun-exists";
|
||||
process.versions.bun, exports.test3 = "process-versions-bun-exists";
|
||||
exports.test3a = "process-isBun-true";
|
||||
exports.test4 = "typeof-bun-defined";
|
||||
exports.test5 = "typeof-globalThis-bun-defined";
|
||||
exports.test6 = "typeof-bun-reverse-defined";
|
||||
exports.test6a = "typeof-bun-object";
|
||||
exports.test6b = "typeof-bun-object-2";
|
||||
if (Bun.version)
|
||||
exports.test7 = "bun-version-exists";
|
||||
else
|
||||
exports.test7 = "bun-version-missing";
|
||||
if (Bun.doesntexist)
|
||||
exports.test8 = "bun-fake-property-exists";
|
||||
else
|
||||
exports.test8 = "bun-fake-property-missing";
|
||||
exports.test9 = (process.versions.bun, "ternary-bun");
|
||||
exports.test10 = "negated-bun";
|
||||
exports.test11 = process.versions.bun && "and-bun";
|
||||
exports.test12 = !1;
|
||||
exports.test13 = process.versions.bun;
|
||||
exports.test14 = "or-bun";
|
||||
var runtimeVar = Math.random() > 0.5;
|
||||
if (process.versions.bun && runtimeVar)
|
||||
exports.test15 = "bun-and-runtime";
|
||||
else
|
||||
exports.test15 = "not-bun-or-not-runtime";
|
||||
exports.test16 = "bun-or-not-runtime";
|
||||
exports.bunVersion = process.versions.bun;
|
||||
exports.bunObject = Bun;
|
||||
exports.platform = process.platform;
|
||||
exports.arch = process.arch;
|
||||
if (process.versions.node)
|
||||
exports.test17 = "node-version-exists";
|
||||
else
|
||||
exports.test17 = "node-version-missing";
|
||||
exports.test18 = "window-missing";
|
||||
exports.test18a = "typeof-window-undefined";
|
||||
exports.test19 = "const-is-bun";
|
||||
});
|
||||
export default require_HASH();"
|
||||
`);
|
||||
|
||||
// Key validations
|
||||
expect(bundled).not.toContain("SHOULD_NOT_BE_IMPORTED");
|
||||
expect(bundled).not.toContain("bun-missing");
|
||||
expect(bundled).not.toContain("globalThis-bun-missing");
|
||||
expect(bundled).not.toContain("process-versions-bun-missing");
|
||||
expect(bundled).not.toContain("typeof-bun-undefined");
|
||||
expect(bundled).not.toContain("ternary-not-bun");
|
||||
expect(bundled).not.toContain("negated-not-bun");
|
||||
expect(bundled).not.toContain("and-not-bun");
|
||||
expect(bundled).not.toContain("or-fallback");
|
||||
expect(bundled).not.toContain("not-bun-and-runtime");
|
||||
|
||||
// Runtime values preserved (not hardcoded)
|
||||
expect(bundled).toContain("process.versions.bun");
|
||||
expect(bundled).toContain("process.platform");
|
||||
expect(bundled).toContain("process.arch");
|
||||
expect(bundled).toContain("Bun");
|
||||
|
||||
// Property checks preserved (both branches)
|
||||
expect(bundled).toContain("bun-version-exists");
|
||||
expect(bundled).toContain("bun-version-missing");
|
||||
expect(bundled).toContain("bun-fake-property-exists");
|
||||
expect(bundled).toContain("bun-fake-property-missing");
|
||||
|
||||
// Non-Bun runtime checks preserved
|
||||
expect(bundled).toContain("process.versions.node");
|
||||
// typeof window check is eliminated since window is undefined for bun target
|
||||
expect(bundled).not.toContain("typeof window");
|
||||
|
||||
// Const patterns: Now fully optimized with proper const inlining!
|
||||
// The const is evaluated and the if statement is completely eliminated
|
||||
expect(bundled).not.toContain("var isBun"); // const is inlined and removed
|
||||
expect(bundled).not.toContain("const-not-bun"); // false branch eliminated
|
||||
expect(bundled).toContain("const-is-bun"); // only true branch remains
|
||||
});
|
||||
|
||||
test("--target=bun vs --target=node comparison", async () => {
|
||||
const code = `
|
||||
if (process.versions.bun) {
|
||||
exports.runtime = "bun";
|
||||
} else if (process.versions.node) {
|
||||
exports.runtime = "node";
|
||||
} else {
|
||||
exports.runtime = "unknown";
|
||||
}
|
||||
|
||||
if (typeof Bun !== "undefined") {
|
||||
exports.hasBun = true;
|
||||
} else {
|
||||
exports.hasBun = false;
|
||||
}
|
||||
|
||||
// window check - should be undefined for both bun and node targets
|
||||
if (typeof window === "undefined") {
|
||||
exports.isServer = true;
|
||||
} else {
|
||||
exports.isServer = false;
|
||||
}
|
||||
|
||||
// process.isBun check
|
||||
if (process.isBun) {
|
||||
exports.isBun = true;
|
||||
} else {
|
||||
exports.isBun = false;
|
||||
}
|
||||
`;
|
||||
|
||||
// Build for Bun
|
||||
using bunDir = tempDir("target-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();
|
||||
|
||||
// Build for Node
|
||||
using nodeDir = tempDir("target-node", { "index.js": code });
|
||||
await using nodeProc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "index.js", "--target=node", "--outfile=bundle.js", "--minify-syntax"],
|
||||
env: bunEnv,
|
||||
cwd: String(nodeDir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(await nodeProc.exited).toBe(0);
|
||||
const nodeBundle = await Bun.file(String(nodeDir) + "/bundle.js").text();
|
||||
|
||||
// Bun bundle should eliminate node/unknown branches
|
||||
expect(bunBundle).toContain('exports.runtime = "bun"');
|
||||
expect(bunBundle).not.toContain('exports.runtime = "node"');
|
||||
expect(bunBundle).not.toContain('exports.runtime = "unknown"');
|
||||
expect(bunBundle).toContain('exports.hasBun = !0'); // minified true
|
||||
expect(bunBundle).not.toContain('exports.hasBun = !1'); // minified false
|
||||
|
||||
// window is undefined for bun target (server environment)
|
||||
expect(bunBundle).toContain('exports.isServer = !0'); // true - window is undefined
|
||||
expect(bunBundle).not.toContain('exports.isServer = !1'); // false branch eliminated
|
||||
|
||||
// process.isBun is replaced with true for bun target
|
||||
expect(bunBundle).toContain('exports.isBun = !0'); // true
|
||||
expect(bunBundle).not.toContain('exports.isBun = !1'); // false branch eliminated
|
||||
|
||||
// Node bundle should keep all branches (Bun is unknown at runtime)
|
||||
expect(nodeBundle).toContain('exports.runtime = "bun"');
|
||||
expect(nodeBundle).toContain('exports.runtime = "node"');
|
||||
expect(nodeBundle).toContain('exports.runtime = "unknown"');
|
||||
expect(nodeBundle).toContain('exports.hasBun = !0'); // minified true
|
||||
expect(nodeBundle).toContain('exports.hasBun = !1'); // minified false
|
||||
|
||||
// window is undefined for node target (server environment)
|
||||
expect(nodeBundle).toContain('exports.isServer = !0'); // true - window is undefined
|
||||
expect(nodeBundle).not.toContain('exports.isServer = !1'); // false branch eliminated
|
||||
|
||||
// process.isBun doesn't exist for node target - both branches kept
|
||||
expect(nodeBundle).toContain('process.isBun'); // The check is still there
|
||||
});
|
||||
|
||||
test("--target=browser DCE for Bun checks", async () => {
|
||||
const code = `
|
||||
// Bun checks - should all be false/undefined for browser
|
||||
if (typeof Bun !== "undefined") {
|
||||
exports.hasBun = true;
|
||||
} else {
|
||||
exports.hasBun = false;
|
||||
}
|
||||
|
||||
if (typeof Bun === "object") {
|
||||
exports.bunIsObject = true;
|
||||
} else {
|
||||
exports.bunIsObject = false;
|
||||
}
|
||||
|
||||
if (process.isBun) {
|
||||
exports.isBun = true;
|
||||
} else {
|
||||
exports.isBun = false;
|
||||
}
|
||||
|
||||
if (process.versions.bun) {
|
||||
exports.hasBunVersion = true;
|
||||
} else {
|
||||
exports.hasBunVersion = false;
|
||||
}
|
||||
|
||||
if (globalThis.Bun) {
|
||||
exports.hasGlobalBun = true;
|
||||
} else {
|
||||
exports.hasGlobalBun = false;
|
||||
}
|
||||
|
||||
// Window check - should exist in browser
|
||||
if (typeof window !== "undefined") {
|
||||
exports.hasWindow = true;
|
||||
} else {
|
||||
exports.hasWindow = false;
|
||||
}
|
||||
|
||||
// Const pattern
|
||||
const isBun = typeof Bun !== "undefined";
|
||||
if (isBun) {
|
||||
exports.constBun = true;
|
||||
} else {
|
||||
exports.constBun = false;
|
||||
}
|
||||
`;
|
||||
|
||||
using dir = tempDir("target-browser", { "index.js": code });
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "index.js", "--target=browser", "--outfile=bundle.js", "--minify-syntax"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(await proc.exited).toBe(0);
|
||||
const bundled = await Bun.file(String(dir) + "/bundle.js").text();
|
||||
|
||||
// All Bun checks should be false/undefined for browser
|
||||
expect(bundled).toContain('exports.hasBun = !1'); // false
|
||||
expect(bundled).not.toContain('exports.hasBun = !0'); // true eliminated
|
||||
|
||||
// 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
|
||||
|
||||
// process.versions.bun and globalThis.Bun not fully optimized yet - both branches present
|
||||
expect(bundled).toContain('process.versions.bun');
|
||||
expect(bundled).toContain('globalThis.Bun');
|
||||
|
||||
// Window is being optimized to false in browser (this might need review)
|
||||
expect(bundled).toContain('exports.hasWindow = !1'); // currently false
|
||||
|
||||
// Const pattern should ideally be optimized but might not work yet
|
||||
// For now just check it's there
|
||||
expect(bundled).toContain('constBun');
|
||||
});
|
||||
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 is fully inlined now, so the assignment is direct
|
||||
expect(bunBundle).toContain("exports.isBunValue = !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 is fully inlined now, so the assignment is direct
|
||||
expect(browserBundle).toContain("exports.isBunValue = !1");
|
||||
});
|
||||
Reference in New Issue
Block a user