Skip to content

Support pnpm 2-part "env lockfile" pnpm-lock.yaml files in parseLockFile - #1197

Merged
Elizabeth Craig (ecraig12345) merged 2 commits into
microsoft:mainfrom
astegmaier:support-pnpm-env-lockfile
Aug 5, 2026
Merged

Support pnpm 2-part "env lockfile" pnpm-lock.yaml files in parseLockFile#1197
Elizabeth Craig (ecraig12345) merged 2 commits into
microsoft:mainfrom
astegmaier:support-pnpm-env-lockfile

Conversation

@astegmaier

Copy link
Copy Markdown
Contributor

Why

Follow-up to #1165 (which taught parseLockFile to read pnpm lockfileVersion 6.0/9.0 lockfiles). I missed a case: pnpm v11+ can write pnpm-lock.yaml as two ----separated YAML documents rather than one, whenever either of two fields needs to be persisted:

  • packageManagerDependencies — resolved integrity for pnpm's own binary (self-managed pnpm versions, packageManager/devEngines.packageManager field in package.json).
  • configDependencies — resolved integrity for config dependencies declared in pnpm-workspace.yaml.

Both are recorded in a separate "env" document (first document in the file) so pnpm's regular install/lockfile logic never needs to read/understand them. See pnpm#10964 ("refactor: merge env lockfile into pnpm-lock.yaml") for the pnpm-side implementation.

readYaml()'s use of js-yaml's load() throws on such files:

YAMLException: expected a single document in the stream, but found more

...which means parseLockFile (and therefore lage's cache-key hasher, and any downstream consumer) completely blows up for any repo whose pnpm lockfile happens to have an env document — e.g. any repo that pins packageManager: pnpm@12.x, uses devEngines.packageManager, or declares configDependencies.

I verified this end-to-end against a real repo generated with an actual pnpm 12 beta binary (published at https://github.com/astegmaier/playground-pnpm-paquet-test, along with a detailed writeup of exactly what causes the 2-part format and when it was introduced).

What changed

  • Added readYamlDocuments() to readYaml.ts — same as the existing readYaml(), but uses js-yaml's loadAll() to return every document in the file instead of throwing when there's more than one.
  • parseLockFile's pnpm branch now uses readYamlDocuments() and takes only the last document.
    • For legacy single-document lockfiles this is a no-op (an array of one document, same as before).
    • For 2-document env lockfiles, the env document is intentionally discarded entirelyconfigDependencies/packageManagerDependencies (and their resolved packages/snapshots entries) never leak into the parsed dependency graph. This also protects against a same-named package appearing in both the env document and the real dependency graph with different resolutions — only the real (main document) resolution is ever used.
    • This only changes behavior for pnpm's env-lockfile feature, which only exists for lockfileVersion 9.0+ (the "6/9 codepath" parsePnpmLock added in Support pnpm lockfileVersion 6.0 and 9.0 in parseLockFile #1165) — readYaml() itself, and its use by yarn/berry parsing elsewhere in the same file, are untouched.

Testing

  • Added a basic-pnpm-9-env-lockfile fixture: the existing basic-pnpm-9 fixture with a real env document prepended (configDependencies + packageManagerDependencies, mirroring the playground repo above). The env document also deliberately declares a which@2.0.2 entry with a bogus dependency edge, to verify a name collision with the main document doesn't leak through.
  • New test in lockfile.test.ts asserts:
    • The real dependency graph (from the main document) still parses correctly.
    • Nothing from the env document — including the colliding which@2.0.2 bogus edge — appears in the parsed output.
  • Full workspace-tools suite passes (347 tests, +1 vs. main), plus lint and yarn api (no public API surface changed — readYamlDocuments is not exported).
  • Manually re-ran parseLockFile against the real playground repo; it now returns the correct parsed graph instead of throwing.

Change type

Includes a beachball change file (workspace-tools, patch).

pnpm v11+ writes `pnpm-lock.yaml` as two `---`-separated YAML documents
(an "env" document holding `configDependencies`/`packageManagerDependencies`,
followed by the regular lockfile document) whenever either of those fields is
recorded (see pnpm/pnpm#10964). `readYaml()`'s use of
js-yaml's `load()` throws on such files ("expected a single document in the
stream, but found more"), so `parseLockFile` completely blew up for any repo
whose pnpm-lock.yaml happened to have an env document.

Add `readYamlDocuments()` (using js-yaml's `loadAll()`) and use it for the
pnpm branch of parseLockFile, taking only the last document. This is a no-op
for legacy single-document lockfiles, and for 2-document env lockfiles the
env document (and its configDependencies/packageManagerDependencies) is
intentionally discarded so it can never leak into the parsed dependency
graph.

Adds a basic-pnpm-9-env-lockfile fixture/test that also verifies a
deliberately colliding package name in the env document does not affect the
parsed output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes a CI type-check failure: setupFixture()'s TestFixtureName union
didn't include the new basic-pnpm-9-env-lockfile fixture added for the
env-lockfile test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@astegmaier
Andrew Stegmaier (astegmaier) marked this pull request as ready for review August 5, 2026 17:01
@ecraig12345
Elizabeth Craig (ecraig12345) merged commit 57c95cc into microsoft:main Aug 5, 2026
14 checks passed
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.

2 participants