本页目录
选 monorepo 依赖图工具,先问清楚你要哪一层。只关心构建顺序、缓存和受影响的任务,用 Nx 或 Turborepo。要在 JS/TS 里抓循环 import、守模块边界,用 Madge 或 dependency-cruiser。Java、Go 项目先用 jdeps、go mod graph 这些自带工具。想同时看到文件级的循环依赖、热点和改动风险,再考虑 repowise。
| 工具 | 图的粒度 | 找循环依赖 | 语言范围 | 带历史和负责人信息 |
|---|---|---|---|---|
| Nx | 项目 + 任务 | 间接(靠边界规则) | 以 JS/TS 为主,插件扩展 | 否 |
| Turborepo | 包 + 任务 | 不是重点 | JS/TS | 否 |
| Madge | 文件 / 模块 | 是(--circular) | JS/TS | 否 |
| dependency-cruiser | 文件 / 模块 | 是,并能写成规则卡在 CI | JS/TS | 否 |
| jdeps / mvn dependency:tree / gradle dependencies | 包、模块、构件 | jdeps 能看包级环 | Java | 否 |
| go mod graph / go list -deps | 模块、包 | Go 编译器本身禁止包级循环 | Go | 否 |
| repowise | 包 + 文件 + 符号 | 是,每组循环一页说明 | 16 种语言,解析到完整 AST | 是 |
Scroll the table sideways to see every column.
为什么 monorepo 的依赖图越来越难看懂
仓库小的时候,依赖关系在每个人脑子里。仓库变成几百个包、上万个文件之后,脑子里那张图就不准了,常见的问题有这些:
- 改一个公共包,不知道下游哪些服务会受影响,只能把 CI 全跑一遍。
- 循环依赖平时没人发现,等到想把某个模块拆出去时才发现拆不动。
- 构建依赖和源码依赖混在一起,
package.json里写着依赖,代码里其实早就不用了。 - 分不清一个文件是“核心”,还是只是“年头久”。
国内很多团队的情况还要再复杂一点:后端是 Java 或 Go,前端是 TS 的 monorepo,中间还有 Python 写的脚本和数据任务。一个工具很难把这些都画在一张图上,所以选工具之前,要先分清包级和文件级这两层。
两层图:包级和文件级
包级(项目级)依赖图回答的是:哪些包依赖这个包?改名会影响谁?构建和发布的顺序是什么?包和包之间有没有环?Nx、Turborepo、Maven、Gradle、Go modules 都工作在这一层。
文件级(符号级)依赖图回答的是:真正的风险落在哪个文件、哪个函数?哪几个文件互相 import、谁也离不开谁?如果要把一个模块拆出去,边界应该划在哪?
包级图会告诉你“A 依赖 B”。但实际出问题的地方,往往是 B 里面某一个工具文件,被十几个地方 import,又反过来 import 了调用它的代码。包级图看不到这一层。
Nx:任务编排为主,依赖图是底座
Nx 的项目图(project graph)记录工作区里有哪些项目、彼此怎么依赖,再用这张图决定构建顺序、缓存和 affected 命令只跑受影响的部分。它也有图形界面可以浏览依赖关系。
适合:已经用 Nx 跑任务的团队;以 TypeScript 为主的工作区;想用 tag 和规则约束项目边界。
局限:它是工作区层面的图。它告诉你项目之间怎么连,但不会告诉你某个文件为什么是热点、谁在维护、上次出问题是什么时候。
Turborepo:包图加任务流水线
Turborepo 从包管理器(npm、pnpm、yarn)读出包依赖图,再把 build、lint、test 这些任务组织成一张有向无环图(DAG)。上手成本低,适合前端 monorepo 做缓存和并行。
局限:它是一个带依赖感知的任务运行器,设计目标是缓存和并行,循环依赖、死代码和文件级的依赖关系都不在它的范围内。
Madge 和 dependency-cruiser:JS/TS 的 import 体检
Madge 是查循环 import 最直接的工具。madge --circular src/ 直接列出环,也能导出 DOT 或 SVG 交给 Graphviz 画图。
dependency-cruiser 更偏“规则”:你可以写“packages/ui 不允许 import packages/server”这样的约束,在 CI 里不通过就失败。它也能输出 DOT、JSON,方便后续分析。
这两个工具我都会推荐给纯前端团队。它们的边界也很清楚:只管 JS/TS,只看 import 关系,不看 git 历史,也不管后端。
Java 和 Go 项目:先用自带工具
后端为主的团队,很多时候不需要额外装东西。
- Java:
mvn dependency:tree和gradle dependencies看构件(jar)之间的依赖,排查依赖冲突、版本打架最常用。JDK 自带的jdeps能分析到包和模块级别,也能看出包之间有没有环。 - Go:
go mod graph列出模块依赖,go list -deps ./...列出包依赖。Go 编译器本身不允许包之间循环 import,所以包级循环在 Go 里不会存在。但这不代表 Go 项目没有结构问题:同一个包里的文件可以随意互相调用,一个包慢慢长成几百个文件的大杂烩,这在依赖图上是看不出来的。
这些工具的结果都很准确,但只能看到构件、模块和包这一层。
repowise:文件级依赖图加历史
repowise 用 tree-sitter 把代码解析成 AST,建出文件和符号级别的有向依赖图,再叠加 git 历史:哪些文件改得最多、修 bug 的提交集中在哪、谁是主要作者。每一组循环依赖会单独生成一页说明,列出环里的文件、每条边,以及从哪个文件下手拆最划算。
举一个真实的例子。deepseek-ai/deepseek-harness 是一个 TypeScript 为主的 monorepo,12,578 个文件、约 90 万行代码(898,685 行)。按 Turborepo 或 Nx 的视角,它是一堆整齐的 package。按文件级的依赖图看,repowise 在里面找出了 47 组循环依赖。
其中一组在它自己的 MCP 客户端里(循环依赖页面):
packages/mcp/mcp-client/src/connection.ts→transport.tstransport.ts→index.tsindex.ts→connection.ts- 另外
connection.ts也直接 importindex.ts
三个文件互相 import,哪一个都没法单独拿出来加载、测试或者抽成独立模块。页面按“在环里承担了几条边”排序,connection.ts 排第一(环内 import 2 条,被 import 1 条),建议从它开始拆,比如把它依赖的类型抽到一个独立文件。
这种环在包级图上完全看不到,因为三个文件都在同一个 package 里。这样的环也不止一组:其他几组的名字里有 Sandbox、Tools、Session Persistence Jsonl、Subagent、Llm、Schedule、Jobs,基本覆盖了一个 agent harness 的核心部分。
再看一个更大的仓库。grafana/grafana 有 18,146 个文件、338 万行代码,repowise 找到 66 组循环依赖,有的叫“Grafana Data”,有的叫“Grafana Ui Components”。repowise 的健康度是 1 到 10 分的评分,衡量一个文件引发 bug 的可能性和修改它的难度,依据是静态检查加上 git 历史;仓库得分是各文件得分按代码行数加权的平均值。Grafana 页面显示的健康度是 7.9 分(满分 10),在大仓库里算高的:30 万行以上的 45 个热门仓库里排第 9,100 万行以上的 8 个里排第 2。可见有循环依赖不等于代码差,大仓库几乎都有,要弄清的是它们在哪、哪些经过热点文件。
所以我会把 git 历史和依赖图放在一起看。deepseek-harness 里修 bug 提交最多的文件之一是 packages/core/agent-loop/src/agent.ts(78 次修复类提交)。如果一个环恰好经过这种文件,它就该排在前面处理;如果环里都是一年没人碰的文件,可以先放着。
repowise 的局限也要说清楚:如果你只想要任务编排,它多余;如果只想在 JS 里快速查一个环,Madge 更轻;它的分析基于静态 import,运行时动态加载的依赖看不到。
拆一个循环依赖,通常怎么做
找到环之后,常见的拆法有下面几种:
- 抽出共享类型:环里的文件互相 import,很多时候只是为了几个类型或常量。把这些类型挪到一个不依赖任何人的文件里,环就断了。上面 MCP 客户端的例子,页面列出的环内符号大多是
ReconnectConfig这样的类型和常量,很可能就属于这种情况,但要看过代码才能确定。 - 依赖倒置:底层模块需要调用上层逻辑时,不要直接 import 上层,而是定义一个接口,由上层在启动时注入实现。
- 合并:如果两个文件谁也离不开谁,也许它们本来就该是一个模块。强行拆开反而增加理解成本。
选哪种,取决于环里的文件是什么角色、多久改一次,后一项只有提交历史才能告诉你。
让 AI 编程助手也能用这张图
现在 AI 编程助手也开始用依赖图。repowise 提供 MCP 服务,助手可以直接调用 get_overview、get_context、get_risk 这些工具查依赖和风险,不用一个个文件去读。
模型也不限于 Claude:按各家官方文档,DeepSeek 和 Kimi 都提供了兼容 Anthropic 格式的接口,可以在 Claude Code 里换成它们的模型(DeepSeek 是 ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic);MCP 客户端跑在本地,模型只需要支持工具调用。用通义千问的话,Qwen Code 可以这样加:
qwen mcp add --transport http repowise https://api.repowise.dev/mcp/OWNER/REPO \
--header "Authorization: Bearer YOUR_API_KEY"
API key 在 repowise 登录后的 Settings → Editor 里生成。
怎么选
- 主要问题是构建慢、CI 跑太多:Nx 或 Turborepo。
- 主要问题是前端 import 乱、想在 CI 里卡住边界:dependency-cruiser,临时排查用 Madge。
- Java/Go 后端排查依赖冲突:
mvn dependency:tree、gradle dependencies、go mod graph,需要包级分析用 jdeps。 - 想知道仓库里哪里在打结、先拆哪一个、谁负责:repowise。
这几类工具并不冲突。常见的组合是:Nx 或 Turborepo 跑任务,dependency-cruiser 守边界,repowise 用来看整体结构和风险。
试一试
在 repowise.dev 贴任何一个 GitHub 仓库地址,就能看到它的依赖图和循环依赖;想让编程助手直接用,看连接 IDE 的文档。本地也可以 pip install repowise && repowise init。
写作说明
文中仓库数据来自 repowise 已索引的公开快照:deepseek-harness 为 2026-09-28 的快照,grafana 为 2026-08-11 的快照。循环依赖数量是 repowise 生成的“循环依赖”文档页数量,基于静态 import 分析,运行时动态加载的依赖不在其中。数据可以在对应仓库页面上核对。第三方工具的能力描述来自各自的官方文档。
常见问题
monorepo 用什么工具画依赖图?
看你要哪一层。包级依赖图用 Nx(`nx graph`)或 Turborepo;文件级依赖和循环 import 用 Madge、dependency-cruiser(JS/TS);跨语言、要结合 git 历史的用 repowise。
怎么检测循环依赖?
JS/TS 项目可以用 `madge --circular`,或在 dependency-cruiser 里加禁止循环的规则。Java 可以用 jdeps 看包之间的环。Go 编译器禁止包级循环,但同一个包内部的纠缠需要文件级工具才能看出来。repowise 会为每组循环依赖单独生成一页,列出环里的文件和建议拆的位置。
包级依赖图和文件级依赖图有什么区别?
包级图看的是 package、模块、jar 之间的关系,适合决定构建顺序和影响范围。文件级图看的是文件、类、函数之间的关系,适合找循环依赖、划拆分边界。同一个 package 里的循环依赖,包级图是看不到的。
Java 项目怎么看依赖关系?
构件层面用 `mvn dependency:tree` 或 `gradle dependencies`,主要用来排查版本冲突。包和模块层面用 JDK 自带的 `jdeps`。如果要看类和文件之间的依赖、结合提交历史看热点,需要额外的分析工具。
循环依赖一定要消除吗?
不一定。大仓库几乎都有循环依赖,Grafana 有 66 组,健康度仍有 7.9 分。优先处理经过热点文件、经常一起改、或者阻碍你拆模块的那几组。
依赖图能给 AI 编程助手用吗?
可以,通过 MCP。repowise 的 MCP 服务可以接入 Claude Code、Cursor、Qwen Code 等客户端,模型可以是 Claude,也可以按官方文档换成 DeepSeek 或 Kimi。