Skip to content

Rust Roadmap #11633

@zkochan

Description

@zkochan

Stage 0

Stage 1 — Headless installer

Make pacquet install --frozen-lockfile feature-complete with pnpm install --frozen-lockfile.

Pacquet does not resolve dependencies in this stage. The user runs pnpm install (or pnpm install --lockfile-only) to produce pnpm-lock.yaml, then pacquet install --frozen-lockfile materializes node_modules from it. The lockfile is the contract between the two tools.

This mirrors pnpm's internal @pnpm/headless boundary, so the seam is natural.

Completed

TODO

Ordered by implementation dependency — items lower in the list generally build on items above.

Tier 1 — Foundations. Correct snapshot filtering and lockfile loading; nothing downstream is sound until these are in place.

Tier 2 — Real-world install enablers. Each unlocks a large slice of real pnpm repos and is largely independent of the others.

Tier 3 — New resolution types and layout modes. Each is a self-contained extension to the install pipeline; the ordering inside this tier is mostly arbitrary.

Tier 4 — Consistency, polish, auxiliary. Quality-of-life fixes that close the parity gap once the structural work is in place.

Integration milestone (between Stage 1 and Stage 2)

Once the headless installer is solid, integrate pacquet as an opt-in install backend for pnpm itself — for example via an install-backend=pacquet setting, or a Node N-API addon hooked at the @pnpm/headless seam. This ships Stage 1 value to every pnpm user, not just those who install pacquet directly, and provides a feedback firehose for parity validation before Stage 2 is built.

Stage 2 — Implementing resolution

Make pacquet install feature-complete with pnpm install (no --frozen-lockfile). Pacquet reads package.json + pnpm-workspace.yaml, resolves the dependency graph, writes a pnpm-lock.yaml that round-trips byte-identically with pnpm's, and materializes node_modules. This unlocks the install subcommands that today only work via pnpm: install (default), install --lockfile-only, add, update, remove, outdated, why.

Pacquet already has scaffolding from Stage 1: crates/registry, crates/resolving-npm-resolver (metadata fetch + cache, named registries, mirroring, trust checks), crates/resolving-resolver-base, and a full lockfile writer (crates/lockfile/save_lockfile). Stage 2 fills in the parts that turn a manifest into a resolved graph.

TODO

Ordered by implementation dependency — items lower in the list generally build on items above.

Tier 1 — Foundations. Nothing downstream is sound until these land.

  • Bare-specifier parser & wanted-dependency model. Port @pnpm/resolving.parse-wanted-dependency (npm alias pnpm:foo@npm:bar, scoped names, tag vs. semver) and the protocol-routing dispatcher at the top of resolving/default-resolver/src/index.ts. Slots into the existing resolving-resolver-base crate.
  • Resolution graph & recursion engine. Port installing/deps-resolver/src/resolveDependencyTree.ts and resolveDependencies.ts (~2.3k LoC combined). Owns dedupe by name (pkgIdsByName), preferredVersions, direct-vs-transitive context, resolutionsByDepPath cache, pendingNodes, linked-workspace tracking. Heart of this stage.
  • Verify lockfile writer parity for newly-resolved entries. Pacquet's save_lockfile round-trips Stage 1 inputs; confirm it serializes new packages / snapshots / importers / catalogs / overrides / patchedDependencies byte-identically with pnpm so pnpm install after pacquet install produces no diff.

Tier 2 — Per-protocol resolvers. Each unlocks a class of spec. Largely independent.

  • npm resolver — version picking. Port pickPackage.ts / pickPackageFromMeta.ts: semver matching, tag handling, allowedDeprecatedVersions, minimumReleaseAge. Metadata fetch already exists in resolving-npm-resolver.
  • Git resolver. Port resolving/git-resolver: git+ssh, github:owner/repo#ref, #semver:^1 selectors, GitHub-tarball preference. Pairs with the Stage-1 git-fetcher crate.
  • Tarball resolver. Port resolving/tarball-resolver: direct https://…/foo.tgz resolution + integrity verify.
  • Local-path / link / file resolver. Port resolving/local-resolver: file:../foo, link:../foo, injected deps. The Stage-1 directory-fetcher crate handles the fetch side.
  • JSR specifier parser. Port resolving/jsr-specifier-parser; wires into the JSR path already drafted in resolving-npm-resolver.
  • Runtime resolver. Pick versions for node@runtime: / deno@runtime: / bun@runtime:. Stage 1 already loads them from the lockfile; resolution still has to choose a version and emit the entry.
  • Catalog protocol. Resolve catalog:default / catalog:react18 through pnpm-workspace.yaml's catalog(s):, then dispatch to npm. Snapshot the selections into the lockfile's catalogs: block.
  • Workspace protocol. workspace:* / workspace:^ / workspace:~ / workspace:1.2.3link: lockfile entry + publish-time spec rewrite. Pacquet already loads workspaces (Add workspace support to pacquet install --frozen-lockfile pacquet#431).

Tier 3 — Resolution policies & constraints. Behaviors layered on top of the recursion that change which version wins or whether the install proceeds.

  • Peer dependency resolution. Port resolvePeers.ts (~1k LoC). Produces dep paths with peer hashes (foo@1.0.0(react@18.0.0)). Heaviest single port in this stage.
  • Overrides. pnpm.overrides from pnpm-workspace.yaml. Apply at parseWantedDependency time; record in the lockfile's overrides: block.
  • allowedDeprecatedVersions + deprecation warning emission. Wire to the same NDJSON deprecation channel pnpm uses so @pnpm/cli.default-reporter formats them identically.
  • blockExoticSubdeps. Reject git/tarball/file specs in transitive deps.
  • patchedDependencies resolution-side. Hash the patch and attach pkgIdWithPatchHash to dep paths so the installer picks the patched store entry. crates/lockfile/pkg_id_with_patch_hash.rs already exists.
  • allowNonAppliedPatches + verifyPatches.
  • minimumReleaseAge at resolve time. Today it gates global installs only; extend it so new transitive versions are also rejected, mirroring upstream's resolve-path enforcement.

Tier 4 — Hooks & custom resolution. Both require running user JavaScript inside pacquet — design call needed.

  • .pnpmfile.cjs / pnpmfile.cjs execution. Port the readPackage, afterAllResolved, preResolution, filterLog hook surface from hooks/pnpmfile. Implementation choice — embed a JS runtime (rquickjs / deno_core / boa) or shell out to node — has its own parity, perf, and dependency-footprint trade-offs and is worth its own design issue before code lands.
  • Custom resolvers (hooks.types.CustomResolver). Same JS-execution concern as pnpmfile.

Tier 5 — Commands unlocked by resolution. Stage 1 only delivers pacquet install --frozen-lockfile. Once resolution lands, each of these is a CLI surface port that follows the underlying capability.

  • pacquet install (default, non-frozen).
  • pacquet install --lockfile-only.
  • pacquet add (incl. -D, --save-peer, --save-exact, --save-prefix).
  • pacquet update (incl. --latest, updateMatching, -i).
  • pacquet remove.
  • pacquet outdated.
  • pacquet why.

Tier 6 — Consistency, polish, auxiliary. Close the parity gap once the structural work is in place.

  • Round-trip stability. A re-resolve that picks the same versions must produce a byte-identical pnpm-lock.yaml (key ordering, anchor reuse, no spurious churn).
  • Project-manifest update on add / remove / update. Port updateProjectManifest.ts: respect save-prefix, save-exact, save-workspace-protocol.
  • afterAllResolved / preResolution log-event parity so @pnpm/cli.default-reporter formats them identically.
  • Backfill missing log emissions in newly-ported resolution code (sister to chore(reporter): backfill missing log events in already-ported code pacquet#347).
  • E2E port. Mirror pnpm/test/install.ts, add.ts, update.ts, remove.ts, outdated.ts into pacquet's test layout as each capability lands.

Integration milestone (between Stage 2 and Stage 3)

Once resolution is solid, extend the Stage-1 → Stage-2 opt-in seam (install-backend=pacquet / N-API) to route everyday pnpm install calls through pacquet end-to-end — not just --frozen-lockfile. Stage 3 then targets the non-install command surface (publish, audit, dlx, exec, run, store).

Stage 3 — TBD


Stage 2 plan drafted by an agent (Claude Code, claude-opus-4-7).

Metadata

Metadata

Labels

No labels
No labels

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