You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on May 14, 2026. It is now read-only.
#333 implements the bin-linking feature (resolves #330) for direct dependencies and per-slot virtual-store children. Two related behaviours from pnpm's bins/linker are not in that PR because each depends on a subsystem pacquet hasn't built yet. This issue tracks them so future implementers don't lose the porting coordinates and have an unambiguous "what's missing, why, and what would be needed."
Deferred behaviours
1. Hoisted-bin precedence
When hoistPattern or publicHoistPattern is configured, pnpm lifts transitive deps to the root node_modules/ directory. If a hoisted package and a direct dependency both declare a bin with the same name, the direct dependency's bin must win so a project never gets a transitive dep silently shadowing its own tooling.
installing/deps-installer/test/install/hoist.ts:567 — the hoisted packages should not override the bin files of the direct dependencies — the canonical correctness test.
installing/deps-restorer/test/index.ts:569 — installing with hoistPattern=** — asserts private hoisted .bin/hello-world-js-bin.
installing/deps-restorer/test/index.ts:628 — installing with publicHoistPattern=** — asserts public .bin/hello-world-js-bin.
Blockers:
Hoisting subsystem itself (no code in pacquet acts on hoistPattern / publicHoistPattern even though Npmrc parses the config).
Specifically: a "lift" pass that decides which transitive deps get placed at root node_modules/<name> (vs only at the per-slot virtual store).
Public-hoist semantics that decide whether a hoisted dep is also exposed under root node_modules/.bin/.
Implementation hints for the bin side, once hoisting lands:
link_bins_of_packages currently takes &[PackageBinSource]. To express "this came from hoisting, that came from direct deps," extend each PackageBinSource (or the call signature) with an origin: BinOrigin { Direct, Hoisted } discriminator.
In pick_winner, add a tier above the existing ownership/lexical compare: Direct > Hoisted regardless of name match.
The preferDirectCmds rule is less than 30 lines of additional code once the upstream caller can tag the candidates.
2. Lifecycle-script-created bins
Some packages generate their bin files at install time via a postinstall script (or other lifecycle hooks). pnpm runs lifecycle scripts before the linkBinsOfDependencies pass, so a bin file that didn't exist after extraction but was created by postinstall still gets a shim. There's also a related guarantee that bins are linked even when lifecycle scripts are explicitly ignored (--ignore-scripts / allowBuilds: false).
installing/deps-installer/test/install/lifecycleScripts.ts:331 — lifecycle scripts run before linking bins — uses fixture @pnpm.e2e/generated-bins whose postinstall writes cmd1 and cmd2 files.
installing/deps-installer/test/install/lifecycleScripts.ts:351 — hoisting does not fail on commands that will be created by lifecycle scripts on a later stage — interaction with hoistPattern: '*'.
installing/deps-installer/test/install/lifecycleScripts.ts:372 — bins are linked even if lifecycle scripts are ignored — direct + nested bins after frozen reinstall with ignored scripts.
installing/deps-installer/test/install/multipleImporters.ts:1902 — link the bin file of a workspace project that is created by a lifecycle script — workspace variant.
Blockers:
Lifecycle-scripts subsystem (exec/lifecycle/ in pnpm). No pacquet code runs preinstall / install / postinstall today.
allowBuilds config plumbing and the build-approval mechanism.
Mock fixtures (@pnpm.e2e/generated-bins, @pnpm.e2e/has-generated-bins-as-dep) need to be reachable from tasks/registry-mock.
The good news for the bin side:
The existing link_bins_of_packages does the right thing already — search_script_runtime reads the target file's shebang at link time, not at extract time. The shim is just a path string, so a file that appears between extract and link (because lifecycle ran in between) gets a working shim with no code change. No new bin-linking code is needed when lifecycle scripts arrive, only the right phase ordering: extract → run lifecycle scripts → link bins. The current order (extract via CreateVirtualStore, then SymlinkDirectDependencies + bin linking) already places bin linking last.
The only nuance: package.json itself is read once during extract (it's part of the CAS). A lifecycle script that adds new entries to its own package.json after install would not be picked up. pnpm's tests don't cover that case, so it's likely safe to ignore.
#333 ports the bin-linking surface end-to-end: parsing, shim generation (Unix + Windows + PowerShell), conflict resolution, virtual-store and direct-dep wiring, all behind per-capability DI. Adding hoisting or lifecycle scripts in the same PR would conflate two milestones that both deserve their own focused review and tests. Both subsystems are listed in plans/TEST_PORTING.md under their own sections, with cross-references from the "Link Dependency Binaries" section's Rust port notes.
Useful for the implementer who picks this up
The bin-linking side has 100% of its code paths covered by unit tests; nothing about hoisting or lifecycle requires re-touching pacquet-cmd-shim itself.
The Api / RealApi DI contract documented at #332 (comment) extends naturally to whatever capabilities lifecycle scripts and hoisting need (SpawnProcess for exec/lifecycle/, GetWorkspacePackages for hoisting, etc.). Add new traits, impl them on the same RealApi, no separate provider needed.
When the hoisting-side caller is ready to call link_bins_of_packages, the cleanest way to mark candidates is a new pub enum BinOrigin { Direct, Hoisted } field on PackageBinSource. The pick_winner change is mechanical.
The mock registry already publishes @pnpm.e2e/hello-world-js-bin and @pnpm.e2e/hello-world-js-bin-parent (used in feat: bin #333's test). The lifecycle-scripts feature will need @pnpm.e2e/generated-bins added too.
Background
#333 implements the bin-linking feature (resolves #330) for direct dependencies and per-slot virtual-store children. Two related behaviours from pnpm's
bins/linkerare not in that PR because each depends on a subsystem pacquet hasn't built yet. This issue tracks them so future implementers don't lose the porting coordinates and have an unambiguous "what's missing, why, and what would be needed."Deferred behaviours
1. Hoisted-bin precedence
When
hoistPatternorpublicHoistPatternis configured, pnpm lifts transitive deps to the rootnode_modules/directory. If a hoisted package and a direct dependency both declare a bin with the same name, the direct dependency's bin must win so a project never gets a transitive dep silently shadowing its own tooling.In pnpm this is the
preferDirectCmdsfilter applied during conflict resolution inbins/linker/src/index.ts(4750fd370c#L92).Tests to port:
installing/deps-installer/test/install/hoist.ts:567— the hoisted packages should not override the bin files of the direct dependencies — the canonical correctness test.installing/deps-restorer/test/index.ts:569— installing with hoistPattern=** — asserts private hoisted.bin/hello-world-js-bin.installing/deps-restorer/test/index.ts:628— installing with publicHoistPattern=** — asserts public.bin/hello-world-js-bin.Blockers:
hoistPattern/publicHoistPatterneven thoughNpmrcparses the config).node_modules/<name>(vs only at the per-slot virtual store).node_modules/.bin/.Implementation hints for the bin side, once hoisting lands:
link_bins_of_packagescurrently takes&[PackageBinSource]. To express "this came from hoisting, that came from direct deps," extend eachPackageBinSource(or the call signature) with anorigin: BinOrigin { Direct, Hoisted }discriminator.pick_winner, add a tier above the existing ownership/lexical compare:Direct > Hoistedregardless of name match.preferDirectCmdsrule is less than 30 lines of additional code once the upstream caller can tag the candidates.2. Lifecycle-script-created bins
Some packages generate their bin files at install time via a
postinstallscript (or other lifecycle hooks). pnpm runs lifecycle scripts before thelinkBinsOfDependenciespass, so a bin file that didn't exist after extraction but was created bypostinstallstill gets a shim. There's also a related guarantee that bins are linked even when lifecycle scripts are explicitly ignored (--ignore-scripts/allowBuilds: false).In pnpm this ordering is enforced by
building/during-install/src/index.ts (4750fd370c#L258).Tests to port:
installing/deps-installer/test/install/lifecycleScripts.ts:331— lifecycle scripts run before linking bins — uses fixture@pnpm.e2e/generated-binswhosepostinstallwritescmd1andcmd2files.installing/deps-installer/test/install/lifecycleScripts.ts:351— hoisting does not fail on commands that will be created by lifecycle scripts on a later stage — interaction withhoistPattern: '*'.installing/deps-installer/test/install/lifecycleScripts.ts:372— bins are linked even if lifecycle scripts are ignored — direct + nested bins after frozen reinstall with ignored scripts.installing/deps-installer/test/install/multipleImporters.ts:1902— link the bin file of a workspace project that is created by a lifecycle script — workspace variant.Blockers:
exec/lifecycle/in pnpm). No pacquet code runspreinstall/install/postinstalltoday.allowBuildsconfig plumbing and the build-approval mechanism.@pnpm.e2e/generated-bins,@pnpm.e2e/has-generated-bins-as-dep) need to be reachable fromtasks/registry-mock.The good news for the bin side:
The existing
link_bins_of_packagesdoes the right thing already —search_script_runtimereads the target file's shebang at link time, not at extract time. The shim is just a path string, so a file that appears between extract and link (because lifecycle ran in between) gets a working shim with no code change. No new bin-linking code is needed when lifecycle scripts arrive, only the right phase ordering: extract → run lifecycle scripts → link bins. The current order (extract viaCreateVirtualStore, thenSymlinkDirectDependencies+ bin linking) already places bin linking last.The only nuance:
package.jsonitself is read once during extract (it's part of the CAS). A lifecycle script that adds new entries to its ownpackage.jsonafter install would not be picked up. pnpm's tests don't cover that case, so it's likely safe to ignore.Why these aren't done in #333
#333 ports the bin-linking surface end-to-end: parsing, shim generation (Unix + Windows + PowerShell), conflict resolution, virtual-store and direct-dep wiring, all behind per-capability DI. Adding hoisting or lifecycle scripts in the same PR would conflate two milestones that both deserve their own focused review and tests. Both subsystems are listed in
plans/TEST_PORTING.mdunder their own sections, with cross-references from the "Link Dependency Binaries" section'sRust port notes.Useful for the implementer who picks this up
pacquet-cmd-shimitself.Api/RealApiDI contract documented at #332 (comment) extends naturally to whatever capabilities lifecycle scripts and hoisting need (SpawnProcessforexec/lifecycle/,GetWorkspacePackagesfor hoisting, etc.). Add new traits, impl them on the sameRealApi, no separate provider needed.link_bins_of_packages, the cleanest way to mark candidates is a newpub enum BinOrigin { Direct, Hoisted }field onPackageBinSource. Thepick_winnerchange is mechanical.@pnpm.e2e/hello-world-js-binand@pnpm.e2e/hello-world-js-bin-parent(used in feat: bin #333's test). The lifecycle-scripts feature will need@pnpm.e2e/generated-binsadded too.Generated by Claude Code