mirror of
https://github.com/oven-sh/bun
synced 2026-02-20 07:42:30 +00:00
Compare commits
1 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23f8098e63 |
@@ -12,7 +12,7 @@ pub const Parser = struct {
|
||||
keep_names: bool = true,
|
||||
ignore_dce_annotations: bool = false,
|
||||
preserve_unused_imports_ts: bool = false,
|
||||
use_define_for_class_fields: bool = false,
|
||||
use_define_for_class_fields: bool = true,
|
||||
suppress_warnings_about_weird_code: bool = true,
|
||||
filepath_hash_for_hmr: u32 = 0,
|
||||
features: RuntimeFeatures = .{},
|
||||
|
||||
@@ -682,7 +682,6 @@ pub fn Visit(
|
||||
}
|
||||
}
|
||||
|
||||
// note: our version assumes useDefineForClassFields is true
|
||||
if (comptime is_typescript_enabled) {
|
||||
if (constructor_function) |constructor| {
|
||||
var to_add: usize = 0;
|
||||
@@ -744,6 +743,95 @@ pub fn Visit(
|
||||
constructor.func.body.stmts = stmts.items;
|
||||
}
|
||||
}
|
||||
|
||||
// When "useDefineForClassFields" is false (legacy TypeScript behavior):
|
||||
// - Type-only fields (no initializer) are stripped entirely
|
||||
// - Instance fields with initializers are moved to constructor assignments
|
||||
if (!p.options.use_define_for_class_fields) {
|
||||
// Count instance fields with initializers that need to be moved to constructor
|
||||
var instance_field_init_count: usize = 0;
|
||||
for (class.properties) |property| {
|
||||
if (property.flags.contains(.is_method) or property.kind == .class_static_block) continue;
|
||||
if (property.flags.contains(.is_static)) continue;
|
||||
// Skip private fields - they don't interact with prototype setters
|
||||
if (property.key != null and @as(Expr.Tag, property.key.?.data) == .e_private_identifier) continue;
|
||||
if (property.initializer != null) {
|
||||
instance_field_init_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Move instance field initializers to the constructor if there's one
|
||||
if (instance_field_init_count > 0 and constructor_function != null) {
|
||||
const ctor = constructor_function.?;
|
||||
var ctor_stmts = std.array_list.Managed(Stmt).fromOwnedSlice(p.allocator, ctor.func.body.stmts);
|
||||
ctor_stmts.ensureUnusedCapacity(instance_field_init_count) catch unreachable;
|
||||
|
||||
// Find super() call index for derived classes
|
||||
var ctor_super_index: ?usize = null;
|
||||
if (class.extends != null) {
|
||||
for (ctor_stmts.items, 0..) |stmt, index| {
|
||||
if (stmt.data == .s_expr and stmt.data.s_expr.value.data == .e_call and stmt.data.s_expr.value.data.e_call.target.data == .e_super) {
|
||||
ctor_super_index = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var insert_offset: usize = 0;
|
||||
for (class.properties) |property| {
|
||||
if (property.flags.contains(.is_method) or property.kind == .class_static_block) continue;
|
||||
if (property.flags.contains(.is_static)) continue;
|
||||
if (property.key != null and @as(Expr.Tag, property.key.?.data) == .e_private_identifier) continue;
|
||||
if (property.initializer) |init_val| {
|
||||
const key_loc = if (property.key) |k| k.loc else logger.Loc{};
|
||||
const field_name: []const u8 = if (property.key) |k| switch (k.data) {
|
||||
.e_string => |s| s.string(p.allocator) catch "",
|
||||
.e_identifier => |ident| p.symbols.items[ident.ref.innerIndex()].original_name,
|
||||
else => "",
|
||||
} else "";
|
||||
ctor_stmts.insert(
|
||||
if (ctor_super_index) |k| insert_offset + k + 1 else insert_offset,
|
||||
Stmt.assign(
|
||||
p.newExpr(E.Dot{
|
||||
.target = p.newExpr(E.This{}, key_loc),
|
||||
.name = field_name,
|
||||
.name_loc = key_loc,
|
||||
}, key_loc),
|
||||
init_val,
|
||||
),
|
||||
) catch unreachable;
|
||||
insert_offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
ctor.func.body.stmts = ctor_stmts.items;
|
||||
}
|
||||
|
||||
// Remove non-private, non-static instance fields from class body.
|
||||
// Type-only fields (no initializer) are just type annotations.
|
||||
// Fields with initializers have been moved to the constructor above.
|
||||
// Keep: methods, static blocks, static fields, private fields.
|
||||
var write_idx: usize = 0;
|
||||
for (class.properties) |property| {
|
||||
if (property.flags.contains(.is_method) or property.kind == .class_static_block) {
|
||||
class.properties[write_idx] = property;
|
||||
write_idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (property.flags.contains(.is_static)) {
|
||||
class.properties[write_idx] = property;
|
||||
write_idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (property.key != null and @as(Expr.Tag, property.key.?.data) == .e_private_identifier) {
|
||||
class.properties[write_idx] = property;
|
||||
write_idx += 1;
|
||||
continue;
|
||||
}
|
||||
// Non-private, non-static instance field: remove from class body
|
||||
}
|
||||
class.properties = class.properties[0..write_idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ pub fn transpileSourceCode(
|
||||
.jsx = jsc_vm.transpiler.options.jsx,
|
||||
.emit_decorator_metadata = jsc_vm.transpiler.options.emit_decorator_metadata,
|
||||
.experimental_decorators = jsc_vm.transpiler.options.experimental_decorators,
|
||||
.use_define_for_class_fields = jsc_vm.transpiler.options.use_define_for_class_fields orelse true,
|
||||
.virtual_source = virtual_source,
|
||||
.dont_bundle_twice = true,
|
||||
.allow_commonjs = true,
|
||||
|
||||
@@ -387,6 +387,7 @@ pub const RuntimeTranspilerStore = struct {
|
||||
.jsx = transpiler.options.jsx,
|
||||
.emit_decorator_metadata = transpiler.options.emit_decorator_metadata,
|
||||
.experimental_decorators = transpiler.options.experimental_decorators,
|
||||
.use_define_for_class_fields = transpiler.options.use_define_for_class_fields orelse true,
|
||||
.virtual_source = null,
|
||||
.dont_bundle_twice = true,
|
||||
.allow_commonjs = true,
|
||||
|
||||
@@ -32,6 +32,7 @@ known_target: options.Target,
|
||||
module_type: options.ModuleType = .unknown,
|
||||
emit_decorator_metadata: bool = false,
|
||||
experimental_decorators: bool = false,
|
||||
use_define_for_class_fields: bool = true,
|
||||
ctx: *BundleV2,
|
||||
package_version: string = "",
|
||||
is_entry_point: bool = false,
|
||||
@@ -120,6 +121,7 @@ pub fn init(resolve_result: *const _resolver.Result, source_index: Index, ctx: *
|
||||
.module_type = resolve_result.module_type,
|
||||
.emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata,
|
||||
.experimental_decorators = resolve_result.flags.experimental_decorators,
|
||||
.use_define_for_class_fields = resolve_result.flags.use_define_for_class_fields,
|
||||
.package_version = if (resolve_result.package_json) |package_json| package_json.version else "",
|
||||
.known_target = ctx.transpiler.options.target,
|
||||
};
|
||||
@@ -1215,6 +1217,7 @@ fn runWithSourceCode(
|
||||
opts.features.minify_whitespace = transpiler.options.minify_whitespace;
|
||||
opts.features.emit_decorator_metadata = task.emit_decorator_metadata;
|
||||
opts.features.standard_decorators = !loader.isTypeScript() or !task.experimental_decorators;
|
||||
opts.use_define_for_class_fields = task.use_define_for_class_fields;
|
||||
opts.features.unwrap_commonjs_packages = transpiler.options.unwrap_commonjs_packages;
|
||||
opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags;
|
||||
opts.features.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime();
|
||||
|
||||
@@ -1741,6 +1741,7 @@ pub const BundleOptions = struct {
|
||||
jsx: JSX.Pragma = JSX.Pragma{},
|
||||
emit_decorator_metadata: bool = false,
|
||||
experimental_decorators: bool = false,
|
||||
use_define_for_class_fields: ?bool = null,
|
||||
auto_import_jsx: bool = true,
|
||||
allow_runtime: bool = true,
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ pub const Result = struct {
|
||||
preserve_unused_imports_ts: bool = false,
|
||||
emit_decorator_metadata: bool = false,
|
||||
experimental_decorators: bool = false,
|
||||
_padding: u1 = 0,
|
||||
use_define_for_class_fields: bool = true,
|
||||
};
|
||||
|
||||
pub const Union = union(enum) {
|
||||
@@ -1002,6 +1002,9 @@ pub const Resolver = struct {
|
||||
result.jsx = tsconfig.mergeJSX(result.jsx);
|
||||
result.flags.emit_decorator_metadata = result.flags.emit_decorator_metadata or tsconfig.emit_decorator_metadata;
|
||||
result.flags.experimental_decorators = result.flags.experimental_decorators or tsconfig.experimental_decorators;
|
||||
if (tsconfig.use_define_for_class_fields) |v| {
|
||||
result.flags.use_define_for_class_fields = v;
|
||||
}
|
||||
}
|
||||
|
||||
// If you use mjs or mts, then you're using esm
|
||||
|
||||
@@ -472,6 +472,9 @@ pub const Transpiler = struct {
|
||||
}
|
||||
transpiler.options.emit_decorator_metadata = tsconfig.emit_decorator_metadata;
|
||||
transpiler.options.experimental_decorators = tsconfig.experimental_decorators;
|
||||
if (tsconfig.use_define_for_class_fields) |v| {
|
||||
transpiler.options.use_define_for_class_fields = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -640,6 +643,7 @@ pub const Transpiler = struct {
|
||||
.jsx = resolve_result.jsx,
|
||||
.emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata,
|
||||
.experimental_decorators = resolve_result.flags.experimental_decorators,
|
||||
.use_define_for_class_fields = resolve_result.flags.use_define_for_class_fields,
|
||||
},
|
||||
client_entry_point_,
|
||||
) orelse {
|
||||
@@ -963,6 +967,7 @@ pub const Transpiler = struct {
|
||||
set_breakpoint_on_first_line: bool = false,
|
||||
emit_decorator_metadata: bool = false,
|
||||
experimental_decorators: bool = false,
|
||||
use_define_for_class_fields: bool = true,
|
||||
remove_cjs_module_wrapper: bool = false,
|
||||
|
||||
dont_bundle_twice: bool = false,
|
||||
@@ -1104,6 +1109,7 @@ pub const Transpiler = struct {
|
||||
|
||||
opts.features.emit_decorator_metadata = this_parse.emit_decorator_metadata;
|
||||
opts.features.standard_decorators = !loader.isTypeScript() or !this_parse.experimental_decorators;
|
||||
opts.use_define_for_class_fields = this_parse.use_define_for_class_fields;
|
||||
opts.features.allow_runtime = transpiler.options.allow_runtime;
|
||||
opts.features.set_breakpoint_on_first_line = this_parse.set_breakpoint_on_first_line;
|
||||
opts.features.trim_unused_imports = transpiler.options.trim_unused_imports orelse loader.isTypeScript();
|
||||
|
||||
239
test/regression/issue/02060.test.ts
Normal file
239
test/regression/issue/02060.test.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
test("useDefineForClassFields: false strips type-only fields", async () => {
|
||||
using dir = tempDir("issue-2060", {
|
||||
"index.ts": `
|
||||
class Base {
|
||||
constructor(data: any) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
class Outer extends Base {
|
||||
stuff: string;
|
||||
things: number;
|
||||
extra: any;
|
||||
}
|
||||
|
||||
class Inner extends Base {
|
||||
more: string;
|
||||
greatness: boolean;
|
||||
}
|
||||
|
||||
let outer = new Outer({
|
||||
stuff: "Hello World",
|
||||
things: 42,
|
||||
extra: new Inner({
|
||||
more: "Bun is becoming great!",
|
||||
greatness: true
|
||||
})
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(outer));
|
||||
`,
|
||||
"tsconfig.json": JSON.stringify({
|
||||
compilerOptions: {
|
||||
useDefineForClassFields: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "index.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
const result = JSON.parse(stdout.trim());
|
||||
expect(result.stuff).toBe("Hello World");
|
||||
expect(result.things).toBe(42);
|
||||
expect(result.extra.more).toBe("Bun is becoming great!");
|
||||
expect(result.extra.greatness).toBe(true);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("useDefineForClassFields: false moves initialized fields to constructor", async () => {
|
||||
using dir = tempDir("issue-2060-init", {
|
||||
"index.ts": `
|
||||
class Foo {
|
||||
x: number = 42;
|
||||
y: string = "hello";
|
||||
constructor() {
|
||||
// With useDefineForClassFields: false, field initializers
|
||||
// should be moved into the constructor as assignments.
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
console.log(JSON.stringify({ x: foo.x, y: foo.y }));
|
||||
`,
|
||||
"tsconfig.json": JSON.stringify({
|
||||
compilerOptions: {
|
||||
useDefineForClassFields: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "index.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
const result = JSON.parse(stdout.trim());
|
||||
expect(result.x).toBe(42);
|
||||
expect(result.y).toBe("hello");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("useDefineForClassFields: false - fields don't override parent constructor assignments", async () => {
|
||||
using dir = tempDir("issue-2060-override", {
|
||||
"index.ts": `
|
||||
class Base {
|
||||
constructor(data: Record<string, any>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends Base {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const child = new Child({ name: "test", value: 123 });
|
||||
// With useDefineForClassFields: false, type-only fields should be stripped,
|
||||
// so Object.assign values should be preserved (not overwritten to undefined).
|
||||
console.log(JSON.stringify({ name: child.name, value: child.value }));
|
||||
`,
|
||||
"tsconfig.json": JSON.stringify({
|
||||
compilerOptions: {
|
||||
useDefineForClassFields: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "index.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
const result = JSON.parse(stdout.trim());
|
||||
expect(result.name).toBe("test");
|
||||
expect(result.value).toBe(123);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("useDefineForClassFields: true (default) keeps fields in class body", async () => {
|
||||
using dir = tempDir("issue-2060-default", {
|
||||
"index.ts": `
|
||||
class Base {
|
||||
constructor(data: any) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends Base {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const child = new Child({ name: "test" });
|
||||
// With useDefineForClassFields: true (default), type-only fields remain,
|
||||
// causing them to be initialized to undefined, overwriting Object.assign values.
|
||||
console.log(JSON.stringify({ name: child.name }));
|
||||
`,
|
||||
"tsconfig.json": JSON.stringify({
|
||||
compilerOptions: {
|
||||
useDefineForClassFields: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "index.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// With define semantics, the field declaration overwrites the parent's assignment
|
||||
const result = JSON.parse(stdout.trim());
|
||||
expect(result.name).toBeUndefined();
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("useDefineForClassFields: false - transpile output strips type-only fields", async () => {
|
||||
using dir = tempDir("issue-2060-build", {
|
||||
"index.ts": `
|
||||
class Foo {
|
||||
name: string;
|
||||
value: number;
|
||||
method() { return 1; }
|
||||
}
|
||||
`,
|
||||
"tsconfig.json": JSON.stringify({
|
||||
compilerOptions: {
|
||||
useDefineForClassFields: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "--no-bundle", "index.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Type-only fields should not appear in the output
|
||||
expect(stdout).not.toContain("name;");
|
||||
expect(stdout).not.toContain("value;");
|
||||
// Method should still be present
|
||||
expect(stdout).toContain("method()");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("useDefineForClassFields: false keeps static fields", async () => {
|
||||
using dir = tempDir("issue-2060-static", {
|
||||
"index.ts": `
|
||||
class Foo {
|
||||
static count: number = 0;
|
||||
name: string;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ count: Foo.count }));
|
||||
`,
|
||||
"tsconfig.json": JSON.stringify({
|
||||
compilerOptions: {
|
||||
useDefineForClassFields: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "index.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
const result = JSON.parse(stdout.trim());
|
||||
expect(result.count).toBe(0);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user