9
9
import { readFile } from 'node:fs/promises' ;
10
10
import { extname , join , posix } from 'node:path' ;
11
11
import { pathToFileURL } from 'node:url' ;
12
+ import { NormalizedApplicationBuildOptions } from '../../builders/application/options' ;
13
+ import { OutputMode } from '../../builders/application/schema' ;
12
14
import { BuildOutputFile , BuildOutputFileType } from '../../tools/esbuild/bundler-context' ;
13
15
import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result' ;
16
+ import { assertIsError } from '../error' ;
14
17
import { urlJoin } from '../url' ;
15
18
import { WorkerPool } from '../worker-pool' ;
16
- import type { RenderWorkerData } from './render-worker' ;
17
- import type {
19
+ import {
20
+ RouteRenderMode ,
18
21
RoutersExtractorWorkerResult ,
19
22
RoutesExtractorWorkerData ,
20
23
SerializableRouteTreeNode ,
21
- } from './routes-extractor-worker' ;
24
+ WritableSerializableRouteTreeNode ,
25
+ } from './models' ;
26
+ import type { RenderWorkerData } from './render-worker' ;
22
27
23
- interface PrerenderOptions {
24
- routesFile ?: string ;
25
- discoverRoutes ?: boolean ;
26
- }
27
-
28
- interface AppShellOptions {
29
- route ?: string ;
30
- }
28
+ type PrerenderOptions = NormalizedApplicationBuildOptions [ 'prerenderOptions' ] ;
29
+ type AppShellOptions = NormalizedApplicationBuildOptions [ 'appShellOptions' ] ;
31
30
32
31
/**
33
32
* Represents the output of a prerendering process.
@@ -48,18 +47,17 @@ type PrerenderOutput = Record<string, { content: string; appShellRoute: boolean
48
47
export async function prerenderPages (
49
48
workspaceRoot : string ,
50
49
baseHref : string ,
51
- appShellOptions : AppShellOptions = { } ,
52
- prerenderOptions : PrerenderOptions = { } ,
50
+ appShellOptions : AppShellOptions | undefined ,
51
+ prerenderOptions : PrerenderOptions | undefined ,
53
52
outputFiles : Readonly < BuildOutputFile [ ] > ,
54
53
assets : Readonly < BuildOutputAsset [ ] > ,
54
+ outputMode : OutputMode | undefined ,
55
55
sourcemap = false ,
56
56
maxThreads = 1 ,
57
- verbose = false ,
58
57
) : Promise < {
59
58
output : PrerenderOutput ;
60
59
warnings : string [ ] ;
61
60
errors : string [ ] ;
62
- prerenderedRoutes : Set < string > ;
63
61
serializableRouteTreeNode : SerializableRouteTreeNode ;
64
62
} > {
65
63
const outputFilesForWorker : Record < string , string > = { } ;
@@ -68,7 +66,7 @@ export async function prerenderPages(
68
66
const errors : string [ ] = [ ] ;
69
67
70
68
for ( const { text, path, type } of outputFiles ) {
71
- if ( type !== BuildOutputFileType . Server ) {
69
+ if ( type !== BuildOutputFileType . ServerApplication && type !== BuildOutputFileType . ServerRoot ) {
72
70
continue ;
73
71
}
74
72
@@ -99,45 +97,64 @@ export async function prerenderPages(
99
97
}
100
98
101
99
// Get routes to prerender
102
- const {
103
- routes : allRoutes ,
104
- warnings : routesWarnings ,
105
- errors : routesErrors ,
106
- serializableRouteTreeNode,
107
- } = await getAllRoutes (
108
- workspaceRoot ,
109
- baseHref ,
110
- outputFilesForWorker ,
111
- assetsReversed ,
112
- appShellOptions ,
113
- prerenderOptions ,
114
- sourcemap ,
115
- verbose ,
116
- ) ;
100
+ const { errors : extractionErrors , serializedRouteTree : serializableRouteTreeNode } =
101
+ await getAllRoutes (
102
+ workspaceRoot ,
103
+ baseHref ,
104
+ outputFilesForWorker ,
105
+ assetsReversed ,
106
+ appShellOptions ,
107
+ prerenderOptions ,
108
+ sourcemap ,
109
+ outputMode ,
110
+ ) . catch ( ( err ) => {
111
+ return {
112
+ errors : [
113
+ `An error occurred while extracting routes.\n\n${ err . stack ?? err . message ?? err } ` ,
114
+ ] ,
115
+ serializedRouteTree : [ ] ,
116
+ } ;
117
+ } ) ;
118
+
119
+ errors . push ( ...extractionErrors ) ;
120
+
121
+ const serializableRouteTreeNodeForPrerender : WritableSerializableRouteTreeNode = [ ] ;
122
+ for ( const metadata of serializableRouteTreeNode ) {
123
+ if ( outputMode !== OutputMode . Static && metadata . redirectTo ) {
124
+ continue ;
125
+ }
117
126
118
- if ( routesErrors ?. length ) {
119
- errors . push ( ...routesErrors ) ;
120
- }
127
+ switch ( metadata . renderMode ) {
128
+ case undefined : /* Legacy building mode */
129
+ case RouteRenderMode . Prerender :
130
+ case RouteRenderMode . AppShell :
131
+ serializableRouteTreeNodeForPrerender . push ( metadata ) ;
121
132
122
- if ( routesWarnings ?. length ) {
123
- warnings . push ( ...routesWarnings ) ;
133
+ break ;
134
+ case RouteRenderMode . Server :
135
+ if ( outputMode === OutputMode . Static ) {
136
+ errors . push (
137
+ `Route '${ metadata . route } ' is configured with server render mode, but the build 'outputMode' is set to 'static'.` ,
138
+ ) ;
139
+ }
140
+ break ;
141
+ }
124
142
}
125
143
126
- if ( allRoutes . size < 1 || errors . length > 0 ) {
144
+ if ( ! serializableRouteTreeNodeForPrerender . length || errors . length > 0 ) {
127
145
return {
128
146
errors,
129
147
warnings,
130
148
output : { } ,
131
149
serializableRouteTreeNode,
132
- prerenderedRoutes : allRoutes ,
133
150
} ;
134
151
}
135
152
136
153
// Render routes
137
154
const { errors : renderingErrors , output } = await renderPages (
138
155
baseHref ,
139
156
sourcemap ,
140
- allRoutes ,
157
+ serializableRouteTreeNodeForPrerender ,
141
158
maxThreads ,
142
159
workspaceRoot ,
143
160
outputFilesForWorker ,
@@ -152,25 +169,18 @@ export async function prerenderPages(
152
169
warnings,
153
170
output,
154
171
serializableRouteTreeNode,
155
- prerenderedRoutes : allRoutes ,
156
172
} ;
157
173
}
158
174
159
- class RoutesSet extends Set < string > {
160
- override add ( value : string ) : this {
161
- return super . add ( addLeadingSlash ( value ) ) ;
162
- }
163
- }
164
-
165
175
async function renderPages (
166
176
baseHref : string ,
167
177
sourcemap : boolean ,
168
- allRoutes : Set < string > ,
178
+ serializableRouteTreeNode : SerializableRouteTreeNode ,
169
179
maxThreads : number ,
170
180
workspaceRoot : string ,
171
181
outputFilesForWorker : Record < string , string > ,
172
182
assetFilesForWorker : Record < string , string > ,
173
- appShellOptions : AppShellOptions ,
183
+ appShellOptions : AppShellOptions | undefined ,
174
184
) : Promise < {
175
185
output : PrerenderOutput ;
176
186
errors : string [ ] ;
@@ -190,7 +200,7 @@ async function renderPages(
190
200
191
201
const renderWorker = new WorkerPool ( {
192
202
filename : require . resolve ( './render-worker' ) ,
193
- maxThreads : Math . min ( allRoutes . size , maxThreads ) ,
203
+ maxThreads : Math . min ( serializableRouteTreeNode . length , maxThreads ) ,
194
204
workerData : {
195
205
workspaceRoot,
196
206
outputFiles : outputFilesForWorker ,
@@ -201,22 +211,31 @@ async function renderPages(
201
211
202
212
try {
203
213
const renderingPromises : Promise < void > [ ] = [ ] ;
204
- const appShellRoute = appShellOptions . route && addLeadingSlash ( appShellOptions . route ) ;
214
+ const appShellRoute = appShellOptions && addLeadingSlash ( appShellOptions . route ) ;
205
215
const baseHrefWithLeadingSlash = addLeadingSlash ( baseHref ) ;
206
216
207
- for ( const route of allRoutes ) {
217
+ for ( const { route, redirectTo , renderMode } of serializableRouteTreeNode ) {
208
218
// Remove base href from file output path.
209
219
const routeWithoutBaseHref = addLeadingSlash (
210
220
route . slice ( baseHrefWithLeadingSlash . length - 1 ) ,
211
221
) ;
222
+ const outPath = posix . join ( removeLeadingSlash ( routeWithoutBaseHref ) , 'index.html' ) ;
223
+
224
+ if ( typeof redirectTo === 'string' ) {
225
+ output [ outPath ] = { content : generateRedirectStaticPage ( redirectTo ) , appShellRoute : false } ;
212
226
213
- const render : Promise < string | null > = renderWorker . run ( { url : route } ) ;
227
+ continue ;
228
+ }
229
+
230
+ const isAppShellRoute =
231
+ renderMode === RouteRenderMode . AppShell ||
232
+ // Legacy handling
233
+ ( renderMode === undefined && appShellRoute === routeWithoutBaseHref ) ;
234
+
235
+ const render : Promise < string | null > = renderWorker . run ( { url : route , isAppShellRoute } ) ;
214
236
const renderResult : Promise < void > = render
215
237
. then ( ( content ) => {
216
238
if ( content !== null ) {
217
- const outPath = posix . join ( removeLeadingSlash ( routeWithoutBaseHref ) , 'index.html' ) ;
218
- const isAppShellRoute = appShellRoute === routeWithoutBaseHref ;
219
-
220
239
output [ outPath ] = { content, appShellRoute : isAppShellRoute } ;
221
240
}
222
241
} )
@@ -246,33 +265,31 @@ async function getAllRoutes(
246
265
baseHref : string ,
247
266
outputFilesForWorker : Record < string , string > ,
248
267
assetFilesForWorker : Record < string , string > ,
249
- appShellOptions : AppShellOptions ,
250
- prerenderOptions : PrerenderOptions ,
268
+ appShellOptions : AppShellOptions | undefined ,
269
+ prerenderOptions : PrerenderOptions | undefined ,
251
270
sourcemap : boolean ,
252
- verbose : boolean ,
253
- ) : Promise < {
254
- routes : Set < string > ;
255
- warnings ?: string [ ] ;
256
- errors ?: string [ ] ;
257
- serializableRouteTreeNode : SerializableRouteTreeNode ;
258
- } > {
259
- const { routesFile, discoverRoutes } = prerenderOptions ;
260
- const routes = new RoutesSet ( ) ;
261
- const { route : appShellRoute } = appShellOptions ;
262
-
263
- if ( appShellRoute !== undefined ) {
264
- routes . add ( urlJoin ( baseHref , appShellRoute ) ) ;
271
+ outputMode : OutputMode | undefined ,
272
+ ) : Promise < { serializedRouteTree : SerializableRouteTreeNode ; errors : string [ ] } > {
273
+ const { routesFile, discoverRoutes } = prerenderOptions ?? { } ;
274
+ const routes : WritableSerializableRouteTreeNode = [ ] ;
275
+
276
+ if ( appShellOptions ) {
277
+ routes . push ( {
278
+ route : urlJoin ( baseHref , appShellOptions . route ) ,
279
+ } ) ;
265
280
}
266
281
267
282
if ( routesFile ) {
268
283
const routesFromFile = ( await readFile ( routesFile , 'utf8' ) ) . split ( / \r ? \n / ) ;
269
284
for ( const route of routesFromFile ) {
270
- routes . add ( urlJoin ( baseHref , route . trim ( ) ) ) ;
285
+ routes . push ( {
286
+ route : urlJoin ( baseHref , route . trim ( ) ) ,
287
+ } ) ;
271
288
}
272
289
}
273
290
274
291
if ( ! discoverRoutes ) {
275
- return { routes , serializableRouteTreeNode : [ ] } ;
292
+ return { errors : [ ] , serializedRouteTree : routes } ;
276
293
}
277
294
278
295
const workerExecArgv = [
@@ -296,47 +313,22 @@ async function getAllRoutes(
296
313
execArgv : workerExecArgv ,
297
314
} ) ;
298
315
299
- const errors : string [ ] = [ ] ;
300
- const { serializedRouteTree : serializableRouteTreeNode } : RoutersExtractorWorkerResult =
301
- await renderWorker
302
- . run ( { } )
303
- . catch ( ( err ) => {
304
- errors . push ( `An error occurred while extracting routes.\n\n${ err . stack } ` ) ;
305
- } )
306
- . finally ( ( ) => {
307
- void renderWorker . destroy ( ) ;
308
- } ) ;
309
-
310
- const skippedRedirects : string [ ] = [ ] ;
311
- const skippedOthers : string [ ] = [ ] ;
312
- for ( const { route, redirectTo } of serializableRouteTreeNode ) {
313
- if ( redirectTo ) {
314
- skippedRedirects . push ( route ) ;
315
- } else if ( route . includes ( '*' ) ) {
316
- skippedOthers . push ( route ) ;
317
- } else {
318
- routes . add ( route ) ;
319
- }
320
- }
316
+ try {
317
+ const { serializedRouteTree, errors } : RoutersExtractorWorkerResult = await renderWorker . run ( {
318
+ outputMode,
319
+ } ) ;
321
320
322
- let warnings : string [ ] | undefined ;
323
- if ( verbose ) {
324
- if ( skippedOthers . length ) {
325
- ( warnings ??= [ ] ) . push (
326
- 'The following routes were skipped from prerendering because they contain routes with dynamic parameters:\n' +
327
- skippedOthers . join ( '\n' ) ,
328
- ) ;
329
- }
321
+ return { errors, serializedRouteTree : [ ...routes , ...serializedRouteTree ] } ;
322
+ } catch ( err ) {
323
+ assertIsError ( err ) ;
330
324
331
- if ( skippedRedirects . length ) {
332
- ( warnings ??= [ ] ) . push (
333
- 'The following routes were skipped from prerendering because they contain redirects:\n' ,
334
- skippedRedirects . join ( '\n' ) ,
335
- ) ;
336
- }
325
+ return {
326
+ errors : [ `An error occurred while extracting routes.\n\n ${ err . stack } ` ] ,
327
+ serializedRouteTree : [ ] ,
328
+ } ;
329
+ } finally {
330
+ void renderWorker . destroy ( ) ;
337
331
}
338
-
339
- return { routes, serializableRouteTreeNode, warnings } ;
340
332
}
341
333
342
334
function addLeadingSlash ( value : string ) : string {
@@ -346,3 +338,28 @@ function addLeadingSlash(value: string): string {
346
338
function removeLeadingSlash ( value : string ) : string {
347
339
return value . charAt ( 0 ) === '/' ? value . slice ( 1 ) : value ;
348
340
}
341
+
342
+ /**
343
+ * Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL.
344
+ *
345
+ * This function creates a simple HTML page that performs a redirect using a meta tag.
346
+ * It includes a fallback link in case the meta-refresh doesn't work.
347
+ *
348
+ * @param url - The URL to which the page should redirect.
349
+ * @returns The HTML content of the static redirect page.
350
+ */
351
+ function generateRedirectStaticPage ( url : string ) : string {
352
+ return `
353
+ <!DOCTYPE html>
354
+ <html>
355
+ <head>
356
+ <meta charset="utf-8">
357
+ <title>Redirecting</title>
358
+ <meta http-equiv="refresh" content="0; url=${ url } ">
359
+ </head>
360
+ <body>
361
+ <pre>Redirecting to <a href="${ url } ">${ url } </a></pre>
362
+ </body>
363
+ </html>
364
+ ` . trim ( ) ;
365
+ }
0 commit comments