@@ -16,12 +16,28 @@ import {
16
16
import * as fs from 'node:fs' ;
17
17
import { dirname , resolve } from 'node:path' ;
18
18
import { 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' ;
20
25
21
26
const SSG_MARKER_REGEXP = / n g - s e r v e r - c o n t e x t = [ " ' ] \w * \| ? s s g \| ? \w * [ " ' ] / ;
22
27
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
+
23
37
export interface CommonEngineRenderOptions {
38
+ /** A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an NgModule. */
24
39
bootstrap ?: Type < { } > | ( ( ) => Promise < ApplicationRef > ) ;
40
+ /** A set of platform level providers for the current request. */
25
41
providers ?: StaticProvider [ ] ;
26
42
url ?: string ;
27
43
document ?: string ;
@@ -39,19 +55,15 @@ export interface CommonEngineRenderOptions {
39
55
}
40
56
41
57
/**
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.
45
59
*/
60
+
46
61
export class CommonEngine {
47
62
private readonly templateCache = new Map < string , string > ( ) ;
48
63
private readonly inlineCriticalCssProcessor : InlineCriticalCssProcessor ;
49
64
private readonly pageIsSSG = new Map < string , boolean > ( ) ;
50
65
51
- constructor (
52
- private bootstrap ?: Type < { } > | ( ( ) => Promise < ApplicationRef > ) ,
53
- private providers : StaticProvider [ ] = [ ] ,
54
- ) {
66
+ constructor ( private options ?: CommonEngineOptions ) {
55
67
this . inlineCriticalCssProcessor = new InlineCriticalCssProcessor ( {
56
68
minify : false ,
57
69
} ) ;
@@ -62,40 +74,87 @@ export class CommonEngine {
62
74
* render options
63
75
*/
64
76
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 ;
86
140
}
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 ) ;
90
143
}
144
+ } else if ( pageIsSSG ) {
145
+ // Serve pre-rendered page.
146
+ return fs . promises . readFile ( pagePath , 'utf-8' ) ;
91
147
}
92
148
}
93
149
94
- // if opts.document dosen't exist then opts.documentFilePath must
150
+ return undefined ;
151
+ }
152
+
153
+ private async renderApplication ( opts : CommonEngineRenderOptions ) : Promise < string > {
95
154
const extraProviders : StaticProvider [ ] = [
96
155
{ provide : ɵSERVER_CONTEXT , useValue : 'ssr' } ,
97
156
...( opts . providers ?? [ ] ) ,
98
- ...this . providers ,
157
+ ...( this . options ?. providers ?? [ ] ) ,
99
158
] ;
100
159
101
160
let document = opts . document ;
@@ -113,29 +172,14 @@ export class CommonEngine {
113
172
} ) ;
114
173
}
115
174
116
- const moduleOrFactory = this . bootstrap || opts . bootstrap ;
175
+ const moduleOrFactory = this . options ?. bootstrap ?? opts . bootstrap ;
117
176
if ( ! moduleOrFactory ) {
118
177
throw new Error ( 'A module or bootstrap option must be provided.' ) ;
119
178
}
120
179
121
- const html = await ( isBootstrapFn ( moduleOrFactory )
180
+ return isBootstrapFn ( moduleOrFactory )
122
181
? 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 } ) ;
139
183
}
140
184
141
185
/** Retrieve the document from the cache or the filesystem */
0 commit comments