feat(@angular-devkit/build-angular): allowing control of index HTML initial 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.
This commit is contained in:
Charles Lyding 2023-12-14 14:55:09 -05:00 committed by Charles
parent f7d538903a
commit 15a669c1ef
4 changed files with 217 additions and 1 deletions

View File

@ -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),
};
}

View File

@ -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"]

View File

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

View File

@ -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