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.

(cherry picked from commit 2dc6566)
  • Loading branch information
alan-agius4 committed Oct 18, 2023
1 parent 657f782 commit b9505ed
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 33 deletions.
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
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,
};
}
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
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;
}
@@ -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 b9505ed

Please sign in to comment.