angular/packages/platform-server/src/utils.ts
Alex Rickabaugh 7de1469be6 feat(core): introduce EnvironmentProviders wrapper type (#47669)
This commit introduces a new type `EnvironmentProviders` which can be used
in contexts where Angular accepted `Provider`s destined for
`EnvironmentInjector`s. This includes contexts such as `@NgModule.providers`
and `Route.providers`.

The new type is useful for preventing such providers from accidentally
ending up in `@Component.providers`. It can be used as the return type of
provider functions (such as `provideRouter`) to enforce this safety.

Because `Provider` allows `any[]` nested arrays, the compile-time safety
provided by `EnvironmentProviders` is easily circumvented. However, the
runtime shape of `EnvironmentProviders` is not compatible with component
injectors and will result in a runtime error if it leaks through (NG0207).

A new function `makeEnvironmentProviders` is used to construct this new type
from an array of providers.

The existing `importProvidersFrom` operation previously returned a very
similar type `ImportedNgModuleProviders` which had the same goal. This
machinery is switched over to use the new `EnvironmentProviders` interface
instead (in fact, `ImportedNgModuleProviders` is now just an alias to
`EnvironmentProviders`).

PR Close #47669
2022-10-07 14:03:13 -07:00

233 lines
9 KiB
TypeScript

/**
* @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 {ApplicationRef, EnvironmentProviders, importProvidersFrom, InjectionToken, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core';
import {BrowserModule, ɵTRANSITION_ID} from '@angular/platform-browser';
import {first} from 'rxjs/operators';
import {PlatformState} from './platform_state';
import {platformDynamicServer, platformServer, ServerModule} from './server';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
import {TRANSFER_STATE_SERIALIZATION_PROVIDERS} from './transfer_state';
interface PlatformOptions {
document?: string|Document;
url?: string;
platformProviders?: Provider[];
}
function _getPlatform(
platformFactory: (extraProviders: StaticProvider[]) => PlatformRef,
options: PlatformOptions): PlatformRef {
const extraProviders = options.platformProviders ?? [];
return platformFactory([
{provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}},
extraProviders
]);
}
/**
* Adds the `ng-server-context` attribute to host elements of all bootstrapped components
* within a given application.
*/
function appendServerContextInfo(serverContext: string, applicationRef: ApplicationRef) {
applicationRef.components.forEach(componentRef => {
const renderer = componentRef.injector.get(Renderer2);
const element = componentRef.location.nativeElement;
if (element) {
renderer.setAttribute(element, 'ng-server-context', serverContext);
}
});
}
function _render<T>(
platform: PlatformRef,
bootstrapPromise: Promise<NgModuleRef<T>|ApplicationRef>): Promise<string> {
return bootstrapPromise.then((moduleOrApplicationRef) => {
const environmentInjector = moduleOrApplicationRef.injector;
const transitionId = environmentInjector.get(ɵTRANSITION_ID, null);
if (!transitionId) {
throw new Error(
`renderModule[Factory]() requires the use of BrowserModule.withServerTransition() to ensure
the server-rendered app can be properly bootstrapped into a client app.`);
}
const applicationRef: ApplicationRef = moduleOrApplicationRef instanceof ApplicationRef ?
moduleOrApplicationRef :
environmentInjector.get(ApplicationRef);
const serverContext =
sanitizeServerContext(environmentInjector.get(SERVER_CONTEXT, DEFAULT_SERVER_CONTEXT));
return applicationRef.isStable.pipe((first((isStable: boolean) => isStable)))
.toPromise()
.then(() => {
appendServerContextInfo(serverContext, applicationRef);
const platformState = platform.injector.get(PlatformState);
const asyncPromises: Promise<any>[] = [];
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
const callbacks = environmentInjector.get(BEFORE_APP_SERIALIZED, null);
if (callbacks) {
for (const callback of callbacks) {
try {
const callbackResult = callback();
if (ɵisPromise(callbackResult)) {
// TODO: in TS3.7, callbackResult is void.
asyncPromises.push(callbackResult as any);
}
} catch (e) {
// Ignore exceptions.
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
}
}
}
const complete = () => {
const output = platformState.renderToString();
platform.destroy();
return output;
};
if (asyncPromises.length === 0) {
return complete();
}
return Promise
.all(asyncPromises.map(asyncPromise => {
return asyncPromise.catch(e => {
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
});
}))
.then(complete);
});
});
}
/**
* Specifies the value that should be used if no server context value has been provided.
*/
const DEFAULT_SERVER_CONTEXT = 'other';
/**
* An internal token that allows providing extra information about the server context
* (e.g. whether SSR or SSG was used). The value is a string and characters other
* than [a-zA-Z0-9\-] are removed. See the default value in `DEFAULT_SERVER_CONTEXT` const.
*/
export const SERVER_CONTEXT = new InjectionToken<string>('SERVER_CONTEXT');
/**
* Sanitizes provided server context:
* - removes all characters other than a-z, A-Z, 0-9 and `-`
* - returns `other` if nothing is provided or the string is empty after sanitization
*/
function sanitizeServerContext(serverContext: string): string {
const context = serverContext.replace(/[^a-zA-Z0-9\-]/g, '');
return context.length > 0 ? context : DEFAULT_SERVER_CONTEXT;
}
/**
* Bootstraps an application using provided NgModule and serializes the page content to string.
*
* @param moduleType A reference to an NgModule that should be used for bootstrap.
* @param options Additional configuration for the render operation:
* - `document` - the document of the page to render, either as an HTML string or
* as a reference to the `document` instance.
* - `url` - the URL for the current render request.
* - `extraProviders` - set of platform level providers for the current render request.
*
* @publicApi
*/
export function renderModule<T>(moduleType: Type<T>, options: {
document?: string|Document,
url?: string,
extraProviders?: StaticProvider[],
}): Promise<string> {
const {document, url, extraProviders: platformProviders} = options;
const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders});
return _render(platform, platform.bootstrapModule(moduleType));
}
/**
* Bootstraps an instance of an Angular application and renders it to a string.
*
* Note: the root component passed into this function *must* be a standalone one (should have the
* `standalone: true` flag in the `@Component` decorator config).
*
* ```typescript
* @Component({
* standalone: true,
* template: 'Hello world!'
* })
* class RootComponent {}
*
* const output: string = await renderApplication(RootComponent, {appId: 'server-app'});
* ```
*
* @param rootComponent A reference to a Standalone Component that should be rendered.
* @param options Additional configuration for the render operation:
* - `appId` - a string identifier of this application. The appId is used to prefix all
* server-generated stylings and state keys of the application in TransferState
* use-cases.
* - `document` - the document of the page to render, either as an HTML string or
* as a reference to the `document` instance.
* - `url` - the URL for the current render request.
* - `providers` - set of application level providers for the current render request.
* - `platformProviders` - the platform level providers for the current render request.
*
* @returns A Promise, that returns serialized (to a string) rendered page, once resolved.
*
* @publicApi
* @developerPreview
*/
export function renderApplication<T>(rootComponent: Type<T>, options: {
appId: string,
document?: string|Document,
url?: string,
providers?: Array<Provider|EnvironmentProviders>,
platformProviders?: Provider[],
}): Promise<string> {
const {document, url, platformProviders, appId} = options;
const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders});
const appProviders = [
importProvidersFrom(BrowserModule.withServerTransition({appId})),
importProvidersFrom(ServerModule),
...TRANSFER_STATE_SERIALIZATION_PROVIDERS,
...(options.providers ?? []),
];
return _render(platform, internalCreateApplication({rootComponent, appProviders}));
}
/**
* Bootstraps an application using provided {@link NgModuleFactory} and serializes the page content
* to string.
*
* @param moduleFactory An instance of the {@link NgModuleFactory} that should be used for
* bootstrap.
* @param options Additional configuration for the render operation:
* - `document` - the document of the page to render, either as an HTML string or
* as a reference to the `document` instance.
* - `url` - the URL for the current render request.
* - `extraProviders` - set of platform level providers for the current render request.
*
* @publicApi
*
* @deprecated
* This symbol is no longer necessary as of Angular v13.
* Use {@link renderModule} API instead.
*/
export function renderModuleFactory<T>(moduleFactory: NgModuleFactory<T>, options: {
document?: string,
url?: string,
extraProviders?: StaticProvider[],
}): Promise<string> {
const {document, url, extraProviders: platformProviders} = options;
const platform = _getPlatform(platformServer, {document, url, platformProviders});
return _render(platform, platform.bootstrapModuleFactory(moduleFactory));
}