Skip to content

Commit 21c94ad

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 21c94ad

File tree

4 files changed

+37
-14
lines changed

4 files changed

+37
-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: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin';
2828
import { SERVER_GENERATED_EXTERNALS, getFeatureSupport, isZonelessApp } from './utils';
2929
import { createVirtualModulePlugin } from './virtual-module-plugin';
3030
import { createWasmPlugin } from './wasm-plugin';
31+
import { ExperimentalPlatform } from '../../builders/application/schema';
3132

3233
export function createBrowserCodeBundleOptions(
3334
options: NormalizedApplicationBuildOptions,
@@ -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,
@@ -373,6 +378,13 @@ export function createSsrEntryCodeBundleOptions(
373378
const ssrEntryNamespace = 'angular:ssr-entry';
374379
const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest';
375380
const ssrInjectRequireNamespace = 'angular:ssr-entry-inject-require';
381+
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
382+
383+
const inject: string[] = [ssrInjectManifestNamespace];
384+
if (isNodePlatform) {
385+
inject.unshift(ssrInjectRequireNamespace);
386+
}
387+
376388
const buildOptions: BuildOptions = {
377389
...getEsBuildServerCommonOptions(options),
378390
target,
@@ -390,7 +402,7 @@ export function createSsrEntryCodeBundleOptions(
390402
styleOptions,
391403
),
392404
],
393-
inject: [ssrInjectRequireNamespace, ssrInjectManifestNamespace],
405+
inject,
394406
};
395407

396408
buildOptions.plugins ??= [];
@@ -490,9 +502,11 @@ export function createSsrEntryCodeBundleOptions(
490502
}
491503

492504
function getEsBuildServerCommonOptions(options: NormalizedApplicationBuildOptions): BuildOptions {
505+
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
506+
493507
return {
494508
...getEsBuildCommonOptions(options),
495-
platform: 'node',
509+
platform: isNodePlatform ? 'node' : 'neutral',
496510
outExtension: { '.js': '.mjs' },
497511
// Note: `es2015` is needed for RxJS v6. If not specified, `module` would
498512
// 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)