fix(pnpm-collector): find nested deps in pnpm hoisted workspaces - #10234
Conversation
…he app is a hoisted workspace package With `nodeLinker: hoisted`, pnpm installs a workspace package's dependencies into the workspace root's node_modules, and the app dir has no node_modules of its own unless it needs a conflicting version. The collector runs with rootDir = the app dir, so: - `isHoisted` scanned `<app>/node_modules`, found nothing, and reported the flat default, disabling the downward search; and - even when hoisted, the downward BFS started at the missing `<app>/node_modules`, never reaching `<root>/node_modules/lazystream/node_modules/readable-stream`. The override fallback then accepted the root readable-stream@3.6.2 for lazystream's ^2 dependency (#10228's symptom, still present on master for workspace apps). Detect the layout from the workspace root when the app dir has no packages, and add a range-satisfying search from the workspace root after the app-dir search. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Evsxm8m75QrJA3XqkuaXNF
… changeset Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Evsxm8m75QrJA3XqkuaXNF
🦋 Changeset detectedLatest commit: e7ee366 The changes in this PR will be included in the next version bump. This PR includes changesets to release 8 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
CI note: The job failed while building the Docker test image, before any test ran. It stopped at the first Why it isn't caused by this PR:
There's no code fix to make. The failed job needs a re-run once the run finishes; GitHub won't re-run a single job while the run is in progress. Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Evsxm8m75QrJA3XqkuaXNF
…space Replace the mocked node_modules trees in pnpmHoistedTest with packTester tests in HoistedNodeModuleTest that run a real `pnpm install` (pnpm pinned to 11.26.0 via `packageManager`) and pack the app: - hoisted workspace (#10228): app is a workspace package with no own node_modules, `nodeLinker: hoisted` in pnpm-workspace.yaml - hoisted single-package project - isolated (.pnpm store) project - isolated project with a `link:` dependency - project without dependencies Each asserts the layout the collector detects (isHoisted) on the real install and the exact packaged node_modules tree, including lazystream's nested readable-stream@2.3.8 next to the root readable-stream@3.6.2. Only the ModuleManager skipDownwardSearch unit test, which a real install cannot observe, stays mocked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Evsxm8m75QrJA3XqkuaXNF
…out tests
HoistedNodeModuleTest resolves to HoistedNodeModuleTest.js.win.snap on
win32, which was missing the five new "pnpm v11 ..." entries, so CI
(which never writes snapshots) failed them as mismatched on Windows.
The entries are the same `{ linux: [] }` dir-target artifact list as the
POSIX baseline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Evsxm8m75QrJA3XqkuaXNF
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes core pnpm dependency-resolution/packaging logic (workspace-root discovery, hoisted-layout detection, and the fallback search order used to pick nested dependency versions), a human look would still be worthwhile.
What was reviewed: the new workspaceRoot/layoutRoots walk-up and the refactored scanNodeModulesLayout helper used by isHoisted; the extended fallback order in locateFromDepOrRoot that now also checks the workspace root; and the new pnpm v11 test scenarios (hoisted workspace, hoisted single project, isolated project, isolated project with a link: dependency, no-dependencies project) plus their lockfile fixtures and snapshots.
Extended reasoning...
The change touches app-builder-lib's pnpm node-modules collector, specifically the algorithm that decides which on-disk copy of a version-conflicted transitive dependency gets packaged into the app; it has no auth/crypto/permissions surface, but a wrong result silently ships the wrong dependency version in a production app, which is what motivated the fix. The diff is non-trivial (new lazy workspace-root traversal, a reusable layout-scan helper, and an extra fallback search location) and several plausible edge cases (e.g. mixed isolated/hoisted layouts across rootDir and workspace root) were already raised and investigated by the automated bug hunt and ruled out rather than fixed, which is exactly the kind of judgment call a human maintainer should also weigh in on. Test coverage is solid: five new pnpm v11 scenarios directly exercise hoisted workspace, hoisted single-project, isolated, isolated-with-link, and no-dependency layouts against the described bug (lazystream's nested readable-stream@ 2 vs the root's readable-stream@ 3), with matching lockfile fixtures and updated snapshots.
Requested by Mike Maietta · Slack thread
In a pnpm hoisted (
nodeLinker: hoisted) workspace the app package usually has nonode_modulesof its own, so the pnpm collector detected the layout as isolated and never searched the workspace root. It then packaged out-of-range copies of nested deps, for example the rootreadable-stream@3.6.2in place oflazystream's nestedreadable-stream@2.3.8, which is the same symptom as #10228. The collector now finds the workspace root by walking up to the nearestpnpm-workspace.yamland uses it both for hoisted detection and for the in-range lookup. This follows up on #10228, and the same fix is being added to the v26 backport #10230. It touches the same files as #10232, so whichever PR merges second may need a small rebase. The packaging integration tests couldn't run locally because the Electron download is blocked, so they rely on CI.🤖 Generated with Claude Code
https://claude.ai/code/session_01Evsxm8m75QrJA3XqkuaXNF
Generated by Claude Code