diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index 3587ff99a618..c776d30ca629 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -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', @@ -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, @@ -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, }, diff --git a/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts index ca9e986bbb89..74a2df7636ec 100644 --- a/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts +++ b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts @@ -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. @@ -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, diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-external-dependencies.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-external-dependencies.ts new file mode 100644 index 000000000000..9d01f375a211 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-external-dependencies.ts @@ -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'); +}