Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,15 +723,12 @@ 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')`: 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);
}
// `@Inject('TOKEN')`: a string token, as ngtsc compiles it.
// The last `@Inject` wins, so the flag is set on every one.
let string_token =
arg.as_expression().and_then(extract_string_inject_token);
inject_token_is_string = string_token.is_some();
inject_token = string_token.or_else(|| extract_inject_token(arg));
}
}
}
Expand Down Expand Up @@ -812,6 +809,18 @@ fn extract_param_dependency<'a>(
dep
}

/// The value of a string `@Inject` token: a string literal, or a template literal with no
/// substitutions, also inside parentheses or a TypeScript cast.
fn extract_string_inject_token<'a>(expr: &'a Expression<'a>) -> Option<Ident<'a>> {
match expr.get_inner_expression() {
Expression::StringLiteral(lit) => lit.value.as_str().map(Ident::from),
Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => {
tpl.quasis.first()?.value.cooked.as_ref()?.as_str().map(Ident::from)
}
_ => None,
}
}

/// Extract the injection token from an @Inject decorator argument.
fn extract_inject_token<'a>(arg: &'a Argument<'a>) -> Option<Ident<'a>> {
match arg {
Expand Down
21 changes: 16 additions & 5 deletions crates/oxc_angular_compiler/src/partial/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ fn ident_array_string_literals<'a>(
/// Converts `component::dependency::R3DependencyMetadata` (which stores
/// tokens as bare `Ident`s) into the `factory::R3DependencyMetadata` shape
/// (which stores tokens as `OutputExpression`s) by wrapping each token in
/// a `ReadVar`.
/// a `ReadVar`, or a string literal for a string `@Inject` token.
fn clone_factory_deps<'a>(
allocator: &'a Allocator,
deps: &Option<Vec<'a, ComponentDep<'a>>>,
Expand All @@ -833,10 +833,21 @@ fn clone_factory_deps<'a>(
let mut out = Vec::with_capacity_in(deps.len(), &allocator);
for dep in deps {
let token_expr = dep.token.as_ref().map(|t| {
OutputExpression::ReadVar(Box::new_in(
ReadVarExpr { name: t.clone(), source_span: None },
&allocator,
))
if dep.token_is_string {
// `@Inject('TOKEN')` injects the string itself
OutputExpression::Literal(Box::new_in(
LiteralExpr {
value: LiteralValue::String(t.clone()),
source_span: None,
},
&allocator,
))
} else {
OutputExpression::ReadVar(Box::new_in(
ReadVarExpr { name: t.clone(), source_span: None },
&allocator,
))
}
});
let attr_expr = dep.attribute_name.as_ref().map(|a| {
OutputExpression::Literal(Box::new_in(
Expand Down
60 changes: 60 additions & 0 deletions crates/oxc_angular_compiler/tests/inject_string_token_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,63 @@ export class APipe {
);
assert_contains(&code, r#"(i0.ɵɵdirectiveInject("API_URL",16))"#);
}

#[test]
fn component_string_token_forms() {
// A template literal with no substitutions, parentheses and TypeScript casts are
// still string tokens.
let code = compile(
r"
import { Component, Inject } from '@angular/core';
@Component({ selector: 'a-cmp', template: '' })
export class ACmp {
constructor(@Inject(`TPL`) a: string, @Inject(('PAREN')) b: string, @Inject('CAST' as any) c: string) {}
}
",
);
assert_contains(
&code,
r#"(i0.ɵɵdirectiveInject("TPL"),i0.ɵɵdirectiveInject("PAREN"),i0.ɵɵdirectiveInject("CAST"))"#,
);
}

#[test]
fn component_last_inject_wins() {
// ngtsc uses the last `@Inject`, so a class token after a string one is not a string.
let code = compile(
r"
import { Component, Inject } from '@angular/core';
class Svc {}
@Component({ selector: 'a-cmp', template: '' })
export class ACmp {
constructor(@Inject('dup') @Inject(Svc) a: any) {}
}
",
);
assert_contains(&code, r"(i0.ɵɵdirectiveInject(Svc))");
}

#[test]
fn partial_component_factory_declares_the_string() {
let allocator = Allocator::default();
let options = TransformOptions {
compilation_mode: oxc_angular_compiler::CompilationMode::Partial,
..TransformOptions::default()
};
let result = transform_angular_file(
&allocator,
"test.ts",
r#"
import { Component, Inject, Optional } from '@angular/core';
@Component({ selector: 'a-cmp', template: '' })
export class ACmp {
constructor(@Inject('API_URL') url: string, @Optional() @Inject("it's") b: string) {}
}
"#,
Some(&options),
None,
);
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
let code: String = result.code.chars().filter(|c| !c.is_whitespace()).collect();
assert_contains(&code, r#"deps:[{token:"API_URL"},{token:"it's",optional:true}]"#);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* `@Inject('TOKEN')` with a string token on a component constructor.
*/
import type { Fixture } from '../types.js'

export const fixtures: Fixture[] = [
{
name: 'component-inject-string-token',
category: 'factory',
description: 'component factory injects string tokens, with flags',
className: 'InjectStringTokenComponent',
type: 'full-transform',
sourceCode: `
import { Component, Inject, Optional, Self } from '@angular/core';

@Component({
selector: 'app-inject-string-token',
template: '',
})
export class InjectStringTokenComponent {
constructor(
@Inject('API_URL') url: string,
@Optional() @Self() @Inject('FLAGS') flags: string[],
@Inject(('PAREN')) paren: string,
) {}
}
`.trim(),
expectedFeatures: ['ɵɵdirectiveInject'],
},
]
12 changes: 12 additions & 0 deletions napi/angular-compiler/e2e/compare/fixtures/known-differences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ export const KNOWN_DIFFERENCES: Record<string, KnownDifference> = {
],
reasons: [SET_CLASS_METADATA_IMPORT],
},
'factory/component-inject-string-token': {
fields: [],
importDiffs: [
{
type: 'different',
moduleSource: '@angular/core',
expected: ['Component'],
actual: ['Component', 'Inject', 'Optional', 'Self'],
},
],
reasons: [SET_CLASS_METADATA_IMPORT],
},
'full-file/component-with-services': {
fields: [],
importDiffs: [
Expand Down
Loading