You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on May 14, 2026. It is now read-only.
pnpm v11 lets a project declare a JavaScript runtime as a dependency: node@runtime:22, deno@runtime:1.x, bun@runtime:1, either through pnpm add or via devEngines.runtime in package.json. The resolution lands in pnpm-lock.yaml as a BinaryResolution (one platform) or VariationsResolution (multiple platforms wrapped in variants), each pointing at a .tar.gz or .zip downloaded from nodejs.org / dl.deno.land / GitHub releases. On install, pnpm picks the variant that matches the host platform/arch/libc, extracts it into the CAS, and links the binary (node, deno, bun) into node_modules/.bin/.
Roadmap (#299) Stage 1 lists "Installation of runtimes" as a remaining checkbox. This issue scopes it to pacquet install --frozen-lockfile: handle BinaryResolution and VariationsResolution entries already present in a lockfile. Resolution (parsing runtime: specifiers, talking to nodejs.org for version lookups, picking the artifact filename) lives in engine/runtime/{node,deno,bun}-resolver and is Stage 2.
Today
Pacquet can't load a lockfile containing a runtime entry. crates/lockfile/src/resolution.rs enumerates Tarball | Registry | Directory | Git — no Binary, no Variations — and TaggedResolution is #[serde(deny_unknown_fields)], so a real v11 lockfile with type: binary or type: variations fails serde before the install dispatcher even runs.
Bin linking for runtime depPaths (the depPath itself takes a non-standard shape: node@runtime:22.0.0).
The current --frozen-lockfile path also asserts lockfile_version.major == 9 at crates/package-manager/src/install.rs:172. Verify whether v11 runtime lockfiles bumped the major — if so, that assertion needs updating; if the change is purely additive within v9, it stays.
Variant picking. Each variant's targets[] lists { os, cpu, libc? } combos. Match against the current platform; the first variant whose targets includes the host wins.
archive === 'tarball' → delegate to the existing remote-tarball fetcher with an ignoreFilePattern so npm/corepack files don't enter the CAS.
archive === 'zip' → download to a temp file, integrity-check with ssri, extract via AdmZip into a temp dir, run addFilesFromDir into the CAS. Per-entry path traversal validation (validatePathSecurity), stateless ignore-regex tester, directory entries skipped to keep the ignore filter effective.
Node's "strip bundled tooling" filter.engine/runtime/node-resolver/src/index.ts:25-30 defines NODE_EXTRAS_IGNORE_PATTERN excluding node_modules/{npm,corepack} and bin/{npm,npx,corepack} (plus .cmd/.ps1). The pattern is applied during binary fetch via archiveFilters[<pkg.name>]. ~2,800 of ~5,800 files are skipped.
Fetcher dispatch.fetching/pick-fetcher/src/index.ts:51-58: resolution.type === 'binary' → binary fetcher. Variations are unwrapped one level higher (the install pipeline picks the matching variant first, then dispatches its inner BinaryResolution).
BinaryResolution { url, integrity, bin: BinSpec, archive: BinaryArchive } where BinSpec = String | HashMap<String, String> and BinaryArchive = Tarball | Zip (serde tag = "archive" or as the existing tagged-enum mechanism). Tag: type: "binary".
Extend LockfileResolution and the ResolutionSerde/TaggedResolution helpers in the same file so round-trip serde stays exhaustive.
Confirm the lockfile-version major hasn't bumped (compare a v11 lockfile output containing a runtime against lockfile_version.major == 9 at crates/package-manager/src/install.rs:172). If it did, update the assertion and the parser; if not, leave the assertion alone.
B. Variant picking
Add pick_variant(variants: &[PlatformAssetResolution], host: PlatformAssetTarget) -> Option<&PlatformAssetResolution>. Match strategy mirrors upstream: first variant whose targets includes the host. libc is only checked on Linux (musl vs glibc) — verify exact semantics against engine/runtime/node-resolver since libc: 'musl' is the only value upstream sets.
Host detection: std::env::consts::{OS, ARCH} for pacquet; libc detection on Linux via the existing pacquet plumbing if any, else reading /proc/self/exe or inspecting ldd/dynamic loader. Stage-1 minimum: glibc fallback on Linux. Track musl detection as a follow-up checkbox.
C. Binary fetcher
New module — crates/binary-fetcher or under crates/tarball if reusing the same CAS-write helpers — porting fetching/binary-fetcher/src/index.ts:
Tarball archive. Reuse pacquet's existing remote-tarball fetcher with an extra ignore_file_pattern: Option<Regex> parameter. The pattern's threaded as a string today across upstream's worker boundary; pacquet can pass a compiled regex::Regex directly.
Zip archive. Download with integrity check (ssri-equivalent — pacquet already uses ssri crate for tarball integrity), extract entries with path-traversal validation. The existing zip dependency situation needs a decision before this lands: pacquet doesn't have a zip crate yet, so flag this as a workspace-dependency request rather than adding zip unilaterally. Per CLAUDE.md, raise the dep question with a human before adding.
Strip bundled tooling: when pkg.name == "node", apply NODE_EXTRAS_IGNORE_PATTERN (port the regex verbatim, including its post-prefix-strip semantics) during extraction. Similar archive-filter slots may exist for deno/bun; check upstream and mirror.
Validate every extracted entry against the target directory boundary (validatePathSecurity equivalent). Reject absolute paths; reject paths that resolve outside the target.
D. Install pipeline wiring
Extend the resolution dispatcher at crates/package-manager/src/install_package_by_snapshot.rs:110-115 and create_virtual_store.rs:513-520:
LockfileResolution::Variations(v) → pick_variant first, then recurse with the picked variant's inner resolution. Error with a clear "no variant matches host platform" if pick_variant returns None.
Bin linking for runtime depPaths. The bin field in BinaryResolution already names the executables; create <project>/node_modules/.bin/<binName> cmd-shims pointing at the extracted location. Pacquet's existing pacquet-cmd-shim infrastructure handles the cross-platform shim part — wire it for runtime entries.
Recognise the @runtime: substring in depPaths where needed (skip lists, reporter prefixes, etc.). Most pipeline code doesn't care, but the upstream skipRuntimes filter at installing/deps-installer/src/install/index.ts:1377-1387 uses it as the discriminator.
Store-index key: BinaryResolution has stable integrity, so the regular storeIndexKey(integrity, pkgId) shape works; nothing special.
E. --no-runtime
Add skip_runtimes: bool to Config and the CLI (--no-runtime).
When set, skip the binary fetch + bin-linking but keep the lockfile parse intact so the install still validates. Mirror upstream's behavior at the call site above: filter runtime depPaths out of dependencies_by_project_id after the lockfile walk.
F. Tests
Frozen-lockfile install with a fixture lockfile containing a VariationsResolution for node@runtime:22. Assert the host-matching variant is fetched, the CAS contains the extracted Node, node_modules/.bin/node resolves, and node_modules/<importer>/node_modules/node/ symlinks correctly. Use a recorded fixture archive (small slice) rather than hitting nodejs.org from CI.
Same as above with a single BinaryResolution (no variations wrapper).
npm/npx/corepack paths are absent from the imported file set when pkg.name == "node" (asserts the ignore pattern fired).
Variant mismatch: lockfile contains only darwin-arm64 variants but the test pretends to run on linux-x64 → clear error.
--no-runtime: lockfile contains a runtime entry, install skips the download + bin-link, the rest of the install succeeds, exit code is 0.
Integrity mismatch on the downloaded archive → TARBALL_INTEGRITY / equivalent error code, no partial state in the CAS.
Path-traversal-malicious zip → PATH_TRAVERSAL error before any extraction succeeds.
Port plans/TEST_PORTING.md "Installation Of Runtimes" entries (six checkboxes against installing/deps-installer/test/install/nodeRuntime.ts). Mark them as done as they land.
Out of scope (Stage 2 / follow-ups)
Resolution.engine/runtime/{node,deno,bun}-resolver — runtime: spec parsing, parseNodeSpecifier (release-channel handling, lts/rc aliases), getNodeMirror, talking to nodejs.org's index.json to resolve 22 → 22.x.y. Needed for pnpm add node@runtime:22 and for fresh pnpm install (without a lockfile). Track with Stage 2.
Background
pnpm v11 lets a project declare a JavaScript runtime as a dependency:
node@runtime:22,deno@runtime:1.x,bun@runtime:1, either throughpnpm addor viadevEngines.runtimeinpackage.json. The resolution lands inpnpm-lock.yamlas aBinaryResolution(one platform) orVariationsResolution(multiple platforms wrapped invariants), each pointing at a.tar.gzor.zipdownloaded fromnodejs.org/dl.deno.land/ GitHub releases. On install, pnpm picks the variant that matches the host platform/arch/libc, extracts it into the CAS, and links the binary (node,deno,bun) intonode_modules/.bin/.Roadmap (#299) Stage 1 lists "Installation of runtimes" as a remaining checkbox. This issue scopes it to
pacquet install --frozen-lockfile: handleBinaryResolutionandVariationsResolutionentries already present in a lockfile. Resolution (parsingruntime:specifiers, talking to nodejs.org for version lookups, picking the artifact filename) lives inengine/runtime/{node,deno,bun}-resolverand is Stage 2.Today
Pacquet can't load a lockfile containing a runtime entry.
crates/lockfile/src/resolution.rsenumeratesTarball | Registry | Directory | Git— noBinary, noVariations— andTaggedResolutionis#[serde(deny_unknown_fields)], so a real v11 lockfile withtype: binaryortype: variationsfails serde before the install dispatcher even runs.Beyond the type gap there's no:
node@runtime:22.0.0).The current
--frozen-lockfilepath also assertslockfile_version.major == 9atcrates/package-manager/src/install.rs:172. Verify whether v11 runtime lockfiles bumped the major — if so, that assertion needs updating; if the change is purely additive within v9, it stays.Upstream references (pnpm v11
94240bc046)lockfile/types/src/index.ts:127-163:BinaryResolution { type: 'binary', url, integrity, bin: string | Record<string, string>, archive: 'zip' | 'tarball' }.PlatformAssetResolution { resolution: Resolution, targets: PlatformAssetTarget[] }.VariationsResolution { type: 'variations', variants: PlatformAssetResolution[] }.PlatformAssetTargetis defined atresolving/resolver-base/src/index.ts:60.targets[]lists{ os, cpu, libc? }combos. Match against the current platform; the first variant whosetargetsincludes the host wins.fetching/binary-fetcher/src/index.ts:archive === 'tarball'→ delegate to the existing remote-tarball fetcher with anignoreFilePatternso npm/corepack files don't enter the CAS.archive === 'zip'→ download to a temp file, integrity-check with ssri, extract via AdmZip into a temp dir, runaddFilesFromDirinto the CAS. Per-entry path traversal validation (validatePathSecurity), stateless ignore-regex tester, directory entries skipped to keep the ignore filter effective.engine/runtime/node-resolver/src/index.ts:25-30definesNODE_EXTRAS_IGNORE_PATTERNexcludingnode_modules/{npm,corepack}andbin/{npm,npx,corepack}(plus.cmd/.ps1). The pattern is applied during binary fetch viaarchiveFilters[<pkg.name>]. ~2,800 of ~5,800 files are skipped.fetching/pick-fetcher/src/index.ts:51-58:resolution.type === 'binary'→binaryfetcher. Variations are unwrapped one level higher (the install pipeline picks the matching variant first, then dispatches its innerBinaryResolution).--no-runtimeflag.installing/commands/src/install.ts:137-138andinstalling/deps-installer/src/install/index.ts:1377-1387. Skips runtime fetch + bin-linking; the lockfile is left untouched so frozen-lockfile checks still pass. Runtime depPaths are recognised via the@runtime:substring.Plan
A. Lockfile types
crates/lockfile/src/resolution.rs:BinaryResolution { url, integrity, bin: BinSpec, archive: BinaryArchive }whereBinSpec = String | HashMap<String, String>andBinaryArchive = Tarball | Zip(serdetag = "archive"or as the existing tagged-enum mechanism). Tag:type: "binary".PlatformAssetResolution { resolution: Resolution, targets: Vec<PlatformAssetTarget> }.PlatformAssetTarget { os: String, cpu: String, libc: Option<String> }.VariationsResolution { variants: Vec<PlatformAssetResolution> }. Tag:type: "variations".LockfileResolutionand theResolutionSerde/TaggedResolutionhelpers in the same file so round-trip serde stays exhaustive.lockfile_version.major == 9atcrates/package-manager/src/install.rs:172). If it did, update the assertion and the parser; if not, leave the assertion alone.B. Variant picking
pick_variant(variants: &[PlatformAssetResolution], host: PlatformAssetTarget) -> Option<&PlatformAssetResolution>. Match strategy mirrors upstream: first variant whosetargetsincludes the host.libcis only checked on Linux (musl vs glibc) — verify exact semantics againstengine/runtime/node-resolversincelibc: 'musl'is the only value upstream sets.std::env::consts::{OS, ARCH}for pacquet; libc detection on Linux via the existing pacquet plumbing if any, else reading/proc/self/exeor inspectingldd/dynamic loader. Stage-1 minimum: glibc fallback on Linux. Track musl detection as a follow-up checkbox.C. Binary fetcher
crates/binary-fetcheror undercrates/tarballif reusing the same CAS-write helpers — portingfetching/binary-fetcher/src/index.ts:ignore_file_pattern: Option<Regex>parameter. The pattern's threaded as a string today across upstream's worker boundary; pacquet can pass a compiledregex::Regexdirectly.ssricrate for tarball integrity), extract entries with path-traversal validation. The existing zip dependency situation needs a decision before this lands: pacquet doesn't have a zip crate yet, so flag this as a workspace-dependency request rather than addingzipunilaterally. Per CLAUDE.md, raise the dep question with a human before adding.pkg.name == "node", applyNODE_EXTRAS_IGNORE_PATTERN(port the regex verbatim, including its post-prefix-strip semantics) during extraction. Similar archive-filter slots may exist fordeno/bun; check upstream and mirror.validatePathSecurityequivalent). Reject absolute paths; reject paths that resolve outside the target.D. Install pipeline wiring
crates/package-manager/src/install_package_by_snapshot.rs:110-115andcreate_virtual_store.rs:513-520:LockfileResolution::Binary(binary)→ binary fetcher path.LockfileResolution::Variations(v)→pick_variantfirst, then recurse with the picked variant's inner resolution. Error with a clear "no variant matches host platform" ifpick_variantreturnsNone.binfield inBinaryResolutionalready names the executables; create<project>/node_modules/.bin/<binName>cmd-shims pointing at the extracted location. Pacquet's existingpacquet-cmd-shiminfrastructure handles the cross-platform shim part — wire it for runtime entries.@runtime:substring in depPaths where needed (skip lists, reporter prefixes, etc.). Most pipeline code doesn't care, but the upstreamskipRuntimesfilter atinstalling/deps-installer/src/install/index.ts:1377-1387uses it as the discriminator.BinaryResolutionhas stableintegrity, so the regularstoreIndexKey(integrity, pkgId)shape works; nothing special.E.
--no-runtimeskip_runtimes: booltoConfigand the CLI (--no-runtime).dependencies_by_project_idafter the lockfile walk.F. Tests
VariationsResolutionfornode@runtime:22. Assert the host-matching variant is fetched, the CAS contains the extracted Node,node_modules/.bin/noderesolves, andnode_modules/<importer>/node_modules/node/symlinks correctly. Use a recorded fixture archive (small slice) rather than hitting nodejs.org from CI.BinaryResolution(no variations wrapper).npm/npx/corepackpaths are absent from the imported file set whenpkg.name == "node"(asserts the ignore pattern fired).darwin-arm64variants but the test pretends to run onlinux-x64→ clear error.--no-runtime: lockfile contains a runtime entry, install skips the download + bin-link, the rest of the install succeeds, exit code is 0.TARBALL_INTEGRITY/ equivalent error code, no partial state in the CAS.PATH_TRAVERSALerror before any extraction succeeds.plans/TEST_PORTING.md"Installation Of Runtimes" entries (six checkboxes againstinstalling/deps-installer/test/install/nodeRuntime.ts). Mark them as done as they land.Out of scope (Stage 2 / follow-ups)
engine/runtime/{node,deno,bun}-resolver—runtime:spec parsing,parseNodeSpecifier(release-channel handling,lts/rcaliases),getNodeMirror, talking to nodejs.org'sindex.jsonto resolve22→22.x.y. Needed forpnpm add node@runtime:22and for freshpnpm install(without a lockfile). Track with Stage 2.pnpm runtime setCLI.engine/runtime/commands/src/runtime. Standalone command for ad-hoc installs, depends on the resolver.devEngines.runtimeauto-resolution. Readingpackage.json#devEngines.runtimeand turning it into a lockfile entry depends on the resolver.runtimeOnFail. A separate Stage-2 behavior (auto-install of a runtime if execution fails).use-node-versionlegacy setting. Older equivalent ofruntime:; verify against upstream whether it's still supported.pacquet install --frozen-lockfile#432 / Add workspace support topacquet install --frozen-lockfile#431 / Partial install with--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots #433 / Add hoisting support:hoistPatternandpublicHoistPattern#435 once those land. Re-test then.Related
plans/TEST_PORTING.md"Installation Of Runtimes"pacquet install --frozen-lockfile#431), GVS (Add global virtual store support topacquet install --frozen-lockfile#432), partial install (Partial install with--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots #433), hoisting (Add hoisting support:hoistPatternandpublicHoistPattern#435), git-hosted packages (Install git-hosted packages frompnpm-lock.yaml(frozen-lockfile) #436)Written by an agent (Claude Code, claude-opus-4-7).