| name | vscode-extension-expert |
| description | Expert-level guidance for VS Code extension development covering WebView panels, Content Security Policy, postMessage bridge, Language Server Protocol, performance optimization, security checklist, and Marketplace publishing requirements. Use when implementing WebViews, designing extension architecture, building sidebar panels, debugging CSP issues, or preparing extensions for production. Triggers on mentions of VS Code API, SidebarProvider, WebviewPanel, CSP, postMessage, vsce publish, or extension performance. |
VS Code Extension Expert Guide
Extension Architecture
my-extension/
โโโ package.json # Manifest: commands, views, config contributions
โโโ src/
โ โโโ extension.ts # activate() / deactivate() entry point
โ โโโ sidebar/
โ โ โโโ SidebarProvider.ts # WebviewViewProvider implementation
โ โโโ engine/ # Core business logic (no VS Code deps here)
โ โโโ utils/ # Pure utility functions
โโโ webview-src/ # React/Vue/Vanilla UI source
โ โโโ App.tsx
โโโ dist/ # Bundled output (committed in .vsix)
โ โโโ extension/
โ โโโ webview/
โโโ media/ # Static assets (icons, CSS)
Keep VS Code API calls isolated in extension.ts and providers. Pure logic in engine/ is testable without VS Code mocks.
WebView Development
SidebarProvider (WebviewViewProvider)
import * as vscode from 'vscode';
export class SidebarProvider implements vscode.WebviewViewProvider {
public static readonly viewId = 'my-ext.sidebar';
private _view?: vscode.WebviewView;
constructor(private readonly _extensionUri: vscode.Uri) {}
resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri],
};
webviewView.webview.html = this._getHtmlContent(webviewView.webview);
webviewView.webview.onDidReceiveMessage(this._handleMessage, this);
}
private _handleMessage(message: { command: string; payload?: unknown }) {
switch (message.command) {
case 'fetch-data':
this._view?.webview.postMessage({ command: 'data-ready', payload: {} });
break;
}
}
private _getHtmlContent(webview: vscode.Webview): string {
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview', 'index.js')
);
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'none';
script-src 'nonce-${nonce}';
style-src ${webview.cspSource} 'unsafe-inline';
img-src ${webview.cspSource} https: data:;
connect-src https:;" />
<title>My Extension</title>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
}
function getNonce(): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
React Webview Side (postMessage)
declare function acquireVsCodeApi(): {
postMessage(msg: unknown): void;
getState(): unknown;
setState(state: unknown): void;
};
const vscode = acquireVsCodeApi();
vscode.postMessage({ command: 'fetch-data', payload: { ticketId: '123' } });
window.addEventListener('message', (event) => {
const message = event.data as { command: string; payload?: unknown };
switch (message.command) {
case 'data-ready':
break;
}
});
Content Security Policy (CSP)
Always implement strict CSP โ it prevents XSS in webviews:
| Directive | Purpose |
|---|
default-src 'none' | Block everything by default |
script-src 'nonce-${nonce}' | Only allow nonce-tagged scripts |
style-src ${webview.cspSource} 'unsafe-inline' | Allow bundled + inline styles |
connect-src https: | Allow fetch/XHR to HTTPS APIs |
img-src ${webview.cspSource} https: data: | Allow webview + remote images |
Never use script-src 'unsafe-eval' or script-src *.
Performance Optimization
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('my-ext.analyze', async () => {
const { CodeAnalyzer } = await import('./engine/CodeAnalyzer');
const analyzer = new CodeAnalyzer();
await analyzer.run();
})
);
}
Use esbuild or webpack to bundle โ don't ship node_modules/ in your extension.
Testing Strategy
npm run test:unit
npx @vscode/test-cli --config .vscode-test.mjs
jest.mock('vscode', () => ({
window: { showInformationMessage: jest.fn() },
workspace: { getConfiguration: jest.fn(() => ({ get: jest.fn() })) },
commands: { registerCommand: jest.fn() },
Uri: { joinPath: jest.fn(), file: jest.fn() },
}), { virtual: true });
Security Checklist
Publishing Requirements
{
"publisher": "your-publisher-id",
"name": "my-extension",
"displayName": "My Extension",
"description": "One clear sentence what this does",
"version": "1.0.0",
"engines": { "vscode": "^1.85.0" },
"categories": ["Other"],
"repository": { "type": "git", "url": "https://github.com/you/repo" },
"license": "MIT",
"icon": "media/icon.png"
}
vsce login your-publisher-id
vsce publish minor
vsce publish patch
vsce publish 2.0.0
Icon must be 128ร128 PNG. Add a README.md with screenshots โ it becomes the Marketplace page.
Common Pitfalls
| Problem | Fix |
|---|
| Webview blank after reload | Persist state with vscode.setState() / vscode.getState() |
| CSP blocks scripts | Add nonce attribute to every <script> tag |
| Extension slow to start | Move work behind commands; use onStartupFinished sparingly |
| Assets 404 in webview | Use webview.asWebviewUri() โ never raw file paths |
Tests fail to find vscode | Add moduleNameMapper in jest config to mock vscode |