diff --git a/packages/core/src/application/create_application.ts b/packages/core/src/application/create_application.ts index c6f8e7de7bd..5a2ba6e0db0 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 44a93d758a0..3f91da59db8 100644 --- a/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts +++ b/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts @@ -38,7 +38,6 @@ export class NgZoneChangeDetectionScheduler { private readonly zone = inject(NgZone); private readonly changeDetectionScheduler = inject(ChangeDetectionScheduler, {optional: true}); private readonly applicationRef = inject(ApplicationRef); - private readonly zonelessEnabled = inject(ZONELESS_ENABLED); private _onMicrotaskEmptySubscription?: Subscription; @@ -73,6 +72,7 @@ export class NgZoneChangeDetectionScheduler { */ export const PROVIDED_NG_ZONE = new InjectionToken( typeof ngDevMode === 'undefined' || ngDevMode ? 'provideZoneChangeDetection token' : '', + {factory: () => false}, ); export function internalProvideZoneChangeDetection({ @@ -167,8 +167,9 @@ export function provideZoneChangeDetection(options?: NgZoneOptions): Environment }); return makeEnvironmentProviders([ typeof ngDevMode === 'undefined' || ngDevMode - ? [{provide: PROVIDED_NG_ZONE, useValue: true}, bothZoneAndZonelessErrorCheckProvider] + ? [{provide: PROVIDED_NG_ZONE, useValue: true}] : [], + {provide: ZONELESS_ENABLED, useValue: false}, zoneProviders, ]); } @@ -301,19 +302,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 04b5207e889..4cfb5fbe3f9 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 ed9506be039..3056b6818fb 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,8 @@ export function provideExperimentalZonelessChangeDetection(): EnvironmentProvide {provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl}, {provide: NgZone, useClass: NoopNgZone}, {provide: ZONELESS_ENABLED, useValue: true}, + typeof ngDevMode === 'undefined' || ngDevMode + ? [{provide: PROVIDED_ZONELESS, useValue: true}] + : [], ]); } diff --git a/packages/core/src/platform/platform_ref.ts b/packages/core/src/platform/platform_ref.ts index 038a537877e..9967ea6e881 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 391d195dd01..64d6730aacd 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 444a397dbba..a43be4141f4 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');