diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index d24d5741f..e6a2367cc 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -20,7 +20,7 @@ use super::transform::ImportMap; use crate::directive::{ HostKey, HostValue, MemberHostBindings, StringConsts, evaluate_host_directives, extract_input_metadata_in, extract_output_metadata_in, merge_by_class_property, - merge_host_bindings, parse_decorator_io, + merge_host_bindings, parse_decorator_io, try_unwrap_forward_ref, }; use crate::output::oxc_converter::convert_oxc_expression; use crate::util::is_metadata_property; @@ -180,6 +180,8 @@ pub fn extract_component_metadata<'a>( // For standalone components, we need: // 1. The identifier list for local analysis metadata.imports = extract_identifier_array(allocator, &prop.value); + metadata.import_entry_classes = + extract_import_entry_classes(allocator, &prop.value, consts); // 2. The raw expression to pass to ɵɵgetComponentDepsFactory in RuntimeResolved mode metadata.raw_imports = convert_oxc_expression(allocator, &prop.value, source_text); @@ -489,6 +491,28 @@ fn extract_identifier_array<'a>( result } +/// The class each element of an array literal names, by position: `X`, or +/// `forwardRef(() => X)` with `@angular/core`'s `forwardRef`. +fn extract_import_entry_classes<'a>( + allocator: &'a Allocator, + expr: &'a Expression<'a>, + consts: &StringConsts<'_>, +) -> Vec<'a, Option>> { + let mut result = Vec::new_in(&allocator); + if let Expression::ArrayExpression(arr) = expr { + for element in &arr.elements { + let class = element.as_expression().and_then(|element| { + match try_unwrap_forward_ref(element, Some(consts)).unwrap_or(element) { + Expression::Identifier(id) => Some(id.name), + _ => None, + } + }); + result.push(class); + } + } + result +} + /// Extract ViewEncapsulation from an expression. fn extract_encapsulation(expr: &Expression<'_>) -> ViewEncapsulation { // Look for patterns like: diff --git a/crates/oxc_angular_compiler/src/component/metadata.rs b/crates/oxc_angular_compiler/src/component/metadata.rs index d4c3a9742..1e1d60453 100644 --- a/crates/oxc_angular_compiler/src/component/metadata.rs +++ b/crates/oxc_angular_compiler/src/component/metadata.rs @@ -526,6 +526,13 @@ pub struct ComponentMetadata<'a> { /// Component imports (for standalone components). pub imports: Vec<'a, Ident<'a>>, + /// The class `@defer` may lazy-load for each element of the `imports` + /// array literal, by position: `X`, or `forwardRef(() => X)` with + /// `@angular/core`'s `forwardRef` (which [`Self::imports`] leaves out), + /// as ngtsc resolves them through its `forwardRefResolver`; `None` for + /// anything else, such as `X as any`, which ngtsc keeps eager. + pub import_entry_classes: Vec<'a, Option>>, + /// Exported names for template references. /// /// In Angular, `exportAs` can be a comma-separated string (e.g., "foo, bar"), @@ -954,6 +961,7 @@ impl<'a> ComponentMetadata<'a> { change_detection: None, host: None, imports: Vec::new_in(&allocator), + import_entry_classes: Vec::new_in(&allocator), export_as: Vec::new_in(&allocator), preserve_whitespaces: None, constructor_deps: None, diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 9b35f4679..097f5254f 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -5636,15 +5636,22 @@ fn compile_component_full<'a>( && let Some(import_elision) = import_elision && !options.hmr && let Some(OutputExpression::LiteralArray(raw_imports)) = &metadata.raw_imports - && raw_imports.entries.iter().all(|entry| matches!(entry, OutputExpression::ReadVar(_))) + && raw_imports.entries.len() == metadata.import_entry_classes.len() + && raw_imports + .entries + .iter() + .zip(&metadata.import_entry_classes) + .all(|(entry, class)| class.is_some() || matches!(entry, OutputExpression::ReadVar(_))) && super::defer_resolver::template_has_defer_block(&r3_result.nodes) { let candidates: Vec<_> = metadata - .imports + .import_entry_classes .iter() + .flatten() + .copied() .filter_map(|name| { let selector = deferrable_selector(name.as_str())?; - Some((*name, crate::pipeline::selector::CssSelector::parse(&selector))) + Some((name, crate::pipeline::selector::CssSelector::parse(&selector))) }) .collect(); let deferred = @@ -5668,8 +5675,9 @@ fn compile_component_full<'a>( let is_deferred = |name: &str| deferred.iter().any(|d| d.as_str() == name); metadata.imports.retain(|name| !is_deferred(name.as_str())); if let Some(OutputExpression::LiteralArray(raw_imports)) = &mut metadata.raw_imports { - raw_imports.entries.retain(|entry| { - !matches!(entry, OutputExpression::ReadVar(var) if is_deferred(var.name.as_str())) + let mut classes = metadata.import_entry_classes.iter(); + raw_imports.entries.retain(|_| { + !classes.next().copied().flatten().is_some_and(|name| is_deferred(&name)) }); } implicitly_deferred_imports = deferred.iter().map(ToString::to_string).collect(); diff --git a/crates/oxc_angular_compiler/src/directive/mod.rs b/crates/oxc_angular_compiler/src/directive/mod.rs index fc854fa3b..f62e44b67 100644 --- a/crates/oxc_angular_compiler/src/directive/mod.rs +++ b/crates/oxc_angular_compiler/src/directive/mod.rs @@ -55,7 +55,7 @@ pub(crate) use property_decorators::{ angular_class_decorator, angular_core_identifier, angular_member_decorator, angular_param_decorator, decorator_written_name, is_angular_core_decorator, parse_decorator_queries, try_parse_signal_input, try_parse_signal_model, - try_parse_signal_output, unwrap_initializer_api_expr, + try_parse_signal_output, try_unwrap_forward_ref, unwrap_initializer_api_expr, }; pub use property_decorators::{ extract_class_queries, extract_content_queries, extract_host_bindings, extract_host_listeners, diff --git a/crates/oxc_angular_compiler/tests/defer_implicit_imports_test.rs b/crates/oxc_angular_compiler/tests/defer_implicit_imports_test.rs index e66f92da6..6f141ccd2 100644 --- a/crates/oxc_angular_compiler/tests/defer_implicit_imports_test.rs +++ b/crates/oxc_angular_compiler/tests/defer_implicit_imports_test.rs @@ -223,6 +223,98 @@ export class Parent {} assert_lazy(&result.code, "./lazy", "default"); } +/// Port of ngtsc's `defer_spec.ts`: "should lazy-load dependency referenced +/// with a fowrardRef". +#[test] +fn forward_ref_import_used_only_inside_defer_is_lazy_loaded() { + for entry in [ + "forwardRef(() => LazyCmp)", + "forwardRef(() => { return LazyCmp; })", + "forwardRef(function () { return LazyCmp; })", + ] { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + let result = transform( + &dir, + &format!( + r#" +import {{ Component, forwardRef }} from '@angular/core'; +import {{ LazyCmp }} from './lazy'; + +@Component({{ + selector: 'app-parent', + imports: [{entry}], + template: '@defer {{ }}', +}}) +export class Parent {{}} +"# + ), + ); + let code = &result.code; + assert_lazy(code, "./lazy", "LazyCmp"); + assert!(compact(code).contains("ɵɵdefer(1,0,()=>[import(\"./lazy\")"), "{code}"); + assert!(compact(code).contains("ɵɵgetComponentDepsFactory(Parent,[])"), "{code}"); + assert!(!code.contains("import { LazyCmp }"), "{code}"); + } +} + +#[test] +fn forward_ref_entry_next_to_a_cast_entry_is_lazy_loaded_alone() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + create_test_file( + dir.path(), + "app/eager.ts", + r#" +import { Component } from '@angular/core'; + +@Component({ selector: 'app-eager', template: 'eager' }) +export class EagerCmp {} +"#, + ); + let result = transform( + &dir, + r#" +import { Component, forwardRef } from '@angular/core'; +import { LazyCmp } from './lazy'; +import { EagerCmp } from './eager'; + +@Component({ + selector: 'app-parent', + imports: [forwardRef(() => LazyCmp), EagerCmp as any], + template: '@defer { }', +}) +export class Parent {} +"#, + ); + let code = &result.code; + assert_lazy(code, "./lazy", "LazyCmp"); + assert!(compact(code).contains("ɵɵgetComponentDepsFactory(Parent,[EagerCmp])"), "{code}"); + assert!(code.contains("import { EagerCmp }"), "{code}"); +} + +#[test] +fn forward_ref_not_from_angular_core_stays_eager() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + let result = transform( + &dir, + r#" +import { Component } from '@angular/core'; +import { forwardRef } from './my-forward-ref'; +import { LazyCmp } from './lazy'; + +@Component({ + selector: 'app-parent', + imports: [forwardRef(() => LazyCmp)], + template: '@defer { }', +}) +export class Parent {} +"#, + ); + assert_eager(&result.code, "./lazy"); +} + #[test] fn import_referenced_elsewhere_in_the_file_stays_eager() { let dir = TempDir::new().unwrap();