diff --git a/modules/@angular/core/src/application_ref.ts b/modules/@angular/core/src/application_ref.ts index 13b18dfca2..d84ec4d89e 100644 --- a/modules/@angular/core/src/application_ref.ts +++ b/modules/@angular/core/src/application_ref.ts @@ -37,6 +37,8 @@ let _devMode: boolean = true; let _runModeLocked: boolean = false; let _platform: PlatformRef; +export const ALLOW_MULTIPLE_PLATFORMS = new InjectionToken('AllowMultipleToken'); + /** * Disable Angular's development mode, which turns off assertions and other * checks within the framework. @@ -83,7 +85,8 @@ export class NgProbeToken { * @experimental APIs related to application bootstrap are currently under review. */ export function createPlatform(injector: Injector): PlatformRef { - if (_platform && !_platform.destroyed) { + if (_platform && !_platform.destroyed && + !_platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { throw new Error( 'There can be only one platform. Destroy the previous one to create a new one.'); } @@ -103,7 +106,8 @@ export function createPlatformFactory( providers: Provider[] = []): (extraProviders?: Provider[]) => PlatformRef { const marker = new InjectionToken(`Platform: ${name}`); return (extraProviders: Provider[] = []) => { - if (!getPlatform()) { + let platform = getPlatform(); + if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { parentPlatformFactory( providers.concat(extraProviders).concat({provide: marker, useValue: true})); @@ -117,8 +121,7 @@ export function createPlatformFactory( } /** - * Checks that there currently is a platform - * which contains the given token as a provider. + * Checks that there currently is a platform which contains the given token as a provider. * * @experimental APIs related to application bootstrap are currently under review. */ diff --git a/modules/@angular/core/src/core_private_export.ts b/modules/@angular/core/src/core_private_export.ts index ffb6c38894..0aaf2a9c1a 100644 --- a/modules/@angular/core/src/core_private_export.ts +++ b/modules/@angular/core/src/core_private_export.ts @@ -14,6 +14,7 @@ import {AnimationSequencePlayer as AnimationSequencePlayer_} from './animation/a import * as animationUtils from './animation/animation_style_util'; import {AnimationStyles as AnimationStyles_} from './animation/animation_styles'; import {AnimationTransition} from './animation/animation_transition'; +import {ALLOW_MULTIPLE_PLATFORMS} from './application_ref'; import * as application_tokens from './application_tokens'; import * as change_detection_util from './change_detection/change_detection_util'; import * as constants from './change_detection/constants'; @@ -124,7 +125,8 @@ export const __core_private__: { FILL_STYLE_FLAG: typeof FILL_STYLE_FLAG_, isPromise: typeof isPromise, isObservable: typeof isObservable, - AnimationTransition: typeof AnimationTransition + AnimationTransition: typeof AnimationTransition, + ALLOW_MULTIPLE_PLATFORMS: typeof ALLOW_MULTIPLE_PLATFORMS, view_utils: typeof view_utils, ERROR_COMPONENT_TYPE: typeof ERROR_COMPONENT_TYPE, viewEngine: typeof viewEngine, @@ -180,6 +182,7 @@ export const __core_private__: { isPromise: isPromise, isObservable: isObservable, AnimationTransition: AnimationTransition, + ALLOW_MULTIPLE_PLATFORMS: ALLOW_MULTIPLE_PLATFORMS, ERROR_COMPONENT_TYPE: ERROR_COMPONENT_TYPE, TransitionEngine: TransitionEngine } as any /* TODO(misko): export these using omega names instead */; diff --git a/modules/@angular/platform-server/src/private_import_core.ts b/modules/@angular/platform-server/src/private_import_core.ts index 2632bc431a..7419291eb0 100644 --- a/modules/@angular/platform-server/src/private_import_core.ts +++ b/modules/@angular/platform-server/src/private_import_core.ts @@ -23,3 +23,6 @@ export type DebugDomRootRenderer = typeof r._DebugDomRootRenderer; export const DebugDomRootRenderer: typeof r.DebugDomRootRenderer = r.DebugDomRootRenderer; export type DebugDomRendererV2 = typeof r._DebugDomRendererV2; export const DebugDomRendererV2: typeof r.DebugDomRendererV2 = r.DebugDomRendererV2; +export type ALLOW_MULTIPLE_PLATFORMS = typeof r.ALLOW_MULTIPLE_PLATFORMS; +export const ALLOW_MULTIPLE_PLATFORMS: typeof r.ALLOW_MULTIPLE_PLATFORMS = + r.ALLOW_MULTIPLE_PLATFORMS; diff --git a/modules/@angular/platform-server/src/server.ts b/modules/@angular/platform-server/src/server.ts index fb6263a0cb..764e31542e 100644 --- a/modules/@angular/platform-server/src/server.ts +++ b/modules/@angular/platform-server/src/server.ts @@ -14,7 +14,7 @@ import {BrowserModule, DOCUMENT} from '@angular/platform-browser'; import {ServerPlatformLocation} from './location'; import {Parse5DomAdapter, parseDocument} from './parse5_adapter'; import {PlatformState} from './platform_state'; -import {DebugDomRendererV2, DebugDomRootRenderer} from './private_import_core'; +import {ALLOW_MULTIPLE_PLATFORMS, DebugDomRendererV2, DebugDomRootRenderer} from './private_import_core'; import {SharedStylesHost, getDOM} from './private_import_platform-browser'; import {ServerRendererV2, ServerRootRenderer} from './server_renderer'; @@ -25,8 +25,9 @@ function notSupported(feature: string): Error { export const INTERNAL_SERVER_PLATFORM_PROVIDERS: Array = [ {provide: DOCUMENT, useFactory: _document, deps: [Injector]}, {provide: PLATFORM_INITIALIZER, useFactory: initParse5Adapter, multi: true, deps: [Injector]}, - {provide: PlatformLocation, useClass: ServerPlatformLocation}, - PlatformState, + {provide: PlatformLocation, useClass: ServerPlatformLocation}, PlatformState, + // Add special provider that allows multiple instances of platformServer* to be created. + {provide: ALLOW_MULTIPLE_PLATFORMS, useValue: true} ]; function initParse5Adapter(injector: Injector) { diff --git a/modules/@angular/platform-server/src/utils.ts b/modules/@angular/platform-server/src/utils.ts index c86ad3203b..e2002b334a 100644 --- a/modules/@angular/platform-server/src/utils.ts +++ b/modules/@angular/platform-server/src/utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Type, destroyPlatform} from '@angular/core'; +import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Type} from '@angular/core'; import {filter} from 'rxjs/operator/filter'; import {first} from 'rxjs/operator/first'; import {toPromise} from 'rxjs/operator/toPromise'; @@ -40,7 +40,7 @@ function _render( .call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable))) .then(() => { const output = platform.injector.get(PlatformState).renderToString(); - destroyPlatform(); + platform.destroy(); return output; }); }); diff --git a/modules/@angular/platform-server/test/integration_spec.ts b/modules/@angular/platform-server/test/integration_spec.ts index bc4b80427b..645c72ac76 100644 --- a/modules/@angular/platform-server/test/integration_spec.ts +++ b/modules/@angular/platform-server/test/integration_spec.ts @@ -25,126 +25,155 @@ class MyServerApp { class ExampleModule { } +@Component({selector: 'app', template: `Works too!`}) +class MyServerApp2 { +} + +@NgModule({declarations: [MyServerApp2], imports: [ServerModule], bootstrap: [MyServerApp2]}) +class ExampleModule2 { +} + +@Component({selector: 'app', template: '{{text}}'}) +class MyAsyncServerApp { + text = ''; + + ngOnInit() { + Promise.resolve(null).then(() => setTimeout(() => { this.text = 'Works!'; }, 10)); + } +} + +@NgModule( + {declarations: [MyAsyncServerApp], imports: [ServerModule], bootstrap: [MyAsyncServerApp]}) +class AsyncServerModule { +} + export function main() { if (getDOM().supportsDOMEvents()) return; // NODE only describe('platform-server integration', () => { - - beforeEach(() => destroyPlatform()); - afterEach(() => destroyPlatform()); + beforeEach(() => { + if (getPlatform()) destroyPlatform(); + }); it('should bootstrap', async(() => { - platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) - .bootstrapModule(ExampleModule) - .then((moduleRef) => { - const doc = moduleRef.injector.get(DOCUMENT); - expect(getDOM().getText(doc)).toEqual('Works!'); - }); + const platform = platformDynamicServer( + [{provide: INITIAL_CONFIG, useValue: {document: ''}}]); + + platform.bootstrapModule(ExampleModule).then((moduleRef) => { + const doc = moduleRef.injector.get(DOCUMENT); + expect(getDOM().getText(doc)).toEqual('Works!'); + platform.destroy(); + }); + })); + + it('should allow multiple platform instances', async(() => { + const platform = platformDynamicServer( + [{provide: INITIAL_CONFIG, useValue: {document: ''}}]); + + const platform2 = platformDynamicServer( + [{provide: INITIAL_CONFIG, useValue: {document: ''}}]); + + + platform.bootstrapModule(ExampleModule).then((moduleRef) => { + const doc = moduleRef.injector.get(DOCUMENT); + expect(getDOM().getText(doc)).toEqual('Works!'); + platform.destroy(); + }); + + platform2.bootstrapModule(ExampleModule2).then((moduleRef) => { + const doc = moduleRef.injector.get(DOCUMENT); + expect(getDOM().getText(doc)).toEqual('Works too!'); + platform2.destroy(); + }); })); describe('PlatformLocation', () => { - it('is injectable', () => { - platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) - .bootstrapModule(ExampleModule) - .then(appRef => { - const location: PlatformLocation = appRef.injector.get(PlatformLocation); - expect(location.pathname).toBe('/'); - }); - }); - it('pushState causes the URL to update', () => { - platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) - .bootstrapModule(ExampleModule) - .then(appRef => { - const location: PlatformLocation = appRef.injector.get(PlatformLocation); - location.pushState(null, 'Test', '/foo#bar'); - expect(location.pathname).toBe('/foo'); - expect(location.hash).toBe('#bar'); - }); - }); + it('is injectable', async(() => { + const platform = platformDynamicServer( + [{provide: INITIAL_CONFIG, useValue: {document: ''}}]); + platform.bootstrapModule(ExampleModule).then(appRef => { + const location: PlatformLocation = appRef.injector.get(PlatformLocation); + expect(location.pathname).toBe('/'); + platform.destroy(); + }); + })); + it('pushState causes the URL to update', async(() => { + const platform = platformDynamicServer( + [{provide: INITIAL_CONFIG, useValue: {document: ''}}]); + platform.bootstrapModule(ExampleModule).then(appRef => { + const location: PlatformLocation = appRef.injector.get(PlatformLocation); + location.pushState(null, 'Test', '/foo#bar'); + expect(location.pathname).toBe('/foo'); + expect(location.hash).toBe('#bar'); + platform.destroy(); + }); + })); it('allows subscription to the hash state', done => { - platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) - .bootstrapModule(ExampleModule) - .then(appRef => { - const location: PlatformLocation = appRef.injector.get(PlatformLocation); - expect(location.pathname).toBe('/'); - location.onHashChange((e: any) => { - expect(e.type).toBe('hashchange'); - expect(e.oldUrl).toBe('/'); - expect(e.newUrl).toBe('/foo#bar'); - done(); - }); - location.pushState(null, 'Test', '/foo#bar'); - }); + const platform = + platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]); + platform.bootstrapModule(ExampleModule).then(appRef => { + const location: PlatformLocation = appRef.injector.get(PlatformLocation); + expect(location.pathname).toBe('/'); + location.onHashChange((e: any) => { + expect(e.type).toBe('hashchange'); + expect(e.oldUrl).toBe('/'); + expect(e.newUrl).toBe('/foo#bar'); + platform.destroy(); + done(); + }); + location.pushState(null, 'Test', '/foo#bar'); + }); }); }); - }); - describe('Platform Server', () => { - @Component({selector: 'app', template: '{{text}}'}) - class MyAsyncServerApp { - text = ''; + describe('render', () => { + let doc: string; + let called: boolean; + let expectedOutput = + 'Works!'; - ngOnInit() { - Promise.resolve(null).then(() => setTimeout(() => { this.text = 'Works!'; }, 10)); - } - } + beforeEach(() => { + // PlatformConfig takes in a parsed document so that it can be cached across requests. + doc = ''; + called = false; + }); + afterEach(() => { expect(called).toBe(true); }); - @NgModule( - {declarations: [MyAsyncServerApp], imports: [ServerModule], bootstrap: [MyAsyncServerApp]}) - class AsyncServerModule { - } + it('using long from should work', async(() => { + const platform = + platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: doc}}]); - let doc: string; - let called: boolean; - let expectedOutput = - 'Works!'; + platform.bootstrapModule(AsyncServerModule) + .then((moduleRef) => { + const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); + return toPromise.call(first.call( + filter.call(applicationRef.isStable, (isStable: boolean) => isStable))); + }) + .then((b) => { + expect(platform.injector.get(PlatformState).renderToString()).toBe(expectedOutput); + platform.destroy(); + called = true; + }); + })); - beforeEach(() => { - destroyPlatform(); - // PlatformConfig takes in a parsed document so that it can be cached across requests. - doc = ''; - called = false; + it('using renderModule should work', async(() => { + renderModule(AsyncServerModule, {document: doc}).then(output => { + expect(output).toBe(expectedOutput); + called = true; + }); + })); + + it('using renderModuleFactory should work', + async(inject([PlatformRef], (defaultPlatform: PlatformRef) => { + const compilerFactory: CompilerFactory = + defaultPlatform.injector.get(CompilerFactory, null); + const moduleFactory = + compilerFactory.createCompiler().compileModuleSync(AsyncServerModule); + renderModuleFactory(moduleFactory, {document: doc}).then(output => { + expect(output).toBe(expectedOutput); + called = true; + }); + }))); }); - afterEach(() => { - expect(called).toBe(true); - // Platform should have been destroyed at the end of rendering. - expect(getPlatform()).toBeNull(); - }); - - it('PlatformState should render to string (Long form rendering)', async(() => { - const platform = - platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: doc}}]); - - platform.bootstrapModule(AsyncServerModule) - .then((moduleRef) => { - const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); - return toPromise.call(first.call( - filter.call(applicationRef.isStable, (isStable: boolean) => isStable))); - }) - .then((b) => { - expect(platform.injector.get(PlatformState).renderToString()).toBe(expectedOutput); - destroyPlatform(); - called = true; - }); - })); - - it('renderModule should render to string (short form rendering)', async(() => { - renderModule(AsyncServerModule, {document: doc}).then(output => { - expect(output).toBe(expectedOutput); - called = true; - }); - })); - - it('renderModuleFactory should render to string (short form rendering)', - async(inject([PlatformRef], (defaultPlatform: PlatformRef) => { - const compilerFactory: CompilerFactory = - defaultPlatform.injector.get(CompilerFactory, null); - const moduleFactory = - compilerFactory.createCompiler().compileModuleSync(AsyncServerModule); - renderModuleFactory(moduleFactory, {document: doc}).then(output => { - expect(output).toBe(expectedOutput); - called = true; - }); - }))); }); }