Skip to content

Support pnpm lockfileVersion 6.0 and 9.0 in parseLockFile#1165

Open
astegmaier wants to merge 15 commits into
microsoft:mainfrom
astegmaier:ansteg/fix-support-newer-pnpm-lockfiles
Open

Support pnpm lockfileVersion 6.0 and 9.0 in parseLockFile#1165
astegmaier wants to merge 15 commits into
microsoft:mainfrom
astegmaier:ansteg/fix-support-newer-pnpm-lockfiles

Conversation

@astegmaier

Copy link
Copy Markdown

Why

parseLockFile (in workspace-tools) normalizes yarn, npm, and pnpm lockfiles into one common ParsedLock shape so the rest of the toolchain is package-manager-agnostic. It's used by lage's content hasher (cache-key computation) and by downstream tools such as dependency-graph visualizers.

The problem: for pnpm lockfileVersion 6.0 (pnpm 8) and 9.0 (pnpm 9+) the parser silently produces garbage. pnpm 6.0 changed dependency-path keys from /(name)/(version) to name@version (with a leading / and trailing (peer)/(patch_hash=…) suffixes), and 9.0 additionally moved dependency edges out of packages: into a new snapshots: section. The existing code only understood the legacy split("/") format, so every modern lockfile key mis-parses and the edges are never read at all.

This PR teaches parseLockFile to read pnpm 6.0/9.0 lockfiles, while leaving the legacy (< 6.0) codepath untouched.

What it looks like today vs. with the fix

TodayparseLockFile(...).object on a lockfileVersion: '9.0' lockfile:

// 33 keys, all garbage. Names are mangled, versions are `undefined`, and because 9.0
// moved edges into `snapshots:` (which the old code never reads) every entry has no deps:
{
  "[email protected]@undefined":        { "version": undefined, "dependencies": undefined },
  "[email protected]@undefined":           { "version": undefined, "dependencies": undefined },
  "[email protected]@undefined": { "version": undefined, "dependencies": undefined }
  //
}

A consumer then can't resolve a single dependency edge.

With the fix — one snapshot key exercises most of the edge cases at once. The raw key
@testing-library/[email protected](@testing-library/[email protected])([email protected]([email protected]))([email protected]) parses to:

"@testing-library/[email protected]": {        // scoped name kept; nested/multiple peer suffixes stripped from the KEY
  "version": "16.0.1",
  "dependencies": {
    "@babel/runtime": "7.29.7",
    "@testing-library/dom": "10.4.1",
    "react": "18.3.1",
    "react-dom": "18.3.1"                  // peer suffix also stripped from the VALUE (was "18.3.1([email protected])")
  }
}

The remaining cases (each covered by a real fixture + test):

// patched dep — `[email protected](patch_hash=…)` -> the patch_hash suffix is stripped:
"[email protected]": { "version": "3.0.1", "dependencies": { "is-number": "6.0.0" } },

// git / non-semver dep — version is kept verbatim, name taken from the entry's `name` field:
"is-positive@https://codeload.github.com/.../<sha>": { "version": "https://codeload.github.com/.../<sha>" },

// optionalDependencies are merged into `dependencies` (matches the yarn parser) — note `fsevents`:
"[email protected]": { "version": "3.6.0", "dependencies": { "anymatch":"3.1.3", …, "fsevents":"2.3.3" } },

// workspace packages from `importers:` are now collected, keyed by path; `link:` refs kept verbatim:
"packages/package-a": { "version": "packages/package-a",
  "dependencies": { "package-b": "link:../package-b", "react": "19.2.4", "react-dom": "19.2.4" } }

No regression for < 6.0 lockfiles

The entire change is gated behind a single version check, and the pre-existing < 6 branch is byte-for-byte unchanged. From packages/workspace-tools/src/lockfile/parsePnpmLock.ts:

const lockfileVersion = Number(yaml?.lockfileVersion ?? 0);

if (lockfileVersion >= 6) {
  // NEW: `name@version` keys, `snapshots:` edges, and `importers:` collection
} else if (yaml?.packages) {
  // LEGACY (< 6.0): original `split("/")` parsing — unchanged
}

So pnpm 5.x lockfiles take exactly the same path they did before, and yarn/npm lockfiles are dispatched elsewhere in parseLockFile and are not touched at all. Reviewers can verify regression-safety by confirming the else if block matches main.

Testing

  • Unit tests against real, generated pnpm 6.0 and 9.0 fixtures (no hand-authored YAML). A describe.each(["basic-pnpm-6", "basic-pnpm-9"]) block runs the edge-case assertions against both the 6.0 (packages:) and 9.0 (snapshots:) codepaths; importer assertions run against a monorepo fixture. workspace-tools: 345 passing.
  • End-to-end, with a real scenario: a dependency-graph visualization tool that reads a repo's lockfile through parseLockFile was run against a large real-world pnpm monorepo. Before this change it emitted thousands of … not found in lockfile warnings and a near-empty graph; after, its graph matches the one produced from the equivalent yarn version of the same repo (same tool) after normalization.
  • Consumers in this repo (the content hasher and its external-dependency resolver) build and pass their tests against the updated API.

Notes for reviewers

  • One-time cache invalidation for pnpm 6/9 repos. lage's hasher previously got garbage from this parser and so hashed only top-level ranges; it now resolves transitive external deps correctly. This changes target hashes once on upgrade (a one-time cache rebuild) — intended, not a bug.
  • PnpmLockFile.packages is now optional (9.0 lockfiles have no packages edges). This is a minor source-level type-narrowing for any TS consumer that reads lock.packages directly; no in-repo consumer does.
  • Out of scope / possible follow-up: the hasher's pre-existing resolveExternalDependencies uses an O(n²) queue-dedup scan that this fix now exercises at scale on large pnpm monorepos. Not changed here; a Set-based membership check would make it linear.

Change type

minor (purely additive: modern pnpm lockfiles that previously failed now work; existing behavior is unchanged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant