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
21 changes: 20 additions & 1 deletion crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ fn extract_param_dependency<'a>(
let mut self_ = false;
let mut host = false;
let mut inject_token: Option<Ident<'a>> = None;
let mut inject_token_is_string = false;
let mut attribute_name: Option<Ident<'a>> = None;

for decorator in &param.decorators {
Expand All @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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) => {
Expand Down
14 changes: 14 additions & 0 deletions crates/oxc_angular_compiler/src/component/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -91,6 +94,7 @@ impl<'a> R3DependencyMetadata<'a> {
self_: false,
skip_self: false,
type_only_invalid: false,
token_is_string: false,
}
}

Expand All @@ -107,6 +111,7 @@ impl<'a> R3DependencyMetadata<'a> {
self_: false,
skip_self: false,
type_only_invalid: false,
token_is_string: false,
}
}

Expand All @@ -126,6 +131,7 @@ impl<'a> R3DependencyMetadata<'a> {
self_: false,
skip_self: false,
type_only_invalid: true,
token_is_string: false,
}
}

Expand Down Expand Up @@ -166,6 +172,7 @@ impl<'a> R3DependencyMetadata<'a> {
self_: false,
skip_self: false,
type_only_invalid: false,
token_is_string: false,
}
}

Expand Down Expand Up @@ -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 {
Expand Down
89 changes: 89 additions & 0 deletions crates/oxc_angular_compiler/tests/inject_string_token_test.rs
Original file line number Diff line number Diff line change
@@ -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))"#);
}
Loading