7
7
*/
8
8
9
9
import { 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' ;
14
11
15
12
export type LoadOutputFileFunctionType = ( file : string ) => Promise < string > ;
16
13
@@ -59,12 +56,14 @@ export interface FileInfo {
59
56
* after processing several configurations in order to build different sets of
60
57
* bundles for differential serving.
61
58
*/
62
- // tslint:disable-next-line: no-big-function
63
59
export 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 ;
65
64
66
65
let { crossOrigin = 'none' } = params ;
67
- if ( params . sri && crossOrigin === 'none' ) {
66
+ if ( sri && crossOrigin === 'none' ) {
68
67
crossOrigin = 'anonymous' ;
69
68
}
70
69
@@ -90,33 +89,12 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
90
89
}
91
90
}
92
91
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 [ ] = [ ] ;
113
93
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 } "` ] ;
117
95
118
96
if ( crossOrigin !== 'none' ) {
119
- attrs . push ( { name : ' crossorigin' , value : crossOrigin } ) ;
97
+ attrs . push ( ` crossorigin=" ${ crossOrigin } "` ) ;
120
98
}
121
99
122
100
// 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
130
108
const isModuleType = moduleFiles . some ( scriptPredictor ) ;
131
109
132
110
if ( isNoModuleType && ! isModuleType ) {
133
- attrs . push (
134
- { name : 'nomodule' , value : '' } ,
135
- { name : 'defer' , value : '' } ,
136
- ) ;
111
+ attrs . push ( 'nomodule' , 'defer' ) ;
137
112
} else if ( isModuleType && ! isNoModuleType ) {
138
- attrs . push ( { name : 'type' , value : ' module' } ) ;
113
+ attrs . push ( 'type=" module"' ) ;
139
114
} else {
140
- attrs . push ( { name : 'defer' , value : '' } ) ;
115
+ attrs . push ( 'defer' ) ;
141
116
}
142
117
} else {
143
- attrs . push ( { name : 'defer' , value : '' } ) ;
118
+ attrs . push ( 'defer' ) ;
144
119
}
145
120
146
- if ( params . sri ) {
121
+ if ( sri ) {
147
122
const content = await loadOutputFile ( script ) ;
148
- attrs . push ( _generateSriAttributes ( content ) ) ;
123
+ attrs . push ( generateSriAttributes ( content ) ) ;
149
124
}
150
125
151
- const baseElement = treeAdapter . createElement ( 'script' , undefined , attrs ) ;
152
- treeAdapter . setTemplateContent ( scriptsElements , baseElement ) ;
126
+ scriptTags . push ( `<script ${ attrs . join ( ' ' ) } ></script>` ) ;
153
127
}
154
128
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 [ ] = [ ] ;
189
130
for ( const stylesheet of stylesheets ) {
190
131
const attrs = [
191
- { name : ' rel' , value : ' stylesheet' } ,
192
- { name : ' href' , value : ( params . deployUrl || '' ) + stylesheet } ,
132
+ ` rel=" stylesheet"` ,
133
+ ` href=" ${ deployUrl } ${ stylesheet } "` ,
193
134
] ;
194
135
195
136
if ( crossOrigin !== 'none' ) {
196
- attrs . push ( { name : ' crossorigin' , value : crossOrigin } ) ;
137
+ attrs . push ( ` crossorigin=" ${ crossOrigin } "` ) ;
197
138
}
198
139
199
- if ( params . sri ) {
140
+ if ( sri ) {
200
141
const content = await loadOutputFile ( stylesheet ) ;
201
- attrs . push ( _generateSriAttributes ( content ) ) ;
142
+ attrs . push ( generateSriAttributes ( content ) ) ;
202
143
}
203
144
204
- const element = treeAdapter . createElement ( 'link' , undefined , attrs ) ;
205
- treeAdapter . setTemplateContent ( styleElements , element ) ;
145
+ linkTags . push ( `<link ${ attrs . join ( ' ' ) } >` ) ;
206
146
}
207
147
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
+ } ) ;
229
196
230
- return indexSource . source ( ) ;
197
+ return transformedContent ;
231
198
}
232
199
233
- function _generateSriAttributes ( content : string ) {
200
+ function generateSriAttributes ( content : string ) : string {
234
201
const algo = 'sha384' ;
235
202
const hash = createHash ( algo )
236
203
. update ( content , 'utf8' )
237
204
. digest ( 'base64' ) ;
238
205
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' ;
240
222
}
0 commit comments