Skip to content

Commit 21f21ed

Browse files
committedDec 3, 2024·
fix(@angular/build): ensure correct handling of index.output for SSR
Previously, the index file was not being renamed correctly when using server-side rendering (SSR). Closes: #29012 (cherry picked from commit 97897b7)
1 parent be21acc commit 21f21ed

File tree

3 files changed

+125
-87
lines changed

3 files changed

+125
-87
lines changed
 

‎packages/angular/build/src/builders/application/options.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -327,24 +327,26 @@ export async function normalizeOptions(
327327
let indexOutput: string;
328328
// The output file will be created within the configured output path
329329
if (typeof options.index === 'string') {
330-
/**
331-
* If SSR is activated, create a distinct entry file for the `index.html`.
332-
* This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file
333-
* if it exists (handling SSG).
334-
*
335-
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
336-
*
337-
* This approach can also be applied to service workers, where the `index.csr.html` is served instead of the prerendered `index.html`.
338-
*/
339-
const indexBaseName = path.basename(options.index);
340-
indexOutput =
341-
(ssrOptions || prerenderOptions) && indexBaseName === 'index.html'
342-
? INDEX_HTML_CSR
343-
: indexBaseName;
330+
indexOutput = options.index;
344331
} else {
345332
indexOutput = options.index.output || 'index.html';
346333
}
347334

335+
/**
336+
* If SSR is activated, create a distinct entry file for the `index.html`.
337+
* This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file
338+
* if it exists (handling SSG).
339+
*
340+
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
341+
*
342+
* This approach can also be applied to service workers, where the `index.csr.html` is served instead of the prerendered `index.html`.
343+
*/
344+
const indexBaseName = path.basename(indexOutput);
345+
indexOutput =
346+
(ssrOptions || prerenderOptions) && indexBaseName === 'index.html'
347+
? INDEX_HTML_CSR
348+
: indexBaseName;
349+
348350
indexHtmlOptions = {
349351
input: path.join(
350352
workspaceRoot,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { buildApplication } from '../../index';
10+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
11+
12+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
13+
describe('Behavior: "index.csr.html"', () => {
14+
beforeEach(async () => {
15+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
16+
const tsConfig = JSON.parse(content);
17+
tsConfig.files ??= [];
18+
tsConfig.files.push('main.server.ts');
19+
20+
return JSON.stringify(tsConfig);
21+
});
22+
});
23+
24+
it(`should generate 'index.csr.html' instead of 'index.html' when ssr is enabled.`, async () => {
25+
harness.useTarget('build', {
26+
...BASE_OPTIONS,
27+
server: 'src/main.server.ts',
28+
ssr: true,
29+
});
30+
31+
const { result } = await harness.executeOnce();
32+
expect(result?.success).toBeTrue();
33+
harness.expectDirectory('dist/server').toExist();
34+
harness.expectFile('dist/browser/index.csr.html').toExist();
35+
harness.expectFile('dist/browser/index.html').toNotExist();
36+
});
37+
38+
it(`should generate 'index.csr.html' instead of 'index.html' when 'output' is 'index.html' and ssr is enabled.`, async () => {
39+
harness.useTarget('build', {
40+
...BASE_OPTIONS,
41+
index: {
42+
input: 'src/index.html',
43+
output: 'index.html',
44+
},
45+
server: 'src/main.server.ts',
46+
ssr: true,
47+
});
48+
49+
const { result } = await harness.executeOnce();
50+
expect(result?.success).toBeTrue();
51+
52+
harness.expectDirectory('dist/server').toExist();
53+
harness.expectFile('dist/browser/index.csr.html').toExist();
54+
harness.expectFile('dist/browser/index.html').toNotExist();
55+
});
56+
});
57+
});

‎packages/angular/build/src/builders/application/tests/options/index_spec.ts

+52-73
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setu
1212
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
1313
describe('Option: "index"', () => {
1414
beforeEach(async () => {
15-
// Application code is not needed for index tests
1615
await harness.writeFile('src/main.ts', 'console.log("TEST");');
1716
});
1817

@@ -140,92 +139,72 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
140139
});
141140
});
142141

143-
it('should generate initial preload link elements when preloadInitial is true', async () => {
144-
harness.useTarget('build', {
145-
...BASE_OPTIONS,
146-
index: {
147-
input: 'src/index.html',
148-
preloadInitial: true,
149-
},
150-
});
151-
152-
// Setup an initial chunk usage for JS
153-
await harness.writeFile('src/a.ts', 'console.log("TEST");');
154-
await harness.writeFile('src/b.ts', 'import "./a";');
155-
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
142+
describe('preload', () => {
143+
it('should generate initial preload link elements when preloadInitial is true', async () => {
144+
harness.useTarget('build', {
145+
...BASE_OPTIONS,
146+
index: {
147+
input: 'src/index.html',
148+
preloadInitial: true,
149+
},
150+
});
156151

157-
const { result } = await harness.executeOnce();
152+
// Setup an initial chunk usage for JS
153+
await harness.writeFile('src/a.ts', 'console.log("TEST");');
154+
await harness.writeFile('src/b.ts', 'import "./a";');
155+
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
158156

159-
expect(result?.success).toBe(true);
160-
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
161-
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
162-
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
163-
});
157+
const { result } = await harness.executeOnce();
164158

165-
it('should generate initial preload link elements when preloadInitial is undefined', async () => {
166-
harness.useTarget('build', {
167-
...BASE_OPTIONS,
168-
index: {
169-
input: 'src/index.html',
170-
preloadInitial: undefined,
171-
},
159+
expect(result?.success).toBe(true);
160+
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
161+
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
162+
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
172163
});
173164

174-
// Setup an initial chunk usage for JS
175-
await harness.writeFile('src/a.ts', 'console.log("TEST");');
176-
await harness.writeFile('src/b.ts', 'import "./a";');
177-
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
165+
it('should generate initial preload link elements when preloadInitial is undefined', async () => {
166+
harness.useTarget('build', {
167+
...BASE_OPTIONS,
168+
index: {
169+
input: 'src/index.html',
170+
preloadInitial: undefined,
171+
},
172+
});
178173

179-
const { result } = await harness.executeOnce();
174+
// Setup an initial chunk usage for JS
175+
await harness.writeFile('src/a.ts', 'console.log("TEST");');
176+
await harness.writeFile('src/b.ts', 'import "./a";');
177+
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
180178

181-
expect(result?.success).toBe(true);
182-
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
183-
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
184-
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
185-
});
179+
const { result } = await harness.executeOnce();
186180

187-
it('should not generate initial preload link elements when preloadInitial is false', async () => {
188-
harness.useTarget('build', {
189-
...BASE_OPTIONS,
190-
index: {
191-
input: 'src/index.html',
192-
preloadInitial: false,
193-
},
181+
expect(result?.success).toBe(true);
182+
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
183+
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
184+
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
194185
});
195186

196-
// Setup an initial chunk usage for JS
197-
await harness.writeFile('src/a.ts', 'console.log("TEST");');
198-
await harness.writeFile('src/b.ts', 'import "./a";');
199-
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
200-
201-
const { result } = await harness.executeOnce();
202-
203-
expect(result?.success).toBe(true);
204-
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
205-
harness.expectFile('dist/browser/index.html').content.not.toContain('modulepreload');
206-
harness.expectFile('dist/browser/index.html').content.not.toContain('chunk-');
207-
});
187+
it('should not generate initial preload link elements when preloadInitial is false', async () => {
188+
harness.useTarget('build', {
189+
...BASE_OPTIONS,
190+
index: {
191+
input: 'src/index.html',
192+
preloadInitial: false,
193+
},
194+
});
208195

209-
it(`should generate 'index.csr.html' instead of 'index.html' by default when ssr is enabled.`, async () => {
210-
await harness.modifyFile('src/tsconfig.app.json', (content) => {
211-
const tsConfig = JSON.parse(content);
212-
tsConfig.files ??= [];
213-
tsConfig.files.push('main.server.ts');
196+
// Setup an initial chunk usage for JS
197+
await harness.writeFile('src/a.ts', 'console.log("TEST");');
198+
await harness.writeFile('src/b.ts', 'import "./a";');
199+
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
214200

215-
return JSON.stringify(tsConfig);
216-
});
201+
const { result } = await harness.executeOnce();
217202

218-
harness.useTarget('build', {
219-
...BASE_OPTIONS,
220-
server: 'src/main.server.ts',
221-
ssr: true,
203+
expect(result?.success).toBe(true);
204+
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
205+
harness.expectFile('dist/browser/index.html').content.not.toContain('modulepreload');
206+
harness.expectFile('dist/browser/index.html').content.not.toContain('chunk-');
222207
});
223-
224-
const { result } = await harness.executeOnce();
225-
expect(result?.success).toBeTrue();
226-
harness.expectDirectory('dist/server').toExist();
227-
harness.expectFile('dist/browser/index.csr.html').toExist();
228-
harness.expectFile('dist/browser/index.html').toNotExist();
229208
});
230209
});
231210
});

0 commit comments

Comments
 (0)
Please sign in to comment.