diff options
Diffstat (limited to '')
-rw-r--r-- | src/js_ast.zig | 1 | ||||
-rw-r--r-- | src/js_parser.zig | 225 | ||||
-rw-r--r-- | src/runtime.footer.bun.js | 2 | ||||
-rw-r--r-- | src/runtime.footer.js | 2 | ||||
-rw-r--r-- | src/runtime.footer.node.js | 2 | ||||
-rw-r--r-- | src/runtime.footer.with-refresh.js | 2 | ||||
-rw-r--r-- | src/runtime.js | 12 | ||||
-rw-r--r-- | src/runtime.zig | 16 | ||||
-rw-r--r-- | test/bun.js/decorators.test.ts | 787 | ||||
-rw-r--r-- | test/scripts/snippets.json | 1 | ||||
-rw-r--r-- | test/snippets/package.json | 1 | ||||
-rw-r--r-- | test/snippets/simple-lit-example.ts | 76 |
12 files changed, 1123 insertions, 4 deletions
diff --git a/src/js_ast.zig b/src/js_ast.zig index 452f6044c..31bc056fb 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -539,6 +539,7 @@ pub const G = struct { body_loc: logger.Loc = logger.Loc.Empty, close_brace_loc: logger.Loc = logger.Loc.Empty, properties: []Property = &([_]Property{}), + has_decorators: bool = false, }; // invalid shadowing if left as Comment diff --git a/src/js_parser.zig b/src/js_parser.zig index 052edc6b1..af71b5453 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -1014,6 +1014,8 @@ const StaticSymbolName = struct { pub const __HMRClient = NewStaticSymbol("Bun"); pub const __FastRefreshModule = NewStaticSymbol("FastHMR"); pub const __FastRefreshRuntime = NewStaticSymbol("FastRefresh"); + pub const __decorateClass = NewStaticSymbol("__decorateClass"); + pub const __decorateParam = NewStaticSymbol("__decorateParam"); pub const @"$$m" = NewStaticSymbol("$$m"); @@ -2010,6 +2012,8 @@ const FnOrArrowDataParse = struct { is_constructor: bool = false, is_typescript_declare: bool = false, + has_argument_decorators: bool = false, + is_return_disallowed: bool = false, is_this_disallowed: bool = false, @@ -2140,6 +2144,7 @@ const PropertyOpts = struct { class_has_extends: bool = false, allow_ts_decorators: bool = false, ts_decorators: []Expr = &[_]Expr{}, + has_argument_decorators: bool = false, }; pub const ScanPassResult = struct { @@ -5504,6 +5509,7 @@ fn NewParser_( // Only allow omitting the body if we're parsing TypeScript .allow_missing_body_for_type_script = is_typescript_enabled, }); + p.fn_or_arrow_data_parse.has_argument_decorators = false; if (comptime is_typescript_enabled) { // Don't output anything if it's just a forward declaration of a function @@ -5606,6 +5612,7 @@ fn NewParser_( p.fn_or_arrow_data_parse.allow_super_call = opts.allow_super_call; p.fn_or_arrow_data_parse.allow_super_property = opts.allow_super_property; + var arg_has_decorators: bool = false; var args = List(G.Arg){}; while (p.lexer.token != T.t_close_paren) { // Skip over "this" type annotations @@ -5626,6 +5633,9 @@ fn NewParser_( var ts_decorators: []ExprNodeIndex = &([_]ExprNodeIndex{}); if (opts.allow_ts_decorators) { ts_decorators = try p.parseTypeScriptDecorators(); + if (ts_decorators.len > 0) { + arg_has_decorators = true; + } } if (!func.flags.contains(.has_rest_arg) and p.lexer.token == T.t_dot_dot_dot) { @@ -5734,6 +5744,8 @@ fn NewParser_( try p.lexer.expect(.t_close_paren); p.fn_or_arrow_data_parse = std.mem.bytesToValue(@TypeOf(p.fn_or_arrow_data_parse), &old_fn_or_arrow_data); + p.fn_or_arrow_data_parse.has_argument_decorators = arg_has_decorators; + // "function foo(): any {}" if (is_typescript_enabled and p.lexer.token == .t_colon) { try p.lexer.next(); @@ -9418,6 +9430,7 @@ fn NewParser_( .allow_await = if (is_async) .allow_expr else .allow_ident, .allow_yield = if (is_generator) .allow_expr else .allow_ident, }); + p.fn_or_arrow_data_parse.has_argument_decorators = false; p.validateFunctionName(func, .expr); p.popScope(); @@ -10232,6 +10245,9 @@ fn NewParser_( .allow_missing_body_for_type_script = is_typescript_enabled and opts.is_class, }); + opts.has_argument_decorators = opts.has_argument_decorators or p.fn_or_arrow_data_parse.has_argument_decorators; + p.fn_or_arrow_data_parse.has_argument_decorators = false; + // "class Foo { foo(): void; foo(): void {} }" if (func.flags.contains(.is_forward_declaration)) { // Skip this property entirely @@ -10335,6 +10351,7 @@ fn NewParser_( // been parsed. We need to start parsing from the "extends" clause. pub fn parseClass(p: *P, class_keyword: logger.Range, name: ?js_ast.LocRef, class_opts: ParseClassOptions) !G.Class { var extends: ?Expr = null; + var has_decorators: bool = false; if (p.lexer.token == .t_extends) { try p.lexer.next(); @@ -10386,12 +10403,13 @@ fn NewParser_( continue; } - opts = PropertyOpts{ .is_class = true, .allow_ts_decorators = class_opts.allow_ts_decorators, .class_has_extends = extends != null }; + opts = PropertyOpts{ .is_class = true, .allow_ts_decorators = class_opts.allow_ts_decorators, .class_has_extends = extends != null, .has_argument_decorators = false }; // Parse decorators for this property const first_decorator_loc = p.lexer.loc(); if (opts.allow_ts_decorators) { opts.ts_decorators = try p.parseTypeScriptDecorators(); + has_decorators = has_decorators or opts.ts_decorators.len > 0; } else { opts.ts_decorators = &[_]Expr{}; } @@ -10411,6 +10429,8 @@ fn NewParser_( else => {}, } } + + has_decorators = has_decorators or opts.has_argument_decorators; } } @@ -10433,6 +10453,7 @@ fn NewParser_( .class_keyword = class_keyword, .body_loc = body_loc, .properties = properties.toOwnedSlice(), + .has_decorators = has_decorators or class_opts.ts_decorators.len > 0, }; } @@ -17355,9 +17376,205 @@ fn NewParser_( ) []Stmt { switch (stmtorexpr) { .stmt => |stmt| { - var stmts = p.allocator.alloc(Stmt, 1) catch unreachable; - stmts[0] = stmt; - return stmts; + if (!stmt.data.s_class.class.has_decorators) { + var stmts = p.allocator.alloc(Stmt, 1) catch unreachable; + stmts[0] = stmt; + return stmts; + } + + var class = &stmt.data.s_class.class; + var constructor_function: ?*E.Function = null; + + var static_decorators = ListManaged(Stmt).init(p.allocator); + var instance_decorators = ListManaged(Stmt).init(p.allocator); + var instance_members = ListManaged(Stmt).init(p.allocator); + var static_members = ListManaged(Stmt).init(p.allocator); + var class_properties = ListManaged(Property).init(p.allocator); + + for (class.properties) |*prop| { + // merge parameter decorators with method decorators + if (prop.flags.contains(.is_method)) { + if (prop.value) |prop_value| { + switch (prop_value.data) { + .e_function => |func| { + const is_constructor = (prop.key.?.data == .e_string and prop.key.?.data.e_string.eqlComptime("constructor")); + for (func.func.args) |arg, i| { + for (arg.ts_decorators.ptr[0..arg.ts_decorators.len]) |arg_decorator| { + var decorators = if (is_constructor) class.ts_decorators.listManaged(p.allocator) else prop.ts_decorators.listManaged(p.allocator); + const args = p.allocator.alloc(Expr, 2) catch unreachable; + args[0] = p.e(E.Number{ .value = @intToFloat(f64, i) }, arg_decorator.loc); + args[1] = arg_decorator; + decorators.append(p.callRuntime(arg_decorator.loc, "__decorateParam", args)) catch unreachable; + if (is_constructor) { + class.ts_decorators.update(decorators); + } else { + prop.ts_decorators.update(decorators); + } + } + } + }, + else => unreachable, + } + } + } + + if (prop.flags.contains(.is_method)) { + if (prop.key.?.data == .e_string and prop.key.?.data.e_string.eqlComptime("constructor")) { + if (prop.value) |prop_value| { + switch (prop_value.data) { + .e_function => |func| { + constructor_function = func; + }, + else => unreachable, + } + } + } + } + + if (prop.ts_decorators.len > 0) { + const loc = prop.key.?.loc; + const descriptor_key = switch (prop.key.?.data) { + .e_identifier => |k| p.e(E.Identifier{ .ref = k.ref }, loc), + .e_number => |k| p.e(E.Number{ .value = k.value }, loc), + .e_string => |k| p.e(E.String{ .data = k.data }, loc), + else => undefined, + }; + + const descriptor_kind: f64 = if (!prop.flags.contains(.is_method)) 2 else 1; + + var target: Expr = undefined; + if (prop.flags.contains(.is_static)) { + p.recordUsage(class.class_name.?.ref.?); + target = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc); + } else { + target = p.e(E.Dot{ .target = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc), .name = "prototype", .name_loc = loc }, loc); + } + + const args = p.allocator.alloc(Expr, 4) catch unreachable; + args[0] = p.e(E.Array{ .items = prop.ts_decorators }, loc); + args[1] = target; + args[2] = descriptor_key; + args[3] = p.e(E.Number{ .value = descriptor_kind }, loc); + + const decorator = p.callRuntime(prop.key.?.loc, "__decorateClass", args); + const decorator_stmt = p.s(S.SExpr{ .value = decorator }, decorator.loc); + + if (prop.flags.contains(.is_static)) { + static_decorators.append(decorator_stmt) catch unreachable; + } else { + instance_decorators.append(decorator_stmt) catch unreachable; + } + } + + if (!prop.flags.contains(.is_method) and prop.key.?.data != .e_private_identifier and prop.ts_decorators.len > 0) { + // remove decorated fields without initializers to avoid assigning undefined. + const initializer = if (prop.initializer) |initializer_value| initializer_value else continue; + + var target: Expr = undefined; + if (prop.flags.contains(.is_static)) { + p.recordUsage(class.class_name.?.ref.?); + target = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc); + } else { + target = p.e(E.This{}, prop.key.?.loc); + } + + if (prop.flags.contains(.is_computed)) { + target = p.e(E.Index{ + .target = target, + .index = prop.key.?, + }, prop.key.?.loc); + } else { + target = p.e(E.Dot{ + .target = target, + .name = prop.key.?.data.e_string.data, + .name_loc = prop.key.?.loc, + }, prop.key.?.loc); + } + + // remove fields with decorators from class body. Move static members outside of class. + if (prop.flags.contains(.is_static)) { + static_members.append(Expr.assignStmt(target, initializer, p.allocator)) catch unreachable; + } else { + instance_members.append(Expr.assignStmt(target, initializer, p.allocator)) catch unreachable; + } + continue; + } + + class_properties.append(prop.*) catch unreachable; + } + + class.properties = class_properties.items; + + if (instance_members.items.len > 0 or class.extends != null) { + if (constructor_function == null) { + var properties = ListManaged(Property).fromOwnedSlice(p.allocator, class.properties); + var constructor_stmts = ListManaged(Stmt).init(p.allocator); + + if (class.extends != null) { + const target = p.e(E.Super{}, stmt.loc); + const arguments_ref = p.newSymbol(.unbound, "arguments") catch unreachable; + p.current_scope.generated.append(p.allocator, arguments_ref) catch unreachable; + + const super = p.e(E.Spread{ .value = p.e(E.Identifier{ .ref = arguments_ref }, stmt.loc) }, stmt.loc); + const args = ExprNodeList.one(p.allocator, super) catch unreachable; + + constructor_stmts.append(p.s(S.SExpr{ .value = p.e(E.Call{ .target = target, .args = args }, stmt.loc) }, stmt.loc)) catch unreachable; + } + + constructor_stmts.appendSlice(instance_members.items) catch unreachable; + + properties.insert(0, G.Property{ + .flags = Flags.Property.init(.{ .is_method = true }), + .key = p.e(E.String{ .data = "constructor" }, stmt.loc), + .value = p.e(E.Function{ .func = G.Fn{ + .name = null, + .open_parens_loc = logger.Loc.Empty, + .args = &[_]Arg{}, + .body = .{ .loc = stmt.loc, .stmts = constructor_stmts.items }, + .flags = Flags.Function.init(.{}), + } }, stmt.loc), + }) catch unreachable; + + class.properties = properties.items; + } else { + var constructor_stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, constructor_function.?.func.body.stmts); + // statements coming from class body inserted after super call or beginning of constructor. + var has_super = false; + for (constructor_stmts.items) |item, index| { + if (item.data != .s_expr or item.data.s_expr.value.data != .e_call or item.data.s_expr.value.data.e_call.target.data != .e_super) continue; + has_super = true; + constructor_stmts.insertSlice(index + 1, instance_members.items) catch unreachable; + } + if (!has_super) { + constructor_stmts.insertSlice(0, instance_members.items) catch unreachable; + } + + constructor_function.?.func.body.stmts = constructor_stmts.items; + } + } + + var stmts_count: usize = 1 + static_members.items.len + instance_decorators.items.len + static_decorators.items.len; + if (class.ts_decorators.len > 0) stmts_count += 1; + var stmts = ListManaged(Stmt).initCapacity(p.allocator, stmts_count) catch unreachable; + stmts.appendAssumeCapacity(stmt); + stmts.appendSliceAssumeCapacity(static_members.items); + stmts.appendSliceAssumeCapacity(instance_decorators.items); + stmts.appendSliceAssumeCapacity(static_decorators.items); + if (class.ts_decorators.len > 0) { + const args = p.allocator.alloc(Expr, 2) catch unreachable; + args[0] = p.e(E.Array{ .items = class.ts_decorators }, stmt.loc); + args[1] = p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc); + + stmts.appendAssumeCapacity(Expr.assignStmt( + p.e(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc), + p.callRuntime(stmt.loc, "__decorateClass", args), + p.allocator, + )); + + p.recordUsage(class.class_name.?.ref.?); + p.recordUsage(class.class_name.?.ref.?); + } + return stmts.items; }, .expr => |expr| { var stmts = p.allocator.alloc(Stmt, 1) catch unreachable; diff --git a/src/runtime.footer.bun.js b/src/runtime.footer.bun.js index a5466f53a..0c83ebc49 100644 --- a/src/runtime.footer.bun.js +++ b/src/runtime.footer.bun.js @@ -11,6 +11,8 @@ export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime; export var __exportValue = BUN_RUNTIME.__exportValue; export var __exportDefault = BUN_RUNTIME.__exportDefault; export var __merge = BUN_RUNTIME.__merge; +export var __decorateClass = BUN_RUNTIME.__decorateClass; +export var __decorateParam = BUN_RUNTIME.__decorateParam; export var $$bun_runtime_json_parse = JSON.parse; export var __internalIsCommonJSNamespace = BUN_RUNTIME.__internalIsCommonJSNamespace; diff --git a/src/runtime.footer.js b/src/runtime.footer.js index be4516473..062961b1f 100644 --- a/src/runtime.footer.js +++ b/src/runtime.footer.js @@ -19,6 +19,8 @@ export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime; export var __exportValue = BUN_RUNTIME.__exportValue; export var __exportDefault = BUN_RUNTIME.__exportDefault; export var __merge = BUN_RUNTIME.__merge; +export var __decorateClass = BUN_RUNTIME.__decorateClass; +export var __decorateParam = BUN_RUNTIME.__decorateParam; export var $$bun_runtime_json_parse = JSON.parse; export var __internalIsCommonJSNamespace = BUN_RUNTIME.__internalIsCommonJSNamespace; diff --git a/src/runtime.footer.node.js b/src/runtime.footer.node.js index 7d2b3a649..a6e425e44 100644 --- a/src/runtime.footer.node.js +++ b/src/runtime.footer.node.js @@ -12,6 +12,8 @@ export var __cJS2eSM = BUN_RUNTIME.__cJS2eSM; export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime; export var __exportValue = BUN_RUNTIME.__exportValue; export var __exportDefault = BUN_RUNTIME.__exportDefault; +export var __decorateClass = BUN_RUNTIME.__decorateClass; +export var __decorateParam = BUN_RUNTIME.__decorateParam; export var $$bun_runtime_json_parse = JSON.parse; export var __internalIsCommonJSNamespace = BUN_RUNTIME.__internalIsCommonJSNamespace; diff --git a/src/runtime.footer.with-refresh.js b/src/runtime.footer.with-refresh.js index 618329baf..a5b5a3b79 100644 --- a/src/runtime.footer.with-refresh.js +++ b/src/runtime.footer.with-refresh.js @@ -18,6 +18,8 @@ export var __cJS2eSM = BUN_RUNTIME.__cJS2eSM; export var regeneratorRuntime = BUN_RUNTIME.regeneratorRuntime; export var __exportValue = BUN_RUNTIME.__exportValue; export var __exportDefault = BUN_RUNTIME.__exportDefault; +export var __decorateClass = BUN_RUNTIME.__decorateClass; +export var __decorateParam = BUN_RUNTIME.__decorateParam; export var $$bun_runtime_json_parse = JSON.parse; export var __FastRefreshRuntime = BUN_RUNTIME.__FastRefreshRuntime; export var __internalIsCommonJSNamespace = diff --git a/src/runtime.js b/src/runtime.js index e1cf9e3df..fe2f5b9df 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -235,3 +235,15 @@ export var __merge = (props, defaultProps) => { ? defaultProps : mergeDefaultProps(props, defaultProps); }; + +export var __decorateClass = (decorators, target, key, kind) => { + var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; + for (var i = decorators.length - 1, decorator; i >= 0; i--) + if (decorator = decorators[i]) + result = (kind ? decorator(target, key, result) : decorator(result)) || result; + if (kind && result) + __defProp(target, key, result); + return result; +}; + +export var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
\ No newline at end of file diff --git a/src/runtime.zig b/src/runtime.zig index 1a490e4d2..87daae829 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -364,6 +364,8 @@ pub const Runtime = struct { __exportDefault: ?GeneratedSymbol = null, __FastRefreshRuntime: ?GeneratedSymbol = null, __merge: ?GeneratedSymbol = null, + __decorateClass: ?GeneratedSymbol = null, + __decorateParam: ?GeneratedSymbol = null, pub const all = [_][]const u8{ // __HMRClient goes first @@ -384,6 +386,8 @@ pub const Runtime = struct { "__exportDefault", "__FastRefreshRuntime", "__merge", + "__decorateClass", + "__decorateParam", }; pub const Name = "bun:wrap"; pub const alt_name = "bun:wrap"; @@ -483,6 +487,16 @@ pub const Runtime = struct { return Entry{ .key = 15, .value = val.ref }; } }, + 16 => { + if (@field(this.runtime_imports, all[16])) |val| { + return Entry{ .key = 16, .value = val.ref }; + } + }, + 17 => { + if (@field(this.runtime_imports, all[17])) |val| { + return Entry{ .key = 17, .value = val.ref }; + } + }, else => { return null; }, @@ -543,6 +557,8 @@ pub const Runtime = struct { 13 => (@field(imports, all[13]) orelse return null).ref, 14 => (@field(imports, all[14]) orelse return null).ref, 15 => (@field(imports, all[15]) orelse return null).ref, + 16 => (@field(imports, all[16]) orelse return null).ref, + 17 => (@field(imports, all[17]) orelse return null).ref, else => null, }; } diff --git a/test/bun.js/decorators.test.ts b/test/bun.js/decorators.test.ts new file mode 100644 index 000000000..545030ef1 --- /dev/null +++ b/test/bun.js/decorators.test.ts @@ -0,0 +1,787 @@ +import { test, expect } from "bun:test"; + +test("decorator order of evaluation", () => { + let counter = 0; + const computedProp: unique symbol = Symbol("computedProp"); + + @decorator1 + @decorator2 + class BugReport { + @decorator7 + type: string; + + @decorator3 + x: number = 20; + + @decorator5 + private _y: number = 12; + + @decorator10 + get y() { + return this._y; + } + @decorator11 + set y(newY: number) { + this._y = newY; + } + + @decorator9 + [computedProp]: string = "yes"; + + constructor(@decorator8 type: string) { + this.type = type; + } + + @decorator6 + move(newX: number, @decorator12 newY: number) { + this.x = newX; + this._y = newY; + } + + @decorator4 + jump() { + this._y += 30; + } + } + + function decorator1(target, propertyKey) { + expect(counter++).toBe(11); + expect(target === BugReport).toBe(true); + expect(propertyKey).toBe(undefined); + } + + function decorator2(target, propertyKey) { + expect(counter++).toBe(10); + expect(target === BugReport).toBe(true); + expect(propertyKey).toBe(undefined); + } + + function decorator3(target, propertyKey) { + expect(counter++).toBe(1); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("x"); + } + + function decorator4(target, propertyKey) { + expect(counter++).toBe(8); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("jump"); + } + + function decorator5(target, propertyKey) { + expect(counter++).toBe(2); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("_y"); + } + + function decorator6(target, propertyKey) { + expect(counter++).toBe(7); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("move"); + } + + function decorator7(target, propertyKey) { + expect(counter++).toBe(0); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("type"); + } + + function decorator8(target, propertyKey) { + expect(counter++).toBe(9); + expect(target === BugReport).toBe(true); + expect(propertyKey).toBe(undefined); + } + + function decorator9(target, propertyKey) { + expect(counter++).toBe(5); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe(computedProp); + } + + function decorator10(target, propertyKey) { + expect(counter++).toBe(3); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("y"); + } + + function decorator11(target, propertyKey) { + expect(counter++).toBe(4); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("y"); + } + + function decorator12(target, propertyKey) { + expect(counter++).toBe(6); + expect(target === BugReport.prototype).toBe(true); + expect(propertyKey).toBe("move"); + } +}); + +test("decorator factories order of evaluation", () => { + let counter = 0; + const computedProp: unique symbol = Symbol("computedProp"); + + @decorator1() + @decorator2() + class BugReport { + @decorator7() + type: string; + + @decorator3() + x: number = 20; + + @decorator5() + private _y: number = 12; + + @decorator10() + get y() { + return this._y; + } + @decorator11() + set y(newY: number) { + this._y = newY; + } + + @decorator9() + [computedProp]: string = "yes"; + + constructor(@decorator8() type: string) { + this.type = type; + } + + @decorator6() + move(newX: number, @decorator12() newY: number) { + this.x = newX; + this._y = newY; + } + + @decorator4() + jump() { + this._y += 30; + } + } + + function decorator1() { + expect(counter++).toBe(18); + return function (target, descriptorKey) { + expect(counter++).toBe(23); + }; + } + + function decorator2() { + expect(counter++).toBe(19); + return function (target, descriptorKey) { + expect(counter++).toBe(22); + }; + } + + function decorator3() { + expect(counter++).toBe(2); + return function (target, descriptorKey) { + expect(counter++).toBe(3); + }; + } + + function decorator4() { + expect(counter++).toBe(16); + return function (target, descriptorKey) { + expect(counter++).toBe(17); + }; + } + + function decorator5() { + expect(counter++).toBe(4); + return function (target, descriptorKey) { + expect(counter++).toBe(5); + }; + } + + function decorator6() { + expect(counter++).toBe(12); + return function (target, descriptorKey) { + expect(counter++).toBe(15); + }; + } + + function decorator7() { + expect(counter++).toBe(0); + return function (target, descriptorKey) { + expect(counter++).toBe(1); + }; + } + + function decorator8() { + expect(counter++).toBe(20); + return function (target, descriptorKey) { + expect(counter++).toBe(21); + }; + } + + function decorator9() { + expect(counter++).toBe(10); + return function (target, descriptorKey) { + expect(counter++).toBe(11); + }; + } + + function decorator10() { + expect(counter++).toBe(6); + return function (target, descriptorKey) { + expect(counter++).toBe(7); + }; + } + + function decorator11() { + expect(counter++).toBe(8); + return function (target, descriptorKey) { + expect(counter++).toBe(9); + }; + } + + function decorator12() { + expect(counter++).toBe(13); + return function (target, descriptorKey) { + expect(counter++).toBe(14); + }; + } +}); + +test("parameter decorators", () => { + let counter = 0; + class HappyDecorator { + width: number; + height: number; + x: number; + y: number; + + move(@d4 x: number, @d5 @d6 y: number) { + this.x = x; + this.y = y; + } + + constructor( + one: number, + two: string, + three: boolean, + @d1 @d2 width: number, + @d3 height: number + ) { + this.width = width; + this.height = height; + } + + dance(@d7 @d8 intensity: number) { + this.width *= intensity; + this.height *= intensity; + } + } + + function d1(target, propertyKey, parameterIndex) { + expect(counter++).toBe(7); + expect(target === HappyDecorator).toBe(true); + expect(propertyKey).toBe(undefined); + expect(parameterIndex).toBe(3); + } + + function d2(target, propertyKey, parameterIndex) { + expect(counter++).toBe(6); + expect(target === HappyDecorator).toBe(true); + expect(propertyKey).toBe(undefined); + expect(parameterIndex).toBe(3); + } + + function d3(target, propertyKey, parameterIndex) { + expect(counter++).toBe(5); + expect(target === HappyDecorator).toBe(true); + expect(propertyKey).toBe(undefined); + expect(parameterIndex).toBe(4); + } + + function d4(target, propertyKey, parameterIndex) { + expect(counter++).toBe(2); + expect(target === HappyDecorator.prototype).toBe(true); + expect(propertyKey).toBe("move"); + expect(parameterIndex).toBe(0); + } + + function d5(target, propertyKey, parameterIndex) { + expect(counter++).toBe(1); + expect(target === HappyDecorator.prototype).toBe(true); + expect(propertyKey).toBe("move"); + expect(parameterIndex).toBe(1); + } + + function d6(target, propertyKey, parameterIndex) { + expect(counter++).toBe(0); + expect(target === HappyDecorator.prototype).toBe(true); + expect(propertyKey).toBe("move"); + expect(parameterIndex).toBe(1); + } + + function d7(target, propertyKey, parameterIndex) { + expect(counter++).toBe(4); + expect(target === HappyDecorator.prototype).toBe(true); + expect(propertyKey).toBe("dance"); + expect(parameterIndex).toBe(0); + } + + function d8(target, propertyKey, parameterIndex) { + expect(counter++).toBe(3); + expect(target === HappyDecorator.prototype).toBe(true); + expect(propertyKey).toBe("dance"); + expect(parameterIndex).toBe(0); + } + + class Maybe { + constructor( + @m1 private x: number, + @m2 public y: boolean, + @m3 protected z: string + ) {} + } + + function m1(target, propertyKey, index) { + expect(target === Maybe).toBe(true); + expect(propertyKey).toBe(undefined); + expect(index).toBe(0); + } + + function m2(target, propertyKey, index) { + expect(target === Maybe).toBe(true); + expect(propertyKey).toBe(undefined); + expect(index).toBe(1); + } + + function m3(target, propertyKey, index) { + expect(target === Maybe).toBe(true); + expect(propertyKey).toBe(undefined); + expect(index).toBe(2); + } +}); + +test("decorators random", () => { + @Frozen + class IceCream {} + + function Frozen(constructor: Function) { + Object.freeze(constructor); + Object.freeze(constructor.prototype); + } + + expect(Object.isFrozen(IceCream)).toBe(true); + + class IceCreamComponent { + @Emoji() + flavor = "vanilla"; + } + + // Property Decorator + function Emoji() { + return function (target: Object, key: string | symbol) { + let val = target[key]; + + const getter = () => { + return val; + }; + const setter = (next) => { + val = `๐ฆ ${next} ๐ฆ`; + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + }; + } + + const iceCream = new IceCreamComponent(); + expect(iceCream.flavor === "๐ฆ vanilla ๐ฆ").toBe(true); + iceCream.flavor = "chocolate"; + expect(iceCream.flavor === "๐ฆ chocolate ๐ฆ").toBe(true); + + const i: unique symbol = Symbol.for("i"); + const h: unique symbol = Symbol.for("h"); + const t: unique symbol = Symbol.for("t"); + const q: unique symbol = Symbol.for("q"); + const p: unique symbol = Symbol.for("p"); + const u3: unique symbol = Symbol.for("u3"); + const u5: unique symbol = Symbol.for("u5"); + const u6: unique symbol = Symbol.for("u6"); + const u8: unique symbol = Symbol.for("u8"); + + class S { + @StringAppender("๐") k = 35; + @StringAppender("๐ค ") static j = 4; + @StringAppender("๐ตโ๐ซ") private static [h] = 30; + @StringAppender("๐คฏ") private static u = 60; + @StringAppender("๐คช") private [t] = 32; + @StringAppender("๐ค") [i] = 8; + @StringAppender("๐") private e = 10; + @StringAppender("๐ป") static [q] = 202; + @StringAppender("๐") r = S[h]; + _y: number; + @StringAppender("๐คก") get y() { + return this._y; + } + set y(next) { + this._y = next; + } + #o = 100; + + @StringAppender("๐") u1: number; + @StringAppender("๐ฅณ") static u2: number; + @StringAppender("๐ค") private static [u3]: number; + @StringAppender("๐ฅบ") private static u4: number; + @StringAppender("๐คฏ") private [u5]: number; + @StringAppender("๐คฉ") [u6]: number; + @StringAppender("โน๏ธ") private u7: number; + @StringAppender("๐") static [u8]: number; + + @StringAppender("๐ค") u9 = this.u1; + @StringAppender("๐คจ") u10 = this.u2; + @StringAppender("๐") u11 = S[u3]; + @StringAppender("๐") u12 = S.u4; + @StringAppender("๐") u13 = this[u5]; + @StringAppender("๐") u14 = this[u6]; + @StringAppender("๐ถ") u15 = this.u7; + @StringAppender("๐") u16 = S[u8]; + + constructor() { + this.k = 3; + expect(this.k).toBe("3 ๐"); + expect(S.j).toBe(4); + expect(this[i]).toBe("8 ๐ค"); + expect(this.e).toBe("10 ๐"); + expect(S[h]).toBe(30); + expect(S.u).toBe(60); + expect(this[t]).toBe("32 ๐คช"); + expect(S[q]).toBe(202); + expect(this.#o).toBe(100); + expect(this.r).toBe("30 ๐"); + expect(this.y).toBe(undefined); + this.y = 100; + expect(this.y).toBe(100); + + expect(this.u1).toBe(undefined); + expect(S.u2).toBe(undefined); + expect(S[u3]).toBe(undefined); + expect(S.u4).toBe(undefined); + expect(this[u5]).toBe(undefined); + expect(this[u6]).toBe(undefined); + expect(this.u7).toBe(undefined); + expect(S[u8]).toBe(undefined); + + expect(this.u9).toBe("undefined ๐ค"); + expect(this.u10).toBe("undefined ๐คจ"); + expect(this.u11).toBe("undefined ๐"); + expect(this.u12).toBe("undefined ๐"); + expect(this.u13).toBe("undefined ๐"); + expect(this.u14).toBe("undefined ๐"); + expect(this.u15).toBe("undefined ๐ถ"); + expect(this.u16).toBe("undefined ๐"); + + this.u1 = 100; + expect(this.u1).toBe("100 ๐"); + S.u2 = 100; + expect(S.u2).toBe("100 ๐ฅณ"); + S[u3] = 100; + expect(S[u3]).toBe("100 ๐ค"); + S.u4 = 100; + expect(S.u4).toBe("100 ๐ฅบ"); + this[u5] = 100; + expect(this[u5]).toBe("100 ๐คฏ"); + this[u6] = 100; + expect(this[u6]).toBe("100 ๐คฉ"); + this.u7 = 100; + expect(this.u7).toBe("100 โน๏ธ"); + S[u8] = 100; + expect(S[u8]).toBe("100 ๐"); + + expect(this.u9).toBe("undefined ๐ค"); + expect(this.u10).toBe("undefined ๐คจ"); + expect(this.u11).toBe("undefined ๐"); + expect(this.u12).toBe("undefined ๐"); + expect(this.u13).toBe("undefined ๐"); + expect(this.u14).toBe("undefined ๐"); + expect(this.u15).toBe("undefined ๐ถ"); + expect(this.u16).toBe("undefined ๐"); + } + } + + let s = new S(); + expect(s.u9).toBe("undefined ๐ค"); + expect(s.u10).toBe("undefined ๐คจ"); + expect(s.u11).toBe("undefined ๐"); + expect(s.u12).toBe("undefined ๐"); + expect(s.u13).toBe("undefined ๐"); + expect(s.u14).toBe("undefined ๐"); + expect(s.u15).toBe("undefined ๐ถ"); + expect(s.u16).toBe("undefined ๐"); + + s.u9 = 35; + expect(s.u9).toBe("35 ๐ค"); + s.u10 = 36; + expect(s.u10).toBe("36 ๐คจ"); + s.u11 = 37; + expect(s.u11).toBe("37 ๐"); + s.u12 = 38; + expect(s.u12).toBe("38 ๐"); + s.u13 = 39; + expect(s.u13).toBe("39 ๐"); + s.u14 = 40; + expect(s.u14).toBe("40 ๐"); + s.u15 = 41; + expect(s.u15).toBe("41 ๐ถ"); + s.u16 = 42; + expect(s.u16).toBe("42 ๐"); + + function StringAppender(emoji: string) { + return function (target: Object, key: string | symbol) { + let val = target[key]; + + const getter = () => { + return val; + }; + const setter = (value) => { + val = `${value} ${emoji}`; + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + }; + } +}); + +test("class field order", () => { + class N { + l = 455; + } + class M { + u = 4; + @d1 w = 9; + constructor() { + // this.w = 9 should be moved here + expect(this.u).toBe(4); + expect(this.w).toBe(9); + this.u = 3; + this.w = 6; + expect(this.u).toBe(3); + expect(this.w).toBe(6); + } + } + + function d1(target, propertyKey) { + expect(target === M.prototype).toBe(true); + expect(propertyKey).toBe("w"); + } + + let m = new M(); + expect(m.u).toBe(3); + expect(m.w).toBe(6); +}); + +test("changing static method", () => { + class A { + static bar() { + return 1; + } + } + + @changeMethodReturn("bar", 5) + class A_2 { + static bar() { + return 7; + } + } + + function changeMethodReturn(method, value) { + return function (target) { + target[method] = function () { + return value; + }; + return target; + }; + } + + @changeMethodReturn("bar", 2) + class B extends A {} + + @changeMethodReturn("bar", 9) + class C extends B {} + + expect(A_2.bar()).toBe(5); + expect(A.bar()).toBe(1); + expect(B.bar()).toBe(2); + expect(C.bar()).toBe(9); +}); + +test("class extending from another class", () => { + class A { + a: number; + constructor() { + this.a = 3; + } + } + + class B extends A { + a: number = 9; + } + + expect(new A().a).toBe(3); + expect(new B().a).toBe(9); + + class C { + a: number = 80; + } + + class D extends C { + a: number = 32; + constructor() { + super(); + } + } + + expect(new C().a).toBe(80); + expect(new D().a).toBe(32); + + class E { + a: number = 40; + constructor() { + expect(this.a).toBe(40); + } + } + + class F extends E { + @d1 a: number = 50; + constructor() { + super(); + expect(this.a).toBe(50); + this.a = 60; + expect(this.a).toBe(60); + } + } + + function d1(target) { + target.a = 100; + } +}); + +test("decorated fields moving to constructor", () => { + class A { + @d1 a = 3; + @d2 b = 4; + @d3 c = 5; + } + + function d1(target, propertyKey) { + expect(target === A.prototype).toBe(true); + expect(propertyKey).toBe("a"); + } + + function d2(target, propertyKey) { + expect(target === A.prototype).toBe(true); + expect(propertyKey).toBe("b"); + } + + function d3(target, propertyKey) { + expect(target === A.prototype).toBe(true); + expect(propertyKey).toBe("c"); + } + + let a = new A(); + expect(a.a).toBe(3); + expect(a.b).toBe(4); + expect(a.c).toBe(5); +}); + +test("only class decorator", () => { + let a = 0; + @d1 + class A {} + + let aa = new A(); + + function d1(target) { + a = 1; + expect(target).toBe(A); + } + + expect(a).toBe(1); +}); + +test("only property decorators", () => { + let a = 0; + class A { + @d1 a() {} + } + + let b = 0; + class B { + @d2 b = 3; + } + + let c = 0; + class C { + @d3 get c() { + return 3; + } + } + + function d1(target, propertyKey) { + a = 1; + expect(target === A.prototype).toBe(true); + expect(propertyKey).toBe("a"); + } + expect(a).toBe(1); + + function d2(target, propertyKey) { + b = 1; + expect(target === B.prototype).toBe(true); + expect(propertyKey).toBe("b"); + } + expect(b).toBe(1); + + function d3(target, propertyKey) { + c = 1; + expect(target === C.prototype).toBe(true); + expect(propertyKey).toBe("c"); + } + expect(c).toBe(1); +}); + +test("only argument decorators", () => { + let a = 0; + class A { + a(@d1 a: string) {} + } + + function d1(target, propertyKey, parameterIndex) { + a = 1; + expect(target === A.prototype).toBe(true); + expect(propertyKey).toBe("a"); + expect(parameterIndex).toBe(0); + } + + expect(a).toBe(1); +}); + +test("no decorators", () => { + let a = 0; + class A { + b: number; + constructor() { + a = 1; + this.b = 300000; + } + } + + let aa = new A(); + expect(a).toBe(1); + expect(aa.b).toBe(300000); +}); diff --git a/test/scripts/snippets.json b/test/scripts/snippets.json index ebdec23d3..1829eb9c4 100644 --- a/test/scripts/snippets.json +++ b/test/scripts/snippets.json @@ -25,6 +25,7 @@ "/optional-chain-with-function.js", "/template-literal.js", "/number-literal-bug.js", + "/simple-lit-example.ts", "/caught-require.js", "/package-json-utf8.js", "/multiple-var.js", diff --git a/test/snippets/package.json b/test/snippets/package.json index 07b349f86..0c05b97be 100644 --- a/test/snippets/package.json +++ b/test/snippets/package.json @@ -4,6 +4,7 @@ "dependencies": { "@emotion/core": "^11.0.0", "@emotion/react": "^11.4.1", + "lit": "^2.4.0", "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/test/snippets/simple-lit-example.ts b/test/snippets/simple-lit-example.ts new file mode 100644 index 000000000..34446e418 --- /dev/null +++ b/test/snippets/simple-lit-example.ts @@ -0,0 +1,76 @@ +import { LitElement, html, css } from "lit"; +import { customElement, property, eventOptions } from "lit/decorators.js"; + +var loadedResolve; +var loadedPromise = new Promise((resolve) => { + loadedResolve = resolve; +}); + +if (document?.readyState === "loading") { + document.addEventListener( + "DOMContentLoaded", + () => { + loadedResolve(); + }, + { once: true } + ); +} else { + loadedResolve(); +} + +@customElement("my-element") +export class MyElement extends LitElement { + static styles = css` + :host { + display: inline-block; + padding: 10px; + background: lightgray; + } + .planet { + color: var(--planet-color, blue); + } + `; + + @property() planet = "Earth"; + + render() { + return html` + <span @click=${this.togglePlanet} class="planet" id="planet-id" + >${this.planet}</span + > + `; + } + + @eventOptions({ once: true }) + togglePlanet() { + this.planet = this.planet === "Earth" ? "Mars" : "Earth"; + } +} + +function setup() { + let element = document.createElement("my-element"); + element.id = "my-element-id"; + document.body.appendChild(element); +} + +export async function test() { + setup(); + await loadedPromise; + + let element = document.getElementById("my-element-id"); + let shadowRoot = element.shadowRoot; + let planet = shadowRoot.getElementById("planet-id"); + if (element.__planet !== "Earth") { + throw new Error("Unexpected planet name: " + element.__planet); + } + planet.click(); + if (element.__planet !== "Mars") { + throw new Error("Unexpected planet name: " + element.__planet); + } + planet.click(); + if (element.__planet !== "Mars") { + throw new Error("Unexpected planet name: " + element.__planet); + } + + return testDone(import.meta.url); +} |