现已推出!阅读 10 月份的新功能和修复。

Web 扩展

Visual Studio Code 可以作为浏览器中的编辑器运行。一个例子是github.dev用户界面,可以通过在 GitHub 中浏览仓库或 Pull Request 时按.(句号键)访问。当 VS Code 在 Web 中使用时,安装的扩展将在浏览器中的扩展主机中运行,称为“Web 扩展主机”。可以在 Web 扩展主机中运行的扩展称为“Web 扩展”。

Web 扩展与普通扩展具有相同的结构,但考虑到不同的运行时,它们不会与为 Node.js 运行时编写的扩展使用相同的代码运行。Web 扩展仍然可以访问完整的 VS Code API,但不再可以访问 Node.js API 和模块加载。相反,Web 扩展受到浏览器沙箱的限制,因此与在 Node.js 运行时运行的普通扩展相比,它们存在限制

VS Code 桌面也支持 Web 扩展运行时。如果你决定将你的扩展创建为 Web 扩展,它将在VS Code for the Web(包括vscode.devgithub.dev)以及桌面和GitHub Codespaces等服务中得到支持。

Web 扩展结构

Web 扩展结构类似于普通扩展。扩展清单(package.json)定义扩展源代码的入口文件并声明扩展贡献。

对于 Web 扩展,主入口文件browser属性定义,而不是像普通扩展一样由main属性定义。

contributes属性对 Web 扩展和普通扩展的工作方式相同。

以下示例显示了一个简单的 hello world 扩展的package.json,该扩展仅在 Web 扩展主机中运行(它只有browser入口点)

{
  "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 之前的版本,则必须在activationEvents中显式列出onCommand:helloworld-web-sample.helloWorld

只有main入口点,但没有browser的扩展不是 Web 扩展。它们会被 Web 扩展主机忽略,并且在“扩展”视图中不可用。

Extensions view

只有声明式贡献(只有contributes,没有mainbrowser)的扩展可以是 Web 扩展。它们可以在VS Code for the Web中安装和运行,而无需扩展作者进行任何修改。具有声明式贡献的扩展示例包括主题、语法和代码片段。

扩展可以同时拥有browsermain入口点,以便在浏览器和 Node.js 运行时中运行。将现有扩展更新为 Web 扩展部分显示了如何迁移扩展以使其在两种运行时中都工作。

Web 扩展启用部分列出了用于确定扩展是否可以在 Web 扩展主机中加载的规则。

Web 扩展主文件

Web 扩展的主文件由browser属性定义。该脚本在 Web 扩展主机中运行,在浏览器 WebWorker环境中。它受到浏览器 worker 沙箱的限制,与在 Node.js 运行时运行的普通扩展相比,它存在限制。

  • 不支持导入或加载其他模块。importScripts也不可用。因此,代码必须打包到单个文件中。
  • 可以通过模式require('vscode')加载VS Code API。这将起作用,因为有一个require的 shim,但此 shim 不能用于加载其他扩展文件或其他节点模块。它只适用于require('vscode')
  • Node.js 全局变量和库,例如processossetImmediatepathutilurl在运行时不可用。但是,它们可以通过 webpack 等工具添加。webpack 配置部分解释了如何做到这一点。
  • 打开的工作区或文件夹位于虚拟文件系统中。访问工作区文件需要通过 VS Code 的文件系统API 进行,该 API 可在vscode.workspace.fs处访问。
  • 扩展上下文位置(ExtensionContext.extensionUri)和存储位置(ExtensionContext.storageUriglobalStorageUri)也位于虚拟文件系统中,需要通过vscode.workspace.fs进行访问。
  • 要访问 Web 资源,必须使用Fetch API。访问的资源需要支持跨域资源共享(CORS)。
  • 无法创建子进程或运行可执行文件。但是,可以通过Worker API 创建 Web 工作者。这用于运行语言服务器,如Web 扩展中的语言服务器协议部分所述。
  • 与普通扩展一样,扩展的activate/deactivate函数需要通过模式exports.activate = ...导出。

开发 Web 扩展

值得庆幸的是,TypeScript 和 webpack 等工具可以隐藏许多浏览器运行时限制,并允许你以与普通扩展相同的方式编写 Web 扩展。Web 扩展和普通扩展通常可以从相同的源代码生成。

例如,由yo code 生成器创建的Hello Web Extension只在构建脚本方面有所不同。你可以像运行传统的 Node.js 扩展一样运行和调试生成的扩展,方法是使用提供的启动配置,可以通过“调试:选择并启动调试”命令访问。

创建 Web 扩展

要搭建一个新的 Web 扩展,请使用yo code并选择New Web Extension。确保已安装最新版本的generator-code(>= [email protected])。要更新生成器和 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包含用于在具有 Web 扩展主机的 VS Code 桌面上运行 Web 扩展和测试的启动配置(设置extensions.webWorker不再需要)。
  • .vscode/task.json包含启动配置使用的构建任务。它使用npm run watch-web,并依赖于 webpack 特定的ts-webpack-watch问题匹配器。
  • .vscode/extensions.json包含提供问题匹配器的扩展。这些扩展需要安装才能使启动配置工作。
  • 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.js.cn/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.jsdist/web/test/suite/index.js
  • target字段指示编译后的 JavaScript 文件将运行在何种类型的环境中。对于 Web 扩展,你希望它为webworker
  • resolve字段包含添加别名和回退的功能,用于在浏览器中无法工作的节点库。
    • 如果你使用的是path之类的库,你可以指定如何在 Web 编译上下文中解析path。例如,你可以指向项目中定义path的文件,例如path: path.resolve(__dirname, 'src/my-path-implementation-for-web.js')。或者,你可以使用名为path-browserify的库的 Browserify 节点打包版本,并指定path: require.resolve('path-browserify')
    • 有关 Node.js 核心模块 polyfills 列表,请参阅webpack resolve.fallback
  • plugins 部分使用 DefinePlugin 插件 来填充全局变量,例如 process Node.js 全局变量。

测试你的 Web 扩展

目前有三种方法在将 Web 扩展发布到市场之前对其进行测试。

  • 在桌面上使用带有 --extensionDevelopmentKind=web 选项运行的 VS Code,在 VS Code 中运行的 Web 扩展主机中运行您的 Web 扩展。
  • 使用 @vscode/test-web 节点模块打开一个包含 VS Code for the Web 的浏览器,包括您的扩展,从本地服务器提供服务。
  • 侧载 您的扩展到 vscode.dev 以查看您的扩展在实际环境中的运行情况。

在桌面上运行的 VS Code 中测试您的 Web 扩展

要使用现有的 VS Code 扩展开发体验,在桌面上运行的 VS Code 支持运行 Web 扩展主机以及常规的 Node.js 扩展主机。

使用 新建 Web 扩展 生成器提供的 pwa-extensionhost 启动配置

{
  "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: watch-web 通过调用 npm run 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 问题匹配器 扩展提供。

在启动的 扩展开发主机 实例中,Web 扩展将可用并在 Web 扩展主机中运行。运行 Hello World 命令以激活扩展。

打开 正在运行的扩展 视图(命令:开发人员:显示正在运行的扩展)以查看哪些扩展在 Web 扩展主机中运行。

使用 @vscode/test-web 在浏览器中测试您的 Web 扩展

@vscode/test-web 节点模块提供 CLI 和 API 以在浏览器中测试 Web 扩展。

该节点模块贡献了一个 npm 二进制文件 vscode-test-web,它可以从命令行打开 VS Code for the Web

  • 它将 VS Code 的 Web 部分下载到 .vscode-test-web
  • localhost:3000 上启动一个本地服务器。
  • 打开一个浏览器(Chromium、Firefox 或 Webkit)。

您可以从命令行运行它

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

或者更好的是,将 @vscode/test-web 作为开发依赖项添加到您的扩展中,并在脚本中调用它

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

查看 @vscode/test-web 自述文件 以获取更多 CLI 选项

选项 参数描述
--browserType 要启动的浏览器:chromium(默认)、firefoxwebkit
--extensionDevelopmentPath 指向正在开发的扩展的路径,该路径要包含在内。
--extensionTestsPath 要运行的测试模块的路径。
--permission 授予打开的浏览器的权限:例如 clipboard-readclipboard-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

然后,将 localhost.pemlocalhost-key.pem 文件生成到您不会丢失它们的位置(例如 $HOME/certs

$ 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://127.0.0.1:5000       │
   │   - On Your Network:  https://172.19.255.26:5000   │
   │                                                    │
   │   Copied local address to clipboard!               │
   │                                                    │
   └────────────────────────────────────────────────────┘

最后,打开 vscode.dev,从命令面板运行 开发人员:从位置安装扩展... (⇧⌘P (Windows, Linux Ctrl+Shift+P)),粘贴上面的 URL,例如 https://127.0.0.1:5000,然后选择 安装

检查日志

您可以检查浏览器开发者工具控制台中的日志,以查看来自您的扩展的任何错误、状态和日志。

您可能会看到来自 vscode.dev 本身的其他日志。此外,您无法轻松地设置断点,也无法看到您的扩展的源代码。这些限制使在 vscode.dev 中调试并非最愉快的体验,因此我们建议在侧载到 vscode.dev 之前使用前两个选项进行测试。侧载是在发布您的扩展之前进行最终完整性检查的一个好方法。

Web 扩展测试

支持 Web 扩展测试,并且可以像常规扩展测试一样实现。查看 测试扩展 文章以了解扩展测试的基本结构。

@vscode/test-web 节点模块相当于 @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。提供的 测试运行器脚本 使用 Mocha 的 Web 版本,并包含特定于 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(内部版本)桌面上运行(和调试)扩展测试,请使用 VS Code 中的扩展测试 启动配置

{
  "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 扩展与其他扩展一起托管在 市场 上。

确保使用最新版本的 vsce 来发布您的扩展。vsce 会标记所有 Web 扩展的扩展。为此,vsce 会使用 Web 扩展启用 部分中列出的规则。

将现有扩展更新为 Web 扩展

没有代码的扩展

没有代码但只有贡献点(例如,主题、代码段和基本语言扩展)的扩展不需要任何修改。它们可以在 Web 扩展主机中运行,并且可以从扩展视图中安装。

重新发布不是必需的,但在发布扩展的新版本时,请确保使用最新版本的 vsce

迁移带有代码的扩展

具有源代码(由 main 属性定义)的扩展需要提供 Web 扩展主文件 并在 package.json 中设置 browser 属性。

使用以下步骤重新编译您的扩展代码以用于浏览器环境

  • 添加一个 webpack 配置文件,如 webpack 配置 部分所示。如果您已经有一个用于 Node.js 扩展代码的 webpack 文件,您可以添加一个用于 Web 的新部分。查看 vscode-css-formatter 作为示例。
  • 添加 launch.jsontasks.json 文件,如 测试您的 Web 扩展 部分所示。
  • 在 webpack 配置文件中,将输入文件设置为现有的 Node.js 主文件,或为 Web 扩展创建一个新的主文件。
  • package.json 中,添加 browserscripts 属性,如 Web 扩展解剖 部分所示。
  • 运行 npm run compile-web 以调用 webpack 并查看需要进行哪些工作才能使您的扩展在 Web 中运行。

为了确保尽可能多的源代码可以重复使用,以下是一些技巧

  • 要填充一个 Node.js 核心模块,例如 path,请在 resolve.fallback 中添加一个条目。
  • 要提供一个 Node.js 全局变量,例如 process,请使用 DefinePlugin 插件
  • 使用既能在浏览器中也能在节点运行时中工作的节点模块。节点模块可以通过定义 browsermain 入口点来做到这一点。Webpack 会自动使用与目标匹配的入口点。执行此操作的节点模块的示例包括 request-lightvscode-nls
  • 要为节点模块或源文件提供备用实现,请使用 resolve.alias
  • 将您的代码分成浏览器部分、Node.js 部分和公共部分。在公共部分中,只使用既能在浏览器中也能在 Node.js 运行时中工作的代码。为在 Node.js 和浏览器中具有不同实现的功能创建抽象。
  • 注意使用pathURI.filecontext.extensionPathrootPathuri.fsPath。这些方法在 VS Code for Web 中无法使用,因为它们依赖于文件系统。请使用 URI.parsecontext.extensionUri 创建 URI。vscode-uri 节点模块提供 joinPathdirNamebaseNameextNameresolvePath 方法。
  • 注意使用 fs。请使用 vscode 的 workspace.fs 代替。

当您的扩展程序在 Web 环境中运行时,提供较少的功能是可以接受的。使用 when 子句上下文 控制在 Web 上的虚拟工作区中运行时哪些命令、视图和任务可用或隐藏。

  • 使用 virtualWorkspace 上下文变量来判断当前工作区是否是无文件系统工作区。
  • 使用 resourceScheme 检查当前资源是否为 file 资源。
  • 如果存在平台 Shell,请使用 shellExecutionSupported
  • 实现替代的命令处理程序,显示对话框解释为什么命令不可用。

Web Workers 可以作为替代进程派生的方法。我们已更新多个语言服务器以作为 Web 扩展运行,包括内置的 JSONCSSHTML 语言服务器。以下 语言服务器协议 部分提供了更多详细信息。

浏览器运行时环境仅支持执行 JavaScript 和 WebAssembly。用其他编程语言编写的库需要进行交叉编译,例如,有一些工具可以将 C/C++Rust 编译为 WebAssembly。例如,vscode-anycode 扩展使用 tree-sitter,它是由 C/C++ 代码编译成的 WebAssembly。

Web 扩展中的语言服务器协议

vscode-languageserver-node语言服务器协议 (LSP) 的一个实现,它用作语言服务器实现(如 JSONCSSHTML)的基础。

从 3.16.0 版本开始,客户端和服务器现在还提供浏览器实现。服务器可以在 Web Worker 中运行,连接基于 Web Workers 的 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

如果您想使用 esbuild 而不是 webpack,请执行以下操作:

添加 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: 'silent',
    // 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}`);
        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 标志,则压缩代码。
    • 除非传递了 --production 标志,否则生成源映射。
    • 从捆绑包中排除 vscode 模块(因为它由 VS Code 运行时提供)。
    • processbuffer 创建 polyfills。
    • 使用 esbuildProblemMatcherPlugin 插件报告阻止捆绑器完成的错误。此插件以 esbuild 问题匹配器可检测的格式发出错误,该匹配器也需要作为扩展安装。
    • 使用 testBundlePlugin 实现一个测试主文件 (extensionTests.js),该文件引用所有测试文件和 mocha 测试运行器 mochaTestRunner.js
  • 如果传递了 --watch 标志,则它会开始监视源文件以检测更改,并在检测到更改时重新构建捆绑包。

esbuild 可以直接使用 TypeScript 文件。但是,esbuild 只是简单地剥离所有类型声明,而不会执行任何类型检查。只会报告语法错误,并可能导致 esbuild 失败。

出于此原因,我们单独运行 TypeScript 编译器 (tsc) 来检查类型,但不会发出任何代码(标志 --noEmit)。

现在 package.json 中的 scripts 部分如下所示:

  "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 是一个节点模块,它并行运行名称匹配给定前缀的脚本。对于我们来说,它运行 watch-web:esbuildwatch-web:tsc 脚本。您需要将 npm-run-all 添加到 package.json 中的 devDependencies 部分。

以下 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);
    }
  });
}

示例