From 886cf6c452abcf85b70058caeb080ff3a5c51cb9 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 2 Dec 2025 10:22:14 +0100 Subject: [PATCH] fix(core): unable to inject viewProviders when host directive with providers is present When registering providers, the DI system assumes that `viewProviders` are registered before plain `providers`. This was reinforced by components always being first in the array of directive matches, only one component being allowed per node and the fact that only components can have `viewProviders`. This breaks down if there are host directives with `providers` on the component, because they'll execute earlier, throwing off the order of operations. These changes fix the issue by separating out the resolvers for `viewProviders` and plain `providers` and explicitly running the component's `viewProviders` resolver before any others. This also has the benefit of not attempting to resolve `viewProviders` for directives which are guaranteed not to have them. Fixes #65724. --- packages/core/src/render3/definition.ts | 1 + packages/core/src/render3/di_setup.ts | 10 +---- .../src/render3/features/providers_feature.ts | 23 +++++----- .../core/src/render3/interfaces/definition.ts | 15 +++++-- packages/core/src/render3/view/directives.ts | 16 +++++-- .../test/acceptance/host_directives_spec.ts | 44 +++++++++++++++++++ .../core/testing/src/test_bed_compiler.ts | 25 ++++++++--- 7 files changed, 102 insertions(+), 32 deletions(-) diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 17c888fa47d..baadb1f29c2 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -625,6 +625,7 @@ function getNgDirectiveDef(directiveDefinition: DirectiveDefinition): Dire return { type: directiveDefinition.type, providersResolver: null, + viewProvidersResolver: null, factory: null, hostBindings: directiveDefinition.hostBindings || null, hostVars: directiveDefinition.hostVars || 0, diff --git a/packages/core/src/render3/di_setup.ts b/packages/core/src/render3/di_setup.ts index b5925dc2bc8..d791c1cfe4a 100644 --- a/packages/core/src/render3/di_setup.ts +++ b/packages/core/src/render3/di_setup.ts @@ -56,17 +56,11 @@ import {getCurrentTNode, getLView, getTView} from './state'; export function providersResolver( def: DirectiveDef, providers: Provider[], - viewProviders: Provider[], + isViewProviders: boolean, ): void { const tView = getTView(); if (tView.firstCreatePass) { - const isComponent = isComponentDef(def); - - // The list of view providers is processed first, and the flags are updated - resolveProvider(viewProviders, tView.data, tView.blueprint, isComponent, true); - - // Then, the list of providers is processed, and the flags are updated - resolveProvider(providers, tView.data, tView.blueprint, isComponent, false); + resolveProvider(providers, tView.data, tView.blueprint, isComponentDef(def), isViewProviders); } } diff --git a/packages/core/src/render3/features/providers_feature.ts b/packages/core/src/render3/features/providers_feature.ts index d64493081f5..b1c104e00a0 100644 --- a/packages/core/src/render3/features/providers_feature.ts +++ b/packages/core/src/render3/features/providers_feature.ts @@ -41,17 +41,18 @@ import {DirectiveDef} from '../interfaces/definition'; * * @codeGenApi */ -export function ɵɵProvidersFeature(providers: Provider[], viewProviders: Provider[] = []) { +export function ɵɵProvidersFeature(providers: Provider[], viewProviders: Provider[]) { return (definition: DirectiveDef) => { - definition.providersResolver = ( - def: DirectiveDef, - processProvidersFn?: ProcessProvidersFunction, - ) => { - return providersResolver( - def, // - processProvidersFn ? processProvidersFn(providers) : providers, // - viewProviders, - ); - }; + definition.providersResolver = (def, processProvidersFn) => + providersResolver(def, processProvidersFn ? processProvidersFn(providers) : providers, false); + + if (viewProviders) { + definition.viewProvidersResolver = (def, processProvidersFn) => + providersResolver( + def, + processProvidersFn ? processProvidersFn(viewProviders) : viewProviders, + true, + ); + } }; } diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 072b7dd9e1b..27a09d329de 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -34,6 +34,12 @@ export type ComponentTemplate = { */ export type ViewQueriesFunction = (rf: RenderFlags, ctx: U) => void; +/** Function that resolves providers and publishes them to the DI system. */ +export type ProvidersResolver = ( + def: DirectiveDef, + processProvidersFn?: ProcessProvidersFunction, +) => void; + /** * Definition of what a content queries function should look like. */ @@ -196,10 +202,11 @@ export interface DirectiveDef { /** Token representing the directive. Used by DI. */ readonly type: Type; - /** Function that resolves providers and publishes them into the DI system. */ - providersResolver: - | ((def: DirectiveDef, processProvidersFn?: ProcessProvidersFunction) => void) - | null; + /** Function that resolves `providers` and publishes them into the DI system. */ + providersResolver: ProvidersResolver | null; + + /** Function that resolves `viewProviders` and publishes them into the DI system. */ + viewProvidersResolver: ProvidersResolver | null; /** The selectors that will be used to match nodes to this directive. */ readonly selectors: CssSelectorList; diff --git a/packages/core/src/render3/view/directives.ts b/packages/core/src/render3/view/directives.ts index afe5abc7dfd..014eab03e46 100644 --- a/packages/core/src/render3/view/directives.ts +++ b/packages/core/src/render3/view/directives.ts @@ -145,14 +145,14 @@ function initializeDirectives( ngDevMode && assertFirstCreatePass(tView); const directivesLength = directives.length; - let hasSeenComponent = false; + let componentDef: ComponentDef | null = null; // Publishes the directive types to DI so they can be injected. Needs to // happen in a separate pass before the TNode flags have been initialized. for (let i = 0; i < directivesLength; i++) { const def = directives[i]; - if (!hasSeenComponent && isComponentDef(def)) { - hasSeenComponent = true; + if (componentDef === null && isComponentDef(def)) { + componentDef = def; markAsComponentHost(tView, tNode, i); } diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, lView), tView, def.type); @@ -166,9 +166,17 @@ function initializeDirectives( // - the last directive in NgModule.declarations has priority over the previous one // So to match these rules, the order in which providers are added in the arrays is very // important. + if (componentDef?.viewProvidersResolver) { + // Only components can have `viewProviders`, `viewProviders` are registered first and there + // can only be one component so we check for it specifically. + componentDef.viewProvidersResolver(componentDef); + } + for (let i = 0; i < directivesLength; i++) { const def = directives[i]; - if (def.providersResolver) def.providersResolver(def); + if (def.providersResolver) { + def.providersResolver(def); + } } let preOrderHooksFound = false; let preOrderCheckHooksFound = false; diff --git a/packages/core/test/acceptance/host_directives_spec.ts b/packages/core/test/acceptance/host_directives_spec.ts index b54759df8c6..2fab249434b 100644 --- a/packages/core/test/acceptance/host_directives_spec.ts +++ b/packages/core/test/acceptance/host_directives_spec.ts @@ -1230,6 +1230,50 @@ describe('host directives', () => { hostDirectiveCdr.detectChanges(); }).not.toThrow(); }); + + // See #65724. + it('should be able to inject host tokens defined through `viewProviders` in a component using host directives', () => { + const token = new InjectionToken('token'); + let value: ProvidesExisting | undefined | null; + + @Directive({ + // These providers aren't injected, but they help hit the relevant code path. + providers: [{provide: new InjectionToken('unusedToken'), useValue: true}], + }) + class HostDirective {} + + @Directive({selector: '[injectsExisting]'}) + class InjectsExisting { + constructor() { + value = inject(token, {host: true, optional: true}); + } + } + + @Directive({selector: '[providesExisting]'}) + class ProvidesExisting {} + + @Component({ + selector: 'comp-with-host-directive', + template: '
', + imports: [InjectsExisting], + hostDirectives: [HostDirective], + viewProviders: [{provide: token, useExisting: ProvidesExisting}], + }) + class CompWithHostDirective {} + + @Component({ + selector: 'app-root', + template: '', + imports: [ProvidesExisting, CompWithHostDirective], + }) + class App {} + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(value).toBeTruthy(); + expect(value instanceof ProvidesExisting).toBe(true); + }); }); describe('outputs', () => { diff --git a/packages/core/testing/src/test_bed_compiler.ts b/packages/core/testing/src/test_bed_compiler.ts index dc25dfa2a5a..d5620e71ced 100644 --- a/packages/core/testing/src/test_bed_compiler.ts +++ b/packages/core/testing/src/test_bed_compiler.ts @@ -1070,15 +1070,30 @@ export class TestBedCompiler { private patchDefWithProviderOverrides(declaration: Type, field: string): void { const def = (declaration as any)[field]; - if (def && def.providersResolver) { - this.maybeStoreNgDef(field, declaration); - const resolver = def.providersResolver; - const processProvidersFn = (providers: Provider[]) => this.getOverriddenProviders(providers); + if (!def) { + return; + } + + if (def.viewProvidersResolver) { + this.maybeStoreNgDef(field, declaration); + const viewProvidersResolver = def.viewProvidersResolver; + this.storeFieldOfDefOnType(declaration, field, 'viewProvidersResolver'); + def.viewProvidersResolver = (ngDef: DirectiveDef) => + viewProvidersResolver(ngDef, this.processProviderOverrides); + } + + if (def.providersResolver) { + this.maybeStoreNgDef(field, declaration); + const providersResolver = def.providersResolver; this.storeFieldOfDefOnType(declaration, field, 'providersResolver'); - def.providersResolver = (ngDef: DirectiveDef) => resolver(ngDef, processProvidersFn); + def.providersResolver = (ngDef: DirectiveDef) => + providersResolver(ngDef, this.processProviderOverrides); } } + + private processProviderOverrides = (providers: Provider[]) => + this.getOverriddenProviders(providers); } function initResolvers(): Resolvers {