🌊

VSCodeの拡張機能を作る手順

2023/04/14に公開

前説

本記事は、下記で紹介したVSCode拡張機能をリリースするにあたって、プロジェクトの作成から公開、その他気にしておくべきことなどをまとめたものになります。

目次

雑文

もはや我々デベロッパーの日常とは切っても切り離せない「VSCode」。毎日お世話になっております!

VSCodeには開発する言語やミドルウェアに応じた無数の拡張機能(Extensions)が提供されていてとても便利です。

とても便利なんですが、使っているとときどき「あ、これできたら便利なのに拡張機能ないのか・・・」「これ手作業で書いてるけど自動でできたら便利だな・・・」のようにちょっとモヤモヤすること、ないでしょうか。

そんなVSCodeのかゆいところに手が届く拡張機能をDIYで開発する手順をご紹介します。
文筆家にとってのペン、料理人にとっての包丁のような、我々デベロッパーの商売道具を、自分で作った拡張機能で自分色に染めあげてみてはいかがでしょうか。(過言)

開発環境

$ volta -v && node -v && npm -v
1.1.1
v18.15.0
9.5.0

YEOMANとgenerator-codeのインストール

YEOMANは言語に依存しない汎用的な、プロジェクトのscaffoldingを行うツールです。
VSCode拡張機能を作るスタートラインとして、このYEOMAN(yo)と、YEOMAN用のジェネレータであるgenerator-codeを使います。

下記のコマンドでこれらをインストールします。

npm install -g yo generator-code

プロジェクトの作成

下記のコマンドでyoを実行するとプロジェクトの作成が始まります。

yo code

ここでヨーマンの登場。
プロジェクト作成のため、いろいろなオプションを訪ねてきます。

yo code

ここでは下記のように選択/入力してみました。

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? sample-extension
? What's the identifier of your extension? sample-extension
? What's the description of your extension? This extension is sample.
? Initialize a git repository? Yes
? Bundle the source code with webpack? Yes
? Which package manager to use? pnpm

いくつか補足しますと、

  • identifierはデフォルトで名称と同じになります。
  • descriptionはこの時点では未入力でもOKです。あとでpackage.jsonに追記できます。
  • webpackは拡張機能としてビルドするなら必須のようです。
  • nodeのパッケージマネージャはnpm、yarn、pnpmのいずれかから選択です。
  • 最後にOpen with codeskipかを選択できます。Open with codeを選択すると、プロジェクトディレクトリが別ウィンドウで開きます。

ここまでの作業でプロジェクトのひな形が作成されます。下記のような構造です。

directories

実行とデバッグ

ここでおもむろにF5キーをプッシュ、もしくはVSCodeの「実行とデバッグ」からRun Extensionをクリックします。

run extention

VSCodeの新しいウィンドウが起動しますので、Command + Shift + p でコマンドパレットを開き、
helloと入力します。

command pallete

Enterして「Hello World」を実行すると、なにやらメッセージが表示されました!

hello world

いま実行された拡張機能の本体はsrc/extension.tsです。ブレークポイントを設定するとデバッグすることもできます。

debugging

vscodeというライブラリがインポートされていて、ウィンドウ上にメッセージを表示させているのが読み取れます。
あとはこのファイルに必要な処理を実装していけばOKです。

関数とVSCodeのコマンドの紐付け

先ほどのソースを見ますと、'sample-extension.helloWorld'という名前でvscodecommandsにコマンドをregistしているのがわかります。

extension.ts(抜粋)
export function activate(context: vscode.ExtensionContext) {

  console.log('Congratulations, your extension "sample-extension" is now active!');

  // 👇の行
  let disposable = vscode.commands.registerCommand('sample-extension.helloWorld', () => {

    vscode.window.showInformationMessage('Hello World from sample-extension!');

  });

  context.subscriptions.push(disposable);
}

この名前はpackage.jsonに記述されていて、

package.json(抜粋)
  "contributes": {
    "commands": [
      {
        "command": "sample-extension.helloWorld", // <-ココ
        "title": "Hello World"
      }
    ]
  },

上記のように、"Hello World"という名称と、"sample-extension.helloWorld"というコマンドを紐づけています。
これは、

  • package.json
    • titlecommand名を紐付け
  • extension.ts
    • command名と実際の関数(function)を紐付け

のような流れで、VSCode上に表示されるコマンド名と、実際の処理を紐づけているという理解です。
したがって、もう1つ別の機能を追加しようと思った場合、

例えばpackage.jsonをこうして、

add package.json

実装をこうすると、

add function

こうできちゃうわけです。

new command

ここではコマンドパレットから実行できるようにcommandという指示を使いましたが、VSCodeの拡張機能開発ではこれらの指示はContribution Pointsと呼ばれ、他にも多数の種類が用意されています。
例えば、拡張機能をエディタ上で右クリックメニュー(コンテキストメニュー)から実行したい場合は次のようにします。

package.json(抜粋)
    "menus": {
      "editor/context": [
        {
          "when": "editorFocus",
          "command": "sample-extension.newCommand",
          "group": "myGroup@1"
        },

その他のContribution Pointsについては下記の公式ドキュメントをご参考ください。

多言語対応

拡張機能を日本語化するための方法を紹介します。

extension.tsがimportしているvscodeモジュールはvscode.env.languageというプロパティを持っており、実行環境の言語コードはここで知ることができます。
例えばなんらかのメッセージを表示する機能であれば、この言語コードを参照してメッセージを切り替えることはそれほど難しくありません。
ただしコマンド名そのものや、コンテキストメニュー上に表示されるラベルを切り替えたい場合、この方法ではちょっと難しいです。

メニューやタイトルの場合

まず、メニューや拡張機能のタイトルなど、package.jsonに記述する項目の多言語化です。

  • 項目のkeyを決める(commandName.helloWorldなど)
  • package.json内の該当の文字列を、%{key名}%に置き換える(%commandName.helloWorld%など)
  • package.nls.json、および対応したい言語のロケールごとにpackage.nls.{locale}.jsonを作る
  • それぞれのファイルにjson形式で、key:文字列のように表示したい文字列を書いていく
package.nls.json(サンプル)
{
  "commandName.helloWorld": "Hello World"
}
package.nls.ja.json(サンプル)
{
  "commandName.helloWorld": "こんにちは世界"
}
package.json(抜粋)
  "contributes": {
    "commands": [
      {
        "command": "sample-extension.helloWorld",
        "title": "%commandName.helloWorld%" // key名を"%"でかこんで対応させる
      }
    ]
  },

プログラム内(extension.ts)のテキスト

さらにこれらpackage.nls.{locale}.jsonの内容をプログラム中でも使ってみます。
こんなコードを書きます。

localeMap.ts
import * as vscode from 'vscode';
import localeDefault from '../package.nls.json';
import localeJa from '../package.nls.ja.json';

export type LocaleKeyType = keyof typeof localeDefault;

interface LocaleEntry
{
  [key: string]: string;
}
const localeTableKey = vscode.env.language;
const localeTable = Object.assign(localeDefault, ((<{[key : string] : LocaleEntry}>{
    ja : localeJa
})[localeTableKey] || { }));
const localeString = (key : string) : string => localeTable[key] || key;

export const localeMap = (key : LocaleKeyType) : string => localeString(key);

上記のコード、記述した時点ではいくつかエラーが出ていることかと思います。
そこでtsconfig.jsonを下記のように編集します。

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2020",
    "lib": [
      "ES2020"
    ],
    "sourceMap": true,
-    "rootDir": "src",
+    "rootDir": ".",  // ルートを1段上げて package.nls.json をソースに含めます。
+    "outDir": "dist",
    "strict": true,
+    "resolveJsonModule": true, // jsonファイルをモジュールとして扱えるようにします。
+    "esModuleInterop": true ,
-  }
+  },
+  "exclude": [ // ルートが1段上がったので、関係ないディレクトリをビルド対象から除外します。
+      ".vscode",
+      "node_modules",
+      "dist",
+      "./src/test/**/*",
+  ]
}

エラーは消えましたでしょうか。

先ほどのlocaleMap、2つの*.nls.jsonを読み込み、localeMap("{key名}")のようにして各jsonに書かれているテキストを読み出すことができるようになっています。
言語ロケールの判断はモジュール内で行いますので、自動的に現在の言語に応じたテキストが設定されます。

今回の場合はlocaleMap("command.message.helloWorld")のように指定すると、対応するテキストをコード中で利用できます。

以上の手順で、メニューやコマンド上の表記、プログラム内での利用と、ひととおりの局面で多言語対応ができそうです。

拡張機能のテスト

この記事ではpnpmを使うようにしましたので、下記のようにコマンドを実行します。

pnpm run test

初回の場合、もろもろライブラリやvscodeの実行環境がインストールされます。
しばらく待ちますと、一瞬VSCodeが別ウィンドウで開いたあと、テストが完了すると思います。

プロジェクト作成直後ではテストコードは以下のような内容で、サンプルのものが実装されています。

src/test/suite/extension.test.ts
import * as assert from 'assert';

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';

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

  test('Sample test', () => {
    assert.strictEqual(-1, [1, 2, 3].indexOf(5));
    assert.strictEqual(-1, [1, 2, 3].indexOf(0));
  });
});

実際の開発では、ここに必要なテストコードを実装していくことになります。

パッケージングとインストール

手元のVSCodeにインストールしたい場合はとりあえずパッケージングという処理が必要になります。
このパッケージングやマーケットプレイスへの公開にはvsceというツールを使います。

このツールで作成したvsixファイルを使い、手元のVSCodeへインストールすることができます。

が、その前にいくつか必要な作業があります。

vsceの実行環境を作る

まずはvsceを実行できる環境を作ります。

vsceはnpmのモジュールでもあるため、ふつうにnpm install -gでインストールすることも可能ですが、
ここではvsce実行用のDockerイメージを作成する方法を紹介します。

公式のドキュメントにしたがって以下のコマンドでDockerイメージをビルドします。

DOCKER_BUILDKIT=1 docker build --tag vsce "https://github.com/microsoft/vscode-vsce.git#main"

ビルドが完了したら、--versionオプションでコンテナの動作確認。うまく動作していれば、vsceのバージョンが出力されるはずです。

docker run --rm -it vsce --version

拡張機能をパッケージングするには下記のようにpackageサブコマンドを使いますが、

docker run --rm -it -v "$(pwd)":/workspace vsce package

ここまでの状態でvsce packageを実行すると下記のようなエラーが出力されるはずです。

 ERROR  It seems the README.md still contains template text. Make sure to edit the README.md file before you package or publish your extension.

このエラーはなんと「README.md がテンプレ文を含んでますよ」という怒られ方です。
package や publish をする前に、README.mdをちゃんと書きなさいよと言われております。

以降でREADMEの編集他、必要な作業を見ていきます。

READMEの編集

取り急ぎ、ということで下記のように編集します。

README.md
# sample-extension README

- This is the README for your extension "sample-extension". After writing up a brief description, we recommend including the following sections.
+ この拡張機能は自宅用です。

## Features

(以下略)

個人用途であればこれで十分です。とりあえずパッケージングできるようになります。

docker run --rm -it -v "$(pwd)":/workspace vsce package

公開する予定がある場合には、しかるべき内容をREADMEに記述しましょう。

package.jsonについて

README.mdを編集してパッケージングを実行すると、下記のようなwarningが出力される場合があります。

 WARNING  A 'repository' field is missing from the 'package.json' manifest file.
Do you want to continue? [y/N] 

これは、拡張機能のマニフェストファイルであるpackage.jsonの内容が不足していますよ、という警告です。
個人用途であれば特に無視してyを選択してしまっても問題ありませんが、公開する予定がある場合は内容をある程度埋めておく必要があると思います。

各項目の意味、用途については、下記の公式ドキュメントをご参考ください。

LICENSE ファイルについて

package.jsonについての警告を無視すると、次に下記のようなwarningが出力される場合があります。

 WARNING  LICENSE.md, LICENSE.txt or LICENSE not found
Do you want to continue? [y/N]

これは拡張機能のライセンシングについてのファイルがありませんよ、という警告です。
これも個人用途であれば無視してしまってかまいません。

公開する予定がある場合は、下記のような記事を参考にライセンスについて明記しておくのがよいと思います。
ルールとして、ファイル名はメッセージのとおりLICENSE.mdLICENSE.txtLICENSE(拡張子なし)のいずれかであればいいようです。

ちなみに、定型のライセンス条文を自動生成してくれるVSCodeの拡張機能もあります。

vsixファイルを使ったインストール

パッケージングが完了すると、vsixファイルが作成されます。
ファイル名はpackage.jsonの内容が使われ、{name}-{version}.vsixのようになります。

vsixファイルからのインストールはアクティビティバーの拡張機能から行います。

install from vsix

作成されたvsixファイルを選択すると拡張機能がインストールできます。

拡張機能の公開

拡張機能の公開までの手順は、以下の公式ドキュメントにまとめられています。ここでは大まかな流れだけを説明します。

  1. Azure DevOps のアカウントを作る
  2. Azure の Organization(組織)を作る
  3. Personal Access Token を発行する
  4. Visual Studio Code MarketPlace のアカウントを作る
  5. 作成した publisher name を package.jsonpublisherに記載する
  6. vsceから publisher name でログインする(3の Personal Access Tokenが必要)
  7. vsceでpublishする

1 から 4 の手順がやや面倒ですが、一度アカウントを作成してしまえばあとは繰り返し publish することができます。
この手順以外にも、MarketPlace上で直接vsixファイルをアップロードする方法もあるようです。

また、Azure DevOps で発行する Personal Access Token には有効期限が設定されていて、期限が切れると利用できなくなります。
そういった場合は同様の手順で、 Azure DevOps から新しいトークンを発行してください。

GitHub Actions を使ったCD/CI

最後に、GitHub Actionsを使ったデプロイ、ついでに自動テストについてですが、
私は下記の記事を参考にさせていただきました。ここでは記事の紹介にとどめさせていただきます。(そのままでいけてしまった)

蛇足ですが、弊リポジトリの例も併せて貼っておきます。

CHANGELOG のこと

CHANGELOG.mdは拡張機能の更新履歴です。VSCodeのマーケットプレイスでは、下のように表示されるようになります。

change log

個人用途であればそこまで気にする必要はありませんが、公開する予定のある場合は、リリースごとにこまめに記載するようにした方がいいと思います。
プロジェクト作成直後は以下のような内容になっており、「注目すべきすべての更新はこのファイルに書いておいてね」「このファイルの書き方はリンク先に書いてあるからよく読むことをおすすめするよ」と書いてあります。

# Change Log

All notable changes to the "sample-extension" extension will be documented in this file.

Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.

## [Unreleased]

- Initial release

まとめ

長くなりましたが、以上です。
今回は、「こんなちょっとした機能があったら便利なのにな〜」という思いつきをきっかけにVSCodeの拡張機能を作ってみて、その全体の流れをご紹介しました。

なにか1つでも参考にしていただければ幸いです。
最後まで読んでいただきありがとうございました。

ではまた!

参考記事

Discussion