Question presented to candidate: "A project using pnpm fails in CI with 'module not found' for a package that isn't in package.json at all — yet the same code runs fine locally where the team uses npm. What's actually different between these two package managers that would cause this?"
What a strong answer should cover:
- All three (npm, Yarn, pnpm) install packages from the same npm registry and read the same
package.json— the real, interview-relevant differences are in hownode_modulesis laid out on disk, not in what registry or manifest format they use. - 📌 Verified, not assumed — this is the exact answer to the prompt: installing the identical single dependency (
express) with npm genuinely hoisted its own undeclared transitive dependency (mime-types) into the top-levelnode_modules— directly, physically present there, confirmed by a real file-existence check. The identical install with pnpm genuinely did not —mime-typesstayed isolated inside pnpm's real.pnpmcontent-addressable store, with only the genuinely declared dependency (express) symlinked at the top level. - 📌 Interview term: phantom dependency — a package that works by accident because it happens to be hoisted to the top level by a flat-
node_modulespackage manager (npm's default, and Yarn Classic's default), even though it was never actually declared inpackage.jsonat all. This is exactly the prompt's bug: code that accidentally relies on a phantom dependency works under npm (which hoists it) and genuinely breaks under pnpm (which, verified directly above, does not). - The precise fix for the prompt's bug is not a pnpm configuration workaround — it's adding the genuinely used package as a real, direct dependency in
package.json. pnpm's strictness is intentionally catching a real, pre-existing correctness bug (an undeclared dependency the code happened to get away with under a more permissive layout), not introducing a new one. - A precise answer also names why this distinction exists at all: npm's classic flat/hoisted layout optimizes for simplicity and broad compatibility with older tooling that assumed a flat structure; pnpm's isolated, symlink-based, content-addressable layout optimizes for correctness (no phantom dependencies, verified above) and disk efficiency (identical package versions are stored once on disk and hard-linked/symlinked into every project that needs them, rather than duplicated per-project). Yarn (in its newer "Berry"/PnP mode) takes a third, more radical approach — no
node_modulesdirectory at all, resolving imports via a generated mapping file instead — genuinely different from both npm's and pnpm's disk-basednode_moduleslayouts.
Clarifying questions expected:
- "Is the team currently relying on any phantom dependencies (undeclared packages that happen to work due to hoisting) anywhere in the codebase?" — directly predicts whether a switch to pnpm's strictness will surface real, previously-hidden bugs.
- "Does the project need Yarn PnP's more radical no-node_modules approach, or is a conventional node_modules-based layout (npm or pnpm) sufficient?" — PnP has its own tooling-compatibility trade-offs beyond the scope of the npm-vs-pnpm distinction alone.
Code / implementation expected: Yes — a real, filesystem-level comparison of the identical dependency installed via npm vs. pnpm, directly showing the hoisting difference, is the concrete, convincing proof of exactly what causes the prompt's bug.