From 47296e24ae4786e4d70761f91f0714e7f2833dad Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Mon, 29 Jun 2026 20:08:31 +0530 Subject: [PATCH] docs: recommend install-strategy=linked to catch phantom dependencies (#9678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encourages package authors to develop under `install-strategy=linked` to surface undeclared ("phantom") dependencies before publishing. Under the default hoisted `node_modules`, a package can `import` a dependency it never declared and still resolve it: a transitive dependency hoisted alongside it (or a workspace root's `node_modules`) happens to satisfy the import. That undeclared dependency passes the author's own build silently, then fails for a consumer who installs the package on its own. The linked (isolated) layout exposes only a package's declared dependencies, so such an import can fail at the author's build instead of shipping broken. The change documents this in two places: - The `install-strategy` entry in the config reference now recommends that authors use `--install-strategy=linked` during development, cross-linked to the developers guide. - The Developers guide gains a "Catching undeclared ("phantom") dependencies" section under "Before Publishing", with a note that this is one check — a dependency satisfied by a `devDependency` or a workspace root's `node_modules` can still resolve locally — alongside auditing the dependencies the published package actually uses. This is documentation-only; the config reference text is generated from the `install-strategy` definition description. ## References Closes #9675 (cherry picked from commit fdcfcee5b68fa3e0dce1afaa7f4ac7d32d74b5cd) --- docs/lib/content/using-npm/developers.md | 18 ++++++++++++++++++ tap-snapshots/test/lib/docs.js.test.cjs | 8 ++++++++ .../config/lib/definitions/definitions.js | 8 ++++++++ 3 files changed, 34 insertions(+) diff --git a/docs/lib/content/using-npm/developers.md b/docs/lib/content/using-npm/developers.md index de0cb848c59ff..b1fff0e3894f3 100644 --- a/docs/lib/content/using-npm/developers.md +++ b/docs/lib/content/using-npm/developers.md @@ -171,6 +171,24 @@ to install it locally into the node_modules folder in that other place. Then go into the node-repl, and try using require("my-thing") to bring in your module's main module. +#### Catching undeclared ("phantom") dependencies + +Under the default hoisted `node_modules` layout, your package can `import` a dependency it never declared and still resolve it. +A transitive dependency hoisted alongside it, or your workspace root's `node_modules`, happens to satisfy the `import`. +That undeclared ("phantom") dependency passes your own build silently, then fails for anyone who installs your package on its own. + +We recommend developing your package under [`install-strategy=linked`](/using-npm/config#install-strategy). +The isolated layout only exposes a package's *declared* dependencies, so an `import` of an undeclared package fails for you during development instead of resolving by accident, shipping broken, and failing for your users: + +```bash +npm install --install-strategy=linked +npm test +``` + +> **Note:** This doesn't catch every case. +> A dependency that's still satisfied at your build by a `devDependency` or by your workspace root's `node_modules` can resolve fine for you and still be missing for whoever installs your package. +> So treat it as one check, not a guarantee, alongside auditing the dependencies your published package actually uses. + ### Create a User Account Create a user with the adduser command. diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index a4c1e235ed784..686a10f794521 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -1092,6 +1092,14 @@ place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. +We recommend that package authors use \`--install-strategy=linked\` during +development to catch undeclared ("phantom") dependencies before publishing: +the isolated layout only exposes a package's declared dependencies, so an +\`import\` of a package that was never added to \`package.json\` can fail +instead of resolving by accident and shipping broken. See [Catching +undeclared ("phantom") +dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). + #### \`json\` diff --git a/workspaces/config/lib/definitions/definitions.js b/workspaces/config/lib/definitions/definitions.js index ed1a25bf93a34..acf2d3e93a48e 100644 --- a/workspaces/config/lib/definitions/definitions.js +++ b/workspaces/config/lib/definitions/definitions.js @@ -1209,6 +1209,14 @@ const definitions = { nested: (formerly --legacy-bundling) install in place, no hoisting. shallow (formerly --global-style) only install direct deps at top-level. linked: install in node_modules/.store, link in place, unhoisted. + + We recommend that package authors use \`--install-strategy=linked\` + during development to catch undeclared ("phantom") dependencies before + publishing: the isolated layout only exposes a package's declared + dependencies, so an \`import\` of a package that was never added to + \`package.json\` can fail instead of resolving by accident and shipping + broken. See [Catching undeclared ("phantom") + dependencies](/using-npm/developers#catching-undeclared-phantom-dependencies). `, flatten, }),