在 VS Code 中尝试

打包扩展

打包 Visual Studio Code 扩展的首要原因是为了确保它可以在任何平台上使用 VS Code 的所有用户那里正常工作。只有打包的扩展才能在 VS Code 的 Web 环境(例如 github.devvscode.dev)中使用。当 VS Code 在浏览器中运行时,它只能为你的扩展加载一个文件,因此扩展代码需要打包成一个单一的、Web 友好的 JavaScript 文件。这同样适用于笔记本输出渲染器,VS Code 也只会为你的渲染器扩展加载一个文件。

此外,扩展的大小和复杂性会迅速增长。它们可能由多个源文件编写,并依赖于来自 npm 的模块。分解和重用是开发的最佳实践,但在安装和运行扩展时会付出代价。加载 100 个小文件比加载一个大文件慢得多。这就是我们推荐打包的原因。打包是将多个小源文件组合成一个文件的过程。

对于 JavaScript,有不同的打包工具可用。常见的有 rollup.jsParcelesbuildwebpack

使用 esbuild

esbuild 是一个配置简单、速度快的 JavaScript 打包器。要获取 esbuild,请打开终端并输入

npm i --save-dev esbuild

运行 esbuild

你可以在命令行运行 esbuild,但为了减少重复并启用问题报告,使用构建脚本 esbuild.js 会有所帮助

const esbuild = require('esbuild');

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

async function main() {
  const ctx = await esbuild.context({
    entryPoints: ['src/extension.ts'],
    bundle: true,
    format: 'cjs',
    minify: production,
    sourcemap: !production,
    sourcesContent: false,
    platform: 'node',
    outfile: 'dist/extension.js',
    external: ['vscode'],
    logLevel: 'warning',
    plugins: [
      /* add to the end of plugins array */
      esbuildProblemMatcherPlugin
    ]
  });
  if (watch) {
    await ctx.watch();
  } else {
    await ctx.rebuild();
    await ctx.dispose();
  }
}

/**
 * @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/extension.ts 中的代码打包成一个文件 dist/extension.js
    • 如果传入了 --production 标志,则压缩代码。
    • 除非传入了 --production 标志,否则生成源映射。
    • 将 'vscode' 模块从打包中排除(因为它由 VS Code 运行时提供)。
  • 使用 esbuildProblemMatcherPlugin 插件报告阻止打包器完成的错误。此插件以一种格式发出错误,该格式可被 esbuild 问题匹配器检测到,该匹配器也需要作为扩展安装。
  • 如果传入了 --watch 标志,它会开始监视源文件是否有更改,并在检测到更改时重新构建打包。

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

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

package.json 中的 scripts 部分现在看起来像这样

"scripts": {
    "compile": "npm run check-types && node esbuild.js",
    "check-types": "tsc --noEmit",
    "watch": "npm-run-all -p watch:*",
    "watch:esbuild": "node esbuild.js --watch",
    "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
    "vscode:prepublish": "npm run package",
    "package": "npm run check-types && node esbuild.js --production"
}

npm-run-all 是一个 Node 模块,它并行运行名称与给定前缀匹配的脚本。对于我们而言,它运行 watch:esbuildwatch:tsc 脚本。你需要将 npm-run-all 添加到 package.json 中的 devDependencies 部分。

compilewatch 脚本用于开发,它们会生成带源映射的打包文件。package 脚本由 vscode:prepublish 脚本使用,后者又由 VS Code 打包和发布工具 vsce 使用,并在发布扩展之前运行。将 --production 标志传递给 esbuild 脚本将使其压缩代码并创建一个小的打包文件,但也会使调试变得困难,因此在开发过程中使用其他标志。要运行上述脚本,请打开终端并输入 npm run watch 或从命令面板(⇧⌘P (Windows, Linux Ctrl+Shift+P))中选择任务:运行任务

如果你按以下方式配置 .vscode/tasks.json,你将为每个监视任务获得一个单独的终端。

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

此监视任务依赖于扩展 connor4312.esbuild-problem-matchers 进行问题匹配,你需要安装此扩展才能让任务在问题视图中报告问题。此扩展需要安装才能完成启动。

为了不忘记这一点,请向工作区添加一个 .vscode/extensions.json 文件

{
  "recommendations": ["connor4312.esbuild-problem-matchers"]
}

最后,你将需要更新你的 .vscodeignore 文件,以便将编译后的文件包含在已发布的扩展中。有关更多详细信息,请查看发布部分。

跳转到测试部分继续阅读。

使用 webpack

Webpack 是一个可从 npm 获取的开发工具。要获取 webpack 及其命令行接口,请打开终端并输入

npm i --save-dev webpack webpack-cli

这将安装 webpack 并更新你的扩展的 package.json 文件以将 webpack 包含在 devDependencies 中。

Webpack 是一个 JavaScript 打包器,但许多 VS Code 扩展是用 TypeScript 编写并只编译为 JavaScript 的。如果你的扩展使用 TypeScript,你可以使用加载器 ts-loader,以便 webpack 能够理解 TypeScript。使用以下命令安装 ts-loader

npm i --save-dev ts-loader

所有文件都可在 webpack-extension 示例中找到。

配置 webpack

安装所有工具后,现在可以配置 webpack。按照惯例,webpack.config.js 文件包含指示 webpack 打包扩展的配置。以下示例配置适用于 VS Code 扩展,并应提供一个很好的起点

//@ts-check

'use strict';

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

/**@type {import('webpack').Configuration}*/
const config = {
  target: 'webworker', // vscode extensions run in webworker context for VS Code web 📖 -> https://webpack.js.cn/configuration/target/#target

  entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.cn/configuration/entry-context/
  output: {
    // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.cn/configuration/output/
    path: path.resolve(__dirname, 'dist'),
    filename: 'extension.js',
    libraryTarget: 'commonjs2',
    devtoolModuleFilenameTemplate: '../[resource-path]'
  },
  devtool: 'source-map',
  externals: {
    vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.cn/configuration/externals/
  },
  resolve: {
    // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
    mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules
    extensions: ['.ts', '.js'],
    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.
    }
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'ts-loader'
          }
        ]
      }
    ]
  }
};
module.exports = config;

该文件是 webpack-extension 示例的一部分,可在此处找到。Webpack 配置文件是必须导出配置对象的普通 JavaScript 模块。

在上面的示例中,定义了以下内容

  • target 指示你的扩展将在哪个上下文中运行。我们建议使用 webworker,以便你的扩展在 VS Code Web 版和 VS Code 桌面版中都能工作。
  • webpack 应使用的入口点。这类似于 package.json 中的 main 属性,只是你为 webpack 提供了一个“源”入口点,通常是 src/extension.ts,而不是“输出”入口点。webpack 打包器理解 TypeScript,因此单独的 TypeScript 编译步骤是多余的。
  • output 配置告诉 webpack 将生成的打包文件放置在哪里。按照惯例,那是 dist 文件夹。在此示例中,webpack 将生成一个 dist/extension.js 文件。
  • resolvemodule/rules 配置用于支持 TypeScript 和 JavaScript 输入文件。
  • externals 配置用于声明排除项,例如不应包含在打包中的文件和模块。vscode 模块不应被打包,因为它不存在于磁盘上,而是在需要时由 VS Code 即时创建。根据扩展使用的 Node 模块,可能需要更多的排除项。

最后,你将需要更新你的 .vscodeignore 文件,以便将编译后的文件包含在已发布的扩展中。有关更多详细信息,请查看发布部分。

运行 webpack

创建 webpack.config.js 文件后,可以调用 webpack。你可以在命令行运行 webpack,但为了减少重复,使用 npm 脚本会很有帮助。

将这些条目合并到 package.jsonscripts 部分

"scripts": {
    "compile": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "vscode:prepublish": "npm run package",
    "package": "webpack --mode production --devtool hidden-source-map",
},

compilewatch 脚本用于开发,它们生成打包文件。vscode:prepublish 由 VS Code 打包和发布工具 vsce 使用,并在发布扩展之前运行。区别在于模式,它控制优化级别。使用 production 会产生最小的打包文件,但也需要更长时间,因此在开发过程中使用 development。要运行上述脚本,请打开终端并输入 npm run compile 或从命令面板(⇧⌘P (Windows, Linux Ctrl+Shift+P))中选择任务:运行任务

运行扩展

在运行扩展之前,package.json 中的 main 属性必须指向打包文件,对于上述配置,该文件是 "./dist/extension"。有了这个更改,扩展现在可以执行和测试了。

测试

扩展作者通常会为其扩展源代码编写单元测试。在正确的架构分层下,当扩展源代码不依赖于测试时,webpack 和 esbuild 生成的打包文件不应包含任何测试代码。要运行单元测试,只需要一个简单的编译即可。

将这些条目合并到 package.jsonscripts 部分

"scripts": {
    "compile-tests": "tsc -p . --outDir out",
    "pretest": "npm run compile-tests",
    "test": "vscode-test"
}

compile-tests 脚本使用 TypeScript 编译器将扩展编译到 out 文件夹中。有了可用的中间 JavaScript,以下 launch.json 片段足以运行测试。

{
  "name": "Extension Tests",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceFolder}",
    "--extensionTestsPath=${workspaceFolder}/out/test"
  ],
  "outFiles": ["${workspaceFolder}/out/test/**/*.js"],
  "preLaunchTask": "npm: compile-tests"
}

此运行测试的配置与未打包的扩展相同。没有理由打包单元测试,因为它们不是扩展已发布部分的一部分。

发布

在发布之前,你应该更新 .vscodeignore 文件。现在打包到 dist/extension.js 文件中的所有内容都可以被排除,通常是 out 文件夹(如果你还没有删除它)以及最重要的是 node_modules 文件夹。

一个典型的 .vscodeignore 文件看起来像这样

.vscode
node_modules
out/
src/
tsconfig.json
webpack.config.js
esbuild.js

迁移现有扩展

将现有扩展迁移到使用 esbuild 或 webpack 很容易,并且类似于上面的入门指南。一个采用 webpack 的真实世界示例是 VS Code 的引用视图,通过此拉取请求实现。

你可以在那里看到

  • 添加 esbuildwebpackwebpack-clits-loader 作为 devDependencies
  • 更新 npm 脚本以使用上述打包器
  • 更新任务配置 tasks.json 文件。
  • 添加和调整 esbuild.jswebpack.config.js 构建文件。
  • 更新 .vscodeignore 以排除 node_modules 和中间输出文件。
  • 享受安装和加载速度快得多的扩展!

故障排除

代码压缩

production 模式下打包还会执行代码压缩。代码压缩通过删除空格和注释以及将变量和函数名更改为难看但简短的名称来压缩源代码。使用 Function.prototype.name 的源代码行为不同,因此你可能需要禁用代码压缩。

webpack 关键依赖

运行 webpack 时,你可能会遇到类似“关键依赖:依赖项的请求是一个表达式”的警告。此类警告必须严肃对待,并且你的打包文件很可能无法工作。该消息表示 webpack 无法静态确定如何打包某些依赖项。这通常是由动态 require 语句引起的,例如 require(someDynamicVariable)

要解决此警告,你应该:

  • 尝试使依赖项静态化,以便可以将其打包。
  • 通过 externals 配置排除该依赖项。此外,请确保这些 JavaScript 文件不会从打包的扩展中排除,可以使用 .vscodeignore 中的否定通配符模式,例如 !node_modules/mySpecialModule

后续步骤

  • 扩展市场 - 了解有关 VS Code 公共扩展市场的更多信息。
  • 测试扩展 - 将测试添加到您的扩展项目中,以确保高质量。
  • 持续集成 - 了解如何在 Azure Pipelines 上运行扩展 CI 构建。