77 */
88import { createHash } from 'crypto' ;
99import { Compiler , compilation } from 'webpack' ;
10- import { RawSource } from 'webpack-sources' ;
10+ import { RawSource , ReplaceSource } from 'webpack-sources' ;
1111
1212const parse5 = require ( 'parse5' ) ;
1313
@@ -95,71 +95,94 @@ export class IndexHtmlWebpackPlugin {
9595
9696 // Find the head and body elements
9797 const treeAdapter = parse5 . treeAdapters . default ;
98- const document = parse5 . parse ( inputContent , { treeAdapter } ) ;
98+ const document = parse5 . parse ( inputContent , { treeAdapter, locationInfo : true } ) ;
9999 let headElement ;
100100 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 ;
106106 }
107- if ( htmlNode . tagName === 'body' ) {
108- bodyElement = htmlNode ;
107+ if ( htmlChild . tagName === 'body' ) {
108+ bodyElement = htmlChild ;
109109 }
110110 }
111111 }
112112 }
113113
114- // Inject into the html
115-
116114 if ( ! headElement || ! bodyElement ) {
117115 throw new Error ( 'Missing head and/or body elements' ) ;
118116 }
119117
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 ( ) ;
120141 for ( const script of scripts ) {
121142 const attrs = [
122143 { name : 'type' , value : 'text/javascript' } ,
123144 { name : 'src' , value : ( this . _options . deployUrl || '' ) + script } ,
124145 ] ;
146+
125147 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 ) ) ;
134150 }
135151
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 ) ;
142154 }
143155
156+ indexSource . insert (
157+ scriptInsertionPoint ,
158+ parse5 . serialize ( scriptElements , { treeAdapter } ) ,
159+ ) ;
160+
144161 // Adjust base href if specified
145- if ( this . _options . baseHref != undefined ) {
162+ if ( typeof this . _options . baseHref == 'string' ) {
146163 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 ;
151167 }
152168 }
153169
170+ const baseFragment = treeAdapter . createDocumentFragment ( ) ;
171+
154172 if ( ! baseElement ) {
155- const element = treeAdapter . createElement (
173+ baseElement = treeAdapter . createElement (
156174 'base' ,
157175 undefined ,
158176 [
159177 { name : 'href' , value : this . _options . baseHref } ,
160178 ] ,
161179 ) ;
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+ ) ;
163186 } else {
164187 let hrefAttribute ;
165188 for ( const attribute of baseElement . attrs ) {
@@ -172,24 +195,51 @@ export class IndexHtmlWebpackPlugin {
172195 } else {
173196 baseElement . attrs . push ( { name : 'href' , value : this . _options . baseHref } ) ;
174197 }
198+
199+ treeAdapter . appendChild ( baseFragment , baseElement ) ;
200+ indexSource . replace (
201+ baseElement . __location . startOffset ,
202+ baseElement . __location . endOffset ,
203+ parse5 . serialize ( baseFragment , { treeAdapter } ) ,
204+ ) ;
175205 }
176206 }
177207
208+ const styleElements = treeAdapter . createDocumentFragment ( ) ;
178209 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 ) ;
188222 }
189223
224+ indexSource . insert (
225+ styleInsertionPoint ,
226+ parse5 . serialize ( styleElements , { treeAdapter } ) ,
227+ ) ;
228+
190229 // 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 ;
193231 } ) ;
194232 }
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+ }
195245}
0 commit comments