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: 12 additions & 9 deletions crates/oxc_angular_compiler/src/class_metadata/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use rustc_hash::{FxHashMap, FxHashSet};

use crate::component::{ImportMap, NamespaceRegistry, R3DependencyMetadata};
use crate::directive::{
QUERY_APIS, R3InputMetadata, StringConsts, initializer_api_call, resolve_template_literal,
QUERY_APIS, R3InputMetadata, StringConsts, evaluate_string_value, initializer_api_call,
try_parse_signal_input, try_parse_signal_model, try_parse_signal_output,
};
use crate::output::ast::{
Expand Down Expand Up @@ -55,11 +55,11 @@ use crate::util::lone_surrogate::js_str_to_ident;
/// upstream's `if (dec.name !== 'Component') return dec;`.
pub fn build_decorator_metadata_array<'a>(
allocator: &'a Allocator,
decorators: &[&Decorator<'a>],
decorators: &[&'a Decorator<'a>],
source_text: Option<&'a str>,
inlined_template: Option<&'a str>,
inlined_styles: Option<&[Ident<'a>]>,
component_decorator: Option<&Decorator<'a>>,
component_decorator: Option<&'a Decorator<'a>>,
consts: Option<&StringConsts<'a>>,
) -> OutputExpression<'a> {
// Whether `transformDecoratorResources` rewrites the `args` of decorators
Expand Down Expand Up @@ -339,7 +339,7 @@ fn inline_component_resources<'a>(
fn drop_unresolvable_template_literal_fields<'a>(
allocator: &'a Allocator,
converted: &mut OutputExpression<'a>,
source: &Expression<'a>,
source: &'a Expression<'a>,
consts: &StringConsts<'a>,
) {
let Expression::ObjectExpression(obj) = source else {
Expand All @@ -360,7 +360,10 @@ fn drop_unresolvable_template_literal_fields<'a>(
if tpl.expressions.is_empty() {
continue;
}
if resolve_template_literal(allocator, tpl, consts).is_some() {
// Read the value the way the compile path reads `template`: the
// const folder first, then the partial evaluator (`${WIDTH}px` with
// `const WIDTH = 150`, `${A + B}`, an imported const, ...).
if evaluate_string_value(allocator, &prop.value, consts).is_some() {
continue; // Resolvable — leave the converted value as-is.
}
if let Some(key) = get_property_key_name(&prop.key) {
Expand Down Expand Up @@ -395,7 +398,7 @@ fn build_template_entry<'a>(allocator: &'a Allocator, content: &'a str) -> Liter
/// [`build_ctor_params_metadata_in`] recognises only Angular's, like ngtsc.
pub fn build_ctor_params_metadata<'a>(
allocator: &'a Allocator,
class: &Class<'a>,
class: &'a Class<'a>,
constructor_deps: Option<&[R3DependencyMetadata<'a>]>,
namespace_registry: &mut NamespaceRegistry<'a>,
import_map: &ImportMap<'a>,
Expand Down Expand Up @@ -426,7 +429,7 @@ pub fn build_ctor_params_metadata<'a>(
/// `decorators: []`.
pub fn build_ctor_params_metadata_in<'a>(
allocator: &'a Allocator,
class: &Class<'a>,
class: &'a Class<'a>,
constructor_deps: Option<&[R3DependencyMetadata<'a>]>,
namespace_registry: &mut NamespaceRegistry<'a>,
import_map: &ImportMap<'a>,
Expand Down Expand Up @@ -537,7 +540,7 @@ pub fn build_ctor_params_metadata_in<'a>(
/// only Angular's, like ngtsc.
pub fn build_prop_decorators_metadata<'a>(
allocator: &'a Allocator,
class: &Class<'a>,
class: &'a Class<'a>,
source_text: Option<&'a str>,
namespace_registry: &mut NamespaceRegistry<'a>,
) -> Option<OutputExpression<'a>> {
Expand All @@ -554,7 +557,7 @@ pub fn build_prop_decorators_metadata<'a>(
/// same way (`outputFromObservable` from `@angular/core/rxjs-interop`).
pub fn build_prop_decorators_metadata_in<'a>(
allocator: &'a Allocator,
class: &Class<'a>,
class: &'a Class<'a>,
source_text: Option<&'a str>,
namespace_registry: &mut NamespaceRegistry<'a>,
consts: Option<&StringConsts<'a>>,
Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub fn extract_component_metadata<'a>(
match key_name.as_str() {
"selector" => {
metadata.selector =
crate::directive::extract_string_value(allocator, &prop.value, consts)
crate::directive::evaluate_string_value(allocator, &prop.value, consts)
// ngtsc maps `selector: ''` to the default selector
// ('ng-component' for components), same as a missing
// selector. See annotations/directive/src/shared.ts
Expand Down Expand Up @@ -195,7 +195,7 @@ pub fn extract_component_metadata<'a>(
"exportAs" => {
// exportAs can be comma-separated: "foo, bar"
if let Some(export_as) =
crate::directive::extract_string_value(allocator, &prop.value, consts)
crate::directive::evaluate_string_value(allocator, &prop.value, consts)
{
for part in export_as.as_str().split(',') {
let trimmed = part.trim();
Expand Down
22 changes: 11 additions & 11 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ fn find_last_import_end(program_body: &[Statement<'_>]) -> Option<usize> {
#[allow(clippy::too_many_arguments)]
fn build_set_class_metadata_decls<'a>(
allocator: &'a Allocator,
class: &oxc_ast::ast::Class<'a>,
class: &'a oxc_ast::ast::Class<'a>,
class_name: &str,
options: &TransformOptions,
source: &'a str,
Expand Down Expand Up @@ -1994,7 +1994,7 @@ fn build_ctor_parameters_text(params: &[JitCtorParam]) -> Option<String> {
fn build_jit_decorator_text<'a>(
allocator: &'a Allocator,
source: &str,
decorator: &oxc_ast::ast::Decorator<'a>,
decorator: &'a oxc_ast::ast::Decorator<'a>,
decorator_kind: AngularDecoratorKind,
resource_counter: &mut u32,
resource_imports: &mut std::vec::Vec<(String, String)>, // (import_name, specifier)
Expand Down Expand Up @@ -2035,11 +2035,11 @@ fn build_jit_decorator_text<'a>(

match key_name {
Some("templateUrl") => {
// Resolve the URL through the same const/template-literal
// folder used by AOT extraction so JIT and AOT accept the
// same metadata shapes.
// Resolve the URL the way AOT extraction reads it (the
// const folder, then the partial evaluator) so JIT and AOT
// accept the same metadata shapes.
if let Some(url) =
crate::directive::extract_string_value(allocator, &prop.value, consts)
crate::directive::evaluate_string_value(allocator, &prop.value, consts)
{
let import_name = format!("__NG_CLI_RESOURCE__{}", *resource_counter);
let specifier = format!("angular:jit:template:file;{}", url.as_str());
Expand All @@ -2056,7 +2056,7 @@ fn build_jit_decorator_text<'a>(
Some("styleUrl") => {
// Single style URL
if let Some(url) =
crate::directive::extract_string_value(allocator, &prop.value, consts)
crate::directive::evaluate_string_value(allocator, &prop.value, consts)
{
let import_name = format!("__NG_CLI_RESOURCE__{}", *resource_counter);
let specifier = format!("angular:jit:style:file;{}", url.as_str());
Expand All @@ -2080,9 +2080,9 @@ fn build_jit_decorator_text<'a>(
// through `as_expression()` and let the resolver
// decide which shapes fold.
let Some(elem_expr) = elem.as_expression() else { continue };
if let Some(url) =
crate::directive::extract_string_value(allocator, elem_expr, consts)
{
if let Some(url) = crate::directive::evaluate_string_value(
allocator, elem_expr, consts,
) {
let import_name =
format!("__NG_CLI_RESOURCE__{}", *resource_counter);
let specifier = format!("angular:jit:style:file;{}", url.as_str());
Expand Down Expand Up @@ -2547,7 +2547,7 @@ fn synthetic_default_name(taken: &mut rustc_hash::FxHashSet<String>) -> String {
fn collect_jit_class_info<'a>(
allocator: &'a Allocator,
source: &str,
class: &oxc_ast::ast::Class<'a>,
class: &'a oxc_ast::ast::Class<'a>,
class_name: String,
stmt_start: u32,
is_exported: bool,
Expand Down
45 changes: 7 additions & 38 deletions crates/oxc_angular_compiler/src/directive/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ use oxc_ast::ast::{
use oxc_diagnostics::OxcDiagnostic;
use oxc_span::{GetSpan, Span};
use oxc_str::Ident;
use oxc_syntax::number::ToJsString;
use oxc_syntax::operator::UnaryOperator;

use super::evaluator::{Evaluator, FileScope, Prop, RefKind, Value, transform_error};
use super::metadata::{
Expand Down Expand Up @@ -145,7 +143,8 @@ pub fn extract_directive_metadata<'a>(

match key_name.as_str() {
"selector" => {
if let Some(selector) = extract_string_value(allocator, &prop.value, consts)
if let Some(selector) =
evaluate_string_value(allocator, &prop.value, consts)
{
builder = builder.selector(selector);
}
Expand All @@ -157,7 +156,7 @@ pub fn extract_directive_metadata<'a>(
}
"exportAs" => {
if let Some(export_as) =
extract_string_value(allocator, &prop.value, consts)
evaluate_string_value(allocator, &prop.value, consts)
{
// exportAs can be comma-separated: "foo, bar"
for part in export_as.as_str().split(',') {
Expand Down Expand Up @@ -492,8 +491,6 @@ fn has_ng_on_changes_method(class: &Class<'_>) -> bool {
#[derive(Default)]
pub struct StringConsts<'a> {
strings: HashMap<&'a str, Ident<'a>>,
/// `const NAME = 123`: numbers a template literal's `${...}` can interpolate.
numbers: HashMap<&'a str, f64>,
program: Option<&'a Program<'a>>,
/// Built on first use: most files have no decorator metadata to evaluate.
scope: std::cell::OnceCell<FileScope<'a>>,
Expand Down Expand Up @@ -539,7 +536,6 @@ impl<'a> StringConsts<'a> {
pub(crate) fn declarations_of(program: &'a Program<'a>) -> Self {
Self {
strings: HashMap::default(),
numbers: HashMap::default(),
program: Some(program),
scope: std::cell::OnceCell::new(),
#[cfg(feature = "cross_file_elision")]
Expand Down Expand Up @@ -599,10 +595,6 @@ pub fn collect_string_consts<'a>(

let mut map = StringConsts {
strings: HashMap::default(),
numbers: pending
.iter()
.filter_map(|(name, init)| Some((*name, numeric_literal_value(init)?)))
.collect(),
program: Some(program),
scope: std::cell::OnceCell::new(),
#[cfg(feature = "cross_file_elision")]
Expand Down Expand Up @@ -641,23 +633,9 @@ fn literal_string_from_expression<'a>(expr: &Expression<'a>) -> Option<Ident<'a>
}
}

/// The value of a number literal (`150`, `-1.5`).
fn numeric_literal_value(expr: &Expression<'_>) -> Option<f64> {
match expr {
Expression::NumericLiteral(lit) => Some(lit.value),
Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
match &unary.argument {
Expression::NumericLiteral(lit) => Some(-lit.value),
_ => None,
}
}
_ => None,
}
}

/// Fold a template literal whose `${...}` interpolations reference known
/// string consts, number consts or number literals. Returns `None` if any
/// interpolation can't be statically resolved to a string — matching Angular's all-or-nothing partial evaluator
/// string consts. Returns `None` if any interpolation can't be statically
/// resolved to a string — matching Angular's all-or-nothing partial evaluator
/// for static metadata fields.
pub(crate) fn resolve_template_literal<'a>(
allocator: &'a Allocator,
Expand All @@ -674,17 +652,8 @@ pub(crate) fn resolve_template_literal<'a>(
let cooked = quasi.value.cooked?.as_str()?;
buf.push_str(cooked);
if let Some(expr) = tpl.expressions.get(i) {
if let Some(resolved) = extract_string_value(allocator, expr, consts) {
buf.push_str(resolved.as_str());
} else {
// `${150}` or `${WIDTH}` with `const WIDTH = 150`: the number as
// JavaScript converts it to a string
let number = match expr {
Expression::Identifier(id) => *consts.numbers.get(id.name.as_str())?,
_ => numeric_literal_value(expr)?,
};
buf.push_str(&number.to_js_string());
}
let resolved = extract_string_value(allocator, expr, consts)?;
buf.push_str(resolved.as_str());
Comment thread
Brooooooklyn marked this conversation as resolved.
}
}
Some(Ident::from(allocator.alloc_str(&buf)))
Expand Down
3 changes: 1 addition & 2 deletions crates/oxc_angular_compiler/src/directive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ pub(crate) use decorator::evaluate_host_directives;
pub(crate) use decorator::find_directive_decorator;
pub(crate) use decorator::{
HostKey, angular_decorator_config, evaluate_host_metadata, evaluate_string_value,
evaluate_style_urls, evaluate_styles, extract_string_value, resolve_template_literal,
styles_error,
evaluate_style_urls, evaluate_styles, extract_string_value, styles_error,
};
pub(crate) use decorator::{
INPUT_API, InitializerApi, MODEL_API, OUTPUT_API, OUTPUT_FROM_OBSERVABLE_API, QUERY_APIS,
Expand Down
39 changes: 39 additions & 0 deletions crates/oxc_angular_compiler/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12808,6 +12808,45 @@ export class AppComponent {}
);
}

/// JIT mode: a resource URL interpolating a number (`${1}`, a number const or
/// an expression of one) folds through the partial evaluator, like in AOT.
#[test]
fn jit_resource_urls_with_number_interpolation() {
let allocator = Allocator::default();
let source = r#"
import { Component } from '@angular/core';

const V = 2;

@Component({
selector: 'app-root',
templateUrl: `./v${1}.html`,
styleUrl: `./v${V}.css`,
standalone: true,
})
export class AppComponent {}

@Component({
selector: 'app-other',
template: '<h1>x</h1>',
styleUrls: [`./w${V * 2}.css`],
})
export class OtherComponent {}
"#;
let options = ComponentTransformOptions { jit: true, ..Default::default() };
let result =
transform_angular_file(&allocator, "app.component.ts", source, Some(&options), None);
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);

for specifier in [
"angular:jit:template:file;./v1.html",
"angular:jit:style:file;./v2.css",
"angular:jit:style:file;./w4.css",
] {
assert!(result.code.contains(specifier), "Expected `{specifier}`.\nCode:\n{}", result.code);
}
}

/// JIT mode: `templateUrl` may be a bare identifier referencing a same-file
/// string `const`. Must also fold.
#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,23 @@ export class E3Cmp {}
"Got:\n{code}"
);
}

#[test]
fn evaluator_only_template_literals_keep_their_fields() {
// `${W * 2}` (with `W` an alias of another const) folds only through the
// partial evaluator. ngtsc keeps the fields as written in setClassMetadata
// and uses the folded selector.
let code = compile(
r#"
import { Component } from '@angular/core';
const BASE = 150;
const W = BASE;
@Component({ selector: `x-${W * 2}`, exportAs: `e${+W}`, template: `<i style="width: ${W * 2}px"></i>` })
export class C {}
"#,
);
assert!(code.contains(r#"selectors:[["x-300"]]"#), "Got:\n{code}");
assert!(code.contains(r#"exportAs:["e150"]"#), "Got:\n{code}");
assert!(code.contains("selector:`x-${"), "Got:\n{code}");
assert!(code.contains("template:`<istyle=\"width:${"), "Got:\n{code}");
}
Loading