We ran
Two reasons cycles hide:
Type-only cycles are invisible at runtime. A cycle that closes through import type { Foo } is erased at compile time — no bundle edge, no module load edge. The risk: the moment someone adds a value export to that file, the type-only cycle becomes a value cycle silently.
Depth limits silently truncate the search. Some detectors impose a maxDepth on DFS traversal. Cycles longer than that limit are never reported — the rule exits clean and nobody knows. The for a one-time audit: see every cycle in your codebase today
ESLint for ongoing prevention: catches cycles as you create them, in CI on every PR
# Audit what you have today
npx madge --circular --extensions ts src/
For CI prevention, eslint-plugin-import-next is the fast-tier replacement for eslint-plugin-import — it resolves each file's import graph once and caches globally, instead of re-traversing from every entry point. .
Where do circular dependencies hide in your codebase — data layer, domain layer, or somewhere you didn't expect? The console.log trick is usually the tell.
Part of the
📦
| |
SOCIAL SHARE CARD GENERATOR