Skip to content

Commit bdf7a06

Browse files
fix(jit): lower non-Angular decorators via __decorate (#204)
When a class has both Angular decorators (e.g. @Injectable) and non-Angular decorators (e.g. NGXS @State, @selector, @action), lower all decorators when converting class declarations to class expressions. Non-Angular member decorators are emitted as __decorate() calls matching TypeScript's tsc output: null for methods/accessors, void 0 for properties, instance members before static members. Co-authored-by: Ashley Hunter <ashh640@users.noreply.github.com>
1 parent 9724ae5 commit bdf7a06

19 files changed

+1690
-53
lines changed

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,33 @@ pub fn collect_member_decorator_spans(class: &Class<'_>, spans: &mut std::vec::V
10991099
}
11001100
}
11011101

1102+
/// Collect ALL decorator spans from class members (properties, methods, accessors),
1103+
/// regardless of whether they are Angular-specific or not.
1104+
///
1105+
/// This is used when lowering a class that has Angular decorators: since the class
1106+
/// declaration is converted to a class expression, ALL member decorators must be
1107+
/// removed (decorators are not valid on class expressions in TypeScript).
1108+
pub fn collect_all_member_decorator_spans(class: &Class<'_>, spans: &mut std::vec::Vec<Span>) {
1109+
for element in &class.body.body {
1110+
let decorators = match element {
1111+
ClassElement::PropertyDefinition(prop) => &prop.decorators,
1112+
ClassElement::MethodDefinition(method) => {
1113+
// Skip constructor - it's handled separately
1114+
if method.kind == MethodDefinitionKind::Constructor {
1115+
continue;
1116+
}
1117+
&method.decorators
1118+
}
1119+
ClassElement::AccessorProperty(accessor) => &accessor.decorators,
1120+
_ => continue,
1121+
};
1122+
1123+
for decorator in decorators {
1124+
spans.push(decorator.span);
1125+
}
1126+
}
1127+
}
1128+
11021129
#[cfg(test)]
11031130
mod tests {
11041131
use super::*;

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 171 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -685,8 +685,8 @@ enum AngularDecoratorKind {
685685
struct JitClassInfo {
686686
/// The class name.
687687
class_name: String,
688-
/// Span of the decorator (including @).
689-
decorator_span: Span,
688+
/// Spans of ALL class-level decorators (including @) to be removed.
689+
all_class_decorator_spans: std::vec::Vec<Span>,
690690
/// Start of the statement (includes export keyword if present).
691691
stmt_start: u32,
692692
/// Start of the class keyword.
@@ -701,10 +701,12 @@ struct JitClassInfo {
701701
is_abstract: bool,
702702
/// Constructor parameter info for ctorParameters.
703703
ctor_params: std::vec::Vec<JitCtorParam>,
704-
/// Member decorator info for propDecorators.
704+
/// Member decorator info for propDecorators (Angular decorators like @Input, @Output).
705705
member_decorators: std::vec::Vec<JitMemberDecorator>,
706-
/// The modified decorator expression text for __decorate call.
707-
decorator_text: String,
706+
/// All class-level decorator expression texts for __decorate call, in source order.
707+
all_class_decorator_texts: std::vec::Vec<String>,
708+
/// Non-Angular member decorators that need __decorate() calls.
709+
non_angular_member_decorators: std::vec::Vec<JitNonAngularMemberDecorator>,
708710
}
709711

710712
/// Constructor parameter info for JIT ctorParameters generation.
@@ -731,6 +733,19 @@ struct JitMemberDecorator {
731733
decorators: std::vec::Vec<JitParamDecorator>,
732734
}
733735

736+
/// A non-Angular member decorator that needs to be lowered via __decorate().
737+
struct JitNonAngularMemberDecorator {
738+
/// The member name.
739+
member_name: String,
740+
/// Whether the member is static.
741+
is_static: bool,
742+
/// Whether this is a property (field) vs a method/accessor.
743+
/// TypeScript uses `void 0` for properties and `null` for methods/accessors.
744+
is_property: bool,
745+
/// The decorator expression texts (e.g., "Selector()", "Action(AddTodo)").
746+
decorator_texts: std::vec::Vec<String>,
747+
}
748+
734749
/// Find any Angular decorator on a class and return its kind and the decorator reference.
735750
fn find_angular_decorator<'a>(
736751
class: &'a oxc_ast::ast::Class<'a>,
@@ -824,38 +839,71 @@ fn extract_jit_ctor_params(
824839
params
825840
}
826841

827-
/// Extract Angular member decorators for JIT propDecorators generation.
842+
/// Angular field decorators that go into `static propDecorators`.
843+
/// Matches Angular's official `FIELD_DECORATORS` constant from `@angular/compiler-cli`.
844+
const ANGULAR_FIELD_DECORATORS: &[&str] = &[
845+
"Input",
846+
"Output",
847+
"HostBinding",
848+
"HostListener",
849+
"ViewChild",
850+
"ViewChildren",
851+
"ContentChild",
852+
"ContentChildren",
853+
];
854+
855+
/// All Angular decorator names from `@angular/core`.
856+
/// Any decorator with one of these names is treated as Angular and excluded from
857+
/// non-Angular `__decorate()` lowering. Angular identifies decorators by import source;
858+
/// we use names since they're unique to `@angular/core`.
859+
const ANGULAR_DECORATOR_NAMES: &[&str] = &[
860+
// Field decorators (→ propDecorators)
861+
"Input",
862+
"Output",
863+
"HostBinding",
864+
"HostListener",
865+
"ViewChild",
866+
"ViewChildren",
867+
"ContentChild",
868+
"ContentChildren",
869+
// Parameter decorators (→ ctorParameters)
870+
"Inject",
871+
"Optional",
872+
"Self",
873+
"SkipSelf",
874+
"Host",
875+
"Attribute",
876+
// Class decorators (→ class __decorate)
877+
"Component",
878+
"Directive",
879+
"Pipe",
880+
"Injectable",
881+
"NgModule",
882+
];
883+
884+
/// Extract all member decorators for JIT transformation in a single pass.
828885
///
829-
/// Collects all Angular-relevant decorators from class properties/methods
830-
/// (excluding constructor) so they can be emitted as a `static propDecorators` property.
831-
fn extract_jit_member_decorators(
886+
/// Returns two collections:
887+
/// - Angular field decorators → emitted as `static propDecorators = { ... }`
888+
/// - Non-Angular decorators → emitted as `__decorate([...], target, "name", desc)` calls
889+
fn extract_all_jit_member_decorators(
832890
source: &str,
833891
class: &oxc_ast::ast::Class<'_>,
834-
) -> std::vec::Vec<JitMemberDecorator> {
892+
) -> (std::vec::Vec<JitMemberDecorator>, std::vec::Vec<JitNonAngularMemberDecorator>) {
835893
use oxc_ast::ast::{ClassElement, MethodDefinitionKind, PropertyKey};
836894

837-
const ANGULAR_MEMBER_DECORATORS: &[&str] = &[
838-
"Input",
839-
"Output",
840-
"HostBinding",
841-
"HostListener",
842-
"ViewChild",
843-
"ViewChildren",
844-
"ContentChild",
845-
"ContentChildren",
846-
];
847-
848-
let mut result: std::vec::Vec<JitMemberDecorator> = std::vec::Vec::new();
895+
let mut angular_members: std::vec::Vec<JitMemberDecorator> = std::vec::Vec::new();
896+
let mut non_angular_members: std::vec::Vec<JitNonAngularMemberDecorator> = std::vec::Vec::new();
849897

850898
for element in &class.body.body {
851-
let (member_name, decorators) = match element {
899+
let (member_name, is_static, is_property, decorators) = match element {
852900
ClassElement::PropertyDefinition(prop) => {
853901
let name = match &prop.key {
854902
PropertyKey::StaticIdentifier(id) => id.name.to_string(),
855903
PropertyKey::StringLiteral(s) => s.value.to_string(),
856904
_ => continue,
857905
};
858-
(name, &prop.decorators)
906+
(name, prop.r#static, true, &prop.decorators)
859907
}
860908
ClassElement::MethodDefinition(method) => {
861909
if method.kind == MethodDefinitionKind::Constructor {
@@ -866,20 +914,21 @@ fn extract_jit_member_decorators(
866914
PropertyKey::StringLiteral(s) => s.value.to_string(),
867915
_ => continue,
868916
};
869-
(name, &method.decorators)
917+
(name, method.r#static, false, &method.decorators)
870918
}
871919
ClassElement::AccessorProperty(accessor) => {
872920
let name = match &accessor.key {
873921
PropertyKey::StaticIdentifier(id) => id.name.to_string(),
874922
PropertyKey::StringLiteral(s) => s.value.to_string(),
875923
_ => continue,
876924
};
877-
(name, &accessor.decorators)
925+
(name, accessor.r#static, false, &accessor.decorators)
878926
}
879927
_ => continue,
880928
};
881929

882930
let mut angular_decs: std::vec::Vec<JitParamDecorator> = std::vec::Vec::new();
931+
let mut non_angular_texts: std::vec::Vec<String> = std::vec::Vec::new();
883932

884933
for decorator in decorators {
885934
let (dec_name, call_args) = match &decorator.expression {
@@ -902,17 +951,37 @@ fn extract_jit_member_decorators(
902951
_ => continue,
903952
};
904953

905-
if ANGULAR_MEMBER_DECORATORS.contains(&dec_name.as_str()) {
954+
if ANGULAR_FIELD_DECORATORS.contains(&dec_name.as_str()) {
955+
// Angular field decorator → goes into propDecorators
906956
angular_decs.push(JitParamDecorator { name: dec_name, args: call_args });
957+
} else if !ANGULAR_DECORATOR_NAMES.contains(&dec_name.as_str()) {
958+
// Non-Angular decorator → goes into __decorate() call
959+
let expr_start = decorator.expression.span().start;
960+
let expr_end = decorator.expression.span().end;
961+
non_angular_texts.push(source[expr_start as usize..expr_end as usize].to_string());
907962
}
963+
// Angular non-field decorators (e.g. @Inject on a member) are silently dropped
964+
// since they have no meaningful effect on members.
908965
}
909966

910967
if !angular_decs.is_empty() {
911-
result.push(JitMemberDecorator { member_name, decorators: angular_decs });
968+
angular_members.push(JitMemberDecorator {
969+
member_name: member_name.clone(),
970+
decorators: angular_decs,
971+
});
972+
}
973+
974+
if !non_angular_texts.is_empty() {
975+
non_angular_members.push(JitNonAngularMemberDecorator {
976+
member_name,
977+
is_static,
978+
is_property,
979+
decorator_texts: non_angular_texts,
980+
});
912981
}
913982
}
914983

915-
result
984+
(angular_members, non_angular_members)
916985
}
917986

918987
/// Build the propDecorators static property text for JIT member decorator metadata.
@@ -1232,28 +1301,46 @@ fn transform_angular_file_jit(
12321301
continue;
12331302
};
12341303

1235-
let Some((decorator_kind, decorator)) = find_angular_decorator(class) else {
1304+
let Some((decorator_kind, angular_decorator)) = find_angular_decorator(class) else {
12361305
continue;
12371306
};
12381307

1239-
// Build modified decorator text (replaces templateUrl/styleUrl with resource imports)
1240-
let decorator_text = build_jit_decorator_text(
1241-
source,
1242-
decorator,
1243-
decorator_kind,
1244-
&mut resource_counter,
1245-
&mut resource_imports,
1246-
);
1308+
// Collect ALL class-level decorator spans and texts (in source order)
1309+
let mut all_class_decorator_spans: std::vec::Vec<Span> = std::vec::Vec::new();
1310+
let mut all_class_decorator_texts: std::vec::Vec<String> = std::vec::Vec::new();
1311+
1312+
for dec in &class.decorators {
1313+
all_class_decorator_spans.push(dec.span);
1314+
1315+
// Check if this is the Angular decorator that needs special text transformation
1316+
if dec.span == angular_decorator.span {
1317+
let text = build_jit_decorator_text(
1318+
source,
1319+
dec,
1320+
decorator_kind,
1321+
&mut resource_counter,
1322+
&mut resource_imports,
1323+
);
1324+
all_class_decorator_texts.push(text);
1325+
} else {
1326+
// Non-Angular decorator: extract expression text from source (without @)
1327+
let expr_start = dec.expression.span().start;
1328+
let expr_end = dec.expression.span().end;
1329+
all_class_decorator_texts
1330+
.push(source[expr_start as usize..expr_end as usize].to_string());
1331+
}
1332+
}
12471333

12481334
// Extract constructor parameters for ctorParameters
12491335
let ctor_params = extract_jit_ctor_params(source, class);
12501336

1251-
// Extract member decorators for propDecorators
1252-
let member_decorators = extract_jit_member_decorators(source, class);
1337+
// Extract Angular and non-Angular member decorators
1338+
let (member_decorators, non_angular_member_decorators) =
1339+
extract_all_jit_member_decorators(source, class);
12531340

12541341
jit_classes.push(JitClassInfo {
12551342
class_name,
1256-
decorator_span: decorator.span,
1343+
all_class_decorator_spans,
12571344
stmt_start,
12581345
class_start: class.span.start,
12591346
class_body_end: class.body.span.end,
@@ -1262,7 +1349,8 @@ fn transform_angular_file_jit(
12621349
is_abstract: class.r#abstract,
12631350
ctor_params,
12641351
member_decorators,
1265-
decorator_text,
1352+
all_class_decorator_texts,
1353+
non_angular_member_decorators,
12661354
});
12671355

12681356
result.component_count +=
@@ -1343,9 +1431,9 @@ fn transform_angular_file_jit(
13431431
continue;
13441432
};
13451433

1346-
// 4a. Remove the Angular decorator (including @ and trailing whitespace)
1347-
{
1348-
let mut end = jit_info.decorator_span.end as usize;
1434+
// 4a. Remove ALL class-level decorators (including @ and trailing whitespace)
1435+
for decorator_span in &jit_info.all_class_decorator_spans {
1436+
let mut end = decorator_span.end as usize;
13491437
let bytes = source.as_bytes();
13501438
while end < bytes.len() {
13511439
let c = bytes[end];
@@ -1355,14 +1443,14 @@ fn transform_angular_file_jit(
13551443
break;
13561444
}
13571445
}
1358-
edits.push(Edit::delete(jit_info.decorator_span.start, end as u32));
1446+
edits.push(Edit::delete(decorator_span.start, end as u32));
13591447
}
13601448

1361-
// 4b. Remove member decorators (@Input, @Output, etc.) and constructor param decorators
1449+
// 4b. Remove ALL member decorators and constructor param decorators
13621450
{
13631451
let mut decorator_spans: std::vec::Vec<Span> = std::vec::Vec::new();
13641452
super::decorator::collect_constructor_decorator_spans(class, &mut decorator_spans);
1365-
super::decorator::collect_member_decorator_spans(class, &mut decorator_spans);
1453+
super::decorator::collect_all_member_decorator_spans(class, &mut decorator_spans);
13661454
for span in &decorator_spans {
13671455
let mut end = span.end as usize;
13681456
let bytes = source.as_bytes();
@@ -1417,11 +1505,41 @@ fn transform_angular_file_jit(
14171505
}
14181506
}
14191507

1420-
// 4e. After class body, add __decorate call and export
1421-
let mut after_class = format!(
1422-
";\n{} = __decorate([\n {}\n], {});\n",
1423-
jit_info.class_name, jit_info.decorator_text, jit_info.class_name
1424-
);
1508+
// 4e. After class body, add member __decorate calls, then class __decorate call, then export
1509+
let mut after_class = String::from(";\n");
1510+
1511+
// Emit __decorate() for non-Angular member decorators (before class __decorate).
1512+
// Match TypeScript's ordering: instance (prototype) members first, then static members.
1513+
// Within each group, preserve source declaration order.
1514+
for member_dec in jit_info
1515+
.non_angular_member_decorators
1516+
.iter()
1517+
.filter(|m| !m.is_static)
1518+
.chain(jit_info.non_angular_member_decorators.iter().filter(|m| m.is_static))
1519+
{
1520+
let target = if member_dec.is_static {
1521+
jit_info.class_name.clone()
1522+
} else {
1523+
format!("{}.prototype", jit_info.class_name)
1524+
};
1525+
// TypeScript uses `null` for methods/accessors (reads existing descriptor)
1526+
// and `void 0` for properties (no existing descriptor).
1527+
let desc = if member_dec.is_property { "void 0" } else { "null" };
1528+
after_class.push_str(&format!(
1529+
"__decorate([{}], {}, \"{}\", {});\n",
1530+
member_dec.decorator_texts.join(", "),
1531+
target,
1532+
member_dec.member_name,
1533+
desc
1534+
));
1535+
}
1536+
1537+
// Emit class-level __decorate() with ALL class decorators
1538+
let all_decorator_text = jit_info.all_class_decorator_texts.join(",\n ");
1539+
after_class.push_str(&format!(
1540+
"{} = __decorate([\n {}\n], {});\n",
1541+
jit_info.class_name, all_decorator_text, jit_info.class_name
1542+
));
14251543

14261544
if jit_info.is_exported {
14271545
after_class.push_str(&format!("export {{ {} }};\n", jit_info.class_name));

0 commit comments

Comments
 (0)