npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@acoyfellow/deadlint

v0.0.1

Published

Find dead cross-boundary code (Workers DurableObject / RpcTarget / Agent methods) and structural clones in a TypeScript codebase.

Downloads

129

Readme


Find dead public methods on Cloudflare Workers DurableObject / WorkerEntrypoint / WorkflowEntrypoint / RpcTarget / Agent subclasses, and structural clones across your TypeScript codebase.

The gap

| Tool | Stops at | Result | | ---------------------------- | ----------------------- | ------------------------------------------------- | | tsc (noUnusedLocals) | function boundary | unused vars only | | oxlint / biome / eslint| file/class boundary | every export is treated as live | | knip / ts-prune | module-export boundary | every public class member is treated as the API | | deadlint | RPC stub / clone | this is the layer that was missing |

In a Workers / Agents codebase, every public method on a DO is — to a static analyzer — an entry point. Anyone with a stub could call it, so nothing dares flag it. Real codebases accumulate dead RPC methods for years and no linter will tell you.

deadlint walks the call graph plus a targeted token scan and tells you which ones are actually unreachable.

What it found, on real repos

$ npx @acoyfellow/deadlint ./artifacts
deadlint report — ./artifacts

Dead RPC methods (1)
───────────────────────────
  GitServer.streamingUploadPack  (extends DurableObject)
    apps/worker/src/git-server.ts:299

Clones — engine: inline (3)
───────────────────────────
  1.00  27L  readBlob ≈ readTree
    src/capabilities/repo.ts:135
    src/capabilities/repo.ts:167
  1.00  27L  readBlob ≈ readCommit
    src/capabilities/repo.ts:135
    src/capabilities/repo.ts:199
  1.00  27L  readTree ≈ readCommit
    src/capabilities/repo.ts:167
    src/capabilities/repo.ts:199

4 findings

streamingUploadPack was a wrapper around streamingUploadPackWithEventType that nothing called for months. readBlob / readTree / readCommit were 27-line copy-pastes of each other. No other linter saw any of it.

Install and run

# one-off
npx @acoyfellow/deadlint /path/to/your/repo

# global
npm i -g @acoyfellow/deadlint
deadlint /path/to/your/repo

The path needs a tsconfig.json somewhere — at the root, in apps/worker/, or anywhere within the first 3 directory levels. Multi-package monorepos without a root config (a packages/api/tsconfig.json + packages/web/tsconfig.json layout) are auto-discovered and scanned per-project.

deadlint ./repo                              # full scan (default)
deadlint ./repo --check dead-rpc             # just the dead methods
deadlint ./repo --check clones               # just the clones
deadlint ./repo --clones-engine both         # similarity-ts + inline engine
deadlint ./repo --json > findings.json       # machine-readable
deadlint --help                              # all flags

Exit 0 = clean, 1 = findings, 2 = misconfig.

What it actually checks

Dead RPC methods. For every public method on a class extending DurableObject, WorkerEntrypoint, WorkflowEntrypoint, RpcTarget, or Agent, deadlint looks for callers across three signals:

  1. The TypeScript language service (precise — but blind to JSRPC stubs).
  2. A token scan for .method( / ["method"]( direct dispatch.
  3. A token scan for .call("method", …) string-key dispatch — the Agents SDK pattern frontend code uses to reach DO methods through the WebSocket proxy.

Patterns 2 and 3 are scanned across both TypeScript files and companion files (.svelte, .vue, .astro, .tsx, .jsx) so frontend call sites that aren't compiled by your tsconfig are still seen. A method is flagged dead only when all three signals turn up zero. Common stdlib names (map, then, set, …) are excluded from the token scans to avoid coincidental keep-alives. Workers/Agents runtime hooks (fetch, alarm, onConnect, …) are allow-listed.

Override the boundary class list with --bases Foo,Bar.

Structural clones. Two engines, run independently, results merged.

similarity (default) shells out to similarity-ts — a Rust binary using oxc-parser and TSED. Higher precision, biased toward larger functions. Install once: cargo install similarity-ts.

inline is built into deadlint, ~150 lines of ts-morph. Each function body is normalized to a SyntaxKind-only token sequence (identifiers and literals erased, so renamed copies match exactly). Findings are emitted on identical shapes (1.00) or 5-gram Jaccard similarity above --clone-threshold (default 0.85). No external dependencies.

Use --clones-engine both to run them side by side. They find largely non-overlapping pairs.

Always on, every repo

deadlint --install-hook

Sets git config --global core.hooksPath ~/.config/git/hooks and writes a pre-push script that runs deadlint . --check dead-rpc before every push. Covers every repo on your machine — public, private, GitHub, GitLab. The hook silently no-ops on non-TypeScript repos.

deadlint --hook-status      # is it installed?
deadlint --uninstall-hook   # remove it (only if we wrote it)
git push --no-verify        # bypass once

The installer refuses to clobber a pre-existing pre-push hook unless you pass --force. The uninstaller refuses to remove anything that isn't deadlint-managed. You can't accidentally lose work.

What it won't catch

  • Fully dynamic RPC: stub[methodFromConfig]() where the method name is computed at runtime. None of the signals can see it.
  • Cross-repo dead code. If your callers live in a different repository, deadlint sees nothing.
  • HTTP routes dispatched by URL path rather than method name. Add the router class to --bases if appropriate.
  • Behavioral clones with different control flow. The inline engine is shape-based; the similarity engine helps but isn't magic.

Build-output directories (dist, build, .svelte-kit, .next, etc.) are excluded by default — they generate ~100% clone matches against the source they were built from. Pass --exclude to replace the list, or --also-exclude to extend it. Note: lib is not in the default list because SvelteKit and many other frameworks use src/lib/ for source code; pass --also-exclude lib if your project emits to it.

Findings are meant for human review. The tool biases toward false negatives — it would rather miss a dead method than wrongly flag a live one.

License

MIT