From 02fde2bfeeacef807a8c9f92bb940e4a1047d00a Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 5 Oct 2022 12:09:21 -0700 Subject: [PATCH 1/4] refactor(core): define shape of EnvironmentProviders interface This commit introduces the `EnvironmentProviders` interface, but does not yet export it as public API. --- packages/core/src/core_private_export.ts | 1 + packages/core/src/di/interface/provider.ts | 35 +++++++++++++++++++++ packages/core/src/di/provider_collection.ts | 12 ++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index e63093538efae..8483fda6493f6 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -14,6 +14,7 @@ export {Console as ɵConsole} from './console'; export {getDebugNodeR2 as ɵgetDebugNodeR2} from './debug/debug_node'; export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility'; export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDeclaration, ɵɵInjectorDef} from './di/interface/defs'; +export {InternalEnvironmentProviders as ɵInternalEnvironmentProviders, isEnvironmentProviders as ɵisEnvironmentProviders} from './di/interface/provider'; export {INJECTOR_SCOPE as ɵINJECTOR_SCOPE} from './di/scope'; export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeError} from './errors'; export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api'; diff --git a/packages/core/src/di/interface/provider.ts b/packages/core/src/di/interface/provider.ts index 3ac95e9cb5147..f2a4be691969e 100644 --- a/packages/core/src/di/interface/provider.ts +++ b/packages/core/src/di/interface/provider.ts @@ -332,6 +332,41 @@ export interface ClassProvider extends ClassSansProvider { export type Provider = TypeProvider|ValueProvider|ClassProvider|ConstructorProvider| ExistingProvider|FactoryProvider|any[]; +/** + * Encapsulated `Provider`s that are only accepted during creation of an `EnvironmentInjector` (e.g. + * in an `NgModule`). + * + * Using this wrapper type prevents providers which are only designed to work in + * application/environment injectors from being accidentally included in + * `@Component.providers` and ending up in a component injector. + * + * This wrapper type prevents access to the `Provider`s inside. + * + * @see `makeEnvironmentProviders` + * @see `importProvidersFrom` + * + * @publicApi + */ +export type EnvironmentProviders = { + ɵbrand: 'EnvironmentProviders'; +}; + +export interface InternalEnvironmentProviders extends EnvironmentProviders { + ɵproviders: Provider[]; + + /** + * If present, indicates that the `EnvironmentProviders` were derived from NgModule providers. + * + * This is used to produce clearer error messages. + */ + ɵfromNgModule?: true; +} + +export function isEnvironmentProviders(value: Provider|InternalEnvironmentProviders): + value is InternalEnvironmentProviders { + return value && !!(value as InternalEnvironmentProviders).ɵproviders; +} + /** * Describes a function that is used to process provider lists (such as provider * overrides). diff --git a/packages/core/src/di/provider_collection.ts b/packages/core/src/di/provider_collection.ts index d5468dc79b49d..5151a86baf8d3 100644 --- a/packages/core/src/di/provider_collection.ts +++ b/packages/core/src/di/provider_collection.ts @@ -21,9 +21,19 @@ import {resolveForwardRef} from './forward_ref'; import {ENVIRONMENT_INITIALIZER} from './initializer_token'; import {ɵɵinject as inject} from './injector_compatibility'; import {getInjectorDef, InjectorType, InjectorTypeWithProviders} from './interface/defs'; -import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, ImportedNgModuleProviders, ModuleWithProviders, Provider, StaticClassProvider, TypeProvider, ValueProvider} from './interface/provider'; +import {ClassProvider, ConstructorProvider, EnvironmentProviders, ExistingProvider, FactoryProvider, ImportedNgModuleProviders, ModuleWithProviders, Provider, StaticClassProvider, TypeProvider, ValueProvider} from './interface/provider'; import {INJECTOR_DEF_TYPES} from './internal_tokens'; +/** + * Wrap an array of `Provider`s into `EnvironmentProviders`, preventing them from being accidentally + * referenced in `@Component in a component injector. + */ +export function makeEnvironmentProviders(providers: Provider[]): EnvironmentProviders { + return { + ɵproviders: providers, + } as unknown as EnvironmentProviders; +} + /** * A source of providers for the `importProvidersFrom` function. * From 4c71d6a76f2b03116609554281d7ef13e6882ca6 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 5 Oct 2022 12:13:06 -0700 Subject: [PATCH 2/4] refactor(core): support `EnvironmentProviders` types internally This commit modifies `R3Injector` and other code in Angular that deals with providers, to handle `EnvironmentProviders` objects as well as normal `Provider` types. There is no user-visible impact to this change, but it prepares the core of the DI system for the introduction of `EnvironmentProviders` as a public feature. --- packages/core/src/di/provider_collection.ts | 30 +++++++--- packages/core/src/di/r3_injector.ts | 22 ++++--- packages/core/src/render3/errors_di.ts | 16 ++++-- .../animations/bundle.golden_symbols.json | 6 ++ .../cyclic_import/bundle.golden_symbols.json | 6 ++ .../forms_reactive/bundle.golden_symbols.json | 6 ++ .../bundle.golden_symbols.json | 6 ++ .../hello_world/bundle.golden_symbols.json | 6 ++ .../injection/bundle.golden_symbols.json | 6 ++ .../router/bundle.golden_symbols.json | 6 ++ .../bundle.golden_symbols.json | 6 ++ .../bundling/todo/bundle.golden_symbols.json | 6 ++ .../core/testing/src/test_bed_compiler.ts | 57 ++++++++++++++----- 13 files changed, 143 insertions(+), 36 deletions(-) diff --git a/packages/core/src/di/provider_collection.ts b/packages/core/src/di/provider_collection.ts index 5151a86baf8d3..bfccf84cdebba 100644 --- a/packages/core/src/di/provider_collection.ts +++ b/packages/core/src/di/provider_collection.ts @@ -21,7 +21,7 @@ import {resolveForwardRef} from './forward_ref'; import {ENVIRONMENT_INITIALIZER} from './initializer_token'; import {ɵɵinject as inject} from './injector_compatibility'; import {getInjectorDef, InjectorType, InjectorTypeWithProviders} from './interface/defs'; -import {ClassProvider, ConstructorProvider, EnvironmentProviders, ExistingProvider, FactoryProvider, ImportedNgModuleProviders, ModuleWithProviders, Provider, StaticClassProvider, TypeProvider, ValueProvider} from './interface/provider'; +import {ClassProvider, ConstructorProvider, EnvironmentProviders, ExistingProvider, FactoryProvider, ImportedNgModuleProviders, InternalEnvironmentProviders, isEnvironmentProviders, ModuleWithProviders, Provider, StaticClassProvider, TypeProvider, ValueProvider} from './interface/provider'; import {INJECTOR_DEF_TYPES} from './internal_tokens'; /** @@ -128,7 +128,7 @@ function processInjectorTypesWithProviders( typesWithProviders: InjectorTypeWithProviders[], providersOut: Provider[]): void { for (let i = 0; i < typesWithProviders.length; i++) { const {ngModule, providers} = typesWithProviders[i]; - deepForEach(providers!, provider => { + deepForEachProvider(providers! as Array, provider => { ngDevMode && validateProvider(provider, providers || EMPTY_ARRAY, ngModule); providersOut.push(provider); }); @@ -261,12 +261,12 @@ export function walkProviderTree( } // Next, include providers listed on the definition itself. - const defProviders = injDef.providers; + const defProviders = injDef.providers as Array; if (defProviders != null && !isDuplicate) { const injectorType = container as InjectorType; - deepForEach(defProviders, provider => { - ngDevMode && validateProvider(provider, defProviders as SingleProvider[], injectorType); - providersOut.push(provider); + deepForEachProvider(defProviders, provider => { + ngDevMode && validateProvider(provider as SingleProvider, defProviders, injectorType); + providersOut.push(provider as SingleProvider); }); } } else { @@ -280,7 +280,8 @@ export function walkProviderTree( } function validateProvider( - provider: SingleProvider, providers: SingleProvider[], containerType: Type): void { + provider: SingleProvider, providers: Array, + containerType: Type): void { if (isTypeProvider(provider) || isValueProvider(provider) || isFactoryProvider(provider) || isExistingProvider(provider)) { return; @@ -294,6 +295,21 @@ function validateProvider( } } +function deepForEachProvider( + providers: Array, + fn: (provider: SingleProvider) => void): void { + for (let provider of providers) { + if (isEnvironmentProviders(provider)) { + provider = provider.ɵproviders; + } + if (Array.isArray(provider)) { + deepForEachProvider(provider, fn); + } else { + fn(provider); + } + } +} + export const USE_VALUE = getClosureSafeProperty({provide: String, useValue: getClosureSafeProperty}); diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index b175be228160b..df67dd2fc34b5 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -27,7 +27,7 @@ import {catchInjectorError, convertToBitFlags, injectArgs, NG_TEMP_TOKEN_PATH, s import {INJECTOR} from './injector_token'; import {getInheritedInjectableDef, getInjectableDef, InjectorType, ɵɵInjectableDeclaration} from './interface/defs'; import {InjectFlags, InjectOptions} from './interface/injector'; -import {ClassProvider, ConstructorProvider, ImportedNgModuleProviders, Provider, StaticClassProvider} from './interface/provider'; +import {ClassProvider, ConstructorProvider, EnvironmentProviders, ImportedNgModuleProviders, InternalEnvironmentProviders, isEnvironmentProviders, Provider, StaticClassProvider} from './interface/provider'; import {INJECTOR_DEF_TYPES} from './internal_tokens'; import {NullInjector} from './null_injector'; import {isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider, SingleProvider} from './provider_collection'; @@ -157,11 +157,14 @@ export class R3Injector extends EnvironmentInjector { private injectorDefTypes: Set>; constructor( - providers: Array, readonly parent: Injector, - readonly source: string|null, readonly scopes: Set) { + providers: Array, + readonly parent: Injector, readonly source: string|null, + readonly scopes: Set) { super(); // Start off by creating Records for every provider. - forEachSingleProvider(providers, provider => this.processProvider(provider)); + forEachSingleProvider( + providers as Array, + provider => this.processProvider(provider)); // Make sure the INJECTOR token provides this injector. this.records.set(INJECTOR, makeRecord(undefined, this)); @@ -460,7 +463,7 @@ function providerToRecord(provider: SingleProvider): Record { export function providerToFactory( provider: SingleProvider, ngModuleType?: InjectorType, providers?: any[]): () => any { let factory: (() => any)|undefined = undefined; - if (ngDevMode && isImportedNgModuleProviders(provider)) { + if (ngDevMode && (isImportedNgModuleProviders(provider) || isEnvironmentProviders(provider))) { throwInvalidProviderError(undefined, providers, provider); } @@ -517,19 +520,20 @@ function couldBeInjectableType(value: any): value is ProviderToken { function isImportedNgModuleProviders(provider: Provider|ImportedNgModuleProviders): provider is ImportedNgModuleProviders { - return !!(provider as ImportedNgModuleProviders).ɵproviders; + return provider && !!(provider as ImportedNgModuleProviders).ɵproviders; } function forEachSingleProvider( - providers: Array, + providers: Array, fn: (provider: SingleProvider) => void): void { for (const provider of providers) { if (Array.isArray(provider)) { forEachSingleProvider(provider, fn); - } else if (isImportedNgModuleProviders(provider)) { + } else if ( + provider && (isImportedNgModuleProviders(provider) || isEnvironmentProviders(provider))) { forEachSingleProvider(provider.ɵproviders, fn); } else { - fn(provider); + fn(provider as SingleProvider); } } } diff --git a/packages/core/src/render3/errors_di.ts b/packages/core/src/render3/errors_di.ts index 5811435154498..b24bb2601675d 100644 --- a/packages/core/src/render3/errors_di.ts +++ b/packages/core/src/render3/errors_di.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ImportedNgModuleProviders} from '../di/interface/provider'; +import {isEnvironmentProviders} from '../di/interface/provider'; import {RuntimeError, RuntimeErrorCode} from '../errors'; import {Type} from '../interface/type'; import {stringify} from '../util/stringify'; @@ -33,10 +33,16 @@ export function throwInvalidProviderError( throw new Error(`Invalid provider for the NgModule '${ stringify(ngModuleType)}' - only instances of Provider and Type are allowed, got: [${ providerDetail.join(', ')}]`); - } else if ((provider as ImportedNgModuleProviders).ɵproviders) { - throw new RuntimeError( - RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT, - `Invalid providers from 'importProvidersFrom' present in a non-environment injector. 'importProvidersFrom' can't be used for component providers.`); + } else if (isEnvironmentProviders(provider)) { + if (provider.ɵfromNgModule) { + throw new RuntimeError( + RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT, + `Invalid providers from 'importProvidersFrom' present in a non-environment injector. 'importProvidersFrom' can't be used for component providers.`); + } else { + throw new RuntimeError( + RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT, + `Invalid providers present in a non-environment injector. 'EnvironmentProviders' can't be used for component providers.`); + } } else { throw new Error('Invalid provider'); } diff --git a/packages/core/test/bundling/animations/bundle.golden_symbols.json b/packages/core/test/bundling/animations/bundle.golden_symbols.json index 547c00771d376..315bac4c8cc6f 100644 --- a/packages/core/test/bundling/animations/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations/bundle.golden_symbols.json @@ -710,6 +710,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "detachMovedView" }, @@ -1001,6 +1004,9 @@ { "name": "isElementNode" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFunction" }, diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 901b1c97a972c..5ea4235ef520e 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -497,6 +497,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "detachMovedView" }, @@ -749,6 +752,9 @@ { "name": "isCurrentTNodeParent" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFunction" }, diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index cc34b5820d893..3318dd0dd5d44 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -722,6 +722,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "defaultIterableDiffersFactory" }, @@ -1097,6 +1100,9 @@ { "name": "isEmptyInputValue" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFormControlState" }, diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index 24014002c6e70..dbf07f3c1eb76 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -692,6 +692,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "defaultIterableDiffersFactory" }, @@ -1055,6 +1058,9 @@ { "name": "isDirectiveHost" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFormControlState" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 0aaeb336a1f53..9ea32f8b839e8 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -359,6 +359,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "detachMovedView" }, @@ -557,6 +560,9 @@ { "name": "isComponentDef" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFunction" }, diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json index 824d7bd0646e7..2d570fd5ad72c 100644 --- a/packages/core/test/bundling/injection/bundle.golden_symbols.json +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -83,6 +83,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "forEachSingleProvider" }, @@ -122,6 +125,9 @@ { "name": "internalImportProvidersFrom" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isImportedNgModuleProviders" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 7a96b86812794..a22647464690e 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -884,6 +884,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "defaultErrorFactory" }, @@ -1340,6 +1343,9 @@ { "name": "isEmptyError" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFunction" }, diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json index e6cc180981bef..9b2948504fb41 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -428,6 +428,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "detachMovedView" }, @@ -641,6 +644,9 @@ { "name": "isComponentDef" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFunction" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index be63cdaaa1e04..525cfb713254f 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -602,6 +602,9 @@ { "name": "deepForEach" }, + { + "name": "deepForEachProvider" + }, { "name": "defaultIterableDiffersFactory" }, @@ -926,6 +929,9 @@ { "name": "isDirectiveHost" }, + { + "name": "isEnvironmentProviders" + }, { "name": "isFunction" }, diff --git a/packages/core/testing/src/test_bed_compiler.ts b/packages/core/testing/src/test_bed_compiler.ts index 57cd4b3f16fb7..f526c1d201339 100644 --- a/packages/core/testing/src/test_bed_compiler.ts +++ b/packages/core/testing/src/test_bed_compiler.ts @@ -7,7 +7,7 @@ */ import {ResourceLoader} from '@angular/compiler'; -import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, resolveForwardRef, Type, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵgetInjectableDef as getInjectableDef, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core'; +import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, resolveForwardRef, Type, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵgetInjectableDef as getInjectableDef, ɵInternalEnvironmentProviders as InternalEnvironmentProviders, ɵisEnvironmentProviders as isEnvironmentProviders, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core'; import {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading'; import {ComponentDef, ComponentType} from '../../src/render3'; @@ -470,7 +470,7 @@ export class TestBedCompiler { this.applyProviderOverridesInScope(dependency); } } else { - const providers = [ + const providers: Array = [ ...injectorDef.providers, ...(this.providerOverridesByModule.get(type as InjectorType) || []) ]; @@ -496,7 +496,8 @@ export class TestBedCompiler { fieldName: 'providers', originalValue: importedModule.providers }); - importedModule.providers = this.getOverriddenProviders(importedModule.providers); + importedModule.providers = this.getOverriddenProviders( + importedModule.providers as Array); } } } @@ -797,21 +798,23 @@ export class TestBedCompiler { return this.providerOverridesByToken.get(token) || null; } - private getProviderOverrides(providers?: Provider[]): Provider[] { + private getProviderOverrides(providers?: Array): + Provider[] { if (!providers || !providers.length || this.providerOverridesByToken.size === 0) return []; - // There are two flattening operations here. The inner flatten() operates on the metadata's - // providers and applies a mapping function which retrieves overrides for each incoming - // provider. The outer flatten() then flattens the produced overrides array. If this is not - // done, the array can contain other empty arrays (e.g. `[[], []]`) which leak into the + // There are two flattening operations here. The inner flattenProviders() operates on the + // metadata's providers and applies a mapping function which retrieves overrides for each + // incoming provider. The outer flatten() then flattens the produced overrides array. If this is + // not done, the array can contain other empty arrays (e.g. `[[], []]`) which leak into the // providers array and contaminate any error messages that might be generated. - return flatten(flatten( + return flatten(flattenProviders( providers, (provider: Provider) => this.getSingleProviderOverrides(provider) || [])); } - private getOverriddenProviders(providers?: Provider[]): Provider[] { + private getOverriddenProviders(providers?: Array): + Provider[] { if (!providers || !providers.length || this.providerOverridesByToken.size === 0) return []; - const flattenedProviders = flatten(providers); + const flattenedProviders = flattenProviders(providers); const overrides = this.getProviderOverrides(flattenedProviders); const overriddenProviders = [...flattenedProviders, ...overrides]; const final: Provider[] = []; @@ -838,7 +841,7 @@ export class TestBedCompiler { return final; } - private hasProviderOverrides(providers?: Provider[]): boolean { + private hasProviderOverrides(providers?: Array): boolean { return this.getProviderOverrides(providers).length > 0; } @@ -887,18 +890,42 @@ function maybeUnwrapFn(maybeFn: (() => T)|T): T { return maybeFn instanceof Function ? maybeFn() : maybeFn; } -function flatten(values: any[], mapFn?: (value: T) => any): T[] { +function flatten(values: any[]): T[] { const out: T[] = []; values.forEach(value => { if (Array.isArray(value)) { - out.push(...flatten(value, mapFn)); + out.push(...flatten(value)); } else { - out.push(mapFn ? mapFn(value) : value); + out.push(value); } }); return out; } +function identityFn(value: T): T { + return value; +} + +function flattenProviders( + providers: Array, mapFn: (provider: Provider) => T): T[]; +function flattenProviders(providers: Array): Provider[]; +function flattenProviders( + providers: Array, + mapFn: (provider: Provider) => any = identityFn): any[] { + const out: any[] = []; + for (let provider of providers) { + if (isEnvironmentProviders(provider)) { + provider = provider.ɵproviders; + } + if (Array.isArray(provider)) { + out.push(...flattenProviders(provider, mapFn)); + } else { + out.push(mapFn(provider)); + } + } + return out; +} + function getProviderField(provider: Provider, field: string) { return provider && typeof provider === 'object' && (provider as any)[field]; } From 6010c0fc0a8ddd4b0c24252ab5551747c9cb98e0 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 5 Oct 2022 12:14:41 -0700 Subject: [PATCH 3/4] feat(core): introduce EnvironmentProviders wrapper type This commit introduces a new type `EnvironmentProviders` which can be used in contexts where Angular accepted `Provider`s destined for `EnvironmentInjector`s. This includes contexts such as `@NgModule.providers` and `Route.providers`. The new type is useful for preventing such providers from accidentally ending up in `@Component.providers`. It can be used as the return type of provider functions (such as `provideRouter`) to enforce this safety. Because `Provider` allows `any[]` nested arrays, the compile-time safety provided by `EnvironmentProviders` is easily circumvented. However, the runtime shape of `EnvironmentProviders` is not compatible with component injectors and will result in a runtime error if it leaks through (NG0207). A new function `makeEnvironmentProviders` is used to construct this new type from an array of providers. The existing `importProvidersFrom` operation previously returned a very similar type `ImportedNgModuleProviders` which had the same goal. This machinery is switched over to use the new `EnvironmentProviders` interface instead (in fact, `ImportedNgModuleProviders` is now just an alias to `EnvironmentProviders`). --- goldens/public-api/core/index.md | 23 +++-- goldens/public-api/platform-browser/index.md | 4 +- goldens/public-api/platform-server/index.md | 4 +- goldens/public-api/router/index.md | 4 +- packages/core/src/application_ref.ts | 4 +- packages/core/src/di/index.ts | 4 +- packages/core/src/di/interface/defs.ts | 6 +- packages/core/src/di/interface/provider.ts | 8 +- packages/core/src/di/provider_collection.ts | 8 +- packages/core/src/di/r3_injector.ts | 21 ++--- packages/core/src/metadata/ng_module.ts | 4 +- packages/core/src/render3/ng_module_ref.ts | 6 +- packages/core/test/acceptance/di_spec.ts | 94 ++++++++++++++++++- .../animations/bundle.golden_symbols.json | 3 - .../cyclic_import/bundle.golden_symbols.json | 3 - .../forms_reactive/bundle.golden_symbols.json | 3 - .../bundle.golden_symbols.json | 3 - .../hello_world/bundle.golden_symbols.json | 3 - .../injection/bundle.golden_symbols.json | 3 - .../router/bundle.golden_symbols.json | 3 - .../bundle.golden_symbols.json | 3 - .../bundling/todo/bundle.golden_symbols.json | 3 - packages/platform-browser/src/browser.ts | 4 +- packages/platform-server/src/utils.ts | 4 +- packages/router/src/models.ts | 4 +- 25 files changed, 145 insertions(+), 84 deletions(-) diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index 452b862a8033c..cf9d606a8f902 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -324,7 +324,7 @@ export function createComponent(component: Type, options: { }): ComponentRef; // @public -export function createEnvironmentInjector(providers: Array, parent: EnvironmentInjector, debugName?: string | null): EnvironmentInjector; +export function createEnvironmentInjector(providers: Array, parent: EnvironmentInjector, debugName?: string | null): EnvironmentInjector; // @public export function createNgModule(ngModule: Type, parentInjector?: Injector): NgModuleRef; @@ -504,6 +504,11 @@ export abstract class EnvironmentInjector implements Injector { abstract runInContext(fn: () => ReturnT): ReturnT; } +// @public +export type EnvironmentProviders = { + ɵbrand: 'EnvironmentProviders'; +}; + // @public export class ErrorHandler { // (undocumented) @@ -622,14 +627,11 @@ export interface HostListenerDecorator { new (eventName: string, args?: string[]): any; } -// @public -export interface ImportedNgModuleProviders { - // (undocumented) - ɵproviders: Provider[]; -} +// @public @deprecated +export type ImportedNgModuleProviders = EnvironmentProviders; // @public -export function importProvidersFrom(...sources: ImportProvidersSource[]): ImportedNgModuleProviders; +export function importProvidersFrom(...sources: ImportProvidersSource[]): EnvironmentProviders; // @public export type ImportProvidersSource = Type | ModuleWithProviders | Array; @@ -873,6 +875,9 @@ export class KeyValueDiffers { // @public export const LOCALE_ID: InjectionToken; +// @public +export function makeEnvironmentProviders(providers: Provider[]): EnvironmentProviders; + // @public export enum MissingTranslationStrategy { // (undocumented) @@ -897,7 +902,7 @@ export interface ModuleWithProviders { // (undocumented) ngModule: Type; // (undocumented) - providers?: Provider[]; + providers?: Array; } // @public @@ -913,7 +918,7 @@ export interface NgModule { id?: string; imports?: Array | ModuleWithProviders<{}> | any[]>; jit?: true; - providers?: Provider[]; + providers?: Array; schemas?: Array; } diff --git a/goldens/public-api/platform-browser/index.md b/goldens/public-api/platform-browser/index.md index 898f22c2d5e98..5445faf6a1051 100644 --- a/goldens/public-api/platform-browser/index.md +++ b/goldens/public-api/platform-browser/index.md @@ -8,9 +8,9 @@ import { ApplicationRef } from '@angular/core'; import { ComponentRef } from '@angular/core'; import { DebugElement } from '@angular/core'; import { DebugNode } from '@angular/core'; +import { EnvironmentProviders } from '@angular/core'; import * as i0 from '@angular/core'; import * as i1 from '@angular/common'; -import { ImportedNgModuleProviders } from '@angular/core'; import { InjectionToken } from '@angular/core'; import { ModuleWithProviders } from '@angular/core'; import { NgZone } from '@angular/core'; @@ -25,7 +25,7 @@ import { Version } from '@angular/core'; // @public export interface ApplicationConfig { - providers: Array; + providers: Array; } // @public diff --git a/goldens/public-api/platform-server/index.md b/goldens/public-api/platform-server/index.md index 29ddeebff7212..a083adcc15a00 100644 --- a/goldens/public-api/platform-server/index.md +++ b/goldens/public-api/platform-server/index.md @@ -4,11 +4,11 @@ ```ts +import { EnvironmentProviders } from '@angular/core'; import * as i0 from '@angular/core'; import * as i1 from '@angular/common/http'; import * as i2 from '@angular/platform-browser/animations'; import * as i3 from '@angular/platform-browser'; -import { ImportedNgModuleProviders } from '@angular/core'; import { InjectionToken } from '@angular/core'; import { NgModuleFactory } from '@angular/core'; import { PlatformRef } from '@angular/core'; @@ -53,7 +53,7 @@ export function renderApplication(rootComponent: Type, options: { appId: string; document?: string | Document; url?: string; - providers?: Array; + providers?: Array; platformProviders?: Provider[]; }): Promise; diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index cf5297b95cfb2..a4d1d4268f370 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -11,9 +11,9 @@ import { ComponentFactoryResolver } from '@angular/core'; import { ComponentRef } from '@angular/core'; import { ElementRef } from '@angular/core'; import { EnvironmentInjector } from '@angular/core'; +import { EnvironmentProviders } from '@angular/core'; import { EventEmitter } from '@angular/core'; import * as i0 from '@angular/core'; -import { ImportedNgModuleProviders } from '@angular/core'; import { InjectionToken } from '@angular/core'; import { Injector } from '@angular/core'; import { Location as Location_2 } from '@angular/common'; @@ -596,7 +596,7 @@ export interface Route { outlet?: string; path?: string; pathMatch?: 'prefix' | 'full'; - providers?: Array; + providers?: Array; redirectTo?: string; resolve?: ResolveData; runGuardsAndResolvers?: RunGuardsAndResolvers; diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index 789c7ee615a30..a16e2107d5c31 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -18,7 +18,7 @@ import {Console} from './console'; import {Injectable} from './di/injectable'; import {InjectionToken} from './di/injection_token'; import {Injector} from './di/injector'; -import {ImportedNgModuleProviders, Provider, StaticProvider} from './di/interface/provider'; +import {EnvironmentProviders, Provider, StaticProvider} from './di/interface/provider'; import {EnvironmentInjector} from './di/r3_injector'; import {INJECTOR_SCOPE} from './di/scope'; import {ErrorHandler} from './error_handler'; @@ -189,7 +189,7 @@ export function runPlatformInitializers(injector: Injector): void { */ export function internalCreateApplication(config: { rootComponent?: Type, - appProviders?: Array, + appProviders?: Array, platformProviders?: Provider[], }): Promise { const {rootComponent, appProviders, platformProviders} = config; diff --git a/packages/core/src/di/index.ts b/packages/core/src/di/index.ts index c6948e4dd0334..6e501ca23fa9c 100644 --- a/packages/core/src/di/index.ts +++ b/packages/core/src/di/index.ts @@ -19,14 +19,14 @@ export {forwardRef, resolveForwardRef, ForwardRefFn} from './forward_ref'; export {Injectable, InjectableDecorator, InjectableProvider} from './injectable'; export {Injector} from './injector'; export {EnvironmentInjector} from './r3_injector'; -export {importProvidersFrom, ImportProvidersSource} from './provider_collection'; +export {importProvidersFrom, ImportProvidersSource, makeEnvironmentProviders} from './provider_collection'; export {ENVIRONMENT_INITIALIZER} from './initializer_token'; export {ProviderToken} from './provider_token'; export {ɵɵinject, inject, ɵɵinvalidFactoryDep} from './injector_compatibility'; export {InjectOptions} from './interface/injector'; export {INJECTOR} from './injector_token'; export {ReflectiveInjector} from './reflective_injector'; -export {ClassProvider, ModuleWithProviders, ClassSansProvider, ImportedNgModuleProviders, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, Provider, StaticClassProvider, StaticClassSansProvider, StaticProvider, TypeProvider, ValueProvider, ValueSansProvider} from './interface/provider'; +export {ClassProvider, ModuleWithProviders, ClassSansProvider, ImportedNgModuleProviders, ConstructorProvider, EnvironmentProviders, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, Provider, StaticClassProvider, StaticClassSansProvider, StaticProvider, TypeProvider, ValueProvider, ValueSansProvider} from './interface/provider'; export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './reflective_provider'; export {ReflectiveKey} from './reflective_key'; export {InjectionToken} from './injection_token'; diff --git a/packages/core/src/di/interface/defs.ts b/packages/core/src/di/interface/defs.ts index 40af50381f1bd..0997718849128 100644 --- a/packages/core/src/di/interface/defs.ts +++ b/packages/core/src/di/interface/defs.ts @@ -9,7 +9,7 @@ import {Type} from '../../interface/type'; import {getClosureSafeProperty} from '../../util/property'; -import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, ValueProvider} from './provider'; +import {ClassProvider, ConstructorProvider, EnvironmentProviders, ExistingProvider, FactoryProvider, StaticClassProvider, ValueProvider} from './provider'; @@ -73,7 +73,7 @@ export interface ɵɵInjectorDef { // TODO(alxhub): Narrow down the type here once decorators properly change the return type of the // class they are decorating (to add the ɵprov property for example). providers: (Type|ValueProvider|ExistingProvider|FactoryProvider|ConstructorProvider| - StaticClassProvider|ClassProvider|any[])[]; + StaticClassProvider|ClassProvider|EnvironmentProviders|any[])[]; imports: (InjectorType|InjectorTypeWithProviders)[]; } @@ -119,7 +119,7 @@ export interface InjectorType extends Type { export interface InjectorTypeWithProviders { ngModule: InjectorType; providers?: (Type|ValueProvider|ExistingProvider|FactoryProvider|ConstructorProvider| - StaticClassProvider|ClassProvider|any[])[]; + StaticClassProvider|ClassProvider|EnvironmentProviders|any[])[]; } diff --git a/packages/core/src/di/interface/provider.ts b/packages/core/src/di/interface/provider.ts index f2a4be691969e..66537a10f1a06 100644 --- a/packages/core/src/di/interface/provider.ts +++ b/packages/core/src/di/interface/provider.ts @@ -384,7 +384,7 @@ export type ProcessProvidersFunction = (providers: Provider[]) => Provider[]; */ export interface ModuleWithProviders { ngModule: Type; - providers?: Provider[]; + providers?: Array; } /** @@ -399,8 +399,6 @@ export interface ModuleWithProviders { * @see `importProvidersFrom` * * @publicApi - * @developerPreview + * @deprecated replaced by `EnvironmentProviders` */ -export interface ImportedNgModuleProviders { - ɵproviders: Provider[]; -} +export type ImportedNgModuleProviders = EnvironmentProviders; diff --git a/packages/core/src/di/provider_collection.ts b/packages/core/src/di/provider_collection.ts index bfccf84cdebba..241d2c5dfd2dd 100644 --- a/packages/core/src/di/provider_collection.ts +++ b/packages/core/src/di/provider_collection.ts @@ -84,9 +84,11 @@ export type ImportProvidersSource = * @publicApi * @developerPreview */ -export function importProvidersFrom(...sources: ImportProvidersSource[]): - ImportedNgModuleProviders { - return {ɵproviders: internalImportProvidersFrom(true, sources)}; +export function importProvidersFrom(...sources: ImportProvidersSource[]): EnvironmentProviders { + return { + ɵproviders: internalImportProvidersFrom(true, sources), + ɵfromNgModule: true, + } as InternalEnvironmentProviders; } export function internalImportProvidersFrom( diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index df67dd2fc34b5..a7505a0f53125 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -27,7 +27,7 @@ import {catchInjectorError, convertToBitFlags, injectArgs, NG_TEMP_TOKEN_PATH, s import {INJECTOR} from './injector_token'; import {getInheritedInjectableDef, getInjectableDef, InjectorType, ɵɵInjectableDeclaration} from './interface/defs'; import {InjectFlags, InjectOptions} from './interface/injector'; -import {ClassProvider, ConstructorProvider, EnvironmentProviders, ImportedNgModuleProviders, InternalEnvironmentProviders, isEnvironmentProviders, Provider, StaticClassProvider} from './interface/provider'; +import {ClassProvider, ConstructorProvider, EnvironmentProviders, InternalEnvironmentProviders, isEnvironmentProviders, Provider, StaticClassProvider} from './interface/provider'; import {INJECTOR_DEF_TYPES} from './internal_tokens'; import {NullInjector} from './null_injector'; import {isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider, SingleProvider} from './provider_collection'; @@ -157,13 +157,12 @@ export class R3Injector extends EnvironmentInjector { private injectorDefTypes: Set>; constructor( - providers: Array, - readonly parent: Injector, readonly source: string|null, - readonly scopes: Set) { + providers: Array, readonly parent: Injector, + readonly source: string|null, readonly scopes: Set) { super(); // Start off by creating Records for every provider. forEachSingleProvider( - providers as Array, + providers as Array, provider => this.processProvider(provider)); // Make sure the INJECTOR token provides this injector. @@ -463,7 +462,7 @@ function providerToRecord(provider: SingleProvider): Record { export function providerToFactory( provider: SingleProvider, ngModuleType?: InjectorType, providers?: any[]): () => any { let factory: (() => any)|undefined = undefined; - if (ngDevMode && (isImportedNgModuleProviders(provider) || isEnvironmentProviders(provider))) { + if (ngDevMode && isEnvironmentProviders(provider)) { throwInvalidProviderError(undefined, providers, provider); } @@ -518,19 +517,13 @@ function couldBeInjectableType(value: any): value is ProviderToken { (typeof value === 'object' && value instanceof InjectionToken); } -function isImportedNgModuleProviders(provider: Provider|ImportedNgModuleProviders): - provider is ImportedNgModuleProviders { - return provider && !!(provider as ImportedNgModuleProviders).ɵproviders; -} - function forEachSingleProvider( - providers: Array, + providers: Array, fn: (provider: SingleProvider) => void): void { for (const provider of providers) { if (Array.isArray(provider)) { forEachSingleProvider(provider, fn); - } else if ( - provider && (isImportedNgModuleProviders(provider) || isEnvironmentProviders(provider))) { + } else if (provider && isEnvironmentProviders(provider)) { forEachSingleProvider(provider.ɵproviders, fn); } else { fn(provider as SingleProvider); diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index 6807526a3509a..ec22a5c3cee14 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ModuleWithProviders, Provider} from '../di/interface/provider'; +import {EnvironmentProviders, ModuleWithProviders, Provider} from '../di/interface/provider'; import {Type} from '../interface/type'; import {SchemaMetadata} from '../metadata/schema'; import {compileNgModule} from '../render3/jit/module'; @@ -79,7 +79,7 @@ export interface NgModule { * } * ``` */ - providers?: Provider[]; + providers?: Array; /** * The set of components, directives, and pipes ([declarables](guide/glossary#declarable)) diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts index 5eb7b722157b3..bd989c4e4a5b8 100644 --- a/packages/core/src/render3/ng_module_ref.ts +++ b/packages/core/src/render3/ng_module_ref.ts @@ -8,7 +8,7 @@ import {createInjectorWithoutInjectorInstances} from '../di/create_injector'; import {Injector} from '../di/injector'; -import {ImportedNgModuleProviders, Provider} from '../di/interface/provider'; +import {EnvironmentProviders, Provider} from '../di/interface/provider'; import {EnvironmentInjector, getNullInjector, R3Injector} from '../di/r3_injector'; import {Type} from '../interface/type'; import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; @@ -119,7 +119,7 @@ class EnvironmentNgModuleRefAdapter extends viewEngine_NgModuleRef { override readonly instance = null; constructor( - providers: Array, parent: EnvironmentInjector|null, + providers: Array, parent: EnvironmentInjector|null, source: string|null) { super(); const injector = new R3Injector( @@ -157,7 +157,7 @@ class EnvironmentNgModuleRefAdapter extends viewEngine_NgModuleRef { * @developerPreview */ export function createEnvironmentInjector( - providers: Array, parent: EnvironmentInjector, + providers: Array, parent: EnvironmentInjector, debugName: string|null = null): EnvironmentInjector { const adapter = new EnvironmentNgModuleRefAdapter(providers, parent, debugName); return adapter.injector; diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 72f516bc1e634..d0bd588354a1a 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE} from '@angular/core'; +import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, createNgModule, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, INJECTOR, Injector, Input, LOCALE_ID, makeEnvironmentProviders, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE, ɵInternalEnvironmentProviders as InternalEnvironmentProviders} from '@angular/core'; import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; @@ -24,7 +24,7 @@ const collectEnvironmentInitializerProviders = (providers: Provider[]) => getProvidersByToken(providers, ENVIRONMENT_INITIALIZER); function unwrappedImportProvidersFrom(...sources: ImportProvidersSource[]): Provider[] { - return importProvidersFrom(...sources).ɵproviders; + return (importProvidersFrom(...sources) as unknown as InternalEnvironmentProviders).ɵproviders; } describe('importProvidersFrom', () => { @@ -276,6 +276,96 @@ describe('importProvidersFrom', () => { }); }); +describe('EnvironmentProviders', () => { + const TOKEN = new InjectionToken('TOKEN'); + const environmentProviders = makeEnvironmentProviders([{ + provide: TOKEN, + useValue: 'token!', + }]); + + it('should be accepted by TestBed providers', () => { + TestBed.configureTestingModule({ + providers: [environmentProviders], + }); + + expect(TestBed.inject(TOKEN)).toEqual('token!'); + }); + + it('should be accepted by @NgModule & createNgModule', () => { + @NgModule({ + providers: [environmentProviders], + }) + class TestModule { + } + + const inj = createNgModule(TestModule).injector; + expect(inj.get(TOKEN)).toEqual('token!'); + }); + + it('should be accepted by @NgModule & TestBed imports', () => { + @NgModule({ + providers: [environmentProviders], + }) + class TestModule { + } + + TestBed.configureTestingModule({ + imports: [TestModule], + }); + + expect(TestBed.inject(TOKEN)).toEqual('token!'); + }); + + it('should be accepted in ModuleWithProviders & createNgModule', () => { + @NgModule({}) + class EmptyModule { + } + + const mwp: ModuleWithProviders = { + ngModule: EmptyModule, + providers: [environmentProviders], + }; + + @NgModule({ + imports: [mwp], + }) + class TestModule { + } + + const inj = createNgModule(TestModule).injector; + expect(inj.get(TOKEN)).toEqual('token!'); + }); + + it('should be accepted by createEnvironmentInjector', () => { + TestBed.configureTestingModule({}); + const inj = + createEnvironmentInjector([environmentProviders], TestBed.inject(EnvironmentInjector)); + expect(inj.get(TOKEN)).toEqual('token!'); + }); + + it('should be overridable by TestBed overrides', () => { + TestBed.configureTestingModule({ + providers: [environmentProviders], + }); + TestBed.overrideProvider(TOKEN, { + useValue: 'overridden!', + }); + + expect(TestBed.inject(TOKEN)).toEqual('overridden!'); + }); + + it('should be rejected by @Component.providers', () => { + @Component({ + providers: [environmentProviders as any], + }) + class TestCmp { + readonly token = inject(TOKEN); + } + + expect(() => TestBed.createComponent(TestCmp)).toThrowError(/NG0207/); + }); +}); + describe('di', () => { describe('no dependencies', () => { it('should create directive with no deps', () => { diff --git a/packages/core/test/bundling/animations/bundle.golden_symbols.json b/packages/core/test/bundling/animations/bundle.golden_symbols.json index 315bac4c8cc6f..8428e0ae89bbd 100644 --- a/packages/core/test/bundling/animations/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations/bundle.golden_symbols.json @@ -1010,9 +1010,6 @@ { "name": "isFunction" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 5ea4235ef520e..eab3aa9ebf4d8 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -758,9 +758,6 @@ { "name": "isFunction" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index 3318dd0dd5d44..56cdf848bc0ca 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -1112,9 +1112,6 @@ { "name": "isFunction" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index dbf07f3c1eb76..06ed7b924458a 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -1070,9 +1070,6 @@ { "name": "isFunction" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 9ea32f8b839e8..c48039ad2d8ee 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -566,9 +566,6 @@ { "name": "isFunction" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json index 2d570fd5ad72c..09516a59acadf 100644 --- a/packages/core/test/bundling/injection/bundle.golden_symbols.json +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -128,9 +128,6 @@ { "name": "isEnvironmentProviders" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isTypeProvider" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index a22647464690e..616bd0f65521d 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -1355,9 +1355,6 @@ { "name": "isImmediateMatch" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json index 9b2948504fb41..3396a30561780 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -650,9 +650,6 @@ { "name": "isFunction" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 525cfb713254f..6b32a8b840355 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -935,9 +935,6 @@ { "name": "isFunction" }, - { - "name": "isImportedNgModuleProviders" - }, { "name": "isInlineTemplate" }, diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index 1ae8c9cc547f0..91cd3f8e41d57 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -7,7 +7,7 @@ */ import {CommonModule, DOCUMENT, XhrFactory, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; -import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, ImportedNgModuleProviders, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalCreateApplication as internalCreateApplication, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core'; +import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, EnvironmentProviders, ErrorHandler, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalCreateApplication as internalCreateApplication, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core'; import {BrowserDomAdapter} from './browser/browser_adapter'; import {SERVER_TRANSITION_PROVIDERS, TRANSITION_ID} from './browser/server-transition'; @@ -31,7 +31,7 @@ export interface ApplicationConfig { /** * List of providers that should be available to the root component and all its children. */ - providers: Array; + providers: Array; } /** diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index f429cace4e34a..db802b2e824ae 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, InjectionToken, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core'; +import {ApplicationRef, EnvironmentProviders, importProvidersFrom, InjectionToken, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core'; import {BrowserModule, ɵTRANSITION_ID} from '@angular/platform-browser'; import {first} from 'rxjs/operators'; @@ -190,7 +190,7 @@ export function renderApplication(rootComponent: Type, options: { appId: string, document?: string|Document, url?: string, - providers?: Array, + providers?: Array, platformProviders?: Provider[], }): Promise { const {document, url, platformProviders, appId} = options; diff --git a/packages/router/src/models.ts b/packages/router/src/models.ts index 48a20e0f9362b..b03366dc7904a 100644 --- a/packages/router/src/models.ts +++ b/packages/router/src/models.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {EnvironmentInjector, ImportedNgModuleProviders, InjectionToken, NgModuleFactory, Provider, ProviderToken, Type} from '@angular/core'; +import {EnvironmentInjector, EnvironmentProviders, InjectionToken, NgModuleFactory, Provider, ProviderToken, Type} from '@angular/core'; import {Observable} from 'rxjs'; import {DeprecatedLoadChildren} from './deprecated_load_children'; @@ -568,7 +568,7 @@ export interface Route { * route also has a `loadChildren` function which returns an `NgModuleRef`, this injector will be * used as the parent of the lazy loaded module. */ - providers?: Array; + providers?: Array; /** * Injector created from the static route providers From 640e59649e1a3ac1086af9021abe0a6686807c72 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 5 Oct 2022 13:17:37 -0700 Subject: [PATCH 4/4] feat(router): prevent `provideRouter()` from usage in @Component This commit switches `provideRouter()` to return the new `EnvironmentProviders` wrapper type, preventing it from being accidentally (or intentionally) included in `@Component.providers`. --- goldens/public-api/router/index.md | 2 +- packages/router/src/provide_router.ts | 8 ++++---- packages/router/testing/src/provide_router_for_testing.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index a4d1d4268f370..717d48214a81a 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -524,7 +524,7 @@ export abstract class PreloadingStrategy { export const PRIMARY_OUTLET = "primary"; // @public -export function provideRouter(routes: Routes, ...features: RouterFeatures[]): Provider[]; +export function provideRouter(routes: Routes, ...features: RouterFeatures[]): EnvironmentProviders; // @public export function provideRoutes(routes: Routes): Provider[]; diff --git a/packages/router/src/provide_router.ts b/packages/router/src/provide_router.ts index 0bd7e14e238d4..af9160b53aa0f 100644 --- a/packages/router/src/provide_router.ts +++ b/packages/router/src/provide_router.ts @@ -7,7 +7,7 @@ */ import {LOCATION_INITIALIZED, ViewportScroller} from '@angular/common'; -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentRef, ENVIRONMENT_INITIALIZER, inject, InjectFlags, InjectionToken, Injector, Provider, Type} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentRef, ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, InjectFlags, InjectionToken, Injector, makeEnvironmentProviders, Provider, Type} from '@angular/core'; import {of, Subject} from 'rxjs'; import {filter, map, take} from 'rxjs/operators'; @@ -59,8 +59,8 @@ const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode; * @param features Optional features to configure additional router behaviors. * @returns A set of providers to setup a Router. */ -export function provideRouter(routes: Routes, ...features: RouterFeatures[]): Provider[] { - return [ +export function provideRouter(routes: Routes, ...features: RouterFeatures[]): EnvironmentProviders { + return makeEnvironmentProviders([ provideRoutes(routes), {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]}, {provide: APP_BOOTSTRAP_LISTENER, multi: true, useFactory: getBootstrapListener}, features.map(feature => feature.ɵproviders), @@ -68,7 +68,7 @@ export function provideRouter(routes: Routes, ...features: RouterFeatures[]): Pr // how we want them to be configured. This API doesn't currently have a way to configure them // and we should decide what the _best_ way to do that is rather than just sticking with the // status quo of how it's done today. - ]; + ]); } export function rootRoute(router: Router): ActivatedRoute { diff --git a/packages/router/testing/src/provide_router_for_testing.ts b/packages/router/testing/src/provide_router_for_testing.ts index 09b2e1fa46155..5f9bf67f972e9 100644 --- a/packages/router/testing/src/provide_router_for_testing.ts +++ b/packages/router/testing/src/provide_router_for_testing.ts @@ -8,7 +8,7 @@ import {Location, LocationStrategy} from '@angular/common'; import {MockLocationStrategy, SpyLocation} from '@angular/common/testing'; -import {Provider} from '@angular/core'; +import {EnvironmentProviders, Provider} from '@angular/core'; import {provideRouter, RouterFeatures, Routes} from '@angular/router'; @@ -46,9 +46,9 @@ import {provideRouter, RouterFeatures, Routes} from '@angular/router'; * @returns A set of providers to setup Router for testing. */ export function provideRouterForTesting( - routes: Routes = [], ...features: RouterFeatures[]): Provider[] { + routes: Routes = [], ...features: RouterFeatures[]): (Provider|EnvironmentProviders)[] { return [ - ...provideRouter(routes, ...features), + provideRouter(routes, ...features), {provide: Location, useClass: SpyLocation}, {provide: LocationStrategy, useClass: MockLocationStrategy}, ];