diff --git a/goldens/public-api/core/errors.md b/goldens/public-api/core/errors.md
index f33aad26f7e6e9..10e5bc5e40a2e4 100644
--- a/goldens/public-api/core/errors.md
+++ b/goldens/public-api/core/errors.md
@@ -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,
diff --git a/packages/core/src/application/create_application.ts b/packages/core/src/application/create_application.ts
index a13534bc52649c..c6f8e7de7bd313 100644
--- a/packages/core/src/application/create_application.ts
+++ b/packages/core/src/application/create_application.ts
@@ -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,
diff --git a/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts b/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts
index 9d07ba90562580..676147c1f1a390 100644
--- a/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts
+++ b/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts
@@ -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 () => {};
+ },
+};
diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts
index 88b4c7350f88b7..411c94a8d95790 100644
--- a/packages/core/src/core_private_export.ts
+++ b/packages/core/src/core_private_export.ts
@@ -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,
diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts
index d09f2ae38fd075..055e57e8849373 100644
--- a/packages/core/src/errors.ts
+++ b/packages/core/src/errors.ts
@@ -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,
diff --git a/packages/core/src/platform/platform_ref.ts b/packages/core/src/platform/platform_ref.ts
index 37cf8724f9d594..038a537877e022 100644
--- a/packages/core/src/platform/platform_ref.ts
+++ b/packages/core/src/platform/platform_ref.ts
@@ -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) {
diff --git a/packages/core/test/acceptance/bootstrap_spec.ts b/packages/core/test/acceptance/bootstrap_spec.ts
index 8f3b6c7b0c1592..87f6517740fe28 100644
--- a/packages/core/test/acceptance/bootstrap_spec.ts
+++ b/packages/core/test/acceptance/bootstrap_spec.ts
@@ -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('', 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('', async () => {
diff --git a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json
index 1f2c281772132d..7c8a2c303dd9ec 100644
--- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json
@@ -1082,6 +1082,9 @@
{
"name": "internalImportProvidersFrom"
},
+ {
+ "name": "internalProvideZoneChangeDetection"
+ },
{
"name": "interpolateParams"
},
@@ -1292,9 +1295,6 @@
{
"name": "profiler"
},
- {
- "name": "provideZoneChangeDetection"
- },
{
"name": "refreshContentQueries"
},
diff --git a/packages/core/test/bundling/animations/bundle.golden_symbols.json b/packages/core/test/bundling/animations/bundle.golden_symbols.json
index 3cccb70c82c5f7..ad40f0a2586c96 100644
--- a/packages/core/test/bundling/animations/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/animations/bundle.golden_symbols.json
@@ -1025,6 +1025,9 @@
{
"name": "getNextLContainer"
},
+ {
+ "name": "getNgZoneOptions"
+ },
{
"name": "getNodeInjectable"
},
diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
index a2b97adcf7a5ff..28f49f043c0a16 100644
--- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
@@ -797,6 +797,9 @@
{
"name": "getNextLContainer"
},
+ {
+ "name": "getNgZoneOptions"
+ },
{
"name": "getNodeInjectable"
},
diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json
index c8b9c24d728562..cac967b702cce7 100644
--- a/packages/core/test/bundling/defer/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json
@@ -2114,6 +2114,9 @@
{
"name": "internalImportProvidersFrom"
},
+ {
+ "name": "internalProvideZoneChangeDetection"
+ },
{
"name": "invokeAllTriggerCleanupFns"
},
@@ -2309,9 +2312,6 @@
{
"name": "profiler"
},
- {
- "name": "provideZoneChangeDetection"
- },
{
"name": "refreshContentQueries"
},
diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json
index 5de5cfba8ca3e8..7041e87ca9c0ef 100644
--- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json
@@ -1112,6 +1112,9 @@
{
"name": "getNgDirectiveDef"
},
+ {
+ "name": "getNgZoneOptions"
+ },
{
"name": "getNodeInjectable"
},
diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json
index ed6e63ff434151..c1c459b04cf322 100644
--- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json
@@ -1079,6 +1079,9 @@
{
"name": "getNgDirectiveDef"
},
+ {
+ "name": "getNgZoneOptions"
+ },
{
"name": "getNodeInjectable"
},
diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json
index c0ab8223b0bc84..5236c7890993db 100644
--- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json
@@ -632,6 +632,9 @@
{
"name": "getNextLContainer"
},
+ {
+ "name": "getNgZoneOptions"
+ },
{
"name": "getNodeInjectable"
},
diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json
index fda5235d87629f..7bbe5a919436f9 100644
--- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json
@@ -998,6 +998,9 @@
{
"name": "internalImportProvidersFrom"
},
+ {
+ "name": "internalProvideZoneChangeDetection"
+ },
{
"name": "invokeHostBindingsInCreationMode"
},
@@ -1211,9 +1214,6 @@
{
"name": "profiler"
},
- {
- "name": "provideZoneChangeDetection"
- },
{
"name": "readableStreamLikeToAsyncGenerator"
},
diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json
index 319d372835ca47..ab00e6e8d984ac 100644
--- a/packages/core/test/bundling/router/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/router/bundle.golden_symbols.json
@@ -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"
},
diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json
index 5b3d5017331ad3..95491d85e83584 100644
--- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json
@@ -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"
},
diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json
index 428da49303ac79..5503d9ce966953 100644
--- a/packages/core/test/bundling/todo/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json
@@ -944,6 +944,9 @@
{
"name": "getNgDirectiveDef"
},
+ {
+ "name": "getNgZoneOptions"
+ },
{
"name": "getNodeInjectable"
},
diff --git a/packages/core/test/change_detection_scheduler_spec.ts b/packages/core/test/change_detection_scheduler_spec.ts
index a43be4141f4c36..444a397dbbaada 100644
--- a/packages/core/test/change_detection_scheduler_spec.ts
+++ b/packages/core/test/change_detection_scheduler_spec.ts
@@ -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');
diff --git a/packages/core/testing/src/test_bed_compiler.ts b/packages/core/testing/src/test_bed_compiler.ts
index 862a8c3521a273..2ce88ada702bef 100644
--- a/packages/core/testing/src/test_bed_compiler.ts
+++ b/packages/core/testing/src/test_bed_compiler.ts
@@ -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,
@@ -931,6 +930,7 @@ export class TestBedCompiler {
});
const providers = [
+ internalProvideZoneChangeDetection({}),
{provide: Compiler, useFactory: () => new R3TestCompiler(this)},
{provide: DEFER_BLOCK_CONFIG, useValue: {behavior: this.deferBlockBehavior}},
...this.providers,
diff --git a/packages/platform-browser/testing/src/browser.ts b/packages/platform-browser/testing/src/browser.ts
index eecca1ec308012..e5a760b6a150a4 100644
--- a/packages/platform-browser/testing/src/browser.ts
+++ b/packages/platform-browser/testing/src/browser.ts
@@ -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},
],
})