エージェント型開発を探求する -

Web 拡張機能

Visual Studio Codeはブラウザー上でエディターとして実行できます。一例として、GitHubでリポジトリやプルリクエストを閲覧中に「.」(ピリオドキー)を押すことで起動する github.dev ユーザーインターフェイスがあります。VS CodeをWeb上で使用する場合、インストールされた拡張機能は、ブラウザー内の「Web拡張機能ホスト(web extension host)」と呼ばれる拡張機能ホストで実行されます。このWeb拡張機能ホストで実行可能な拡張機能は、「Web拡張機能(web extension)」と呼ばれます。

Web拡張機能は通常の拡張機能と同じ構造を共有していますが、ランタイムが異なるため、Node.jsランタイム用に書かれた拡張機能と同じコードは実行できません。Web拡張機能は引き続き完全なVS Code APIにアクセスできますが、Node.js APIやモジュール読み込みは利用できません。その代わり、Web拡張機能はブラウザーのサンドボックスによって制限されており、通常の拡張機能と比較していくつかの制約があります。

Web拡張機能のランタイムは、VS Codeデスクトップでもサポートされています。拡張機能をWeb拡張機能として作成すれば、VS Code for the Webvscode.devgithub.devを含む)だけでなく、デスクトップ環境やGitHub Codespacesのようなサービス上でも動作します。

Web拡張機能の構成

Web拡張機能は、通常の拡張機能と同様の構造をしています。拡張機能マニフェスト(package.json)で、拡張機能のソースコードのメインエントリファイルと、拡張機能の寄与(contributions)を定義します。

Web拡張機能の場合、メインエントリファイルは、通常の拡張機能のような main プロパティではなく、browser プロパティによって定義されます。

contributes プロパティは、Web拡張機能と通常の拡張機能のどちらでも同じように動作します。

以下の例は、Web拡張機能ホストでのみ実行される(browser エントリポイントのみを持つ)シンプルなHello World拡張機能の package.json です。

{
  "name": "helloworld-web-sample",
  "displayName": "helloworld-web-sample",
  "description": "HelloWorld example for VS Code in the browser",
  "version": "0.0.1",
  "publisher": "vscode-samples",
  "repository": "https://github.com/microsoft/vscode-extension-samples/helloworld-web-sample",
  "engines": {
    "vscode": "^1.74.0"
  },
  "categories": ["Other"],
  "activationEvents": [],
  "browser": "./dist/web/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "helloworld-web-sample.helloWorld",
        "title": "Hello World"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "npm run package-web",
    "compile-web": "webpack",
    "watch-web": "webpack --watch",
    "package-web": "webpack --mode production --devtool hidden-source-map"
  },
  "devDependencies": {
    "@types/vscode": "^1.59.0",
    "ts-loader": "^9.2.2",
    "webpack": "^5.38.1",
    "webpack-cli": "^4.7.0",
    "@types/webpack-env": "^1.16.0",
    "process": "^0.11.10"
  }
}

注意: 拡張機能がVS Codeバージョン1.74より前を対象としている場合は、activationEventsonCommand:helloworld-web-sample.helloWorld を明示的に記述する必要があります。

main エントリポイントのみを持ち、browser を持たない拡張機能はWeb拡張機能ではありません。これらはWeb拡張機能ホストによって無視され、拡張機能ビューからダウンロードすることはできません。

Extensions view

宣言的な寄与のみを持つ拡張機能(mainbrowser を含まず、contributes のみを持つもの)はWeb拡張機能になり得ます。これらは、拡張機能の作成者側で修正を加えることなく、VS Code for the Web にインストールして実行できます。宣言的な寄与を持つ拡張機能の例としては、テーマ、文法定義、スニペットなどがあります。

ブラウザーとNode.jsランタイムの両方で実行するために、browsermain の両方のエントリポイントを持つ拡張機能も作成可能です。既存の拡張機能をWeb拡張機能に更新するセクションでは、両方のランタイムで動作するように拡張機能を移行する方法を説明しています。

Web拡張機能の有効化セクションには、拡張機能がWeb拡張機能ホストで読み込めるかどうかを判断するためのルールが記載されています。

Web拡張機能のメインファイル

Web拡張機能のメインファイルは browser プロパティによって定義されます。このスクリプトは、Web拡張機能ホスト内の Browser WebWorker 環境で実行されます。ブラウザーのワーカーサンドボックスによる制限を受けるため、Node.jsランタイムで動作する通常の拡張機能と比較して制約があります。

  • 他のモジュールのインポートやrequireはサポートされていません。importScripts も使用できません。そのため、コードは単一のファイルにパッケージ化する必要があります。
  • VS Code APIrequire('vscode') というパターンで読み込むことができます。require のためのshim(シム)が存在するため動作しますが、このshimを使用して他の拡張機能ファイルや追加のNodeモジュールを読み込むことはできません。これは require('vscode') とのみ機能します。
  • processossetImmediatepathutilurl などのNode.jsのグローバル変数やライブラリは、実行時には利用できません。ただし、webpackなどのツールを使用して追加することは可能です。webpack設定セクションでその方法を説明しています。
  • 開かれたワークスペースやフォルダーは仮想ファイルシステム上にあります。ワークスペース内のファイルにアクセスするには、vscode.workspace.fs で利用可能な VS Code ファイルシステム APIを経由する必要があります。
  • Extension context の場所(ExtensionContext.extensionUri)やストレージの場所(ExtensionContext.storageUriglobalStorageUri)も仮想ファイルシステム上にあるため、vscode.workspace.fs を介する必要があります。
  • Webリソースにアクセスするには、Fetch APIを使用する必要があります。アクセスするリソースは Cross-Origin Resource Sharing (CORS) に対応している必要があります。
  • 子プロセスの作成や実行ファイルの実行は不可能です。ただし、Worker APIを通じてWebワーカーを作成することは可能です。これは、Web拡張機能におけるLanguage Server Protocolセクションで説明されているように、言語サーバーを実行するために使用されます。
  • 通常の拡張機能と同様に、拡張機能の activate/deactivate 関数は exports.activate = ... というパターンでエクスポートされる必要があります。

Web拡張機能の開発

幸いなことに、TypeScriptやwebpackなどのツールはブラウザーランタイムの多くの制約を隠蔽し、通常の拡張機能と同じ方法でWeb拡張機能を作成できるようにしてくれます。多くの場合、Web拡張機能と通常の拡張機能の両方を同じソースコードから生成できます。

例えば、yo code ジェネレーター によって作成された Hello Web Extension は、ビルドスクリプトのみが異なります。Debug: Select and Start Debugging コマンドからアクセスできるlaunch構成を使用して、従来のNode.js拡張機能と全く同じように生成された拡張機能を実行およびデバッグできます。

Web拡張機能の作成

新しいWeb拡張機能をスキャフォールディング(雛形作成)するには、yo code を使用して New Web Extension を選択します。generator-code の最新バージョン(>= generator-code@1.6)がインストールされていることを確認してください。ジェネレーターとyoを更新するには、npm i -g yo generator-code を実行します。

作成される拡張機能は、拡張機能のソースコード(Hello World通知を表示するコマンド)、package.json マニフェストファイル、そしてwebpackまたはesbuildの設定ファイルで構成されています。

話を単純にするため、ここではバンドラーとして webpack を使用することを前提とします。記事の最後で、esbuild を選択した場合の違いについても説明します。

  • src/web/extension.ts は、拡張機能のメインソースコードファイルです。通常のhello拡張機能と同一です。
  • package.json は拡張機能のマニフェストです。
    • browser プロパティを使用してメインファイルを示します。
    • コンパイル、監視、パッケージ化を行うための compile-webwatch-webpackage-web スクリプトを提供します。
  • webpack.config.js は、拡張機能のソースを単一のファイルにコンパイルおよびバンドルするwebpackの設定ファイルです。
  • .vscode/launch.json には、VS Codeデスクトップ上でWeb拡張機能ホストを使用してWeb拡張機能およびテストを実行するためのlaunch構成が含まれています(extensions.webWorker 設定は不要になりました)。
  • .vscode/task.json には、launch構成で使用されるビルドタスクが含まれています。これは npm run watch-web を使用し、webpack固有の ts-webpack-watch 問題マッチャーに依存します。
  • .vscode/extensions.json には、問題マッチャーを提供する拡張機能が含まれています。launch構成を動作させるには、これらの拡張機能がインストールされている必要があります。
  • tsconfig.json は、webworker ランタイムに合わせたコンパイルオプションを定義します。

helloworld-web-sample のソースコードは、ジェネレーターによって作成されるものと類似しています。

Webpack設定

webpackの設定ファイルは yo code によって自動生成されます。これは拡張機能のソースコードを単一のJavaScriptファイルにバンドルし、Web拡張機能ホストに読み込めるようにします。

後ほどバンドラーとしてesbuildを使用する方法を説明しますが、まずはwebpackから始めます。

webpack.config.js

const path = require('path');
const webpack = require('webpack');

/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const webExtensionConfig = {
  mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
  target: 'webworker', // extensions run in a webworker context
  entry: {
    extension: './src/web/extension.ts', // source of the web extension main file
    'test/suite/index': './src/web/test/suite/index.ts' // source of the web extension test runner
  },
  output: {
    filename: '[name].js',
    path: path.join(__dirname, './dist/web'),
    libraryTarget: 'commonjs',
    devtoolModuleFilenameTemplate: '../../[resource-path]'
  },
  resolve: {
    mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules
    extensions: ['.ts', '.js'], // support ts-files and js-files
    alias: {
      // provides alternate implementation for node module and source files
    },
    fallback: {
      // Webpack 5 no longer polyfills Node.js core modules automatically.
      // see https://webpack.dokyumento.jp/configuration/resolve/#resolvefallback
      // for the list of Node.js core module polyfills.
      assert: require.resolve('assert')
    }
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'ts-loader'
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.ProvidePlugin({
      process: 'process/browser' // provide a shim for the global `process` variable
    })
  ],
  externals: {
    vscode: 'commonjs vscode' // ignored because it doesn't exist
  },
  performance: {
    hints: false
  },
  devtool: 'nosources-source-map' // create a source map that points to the original source file
};
module.exports = [webExtensionConfig];

webpack.config.js の重要なフィールドをいくつか挙げます。

  • entry フィールドには、拡張機能とテストスイートへのメインエントリポイントが含まれます。
    • 拡張機能のエントリポイントを適切に示すように、このパスを調整する必要がある場合があります。
    • 既存の拡張機能の場合、このパスを package.jsonmain で現在使用しているファイルに向けてから始めることができます。
    • テストをパッケージ化する必要がない場合は、テストスイートのフィールドを省略できます。
  • output フィールドは、コンパイルされたファイルがどこに配置されるかを示します。
    • [name]entry で使用されるキーに置き換えられます。そのため、生成された設定ファイルでは dist/web/extension.js および dist/web/test/suite/index.js が生成されます。
  • target フィールドは、コンパイルされたJavaScriptファイルが実行される環境の種類を示します。Web拡張機能の場合、これは webworker にする必要があります。
  • resolve フィールドには、ブラウザーで動作しないNodeライブラリに対してエイリアスやフォールバックを追加する機能があります。
    • path のようなライブラリを使用している場合、Webコンパイル環境で path をどのように解決するかを指定できます。例えば、path: path.resolve(__dirname, 'src/my-path-implementation-for-web.js') のように、プロジェクト内の path を定義するファイルを指定できます。あるいは、BrowserifyのNode用パッケージである path-browserify を使用し、path: require.resolve('path-browserify') を指定することも可能です。
    • Node.jsコアモジュールのポリフィルのリストについては、webpack resolve.fallback を参照してください。
  • plugins セクションでは、DefinePluginプラグインを使用して、process といったNode.jsグローバル変数をポリフィルします。

Web拡張機能をテストする

マーケットプレイスに公開する前にWeb拡張機能をテストする方法は、現在3つあります。

  • --extensionDevelopmentKind=web オプションを付けてデスクトップ版VS Codeを実行し、VS Code内でWeb拡張機能ホストを起動してテストします。
  • @vscode/test-web nodeモジュールを使用して、ローカルサーバーから提供される拡張機能を含んだブラウザー上のVS Code for the Webを開きます。
  • 拡張機能を vscode.devサイドロード(直接読み込み) して、実際の環境で動作を確認します。

デスクトップ版VS CodeでのWeb拡張機能テスト

既存のVS Code拡張機能開発体験を利用するため、デスクトップ版VS Codeは通常のNode.js拡張機能ホストと並行してWeb拡張機能ホストを実行することをサポートしています。

New Web Extension ジェネレーターが提供する pwa-extensionhost launch構成を使用します。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Web Extension in VS Code",
      "type": "pwa-extensionHost",
      "debugWebWorkerHost": true,
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}",
        "--extensionDevelopmentKind=web"
      ],
      "outFiles": ["${workspaceFolder}/dist/web/**/*.js"],
      "preLaunchTask": "npm: watch-web"
    }
  ]
}

これは npm run watch-web を呼び出して拡張機能をコンパイルするタスク npm: watch-web を使用します。そのタスクは tasks.json に定義されている必要があります。

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "script": "watch-web",
      "group": "build",
      "isBackground": true,
      "problemMatcher": ["$ts-webpack-watch"]
    }
  ]
}

$ts-webpack-watch は、webpackツールからの出力を解析できる問題マッチャーです。これは TypeScript + Webpack Problem Matchers 拡張機能によって提供されます。

起動した Extension Development Host インスタンス内で、Web拡張機能がWeb拡張機能ホスト上で動作します。Hello World コマンドを実行して拡張機能をアクティブ化してください。

Running Extensions ビュー(コマンド: Developer: Show Running Extensions)を開いて、どの拡張機能がWeb拡張機能ホストで実行されているかを確認できます。

@vscode/test-web を使用したブラウザーでのテスト

@vscode/test-web nodeモジュールは、ブラウザーでWeb拡張機能をテストするためのCLIとAPIを提供します。

このnodeモジュールは、コマンドラインからVS Code for the Webを開くことができるnpmバイナリ vscode-test-web を提供します。

  • これは、VS CodeのWeb用バイナリを .vscode-test-web にダウンロードします。
  • localhost:3000 でローカルサーバーを起動します。
  • ブラウザー(Chromium、Firefox、またはWebkit)を開きます。

コマンドラインから直接実行できます。

npx @vscode/test-web --extensionDevelopmentPath=$extensionFolderPath $testDataPath

もしくは、より良い方法として @vscode/test-web を拡張機能のdevelopment依存関係として追加し、スクリプト内で呼び出すことも可能です。

  "devDependencies": {
    "@vscode/test-web": "*"
  },
  "scripts": {
    "open-in-browser": "vscode-test-web --extensionDevelopmentPath=. ."
  }

その他のCLIオプションについては @vscode/test-web README を参照してください。

オプション 引数 説明
--browserType 起動するブラウザー: chromium (デフォルト)、firefox、または webkit
--extensionDevelopmentPath 開発中の拡張機能へのパスを含めます。
--extensionTestsPath 実行するテストモジュールへのパス。
--permission 開いたブラウザーに与える権限: 例: clipboard-read, clipboard-write
オプションの完全リストを参照してください。引数は複数回指定可能です。
--folder-uri VS Codeを開くワークスペースのURI。folderPath が指定されている場合は無視されます。
--extensionPath 追加で含める拡張機能が入ったフォルダーへのパス。
引数は複数回指定可能です。
folderPath VS Codeを開くローカルフォルダー。
フォルダーの内容は仮想ファイルシステムとして利用可能になり、ワークスペースとして開かれます。

VS CodeのWebバイナリは .vscode-test-web フォルダーにダウンロードされます。これは .gitignore に追加しておくのが望ましいです。

vscode.dev でのWeb拡張機能のテスト

VS Code for the Webを利用する全ユーザー向けに公開する前に、実際の vscode.dev 環境で拡張機能がどのように振る舞うかを確認できます。

vscode.dev 上で自分の拡張機能を確認するには、まず自分のマシンからホストして、vscode.dev がダウンロードおよび実行できるようにする必要があります。

まず、mkcert をインストールする必要があります。

次に、失くさない場所(例: $HOME/certs)に localhost.pemlocalhost-key.pem ファイルを生成します。

$ mkdir -p $HOME/certs
$ cd $HOME/certs
$ mkcert -install
$ mkcert localhost

その後、拡張機能のパスから npx serve を実行してHTTPサーバーを起動します。

$ npx serve --cors -l 5000 --ssl-cert $HOME/certs/localhost.pem --ssl-key $HOME/certs/localhost-key.pem
npx: installed 78 in 2.196s

   ┌────────────────────────────────────────────────────┐
   │                                                    │
   │   Serving!                                         │
   │                                                    │
   │   - Local:            https://:5000       │
   │   - On Your Network:  https://172.19.255.26:5000   │
   │                                                    │
   │   Copied local address to clipboard!               │
   │                                                    │
   └────────────────────────────────────────────────────┘

最後に、vscode.dev を開き、コマンドパレット(⇧⌘P (Windows, Linux Ctrl+Shift+P))から Developer: Install Extension From Location... を実行し、上記URL(例: https://:5000)を貼り付けて Install を選択します。

ログを確認する

ブラウザーの開発者ツールのコンソールでログを確認し、拡張機能からのエラー、ステータス、ログを見ることができます。

vscode.dev 自体からの他のログが表示されることもあります。また、vscode.dev では簡単にブレークポイントを設定したり、拡張機能のソースコードを確認したりすることはできません。これらの制限により、vscode.dev でのデバッグは必ずしも快適ではないため、vscode.dev にサイドロードする前に最初の2つの方法を使用してテストすることを強く推奨します。サイドロードは、拡張機能を公開する前の最終的な健全性チェックとして適しています。

Web拡張機能のテスト

Web拡張機能のテストはサポートされており、通常の拡張機能テストと同様に実装できます。テストの基本構造については Testing Extensions 記事を参照してください。

@vscode/test-web nodeモジュールは @vscode/test-electron(旧 vscode-test)と同等です。Chromium、Firefox、Safari上でコマンドラインから拡張機能テストを実行できます。

このユーティリティは以下のステップを実行します:

  1. ローカルWebサーバーからVS Code for the Webエディターを起動します。
  2. 指定されたブラウザーを開きます。
  3. 提供されたテストランナースクリプトを実行します。

継続的ビルドでテストを実行し、すべてのブラウザーで拡張機能が機能することを確認できます。

テストランナースクリプトは、Web拡張機能のメインファイルと同じ制限を持つWeb拡張機能ホスト上で実行されます。

  • すべてのファイルは単一ファイルにバンドルされます。これにはテストランナー(例えばMocha)とすべてのテスト(通常は *.test.ts)が含まれる必要があります。
  • require('vscode') のみがサポートされています。

yo code Web拡張機能ジェネレーターによって作成される webpack設定 には、テスト用のセクションがあります。これは ./src/web/test/suite/index.ts にテストランナースクリプトがあることを想定しています。提供されている テストランナースクリプト はWeb版のMochaを使用し、すべてのテストファイルをインポートするためのwebpack固有の構文を含んでいます。

require('mocha/mocha'); // import the mocha web build

export function run(): Promise<void> {
  return new Promise((c, e) => {
    mocha.setup({
      ui: 'tdd',
      reporter: undefined
    });

    // bundles all files in the current directory matching `*.test`
    const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r);
    importAll(require.context('.', true, /\.test$/));

    try {
      // Run the mocha test
      mocha.run(failures => {
        if (failures > 0) {
          e(new Error(`${failures} tests failed.`));
        } else {
          c();
        }
      });
    } catch (err) {
      console.error(err);
      e(err);
    }
  });
}

コマンドラインからWebテストを実行するには、以下を package.json に追加し、npm test で実行します。

  "devDependencies": {
    "@vscode/test-web": "*"
  },
  "scripts": {
    "test": "vscode-test-web --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/index.js"
  }

テストデータを含むフォルダー上でVS Codeを開くには、最後のパラメーターとしてローカルフォルダーパス(folderPath)を渡します。

VS Code (Insiders) デスクトップ上で拡張機能テストを実行(およびデバッグ)するには、Extension Tests in VS Code launch構成を使用します。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Extension Tests in VS Code",
      "type": "extensionHost",
      "debugWebWorkerHost": true,
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}",
        "--extensionDevelopmentKind=web",
        "--extensionTestsPath=${workspaceFolder}/dist/web/test/suite/index"
      ],
      "outFiles": ["${workspaceFolder}/dist/web/**/*.js"],
      "preLaunchTask": "npm: watch-web"
    }
  ]
}

Web拡張機能を公開する

Web拡張機能は、他の拡張機能と同様に Marketplace でホストされます。

拡張機能を公開する際は、必ず最新バージョンの vsce を使用してください。vsce はすべてのWeb拡張機能にタグ付けを行います。そのため、vsceWeb拡張機能の有効化 セクションに記載されているルールを使用します。

既存の拡張機能をWeb拡張機能に更新する

コードを含まない拡張機能

コードを含まず、寄与ポイントのみを持つ拡張機能(例: テーマ、スニペット、基本的な言語拡張機能)は、修正の必要はありません。Web拡張機能ホストで実行でき、拡張機能ビューからインストール可能です。

再公開は必須ではありませんが、新しいバージョンを公開する際は、最新バージョンの vsce を使用していることを確認してください。

コードを含む拡張機能を移行する

ソースコードを含む拡張機能(main プロパティで定義されているもの)は、Web拡張機能のメインファイルを提供し、package.jsonbrowser プロパティを設定する必要があります。

拡張機能のコードをブラウザー環境用に再コンパイルするには、以下の手順に従います。

  • webpack設定セクションに示すように、webpack設定ファイルを追加します。Node.js拡張機能コード用に既にwebpackファイルがある場合は、Web用に新しいセクションを追加できます。vscode-css-formatter を例として参照してください。
  • Web拡張機能のテストセクションに示すように、launch.json および tasks.json ファイルを追加します。
  • webpack設定ファイルで、入力ファイルを既存のNode.jsメインファイルに設定するか、Web拡張機能用に新しいメインファイルを作成します。
  • package.json に、Web拡張機能の構成セクションに示すように、browser プロパティと scripts プロパティを追加します。
  • npm run compile-web を実行してwebpackを呼び出し、拡張機能をWebで動作させるために修正が必要な箇所を確認します。

可能な限りソースコードを再利用するために、いくつかの手法があります。

  • path などのNode.jsコアモジュールをポリフィルするには、resolve.fallback にエントリを追加します。
  • process のようなNode.jsグローバル変数を提供するには、DefinePluginプラグインを使用します。
  • ブラウザーとNodeランタイムの両方で動作するNodeモジュールを使用します。Nodeモジュールは、browsermain の両方のエントリポイントを定義することでこれを行うことができます。Webpackはターゲットに一致するものを自動的に使用します。これを行っているNodeモジュールの例としては request-light@vscode/l10n があります。
  • Nodeモジュールやソースファイルの代替実装を提供するには、resolve.alias を使用します。
  • コードをブラウザー用、Node.js用、共通部分に分離します。共通部分には、ブラウザーとNode.jsの両方で動作するコードのみを使用します。Node.jsとブラウザーで実装が異なる機能については、抽象化を作成します。
  • pathURI.filecontext.extensionPathrootPathuri.fsPath の使用には注意してください。これらはVS Code for the Webで使用される仮想ワークスペース(非ファイルシステム)では動作しません。代わりに URI.parsecontext.extensionUri を使用したURIを使用してください。vscode-uri nodeモジュールは、joinPathdirNamebaseNameextNameresolvePath を提供します。
  • fs の使用には注意してください。VS Codeの workspace.fs を使用するように置き換えてください。

拡張機能がWeb上で動作しているときに機能が制限されても問題ありません。when節のコンテキストを使用して、Web上の仮想ワークスペースで動作している際に、どのコマンド、ビュー、タスクを利用可能にするか、あるいは隠すかを制御します。

  • virtualWorkspace コンテキスト変数を使用して、現在のワークスペースが非ファイルシステムワークスペースかどうかを確認します。
  • resourceScheme を使用して、現在のリソースが file リソースかどうかを確認します。
  • プラットフォームシェルが存在する場合は shellExecutionSupported を使用します。
  • コマンドが適用できない理由を説明するダイアログを表示する代替コマンドハンドラーを実装します。

WebWorkerは、プロセスのフォークに代わる方法として使用できます。組み込みの JSONCSS、および HTML 言語サーバーを含む、いくつかの言語サーバーをWeb拡張機能として実行するように更新しました。以下の Language Server Protocol セクションで詳細を説明します。

ブラウザーのランタイム環境は、JavaScriptと WebAssembly の実行のみをサポートしています。他のプログラミング言語で書かれたライブラリはクロスコンパイルが必要です。例えば、C/C++Rust をWebAssemblyにコンパイルするツールがあります。例えば vscode-anycode 拡張機能は、WebAssemblyにコンパイルされたC/C++コードである tree-sitter を使用しています。

Web拡張機能におけるLanguage Server Protocol

vscode-languageserver-node は、JSONCSSHTML などの言語サーバー実装の基礎として使用されている Language Server Protocol (LSP) の実装です。

3.16.0以降、クライアントとサーバーはブラウザー実装も提供するようになりました。サーバーはWebワーカー内で実行でき、接続はWebワーカーの postMessage プロトコルに基づいています。

ブラウザー用のクライアントは 'vscode-languageclient/browser' にあります。

import { LanguageClient } from `vscode-languageclient/browser`;

サーバーは vscode-languageserver/browser にあります。

lsp-web-extension-sample でその仕組みを確認できます。

Web拡張機能の有効化

VS Codeは、以下の場合に自動的に拡張機能をWeb拡張機能として扱います。

  • 拡張機能マニフェスト(package.json)に browser エントリポイントがある。
  • 拡張機能マニフェストに main エントリポイントがなく、以下の寄与ポイントのいずれも含まれていない: localizationsdebuggersterminaltypescriptServerPlugins

Web拡張機能ホストでも動作するデバッガーやターミナルを提供したい場合は、browser エントリポイントを定義する必要があります。

ESBuildの使用

webpackの代わりにesbuildを使用したい場合は、以下のようにしてください。

esbuild.js ビルドスクリプトを追加します。

const esbuild = require('esbuild');
const glob = require('glob');
const path = require('path');
const polyfill = require('@esbuild-plugins/node-globals-polyfill');

const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');

async function main() {
  const ctx = await esbuild.context({
    entryPoints: ['src/web/extension.ts', 'src/web/test/suite/extensionTests.ts'],
    bundle: true,
    format: 'cjs',
    minify: production,
    sourcemap: !production,
    sourcesContent: false,
    platform: 'browser',
    outdir: 'dist/web',
    external: ['vscode'],
    logLevel: 'warning',
    // Node.js global to browser globalThis
    define: {
      global: 'globalThis'
    },

    plugins: [
      polyfill.NodeGlobalsPolyfillPlugin({
        process: true,
        buffer: true
      }),
      testBundlePlugin,
      esbuildProblemMatcherPlugin /* add to the end of plugins array */
    ]
  });
  if (watch) {
    await ctx.watch();
  } else {
    await ctx.rebuild();
    await ctx.dispose();
  }
}

/**
 * For web extension, all tests, including the test runner, need to be bundled into
 * a single module that has a exported `run` function .
 * This plugin bundles implements a virtual file extensionTests.ts that bundles all these together.
 * @type {import('esbuild').Plugin}
 */
const testBundlePlugin = {
  name: 'testBundlePlugin',
  setup(build) {
    build.onResolve({ filter: /[\/\\]extensionTests\.ts$/ }, args => {
      if (args.kind === 'entry-point') {
        return { path: path.resolve(args.path) };
      }
    });
    build.onLoad({ filter: /[\/\\]extensionTests\.ts$/ }, async args => {
      const testsRoot = path.join(__dirname, 'src/web/test/suite');
      const files = await glob.glob('*.test.{ts,tsx}', { cwd: testsRoot, posix: true });
      return {
        contents:
          `export { run } from './mochaTestRunner.ts';` +
          files.map(f => `import('./${f}');`).join(''),
        watchDirs: files.map(f => path.dirname(path.resolve(testsRoot, f))),
        watchFiles: files.map(f => path.resolve(testsRoot, f))
      };
    });
  }
};

/**
 * This plugin hooks into the build process to print errors in a format that the problem matcher in
 * Visual Studio Code can understand.
 * @type {import('esbuild').Plugin}
 */
const esbuildProblemMatcherPlugin = {
  name: 'esbuild-problem-matcher',

  setup(build) {
    build.onStart(() => {
      console.log('[watch] build started');
    });
    build.onEnd(result => {
      result.errors.forEach(({ text, location }) => {
        console.error(`✘ [ERROR] ${text}`);
        if (location == null) return;
        console.error(`    ${location.file}:${location.line}:${location.column}:`);
      });
      console.log('[watch] build finished');
    });
  }
};

main().catch(e => {
  console.error(e);
  process.exit(1);
});

ビルドスクリプトは以下を実行します。

  • esbuildでビルドコンテキストを作成します。コンテキストは以下のように設定されます。
    • src/web/extension.ts のコードを単一ファイル dist/web/extension.js にバンドルします。
    • テストランナー(mocha)を含むすべてのテストを単一ファイル dist/web/test/suite/extensionTests.js にバンドルします。
    • --production フラグが渡された場合は、コードを最小化(minify)します。
    • --production フラグが渡されない限り、ソースマップを生成します。
    • 'vscode' モジュールをバンドルから除外します(VS Codeランタイムによって提供されるため)。
    • processbuffer のポリフィルを作成します。
    • esbuildProblemMatcherPluginプラグインを使用して、バンドラーの完了を妨げたエラーを報告します。このプラグインは、拡張機能としてもインストールが必要な esbuild 問題マッチャーによって検出される形式でエラーを出力します。
    • testBundlePluginを使用して、すべてのテストファイルとmochaテストランナー mochaTestRunner.js を参照するテストメインファイル(extensionTests.js)を実装します。
  • --watch フラグが渡された場合は、ソースファイルの変更監視を開始し、変更が検出されるたびにバンドルをリビルドします。

esbuildはTypeScriptファイルを直接処理できます。ただし、esbuildは型チェックを行わずにすべての型宣言を単純に削除します。構文エラーのみが報告され、esbuildの失敗を引き起こす可能性があります。

そのため、型をチェックするためにTypeScriptコンパイラ(tsc)を個別に実行しますが、コードは出力しません(フラグ --noEmit)。

package.jsonscripts セクションは以下のようになります。

  "scripts": {
    "vscode:prepublish": "npm run package-web",
    "compile-web": "npm run check-types && node esbuild.js",
    "watch-web": "npm-run-all -p watch-web:*",
    "watch-web:esbuild": "node esbuild.js --watch",
    "watch-web:tsc": "tsc --noEmit --watch --project tsconfig.json",
    "package-web": "npm run check-types && node esbuild.js --production",
    "check-types": "tsc --noEmit",
    "pretest": "npm run compile-web",
    "test": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/extensionTests.js",
    "run-in-browser": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. ."
  }

npm-run-all は、指定されたプレフィックスに一致する名前のスクリプトを並行して実行するnodeモジュールです。これを使用して watch-web:esbuildwatch-web:tsc スクリプトを実行します。package.jsondevDependencies セクションに npm-run-all を追加する必要があります。

以下の tasks.json ファイルは、各監視タスク用に個別のターミナルを提供します。

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "watch-web",
      "dependsOn": ["npm: watch-web:tsc", "npm: watch-web:esbuild"],
      "presentation": {
        "reveal": "never"
      },
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "runOptions": {
        "runOn": "folderOpen"
      }
    },
    {
      "type": "npm",
      "script": "watch-web:esbuild",
      "group": "build",
      "problemMatcher": "$esbuild-watch",
      "isBackground": true,
      "label": "npm: watch-web:esbuild",
      "presentation": {
        "group": "watch",
        "reveal": "never"
      }
    },
    {
      "type": "npm",
      "script": "watch-web:tsc",
      "group": "build",
      "problemMatcher": "$tsc-watch",
      "isBackground": true,
      "label": "npm: watch-web:tsc",
      "presentation": {
        "group": "watch",
        "reveal": "never"
      }
    },
    {
      "label": "compile",
      "type": "npm",
      "script": "compile-web",
      "problemMatcher": ["$tsc", "$esbuild"]
    }
  ]
}

これは、esbuildビルドスクリプトで参照される mochaTestRunner.js です。

// Imports mocha for the browser, defining the `mocha` global.
import 'mocha/mocha';

mocha.setup({
  ui: 'tdd',
  reporter: undefined
});

export function run(): Promise<void> {
  return new Promise((c, e) => {
    try {
      // Run the mocha test
      mocha.run(failures => {
        if (failures > 0) {
          e(new Error(`${failures} tests failed.`));
        } else {
          c();
        }
      });
    } catch (err) {
      console.error(err);
      e(err);
    }
  });
}

サンプル

© . This site is unofficial and not affiliated with Microsoft.