Monorepo Tools with Dependency Mapping: Migrating 50+ Projects (Checklist)

Swati Ahuja9 min read

monorepo tools with dependency mapping · top monorepo platforms with dependency graph · migrate to monorepo · monorepo migration checklist · nx vs turborepo dependency graph · map dependencies across projects

On this page

If your team manages 50+ projects and wants one repo, choose the monorepo tool by how it finds dependencies. Nx infers them from source imports and package.json and draws them with nx graph. Turborepo reads what each package.json declares. Bazel and Pants model targets across languages. Then map the dependencies before you move anything, while the projects are still separate.

ToolWhere the graph comes fromFinest unitHow you see itOutput formatsBest fit
NxSource imports, installed packages and tsconfig, on top of project.json or package.json workspacesProject and tasknx graphBrowser UI, --file=graph.json, --file=graph.htmlJS/TS first, plugins for more
TurborepoDependencies declared in each package.jsonPackage and taskturbo run build --graph=graph.htmlsvg, html, mermaid, dotJS/TS teams that want a thin task runner
BazelExplicit targets in BUILD filesTarget (often a file)bazel query 'deps(//app)' --output graphDOTPolyglot, hermetic, very large
PantsInferred from import statementsFilepants dependencies, pants dependents, pants pathsText, JSON from pants peekPython, JVM, Go, shell
moondependsOn in moon config plus manifests per languageProject and taskmoon project-graphBrowser UI, --dot, --jsonMixed-language task runner
RushProjects registered in rush.json, local deps from package.jsonPackagerush list --to <pkg> --json, Lockfile ExplorerJSON, local web appLarge JS/TS orgs on pnpm
repowiseParsed imports and calls from source, plus git co-change; workspace mode adds manifests, HTTP, gRPC and topic contracts across reposFile and symbol; service across reposHosted repo pages, repowise serve, MCPInteractive graph, JSONMapping before and after the move. Not a build tool: no task running or caching

Scroll the table sideways to see every column.

We have all seen the plan for this: one repo, one lockfile, apps/ and packages/, one CI pipeline, yada yada. Drawing the folders is the easy part, and the hard part is knowing which of the 50 projects depend on each other without saying so, and the tools above answer that differently.

Which monorepo tools map dependencies, and how

Every tool in that table draws a graph. They disagree on where the edges come from, and that decides what you can trust during a migration.

Turborepo and Rush build a declared graph, which means they read the manifest. If apps/web/package.json lists "@acme/ui": "workspace:*", there is an edge. If apps/web imports @acme/ui without declaring it, there is no edge. That is fine once the repo is tidy, and misleading while you are still moving code around, which is exactly when you are looking at it.

Nx and Pants build an inferred graph by reading the code. Nx's docs say it analyzes "source code, your installed dependencies, and TypeScript configuration", and Pants analyzes import statements. An undeclared import still shows up, which is what you want during a migration, because undeclared imports are exactly what breaks.

Bazel builds an explicit graph from BUILD files you write or generate. A missing dependency is a build error, so the graph is precise, and getting 50 projects into BUILD files is a project of its own. Gazelle generates a first pass for Bazel, and pants tailor does the same for Pants.

moon sits between the first two: you can declare dependsOn, and it also discovers dependencies from each language's manifests.

None of these build tools look at git history, so none of them can say which files change together across projects when nothing imports anything. That is the gap repowise fills in the table. It doesn't replace Nx or Turborepo, and it can't run a build.

Why map dependencies before you move

While the projects live in separate repos, their dependencies hide in places that look harmless until moving day.

  1. Published versions hide the first kind. Project A depends on @acme/auth@2.3.1 from your registry, and after the move it depends on whatever packages/auth is at HEAD. Every consumer that was pinned to an old version is now on the new one, on day one.
  2. Network calls hide the second kind. Service B calls POST /v1/orders on service C, and no import connects them, so no import graph shows it, before or after.
  3. Shared tables and topics hide the third kind. Two services that write the same table, or publish to the same queue, are coupled without a single line of shared code.

A monorepo tool's graph only covers the first kind, and only after the code is in one place. So the mapping has to happen first, with tools that can read separate repos.

How to map 50 separate repos before they are one repo

You can script it, declare it, or extract it, and each approach catches a different share of the edges.

Scripting the manifests is the afternoon version. Clone every repo, read each package.json, pyproject.toml, go.mod or pom.xml, and list which internal package names each one depends on. It catches the published-version edges and misses HTTP calls, tables and topics.

Declaring it in a catalog is the version that relies on people. Backstage lets each component list dependsOn, providesApis and consumesApis in its catalog file. That is useful as a record of intent, and it is only as accurate as the last person who edited the YAML.

Extracting it from code and history is the version that doesn't depend on anyone remembering anything, and it is what repowise's workspace mode is for. Put the repos under one folder and run:

bash
pip install repowise
cd all-our-repos
repowise init .

It scans for git repositories up to 3 levels deep, indexes each one, then links them: package dependencies from manifests, HTTP routes matched to the clients that call them, gRPC services, queue topics, database tables, and files in different repos that keep changing together. repowise workspace diagnostics shows the calls it could not match and why, so you can read a low link count as missing evidence instead of as "no coupling". The cross-repo docs list every framework it reads.

The limits matter for a move this size. The CLI is open source (AGPL-3.0) and runs locally. The hosted Pro plan holds 5 repos and Teams 25 pooled, so 50 repos is a self-hosted or Enterprise job today. A topic or URL path built at runtime is skipped, while a call with an unresolved base like ${API_BASE}/users still matches on its path. Cross-repo and cross-language dependency graphs goes through what each kind of link really resolves.

A mapped monorepo: react/react

React is already a monorepo, which makes it a useful preview of what you inherit after a move. On its repowise architecture page, the index covers 2,764 source files and 15,233 symbols, in JavaScript, TypeScript and Rust.

The Packages view shows 360 third-party packages declared 750 times across 70 manifests. That is normal for a long-lived monorepo, and it is exactly the work that lands on you when 50 repos with their own pins move in at once. Of the 360 packages, 195 have no import edge in the graph. Some are CLI tools run from scripts, so treat that number as a place to start looking for unused dependencies and check each one by hand.

The migration checklist

I'd work through it in order, because each step is cheap compared with finding the same thing in CI after the move.

  1. Inventory the projects: name, language, build command, test command, owner and deploy target. If a project has no owner, decide that now.
  2. Map package dependencies: which internal packages each project consumes, and at which version. Flag every consumer more than one major version behind.
  3. Map runtime dependencies: HTTP routes, gRPC services, topics and shared tables. These never show in an import graph.
  4. Map co-change by finding which files in different repos change in the same week. Those pairs belong close together in the new layout, or behind a clear interface.
  5. Find cycles. A cycle between two published packages is survivable with versions, and at HEAD it is a build-order problem. Find circular dependencies in a monorepo has a recipe per language.
  6. Pick the layout. apps/ and packages/ is a common start. Put things that change together near each other.
  7. Decide one version per third-party package, or write down which exceptions you accept. Turborepo, Nx and Rush all work better with one lockfile.
  8. Pick the build tool from the table above, by where its graph comes from and which languages you have.
  9. Move with history. nx import ../inventory-app apps/inventory keeps git history. Outside Nx, git filter-repo --to-subdirectory-filter <dir> rewrites a repo into a subdirectory before you merge it. History is what blame, ownership and hotspot analysis run on, so don't squash it away.
  10. Pilot with one app and two or three shared libraries, and run old and new pipelines side by side until test results match.
  11. Add boundary rules once things build: Nx module boundaries, dependency-cruiser rules, or Bazel visibility.
  12. Re-check the graph after each wave and compare it with the map from steps 2 to 4. New edges you didn't expect are the bugs you haven't hit yet.

Dependencies the graph will not show you

Some dependencies never become an edge: environment variables that point one service at another, feature flags, dynamic imports built from strings, build scripts that copy files between folders, and cron jobs on a box somewhere. Static analysis reads code, so ask the people who run production for that list before you trust any graph as complete.

For a wider comparison of graph tools once you are in, see best dependency graph tools for monorepos. For keeping docs, graphs and ownership readable after the move, see how to index a monorepo. For JS and TS specifically, 5 ways to generate a JavaScript/TypeScript dependency graph has runnable commands.

The graph is the part a tool can draw for you, and the cron job on a box somewhere is still the part you have to go and ask about.

How we measured

On 2026-10-06 we checked every command and output format above against each tool's own docs (Nx, Turborepo, Bazel, Pants, moon, Rush, git-filter-repo). We did not benchmark build speed. The react/react numbers come from repowise's hosted index of that repo (snapshot completed 2026-10-02), read from the live architecture page and the database the same day. They describe one commit, and they count what manifests declare, which can differ from the packages installed at runtime.

FAQ

What are the top monorepo platforms with a dependency graph?

Nx, Turborepo, Bazel, Pants, moon and Rush all build one. Nx and moon have the most usable built-in visual graphs. Turborepo exports svg, html, mermaid or dot from `--graph`. Bazel and Pants give you precise graphs that you query from the command line, and neither puts a polished UI on top.

Which monorepo tools map dependencies automatically?

Nx and Pants infer dependencies from source code, and moon discovers them from language manifests. Turborepo and Rush rely on what package.json declares. Bazel needs explicit BUILD files, though Gazelle can generate them for some languages.

Is Nx or Turborepo better for 50+ projects?

It depends on how much structure you want. Nx infers the graph from imports, enforces module boundaries and runs affected-only tasks, at the cost of more concepts to learn. Turborepo stays close to your package manager and is quicker to adopt, but its graph is only as good as your package.json files.

Can I keep git history when merging repos into a monorepo?

Yes. `nx import` keeps history when it brings a repo in. Without Nx, rewrite each repo into a subdirectory with `git filter-repo --to-subdirectory-filter`, then merge it with `--allow-unrelated-histories`.

Our monorepo has 200 interconnected packages and builds keep getting slower. Where do I start?

Look at the graph before you tune the cache. Find the few packages that almost everything depends on, because every change there rebuilds the world. Splitting one of those, or breaking a cycle through it, usually does more than tuning remote caching.

Do I need Bazel for a polyglot monorepo?

You don't always need it. Pants covers Python, JVM, Go and shell with dependency inference, and moon and Nx run tasks across languages. Bazel earns its setup cost when you need hermetic, reproducible builds and remote execution at large scale.