Files
bun-src/test/bundler/transpiler/ts-use-define-for-class-fields.test.ts
2026-08-27 21:09:14 +00:00

249 lines
9.2 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// With `useDefineForClassFields: false` TypeScript lowers instance fields to
// `this.x = init` in the constructor, after parameter-property assignments.
// https://github.com/oven-sh/bun/issues/10961
async function run(dir: string, entry = "index.ts") {
await using proc = Bun.spawn({
cmd: [bunExe(), entry],
env: bunEnv,
cwd: dir,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}
describe("tsconfig compilerOptions.useDefineForClassFields", () => {
test.concurrent("false: field initializers see parameter properties (#10961)", async () => {
using dir = tempDir("udfcf-param-prop", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
class Logger { createChildContext(n: string) { return new Logger(); } }
class PlayerManager { constructor(public log: Logger) {} }
class GameServer {
private _playerManager = new PlayerManager(this._log.createChildContext("pm"));
constructor(private _log: Logger) {}
get playerManager() { return this._playerManager; }
}
const gs = new GameServer(new Logger());
console.log(gs.playerManager instanceof PlayerManager);
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(stdout.trim()).toBe("true");
expect(exitCode).toBe(0);
});
test.concurrent("false: matches tsc/esbuild ordering and [[Set]] semantics", async () => {
using dir = tempDir("udfcf-order", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
const order: string[] = [];
let setterCalled = false;
class Base {
set p(v: any) { setterCalled = true; }
get p() { return "getter"; }
constructor(..._a: any[]) { order.push("Base"); }
}
class Derived extends Base {
a = (order.push("a"), this.x + 1);
b: number;
p: any = "field";
#d = (order.push("#d"), 7);
constructor(public x: number) { super(x); order.push("ctor"); }
getD() { return this.#d; }
}
const d = new Derived(10);
process.stdout.write(JSON.stringify({
order,
a: d.a,
hasB: "b" in d,
p: d.p,
setterCalled,
priv: d.getD(),
}));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({
order: ["Base", "a", "#d", "ctor"],
a: 11,
hasB: false,
p: "getter",
setterCalled: true,
priv: 7,
});
expect(exitCode).toBe(0);
});
test.concurrent("false: synthesizes constructor when none is declared", async () => {
using dir = tempDir("udfcf-synth", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
class NoCtor { a = 1; b: number; }
class ExtNoCtor extends NoCtor { c = this.a + 1; }
const e = new ExtNoCtor();
process.stdout.write(JSON.stringify({ a: e.a, c: e.c, hasB: "b" in e }));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ a: 1, c: 2, hasB: false });
expect(exitCode).toBe(0);
});
test.concurrent("false: computed literal keys use [[Set]] semantics", async () => {
using dir = tempDir("udfcf-computed", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
let sets = 0;
class Base { set lit(v: any) { sets++; } get lit() { return "g"; } }
class C extends Base {
["lit"]: any = this.x;
[0] = this.x * 2;
constructor(public x: number) { super(); }
}
const c = new C(3);
process.stdout.write(JSON.stringify({ sets, lit: c.lit, zero: c[0] }));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ sets: 1, lit: "g", zero: 6 });
expect(exitCode).toBe(0);
});
test.concurrent("false: computed non-literal key keeps the class native", async () => {
using dir = tempDir("udfcf-computed-dyn", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
const order: string[] = [];
const K = Symbol();
class C {
a = (order.push("a"), 1);
[K]: any = (order.push("K"), this.a);
b = (order.push("b"), 2);
}
const c: any = new C();
let called = 0;
const key = () => (called++, "k");
class D { [key()]: any; a = 1; }
new D();
new D();
process.stdout.write(JSON.stringify({ order, K: c[K], called }));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ order: ["a", "K", "b"], K: 1, called: 1 });
expect(exitCode).toBe(0);
});
for (const explicit of [true, false]) {
test.concurrent(`${explicit ? "true" : "unset"}: keeps native class-field ([[Define]]) semantics`, async () => {
using dir = tempDir("udfcf-define", {
"tsconfig.json": JSON.stringify({
compilerOptions: explicit ? { useDefineForClassFields: true } : {},
}),
"index.ts": `
let setterCalled = false;
class Base { set p(v: any) { setterCalled = true; } get p() { return "getter"; } }
class C extends Base { p: any = "field"; b: number; }
const c = new C();
process.stdout.write(JSON.stringify({
setterCalled,
p: c.p,
ownP: Object.prototype.hasOwnProperty.call(c, "p"),
hasB: "b" in c,
}));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ setterCalled: false, p: "field", ownP: true, hasB: true });
expect(exitCode).toBe(0);
});
}
test.concurrent("false: standard-decorated field keeps its initializer for the decorator", async () => {
using dir = tempDir("udfcf-std-dec", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
function dec(_v: any, _ctx: any) { return (v: any) => v * 10; }
class C {
@dec a = 1;
constructor(public x: number) {}
}
process.stdout.write(JSON.stringify({ a: new C(5).a }));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ a: 10 });
expect(exitCode).toBe(0);
});
test.concurrent("false: static method named 'constructor' is not treated as the constructor", async () => {
using dir = tempDir("udfcf-static-ctor", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
class C {
x = 1;
static constructor() { return "static"; }
}
process.stdout.write(JSON.stringify({ x: new C().x, s: C.constructor() }));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ x: 1, s: "static" });
expect(exitCode).toBe(0);
});
test.concurrent("false: decorated field initializers also see parameter properties", async () => {
using dir = tempDir("udfcf-decorated", {
"tsconfig.json": JSON.stringify({
compilerOptions: { useDefineForClassFields: false, experimentalDecorators: true },
}),
"index.ts": `
let decorated: string | undefined;
function dec(_t: any, k: string) { decorated = k; }
const order: string[] = [];
class Foo {
@dec a = (order.push("a=" + this.x), this.x);
b = (order.push("b=" + this.x), this.x);
constructor(public x: number) { order.push("ctor"); }
}
new Foo(5);
process.stdout.write(JSON.stringify({ order, decorated }));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ order: ["a=5", "b=5", "ctor"], decorated: "a" });
expect(exitCode).toBe(0);
});
test.concurrent("false: Bun.Transpiler honors the option", async () => {
const t = new Bun.Transpiler({
loader: "ts",
tsconfig: { compilerOptions: { useDefineForClassFields: false } },
});
const out = t.transformSync(`
class A {
v = this.x + 1;
constructor(public x: number) {}
}
`);
expect(out).toContain("this.x = x");
expect(out).toContain("this.v = this.x + 1");
// No declaration-only native class field for the parameter property.
expect(out).not.toMatch(/^\s*x;\s*$/m);
});
});