Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): generate a file containing a list…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
alan-agius4 committed Oct 18, 2023
1 parent 08c1229 commit 2dc6566
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -198,25 +198,41 @@ export async function executeBuild(
}

// Perform i18n translation inlining if enabled
let prerenderedRoutes: string[];
let errors: string[];
let warnings: string[];
if (i18nOptions.shouldInline) {
const { errors, warnings } = await inlineI18n(options, executionResult, initialFiles);
printWarningsAndErrorsToConsole(context, warnings, errors);
const result = await inlineI18n(options, executionResult, initialFiles);
errors = result.errors;
warnings = result.warnings;
prerenderedRoutes = result.prerenderedRoutes;
} else {
const { errors, warnings, additionalAssets, additionalOutputFiles } =
await executePostBundleSteps(
options,
executionResult.outputFiles,
executionResult.assetFiles,
initialFiles,
// Set lang attribute to the defined source locale if present
i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined,
);
const result = await executePostBundleSteps(
options,
executionResult.outputFiles,
executionResult.assetFiles,
initialFiles,
// Set lang attribute to the defined source locale if present
i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined,
);

executionResult.outputFiles.push(...additionalOutputFiles);
executionResult.assetFiles.push(...additionalAssets);
printWarningsAndErrorsToConsole(context, warnings, errors);
errors = result.errors;
warnings = result.warnings;
prerenderedRoutes = result.prerenderedRoutes;
executionResult.outputFiles.push(...result.additionalOutputFiles);
executionResult.assetFiles.push(...result.additionalAssets);
}

if (prerenderOptions) {
executionResult.addOutputFile(
'prerendered-routes.json',
JSON.stringify({ routes: prerenderedRoutes.sort((a, b) => a.localeCompare(b)) }, null, 2),
BuildOutputFileType.Root,
);
}

printWarningsAndErrorsToConsole(context, warnings, errors);

logBuildStats(context, metafile, initialFiles, budgetFailures, estimatedTransferSizes);

const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ export async function executePostBundleSteps(
warnings: string[];
additionalOutputFiles: BuildOutputFile[];
additionalAssets: BuildOutputAsset[];
prerenderedRoutes: string[];
}> {
const additionalAssets: BuildOutputAsset[] = [];
const additionalOutputFiles: BuildOutputFile[] = [];
const allErrors: string[] = [];
const allWarnings: string[] = [];
const prerenderedRoutes: string[] = [];

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

const { output, warnings, errors } = await prerenderPages(
const {
output,
warnings,
errors,
prerenderedRoutes: generatedRoutes,
} = await prerenderPages(
workspaceRoot,
appShellOptions,
prerenderOptions,
Expand All @@ -119,6 +126,7 @@ export async function executePostBundleSteps(

allErrors.push(...errors);
allWarnings.push(...warnings);
prerenderedRoutes.push(...Array.from(generatedRoutes));

for (const [path, content] of Object.entries(output)) {
additionalOutputFiles.push(
Expand Down Expand Up @@ -155,6 +163,7 @@ export async function executePostBundleSteps(
errors: allErrors,
warnings: allWarnings,
additionalAssets,
prerenderedRoutes,
additionalOutputFiles,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

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

const inlineResult: { errors: string[]; warnings: string[] } = {
const inlineResult: { errors: string[]; warnings: string[]; prerenderedRoutes: string[] } = {
errors: [],
warnings: [],
prerenderedRoutes: [],
};

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

const { errors, warnings, additionalAssets, additionalOutputFiles } =
await executePostBundleSteps(
{
...options,
baseHref,
},
localeOutputFiles,
executionResult.assetFiles,
initialFiles,
locale,
);
const {
errors,
warnings,
additionalAssets,
additionalOutputFiles,
prerenderedRoutes: generatedRoutes,
} = await executePostBundleSteps(
{
...options,
baseHref,
},
localeOutputFiles,
executionResult.assetFiles,
initialFiles,
locale,
);

localeOutputFiles.push(...additionalOutputFiles);
inlineResult.errors.push(...errors);
Expand All @@ -87,7 +93,12 @@ export async function inlineI18n(
destination: join(locale, assetFile.destination),
});
}

inlineResult.prerenderedRoutes.push(
...generatedRoutes.map((route) => posix.join('/', locale, route)),
);
} else {
inlineResult.prerenderedRoutes.push(...generatedRoutes);
executionResult.assetFiles.push(...additionalAssets);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export async function prerenderPages(
output: Record<string, string>;
warnings: string[];
errors: string[];
prerenderedRoutes: Set<string>;
}> {
const output: Record<string, string> = {};
const warnings: string[] = [];
Expand Down Expand Up @@ -92,6 +93,7 @@ export async function prerenderPages(
errors,
warnings,
output,
prerenderedRoutes: allRoutes,
};
}

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

try {
const renderingPromises: Promise<void>[] = [];
const appShellRoute = appShellOptions.route && removeLeadingSlash(appShellOptions.route);
const appShellRoute = appShellOptions.route && addLeadingSlash(appShellOptions.route);

for (const route of allRoutes) {
const isAppShellRoute = appShellRoute === route;
Expand All @@ -123,7 +125,9 @@ export async function prerenderPages(
const render: Promise<RenderResult> = renderWorker.run({ route, serverContext });
const renderResult: Promise<void> = render.then(({ content, warnings, errors }) => {
if (content !== undefined) {
const outPath = isAppShellRoute ? 'index.html' : posix.join(route, 'index.html');
const outPath = isAppShellRoute
? 'index.html'
: removeLeadingSlash(posix.join(route, 'index.html'));
output[outPath] = content;
}

Expand All @@ -148,12 +152,13 @@ export async function prerenderPages(
errors,
warnings,
output,
prerenderedRoutes: allRoutes,
};
}

class RoutesSet extends Set<string> {
override add(value: string): this {
return super.add(removeLeadingSlash(value));
return super.add(addLeadingSlash(value));
}
}

Expand Down Expand Up @@ -213,6 +218,10 @@ async function getAllRoutes(
return { routes, warnings };
}

function addLeadingSlash(value: string): string {
return value.charAt(0) === '/' ? value : '/' + value;
}

function removeLeadingSlash(value: string): string {
return value.charAt(0) === '/' ? value.slice(1) : value;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { join } from 'path';
import { getGlobalVariable } from '../../../utils/env';
import { expectFileToMatch, rimraf, writeFile } from '../../../utils/fs';
import { expectFileToMatch, readFile, rimraf, writeFile } from '../../../utils/fs';
import { installWorkspacePackages } from '../../../utils/packages';
import { ng } from '../../../utils/process';
import { useSha } from '../../../utils/project';
import { deepStrictEqual } from 'node:assert';

export default async function () {
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
Expand Down Expand Up @@ -111,5 +112,21 @@ export default async function () {
for (const [filePath, fileMatch] of Object.entries(expects)) {
await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch);
}

if (!useWebpackBuilder) {
// prerendered-routes.json file is only generated when using esbuild.
const generatedRoutesStats = await readFile('dist/test-project/prerendered-routes.json');
deepStrictEqual(JSON.parse(generatedRoutesStats), {
routes: [
'/',
'/lazy-one',
'/lazy-one/lazy-one-child',
'/lazy-two',
'/two',
'/two/two-child-one',
'/two/two-child-two',
],
});
}
}
}

0 comments on commit 2dc6566

Please sign in to comment.