Skip to content

Commit 15a669c

Browse files
committed
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.
1 parent f7d5389 commit 15a669c

File tree

4 files changed

+217
-1
lines changed

4 files changed

+217
-1
lines changed

packages/angular_devkit/build_angular/src/builders/application/options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ export async function normalizeOptions(
220220
styles: options.styles ?? [],
221221
}),
222222
transformer: extensions?.indexHtmlTransformer,
223+
// Preload initial defaults to true
224+
preloadInitial: typeof options.index !== 'object' || (options.index.preloadInitial ?? true),
223225
};
224226
}
225227

packages/angular_devkit/build_angular/src/builders/application/schema.json

+5
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@
416416
"minLength": 1,
417417
"default": "index.html",
418418
"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."
419+
},
420+
"preloadInitial": {
421+
"type": "boolean",
422+
"default": true,
423+
"description": "Generates 'preload', `modulepreload', and 'preconnect' link elements for initial application files and resources."
419424
}
420425
},
421426
"required": ["input"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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.io/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('Option: "index"', () => {
14+
beforeEach(async () => {
15+
// Application code is not needed for index tests
16+
await harness.writeFile('src/main.ts', 'console.log("TEST");');
17+
});
18+
19+
describe('short form syntax', () => {
20+
it('should not generate an output file when false', async () => {
21+
harness.useTarget('build', {
22+
...BASE_OPTIONS,
23+
index: false,
24+
});
25+
26+
const { result } = await harness.executeOnce();
27+
28+
expect(result?.success).toBe(true);
29+
30+
harness.expectFile('dist/browser/index.html').toNotExist();
31+
});
32+
33+
// TODO: This fails option validation when used in the CLI but not when used directly
34+
xit('should fail build when true', async () => {
35+
harness.useTarget('build', {
36+
...BASE_OPTIONS,
37+
index: true,
38+
});
39+
40+
const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
41+
42+
expect(result?.success).toBe(false);
43+
harness.expectFile('dist/browser/index.html').toNotExist();
44+
expect(logs).toContain(
45+
jasmine.objectContaining({ message: jasmine.stringMatching('Schema validation failed') }),
46+
);
47+
});
48+
49+
it('should use the provided file path to generate the output file when a string path', async () => {
50+
harness.useTarget('build', {
51+
...BASE_OPTIONS,
52+
index: 'src/index.html',
53+
});
54+
55+
await harness.writeFile(
56+
'src/index.html',
57+
'<html><head><title>TEST_123</title></head><body></body>',
58+
);
59+
60+
const { result } = await harness.executeOnce();
61+
62+
expect(result?.success).toBe(true);
63+
harness.expectFile('dist/browser/index.html').content.toContain('TEST_123');
64+
});
65+
66+
// TODO: Build needs to be fixed to not throw an unhandled exception for this case
67+
xit('should fail build when a string path to non-existent file', async () => {
68+
harness.useTarget('build', {
69+
...BASE_OPTIONS,
70+
index: 'src/not-here.html',
71+
});
72+
73+
const { result } = await harness.executeOnce({ outputLogsOnFailure: false });
74+
75+
expect(result?.success).toBe(false);
76+
harness.expectFile('dist/browser/index.html').toNotExist();
77+
});
78+
79+
it('should generate initial preload link elements', async () => {
80+
harness.useTarget('build', {
81+
...BASE_OPTIONS,
82+
index: {
83+
input: 'src/index.html',
84+
preloadInitial: true,
85+
},
86+
});
87+
88+
// Setup an initial chunk usage for JS
89+
await harness.writeFile('src/a.ts', 'console.log("TEST");');
90+
await harness.writeFile('src/b.ts', 'import "./a";');
91+
await harness.writeFile('src/main.ts', 'import "./a";\n(() => import("./b"))();');
92+
93+
const { result } = await harness.executeOnce();
94+
95+
expect(result?.success).toBe(true);
96+
harness.expectFile('dist/browser/main.js').content.toContain('chunk-');
97+
harness.expectFile('dist/browser/index.html').content.toContain('modulepreload');
98+
harness.expectFile('dist/browser/index.html').content.toContain('chunk-');
99+
});
100+
});
101+
102+
describe('long form syntax', () => {
103+
it('should use the provided input path to generate the output file when present', async () => {
104+
harness.useTarget('build', {
105+
...BASE_OPTIONS,
106+
index: {
107+
input: 'src/index.html',
108+
},
109+
});
110+
111+
await harness.writeFile(
112+
'src/index.html',
113+
'<html><head><title>TEST_123</title></head><body></body>',
114+
);
115+
116+
const { result } = await harness.executeOnce();
117+
118+
expect(result?.success).toBe(true);
119+
harness.expectFile('dist/browser/index.html').content.toContain('TEST_123');
120+
});
121+
122+
it('should use the provided output path to generate the output file when present', async () => {
123+
harness.useTarget('build', {
124+
...BASE_OPTIONS,
125+
index: {
126+
input: 'src/index.html',
127+
output: 'output.html',
128+
},
129+
});
130+
131+
await harness.writeFile(
132+
'src/index.html',
133+
'<html><head><title>TEST_123</title></head><body></body>',
134+
);
135+
136+
const { result } = await harness.executeOnce();
137+
138+
expect(result?.success).toBe(true);
139+
harness.expectFile('dist/browser/output.html').content.toContain('TEST_123');
140+
});
141+
});
142+
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"))();');
156+
157+
const { result } = await harness.executeOnce();
158+
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+
});
164+
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+
});
173+
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"))();');
178+
179+
const { result } = await harness.executeOnce();
180+
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+
});
186+
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+
});
195+
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+
});
208+
});
209+
});

packages/angular_devkit/build_angular/src/tools/esbuild/index-html-generator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export async function generateIndexHtml(
3838

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

41-
if (!externalPackages) {
41+
if (!externalPackages && indexHtmlOptions.preloadInitial) {
4242
for (const [key, value] of initialFiles) {
4343
if (value.entrypoint) {
4444
// Entry points are already referenced in the HTML

0 commit comments

Comments
 (0)