Skip to content

Commit b9505ed

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. (cherry picked from commit 2dc6566)
1 parent 657f782 commit b9505ed

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

Lines changed: 30 additions & 14 deletions
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

Lines changed: 10 additions & 1 deletion
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

Lines changed: 25 additions & 14 deletions
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

Lines changed: 12 additions & 3 deletions
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

Lines changed: 18 additions & 1 deletion
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)