From 7da98684b5e2ce475bc82be78545c58ee6e83288 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 28 Oct 2026 15:28:57 +0800 Subject: [PATCH] fix(defer): give each @defer block its own deps fn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With cross-file analysis (resolveImportedValues), oxc built one shared resolver per component and passed it to every ɵɵdefer, so the first block to trigger also fetched the chunks of the other blocks. Blocks that used only eager or same-file deps got no deps fn at all. Port ngtsc's full-compilation PerBlock mode: each block lists the `imports` entries its main content matches (nested blocks own their content, their @placeholder/@loading/@error count for the outer one), as a dynamic import() when the dep is lazy and a plain reference when it is eager. The fn is pooled as `_Defer__DepsFn`, where only the first `_Template` of the view's fn name is dropped, and the pool takes the name as is, without using up a `_cN` index. setClassMetadataAsync lists the lazy deps block by block. Same-file standalone classes are matched by their selector too. An entry used only inside @defer blocks leaves `dependencies`, even when it can't be lazy-loaded: the blocks' fns provide it. When no entry is left, the component gets no `dependencies` key. Unknown selectors (node_modules, pipes) stay out of the lists, as before; `dependencies` still provides them. An explicit `deferredImports`, HMR, JIT and the no-analysis path keep the shared resolver. Ported from @angular/compiler-cli 22.2.3 ComponentDecoratorHandler (resolveDeferBlocks, resolveAllDeferredDependencies, compileDeferBlocks, handleDependencyCycles) and @angular/compiler 22.2.2 compileDeferResolverFunction, resolveDeferDepsFns and ConstantPool.getSharedFunctionReference. Co-Authored-By: Claude Opus 5.5 --- .../src/class_metadata/compiler.rs | 142 +++--- .../src/class_metadata/mod.rs | 3 +- .../src/component/cross_file_elision.rs | 37 ++ .../src/component/defer_resolver.rs | 244 +++++++--- .../src/component/transform.rs | 161 +++++-- .../src/pipeline/constant_pool.rs | 44 +- .../pipeline/phases/resolve_defer_deps_fns.rs | 6 +- .../tests/defer_implicit_imports_test.rs | 416 +++++++++++++++++- 8 files changed, 857 insertions(+), 196 deletions(-) diff --git a/crates/oxc_angular_compiler/src/class_metadata/compiler.rs b/crates/oxc_angular_compiler/src/class_metadata/compiler.rs index b2f2cac53..a75c618d4 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/compiler.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/compiler.rs @@ -119,73 +119,7 @@ pub fn compile_component_metadata_async_resolver<'a>( let mut dynamic_imports = Vec::new_in(&allocator); for dep in dependencies { - // Create: (m) => m.CmpA (or m.default for default imports) - let mut inner_params = Vec::new_in(&allocator); - inner_params.push(FnParam { name: Ident::from("m") }); - - let prop_name = - if dep.is_default_import { Ident::from("default") } else { dep.export_name.clone() }; - - let inner_body = OutputExpression::ReadProp(Box::new_in( - ReadPropExpr { - receiver: Box::new_in( - OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("m"), source_span: None }, - &allocator, - )), - &allocator, - ), - name: prop_name, - optional: false, - source_span: None, - }, - &allocator, - )); - - let inner_fn = OutputExpression::ArrowFunction(Box::new_in( - ArrowFunctionExpr { - params: inner_params, - body: ArrowFunctionBody::Expression(Box::new_in(inner_body, &allocator)), - source_span: None, - }, - &allocator, - )); - - // Create: import('./cmp-a').then((m) => m.CmpA) - let dynamic_import = OutputExpression::DynamicImport(Box::new_in( - DynamicImportExpr { - url: DynamicImportUrl::String(dep.import_path.clone()), - url_comment: None, - source_span: None, - }, - &allocator, - )); - - let then_prop = OutputExpression::ReadProp(Box::new_in( - ReadPropExpr { - receiver: Box::new_in(dynamic_import, &allocator), - name: Ident::from("then"), - optional: false, - source_span: None, - }, - &allocator, - )); - - let mut then_args = Vec::new_in(&allocator); - then_args.push(inner_fn); - - let then_call = OutputExpression::InvokeFunction(Box::new_in( - InvokeFunctionExpr { - fn_expr: Box::new_in(then_prop, &allocator), - args: then_args, - pure: false, - optional: false, - source_span: None, - }, - &allocator, - )); - - dynamic_imports.push(then_call); + dynamic_imports.push(compile_deferred_dependency_import(allocator, dep)); } // Create: () => [...] @@ -205,6 +139,80 @@ pub fn compile_component_metadata_async_resolver<'a>( )) } +/// One entry of a dependency resolver function: +/// `import('./cmp-a').then((m) => m.CmpA)`, or `m.default` for a default +/// import (the `isDeferrable` branch of Angular's +/// `compileDeferResolverFunction`). +pub fn compile_deferred_dependency_import<'a>( + allocator: &'a Allocator, + dep: &R3DeferPerComponentDependency<'a>, +) -> OutputExpression<'a> { + // Create: (m) => m.CmpA (or m.default for default imports) + let mut inner_params = Vec::new_in(&allocator); + inner_params.push(FnParam { name: Ident::from("m") }); + + let prop_name = if dep.is_default_import { Ident::from("default") } else { dep.export_name }; + + let inner_body = OutputExpression::ReadProp(Box::new_in( + ReadPropExpr { + receiver: Box::new_in( + OutputExpression::ReadVar(Box::new_in( + ReadVarExpr { name: Ident::from("m"), source_span: None }, + &allocator, + )), + &allocator, + ), + name: prop_name, + optional: false, + source_span: None, + }, + &allocator, + )); + + let inner_fn = OutputExpression::ArrowFunction(Box::new_in( + ArrowFunctionExpr { + params: inner_params, + body: ArrowFunctionBody::Expression(Box::new_in(inner_body, &allocator)), + source_span: None, + }, + &allocator, + )); + + // Create: import('./cmp-a').then((m) => m.CmpA) + let dynamic_import = OutputExpression::DynamicImport(Box::new_in( + DynamicImportExpr { + url: DynamicImportUrl::String(dep.import_path), + url_comment: None, + source_span: None, + }, + &allocator, + )); + + let then_prop = OutputExpression::ReadProp(Box::new_in( + ReadPropExpr { + receiver: Box::new_in(dynamic_import, &allocator), + name: Ident::from("then"), + optional: false, + source_span: None, + }, + &allocator, + )); + + let mut then_args = Vec::new_in(&allocator); + then_args.push(inner_fn); + + OutputExpression::InvokeFunction(Box::new_in( + InvokeFunctionExpr { + fn_expr: Box::new_in(then_prop, &allocator), + args: then_args, + pure: false, + optional: false, + source_span: None, + }, + &allocator, + )) +} + // ============================================================================ // Internal helpers // ============================================================================ diff --git a/crates/oxc_angular_compiler/src/class_metadata/mod.rs b/crates/oxc_angular_compiler/src/class_metadata/mod.rs index 0073b8426..98fe7cf50 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/mod.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/mod.rs @@ -21,6 +21,7 @@ pub use builders::{ pub(crate) use builders::{entity_path, param_type_reference, type_param_names_of}; pub use compiler::{ compile_class_metadata, compile_component_class_metadata, - compile_component_metadata_async_resolver, compile_opaque_async_class_metadata, + compile_component_metadata_async_resolver, compile_deferred_dependency_import, + compile_opaque_async_class_metadata, }; pub use metadata::{R3ClassMetadata, R3DeferPerComponentDependency}; diff --git a/crates/oxc_angular_compiler/src/component/cross_file_elision.rs b/crates/oxc_angular_compiler/src/component/cross_file_elision.rs index e4ae4d95f..d0d9f81a1 100644 --- a/crates/oxc_angular_compiler/src/component/cross_file_elision.rs +++ b/crates/oxc_angular_compiler/src/component/cross_file_elision.rs @@ -210,7 +210,44 @@ fn standalone_class_selector<'a>( } _ => None, })?; + decorated_class_selector(class, consts, implicit_standalone) +} + +/// The selector of the top-level class `local` declares in `program`, +/// exported or not, when it is a standalone `@Component` or `@Directive` whose +/// `selector` and `standalone` are literals: an `imports` entry that names a +/// class of the file being compiled. `consts` are the program's. +pub fn local_standalone_class_selector<'a>( + program: &'a Program<'a>, + consts: &StringConsts<'a>, + local: &str, + implicit_standalone: bool, +) -> Option { + let is_local = |class: &Class| class.id.as_ref().is_some_and(|id| id.name == local); + let class = program.body.iter().find_map(|stmt| match stmt { + Statement::ClassDeclaration(class) if is_local(class) => Some(&**class), + Statement::ExportDeclaration(decl) => match &decl.declaration { + Declaration::ClassDeclaration(class) if is_local(class) => Some(&**class), + _ => None, + }, + Statement::ExportDefaultDeclaration(decl) => match &decl.declaration { + ExportDefaultDeclarationKind::ClassDeclaration(class) if is_local(class) => { + Some(&**class) + } + _ => None, + }, + _ => None, + })?; + decorated_class_selector(class, consts, implicit_standalone) +} +/// The selector of `class` when it is a standalone `@Component` or +/// `@Directive` whose `selector` and `standalone` are literals. +fn decorated_class_selector<'a>( + class: &Class<'a>, + consts: &StringConsts<'a>, + implicit_standalone: bool, +) -> Option { let decorator = ["Component", "Directive"].iter().find_map(|name| { crate::directive::find_angular_class_decorator(&class.decorators, name, consts) })?; diff --git a/crates/oxc_angular_compiler/src/component/defer_resolver.rs b/crates/oxc_angular_compiler/src/component/defer_resolver.rs index b78871859..3801879ad 100644 --- a/crates/oxc_angular_compiler/src/component/defer_resolver.rs +++ b/crates/oxc_angular_compiler/src/component/defer_resolver.rs @@ -34,7 +34,9 @@ //! OXC does the same unless cross-file analysis can read the imported //! class's selector: then [`implicitly_deferred_imports`] picks the //! standalone components and directives the template matches only inside -//! `@defer` blocks, and they join `deferredImports` (#313). +//! `@defer` blocks, and they join `deferredImports` (#313). Each `@defer` +//! block then gets its own dependency function, as in ngc's full compilation +//! ([`defer_block_dependencies`], [`compile_defer_block_resolver`]). use oxc_allocator::Allocator; @@ -45,9 +47,12 @@ use crate::ast::r3::{ }; use crate::class_metadata::{ R3DeferPerComponentDependency, compile_component_metadata_async_resolver, + compile_deferred_dependency_import, }; use crate::i18n::serializer::is_i18n_attribute; -use crate::output::ast::OutputExpression; +use crate::output::ast::{ + ArrowFunctionBody, ArrowFunctionExpr, LiteralArrayExpr, OutputExpression, ReadVarExpr, +}; use crate::parser::html::split_ns_name; use crate::pipeline::selector::CssSelector; use oxc_str::Ident; @@ -211,60 +216,48 @@ pub(super) fn implicitly_deferred_imports<'a>( nodes: &[R3Node<'a>], candidates: &[(Ident<'a>, std::vec::Vec)], ) -> std::vec::Vec> { - struct Matcher<'c, 'a> { - candidates: &'c [(Ident<'a>, std::vec::Vec)], - in_defer: bool, - deferred: std::vec::Vec, - eager: std::vec::Vec, - selectorless: bool, + let mut matcher = CandidateMatcher::new(candidates); + visit_all(&mut matcher, nodes); + if matcher.selectorless { + return std::vec::Vec::new(); } + candidates + .iter() + .enumerate() + .filter(|(index, _)| matcher.deferred[*index] && !matcher.eager[*index]) + .map(|(_, (name, _))| *name) + .collect() +} - impl Matcher<'_, '_> { - fn record(&mut self, element: &CssSelector) { - for (index, (_, selectors)) in self.candidates.iter().enumerate() { - if selectors.iter().any(|selector| selector.matches(element)) { - if self.in_defer { - self.deferred[index] = true; - } else { - self.eager[index] = true; - } - } - } - } +/// The `candidates` each `@defer` block uses, with the block's span, in +/// [`defer_block_spans`] order. ngc binds the main content of each block on +/// its own (`resolveDeferBlocks` in its component `handler.ts`) and keeps +/// what that content uses eagerly: a candidate matched only inside a nested +/// `@defer` belongs to the nested block, while the nested block's +/// `@placeholder`, `@loading` and `@error` belong to the outer one. Each list +/// keeps the order of `candidates`. `None` for a selectorless template. +pub(super) fn defer_block_dependencies<'a>( + nodes: &[R3Node<'a>], + candidates: &[(Ident<'a>, std::vec::Vec)], +) -> Option>)>> { + struct BlockCollector<'c, 'a> { + candidates: &'c [(Ident<'a>, std::vec::Vec)], + blocks: std::vec::Vec<(oxc_span::Span, std::vec::Vec>)>, } - impl<'a> R3Visitor<'a> for Matcher<'_, 'a> { - fn visit_element(&mut self, element: &R3Element<'a>) { - if !element.directives.is_empty() { - self.selectorless = true; - } - self.record(&element_selector( - &element.name, - &element.attributes, - &element.inputs, - &element.outputs, - )); - self.visit_element_children(element); - } - - fn visit_template(&mut self, template: &R3Template<'a>) { - if !template.directives.is_empty() { - self.selectorless = true; - } - self.record(&template_selector(template)); - self.visit_template_children(template); - } - + impl<'a> R3Visitor<'a> for BlockCollector<'_, 'a> { fn visit_deferred_block(&mut self, block: &R3DeferredBlock<'a>) { - let outer = self.in_defer; - self.in_defer = true; - for child in &block.children { - child.visit(self); + let mut matcher = CandidateMatcher::new(self.candidates); + visit_all(&mut matcher, &block.children); + let mut dependencies = std::vec::Vec::new(); + for (index, (name, _)) in self.candidates.iter().enumerate() { + if matcher.eager[index] && !dependencies.contains(name) { + dependencies.push(*name); + } } - // `@placeholder`, `@loading` and `@error` render before the chunk - // loads, so they are eager unless an outer `@defer` holds them - // (`visitDeferredBlock` in t2_binder's `DirectiveBinder`). - self.in_defer = outer; + self.blocks.push((block.source_span, dependencies)); + + visit_all(self, &block.children); if let Some(placeholder) = &block.placeholder { self.visit_deferred_block_placeholder(placeholder); } @@ -275,33 +268,148 @@ pub(super) fn implicitly_deferred_imports<'a>( self.visit_deferred_block_error(error); } } + } - fn visit_component(&mut self, _component: &R3Component<'a>) { + let mut matcher = CandidateMatcher::new(candidates); + visit_all(&mut matcher, nodes); + if matcher.selectorless { + return None; + } + let mut collector = BlockCollector { candidates, blocks: std::vec::Vec::new() }; + visit_all(&mut collector, nodes); + Some(collector.blocks) +} + +/// A `@defer` block's dependency function, for `DeferBlockDepsEmitMode` +/// `PerBlock` (`compileDeferResolverFunction` in Angular's +/// `render3/view/compiler.ts`): a dynamic `import()` for each of the +/// `dependencies` that `deferred` lists, and a plain reference to the others. +/// `None` when the block has no dependencies (`compileDeferBlocks` in ngc's +/// component `handler.ts`). +pub(super) fn compile_defer_block_resolver<'a>( + allocator: &'a Allocator, + dependencies: &[Ident<'a>], + deferred: &[Ident<'a>], + import_map: &ImportMap<'a>, +) -> Option> { + if dependencies.is_empty() { + return None; + } + let mut entries = oxc_allocator::Vec::new_in(&allocator); + for name in dependencies { + let deferrable = if deferred.contains(name) { + build_defer_per_component_deps(allocator, std::slice::from_ref(name), import_map) + .first() + .map(|dep| compile_deferred_dependency_import(allocator, dep)) + } else { + None + }; + entries.push(deferrable.unwrap_or_else(|| { + OutputExpression::ReadVar(oxc_allocator::Box::new_in( + ReadVarExpr { name: *name, source_span: None }, + &allocator, + )) + })); + } + let array = OutputExpression::LiteralArray(oxc_allocator::Box::new_in( + LiteralArrayExpr { entries, source_span: None }, + &allocator, + )); + Some(OutputExpression::ArrowFunction(oxc_allocator::Box::new_in( + ArrowFunctionExpr { + params: oxc_allocator::Vec::new_in(&allocator), + body: ArrowFunctionBody::Expression(oxc_allocator::Box::new_in(array, &allocator)), + source_span: None, + }, + &allocator, + ))) +} + +/// Which `candidates` a template matches inside the main content of `@defer` +/// blocks and which it matches anywhere else, as t2_binder's +/// `DirectiveBinder` tells eagerly used directives apart. +struct CandidateMatcher<'c, 'a> { + candidates: &'c [(Ident<'a>, std::vec::Vec)], + in_defer: bool, + deferred: std::vec::Vec, + eager: std::vec::Vec, + selectorless: bool, +} + +impl<'c, 'a> CandidateMatcher<'c, 'a> { + fn new(candidates: &'c [(Ident<'a>, std::vec::Vec)]) -> Self { + Self { + candidates, + in_defer: false, + deferred: vec![false; candidates.len()], + eager: vec![false; candidates.len()], + selectorless: false, + } + } + + fn record(&mut self, element: &CssSelector) { + for (index, (_, selectors)) in self.candidates.iter().enumerate() { + if selectors.iter().any(|selector| selector.matches(element)) { + if self.in_defer { + self.deferred[index] = true; + } else { + self.eager[index] = true; + } + } + } + } +} + +impl<'a> R3Visitor<'a> for CandidateMatcher<'_, 'a> { + fn visit_element(&mut self, element: &R3Element<'a>) { + if !element.directives.is_empty() { self.selectorless = true; } + self.record(&element_selector( + &element.name, + &element.attributes, + &element.inputs, + &element.outputs, + )); + self.visit_element_children(element); + } - fn visit_directive(&mut self, _directive: &R3Directive<'a>) { + fn visit_template(&mut self, template: &R3Template<'a>) { + if !template.directives.is_empty() { self.selectorless = true; } + self.record(&template_selector(template)); + self.visit_template_children(template); } - let mut matcher = Matcher { - candidates, - in_defer: false, - deferred: vec![false; candidates.len()], - eager: vec![false; candidates.len()], - selectorless: false, - }; - visit_all(&mut matcher, nodes); - if matcher.selectorless { - return std::vec::Vec::new(); + fn visit_deferred_block(&mut self, block: &R3DeferredBlock<'a>) { + let outer = self.in_defer; + self.in_defer = true; + for child in &block.children { + child.visit(self); + } + // `@placeholder`, `@loading` and `@error` render before the chunk + // loads, so they are eager unless an outer `@defer` holds them + // (`visitDeferredBlock` in t2_binder's `DirectiveBinder`). + self.in_defer = outer; + if let Some(placeholder) = &block.placeholder { + self.visit_deferred_block_placeholder(placeholder); + } + if let Some(loading) = &block.loading { + self.visit_deferred_block_loading(loading); + } + if let Some(error) = &block.error { + self.visit_deferred_block_error(error); + } + } + + fn visit_component(&mut self, _component: &R3Component<'a>) { + self.selectorless = true; + } + + fn visit_directive(&mut self, _directive: &R3Directive<'a>) { + self.selectorless = true; } - candidates - .iter() - .enumerate() - .filter(|(index, _)| matcher.deferred[*index] && !matcher.eager[*index]) - .map(|(_, (name, _))| *name) - .collect() } /// Whether any element or template anywhere in the template (`@defer` blocks diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index ea6984a72..75d21950d 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -3562,43 +3562,52 @@ pub fn transform_angular_file( content_queries.iter().map(|q| q.property_name.to_string()).collect(); // The selector of the standalone component or directive an - // `imports` entry refers to, when `@defer` may lazy-load it - // (#313): its import is referenced nowhere else in this file - // or in the compiled output, so the static import can go, and - // cross-file analysis can read the selector. Watched files - // are only reported with `resolve_imported_values`, so it is - // required here too. + // `imports` entry refers to, when cross-file analysis can + // read it or the class is declared in this file, and whether + // `@defer` may lazy-load it (#313): its import is referenced + // nowhere else in this file or in the compiled output, so the + // static import can go. Watched files are only reported with + // `resolve_imported_values`, so it is required here too. #[cfg(feature = "cross_file_elision")] - let deferrable_selector = cross_file_analyzer + let import_selector = cross_file_analyzer .as_ref() .filter(|_| options.resolve_imported_values) .map(|analyzer| { let import_map = &import_map; let import_elision = &import_elision_mut; - move |local: &str| -> Option { - if import_elision.value_reference_count(local)? != 1 - || import_elision.is_kept_by_output(local) - { - return None; - } - let info = import_map.get(&Ident::from(local))?; + let program = &parser_ret.program; + let string_consts = &string_consts; + move |local: &str| -> Option<(String, bool)> { + let Some(info) = import_map.get(&Ident::from(local)) else { + let selector = + super::cross_file_elision::local_standalone_class_selector( + program, + string_consts, + local, + implicit_standalone, + )?; + return Some((selector, false)); + }; if !info.is_named_import || info.is_type_only { return None; } let export_name = info.imported_name.as_deref().unwrap_or(local); - analyzer.standalone_directive_selector( + let selector = analyzer.standalone_directive_selector( info.source_module.as_str(), export_name, std::path::Path::new(path), implicit_standalone, - ) + )?; + let deferrable = import_elision.value_reference_count(local) + == Some(1) + && !import_elision.is_kept_by_output(local); + Some((selector, deferrable)) } }); #[cfg(feature = "cross_file_elision")] - let deferrable_selector = - deferrable_selector.as_ref().map(|f| f as DeferrableSelector<'_>); + let import_selector = import_selector.as_ref().map(|f| f as ImportSelector<'_>); #[cfg(not(feature = "cross_file_elision"))] - let deferrable_selector = None; + let import_selector = None; // 4. Compile the template and generate ɵcmp/ɵfac // Pass the shared pool index to ensure unique constant names @@ -3616,7 +3625,7 @@ pub fn transform_angular_file( &mut file_namespace_registry, &import_map, resolved_resources, - deferrable_selector, + import_selector, Some(&import_elision_mut), false, ) { @@ -5442,8 +5451,8 @@ fn html_parse_error_diagnostic( } /// The selector of the standalone component or directive an `imports` -/// entry refers to, by local name, when `@defer` may lazy-load it. -type DeferrableSelector<'f> = &'f dyn Fn(&str) -> Option; +/// entry refers to, by local name, and whether `@defer` may lazy-load it. +type ImportSelector<'f> = &'f dyn Fn(&str) -> Option<(String, bool)>; fn compile_component_full<'a>( allocator: &'a Allocator, @@ -5458,7 +5467,7 @@ fn compile_component_full<'a>( namespace_registry: &mut NamespaceRegistry<'a>, import_map: &ImportMap<'a>, resolved_resources: Option<&ResolvedResources>, - deferrable_selector: Option>, + import_selector: Option>, import_elision: Option<&ImportElisionAnalyzer<'_>>, hmr_update: bool, ) -> Result> { @@ -5632,7 +5641,10 @@ fn compile_component_full<'a>( // ngc's full compilation (#313). Needs cross-file analysis for the // selectors; skipped under HMR, like ngc. let mut implicitly_deferred_imports = Vec::new(); - if let Some(deferrable_selector) = deferrable_selector + // What each `@defer` block uses, keyed by the block's span, when its own + // dependency function can be built (see below). + let mut defer_block_dependencies = None; + if let Some(import_selector) = import_selector && let Some(import_elision) = import_elision && !options.hmr && let Some(OutputExpression::LiteralArray(raw_imports)) = &metadata.raw_imports @@ -5644,24 +5656,30 @@ fn compile_component_full<'a>( .all(|(entry, class)| class.is_some() || matches!(entry, OutputExpression::ReadVar(_))) && super::defer_resolver::template_has_defer_block(&r3_result.nodes) { + let mut deferrable = Vec::new(); let candidates: Vec<_> = metadata .import_entry_classes .iter() .flatten() .copied() .filter_map(|name| { - let selector = deferrable_selector(name.as_str())?; + let (selector, can_defer) = import_selector(name.as_str())?; + if can_defer { + deferrable.push(name); + } Some((name, crate::pipeline::selector::CssSelector::parse(&selector))) }) .collect(); - let deferred = + let block_only = super::defer_resolver::implicitly_deferred_imports(&r3_result.nodes, &candidates); + let mut deferred = block_only.clone(); + deferred.retain(|name| deferrable.contains(name)); // ngc's `DeferredSymbolTracker.canDefer`: an import declaration goes // only when every non-`type` specifier in it is deferred, or the // static import would stay and nothing would be split off. An // explicit `deferredImports` entry doesn't count: OXC keeps its // static import. - let deferred: std::vec::Vec<_> = deferred + let mut deferred: std::vec::Vec<_> = deferred .iter() .copied() .filter(|name| { @@ -5671,15 +5689,53 @@ fn compile_component_full<'a>( .all(|specifier| deferred.contains(specifier)) }) .collect(); - if !deferred.is_empty() { - let is_deferred = |name: &str| deferred.iter().any(|d| d.as_str() == name); - metadata.imports.retain(|name| !is_deferred(name.as_str())); + // ngc's full compilation gives each `@defer` block its own dependency + // function (`DeferBlockDepsEmitMode.PerBlock`) listing what the + // block's main content uses: a dynamic `import()` for a lazy + // dependency and a plain reference for an eager one + // (`resolveDeferBlocks` and `compileDeferBlocks` in its component + // `handler.ts`). An `imports` entry whose selector is unknown is left + // out; it stays eager, and `dependencies` already provides it to the + // block. An explicit `deferredImports` keeps the shared function: its + // selectors aren't read. + if metadata.deferred_imports.is_empty() && !options.jit { + defer_block_dependencies = + super::defer_resolver::defer_block_dependencies(&r3_result.nodes, &candidates); + } + if let Some(blocks) = &defer_block_dependencies { + // `resolveAllDeferredDependencies` lists the lazy dependencies of + // `setClassMetadataAsync` block by block. + deferred.sort_by_key(|name| { + blocks.iter().flat_map(|(_, dependencies)| dependencies).position(|d| d == name) + }); + } + // `dependencies` lists only what the template uses eagerly + // (`handleDependencyCycles` in ngc's component `handler.ts`). With a + // dependency function per block, an entry used only inside `@defer` + // blocks leaves it even when it can't be lazy-loaded: the blocks' + // functions provide it. Otherwise only the deferred entries leave. + let removed = if defer_block_dependencies.is_some() { &block_only } else { &deferred }; + if !removed.is_empty() { + let is_removed = |name: &str| removed.iter().any(|d| d.as_str() == name); + metadata.imports.retain(|name| !is_removed(name.as_str())); if let Some(OutputExpression::LiteralArray(raw_imports)) = &mut metadata.raw_imports { let mut classes = metadata.import_entry_classes.iter(); raw_imports.entries.retain(|_| { - !classes.next().copied().flatten().is_some_and(|name| is_deferred(&name)) + !classes.next().copied().flatten().is_some_and(|name| is_removed(&name)) }); + // Nothing left to resolve at runtime: like a standalone + // component without `imports`, it gets no `dependencies`. + if raw_imports.entries.is_empty() + && defer_block_dependencies.is_some() + && metadata.standalone + { + metadata.raw_imports = None; + metadata.declaration_list_emit_mode = + super::metadata::DeclarationListEmitMode::Direct; + } } + } + if !deferred.is_empty() { implicitly_deferred_imports = deferred.iter().map(ToString::to_string).collect(); metadata.deferred_imports.extend(deferred); } @@ -5702,8 +5758,9 @@ fn compile_component_full<'a>( // Note: Angular's full compilation mode also derives deferrable symbols // from `imports: [...]` by matching template selectors against imported // directive classes (`resolveAllDeferredDependencies`). OXC does that - // above when cross-file analysis is available (#313) and otherwise - // follows Angular's local path, which requires explicit `deferredImports`. + // above when cross-file analysis is available (#313), with one dependency + // function per block, and otherwise follows Angular's local path, which + // requires explicit `deferredImports`. // Computed once and reused below: the resolver decision and the eventual // `setClassMetadataAsync` gate both depend on whether the template uses // `@defer`. @@ -5713,20 +5770,23 @@ fn compile_component_full<'a>( // would resolve against the update module's URL, not the component file. // The update keeps a null dependency function instead (PerComponent mode // below), as `compile_template_for_hmr` always has. - let defer_resolver_fn = if metadata.deferred_imports.is_empty() || hmr_update { - None - } else if has_defer_block { - super::defer_resolver::build_defer_resolver_expression(allocator, metadata, import_map) - } else { - // `deferredImports` declared but no `@defer` block — the resolver - // would be unused dead code. Leave it off. - None - }; + let defer_resolver_fn = + if metadata.deferred_imports.is_empty() || hmr_update || defer_block_dependencies.is_some() + { + None + } else if has_defer_block { + super::defer_resolver::build_defer_resolver_expression(allocator, metadata, import_map) + } else { + // `deferredImports` declared but no `@defer` block — the resolver + // would be unused dead code. Leave it off. + None + }; // When we have a resolver, switch to PerComponent emit mode so the // ingest stage threads `all_deferrable_deps_fn` into each `DeferOp`. // Without a resolver, the existing PerBlock-vs-JIT behavior stays in - // place (and `all_deferrable_deps_fn = None`). + // place (and `all_deferrable_deps_fn = None`). Per-block dependencies + // never come with a resolver, so they get PerBlock. let defer_block_deps_emit_mode = if defer_resolver_fn.is_some() || (hmr_update && !metadata.deferred_imports.is_empty() && has_defer_block) { @@ -5758,7 +5818,20 @@ fn compile_component_full<'a>( // PerComponent mode: pass the shared resolver expression so the // ingest stage attaches it to every defer block's create op. all_deferrable_deps_fn: defer_resolver_fn, - defer_blocks: FxHashMap::default(), + // PerBlock mode: each block's own dependency function, if any. + defer_blocks: defer_block_dependencies + .unwrap_or_default() + .into_iter() + .map(|(span, dependencies)| { + let deps_fn = super::defer_resolver::compile_defer_block_resolver( + allocator, + &dependencies, + &metadata.deferred_imports, + import_map, + ); + (span, deps_fn) + }) + .collect(), // Use the shared pool starting index to avoid duplicate constant names // when compiling multiple components in the same file pool_starting_index, diff --git a/crates/oxc_angular_compiler/src/pipeline/constant_pool.rs b/crates/oxc_angular_compiler/src/pipeline/constant_pool.rs index 640a40ee4..33dafca1a 100644 --- a/crates/oxc_angular_compiler/src/pipeline/constant_pool.rs +++ b/crates/oxc_angular_compiler/src/pipeline/constant_pool.rs @@ -329,8 +329,14 @@ impl<'a> ConstantPool<'a> { } } - // No equivalent function found, create a new entry - let fn_name = self.unique_name(name, use_unique_name); + // No equivalent function found, create a new entry. Without + // `use_unique_name` the name is taken as is and not claimed, so it + // doesn't move the pool's name indices. + let fn_name = if use_unique_name { + self.unique_name(name, true) + } else { + Ident::from(self.allocator.alloc_str(name)) + }; // Add the function to statements (not values) to match Angular TypeScript // Arrow functions become DeclareVarStmt, regular functions become DeclareFunctionStmt @@ -629,6 +635,7 @@ fn output_stmt_is_equivalent(a: &OutputStatement<'_>, b: &OutputStatement<'_>) - #[cfg(test)] mod tests { use super::*; + use crate::output::ast::{ArrowFunctionBody, ArrowFunctionExpr, LiteralExpr, LiteralValue}; #[test] fn test_string_deduplication() { @@ -643,4 +650,37 @@ mod tests { assert_ne!(idx1, idx3); assert_eq!(pool.len(), 2); } + + #[test] + fn test_shared_function_without_unique_name_takes_no_index() { + let allocator = &Allocator::default(); + let mut pool = ConstantPool::with_starting_index(allocator, 2); + let arrow = |value: f64| { + OutputExpression::ArrowFunction(Box::new_in( + ArrowFunctionExpr { + params: Vec::new_in(&allocator), + body: ArrowFunctionBody::Expression(Box::new_in( + OutputExpression::Literal(Box::new_in( + LiteralExpr { value: LiteralValue::Number(value), source_span: None }, + &allocator, + )), + &allocator, + )), + source_span: None, + }, + &allocator, + )) + }; + + let first = pool.get_shared_function_reference(arrow(1.0), "Cmp_Defer_1_DepsFn", false); + let second = pool.get_shared_function_reference(arrow(2.0), "Cmp_Defer_3_DepsFn", false); + + let name = |expr: &OutputExpression<'_>| match expr { + OutputExpression::ReadVar(read) => read.name.to_string(), + _ => panic!("expected a variable reference"), + }; + assert_eq!(name(&first), "Cmp_Defer_1_DepsFn"); + assert_eq!(name(&second), "Cmp_Defer_3_DepsFn"); + assert_eq!(pool.next_name_index(), 2); + } } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_defer_deps_fns.rs b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_defer_deps_fns.rs index f57ee80d5..ef6dd16a2 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/resolve_defer_deps_fns.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/resolve_defer_deps_fns.rs @@ -57,12 +57,12 @@ pub fn resolve_defer_deps_fns(job: &mut ComponentCompilationJob<'_>) { for (view_xref, slot_id) in defer_locations { // Get the full path name from the view's fn_name // In Angular TS: const fullPathName = unit.fnName?.replace('_Template', ''); + // `String.prototype.replace` with a string pattern replaces only the + // first match. let full_path_name = job .view(view_xref) .and_then(|v| v.fn_name.as_ref()) - .map(|n| n.as_str().trim_end_matches("_Template")) - .unwrap_or(job.component_name.as_str()) - .to_string(); // Convert to owned String to avoid borrow issues + .map_or_else(|| job.component_name.to_string(), |n| n.replacen("_Template", "", 1)); // Find the defer op and take ownership of own_resolver_fn let view = 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 6f141ccd2..b364c73d2 100644 --- a/crates/oxc_angular_compiler/tests/defer_implicit_imports_test.rs +++ b/crates/oxc_angular_compiler/tests/defer_implicit_imports_test.rs @@ -92,11 +92,13 @@ export class Parent {} assert_lazy(code, "./lazy", "LazyCmp"); assert!( - compact.contains("ɵɵdefer(1,0,()=>[import(\"./lazy\")"), - "The resolver must be wired into `ɵɵdefer`. Output:\n{code}" + compact.contains("constParent_Defer_1_DepsFn=()=>[import(\"./lazy\")") + && compact.contains("ɵɵdefer(1,0,Parent_Defer_1_DepsFn)"), + "The block's resolver must be wired into `ɵɵdefer`. Output:\n{code}" ); + // Like ngc: nothing is used eagerly, so there is no `dependencies`. assert!( - compact.contains("ɵɵgetComponentDepsFactory(Parent,[])"), + !compact.contains("dependencies:"), "The lazy component must leave the eager dependencies. Output:\n{code}" ); assert!( @@ -252,8 +254,8 @@ 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!(compact(code).contains("ɵɵdefer(1,0,Parent_Defer_1_DepsFn)"), "{code}"); + assert!(!compact(code).contains("dependencies:"), "{code}"); assert!(!code.contains("import { LazyCmp }"), "{code}"); } } @@ -822,10 +824,10 @@ export class Parent {} let code = &result.code; assert_eager(code, "./cmps"); assert!(code.contains("import { LazyCmp, EagerCmp }"), "{code}"); - assert!( - compact(code).contains("ɵɵgetComponentDepsFactory(Parent,[LazyCmp,EagerCmp])"), - "{code}" - ); + // Like ngc: `LazyCmp` stays eager but is used only by the block, whose + // function provides it. + assert!(compact(code).contains("constParent_Defer_2_DepsFn=()=>[LazyCmp];"), "{code}"); + assert!(compact(code).contains("ɵɵgetComponentDepsFactory(Parent,[EagerCmp])"), "{code}"); } #[test] @@ -899,7 +901,7 @@ export class Parent {} let code = &result.code; assert_lazy(code, "./cmps", "LazyCmp"); assert_lazy(code, "./cmps", "EagerCmp"); - assert!(compact(code).contains("ɵɵgetComponentDepsFactory(Parent,[])"), "{code}"); + assert!(!compact(code).contains("dependencies:"), "{code}"); assert!(!code.contains("from \"./cmps\""), "{code}"); } @@ -952,7 +954,8 @@ export class Parent { ); let code = &result.code; assert_eager(code, "./cmps"); - assert!(compact(code).contains("ɵɵgetComponentDepsFactory(Parent,[LazyCmp])"), "{code}"); + assert!(compact(code).contains("constParent_Defer_1_DepsFn=()=>[LazyCmp];"), "{code}"); + assert!(!compact(code).contains("dependencies:"), "{code}"); } #[test] @@ -1105,3 +1108,394 @@ export class Parent {} ); assert_eager(&result.code, "./lazy"); } + +// --- Per-block dependency functions: ngc's full compilation gives each +// `@defer` block its own `_Defer__DepsFn` (`resolveDeferBlocks`, +// `compileDeferBlocks` and `compileDeferResolverFunction`) --- + +/// `app/other.ts`, a standalone component `OtherCmp` with `app-other`. +fn other_cmp(dir: &TempDir) { + create_test_file( + dir.path(), + "app/other.ts", + r" +import { Component } from '@angular/core'; + +@Component({ selector: 'app-other', template: 'other' }) +export class OtherCmp {} +", + ); +} + +#[test] +fn each_defer_block_loads_only_its_own_dependencies() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + other_cmp(&dir); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; +import { LazyCmp } from './lazy'; +import { OtherCmp } from './other'; + +@Component({ + selector: 'app-parent', + imports: [LazyCmp, OtherCmp], + template: '@defer { } @defer (on viewport) { } @placeholder {

x

}', +}) +export class Parent {} +", + ); + let code = &result.code; + let compact = compact(code); + assert!( + compact.contains( + "constParent_Defer_1_DepsFn=()=>[import(\"./lazy\").then((m)=>m.LazyCmp)];\ + constParent_Defer_5_DepsFn=()=>[import(\"./other\").then((m)=>m.OtherCmp)];" + ), + "Each block must get its own resolver. Output:\n{code}" + ); + assert!(compact.contains("ɵɵdefer(1,0,Parent_Defer_1_DepsFn)"), "{code}"); + assert!(compact.contains("ɵɵdefer(5,3,Parent_Defer_5_DepsFn,null,4)"), "{code}"); + assert!(!compact.contains("dependencies:"), "{code}"); + assert!( + compact.contains( + "setClassMetadataAsync(Parent,()=>[import(\"./lazy\").then((m)=>m.LazyCmp),\ + import(\"./other\").then((m)=>m.OtherCmp)],(LazyCmp,OtherCmp)=>" + ), + "{code}" + ); +} + +#[test] +fn eager_dependency_used_in_a_defer_block_is_listed_by_reference() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; +import { LazyCmp } from './lazy'; + +@Component({ + selector: 'app-parent', + imports: [LazyCmp], + template: ' @defer { }', +}) +export class Parent {} +", + ); + let code = &result.code; + let compact = compact(code); + assert_eager(code, "./lazy"); + assert!(compact.contains("constParent_Defer_2_DepsFn=()=>[LazyCmp];"), "{code}"); + assert!(compact.contains("ɵɵdefer(2,1,Parent_Defer_2_DepsFn)"), "{code}"); + assert!(compact.contains("ɵɵgetComponentDepsFactory(Parent,[LazyCmp])"), "{code}"); +} + +#[test] +fn import_referenced_elsewhere_is_listed_by_reference_next_to_a_lazy_one() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + other_cmp(&dir); + let result = transform( + &dir, + r" +import { Component, ViewChild } from '@angular/core'; +import { LazyCmp } from './lazy'; +import { OtherCmp } from './other'; + +@Component({ + selector: 'app-parent', + imports: [LazyCmp, OtherCmp], + template: '@defer { }', +}) +export class Parent { @ViewChild(LazyCmp) lazy: any; } +", + ); + let code = &result.code; + assert!( + compact(code).contains( + "constParent_Defer_1_DepsFn=()=>[LazyCmp,import(\"./other\").then((m)=>m.OtherCmp)];" + ), + "{code}" + ); +} + +#[test] +fn same_file_component_used_in_a_defer_block_is_listed_by_reference() { + let dir = TempDir::new().unwrap(); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; + +@Component({ selector: 'app-local', template: 'local' }) +class LocalCmp {} + +@Component({ + selector: 'app-parent', + imports: [LocalCmp], + template: '@defer { }', +}) +export class Parent {} +", + ); + let code = &result.code; + let compact = compact(code); + assert!(compact.contains("constParent_Defer_1_DepsFn=()=>[LocalCmp];"), "{code}"); + assert!(compact.contains("ɵɵdefer(1,0,Parent_Defer_1_DepsFn)"), "{code}"); + // Like ngc: the block's function provides `LocalCmp`, so it leaves + // `dependencies`. + assert!(!compact.contains("dependencies:"), "{code}"); + assert!(!code.contains("setClassMetadataAsync"), "{code}"); +} + +#[test] +fn defer_block_mixes_a_lazy_import_and_a_same_file_component() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; +import { LazyCmp } from './lazy'; + +@Component({ selector: 'app-local', template: 'local' }) +export class LocalCmp {} + +@Component({ + selector: 'app-parent', + imports: [LazyCmp, LocalCmp], + template: '@defer { }', +}) +export class Parent {} +", + ); + let code = &result.code; + let compact = compact(code); + assert!( + compact.contains( + "constParent_Defer_1_DepsFn=()=>[import(\"./lazy\").then((m)=>m.LazyCmp),LocalCmp];" + ), + "{code}" + ); + assert!(!compact.contains("dependencies:"), "{code}"); +} + +#[test] +fn nested_defer_block_placeholder_belongs_to_the_outer_block() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + other_cmp(&dir); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; +import { LazyCmp } from './lazy'; +import { OtherCmp } from './other'; + +@Component({ + selector: 'app-parent', + imports: [LazyCmp, OtherCmp], + template: '@defer { @defer (on viewport) { } @placeholder { } }', +}) +export class Parent {} +", + ); + let code = &result.code; + let compact = compact(code); + assert!( + compact + .contains("constParent_Defer_1_DepsFn=()=>[import(\"./lazy\").then((m)=>m.LazyCmp)];"), + "{code}" + ); + assert!( + compact.contains( + "constParent_Defer_0_Defer_2_DepsFn=()=>[import(\"./other\").then((m)=>m.OtherCmp)];" + ), + "{code}" + ); +} + +#[test] +fn class_metadata_lists_lazy_dependencies_block_by_block() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + other_cmp(&dir); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; +import { LazyCmp } from './lazy'; +import { OtherCmp } from './other'; + +@Component({ + selector: 'app-parent', + imports: [OtherCmp, LazyCmp], + template: '@defer { } @defer (on viewport) { } @placeholder {

x

}', +}) +export class Parent {} +", + ); + let code = &result.code; + assert!( + compact(code).contains( + "setClassMetadataAsync(Parent,()=>[import(\"./lazy\").then((m)=>m.LazyCmp),\ + import(\"./other\").then((m)=>m.OtherCmp)],(LazyCmp,OtherCmp)=>" + ), + "{code}" + ); +} + +#[test] +fn defer_block_without_dependencies_has_no_resolver() { + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; +import { LazyCmp } from './lazy'; + +@Component({ + selector: 'app-parent', + imports: [LazyCmp], + template: '@defer {

x

} ', +}) +export class Parent {} +", + ); + let code = &result.code; + assert!(compact(code).contains("ɵɵdefer(1,0);"), "{code}"); + assert!(!code.contains("DepsFn"), "{code}"); +} + +#[test] +fn deps_fn_name_drops_only_the_first_template_suffix() { + // ngc names the function after `unit.fnName?.replace('_Template', '')`, + // which replaces the first match only. + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + other_cmp(&dir); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; +import { LazyCmp } from './lazy'; +import { OtherCmp } from './other'; + +@Component({ selector: 'app-a', imports: [LazyCmp], template: '@defer { }' }) +export class Foo {} + +@Component({ selector: 'app-b', imports: [OtherCmp], template: '@defer { }' }) +export class Foo_Template {} + +@Component({ + selector: 'app-c', + imports: [LazyCmp], + template: '@if (x) { @defer { } }', +}) +export class My_TemplateCmp { + x = true; +} +", + ); + let code = &result.code; + let compact = compact(code); + assert!(compact.contains("constFoo_Defer_1_DepsFn=()=>[LazyCmp];"), "{code}"); + assert!(compact.contains("ɵɵdefer(1,0,Foo_Defer_1_DepsFn)"), "{code}"); + assert!( + compact.contains("constFoo_Template_Defer_1_DepsFn=()=>[import(\"./other\")"), + "{code}" + ); + assert!(compact.contains("ɵɵdefer(1,0,Foo_Template_Defer_1_DepsFn)"), "{code}"); + assert!( + compact.contains("constMyCmp_Conditional_0_Template_Defer_1_DepsFn=()=>[LazyCmp];"), + "{code}" + ); + assert_eq!(code.matches("const Foo_Defer_1_DepsFn").count(), 1, "{code}"); +} + +#[test] +fn deps_fns_do_not_move_the_constant_names_of_later_components() { + // ngc pools the function under its own name (`getSharedFunctionReference` + // with `useUniqueName` false), which takes no counter value. + let dir = TempDir::new().unwrap(); + lazy_cmp(&dir); + let source = |imports: &str| { + format!( + r#" +import {{ Component }} from '@angular/core'; +import {{ LazyCmp }} from './lazy'; + +@Component({{ selector: 'app-local', template: 'local' }}) +export class LocalCmp {{}} + +@Component({{ + selector: 'app-a', + imports: [{imports}], + template: '

@defer {{ }} @defer {{ }}', +}}) +export class A1 {{ + x = 1; +}} + +@Component({{ + selector: 'app-b', + imports: [LocalCmp], + template: '

@defer {{ }}', +}}) +export class A2 {{ + x = 1; +}} +"# + ) + }; + // The name of A2's first pure function. + let a2_constant = |code: &str| { + let compact = compact(code); + let end = compact.find("=(a0)=>[2,a0]").expect(code); + let start = compact[..end].rfind("const").expect(code) + "const".len(); + compact[start..end].to_string() + }; + let with_deps_fns = transform(&dir, &source("LazyCmp, LocalCmp")).code; + assert!(with_deps_fns.contains("const A1_Defer_2_DepsFn"), "{with_deps_fns}"); + assert!(with_deps_fns.contains("const A1_Defer_5_DepsFn"), "{with_deps_fns}"); + let without = transform(&dir, &source("")).code; + assert!(!without.contains("const A1_Defer"), "{without}"); + assert_eq!(a2_constant(&with_deps_fns), a2_constant(&without), "{with_deps_fns}"); +} + +#[test] +fn block_only_dependencies_leave_dependencies_of_each_component() { + // Like ngc, a same-file component used only inside `@defer` is provided + // by the block's function, not by `dependencies`. + let dir = TempDir::new().unwrap(); + let result = transform( + &dir, + r" +import { Component } from '@angular/core'; + +@Component({ selector: 'app-local', template: 'local' }) +export class LocalCmp {} + +@Component({ selector: 'app-a', imports: [LocalCmp], template: '@defer { }' }) +export class App1 {} + +@Component({ + selector: 'app-b', + imports: [LocalCmp], + template: ' @defer { }', +}) +export class App2 {} +", + ); + let code = &result.code; + let compact = compact(code); + assert!(compact.contains("constApp1_Defer_1_DepsFn=()=>[LocalCmp];"), "{code}"); + assert!(compact.contains("constApp2_Defer_2_DepsFn=()=>[LocalCmp];"), "{code}"); + assert!(!compact.contains("ɵɵgetComponentDepsFactory(App1"), "{code}"); + assert!(compact.contains("ɵɵgetComponentDepsFactory(App2,[LocalCmp])"), "{code}"); +}