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
94 changes: 75 additions & 19 deletions crates/oxc_angular_compiler/src/class_metadata/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use crate::directive::{
try_parse_signal_input, try_parse_signal_model, try_parse_signal_output,
};
use crate::output::ast::{
ArrowFunctionBody, ArrowFunctionExpr, LiteralArrayExpr, LiteralExpr, LiteralMapEntry,
LiteralMapExpr, LiteralValue, OutputExpression, ReadPropExpr, ReadVarExpr,
ArrowFunctionBody, ArrowFunctionExpr, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr,
LiteralMapEntry, LiteralMapExpr, LiteralValue, OutputExpression, ReadPropExpr, ReadVarExpr,
};
use crate::output::oxc_converter::{
convert_oxc_expression, convert_plain_properties, is_plain_property, make_raw_source,
Expand Down Expand Up @@ -781,7 +781,23 @@ fn build_signal_query_decorator<'a>(
// query's `WrappedNodeExpr` fallback, only the raw source can stand in.
let first = call.arguments.first()?;
let predicate = match first.as_expression() {
Some(expr) => convert_oxc_expression(allocator, expr, source_text)?,
// Like ngtsc's `memberMetadataFromSignalQuery`, a predicate other than a string or a
// call (a class) is wrapped in `forwardRef(() => <predicate>)`: `setClassMetadata` runs
// as soon as the class is defined, unlike the query initializer, so a class declared
// later in the file would be read before it is initialized.
Some(expr) => {
let predicate = convert_oxc_expression(allocator, expr, source_text)?;
let string_or_call = match expr {
Expression::StringLiteral(_) | Expression::CallExpression(_) => true,
Expression::TemplateLiteral(template) => template.expressions.is_empty(),
_ => false,
Comment thread
Brooooooklyn marked this conversation as resolved.
};
if string_or_call {
predicate
} else {
forward_ref(allocator, namespace_registry, predicate)
}
}
None => make_raw_source(allocator, source_text, first.span())?,
};
let mut args = AllocVec::new_in(&allocator);
Expand Down Expand Up @@ -810,30 +826,45 @@ fn build_signal_query_decorator<'a>(
Some(build_core_decorator(allocator, namespace_registry, decorator_name, args))
}

/// Build `{ type: i0.<name>, args: ["<arg>"] }` for a decorator taking a single string.
fn build_core_decorator_with_string_arg<'a>(
/// `i0.forwardRef(() => <expression>)`.
fn forward_ref<'a>(
allocator: &'a Allocator,
namespace_registry: &mut NamespaceRegistry<'a>,
decorator_name: &'static str,
arg: Ident<'a>,
expression: OutputExpression<'a>,
) -> OutputExpression<'a> {
let arrow = OutputExpression::ArrowFunction(Box::new_in(
ArrowFunctionExpr {
params: AllocVec::new_in(&allocator),
body: ArrowFunctionBody::Expression(Box::new_in(expression, &allocator)),
source_span: None,
},
&allocator,
));
let mut args = AllocVec::new_in(&allocator);
args.push(string_literal(allocator, arg));
build_core_decorator(allocator, namespace_registry, decorator_name, args)
args.push(arrow);
OutputExpression::InvokeFunction(Box::new_in(
InvokeFunctionExpr {
fn_expr: Box::new_in(
core_member(allocator, namespace_registry, "forwardRef"),
&allocator,
),
args,
pure: false,
optional: false,
source_span: None,
},
&allocator,
))
}

/// Build a synthetic Angular core decorator metadata object: `{ type: i0.<name>, args: [...] }`.
/// The decorator type is referenced through the `@angular/core` namespace import (`i0`), since a
/// component using signal APIs imports `input`/`output`/… rather than the `Input`/`Output`/query
/// decorators themselves.
fn build_core_decorator<'a>(
/// `i0.<name>`: a member of the `@angular/core` namespace import.
fn core_member<'a>(
allocator: &'a Allocator,
namespace_registry: &mut NamespaceRegistry<'a>,
decorator_name: &'static str,
args: AllocVec<'a, OutputExpression<'a>>,
name: &'static str,
) -> OutputExpression<'a> {
let core_namespace = namespace_registry.get_or_assign(&Ident::from(crate::r3::CORE));
let type_expr = OutputExpression::ReadProp(Box::new_in(
OutputExpression::ReadProp(Box::new_in(
ReadPropExpr {
receiver: Box::new_in(
OutputExpression::ReadVar(Box::new_in(
Expand All @@ -842,12 +873,37 @@ fn build_core_decorator<'a>(
)),
&allocator,
),
name: Ident::from(decorator_name),
name: Ident::from(name),
optional: false,
source_span: None,
},
&allocator,
));
))
}

/// Build `{ type: i0.<name>, args: ["<arg>"] }` for a decorator taking a single string.
fn build_core_decorator_with_string_arg<'a>(
allocator: &'a Allocator,
namespace_registry: &mut NamespaceRegistry<'a>,
decorator_name: &'static str,
arg: Ident<'a>,
) -> OutputExpression<'a> {
let mut args = AllocVec::new_in(&allocator);
args.push(string_literal(allocator, arg));
build_core_decorator(allocator, namespace_registry, decorator_name, args)
}

/// Build a synthetic Angular core decorator metadata object: `{ type: i0.<name>, args: [...] }`.
/// The decorator type is referenced through the `@angular/core` namespace import (`i0`), since a
/// component using signal APIs imports `input`/`output`/… rather than the `Input`/`Output`/query
/// decorators themselves.
fn build_core_decorator<'a>(
allocator: &'a Allocator,
namespace_registry: &mut NamespaceRegistry<'a>,
decorator_name: &'static str,
args: AllocVec<'a, OutputExpression<'a>>,
) -> OutputExpression<'a> {
let type_expr = core_member(allocator, namespace_registry, decorator_name);

let mut entries = AllocVec::new_in(&allocator);
entries.push(LiteralMapEntry::new(Ident::from("type"), type_expr, false));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! In `setClassMetadata`, a signal query's predicate (`contentChildren(Pane)`) is
//! wrapped in `forwardRef(() => Pane)` unless it is a string or a call, as
//! ngtsc's `memberMetadataFromSignalQuery` does: `setClassMetadata` runs as soon
//! as the class is defined, so a class declared later in the file would be read
//! before it is initialized (a TDZ error in development builds). The expected
//! metadata is `@angular/compiler-cli` 21.0.1's for the same classes.

use oxc_allocator::Allocator;
use oxc_angular_compiler::{TransformOptions, transform_angular_file};

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 query_of_a_class_declared_later_is_a_forward_ref() {
let code = compile(
r"
import { Component, contentChildren, viewChild } from '@angular/core';
@Component({ selector: 'd-parent', template: '<ng-content />' })
export class DParent {
panes = contentChildren(DPane);
first = viewChild(DPane);
}
@Component({ selector: 'd-pane', template: '' })
export class DPane {}
",
);
assert_contains(
&code,
"panes:[{type:i0.ContentChildren,args:[i0.forwardRef(()=>DPane),{isSignal:true}]}]",
);
assert_contains(
&code,
"first:[{type:i0.ViewChild,args:[i0.forwardRef(()=>DPane),{isSignal:true}]}]",
);
}

#[test]
fn query_of_a_class_declared_earlier_is_a_forward_ref_too() {
let code = compile(
r"
import { Component, ContentChildren, ElementRef, QueryList, ViewChildren, contentChild, viewChildren } from '@angular/core';
@Component({ selector: 'f-pane', template: '' })
export class FPane {}
@Component({ selector: 'f-parent', template: '<ng-content />' })
export class FParent {
one = contentChild(FPane, { read: ElementRef });
views = viewChildren('ref');
@ContentChildren(FPane) legacy!: QueryList<FPane>;
@ViewChildren('a, b') strings!: QueryList<unknown>;
}
",
);
assert_contains(
&code,
"one:[{type:i0.ContentChild,args:[i0.forwardRef(()=>FPane),{...{read:ElementRef},isSignal:true}]}]",
);
// a string predicate, and decorator queries, stay as written
assert_contains(&code, r#"views:[{type:i0.ViewChildren,args:["ref",{isSignal:true}]}]"#);
assert_contains(&code, "legacy:[{type:ContentChildren,args:[FPane]}]");
assert_contains(&code, r#"strings:[{type:ViewChildren,args:["a,b"]}]"#);
}
Loading