Skip to content

Commit d1caf8d

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
1 parent bcb24b9 commit d1caf8d

File tree

2 files changed

+121
-6
lines changed

2 files changed

+121
-6
lines changed

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

Lines changed: 11 additions & 6 deletions
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
/**
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
import { SsgWithParamsComponent } from './ssg-with-params/ssg-with-params.component';
34+
35+
export const routes: Routes = [
36+
{
37+
path: '',
38+
component: HomeComponent,
39+
},
40+
{
41+
path: 'ssg',
42+
component: SsgComponent,
43+
},
44+
{
45+
path: '**',
46+
component: HomeComponent,
47+
},
48+
];
49+
`,
50+
);
51+
52+
// Add server routing
53+
await writeFile(
54+
'src/app/app.routes.server.ts',
55+
`
56+
import { RenderMode, ServerRoute } from '@angular/ssr';
57+
58+
export const serverRoutes: ServerRoute[] = [
59+
{
60+
path: '**',
61+
renderMode: RenderMode.Prerender,
62+
},
63+
];
64+
`,
65+
);
66+
67+
await writeFile(
68+
'src/app/app.config.ts',
69+
`
70+
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
71+
import { provideRouter } from '@angular/router';
72+
73+
import { routes } from './app.routes';
74+
import { provideClientHydration } from '@angular/platform-browser';
75+
import { APP_BASE_HREF } from '@angular/common';
76+
77+
export const appConfig: ApplicationConfig = {
78+
providers: [
79+
provideZoneChangeDetection({ eventCoalescing: true }),
80+
provideRouter(routes),
81+
provideClientHydration(),
82+
{
83+
provide: APP_BASE_HREF,
84+
useValue: '/',
85+
},
86+
],
87+
};
88+
`,
89+
);
90+
91+
// Generate components for the above routes
92+
await silentNg('generate', 'component', 'home');
93+
await silentNg('generate', 'component', 'ssg');
94+
95+
await noSilentNg('build', '--output-mode=static');
96+
97+
for (const { lang, outputPath } of langTranslations) {
98+
await expectFileToMatch(join(outputPath, 'index.html'), `<p id="locale">${lang}</p>`);
99+
await expectFileToMatch(join(outputPath, 'ssg/index.html'), `<p id="locale">${lang}</p>`);
100+
}
101+
102+
// Check that server directory does not exist
103+
assert(
104+
!existsSync('dist/test-project/server'),
105+
'Server directory should not exist when output-mode is static',
106+
);
107+
108+
// Should not prerender the catch all
109+
await expectFileNotToExist(join('dist/test-project/browser/**/index.html'));
110+
}

0 commit comments

Comments
 (0)