47 lines
2.0 KiB
TypeScript
47 lines
2.0 KiB
TypeScript
import type { Webview } from 'vscode'
|
|
import { randomBytes } from 'node:crypto'
|
|
import * as fs from 'node:fs'
|
|
import { Uri } from 'vscode'
|
|
|
|
export function makeNonce(): string {
|
|
return randomBytes(16).toString('base64').replace(/[+/=]/g, '')
|
|
}
|
|
|
|
/** vite 产物目录,也是 webview 唯一放行的本地资源目录(localResourceRoots)。 */
|
|
export function webviewUiRoot(extensionUri: Uri): Uri {
|
|
return Uri.joinPath(extensionUri, 'dist', 'webview-ui')
|
|
}
|
|
|
|
/**
|
|
* 读 dist/webview-ui 下的一个入口页面,改成能在 webview 里跑的样子:
|
|
* 资源路径改写成 webview URI、每个 `<script>` 带上 nonce、注入只放行这个 nonce 的 CSP。
|
|
* entry 是入口 html 文件名(例如 'index.html');调用方还要往页面里追加别的 `<script>` 时,把同一个 nonce 传进来。
|
|
*/
|
|
export function webviewHtml(webview: Webview, extensionUri: Uri, entry: string, nonce = makeNonce()): string {
|
|
const distRoot = webviewUiRoot(extensionUri)
|
|
let html = fs.readFileSync(Uri.joinPath(distRoot, entry).fsPath, 'utf-8')
|
|
|
|
// vite 产物的入口是固定名(无 hash),webview 按 URL 缓存资源——必须按文件 mtime 加 ?v= 版本号,
|
|
// 否则 reload 后仍会跑缓存里的旧入口脚本 / 样式
|
|
html = html.replace(/(src|href)="(\/[^"]+|[^"/][^"]*)"/g, (_m, attr, p) => {
|
|
const fileUri = Uri.joinPath(distRoot, p.replace(/^\.?\//, ''))
|
|
let version = ''
|
|
try {
|
|
version = `?v=${fs.statSync(fileUri.fsPath).mtimeMs}`
|
|
}
|
|
catch {
|
|
// 资源文件不存在则不加版本号
|
|
}
|
|
return `${attr}="${webview.asWebviewUri(fileUri)}${version}"`
|
|
})
|
|
|
|
html = html.replace(/<script(\s[^>]*)?>/g, (_m, attrs) => `<script nonce="${nonce}"${attrs ?? ''}>`)
|
|
|
|
const cspSource = webview.cspSource
|
|
const csp = `default-src 'none'; img-src ${cspSource} data:; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; font-src ${cspSource};`
|
|
return html.replace(
|
|
/<head>/,
|
|
`<head>\n <meta http-equiv="Content-Security-Policy" content="${csp}">`,
|
|
)
|
|
}
|