Skip to content

Commit 9eb7478

Browse files
atscottAndrewKushnir
authored andcommitted
refactor(core): Throw a runtime error if both zone and zoneless are provided (angular#55410)
This commit adds a dev-mode error if both the zone and zoneless providers are used together. PR Close angular#55410
1 parent 42263e1 commit 9eb7478

File tree

21 files changed

+120
-35
lines changed

21 files changed

+120
-35
lines changed

goldens/public-api/core/errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ export const enum RuntimeErrorCode {
117117
// (undocumented)
118118
PLATFORM_NOT_FOUND = 401,
119119
// (undocumented)
120+
PROVIDED_BOTH_ZONE_AND_ZONELESS = 408,
121+
// (undocumented)
120122
PROVIDER_IN_WRONG_CONTEXT = 207,
121123
// (undocumented)
122124
PROVIDER_NOT_FOUND = -201,

packages/core/src/application/create_application.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Subscription} from 'rxjs';
1010

11-
import {provideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
11+
import {internalProvideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
1212
import {EnvironmentProviders, Provider, StaticProvider} from '../di/interface/provider';
1313
import {EnvironmentInjector} from '../di/r3_injector';
1414
import {ErrorHandler} from '../error_handler';
@@ -55,7 +55,7 @@ export function internalCreateApplication(config: {
5555

5656
// Create root application injector based on a set of providers configured at the platform
5757
// bootstrap level as well as providers passed to the bootstrap call by a user.
58-
const allAppProviders = [provideZoneChangeDetection(), ...(appProviders || [])];
58+
const allAppProviders = [internalProvideZoneChangeDetection({}), ...(appProviders || [])];
5959
const adapter = new EnvironmentNgModuleRefAdapter({
6060
providers: allAppProviders,
6161
parent: platformInjector as EnvironmentInjector,

packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import {NgZone} from '../../zone';
2626
import {InternalNgZoneOptions} from '../../zone/ng_zone';
2727

2828
import {alwaysProvideZonelessScheduler} from './flags';
29-
import {ChangeDetectionScheduler, ZONELESS_SCHEDULER_DISABLED} from './zoneless_scheduling';
29+
import {
30+
ChangeDetectionScheduler,
31+
ZONELESS_ENABLED,
32+
ZONELESS_SCHEDULER_DISABLED,
33+
} from './zoneless_scheduling';
3034
import {ChangeDetectionSchedulerImpl} from './zoneless_scheduling_impl';
3135

3236
@Injectable({providedIn: 'root'})
@@ -74,9 +78,10 @@ export function internalProvideZoneChangeDetection({
7478
ngZoneFactory,
7579
ignoreChangesOutsideZone,
7680
}: {
77-
ngZoneFactory: () => NgZone;
81+
ngZoneFactory?: () => NgZone;
7882
ignoreChangesOutsideZone?: boolean;
7983
}): StaticProvider[] {
84+
ngZoneFactory ??= () => new NgZone(getNgZoneOptions());
8085
return [
8186
{provide: NgZone, useFactory: ngZoneFactory},
8287
{
@@ -161,7 +166,7 @@ export function provideZoneChangeDetection(options?: NgZoneOptions): Environment
161166
});
162167
return makeEnvironmentProviders([
163168
typeof ngDevMode === 'undefined' || ngDevMode
164-
? {provide: PROVIDED_NG_ZONE, useValue: true}
169+
? [{provide: PROVIDED_NG_ZONE, useValue: true}, bothZoneAndZonelessErrorCheckProvider]
165170
: [],
166171
zoneProviders,
167172
]);
@@ -295,3 +300,19 @@ export class ZoneStablePendingTask {
295300
this.subscription.unsubscribe();
296301
}
297302
}
303+
304+
const bothZoneAndZonelessErrorCheckProvider = {
305+
provide: ENVIRONMENT_INITIALIZER,
306+
multi: true,
307+
useFactory: () => {
308+
const providedZoneless = inject(ZONELESS_ENABLED, {optional: true});
309+
if (providedZoneless) {
310+
throw new RuntimeError(
311+
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
312+
'Invalid change detection configuration: ' +
313+
'provideZoneChangeDetection and provideExperimentalZonelessChangeDetection cannot be used together.',
314+
);
315+
}
316+
return () => {};
317+
},
318+
};

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ export {
2121
defaultIterableDiffers as ɵdefaultIterableDiffers,
2222
defaultKeyValueDiffers as ɵdefaultKeyValueDiffers,
2323
} from './change_detection/change_detection';
24+
export {internalProvideZoneChangeDetection as ɵinternalProvideZoneChangeDetection} from './change_detection/scheduling/ng_zone_scheduling';
2425
export {
2526
ChangeDetectionScheduler as ɵChangeDetectionScheduler,
2627
NotificationSource as ɵNotificationSource,
2728
ZONELESS_ENABLED as ɵZONELESS_ENABLED,
2829
} from './change_detection/scheduling/zoneless_scheduling';
29-
export {PROVIDED_NG_ZONE as ɵPROVIDED_NG_ZONE} from './change_detection/scheduling/ng_zone_scheduling';
3030
export {Console as ɵConsole} from './console';
3131
export {
3232
DeferBlockDetails as ɵDeferBlockDetails,

packages/core/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const enum RuntimeErrorCode {
6969
ASYNC_INITIALIZERS_STILL_RUNNING = 405,
7070
APPLICATION_REF_ALREADY_DESTROYED = 406,
7171
RENDERER_NOT_FOUND = 407,
72+
PROVIDED_BOTH_ZONE_AND_ZONELESS = 408,
7273

7374
// Hydration Errors
7475
HYDRATION_NODE_MISMATCH = -500,

packages/core/src/platform/platform_ref.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
internalProvideZoneChangeDetection,
2121
PROVIDED_NG_ZONE,
2222
} from '../change_detection/scheduling/ng_zone_scheduling';
23+
import {ZONELESS_ENABLED} from '../change_detection/scheduling/zoneless_scheduling';
2324
import {Injectable, InjectionToken, Injector} from '../di';
2425
import {ErrorHandler} from '../error_handler';
2526
import {RuntimeError, RuntimeErrorCode} from '../errors';
@@ -103,6 +104,17 @@ export class PlatformRef {
103104
'`bootstrapModule` does not support `provideZoneChangeDetection`. Use `BootstrapOptions` instead.',
104105
);
105106
}
107+
if (
108+
(typeof ngDevMode === 'undefined' || ngDevMode) &&
109+
moduleRef.injector.get(ZONELESS_ENABLED, null) &&
110+
options?.ngZone !== 'noop'
111+
) {
112+
throw new RuntimeError(
113+
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
114+
'Invalid change detection configuration: ' +
115+
"`ngZone: 'noop'` must be set in `BootstrapOptions` with provideExperimentalZonelessChangeDetection.",
116+
);
117+
}
106118

107119
const exceptionHandler = moduleRef.injector.get(ErrorHandler, null);
108120
if ((typeof ngDevMode === 'undefined' || ngDevMode) && exceptionHandler === null) {

packages/core/test/acceptance/bootstrap_spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
forwardRef,
1515
NgModule,
1616
NgZone,
17+
provideExperimentalZonelessChangeDetection,
1718
TestabilityRegistry,
1819
ViewContainerRef,
1920
ViewEncapsulation,
@@ -327,6 +328,38 @@ describe('bootstrap', () => {
327328
}),
328329
);
329330

331+
it(
332+
'should throw when using zoneless without ngZone: "noop"',
333+
withBody('<my-app></my-app>', async () => {
334+
@Component({
335+
template: '...',
336+
})
337+
class App {}
338+
339+
@NgModule({
340+
declarations: [App],
341+
providers: [provideExperimentalZonelessChangeDetection()],
342+
imports: [BrowserModule],
343+
bootstrap: [App],
344+
})
345+
class MyModule {}
346+
347+
try {
348+
await platformBrowserDynamic().bootstrapModule(MyModule);
349+
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+
}
360+
}),
361+
);
362+
330363
it(
331364
'should throw when standalone component wrapped in `forwardRef` is used in @NgModule.bootstrap',
332365
withBody('<my-app></my-app>', async () => {

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,9 @@
10821082
{
10831083
"name": "internalImportProvidersFrom"
10841084
},
1085+
{
1086+
"name": "internalProvideZoneChangeDetection"
1087+
},
10851088
{
10861089
"name": "interpolateParams"
10871090
},
@@ -1292,9 +1295,6 @@
12921295
{
12931296
"name": "profiler"
12941297
},
1295-
{
1296-
"name": "provideZoneChangeDetection"
1297-
},
12981298
{
12991299
"name": "refreshContentQueries"
13001300
},

packages/core/test/bundling/animations/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,9 @@
10251025
{
10261026
"name": "getNextLContainer"
10271027
},
1028+
{
1029+
"name": "getNgZoneOptions"
1030+
},
10281031
{
10291032
"name": "getNodeInjectable"
10301033
},

packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,9 @@
797797
{
798798
"name": "getNextLContainer"
799799
},
800+
{
801+
"name": "getNgZoneOptions"
802+
},
800803
{
801804
"name": "getNodeInjectable"
802805
},

0 commit comments

Comments
 (0)