mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(platform-server): include info about enabled features into ng-server-context (#49773)
This commit updates the logic that adds the "ng-server-context" attribute to the root elements to also include information about SSR feature enabled got an application. PR Close #49773
This commit is contained in:
parent
1da3e5f0bd
commit
7ee542d263
6 changed files with 118 additions and 14 deletions
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {APP_BOOTSTRAP_LISTENER, ApplicationRef, inject, InjectionToken, makeStateKey, Provider, StateKey, TransferState, ɵInitialRenderPendingTasks as InitialRenderPendingTasks} from '@angular/core';
|
||||
import {APP_BOOTSTRAP_LISTENER, ApplicationRef, inject, InjectionToken, makeStateKey, Provider, StateKey, TransferState, ɵENABLED_SSR_FEATURES as ENABLED_SSR_FEATURES, ɵInitialRenderPendingTasks as InitialRenderPendingTasks} from '@angular/core';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {first, tap} from 'rxjs/operators';
|
||||
|
||||
|
|
@ -149,6 +149,7 @@ export function withHttpTransferCache(): Provider[] {
|
|||
{
|
||||
provide: CACHE_STATE,
|
||||
useFactory: () => {
|
||||
inject(ENABLED_SSR_FEATURES).add('httpcache');
|
||||
return {isCacheActive: true};
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -112,3 +112,15 @@ export const CSP_NONCE = new InjectionToken<string|null>('CSP nonce', {
|
|||
return getDocument().body.querySelector('[ngCspNonce]')?.getAttribute('ngCspNonce') || null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal token to collect all SSR-related features enabled for this application.
|
||||
*
|
||||
* Note: the token is in `core` to let other packages register features (the `core`
|
||||
* package is imported in other packages).
|
||||
*/
|
||||
export const ENABLED_SSR_FEATURES = new InjectionToken<Set<string>>(
|
||||
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'ENABLED_SSR_FEATURES' : '', {
|
||||
providedIn: 'root',
|
||||
factory: () => new Set(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication} from './application_ref';
|
||||
export {ENABLED_SSR_FEATURES as ɵENABLED_SSR_FEATURES} from './application_tokens';
|
||||
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
|
||||
export {Console as ɵConsole} from './console';
|
||||
export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import {first} from 'rxjs/operators';
|
||||
|
||||
import {APP_BOOTSTRAP_LISTENER, ApplicationRef} from '../application_ref';
|
||||
import {PLATFORM_ID} from '../application_tokens';
|
||||
import {ENABLED_SSR_FEATURES, PLATFORM_ID} from '../application_tokens';
|
||||
import {Console} from '../console';
|
||||
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, Injector, makeEnvironmentProviders} from '../di';
|
||||
import {inject} from '../di/injector_compatibility';
|
||||
|
|
@ -109,12 +109,13 @@ export function withDomHydration(): EnvironmentProviders {
|
|||
{
|
||||
provide: IS_HYDRATION_FEATURE_ENABLED,
|
||||
useFactory: () => {
|
||||
let isEnabled = true;
|
||||
if (isBrowser()) {
|
||||
// On the client, verify that the server response contains
|
||||
// hydration annotations. Otherwise, keep hydration disabled.
|
||||
const transferState = inject(TransferState, {optional: true});
|
||||
const hasHydrationAnnotations = !!transferState?.get(NGH_DATA_KEY, null);
|
||||
if (!hasHydrationAnnotations) {
|
||||
isEnabled = !!transferState?.get(NGH_DATA_KEY, null);
|
||||
if (!isEnabled) {
|
||||
const console = inject(Console);
|
||||
const message = formatRuntimeError(
|
||||
RuntimeErrorCode.MISSING_HYDRATION_ANNOTATIONS,
|
||||
|
|
@ -126,10 +127,11 @@ export function withDomHydration(): EnvironmentProviders {
|
|||
// tslint:disable-next-line:no-console
|
||||
console.warn(message);
|
||||
}
|
||||
return hasHydrationAnnotations;
|
||||
}
|
||||
// We are running on the server, always return `true`.
|
||||
return true;
|
||||
if (isEnabled) {
|
||||
inject(ENABLED_SSR_FEATURES).add('hydration');
|
||||
}
|
||||
return isEnabled;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ApplicationRef, InjectionToken, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵannotateForHydration as annotateForHydration, ɵInitialRenderPendingTasks as InitialRenderPendingTasks, ɵIS_HYDRATION_FEATURE_ENABLED as IS_HYDRATION_FEATURE_ENABLED, ɵisPromise} from '@angular/core';
|
||||
import {ApplicationRef, InjectionToken, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵannotateForHydration as annotateForHydration, ɵENABLED_SSR_FEATURES as ENABLED_SSR_FEATURES, ɵInitialRenderPendingTasks as InitialRenderPendingTasks, ɵIS_HYDRATION_FEATURE_ENABLED as IS_HYDRATION_FEATURE_ENABLED, ɵisPromise} from '@angular/core';
|
||||
import {first} from 'rxjs/operators';
|
||||
|
||||
import {PlatformState} from './platform_state';
|
||||
|
|
@ -33,7 +33,14 @@ function _getPlatform(
|
|||
* Adds the `ng-server-context` attribute to host elements of all bootstrapped components
|
||||
* within a given application.
|
||||
*/
|
||||
function appendServerContextInfo(serverContext: string, applicationRef: ApplicationRef) {
|
||||
function appendServerContextInfo(applicationRef: ApplicationRef) {
|
||||
const injector = applicationRef.injector;
|
||||
let serverContext = sanitizeServerContext(injector.get(SERVER_CONTEXT, DEFAULT_SERVER_CONTEXT));
|
||||
const features = injector.get(ENABLED_SSR_FEATURES);
|
||||
if (features.size > 0) {
|
||||
// Append features information into the server context value.
|
||||
serverContext += `|${Array.from(features).join(',')}`;
|
||||
}
|
||||
applicationRef.components.forEach(componentRef => {
|
||||
const renderer = componentRef.injector.get(Renderer2);
|
||||
const element = componentRef.location.nativeElement;
|
||||
|
|
@ -51,15 +58,11 @@ function _render<T>(
|
|||
const applicationRef: ApplicationRef = moduleOrApplicationRef instanceof ApplicationRef ?
|
||||
moduleOrApplicationRef :
|
||||
environmentInjector.get(ApplicationRef);
|
||||
const serverContext =
|
||||
sanitizeServerContext(environmentInjector.get(SERVER_CONTEXT, DEFAULT_SERVER_CONTEXT));
|
||||
const isStablePromise =
|
||||
applicationRef.isStable.pipe((first((isStable: boolean) => isStable))).toPromise();
|
||||
const pendingTasks = environmentInjector.get(InitialRenderPendingTasks);
|
||||
const pendingTasksPromise = pendingTasks.whenAllTasksComplete;
|
||||
return Promise.allSettled([isStablePromise, pendingTasksPromise]).then(() => {
|
||||
appendServerContextInfo(serverContext, applicationRef);
|
||||
|
||||
const platformState = platform.injector.get(PlatformState);
|
||||
|
||||
const asyncPromises: Promise<any>[] = [];
|
||||
|
|
@ -88,6 +91,7 @@ function _render<T>(
|
|||
}
|
||||
|
||||
const complete = () => {
|
||||
appendServerContextInfo(applicationRef);
|
||||
const output = platformState.renderToString();
|
||||
platform.destroy();
|
||||
return output;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {HttpClientTestingModule, HttpTestingController} from '@angular/common/ht
|
|||
import {ApplicationConfig, ApplicationRef, Component, destroyPlatform, EnvironmentProviders, getPlatform, HostListener, Inject, inject as coreInject, Injectable, Input, makeStateKey, mergeApplicationConfig, NgModule, NgZone, PLATFORM_ID, Provider, TransferState, Type, ViewEncapsulation} from '@angular/core';
|
||||
import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pending_tasks';
|
||||
import {TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {bootstrapApplication, BrowserModule, Title} from '@angular/platform-browser';
|
||||
import {bootstrapApplication, BrowserModule, provideClientHydration, Title, withNoDomReuse, withNoHttpTransferCache} from '@angular/platform-browser';
|
||||
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, provideServerRendering, renderModule, ServerModule} from '@angular/platform-server';
|
||||
import {provideRouter, RouterOutlet, Routes} from '@angular/router';
|
||||
import {Observable} from 'rxjs';
|
||||
|
|
@ -903,6 +903,90 @@ describe('platform-server integration', () => {
|
|||
});
|
||||
}));
|
||||
|
||||
it('includes a set of features into `ng-server-context` attribute', waitForAsync(() => {
|
||||
const options = {
|
||||
document: doc,
|
||||
};
|
||||
const providers = [{
|
||||
provide: SERVER_CONTEXT,
|
||||
useValue: 'ssg',
|
||||
}];
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `<div>Works!</div>`,
|
||||
})
|
||||
class SimpleApp {
|
||||
}
|
||||
|
||||
const bootstrap = renderApplication(
|
||||
getStandaloneBoostrapFn(SimpleApp, [provideClientHydration()]),
|
||||
{...options, platformProviders: providers});
|
||||
bootstrap.then(output => {
|
||||
// HttpClient cache and DOM hydration are enabled by default.
|
||||
expect(output).toMatch(/ng-server-context="ssg\|httpcache,hydration"/);
|
||||
called = true;
|
||||
});
|
||||
}));
|
||||
|
||||
it('should include a set of features into `ng-server-context` attribute ' +
|
||||
'(excluding disabled hydration feature)',
|
||||
waitForAsync(() => {
|
||||
const options = {
|
||||
document: doc,
|
||||
};
|
||||
const providers = [{
|
||||
provide: SERVER_CONTEXT,
|
||||
useValue: 'ssg',
|
||||
}];
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `<div>Works!</div>`,
|
||||
})
|
||||
class SimpleApp {
|
||||
}
|
||||
|
||||
const bootstrap = renderApplication(
|
||||
getStandaloneBoostrapFn(SimpleApp, [provideClientHydration(withNoDomReuse())]),
|
||||
{...options, platformProviders: providers});
|
||||
bootstrap.then(output => {
|
||||
// Dom hydration is disabled, so it should not be included.
|
||||
expect(output).toMatch(/ng-server-context="ssg\|httpcache"/);
|
||||
called = true;
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not include features into `ng-server-context` attribute ' +
|
||||
'when all features are disabled',
|
||||
waitForAsync(() => {
|
||||
const options = {
|
||||
document: doc,
|
||||
};
|
||||
const providers = [{
|
||||
provide: SERVER_CONTEXT,
|
||||
useValue: 'ssg',
|
||||
}];
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `<div>Works!</div>`,
|
||||
})
|
||||
class SimpleApp {
|
||||
}
|
||||
|
||||
const bootstrap = renderApplication(
|
||||
getStandaloneBoostrapFn(
|
||||
SimpleApp,
|
||||
[provideClientHydration(withNoDomReuse(), withNoHttpTransferCache())]),
|
||||
{...options, platformProviders: providers});
|
||||
bootstrap.then(output => {
|
||||
// All features were disabled, so none of them are included.
|
||||
expect(output).toMatch(/ng-server-context="ssg"/);
|
||||
called = true;
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle false values on attributes', waitForAsync(() => {
|
||||
const options = {document: doc};
|
||||
const bootstrap = isStandalone ? renderApplication(MyHostComponentStandalone, options) :
|
||||
|
|
|
|||
Loading…
Reference in a new issue