1
- import { JSDOM } from 'jsdom'
2
1
import * as path from 'path'
3
2
import * as vscode from 'vscode'
4
3
import { asyncReadFile } from '../node'
@@ -14,39 +13,64 @@ const getNonce = (): string => {
14
13
return text
15
14
}
16
15
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
+ */
17
34
async 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
+
18
43
try {
19
44
// 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' )
22
46
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 } " />` )
28
50
29
51
// used for CSP
30
52
const nonces : string [ ] = [ ]
31
53
const hashes : string [ ] = [ ]
32
54
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 } ` )
39
64
}
40
65
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 } ` )
50
74
}
51
75
52
76
// support additional CSP exemptions when CodeRoad is embedded
@@ -61,43 +85,45 @@ async function render(panel: vscode.WebviewPanel, rootPath: string): Promise<voi
61
85
}
62
86
}
63
87
64
- // add run-time script from webpack
65
- const runTimeScript = document . createElement ( 'script' )
66
- runTimeScript . nonce = getNonce ( )
67
- nonces . push ( runTimeScript . nonce )
68
-
69
88
// note: file cannot be imported or results in esbuild error. Easier to read it.
70
89
let manifest
71
90
try {
72
91
const manifestPath = path . join ( rootPath , 'asset-manifest.json' )
73
- console . log ( manifestPath )
74
92
const manifestFile = await asyncReadFile ( manifestPath , 'utf8' )
75
93
manifest = JSON . parse ( manifestFile )
76
94
} catch ( e ) {
77
95
throw new Error ( 'Failed to read manifest file' )
78
96
}
79
97
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
+ }
82
111
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 )
89
118
}
90
119
91
120
// set CSP (content security policy) to grant permission to local files
92
121
// while blocking unexpected malicious network requests
93
- const cspMeta : HTMLMetaElement = document . createElement ( 'meta' )
94
- cspMeta . httpEquiv = 'Content-Security-Policy'
95
-
96
122
const wrapInQuotes = ( str : string ) => `'${ str } '`
97
123
const nonceString = nonces . map ( ( nonce : string ) => wrapInQuotes ( `nonce-${ nonce } ` ) ) . join ( ' ' )
98
124
const hashString = hashes . map ( wrapInQuotes ) . join ( ' ' )
99
125
100
- cspMeta . content =
126
+ const cspMetaString =
101
127
[
102
128
`default-src 'self'` ,
103
129
`manifest-src ${ hashString } 'self'` ,
@@ -110,10 +136,8 @@ async function render(panel: vscode.WebviewPanel, rootPath: string): Promise<voi
110
136
// @ts -ignore
111
137
`style-src ${ panel . webview . cspSource } https: 'self' 'unsafe-inline'` ,
112
138
] . 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>` )
117
141
118
142
// set view
119
143
panel . webview . html = html
0 commit comments