Skip to content

Commit 3459289

Browse files
atscottthePunderWoman
authored andcommitted
feat(core): bootstrapModule can configure NgZone in providers (angular#57060)
This commit allows configuring `NgZone` through the providers for `bootstrapModule`. Prior to this change, developers had to configure `NgZone` in the `BootstrapOptions`. PR Close angular#57060
1 parent 03553c4 commit 3459289

File tree

7 files changed

+95
-70
lines changed

7 files changed

+95
-70
lines changed

packages/core/src/application/application_ref.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export class NgProbeToken {
101101
*/
102102
export interface BootstrapOptions {
103103
/**
104-
* Optionally specify which `NgZone` should be used.
104+
* Optionally specify which `NgZone` should be used when not configured in the providers.
105105
*
106106
* - Provide your own `NgZone` instance.
107107
* - `zone.js` - Use default `NgZone` which requires `Zone.js`.

packages/core/src/linker/ng_module_factory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {Injector} from '../di/injector';
10-
import {EnvironmentInjector} from '../di/r3_injector';
10+
import {EnvironmentInjector, R3Injector} from '../di/r3_injector';
1111
import {Type} from '../interface/type';
1212

1313
import {ComponentFactoryResolver} from './component_factory_resolver';
@@ -56,6 +56,7 @@ export interface InternalNgModuleRef<T> extends NgModuleRef<T> {
5656
// Note: we are using the prefix _ as NgModuleData is an NgModuleRef and therefore directly
5757
// exposed to the user.
5858
_bootstrapComponents: Type<any>[];
59+
resolveInjectorInitializers(): void;
5960
}
6061

6162
/**

packages/core/src/platform/platform_ref.ts

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from '../change_detection/scheduling/ng_zone_scheduling';
2323
import {
2424
ChangeDetectionScheduler,
25-
ZONELESS_ENABLED,
25+
PROVIDED_ZONELESS,
2626
} from '../change_detection/scheduling/zoneless_scheduling';
2727
import {ChangeDetectionSchedulerImpl} from '../change_detection/scheduling/zoneless_scheduling_impl';
2828
import {Injectable, InjectionToken, Injector} from '../di';
@@ -36,7 +36,7 @@ import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from '../linker/ng_mo
3636
import {setLocaleId} from '../render3';
3737
import {createNgModuleRefWithProviders} from '../render3/ng_module_ref';
3838
import {stringify} from '../util/stringify';
39-
import {getNgZone} from '../zone/ng_zone';
39+
import {getNgZone, NgZone, NoopNgZone} from '../zone/ng_zone';
4040

4141
/**
4242
* Internal token that allows to register extra callbacks that should be invoked during the
@@ -76,54 +76,49 @@ export class PlatformRef {
7676
moduleFactory: NgModuleFactory<M>,
7777
options?: BootstrapOptions,
7878
): Promise<NgModuleRef<M>> {
79-
// Note: We need to create the NgZone _before_ we instantiate the module,
80-
// as instantiating the module creates some providers eagerly.
81-
// So we create a mini parent injector that just contains the new NgZone and
82-
// pass that as parent to the NgModuleFactory.
83-
const ngZone = getNgZone(
84-
options?.ngZone,
85-
getNgZoneOptions({
86-
eventCoalescing: options?.ngZoneEventCoalescing,
87-
runCoalescing: options?.ngZoneRunCoalescing,
79+
const ngZoneFactory = () =>
80+
getNgZone(
81+
options?.ngZone,
82+
getNgZoneOptions({
83+
eventCoalescing: options?.ngZoneEventCoalescing,
84+
runCoalescing: options?.ngZoneRunCoalescing,
85+
}),
86+
);
87+
const ignoreChangesOutsideZone = options?.ignoreChangesOutsideZone;
88+
const allAppProviders = [
89+
internalProvideZoneChangeDetection({
90+
ngZoneFactory,
91+
ignoreChangesOutsideZone,
8892
}),
93+
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
94+
];
95+
const moduleRef = createNgModuleRefWithProviders(
96+
moduleFactory.moduleType,
97+
this.injector,
98+
allAppProviders,
8999
);
90-
// Note: Create ngZoneInjector within ngZone.run so that all of the instantiated services are
91-
// created within the Angular zone
92-
// Do not try to replace ngZone.run with ApplicationRef#run because ApplicationRef would then be
93-
// created outside of the Angular zone.
94-
return ngZone.run(() => {
95-
const ignoreChangesOutsideZone = options?.ignoreChangesOutsideZone;
96-
const moduleRef = createNgModuleRefWithProviders(moduleFactory.moduleType, this.injector, [
97-
...internalProvideZoneChangeDetection({
98-
ngZoneFactory: () => ngZone,
99-
ignoreChangesOutsideZone,
100-
}),
101-
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
102-
]);
100+
const envInjector = moduleRef.injector;
101+
const ngZone = envInjector.get(NgZone);
103102

103+
return ngZone.run(() => {
104+
moduleRef.resolveInjectorInitializers();
105+
const exceptionHandler = envInjector.get(ErrorHandler, null);
104106
if (typeof ngDevMode === 'undefined' || ngDevMode) {
105-
if (moduleRef.injector.get(PROVIDED_NG_ZONE)) {
107+
if (exceptionHandler === null) {
106108
throw new RuntimeError(
107-
RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT,
108-
'`bootstrapModule` does not support `provideZoneChangeDetection`. Use `BootstrapOptions` instead.',
109+
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
110+
'No ErrorHandler. Is platform module (BrowserModule) included?',
109111
);
110112
}
111-
if (moduleRef.injector.get(ZONELESS_ENABLED) && options?.ngZone !== 'noop') {
113+
if (envInjector.get(PROVIDED_ZONELESS) && envInjector.get(PROVIDED_NG_ZONE)) {
112114
throw new RuntimeError(
113115
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
114116
'Invalid change detection configuration: ' +
115-
"`ngZone: 'noop'` must be set in `BootstrapOptions` with provideExperimentalZonelessChangeDetection.",
117+
'provideZoneChangeDetection and provideExperimentalZonelessChangeDetection cannot be used together.',
116118
);
117119
}
118120
}
119121

120-
const exceptionHandler = moduleRef.injector.get(ErrorHandler, null);
121-
if ((typeof ngDevMode === 'undefined' || ngDevMode) && exceptionHandler === null) {
122-
throw new RuntimeError(
123-
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
124-
'No ErrorHandler. Is platform module (BrowserModule) included?',
125-
);
126-
}
127122
ngZone.runOutsideAngular(() => {
128123
const subscription = ngZone.onError.subscribe({
129124
next: (error: any) => {
@@ -136,11 +131,12 @@ export class PlatformRef {
136131
});
137132
});
138133
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
139-
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
134+
const initStatus = envInjector.get(ApplicationInitStatus);
140135
initStatus.runInitializers();
136+
141137
return initStatus.donePromise.then(() => {
142138
// If the `LOCALE_ID` provider is defined at bootstrap then we set the value for ivy
143-
const localeId = moduleRef.injector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
139+
const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
144140
setLocaleId(localeId || DEFAULT_LOCALE_ID);
145141
this._moduleDoBootstrap(moduleRef);
146142
return moduleRef;

packages/core/src/render3/ng_module_ref.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
5252
// tslint:disable-next-line:require-internal-with-underscore
5353
_bootstrapComponents: Type<any>[] = [];
5454
// tslint:disable-next-line:require-internal-with-underscore
55-
_r3Injector: R3Injector;
56-
override instance: T;
55+
private readonly _r3Injector: R3Injector;
56+
override instance!: T;
5757
destroyCbs: (() => void)[] | null = [];
5858

5959
// When bootstrapping a module we have a dependency graph that looks like this:
@@ -66,9 +66,10 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
6666
new ComponentFactoryResolver(this);
6767

6868
constructor(
69-
ngModuleType: Type<T>,
69+
private readonly ngModuleType: Type<T>,
7070
public _parent: Injector | null,
7171
additionalProviders: StaticProvider[],
72+
runInjectorInitializers = true,
7273
) {
7374
super();
7475
const ngModuleDef = getNgModuleDef(ngModuleType);
@@ -97,8 +98,14 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
9798
// We need to resolve the injector types separately from the injector creation, because
9899
// the module might be trying to use this ref in its constructor for DI which will cause a
99100
// circular error that will eventually error out, because the injector isn't created yet.
101+
if (runInjectorInitializers) {
102+
this.resolveInjectorInitializers();
103+
}
104+
}
105+
106+
resolveInjectorInitializers() {
100107
this._r3Injector.resolveInjectorInitializers();
101-
this.instance = this._r3Injector.get(ngModuleType);
108+
this.instance = this._r3Injector.get(this.ngModuleType);
102109
}
103110

104111
override get injector(): EnvironmentInjector {
@@ -133,7 +140,7 @@ export function createNgModuleRefWithProviders<T>(
133140
parentInjector: Injector | null,
134141
additionalProviders: StaticProvider[],
135142
): InternalNgModuleRef<T> {
136-
return new NgModuleRef(moduleType, parentInjector, additionalProviders);
143+
return new NgModuleRef(moduleType, parentInjector, additionalProviders, false);
137144
}
138145

139146
export class EnvironmentNgModuleRefAdapter extends viewEngine_NgModuleRef<null> {

packages/core/test/acceptance/bootstrap_spec.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import {
1515
NgModule,
1616
NgZone,
1717
provideExperimentalZonelessChangeDetection,
18+
provideZoneChangeDetection,
1819
TestabilityRegistry,
1920
ViewContainerRef,
2021
ViewEncapsulation,
22+
ɵNoopNgZone,
23+
ɵZONELESS_ENABLED,
2124
} from '@angular/core';
2225
import {bootstrapApplication, BrowserModule} from '@angular/platform-browser';
2326
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@@ -329,34 +332,47 @@ describe('bootstrap', () => {
329332
);
330333

331334
it(
332-
'should throw when using zoneless without ngZone: "noop"',
335+
'can configure zone with provideZoneChangeDetection',
333336
withBody('<my-app></my-app>', async () => {
334337
@Component({
338+
selector: 'my-app',
335339
template: '...',
336340
})
337341
class App {}
338342

339343
@NgModule({
340344
declarations: [App],
341-
providers: [provideExperimentalZonelessChangeDetection()],
345+
providers: [provideZoneChangeDetection({eventCoalescing: true})],
342346
imports: [BrowserModule],
343347
bootstrap: [App],
344348
})
345349
class MyModule {}
346350

347-
try {
348-
await platformBrowserDynamic().bootstrapModule(MyModule);
351+
const {injector} = await platformBrowserDynamic().bootstrapModule(MyModule);
352+
expect((injector.get(NgZone) as any).shouldCoalesceEventChangeDetection).toBe(true);
353+
}),
354+
);
349355

350-
// This test tries to bootstrap a standalone component using NgModule-based bootstrap
351-
// mechanisms. We expect standalone components to be bootstrapped via
352-
// `bootstrapApplication` API instead.
353-
fail('Expected to throw');
354-
} catch (e: unknown) {
355-
const expectedErrorMessage =
356-
"Invalid change detection configuration: `ngZone: 'noop'` must be set in `BootstrapOptions`";
357-
expect(e).toBeInstanceOf(Error);
358-
expect((e as Error).message).toContain(expectedErrorMessage);
359-
}
356+
it(
357+
'can configure zoneless correctly without `ngZone: "noop"`',
358+
withBody('<my-app></my-app>', async () => {
359+
@Component({
360+
selector: 'my-app',
361+
template: '...',
362+
})
363+
class App {}
364+
365+
@NgModule({
366+
declarations: [App],
367+
providers: [provideExperimentalZonelessChangeDetection()],
368+
imports: [BrowserModule],
369+
bootstrap: [App],
370+
})
371+
class MyModule {}
372+
373+
const {injector} = await platformBrowserDynamic().bootstrapModule(MyModule);
374+
expect(injector.get(NgZone)).toBeInstanceOf(ɵNoopNgZone);
375+
expect(injector.get(ɵZONELESS_ENABLED)).toBeTrue();
360376
}),
361377
);
362378

packages/platform-browser/test/browser/bootstrap_spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,13 @@ describe('bootstrap factory method', () => {
718718
})();
719719
});
720720

721-
it('should not allow provideZoneChangeDetection in bootstrapModule', async () => {
721+
it('should allow provideZoneChangeDetection in bootstrapModule', async () => {
722722
@NgModule({imports: [BrowserModule], providers: [provideZoneChangeDetection()]})
723-
class SomeModule {}
723+
class SomeModule {
724+
ngDoBootstrap(){}
725+
}
724726

725-
await expectAsync(platformBrowserDynamic().bootstrapModule(SomeModule)).toBeRejectedWithError(
726-
/provideZoneChangeDetection.*BootstrapOptions/,
727-
);
727+
await expectAsync(platformBrowserDynamic().bootstrapModule(SomeModule)).toBeResolved();
728728
});
729729

730730
it('should register each application with the testability registry', async () => {

packages/router/test/router_preloader.spec.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ import {
1717
NgModuleFactory,
1818
NgModuleRef,
1919
Type,
20+
EnvironmentInjector,
2021
} from '@angular/core';
21-
import {R3Injector} from '@angular/core/src/di/r3_injector';
22-
import {NgModuleRef as R3NgModuleRef} from '@angular/core/src/render3';
2322
import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
2423
import {
2524
PreloadAllModules,
@@ -127,7 +126,11 @@ describe('RouterPreloader', () => {
127126
it('should work', fakeAsync(
128127
inject(
129128
[RouterPreloader, Router, NgModuleRef],
130-
(preloader: RouterPreloader, router: Router, testModule: R3NgModuleRef<unknown>) => {
129+
(
130+
preloader: RouterPreloader,
131+
router: Router,
132+
testModule: {_r3Injector: EnvironmentInjector},
133+
) => {
131134
const events: Array<RouteConfigLoadStart | RouteConfigLoadEnd> = [];
132135
@NgModule({
133136
declarations: [LazyLoadedCmp],
@@ -238,7 +241,7 @@ describe('RouterPreloader', () => {
238241
(
239242
preloader: RouterPreloader,
240243
router: Router,
241-
testModule: R3NgModuleRef<unknown>,
244+
testModule: {_r3Injector: EnvironmentInjector},
242245
compiler: Compiler,
243246
) => {
244247
@NgModule()
@@ -270,13 +273,15 @@ describe('RouterPreloader', () => {
270273

271274
const c = router.config;
272275

273-
const injector = getLoadedInjector(c[0]) as R3Injector;
276+
const injector = getLoadedInjector(c[0]) as unknown as {parent: EnvironmentInjector};
274277

275278
const loadedRoutes = getLoadedRoutes(c[0])!;
276279
expect(injector.parent).toBe(testModule._r3Injector);
277280

278281
const loadedRoutes2: Route[] = getLoadedRoutes(loadedRoutes[0])!;
279-
const injector3 = getLoadedInjector(loadedRoutes2[0]) as R3Injector;
282+
const injector3 = getLoadedInjector(loadedRoutes2[0]) as unknown as {
283+
parent: EnvironmentInjector;
284+
};
280285
expect(injector3.parent).toBe(module2.injector);
281286
},
282287
),

0 commit comments

Comments
 (0)