From 3da0254478db82dfeb2461ccf9290c15c2c1d2b7 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Fri, 19 Jul 2024 15:50:01 -0700 Subject: [PATCH] refactor(core): de-duplicate bootstrap code between bootstrapApplication and bootstrapModule (#57060) This commit de-duplicates the code for bootstrapping between `bootstrapApplication` and `bootstrapModule`. A majority of the bootstrap code was identical between the two, with some minor differences that can be handled with a function overload. PR Close #57060 --- .../src/application/create_application.ts | 85 +--------- packages/core/src/platform/bootstrap.ts | 159 ++++++++++++++++++ packages/core/src/platform/platform.ts | 3 +- .../platform/platform_destroy_listeners.ts | 19 +++ packages/core/src/platform/platform_ref.ts | 96 +---------- 5 files changed, 194 insertions(+), 168 deletions(-) create mode 100644 packages/core/src/platform/bootstrap.ts create mode 100644 packages/core/src/platform/platform_destroy_listeners.ts diff --git a/packages/core/src/application/create_application.ts b/packages/core/src/application/create_application.ts index c68bee53a9b..cea0a8598c4 100644 --- a/packages/core/src/application/create_application.ts +++ b/packages/core/src/application/create_application.ts @@ -6,34 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {Subscription} from 'rxjs'; - -import { - internalProvideZoneChangeDetection, - PROVIDED_NG_ZONE, -} 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'; -import {RuntimeError, RuntimeErrorCode} from '../errors'; -import {DEFAULT_LOCALE_ID} from '../i18n/localization'; -import {LOCALE_ID} from '../i18n/tokens'; -import {ImagePerformanceWarning} from '../image_performance_warning'; import {Type} from '../interface/type'; import {createOrReusePlatformInjector} from '../platform/platform'; -import {PLATFORM_DESTROY_LISTENERS} from '../platform/platform_ref'; import {assertStandaloneComponentType} from '../render3/errors'; -import {setLocaleId} from '../render3/i18n/i18n_locale_id'; import {EnvironmentNgModuleRefAdapter} from '../render3/ng_module_ref'; import {NgZone} from '../zone/ng_zone'; -import {ApplicationInitStatus} from './application_init'; import {_callAndReportToErrorHandler, ApplicationRef} from './application_ref'; -import { - PROVIDED_ZONELESS, - ChangeDetectionScheduler, -} from '../change_detection/scheduling/zoneless_scheduling'; +import {ChangeDetectionScheduler} from '../change_detection/scheduling/zoneless_scheduling'; import {ChangeDetectionSchedulerImpl} from '../change_detection/scheduling/zoneless_scheduling_impl'; +import {bootstrap} from '../platform/bootstrap'; /** * Internal create application API that implements the core application creation logic and optional @@ -76,67 +61,11 @@ export function internalCreateApplication(config: { // happens after we get the NgZone instance from the Injector. runEnvironmentInitializers: false, }); - const envInjector = adapter.injector; - const ngZone = envInjector.get(NgZone); - return ngZone.run(() => { - envInjector.resolveInjectorInitializers(); - const exceptionHandler: ErrorHandler | null = envInjector.get(ErrorHandler, null); - 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; - ngZone.runOutsideAngular(() => { - onErrorSubscription = ngZone.onError.subscribe({ - next: (error: any) => { - exceptionHandler!.handleError(error); - }, - }); - }); - - // If the whole platform is destroyed, invoke the `destroy` method - // for all bootstrapped applications as well. - const destroyListener = () => envInjector.destroy(); - const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS); - onPlatformDestroyListeners.add(destroyListener); - - envInjector.onDestroy(() => { - onErrorSubscription.unsubscribe(); - onPlatformDestroyListeners.delete(destroyListener); - }); - - return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => { - const initStatus = envInjector.get(ApplicationInitStatus); - initStatus.runInitializers(); - - return initStatus.donePromise.then(() => { - const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID); - setLocaleId(localeId || DEFAULT_LOCALE_ID); - - const appRef = envInjector.get(ApplicationRef); - if (rootComponent !== undefined) { - appRef.bootstrap(rootComponent); - } - if (typeof ngDevMode === 'undefined' || ngDevMode) { - const imagePerformanceService = envInjector.get(ImagePerformanceWarning); - imagePerformanceService.start(); - } - return appRef; - }); - }); + return bootstrap({ + r3Injector: adapter.injector, + platformInjector, + rootComponent, }); } catch (e) { return Promise.reject(e); diff --git a/packages/core/src/platform/bootstrap.ts b/packages/core/src/platform/bootstrap.ts new file mode 100644 index 00000000000..d67abc9a6fd --- /dev/null +++ b/packages/core/src/platform/bootstrap.ts @@ -0,0 +1,159 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Subscription} from 'rxjs'; + +import {PROVIDED_NG_ZONE} from '../change_detection/scheduling/ng_zone_scheduling'; +import {EnvironmentInjector, R3Injector} from '../di/r3_injector'; +import {ErrorHandler} from '../error_handler'; +import {RuntimeError, RuntimeErrorCode} from '../errors'; +import {DEFAULT_LOCALE_ID} from '../i18n/localization'; +import {LOCALE_ID} from '../i18n/tokens'; +import {ImagePerformanceWarning} from '../image_performance_warning'; +import {Type} from '../interface/type'; +import {PLATFORM_DESTROY_LISTENERS} from './platform_destroy_listeners'; +import {setLocaleId} from '../render3/i18n/i18n_locale_id'; +import {NgZone} from '../zone/ng_zone'; + +import {ApplicationInitStatus} from '../application/application_init'; +import {_callAndReportToErrorHandler, ApplicationRef, remove} from '../application/application_ref'; +import {PROVIDED_ZONELESS} from '../change_detection/scheduling/zoneless_scheduling'; +import {Injector} from '../di'; +import {InternalNgModuleRef, NgModuleRef} from '../linker/ng_module_factory'; +import {stringify} from '../util/stringify'; + +export interface ModuleBootstrapConfig { + moduleRef: InternalNgModuleRef; + allPlatformModules: NgModuleRef[]; +} + +export interface ApplicationBootstrapConfig { + r3Injector: R3Injector; + platformInjector: Injector; + rootComponent: Type | undefined; +} + +function isApplicationBootstrapConfig( + config: ApplicationBootstrapConfig | ModuleBootstrapConfig, +): config is ApplicationBootstrapConfig { + return !!(config as ApplicationBootstrapConfig).platformInjector; +} + +export function bootstrap( + moduleBootstrapConfig: ModuleBootstrapConfig, +): Promise>; +export function bootstrap( + applicationBootstrapConfig: ApplicationBootstrapConfig, +): Promise; +export function bootstrap( + config: ModuleBootstrapConfig | ApplicationBootstrapConfig, +): Promise | Promise> { + const envInjector = isApplicationBootstrapConfig(config) + ? config.r3Injector + : config.moduleRef.injector; + const ngZone = envInjector.get(NgZone); + return ngZone.run(() => { + if (isApplicationBootstrapConfig(config)) { + config.r3Injector.resolveInjectorInitializers(); + } else { + config.moduleRef.resolveInjectorInitializers(); + } + const exceptionHandler = envInjector.get(ErrorHandler, null); + if (typeof ngDevMode === 'undefined' || ngDevMode) { + if (exceptionHandler === null) { + const errorMessage = isApplicationBootstrapConfig(config) + ? 'No `ErrorHandler` found in the Dependency Injection tree.' + : 'No ErrorHandler. Is platform module (BrowserModule) included'; + throw new RuntimeError( + RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP, + errorMessage, + ); + } + 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; + ngZone.runOutsideAngular(() => { + onErrorSubscription = ngZone.onError.subscribe({ + next: (error: any) => { + exceptionHandler!.handleError(error); + }, + }); + }); + + if (isApplicationBootstrapConfig(config)) { + // If the whole platform is destroyed, invoke the `destroy` method + // for all bootstrapped applications as well. + const destroyListener = () => envInjector.destroy(); + const onPlatformDestroyListeners = config.platformInjector.get(PLATFORM_DESTROY_LISTENERS); + onPlatformDestroyListeners.add(destroyListener); + + envInjector.onDestroy(() => { + onErrorSubscription.unsubscribe(); + onPlatformDestroyListeners.delete(destroyListener); + }); + } else { + config.moduleRef.onDestroy(() => { + remove(config.allPlatformModules, config.moduleRef); + onErrorSubscription.unsubscribe(); + }); + } + + return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => { + const initStatus = envInjector.get(ApplicationInitStatus); + initStatus.runInitializers(); + + return initStatus.donePromise.then(() => { + // If the `LOCALE_ID` provider is defined at bootstrap then we set the value for ivy + const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID); + setLocaleId(localeId || DEFAULT_LOCALE_ID); + + if (isApplicationBootstrapConfig(config)) { + const appRef = envInjector.get(ApplicationRef); + if (config.rootComponent !== undefined) { + appRef.bootstrap(config.rootComponent); + } + if (typeof ngDevMode === 'undefined' || ngDevMode) { + const imagePerformanceService = envInjector.get(ImagePerformanceWarning); + imagePerformanceService.start(); + } + return appRef; + } else { + moduleDoBootstrap(config.moduleRef, config.allPlatformModules); + return config.moduleRef; + } + }); + }); + }); +} + +function moduleDoBootstrap( + moduleRef: InternalNgModuleRef, + allPlatformModules: NgModuleRef[], +): void { + const appRef = moduleRef.injector.get(ApplicationRef); + if (moduleRef._bootstrapComponents.length > 0) { + moduleRef._bootstrapComponents.forEach((f) => appRef.bootstrap(f)); + } else if (moduleRef.instance.ngDoBootstrap) { + moduleRef.instance.ngDoBootstrap(appRef); + } else { + throw new RuntimeError( + RuntimeErrorCode.BOOTSTRAP_COMPONENTS_NOT_FOUND, + ngDevMode && + `The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, ` + + `but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` + + `Please define one of these.`, + ); + } + allPlatformModules.push(moduleRef); +} diff --git a/packages/core/src/platform/platform.ts b/packages/core/src/platform/platform.ts index f6aac1eced3..9a14bc0efef 100644 --- a/packages/core/src/platform/platform.ts +++ b/packages/core/src/platform/platform.ts @@ -15,7 +15,8 @@ import {InjectionToken, Injector, StaticProvider} from '../di'; import {INJECTOR_SCOPE} from '../di/scope'; import {RuntimeError, RuntimeErrorCode} from '../errors'; -import {PLATFORM_DESTROY_LISTENERS, PlatformRef} from './platform_ref'; +import {PlatformRef} from './platform_ref'; +import {PLATFORM_DESTROY_LISTENERS} from './platform_destroy_listeners'; let _platformInjector: Injector | null = null; diff --git a/packages/core/src/platform/platform_destroy_listeners.ts b/packages/core/src/platform/platform_destroy_listeners.ts new file mode 100644 index 00000000000..024aa7c0476 --- /dev/null +++ b/packages/core/src/platform/platform_destroy_listeners.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InjectionToken} from '../di'; + +/** + * Internal token that allows to register extra callbacks that should be invoked during the + * `PlatformRef.destroy` operation. This token is needed to avoid a direct reference to the + * `PlatformRef` class (i.e. register the callback via `PlatformRef.onDestroy`), thus making the + * entire class tree-shakeable. + */ +export const PLATFORM_DESTROY_LISTENERS = new InjectionToken>( + ngDevMode ? 'PlatformDestroyListeners' : '', +); diff --git a/packages/core/src/platform/platform_ref.ts b/packages/core/src/platform/platform_ref.ts index d39f7d899e5..a9e5870dcae 100644 --- a/packages/core/src/platform/platform_ref.ts +++ b/packages/core/src/platform/platform_ref.ts @@ -6,47 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus} from '../application/application_init'; import {compileNgModuleFactory} from '../application/application_ngmodule_factory_compiler'; import { _callAndReportToErrorHandler, - ApplicationRef, BootstrapOptions, optionsReducer, - remove, } from '../application/application_ref'; import { getNgZoneOptions, internalProvideZoneChangeDetection, - PROVIDED_NG_ZONE, } from '../change_detection/scheduling/ng_zone_scheduling'; -import { - ChangeDetectionScheduler, - PROVIDED_ZONELESS, -} from '../change_detection/scheduling/zoneless_scheduling'; +import {ChangeDetectionScheduler} from '../change_detection/scheduling/zoneless_scheduling'; import {ChangeDetectionSchedulerImpl} from '../change_detection/scheduling/zoneless_scheduling_impl'; -import {Injectable, InjectionToken, Injector} from '../di'; -import {ErrorHandler} from '../error_handler'; +import {Injectable, Injector} from '../di'; import {RuntimeError, RuntimeErrorCode} from '../errors'; -import {DEFAULT_LOCALE_ID} from '../i18n/localization'; -import {LOCALE_ID} from '../i18n/tokens'; import {Type} from '../interface/type'; import {CompilerOptions} from '../linker'; -import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory'; -import {setLocaleId} from '../render3'; +import {NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory'; import {createNgModuleRefWithProviders} from '../render3/ng_module_ref'; -import {stringify} from '../util/stringify'; -import {getNgZone, NgZone, NoopNgZone} from '../zone/ng_zone'; - -/** - * Internal token that allows to register extra callbacks that should be invoked during the - * `PlatformRef.destroy` operation. This token is needed to avoid a direct reference to the - * `PlatformRef` class (i.e. register the callback via `PlatformRef.onDestroy`), thus making the - * entire class tree-shakeable. - */ -export const PLATFORM_DESTROY_LISTENERS = new InjectionToken>( - ngDevMode ? 'PlatformDestroyListeners' : '', -); +import {getNgZone, NgZone} from '../zone/ng_zone'; +import {bootstrap} from './bootstrap'; +import {PLATFORM_DESTROY_LISTENERS} from './platform_destroy_listeners'; /** * The Angular platform is the entry point for Angular on a web page. @@ -97,52 +77,8 @@ export class PlatformRef { this.injector, allAppProviders, ); - const envInjector = moduleRef.injector; - const ngZone = envInjector.get(NgZone); - return ngZone.run(() => { - moduleRef.resolveInjectorInitializers(); - const exceptionHandler = envInjector.get(ErrorHandler, null); - if (typeof ngDevMode === 'undefined' || ngDevMode) { - if (exceptionHandler === null) { - throw new RuntimeError( - RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP, - 'No ErrorHandler. Is platform module (BrowserModule) included?', - ); - } - 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.', - ); - } - } - - ngZone.runOutsideAngular(() => { - const subscription = ngZone.onError.subscribe({ - next: (error: any) => { - exceptionHandler!.handleError(error); - }, - }); - moduleRef.onDestroy(() => { - remove(this._modules, moduleRef); - subscription.unsubscribe(); - }); - }); - return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => { - const initStatus = envInjector.get(ApplicationInitStatus); - initStatus.runInitializers(); - - return initStatus.donePromise.then(() => { - // If the `LOCALE_ID` provider is defined at bootstrap then we set the value for ivy - const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID); - setLocaleId(localeId || DEFAULT_LOCALE_ID); - this._moduleDoBootstrap(moduleRef); - return moduleRef; - }); - }); - }); + return bootstrap({moduleRef, allPlatformModules: this._modules}); } /** @@ -173,24 +109,6 @@ export class PlatformRef { ); } - private _moduleDoBootstrap(moduleRef: InternalNgModuleRef): void { - const appRef = moduleRef.injector.get(ApplicationRef); - if (moduleRef._bootstrapComponents.length > 0) { - moduleRef._bootstrapComponents.forEach((f) => appRef.bootstrap(f)); - } else if (moduleRef.instance.ngDoBootstrap) { - moduleRef.instance.ngDoBootstrap(appRef); - } else { - throw new RuntimeError( - RuntimeErrorCode.BOOTSTRAP_COMPONENTS_NOT_FOUND, - ngDevMode && - `The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, ` + - `but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` + - `Please define one of these.`, - ); - } - this._modules.push(moduleRef); - } - /** * Registers a listener to be called when the platform is destroyed. */