Detect transitive external patches via the workspace lockfile#181
Detect transitive external patches via the workspace lockfile#181
Conversation
Manifest-based reachability cannot see external-to-external transitives, so a patch for a deep dep like @react-pdf/render (reachable only through @react-pdf/renderer) was dropped during isolation. Walk the workspace pnpm-lock.yaml or bun.lock from the target plus its internal importers and union the resulting set of installed package names into the existing reachableDependencyNames passed to filterPatchedDependencies. The bun graph walker is shared with generateBunLockfile via a new bun-lockfile module.
There was a problem hiding this comment.
Pull request overview
Adds lockfile-based dependency reachability to ensure pnpm.patchedDependencies and Bun patchedDependencies are preserved during isolation even when the patched package is only reachable via external transitive dependencies (regression in issue #167 follow-up).
Changes:
- Extend
copyPatchesreachability by unioning manifest-derived reachable names with names collected by walking the workspace lockfile (pnpm + bun). - Add pnpm lockfile walker (
pnpm-lock.yamlv8/v9, Rush-aware) and bun lockfile walker (bun.lock) to collect the set of installed package names. - Refactor Bun lockfile graph-walker logic into a shared helper module reused by both lockfile generation and patch detection; add regression tests.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/lib/patches/copy-patches.ts | Adds lockfile-derived reachable package names to preserve patches for deep external transitives. |
| src/lib/patches/copy-patches.test.ts | Updates mocks/inputs and adds regression test for deep pnpm transitive patch reachability. |
| src/lib/patches/collect-installed-names-pnpm.ts | New pnpm lockfile walker to collect installed package names starting from target + internal importers. |
| src/lib/patches/collect-installed-names-pnpm.test.ts | New unit tests covering pnpm v9 walking, devDep inclusion, peer suffix trimming, and error handling. |
| src/lib/patches/collect-installed-names-bun.ts | New bun.lock walker that reuses shared bun lockfile traversal helpers. |
| src/lib/patches/collect-installed-names-bun.test.ts | New unit tests for bun.lock walking and error handling. |
| src/lib/lockfile/helpers/generate-bun-lockfile.ts | Refactors previously inlined bun.lock traversal helpers into a shared module. |
| src/lib/lockfile/helpers/bun-lockfile.ts | New shared Bun lockfile types + dependency graph traversal helpers. |
| src/isolate.ts | Passes targetPackageDir and internalDepPackageNames into copyPatches. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Code reviewFound 2 issues:
isolate-package/src/lib/patches/collect-installed-names-pnpm.ts Lines 114 to 130 in 4202406 isolate-package/src/lib/patches/collect-installed-names-pnpm.ts Lines 188 to 208 in 4202406
isolate-package/src/lib/patches/collect-installed-names-pnpm.ts Lines 117 to 122 in 4202406 isolate-package/src/lib/patches/collect-installed-names-pnpm.ts Lines 146 to 162 in 4202406 isolate-package/src/lib/registry/collect-reachable-package-names.ts Lines 14 to 23 in 4202406 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
The inline refToRelative produced v9-style keys regardless of pnpm major version, so for pnpm 8 (lockfileVersion 6.x normalized to v5 keys like /foo/1.0.0) the BFS lookup always missed and transitive walking silently degraded to importer-direct names only. Fork refToRelative into v8 and v9 variants and teach extractPackageName to parse the leading-slash v5 shape. Walk peerDependencies on package snapshots (and importers as a fallback) to keep the walker aligned with collectReachablePackageNames and the bun walker added in this PR. Peer values are name to semver-range, so they contribute names but don't seed the BFS. The previous v8 test fixture used a bogus key shape (/foo@1.0.0) that masked the bug. Replaced with realistic v5-style keys (/@react-pdf/...) proving transitive walking works for v8.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 73e2408. Configure here.
getLockfileImporterId returns OS-native separators; on Windows that's backslashes. The importerIds array was POSIX-normalized so the lockfile lookup works, but the isTarget equality check still compared against the raw value. The result is that on Windows the target's devDependencies would never apply even with includeDevDependencies=true. Extract a toLockfileImporterKey helper and use it for both the importerIds list and the targetImporterId used in the comparison. Normalize backslashes unconditionally rather than relying on path.sep, since lockfile keys are always POSIX regardless of host OS. Add a regression test that simulates the Windows shape.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Problem
pnpm.patchedDependenciesandbun'spatchedDependenciesare dropped during isolation when the patched package is reachable only through an external transitive dep. The follow-up case from issue #167 shows the shape: target depends on@react-pdf/renderer, which depends on the patched@react-pdf/render. Since@react-pdf/renderis not listed on any internal manifest,collectReachablePackageNames(which only walks workspace manifests) doesn't see it,filterPatchedDependenciesexcludes it, the patch file is not copied, and the isolated install runs with an unpatched copy.Solution
Walk the workspace lockfile from the target plus its internal importers and union the resulting set of installed package names into the existing
reachableDependencyNamesargument thatfilterPatchedDependenciesconsumes. The three current tiers (direct prod, direct dev, internal-reachable) are unchanged; the lockfile-derived set is added on top so the filter already handles it via its existing reachable check.For pnpm, the new helper reads
pnpm-lock.yaml(v8 or v9, Rush-aware), seeds a BFS frontier from each importer'sdependencies/optionalDependencies(plus the target'sdevDependencieswhenincludeDevDependenciesis true), and walkslockfile.packages, mirroring@pnpm/dependency-path'srefToRelativeandindexOfPeersSuffixinline so the package set stays a strict subset of what pnpm will install. For bun, the BFS that already exists ingenerateBunLockfileis extracted into a shared module so both lockfile generation and patch detection use the same walker.Walk failures (missing lockfile, parse error, unknown pnpm version) return an empty set so the existing manifest-based tiers still apply as a fallback.
NPM is unaffected:
copyPatchesis still gated to pnpm/bun viaisolate.tsand skipped entirely underforceNpm.Scope: packages (isolate-package)
Visibility: user-facing