Commit Graph

25 Commits

Author SHA1 Message Date
Zoltan Kochan
83f06a6046 refactor(pacquet): trim comments that restate code or record history (#12482)
## What

A repository-wide sweep of the Rust source under `pacquet/` to bring comments in line with the comment policy in `AGENTS.md` / `pacquet/AGENTS.md`: **comments are for the non-obvious _why_, not a translation of the _what_, and behavior already proven by a test should not be re-narrated in code.**

Two focused commits:

**1. Trim comments that restate code or record history** (`refactor(pacquet): trim comments that restate code or record history`)
- comments that merely restate the adjacent code;
- history / refactor-shape narration (`used to`, `before this PR`, `Copilot flagged`, "the old X", removed-type references);
- call-site comments duplicating a callee's own doc comment;
- test prose that just re-describes what a test's name and assertions demonstrate.

**2. Drop comments that narrate test-covered behavior** (`refactor(pacquet): drop comments that narrate test-covered behavior`) — enforcing the "tests are documentation" rule. Each implementation file was paired with its co-located test module so the duplication was visible, then comments enumerating tested scenarios, edge cases, failure modes, or worked examples were removed — **including when they carried a pnpm-parity reference/link**, since that context lives in the test names and git history.

Preserved throughout: genuine `///`/`//!` contracts (guarantees, preconditions, postconditions, panics), hidden invariants, workarounds, ordering/safety reasons, parity notes that aren't behavior-restatement, and `SAFETY:`/`TODO:` notes.

## Scope

Comments only — **no code, identifiers, or string literals were modified** in either commit. ~500 file edits in total; several thousand comment lines removed.

## How it was done

Multi-agent sweeps: one agent per batch applied the policy, and a second agent diff-checked each edited batch for over-deletion (lost contracts) or accidental code changes. Over-trimmed contracts were restored; zero code changes were introduced (independently re-verified: every added line is identical to a removed line modulo a stripped trailing comment).
2026-06-17 23:05:55 +02:00
Felipe Santos
23716ed9b0 fix: preserve user-defined npm_config_* env vars in lifecycle scripts (#12400)
* fix: preserve user-defined npm_config_* env vars in lifecycle scripts

* fix: use released `@pnpm/npm-lifecycle` and port npm_config_* filter to pacquet

Pin the catalog to the released `@pnpm/npm-lifecycle` ^1100.0.0 instead of a
mutable PR-head ref, regenerating the lockfile to the immutable registry
tarball.

Port the upstream env filter to pacquet's make_env so user-defined
npm_config_* vars (e.g. npm_config_platform_arch) survive lifecycle scripts
while (npm|pnpm)_config_* auth keys are still stripped, matching
`@pnpm/npm-lifecycle` 9e2ac78148.

Harden the new TS test to save/restore npm_config_platform_arch.

* test(executor): restore env vars to pre-test value in lifecycle EnvGuard

The guard removed the seeded var unconditionally on drop, which would
discard any value the process env already had. Capture the original via
var_os and restore it (or remove only when originally absent).

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-14 14:35:44 +02:00
Khải
4759e0559a chore(rust/clippy): enable some lints (#12363)
* chore(rust/clippy): enable undocumented_unsafe_blocks

Move the SAFETY rationale out of the EnvGuard doc comment and onto the
two env-var unsafe blocks it actually justifies, so every unsafe block
in the workspace now carries its own // SAFETY: comment.

* chore(rust/clippy): enable unnecessary_safety_comment

Reword a comment in local_tracing that used the // SAFETY: prefix to
justify an expect() on safe code; the prefix is reserved for unsafe
blocks.

* chore(rust/clippy): enable todo

No occurrences in the workspace; the lint keeps stray todo!() markers
out of committed code from now on.

* chore(rust/clippy): enable unimplemented

No occurrences in the workspace; the lint keeps stray unimplemented!()
markers out of committed code from now on.

* chore(rust/clippy): enable exit

The one deliberate process exit — dlx propagating the spawned command's
exit status, matching pnpm — now carries an #[expect] stating that;
any new std::process::exit call site has to justify itself the same
way.

* chore(rust/clippy): enable infinite_loop

No occurrences in the workspace; a loop that can never terminate must
return ! from now on.

* chore(rust/clippy): enable mem_forget

The overrides test helper leaked its TempDir via mem::forget to keep
the fixture on disk; TempDir::keep() is the purpose-built API for
that. The two registry-mock forget(mock_instance) calls stay — the
leak is how the shared mock-registry process survives the spawning
test process — but now carry #[expect] so the intent is
machine-checked.

* chore(rust/clippy): enable get_unwrap

Replace .get(...).unwrap() / .get_mut(...).unwrap() with indexing
across the test suites (machine-applicable clippy fixes): the panic is
the same, but the intent is clearer and the panic message names the
missing key instead of 'unwrap on None'.

* chore(rust/clippy): enable unused_result_ok

Two best-effort calls in tests discarded their Result with .ok();
spell the discard out with let _ = instead, so .ok() is reserved for
actually consuming the Option.

* chore(rust/clippy): enable pathbuf_init_then_push

resolve_path built its joined path with to_path_buf + push; Path::join
expresses the same thing in one allocation-aware call.

* chore(rust/clippy): enable string_add

The phase-event fixture concatenated string literals at runtime with
String + &str; concat! assembles the same fixture at compile time.

* chore(rust/clippy): enable verbose_file_reads

No occurrences in the workspace; whole-file reads go through fs::read
/ fs::read_to_string rather than manual open + read_to_end loops from
now on.

* chore(rust/clippy): enable allow_attributes

Convert every #[allow] in the workspace to #[expect]. The conversion
immediately paid for itself by surfacing four suppressions whose lint
no longer fires:

- resolve_peers: a too_many_arguments expect on a three-argument
  method (stale since a refactor) - removed.
- resolve_peers: two dead_code expects on MissingPeerInfo fields the
  compiler considers used - removed.
- tarball: fetch_and_extract_zip_once stacked two copies of the same
  too_many_arguments suppression - deduplicated.
- ensure_file: a cfg_attr(windows, allow(unused)) on a mode parameter
  that IS used on Windows (via verify_or_rewrite) - removed.

The remaining windows-side cfg_attr suppressions become expect with a
reason, and fire correctly there (the parameters are genuinely unused
on that cfg).

* chore(rust/clippy): enable allow_attributes_without_reason

Every #[expect] in the workspace already carries a reason; the lint
locks the convention in.

* chore(rust/clippy): enable negative_feature_names

No occurrences in the workspace; cargo features stay additive
(no no-std / not-x style names) from now on.

* chore(rust/clippy): enable redundant_feature_names

No occurrences in the workspace; feature names won't restate that they
are features (use-x / with-x prefixes and suffixes).

* chore(rust/clippy): enable wildcard_dependencies

No occurrences in the workspace; every dependency keeps a real version
requirement instead of "*".

* chore(rust/clippy): drop allow_attributes and allow_attributes_without_reason

perfectionist is adding better-targeted replacements for both, so leave
the clippy versions off to avoid duplicate diagnostics. The #[expect]
conversions they prompted stay — they are valid without the lints and
already removed four stale suppressions.

* chore(rust/clippy): trim redundant lint comments

The enabled lints' names already say what they enforce, so the inline
notes and per-theme section headers restated the obvious. Drop them to
match the convention of the surrounding activations, keeping only the
two category headers that carry non-obvious context.

* chore(rust/clippy): drop get_unwrap

The lint only rewrites `x.get(i).unwrap()` to `x[i]`, erasing the word
"unwrap" from panicking call sites and making them harder to grep for.
No safety benefit, so revert the enablement and all of its rewrites.

This reverts commit f274ad8.

* chore(rust/clippy): revert allow_attributes enablement and its code changes

Undo the #[allow] -> #[expect] conversions from bde9865 and restore the
suppressions that conversion dropped. perfectionist is adding a
better-targeted replacement; notably it exempts `allow` inside
`cfg_attr`, so the #[cfg_attr(windows, allow(unused))] attributes are
restored as-is. The clippy lints themselves were already removed.

This reverts the code changes of commit bde9865.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-13 00:03:08 +02:00
Khải
ac367fce91 chore(rust/clippy): pedantic, nursery, and some (#12209)
* chore: enable clippy::pedantic lint group for pacquet workspace

* style(pacquet): comply with clippy::pedantic

Apply clippy's machine-applicable pedantic fixes across the workspace
(inlined format args, removed needless borrows/closures, added
must_use, etc.), fix a few doc-comment backtick nits, and drop
pointless #[inline(always)] on trivial accessors.

Opt specific pedantic lints back out in [workspace.lints.clippy] with
documented justifications, grouped into false positives, library-API
hygiene that doesn't fit an internal CLI, suggestions that conflict
with the cardinal rule of porting pnpm 1:1, and opinionated style.

* style: taplo-format Cargo.toml lint table

* style(pnpr): comply with clippy::pedantic in merged auth backend code

Re-apply pedantic compliance to the networked-SQLite auth backend that
landed on main (#12186, #12199/#12206): doc-comment backticks, #[must_use]
on constructors and status_code, i64::from over `as`, map_or, and a
method-reference closure.

* docs(clippy): trim and inline the pedantic allow-list comments

* docs(clippy): note perfectionist supersedes many_single_char_names

* docs(clippy): note pnpm-mirroring rationale on structure/naming lints

* docs(clippy): mark unused_async as deferred pending audit

* style: enable clippy::match_wildcard_for_single_variants

* refactor: enable clippy::unused_self

Convert two self-less private methods (overrides pick_most_specific,
tarball head_only_result) to associated functions.

* refactor: enable clippy::ref_option

Widen engine_json to Option<&str>; #[expect] the two serde
serialize_with helpers, which serde must call as f(&field, ser).

* perf: enable clippy::trivially_copy_pass_by_ref

Pass the 1-byte Copy types NodeLinker and FilterWorkspaceProjectsOptions
by value; #[expect] the serde skip_serializing_if helper is_false.

* perf: enable clippy::assigning_clones

Use clone_from for seven field assignments to reuse allocations.

* style: enable clippy::manual_let_else

Convert 27 match/if-let guards to let-else; preserve the non-UTF-8
skip rationale comment in the directory walker.

* style: enable clippy::default_trait_access

Name the concrete type on Default::default() call sites; #[expect] two
struct-literal test fixtures where naming each field type would force
~20 imports.

* refactor: enable clippy::format_push_string

Replace push_str(&format!(...)) with write!/writeln! into the target
String (local 'use std::fmt::Write as _'); writeln! preserves the
exact LF/CRLF shell-shim output.

* refactor: enable clippy::needless_pass_by_value

Take by reference where the argument is only read (incl. dropping
some redundant clones in resolve_peers' recursion). Where converting
would cascade badly, #[expect] with a reason: functions that
destructure/consume the arg (build_resolve_result, PrefetchingResolver,
S3Store::new), the by-value `impl IntoIterator + Clone` in
build_direct_deps_by_importer, and the serde/test helpers whose owned
fixtures keep call sites clean.

* fix(perfectionist): satisfy dylint after format_push_string changes

Add trailing commas to the multi-line writeln! shell-shim templates
(macro_trailing_comma) and merge the new `fmt::Write as _` imports into
each file's existing `use std::{...}` block (import_granularity).

* docs(clippy): explain missing_errors_doc suppression; mark missing_panics_doc deferred

* fix(perfectionist): collapse fmt::{self, Write as _} in work_env imports

The format_push_string Write import landed as a sibling fmt:: path next
to the existing fmt import; merge them so import_granularity passes.

* style: enable clippy::return_self_not_must_use

Add #[must_use] to the WorkspaceTreeCtx builder methods, matching the
#[must_use] already on the parallel TreeCtx builders.

* perf: enable clippy::large_stack_arrays

Heap-allocate the 64 KiB read buffer in verify_file_integrity with a Vec
instead of placing it on the stack.

* chore(clippy): enable clippy::nursery group

Enable the nursery lint group on the pacquet/pnpr workspace and bring the
code into compliance.

Fixed in code:
- iter_on_single_items: [x].into_iter()/.iter() -> std::iter::once
- equatable_if_let: pattern match -> equality check (the install_accelerator
  rewrite wraps in a multi-line matches!, which gets a trailing comma for
  perfectionist::macro_trailing_comma)
- needless_pass_by_ref_mut: load_pending_row/apply_write_msg take &StoreIndex

Opted back out in Cargo.toml, each with a documented justification: use_self,
too_long_first_doc_paragraph, missing_const_for_fn, option_if_let_else,
significant_drop_tightening, redundant_pub_crate, derive_partial_eq_without_eq,
branches_sharing_code, useless_let_if_seq, single_option_map, iter_with_drain,
literal_string_with_formatting_args, collection_is_never_read.

Dropped the now-redundant individual nursery warns (needless_collect,
or_fun_call, redundant_clone) the group now covers, plus the default-on
unnecessary_lazy_evaluations. Kept clone_on_ref_ptr and if_then_some_else_none
(restriction lints not enabled by any group).

* style: bring merged main code into clippy pedantic compliance

The 17 commits merged from main predate this branch's pedantic/nursery
lint config, so their new code tripped pedantic lints. Apply the
machine-applicable fixes (uninlined_format_args, if_not_else,
elidable_lifetime_names, must_use_candidate, single_match_else,
map_unwrap_or, default_trait_access, assigning_clones, doc_markdown, ...)
and re-add the documented #[expect(needless_pass_by_value)] on
S3Store::new that this branch had carried on the now-replaced file.

* style: bring merged main code into clippy pedantic compliance

The 28 commits merged from main predate this branch's lint config, so
their new code tripped pedantic lints. Apply the machine-applicable fixes
(uninlined_format_args, manual_let_else, needless_raw_string_hashes,
redundant_closure_for_method_calls, map_unwrap_or, elidable_lifetime_names,
doc_markdown, ...) plus a few by hand:
- derive Copy on LinkSlotsParallel (all fields are Copy/refs) to clear
  needless_pass_by_value without a signature change
- deduplicate_all takes &[Vec<DepPath>] (it only borrows the duplicates)
- pick_most_specific becomes an associated fn (it never used self)
- default_trait_access -> concrete types; assigning_clones -> clone_from;
  format_push_string -> write!
- #[expect] with reasons where a fix would churn main's feature code:
  needless_pass_by_value on the recursive resolve_node and a test helper,
  and float_cmp on two deterministic-fixture assertions

* style: enable clippy::allow_attributes and allow_attributes_without_reason

Both are restriction lints (not implied by any group), enabled alongside
the existing clone_on_ref_ptr / if_then_some_else_none. Convert every
#[allow(...)] (including one nested in cfg_attr) to #[expect(...)]; all
already carried a reason, so allow_attributes_without_reason is satisfied.

Drop two now-redundant suppressions surfaced by the conversion: a
duplicated #[expect(too_many_arguments)] on fetch_and_extract_zip_once
(a prior merge left both an allow and an expect), and the
#[expect(dead_code)] on MissingPeerInfo's fields (the #[derive(Debug,
Clone)] already reads them, so dead_code never fired).

clone_on_ref_ptr was already enabled. mod_module_files is intentionally
NOT enabled: it mandates mod.rs, the opposite of the flat module.rs
pattern this project requires (CODE_STYLE_GUIDE.md, enforced by
perfectionist::flat_module_pattern).

* style: enable clippy::mod_module_files to enforce the flat module layout

mod_module_files bans mod.rs files, enforcing the flat module.rs pattern
this project already uses (0 mod.rs in the tree, so no violations). Update
CODE_STYLE_GUIDE.md to cite it as the enforcer; perfectionist's
flat_module_pattern is being retired in favor of this Clippy rule.

* fix(perfectionist): trailing comma on wrapped assert_eq! in workspace_yaml tests

The default_trait_access fix lengthened the assert_eq! so fmt wrapped it
to multi-line, which perfectionist::macro_trailing_comma requires to end
with a trailing comma.

* fix(fs): use cfg_attr expect instead of allow for Windows-unused mode args

With clippy::allow_attributes enabled, the #[cfg_attr(windows, allow(unused))]
on make_file_executable and the ensure_file/write_atomic mode params fails
Windows CI. Switch to #[cfg_attr(windows, expect(unused, reason = ...))];
on Windows the lint fires (Unix mode unused there) so the expectation is
fulfilled, and the attribute stays inert on Unix.

* fix(fs): drop the Windows unused suppression on ensure_file's mode arg

ensure_file forwards mode to verify_or_rewrite unconditionally, so it is
used on Windows too; the #[cfg_attr(windows, expect(unused))] was therefore
unfulfilled and failed Windows CI under -D warnings. write_atomic and
make_file_executable keep their expect — they use mode/file only under
#[cfg(unix)], so the lint fires (and the expectation holds) on Windows.

* chore(git): revert "fix(fs): drop the Windows unused suppression on ensure_file's mode arg"

This reverts commit 1d617c3e1f.

* chore(git): revert "fix(fs): use cfg_attr expect instead of allow for Windows-unused mode args"

This reverts commit 155e4a3dde.

* chore(git): revert "style: enable clippy::allow_attributes and allow_attributes_without_reason"

This reverts commit a47d7926f2.

* style: bring merged main code into clippy compliance + fix merge mismatch

- Add & at the two run_postinstall_hooks / run_project_lifecycle_scripts
  call sites: this branch widened lifecycle.rs to take &RunPostinstallHooks,
  but main's by-value call sites came in via the conflict resolution.
- pedantic fixes on main's new code: must_use_candidate, unnested_or_patterns,
  manual_let_else, default_trait_access, iter_on_single_items, and
  trivially_copy_pass_by_ref (map_node_linker takes NodeLinker by value).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-11 00:43:22 +02:00
Khải
622c056bbc feat(pacquet/cli): initial implementations of run, exec, dlx (#11938)
> [!WARNING]
> **Scope note.** Per [`pacquet/CONTRIBUTING.md`](https://github.com/pnpm/pnpm/blob/d4a2b0364c/pacquet/CONTRIBUTING.md), pacquet's current focus is Stage 1 (the headless installer); `exec` and `dlx` are new top-level commands, so this PR sits outside Stage 1 and is opened for review/discussion ([roadmap pnpm/pacquet#299](https://github.com/pnpm/pacquet/issues/299)).

## Summary

Ports of `run`, `exec`, and `dlx` from the TypeScript pnpm CLI.

- **`run`**: runs scripts through a new foreground `run_script` in `pacquet-executor` (sets up `node_modules/.bin` on `PATH` + the `npm_*` env). Handles `pre`/`post` scripts under `enablePrePostScripts`, arg shell-quoting (with the Windows `cmd /d /s /c` verbatim `raw_arg` path), script listing, hidden (`.`-prefixed) scripts, `--if-present`, the `start`→`server.js` fallback (with the NO_SCRIPT_OR_SERVER guard, including empty-string `start`), the `[ELIFECYCLE]` failure line (with the `test`-stage and signal-killed variants), and exit-code propagation. The recursive runner's scaffolding (`--resume-from` / `--report-summary`) landed separately on `main` via [#12093](https://github.com/pnpm/pnpm/pull/12093); this PR dispatches to it when `-r` is set and hardens it to match pnpm — per-project `pre`/`post`, the `PNPM_SCRIPT_SRC_DIR` recursion guard, pnpm's per-stage no-op guards, hidden-script handling, and `--no-bail`.
- **`exec`**: runs a command with `node_modules/.bin` + `extraBinPaths` on `PATH` (resolved via `which`), stamps `npm_config_user_agent` / `PNPM_PACKAGE_NAME` / `NODE_OPTIONS`, supports `--shell-mode` (the joined command goes through the shared `push_script_arg` helper, so the Windows `cmd /d /s /c` verbatim path uses `raw_arg` and embedded quoting survives), rejects delimiter-containing dirs (`ERR_PNPM_BAD_PATH_DIR`). The **recursive variant** (`-r`) runs the command in every workspace project, topologically sorted and sequential, with `--resume-from` / `--report-summary` / `--no-bail` and pnpm's error codes (`ERR_PNPM_RECURSIVE_EXEC_NO_PACKAGE` / `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL` / `ERR_PNPM_RECURSIVE_FAIL`). The workspace-graph / summary machinery is shared with recursive `run` through a new `cli_args::recursive` module.
- **`dlx`**: installs the package(s) into a TTL cache dir (reusing the install pipeline, anchored at the cache dir, with a *fresh* per-install build-script allow-list — caller's `allow_builds`/`dangerouslyAllowAllBuilds` don't leak in), then runs the resolved bin in the process cwd. Supports `--package`, `--allow-build`, `--shell-mode` (same `push_script_arg` verbatim path as exec), `--cpu`/`--os`/`--libc` architecture overrides (folded into the per-axis `supportedArchitectures` of the dlx install **and** into the cache key, so different overrides don't share a cached install), `dlxCacheMaxAge`; same PATH guard as exec.

New `Config` settings: `enablePrePostScripts`, `scriptShell`, `nodeOptions`, `dlxCacheMaxAge` (wired into `pnpm-workspace.yaml` + the `PNPM_CONFIG_*` overlay). Their defaults match pnpm and are asserted by the `pnpm_default_parity` contract test — this PR moves `enablePrePostScripts` (which pnpm defaults to `true`, a breaking change in [#7634](https://github.com/pnpm/pnpm/pull/7634) shipped in v9) and `dlxCacheMaxAge` into its mapped rows. `extraBinPaths` is kept as a computed field (empty until workspace support lands), matching pnpm — it is not a user-settable key.

## Deferred (documented in code)

- **`--filter` and `--workspace-concurrency`.** Recursive `run` and `exec` run every workspace project sequentially; the `--filter` package-selector subsystem and `--workspace-concurrency` parallelism are not ported yet (the global `--filter` / `--recursive` flags are accepted via clap but `--filter` is not consumed). `dlx` stays single-package by design (matches pnpm).
- `run`: the `/regexp/` script selector and the fuzzy "did you mean" hint are not ported (no regex/levenshtein dep); `scriptsPrependNodePath: always` can't prepend the node dir (pacquet resolves no node execpath anywhere yet).
- `dlx`: the cache key uses raw specs (not resolved ids); no `approve-builds` prompt.
2026-06-02 21:06:11 +02:00
Khải
577a90f819 feat(pacquet): --resume-from, --report-summary (#12093)
* feat(cli): port recursive run with --resume-from and --report-summary

Port pnpm's `pnpm run -r` (recursive run) to pacquet, including the
`--resume-from` and `--report-summary` flags, which previously existed
only in the TypeScript CLI.

- `pacquet -r run <script>` now runs the script in every workspace
  project in topological order, mirroring pnpm's runRecursive: discover
  projects, build the inter-project dependency graph, sort it into
  chunks via graph_sequencer (the port of sortProjects), and execute.
- `--resume-from <pkg>` drops every chunk before the one containing
  <pkg>, mirroring getResumedPackageChunks; an unknown package fails
  with ERR_PNPM_RESUME_FROM_NOT_FOUND.
- `--report-summary` writes pnpm-exec-summary.json with the per-package
  status (queued/running/passed/skipped/failure) and duration, nested
  under an executionStatus key, mirroring writeRecursiveSummary.
- `--no-bail` keeps running after a failure (recursive runs bail by
  default). Failures surface ERR_PNPM_RECURSIVE_FAIL, or
  ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL when bailing; a run that matches no
  script fails with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT.

A new executor helper, execute_shell_with_status, returns the child's
exit status so per-package pass/fail can be recorded; execute_shell is
unchanged.

Not yet ported (noted in the module): --no-sort, --reverse,
--workspace-concurrency parallelism, --filter narrowing of the selected
set, and the RegExp script selector. The selected set is every workspace
project, matching pacquet's currently-unfiltered install.

Integration tests port the upstream resume-from and report-summary
cases from exec/commands/test/runRecursive.ts.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ

* test(cli): gate recursive-run tests to unix and add macro trailing commas

Fix two CI failures on the recursive-run integration tests:

- Dylint (`perfectionist::macro_trailing_comma`): add the trailing comma
  to the four multi-line `assert!` invocations.
- Lint and Test (windows-latest): the shared helpers are used only by
  the Unix-gated tests, so on Windows they tripped `dead_code` under
  `-D warnings`. Gate the whole file with `#![cfg(unix)]` (the build
  scripts run through pacquet's `sh -c` executor anyway), matching the
  single-package `run` tests.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ

* test(cli): cover recursive-run bail summary and no-script branches

Fill the two coverage holes in the recursive-run handler:

- bail + report-summary: the first failing script writes the summary,
  then aborts with ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL; a package that
  sorts after the failure stays `queued`.
- no-script: a recursive run for a script no package defines fails with
  ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, and `--if-present` turns that into a
  clean no-op.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ

* test(cli): cover the bail path without --report-summary

The bail tests always passed --report-summary, leaving the
report-summary-off side of the bail block (recursive.rs:136) uncovered.
Add a test for a failing script with bail on and no --report-summary:
it still fails with ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL and writes no
summary file. Verified with cargo llvm-cov that recursive.rs now has no
missing lines.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ

* fix(cli): run each recursive-run script from its own package root

Recursive run spawned every package's script via `sh -c` without
setting the working directory, so scripts ran in pacquet's process CWD
(the workspace root) instead of their own package root. That breaks
scripts relying on relative paths and diverges from pnpm, whose
`runLifecycleHook` runs with `pkgRoot` as the working directory.

- Give `execute_shell_with_status` a `current_dir` argument (factored
  through a private `spawn_shell` helper); `execute_shell` keeps its
  inherited-CWD behavior, so its callers are unchanged.
- Pass each package's root as the script's working directory.
- Make the marker-based recursive-run tests cwd-sensitive: scripts now
  write a relative `ran.txt`, and the tests assert it lands under each
  package root (and not at the workspace root), so a wrong-CWD
  regression fails the suite.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-01 12:48:10 +02:00
Khải
13b1b9aaa2 chore(rust/dylint): upgrade perfectionist to 0.0.0-rc.17 (#12070)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 20:58:31 +00:00
Zoltan Kochan
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
2026-05-29 11:38:38 +02:00
Khải
d4136eb6f6 chore(pacquet/lint): more clippy (#11839)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-22 06:48:50 +00:00
Khải
61e724cef3 ci(pacquet): pass --workspace --all-targets to clippy and check (#11735)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 08:24:07 +02:00
Khải
e2a3c68309 chore(pacquet/dylint): upgrade perfectionist to 0.0.0-rc.15 (#11709)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 11:25:35 +00:00
Khải
e74a6b7e48 test(pacquet): coverage (#11710)
* test: fill coverage holes across lockfile, package-manifest, fs, and friends

Adds ~44 unit and integration tests to close the easier targets in the
coverage analysis at #339-style boundaries. Touches:

- `lockfile::resolved_dependency`: cover `as_alias` / `ver_peer` for
  non-matching variants, alias / parse error variants, `TryFrom<Cow>`,
  serialize-alias / serialize-link, `From<PkgVerPeer> / From<PkgNameVerPeer>`,
  and a Display vs. `String` round-trip.
- `lockfile::freshness`: cover the plural arms of `SpecDiff::Display`
  and the comma separator inside the removed/modified loops.
- `lockfile::pkg_name`: cover `TryFrom<String>` and `TryFrom<Cow>` happy
  and empty-input paths.
- `lockfile::snapshot_dep_ref`: cover `ver_peer` and `From<PkgVerPeer>`.
- `lockfile::save_lockfile`: cover the `CreateDir` / `RemoveFile` /
  `RenameFile` error classifications by planting a regular file or
  directory where the writer expects a file or a writable path.
- `registry::package`: cover `PartialEq`, `latest`, and `pinned_version`
  happy and no-match paths.
- `graph-hasher::engine_name`: cover `detect_node_version` /
  `detect_node_major` when `node` is on PATH; skip cleanly otherwise.
- `graph-hasher::object_hasher`: cover the null / bool / array arms of
  the bytestream serializer.
- `patching::apply`: cover non-NotFound read-patch error, partial-delete
  non-empty result, unsupported rename/copy operation, and `Create`
  with an unwritable parent.
- `workspace-state`: cover the `CreateDir` / `ReadFile` / `ParseJson`
  error variants.
- `package-manifest`: cover `from_path` ENOENT, `add_dependency` on a
  non-object field, `safe_read_package_json_from_dir` non-NotFound IO
  error, and `convert_engines_runtime_to_dependencies` for unsupported
  shapes.
- `fs::file_mode`: cover `EXEC_MASK` / `EXEC_MODE` constants,
  `is_executable` for all positions, and `make_file_executable` on Unix.
- `executor`: cover `execute_shell` happy and non-zero-exit paths.
- `cli/tests/run.rs`: new integration tests for `pacquet run`,
  including argument forwarding and `--if-present`.

Also bumps `hex_decode` in `graph-hasher::tests` to use
`is_multiple_of` so test-time clippy stays clean under Rust 1.95.

https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL

* style(package-manifest): drop trailing comma in single-line `assert!`

Dylint's perfectionist::macro-trailing-comma rule rejected the single-
line `assert!` formatting I'd accidentally inherited from the multi-
line shape — remove the trailing comma so the lint clears.

https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL

* test(graph-hasher): make `node` a hard prerequisite for detect tests

The two `detect_node_*` tests previously skipped silently when `node`
was missing from `PATH`. `node` is a documented prerequisite of the
test suite (see `pacquet/CONTRIBUTING.md`'s setup section), so a
missing binary is a test-env bug to surface, not a condition to paper
over. Switch the `let Some(...) = ... else { return }` skips to
`expect` so a missing `node` fails loudly with a clear message.

https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL

* test(cli,patching): switch fixtures to `json!` and `text_block_fnl!`

Addresses six review comments on #11710:

- `cli/tests/run.rs`: the three `package.json` fixtures used
  `format!("{{ ... }}", marker = …)` with literal-brace escapes,
  which is awkward and brittle when path strings contain
  JSON-special characters. Switched to
  `json!({...}).to_string()` so serde handles escaping and the
  shape reads as JSON, not as a brace-escaped template.
- `patching/src/apply/tests.rs`: the three patch fixtures I
  added used `"\<newline>...\n"` raw strings. Converted them to
  `text_block_fnl!`, matching the convention the rest of the
  workspace (lockfile, package-manager, testing-utils) uses for
  multi-line fixture text. The `_fnl` variant keeps the trailing
  newline that git-diff parsers expect.

Pre-existing fixtures in the same file (`IS_POSITIVE_PATCH`,
etc.) were not touched — keeping the scope to the lines
reviewers flagged.

https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL

* style(registry): use `assert_ne!` over `assert!(lhs != rhs)`

Idiomatic Rust and produces a better failure message when the
assertion trips. Caught during a self-review pass over #11710.

https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL

* test: address zkochan + Copilot review feedback on #11710

- Move executor + fs::file_mode unit tests from inline modules
  to the project's standard `src/<parent>/tests.rs` layout. The
  convention is documented in `CODE_STYLE_GUIDE.md` under "Unit
  test file layout"; zkochan flagged the violation in the
  executor file.

- Gate `pacquet-executor::tests` on `cfg(all(test, unix))` at
  the declaration site so the `use super::execute_shell` import
  isn't dead on Windows. Copilot flagged that
  `clippy --tests --deny warnings` would fail there.

- Drop `execute_shell_propagates_nonzero_exit_as_ok`. Copilot
  flagged that it regression-pinned a behavior that *should*
  change: pnpm/npm `run` exits with the script's status, but
  pacquet's wrapper currently returns `Ok(())` regardless. A
  fix belongs in its own PR; until then we should not add tests
  that block that fix.

- Quote the temp-path redirect targets in the `cli/tests/run.rs`
  shell fixtures so a tempdir path containing a space
  (`/var/folders/...` on macOS) doesn't split the `touch` /
  redirect argument. Copilot flagged the unquoted paths.

The `fs::file_mode::make_file_executable_sets_exec_bits` test
keeps `#[cfg(unix)]` at the test site (and moves its
`make_file_executable` import inside) rather than gating the
whole `tests` module — the other two tests
(`exec_constants_pin_pnpm_layout`,
`is_executable_matches_any_exec_bit`) are platform-neutral and
should still run on Windows.

Copilot's concern about `detect_node_version`'s `expect` (in
`engine_name.rs`) is intentionally not addressed: KSXGitHub
asked for the hard-fail in 5d0987c on grounds that `node` is a
documented prerequisite of the test suite, and the human
reviewer's call wins over the bot's.

https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 10:21:42 +02:00
Zoltan Kochan
540fa7664a feat(git-fetcher): install git-hosted packages via git CLI + preparePackage (#436 §B+D) (#446)
Builds on §A (#440). Adds a new `pacquet-git-fetcher` crate that handles `LockfileResolution::Git` snapshots during frozen-lockfile installs, plus the supporting `preparePackage` port. The install dispatcher now routes git resolutions through it instead of returning `UnsupportedResolution { resolution_kind: "git" }`.

### New crate `pacquet-git-fetcher`

- **`GitFetcher`** shells out to the system `git` (clone, or `init` + `fetch --depth 1` on hosts in `git_shallow_hosts`), checks out the pinned commit, verifies via `rev-parse HEAD`, runs `preparePackage`, removes `.git`, computes a packlist, and imports the resulting file set into the CAS. Surfaces a friendly `GitNotFound` when `git` isn't on `PATH` — pacquet does not bundle it. Mirrors [`fetching/git-fetcher/src/index.ts`](https://github.com/pnpm/pnpm/blob/94240bc046/fetching/git-fetcher/src/index.ts).
- **`prepare_package`** ports [`exec/prepare-package/src/index.ts`](https://github.com/pnpm/pnpm/blob/94240bc046/exec/prepare-package/src/index.ts). Reads the manifest, decides via `package_should_be_built`, honors an `AllowBuildFn` (the dispatcher adapts pacquet's `AllowBuildPolicy` into this shape), runs `<pm>-install` plus any `prepublish` / `prepack` / `publish` scripts via the existing `pacquet_executor::run_lifecycle_hook`, then deletes `node_modules`. Emits the upstream error codes `GIT_DEP_PREPARE_NOT_ALLOWED`, `ERR_PNPM_PREPARE_PACKAGE`, `INVALID_PATH`.
- **`detect_preferred_pm`** sniffs the cloned tree for a lockfile to pick the install pm. Workspace-root walking deferred.
- **Minimal `packlist`** honors the manifest's `files` field with a tiny `*` / `**` / `?` glob matcher (`?` and single `*` reject `/`; `**` matches across directories), always-includes the README / LICENSE / package.json set, and always-excludes `.git`, `node_modules`, lockfiles for sibling pms, and common cruft. Full `.npmignore` / `.gitignore` semantics and `bundleDependencies` walking are deferred and tracked in module docs.

### Wiring in `pacquet-package-manager`

- `InstallPackageBySnapshot` now matches `LockfileResolution::Git` and runs the fetcher in place of `DownloadTarballToStore`.
- `&AllowBuildPolicy` is computed once per install in `InstallFrozenLockfile` and threaded through `CreateVirtualStore` → `InstallPackageBySnapshot`.
- `snapshot_cache_key` returns `Ok(None)` for `Git` resolutions so they cold-batch. Warm prefetch for git-hosted slots is a follow-up alongside §C.

### Supporting changes

- `GitResolution.path: Option<String>` so a lockfile pinning a sub-directory of a git-hosted monorepo deserializes without tripping `deny_unknown_fields`.
- `Config::git_shallow_hosts` with pnpm's default list + `pnpm-workspace.yaml` override.
- `pacquet_executor::run_lifecycle_hook` is now `pub` so the preparePackage port can dispatch by arbitrary stage name.

## Out of scope (follow-ups for #436)

- §C — git-hosted *tarball* fetcher (the `gitHosted: true` shape).
- §E — full integration-test matrix (`plans/TEST_PORTING.md` 563-572). This PR ships crate-level tests against a local bare repo that prove the path works end-to-end.
- Warm prefetch for git resolutions (re-clones on every install today — slow but correct).
- Full `npm-packlist` semantics (`.npmignore`, gitignore layering, `bundleDependencies` walking).
2026-05-13 14:16:28 +02:00
Zoltan Kochan
2291bc6c2e feat: build concurrency, unsafe-perm, scriptsPrependNodePath (#397 items 12, 14, 15) (#429)
Bundles the three remaining moderate/minor parity items from #397 into one PR. Each adds a `pnpm-workspace.yaml` setting, threads it through `Config`, and applies it to `BuildModules` so a pnpm yaml that exercises these knobs produces the same install behavior in pacquet.

- **Item 12 — `childConcurrency`** ([upstream `getWorkspaceConcurrency`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/concurrency.ts#L25-L34); [`runGroups(...)`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L124)). Adds `Config.child_concurrency: u32` with the upstream-matching default `min(4, availableParallelism())` and the negative-offset semantics (`n < 0` → `max(1, parallelism - |n|)`). `BuildModules::run` now dispatches each chunk's members across a bounded rayon thread pool via `par_iter().try_for_each` — chunks themselves remain sequential to preserve topological order. `ignored_builds` and `deps_state_cache` are wrapped in `Mutex` so the recursive memo and the dedup set survive concurrent chunk-member access.
- **Item 14 — `unsafePerm`**. Adds `Config.unsafe_perm: bool` (default `true`) and threads it through to `RunPostinstallHooks`. When `false`, the executor sets `TMPDIR=node_modules/.tmp` for the spawn. The uid/gid drop side is a no-op upstream too because pnpm's [`@pnpm/npm-lifecycle`](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/index.js#L204-L220) never populates `opts.user` / `opts.group`. The POSIX auto-detect (`getuid() === 0 && setgid → unsafePerm = false`) needs `libc`, which isn't in `[workspace.dependencies]` yet — for now, root-run CI must set `unsafePerm: false` in yaml explicitly. Windows is force-overridden to `true` in `WorkspaceSettings::apply_to`, matching upstream's `process.platform === 'win32'` gate.
- **Item 15 — `scriptsPrependNodePath`** ([`Config.scriptsPrependNodePath`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/Config.ts#L108)). Adds a tri-state `ScriptsPrependNodePath` enum in `pacquet-config` (custom serde `Deserialize` for the `boolean | "warn-only"` yaml shape) and converts to `pacquet_executor::ScriptsPrependNodePath` at the `BuildModules` call site so the executor crate stays free of serde wiring. Default is `Never` to match upstream's [`StrictBuildOptions.scriptsPrependNodePath: false`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/after-install/src/extendBuildOptions.ts#L78).
- **Item 16 — `getSubgraphToBuild` filter trimming**. Already implemented in pacquet's `get_subgraph_to_build` via the `child_should_be_built || needs_build || has_patch` gate. Confirmed during this slice; no code change needed.

Why one PR: items 12 / 14 / 15 are three independent yaml-driven config knobs, all small. Bundling keeps the cross-crate signature churn from happening three times (`BuildModules`'s field list, `install_frozen_lockfile`'s call site, and the test fixtures all touch each one), and the upstream sources for the three settings live next to each other so the porting context is shared.
2026-05-13 01:07:15 +02:00
Zoltan Kochan
2f64c727ea feat(executor): swallow optional-dep build failures and report via pnpm:skipped-optional-dependency (#397) (#419)
## Summary

Closes item #6 of #397. Optional dependencies whose `postinstall`
hooks fail no longer abort the install — pacquet now reports the
failure through the `pnpm:skipped-optional-dependency` channel
(reason `build_failure`) and continues, matching
[pnpm v11's behavior at `building/during-install/src/index.ts:218-240`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L218-L240).

Three commits:

- **`feat(lockfile): surface snapshot-level optional flag`** —
  pacquet's lockfile reader previously dropped the
  `snapshots[<key>].optional` field. Adding it as `pub optional: bool`
  on `SnapshotEntry` with `#[serde(default, skip_serializing_if = "is_false")]`
  so absent ↔ false and `false` never serializes. The flag is
  pre-computed by pnpm's resolver at install time (ALL-paths-
  optional fold), so pacquet trusts the precomputed value rather
  than re-deriving it — matching [`lockfileToDepGraph` at deps/graph-builder/src/lockfileToDepGraph.ts:315](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/deps/graph-builder/src/lockfileToDepGraph.ts#L315).
- **`feat(reporter): add pnpm:skipped-optional-dependency event`** —
  new `LogEvent::SkippedOptionalDependency(SkippedOptionalDependencyLog)`
  variant + `SkippedOptionalPackage` + `SkippedOptionalReason` enum.
  Wire shape mirrors [pnpm's `SkippedOptionalDependencyMessage`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/core/core-loggers/src/skippedOptionalDependencyLogger.ts).
  All four upstream reasons (`BuildFailure`, `UnsupportedEngine`,
  `UnsupportedPlatform`, `ResolutionFailure`) are declared even
  though only `BuildFailure` is emitted today — keeps the enum
  closed so the other emit sites can land without widening it.
- **`feat(package-manager): swallow optional-dep build failures`** —
  `BuildModules.run` reads `snapshot.optional`. When
  `run_postinstall_hooks` returns an `Err`, the dep's optional
  flag decides: optional ⇒ emit a `pnpm:skipped-optional-dependency`
  event (`reason: build_failure`, `package.id` = the dep's pkg dir
  to match upstream's `depNode.dir`) and continue; non-optional ⇒
  propagate as `BuildModulesError::LifecycleScript` so the install
  aborts. Two `cfg(unix)`-gated tests cover both branches.
2026-05-12 12:59:04 +02:00
Zoltan Kochan
1452682669 feat(executor): port makeEnv, extendPath, and shell selection for lifecycle scripts (#397) (#418)
## Summary

Closes three of the critical items in #397 by porting the corresponding
behaviors from `@pnpm/npm-lifecycle@d2d8e790` and `pnpm/pnpm@b4f8f47ac2`:

- **#1 — Lifecycle env vars.** New `make_env` module ports `makeEnv` and
  the surrounding env block in `lifecycle()` ([npm-lifecycle/index.js:73-104](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/index.js#L73-L104), [:354-414](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/index.js#L354-L414)) plus the pnpm wrapper's `extraEnv` additions ([runLifecycleHook.ts:119-124](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/src/runLifecycleHook.ts#L119-L124)). Lifecycle scripts now see `npm_lifecycle_event`, `npm_lifecycle_script`, `npm_node_execpath`/`NODE`, `npm_package_json`, `npm_execpath`, `npm_package_*` (`name`, `version`, and recursive `config`/`engines`/`bin`), `npm_config_node_gyp`, `npm_config_user_agent`, `INIT_CWD`, `PNPM_SCRIPT_SRC_DIR`, and `TMPDIR` (when `unsafe_perm` is false). The spawn now strips inherited env (`env_clear()`) so leftover `npm_*` keys from a wrapping invocation cannot leak through.
- **#2 — PATH ancestor walk.** New `extend_path` module ports `extendPath` ([npm-lifecycle/lib/extendPath.js:5-27](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/lib/extendPath.js#L5-L27)) plus the tri-state `scriptsPrependNodePath` gating ([:29-61](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/lib/extendPath.js#L29-L61)). For a dep at `<root>/node_modules/.pnpm/<slot>/node_modules/<pkg>`, `PATH` now contains the dep's own `.bin`, the `.pnpm` slot's `.bin`, the project root's `.bin`, the bundled `node-gyp-bin` (when supplied), `extra_bin_paths`, and finally the inherited PATH.
- **#4 — Shell selection.** New `shell` module ports the [shell-selection block](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/index.js#L241-L252) and the [pnpm-side `.bat`/`.cmd` guard](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/src/runLifecycleHook.ts#L63-L71). `cmd /d /s /c` on Windows, custom `scriptShell` on either platform, otherwise `sh -c`. The Windows-batch-file `scriptShell` case surfaces as `ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS` (matching upstream's error code).

`RunPostinstallHooks` grows seven new fields to surface these knobs; `BuildModules` passes safe defaults (`None` / `true` / `Never`) for all of them — full config plumbing for `user-agent`, `unsafe-perm`, `scripts-prepend-node-path`, `node-gyp` bundling, and `script-shell` are tracked as separate items in #397 (#14, #15) or follow-ups.

### Explicit non-goals in this PR

Three caveats called out in #397 that are deliberately deferred:

- `windowsVerbatimArguments` (Rust equivalent: `CommandExt::raw_arg`) is signalled by `SelectedShell.windows_verbatim_args` but not yet applied to the spawned `Command`.
- `@yarnpkg/shell` / `shellEmulator: true` has no clean Rust port; pacquet ignores the flag for now.
- `unsafe_perm` uid/gid drop (#14) — `BuildModules` passes `true`, which keeps current behavior (no TMPDIR creation, no privilege drop).

### Test parity

Per the project guide ("port the relevant pnpm tests too whenever they translate"), this branch ports:

- `test('makeEnv')` from [npm-lifecycle/test/index.js:97-124](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/test/index.js#L97-L124).
- The `extendPath` ordering test from [npm-lifecycle/test/extendPath.test.js:5-8](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/test/extendPath.test.js#L5-L8).
- `runLifecycleHook() does not set npm_config env vars` from [pnpm/exec/lifecycle/test/index.ts:65-77](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/test/index.ts#L65-L77), adapted to a file-dump model so we don't need the IPC fixture.
- `onlyOnWindows('pnpm shows error if script-shell is .cmd')` from [pnpm/exec/commands/test/index.ts:509-542](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/commands/test/index.ts#L509-L542) and the custom-shell case from [:478-508](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/commands/test/index.ts#L478-L508).

Plus fills the gaps where upstream coverage is thin: tri-state `scriptsPrependNodePath`, two-level pnpm virtual-store PATH walks, `extra_bin_paths` ordering, TMPDIR gating on `unsafe_perm`, and `extra_env` precedence vs `npm_lifecycle_script`.
2026-05-12 11:42:51 +02:00
Khải
a8e8dd3091 ci: setup dylint with perfectionist (#416)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-12 02:11:19 +07:00
Allan Kimmer Jensen
d685005de9 feat: lifecycle script execution and allowBuilds policy (#391)
## Summary

- Add lifecycle script runner to `pacquet-executor`: preinstall, install, postinstall in correct order, `binding.gyp` auto-detection, `INIT_CWD`/`PNPM_SCRIPT_SRC_DIR`/PATH environment setup
- Add `allowBuilds` build policy matching pnpm 11's default-deny behavior, with `dangerouslyAllowAllBuilds` escape hatch
- Wire build step into the frozen-lockfile install path
- Port `buildSequence` + `graphSequencer` so packages with `requires_build` run children-first
- Add `pnpm:lifecycle` and `pnpm:ignored-scripts` reporter channels — script start/stdio/exit events flow through the same NDJSON pipeline `@pnpm/cli.default-reporter` consumes from upstream pnpm
- Port 11 upstream lifecycle script tests as `known_failures`

Upstream refs:
- [`runPostinstallHooks`](https://github.com/pnpm/pnpm/blob/7e91e4b35f/exec/lifecycle/src/index.ts)
- [`runLifecycleHook`](https://github.com/pnpm/pnpm/blob/80037699fb/exec/lifecycle/src/runLifecycleHook.ts)
- [`createAllowBuildFunction`](https://github.com/pnpm/pnpm/blob/7e91e4b35f/building/policy/src/index.ts)
- [`buildSequence`](https://github.com/pnpm/pnpm/blob/80037699fb/building/during-install/src/buildSequence.ts)
- [`graphSequencer`](https://github.com/pnpm/pnpm/blob/80037699fb/deps/graph-sequencer/src/index.ts)
- [`lifecycleLogger`](https://github.com/pnpm/pnpm/blob/80037699fb/core/core-loggers/src/lifecycleLogger.ts)
- [`ignoredScriptsLogger`](https://github.com/pnpm/pnpm/blob/80037699fb/core/core-loggers/src/ignoredScriptsLogger.ts)

## What's implemented

- `run_postinstall_hooks` in `pacquet-executor` faithfully ports pnpm's hook runner. The hook is now generic over `R: Reporter` and emits `pnpm:lifecycle` events through the standard pacquet reporter pipeline: `Script` before each spawn, one `Stdio` event per stdout/stderr line read through `Stdio::piped()` byline buffering, and `Exit` after the script returns
- `AllowBuildPolicy` reads `pnpm.allowBuilds` and `pnpm.dangerouslyAllowAllBuilds` from `package.json` with correct tri-state semantics (allow/deny/ignored)
- Default-deny matches pnpm 11: packages must be explicitly listed in `allowBuilds` to run their scripts
- `BuildModules::run::<R>` runs lifecycle scripts for `requires_build: true` packages in the frozen-lockfile path and returns the sorted (peer-stripped) list of packages whose scripts were skipped because they weren't in `allowBuilds`. `InstallFrozenLockfile::run` emits one `pnpm:ignored-scripts` event with that list (always, even when empty), mirroring upstream
- `build_sequence` walks the dep graph from each importer, keeps only nodes whose subtree contains a build node, and feeds that subgraph into `graph_sequencer` to produce topologically ordered chunks. `BuildModules` iterates the chunks in order so children build before parents
- `AllowBuildPolicy` and `BuildModules.lockfile_dir` use the existing `requester` field (derived from `manifest.path().parent()`) instead of `std::env::current_dir()`
- New `LogEvent::Lifecycle` and `LogEvent::IgnoredScripts` variants with wire-shape tests
- Recording-fake reporter tests for `run_postinstall_hooks` (Script→Stdio→Exit ordering, non-zero exit propagation) and for `BuildModules::run` (sorted ignored-builds, explicit-deny exclusion). Existing `install_emits_pnpm_event_sequence` updated to expect the new `IgnoredScripts` event between `Stats` and `ImportingDone`
- Unit tests for the build policy, key parsing, `graph_sequencer`, and `build_sequence`

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-11 12:20:33 +02:00
Khải
0db25f85a2 refactor: re-organize code (#143)
## Summary

* Move code that handles package managing to its own crate named `pacquet_package_manager`.
  * Convert functions with multiple arguments into structs.
* Make `pacquet add` reuses the algorithm from `pacquet install`.
  * Achieved by having `pacquet add` creates an in-memory `package.json` and pass it to the `Install` subroutine.
* Convert the error-prone tests in `crates/cli` into proper integration tests that invoke the `pacquet` binary.
  * Tests become shorter and more readable.
  * Solve race condition issues with `cargo test`.
* All code with `set_current_dir` have been replaced by either integration tests or dependency injection.
  * Solve race condition issues with `cargo test`.
* `thiserror` has been completely removed. `derive_more` is used in its stead.
  * `thiserror` cannot handle generic trait bound.
  * `derive_more` is already used to generate custom string types in the lockfile.
  * one less macro crate leads to slightly faster compile time.
* `pacquet_package_json::PackageJson` has been renamed to `pacquet_package_manifest::PackageManifest`.
* Other minor changes.
2023-10-26 22:34:09 +03:00
Yagiz Nizipli
cdb53246a1 feat: add pacquet_diagnostics module 2023-08-05 17:56:40 -04:00
Yagiz Nizipli
71a705aee6 feat: add criterion benchmarks 2023-07-31 16:54:31 -04:00
Yakiyo
888a613210 fix: use sh to run commands instead of directly passing script 2023-07-31 08:16:44 -04:00
Yagiz Nizipli
9086e21ad0 feat: use miette for diagnostics 2023-07-30 17:20:25 -04:00
Yagiz Nizipli
e49712744e refactor: simplify error messages 2023-07-27 17:49:52 -04:00
Yagiz Nizipli
c76079d138 feat: add pacquet start command 2023-07-27 17:49:52 -04:00