addons/vscode/extension/htmlForWebview.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import * as crypto from 'crypto';
b69ab319import * as vscode from 'vscode';
b69ab3110
b69ab3111export const devPort = 3015;
b69ab3112export const devUri = `http://localhost:${devPort}`;
b69ab3113
b69ab3114export function getWebviewOptions(
b69ab3115 context: vscode.ExtensionContext,
b69ab3116 distFolder: string,
b69ab3117): vscode.WebviewOptions & vscode.WebviewPanelOptions {
b69ab3118 return {
b69ab3119 enableScripts: true,
b69ab3120 retainContextWhenHidden: true,
b69ab3121 // Restrict the webview to only loading content from our extension's output directory.
b69ab3122 localResourceRoots: [
b69ab3123 vscode.Uri.joinPath(context.extensionUri, distFolder),
b69ab3124 vscode.Uri.parse(devUri),
b69ab3125 ],
b69ab3126 portMapping: [{webviewPort: devPort, extensionHostPort: devPort}],
b69ab3127 };
b69ab3128}
b69ab3129
b69ab3130/**
b69ab3131 * Get any extra styles to inject into the webview.
b69ab3132 * Important: this is injected into the HTML directly, and should not
b69ab3133 * use any user-controlled data that could be used maliciously.
b69ab3134 */
b69ab3135function getVSCodeCompatibilityStyles(): string {
b69ab3136 const globalStyles = new Map();
b69ab3137
b69ab3138 const fontFeatureSettings = vscode.workspace
b69ab3139 .getConfiguration('editor')
b69ab3140 .get<string | boolean>('fontLigatures');
b69ab3141 const validFontFeaturesRegex = /^[0-9a-zA-Z"',\-_ ]*$/;
b69ab3142 if (fontFeatureSettings === true) {
b69ab3143 // no need to specify specific additional settings
b69ab3144 } else if (
b69ab3145 !fontFeatureSettings ||
b69ab3146 typeof fontFeatureSettings !== 'string' ||
b69ab3147 !validFontFeaturesRegex.test(fontFeatureSettings)
b69ab3148 ) {
b69ab3149 globalStyles.set('font-variant-ligatures', 'none');
b69ab3150 } else {
b69ab3151 globalStyles.set('font-feature-settings', fontFeatureSettings);
b69ab3152 }
b69ab3153
b69ab3154 const tabSizeSettings = vscode.workspace.getConfiguration('editor').get<number>('tabSize');
b69ab3155 if (typeof tabSizeSettings === 'number') {
b69ab3156 globalStyles.set('--tab-size', tabSizeSettings);
b69ab3157 }
b69ab3158
b69ab3159 const globalStylesFlat = Array.from(globalStyles, ([k, v]) => `${k}: ${v};`);
b69ab3160 return `
b69ab3161 html {
b69ab3162 ${globalStylesFlat.join('\n')};
b69ab3163 }`;
b69ab3164}
b69ab3165
b69ab3166/**
b69ab3167 * When built in dev mode using vite, files are not written to disk.
b69ab3168 * In order to get files to load, we need to set up the server path ourself.
b69ab3169 *
b69ab3170 * Note: no CSPs in dev mode. This should not be used in production!
b69ab3171 */
b69ab3172function devModeHtmlForWebview(
b69ab3173 /**
b69ab3174 * CSS to inject into the HTML in a <style> tag
b69ab3175 */
b69ab3176 extraStyles: string,
b69ab3177 /**
b69ab3178 * javascript to inject into the HTML in a <script> tag
b69ab3179 * IMPORTANT: this MUST be sanitized to avoid XSS attacks
b69ab3180 */
b69ab3181 initialScript: (nonce: string) => string,
b69ab3182 devModeScripts: Array<string>,
b69ab3183 rootClass: string,
b69ab3184 placeholderHtml?: string,
b69ab3185) {
b69ab3186 return `<!DOCTYPE html>
b69ab3187 <html lang="en">
b69ab3188 <head>
b69ab3189 <meta charset="UTF-8">
b69ab3190 <meta name="viewport" content="width=device-width, initial-scale=1.0">
b69ab3191 <base href="${vscode.Uri.parse(devUri)}">
b69ab3192
b69ab3193 <!-- Hot reloading code from Vite. Normally, vite injects this into the HTML.
b69ab3194 But since we have to load this statically, we insert it manually here.
b69ab3195 See https://github.com/vitejs/vite/blob/734a9e3a4b9a0824a5ba4a5420f9e1176ce74093/docs/guide/backend-integration.md?plain=1#L50-L56 -->
b69ab3196 <script type="module">
b69ab3197 import RefreshRuntime from "/@react-refresh"
b69ab3198 RefreshRuntime.injectIntoGlobalHook(window)
b69ab3199 window.$RefreshReg$ = () => {}
b69ab31100 window.$RefreshSig$ = () => (type) => type
b69ab31101 window.__vite_plugin_react_preamble_installed__ = true
b69ab31102 </script>
b69ab31103 <script type="module" src="/@vite/client"></script>
b69ab31104 <style>
b69ab31105 ${getVSCodeCompatibilityStyles()}
b69ab31106 ${extraStyles}
b69ab31107 </style>
b69ab31108 ${initialScript('')}
b69ab31109 ${devModeScripts.map(script => `<script type="module" src="${script}"></script>`).join('\n')}
b69ab31110 </head>
b69ab31111 <body>
b69ab31112 <div id="root" class="${rootClass}">
b69ab31113 ${placeholderHtml ?? 'loading (dev mode)'}
b69ab31114 </div>
b69ab31115 </body>
b69ab31116 </html>`;
b69ab31117}
b69ab31118
b69ab31119const IS_DEV_BUILD = process.env.NODE_ENV === 'development';
b69ab31120export function htmlForWebview({
b69ab31121 webview,
b69ab31122 context,
b69ab31123 extraStyles,
b69ab31124 initialScript,
b69ab31125 title,
b69ab31126 rootClass,
b69ab31127 extensionRelativeBase,
b69ab31128 entryPointFile,
b69ab31129 cssEntryPointFile,
b69ab31130 devModeScripts,
b69ab31131 placeholderHtml,
b69ab31132}: {
b69ab31133 webview: vscode.Webview;
b69ab31134 context: vscode.ExtensionContext;
b69ab31135 /**
b69ab31136 * CSS to inject into the HTML in a <style> tag
b69ab31137 */
b69ab31138 extraStyles: string;
b69ab31139 /**
b69ab31140 * javascript to inject into the HTML in a <script> tag
b69ab31141 * IMPORTANT: this MUST be sanitized to avoid XSS attacks
b69ab31142 */
b69ab31143 initialScript: (nonce: string) => string;
b69ab31144 /** <head>'s <title> of the webview */
b69ab31145 title: string;
b69ab31146 /** className to apply to the root <div> */
b69ab31147 rootClass: string;
b69ab31148 /** Base directory the webview loads from, where `/` in HTTP requests is relative to */
b69ab31149 extensionRelativeBase: string;
b69ab31150 /** Built entry point .js javascript file name to load, relative to extensionRelativeBase */
b69ab31151 entryPointFile: string;
b69ab31152 /** Built bundle .css file name to load, relative to extensionRelativeBase */
b69ab31153 cssEntryPointFile: string;
b69ab31154 /** Entry point scripts used in dev mode, needed for hot reloading */
b69ab31155 devModeScripts: Array<string>;
b69ab31156 /** Placeholder HTML element to show while the webview is loading */
b69ab31157 placeholderHtml?: string;
b69ab31158}) {
b69ab31159 // Only allow accessing resources relative to webview dir,
b69ab31160 // and make paths relative to here.
b69ab31161 const baseUri = webview.asWebviewUri(
b69ab31162 vscode.Uri.joinPath(context.extensionUri, extensionRelativeBase),
b69ab31163 );
b69ab31164
b69ab31165 if (IS_DEV_BUILD) {
b69ab31166 return devModeHtmlForWebview(
b69ab31167 extraStyles,
b69ab31168 initialScript,
b69ab31169 devModeScripts,
b69ab31170 rootClass,
b69ab31171 placeholderHtml,
b69ab31172 );
b69ab31173 }
b69ab31174
b69ab31175 const scriptUri = entryPointFile;
b69ab31176
b69ab31177 // Use a nonce to only allow specific scripts to be run
b69ab31178 const nonce = getNonce();
b69ab31179
b69ab31180 const CSP = [
b69ab31181 `default-src ${webview.cspSource}`,
b69ab31182 `style-src ${webview.cspSource} 'unsafe-inline'`,
b69ab31183 // vscode-webview-ui needs to use style-src-elem without the nonce
b69ab31184 `style-src-elem ${webview.cspSource} 'unsafe-inline'`,
b69ab31185 `font-src ${webview.cspSource} data:`,
b69ab31186 `img-src ${webview.cspSource} https: data:`,
b69ab31187 `script-src ${webview.cspSource} 'nonce-${nonce}' 'wasm-unsafe-eval'`,
b69ab31188 `script-src-elem ${webview.cspSource} 'nonce-${nonce}'`,
b69ab31189 `worker-src ${webview.cspSource} 'nonce-${nonce}' blob:`,
b69ab31190 ].join('; ');
b69ab31191
b69ab31192 return `<!DOCTYPE html>
b69ab31193 <html lang="en">
b69ab31194 <head>
b69ab31195 <meta charset="UTF-8">
b69ab31196 <meta http-equiv="Content-Security-Policy" content="${CSP}">
b69ab31197 <meta name="viewport" content="width=device-width, initial-scale=1.0">
b69ab31198 <base href="${baseUri}/">
b69ab31199 <title>${title}</title>
b69ab31200
b69ab31201 <link href="${cssEntryPointFile}" rel="stylesheet">
b69ab31202 <link href="res/stylex.css" rel="stylesheet">
b69ab31203 <style>
b69ab31204 ${getVSCodeCompatibilityStyles()}
b69ab31205 ${extraStyles}
b69ab31206 </style>
b69ab31207 ${initialScript(nonce)}
b69ab31208 <script type="module" defer="defer" nonce="${nonce}" src="${scriptUri}"></script>
b69ab31209 </head>
b69ab31210 <body>
b69ab31211 <div id="root" class="${rootClass}">
b69ab31212 ${placeholderHtml ?? 'loading...'}
b69ab31213 </div>
b69ab31214 </body>
b69ab31215 </html>`;
b69ab31216}
b69ab31217
b69ab31218function getNonce(): string {
b69ab31219 return crypto.randomBytes(16).toString('base64');
b69ab31220}