Skip to main content
Version: 3.x

Monorepo integration guide

This guide takes you from zero to a working rev-dep setup in a monorepo, one step at a time. The order matters: each step builds the foundation the next one relies on.

This guide assumes rev-dep is already installed. If not, see Installation and the intro first. Working in a single workspace project? Use the single workspace integration guide instead.

The mental model is simple: rev-dep builds one dependency graph from your source files, and each check is a query against that graph. Most of the work is making sure the graph is accurate - resolution (step 3) and entry points (step 6); after that, enabling checks is quick.

The steps come in four groups:

  • 1-2 - generate the config and learn how to run it. config init may already have done part of steps 3-6 for you; each step says so.
  • 3-5 - checks that need nothing but correct resolution.
  • 6-9 - entry points, then everything that depends on them.
  • 10-13 - architecture rules you author yourself, then CI.

1. Generate the base config

rev-dep config init

Run at the monorepo root, this creates a root workspace plus one workspace per discovered workspace package. It prints the packages it found, so you can confirm none is missing.

It writes .rev-dep.config.jsonc (JSONC - comments allowed) with a $schema for editor autocomplete. See config file structure.

What init asks you - and which steps it already covers

config init is interactive, and your answers pre-fill part of this guide. You can confirm every question default option with Enter. In a non-interactive shell (CI, scripts) nothing is asked: the defaults apply and entry-point detection is skipped.

QuestionDefaultWhat your answer changes
Standalone packages were found in subfolders. What should the config cover? (only asked when packages outside the workspace globs exist)Monorepo only - root + workspace packagesAdds a workspace per non-workspace subfolder package too. The curated option skips fixture/test/example-looking folders
Auto-detect entry points for each package?Yes - analyze and fill them inFills prodEntryPoints / devEntryPoints / ignoreEntryPoints per package workspace, so step 6 becomes a review instead of authoring. The root workspace is left without entry points on purpose
Fold near-covered directories into one dir/** glob? (only asked when some directory almost qualifies)Strict (100%)Trades entry-point precision for a shorter config - the caveat is in step 6
Which detectors should the config enable?Unresolved + circular imports + duplicated codePre-enables steps 3, 4 and 5 on every package workspace. The fourth option additionally writes the remaining detectors as "detector": false placeholders; "All detectors" turns everything on at once

So when a step below says enable X, that means flipping an existing "X": false to true if init wrote the placeholder, or adding the key if it did not. The root workspace always gets "orphanFilesDetection": false and "unusedExportsDetection": false placeholders - steps 8 and 9 are where you turn them on.

config init never overwrites an existing config - it stops with an error if one is present. Delete the file if you want to start over.

The examples below use the compact detector syntax: "detector": true switches a check on, and when a detector carries options the enabled flag is optional. Write { "enabled": true } if you prefer the explicit form.

Workspaces: monorepo root vs per-package

A workspace is a scope - usually one package. Two levels are available, and you will use both:

  • The root workspace (path: ".") sees the whole monorepo. Use it for checks that need cross-package context - most importantly orphan files and unused exports of shared packages.
  • A per-package workspace (eg. path: "apps/web") is scoped to one package (plus the packages it follows). Use it for package-local checks and for compiled apps, where not imported code is genuinely dead.

Every check below can be placed at either level. The guidance per check notes where it belongs.

Following monorepo packages

When a workspace encounters an import of another workspace package, followMonorepoPackages decides whether rev-dep traces into that package's source (follow) or treats it as an external boundary (don't). Workspaces follow all packages by default.

This matters for almost every check in a monorepo: follow the packages whose source is compiled by the consumer (just-in-time/shared packages), and leave pre-compiled packages unfollowed. See Following monorepo packages.

Node modules resolution

Once you follow a package, its source brings in its own third-party imports. The top-level nodeModulesResolution option decides which package.json those imports are validated against for the missingNodeModules, unusedNodeModules, and unresolvedImports checks. Set it once; it applies to every workspace.

Configure it as an object and set resolutionType to one of:

  • "entry-package" (default) validates every import against the consuming workspace's package.json. Use it when the consumer can resolve every dependency it pulls in (npm, classic yarn).
  • "nearest-package" validates each import against the package.json that owns the file it lives in. Use it when every package resolves only the dependencies it declares itself (pnpm's default layout).
{
"nodeModulesResolution": { "resolutionType": "nearest-package", "includeDevDepsFromRoot": false },
"workspaces": [ /* ... */ ]
}

This is the form rev-dep config init generates - always with "entry-package", since init cannot tell which package manager layout you use. This is the one top-level setting it cannot decide for you.

If you use pnpm with its default layout, set resolutionType: "nearest-package". Otherwise a followed package's own declared dependencies get reported as missing against the consumer that doesn't declare them - a flood of false positives. In nearest-package mode each package is judged by its own package.json: a followed package that imports something it forgot to declare is correctly flagged (just as a strict pnpm install would break), while its properly-declared dependencies are left alone. Full details and per-check effects: Node modules resolution.

The object also accepts includeDevDepsFromRoot (default false). If your monorepo declares shared dev dependencies (linters, test runners, build tools, type packages) once at the repo root instead of in each package, set it to true so importing them from package code isn't reported as missing or unresolved. It is strictly opt-in because root-shared devDeps make dependency ownership less explicit - enable it only if your project deliberately follows that approach. See Including root devDependencies.

A bare string (e.g. "nodeModulesResolution": "nearest-package") is also accepted as a shorthand for resolutionType, kept for backward compatibility - prefer the object form shown above.

2. Run and (auto)fix

rev-dep config run # report (5 first issues per check)
rev-dep config run --list-all-issues # full report
rev-dep config run --workspaces apps/web # only selected workspaces

Adopt checks incrementally - enable one, run it, resolve the findings, commit, repeat. That keeps CI noise manageable.

Autofix needs two things together: the check must have "autofix": true in the config, and you must pass --fix:

rev-dep config run --fix # apply fixes for autofix-enabled checks
rev-dep config run --fix --recheck # apply, then re-validate

Only unusedExportsDetection, orphanFilesDetection, and importConventions support autofix. Details: Running checks and autofix.

3. Verify resolution: unresolved imports

Enable this first. It tells you whether rev-dep can parse and resolve your project the way your tooling does - the precondition for every other check.

Every detector option except "No detectors" already enabled it at init - on each package workspace, where a failure names the package that owns it. So usually there is nothing to add here, just run it. If your config has no unresolvedImportsDetection, add it per package:

{
"workspaces": [
{ "path": "apps/web", "unresolvedImportsDetection": true },
{ "path": "packages/ui", "unresolvedImportsDetection": true }
]
}

If it reports nothing, your imports parse and resolve cleanly - move on. If it reports imports you expected to resolve, work through the unresolved imports troubleshooting guide before going further (common causes: unsupported aliases, condition names, asset extensions, gitignored files).

When investigating with the exploratory CLI in a monorepo, pass --follow-monorepo-packages - those commands do not follow by default, so cross-package imports look unresolved otherwise.

Learn more about the unresolvedImportsDetection check configuration.

4. Circular dependencies

The first check that needs nothing but correct resolution - no entry points, no tuning. config init enables it on every package workspace (unless you picked one of the first two detector options), so it works as generated. A high-signal, low-effort first architectural win. ignoreTypeImports lets you catch new runtime import cycles while tolerating existing type-only ones.

{
"circularImportsDetection": {
"ignoreTypeImports": true
}
}

Learn more about the circularImportsDetection check configuration.

5. Duplicated code

Also independent of entry points. config init enables it alongside circular imports (unless you picked one of the first two detector options) and, unlike every other check, puts it on the root workspace only: a block pasted from one package into another is one finding for the repository, not one per package, and the root workspace already scans every package below it.

It points the detection at a snapshot file it has not created, so the first run reports the duplication that exists and tells you the baseline is missing:

❌ Duplicated Code: 34 duplicated snippets in 21 files (79 occurrences), scanned 812 files
To see them, run:
rev-dep duplicated-code
No baseline recorded yet at duplicated-code-snapshot.json
To accept them as the baseline, run: rev-dep config run --update-snapshot

Two ways forward, and both are legitimate:

Look at them now. The printed command shows the locations and the source of every copy - it finds repeated code blocks and JSX elements that you can extract into shared implementations. Refactor them straight away or accept the current state and move on. This is the usual choice on an existing codebase. Either way, generate the snapshot file to establish the baseline for future runs:

rev-dep config run --update-snapshot

Commit the snapshot. From then on the check reports only the delta - new duplication fails, acknowledged duplication passes. Run the same command again after you resolve some of it, so the baseline follows the code.

If the findings are dominated by configuration objects rather than logic, the detection can be tuned - size and nesting floors, whether object literals count, how many copies it takes, and whether renamed copies should match. Learn more about the duplicatedCodeDetection check configuration and how duplicated code detection works.

6. Entry points

Entry points are the roots of reachability - the foundation for orphan files, unused exports, and dev-dependency checks. They are defined once per workspace, and everything from step 7 on depends on this step being honest.

  • prodEntryPoints - real application roots (what ships).
  • devEntryPoints - tests, scripts, stories, config; keeps dev-only files from looking orphaned and feeds the dev-deps check.
  • ignoreEntryPoints - leftover-but-committed files you no longer use. Matching files are excluded from reporting: never flagged as orphan files, and their unused exports are suppressed.

If init detected them for you (the default)

Every package workspace already has its patterns filled in. Detection is purely structural: any file that nothing else imports is treated as an entry point, then classified by path (tests, scripts, stories, *.config.* → dev; fixtures and snapshots → ignore; everything else → prod). Your job is to review, not to rewrite:

  • Dead files look exactly like roots. A leftover file nobody imports was listed as a production entry point. Delete it, or move it to ignoreEntryPoints - left in prodEntryPoints it is a root, and step 8 will never report it.
  • Check the prod/dev split. A dev-only file left in prodEntryPoints inflates the dev-dependency check in step 7; something that ships but landed in devEntryPoints hides real production usage.
  • Folded dir/** globs cover the whole directory. If you accepted folding below 100%, that glob also marks the directory's non-entry files as roots, so orphans inside it can no longer be reported. Where a directory mixes roots and internals, replace the glob with the explicit files.
  • A shared package's detected entry points are mostly noise. Its files are imported by other packages, so within its own scope they look like roots. Leave them, but keep reachability checks for that package at the root workspace - see steps 8 and 9.
  • The root workspace has none, by design. It is the cross-package scope; give it broad app entry points only when you enable the checks in steps 8 and 9 there.

If you skipped detection

Write the patterns yourself, per workspace:

{
"workspaces": [
{
"path": "apps/web",
"prodEntryPoints": ["app/**/page.tsx", "pages/**", "server/index.ts"],
"devEntryPoints": ["**/*.test.*", "*.config.*", "scripts/**"],
"ignoreEntryPoints": ["app/legacy/oldDashboard.tsx"]
}
]
}

Keep the list honest and start narrow. See entry points definition guide.

Keep the patterns alive as the project moves

Entry-point patterns rot: a file gets renamed, a package is restructured, and the glob that pointed at it silently matches nothing - so a real root quietly stops being a root and orphan files and unused exports start reporting nonsense. Nothing fails, which is exactly why it goes unnoticed - and in a monorepo it happens one package at a time.

rev-dep config lint finds every config glob that no longer matches a file - entry points, workspace paths, deny lists, boundary selectors - so run it as you iterate, or fold it into the run so each check tells you:

rev-dep config lint # report (--fix removes dead patterns)
rev-dep config run --lint-config # lint alongside every run, reusing its graph

See linting the config.

Paths with brackets? Entry-point patterns are globs, so a literal path containing [ ] or { } - such as a Next.js dynamic route pages/[clientId]/edit.tsx - won't match unless you escape the brackets: "pages/\\[clientId\\]/edit.tsx". See Glob patterns.

7. Node-module hygiene

rev-dep checks dependency declarations against a package.json (it never scans node_modules/). The rule of thumb: whoever compiles the code declares its dependencies. Enable these on each package that owns a package.json.

Which package.json is "whoever" depends on nodeModulesResolution from step 1 - the consuming workspace's package (entry-package, default) or each file's own nearest package (nearest-package, for pnpm's default layout). Set that first, or the two checks below may report false positives.

Unused node modules

Declared in package.json but never imported.

{
"unusedNodeModulesDetection": true
}

Tooling-only packages (bundler plugins, CLIs) aren't imported by source and may look unused - point the detector at them with pkgJsonFieldsWithBinaries / filesWithBinaries / filesWithModules instead of disabling it.

Learn more about the unusedNodeModulesDetection check configuration.

Missing node modules

Imported in code but not declared.

{
"missingNodeModulesDetection": true
}

If you see false positives in a monorepo - usually nodeModulesResolution not set to nearest-package for a pnpm-style layout, a dependency declared in the wrong package, or following misconfigured - the missing or unused dependency false positives troubleshooting guide explains how to fix them.

Learn more about the missingNodeModulesDetection check configuration.

Dev dependencies in production

A follow-up to the above: flags devDependencies reachable from your production entry points (they would crash a production install). Needs prodEntryPoints from step 6.

{
"devDepsUsageOnProdDetection": {
// Optional: type-only imports are stripped from production builds, so a dev
// dependency imported only as a type is not a runtime risk. Opt in to skip them.
"ignoreTypeImports": true
}
}

Learn more about the devDepsUsageOnProdDetection check configuration.

8. Orphan files

Files not reachable from any entry point - dead files left by refactors. Supports autofix (deletes the file).

{
"orphanFilesDetection": {
"autofix": true
}
}

A useful pattern is a second detector that excludes test files from the graph, to surface utilities used only by tests:

{
"orphanFilesDetection": [
true,
{
"graphExclude": ["**/*.test.*"]
}
]
}

Monorepo placement matters. Enable it on compiled apps (their dead code is truly dead) and at the root to catch unused files in shared packages - but not scoped to a shared package on its own, where cross-package usage is invisible.

At the root this is the "orphanFilesDetection": false placeholder init wrote - flip it to true, and give the root broad app entry points first (e.g. "prodEntryPoints": ["apps/web/**", "apps/mobile/**"]). Init leaves the root without entry points, and a root workspace with no entry points reports every file as an orphan. If shared-package files are wrongly reported, the orphan files and unused exports in shared packages troubleshooting guide explains the fix.

Learn more about the orphanFilesDetection check configuration.

9. Unused exports

Exported members never imported anywhere reachable. Enable it only once entry points are trustworthy - incomplete roots make live exports look dead. Supports autofix.

{
"unusedExportsDetection": {
"autofix": true
}
}

Like orphan files, run it at the root for shared packages so cross-package usage is counted - the same false placeholder to flip, and the same broad root entry points from step 8. To also catch app-local unused exports, enable it on the compiled app - but because the app follows shared source, its report will include shared-package exports; suppress those with an ignoreFiles pattern relative to the workspace (e.g. "ignoreFiles": ["../../packages/**"]) and let the root workspace own the shared findings. The orphan files and unused exports in shared packages troubleshooting guide explains why unused exports leaks across packages while orphan files does not, and walks through this setup.

Learn more about the unusedExportsDetection check configuration.

10. Restricted imports

Block specific files/modules from being reachable from chosen entry points - e.g. keep server-only code out of client bundles. Entry-point driven; entryPoints does not fall back to workspace-level entry points.

{
"restrictedImportsDetection": {
"entryPoints": ["app/**/page.tsx"],
"denyFiles": ["server/**"],
"denyModules": ["fs", "child_process"]
}
}

Learn more about the restrictedImportsDetection check configuration.

11. Module boundaries

Enforce layer/feature separation by file-path patterns: files matching pattern may not import paths in deny. Ideal for monorepo package boundaries (e.g. mobile must not import web).

{
"moduleBoundaries": [
{
"name": "mobile-isolation",
"pattern": "apps/mobile/**",
"allow": ["apps/mobile/**", "packages/shared/**"]
}
]
}

Boundaries are path-to-path; restricted imports is reachability-from-entry-points.

Learn more about the moduleBoundaries check configuration.

12. Import conventions

Enforce a consistent import style - relative within a domain, aliased across domains - so import shape alone reveals whether colocation is correct. Supports autofix.

{
"importConventions": [
{
"rule": "relative-internal-absolute-external",
"autofix": true,
"domains": [
{
"path": "src/utils/ui",
"alias": "@ui-utils"
},
{
"path": "src/utils/server",
"alias": "@server-utils"
},
{
"path": "src/components",
"alias": "@design-system"
}
]
}
]
}

Learn more about the importConventions check configuration.

13. Wire into CI

rev-dep config run exits 0 when everything passes and 1 when any check fails, so a single step gates your pipeline:

rev-dep config run

Run it on every PR to keep the dependency graph clean against both human and AI-introduced regressions. Add --lint-config to that step so the config itself is held to the same standard - a pattern that stopped matching anything (see step 6) then fails the build instead of quietly weakening a check. For machine-readable results, see output formats.