From 72b7328b07bf4ef830a311a5ece886a158f0ac8e Mon Sep 17 00:00:00 2001 From: Ashley Hunter Date: Wed, 1 Apr 2026 22:13:05 +0000 Subject: [PATCH] fix(jit): lower non-Angular decorators via __decorate 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. --- .../src/component/decorator.rs | 27 + .../src/component/transform.rs | 224 +++- .../tests/integration_test.rs | 1101 +++++++++++++++++ ...t_angular_param_decorators_on_members.snap | 17 + ...test__jit_complex_decorator_arguments.snap | 24 + ...ration_test__jit_default_export_class.snap | 17 + ...tegration_test__jit_full_ngxs_example.snap | 41 + ...on_test__jit_getter_setter_decorators.snap | 29 + ...mixed_angular_non_angular_same_member.snap | 26 + ..._test__jit_multiple_classes_same_file.snap | 23 + ...__jit_multiple_decorators_same_member.snap | 25 + ...est__jit_non_angular_class_decorators.snap | 13 + ...st__jit_non_angular_method_decorators.snap | 19 + ...egration_test__jit_non_exported_class.snap | 16 + ...n_test__jit_property_decorator_void_0.snap | 20 + ...t_reference_angular_member_decorators.snap | 50 + ...ion_test__jit_reference_animals_state.snap | 19 + ...test__jit_reference_decorate_patterns.snap | 27 + ...egration_test__jit_reference_ordering.snap | 25 + 19 files changed, 1690 insertions(+), 53 deletions(-) create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_angular_param_decorators_on_members.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_complex_decorator_arguments.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_default_export_class.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_full_ngxs_example.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_getter_setter_decorators.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_mixed_angular_non_angular_same_member.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_classes_same_file.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_decorators_same_member.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_class_decorators.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_method_decorators.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_exported_class.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_property_decorator_void_0.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_angular_member_decorators.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_animals_state.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_decorate_patterns.snap create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_ordering.snap diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index deca15ac6..a7ede7ccf 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -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) { + 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::*; diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 767a4428c..8cf80acf9 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -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, /// Start of the statement (includes export keyword if present). stmt_start: u32, /// Start of the class keyword. @@ -701,10 +701,12 @@ struct JitClassInfo { is_abstract: bool, /// Constructor parameter info for ctorParameters. ctor_params: std::vec::Vec, - /// Member decorator info for propDecorators. + /// Member decorator info for propDecorators (Angular decorators like @Input, @Output). member_decorators: std::vec::Vec, - /// 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, + /// Non-Angular member decorators that need __decorate() calls. + non_angular_member_decorators: std::vec::Vec, } /// Constructor parameter info for JIT ctorParameters generation. @@ -731,6 +733,19 @@ struct JitMemberDecorator { decorators: std::vec::Vec, } +/// 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, +} + /// 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>, @@ -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 { +) -> (std::vec::Vec, std::vec::Vec) { 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 = std::vec::Vec::new(); + let mut angular_members: std::vec::Vec = std::vec::Vec::new(); + let mut non_angular_members: std::vec::Vec = 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 { @@ -866,7 +914,7 @@ 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 { @@ -874,12 +922,13 @@ fn extract_jit_member_decorators( 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 = std::vec::Vec::new(); + let mut non_angular_texts: std::vec::Vec = std::vec::Vec::new(); for decorator in decorators { let (dec_name, call_args) = match &decorator.expression { @@ -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. @@ -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 = std::vec::Vec::new(); + let mut all_class_decorator_texts: std::vec::Vec = 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, @@ -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 += @@ -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]; @@ -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 = 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(); @@ -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)); diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 79960280c..31c8b605c 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -6332,6 +6332,1107 @@ export abstract class BaseProvider { insta::assert_snapshot!("jit_abstract_class", result.code); } +#[test] +fn test_jit_non_angular_class_decorators_lowered() { + // When a class has both Angular and non-Angular class-level decorators, + // ALL decorators must be lowered into the __decorate() call. + // Non-Angular decorators left as raw @Decorator syntax on a class expression + // cause TS1206 (decorators are not valid on class expressions). + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; +import { State } from '@ngxs/store'; + +interface TodoStateModel { + items: string[]; +} + +@State({ name: 'todo', defaults: { items: [] } }) +@Injectable() +export class TodoState {} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "todo.state.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // No raw @State decorator should remain in the output + assert!( + !result.code.contains("@State"), + "Non-Angular class decorators should be lowered, not left as raw syntax. Got:\n{}", + result.code + ); + + // Both decorators should appear in the __decorate call + assert!( + result.code.contains("State("), + "Non-Angular class decorator State should appear in __decorate call. Got:\n{}", + result.code + ); + assert!( + result.code.contains("Injectable()"), + "Angular class decorator Injectable should appear in __decorate call. Got:\n{}", + result.code + ); + + // Decorator order should be preserved (State before Injectable) + let state_pos = result.code.find("State(").unwrap(); + let injectable_pos = result.code.find("Injectable()").unwrap(); + assert!( + state_pos < injectable_pos, + "Decorator order should be preserved (State before Injectable). Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_non_angular_class_decorators", result.code); +} + +#[test] +fn test_jit_non_angular_method_decorators_lowered() { + // Non-Angular method decorators should be lowered to __decorate() calls + // on the class prototype (for instance methods) or class itself (for static methods). + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; +import { State, Action, Selector } from '@ngxs/store'; + +@State({ name: 'todo' }) +@Injectable() +export class TodoState { + @Selector() + static todos(state: any): any[] { return state.items; } + + @Action(AddTodo) + add(ctx: any, action: any) { ctx.setState(action); } +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "todo.state.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // No raw @Selector or @Action decorator should remain + assert!( + !result.code.contains("@Selector"), + "Non-Angular method decorators should be lowered. Got:\n{}", + result.code + ); + assert!( + !result.code.contains("@Action"), + "Non-Angular method decorators should be lowered. Got:\n{}", + result.code + ); + + // Static method → __decorate([Selector()], TodoState, "todos", null) + assert!( + result.code.contains("__decorate([Selector()], TodoState, \"todos\", null)"), + "Static method decorator should use class directly (no .prototype). Got:\n{}", + result.code + ); + + // Instance method → __decorate([Action(AddTodo)], TodoState.prototype, "add", null) + assert!( + result.code.contains("__decorate([Action(AddTodo)], TodoState.prototype, \"add\", null)"), + "Instance method decorator should use .prototype. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_non_angular_method_decorators", result.code); +} + +#[test] +fn test_jit_full_ngxs_example() { + // Full example with NGXS-style decorators: @State, @Selector, @Action combined with @Injectable + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; +import { State, Action, Selector, StateContext } from '@ngxs/store'; + +interface TodoStateModel { + items: TodoItem[]; + filter: string; +} + +interface TodoItem { + text: string; + done: boolean; +} + +class AddTodo { + static readonly type = '[Todo] Add'; + constructor(public text: string) {} +} + +class ToggleTodo { + static readonly type = '[Todo] Toggle'; + constructor(public index: number) {} +} + +@State({ name: 'todo', defaults: { items: [], filter: 'all' } }) +@Injectable() +export class TodoState { + @Selector() + static todos(state: TodoStateModel): TodoItem[] { return state.items; } + + @Selector() + static filter(state: TodoStateModel): string { return state.filter; } + + @Action(AddTodo) + add(ctx: StateContext, action: AddTodo) { /* ... */ } + + @Action(ToggleTodo) + toggle(ctx: StateContext, action: ToggleTodo) { /* ... */ } +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "todo.state.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // No raw decorators should remain anywhere + assert!( + !result.code.contains("@State") + && !result.code.contains("@Injectable") + && !result.code.contains("@Selector") + && !result.code.contains("@Action"), + "No raw decorator syntax should remain in output. Got:\n{}", + result.code + ); + + // Member __decorate calls should come before class __decorate + let selector_decorate = + result.code.find("__decorate([Selector()], TodoState, \"todos\"").unwrap(); + let class_decorate = result.code.find("TodoState = __decorate(").unwrap(); + assert!( + selector_decorate < class_decorate, + "Member decorators should be emitted before class decorator. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_full_ngxs_example", result.code); +} + +#[test] +fn test_jit_non_angular_property_decorator_uses_void_0() { + // TypeScript uses `void 0` (not `null`) as the 4th argument for property decorators + // because properties don't have an existing descriptor on the prototype. + // Methods use `null` which tells __decorate to call Object.getOwnPropertyDescriptor. + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; + +function Validate() { return function(t: any, k: string) {}; } +function Log(target: any, key: string, desc: PropertyDescriptor) {} + +@Injectable() +export class MyService { + @Validate() + name: string = ''; + + @Log + greet() { return 'hello'; } +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "my.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Property decorator should use `void 0` + assert!( + result.code.contains("__decorate([Validate()], MyService.prototype, \"name\", void 0)"), + "Property decorator should use `void 0` as 4th arg. Got:\n{}", + result.code + ); + + // Method decorator should use `null` + assert!( + result.code.contains("__decorate([Log], MyService.prototype, \"greet\", null)"), + "Method decorator should use `null` as 4th arg. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_property_decorator_void_0", result.code); +} + +#[test] +fn test_jit_mixed_angular_and_non_angular_decorators_on_same_member() { + // When a member has both Angular and non-Angular decorators, the Angular + // decorator goes into propDecorators while the non-Angular one is lowered + // to a __decorate() call. Both must be stripped from the class body. + let allocator = Allocator::default(); + let source = r#" +import { Directive, Input, Output, EventEmitter } from '@angular/core'; + +function Required() { return function(t: any, k: string) {}; } +function Throttle(ms: number) { return function(t: any, k: string, d: any) {}; } + +@Directive({ selector: '[appField]' }) +export class FieldDirective { + @Required() + @Input() + value: string = ''; + + @Throttle(300) + @Output() + valueChange = new EventEmitter(); + + @Throttle(100) + onChange() {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = + transform_angular_file(&allocator, "field.directive.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // No raw decorators should remain + assert!( + !result.code.contains("@Required") + && !result.code.contains("@Input") + && !result.code.contains("@Throttle") + && !result.code.contains("@Output"), + "No raw decorator syntax should remain. Got:\n{}", + result.code + ); + + // Angular decorators should appear in propDecorators + assert!( + result.code.contains("propDecorators"), + "Angular member decorators should be in propDecorators. Got:\n{}", + result.code + ); + assert!( + result.code.contains("type: Input"), + "propDecorators should contain Input. Got:\n{}", + result.code + ); + assert!( + result.code.contains("type: Output"), + "propDecorators should contain Output. Got:\n{}", + result.code + ); + + // Non-Angular decorators should be lowered via __decorate() + assert!( + result + .code + .contains("__decorate([Required()], FieldDirective.prototype, \"value\", void 0)"), + "Non-Angular property decorator should use __decorate with void 0. Got:\n{}", + result.code + ); + assert!( + result.code.contains( + "__decorate([Throttle(300)], FieldDirective.prototype, \"valueChange\", void 0)" + ), + "Non-Angular property decorator should use __decorate with void 0. Got:\n{}", + result.code + ); + assert!( + result + .code + .contains("__decorate([Throttle(100)], FieldDirective.prototype, \"onChange\", null)"), + "Non-Angular method decorator should use __decorate with null. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_mixed_angular_non_angular_same_member", result.code); +} + +#[test] +fn test_jit_multiple_non_angular_decorators_on_same_member() { + // Multiple non-Angular decorators on the same member should all appear + // in a single __decorate() call for that member. + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; + +function Log() { return function(t: any, k: string, d: any) {}; } +function Memoize() { return function(t: any, k: string, d: any) {}; } +function Validate() { return function(t: any, k: string) {}; } + +@Injectable() +export class MyService { + @Log() + @Memoize() + compute() { return 42; } + + @Validate() + @Log() + name: string = ''; +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "my.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Multiple decorators on method should be in single __decorate call, in source order + assert!( + result + .code + .contains("__decorate([Log(), Memoize()], MyService.prototype, \"compute\", null)"), + "Multiple method decorators should be in one __decorate call. Got:\n{}", + result.code + ); + + // Multiple decorators on property should also be in single __decorate call + assert!( + result + .code + .contains("__decorate([Validate(), Log()], MyService.prototype, \"name\", void 0)"), + "Multiple property decorators should be in one __decorate call. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_multiple_decorators_same_member", result.code); +} + +#[test] +fn test_jit_multiple_decorated_classes_in_same_file() { + // Multiple Angular-decorated classes in the same file should each get + // their own class expression conversion and __decorate calls. + let allocator = Allocator::default(); + let source = r#" +import { Component, Injectable } from '@angular/core'; + +function Logger() { return function(t: any) { return t; }; } + +@Component({ selector: 'app-foo', template: '

foo

' }) +export class FooComponent {} + +@Logger() +@Injectable() +export class FooService { + @Logger() + doWork() {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "foo.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Both classes should be converted to class expressions + assert!( + result.code.contains("let FooComponent = class FooComponent"), + "FooComponent should be a class expression. Got:\n{}", + result.code + ); + assert!( + result.code.contains("let FooService = class FooService"), + "FooService should be a class expression. Got:\n{}", + result.code + ); + + // Both should have __decorate calls + assert!( + result.code.contains("FooComponent = __decorate("), + "FooComponent should have a __decorate call. Got:\n{}", + result.code + ); + assert!( + result.code.contains("FooService = __decorate("), + "FooService should have a __decorate call. Got:\n{}", + result.code + ); + + // No raw decorators + assert!( + !result.code.contains("@Component") + && !result.code.contains("@Injectable") + && !result.code.contains("@Logger"), + "No raw decorator syntax should remain. Got:\n{}", + result.code + ); + + // FooService should include Logger in its class __decorate + let service_decorate_pos = result.code.find("FooService = __decorate(").unwrap(); + let service_decorate_section = &result.code[service_decorate_pos..]; + assert!( + service_decorate_section.contains("Logger()"), + "FooService __decorate should include Logger. Got:\n{}", + result.code + ); + + // FooService member decorator should also be lowered + assert!( + result.code.contains("__decorate([Logger()], FooService.prototype, \"doWork\", null)"), + "FooService method decorator should be lowered. Got:\n{}", + result.code + ); + + // Both should be re-exported + assert!( + result.code.contains("export { FooComponent }"), + "FooComponent should be re-exported. Got:\n{}", + result.code + ); + assert!( + result.code.contains("export { FooService }"), + "FooService should be re-exported. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_multiple_classes_same_file", result.code); +} + +#[test] +fn test_jit_non_exported_class_with_decorators() { + // A non-exported Angular class with non-Angular decorators should still + // be lowered but without an export statement. + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; + +function Singleton() { return function(t: any) { return t; }; } + +@Singleton() +@Injectable() +class InternalService { + @Singleton() + getInstance() {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = + transform_angular_file(&allocator, "internal.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Should be converted to class expression + assert!( + result.code.contains("let InternalService = class InternalService"), + "Non-exported class should still be converted. Got:\n{}", + result.code + ); + + // No raw decorators + assert!( + !result.code.contains("@Singleton") && !result.code.contains("@Injectable"), + "No raw decorator syntax should remain. Got:\n{}", + result.code + ); + + // Should NOT have an export statement + assert!( + !result.code.contains("export {") && !result.code.contains("export default"), + "Non-exported class should not get an export statement. Got:\n{}", + result.code + ); + + // Both class decorators should be in __decorate + assert!( + result.code.contains("InternalService = __decorate("), + "Should have class __decorate. Got:\n{}", + result.code + ); + + // Member decorator should be lowered + assert!( + result.code.contains( + "__decorate([Singleton()], InternalService.prototype, \"getInstance\", null)" + ), + "Member decorator should be lowered. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_non_exported_class", result.code); +} + +#[test] +fn test_jit_default_exported_class_with_decorators() { + // A default-exported Angular class with non-Angular decorators should + // be lowered with `export default ClassName` at the end. + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; + +function Logger() { return function(t: any) { return t; }; } + +@Logger() +@Injectable() +export default class AppService { + @Logger() + process() {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "app.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Should be class expression + assert!( + result.code.contains("let AppService = class AppService"), + "Default-exported class should be converted. Got:\n{}", + result.code + ); + + // Should have `export default AppService` (not `export { AppService }`) + assert!( + result.code.contains("export default AppService"), + "Should use export default. Got:\n{}", + result.code + ); + assert!( + !result.code.contains("export { AppService }"), + "Should NOT use named export for default export. Got:\n{}", + result.code + ); + + // No raw decorators + assert!( + !result.code.contains("@Logger") && !result.code.contains("@Injectable"), + "No raw decorator syntax should remain. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_default_export_class", result.code); +} + +#[test] +fn test_jit_getter_setter_decorators() { + // Decorators on getter/setter methods should be lowered like regular methods + // (using null, not void 0, since they are accessor methods not property fields). + let allocator = Allocator::default(); + let source = r#" +import { Directive, Input } from '@angular/core'; + +function Validate() { return function(t: any, k: string, d: any) {}; } +function Transform() { return function(t: any, k: string, d: any) {}; } + +@Directive({ selector: '[appField]' }) +export class FieldDirective { + private _value = ''; + + @Validate() + @Input() + get value() { return this._value; } + set value(v: string) { this._value = v; } + + @Transform() + get computed() { return this._value.toUpperCase(); } +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = + transform_angular_file(&allocator, "field.directive.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // No raw decorators + assert!( + !result.code.contains("@Validate") + && !result.code.contains("@Input") + && !result.code.contains("@Transform"), + "No raw decorator syntax should remain. Got:\n{}", + result.code + ); + + // Getter decorator should use null (method/accessor, not property) + assert!( + result.code.contains("__decorate([Validate()], FieldDirective.prototype, \"value\", null)"), + "Getter decorator should use null (accessor). Got:\n{}", + result.code + ); + assert!( + result + .code + .contains("__decorate([Transform()], FieldDirective.prototype, \"computed\", null)"), + "Getter decorator should use null (accessor). Got:\n{}", + result.code + ); + + // Angular decorator should be in propDecorators + assert!( + result.code.contains("type: Input"), + "Angular getter decorator should be in propDecorators. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_getter_setter_decorators", result.code); +} + +#[test] +fn test_jit_decorator_with_complex_arguments() { + // Decorators with complex arguments (objects, arrays, arrow functions, + // template literals) should have their argument text preserved verbatim. + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; + +function Config(opts: any) { return function(t: any) { return t; }; } +function Transform(fn: any) { return function(t: any, k: string, d: any) {}; } + +@Config({ + name: 'test', + deps: [ServiceA, ServiceB], + factory: () => new TestService(), +}) +@Injectable() +export class TestService { + @Transform((val: string) => val.trim()) + process() {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = + transform_angular_file(&allocator, "test.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // No raw decorators should remain + assert!( + !result.code.contains("@Config") + && !result.code.contains("@Injectable") + && !result.code.contains("@Transform"), + "No raw decorator syntax should remain. Got:\n{}", + result.code + ); + + // Complex arguments should be preserved in the __decorate call + assert!( + result.code.contains("Config("), + "Config decorator with complex args should be in __decorate. Got:\n{}", + result.code + ); + assert!( + result.code.contains("factory: () => new TestService()"), + "Arrow function argument should be preserved. Got:\n{}", + result.code + ); + assert!( + result.code.contains("deps: [ServiceA, ServiceB]"), + "Array argument should be preserved. Got:\n{}", + result.code + ); + + // Method decorator with arrow function arg + assert!( + result.code.contains("Transform((val) => val.trim())"), + "Arrow function in method decorator should be preserved. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_complex_decorator_arguments", result.code); +} + +#[test] +fn test_jit_angular_param_decorators_not_in_member_decorate() { + // Angular parameter decorators (@Inject, @Optional, @Self, @SkipSelf, @Host, @Attribute) + // should NOT be emitted in __decorate() calls if they appear on a member. + // While these are designed for constructor params, if someone puts them on a member, + // they should be treated as Angular decorators (not lowered via __decorate). + let allocator = Allocator::default(); + let source = r#" +import { Injectable, Inject, Optional } from '@angular/core'; + +function Custom() { return function(t: any, k: string) {}; } + +@Injectable() +export class MyService { + @Inject('TOKEN') + token: any; + + @Optional() + optionalDep: any; + + @Custom() + customProp: string = ''; +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "my.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // @Custom should be lowered via __decorate (it's non-Angular) + assert!( + result.code.contains("__decorate([Custom()], MyService.prototype, \"customProp\", void 0)"), + "Non-Angular decorator should be in __decorate. Got:\n{}", + result.code + ); + + // @Inject and @Optional should NOT appear in __decorate calls for members + // They are Angular decorators and should not be treated as non-Angular + let member_decorate_calls: Vec<&str> = result + .code + .lines() + .filter(|l| l.contains("__decorate(") && l.contains(".prototype")) + .collect(); + for call in &member_decorate_calls { + assert!( + !call.contains("Inject(") && !call.contains("Optional()"), + "Angular param decorators should not appear in member __decorate calls. Got:\n{}", + call + ); + } + + insta::assert_snapshot!("jit_angular_param_decorators_on_members", result.code); +} + +// ========================================================================= +// Reference output comparison tests +// ========================================================================= +// These tests compare our output against the actual output from Angular's +// official JIT compiler (@angular/compiler-cli) + TypeScript emit pipeline. +// Reference outputs were generated by compiling TypeScript files with +// Angular's downlevel_decorators_transform followed by tsc emit. + +#[test] +fn test_jit_reference_ngxs_animals_state() { + // Reference: AnimalsState from Angular's actual JIT output + // Non-Angular @State class decorator + @Injectable, with @Selector (static) and @Action (instance) + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; +import { State, Action, Selector } from '@ngxs/store'; + +@State({ + name: 'animals', + defaults: [] +}) +@Injectable() +class AnimalsState { + @Selector() + static getAnimals(state: string[]): string[] { + return state; + } + + @Action({ type: 'AddAnimal' }) + addAnimal(ctx: any, action: any): void {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = + transform_angular_file(&allocator, "animals.state.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Angular reference output (from full-compiled-output.js): + // __decorate([Action({type:'AddAnimal'})], AnimalsState.prototype, "addAnimal", null); + // __decorate([Selector()], AnimalsState, "getAnimals", null); + // AnimalsState = __decorate([State({...}), Injectable()], AnimalsState); + + // Instance method → prototype, null + assert!( + result.code.contains("__decorate([Action({ type: \"AddAnimal\" })], AnimalsState.prototype, \"addAnimal\", null)"), + "Instance method should match Angular reference output. Got:\n{}", + result.code + ); + + // Static method → class directly, null + assert!( + result.code.contains("__decorate([Selector()], AnimalsState, \"getAnimals\", null)"), + "Static method should match Angular reference output. Got:\n{}", + result.code + ); + + // Instance __decorate calls should come before static ones (TypeScript ordering) + let instance_pos = result.code.find("AnimalsState.prototype").unwrap(); + let static_pos = result.code.find("AnimalsState, \"getAnimals\"").unwrap(); + assert!( + instance_pos < static_pos, + "Instance member __decorate should come before static. Got:\n{}", + result.code + ); + + // Class __decorate should include both State and Injectable in source order + let class_decorate = result.code.find("AnimalsState = __decorate(").unwrap(); + let class_section = &result.code[class_decorate..]; + assert!( + class_section.contains("State(") && class_section.contains("Injectable()"), + "Class __decorate should include both decorators. Got:\n{}", + result.code + ); + + // No raw decorators + assert!( + !result.code.contains("@State") + && !result.code.contains("@Injectable") + && !result.code.contains("@Selector") + && !result.code.contains("@Action"), + "No raw decorator syntax should remain. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_reference_animals_state", result.code); +} + +#[test] +fn test_jit_reference_ordering() { + // Reference: OrderTestState from Angular's actual JIT output + // Tests that instance members are emitted before static members, + // each group in source order. This matches TypeScript's emit behavior. + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; +import { State, Action, Selector } from '@ngxs/store'; + +@State({ name: 'order', defaults: {} }) +@Injectable() +class OrderTestState { + @Action({ type: 'First' }) + instanceFirst(ctx: any): void {} + + @Selector() + static staticSecond(state: any): any { return state; } + + @Action({ type: 'Third' }) + instanceThird(ctx: any): void {} + + @Selector() + static staticFourth(state: any): any { return state; } +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "order.state.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Angular reference output ordering (from decorate-patterns-output.js): + // __decorate([Action({type:'First'})], OrderTestState.prototype, "instanceFirst", null); + // __decorate([Action({type:'Third'})], OrderTestState.prototype, "instanceThird", null); + // __decorate([Selector()], OrderTestState, "staticSecond", null); + // __decorate([Selector()], OrderTestState, "staticFourth", null); + // OrderTestState = __decorate([State({...}), Injectable()], OrderTestState); + + let first_pos = result.code.find("\"instanceFirst\"").unwrap(); + let third_pos = result.code.find("\"instanceThird\"").unwrap(); + let second_pos = result.code.find("\"staticSecond\"").unwrap(); + let fourth_pos = result.code.find("\"staticFourth\"").unwrap(); + let class_pos = result.code.find("OrderTestState = __decorate(").unwrap(); + + // Instance members first (in source order) + assert!(first_pos < third_pos, "instanceFirst before instanceThird"); + // Then static members (in source order) + assert!(third_pos < second_pos, "instance group before static group"); + assert!(second_pos < fourth_pos, "staticSecond before staticFourth"); + // Class decorator last + assert!(fourth_pos < class_pos, "member decorators before class decorator"); + + insta::assert_snapshot!("jit_reference_ordering", result.code); +} + +#[test] +fn test_jit_reference_decorate_patterns() { + // Reference: TestDecoratePatternsService from Angular's actual JIT output + // Tests property/method/static/getter/setter decorator patterns + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; + +function CustomPropDecorator(): any { return () => {}; } +function CustomMethodDecorator(): any { return () => {}; } + +@Injectable() +class TestDecoratePatternsService { + @CustomPropDecorator() + myProp: string = 'hello'; + + @CustomMethodDecorator() + myMethod(): void {} + + @CustomMethodDecorator() + static myStaticMethod(): void {} + + @CustomPropDecorator() + get myGetter(): string { return ''; } + + @CustomPropDecorator() + set mySetter(val: string) {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = + transform_angular_file(&allocator, "patterns.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Angular reference output (from decorate-patterns-output.js): + // __decorate([CustomPropDecorator()], X.prototype, "myProp", void 0); + // __decorate([CustomMethodDecorator()], X.prototype, "myMethod", null); + // __decorate([CustomPropDecorator()], X.prototype, "myGetter", null); + // __decorate([CustomPropDecorator()], X.prototype, "mySetter", null); + // __decorate([CustomMethodDecorator()], X, "myStaticMethod", null); + + // Property → void 0 + assert!( + result.code.contains("__decorate([CustomPropDecorator()], TestDecoratePatternsService.prototype, \"myProp\", void 0)"), + "Property decorator should use void 0 (Angular reference). Got:\n{}", + result.code + ); + + // Method → null + assert!( + result.code.contains("__decorate([CustomMethodDecorator()], TestDecoratePatternsService.prototype, \"myMethod\", null)"), + "Method decorator should use null (Angular reference). Got:\n{}", + result.code + ); + + // Static method → class, null + assert!( + result.code.contains("__decorate([CustomMethodDecorator()], TestDecoratePatternsService, \"myStaticMethod\", null)"), + "Static method should use class directly (Angular reference). Got:\n{}", + result.code + ); + + // Getter → null (accessor, not property) + assert!( + result.code.contains("__decorate([CustomPropDecorator()], TestDecoratePatternsService.prototype, \"myGetter\", null)"), + "Getter should use null (Angular reference). Got:\n{}", + result.code + ); + + // Setter → null (accessor, not property) + assert!( + result.code.contains("__decorate([CustomPropDecorator()], TestDecoratePatternsService.prototype, \"mySetter\", null)"), + "Setter should use null (Angular reference). Got:\n{}", + result.code + ); + + // Ordering: instance members first (myProp, myMethod, myGetter, mySetter), then static + let prop_pos = result.code.find("\"myProp\"").unwrap(); + let method_pos = result.code.find("\"myMethod\"").unwrap(); + let getter_pos = result.code.find("\"myGetter\"").unwrap(); + let setter_pos = result.code.find("\"mySetter\"").unwrap(); + let static_pos = result.code.find("\"myStaticMethod\"").unwrap(); + + assert!(prop_pos < static_pos, "instance before static"); + assert!(method_pos < static_pos, "instance before static"); + assert!(getter_pos < static_pos, "instance before static"); + assert!(setter_pos < static_pos, "instance before static"); + + insta::assert_snapshot!("jit_reference_decorate_patterns", result.code); +} + +#[test] +fn test_jit_reference_angular_member_decorators() { + // Reference: MyService from Angular's actual JIT output + // Angular member decorators go into propDecorators, constructor params into ctorParameters + let allocator = Allocator::default(); + let source = r#" +import { Injectable, Inject, Optional, Input, Output, ViewChild, HostListener, HostBinding, ContentChild } from '@angular/core'; + +@Injectable() +class MyService { + @Input() + myInput: string = ''; + + @Output() + myOutput: any; + + @ViewChild('ref') + myViewChild: any; + + @HostBinding('class.active') + isActive: boolean = false; + + @HostListener('click', ['$event']) + onClick(event: Event): void {} + + @ContentChild('content') + myContent: any; + + constructor( + @Inject('TOKEN') private token: string, + @Optional() private optService: any, + ) {} + + normalMethod(): void {} +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "my.service.ts", source, Some(&options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // Angular reference: propDecorators should contain all Angular member decorators + // From full-compiled-output.js: + // static propDecorators = { + // myInput: [{ type: Input }], + // myOutput: [{ type: Output }], + // myViewChild: [{ type: ViewChild, args: ['ref',] }], + // isActive: [{ type: HostBinding, args: ['class.active',] }], + // onClick: [{ type: HostListener, args: ['click', ['$event'],] }], + // myContent: [{ type: ContentChild, args: ['content',] }] + // }; + + assert!( + result.code.contains("propDecorators"), + "Should have propDecorators. Got:\n{}", + result.code + ); + assert!(result.code.contains("type: Input"), "propDecorators: Input. Got:\n{}", result.code); + assert!(result.code.contains("type: Output"), "propDecorators: Output. Got:\n{}", result.code); + assert!( + result.code.contains("type: ViewChild"), + "propDecorators: ViewChild. Got:\n{}", + result.code + ); + assert!( + result.code.contains("type: HostBinding"), + "propDecorators: HostBinding. Got:\n{}", + result.code + ); + assert!( + result.code.contains("type: HostListener"), + "propDecorators: HostListener. Got:\n{}", + result.code + ); + assert!( + result.code.contains("type: ContentChild"), + "propDecorators: ContentChild. Got:\n{}", + result.code + ); + + // Angular reference: ctorParameters should contain constructor param types and decorators + // From full-compiled-output.js: + // static ctorParameters = () => [ + // { type: String, decorators: [{ type: Inject, args: ['TOKEN',] }] }, + // { type: undefined, decorators: [{ type: Optional }] } + // ]; + assert!( + result.code.contains("ctorParameters"), + "Should have ctorParameters. Got:\n{}", + result.code + ); + assert!(result.code.contains("type: Inject"), "ctorParameters: Inject. Got:\n{}", result.code); + assert!( + result.code.contains("type: Optional"), + "ctorParameters: Optional. Got:\n{}", + result.code + ); + + // No raw Angular decorators should remain + assert!( + !result.code.contains("@Input") + && !result.code.contains("@Output") + && !result.code.contains("@ViewChild") + && !result.code.contains("@HostBinding") + && !result.code.contains("@HostListener") + && !result.code.contains("@ContentChild") + && !result.code.contains("@Inject") + && !result.code.contains("@Optional"), + "No raw Angular decorator syntax should remain. Got:\n{}", + result.code + ); + + // No __decorate calls for Angular member decorators (they go in propDecorators instead) + // Only the class __decorate([Injectable()], ...) should exist + let decorate_count = result.code.matches("__decorate(").count(); + assert!( + decorate_count == 1, + "Should have exactly 1 __decorate call (class only, not members). Got {} calls:\n{}", + decorate_count, + result.code + ); + + insta::assert_snapshot!("jit_reference_angular_member_decorators", result.code); +} + // ========================================================================= // Source map tests // ========================================================================= diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_angular_param_decorators_on_members.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_angular_param_decorators_on_members.snap new file mode 100644 index 000000000..f4b64ba62 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_angular_param_decorators_on_members.snap @@ -0,0 +1,17 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable, Inject, Optional } from "@angular/core"; +import { __decorate } from "tslib"; +function Custom() { + return function(t, k) {}; +} +let MyService = class MyService { + token; + optionalDep; + customProp = ""; +}; +__decorate([Custom()], MyService.prototype, "customProp", void 0); +MyService = __decorate([Injectable()], MyService); +export { MyService }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_complex_decorator_arguments.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_complex_decorator_arguments.snap new file mode 100644 index 000000000..30be04418 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_complex_decorator_arguments.snap @@ -0,0 +1,24 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { __decorate } from "tslib"; +function Config(opts) { + return function(t) { + return t; + }; +} +function Transform(fn) { + return function(t, k, d) {}; +} +let TestService = class TestService { + process() {} +}; +__decorate([Transform((val) => val.trim())], TestService.prototype, "process", null); +TestService = __decorate([Config({ + name: "test", + deps: [ServiceA, ServiceB], + factory: () => new TestService() +}), Injectable()], TestService); +export { TestService }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_default_export_class.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_default_export_class.snap new file mode 100644 index 000000000..4c10df838 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_default_export_class.snap @@ -0,0 +1,17 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { __decorate } from "tslib"; +function Logger() { + return function(t) { + return t; + }; +} +let AppService = class AppService { + process() {} +}; +__decorate([Logger()], AppService.prototype, "process", null); +AppService = __decorate([Logger(), Injectable()], AppService); +export default AppService; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_full_ngxs_example.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_full_ngxs_example.snap new file mode 100644 index 000000000..358c501b3 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_full_ngxs_example.snap @@ -0,0 +1,41 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { State, Action, Selector, StateContext } from "@ngxs/store"; +import { __decorate } from "tslib"; +class AddTodo { + static type = "[Todo] Add"; + constructor(text) { + this.text = text; + } +} +class ToggleTodo { + static type = "[Todo] Toggle"; + constructor(index) { + this.index = index; + } +} +let TodoState = class TodoState { + static todos(state) { + return state.items; + } + static filter(state) { + return state.filter; + } + add(ctx, action) { /* ... */} + toggle(ctx, action) { /* ... */} +}; +__decorate([Action(AddTodo)], TodoState.prototype, "add", null); +__decorate([Action(ToggleTodo)], TodoState.prototype, "toggle", null); +__decorate([Selector()], TodoState, "todos", null); +__decorate([Selector()], TodoState, "filter", null); +TodoState = __decorate([State({ + name: "todo", + defaults: { + items: [], + filter: "all" + } +}), Injectable()], TodoState); +export { TodoState }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_getter_setter_decorators.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_getter_setter_decorators.snap new file mode 100644 index 000000000..54a4fa197 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_getter_setter_decorators.snap @@ -0,0 +1,29 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Directive, Input } from "@angular/core"; +import { __decorate } from "tslib"; +function Validate() { + return function(t, k, d) {}; +} +function Transform() { + return function(t, k, d) {}; +} +let FieldDirective = class FieldDirective { + _value = ""; + get value() { + return this._value; + } + set value(v) { + this._value = v; + } + get computed() { + return this._value.toUpperCase(); + } + static propDecorators = { value: [{ type: Input }] }; +}; +__decorate([Validate()], FieldDirective.prototype, "value", null); +__decorate([Transform()], FieldDirective.prototype, "computed", null); +FieldDirective = __decorate([Directive({ selector: "[appField]" })], FieldDirective); +export { FieldDirective }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_mixed_angular_non_angular_same_member.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_mixed_angular_non_angular_same_member.snap new file mode 100644 index 000000000..2316e1a00 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_mixed_angular_non_angular_same_member.snap @@ -0,0 +1,26 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Directive, Input, Output, EventEmitter } from "@angular/core"; +import { __decorate } from "tslib"; +function Required() { + return function(t, k) {}; +} +function Throttle(ms) { + return function(t, k, d) {}; +} +let FieldDirective = class FieldDirective { + value = ""; + valueChange = new EventEmitter(); + onChange() {} + static propDecorators = { + value: [{ type: Input }], + valueChange: [{ type: Output }] + }; +}; +__decorate([Required()], FieldDirective.prototype, "value", void 0); +__decorate([Throttle(300)], FieldDirective.prototype, "valueChange", void 0); +__decorate([Throttle(100)], FieldDirective.prototype, "onChange", null); +FieldDirective = __decorate([Directive({ selector: "[appField]" })], FieldDirective); +export { FieldDirective }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_classes_same_file.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_classes_same_file.snap new file mode 100644 index 000000000..b434c82b7 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_classes_same_file.snap @@ -0,0 +1,23 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Component, Injectable } from "@angular/core"; +import { __decorate } from "tslib"; +function Logger() { + return function(t) { + return t; + }; +} +let FooComponent = class FooComponent {}; +FooComponent = __decorate([Component({ + selector: "app-foo", + template: "

foo

" +})], FooComponent); +export { FooComponent }; +let FooService = class FooService { + doWork() {} +}; +__decorate([Logger()], FooService.prototype, "doWork", null); +FooService = __decorate([Logger(), Injectable()], FooService); +export { FooService }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_decorators_same_member.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_decorators_same_member.snap new file mode 100644 index 000000000..5c1c68d95 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_multiple_decorators_same_member.snap @@ -0,0 +1,25 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { __decorate } from "tslib"; +function Log() { + return function(t, k, d) {}; +} +function Memoize() { + return function(t, k, d) {}; +} +function Validate() { + return function(t, k) {}; +} +let MyService = class MyService { + compute() { + return 42; + } + name = ""; +}; +__decorate([Log(), Memoize()], MyService.prototype, "compute", null); +__decorate([Validate(), Log()], MyService.prototype, "name", void 0); +MyService = __decorate([Injectable()], MyService); +export { MyService }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_class_decorators.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_class_decorators.snap new file mode 100644 index 000000000..1f22f25bf --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_class_decorators.snap @@ -0,0 +1,13 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { State } from "@ngxs/store"; +import { __decorate } from "tslib"; +let TodoState = class TodoState {}; +TodoState = __decorate([State({ + name: "todo", + defaults: { items: [] } +}), Injectable()], TodoState); +export { TodoState }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_method_decorators.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_method_decorators.snap new file mode 100644 index 000000000..e5595054a --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_angular_method_decorators.snap @@ -0,0 +1,19 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { State, Action, Selector } from "@ngxs/store"; +import { __decorate } from "tslib"; +let TodoState = class TodoState { + static todos(state) { + return state.items; + } + add(ctx, action) { + ctx.setState(action); + } +}; +__decorate([Action(AddTodo)], TodoState.prototype, "add", null); +__decorate([Selector()], TodoState, "todos", null); +TodoState = __decorate([State({ name: "todo" }), Injectable()], TodoState); +export { TodoState }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_exported_class.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_exported_class.snap new file mode 100644 index 000000000..5db9b1716 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_non_exported_class.snap @@ -0,0 +1,16 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { __decorate } from "tslib"; +function Singleton() { + return function(t) { + return t; + }; +} +let InternalService = class InternalService { + getInstance() {} +}; +__decorate([Singleton()], InternalService.prototype, "getInstance", null); +InternalService = __decorate([Singleton(), Injectable()], InternalService); diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_property_decorator_void_0.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_property_decorator_void_0.snap new file mode 100644 index 000000000..c04096c9e --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_property_decorator_void_0.snap @@ -0,0 +1,20 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { __decorate } from "tslib"; +function Validate() { + return function(t, k) {}; +} +function Log(target, key, desc) {} +let MyService = class MyService { + name = ""; + greet() { + return "hello"; + } +}; +__decorate([Validate()], MyService.prototype, "name", void 0); +__decorate([Log], MyService.prototype, "greet", null); +MyService = __decorate([Injectable()], MyService); +export { MyService }; diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_angular_member_decorators.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_angular_member_decorators.snap new file mode 100644 index 000000000..2f5c3fe7b --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_angular_member_decorators.snap @@ -0,0 +1,50 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable, Inject, Optional, Input, Output, ViewChild, HostListener, HostBinding, ContentChild } from "@angular/core"; +import { __decorate } from "tslib"; +let MyService = class MyService { + myInput = ""; + myOutput; + myViewChild; + isActive = false; + onClick(event) {} + myContent; + constructor(token, optService) { + this.token = token; + this.optService = optService; + } + normalMethod() {} + static ctorParameters = () => [{ + type: undefined, + decorators: [{ + type: Inject, + args: ["TOKEN"] + }] + }, { + type: undefined, + decorators: [{ type: Optional }] + }]; + static propDecorators = { + myInput: [{ type: Input }], + myOutput: [{ type: Output }], + myViewChild: [{ + type: ViewChild, + args: ["ref"] + }], + isActive: [{ + type: HostBinding, + args: ["class.active"] + }], + onClick: [{ + type: HostListener, + args: ["click", ["$event"]] + }], + myContent: [{ + type: ContentChild, + args: ["content"] + }] + }; +}; +MyService = __decorate([Injectable()], MyService); diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_animals_state.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_animals_state.snap new file mode 100644 index 000000000..f82045c1d --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_animals_state.snap @@ -0,0 +1,19 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { State, Action, Selector } from "@ngxs/store"; +import { __decorate } from "tslib"; +let AnimalsState = class AnimalsState { + static getAnimals(state) { + return state; + } + addAnimal(ctx, action) {} +}; +__decorate([Action({ type: "AddAnimal" })], AnimalsState.prototype, "addAnimal", null); +__decorate([Selector()], AnimalsState, "getAnimals", null); +AnimalsState = __decorate([State({ + name: "animals", + defaults: [] +}), Injectable()], AnimalsState); diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_decorate_patterns.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_decorate_patterns.snap new file mode 100644 index 000000000..4d81d18eb --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_decorate_patterns.snap @@ -0,0 +1,27 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { __decorate } from "tslib"; +function CustomPropDecorator() { + return () => {}; +} +function CustomMethodDecorator() { + return () => {}; +} +let TestDecoratePatternsService = class TestDecoratePatternsService { + myProp = "hello"; + myMethod() {} + static myStaticMethod() {} + get myGetter() { + return ""; + } + set mySetter(val) {} +}; +__decorate([CustomPropDecorator()], TestDecoratePatternsService.prototype, "myProp", void 0); +__decorate([CustomMethodDecorator()], TestDecoratePatternsService.prototype, "myMethod", null); +__decorate([CustomPropDecorator()], TestDecoratePatternsService.prototype, "myGetter", null); +__decorate([CustomPropDecorator()], TestDecoratePatternsService.prototype, "mySetter", null); +__decorate([CustomMethodDecorator()], TestDecoratePatternsService, "myStaticMethod", null); +TestDecoratePatternsService = __decorate([Injectable()], TestDecoratePatternsService); diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_ordering.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_ordering.snap new file mode 100644 index 000000000..b78961089 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_reference_ordering.snap @@ -0,0 +1,25 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: result.code +--- +import { Injectable } from "@angular/core"; +import { State, Action, Selector } from "@ngxs/store"; +import { __decorate } from "tslib"; +let OrderTestState = class OrderTestState { + instanceFirst(ctx) {} + static staticSecond(state) { + return state; + } + instanceThird(ctx) {} + static staticFourth(state) { + return state; + } +}; +__decorate([Action({ type: "First" })], OrderTestState.prototype, "instanceFirst", null); +__decorate([Action({ type: "Third" })], OrderTestState.prototype, "instanceThird", null); +__decorate([Selector()], OrderTestState, "staticSecond", null); +__decorate([Selector()], OrderTestState, "staticFourth", null); +OrderTestState = __decorate([State({ + name: "order", + defaults: {} +}), Injectable()], OrderTestState);