从零开发一个Coding Agent(二):monorepo项目搭建

616 阅读6分钟

本篇文章是《从零开发一个Coding Agent》系列第二篇,主要介绍如何使用npm搭建一个monorepo项目,以及如何管理多个子项目。如果你对Agent开发也感兴趣,欢迎点赞收藏+关注。专栏:从零开发一个Coding Agent - 东方小月的专栏 - 掘金

什么是monorepo项目

monorepo项目是指在一个代码仓库中管理多个子项目的项目。每个子项目都是一个独立的npm包,它们之间通过npm的依赖管理机制进行协调。npm workspaces 中,本地包之间仍然通过 package.json 声明依赖;区别是安装时 npm 会把依赖链接到本地 workspace,,而不是从 npm registry 下载。 举个例子: agent包 需要引用 ai 包。

根目录已经声明:

{
  "workspaces": ["packages/*"]
}

packages/ai/package.json:

{
  "name": "@di-code/ai",
  "version": "0.0.0"
}

在 packages/agent/package.json 中声明:

{
  "name": "@di-code/agent",
  "version": "0.0.0",
  "dependencies": {
    "@di-code/ai": "0.0.0"
  }
}

然后从根目录安装: npm install

npm 会建立类似下面的本地链接:

node_modules/@di-code/ai
    -> D:\pi\di-code\packages\ai

Windows 上实际可能是 junction(目录联接),但效果与符号链接一致,不会复制一份源码。 于是 agent 中可以像使用普通 npm 包一样导入:

import type { Message } from "@di-code/ai";

不要使用跨目录相对导入:

import type { Message } from "../../ai/src/types.js";

环境和目录

接下来我们就进入了项目的真正开发了,首先需要检查自己的环境是否符合要求。node版本至少为22.19.0

新建项目目录,我这里选择名称为di-code,并初始化根 package.json

{
	"name": "di-code",
	"version": "0.0.0",
	"private": true,
	"type": "module",
	"workspaces": ["packages/*"],
	"scripts": {
		"build": "npm run build --workspace @di-code/tui && npm run build --workspace @di-code/ai && npm run build --workspace @di-code/agent && npm run build --workspace @di-code/coding-agent",
		"check": "biome check . && tsc --noEmit -p tsconfig.json",
		"test": "npm run test --workspaces --if-present"
	},
	"devDependencies": {
		"@biomejs/biome": "2.3.5",
		"@types/node": "22.19.19",
		"typescript": "5.9.3",
		"vitest": "4.1.9"
	},
	"engines": {
		"node": ">=22.19.0"
	}
}

关键字段说明:

  • private: true 防止对根聚合包执行意外发布。
  • type: module 确立根脚本的 ESM 语义。
  • workspaces 只匹配四个直接子包,不把未来示例或临时目录意外变成 workspace。
  • build 的显式顺序对应未来依赖方向。
  • check 先运行 Biome;只有 Biome 成功才运行 TypeScript。
  • test 将测试命令分发到所有定义了 test script 的 workspace。
  • 所有 devDependency 都使用精确版本,不使用 ^~

然后新建packages目录,用于存放所有的子项目。在该目录下创建ai, agent, coding-agent, tui四个子目录。 每个子目录下创建src目录,用于存放源代码。

TypeScript配置

在 根目录创建一个tsconfig.base.json文件:

{
	"compilerOptions": {
		"target": "ES2022",
		"lib": ["ES2022"],
		"module": "NodeNext",
		"moduleResolution": "NodeNext",
		"strict": true,
		"erasableSyntaxOnly": true,
		"verbatimModuleSyntax": true,
		"noEmitOnError": true,
		"declaration": true,
		"declarationMap": true,
		"sourceMap": true,
		"inlineSources": true,
		"esModuleInterop": true,
		"forceConsistentCasingInFileNames": true,
		"skipLibCheck": true,
		"resolveJsonModule": true,
		"types": ["node"]
	}
}

部分选项说明:

  • target 决定输出 JavaScript 的语法基线;ES2022 在最低 Node 22 运行时上可用。
  • modulemoduleResolution 同时使用 NodeNext,让 TypeScript 按现代 Node.js 的 ESM/CJS 规则判断包边界。不要把一个改为 NodeNext、另一个留成旧的 node
  • strict 是项目约束,后续不得为了消除错误而关闭。
  • verbatimModuleSyntax 要求 import/export 写法与最终模块语义一致,并促使类型导入使用 import type
  • noEmitOnError 防止类型错误时留下看似可用的构建产物。
  • declarationdeclarationMap 为 workspace 公共类型生成 .d.ts 与映射。
  • sourceMapinlineSources 为后续调试保留源码定位。
  • skipLibCheck 只跳过第三方声明文件内部的检查,不跳过本项目源码检查。
  • types: ["node"] 明确引入 Node.js 全局类型,避免环境中其他 @types/* 包意外污染全局命名空间。

然后在根目录写入TS配置文件tsconfig.json

{
	"extends": "./tsconfig.base.json",
	"compilerOptions": {
		"noEmit": true
	},
	"include": ["packages/*/src/**/*.ts", "packages/*/test/**/*.ts"],
	"exclude": ["**/node_modules/**", "**/dist/**"]
}

根配置的职责是全仓检查,不是构建:

  • include 同时覆盖源码和未来测试。
  • noEmit 保证 npm run check 不修改工作区。
  • dist 被排除,避免检查生成文件并重复报告问题。

编写四个 workspace package.json

在四个目录 ai, agent, coding-agent, tui 下创建 package.json 文件。它们的脚本和导出形状保持一致,名称和描述不同。

packages/ai/package.json

{
	"name": "@di-code/ai",
	"version": "0.0.0",
	"description": "Provider-neutral AI contracts and adapters for di-code",
	"private": true,
	"type": "module",
	"main": "./dist/index.js",
	"types": "./dist/index.d.ts",
	"exports": {
		".": {
			"types": "./dist/index.d.ts",
			"import": "./dist/index.js"
		}
	},
	"scripts": {
		"build": "tsc -p tsconfig.build.json",
		"test": "vitest run --passWithNoTests"
	},
	"engines": {
		"node": ">=22.19.0"
	}
}

packages/agent/package.json

{
	"name": "@di-code/agent",
	"version": "0.0.0",
	"description": "Core agent state and loop for di-code",
	"private": true,
	"type": "module",
	"main": "./dist/index.js",
	"types": "./dist/index.d.ts",
	"exports": {
		".": {
			"types": "./dist/index.d.ts",
			"import": "./dist/index.js"
		}
	},
	"scripts": {
		"build": "tsc -p tsconfig.build.json",
		"test": "vitest run --passWithNoTests"
	},
	"engines": {
		"node": ">=22.19.0"
	}
}

packages/coding-agent/package.json

{
	"name": "@di-code/coding-agent",
	"version": "0.0.0",
	"description": "Coding agent product layer for di-code",
	"private": true,
	"type": "module",
	"main": "./dist/index.js",
	"types": "./dist/index.d.ts",
	"exports": {
		".": {
			"types": "./dist/index.d.ts",
			"import": "./dist/index.js"
		}
	},
	"scripts": {
		"build": "tsc -p tsconfig.build.json",
		"test": "vitest run --passWithNoTests"
	},
	"engines": {
		"node": ">=22.19.0"
	}
}

packages/tui/package.json

{
	"name": "@di-code/tui",
	"version": "0.0.0",
	"description": "Reusable terminal UI library for di-code",
	"private": true,
	"type": "module",
	"main": "./dist/index.js",
	"types": "./dist/index.d.ts",
	"exports": {
		".": {
			"types": "./dist/index.d.ts",
			"import": "./dist/index.js"
		}
	},
	"scripts": {
		"build": "tsc -p tsconfig.build.json",
		"test": "vitest run --passWithNoTests"
	},
	"engines": {
		"node": ">=22.19.0"
	}
}
  • main 指向运行时 ESM 入口。
  • types 指向 TypeScript 声明入口。
  • exports 限制包的公开入口为根入口,防止后续调用者依赖内部文件路径。

编写 workspace 构建配置

在以下四个位置创建相同内容的 tsconfig.build.json

packages\ai\tsconfig.build.json
packages\agent\tsconfig.build.json
packages\coding-agent\tsconfig.build.json
packages\tui\tsconfig.build.json

内容均为:

{
	"extends": "../../tsconfig.base.json",
	"compilerOptions": {
		"rootDir": "./src",
		"outDir": "./dist"
	},
	"include": ["src/**/*.ts"],
	"exclude": ["node_modules", "dist", "test"]
}
  • extends 让四个包继承完全相同的严格模式和 ESM 语义。
  • rootDir 告诉编译器源码相对根为 src
  • outDir 使输出留在各自包内,不把不同包混到根 dist
  • build 只包含 src,测试不进入发布产物。

入口创建

在以下四个文件中各写一行:

packages\ai\src\index.ts
packages\agent\src\index.ts
packages\coding-agent\src\index.ts
packages\tui\src\index.ts

内容:

export {};

配置 Biome 格式化工具

Biome 是面向 JavaScript、TypeScript、JSON 等文件的代码质量工具,简单来说就是ESLint + Prettier

di-code\biome.json 写入:

{
	"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
	"files": {
		"includes": [
			"package.json",
			"biome.json",
			"tsconfig*.json",
			"packages/*/package.json",
			"packages/*/tsconfig*.json",
			"packages/*/src/**/*.ts",
			"packages/*/test/**/*.ts"
		]
	},
	"formatter": {
		"enabled": true,
		"formatWithErrors": false,
		"indentStyle": "tab",
		"lineWidth": 120
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	}
}

编写 .gitignore

在根目录下创建 .gitignore 写入:

# Dependencies
node_modules/

# Build and test output
dist/
coverage/
*.tsbuildinfo

# Logs
*.log
npm-debug.log*

# Environment and credentials
.env
.env.*
!.env.example

# Editors and operating systems
.vscode/
.idea/
.DS_Store
Thumbs.db

这些都不需要提交代码仓库

安装依赖与构建

到这里我们基本已经完成了基础的 monorepo 配置。 接下来,我们可以开始安装依赖并构建项目。

npm install
npm run build

构建完成之后我们就能看到每个包下面都有一个 dist 目录,里面包含了编译后的代码。

最后执行一下检查,确保代码质量。

npm run check

如果 Biome 只报告格式差异,可以执行

npx biome check --write package.json tsconfig.base.json tsconfig.json biome.json packages

进行修复,然后再执行 npm run check 确保格式符合要求。

到这里我们已经完成了基础的 monorepo 配置。本章节git分支Monorepo 初始化