Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 25 additions & 19 deletions packages/angular/ssr/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export class AngularServerApp {
constructor(private readonly options: Readonly<AngularServerAppOptions> = {}) {
this.allowStaticRouteRender = this.options.allowStaticRouteRender ?? false;
this.hooks = options.hooks ?? new Hooks();

if (this.manifest.inlineCriticalCss) {
this.inlineCriticalCssProcessor = new InlineCriticalCssProcessor((path: string) => {
const fileName = path.split('/').pop() ?? path;

return this.assets.getServerAsset(fileName).text();
});
}
}

/**
Expand Down Expand Up @@ -267,7 +275,7 @@ export class AngularServerApp {
const platformProviders: StaticProvider[] = [];

const {
manifest: { bootstrap, inlineCriticalCss, locale },
manifest: { bootstrap, locale },
assets,
} = this;

Expand Down Expand Up @@ -315,49 +323,47 @@ export class AngularServerApp {
this.boostrap ??= await bootstrap();
let html = await assets.getIndexServerHtml().text();
html = await this.runTransformsOnHtml(html, url, preload);
html = await renderAngular(

const { content } = await renderAngular(
html,
this.boostrap,
url,
platformProviders,
SERVER_CONTEXT_VALUE[renderMode],
);

if (!inlineCriticalCss) {
return new Response(html, responseInit);
}

this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path: string) => {
const fileName = path.split('/').pop() ?? path;

return this.assets.getServerAsset(fileName).text();
});

const { inlineCriticalCssProcessor, criticalCssLRUCache, textDecoder } = this;

// Use a stream to send the response before inlining critical CSS, improving performance via header flushing.
// Use a stream to send the response before finishing rendering and inling critical CSS, improving performance via header flushing.
const stream = new ReadableStream({
async start(controller) {
let htmlWithCriticalCss;
const renderedHtml = await content();

if (!inlineCriticalCssProcessor) {
controller.enqueue(textDecoder.encode(renderedHtml));
controller.close();

return;
}

let htmlWithCriticalCss;
try {
if (renderMode === RenderMode.Server) {
const cacheKey = await sha256(html);
const cacheKey = await sha256(renderedHtml);
htmlWithCriticalCss = criticalCssLRUCache.get(cacheKey);
if (!htmlWithCriticalCss) {
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(html);
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(renderedHtml);
criticalCssLRUCache.put(cacheKey, htmlWithCriticalCss);
}
} else {
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(html);
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(renderedHtml);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(`An error occurred while inlining critical CSS for: ${url}.`, error);
}

controller.enqueue(textDecoder.encode(htmlWithCriticalCss ?? html));

controller.enqueue(textDecoder.encode(htmlWithCriticalCss ?? renderedHtml));
controller.close();
},
});
Expand Down
87 changes: 65 additions & 22 deletions packages/angular/ssr/src/utils/ng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { ɵConsole } from '@angular/core';
import type { ApplicationRef, StaticProvider, Type } from '@angular/core';
import {
ApplicationRef,
type PlatformRef,
type StaticProvider,
type Type,
ɵConsole,
} from '@angular/core';
import {
INITIAL_CONFIG,
ɵSERVER_CONTEXT as SERVER_CONTEXT,
renderApplication,
renderModule,
platformServer,
ɵrenderInternal as renderInternal,
} from '@angular/platform-server';
import { Console } from '../console';
import { stripIndexHtmlFromURL } from './url';
Expand Down Expand Up @@ -41,16 +47,26 @@ export type AngularBootstrap = Type<unknown> | (() => Promise<ApplicationRef>);
* rendering process.
* @param serverContext - A string representing the server context, used to provide additional
* context or metadata during server-side rendering.
* @returns A promise that resolves to a string containing the rendered HTML.
* @returns A promise resolving to an object containing a `content` method, which returns a
* promise that resolves to the rendered HTML string.
*/
export function renderAngular(
export async function renderAngular(
html: string,
bootstrap: AngularBootstrap,
url: URL,
platformProviders: StaticProvider[],
serverContext: string,
): Promise<string> {
const providers = [
): Promise<{ content: () => Promise<string> }> {
// A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
const urlToRender = stripIndexHtmlFromURL(url).toString();
const platformRef = platformServer([
{
provide: INITIAL_CONFIG,
useValue: {
url: urlToRender,
document: html,
},
},
{
provide: SERVER_CONTEXT,
useValue: serverContext,
Expand All @@ -64,22 +80,34 @@ export function renderAngular(
useFactory: () => new Console(),
},
...platformProviders,
];
]);

// A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
const urlToRender = stripIndexHtmlFromURL(url).toString();
try {
let applicationRef: ApplicationRef;
if (isNgModule(bootstrap)) {
const moduleRef = await platformRef.bootstrapModule(bootstrap);
applicationRef = moduleRef.injector.get(ApplicationRef);
} else {
applicationRef = await bootstrap();
}

return isNgModule(bootstrap)
? renderModule(bootstrap, {
url: urlToRender,
document: html,
extraProviders: providers,
})
: renderApplication(bootstrap, {
url: urlToRender,
document: html,
platformProviders: providers,
});
// Block until application is stable.
await applicationRef.whenStable();

return {
content: async () => {
try {
return renderInternal(platformRef, applicationRef);
} finally {
await asyncDestroyPlatform(platformRef);
}
},
};
} catch (error) {
await asyncDestroyPlatform(platformRef);

throw error;
}
}

/**
Expand All @@ -93,3 +121,18 @@ export function renderAngular(
export function isNgModule(value: AngularBootstrap): value is Type<unknown> {
return 'ɵmod' in value;
}

/**
* Gracefully destroys the application in a macrotask, allowing pending promises to resolve
* and surfacing any potential errors to the user.
*
* @param platformRef - The platform reference to be destroyed.
*/
function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
platformRef.destroy();
resolve();
}, 0);
});
}