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 ( / .j s $ / ) ) {
123
+ return 'script'
124
+ }
125
+ if ( fileName . match ( / .c s s $ / ) ) {
126
+ return 'style'
127
+ }
128
+ if ( fileName . match ( / .( w o f f 2 | w o f f | t t f | o t f ) $ / ) ) {
129
+ return 'font'
130
+ }
131
+ if ( fileName . match ( / .( g i f | j p e g | p n g | s v g ) $ / ) ) {
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
+ }
0 commit comments