Skip to content

Why this exists

This tool grew out of a recurring, hard-to-catch problem in Gutenberg — a monorepo of 100+ published @wordpress/* packages that power the WordPress block editor. Package after package referenced a module in its published files — most often import('react').ReactNode in an emitted build-types/*.d.ts — without declaring it where a consumer could resolve it. It built fine in the repo, because the module was resolvable at the author’s build somehow: hoisted into the root node_modules on classic npm, or present as a devDependency, or linked from a workspace sibling. A consumer installing the package from npm gets none of that — only the package’s consumer-visible deps (dependencies / peerDependencies / optionalDependencies, never devDependencies) — so the reference broke. With skipLibCheck: true (a common setting) their TypeScript silently degraded the affected exports to any; without it, resolution failed outright.

These were fixed one batch at a time — csstype in @wordpress/components, a sweep of missing package deps, and @types/react across many packages — but the question raised in review was the real problem:

Can we enforce this somehow…? As far as I understand, we have no protections against regressions, and new dependencies will be easy to miss in the same way we missed these until now. … How do we expect someone to choose between devDependencies and dependencies?

This isn’t really a hoisted-vs-isolated problem. Moving to an isolated layout (pnpm, or npm install-strategy=linked) surfaces the fully-undeclared phantom deps, but it can’t see the larger share of these: a dependency declared in the wrong place. @types/react in devDependencies resolves perfectly at the author’s isolated build, yet still leaks into the published .d.ts and breaks consumers — and whether it belongs in dependencies or devDependencies hinges on whether it appears in the emitted surface, which neither an isolated install nor a source-level linter (ESLint / eslint-plugin-import only see source) can determine. The reply in that thread“I am exploring the options to fix this via some tool” — is what became dependency-audit: it materializes only the consumer-visible declared deps (production + peer + optional, never dev) and resolves the released surface (both .d.ts and runtime JS) against them, flagging anything undeclared or unresolvable — so this class of bug fails CI before it ships, regardless of how anyone’s node_modules is laid out.