Skip to content

Commit f90791f

Browse files
committed
feat(@angular/build): introduce outputMode option to the application builder
The `outputMode` option defines the build output target, offering two modes: - `'static'`: Generates a static site suitable for deployment on any static hosting service. This mode can produce a fully client-side rendered (CSR) or static site generated (SSG) site. When SSG is enabled, redirects are handled using the `<meta>` tag. - `'server'`: Produces an application designed for deployment on a server that supports server-side rendering (SSR) or a hybrid approach. Additionally, the `outputMode` option determines whether the new API is used. If enabled, it bundles the `server.ts` as a separate entry point, preventing it from directly referencing `main.server.ts` and excluding it from localization. In the future, it is also being considered that this option will replace `appShell` and `prerendering` when server routing configuration is present.
1 parent 84e14d6 commit f90791f

File tree

15 files changed

+260
-48
lines changed

15 files changed

+260
-48
lines changed

goldens/public-api/angular/build/index.api.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface ApplicationBuilderOptions {
4040
namedChunks?: boolean;
4141
optimization?: OptimizationUnion;
4242
outputHashing?: OutputHashing;
43+
outputMode?: OutputMode;
4344
outputPath: OutputPathUnion;
4445
poll?: number;
4546
polyfills?: string[];
@@ -99,13 +100,15 @@ export interface BuildOutputFile extends OutputFile {
99100
// @public (undocumented)
100101
export enum BuildOutputFileType {
101102
// (undocumented)
102-
Browser = 1,
103+
Browser = 0,
103104
// (undocumented)
104-
Media = 2,
105+
Media = 1,
105106
// (undocumented)
106107
Root = 4,
107108
// (undocumented)
108-
Server = 3
109+
Server = 2,
110+
// (undocumented)
111+
SSRServer = 3
109112
}
110113

111114
// @public

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function executeBuild(
3737
optimizationOptions,
3838
assets,
3939
cacheOptions,
40-
prerenderOptions,
40+
serverEntryPoint,
4141
ssrOptions,
4242
verbose,
4343
colors,
@@ -183,7 +183,7 @@ export async function executeBuild(
183183
executionResult.assetFiles.push(...result.additionalAssets);
184184
}
185185

186-
if (prerenderOptions) {
186+
if (serverEntryPoint) {
187187
const prerenderedRoutes = executionResult.prerenderedRoutes;
188188
executionResult.addOutputFile(
189189
'prerendered-routes.json',

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { prerenderPages } from '../../utils/server-rendering/prerender';
2424
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
2525
import { INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options';
26+
import { OutputMode } from './schema';
2627

2728
/**
2829
* Run additional builds steps including SSG, AppShell, Index HTML file and Service worker generation.
@@ -57,10 +58,13 @@ export async function executePostBundleSteps(
5758
indexHtmlOptions,
5859
optimizationOptions,
5960
sourcemapOptions,
61+
outputMode,
62+
serverEntryPoint,
6063
ssrOptions,
6164
prerenderOptions,
6265
appShellOptions,
6366
workspaceRoot,
67+
disableFullServerManifestGeneration,
6468
verbose,
6569
} = options;
6670

@@ -97,7 +101,7 @@ export async function executePostBundleSteps(
97101
}
98102

99103
// Create server manifest
100-
if (prerenderOptions || appShellOptions || ssrOptions) {
104+
if (serverEntryPoint) {
101105
additionalOutputFiles.push(
102106
createOutputFile(
103107
SERVER_APP_MANIFEST_FILENAME,
@@ -155,7 +159,7 @@ export async function executePostBundleSteps(
155159
);
156160
}
157161

158-
if (ssrOptions) {
162+
if (outputMode === OutputMode.Server && !disableFullServerManifestGeneration) {
159163
// Regenerate the manifest to append route tree. This is only needed if SSR is enabled.
160164
const manifest = additionalOutputFiles.find((f) => f.path === SERVER_APP_MANIFEST_FILENAME);
161165
assert(manifest, `${SERVER_APP_MANIFEST_FILENAME} was not found in output files.`);

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ export async function inlineI18n(
112112

113113
// Update the result with all localized files.
114114
executionResult.outputFiles = [
115-
// Root files are not modified.
116-
...executionResult.outputFiles.filter(({ type }) => type === BuildOutputFileType.Root),
115+
// Root and SSR entry files are not modified.
116+
...executionResult.outputFiles.filter(
117+
({ type }) => type === BuildOutputFileType.Root || type === BuildOutputFileType.SSRServer,
118+
),
117119
// Updated files for each locale.
118120
...updatedOutputFiles,
119121
];

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ export async function* buildApplicationInternal(
8888

8989
yield* runEsBuildBuildAction(
9090
async (rebuildState) => {
91-
const { prerenderOptions, jsonLogs } = normalizedOptions;
91+
const { serverEntryPoint, jsonLogs } = normalizedOptions;
9292

9393
const startTime = process.hrtime.bigint();
9494
const result = await executeBuild(normalizedOptions, context, rebuildState);
9595

9696
if (jsonLogs) {
9797
result.addLog(await createJsonBuildManifest(result, normalizedOptions));
9898
} else {
99-
if (prerenderOptions) {
99+
if (serverEntryPoint) {
100100
const prerenderedRoutesLength = result.prerenderedRoutes.length;
101101
let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`;
102102
prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.';
@@ -236,6 +236,7 @@ export async function* buildApplication(
236236
typeDirectory = outputOptions.browser;
237237
break;
238238
case BuildOutputFileType.Server:
239+
case BuildOutputFileType.SSRServer:
239240
typeDirectory = outputOptions.server;
240241
break;
241242
case BuildOutputFileType.Root:

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

+37
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
Schema as ApplicationBuilderOptions,
3030
I18NTranslation,
3131
OutputHashing,
32+
OutputMode,
3233
OutputPathClass,
3334
} from './schema';
3435

@@ -79,6 +80,16 @@ interface InternalOptions {
7980
* This is only used by the development server which currently only supports a single locale per build.
8081
*/
8182
forceI18nFlatOutput?: boolean;
83+
84+
/**
85+
* When set to `true`, disables the generation of a full manifest with routes.
86+
*
87+
* This option is primarily used during development to improve performance,
88+
* as the full manifest is generated at runtime when using the development server.
89+
*
90+
* @default false
91+
*/
92+
disableFullServerManifestGeneration?: boolean;
8293
}
8394

8495
/** Full set of options for `application` builder. */
@@ -179,6 +190,28 @@ export async function normalizeOptions(
179190
}
180191
}
181192

193+
// Validate prerender and ssr options when using the outputMode
194+
if (
195+
options.outputMode === OutputMode.Server &&
196+
(typeof options.ssr === 'boolean' || !options.ssr?.entry)
197+
) {
198+
throw new Error('The "ssr.entry" option is required when "outputMode" is set to "server".');
199+
}
200+
201+
if (options.outputMode === OutputMode.Server && !options.server) {
202+
throw new Error('The "server" option is required when "outputMode" is set to "server".');
203+
}
204+
205+
if (
206+
options.outputMode &&
207+
options.prerender !== undefined &&
208+
typeof options.prerender !== 'boolean'
209+
) {
210+
context.logger.warn(
211+
'The "prerender" option must be either a boolean value or omitted when "outputMode" is specified.',
212+
);
213+
}
214+
182215
// A configuration file can exist in the project or workspace root
183216
const searchDirectories = await generateSearchDirectories([projectRoot, workspaceRoot]);
184217
const postcssConfiguration = await loadPostcssConfiguration(searchDirectories);
@@ -317,6 +350,7 @@ export async function normalizeOptions(
317350
poll,
318351
polyfills,
319352
statsJson,
353+
outputMode,
320354
stylePreprocessorOptions,
321355
subresourceIntegrity,
322356
verbose,
@@ -328,6 +362,7 @@ export async function normalizeOptions(
328362
deployUrl,
329363
clearScreen,
330364
define,
365+
disableFullServerManifestGeneration = false,
331366
} = options;
332367

333368
// Return all the normalized options
@@ -352,6 +387,7 @@ export async function normalizeOptions(
352387
serverEntryPoint,
353388
prerenderOptions,
354389
appShellOptions,
390+
outputMode,
355391
ssrOptions,
356392
verbose,
357393
watch,
@@ -387,6 +423,7 @@ export async function normalizeOptions(
387423
colors: supportColor(),
388424
clearScreen,
389425
define,
426+
disableFullServerManifestGeneration,
390427
};
391428
}
392429

packages/angular/build/src/builders/application/schema.json

+5
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,11 @@
528528
"type": "boolean",
529529
"description": "Generates an application shell during build time.",
530530
"default": false
531+
},
532+
"outputMode": {
533+
"type": "string",
534+
"description": "Defines the build output target. 'static': Generates a static site for deployment on any static hosting service. 'server': Produces an application designed for deployment on a server that supports server-side rendering (SSR).",
535+
"enum": ["static", "server"]
531536
}
532537
},
533538
"additionalProperties": false,

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

+16-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
createBrowserPolyfillBundleOptions,
1313
createServerMainCodeBundleOptions,
1414
createServerPolyfillBundleOptions,
15+
createSsrEntryCodeBundleOptions,
1516
} from '../../tools/esbuild/application-code-bundle';
1617
import { BundlerContext } from '../../tools/esbuild/bundler-context';
1718
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
@@ -36,9 +37,10 @@ export function setupBundlerContexts(
3637
codeBundleCache: SourceFileCache,
3738
): BundlerContext[] {
3839
const {
40+
outputMode,
41+
serverEntryPoint,
3942
appShellOptions,
4043
prerenderOptions,
41-
serverEntryPoint,
4244
ssrOptions,
4345
workspaceRoot,
4446
watch = false,
@@ -90,9 +92,9 @@ export function setupBundlerContexts(
9092
}
9193

9294
// Skip server build when none of the features are enabled.
93-
if (serverEntryPoint && (prerenderOptions || appShellOptions || ssrOptions)) {
95+
if (serverEntryPoint && (outputMode || prerenderOptions || appShellOptions || ssrOptions)) {
9496
const nodeTargets = [...target, ...getSupportedNodeTargets()];
95-
// Server application code
97+
9698
bundlerContexts.push(
9799
new BundlerContext(
98100
workspaceRoot,
@@ -101,6 +103,17 @@ export function setupBundlerContexts(
101103
),
102104
);
103105

106+
if (outputMode && ssrOptions?.entry) {
107+
// New behavior introduced: 'server.ts' is now bundled separately from 'main.server.ts'.
108+
bundlerContexts.push(
109+
new BundlerContext(
110+
workspaceRoot,
111+
watch,
112+
createSsrEntryCodeBundleOptions(options, nodeTargets, codeBundleCache),
113+
),
114+
);
115+
}
116+
104117
// Server polyfills code
105118
const serverPolyfillBundleOptions = createServerPolyfillBundleOptions(
106119
options,

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,17 @@ export async function* serveWithVite(
9393
// This is so instead of prerendering all the routes for every change, the page is "prerendered" when it is requested.
9494
browserOptions.prerender = false;
9595

96-
// Avoid bundling and processing the ssr entry-point as this is not used by the dev-server.
97-
browserOptions.ssr = true;
98-
9996
// https://nodejs.org/api/process.html#processsetsourcemapsenabledval
10097
process.setSourceMapsEnabled(true);
10198
}
10299

103100
// Set all packages as external to support Vite's prebundle caching
104101
browserOptions.externalPackages = serverOptions.prebundle;
105102

103+
// Disable generating a full manifest with routes.
104+
// This is done during runtime when using the dev-server.
105+
browserOptions.disableFullServerManifestGeneration = true;
106+
106107
// The development server currently only supports a single locale when localizing.
107108
// This matches the behavior of the Webpack-based development server but could be expanded in the future.
108109
if (

0 commit comments

Comments
 (0)