Skip to main content

Dead code, duplicate code, dependency cycles, and architecture violations. Found before you finish reading this.

Rev-dep consolidates fragmented, sequential checks
from multiple slow tools into a single high-performance engine.
Evaluate your entire JS/TS monorepo without breaking a sweat.

rev-dep config run
$ rev-dep config run ๐Ÿ“ Workspace: . (root) (7015 files)โœ… Orphan Filesโœ… Duplicated Codeโœ… Module Boundariesโœ… Unused Exports ๐Ÿ“ Workspace: apps/web (6096 files)โœ… Circular Dependenciesโœ… Orphan Filesโœ… Unused Node Modulesโœ… Missing Node Modulesโœ… Import Conventionsโœ… Unresolved Importsโœ… Dev Deps Usage On Prodโœ… Restricted Importsโœ… Restricted Importersโœ… Restricted Direct Importers ๐Ÿ“ Workspace: apps/mobile (742 files)โœ… Circular Dependenciesโœ… Orphan Filesโœ… Unused Node Modulesโœ… Missing Node Modulesโœ… Unresolved Importsโœ… Dev Deps Usage On Prod ๐Ÿ“ Workspace: packages/shared (88 files)โœ… Circular Dependencies โœ… All checks passed! โœจ  Done in 175ms.

Performance

Other tools take seconds. Rev-dep takes milliseconds.

Circular import detection is the one check every tool below implements, so it is the only place all seven can be lined up at once. The bars run at real speed: each one takes exactly as long to fill as that tool takes. rev-dep is done in 154 milliseconds. madge keeps going for another thirteen seconds.

Benchmark setup

Project
580,000 lines of code ยท 6,024 source files ยท Next.js app
Hardware
Intel i9-14900KF ยท WSL Debian
Method
hyperfine -w 4 -r 8 (4 warm-up + 8 measured runs)
rev-deprev-dep circular
154 ms
knipknip --cycles
3 040 ms
circular-dependency-scannerds .
3 355 ms
dpdm-fastdpdm --no-tree (fast fork)
6 070 ms
dpdmdpdm --no-tree
6 667 ms
dependency-cruiserdepcruise --output-type err
8 258 ms
madgemadge --circular
13 569 ms

Also measured: skott takes 61.6 s on the same project - 400x rev-dep, and too far off the scale to plot.

The same gap on every other check

Eight more tasks, each against the fastest tool that does the same job. Same project, same machine, same method.

Find unused exports17ร— faster
rev-dep
186 msknip
3 176 ms
Find unused files18ร— faster
rev-dep
168 msknip
3 006 ms
Find unused dependencies18ร— faster
rev-dep
170 msknip
3 069 ms
Find missing dependencies19ร— faster
rev-dep
160 msknip
3 076 ms
Enforce module boundaries50ร— faster
rev-dep
164 msdependency-cruiser
8 140 ms
Find restricted imports65ร— faster
rev-dep
170 msdependency-cruiser
10 995 ms
List files reachable from an entry point81ร— faster
rev-dep
81 msmadge
6 591 ms
Discover entry points92ร— faster
rev-dep
149 msmadge
13 632 ms

Every number above is a single check, measured on its own. In practice the gap gets wider: running all twelve checks costs rev-dep almost nothing extra, because the graph is built once and shared between them. Three separate tools build it three times.

What it catches

Multiple checks, unlimited workspaces, one extremely fast pass

Grouped by the problem they solve. Turn on what you need - each check is opt-in per workspace.

Dead weight

Code and dependencies that earn nothing - unreachable, unused, or written twice.

Orphan files

--fix

Files no entry point can reach. Removing one often orphans the next, so --fix peels them layer by layer.

Unused exports

--fix

Exported symbols nothing imports - the public surface a module never actually needed.

Duplicated code

Start here

Copy-pasted code blocks and JSX elements. Each finding is a unit you can extract, not a line range you have to untangle first.

Unused dependencies

Packages declared in package.json that no code imports. Smaller installs, smaller attack surface.

Structural bugs

Graph-level defects that a per-file linter or type-checker cannot see.

Circular imports

Import cycles that cause initialisation-order bugs and make modules impossible to test in isolation.

Unresolved imports

Import requests that resolve to nothing - broken paths, stale aliases, renamed files.

Missing dependencies

Packages the code imports but never declares. They work locally and break in production.

Dev deps in production

devDependencies reachable from a production entry point - shipped by accident.

Architecture rules

The layering you agreed on, written down and enforced. Most tools in this category have nothing here.

Module boundaries

Declare which parts of the codebase may import which. UI cannot reach the database layer; features stay independent.

Restricted imports

Forbid specific files or packages from being reachable at all from a given entry point.

Restricted importers

The allow-list form: only these entry points may transitively reach this code.

Import conventions

--fix

Enforce relative-vs-alias import style per domain, and rewrite the offenders automatically.

AI-generated code

Agents write fast. Something has to check the structure.

Coding agents can produce code at remarkable speed. They can also leave behind abandoned files, hidden import cycles, and architectural boundary violations-because they don't understand the reasoning behind your system design. And because each task is answered in isolation, they write the same helper a third time instead of finding the two that already exist. Left unchecked, task after task, they gradually turn a well-structured codebase into a dumping ground.

Deterministic, not another model

No LLM inside. The same code gives the same verdict every time, so it can gate a loop that is already probabilistic.

Exact edit locations

Every issue carries file, line and column. The agent jumps straight to the fix instead of re-reading the repo to find it.

Fast enough to run every time

A sub-second check can run after each edit. A ten-second one gets run at the end, if at all.

It can clean up after itself

rev-dep config run --fix --recheck removes dead files and unused exports, then verifies the result in one command.

machine-readable output
$ rev-dep config run --format json {  "version": "2.0",  "hasFailures": true,  "workspaces": [    {      "path": "apps/web",      "checks": {        "moduleBoundaries": {          "status": "fail",          "issues": [            {              "ruleName": "ui-cannot-import-api",              "filePath": "src/ui/Chart.tsx",              "importPath": "src/api/client.ts",              "startLine": 3,              "startCol": 1            }          ]        },        "duplicatedCode": [{          "status": "fail",          "blinding": "exact",          "snippets": 3,          "occurrences": 9,          "files": 6,          "configIndex": 0        }]      }    }  ]}

Add rev-dep config run --format json to your AGENTS.md. The agent runs it after editing, reads the exact locations, and fixes its own mistakes before you see the pull request.

Resolution

A setting for the way your project actually resolves

A dependency checker is only as accurate as its resolution, and no tool can infer every setup. rev-dep does not try to: each way real projects resolve imports is a setting you point at yours. Expect to spend time on the first config - that is the part that makes every finding afterwards trustworthy.

Source packages and compiled packages

Most monorepos mix both. You declare which packages rev-dep follows into their source and which it treats as an external boundary - the same distinction your bundler makes, set per package rather than guessed.

pnpm strictness, or npm and yarn hoisting

Choose whether dependencies are validated against the consuming package or against the package that owns each file. The second matches how pnpm actually resolves; the first suits a hoisted npm or yarn tree. Picking the wrong one is the usual source of false missing-dependency reports.

Path aliases and exports maps

tsconfig paths and baseUrl, package.json imports and exports maps, and conditional targets are read from your project rather than reimplemented. Which condition names apply is yours to set - a browser build and a test run do not resolve the same way.

Globs that behave like .gitignore

Every path pattern - entry points, ignores, boundaries - uses gitignore matching, negation with ! included. One syntax across the whole config, and it is one you already know.

Different rules per package

Checks are configured per workspace, so a package that is not ready keeps them off while the rest of the monorepo is enforced. Tuning does not have to happen everywhere at once - get one workspace right, then move to the next.

More than .ts and .js

TypeScript and JavaScript in every extension, plus the script blocks of .vue and .svelte components. Asset imports resolve once their extensions are declared - the common ones ship as defaults, anything else your project imports is one line of config.

Getting started

You will get a lot of findings on day one. That is the point.

Every tool like this shows a backlog on the first run - it is the work that piled up while nothing was checking. What matters is that it is a one-time cleanup, and that you can take it in pieces instead of all at once. After that, a normal pull request surfaces a handful of issues at most.

rev-dep config init
$ rev-dep config init Which detectors should the config enable? 1) No detectors2) Unresolved imports only (a good start to confirm   your setup and that resolution works)*  3) Unresolved + circular imports4) Circular + unresolved enabled, other detectors   listed but disabled5) All detectors enabled Type 1-5 [default: 3]: โœ… Created .rev-dep.config.jsonc ๐Ÿ“ฆ Monorepo detected: discovered 3 workspace packages   and created a rule for each:- apps/web- apps/mobile- packages/shared Adjust rules to make them relevant to your project setup.

Start with two checks

The default is unresolved + circular imports. Both are near-zero noise and they prove your path aliases and monorepo resolution are set up correctly.

Add one detector at a time

Each check is a separate key in the config. Turn on unused exports, fix that backlog, commit, then move to the next one.

Let --fix do the boring part

Orphan files, unused exports and import conventions repair themselves. Deleting one dead file often reveals another, so run it until it comes back clean.

Scope rules per workspace

A monorepo package that is not ready keeps its checks off. The rest of the repo starts failing on regressions straight away, instead of waiting for the worst package to be cleaned up first.

Exploratory toolkit

Ask questions about your dependency graph

Checks tell you something is wrong. These commands tell you why. Useful when a check fails, and before any refactor you are nervous about.
Teach your agent to reach for them and it can do the research itself - reading the real graph instead of guessing at it from the files it happens to have open.

rev-dep resolve --file src/utils/legacyFormat.ts
$ rev-dep resolve --file src/utils/legacyFormat.ts Dependency paths from entry points to 'src/utils/legacyFormat.ts': /src/index.ts (1): Path 1: โžž /src/index.ts  โžž /src/pages/Dashboard.tsx   โžž /src/widgets/RevenueCard.tsx    โžž /src/utils/legacyFormat.ts Total: 1

Prints the actual import chains that lead to a file or package. This is the command for "I deleted the import, so why is this still in the build?" - it shows every route from every entry point.

rev-dep resolve reference

Feedback

What early users say

Unprompted comments from public GitHub threads, posted while teams were evaluating rev-dep on their own codebases. Every one links back to the issue it came from.

โ€œThe potential speed up gained from rev-dep is super attractive so it would be amazing if we could use it.โ€

โ€œI think this project is quite great, and will be able to replace dependency-cruiser for my needs.โ€

In production

From 400-file packages to 197-workspace monorepos

Anonymous usage data from projects running rev-dep. Each card is one real codebase, except the last, which is the median. The interesting part is the range.

Large monorepo ยท Italy

197

workspaces in one repo

  • 3,473 source files
  • 5 checks configured
  • Mostly run locally

Large single codebase ยท Japan

17,604

source files, one workspace

  • No monorepo required
  • The largest tracked codebase

Architecture-first ยท Germany

104

module boundary rules

  • 9,288 source files
  • One workspace, heavily governed

Whole engineering team ยท United States

25

developers on one codebase

  • 8,339 source files
  • 174 workspaces
  • Run locally, not only in CI

Typical single package

374

source files

  • The median tracked project
  • 2 workspaces
  • Same binary, same config

Largest recorded so far

17,604files in the largest codebase
197workspaces in the largest monorepo
104boundary rules on one project
5,467runs on one project in 90 days

Questions

The things people ask before installing

The objections that actually come up - answered with the mechanism, not a slogan.

Ask something else on GitHub โ†’
Why is it so much faster? What is the catch?

It is a native binary, so there is no Node startup and no JIT warm-up. It uses a purpose-built parser that extracts only imports and exports - it never builds a full AST, never walks function bodies and never runs a type-checker, so most of every file is skipped because most of it is irrelevant to a dependency graph.

Everything runs in parallel: file discovery, parsing, module resolution and check evaluation. And the graph is built once and shared by every enabled check, so several checks within multiple workspaces cost roughly what one costs.

What are the performance limitations?

In practice there is no size at which it stops being fast. A large monorepo evaluates in subsecond time on modest hardware.

The one thing that might slow it down is being pointed at files that were never meant to be read: committed build output. A single minified bundle can be longer than the rest of the source put together, and the parser has no way to know it is looking at generated code.

Gitignored files are skipped automatically, so this only bites when dist or build folders are committed to the repository. Add them to ignoreFiles and the analysis goes back to full speed.

How ignoring files works โ†’
What about false positives?

Once the config is right, there are none. An import either resolves or it does not - there is no heuristic doing guesswork behind the scenes.

What gets experienced as a false positive is almost always the config describing the wrong graph: an entry point that was never declared, so live code looks orphaned; an asset extension the resolver does not recognise; a file reached only through tooling the config was not told about. If a finding looks wrong, the graph is telling you something true about a configuration that is incomplete.

Will it understand my setup?

Almost certainly. Resolution follows the ESM module resolution algorithm and industry standard aliasing mechanisms: tsconfig path aliases, package.json exports and imports maps including condition names, and cross-package resolution across pnpm, yarn and npm workspaces - all without extra configuration.

It parses every JavaScript and TypeScript extension in common use - .ts, .tsx, .mts, .d.ts, .js, .jsx, .cjs, .mjs - plus the script blocks of .vue and .svelte components. Imports of images, fonts, styles, JSON and YAML resolve out of the box, and any other extension your project uses takes one line of config.

If something behaves differently than your project expects, or you need something that is not listed here, open an issue. That is the fastest way to get it covered, and it is the kind of report that makes the resolver better for everyone.

Report it on GitHub โ†’
Do I have to rip out ESLint and knip?

No, but there is something worth deleting. If you use eslint-plugin-import, drop the import/no-cycle and import/no-unused-modules rules: they are typically the slowest rules in a config, because ESLint re-resolves the import graph for every file. Removing them makes ESLint faster and improves detection, since a per-file linter structurally cannot see that a file is unreachable or that a cycle spans eight files across three packages. Keep ESLint for what only it can do - inline feedback as you type.

knip overlaps with rev-dep on unused files, exports and dependencies, so running both means building the graph twice for the same findings. Where knip is genuinely different is finer granularity: unused class and enum members, namespace-level exports, duplicate exports. If you need that, run rev-dep on every commit and knip occasionally.

Will it slow down my CI?

It should do the opposite, and not only because each check is faster. A typical stack runs several tools in sequence, each paying its own Node startup, its own file discovery and its own graph build - the same expensive work repeated. rev-dep builds the graph once and runs every enabled check across it in parallel.

It also installs no dependency tree of its own: the npm package is a launcher plus one prebuilt binary, so the install step shrinks too.

What does it not do?

It does not draw dependency graphs - it reports, it does not visualise. It analyses at file, export and dependency granularity, so it will not find an unused class member or enum member. And it has no plugin ecosystem - instead it supports different resolution strategies so it's a matter of proper configuration to make it work in your project. Each project is different and plugins usually only works partially. If something is genuinely missing, please open a github issue.

Get started

Point it at your repo and see what it finds

Install rev-dep, generate initial config
and follow integration guides to set it up.

MIT licensed ยท no account ยท no 3rd party dependencies