mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
2 Commits
claude/gra
...
claude/ecm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88bd9eca14 | ||
|
|
8d8ed76e5b |
@@ -5,6 +5,12 @@ pub const Decl = struct {
|
||||
pub const List = BabyList(Decl);
|
||||
};
|
||||
|
||||
pub const Decorator = struct {
|
||||
value: ExprNodeIndex,
|
||||
at_loc: logger.Loc,
|
||||
omit_newline_after: bool = false,
|
||||
};
|
||||
|
||||
pub const NamespaceAlias = struct {
|
||||
namespace_ref: Ref,
|
||||
alias: string,
|
||||
@@ -26,12 +32,14 @@ pub const ExportStarAlias = struct {
|
||||
pub const Class = struct {
|
||||
class_keyword: logger.Range = logger.Range.None,
|
||||
ts_decorators: ExprNodeList = ExprNodeList{},
|
||||
decorators: []Decorator = &([_]Decorator{}),
|
||||
class_name: ?LocRef = null,
|
||||
extends: ?ExprNodeIndex = null,
|
||||
body_loc: logger.Loc = logger.Loc.Empty,
|
||||
close_brace_loc: logger.Loc = logger.Loc.Empty,
|
||||
properties: []Property = &([_]Property{}),
|
||||
has_decorators: bool = false,
|
||||
should_lower_standard_decorators: bool = false,
|
||||
|
||||
pub fn canBeMoved(this: *const Class) bool {
|
||||
if (this.extends != null)
|
||||
@@ -96,6 +104,7 @@ pub const Property = struct {
|
||||
|
||||
class_static_block: ?*ClassStaticBlock = null,
|
||||
ts_decorators: ExprNodeList = .{},
|
||||
decorators: []Decorator = &([_]Decorator{}),
|
||||
// Key is optional for spread
|
||||
key: ?ExprNodeIndex = null,
|
||||
|
||||
@@ -120,6 +129,7 @@ pub const Property = struct {
|
||||
.flags = this.flags,
|
||||
.class_static_block = class_static_block,
|
||||
.ts_decorators = try this.ts_decorators.deepClone(allocator),
|
||||
.decorators = try allocator.dupe(Decorator, this.decorators),
|
||||
.key = if (this.key) |key| try key.deepClone(allocator) else null,
|
||||
.value = if (this.value) |value| try value.deepClone(allocator) else null,
|
||||
.ts_metadata = this.ts_metadata,
|
||||
@@ -189,6 +199,7 @@ pub const Fn = struct {
|
||||
};
|
||||
pub const Arg = struct {
|
||||
ts_decorators: ExprNodeList = ExprNodeList{},
|
||||
decorators: []Decorator = &([_]Decorator{}),
|
||||
binding: BindingNodeIndex,
|
||||
default: ?ExprNodeIndex = null,
|
||||
|
||||
@@ -200,6 +211,7 @@ pub const Arg = struct {
|
||||
pub fn deepClone(this: *const Arg, allocator: std.mem.Allocator) !Arg {
|
||||
return .{
|
||||
.ts_decorators = try this.ts_decorators.deepClone(allocator),
|
||||
.decorators = try allocator.dupe(Decorator, this.decorators),
|
||||
.binding = this.binding,
|
||||
.default = if (this.default) |d| try d.deepClone(allocator) else null,
|
||||
.is_typescript_ctor_field = this.is_typescript_ctor_field,
|
||||
|
||||
@@ -99,6 +99,8 @@ pub fn NewParser_(
|
||||
pub const parseStmtsUpTo = parse_zig.parseStmtsUpTo;
|
||||
pub const parseAsyncPrefixExpr = parse_zig.parseAsyncPrefixExpr;
|
||||
pub const parseTypeScriptDecorators = parse_zig.parseTypeScriptDecorators;
|
||||
pub const parseDecorators = parse_zig.parseDecorators;
|
||||
pub const parseDecoratorExpression = parse_zig.parseDecoratorExpression;
|
||||
pub const parseTypeScriptNamespaceStmt = parse_zig.parseTypeScriptNamespaceStmt;
|
||||
pub const parseTypeScriptImportEqualsStmt = parse_zig.parseTypeScriptImportEqualsStmt;
|
||||
pub const parseTypescriptEnumStmt = parse_zig.parseTypescriptEnumStmt;
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const Parser = struct {
|
||||
ignore_dce_annotations: bool = false,
|
||||
preserve_unused_imports_ts: bool = false,
|
||||
use_define_for_class_fields: bool = false,
|
||||
experimental_decorators: bool = false,
|
||||
suppress_warnings_about_weird_code: bool = true,
|
||||
filepath_hash_for_hmr: u32 = 0,
|
||||
features: RuntimeFeatures = .{},
|
||||
|
||||
@@ -22,6 +22,8 @@ pub fn Parse(
|
||||
pub const parseImportClause = @import("./parseImportExport.zig").ParseImportExport(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseImportClause;
|
||||
pub const parseExportClause = @import("./parseImportExport.zig").ParseImportExport(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseExportClause;
|
||||
pub const parseTypeScriptDecorators = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypeScriptDecorators;
|
||||
pub const parseDecorators = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseDecorators;
|
||||
pub const parseDecoratorExpression = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseDecoratorExpression;
|
||||
pub const parseTypeScriptNamespaceStmt = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypeScriptNamespaceStmt;
|
||||
pub const parseTypeScriptImportEqualsStmt = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypeScriptImportEqualsStmt;
|
||||
pub const parseTypescriptEnumStmt = @import("./parseTypescript.zig").ParseTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).parseTypescriptEnumStmt;
|
||||
@@ -201,10 +203,12 @@ pub fn Parse(
|
||||
.extends = extends,
|
||||
.close_brace_loc = close_brace_loc,
|
||||
.ts_decorators = ExprNodeList.fromOwnedSlice(class_opts.ts_decorators),
|
||||
.decorators = class_opts.decorators,
|
||||
.class_keyword = class_keyword,
|
||||
.body_loc = body_loc,
|
||||
.properties = properties.items,
|
||||
.has_decorators = has_decorators or class_opts.ts_decorators.len > 0,
|
||||
.has_decorators = has_decorators or class_opts.ts_decorators.len > 0 or class_opts.decorators.len > 0,
|
||||
.should_lower_standard_decorators = class_opts.decorators.len > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -557,6 +561,9 @@ pub fn Parse(
|
||||
if (opts.ts_decorators) |dec| {
|
||||
class_opts.ts_decorators = dec.values;
|
||||
}
|
||||
if (opts.decorators) |dec| {
|
||||
class_opts.decorators = dec;
|
||||
}
|
||||
|
||||
const scope_index = p.pushScopeForParsePass(.class_name, loc) catch unreachable;
|
||||
const class = try p.parseClass(class_keyword, name, class_opts);
|
||||
|
||||
@@ -445,8 +445,13 @@ pub fn ParseStmt(
|
||||
}
|
||||
fn t_at(p: *P, opts: *ParseStatementOptions) anyerror!Stmt {
|
||||
// Parse decorators before class statements, which are potentially exported
|
||||
if (is_typescript_enabled) {
|
||||
const scope_index = p.scopes_in_order.items.len;
|
||||
const scope_index = p.scopes_in_order.items.len;
|
||||
|
||||
// Determine whether to use experimental TypeScript decorators or standard ECMAScript decorators
|
||||
const use_experimental = is_typescript_enabled and p.options.experimental_decorators;
|
||||
|
||||
if (use_experimental) {
|
||||
// Use TypeScript experimental decorators
|
||||
const ts_decorators = try p.parseTypeScriptDecorators();
|
||||
|
||||
// If this turns out to be a "declare class" statement, we need to undo the
|
||||
@@ -462,27 +467,27 @@ pub fn ParseStmt(
|
||||
.values = ts_decorators,
|
||||
.scope_index = scope_index,
|
||||
};
|
||||
|
||||
// "@decorator class Foo {}"
|
||||
// "@decorator abstract class Foo {}"
|
||||
// "@decorator declare class Foo {}"
|
||||
// "@decorator declare abstract class Foo {}"
|
||||
// "@decorator export class Foo {}"
|
||||
// "@decorator export abstract class Foo {}"
|
||||
// "@decorator export declare class Foo {}"
|
||||
// "@decorator export declare abstract class Foo {}"
|
||||
// "@decorator export default class Foo {}"
|
||||
// "@decorator export default abstract class Foo {}"
|
||||
if (p.lexer.token != .t_class and p.lexer.token != .t_export and !p.lexer.isContextualKeyword("abstract") and !p.lexer.isContextualKeyword("declare")) {
|
||||
try p.lexer.expected(.t_class);
|
||||
}
|
||||
|
||||
return p.parseStmt(opts);
|
||||
} else {
|
||||
// Use standard ECMAScript decorators
|
||||
const decorators = try p.parseDecorators(false);
|
||||
opts.decorators = decorators;
|
||||
}
|
||||
// notimpl();
|
||||
|
||||
try p.lexer.unexpected();
|
||||
return error.SyntaxError;
|
||||
// "@decorator class Foo {}"
|
||||
// "@decorator abstract class Foo {}"
|
||||
// "@decorator declare class Foo {}"
|
||||
// "@decorator declare abstract class Foo {}"
|
||||
// "@decorator export class Foo {}"
|
||||
// "@decorator export abstract class Foo {}"
|
||||
// "@decorator export declare class Foo {}"
|
||||
// "@decorator export declare abstract class Foo {}"
|
||||
// "@decorator export default class Foo {}"
|
||||
// "@decorator export default abstract class Foo {}"
|
||||
if (p.lexer.token != .t_class and p.lexer.token != .t_export and !p.lexer.isContextualKeyword("abstract") and !p.lexer.isContextualKeyword("declare")) {
|
||||
try p.lexer.expected(.t_class);
|
||||
}
|
||||
|
||||
return p.parseStmt(opts);
|
||||
}
|
||||
fn t_class(p: *P, opts: *ParseStatementOptions, loc: logger.Loc) anyerror!Stmt {
|
||||
if (opts.lexical_decl != .allow_all) {
|
||||
|
||||
@@ -32,6 +32,111 @@ pub fn ParseTypescript(
|
||||
return decorators.items;
|
||||
}
|
||||
|
||||
pub fn parseDecorators(p: *P, use_experimental: bool) ![]G.Decorator {
|
||||
var decorators = ListManaged(G.Decorator).init(p.allocator);
|
||||
|
||||
while (p.lexer.token == T.t_at) {
|
||||
const at_loc = p.lexer.loc();
|
||||
try p.lexer.next();
|
||||
|
||||
var value: Expr = undefined;
|
||||
|
||||
if (use_experimental and is_typescript_enabled) {
|
||||
// TypeScript's experimental decorator syntax is more permissive
|
||||
// Parse a new/call expression with "exprFlagTSDecorator" so we ignore
|
||||
// EIndex expressions, since they may be part of a computed property
|
||||
try p.parseExprWithFlags(.new, Expr.EFlags.ts_decorator, &value);
|
||||
} else {
|
||||
// JavaScript's decorator syntax is more restrictive
|
||||
// Parse using special decorator parser
|
||||
value = try p.parseDecoratorExpression();
|
||||
}
|
||||
|
||||
decorators.append(G.Decorator{
|
||||
.value = value,
|
||||
.at_loc = at_loc,
|
||||
.omit_newline_after = !p.lexer.has_newline_before,
|
||||
}) catch unreachable;
|
||||
}
|
||||
|
||||
return decorators.items;
|
||||
}
|
||||
|
||||
pub fn parseDecoratorExpression(p: *P) !Expr {
|
||||
// Handle parenthesized expression
|
||||
if (p.lexer.token == .t_open_paren) {
|
||||
try p.lexer.next();
|
||||
const value = try p.parseExpr(.lowest);
|
||||
try p.lexer.expect(.t_close_paren);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Parse member expression starting with identifier
|
||||
const name_range = p.lexer.range();
|
||||
const name = p.lexer.identifier;
|
||||
try p.lexer.expect(.t_identifier);
|
||||
|
||||
// Check for invalid identifiers
|
||||
if ((p.fn_or_arrow_data_parse.allow_await != .allow_ident and strings.eqlComptime(name, "await")) or
|
||||
(p.fn_or_arrow_data_parse.allow_yield != .allow_ident and strings.eqlComptime(name, "yield"))) {
|
||||
try p.log.addRangeError(p.source, name_range, try std.fmt.allocPrint(p.allocator, "Cannot use \"{s}\" as an identifier here", .{name}));
|
||||
}
|
||||
|
||||
var member_expr = p.newExpr(E.Identifier{ .ref = try p.storeNameInRef(name) }, name_range.loc);
|
||||
|
||||
while (true) {
|
||||
switch (p.lexer.token) {
|
||||
.t_exclamation => {
|
||||
// Skip over TypeScript non-null assertions
|
||||
if (p.lexer.has_newline_before) break;
|
||||
if (!is_typescript_enabled) try p.lexer.unexpected();
|
||||
try p.lexer.next();
|
||||
},
|
||||
.t_dot, .t_question_dot => {
|
||||
const is_optional = p.lexer.token == .t_question_dot;
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.token == .t_private_identifier) {
|
||||
const private_name = p.lexer.identifier;
|
||||
const private_loc = p.lexer.loc();
|
||||
try p.lexer.next();
|
||||
member_expr = p.newExpr(E.Index{
|
||||
.target = member_expr,
|
||||
.index = p.newExpr(E.PrivateIdentifier{
|
||||
.ref = try p.storeNameInRef(private_name),
|
||||
}, private_loc),
|
||||
.optional_chain = if (is_optional) .start else null,
|
||||
}, member_expr.loc);
|
||||
} else {
|
||||
const name_loc = p.lexer.loc();
|
||||
const field_name = p.lexer.identifier;
|
||||
try p.lexer.expect(.t_identifier);
|
||||
member_expr = p.newExpr(E.Dot{
|
||||
.target = member_expr,
|
||||
.name = field_name,
|
||||
.name_loc = name_loc,
|
||||
.optional_chain = if (is_optional) .start else null,
|
||||
}, member_expr.loc);
|
||||
}
|
||||
},
|
||||
.t_open_paren => {
|
||||
const args = try p.parseCallArgs();
|
||||
member_expr = p.newExpr(E.Call{
|
||||
.target = member_expr,
|
||||
.args = args.list,
|
||||
.close_paren_loc = args.loc,
|
||||
.optional_chain = null,
|
||||
}, member_expr.loc);
|
||||
// Grammar forbids anything after call expression in decorators
|
||||
break;
|
||||
},
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
|
||||
return member_expr;
|
||||
}
|
||||
|
||||
pub fn parseTypeScriptNamespaceStmt(p: *P, loc: logger.Loc, opts: *ParseStatementOptions) anyerror!Stmt {
|
||||
// "namespace foo {}";
|
||||
const name_loc = p.lexer.loc();
|
||||
|
||||
@@ -806,12 +806,14 @@ const LexicalDecl = enum(u8) { forbid, allow_all, allow_fn_inside_if, allow_fn_i
|
||||
|
||||
pub const ParseClassOptions = struct {
|
||||
ts_decorators: []Expr = &[_]Expr{},
|
||||
decorators: []G.Decorator = &[_]G.Decorator{},
|
||||
allow_ts_decorators: bool = false,
|
||||
is_type_script_declare: bool = false,
|
||||
};
|
||||
|
||||
pub const ParseStatementOptions = struct {
|
||||
ts_decorators: ?DeferredTsDecorators = null,
|
||||
decorators: ?[]G.Decorator = null,
|
||||
lexical_decl: LexicalDecl = .forbid,
|
||||
is_module_scope: bool = false,
|
||||
is_namespace_scope: bool = false,
|
||||
@@ -822,8 +824,11 @@ pub const ParseStatementOptions = struct {
|
||||
is_for_loop_init: bool = false,
|
||||
|
||||
pub fn hasDecorators(self: *ParseStatementOptions) bool {
|
||||
const decs = self.ts_decorators orelse return false;
|
||||
return decs.values.len > 0;
|
||||
if (self.decorators) |decs| {
|
||||
if (decs.len > 0) return true;
|
||||
}
|
||||
const ts_decs = self.ts_decorators orelse return false;
|
||||
return ts_decs.values.len > 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ pub const TSConfigJSON = struct {
|
||||
|
||||
emit_decorator_metadata: bool = false,
|
||||
|
||||
experimental_decorators: ?bool = null,
|
||||
|
||||
pub fn hasBaseURL(tsconfig: *const TSConfigJSON) bool {
|
||||
return tsconfig.base_url.len > 0;
|
||||
}
|
||||
@@ -233,6 +235,13 @@ pub const TSConfigJSON = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse "experimentalDecorators"
|
||||
if (compiler_opts.expr.asProperty("experimentalDecorators")) |experimental_decorators_prop| {
|
||||
if (experimental_decorators_prop.expr.asBool()) |val| {
|
||||
result.experimental_decorators = val;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse "importsNotUsedAsValues"
|
||||
if (compiler_opts.expr.asProperty("importsNotUsedAsValues")) |jsx_prop| {
|
||||
// This should never allocate since it will be utf8
|
||||
|
||||
364
test/js/bun/transpiler/decorators.test.ts
Normal file
364
test/js/bun/transpiler/decorators.test.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
describe("ECMAScript Decorators", () => {
|
||||
test("class decorators - basic", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"test.js": `
|
||||
let decoratorCalled = false;
|
||||
let originalClass;
|
||||
|
||||
function decorator(cls, ctx) {
|
||||
decoratorCalled = true;
|
||||
originalClass = cls;
|
||||
console.log("decorator called:", ctx.kind, ctx.name);
|
||||
}
|
||||
|
||||
@decorator
|
||||
class Foo {
|
||||
value = 42;
|
||||
}
|
||||
|
||||
console.log("decoratorCalled:", decoratorCalled);
|
||||
console.log("same class:", Foo === originalClass);
|
||||
console.log("instance value:", new Foo().value);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("decorator called: class Foo");
|
||||
expect(stdout).toContain("decoratorCalled: true");
|
||||
expect(stdout).toContain("same class: true");
|
||||
expect(stdout).toContain("instance value: 42");
|
||||
});
|
||||
|
||||
test("method decorators", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"test.js": `
|
||||
function logMethod(fn, ctx) {
|
||||
console.log("decorating method:", ctx.kind, ctx.name);
|
||||
return function(...args) {
|
||||
console.log("calling method:", ctx.name, "with args:", args);
|
||||
return fn.apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
class Calculator {
|
||||
@logMethod
|
||||
add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
}
|
||||
|
||||
const calc = new Calculator();
|
||||
console.log("result:", calc.add(2, 3));
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("decorating method: method add");
|
||||
expect(stdout).toContain("calling method: add with args: [2,3]");
|
||||
expect(stdout).toContain("result: 5");
|
||||
});
|
||||
|
||||
test("field decorators", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"test.js": `
|
||||
function defaultValue(value) {
|
||||
return function(target, ctx) {
|
||||
console.log("decorating field:", ctx.kind, ctx.name);
|
||||
return function(initialValue) {
|
||||
return initialValue ?? value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Config {
|
||||
@defaultValue("default")
|
||||
name;
|
||||
|
||||
@defaultValue(100)
|
||||
timeout;
|
||||
}
|
||||
|
||||
const config = new Config();
|
||||
console.log("name:", config.name);
|
||||
console.log("timeout:", config.timeout);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("decorating field: field name");
|
||||
expect(stdout).toContain("decorating field: field timeout");
|
||||
expect(stdout).toContain('name: default');
|
||||
expect(stdout).toContain("timeout: 100");
|
||||
});
|
||||
|
||||
test("accessor decorators", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"test.js": `
|
||||
function logged(accessor, ctx) {
|
||||
const { get, set } = accessor;
|
||||
console.log("decorating accessor:", ctx.kind, ctx.name);
|
||||
|
||||
return {
|
||||
get() {
|
||||
const value = get.call(this);
|
||||
console.log("getting", ctx.name, ":", value);
|
||||
return value;
|
||||
},
|
||||
set(value) {
|
||||
console.log("setting", ctx.name, "to:", value);
|
||||
set.call(this, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class State {
|
||||
@logged
|
||||
accessor value = 42;
|
||||
}
|
||||
|
||||
const state = new State();
|
||||
console.log("initial:", state.value);
|
||||
state.value = 100;
|
||||
console.log("updated:", state.value);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("decorating accessor: accessor value");
|
||||
expect(stdout).toContain("getting value : 42");
|
||||
expect(stdout).toContain("setting value to: 100");
|
||||
expect(stdout).toContain("getting value : 100");
|
||||
});
|
||||
|
||||
test("multiple decorators", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"test.js": `
|
||||
function first(cls, ctx) {
|
||||
console.log("first decorator");
|
||||
return class extends cls {
|
||||
firstAdded = true;
|
||||
};
|
||||
}
|
||||
|
||||
function second(cls, ctx) {
|
||||
console.log("second decorator");
|
||||
return class extends cls {
|
||||
secondAdded = true;
|
||||
};
|
||||
}
|
||||
|
||||
@first
|
||||
@second
|
||||
class MyClass {
|
||||
original = true;
|
||||
}
|
||||
|
||||
const instance = new MyClass();
|
||||
console.log("original:", instance.original);
|
||||
console.log("firstAdded:", instance.firstAdded);
|
||||
console.log("secondAdded:", instance.secondAdded);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("second decorator");
|
||||
expect(stdout).toContain("first decorator");
|
||||
expect(stdout).toContain("original: true");
|
||||
expect(stdout).toContain("firstAdded: true");
|
||||
expect(stdout).toContain("secondAdded: true");
|
||||
});
|
||||
|
||||
test("decorator metadata", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"test.js": `
|
||||
// Polyfill Symbol.metadata if not available
|
||||
if (!('metadata' in Symbol)) {
|
||||
Symbol.metadata = Symbol('Symbol.metadata');
|
||||
}
|
||||
if (!(Symbol.metadata in Function)) {
|
||||
Object.defineProperty(Function.prototype, Symbol.metadata, { value: null });
|
||||
}
|
||||
|
||||
function addMetadata(key, value) {
|
||||
return function(target, ctx) {
|
||||
ctx.metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@addMetadata("type", "component")
|
||||
class MyComponent {
|
||||
@addMetadata("type", "property")
|
||||
name = "Component";
|
||||
|
||||
@addMetadata("type", "method")
|
||||
render() {}
|
||||
}
|
||||
|
||||
const metadata = MyComponent[Symbol.metadata];
|
||||
console.log("class metadata type:", metadata?.type);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("class metadata type: component");
|
||||
});
|
||||
|
||||
test("tsconfig experimentalDecorators vs standard decorators", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"tsconfig.json": `{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": false
|
||||
}
|
||||
}`,
|
||||
"test.ts": `
|
||||
// This should use standard ECMAScript decorators, not TypeScript experimental
|
||||
function decorator(cls: any, ctx: any) {
|
||||
console.log("Standard decorator context:", ctx.kind);
|
||||
}
|
||||
|
||||
@decorator
|
||||
class Foo {}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.ts"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Standard decorator context: class");
|
||||
});
|
||||
|
||||
test("tsconfig with experimental decorators enabled", async () => {
|
||||
using dir = tempDir("decorator-test", {
|
||||
"tsconfig.json": `{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}`,
|
||||
"test.ts": `
|
||||
// This should use TypeScript experimental decorators
|
||||
function decorator(target: any) {
|
||||
console.log("Experimental decorator target:", target.name);
|
||||
}
|
||||
|
||||
@decorator
|
||||
class Foo {}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.ts"],
|
||||
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(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Experimental decorator target: Foo");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user