Files
pnpm/pacquet
Zoltan Kochan 4cf66757d6 feat(package-manager): hoisted dep-graph walker (#438 slice 4b) (#486)
Sub-slice 4b of #438. Implements `lockfile_to_hoisted_dep_graph(lockfile, opts) -> LockfileToDepGraphResult` — the walker that consumes the Slice 3 hoister output and produces the directory-keyed graph + hierarchy + hoisted_locations + direct-deps-by-importer-id + injection-targets-by-dep-path that 4a (#478) pinned the type shape of.

Single-importer only (multi-importer lockfiles surface as `HoistError::UnsupportedWorkspace` via the underlying hoister).

## What's deferred

Three things are intentionally left to follow-ups so 4b stays focused:

- **Store fetch wiring** — `fetching` / `files_index_file` on the graph node are still defaulted. 4c will wire `StoreController::fetchPackage` (and the skip-fetch optimization that consults `currentHoistedLocations`).
- **Installability check** — the walker honors `opts.skipped` as input but doesn't run `packageIsInstallable` to add new entries. 4c will plumb `package_is_installable` from `pacquet-package-is-installable` (already used by the isolated linker path).
- **`prev_graph` diff** — `prev_graph` is `None`. 4d will walk the *current* lockfile alongside the wanted one (using `force: true` + empty `skipped` per upstream) and produce the orphan-removal input Slice 5's linker consumes.

## Two-pass design

Upstream's `fetchDeps` walks asynchronously via `await Promise.all(deps.map(async (dep) => { ... }))`. The key invariant: each sibling's async function runs its sync prologue (insert + push to `pkgLocationsByDepPath`) before any continuation fires, because `Promise.all` sync-invokes each function and the inner `await fetchDeps(...)` only yields after the prologue completes. So by the time any node's post-recursion `graph[dir].children = getChildren(...)` line runs, every node in the entire tree is already in `pkgLocationsByDepPath`.

Pacquet runs synchronously. To preserve that invariant cleanly:

1. **Pass 1 (recursive walk).** Insert each node into the graph with `children: BTreeMap::new()` and push its dir to `pkg_locations_by_dep_path`. Recursion order matches upstream's outer body (insert → push → recurse → push to `hoistedLocations`).
2. **Pass 2 (`fill_children`).** Iterate every graph node and resolve `children: alias → dir` from the snapshot's `dependencies` + `optionalDependencies` via `SnapshotDepRef::resolve`, consulting the now-complete `pkg_locations_by_dep_path`.

A single-pass walker that computed children inline produced incorrect `children` maps for hoisted transitive deps — the canonical case `root → a → b` with `b` flattened to root left `a.children["b"]` empty because `b`'s pkg_locations entry hadn't been recorded yet. Caught by `walker_transitive_dep_flattens_under_root` before the refactor.

## Tests

- `walker_empty_lockfile_produces_empty_result` — empty importer → empty graph, with the `"."` root importer key still present.
- `walker_single_root_dep_emits_one_node` — `root → a`, node at `<lockfile_dir>/node_modules/a`.
- `walker_transitive_dep_flattens_under_root` — `root → a → b`: `b` hoists to root, `a.children["b"]` points at the root-level dir.
- `walker_version_conflict_keeps_loser_nested` — `root → {a@1, c}` with `c → a@2`: `a@1` at root, `a@2` nested under `c`; `hoisted_locations` records both directories; `c.children["a"]` points at the nested copy.
- `walker_honors_pre_skipped_dep_path` — depPaths in `opts.skipped` are dropped from the walk entirely (and not recorded in `hoisted_locations`).
- `walker_records_directory_resolution_as_injection_target` — `directory:` resolutions populate `injection_targets_by_dep_path` for the post-install re-mirror pass.
2026-05-13 22:16:54 +02:00
..
2026-05-12 02:16:36 +02:00
2026-05-13 20:39:02 +02:00
2023-10-28 20:19:05 +03:00
2026-05-12 02:07:14 +02:00
2026-04-30 16:35:57 +02:00
2026-05-07 22:30:00 +02:00
2026-05-07 22:30:00 +02:00

pacquet

Warning

pacquet is under active development and not yet ready for production use.

The official pnpm rewrite in Rust.

pacquet is a port of the pnpm CLI from TypeScript to Rust. It is not a new package manager and not a reimagining of pnpm. Its behavior, flags, defaults, error codes, file formats, and directory layout will match pnpm exactly.

Roadmap

pacquet will become the installation engine of pnpm. The transition will happen in two phases.

Phase 1: fetching and linking

pacquet replaces fetching and linking only. pnpm continues to create the lockfile, and pacquet does the rest. We expect this alone to make pnpm at least twice as fast in most scenarios. Shipping this phase is the current focus.

Phase 2: resolution

pacquet also takes over dependency resolution.

See CONTRIBUTING.md for development setup, debugging, testing, and benchmarking.

Benchmark