dead-dep
v1.0.0
Published
Detect unused dependencies and aging TODOs in a project
Maintainers
Readme
dead-dep
A CLI tool that detects unused dependencies and aging TODO comments in a TypeScript/JavaScript project.
Usage
node dist/index.js [path] # analyse a project (defaults to current directory)
node dist/index.js . --no-deps # skip dependency check
node dist/index.js . --no-todos # skip TODO scan
node dist/index.js . --min-age 30 # only show TODOs older than 30 days
node dist/index.js . --json # machine-readable outputThe tool exits with code 1 if any findings are reported, 0 if clean — so it can block a CI pipeline automatically.
Building and testing
npm run build # compile TypeScript → dist/
npm test # run the test suite (or use the ddt alias if configured)How it works
The tool is made up of eight focused modules that snap together in index.ts.
src/normalize.ts — Clean up import strings
Every import string found in source code passes through here before being compared against package.json. The function returns the bare package name, or null if the import should be ignored entirely.
'./utils' → null (relative — not a package)
'fs' → null (Node.js built-in)
'node:path' → null (Node.js built-in, prefixed form)
'lodash/fp' → 'lodash' (strip subpath)
'@org/pkg/deep/sub' → '@org/pkg' (strip subpath from scoped package)A Set of all Node built-in names is built once at startup from Node's own builtinModules list so the check is fast.
src/parsePackage.ts — Read package.json
Reads and parses package.json from the target directory and returns three Set<string> values: dependencies, devDependencies, and all (the union of both).
Two defensive measures:
JSON.parseis wrapped in atry/catchso a malformed file produces a friendly error instead of a raw stack trace.- A
safeKeys()helper checks thatdependencies/devDependenciesare actually objects before callingObject.keys()on them — guarding against unusual (but valid) package.json shapes.
src/findFiles.ts — Discover source files
Uses the glob library to recursively find all .ts, .tsx, .js, and .jsx files under the target directory. Common directories that should never be scanned (node_modules, dist, build, coverage, .git) are excluded automatically.
src/extractImports.ts — Find what each file imports
Uses ts-morph to parse source files into an AST (Abstract Syntax Tree) — a structured representation of the code the same way a compiler sees it. This is more reliable than regex because it understands the language.
Three forms of import are detected:
| Form | Example |
|---|---|
| Static import | import lodash from 'lodash' |
| CommonJS require | const x = require('lodash') |
| Dynamic import | const x = await import('lodash') |
| Re-export | export { foo } from 'lodash' |
Each specifier is passed through normalizeSpecifier before being added to the result set.
src/analyzeDeps.ts — Compare the two lists
Takes the set of declared packages (from package.json) and the set of used packages (from source files) and produces two lists:
- Unused — declared but never imported
- Missing — imported but not declared
Some packages are legitimately never imported directly — build tools, test runners, linters, type packages. These are filtered out before reporting to avoid false positives:
// Never imported directly, but genuinely used:
'typescript', 'ts-node', 'jest', 'eslint', 'prettier', 'webpack', '@types/*', ...src/scanTodos.ts — Find TODO comments
Reads each source file line by line and tests each line against a regular expression that matches TODO-style comments in any comment syntax:
// TODO: fix this
/* FIXME: urgent */
/** TODO: add docs */
* HACK: workaround ← line inside a JSDoc blockTags detected: TODO, FIXME, HACK, XXX. The tag and description text are captured separately so they can be displayed and coloured independently.
src/blameCache.ts — Git blame with caching
For each TODO found, this module asks git who wrote that line and when. It calls git blame --line-porcelain which outputs detailed per-line author and timestamp data, then parses that output to extract the author name and compute how many days ago the commit was made.
Caching: blame is run once per file and the results stored in a Map. If a file contains ten TODOs, git is only called once for that file and the per-line lookups hit the in-memory cache. The cache is cleared at the start of each run via clearCache().
src/report.ts — Format and print results
Renders the findings to the terminal using chalk for colour. TODOs are sorted oldest-first so the most stale items appear at the top. Age is colour-coded:
| Age | Colour | |---|---| | > 180 days | Red | | > 60 days | Yellow | | ≤ 60 days | Green |
When --json is passed, the entire output is serialised with JSON.stringify instead, suitable for piping into other tools.
src/index.ts — The entry point
Wires all the modules together using the commander library for CLI argument parsing. The key steps in order:
- Clear the blame cache (
clearCache()) - Resolve the target path and discover all source files
- If
--deps: parsepackage.json, extract imports, diff the two sets - If
--todos: scan for TODOs, pre-warm the blame cache for all unique files in parallel withPromise.all, then look up blame per TODO - Print the report
- Exit with code
0(clean) or1(findings found)
The parallel blame pre-warming in step 4 means all git subprocess calls happen concurrently rather than sequentially — important for projects with many TODO-containing files.
Project structure
src/
├── index.ts CLI entry point — wires everything together
├── parsePackage.ts Reads and validates package.json
├── findFiles.ts Globs source files
├── extractImports.ts AST-based import extraction via ts-morph
├── normalize.ts Cleans import specifiers
├── analyzeDeps.ts Set diff: unused and missing deps
├── scanTodos.ts Regex scan for TODO/FIXME/HACK/XXX
├── blameCache.ts git blame with per-file caching
└── report.ts Formats and prints results
tests/
├── normalize.test.ts
├── analyzeDeps.test.ts
├── parsePackage.test.ts
├── scanTodos.test.ts
├── extractImports.test.ts
├── blameCache.test.ts
└── report.test.ts