Skip to content

Commit

Permalink
fix(@angular/build): handle external @angular/ packages during SSR (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
alan-agius4 authored Dec 9, 2024
1 parent 28bdbeb commit d811a7f
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 21 deletions.
46 changes: 26 additions & 20 deletions packages/angular/build/src/tools/esbuild/application-code-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,16 @@ export function createServerPolyfillBundleOptions(
return;
}

const jsBanner: string[] = [`globalThis['ngServerMode'] = true;`];
if (isNodePlatform) {
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
// See: https://github.com/evanw/esbuild/issues/1921.
jsBanner.push(
`import { createRequire } from 'node:module';`,
`globalThis['require'] ??= createRequire(import.meta.url);`,
);
}

const buildOptions: BuildOptions = {
...polyfillBundleOptions,
platform: isNodePlatform ? 'node' : 'neutral',
Expand All @@ -210,16 +220,9 @@ export function createServerPolyfillBundleOptions(
// More details: https://github.com/angular/angular-cli/issues/25405.
mainFields: ['es2020', 'es2015', 'module', 'main'],
entryNames: '[name]',
banner: isNodePlatform
? {
js: [
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
// See: https://github.com/evanw/esbuild/issues/1921.
`import { createRequire } from 'node:module';`,
`globalThis['require'] ??= createRequire(import.meta.url);`,
].join('\n'),
}
: undefined,
banner: {
js: jsBanner.join('\n'),
},
target,
entryPoints: {
'polyfills.server': namespace,
Expand Down Expand Up @@ -391,19 +394,22 @@ export function createSsrEntryCodeBundleOptions(
const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest';
const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral;

const jsBanner: string[] = [`globalThis['ngServerMode'] = true;`];
if (isNodePlatform) {
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
// See: https://github.com/evanw/esbuild/issues/1921.
jsBanner.push(
`import { createRequire } from 'node:module';`,
`globalThis['require'] ??= createRequire(import.meta.url);`,
);
}

const buildOptions: BuildOptions = {
...getEsBuildServerCommonOptions(options),
target,
banner: isNodePlatform
? {
js: [
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
// See: https://github.com/evanw/esbuild/issues/1921.
`import { createRequire } from 'node:module';`,
`globalThis['require'] ??= createRequire(import.meta.url);`,
].join('\n'),
}
: undefined,
banner: {
js: jsBanner.join('\n'),
},
entryPoints: {
'server': ssrEntryNamespace,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { pathToFileURL } from 'node:url';
import { fileURLToPath } from 'url';
import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer';

/**
* @note For some unknown reason, setting `globalThis.ngServerMode = true` does not work when using ESM loader hooks.
*/
const NG_SERVER_MODE_INIT_BYTES = new TextEncoder().encode('var ngServerMode=true;');

/**
* Node.js ESM loader to redirect imports to in memory files.
* @see: https://nodejs.org/api/esm.html#loaders for more information about loaders.
Expand Down Expand Up @@ -133,7 +138,12 @@ export async function load(url: string, context: { format?: string | null }, nex
// need linking are ESM only.
if (format === 'module' && isFileProtocol(url)) {
const filePath = fileURLToPath(url);
const source = await javascriptTransformer.transformFile(filePath);
let source = await javascriptTransformer.transformFile(filePath);

if (filePath.includes('@angular/')) {
// Prepend 'var ngServerMode=true;' to the source.
source = Buffer.concat([NG_SERVER_MODE_INIT_BYTES, source]);
}

return {
format,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from 'node:assert';
import { ng } from '../../../utils/process';
import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
import { updateJsonFile, useSha } from '../../../utils/project';
import { getGlobalVariable } from '../../../utils/env';

export default async function () {
assert(
getGlobalVariable('argv')['esbuild'],
'This test should not be called in the Webpack suite.',
);

// Forcibly remove in case another test doesn't clean itself up.
await uninstallPackage('@angular/ssr');
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
await useSha();
await installWorkspacePackages();

await updateJsonFile('angular.json', (json) => {
const build = json['projects']['test-project']['architect']['build'];
build.options.externalDependencies = [
'@angular/platform-browser',
'@angular/core',
'@angular/router',
'@angular/common',
'@angular/common/http',
'@angular/platform-browser/animations',
];
});

await ng('build');
}

0 comments on commit d811a7f

Please sign in to comment.