mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): Throw a runtime error if both zone and zoneless are provided (#55410)
This commit adds a dev-mode error if both the zone and zoneless providers are used together. PR Close #55410
This commit is contained in:
committed by
Andrew Kushnir
parent
8cb838f9f6
commit
abbaf8f639
@@ -117,6 +117,8 @@ export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
PLATFORM_NOT_FOUND = 401,
|
||||
// (undocumented)
|
||||
PROVIDED_BOTH_ZONE_AND_ZONELESS = 408,
|
||||
// (undocumented)
|
||||
PROVIDER_IN_WRONG_CONTEXT = 207,
|
||||
// (undocumented)
|
||||
PROVIDER_NOT_FOUND = -201,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
import {provideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
|
||||
import {internalProvideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
|
||||
import {EnvironmentProviders, Provider, StaticProvider} from '../di/interface/provider';
|
||||
import {EnvironmentInjector} from '../di/r3_injector';
|
||||
import {ErrorHandler} from '../error_handler';
|
||||
@@ -55,7 +55,7 @@ export function internalCreateApplication(config: {
|
||||
|
||||
// Create root application injector based on a set of providers configured at the platform
|
||||
// bootstrap level as well as providers passed to the bootstrap call by a user.
|
||||
const allAppProviders = [provideZoneChangeDetection(), ...(appProviders || [])];
|
||||
const allAppProviders = [internalProvideZoneChangeDetection({}), ...(appProviders || [])];
|
||||
const adapter = new EnvironmentNgModuleRefAdapter({
|
||||
providers: allAppProviders,
|
||||
parent: platformInjector as EnvironmentInjector,
|
||||
|
||||
@@ -26,7 +26,11 @@ import {NgZone} from '../../zone';
|
||||
import {InternalNgZoneOptions} from '../../zone/ng_zone';
|
||||
|
||||
import {alwaysProvideZonelessScheduler} from './flags';
|
||||
import {ChangeDetectionScheduler, ZONELESS_SCHEDULER_DISABLED} from './zoneless_scheduling';
|
||||
import {
|
||||
ChangeDetectionScheduler,
|
||||
ZONELESS_ENABLED,
|
||||
ZONELESS_SCHEDULER_DISABLED,
|
||||
} from './zoneless_scheduling';
|
||||
import {ChangeDetectionSchedulerImpl} from './zoneless_scheduling_impl';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
@@ -74,9 +78,10 @@ export function internalProvideZoneChangeDetection({
|
||||
ngZoneFactory,
|
||||
ignoreChangesOutsideZone,
|
||||
}: {
|
||||
ngZoneFactory: () => NgZone;
|
||||
ngZoneFactory?: () => NgZone;
|
||||
ignoreChangesOutsideZone?: boolean;
|
||||
}): StaticProvider[] {
|
||||
ngZoneFactory ??= () => new NgZone(getNgZoneOptions());
|
||||
return [
|
||||
{provide: NgZone, useFactory: ngZoneFactory},
|
||||
{
|
||||
@@ -161,7 +166,7 @@ export function provideZoneChangeDetection(options?: NgZoneOptions): Environment
|
||||
});
|
||||
return makeEnvironmentProviders([
|
||||
typeof ngDevMode === 'undefined' || ngDevMode
|
||||
? {provide: PROVIDED_NG_ZONE, useValue: true}
|
||||
? [{provide: PROVIDED_NG_ZONE, useValue: true}, bothZoneAndZonelessErrorCheckProvider]
|
||||
: [],
|
||||
zoneProviders,
|
||||
]);
|
||||
@@ -295,3 +300,19 @@ export class ZoneStablePendingTask {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
const bothZoneAndZonelessErrorCheckProvider = {
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useFactory: () => {
|
||||
const providedZoneless = inject(ZONELESS_ENABLED, {optional: true});
|
||||
if (providedZoneless) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
|
||||
'Invalid change detection configuration: ' +
|
||||
'provideZoneChangeDetection and provideExperimentalZonelessChangeDetection cannot be used together.',
|
||||
);
|
||||
}
|
||||
return () => {};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,12 +21,12 @@ export {
|
||||
defaultIterableDiffers as ɵdefaultIterableDiffers,
|
||||
defaultKeyValueDiffers as ɵdefaultKeyValueDiffers,
|
||||
} from './change_detection/change_detection';
|
||||
export {internalProvideZoneChangeDetection as ɵinternalProvideZoneChangeDetection} from './change_detection/scheduling/ng_zone_scheduling';
|
||||
export {
|
||||
ChangeDetectionScheduler as ɵChangeDetectionScheduler,
|
||||
NotificationSource as ɵNotificationSource,
|
||||
ZONELESS_ENABLED as ɵZONELESS_ENABLED,
|
||||
} from './change_detection/scheduling/zoneless_scheduling';
|
||||
export {PROVIDED_NG_ZONE as ɵPROVIDED_NG_ZONE} from './change_detection/scheduling/ng_zone_scheduling';
|
||||
export {Console as ɵConsole} from './console';
|
||||
export {
|
||||
DeferBlockDetails as ɵDeferBlockDetails,
|
||||
|
||||
@@ -69,6 +69,7 @@ export const enum RuntimeErrorCode {
|
||||
ASYNC_INITIALIZERS_STILL_RUNNING = 405,
|
||||
APPLICATION_REF_ALREADY_DESTROYED = 406,
|
||||
RENDERER_NOT_FOUND = 407,
|
||||
PROVIDED_BOTH_ZONE_AND_ZONELESS = 408,
|
||||
|
||||
// Hydration Errors
|
||||
HYDRATION_NODE_MISMATCH = -500,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
internalProvideZoneChangeDetection,
|
||||
PROVIDED_NG_ZONE,
|
||||
} from '../change_detection/scheduling/ng_zone_scheduling';
|
||||
import {ZONELESS_ENABLED} from '../change_detection/scheduling/zoneless_scheduling';
|
||||
import {Injectable, InjectionToken, Injector} from '../di';
|
||||
import {ErrorHandler} from '../error_handler';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
@@ -103,6 +104,17 @@ export class PlatformRef {
|
||||
'`bootstrapModule` does not support `provideZoneChangeDetection`. Use `BootstrapOptions` instead.',
|
||||
);
|
||||
}
|
||||
if (
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) &&
|
||||
moduleRef.injector.get(ZONELESS_ENABLED, null) &&
|
||||
options?.ngZone !== 'noop'
|
||||
) {
|
||||
throw new RuntimeError(
|
||||
RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS,
|
||||
'Invalid change detection configuration: ' +
|
||||
"`ngZone: 'noop'` must be set in `BootstrapOptions` with provideExperimentalZonelessChangeDetection.",
|
||||
);
|
||||
}
|
||||
|
||||
const exceptionHandler = moduleRef.injector.get(ErrorHandler, null);
|
||||
if ((typeof ngDevMode === 'undefined' || ngDevMode) && exceptionHandler === null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
forwardRef,
|
||||
NgModule,
|
||||
NgZone,
|
||||
provideExperimentalZonelessChangeDetection,
|
||||
TestabilityRegistry,
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation,
|
||||
@@ -327,6 +328,38 @@ describe('bootstrap', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should throw when using zoneless without ngZone: "noop"',
|
||||
withBody('<my-app></my-app>', async () => {
|
||||
@Component({
|
||||
template: '...',
|
||||
})
|
||||
class App {}
|
||||
|
||||
@NgModule({
|
||||
declarations: [App],
|
||||
providers: [provideExperimentalZonelessChangeDetection()],
|
||||
imports: [BrowserModule],
|
||||
bootstrap: [App],
|
||||
})
|
||||
class MyModule {}
|
||||
|
||||
try {
|
||||
await platformBrowserDynamic().bootstrapModule(MyModule);
|
||||
|
||||
// This test tries to bootstrap a standalone component using NgModule-based bootstrap
|
||||
// mechanisms. We expect standalone components to be bootstrapped via
|
||||
// `bootstrapApplication` API instead.
|
||||
fail('Expected to throw');
|
||||
} catch (e: unknown) {
|
||||
const expectedErrorMessage =
|
||||
"Invalid change detection configuration: `ngZone: 'noop'` must be set in `BootstrapOptions`";
|
||||
expect(e).toBeInstanceOf(Error);
|
||||
expect((e as Error).message).toContain(expectedErrorMessage);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should throw when standalone component wrapped in `forwardRef` is used in @NgModule.bootstrap',
|
||||
withBody('<my-app></my-app>', async () => {
|
||||
|
||||
@@ -1082,6 +1082,9 @@
|
||||
{
|
||||
"name": "internalImportProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "internalProvideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "interpolateParams"
|
||||
},
|
||||
@@ -1292,9 +1295,6 @@
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
{
|
||||
"name": "provideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "refreshContentQueries"
|
||||
},
|
||||
|
||||
@@ -1025,6 +1025,9 @@
|
||||
{
|
||||
"name": "getNextLContainer"
|
||||
},
|
||||
{
|
||||
"name": "getNgZoneOptions"
|
||||
},
|
||||
{
|
||||
"name": "getNodeInjectable"
|
||||
},
|
||||
|
||||
@@ -797,6 +797,9 @@
|
||||
{
|
||||
"name": "getNextLContainer"
|
||||
},
|
||||
{
|
||||
"name": "getNgZoneOptions"
|
||||
},
|
||||
{
|
||||
"name": "getNodeInjectable"
|
||||
},
|
||||
|
||||
@@ -2114,6 +2114,9 @@
|
||||
{
|
||||
"name": "internalImportProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "internalProvideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "invokeAllTriggerCleanupFns"
|
||||
},
|
||||
@@ -2309,9 +2312,6 @@
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
{
|
||||
"name": "provideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "refreshContentQueries"
|
||||
},
|
||||
|
||||
@@ -1112,6 +1112,9 @@
|
||||
{
|
||||
"name": "getNgDirectiveDef"
|
||||
},
|
||||
{
|
||||
"name": "getNgZoneOptions"
|
||||
},
|
||||
{
|
||||
"name": "getNodeInjectable"
|
||||
},
|
||||
|
||||
@@ -1079,6 +1079,9 @@
|
||||
{
|
||||
"name": "getNgDirectiveDef"
|
||||
},
|
||||
{
|
||||
"name": "getNgZoneOptions"
|
||||
},
|
||||
{
|
||||
"name": "getNodeInjectable"
|
||||
},
|
||||
|
||||
@@ -632,6 +632,9 @@
|
||||
{
|
||||
"name": "getNextLContainer"
|
||||
},
|
||||
{
|
||||
"name": "getNgZoneOptions"
|
||||
},
|
||||
{
|
||||
"name": "getNodeInjectable"
|
||||
},
|
||||
|
||||
@@ -998,6 +998,9 @@
|
||||
{
|
||||
"name": "internalImportProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "internalProvideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
@@ -1211,9 +1214,6 @@
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
{
|
||||
"name": "provideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "readableStreamLikeToAsyncGenerator"
|
||||
},
|
||||
|
||||
@@ -1523,6 +1523,9 @@
|
||||
{
|
||||
"name": "internalImportProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "internalProvideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "invokeDirectivesHostBindings"
|
||||
},
|
||||
@@ -1670,9 +1673,6 @@
|
||||
{
|
||||
"name": "lookupTokenUsingNodeInjector"
|
||||
},
|
||||
{
|
||||
"name": "makeEnvironmentProviders"
|
||||
},
|
||||
{
|
||||
"name": "makeRecord"
|
||||
},
|
||||
@@ -1817,9 +1817,6 @@
|
||||
{
|
||||
"name": "pathCompareMap"
|
||||
},
|
||||
{
|
||||
"name": "performanceMarkFeature"
|
||||
},
|
||||
{
|
||||
"name": "pipeFromArray"
|
||||
},
|
||||
@@ -1844,9 +1841,6 @@
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
{
|
||||
"name": "provideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "readableStreamLikeToAsyncGenerator"
|
||||
},
|
||||
|
||||
@@ -815,6 +815,9 @@
|
||||
{
|
||||
"name": "internalImportProvidersFrom"
|
||||
},
|
||||
{
|
||||
"name": "internalProvideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
@@ -947,9 +950,6 @@
|
||||
{
|
||||
"name": "parseAndConvertBindingsForDefinition"
|
||||
},
|
||||
{
|
||||
"name": "performanceMarkFeature"
|
||||
},
|
||||
{
|
||||
"name": "processInjectorTypesWithProviders"
|
||||
},
|
||||
@@ -962,9 +962,6 @@
|
||||
{
|
||||
"name": "profiler"
|
||||
},
|
||||
{
|
||||
"name": "provideZoneChangeDetection"
|
||||
},
|
||||
{
|
||||
"name": "refreshContentQueries"
|
||||
},
|
||||
|
||||
@@ -944,6 +944,9 @@
|
||||
{
|
||||
"name": "getNgDirectiveDef"
|
||||
},
|
||||
{
|
||||
"name": "getNgZoneOptions"
|
||||
},
|
||||
{
|
||||
"name": "getNodeInjectable"
|
||||
},
|
||||
|
||||
@@ -64,6 +64,14 @@ describe('Angular with zoneless enabled', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if used with zone provider', () => {
|
||||
TestBed.configureTestingModule({providers: [provideZoneChangeDetection()]});
|
||||
|
||||
expect(() => TestBed.inject(NgZone)).toThrowError(
|
||||
/NG0408: Invalid change detection configuration/,
|
||||
);
|
||||
});
|
||||
|
||||
describe('notifies scheduler', () => {
|
||||
it('contributes to application stableness', async () => {
|
||||
const val = signal('initial');
|
||||
|
||||
@@ -20,11 +20,9 @@ import {
|
||||
ModuleWithProviders,
|
||||
NgModule,
|
||||
NgModuleFactory,
|
||||
NgZone,
|
||||
Pipe,
|
||||
PlatformRef,
|
||||
Provider,
|
||||
provideZoneChangeDetection,
|
||||
resolveForwardRef,
|
||||
StaticProvider,
|
||||
Type,
|
||||
@@ -42,6 +40,7 @@ import {
|
||||
ɵgetAsyncClassMetadataFn as getAsyncClassMetadataFn,
|
||||
ɵgetInjectableDef as getInjectableDef,
|
||||
ɵInternalEnvironmentProviders as InternalEnvironmentProviders,
|
||||
ɵinternalProvideZoneChangeDetection as internalProvideZoneChangeDetection,
|
||||
ɵisComponentDefPendingResolution,
|
||||
ɵisEnvironmentProviders as isEnvironmentProviders,
|
||||
ɵNG_COMP_DEF as NG_COMP_DEF,
|
||||
@@ -927,7 +926,7 @@ export class TestBedCompiler {
|
||||
private compileTestModule(): void {
|
||||
class RootScopeModule {}
|
||||
compileNgModuleDefs(RootScopeModule as NgModuleType<any>, {
|
||||
providers: [...this.rootProviderOverrides, provideZoneChangeDetection()],
|
||||
providers: [...this.rootProviderOverrides, internalProvideZoneChangeDetection({})],
|
||||
});
|
||||
|
||||
const providers = [
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
NgModule,
|
||||
PLATFORM_INITIALIZER,
|
||||
platformCore,
|
||||
provideZoneChangeDetection,
|
||||
StaticProvider,
|
||||
ɵinternalProvideZoneChangeDetection as internalProvideZoneChangeDetection,
|
||||
} from '@angular/core';
|
||||
import {BrowserModule, ɵBrowserDomAdapter as BrowserDomAdapter} from '@angular/platform-browser';
|
||||
|
||||
@@ -46,7 +46,7 @@ export const platformBrowserTesting = createPlatformFactory(
|
||||
exports: [BrowserModule],
|
||||
providers: [
|
||||
{provide: APP_ID, useValue: 'a'},
|
||||
provideZoneChangeDetection(),
|
||||
internalProvideZoneChangeDetection({}),
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation},
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user