diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 72502a9a0..c1a861c2d 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -387,15 +387,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/src/dts.rs b/crates/oxc_angular_compiler/src/dts.rs index e45a4fb41..e05462aed 100644 --- a/crates/oxc_angular_compiler/src/dts.rs +++ b/crates/oxc_angular_compiler/src/dts.rs @@ -597,18 +597,24 @@ fn generate_ctor_deps_type_from_component_deps(deps: Option<&[R3DependencyMetada /// pipes, NgModules, and injectables. /// /// Returns `never` if no dependency has any special flags (attribute, optional, host, self, skipSelf). -/// Otherwise returns a tuple type like `[null, {attribute: string, optional: true}, null]`. +/// Otherwise returns a tuple type like `[null, {attribute: "title", optional: true}, null]`. fn generate_ctor_deps_type_from_factory_deps( deps: Option<&[crate::factory::R3DependencyMetadata]>, ) -> String { + use crate::output::ast::{LiteralValue, OutputExpression}; match deps { None => "never".to_string(), Some(deps) => generate_ctor_deps_type(deps.iter().map(|d| { - let attribute_entry = if d.attribute_name_type.is_some() { - Some("attribute: string".to_string()) - } else { - None - }; + // As ngtsc's `createCtorDepType`: the attribute name's literal type. + let attribute_entry = d.attribute_name_type.as_ref().map(|name| match name { + OutputExpression::Literal(lit) => match &lit.value { + LiteralValue::String(name) => { + format!("attribute: \"{}\"", escape_dts_string(name.as_str())) + } + _ => "attribute: unknown".to_string(), + }, + _ => "attribute: unknown".to_string(), + }); (attribute_entry, d.optional, d.host, d.self_, d.skip_self) })), } diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index d4ee39076..2b552f937 100644 --- a/crates/oxc_angular_compiler/src/injectable/decorator.rs +++ b/crates/oxc_angular_compiler/src/injectable/decorator.rs @@ -651,15 +651,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/src/ng_module/decorator.rs b/crates/oxc_angular_compiler/src/ng_module/decorator.rs index 13e8da266..fe46fc0cc 100644 --- a/crates/oxc_angular_compiler/src/ng_module/decorator.rs +++ b/crates/oxc_angular_compiler/src/ng_module/decorator.rs @@ -584,15 +584,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/src/pipe/decorator.rs b/crates/oxc_angular_compiler/src/pipe/decorator.rs index 3933574d9..513f28846 100644 --- a/crates/oxc_angular_compiler/src/pipe/decorator.rs +++ b/crates/oxc_angular_compiler/src/pipe/decorator.rs @@ -349,15 +349,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/tests/attribute_inject_test.rs b/crates/oxc_angular_compiler/tests/attribute_inject_test.rs new file mode 100644 index 000000000..67333b0b9 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/attribute_inject_test.rs @@ -0,0 +1,135 @@ +//! `@Attribute('name')` on a constructor parameter compiles to +//! `ɵɵinjectAttribute('name')` whatever the class kind, as ngtsc does +//! (`compileInjectDependency` in `r3_factory.ts` checks `attributeNameType` +//! before the target). A plain token inject makes Angular look the attribute +//! name up as a DI token at runtime. + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{TransformOptions, transform_angular_file}; + +/// The compiled code for `source`, without whitespace and with `'` as `"`. +fn compile(source: &str) -> String { + let allocator = Allocator::default(); + let result = transform_angular_file( + &allocator, + "test.ts", + source, + Some(&TransformOptions::default()), + None, + ); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + result + .code + .chars() + .filter(|c| !c.is_whitespace()) + .map(|c| if c == '\'' { '"' } else { c }) + .collect() +} + +fn assert_injects_attribute(code: &str) { + assert!(code.contains(r#"(i0.ɵɵinjectAttribute("title"))"#), "Got:\n{code}"); +} + +#[test] +fn directive_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, Directive } from '@angular/core'; +@Directive({ selector: '[d]' }) +export class D { + constructor(@Attribute('title') title: string) {} +} +", + ); + assert_injects_attribute(&code); +} + +#[test] +fn pipe_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, Pipe } from '@angular/core'; +@Pipe({ name: 'p' }) +export class P { + constructor(@Attribute('title') title: string) {} + transform(v: unknown) { return v; } +} +", + ); + assert_injects_attribute(&code); +} + +#[test] +fn injectable_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, Injectable } from '@angular/core'; +@Injectable() +export class S { + constructor(@Attribute('title') title: string) {} +} +", + ); + assert_injects_attribute(&code); +} + +#[test] +fn ng_module_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, NgModule } from '@angular/core'; +@NgModule({}) +export class M { + constructor(@Attribute('title') title: string) {} +} +", + ); + assert_injects_attribute(&code); +} + +/// The `.d.ts` factory type names the attribute, as ngtsc's `createCtorDepType` +/// writes `attributeNameType`: `[{ attribute: "title"; optional: true; }]`. +#[test] +fn factory_declaration_names_the_attribute() { + let source = r#" +import { Attribute, Directive, Injectable, NgModule, Optional, Pipe } from '@angular/core'; +@Directive({ selector: '[d]' }) +export class D { constructor(@Optional() @Attribute('title') t: string) {} } +@Pipe({ name: 'p' }) +export class P { constructor(@Attribute('say "hi"') t: string) {} transform(v: unknown) { return v; } } +@Injectable() +export class S { constructor(@Attribute('title') t: string) {} } +@NgModule({}) +export class M { constructor(@Attribute('title') t: string) {} } +"#; + let allocator = Allocator::default(); + let result = transform_angular_file( + &allocator, + "test.ts", + source, + Some(&TransformOptions::default()), + None, + ); + let factory_type = |class: &str| { + let decl = result.dts_declarations.iter().find(|d| d.class_name == class).unwrap(); + let line = decl.members.lines().find(|l| l.contains("static ɵfac")).unwrap(); + line.trim().to_string() + }; + assert!( + factory_type("D").contains(r#"[{ attribute: "title", optional: true }]"#), + "{}", + factory_type("D") + ); + assert!( + factory_type("P").contains(r#"[{ attribute: "say \"hi\"" }]"#), + "{}", + factory_type("P") + ); + for class in ["S", "M"] { + assert!( + factory_type(class).contains(r#"[{ attribute: "title" }]"#), + "{}", + factory_type(class) + ); + } +}