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}, ], })