Skip to content

Commit

Permalink
feat(core): support for bootstrap with custom zone (#17672)
Browse files Browse the repository at this point in the history
PR Close #17672
  • Loading branch information
mhevery authored and IgorMinar committed Sep 21, 2017
1 parent 6e1896b commit 344a5ca
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 35 deletions.
66 changes: 48 additions & 18 deletions packages/core/src/application_ref.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {InternalViewRef, ViewRef} from './linker/view_ref';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
import {Testability, TestabilityRegistry} from './testability/testability'; import {Testability, TestabilityRegistry} from './testability/testability';
import {Type} from './type'; import {Type} from './type';
import {NgZone} from './zone/ng_zone'; import {NgZone, NoopNgZone} from './zone/ng_zone';


let _devMode: boolean = true; let _devMode: boolean = true;
let _runModeLocked: boolean = false; let _runModeLocked: boolean = false;
Expand Down Expand Up @@ -158,6 +158,22 @@ export function getPlatform(): PlatformRef|null {
return _platform && !_platform.destroyed ? _platform : null; return _platform && !_platform.destroyed ? _platform : null;
} }


/**
* Provides additional options to the bootstraping process.
*
* @stable
*/
export interface BootstrapOptions {
/**
* Optionally specify which `NgZone` should be used.
*
* - Provide your own `NgZone` instance.
* - `zone.js` - Use default `NgZone` which requires `Zone.js`.
* - `noop` - Use `NoopNgZone` which does nothing.
*/
ngZone?: NgZone|'zone.js'|'noop';
}

/** /**
* The Angular platform is the entry point for Angular on a web page. Each page * The Angular platform is the entry point for Angular on a web page. Each page
* has exactly one platform, and services (such as reflection) which are common * has exactly one platform, and services (such as reflection) which are common
Expand All @@ -168,6 +184,7 @@ export function getPlatform(): PlatformRef|null {
* *
* @stable * @stable
*/ */
@Injectable()
export class PlatformRef { export class PlatformRef {
private _modules: NgModuleRef<any>[] = []; private _modules: NgModuleRef<any>[] = [];
private _destroyListeners: Function[] = []; private _destroyListeners: Function[] = [];
Expand Down Expand Up @@ -199,17 +216,14 @@ export class PlatformRef {
* *
* @experimental APIs related to application bootstrap are currently under review. * @experimental APIs related to application bootstrap are currently under review.
*/ */
bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>): Promise<NgModuleRef<M>> { bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):
return this._bootstrapModuleFactoryWithZone(moduleFactory);
}

private _bootstrapModuleFactoryWithZone<M>(moduleFactory: NgModuleFactory<M>, ngZone?: NgZone):
Promise<NgModuleRef<M>> { Promise<NgModuleRef<M>> {
// Note: We need to create the NgZone _before_ we instantiate the module, // Note: We need to create the NgZone _before_ we instantiate the module,
// as instantiating the module creates some providers eagerly. // as instantiating the module creates some providers eagerly.
// So we create a mini parent injector that just contains the new NgZone and // So we create a mini parent injector that just contains the new NgZone and
// pass that as parent to the NgModuleFactory. // pass that as parent to the NgModuleFactory.
if (!ngZone) ngZone = new NgZone({enableLongStackTrace: isDevMode()}); const ngZoneOption = options ? options.ngZone : undefined;
const ngZone = getNgZone(ngZoneOption);
// Attention: Don't use ApplicationRef.run here, // Attention: Don't use ApplicationRef.run here,
// as we want to be sure that all possible constructor calls are inside `ngZone.run`! // as we want to be sure that all possible constructor calls are inside `ngZone.run`!
return ngZone.run(() => { return ngZone.run(() => {
Expand Down Expand Up @@ -249,20 +263,15 @@ export class PlatformRef {
* ``` * ```
* @stable * @stable
*/ */
bootstrapModule<M>(moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = []): bootstrapModule<M>(
Promise<NgModuleRef<M>> { moduleType: Type<M>, compilerOptions: (CompilerOptions&BootstrapOptions)|
return this._bootstrapModuleWithZone(moduleType, compilerOptions); Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {
}

private _bootstrapModuleWithZone<M>(
moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = [],
ngZone?: NgZone): Promise<NgModuleRef<M>> {
const compilerFactory: CompilerFactory = this.injector.get(CompilerFactory); const compilerFactory: CompilerFactory = this.injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler( const options = optionsReducer({}, compilerOptions);
Array.isArray(compilerOptions) ? compilerOptions : [compilerOptions]); const compiler = compilerFactory.createCompiler([options]);


return compiler.compileModuleAsync(moduleType) return compiler.compileModuleAsync(moduleType)
.then((moduleFactory) => this._bootstrapModuleFactoryWithZone(moduleFactory, ngZone)); .then((moduleFactory) => this.bootstrapModuleFactory(moduleFactory, options));
} }


private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void { private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
Expand Down Expand Up @@ -305,6 +314,18 @@ export class PlatformRef {
get destroyed() { return this._destroyed; } get destroyed() { return this._destroyed; }
} }


function getNgZone(ngZoneOption?: NgZone | 'zone.js' | 'noop'): NgZone {
let ngZone: NgZone;

if (ngZoneOption === 'noop') {
ngZone = new NoopNgZone();
} else {
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) ||
new NgZone({enableLongStackTrace: isDevMode()});
}
return ngZone;
}

function _callAndReportToErrorHandler( function _callAndReportToErrorHandler(
errorHandler: ErrorHandler, ngZone: NgZone, callback: () => any): any { errorHandler: ErrorHandler, ngZone: NgZone, callback: () => any): any {
try { try {
Expand All @@ -325,6 +346,15 @@ function _callAndReportToErrorHandler(
} }
} }


function optionsReducer<T extends Object>(dst: any, objs: T | T[]): T {
if (Array.isArray(objs)) {
dst = objs.reduce(optionsReducer, dst);
} else {
dst = {...dst, ...(objs as any)};
}
return dst;
}

/** /**
* A reference to an Angular application running on a page. * A reference to an Angular application running on a page.
* *
Expand Down
28 changes: 26 additions & 2 deletions packages/core/src/zone/ng_zone.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class NgZone {
readonly onUnstable: EventEmitter<any> = new EventEmitter(false); readonly onUnstable: EventEmitter<any> = new EventEmitter(false);


/** /**
* Notifies when there is no more microtasks enqueue in the current VM Turn. * Notifies when there is no more microtasks enqueued in the current VM Turn.
* This is a hint for Angular to do change detection, which may enqueue more microtasks. * This is a hint for Angular to do change detection, which may enqueue more microtasks.
* For this reason this event can fire multiple times per VM Turn. * For this reason this event can fire multiple times per VM Turn.
*/ */
Expand Down Expand Up @@ -216,7 +216,7 @@ export class NgZone {
} }
} }


function noop(){}; function noop() {}
const EMPTY_PAYLOAD = {}; const EMPTY_PAYLOAD = {};




Expand Down Expand Up @@ -308,3 +308,27 @@ function onLeave(zone: NgZonePrivate) {
zone._nesting--; zone._nesting--;
checkStable(zone); checkStable(zone);
} }

/**
* Provides a noop implementation of `NgZone` which does nothing. This zone requires explicit calls
* to framework to perform rendering.
*
* @internal
*/
export class NoopNgZone implements NgZone {
readonly hasPendingMicrotasks: boolean = false;
readonly hasPendingMacrotasks: boolean = false;
readonly isStable: boolean = true;
readonly onUnstable: EventEmitter<any> = new EventEmitter();
readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter();
readonly onStable: EventEmitter<any> = new EventEmitter();
readonly onError: EventEmitter<any> = new EventEmitter();

run(fn: () => any): any { return fn(); }

runGuarded(fn: () => any): any { return fn(); }

runOutsideAngular(fn: () => any): any { return fn(); }

runTask<T>(fn: () => any): any { return fn(); }
}
13 changes: 11 additions & 2 deletions packages/core/test/application_ref_spec.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ServerModule} from '@angular/platform-server'; import {ServerModule} from '@angular/platform-server';

import {NoopNgZone} from '../src/zone/ng_zone';
import {ComponentFixture, ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing'; import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';


@Component({selector: 'bootstrap-app', template: 'hello'}) @Component({selector: 'bootstrap-app', template: 'hello'})
class SomeComponent { class SomeComponent {
Expand Down Expand Up @@ -287,6 +287,15 @@ export function main() {
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]})) defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
.then(module => expect((<any>defaultPlatform)._modules).toContain(module)); .then(module => expect((<any>defaultPlatform)._modules).toContain(module));
})); }));

it('should bootstrap with NoopNgZone', async(() => {
defaultPlatform
.bootstrapModule(createModule({bootstrap: [SomeComponent]}), {ngZone: 'noop'})
.then((module) => {
const ngZone = module.injector.get(NgZone);
expect(ngZone instanceof NoopNgZone).toBe(true);
});
}));
}); });


describe('bootstrapModuleFactory', () => { describe('bootstrapModuleFactory', () => {
Expand Down
22 changes: 21 additions & 1 deletion packages/core/test/zone/ng_zone_spec.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */


import {NgZone} from '@angular/core/src/zone/ng_zone'; import {EventEmitter, NgZone} from '@angular/core';
import {async, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import {async, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {AsyncTestCompleter, Log, beforeEach, describe, expect, inject, it, xit} from '@angular/core/testing/src/testing_internal'; import {AsyncTestCompleter, Log, beforeEach, describe, expect, inject, it, xit} from '@angular/core/testing/src/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {scheduleMicroTask} from '../../src/util'; import {scheduleMicroTask} from '../../src/util';
import {NoopNgZone} from '../../src/zone/ng_zone';


const needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge; const needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge;
const resultTimer = 1000; const resultTimer = 1000;
Expand Down Expand Up @@ -170,6 +171,25 @@ export function main() {
}), testTimeout); }), testTimeout);
}); });
}); });

describe('NoopNgZone', () => {
const ngZone = new NoopNgZone();

it('should run', () => {
let runs = false;
ngZone.run(() => {
ngZone.runGuarded(() => { ngZone.runOutsideAngular(() => { runs = true; }); });
});
expect(runs).toBe(true);
});

it('should have EventEmitter instances', () => {
expect(ngZone.onError instanceof EventEmitter).toBe(true);
expect(ngZone.onStable instanceof EventEmitter).toBe(true);
expect(ngZone.onUnstable instanceof EventEmitter).toBe(true);
expect(ngZone.onMicrotaskEmpty instanceof EventEmitter).toBe(true);
});
});
} }


function commonTests() { function commonTests() {
Expand Down
6 changes: 3 additions & 3 deletions packages/upgrade/src/dynamic/upgrade_adapter.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -572,9 +572,9 @@ export class UpgradeAdapter {
constructor() {} constructor() {}
ngDoBootstrap() {} ngDoBootstrap() {}
} }
(platformRef as any) platformRef
._bootstrapModuleWithZone( .bootstrapModule(
DynamicNgUpgradeModule, this.compilerOptions, this.ngZone) DynamicNgUpgradeModule, [this.compilerOptions !, {ngZone: this.ngZone}])
.then((ref: NgModuleRef<any>) => { .then((ref: NgModuleRef<any>) => {
this.moduleRef = ref; this.moduleRef = ref;
this.ngZone.run(() => { this.ngZone.run(() => {
Expand Down
19 changes: 12 additions & 7 deletions packages/upgrade/test/dynamic/upgrade_spec.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */


import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgZone, OnChanges, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core'; import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, NgZone, OnChanges, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
Expand Down Expand Up @@ -56,7 +56,7 @@ export function main() {
template: `{{ 'ng2(' }}<ng1>{{'transclude'}}</ng1>{{ ')' }}`, template: `{{ 'ng2(' }}<ng1>{{'transclude'}}</ng1>{{ ')' }}`,
}) })
class Ng2 { class Ng2 {
}; }


@NgModule({ @NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2], declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
Expand All @@ -80,7 +80,8 @@ export function main() {


it('supports the compilerOptions argument', async(() => { it('supports the compilerOptions argument', async(() => {
const platformRef = platformBrowserDynamic(); const platformRef = platformBrowserDynamic();
spyOn(platformRef, '_bootstrapModuleWithZone').and.callThrough(); spyOn(platformRef, 'bootstrapModule').and.callThrough();
spyOn(platformRef, 'bootstrapModuleFactory').and.callThrough();


const ng1Module = angular.module('ng1', []); const ng1Module = angular.module('ng1', []);
@Component({selector: 'ng2', template: `{{ 'NG2' }}(<ng-content></ng-content>)`}) @Component({selector: 'ng2', template: `{{ 'NG2' }}(<ng-content></ng-content>)`})
Expand All @@ -96,13 +97,17 @@ export function main() {
}) })
class Ng2AppModule { class Ng2AppModule {
ngDoBootstrap() {} ngDoBootstrap() {}
}; }


const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2AppModule, {providers: []}); const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2AppModule, {providers: []});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect((platformRef as any)._bootstrapModuleWithZone) expect(platformRef.bootstrapModule).toHaveBeenCalledWith(jasmine.any(Function), [
.toHaveBeenCalledWith(jasmine.any(Function), {providers: []}, jasmine.any(Object)); {providers: []}, jasmine.any(Object)
]);
expect(platformRef.bootstrapModuleFactory)
.toHaveBeenCalledWith(
jasmine.any(NgModuleFactory), {providers: [], ngZone: jasmine.any(NgZone)});
ref.dispose(); ref.dispose();
}); });
})); }));
Expand Down Expand Up @@ -395,7 +400,7 @@ export function main() {
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
}; }


const element = html(`<div> const element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{name}}" <ng2 literal="Text" interpolate="Hello {{name}}"
Expand Down
4 changes: 2 additions & 2 deletions tools/public_api_guard/core/core.d.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -698,8 +698,8 @@ export declare const platformCore: (extraProviders?: StaticProvider[] | undefine
export declare class PlatformRef { export declare class PlatformRef {
readonly destroyed: boolean; readonly destroyed: boolean;
readonly injector: Injector; readonly injector: Injector;
/** @stable */ bootstrapModule<M>(moduleType: Type<M>, compilerOptions?: CompilerOptions | CompilerOptions[]): Promise<NgModuleRef<M>>; /** @stable */ bootstrapModule<M>(moduleType: Type<M>, compilerOptions?: (CompilerOptions & BootstrapOptions) | Array<CompilerOptions & BootstrapOptions>): Promise<NgModuleRef<M>>;
/** @experimental */ bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>): Promise<NgModuleRef<M>>; /** @experimental */ bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Promise<NgModuleRef<M>>;
destroy(): void; destroy(): void;
onDestroy(callback: () => void): void; onDestroy(callback: () => void): void;
} }
Expand Down

0 comments on commit 344a5ca

Please sign in to comment.