77 */
88
99import { createHash } from 'crypto' ;
10- import { RawSource , ReplaceSource } from 'webpack-sources' ;
11-
12- const parse5 = require ( 'parse5' ) ;
13- const treeAdapter = require ( 'parse5-htmlparser2-tree-adapter' ) ;
10+ import { htmlRewritingStream } from './html-rewriting-stream' ;
1411
1512export type LoadOutputFileFunctionType = ( file : string ) => Promise < string > ;
1613
@@ -59,12 +56,14 @@ export interface FileInfo {
5956 * after processing several configurations in order to build different sets of
6057 * bundles for differential serving.
6158 */
62- // tslint:disable-next-line: no-big-function
6359export async function augmentIndexHtml ( params : AugmentIndexHtmlOptions ) : Promise < string > {
64- const { loadOutputFile, files, noModuleFiles = [ ] , moduleFiles = [ ] , entrypoints } = params ;
60+ const {
61+ loadOutputFile, files, noModuleFiles = [ ] , moduleFiles = [ ] , entrypoints,
62+ sri, deployUrl = '' , lang, baseHref, inputContent,
63+ } = params ;
6564
6665 let { crossOrigin = 'none' } = params ;
67- if ( params . sri && crossOrigin === 'none' ) {
66+ if ( sri && crossOrigin === 'none' ) {
6867 crossOrigin = 'anonymous' ;
6968 }
7069
@@ -90,33 +89,12 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
9089 }
9190 }
9291
93- // Find the head and body elements
94- const document = parse5 . parse ( params . inputContent , {
95- treeAdapter,
96- sourceCodeLocationInfo : true ,
97- } ) ;
98-
99- // tslint:disable: no-any
100- const htmlElement = document . children . find ( ( c : any ) => c . name === 'html' ) ;
101- const headElement = htmlElement . children . find ( ( c : any ) => c . name === 'head' ) ;
102- const bodyElement = htmlElement . children . find ( ( c : any ) => c . name === 'body' ) ;
103- // tslint:enable: no-any
104-
105- if ( ! headElement || ! bodyElement ) {
106- throw new Error ( 'Missing head and/or body elements' ) ;
107- }
108-
109- // Inject into the html
110- const indexSource = new ReplaceSource ( new RawSource ( params . inputContent ) , params . input ) ;
111-
112- const scriptsElements = treeAdapter . createDocumentFragment ( ) ;
92+ const scriptTags : string [ ] = [ ] ;
11393 for ( const script of scripts ) {
114- const attrs : { name : string ; value : string } [ ] = [
115- { name : 'src' , value : ( params . deployUrl || '' ) + script } ,
116- ] ;
94+ const attrs = [ `src="${ deployUrl } ${ script } "` ] ;
11795
11896 if ( crossOrigin !== 'none' ) {
119- attrs . push ( { name : ' crossorigin' , value : crossOrigin } ) ;
97+ attrs . push ( ` crossorigin=" ${ crossOrigin } "` ) ;
12098 }
12199
122100 // We want to include nomodule or module when a file is not common amongs all
@@ -130,111 +108,115 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
130108 const isModuleType = moduleFiles . some ( scriptPredictor ) ;
131109
132110 if ( isNoModuleType && ! isModuleType ) {
133- attrs . push (
134- { name : 'nomodule' , value : '' } ,
135- { name : 'defer' , value : '' } ,
136- ) ;
111+ attrs . push ( 'nomodule' , 'defer' ) ;
137112 } else if ( isModuleType && ! isNoModuleType ) {
138- attrs . push ( { name : 'type' , value : ' module' } ) ;
113+ attrs . push ( 'type=" module"' ) ;
139114 } else {
140- attrs . push ( { name : 'defer' , value : '' } ) ;
115+ attrs . push ( 'defer' ) ;
141116 }
142117 } else {
143- attrs . push ( { name : 'defer' , value : '' } ) ;
118+ attrs . push ( 'defer' ) ;
144119 }
145120
146- if ( params . sri ) {
121+ if ( sri ) {
147122 const content = await loadOutputFile ( script ) ;
148- attrs . push ( _generateSriAttributes ( content ) ) ;
123+ attrs . push ( generateSriAttributes ( content ) ) ;
149124 }
150125
151- const baseElement = treeAdapter . createElement ( 'script' , undefined , attrs ) ;
152- treeAdapter . setTemplateContent ( scriptsElements , baseElement ) ;
126+ scriptTags . push ( `<script ${ attrs . join ( ' ' ) } ></script>` ) ;
153127 }
154128
155- indexSource . insert (
156- // parse5 does not provide locations if malformed html is present
157- bodyElement . sourceCodeLocation ?. endTag ?. startOffset || params . inputContent . indexOf ( '</body>' ) ,
158- parse5 . serialize ( scriptsElements , { treeAdapter } ) . replace ( / \= " " / g, '' ) ,
159- ) ;
160-
161- // Adjust base href if specified
162- if ( typeof params . baseHref == 'string' ) {
163- // tslint:disable-next-line: no-any
164- let baseElement = headElement . children . find ( ( t : any ) => t . name === 'base' ) ;
165- const baseFragment = treeAdapter . createDocumentFragment ( ) ;
166-
167- if ( ! baseElement ) {
168- baseElement = treeAdapter . createElement ( 'base' , undefined , [
169- { name : 'href' , value : params . baseHref } ,
170- ] ) ;
171-
172- treeAdapter . setTemplateContent ( baseFragment , baseElement ) ;
173- indexSource . insert (
174- headElement . sourceCodeLocation . startTag . endOffset ,
175- parse5 . serialize ( baseFragment , { treeAdapter } ) ,
176- ) ;
177- } else {
178- baseElement . attribs [ 'href' ] = params . baseHref ;
179- treeAdapter . setTemplateContent ( baseFragment , baseElement ) ;
180- indexSource . replace (
181- baseElement . sourceCodeLocation . startOffset ,
182- baseElement . sourceCodeLocation . endOffset - 1 ,
183- parse5 . serialize ( baseFragment , { treeAdapter } ) ,
184- ) ;
185- }
186- }
187-
188- const styleElements = treeAdapter . createDocumentFragment ( ) ;
129+ const linkTags : string [ ] = [ ] ;
189130 for ( const stylesheet of stylesheets ) {
190131 const attrs = [
191- { name : ' rel' , value : ' stylesheet' } ,
192- { name : ' href' , value : ( params . deployUrl || '' ) + stylesheet } ,
132+ ` rel=" stylesheet"` ,
133+ ` href=" ${ deployUrl } ${ stylesheet } "` ,
193134 ] ;
194135
195136 if ( crossOrigin !== 'none' ) {
196- attrs . push ( { name : ' crossorigin' , value : crossOrigin } ) ;
137+ attrs . push ( ` crossorigin=" ${ crossOrigin } "` ) ;
197138 }
198139
199- if ( params . sri ) {
140+ if ( sri ) {
200141 const content = await loadOutputFile ( stylesheet ) ;
201- attrs . push ( _generateSriAttributes ( content ) ) ;
142+ attrs . push ( generateSriAttributes ( content ) ) ;
202143 }
203144
204- const element = treeAdapter . createElement ( 'link' , undefined , attrs ) ;
205- treeAdapter . setTemplateContent ( styleElements , element ) ;
145+ linkTags . push ( `<link ${ attrs . join ( ' ' ) } >` ) ;
206146 }
207147
208- indexSource . insert (
209- // parse5 does not provide locations if malformed html is present
210- headElement . sourceCodeLocation ?. endTag ?. startOffset || params . inputContent . indexOf ( '</head>' ) ,
211- parse5 . serialize ( styleElements , { treeAdapter } ) ,
212- ) ;
213-
214- // Adjust document locale if specified
215- if ( typeof params . lang == 'string' ) {
216- const htmlFragment = treeAdapter . createDocumentFragment ( ) ;
217- htmlElement . attribs [ 'lang' ] = params . lang ;
218-
219- // we want only openning tag
220- htmlElement . children = [ ] ;
221-
222- treeAdapter . setTemplateContent ( htmlFragment , htmlElement ) ;
223- indexSource . replace (
224- htmlElement . sourceCodeLocation . startTag . startOffset ,
225- htmlElement . sourceCodeLocation . startTag . endOffset - 1 ,
226- parse5 . serialize ( htmlFragment , { treeAdapter } ) . replace ( '</html>' , '' ) ,
227- ) ;
228- }
148+ const { rewriter, transformedContent } = await htmlRewritingStream ( inputContent ) ;
149+ const baseTagExists = inputContent . includes ( '<base' ) ;
150+
151+ rewriter
152+ . on ( 'startTag' , tag => {
153+ switch ( tag . tagName ) {
154+ case 'html' :
155+ // Adjust document locale if specified
156+ if ( isString ( lang ) ) {
157+ updateAttribute ( tag , 'lang' , lang ) ;
158+ }
159+ break ;
160+ case 'head' :
161+ // Base href should be added before any link, meta tags
162+ if ( ! baseTagExists && isString ( baseHref ) ) {
163+ rewriter . emitStartTag ( tag ) ;
164+ rewriter . emitRaw ( `<base href="${ baseHref } ">` ) ;
165+
166+ return ;
167+ }
168+ break ;
169+ case 'base' :
170+ // Adjust base href if specified
171+ if ( isString ( baseHref ) ) {
172+ updateAttribute ( tag , 'href' , baseHref ) ;
173+ }
174+ break ;
175+ }
176+
177+ rewriter . emitStartTag ( tag ) ;
178+ } )
179+ . on ( 'endTag' , tag => {
180+ switch ( tag . tagName ) {
181+ case 'head' :
182+ for ( const linkTag of linkTags ) {
183+ rewriter . emitRaw ( linkTag ) ;
184+ }
185+ break ;
186+ case 'body' :
187+ // Add script tags
188+ for ( const scriptTag of scriptTags ) {
189+ rewriter . emitRaw ( scriptTag ) ;
190+ }
191+ break ;
192+ }
193+
194+ rewriter . emitEndTag ( tag ) ;
195+ } ) ;
229196
230- return indexSource . source ( ) ;
197+ return transformedContent ;
231198}
232199
233- function _generateSriAttributes ( content : string ) {
200+ function generateSriAttributes ( content : string ) : string {
234201 const algo = 'sha384' ;
235202 const hash = createHash ( algo )
236203 . update ( content , 'utf8' )
237204 . digest ( 'base64' ) ;
238205
239- return { name : 'integrity' , value : `${ algo } -${ hash } ` } ;
206+ return `integrity="${ algo } -${ hash } "` ;
207+ }
208+
209+ function updateAttribute ( tag : { attrs : { name : string , value : string } [ ] } , name : string , value : string ) : void {
210+ const index = tag . attrs . findIndex ( a => a . name === name ) ;
211+ const newValue = { name, value } ;
212+
213+ if ( index === - 1 ) {
214+ tag . attrs . push ( newValue ) ;
215+ } else {
216+ tag . attrs [ index ] = newValue ;
217+ }
218+ }
219+
220+ function isString ( value : unknown ) : value is string {
221+ return typeof value === 'string' ;
240222}
0 commit comments