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.
This commit is contained in:
Kristiyan Kostadinov 2025-12-02 10:22:14 +01:00 committed by Pawel Kozlowski
parent d6087841e3
commit 886cf6c452
7 changed files with 102 additions and 32 deletions

View file

@ -625,6 +625,7 @@ function getNgDirectiveDef<T>(directiveDefinition: DirectiveDefinition<T>): Dire
return {
type: directiveDefinition.type,
providersResolver: null,
viewProvidersResolver: null,
factory: null,
hostBindings: directiveDefinition.hostBindings || null,
hostVars: directiveDefinition.hostVars || 0,

View file

@ -56,17 +56,11 @@ import {getCurrentTNode, getLView, getTView} from './state';
export function providersResolver<T>(
def: DirectiveDef<T>,
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);
}
}

View file

@ -41,17 +41,18 @@ import {DirectiveDef} from '../interfaces/definition';
*
* @codeGenApi
*/
export function ɵɵProvidersFeature<T>(providers: Provider[], viewProviders: Provider[] = []) {
export function ɵɵProvidersFeature<T>(providers: Provider[], viewProviders: Provider[]) {
return (definition: DirectiveDef<T>) => {
definition.providersResolver = (
def: DirectiveDef<T>,
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,
);
}
};
}

View file

@ -34,6 +34,12 @@ export type ComponentTemplate<T> = {
*/
export type ViewQueriesFunction<T> = <U extends T>(rf: RenderFlags, ctx: U) => void;
/** Function that resolves providers and publishes them to the DI system. */
export type ProvidersResolver = (
def: DirectiveDef<unknown>,
processProvidersFn?: ProcessProvidersFunction,
) => void;
/**
* Definition of what a content queries function should look like.
*/
@ -196,10 +202,11 @@ export interface DirectiveDef<T> {
/** Token representing the directive. Used by DI. */
readonly type: Type<T>;
/** Function that resolves providers and publishes them into the DI system. */
providersResolver:
| (<U extends T>(def: DirectiveDef<U>, 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;

View file

@ -145,14 +145,14 @@ function initializeDirectives(
ngDevMode && assertFirstCreatePass(tView);
const directivesLength = directives.length;
let hasSeenComponent = false;
let componentDef: ComponentDef<unknown> | 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;

View file

@ -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<ProvidesExisting>('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: '<div injectsExisting></div>',
imports: [InjectsExisting],
hostDirectives: [HostDirective],
viewProviders: [{provide: token, useExisting: ProvidesExisting}],
})
class CompWithHostDirective {}
@Component({
selector: 'app-root',
template: '<comp-with-host-directive providesExisting/>',
imports: [ProvidesExisting, CompWithHostDirective],
})
class App {}
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(value).toBeTruthy();
expect(value instanceof ProvidesExisting).toBe(true);
});
});
describe('outputs', () => {

View file

@ -1070,15 +1070,30 @@ export class TestBedCompiler {
private patchDefWithProviderOverrides(declaration: Type<any>, 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<any>) =>
viewProvidersResolver(ngDef, this.processProviderOverrides);
}
if (def.providersResolver) {
this.maybeStoreNgDef(field, declaration);
const providersResolver = def.providersResolver;
this.storeFieldOfDefOnType(declaration, field, 'providersResolver');
def.providersResolver = (ngDef: DirectiveDef<any>) => resolver(ngDef, processProvidersFn);
def.providersResolver = (ngDef: DirectiveDef<any>) =>
providersResolver(ngDef, this.processProviderOverrides);
}
}
private processProviderOverrides = (providers: Provider[]) =>
this.getOverriddenProviders(providers);
}
function initResolvers(): Resolvers {