← Blog
Colin

De-phantoming the npm ecosystem

A daily scan of 10,000 high-download npm packages, published as a package extensions database for other tools to use.

We've used Nub's built-in phantom detector to scan the 10,000 most downloaded packages on npm for undeclared dependencies. The resulting database covers 791 packages, including 649 missing from @yarnpkg/extensions, which undergirds the global virtual stores of pnpm v12 and Aube.

The results are published as @nubjs/extensions.

What are package extensions?

Many packages in the npm ecosystem fail to properly declare their dependencies in package.json, especially peer dependencies. Due to flat layout algorithms used by npm and Yarn, these unsound configurations often went undiscovered for years. As pnpm, Nub, and other package managers have moved towards more disk-efficient isolated node_modules layouts, these issues are manifesting in the form of ERR_MODULE_NOT_FOUND errors.

Yarn encountered this problem with Plug'n'Play and worked around it with @yarnpkg/extensions—a hand-maintained list of packages and their associated undeclared dependencies. It was a game of whack-a-mole, but this list grew over time to include a range of widely used packages.

They look like this:

export const packageExtensions = [
  [
    '@nrwl/devkit@*',
    {
      dependencies: {
        tslib: '*',
      },
    },
  ],
  // ...
];

More recently, this package has come to underpin the Aube and pnpm v12 global virtual stores.

Bun 1.4 shipped an opt-in global virtual store, but it ships without extension-based patching at all. Packages with undeclared dependencies can install successfully, only to fail with MODULE_NOT_FOUND at runtime.

The problem

Here's the problem: it's woefully incomplete and minimally maintained. There have been just a handful of updates in the last three years. In our scan of the top 10,000 packages, we found 649 additional packages with undeclared imports beyond the 142 already covered by Yarn's database, bringing the total to 791 packages.

DatabasePackages
@yarnpkg/extensions@2.0.7142
@nubjs/extensions@1.0.4791

Not every undeclared import breaks an install, so the scan classes each package by the worst edge it carries:

What the scan foundPackagesBreaks under a strict layout?
Declaration-file reference only341Yes, at type-check time. The package's .d.ts files import something it never declares, so tsc reports TS2307 inside them; with skipLibCheck the import silently becomes any instead. No unguarded runtime import.
Guarded load only114No error. The try/catch around the import turns the optional feature off.
Undeclared framework peer101Yes. The app already has react, typescript or expo-modules-core, but a strict linker cannot connect the two until the peer is declared.
Forgotten dependency104Yes, when that code path runs. The import is unguarded on the package's main entry graph; 25 have a reproduced failure under Yarn Plug'n'Play.

Every entry ships as an optional peer, which installs nothing on its own; the 25 reproduced cases ship as dependencies.

Every package in the database is below, ranked by weekly downloads. A row lists the imports that package does not declare; hover one to see how it goes undeclared and what the rule adds.

Some rules are scoped to a version range, because the package fixed the phantom in a later release — redux-thunk@<=2.3.0 needs the rule, and redux-thunk 2.4.0 onward declares the peer itself. Those rules stay in the database, since a lockfile pinning an older version still needs them, but the table hides them by default: 95 of the 791 packages are covered only by rules their current release has moved past. Tick the box to see them, and a shown rule names the version it was fixed in.

696 of 791 packages
#PackageWeekly downloadsPhantom dependencies
2esbuild204M
4@babel/parser173M
6vite132M
7@typescript-eslint/types128M
8@eslint/eslintrc93M
9esprima80M
10vitest77M
11playwright-core76M
12gaxios76M
13sharp75M
14es-abstract72M
15typed-array-byte-offset60M
16typed-array-byte-length60M
17@tailwindcss/oxide59M
19@emnapi/core58M
20event-target-shim49M
21@testing-library/jest-dom46M
23eslint-module-utils45M
24eslint-plugin-import44M
25next43M
26jest-validate43M
27unplugin43M
28pg-connection-string42M
30recharts40M
32escodegen40M
Page 1 of 28
Package data from @nubjs/extensions 1.0.4. Weekly downloads for 2026-09-05 to 2026-09-11. 142 of the 791 entries are carried from @yarnpkg/extensions.

The new @nubjs/extensions package serves as a modern, 100%-compatible drop-in replacement.

  • Every Yarn rule is preserved, including its dependency ranges and peer metadata.
  • The export format is unchanged: packageExtensions, an array of [selector, extension] pairs.
  • CommonJS and ESM are supported.

Other tools can adopt this with a one-line change:

import {
  packageExtensions,
} from '@yarnpkg/extensions';
} from '@nubjs/extensions';

Nub itself has incorporated this list into its resolution engine. Nub also statically analyzes source code to identify phantom dependencies on the fly. This phantom dependency finder in Nub is exactly what we used to generate the @nubjs/extensions data set. Our approach is detailed in Unblocking the global virtual store.

The package is updated autonomously via CI.

  • Periodic refreshes of the npm download ranking to identify new or rising packages.
  • Daily scans check current published package versions.
  • Handwritten overrides are hard-coded and can be updated via PR. Some packages contain phantom dependencies that are not identifiable via static analysis.
npm install @nubjs/extensions