mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-19 12:12:34 -04:00
port-e2e
11575 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c4e421bad |
test(pnpm): mark Linux-only failures missed by the macOS sweep
The first scoped pacquet-e2e run on Linux turned up two cases the macOS sweep couldn't observe: - test/cli.ts `pnpx works`: exercises pnpx, which has no pacquet equivalent. - test/install/supportedArchitectures.ts: the whole matrix is gated on Linux (other hosts can't see every architecture), and it passes `--reporter=append-only` plus CLI flags pacquet doesn't accept. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
86af5326ff |
ci(pacquet): scope the e2e run to the pnpm package only
Now that --no-runtime fixed the VM-modules loader, tests actually run. The next failure mode was real: other workspace packages (e.g. @pnpm/building.commands) shell out to pnpm in their tests, and PNPM_E2E_BIN routes those calls through pacquet, which doesn't implement every command they exercise. Add a dedicated test-pkgs-pnpm script so the e2e job runs jest only in the pnpm package and PNPM_E2E_BIN affects nothing else. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
43d8c05697 |
ci(pacquet): pass --no-runtime to pnpm install
test.yml's working install path uses --no-runtime so pnpm doesn't try to materialize an executionEnv runtime that conflicts with the one pnpm/setup pinned. The competing runtime install is the last discoverable difference between this job and the standard e2e job. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
3a948505f7 |
ci(pacquet): disable verifyDepsBeforeRun for the test step
pnpm-workspace.yaml sets verifyDepsBeforeRun: install, so every script gets a re-install side effect before it runs. test.yml's standard e2e job sets pnpm_config_verify_deps_before_run: false to disable this; without that override the auto-reinstall seems to leave the loader in the state that triggers jest's "module is already linked". Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
397a400d29 |
ci(pacquet): call .test through the test-pkgs-all script chain
pnpm's hidden-script guard fires when \`pn -r .test\` is the top-level shell command but not when the same call lives inside another script. Driving the run through \`pn run test-pkgs-all\` (which then chains into \`pn -r .test\` internally) is the call shape main CI uses successfully. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
5d24e2e7cd |
ci(pacquet): drop --filter so pn -r can call the hidden .test script
Adding --filter=pnpm re-enabled pnpm's hidden-script guard and broke the invocation with ERR_PNPM_HIDDEN_SCRIPT. Recursive mode bypasses that guard on its own; --if-present silently skips workspace packages that don't define .test, so the unfiltered form does the right thing. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
5a04ddb96b |
ci(pacquet): invoke .test via pn -r so the call shape matches test.yml
test.yml's standard e2e job drives the same jest config successfully via \`pn -r .test\`, which executes the hidden \`.test\` script under pnpm's spawn path. Running jest directly (pnpm exec, ./node_modules/.bin/jest, etc.) hits the \`module is already linked\` VM-modules regression for reasons that aren't clear yet but seem tied to that spawn path. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
6dce0847d4 |
ci(pacquet): run tsgo --build from pnpm/
The repo root has no tsconfig.json — tsgo needs to start from pnpm/tsconfig.json so it can walk the project references and build all the workspace packages the e2e tests import. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
1e606e2fdf |
ci(pacquet): run jest under pnpm's pinned runtime to match the standard e2e job
Earlier attempts to drive the suite under the runner's own Node (or a separately-installed Node from actions/setup-node) all tripped jest's "module is already linked" VM-modules regression. The working pattern test.yml uses for the standard e2e job is to pin a runtime through pnpm/setup and run jest via `pn` so it inherits that Node — the suite already passes there on 22.13.0. PNPM_E2E_BIN points at pacquet, so `dist/pnpm.mjs` is never read; we can skip the bundle step (and with it the `pnx node@runtime:24.6.0` collision) and run only `tsgo --build` to produce the lib/ outputs the tests import from. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
815e8f6d42 |
ci(pacquet): pass --runInBand so jest's VM-modules loader stays in one process
With --experimental-vm-modules and multiple worker processes the same module ends up linked twice and every suite errors with "module is already linked". The base config's maxWorkers: 1 keeps file execution sequential but still spawns worker processes; --runInBand forces the main process to do everything, which is what the VM-modules path expects. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
01fcfccc50 |
ci(pacquet): resolve the jest shim from the workspace root
pnpm hoists the jest .bin shim to the monorepo root rather than to pnpm/, so the previous relative path 404'd. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
4564af4c24 |
ci(pacquet): exec the jest shim directly so PATH Node 22.13.0 runs it
`node $(which jest)` tried to feed Node the shell shim and crashed with SyntaxError. Calling the shim directly lets it `exec node` against PATH, which is the pinned 22.13.0 — the same trick that side-steps pnpm exec's embedded Node 24.x. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
c941f6ab12 |
ci(pacquet): run jest under the pinned Node, not pnpm exec's embedded one
`pnpm exec jest` was still hitting the "module is already linked" regression because pnpm spawns under its own embedded Node 24.x regardless of what `actions/setup-node` puts on PATH. Resolve the jest script path through pnpm but invoke it directly with `node`, so the pinned Node 22.13.0 is the one that actually runs the suite. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
e37b694951 |
ci(pacquet): pin Node 22.13.0 on PATH for the jest step
The runner image's Node 24.x patches trip jest's ESM VM-modules path with "module is already linked", failing all suites before any test runs. Bundling still needs the runner's Node so `pnx node@runtime:24.6.0` can resolve, so the pin is added after `pn compile-only` and only affects the test step. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
3e138137cb |
ci(pacquet): use the runner Node so pnx node@runtime:24.6.0 can resolve
Pinning the action's `runtime:` to Node 22.13.0 broke `pn bundle`'s `pnx node@runtime:24.6.0 bundle.ts` call with ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND — the on-demand runtime fetch collides with the pre-installed one. ci.yml's compile job already demonstrates the working pattern: leave runtime unset, drive compilation through `pn compile-only`. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
e9f7c1ed76 |
test(pnpm): cast skipIfPacquet back to typeof test
`test.skip` is declared on jest's `ItBase` interface, which omits the `.each`, `.only`, etc. accessors that callers reach for. The runtime delegate exposes them, so a cast keeps the helper's call sites type-safe without changing behavior. Same for the describe variant. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
8986f536d9 |
test(pnpm): restore missing test import and tighten skipIfPacquet typing
Fixes the pre-push type-check failures from the previous commit: - `run.ts` still has one upstream-passing `test(...)` call, so the `test` import must stay. - `skipIfPacquet` / `describeSkipIfPacquet` need explicit `typeof test` / `typeof describe` annotations so their generated declarations don't reach into `@jest/types` types that aren't exported by name. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
19d5d714c7 |
test(pnpm): run e2e suite against pacquet via PNPM_E2E_BIN
The pnpm CLI e2e tests already exercise the bundled `pnpm.mjs` end-to-end, so they can serve as a parity probe for the Rust port too. A new `PNPM_E2E_BIN` env var swaps the spawned binary in `execPnpm`/`execPnpmSync`/ `spawnPnpm`, and a `skipIfPacquet` helper gates the per-test cases that the port doesn't yet pass. Every failing test in the existing suite was marked from a local sweep against `target/release/pacquet`; pacquet-CI gets a new job that builds the binary, builds the pnpm bundle, and runs jest with the env var set so future regressions surface in CI. Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
a5a2c2482e |
fix(pacquet/store-dir): gate verify_file's destructive branch on cas_write_lock (#11825)
The verifier was racing in-flight writers: while `ensure_file` held `cas_write_lock(path)` and was partway through `write_all`, a sibling snapshot's `check_pkg_files_integrity` would stat the same CAS path (no lock), see a partial size, and call `remove_stale_cafs_entry(path)`. The writer's `write_all` then continued against an orphan inode, the writer's `cas_paths` was populated with the now-deleted path, and `link_file` later hit ENOENT — the CI failure shape on #11816 / 0.2.2-7. Fix (Option C): keep `verify_file`'s lock-free fast path (the common case: file unchanged since prior install, `is_modified` false), but acquire `cas_write_lock(path)` before any branch that could call `remove_stale_cafs_entry`. Re-`check_file` under the lock so a writer's full `write_all` lands before we evaluate. Performance: the fast path adds zero overhead. The slow path — files whose mtime is > 100 ms past the recorded `checked_at` — takes one uncontended Mutex acquire per file, sub-microsecond on uncontended locks. Contention only fires when a writer + a verifier hit the same blob simultaneously; the wait is bounded by one `write_all` and trades a millisecond-scale wait for avoiding a network re-fetch. The new integration test in `pacquet-store-dir/tests/` is a deterministic reproducer: it acquires `cas_write_lock` from the test thread (standing in for an in-flight writer), pre-seeds a partial CAS file at the matching path, and runs the verifier in the background. Pre-fix, the verifier unlinks the file while the "writer" is still simulated as in-progress; post-fix, the verifier blocks on the lock until released. To make `cas_write_lock` reachable from the store-dir crate the function was promoted from `fn` to `pub fn` in pacquet-fs. |
||
|
|
f9a0abe02d |
test(pacquet/fs): port upstream multi-process CAS stress tests (#11823)
Adds the three cross-process scenarios upstream pnpm covers in
store/cafs/test/writeBufferToCafs.test.ts but pacquet only covered
intra-process (one 32-thread test): N workers racing on the same
target with a corrupt pre-seed, with a truncated pre-seed, and on a
clean target.
Pacquet's `cas_write_lock` is process-local (`OnceLock<DashMap<...>>`)
just like upstream's `locker: Map<string, number>`, so the cross-
process safety contract lives entirely in `O_CREAT | O_EXCL` +
atomic-rename. The existing 32-thread test in `ensure_file::tests`
exercises the lock; the new suite exercises the unprotected
filesystem-only path so a regression in the `verify_or_rewrite` +
`write_atomic` recovery would surface as a test failure instead of a
production install failing to import a CAS file at link time.
Approach:
- New `[[bin]]` `cafs_stress_worker` under `pacquet-fs/src/bin/`.
Reads a content fixture from argv[1] and a target path from
argv[2], calls `ensure_file`, exits 0/1. Tiny and test-only;
`pacquet-fs` is `publish = false` so an extra bin target is free.
- New integration test `pacquet-fs/tests/ensure_file_stress.rs`.
Uses `env!("CARGO_BIN_EXE_cafs_stress_worker")` to find the bin
Cargo builds alongside the test, then spawns 8 instances per
scenario via `std::process::Command`.
- Each scenario then asserts every worker exited 0 and the final
on-disk content sha-512-matches the expected payload.
Each recovery test was verified to catch a regression: temporarily
bypassing `verify_or_rewrite` flips both recovery tests red while
the clean-target test (which doesn't pre-seed anything) stays
green, matching upstream's coverage shape.
|
||
|
|
1386b5987d |
feat(pacquet): honor preferFrozenLockfile in the install dispatch (#11824)
`pacquet install` (no flag) didn't consult `preferFrozenLockfile`. A fresh lockfile got re-resolved from the registry instead of taking the cheap frozen path, and a stale lockfile was silently overwritten without seeding the resolver from the existing pins. Closes pnpm/pnpm#11815. The install dispatch now has four ordered states: 1. `--frozen-lockfile` flag → frozen path (lockfile required, freshness check fatal). 2. No flag + lockfile present + effective `preferFrozenLockfile == true` + freshness check passes → frozen path (same code as state 1). 3. No flag + lockfile present + opt-out or stale → fresh-resolve, seeded from the existing lockfile's snapshots so unrelated pins survive the rewrite (mirrors upstream's `update: false` resolver mode). 4. No lockfile → fresh-resolve with no seed. `check_lockfile_freshness` is the shared helper: it runs `pnpm.overrides` parsing, `check_lockfile_settings`, the overrides-aware manifest re-apply, and `satisfies_package_manifest`. State 1 surfaces its `Err` as `InstallError`; state 2 treats a stale-lockfile `Err` as fall-through and surfaces `InvalidOverrides` as fatal. CLI exposes `--prefer-frozen-lockfile` / `--no-prefer-frozen-lockfile` mirroring pnpm so users can override per invocation; `pacquet add` opts out of the fast path explicitly since the manifest is necessarily stale by the time the install dispatch runs. |
||
|
|
5881b57115 |
feat(pacquet): honor enableGlobalVirtualStore in the fresh-resolve install path (#11819)
`pacquet install` (no flag, fresh project) was hardcoded to `VirtualStoreLayout::legacy`, so it materialized packages under the project-local `node_modules/.pnpm/` even when `enableGlobalVirtualStore: true` was configured. Closes pnpm/pnpm#11814. A new `build_lockfile_view_from_resolver_graph` adapter converts the resolver's `DependenciesGraph` into the `snapshots:` / `packages:` shape `VirtualStoreLayout::new` already consumes, so all hashing, slot-dir, and bin-linker code is shared with the frozen-lockfile path. The without-lockfile flow now also calls `register_project` against the shared store when GVS is on, mirroring the frozen-lockfile branch. Side effect: aligns the without-lockfile path's per-package save directory with the peer-suffixed slot the children-recursion was already using, so peer-suffixed snapshots no longer split between two unreachable slot directories. |
||
|
|
8695496f58 |
feat(pacquet): write pnpm-lock.yaml and <vsd>/lock.yaml on fresh install (#11816)
* feat(pacquet): write pnpm-lock.yaml on fresh install Adds a `dependencies_graph_to_lockfile` adapter that converts the resolver's `DependenciesGraph` into a v9 `Lockfile`, hooks it into the install path so a fresh `pacquet install` produces a wanted lockfile, renames `InstallWithoutLockfile` to `InstallWithFreshLockfile` to match the new behavior, drops the `UnsupportedLockfileMode` branch from the dispatch, and flips the `config.lockfile` default from `false` to `true` to match pnpm. Closes pnpm/pnpm#11813. * chore(pacquet): fix rustdoc + rustfmt CI failures - Drop the broken `[ResolvedPackage.optional]` intra-doc link (no such item in scope; the reference is upstream's, and the prose is enough without the link). - Disambiguate two `dependencies_graph_to_lockfile` intra-doc links to the function form so rustdoc doesn't error on the function/module collision. - Flatten an `assert!(prod.contains_key(...))` so rustfmt + dylint agree on the body (rustfmt wanted the call wrapped, dylint wanted a trailing comma; extracting the key into a local resolves both). * feat(pacquet): write <virtual_store_dir>/lock.yaml in the fresh path After a fresh install, also persist the current-lockfile alongside `pnpm-lock.yaml` so the next install can diff each snapshot against it and skip the unchanged slots — the same optimization the frozen path already enables. `InstallWithFreshLockfile::run` now returns an `InstallWithFreshLockfileResult` that surfaces the freshly-built `Lockfile` to the caller. `install.rs` saves it as `<virtual_store_dir>/lock.yaml` after `.modules.yaml` succeeds, mirroring the frozen path's safety property: a manifest-write failure can't leave a current-lockfile pointing at incomplete install state. The wanted lockfile and the current lockfile describe the same resolved graph here — the resolver only walked what the install requested, so no `filter_lockfile_for_current` step is needed. Both writes are gated on `config.lockfile`, matching upstream pnpm's `useLockfile` opt-out. * feat(pacquet): propagate optional flag from ResolvedPackage to SnapshotEntry `SnapshotEntry.optional` was hard-coded to `false`, so every snapshot looked "non-optional" and `BuildModules` would treat any build failure as fatal even for packages reachable only via `optionalDependencies`. Port upstream pnpm's `ResolvedPackage.optional` propagation: 1. `ResolvedPackage` (the dedup envelope) gains an `optional: bool` field. The walker seeds it from `wanted.optional || parent.optional` on the first visit and AND-folds it with `current_is_optional` on every subsequent visit, so a single non-optional path flips it back to `false` and keeps it there. Mirrors resolveDependencies.ts:1625-1630. 2. `extract_children` now emits a per-child `is_optional` flag — `true` when the entry came from the package's `optionalDependencies` map. 3. `resolve_dependency_tree` and `resolve_importer` look up the importer's `optionalDependencies` set and tag each direct dep, then propagate the flag down the recursion. 4. `DependenciesGraphNode` carries the resolved package's `optional` field so peer-variants of the same `pkgIdWithPatchHash` share it. 5. The lockfile adapter writes `SnapshotEntry.optional = node.optional` instead of hard-coding `false`. Tests: - 4 unit tests on the resolver (direct optional seed, transitive inheritance, AND-fold when a non-optional path exists, transitive via a parent's `optionalDependencies` edge). - 1 unit test on the adapter (`SnapshotEntry.optional` round-trips). - 1 integration test through `Install` asserting a top-level `optionalDependencies` entry produces `optional: true` in the written `pnpm-lock.yaml`. Each test was verified to catch a regression by temporarily breaking the implementation before re-checking it green. * fix(pacquet): fail fast when fresh install gets node_linker: hoisted or --no-runtime The fresh-lockfile dispatch silently dropped `skip_runtimes` and `node_linker` — neither was forwarded into `InstallWithFreshLockfile`, so a fresh `pacquet install` with `--node-linker=hoisted` produced an isolated `node_modules` layout, and `--no-runtime` materialized runtime archives anyway. Pacquet's hoist pass and runtime-snapshot filter both run only against a loaded lockfile (frozen-lockfile path); honoring the flags on a fresh install needs upstream's per-snapshot filter and a hoist pass over the freshly-built graph. Both ports are out of scope for this PR. Refuse the unsupported combinations up front instead, before any reporter event fires or any state file is written, so a follow-up retry under `--frozen-lockfile` against an existing lockfile lands on the supported path. Adds `UnsupportedFreshInstallNodeLinker` and `UnsupportedFreshInstallSkipRuntimes` error variants plus integration tests asserting both that the error fires and that no `pnpm-lock.yaml`, `lock.yaml`, or `.modules.yaml` ends up on disk. * chore(pacquet): broaden fresh-install-flag error messages to non-frozen installs The `UnsupportedFreshInstall*` errors fire on any `!frozen_lockfile` run, not just first installs — pacquet doesn't honor an existing lockfile without `--frozen-lockfile` yet (stale-lockfile rewrite is a follow-up). So a re-install with a pinned `pnpm-lock.yaml` but no `--frozen-lockfile` would hit the same gate, and the "on a fresh install yet" wording pointed users at the wrong condition. Reword the display strings to "without --frozen-lockfile yet" so the prompt matches the actual gate. Variant names and diagnostic codes stay put — the next round will rename them along with the fresh-install-path semantics. Surfaced by coderabbitai review on #11816. |
||
|
|
22d6742960 |
fix(pacquet): resolve catalog: in pnpm.overrides before freshness check (#11820)
The frozen-lockfile freshness check compared the lockfile's overrides map (with `catalog:` already expanded by pnpm) against the raw config map (still containing `catalog:` strings), so every catalog-backed override surfaced as `ERR_PNPM_OUTDATED_LOCKFILE` on every install. Mirror pnpm's `parseOverrides(overrides, catalogs)` → `createOverridesMapFromParsed` pipeline: thread `&Catalogs` through `parse_overrides[_iter]`, resolve each value via `resolve_from_catalog`, and flatten the resolved entries into the map handed to `check_lockfile_settings`. |
||
|
|
501681044e | chore(release): 11.2.2 (#11817) v11.2.2 | ||
|
|
f279d77d60 |
fix(pacquet/cli): patch --version literal during release build (#11812)
The `pacquet --version` string is a hardcoded clap attribute in `cli_args.rs`. It didn't get bumped for the 0.2.2 release, so the published binary still reports 0.2.1. Bump the literal to 0.2.2 and add a release-workflow step that rewrites the attribute from `inputs.version` before `cross build`, so future releases stay correct automatically. A trailing `grep -F` fails the job loudly if the regex stops matching after a future refactor of the attribute. |
||
|
|
881a86541b |
fix(installing.commands): forward pnpm install flags to pacquet (#11781)
* fix(installing.commands): forward `pnpm install` flags to pacquet When the install engine is delegated to pacquet via configDependencies, pnpm hard-coded the args to `install --frozen-lockfile --reporter=ndjson` and silently dropped the user's other CLI flags. `pnpm install --no-runtime` therefore still installed the workspace's runtime devDependency, clobbering the Node version the surrounding tooling had set up — visible as the `Verify Node version` failure on PR #11765 where setup-pnpm provisions Node 24.0.0 but pacquet then materializes node 24.6.0. Pacquet's `install` subcommand already mirrors pnpm's surface for the common flags (`--no-runtime`, `--prod`, `--dev`, `--no-optional`, `--node-linker`, `--offline`, `--prefer-offline`, `--cpu`/`--os`/`--libc`). Forward the user's argv verbatim when the command is `install`/`i`; `add`/`update`/`dedupe` still don't forward — their flag surfaces don't line up with pacquet's `install`. * fix(installing.commands): pass --ignore-manifest-check to pacquet `pnpm up` / `add` / `remove` were aborting with `pacquet_package_manager::outdated_lockfile` whenever pacquet was declared in `configDependencies`. After resolving and writing the updated lockfile, pnpm hands materialization off to pacquet but hasn't yet written the post-mutation `package.json` — that write happens after `mutateModules` returns. Pacquet's frozen-lockfile freshness gate then saw the new lockfile paired with the pre-mutation manifest and refused to install. Pass pacquet's new `--ignore-manifest-check` flag (pacquet PR #11811) on every delegation. The flag is narrow: it only skips `satisfies_package_manifest`. Settings drift like `overrides` is still enforced, and pnpm already re-validated the lockfile before delegating, so re-checking the manifest here was redundant work that only ever fired false positives on the mutate-then-materialize path. Requires a pacquet release that ships the flag; bump `PACQUET_VERSION` in `pnpm/test/install/pacquet.ts` once it does, or the existing e2e tests will fail against pacquet 0.2.2-9 (which doesn't recognize the flag and clap would reject). Closes #11797. --- Written by an agent (Claude Code, claude-opus-4-7). * fix: update pacquet in tests * fix(installing.commands): strip positionals + always-injected flags when forwarding to pacquet `collectForwardedFlags` checked `argv[0] === 'install'` to find the command token to strip. Any global flag the user typed before `install` (e.g. `--config.registry=...` in the e2e test) shifted the token out of position, so the function returned the full argv and pacquet saw `install` twice — `error: unexpected argument 'install' found`. Use the parsed argv that `@pnpm/cli.parse-cli-args` already produced: `remain` lists positionals (the `install`/`i` token and nothing else on this code path, since `isInstallCommand` is only true when no package params are present), and `original` preserves the user's exact tokens. Drop positionals + the flags we always inject (`--reporter=ndjson`, `--frozen-lockfile`, `--ignore-manifest-check`) so clap doesn't reject duplicates either. `original` over `cooked` deliberately: nopt's `cooked` splits `--key=value` into two tokens, which would break pacquet's `--config.<key>=<value>` parser (it requires the `=` form). * fix(installing.commands): make argv.cooked/remain optional on InstallCommandOptions Widening these to required broke test fixtures elsewhere (publish/pack/ deprecate/dist-tag/deploy) that construct minimal `argv: { original }` options for code paths that never reach pacquet. Only the pacquet delegation actually reads `remain`, so make the two new fields optional on the shared options type and supply a default at the runPacquet call site. The runtime path through main.ts already populates all three. * fix(installing.commands): strip any user-supplied --reporter when forwarding to pacquet Pacquet's `--reporter` is a clap value option with last-value-wins semantics, so `pnpm install --reporter=silent` (or `--reporter silent` two-token form) reached pacquet and overrode the `--reporter=ndjson` pnpm injects, breaking the NDJSON-to- streamParser plumbing the default reporter depends on. The previous filter only matched the exact `--reporter=ndjson` token. Walk argv with a lookahead so both `--reporter=<value>` and `--reporter <value>` are dropped without consuming an adjacent flag. * fix(installing.commands): drop negated/value forms of always-injected flags `collectForwardedFlags` only matched the exact positive tokens `--frozen-lockfile` and `--ignore-manifest-check`, so a user typing `pnpm install --no-frozen-lockfile` (or `--frozen-lockfile=false`) forwarded the negation to pacquet, which then saw both our injected `--frozen-lockfile` and the user's `--no-frozen-lockfile` and crashed clap with "unexpected argument". Match every shape the user can write the same flag in: positive, `--no-` negated, and any `=value` form. Can't blindly strip `--no-` either way — pacquet has flags whose literal name starts with `no-` (`--no-runtime`, `--no-optional`); those must still forward. The user's `--no-frozen-lockfile` intent is honored upstream — pnpm did a fresh resolve before delegating; pacquet's role here is just lockfile-driven materialization, which is always frozen. * fix(installing.commands): match positionals by index, hide reporter from dropped-flags warning `collectForwardedFlags` matched positionals via `new Set(argv.remain)`, which strips by value: a flag value that happened to equal a positional token (e.g. `pnpm install --node-linker install`) was wrongly dropped from the forwarded list, costing pacquet the value of `--node-linker`. Walk `argv.original` with a subsequence pointer into `argv.remain` so only the actual positional indexes get skipped. `collectDroppedFlags` still surfaced `--reporter foo` / `--reporter=foo` in the "may not be honored" warning on `add`/`update`/`dedupe`, but pnpm honors reporter selection itself before delegation — so the warning was misleading. Route both helpers through the same `isAlwaysInjected` check and consume `--reporter` and its value the same way `collectForwardedFlags` already does. |
||
|
|
0dd1ec445c |
feat(pacquet): add --ignore-manifest-check to skip frozen-lockfile manifest gate (#11811)
Surfaces a narrow CLI flag on `pacquet install` that gates only `satisfies_package_manifest`. Settings-drift checks (`overrides`, `ignoredOptionalDependencies`, …) still fire, and the broader `--ignore-package-manifest` name is reserved for a future port of pnpm's `pnpm fetch` semantics (which skip linking / hoisting / pruning too). Intended for the pnpm CLI's `configDependencies` delegation path (issue #11797): pnpm resolves and writes the lockfile, then hands materialization to pacquet but hasn't yet written the post-mutation `package.json`. With the flag set, the freshness gate skips the per-importer manifest check that would otherwise reject every `pnpm up` / `add` / `remove` with `ERR_PNPM_OUTDATED_LOCKFILE`. The matching pnpm-side change to forward the flag lands separately. Refs #11797. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
3a6392828c |
fix(pacquet/config): pick the store on the project's volume (#11804)
* fix(pacquet/config): pick the store on the project's volume Port pnpm's `getStorePath` / `storePathRelativeToHome` cross-volume detection to pacquet's `default_store_dir`. When no `storeDir` is explicitly set (global config.yaml, pnpm-workspace.yaml, or `PNPM_CONFIG_STORE_DIR`), `Config::current` now probes whether the project root can be hardlinked into the user's pnpm home dir. If not, it walks from the filesystem root toward the project to find the volume mount point and falls back to `<mountpoint>/.pnpm-store` — matching pnpm's behaviour at https://github.com/pnpm/pnpm/blob/29a42efc3b/store/path/src/index.ts#L14-L78. Before this fix, a workspace on a separate volume (e.g. `/Volumes/src/` on macOS) installed into the home-volume store (`~/Library/pnpm/store`). When the home volume is case-insensitive and the workspace volume is case-sensitive, typescript-eslint's path-cache canonicalises against the home store and then can't locate the same files in TypeScript's case-sensitive program loaded from the workspace, so `eslint --fix` fails with "TSConfig does not include this file" on every project file. The hardlink probe goes through a new `LinkProbe` capability in `pacquet-config::api`, threaded into `Config::current`'s `Sys` bound so tests can pin the linkability answer without touching disk. The production `Host` impl performs real link attempts via `store_path::host_can_link_between_dirs`. Test fakes get an inert `LinkProbe` impl via the `inert_link_probe!` macro added to the `tests` module — every probe returns `false`, the algorithm walks without finding a mount, and the SmartDefault home store survives unchanged, so existing cascade assertions stay valid. * fix(pacquet/config): satisfy rustdoc + Windows clippy - Drop the redundant explicit `[LinkProbe][crate::api::LinkProbe]` link target and fully qualify `[StoreDir]` as `[pacquet_store_dir::StoreDir]` so the `Doc` job passes under `-D rustdoc::broken-intra-doc-links` and `-D rustdoc::redundant-explicit-links`. - Gate the `PrefixProbe` fake, its `ALLOW_PREFIXES` static, and the related imports with `#[cfg(unix)]`. Every consumer is already `#[cfg(unix)]`, so on Windows the items were unreferenced and the `Lint and Test (windows-latest)` job's clippy run failed under `-D dead-code`. * test(pacquet/config): serialize PrefixProbe scenarios with a scenario lock `ALLOW_PREFIXES` was only locked for the read/write of its `Vec`, not across "set allowlist then probe" — under nextest's default parallel execution two scenarios could race: scenario A would set its prefixes, scenario B would overwrite them, and A's `resolve_store_dir` would observe B's allowlist. Add `PREFIX_PROBE_SCENARIO_LOCK` and a `PrefixProbe::with_allow(prefixes, body)` helper that holds it across the entire set-and-probe so scenarios serialise cleanly. Per CodeRabbit review on pnpm/pnpm#11804. * test(pacquet/config): rename single-letter `R` to `Output` for Perfectionist Dylint's `perfectionist::single-letter-generic` flagged the `R` return-type parameter on `PrefixProbe::with_allow`. Rename to `Output`. |
||
|
|
09b7e9d354 |
docs: widen pacquet parity scope to all dep-management commands (#11810)
Update the scope caveat in AGENTS.md to state that pacquet's parity surface now covers `install`, `add`, `update`, and `remove`, rather than `install` alone. |
||
|
|
86bf5db996 |
feat(pacquet/cli): accept --config.<key>=<value> overrides (#11806)
When pnpm delegates `install` to the pacquet binary it forwards the user's `pnpm install` flags verbatim, including pnpm's `--config.<key>=<value>` universal syntax (handled by npm-conf upstream). Pacquet's clap parser previously rejected those tokens with `unexpected argument '--config.registry'`, causing the delegated install to fail. Strip every `--config.*` token out of argv before clap parses it and layer the values onto `Config` after `.npmrc`/yaml have been applied, mirroring pnpm 11's `CLI > yaml > .npmrc > defaults` precedence. Only `registry` is wired in for now (matches the immediate test gap); unknown keys are accepted silently so a pnpm-side key pacquet hasn't ported yet doesn't break the delegation. Malformed tokens (`--config.foo`, `--config.=value`) are dropped on the same path so clap never sees them either. |
||
|
|
69f8ea8de4 |
feat(pacquet): port blockExoticSubdeps to reject exotic subdeps (#11792)
Rejects git/tarball/file resolutions reached transitively from the
importer when `blockExoticSubdeps` is on. Direct deps remain allowed.
Mirrors pnpm's gate at
|
||
|
|
213136dec3 |
feat(pacquet/resolving-npm-resolver): port abbreviated metadata fast path (#11794)
* feat(pacquet/resolving-npm-resolver): port abbreviated metadata fast path Default the resolver-time fetch to the abbreviated install-v1 packument (`application/vnd.npm.install-v1+json`) and only request full metadata when the call explicitly needs it. Mirrors pnpm's `fullMetadata = opts.optional || ctx.fullMetadata` derivation in [pickPackage.ts L201](https://github.com/pnpm/pnpm/blob/2a9bd897bf/resolving/npm-resolver/src/pickPackage.ts#L201). - Adds `full_metadata` to the cached/non-cached fetchers and routes the Accept header + mirror dir (`ABBREVIATED_META_DIR` vs `FULL_META_DIR`) off it. - `PickPackageContext.full_metadata` is the install-wide bias; `PickPackageOptions.optional` forces full per-call (pnpm#9950). In-memory cache key gains a `:full` suffix when full so the two modes can coexist without contamination. - Ports `maybeUpgradeAbbreviatedMetaForReleaseAge`: when `published_by` is active, the picker ends up with an abbreviated packument that lacks per-version `time`, and the package's top-level `modified` falls past the cutoff, the orchestrator re-fetches full metadata so the maturity check runs on real timestamps. The upgraded full meta is persisted back to the abbreviated mirror so the next install skips the upgrade fetch. - Verifier and resolver pass the appropriate flag explicitly (`true` for the verifier, `false` for the resolver and named registry resolver). Tests cover the abbreviated default, the optional override, the cache-key separation, the boundary case `modified == cutoff`, and the upgrade trigger when `modified > cutoff`. --- Written by an agent (Claude Code, claude-opus-4-7). * docs(pacquet/resolving-npm-resolver): disambiguate fetch_full_metadata intra-doc link `fetch_full_metadata` is both a function and a module, so the bare `[fetch_full_metadata]` link triggers `rustdoc::broken_intra_doc_links` under `--deny warnings`. Add parentheses so it resolves to the function. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
400b21a90f |
feat(pacquet): port pnpm-workspace.yaml overrides support to the install chain (#11793)
* feat(pacquet): port pnpm.overrides support to the install chain
Adds a new `pacquet-config-parse-overrides` crate (port of
`@pnpm/config.parse-overrides`), threads `overrides` through
`Config`/`WorkspaceSettings`, surfaces lockfile-side drift as
`StalenessReason::OverridesChanged` (matching upstream's
`getOutdatedLockfileSetting` overrides branch), and applies the parsed
overrides to a cloned root manifest before the frozen-lockfile
freshness check so post-override lockfile specifiers line up with the
on-disk manifest. The read-package-hook port (`VersionsOverrider`)
mirrors upstream's `createVersionsOverrider` minus the peer-arm
promotion, which is deferred until peer install lands. Catalog refs in
override values surface as `INVALID_OVERRIDES` until catalogs are
ported.
* chore(pacquet): satisfy Dylint Perfectionist lints and fix stale doc link
Renames single-letter closure / function / generic params introduced
by the overrides port to descriptive names, fixes trailing-comma
policy in test macro invocations, swaps the Windows path literal to
a raw string, and removes a stale `[`Self::root_dir`]` rustdoc link
left behind when the `root_dir` field was dropped from
`VersionsOverrider`.
* style(pacquet): apply rustfmt to install.rs overrides_map binding
* fix(pacquet/overrides): address review feedback
- `parse_overrides` doc no longer claims insertion-order behavior; it
accurately states that `HashMap` iteration is unordered and points
ordered-output callers at `parse_overrides_iter`.
- `WorkspaceSettings::apply_to` now collapses `overrides: {}` from a
later layer (env overlay, repeat `apply_to`) to `None` on `Config`,
so an explicit empty map clears an earlier non-empty assignment
instead of silently being skipped. Adds a regression test for the
env-overlay-clears-yaml shape.
- `sort_by_specificity` widens its comparator to a 3-way result so
Rust's `sort_by` total-order precondition holds. The strict
Less/Greater arms keep the sort outcome identical to upstream's
first-match choice; the `Equal` arm covers mutually-intersecting
ranges.
- `resolve_local_override_spec` routes the absolute-path and
diff-paths-fallback branches through `normalize_path` too, so
Windows `\` separators get rewritten to `/` for every `link:` /
`file:` shape (not just the diff-paths success branch).
|
||
|
|
667e587392 |
feat(pacquet): attach patch hashes to resolved pkg ids (#11791)
* feat(pacquet): attach patch hashes to resolved pkg ids Thread `patchedDependencies` into the tree walker so each matched package's `pkgIdWithPatchHash` gains the `(patch_hash=<hash>)` suffix upstream's `resolveDependencies.ts` produces. The peer resolver concatenates the peer suffix onto the patched id, so the install layer's depPath-keyed lookups land on the patched virtual-store slot without further changes. Surfaces `ResolvedTree::applied_patches` for the post-walk `ERR_PNPM_UNUSED_PATCH` check, and propagates `ERR_PNPM_PATCH_KEY_CONFLICT` from `get_patch_info` through the resolver error surface. * docs(pacquet): drop private-item intra-doc links The two `pub` items pointed at `resolve_node`, which is private — fine under `--document-private-items` but rejected when CI runs `cargo doc` with `-D rustdoc::private-intra-doc-links`. Rephrase to plain prose; the call site is obvious from the surrounding context. * style(pacquet/resolving-deps-resolver): drop trailing comma in assert! Dylint's `perfectionist::macro_trailing_comma` rejects a trailing comma on a single-line macro invocation. |
||
|
|
71dfccce9a |
feat(pacquet): port workspace: protocol resolution and publish-time rewrite (#11789)
* feat(pacquet): port workspace: protocol resolution and publish-time rewrite
Ports the `workspace:` family of bare specifiers end-to-end:
- `pacquet-workspace-spec` (new) ports `workspace/spec-parser`'s
`WorkspaceSpec` parser + `toString`.
- `pacquet-workspace-range-resolver` (new) ports
`workspace/range-resolver`'s `resolveWorkspaceRange` — `*`/`^`/`~`/`""`
pick the highest version with prereleases included; other inputs
follow standard semver range rules.
- `pacquet-resolving-npm-resolver` grows two helpers from the upstream
npm-resolver: `workspace_pref_to_npm` (port of `workspacePrefToNpm.ts`)
and `try_resolve_from_workspace` (port of `tryResolveFromWorkspace` +
`tryResolveFromWorkspacePackages` + `pickMatchingLocalVersionOrNull` +
`resolveFromLocalPackage`). `NpmResolver::resolve_impl` now intercepts
`workspace:` specs before the npm pick, deferring `workspace:./` /
`workspace:../` to the local resolver. Emits `link:` / `file:`
(injected) lockfile resolutions, with the matching
`WORKSPACE_PKG_NOT_FOUND` / `NO_MATCHING_VERSION_INSIDE_WORKSPACE`
/ `CANNOT_RESOLVE_WORKSPACE_PROTOCOL` error codes preserved.
- `pacquet-exportable-manifest` (new) ports the publish-time
`replaceWorkspaceProtocol` and `replaceWorkspaceProtocolPeerDependency`
helpers from `releasing/exportable-manifest`. The full
`createExportableManifest` (catalog rewrite, jsr rewrite, pre-pack
hooks, publishConfig overrides) lands as pacquet ports the surrounding
commands.
- `Install::run` builds a workspace-packages map via
`find_workspace_projects` when a `pnpm-workspace.yaml` is present and
threads it through `ResolveOptions::workspace_packages` so the
resolver chain can satisfy `workspace:` specs from local projects in
the no-lockfile install path.
Test ports:
- `workspace/spec-parser/test/workspace-spec.test.ts`
- `workspace/range-resolver/test/index.test.ts`
- `resolving/npm-resolver/test/workspacePrefToNpm.test.ts`
- `releasing/exportable-manifest/test/index.test.ts` (workspace cases)
- plus new unit tests for `try_resolve_from_workspace` covering
`WORKSPACE_PKG_NOT_FOUND`, `NO_MATCHING_VERSION_INSIDE_WORKSPACE`,
the inject branch, and the `publishConfig.directory` /
`linkDirectory` handling.
Frozen-lockfile installs already record `link:` entries directly; the
new resolution path matters for the no-lockfile install path.
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet): dylint + doc-link nits in workspace-protocol port
- Rename single-letter params (perfectionist::single-letter-*).
- Add trailing commas in multi-line macro invocations.
- Avoid the ambiguous `crate::parse_bare_specifier` doc link and the
cross-crate `pacquet_workspace_spec::WorkspaceSpec` /
`pacquet_workspace_range_resolver::resolve_workspace_range` doc
links (the crates don't have a Cargo dependency on each other, so
rustdoc can't resolve them).
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet): address coderabbit review comments
- replace_workspace_protocol_peer_dependency: use replacen("workspace:", "", 1)
so compound peer specs match upstream JS String.replace's first-only
semantics; locked with a new test
peer_workspace_strip_only_removes_first_occurrence.
- npm-resolver module docs: drop the stale "workspace: returns
Ok(None)" bullet and explain that non-path workspace specs now route
through try_resolve_from_workspace while path-relative forms still
fall through to the local resolver.
The third coderabbit nit (read_workspace_manifest error swallowing in
install.rs) was already addressed by the rebase: the workspace manifest
is now read once at the top of Install::run with proper error
propagation, and build_workspace_packages_map takes the pre-loaded
Option<&WorkspaceManifest> instead of re-reading the file.
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet/resolving-npm-resolver): surface WorkspacePkgNotFound.hint
Upstream pnpm's WORKSPACE_PKG_NOT_FOUND error carries a 'hint' field
that PnpmError prints as guidance after the message ('Packages found in
the workspace: ...'). The Rust port was populating the field but the
miette diagnostic didn't reference it, so the help text never reached
the user. Add help("{hint}") to the diagnostic attribute so miette
renders it under the message — matching pnpm's output verbatim.
---
Written by an agent (Claude Code, claude-opus-4-7).
|
||
|
|
b2a95fa1f7 |
feat(pacquet): port catalogs (types, protocol-parser, resolver, config) (#11787)
* feat(pacquet): port catalogs (types, protocol-parser, resolver, config)
Adds four new crates mirroring upstream's `catalogs/*` packages:
- pacquet-catalogs-types: `Catalog`/`Catalogs` map aliases plus
`DEFAULT_CATALOG_NAME`.
- pacquet-catalogs-protocol-parser: `parse_catalog_protocol`; folds
`catalog:` shorthand into `"default"` to match upstream.
- pacquet-catalogs-resolver: `resolve_from_catalog` with the four
upstream `PnpmError` codes (`CATALOG_ENTRY_NOT_FOUND_FOR_SPEC`,
`CATALOG_ENTRY_INVALID_RECURSIVE_DEFINITION`,
`CATALOG_ENTRY_INVALID_WORKSPACE_SPEC`,
`CATALOG_ENTRY_INVALID_SPEC`). Rust callers `match` on the result
enum directly instead of porting the TS-ergonomic
`matchCatalogResolveResult` visitor.
- pacquet-catalogs-config: `get_catalogs_from_workspace_manifest` plus
the `INVALID_CATALOGS_CONFIGURATION` mutual-exclusion check.
`pacquet-workspace`'s `WorkspaceManifest` now actually deserializes
the `catalog` and `catalogs` fields (previously dropped). The
resolver is not yet wired into the install path — `deps-installer`
hasn't been ported — but the crates are ready for that next step.
Tests are 1:1 ports of the upstream Jest suites.
* fix(pacquet): satisfy rustdoc and perfectionist lints in catalogs port
- catalogs-resolver: drop the intra-doc-link form on the
`pacquet_resolving_resolver_base::WantedDependency` reference; the
crate isn't a dependency, so rustdoc failed to resolve it under
`-D rustdoc::broken-intra-doc-links`.
- catalogs-resolver, catalogs-config: reorder the `CatalogResolutionError`
/ `InvalidCatalogsConfigurationError` derive lists to
`Debug, Display, Error, Diagnostic, Clone, PartialEq, Eq` so they
match the `prefix_then_alphabetical` rule that the CI-only
Perfectionist dylint enforces. `just ready` doesn't surface this lint
locally.
* feat(pacquet): resolve catalog: specifiers during install
Wires the catalogs port into the install path:
- `install.rs`: read `pnpm-workspace.yaml` after `find_workspace_dir`
and normalize via `get_catalogs_from_workspace_manifest` into a
`Catalogs` map. Adds `InstallError::{ReadWorkspaceManifest,
InvalidCatalogsConfiguration}` so the upstream
`ERR_PNPM_INVALID_CATALOGS_CONFIGURATION` propagates verbatim.
- `install_without_lockfile.rs`: threads the `Catalogs` map into
`ResolveDependencyTreeOptions`. (Frozen-lockfile catalog handling
needs a lockfile-snapshot pass and is a separate slice.)
- `resolve_dependency_tree`: replaces direct (importer-level)
`catalog:` bare specifiers with the catalog's recorded version
before the resolver chain dispatches. Catalog resolution does NOT
run on transitive deps, matching upstream's importer-only scope.
Misconfigured entries surface as `CatalogMisconfiguration` with
the upstream `ERR_PNPM_CATALOG_ENTRY_*` code instead of leaking
through to `SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER`.
Tests:
- deps-resolver: two new unit tests prove direct-dep rewriting and
the misconfiguration code path.
- cli (e2e): `install_resolves_catalog_protocol` runs the binary
against a workspace with a `catalog:` entry and checks the
virtual-store layout; `install_surfaces_catalog_misconfiguration`
asserts the upstream message is surfaced when the catalog has no
matching alias.
* style(pacquet): apply rustfmt to catalogs test after rebase
* fix(pacquet): rename single-letter closure param in catalogs e2e test
Perfectionist's `single_letter_closure_param` lint (CI-only via
Dylint) flagged `|c|` in the box-drawing-strip filter.
|
||
|
|
df990fdb51 |
feat(pacquet): port node/deno/bun runtime resolvers (#11783)
* feat(pacquet): port node/deno/bun runtime resolvers and wire them into the install chain
Ports the three `@pnpm/engine.runtime.*-resolver` packages and the shared
`@pnpm/crypto.shasums-file` helper into pacquet, and slots them into the
default-resolver chain so `node@runtime:<spec>`, `deno@runtime:<spec>`,
and `bun@runtime:<spec>` resolve through pacquet as they do in pnpm.
New crates under `pacquet/crates/`:
- `crypto-shasums-file` — downloads and decodes `SHASUMS256.txt`,
shared by node and bun. Mirrors `FAILED_DOWNLOAD_SHASUM_FILE`,
`NODE_INTEGRITY_HASH_NOT_FOUND`, `NODE_MALFORMED_INTEGRITY_HASH`.
- `engine-runtime-node-resolver` — `parse_node_specifier`,
`get_node_mirror`, `get_node_artifact_address`, `normalize_arch`,
`resolve_node_version[s]`, and the `Resolver`-impl entry point.
Handles the unofficial-musl mirror fan-out, the `lts` / LTS-codename
/ channel / range selectors, and the `darwin/arm64 <16 → x64`,
`win32/ia32 → x86`, `arm → armv7l` arch quirks. Error codes
`NO_OFFLINE_NODEJS_RESOLUTION`, `NODEJS_VERSION_NOT_FOUND`,
`INVALID_NODE_RELEASE_CHANNEL` match upstream.
- `engine-runtime-deno-resolver` — version selection delegates to the
npm resolver; assets come from the GitHub Releases API + per-asset
SHA256 sidecars. Windows x64 covers arm64 under emulation.
Errors: `DENO_RESOLUTION_FAILURE`, `DENO_MISSING_ASSETS`,
`DENO_GITHUB_FAILURE`, `DENO_PARSE_HASH`.
- `engine-runtime-bun-resolver` — version selection delegates to npm;
assets come from the GitHub-release `SHASUMS256.txt`. `windows` /
`aarch64` are normalised to `win32` / `arm64`. Error:
`BUN_RESOLUTION_FAILURE`.
Wiring (`install_without_lockfile.rs`): chain order is now
`npm → git → node → deno → bun`, matching upstream's
`resolving/default-resolver/src/index.ts` at
|
||
|
|
d08e9abfb9 |
fix(pacquet/lockfile): accept link: refs in snapshot dependencies (#11788)
* fix(lockfile): accept link: refs in snapshot dependencies An injected workspace package's snapshot can carry a `link:<path>` dependency value when the dep is a workspace sibling. Pnpm accepts the shape — `refToRelative` short-circuits to `null` for `link:` — but pacquet's `SnapshotDepRef` only handled the plain/alias shapes and rejected `link:` at parse time. Add a `SnapshotDepRef::Link(String)` variant mirroring `ImporterDepVersion::Link`, return `None` from `resolve` / `ver_peer` for it, and skip it at every consumer (matching upstream's `if (childDepPath)` guards in `getChildren` / `lockfileDepsToGraphChildren`). Fixes https://github.com/pnpm/pnpm/issues/11775. * test(lockfile): trim redundant doc comments on link snapshot dep tests Drop test doc-comments that restated the assertion. The test name + asserts already convey the intent; the upstream-parity reason lives at the `SnapshotDepRef::resolve` / `Link` variant docs. |
||
|
|
ee8fd0d4cf |
feat(pacquet): port auto-install-peers algorithm (#11784)
* feat(pacquet): port auto-install-peers algorithm
Replaces the placeholder peer-folding behavior with a faithful port of
pnpm's `hoistPeers` algorithm. Missing required peers are hoisted to
the importer's direct deps (shared across consumers, not nested), with
a multi-pass loop that re-resolves until no required peer remains and
the optional-peer pass picks already-available versions from the
preferred-versions map.
- New `pacquet-lockfile-preferred-versions` crate seeds the version-
picker tie-break table from manifest + lockfile snapshots.
- New `hoist_peers` module ports `hoistPeers` and
`getHoistableOptionalPeers` with the full upstream test suite.
- New `resolve_importer` orchestrator drives the multi-pass hoist
loop and threads `parent_pkg_aliases` / `all_preferred_versions`
across iterations.
- `resolve_dependency_tree` exposes `TreeCtx` / `extend_tree` /
`snapshot` so the orchestrator can extend the tree incrementally
without re-walking already-resolved subtrees.
- `auto-install-peers-from-highest-match` config setting added,
mirroring upstream's flag for range-merge behavior.
Ported from pnpm's installing/deps-resolver `resolveRootDependencies`
and hoistPeers.ts at commit
|
||
|
|
e2791ab6fe |
feat(pacquet): port named registries to the install chain (#11785)
* feat(pacquet): port named registries to the install chain Adds the user-facing `<alias>:` resolver surface so a manifest entry like `"@acme/private": "gh:^1.0.0"` resolves against GitHub Packages (or any other registry the user configures under `namedRegistries:` in `pnpm-workspace.yaml`). Mirrors upstream pnpm/pnpm@b61e268d57: - `parse_named_registry_specifier_to_registry_package_spec` parses `<alias>:[@<owner>/]<name>[@<version>]` and `<alias>:<version>` bodies. Rejects scope-without-name with `ERR_PNPM_INVALID_NAMED_REGISTRY_PACKAGE_NAME`. - `merge_named_registries` folds the user map onto pacquet's built-in aliases (`gh:` -> GitHub Packages) and validates URLs at resolver construction (`ERR_PNPM_INVALID_NAMED_REGISTRY_URL`). - `NamedRegistryResolver` is a third `Resolver` alongside the npm / jsr paths, emitting `resolved_via: "named-registry"`. Auth headers flow through the existing per-URL `.npmrc` lookup. - `Config::named_registries` reads `namedRegistries:` from `pnpm-workspace.yaml`, with `${VAR}` substitution on the values (matches upstream's `replaceEnvInStringValues`). - `install_without_lockfile` constructs the merged map once and threads it through both the resolver chain (after npm/git, so configured aliases cannot hijack built-in schemes) and `build_resolution_verifiers` (so tarball-URL prefix routing honours user aliases). * fix(pacquet): rustdoc broken links + spell-check typo - `resolving-local-resolver/src/chain.rs`: drop the redundant explicit `(crate::...)` target on `[resolve_latest_from_local]` (clippy's `redundant_explicit_links` lint flagged it under `RUSTDOCFLAGS=-D warnings`) and demote the `[contains_path_sep]` intra-doc reference to backticks since the helper is private. - `resolving-npm-resolver/src/parse_bare_specifier.rs`: rename `unparseable` to `unparsable` in a doc comment so the workspace typos check passes. * test(pacquet): cover the remaining named-registry test cases Ports upstream's [`resolving/default-resolver/test/namedRegistry.ts`](https://github.com/pnpm/pnpm/blob/b61e268d57/resolving/default-resolver/test/namedRegistry.ts) and the two `resolveNamedRegistry.test.ts` cases the previous commit hadn't covered. - New `tests/chain.rs` integration tests assert that the explicit `link:` / `workspace:` / `file:` schemes win over a colliding named-registry alias. These pin the local-scheme / local-path split: with combined `LocalResolver`, named-registry would slot *after* both halves and a `link` alias could hijack `link:./pkg`. - `resolves_via_builtin_gh_alias` covers the `gh:` happy path that was previously only exercised via a user-supplied `work:` alias. - `preserves_scoped_pkg_name_when_alias_differs` verifies that the resolver records the dependency under the registry's name, not the local manifest alias. - `user_config_overrides_builtin_gh_alias` covers the GHES override scenario where a user points `gh` at their enterprise host. 11 resolver tests pass (5 unit + 3 chain integration + 3 happy-path scenarios) plus the 12 parser cases already in place. |
||
|
|
998ea4cd37 |
fix(pacquet/lockfile): accept non-semver version slots (URLs, git refs) (#11786)
* fix(pacquet/lockfile): accept non-semver version slots (URLs, git refs) Pnpm's `parse` in `deps/path/src/index.ts` falls back to the `nonSemverVersion` branch whenever `semver.valid` rejects the version slot, so a `https://codeload.github.com/...` tarball URL or `git+...` URL parses cleanly. Pacquet's `PkgVerPeer` parser was strict and errored on anything that wasn't a semver or a `file:` path, which made the lockfile reader fail with `Failed to parse importer dependency version "https://codeload..."` on the install path. Add `VersionPart::NonSemver(String)` and route the parser's bare-version fallback through it, mirroring upstream byte-for-byte. Closes the parse failure reported in pnpm/pnpm#11776. * fix(pacquet/lockfile): add trailing commas to satisfy dylint perfectionist |
||
|
|
4b25d60f62 |
fix(pacquet/patching): apply patches without mutating the store (#11782)
Reported against pacquet's configDependencies preview engine for `msw@2.12.14`: install fails with `ERR_PNPM_PATCH_FAILED ... error applying hunk #1` even though the patch is perfectly valid. Original report (patch file + error log): [Stanzilla/21648ae644068c3aef3d18693c7c32c9](https://gist.github.com/Stanzilla/21648ae644068c3aef3d18693c7c32c9). **Root cause.** When two snapshots of the same patched package hardlink (or reflink, where the filesystem doesn't break-on-write) from the same store file, pacquet's `apply_patch_to_dir` did a plain `fs::write` — which truncates and writes through the shared inode, **corrupting the on-disk store copy** and leaking patched content into every other snapshot that linked the same file. The next worker then read already-patched content and `diffy::apply` failed on the missing `-` lines. The patched output already lives in the side-effects cache after apply returns, so nothing requires the store copy to carry it. The fix is three layered changes to `apply_patch_to_dir`: - **Atomic temp + rename for Modify** ([apply.rs](pacquet/crates/patching/src/apply.rs)). Stage the patched bytes in a sibling `.{name}.{pid}.{counter}.pacquet-tmp` opened with `create_new(true)` (no symlink-follow, no truncation of an attacker-pre-seeded path), chmod the temp to match the original *before* the rename so the final mode lands atomically, then `fs::rename` over the target. `rename` is atomic on Unix and replaces in-place on Windows, so an IO failure mid-write leaves either the original or the rewritten file — never an empty dirent. As a side effect of creating a new dirent → inode mapping, it also breaks any hardlink the path previously shared with the store. Matches the [`pacquet_lockfile::save_lockfile::write_atomic`](pacquet/crates/lockfile/src/save_lockfile.rs) pattern. - **Idempotency via reverse-dry-run** (Modify/Create/Delete). When forward apply fails, try reverse-apply against the on-disk content; if it succeeds the file is already in the post-patch state and we no-op. Mirrors upstream [`@pnpm/patch-package`'s `applyPatch`](https://github.com/ds300/patch-package/blob/master/src/applyPatches.ts) which retries `executeEffects(reversePatch, { dryRun: true })` on failure. Defense in depth for re-runs that find the directory pre-patched (side-effects-cache hit that fell through, manual edits, partial install recovery) — the hardlink-break above prevents fresh installs from ever producing that state. - **Idempotency for Create and Delete**. Create skips when the existing file already contains the post-patch content; Delete treats `NotFound` on the target as already-deleted. Equivalent fix for the TS pnpm CLI side is in [`pnpm/patch-package@c97a62e`](https://github.com/pnpm/patch-package/commit/c97a62e) (`@pnpm/patch-package` is the apply implementation pnpm imports); pnpm will pick it up once that package is released and the dep gets bumped. |
||
|
|
a8a8cbce6d |
feat(pacquet): port resolving.local-resolver (file:/link:/workspace:) (#11778)
* feat(pacquet/resolving-local-resolver): port file:/link:/workspace: resolver
Ports pnpm's @pnpm/resolving.local-resolver:
- parse_bare_specifier mirrors parseLocalScheme/parseLocalPath/fromLocal
(link:/workspace:/file: prefixes, bare path shapes, tarball-filename
detection, tilde/drive-letter handling, preserveAbsolutePaths,
injected directories get file: not link:).
- local_resolver provides resolve_from_local_scheme / _path /
resolve_latest_from_local matching upstream's three exports;
ssri-based tarball integrity for file: tarballs;
safe_read_package_json_from_dir for directories with the upstream
fallback (warn + name=basename / version='0.0.0' for missing link:
targets, LINKED_PKG_DIR_NOT_FOUND for missing file: targets,
NOT_PACKAGE_DIRECTORY for ENOTDIR + Windows stat-check) and
PATH_IS_UNSUPPORTED_PROTOCOL for path:.
- chain.rs wraps the free functions behind the Resolver trait so the
default-resolver dispatcher can compose this in alongside the npm
resolver. ResolveResult.name_ver is None for local resolutions —
the canonical name lives in the fetched manifest, not the
resolver-time signal.
17 ported tests mirror resolving/local-resolver/test/index.ts plus
3 chain-dispatch tests verifying the trait wiring. The missing-link:
target warn is emitted via tracing::warn! because pacquet's reporter
doesn't yet have a generic pnpm:logger channel.
Install-side wiring is left for a follow-up alongside the Stage-1
directory-fetcher integration: surfacing Directory resolutions to
install_without_lockfile today would only swap the
SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER error for an UnsupportedResolution
one in install_package_from_registry.
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet): satisfy doc + dylint CI
Doc:
- pacquet_directory_fetcher intra-doc link was unresolved
(resolving-local-resolver has no such dep — it's a sibling).
- LocalSpecError doc linked to crate-private parse_local_scheme /
parse_local_path.
Dylint (perfectionist):
- PkgResolutionId / WantedLocalDependency / ParseOptions /
PathProtocolNotSupportedError derive lists reordered to
prefix_then_alphabetical.
- Single-letter closure params (|p|, |s|, |v|) renamed.
- Impure expression passed to tracing::warn! bound to a let first.
- Multi-line format!/assert_eq! macro invocations gained trailing
commas; the single-line assert! shed its stray trailing comma.
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet/resolving-local-resolver): surface NOT_PACKAGE_DIRECTORY on Windows
`safe_read_package_json_from_dir` opens `<spec>/package.json` and lets
the OS error surface. On Unix that's `ENOTDIR` for a file path; on
Windows it's `NotFound`, so the resolver fell through to the
fallback-manifest branch instead of returning `NOT_PACKAGE_DIRECTORY`.
Upstream pnpm has the same gap on Windows and patches around it inside
`readProjectManifestOnly` (workspace/project-manifest-reader/src/index.ts#L100-L114
at
|
||
|
|
606ff8f648 |
feat(pacquet): port resolving/tarball-resolver (#11773)
* feat(pacquet): port resolving/tarball-resolver Adds pacquet-resolving-tarball-resolver, the Rust port of resolving/tarball-resolver/src/index.ts. The new crate claims any WantedDependency whose bare specifier starts with `http://` or `https://`, normalizes the URL through `reqwest::Url::parse`, runs a HEAD pre-flight that follows redirects, and stores the post-redirect URL in the resolution when the response carries `cache-control: immutable`. Mutable responses keep the normalized request URL. Integrity stays None at resolve time, matching upstream (integrity is stamped later in package-requester). To make the seam fit, ResolveResult.id is now an opaque `PkgResolutionId(String)` newtype in resolver-base mirroring upstream's branded string at core/types/misc.ts:59. PkgNameVer was a poor fit because tarball ids are URLs and git ids are `repo#commit` — not name@version. NpmResolver round-trips the existing PkgNameVer through PkgResolutionId::from(string); the npm-only install paths in package-manager parse the id back to PkgNameVer at their boundary (safe because the npm resolver stamps that shape). The deps-resolver alias fallback drops its `.id.name` access since the id is opaque now. Test coverage in the new crate (7 tests, mockito): http(s) claim vs decline, mutable vs immutable response, immutable-after-redirect follow, resolve_latest for http(s) vs non-http(s). * fix(pacquet/resolving-tarball-resolver): rename single-letter closure params Dylint's perfectionist::single-letter-closure-param rejects |v|; rename to |header| in the cache-control header check. * feat(pacquet/package-manager): wire TarballResolver into the install chain Insert TarballResolver after the GitResolver in install_without_lockfile's DefaultResolver chain so http(s):// bare specifiers actually route through the new resolver. Order mirrors upstream's chain (npm → git → tarball → local/...). * fix(pacquet/resolving-tarball-resolver): forward wanted alias and resync lockfile - Echo `wanted_dependency.alias` on the `ResolveResult` so a spec like `"foo": "https://.../bar.tgz"` preserves `foo` as the install name downstream. Matches the npm and git resolvers' convention even though upstream TS doesn't surface alias on `ResolveResult` (downstream consumers fall back to `wantedDependency.alias` over there). - Drop a stray blank line in resolve.rs that cargo fmt rejected. - Record the new `package-manager -> resolving-tarball-resolver` edge in Cargo.lock so `cargo build --locked` succeeds on CI. |
||
|
|
35d5440ce1 |
feat(pacquet): port resolving/git-resolver and wire it into the install chain (#11779)
* feat(pacquet): port resolving/git-resolver and wire it into the install chain Adds `pacquet-resolving-git-resolver`, the Rust port of pnpm's `@pnpm/resolving.git-resolver`. Recognises GitHub / GitLab / Bitbucket shortcut forms and full `git+ssh:` / `git+https:` / `ssh:` / plain `https://…/repo.git` URLs, runs `git ls-remote` to pin the commit (partial commit search, annotated-tag dereference, semver-range matching), and emits either a git-hosted tarball resolution or a `Git{repo,commit}` resolution. Production runners shell out to the system `git` binary via `tokio::task::spawn_blocking` and use the install-wide `ThrottledClient` for the HEAD probe. Widens the resolver-base contract so URL-shaped IDs fit: adds a `PkgResolutionId` newtype (rule-3 branded string, infallible `From<String>`/`From<&str>`/`From<&PkgNameVer>`), changes `ResolveResult.id` to that type, and adds `name_ver: Option<PkgNameVer>` so callers that need the structured `name@version` form keep working. npm-resolver fills both fields; git-resolver leaves `name_ver` `None` (the install path that consumes git resolutions hasn't landed yet, so those call sites panic with a TODO message until then). `DefaultResolver` now implements `Resolver` too (returns `Ok(None)` when no resolver in the chain claims), letting `resolve_dependency_tree` accept the chain directly. The install-side wiring in `install_without_lockfile.rs` constructs `DefaultResolver::new(vec![Box::new(npm_resolver), Box::new(git_resolver)])` with `RealGitProbe` / `RealGitRunner`, mirroring upstream's `createResolver` chain order. Test coverage: 51 unit tests in the new crate, including the full SCP-style URL repair matrix ported from `parsePref.test.ts` and the GitLab `/-/archive/` tarball regression for pnpm #11533. Full workspace `cargo nextest run` is green at 1635 tests. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet): satisfy dylint perfectionist + rustdoc on git-resolver port * Reorder `#[derive(...)]` on `PkgResolutionId` to match the `prefix_then_alphabetical` rule the dylint Perfectionist lint enforces (`From` last after `Serialize`/`Deserialize`). * Add `()` to function intra-doc links that collide with same-named modules (`create_git_hosted_pkg_id`, `parse_bare_specifier`) so rustdoc's `broken-intra-doc-links` lint stops treating them as ambiguous. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet): satisfy Perfectionist dylint lints on git-resolver port CI's `just ready` doesn't surface Perfectionist (it runs only as a dedicated dylint job on a nightly toolchain). Fixes: * Rename single-letter generics `P`/`R` → `Probe`/`Runner` on `GitResolver`, `PartialSpec::finalize`, `from_hosted_git`, and `resolve_ref`. * Rename single-letter closure / function / let-binding params (`s`/`h`/`c`/`p`/`i`/`g`/...) to descriptive names. * Replace Unicode ellipsis (`…`, U+2026) with ASCII `...` in comments. * Add trailing commas to multi-line `assert_eq!` / `assert!` invocations, and remove the stray trailing comma on a single-line one. Also fix follow-on JSR-resolver test cases that still read `result.id.{name,suffix}`: switch them to `result.name_ver.as_ref()...` to match the post-widening `ResolveResult` shape. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet): address PR review on git-resolver port * Replace the two `.expect()` calls on `ResolveResult.name_ver` in the install path with `.ok_or_else()` that surfaces a typed error: `InstallPackageFromRegistryError::UnsupportedResolution` and a new `InstallWithoutLockfileError::UnsupportedInstallResolution`. Now that the git resolver is in the chain, a git/tarball/local resolution reaching the without-lockfile install path returns an error end-to-end instead of panicking. Add a regression test pinning the contract. * Make `percent_decode` (in `hosted_git.rs`) and `percent_decode_str` (in `parse_bare_specifier.rs`) UTF-8 aware: collect decoded bytes into a `Vec<u8>` and reassemble via `String::from_utf8`, falling back to the original input on malformed UTF-8 (matches Node's `decodeURIComponent` throwing a `URIError` that upstream's `try/catch` swallows). The byte→`char` cast was corrupting any multi-byte sequence (e.g., `%E2%80%A6` → ellipsis); regression test added. --- Written by an agent (Claude Code, claude-opus-4-7). * chore(pacquet): drop unused UnsupportedInstallResolution after rebase Main's `feat(pacquet): peer-dependency resolution stage` reworked `install_without_lockfile.rs` to derive the virtual-store name from the resolved depPath via `pacquet_deps_path::dep_path_to_filename` instead of reading `result.name_ver`. That removed the `.expect()` / `.ok_or_else()` site this error variant was added for; with no remaining callers, drop the dead variant. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
a1f91e1770 |
feat(pacquet): peer-dependency resolution stage (#11774)
* feat(pacquet): peer-dependency resolution stage Ports pnpm's `resolvePeers` algorithm to pacquet's `resolving-deps-resolver` crate and wires it into `install_without_lockfile`. The depPath-keyed `DependenciesGraph` produced by the new stage replaces the flat `(name, version)` keying the install pass used to drive virtual-store slot names from. New `pacquet-deps-path` crate ports the `@pnpm/deps.path` helpers (`createPeerDepGraphHash`, `depPathToFilename`, balanced-paren suffix scan). `ResolvedTree` now carries both the flat dedup map (`packages`) and the per-occurrence tree (`dependencies_tree`, keyed by a new `NodeId`) so two parents sharing the same package can compute different peer suffixes — the whole point of the stage. Cycles in the tree pass are broken by skipping the cycled edge entirely (matches upstream's `parentIdsContainSequence` gate in `buildTree`), and peer-resolution falls back to `name@version` peer-ids when re-entering an in-progress node. Three upstream optimisations are intentionally not ported in this slice: `peersCache`, the `purePkgs` fast path, and `graph-cycles`-driven async deferment. Each is correctness-preserving — the algorithm produces the same depPaths, just without the short-circuits. `autoInstallPeers` keeps its existing "fold peers into the regular walk" behavior until the full `hoistPeers` algorithm lands. * fix(pacquet/deps-path): satisfy Doc / Dylint / Spell Check CI checks - Dylint perfectionist::single-letter-let-binding: rename `i` → `cursor` in `suffix_index.rs`. - Dylint perfectionist::macro-trailing-comma: drop trailing comma in the `dep_path_to_filename` `file:` test. - rustdoc broken-intra-doc-links: link `[`DependenciesTree`]` to the newly-exported `pacquet_resolving_deps_resolver::DependenciesTree` type alias and downgrade the `pacquet_lockfile::PkgNameVerPeer` mention in `pacquet-deps-path`'s module doc to plain text (the crate deliberately doesn't depend on `pacquet-lockfile`). - rustdoc fn-vs-mod ambiguity: prefix [`resolve_dependency_tree`], [`resolve_peers`], and [`create_peer_dep_graph_hash`] doc-links with `fn@` so they bind to the function rather than the same-named module. - typos: rename `unparseable` → `unparsable` in a test name. * fix(pacquet/resolving-deps-resolver): satisfy Dylint perfectionist lints - `derive_ordering`: reorder `#[derive(...)]` on `DepPath` and `NodeId` to match the configured `prefix_then_alphabetical` style — `PartialOrd, Ord, Hash` rather than `Hash, PartialOrd, Ord`. See `dylint.toml`'s `[perfectionist::derive_ordering]` prefix list for the canonical order. - `single_letter_function_param`: rename `s` → `value` on `impl From<String> for DepPath`. * fix(pacquet): address PR review feedback Round of correctness fixes flagged by CodeRabbit and qodo-code-review on #11774. - **`dep_path_to_filename_unescaped`**: guard against empty / single-byte input. The previous `trimmed.as_bytes()[1..]` slice panicked when `trimmed.len() < 2`; the function now short-circuits to `trimmed.to_string()` in that case, mirroring upstream's `indexOf` with `fromIndex = 1` returning -1 on out-of-range scan. - **`extract_children` / `extract_peer_dependencies` mismatch**: the child walker added every `peerDependencies` entry when `auto_install_peers` was on, but the peer-resolution stage's `peerDependenciesWithoutOwn` filter skips peer names also in `dependencies` / `optionalDependencies`. `BTreeMap` collection of the result by alias could silently drop the optional edge for a name that appeared in both. Fix: collect `optionalDependencies` into children too, and dedupe peer entries against own deps before appending. - **Dropped peer edges in `graph_children`**: when a peer pointed at a later sibling direct dep (e.g., manifest order `{ react-dom: …, react: … }`), the peer's `DepPath` wasn't in `node_dep_paths` yet at the time the parent's `graph_children` was built, so the symlink edge was silently dropped. Install would then walk react-dom's slot without finding react. Add `pending_peer_edges` + a `patch_pending_peer_edges` post-pass that runs after every direct dep is walked, with regression test. - **Aliased child peer misfilter**: the "is this peer one of my own children?" check compared the peer alias against `tree_node.children` keys, but `children` is keyed by install alias while peers can match by real name via the dual-keyed `ParentRefs`. Switch both filter sites to compare by `NodeId` so an npm-aliased child satisfying a peer is correctly classified as internal. - **`DepPath` newtype consolidation**: `PeerId::DepPath(String)` collapsed the depPath brand. Move the `DepPath` newtype from `resolving-deps-resolver::dependencies_graph` into the lower-level `pacquet-deps-path` crate so the `PeerId` enum carries the branded type instead of `String`. `resolving-deps-resolver` re-exports `DepPath` from `pacquet_deps_path` to keep existing imports working. - **Prerelease-tolerant semver match**: `node-semver`'s `Range::satisfies` rejects prereleases against non-prerelease comparators (`18.0.0-rc.1` vs `^18.0.0` returns `false`). Add a fallback in `satisfies_with_prereleases`: if the straight check fails and the candidate is a prerelease, retry against the stripped `MAJOR.MINOR.PATCH` base. Approximates Yarn's `satisfiesWithPrereleases` for the cases pnpm cares about without importing the full per-comparator algorithm. The `tracing::warn!` peer-issue emission flagged by qodo and the deeper prerelease semantics gap (per-comparator) remain documented follow-ups; the slice's `resolve_peers.rs` module doc lists what's intentionally not ported in this PR. * fix(pacquet): apply cargo fmt to peer-resolution slice Three sites where the previous edits left non-canonical wrapping: - `create_peer_dep_graph_hash.rs` test calls now fit one line. - `lib.rs` re-export ordering (alphabetical). - `tests.rs` whitespace. CI Format check is `cargo fmt --all -- --check`; the local pre-push hook didn't catch these because the edits landed via `Edit` without a follow-up `cargo fmt`. * fix(pacquet/resolving-deps-resolver): drop trailing comma rustfmt re-added `cargo fmt` collapsed the regression test's `assert_eq!` onto one line and kept the trailing comma, which tripped the same `perfectionist::macro_trailing_comma` rule that #11774's earlier commit fixed elsewhere. The lint forbids a trailing comma on single-line macro calls; rustfmt leaves it alone. Drop the comma to satisfy both. |
||
|
|
f807f6d402 |
feat(pacquet): port JSR specifier parser and wire resolve_jsr (#11772)
* feat(pacquet/resolving-npm-resolver): port JSR specifier parser and wire resolve_jsr Adds a new `pacquet-resolving-jsr-specifier-parser` crate that ports `@pnpm/resolving.jsr-specifier-parser` (`parseJsrSpecifier` + `JsrSpec`), mirroring upstream's `ERR_PNPM_MISSING_JSR_PACKAGE_SCOPE` / `ERR_PNPM_INVALID_JSR_PACKAGE_NAME` / `ERR_PNPM_INVALID_JSR_SPECIFIER` error codes 1:1. Wires the parser into the npm resolver: - `parse_jsr_specifier_to_registry_package_spec` adapter alongside `parse_bare_specifier`, matching upstream's same-file shape in `resolving/npm-resolver/src/parseBareSpecifier.ts`. - `NpmResolver::resolve_impl` detects `jsr:` early and routes to a new `resolve_jsr_impl` that picks against `registries['@jsr']` (with the `https://npm.jsr.io/` `DEFAULT_REGISTRIES` fallback) and stamps `resolved_via = "jsr-registry"` plus `alias = spec.jsr_pkg_name`. - Extracts a shared `pick_from_registry` helper so npm and JSR paths share the picker invocation; `build_resolve_result` now takes a `resolved_via` parameter. Ports the 6 upstream parser test cases and adds adapter + resolver integration tests covering the JSR happy path, default-tag fallback, and parser-error propagation. Ports https://github.com/pnpm/pnpm/blob/05dd45ea82/resolving/jsr-specifier-parser/src/index.ts and https://github.com/pnpm/pnpm/blob/1627943d2a/resolving/npm-resolver/src/index.ts. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pacquet/resolving-jsr-specifier-parser): treat empty alias as missing in version-only specs Upstream's `parseJsrSpecifier` guards the version-only branch with the truthy check `if (!alias)`, so an empty string falls into the `INVALID_JSR_SPECIFIER` arm. Pacquet's `let Some(alias) = alias else` form only triggers on `None`, so `Some("")` was instead carried into `jsr_to_npm_package_name` and surfaced `MISSING_JSR_PACKAGE_SCOPE`. Filter empties out before the destructure so both sides agree on the error code. Also tighten the resolver-level integration test for the unscoped-name case to assert the upstream-defined error message instead of a generic "JSR" substring. Reported by CodeRabbit on https://github.com/pnpm/pnpm/pull/11772. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
11a43b15da | chore(release): 11.2.1 (#11777) v11.2.1 | ||
|
|
2061c55b2a |
fix(env-installer): mark optional config subdep snapshots with optional: true (#11770)
Match how optional packages are recorded elsewhere in pnpm-lock.yaml so non-host platform variants pulled in via a config dep's optionalDependencies aren't treated as required. |