Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(@angular/build): ensure correct handling of index.output for SSR #29024

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
});
Loading