🦦

社内でのみ共有したい自作VS Code拡張機能のREADMEで画像とMermaidの図が表示できない問題を解決した

2024/11/12に公開

(お急ぎの方は「Base64エンコード自動化スクリプト」まで飛んでください。)
この記事はVS Code拡張機能の概要を把握していることを前提としています。入門向けの記事はきっと今後公開する予定です。

VS Code Meetup #32 - VS Code x Copilot - connpassでLTした内容の詳細を記したものになります。
アーカイブはこちら(一番最後に登壇しています)(枠が空いていて前日夜に登壇を決めたので粗い)↓

https://www.youtube.com/watch?v=UKpbIv0_J4A

はじめに

自作のVS Code拡張機能はVS Code Marketplaceに公開するだけでなく、vsixという形式のパッケージファイルで共有することもできます。

パブリックに公開せず、社内でミニマムに共有したい場合 vsix ファイルをプライベートな共有ドライブかどこかに置いて、各自でダウンロードしてインストールする、という手段をとることができます。(※別の方法があればコメントください)

その際、vsixからインストールした拡張機能のREADMEで
「画像、gifが表示されない」
「Mermaidがそのまま表示される(描画されない)」
といった問題が発生しました。

公式ドキュメントで拡張機能の発行に関する記事(Publishing Extensions | Visual Studio Code Extension API)を見てもこの辺りの情報は見当たらなかったので、色々頑張って解決した方法を共有します。
VS Code Meetupでは「力技」と言われました。

問題の概要

READMEで画像を表示する正攻法

画像表示は通常のMarkdown記法で記述します。

![画像の説明](images/image.png)

しかし、VS Code拡張機能のREADMEで画像を表示させる場合、参照先の画像がパブリックな場所にある必要があります。

詳細には、package.jsonrepositoryフィールドで指定したリポジトリのURL内に画像を配置し、そのURLからの相対パスが画像パスになります。

package.json
{
  "repository": {
    "type": "git",
    "url": "{パブリックなリポジトリのURL}"
  },
  ...

例えばREADMEで以下のように記述した場合、どうなるでしょうか。
ローカルでは、パブリックに公開したくない画像 images/private.png とパブリックに公開もされている画像images/public.jpgが存在するものとします。

package.json
{
  "repository": {
    "type": "git",
    "url": "https://github.com/yuma-722/test"
    },
    ...
README.md
パブリックに公開したくない画像:images/private.png

![相対パス](images/private.png)

パブリックに公開している画像:images/public.jpg

![パブリックな画像の相対パス](images/public.jpg)

パブリックに公開している画像:https://github.com/yuma-722/test/raw/HEAD/images/public.jpg

![パブリックな画像のフルパス](https://github.com/yuma-722/test/raw/HEAD/images/public.jpg)

この場合、次のようになります

  • ローカルで編集中のMarkdownプレビュー
    • 「相対パス」「パブリックな画像の相対パス」はローカルのファイルを参照して表示される
    • 「パブリックな画像のフルパス」はURLから画像を取得して表示される

ローカルでのプレビュー

  • vsix ファイルから拡張機能をインストールした場合のREADME
    • 「パブリックな画像の相対パス」「パブリックな画像のフルパス」のみ表示される
      • ローカルにのみ存在する「相対パス」は表示されない
    • 「package.jsonのrepositoryフィールドで指定したリポジトリのURL」+「相対パス」に画像が存在する場合(つまりパブリックな場所に画像が存在する場合)は「相対パス」の画像が表示される

vsixでのREADME

この仕様だと、相対パスで記述した場合にローカルでREADMEを書いているときのMarkdownプレビューとパッケージ化後で参照している画像が異なる可能性があるので非常に不便ですね…。ローカル編集中は見えていたのに!という事態もあり得ます。

なぜパブリックに公開された画像でのみ画像が表示されるのか

パッケージ化するときに画像も入っていれば参照されるんじゃないの?と思ったのでなぜこんなことになるのか調べてみました。

この理由を解明するために、vsix ファイルの中身を見てみましょう。
vsix ファイルは、拡張子を.zipに変更して解凍することで中身を確認できます。
一般的には次のような構成になっています。

vsix
├── extension
│   ├── package.json
│   ├── README.md
│   ├── out
│   │   └── extension.js
│   ├── src
│   │   └── extension.ts
│   ├── images
│   │   └── icon.png
|   |   └── 240718_AISpeech.jpg
│   └── node_modules
│       └── (dependencies)
├── extension.vsixmanifest
└── [Content_Types].xml

この構造であればimagesフォルダ内の画像を参照してくれてもいいものなのですが、実際にはしてくれません。
この中のREADME.mdをみてみましょう。

パッケージ化後のREADME.md
パブリックに公開したくない画像:images/private.png

![相対パス](https://github.com/yuma-722/test/raw/HEAD/images/private.png)

パブリックに公開している画像:images/public.jpg

![パブリックな画像の相対パス](https://github.com/yuma-722/test/raw/HEAD/images/public.jpg)

パブリックに公開している画像:https://github.com/yuma-722/test/raw/HEAD/images/public.jpg

![パブリックな画像のフルパス](https://github.com/yuma-722/test/raw/HEAD/images/public.jpg)

vcse package コマンドが実行されるとREADME.mdの画像パスの先頭に「packege.jsonrepositoryフィールドで指定したリポジトリのURL+/raw/HEAD」が付け加えられます。
そのため、その結果のURLに画像が存在しているもののみが表示されていたのです。

vcse package コマンド実行後にこのURLを手動で削除して相対パスに戻せばvsixファイル内の画像を参照できるのでは!と思いやってみましたが、残念ながらダメでした。
READMEのiconはvsixパッケージのimagesフォルダ内を参照してくれるくせに…なんで…。

解決方法

まずはこのアイディアをくださった@hori__hiroさんに感謝を申し上げます。

https://x.com/hori__hiro/status/1839269492117090330

画像をBase64エンコードしてREADMEに記述する

× 画像をBase64エンコードしたもので画像パスを置き換えるだけではダメ

単純にREADMEの画像パスの部分を、画像をBase64に変換したものに置き換えるだけでは画像が表示されませんでした。

これは先ほどの「vcse package コマンド実行後にREADME.mdの画像パスの先頭にpackege.jsonrepositoryフィールドのURLが付け加えられてしまう問題」と同じことが発生するためです。

さらに、もとのREADME.mdで画像パスをBase64エンコード文字列に置き換えるとお察しの通りREADMEがめちゃくちゃ更新しにくくなります。
画像表示の表記の度に下記のような呪文が何行も続きます。

Base64に変換した画像データ

〇 パッケージ化した後に手を加える

画像をBase64に変換すれば表示はできそうだったので、パッケージ化した後に手を加えることにしました。
手順は次の通りです。

1. vsixファイルをzipにして解凍
2. 正規表現でREADME内の画像表記を探し、実画像をBase64エンコードしたもので画像パスを置き換え
3. 再zip化
4. 拡張子をvsixに変更

この手順で画像とgifは表示できました。
Mermaidについては、Base64エンコードまでにひと手間加える必要があります。

1. Mermaidの図を画像に変換
2. 画像をBase64に変換

このあたりの処理をパッケージ化とともに自動で行えるようにスクリプトを作成してコマンド一つで完結するようにしました。

  • パッケージ化した後のREADMEで画像パスをBase64エンコード文字列に置き換えているため、もとのREADMEは影響を受けない
  • READMEは普通のMarkdownと同じ書き心地になる
  • Markdownプレビューで拡張機能としてインストール後のREADMEと同じ場所にある画像を表示して確認できる

というのが個人的に満足しているポイントです。
次のセクションで今回作成したスクリプトを紹介します。

Base64エンコード自動化スクリプト

私はJavaScript初心者なのでほとんどGitHub Copilotに書いてもらいました。

前提となるディレクトリ構成

このスクリプトの前提となる自作拡張機能のディレクトリ構成(簡略化したもの)は次の通りです。scriptsフォルダの下にpostPackage.js(今回のスクリプトファイル), imagesフォルダの下に画像ファイルを配置している前提でpostPackege.js内の正規表現を書いています。

VS Code 拡張機能のroot/
├── package.json
├── scripts/
│   └── postPackage.js
└──images/
    └── example.png

前提となるパッケージ

パッケージが不足していると、スクリプトが正常に動作しない場合があります。
正常に動作しない場合は必要なパッケージが足りているかをご確認ください。

  1. vsce
    vsixファイルを作成(自作拡張機能をパッケージ化)するために使用します。検証時にはバージョン3.2.1を使用しています。
    バージョンによってオプション引数が異なるため、オプションでエラーになった場合は packege.json 内のvsce packageのオプションを修正するかvsceのバージョンを変更するかでご対応ください。

https://github.com/yuma-722/demo/blob/main/vscodemeetup-internal-extension-readme/chat-sample/package.json#L150-L156

  1. mmdc(@mermaid-js/mermaid-cli)

Mermaidの図を画像に変換するために使用しています。検証時にはバージョン11.4.0を使用しています。

 npm install -g @mermaid-js/mermaid-cli@11.4.0

スクリプトのフロー図

スクリプトのフロー図は次の通りです。(GitHub Copilotに書いてもらいました)

スクリプトのフロー図

スクリプト

こちらのVS Code公式のサンプルをベースにしています。

https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample

今回の検証のために主に変更を加えているのは以下の2つのファイルです。

  • package.json
    • npm run packageでVS Code拡張機能のパッケージ化とBase64エンコードを完了するための設定

https://github.com/yuma-722/demo/blob/main/vscodemeetup-internal-extension-readme/chat-sample/package.json#L150-L156

  • scripts/postPackage.js
    • vsce package後に実行されるBase64エンコードのスクリプト

https://github.com/yuma-722/demo/blob/main/vscodemeetup-internal-extension-readme/chat-sample/scripts/postPackage.js

  • 実際に実行するコマンドは npm run package
    • コマンド一つでVS Code拡張機能のパッケージ化とBase64エンコードが完了
  • npm run package実行後に 「VSIXパッケージがBase64画像で更新されました。」 と表示されれば成功
  • 実行後に private-readme-sample-0.1.0 というフォルダが残るが、これはBase64エンコード処理完了後の private-readme-sample-0.1.0.vsix を解凍したもの
    • このフォルダ内のREADMEを見ることでpostPackage.jsの処理が正常に行われているか確認できる
  • トラブルシューティング
    • 「vsixファイルはできているのにBase64エンコードができていないので画像が表示されない」という場合は必要なモジュールが足りていない場合があるため、エラーメッセージを確認
    • 今後のvcseの更新でオプションが効かなくなる場合もあるので、その際はエラーメッセージを参照して対処

補足:vsixから拡張機能のインストール・READMEの確認・アンインストール

vsixから拡張機能をインストール

VS CodeでCtrl + Shift + Pを押して、Extensions: Install from VSIX...を選択して、インストールしたいvsixファイルを選択します。

vsixでインストール

vsixからインストールした拡張機能のREADME確認方法

vsixファイルからインストールした拡張機能は、拡張機能の検索バーで@installed {package.jsonのdisplayName}と入力することで確認できます。

拡張機能の検索

vsixからインストールした拡張機能のアンインストール方法

拡張機能のREADMEからアンインストールすることもできますが、コマンドでアンインストールすることもできます。

まず以下のコマンドでインストール済み拡張機能のID一覧を出力して、削除したい拡張機能のIDを確認します。

code --list-extensions

出力された結果から、アンインストールしたい拡張機能のIDをコピーして、以下のコマンドでアンインストールします。

code --uninstall-extension 拡張機能のID

例えば、code --uninstall-extension undefined_publisher.chat-sample のような形式です。

おわりに

自動化してやる…コマンド一つで終わらせてやる…という執念でここまで来ました。
GitHub Copilotのおかげで素人でもここまでできたので、GitHub Copilotさまさまです。

とはいえ、この方法は公式でサポートされているものではないので、より良い方法をご存知の方はコメントで教えていただけると幸いです。
VS Codeのissueを見ると同じお悩みの人が結構いるのに公式に解決してもらえない不思議。

GitHub Copilotを拡張したい人が増えたり、GitHub Copilotのおかげで拡張機能作成のハードルが下がったりで(私のように)今後拡張機能開発に手を出す人が増えるのではと思っているので、そんな方の助けになれば幸いです。

Discussion