首页
/ VS Code 内置扩展开发指南:目录结构、esbuild 构建管线与浏览器端支持

VS Code 内置扩展开发指南:目录结构、esbuild 构建管线与浏览器端支持

2026-09-05 10:55:25作者:农烁颖Land

本文基于 VS Code 仓库的 extensions/CONTRIBUTING.md,系统讲解内置扩展(Built-In Extensions)的标准目录结构、TypeScript 与 esbuild 双构建管线,以及如何把一个扩展从"仅桌面端"扩展为"桌面 + 浏览器"双目标。读完本文,你可以照着仓库内真实扩展的骨架新建一个内置扩展,理解 out/dist 输出目录的分工,并独立完成 esbuild.browser.mtstsconfig.browser.json 的接入。

一、extensions/ 目录:随 VS Code 一起发布的内置扩展仓库

仓库根目录下的 extensions/ 存放随 VS Code 一起发布的内置扩展(built-in extensions),包括语言包(如 json/python/)、语言服务(如 typescript-language-features/css-language-features/)、主题(如 theme-monokai/)等。

extensions/package.json 并不是某个扩展的清单,而是"所有扩展共享的依赖"声明(其 description 为 Dependencies shared by all extensions),其中可以看到构建工具链的三个关键依赖:

  • typescript:用于开发构建(type-check + 编译到 out/);
  • esbuild:用于生产构建(打包压缩到 dist/);
  • @parcel/watcher:用于 watch 模式下低 CPU 占用的文件监听(在 extensions/esbuild-common.mts 中被使用,源码注释说明其空闲 CPU 占用远低于 esbuild 自带的 watch 模式)。

二、一个典型 TypeScript 内置扩展的目录结构

按照 extensions/CONTRIBUTING.md 的规范,一个典型的 TypeScript 内置扩展包含以下文件:

文件 作用
package.json 扩展清单(manifest),声明 main/browser 入口、激活事件、贡献点等
src/ TypeScript 源码主目录
tsconfig.json 主 TypeScript 配置,必须继承自 tsconfig.base.json
esbuild.mts 生产构建使用的 esbuild 构建脚本
.vscodeignore 打包忽略清单,可直接从现有扩展复制

以仓库中较简单的 extensions/debug-auto-launch/ 为例,其实际目录正是这套骨架:package.jsonsrc/tsconfig.jsonesbuild.mts.vscodeignoremedia/(图标)。

2.1 package.json 清单的要点

对照 extensions/debug-auto-launch/package.json,内置扩展清单的几个关键字段:

{
  "publisher": "vscode",
  "engines": { "vscode": "^1.5.0" },
  "activationEvents": ["onStartupFinished"],
  "main": "./out/extension",
  "scripts": {
    "compile": "gulp compile-extension:debug-auto-launch",
    "watch": "gulp watch-extension:debug-auto-launch"
  }
}
  • publisher 固定为 vscode
  • main 指向开发构建产物 ./out/extension(见下文 2.3 节,out 是开发构建目录);
  • 每个扩展的 scripts 中都提供 compilewatch 两个 npm 脚本,分别转调 gulp compile-extension:<扩展名>gulp watch-extension:<扩展名> 任务,这是仓库统一的单扩展构建/监听入口。

2.2 .vscodeignore:打包时忽略什么

extensions/debug-auto-launch/.vscodeignore 的真实内容展示了应该从发布包中排除的文件:

src/**
tsconfig*.json
**/*.tsbuildinfo
out/**
esbuild*.mts
package-lock.json

即源码、TypeScript 配置、构建脚本与锁文件都不随扩展发布,只保留清单声明的入口产物。CONTRIBUTING 文档建议新扩展的 .vscodeignore 直接"从现有扩展复制",上述文件即为可直接参照的模板。

2.3 输出目录约定:out(开发)与 dist(生产)

文档明确规定 TypeScript 扩展有两种输出结构:

  • out开发构建(development builds)输出目录;
  • dist生产构建(production builds)输出目录。

两者对应两条构建管线:

  1. 开发管线(tsc → out/tsconfig.json 编译源码到 out/,产物按模块结构展开、不压缩,适合断点调试。对照 extensions/debug-auto-launch/tsconfig.json

    {
      "extends": "../tsconfig.base.json",
      "compilerOptions": {
        "rootDir": "./src",
        "outDir": "./out",
        "types": ["node"],
        "typeRoots": ["./node_modules/@types"]
      },
      "include": [
        "src/**/*",
        "../../src/vscode-dts/vscode.d.ts"
      ]
    }
    

    注意两点:一是 extends 指向上一级的 tsconfig.base.json(相对路径 ../tsconfig.base.json),二是 include 中显式引用了 src/vscode-dts/vscode.d.ts,这是扩展 API 类型的来源。

  2. 生产管线(esbuild → dist/esbuild.mts 将入口打包压缩为单个 bundle 输出到 dist/。以 extensions/debug-auto-launch/esbuild.mts 为例:

    const srcDir = path.join(import.meta.dirname, 'src');
    const outDir = path.join(import.meta.dirname, 'dist');
    
    run({
        platform: 'node',
        entryPoints: {
            'extension': path.join(srcDir, 'extension.ts'),
        },
        srcDir,
        outdir: outDir,
    }, process.argv);
    

    入口 src/extension.ts 被打包为 dist/extension.js。生产打包时,package.json 的入口会被指向 dist/ 产物;main 指向 out/ 的形式服务于开发调试。

三、tsconfig 继承体系:tsconfig.base.json 定了什么

CONTRIBUTING 要求 tsconfig.json 继承 tsconfig.base.json。查看 extensions/tsconfig.base.json,所有内置扩展共享以下编译约束:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "target": "ES2024",
    "lib": ["ES2024"],
    "module": "commonjs",
    "strict": true,
    "exactOptionalPropertyTypes": false,
    "useUnknownInCatchVariables": false,
    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitOverride": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true
  }
}

关键含义:

  • target: ES2024 / module: commonjs:与 esbuild 生产管线的 target: ['es2024']format: 'cjs' 保持一致(见下节),开发产物与生产产物在语言特性层面行为一致;
  • strict: true 叠加 noUnusedLocalsnoUnusedParametersnoImplicitReturnsnoImplicitOverride:内置扩展的源码必须通过更严格的质量门槛,未使用变量和隐式返回都会报错;
  • 扩展侧的 tsconfig.json 只需关心 rootDiroutDirtypesinclude 等项目级配置,语言标准由基类统一约束——这是"必须继承"的实质原因。

四、esbuild 构建管线深度解析:两个公共脚本

所有扩展的 esbuild.mts 都不直接调用 esbuild API,而是委托给两层公共脚本,这也是新扩展写构建脚本时应遵循的方式。

4.1 esbuild-extension-common.mts:扩展层公共选项

extensions/esbuild-extension-common.mts 中的 run() 负责把每个扩展的少量配置与公共基线合并。resolveBaseOptions() 为所有扩展统一设置:

const options: esbuild.BuildOptions = {
    platform: config.platform,   // 'node' | 'browser'
    bundle: true,
    minify: true,
    treeShaking: true,
    sourcemap: true,
    target: ['es2024'],
    external: ['vscode'],       // 扩展 API 由宿主提供,必须 external
    format: config.format ?? 'cjs',
    logOverride: { 'import-is-undefined': 'error' },
};

其中 external: ['vscode'] 体现了 VS Code 扩展的本质:import * as vscode from 'vscode' 在运行时由宿主注入,打包时不可解析也不应打入 bundle。

平台差异同样在此处理——当 platform === 'browser' 时:

options.mainFields = ['browser', 'module', 'main'];
options.alias = { 'path': 'path-browserify' };
options.define = {
    'process.platform': JSON.stringify('web'),
    'process.env': JSON.stringify({}),
    'process.env.BROWSER_ENV': JSON.stringify('true'),
};

即浏览器构建会优先解析依赖包的 browser 字段、用 path-browserify 替换 Node 的 path 模块、并把 process.platform 静态替换为 'web'。而 platform === 'node'mainFields['module', 'main']。这段实现是后文"浏览器构建只能用 browser-safe API"这一约束的底层落地方式。

4.2 esbuild-common.mts:构建与 watch 的共享执行器

extensions/esbuild-common.mtsrunBuild() 提供了所有扩展共用的执行语义:

  • --outputRoot <dir>:命令行参数,把输出目录重定向到 <root>/<原 outdir 名>,供上层构建任务统一收集产物;
  • --watch:进入监听模式。源码注释说明此处选择 @parcel/watcher 是因为其空闲时 CPU 占用远低于 esbuild watch;watch 回调带 100ms 防抖,且每次变更都完整重跑构建(而不是保留 esbuild context),源码注释解释这是为了降低内存占用;监听时自动忽略 **/node_modules/****/dist/****/out/**,避免产物回弹触发死循环;
  • esbuild 服务生命周期esbuild.stop() 会关掉所有并发构建共享的单一 esbuild 服务,因此 buildOnce()pendingBuilds 计数,确保只有在没有构建在途时才 stop(),防止兄弟构建(例如同一 Promise.all 中的其他构建)被误拆服务。

对照第二节示例,node ./esbuild.mts --watch 即可获得开发监听;而扩展 package.json 中的 gulp watch-extension:<扩展名> 脚本是仓库层面统一封装的等价入口。

五、让扩展在浏览器中运行:三步接入法

CONTRIBUTING 文档的 "Enabling an Extension in the Browser" 一节给出了完整方法:默认情况下内置扩展只面向桌面端,要同时支持浏览器版 VS Code,需要做三件事。

5.1 三步清单

  1. package.json 中添加 "browser" 条目,指向浏览器 bundle(例如 "./dist/browser/extension");
  2. 添加 tsconfig.browser.json,仅做浏览器安全源码的类型检查;
  3. 添加 esbuild.browser.mts,其中 platform 必须设为 'browser'

文档同时强调:浏览器构建只能使用 browser-safe API。若扩展在桌面与 Web 端需要不同行为,可以为每个目标建立独立入口:

  • src/extension.ts:桌面端入口;
  • src/extension.browser.ts:浏览器端入口,并确认 esbuild.browser.mts 构建的是这个入口、tsconfig.browser.json 也指向它。

5.2 真实案例:configuration-editing

extensions/configuration-editing/ 是这套流程的完整参照。

package.json 双入口声明(见 extensions/configuration-editing/package.json):

"main": "./out/configurationEditingMain",
"browser": "./dist/browser/configurationEditingMain",
"scripts": {
  "compile": "gulp compile-extension:configuration-editing",
  "watch": "gulp watch-extension:configuration-editing",
  "compile-web": "npm-run-all2 -lp bundle-web typecheck-web",
  "bundle-web": "node ./esbuild.browser.mts",
  "typecheck-web": "node ../../node_modules/@typescript/native/lib/tsc.js --project ./tsconfig.browser.json --noEmit",
  "watch-web": "npm-run-all2 -lp watch-bundle-web watch-typecheck-web",
  "watch-bundle-web": "node ./esbuild.browser.mts --watch",
  "watch-typecheck-web": "node ../../node_modules/@typescript/native/lib/tsc.js --project ./tsconfig.browser.json --noEmit --watch"
}

main 指向桌面端开发产物,browser 指向 dist/browser/ 下的生产 bundle,与文档给出的示例路径形态完全一致。compile-web 把"打包 + 类型检查"串成一条命令,watch-web 则并行跑监听打包与监听类型检查。

tsconfig.browser.json 只检查浏览器安全源码extensions/configuration-editing/tsconfig.browser.json):

{
  "extends": "./tsconfig.json",
  "compilerOptions": {},
  "exclude": ["./src/test/**"],
  "files": ["./src/configurationEditingMain.ts"]
}

做法是继承主 tsconfig.json(间接继承 tsconfig.base.json),再用 files 把检查范围收敛到浏览器入口,配合 exclude 排除测试目录——这样 Node-only 的源码只要不被浏览器入口引用链触及,就不会进入浏览器类型检查的视野。

esbuild.browser.mts:platform 为 browser,且入口产物落在 dist/browserextensions/configuration-editing/esbuild.browser.mts):

const srcDir = path.join(import.meta.dirname, 'src');
const outDir = path.join(import.meta.dirname, 'dist', 'browser');

run({
    platform: 'browser',
    entryPoints: {
        'configurationEditingMain': path.join(srcDir, 'configurationEditingMain.ts'),
    },
    srcDir,
    outdir: outDir,
    additionalOptions: {
        plugins: [browserNetPlugin],
        tsconfig: path.join(import.meta.dirname, 'tsconfig.browser.json'),
    },
}, process.argv);

值得注意的是其中的 browserNetPlugin——一个用 esbuild onResolve 钩子把 ./node/net 的导入重定向到 ./browser/net.ts 的插件。这正是"同一份共享代码、按平台切换实现"的典型手段:桌面端走 src/node/,浏览器端由插件强制替换为 src/browser/ 下的安全实现,比单纯维护两个入口文件更细粒度。

从该扩展的目录结构看,src/ 下同时存在 node/browser/ 两组实现目录,tsconfig.browser.jsonfiles 收敛 + esbuild 插件重定向两者配合,就构成了"browser-safe 约束"的完整闭环:静态层面由 platform: 'browser'mainFields/alias/define 处理依赖替换,入口层面由类型检查保证不引用 Node-only 模块。

六、新扩展落地的核对清单

综合 CONTRIBUTING 文档与仓库内实例,新建一个内置扩展时可按以下清单核对:

  1. 目录骨架:package.json + src/ + tsconfig.json + esbuild.mts + .vscodeignore.vscodeignore 可复制 extensions/debug-auto-launch/.vscodeignore);
  2. tsconfig.json"extends": "../tsconfig.base.json" 继承基类,配置 rootDir: ./srcoutDir: ./out,并在 include 中加入 ../../src/vscode-dts/vscode.d.ts
  3. esbuild.mts 调用 extensions/esbuild-extension-common.mtsrun()platform: 'node'outdir 指向 dist/,入口指向 src/extension.ts
  4. package.jsonmain 指向 ./out/...(开发),提供 compile/watch 两个 gulp 转调脚本;
  5. 如需支持浏览器:加 "browser": "./dist/browser/<入口>" 字段、tsconfig.browser.jsonfiles 收敛到浏览器入口)、esbuild.browser.mtsplatform: 'browser'outdirdist/browser,必要时用插件重定向 Node-only 模块),并确认浏览器入口只使用 browser-safe API。

以上每条都能在仓库中找到现成范本:纯桌面端参考 extensions/debug-auto-launch/,桌面 + 浏览器双目标参考 extensions/configuration-editing/

登录后查看全文
热门项目推荐
相关项目推荐