Skip to content

Commit c41529c

Browse files
committed
fix(@angular/build): handle APP_BASE_HREF correctly in prerendered routes
This commit resolves path stripping issues when `APP_BASE_HREF` does not align with the expected value. Closes #28775 (cherry picked from commit a1fa483)
1 parent 47bbaca commit c41529c

File tree

2 files changed

+117
-6
lines changed

2 files changed

+117
-6
lines changed

packages/angular/build/src/utils/server-rendering/prerender.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,11 @@ async function renderPages(
219219
const baseHrefWithLeadingSlash = addLeadingSlash(baseHref);
220220

221221
for (const { route, redirectTo, renderMode } of serializableRouteTreeNode) {
222-
// Remove base href from file output path.
223-
const routeWithoutBaseHref = addLeadingSlash(
224-
route.slice(baseHrefWithLeadingSlash.length - 1),
225-
);
222+
// Remove the base href from the file output path.
223+
const routeWithoutBaseHref = addTrailingSlash(route).startsWith(baseHrefWithLeadingSlash)
224+
? addLeadingSlash(route.slice(baseHrefWithLeadingSlash.length - 1))
225+
: route;
226+
226227
const outPath = posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html');
227228

228229
if (typeof redirectTo === 'string') {
@@ -336,11 +337,15 @@ async function getAllRoutes(
336337
}
337338

338339
function addLeadingSlash(value: string): string {
339-
return value.charAt(0) === '/' ? value : '/' + value;
340+
return value[0] === '/' ? value : '/' + value;
341+
}
342+
343+
function addTrailingSlash(url: string): string {
344+
return url[url.length - 1] === '/' ? url : `${url}/`;
340345
}
341346

342347
function removeLeadingSlash(value: string): string {
343-
return value.charAt(0) === '/' ? value.slice(1) : value;
348+
return value[0] === '/' ? value.slice(1) : value;
344349
}
345350

346351
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { join } from 'node:path';
2+
import { existsSync } from 'node:fs';
3+
import assert from 'node:assert';
4+
import { expectFileNotToExist, expectFileToMatch, writeFile } from '../../../utils/fs';
5+
import { ng, noSilentNg, silentNg } from '../../../utils/process';
6+
import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
7+
import { useSha } from '../../../utils/project';
8+
import { getGlobalVariable } from '../../../utils/env';
9+
import { langTranslations, setupI18nConfig } from '../../i18n/setup';
10+
11+
export default async function () {
12+
assert(
13+
getGlobalVariable('argv')['esbuild'],
14+
'This test should not be called in the Webpack suite.',
15+
);
16+
17+
// Setup project
18+
await setupI18nConfig();
19+
20+
// Forcibly remove in case another test doesn't clean itself up.
21+
await uninstallPackage('@angular/ssr');
22+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
23+
await useSha();
24+
await installWorkspacePackages();
25+
26+
// Add routes
27+
await writeFile(
28+
'src/app/app.routes.ts',
29+
`
30+
import { Routes } from '@angular/router';
31+
import { HomeComponent } from './home/home.component';
32+
import { SsgComponent } from './ssg/ssg.component';
33+
34+
export const routes: Routes = [
35+
{
36+
path: '',
37+
component: HomeComponent,
38+
},
39+
{
40+
path: 'ssg',
41+
component: SsgComponent,
42+
},
43+
{
44+
path: '**',
45+
component: HomeComponent,
46+
},
47+
];
48+
`,
49+
);
50+
51+
// Add server routing
52+
await writeFile(
53+
'src/app/app.routes.server.ts',
54+
`
55+
import { RenderMode, ServerRoute } from '@angular/ssr';
56+
57+
export const serverRoutes: ServerRoute[] = [
58+
{
59+
path: '**',
60+
renderMode: RenderMode.Prerender,
61+
},
62+
];
63+
`,
64+
);
65+
66+
await writeFile(
67+
'src/app/app.config.ts',
68+
`
69+
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
70+
import { provideRouter } from '@angular/router';
71+
72+
import { routes } from './app.routes';
73+
import { provideClientHydration } from '@angular/platform-browser';
74+
import { APP_BASE_HREF } from '@angular/common';
75+
76+
export const appConfig: ApplicationConfig = {
77+
providers: [
78+
provideZoneChangeDetection({ eventCoalescing: true }),
79+
provideRouter(routes),
80+
provideClientHydration(),
81+
{
82+
provide: APP_BASE_HREF,
83+
useValue: '/',
84+
},
85+
],
86+
};
87+
`,
88+
);
89+
90+
// Generate components for the above routes
91+
await silentNg('generate', 'component', 'home');
92+
await silentNg('generate', 'component', 'ssg');
93+
94+
await noSilentNg('build', '--output-mode=static');
95+
96+
for (const { lang, outputPath } of langTranslations) {
97+
await expectFileToMatch(join(outputPath, 'index.html'), `<p id="locale">${lang}</p>`);
98+
await expectFileToMatch(join(outputPath, 'ssg/index.html'), `<p id="locale">${lang}</p>`);
99+
}
100+
101+
// Check that server directory does not exist
102+
assert(
103+
!existsSync('dist/test-project/server'),
104+
'Server directory should not exist when output-mode is static',
105+
);
106+
}

0 commit comments

Comments
 (0)