@@ -16,12 +16,28 @@ import {
1616import * as fs from 'node:fs' ;
1717import { dirname , resolve } from 'node:path' ;
1818import { URL } from 'node:url' ;
19- import { InlineCriticalCssProcessor } from './inline-css-processor' ;
19+ import { InlineCriticalCssProcessor , InlineCriticalCssResult } from './inline-css-processor' ;
20+ import {
21+ noopRunMethodAndMeasurePerf ,
22+ printPerformanceLogs ,
23+ runMethodAndMeasurePerf ,
24+ } from './peformance-profiler' ;
2025
2126const SSG_MARKER_REGEXP = / n g - s e r v e r - c o n t e x t = [ " ' ] \w * \| ? s s g \| ? \w * [ " ' ] / ;
2227
28+ export interface CommonEngineOptions {
29+ /** A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an NgModule. */
30+ bootstrap ?: Type < { } > | ( ( ) => Promise < ApplicationRef > ) ;
31+ /** A set of platform level providers for all requests. */
32+ providers ?: StaticProvider [ ] ;
33+ /** Enable request performance profiling data collection and printing the results in the server console. */
34+ enablePeformanceProfiler ?: boolean ;
35+ }
36+
2337export interface CommonEngineRenderOptions {
38+ /** A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an NgModule. */
2439 bootstrap ?: Type < { } > | ( ( ) => Promise < ApplicationRef > ) ;
40+ /** A set of platform level providers for the current request. */
2541 providers ?: StaticProvider [ ] ;
2642 url ?: string ;
2743 document ?: string ;
@@ -39,19 +55,15 @@ export interface CommonEngineRenderOptions {
3955}
4056
4157/**
42- * A common rendering engine utility. This abstracts the logic
43- * for handling the platformServer compiler, the module cache, and
44- * the document loader
58+ * A common engine to use to server render an application.
4559 */
60+
4661export class CommonEngine {
4762 private readonly templateCache = new Map < string , string > ( ) ;
4863 private readonly inlineCriticalCssProcessor : InlineCriticalCssProcessor ;
4964 private readonly pageIsSSG = new Map < string , boolean > ( ) ;
5065
51- constructor (
52- private bootstrap ?: Type < { } > | ( ( ) => Promise < ApplicationRef > ) ,
53- private providers : StaticProvider [ ] = [ ] ,
54- ) {
66+ constructor ( private options ?: CommonEngineOptions ) {
5567 this . inlineCriticalCssProcessor = new InlineCriticalCssProcessor ( {
5668 minify : false ,
5769 } ) ;
@@ -62,40 +74,87 @@ export class CommonEngine {
6274 * render options
6375 */
6476 async render ( opts : CommonEngineRenderOptions ) : Promise < string > {
65- const { inlineCriticalCss = true , url } = opts ;
66-
67- if ( opts . publicPath && opts . documentFilePath && url !== undefined ) {
68- const pathname = canParseUrl ( url ) ? new URL ( url ) . pathname : url ;
69- // Remove leading forward slash.
70- const pagePath = resolve ( opts . publicPath , pathname . substring ( 1 ) , 'index.html' ) ;
71-
72- if ( pagePath !== resolve ( opts . documentFilePath ) ) {
73- // View path doesn't match with prerender path.
74- const pageIsSSG = this . pageIsSSG . get ( pagePath ) ;
75- if ( pageIsSSG === undefined ) {
76- if ( await exists ( pagePath ) ) {
77- const content = await fs . promises . readFile ( pagePath , 'utf-8' ) ;
78- const isSSG = SSG_MARKER_REGEXP . test ( content ) ;
79- this . pageIsSSG . set ( pagePath , isSSG ) ;
80-
81- if ( isSSG ) {
82- return content ;
83- }
84- } else {
85- this . pageIsSSG . set ( pagePath , false ) ;
77+ const enablePeformanceProfiler = this . options ?. enablePeformanceProfiler ;
78+
79+ const runMethod = enablePeformanceProfiler
80+ ? runMethodAndMeasurePerf
81+ : noopRunMethodAndMeasurePerf ;
82+
83+ let html = await runMethod ( 'Retrieve SSG Page' , ( ) => this . retrieveSSGPage ( opts ) ) ;
84+
85+ if ( html === undefined ) {
86+ html = await runMethod ( 'Render Page' , ( ) => this . renderApplication ( opts ) ) ;
87+
88+ if ( opts . inlineCriticalCss !== false ) {
89+ const { content, errors, warnings } = await runMethod ( 'Inline Critical CSS' , ( ) =>
90+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
91+ this . inlineCriticalCss ( html ! , opts ) ,
92+ ) ;
93+
94+ html = content ;
95+
96+ // eslint-disable-next-line no-console
97+ warnings ?. forEach ( ( m ) => console . warn ( m ) ) ;
98+ // eslint-disable-next-line no-console
99+ errors ?. forEach ( ( m ) => console . error ( m ) ) ;
100+ }
101+ }
102+
103+ if ( enablePeformanceProfiler ) {
104+ printPerformanceLogs ( ) ;
105+ }
106+
107+ return html ;
108+ }
109+
110+ private inlineCriticalCss (
111+ html : string ,
112+ opts : CommonEngineRenderOptions ,
113+ ) : Promise < InlineCriticalCssResult > {
114+ return this . inlineCriticalCssProcessor . process ( html , {
115+ outputPath : opts . publicPath ?? ( opts . documentFilePath ? dirname ( opts . documentFilePath ) : '' ) ,
116+ } ) ;
117+ }
118+
119+ private async retrieveSSGPage ( opts : CommonEngineRenderOptions ) : Promise < string | undefined > {
120+ const { publicPath, documentFilePath, url } = opts ;
121+ if ( ! publicPath || ! documentFilePath || url === undefined ) {
122+ return undefined ;
123+ }
124+
125+ const pathname = canParseUrl ( url ) ? new URL ( url ) . pathname : url ;
126+ // Remove leading forward slash.
127+ const pagePath = resolve ( publicPath , pathname . substring ( 1 ) , 'index.html' ) ;
128+
129+ if ( pagePath !== resolve ( documentFilePath ) ) {
130+ // View path doesn't match with prerender path.
131+ const pageIsSSG = this . pageIsSSG . get ( pagePath ) ;
132+ if ( pageIsSSG === undefined ) {
133+ if ( await exists ( pagePath ) ) {
134+ const content = await fs . promises . readFile ( pagePath , 'utf-8' ) ;
135+ const isSSG = SSG_MARKER_REGEXP . test ( content ) ;
136+ this . pageIsSSG . set ( pagePath , isSSG ) ;
137+
138+ if ( isSSG ) {
139+ return content ;
86140 }
87- } else if ( pageIsSSG ) {
88- // Serve pre-rendered page.
89- return fs . promises . readFile ( pagePath , 'utf-8' ) ;
141+ } else {
142+ this . pageIsSSG . set ( pagePath , false ) ;
90143 }
144+ } else if ( pageIsSSG ) {
145+ // Serve pre-rendered page.
146+ return fs . promises . readFile ( pagePath , 'utf-8' ) ;
91147 }
92148 }
93149
94- // if opts.document dosen't exist then opts.documentFilePath must
150+ return undefined ;
151+ }
152+
153+ private async renderApplication ( opts : CommonEngineRenderOptions ) : Promise < string > {
95154 const extraProviders : StaticProvider [ ] = [
96155 { provide : ɵSERVER_CONTEXT , useValue : 'ssr' } ,
97156 ...( opts . providers ?? [ ] ) ,
98- ...this . providers ,
157+ ...( this . options ?. providers ?? [ ] ) ,
99158 ] ;
100159
101160 let document = opts . document ;
@@ -113,29 +172,14 @@ export class CommonEngine {
113172 } ) ;
114173 }
115174
116- const moduleOrFactory = this . bootstrap || opts . bootstrap ;
175+ const moduleOrFactory = this . options ?. bootstrap ?? opts . bootstrap ;
117176 if ( ! moduleOrFactory ) {
118177 throw new Error ( 'A module or bootstrap option must be provided.' ) ;
119178 }
120179
121- const html = await ( isBootstrapFn ( moduleOrFactory )
180+ return isBootstrapFn ( moduleOrFactory )
122181 ? renderApplication ( moduleOrFactory , { platformProviders : extraProviders } )
123- : renderModule ( moduleOrFactory , { extraProviders } ) ) ;
124-
125- if ( ! inlineCriticalCss ) {
126- return html ;
127- }
128-
129- const { content, errors, warnings } = await this . inlineCriticalCssProcessor . process ( html , {
130- outputPath : opts . publicPath ?? ( opts . documentFilePath ? dirname ( opts . documentFilePath ) : '' ) ,
131- } ) ;
132-
133- // eslint-disable-next-line no-console
134- warnings ?. forEach ( ( m ) => console . warn ( m ) ) ;
135- // eslint-disable-next-line no-console
136- errors ?. forEach ( ( m ) => console . error ( m ) ) ;
137-
138- return content ;
182+ : renderModule ( moduleOrFactory , { extraProviders } ) ;
139183 }
140184
141185 /** Retrieve the document from the cache or the filesystem */
0 commit comments