Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.
This repository was archived by the owner on May 14, 2026. It is now read-only.

Hoisted-bin precedence and lifecycle-script-created bins (deferred from #333) #342

@KSXGitHub

Description

@KSXGitHub

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/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.

In pnpm this is the preferDirectCmds filter applied during conflict resolution in bins/linker/src/index.ts (4750fd370c#L92).

Tests to port:

  • installing/deps-installer/test/install/hoist.ts:567the hoisted packages should not override the bin files of the direct dependencies — the canonical correctness test.
  • installing/deps-restorer/test/index.ts:569installing with hoistPattern=** — asserts private hoisted .bin/hello-world-js-bin.
  • installing/deps-restorer/test/index.ts:628installing 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).

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:331lifecycle 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:351hoisting 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:372bins are linked even if lifecycle scripts are ignored — direct + nested bins after frozen reinstall with ignored scripts.
  • installing/deps-installer/test/install/multipleImporters.ts:1902link 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.

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.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.

Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    rustPull requests that update Rust code

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions