Содержание
- Граф большого монорепозитория в голове не удержать
- Два уровня графа: пакеты и символы
- Nx
- Turborepo
- Madge
- dependency-cruiser
- Go: граф уже есть в тулчейне
- Java и Kotlin: jdeps, Maven, Gradle
- Циклические зависимости: как найти и где резать
- Как я бы искал и разбирал цикл
- Где здесь repowise
- Как выбрать
- Как мы считали
- Частые вопросы
Короткий ответ такой: если граф нужен, чтобы собирать и тестировать только затронутое, берите Nx или Turborepo. Найти циклические импорты в JS/TS поможет Madge, а запретить их в CI можно с помощью dependency-cruiser. Для Go и Java граф пакетов уже есть в стандартном инструментарии. Если нужен граф до уровня файлов и функций вместе с историей изменений и владельцами, для этого есть repowise.
| Инструмент | Уровень графа | Циклы | Языки | История и владельцы |
|---|---|---|---|---|
| Nx | проекты + задачи | косвенно, через границы | в основном JS/TS, остальное плагинами | нет |
| Turborepo | пакеты + задачи | не его задача | JS/TS | нет |
| Madge | файлы и модули | да, --circular | JS/TS | нет |
| dependency-cruiser | файлы и модули | да, правилами | JS/TS | нет |
go list -deps, go mod graph | пакеты и модули Go | компилятор запрещает циклы пакетов | Go | нет |
jdeps, mvn dependency:tree, gradle dependencies | классы/пакеты, артефакты | видно в выводе jdeps (-dotoutput), в выводе сборщиков нет | Java/Kotlin | нет |
| repowise | пакеты, файлы, символы | да, с разбором каждого цикла | 16 языков | да |
Scroll the table sideways to see every column.
Ниже каждый инструмент разобран подробнее, с цифрами по реальным репозиториям.
Граф большого монорепозитория в голове не удержать
Пока в репозитории десяток пакетов, связи между ними помнит каждый, кто с ним работает. Когда пакетов сотни, а файлов десятки тысяч, на простые вопросы уже никто не ответит уверенно: что сломается, если поменять этот пакет; где цикл, из-за которого модуль не получается вынести; какой файл тянет за собой половину репозитория.
Масштаб хорошо видно на открытых проектах. В Grafana 18 146 файлов и 3 379 253 строки кода. В Keycloak 10 414 файлов и чуть больше миллиона строк, в Gitea 3 158 файлов и 512 376 строк. Ни один человек не держит такие графы в голове, и это нормально, но плохо, если их не видит и ни один инструмент.
Два уровня графа: пакеты и символы
Сначала стоит решить, на каком уровне нужен ответ.
Граф пакетов отвечает на вопросы сборки: в каком порядке собирать, какие пакеты затронуты изменением, что пересобрать в CI. На этом уровне работают Nx, Turborepo, go list, Maven и Gradle.
Граф файлов и символов отвечает на вопросы о правке кода: какой файл держит цикл, через какую функцию идёт половина импортов, где лежит безопасная граница для рефакторинга. Граф пакетов может честно сказать «A зависит от B», а весь риск при этом сидит в одном внутреннем утилитном файле, который импортируют все.
Большинство инструментов работают на одном уровне, и при выборе это надо учитывать.
Nx
Nx строит граф проектов воркспейса, а на его основе граф задач: что собирать, что брать из кэша, какие проекты затронуты (nx affected). Граф можно открыть в браузере через nx graph. Границы между проектами задаются тегами и правилами, и нарушения ловит линтер.
Где хорош: TypeScript-монорепозитории, в которых граф нужен прежде всего для CI и кэширования.
Где заканчивается: Nx знает, как связаны проекты, но не пытается объяснять код внутри них. Владельцев файлов, частоту изменений и горячие точки он не показывает.
Turborepo
Turborepo берёт граф пакетов из менеджера пакетов (npm, pnpm, yarn) и строит поверх него граф задач в виде DAG. Это самый простой способ получить кэширование и параллельный запуск build/lint/test в JS/TS-монорепозитории.
Где заканчивается: Turborepo задуман как таск-раннер, который знает про граф. Архитектуру он не исследует, циклы между файлами и мёртвый код не ищет.
Madge
Madge строит граф модулей JS/TS и находит циклы одной командой:
npx madge --circular --extensions ts,tsx src/
Умеет выводить граф в DOT и SVG (через Graphviz), искать файлы-сироты (--orphans) и листья (--leaves). Если вы подозреваете циклический импорт, это самый быстрый первый шаг.
Где заканчивается: уровень файлов и модулей, только JS/TS, без истории и без контекста монорепозитория.
dependency-cruiser
dependency-cruiser ближе к линтеру архитектуры. Правила описываются в конфиге (например, «пакет ui не импортирует server» или «циклов быть не должно»), и при нарушении CI падает. Отчёты выводятся в DOT, JSON и CSV.
Где хорош: когда граница уже понятна и её нужно удержать.
Где заканчивается: он проверяет правила, но не объясняет, почему граница проходит именно здесь.
Go: граф уже есть в тулчейне
В Go отдельный инструмент для графа пакетов часто не нужен:
go list -deps ./... # все пакеты, от которых зависит модуль
go mod graph # граф зависимостей между модулями
Циклические импорты пакетов в Go запрещены компилятором, поэтому классической проблемы «пакет A импортирует B, а B импортирует A» нет. Но связность от этого никуда не девается, она уходит внутрь пакетов: в больших пакетах десятки файлов зависят друг от друга. В Gitea мы нашли 7 групп циклических зависимостей на уровне файлов. Для полумиллиона строк это немного, но как раз такие циклы компилятор не ловит.
Java и Kotlin: jdeps, Maven, Gradle
Для JVM есть три стандартных способа посмотреть на зависимости:
jdeps -summary -recursive target/classes # зависимости между пакетами и JAR
mvn dependency:tree # дерево артефактов Maven
gradle dependencies # то же для Gradle
jdeps входит в JDK и показывает зависимости на уровне пакетов и классов. С -dotoutput его вывод можно отдать в Graphviz и увидеть циклы между пакетами своими глазами. dependency:tree и gradle dependencies показывают дерево артефактов, то есть внешние библиотеки и модули сборки. Связей внутри кода в этом дереве нет.
Почему этого мало, хорошо видно на Keycloak. Это зрелый проект с аккуратной модульной структурой, и всё равно внутри набирается 135 групп циклических зависимостей на уровне файлов. Среди них, например, «Services and Server Spi Private», «Storage Ldap», «Util Oauth». Для Java с её SPI, плагинами и рефлексией это ожидаемо: часть связей появляется только на уровне файлов и не видна в дереве артефактов.
Циклические зависимости: как найти и где резать
Чаще всего за циклом стоит одна из четырёх причин:
- утилитный модуль, в который со временем стали складывать всё подряд;
- граница пакета проведена не там;
- два модуля отвечают за одно и то же понятие;
- короткий импорт «на время», который пережил три релиза.
Найти цикл помогают Madge, dependency-cruiser и вывод jdeps. Дальше нужно понять, какой файл в нём самый запутанный и где разрыв обойдётся дешевле всего.
Пример есть в deepseek-harness. На странице цикла «Mcp Mcp Client» три файла импортируют друг друга по кругу:
packages/mcp/mcp-client/src/connection.ts → transport.ts → index.ts → connection.ts
Страница ранжирует файлы по числу рёбер внутри цикла. Наверху connection.ts: два импорта внутрь цикла и один входящий. Его разумно трогать первым: вынести интерфейс или перенести импорт. Всего в deepseek-harness таких групп 47, и у каждой есть такая страница.
Для сравнения по масштабу:
| Репозиторий | Файлов | Строк кода | Групп циклов |
|---|---|---|---|
| grafana/grafana | 18 146 | 3 379 253 | 66 |
| keycloak/keycloak | 10 414 | 1 071 725 | 135 |
| deepseek-ai/deepseek-harness | 12 578 | 898 685 | 47 |
| go-gitea/gitea | 3 158 | 512 376 | 7 |
Scroll the table sideways to see every column.
Качество кода эта таблица не показывает. Количество циклов зависит от языка, возраста проекта и того, как устроена сборка. Gitea на Go и Keycloak на Java сравнивать по этой цифре напрямую нельзя: компилятор Go запрещает циклы пакетов, а Java нет.
Как я бы искал и разбирал цикл
- Построить граф пакетов стандартным инструментом (
go list,jdeps, Nx). - Найти самый маленький цикл: его проще понять и дешевле разорвать.
- Посмотреть, кто владеет файлами в цикле и как часто они меняются.
- Если файл в цикле ещё и горячая точка (его часто правят и часто чинят), начать с него.
- Рвать там, где меньше всего рёбер наружу, и закрепить результат правилом в CI.
Где здесь repowise
Пункты 3 и 4 обычно делаются руками: git log, git blame, вопросы в чате команды. repowise мы делали именно для этого: он строит граф по AST для 16 языков, от пакетов до символов, и кладёт рядом историю git (владельцев, частоту правок, горячие точки), а для каждого цикла генерирует страницу разбора. Открытый код, AGPL-3.0, запускается локально:
pip install repowise && repowise init
Он не заменяет Nx или Turborepo в CI. Хорошая связка выглядит так: Nx или Turborepo гоняют задачи, dependency-cruiser охраняет границы, а repowise показывает, что происходит внутри и где риск.
Как выбрать
- Если нужно собирать и тестировать только затронутое, подойдут Nx или Turborepo.
- Если подозреваете циклический импорт в JS/TS, запустите Madge.
- Если хотите запретить нарушения архитектуры в CI, настройте dependency-cruiser.
- Если у вас Go или Java и нужен граф пакетов, хватит стандартного тулчейна.
- Если нужно понять, где риск и с какого файла начинать, используйте repowise.
Как мы считали
Количество файлов и строк взято из последнего индекса каждого репозитория в repowise: Grafana и Keycloak от 11 августа 2026, Gitea от 17 июля, deepseek-harness от 28 сентября. «Группой циклов» мы называем компоненту сильной связности в графе импортов между файлами. Для каждой такой компоненты repowise генерирует отдельную страницу, их мы и посчитали. Цифры проверены 6 октября 2026 года. Если репозиторий с тех пор переиндексирован, на странице могут быть другие значения.
Посмотреть свой репозиторий: вставьте ссылку на GitHub на repowise.dev или откройте repowise.dev/repo/<владелец>/<репозиторий>.
Частые вопросы
Чем построить граф зависимостей для монорепозитория?
Выбор зависит от задачи: для сборки и CI берите Nx или Turborepo, а для исследования связей между файлами в JS/TS подойдут Madge или dependency-cruiser. В Go и Java есть `go list -deps` и `jdeps`. Граф до уровня функций вместе с историей git строит repowise.
Как найти циклические зависимости в проекте?
В JS/TS запустите `npx madge --circular src/`. В Java постройте граф пакетов через `jdeps -dotoutput` по скомпилированным классам и посмотрите его в Graphviz. В Go циклы пакетов запрещает компилятор, но циклы между файлами внутри пакетов остаются. repowise находит циклы на уровне файлов в любом из 16 языков и для каждого показывает, какой файл в нём самый связанный.
Чем граф пакетов отличается от графа символов?
Граф пакетов показывает, какие пакеты или проекты зависят друг от друга, и нужен для порядка сборки. Граф символов показывает связи между файлами, классами и функциями и нужен для рефакторинга: по нему видно, где на самом деле проходит связь.
Можно ли построить граф зависимостей для Go без сторонних инструментов?
Да: `go list -deps ./...` для пакетов и `go mod graph` для модулей. Чтобы получить картинку, результат можно передать в Graphviz.
Можно ли построить граф зависимостей для приватного репозитория?
Да. Локальный repowise работает на вашей машине и никуда не отправляет код. Облачная версия индексирует приватные репозитории на платном тарифе, есть бесплатный пробный период на 10 дней.