Skip to content

Commit 050499b

Browse files
committed
add preload link for chunks with webpack preload magic comment in entry point
1 parent 647ebcb commit 050499b

18 files changed

+362
-7
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,5 @@ src/*.js
9999
__tests__/CHANGELOG-heavy.md
100100
lib
101101
test/dist/
102-
package-lock.json
102+
package-lock.json
103+
test/**/dist

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
"peacock.color": "#000000",
1010
"peacock.affectActivityBar": false,
1111
"peacock.affectTitleBar": false,
12+
"liveServer.settings.multiRootWorkspaceName": "html-webpack-inject-preload",
1213
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module.exports = {
5050
* files: An array of files object
5151
* match: A regular expression to target files you want to preload
5252
* attributes: Any attributes you want to use. The plugin will add the attribute `rel="preload"` by default.
53+
* entryPointWebpackPreload: boolean. support `/* webpackPreload: true */ ` magic comment, when preloading files in initial chunk, see this [issue](https://github.com/jantimon/html-webpack-plugin/issues/1317) in detail. `crossorigin` for font will be add automatically.
5354

5455
**Usage**
5556

src/entry-point-webpack-preload.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import type { Compilation } from 'webpack';
2+
import type {
3+
default as HtmlWebpackPluginInstance,
4+
HtmlTagObject,
5+
} from 'html-webpack-plugin';
6+
7+
8+
type AlterAssetTagGroupsHookParam = Parameters<Parameters<HtmlWebpackPluginInstance.Hooks['alterAssetTagGroups']['tapAsync']>[1]>[0];
9+
10+
type EntryName = string;
11+
type File = string;
12+
13+
export function addLinkForEntryPointWebpackPreload(
14+
compilation: Compilation,
15+
htmlPluginData: AlterAssetTagGroupsHookParam,
16+
) {
17+
18+
// Html can contain multiple entrypoints, entries contains preloaded ChunkGroups, ChunkGroups contains chunks, chunks contains files.
19+
// Files are what we need.
20+
21+
const entryFileMap = prepareEntryFileMap(compilation, htmlPluginData);
22+
23+
// Prepare link tags for HtmlWebpackPlugin
24+
const publicPath = getPublicPath(compilation, htmlPluginData);
25+
const entryHtmlTagObjectMap = generateHtmlTagObject(entryFileMap, publicPath);
26+
27+
// Related files's link tags should follow parent script tag (the entries scripts' tag)
28+
// according to this [blog](https://web.dev/priority-hints/#using-preload-after-chrome-95).
29+
alterAssetTagGroups(entryHtmlTagObjectMap, compilation, htmlPluginData);
30+
}
31+
32+
function alterAssetTagGroups(entryHtmlTagObjectMap: Map<EntryName, Set<HtmlTagObject>>, compilation: Compilation, htmlPluginData: AlterAssetTagGroupsHookParam) {
33+
for (const [entryName, linkTags] of entryHtmlTagObjectMap) {
34+
//Find first link index to inject before, which is the script elemet for the entrypoint.
35+
let files = compilation.entrypoints.get(entryName)?.getEntrypointChunk().files;
36+
if (!files || files.size === 0) {
37+
continue;
38+
}
39+
const lastFile = [...files][files.size - 1]
40+
const findLastFileScriptTagIndex = tag => tag.tagName === 'script' && (tag.attributes.src as string).indexOf(lastFile) !== -1;
41+
let linkIndex = htmlPluginData.headTags.findIndex(
42+
findLastFileScriptTagIndex
43+
);
44+
if (linkIndex === -1) {
45+
htmlPluginData.bodyTags.findIndex(findLastFileScriptTagIndex);
46+
}
47+
if (linkIndex === -1) {
48+
console.warn(`cannot find entrypoints\'s script tags for entry: ${entryName}`);
49+
continue;
50+
};
51+
htmlPluginData.headTags.splice(linkIndex, 0, ...linkTags);
52+
}
53+
}
54+
55+
/**
56+
* Get entrypoints related preload files' names
57+
*
58+
* Html can contain multiple entrypoints, entries contains preloaded ChunkGroups, ChunkGroups contains chunks, chunks contains files.
59+
* Files are what we need.
60+
* @param compilation
61+
* @param htmlPluginData
62+
*/
63+
function prepareEntryFileMap(
64+
compilation: Compilation,
65+
htmlPluginData: AlterAssetTagGroupsHookParam) {
66+
const entryFileMap = new Map<EntryName, Set<File>>;
67+
68+
const entries = htmlPluginData.plugin.options?.chunks ?? 'all';
69+
let entriesKeys = Array.isArray(entries) ? entries : Array.from(compilation.entrypoints.keys());
70+
71+
for (const key of entriesKeys) {
72+
const files = new Set<string>();
73+
const preloaded = compilation.entrypoints.get(key)?.getChildrenByOrders(compilation.moduleGraph, compilation.chunkGraph).preload;
74+
if (!preloaded) continue;
75+
entryFileMap.set(key, files);
76+
// cannot get font files in `preload`
77+
for (const group of preloaded) { // the order of preloaded is relevant
78+
for (const chunk of group.chunks)
79+
for (const file of chunk.files) files.add(file);
80+
}
81+
}
82+
83+
return entryFileMap;
84+
}
85+
86+
/**
87+
* Generate HtmlTagObjects for HtmlWebpackPlugin
88+
* @param entryFileMap
89+
* @param publicPath
90+
* @returns
91+
*/
92+
function generateHtmlTagObject(entryFileMap: Map<string, Set<string>>, publicPath: string): Map<EntryName, Set<HtmlTagObject>> {
93+
const map = new Map();
94+
for (const [key, filesNames] of entryFileMap) {
95+
map.set(key, [...filesNames].map(fileName => {
96+
const href = `${publicPath}${fileName}`;
97+
const as = getTypeOfResource(fileName);
98+
const crossOrigin = as === 'font';
99+
let attributes: HtmlTagObject['attributes'] = {
100+
rel: 'preload',
101+
href,
102+
as
103+
}
104+
if (crossOrigin) {
105+
attributes = { ...attributes, crossorigin: undefined }
106+
}
107+
return {
108+
tagName: 'link',
109+
attributes,
110+
voidTag: true,
111+
meta: {
112+
plugin: 'html-webpack-inject-preload',
113+
},
114+
}
115+
}));
116+
117+
}
118+
return map;
119+
}
120+
121+
function getTypeOfResource(fileName: String) {
122+
if (fileName.match(/.js$/)) {
123+
return 'script'
124+
}
125+
if (fileName.match(/.css$/)) {
126+
return 'style'
127+
}
128+
if (fileName.match(/.(woff2|woff|ttf|otf)$/)) {
129+
return 'font'
130+
}
131+
if (fileName.match(/.(gif|jpeg|png|svg)$/)) {
132+
return 'image'
133+
}
134+
}
135+
136+
function getPublicPath(compilation: Compilation, htmlPluginData: AlterAssetTagGroupsHookParam) {
137+
//Get public path
138+
//html-webpack-plugin v5
139+
let publicPath = htmlPluginData.publicPath;
140+
141+
//html-webpack-plugin v4
142+
if (typeof publicPath === 'undefined') {
143+
if (
144+
htmlPluginData.plugin.options?.publicPath &&
145+
htmlPluginData.plugin.options?.publicPath !== 'auto'
146+
) {
147+
publicPath = htmlPluginData.plugin.options?.publicPath;
148+
} else {
149+
publicPath =
150+
typeof compilation.options.output.publicPath === 'string'
151+
? compilation.options.output.publicPath
152+
: '/';
153+
}
154+
155+
//prevent wrong url
156+
if (publicPath[publicPath.length - 1] !== '/') {
157+
publicPath = publicPath + '/';
158+
}
159+
}
160+
return publicPath;
161+
}

src/main.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import type {
22
default as HtmlWebpackPluginInstance,
33
HtmlTagObject,
44
} from 'html-webpack-plugin';
5-
import type {Compilation, Compiler, WebpackPluginInstance} from 'webpack';
5+
import type { Compilation, Compiler, WebpackPluginInstance } from 'webpack';
6+
import { addLinkForEntryPointWebpackPreload } from './entry-point-webpack-preload';
67

78
declare namespace HtmlWebpackInjectPreload {
89
interface Options {
9-
files: HtmlWebpackInjectPreload.File[];
10+
files?: HtmlWebpackInjectPreload.File[];
11+
/**
12+
* generate link tag for `import()`s in a entry chunk
13+
* @defaultValue false
14+
*/
15+
entryPointWebpackPreload?: boolean;
1016
}
1117

1218
interface File {
@@ -46,6 +52,7 @@ interface HtmlWebpackPluginData {
4652
class HtmlWebpackInjectPreload implements WebpackPluginInstance {
4753
private options: HtmlWebpackInjectPreload.Options = {
4854
files: [],
55+
entryPointWebpackPreload: false
4956
};
5057

5158
/**
@@ -106,6 +113,11 @@ class HtmlWebpackInjectPreload implements WebpackPluginInstance {
106113
}
107114
}
108115

116+
// generate link tag for `import()`s in a entry chunk
117+
if (this.options.entryPointWebpackPreload) {
118+
addLinkForEntryPointWebpackPreload(compilation, htmlPluginData);
119+
}
120+
109121
//Get assets name
110122
const assets = new Set(Object.keys(compilation.assets));
111123
compilation.chunks.forEach(chunk => {
@@ -116,10 +128,13 @@ class HtmlWebpackInjectPreload implements WebpackPluginInstance {
116128
const linkIndex = htmlPluginData.headTags.findIndex(
117129
tag => tag.tagName === 'link',
118130
);
119-
131+
const files = this.options.files;
132+
if (!files) {
133+
return;
134+
}
120135
assets.forEach(asset => {
121-
for (let index = 0; index < this.options.files.length; index++) {
122-
const file = this.options.files[index];
136+
for (let index = 0; index < files.length; index++) {
137+
const file = files[index];
123138

124139
if (file.match.test(asset)) {
125140
let href =
@@ -185,3 +200,5 @@ class HtmlWebpackInjectPreload implements WebpackPluginInstance {
185200
}
186201

187202
export = HtmlWebpackInjectPreload;
203+
// exports.default = HtmlWebpackInjectPreload;
204+
// module.exports = HtmlWebpackInjectPreload;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
body {
2+
background-color: blue;
3+
font-family: Roboto, sans-serif;
4+
}
5+
6+
@font-face {
7+
font-family: 'Roboto';
8+
font-weight: 400;
9+
src: url('../../../../Roboto-Regular.woff2') format('woff2'),
10+
url('../../../..//Roboto-Regular.woff') format('woff');
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import './await-imported-by-async-chunk.css';
2+
export async function awaitImportByAsyncChunk() {
3+
console.log('awaitImportByAsyncChunk');
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
body {
2+
background-color: green;
3+
font-family: Roboto, sans-serif;
4+
}
5+
6+
@font-face {
7+
font-family: 'Roboto';
8+
font-weight: 400;
9+
src: url('../../../../Roboto-Regular.woff2') format('woff2'),
10+
url('../../../..//Roboto-Regular.woff') format('woff');
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './await-imported-by-intial-chunk.css';
2+
export async function awaitImportByInitialChunk() {
3+
const { awaitImportByAsyncChunk } = await import(/* webpackPreload: true */'./await-imported-by-async-chunk');
4+
awaitImportByAsyncChunk();
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
async function main() {
2+
const { awaitImportByInitialChunk } = await import(/* webpackPreload: true */ './await-imported-by-intial-chunk');
3+
awaitImportByInitialChunk();
4+
}
5+
6+
main();

0 commit comments

Comments
 (0)