Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### 🐛 Fixed
- **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.

## [1.6.0] - 2026-04-14

### 🆕 Added
Expand Down
15 changes: 13 additions & 2 deletions src/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,19 @@ export const Type = Object.freeze({
function tryParseNpm(content) {
try {
const parsed = JSON.parse(content);
// Must have lockfileVersion as a number at root level
return typeof parsed.lockfileVersion === 'number';
// v2/v3: lockfileVersion as a number at root level
if (typeof parsed.lockfileVersion === 'number') return true;
// v1 lockfiles and shrinkwraps often omit lockfileVersion entirely.
// Detect them by checking for a dependencies tree with object values
// (package.json dependencies are version-range strings, not objects).
if (parsed.dependencies && typeof parsed.dependencies === 'object') {
for (const value of Object.values(parsed.dependencies)) {
if (value && typeof value === 'object' && 'version' in value) {
return true;
}
}
}
return false;
} catch {
return false;
}
Expand Down
72 changes: 72 additions & 0 deletions test/lockfile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,52 @@ describe('flatlock', () => {
assert.equal(type, flatlock.Type.NPM);
});

test('detects npm v1 lockfile without lockfileVersion', () => {
const content = JSON.stringify({
name: 'my-project',
version: '1.0.0',
dependencies: {
lodash: {
version: '4.17.21',
resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
integrity: 'sha512-v2kDE=='
}
}
});
const type = flatlock.detectType({ content });
assert.equal(type, flatlock.Type.NPM);
});

test('detects npm shrinkwrap without lockfileVersion', () => {
const content = JSON.stringify({
name: 'my-project',
version: '1.0.0',
dependencies: {
express: {
version: '4.18.0',
resolved: 'https://registry.npmjs.org/express/-/express-4.18.0.tgz'
}
}
});
const type = flatlock.detectType({ path: 'npm-shrinkwrap.json', content });
assert.equal(type, flatlock.Type.NPM);
});

test('does not detect package.json as npm lockfile', () => {
// package.json has string dependency values, not objects
const content = JSON.stringify({
name: 'my-project',
version: '1.0.0',
dependencies: {
lodash: '^4.17.21',
express: '~4.18.0'
}
});
assert.throws(() => {
flatlock.detectType({ content });
}, /Unable to detect lockfile type/);
});

test('detects pnpm from content only (no path)', () => {
const content = 'lockfileVersion: 6.0\npackages:\n /lodash@4.17.21:';
const type = flatlock.detectType({ content });
Expand Down Expand Up @@ -169,6 +215,32 @@ packages:
assert.ok(deps.length > 0, 'Should have dependencies');
});

test('parses v1 lockfile without lockfileVersion via fromString', () => {
const content = JSON.stringify({
name: 'my-project',
version: '1.0.0',
dependencies: {
lodash: {
version: '4.17.21',
resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
integrity: 'sha512-v2kDE=='
},
express: {
version: '4.18.0',
resolved: 'https://registry.npmjs.org/express/-/express-4.18.0.tgz'
}
}
});

// Should auto-detect as npm and parse via dependencies tree fallback
const deps = [...flatlock.fromString(content)];
assert.equal(deps.length, 2);
assert.equal(deps[0].name, 'lodash');
assert.equal(deps[0].version, '4.17.21');
assert.equal(deps[1].name, 'express');
assert.equal(deps[1].version, '4.18.0');
});

test('fromPackageLock parses directly', () => {
const content = JSON.stringify({
lockfileVersion: 2,
Expand Down
Loading