mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-30 19:46:44 -04:00
dependabot/github_actions/github-actions-48d2ea223b
11684 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2034fc0ffb |
chore(deps): bump the github-actions group across 1 directory with 8 updates
Bumps the github-actions group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github/codeql-action](https://github.com/github/codeql-action) | `4.35.5` | `4.36.0` | | [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.0.0` | `4.1.0` | | [docker/login-action](https://github.com/docker/login-action) | `4.1.0` | `4.2.0` | | [docker/build-push-action](https://github.com/docker/build-push-action) | `7.1.0` | `7.2.0` | | [taiki-e/install-action](https://github.com/taiki-e/install-action) | `2.78.1` | `2.79.5` | | [crate-ci/typos](https://github.com/crate-ci/typos) | `1.46.1` | `1.46.2` | | [codecov/codecov-action](https://github.com/codecov/codecov-action) | `6.0.0` | `6.0.1` | | [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.5.5` | `0.5.6` | Updates `github/codeql-action` from 4.35.5 to 4.36.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits]( |
||
|
|
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. |
||
|
|
8c2d53bea1 |
feat(registry): add whoami, profile, and token CRUD endpoints (#12011)
Wires the five remaining auth surfaces from the [pnpr parity tracking issue](https://github.com/pnpm/pnpm/issues/11973) — everything under **Auth & user endpoints** except the pluggable-auth decision. - `GET /-/whoami` — `{ username }` for the bearer caller, 401 anonymous. - `GET /-/npm/v1/user` — profile shape (`name`, `tfa`, `email`, `cidr_whitelist`, `fullname`) that `npm profile get`'s table renderer parses cleanly. - `GET /-/npm/v1/tokens` — `{ objects, urls }` listing only the caller's tokens. `key` is the SHA-256 hex digest of the raw token (matches what `npm token revoke` sends back); `token` is the 6-char preview surfaced when the original value isn't recoverable, which is what verdaccio does for the same reason. - `DELETE /-/npm/v1/tokens/token/:key` — `npm token revoke`. Ownership-gated: 401 anonymous, 403 cross-user, 404 unknown key. Persists through the SQLite store so a restart can't resurrect a revoked token. - `DELETE /-/user/token/:tok` — `npm logout`. Looks up by raw token, revokes by hash; same gating as the listing-side revoke. `TokenStore` gains `find_by_key`, `list_for_user`, `revoke_by_key`, and `revoke_by_raw`; `publish::now_iso` is split so token timestamps render in the same ISO-8601 shape as `time.modified`. |
||
|
|
2a0032edc0 |
fix(pacquet/workspace): support negated workspace package globs (#11961)
Fixes #11949. Pacquet fails with `pacquet_workspace::invalid_glob` when `pnpm-workspace.yaml` uses negated `packages` patterns such as `!**/tools/*`. The failure occurs because `wax` only supports `!` within character classes (e.g., `[!a]`), not as a leading glob negation. This change separates negated patterns from include patterns and applies them via `wax`'s ignore path. It also preserves pnpm's `!/foo` behavior, which is a no-op for relative workspace paths. This PR keeps the changes minimal and local to workspace project discovery, avoiding unnecessary refactoring or moving logic into shared crates. Added workspace tests covering both `!libs/**` exclusion and the `!/libs/**` no-op case. |
||
|
|
87136edaa3 |
fix(registry): verify tarball SHA on publish (#11976)
Closes #11975. `pnpm-registry`'s `PUT /:pkg` endpoint accepted tarballs without verifying that their bytes matched the integrity declared in the packument. A buggy or partial-upload client could land a tarball whose hash didn't match `dist.integrity` / `dist.shasum`, and a malicious client could deliberately decouple the advertised hash from the bytes — both classes of silent-corruption bug that only surface when downstream `pnpm install` later fails with `EINTEGRITY`. This change hashes every attachment before any I/O happens and rejects mismatches up-front so the bad bytes never reach the cache. The decode + hash + write runs in one streaming pass so the full decoded payload never lives in memory at once. ## What lands - `stream_decode_verify_and_write` in `publish.rs` — pulls base64 chunks out of `base64::read::DecoderReader`, feeds each 64 KiB chunk to an ssri `IntegrityChecker` (SHA-512), an optional `IntegrityOpts(Sha1)` for legacy shasum, and the on-disk tmp file in lockstep. Verifies declared `length`, integrity, and shasum at the end; any failure removes the tmp file before returning. - All failure modes surface as `400 BAD_REQUEST` with an `EINTEGRITY:` prefix in the body so pnpm / npm clients can recognize the error code. - `extract_attachments` no longer base64-decodes eagerly; it returns `PendingAttachment { filename, data, declared_length }` and lets the streaming path consume `data` directly. - `PackageName::parse_tarball_name` returns `(canonical, version)` so the publish handler can pull the matching `versions[v].dist` block out of the body. `canonicalize_tarball_name` becomes a thin wrapper. - `Cache::reserve_tarball_paths` + `finalize_tarball_slot` expose a tmp/final path pair so the publish handler can do the actual write inside `tokio::task::spawn_blocking` (sync `std::fs`) and finalize via async `tokio::fs::rename` afterward. - `publish_package` calls `stream_decode_verify_and_write` *before* any tarball lands at its final path. On any failure it removes every in-progress tmp file so a rejected publish leaves no on-disk artifact. ## Memory win On a 100 MiB tarball publish, the old flow held the full payload in three places simultaneously (HTTP body Bytes + serde_json owned base64 String + decoded `Vec<u8>`). The streaming flow drops the decoded `Vec<u8>` entirely — only the base64 string and a 64 KiB working buffer remain. Roughly 100 MiB of peak heap saved per concurrent publish. |
||
|
|
f5490def66 |
perf(pacquet/package-manager): treat unsupported settings as no-opinion in optimistic-repeat-install (#12005)
The `state.settings == current` check compared every field on `WorkspaceStateSettings`, including settings pacquet doesn't yet implement (`peersSuffixMaxLength`, `dedupeDirectDeps`, `packageExtensions`, …). Pnpm's defaults populate those fields when it writes the state, while pacquet's `current_settings` leaves them `None`, so any cross-package-manager scenario (pnpm wrote the workspace state, pacquet runs next) rejected the fast path on iter 1 and fell into the slower frozen no-op short-circuit. That's the 9.09× `babylon × lockfile+node_modules` regression in pnpm/pnpm#11992. Switch `settings_match` to compare only the fields `current_settings` actively writes. Mirrors pnpm's [`Object.entries(workspaceState.settings)`](https://github.com/pnpm/pnpm/blob/72d997cc34/deps/status/src/checkDepsStatus.ts) walk: pnpm iterates the settings present in the state, which by symmetry are the settings the writer cared about. Pacquet's `current_settings` is the symmetric "settings pacquet cares about" set, so comparing against it is the natural way to honour the same contract. Special-case `allowBuilds`: pnpm writes `Some({})` for an empty allow-list, pacquet writes `None` — both mean "no opinion," so treat them as equivalent (mirroring pnpm's `opts.allowBuilds ?? {}` coercion on the read side). The settings *not* compared are tracked at pnpm/pnpm#12009 with a matching skip-list comment in code; each one drops out of the skip list as it's ported end-to-end (yaml plumbing → `Config` field → real install consumer → joined into `current_settings`). Two extra groups (`minimumReleaseAge*`, `trustPolicy*`) hitch a ride: pacquet *does* consume them during install, but they aren't surfaced through the workspace-state crate yet. And two stay outside the comparison permanently — `catalogs` (pnpm itself always ignores) and `workspacePackagePatterns` (covered via `WorkspaceManifest.packages` from `pnpm-workspace.yaml`). End-to-end on `vltpkg/benchmarks/fixtures/babylon`, after pnpm has written the workspace-state file: - before fix: pacquet iter-1 falls through to the frozen no-op short-circuit (~531 ms locally, ~9× pnpm on vlt CI). - after fix: pacquet iter-1 fires the optimistic fast path (~96 ms locally, faster than pnpm's own warm fast path at ~687 ms — same 7-8× lead as iter-2+). The `returns_skipped_when_unported_pnpm_settings_present` test that locked in the conservative posture is replaced with `returns_up_to_date_when_state_carries_unported_pnpm_settings`, which exercises every #12009 setting plus `catalogs` and `workspacePackagePatterns` together. A second new test, `returns_up_to_date_when_state_has_empty_allow_builds_and_current_has_none`, locks in the `allowBuilds` empty-vs-`None` coercion. Closes pnpm/pnpm#11992. Refs: pnpm/pnpm#12009 |
||
|
|
0cefccf158 |
feat(registry): persist pnpr users and tokens to disk (#11977)
* feat(registry): persist pnpr users and tokens to disk
Backs UserStore with a verdaccio-shaped htpasswd file (bcrypt $2y$
hashes, atomically rewritten on every adduser) and TokenStore with a
SQLite database that stores SHA-256 token hashes plus the per-record
fields the upcoming /-/npm/v1/tokens surface will need (created_at,
last_used_at, readonly, cidr_whitelist).
Configuration mirrors verdaccio's auth.htpasswd.{file,max_users}
under the existing YAML schema; tokens default to a tokens.db
sibling of htpasswd, overridable via auth.tokens.file. max_users=-1
disables registration end-to-end. Both files are written via
tmp+rename and loaded eagerly on startup so a malformed htpasswd
fails fast rather than booting with a silent empty user list.
Closes #11974.
* fix(registry): use OS CSPRNG, satisfy dylint + rustdoc
- TokenStore's per-process secret now comes from getrandom (OS-backed
CSPRNG) instead of time/pid/stack address. Tokens are derived from
this secret + a per-issue nonce, so weak entropy was making mint
outputs guessable to an attacker who could bound those inputs.
- Reorder derives on AuthConfig / HtpasswdConfig / TokensConfig /
MaxUsers to satisfy perfectionist::derive-ordering (prefix-then-
alphabetical: Debug, Default first, then the rest).
- Re-export auth::identify so the rustdoc link from the now-public
UserStore::verify resolves; rustdoc::private-intra-doc-links no
longer fails the workspace doc build.
- Drop the inaccurate "+inf" mention from MaxUsers' doc — serde-saphyr
treats +inf as a float and can't deserialize it into i64, so the
only way to get Unlimited is to omit max_users.
|
||
|
|
a33c4bfcb0 |
perf: skip resolution when only pnpm-lock.yaml is missing (pnpm + pacquet) (#12004)
* perf: skip resolution when only pnpm-lock.yaml is missing
When pnpm-lock.yaml is absent but node_modules/.pnpm/lock.yaml exists and still
satisfies the manifest, reuse the materialized snapshot to regenerate the
wanted lockfile instead of walking the registry to rebuild it. Closes the
cache+node_modules variation gap in the vlt.sh benchmarks for the pnpm CLI
side; the pacquet port is tracked separately at #11993.
`--frozen-lockfile` still fails when pnpm-lock.yaml is absent: the regenerated
file must be committed, so failing loudly is the correct behavior for CI.
* perf(pacquet): port the cache+node_modules shortcut
When `pnpm-lock.yaml` is absent but `node_modules/.pnpm/lock.yaml` exists
and still satisfies the manifest, synthesize the wanted lockfile from the
materialized snapshot and take the frozen-install path. The install skips
resolution and regenerates `pnpm-lock.yaml` from the synthesized object.
Mirrors the pnpm-side change at
|
||
|
|
c94b4f89c7 |
fix: publish with default access (#11991)
* fix: preserve default publish access * chore(publish): add changeset |
||
|
|
72d997cc34 | chore(release): 11.4.0 (#11989) v11.4.0 | ||
|
|
b73908f088 | chore: update pnpm-lock.yaml (#11897) | ||
|
|
aa6149df65 |
fix: fail by default when a tarball does not match the locked integrity (#11968)
`pnpm install` (non-frozen) used to react to `ERR_PNPM_TARBALL_INTEGRITY` by logging the error, silently re-resolving from the registry, and overwriting the locked integrity. The lockfile's integrity was effectively advisory by default — a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed `pnpm-lock.yaml`. Integrity mismatches against the lockfile now fail by default. The **only** opt-in is **`pnpm install --update-checksums`** — a new flag, narrowly scoped to refreshing the locked integrity values. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the rewrite stays auditable. `--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass. Combining `--frozen-lockfile` with `--update-checksums` errors out — frozen mode refuses to rewrite the lockfile, which is exactly what `--update-checksums` is for. `--update-checksums` also bypasses the resolver's on-disk metadata cache fast path (`pickPackage.ts:271`, `pick_package.rs:531`). Without that, a stale on-disk packument that already contained the pinned version would short-circuit the registry entirely and the flag would silently no-op on dev machines. With the gate, every first-encounter goes through a conditional GET; the in-memory cache is left alone so second-and-onward references within the same install still hit cached fresh data (one network round-trip per *unique* package, not per reference). ## Reported by Reported privately via the security channel. The reproduction: 1. Publish `example-package@1.0.0` with content `v1` and install with pnpm; lockfile records the `v1` integrity. 2. Replace the registry's tarball+metadata for the same `1.0.0` with content `v2`. 3. On a clean store/cache, run `pnpm install`. Before this fix, pnpm logged `ERR_PNPM_TARBALL_INTEGRITY` but exited 0 with `v2` installed and the lockfile rewritten to the new integrity. After this fix, the same install exits non-zero. ## Prior art - **npm** ([sebhastian](https://sebhastian.com/npm-err-code-eintegrity/)): hard-fails with `EINTEGRITY`. No dedicated override flag — recovery is `npm cache clean --force`, manually editing the lockfile, or deleting it. - **yarn** ([Sean C Davis](https://www.seancdavis.com/posts/fix-yarn-integrity-check-failed/)): hard-fails with "Integrity check failed". Has a dedicated **`yarn install --update-checksums`** flag — pnpm now adopts the same name. ## Pacquet parity Pacquet was already fail-hard on integrity mismatch by default (no auto-repair path to remove). This PR brings the rest of the surface into line so `pnpm install --update-checksums` keeps working when pacquet is the materialization target, and `pacquet install --update-checksums` behaves identically standalone: - New `--update-checksums` flag on `pacquet install` (`crates/cli/src/cli_args/install.rs`), plumbed through `Install` and `InstallWithFreshLockfile` into the resolver. - When the flag is set, pacquet skips the frozen-lockfile fast path and routes through the fresh-resolve path so locked integrity values get rewritten from the registry. - `--frozen-lockfile + --update-checksums` errors with `pacquet_package_manager::frozen_lockfile_with_outdated_lockfile`, mirroring pnpm's `ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE`. - `pacquet_tarball::verify_checksum_error` now carries a help hint pointing at `--update-checksums` and calling out the supply-chain implication, matching the updated pnpm `TarballIntegrityError`. - The disk fast-path gate is mirrored in `crates/resolving-npm-resolver/src/pick_package.rs:531`, with the flag threaded from `ResolveOptions` → `PickPackageOptions`. |
||
|
|
c12681f68d |
docs(registry): flesh out @pnpm/pnpr README (#11972)
* docs(registry): flesh out @pnpm/pnpr README Document install, default behavior, CLI flags, RUST_LOG, and a minimal verdaccio-shaped YAML config example. * docs(registry): reword pnpr intro Drop the "runs locally / verdaccio-like" framing — pnpr is hostable and not scoped to a local-dev role. * chore(cspell): add packuments, refetched |
||
|
|
f03dc2d15d |
refactor(registry): adopt verdaccio-shaped YAML config (#11970)
Reshape pnpm-registry's Config to match verdaccio's `config.yaml` schema (storage, uplinks, packages) so the same file can drive either server. The previous Config exposed a single `upstream: Option<String>` resolved at startup; this replaces it with named uplinks plus per-package `proxy:` rules walked in declared order — same semantics as verdaccio, minus the surface pnpm-registry does not implement (auth, web, plugins, middlewares, logs routing, secret), which are accepted and ignored.
Highlights:
* `Config { listen, public_url, storage, uplinks, packages, packument_ttl }` with `UplinkConfig { url }` and `PackageAccess { access, publish, unpublish, proxy }`. `packages` is an `IndexMap` walked in declared order, first-match-wins: the first pattern matching a request is the rule that applies, and if that rule has no `proxy:` the package is storage-only (resolution returns `None` instead of falling through to a later catch-all). That makes the bundled `@private/*` / `@pnpm.e2e/needs-auth` / unscoped-fixture rules behave the way the YAML says they should.
* `Config::from_yaml(path, ...)` loads via `serde-saphyr` and resolves a relative `storage:` against the config file's parent. The verdaccio-only sections in the YAML are skipped silently so `registry-mock`'s upstream `config.yaml` parses untouched.
* `DEFAULT_CONFIG_YAML` — the bundled file mirrored from `@pnpm/registry-mock` — is `include_str!`-ed and re-exported from `lib.rs` so other crates (tests, benchmarks, future embedders) can use the same defaults without reading from disk. `Config::from_default_yaml(base_dir, ...)` parses it.
* CLI: `-c` / `--config <path>` overrides the bundled default. `--storage` survives as a runtime override (handy for tests without a custom YAML); `--upstream` and `--static` are gone because the YAML now drives both. `--packument-ttl-secs` is optional — the loaded config's value wins when the flag is not supplied. The mock orchestrator at `pacquet/tasks/registry-mock` drops its `--upstream` flag — that command spawns the locally-built binary so it tracks this PR's source directly. The jest harness at `__utils__/jest-config/with-registry/globalSetup.js` keeps `--upstream` for now because CI installs `@pnpm/pnpr` from npm; the flag will be dropped in the same PR that bumps `@pnpm/pnpr` to a build that lacks it.
* `server.rs` pre-builds one `Upstream` per declared uplink at router construction (keyed by name, in an `IndexMap`) and resolves the right client per request via `Config::resolve_uplink`. No per-request `ThrottledClient` allocations.
Existing tests are kept working by retaining the `Config::proxy` / `Config::static_serve` constructors and switching the test helper from `config.upstream = ...` to mutating
`config.uplinks["npmjs"].url`. All 107 tests in `pnpm-registry` pass (55 unit + 26 + 9 + 17 integration).
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **New Features**
* YAML-based registry configuration with package-level routing, multiple uplinks, auth and audit middleware
* New --config option to load custom registry configs; optional storage override and packument TTL setting
* **Chores**
* Default registry now uses the bundled configuration (web UI disabled by default)
* Configuration refactor to support Verdaccio-style routing patterns and per-package access/publish rules
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
|