Trovella Wiki

Dependency Graph Validation

dependency-cruiser configuration reference -- forbidden rules, orphan detection, resolution options, and how to run it locally.

dependency-cruiser is a static analysis tool that validates JavaScript/TypeScript import graphs against configurable rules. It parses actual import and require statements across the entire codebase -- including dynamic imports and re-exports -- and checks them against forbidden patterns. With 978K weekly npm downloads, it is the most widely adopted import graph analysis tool in the JavaScript ecosystem.

Running Locally

pnpm dep-cruise

This expands to depcruise apps packages --config .dependency-cruiserrc.mjs, scanning all source directories against the rules in the configuration file.

Configuration

The full configuration lives in .dependency-cruiserrc.mjs at the monorepo root.

Forbidden Rules (severity: error)

Six rules enforce the 5-layer package hierarchy. All are error severity -- violations block CI.

no-circular -- Circular dependencies between packages break Turborepo build ordering. This catches any import cycle across the entire graph, not just direct A-imports-B-imports-A cycles.

{ name: "no-circular", from: {}, to: { circular: true } }

no-leaf-to-higher -- Leaf packages (utils, validators, design-tokens) cannot import from any higher layer.

{
  from: { path: "^packages/(utils|validators|design-tokens)/" },
  to: { path: "^(apps/|packages/(api|mcp|ai|auth|cache|logger|search|db)/)" }
}

no-core-to-service-or-app -- Core layer (db) cannot import from service (api, mcp) or app layers.

{
  from: { path: "^packages/db/" },
  to: { path: "^(apps/|packages/(api|mcp)/)" }
}

no-core-to-infra -- Core layer cannot import from infrastructure packages (ai, auth, cache, search).

{
  from: { path: "^packages/db/" },
  to: { path: "^packages/(ai|auth|cache|search)/" }
}

no-infra-to-service-or-app -- Infrastructure packages cannot import from service or app layers.

{
  from: { path: "^packages/(ai|auth|cache|logger|search)/" },
  to: { path: "^(apps/|packages/(api|mcp)/)" }
}

no-service-to-app -- Service packages (api, mcp) cannot import from the app layer.

{
  from: { path: "^packages/(api|mcp)/" },
  to: { path: "^apps/" }
}

Orphan Detection (severity: warn)

The no-orphans rule warns about files that nothing imports and that import nothing -- likely dead code. This is warn rather than error because legitimate orphans exist.

Excluded patterns (files that are orphans by design):

PatternWhy
DotfilesConfiguration files
*.d.tsType declaration files
tsconfig.json, vitest.config.ts, eslint.config.*Tooling configuration
__tests__/, .test., .spec.Test files
seed, migrateDatabase scripts
index.ts, route.ts, server.tsEntry points
layout.tsx, page.tsx, middleware.tsNext.js App Router conventions
instrumentation.ts, global-error.tsx, not-found.tsxNext.js special files
postcss.config.*PostCSS config
trovella-logo.tsxUsed via @/ alias that dependency-cruiser cannot resolve
source.config.tsFumadocs MDX source config (consumed by plugin)
content/MDX files compiled by Fumadocs, not imported via module resolution

Resolution Options

options: {
  doNotFollow: { path: "node_modules" },
  exclude: { path: "coverage/" },
  tsPreCompilationDeps: true,
  enhancedResolveOptions: {
    exportsFields: ["exports"],
    conditionNames: ["import", "require", "node", "default"],
  },
}
  • tsPreCompilationDeps: true -- resolves TypeScript path aliases and tsconfig.json paths before analysis
  • enhancedResolveOptions -- respects package.json exports fields and standard Node.js conditions

What dependency-cruiser Catches That ESLint Misses

ESLint no-restricted-imports sees one file at a time. It catches direct import { db } from "@repo/db" but cannot detect:

  • Dynamic imports: const mod = await import("@repo/db") -- dependency-cruiser follows these
  • Re-exports: Package A re-exports from Package B, and Package C imports from Package A -- ESLint only sees the A-to-C import, not the transitive B-to-C dependency
  • Transitive chains: A imports B imports C -- if A-to-C is forbidden, only the whole-graph analysis catches it

CI Integration

dependency-cruiser runs as part of pnpm ci:check (the pre-push quality gate) and in the GitHub Actions quality job. It runs after pnpm lint and before pnpm lint:dead-code.

A non-zero exit code from pnpm dep-cruise blocks the PR from merging.

On this page