网页扩展

Visual Studio Code 可以作为浏览器中的编辑器运行。一个例子是当在 GitHub 中浏览仓库或拉取请求时,按下 .(句号键)进入的 github.dev 用户界面。当 VS Code 在 Web 中使用时,安装的扩展程序会在浏览器中的扩展主机中运行,称为“Web 扩展主机”。能够在 Web 扩展主机中运行的扩展程序称为“Web 扩展”。

Web扩展与常规扩展共享相同的结构,但由于运行环境不同,它们不会像为Node.js运行时编写的扩展那样运行相同的代码。Web扩展仍然可以访问完整的VS Code API,但不再访问Node.js API和模块加载。相反,Web扩展受到浏览器沙箱的限制,因此与普通扩展相比存在限制

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

Web扩展结构

一个网页扩展结构类似于常规扩展。扩展清单(package.json)定义了扩展源代码的入口文件,并声明了扩展的贡献。

对于网页扩展,主入口文件browser属性定义,而不是像常规扩展那样由main属性定义。

contributes 属性在网页扩展和常规扩展中的工作方式相同。

下面的示例展示了一个简单的“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"
  }
}

注意: 如果你的扩展目标是一个早于1.74版本的VS Code,你必须在activationEvents中明确列出onCommand:helloworld-web-sample.helloWorld

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

扩展视图

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

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

web extension enablement 部分列出了用于决定是否可以在web扩展主机中加载扩展的规则。

Web 扩展主文件

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

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

开发一个网页扩展

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

例如,由yo code generator创建的Hello Web Extension仅在构建脚本上有所不同。您可以使用提供的启动配置来运行和调试生成的扩展,就像传统的Node.js扩展一样,这些配置可以通过调试:选择并开始调试命令访问。

创建一个网页扩展

要搭建一个新的网页扩展,请使用yo code并选择新建网页扩展。确保安装了最新版本的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 扩展和测试的启动配置(不再需要设置 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.org/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 插件 来填充全局变量,例如 Node.js 的 process 全局变量。

测试你的网页扩展

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

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

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

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

使用New Web Extension生成器提供的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的网页部分下载到.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 README以获取更多CLI选项:

Option Argument Description
--browserType The browser to launch: chromium (default), firefox or webkit
--extensionDevelopmentPath A path pointing to an extension under development to include.
--extensionTestsPath A path to a test module to run.
--permission Permission granted to the opened browser: e.g. clipboard-read, clipboard-write.
See full list of options. Argument can be provided multiple times.
--folder-uri URI of the workspace to open VS Code on. Ignored when folderPath is provided
--extensionPath A path pointing to a folder containing additional extensions to include.
Argument can be provided multiple times.
folderPath A local folder to open VS Code on.
The folder content will be available as a virtual file system and opened as workspace.

VS Code 的网页部分被下载到一个文件夹 .vscode-test-web。您希望将其添加到您的 .gitignore 文件中。

在 vscode.dev 中测试你的网页扩展

在您发布扩展以供每个人在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://localhost: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://localhost: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);
    }
  });
}

要从命令行运行网络测试,请将以下内容添加到您的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启动配置:

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

确保使用最新版本的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文件,如测试你的网页扩展部分所示。
  • 在webpack配置文件中,将输入文件设置为现有的Node.js主文件或为web扩展创建一个新的主文件。
  • package.json中,添加browserscripts属性,如Web扩展结构部分所示。
  • 运行 npm run compile-web 以调用 webpack 并查看需要哪些工作才能使您的扩展在 web 上运行。

为了确保尽可能多的源代码可以被重用,这里有一些技巧:

  • 要为Node.js核心模块(如path)提供polyfill,请向resolve.fallback添加一个条目。
  • 要提供一个Node.js全局变量,例如process,请使用DefinePlugin插件
  • 使用在浏览器和Node运行时都能工作的node模块。Node模块可以通过定义browsermain入口点来实现这一点。Webpack会自动使用与其目标匹配的入口点。实现这一点的node模块示例包括request-light@vscode/l10n
  • 要为节点模块或源文件提供替代实现,请使用 resolve.alias
  • 将你的代码分为浏览器部分、Node.js部分和公共部分。在公共部分,只使用在浏览器和Node.js运行时都能工作的代码。为在Node.js和浏览器中有不同实现的功能创建抽象。
  • 注意使用pathURI.filecontext.extensionPathrootPathuri.fsPath的情况。这些在VS Code for the Web中不会适用于虚拟工作区(非文件系统)。相反,使用带有URI.parsecontext.extensionUri的URI。vscode-uri节点模块提供了joinPathdirNamebaseNameextNameresolvePath
  • 注意fs的使用。通过使用vscode的workspace.fs来替换。

当您的扩展在网络上运行时,提供较少的功能是可以的。使用when子句上下文来控制哪些命令、视图和任务在网络的虚拟工作空间中可用或隐藏。

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

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

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

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

vscode-languageserver-node语言服务器协议 (LSP) 的一个实现,它被用作语言服务器实现的基础,例如 JSON, CSS, 和 HTML

自3.16.0版本以来,客户端和服务器现在也提供了浏览器实现。服务器可以在web worker中运行,连接基于webworkers的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);
    }
  });
}

示例