Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -934,32 +934,6 @@ impl<'a> Parser<'a> {
p.append_part(parts_list, sliced)?;
}

js_ast::StmtData::SClass(class) => {
// Move class export statements to the top of the file if we can
// This automatically resolves some cyclical import issues
// https://github.com/kysely-org/kysely/issues/412
let should_move = !p.options.bundle && class.class.can_be_moved();

let sliced = arena.alloc_slice_copy(&[*stmt]);
p.append_part(&mut parts, sliced)?;

if should_move {
// `Part` isn't `Copy`; pop+push instead of last+truncate.
before.push(parts.pop().expect("unreachable"));
}
}
js_ast::StmtData::SExportDefault(value) => {
// We move export default statements when we can
// This automatically resolves some cyclical import issues in packages like luxon
// https://github.com/oven-sh/bun/issues/1961
let should_move = !p.options.bundle && value.can_be_moved();
let sliced = arena.alloc_slice_copy(&[*stmt]);
p.append_part(&mut parts, sliced)?;

if should_move {
before.push(parts.pop().expect("unreachable"));
}
}
js_ast::StmtData::SEnum(_) => {
// `Part` isn't `Clone`; move out the
// pre-visited parts instead of `appendSlice`.
Expand Down
85 changes: 85 additions & 0 deletions test/bundler/transpiler/runtime-transpiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,88 @@
expect(exitCode).toBe(1);
});
});

describe("class declaration TDZ is preserved", () => {
const probe =
`const t = (f) => { try { return f(); } catch (e) { return "THROW:" + e.constructor.name; } };\n` +
`console.log(JSON.stringify([t(() => typeof Pure), t(() => new Pure().m()), t(() => typeof WithBlock)]));\n`;

test.concurrent.each([
["class", ""],
["export class", "export "],
])("runtime: %s stays in TDZ until its declaration", async (_, prefix) => {
using dir = tempDir("transpiler-class-tdz", {
"entry.mjs":
probe +
`${prefix}class Pure { m() { return "ok"; } f = 1; get g() { return 2; } static s = 3; }\n` +
`${prefix}class WithBlock { static { void 0; } }\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "entry.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual(["THROW:ReferenceError", "THROW:ReferenceError", "THROW:ReferenceError"]);
expect(exitCode).toBe(0);
});

test.concurrent("runtime: export default class stays in TDZ until its declaration", async () => {
using dir = tempDir("transpiler-class-tdz-default", {
"entry.mjs":
`const t = (f) => { try { return f(); } catch (e) { return "THROW:" + e.constructor.name; } };\n` +
`console.log(JSON.stringify([t(() => typeof Named), t(() => new Named().m())]));\n` +
`export default class Named { m() { return "ok"; } static s = 3; }\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "entry.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual(["THROW:ReferenceError", "THROW:ReferenceError"]);
expect(exitCode).toBe(0);
});

test.concurrent("--no-bundle output keeps class declarations in source order", async () => {
using dir = tempDir("transpiler-class-tdz-print", {
"entry.mjs": `const marker = 1;\nclass Pure { m() { return "ok"; } static s = 3; }\nexport default class Named { static s = 3; }\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "--target=bun", "entry.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
const markerPos = stdout.indexOf("marker");
const purePos = stdout.indexOf("class Pure");
const namedPos = stdout.indexOf("class Named");
expect({ markerPos, purePos, namedPos }).toEqual({
markerPos: expect.any(Number),
purePos: expect.any(Number),
namedPos: expect.any(Number),
});

Check warning on line 333 in test/bundler/transpiler/runtime-transpiler.test.ts

View check run for this annotation

Claude / Claude Code Review

Vacuous expect.any(Number) assertion on indexOf results

This assertion is vacuous — `String.prototype.indexOf` always returns a number (an index or `-1`), so `expect.any(Number)` is trivially satisfied for all three fields and the check can never fail. The subsequent `markerPos >= 0` plus the transitive `markerPos < purePos < namedPos` chain already proves all three tokens were found and in order, so this block can just be deleted.
Comment thread
robobun marked this conversation as resolved.
Outdated
expect(markerPos).toBeGreaterThanOrEqual(0);
expect(markerPos).toBeLessThan(purePos);
expect(purePos).toBeLessThan(namedPos);
expect(exitCode).toBe(0);
});
});
Loading