Skip to content

pacquet: support custom fetchers written in Rust (pnpmfile.wasm) #11685

Description

@zkochan

Background

pnpm lets users register custom fetchers by exporting a fetchers array from .pnpmfile.mjs. Each fetcher implements:

interface CustomFetcher {
  canFetch?(pkgId: string, resolution: Resolution): boolean | Promise<boolean>
  fetch?(cafs: Cafs, resolution: Resolution, opts: FetchOptions, fetchers: Fetchers): FetchResult | Promise<FetchResult>
}

Custom fetchers are consulted before the built-in fetchers (see fetching/pick-fetcher/src/index.ts) and can fully replace tarball/git/directory/binary fetching for matching packages, or delegate back to them.

Pacquet (the Rust port) doesn't load .pnpmfile.mjs and hosting a JS runtime to support it would drag a large dependency into pacquet for a feature most users don't need. A more idiomatic alternative is to let users author fetchers in Rust and load them as compiled plugins.

Proposal

Support a pnpmfile.wasm (or similar) plugin that exposes a Rust-implemented CustomFetcher to pacquet, compiled to the WASM Component Model.

Why WASM Component Model

  • Portable — single .wasm artifact runs on every platform pacquet supports, no per-target binaries.
  • Sandboxed — a buggy or malicious plugin can't segfault the installer or escape WASI capabilities.
  • No rustc on user machines — the plugin author compiles once and ships the .wasm.
  • Typed contract — define the interface once in WIT, generate host + guest bindings via wit-bindgen. The contract maps cleanly onto the existing canFetch / fetch shape.
  • Precedent — Zellij, Lapce, and recent Zed plugins use the same approach.

Embed via wasmtime. Per-call overhead is in the microsecond range, which is negligible compared to network/IO.

Alternatives considered

  • Native dylibs via libloading — zero overhead and full Rust ergonomics, but the plugin author has to publish ~6 per-platform binaries and a crash takes down the installer. Realistic only for first-party / internal use.
  • Embedded scripting (Rhai, mlua, Steel) — easy to ship, but it isn't actually Rust, which defeats the appeal.
  • Hosting V8/QuickJS to run .pnpmfile.mjs directly — preserves parity with the TS implementation but adds a JS runtime to pacquet and a large binding surface. Worth a separate issue if there's demand; orthogonal to this one.

Open design questions

  1. Re-entrancy. pnpm's JS contract passes the built-in fetchers into fetch() so plugins can delegate (e.g. "use the standard tarball fetcher after rewriting the URL"). In WASM this becomes host-imported functions and a re-entrancy hop — worth benchmarking before committing to the shape.
  2. Cafs surface. The content-addressable filesystem handle is currently a rich Rust object. We'd need to expose a minimal capability-style interface over WIT (add file by stream, finalize, etc.) rather than the full Rust API.
  3. Discovery and resolution order. Where does pacquet look for the plugin (pnpmfile.wasm next to package.json? a config setting?), and how does it interact with .pnpmfile.mjs when both exist (pnpm runs custom fetchers in declared order — pacquet would need a deterministic merge rule).
  4. Internal trait. Define a Rust CustomFetcher trait in pacquet that both built-in fetchers and WASM-hosted plugins implement, so the WASM layer is a thin adapter rather than a special case. This also leaves the door open for a future JS adapter without re-plumbing pick-fetcher.

Scope

This issue is about the Rust/WASM plugin path only. Supporting JS .pnpmfile.mjs fetchers in pacquet is a separate (and larger) design question.

Prerequisites

Pacquet currently only implements install and doesn't load .pnpmfile.mjs at all. Before custom fetchers become reachable, pacquet needs a pnpmfile-equivalent loader and the hook-dispatch points in installing/package-requester. The plugin contract proposed here can be designed in parallel, but it can't ship until that scaffolding lands.


Written by an agent (Claude Code, claude-opus-4-7).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    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