diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 72c7ee1..240a7fa 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -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 diff --git a/src/detect.js b/src/detect.js index f03b4d9..22ed426 100644 --- a/src/detect.js +++ b/src/detect.js @@ -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; } diff --git a/test/lockfile.test.js b/test/lockfile.test.js index 92cbc10..ce953cc 100644 --- a/test/lockfile.test.js +++ b/test/lockfile.test.js @@ -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 }); @@ -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,