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 编译器:
| 子项目 | 编译器 | 输出 |
|---|---|---|
| gui | tsc --project gui/tsconfig.json --watch | GUI 类型检查 |
| vscode | tsc --project extensions/vscode/tsconfig.json --watch | VS Code 扩展类型检查 |
| core | tsc --project core/tsconfig.json --watch | Core 类型检查 |
| binary | tsc --project binary/tsconfig.json --watch | Binary 类型检查 |
关键优化:每个子项目独立编译,互不阻塞。修改 core/ 中的类型时,VS Code 扩展和 GUI 会自动重新检查(因为它们引用 core/ 的类型)。
core/ 命名空间
Continue 使用路径别名让所有包都通过 import "core/xxx" 引用 Core:
{ "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 构建:
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 编译:
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 安装方式:
# 通过 curl 脚本安装curl -fsSL https://raw.githubusercontent.com/continuedev/continue/main/extensions/cli/scripts/install.sh | bash
# 或通过 npmnpm i -g @continuedev/cliVS Code 扩展打包
VS Code 扩展使用 vsce 打包:
vsce package # 生成 .vsix 文件vsce publish # 发布到 VS Code Marketplacepackage.json 中的 contributes 定义:
- 命令(commands)
- 视图容器(viewsContainers)
- 配置项(configuration)
- 语言(languages)
原生模块处理
LanceDB 等包包含原生代码(native modules),在不同平台上需要不同的二进制文件:
"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 模式只重新编译变更的文件,增量编译的性能开销可控。在大型项目中可以考虑使用 turborepo 或 nx 等缓存构建工具。
陷阱 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 包会导致发布周期与扩展不同步的问题。