Problem:
I recently upgraded from Node 16 (which has npm 8) to Node 18 (which has npm 9). package-lock.json
files generated with npm 8 have a lockfileVersion: 2
whereas package-lock.json
files generated with npm 9 have a lockfileVersion: 3
. The differences are greater than that, however, and I’ve run into an issue trying to determine the version of a dependency by inspecting the package-lock.json
.
Here is an example of JSON for a package-lock.json
generated with npm 8:
{
name: 'npm8 package',
lockfileVersion: 2,
packages: {
'': {
name: 'npm8 package',
dependencies: {
dependency1: '^1.2.3',
},
},
"node_modules/dependency1": {
"version": "1.2.8",
"resolved": "https://someurl.com",
"integrity": "sha512-blahblahblah"
},
},
dependencies: {
"dependency1": {
"version": "1.2.8",
"resolved": "https://someurl.com",
"integrity": "sha512-blahblahblah"
}
},
}
Here is an example of JSON for a package-lock.json
generated with npm 9:
{
name: 'npm9 package',
lockfileVersion: 3,
packages: {
'': {
name: 'npm9 package',
dependencies: {
dependency1: '^1.2.3',
},
},
"node_modules/dependency1": {
"version": "1.2.8",
"resolved": "https://someurl.com",
"integrity": "sha512-blahblahblah"
},
}
}
As you can see, a package-lock.json
file generated in npm 9 omits the dependencies
node entirely.
My problem is that I was previously programmatically retrieving the version of an installed dependency by parsing the package-lock.json
JSON and grabbing the version like this: parsedPackageLockJSON.dependencies['dependency1'].version
. Now that does not exist and I’m unclear on how to get it from the packages
node. packages.dependencies['dependency1']
would only get me the version of the dependency from the package.json
, not necessarily the installed version. The version I need appears to be at packages.dependencies['node_modules/dependency1']
, but is it safe to prefix my dependency name with node_modules/
?
ps. I understand I can get the installed version by running npm ls --json
, but I need to do it using the package-lock.json
. npm ls
relies on the existence of the node_modules
directory and, in my case, I’m trying to obtain the dependency version after programmatically cloning a repo. I don’t want to also have to build the codebase so that the node_modules
directory will exist so the npm ls
command will work if the information should be available in the package-lock.json
file.
Solution:
As mentioned in the comments 1, 2, npm ls
supports additional arguments which can be used to produce JSON-formatted output in a schema that works with the code you previously used, and won’t require installation of any dependencies:
-
json
: (Default: false, Type: Boolean) Whether or not to output JSON data, rather than the normal output.
-
package-lock-only
: (Default: false, Type: Boolean) If set to true, the current operation will only use the package-lock.json
, ignoring node_modules
. For update
this means only the package-lock.json
will be updated, instead of checking node_modules
and downloading dependencies. For list
this means the output will be based on the tree described by the package-lock.json
, rather than the contents of node_modules
.
Below is a reproducible example to demonstrate.
Given a directory so-77311802
whose only contents are the following 3 files:
so-77311802 % ls
package-lock.json print_dependency_versions.mjs
package.json
./package.json
:
{
"name": "so-77311802",
"version": "0.1.0",
"type": "module",
"license": "MIT",
"devDependencies": {
"@types/node": "^20",
"typescript": "^5"
}
}
./package-lock.json
:
{
"name": "so-77311802",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "so-77311802",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^20",
"typescript": "^5"
}
},
"node_modules/@types/node": {
"version": "20.8.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
"integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.25.1"
}
},
"node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.25.3",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
"dev": true
}
}
}
./print_dependency_versions.mjs
:
import { exec as execCb } from "node:child_process";
import { promisify } from "node:util";
const exec = promisify(execCb);
const npmLsOutput = JSON.parse(
(await exec("npm ls --json --package-lock-only")).stdout,
);
for (const [name, { version }] of Object.entries(npmLsOutput.dependencies)) {
console.log({ name, version });
}
Using npm ls
with the arguments above produces this result in stdout:
so-77311802 % npm --version
9.8.1
so-77311802 % npm ls --json --package-lock-only
{
"version": "0.1.0",
"name": "so-77311802",
"dependencies": {
"@types/node": {
"version": "20.8.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
"overridden": false
},
"typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"overridden": false
}
}
}
Using Node.js to run the JavaScript module (which uses child_process.exec
) produces this output:
so-77311802 % node --version
v18.18.2
so-77311802 % node print_dependency_versions.mjs
{ name: '@types/node', version: '20.8.6' }
{ name: 'typescript', version: '5.2.2' }