π¨ Find the files your project stopped using - including the ones that only import each other.
npx orphan-filesMost tools ask "does anything import this file?". That question misses dead islands: a group of files that import each other, but which nothing reachable ever imports. They keep each other "used" forever.
orphan-files instead walks the import graph from your entry points outwards (import, require, dynamic import(), import.meta.glob, jest.mock, export * from, β¦). Anything the walk never reaches is unused - islands included.
Here report-builder and report-utils import each other, and nothing else imports either of them. An importedBy > 0 check calls both "used"; a reachability walk reports both:
$ npx orphan-files
Found 2 unused files (204 B, 7 LOC reclaimable):
src/legacy/report-builder.js 89 B
src/legacy/report-utils.js 115 B
$ npx orphan-files --why src/legacy/report-builder.js
src/legacy/report-builder.js: unused - not reachable from any entry pointGive a βοΈ if this project helped you!
- π§ True reachability analysis - dead islands that only import each other don't mask each other
- π³ Babel AST parsing - JSX, TypeScript, decorators, top-level await
- π§ Framework auto-detection - Next.js, Vite, Storybook, Remotion
- πΊοΈ Resolves
tsconfigpath aliases andbaseUrl - π Four output formats -
cli,json,sarif,pdf - π§Ή Auto-deletion -
--fix(dry-run) and--force - π Explainability -
--why <file>tells you why a file is kept or unused - πΈοΈ Graph export -
mermaid,dot,html - π Baseline for incremental adoption
- π€ Ready-made composite GitHub Action with SARIF for the Code scanning tab
- π¦ Monorepo support - honours each workspace package's entry points
- π Honours
.gitignore
knip is the de-facto standard and does much more than this tool: unused files, exports, dependencies, and ~150 framework plugins. If you want one tool for all of that, use knip.
orphan-files deliberately does one job: whole unused files. That focus is what it buys you:
orphan-files |
Typical file-level alternatives | |
|---|---|---|
| Analysis model | Reachability from entry points - catches dead islands | Usually importedBy == 0, which islands defeat |
| "Why is this file kept?" | --why <file> prints the import chain |
Rarely available |
| Output formats | cli, json, sarif, pdf |
Usually cli only |
| CI adoption on a legacy repo | --baseline + --max-unused |
Often all-or-nothing |
| GitHub integration | Composite Action + SARIF β Code scanning tab | Usually DIY |
| Graph export | mermaid, dot, html |
Separate tool |
Full, sourced breakdown of 20+ tools: docs/comparison.md.
- Globs the project for source files (honouring
.gitignoreand yourexcludepatterns). - Parses each file with Babel and extracts every import specifier.
- Resolves each specifier (relative paths,
tsconfigpath aliases,baseUrl,import.meta.glob). - Determines entry points -
package.json(main/module/exports/bin/types), framework conventions (Next.js, Vite, Storybook, Remotionβ¦), tests, configs,index/mainbarrels, and your config. - Walks the graph from those entry points; anything it can't reach is unused.
This is a true reachability analysis: two dead files that import each other will not mask each other.
npm install -g orphan-files# scan current directory
orphan-files
# scan a specific project
orphan-files /path/to/project
# preview what would be deleted, then actually delete
orphan-files --fix
orphan-files --fix --force
# explain why a file is kept (or unused)
orphan-files --why src/utils/helpers.ts
# CI: machine-readable output (exit code 1 when unused files are found)
orphan-files --format json
orphan-files --format sarif > orphan.sarif
# scaffold a config file
orphan-files --init| Option | Description |
|---|---|
-c, --config <path> |
Config file (default: orphan-files.config.{js,mjs,cjs,json}) |
-f, --format <type> |
Output: cli, json, sarif, pdf (default: cli) |
--sort <key> |
Sort unused files: path, name, size |
--group |
Group unused files by directory |
--why <file> |
Explain why a file is kept or unused, then exit |
--graph <type> |
Print the dependency graph: mermaid, dot, html |
--fix |
Preview files that would be deleted (dry-run) |
--force |
With --fix, actually delete the files |
--baseline <path> |
Ignore unused files recorded in a baseline file |
--update-baseline [path] |
Write the current unused files as the baseline, then exit |
--max-unused <n> |
Exit 0 when the unused count is at most <n> |
--no-gitignore |
Do not honour .gitignore |
--init |
Write a starter config file, then exit |
-v, --version |
Print version |
-h, --help |
Show help |
Record the current unused files and fail CI only on new ones:
npx orphan-files --update-baseline # writes .orphan-files-baseline.json
npx orphan-files --baseline .orphan-files-baseline.jsonnpx orphan-files --graph mermaid # paste into a Mermaid renderer
npx orphan-files --graph html > graph.htmlCreate an orphan-files.config.js file in your project root - .mjs, .cjs and .json work too, and are picked up automatically:
export default {
include: ["**/*.{js,jsx,ts,tsx,mjs,cjs}"],
exclude: [
"**/node_modules/**",
"**/dist/**",
"**/.next/**",
"**/storybook-static/**",
],
// Entry points: kept, and everything they import is kept transitively.
entry: ["src/index.ts"],
// Also treated as entry points (kept and seed reachability).
exceptions: [
"index.{js,ts}",
"*.config.{js,ts,mjs,cjs}",
"**/*.test.{js,ts,tsx}",
"**/*.spec.{js,ts,tsx}",
"bin/**",
"scripts/**",
],
};The .js, .mjs and .cjs forms use a default export; .json holds the object itself. Extensions are tried in the order .js, .mjs, .cjs, .json, and --config <path> overrides the lookup entirely.
In a CommonJS project (no "type": "module" in package.json) prefer orphan-files.config.mjs: an ESM config in a plain .js file still works, but Node reparses it and prints a MODULE_TYPELESS_PACKAGE_JSON warning on every run.
Monorepos are supported: each workspace package's package.json entry points are honoured.
The tool reads package.json and automatically adds exceptions for known frameworks:
| Framework | Detected exceptions |
|---|---|
| Next.js | app/**/page.tsx, app/**/route.ts, app/**/layout.tsx, sitemap.ts, middleware.ts, etc. |
| Storybook | **/*.stories.{ts,tsx} |
| Remotion | remotion.config.*, src/index.ts |
Reads tsconfig.json and resolves path aliases automatically (e.g. @/* β src/*).
import '...'/import x from '...'require('...')import('...')(dynamic import) - template literals likeimport(`./pages/${name}.js`)are matched as a globimport.meta.glob('./dir/*.js')(Vite)jest.mock('...')/vi.mock('...')export * from '...'/export { x } from '...'
Files using decorators (Angular, NestJS, TypeORM, MobX) and other modern TypeScript syntax are parsed correctly.
Run it in CI and fail the build when unused files appear:
# .github/workflows/orphan-files.yml
name: orphan-files
on: [push, pull_request]
jobs:
unused:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: piecioshka/orphan-files@v1| Input | Default | Description |
|---|---|---|
directory |
. |
Project directory to scan |
format |
cli |
Output format: cli, json, sarif, pdf |
args |
(none) | Extra CLI arguments, e.g. --baseline .orphan-files-baseline.json |
output-file |
(none) | Write the report to this file instead of stdout |
version |
latest |
npm version/tag of orphan-files to run |
comment |
false |
Comment the report on the pull request |
token |
${{ github.token }} |
Token used to post the comment |
| Output | Description |
|---|---|
unused-count |
Number of unused files found |
reclaimable-bytes |
Total size of those files, in bytes |
total-files |
Number of files scanned |
report-file |
Path the report was written to, when output-file was given |
The three counters are read from the JSON report, so they are only set when format: json. Other formats leave them empty rather than paying for a second scan.
- uses: piecioshka/orphan-files@v1
id: orphans
with:
format: json
args: "--max-unused 5"
continue-on-error: true
- run: echo "${{ steps.orphans.outputs.unused-count }} unused files, ${{ steps.orphans.outputs.reclaimable-bytes }} B reclaimable"The action exits with code 1 when unused files are found, so the job fails by default. Use args: "--max-unused <n>" or a baseline to adopt it incrementally.
Set comment: true to have the report posted on the pull request. The action keeps one comment and edits it on every run, so a busy branch does not collect a stack of near-identical bot messages.
# .github/workflows/orphan-files.yml
name: orphan-files
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
unused:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: piecioshka/orphan-files@v1
with:
comment: true
args: "--baseline .orphan-files-baseline.json"The job needs pull-requests: write. A clean run stays silent, and clears a previous report if there was one. Failing to comment (a missing permission, a fork's read-only token) only logs a warning - the scan result stays the signal that fails the build.
The comment needs the JSON report, so when format is anything else the action runs a second scan to produce it.
Upload SARIF to get inline annotations in the GitHub "Code scanning" tab:
# .github/workflows/orphan-files.yml
name: orphan-files
on: [push, pull_request]
permissions:
contents: read
security-events: write
jobs:
unused:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: piecioshka/orphan-files@v1
with:
format: sarif
output-file: orphan.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: orphan.sarifWithout the action (plain npx):
- run: npx orphan-files --format sarif > orphan.sarifimport {
scanProject,
extractImports,
analyze,
findUnusedFiles,
explainFile,
} from "orphan-files";
const files = await scanProject("/path/to/project", "**/*.{js,ts}");
const fileImports = {};
for (const file of files) {
fileImports[file] = extractImports(file);
}
// High-level: graph + entry points + reachability + unused list
const result = analyze(files, fileImports, { projectDir: "/path/to/project" });
console.log(result.unused);
console.log(explainFile(files[0], result, "/path/to/project"));
// Or the simple helper (exceptionPatterns double as entry points):
const unused = findUnusedFiles(files, fileImports, [], "/path/to/project");
console.log(unused);Contributions, issues and feature requests are welcome!
Feel free to check issues page.
Setup, tests and the release flow are described in CONTRIBUTING.md.
Tip
See docs/comparison.md for a sourced, up-to-date comparison of 20+ tools against orphan-files - analysis model, file/export/dependency detection, monorepo support, output formats and maintenance status.
Closest alternatives (whole files + reachability + standalone CLI):
- knip - the de-facto standard; files, exports and dependencies, ~150 framework plugins.
- skott - builds and visualises the dependency graph, detects disconnected files.
- rev-dep - tracks imports, detects unused code, fast CLI.
Different job, complementary - unused exports (ts-unused-exports, find-unused-exports, tsr), local dead code (dead-code-checker), dependencies (depcheck), imports (eslint-plugin-unused-imports) and graph visualisation (madge).
Bundler plugins require running a build, unlike a static CLI - webpack-deadcode-plugin, vite-plugin-unused-code, rollup-plugin-unused, unplugin-slim.
Unmaintained - unimported (archived 2024), depcheck (archived 2025), deadfile, next-unused, orphan.
The MIT License @ 2026
