Skip to content
Merged
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
27 changes: 27 additions & 0 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,33 @@ pub fn collect_member_decorator_spans(class: &Class<'_>, spans: &mut std::vec::V
}
}

/// Collect ALL decorator spans from class members (properties, methods, accessors),
/// regardless of whether they are Angular-specific or not.
///
/// This is used when lowering a class that has Angular decorators: since the class
/// declaration is converted to a class expression, ALL member decorators must be
/// removed (decorators are not valid on class expressions in TypeScript).
pub fn collect_all_member_decorator_spans(class: &Class<'_>, spans: &mut std::vec::Vec<Span>) {
for element in &class.body.body {
let decorators = match element {
ClassElement::PropertyDefinition(prop) => &prop.decorators,
ClassElement::MethodDefinition(method) => {
// Skip constructor - it's handled separately
if method.kind == MethodDefinitionKind::Constructor {
continue;
}
&method.decorators
}
ClassElement::AccessorProperty(accessor) => &accessor.decorators,
_ => continue,
};

for decorator in decorators {
spans.push(decorator.span);
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
224 changes: 171 additions & 53 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,8 +685,8 @@ enum AngularDecoratorKind {
struct JitClassInfo {
/// The class name.
class_name: String,
/// Span of the decorator (including @).
decorator_span: Span,
/// Spans of ALL class-level decorators (including @) to be removed.
all_class_decorator_spans: std::vec::Vec<Span>,
/// Start of the statement (includes export keyword if present).
stmt_start: u32,
/// Start of the class keyword.
Expand All @@ -701,10 +701,12 @@ struct JitClassInfo {
is_abstract: bool,
/// Constructor parameter info for ctorParameters.
ctor_params: std::vec::Vec<JitCtorParam>,
/// Member decorator info for propDecorators.
/// Member decorator info for propDecorators (Angular decorators like @Input, @Output).
member_decorators: std::vec::Vec<JitMemberDecorator>,
/// The modified decorator expression text for __decorate call.
decorator_text: String,
/// All class-level decorator expression texts for __decorate call, in source order.
all_class_decorator_texts: std::vec::Vec<String>,
/// Non-Angular member decorators that need __decorate() calls.
non_angular_member_decorators: std::vec::Vec<JitNonAngularMemberDecorator>,
}

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

/// A non-Angular member decorator that needs to be lowered via __decorate().
struct JitNonAngularMemberDecorator {
/// The member name.
member_name: String,
/// Whether the member is static.
is_static: bool,
/// Whether this is a property (field) vs a method/accessor.
/// TypeScript uses `void 0` for properties and `null` for methods/accessors.
is_property: bool,
/// The decorator expression texts (e.g., "Selector()", "Action(AddTodo)").
decorator_texts: std::vec::Vec<String>,
}

/// Find any Angular decorator on a class and return its kind and the decorator reference.
fn find_angular_decorator<'a>(
class: &'a oxc_ast::ast::Class<'a>,
Expand Down Expand Up @@ -824,38 +839,71 @@ fn extract_jit_ctor_params(
params
}

/// Extract Angular member decorators for JIT propDecorators generation.
/// Angular field decorators that go into `static propDecorators`.
/// Matches Angular's official `FIELD_DECORATORS` constant from `@angular/compiler-cli`.
const ANGULAR_FIELD_DECORATORS: &[&str] = &[
"Input",
"Output",
"HostBinding",
"HostListener",
"ViewChild",
"ViewChildren",
"ContentChild",
"ContentChildren",
];

/// All Angular decorator names from `@angular/core`.
/// Any decorator with one of these names is treated as Angular and excluded from
/// non-Angular `__decorate()` lowering. Angular identifies decorators by import source;
/// we use names since they're unique to `@angular/core`.
const ANGULAR_DECORATOR_NAMES: &[&str] = &[
// Field decorators (→ propDecorators)
"Input",
"Output",
"HostBinding",
"HostListener",
"ViewChild",
"ViewChildren",
"ContentChild",
"ContentChildren",
// Parameter decorators (→ ctorParameters)
"Inject",
"Optional",
"Self",
"SkipSelf",
"Host",
"Attribute",
// Class decorators (→ class __decorate)
"Component",
"Directive",
"Pipe",
"Injectable",
"NgModule",
];

/// Extract all member decorators for JIT transformation in a single pass.
///
/// Collects all Angular-relevant decorators from class properties/methods
/// (excluding constructor) so they can be emitted as a `static propDecorators` property.
fn extract_jit_member_decorators(
/// Returns two collections:
/// - Angular field decorators → emitted as `static propDecorators = { ... }`
/// - Non-Angular decorators → emitted as `__decorate([...], target, "name", desc)` calls
fn extract_all_jit_member_decorators(
source: &str,
class: &oxc_ast::ast::Class<'_>,
) -> std::vec::Vec<JitMemberDecorator> {
) -> (std::vec::Vec<JitMemberDecorator>, std::vec::Vec<JitNonAngularMemberDecorator>) {
use oxc_ast::ast::{ClassElement, MethodDefinitionKind, PropertyKey};

const ANGULAR_MEMBER_DECORATORS: &[&str] = &[
"Input",
"Output",
"HostBinding",
"HostListener",
"ViewChild",
"ViewChildren",
"ContentChild",
"ContentChildren",
];

let mut result: std::vec::Vec<JitMemberDecorator> = std::vec::Vec::new();
let mut angular_members: std::vec::Vec<JitMemberDecorator> = std::vec::Vec::new();
let mut non_angular_members: std::vec::Vec<JitNonAngularMemberDecorator> = std::vec::Vec::new();

for element in &class.body.body {
let (member_name, decorators) = match element {
let (member_name, is_static, is_property, decorators) = match element {
ClassElement::PropertyDefinition(prop) => {
let name = match &prop.key {
PropertyKey::StaticIdentifier(id) => id.name.to_string(),
PropertyKey::StringLiteral(s) => s.value.to_string(),
_ => continue,
};
(name, &prop.decorators)
(name, prop.r#static, true, &prop.decorators)
}
ClassElement::MethodDefinition(method) => {
if method.kind == MethodDefinitionKind::Constructor {
Expand All @@ -866,20 +914,21 @@ fn extract_jit_member_decorators(
PropertyKey::StringLiteral(s) => s.value.to_string(),
_ => continue,
};
(name, &method.decorators)
(name, method.r#static, false, &method.decorators)
}
ClassElement::AccessorProperty(accessor) => {
let name = match &accessor.key {
PropertyKey::StaticIdentifier(id) => id.name.to_string(),
PropertyKey::StringLiteral(s) => s.value.to_string(),
_ => continue,
};
(name, &accessor.decorators)
(name, accessor.r#static, false, &accessor.decorators)
}
_ => continue,
};

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

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

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

if !angular_decs.is_empty() {
result.push(JitMemberDecorator { member_name, decorators: angular_decs });
angular_members.push(JitMemberDecorator {
member_name: member_name.clone(),
decorators: angular_decs,
});
}

if !non_angular_texts.is_empty() {
non_angular_members.push(JitNonAngularMemberDecorator {
member_name,
is_static,
is_property,
decorator_texts: non_angular_texts,
});
}
}

result
(angular_members, non_angular_members)
}

/// Build the propDecorators static property text for JIT member decorator metadata.
Expand Down Expand Up @@ -1232,28 +1301,46 @@ fn transform_angular_file_jit(
continue;
};

let Some((decorator_kind, decorator)) = find_angular_decorator(class) else {
let Some((decorator_kind, angular_decorator)) = find_angular_decorator(class) else {
continue;
};

// Build modified decorator text (replaces templateUrl/styleUrl with resource imports)
let decorator_text = build_jit_decorator_text(
source,
decorator,
decorator_kind,
&mut resource_counter,
&mut resource_imports,
);
// Collect ALL class-level decorator spans and texts (in source order)
let mut all_class_decorator_spans: std::vec::Vec<Span> = std::vec::Vec::new();
let mut all_class_decorator_texts: std::vec::Vec<String> = std::vec::Vec::new();

for dec in &class.decorators {
all_class_decorator_spans.push(dec.span);

// Check if this is the Angular decorator that needs special text transformation
if dec.span == angular_decorator.span {
let text = build_jit_decorator_text(
source,
dec,
decorator_kind,
&mut resource_counter,
&mut resource_imports,
);
all_class_decorator_texts.push(text);
} else {
// Non-Angular decorator: extract expression text from source (without @)
let expr_start = dec.expression.span().start;
let expr_end = dec.expression.span().end;
all_class_decorator_texts
.push(source[expr_start as usize..expr_end as usize].to_string());
}
}

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

// Extract member decorators for propDecorators
let member_decorators = extract_jit_member_decorators(source, class);
// Extract Angular and non-Angular member decorators
let (member_decorators, non_angular_member_decorators) =
extract_all_jit_member_decorators(source, class);

jit_classes.push(JitClassInfo {
class_name,
decorator_span: decorator.span,
all_class_decorator_spans,
stmt_start,
class_start: class.span.start,
class_body_end: class.body.span.end,
Expand All @@ -1262,7 +1349,8 @@ fn transform_angular_file_jit(
is_abstract: class.r#abstract,
ctor_params,
member_decorators,
decorator_text,
all_class_decorator_texts,
non_angular_member_decorators,
});

result.component_count +=
Expand Down Expand Up @@ -1343,9 +1431,9 @@ fn transform_angular_file_jit(
continue;
};

// 4a. Remove the Angular decorator (including @ and trailing whitespace)
{
let mut end = jit_info.decorator_span.end as usize;
// 4a. Remove ALL class-level decorators (including @ and trailing whitespace)
for decorator_span in &jit_info.all_class_decorator_spans {
let mut end = decorator_span.end as usize;
let bytes = source.as_bytes();
while end < bytes.len() {
let c = bytes[end];
Expand All @@ -1355,14 +1443,14 @@ fn transform_angular_file_jit(
break;
}
}
edits.push(Edit::delete(jit_info.decorator_span.start, end as u32));
edits.push(Edit::delete(decorator_span.start, end as u32));
}

// 4b. Remove member decorators (@Input, @Output, etc.) and constructor param decorators
// 4b. Remove ALL member decorators and constructor param decorators
{
let mut decorator_spans: std::vec::Vec<Span> = std::vec::Vec::new();
super::decorator::collect_constructor_decorator_spans(class, &mut decorator_spans);
super::decorator::collect_member_decorator_spans(class, &mut decorator_spans);
super::decorator::collect_all_member_decorator_spans(class, &mut decorator_spans);
for span in &decorator_spans {
let mut end = span.end as usize;
let bytes = source.as_bytes();
Expand Down Expand Up @@ -1417,11 +1505,41 @@ fn transform_angular_file_jit(
}
}

// 4e. After class body, add __decorate call and export
let mut after_class = format!(
";\n{} = __decorate([\n {}\n], {});\n",
jit_info.class_name, jit_info.decorator_text, jit_info.class_name
);
// 4e. After class body, add member __decorate calls, then class __decorate call, then export
let mut after_class = String::from(";\n");

// Emit __decorate() for non-Angular member decorators (before class __decorate).
// Match TypeScript's ordering: instance (prototype) members first, then static members.
// Within each group, preserve source declaration order.
for member_dec in jit_info
.non_angular_member_decorators
.iter()
.filter(|m| !m.is_static)
.chain(jit_info.non_angular_member_decorators.iter().filter(|m| m.is_static))
{
let target = if member_dec.is_static {
jit_info.class_name.clone()
} else {
format!("{}.prototype", jit_info.class_name)
};
// TypeScript uses `null` for methods/accessors (reads existing descriptor)
// and `void 0` for properties (no existing descriptor).
let desc = if member_dec.is_property { "void 0" } else { "null" };
after_class.push_str(&format!(
"__decorate([{}], {}, \"{}\", {});\n",
member_dec.decorator_texts.join(", "),
target,
member_dec.member_name,
desc
));
}

// Emit class-level __decorate() with ALL class decorators
let all_decorator_text = jit_info.all_class_decorator_texts.join(",\n ");
after_class.push_str(&format!(
"{} = __decorate([\n {}\n], {});\n",
jit_info.class_name, all_decorator_text, jit_info.class_name
));

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