PROJECT: VS Code Extension
Skill gerado a partir do pack templates-claude-code. Arquivo de origem: backend/13-vscode-extension.md. Use como baseline e adapte ao projeto antes de mudancas grandes.
Conteudo do template
STACK
- Platform: VS Code Extension API 1.90+
- Language: TypeScript 5.x (strict)
- Bundler: esbuild (via npm script)
- Testing: Mocha + @vscode/test-electron
- UI Patterns: TreeDataProvider, WebviewPanel, StatusBarItem
- Publishing: @vscode/vsce
PROJECT STRUCTURE
src/
├── extension.ts # activate() / deactivate() - entry point
├── commands/
│ ├── index.ts # All command registrations
│ ├── openPanel.ts
│ └── refreshTree.ts
├── providers/
│ ├── MyTreeProvider.ts # TreeDataProvider implementation
│ └── MyWebviewProvider.ts # WebviewViewProvider implementation
├── views/
│ └── webview/
│ ├── index.html # Webview HTML template
│ └── main.ts # Webview-side script (compiled separately)
├── services/
│ └── config.ts # WorkspaceConfiguration helpers
└── utils/
└── logger.ts # OutputChannel wrapper
test/
├── suite/
│ ├── index.ts # Mocha runner
│ └── tree-provider.test.ts
└── runTests.ts # Test entry
.vscode/
└── launch.json # "Run Extension" + "Extension Tests" configs
package.json # contributes: commands, views, menus
tsconfig.json
esbuild.js # Build script
ARCHITECTURE RULES
activate()registers disposables — everyvscode.commands.registerCommand, event listener, and provider must be pushed tocontext.subscriptions.- Activation events are specific — never use
"*"activationEvent inpackage.json; useonCommand,onView,onLanguage, etc. - WebviewPanel uses nonce + CSP — every webview HTML sets
Content-Security-Policywith a unique nonce for<script>tags. - Messages are typed — define
ExtensionMessageandWebviewMessagediscriminated unions; never send untyped objects. - Heavy work is async — never block the extension host thread; use
vscode.window.withProgressfor long operations. - WorkspaceFolder handling — always check
vscode.workspace.workspaceFoldersbefore accessing files; handle multi-root. - No bundled node_modules in VSIX — mark all
vscode.*asexternalin esbuild; dependencies must be bundled.
ACTIVATION + COMMAND REGISTRATION
// src/extension.ts
import * as vscode from 'vscode'
import { registerCommands } from './commands/index.js'
import { MyTreeProvider } from './providers/MyTreeProvider.js'
import { Logger } from './utils/logger.js'
export function activate(context: vscode.ExtensionContext): void {
const logger = new Logger('MyExtension')
logger.info('Extension activated')
// Register tree view
const treeProvider = new MyTreeProvider(context)
const treeView = vscode.window.createTreeView('myExtension.treeView', {
treeDataProvider: treeProvider,
showCollapseAll: true,
})
// All registrations pushed to subscriptions
context.subscriptions.push(
treeView,
...registerCommands(context, treeProvider, logger),
)
}
export function deactivate(): void {
// Cleanup handled by context.subscriptions dispose chain
}
// src/commands/index.ts
import * as vscode from 'vscode'
import type { MyTreeProvider } from '../providers/MyTreeProvider.js'
import type { Logger } from '../utils/logger.js'
export function registerCommands(
context: vscode.ExtensionContext,
tree: MyTreeProvider,
logger: Logger,
): vscode.Disposable[] {
return [
vscode.commands.registerCommand('myExtension.refresh', () => {
tree.refresh()
logger.info('Tree refreshed')
}),
vscode.commands.registerCommand('myExtension.openPanel', (item?: MyTreeItem) => {
// Opens webview panel
openWebviewPanel(context, item)
}),
]
}
TREEDATAPROVIDER PATTERN
// src/providers/MyTreeProvider.ts
import * as vscode from 'vscode'
export class MyTreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly type: 'group' | 'item',
public readonly data?: unknown,
) {
super(
label,
type === 'group'
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None,
)
this.tooltip = this.label
this.contextValue = type
if (type === 'item') {
this.command = {
command: 'myExtension.openPanel',
title: 'Open',
arguments: [this],
}
}
}
}
export class MyTreeProvider implements vscode.TreeDataProvider<MyTreeItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<MyTreeItem | undefined>()
readonly
constructor(private readonly context: vscode.ExtensionContext) {}
refresh(item?: MyTreeItem): void {
this._onDidChangeTreeData.fire(item)
}
getTreeItem(element: MyTreeItem): MyTreeItem { return element }
async getChildren(element?: MyTreeItem): Promise<MyTreeItem[]> {
const folders = vscode.workspace.workspaceFolders
if (!folders) return []
if (!element) {
return folders.map(f => new MyTreeItem(f.name, 'group'))
}
// Return items under the group
return this.loadItems(element.label as string)
}
private async loadItems(folderName: string): Promise<MyTreeItem[]> {
// Load data for this folder
return [new MyTreeItem(`${folderName}/item-1`, 'item')]
}
}
WEBVIEW PANEL WITH CSP + NONCE
// src/commands/openPanel.ts
import * as vscode from 'vscode'
import * as crypto from 'node:crypto'
import * as path from 'node:path'
import * as fs from 'node:fs'
export function openWebviewPanel(
context: vscode.ExtensionContext,
item?: unknown,
): void {
const panel = vscode.window.createWebviewPanel(
'myExtension.panel',
'My Panel',
vscode.ViewColumn.Beside,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'dist', 'webview')],
retainContextWhenHidden: true,
},
)
const nonce = crypto.randomBytes(16).toString('base64')
const scriptUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'dist', 'webview', 'main.js')
)
panel.webview.html = getHtml(nonce, scriptUri, panel.webview)
// Typed message passing
panel.webview.onDidReceiveMessage((msg: WebviewMessage) => {
if (msg.type === 'READY') {
panel.webview.postMessage({ type: 'INIT', data: item } satisfies ExtensionMessage)
}
}, undefined, context.subscriptions)
}
type WebviewMessage = { type: 'READY' } | { type: 'UPDATE'; value: string }
type ExtensionMessage = { type: 'INIT'; data: unknown } | { type: 'ERROR'; message: string }
function getHtml(nonce: string, scriptUri: vscode.Uri, webview: vscode.Webview): string {
const csp = `default-src 'none'; script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'unsafe-inline';`
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="${csp}">
<title>My Panel</title>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`
}
ROUTING TABLE (Command → Handler)
| Command ID | Trigger | Handler | Action |
|---|---|---|---|
myExtension.refresh |
Context menu / button | registerCommands |
Refresh tree view |
myExtension.openPanel |
Tree item click | openWebviewPanel |
Open WebviewPanel |
myExtension.configSet |
Command palette | configCommand |
Update workspace config |
myExtension.showOutput |
Status bar click | logger.show() |
Show output channel |
myExtension.runTask |
Command palette | taskRunner |
vscode.tasks.executeTask |
onDidSaveTextDocument |
File save event | auto-handler | Re-analyze on save |
onDidChangeWorkspaceFolders |
Workspace change | treeProvider.refresh() |
Refresh tree |
onDidChangeConfiguration |
Settings change | configWatcher |
Apply new config |
myExtension.openSettings |
Command palette | — | workbench.action.openSettings |
QUALITY GATES
-
tsc --noEmit— zero errors -
vsce package— VSIX builds without warnings - All commands/views declared in
package.jsoncontributes - No
activationEvents: ["*"] - Every
registerCommanddisposable incontext.subscriptions - Webview HTML has valid CSP with nonce
-
npm testvia@vscode/test-electronpasses - Extension size < 5 MB (check
vsce ls) - No
console.login activated code — useOutputChannel
FORBIDDEN
- ❌
activationEvents: ["*"]— lazy activation only - ❌
setTimeout/setIntervalwithout cleanup ondeactivate - ❌ Webview
enableScripts: truewithout CSP + nonce - ❌ Accessing
vscode.workspace.rootPath(deprecated) — useworkspaceFolders - ❌ Storing secrets in
context.globalState— usecontext.secretsAPI - ❌ Synchronous file I/O in event handlers — always
async/await - ❌ Registering disposables without adding to
context.subscriptions - ❌ Direct DOM calls from extension host code — all DOM work goes in the webview