Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 79 additions & 9 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6501,6 +6501,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mut instance_members = BumpVec::<Stmt>::new_in(self.arena);
let mut static_members = BumpVec::<Stmt>::new_in(self.arena);
let mut class_properties = BumpVec::<G::Property>::new_in(self.arena);
// Emitted as one `var` statement before the class.
let mut computed_key_decls = BumpVec::<Decl>::new_in(self.arena);

for prop in s_class.class.properties.slice_mut().iter_mut() {
// merge parameter decorators with method decorators
Expand Down Expand Up @@ -6550,8 +6552,45 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// TODO: prop.kind == .declare and prop.value == null

if prop.ts_decorators.len_u32() > 0 {
let descriptor_key = prop.key.expect("infallible: prop has key");
let loc = descriptor_key.loc;
let key = prop.key.expect("infallible: prop has key");
let loc = key.loc;

// The key is used both to define the member and as the
// `__legacyDecorateClassTS` argument. Printing a computed key in both
// places evaluates it twice, so an impure key would decorate a different
// property than the one defined. Evaluate it once into a temporary
// instead. Methods do that in place, like tsc: `[_computedKey = expr]() {}`.
// Fields are moved out of the body below, and the constructor that
// receives them can already run while the class is being defined (a
// static initializer constructing the class), so their key is evaluated
// before the class: `var _computedKey = expr;`.
let key_is_evaluated = prop.flags.contains(Flags::Property::IsComputed)
&& !key.unwrap_inlined().is_primitive_literal();
let descriptor_key: Expr = if key_is_evaluated {
let key_ref = self.declare_var_temp_ref(b"_computedKey");
let binding = self.b(B::Identifier { r#ref: key_ref }, loc);
self.record_usage(key_ref);
let key_temp = self.new_expr(E::Identifier::init(key_ref), loc);
if prop.flags.contains(Flags::Property::IsMethod) {
computed_key_decls.push(Decl {
binding,
value: None,
});
prop.key = Some(Expr::assign(key_temp, key));
} else {
computed_key_decls.push(Decl {
binding,
value: Some(key),
});
// Becomes the `this[_computedKey]` of the relocated initializer.
prop.key = Some(key_temp);
}

self.record_usage(key_ref);
self.new_expr(E::Identifier::init(key_ref), loc)
} else {
key
};

// TODO: when we have the `accessor` modifier, add `and !prop.flags.contains(.has_accessor_modifier)` to
// the if statement.
Expand Down Expand Up @@ -6609,11 +6648,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
]);
let args = ExprNodeList::from_arena_slice(args_slice);

let decorator = self.call_runtime(
prop.key.expect("infallible: prop has key").loc,
b"__legacyDecorateClassTS",
args,
);
let decorator = self.call_runtime(loc, b"__legacyDecorateClassTS", args);
let decorator_stmt = self.s(
S::SExpr {
value: decorator,
Expand Down Expand Up @@ -6806,12 +6841,24 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// https://github.com/evanw/esbuild/blob/e9413cc4f7ab87263ea244a999c6fa1f1e34dc65/internal/js_parser/js_parser_lower.go#L2742
}

let mut stmts_count: usize =
1 + static_members.len() + instance_decorators.len() + static_decorators.len();
let mut stmts_count: usize = 1
+ usize::from(!computed_key_decls.is_empty())
+ static_members.len()
+ instance_decorators.len()
+ static_decorators.len();
if s_class.class.ts_decorators.len_u32() > 0 {
stmts_count += 1;
}
let mut stmts = BumpVec::<Stmt>::with_capacity_in(stmts_count, self.arena);
if !computed_key_decls.is_empty() {
stmts.push(self.s(
S::Local {
decls: G::DeclList::from_bump_vec(computed_key_decls),
..Default::default()
},
stmt.loc,
));
}
stmts.push(stmt);
stmts.extend_from_slice(&static_members);
stmts.extend_from_slice(&instance_decorators);
Expand Down Expand Up @@ -7460,6 +7507,29 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
r#ref
}

/// A temporary for a `var` statement emitted next to the statement being visited. The
/// symbol is registered in the scope the `var` hoists to, so the renamer keeps it distinct
/// from the temporaries of sibling blocks sharing that binding, and as a declared symbol
/// of the current part, which is what the bundler's renamer reads for top-level names
/// (see `declare_generated_symbol`). Without the renamer, `generate_temp_ref_with_scope`
/// gives it a file-unique name instead.
fn declare_var_temp_ref(&mut self, default_name: &'a [u8]) -> Ref {
let mut scope = self.current_scope_ref();
while !scope.kind_stops_hoisting() {
scope = scope
.parent
.expect("infallible: the module scope stops hoisting");
}
let r#ref = self.generate_temp_ref_with_scope(Some(default_name), scope);
self.declared_symbols
.append(DeclaredSymbol {
ref_: r#ref,
is_top_level: scope == self.module_scope,
})
.expect("oom");
r#ref
}

pub(crate) fn should_lower_using_declarations(&self, stmts: &[Stmt]) -> bool {
// TODO: We do not support lowering await, but when we do this needs to point to that var
let lower_await = false;
Expand Down
63 changes: 63 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,69 @@ describe("bundler", () => {
stdout: '[{"Y":{"0":"A","1":"B","A":0,"B":1,"Z":1}},0,1]',
},
});
// experimentalDecorators lowering evaluates each decorated computed key into a temporary.
// A decorated field reads its temporary from the constructor, so the temporaries of every
// class in the chunk (other modules, sibling blocks in the same module) have to stay distinct.
itBundled("edgecase/TypeScriptDecoratorComputedKeyTemporaries", {
files: {
"/entry.ts": /* ts */ `
import { A } from "./a";
import { B } from "./b";
import { dec, key, decorated } from "./shared";
let C, D;
{
class Block { @dec [key("cField")] = 1 }
C = Block;
}
{
class Block { @dec [key("dField")] = 1 }
D = Block;
}
console.log(JSON.stringify({
decorated,
a: Object.keys(new A()),
aPrototype: Object.getOwnPropertyNames(A.prototype),
b: Object.keys(new B()),
bPrototype: Object.getOwnPropertyNames(B.prototype),
c: Object.keys(new C()),
d: Object.keys(new D()),
}));
`,
"/a.ts": /* ts */ `
import { dec, key } from "./shared";
export class A {
@dec [key("aField")] = 1;
@dec [key("aMethod")]() {}
}
`,
"/b.ts": /* ts */ `
import { dec, key } from "./shared";
export class B {
@dec [key("bField")] = 1;
@dec [key("bMethod")]() {}
}
`,
"/shared.ts": /* ts */ `
let evaluations = 0;
export const decorated: string[] = [];
// Returns a different key every time it is evaluated.
export const key = (name: string) => name + ++evaluations;
export const dec = (_target: unknown, propertyKey: string) => { decorated.push(propertyKey); };
`,
"/tsconfig.json": /* json */ `{ "compilerOptions": { "experimentalDecorators": true } }`,
},
run: {
stdout: JSON.stringify({
decorated: ["aField1", "aMethod2", "bField3", "bMethod4", "cField5", "dField6"],
a: ["aField1"],
aPrototype: ["constructor", "aMethod2"],
b: ["bField3"],
bPrototype: ["constructor", "bMethod4"],
c: ["cField5"],
d: ["dField6"],
}),
},
});
itBundled("edgecase/TypeScriptNamespaceSiblingVariable", {
files: {
"/entry.ts": `
Expand Down
Loading
Loading