Package Management with PNPM
See also why-does-npm-install-rewrite-package-lock-json
Semver
The semver specification explains how to use semantic versioning though you can probably skip to the npm docs.
As you probably know the numbers are in the form major.minor.patch
. If you don't mind which patch release you have as long as it is the specified major and minor version you can use the ~
prefix. Similarly, to allow any minor version use ^
.
Walkthrough
Inital Setup
pnpm init
pnpm add express
The package.json
will contain (at time of writing):
"express": `"^4.18.2"`
A pnpm-lock.yaml
is also created:
specifiers:
express: ^4.18.2
dependencies:
express: 4.18.2
express -> '.pnpm/express@4.18.2/node_modules/express'/
Using pnpm install
Giving it a first run without changing anything produces:
$ pnpm install
Lockfile is up to date, resolution step is skipped
Already up to date
Done in 653ms
Now if I change package.json
to be exactly v4.16.0
we shall see an update to pnpm-lock.yaml
specifiers:
express: 4.16.0
dependencies:
express: 4.16.0
Adding the patch wildcard ~4.16.0
and running pnpm install
again gives:
specifiers:
express: ~4.16.0
dependencies:
express: 4.16.0
Note that the install version did not change. If I delete the node_modules/
directory and reinstall, still no change.
Ok, now try updating the minor version in package.json
to ~4.17.0
.
specifiers:
express: ~4.17.0
dependencies:
express: 4.17.3
This time it did update the dependency and installed the latest patch version but did install the exact major and minor version. If you think about what the ~
means then this is expected.
The specifiers
section in the lock file is just what we specify as the dependency in the package.json
file. The dependencies
section in the lock file should reflect the version that is installed, or will be installed.
If I delete the node_modules/
folder and pnpm install
again then we still have 4.17.3
.
Explanation
What confuses a lot of people about pnpm install
/npm install
is how the lock-file works with the semver specifier:
The installed version listed as a dependency in the lockfile must be compatible with the version specified in the package file.
If it is compatible, no changes will be made.
If it is incompatible, then the latest compatible version will be installed.
Perhaps because sometimes it seems to install the latest version, and not othertimes, the behaviour is not clear. To state this again, changes will only be made when there is an incompatibility between the packge version and lockfile version. The lockfile dependency never has the ~
or ^
wildcards because only one version is actually installed and that's what the lockfile is supposed to track.
Using --frozen-lockfile
in a CI environment
The docs for pnpm install
describe how the install will fail if the lockfile is out of sync or needs updating.
Changing the package.json
back to ~4.16.0
and then doing the install:
$ pnpm install --frozen-lockfile
Lockfile is up to date, resolution step is skipped
ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with package.json
Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile"
In fact, even if I specify the installed version exactly 4.17.3
, because it differs to the specifier ~4.17.0
, then it will err. The package.json
and pnpm-lock.yaml
are out of sync even though the version are compatible.
Finally I will make our package compatible with the latest version that was installed with the first pnpm add express
command. To do this I use the minor version wildcard ^4.0.0
and unfreeze the lockfile with pnpm install --no-frozen-lockfile
.
specifiers:
express: ^4.0.0
dependencies:
express: 4.17.3
While the specifier is updated to match the package file, the version is not chaged; it is compatible.
Running pnpm install --frozen-lockfile
will work again, but not update the installed version.
Conclusion
In a normal environment the lockfile will determine the exact version installed unless it is not compatible with the package file, in which case it will install the lastest version specified by the package file.
In a CI environment the lockfile will not by default be updated and will need to be compatible with the package file for installs to occur.
If you want the latest version specified pnpm update
will do the update to the lastest compatible version given in the package file.
Disclaimer
I've tested out everything here but it is complex and I have limited experience using pnpm in a real CI environment.