|
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 = /\/static\/js\/[\d].[^"]*\.js/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 = /\/static\/js\/main.[^"]*\.js/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 = /\/static\/css\/[\d].[^"]*\.css/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 = /\/static\/css\/main.[^"]*\.css/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