Skip to content

Commit 1449c84

Browse files
committed
refactor(@angular/build): use new Angular SSR API
This commit introduces several key updates: - Updates Vite middleware to utilize the new SSR API. - Refactors prerendering, app-shell, and route extraction to align with the new rendering API. - Generates manifest files required by the new SSR API. **Note:** This is primarily a refactor; new features and improvements will be implemented in future updates.
1 parent f5b070d commit 1449c84

21 files changed

+597
-549
lines changed

goldens/circular-deps/packages.json

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"packages/angular/build/src/tools/esbuild/utils.ts",
1313
"packages/angular/build/src/tools/esbuild/bundler-execution-result.ts"
1414
],
15+
[
16+
"packages/angular/build/src/tools/esbuild/bundler-context.ts",
17+
"packages/angular/build/src/tools/esbuild/utils.ts",
18+
"packages/angular/build/src/utils/server-rendering/manifest.ts"
19+
],
1520
[
1621
"packages/angular/build/src/tools/esbuild/bundler-execution-result.ts",
1722
"packages/angular/build/src/tools/esbuild/utils.ts"

packages/angular/build/src/builders/application/execute-post-bundle.ts

+59-24
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
1616
import { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
1717
import { createOutputFile } from '../../tools/esbuild/utils';
1818
import { maxWorkers } from '../../utils/environment-options';
19+
import {
20+
SERVER_APP_MANIFEST_FILENAME,
21+
generateAngularServerAppManifest,
22+
} from '../../utils/server-rendering/manifest';
1923
import { prerenderPages } from '../../utils/server-rendering/prerender';
2024
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
21-
import { NormalizedApplicationBuildOptions } from './options';
25+
import { INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options';
2226

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

5054
const {
55+
baseHref = '/',
5156
serviceWorker,
5257
indexHtmlOptions,
5358
optimizationOptions,
5459
sourcemapOptions,
60+
ssrOptions,
5561
prerenderOptions,
5662
appShellOptions,
5763
workspaceRoot,
5864
verbose,
5965
} = options;
6066

61-
/**
62-
* Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR).
63-
*
64-
* NOTE: we don't perform critical CSS inlining as this will be done during server rendering.
65-
*/
66-
let ssrIndexContent: string | undefined;
67-
68-
// When using prerender/app-shell the index HTML file can be regenerated.
69-
// Thus, we use a Map so that we do not generate 2 files with the same filename.
67+
// Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR).
68+
// NOTE: Critical CSS inlining is deliberately omitted here, as it will be handled during server rendering.
69+
// Additionally, when using prerendering or AppShell, the index HTML file may be regenerated.
70+
// To prevent generating duplicate files with the same filename, a `Map` is used to store and manage the files.
7071
const additionalHtmlOutputFiles = new Map<string, BuildOutputFile>();
7172

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

9091
if (ssrContent) {
91-
const serverIndexHtmlFilename = 'index.server.html';
9292
additionalHtmlOutputFiles.set(
93-
serverIndexHtmlFilename,
94-
createOutputFile(serverIndexHtmlFilename, ssrContent, BuildOutputFileType.Server),
93+
INDEX_HTML_SERVER,
94+
createOutputFile(INDEX_HTML_SERVER, ssrContent, BuildOutputFileType.Server),
9595
);
96-
97-
ssrIndexContent = ssrContent;
9896
}
9997
}
10098

99+
// Create server manifest
100+
if (prerenderOptions || appShellOptions || ssrOptions) {
101+
additionalOutputFiles.push(
102+
createOutputFile(
103+
SERVER_APP_MANIFEST_FILENAME,
104+
generateAngularServerAppManifest(
105+
additionalHtmlOutputFiles,
106+
outputFiles,
107+
optimizationOptions.styles.inlineCritical ?? false,
108+
undefined,
109+
),
110+
BuildOutputFileType.Server,
111+
),
112+
);
113+
}
114+
101115
// Pre-render (SSG) and App-shell
102116
// If localization is enabled, prerendering is handled in the inlining process.
103-
if (prerenderOptions || appShellOptions) {
117+
if ((prerenderOptions || appShellOptions) && !allErrors.length) {
104118
assert(
105-
ssrIndexContent,
119+
indexHtmlOptions,
106120
'The "index" option is required when using the "ssg" or "appShell" options.',
107121
);
108122

@@ -111,15 +125,15 @@ export async function executePostBundleSteps(
111125
warnings,
112126
errors,
113127
prerenderedRoutes: generatedRoutes,
128+
serializableRouteTreeNode,
114129
} = await prerenderPages(
115130
workspaceRoot,
131+
baseHref,
116132
appShellOptions,
117133
prerenderOptions,
118-
outputFiles,
134+
[...outputFiles, ...additionalOutputFiles],
119135
assetFiles,
120-
ssrIndexContent,
121136
sourcemapOptions.scripts,
122-
optimizationOptions.styles.inlineCritical,
123137
maxWorkers,
124138
verbose,
125139
);
@@ -128,10 +142,31 @@ export async function executePostBundleSteps(
128142
allWarnings.push(...warnings);
129143
prerenderedRoutes.push(...Array.from(generatedRoutes));
130144

131-
for (const [path, content] of Object.entries(output)) {
145+
const indexHasBeenPrerendered = generatedRoutes.has(indexHtmlOptions.output);
146+
147+
for (const [path, { content, appShellRoute }] of Object.entries(output)) {
148+
// Update the index contents with the app shell under these conditions:
149+
// - Replace 'index.html' with the app shell only if it hasn't been prerendered yet.
150+
// - Always replace 'index.csr.html' with the app shell.
151+
const filePath = appShellRoute && !indexHasBeenPrerendered ? indexHtmlOptions.output : path;
132152
additionalHtmlOutputFiles.set(
133-
path,
134-
createOutputFile(path, content, BuildOutputFileType.Browser),
153+
filePath,
154+
createOutputFile(filePath, content, BuildOutputFileType.Browser),
155+
);
156+
}
157+
158+
if (ssrOptions) {
159+
// Regenerate the manifest to append route tree. This is only needed if SSR is enabled.
160+
const manifest = additionalOutputFiles.find((f) => f.path === SERVER_APP_MANIFEST_FILENAME);
161+
assert(manifest, `${SERVER_APP_MANIFEST_FILENAME} was not found in output files.`);
162+
163+
manifest.contents = new TextEncoder().encode(
164+
generateAngularServerAppManifest(
165+
additionalHtmlOutputFiles,
166+
outputFiles,
167+
optimizationOptions.styles.inlineCritical ?? false,
168+
serializableRouteTreeNode,
169+
),
135170
);
136171
}
137172
}
@@ -145,7 +180,7 @@ export async function executePostBundleSteps(
145180
const serviceWorkerResult = await augmentAppWithServiceWorkerEsbuild(
146181
workspaceRoot,
147182
serviceWorker,
148-
options.baseHref || '/',
183+
baseHref,
149184
options.indexHtmlOptions?.output,
150185
// Ensure additional files recently added are used
151186
[...outputFiles, ...additionalOutputFiles],

packages/angular/build/src/builders/application/i18n.ts

+1-18
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ import { I18nInliner } from '../../tools/esbuild/i18n-inliner';
1414
import { maxWorkers } from '../../utils/environment-options';
1515
import { loadTranslations } from '../../utils/i18n-options';
1616
import { createTranslationLoader } from '../../utils/load-translations';
17-
import { urlJoin } from '../../utils/url';
1817
import { executePostBundleSteps } from './execute-post-bundle';
19-
import { NormalizedApplicationBuildOptions } from './options';
18+
import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options';
2019

2120
/**
2221
* Inlines all active locales as specified by the application build options into all
@@ -127,22 +126,6 @@ export async function inlineI18n(
127126
return inlineResult;
128127
}
129128

130-
function getLocaleBaseHref(
131-
baseHref: string | undefined,
132-
i18n: NormalizedApplicationBuildOptions['i18nOptions'],
133-
locale: string,
134-
): string | undefined {
135-
if (i18n.flatOutput) {
136-
return undefined;
137-
}
138-
139-
if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') {
140-
return urlJoin(baseHref || '', i18n.locales[locale].baseHref ?? `/${locale}/`);
141-
}
142-
143-
return undefined;
144-
}
145-
146129
/**
147130
* Loads all active translations using the translation loaders from the `@angular/localize` package.
148131
* @param context The architect builder context for the current build.

packages/angular/build/src/builders/application/options.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,26 @@ import {
2424
generateSearchDirectories,
2525
loadPostcssConfiguration,
2626
} from '../../utils/postcss-configuration';
27+
import { urlJoin } from '../../utils/url';
2728
import {
2829
Schema as ApplicationBuilderOptions,
2930
I18NTranslation,
3031
OutputHashing,
3132
OutputPathClass,
3233
} from './schema';
3334

35+
/**
36+
* The filename for the client-side rendered HTML template.
37+
* This template is used for client-side rendering (CSR) in a web application.
38+
*/
39+
export const INDEX_HTML_CSR = 'index.csr.html';
40+
41+
/**
42+
* The filename for the server-side rendered HTML template.
43+
* This template is used for server-side rendering (SSR) in a web application.
44+
*/
45+
export const INDEX_HTML_SERVER = 'index.server.html';
46+
3447
export type NormalizedOutputOptions = Required<OutputPathClass> & {
3548
clean: boolean;
3649
ignoreServer: boolean;
@@ -252,7 +265,7 @@ export async function normalizeOptions(
252265
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
253266
*/
254267
const indexBaseName = path.basename(options.index);
255-
indexOutput = ssrOptions && indexBaseName === 'index.html' ? 'index.csr.html' : indexBaseName;
268+
indexOutput = ssrOptions && indexBaseName === 'index.html' ? INDEX_HTML_CSR : indexBaseName;
256269
} else {
257270
indexOutput = options.index.output || 'index.html';
258271
}
@@ -532,3 +545,19 @@ function normalizeGlobalEntries(
532545

533546
return [...bundles.values()];
534547
}
548+
549+
export function getLocaleBaseHref(
550+
baseHref: string | undefined,
551+
i18n: NormalizedApplicationBuildOptions['i18nOptions'],
552+
locale: string,
553+
): string | undefined {
554+
if (i18n.flatOutput) {
555+
return undefined;
556+
}
557+
558+
if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') {
559+
return urlJoin(baseHref || '', i18n.locales[locale].baseHref ?? `/${locale}/`);
560+
}
561+
562+
return undefined;
563+
}

packages/angular/build/src/builders/application/setup-bundling.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache';
1010
import {
1111
createBrowserCodeBundleOptions,
1212
createBrowserPolyfillBundleOptions,
13-
createServerCodeBundleOptions,
13+
createServerMainCodeBundleOptions,
1414
createServerPolyfillBundleOptions,
1515
} from '../../tools/esbuild/application-code-bundle';
1616
import { BundlerContext } from '../../tools/esbuild/bundler-context';
@@ -35,16 +35,22 @@ export function setupBundlerContexts(
3535
browsers: string[],
3636
codeBundleCache: SourceFileCache,
3737
): BundlerContext[] {
38-
const { appShellOptions, prerenderOptions, serverEntryPoint, ssrOptions, workspaceRoot } =
39-
options;
38+
const {
39+
appShellOptions,
40+
prerenderOptions,
41+
serverEntryPoint,
42+
ssrOptions,
43+
workspaceRoot,
44+
watch = false,
45+
} = options;
4046
const target = transformSupportedBrowsersToTargets(browsers);
4147
const bundlerContexts = [];
4248

4349
// Browser application code
4450
bundlerContexts.push(
4551
new BundlerContext(
4652
workspaceRoot,
47-
!!options.watch,
53+
watch,
4854
createBrowserCodeBundleOptions(options, target, codeBundleCache),
4955
),
5056
);
@@ -56,9 +62,7 @@ export function setupBundlerContexts(
5662
codeBundleCache,
5763
);
5864
if (browserPolyfillBundleOptions) {
59-
bundlerContexts.push(
60-
new BundlerContext(workspaceRoot, !!options.watch, browserPolyfillBundleOptions),
61-
);
65+
bundlerContexts.push(new BundlerContext(workspaceRoot, watch, browserPolyfillBundleOptions));
6266
}
6367

6468
// Global Stylesheets
@@ -67,7 +71,7 @@ export function setupBundlerContexts(
6771
const bundleOptions = createGlobalStylesBundleOptions(options, target, initial);
6872
if (bundleOptions) {
6973
bundlerContexts.push(
70-
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
74+
new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial),
7175
);
7276
}
7377
}
@@ -79,7 +83,7 @@ export function setupBundlerContexts(
7983
const bundleOptions = createGlobalScriptsBundleOptions(options, target, initial);
8084
if (bundleOptions) {
8185
bundlerContexts.push(
82-
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
86+
new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial),
8387
);
8488
}
8589
}
@@ -92,8 +96,8 @@ export function setupBundlerContexts(
9296
bundlerContexts.push(
9397
new BundlerContext(
9498
workspaceRoot,
95-
!!options.watch,
96-
createServerCodeBundleOptions(options, nodeTargets, codeBundleCache),
99+
watch,
100+
createServerMainCodeBundleOptions(options, nodeTargets, codeBundleCache),
97101
),
98102
);
99103

@@ -105,9 +109,7 @@ export function setupBundlerContexts(
105109
);
106110

107111
if (serverPolyfillBundleOptions) {
108-
bundlerContexts.push(
109-
new BundlerContext(workspaceRoot, !!options.watch, serverPolyfillBundleOptions),
110-
);
112+
bundlerContexts.push(new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions));
111113
}
112114
}
113115

packages/angular/build/src/builders/dev-server/internal.ts

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export { type BuildOutputFile, BuildOutputFileType } from '@angular/build';
1010
export { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin';
1111
export { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
1212
export { getFeatureSupport, isZonelessApp } from '../../tools/esbuild/utils';
13-
export { renderPage } from '../../utils/server-rendering/render-page';
1413
export { type IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
1514
export { purgeStaleBuildCache } from '../../utils/purge-cache';
1615
export { getSupportedBrowsers } from '../../utils/supported-browsers';

0 commit comments

Comments
 (0)