diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index d4ceea707..ee3304aff 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -954,6 +954,7 @@ fn extract_param_dependency<'a>( let mut self_ = false; let mut host = false; let mut inject_token: Option> = None; + let mut inject_token_is_string = false; let mut attribute_name: Option> = None; for decorator in ¶m.decorators { @@ -963,7 +964,15 @@ fn extract_param_dependency<'a>( // @Inject(TOKEN) - extract the token if let Expression::CallExpression(call) = &decorator.expression { if let Some(arg) = call.arguments.first() { - inject_token = extract_inject_token(arg); + // `@Inject('TOKEN')`: a string token, as ngtsc compiles it + if let Argument::StringLiteral(lit) = arg + && let Some(value) = lit.value.as_str() + { + inject_token = Some(value.into()); + inject_token_is_string = true; + } else { + inject_token = extract_inject_token(arg); + } } } } @@ -996,6 +1005,16 @@ fn extract_param_dependency<'a>( return R3DependencyMetadata::attribute(attr_name); } + if inject_token_is_string && let Some(value) = token { + let mut dep = R3DependencyMetadata::new(value); + dep.token_is_string = true; + dep.optional = optional; + dep.skip_self = skip_self; + dep.self_ = self_; + dep.host = host; + return dep; + } + // Build the dependency metadata let mut dep = match &token { Some(token_name) => { diff --git a/crates/oxc_angular_compiler/src/component/dependency.rs b/crates/oxc_angular_compiler/src/component/dependency.rs index 66486d411..e247bcbae 100644 --- a/crates/oxc_angular_compiler/src/component/dependency.rs +++ b/crates/oxc_angular_compiler/src/component/dependency.rs @@ -75,6 +75,9 @@ pub struct R3DependencyMetadata<'a> { /// as a whole becomes `ɵɵinvalidFactory()` — matching Angular's /// `ValueUnavailableKind.TYPE_ONLY_IMPORT` behaviour. See issue #288. pub type_only_invalid: bool, + + /// The token is a string literal (`@Inject('TOKEN')`), not a reference. + pub token_is_string: bool, } impl<'a> R3DependencyMetadata<'a> { @@ -91,6 +94,7 @@ impl<'a> R3DependencyMetadata<'a> { self_: false, skip_self: false, type_only_invalid: false, + token_is_string: false, } } @@ -107,6 +111,7 @@ impl<'a> R3DependencyMetadata<'a> { self_: false, skip_self: false, type_only_invalid: false, + token_is_string: false, } } @@ -126,6 +131,7 @@ impl<'a> R3DependencyMetadata<'a> { self_: false, skip_self: false, type_only_invalid: true, + token_is_string: false, } } @@ -166,6 +172,7 @@ impl<'a> R3DependencyMetadata<'a> { self_: false, skip_self: false, type_only_invalid: false, + token_is_string: false, } } @@ -373,6 +380,13 @@ fn create_token_expression<'a>( token_name: &Ident<'a>, namespace_registry: &mut NamespaceRegistry<'a>, ) -> OutputExpression<'a> { + if dep.token_is_string { + // `@Inject('TOKEN')` injects the string itself + return OutputExpression::Literal(Box::new_in( + LiteralExpr { value: LiteralValue::String(token_name.clone()), source_span: None }, + &allocator, + )); + } if let Some(ref source_module) = dep.token_source_module { // Check if this token has an existing named import that can be reused if dep.has_named_import { diff --git a/crates/oxc_angular_compiler/tests/inject_string_token_test.rs b/crates/oxc_angular_compiler/tests/inject_string_token_test.rs new file mode 100644 index 000000000..713eea1ea --- /dev/null +++ b/crates/oxc_angular_compiler/tests/inject_string_token_test.rs @@ -0,0 +1,89 @@ +//! `@Inject('TOKEN')` with a string token injects the string, as ngtsc compiles +//! it: `ɵɵdirectiveInject('TOKEN')` in a component's, directive's or pipe's +//! factory and `ɵɵinject('TOKEN')` in an injectable's, with the parameter's other +//! flags. The expected calls are `@angular/compiler-cli` 21.0.1's output for the +//! same classes. + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{TransformOptions, transform_angular_file}; + +/// The factory 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_contains(code: &str, expected: &str) { + assert!(code.contains(expected), "Expected `{expected}` in:\n{code}"); +} + +#[test] +fn component_factory_injects_the_string() { + let code = compile( + r" +import { Component, Inject, Optional, Self } from '@angular/core'; +@Component({ selector: 'a-cmp', template: '' }) +export class ACmp { + constructor(@Inject('API_URL') url: string, @Optional() @Self() @Inject('FLAGS') flags: string[]) {} +} +", + ); + assert_contains(&code, r#"ɵɵdirectiveInject("API_URL"),i0.ɵɵdirectiveInject("FLAGS",10))"#); + assert!(!code.contains("ɵɵinvalidFactoryDep"), "Got:\n{code}"); +} + +#[test] +fn injectable_factory_injects_the_string() { + let code = compile( + r" +import { Inject, Injectable } from '@angular/core'; +@Injectable({ providedIn: 'root' }) +export class AService { + constructor(@Inject('API_URL') url: string) {} +} +", + ); + assert_contains(&code, r#"(i0.ɵɵinject("API_URL"))"#); +} + +#[test] +fn directive_factory_injects_the_string() { + let code = compile( + r" +import { Directive, Inject } from '@angular/core'; +@Directive({ selector: '[aDir]' }) +export class ADir { + constructor(@Inject('API_URL') url: string) {} +} +", + ); + assert_contains(&code, r#"(i0.ɵɵdirectiveInject("API_URL"))"#); +} + +#[test] +fn pipe_factory_injects_the_string() { + let code = compile( + r" +import { Inject, Pipe } from '@angular/core'; +@Pipe({ name: 'aPipe' }) +export class APipe { + constructor(@Inject('API_URL') url: string) {} + transform(v: unknown) { return v; } +} +", + ); + assert_contains(&code, r#"(i0.ɵɵdirectiveInject("API_URL",16))"#); +}