Skip to content

Commit 99c9f1a

Browse files
committed
fix(@angular/build): handle external @angular/ packages during SSR
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
1 parent e126bf9 commit 99c9f1a

File tree

3 files changed

+83
-21
lines changed

3 files changed

+83
-21
lines changed

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

Lines changed: 26 additions & 20 deletions
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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ export async function load(url: string, context: { format?: string | null }, nex
133133
// need linking are ESM only.
134134
if (format === 'module' && isFileProtocol(url)) {
135135
const filePath = fileURLToPath(url);
136-
const source = await javascriptTransformer.transformFile(filePath);
136+
let source = await javascriptTransformer.transformFile(filePath);
137+
138+
if (filePath.includes('@angular/')) {
139+
source = prependNgServerMode(source);
140+
}
137141

138142
return {
139143
format,
@@ -154,6 +158,26 @@ function handleProcessExit(): void {
154158
void javascriptTransformer.close();
155159
}
156160

161+
/**
162+
* @note For some unknown reason, setting `globalThis.ngServerMode = true` does not work when using ESM loader hooks.
163+
*/
164+
const NG_SERVER_MODE_INIT_BYTES = new TextEncoder().encode('var ngServerMode=true;');
165+
166+
/**
167+
* Prepend the hardcoded string 'var ngServerMode=true;' to a Uint8Array.
168+
*
169+
* @param inputArray - The original Uint8Array to which the hardcoded string will be prepended.
170+
* @returns A new Uint8Array containing the hardcoded string followed by the input Uint8Array content.
171+
*/
172+
function prependNgServerMode(inputArray: Uint8Array): Uint8Array {
173+
const combinedLength = NG_SERVER_MODE_INIT_BYTES.length + inputArray.length;
174+
const combinedArray = new Uint8Array(combinedLength);
175+
combinedArray.set(NG_SERVER_MODE_INIT_BYTES, 0);
176+
combinedArray.set(inputArray, NG_SERVER_MODE_INIT_BYTES.length);
177+
178+
return combinedArray;
179+
}
180+
157181
process.once('exit', handleProcessExit);
158182
process.once('SIGINT', handleProcessExit);
159183
process.once('uncaughtException', handleProcessExit);
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)