FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(@angular/build): fail build and exclude routes when prerendering fails by alan-agius4 · Pull Request #33971 · angular/angular-cli · GitHub

Repository navigation

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

Filter by extension

Filter by extension .ts  (4) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,13 @@ export async function executePostBundleSteps(
'The "index" option is required when using the "ssg" or "appShell" options.',
);

const { output, warnings, errors, serializableRouteTreeNode } = await prerenderPages(
const {
output,
warnings,
errors,
serializableRouteTreeNode,
prerenderedRoutes: generatedPrerenderedRoutes,
} = await prerenderPages(
workspaceRoot,
baseHref,
appShellOptions,
Expand All @@ -171,6 +177,7 @@ export async function executePostBundleSteps(

allErrors.push(...errors);
allWarnings.push(...warnings);
Object.assign(prerenderedRoutes, generatedPrerenderedRoutes);

const indexHasBeenPrerendered = output[indexHtmlOptions.output];
for (const [path, { content, appShellRoute }] of Object.entries(output)) {
Expand All @@ -195,10 +202,6 @@ export async function executePostBundleSteps(
const serializableRouteTreeNodeForManifest: WritableSerializableRouteTreeNode = [];
for (const metadata of serializableRouteTreeNode) {
serializableRouteTreeNodeForManifest.push(metadata);

if (metadata.renderMode === RouteRenderMode.Prerender && !metadata.route.includes('*')) {
prerenderedRoutes[metadata.route] = { headers: metadata.headers };
}
}

if (outputMode === OutputMode.Server) {
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { readFile } from 'node:fs/promises';
import { extname, posix } from 'node:path';
import { NormalizedApplicationBuildOptions } from '../../builders/application/options';
import { OutputMode } from '../../builders/application/schema';
import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
import {
BuildOutputAsset,
PrerenderedRoutesRecord,
} from '../../tools/esbuild/bundler-execution-result';
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files';
import { assertIsError } from '../error';
import { toPosixPath } from '../path';
Expand Down Expand Up @@ -65,6 +68,7 @@ export async function prerenderPages(
output: PrerenderOutput;
warnings: string[];
errors: string[];
prerenderedRoutes: PrerenderedRoutesRecord;
serializableRouteTreeNode: SerializableRouteTreeNode;
}> {
const rawOutputFiles: Record<string, string> = {};
Expand Down Expand Up @@ -167,6 +171,7 @@ export async function prerenderPages(
errors,
warnings,
output: {},
prerenderedRoutes: {},
serializableRouteTreeNode,
};
}
Expand Down Expand Up @@ -200,10 +205,22 @@ export async function prerenderPages(

errors.push(...renderingErrors);

const prerenderedRoutes: PrerenderedRoutesRecord = {};
const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname;

for (const metadata of serializableRouteTreeNodeForPrerender) {
const outPath = getRouteOutPath(metadata.route, baseHrefPathnameWithLeadingSlash);

if (output[outPath]) {
prerenderedRoutes[metadata.route] = { headers: metadata.headers };
}
}
Comment thread
alan-agius4 marked this conversation as resolved.

return {
errors,
warnings,
output,
prerenderedRoutes,
serializableRouteTreeNode,
};
}
Expand All @@ -227,22 +244,15 @@ async function renderPages(

const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname;
const appShellRouteWithoutBaseHref = appShellRoute
? addTrailingSlash(appShellRoute).startsWith(baseHrefPathnameWithLeadingSlash)
? addLeadingSlash(appShellRoute.slice(baseHrefPathnameWithLeadingSlash.length))
: addLeadingSlash(appShellRoute)
? addLeadingSlash(getRouteWithoutBaseHref(appShellRoute, baseHrefPathnameWithLeadingSlash))
: undefined;

const routesToRender: { route: string; outPath: string; isAppShell: boolean }[] = [];

for (const { route, redirectTo } of serializableRouteTreeNode) {
// Remove the base href from the file output path.
const routeWithoutBaseHref = addTrailingSlash(route).startsWith(
baseHrefPathnameWithLeadingSlash,
)
? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length))
: route;

const outPath = stripLeadingSlash(posix.join(routeWithoutBaseHref, 'index.html'));
const routeWithoutBaseHref = getRouteWithoutBaseHref(route, baseHrefPathnameWithLeadingSlash);
const outPath = getRouteOutPath(route, baseHrefPathnameWithLeadingSlash);

if (typeof redirectTo === 'string') {
output[outPath] = { content: generateRedirectStaticPage(redirectTo), appShellRoute: false };
Expand Down Expand Up @@ -305,20 +315,20 @@ async function renderPages(
const renderBatchPromise: Promise<RenderResult> = renderWorker.run(urls);
const batchResultPromise = renderBatchPromise
.then((results) => {
for (const { url, content, error } of results) {
if (error) {
errors.push(`An error occurred while prerendering route '${url}'.\n\n${error}`);
for (const result of results) {
if ('error' in result) {
errors.push(
`An error occurred while prerendering route '${result.url}'.\n\n${result.error}`,
);
continue;
}

if (content !== null) {
const routeInfo = routeOutPathMap.get(url);
if (routeInfo) {
output[routeInfo.outPath] = {
content,
appShellRoute: routeInfo.isAppShell,
};
}
const routeInfo = routeOutPathMap.get(result.url);
if (routeInfo) {
output[routeInfo.outPath] = {
content: result.content,
appShellRoute: routeInfo.isAppShell,
};
}
}
})
Expand Down Expand Up @@ -439,3 +449,15 @@ async function getAllRoutes(
void renderWorker.destroy();
}
}

function getRouteWithoutBaseHref(route: string, baseHrefPathname: string): string {
return addTrailingSlash(route).startsWith(baseHrefPathname)
? addLeadingSlash(route.slice(baseHrefPathname.length))
: route;
}

function getRouteOutPath(route: string, baseHrefPathname: string): string {
const routeWithoutBaseHref = getRouteWithoutBaseHref(route, baseHrefPathname);

return stripLeadingSlash(posix.join(routeWithoutBaseHref, 'index.html'));
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
hasSsrEntry: boolean;
}

export interface RenderResultItem {
url: string;
content: string | null;
error?: string;
}
export type RenderResultItem =
| {
url: string;
content: string;
}
| {
url: string;
error: string;
};

export type RenderResult = RenderResultItem[];

Expand Down Expand Up @@ -74,12 +78,16 @@ async function renderPages(urls: string[]): Promise<RenderResult> {
for (const currentUrl of urls) {
try {
const content = await renderPage(currentUrl, angularServerApp);

if (content === null) {
throw new Error('The content returned was empty.');
}

results.push({ url: currentUrl, content });
} catch (err) {
assertIsError(err);
results.push({
url: currentUrl,
content: null,
error: err.stack ?? err.message ?? err.code ?? `${err}`,
});
}
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { existsSync } from 'node:fs';
import assert, { match } from 'node:assert';
import { getGlobalVariable } from '../../../utils/env';
import { expectFileNotToExist, readFile, rimraf, writeMultipleFiles } from '../../../utils/fs';
import { installWorkspacePackages } from '../../../utils/packages';
import { ng } from '../../../utils/process';
import { useSha } from '../../../utils/project';
import { expectToFail } from '../../../utils/utils';

export default async function () {
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
if (useWebpackBuilder) {
return;
}

// Forcibly remove in case another test doesn't clean itself up.
await rimraf('node_modules/@angular/ssr');
await ng('add', '@angular/ssr', '--skip-confirmation');
await useSha();
await installWorkspacePackages();

await writeMultipleFiles({
'src/app/app.routes.ts': `
import { Routes } from '@angular/router';
import { Component } from '@angular/core';

@Component({
selector: 'app-home',
standalone: true,
template: '<p>home works!</p>',
})
export class HomeRoute {}

@Component({
selector: 'app-second',
standalone: true,
template: '<p>second works!</p>',
})
export class SecondRoute {
constructor() {
throw new Error('render failure');
}
}

export const routes: Routes = [
{ path: '', component: HomeRoute },
{ path: 'second', component: SecondRoute },
];
`,
'src/app/app.routes.server.ts': `
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
{ path: 'second', renderMode: RenderMode.Prerender },
{ path: '**', renderMode: RenderMode.Prerender },
];
`,
});

const { message } = await expectToFail(() => ng('build', '--output-mode=server'));

match(message, /An error occurred while prerendering route '\/second'\./);

await expectFileNotToExist('dist/test-project/browser/second/index.html');

// prerendered-routes.json should only contain successfully prerendered routes if emitted
const statsPath = 'dist/test-project/prerendered-routes.json';
if (existsSync(statsPath)) {
const stats = JSON.parse(await readFile(statsPath));
assert.strictEqual(stats.routes['/second'], undefined);
}
}

Back | FazBrowse Home | New Git URL