Skip to content

Commit

Permalink
fix(@angular/build): ensure correct handling of index.output for SSR
Browse files Browse the repository at this point in the history
Previously, the index file was not being renamed correctly when using server-side rendering (SSR).

Closes: #29012
(cherry picked from commit 97897b7)
  • Loading branch information
alan-agius4 committed Dec 3, 2024
1 parent be21acc commit 21f21ed
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 87 deletions.
30 changes: 16 additions & 14 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,24 +327,26 @@ export async function normalizeOptions(
let indexOutput: string;
// The output file will be created within the configured output path
if (typeof options.index === 'string') {
/**
* If SSR is activated, create a distinct entry file for the `index.html`.
* This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file
* if it exists (handling SSG).
*
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
*
* This approach can also be applied to service workers, where the `index.csr.html` is served instead of the prerendered `index.html`.
*/
const indexBaseName = path.basename(options.index);
indexOutput =
(ssrOptions || prerenderOptions) && indexBaseName === 'index.html'
? INDEX_HTML_CSR
: indexBaseName;
indexOutput = options.index;
} else {
indexOutput = options.index.output || 'index.html';
}

/**
* If SSR is activated, create a distinct entry file for the `index.html`.
* This is necessary because numerous server/cloud providers automatically serve the `index.html` as a static file
* if it exists (handling SSG).
*
* For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server.
*
* This approach can also be applied to service workers, where the `index.csr.html` is served instead of the prerendered `index.html`.
*/
const indexBaseName = path.basename(indexOutput);
indexOutput =
(ssrOptions || prerenderOptions) && indexBaseName === 'index.html'
? INDEX_HTML_CSR
: indexBaseName;

indexHtmlOptions = {
input: path.join(
workspaceRoot,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { buildApplication } from '../../index';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Behavior: "index.csr.html"', () => {
beforeEach(async () => {
await harness.modifyFile('src/tsconfig.app.json', (content) => {
const tsConfig = JSON.parse(content);
tsConfig.files ??= [];
tsConfig.files.push('main.server.ts');

return JSON.stringify(tsConfig);
});
});

it(`should generate 'index.csr.html' instead of 'index.html' when ssr is enabled.`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
ssr: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectDirectory('dist/server').toExist();
harness.expectFile('dist/browser/index.csr.html').toExist();
harness.expectFile('dist/browser/index.html').toNotExist();
});

it(`should generate 'index.csr.html' instead of 'index.html' when 'output' is 'index.html' and ssr is enabled.`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
output: 'index.html',
},
server: 'src/main.server.ts',
ssr: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();

harness.expectDirectory('dist/server').toExist();
harness.expectFile('dist/browser/index.csr.html').toExist();
harness.expectFile('dist/browser/index.html').toNotExist();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setu
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Option: "index"', () => {
beforeEach(async () => {
// Application code is not needed for index tests
await harness.writeFile('src/main.ts', 'console.log("TEST");');
});

Expand Down Expand Up @@ -140,92 +139,72 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
});
});

it('should generate initial preload link elements when preloadInitial is true', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
preloadInitial: true,
},
});

// Setup an initial chunk usage for JS
await harness.writeFile('src/a.ts', 'console.log("TEST");');
await harness.writeFile('src/b.ts', 'import "./a";');
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
describe('preload', () => {
it('should generate initial preload link elements when preloadInitial is true', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
preloadInitial: true,
},
});

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

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
});
const { result } = await harness.executeOnce();

it('should generate initial preload link elements when preloadInitial is undefined', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
preloadInitial: undefined,
},
expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
});

// Setup an initial chunk usage for JS
await harness.writeFile('src/a.ts', 'console.log("TEST");');
await harness.writeFile('src/b.ts', 'import "./a";');
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
it('should generate initial preload link elements when preloadInitial is undefined', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
preloadInitial: undefined,
},
});

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

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
});
const { result } = await harness.executeOnce();

it('should not generate initial preload link elements when preloadInitial is false', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
preloadInitial: false,
},
expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
});

// Setup an initial chunk usage for JS
await harness.writeFile('src/a.ts', 'console.log("TEST");');
await harness.writeFile('src/b.ts', 'import "./a";');
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
harness.expectFile('dist/browser/index.html').content.not.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.not.toContain('chunk-');
});
it('should not generate initial preload link elements when preloadInitial is false', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
preloadInitial: false,
},
});

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

return JSON.stringify(tsConfig);
});
const { result } = await harness.executeOnce();

harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
ssr: true,
expect(result?.success).toBe(true);
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
harness.expectFile('dist/browser/index.html').content.not.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.not.toContain('chunk-');
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectDirectory('dist/server').toExist();
harness.expectFile('dist/browser/index.csr.html').toExist();
harness.expectFile('dist/browser/index.html').toNotExist();
});
});
});

0 comments on commit 21f21ed

Please sign in to comment.