Problem
For lockfile v2/v3, the npm scanner currently treats every package whose key contains exactly one node_modules/ segment as direct:
func isDirectFromKey(key string) bool {
return strings.Count(key, "node_modules/") == 1
}
That describes the package's physical install depth, not whether the root project declared it. npm hoists transitive dependencies to the top-level node_modules whenever it can, so a transitive package commonly has a key such as node_modules/is-number and is incorrectly emitted with direct_dependency=true.
Reproduction
With npm 11.19.0:
npm init -y
npm install --package-lock-only --ignore-scripts is-odd@3.0.1
bumblebee scan --profile project --root "$PWD" --ecosystem npm
The generated lockfile has:
{
"packages": {
"": { "dependencies": { "is-odd": "^3.0.1" } },
"node_modules/is-number": { "version": "6.0.0" },
"node_modules/is-odd": { "version": "3.0.1" }
}
}
is-odd declares is-number; the root project does not. Current Bumblebee output nevertheless reports:
{"package_name":"is-number","version":"6.0.0","direct_dependency":true}
{"package_name":"is-odd","version":"3.0.1","direct_dependency":true}
This can overstate direct exposure across ordinary npm projects.
Expected behavior
For a normal package lock, directness should come from the root package descriptor at packages[""] (dependencies, devDependencies, optionalDependencies, and peerDependencies), together with a top-level package key. Physical hoisting alone should not make a package direct.
For npm's hidden node_modules/.package-lock.json, which normally omits the root project descriptor, direct_dependency should remain absent/unknown rather than being inferred from install depth.
The npm documentation describes packages as a location map and notes that the root project normally uses the empty-string key: https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json#packages. npm's default installation strategy hoists dependencies to the highest possible level, so location depth is not dependency-graph depth.
Problem
For lockfile v2/v3, the npm scanner currently treats every package whose key contains exactly one
node_modules/segment as direct:That describes the package's physical install depth, not whether the root project declared it. npm hoists transitive dependencies to the top-level
node_moduleswhenever it can, so a transitive package commonly has a key such asnode_modules/is-numberand is incorrectly emitted withdirect_dependency=true.Reproduction
With npm 11.19.0:
npm init -y npm install --package-lock-only --ignore-scripts is-odd@3.0.1 bumblebee scan --profile project --root "$PWD" --ecosystem npmThe generated lockfile has:
{ "packages": { "": { "dependencies": { "is-odd": "^3.0.1" } }, "node_modules/is-number": { "version": "6.0.0" }, "node_modules/is-odd": { "version": "3.0.1" } } }is-odddeclaresis-number; the root project does not. Current Bumblebee output nevertheless reports:{"package_name":"is-number","version":"6.0.0","direct_dependency":true} {"package_name":"is-odd","version":"3.0.1","direct_dependency":true}This can overstate direct exposure across ordinary npm projects.
Expected behavior
For a normal package lock, directness should come from the root package descriptor at
packages[""](dependencies,devDependencies,optionalDependencies, andpeerDependencies), together with a top-level package key. Physical hoisting alone should not make a package direct.For npm's hidden
node_modules/.package-lock.json, which normally omits the root project descriptor,direct_dependencyshould remain absent/unknown rather than being inferred from install depth.The npm documentation describes
packagesas a location map and notes that the root project normally uses the empty-string key: https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json#packages. npm's default installation strategy hoists dependencies to the highest possible level, so location depth is not dependency-graph depth.