Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 0 additions & 7 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8975,13 +8975,6 @@ impl LowerUsingDeclarationsContext {
result.push(stmt);
continue;
}
js_ast::StmtData::SClass(c) => {
if c.is_export {
// can't go in try/catch; hoist out
result.push(stmt);
continue;
}
}
js_ast::StmtData::SExportDefault(_) => {
continue; // this prevents re-exporting default since we already have it as an .s_export_clause
}
Expand Down
34 changes: 33 additions & 1 deletion src/js_parser/visit/visit_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let lowered = p.lower_class(js_ast::StmtOrExpr::Stmt(*stmt));

if !mark_as_dead || was_export_inside_namespace {
// Lower class field syntax for browsers that don't support it
// Class counterpart of `select_local_kind` turning top-level `let`/`const` into `var`.
if p.will_wrap_module_in_try_catch_for_using && p.current_scope().parent.is_none() {
Self::convert_class_stmt_to_var(p, lowered, data);
}
stmts.extend_from_slice(lowered);
} else {
let ref_ = data
Expand Down Expand Up @@ -1118,6 +1121,35 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Ok(())
}

/// Swaps the class statement in `lowered` for `var Foo = class Foo {}` (exported if it was).
fn convert_class_stmt_to_var(p: &mut Self, lowered: &mut [Stmt], data: &mut S::Class) {
let Some(slot) = lowered
.iter_mut()
.find(|s| matches!(s.data, StmtData::SClass(_)))
else {
return;
};
let loc = slot.loc;
let name = data
.class
.class_name
.expect("infallible: class statements are always named");
let class_expr = p.new_expr(core::mem::take(&mut data.class), loc);
let binding = p.b(B::Identifier { r#ref: name.ref_ }, name.loc);
*slot = p.s(
S::Local {
kind: S::Kind::KVar,
is_export: data.is_export,
decls: G::DeclList::from_slice(&[G::Decl {
binding,
value: Some(class_expr),
}]),
..Default::default()
},
loc,
);
}

fn s_local(
p: &mut Self,
stmts: &mut StmtList<'a>,
Expand Down
171 changes: 171 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2348,6 +2348,177 @@ describe("bundler", () => {
},
});

// Lowering a top-level `using` wraps the module body in try/catch/finally.
// Class declarations have to stay in source order inside that block, but as
// module-scoped `var`s, like esbuild: exported classes used to be hoisted
// above the block (evaluating before the `using` value was initialized) and
// non-exported ones stayed block-scoped to it, so the hoisted function
// declarations could not see them.
itBundled("edgecase/UsingTopLevelClassDeclarations", {
files: {
"/entry.ts": `
import { Exported, Clause, viaFunction, Sub } from "./module.ts";
console.log(Exported.name, Exported.v);
console.log(Clause.name, Clause.v, Clause.self() === Clause);
console.log(viaFunction().name, viaFunction().v);
console.log(Sub.name, Sub.v, Object.getPrototypeOf(Sub) === Exported);
`,
"/module.ts": `
using resource = { v: 42, [Symbol.dispose]() { console.log("Disposing"); } };
export class Exported { static v = resource.v; }
class Clause { static v = resource.v; static self() { return Clause; } }
export { Clause };
class Hidden { static v = resource.v; }
export function viaFunction() { return Hidden; }
export class Sub extends Exported {}
`,
},
run: {
stdout: `
Disposing
Exported 42
Clause 42 true
Hidden 42
Sub 42 true
`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).not.toMatch(/^\s*class /m);
// Only the assignment is pinned down: when bundling, the `var` itself is
// relocated into a top-level declaration, and the class expression may or
// may not carry a name.
expect(output).toMatch(/\bExported = class\b/);
expect(output).toMatch(/\bHidden = class\b/);
},
});

// A module that is also reached through import() is evaluated inside an
// `__esm(...)` wrapper. The linker only hoists top-level declarations out of
// it, so the classes rely on the try block's `var`s being relocated into a
// top-level declaration (see LoweredTopLevelUsingInEsmWrapper below).
itBundled("edgecase/UsingTopLevelClassDeclarationsInEsmWrapper", {
files: {
"/entry.ts": `
import { Exported, viaFunction } from "./module.ts";
console.log("static", Exported.v, viaFunction().v);
const mod = await import("./module.ts");
console.log("dynamic", mod.Exported === Exported, mod.viaFunction().v);
`,
"/module.ts": `
using resource = { v: 42, [Symbol.dispose]() { console.log("Disposing"); } };
export class Exported { static v = resource.v; }
class Hidden { static v = resource.v; }
export function viaFunction() { return Hidden; }
`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain("__esm(");
expect(output).toMatch(/^var __stack, resource, Exported, Hidden;$/m);
},
run: {
stdout: `
Disposing
static 42 42
dynamic true 42
`,
},
});

itBundled("edgecase/AwaitUsingTopLevelClassDeclarations", {
files: {
"/entry.ts": `
import { Exported, viaFunction } from "./module.ts";
console.log(Exported.v, viaFunction().v);
`,
"/module.ts": `
await using resource = { v: 42, async [Symbol.asyncDispose]() { console.log("Disposing"); } };
export class Exported { static v = resource.v; }
class Hidden { static v = resource.v; }
export function viaFunction() { return Hidden; }
`,
},
run: {
stdout: "Disposing\n42 42",
},
});

// `lower_class` emits extra statements around a decorated class; the class
// statement among them is the one that gets rewritten to a `var`.
itBundled("edgecase/UsingTopLevelClassStandardDecorators", {
files: {
"/entry.ts": `
import { Exported, viaFunction } from "./module.ts";
console.log(Exported.decorated, Exported.v, viaFunction().decorated, viaFunction().v);
`,
"/module.ts": `
using resource = { v: 42, [Symbol.dispose]() { console.log("Disposing"); } };
function decorate(value: any, context: ClassDecoratorContext) {
return class extends value { static decorated = context.name; };
}
@decorate
export class Exported { static v = resource.v; }
@decorate
class Hidden { static v = resource.v; }
export function viaFunction() { return Hidden; }
`,
},
run: {
stdout: "Disposing\nExported 42 Hidden 42",
},
});

itBundled("edgecase/UsingTopLevelClassExperimentalDecorators", {
files: {
"/entry.ts": `
import { Exported, viaFunction } from "./module.ts";
console.log(Exported.decorated, Exported.v, Exported.fields, viaFunction().decorated, viaFunction().v);
`,
"/module.ts": `
using resource = { v: 42, [Symbol.dispose]() { console.log("Disposing"); } };
function decorate(target: any) {
return class extends target { static decorated = target.name; };
}
function field(target: any, key: string) {
target.fields = key;
}
@decorate
export class Exported { static v = resource.v; @field static f = 1; }
@decorate
class Hidden { static v = resource.v; }
export function viaFunction() { return Hidden; }
`,
"/tsconfig.json": /* json */ `{ "compilerOptions": { "experimentalDecorators": true } }`,
},
run: {
stdout: "Disposing\nExported 42 f Hidden 42",
},
});

itBundled("edgecase/UsingTopLevelClassBakeDev", {
// The dev server's module format builds its export list from the same
// lowered statements.
format: "internal_bake_dev",
files: {
"/entry.ts": `
using resource = { [Symbol.dispose]() {} };
export class Exported {}
class Hidden {}
export function viaFunction() { return Hidden; }
`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).not.toMatch(/^\s*class (Exported|Hidden) /m);
expect(output).toMatch(/\bExported = class\b/);
expect(output).toMatch(/\bHidden = class\b/);
const [, exportList] = output.match(/hmr\.exports = \{([^}]*)\}/)!;
const exportNames = exportList.split(/[\s,]+/).filter(Boolean);
expect(exportNames.sort()).toEqual(["Exported", "viaFunction"]);
},
});

itBundled("edgecase/UsingExportFails", {
files: {
"/entry.ts": `
Expand Down
34 changes: 34 additions & 0 deletions test/bundler/transpiler/__snapshots__/transpiler.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,37 @@ export {
};
"
`;

exports[`Bun.Transpiler using top level turns class declarations into vars 1`] = `
"import { __callDispose as __callDispose, __using as __using } from "bun:wrap";
export function f() {

class G {
}
return [new E, new G];
}
let __bun_temp_ref_1$ = [];
try {
var a = __using(__bun_temp_ref_1$, b(), 0);
var C = class C {
static a = a;
};
var D = class D extends C {
static self() {
return D;
}
};
var E = class E {
};
} catch (__bun_temp_ref_2$) {
var __bun_temp_ref_3$ = __bun_temp_ref_2$, __bun_temp_ref_4$ = 1;
} finally {
__callDispose(__bun_temp_ref_1$, __bun_temp_ref_3$, __bun_temp_ref_4$);
}

export {
C,
D
};
"
`;
18 changes: 18 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4700,6 +4700,24 @@ console.log("boop");
export var q = r;
`);
});

it("using top level turns class declarations into vars", () => {
// Everything but imports, exports and function declarations moves into the
// try block, so classes become module-scoped `var`s in source order (and
// exported ones move to the export clause), matching esbuild. Classes in
// nested scopes are left alone.
expectPrintedSnapshot(`
using a = b();
export class C { static a = a; }
class D extends C { static self() { return D; } }
export { D };
class E {}
export function f() {
class G {}
return [new E(), new G()];
}
`);
});
});

describe("await can only be used inside an async function message", () => {
Expand Down
Loading