import/no-extraneous-dependencies

Disallow importing a package that package.json does not list

Also called import-x/no-extraneous-dependencies

A package can end up in node_modules only because another package depends on it. Importing it works on your machine until that other package drops or upgrades it. It also fails for anyone who installs your package, because nothing tells the package manager to install it.

The rule reads the closest package.json above the file. A package is fine if it's in dependencies, devDependencies, optionalDependencies, peerDependencies or bundledDependencies. The rule checks import, export ... from, import() and require(). It skips relative paths, Node.js builtins, import type and packages that aren't installed.

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"import/no-extraneous-dependencies" = "error" # or "warn"

Every option, set to its default. The ones that are commented out are unset by default.

bunfig.toml
[lint.rules]
"import/no-extraneous-dependencies" = ["error", {
  devDependencies = true,
  optionalDependencies = true,
  peerDependencies = true,
  bundledDependencies = true,
  # packageDir = <string or list>,
  includeInternal = false,
  includeTypes = false,
}]

Examples#

Here ms is installed, but only express is listed. Reported:

package.json
{
  "name": "shop",
  "dependencies": { "express": "^5.0.0" }
}
node_modules/ms/package.json
{ "name": "ms", "main": "index.js" }
node_modules/ms/index.js
module.exports = text => Number.parseInt(text) * 1000;
example.js1 problem
import express from "express";
import ms from "ms";

Not reported, with the same package.json:

package.json
{
  "name": "shop",
  "dependencies": { "express": "^5.0.0" }
}
example.jsNo problems
import express from "express";
import { join } from "node:path";
import { cart } from "./cart";

Options#

OptionDefaultWhat it does
devDependenciestruefalse reports imports of packages that are only in devDependencies. An array of globs lists the files that can import them.
optionalDependenciestrueThe same for optionalDependencies.
peerDependenciestrueThe same for peerDependencies.
bundledDependenciestrueThe same for bundledDependencies.
packageDirnoneA directory, or an array of directories, to read package.json from instead of the closest one. In a monorepo, list the package and the root.
includeInternalfalseAlso check names that resolve to a file in your own project.
includeTypesfalseAlso check import type.

Globs and packageDir are relative to the directory where bun lint runs.

Most projects allow devDependencies in tests and scripts only:

bunfig.toml
[lint.rules]
"import/no-extraneous-dependencies" = ["error", {
  devDependencies = ["test/**", "scripts/**"],
}]

With that config, code that ships can't import a package that's missing in production:

package.json
{
  "name": "shop",
  "devDependencies": { "ms": "^2.0.0" }
}
node_modules/ms/package.json
{ "name": "ms", "main": "index.js" }
node_modules/ms/index.js
module.exports = text => Number.parseInt(text) * 1000;
src/cart.jswith { "devDependencies": ["test/**", "scripts/**"] }1 problem
import ms from "ms";