The dependency bugs that only surface for your users

You publish a package. CI is green, the types look right, you ship it. Then a consumer installs it from npm, and TypeScript quietly resolves part of your public API to any — or an import just fails to resolve.

The dependency was there on your machine. It was hoisted into the root node_modules, or it was a devDependency, or it was linked from a workspace sibling. Your build found it. A consumer gets none of that — only your consumer-visible dependencies (dependencies / peerDependencies / optionalDependencies, never devDependencies) — so the reference breaks. With skipLibCheck: true (very common) the failure is silent: the affected exports degrade to any and nobody notices until someone files an issue.

There’s a subtler version, too. For a shared singleton like react (or its types, @types/react), the right home is usually peerDependencies — you want the consumer’s single copy, not a second one pulled in alongside yours (a duplicate react is its own class of bug). Yet packages routinely declare these as a peer dependency nowhere at all — they lean on the consumer “probably having react anyway,” which holds until it doesn’t.

dependency-audit catches this class of bug before you publish.

Where it came from

This grew out of a recurring, hard-to-catch problem in Gutenberg — a monorepo of 100+ published @wordpress/* packages behind the WordPress block editor. Package after package referenced a module in its published files — most often import('react').ReactNode in an emitted .d.ts — without declaring it where a consumer could resolve it. These got fixed one batch at a time, until the real question came up in review:

Can we enforce this somehow? … we have no protections against regressions, and new dependencies will be easy to miss in the same way we missed these until now.

That tool is dependency-audit – which I built last weekend, with Claude Code and Codex working together, to do exactly that: fail this class of bug in CI before it ships, no matter how anyone’s node_modules is laid out.

Why existing tools miss it

It’s tempting to think “use an isolated install — pnpm, or npm’s install-strategy=linked — and you’ll catch these.” Isolated layouts surface the fully undeclared phantom deps — but they can’t see the bigger share: a dependency declared in the wrong place. @types/react in devDependencies resolves perfectly at your isolated build, yet still leaks into your published .d.ts and breaks consumers. Whether it belongs in dependencies or devDependencies hinges on whether it appears in your emitted surface — which neither an isolated install nor a source-level linter (ESLint only sees source) can determine. And publint / attw answer different questions (is the package well-formed? are the types correct under each resolution mode?) — not “is every reachable import declared and resolvable?”

What it actually does

dependency-audit materializes only your consumer-visible declared deps (production + peer + optional — never dev), fresh, into a throwaway tree — never your ambient node_modules — and resolves your package’s released surface against them:

  • Both surfaces — the type (.d.ts) surface and the runtime (JS) surface. It follows the real import graph from your exports / bin / main.
  • Anything reachable but undeclared, or declared but unresolvable, is a finding. No target or dependency code is ever executed — it’s fully static.
  • Point it at a directory, a .tgz, or any published npm specname@version, a tag, a scope.
# no install — audit any published package right now
npx @mawesome/dependency-audit react@18

# in CI: exit 0 clean, 1 findings, 2 error; --json for machine output
npx @mawesome/dependency-audit --json ./packages/*

Terminal: npx @mawesome/dependency-audit debug@4 reports one finding — supports-color is imported at runtime but not declared as a dependency.

A clean package reports nothing reachable-but-undeclared:

Terminal: npx @mawesome/dependency-audit react reports 0 findings — no undeclared imports.

Try it in your browser — no install

There’s a live, fully client-side playground: type any published package and it fetches the tarball, extracts it, and runs the same analysis core in your browser.

👉 mawesome.pages.dev/dependency-audit/playground

Try chalk@5 (a vendored .d.ts references node:tty → undeclared @types/node) or debug@4 (supports-color required at runtime, not declared).

Browser playground auditing chalk@5: a types finding — the declaration references node:tty, so @types/node is undeclared.

A clean result:

Browser playground auditing react: no issues found — every reachable import is declared and resolvable.

Docs, the resolution model, and how it compares to publint/attw: mawesome.pages.dev/dependency-audit. It’s MIT, on npm as @mawesome/dependency-audit. Feedback and bug reports welcome.