From 7968780a704bf10334eed1bac44c59f4da9125ef Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:44:21 +0000 Subject: [PATCH] fix(@angular/build): resolve assets correctly during i18n prerendering In i18n static output builds, requests for assets made during prerendering (e.g., via `HttpClient`) include the locale's `baseHref`. However, the in-memory asset mapping used by the patched server-side `fetch` did not account for this `baseHref`, causing assets to fail to resolve. Closes #32713 --- .../src/utils/server-rendering/prerender.ts | 4 +- ...utes-output-mode-static-i18n-http-calls.ts | 137 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n-http-calls.ts diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index 39d0f0934c92..d2f60744381a 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -96,7 +96,9 @@ export async function prerenderPages( const assetsReversed: Record** Destination */ string, /** Source */ string> = {}; for (const { source, destination } of assets) { - assetsReversed[addLeadingSlash(toPosixPath(destination))] = source; + // Assets are not stored with baseHref when using i18n, + // we append the base href so that requests are resolved correctly. + assetsReversed[joinUrlParts(baseHref, toPosixPath(destination))] = source; } // Get routes to prerender diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n-http-calls.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n-http-calls.ts new file mode 100644 index 000000000000..964c7827c23e --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n-http-calls.ts @@ -0,0 +1,137 @@ +import assert, { match } from 'node:assert'; +import { join } from 'node:path'; +import { readFile, writeMultipleFiles } from '../../../utils/fs'; +import { ng, noSilentNg, silentNg } from '../../../utils/process'; +import { getGlobalVariable } from '../../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; +import { langTranslations, setupI18nConfig } from '../../i18n/setup'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Setup project + await setupI18nConfig(); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + // Add asset + 'public/media.json': JSON.stringify({ dataFromAssets: true }), + // Update component to do an HTTP call to asset and API. + 'src/app/app.ts': ` + import { ChangeDetectorRef, Component, inject } from '@angular/core'; + import { JsonPipe } from '@angular/common'; + import { RouterOutlet } from '@angular/router'; + import { HttpClient } from '@angular/common/http'; + + @Component({ + selector: 'app-root', + imports: [JsonPipe, RouterOutlet], + template: \` +
{{ assetsData | json }}
+{{ apiData | json }}
+{[\S\s]*"dataFromAssets":[\s\S]*true[\S\s]*}<\/p>/); + match(contents, /
{[\S\s]*"dataFromAPI":[\s\S]*true[\S\s]*}<\/p>/);
+ match(contents, new RegExp(`