跳转到内容

Monorepo 构建与分发

Monorepo 构建与分发

学习目标

本章要解决的核心问题:一个包含 Core、GUI、CLI、VS Code 扩展、JetBrains 扩展和多个共享包的 Monorepo,如何组织代码、构建和分发?

你将学到:

  • Monorepo 的目录结构与包划分
  • 并发 TypeScript Watch 的开发体验优化
  • core/ 作为命名空间的路径别名设计
  • GUI 构建(Vite)、CLI 打包(esbuild)、VS Code 扩展打包(vsce)
  • 原生模块(LanceDB)的平台兼容性处理

项目实践

Monorepo 目录结构

continue/
├── package.json # 根 package.json(npm workspaces)
├── core/ # 核心引擎(最大的包)
│ ├── core.ts # Core 类
│ ├── index.d.ts # 类型定义(2000+ 行)
│ ├── package.json
│ ├── config/ # 配置管理
│ ├── llm/ # LLM 抽象层
│ ├── indexing/ # 代码索引
│ ├── autocomplete/ # 补全引擎
│ ├── context/ # 上下文提供者
│ ├── tools/ # 工具系统
│ ├── protocol/ # IPC 协议
│ └── ...
├── packages/ # 共享包
│ ├── config-yaml/ # YAML 配置解析
│ ├── openai-adapters/ # LLM 提供商适配器
│ ├── fetch/ # 带代理的 fetch
│ ├── llm-info/ # LLM 元数据
│ ├── continue-sdk/ # Continue SDK
│ ├── hub/ # Hub 客户端
│ └── terminal-security/ # 终端命令安全
├── extensions/ # IDE 扩展
│ ├── vscode/ # VS Code 扩展
│ ├── intellij/ # JetBrains 插件
│ └── cli/ # CLI 工具
├── gui/ # Web GUI(React)
├── binary/ # 二进制打包
└── actions/ # GitHub Actions

并发 TypeScript Watch

package.json 定义了并发编译:

{
"scripts": {
"tsc:watch": "concurrently -n gui,vscode,core,binary -c cyan,magenta,yellow,green \"npm run tsc:watch:gui\" \"npm run tsc:watch:vscode\" \"npm run tsc:watch:core\" \"npm run tsc:watch:binary\""
}
}

使用 concurrently 同时启动四个 TypeScript 编译器:

子项目编译器输出
guitsc --project gui/tsconfig.json --watchGUI 类型检查
vscodetsc --project extensions/vscode/tsconfig.json --watchVS Code 扩展类型检查
coretsc --project core/tsconfig.json --watchCore 类型检查
binarytsc --project binary/tsconfig.json --watchBinary 类型检查

关键优化:每个子项目独立编译,互不阻塞。修改 core/ 中的类型时,VS Code 扩展和 GUI 会自动重新检查(因为它们引用 core/ 的类型)。

core/ 命名空间

Continue 使用路径别名让所有包都通过 import "core/xxx" 引用 Core:

core/tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"core/*": ["./*"]
}
}
}

其他包通过 TypeScript path alias 或 npm workspace 引用:

// 在 VS Code 扩展中
import { IDE, ContinueConfig } from "core";
import { VsCodeIde } from "./VsCodeIde";
// 在 packages 中
import { BaseLlmApi } from "@continuedev/openai-adapters";

这种设计的关键决策:

  • core/ 不是 npm 包,它不发布到 npm
  • 它作为源码引用目标,通过路径别名被其他包引用
  • 构建时需要将 core/ 的源码合并到输出中(或作为依赖)

GUI 构建(Vite)

GUI 是一个独立的 React 应用,使用 Vite 构建:

gui/vite.config.ts
export default defineConfig({
plugins: [react()],
css: { postcss: true },
build: {
outDir: "../extensions/vscode/dist/gui", // 输出到 VS Code 扩展目录
rollupOptions: {
// 打包配置
}
}
});

关键设计:

  • GUI 构建产物直接输出到 VS Code 扩展的 dist/gui/ 目录
  • 使用 Tailwind CSS 处理样式
  • 通过 Vite 的 define 注入运行时变量(如服务器 URL)

CLI 打包(esbuild)

CLI 使用 esbuild 编译:

extensions/cli/build.mjs
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
platform: "node",
target: "node20",
outfile: "dist/index.js",
external: [
// 排除 native modules
"@lancedb/vectordb-linux-x64-gnu",
// ...
],
});

CLI 安装方式:

Terminal window
# 通过 curl 脚本安装
curl -fsSL https://raw.githubusercontent.com/continuedev/continue/main/extensions/cli/scripts/install.sh | bash
# 或通过 npm
npm i -g @continuedev/cli

VS Code 扩展打包

VS Code 扩展使用 vsce 打包:

Terminal window
vsce package # 生成 .vsix 文件
vsce publish # 发布到 VS Code Marketplace

package.json 中的 contributes 定义:

  • 命令(commands)
  • 视图容器(viewsContainers)
  • 配置项(configuration)
  • 语言(languages)

原生模块处理

LanceDB 等包包含原生代码(native modules),在不同平台上需要不同的二进制文件:

package.json
"optionalDependencies": {
"@lancedb/vectordb-darwin-arm64": "*",
"@lancedb/vectordb-linux-x64-gnu": "*",
"@lancedb/vectordb-win32-x64-msvc": "*"
}

npm 会根据当前平台自动选择正确的子包。在构建 CLI 的二进制版本时,这些原生模块通过 pkg 打包。


问题与规避

陷阱 1:路径别名在运行时不可用

现象:开发时 import "core/xxx" 正常工作,但打包后找不到模块。

规避:构建配置中需要正确处理路径别名。Vite 和 esbuild 都支持 TypeScript 的 path alias 配置。VS Code 扩展打包时,core/ 的源码被合并到 dist/ 中。

陷阱 2:并发 Watch 的资源消耗

现象:四个 TypeScript 编译器同时运行时占用大量内存。

规避:TypeScript --watch 模式只重新编译变更的文件,增量编译的性能开销可控。在大型项目中可以考虑使用 turboreponx 等缓存构建工具。

陷阱 3:跨包类型引用

现象:修改 core/index.d.ts 后,其他包的类型检查需要等待 tsc:watch 重新编译。

规避tsc:watch 的增量编译通常在 1-2 秒内完成。如果需要即时反馈,可以使用 tsc --noEmit 做一次性全量检查。


设计取舍

为什么不是 Turborepo / Nx

方案优势代价
npm workspaces + concurrently简单、零额外依赖、npm 原生支持无构建缓存、无任务依赖图
Turborepo / Nx构建缓存、任务依赖、远程缓存额外配置复杂度、学习曲线

Continue 选择了更简单的方案,因为:

  • 项目规模适中(6 个主要包)
  • concurrently 的并行编译已经足够快
  • 不需要复杂的任务依赖图

Core 作为命名空间 vs 独立 npm 包

方案优势代价
路径别名(当前方案)开发体验好、无需发布构建配置复杂、IDE 需要正确配置
npm 包标准引用、版本管理每次修改都需要重新发布

Continue 选择路径别名,因为 core/ 是所有扩展的内核,与扩展代码紧密耦合。作为独立 npm 包会导致发布周期与扩展不同步的问题。


参考来源