Fix pnpm v9 snapshot collisions, optionalDependencies, and env labeling#1669
Fix pnpm v9 snapshot collisions, optionalDependencies, and env labeling#1669
Conversation
pnpm lockfile v9 removed the `dev: true/false` flag from packages. The previous `isPnpm9Dev` only checked direct deps, missing transitive dev deps entirely. Replace with the `hydrateDepEnvs` pattern (matching Yarn V1/V2/Poetry): all v9 deps start with empty environments, direct deps get labeled from importer sections, then labels propagate via graph reachability. Shared deps correctly get both environments. - Remove `devOverride` param from `toDependency`/`toResolvedDependency` - Remove `isPnpm9Dev`, `isPnpm9ProjectDev`, `isPnpm9ProjectDep` - Add `maybeHydrate`, `labelV9DirectDeps`, `v9ProdDirectNames`, `v9DevDirectNames` - `toEnv` returns `mempty` for v9 (hydration sets envs instead) - Non-v9 behavior unchanged (`maybeHydrate = id`, `toEnv` uses `isDev`) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sax and xmlbuilder are transitive deps of xml2js (a devDependency), so they should be marked as dev deps, not prod deps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New test fixture where sax is a transitive dep of both uri-js (prod) and xml2js (dev). With hydrateDepEnvs, sax correctly gets both EnvProduction and EnvDevelopment. punycode stays prod-only, xmlbuilder stays dev-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reorder buildGraph pipeline to run maybeHydrate before withoutLocalPackages, so transitive deps of local (file:) packages inherit environment labels before the local nodes are removed. - Replace name-only matching in labelV9DirectDeps with full dependency identity (type + name + version) so multiple versions of the same package get distinct environments and git/tarball deps match correctly. - Replace match guard in toEnv with an if expression per style guidelines. - Add regression tests for local dep env propagation and multi-version env labeling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Caution Review failedFailed to post review comments WalkthroughThis change extends PNPM lockfile parsing to support optional dependencies and introduces PNPM v9-specific environment labeling for direct dependencies. Core updates include expanding the Dependency data constructor with name and version fields, adding optionalDependencies fields to ProjectMap and PackageData, and updating JSON parsing to read these new fields. Graph construction logic incorporates optional dependencies into direct and deep dependency aggregation. PNPM v9 logic introduces environment-aware labeling of direct dependencies via new helpers. Function signatures for toDependency and toResolvedDependency are simplified. Test coverage includes new test data files and test cases validating optional dependency and v9 environment propagation scenarios. 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When two snapshot entries share the same base key but differ in peer dep suffix (e.g. button@1.0.0(react@16) vs button@1.0.0(react@17)), HashMap.mapKeys silently drops one entry. Use fromListWith (<>) to merge their dependency lists instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test verifies that when two snapshot entries share the same base key (e.g. button@1.0.0(react@16) and button@1.0.0(react@17)), their dependency lists are merged rather than one silently dropped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse optionalDependencies from importer sections and the non-workspace fallback. Optional direct deps are included in the graph as production dependencies alongside regular dependencies, and added to v9ProdDirectNames for correct environment labeling in v9 lockfiles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse optionalDependencies from the packages section (v6) and include them as edges in the dependency graph alongside regular and peer dependencies. This ensures transitive optional deps like fsevents are not silently dropped from the graph. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse optionalDependencies from snapshot entries alongside regular dependencies, so transitive optional deps are included in the v9 dependency graph. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test fixtures verify that: - Optional direct deps appear as production direct dependencies - Edges from packages to their optional deps are present (v6 via PackageData, v9 via snapshot optionalDependencies) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Overview
This PR builds on #1668 with additional correctness fixes for pnpm lockfile analysis, based on a spec review of pnpm lockfile v6 and v9 against the parser.
Snapshot peer dep suffix collisions: When multiple peer dependency variants of the same package exist (e.g.
button@1.0.0(react@16.0.0)andbutton@1.0.0(react@17.0.0)), their snapshot dependency lists are now merged usingfromListWith (<>)instead of silently dropping one. Previously,HashMap.mapKeys withoutPeerDepSuffixwas non-injective — if two snapshot keys mapped to the same stripped key, one overwrote the other non-deterministically.`optionalDependencies` silently dropped everywhere: The pnpm spec defines
optionalDependenciesas a valid field in importers, in thepackagessection (v6), and insnapshots(v9). The parser never read any of them, so edges from packages to their optional dependencies were missing from the graph. This is a pre-existing issue across all lockfile versions, not a v9 regression. Optional deps are treated as production because they may be shipped (e.g., platform-specific packages likefsevents).Environment labeling correctness (from base PR, included here):
Acceptance criteria
optionalDependencies, those packages appear in the dependency graph as production dependencies.file:local package dependency, the transitive deps of that local package correctly inherit the environment (prod/dev) of the importer.Testing plan
First, build and install the CLI from this branch:
Then run each test:
1. Optional dependencies
Verify that
fsevents,uri-js, andpunycodeappear in the output (fossa-dev analyze --outputonly shows production deps).colorjs(dev) should be absent. Without this fix,fseventswould be missing entirely.2. Local dep environment propagation
Verify that
saxappears in the pnpm source unit output. Without this fix, transitive deps offile:local packages would lose their environment labels and could be excluded.3. Multi-version environment labeling
mkdir -p /tmp/test-multi-version/packages/{app-a,app-b} && cd /tmp/test-multi-version cat > package.json <<'EOF' { "name": "test-multi-version", "version": "1.0.0", "private": true } EOF cat > pnpm-workspace.yaml <<'EOF' packages: - 'packages/*' EOF cat > packages/app-a/package.json <<'EOF' { "name": "app-a", "version": "1.0.0", "dependencies": { "sax": "1.2.1" } } EOF cat > packages/app-b/package.json <<'EOF' { "name": "app-b", "version": "1.0.0", "devDependencies": { "sax": "1.4.1" } } EOF pnpm install fossa-dev analyze --output .Verify that only
sax@1.2.1appears in the output (production dep from app-a).sax@1.4.1(dev dep from app-b) should be absent. Without this fix, both versions would be co-labeled as production.4. Peer dep suffix collision
This scenario requires a lockfile where two snapshot entries share the same base package key but have different peer dep suffixes (e.g.
button@1.0.0(react@16.0.0)andbutton@1.0.0(react@17.0.0)) with different dependency lists. This is difficult to reproduce withpnpm installand is covered by thepeer suffix collisionunit test with a hand-crafted lockfile fixture.Risks
This PR fixes both risks noted in #1668:
labelV9DirectDepspreviously matched bydependencyNameonly, which could co-label multiple versions of the same package. Now uses full(type, name, version)identity.Remaining risks:
fromListWith (<>)merge for snapshot peer dep suffixes assumes that merging dependency lists is the correct behavior when multiple peer variants exist. In practice this is safe because it is equivalent to saying "this package transitively depends on the union of all variants' deps."Metrics
N/A — correctness fix, no new telemetry needed.
References
Checklist
docs/.docs/README.msand gave consideration to how discoverable or not my documentation is.Changelog.md. If this PR did not mark a release, I added my changes into an## Unreleasedsection at the top..fossa.ymlorfossa-deps.{json.yml}, I updateddocs/references/files/*.schema.jsonAND I have updated example files used byfossa initcommand. You may also need to update these if you have added/removed new dependency type (e.g.pip) or analysis target type (e.g.poetry).docs/references/subcommands/<subcommand>.md.