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>
This commit is contained in:
Khải
2026-06-11 05:43:22 +07:00
committed by GitHub
parent 9cd8070722
commit ac367fce91
313 changed files with 1709 additions and 1371 deletions

View File

@@ -184,12 +184,53 @@ allow_branch = "main"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(dylint_lib, values("perfectionist"))'] }
[workspace.lints.clippy]
clone_on_ref_ptr = "warn"
if_then_some_else_none = "warn"
needless_collect = "warn"
or_fun_call = "warn"
redundant_clone = "warn"
unnecessary_lazy_evaluations = "warn"
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
doc_link_with_quotes = "allow" # false positive: matches markdown link titles
unreadable_literal = "allow" # false positive: octal file modes
case_sensitive_file_extension_comparisons = "allow" # extension checks are intentionally case-sensitive
implicit_hasher = "allow" # only matters across a public API
unnecessary_debug_formatting = "allow" # keeps the quoted `Path` output
option_option = "allow" # distinguishes missing from null
cast_possible_truncation = "allow" # deliberate numeric narrowing
cast_sign_loss = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
missing_errors_doc = "allow" # error enums are self-documenting (#[display] + #[diagnostic(code)]); a # Errors section would just restate them
missing_panics_doc = "allow" # deferred: refactor away / document / #[expect] each panic before enabling
too_many_lines = "allow" # pacquet mirrors pnpm's function decomposition
struct_excessive_bools = "allow" # option structs mirror pnpm's
fn_params_excessive_bools = "allow" # mirrors pnpm's option parameters
match_same_arms = "allow" # arms kept explicit to mirror pnpm's switch cases
unnecessary_wraps = "allow"
unused_async = "allow" # deferred: audit each async signature (trait/seam-required?) before enabling
similar_names = "allow" # names ported verbatim from pnpm
items_after_statements = "allow"
needless_continue = "allow"
many_single_char_names = "allow" # perfectionist's single_letter_* lints already cover this with finer per-position control
# nursery-group opt-outs (the group is experimental; these lints are either
# false positives on this code or fight conventions the project keeps on purpose)
use_self = "allow" # spelling out the concrete type name over Self is left to author discretion
too_long_first_doc_paragraph = "allow" # doc comments deliberately open with a detailed pnpm-parity summary, not a one-line lede
missing_const_for_fn = "allow" # const-ness is a forward semver commitment and this nursery lint is noisy / churns as const-eval grows
option_if_let_else = "allow" # the suggested map_or_else closures read worse than the if-let they replace
significant_drop_tightening = "allow" # the flagged guards are held deliberately (atomic check-then-act); early-dropping is FP-prone and behavioral
redundant_pub_crate = "allow" # pub(crate) is kept to document intended visibility even where technically redundant
derive_partial_eq_without_eq = "allow" # deriving Eq is a forward semver commitment that all fields stay Eq
branches_sharing_code = "allow" # false-positive-prone: clippy itself flags its own suggestion as needing adjustment
useless_let_if_seq = "allow" # rewrites large init blocks into an awkward if-as-expression
single_option_map = "allow" # flags test helpers that exist precisely to dedupe a `.map` over an Option
iter_with_drain = "allow" # false positive: drain(..) reuses the batch Vec's allocation across loop iterations; into_iter() would consume it
literal_string_with_formatting_args = "allow" # false positive: `${VAR:-default}` strings are .npmrc / env-replace fixtures, not Rust format args
collection_is_never_read = "allow" # false positive: the flagged Vec owns sockets to keep them alive, it is never meant to be read
# restriction lints opted into individually — not part of any default group or
# the nursery group, so these lines are what enable them (kept on purpose).
clone_on_ref_ptr = "warn"
if_then_some_else_none = "warn"
mod_module_files = "warn" # forbids mod.rs, enforcing the flat module.rs layout (see CODE_STYLE_GUIDE.md)
[profile.release]
opt-level = 3

View File

@@ -46,7 +46,7 @@ Follow [the Rust API guidelines](https://rust-lang.github.io/api-guidelines/nami
### Module Organization
- Use the flat file pattern (`module.rs`) rather than `module/mod.rs` for submodules. Enforced by [`perfectionist::flat_module_pattern`](https://github.com/KSXGitHub/perfectionist/blob/0.0.0-rc.17/rules/flat_module_pattern.md).
- Use the flat file pattern (`module.rs`) rather than `module/mod.rs` for submodules. Enforced by [`clippy::mod_module_files`](https://rust-lang.github.io/rust-clippy/master/index.html#mod_module_files), which bans `mod.rs` files. (`perfectionist::flat_module_pattern` covered this previously and is being retired in favor of the Clippy rule.)
- List `pub mod` declarations first, then `pub use` re-exports, then private imports and items.
- Use `pub use` to re-export key types at the module level for convenience.

View File

@@ -18,6 +18,7 @@ const CATALOG_PROTOCOL: &str = "catalog:";
///
/// Mirrors upstream's `parseCatalogProtocol`
/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/protocol-parser/src/parseCatalogProtocol.ts#L3-L16)).
#[must_use]
pub fn parse_catalog_protocol(bare_specifier: &str) -> Option<&str> {
let raw = bare_specifier.strip_prefix(CATALOG_PROTOCOL)?.trim();
Some(if raw.is_empty() { DEFAULT_CATALOG_NAME } else { raw })

View File

@@ -114,6 +114,7 @@ pub enum CatalogResolutionError {
///
/// Mirrors upstream's `resolveFromCatalog`
/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L60-L130)).
#[must_use]
pub fn resolve_from_catalog(
catalogs: &Catalogs,
wanted_dependency: &WantedDependency,

View File

@@ -313,8 +313,7 @@ impl CliArgs {
let manifest = PackageManifest::from_path(manifest_path())
.wrap_err("getting the package.json in current directory")?;
if let Some(script) = manifest.script("test", false)? {
execute_shell(script)
.wrap_err(format!("executing command: \"{0}\"", script))?;
execute_shell(script).wrap_err(format!("executing command: \"{script}\""))?;
}
}
CliCommand::Run(args) => {
@@ -343,7 +342,7 @@ impl CliArgs {
let manifest = PackageManifest::from_path(manifest_path())
.wrap_err("getting the package.json in current directory")?;
let command = manifest.script("start", true)?.unwrap_or("node server.js");
execute_shell(command).wrap_err(format!("executing command: \"{0}\"", command))?;
execute_shell(command).wrap_err(format!("executing command: \"{command}\""))?;
}
CliCommand::Store(command) => command.run(|| config().map(|m| &*m))?,
}

View File

@@ -27,7 +27,6 @@ impl AddDependencyOptions {
/// Whether to add entry to `"dependencies"`.
///
/// **NOTE:** no `--save-*` flags implies save as prod.
#[inline(always)]
fn save_prod(&self) -> bool {
let &AddDependencyOptions { save_prod, save_dev, save_optional, save_peer } = self;
save_prod || (!save_dev && !save_optional && !save_peer)
@@ -36,20 +35,17 @@ impl AddDependencyOptions {
/// Whether to add entry to `"devDependencies"`.
///
/// **NOTE:** `--save-peer` without any other `--save-*` flags implies save as dev.
#[inline(always)]
fn save_dev(&self) -> bool {
let &AddDependencyOptions { save_prod, save_dev, save_optional, save_peer } = self;
save_dev || (!save_prod && !save_optional && save_peer)
}
/// Whether to add entry to `"optionalDependencies"`.
#[inline(always)]
fn save_optional(&self) -> bool {
self.save_optional
}
/// Whether to add entry to `"peerDependencies"`.
#[inline(always)]
fn save_peer(&self) -> bool {
self.save_peer
}
@@ -104,7 +100,7 @@ pub struct AddArgs {
/// is created. Mirrors pnpm's `--lockfile-only`.
#[clap(long = "lockfile-only")]
pub lockfile_only: bool,
/// The directory with links to the store (default is node_modules/.pacquet).
/// The directory with links to the store (default is `node_modules/.pacquet`).
/// All direct and indirect dependencies of the project are linked into this directory
#[clap(long = "virtual-store-dir", default_value = "node_modules/.pacquet")]
pub virtual_store_dir: Option<PathBuf>, // TODO: make use of this

View File

@@ -195,9 +195,10 @@ impl DlxArgs {
.wrap_err("canonicalizing the dlx cache directory")?;
let cache_link = dlx_command_cache_dir.join("pkg");
let cached_dir = match get_valid_cache_dir(&cache_link, max_age, SystemTime::now()) {
Some(dir) => dir,
None => {
let cached_dir =
if let Some(dir) = get_valid_cache_dir(&cache_link, max_age, SystemTime::now()) {
dir
} else {
let prepare_dir =
get_prepare_dir(&dlx_command_cache_dir, SystemTime::now(), std::process::id());
if let Err(error) = install_into_cache::<Reporter>(
@@ -221,8 +222,7 @@ impl DlxArgs {
// ignore the failure and run from our own prepare dir.
let _ = force_symlink_dir(&prepare_dir, &cache_link);
prepare_dir
}
};
};
let bins_dir = cached_dir.join("node_modules").join(".bin");
let bin_name =
@@ -445,7 +445,7 @@ fn get_valid_cache_dir(
/// prepared in. Ports `getPrepareDir`
/// (<https://github.com/pnpm/pnpm/blob/d4a2b0364c/exec/commands/src/dlx.ts#L433-L436>).
fn get_prepare_dir(cache_path: &Path, now: SystemTime, pid: u32) -> PathBuf {
let millis = now.duration_since(UNIX_EPOCH).map(|elapsed| elapsed.as_millis()).unwrap_or(0);
let millis = now.duration_since(UNIX_EPOCH).map_or(0, |elapsed| elapsed.as_millis());
cache_path.join(format!("{millis:x}-{pid:x}"))
}
@@ -503,7 +503,7 @@ fn read_json(path: &Path) -> Result<Value, DlxError> {
/// (dlx.ts:297-302).
fn scopeless(pkg_name: &str) -> &str {
if let Some(rest) = pkg_name.strip_prefix('@') {
rest.split_once('/').map(|(_, name)| name).unwrap_or(pkg_name)
rest.split_once('/').map_or(pkg_name, |(_, name)| name)
} else {
pkg_name
}

View File

@@ -177,10 +177,14 @@ fn get_valid_cache_dir_honors_max_age() {
);
// Past the window: expired.
let past = mtime + Duration::from_secs(1440 * 60 + 60);
let past = mtime + Duration::from_mins(1441);
assert!(get_valid_cache_dir(&link, 1440, past).is_none(), "an expired link must be rejected");
}
#[expect(
clippy::needless_pass_by_value,
reason = "test helper called from multiple sites with owned literals; by-value keeps the call sites clean"
)]
fn write_pkg(dir: &std::path::Path, name: &str, manifest: serde_json::Value) {
let pkg_dir = dir.join("node_modules").join(name);
fs::create_dir_all(&pkg_dir).expect("create pkg dir");

View File

@@ -37,13 +37,13 @@ impl NodeLinkerArg {
#[derive(Debug, Args)]
pub struct InstallDependencyOptions {
/// pacquet will not install any package listed in devDependencies and will remove those insofar
/// they were already installed, if the NODE_ENV environment variable is set to production.
/// Use this flag to instruct pacquet to ignore NODE_ENV and take its production status from this
/// they were already installed, if the `NODE_ENV` environment variable is set to production.
/// Use this flag to instruct pacquet to ignore `NODE_ENV` and take its production status from this
/// flag instead.
#[arg(short = 'P', long)]
prod: bool,
/// Only devDependencies are installed and dependencies are removed insofar they were
/// already installed, regardless of the NODE_ENV.
/// already installed, regardless of the `NODE_ENV`.
#[arg(short = 'D', long)]
dev: bool,
/// optionalDependencies are not installed.
@@ -302,7 +302,7 @@ impl InstallArgs {
// `--node-linker` flag (if passed) overrides the
// yaml/npmrc value for this invocation. Mirrors pnpm's
// override-on-explicit-flag semantics.
let node_linker = node_linker.map(NodeLinkerArg::into_config).unwrap_or(config.node_linker);
let node_linker = node_linker.map_or(config.node_linker, NodeLinkerArg::into_config);
// The lockfile-verification gate keys its on-disk cache off
// `<manifest_dir>/pnpm-lock.yaml`. Once workspace support
// lands (pacquet#431), this becomes `workspace_root` to
@@ -466,9 +466,10 @@ async fn install_via_pnpr<Reporter: self::Reporter + 'static>(
.map_err(|err| miette::miette!("failed to serialize overrides: {err}"))?;
let benchmark_registry_override =
PnprBenchmarkRegistryOverride::from_env(&state.config.registry);
let resolve_registry = benchmark_registry_override
.as_ref()
.map_or_else(|| state.config.registry.clone(), |registry| registry.resolve_registry());
let resolve_registry = benchmark_registry_override.as_ref().map_or_else(
|| state.config.registry.clone(),
PnprBenchmarkRegistryOverride::resolve_registry,
);
// Send the on-disk lockfile + the full client policy so the server
// verifies the input lockfile under *our* policy before resolving;

View File

@@ -438,7 +438,7 @@ fn change_priority(change: Change) -> u8 {
fn sort_outdated(outdated: &mut [OutdatedPackage], sort_by: Option<SortBy>) {
match sort_by {
Some(SortBy::Name) => {
outdated.sort_by(|left, right| left.package_name.cmp(&right.package_name))
outdated.sort_by(|left, right| left.package_name.cmp(&right.package_name));
}
None => outdated.sort_by(|left, right| {
let by_change = change_priority(classify(&left.current, &left.target))

View File

@@ -8,6 +8,7 @@ use serde_json::Value;
use std::{
collections::HashMap,
env,
fmt::Write as _,
path::{Path, PathBuf},
};
@@ -295,7 +296,7 @@ pub(super) fn run_stages(
/// Run one lifecycle stage. Returns `Ok(None)` when pnpm's per-stage
/// no-op guards apply (empty body, or `npx only-allow pnpm` with no
/// args), so the caller can record "didn't actually run" without
/// inventing a synthetic ExitStatus. A non-success ExitStatus is
/// inventing a synthetic `ExitStatus`. A non-success `ExitStatus` is
/// returned to the caller — single-project `RunArgs::run` exits with
/// the code; recursive `run_recursive` records `Failure` and decides
/// whether to bail.
@@ -322,7 +323,7 @@ pub(super) fn run_stage(
return Ok(None);
}
let status = run_script(RunScript {
let status = run_script(&RunScript {
manifest: ctx.manifest.value(),
stage,
script,
@@ -443,16 +444,14 @@ fn render_project_commands(manifest: &Value) -> String {
let mut output = String::new();
if !lifecycle.is_empty() {
output.push_str(&format!("Lifecycle scripts:\n{}", render_commands(&lifecycle)));
write!(output, "Lifecycle scripts:\n{}", render_commands(&lifecycle)).unwrap();
}
if !other.is_empty() {
if !output.is_empty() {
output.push_str("\n\n");
}
output.push_str(&format!(
"Commands available via \"pnpm run\":\n{}",
render_commands(&other),
));
write!(output, "Commands available via \"pnpm run\":\n{}", render_commands(&other))
.unwrap();
}
output
}

View File

@@ -12,7 +12,7 @@
//! Scope versus upstream: projects are sorted topologically (upstream's
//! default) and run sequentially. `--no-sort`, `--reverse`,
//! `--workspace-concurrency` parallelism, `--filter` narrowing of the
//! selected set, and the RegExp script selector are not ported yet — the
//! selected set, and the `RegExp` script selector are not ported yet — the
//! selected set is every workspace project, matching pacquet's
//! currently-unfiltered `install`.

View File

@@ -55,7 +55,7 @@ impl ConfigOverrides {
/// Mirrors pnpm 11's "CLI > yaml > .npmrc > defaults" precedence.
pub fn apply(&self, config: &mut Config) {
if let Some(registry) = &self.registry {
config.registry = registry.clone();
config.registry.clone_from(registry);
}
}
}

View File

@@ -70,8 +70,7 @@ fn configure_rayon_pool() {
return;
}
let n = std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1)
.map_or(1, std::num::NonZeroUsize::get)
.saturating_mul(2)
// `.max(4)` is an intentional minimum: even on quota-limited
// 1-2-CPU runners, dropping below 4 puts us back into the

View File

@@ -25,9 +25,10 @@ pub fn enable_gvs_in_workspace_yaml(workspace: &Path, extra_yaml: &str) {
/// Snapshot-friendly view of every row in `<store>/v11/index.db`.
///
/// The outer key is the SQLite key (`"{integrity}\t{pkgId}"`). The inner
/// The outer key is the `SQLite` key (`"{integrity}\t{pkgId}"`). The inner
/// map is the package's files — one entry per path inside the tarball.
/// `checked_at` is scrubbed because its value depends on install time.
#[must_use]
pub fn index_file_contents(store_dir: &Path) -> BTreeMap<String, BTreeMap<String, CafsFileInfo>> {
let store = StoreDir::new(store_dir);
// open_readonly: we're just reading for snapshot assertions, so don't

View File

@@ -61,6 +61,10 @@ fn write_workspace_yaml(workspace: &Path, extra: &str) {
}
/// Write a one-dependency `package.json` and return the manifest path.
#[expect(
clippy::needless_pass_by_value,
reason = "test helper called many times with json!(...) literals; owned arg keeps call sites clean"
)]
fn write_manifest(workspace: &Path, deps: serde_json::Value) {
let manifest = serde_json::json!({ "dependencies": deps });
fs::write(workspace.join("package.json"), manifest.to_string()).expect("write package.json");
@@ -409,7 +413,7 @@ fn public_hoist_bin_is_linked_via_root_bin_dir() {
}
/// Workspace install (pnpm/pacquet#431) lands per-importer
/// node_modules layouts; hoist must walk every importer's direct
/// `node_modules` layouts; hoist must walk every importer's direct
/// deps, not just the root, so transitives unique to a workspace
/// project still reach the shared `<vs>/node_modules` private
/// hoist. Sets up a two-importer workspace where the workspace
@@ -812,7 +816,7 @@ mod known_failures {
}
/// Upstream: [`hoist.ts:514` "should recreate node_modules with hoisting"](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/hoist.ts#L514).
/// Removes node_modules and re-installs from the lockfile —
/// Removes `node_modules` and re-installs from the lockfile —
/// needs partial-install state for the rehoist comparison.
#[test]
fn should_recreate_node_modules_with_hoisting() {

View File

@@ -41,6 +41,10 @@ fn write_workspace_yaml(workspace: &Path, extra: &str) {
}
/// Write a `package.json` with the given `dependencies` object.
#[expect(
clippy::needless_pass_by_value,
reason = "test helper called many times with json!(...) literals; owned arg keeps call sites clean"
)]
fn write_manifest(workspace: &Path, deps: serde_json::Value) {
let manifest = serde_json::json!({ "dependencies": deps });
fs::write(workspace.join("package.json"), manifest.to_string()).expect("write package.json");

View File

@@ -953,6 +953,10 @@ fn compatible_existing_peer_contexts_survive_writable_lockfile_regeneration() {
drop((root, mock_instance)); // cleanup
}
#[expect(
clippy::needless_pass_by_value,
reason = "test fixture; the value is embedded whole into a serde_json::json! object"
)]
fn install_with_peer_alias_deps(dependencies: serde_json::Value) -> String {
let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } =
CommandTempCwd::init().add_mocked_registry();

View File

@@ -263,7 +263,7 @@ fn outdated_long_shows_deprecation_details() {
}
/// An npm-aliased dependency is reported under its real registry name,
/// not the alias. Ports pnpm's "outdated() aliased dependency".
/// not the alias. Ports pnpm's "`outdated()` aliased dependency".
#[test]
fn outdated_npm_alias_reports_real_name() {
let (root, workspace, anchor) = setup();

View File

@@ -45,9 +45,9 @@ fn run_executes_declared_script() {
}
/// Positional arguments after the script name flow through to the
/// spawned shell verbatim, joined by spaces. Mirrors `pnpm run
/// <script> -- <args>` minus the npm `--` separator (pacquet does
/// not require it).
/// spawned shell verbatim, joined by spaces. Mirrors
/// `pnpm run <script> -- <args>` minus the npm `--` separator
/// (pacquet does not require it).
#[cfg(unix)]
#[test]
fn run_passes_extra_arguments_to_the_script() {
@@ -103,7 +103,7 @@ fn run_errors_on_missing_script_without_if_present() {
}
/// `pnpm run start` with no `start` script and no `server.js` file fails
/// with NO_SCRIPT_OR_SERVER, matching pnpm's runLifecycleHook guard. (A
/// with `NO_SCRIPT_OR_SERVER`, matching pnpm's runLifecycleHook guard. (A
/// bare `node server.js` fallback would instead surface node's
/// "Cannot find module" error, so the assertion pins the pnpm message.)
#[test]
@@ -132,7 +132,7 @@ fn run_start_without_script_or_server_errors() {
/// An empty `start` script (`"start": ""`) is falsy in pnpm
/// (`!m.scripts.start`), so it falls back to the `node server.js` path
/// like a missing one — and with no `server.js` it must raise
/// NO_SCRIPT_OR_SERVER rather than silently exit 0.
/// `NO_SCRIPT_OR_SERVER` rather than silently exit 0.
#[test]
fn run_empty_start_script_hits_server_js_guard() {
let CommandTempCwd { pacquet, root, workspace, .. } = CommandTempCwd::init();

View File

@@ -3,7 +3,7 @@ use command_extra::CommandExtra;
use pacquet_package_manifest::{DependencyGroup, PackageManifest};
use pacquet_testing_utils::bin::{AddMockedRegistry, CommandTempCwd};
use pretty_assertions::assert_eq;
use std::{ffi::OsStr, fs, path::Path, process::Command};
use std::{ffi::OsStr, fmt::Write as _, fs, path::Path, process::Command};
use tempfile::TempDir;
const DEP: &str = "@pnpm.e2e/dep-of-pkg-with-1-dep";
@@ -53,7 +53,7 @@ fn set_ignore_dependencies(workspace: &Path, names: &[&str]) {
}
yaml.push_str("updateConfig:\n ignoreDependencies:\n");
for name in names {
yaml.push_str(&format!(" - \"{name}\"\n"));
writeln!(yaml, " - \"{name}\"").unwrap();
}
fs::write(&yaml_path, yaml).expect("write pnpm-workspace.yaml");
}
@@ -422,7 +422,7 @@ fn set_strict_catalog(workspace: &Path, entries: &[(&str, &str)]) {
}
yaml.push_str("catalogMode: strict\ncatalog:\n");
for (name, spec) in entries {
yaml.push_str(&format!(" \"{name}\": \"{spec}\"\n"));
writeln!(yaml, " \"{name}\": \"{spec}\"").unwrap();
}
fs::write(&yaml_path, yaml).expect("write pnpm-workspace.yaml");
}

View File

@@ -33,6 +33,7 @@ const BIN_OWNER_OVERRIDES: &[(&str, &[&str])] = &[
/// Whether `pkg_name` is a legitimate owner of the given `bin_name`. The
/// default rule is "the package named `X` owns the `X` bin"; overrides cover
/// cases like `npx` shipping inside `npm`. Mirrors `pkgOwnsBin`.
#[must_use]
pub fn pkg_owns_bin(bin_name: &str, pkg_name: &str) -> bool {
if bin_name == pkg_name {
return true;

View File

@@ -384,9 +384,9 @@ fn reserved_relative_bin_names_are_rejected() {
assert_eq!(commands[0].name, "good");
}
/// [`lexical_normalize`] drops `.` (CurDir) segments. This is a direct
/// [`lexical_normalize`] drops `.` (`CurDir`) segments. This is a direct
/// test on the helper. The integration-style test below covers the same
/// arm via `directories.bin`, but a direct assertion makes the CurDir
/// arm via `directories.bin`, but a direct assertion makes the `CurDir`
/// branch visible to coverage tooling that can't see through inlined
/// call chains.
#[test]
@@ -464,8 +464,8 @@ fn directories_bin_handles_curdir_in_relative_path() {
/// `commands_from_directories_bin` skips entries whose path doesn't
/// yield a usable file name. That covers the `path.file_name() == None`
/// and `to_str() == None` branches, both of which the real fs hardly
/// ever reaches (file_name() returns None only for paths ending in
/// `..`, and to_str() fails only on non-UTF-8 bytes which are rare on
/// ever reaches (`file_name()` returns None only for paths ending in
/// `..`, and `to_str()` fails only on non-UTF-8 bytes which are rare on
/// Unix and impossible on Windows). A fake [`FsWalkFiles`] hands back one
/// such path so the `continue` arm gets exercised directly. The
/// regular `cli` entry alongside it confirms that the well-formed

View File

@@ -59,6 +59,7 @@ impl PackageBinSource {
/// candidates and for any call site that doesn't need to
/// distinguish direct from hoisted (per-slot bin linking,
/// most tests).
#[must_use]
pub fn new(location: PathBuf, manifest: Arc<Value>) -> Self {
Self { location, manifest, origin: BinOrigin::Direct }
}
@@ -67,6 +68,7 @@ impl PackageBinSource {
/// helper so call sites that need to mark candidates as
/// [`BinOrigin::Hoisted`] don't have to spell out the struct
/// literal.
#[must_use]
pub fn with_origin(mut self, origin: BinOrigin) -> Self {
self.origin = origin;
self
@@ -599,8 +601,8 @@ fn link_node_bin(target_path: &Path, shim_path: &Path) -> Result<bool, LinkBinsE
/// Remove an existing dirent at `path`, swallowing `NotFound`. Used by
/// [`link_node_bin`] to clear any prior shim / symlink / hardlink
/// before laying down the new one. Any other IO error (PermissionDenied,
/// EROFS, AppArmor deny, ...) surfaces as [`LinkBinsError::RemoveStaleBin`]
/// before laying down the new one. Any other IO error (`PermissionDenied`,
/// EROFS, `AppArmor` deny, ...) surfaces as [`LinkBinsError::RemoveStaleBin`]
/// so a real failure isn't hidden behind a silent skip.
fn remove_stale_bin(path: &Path) -> Result<(), LinkBinsError> {
match std::fs::remove_file(path) {

View File

@@ -250,7 +250,7 @@ fn link_bins_propagates_parse_manifest_error() {
/// [`link_bins`] must idempotently short-circuit when an existing shim
/// already targets the same bin file. Pins [`is_shim_pointing_at`]'s
/// integration with the writer. Mirrors pnpm's
/// "linkBins() skips bins that already reference the correct target":
/// "`linkBins()` skips bins that already reference the correct target":
/// <https://github.com/pnpm/pnpm/blob/4750fd370c/bins/linker/test/index.ts#L79-L99>.
#[test]
fn link_bins_skips_existing_shim_with_matching_marker() {

View File

@@ -1,5 +1,6 @@
use crate::{capabilities::FsReadHead, path_util::lexical_normalize};
use std::{
fmt::Write as _,
io,
path::{Path, PathBuf},
};
@@ -107,6 +108,7 @@ pub fn read_head_filled<Sys: FsReadHead>(path: &Path, buf: &mut [u8]) -> io::Res
/// wrong runtime for files that just happen to mention `#!` after some
/// whitespace. The first line is taken exactly as-is (`#!` is matched
/// at column 0 of that line via `strip_prefix` in `parse_shebang`).
#[must_use]
pub fn parse_shebang_from_bytes(bytes: &[u8]) -> Option<ScriptRuntime> {
let head = String::from_utf8_lossy(bytes);
let first_line = head.split('\n').next().unwrap_or("").trim_end_matches('\r');
@@ -173,6 +175,7 @@ fn strip_env_prefix(input: &str) -> (&str, bool) {
/// When [`search_script_runtime`] returned `None` (no shebang, unknown
/// extension), the shim execs the target directly via the second branch
/// upstream uses for that case.
#[must_use]
pub fn generate_sh_shim(
target_path: &Path,
shim_path: &Path,
@@ -193,20 +196,22 @@ pub fn generate_sh_shim(
// It always carries the leading `$basedir/` and quotes; never
// just the program name on its own.
let sh_long_prog = format!("\"$basedir/{prog}\"");
sh.push_str(&format!(
"if [ -x {sh_long_prog} ]; then\n exec {sh_long_prog} {args} {quoted_target} \"$@\"\nelse\n exec {prog} {args} {quoted_target} \"$@\"\nfi\n",
));
writeln!(
sh,
"if [ -x {sh_long_prog} ]; then\n exec {sh_long_prog} {args} {quoted_target} \"$@\"\nelse\n exec {prog} {args} {quoted_target} \"$@\"\nfi",
)
.unwrap();
}
// No runtime detected, so exec the target directly. Upstream still
// emits `exit $?` on this branch for parity with non-execve POSIX
// shells.
runtime_opt => {
let args = runtime_opt.map(|runtime| runtime.args.as_str()).unwrap_or("");
sh.push_str(&format!("{quoted_target} {args} \"$@\"\nexit $?\n"));
let args = runtime_opt.map_or("", |runtime| runtime.args.as_str());
writeln!(sh, "{quoted_target} {args} \"$@\"\nexit $?").unwrap();
}
}
sh.push_str(&format!("# {}\n", shim_target_marker(target_path)));
writeln!(sh, "# {}", shim_target_marker(target_path)).unwrap();
sh
}
@@ -217,6 +222,7 @@ pub fn generate_sh_shim(
///
/// CRLF line endings are part of the on-disk contract for `.cmd` files
/// on Windows, so the template uses literal `\r\n`.
#[must_use]
pub fn generate_cmd_shim(
target_path: &Path,
shim_path: &Path,
@@ -234,14 +240,16 @@ pub fn generate_cmd_shim(
match runtime {
Some(ScriptRuntime { prog: Some(prog), args }) => {
let long_prog = format!("\"%~dp0\\{prog}.exe\"");
cmd.push_str(&format!(
"@IF EXIST {long_prog} (\r\n {long_prog} {args} {quoted_target} %*\r\n) ELSE (\r\n @SET PATHEXT=%PATHEXT:;.JS;=;%\r\n {prog} {args} {quoted_target} %*\r\n)\r\n",
));
writeln!(
cmd,
"@IF EXIST {long_prog} (\r\n {long_prog} {args} {quoted_target} %*\r\n) ELSE (\r\n @SET PATHEXT=%PATHEXT:;.JS;=;%\r\n {prog} {args} {quoted_target} %*\r\n)\r",
)
.unwrap();
}
runtime_opt => {
let args = runtime_opt.map(|runtime| runtime.args.as_str()).unwrap_or("");
let args = runtime_opt.map_or("", |runtime| runtime.args.as_str());
// No runtime detected, so exec the target directly.
cmd.push_str(&format!("@{quoted_target} {args} %*\r\n"));
writeln!(cmd, "@{quoted_target} {args} %*\r").unwrap();
}
}
@@ -253,6 +261,7 @@ pub fn generate_cmd_shim(
/// minus the `nodePath`/`prependToPath`/`nodeExecPath`/`progArgs`
/// branches we don't use. The shim self-detects Windows vs. POSIX-ish
/// pwsh and adjusts the executable suffix accordingly.
#[must_use]
pub fn generate_pwsh_shim(
target_path: &Path,
shim_path: &Path,
@@ -294,7 +303,7 @@ pub fn generate_pwsh_shim(
writeln!(pwsh, "exit $ret").unwrap();
}
runtime_opt => {
let args = runtime_opt.map(|runtime| runtime.args.as_str()).unwrap_or("");
let args = runtime_opt.map_or("", |runtime| runtime.args.as_str());
writeln!(pwsh).unwrap();
writeln!(pwsh, "# Support pipeline input").unwrap();
writeln!(pwsh, "if ($MyInvocation.ExpectingInput) {{").unwrap();
@@ -356,6 +365,7 @@ fn shim_target_marker(target_path: &Path) -> String {
/// Whether an already-on-disk shim targets `target_path`. Mirrors
/// `isShimPointingAt`. The check looks for the trailing marker line so the
/// header text never has to be byte-identical between cmd-shim versions.
#[must_use]
pub fn is_shim_pointing_at(shim_content: &str, target_path: &Path) -> bool {
let marker = format!("# {}", shim_target_marker(target_path));
shim_content.lines().any(|line| line == marker)

View File

@@ -221,10 +221,10 @@ fn lexical_normalize_keeps_leading_parent_segments() {
assert_eq!(result, "../../../shared/cli", "leading `..` must propagate");
}
/// [`lexical_normalize`] drops `.` (CurDir) components. This is a direct
/// [`lexical_normalize`] drops `.` (`CurDir`) components. This is a direct
/// test on the helper itself. The indirect test below pins the same
/// behavior at the `relative_target` level, but a direct assertion makes
/// the CurDir arm visible to coverage tooling that can't see through
/// the `CurDir` arm visible to coverage tooling that can't see through
/// inlined call chains.
#[test]
fn lexical_normalize_drops_curdir_segments_directly() {
@@ -234,7 +234,7 @@ fn lexical_normalize_drops_curdir_segments_directly() {
assert_eq!(lexical_normalize(Path::new("./.")), PathBuf::new());
}
/// [`lexical_normalize`] discards `.` (CurDir) components silently.
/// [`lexical_normalize`] discards `.` (`CurDir`) components silently.
/// Verify via [`relative_target`]. A target with embedded `./`
/// resolves the same as without.
#[test]

View File

@@ -149,6 +149,7 @@ where
/// The resulting map's values are post-catalog-resolution, so it
/// compares apples-to-apples against `lockfile.overrides`, which pnpm
/// writes out with `catalog:` already expanded.
#[must_use]
pub fn create_overrides_map_from_parsed(
parsed_overrides: &[VersionOverride],
) -> HashMap<String, String> {

View File

@@ -58,7 +58,7 @@ pub trait GetHomeDir {
/// Capability: read the process's current working directory.
///
/// Mirrors [`std::env::current_dir`]. Only used by code that
/// genuinely needs the cwd — the SmartDefault for
/// genuinely needs the cwd — the `SmartDefault` for
/// [`crate::Config::store_dir`] consults it on Windows for the
/// drive-letter derivation. Code that needs a "starting path" — like
/// [`crate::Config::current`] — takes a direct path parameter

View File

@@ -13,6 +13,7 @@ pub fn default_hoist_pattern() -> Vec<String> {
/// <https://github.com/pnpm/pnpm/blob/94240bc046/config/reader/src/index.ts#L155-L162>,
/// which follows
/// <https://github.com/npm/git/blob/1e1dbd26bd/lib/clone.js#L13-L19>.
#[must_use]
pub fn default_git_shallow_hosts() -> Vec<String> {
vec![
"github.com".to_string(),
@@ -174,9 +175,10 @@ where
let home_dir = Sys::home_dir().expect("Home directory is not available");
match env::consts::OS {
"macos" => home_dir.join("Library/Caches/pnpm"),
"windows" => Sys::var("LOCALAPPDATA")
.map(|local_app_data| PathBuf::from(local_app_data).join("pnpm-cache"))
.unwrap_or_else(|| home_dir.join(".pnpm-cache")),
"windows" => Sys::var("LOCALAPPDATA").map_or_else(
|| home_dir.join(".pnpm-cache"),
|local_app_data| PathBuf::from(local_app_data).join("pnpm-cache"),
),
_ => home_dir.join(".cache/pnpm"),
}
}
@@ -223,6 +225,7 @@ pub fn default_modules_cache_max_age() -> u64 {
/// `pacquet-config` doesn't pull in the modules-yaml crate just for one
/// integer. Both copies must agree; the modules-yaml side carries the
/// same upstream link.
#[must_use]
pub fn default_virtual_store_dir_max_length() -> u64 {
120
}
@@ -236,6 +239,7 @@ pub fn default_virtual_store_dir_max_length() -> u64 {
/// `pacquet-config` doesn't pull in the lockfile crate just for one
/// integer. Both copies must agree; the lockfile side carries the
/// same upstream link.
#[must_use]
pub fn default_peers_suffix_max_length() -> u64 {
1000
}
@@ -309,6 +313,7 @@ pub fn default_child_concurrency_with_parallelism(parallelism: u32) -> u32 {
/// — but exposed under its own name so the
/// [`crate::Config::workspace_concurrency`] field default reads at its
/// own call site.
#[must_use]
pub fn default_workspace_concurrency() -> u32 {
default_child_concurrency()
}
@@ -316,8 +321,9 @@ pub fn default_workspace_concurrency() -> u32 {
/// Available CPU parallelism, mirroring upstream's
/// [`getAvailableParallelism`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/concurrency.ts#L5-L13).
/// Floors at 1.
#[must_use]
pub fn available_parallelism() -> u32 {
std::thread::available_parallelism().map(|count| count.get() as u32).unwrap_or(1).max(1)
std::thread::available_parallelism().map_or(1, |count| count.get() as u32).max(1)
}
/// Resolve `childConcurrency` from a possibly-negative yaml value
@@ -330,6 +336,7 @@ pub fn available_parallelism() -> u32 {
///
/// The negative-offset semantics let users say "use all cores minus
/// N" without hardcoding the core count.
#[must_use]
pub fn resolve_child_concurrency(option: Option<i32>) -> u32 {
resolve_child_concurrency_with_parallelism(option, available_parallelism())
}
@@ -389,6 +396,7 @@ pub fn resolve_child_concurrency_with_parallelism(option: Option<i32>, paralleli
/// where `setgid` doesn't exist; it doesn't translate to Rust
/// (libc's `setgid` is always available on POSIX hosts where libc
/// compiles).
#[must_use]
pub fn default_unsafe_perm() -> bool {
platform_unsafe_perm_default()
}
@@ -419,6 +427,7 @@ fn platform_unsafe_perm_default() -> bool {
/// Pure-logic helper exposed for tests so the POSIX branch can be
/// exercised under both root and non-root uids without root
/// privileges. Mirrors the POSIX half of [`default_unsafe_perm`].
#[must_use]
pub fn is_unsafe_perm_posix(uid: u32) -> bool {
// `unsafe_perm = true` means "do NOT drop privileges". Drop
// only when we *are* root (uid == 0).

View File

@@ -78,6 +78,7 @@ impl WorkspaceSettings {
/// settings via [`Self::apply_to`] *after* `pnpm-workspace.yaml` so
/// env vars win over yaml, mirroring upstream's order at
/// [`config/reader/src/index.ts:471-488`](https://github.com/pnpm/pnpm/blob/2a9bd897bf/config/reader/src/index.ts#L471-L488).
#[must_use]
pub fn from_pnpm_config_env<Sys: EnvVar>() -> Self {
let mut settings = WorkspaceSettings::default();

View File

@@ -47,15 +47,15 @@ pub use workspace_yaml::{
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NodeLinker {
/// dependencies are symlinked from a virtual store at node_modules/.pnpm.
/// dependencies are symlinked from a virtual store at `node_modules/.pnpm`.
#[default]
Isolated,
/// flat node_modules without symlinks is created. Same as the node_modules created by npm or
/// flat `node_modules` without symlinks is created. Same as the `node_modules` created by npm or
/// Yarn Classic.
Hoisted,
/// no node_modules. Plug'n'Play is an innovative strategy for Node that is used by
/// no `node_modules`. Plug'n'Play is an innovative strategy for Node that is used by
/// Yarn Berry. It is recommended to also set symlink setting to false when using pnp as
/// your linker.
Pnp,
@@ -149,7 +149,7 @@ impl<'de> serde::Deserialize<'de> for ScriptsPrependNodePath {
use std::fmt;
struct V;
impl<'de> Visitor<'de> for V {
impl Visitor<'_> for V {
type Value = ScriptsPrependNodePath;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(r#"a boolean or the string "warn-only""#)
@@ -211,6 +211,7 @@ impl LinkWorkspacePackages {
/// [`Self::DirectOnly`] arm only fires at the importer level
/// (`current_depth == 0`); pacquet's caller decides which arm
/// to expose by passing in the current depth.
#[must_use]
pub fn enabled_at_depth(self, current_depth: u32) -> bool {
match self {
LinkWorkspacePackages::Off => false,
@@ -239,7 +240,7 @@ impl<'de> serde::Deserialize<'de> for LinkWorkspacePackages {
use std::fmt;
struct V;
impl<'de> Visitor<'de> for V {
impl Visitor<'_> for V {
type Value = LinkWorkspacePackages;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(r#"a boolean or the string "deep""#)
@@ -302,6 +303,7 @@ impl ResolutionMode {
/// [`Self::LowestDirect`]. Mirrors pnpm's
/// [`pickLowestVersion`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/installing/deps-resolver/src/resolveDependencies.ts#L470)
/// computation.
#[must_use]
pub fn picks_lowest_direct(self) -> bool {
matches!(self, ResolutionMode::TimeBased | ResolutionMode::LowestDirect)
}
@@ -362,12 +364,12 @@ pub enum PackageImportMethod {
/// (project-structural settings).
#[derive(Debug, SmartDefault)]
pub struct Config {
/// When true, all dependencies are hoisted to node_modules/.pnpm/node_modules.
/// This makes unlisted dependencies accessible to all packages inside node_modules.
/// When true, all dependencies are hoisted to `node_modules/.pnpm/node_modules`.
/// This makes unlisted dependencies accessible to all packages inside `node_modules`.
#[default = true]
pub hoist: bool,
/// Tells pnpm which packages should be hoisted to node_modules/.pnpm/node_modules.
/// Tells pnpm which packages should be hoisted to `node_modules/.pnpm/node_modules`.
/// By default, all packages are hoisted - however, if you know that only some flawed packages
/// have phantom dependencies, you can use this option to exclusively hoist the phantom
/// dependencies (recommended).
@@ -403,10 +405,10 @@ pub struct Config {
#[default(_code = "Some(default_public_hoist_pattern())")]
pub public_hoist_pattern: Option<Vec<String>>,
/// By default, pnpm creates a semistrict node_modules, meaning dependencies have access to
/// undeclared dependencies but modules outside of node_modules do not. With this layout,
/// By default, pnpm creates a semistrict `node_modules`, meaning dependencies have access to
/// undeclared dependencies but modules outside of `node_modules` do not. With this layout,
/// most of the packages in the ecosystem work with no issues. However, if some tooling only
/// works when the hoisted dependencies are in the root of node_modules, you can set this to
/// works when the hoisted dependencies are in the root of `node_modules`, you can set this to
/// true to hoist them for you.
pub shamefully_hoist: bool,
@@ -416,7 +418,7 @@ pub struct Config {
#[default(_code = "default_store_dir::<Host>()")]
pub store_dir: StoreDir,
/// The directory in which dependencies will be installed (instead of node_modules).
/// The directory in which dependencies will be installed (instead of `node_modules`).
#[default(_code = "default_modules_dir()")]
pub modules_dir: PathBuf,
@@ -482,7 +484,7 @@ pub struct Config {
pub global_virtual_store_dir: PathBuf,
/// Controls the way packages are imported from the store (if you want to disable symlinks
/// inside node_modules, then you need to change the node-linker setting, not this one).
/// inside `node_modules`, then you need to change the node-linker setting, not this one).
pub package_import_method: PackageImportMethod,
/// The time in minutes after which orphan packages from the modules directory should be
@@ -585,7 +587,7 @@ pub struct Config {
/// spec. Pacquet doesn't have a metadata-fetch path yet (no
/// resolver until Stage 2), so the same flag instead gates
/// pacquet's tarball-fetch fall-through: when both the warm
/// prefetch and the SQLite `index.db` lookup miss, the tarball
/// prefetch and the `SQLite` `index.db` lookup miss, the tarball
/// fetcher fails fast with `ERR_PACQUET_NO_OFFLINE_TARBALL`
/// rather than hitting the registry. The frozen-lockfile install
/// path needs no metadata, so the surface area collapses to
@@ -1384,6 +1386,7 @@ pub struct Config {
}
impl Config {
#[must_use]
pub fn new() -> Self {
Self::default()
}
@@ -1724,7 +1727,7 @@ impl Config {
auth
};
let project_npmrc_dir =
workspace_yaml.as_ref().map(|(base_dir, _)| base_dir.as_path()).unwrap_or(start_dir);
workspace_yaml.as_ref().map_or(start_dir, |(base_dir, _)| base_dir.as_path());
let project_source = read_npmrc(project_npmrc_dir).map(|text| {
let mut auth =
crate::npmrc_auth::NpmrcAuth::from_project_ini::<Sys>(&text, project_npmrc_dir);
@@ -1741,7 +1744,7 @@ impl Config {
// the file's directory; for a bare filename (no parent)
// that's the empty path — i.e. the process cwd — never
// the file itself.
let dir = path.parent().map(|parent| parent.to_path_buf()).unwrap_or_default();
let dir = path.parent().map(std::path::Path::to_path_buf).unwrap_or_default();
parse_trusted_source(text, dir, "<user>/.npmrc")
}),
None => Sys::home_dir().and_then(|dir| {

View File

@@ -36,6 +36,7 @@ use std::sync::Arc;
/// Mirrors upstream's [`createMatcherWithIndex`](https://github.com/pnpm/pnpm/blob/94240bc046/config/matcher/src/index.ts#L16-L40).
/// The numeric index is `Option<usize>` here rather than `i32` /
/// `-1`-sentinel — the same information, idiomatic for Rust.
#[must_use]
pub fn create_matcher_with_index(patterns: &[String]) -> MatcherWithIndex {
match patterns.len() {
0 => MatcherWithIndex(MatcherImpl::Never),
@@ -47,6 +48,7 @@ pub fn create_matcher_with_index(patterns: &[String]) -> MatcherWithIndex {
/// Compile a list of patterns into a matcher returning `true` whenever
/// any include matches and no ignore overrides it. Mirrors upstream's
/// [`createMatcher`](https://github.com/pnpm/pnpm/blob/94240bc046/config/matcher/src/index.ts#L7-L10).
#[must_use]
pub fn create_matcher(patterns: &[String]) -> Matcher {
Matcher(create_matcher_with_index(patterns))
}
@@ -58,6 +60,7 @@ pub struct Matcher(MatcherWithIndex);
impl Matcher {
/// Returns `true` when `input` matches at least one include and no
/// ignore rule overrides it. Empty pattern lists never match.
#[must_use]
pub fn matches(&self, input: &str) -> bool {
self.0.matches(input).is_some()
}
@@ -72,6 +75,7 @@ impl Matcher {
/// even when no realistic input would match (e.g. `["nonexistent-prefix-*"]`)
/// — the fast path is a static check on the pattern list, not a
/// runtime analysis of the compiled regex shape.
#[must_use]
pub fn is_empty(&self) -> bool {
matches!(self.0.0, MatcherImpl::Never)
}
@@ -85,6 +89,7 @@ impl Matcher {
pub struct MatcherWithIndex(MatcherImpl);
impl MatcherWithIndex {
#[must_use]
pub fn matches(&self, input: &str) -> Option<usize> {
self.0.matches(input)
}

View File

@@ -1,7 +1,7 @@
use super::{create_matcher, create_matcher_with_index};
fn pats<const LEN: usize>(patterns: [&str; LEN]) -> Vec<String> {
patterns.iter().map(|pattern| pattern.to_string()).collect()
patterns.iter().map(std::string::ToString::to_string).collect()
}
/// Direct port of upstream's `matcher()` test at

View File

@@ -513,8 +513,7 @@ impl NpmrcAuth {
.registry
.as_deref()
.filter(|registry| !registry.is_empty())
.map(normalize_registry_url)
.unwrap_or_else(|| DEFAULT_REGISTRY.to_string());
.map_or_else(|| DEFAULT_REGISTRY.to_string(), normalize_registry_url);
let key = pacquet_network::nerf_dart(&registry);
if key.is_empty() {
// Unparsable registry (e.g. an unresolved `${VAR}`). Drop

View File

@@ -934,7 +934,7 @@ fn parses_scoped_inline_ca() {
);
let entry = auth.tls_by_uri.get("//reg.example.com/").expect("entry present");
let ca = entry.ca.as_deref().expect("ca set");
assert!(ca.contains("\n"), "expected `\\n` → newline expansion: {ca:?}");
assert!(ca.contains('\n'), "expected `\\n` → newline expansion: {ca:?}");
assert!(ca.contains("BEGIN CERTIFICATE"), "expected PEM header: {ca:?}");
}

View File

@@ -356,8 +356,10 @@ fn every_pnpm_default_is_classified() {
let mapped: BTreeSet<String> =
mapped_rows(&cfg).into_iter().map(|(key, _)| key.to_string()).collect();
let non_literal: BTreeSet<String> = NON_LITERAL.iter().map(|key| key.to_string()).collect();
let not_ported: BTreeSet<String> = NOT_PORTED.iter().map(|key| key.to_string()).collect();
let non_literal: BTreeSet<String> =
NON_LITERAL.iter().map(std::string::ToString::to_string).collect();
let not_ported: BTreeSet<String> =
NOT_PORTED.iter().map(std::string::ToString::to_string).collect();
// The three buckets must be disjoint — a key can't be both mapped
// and skipped.

View File

@@ -25,7 +25,7 @@
//! parser cache against the home store and then can't find the same
//! source files in the case-sensitive TypeScript program loaded from
//! the workspace volume, so `eslint --fix` fails with a
//! "TSConfig does not include this file" error on every project file.
//! "`TSConfig` does not include this file" error on every project file.
//!
//! The hardlink attempt itself is threaded through the
//! [`LinkProbe`] capability so tests can answer the linkability
@@ -53,7 +53,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH},
};
/// Resolve where to place the default pnpm store given the SmartDefault
/// Resolve where to place the default pnpm store given the `SmartDefault`
/// home-based path and the project root.
///
/// Returns `home_default` unchanged when the project's volume can be
@@ -197,7 +197,7 @@ pub(crate) fn host_can_link_between_dirs(from_dir: &Path, to_dir: &Path) -> bool
/// once and removes it.
fn path_temp_in(folder: &Path) -> PathBuf {
let pid = std::process::id();
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.subsec_nanos()).unwrap_or(0);
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.subsec_nanos());
folder.join(format!("_tmp_{pid}_{nanos:08x}"))
}

View File

@@ -65,8 +65,8 @@ fn filesystem_root_windows_keeps_drive_prefix() {
/// Real-fixture happy path: project and home are on the same volume
/// (both inside the same `tempdir`), so the Host impl observes a
/// successful hardlink and `resolve_store_dir` returns the
/// home_default unchanged. This is the dominant branch on a single-
/// volume developer setup and the one the SmartDefault used to
/// `home_default` unchanged. This is the dominant branch on a single-
/// volume developer setup and the one the `SmartDefault` used to
/// short-circuit to without checking.
#[test]
fn resolve_store_dir_same_volume_uses_home_default() {
@@ -95,7 +95,7 @@ fn resolve_store_dir_same_volume_uses_home_default() {
/// call (nextest runs tests in parallel by default), every scenario
/// goes through [`PrefixProbe::with_allow`], which holds
/// [`PREFIX_PROBE_SCENARIO_LOCK`] across the entire set-and-probe.
/// Per CodeRabbit review on pnpm/pnpm#11804.
/// Per `CodeRabbit` review on pnpm/pnpm#11804.
#[cfg(unix)]
static ALLOW_PREFIXES: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
@@ -266,7 +266,7 @@ fn host_can_link_between_dirs_same_volume_is_true() {
/// (so the temp source file can't be created). Mirrors pnpm's
/// `canLink` returning `false` on `EACCES` / `EPERM` / `EXDEV` /
/// anything else — pacquet's probe widens that to "any error means
/// not linkable" so the algorithm degrades to home_default rather
/// not linkable" so the algorithm degrades to `home_default` rather
/// than aborting the install.
#[test]
fn host_can_link_between_dirs_missing_from_dir_is_false() {

View File

@@ -20,7 +20,7 @@ use tempfile::tempdir;
/// module pin specific config-cascade behaviours, none of which
/// turn on cross-volume detection, so the test fakes return
/// `false` for every probe. The probe failing collapses to the
/// pre-existing SmartDefault `store_dir` value, which is what the
/// pre-existing `SmartDefault` `store_dir` value, which is what the
/// pre-port assertions already assume.
///
/// `inert_link_probe!(Name)` wires the impl onto a local test
@@ -366,12 +366,12 @@ pub fn global_config_yaml_request_destination_values_expand_env() {
fs::create_dir_all(&config_dir).expect("create config dir");
fs::write(
config_dir.join("config.yaml"),
r#"
r"
registry: https://${REGISTRY_HOST}/npm/
pnprServer: https://${REGISTRY_HOST}/pnpr/
namedRegistries:
work: https://${REGISTRY_HOST}/work/
"#,
",
)
.expect("write global config.yaml");
@@ -1215,7 +1215,7 @@ pub fn pnpm_workspace_yaml_overrides_global_config_yaml() {
/// `!virtual_store_dir_explicit` guard on the re-anchor, the
/// workspace-root default (`<workspace>/node_modules/.pnpm`)
/// would overwrite the global value any time a `pnpm-workspace.yaml`
/// is present. Regression test for a CodeRabbit review finding on
/// is present. Regression test for a `CodeRabbit` review finding on
/// pnpm/pnpm#11752.
#[test]
pub fn global_virtual_store_dir_survives_workspace_yaml_anchor() {

View File

@@ -157,6 +157,7 @@ impl PackageVersionPolicy {
/// matching rule's payload (`AnyVersion` for a bare-name rule,
/// `ExactVersions` for `name@versions...`), or `PolicyMatch::No`
/// when no rule matched.
#[must_use]
pub fn matches(&self, pkg_name: &str) -> PolicyMatch {
for rule in &self.rules {
if !rule.name_matcher.matches(pkg_name) {

View File

@@ -954,6 +954,7 @@ fn find_workspace_manifest(start: &Path) -> Option<PathBuf> {
/// directory containing the nearest ancestor `pnpm-workspace.yaml`.
/// Returns `start` itself if no manifest is found, so callers can always
/// use the result as a resolution base.
#[must_use]
pub fn workspace_root_or(start: &Path) -> PathBuf {
find_workspace_manifest(start)
.and_then(|path| path.parent().map(Path::to_path_buf))

View File

@@ -11,7 +11,7 @@ use std::{fs, path::Path};
#[test]
fn parses_common_settings_from_yaml() {
let yaml = r#"
let yaml = r"
storeDir: ../my-store
registry: https://reg.example
lockfile: false
@@ -21,7 +21,7 @@ preferWorkspacePackages: true
nodeLinker: hoisted
packages:
- packages/*
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
assert_eq!(settings.store_dir.as_deref(), Some("../my-store"));
assert_eq!(settings.registry.as_deref(), Some("https://reg.example"));
@@ -52,11 +52,11 @@ packages:
#[test]
fn apply_overrides_npmrc_defaults() {
let yaml = r#"
let yaml = r"
storeDir: /absolute/store
lockfile: false
registry: https://reg.example
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let mut config = Config::new();
config.lockfile = true;
@@ -94,12 +94,12 @@ fn apply_resolves_relative_paths_against_base_dir() {
/// crates/tarball.
#[test]
fn parses_fetch_retry_settings_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
fetchRetries: 5
fetchRetryFactor: 3
fetchRetryMintimeout: 1000
fetchRetryMaxtimeout: 4000
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
assert_eq!(settings.fetch_retries, Some(5));
assert_eq!(settings.fetch_retry_factor, Some(3));
@@ -119,11 +119,11 @@ fetchRetryMaxtimeout: 4000
/// onto the `Config`, matching pnpm.
#[test]
fn parses_network_settings_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
networkConcurrency: 8
fetchTimeout: 120000
userAgent: my-agent/2.0
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
assert_eq!(settings.network_concurrency, Some(8));
assert_eq!(settings.fetch_timeout, Some(120_000));
@@ -144,11 +144,11 @@ userAgent: my-agent/2.0
/// schema.
#[test]
fn parses_named_registries_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
namedRegistries:
gh: https://npm.pkg.ghes.example.com/
work: https://npm.work.example.com/
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let named = settings.named_registries.as_ref().expect("named_registries present");
assert_eq!(named.get("gh").map(String::as_str), Some("https://npm.pkg.ghes.example.com/"));
@@ -178,14 +178,14 @@ fn ignores_env_vars_inside_workspace_request_destination_values() {
}
}
let yaml = r#"
let yaml = r"
pnprServer: https://${WORK_HOST}/pnpr/
registry: https://${WORK_HOST}/npm/
namedRegistries:
literal: 'https://registry.example.com/${/npm/'
stable: https://registry.example.com/npm/
work: https://${WORK_HOST}/npm/
"#;
";
let mut settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
settings.substitute_env_untrusted::<EnvWithHost>();
let mut config = Config::new();
@@ -219,13 +219,13 @@ fn expands_env_vars_inside_non_registry_workspace_values() {
}
}
let yaml = r#"
let yaml = r"
storeDir: ${STORE_DIR}
cacheDir: ${CACHE_DIR}
scriptShell: ${SHELL}
nodeOptions: --require=${HOOK}
userAgent: ${USER_AGENT}
"#;
";
let mut settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
settings.substitute_env_untrusted::<EnvWithPaths>();
@@ -249,13 +249,13 @@ fn trusted_settings_expand_env_vars_inside_request_destination_values() {
}
}
let yaml = r#"
let yaml = r"
pnprServer: https://${WORK_HOST}/pnpr/
registry: https://${WORK_HOST}/npm/
namedRegistries:
stable: https://registry.example.com/npm/
work: https://${WORK_HOST}/work/
"#;
";
let mut settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
settings.substitute_env_trusted::<EnvWithHost>();
let mut config = Config::new();
@@ -503,7 +503,7 @@ fn parses_dangerously_allow_all_builds_from_yaml_and_applies() {
/// `scriptsPrependNodePath` is the tri-state from upstream
/// [`Config.scriptsPrependNodePath: boolean | 'warn-only'`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/Config.ts#L108).
/// `true` → Always, `false` → Never, `"warn-only"` → WarnOnly.
/// `true` → Always, `false` → Never, `"warn-only"` → `WarnOnly`.
/// Pacquet's default is Never (matches upstream's
/// [`StrictBuildOptions.scriptsPrependNodePath: false`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/after-install/src/extendBuildOptions.ts#L78)).
#[test]
@@ -804,10 +804,10 @@ fn find_returns_none_when_no_manifest() {
fn apply_replaces_git_shallow_hosts_defaults() {
// pnpm replaces the built-in default array wholesale rather than
// merging it, so we mirror that. See `default_git_shallow_hosts`.
let yaml = r#"
let yaml = r"
gitShallowHosts:
- corp-git.example.com
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let mut config = Config::new();
@@ -827,12 +827,12 @@ gitShallowHosts:
/// [`Config::supported_architectures`] at install time.
#[test]
fn parses_supported_architectures_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
supportedArchitectures:
os: [darwin, linux]
cpu: [arm64, x64]
libc: [glibc]
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let raw = settings.supported_architectures.clone().expect("field present");
assert_eq!(raw.os.as_deref(), Some(&["darwin".to_string(), "linux".to_string()][..]));
@@ -868,10 +868,10 @@ fn omitting_supported_architectures_keeps_default() {
/// is independently overridable.
#[test]
fn partial_supported_architectures_only_sets_listed_axes() {
let yaml = r#"
let yaml = r"
supportedArchitectures:
os: [darwin]
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let raw = settings.supported_architectures.expect("field present");
assert_eq!(raw.os.as_deref(), Some(&["darwin".to_string()][..]));
@@ -961,11 +961,11 @@ fn hoist_false_disables_private_hoist_pattern() {
/// [`createOptionalDependenciesRemover`](https://github.com/pnpm/pnpm/blob/94240bc046/hooks/read-package-hook/src/createOptionalDependenciesRemover.ts).
#[test]
fn parses_ignored_optional_dependencies_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
ignoredOptionalDependencies:
- 'foo'
- '@scope/bar'
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
assert_eq!(
settings.ignored_optional_dependencies.as_deref(),
@@ -1001,12 +1001,12 @@ fn omitting_ignored_optional_dependencies_keeps_default() {
/// downstream diagnostics reference the keys in user-supplied order.
#[test]
fn parses_overrides_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
overrides:
foo: '1.2.3'
'@scope/bar': '^2.0.0'
'baz>qux': '-'
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let overrides = settings.overrides.as_ref().expect("overrides parsed");
let entries: Vec<_> =
@@ -1156,11 +1156,11 @@ fn parses_hoisting_limits_from_yaml_and_applies() {
/// `BTreeSet::default()` empty value.
#[test]
fn parses_external_dependencies_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
externalDependencies:
- bit-bin
- some-other-external
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let raw = settings.external_dependencies.clone().expect("field present");
assert!(raw.contains("bit-bin") && raw.contains("some-other-external"));
@@ -1393,7 +1393,11 @@ peerDependencyRules:
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
let mut config = Config::new();
assert_eq!(config.peer_dependency_rules, Default::default(), "default is empty");
assert_eq!(
config.peer_dependency_rules,
crate::PeerDependencyRules::default(),
"default is empty",
);
settings.apply_to(&mut config, Path::new("/irrelevant"));
let rules = &config.peer_dependency_rules;
assert_eq!(rules.ignore_missing.as_deref(), Some(&["ajv".to_string()][..]));
@@ -1408,10 +1412,10 @@ peerDependencyRules:
/// `Config` fields. A present string deserializes to `Some(Some(_))`.
#[test]
fn parses_script_shell_and_node_options_from_yaml_and_applies() {
let yaml = r#"
let yaml = r"
scriptShell: /usr/bin/bash
nodeOptions: --max-old-space-size=4096
"#;
";
let settings: WorkspaceSettings = serde_saphyr::from_str(yaml).unwrap();
assert_eq!(settings.script_shell, Some(Some("/usr/bin/bash".to_string())));
assert_eq!(settings.node_options, Some(Some("--max-old-space-size=4096".to_string())));

View File

@@ -22,6 +22,7 @@ use std::{io, path::Path};
/// `` `sha256-${crypto.hash('sha256', input, 'base64')}` ``. This is the
/// shape pnpm writes for `pnpmfileChecksum` and (via the object hasher)
/// `packageExtensionsChecksum`.
#[must_use]
pub fn create_hash(input: &str) -> String {
let digest = Sha256::digest(input.as_bytes());
format!("sha256-{}", BASE64.encode(digest))
@@ -50,6 +51,7 @@ pub fn create_hash_from_file(path: &Path) -> io::Result<String> {
/// this hash (project-registry slugs, virtual-store dirnames that
/// overflowed `virtualStoreDirMaxLength`, etc.) must use the same 32-char
/// length so pacquet and pnpm produce the same directory layout.
#[must_use]
pub fn create_short_hash(input: &str) -> String {
let digest = Sha256::digest(input.as_bytes());
let mut hex = format!("{digest:x}");
@@ -80,6 +82,7 @@ pub fn create_short_hash(input: &str) -> String {
/// → underscores, scoped-name slashes → `+`, etc) — this helper only
/// applies the final length/case decision so the escape rules can stay
/// where the structured input lives.
#[must_use]
pub fn shorten_virtual_store_name(filename: String, max_length: usize) -> String {
let lower = filename.to_ascii_lowercase();
let needs_shortening =

View File

@@ -14,7 +14,7 @@
//! The node-resolver and bun-resolver fan the parsed rows out across
//! every artifact a release ships.
//! - [`fetch_verified_node_shasums_file`] — download a Node.js release
//! SHASUMS file, verify its detached OpenPGP signature against the
//! SHASUMS file, verify its detached `OpenPGP` signature against the
//! embedded Node.js release keys, then parse the trusted body.
//! - [`pick_file_checksum_from_shasums_file`] — re-parse a previously
//! downloaded body to extract the integrity of a single file. The
@@ -170,7 +170,7 @@ pub async fn fetch_shasums_file(
}
/// Fetch a Node.js release's `SHASUMS256.txt` and verify its
/// detached OpenPGP signature (`SHASUMS256.txt.sig`) against the
/// detached `OpenPGP` signature (`SHASUMS256.txt.sig`) against the
/// embedded Node.js release keys before returning the body.
pub async fn fetch_verified_node_shasums(
http_client: &ThrottledClient,
@@ -195,7 +195,7 @@ pub async fn fetch_verified_node_shasums(
}
/// Like [`fetch_shasums_file`], but first verifies the SHASUMS file's
/// detached OpenPGP signature against the Node.js release keys.
/// detached `OpenPGP` signature against the Node.js release keys.
pub async fn fetch_verified_node_shasums_file(
http_client: &ThrottledClient,
shasums_url: &str,
@@ -310,6 +310,7 @@ fn signature_unreadable(error: pgp::errors::Error) -> FetchVerifiedNodeShasumsEr
/// Split out from [`fetch_shasums_file`] so verifier-side code that
/// already has the body in hand can decode it without re-issuing the
/// network request.
#[must_use]
pub fn parse_shasums_file(body: &str) -> Vec<ShasumsFileItem> {
body.lines()
.filter_map(|line| {

View File

@@ -12,7 +12,7 @@ pub(crate) struct NodeReleaseKey {
pub(crate) const NODE_RELEASE_KEYS: &[NodeReleaseKey] = &[
NodeReleaseKey {
fingerprint: "4ED778F539E3634C779C87C6D7062848A1AB005C",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFq44CwBCADNRnp3EGOqifmbqOgRb64hkObYdNAClPy/aQfxyWvrZBuVw8OF
DhtziM8M8g986wALaE/nCMufVLrWLVFr4hDHrKr9weaX8vdrPVgvbk/wLfokumnT
@@ -101,11 +101,11 @@ CGGhgXlIlvNNx29Ru5eTE0k4BVuq1ZM+rgQTTQis3x5tTICxog+joxMGVWfC5s2r
MH8=
=H626
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "94AE36675C464D64BAFA68DD7434390BDBE9B9C5",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFWujE4BEAC8YwRvCMbhE0CV0F7U3Swr96hLPerVWEVmFoVshq9acXc8x+NL
a4BjGBt+GBZR2E6cRw4kEbIHmNj/XEE2fF4MG/L149feRxMwk8SuakQbcbvopSmZ
@@ -156,11 +156,11 @@ zTbeFyQlqZoK409fCdxzre1KxNOoox38cH79ffsjinQf1Q5zUIdR2QCnVNhbHMj4
mMJZL+1dR71q
=sm/t
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "1C050899334244A8AF75E53792EF661D867B9DFA",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFtYnJsBCACdDOWJYl/Zd38Mt6kg3j+ooN7sl1bR+SBPEv4yS+lK0cLenv3F
M+cYZEy152H8542OpJpASvQQ+N4TSHcoqDauSR1oQYKkHOj3k+U7wgODlkh5LioO
@@ -198,11 +198,11 @@ DpOCw22UTW2cf5wZTr6THRHeuuAZUAxJXWCJ9WacTkJu7irCfNc1BebbINd+O3se
HiCpJpIGZQQfnMzE+CsMSu3u2gwUnWHBVHzof7wbgs94u+xEgNBjQ6QkbbY=
=ZsQu
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "B9AE9905FFD7803F25714661B63B535A4C206CA9",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFZypZgBEADeIdm42LaylSWw5CosOAte2m6S9DgAGEBrg/yHSFTZWz341EZr
lq1fghIC9nHh09wVlJNOOo3orB9tYoJ3LArB0MQb7Ha7dcnfn98O1od0T4QTlEro
@@ -277,11 +277,11 @@ W+4+S206EIfKl43eLd/PME1ot+FAOepT9CXkQxaAs+ZH7rKmlx8mmowujwX0Cb1j
pkatHhJZAsqY8KUjSXqFfVjPHglxLyP/8ywgi/MBlwSnTTtbHyKmqDyts1PP
=CMP0
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "77984A986EBC2AA786BC0F66B01FBB92821C587A",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFf3hmYBEAC6YQyQighf1meU1gG8OyjlfRp7KMJJHmxtBjtH5fWtM8IWCdmZ
nCoUgNbJHIYD2Fn3h/ijX4/S/492mbymadmW24D7mYde/OBuAQCmffjypBCUS5Gh
@@ -332,11 +332,11 @@ wzSUzZBpKVw/+hPrbFbpoRzpjDSAe0XePuiTcVbGZIQtjsAgJsK6t+mGHppWTri/
KoZmFSI=
=Fwcs
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "71DCFD284A79C3B38668286BC97EC7A07EDE3FC1",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFRgAMsBEAC1SlN8Db9p/+pQcrlXM0xtbVDOZksBQynOzUV+Y/NBTmeBnYMo
gh+gTau0Iv7UlDanKlB8pQubo1Gwp6ToLB6pcoh+Zuy6BRB1yHYtNFhrb33QZ5Qu
@@ -481,11 +481,11 @@ GuyYaS3LXdYyGTKWNrxto1CEkGQArS4Ei+CrTbeb6GAXEn8GDhzfpw09JT2c3Ab5
ntI6COSRy17o3GzXm9d4V70=
=h9up
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "61FC681DFB92A079F1685E77973F295594EC4689",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF5MceABEADFVslAVcrIyj7pcWEPeYgnr+psd6CNKlqOslf0+WUFSf0RVl45
uTckfS/D46llZRGbnOOixtM0v0fK60iSjLfWOTQJWAF8BIHaCEb3nAafZcFGnRb/
@@ -537,11 +537,11 @@ I24uLT/RC+8e8xwGqvcPc5HZ5POrQJLknW+xnSDoAQNcjwcyD3oX9nH3dfPAV6LP
+SMSBsOPDf0=
=i++9
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "FD3A5288F042B6850C66B31F09FE44734EB7990E",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFM7JpoBEACmf7uB5P5QJ8X38ARQn+dr+/O+6/wzkKzUcoFvRArwZTcpdEO/
0C12kNSpK2UkVMh4sorYwA8W0yv3spZJWU3TiIfCVryxqZaAWEIU+dwsQ0P6EAUy
@@ -652,11 +652,11 @@ FuIInPdrU0Bg1jqBWQZDqjEvfRHYIDQpd3Ahxbv56J02tl6s29gMT5dYRFE7OhaJ
0CCphsH66qcPvWImsyQ3OdVJ7AU3fuFRFVIgQwoohTpKKCIGTJ8u1g==
=NfOh
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFcGZx4BEACa92SjEniMQIBdb0btnZRu8vzOGNe+ndzXIWPyu2h+p0xZ/2JN
MDQW5hc8USoV4/rTssdqDOqcu3AkmLtZi14IaRJ1TQP6Zb05I8MOEm58WXXn7fSF
@@ -724,11 +724,11 @@ VpZxqzy2y5PDphbf/Rbx4ePeziYIsa0PRyjrS6fPu5QUV/EBK2V6nibMHsGuSfin
R/5R
=+ahi
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFaVjpQBEADt/ZC4FsskPNkAgLq240K+CjPJzq/0cuEyABJeAVeYWJFUJRcb
zNHBVzr85vW0pEKJUGyTyVxGV1P9VzkqaL5RRiupViwC5lf48P78fCMgEa2z4LIt
@@ -956,11 +956,11 @@ Ku9xqajxZHvDBZ9cNCnpq3PQIWLai1YFCGDmsEDc7nF/g89NnfQRoTvbTm2v+iYt
4G5Uhcw=
=YIVB
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGKD3OQBDAC3ESxNd7dHfM7Hl3sE7Xn2osS4UZkJtmXA7hdbfybzf164wCbL
cYECq0GrGfgdnKEMWNzr2S08KkUWQwJZd70Jt1voVpqWVXdtBH/KxGwifixMkTEy
@@ -1001,11 +1001,11 @@ Mj+1hz8EOU7J3vY04ZRHDyapCRYoRXd/N8MUnsZ/ySRdA/Sk57Ujn9UH3nFEhD6A
H9y08K6zB4JFN7saJiCj2Q7W2nXuEUhRVs2K
=L7AX
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF222c0BEAC/wIiI7EYmA7yprNa/0en2leF+CrF09BlCItTHH5IgjSLGq2tI
Bi3hIhf7TitDlu6GphHlFjvhj6UDgdEmr0itcoOLRhtER6WlmMaXtS+im5fPSLWW
@@ -1095,11 +1095,11 @@ ibYKg149T1Y2+JrwHaXdKiM0U4k3a9X+TjA8BCV0+m3eY97cuFqXItaEGg/3FJyf
NBG2Fw==
=Cg9A
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "DD8F2338BAE7501E3DD5AC78C273792F7D83545D",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFKKodABCADiE7Ex8GXnQNgipqbTADO5+BfufYFeq9YLEKkuOUfnjAZ8Wzle
4eLL4rdfFSuwuUO0rkSFOpNjkjKqxfRo0RkmlMxdHwT2auf/yrfX4EyhyKDn1Vh8
@@ -1135,11 +1135,11 @@ Zpiqs673aIg0MoZPCyTTO6Atfsv2Li8EossDZpvJuroJFZw5zvIEy7AiDAcCZjMj
8FLoLzom0A1FNxCvgzOraMITOobs
=dTMc
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "A48C2BEE680E841632CD4E44F07496B3EB3C1762",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFhJehkBEADNX8qrO9msK8u1znGaBG+Fr0FS5qzMxpC9oStGZV8abX/rrLkN
NiImTUMQG6Ooz7luOBSSF7VbYT2xSrgzuYV8WrSJE4Oo0AjVNn2YnbwgyzcQ5FhY
@@ -1190,11 +1190,11 @@ npqSZaj8YOWI+DPDYGYzqQwFgyGLH0J32l5zzpcggwhEoDsQSMHKxekHD9bX0Bck
fyCIa9sx6lcYB+gDJgESL0Nt
=hSfG
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "B9E2F5981AA6E0CD28160D9FF13993A75599653C",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFl60g4BEAChHPOjxooUAUjigTKIERl8uYyOTA0JL9nICb7Azbl2J3ygmku6
HdbaqgfaHRwap+hE0s8/oLkccFJVnab6b0rexWQEvarOtzkARJ0wqbxQIQBJKhfS
@@ -1245,11 +1245,11 @@ d83bmcM7vvf4bBddioM4BiiydSKXAYoFDZzs7xXuce3LpCleYnUISVBF5g3KVLSE
1j7Q6fERAXlFlv7ciEuKBz5A3SK7Xt7Eb7vQ
=kARQ
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "108F52B48DB57BB0CC439B2997B01419BD92F80A",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFltAggBEADHYmcgBOWwJTVRJCnqEpC8IvOber468ikSgNolQHFbyUkJy/kd
cx+byBnvqs+s050N6EocIkmPvaa+ptNYf21uDnGKPyFqPMKn68iAXwVDasUK1SST
@@ -1300,11 +1300,11 @@ n+uWqOu9Yo24d3z8jRopp7dTponoPYTQKmH8uzNeqvO3DFxm7fJ7TSrXaYJndOQ3
AI4=
=Sf/w
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "9554F04D7259F04124DE6B476D5A82AC7E37093B",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFTROWUBCACslqx8p2znj3CwEqWK+bEgyfxykVC1iFEABB6UCQ5UrlAiYTjI
0vwFaTlbWkD3dWBxiFN2+n24Lro549ATmXGO+i0Sacr7DTgawZdkJuM17nNmlAW1
@@ -1334,11 +1334,11 @@ fb3Fyii+q1BCWWZsyZjkLW7kcpf62i/YlrJHjWRheWLblLtxqy4ZiKqqLCtyfh9a
xr5u2jsokh8=
=2Ugv
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "93C7E9E91B49E432C2F75674B0A78B0A6C481CF6",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBEx8nbMBCACjmblSAGggunFHAWRGZLWLKltA2PG6rIM0bOokJFWtGwRqBCAa
dKuNwBGy7eBwFQAxuHnNwDKqyGgCKUBe4RUuz0gKr1WgkPAzHZppo/n5BhE4iIxr
@@ -1367,11 +1367,11 @@ ZqC+u0j/R8OQqS/kVe7qt41nmNZzwsROuKuJt1xFD2GMquL+Z4oCu4YLKM13WPkd
pYtVXNkSmiSUWMmh7QfVNFrWDqlP8vUi3BIEV3FtjXblPkbKbow=
=o1Jn
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "56730D5401028683275BD23C23EFEFE93C4CFFFE",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFiGktYBEADoBPdUkwVA9dNViz2wxb+e3XiaQaesSHvRReDpOpWQ7yuw2yLd
xeUer6Iexcoje/i18x+eD7FF1gi+Lo2J1LVIRchTCx2vGs9P2iYW1iypCRl89C72
@@ -1422,11 +1422,11 @@ Q6hquBOorI1gxgDFkGoq928iFLyN0C3mV1qAmlWq5EKlKr3U4DEwbRMUYIFcqO2r
OweTnxa0BqWTj1O87/SvSKdOd9ZI5GIp
=K6ke
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "114F43EE0176B71C7BC219DD50A3051F888C628D",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFSvCTEBEADIa8J6pku+vT9RZ/cU4wKmC441OVghEZ8Cuct4AynkZQZ8Hpra
4mq9SEjB9t2KSM7kvN/yBrrJBJwAnR7Q+e3+gfthL8Hmgr7K2J+qJdSm7Va/LcSK
@@ -1477,11 +1477,11 @@ k7rTC4J6kM9hoUCMOJ0XqBfQZVeQd2x3bkZpx1ubH6L5ilZHKSvHcqOy6HE4wyDz
USXWhEefc3s6beE=
=v6Nn
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "7937DFD2AB06298B2293C3187D33FF9D0246406D",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGiBEPOjg4RBAC2iPU+EHukOrmApMnhYym03gV/VbdPDydVVj+fc7TyjULlKWP7
iEMFZb58EBjjK4Db3O5JiC9yZ7NRgYvqCFGP7hTVBTykJucXIyT8wCmaAImrtAlg
@@ -1555,11 +1555,11 @@ gEoCGwwACgkQfTP/nQJGQG2l6ACfbyXz7xaWlSaubTDOo/N6yKFkKSQAoILPCUOi
Uhw2iju+XrCVJ/AXUtwH
=AG5V
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "74F12602B6F1C4E913FAA37AD3A89613643B6201",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF/qTWsBEADlnzvN5W//gwj5oOpnyPQLjjguiXi0NPe9o0LcQgOmccD8a76R
r4VQDDM9iFieOcmdcJzTeEcTli165+pBTilqR/RBjq63N4jFzzsiCDJaf8utUhlW
@@ -1688,11 +1688,11 @@ ZZ4mxiQ4srJvZcj63UV3H4Jsr4vXJrhRSDX5Va8mAyF+rF6g4Cjgxp1fqkB0A1lK
ni5lUGYmUmXIuRub
=EVa1
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "141F07595B7B3FFE74309A937405533BE57C7D57",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGHo9TgBEADbSK0AjEvbVkjrvHk3HG1InM3H0kqEjXxenzTukSHQOl8ytLUD
gP1PPzuHmgTqgONkNa3JNHv7AMO7lUukIYTSvtzOI6fl2i13ZeASDGCBfFHYjxeA
@@ -1778,11 +1778,11 @@ PggS8SeMj1QnC9JLQIiMKNUMpZ8Pq0XAQSQy7mWYwYN6/W3SqqZuJSxogDSIK1J6
AdTumtl+mDJRFlYwC0LHorFIhLjym1o4FbcdU440P6xC
=48+4
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZAFttBYJKwYBBAHaRw8BAQdA9UUQNclFp0rIrgtQnNw6BgjDINkFPoVbuS4H
sQNEf+e0LEp1YW4gSm9zw6kgQXJib2xlZGEgPHNveWp1YW5hcmJvbEBnbWFpbC5j
@@ -1795,11 +1795,11 @@ twUCZAFttAIbDAAKCRDHer+gDdvytzxaAQDvYX4o1Y6R30bYwIXemGgbO8GlCkgk
5it7MjeSk8vUygEApE5zIi5OtP8TlPiMgu2MoJbIltqqDCKWTtHPIQRNIwk=
=IisY
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "A363A499291CBBC940DD62E41F10027AF002F8B0",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: A363 A499 291C BBC9 40DD 62E4 1F10 027A F002 F8B0
Comment: ulises Gascon <ulisesgascongonzalez@gmail.com>
@@ -1984,11 +1984,11 @@ LDARUWnKPkw2YY4m7cWPDHzXF+pN3KHLej+u1O+0y1q9OmpQ0yogo1fnzmeIHvJY
iYmSNsSq9FUzTw==
=CCuY
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "CC68F5A3106FF448322E48ED27F5E38D5B0A215F",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGYFoj0BEACm4UKYcykICb5oxZQQxSZRYwzkSngpeFcrruHVHfg2jcQ+VmRV
C3NrbhSrBQuJ0pMx/zq/yZB6K4JS+EMf5GpaX2ZsVsj/MPoSKVHcyXR4clIulCxN
@@ -2040,11 +2040,11 @@ eu0agvd1+yKZrcoEo+npXyzXPckRsHchS6pbhck1vFtgKwXpPCjSC0e6IwrDgAGw
ZWf0a3VP6Gco5bmDPhvGoLEs9Vw5
=57kS
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "C0D6248439F1D5604AAFFB4021D900FFDB233756",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGHwIyoBEADN55NGkn1hvOjFotJVr8aeU6/xGZF3gPLi7q2qaX5CXtVMGS2B
5h9kiBKrNo1+xeC1+jRu9r5179lTiYV808qNFBQdr+5ZBnOoszadlMPMtU89qsR9
@@ -2096,11 +2096,11 @@ lzRFvHZnq6EP1QgDOW55OhdXTqb9v4P3bM77Ez7qHQWKoZOoC0mYvXJff3SOzkOE
Th80Z8v5rFX5xNw+zn0Ee+yr
=SZFY
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
NodeReleaseKey {
fingerprint: "5BE8A3F6C8A5C01D106C0AD820B1A390B168D356",
armored_key: r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaGA63BYJKwYBBAHaRw8BAQdAo/yU+MutacFmmn0CEX495goNrBxR24235XLM
cvHYjfq0L0FudG9pbmUgZHUgSGFtZWwgPGR1aGFtZWxhbnRvaW5lMTk5NUBnbWFp
@@ -2117,6 +2117,6 @@ sWjTVgUCaGA63AIbDAAKCRAgsaOQsWjTVu8oAP9Bc+QY+9FikX3YvMgWAqiDlVOy
o0y6UIZGBMSQlF80wAD/d34LqtVIVe9oe5NO3xA75+6Ew8tGeAjUq/ovagr5dAU=
=JsVv
-----END PGP PUBLIC KEY BLOCK-----
"#,
",
},
];

View File

@@ -94,7 +94,9 @@ be127be1d98cad94c56f46245d0f2de89934d300028694456861a6d5ac558bf3 foo.msi";
assert_eq!(file_name, "foo.tar.gz");
assert_eq!(sha256, "ed52239294ad517fbe91");
}
other => panic!("expected Malformed, got {other:?}"),
other @ PickFileChecksumError::NotFound { .. } => {
panic!("expected Malformed, got {other:?}")
}
}
}

View File

@@ -16,6 +16,7 @@ pub struct DepPath(String);
impl DepPath {
/// Borrow the underlying depPath string.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}

View File

@@ -22,6 +22,7 @@ use pacquet_crypto_hash::shorten_virtual_store_name;
/// [`pacquet_crypto_hash::shorten_virtual_store_name`]. Same trailing
/// branch the flat-name call sites already consume — single source of
/// truth for the truncation arithmetic and `file+` carve-out.
#[must_use]
pub fn dep_path_to_filename(dep_path: &str, max_length_without_hash: usize) -> String {
let mut filename = dep_path_to_filename_unescaped(dep_path);
filename = filename.replace(['\\', '/', ':', '*', '?', '"', '<', '>', '|', '#'], "+");

View File

@@ -5,6 +5,7 @@
///
/// The check is byte-level (not parsed) so callers that hold the raw
/// snapshot key can filter without round-tripping through [`crate::DepPath`].
#[must_use]
pub fn is_runtime_dep_path(dep_path: &str) -> bool {
for prefix in ["node@runtime:", "bun@runtime:", "deno@runtime:"] {
if let Some(rest) = dep_path.strip_prefix(prefix)

View File

@@ -11,6 +11,7 @@
/// all collapse to `packages+b`, and `.hidden/pkg` collapses to
/// `hidden+pkg`. Pnpm accepts the rare collision for lockfile
/// stability; see [pnpm/pnpm#11272](https://github.com/pnpm/pnpm/issues/11272).
#[must_use]
pub fn link_path_to_peer_version(rel_path: &str) -> String {
let trimmed = rel_path.trim_start_matches('.');

View File

@@ -20,6 +20,7 @@ impl PeerId {
/// `name@version`; depPaths are passed through with a single
/// leading `/` stripped (mirrors upstream's `peerId[0] === '/'`
/// fast path for the absolute / relative depPath distinction).
#[must_use]
pub fn as_segment(&self) -> String {
match self {
PeerId::Pair { name, version } => format!("{name}@{version}"),

View File

@@ -18,6 +18,7 @@ pub struct DepPathSuffixIndex {
/// the scan from — the depPath has neither a peer suffix nor a patch
/// hash, and the whole string is the `pkgIdWithPatchHash` (without a
/// patch hash, that's just the bare `name@version` id).
#[must_use]
pub fn index_of_dep_path_suffix(dep_path: &str) -> DepPathSuffixIndex {
let bytes = dep_path.as_bytes();
let absent = DepPathSuffixIndex { peers_index: None, patch_hash_index: None };
@@ -58,6 +59,7 @@ pub fn index_of_dep_path_suffix(dep_path: &str) -> DepPathSuffixIndex {
/// Strip the peer-suffix and `(patch_hash=…)` segments from `dep_path`,
/// returning just the `pkgId` (no patch hash) prefix. Mirrors pnpm's
/// [`removeSuffix`](https://github.com/pnpm/pnpm/blob/097983fbca/deps/path/src/index.ts#L52-L61).
#[must_use]
pub fn remove_suffix(dep_path: &str) -> &str {
let DepPathSuffixIndex { peers_index, patch_hash_index } = index_of_dep_path_suffix(dep_path);
if let Some(idx) = patch_hash_index {
@@ -72,6 +74,7 @@ pub fn remove_suffix(dep_path: &str) -> &str {
/// Strip just the peer-suffix from `dep_path`, keeping the
/// `(patch_hash=…)` segment if present. Mirrors pnpm's
/// [`getPkgIdWithPatchHash`](https://github.com/pnpm/pnpm/blob/097983fbca/deps/path/src/index.ts#L63-L70).
#[must_use]
pub fn get_pkg_id_with_patch_hash(dep_path: &str) -> &str {
let DepPathSuffixIndex { peers_index, .. } = index_of_dep_path_suffix(dep_path);
match peers_index {

View File

@@ -18,6 +18,7 @@ use crate::suffix_index::index_of_dep_path_suffix;
/// applies, or an owned [`String`] when the second transform (name
/// prefix strip) runs. Callers that always need ownership can
/// `.to_string()`.
#[must_use]
pub fn try_get_package_id(dep_path: &str) -> std::borrow::Cow<'_, str> {
let suffix_index = index_of_dep_path_suffix(dep_path);
let sep_index = suffix_index.patch_hash_index.or(suffix_index.peers_index);

View File

@@ -1,7 +1,7 @@
use crate::Implementation;
/// Detect libc implementation from the ELF interpreter
/// (`/proc/self/exe` PT_INTERP).
/// (`/proc/self/exe` `PT_INTERP`).
///
/// Returns `Some(Implementation::Musl)` when the interpreter path
/// contains `"/ld-musl-"`, `Some(Implementation::Glibc)` when it

View File

@@ -14,6 +14,7 @@ pub enum Implementation {
impl Implementation {
/// Return the string used in pnpm's platform selector: `"glibc"` or
/// `"musl"`.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Implementation::Glibc => "glibc",
@@ -30,7 +31,7 @@ impl Implementation {
/// detection methods fail.
///
/// Detection order:
/// 1. **ELF interpreter** — read PT_INTERP from `/proc/self/exe`.
/// 1. **ELF interpreter** — read `PT_INTERP` from `/proc/self/exe`.
/// If the dynamic linker path contains `"/ld-musl-"` → musl;
/// if it contains `"/ld-linux-"` → glibc.
/// 2. **Filesystem** — read first 2048 bytes of `/usr/bin/ldd`.
@@ -44,6 +45,7 @@ impl Implementation {
/// and the command fallback is only reached when cheaper methods
/// fail. This makes detection work in slim containers where
/// `getconf` or `ldd` may not be on PATH or installed at all.
#[must_use]
pub fn detect() -> Option<Implementation> {
if !is_linux() {
return None;
@@ -65,6 +67,7 @@ fn detect_implementation() -> Option<Implementation> {
/// `sunos` / `aix` / `android`. Rust uses `macos` / `linux` /
/// `windows` / `freebsd` / `openbsd` / `solaris` / `aix` /
/// `android`. Only `macos`, `windows`, and `solaris` differ.
#[must_use]
pub fn host_platform() -> &'static str {
match std::env::consts::OS {
"macos" => "darwin",
@@ -82,6 +85,7 @@ pub fn host_platform() -> &'static str {
/// what Node itself emits on each target — anything left as
/// passthrough (e.g. `arm`, `s390x`, `riscv64`) already matches
/// between the two naming schemes.
#[must_use]
pub fn host_arch() -> &'static str {
match std::env::consts::ARCH {
"x86_64" => "x64",

View File

@@ -81,21 +81,15 @@ fn walk_all_inner(
source,
})?;
let file_name = entry.file_name();
let file_name_str = match file_name.to_str() {
Some(s) => s,
// Non-UTF-8 names can't round-trip through pacquet's
// forward-slash relative-path map; skip them, matching
// upstream's implicit JS string semantics.
None => continue,
};
// Non-UTF-8 names can't round-trip through pacquet's forward-slash
// relative-path map; skip them, matching upstream's implicit JS
// string semantics.
let Some(file_name_str) = file_name.to_str() else { continue };
if file_name_str == "node_modules" {
continue;
}
let entry_path = entry.path();
let resolved = match resolve_entry(&entry_path, resolve_symlinks)? {
Some(r) => r,
None => continue,
};
let Some(resolved) = resolve_entry(&entry_path, resolve_symlinks)? else { continue };
let rel = if rel_prefix.is_empty() {
file_name_str.to_string()
} else {

View File

@@ -26,10 +26,10 @@ fn collect_rels(
// Use `dunce::canonicalize` semantics indirectly: strip
// the tmp root prefix off the absolute path and report
// the remainder. That keeps assertions deterministic.
let stripped = abs
.strip_prefix(root)
.map(|path| path.display().to_string().replace('\\', "/"))
.unwrap_or_else(|_| abs.display().to_string());
let stripped = abs.strip_prefix(root).map_or_else(
|_| abs.display().to_string(),
|path| path.display().to_string().replace('\\', "/"),
);
(rel, stripped)
})
.collect()

View File

@@ -234,7 +234,7 @@ fn extract_sha256(body: &str) -> Option<String> {
let bytes = body.as_bytes();
bytes
.windows(64)
.find(|window| window.iter().all(|byte| byte.is_ascii_hexdigit()))
.find(|window| window.iter().all(u8::is_ascii_hexdigit))
.map(|window| String::from_utf8_lossy(window).to_ascii_lowercase())
}

View File

@@ -34,6 +34,7 @@ pub struct GetNodeArtifactAddressOptions<'a> {
}
/// Compose the archive URL pieces for a single Node.js platform variant.
#[must_use]
pub fn get_node_artifact_address(opts: GetNodeArtifactAddressOptions<'_>) -> NodeArtifactAddress {
let is_windows = opts.platform == "win32";
let normalized_platform = if is_windows { "win" } else { opts.platform };

View File

@@ -23,6 +23,7 @@ pub const UNOFFICIAL_NODE_MIRROR_BASE_URL: &str =
/// A missing entry falls back to the official nodejs.org tree. The
/// returned URL always ends with `/` so callers can concatenate
/// `v<version>/...` without a defensive check.
#[must_use]
pub fn get_node_mirror(
node_download_mirrors: Option<&HashMap<String, String>>,
release_channel: &str,

View File

@@ -14,6 +14,7 @@
/// `node_version` is optional because the macOS Apple-Silicon rule
/// only matters when a concrete version is in hand; callers that
/// haven't picked one yet pass `None` and accept the default mapping.
#[must_use]
pub fn get_normalized_arch(platform: &str, arch: &str, node_version: Option<&str>) -> String {
if let Some(version) = node_version
&& let Some(major) = node_major(version)

View File

@@ -107,16 +107,12 @@ pub async fn resolve_node_versions(
.unwrap_or_default());
}
let (versions, range) = filter_versions(&all_versions, version_spec);
let parsed_range = match Range::parse(&range) {
Ok(parsed) => parsed,
Err(_) => return Ok(Vec::new()),
};
let Ok(parsed_range) = Range::parse(&range) else { return Ok(Vec::new()) };
Ok(versions
.into_iter()
.filter(|version| {
Version::parse(version)
.map(|parsed| satisfies_with_prereleases(&parsed, &parsed_range))
.unwrap_or(false)
.is_ok_and(|parsed| satisfies_with_prereleases(&parsed, &parsed_range))
})
.collect())
}

View File

@@ -410,7 +410,7 @@ fn read_optional_subdeps(
) -> Result<Vec<NormalizedSubdep>, ConfigDepError> {
let mut subdeps = Vec::new();
for (subdep_name, dep_ref) in optionals {
let version = dep_ref.ver_peer().map(|ver_peer| ver_peer.to_string()).unwrap_or_default();
let version = dep_ref.ver_peer().map(std::string::ToString::to_string).unwrap_or_default();
let subdep_name = subdep_name.to_string();
let subdep_key = format!("{subdep_name}@{version}");
let key = subdep_key.parse().map_err(|_| ConfigDepError::EnvLockfileCorrupted {
@@ -438,7 +438,7 @@ fn read_optional_subdeps(
),
})?;
subdeps.push(NormalizedSubdep {
name: subdep_name.to_string(),
name: subdep_name.clone(),
version,
integrity,
tarball,
@@ -480,8 +480,7 @@ fn integrity_and_tarball(
/// layout falls back to a recursive remove. A genuine failure (anything
/// but "already gone") is logged rather than silently swallowed.
fn prune_link(path: &Path) {
let is_link =
fs::symlink_metadata(path).map(|meta| meta.file_type().is_symlink()).unwrap_or(false);
let is_link = fs::symlink_metadata(path).is_ok_and(|meta| meta.file_type().is_symlink());
let result =
if is_link { pacquet_fs::remove_symlink_dir(path) } else { fs::remove_dir_all(path) };
if let Err(error) = result

View File

@@ -103,7 +103,7 @@ fn options<'a>(
registries: &harness.registries,
verify_store_integrity: true,
offline: false,
package_import_method: Default::default(),
package_import_method: pacquet_config::PackageImportMethod::default(),
retry_opts: RetryOpts::default(),
frozen_lockfile: frozen,
supported_architectures: None,

View File

@@ -75,6 +75,7 @@ impl EnvVar for SystemEnv {
/// dropped to `""`.
///
/// [`Sys::var`]: EnvVar::var
#[must_use]
pub fn env_replace_lossy<Sys: EnvVar>(text: &str) -> (String, Vec<String>) {
let bytes = text.as_bytes();
let mut output = String::with_capacity(text.len());

View File

@@ -39,6 +39,7 @@ pub enum ScriptsPrependNodePath {
/// 5. `dirname(node_execpath)` when `scripts_prepend_node_path` is
/// [`Always`](ScriptsPrependNodePath::Always),
/// 6. `original_path` (typically the inherited system PATH).
#[must_use]
pub fn extend_path(
wd: &Path,
original_path: Option<&OsString>,

View File

@@ -158,7 +158,7 @@ fn original_path_is_appended_last() {
}
/// `scripts_prepend_node_path: Always` appends `dirname(node)` after
/// extra_bin_paths but before originalPath.
/// `extra_bin_paths` but before originalPath.
#[test]
fn scripts_prepend_node_path_always_appends_dirname_of_node() {
let wd = Path::new("/proj");

View File

@@ -144,9 +144,9 @@ pub const PROJECT_LIFECYCLE_STAGES: [&str; 6] =
///
/// Returns `true` if any script was present and executed.
pub fn run_postinstall_hooks<Reporter: self::Reporter>(
opts: RunPostinstallHooks<'_>,
opts: &RunPostinstallHooks<'_>,
) -> Result<bool, LifecycleScriptError> {
run_lifecycle_stages::<Reporter>(&opts, &DEPENDENCY_LIFECYCLE_STAGES)
run_lifecycle_stages::<Reporter>(opts, &DEPENDENCY_LIFECYCLE_STAGES)
}
/// Run a workspace project's own lifecycle scripts during
@@ -161,9 +161,9 @@ pub fn run_postinstall_hooks<Reporter: self::Reporter>(
///
/// Returns `true` if any script was present and executed.
pub fn run_project_lifecycle_scripts<Reporter: self::Reporter>(
opts: RunPostinstallHooks<'_>,
opts: &RunPostinstallHooks<'_>,
) -> Result<bool, LifecycleScriptError> {
run_lifecycle_stages::<Reporter>(&opts, &PROJECT_LIFECYCLE_STAGES)
run_lifecycle_stages::<Reporter>(opts, &PROJECT_LIFECYCLE_STAGES)
}
/// Read the manifest at `opts.pkg_root` and run each of `stages` whose

View File

@@ -60,7 +60,7 @@ fn lifecycle_emits_script_stdio_and_exit_in_order() {
optional: false,
};
let ran = run_postinstall_hooks::<RecordingReporter>(opts).expect("postinstall");
let ran = run_postinstall_hooks::<RecordingReporter>(&opts).expect("postinstall");
assert!(ran, "postinstall script should report executed");
let captured = EVENTS.lock().expect("lock").clone();
@@ -169,7 +169,7 @@ fn lifecycle_events_carry_optional_flag() {
optional: true,
};
run_postinstall_hooks::<RecordingReporter>(opts).expect("postinstall");
run_postinstall_hooks::<RecordingReporter>(&opts).expect("postinstall");
let captured = EVENTS.lock().expect("lock").clone();
let lifecycle_events: Vec<_> = captured
@@ -242,7 +242,7 @@ fn lifecycle_emits_exit_with_nonzero_code_on_failure() {
optional: false,
};
let err = run_postinstall_hooks::<RecordingReporter>(opts).expect_err("script must fail");
let err = run_postinstall_hooks::<RecordingReporter>(&opts).expect_err("script must fail");
eprintln!("ERR: {err}");
let captured = EVENTS.lock().expect("lock").clone();
@@ -292,7 +292,7 @@ fn lifecycle_runs_under_silent_reporter() {
optional: false,
};
let ran = run_postinstall_hooks::<SilentReporter>(opts).expect("postinstall");
let ran = run_postinstall_hooks::<SilentReporter>(&opts).expect("postinstall");
assert!(ran, "postinstall script should report executed: ran={ran}");
}
@@ -326,7 +326,7 @@ fn missing_manifest_returns_false() {
optional: false,
};
let ran = run_postinstall_hooks::<SilentReporter>(opts).expect("missing manifest is OK");
let ran = run_postinstall_hooks::<SilentReporter>(&opts).expect("missing manifest is OK");
assert!(!ran, "missing manifest must report no scripts ran: ran={ran}");
}
@@ -399,7 +399,7 @@ fn child_sees_stamped_npm_package_and_no_leaked_npm_config() {
optional: false,
};
let ran = run_postinstall_hooks::<SilentReporter>(opts).expect("postinstall");
let ran = run_postinstall_hooks::<SilentReporter>(&opts).expect("postinstall");
assert!(ran, "run_postinstall_hooks must report at least one script ran: ran={ran}");
let dump = fs::read_to_string(&dump_path).expect("read env dump");
@@ -454,7 +454,7 @@ fn malformed_manifest_propagates_error() {
optional: false,
};
let err = run_postinstall_hooks::<SilentReporter>(opts).expect_err("malformed JSON must fail");
let err = run_postinstall_hooks::<SilentReporter>(&opts).expect_err("malformed JSON must fail");
eprintln!("ERR: {err}");
assert!(
matches!(

View File

@@ -127,8 +127,8 @@ pub fn build_env(
}
/// Keep PATH (handled by the caller) and everything that does not
/// start with `npm_`; drop NODE / TMPDIR / INIT_CWD /
/// PNPM_SCRIPT_SRC_DIR because we re-derive them.
/// start with `npm_`; drop NODE / TMPDIR / `INIT_CWD` /
/// `PNPM_SCRIPT_SRC_DIR` because we re-derive them.
///
/// On Windows the comparison is case-insensitive because Rust's
/// `Command::env` treats env keys case-insensitively on that

View File

@@ -171,7 +171,7 @@ fn make_env_tmpdir_gating_mirrors_unsafe_perm() {
}
/// `extra_env` is applied AFTER the lifecycle-area writes, so it can
/// override INIT_CWD etc. — matches index.js:88-92's `Object.entries(opts.extraEnv)`
/// override `INIT_CWD` etc. — matches index.js:88-92's `Object.entries(opts.extraEnv)`
/// loop order. But `npm_lifecycle_script` is stamped *after* extraEnv
/// (set in lifecycle_ at index.js:125), so the caller can never
/// clobber it.

View File

@@ -86,7 +86,7 @@ pub struct RunScript<'a> {
/// Returns the script's [`ExitStatus`] so the caller can propagate its
/// exit code, matching pnpm's behavior where a failing script sets the
/// process exit code.
pub fn run_script(opts: RunScript<'_>) -> Result<ExitStatus, RunScriptError> {
pub fn run_script(opts: &RunScript<'_>) -> Result<ExitStatus, RunScriptError> {
let command = build_command(opts.script, opts.args);
let shell =

View File

@@ -40,7 +40,7 @@ fn manifest() -> serde_json::Value {
fn run(pkg_root: &Path, stage: &str, script: &str, args: &[String]) -> std::process::ExitStatus {
let extra_env = HashMap::new();
run_script(RunScript {
run_script(&RunScript {
manifest: &manifest(),
stage,
script,

View File

@@ -84,8 +84,7 @@ pub fn select_shell(
if is_windows {
let comspec = env::var_os("ComSpec")
.or_else(|| env::var_os("COMSPEC"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("cmd"));
.map_or_else(|| PathBuf::from("cmd"), PathBuf::from);
return Ok(SelectedShell {
program: comspec,
args: vec![OsString::from("/d"), OsString::from("/s"), OsString::from("/c")],

View File

@@ -23,7 +23,7 @@ fn posix_default_is_sh_minus_c() {
/// Default on Windows: `cmd /d /s /c` with verbatim args. When
/// `ComSpec` / `COMSPEC` is set the env var wins; otherwise the
/// literal string `cmd` is used. We do not assert the program path
/// here because the runner env may or may not have ComSpec set —
/// here because the runner env may or may not have `ComSpec` set —
/// the args + verbatim flag are the load-bearing part.
#[test]
fn windows_default_uses_cmd_with_d_s_c_and_verbatim_args() {

View File

@@ -70,12 +70,11 @@ pub fn replace_workspace_protocol(
if let Some(parsed) = parse_version_alias_spec(rest) {
let modules_dir_owned: PathBuf;
let modules_dir = match modules_dir {
Some(path) => path,
None => {
modules_dir_owned = dir.join("node_modules");
&modules_dir_owned
}
let modules_dir = if let Some(path) = modules_dir {
path
} else {
modules_dir_owned = dir.join("node_modules");
&modules_dir_owned
};
let manifest = read_and_check_manifest(dep_name, &modules_dir.join(dep_name))?;
let semver_range_token = match parsed.sentinel {
@@ -146,12 +145,11 @@ pub fn replace_workspace_protocol_peer_dependency(
}
let modules_dir_owned: PathBuf;
let modules_dir = match modules_dir {
Some(path) => path,
None => {
modules_dir_owned = dir.join("node_modules");
&modules_dir_owned
}
let modules_dir = if let Some(path) = modules_dir {
path
} else {
modules_dir_owned = dir.join("node_modules");
&modules_dir_owned
};
let manifest = read_and_check_manifest(dep_name, &modules_dir.join(dep_name))?;
let token = if matched.range_group == "*" { "" } else { matched.range_group };
@@ -289,7 +287,7 @@ fn find_workspace_peer_segment(spec: &str) -> Option<WorkspacePeerSegment<'_>> {
/// zero when no comparator is present.
fn parse_peer_range_comparator(bytes: &[u8], pos: usize) -> usize {
match (bytes.get(pos), bytes.get(pos + 1)) {
(Some(b'>'), Some(b'=')) | (Some(b'<'), Some(b'=')) => 2,
(Some(b'>' | b'<'), Some(b'=')) => 2,
(Some(b'>' | b'<' | b'^' | b'~' | b'*'), _) => 1,
_ => 0,
}

View File

@@ -52,7 +52,7 @@ where
for _ in 0..32 {
match op() {
Ok(value) => return Ok(value),
Err(error) if matches!(error.raw_os_error(), Some(EMFILE) | Some(ENFILE)) => {
Err(error) if matches!(error.raw_os_error(), Some(EMFILE | ENFILE)) => {
std::thread::sleep(backoff);
backoff = (backoff * 2).min(Duration::from_millis(200));
}
@@ -171,7 +171,7 @@ pub fn ensure_file(
// See the "Process-local per-path mutex" bullet above and
// [`cas_write_lock`] for the rationale.
let lock = cas_write_lock(file_path);
let _guard = lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let _guard = lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut options = OpenOptions::new();
options.write(true).create_new(true);
@@ -452,7 +452,7 @@ fn write_atomic(
/// Total budget for retrying a rename that keeps hitting transient
/// errors. Matches pnpm's `rename-overwrite` retry window.
const RENAME_RETRY_BUDGET: Duration = Duration::from_secs(60);
const RENAME_RETRY_BUDGET: Duration = Duration::from_mins(1);
/// Cap on per-iteration sleep — pnpm grows the backoff by 10 ms each
/// loop and stops growing at 100 ms.
@@ -509,7 +509,7 @@ fn rename_with_retry(src: &Path, dst: &Path) -> io::Result<()> {
///
/// On Unix, `rename` returning `EACCES`/`EPERM` is essentially
/// always a permanent permission issue (non-writable directory,
/// sticky-bit conflict, AppArmor deny) — retrying for 60 s just
/// sticky-bit conflict, `AppArmor` deny) — retrying for 60 s just
/// stretches out the failure. `EBUSY` on Unix also tends to be
/// permanent (mount-point conflicts). So on non-Windows the
/// classifier is disabled and any `rename` error propagates

View File

@@ -12,6 +12,7 @@ pub const EXEC_MODE: u32 = 0b111_101_101;
/// `-exec` suffix or has its on-disk mode flipped executable. Tarballs
/// from npm frequently ship scripts as `0o744` (user exec only) or
/// `0o755` (all); both must be treated as executable for pnpm-interop.
#[must_use]
pub fn is_executable(mode: u32) -> bool {
mode & EXEC_MASK != 0
}

View File

@@ -10,6 +10,7 @@ use crate::lexical_normalize;
/// Filesystem-free: both paths are lexically normalised
/// (`.` / `..` collapsed) before the prefix check, so callers can
/// run this against targets that don't exist yet.
#[must_use]
pub fn is_subdir(parent: &Path, child: &Path) -> bool {
let parent_norm = lexical_normalize(parent);
let child_norm = lexical_normalize(child);

View File

@@ -21,6 +21,7 @@ use std::path::{Component, Path, PathBuf};
///
/// Filesystem-free: callers run this against paths whose targets may
/// not exist yet, where [`std::fs::canonicalize`] cannot help.
#[must_use]
pub fn lexical_normalize(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {

View File

@@ -214,55 +214,52 @@ fn force_symlink_inner(
}
// Path exists. Is it already a symlink pointing where we want?
match read_symlink_dir(link) {
Ok(existing) => {
if existing_symlink_up_to_date(target, link, &existing) {
return Ok(ForceSymlinkOutcome { reused: true, warning: None });
}
// Stale link — unlink and retry. Ignore `NotFound` in
// case a parallel installer beat us to the unlink.
match remove_symlink_dir(link) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
force_symlink_inner(target, link, rename_tried)
if let Ok(existing) = read_symlink_dir(link) {
if existing_symlink_up_to_date(target, link, &existing) {
return Ok(ForceSymlinkOutcome { reused: true, warning: None });
}
Err(_) => {
// `link` is occupied by a regular file or directory.
// Move it out of the way, then retry. On the second
// attempt (`rename_tried`) drop down to a plain unlink
// — pnpm carries the same fallback for an intermittent
// macOS bug, see
// <https://github.com/pnpm/pnpm/issues/5909#issuecomment-1400066890>.
let parent = link.parent().unwrap_or_else(|| Path::new(""));
let basename = link.file_name().unwrap_or_default().to_string_lossy().into_owned();
let warning = if rename_tried {
remove_occupant(link)?;
format!(
"Symlink wanted name was occupied by directory or file. \
Old entity removed: {parent:?}{sep}{basename}",
sep = std::path::MAIN_SEPARATOR,
)
} else {
let ignore_name = format!(".ignored_{basename}");
let ignore_path = parent.join(&ignore_name);
if let Err(rename_err) = rename_overwrite(link, &ignore_path) {
if rename_err.kind() == io::ErrorKind::NotFound {
return Err(initial_err);
}
return Err(rename_err);
// Stale link — unlink and retry. Ignore `NotFound` in
// case a parallel installer beat us to the unlink.
match remove_symlink_dir(link) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
force_symlink_inner(target, link, rename_tried)
} else {
// `link` is occupied by a regular file or directory.
// Move it out of the way, then retry. On the second
// attempt (`rename_tried`) drop down to a plain unlink
// — pnpm carries the same fallback for an intermittent
// macOS bug, see
// <https://github.com/pnpm/pnpm/issues/5909#issuecomment-1400066890>.
let parent = link.parent().unwrap_or_else(|| Path::new(""));
let basename = link.file_name().unwrap_or_default().to_string_lossy().into_owned();
let warning = if rename_tried {
remove_occupant(link)?;
format!(
"Symlink wanted name was occupied by directory or file. \
Old entity removed: {parent:?}{sep}{basename}",
sep = std::path::MAIN_SEPARATOR,
)
} else {
let ignore_name = format!(".ignored_{basename}");
let ignore_path = parent.join(&ignore_name);
if let Err(rename_err) = rename_overwrite(link, &ignore_path) {
if rename_err.kind() == io::ErrorKind::NotFound {
return Err(initial_err);
}
format!(
"Symlink wanted name was occupied by directory or file. \
Old entity moved: {parent:?}{sep}{basename} => {ignore_name}",
sep = std::path::MAIN_SEPARATOR,
)
};
let mut outcome = force_symlink_inner(target, link, true)?;
outcome.warning = Some(warning);
Ok(outcome)
}
return Err(rename_err);
}
format!(
"Symlink wanted name was occupied by directory or file. \
Old entity moved: {parent:?}{sep}{basename} => {ignore_name}",
sep = std::path::MAIN_SEPARATOR,
)
};
let mut outcome = force_symlink_inner(target, link, true)?;
outcome.warning = Some(warning);
Ok(outcome)
}
}

View File

@@ -96,7 +96,7 @@ pub struct GitFetchOutput {
pub built: bool,
}
impl<'a> GitFetcher<'a> {
impl GitFetcher<'_> {
/// Run the fetcher. Blocks under
/// [`tokio::task::block_in_place`] for the git CLI invocations and
/// the lifecycle-script-running prepare step. Returns the CAS file
@@ -223,7 +223,7 @@ impl<'a> GitFetcher<'a> {
/// We do this via the source chain instead of mutating the message
/// (no JS-style `err.message = ...` available), so the wrapped error
/// shows up in `miette`'s rendered chain as "Failed to prepare git-
/// hosted package ... → Failed to prepare package → ERR_PNPM_PREPARE_PACKAGE".
/// hosted package ... → Failed to prepare package → `ERR_PNPM_PREPARE_PACKAGE`".
fn wrap_prepare_error(_repo: &str, err: PreparePackageError) -> GitFetcherError {
// For the MVP we preserve `err` as the source; the install log
// line at the dispatcher level already includes the repo URL via

View File

@@ -34,8 +34,7 @@ fn make_bare_repo_with_prepare_script(tmp: &Path, prepare_script: &str) -> (Path
// stays self-contained even without verdaccio / a mock registry.
// The prepare script is plumbed straight in.
let manifest = format!(
r#"{{"name":"x","version":"1.0.0","main":"index.js","scripts":{{"prepare":{prepare:?}}}}}"#,
prepare = prepare_script,
r#"{{"name":"x","version":"1.0.0","main":"index.js","scripts":{{"prepare":{prepare_script:?}}}}}"#,
);
fs::write(work.join("package.json"), manifest).unwrap();
fs::write(work.join("index.js"), "module.exports = 'src';\n").unwrap();

View File

@@ -26,6 +26,7 @@ pub enum PreferredPm {
impl PreferredPm {
/// Binary name to invoke (also the prefix of the synthesized
/// script name written into the manifest).
#[must_use]
pub fn name(self) -> &'static str {
match self {
PreferredPm::Pnpm => "pnpm",
@@ -45,6 +46,7 @@ impl PreferredPm {
/// the most specific shape and the most likely to be set by the dep's
/// author), then yarn, npm, bun. Each check is a single `path.exists`,
/// so even worst-case the sniff is four `stat()` calls.
#[must_use]
pub fn detect_preferred_pm(dir: &Path) -> PreferredPm {
if dir.join("pnpm-lock.yaml").exists() {
return PreferredPm::Pnpm;

View File

@@ -88,7 +88,9 @@ pub fn prepare_package<Reporter: self::Reporter>(
return Ok(PreparedPackage { pkg_dir, should_be_built: false });
};
let scripts = manifest.get("scripts").and_then(Value::as_object);
if scripts.is_none_or(|s| s.is_empty()) || !package_should_be_built(&manifest, &pkg_dir) {
if scripts.is_none_or(serde_json::Map::is_empty)
|| !package_should_be_built(&manifest, &pkg_dir)
{
return Ok(PreparedPackage { pkg_dir, should_be_built: false });
}
if opts.ignore_scripts {
@@ -204,11 +206,8 @@ fn safe_join_path(root: &Path, sub: Option<&str>) -> Result<PathBuf, PreparePack
let sub = sub.unwrap_or("");
let joined = if sub.is_empty() { root.to_path_buf() } else { root.join(sub) };
let canonical_root = root.canonicalize().map_err(PreparePackageError::Io)?;
let canonical_joined = match joined.canonicalize() {
Ok(p) => p,
Err(_) => {
return Err(PreparePackageError::InvalidPath { path: sub.to_string() });
}
let Ok(canonical_joined) = joined.canonicalize() else {
return Err(PreparePackageError::InvalidPath { path: sub.to_string() });
};
if !canonical_joined.starts_with(&canonical_root) {
return Err(PreparePackageError::InvalidPath { path: sub.to_string() });

View File

@@ -62,7 +62,7 @@ fn opts_allow_registry_artifacts_only<'a>() -> PreparePackageOptions<'a> {
}
}
fn opts_allow_dep_path<'a>(dep_path: &'a str) -> PreparePackageOptions<'a> {
fn opts_allow_dep_path(dep_path: &str) -> PreparePackageOptions<'_> {
static EMPTY_BIN_PATHS: &[std::path::PathBuf] = &[];
PreparePackageOptions {
allow_build: Box::new(move |actual_dep_path| actual_dep_path == dep_path),

View File

@@ -17,7 +17,7 @@
//! from the tarball download and copies it to the prepared key on
//! the fast path. Pacquet's tarball download path doesn't write a
//! raw row at this key, so on the fast path we synthesize the
//! prepared row directly from the input `cas_paths` (no fs::read,
//! prepared row directly from the input `cas_paths` (no `fs::read`,
//! no re-hash). When fast-path triggers and `should_be_built` is
//! false, the synthesized row lands at the final key — matches the
//! shape upstream's "copy raw→prepared" produces. The skipped
@@ -83,7 +83,7 @@ pub struct GitHostedTarballFetcher<'a> {
pub files_index_file: &'a str,
}
impl<'a> GitHostedTarballFetcher<'a> {
impl GitHostedTarballFetcher<'_> {
/// Run the fetcher. Blocks under
/// [`tokio::task::block_in_place`] so the synchronous
/// `preparePackage` work doesn't tie up the async runtime.

View File

@@ -3,7 +3,7 @@ use pretty_assertions::assert_eq;
use std::collections::HashMap;
/// Engine-only key (no dep graph, no patch). Pure prefix path
/// for the cheapest cache lookup. Mirrors the "include_dep_graph_hash:
/// for the cheapest cache lookup. Mirrors the "`include_dep_graph_hash`:
/// false" path at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/deps/graph-hasher/src/index.ts#L36>.
#[test]
@@ -214,7 +214,7 @@ fn transitively_requires_build_self_in_built_set() {
},
);
let built: std::collections::HashSet<String> =
["builder@1.0.0".to_string()].into_iter().collect();
std::iter::once("builder@1.0.0".to_string()).collect();
let mut cache = HashMap::new();
let mut parents = std::collections::HashSet::new();
assert!(transitively_requires_build(
@@ -246,7 +246,7 @@ fn transitively_requires_build_walks_to_descendant_builder() {
},
);
let built: std::collections::HashSet<String> =
["builder@1.0.0".to_string()].into_iter().collect();
std::iter::once("builder@1.0.0".to_string()).collect();
let mut cache = HashMap::new();
let mut parents = std::collections::HashSet::new();
assert!(transitively_requires_build(
@@ -276,7 +276,7 @@ fn transitively_requires_build_returns_false_for_unrelated_tree() {
DepsGraphNode { full_pkg_id: "leaf@1.0.0:sha512-l".to_string(), children: HashMap::new() },
);
let built: std::collections::HashSet<String> =
["builder@9.9.9".to_string()].into_iter().collect();
std::iter::once("builder@9.9.9".to_string()).collect();
let mut cache = HashMap::new();
let mut parents = std::collections::HashSet::new();
assert!(!transitively_requires_build(
@@ -378,7 +378,7 @@ fn transitively_requires_build_cycle_does_not_mask_sibling_builder() {
},
);
let built: std::collections::HashSet<String> =
["builder@1.0.0".to_string()].into_iter().collect();
std::iter::once("builder@1.0.0".to_string()).collect();
let mut cache = HashMap::new();
let mut parents = std::collections::HashSet::new();
assert!(transitively_requires_build(

View File

@@ -23,6 +23,7 @@ pub use pacquet_detect_libc::{host_arch, host_platform};
/// static `std::env::consts` constants mapped through Node's
/// naming scheme. Production callers can pass `None` to get the
/// host values; tests can pin both for cache-key round-trip.
#[must_use]
pub fn engine_name(node_major: u32, platform: Option<&str>, arch: Option<&str>) -> String {
let platform = platform.unwrap_or_else(|| host_platform());
let arch = arch.unwrap_or_else(|| host_arch());
@@ -53,6 +54,7 @@ pub fn engine_name(node_major: u32, platform: Option<&str>, arch: Option<&str>)
/// Callers should fall back to either a sentinel cache key (which
/// won't match any pnpm-written entry — safe) or skip the
/// cache-read entirely when this returns `None`.
#[must_use]
pub fn detect_node_major() -> Option<u32> {
let raw = detect_node_version_raw()?;
parse_node_version_output(&raw)
@@ -68,6 +70,7 @@ pub fn detect_node_major() -> Option<u32> {
/// evaluate `engines.node` ranges. Pacquet's installability check
/// needs the full version, not just the major, because ranges like
/// `>=14.18.0` would otherwise spuriously reject `14.17.x`.
#[must_use]
pub fn detect_node_version() -> Option<String> {
let raw = detect_node_version_raw()?;
Some(raw.strip_prefix('v').unwrap_or(&raw).to_string())
@@ -111,9 +114,7 @@ pub fn host_libc() -> &'static str {
static CACHED: OnceLock<&'static str> = OnceLock::new();
CACHED.get_or_init(|| {
pacquet_detect_libc::detect()
.map(pacquet_detect_libc::Implementation::as_str)
.unwrap_or("unknown")
pacquet_detect_libc::detect().map_or("unknown", pacquet_detect_libc::Implementation::as_str)
})
}

View File

@@ -102,6 +102,7 @@ where
/// uses it for the platform-specific optional subdeps of a config
/// dependency, which are installed one level deep with no further
/// children of their own.
#[must_use]
pub fn calc_leaf_global_virtual_store_path(full_pkg_id: &str, name: &str, version: &str) -> String {
let deps_hash = hash_object(&json!({ "id": full_pkg_id, "deps": {} }));
let payload = json!({ "engine": Value::Null, "deps": deps_hash });
@@ -120,6 +121,7 @@ pub fn calc_leaf_global_virtual_store_path(full_pkg_id: &str, name: &str, versio
/// underneath it — without this, changing a subdep while keeping the
/// parent pinned would silently overwrite the previous sibling
/// symlinks.
#[must_use]
pub fn calc_global_virtual_store_path_with_subdeps(
full_pkg_id: &str,
name: &str,
@@ -144,6 +146,7 @@ pub fn calc_global_virtual_store_path_with_subdeps(
/// shared store at the same `<scope>/<name>/<version>/<hash>` depth,
/// so a single `readdir` pass per level can enumerate the store
/// without special-casing the unscoped path layout.
#[must_use]
pub fn format_global_virtual_store_path(name: &str, version: &str, hex_digest: &str) -> String {
let prefix = if name.starts_with('@') { "" } else { "@/" };
format!("{prefix}{name}/{version}/{hex_digest}")

View File

@@ -116,7 +116,7 @@ fn engine_agnostic_when_subtree_has_no_builders() {
},
);
// builtDepPaths exists but doesn't contain pure-js. Gating fires.
let built: HashSet<String> = ["someone-else@1.0.0".to_string()].into_iter().collect();
let built: HashSet<String> = std::iter::once("someone-else@1.0.0".to_string()).collect();
let mut cache_a = HashMap::new();
let mut br_a = HashMap::new();
let darwin = calc_graph_node_hash(
@@ -156,7 +156,7 @@ fn engine_included_when_self_in_built_set() {
children: HashMap::new(),
},
);
let built: HashSet<String> = ["native@1.0.0".to_string()].into_iter().collect();
let built: HashSet<String> = std::iter::once("native@1.0.0".to_string()).collect();
let mut cache_a = HashMap::new();
let mut br_a = HashMap::new();
let darwin = calc_graph_node_hash(
@@ -199,7 +199,7 @@ fn engine_included_for_ancestor_of_builder() {
children: HashMap::new(),
},
);
let built: HashSet<String> = ["native@1.0.0".to_string()].into_iter().collect();
let built: HashSet<String> = std::iter::once("native@1.0.0".to_string()).collect();
let mut cache_a = HashMap::new();
let mut br_a = HashMap::new();
let darwin = calc_graph_node_hash(

View File

@@ -16,12 +16,14 @@ use sha2::{Digest, Sha256};
/// short-circuit `hashUnknown(undefined)` returns 44 zero characters
/// regardless of options. Callers who need that semantic should
/// branch on the optional before calling.
#[must_use]
pub fn hash_object(value: &Value) -> String {
hash_object_with_encoding(value, HashEncoding::Base64, /* sort */ true)
}
/// Mirrors `hashObjectWithoutSorting` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/crypto/object-hasher/src/index.ts#L37>.
#[must_use]
pub fn hash_object_without_sorting(value: &Value, encoding: HashEncoding) -> String {
hash_object_with_encoding(value, encoding, /* sort */ false)
}
@@ -39,6 +41,7 @@ pub fn hash_object_without_sorting(value: &Value, encoding: HashEncoding) -> Str
/// practice for this caller (`packageExtensions` is always a map),
/// but we hash them anyway rather than panic — pacquet's hasher
/// already handles them.
#[must_use]
pub fn hash_object_nullable_with_prefix(value: &Value) -> Option<String> {
let is_nullish = match value {
Value::Null => true,
@@ -54,6 +57,7 @@ pub fn hash_object_nullable_with_prefix(value: &Value) -> Option<String> {
/// General form. `sort = true` sorts object keys before serialization
/// (the `unorderedObjects` option upstream); `sort = false` preserves
/// insertion order.
#[must_use]
pub fn hash_object_with_encoding(value: &Value, encoding: HashEncoding, sort: bool) -> String {
let mut bytes = Vec::new();
serialize(&mut bytes, value, sort);

View File

@@ -5,6 +5,7 @@ use std::{
use super::PnpmfileHooks;
#[must_use]
pub fn find_pnpmfile(root: &Path) -> Option<std::path::PathBuf> {
let candidates = [".pnpmfile.mjs", ".pnpmfile.cjs"];
@@ -17,6 +18,7 @@ pub fn find_pnpmfile(root: &Path) -> Option<std::path::PathBuf> {
None
}
#[must_use]
pub fn load_pnpmfile(root: &Path) -> Option<Arc<dyn PnpmfileHooks>> {
let file = find_pnpmfile(root)?;
Some(Arc::new(super::node_runtime::NodeJsHooks::new(file)))
@@ -25,6 +27,7 @@ pub fn load_pnpmfile(root: &Path) -> Option<Arc<dyn PnpmfileHooks>> {
/// Load a pnpmfile from an explicit path (used for config-dependency
/// plugin pnpmfiles, which live at
/// `node_modules/.pnpm-config/<plugin>/pnpmfile.{mjs,cjs}`).
#[must_use]
pub fn load_pnpmfile_at(file: PathBuf) -> Arc<dyn PnpmfileHooks> {
Arc::new(super::node_runtime::NodeJsHooks::new(file))
}
@@ -36,6 +39,7 @@ pub fn load_pnpmfile_at(file: PathBuf) -> Arc<dyn PnpmfileHooks> {
/// - unscoped `pnpm-plugin-*`,
/// - scoped `@pnpm/plugin-*`,
/// - scoped `@<org>/pnpm-plugin-*`.
#[must_use]
pub fn is_plugin_name(name: &str) -> bool {
if name.starts_with("pnpm-plugin-") {
return true;

View File

@@ -25,6 +25,7 @@ pub struct NodeJsHooks {
const HOOK_TIMEOUT: Duration = Duration::from_secs(30);
impl NodeJsHooks {
#[must_use]
pub fn new(file: PathBuf) -> Self {
NodeJsHooks { file, worker: OnceCell::new() }
}
@@ -46,14 +47,8 @@ impl NodeJsHooks {
logger: &crate::PreResolutionHookLogger,
) {
let file_path = self.file.to_string_lossy();
let file_path_escaped = match serde_json::to_string(&file_path) {
Ok(s) => s,
Err(_) => return,
};
let ctx_payload = match serde_json::to_string(&args) {
Ok(s) => s,
Err(_) => return,
};
let Ok(file_path_escaped) = serde_json::to_string(&file_path) else { return };
let Ok(ctx_payload) = serde_json::to_string(&args) else { return };
let (input_type, wrapper) = if file_path.ends_with(".mjs") {
(
@@ -68,7 +63,6 @@ const logger = {{
}};
await (hooks.hooks && hooks.hooks['{func}'])?.(ctx, logger);
"#,
file_path_escaped = file_path_escaped,
),
)
} else {
@@ -85,12 +79,11 @@ await (hooks.hooks && hooks.hooks['{func}'])?.(ctx, logger);
await (hooks.hooks && hooks.hooks['{func}'])?.(ctx, logger);
}})();
"#,
file_path_escaped = file_path_escaped,
),
)
};
let mut child = match Command::new("node")
let Ok(mut child) = Command::new("node")
.arg("--input-type")
.arg(input_type)
.arg("-e")
@@ -98,12 +91,9 @@ await (hooks.hooks && hooks.hooks['{func}'])?.(ctx, logger);
.kill_on_drop(true)
.stdin(std::process::Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(_) => {
(logger.warn)("pnpmfile hook failed to start".to_string());
return;
}
else {
(logger.warn)("pnpmfile hook failed to start".to_string());
return;
};
if let Some(mut stdin) = child.stdin.take()
@@ -113,17 +103,14 @@ await (hooks.hooks && hooks.hooks['{func}'])?.(ctx, logger);
return;
}
let output = match timeout(HOOK_TIMEOUT, child.wait_with_output()).await {
Ok(Ok(output)) => output,
Ok(Err(_)) | Err(_) => {
(logger.warn)("pnpmfile hook timed out or failed to execute".to_string());
return;
}
let Ok(Ok(output)) = timeout(HOOK_TIMEOUT, child.wait_with_output()).await else {
(logger.warn)("pnpmfile hook timed out or failed to execute".to_string());
return;
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
(logger.warn)(format!("pnpmfile hook failed: {}", stderr));
(logger.warn)(format!("pnpmfile hook failed: {stderr}"));
}
}
}

View File

@@ -68,7 +68,7 @@ async fn test_node_js_hooks_read_package() {
let pnpmfile_path = tmp.path().join(".pnpmfile.cjs");
std::fs::write(
&pnpmfile_path,
r#"
r"
module.exports = {
hooks: { readPackage }
}
@@ -79,7 +79,7 @@ function readPackage(pkg) {
}
return pkg;
}
"#,
",
)
.expect("write pnpmfile");
@@ -104,7 +104,7 @@ async fn test_node_js_hooks_read_package_no_match() {
let pnpmfile_path = tmp.path().join(".pnpmfile.cjs");
std::fs::write(
&pnpmfile_path,
r#"
r"
module.exports = {
hooks: { readPackage }
}
@@ -112,7 +112,7 @@ module.exports = {
function readPackage(pkg) {
return pkg;
}
"#,
",
)
.expect("write pnpmfile");
@@ -137,7 +137,7 @@ async fn test_node_js_hooks_filter_log() {
let pnpmfile_path = tmp.path().join(".pnpmfile.cjs");
std::fs::write(
&pnpmfile_path,
r#"
r"
module.exports = {
hooks: { filterLog }
}
@@ -145,7 +145,7 @@ module.exports = {
function filterLog(log) {
return log.level === 'debug' || log.level === 'error';
}
"#,
",
)
.expect("write pnpmfile");
@@ -180,7 +180,7 @@ async fn test_node_js_hooks_read_package_mjs() {
let pnpmfile_path = tmp.path().join(".pnpmfile.mjs");
std::fs::write(
&pnpmfile_path,
r#"
r"
export const hooks = { readPackage };
function readPackage(pkg) {
@@ -189,7 +189,7 @@ function readPackage(pkg) {
}
return pkg;
}
"#,
",
)
.expect("write pnpmfile");
@@ -219,7 +219,7 @@ async fn test_node_js_hooks_pre_resolution() {
let pnpmfile_path = tmp.path().join(".pnpmfile.cjs");
std::fs::write(
&pnpmfile_path,
r#"
r"
module.exports = {
hooks: { preResolution }
}
@@ -231,7 +231,7 @@ function preResolution(ctx, logger) {
if (typeof logger.info !== 'function') throw new Error('missing logger.info');
if (typeof logger.warn !== 'function') throw new Error('missing logger.warn');
}
"#,
",
)
.expect("write pnpmfile");
@@ -265,7 +265,7 @@ async fn test_node_js_hooks_pre_resolution_mjs() {
let pnpmfile_path = tmp.path().join(".pnpmfile.mjs");
std::fs::write(
&pnpmfile_path,
r#"
r"
export const hooks = { preResolution };
function preResolution(ctx, logger) {
@@ -275,7 +275,7 @@ function preResolution(ctx, logger) {
if (typeof logger.info !== 'function') throw new Error('missing logger.info');
if (typeof logger.warn !== 'function') throw new Error('missing logger.warn');
}
"#,
",
)
.expect("write pnpmfile");
@@ -367,13 +367,13 @@ async fn read_package_normalizes_missing_dependency_fields() {
// directly, relying on pnpm's normalization that defaults each to `{}`
// before the hook runs.
let (hooks, _tmp) = cjs_hooks(
r#"module.exports = { hooks: { readPackage (pkg) {
r"module.exports = { hooks: { readPackage (pkg) {
pkg.dependencies['is-positive'] = '*';
pkg.optionalDependencies['is-negative'] = '*';
pkg.peerDependencies['is-negative'] = '*';
pkg.devDependencies['is-positive'] = '*';
return pkg;
} } }"#,
} } }",
);
let updated = hooks
@@ -407,10 +407,10 @@ async fn read_package_fails_when_pnpmfile_requires_missing_module() {
#[tokio::test]
async fn worker_multiplexes_concurrent_read_package_calls() {
let (hooks, _tmp) = cjs_hooks(
r#"module.exports = { hooks: { readPackage (pkg) {
r"module.exports = { hooks: { readPackage (pkg) {
pkg.dependencies['self'] = pkg.name;
return pkg;
} } }"#,
} } }",
);
let hooks = Arc::new(hooks);
@@ -441,10 +441,10 @@ async fn worker_multiplexes_concurrent_read_package_calls() {
#[tokio::test]
async fn worker_forwards_read_package_context_log() {
let (hooks, _tmp) = cjs_hooks(
r#"module.exports = { hooks: { readPackage (pkg, context) {
r"module.exports = { hooks: { readPackage (pkg, context) {
context.log('hello from ' + pkg.name);
return pkg;
} } }"#,
} } }",
);
let logs = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
@@ -514,10 +514,10 @@ async fn update_config_applies_hook_result() {
let pnpmfile_path = tmp.path().join("pnpmfile.cjs");
std::fs::write(
&pnpmfile_path,
r#"module.exports = { hooks: { updateConfig (config) {
r"module.exports = { hooks: { updateConfig (config) {
config.catalogs = { default: { foo: '1.0.0' } };
return config;
} } }"#,
} } }",
)
.expect("write pnpmfile");

View File

@@ -37,6 +37,7 @@ pub use version_selector_type::get_version_selector_type;
/// Pass `snapshots = None` when the wanted lockfile is absent (e.g.
/// the `install-without-lockfile` path); only manifest-derived entries
/// are produced.
#[must_use]
pub fn get_preferred_versions_from_lockfile_and_manifests(
snapshots: Option<&HashMap<PackageKey, SnapshotEntry>>,
manifests: &[&PackageManifest],

View File

@@ -10,6 +10,10 @@ use pretty_assertions::assert_eq;
use super::get_preferred_versions_from_lockfile_and_manifests;
#[expect(
clippy::needless_pass_by_value,
reason = "test helper called from multiple sites with owned literals; by-value keeps the call sites clean"
)]
fn fake_manifest(deps_json: serde_json::Value) -> (tempfile::TempDir, PackageManifest) {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("package.json");

View File

@@ -12,6 +12,7 @@ use pacquet_resolving_resolver_base::VersionSelectorType;
/// Classify a manifest spec the same way upstream's loose
/// `getVersionSelectorType` does.
#[must_use]
pub fn get_version_selector_type(spec: &str) -> Option<VersionSelectorType> {
if spec.parse::<Version>().is_ok() {
return Some(VersionSelectorType::Version);

View File

@@ -154,13 +154,10 @@ pub fn try_lockfile_verification_cache(
verifiers: &[Arc<dyn ResolutionVerifier>],
mut hash_lockfile: impl FnMut() -> String,
) -> CacheLookupResult {
let indexes = match read_cache(cache_dir) {
Ok(indexes) => indexes,
Err(_) => {
// A corrupt cache file should never block the install;
// fall through to verification so the gate still runs.
return CacheLookupResult::default();
}
let Ok(indexes) = read_cache(cache_dir) else {
// A corrupt cache file should never block the install;
// fall through to verification so the gate still runs.
return CacheLookupResult::default();
};
let Some(stat) = stat_lockfile(lockfile_path) else {
@@ -237,10 +234,7 @@ pub fn record_verification(
mut hash_lockfile: impl FnMut() -> String,
precomputed: CachePrecomputed,
) {
let stat = match precomputed.stat.or_else(|| stat_lockfile(lockfile_path)) {
Some(stat) => stat,
None => return,
};
let Some(stat) = precomputed.stat.or_else(|| stat_lockfile(lockfile_path)) else { return };
let hash = precomputed.hash.unwrap_or_else(&mut hash_lockfile);
let record = CacheRecord {
lockfile: CacheLockfile {
@@ -307,8 +301,7 @@ fn stat_lockfile(lockfile_path: &Path) -> Option<LockfileStat> {
.modified()
.ok()
.and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|duration| duration.as_nanos().to_string())
.unwrap_or_else(|| "0".to_string());
.map_or_else(|| "0".to_string(), |duration| duration.as_nanos().to_string());
let inode = inode_of(&metadata);
Some(LockfileStat { size, mtime_ns, inode })
}
@@ -376,10 +369,7 @@ fn maybe_compact_cache(cache_dir: &Path) {
if size <= COMPACT_TRIGGER_BYTES {
return;
}
let contents = match fs::read_to_string(&cache_file_path) {
Ok(contents) => contents,
Err(_) => return,
};
let Ok(contents) = fs::read_to_string(&cache_file_path) else { return };
// Walk reverse so the newest record per (path, hash) wins, drop
// older duplicates, then trim to MAX_CACHE_ENTRIES.

Some files were not shown because too many files have changed in this diff Show More