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
- 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.
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.
- 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).
- 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).
Background
pnpm lets users register custom fetchers by exporting a
fetchersarray from.pnpmfile.mjs. Each fetcher implements: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.mjsand 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-implementedCustomFetcherto pacquet, compiled to the WASM Component Model.Why WASM Component Model
.wasmartifact runs on every platform pacquet supports, no per-target binaries..wasm.wit-bindgen. The contract maps cleanly onto the existingcanFetch/fetchshape.Embed via
wasmtime. Per-call overhead is in the microsecond range, which is negligible compared to network/IO.Alternatives considered
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..pnpmfile.mjsdirectly — 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
fetchersintofetch()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.Cafssurface. 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.pnpmfile.wasmnext topackage.json? a config setting?), and how does it interact with.pnpmfile.mjswhen both exist (pnpm runs custom fetchers in declared order — pacquet would need a deterministic merge rule).CustomFetchertrait 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-plumbingpick-fetcher.Scope
This issue is about the Rust/WASM plugin path only. Supporting JS
.pnpmfile.mjsfetchers in pacquet is a separate (and larger) design question.Prerequisites
Pacquet currently only implements
installand doesn't load.pnpmfile.mjsat all. Before custom fetchers become reachable, pacquet needs a pnpmfile-equivalent loader and the hook-dispatch points ininstalling/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).