Skip to content

Commit e3ca169

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 86cb387 commit e3ca169

File tree

3 files changed

+158
-5
lines changed

3 files changed

+158
-5
lines changed

packages/angular/build/src/builders/application/execute-post-bundle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ export async function executePostBundleSteps(
206206
locale,
207207
);
208208

209+
manifest.contents = new TextEncoder().encode(manifestContent);
210+
209211
for (const chunk of serverAssetsChunks) {
210212
const idx = additionalOutputFiles.findIndex(({ path }) => path === chunk.path);
211213
if (idx === -1) {
@@ -214,8 +216,6 @@ export async function executePostBundleSteps(
214216
additionalOutputFiles[idx] = chunk;
215217
}
216218
}
217-
218-
manifest.contents = new TextEncoder().encode(manifestContent);
219219
}
220220
}
221221

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,10 @@ async function renderPages(
220220

221221
for (const { route, redirectTo, renderMode } of serializableRouteTreeNode) {
222222
// Remove base href from file output path.
223-
const routeWithoutBaseHref = addLeadingSlash(
224-
route.slice(baseHrefWithLeadingSlash.length - 1),
225-
);
223+
const routeWithoutBaseHref = 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') {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { join } from 'node:path';
2+
import { existsSync } from 'node:fs';
3+
import assert from 'node:assert';
4+
import {
5+
expectFileNotToExist,
6+
expectFileToMatch,
7+
replaceInFile,
8+
writeFile,
9+
} from '../../../utils/fs';
10+
import { ng, noSilentNg, silentNg } from '../../../utils/process';
11+
import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
12+
import { useSha } from '../../../utils/project';
13+
import { getGlobalVariable } from '../../../utils/env';
14+
import { expectToFail } from '../../../utils/utils';
15+
import { setupI18nConfig } from '../../i18n/setup';
16+
17+
export default async function () {
18+
assert(
19+
getGlobalVariable('argv')['esbuild'],
20+
'This test should not be called in the Webpack suite.',
21+
);
22+
23+
// Setup project
24+
await setupI18nConfig();
25+
26+
// Forcibly remove in case another test doesn't clean itself up.
27+
await uninstallPackage('@angular/ssr');
28+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
29+
await useSha();
30+
await installWorkspacePackages();
31+
32+
// Add routes
33+
await writeFile(
34+
'src/app/app.routes.ts',
35+
`
36+
import { Routes } from '@angular/router';
37+
import { HomeComponent } from './home/home.component';
38+
import { SsgComponent } from './ssg/ssg.component';
39+
import { SsgWithParamsComponent } from './ssg-with-params/ssg-with-params.component';
40+
41+
export const routes: Routes = [
42+
{
43+
path: '',
44+
component: HomeComponent,
45+
},
46+
{
47+
path: 'ssg',
48+
component: SsgComponent,
49+
},
50+
{
51+
path: 'ssg-redirect',
52+
redirectTo: 'ssg'
53+
},
54+
{
55+
path: 'ssg/:id',
56+
component: SsgWithParamsComponent,
57+
},
58+
{
59+
path: '**',
60+
component: HomeComponent,
61+
},
62+
];
63+
`,
64+
);
65+
66+
// Add server routing
67+
await writeFile(
68+
'src/app/app.routes.server.ts',
69+
`
70+
import { RenderMode, ServerRoute } from '@angular/ssr';
71+
72+
export const serverRoutes: ServerRoute[] = [
73+
{
74+
path: 'ssg/:id',
75+
renderMode: RenderMode.Prerender,
76+
getPrerenderParams: async() => [{id: 'one'}, {id: 'two'}],
77+
},
78+
{
79+
path: '**',
80+
renderMode: RenderMode.Server,
81+
},
82+
];
83+
`,
84+
);
85+
86+
await writeFile(
87+
'src/app/app.config.ts',
88+
`
89+
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
90+
import { provideRouter } from '@angular/router';
91+
92+
import { routes } from './app.routes';
93+
import { provideClientHydration } from '@angular/platform-browser';
94+
import { APP_BASE_HREF } from '@angular/common';
95+
96+
export const appConfig: ApplicationConfig = {
97+
providers: [
98+
provideZoneChangeDetection({ eventCoalescing: true }),
99+
provideRouter(routes),
100+
provideClientHydration(),
101+
{
102+
provide: APP_BASE_HREF,
103+
useValue: '/',
104+
},
105+
],
106+
};
107+
`,
108+
);
109+
110+
// Generate components for the above routes
111+
const componentNames: string[] = ['home', 'ssg', 'ssg-with-params'];
112+
for (const componentName of componentNames) {
113+
await silentNg('generate', 'component', componentName);
114+
}
115+
116+
// Should error as above we set `RenderMode.Server`
117+
const { message: errorMessage } = await expectToFail(() =>
118+
noSilentNg('build', '--output-mode=static'),
119+
);
120+
assert.match(
121+
errorMessage,
122+
new RegExp(
123+
`Route '/' is configured with server render mode, but the build 'outputMode' is set to 'static'.`,
124+
),
125+
);
126+
127+
// Fix the error
128+
await replaceInFile('src/app/app.routes.server.ts', 'RenderMode.Server', 'RenderMode.Prerender');
129+
await noSilentNg('build', '--output-mode=static');
130+
131+
const expects: Record<string, string> = {
132+
'index.html': 'home works!',
133+
'ssg/index.html': 'ssg works!',
134+
'ssg/one/index.html': 'ssg-with-params works!',
135+
'ssg/two/index.html': 'ssg-with-params works!',
136+
// When static redirects as generated as meta tags.
137+
'ssg-redirect/index.html': '<meta http-equiv="refresh" content="0; url=/ssg">',
138+
};
139+
140+
for (const [filePath, fileMatch] of Object.entries(expects)) {
141+
await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch);
142+
}
143+
144+
// Check that server directory does not exist
145+
assert(
146+
!existsSync('dist/test-project/server'),
147+
'Server directory should not exist when output-mode is static',
148+
);
149+
150+
// Should not prerender the catch all
151+
await expectFileNotToExist(join('dist/test-project/browser/**/index.html'));
152+
}

0 commit comments

Comments
 (0)