Skip to content

Commit 1047b86

Browse files
committed
fix(@angular/build): handle external @angular/ packages during SSR (#29094)
This commit introduces `ngServerMode` to ensure proper handling of external `@angular/` packages when they are used as externals during server-side rendering (SSR). Closes: #29092 (cherry picked from commit d811a7f)
1 parent fc553c1 commit 1047b86

File tree

3 files changed

+69
-21
lines changed

3 files changed

+69
-21
lines changed

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

+26-20
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ export function createServerPolyfillBundleOptions(
200200
return;
201201
}
202202

203+
const jsBanner: string[] = [`globalThis['ngServerMode'] = true;`];
204+
if (isNodePlatform) {
205+
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
206+
// See: https://github.com/evanw/esbuild/issues/1921.
207+
jsBanner.push(
208+
`import { createRequire } from 'node:module';`,
209+
`globalThis['require'] ??= createRequire(import.meta.url);`,
210+
);
211+
}
212+
203213
const buildOptions: BuildOptions = {
204214
...polyfillBundleOptions,
205215
platform: isNodePlatform ? 'node' : 'neutral',
@@ -210,16 +220,9 @@ export function createServerPolyfillBundleOptions(
210220
// More details: https://github.com/angular/angular-cli/issues/25405.
211221
mainFields: ['es2020', 'es2015', 'module', 'main'],
212222
entryNames: '[name]',
213-
banner: isNodePlatform
214-
? {
215-
js: [
216-
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
217-
// See: https://github.com/evanw/esbuild/issues/1921.
218-
`import { createRequire } from 'node:module';`,
219-
`globalThis['require'] ??= createRequire(import.meta.url);`,
220-
].join('\n'),
221-
}
222-
: undefined,
223+
banner: {
224+
js: jsBanner.join('\n'),
225+
},
223226
target,
224227
entryPoints: {
225228
'polyfills.server': namespace,
@@ -391,19 +394,22 @@ export function createSsrEntryCodeBundleOptions(
391394
const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest';
392395
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;
393396

397+
const jsBanner: string[] = [`globalThis['ngServerMode'] = true;`];
398+
if (isNodePlatform) {
399+
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
400+
// See: https://github.com/evanw/esbuild/issues/1921.
401+
jsBanner.push(
402+
`import { createRequire } from 'node:module';`,
403+
`globalThis['require'] ??= createRequire(import.meta.url);`,
404+
);
405+
}
406+
394407
const buildOptions: BuildOptions = {
395408
...getEsBuildServerCommonOptions(options),
396409
target,
397-
banner: isNodePlatform
398-
? {
399-
js: [
400-
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
401-
// See: https://github.com/evanw/esbuild/issues/1921.
402-
`import { createRequire } from 'node:module';`,
403-
`globalThis['require'] ??= createRequire(import.meta.url);`,
404-
].join('\n'),
405-
}
406-
: undefined,
410+
banner: {
411+
js: jsBanner.join('\n'),
412+
},
407413
entryPoints: {
408414
'server': ssrEntryNamespace,
409415
},

packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import { pathToFileURL } from 'node:url';
1313
import { fileURLToPath } from 'url';
1414
import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer';
1515

16+
/**
17+
* @note For some unknown reason, setting `globalThis.ngServerMode = true` does not work when using ESM loader hooks.
18+
*/
19+
const NG_SERVER_MODE_INIT_BYTES = new TextEncoder().encode('var ngServerMode=true;');
20+
1621
/**
1722
* Node.js ESM loader to redirect imports to in memory files.
1823
* @see: https://nodejs.org/api/esm.html#loaders for more information about loaders.
@@ -133,7 +138,12 @@ export async function load(url: string, context: { format?: string | null }, nex
133138
// need linking are ESM only.
134139
if (format === 'module' && isFileProtocol(url)) {
135140
const filePath = fileURLToPath(url);
136-
const source = await javascriptTransformer.transformFile(filePath);
141+
let source = await javascriptTransformer.transformFile(filePath);
142+
143+
if (filePath.includes('@angular/')) {
144+
// Prepend 'var ngServerMode=true;' to the source.
145+
source = Buffer.concat([NG_SERVER_MODE_INIT_BYTES, source]);
146+
}
137147

138148
return {
139149
format,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from 'node:assert';
2+
import { ng } from '../../../utils/process';
3+
import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
4+
import { updateJsonFile, useSha } from '../../../utils/project';
5+
import { getGlobalVariable } from '../../../utils/env';
6+
7+
export default async function () {
8+
assert(
9+
getGlobalVariable('argv')['esbuild'],
10+
'This test should not be called in the Webpack suite.',
11+
);
12+
13+
// Forcibly remove in case another test doesn't clean itself up.
14+
await uninstallPackage('@angular/ssr');
15+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
16+
await useSha();
17+
await installWorkspacePackages();
18+
19+
await updateJsonFile('angular.json', (json) => {
20+
const build = json['projects']['test-project']['architect']['build'];
21+
build.options.externalDependencies = [
22+
'@angular/platform-browser',
23+
'@angular/core',
24+
'@angular/router',
25+
'@angular/common',
26+
'@angular/common/http',
27+
'@angular/platform-browser/animations',
28+
];
29+
});
30+
31+
await ng('build');
32+
}

0 commit comments

Comments
 (0)