mirror of
https://github.com/pnpm/pnpm.git
synced 2026-10-11 00:32:05 -04:00
9daea1ecb021ff7b5a966e3541baca98df2beaaa
11677
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
e55f4b5efd |
fix: require integrity for tarball-shaped lockfile resolutions (#11966)
* fix(lockfile.utils): require integrity for tarball-shaped lockfile resolutions A tampered lockfile that strips the `integrity` field from a tarball resolution let the worker download the URL contents and mint a fresh integrity from the unverified bytes, so an attacker who could also serve content at the referenced URL would install a tampered package without any error — including under `--frozen-lockfile`. pnpm now rejects such entries at lockfile-read time with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`, matching pacquet's existing `pacquet_package_manager::missing_tarball_integrity` guard. * test(lockfile.utils): drop redundant integrity-less snapshot that fails strict typecheck * test(pacquet/package-manager): cover MissingTarballIntegrity rejection in snapshot_cache_key Match the upstream guard landed alongside pnpm/pnpm#11966 (`lockfile/utils/src/pkgSnapshotToResolution.ts`) with a test on the pacquet side: a `LockfileResolution::Tarball` with `integrity: None` — what a tampered lockfile looks like — must short-circuit the warm-batch cache-key derivation by surfacing `InstallPackageBySnapshotError::MissingTarballIntegrity`. The structural guard already existed but had no negative test. * fix(lockfile.utils): exempt git-hosted and file: tarballs from the integrity guard The strict guard added in the parent commit broke pnpm's own `with-git-protocol-dep` and `with-non-package-dep` fixtures: the install pipeline writes git-hosted tarball entries (codeload.github.com URLs) to the lockfile without an `integrity:` line, because the commit SHA in the URL is the integrity anchor — git's content-addressed model binds the bytes to the commit, so a separate hash adds nothing. Exempt git-hosted tarballs (detected either via the `gitHosted: true` flag or a URL on the known git hosts, matching the URL fallback in `toLockfileResolution`) and `file:` tarballs (local paths the user already controls). The strict check still fires for any other remote tarball — which is where the AutoFyn-reported vector actually manifests. Also export `isGitHostedTarballUrl` from `toLockfileResolution.ts` so the URL fallback can be shared rather than duplicated. * test(pacquet/package-manager): trim doc comment to the contract-level intent Per the repo convention that tests are documentation, the test name and body already cover what's being asserted; the prior comment duplicated that. Keep only the non-obvious why: why this guard exists at the cache-key site at all (warm-batch short-circuit) when the install-side check also rejects the same input. |
||
|
|
90d1ce6b60 |
fix(git-fetcher): reject non-SHA commit values before invoking git (#11967)
`fetching/git-fetcher/src/index.ts` passed the lockfile-controlled `resolution.commit` value straight to `git fetch --depth 1 origin <commit>` and `git checkout <commit>` with no `--` separator and no format validation. A malicious `pnpm-lock.yaml` could put a value such as `--upload-pack=touch /tmp/pwned` in `resolution.commit`; `git` parses anything starting with `-` as an option, and on SSH or local-file transports `--upload-pack` runs the supplied command as the user running `pnpm install`. HTTPS ignores `--upload-pack`, but the SSH/file paths are enough to reach code execution.
The fix validates `resolution.commit` against `/^[0-9a-f]{40}$/i` at the entry of the fetcher and throws `INVALID_GIT_COMMIT` otherwise. This is strictly stronger than adding a `--` separator — a validated value cannot start with `-` or contain shell-significant characters at all.
Pacquet's `pacquet-git-fetcher` crate shells out to `git` along the same code path (`pacquet/crates/git-fetcher/src/fetcher.rs`) and had the identical issue. Ported the same check there, with a new `GitFetcherError::InvalidCommit` variant carrying the `INVALID_GIT_COMMIT` diagnostic code.
Reported by [AutoFyn](https://github.com/SignalPilot-Labs/AutoFyn).
|
||
|
|
89c2b52728 |
ci(pacquet): pin cargo-dylint to 6.0.0 to unblock CI (#11969)
trailofbits/dylint 6.0.1 (published 2026-05-26 17:51 UTC) ships
prebuilt cargo-binstall artifacts that bake in a path from the
dylint repo's own CI workspace:
error: failed to get `dylint_driver` as a dependency of package
`dylint_driver-nightly-2026-04-16-x86_64-unknown-linux-gnu`
Caused by:
failed to read `/home/runner/work/dylint/dylint/driver/Cargo.toml`
Caused by:
No such file or directory (os error 2)
Downstream runners don't have that workspace, so the driver bootstrap
fails before any lint runs and the Dylint job goes red on every PR.
6.0.0 (the version main was passing with 90 minutes earlier) is
unaffected. Pin both binaries until upstream cuts 6.0.2.
---
Written by an agent (Claude Code, claude-opus-4-7).
|
||
|
|
f107b1e13a |
ci(test): install @pnpm/pnpr from npm instead of building locally (#11964)
The e2e/integration test harness spawns `pnpm-registry` as a faster verdaccio replacement. CI used to install Rust and build the crate from source on every test job — adding several minutes per platform. `@pnpm/pnpr` now publishes the prebuilt binary to npm, and `pnpm install` already pulls in the matching `@pnpm/pnpr.<platform>-<arch>` package via optionalDependencies. The Jest globalSetup resolves that binary through `@pnpm/pnpr/bin/pnpr`'s own module path (the wrapper carries the platform packages as siblings in its `node_modules`, not on the parent chain of this file). - Add `@pnpm/pnpr` to `pnpm-workspace.yaml` catalog and depend on it from `@pnpm/jest-config`. - Replace `resolvePnpmRegistryBin`'s `$CARGO_TARGET_DIR` lookup with `require.resolve` through the npm-installed wrapper. The `PNPM_REGISTRY_BIN` env var is still honored as an escape hatch for contributors who want to point at a locally-built Rust binary. - Remove the "Install Rust toolchain" + "Build pnpm-registry" steps from `.github/workflows/test.yml`. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
0f5f338a89 | chore: add pnpr to cspell | ||
|
|
f578281d2a |
feat(pacquet/config): --recursive, --workspace-concurrency (#11959)
* feat(pacquet/config): port --recursive and --workspace-concurrency settings Mirror pnpm's `workspaceConcurrency` (a `.npmrc` / `pnpm-workspace.yaml` / `PNPM_CONFIG_WORKSPACE_CONCURRENCY` config-file key, default `getDefaultWorkspaceConcurrency()`, resolved through `getWorkspaceConcurrency`) and CLI-only `recursive` (`-r`) boolean. - Add `Config::workspace_concurrency` (resolved via the existing `resolve_child_concurrency` port of `getWorkspaceConcurrency`) and `Config::recursive`, plus `default_workspace_concurrency`. - Read `workspaceConcurrency` from workspace yaml, global config.yaml, and the `PNPM_CONFIG_*` env overlay; resolve negative offsets the same way `childConcurrency` does. - Add the `--workspace-concurrency` install flag (overrides the config-resolved value) and the global `-r` / `--recursive` flag (sets `Config::recursive`, matching pnpm's CLI-only nature). `workspaceConcurrency` is parsed and stored for config-surface parity; pacquet's frozen install materializes the whole workspace in one shared pass, so there is no per-project parallel loop for it to throttle yet (same posture as `preferOffline`). `recursive` is likewise a surface flag on install today, since install already spans the workspace. * docs(pacquet/config): drop private intra-doc link to fix Doc CI `default_workspace_concurrency`'s public doc linked the crate-private `default_child_concurrency`, which `rustdoc -D rustdoc::private-intra-doc-links` (implied by `-D warnings`) rejects. Use plain backticks instead. * style(pacquet/cli): drop redundant comment on workspace_concurrency destructure bind The sibling `offline: _` / `prefer_offline: _` binds carry no comment; match them for consistency. * test(pacquet/cli): cover the --workspace-concurrency override resolution The inline `if let Some(value) = args.workspace_concurrency` override body was only reachable when install ran *with* the flag, so the flag-absent integration runs left it uncovered. Extract the resolution into `InstallArgs::resolve_workspace_concurrency` and apply it unconditionally at the dispatch (matching upstream's final `getWorkspaceConcurrency` pass), so the substantive logic is covered by fast unit tests (absent / positive / negative) and the call site is an unconditional assignment the existing install tests already exercise. * style(pacquet/cli): pipe-trait the workspace-concurrency test parse calls Per review: rewrite the `InstallArgsHarness::try_parse_from([...])` calls in the new workspace-concurrency tests as `[...].pipe(InstallArgsHarness::try_parse_from)` so they read left-to-right, matching the codebase's pipe-trait convention. * docs(pacquet/config): drop inaccurate .npmrc source from workspaceConcurrency docs workspace_concurrency is populated only from pnpm-workspace.yaml, global config.yaml, and the PNPM_CONFIG_WORKSPACE_CONCURRENCY env overlay. Config::current reads .npmrc but applies only the auth/network subset, so a workspace-concurrency= entry in .npmrc never reaches the field. Align the doc comments and PR summary with actual behavior, matching the sibling childConcurrency docs. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c42f69ffaf |
ci(pnpr): add manual release workflow that publishes @pnpm/pnpr to npm (#11963)
* ci(pnpm-registry): add manual release workflow that publishes to npm Mirror pacquet's release pipeline for the pnpm-registry crate: - New `Release pnpm-registry` GitHub workflow (manual `workflow_dispatch` with a version input) builds the `pnpm-registry` binary for six `<os>-<cpu>` matrix legs via `cross`, attests provenance, and uploads artifacts. - The publish job patches `registry/npm/pnpm-registry/package.json`'s `version` field with the input, downloads the artifacts, runs `generate-packages.mjs` to produce per-platform packages, then publishes everything via `pnpm publish --provenance` using OIDC. - The wrapper package is `pnpm-registry`; the per-platform binary packages are scoped `@pnpm/registry.<os>-<cpu>` and resolved by a Node shim under `bin/pnpm-registry`. The CLI's `--version` output comes from `CARGO_PKG_VERSION` via clap's derive `version` attribute, so the build leg patches the crate's `Cargo.toml` `version` field rather than a hardcoded clap string. --- Written by an agent (Claude Code, claude-opus-4-7). * ci(pnpr): rename package to @pnpm/pnpr with @pnpm/pnpr.<os>-<cpu> binaries Shorter, scoped name that reads naturally on npm: `@pnpm/pnpr` for the wrapper, `@pnpm/pnpr.<os>-<cpu>` for the six per-platform binary packages (mirroring `@pacquet/<os>-<cpu>` under a different scope). - Move `registry/npm/pnpm-registry/` to `registry/npm/pnpr/` and rename the JS shim and `bin` entry to `pnpr`. - Update `generate-packages.mjs` so `BIN_NAME = pnpr` (used for both the binary file inside each platform package and the autogenerated package dir) and the scope/prefix produce `@pnpm/pnpr.<os>-<cpu>`. - Rename the workflow file to `pnpr-release-to-npm.yml` and rename the built `pnpm-registry` binary to `pnpr-<code-target>` at archive time, so `generate-packages.mjs` finds it at `REPO_ROOT/pnpr-<plat>-<arch>`. - The Rust crate name stays `pnpm-registry` (still what `cross build -p` targets); only the npm-publishing layer is renamed. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(pnpr): propagate signal exits through the Node shim `spawnSync` returns `result.status === null` when the child terminates via a signal. Assigning that to `process.exitCode` makes the parent exit 0 and masks the signal — bad for a long-running server where the operator is most likely to kill it with SIGINT/SIGTERM. Re-raise the signal so the parent terminates the same way, falling back to a non-zero exit code if for some reason we can't. --- Written by an agent (Claude Code, claude-opus-4-7). |