diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel index daf7755a35..ec996eb8c1 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel @@ -16,6 +16,7 @@ ng_module( "//packages/core", "//packages/platform-browser", "//packages/platform-server", + "//packages/router", "@rxjs", ], ) diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts new file mode 100644 index 0000000000..d04e603618 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {APP_ROOT_SCOPE, Component, Injectable, NgModule, Optional, Self} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {ServerModule} from '@angular/platform-server'; +import {RouterModule} from '@angular/router'; + +import {LazyModuleNgFactory} from './root_lazy.ngfactory'; + +@Component({ + selector: 'root-app', + template: '', +}) +export class AppComponent { +} + +export function children(): any { + console.error('children', LazyModuleNgFactory); + return LazyModuleNgFactory; +} + + +@NgModule({ + imports: [ + BrowserModule.withServerTransition({appId: 'id-app'}), + ServerModule, + RouterModule.forRoot( + [ + {path: '', pathMatch: 'prefix', loadChildren: children}, + ], + {initialNavigation: 'enabled'}), + ], + declarations: [AppComponent], + bootstrap: [AppComponent], +}) +export class RootAppModule { +} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_lazy.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_lazy.ts new file mode 100644 index 0000000000..1a56081266 --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_lazy.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, NgModule, Optional, Self} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {Service} from './root_service'; + +@Component({ + selector: 'lazy-route', + template: '{{service}}:{{serviceInLazyInjector}}', +}) +export class RouteComponent { + service: boolean; + serviceInLazyInjector: boolean; + constructor(@Optional() service: Service, @Optional() @Self() lazyService: Service) { + this.service = !!service; + this.serviceInLazyInjector = !!lazyService; + } +} + +@NgModule({ + declarations: [RouteComponent], + imports: [ + RouterModule.forChild([ + {path: '', pathMatch: 'prefix', component: RouteComponent}, + ]), + ], +}) +export class LazyModule { +} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts new file mode 100644 index 0000000000..77a749b42f --- /dev/null +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {APP_ROOT_SCOPE, Injectable} from '@angular/core'; + +@Injectable({ + scope: APP_ROOT_SCOPE, +}) +export class Service { +} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts index 3cf74df8f9..907941877c 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts @@ -10,6 +10,7 @@ import {enableProdMode} from '@angular/core'; import {renderModuleFactory} from '@angular/platform-server'; import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory'; import {HierarchyAppModuleNgFactory} from 'app_built/src/hierarchy.ngfactory'; +import {RootAppModuleNgFactory} from 'app_built/src/root.ngfactory'; import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory'; import {TokenAppModuleNgFactory} from 'app_built/src/token.ngfactory'; @@ -55,4 +56,14 @@ describe('ngInjectableDef Bazel Integration', () => { done(); }); }); + + it('APP_ROOT_SCOPE works', done => { + renderModuleFactory(RootAppModuleNgFactory, { + document: '', + url: '/', + }).then(html => { + expect(html).toMatch(/>true:false<\//); + done(); + }); + }); }); diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index bb49c55376..79e4993603 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -23,3 +23,4 @@ export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provid export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider'; export {ReflectiveKey} from './di/reflective_key'; export {InjectionToken} from './di/injection_token'; +export {APP_ROOT_SCOPE} from './di/scope'; diff --git a/packages/core/src/di/scope.ts b/packages/core/src/di/scope.ts new file mode 100644 index 0000000000..b9f546ea50 --- /dev/null +++ b/packages/core/src/di/scope.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Type} from '../type'; +import {InjectionToken} from './injection_token'; + + +// APP_ROOT_SCOPE is cast as a Type to allow for its usage as the scope parameter of @Injectable(). + +/** + * A scope which targets the root injector. + * + * When specified as the `scope` parameter to `@Injectable` or `InjectionToken`, this special + * scope indicates the provider for the service or token being configured belongs in the root + * injector. This is loosely equivalent to the convention of having a `forRoot()` static + * function within a module that configures the provider, and expecting users to only import that + * module via its `forRoot()` function in the root injector. + * + * @experimental + */ +export const APP_ROOT_SCOPE: Type = new InjectionToken( + 'The presence of this token marks an injector as being the root injector.') as any; diff --git a/packages/core/src/view/ng_module.ts b/packages/core/src/view/ng_module.ts index 252edef56b..f145e6c4c4 100644 --- a/packages/core/src/view/ng_module.ts +++ b/packages/core/src/view/ng_module.ts @@ -8,6 +8,7 @@ import {resolveForwardRef} from '../di/forward_ref'; import {InjectFlags, Injector, setCurrentInjector} from '../di/injector'; +import {APP_ROOT_SCOPE} from '../di/scope'; import {NgModuleRef} from '../linker/ng_module_factory'; import {stringify} from '../util'; @@ -43,8 +44,12 @@ export function moduleProvideDef( export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition { const providersByKey: {[key: string]: NgModuleProviderDef} = {}; const modules = []; + let isRoot: boolean = false; for (let i = 0; i < providers.length; i++) { const provider = providers[i]; + if (provider.token === APP_ROOT_SCOPE) { + isRoot = true; + } if (provider.flags & NodeFlags.TypeNgModule) { modules.push(provider.token); } @@ -57,6 +62,7 @@ export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition providersByKey, providers, modules, + isRoot, }; } @@ -119,8 +125,13 @@ export function resolveNgModuleDep( return data._parent.get(depDef.token, notFoundValue); } +function moduleTransitivelyPresent(ngModule: NgModuleData, scope: any): boolean { + return ngModule._def.modules.indexOf(scope) > -1; +} + function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean { - return def.scope != null && ngModule._def.modules.indexOf(def.scope) > -1; + return def.scope != null && (moduleTransitivelyPresent(ngModule, def.scope) || + def.scope === APP_ROOT_SCOPE && ngModule._def.isRoot); } function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any { diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index 44c190eb10..8fbbe55078 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -43,6 +43,7 @@ export interface NgModuleDefinition extends Definition {} diff --git a/packages/core/test/view/ng_module_spec.ts b/packages/core/test/view/ng_module_spec.ts index 5592356384..8e5c8c2ea2 100644 --- a/packages/core/test/view/ng_module_spec.ts +++ b/packages/core/test/view/ng_module_spec.ts @@ -97,7 +97,7 @@ function makeProviders(classes: any[], modules: any[]): NgModuleDefinition { })); const providersByKey: {[key: string]: NgModuleProviderDef} = {}; providers.forEach(provider => providersByKey[tokenKey(provider.token)] = provider); - return {factory: null, providers, providersByKey, modules}; + return {factory: null, providers, providersByKey, modules, isRoot: true}; } describe('NgModuleRef_ injector', () => { diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index 7043cdb8b5..7aaefaf46e 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -7,7 +7,7 @@ */ import {CommonModule, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; -import {APP_ID, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore} from '@angular/core'; +import {APP_ID, APP_ROOT_SCOPE, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore} from '@angular/core'; import {BrowserDomAdapter} from './browser/browser_adapter'; import {BrowserPlatformLocation} from './browser/location/browser_platform_location'; @@ -71,6 +71,7 @@ export function _document(): any { @NgModule({ providers: [ BROWSER_SANITIZATION_PROVIDERS, + {provide: APP_ROOT_SCOPE, useValue: true}, {provide: ErrorHandler, useFactory: errorHandler, deps: []}, {provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true}, {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true}, diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 3774caf383..e5bc87993e 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -112,6 +112,9 @@ export declare const APP_ID: InjectionToken; /** @experimental */ export declare const APP_INITIALIZER: InjectionToken<(() => void)[]>; +/** @experimental */ +export declare const APP_ROOT_SCOPE: Type; + /** @experimental */ export declare class ApplicationInitStatus { readonly done: boolean;