diff --git a/packages/core/src/application/create_application.ts b/packages/core/src/application/create_application.ts index c6f8e7de7bd313..5a2ba6e0db0ba0 100644 --- a/packages/core/src/application/create_application.ts +++ b/packages/core/src/application/create_application.ts @@ -8,7 +8,10 @@ import {Subscription} from 'rxjs'; -import {internalProvideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling'; +import { + internalProvideZoneChangeDetection, + PROVIDED_NG_ZONE, +} 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'; @@ -26,6 +29,7 @@ import {NgZone} from '../zone/ng_zone'; import {ApplicationInitStatus} from './application_init'; import {_callAndReportToErrorHandler, ApplicationRef} from './application_ref'; +import {PROVIDED_ZONELESS} from '../change_detection/scheduling/zoneless_scheduling'; /** * Internal create application API that implements the core application creation logic and optional @@ -70,11 +74,20 @@ export function internalCreateApplication(config: { return ngZone.run(() => { envInjector.resolveInjectorInitializers(); const exceptionHandler: ErrorHandler | null = envInjector.get(ErrorHandler, null); - if ((typeof ngDevMode === 'undefined' || ngDevMode) && !exceptionHandler) { - throw new RuntimeError( - RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP, - 'No `ErrorHandler` found in the Dependency Injection tree.', - ); + if (typeof ngDevMode === 'undefined' || ngDevMode) { + if (!exceptionHandler) { + throw new RuntimeError( + RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP, + 'No `ErrorHandler` found in the Dependency Injection tree.', + ); + } + if (envInjector.get(PROVIDED_ZONELESS) && envInjector.get(PROVIDED_NG_ZONE)) { + throw new RuntimeError( + RuntimeErrorCode.PROVIDED_BOTH_ZONE_AND_ZONELESS, + 'Invalid change detection configuration: ' + + 'provideZoneChangeDetection and provideExperimentalZonelessChangeDetection cannot be used together.', + ); + } } let onErrorSubscription: Subscription; 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 44a93d758a0b05..f78e1263624fd1 100644 --- a/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts +++ b/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts @@ -73,6 +73,7 @@ export class NgZoneChangeDetectionScheduler { */ export const PROVIDED_NG_ZONE = new InjectionToken( typeof ngDevMode === 'undefined' || ngDevMode ? 'provideZoneChangeDetection token' : '', + {factory: () => false}, ); export function internalProvideZoneChangeDetection({ @@ -85,6 +86,7 @@ export function internalProvideZoneChangeDetection({ ngZoneFactory ??= () => new NgZone(getNgZoneOptions()); return [ {provide: NgZone, useFactory: ngZoneFactory}, + {provide: ZONELESS_ENABLED, useValue: false}, { provide: ENVIRONMENT_INITIALIZER, multi: true, @@ -165,12 +167,7 @@ export function provideZoneChangeDetection(options?: NgZoneOptions): Environment }, ignoreChangesOutsideZone, }); - return makeEnvironmentProviders([ - typeof ngDevMode === 'undefined' || ngDevMode - ? [{provide: PROVIDED_NG_ZONE, useValue: true}, bothZoneAndZonelessErrorCheckProvider] - : [], - zoneProviders, - ]); + return makeEnvironmentProviders([zoneProviders]); } /** @@ -301,19 +298,3 @@ 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/change_detection/scheduling/zoneless_scheduling.ts b/packages/core/src/change_detection/scheduling/zoneless_scheduling.ts index 04b5207e889402..4cfb5fbe3f9bbe 100644 --- a/packages/core/src/change_detection/scheduling/zoneless_scheduling.ts +++ b/packages/core/src/change_detection/scheduling/zoneless_scheduling.ts @@ -59,6 +59,12 @@ export const ZONELESS_ENABLED = new InjectionToken( {providedIn: 'root', factory: () => false}, ); +/** Token used to indicate `provideExperimentalZonelessChangeDetection` was used. */ +export const PROVIDED_ZONELESS = new InjectionToken( + typeof ngDevMode === 'undefined' || ngDevMode ? 'Zoneless provided' : '', + {providedIn: 'root', factory: () => false}, +); + export const ZONELESS_SCHEDULER_DISABLED = new InjectionToken( typeof ngDevMode === 'undefined' || ngDevMode ? 'scheduler disabled' : '', ); diff --git a/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts b/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts index ed9506be0397bd..f15ecf1b1581f7 100644 --- a/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts +++ b/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts @@ -26,6 +26,7 @@ import { ChangeDetectionScheduler, NotificationSource, ZONELESS_ENABLED, + PROVIDED_ZONELESS, ZONELESS_SCHEDULER_DISABLED, } from './zoneless_scheduling'; @@ -297,5 +298,6 @@ export function provideExperimentalZonelessChangeDetection(): EnvironmentProvide {provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl}, {provide: NgZone, useClass: NoopNgZone}, {provide: ZONELESS_ENABLED, useValue: true}, + {provide: PROVIDED_ZONELESS, useValue: true}, ]); } diff --git a/packages/core/src/platform/platform_ref.ts b/packages/core/src/platform/platform_ref.ts index 038a537877e022..9967ea6e8815cc 100644 --- a/packages/core/src/platform/platform_ref.ts +++ b/packages/core/src/platform/platform_ref.ts @@ -95,25 +95,20 @@ export class PlatformRef { internalProvideZoneChangeDetection({ngZoneFactory: () => ngZone, ignoreChangesOutsideZone}), ); - if ( - (typeof ngDevMode === 'undefined' || ngDevMode) && - moduleRef.injector.get(PROVIDED_NG_ZONE, null) !== null - ) { - throw new RuntimeError( - RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT, - '`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.", - ); + if (typeof ngDevMode === 'undefined' || ngDevMode) { + if (moduleRef.injector.get(PROVIDED_NG_ZONE)) { + throw new RuntimeError( + RuntimeErrorCode.PROVIDER_IN_WRONG_CONTEXT, + '`bootstrapModule` does not support `provideZoneChangeDetection`. Use `BootstrapOptions` instead.', + ); + } + if (moduleRef.injector.get(ZONELESS_ENABLED) && 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); diff --git a/packages/core/test/acceptance/change_detection_spec.ts b/packages/core/test/acceptance/change_detection_spec.ts index 391d195dd01491..64d6730aacd796 100644 --- a/packages/core/test/acceptance/change_detection_spec.ts +++ b/packages/core/test/acceptance/change_detection_spec.ts @@ -45,6 +45,14 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; import {BehaviorSubject} from 'rxjs'; describe('change detection', () => { + it('can provide zone and zoneless (last one wins like any other provider) in TestBed', () => { + expect(() => { + TestBed.configureTestingModule({ + providers: [provideExperimentalZonelessChangeDetection(), provideZoneChangeDetection()], + }); + TestBed.inject(ApplicationRef); + }).not.toThrow(); + }); describe('embedded views', () => { @Directive({selector: '[viewManipulation]', exportAs: 'vm'}) class ViewManipulation { diff --git a/packages/core/test/change_detection_scheduler_spec.ts b/packages/core/test/change_detection_scheduler_spec.ts index 444a397dbbaada..a43be4141f4c36 100644 --- a/packages/core/test/change_detection_scheduler_spec.ts +++ b/packages/core/test/change_detection_scheduler_spec.ts @@ -64,14 +64,6 @@ 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');