| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 4fa81d4 commit 5b850fd
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -157,7 +157,13 @@ export async function executePostBundleSteps( | |||
| 157 | 157 | 'The "index" option is required when using the "ssg" or "appShell" options.', | |
| 158 | 158 | ); | |
| 159 | 159 | ||
| 160 | - const { output, warnings, errors, serializableRouteTreeNode } = await prerenderPages( | ||
| 160 | + const { | ||
| 161 | + output, | ||
| 162 | + warnings, | ||
| 163 | + errors, | ||
| 164 | + serializableRouteTreeNode, | ||
| 165 | + prerenderedRoutes: generatedPrerenderedRoutes, | ||
| 166 | + } = await prerenderPages( | ||
| 161 | 167 | workspaceRoot, | |
| 162 | 168 | baseHref, | |
| 163 | 169 | appShellOptions, | |
@@ -171,6 +177,7 @@ export async function executePostBundleSteps( | |||
| 171 | 177 | ||
| 172 | 178 | allErrors.push(...errors); | |
| 173 | 179 | allWarnings.push(...warnings); | |
| 180 | + Object.assign(prerenderedRoutes, generatedPrerenderedRoutes); | ||
| 174 | 181 | ||
| 175 | 182 | const indexHasBeenPrerendered = output[indexHtmlOptions.output]; | |
| 176 | 183 | for (const [path, { content, appShellRoute }] of Object.entries(output)) { | |
@@ -195,10 +202,6 @@ export async function executePostBundleSteps( | |||
| 195 | 202 | const serializableRouteTreeNodeForManifest: WritableSerializableRouteTreeNode = []; | |
| 196 | 203 | for (const metadata of serializableRouteTreeNode) { | |
| 197 | 204 | serializableRouteTreeNodeForManifest.push(metadata); | |
| 198 | - | ||
| 199 | - if (metadata.renderMode === RouteRenderMode.Prerender && !metadata.route.includes('*')) { | ||
| 200 | - prerenderedRoutes[metadata.route] = { headers: metadata.headers }; | ||
| 201 | - } | ||
| 202 | 205 | } | |
| 203 | 206 | ||
| 204 | 207 | if (outputMode === OutputMode.Server) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,7 +10,10 @@ import { readFile } from 'node:fs/promises'; | |||
| 10 | 10 | import { extname, posix } from 'node:path'; | |
| 11 | 11 | import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; | |
| 12 | 12 | import { OutputMode } from '../../builders/application/schema'; | |
| 13 | - import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result'; | ||
| 13 | + import { | ||
| 14 | + BuildOutputAsset, | ||
| 15 | + PrerenderedRoutesRecord, | ||
| 16 | + } from '../../tools/esbuild/bundler-execution-result'; | ||
| 14 | 17 | import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files'; | |
| 15 | 18 | import { assertIsError } from '../error'; | |
| 16 | 19 | import { toPosixPath } from '../path'; | |
@@ -65,6 +68,7 @@ export async function prerenderPages( | |||
| 65 | 68 | output: PrerenderOutput; | |
| 66 | 69 | warnings: string[]; | |
| 67 | 70 | errors: string[]; | |
| 71 | + prerenderedRoutes: PrerenderedRoutesRecord; | ||
| 68 | 72 | serializableRouteTreeNode: SerializableRouteTreeNode; | |
| 69 | 73 | }> { | |
| 70 | 74 | const rawOutputFiles: Record<string, string> = {}; | |
@@ -167,6 +171,7 @@ export async function prerenderPages( | |||
| 167 | 171 | errors, | |
| 168 | 172 | warnings, | |
| 169 | 173 | output: {}, | |
| 174 | + prerenderedRoutes: {}, | ||
| 170 | 175 | serializableRouteTreeNode, | |
| 171 | 176 | }; | |
| 172 | 177 | } | |
@@ -200,10 +205,22 @@ export async function prerenderPages( | |||
| 200 | 205 | ||
| 201 | 206 | errors.push(...renderingErrors); | |
| 202 | 207 | ||
| 208 | + const prerenderedRoutes: PrerenderedRoutesRecord = {}; | ||
| 209 | + const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname; | ||
| 210 | + | ||
| 211 | + for (const metadata of serializableRouteTreeNodeForPrerender) { | ||
| 212 | + const outPath = getRouteOutPath(metadata.route, baseHrefPathnameWithLeadingSlash); | ||
| 213 | + | ||
| 214 | + if (output[outPath]) { | ||
| 215 | + prerenderedRoutes[metadata.route] = { headers: metadata.headers }; | ||
| 216 | + } | ||
| 217 | + } | ||
| 218 | + | ||
| 203 | 219 | return { | |
| 204 | 220 | errors, | |
| 205 | 221 | warnings, | |
| 206 | 222 | output, | |
| 223 | + prerenderedRoutes, | ||
| 207 | 224 | serializableRouteTreeNode, | |
| 208 | 225 | }; | |
| 209 | 226 | } | |
@@ -227,22 +244,15 @@ async function renderPages( | |||
| 227 | 244 | ||
| 228 | 245 | const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname; | |
| 229 | 246 | const appShellRouteWithoutBaseHref = appShellRoute | |
| 230 | - ? addTrailingSlash(appShellRoute).startsWith(baseHrefPathnameWithLeadingSlash) | ||
| 231 | - ? addLeadingSlash(appShellRoute.slice(baseHrefPathnameWithLeadingSlash.length)) | ||
| 232 | - : addLeadingSlash(appShellRoute) | ||
| 247 | + ? addLeadingSlash(getRouteWithoutBaseHref(appShellRoute, baseHrefPathnameWithLeadingSlash)) | ||
| 233 | 248 | : undefined; | |
| 234 | 249 | ||
| 235 | 250 | const routesToRender: { route: string; outPath: string; isAppShell: boolean }[] = []; | |
| 236 | 251 | ||
| 237 | 252 | for (const { route, redirectTo } of serializableRouteTreeNode) { | |
| 238 | 253 | // Remove the base href from the file output path. | |
| 239 | - const routeWithoutBaseHref = addTrailingSlash(route).startsWith( | ||
| 240 | - baseHrefPathnameWithLeadingSlash, | ||
| 241 | - ) | ||
| 242 | - ? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length)) | ||
| 243 | - : route; | ||
| 244 | - | ||
| 245 | - const outPath = stripLeadingSlash(posix.join(routeWithoutBaseHref, 'index.html')); | ||
| 254 | + const routeWithoutBaseHref = getRouteWithoutBaseHref(route, baseHrefPathnameWithLeadingSlash); | ||
| 255 | + const outPath = getRouteOutPath(route, baseHrefPathnameWithLeadingSlash); | ||
| 246 | 256 | ||
| 247 | 257 | if (typeof redirectTo === 'string') { | |
| 248 | 258 | output[outPath] = { content: generateRedirectStaticPage(redirectTo), appShellRoute: false }; | |
@@ -305,20 +315,20 @@ async function renderPages( | |||
| 305 | 315 | const renderBatchPromise: Promise<RenderResult> = renderWorker.run(urls); | |
| 306 | 316 | const batchResultPromise = renderBatchPromise | |
| 307 | 317 | .then((results) => { | |
| 308 | - for (const { url, content, error } of results) { | ||
| 309 | - if (error) { | ||
| 310 | - errors.push(`An error occurred while prerendering route '${url}'.\n\n${error}`); | ||
| 318 | + for (const result of results) { | ||
| 319 | + if ('error' in result) { | ||
| 320 | + errors.push( | ||
| 321 | + `An error occurred while prerendering route '${result.url}'.\n\n${result.error}`, | ||
| 322 | + ); | ||
| 311 | 323 | continue; | |
| 312 | 324 | } | |
| 313 | 325 | ||
| 314 | - if (content !== null) { | ||
| 315 | - const routeInfo = routeOutPathMap.get(url); | ||
| 316 | - if (routeInfo) { | ||
| 317 | - output[routeInfo.outPath] = { | ||
| 318 | - content, | ||
| 319 | - appShellRoute: routeInfo.isAppShell, | ||
| 320 | - }; | ||
| 321 | - } | ||
| 326 | + const routeInfo = routeOutPathMap.get(result.url); | ||
| 327 | + if (routeInfo) { | ||
| 328 | + output[routeInfo.outPath] = { | ||
| 329 | + content: result.content, | ||
| 330 | + appShellRoute: routeInfo.isAppShell, | ||
| 331 | + }; | ||
| 322 | 332 | } | |
| 323 | 333 | } | |
| 324 | 334 | }) | |
@@ -439,3 +449,15 @@ async function getAllRoutes( | |||
| 439 | 449 | void renderWorker.destroy(); | |
| 440 | 450 | } | |
| 441 | 451 | } | |
| 452 | + | ||
| 453 | + function getRouteWithoutBaseHref(route: string, baseHrefPathname: string): string { | ||
| 454 | + return addTrailingSlash(route).startsWith(baseHrefPathname) | ||
| 455 | + ? addLeadingSlash(route.slice(baseHrefPathname.length)) | ||
| 456 | + : route; | ||
| 457 | + } | ||
| 458 | + | ||
| 459 | + function getRouteOutPath(route: string, baseHrefPathname: string): string { | ||
| 460 | + const routeWithoutBaseHref = getRouteWithoutBaseHref(route, baseHrefPathname); | ||
| 461 | + | ||
| 462 | + return stripLeadingSlash(posix.join(routeWithoutBaseHref, 'index.html')); | ||
| 463 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,11 +21,15 @@ export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData { | |||
| 21 | 21 | hasSsrEntry: boolean; | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | - export interface RenderResultItem { | ||
| 25 | - url: string; | ||
| 26 | - content: string | null; | ||
| 27 | - error?: string; | ||
| 28 | - } | ||
| 24 | + export type RenderResultItem = | ||
| 25 | + | { | ||
| 26 | + url: string; | ||
| 27 | + content: string; | ||
| 28 | + } | ||
| 29 | + | { | ||
| 30 | + url: string; | ||
| 31 | + error: string; | ||
| 32 | + }; | ||
| 29 | 33 | ||
| 30 | 34 | export type RenderResult = RenderResultItem[]; | |
| 31 | 35 | ||
@@ -74,12 +78,16 @@ async function renderPages(urls: string[]): Promise<RenderResult> { | |||
| 74 | 78 | for (const currentUrl of urls) { | |
| 75 | 79 | try { | |
| 76 | 80 | const content = await renderPage(currentUrl, angularServerApp); | |
| 81 | + | ||
| 82 | + if (content === null) { | ||
| 83 | + throw new Error('The content returned was empty.'); | ||
| 84 | + } | ||
| 85 | + | ||
| 77 | 86 | results.push({ url: currentUrl, content }); | |
| 78 | 87 | } catch (err) { | |
| 79 | 88 | assertIsError(err); | |
| 80 | 89 | results.push({ | |
| 81 | 90 | url: currentUrl, | |
| 82 | - content: null, | ||
| 83 | 91 | error: err.stack ?? err.message ?? err.code ?? `${err}`, | |
| 84 | 92 | }); | |
| 85 | 93 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,72 @@ | |||
| 1 | + import { existsSync } from 'node:fs'; | ||
| 2 | + import assert, { match } from 'node:assert'; | ||
| 3 | + import { getGlobalVariable } from '../../../utils/env'; | ||
| 4 | + import { expectFileNotToExist, readFile, rimraf, writeMultipleFiles } from '../../../utils/fs'; | ||
| 5 | + import { installWorkspacePackages } from '../../../utils/packages'; | ||
| 6 | + import { ng } from '../../../utils/process'; | ||
| 7 | + import { useSha } from '../../../utils/project'; | ||
| 8 | + import { expectToFail } from '../../../utils/utils'; | ||
| 9 | + | ||
| 10 | + export default async function () { | ||
| 11 | + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; | ||
| 12 | + if (useWebpackBuilder) { | ||
| 13 | + return; | ||
| 14 | + } | ||
| 15 | + | ||
| 16 | + // Forcibly remove in case another test doesn't clean itself up. | ||
| 17 | + await rimraf('node_modules/@angular/ssr'); | ||
| 18 | + await ng('add', '@angular/ssr', '--skip-confirmation'); | ||
| 19 | + await useSha(); | ||
| 20 | + await installWorkspacePackages(); | ||
| 21 | + | ||
| 22 | + await writeMultipleFiles({ | ||
| 23 | + 'src/app/app.routes.ts': ` | ||
| 24 | + import { Routes } from '@angular/router'; | ||
| 25 | + import { Component } from '@angular/core'; | ||
| 26 | + | ||
| 27 | + @Component({ | ||
| 28 | + selector: 'app-home', | ||
| 29 | + standalone: true, | ||
| 30 | + template: '<p>home works!</p>', | ||
| 31 | + }) | ||
| 32 | + export class HomeRoute {} | ||
| 33 | + | ||
| 34 | + @Component({ | ||
| 35 | + selector: 'app-second', | ||
| 36 | + standalone: true, | ||
| 37 | + template: '<p>second works!</p>', | ||
| 38 | + }) | ||
| 39 | + export class SecondRoute { | ||
| 40 | + constructor() { | ||
| 41 | + throw new Error('render failure'); | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + export const routes: Routes = [ | ||
| 46 | + { path: '', component: HomeRoute }, | ||
| 47 | + { path: 'second', component: SecondRoute }, | ||
| 48 | + ]; | ||
| 49 | + `, | ||
| 50 | + 'src/app/app.routes.server.ts': ` | ||
| 51 | + import { RenderMode, ServerRoute } from '@angular/ssr'; | ||
| 52 | + | ||
| 53 | + export const serverRoutes: ServerRoute[] = [ | ||
| 54 | + { path: 'second', renderMode: RenderMode.Prerender }, | ||
| 55 | + { path: '**', renderMode: RenderMode.Prerender }, | ||
| 56 | + ]; | ||
| 57 | + `, | ||
| 58 | + }); | ||
| 59 | + | ||
| 60 | + const { message } = await expectToFail(() => ng('build', '--output-mode=server')); | ||
| 61 | + | ||
| 62 | + match(message, /An error occurred while prerendering route '\/second'\./); | ||
| 63 | + | ||
| 64 | + await expectFileNotToExist('dist/test-project/browser/second/index.html'); | ||
| 65 | + | ||
| 66 | + // prerendered-routes.json should only contain successfully prerendered routes if emitted | ||
| 67 | + const statsPath = 'dist/test-project/prerendered-routes.json'; | ||
| 68 | + if (existsSync(statsPath)) { | ||
| 69 | + const stats = JSON.parse(await readFile(statsPath)); | ||
| 70 | + assert.strictEqual(stats.routes['/second'], undefined); | ||
| 71 | + } | ||
| 72 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments