Lock Your Build Environment Before It Locks You Out
Two package.json fields that quietly cause most Node dependency drift, and why I stopped letting either one float.
The pipeline from the last few posts assumes something it never actually checks: that everyone building this project has the same Node version, installing the same package versions, every single time.
That assumption breaks on its own. Nothing enforces it unless you write it down.
The Node version nobody agreed on
Two developers on the same repo, one on Node 18, one on Node 24, both run npm install. Nothing warns either of them anything is wrong.
Weeks later, a dependency behaves differently between their machines, and nobody remembers why. The bug isn't in the code. It's in two people never having run the same runtime in the first place.
package.json has a field for exactly this, and almost nobody uses it:
{
"engines": {
"node": "22.22.0"
}
}Combine it with .npmrc:
engine-strict=trueNow npm install refuses to run on the wrong version instead of silently succeeding. The repo enforces the rule instead of a wiki page nobody reads.
This is the same idea behind treating CI as the source of truth instead of a developer's laptop, just one layer lower: the version of Node itself is now something the repo checks, not something everyone is trusted to remember.
The caret that lets your dependencies drift
Open any package.json and you'll see version numbers prefixed with ^.
{
"dependencies": {
"astro": "^5.0.0"
}
}That caret means: install any version from 5.0.0 up to, but not including, 6.0.0. Every npm install after that is free to pull a newer minor or patch release, even though nobody touched this file.
Most of the time that's harmless. Sometimes it's the entire reason a build passed yesterday and fails today, with a diff that shows zero code changes.
"astro": "^5.0.0"Free to install 5.1.0, 5.4.2, anything under 6.0.0, on any install.
"astro": "5.0.0"Installs that exact version, every time, on every machine.
Deleting three characters, the caret, is the entire fix. No tool required, no extra config, just a version number that means exactly what it says.
Why I stopped trusting the caret at all
The usual defense of the caret is that patch and minor releases are supposed to be safe under semver. In theory, a 5.1.0 shouldn't break anything a 5.0.0 relied on.
In practice, "supposed to be safe" is a promise every dependency occasionally breaks, and CI is where you find out. A pinned version doesn't stop a package from ever having bugs. It stops your build from picking up a new one on a Tuesday you didn't ask for.
This costs you nothing you were actually using. Upgrading is still one command away, npm install astro@5.1.0, except now it's a decision you made on purpose, with a diff in your PR, instead of something that happened silently between two builds three weeks apart.
The Essentials
- Nothing enforces a shared Node version unless you write it down. Two developers on two Node versions can both run
npm installwithout a single warning. enginesplusengine-strict=trueturns that into a hard rule. The wrong Node version fails the install instead of silently succeeding.- Both fixes are the same underlying idea at different layers. Lock down what your build environment is allowed to vary on, the runtime and the dependencies both.
Further Reading and Watching
Keep reading