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-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 ๐ 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
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
Also measured: skott takes 61.6 s on the same project - 400x rev-dep, and too far off the scale to plot.
Eight more tasks, each against the fastest tool that does the same job. Same project, same machine, same method.
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
Grouped by the problem they solve. Turn on what you need - each check is opt-in per workspace.
Code and dependencies that earn nothing - unreachable, unused, or written twice.
Files no entry point can reach. Removing one often orphans the next, so --fix peels them layer by layer.
Exported symbols nothing imports - the public surface a module never actually needed.
Copy-pasted code blocks and JSX elements. Each finding is a unit you can extract, not a line range you have to untangle first.
Packages declared in package.json that no code imports. Smaller installs, smaller attack surface.
Graph-level defects that a per-file linter or type-checker cannot see.
Import cycles that cause initialisation-order bugs and make modules impossible to test in isolation.
Import requests that resolve to nothing - broken paths, stale aliases, renamed files.
Packages the code imports but never declares. They work locally and break in production.
devDependencies reachable from a production entry point - shipped by accident.
The layering you agreed on, written down and enforced. Most tools in this category have nothing here.
Declare which parts of the codebase may import which. UI cannot reach the database layer; features stay independent.
Forbid specific files or packages from being reachable at all from a given entry point.
The allow-list form: only these entry points may transitively reach this code.
Enforce relative-vs-alias import style per domain, and rewrite the offenders automatically.
AI-generated code
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.
No LLM inside. The same code gives the same verdict every time, so it can gate a loop that is already probabilistic.
Every issue carries file, line and column. The agent jumps straight to the fix instead of re-reading the repo to find it.
A sub-second check can run after each edit. A ten-second one gets run at the end, if at all.
rev-dep config run --fix --recheck removes dead files and unused exports, then verifies the result in one command.
$ 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.
One tool instead of several
Rev-dep covers every one of these. Each one starts its own Node process, discovers your files again and builds its own graph - run four and you pay for that four times. Follow any row for an honest side-by-side.
Resolution
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.
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.
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.
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.
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.
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.
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
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 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.
The default is unresolved + circular imports. Both are near-zero noise and they prove your path aliases and monorepo resolution are set up correctly.
Each check is a separate key in the config. Turn on unused exports, fix that backlog, commit, then move to the next one.
Orphan files, unused exports and import conventions repair themselves. Deleting one dead file often reveals another, so run it until it comes back clean.
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
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 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 referenceFeedback
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.
โThank a lot for your efforts in this project. I think its an amazing job!!โ
โ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
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
Large single codebase ยท Japan
17,604
source files, one workspace
Architecture-first ยท Germany
104
module boundary rules
Whole engineering team ยท United States
25
developers on one codebase
Typical single package
374
source files
Largest recorded so far
Questions
The objections that actually come up - answered with the mechanism, not a slogan.
Ask something else on GitHub โ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.
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 โ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.
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 โ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.
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.
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
Install rev-dep, generate initial config
and follow integration guides to set it up.