7
7
*/
8
8
import { createHash } from 'crypto' ;
9
9
import { Compiler , compilation } from 'webpack' ;
10
- import { RawSource } from 'webpack-sources' ;
10
+ import { RawSource , ReplaceSource } from 'webpack-sources' ;
11
11
12
12
const parse5 = require ( 'parse5' ) ;
13
13
@@ -95,71 +95,94 @@ export class IndexHtmlWebpackPlugin {
95
95
96
96
// Find the head and body elements
97
97
const treeAdapter = parse5 . treeAdapters . default ;
98
- const document = parse5 . parse ( inputContent , { treeAdapter } ) ;
98
+ const document = parse5 . parse ( inputContent , { treeAdapter, locationInfo : true } ) ;
99
99
let headElement ;
100
100
let bodyElement ;
101
- for ( const topNode of document . childNodes ) {
102
- if ( topNode . tagName === 'html' ) {
103
- for ( const htmlNode of topNode . childNodes ) {
104
- if ( htmlNode . tagName === 'head' ) {
105
- headElement = htmlNode ;
101
+ for ( const docChild of document . childNodes ) {
102
+ if ( docChild . tagName === 'html' ) {
103
+ for ( const htmlChild of docChild . childNodes ) {
104
+ if ( htmlChild . tagName === 'head' ) {
105
+ headElement = htmlChild ;
106
106
}
107
- if ( htmlNode . tagName === 'body' ) {
108
- bodyElement = htmlNode ;
107
+ if ( htmlChild . tagName === 'body' ) {
108
+ bodyElement = htmlChild ;
109
109
}
110
110
}
111
111
}
112
112
}
113
113
114
- // Inject into the html
115
-
116
114
if ( ! headElement || ! bodyElement ) {
117
115
throw new Error ( 'Missing head and/or body elements' ) ;
118
116
}
119
117
118
+ // Determine script insertion point
119
+ let scriptInsertionPoint ;
120
+ if ( bodyElement . __location && bodyElement . __location . endTag ) {
121
+ scriptInsertionPoint = bodyElement . __location . endTag . startOffset ;
122
+ } else {
123
+ // Less accurate fallback
124
+ // parse5 4.x does not provide locations if malformed html is present
125
+ scriptInsertionPoint = inputContent . indexOf ( '</body>' ) ;
126
+ }
127
+
128
+ let styleInsertionPoint ;
129
+ if ( headElement . __location && headElement . __location . endTag ) {
130
+ styleInsertionPoint = headElement . __location . endTag . startOffset ;
131
+ } else {
132
+ // Less accurate fallback
133
+ // parse5 4.x does not provide locations if malformed html is present
134
+ styleInsertionPoint = inputContent . indexOf ( '</head>' ) ;
135
+ }
136
+
137
+ // Inject into the html
138
+ const indexSource = new ReplaceSource ( new RawSource ( inputContent ) , this . _options . input ) ;
139
+
140
+ const scriptElements = treeAdapter . createDocumentFragment ( ) ;
120
141
for ( const script of scripts ) {
121
142
const attrs = [
122
143
{ name : 'type' , value : 'text/javascript' } ,
123
144
{ name : 'src' , value : ( this . _options . deployUrl || '' ) + script } ,
124
145
] ;
146
+
125
147
if ( this . _options . sri ) {
126
- const algo = 'sha384' ;
127
- const hash = createHash ( algo )
128
- . update ( compilation . assets [ script ] . source ( ) , 'utf8' )
129
- . digest ( 'base64' ) ;
130
- attrs . push (
131
- { name : 'integrity' , value : `${ algo } -${ hash } ` } ,
132
- { name : 'crossorigin' , value : 'anonymous' } ,
133
- ) ;
148
+ const content = compilation . assets [ script ] . source ( ) ;
149
+ attrs . push ( ...this . _generateSriAttributes ( content ) ) ;
134
150
}
135
151
136
- const element = treeAdapter . createElement (
137
- 'script' ,
138
- undefined ,
139
- attrs ,
140
- ) ;
141
- treeAdapter . appendChild ( bodyElement , element ) ;
152
+ const element = treeAdapter . createElement ( 'script' , undefined , attrs ) ;
153
+ treeAdapter . appendChild ( scriptElements , element ) ;
142
154
}
143
155
156
+ indexSource . insert (
157
+ scriptInsertionPoint ,
158
+ parse5 . serialize ( scriptElements , { treeAdapter } ) ,
159
+ ) ;
160
+
144
161
// Adjust base href if specified
145
- if ( this . _options . baseHref != undefined ) {
162
+ if ( typeof this . _options . baseHref == 'string' ) {
146
163
let baseElement ;
147
- for ( const node of headElement . childNodes ) {
148
- if ( node . tagName === 'base' ) {
149
- baseElement = node ;
150
- break ;
164
+ for ( const headChild of headElement . childNodes ) {
165
+ if ( headChild . tagName === 'base' ) {
166
+ baseElement = headChild ;
151
167
}
152
168
}
153
169
170
+ const baseFragment = treeAdapter . createDocumentFragment ( ) ;
171
+
154
172
if ( ! baseElement ) {
155
- const element = treeAdapter . createElement (
173
+ baseElement = treeAdapter . createElement (
156
174
'base' ,
157
175
undefined ,
158
176
[
159
177
{ name : 'href' , value : this . _options . baseHref } ,
160
178
] ,
161
179
) ;
162
- treeAdapter . appendChild ( headElement , element ) ;
180
+
181
+ treeAdapter . appendChild ( baseFragment , baseElement ) ;
182
+ indexSource . insert (
183
+ headElement . __location . startTag . endOffset + 1 ,
184
+ parse5 . serialize ( baseFragment , { treeAdapter } ) ,
185
+ ) ;
163
186
} else {
164
187
let hrefAttribute ;
165
188
for ( const attribute of baseElement . attrs ) {
@@ -172,24 +195,51 @@ export class IndexHtmlWebpackPlugin {
172
195
} else {
173
196
baseElement . attrs . push ( { name : 'href' , value : this . _options . baseHref } ) ;
174
197
}
198
+
199
+ treeAdapter . appendChild ( baseFragment , baseElement ) ;
200
+ indexSource . replace (
201
+ baseElement . __location . startOffset ,
202
+ baseElement . __location . endOffset ,
203
+ parse5 . serialize ( baseFragment , { treeAdapter } ) ,
204
+ ) ;
175
205
}
176
206
}
177
207
208
+ const styleElements = treeAdapter . createDocumentFragment ( ) ;
178
209
for ( const stylesheet of stylesheets ) {
179
- const element = treeAdapter . createElement (
180
- 'link' ,
181
- undefined ,
182
- [
183
- { name : 'rel' , value : 'stylesheet' } ,
184
- { name : 'href' , value : ( this . _options . deployUrl || '' ) + stylesheet } ,
185
- ] ,
186
- ) ;
187
- treeAdapter . appendChild ( headElement , element ) ;
210
+ const attrs = [
211
+ { name : 'rel' , value : 'stylesheet' } ,
212
+ { name : 'href' , value : ( this . _options . deployUrl || '' ) + stylesheet } ,
213
+ ] ;
214
+
215
+ if ( this . _options . sri ) {
216
+ const content = compilation . assets [ stylesheet ] . source ( ) ;
217
+ attrs . push ( ...this . _generateSriAttributes ( content ) ) ;
218
+ }
219
+
220
+ const element = treeAdapter . createElement ( 'link' , undefined , attrs ) ;
221
+ treeAdapter . appendChild ( styleElements , element ) ;
188
222
}
189
223
224
+ indexSource . insert (
225
+ styleInsertionPoint ,
226
+ parse5 . serialize ( styleElements , { treeAdapter } ) ,
227
+ ) ;
228
+
190
229
// Add to compilation assets
191
- const outputContent = parse5 . serialize ( document , { treeAdapter } ) ;
192
- compilation . assets [ this . _options . output ] = new RawSource ( outputContent ) ;
230
+ compilation . assets [ this . _options . output ] = indexSource ;
193
231
} ) ;
194
232
}
233
+
234
+ private _generateSriAttributes ( content : string ) {
235
+ const algo = 'sha384' ;
236
+ const hash = createHash ( algo )
237
+ . update ( content , 'utf8' )
238
+ . digest ( 'base64' ) ;
239
+
240
+ return [
241
+ { name : 'integrity' , value : `${ algo } -${ hash } ` } ,
242
+ { name : 'crossorigin' , value : 'anonymous' } ,
243
+ ] ;
244
+ }
195
245
}
0 commit comments