aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Dylan Conway <35280289+dylan-conway@users.noreply.github.com> 2022-11-01 21:25:30 -0700
committerGravatar GitHub <noreply@github.com> 2022-11-01 21:25:30 -0700
commit54e7a6f57a80ac3a9e96348f0f796b219c958b73 (patch)
tree5155a900fb7d371a564c7e5c79d78f5518d19348
parent32826119fc69feb07faa581bed0b3fd76a154744 (diff)
downloadbun-54e7a6f57a80ac3a9e96348f0f796b219c958b73.tar.gz
bun-54e7a6f57a80ac3a9e96348f0f796b219c958b73.tar.zst
bun-54e7a6f57a80ac3a9e96348f0f796b219c958b73.zip
typescript decorators round 2 (#1445)
* __decorateClass, __decorateParam, lit test, wiptest test * decorator factories test * parameter decorator test * test for decorators with constructor parameter properties * Fix issue with `CryptoKey` and `SubtleCrypto` constructors * Limit concurrency for GitHub Actions due to issues with cache poisoning If multiple actions are running, sometimes the cache is poisoned from another action. We need to fix this, but this is an interim measure to make actions less flaky * Make these tests better * Move this to dependencies so the .a files upload * Fixup * temporary fix * Limit concurrency for MacOS Object actions * try againn * againn * Make `"tls"` an explicit object we pass instead of implicit top-level options cc @Electroid @colinhacks * Update server.zig * Prefer `BUN_PORT` over `PORT` * Fix typo in homebrew action * Run homebrew action when release is edited * Check published_at instead of draft in GitHub action * Implement `process.release` * Add missing dependencies to `make devcontainer` * Allow overriding node polyfills via `BUN_OVERRIDE_MODULE_PATH` * Add a stub for io_darwin on linux cc @sno2 hopefully this helps but i'm not sure * Add missing `break` * Download more RAM * feat(core): optimize zig slice (#1408) * feat(core): optimize zig slice * address concerns * Remove webcrypto from vendor-without-check * Update default tsconfig (#1418) Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu> * Enable `BUN_OVERRIDE_MODULE_PATH` in `bun wiptest` * Upgrade WebKit * space * Support getting cached values and pending activity in the bindings generator * Remove :scissors: function * constructor creation, initializers, handle static and computed fields with decorators * Updating libuwebsockets C API (#1423) * fix for repeated regex match calls and fix for '^' in character classes (#1419) * tests and formatting * fix for ^ in character class * formatting * test for repeated match and exec calls * create oniguruma regex for each exec/test * check errorCode from creating oniguruma regexp and always return {} on failure * oops * call onig_initialize once * fix incorrect escaping, removed unnecessary oniguruma settings * tests for "-" and "^" in character classes * free regex object before returns * force gc for some tests * Update React fizz server (#1432) * Update fizz server * Use production build Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu> * more decorator tests * optional setup function for loading elements, simulate clicks in lit test * fix createWriteStream (#1433) * fix createWriteStream * remove comment * Update build docs and commands for dev containers (#1438) * Update build documentation for dev containers * Add devcontainer-rebuild make target * Add make devcontainer-sh target * Fix missing .PHONY for vendor-without-check (#1437) * Fix check for ninja on Debian/Ubuntu (#1436) Even though the package is named ninja-build, the ninja binary is still named ninja, so use `which ninja` to check for it * Fix #1410 woops * await on DOMContentLoaded for elements instead of setup function * avoid lowering class if no decorators Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: Ashcon Partovi <ashcon@partovi.net> Co-authored-by: Carter Snook <cartersnook04@gmail.com> Co-authored-by: Colin McDonnell <colinmcd94@gmail.com> Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu> Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com> Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: Joรฃo Paquim <jpaquim@users.noreply.github.com>
-rw-r--r--src/js_ast.zig1
-rw-r--r--src/js_parser.zig225
-rw-r--r--src/runtime.footer.bun.js2
-rw-r--r--src/runtime.footer.js2
-rw-r--r--src/runtime.footer.node.js2
-rw-r--r--src/runtime.footer.with-refresh.js2
-rw-r--r--src/runtime.js12
-rw-r--r--src/runtime.zig16
-rw-r--r--test/bun.js/decorators.test.ts787
-rw-r--r--test/scripts/snippets.json1
-rw-r--r--test/snippets/package.json1
-rw-r--r--test/snippets/simple-lit-example.ts76
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);
+}