Skip to content

Commit bf03e80

Browse files
committed
feat(@angular/build): introduce ssr.experimentalPlatform option
This commit introduces a new option called `experimentalPlatform` to the Angular SSR configuration. The `experimentalPlatform` option allows developers to specify the target platform for the server bundle, enabling the generation of platform-neutral bundles suitable for deployment in environments like edge workers and other serverless platforms that do not rely on Node.js APIs. This change enhances the portability of Angular SSR applications and expands their deployment possibilities. **Note:** that this feature does not include polyfills for Node.js modules and is experimental, subject to future changes.
1 parent 6ce5c69 commit bf03e80

File tree

4 files changed

+52
-14
lines changed

4 files changed

+52
-14
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import { urlJoin } from '../../utils/url';
2828
import {
2929
Schema as ApplicationBuilderOptions,
30+
ExperimentalPlatform,
3031
I18NTranslation,
3132
OutputHashing,
3233
OutputMode,
@@ -264,10 +265,11 @@ export async function normalizeOptions(
264265
if (options.ssr === true) {
265266
ssrOptions = {};
266267
} else if (typeof options.ssr === 'object') {
267-
const { entry } = options.ssr;
268+
const { entry, experimentalPlatform = ExperimentalPlatform.Node } = options.ssr;
268269

269270
ssrOptions = {
270271
entry: entry && path.join(workspaceRoot, entry),
272+
platform: experimentalPlatform,
271273
};
272274
}
273275

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,11 @@
518518
"entry": {
519519
"type": "string",
520520
"description": "The server entry-point that when executed will spawn the web server."
521+
},
522+
"experimentalPlatform": {
523+
"description": "Specifies the platform for which the server bundle is generated. This affects the APIs and modules available in the server-side code. \n\n- `node`: (Default) Generates a bundle optimized for Node.js environments. \n- `neutral`: Generates a platform-neutral bundle suitable for environments like edge workers, and other serverless platforms. This option avoids using Node.js-specific APIs, making the bundle more portable. \n\nPlease note that this feature does not provide polyfills for Node.js modules. Additionally, it is experimental, and the schematics may undergo changes in future versions.",
524+
"default": "node",
525+
"enum": ["node", "neutral"]
521526
}
522527
},
523528
"additionalProperties": false

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import assert from 'node:assert';
1111
import { createHash } from 'node:crypto';
1212
import { extname, relative } from 'node:path';
1313
import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
14+
import { ExperimentalPlatform } from '../../builders/application/schema';
1415
import { allowMangle } from '../../utils/environment-options';
1516
import {
1617
SERVER_APP_ENGINE_MANIFEST_FILENAME,
@@ -160,8 +161,10 @@ export function createServerPolyfillBundleOptions(
160161
): BundlerOptionsFactory | undefined {
161162
const serverPolyfills: string[] = [];
162163
const polyfillsFromConfig = new Set(options.polyfills);
164+
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
165+
163166
if (!isZonelessApp(options.polyfills)) {
164-
serverPolyfills.push('zone.js/node');
167+
serverPolyfills.push(isNodePlatform ? 'zone.js/node' : 'zone.js');
165168
}
166169

167170
if (
@@ -190,22 +193,24 @@ export function createServerPolyfillBundleOptions(
190193

191194
const buildOptions: BuildOptions = {
192195
...polyfillBundleOptions,
193-
platform: 'node',
196+
platform: isNodePlatform ? 'node' : 'neutral',
194197
outExtension: { '.js': '.mjs' },
195198
// Note: `es2015` is needed for RxJS v6. If not specified, `module` would
196199
// match and the ES5 distribution would be bundled and ends up breaking at
197200
// runtime with the RxJS testing library.
198201
// More details: https://github.com/angular/angular-cli/issues/25405.
199202
mainFields: ['es2020', 'es2015', 'module', 'main'],
200203
entryNames: '[name]',
201-
banner: {
202-
js: [
203-
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
204-
// See: https://github.com/evanw/esbuild/issues/1921.
205-
`import { createRequire } from 'node:module';`,
206-
`globalThis['require'] ??= createRequire(import.meta.url);`,
207-
].join('\n'),
208-
},
204+
banner: isNodePlatform
205+
? {
206+
js: [
207+
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
208+
// See: https://github.com/evanw/esbuild/issues/1921.
209+
`import { createRequire } from 'node:module';`,
210+
`globalThis['require'] ??= createRequire(import.meta.url);`,
211+
].join('\n'),
212+
}
213+
: undefined,
209214
target,
210215
entryPoints: {
211216
'polyfills.server': namespace,
@@ -285,6 +290,14 @@ export function createServerMainCodeBundleOptions(
285290

286291
// Mark manifest and polyfills file as external as these are generated by a different bundle step.
287292
(buildOptions.external ??= []).push(...SERVER_GENERATED_EXTERNALS);
293+
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
294+
295+
if (!isNodePlatform) {
296+
// `@angular/platform-server` lazily depends on `xhr2` for XHR usage with the HTTP client.
297+
// Since `xhr2` has Node.js dependencies, it cannot be used when targeting non-Node.js platforms.
298+
// Note: The framework already issues a warning when using XHR with SSR.
299+
buildOptions.external.push('xhr2');
300+
}
288301

289302
buildOptions.plugins.push(
290303
createVirtualModulePlugin({
@@ -373,6 +386,13 @@ export function createSsrEntryCodeBundleOptions(
373386
const ssrEntryNamespace = 'angular:ssr-entry';
374387
const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest';
375388
const ssrInjectRequireNamespace = 'angular:ssr-entry-inject-require';
389+
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
390+
391+
const inject: string[] = [ssrInjectManifestNamespace];
392+
if (isNodePlatform) {
393+
inject.unshift(ssrInjectRequireNamespace);
394+
}
395+
376396
const buildOptions: BuildOptions = {
377397
...getEsBuildServerCommonOptions(options),
378398
target,
@@ -390,7 +410,7 @@ export function createSsrEntryCodeBundleOptions(
390410
styleOptions,
391411
),
392412
],
393-
inject: [ssrInjectRequireNamespace, ssrInjectManifestNamespace],
413+
inject,
394414
};
395415

396416
buildOptions.plugins ??= [];
@@ -404,6 +424,13 @@ export function createSsrEntryCodeBundleOptions(
404424
// Mark manifest file as external. As this will be generated later on.
405425
(buildOptions.external ??= []).push('*/main.server.mjs', ...SERVER_GENERATED_EXTERNALS);
406426

427+
if (!isNodePlatform) {
428+
// `@angular/platform-server` lazily depends on `xhr2` for XHR usage with the HTTP client.
429+
// Since `xhr2` has Node.js dependencies, it cannot be used when targeting non-Node.js platforms.
430+
// Note: The framework already issues a warning when using XHR with SSR.
431+
buildOptions.external.push('xhr2');
432+
}
433+
407434
buildOptions.plugins.push(
408435
{
409436
name: 'angular-ssr-metadata',
@@ -490,9 +517,11 @@ export function createSsrEntryCodeBundleOptions(
490517
}
491518

492519
function getEsBuildServerCommonOptions(options: NormalizedApplicationBuildOptions): BuildOptions {
520+
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
521+
493522
return {
494523
...getEsBuildCommonOptions(options),
495-
platform: 'node',
524+
platform: isNodePlatform ? 'node' : 'neutral',
496525
outExtension: { '.js': '.mjs' },
497526
// Note: `es2015` is needed for RxJS v6. If not specified, `module` would
498527
// match and the ES5 distribution would be bundled and ends up breaking at

packages/angular/build/src/tools/esbuild/bundler-context.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,9 @@ export class BundlerContext {
423423
}
424424

425425
get #platformIsServer(): boolean {
426-
return this.#esbuildOptions?.platform === 'node';
426+
return (
427+
this.#esbuildOptions?.platform === 'node' || this.#esbuildOptions?.platform === 'neutral'
428+
);
427429
}
428430

429431
/**

0 commit comments

Comments
 (0)