Skip to content

Commit 5771b18

Browse files
AndrewKushniratscott
authored andcommitted
feat(core): add the bootstrapApplication function (#45674)
This commit implements the `bootstrapApplication` function that allows bootstrapping an application and pass a standalone component as a root component. PR Close #45674
1 parent 612d6e0 commit 5771b18

16 files changed

Lines changed: 411 additions & 35 deletions

File tree

goldens/public-api/core/errors.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const enum RuntimeErrorCode {
4545
// (undocumented)
4646
INVALID_INJECTION_TOKEN = 204,
4747
// (undocumented)
48+
MISSING_GENERATED_DEF = 906,
49+
// (undocumented)
4850
MISSING_INJECTION_CONTEXT = 203,
4951
// (undocumented)
5052
MULTIPLE_COMPONENTS_MATCH = -300,
@@ -65,6 +67,8 @@ export const enum RuntimeErrorCode {
6567
// (undocumented)
6668
TEMPLATE_STRUCTURE_ERROR = 305,
6769
// (undocumented)
70+
TYPE_IS_NOT_STANDALONE = 907,
71+
// (undocumented)
6872
UNKNOWN_BINDING = 303,
6973
// (undocumented)
7074
UNKNOWN_ELEMENT = 304,

goldens/public-api/platform-browser/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
```ts
66

7+
import { ApplicationRef } from '@angular/core';
78
import { ComponentRef } from '@angular/core';
89
import { DebugElement } from '@angular/core';
910
import { DebugNode } from '@angular/core';
@@ -14,12 +15,21 @@ import { ModuleWithProviders } from '@angular/core';
1415
import { NgZone } from '@angular/core';
1516
import { PlatformRef } from '@angular/core';
1617
import { Predicate } from '@angular/core';
18+
import { Provider } from '@angular/core';
1719
import { Sanitizer } from '@angular/core';
1820
import { SecurityContext } from '@angular/core';
1921
import { StaticProvider } from '@angular/core';
2022
import { Type } from '@angular/core';
2123
import { Version } from '@angular/core';
2224

25+
// @public
26+
export interface ApplicationConfig {
27+
providers: Provider[];
28+
}
29+
30+
// @public
31+
export function bootstrapApplication(rootComponent: Type<unknown>, options?: ApplicationConfig): Promise<ApplicationRef>;
32+
2333
// @public
2434
export class BrowserModule {
2535
constructor(parentModule: BrowserModule | null);

packages/core/src/application_ref.ts

Lines changed: 139 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import {ApplicationInitStatus} from './application_init';
1515
import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens';
1616
import {getCompilerFacade, JitCompilerUsage} from './compiler/compiler_facade';
1717
import {Console} from './console';
18+
import {importProvidersFrom} from './di';
1819
import {Injectable} from './di/injectable';
1920
import {InjectionToken} from './di/injection_token';
2021
import {Injector} from './di/injector';
21-
import {StaticProvider} from './di/interface/provider';
22+
import {Provider, StaticProvider} from './di/interface/provider';
23+
import {EnvironmentInjector} from './di/r3_injector';
2224
import {INJECTOR_SCOPE} from './di/scope';
2325
import {ErrorHandler} from './error_handler';
2426
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from './errors';
@@ -33,9 +35,13 @@ import {InternalViewRef, ViewRef} from './linker/view_ref';
3335
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from './metadata/resource_loading';
3436
import {assertNgModuleType} from './render3/assert';
3537
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
38+
import {getComponentDef} from './render3/definition';
39+
import {assertStandaloneComponentType} from './render3/errors';
40+
import {StandaloneService} from './render3/features/standalone_feature';
3641
import {setLocaleId} from './render3/i18n/i18n_locale_id';
3742
import {setJitOptions} from './render3/jit/jit_options';
38-
import {NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
43+
import {isStandalone} from './render3/jit/module';
44+
import {createEnvironmentInjector, NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
3945
import {publishDefaultGlobalUtils as _publishDefaultGlobalUtils} from './render3/util/global_utils';
4046
import {Testability, TestabilityRegistry} from './testability/testability';
4147
import {isPromise} from './util/lang';
@@ -143,11 +149,107 @@ export function createPlatform(injector: Injector): PlatformRef {
143149
publishDefaultGlobalUtils();
144150
_platformInjector = injector;
145151
const platform = injector.get(PlatformRef);
146-
const inits = injector.get(PLATFORM_INITIALIZER, null);
147-
if (inits) inits.forEach(initFn => initFn());
152+
runPlatformInitializers(injector);
148153
return platform;
149154
}
150155

156+
/**
157+
* The goal of this function is to bootstrap a platform injector,
158+
* but avoid referencing `PlatformRef` class.
159+
* This function is needed for bootstrapping a Standalone Component.
160+
*/
161+
export function createOrReusePlatformInjector(providers: StaticProvider[] = []): Injector {
162+
// If a platform injector already exists, it means that the platform
163+
// is already bootstrapped and no additional actions are required.
164+
if (_platformInjector) return _platformInjector;
165+
166+
// Otherwise, setup a new platform injector and run platform initializers.
167+
const injector = createPlatformInjector(providers);
168+
_platformInjector = injector;
169+
publishDefaultGlobalUtils();
170+
runPlatformInitializers(injector);
171+
return injector;
172+
}
173+
174+
export function runPlatformInitializers(injector: Injector): void {
175+
const inits = injector.get(PLATFORM_INITIALIZER, null);
176+
if (inits) {
177+
inits.forEach((init: any) => init());
178+
}
179+
}
180+
181+
/**
182+
* Internal bootstrap application API that implements the core bootstrap logic.
183+
*
184+
* Platforms (such as `platform-browser`) may require different set of application and platform
185+
* providers for an application to function correctly. As a result, platforms may use this function
186+
* internally and supply the necessary providers during the bootstrap, while exposing
187+
* platform-specific APIs as a part of their public API.
188+
*
189+
* @returns A promise that returns an `ApplicationRef` instance once resolved.
190+
*/
191+
export function bootstrapApplication(config: {
192+
rootComponent: Type<unknown>,
193+
appProviders?: Provider[],
194+
platformProviders?: Provider[],
195+
}): Promise<ApplicationRef> {
196+
const {rootComponent, appProviders, platformProviders} = config;
197+
NG_DEV_MODE && assertStandaloneComponentType(rootComponent);
198+
199+
const platformInjector = createOrReusePlatformInjector(platformProviders as StaticProvider[]);
200+
201+
const ngZone = new NgZone(getNgZoneOptions());
202+
203+
return ngZone.run(() => {
204+
// Create root application injector based on a set of providers configured at the platform
205+
// bootstrap level as well as providers passed to the bootstrap call by a user.
206+
const allAppProviders = [
207+
{provide: NgZone, useValue: ngZone}, //
208+
...(appProviders || []), //
209+
// Collect providers from the root standalone component
210+
// and all of its dependencies (NgModule, other standalone Components)
211+
// and make those providers available in the DI tree.
212+
...importProvidersFrom(rootComponent),
213+
];
214+
const appInjector = createEnvironmentInjector(
215+
allAppProviders, platformInjector as EnvironmentInjector, 'Environment Injector');
216+
217+
// Instruct `StandaloneService` to use the `appInjector` as an injector for this
218+
// root component, so that there is no extra injector instance created.
219+
const componentDef = getComponentDef(rootComponent)!;
220+
appInjector.get(StandaloneService).setInjector(componentDef, appInjector);
221+
222+
const exceptionHandler: ErrorHandler|null = appInjector.get(ErrorHandler, null);
223+
if (NG_DEV_MODE && !exceptionHandler) {
224+
throw new RuntimeError(
225+
RuntimeErrorCode.ERROR_HANDLER_NOT_FOUND,
226+
'No `ErrorHandler` found in the Dependency Injection tree.');
227+
}
228+
229+
let onErrorSubscription: Subscription;
230+
ngZone.runOutsideAngular(() => {
231+
onErrorSubscription = ngZone.onError.subscribe({
232+
next: (error: any) => {
233+
exceptionHandler!.handleError(error);
234+
}
235+
});
236+
});
237+
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
238+
const initStatus = appInjector.get(ApplicationInitStatus);
239+
initStatus.runInitializers();
240+
return initStatus.donePromise.then(() => {
241+
const localeId = appInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
242+
setLocaleId(localeId || DEFAULT_LOCALE_ID);
243+
244+
const appRef = appInjector.get(ApplicationRef);
245+
appRef.onDestroy(() => onErrorSubscription.unsubscribe());
246+
appRef.bootstrap(rootComponent);
247+
return appRef;
248+
});
249+
});
250+
});
251+
}
252+
151253
/**
152254
* Creates a factory for a platform. Can be used to provide or override `Providers` specific to
153255
* your application's runtime needs, such as `PLATFORM_INITIALIZER` and `PLATFORM_ID`.
@@ -242,8 +344,6 @@ export function getPlatform(): PlatformRef|null {
242344

243345
/**
244346
* Provides additional options to the bootstraping process.
245-
*
246-
*
247347
*/
248348
export interface BootstrapOptions {
249349
/**
@@ -326,10 +426,7 @@ export class PlatformRef {
326426
// as instantiating the module creates some providers eagerly.
327427
// So we create a mini parent injector that just contains the new NgZone and
328428
// pass that as parent to the NgModuleFactory.
329-
const ngZoneOption = options ? options.ngZone : undefined;
330-
const ngZoneEventCoalescing = (options && options.ngZoneEventCoalescing) || false;
331-
const ngZoneRunCoalescing = (options && options.ngZoneRunCoalescing) || false;
332-
const ngZone = getNgZone(ngZoneOption, {ngZoneEventCoalescing, ngZoneRunCoalescing});
429+
const ngZone = getNgZone(options?.ngZone, getNgZoneOptions(options));
333430
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
334431
// Note: Create ngZoneInjector within ngZone.run so that all of the instantiated services are
335432
// created within the Angular zone
@@ -456,19 +553,31 @@ export class PlatformRef {
456553
}
457554
}
458555

459-
function getNgZone(
460-
ngZoneOption: NgZone|'zone.js'|'noop'|undefined,
461-
extra?: {ngZoneEventCoalescing: boolean, ngZoneRunCoalescing: boolean}): NgZone {
556+
// Set of options recognized by the NgZone.
557+
interface NgZoneOptions {
558+
enableLongStackTrace: boolean;
559+
shouldCoalesceEventChangeDetection: boolean;
560+
shouldCoalesceRunChangeDetection: boolean;
561+
}
562+
563+
// Transforms a set of `BootstrapOptions` (supported by the NgModule-based bootstrap APIs) ->
564+
// `NgZoneOptions` that are recognized by the NgZone constructor. Passing no options will result in
565+
// a set of default options returned.
566+
function getNgZoneOptions(options?: BootstrapOptions): NgZoneOptions {
567+
return {
568+
enableLongStackTrace: typeof ngDevMode === 'undefined' ? false : !!ngDevMode,
569+
shouldCoalesceEventChangeDetection: !!(options && options.ngZoneEventCoalescing) || false,
570+
shouldCoalesceRunChangeDetection: !!(options && options.ngZoneRunCoalescing) || false,
571+
};
572+
}
573+
574+
function getNgZone(ngZoneToUse: NgZone|'zone.js'|'noop'|undefined, options: NgZoneOptions): NgZone {
462575
let ngZone: NgZone;
463576

464-
if (ngZoneOption === 'noop') {
577+
if (ngZoneToUse === 'noop') {
465578
ngZone = new NoopNgZone();
466579
} else {
467-
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({
468-
enableLongStackTrace: typeof ngDevMode === 'undefined' ? false : !!ngDevMode,
469-
shouldCoalesceEventChangeDetection: !!extra?.ngZoneEventCoalescing,
470-
shouldCoalesceRunChangeDetection: !!extra?.ngZoneRunCoalescing
471-
});
580+
ngZone = (ngZoneToUse === 'zone.js' ? undefined : ngZoneToUse) || new NgZone(options);
472581
}
473582
return ngZone;
474583
}
@@ -815,15 +924,20 @@ export class ApplicationRef {
815924
bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>, rootSelectorOrNode?: string|any):
816925
ComponentRef<C> {
817926
NG_DEV_MODE && this.warnIfDestroyed();
927+
const isComponentFactory = componentOrFactory instanceof ComponentFactory;
928+
818929
if (!this._initStatus.done) {
819-
const errorMessage = (typeof ngDevMode === 'undefined' || ngDevMode) ?
820-
'Cannot bootstrap as there are still asynchronous initializers running. ' +
821-
'Bootstrap components in the `ngDoBootstrap` method of the root module.' :
822-
'';
823-
throw new RuntimeError(RuntimeErrorCode.ASYNC_INITIALIZERS_STILL_RUNNING, errorMessage);
930+
const standalone = !isComponentFactory && isStandalone(componentOrFactory);
931+
const errorMessage =
932+
'Cannot bootstrap as there are still asynchronous initializers running.' +
933+
(standalone ? '' :
934+
' Bootstrap components in the `ngDoBootstrap` method of the root module.');
935+
throw new RuntimeError(
936+
RuntimeErrorCode.ASYNC_INITIALIZERS_STILL_RUNNING, NG_DEV_MODE && errorMessage);
824937
}
938+
825939
let componentFactory: ComponentFactory<C>;
826-
if (componentOrFactory instanceof ComponentFactory) {
940+
if (isComponentFactory) {
827941
componentFactory = componentOrFactory;
828942
} else {
829943
const resolver = this._injector.get(ComponentFactoryResolver);

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS} from './application_ref';
9+
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, bootstrapApplication as ɵbootstrapApplication} from './application_ref';
1010
export {APP_ID_RANDOM_PROVIDER as ɵAPP_ID_RANDOM_PROVIDER} from './application_tokens';
1111
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
1212
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';

packages/core/src/errors.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ export const enum RuntimeErrorCode {
6363
VIEW_ALREADY_ATTACHED = 902,
6464
INVALID_INHERITANCE = 903,
6565
UNSAFE_VALUE_IN_RESOURCE_URL = 904,
66-
UNSAFE_VALUE_IN_SCRIPT = 905
66+
UNSAFE_VALUE_IN_SCRIPT = 905,
67+
MISSING_GENERATED_DEF = 906,
68+
TYPE_IS_NOT_STANDALONE = 907,
6769
}
6870

6971
/**

packages/core/src/render3/errors.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,30 @@
1010
import {RuntimeError, RuntimeErrorCode} from '../errors';
1111
import {Type} from '../interface/type';
1212

13+
import {getComponentDef} from './definition';
1314
import {TNode} from './interfaces/node';
1415
import {LView, TVIEW} from './interfaces/view';
1516
import {INTERPOLATION_DELIMITER} from './util/misc_utils';
1617
import {stringifyForError} from './util/stringify_utils';
1718

18-
19+
/** Verifies that a given type is a Standalone Component. */
20+
export function assertStandaloneComponentType(type: Type<unknown>) {
21+
const componentDef = getComponentDef(type);
22+
if (!componentDef) {
23+
throw new RuntimeError(
24+
RuntimeErrorCode.MISSING_GENERATED_DEF,
25+
`The ${stringifyForError(type)} is not an Angular component, ` +
26+
`make sure it has the \`@Component\` decorator.`);
27+
}
28+
if (!componentDef.standalone) {
29+
throw new RuntimeError(
30+
RuntimeErrorCode.TYPE_IS_NOT_STANDALONE,
31+
`The ${stringifyForError(type)} component is not marked as standalone, ` +
32+
`but Angular expects to have a standalone component here. ` +
33+
`Please make sure the ${stringifyForError(type)} component has ` +
34+
`the \`standalone: true\` flag in the decorator.`);
35+
}
36+
}
1937

2038
/** Called when there are multiple component selectors that match a given node */
2139
export function throwMultipleComponentError(

packages/core/src/render3/features/standalone_feature.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {createEnvironmentInjector} from '../ng_module_ref';
1818
* created on demand in case of dynamic component instantiation and contain ambient providers
1919
* collected from the imports graph rooted at a given standalone component.
2020
*/
21-
class StandaloneService implements OnDestroy {
21+
export class StandaloneService implements OnDestroy {
2222
cachedInjectors = new Map<ComponentDef<unknown>, EnvironmentInjector|null>();
2323

2424
constructor(private _injector: EnvironmentInjector) {}

packages/core/src/render3/jit/module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export function compileNgModuleDefs(
195195
});
196196
}
197197

198-
function isStandalone<T>(type: Type<T>) {
198+
export function isStandalone<T>(type: Type<T>) {
199199
const def = getComponentDef(type) || getDirectiveDef(type) || getPipeDef(type);
200200
return def !== null ? def.standalone : false;
201201
}

packages/core/test/bundling/animations/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,9 @@
812812
{
813813
"name": "getDeclarationTNode"
814814
},
815+
{
816+
"name": "getDirectiveDef"
817+
},
815818
{
816819
"name": "getFactoryDef"
817820
},

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,9 @@
860860
{
861861
"name": "getDeclarationTNode"
862862
},
863+
{
864+
"name": "getDirectiveDef"
865+
},
863866
{
864867
"name": "getFactoryDef"
865868
},

0 commit comments

Comments
 (0)