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: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ Each yielded package has:
name: string; // Package name (e.g., "@babel/core")
version: string; // Resolved version (e.g., "7.23.0")
integrity?: string; // Integrity hash (sha512, sha384, sha256, sha1)
resolved?: string; // Download URL
resolved?: string; // Download URL (registry or private)
link?: boolean; // True if this is a workspace symlink
}
```

Expand Down
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]

### 🆕 Added
- **npm lockfileVersion 1 support**: `fromPackageLock` now parses v1 lockfiles by falling back to a new `fromDependenciesTree` generator when the `packages` map is absent. v1 lockfiles use a nested `dependencies` tree instead of the flat `packages` map — `fromDependenciesTree` walks the tree iteratively and yields the same `Dependency` shape. The README already claimed v1 support; the parser now delivers on it.

## [1.5.1] - 2026-03-16

### 🐛 Fixed
Expand Down
9 changes: 8 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFile } from 'node:fs/promises';
import { detectType, Type } from './detect.js';
import {
fromDependenciesTree,
fromPackageLock,
fromPnpmLock,
fromYarnBerryLock,
Expand All @@ -25,7 +26,13 @@ export { Type, detectType };
export { Ok, Err };

// Re-export individual parsers
export { fromPackageLock, fromPnpmLock, fromYarnClassicLock, fromYarnBerryLock };
export {
fromDependenciesTree,
fromPackageLock,
fromPnpmLock,
fromYarnClassicLock,
fromYarnBerryLock
};

// Re-export FlatlockSet class
export { FlatlockSet };
Expand Down
1 change: 1 addition & 0 deletions src/parsers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export {
buildWorkspacePackages as buildNpmWorkspacePackages,
extractWorkspacePaths as extractNpmWorkspacePaths,
fromDependenciesTree,
fromPackageLock,
parseLockfileKey as parseNpmKey
} from './npm.js';
Expand Down
71 changes: 71 additions & 0 deletions src/parsers/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ export function parseLockfileKey(path) {

/**
* Parse npm package-lock.json (v1, v2, v3)
*
* v2/v3 lockfiles use the `packages` map (flat, path-keyed).
* v1 lockfiles use the `dependencies` tree (nested, name-keyed).
* When `packages` is empty or absent and `dependencies` exists,
* falls back to `fromDependenciesTree`.
*
* @param {string | object} input - Lockfile content string or pre-parsed object
* @param {Object} [_options] - Parser options (unused, reserved for future use)
* @returns {Generator<Dependency>}
Expand All @@ -130,6 +136,21 @@ export function* fromPackageLock(input, _options = {}) {
const lockfile = typeof input === 'string' ? JSON.parse(input) : input;
const packages = lockfile.packages || {};

// Check if the packages map has any non-root entries
let hasPackageEntries = false;
for (const path of Object.keys(packages)) {
if (path !== '') {
hasPackageEntries = true;
break;
}
}

// v1 fallback: no packages entries but has dependencies tree
if (!hasPackageEntries && lockfile.dependencies) {
yield* fromDependenciesTree(lockfile);
return;
}

for (const [path, pkg] of Object.entries(packages)) {
// Skip root package
if (path === '') continue;
Expand All @@ -155,6 +176,56 @@ export function* fromPackageLock(input, _options = {}) {
}
}

/**
* Parse npm lockfileVersion 1 dependencies tree.
*
* v1 lockfiles store dependencies as a nested object tree where each key
* is a package name and each value contains { version, resolved, integrity,
* requires, dependencies }. Nested `dependencies` represent version conflicts
* that couldn't be hoisted.
*
* @param {string | object} input - Lockfile content string or pre-parsed object
* @param {Object} [_options] - Parser options (unused, reserved for future use)
* @returns {Generator<Dependency>}
*/
export function* fromDependenciesTree(input, _options = {}) {
const lockfile = typeof input === 'string' ? JSON.parse(input) : input;
const dependencies = lockfile.dependencies;
if (!dependencies) return;

// Iterative depth-first walk to avoid stack overflow on deep trees
/** @type {Array<[string, object]>} */
const stack = [];

// Push in reverse order so first entries are yielded first
const topLevel = Object.entries(dependencies);
for (let i = topLevel.length - 1; i >= 0; i--) {
stack.push(topLevel[i]);
}

while (stack.length > 0) {
const [name, info] = /** @type {[string, any]} */ (stack.pop());
const { version, integrity, resolved } = info;

if (name && version) {
/** @type {Dependency} */
const dep = { name, version };
if (integrity) dep.integrity = integrity;
if (resolved) dep.resolved = resolved;
yield dep;
}

// Push nested dependencies (conflict resolution overrides)
const nested = info.dependencies;
if (nested) {
const entries = Object.entries(nested);
for (let i = entries.length - 1; i >= 0; i--) {
stack.push(entries[i]);
}
}
}
}

/**
* Extract workspace paths from npm lockfile.
*
Expand Down
Loading
Loading