1- import { JSDOM } from 'jsdom'
21import * as path from 'path'
32import * as vscode from 'vscode'
43import { asyncReadFile } from '../node'
@@ -14,39 +13,64 @@ const getNonce = (): string => {
1413 return text
1514}
1615
16+ /**
17+ * render
18+ * configures the index.html into a webview panel
19+ *
20+ * React + webpack generate a number of files that are injected on build time
21+ * and must be accommodated for the webview using a vscode:// path.
22+ *
23+ * Modified paths include
24+ * - /static/js/x.chunk.js
25+ * - /static/js/main.chunk.js
26+ * - runtime-main.js
27+ * - /static/css/x.chunk.css
28+ * - /static/css/main.chunk.css
29+ *
30+ * This function also:
31+ * - modifies the base href of the index.html to be the root of the workspace
32+ * - manages the CSP policy for all the loaded files
33+ */
1734async function render ( panel : vscode . WebviewPanel , rootPath : string ) : Promise < void > {
35+ // generate vscode-resource build path uri
36+ const createUri = ( _filePath : string ) : any => {
37+ const filePath = ( _filePath . startsWith ( 'vscode' ) ? _filePath . substr ( 16 ) : _filePath ) . replace ( '///' , '\\' )
38+
39+ // @ts -ignore
40+ return panel . webview . asWebviewUri ( vscode . Uri . file ( path . join ( rootPath , filePath ) ) )
41+ }
42+
1843 try {
1944 // load copied index.html from web app build
20- const dom = await JSDOM . fromFile ( path . join ( rootPath , 'index.html' ) )
21- const { document } = dom . window
45+ let html = await asyncReadFile ( path . join ( rootPath , 'index.html' ) , 'utf8' )
2246
23- // set base href
24- const base : HTMLBaseElement = document . createElement ( 'base' )
25- base . href = `${ vscode . Uri . file ( path . join ( rootPath , 'build' ) ) . with ( { scheme : 'vscode-resource' } ) } `
26-
27- document . head . appendChild ( base )
47+ // set base href at top of <head>
48+ const baseHref = `${ vscode . Uri . file ( path . join ( rootPath , 'build' ) ) . with ( { scheme : 'vscode-resource' } ) } `
49+ html = html . replace ( '<head>' , `<head><base href="${ baseHref } " />` )
2850
2951 // used for CSP
3052 const nonces : string [ ] = [ ]
3153 const hashes : string [ ] = [ ]
3254
33- // generate vscode-resource build path uri
34- const createUri = ( _filePath : string ) : any => {
35- const filePath = ( _filePath . startsWith ( 'vscode' ) ? _filePath . substr ( 16 ) : _filePath ) . replace ( '///' , '\\' )
36-
37- // @ts -ignore
38- return panel . webview . asWebviewUri ( vscode . Uri . file ( path . join ( rootPath , filePath ) ) )
55+ // fix paths for react static scripts to use vscode-resource paths
56+ var jsBundleChunkRegex = / \/ s t a t i c \/ j s \/ [ \d ] .[ ^ " ] * \. j s / g
57+ var jsBundleChunk : RegExpExecArray | null = jsBundleChunkRegex . exec ( html )
58+ if ( jsBundleChunk ) {
59+ const nonce : string = getNonce ( )
60+ nonces . push ( nonce )
61+ const src = createUri ( jsBundleChunk [ 0 ] )
62+ // replace script src, add nonce
63+ html = html . replace ( jsBundleChunk [ 0 ] , `${ src } " nonce="${ nonce } ` )
3964 }
4065
41- // fix paths for scripts
42- const scripts : HTMLScriptElement [ ] = Array . from ( document . getElementsByTagName ( 'script' ) )
43- for ( const script of scripts ) {
44- if ( script . src ) {
45- const nonce : string = getNonce ( )
46- nonces . push ( nonce )
47- script . nonce = nonce
48- script . src = createUri ( script . src )
49- }
66+ var mainBundleChunkRegex = / \/ s t a t i c \/ j s \/ m a i n .[ ^ " ] * \. j s / g
67+ var mainBundleChunk : RegExpExecArray | null = mainBundleChunkRegex . exec ( html )
68+ if ( mainBundleChunk ) {
69+ const nonce : string = getNonce ( )
70+ nonces . push ( nonce )
71+ const src = createUri ( mainBundleChunk [ 0 ] )
72+ // replace script src, add nonce
73+ html = html . replace ( mainBundleChunk [ 0 ] , `${ src } " nonce="${ nonce } ` )
5074 }
5175
5276 // support additional CSP exemptions when CodeRoad is embedded
@@ -61,43 +85,45 @@ async function render(panel: vscode.WebviewPanel, rootPath: string): Promise<voi
6185 }
6286 }
6387
64- // add run-time script from webpack
65- const runTimeScript = document . createElement ( 'script' )
66- runTimeScript . nonce = getNonce ( )
67- nonces . push ( runTimeScript . nonce )
68-
6988 // note: file cannot be imported or results in esbuild error. Easier to read it.
7089 let manifest
7190 try {
7291 const manifestPath = path . join ( rootPath , 'asset-manifest.json' )
73- console . log ( manifestPath )
7492 const manifestFile = await asyncReadFile ( manifestPath , 'utf8' )
7593 manifest = JSON . parse ( manifestFile )
7694 } catch ( e ) {
7795 throw new Error ( 'Failed to read manifest file' )
7896 }
7997
80- runTimeScript . src = createUri ( manifest . files [ 'runtime-main.js' ] )
81- document . body . appendChild ( runTimeScript )
98+ // add run-time script from webpack at top of <body>
99+ const runtimeNonce = getNonce ( )
100+ nonces . push ( runtimeNonce )
101+ const runtimeSrc = createUri ( manifest . files [ 'runtime-main.js' ] )
102+ html = html . replace ( '<body>' , `<body><script src="${ runtimeSrc } " nonce="${ runtimeNonce } "></script>` )
103+
104+ var cssBundleChunkRegex = / \/ s t a t i c \/ c s s \/ [ \d ] .[ ^ " ] * \. c s s / g
105+ var cssBundleChunk : RegExpExecArray | null = cssBundleChunkRegex . exec ( html )
106+ if ( cssBundleChunk ) {
107+ const href = createUri ( cssBundleChunk [ 0 ] )
108+ // replace script src, add nonce
109+ html = html . replace ( cssBundleChunk [ 0 ] , href )
110+ }
82111
83- // fix paths for links
84- const styles : HTMLLinkElement [ ] = Array . from ( document . getElementsByTagName ( 'link' ) )
85- for ( const style of styles ) {
86- if ( style . href ) {
87- style . href = createUri ( style . href )
88- }
112+ var mainCssBundleChunkRegex = / \/ s t a t i c \/ c s s \/ m a i n . [ ^ " ] * \. c s s / g
113+ var mainCssBundleChunk : RegExpExecArray | null = mainCssBundleChunkRegex . exec ( html )
114+ if ( mainCssBundleChunk ) {
115+ const href = createUri ( mainCssBundleChunk [ 0 ] )
116+ // replace script src, add nonce
117+ html = html . replace ( mainCssBundleChunk [ 0 ] , href )
89118 }
90119
91120 // set CSP (content security policy) to grant permission to local files
92121 // while blocking unexpected malicious network requests
93- const cspMeta : HTMLMetaElement = document . createElement ( 'meta' )
94- cspMeta . httpEquiv = 'Content-Security-Policy'
95-
96122 const wrapInQuotes = ( str : string ) => `'${ str } '`
97123 const nonceString = nonces . map ( ( nonce : string ) => wrapInQuotes ( `nonce-${ nonce } ` ) ) . join ( ' ' )
98124 const hashString = hashes . map ( wrapInQuotes ) . join ( ' ' )
99125
100- cspMeta . content =
126+ const cspMetaString =
101127 [
102128 `default-src 'self'` ,
103129 `manifest-src ${ hashString } 'self'` ,
@@ -110,10 +136,8 @@ async function render(panel: vscode.WebviewPanel, rootPath: string): Promise<voi
110136 // @ts -ignore
111137 `style-src ${ panel . webview . cspSource } https: 'self' 'unsafe-inline'` ,
112138 ] . join ( '; ' ) + ';'
113- document . head . appendChild ( cspMeta )
114-
115- // stringify dom
116- const html = dom . serialize ( )
139+ // add CSP to end of <head>
140+ html = html . replace ( '</head>' , `<meta http-equiv="Content-Security-Policy" content="${ cspMetaString } " /></head>` )
117141
118142 // set view
119143 panel . webview . html = html
0 commit comments