Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
23 changes: 23 additions & 0 deletions src/js_parser/parse/parse_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,32 @@

let mut ts_decorators = bun_alloc::AstAlloc::vec();
if opts.allow_ts_decorators {
let at_range = p.lexer.range();
ts_decorators = p.parse_type_script_decorators()?;
if ts_decorators.len_u32() > 0 {
arg_has_decorators = true;
if p.options.features.standard_decorators {
if Self::IS_TYPESCRIPT_ENABLED {
p.log().add_range_error_with_notes(
Some(p.source),
at_range,
b"Parameter decorators only work when experimental decorators are enabled"
.as_slice(),
Box::new([bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"Add \"experimentalDecorators\": true to your tsconfig.json to enable them",
),
..Default::default()
}]),

Check notice on line 251 in src/js_parser/parse/parse_fn.rs

View check run for this annotation

Claude / Claude Code Review

tsconfig extends chain drops experimentalDecorators — new error's hint can mislead (pre-existing resolver bug)

Heads-up on a pre-existing resolver bug this error will surface more loudly: the tsconfig `extends`-chain merge (`src/resolver/resolver.rs:6404-6449`) never propagates `experimental_decorators` from child configs into the merged config — it OR-merges `emit_decorator_metadata` (line 6409) but has no corresponding line for `experimental_decorators`. So a project whose leaf `tsconfig.json` sets `"experimentalDecorators": true` while extending a base that omits it will now get this new error with a
Comment thread
robobun marked this conversation as resolved.
Resolved
);
} else {
p.log().add_range_error(
Some(p.source),
at_range,
b"Parameter decorators are not allowed in JavaScript",
);
}
}
}
}

Expand Down
75 changes: 75 additions & 0 deletions test/bundler/transpiler/es-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,81 @@ describe("ES Decorators", () => {
expect(stdout).toBe("class Foo\n");
expect(exitCode).toBe(0);
});

test("TS parameter decorators are rejected without experimentalDecorators", async () => {
using dir = tempDir("es-dec-ts-param", {
"tsconfig.json": JSON.stringify({ compilerOptions: {} }),
"test.ts": `
function d(..._args: any[]) {}
class C {
constructor(@d y?: number) {}
m(@d x: number) { return x; }
}
new C(1);
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const stderr = filterStderr(rawStderr);
expect(stderr).toContain("Parameter decorators only work when experimental decorators are enabled");
expect(stderr).toContain('"experimentalDecorators": true');
expect(stdout).toBe("");
expect(exitCode).not.toBe(0);
});

test("JS parameter decorators are rejected", async () => {
const { stderr, stdout, exitCode } = await runDecorator(`
function d() {}
class C { m(@d x) { return x; } }
new C().m(1);
`);
expect(stderr).toContain("Parameter decorators are not allowed in JavaScript");
expect(stdout).toBe("");
expect(exitCode).not.toBe(0);
});

test("Bun.Transpiler rejects parameter decorators without experimentalDecorators", () => {
const transpiler = new Bun.Transpiler({ loader: "ts" });
expect(() => transpiler.transformSync(`function d(){}\nclass C { constructor(@d y) {} }`)).toThrow(
/Parameter decorators only work when experimental decorators are enabled/,
);
expect(() => transpiler.transformSync(`function d(){}\nclass C { m(@d x) { return x; } }`)).toThrow(
/Parameter decorators only work when experimental decorators are enabled/,
);
});

test("TS parameter decorators still work with experimentalDecorators", async () => {
using dir = tempDir("es-dec-ts-param-exp", {
"tsconfig.json": JSON.stringify({ compilerOptions: { experimentalDecorators: true } }),
"test.ts": `
const seen: string[] = [];
function d(...args: any[]) { seen.push("called:" + args.length); }
class C {
constructor(@d y?: number) {}
m(@d x: number) { return x; }
}
new C(1); new C().m(2);
console.log(JSON.stringify(seen));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(filterStderr(rawStderr)).toBe("");
expect(stdout).toBe('["called:3","called:3"]\n');
expect(exitCode).toBe(0);
});
});

describe("extends clause", () => {
Expand Down
Loading