mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 09:55:39 -04:00
37669c200d3fff2db20a032bc092ed7dc797be45
11696 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
37669c200d |
fix: avoid workspace state parse crashes (#12094)
* fix: avoid workspace state parse crashes * fix(workspace-state): write workspace state atomically Port the atomic-write half of the pnpm fix for #12020 to pacquet. pacquet's install/add/update/remove all call update_workspace_state, and the on-disk file is shared with the pnpm CLI, so a non-atomic fs::write could leave a torn file that a concurrent `pnpm run` reads and crashes on. Write to a temp file in the same directory and rename it into place, mirroring pnpm's switch to write-file-atomic. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
118e9be809 |
fix: set user agent in headless lifecycle scripts (#12092)
Co-authored-by: morning-verlu <258725120+morning-verlu@users.noreply.github.com> |
||
|
|
719cc21c6f |
fix(audit): prune path traversal (#12087)
* fix(audit): prune path traversal * fix(audit): memoize placeholder set before recursion to preserve cycle reachability The reachable-vulnerabilities getter returned a non-memoized empty Set for back-edges, causing incomplete results for nodes in dependency cycles. Memoize the result Set immediately so the same mutable placeholder is returned for back-edges and filled as recursion unwinds. * fix(audit): only memoize acyclic reachability subtrees The placeholder-before-recursion approach only made the SCC entry node's reachable set correct; non-entry cycle members were memoized with an under-approximated set, dropping valid audit paths reached through them. Cache a node's reachable vulnerabilities only when no descendant back-edges to an ancestor; recompute cycle-touching nodes per query. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
0a4d6656c9 |
feat(pacquet): implement the update command (#12102)
* feat(package-manager): implement the `update` command in pacquet Port pnpm's `update` (aliases `up`/`upgrade`) onto pacquet's always-fresh-resolve install path. - Compatible bump: withhold matched names' lockfile pins from the preferred-versions seed (new `UpdateSeedPolicy`) so they re-resolve to highest-in-range; `package.json` is left untouched. This is what distinguishes `update` from `install`. - `--latest`: fetch each matched direct dep's `latest` tag and rewrite the manifest range (`^v`, or exact under `--save-exact`), like `add`. - Selectors: bare-name/glob patterns (`depth>0`, no `--latest`) match every locked package name at any depth; versioned (`foo@2`) or `--latest` selectors match direct deps only; `--latest` + spec is rejected with `ERR_PNPM_LATEST_WITH_SPEC`. - CLI flags: `-L/--latest`, `-E/--save-exact`, `-P/-D/--no-optional` (faithful `makeIncludeDependenciesFromCLI`), `--depth`, `--lockfile-only`, `-i/--interactive` (dialoguer + inline outdated check). - `--global` and `--workspace` error out for now: the global-dir and workspace-version-linking subsystems are not ported yet. The resolver's `UpdateBehavior` tri-state and the npm resolver's `include_latest_tag` already existed; this change drives them from a CLI command. Written by an agent (Claude Code, claude-opus-4-8). * feat(package-manager): port more pnpm update tests + fix selector parsing Reviewing pnpm's `update` test suite surfaced gaps: - Fix `parse_update_param` to match pnpm's `parseUpdateParam`: search for the version `@` starting at index 2 for `!`-negated patterns (1 otherwise), so `!@scope/pkg-*` is no longer wrongly split into pattern `!` + version `scope/pkg-*`. Negation selectors now work. - Add `--no-save`: the range rewrites still drive resolution (lockfile updates) but `package.json` is not persisted. Mirrors pnpm's `updatePackageManifest: opts.save !== false`. - Add `ERR_PNPM_NO_PACKAGE_IN_DEPENDENCIES`: a selector that matches no direct dependency under `--depth 0` (without `--latest`) now errors, matching pnpm; `--latest` with an unmatched selector is a no-op. Ports the negation-pattern, `--no-save`, and no-package-in-deps tests from pnpm's `installing/commands/test/update/update.ts`. Written by an agent (Claude Code, claude-opus-4-8). * perf(package-manager): avoid per-snapshot String alloc in update seed filter `UpdateSeedPolicy::DropOnly`'s snapshot filter allocated a `String` for every lockfile snapshot key. Parse the (small) update-target set to `PkgName` once and compare against `key.name` directly. Addresses a review comment on #12102. Written by an agent (Claude Code, claude-opus-4-8). |
||
|
|
577a90f819 |
feat(pacquet): --resume-from, --report-summary (#12093)
* feat(cli): port recursive run with --resume-from and --report-summary Port pnpm's `pnpm run -r` (recursive run) to pacquet, including the `--resume-from` and `--report-summary` flags, which previously existed only in the TypeScript CLI. - `pacquet -r run <script>` now runs the script in every workspace project in topological order, mirroring pnpm's runRecursive: discover projects, build the inter-project dependency graph, sort it into chunks via graph_sequencer (the port of sortProjects), and execute. - `--resume-from <pkg>` drops every chunk before the one containing <pkg>, mirroring getResumedPackageChunks; an unknown package fails with ERR_PNPM_RESUME_FROM_NOT_FOUND. - `--report-summary` writes pnpm-exec-summary.json with the per-package status (queued/running/passed/skipped/failure) and duration, nested under an executionStatus key, mirroring writeRecursiveSummary. - `--no-bail` keeps running after a failure (recursive runs bail by default). Failures surface ERR_PNPM_RECURSIVE_FAIL, or ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL when bailing; a run that matches no script fails with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT. A new executor helper, execute_shell_with_status, returns the child's exit status so per-package pass/fail can be recorded; execute_shell is unchanged. Not yet ported (noted in the module): --no-sort, --reverse, --workspace-concurrency parallelism, --filter narrowing of the selected set, and the RegExp script selector. The selected set is every workspace project, matching pacquet's currently-unfiltered install. Integration tests port the upstream resume-from and report-summary cases from exec/commands/test/runRecursive.ts. https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ * test(cli): gate recursive-run tests to unix and add macro trailing commas Fix two CI failures on the recursive-run integration tests: - Dylint (`perfectionist::macro_trailing_comma`): add the trailing comma to the four multi-line `assert!` invocations. - Lint and Test (windows-latest): the shared helpers are used only by the Unix-gated tests, so on Windows they tripped `dead_code` under `-D warnings`. Gate the whole file with `#![cfg(unix)]` (the build scripts run through pacquet's `sh -c` executor anyway), matching the single-package `run` tests. https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ * test(cli): cover recursive-run bail summary and no-script branches Fill the two coverage holes in the recursive-run handler: - bail + report-summary: the first failing script writes the summary, then aborts with ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL; a package that sorts after the failure stays `queued`. - no-script: a recursive run for a script no package defines fails with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, and `--if-present` turns that into a clean no-op. https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ * test(cli): cover the bail path without --report-summary The bail tests always passed --report-summary, leaving the report-summary-off side of the bail block (recursive.rs:136) uncovered. Add a test for a failing script with bail on and no --report-summary: it still fails with ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL and writes no summary file. Verified with cargo llvm-cov that recursive.rs now has no missing lines. https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ * fix(cli): run each recursive-run script from its own package root Recursive run spawned every package's script via `sh -c` without setting the working directory, so scripts ran in pacquet's process CWD (the workspace root) instead of their own package root. That breaks scripts relying on relative paths and diverges from pnpm, whose `runLifecycleHook` runs with `pkgRoot` as the working directory. - Give `execute_shell_with_status` a `current_dir` argument (factored through a private `spawn_shell` helper); `execute_shell` keeps its inherited-CWD behavior, so its callers are unchanged. - Pass each package's root as the script's working directory. - Make the marker-based recursive-run tests cwd-sensitive: scripts now write a relative `ran.txt`, and the tests assert it lands under each package root (and not at the workspace root), so a wrong-CWD regression fails the suite. https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
54d2b57000 |
feat(pnpr): server-accelerated installs via pnprServer (endpoints + client + CLI) (#12077)
## What Adds an opt-in **`pnprServer`** setting that offloads the slow part of an install — dependency resolution and computing which files the local store is missing — to a [pnpr](https://github.com/pnpm/pnpm/tree/main/pnpr) server, which streams back only the missing files. `node_modules` is still linked **locally** from the server-produced lockfile (like server-side rendering: the compute runs remotely, the result is materialized locally). Realizes the agent concept from [RFC #9](https://github.com/pnpm/rfcs/pull/9), reworked around how it's actually used and rewritten in Rust on pacquet + pnpr. ## How it works 1. `pacquet install` (with `pnprServer` set) handshakes the server — `GET /-/pnpr` — to negotiate a protocol version. 2. It `POST`s `/v1/install` with the project's dependencies, the integrities already in its store, and **its own registry config** (default `registry`, `namedRegistries`, `overrides`, `minimumReleaseAge`). 3. The server resolves against *those* registries, fetches any uncached packages into its store, and streams NDJSON: `D` (missing file digests), `I` (pre-packed store-index entries), `L` (lockfile + stats). 4. The client downloads the missing files from `/v1/files` (gzip binary), writes them into its CAFS **by digest** (no re-hashing), writes the index entries, and runs a frozen install to link `node_modules` from the server's lockfile. ## Pieces - **Server (`pnpr`)** — `GET /-/pnpr` handshake + `POST /v1/install` (NDJSON) + `POST /v1/files` (gzip), additive and opt-in alongside the npm-compatible API. Resolves against the client-sent registries, interning a `&'static Config` per distinct client config to bound the leak. - **Client (`pacquet-pnpr-client`)** — `PnprClient`: reads store integrities, negotiates the protocol version, sends the registry config, parses the stream, materializes files + index entries, returns the lockfile. Rejects unrequested file entries and repairs truncated CAFS files. - **CLI** — the `pnprServer` setting (`--pnpr-server`, `pnprServer:` in `pnpm-workspace.yaml`, `PNPM_CONFIG_PNPR_SERVER`). When set, `pacquet install` routes through the client and then links locally — pnpm's `install()` → `installFromPnpmRegistry` shape. `trustPolicy: no-downgrade` is refused (the server can't enforce it), matching pnpm's `TRUST_POLICY_INCOMPATIBLE_WITH_AGENT`. ## Design notes - **A distinct URL, not the registry.** The server resolves from the registries the client sends, so it's a compute service — not "a registry that resolves from itself" — which is why it's a separate `pnprServer` URL rather than reusing `registry`. The same server works for any client's registry setup, and a single pnpr can be both registry and `pnprServer`. - **Handshake = version negotiation + fail-fast.** Explicit opt-in, so there's no silent fallback to local resolution; a non-pnpr server (404) or a version mismatch errors clearly. - **Naming:** everything is `pnpr`; "agent" survives only in upstream citations (`@pnpm/agent.client`, the pnpm-agent PoC, pnpm's `TRUST_POLICY_INCOMPATIBLE_WITH_AGENT` error code). ## Tests - `pacquet-pnpr-client`: resolve + download, multi-file package, warm-store no-op, and handshake rejection. The pnpr server's own uplink is left at the default, so resolution provably uses the **client-sent** registry. - `pacquet-cli`: a real `pacquet install --pnpr-server <url>` against an in-process pnpr (resolving from the mocked fixtures registry) links `node_modules`. - `pnpr`: `/v1/files` binary-framing round-trip + handshake route. Full suites green; clippy / dylint (Perfectionist) / fmt / taplo / `cargo doc -D warnings` clean. ## Deferred Auth/credential forwarding (so private/scoped registries resolve), `pacquet add` / `remove` via `pnprServer`, multi-project workspaces, and true streaming (responses are buffered today). Refs https://github.com/pnpm/rfcs/pull/9 |
||
|
|
1db05c6fca |
fix: inconsistent resolution of a peer shared through a diamond (#12081)
* fix: inconsistent resolution of a peer shared through a diamond When a package peer-depends both another package and one of that package's own peer dependencies (e.g. @typescript-eslint/eslint-plugin peer-depends both @typescript-eslint/parser and typescript, and @typescript-eslint/parser peer-depends typescript), pnpm reused a hoisted instance of the shared peer that was resolved against a different version, producing an inconsistent resolution. Close #12079 * test: cover the peer-diamond resolution in pnpm and pacquet Add @pnpm.e2e/peer-diamond-* fixtures modeling #12079 (plugin peer-depends both parser and ts; parser peer-depends ts) and integration tests on both stacks. The pnpm test guards the fix; the pacquet test confirms pacquet already resolves the diamond consistently (its merge always prefers the node's own child). * docs: fix grammar in changeset (peer-depends on both) |
||
|
|
d99b725878 |
chore: license pnpr and pnpm-agent under PolyForm Shield 1.0.0 (#12082)
* chore(pnpr): license under PolyForm Shield 1.0.0 Relicense the pnpr/ subtree (the pnpm-compatible registry server) from MIT to the source-available PolyForm Shield License 1.0.0. The rest of the monorepo stays MIT. pnpr may be run, modified, and self-hosted for any purpose except providing a product that competes with it. - Add pnpr/LICENSE.md (PolyForm Shield 1.0.0). - Override the inherited workspace MIT in the pnpr crates via license-file. - Point the @pnpm/pnpr npm wrapper at the bundled LICENSE.md. - Note the carve-out in the root README (the root LICENSE stays pristine MIT so license detection keeps recognizing it). * chore(agent): license pnpm-agent under PolyForm Shield 1.0.0 Relicense the pnpm-agent server (agent/server) from MIT to the source-available PolyForm Shield License 1.0.0, matching pnpr. The @pnpm/agent.client package stays MIT so the agent protocol remains openly implementable. - Add agent/server/LICENSE.md (PolyForm Shield 1.0.0). - Set the package license to "SEE LICENSE IN LICENSE.md". - Exempt pnpm-agent from meta-updater's MIT normalization via a SOURCE_AVAILABLE_PKGS set, so lint:meta stays green. - Note the carve-out in the agent/server README + add a changeset. pnpm-agent is only a devDependency of the pnpm CLI, so no source- available code ships in the MIT-licensed CLI artifact. * docs(license): add contribution terms with relicensing grant for pnpr and pnpm-agent Contributions to the source-available trees (pnpr/, agent/server) are accepted under the same PolyForm Shield License plus a grant letting the licensor relicense them under other terms. This preserves the option to later relax to a more permissive source-available license or offer a separate commercial license without per-contributor consent. - Add pnpr/CONTRIBUTING.md and agent/server/CONTRIBUTING.md. - Point to them from each tree's README license section. * docs(license): add npm trademark/non-affiliation notice to pnpr and pnpm-agent State that pnpr and pnpm-agent are not affiliated with or endorsed by npm, Inc., GitHub, or Microsoft, and that "npm" is used only to describe registry-protocol compatibility. Also add a License section to the published @pnpm/pnpr npm wrapper README. |
||
|
|
394ee27e09 | feat(tarball): support remote https-tarball direct dependencies (#12076) | ||
|
|
e2f801e4ec |
chore(cargo): bump tar from 0.4.45 to 0.4.46 (#12074)
Bumps [tar](https://github.com/composefs/tar-rs) from 0.4.45 to 0.4.46. - [Release notes](https://github.com/composefs/tar-rs/releases) - [Commits](https://github.com/composefs/tar-rs/compare/0.4.45...0.4.46) --- updated-dependencies: - dependency-name: tar dependency-version: 0.4.46 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
13b1b9aaa2 |
chore(rust/dylint): upgrade perfectionist to 0.0.0-rc.17 (#12070)
Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
55ba310835 |
fix(pnpr): drop version floor on pnpr workspace deps so releases resolve (#12073)
The release workflow injects the dispatched version into pnpr's crate version field. Because the root workspace declared the pnpr and pnpr-fixtures path deps with a version = "0.0.1" floor, injecting any version below 0.0.1 or a prerelease (e.g. 0.0.0-26052901) made cargo fail to resolve pacquet-testing-utils, which depends on pnpr via the workspace. None of these crates is published to crates.io (the binary ships to npm), so the version floor served no purpose. Drop it to match every pacquet workspace path dep, which carries no version requirement. |
||
|
|
248223fa44 |
test(exe): fall back to copy when seeding sandbox binary across drives (#12072)
GitHub's Windows runners place the workspace (and node) on D: while os.tmpdir() is on C:, so hardlinking process.execPath into the temp sandbox fails with EXDEV. The seed binary's identity is irrelevant to the assertions, so copy when the link would cross volumes. |
||
|
|
c5d9d3a8f3 |
refactor(pnpr): rename pnpm-registry to pnpr (#12069)
* refactor(pnpr): rename pnpm-registry to pnpr Rename the registry server across the board to match the npm wrapper package name, which was already `@pnpm/pnpr`. - crate `pnpm-registry` -> `pnpr`, `pnpm-registry-fixtures` -> `pnpr-fixtures` - binaries `pnpm-registry` -> `pnpr`, `pnpm-registry-prepare` -> `pnpr-prepare` - module paths and log targets `pnpm_registry::*` -> `pnpr::*` - binary-locating env vars `PNPM_REGISTRY_BIN` -> `PNPR_BIN`, `PNPM_REGISTRY_PREPARE_BIN` -> `PNPR_PREPARE_BIN` - top-level directory `registry/` -> `pnpr/` (crates, npm wrapper, fixtures) The registry-mock storage concept is intentionally left as-is: `PNPM_REGISTRY_MOCK_PORT`/`PNPM_REGISTRY_MOCK_STORAGE`/`PNPM_REGISTRY_STORAGE`, the `~/.cache/pnpm-registry/storage` path + benchmark cache keys, and the external `pnpm-registry-mock` npm package referenced in test fixtures. * style(pnpr): rustfmt import grouping after rename * ci(pnpr): point typos at pnpr instead of removed registry dir * chore(pnpr): update pre-push path filter from registry to pnpr |
||
|
|
36e6a27066 |
chore(cargo): bump sysinfo from 0.39.1 to 0.39.2 (#12059)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.39.1 to 0.39.2. - [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/main/CHANGELOG.md) - [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.39.1...v0.39.2) --- updated-dependencies: - dependency-name: sysinfo dependency-version: 0.39.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
4024f13741 | chore: only run expensive Rust pre-push checks when pacquet or registry change (#12050) | ||
|
|
3db9dcdaf3 |
chore(cargo): bump serde_json from 1.0.149 to 1.0.150 (#12063)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.149 to 1.0.150. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150) --- updated-dependencies: - dependency-name: serde_json dependency-version: 1.0.150 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b741d91e67 | chore(release): 11.5.0 (#12068) v11.5.0 | ||
|
|
e56b126299 |
chore(cargo): bump getrandom from 0.3.4 to 0.4.2 (#12060)
Bumps [getrandom](https://github.com/rust-random/getrandom) from 0.3.4 to 0.4.2. - [Changelog](https://github.com/rust-random/getrandom/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-random/getrandom/compare/v0.3.4...v0.4.2) --- updated-dependencies: - dependency-name: getrandom dependency-version: 0.4.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
9daea1ecb0 |
chore(cargo): bump bcrypt from 0.17.1 to 0.19.1 (#12062)
Bumps [bcrypt](https://github.com/Keats/rust-bcrypt) from 0.17.1 to 0.19.1. - [Commits](https://github.com/Keats/rust-bcrypt/compare/v0.17.1...v0.19.1) --- updated-dependencies: - dependency-name: bcrypt dependency-version: 0.19.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b5b1cd6e62 |
chore(cargo): bump tower-http from 0.6.8 to 0.6.11 (#12064)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.6.8 to 0.6.11. - [Release notes](https://github.com/tower-rs/tower-http/releases) - [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.6.8...tower-http-0.6.11) --- updated-dependencies: - dependency-name: tower-http dependency-version: 0.6.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
faf848434b |
feat(pacquet/cli): --filter, --filter-prod (initial) (#12000)
* feat(pacquet/cli): add `--filter` / `--filter-prod` and workspace project filtering
Port pnpm's `@pnpm/workspace.projects-graph` and
`@pnpm/workspace.projects-filter` as the new
`pacquet-workspace-projects-graph` and `pacquet-workspace-projects-filter`
crates, and expose the `--filter` / `--filter-prod` CLI flags (stored into
`Config::filter` / `Config::filter_prod`).
- `parse_project_selector` parses selector strings (name glob, `...`
dependents/dependencies, `^` exclude-self, `!` exclude, `{dir}`, `[since]`).
- `create_projects_graph` computes inter-project edges via `workspace:`,
semver version/range, and local-path resolution.
- `filter_workspace_projects` resolves selectors against the graph;
`filter_projects` builds the graph(s) and applies a `WorkspaceFilter` list.
The `[since]` changed-packages selector parses but is rejected with
`FilterError::UnsupportedDiffSelector` (git-diff project selection is not
ported). As with `--recursive`, the install still materializes every
workspace importer in one shared pass, so narrowing the install to the
selected projects remains a follow-up; the `known_failures` hoist stubs for
`--filter` selected-projects installs are unchanged.
Ports the upstream `parseProjectSelector` and `filterWorkspaceProjects` tests;
the diff-based cases are stubbed as `known_failures`. Updates
plans/TEST_PORTING.md.
https://claude.ai/code/session_01BsrsKRRp5ntwqnTB7YREes
* docs(pacquet/workspace-projects-filter): disambiguate intra-doc link
`parse_project_selector` is both a module and a re-exported function at
the crate root, so the bare intra-doc link was ambiguous and failed
`cargo doc` under `-D warnings`. Link to the function with `()`.
https://claude.ai/code/session_01BsrsKRRp5ntwqnTB7YREes
* fix(pacquet/workspace-projects-filter): faithful selector parse + graph parity
Review-driven fixes for parity with upstream `@pnpm/workspace.projects-filter`
and `projects-graph`:
- `parse_project_selector`: the hand-rolled selector matcher now backtracks
like the upstream regex `^([^.][^{}[\]]*)?(\{[^}]+\})?(\[[^\]]+\])?$`. The
name group's first char may be a brace/bracket, so `!{foo` is an exclude
selector (was previously parsed as include, inverting selection) and
`{[master]` keeps its `[master]` diff. Added cases.
- `create_projects_graph`: a path-style `workspace:` token (`workspace:../foo`)
now resolves by directory rather than failing version resolution and
emitting a spurious `unmatched` entry, matching upstream's
`workspacePrefToNpm` + `npa.resolve` path.
- `glob::segment_match`: replaced the greedy single-pass match with a
backtracking wildcard match so multiple `*` in one path segment
(`{packages/a*-*}`) match correctly.
- Deduplicated `lexical_normalize`: it now lives in
`pacquet-workspace-projects-graph` and the filter crate reuses it.
https://claude.ai/code/session_01BsrsKRRp5ntwqnTB7YREes
* fix(pacquet/workspace-projects-graph): satisfy typos, perfectionist, rustdoc CI
- Rename closure params off single letters (`|p|` → `|project|`,
`|&v|` → `|&version|`) to satisfy `perfectionist::single_letter_closure_param`.
- Use raw strings for the `.\` / `..\` path prefixes
(`perfectionist::prefer_raw_string`) and add the missing `matches!`
trailing comma (`perfectionist::macro_trailing_comma`).
- Disambiguate every `create_projects_graph` intra-doc link with `()`
(module and re-exported function share the name), fixing
`rustdoc::broken_intra_doc_links` under nightly.
- Rename the `unparseable_braces…` test to `unparsable_…` (typos).
https://claude.ai/code/session_01BsrsKRRp5ntwqnTB7YREes
* fix(pacquet/workspace-projects-filter): clear remaining Dylint findings; add filter-prod coverage
- Reorder derives to `prefix_then_alphabetical` (Default before Clone)
for `perfectionist::derive_ordering`.
- Replace U+2026 `…` with ASCII `...` in comments
(`perfectionist::unicode_ellipsis_in_comments`).
- Add coverage the cloud review flagged as missing: the `--filter-prod`
production-only graph (dev edges dropped), the prod-then-all union
order, and `unmatched_filters` for a path selector that matches
nothing.
- Document the clap global-flag limitation: `--filter` occurrences only
merge within one side of the subcommand boundary.
Verified locally with `cargo dylint --all` and `cargo doc -D warnings`.
https://claude.ai/code/session_01BsrsKRRp5ntwqnTB7YREes
* test(pacquet/workspace-projects-filter): cover branches flagged by coverage
Add unit tests for every reachable line CodeCov flagged uncovered, and
delete the one unreachable branch:
- parser: empty input (`...`), empty brace group (`{}`), dot-prefixed
name (`.foo`).
- filter: selector combining a directory and a name pattern, a selector
with no name/dir/diff (`UnsupportedSelector`), and the `is_subdir`
contract (incomparable / descendant / equal paths).
- glob: trailing `*` after an exact prefix.
- graph: `./`-segment collapse in a path spec, and the Windows
drive-prefix predicate.
- Remove the unreachable `candidates.is_empty()` guard in
`resolve_by_name_version` (`by_name` never holds empty vecs; `?`
already covers absence).
All pure in-memory logic, so plain unit tests with constructed inputs
fit — no integration, filesystem fixtures, or DI seam needed. Verified
with `cargo dylint --all` and `cargo doc -D warnings`.
https://claude.ai/code/session_01BsrsKRRp5ntwqnTB7YREes
* refactor(pacquet/workspace-projects-graph): reuse pacquet-fs lexical_normalize
The graph crate carried its own copy of `lexical_normalize`, duplicating
`pacquet-fs`'s already fully-tested helper. For every input the graph
passes (absolute project roots and absolute-base joins) the two behave
identically — they differ only on unanchored leading `..`, which never
reaches this code. Drop the duplicate and re-export the `pacquet-fs` one,
removing the lone uncovered `Component::CurDir` branch in the process
(the shared helper already covers it).
https://claude.ai/code/session_0144D5iK3jHxrWENJFq6m8KF
* test(pacquet/workspace-projects-filter): cover already-walked node dedup
`pick_subgraph` skips a node it has already inserted via the
`walked.contains` guard, the branch that keeps the walk terminating on
diamonds and cycles. None of the existing fixtures reach a node by two
paths, so add a diamond graph (`top` -> {`left`, `right`} -> `shared`)
whose `...` dependency walk visits `shared` twice, exercising the guard.
https://claude.ai/code/session_0144D5iK3jHxrWENJFq6m8KF
* docs(pacquet/config): trim filter doc to its stable contract
Drop the second paragraph narrating the current install behavior
("still materializes every importer ... follow-up ... stored for
parity"). That described a transient state that goes stale the moment
pacquet consumes the filter during install. Keep only the stable
contract: what the field holds, that it mirrors pnpm's CLI-only
`filter` array, and that only the CLI layer populates it.
https://claude.ai/code/session_0144D5iK3jHxrWENJFq6m8KF
* fix(pacquet/workspace-projects-filter): concatenate absolute directory selectors like path.join
Upstream's `parseProjectSelector` resolves directory selectors with
`path.join(prefix, rel)`, which concatenates even when `rel` is absolute
(`path.join('/ws', '/pkg')` -> `/ws/pkg`). Rust's `Path::join` instead
drops `prefix` for an absolute `rel`, so `lexical_join` resolved a `{/pkg}`
selector to `/pkg` rather than `/ws/pkg`, diverging from pnpm.
Strip leading separators from `rel` before joining so an absolute
selector extends the prefix, and correct the doc comment, which described
the wrong (Rust) semantics. Add a parse test covering `{/pkg}`.
* fix(pacquet/workspace-projects-filter): normalize candidate path separators in dir globs
Address PR review comments:
- glob: normalize backslashes to `/` in the candidate path as well as the
pattern, so a Windows `ProjectRootDir` rendered with backslashes by
`PathBuf::to_string_lossy()` still matches a glob dir selector. Matches
micromatch's effective separator handling.
- filter: include the offending selector in `UnsupportedSelector`,
mirroring upstream's `Unsupported project selector: ${JSON.stringify(selector)}`.
- cli: add a regression test locking the documented clap mixed-placement
limitation for the global `-F` flag.
- parse_project_selector: fix a doc typo ("directory directory-selectors").
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
|
||
|
|
90fe4dc2d2 |
fix(package-manager): record configDependencies in workspace state (#12065)
* fix(package-manager): record configDependencies in workspace state
build_workspace_state hardcoded config_dependencies: None, so a pacquet
install wrote .pnpm-workspace-state-v1.json without the configDependencies
map. On the next pnpm run/node/exec, pnpm's checkDepsStatus compared the
live config's configDependencies against the missing recorded value, judged
node_modules out of sync, and reinstalled every time. When devEngines.runtime
is set with onFail: download, that reinstall also re-provisions the runtime.
Parse configDependencies from pnpm-workspace.yaml into Config (workspace-only,
cleared from global config.yaml like patchedDependencies) and write it through
build_workspace_state, mirroring pnpm's createWorkspaceState.
* refactor(package-manager): drop redundant comment in build_workspace_state
The fn doc comment and the WorkspaceSettings.config_dependencies field doc
already explain the createWorkspaceState parity and reinstall consequence.
* fix(config): model both configDependencies value shapes
The string-only Option<BTreeMap<String, String>> made an object-form
configDependencies entry ({ tarball?, integrity }) — valid in pnpm — fail
deserialization, turning a supported manifest into a hard config-load error.
Introduce ConfigDependency (untagged: VersionWithIntegrity string or
{ tarball?, integrity }) in the workspace-state crate and thread it through
Config and the workspace-state writer so both shapes round-trip verbatim,
matching pnpm. Also add the trailing comma perfectionist/dylint requires on
the new multi-line assert_eq! invocations.
|
||
|
|
49e6074644 |
test: replace @pnpm/registry-mock with an in-repo in-process registry (#11927)
Replace the external `@pnpm/registry-mock` (Verdaccio) test dependency with an in-repo, in-process registry that serves package fixtures to **both** the pacquet Rust tests and the pnpm CLI (Jest) tests. No separately managed registry process is needed. ### How it works - **Fixtures** live at `registry/.fixtures/packages/<name>/<version>/…`, moved verbatim from [`pnpm/registry-mock`](https://github.com/pnpm/registry-mock) (keyed by each `package.json`'s `name`+`version`). - **`pnpm-registry-fixtures`** builds verdaccio-shaped storage from those fixtures; the in-tree **`pnpm-registry`** crate serves it. - Files whose names differ only by case (`@pnpm.e2e/with-same-file-in-different-cases`) and `bundleDependencies` trees are composed **in memory** by the builder, since neither can be committed to the working tree. - **pacquet**: `pacquet-testing-utils`' `TestRegistry` starts the server lazily (once per process) in proxy mode, serving `@pnpm.e2e` fixtures locally and falling through to the npm uplink for real packages (`is-positive`, `is-negative`, …) — matching how registry-mock behaved. - **pnpm CLI**: the `with-registry` Jest `globalSetup` builds storage from the fixtures via the new `pnpm-registry-prepare` binary (built from source in the Test CI job) and serves it with `pnpm-registry`. `REGISTRY_MOCK_PORT` / `REGISTRY_MOCK_CREDENTIALS` / `getIntegrity` now come from `@pnpm/testing.registry-mock`. ### Result `@pnpm/registry-mock` is removed from every manifest, the catalog, and `packageExtensions`; `cargo test` / `cargo nextest run` / `just test` and the pnpm CLI Jest suites all run registry-backed tests without launching Verdaccio. |
||
|
|
1e9ab2935f |
feat: support staged publishes in trust scale (#12056)
Fixes #11887. Staged publishes now have a signal in the packument: `approver`. If this is set, the package is more trustworthy than a "trusted publisher" package, since it requires 2FA publish approvals. ## Changes **pnpm (TypeScript)** - `getTrustEvidence` recognizes `_npmUser.approver` and classifies it as a new `stagedPublish` trust evidence, ranked above `trustedPublisher` and `provenance`. - Trust-downgrade detection treats `stagedPublish` as the strongest rank, and the resolution verifier's PII-minimizing metadata projection retains the approver *signal* (without keeping the approver's name/email). **pacquet (Rust port)** - Ported the same staged-publish support: an `Approver` registry type, a `StagedPublish` trust evidence (rank 3 — above `TrustedPublisher`/`Provenance`), detection, pretty-printing, and the PII-stripping trust-meta projection. - Wired `trustPolicy='no-downgrade'` enforcement into the **resolver-time** path, not just the lockfile verifier. Previously pacquet only re-checked entries already in `pnpm-lock.yaml`; fresh resolutions weren't gated. The npm resolver now runs `fail_if_trust_downgraded` on each freshly picked version (full metadata is already forced under this policy), mirroring pnpm's resolver-time `failIfTrustDowngraded` call. - Ported the matching `trustChecks` tests for full parity with the TypeScript suite (staged-publish classification/downgrade, plus previously-unported `trustedPublisher → none`, no-evidence-anywhere, and exclude + missing-time cases). --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
98d4c60f61 |
fix(ci): inject release version into PACQUET_VERSION constant (#12058)
The pacquet release workflow patched the clap `version` attribute in cli_args.rs, expecting a string literal. Since #12047 that attribute references the `pacquet_config::PACQUET_VERSION` constant, so the perl substitution matched nothing and the verifying grep failed, aborting the whole release. Patch the `PACQUET_VERSION` constant in defaults.rs instead. That single constant feeds both `pacquet --version` and the default User-Agent, so both report the published version. Also tag the default User-Agent as `pnpm/pacquet-<version>` so registries can tell pacquet's traffic apart from the TypeScript pnpm CLI, while keeping the `pnpm` token for UA-keyed allow / rate-limit rules. |
||
|
|
74dd8ba6e5 |
feat(config): make network settings configurable in pacquet (#12047)
* feat(config): make network settings configurable in pacquet Port pnpm's `networkConcurrency`, `fetchTimeout`, `userAgent`, and `npmrcAuthFile` to pacquet, replacing the hardcoded network-client constants. Each is configurable via `pnpm-workspace.yaml`, the `PNPM_CONFIG_*` env overlay, and a dedicated CLI flag, matching pnpm's config surface. - `pacquet-network`: add `NetworkSettings` (concurrency, timeout, UA) threaded into `ThrottledClient::for_installs`; `fetch_timeout` drives both the response and connect deadlines. `DEFAULT_FETCH_TIMEOUT_MS` (60s) and `DEFAULT_USER_AGENT` are the default sources. - `pacquet-config`: add the four `Config` fields with cascade wiring; resolve `npmrcAuthFile` (and the `--userconfig` alias / env vars) to redirect the user-level `.npmrc` read in `Config::current`. - Relocate the Rust->Node `host_platform`/`host_arch` mappers into `pacquet-detect-libc` (re-exported from `graph-hasher`) and reuse them to build pnpm's `pnpm/<version> npm/? node/? <platform> <arch>` UA. Two intentional behaviour changes for parity: the default request timeout moves from 300s to pnpm's 60s, and the default User-Agent moves from the literal `pnpm` to pnpm's full UA format. Refs https://github.com/pnpm/pnpm/issues/12042 * fix(config): correct npmrcAuthFile env resolution; port pnpm precedence tests The user-level `.npmrc` path resolution applied the empty-string filter once after the whole `or_else` chain, so an exported-but-empty `PNPM_CONFIG_NPMRC_AUTH_FILE` short-circuited resolution instead of falling through. Mirror pnpm's `readEnvVar` / `readNpmEnvVar` exactly: filter `value !== ''` per variable, accept both `pnpm_config_*` / `PNPM_CONFIG_*` cases, and add the `npm_config_userconfig` / `NPM_CONFIG_USERCONFIG` compatibility fallback. Port the translatable pnpm precedence tests from `config/reader/test/index.ts` (resolution from the PNPM_CONFIG_* family, lowercase variant, empty-falls-through, npmrc_auth_file outranks userconfig, npm_config_userconfig compat fallback + pnpm-wins). pnpm's credential-scoping tests that read the workspace .npmrc and the userconfig simultaneously and re-scope per file don't translate yet: pacquet uses a single-file (project-or-user) model, not pnpm's layered merge. Global config.yaml sourcing of npmrcAuthFile remains deferred. Refs https://github.com/pnpm/pnpm/issues/12042 * feat(config): merge multiple .npmrc sources with per-file credential scoping Port pnpm's multi-file `.npmrc` layering and the credential-isolation security boundary it provides. `Config::current` now reads the user-level file (npmrcAuthFile / userconfig / ~/.npmrc), the global `auth.ini`, and the project `.npmrc` together and merges them (`user < auth.ini < workspace`) instead of reading just the first one found. Each source is rescoped before the merge (`NpmrcAuth::rescope_unscoped`, ported from pnpm's `rescopeUnscopedCreds`): a file's *unscoped* `_authToken` / `_auth` / `username` / `_password` / `cert` / `key` are pinned to that file's own `registry=` (or the npmjs default when it declares none), nerf-darted into a per-URI key. Once pinned, a credential can never be pulled to a different registry that a higher-priority file — or a later `pnpm-workspace.yaml` registry override — sets. A deprecation warning is queued for each rescoped key. `npmrcAuthFile` is now also sourced from the global `config.yaml` (between `PNPM_CONFIG_USERCONFIG` and `npm_config_userconfig`), completing pnpm's resolution order. Behaviour changes for parity, both covered by ported tests: an unscoped top-level `cert`/`key` becomes per-registry client identity (pinned to its file's registry) rather than a global identity sent to every host; and a user-file credential no longer leaks to a workspace registry override. Ports the credential-scoping suite from `config/reader/test/index.ts`. Refs https://github.com/pnpm/pnpm/issues/12042 * fix(network): reject networkConcurrency=0; resolve user .npmrc base dir safely Address review feedback on #12042: - `ThrottledClient::for_installs` now returns `ForInstallsError::ZeroNetworkConcurrency` when `network_concurrency` is 0 instead of building a zero-permit semaphore that would hang every fetch. Matches pnpm, which rejects the value (p-queue requires a concurrency >= 1). - When `npmrcAuthFile` is a bare filename with no parent, resolve its relative `cafile`/`certfile` entries against the process cwd (empty base path) rather than treating the file itself as the base directory. * style(config): add trailing comma to multi-line format! (dylint) Satisfies dylint's `perfectionist::macro-trailing-comma`, which the pre-push hook enforces but `just ready` / clippy don't surface. * style(config): fix "Unparseable" -> "Unparsable" typo in comment Satisfies the Spell Check (crate-ci/typos) CI job. |
||
|
|
aa3135b9f2 |
feat(pacquet): port resolution/versions settings (#12042) (#12052)
Port the "Resolution / versions" section of the pnpm-parity tracking issue to pacquet. Full behavioral wiring: - `resolutionMode` (`highest` / `time-based` / `lowest-direct`): direct deps pick lowest under `time-based`/`lowest-direct`; `time-based` additionally constrains transitive deps to a publish-date cutoff (newest resolved direct dep + 1h, clamped by `minimumReleaseAge`), computed workspace-wide in a pre-pass. - `registrySupportsTimeField`: gates the full-metadata fetch the `time-based` cutoff (and the `no-downgrade` trust policy) need — `(time-based || no-downgrade) && !registrySupportsTimeField`. Config-surface parity (parsed + stored, consumed once the surrounding feature lands, as pacquet already does for other not-yet-active settings): - `allowedDeprecatedVersions` (no deprecation warnings yet), - `updateConfig.ignoreDependencies` (no `update`/`outdated` command yet), - `peerDependencyRules` (no peer-issue reporting pass yet). Also treat `minimumReleaseAge: 0` as disabled (matching pnpm's falsy `opts.minimumReleaseAge ? ... : undefined` check). While a maturity cutoff is active the picker always prefers the highest mature version, so this is what lets `lowest-direct`/`time-based` take effect on direct deps. |
||
|
|
64ce69aadc |
feat(package-manager): run project lifecycle scripts during install (#12051)
Port pnpm's project (workspace/root) lifecycle scripts that run during `pnpm install` — preinstall, install, postinstall, preprepare, prepare, postprepare — distinct from the dependency build scripts already run in `BuildModules`. - executor: extract the per-stage loop into a shared `run_lifecycle_stages`, keeping the `binding.gyp -> node-gyp rebuild` fallback and the `npx only-allow pnpm` skip identical for both paths. Add `run_project_lifecycle_scripts` + `PROJECT_LIFECYCLE_STAGES`, mirroring the `runLifecycleHooksConcurrently` call sites in pkg-manager/core and pkg-manager/headless. - executor: honor `SelectedShell::windows_verbatim_args` — on the Windows `cmd /d /s /c` path the script body is now appended with `raw_arg` so embedded quoting (e.g. `node -e "..."`) reaches the child intact, matching Node's `windowsVerbatimArguments`. Previously a no-op, which mangled quoted scripts under cmd.exe. - package-manager: run each project's scripts in `Install::run` after the dependency graph is materialized, bins are linked, and `.modules.yaml` / the current lockfile are written, before the closing `pnpm:summary`. Runs on both the frozen and fresh paths. A project script failure always fails the install via the new `InstallError::ProjectLifecycleScript` variant (unlike optional dependency build failures). - gate on `Install::is_full_install`: `pacquet add` is a partial install (pnpm's `mutation: 'installSome'`), so the project's own scripts must not run — mirroring pnpm's `mutation === 'install'` filter. - tests: stage ordering, re-run on --frozen-lockfile, failure propagation, name-differs-from-directory, INIT_CWD, and the `add`-skips-project-scripts gate. Projects run root-first and sequentially; pnpm's buildIndex ordering and child_concurrency fan-out are a follow-up once pacquet computes a per-importer build index. No `ignoreScripts` gate yet — pacquet hardcodes `ignore_scripts: false` across the dep-build path, so this matches pnpm's default of running them. Ref: https://github.com/pnpm/pnpm/blob/80037699fb/pkg-manager/core/src/install/index.ts#L1517-L1530 |
||
|
|
3cf2b86579 |
fix: preserve tarball dependency integrity in the lockfile (#12040)
* fix: preserve tarball dependency integrity in the lockfile URL/tarball resolvers do not return an integrity (it is only known after the tarball is downloaded). When a remote-tarball dependency was reused from the lockfile without being re-fetched, the freshly resolved resolution had no integrity and the existing one was dropped, breaking subsequent --frozen-lockfile installs under the lockfile-integrity hardening (ERR_PNPM_MISSING_TARBALL_INTEGRITY). Carry the integrity over from the current resolution instead. Closes #12001 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(package-requester): simplify tarball integrity carryover guard Align the integrity carryover added in the previous commit with its sibling block in the download path: use `!resolution.type` (the idiom already used there) and drop the `newIntegrity == null` clause, which is redundant once `resolution` is the freshly resolved resolution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(pacquet): cover tarball-dependency integrity preservation (#12001) pnpm #12040 fixes dropping a remote-tarball dependency's integrity when an unrelated package is installed afterwards. Pacquet can't reach that scenario yet: a non-registry https-tarball direct dependency hits the TarballResolver, which returns no name_ver/integrity, so lockfile build panics with MissingSuffix. Add the regression test for the target behavior, gated with allow_known_failure! until external tarball deps install. Tracked in #12053. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
efcc33655f |
feat(registry): per-user access lists (verdaccio-style groups) (#12048)
* feat(registry): support multi-token access lists with usernames Model each packages[*] access/publish permission as a list of tokens (verdaccio's space-separated groups) instead of a single rule, and match it against the caller's identity. Tokens are the built-in pseudo-groups ($all/$authenticated/$anonymous and their @/bare aliases) or a name; a name matches an authenticated caller whose username equals it — verdaccio's per-user access, where the htpasswd backend contributes the username and richer groups would come from a group-providing auth backend. - AccessRule (single enum) -> AccessList (Vec<AccessToken>) + Identity. - Accept both a space-separated string and a YAML sequence for each permission, like verdaccio. - Bare names are now valid tokens (drops the InvalidAccessRule stopgap from #12043 that rejected them). - enforce_access: anonymous denial -> 401, authenticated denial -> 403. Refs #11973. * docs(registry): note the bare authenticated/anonymous token aliases AccessToken::from also accepts the unprefixed `authenticated` / `anonymous` forms; document them on the variants so they aren't misread as usernames. |
||
|
|
75e11a6a09 |
feat(pacquet/package-manager): port lockfileOnly setting (#12042) (#12046)
Ports pnpm's `--lockfile-only` to pacquet: resolve dependencies and write `pnpm-lock.yaml` without fetching any tarball into the store or materializing `node_modules`. `lockfile-only` is in pnpm's `excludedPnpmKeys` (like `frozen-lockfile`), so it's a pure per-invocation CLI flag with no `pnpm-workspace.yaml` / `config.yaml` counterpart. It's threaded straight from the CLI through `Install` / `Add`, mirroring the existing `frozen_lockfile` plumbing — no config-crate changes. - `--lockfile-only` flag added to `install` and `add`. - `Install::run`: frozen / auto-frozen + `--lockfile-only` validates the on-disk lockfile (errors on stale, matching pnpm), re-persists the wanted lockfile, and returns without materializing. Otherwise the fresh-resolve path runs and `.modules.yaml`, the current lockfile, and the workspace-state file are skipped (mirrors pnpm skipping `updateWorkspaceState` under `lockfileOnly`). - `InstallWithFreshLockfile::run`: skips the `PrefetchingResolver` wrapper so no tarball is fetched (matching pnpm's `dryRun: opts.lockfileOnly`), then writes `pnpm-lock.yaml` and returns before prefetch / virtual-store / symlink / hoist / bin steps. - `--lockfile-only` with `lockfile: false` (pnpm's `useLockfile: false`) now fails with `ERR_PNPM_CONFIG_CONFLICT_LOCKFILE_ONLY_WITH_NO_LOCKFILE`, matching pnpm's `extendInstallOptions` guard. Tests: new `crates/cli/tests/lockfile_only.rs` ports every lockfileOnly test from `installing/deps-installer/test/` — the two in `install/lockfileOnly.ts` (resolve-and-write with no download/link; `--frozen-lockfile --lockfile-only` rejects a stale lockfile) plus the two lockfileOnly-focused cases in `lockfile.ts` (the `useLockfile: false` conflict and the workspace "new project added" importer update). |
||
|
|
a39a83d19e |
feat: support nodeLinker: hoisted on fresh installs + add hoistingLimits setting (#12041)
## 1. Support `nodeLinker: hoisted` on the fresh-lockfile install path (pacquet) Closes #11871. Until now pacquet's `Install::run` hard-refused `nodeLinker: hoisted` without a checked-in lockfile (`ERR_PNPM_…UNSUPPORTED_FRESH_INSTALL_NODE_LINKER`). - Extracted a shared `run_hoisted_linker` helper from the frozen path's hoisted branch (walker → `link_hoisted_modules` → `SymlinkDirectDependencies { link_only: true }` → `pkg_root_by_key` → walker-skip folding), so both install paths run identical logic. - Fresh path now threads `node_linker` + `supported_architectures`, hands `CreateVirtualStore` the real linker (populating `cas_paths_by_pkg_id`), branches on `is_hoisted`, and returns `hoisted_locations` so `.modules.yaml` round-trips. - Removed the guard and the dead `UnsupportedFreshInstallNodeLinker` error variant. Ported upstream's `hoistedNodeLinker/install.ts` into `crates/cli/tests/hoisted_node_linker.rs` (real tests for the core layout, no-lockfile, `externalDependencies`, `autoInstallPeers`, and `hoistingLimits`; the rest stubbed as `known_failures` against `pnpm add`/update (#433) and build-phase (#11870) gaps), and ticked the boxes in `plans/TEST_PORTING.md`. ## 2. Add the `hoistingLimits` setting (pnpm CLI **and** pacquet) Revives the stale #6468 (closes #6457) and brings both stacks to parity. `hoistingLimits` mirrors yarn's `nmHoistingLimits`: `none` (default — hoist as far as possible), `workspaces` (hoist only as far as each workspace package), `dependencies` (hoist only up to each workspace package's direct deps). It was previously a programmatic-only option in pnpm (no config surface) and a pacquet-only raw-map yaml field. **pnpm CLI:** `config/reader` (`types.ts` enum + `Config.ts` + `configFileKey.ts`), `installing/linking/real-hoist`'s new `getHoistingLimits` (mode → the `@yarnpkg/nm` hoister's per-locator border map), and the install/add/recursive command option lists. Tests: `hoistedNodeLinker/install.ts` (`dependencies` mode) + `real-hoist` `getHoistingLimits` unit tests. Changeset included (minor). **pacquet:** replaced the raw-map config with the same enum; added `get_hoisting_limits` (port of `getHoistingLimits`); and **fixed `real-hoist`'s border semantics** — a name in the limits marks a *border* whose descendants stay nested beneath it, not a leaf to block. (The earlier leaf-blocking behavior was the divergence flagged while porting; its unit tests were rewritten to the corrected semantics.) |
||
|
|
a59308230a |
feat(registry): enforce per-package access policy from YAML (#12043)
* feat(registry): enforce per-package access policy from YAML Derive PackagePolicies from each packages[*] entry's access/publish tokens in from_yaml/from_default_yaml, instead of hardcoding registry_mock_defaults — configured ACLs were silently ignored before, so a user-defined private scope got no protection. - Add the $anonymous AccessRule: admits only unauthenticated callers; an authenticated caller falls outside the group and gets 403. - Unknown access tokens now fail config parsing rather than silently mis-enforcing; named groups remain unsupported (tracked separately). - Programmatic Config::proxy / Config::static_serve keep registry_mock_defaults. Refs #11973. * docs(registry): reword comments to satisfy typos spell check `mis-enforce` tripped the `typos` check (read as `mis` → `miss`/`mist`). * fix(registry): validate the unpublish access token too build_policies validated access/publish but let unknown unpublish tokens slip through. Validate it as well (defaulting to the publish rule when absent) so the fail-closed behavior is consistent. |
||
|
|
a9011ad83b |
feat(registry): add logging support for requests (#12033)
Adds YAML-driven logging to `pnpm-registry`. Format (`pretty` or `json`) and level come from a `log:` block in `config.yaml`. Every HTTP request emits one structured access record (method, URI, status, latency) under the `pnpm_registry::access` tracing target — pino-shaped. When `-c` isn't passed, the global `config.yaml` is auto-discovered using the same per-OS rules pnpm uses for its own config dir, under a `pnpr` leaf. ```yaml log: type: stdout format: json level: info ``` ## What's in - New `LogConfig` / `LogFormat` / `LogLevel` types; defaults to pretty/info. - `init_logging` picks `.json()` vs `.compact()` from the resolved config; `RUST_LOG` still overrides. JSON keeps the request span's `method`/`uri` on each access record (`with_current_span(true)`). - `TraceLayer` emits exactly one INFO access record per request — both the span and the completion event under `target: "pnpm_registry::access"`, with tower-http's default emissions suppressed — so `LogLevel::Http`'s filter directive can scope to them. - Global-config auto-discovery follows pnpm's `getConfigDir` rules (`XDG_CONFIG_HOME` / macOS `Library/Preferences` / Windows `LOCALAPPDATA` / `~/.config`) under a `pnpr` leaf. That resolution is shared with `pacquet-config` via a new dependency-free `pacquet-config-dir` crate, parameterized by app-name leaf. - Verdaccio 6+ shape (`log:`, singular). The older plural `logs:` is silently ignored. - `log.type` values other than `stdout` parse (verdaccio compatibility) but are ignored at runtime with a startup warning; only `stdout` is implemented. ## Notes - File/syslog sinks are future work. - New crate: `pacquet-config-dir` (std-only, used by both `pacquet-config` and `pnpm-registry`). New `pnpm-registry` deps: `home` and `tracing-subscriber`'s `json` feature — both already in `[workspace.dependencies]`. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
eb48bfd6c1 |
feat(registry): add /-/ping endpoint and shared config env var substitution (#12036)
This PR adds two features to the pnpm private registry:
- **`/-/ping` endpoint** — implements the npm `ping` endpoint (`GET /-/ping`), returning an empty JSON object `{}` with `200 OK`, matching npm's and verdaccio's behavior. Usable as a liveness/health check by clients and orchestrators.
- **Environment variable substitution in config** — the registry's YAML config now expands `${VAR}` and `${VAR:-DEFAULT}` placeholders before deserialization.
### Shared env-var substitution with pacquet
Rather than reimplement placeholder expansion, the registry reuses pnpm's exact semantics. The `${VAR}` substitution logic that already lived in `pacquet-config` (a faithful port of `@pnpm/config.env-replace`) is extracted into a new standalone crate, **`pacquet-env-replace`**, which both `pacquet-config` and the registry depend on. This means the registry inherits pnpm's behavior for free:
- `${VAR:-default}` falls back when the variable is set-but-empty, not just when it's absent.
- `\${VAR}` backslash escaping is honored.
- Unresolved placeholders are surfaced (logged via `tracing::warn!`) instead of being silently emptied — mirroring pnpm's lossy fallback (the OIDC `${VAR}`-leak guard, [#11513](https://github.com/pnpm/pnpm/issues/11513)).
`pacquet-config` re-exports `EnvVar` and keeps threading its own `Host` provider, so its behavior is unchanged.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
|
||
|
|
50c42712bc |
fix(package-manager): match pnpm's GVS path segment for injected file: deps (#12039)
Under `enableGlobalVirtualStore: true`, pacquet built the slot path for an injected `file:` workspace dep as `<store>/links/@/<name>/file:<path>/<hash>`, embedding the raw depPath version. The `:` (and embedded `/`) make the path invalid on Windows (`ERROR_INVALID_NAME`) and diverge from pnpm. `gvs_version_segment` now mirrors pnpm's `nameVerFromPkgSnapshot` feeding `formatGlobalVirtualStorePath`: a `file:` directory dep has no lockfile `version` and a non-semver depPath, so upstream renders the literal segment `undefined`. Pacquet does the same, keeping the colon out of the path while matching pnpm's frozen-lockfile output byte-for-byte. The materialise step itself already works (the directory fetcher landed with the injectWorkspacePackages/dedupeInjectedDeps ports), so re-enable the Windows-skipped e2e test and strengthen it to assert materialisation. Add a GVS regression test covering the colon fix. Closes #12038 |
||
|
|
ddf4ec4612 |
feat(pacquet): port dedupeInjectedDeps (#12023)
Ports pnpm's [`dedupeInjectedDeps`](https://github.com/pnpm/pnpm/blob/39101f5e37/installing/deps-resolver/src/dedupeInjectedDeps.ts) to pacquet end-to-end, and restructures the resolver to match pnpm's multi-importer shape so the dedupe lives where pnpm puts it. - **Config plumbing** — `dedupe_injected_deps: bool` on `Config` (default `true`), read from `pnpm-workspace.yaml`'s `dedupeInjectedDeps` key, overridable via `PNPM_CONFIG_DEDUPE_INJECTED_DEPS`. Cleared as a workspace-only field in `WorkspaceSettings::clear_workspace_only_fields`. - **`dependenciesMeta.injected` plumbing** — pacquet's deps-resolver previously constructed `WantedDependency` with `..Default::default()`, so the per-package `dependenciesMeta[<alias>].injected: true` flag never reached the npm/local resolvers and no install path produced a `file:<workspace>` direct dep. Reading `dependenciesMeta` at the importer-level wanted-dep collection unlocks the `file:` workspace-pick branch the dedupe consumer is designed to collapse. - **Multi-importer resolver refactor** — new `resolve_workspace` orchestrator (`pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs`) mirrors pnpm's [`resolveDependencies(importers, opts)`](https://github.com/pnpm/pnpm/blob/39101f5e37/installing/deps-resolver/src/index.ts#L128). It constructs one shared `WorkspaceTreeCtx` (resolved-pkgs dedup, children-spec cache, resolver-call memo, peer-walker seed sets), hands `Arc::clone` to every per-importer `resolve_importer_with_workspace`, then runs a single `resolve_peers_workspace` pass that shares `peersCache` + `purePkgs` across importers. Importer N's tree walk now reuses importer M's resolver hits instead of re-running the chain. - **`dedupeInjectedDeps` lives in `resolve_peers_workspace`** (`pacquet/crates/resolving-deps-resolver/src/dedupe_injected_deps.rs`), matching pnpm's `resolvePeers` integration. The install layer no longer carries any dedupe wiring; it just hands importers + a per-importer `ResolveOptions` closure to `resolve_workspace`. After dedupe, unreachable `file:<workspace>` snapshots are pruned from the graph so they don't leak into `pnpm-lock.yaml`. - **`ImporterDepVersion::File` arm** — when `dedupeInjectedDeps: false` leaves an injected workspace dep as `file:packages/<name>` at the importer level, the lockfile writer used to panic at `importer_dep_version` (the `parse::<PkgNameVerPeer>().expect(...)` arm). Adds a `File(String)` variant to `ImporterDepVersion`, wires it through `dependencies_graph_to_lockfile` and `symlink_direct_dependencies`, and the new e2e test `injected_workspace_dep_with_dedupe_off_writes_file_arm` exercises that path end-to-end. - **Workspace state** — surfaces `dedupe_injected_deps` in `current_settings` and adds it to the `settings_match` comparison in `optimistic_repeat_install`; drops it from the "deliberately not compared" list so settings drift now triggers a reinstall. Tracked under pnpm/pnpm#12009 (one item; the rest are separate PRs). |
||
|
|
bf3cc86952 |
feat(pacquet/package-manager): port dedupeDirectDeps setting (#12024)
Ports pnpm's [`dedupeDirectDeps`](https://pnpm.io/settings#dedupedirectdeps) setting to pacquet end-to-end — one of the items tracked in #12009. When the workspace root resolves an alias to the same target as a non-root project, the non-root project skips the symlink under its own `node_modules/`. A project whose direct deps are entirely covered by root ends up without a `node_modules/` at all. Matches pnpm's [`linkDirectDepsAndDedupe`](https://github.com/pnpm/pnpm/blob/39101f5e37/installing/linking/direct-dep-linker/src/linkDirectDeps.ts#L34) in all three regimes (direct-vs-direct, direct-vs-public-hoist, direct-vs-shamefully-hoist), on both the fresh-install and frozen-lockfile paths. Default `true`, matching pnpm. Set `dedupeDirectDeps: false` in `pnpm-workspace.yaml` to opt out. ## What changed - **Config:** new `dedupe_direct_deps: bool` field on `Config` (default `true`), wired through `pnpm-workspace.yaml`, the `DEDUPE_DIRECT_DEPS` env override, and `clear_workspace_only_fields`. - **Linker:** `SymlinkDirectDependencies::run` computes the root importer's resolved targets up front and filters non-root importers against that map. Pulled out helpers `collect_resolved_entries` / `resolve_target_path` / `collect_resolved_targets` so the dedupe pass and the symlink pass share one target-path computation. The `link_only` (hoisted) path participates too — `link:` workspace siblings dedupe consistently. Link-target paths are run through `pacquet_fs::lexical_normalize` before equality comparison so `<workspace>/packages/a` and `<workspace>/packages/sibling/../a` correctly dedupe (matching pnpm, whose `path.relative` normalizes via `path.resolve`). - **Hoist + dedupe interaction:** pacquet's pipeline runs `SymlinkDirectDependencies` *before* the on-disk hoist phase, so the dedupe map would have missed publicly-hoisted root entries that pnpm sees because hoist runs first there. Lifted the hoist plan computation into `pub(crate)` helpers (`compute_hoist_plan`, `collect_public_hoist_targets`, `HoistPlan`) — pure BFS, no I/O — and pass the publicly-hoisted alias → target-path map into the dedupe pass via a new `public_hoist_targets` field. The on-disk hoist phase reuses the same `HoistResult` instead of re-running the BFS. - **Fresh-install hoist:** pacquet's fresh-install path previously didn't run hoist at all, so `pacquet install` (no `--frozen-lockfile`) never produced `<vs>/node_modules/<alias>` (private hoist) or `<root>/node_modules/<alias>` (public hoist) entries — a parity gap with pnpm independent of dedupe. The same helpers are now called from `install_with_fresh_lockfile.rs` after `CreateVirtualStore`. The fresh-install result's `hoisted_dependencies` slot now carries the real map (was always empty before). - **Workspace state:** `current_settings` writes `dedupeDirectDeps`, `settings_match` participates in the comparison. Flipping the flag trips the optimistic-repeat-install fast-path gate. ## Tests `pacquet/crates/cli/tests/dedupe_direct_deps.rs` (new) — 7 integration tests against the real binary + mocked registry: - `dedupes_direct_deps_against_workspace_root_by_default` — default-on dedupes against root (sibling has no `node_modules`) - `dedupe_direct_deps_disabled_keeps_per_project_symlinks` — `dedupeDirectDeps: false` keeps per-project symlinks - `dedupes_only_overlapping_direct_deps` — partial dedupe (one shared, one unique) - `dedupes_direct_deps_with_frozen_lockfile` — frozen-lockfile install dedupes too - `dedupes_link_deps_resolving_to_the_same_dir_via_different_segments` — `link:packages/a` vs `link:../a` dedupes via lexical normalization - `dedupes_direct_dep_against_publicly_hoisted_root_dep` — mirrors `installing/deps-installer/test/install/dedupeDirectDeps.ts:113`, exercises dedupe-vs-public-hoist via the frozen path - `dedupe_under_shamefully_hoist` — mirrors `pnpm/test/install/hoist.ts:77`, exercises dedupe + shamefully-hoist on the fresh-install path Plus: - `returns_skipped_when_dedupe_direct_deps_drifts` — drift test on the optimistic-repeat-install settings gate. - Two snapshot tests (`add__should_install_all_dependencies`, `add__should_symlink_correctly`, `install__should_install_dependencies`) updated to include the new `node_modules/.pnpm/node_modules/<scope>/<alias>` private-hoist entries that fresh install now produces (matches pnpm's default `hoistPattern: ["*"]`). All upstream tests referencing `dedupeDirectDeps` are ported. |
||
|
|
2cca6ab9df |
feat(package-manager): port packageExtensions setting (#12027)
Ports the `packageExtensions` setting from pnpm to pacquet, end-to-end. Refs pnpm/pnpm#12009. ## What lands **Config + yaml plumbing** (`pacquet-config`): - `PackageExtension` / `PeerDependencyMeta` structs (mirror pnpm's `Pick<BaseManifest, 'dependencies' | 'optionalDependencies' | 'peerDependencies' | 'peerDependenciesMeta'>`). - `WorkspaceSettings::package_extensions` with yaml deserialization, `clear_workspace_only_fields` entry, and empty-map collapse (matches the `overrides` shape). - `Config::package_extensions` field. - `PNPM_CONFIG_PACKAGE_EXTENSIONS` env var. **Hook** (`pacquet-package-manager::PackageExtender`): - Port of pnpm's [`createPackageExtender`](https://github.com/pnpm/pnpm/blob/39101f5e37/hooks/read-package-hook/src/createPackageExtender.ts): groups extensions by package name once, then applies semver range filter and merges fields onto the manifest. Manifest fields win over extension fields on conflict (matches upstream's `{ ...extension, ...manifest }` spread order). - `apply_to_arc` deep-clones the inner `Value` only when an extension matches — unrelated manifests keep sharing the resolver's cached `Arc`. **Resolver wiring** (`pacquet-resolving-deps-resolver`): - New `ManifestHook` type, threaded through `ResolveDependencyTreeOptions` / `ResolveImporterOptions` / `TreeCtx`. - Applied in `resolve_node` right after `Resolver::resolve` returns and before the result enters the wanted-dep cache — same site as pnpm's [`ctx.readPackageHook(pkg)`](https://github.com/pnpm/pnpm/blob/39101f5e37/installing/deps-resolver/src/resolveDependencies.ts#L1481-L1483) call. - `install_with_fresh_lockfile` builds the hook from `Config.package_extensions` once per install. **Workspace state / drift check** (`optimistic_repeat_install`): - `current_settings` now emits `package_extensions` so `.pnpm-workspace-state-v1.json` round-trips through pnpm. - `settings_match` compares the field (treating empty map as absent for cross-implementation parity). - Removed from the "deliberately not compared" list in `settings_match`. **Lockfile checksum + frozen-install drift gate** (`pacquet-graph-hasher`, `pacquet-lockfile`): - New `hash_object_nullable_with_prefix` in `pacquet-graph-hasher` — port of pnpm's [`hashObjectNullableWithPrefix`](https://github.com/pnpm/pnpm/blob/39101f5e37/crypto/object-hasher/src/index.ts#L44-L48), byte-for-byte identical (tests pin against the known `sha256-48AVoXIXcTKcnHt8qVKp5vNw4gyOB5VfztHwtYBRcAQ=` output upstream's test asserts). - New `Lockfile::package_extensions_checksum` top-level field (matches upstream's wire shape). - `install_with_fresh_lockfile` computes and writes the checksum on every fresh install; `current_lockfile` clones it into the materialized lockfile. - `check_lockfile_settings` gained a `package_extensions_checksum` argument and a new `StalenessReason::PackageExtensionsChecksumChanged` variant. The check fires after `overrides` and before `ignoredOptionalDependencies` — matches upstream's ordering at [`getOutdatedLockfileSetting.ts:53-55`](https://github.com/pnpm/pnpm/blob/39101f5e37/lockfile/settings-checker/src/getOutdatedLockfileSetting.ts#L53-L55) so the first-drift error reads the same on both sides. |
||
|
|
a0eb2ad778 |
feat(pacquet): port injectWorkspacePackages setting (#12021)
Ports `injectWorkspacePackages` to pacquet end-to-end — config layer, resolver (global + per-dep), lockfile, and freshness gates — closing one of the eight settings tracked at #12009. ### What the feature does When set globally, workspace-package resolutions materialize as `file:` (hard-linked copies into the virtual store) instead of `link:` symlinks back to the source. Per-dependency `dependenciesMeta[<name>].injected = true` opts a single dep into the same behavior even when the global flag is off. ### Changes **Config layer (commit b3a68957f8):** - Adds `inject_workspace_packages: bool` to `Config` (default `false`, matching pnpm's `'inject-workspace-packages': false`). - Adds the field to `WorkspaceSettings` so it parses from `pnpm-workspace.yaml` (camelCase). - Threads `config.inject_workspace_packages` into `ResolveOptions` at the install entry point. - Joins it to the workspace-state freshness comparison: `current_settings` writes the value, `settings_match` compares it, a flip between runs invalidates the optimistic-repeat-install cache. **End-to-end pipeline (commit 44e2ea918a):** - `LockfileSettings.inject_workspace_packages` round-trips through the v9 lockfile. `false` omits the key on save, matching pnpm's [`lockfileFormatConverters.ts:70-72`](https://github.com/pnpm/pnpm/blob/39101f5e37/lockfile/fs/src/lockfileFormatConverters.ts#L70-L72). - `dependencies_graph_to_lockfile` populates the new field from `Config.inject_workspace_packages`. - `check_lockfile_settings` adds a Boolean-normalized drift gate for `settings.injectWorkspacePackages`, mirroring upstream's [`getOutdatedLockfileSetting.ts:80-82`](https://github.com/pnpm/pnpm/blob/39101f5e37/lockfile/settings-checker/src/getOutdatedLockfileSetting.ts#L80-L82). - `build_pkg_id_with_patch_hash` now prefixes file:/git:/tarball: resolutions with `name@` when the resolver didn't set `name_ver`, reading the name from the manifest — matching pnpm at [`resolveDependencies.ts:1502-1507`](https://github.com/pnpm/pnpm/blob/097983fbca/installing/deps-resolver/src/resolveDependencies.ts#L1502-L1507). Without this, file: dep paths end up unprefixed and `PkgNameVerPeer`'s `@`-split parser fails on the `@` inside the peer suffix. `link:` ids are deliberately exempted so the downstream `is_link` short-circuit still triggers. **Cross-platform fix (commit 38a1db9cc1):** - `PkgNameVerPeer::to_virtual_store_name` previously escaped only `/`, leaving `:` in `file:`-resolution slot names. NTFS / FAT reject these with `ERROR_INVALID_NAME (123)`, but APFS / ext4 accept them, so the bug only surfaced once a workspace-injected dep hit the Windows CI of this PR. The escape now covers the full Windows-invalid charset `[\\/:*?"<>|#]`, matching upstream's [`depPathToFilename` regex](https://github.com/pnpm/pnpm/blob/1819226b51/deps/path/src/index.ts#L169-L170). **Per-dep `dependenciesMeta` thread (commit 7cc0f98bd7):** - `importer_injected_dependency_names(manifest)` reads `dependenciesMeta` from the importer manifest and returns the names whose `injected` flag is `true`. - The wanted-spec tuple in `extend_tree` / `resolve_catalog_specifiers` widens from `(alias, range, optional)` to `(alias, range, optional, injected)`. `extend_tree` lifts the spec's `injected` onto `WantedDependency::injected` as `Some(true)` / `None`, matching pnpm's `injected: opts.dependenciesMeta[alias]?.injected` shape so absent meta yields `None` (not `Some(false)`). - `WantedKey` cache key gains a fourth slot for `injected` — the workspace branch of the npm resolver emits a `file:` resolution when injected and a `link:` one otherwise, so two importers asking for the same workspace dep with different per-dep flags must take different cache buckets. - Importer-only scope: the child-spec walker keeps its 3-tuple shape; no resolved package's own `dependenciesMeta` is inherited. Hoisted-required / hoisted-optional peer arms default `injected` to `false` since they construct fresh wanteds without dep meta. Refs #12009. |
||
|
|
4b60515af5 |
feat(pacquet/config): port peersSuffixMaxLength setting (#12026)
Ports the [`peersSuffixMaxLength`](https://pnpm.io/settings#peerssuffixmaxlength) setting (default `1000`) to pacquet end-to-end, matching pnpm: - read from `pnpm-workspace.yaml`, global `config.yaml`, and `PNPM_CONFIG_PEERS_SUFFIX_MAX_LENGTH` - threaded into `ResolveImporterOptions` so the deps resolver caps the rendered peer-suffix at the configured value (was hardcoded `1000` via `ResolvePeersOptions::default()` before) - written to `pnpm-lock.yaml`'s `settings.peersSuffixMaxLength` only when non-default — mirrors pnpm's strip-on-default in [`convertToLockfileFile`](https://github.com/pnpm/pnpm/blob/39101f5e37/lockfile/fs/src/lockfileFormatConverters.ts#L67-L69) - `check_lockfile_settings` now reports drift as `StalenessReason::PeersSuffixMaxLengthChanged` - joins `optimistic_repeat_install`'s `settings_match` comparison via `current_settings`, so a change here invalidates the fast path Closes one of the checkboxes tracked in #12009. |
||
|
|
e375a58261 |
chore: run cargo fmt, doc, and dylint checks in husky pre-push (#12035)
The Rust workspace's pre-push checks were sitting in `pacquet/.githooks/pre-push` and only fired if a developer ran `just install-hooks`, which would also disable every husky-managed TypeScript hook by replacing `core.hooksPath`. Move the bash logic to `pacquet/scripts/pre-push-rust.sh`, invoke it from `.husky/pre-push` alongside the existing TS compile and lint checks, and drop the `install-hooks` recipe so nobody re-points `core.hooksPath` by mistake. The script now also runs `cargo doc --no-deps --workspace --all-features` (with `RUSTDOCFLAGS=-D warnings`) and `cargo dylint --all -- --all-targets --workspace` (with `RUSTFLAGS=-D warnings`), matching CI. `--workspace` covers both `pacquet/crates/*` and `registry/crates/*` since they share the root Cargo workspace. |
||
|
|
7c9a6c29ea |
feat(pacquet/config): port preferWorkspacePackages setting (#12032)
Port the `preferWorkspacePackages` setting from pnpm. When enabled, a workspace package wins over a newer registry pick during resolution. Default `false`, matching pnpm. - Config plumbing: `Config.prefer_workspace_packages`, `WorkspaceSettings.prefer_workspace_packages`, `PNPM_CONFIG_PREFER_WORKSPACE_PACKAGES` env overlay, and the `clear_workspace_only_fields` / `apply_to` wiring. - Install pipeline: thread `config.prefer_workspace_packages` into the `ResolveOptions` built in `install_with_fresh_lockfile`. The npm resolver already consumes this flag in `try_workspace_shadow`. - Optimistic-repeat-install drift: `WorkspaceStateSettings.prefer_workspace_packages` is now compared by `settings_match` and written by `current_settings`. A switch to the setting between installs invalidates the cached-modules fast path. Closes one of the items on #12009. |
||
|
|
82e31d6627 |
feat(pacquet): port excludeLinksFromLockfile (#12025)
Port the `excludeLinksFromLockfile` setting to pacquet so its behavior matches the TypeScript pnpm CLI (which has had this setting since [pnpm/pnpm#6570](https://github.com/pnpm/pnpm/pull/6570)). Refs [pnpm/pnpm#12009](https://github.com/pnpm/pnpm/issues/12009). When `excludeLinksFromLockfile: true` is set, pacquet now: - Omits bare `link:` direct dependencies from each importer's `pnpm-lock.yaml` entry (keeping `workspace:`-resolved links recorded). - Remaps external link targets to a stable `link:<rel-from-lockfile_dir-to-modules_dir>/<alias>` node id when seeding peer-resolution parents, so peer suffixes for snapshots that depend on a linked package don't carry the absolute path of the external link. The remap fires only when the link target is *outside* the lockfile root (per upstream's `isSubdir` gate). - Round-trips the setting into the lockfile's top-level `settings:` block so `getOutdatedLockfileSetting` can spot drift on the next install. ## Where it's hooked (mirrors pnpm) - **Config flag**: `pacquet-config` adds `exclude_links_from_lockfile: bool` (default `false`) — mirrors [`config/reader/src/Config.ts:71`](https://github.com/pnpm/pnpm/blob/094aa6e57b/config/reader/src/Config.ts#L71) and the [`false` default](https://github.com/pnpm/pnpm/blob/094aa6e57b/config/reader/src/index.ts#L144). - **Importer entry**: `dependencies_graph_to_lockfile::build_importer` mirrors upstream's [`addDirectDependenciesToLockfile` exclude-link gate](https://github.com/pnpm/pnpm/blob/094aa6e57b/installing/deps-resolver/src/index.ts#L449-L456) — drops `link:` direct deps from the importer entry unless their manifest specifier starts with `workspace:`. - **Peer-resolution remap**: `resolve_peers::build_importer_parents` ports the [`target` rewrite at `index.ts:232-244`](https://github.com/pnpm/pnpm/blob/094aa6e57b/installing/deps-resolver/src/index.ts#L232-L244). External `link:` parents are seeded into `ParentRefs` with a remapped node id (`link:<rel-from-lockfile_dir-to-modules_dir>/<alias>`), so the peer suffix stays stable across machines. - **Peer-id translation**: `build_peer_id` special-cases `link:` node ids and emits `PeerId::Pair { name: peer_alias, version: link_path_to_peer_version(rel) }` — the exact port of upstream's [`peerNodeIdToPeerId` link arm](https://github.com/pnpm/pnpm/blob/094aa6e57b/installing/deps-resolver/src/resolvePeers.ts#L984-L989). - **Snapshot child fallback**: a peer-resolved child whose node id is `link:<rel>` and isn't in `node_dep_paths` uses the link node id verbatim as the snapshot child ref — mirrors upstream's [`pathsByNodeId.get(childNodeId) ?? (childNodeId as DepPath)`](https://github.com/pnpm/pnpm/blob/094aa6e57b/installing/deps-resolver/src/resolvePeers.ts#L164) in `resolveChildren`. ## New shared helpers - `pacquet-deps-path::link_path_to_peer_version` — faithful port of upstream's [`linkPathToPeerVersion.ts`](https://github.com/pnpm/pnpm/blob/094aa6e57b/installing/deps-resolver/src/linkPathToPeerVersion.ts). Iterates Unicode scalars (not raw bytes), so multi-byte UTF-8 path segments round-trip intact; pinned by a `non_ascii_path_segments_round_trip` test covering Latin-1, CJK, and a non-BMP emoji. - `pacquet-fs::is_subdir` — promoted from a private `pacquet-cmd-shim` helper so the resolver and the cmd-shim share one implementation. Mirrors npm's [`is-subdir`](https://github.com/zkochan/packages/blob/main/is-subdir/index.js). |
||
|
|
7ecaf3d5a6 |
feat(pacquet/deps-resolver): port dedupePeers setting (#12022)
Port the [`dedupePeers`](https://pnpm.io/settings#dedupepeers) setting from pnpm. When enabled, peer-dependency suffixes in `depPath`s use version-only identifiers (`name@version`) instead of recursive dep paths, collapsing nested suffixes like `(@emotion/react@11.0.0(react@18.0.0))` into `(@emotion/react@11.0.0)`. Default `false`, matching pnpm. - **Config plumbing:** `Config.dedupe_peers`, `WorkspaceSettings.dedupe_peers`, `PACQUET_DEDUPE_PEERS` env overlay, and the `clear_workspace_only_fields` / `apply_to` wiring. - **Resolver behavior:** `ResolvePeersOptions.dedupe_peers` threaded through `ResolveImporterOptions` and consumed in `Walker::build_peer_id` — when on, emits `PeerId::Pair { name, version }` from the resolved package instead of the peer's own `DepPath`. Mirrors pnpm's [`peerNodeIdToPeerId` `dedupePeers` branch](https://github.com/pnpm/pnpm/blob/39101f5e37/installing/deps-resolver/src/resolvePeers.ts#L990-L997). - **Lockfile round-trip:** `LockfileSettings.dedupe_peers: Option<bool>` (omitted when `false`, mirroring pnpm's `dedupePeers: opts.dedupePeers || undefined`). `GraphToLockfileOptions.dedupe_peers` plumbed from `install_with_fresh_lockfile`. - **Optimistic-repeat-install drift:** `WorkspaceStateSettings.dedupe_peers` is now compared by `settings_match` and written by `current_settings`. A switch to the setting between installs invalidates the cached-modules fast path. Closes one of the items on #12009. |
||
|
|
2cadfb5d3d |
refactor: replace enquirer with @inquirer/prompts (#11942)
Replaces the unmaintained `enquirer` package with `@inquirer/prompts` for all interactive CLI prompts. Fixes the `update -i` scrolling overflow bug where long choice lists were clipped in the terminal. Fixes #6643 ## User-facing changes - **`pnpm update -i` / `pnpm update -i --latest`**: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination via `usePagination` - **`pnpm audit --fix -i`**: Same scrolling fix for vulnerability selection - **`pnpm approve-builds`**: Interactive build approval prompts updated - **`pnpm patch`**: Version selection and "apply to all" prompts updated - **`pnpm patch-remove`**: Patch removal selection updated - **`pnpm publish`**: Branch confirmation prompt updated - **`pnpm login`**: Credential prompts updated - **`pnpm run` / `pnpm exec`** (with `verifyDepsBeforeRun=prompt`): Confirmation prompt updated ## Internal changes - `OtpEnquirer` DI interface changed from `{ prompt }` to `{ input }` - `LoginEnquirer` DI interface changed from `{ prompt }` to `{ input, password }` - `enquirer` removed from catalog and all 8 package.json files - `@inquirer/prompts` v8.4.3 added to catalog and all 8 package.json files - Removed `OtpPromptOptions` and `OtpPromptResponse` exports from `@pnpm/network.web-auth` (no longer needed) --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
39101f5e37 |
fix: hang on cyclic aliased peer dependency (#12018)
- `pnpm i nuxt@npm:nuxt-nightly@5x` (and similar aliased installs) hung at 0% CPU during peer resolution after `resolved N, reused 0, downloaded N, added 0`. - `resolvePeers.calculateDepPath` only short-circuited cycles whose members included `currentAlias`. When two peers form a mutual cycle (e.g. `vite` ↔ `@vitejs/devtools`) and both hit the `findHit` cache instead of running their own `calculateDepPath`, the cycle surfaced at a level where no participant could break it — a sibling's `calculateDepPath` saw the cycle in the `cycles` argument but kept awaiting `pathsByNodeIdPromises` on cyclic peer node IDs. - The fix expands `cyclicPeerAliases` to also include any cycle that intersects the current call's pending peers, so awaiting siblings emit the `name@version` peer id and the cached promise gets released. - Pacquet's `resolve_peers` walks synchronously with an `in_progress` set and returns already-realized `DepPath` values from `find_hit`, so the deadlock does not occur there. A pacquet regression test locks in that the aliased-install + transitive-mutual-peer scenario terminates with the expected graph entries. Closes #11999. |
||
|
|
b1fa2d5979 |
fix(dist-tag): support npm's web-based 2FA flow (#11998)
* fix(dist-tag): open the browser for npm web 2FA when --otp is absent
Without `--otp`, `pnpm dist-tag add` (and `rm`) failed against
npmjs.org with `[ERR_PNPM_UNAUTHORIZED] You must be logged in to set
dist-tag … "You must provide a one-time pass. Upgrade your client to
npm@latest in order to use 2FA."` — the browser never opened. The
fallback "upgrade your client" message is what npmjs.org returns when
the client doesn't announce `npm-auth-type: web`; without that header
the server skips the web challenge and tells the user to install a
newer npm. `--otp=<6-digit code>` already worked because the OTP went
out in `npm-otp` directly.
Send `npm-auth-type: web` on dist-tag writes when no `--otp` is
given, surface 401 responses carrying `authUrl`/`doneUrl` (or the
legacy "one-time pass" text) as `SyntheticOtpError`, and wrap the
call in the existing `withOtpHandling` helper (already used by
`pnpm publish`), which opens the browser, polls the done URL, and
retries with the resulting token as `npm-otp` while keeping
`npm-auth-type: web` in place.
Drive-by cleanup in `@pnpm/network.fetch`: the abbreviated-metadata
`Accept` header is no longer attached to non-GET requests, matching
`npm-registry-fetch`'s behavior.
* fix(dist-tag): default authType to 'web'; inherit network config in OTP context
Two review fixes:
- `setDistTag` documented `authType` as defaulting to `'web'` but only
sent the `npm-auth-type` header when the field was explicitly passed.
Always send the header, defaulting to `'web'`.
- `OTP_CONTEXT` used a module-level `createFetchFromRegistry({})`, so
the `withOtpHandling` doneUrl poll ignored the command's proxy / TLS
/ `configByUri` config. Build the OTP context per call from the
command's `opts` instead.
Also rename `toOtpOrUnauthorizedError` → `parseAuthError` and drop the
spurious `async` (the body string is already awaited at the call site).
|
||
|
|
623542873a |
fix(npm-resolver): revalidate excluded packages instead of trusting mtime cache (#12010)
Skip the publishedBy file-mtime fast path for fully excluded packages so stale abbreviated metadata cannot pin older versions, and add matching regression tests. |