Catch dependencies that quietly raise your project's minimum Node version.
You declare engines.node in package.json (say >=20) and your team runs the
latest LTS (say v24). A dependency upgrade — direct or transitive — starts
requiring engines.node >= 22 in the lockfile. Nothing flags it: engine-strict
only checks the Node version of the developer running the install, not the range
your project promises to support. check-my-engine reads the lockfile, finds every
package whose engines.node no longer covers your declared range, and points you
at the newest version of each that still fits.
npm (v9+, lockfileVersion: 3 —
package-lock.jsonornpm-shrinkwrap.json) and pnpm (9+, lockfileVersion: '9.0') are supported at this time.
A dependency's engines.node is compared against your project's
engines.node. Internally the covers check is
semver.subset(projectRange, depRange). A non-covering dep is classified by
whether your range's minimum Node (the floor) is still supported:
- violation — the dep doesn't even support your floor (it needs a newer
Node than your project's minimum). Fails the audit (exit
1). - warning — the dep supports your floor but not every higher version your
range permits. This is the common "even-major / LTS-only" pattern: a dep that
declares
^20 || ^22 || >=24excludes the non-LTS21.x/23.xlines that>=20technically allows. Reported, but does not fail the audit.
project engines.node |
dep engines.node |
result |
|---|---|---|
>=20 |
>=18 |
ok (covers everything ≥20) |
>=20 |
>=20 |
ok |
>=20 |
>=22 |
violation (floor 20 broken) |
>=20 |
^20 || ^22 || >=24 |
warning (floor 20 ok; 21/23 excluded) |
>=20 |
(none) / * |
ok (no constraint) |
To silence a warning, tighten engines.node to match what you actually support
(e.g. ^20 || ^22 || >=24) or add the package to ignore.
npm install --save-dev check-my-enginenpx check-my-engine # `check` is the default command
npx check-my-engine checkExample output:
✗ 2 dependencies require a newer Node than your project allows (>=20):
needs-22@3.0.0 needs node >=22
-> newest in-range version: needs-22@2.4.1
child-needs-24@2.5.0 needs node >=24
-> no published version supports >=20| Flag | Description |
|---|---|
-C, --cwd <dir> |
Project directory (default: current). |
--node <range> |
Override or supply the project node range. |
--json |
Machine-readable output. |
--prod |
Skip devDependencies (npm only; no-op for pnpm). |
--ignore <pkg> |
Ignore name or name@version (repeatable). |
--no-suggest |
Skip registry lookups for in-range versions (offline). |
--allow-prerelease |
Consider prerelease versions when suggesting. |
--fallback-to-node-modules |
Read engines from node_modules when the lockfile omits them (npm only; no-op for pnpm). |
--no-cache |
Skip the on-disk registry cache (always hit the network). |
Suggestion lookups hit the npm registry once per package name. Responses are
cached on disk so repeated runs — postinstall, CI, watch loops — stay fast and
avoid registry rate limits. The cache is best-effort: a corrupt or unwritable
cache degrades to a normal network fetch, never an error.
| Setting | Default | Meaning |
|---|---|---|
CHECK_MY_ENGINE_CACHE_DIR |
$XDG_CACHE_HOME/check-my-engine or ~/.cache/check-my-engine |
Cache directory. |
CHECK_MY_ENGINE_CACHE_TTL |
3600 (seconds) |
How long a cached packument stays fresh. |
--no-cache |
cache on | Bypass the cache for one run. |
For development, set NODE_DEBUG=check-my-engine to trace registry traffic on
stderr — each network fetch, registry hit, and cache hit. It is silent unless
the namespace is enabled, so end-user runs (including postinstall) print
nothing, and the trace never touches stdout (safe alongside --json).
$ NODE_DEBUG=check-my-engine npx check-my-engine
check-my-engine registry fetch: https://registry.npmjs.org/needs-22
check-my-engine registry hit: needs-22 (200, 12 versions)
check-my-engine cache hit: needs-22Wire the audit into npm's postinstall so an install that brings in an
incompatible dependency fails loudly:
npx check-my-engine initThis adds (idempotently, preserving any existing postinstall):
{
"scripts": {
"postinstall": "check-my-engine check"
}
}After a full npm install / npm ci, the audit runs and exits non-zero if an
installed dependency's engines.node doesn't cover your project range, so the
install reports an error:
✗ 1 dependency requires a newer Node than your project allows (>=2):
is-odd@3.0.1 needs node >=4
-> newest in-range version: is-odd@2.0.0It only reports — there's no rollback. The files npm wrote are left in place;
fix it yourself by pinning an in-range version, widening engines.node, or
adding the package to ignore.
Heads up: npm runs
postinstallon a fullnpm install/npm ci, but not for a targetednpm install <pkg>/npm add <pkg>/npm update. Use the shell hook below to cover those, and/or runcheck-my-engine checkin CI on the committed lockfile (npx check-my-engine checkexits non-zero on any violation).
Because npm skips postinstall for npm install <pkg>, add a small shell
function that runs the audit after every dependency-changing npm command —
including targeted installs:
# add to ~/.zshrc or ~/.bashrc (bash and zsh)
eval "$(check-my-engine shell-hook)"You keep using npm exactly as before; the wrapper calls the real binary via
command npm, then runs the audit and fails with a non-zero exit on a
violation:
$ npm install is-odd
added 2 packages in 281ms
✗ 1 dependency requires a newer Node than your project allows (>=2):
is-odd@3.0.1 needs node >=4
-> newest in-range version: is-odd@2.0.0- Report-only, like
init— it never modifies your files. - Only dependency-changing subcommands are audited; everything else
(
npm run,npm test, …) passes straight through. Global installs (-g) and directories without apackage.jsonare skipped. - Prefers
./node_modules/.bin/check-my-engine; setCHECK_MY_ENGINE_BINto override the binary used.
Settings come from .check-my-engine.json or a checkMyEngine key in
package.json (the rc file wins). CLI flags override config.
{
"ignore": ["fsevents", "some-pkg@1.2.3"],
"node": ">=20",
"includeDev": true,
"allowPrerelease": false
}| Key | Type | Default | Meaning |
|---|---|---|---|
ignore |
string[] |
[] |
Packages to skip (name or name@version). |
node |
string |
— | Fallback/override range when no engines.node. |
includeDev |
boolean |
true |
Audit devDependencies (--prod overrides). |
allowPrerelease |
boolean |
false |
Allow prereleases when suggesting in-range versions. |
| Code | Meaning |
|---|---|
0 |
No violations (warnings may still be printed). |
1 |
One or more violations found. |
2 |
Usage/config error (no lockfile, or no project range and no --node). |
--json output includes both violations and warnings arrays; ok is
true when there are no violations (warnings don't affect it).
flowchart LR
pkg["package.json engines.node"] --> core[audit core]
lock["package-lock.json packages map"] --> core
core --> viol{"covers()?"}
viol -->|no| reg["registry: newest in-range version"]
reg --> report[report + exit code]
viol -->|yes| report
- Project range comes from
engines.nodeinpackage.json(or--node). - The graph and each dep's engines come from the npm v3 lockfile
packagesmap (with an optionalnode_modulesfallback), or the pnpmpnpm-lock.yamlv9packagesmap. - Suggestions come from the registry's abbreviated metadata
(
application/vnd.npm.install-v1+json), which includes per-versionengines.
- npm
package-lock.json/npm-shrinkwrap.jsonv3 (npm v9+) and pnpmpnpm-lock.yamlv9.0 (pnpm v9/v10/v11). yarn/bun not yet supported. - For pnpm,
--prodand--fallback-to-node-modulesare no-ops: the lockfile has no per-package dev flag and already carries engines. - Single-package projects (monorepo/workspace graphs not yet handled).
- Only the
nodeengine is considered.
MIT