Skip to content
Closed
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
10 changes: 5 additions & 5 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2448,11 +2448,11 @@ pub mod parse_worker {
opts.features.minify_keep_names = topts.keep_names;
opts.features.minify_whitespace = topts.minify_whitespace;
opts.features.emit_decorator_metadata = task.emit_decorator_metadata;
// emitDecoratorMetadata implies legacy/experimental decorators, as it only
// makes sense with TypeScript's legacy decorator system (reflect-metadata).
// TC39 standard decorators have their own metadata mechanism.
opts.features.standard_decorators = !loader.is_typescript()
|| !(task.experimental_decorators || task.emit_decorator_metadata);
opts.features.standard_decorators = js_parser::RuntimeFeatures::standard_decorators_for(
loader,
task.experimental_decorators,
task.emit_decorator_metadata,
);
opts.features.unwrap_commonjs_packages = topts.unwrap_commonjs_packages;
// Modeled as
// `Option<Box<StringSet>>` on both sides, so we deep-clone (small —
Expand Down
11 changes: 6 additions & 5 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1595,11 +1595,12 @@ impl<'a> Transpiler<'a> {
};

opts.features.emit_decorator_metadata = this_parse.emit_decorator_metadata;
// emitDecoratorMetadata implies legacy/experimental decorators, as it only
// makes sense with TypeScript's legacy decorator system (reflect-metadata).
// TC39 standard decorators have their own metadata mechanism.
opts.features.standard_decorators = !loader.is_typescript()
|| !(this_parse.experimental_decorators || this_parse.emit_decorator_metadata);
opts.features.standard_decorators =
js_ast::RuntimeFeatures::standard_decorators_for(
loader,
this_parse.experimental_decorators,
this_parse.emit_decorator_metadata,
);
opts.features.allow_runtime = self.options.allow_runtime;
opts.features.set_breakpoint_on_first_line =
this_parse.set_breakpoint_on_first_line;
Expand Down
11 changes: 11 additions & 0 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,17 @@ pub mod Runtime {
self.runtime_transpiler_cache.map(|p| unsafe { &mut *p })
}

/// Whether to parse TC39 standard decorators (and the `accessor` keyword).
/// Only TypeScript files that opted into legacy decorators use the old grammar;
/// `emitDecoratorMetadata` implies them, as it only makes sense with that system.
pub fn standard_decorators_for(
loader: bun_ast::Loader,
experimental_decorators: bool,
emit_decorator_metadata: bool,
) -> bool {
!loader.is_typescript() || !(experimental_decorators || emit_decorator_metadata)
}

/// Initialize bundler feature flags for dead-code elimination via `import { feature } from "bun:bundle"`.
/// Returns an owned `Box<StringSet>`, or `None` if no flags are provided.
/// Keys are kept sorted so iteration order is deterministic (for RuntimeTranspilerCache hashing).
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/api/JSTranspiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,14 @@ impl JSTranspiler {
};

let mut opts = bun_js_parser::ParserOptions::init(jsx, loader);
// Keep the scanner's grammar in sync with `parse()` (see `get_parse_result`),
// or the fast path rejects decorators and `accessor` fields the transpiler accepts.
let tsconfig = self.config.get().tsconfig.as_deref();
opts.features.standard_decorators = bun_js_parser::RuntimeFeatures::standard_decorators_for(
loader,
tsconfig.is_some_and(|ts| ts.experimental_decorators),
tsconfig.is_some_and(|ts| ts.emit_decorator_metadata),
);
// SAFETY: see `transpiler_mut`. The `&mut Transpiler` is reborrowed
// disjointly for `macro_context` (stored in `opts`) and `options.define`
// (raw-addr read) below; both end when `opts` is consumed by `scan()`.
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/cli/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1799,6 +1799,12 @@ impl<'a> Repl<'a> {
opts.repl_mode = true;
opts.features.dead_code_elimination = false; // REPL needs all code
opts.features.top_level_await = true; // Enable top-level await in REPL
opts.features.emit_decorator_metadata = vm.transpiler.options.emit_decorator_metadata;
opts.features.standard_decorators = bun_js_parser::RuntimeFeatures::standard_decorators_for(
bun_ast::Loader::Tsx,
vm.transpiler.options.experimental_decorators,
vm.transpiler.options.emit_decorator_metadata,
);
// Keep `lower_using` at its default (true) here even though JavaScriptCore
// supports `using` / `await using` natively. The REPL transform in
// `js_parser/repl_transforms.rs` rewrites every top-level `s_local` into a
Expand Down
42 changes: 42 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,48 @@ console.log(<div {...obj} key="after" />);`),
expect(imports.filter(({ path }) => path === "react")).toHaveLength(1);
expect(imports).toHaveLength(2);
});

describe.each(["ts", "tsx", "js", "jsx"])("with the %s loader", loader => {
const scanner = new Bun.Transpiler({ loader });
const expected = [{ kind: "import-statement", path: "./dep" }];

it("accepts auto-accessor fields", () => {
const source = `import { a } from "./dep";\nclass C { accessor x = a; }`;
expect(scanner.scanImports(source)).toEqual(expected);
expect(scanner.scan(source).imports).toEqual(expected);
});

it("accepts standard decorators on a class", () => {
const source = `import { dec } from "./dep";\n@dec class C {}`;
expect(scanner.scanImports(source)).toEqual(expected);
expect(scanner.scan(source).imports).toEqual(expected);
});

it("accepts standard decorators on class members", () => {
const source = `import { dec } from "./dep";\nclass C { @dec accessor x = 1; @dec foo() {} }`;
expect(scanner.scanImports(source)).toEqual(expected);
expect(scanner.scan(source).imports).toEqual(expected);
});
});

// `@dec()()` parses under TypeScript's legacy decorator grammar but not under the
// TC39 standard one, so it tells the two apart.
const legacyOnlyDecorator = `import { dec } from "./dep";\n@dec()() class C {}`;

it("uses the standard decorator grammar by default, like transformSync", () => {
const standard = new Bun.Transpiler({ loader: "ts" });
expect(() => standard.scanImports(legacyOnlyDecorator)).toThrow("Failed to scan imports");
expect(() => standard.transformSync(legacyOnlyDecorator)).toThrow("Parse error");
});

it("honors experimentalDecorators, like transformSync", () => {
const legacy = new Bun.Transpiler({
loader: "ts",
tsconfig: { compilerOptions: { experimentalDecorators: true } },
});
expect(legacy.scanImports(legacyOnlyDecorator)).toEqual([{ kind: "import-statement", path: "./dep" }]);
expect(legacy.transformSync(legacyOnlyDecorator)).toContain("class C");
});
});

const parsed = (code, trim = true, autoExport = false, transpiler_ = transpiler) => {
Expand Down
38 changes: 37 additions & 1 deletion test/js/bun/repl/repl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ async function runRepl(
input: string | string[],
options: {
env?: Record<string, string>;
cwd?: string;
} = {},
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const inputStr = Array.isArray(input) ? input.join("\n") + "\n" : input;
const { env = {} } = options;
const { env = {}, cwd } = options;

await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
stdin: Buffer.from(inputStr),
stdout: "pipe",
stderr: "pipe",
cwd,
env: {
...bunEnv,
TERM: "dumb",
Expand Down Expand Up @@ -751,6 +753,40 @@ describe.concurrent("Bun REPL", () => {
expect(stripAnsi(stdout)).toContain("test");
expect(exitCode).toBe(0);
});

test("auto-accessor fields are lowered", async () => {
using dir = tempDir("repl-accessor", {});
const { stdout, exitCode } = await runRepl(
["class Counter { accessor count = 7 }", "console.log('count is ' + new Counter().count)", ".exit"],
{ cwd: String(dir) },
);
// The REPL echoes typed input back, so assert on a string only evaluation can produce.
const out = stripAnsi(stdout);
expect(out).toContain("count is 7");
expect(out).not.toContain("SyntaxError");
expect(exitCode).toBe(0);
});

test("tsconfig selects the legacy decorator grammar and metadata", async () => {
using dir = tempDir("repl-legacy-decorators", {
"tsconfig.json": JSON.stringify({
compilerOptions: { experimentalDecorators: true, emitDecoratorMetadata: true },
}),
});
const { stdout, exitCode } = await runRepl(
[
"Reflect.metadata = (k, v) => t => { console.log('got ' + k); return t }",
"function dec(t) { return t }",
"class Legacy { @dec foo(a: string) {} }",
".exit",
],
{ cwd: String(dir) },
);
// Only the legacy decorator lowering emits design-time metadata, so this also
// proves the REPL did not blanket-enable standard decorators.
expect(stripAnsi(stdout)).toContain("got design:type");
expect(exitCode).toBe(0);
});
});

describe("welcome message", () => {
Expand Down
Loading