monorepo 依赖图工具怎么选:Nx、Turborepo、Madge、dependency-cruiser、jdeps 和 repowise

Raghav Chamadiya12 分钟

monorepo 依赖图 · 代码依赖关系可视化 · 循环依赖检测 · 依赖分析工具 · Nx 依赖图 · jdeps

本页目录

选 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文件 / 模块是,并能写成规则卡在 CIJS/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.ts
  • transport.ts → index.ts
  • index.ts → connection.ts
  • 另外 connection.ts 也直接 import index.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 可以这样加:

bash
qwen mcp add --transport http repowise https://api.repowise.dev/mcp/OWNER/REPO \
  --header "Authorization: Bearer YOUR_API_KEY"

API key 在 repowise 登录后的 Settings → Editor 里生成。

怎么选

  1. 主要问题是构建慢、CI 跑太多:Nx 或 Turborepo。
  2. 主要问题是前端 import 乱、想在 CI 里卡住边界:dependency-cruiser,临时排查用 Madge。
  3. Java/Go 后端排查依赖冲突:mvn dependency:tree、gradle dependencies、go mod graph,需要包级分析用 jdeps。
  4. 想知道仓库里哪里在打结、先拆哪一个、谁负责: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。

索引你的仓库, 免费