Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

Β 

History

30 Commits

Folders and files

Repository files navigation

orphan-files

orphan-files logo

cli-available node version npm version downloads count size license github-ci

πŸ”¨ Find the files your project stopped using - including the ones that only import each other.

npx orphan-files

orphan-files demo

Most 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 point

Give a ⭐️ if this project helped you!

Features ✨

  • 🧠 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 tsconfig path aliases and baseUrl
  • πŸ“Š 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

Why not just use knip?

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.

How it works

  1. Globs the project for source files (honouring .gitignore and your exclude patterns).
  2. Parses each file with Babel and extracts every import specifier.
  3. Resolves each specifier (relative paths, tsconfig path aliases, baseUrl, import.meta.glob).
  4. Determines entry points - package.json (main/module/exports/bin/types), framework conventions (Next.js, Vite, Storybook, Remotion…), tests, configs, index/main barrels, and your config.
  5. 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.

Installation

npm install -g orphan-files

CLI

# 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

Options

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

Incremental adoption (baseline)

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.json

Visualise the dependency graph

npx orphan-files --graph mermaid          # paste into a Mermaid renderer
npx orphan-files --graph html > graph.html

Configuration

Create 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.

Framework auto-detection

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

TypeScript path aliases

Reads tsconfig.json and resolves path aliases automatically (e.g. @/* β†’ src/*).

Supported import expressions

  • import '...' / import x from '...'
  • require('...')
  • import('...') (dynamic import) - template literals like import(`./pages/${name}.js`) are matched as a glob
  • import.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.

GitHub Action

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

Inputs

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

Outputs

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.

Pull request comments

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.

Code scanning (SARIF)

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.sarif

Without the action (plain npx):

- run: npx orphan-files --format sarif > orphan.sarif

API

import {
  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);

🀝 Contributing

Contributions, issues and feature requests are welcome!
Feel free to check issues page.

Setup, tests and the release flow are described in CONTRIBUTING.md.

Related packages

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.


License

The MIT License @ 2026

Releases

Packages

Contributors

Languages