Three papercuts that together make a lint cost a full pre-push sweep. `test-affected` took its diff against the local `main`, which is only as current as the last time someone checked it out — and `main` usually lives in another worktree here, so it lags. Every commit it lagged by read as a change of the current branch: crates someone else touched got tested, and a file every crate compiles against, landing upstream, made the whole run refuse to scope and send you to `just ready`. Six commits of lag was enough to attribute an unrelated `Cargo.lock` to a branch that had not touched it. A `--base` that names a branch now diffs against that branch's remote-tracking ref. A tag, a revision expression, a qualified ref and `HEAD` are taken as given, since each already names one commit. The pre-commit hook formats the Rust files being committed, with the pinned rustfmt. Formatting is the one Rust check that needs no compile, so it is the one that can run per commit; clippy and dylint stay in pre-push where their cost is paid once. A file with unstaged changes as well is left alone and named, since formatting the working tree and staging the result would commit the part of it the author held back. The pathnames are handled in Node, one argument per file, so a path holding a space, a glob character, or a leading `-` reaches rustfmt intact, and only a regular file is formatted: rustfmt writes through a symlink. Both lint failures in pre-push named the command that reports the findings rather than the one that applies them. `just fix` and the new `just dylint-fix` fix most of what either finds, which is the difference between one more sweep and several.
12 KiB
Contributing to pacquet
See also CODE_STYLE_GUIDE.md for the code style guide.
Scope and Version Policy
pacquet is pnpm v12 and the target for new feature development. New commands, settings, and other user-visible features are implemented here and are not backported to the TypeScript pnpm v11 CLI under ../pnpm11/.
For bug fixes, determine which supported versions contain the bug. A bug present in both v11 and v12 must be fixed and tested in both implementations. A bug present in only one version is fixed only in that version. See AGENTS.md for the full version policy.
Opening an issue first is optional for a clearly scoped change. Open one when the intended user-visible behavior or design is not obvious, or when coordination is needed for work already in progress.
Bug fixes, performance improvements, tests, and documentation may be sent directly as pull requests. New features must follow the repository's normal design, documentation, testing, and changeset requirements.
Commit Message Convention
This project uses Conventional Commits.
Format
type(scope): lowercase description
Rules
- Types:
feat,fix,refactor,perf,docs,style,chore,ci,test,lint. - Scopes (optional): a crate name (
cli,store,tarball,registry,lockfile,npmrc,network,fs,package-manager, etc.), or another relevant area such asdeps,readme,benchmark, ortoolchain. - Description: always lowercase after the colon, no trailing period, brief (3-7 words preferred).
- Breaking changes: append
!before the colon. For example:feat(cli)!: remove deprecated flag. - Code identifiers in descriptions should be wrapped in backticks. For example:
chore(deps): update `serde`.
There are no exceptions to this format. Version release commits follow the same rules as any other commit.
Writing Style
Write documentation, comments, and other prose for ease of understanding first. Prefer a formal tone when it does not hurt clarity, and use complete sentences. Avoid mid-sentence breaks introduced by em dashes or long parenthetical clauses. Em dashes are a reliable symptom of loose phrasing; when one appears, restructure the surrounding sentence so each clause stands on its own rather than swapping the em dash for another punctuation mark.
Code Style
See CODE_STYLE_GUIDE.md. Formatting and lint-level rules are enforced by the pinned rustfmt fork, taplo format, and cargo clippy; the style guide covers everything those tools cannot enforce.
Dylint / perfectionist
A separate CI job (Dylint) runs perfectionist over the workspace. perfectionist is early, unstable software and is not yet battle-tested, so it can produce false positives and false negatives.
If perfectionist flags code that is actually correct, or fails to flag code its rule description says it should, do not work around the lint silently:
- Silence the specific finding at the affected site with
#[expect(perfectionist::rule_name, reason = "...")]. Always include areason, and write it as a sentence explaining why the lint is wrong here. Do not use#[allow(...)];#[expect]errors when the suppression is no longer needed, so the workaround disappears once perfectionist is fixed. - Open a new issue on
KSXGitHub/perfectionistdescribing the false positive or false negative, with a minimal repro, and tag/cc @KSXGitHubin the issue body.
The same procedure applies when a perfectionist rule itself is wrong — for example, a rule that flags an idiom the rule's documentation says it should permit. Silence the site with #[expect(..., reason = "...")], link the upstream issue from the reason if one already exists, and file the issue if it does not. Do not edit dylint.toml to globally disable a rule, and do not pin perfectionist to an older tag to dodge a finding.
You can run the same check locally with just dylint. It requires cargo-dylint and dylint-link, which just init does not install; install them from source as described under Rust toolchain and git hooks in the root guide.
Setup
Prerequisites
Install these first:
rustupcargo-binstalljust- Node.js
pnpmgit
The repository root CONTRIBUTING.md covers the Rust toolchain and the tools the git hooks need, including a note on why cargo-dylint must be installed from source and why ~/.cargo/bin has to be on your PATH. Read it first, then use the pacquet-specific steps below.
Install
Install the project's task tools and the git pre-push hook:
just init
just init invokes cargo-binstall to install cargo-nextest, cargo-watch, cargo-insta, typos-cli, taplo-cli, wasm-pack, and cargo-llvm-cov, then installs cargo-fixit@0.1.15 from source with cargo install ... --locked (it has no prebuilt binaries). cargo-fixit backs the just fix task. It also installs the pinned formatter. The repo-wide pnpm install wires up husky, whose pre-commit hook formats the Rust files being committed and whose pre-push hook runs pnpm/scripts/pre-push-rust.sh (format, doc, dylint, typos) alongside the TypeScript compile and lint checks.
just init does not install the dylint tools. To run the Dylint job's checks locally, install cargo-dylint and dylint-link as described under Rust toolchain and git hooks in the root guide.
Install the test dependencies:
just install
The Python ecosystem end-to-end tests also require Python 3.10 or newer with
venv and either packaging or pip's bundled copy of packaging. CI installs
Python 3.13. These tests run the real pnpm CLI and Python interpreter.
The pnpr compiler-cache tests require sccache 0.17.0 with WebDAV support,
installed by just init. They run Cargo in isolated checkouts and use separate
sccache daemons to verify remote reuse and local cache backfill.
Automated Checks
Run this before every commit:
typos pnpm pnpr
just fmt
just check
just lint
Then run the tests that cover what you changed:
pnpm test:rust-affected
The root package.json scripts (test:rust-affected, test:rust, test:rust-smoke, check:rust, lint:rust, ready:rust, build:pnpm) wrap the just recipes and node scripts this section names, and they are the way to invoke them: a task concurrency group (concurrencyGroup on the task and a limit under concurrencyGroups in pnpm-workspace.yaml) holds the builds and test runs of every worktree on a machine to a limit it can carry, and only a run that starts as pnpm <script> is counted. Override a limit for your machine with PNPM_CONFIG_CONCURRENCY_GROUPS='{"cargo":1}' in your environment.
This maps the working tree's changes to crates and runs those crates' tests, with the same sanitized environment just test uses. It does not include the CLI end-to-end suite unless you changed pnpm-cli itself, so for a user-visible change add the suite modules for the area: pnpm test:rust-affected -- -p pnpm-cli -E 'test(catalog::)'. The testing-changes skill covers picking them.
When crates depend on what changed without being selected themselves, it also runs the smoke profile in their place: one end-to-end test per area of CLI behavior, rather than the dependents' full test sets or nothing at all. --no-smoke skips that, and just smoke runs the same set on its own.
Scope the tests, not the rest. just check and just lint stay workspace-wide: both cost far less than the test run, and they catch the cross-crate breakage a -p selection hides.
CI runs the full suite on Linux, macOS, and Windows for every pull request, so there is no need to reproduce it locally first. Run everything yourself when the change reaches past the crates you can name — a workspace dependency, Cargo.lock, rust-toolchain.toml, a shared crate such as pnpm-testing-utils, or a rename that crosses crate boundaries:
just ready
just ready runs typos, the pinned formatter, just check (cargo check --locked --workspace --all-targets), just test (cargo nextest run over the whole workspace), and just lint (cargo clippy --locked --workspace --all-targets -- --deny warnings), then prints git status. These are the same commands CI runs.
just test-pacquet and just test-pnpr split the suite along the product boundary when you want more than one crate but less than everything. Select every pnpr-* crate together rather than one alone: cargo's feature unification gives a lone crate a bare feature set, and its backend tests then silently skip.
To let clippy rewrite the lints it can fix automatically, run just fix instead of hand-editing each warning:
just fix
just fix runs cargo fixit --clippy --workspace --all-targets --allow-dirty --allow-staged (via the pinned cargo-fixit). It is faster than cargo clippy --fix on repeated runs because cargo fixit skips the full re-check compile between fix rounds, so iterating on a lint cleanup does not rebuild the workspace each pass. Run just lint afterward to confirm no warnings remain (clippy can't autofix everything).
Important
A change that touches no Rust source — a documentation edit, a comment change, a config tweak — still needs
typos pnpm pnprand the formatter. It does not need the test suite.
Note
Integration tests that need the local registry mock start
pnprautomatically. After dependencies are installed,cargo test,cargo nextest run, andjust testshould not require a separate registry process.
Debugging
Set the TRACE environment variable to enable trace-level logging for a given module:
TRACE=pnpm_tarball just cli add fastify
Testing
just install # install necessary dependencies
just test-affected # the crates the working tree changes
just smoke # one end-to-end test per area of CLI behavior
just test # run every test in the workspace
just test-pacquet # pacquet crates only
just test-pnpr # pnpr crates only
node pnpm/scripts/run-rust-tests.mjs -p pnpm-lockfile # one crate
When porting tests from the upstream pnpm/pnpm TypeScript repository, see
plans/TEST_PORTING.md. It tracks the tests
scheduled for porting (with upstream file paths and line numbers), the
expected layout for not-yet-implemented behavior (known_failures modules
guarded by pnpm_testing_utils::allow_known_failure!), and the
verification step of temporarily breaking the implementation to confirm a
ported test actually fails for the right reason before committing.
Benchmarking
First, start a local registry server, such as verdaccio:
verdaccio
Then use the integrated-benchmark task to run benchmarks. For example:
# Compare the branch you are working on against main
just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store pacquet@my-branch pacquet@main
# Compare the current commit against the previous commit
just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store pacquet@HEAD pacquet@HEAD~
# Compare pacquet of the current commit against pnpm
just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store --with-pnpm pacquet@HEAD
# Compare pacquet of the current commit, pacquet of main, and pnpm against each other
just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store --with-pnpm pacquet@HEAD pacquet@main
# See more options
just integrated-benchmark --help