mcpField
V
vscode-extension-expert

vscode-extension-expert

github.com/s-hiraoku開発22160 回インストール8 週間前に更新
リポジトリ

vscode-extension-expert について

VS Code 拡張機能を開発・保守する人向けのリファレンススキルです。API、設計、WebView と LSP の実装に加え、セキュリティ、パフォーマンス、テスト、公開の指針を扱います。

主な機能

  1. 01VS Code Extension API のガイド
  2. 02設計とアクティベーションのパターン
  3. 03WebView のセキュリティ対策
  4. 04LSP 実装ガイド
  5. 05パフォーマンスとテストの助言
  6. 06Marketplace 公開チェックリスト

活用シーン

  1. 01新しい VS Code 拡張機能の設計
  2. 02WebView や言語機能の追加
  3. 03起動や実行時の問題の調査
  4. 04拡張機能の公開準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/s-hiraoku/vscode-sidebar-terminal --skill vscode-extension-expert

インストール後、Claude Code で/vscode-extension-expertを入力して読み込まれたか確認します。

詳細

VS Code 拡張機能エキスパート

概要

このスキルは、VS Code Extension API、アーキテクチャパターン、セキュリティ要件、ベストプラクティスに関する包括的な知識を提供し、エキスパートレベルの VS Code 拡張機能開発を可能にします。新しい拡張機能の作成、既存の拡張機能への機能追加、WebView の実装、言語サポートの設計、パフォーマンスの最適化を行う際に使用してください。

このスキルを使用する場面

  • VS Code 拡張機能の新機能を実装するとき
  • 拡張機能のアーキテクチャと構成を設計するとき
  • 適切なセキュリティを備えた WebView ベースの UI を作成するとき
  • Language Server Protocol(LSP)の機能を実装するとき
  • 拡張機能のアクティベーションや実行時の問題をデバッグするとき
  • 拡張機能のパフォーマンスや起動時間を最適化するとき
  • Marketplace への公開に向けて拡張機能を準備するとき

基本概念

拡張機能の構成

すべての VS Code 拡張機能には、次のものが必要です。

extension-name/
├── .vscode/              # Debug configurations
│   ├── launch.json
│   └── tasks.json
├── src/
│   └── extension.ts      # Main entry point
├── package.json          # Extension manifest (critical)
├── tsconfig.json         # TypeScript config
└── .vscodeignore         # Exclude from package
package.json の必須フィールド
{
  "name": "extension-name",
  "publisher": "publisher-id",
  "version": "0.0.1",
  "engines": { "vscode": "^1.80.0" },
  "main": "./out/extension.js",
  "activationEvents": [],
  "contributes": {
    "commands": [],
    "configuration": {},
    "views": {}
  },
  "extensionKind": ["workspace"]
}
拡張機能のエントリポイントパターン
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  // Register commands, providers, listeners
  const disposable = vscode.commands.registerCommand('ext.command', () => {
    // Command implementation
  });

  context.subscriptions.push(disposable);
}

export function deactivate() {
  // Cleanup resources
}

アクティベーションイベント

起動時の負荷を最小限に抑えるため、最も具体的なアクティベーションイベントを選択してください。

イベント使用例例
onLanguage:<lang>言語固有の機能onLanguage:python
onCommand:<command>コマンド駆動型の拡張機能onCommand:ext.showPanel
onView:<viewId>サイドバービューの展開onView:myTreeView
workspaceContains:<glob>プロジェクト固有の機能workspaceContains:**/.eslintrc*
onFileSystem:<scheme>カスタムファイルシステムonFileSystem:sftp
onStartupFinishedバックグラウンドタスク(* よりも優先)

重要:* は VS Code の起動ごとにアクティベートされるため、使用しないでください。

コントリビューションポイント

コマンド
{
  "contributes": {
    "commands": [{
      "command": "ext.doSomething",
      "title": "Do Something",
      "category": "My Extension",
      "icon": "$(symbol-method)"
    }]
  }
}
構成
{
  "contributes": {
    "configuration": {
      "title": "My Extension",
      "properties": {
        "myExtension.enabled": {
          "type": "boolean",
          "default": true,
          "description": "Enable the extension"
        }
      }
    }
  }
}
ビュー(ツリービュー)
{
  "contributes": {
    "views": {
      "explorer": [{
        "id": "myTreeView",
        "name": "My View"
      }]
    },
    "viewsContainers": {
      "activitybar": [{
        "id": "myContainer",
        "title": "My Extension",
        "icon": "resources/icon.svg"
      }]
    }
  }
}

VS Code API 名前空間

window API
// Show messages
vscode.window.showInformationMessage('Hello!');
vscode.window.showErrorMessage('Error occurred');

// Quick picks
const item = await vscode.window.showQuickPick(['Option 1', 'Option 2']);

// Input boxes
const input = await vscode.window.showInputBox({ prompt: 'Enter value' });

// Active editor
const editor = vscode.window.activeTextEditor;
workspace API
// Read configuration
const config = vscode.workspace.getConfiguration('myExtension');
const value = config.get<boolean>('enabled');

// Watch files
const watcher = vscode.workspace.createFileSystemWatcher('**/*.ts');
watcher.onDidChange(uri => { /* handle change */ });

// Open documents
const doc = await vscode.workspace.openTextDocument(uri);
commands API
// Register
const disposable = vscode.commands.registerCommand('ext.cmd', (arg) => {
  // Implementation
});

// Execute
await vscode.commands.executeCommand('ext.cmd', argument);

WebView 開発

セキュリティ要件(重要)
  1. コンテンツセキュリティポリシー(CSP) — 常に厳格な CSP を実装してください。
function getWebviewContent(webview: vscode.Webview): string {
  const nonce = getNonce();

  return `<!DOCTYPE html>
  <html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="
      default-src 'none';
      style-src ${webview.cspSource} 'unsafe-inline';
      script-src 'nonce-${nonce}';
      img-src ${webview.cspSource} https:;
    ">
  </head>
  <body>
    <script nonce="${nonce}">
      const vscode = acquireVsCodeApi();
      // Use vscode.postMessage() for communication
    </script>
  </body>
  </html>`;
}
  1. 入力のサニタイズ — ユーザー入力は必ずサニタイズしてください。
  2. HTTPS のみ — 外部リソースには HTTPS を使用してください。
  3. 権限の最小化 — localResourceRoots を制限してください。
メッセージの受け渡しパターン
// Extension → WebView
panel.webview.postMessage({ type: 'update', data: payload });

// WebView → Extension
panel.webview.onDidReceiveMessage(message => {
  switch (message.type) {
    case 'action':
      handleAction(message.data);
      break;
  }
});

// In WebView JavaScript
window.addEventListener('message', event => {
  const message = event.data;
  // Handle message
});

vscode.postMessage({ type: 'action', data: result });
状態の永続化
// Simple state (survives webview hide/show)
const state = webview.getState() || { count: 0 };
webview.setState({ count: state.count + 1 });

// Full persistence (survives VS Code restart)
class MySerializer implements vscode.WebviewPanelSerializer {
  async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: any) {
    panel.webview.html = getHtmlForWebview(panel.webview, state);
  }
}

vscode.window.registerWebviewPanelSerializer('myWebview', new MySerializer());

Language Server Protocol(LSP)

アーキテクチャ
┌─────────────────────┐     ┌─────────────────────┐
│  Language Client    │────│  Language Server    │
│  (VS Code Extension)│ LSP │  (Separate Process) │
│  vscode-languageclient    │  vscode-languageserver
└─────────────────────┘     └─────────────────────┘
クライアントの実装
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';

const serverOptions: ServerOptions = {
  run: { module: serverPath, transport: TransportKind.ipc },
  debug: { module: serverPath, transport: TransportKind.ipc }
};

const clientOptions: LanguageClientOptions = {
  documentSelector: [{ scheme: 'file', language: 'mylang' }],
  synchronize: {
    fileEvents: vscode.workspace.createFileSystemWatcher('**/*.mylang')
  }
};

const client = new LanguageClient('mylang', 'My Language', serverOptions, clientOptions);
client.start();
サーバーの実装
import { createConnection, TextDocuments, ProposedFeatures } from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';

const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);

connection.onInitialize((params) => {
  return {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      completionProvider: { resolveProvider: true },
      hoverProvider: true
    }
  };
});

connection.onCompletion((params) => {
  return [
    { label: 'suggestion1', kind: CompletionItemKind.Text }
  ];
});

documents.listen(connection);
connection.listen();

ツリービューの実装

class MyTreeDataProvider implements vscode.TreeDataProvider<MyItem> {
  private _onDidChangeTreeData = new vscode.EventEmitter<MyItem | undefined>();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  refresh(): void {
    this._onDidChangeTreeData.fire(undefined);
  }

  getTreeItem(element: MyItem): vscode.TreeItem {
    return {
      label: element.name,
      collapsibleState: element.children ?
        vscode.TreeItemCollapsibleState.Collapsed :
        vscode.TreeItemCollapsibleState.None,
      command: {
        command: 'ext.selectItem',
        title: 'Select',
        arguments: [element]
      }
    };
  }

  getChildren(element?: MyItem): Thenable<MyItem[]> {
    if (!element) {
      return Promise.resolve(this.getRootItems());
    }
    return Promise.resolve(element.children || []);
  }
}

// Register
const provider = new MyTreeDataProvider();
vscode.window.registerTreeDataProvider('myTreeView', provider);

パフォーマンスのベストプラクティス

遅延読み込み
// Delay expensive imports
let heavyModule: typeof import('./heavyModule') | undefined;

async function getHeavyModule() {
  if (!heavyModule) {
    heavyModule = await import('./heavyModule');
  }
  return heavyModule;
}
バンドル(VS Code Web では必須)

高速なバンドルには esbuild を使用してください。

// esbuild.config.js
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['./src/extension.ts'],
  bundle: true,
  outfile: './out/extension.js',
  external: ['vscode'],
  format: 'cjs',
  platform: 'node',
  minify: process.env.NODE_ENV === 'production',
  sourcemap: true
});
リソースのクリーンアップ
export function activate(context: vscode.ExtensionContext) {
  // Always add to subscriptions for automatic cleanup
  context.subscriptions.push(
    vscode.commands.registerCommand(...),
    vscode.window.registerTreeDataProvider(...),
    watcher,
    client
  );
}

export function deactivate() {
  // Explicit cleanup for async resources
  return client?.stop();
}

テスト戦略

@vscode/test-cli を使用した統合テスト
// .vscode-test.js
const { defineConfig } = require('@vscode/test-cli');

module.exports = defineConfig({
  files: 'out/test/**/*.test.js',
  version: 'stable',
  workspaceFolder: './test-fixtures',
  mocha: {
    timeout: 20000  // Note: @vscode/test-cli uses Mocha for VS Code extension host tests
  }
});
テスト構成
import * as assert from 'assert';
import * as vscode from 'vscode';

suite('Extension Test Suite', () => {
  vscode.window.showInformationMessage('Start tests.');

  test('Command registration', async () => {
    const commands = await vscode.commands.getCommands();
    assert.ok(commands.includes('ext.myCommand'));
  });

  test('Configuration access', () => {
    const config = vscode.workspace.getConfiguration('myExtension');
    assert.strictEqual(config.get('enabled'), true);
  });
});

よくある落とし穴と解決策

拡張機能がアクティベートされない

原因:アクティベーションイベントがユーザーの操作と一致していません。
解決策:package.json の activationEvents が実際のトリガーと一致していることを確認してください。

WebView のセキュリティエラー

原因:CSP がないか、正しく設定されていません。
解決策:厳格な Content-Security-Policy の meta タグを必ず含めてください。

メモリリーク

原因:イベントリスナーや破棄可能なリソースが追跡されていません。
解決策:すべての破棄可能なリソースを context.subscriptions に追加してください。

起動が遅い

原因:activate() 内で負荷の高い処理を同期的に実行しています。
解決策:遅延読み込みを使用し、重要でない初期化は後回しにしてください。

コマンドがパレットに表示されない

原因:contributes.commands の宣言がありません。
解決策:コマンドが package.json で宣言され、かつ registerCommand で登録されていることを確認してください。

セキュリティチェックリスト

  • WebView に厳格な Content Security Policy を実装する
  • レンダリング前にすべてのユーザー入力をサニタイズする
  • 外部リソースには HTTPS を使用する
  • WebView からのすべてのメッセージを検証する
  • localResourceRoots を必要なパスに限定する
  • URL の検証には単語境界を含む正規表現を使用する(includes() は使用しない)
  • 設定にシークレットを保存しない(SecretStorage を使用する)

公開チェックリスト

  • 名前と発行元の組み合わせが一意であること
  • PNG アイコン(最小 128x128)
  • 機能とスクリーンショットを含む完全な README.md
  • バージョン履歴を記載した CHANGELOG.md
  • LICENSE ファイル
  • セマンティックバージョニング
  • 開発用ファイルを除外する .vscodeignore
  • Windows、macOS、Linux でテスト済みであること
  • 必要に応じて、Web 互換性のためにバンドルすること

リソース

詳しいリファレンスドキュメントについては、以下を参照してください。

  • references/api-reference.md - VS Code API の完全なドキュメント
  • references/webview-security.md - WebView のセキュリティガイドライン
  • references/lsp-guide.md - Language Server Protocol の実装ガイド

動作するサンプルについては、公式サンプルを参照してください。

コメント