Skip to content

Commit 234e878

Browse files
indexzeroclaude
andauthored
fix(lockfiles) detect v1 lockfiles and shrinkwraps without lockfileVersion (#27)
v1 lockfiles and npm-shrinkwrap.json files often omit the lockfileVersion field entirely. tryParseNpm required typeof lockfileVersion === 'number', so these files failed content-based detection. The parser (fromPackageLock) already handled them via the fromDependenciesTree fallback — only routing in detectType was broken. The new heuristic checks for a dependencies tree with object values (which distinguishes lockfiles from package.json where values are version-range strings). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c01239f commit 234e878

3 files changed

Lines changed: 88 additions & 2 deletions

File tree

doc/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### 🐛 Fixed
11+
- **v1 lockfile detection without `lockfileVersion`**: `detectType` now recognizes v1 lockfiles and shrinkwrap files that omit the `lockfileVersion` field. Previously these files failed content-based detection with "Unable to detect lockfile type." The parser already handled them correctly — only routing was broken.
12+
1013
## [1.6.0] - 2026-04-14
1114

1215
### 🆕 Added

src/detect.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,19 @@ export const Type = Object.freeze({
2424
function tryParseNpm(content) {
2525
try {
2626
const parsed = JSON.parse(content);
27-
// Must have lockfileVersion as a number at root level
28-
return typeof parsed.lockfileVersion === 'number';
27+
// v2/v3: lockfileVersion as a number at root level
28+
if (typeof parsed.lockfileVersion === 'number') return true;
29+
// v1 lockfiles and shrinkwraps often omit lockfileVersion entirely.
30+
// Detect them by checking for a dependencies tree with object values
31+
// (package.json dependencies are version-range strings, not objects).
32+
if (parsed.dependencies && typeof parsed.dependencies === 'object') {
33+
for (const value of Object.values(parsed.dependencies)) {
34+
if (value && typeof value === 'object' && 'version' in value) {
35+
return true;
36+
}
37+
}
38+
}
39+
return false;
2940
} catch {
3041
return false;
3142
}

test/lockfile.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,52 @@ describe('flatlock', () => {
3838
assert.equal(type, flatlock.Type.NPM);
3939
});
4040

41+
test('detects npm v1 lockfile without lockfileVersion', () => {
42+
const content = JSON.stringify({
43+
name: 'my-project',
44+
version: '1.0.0',
45+
dependencies: {
46+
lodash: {
47+
version: '4.17.21',
48+
resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
49+
integrity: 'sha512-v2kDE=='
50+
}
51+
}
52+
});
53+
const type = flatlock.detectType({ content });
54+
assert.equal(type, flatlock.Type.NPM);
55+
});
56+
57+
test('detects npm shrinkwrap without lockfileVersion', () => {
58+
const content = JSON.stringify({
59+
name: 'my-project',
60+
version: '1.0.0',
61+
dependencies: {
62+
express: {
63+
version: '4.18.0',
64+
resolved: 'https://registry.npmjs.org/express/-/express-4.18.0.tgz'
65+
}
66+
}
67+
});
68+
const type = flatlock.detectType({ path: 'npm-shrinkwrap.json', content });
69+
assert.equal(type, flatlock.Type.NPM);
70+
});
71+
72+
test('does not detect package.json as npm lockfile', () => {
73+
// package.json has string dependency values, not objects
74+
const content = JSON.stringify({
75+
name: 'my-project',
76+
version: '1.0.0',
77+
dependencies: {
78+
lodash: '^4.17.21',
79+
express: '~4.18.0'
80+
}
81+
});
82+
assert.throws(() => {
83+
flatlock.detectType({ content });
84+
}, /Unable to detect lockfile type/);
85+
});
86+
4187
test('detects pnpm from content only (no path)', () => {
4288
const content = 'lockfileVersion: 6.0\npackages:\n /lodash@4.17.21:';
4389
const type = flatlock.detectType({ content });
@@ -169,6 +215,32 @@ packages:
169215
assert.ok(deps.length > 0, 'Should have dependencies');
170216
});
171217

218+
test('parses v1 lockfile without lockfileVersion via fromString', () => {
219+
const content = JSON.stringify({
220+
name: 'my-project',
221+
version: '1.0.0',
222+
dependencies: {
223+
lodash: {
224+
version: '4.17.21',
225+
resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
226+
integrity: 'sha512-v2kDE=='
227+
},
228+
express: {
229+
version: '4.18.0',
230+
resolved: 'https://registry.npmjs.org/express/-/express-4.18.0.tgz'
231+
}
232+
}
233+
});
234+
235+
// Should auto-detect as npm and parse via dependencies tree fallback
236+
const deps = [...flatlock.fromString(content)];
237+
assert.equal(deps.length, 2);
238+
assert.equal(deps[0].name, 'lodash');
239+
assert.equal(deps[0].version, '4.17.21');
240+
assert.equal(deps[1].name, 'express');
241+
assert.equal(deps[1].version, '4.18.0');
242+
});
243+
172244
test('fromPackageLock parses directly', () => {
173245
const content = JSON.stringify({
174246
lockfileVersion: 2,

0 commit comments

Comments
 (0)