async-phase
v0.1.9
Published
Detect low fan-out abstractions in TypeScript/React/Node codebases.
Downloads
89
Maintainers
Readme
modgraph
Detect low fan-out abstractions in your TypeScript/React/Node codebase.
modgraph is a small CLI that scans your project for single-use abstractions: exported functions, hooks, classes, and factories that are only used from a single other module.
The goal is to make hidden complexity visible, not to enforce a particular architecture. It is an informational + CI-enforceable tool that helps you have better conversations about premature abstractions.
Why this exists (premature abstraction)
Many codebases accumulate:
- Small utility modules with a single caller
- Hooks that wrap a single component’s logic
- Factories that are only ever used once
These “abstractions” add indirection and maintenance cost without providing reuse. They:
- Make code harder to navigate
- Hide behavior behind extra files
- Add overhead when refactoring
modgraph surfaces these cases so you can decide whether to:
- Inline the abstraction back into its only caller, or
- Invest in making it genuinely reusable
It never rewrites code or enforces a specific style; it just gives you data.
Install
npm install --save-dev modgraphYou can also run it via npx without installing globally:
npx modgraph scanWhat it detects
modgraph parses your project and looks for exports that:
- Are exported from a
.tsor.tsxfile - Are used from exactly one other module (fan-out = 1)
It considers:
- Exported functions
- Exported custom hooks (name starts with
use) - Exported classes
- Exported factory functions (name starts with
createor ends withFactory)
It intentionally ignores:
- Default-exported React components
- Next.js page files and API route handlers (as long as they are default exports)
- Barrel re-export-only files (
index.tsthat just re-export) - Type-only exports (files that only export types/interfaces)
- Test files (
*.test.ts,*.spec.ts,tests/,__tests__/) - Storybook files (
*.stories.ts[x],.storybook/, etc.)
For each abstraction it keeps, it computes:
nametype(hook | function | class | factory)file(full path)loc(lines of code in the abstraction)internalImports(number of import statements in the file that defines it)fanOut(number of other files that import it)score(Abstraction Cost Score)
Abstraction Cost Score
The score is a rough measure of how “expensive” the abstraction is to maintain:
score = (LOC * 0.5) + (internalImports * 2)The result is rounded to the nearest integer.
Roughly:
- More lines → higher score
- More imports in that file → higher score
Combined with low fan-out, this can indicate over-abstracted code.
CLI usage
Basic scan (table output):
npx modgraph scanOptions:
--ciExit with non-zero status code if the number of single-use abstractions exceeds the threshold.--threshold <n>Maximum allowed single-use abstractions. Default:10.--jsonOutput JSON instead of a table (useful for tooling/CI).--root <path>Project root directory. Defaults toprocess.cwd().
Examples:
# Scan the current project and print a table
npx modgraph scan
# Scan another project directory
npx modgraph scan --root ../my-app
# Print JSON instead of a table
npx modgraph scan --json
# CI-style run with a stricter threshold
npx modgraph scan --ci --threshold 5Sample table output
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Modgraph Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Detected Single-Use Abstractions: 3
┌───────────────────────┬───────────┬───────┬──────────┬──────┐
│ Name │ Kind │ Lines │ Used by │ Cost │
├───────────────────────┼───────────┼───────┼──────────┼──────┤
│ useFormattedDate │ hook │ 23 │ 1 │ 14 │
│ createApiClient │ factory │ 51 │ 1 │ 33 │
│ formatCurrency │ function │ 18 │ 1 │ 9 │
└───────────────────────┴───────────┴───────┴──────────┴──────┘
Lines = lines of code in the abstraction.
Used by = number of other files that import it.
Cost = rough maintenance cost (higher = more complex).
Recommendation:
Review abstractions with low reuse and high cost scores.If no single-use abstractions are found:
No single-use abstractions detected. Architecture looks lean.JSON output
When you pass --json, modgraph prints a machine-readable JSON report:
npx modgraph scan --jsonExample:
{
"total": 3,
"threshold": 10,
"abstractions": [
{
"name": "useFormattedDate",
"type": "hook",
"file": "/app/src/hooks/useFormattedDate.ts",
"loc": 23,
"internalImports": 1,
"fanOut": 1,
"score": 14
},
{
"name": "createApiClient",
"type": "factory",
"file": "/app/src/api/createApiClient.ts",
"loc": 51,
"internalImports": 4,
"fanOut": 1,
"score": 33
},
{
"name": "formatCurrency",
"type": "function",
"file": "/app/src/utils/formatCurrency.ts",
"loc": 18,
"internalImports": 0,
"fanOut": 1,
"score": 9
}
]
}This is stable enough to consume in:
- Custom CI checks
- Dashboards
- Editor extensions
CI usage (GitHub Actions example)
You can enforce a maximum number of single-use abstractions in CI. For example, in GitHub Actions:
name: modgraph
on:
push:
branches: [main]
pull_request:
jobs:
modgraph:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install
- name: Run modgraph
run: npx modgraph scan --ci --threshold 10If the number of single-use abstractions is greater than the threshold:
- The report is printed.
modgraphprints:CI FAILED: Single-use abstraction threshold exceeded.- The process exits with code
1, causing the job to fail.
If the count is within the threshold, it exits with code 0.
Philosophy
modgraph is intentionally:
- Informational – it tells you what exists; it does not tell you what to do.
- Non-prescriptive – it does not enforce a specific architecture or folder layout.
- Safe – it never rewrites files or mutates your codebase.
Use it as a conversation starter:
- Are these abstractions buying us anything?
- Should we inline them?
- Should we invest in making them reusable?
The tool does not know the answers, but it helps you ask better questions.
License
MIT
