Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): allowing control of index HTML i…
Browse files Browse the repository at this point in the history
…nitial preload generation

The long-form variant of the `index` option for the `application` builder now supports
an addition sub-option named `preloadInitial`. This new option is a boolean option that controls
the generation of initial preload related link elements in the generated index HTML file
for the application. Preload related link elements include `preload`, `modulepreload`,
and `preconnect` link rels for initial JavaScript and stylesheet application files.
  • Loading branch information
clydin committed Dec 18, 2023
1 parent f7d5389 commit 15a669c
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ export async function normalizeOptions(
styles: options.styles ?? [],
}),
transformer: extensions?.indexHtmlTransformer,
// Preload initial defaults to true
preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,11 @@
"minLength": 1,
"default": "index.html",
"description": "The output path of the application's generated HTML index file. The full provided path will be used and will be considered relative to the application's configured output path."
},
"preloadInitial": {
"type": "boolean",
"default": true,
"description": "Generates 'preload', `modulepreload', and 'preconnect' link elements for initial application files and resources."
}
},
"required": ["input"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* @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.io/license
*/

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

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");');
});

describe('short form syntax', () => {
it('should not generate an output file when false', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: false,
});

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

expect(result?.success).toBe(true);

harness.expectFile('dist/browser/index.html').toNotExist();
});

// TODO: This fails option validation when used in the CLI but not when used directly
xit('should fail build when true', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: true,
});

const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });

expect(result?.success).toBe(false);
harness.expectFile('dist/browser/index.html').toNotExist();
expect(logs).toContain(
jasmine.objectContaining({ message: jasmine.stringMatching('Schema validation failed') }),
);
});

it('should use the provided file path to generate the output file when a string path', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: 'src/index.html',
});

await harness.writeFile(
'src/index.html',
'<html><head><title>TEST_123</title></head><body></body>',
);

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

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/index.html').content.toContain('TEST_123');
});

// TODO: Build needs to be fixed to not throw an unhandled exception for this case
xit('should fail build when a string path to non-existent file', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: 'src/not-here.html',
});

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

expect(result?.success).toBe(false);
harness.expectFile('dist/browser/index.html').toNotExist();
});

it('should generate initial preload link elements', 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"))();');

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.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
});
});

describe('long form syntax', () => {
it('should use the provided input path to generate the output file when present', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
},
});

await harness.writeFile(
'src/index.html',
'<html><head><title>TEST_123</title></head><body></body>',
);

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

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/index.html').content.toContain('TEST_123');
});

it('should use the provided output path to generate the output file when present', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
index: {
input: 'src/index.html',
output: 'output.html',
},
});

await harness.writeFile(
'src/index.html',
'<html><head><title>TEST_123</title></head><body></body>',
);

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

expect(result?.success).toBe(true);
harness.expectFile('dist/browser/output.html').content.toContain('TEST_123');
});
});

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"))();');

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.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
});

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

// 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.toContain('modulepreload');
harness.expectFile('dist/browser/index.html').content.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,
},
});

// 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-');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function generateIndexHtml(

assert(indexHtmlOptions, 'indexHtmlOptions cannot be undefined.');

if (!externalPackages) {
if (!externalPackages && indexHtmlOptions.preloadInitial) {
for (const [key, value] of initialFiles) {
if (value.entrypoint) {
// Entry points are already referenced in the HTML
Expand Down

0 comments on commit 15a669c

Please sign in to comment.