Skip to content

refactor(@angular/build): use new Angular SSR API #28283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions goldens/circular-deps/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"packages/angular/build/src/tools/esbuild/utils.ts",
"packages/angular/build/src/tools/esbuild/bundler-execution-result.ts"
],
[
"packages/angular/build/src/tools/esbuild/bundler-context.ts",
"packages/angular/build/src/tools/esbuild/utils.ts",
"packages/angular/build/src/utils/server-rendering/manifest.ts"
],
[
"packages/angular/build/src/tools/esbuild/bundler-execution-result.ts",
"packages/angular/build/src/tools/esbuild/utils.ts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
import { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
import { createOutputFile } from '../../tools/esbuild/utils';
import { maxWorkers } from '../../utils/environment-options';
import {
SERVER_APP_MANIFEST_FILENAME,
generateAngularServerAppManifest,
} from '../../utils/server-rendering/manifest';
import { prerenderPages } from '../../utils/server-rendering/prerender';
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
import { NormalizedApplicationBuildOptions } from './options';
import { INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options';

/**
* Run additional builds steps including SSG, AppShell, Index HTML file and Service worker generation.
Expand Down Expand Up @@ -48,25 +52,22 @@ export async function executePostBundleSteps(
const prerenderedRoutes: string[] = [];

const {
baseHref = '/',
serviceWorker,
indexHtmlOptions,
optimizationOptions,
sourcemapOptions,
ssrOptions,
prerenderOptions,
appShellOptions,
workspaceRoot,
verbose,
} = options;

/**
* Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR).
*
* NOTE: we don't perform critical CSS inlining as this will be done during server rendering.
*/
let ssrIndexContent: string | undefined;

// When using prerender/app-shell the index HTML file can be regenerated.
// Thus, we use a Map so that we do not generate 2 files with the same filename.
// Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR).
// NOTE: Critical CSS inlining is deliberately omitted here, as it will be handled during server rendering.
// Additionally, when using prerendering or AppShell, the index HTML file may be regenerated.
// To prevent generating duplicate files with the same filename, a `Map` is used to store and manage the files.
const additionalHtmlOutputFiles = new Map<string, BuildOutputFile>();

// Generate index HTML file
Expand All @@ -88,21 +89,34 @@ export async function executePostBundleSteps(
);

if (ssrContent) {
const serverIndexHtmlFilename = 'index.server.html';
additionalHtmlOutputFiles.set(
serverIndexHtmlFilename,
createOutputFile(serverIndexHtmlFilename, ssrContent, BuildOutputFileType.Server),
INDEX_HTML_SERVER,
createOutputFile(INDEX_HTML_SERVER, ssrContent, BuildOutputFileType.Server),
);

ssrIndexContent = ssrContent;
}
}

// Create server manifest
if (prerenderOptions || appShellOptions || ssrOptions) {
additionalOutputFiles.push(
createOutputFile(
SERVER_APP_MANIFEST_FILENAME,
generateAngularServerAppManifest(
additionalHtmlOutputFiles,
outputFiles,
optimizationOptions.styles.inlineCritical ?? false,
undefined,
),
BuildOutputFileType.Server,
),
);
}

// Pre-render (SSG) and App-shell
// If localization is enabled, prerendering is handled in the inlining process.
if (prerenderOptions || appShellOptions) {
if ((prerenderOptions || appShellOptions) && !allErrors.length) {
assert(
ssrIndexContent,
indexHtmlOptions,
'The "index" option is required when using the "ssg" or "appShell" options.',
);

Expand All @@ -111,15 +125,15 @@ export async function executePostBundleSteps(
warnings,
errors,
prerenderedRoutes: generatedRoutes,
serializableRouteTreeNode,
} = await prerenderPages(
workspaceRoot,
baseHref,
appShellOptions,
prerenderOptions,
outputFiles,
[...outputFiles, ...additionalOutputFiles],
assetFiles,
ssrIndexContent,
sourcemapOptions.scripts,
optimizationOptions.styles.inlineCritical,
maxWorkers,
verbose,
);
Expand All @@ -128,10 +142,31 @@ export async function executePostBundleSteps(
allWarnings.push(...warnings);
prerenderedRoutes.push(...Array.from(generatedRoutes));

for (const [path, content] of Object.entries(output)) {
const indexHasBeenPrerendered = generatedRoutes.has(indexHtmlOptions.output);

for (const [path, { content, appShellRoute }] of Object.entries(output)) {
// Update the index contents with the app shell under these conditions:
// - Replace 'index.html' with the app shell only if it hasn't been prerendered yet.
// - Always replace 'index.csr.html' with the app shell.
const filePath = appShellRoute && !indexHasBeenPrerendered ? indexHtmlOptions.output : path;
additionalHtmlOutputFiles.set(
path,
createOutputFile(path, content, BuildOutputFileType.Browser),
filePath,
createOutputFile(filePath, content, BuildOutputFileType.Browser),
);
}

if (ssrOptions) {
// Regenerate the manifest to append route tree. This is only needed if SSR is enabled.
const manifest = additionalOutputFiles.find((f) => f.path === SERVER_APP_MANIFEST_FILENAME);
assert(manifest, `${SERVER_APP_MANIFEST_FILENAME} was not found in output files.`);

manifest.contents = new TextEncoder().encode(
generateAngularServerAppManifest(
additionalHtmlOutputFiles,
outputFiles,
optimizationOptions.styles.inlineCritical ?? false,
serializableRouteTreeNode,
),
);
}
}
Expand All @@ -145,7 +180,7 @@ export async function executePostBundleSteps(
const serviceWorkerResult = await augmentAppWithServiceWorkerEsbuild(
workspaceRoot,
serviceWorker,
options.baseHref || '/',
baseHref,
options.indexHtmlOptions?.output,
// Ensure additional files recently added are used
[...outputFiles, ...additionalOutputFiles],
Expand Down
19 changes: 1 addition & 18 deletions packages/angular/build/src/builders/application/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ import { I18nInliner } from '../../tools/esbuild/i18n-inliner';
import { maxWorkers } from '../../utils/environment-options';
import { loadTranslations } from '../../utils/i18n-options';
import { createTranslationLoader } from '../../utils/load-translations';
import { urlJoin } from '../../utils/url';
import { executePostBundleSteps } from './execute-post-bundle';
import { NormalizedApplicationBuildOptions } from './options';
import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options';

/**
* Inlines all active locales as specified by the application build options into all
Expand Down Expand Up @@ -127,22 +126,6 @@ export async function inlineI18n(
return inlineResult;
}

function getLocaleBaseHref(
baseHref: string | undefined,
i18n: NormalizedApplicationBuildOptions['i18nOptions'],
locale: string,
): string | undefined {
if (i18n.flatOutput) {
return undefined;
}

if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') {
return urlJoin(baseHref || '', i18n.locales[locale].baseHref ?? `/${locale}/`);
}

return undefined;
}

/**
* Loads all active translations using the translation loaders from the `@angular/localize` package.
* @param context The architect builder context for the current build.
Expand Down
31 changes: 30 additions & 1 deletion packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,26 @@ import {
generateSearchDirectories,
loadPostcssConfiguration,
} from '../../utils/postcss-configuration';
import { urlJoin } from '../../utils/url';
import {
Schema as ApplicationBuilderOptions,
I18NTranslation,
OutputHashing,
OutputPathClass,
} from './schema';

/**
* The filename for the client-side rendered HTML template.
* This template is used for client-side rendering (CSR) in a web application.
*/
export const INDEX_HTML_CSR = 'index.csr.html';

/**
* The filename for the server-side rendered HTML template.
* This template is used for server-side rendering (SSR) in a web application.
*/
export const INDEX_HTML_SERVER = 'index.server.html';

export type NormalizedOutputOptions = Required<OutputPathClass> & {
clean: boolean;
ignoreServer: boolean;
Expand Down Expand Up @@ -252,7 +265,7 @@ export async function normalizeOptions(
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
*/
const indexBaseName = path.basename(options.index);
indexOutput = ssrOptions && indexBaseName === 'index.html' ? 'index.csr.html' : indexBaseName;
indexOutput = ssrOptions && indexBaseName === 'index.html' ? INDEX_HTML_CSR : indexBaseName;
} else {
indexOutput = options.index.output || 'index.html';
}
Expand Down Expand Up @@ -532,3 +545,19 @@ function normalizeGlobalEntries(

return [...bundles.values()];
}

export function getLocaleBaseHref(
baseHref: string | undefined,
i18n: NormalizedApplicationBuildOptions['i18nOptions'],
locale: string,
): string | undefined {
if (i18n.flatOutput) {
return undefined;
}

if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') {
return urlJoin(baseHref || '', i18n.locales[locale].baseHref ?? `/${locale}/`);
}

return undefined;
}
30 changes: 16 additions & 14 deletions packages/angular/build/src/builders/application/setup-bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache';
import {
createBrowserCodeBundleOptions,
createBrowserPolyfillBundleOptions,
createServerCodeBundleOptions,
createServerMainCodeBundleOptions,
createServerPolyfillBundleOptions,
} from '../../tools/esbuild/application-code-bundle';
import { BundlerContext } from '../../tools/esbuild/bundler-context';
Expand All @@ -35,16 +35,22 @@ export function setupBundlerContexts(
browsers: string[],
codeBundleCache: SourceFileCache,
): BundlerContext[] {
const { appShellOptions, prerenderOptions, serverEntryPoint, ssrOptions, workspaceRoot } =
options;
const {
appShellOptions,
prerenderOptions,
serverEntryPoint,
ssrOptions,
workspaceRoot,
watch = false,
} = options;
const target = transformSupportedBrowsersToTargets(browsers);
const bundlerContexts = [];

// Browser application code
bundlerContexts.push(
new BundlerContext(
workspaceRoot,
!!options.watch,
watch,
createBrowserCodeBundleOptions(options, target, codeBundleCache),
),
);
Expand All @@ -56,9 +62,7 @@ export function setupBundlerContexts(
codeBundleCache,
);
if (browserPolyfillBundleOptions) {
bundlerContexts.push(
new BundlerContext(workspaceRoot, !!options.watch, browserPolyfillBundleOptions),
);
bundlerContexts.push(new BundlerContext(workspaceRoot, watch, browserPolyfillBundleOptions));
}

// Global Stylesheets
Expand All @@ -67,7 +71,7 @@ export function setupBundlerContexts(
const bundleOptions = createGlobalStylesBundleOptions(options, target, initial);
if (bundleOptions) {
bundlerContexts.push(
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial),
);
}
}
Expand All @@ -79,7 +83,7 @@ export function setupBundlerContexts(
const bundleOptions = createGlobalScriptsBundleOptions(options, target, initial);
if (bundleOptions) {
bundlerContexts.push(
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial),
);
}
}
Expand All @@ -92,8 +96,8 @@ export function setupBundlerContexts(
bundlerContexts.push(
new BundlerContext(
workspaceRoot,
!!options.watch,
createServerCodeBundleOptions(options, nodeTargets, codeBundleCache),
watch,
createServerMainCodeBundleOptions(options, nodeTargets, codeBundleCache),
),
);

Expand All @@ -105,9 +109,7 @@ export function setupBundlerContexts(
);

if (serverPolyfillBundleOptions) {
bundlerContexts.push(
new BundlerContext(workspaceRoot, !!options.watch, serverPolyfillBundleOptions),
);
bundlerContexts.push(new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions));
}
}

Expand Down
1 change: 0 additions & 1 deletion packages/angular/build/src/builders/dev-server/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export { type BuildOutputFile, BuildOutputFileType } from '@angular/build';
export { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin';
export { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
export { getFeatureSupport, isZonelessApp } from '../../tools/esbuild/utils';
export { renderPage } from '../../utils/server-rendering/render-page';
export { type IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
export { purgeStaleBuildCache } from '../../utils/purge-cache';
export { getSupportedBrowsers } from '../../utils/supported-browsers';
Expand Down
Loading