Skip to content

Commit aaf9ee9

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): add internal support for index HTML link hints
The index HTML generation functionality for both the Webpack-based and esbuild-based browser application builder now supports adding link hint elements to the generated output. This includes `prefetch`, `preload`, `modulepreload`, `preconnect`, and `dns-prefetch` hint modes. This functionality is not yet used by builds and will be integrated within future changes.
1 parent 52e5cd9 commit aaf9ee9

File tree

3 files changed

+251
-2
lines changed

3 files changed

+251
-2
lines changed

packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { createHash } from 'crypto';
9+
import { createHash } from 'node:crypto';
10+
import { extname } from 'node:path';
1011
import { loadEsmModule } from '../load-esm';
1112
import { htmlRewritingStream } from './html-rewriting-stream';
1213

@@ -38,6 +39,7 @@ export interface AugmentIndexHtmlOptions {
3839
entrypoints: Entrypoint[];
3940
/** Used to set the document default locale */
4041
lang?: string;
42+
hints?: { url: string; mode: string }[];
4143
}
4244

4345
export interface FileInfo {
@@ -133,6 +135,38 @@ export async function augmentIndexHtml(
133135
linkTags.push(`<link ${attrs.join(' ')}>`);
134136
}
135137

138+
if (params.hints?.length) {
139+
for (const hint of params.hints) {
140+
const attrs = [`rel="${hint.mode}"`, `href="${deployUrl}${hint.url}"`];
141+
142+
if (hint.mode !== 'modulepreload' && crossOrigin !== 'none') {
143+
// Value is considered anonymous by the browser when not present or empty
144+
attrs.push(crossOrigin === 'anonymous' ? 'crossorigin' : `crossorigin="${crossOrigin}"`);
145+
}
146+
147+
if (hint.mode === 'preload' || hint.mode === 'prefetch') {
148+
switch (extname(hint.url)) {
149+
case '.js':
150+
attrs.push('as="script"');
151+
break;
152+
case '.css':
153+
attrs.push('as="style"');
154+
break;
155+
}
156+
}
157+
158+
if (
159+
sri &&
160+
(hint.mode === 'preload' || hint.mode === 'prefetch' || hint.mode === 'modulepreload')
161+
) {
162+
const content = await loadOutputFile(hint.url);
163+
attrs.push(generateSriAttributes(content));
164+
}
165+
166+
linkTags.push(`<link ${attrs.join(' ')}>`);
167+
}
168+
}
169+
136170
const dir = lang ? await getLanguageDirection(lang, warnings) : undefined;
137171
const { rewriter, transformedContent } = await htmlRewritingStream(html);
138172
const baseTagExists = html.includes('<base');

packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html_spec.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,217 @@ describe('augment-index-html', () => {
164164
`);
165165
});
166166

167+
it(`should add preconnect and dns-prefetch hints when provided with cross origin`, async () => {
168+
const { content, warnings } = await augmentIndexHtml({
169+
...indexGeneratorOptions,
170+
hints: [
171+
{ mode: 'preconnect', url: 'http://example.com' },
172+
{ mode: 'dns-prefetch', url: 'http://example.com' },
173+
],
174+
});
175+
176+
expect(warnings).toHaveSize(0);
177+
expect(content).toEqual(oneLineHtml`
178+
<html>
179+
<head>
180+
<base href="/">
181+
<link rel="preconnect" href="http://example.com">
182+
<link rel="dns-prefetch" href="http://example.com">
183+
</head>
184+
<body>
185+
</body>
186+
</html>
187+
`);
188+
});
189+
190+
it(`should add preconnect and dns-prefetch hints when provided with "use-credentials" cross origin`, async () => {
191+
const { content, warnings } = await augmentIndexHtml({
192+
...indexGeneratorOptions,
193+
crossOrigin: 'use-credentials',
194+
hints: [
195+
{ mode: 'preconnect', url: 'http://example.com' },
196+
{ mode: 'dns-prefetch', url: 'http://example.com' },
197+
],
198+
});
199+
200+
expect(warnings).toHaveSize(0);
201+
expect(content).toEqual(oneLineHtml`
202+
<html>
203+
<head>
204+
<base href="/">
205+
<link rel="preconnect" href="http://example.com" crossorigin="use-credentials">
206+
<link rel="dns-prefetch" href="http://example.com" crossorigin="use-credentials">
207+
</head>
208+
<body>
209+
</body>
210+
</html>
211+
`);
212+
});
213+
214+
it(`should add preconnect and dns-prefetch hints when provided with "anonymous" cross origin`, async () => {
215+
const { content, warnings } = await augmentIndexHtml({
216+
...indexGeneratorOptions,
217+
crossOrigin: 'anonymous',
218+
hints: [
219+
{ mode: 'preconnect', url: 'http://example.com' },
220+
{ mode: 'dns-prefetch', url: 'http://example.com' },
221+
],
222+
});
223+
224+
expect(warnings).toHaveSize(0);
225+
expect(content).toEqual(oneLineHtml`
226+
<html>
227+
<head>
228+
<base href="/">
229+
<link rel="preconnect" href="http://example.com" crossorigin>
230+
<link rel="dns-prefetch" href="http://example.com" crossorigin>
231+
</head>
232+
<body>
233+
</body>
234+
</html>
235+
`);
236+
});
237+
238+
it(`should add preconnect and dns-prefetch hints when provided with "none" cross origin`, async () => {
239+
const { content, warnings } = await augmentIndexHtml({
240+
...indexGeneratorOptions,
241+
crossOrigin: 'none',
242+
hints: [
243+
{ mode: 'preconnect', url: 'http://example.com' },
244+
{ mode: 'dns-prefetch', url: 'http://example.com' },
245+
],
246+
});
247+
248+
expect(warnings).toHaveSize(0);
249+
expect(content).toEqual(oneLineHtml`
250+
<html>
251+
<head>
252+
<base href="/">
253+
<link rel="preconnect" href="http://example.com">
254+
<link rel="dns-prefetch" href="http://example.com">
255+
</head>
256+
<body>
257+
</body>
258+
</html>
259+
`);
260+
});
261+
262+
it(`should add preconnect and dns-prefetch hints when provided with no cross origin`, async () => {
263+
const { content, warnings } = await augmentIndexHtml({
264+
...indexGeneratorOptions,
265+
hints: [
266+
{ mode: 'preconnect', url: 'http://example.com' },
267+
{ mode: 'dns-prefetch', url: 'http://example.com' },
268+
],
269+
});
270+
271+
expect(warnings).toHaveSize(0);
272+
expect(content).toEqual(oneLineHtml`
273+
<html>
274+
<head>
275+
<base href="/">
276+
<link rel="preconnect" href="http://example.com">
277+
<link rel="dns-prefetch" href="http://example.com">
278+
</head>
279+
<body>
280+
</body>
281+
</html>
282+
`);
283+
});
284+
285+
it(`should add modulepreload hint when provided`, async () => {
286+
const { content, warnings } = await augmentIndexHtml({
287+
...indexGeneratorOptions,
288+
hints: [
289+
{ mode: 'modulepreload', url: 'x.js' },
290+
{ mode: 'modulepreload', url: 'y/z.js' },
291+
],
292+
});
293+
294+
expect(warnings).toHaveSize(0);
295+
expect(content).toEqual(oneLineHtml`
296+
<html>
297+
<head>
298+
<base href="/">
299+
<link rel="modulepreload" href="x.js">
300+
<link rel="modulepreload" href="y/z.js">
301+
</head>
302+
<body>
303+
</body>
304+
</html>
305+
`);
306+
});
307+
308+
it(`should add modulepreload hint with no crossorigin attribute when provided with cross origin set`, async () => {
309+
const { content, warnings } = await augmentIndexHtml({
310+
...indexGeneratorOptions,
311+
crossOrigin: 'anonymous',
312+
hints: [
313+
{ mode: 'modulepreload', url: 'x.js' },
314+
{ mode: 'modulepreload', url: 'y/z.js' },
315+
],
316+
});
317+
318+
expect(warnings).toHaveSize(0);
319+
expect(content).toEqual(oneLineHtml`
320+
<html>
321+
<head>
322+
<base href="/">
323+
<link rel="modulepreload" href="x.js">
324+
<link rel="modulepreload" href="y/z.js">
325+
</head>
326+
<body>
327+
</body>
328+
</html>
329+
`);
330+
});
331+
332+
it(`should add prefetch/preload hints with as=script when specified with a JS url`, async () => {
333+
const { content, warnings } = await augmentIndexHtml({
334+
...indexGeneratorOptions,
335+
hints: [
336+
{ mode: 'prefetch', url: 'x.js' },
337+
{ mode: 'preload', url: 'y/z.js' },
338+
],
339+
});
340+
341+
expect(warnings).toHaveSize(0);
342+
expect(content).toEqual(oneLineHtml`
343+
<html>
344+
<head>
345+
<base href="/">
346+
<link rel="prefetch" href="x.js" as="script">
347+
<link rel="preload" href="y/z.js" as="script">
348+
</head>
349+
<body>
350+
</body>
351+
</html>
352+
`);
353+
});
354+
355+
it(`should add prefetch/preload hints with as=style when specified with a CSS url`, async () => {
356+
const { content, warnings } = await augmentIndexHtml({
357+
...indexGeneratorOptions,
358+
hints: [
359+
{ mode: 'prefetch', url: 'x.css' },
360+
{ mode: 'preload', url: 'y/z.css' },
361+
],
362+
});
363+
364+
expect(warnings).toHaveSize(0);
365+
expect(content).toEqual(oneLineHtml`
366+
<html>
367+
<head>
368+
<base href="/">
369+
<link rel="prefetch" href="x.css" as="style">
370+
<link rel="preload" href="y/z.css" as="style">
371+
</head>
372+
<body>
373+
</body>
374+
</html>
375+
`);
376+
});
377+
167378
it('should add `.mjs` script tags', async () => {
168379
const { content } = await augmentIndexHtml({
169380
...indexGeneratorOptions,

packages/angular_devkit/build_angular/src/utils/index-file/index-html-generator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ type IndexHtmlGeneratorPlugin = (
2121
options: IndexHtmlGeneratorProcessOptions,
2222
) => Promise<string | IndexHtmlTransformResult>;
2323

24+
export type HintMode = 'prefetch' | 'preload' | 'modulepreload' | 'preconnect' | 'dns-prefetch';
25+
2426
export interface IndexHtmlGeneratorProcessOptions {
2527
lang: string | undefined;
2628
baseHref: string | undefined;
2729
outputPath: string;
2830
files: FileInfo[];
31+
hints?: { url: string; mode: HintMode }[];
2932
}
3033

3134
export interface IndexHtmlGeneratorOptions {
@@ -112,7 +115,7 @@ function augmentIndexHtmlPlugin(generator: IndexHtmlGenerator): IndexHtmlGenerat
112115
const { deployUrl, crossOrigin, sri = false, entrypoints } = generator.options;
113116

114117
return async (html, options) => {
115-
const { lang, baseHref, outputPath = '', files } = options;
118+
const { lang, baseHref, outputPath = '', files, hints } = options;
116119

117120
return augmentIndexHtml({
118121
html,
@@ -124,6 +127,7 @@ function augmentIndexHtmlPlugin(generator: IndexHtmlGenerator): IndexHtmlGenerat
124127
entrypoints,
125128
loadOutputFile: (filePath) => generator.readAsset(join(outputPath, filePath)),
126129
files,
130+
hints,
127131
});
128132
};
129133
}

0 commit comments

Comments
 (0)