Skip to content

Commit 2dc6566

Browse files
committed
fix(@angular-devkit/build-angular): generate a file containing a list of prerendered routes
With this change when SSG is enabled a `prerendered-routes.json` file is emitted that contains all the prerendered routes. This is useful for Cloud providers and other server engines to have server rules to serve these files as static.
1 parent 08c1229 commit 2dc6566

File tree

5 files changed

+95
-33
lines changed

5 files changed

+95
-33
lines changed

packages/angular_devkit/build_angular/src/builders/application/execute-build.ts

+30-14
Original file line numberDiff line numberDiff line change
@@ -198,25 +198,41 @@ export async function executeBuild(
198198
}
199199

200200
// Perform i18n translation inlining if enabled
201+
let prerenderedRoutes: string[];
202+
let errors: string[];
203+
let warnings: string[];
201204
if (i18nOptions.shouldInline) {
202-
const { errors, warnings } = await inlineI18n(options, executionResult, initialFiles);
203-
printWarningsAndErrorsToConsole(context, warnings, errors);
205+
const result = await inlineI18n(options, executionResult, initialFiles);
206+
errors = result.errors;
207+
warnings = result.warnings;
208+
prerenderedRoutes = result.prerenderedRoutes;
204209
} else {
205-
const { errors, warnings, additionalAssets, additionalOutputFiles } =
206-
await executePostBundleSteps(
207-
options,
208-
executionResult.outputFiles,
209-
executionResult.assetFiles,
210-
initialFiles,
211-
// Set lang attribute to the defined source locale if present
212-
i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined,
213-
);
210+
const result = await executePostBundleSteps(
211+
options,
212+
executionResult.outputFiles,
213+
executionResult.assetFiles,
214+
initialFiles,
215+
// Set lang attribute to the defined source locale if present
216+
i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined,
217+
);
214218

215-
executionResult.outputFiles.push(...additionalOutputFiles);
216-
executionResult.assetFiles.push(...additionalAssets);
217-
printWarningsAndErrorsToConsole(context, warnings, errors);
219+
errors = result.errors;
220+
warnings = result.warnings;
221+
prerenderedRoutes = result.prerenderedRoutes;
222+
executionResult.outputFiles.push(...result.additionalOutputFiles);
223+
executionResult.assetFiles.push(...result.additionalAssets);
218224
}
219225

226+
if (prerenderOptions) {
227+
executionResult.addOutputFile(
228+
'prerendered-routes.json',
229+
JSON.stringify({ routes: prerenderedRoutes.sort((a, b) => a.localeCompare(b)) }, null, 2),
230+
BuildOutputFileType.Root,
231+
);
232+
}
233+
234+
printWarningsAndErrorsToConsole(context, warnings, errors);
235+
220236
logBuildStats(context, metafile, initialFiles, budgetFailures, estimatedTransferSizes);
221237

222238
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;

packages/angular_devkit/build_angular/src/builders/application/execute-post-bundle.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ export async function executePostBundleSteps(
3939
warnings: string[];
4040
additionalOutputFiles: BuildOutputFile[];
4141
additionalAssets: BuildOutputAsset[];
42+
prerenderedRoutes: string[];
4243
}> {
4344
const additionalAssets: BuildOutputAsset[] = [];
4445
const additionalOutputFiles: BuildOutputFile[] = [];
4546
const allErrors: string[] = [];
4647
const allWarnings: string[] = [];
48+
const prerenderedRoutes: string[] = [];
4749

4850
const {
4951
serviceWorker,
@@ -105,7 +107,12 @@ export async function executePostBundleSteps(
105107
'The "index" option is required when using the "ssg" or "appShell" options.',
106108
);
107109

108-
const { output, warnings, errors } = await prerenderPages(
110+
const {
111+
output,
112+
warnings,
113+
errors,
114+
prerenderedRoutes: generatedRoutes,
115+
} = await prerenderPages(
109116
workspaceRoot,
110117
appShellOptions,
111118
prerenderOptions,
@@ -119,6 +126,7 @@ export async function executePostBundleSteps(
119126

120127
allErrors.push(...errors);
121128
allWarnings.push(...warnings);
129+
prerenderedRoutes.push(...Array.from(generatedRoutes));
122130

123131
for (const [path, content] of Object.entries(output)) {
124132
additionalOutputFiles.push(
@@ -155,6 +163,7 @@ export async function executePostBundleSteps(
155163
errors: allErrors,
156164
warnings: allWarnings,
157165
additionalAssets,
166+
prerenderedRoutes,
158167
additionalOutputFiles,
159168
};
160169
}

packages/angular_devkit/build_angular/src/builders/application/i18n.ts

+25-14
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { BuilderContext } from '@angular-devkit/architect';
10-
import { join } from 'node:path';
10+
import { join, posix } from 'node:path';
1111
import { InitialFileRecord } from '../../tools/esbuild/bundler-context';
1212
import { ExecutionResult } from '../../tools/esbuild/bundler-execution-result';
1313
import { I18nInliner } from '../../tools/esbuild/i18n-inliner';
@@ -29,7 +29,7 @@ export async function inlineI18n(
2929
options: NormalizedApplicationBuildOptions,
3030
executionResult: ExecutionResult,
3131
initialFiles: Map<string, InitialFileRecord>,
32-
): Promise<{ errors: string[]; warnings: string[] }> {
32+
): Promise<{ errors: string[]; warnings: string[]; prerenderedRoutes: string[] }> {
3333
// Create the multi-threaded inliner with common options and the files generated from the build.
3434
const inliner = new I18nInliner(
3535
{
@@ -40,9 +40,10 @@ export async function inlineI18n(
4040
maxWorkers,
4141
);
4242

43-
const inlineResult: { errors: string[]; warnings: string[] } = {
43+
const inlineResult: { errors: string[]; warnings: string[]; prerenderedRoutes: string[] } = {
4444
errors: [],
4545
warnings: [],
46+
prerenderedRoutes: [],
4647
};
4748

4849
// For each active locale, use the inliner to process the output files of the build.
@@ -59,17 +60,22 @@ export async function inlineI18n(
5960
const baseHref =
6061
getLocaleBaseHref(options.baseHref, options.i18nOptions, locale) ?? options.baseHref;
6162

62-
const { errors, warnings, additionalAssets, additionalOutputFiles } =
63-
await executePostBundleSteps(
64-
{
65-
...options,
66-
baseHref,
67-
},
68-
localeOutputFiles,
69-
executionResult.assetFiles,
70-
initialFiles,
71-
locale,
72-
);
63+
const {
64+
errors,
65+
warnings,
66+
additionalAssets,
67+
additionalOutputFiles,
68+
prerenderedRoutes: generatedRoutes,
69+
} = await executePostBundleSteps(
70+
{
71+
...options,
72+
baseHref,
73+
},
74+
localeOutputFiles,
75+
executionResult.assetFiles,
76+
initialFiles,
77+
locale,
78+
);
7379

7480
localeOutputFiles.push(...additionalOutputFiles);
7581
inlineResult.errors.push(...errors);
@@ -87,7 +93,12 @@ export async function inlineI18n(
8793
destination: join(locale, assetFile.destination),
8894
});
8995
}
96+
97+
inlineResult.prerenderedRoutes.push(
98+
...generatedRoutes.map((route) => posix.join('/', locale, route)),
99+
);
90100
} else {
101+
inlineResult.prerenderedRoutes.push(...generatedRoutes);
91102
executionResult.assetFiles.push(...additionalAssets);
92103
}
93104

packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export async function prerenderPages(
4141
output: Record<string, string>;
4242
warnings: string[];
4343
errors: string[];
44+
prerenderedRoutes: Set<string>;
4445
}> {
4546
const output: Record<string, string> = {};
4647
const warnings: string[] = [];
@@ -92,6 +93,7 @@ export async function prerenderPages(
9293
errors,
9394
warnings,
9495
output,
96+
prerenderedRoutes: allRoutes,
9597
};
9698
}
9799

@@ -114,7 +116,7 @@ export async function prerenderPages(
114116

115117
try {
116118
const renderingPromises: Promise<void>[] = [];
117-
const appShellRoute = appShellOptions.route && removeLeadingSlash(appShellOptions.route);
119+
const appShellRoute = appShellOptions.route && addLeadingSlash(appShellOptions.route);
118120

119121
for (const route of allRoutes) {
120122
const isAppShellRoute = appShellRoute === route;
@@ -123,7 +125,9 @@ export async function prerenderPages(
123125
const render: Promise<RenderResult> = renderWorker.run({ route, serverContext });
124126
const renderResult: Promise<void> = render.then(({ content, warnings, errors }) => {
125127
if (content !== undefined) {
126-
const outPath = isAppShellRoute ? 'index.html' : posix.join(route, 'index.html');
128+
const outPath = isAppShellRoute
129+
? 'index.html'
130+
: removeLeadingSlash(posix.join(route, 'index.html'));
127131
output[outPath] = content;
128132
}
129133

@@ -148,12 +152,13 @@ export async function prerenderPages(
148152
errors,
149153
warnings,
150154
output,
155+
prerenderedRoutes: allRoutes,
151156
};
152157
}
153158

154159
class RoutesSet extends Set<string> {
155160
override add(value: string): this {
156-
return super.add(removeLeadingSlash(value));
161+
return super.add(addLeadingSlash(value));
157162
}
158163
}
159164

@@ -213,6 +218,10 @@ async function getAllRoutes(
213218
return { routes, warnings };
214219
}
215220

221+
function addLeadingSlash(value: string): string {
222+
return value.charAt(0) === '/' ? value : '/' + value;
223+
}
224+
216225
function removeLeadingSlash(value: string): string {
217226
return value.charAt(0) === '/' ? value.slice(1) : value;
218227
}

tests/legacy-cli/e2e/tests/build/prerender/discover-routes-standalone.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { join } from 'path';
22
import { getGlobalVariable } from '../../../utils/env';
3-
import { expectFileToMatch, rimraf, writeFile } from '../../../utils/fs';
3+
import { expectFileToMatch, readFile, rimraf, writeFile } from '../../../utils/fs';
44
import { installWorkspacePackages } from '../../../utils/packages';
55
import { ng } from '../../../utils/process';
66
import { useSha } from '../../../utils/project';
7+
import { deepStrictEqual } from 'node:assert';
78

89
export default async function () {
910
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
@@ -111,5 +112,21 @@ export default async function () {
111112
for (const [filePath, fileMatch] of Object.entries(expects)) {
112113
await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch);
113114
}
115+
116+
if (!useWebpackBuilder) {
117+
// prerendered-routes.json file is only generated when using esbuild.
118+
const generatedRoutesStats = await readFile('dist/test-project/prerendered-routes.json');
119+
deepStrictEqual(JSON.parse(generatedRoutesStats), {
120+
routes: [
121+
'/',
122+
'/lazy-one',
123+
'/lazy-one/lazy-one-child',
124+
'/lazy-two',
125+
'/two',
126+
'/two/two-child-one',
127+
'/two/two-child-two',
128+
],
129+
});
130+
}
114131
}
115132
}

0 commit comments

Comments
 (0)