feat(executor): swallow optional-dep build failures and report via pnpm:skipped-optional-dependency (#397) (#419)

## Summary

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

Three commits:

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

View File

@@ -1,5 +1,6 @@
---
source: crates/cli/tests/install.rs
assertion_line: 48
expression: "(workspace_folders, store_files)"
---
(
@@ -23,9 +24,10 @@ expression: "(workspace_folders, store_files)"
],
[
"v11/files/02/f9d952341eade675d0a8f5da14d4277fded62f937242292c2cf5f1bb0eda101b5f803098d64e2a138f4bfe4cdf326cfaf8f6b4015476ab86d25df03ddec731-exec",
"v11/files/68/b837b4fd7982b081521e51b14a167ccdb272b3c499202449d07d48d78a2fa7bb8652d3a894bb06ac6e3995e5126ee3470f73b5775584940751fce8be468405",
"v11/files/93/e31fcd9144a604f87d04fa8fb4a058202c579df17fd2b21301e17ac0ae4f3e2bae90ced7ae71b71218bd4a789e355ef7b8bd8899cdb785250071b229503b2e",
"v11/files/2f/b86ecd88e34f18360bf865231219f7898cf713f63ad6d099f72b821f47cded6b92ece8d468775219a2950c50451d3f3e2f3920d15b4a3c372098f2daff864b",
"v11/files/65/d9491d753c9cff7b6d93461c83b8542a36db429a4cba4c712f841ae657f0d82b36d94081816d5a92ce761146c64b036ea1d89d53890c86b115d457bb98fa6b",
"v11/files/bf/0c878f2a4f2ec6f231bb2d414547efb15eb22a673815cba36acfd89633cd4c8affcbdc9378e90ad4d8fc654de921d1f67444be94e6a2f81bda9330be60be93-exec",
"v11/files/fe/06f6dff7c043b7ea30547ff55a8fd06fecc30fe143e3acf09cdd30def35d660f8cafa74ff28287748f661bbd767987eed52b440c77942eb0fb9aa6f44cfb37",
"v11/index.db",
],
)

View File

@@ -1,11 +1,13 @@
---
source: crates/cli/tests/install.rs
assertion_line: 108
expression: store_files
---
[
"v11/files/02/f9d952341eade675d0a8f5da14d4277fded62f937242292c2cf5f1bb0eda101b5f803098d64e2a138f4bfe4cdf326cfaf8f6b4015476ab86d25df03ddec731-exec",
"v11/files/68/b837b4fd7982b081521e51b14a167ccdb272b3c499202449d07d48d78a2fa7bb8652d3a894bb06ac6e3995e5126ee3470f73b5775584940751fce8be468405",
"v11/files/93/e31fcd9144a604f87d04fa8fb4a058202c579df17fd2b21301e17ac0ae4f3e2bae90ced7ae71b71218bd4a789e355ef7b8bd8899cdb785250071b229503b2e",
"v11/files/2f/b86ecd88e34f18360bf865231219f7898cf713f63ad6d099f72b821f47cded6b92ece8d468775219a2950c50451d3f3e2f3920d15b4a3c372098f2daff864b",
"v11/files/65/d9491d753c9cff7b6d93461c83b8542a36db429a4cba4c712f841ae657f0d82b36d94081816d5a92ce761146c64b036ea1d89d53890c86b115d457bb98fa6b",
"v11/files/bf/0c878f2a4f2ec6f231bb2d414547efb15eb22a673815cba36acfd89633cd4c8affcbdc9378e90ad4d8fc654de921d1f67444be94e6a2f81bda9330be60be93-exec",
"v11/files/fe/06f6dff7c043b7ea30547ff55a8fd06fecc30fe143e3acf09cdd30def35d660f8cafa74ff28287748f661bbd767987eed52b440c77942eb0fb9aa6f44cfb37",
"v11/index.db",
]

View File

@@ -1,22 +1,31 @@
---
source: crates/cli/tests/install.rs
assertion_line: 133
expression: index_file_contents
---
"sha512-EDvJzSLCEMnUnrrIGdV+IUWP/8w+nUjOuay2u0KW20Mnfnm3lyrdquQ+gKPNAOUfAsKL1y21np1nQN0PyP+57A==\t@pnpm.e2e/hello-world-js-bin@1.0.0":
index.js:
digest: bf0c878f2a4f2ec6f231bb2d414547efb15eb22a673815cba36acfd89633cd4c8affcbdc9378e90ad4d8fc654de921d1f67444be94e6a2f81bda9330be60be93
mode: 493
size: 48
package.json:
digest: 68b837b4fd7982b081521e51b14a167ccdb272b3c499202449d07d48d78a2fa7bb8652d3a894bb06ac6e3995e5126ee3470f73b5775584940751fce8be468405
"sha512-G3aEYkPR7vdClnAaLekaPPXJdy1fDE7I0j8HagQDeJ9x3hX4iACybYTL7yZr6i69UsmK/Ho7xyxQII0OARxawA==\t@pnpm.e2e/hello-world-js-bin-parent@1.0.0":
LICENSE:
digest: 65d9491d753c9cff7b6d93461c83b8542a36db429a4cba4c712f841ae657f0d82b36d94081816d5a92ce761146c64b036ea1d89d53890c86b115d457bb98fa6b
mode: 420
size: 379
"sha512-fEzGMnAjelrr4l9WMg5NRF6Tym3HhLBbdUNe3C+EQOrWVx/jVSMlsefokijv2mIDgCLm7Fv6FzUliQBAdsq91Q==\t@pnpm.e2e/hello-world-js-bin-parent@1.0.0":
size: 1066
index.js:
digest: 02f9d952341eade675d0a8f5da14d4277fded62f937242292c2cf5f1bb0eda101b5f803098d64e2a138f4bfe4cdf326cfaf8f6b4015476ab86d25df03ddec731
mode: 493
size: 79
package.json:
digest: 93e31fcd9144a604f87d04fa8fb4a058202c579df17fd2b21301e17ac0ae4f3e2bae90ced7ae71b71218bd4a789e355ef7b8bd8899cdb785250071b229503b2e
digest: fe06f6dff7c043b7ea30547ff55a8fd06fecc30fe143e3acf09cdd30def35d660f8cafa74ff28287748f661bbd767987eed52b440c77942eb0fb9aa6f44cfb37
mode: 420
size: 537
size: 536
"sha512-bFWeEhV2pspARSwVwLnMfQStl22sXBuexcX+OvSlMw5hKw3tpyqeDCEA04qhdo1XCX1Hag0pBuI6H9Ri0ctzOw==\t@pnpm.e2e/hello-world-js-bin@1.0.0":
LICENSE:
digest: 65d9491d753c9cff7b6d93461c83b8542a36db429a4cba4c712f841ae657f0d82b36d94081816d5a92ce761146c64b036ea1d89d53890c86b115d457bb98fa6b
mode: 420
size: 1066
index.js:
digest: bf0c878f2a4f2ec6f231bb2d414547efb15eb22a673815cba36acfd89633cd4c8affcbdc9378e90ad4d8fc654de921d1f67444be94e6a2f81bda9330be60be93
mode: 493
size: 48
package.json:
digest: 2fb86ecd88e34f18360bf865231219f7898cf713f63ad6d099f72b821f47cded6b92ece8d468775219a2950c50451d3f3e2f3920d15b4a3c372098f2daff864b
mode: 420
size: 410

View File

@@ -111,6 +111,16 @@ pub struct RunPostinstallHooks<'a> {
/// `/usr/local/bin/bash`). `None` means use the platform default
/// (`sh -c` on POSIX, `cmd /d /s /c` on Windows).
pub script_shell: Option<&'a Path>,
/// Whether the dep is reachable only through optional edges
/// (`snapshots[<key>].optional` in the v9 lockfile). Stamped
/// into the `pnpm:lifecycle` `Script` and `Exit` events so
/// downstream reporters can dispatch correctly, mirroring
/// upstream's `lifecycleLogger.debug({ optional, … })` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/src/runLifecycleHook.ts#L102>.
/// Does NOT affect failure handling — `BuildModules` consults the
/// same flag independently to decide whether to swallow a build
/// failure (see #397 item 6).
pub optional: bool,
}
/// Run the preinstall, install, and postinstall lifecycle scripts for
@@ -208,7 +218,7 @@ fn run_lifecycle_hook<R: Reporter>(
level: LogLevel::Debug,
message: LifecycleMessage::Script {
dep_path: opts.dep_path.to_string(),
optional: false,
optional: opts.optional,
script: script.to_string(),
stage: stage.to_string(),
wd: pkg_root_str.clone(),
@@ -333,7 +343,7 @@ fn run_lifecycle_hook<R: Reporter>(
message: LifecycleMessage::Exit {
dep_path: opts.dep_path.to_string(),
exit_code: status.code().unwrap_or(-1),
optional: false,
optional: opts.optional,
stage: stage.to_string(),
wd: pkg_root_str,
},

View File

@@ -56,6 +56,7 @@ fn lifecycle_emits_script_stdio_and_exit_in_order() {
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
optional: false,
};
let ran = run_postinstall_hooks::<RecordingReporter>(opts).expect("postinstall");
@@ -118,6 +119,84 @@ fn lifecycle_emits_script_stdio_and_exit_in_order() {
);
}
/// `RunPostinstallHooks.optional` is stamped into both the `Script`
/// and `Exit` `pnpm:lifecycle` events, matching upstream's
/// `lifecycleLogger.debug({ optional, … })` shape at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/src/runLifecycleHook.ts#L102>
/// and `:165`. The two-bit truth on the wire lets the default
/// reporter dispatch (e.g. quieting optional-dep noise) the same
/// way it does against pnpm.
#[cfg(unix)]
#[test]
fn lifecycle_events_carry_optional_flag() {
static EVENTS: Mutex<Vec<LogEvent>> = Mutex::new(Vec::new());
EVENTS.lock().expect("lock").clear();
struct RecordingReporter;
impl Reporter for RecordingReporter {
fn emit(event: &LogEvent) {
EVENTS.lock().expect("lock").push(event.clone());
}
}
let dir = tempdir().expect("create temp dir");
let pkg_root = dir.path();
let manifest = serde_json::json!({
"name": "opt",
"version": "1.0.0",
"scripts": { "postinstall": "true" },
});
fs::write(pkg_root.join("package.json"), manifest.to_string()).expect("write manifest");
let extra_env: HashMap<String, String> = HashMap::new();
let extra_bin_paths: Vec<std::path::PathBuf> = vec![];
let opts = RunPostinstallHooks {
dep_path: "/opt@1.0.0",
pkg_root,
root_modules_dir: pkg_root,
init_cwd: pkg_root,
extra_bin_paths: &extra_bin_paths,
extra_env: &extra_env,
node_execpath: None,
npm_execpath: None,
node_gyp_path: None,
user_agent: None,
unsafe_perm: true,
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
optional: true,
};
run_postinstall_hooks::<RecordingReporter>(opts).expect("postinstall");
let captured = EVENTS.lock().expect("lock").clone();
let lifecycle_events: Vec<_> = captured
.iter()
.filter_map(|e| match e {
LogEvent::Lifecycle(l) => Some(&l.message),
_ => None,
})
.collect();
dbg!(&lifecycle_events);
let script_optional = lifecycle_events
.iter()
.find_map(|m| match m {
LifecycleMessage::Script { optional, .. } => Some(*optional),
_ => None,
})
.expect("must emit a Script event");
assert!(script_optional, "Script event must carry optional=true");
let exit_optional = lifecycle_events
.iter()
.find_map(|m| match m {
LifecycleMessage::Exit { optional, .. } => Some(*optional),
_ => None,
})
.expect("must emit an Exit event");
assert!(exit_optional, "Exit event must carry optional=true");
}
/// Failing scripts emit a Script event, the captured stdio, and an Exit
/// event with the resolved non-zero exit code, then return a
/// [`LifecycleScriptError::ScriptFailed`].
@@ -159,6 +238,7 @@ fn lifecycle_emits_exit_with_nonzero_code_on_failure() {
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
optional: false,
};
let err = run_postinstall_hooks::<RecordingReporter>(opts).expect_err("script must fail");
@@ -208,6 +288,7 @@ fn lifecycle_runs_under_silent_reporter() {
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
optional: false,
};
let ran = run_postinstall_hooks::<SilentReporter>(opts).expect("postinstall");
@@ -241,6 +322,7 @@ fn missing_manifest_returns_false() {
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
optional: false,
};
let ran = run_postinstall_hooks::<SilentReporter>(opts).expect("missing manifest is OK");
@@ -313,6 +395,7 @@ fn child_sees_stamped_npm_package_and_no_leaked_npm_config() {
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
optional: false,
};
let ran = run_postinstall_hooks::<SilentReporter>(opts).expect("postinstall");
@@ -367,6 +450,7 @@ fn malformed_manifest_propagates_error() {
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
optional: false,
};
let err = run_postinstall_hooks::<SilentReporter>(opts).expect_err("malformed JSON must fail");

View File

@@ -2,6 +2,9 @@ use crate::{PkgName, SnapshotDepRef};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(test)]
mod tests;
/// Per-instance snapshot information stored in the v9 `snapshots:` map.
///
/// An entry describes the wiring of one concrete installation of a package:
@@ -24,4 +27,23 @@ pub struct SnapshotEntry {
pub transitive_peer_dependencies: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patched: Option<bool>,
/// `true` when every path from any importer to this package
/// goes through an `optionalDependencies` edge — folded by
/// pnpm's resolver at install time and written verbatim into
/// `snapshots[<key>].optional`. Pacquet trusts the precomputed
/// flag rather than re-deriving from the importer graph,
/// matching upstream's `lockfileToDepGraph` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/deps/graph-builder/src/lockfileToDepGraph.ts#L315>.
///
/// `BuildModules` consults this flag to decide whether a failed
/// build should be swallowed and reported via
/// `pnpm:skipped-optional-dependency` (mirrors
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L218-L240>).
#[serde(default, skip_serializing_if = "is_false")]
pub optional: bool,
}
fn is_false(value: &bool) -> bool {
!*value
}

View File

@@ -0,0 +1,44 @@
use super::SnapshotEntry;
use crate::serialize_yaml;
use text_block_macros::text_block;
/// `optional: true` round-trips through YAML deserialize → serialize.
/// Source of truth is pnpm's v9 lockfile spec at
/// <https://github.com/pnpm/spec/blob/834f2815cc/lockfile/9.0.md>;
/// see `snapshots[<key>].optional` in the upstream type at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/lockfile/types/src/index.ts#L34-L39>.
#[test]
fn optional_true_round_trips() {
let yaml = text_block! {
"dependencies:"
" foo: 1.2.3"
"optional: true"
};
let entry: SnapshotEntry = serde_saphyr::from_str(yaml).expect("parse");
assert!(entry.optional, "deserialize must capture optional: true");
let out = serialize_yaml::to_string(&entry).expect("serialize");
assert!(out.contains("optional: true"), "serialize must round-trip optional: true:\n{out}");
}
/// Absent `optional:` defaults to `false`, and `false` does NOT
/// serialize (matching every other `optional?: true` field in
/// upstream's lockfile types).
#[test]
fn optional_defaults_false_and_omits_when_false() {
let yaml = text_block! {
"dependencies:"
" bar: 1.0.0"
};
let entry: SnapshotEntry = serde_saphyr::from_str(yaml).expect("parse");
assert!(!entry.optional, "default must be false when absent");
let out = serialize_yaml::to_string(&entry).expect("serialize");
// Match the exact key spelling (`optional:` followed by a space
// or a newline) so a future fixture containing
// `optionalDependencies:` doesn't fool this assertion.
assert!(
!out.contains("optional: ") && !out.contains("optional:\n"),
"the `optional` key must not be serialized when false:\n{out}",
);
}

View File

@@ -6,7 +6,10 @@ use pacquet_executor::{
};
use pacquet_lockfile::{PackageKey, ProjectSnapshot, SnapshotEntry};
use pacquet_package_manifest::pkg_requires_build;
use pacquet_reporter::Reporter;
use pacquet_reporter::{
LogEvent, LogLevel, Reporter, SkippedOptionalDependencyLog, SkippedOptionalPackage,
SkippedOptionalReason,
};
use std::{
collections::{BTreeSet, HashMap},
fs,
@@ -217,7 +220,9 @@ impl<'a> BuildModules<'a> {
continue;
}
run_postinstall_hooks::<R>(RunPostinstallHooks {
let optional = snapshots.get(&snapshot_key).is_some_and(|entry| entry.optional);
let result = run_postinstall_hooks::<R>(RunPostinstallHooks {
dep_path: &snapshot_key.to_string(),
pkg_root: &pkg_dir,
root_modules_dir: modules_dir,
@@ -236,8 +241,34 @@ impl<'a> BuildModules<'a> {
node_gyp_bin: None,
scripts_prepend_node_path: ScriptsPrependNodePath::Never,
script_shell: None,
})
.map_err(BuildModulesError::LifecycleScript)?;
optional,
});
if let Err(err) = result {
if optional {
// Mirrors `building/during-install/src/index.ts:226-238`:
// a build failure on an optional dep is logged
// through the `pnpm:skipped-optional-dependency`
// channel and swallowed so the install can
// continue. The `package.id` field upstream is
// `depNode.dir`; we use the same.
R::emit(&LogEvent::SkippedOptionalDependency(
SkippedOptionalDependencyLog {
level: LogLevel::Debug,
details: Some(err.to_string()),
package: SkippedOptionalPackage {
id: pkg_dir.to_string_lossy().into_owned(),
name: name.clone(),
version: version.clone(),
},
prefix: lockfile_dir.to_string_lossy().into_owned(),
reason: SkippedOptionalReason::BuildFailure,
},
));
continue;
}
return Err(BuildModulesError::LifecycleScript(err));
}
}
}

View File

@@ -3,7 +3,9 @@ use pacquet_lockfile::{
PackageKey, PkgName, PkgVerPeer, ProjectSnapshot, ResolvedDependencyMap,
ResolvedDependencySpec, SnapshotEntry,
};
use pacquet_reporter::{IgnoredScriptsLog, LogEvent, Reporter, SilentReporter};
use pacquet_reporter::{
IgnoredScriptsLog, LogEvent, Reporter, SilentReporter, SkippedOptionalReason,
};
use pretty_assertions::assert_eq;
use std::{
collections::HashMap,
@@ -259,6 +261,148 @@ fn build_modules_excludes_explicit_deny_from_ignored() {
);
}
/// Optional dep whose postinstall fails must be reported through the
/// `pnpm:skipped-optional-dependency` channel (reason `build_failure`)
/// and NOT abort the install. Mirrors upstream
/// `building/during-install/src/index.ts:218-240` and the spirit of
/// `'do not fail on an optional dependency that has a non-optional
/// dependency with a failing postinstall script'` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/installing/deps-installer/test/install/optionalDependencies.ts#L563-L572>.
///
/// The test uses the upstream fixture `@pnpm.e2e/failing-postinstall@1.0.0`
/// (script body verbatim from `/Volumes/src/pnpm/registry-mock/packages/failing-postinstall/package.json`)
/// so the failure mode is exactly the one upstream's optional-dep
/// tests exercise.
///
/// Unix-gated because the upstream script (`echo hello && echo world && exit 1`)
/// is POSIX shell syntax. The cmd-on-Windows path picks a different
/// shell — `pacquet_executor::select_shell` (tested in the executor
/// crate's `shell::tests`) covers the shell-selection branches in
/// isolation.
#[cfg(unix)]
#[test]
fn do_not_fail_on_optional_dep_with_failing_postinstall() {
static EVENTS: Mutex<Vec<LogEvent>> = Mutex::new(Vec::new());
EVENTS.lock().expect("lock").clear();
struct RecordingReporter;
impl Reporter for RecordingReporter {
fn emit(event: &LogEvent) {
EVENTS.lock().expect("lock").push(event.clone());
}
}
let pkg_key = key("@pnpm.e2e/failing-postinstall", "1.0.0");
let mut optional_snapshot = SnapshotEntry::default();
optional_snapshot.optional = true;
let snapshots = HashMap::from([(pkg_key.clone(), optional_snapshot)]);
let importers = root_importers(&[("@pnpm.e2e/failing-postinstall", "1.0.0")]);
// `dangerouslyAllowAllBuilds` so the policy lets the failing
// script through to actually run — this test exercises the
// build-failure path, not the policy gate.
let policy = AllowBuildPolicy::new(rules([]), true);
let virtual_store_dir = tempdir().expect("create temp dir");
let modules_dir = tempdir().expect("create temp dir");
let lockfile_dir = tempdir().expect("create temp dir");
create_failing_postinstall_fixture(virtual_store_dir.path(), &pkg_key);
let ignored = BuildModules {
virtual_store_dir: virtual_store_dir.path(),
modules_dir: modules_dir.path(),
lockfile_dir: lockfile_dir.path(),
snapshots: Some(&snapshots),
importers: &importers,
allow_build_policy: &policy,
}
.run::<RecordingReporter>()
.expect("optional build failure must NOT abort the install");
dbg!(&ignored);
let captured = EVENTS.lock().expect("lock").clone();
dbg!(&captured);
let skipped_event = captured
.iter()
.find_map(|e| match e {
LogEvent::SkippedOptionalDependency(log) => Some(log),
_ => None,
})
.expect("must emit pnpm:skipped-optional-dependency");
assert_eq!(skipped_event.reason, SkippedOptionalReason::BuildFailure);
assert_eq!(skipped_event.package.name, "@pnpm.e2e/failing-postinstall");
assert_eq!(skipped_event.package.version, "1.0.0");
assert!(skipped_event.details.is_some(), "details must carry the error toString");
}
/// Mirrors `'fail on a package with failing postinstall if the
/// package is both an optional and non-optional dependency'` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/installing/deps-installer/test/install/optionalDependencies.ts#L574-L591>.
///
/// Upstream's resolver folds reachability ALL-paths-optional, so a
/// package reachable through any non-optional edge has
/// `snapshots[...].optional = false` in the lockfile (cf.
/// `installing/deps-resolver/src/resolveDependencies.ts:1605-1610`).
/// `BuildModules` then propagates the build failure rather than
/// swallowing it. Pacquet trusts the precomputed flag; this test
/// pins the propagation branch by supplying the same fixture with
/// `optional: false`, which is the lockfile shape upstream produces
/// for the dual-reachability case.
#[cfg(unix)]
#[test]
fn fail_when_failing_postinstall_is_required() {
let pkg_key = key("@pnpm.e2e/failing-postinstall", "1.0.0");
// `optional: false` — pacquet's analog of upstream's
// ALL-paths-optional fold concluding the dep is required.
let snapshots = HashMap::from([(pkg_key.clone(), SnapshotEntry::default())]);
let importers = root_importers(&[("@pnpm.e2e/failing-postinstall", "1.0.0")]);
let policy = AllowBuildPolicy::new(rules([]), true);
let virtual_store_dir = tempdir().expect("create temp dir");
let modules_dir = tempdir().expect("create temp dir");
let lockfile_dir = tempdir().expect("create temp dir");
create_failing_postinstall_fixture(virtual_store_dir.path(), &pkg_key);
let err = BuildModules {
virtual_store_dir: virtual_store_dir.path(),
modules_dir: modules_dir.path(),
lockfile_dir: lockfile_dir.path(),
snapshots: Some(&snapshots),
importers: &importers,
allow_build_policy: &policy,
}
.run::<SilentReporter>()
.expect_err("required build failure must propagate");
eprintln!("ERR: {err}");
assert!(matches!(err, crate::build_modules::BuildModulesError::LifecycleScript(_)));
}
/// Materialize a package fixture whose contents are byte-identical
/// to upstream's `@pnpm.e2e/failing-postinstall@1.0.0` at
/// `/Volumes/src/pnpm/registry-mock/packages/failing-postinstall/package.json`.
/// Reusing the upstream script body (`echo hello && echo world && exit 1`)
/// keeps the failure mode and exit code identical to what
/// `optionalDependencies.ts` exercises against the live mock
/// registry, without dragging the lockfile-with-real-integrity
/// machinery into a `BuildModules`-unit test.
fn create_failing_postinstall_fixture(virtual_store_dir: &Path, key: &PackageKey) -> PathBuf {
let key_str = key.without_peer().to_string();
let name_version = key_str.strip_prefix('/').unwrap_or(&key_str);
let at_idx = name_version.rfind('@').unwrap_or(name_version.len());
let pkg_name = &name_version[..at_idx];
let store_name = name_version.replace('/', "+");
let pkg_dir = virtual_store_dir.join(&store_name).join("node_modules").join(pkg_name);
fs::create_dir_all(&pkg_dir).expect("create pkg dir");
let manifest = serde_json::json!({
"name": pkg_name,
"version": name_version[at_idx + 1..].to_string(),
"scripts": { "postinstall": "echo hello && echo world && exit 1" },
});
fs::write(pkg_dir.join("package.json"), manifest.to_string()).expect("write manifest");
pkg_dir
}
/// Recording fake confirms `pnpm:ignored-scripts` is the right channel
/// for the package list. The frozen-install path emits this once after
/// `BuildModules::run` returns; this test exercises the equivalent

View File

@@ -34,6 +34,7 @@ fn snap(deps: &[(&str, &str)]) -> SnapshotEntry {
optional_dependencies: None,
transitive_peer_dependencies: None,
patched: None,
optional: false,
}
}

View File

@@ -107,6 +107,7 @@ pub fn build_package_snapshot(
optional_dependencies: None,
transitive_peer_dependencies: None,
patched: None,
optional: false,
};
Ok(BuiltSnapshot { package_key, metadata, snapshot })

View File

@@ -649,3 +649,79 @@ async fn install_writes_modules_yaml() {
drop(dir);
}
/// Ports `'do not fail on an optional dependency that has a non-optional
/// dependency with a failing postinstall script'` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/installing/deps-installer/test/install/optionalDependencies.ts#L563-L572>.
///
/// Resolves `@pnpm.e2e/has-failing-postinstall-dep@1.0.0` as an
/// optional dependency through the live registry-mock instance. The
/// transitive `@pnpm.e2e/failing-postinstall@1.0.0` has a
/// `postinstall` that exits non-zero. Pacquet's
/// `frozen_lockfile=false` path stops at extraction (script execution
/// lives behind `BuildModules` in the frozen-lockfile branch —
/// `BuildModules` itself is unit-tested against the same fixture in
/// `crate::build_modules::tests::do_not_fail_on_optional_dep_with_failing_postinstall`).
/// This test pins the fetch + extract behavior on the optional edge:
/// both packages must land in the virtual store and the install must
/// NOT abort, matching the upstream expectation that `addDependenciesToPackage`
/// resolves.
#[tokio::test]
async fn install_optional_failing_postinstall_dep_via_registry_mock_succeeds() {
let mock_instance = AutoMockInstance::load_or_init();
let dir = tempdir().unwrap();
let store_dir = dir.path().join("pacquet-store");
let project_root = dir.path().join("project");
let modules_dir = project_root.join("node_modules");
let virtual_store_dir = modules_dir.join(".pacquet");
let manifest_path = dir.path().join("package.json");
let mut manifest = PackageManifest::create_if_needed(manifest_path.clone()).unwrap();
manifest
.add_dependency("@pnpm.e2e/has-failing-postinstall-dep", "1.0.0", DependencyGroup::Optional)
.unwrap();
manifest.save().unwrap();
let mut config = Npmrc::new();
config.store_dir = store_dir.into();
config.modules_dir = modules_dir.to_path_buf();
config.virtual_store_dir = virtual_store_dir.to_path_buf();
config.registry = mock_instance.url();
let config = config.leak();
Install {
tarball_mem_cache: &Default::default(),
http_client: &Default::default(),
config,
manifest: &manifest,
lockfile: None,
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
frozen_lockfile: false,
resolved_packages: &Default::default(),
}
.run::<SilentReporter>()
.await
.expect("optional dep with failing transitive postinstall must NOT abort the install");
// Both the wrapper and the transitive must reach the virtual store.
assert!(
is_symlink_or_junction(
&project_root.join("node_modules/@pnpm.e2e/has-failing-postinstall-dep"),
)
.unwrap(),
"wrapper symlink missing",
);
assert!(
project_root
.join("node_modules/.pacquet/@pnpm.e2e+has-failing-postinstall-dep@1.0.0")
.is_dir(),
"wrapper virtual-store dir missing",
);
assert!(
project_root.join("node_modules/.pacquet/@pnpm.e2e+failing-postinstall@1.0.0").is_dir(),
"transitive `failing-postinstall` must be extracted to the virtual store",
);
drop((dir, mock_instance));
}

View File

@@ -149,6 +149,19 @@ pub enum LogEvent {
/// Emit site: <https://github.com/pnpm/pnpm/blob/80037699fb/installing/deps-installer/src/install/index.ts#L414>.
#[serde(rename = "pnpm:ignored-scripts")]
IgnoredScripts(IgnoredScriptsLog),
/// One per optional-dependency that pnpm decided to skip rather
/// than fail the install over. Reason discriminates the cause —
/// pacquet currently only emits `build_failure` (from
/// `BuildModules` when a postinstall fails on an optional dep);
/// the `unsupported_engine` / `unsupported_platform` /
/// `resolution_failure` reasons upstream uses come from earlier
/// phases that haven't landed in pacquet yet.
///
/// Upstream: <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/core/core-loggers/src/skippedOptionalDependencyLogger.ts>.
/// Emit site (build_failure): <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L218-L240>.
#[serde(rename = "pnpm:skipped-optional-dependency")]
SkippedOptionalDependency(SkippedOptionalDependencyLog),
}
/// `pnpm:context` payload.
@@ -517,6 +530,60 @@ pub struct IgnoredScriptsLog {
pub package_names: Vec<String>,
}
/// `pnpm:skipped-optional-dependency` payload.
///
/// Upstream's `SkippedOptionalDependencyMessage` is a discriminated
/// union over `reason` with two distinct `package` shapes:
/// `build_failure` / `unsupported_engine` / `unsupported_platform`
/// all carry `package: { id, name, version }`; `resolution_failure`
/// carries `package: { name?, version?, bareSpecifier }` with no
/// `id`. This struct models only the **first** shape — the three
/// reasons that share `{ id, name, version }`. The
/// `ResolutionFailure` variant in [`SkippedOptionalReason`] is
/// declared for forward compatibility on the enum side, but its
/// distinct `package` shape means a `ResolutionFailure` emit will
/// require a sibling struct (or a `#[serde(untagged)]` enum
/// substituting for `SkippedOptionalDependencyLog`) — not just
/// flipping the `reason` value. Refactoring is deferred until
/// pacquet actually has a resolver-time emit site to produce that
/// payload.
///
/// `parents` is a TODO upstream too (see
/// `during-install/src/index.ts:227`) and is omitted here.
#[derive(Debug, Clone, Serialize)]
pub struct SkippedOptionalDependencyLog {
pub level: LogLevel,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
pub package: SkippedOptionalPackage,
pub prefix: String,
pub reason: SkippedOptionalReason,
}
/// Package identifier carried on a [`SkippedOptionalDependencyLog`].
/// Matches the upstream "non-resolution-failure" branch of
/// `SkippedOptionalDependencyMessage` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/core/core-loggers/src/skippedOptionalDependencyLogger.ts#L15-L19>.
#[derive(Debug, Clone, Serialize)]
pub struct SkippedOptionalPackage {
pub id: String,
pub name: String,
pub version: String,
}
/// Discriminator on a [`SkippedOptionalDependencyLog`]. Only
/// `BuildFailure` lands at pacquet's current emit sites; the others
/// are kept in the enum for forward compatibility so callers don't
/// have to widen the type when more reasons are wired up.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SkippedOptionalReason {
BuildFailure,
UnsupportedEngine,
UnsupportedPlatform,
ResolutionFailure,
}
/// Severity level on the [bunyan]-shaped envelope.
///
/// pnpm's logger uses the [bole] library, which writes one of these strings

View File

@@ -9,7 +9,8 @@ use crate::{
GetHostName, IgnoredScriptsLog, LifecycleLog, LifecycleMessage, LifecycleStdio, LogEvent,
LogLevel, PackageImportMethod, PackageImportMethodLog, PackageManifestLog,
PackageManifestMessage, ProgressLog, ProgressMessage, RealApi, RemovedRoot, Reporter,
RequestRetryError, RequestRetryLog, RootLog, RootMessage, SilentReporter, Stage, StageLog,
RequestRetryError, RequestRetryLog, RootLog, RootMessage, SilentReporter,
SkippedOptionalDependencyLog, SkippedOptionalPackage, SkippedOptionalReason, Stage, StageLog,
StatsLog, StatsMessage, SummaryLog,
};
@@ -576,6 +577,81 @@ fn ignored_scripts_event_matches_pnpm_wire_shape() {
assert_eq!(json["packageNames"], serde_json::json!(["foo@1.0.0", "bar@2.0.0"]));
}
/// `pnpm:skipped-optional-dependency` matches upstream's wire
/// shape: top-level `details`, `package: { id, name, version }`,
/// `prefix`, and `reason` (snake_case). Mirrors
/// `SkippedOptionalDependencyMessage` at
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/core/core-loggers/src/skippedOptionalDependencyLogger.ts>.
#[test]
fn skipped_optional_dependency_event_matches_pnpm_wire_shape() {
let event = LogEvent::SkippedOptionalDependency(SkippedOptionalDependencyLog {
level: LogLevel::Debug,
details: Some("build failed: exit code 1".to_string()),
package: SkippedOptionalPackage {
id: "/foo/1.0.0".to_string(),
name: "foo".to_string(),
version: "1.0.0".to_string(),
},
prefix: "/projects/x".to_string(),
reason: SkippedOptionalReason::BuildFailure,
});
let envelope = Envelope { time: 1_700_000_000_000, hostname: "host", pid: 4242, event: &event };
let json: Value = envelope
.pipe_ref(serde_json::to_string)
.expect("serialize envelope")
.pipe_as_ref(serde_json::from_str)
.expect("parse JSON");
dbg!(&json);
assert_eq!(json["name"], "pnpm:skipped-optional-dependency");
assert_eq!(json["level"], "debug");
assert_eq!(json["reason"], "build_failure");
assert_eq!(json["details"], "build failed: exit code 1");
assert_eq!(json["prefix"], "/projects/x");
assert_eq!(json["package"]["id"], "/foo/1.0.0");
assert_eq!(json["package"]["name"], "foo");
assert_eq!(json["package"]["version"], "1.0.0");
}
/// `details` is optional upstream and must be omitted from the wire
/// when absent (`skip_serializing_if = "Option::is_none"`).
#[test]
fn skipped_optional_omits_absent_details() {
let event = LogEvent::SkippedOptionalDependency(SkippedOptionalDependencyLog {
level: LogLevel::Debug,
details: None,
package: SkippedOptionalPackage {
id: "/bar/2.0.0".to_string(),
name: "bar".to_string(),
version: "2.0.0".to_string(),
},
prefix: "/projects/y".to_string(),
reason: SkippedOptionalReason::BuildFailure,
});
let envelope = Envelope { time: 1_700_000_000_000, hostname: "host", pid: 4242, event: &event };
let json: Value = envelope
.pipe_ref(serde_json::to_string)
.expect("serialize envelope")
.pipe_as_ref(serde_json::from_str)
.expect("parse JSON");
assert!(json.get("details").is_none(), "details must be omitted when absent, got {json:?}");
}
/// All four reason variants serialize as the snake_case strings
/// pnpm's reporter dispatches on.
#[test]
fn skipped_optional_reason_serializes_in_pnpm_form() {
let cases = [
(SkippedOptionalReason::BuildFailure, "build_failure"),
(SkippedOptionalReason::UnsupportedEngine, "unsupported_engine"),
(SkippedOptionalReason::UnsupportedPlatform, "unsupported_platform"),
(SkippedOptionalReason::ResolutionFailure, "resolution_failure"),
];
for (reason, expected) in cases {
let json = serde_json::to_string(&reason).expect("serialize reason");
assert_eq!(json, format!("\"{expected}\""), "{reason:?} must serialize as {expected:?}");
}
}
/// Phase markers serialize as the snake_case strings pnpm uses.
#[test]
fn stage_phases_serialize_in_pnpm_form() {

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env node
// Wrapper for `@pnpm/registry-mock` that drives the server via the
// programmatic `start({ useNodeVersion, listen })` entry point
// instead of the bare CLI. Mirrors pnpm's own jest globalSetup at
// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/__utils__/jest-config/with-registry/globalSetup.js>.
//
// Why a wrapper: verdaccio 5.33 (the version `@pnpm/registry-mock@6`
// bundles) rejects its own auto-generated 64-character storage
// secret on Node 22+ with `Invalid storage secret key length, must
// be 32 characters long but is 64`. Pnpm pins
// `useNodeVersion: '20.16.0'` so verdaccio runs under a Node that
// skips that enforcement. The default CLI export of
// `@pnpm/registry-mock` does NOT pass `useNodeVersion` through, so
// the CLI launch fails on a modern host Node. The programmatic
// `start()` does.
//
// Pacquet's Rust launcher (`tasks/registry-mock/src/node_registry_mock.rs`)
// invokes this script via `node`. Calling pattern:
// `node launch.mjs prepare` — publish fixtures
// `node launch.mjs` (or any other arg) — launch the server; port
// comes from
// `PNPM_REGISTRY_MOCK_PORT`.
import { prepare, start } from '@pnpm/registry-mock'
const NODE_RUNTIME = '20.16.0'
if (process.argv[2] === 'prepare') {
prepare()
process.exit(0)
}
const listen = process.env.PNPM_REGISTRY_MOCK_PORT
if (!listen) {
console.error('PNPM_REGISTRY_MOCK_PORT must be set when launching the mock')
process.exit(1)
}
const server = start({
useNodeVersion: NODE_RUNTIME,
stdio: 'inherit',
listen,
})
// Shell-convention exit code when the child was killed by a signal:
// 128 + signal number. Anything Node knows the number for; fall
// back to 1 otherwise.
const SIGNAL_NUMBERS = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 }
server.on('exit', (code, signal) => {
if (signal != null) {
process.exit(128 + (SIGNAL_NUMBERS[signal] ?? 1))
} else {
process.exit(code ?? 0)
}
})
// Forward the usual termination signals to the child so it can shut
// down cleanly. The child's `exit` handler above is what actually
// terminates the wrapper — we do NOT re-raise the signal to ourselves
// (re-raising would either hit our own handler in a loop or, after
// removing it, race with the child's exit propagation).
for (const sig of Object.keys(SIGNAL_NUMBERS)) {
process.on(sig, () => {
if (!server.killed) server.kill(sig)
})
}

View File

@@ -1,5 +1,5 @@
{
"devDependencies": {
"@pnpm/registry-mock": "^3.16.0"
"@pnpm/registry-mock": "^6.0.0"
}
}

View File

@@ -9,8 +9,8 @@ importers:
.:
devDependencies:
'@pnpm/registry-mock':
specifier: ^3.16.0
version: 3.50.0(typanion@3.14.0)
specifier: ^6.0.0
version: 6.0.0(verdaccio@5.33.0(typanion@3.14.0))
packages:
@@ -30,10 +30,26 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@pnpm/registry-mock@3.50.0':
resolution: {integrity: sha512-/YGi3OCMWXkk8JwUbVOQo5Ws89yA9ub+jnYboAgxUJ84K6a2m3xZiRn1im0zxC7L6F63Xx++ZuGrlwKEWHqf+A==}
engines: {node: '>=10.13'}
'@pnpm/registry-mock@6.0.0':
resolution: {integrity: sha512-OdeuaWEcAg0yFQacHwujnTckr5muhZ5P2PRoxoMMtiGDoOVKtEXqb4mXkz9VwsFeuSrj/3bbI5v42SVNZr+EHw==}
engines: {node: '>=18.12'}
hasBin: true
peerDependencies:
verdaccio: ^5.20.1 || ^6.1.6
'@postman/form-data@3.1.1':
resolution: {integrity: sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==}
engines: {node: '>= 6'}
'@postman/tough-cookie@4.1.3-postman.1':
resolution: {integrity: sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==}
engines: {node: '>=6'}
'@postman/tunnel-agent@0.6.8':
resolution: {integrity: sha512-2U42SmZW5G+suEcS++zB94sBWNO4qD4bvETGFRFDTqSpYl5ksfjcPqzYpgQgXgUmb6dfz+fAGbkcRamounGm0w==}
'@qiwi/npm-types@1.0.3':
resolution: {integrity: sha512-fHUud+Fo8JiGOPMmnVaOYd/crEnBM+qB8qXrSRz1AkYZAj8BtudFYcsiFweL6DcYXguq7+iXKdzx42dGCEPHiQ==}
'@verdaccio/auth@8.0.0-next-8.1':
resolution: {integrity: sha512-sPmHdnYuRSMgABCsTJEfz8tb/smONsWVg0g4KK2QycyYZ/A+RwZLV1JLiQb4wzu9zvS0HSloqWqkWlyNHW3mtw==}
@@ -51,6 +67,10 @@ packages:
resolution: {integrity: sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw==}
engines: {node: '>=14'}
'@verdaccio/file-locking@10.3.0':
resolution: {integrity: sha512-FE5D5H4wy/nhgR/d2J5e1Na9kScj2wMjlLPBHz7XF4XZAVSRdm45+kL3ZmrfA6b2HTADP/uH7H05/cnAYW8bhw==}
engines: {node: '>=8'}
'@verdaccio/file-locking@10.3.1':
resolution: {integrity: sha512-oqYLfv3Yg3mAgw9qhASBpjD50osj2AX4IwbkUtyuhhKGyoFU9eZdrbeW6tpnqUnj6yBMtAPm2eGD4BwQuX400g==}
engines: {node: '>=12'}
@@ -140,8 +160,8 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
anonymous-npm-registry-client@0.2.0:
resolution: {integrity: sha512-ym3GCDQU8B6PZrswCvanRiWoSg2QrrlPwoRlMr4oCpGvyK2KlwTujdCZfxrGapqxrqEY3TpxEqLf+7PhFnyaLA==}
anonymous-npm-registry-client@0.3.2:
resolution: {integrity: sha512-Cw96dHAq3W/xVBNkPwLiTZnIbLgNXbmIxFAh63x8LjeaRVEGjMnZ6X/u7xSuqRqbig5jrYWJp5UTgRoXKUotVQ==}
ansi-regex@2.1.1:
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
@@ -255,6 +275,13 @@ packages:
bcryptjs@2.4.3:
resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
bluebird@2.11.0:
resolution: {integrity: sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==}
body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -269,6 +296,9 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
brotli@1.3.3:
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
browserify-zlib@0.1.4:
resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
@@ -330,9 +360,9 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
@@ -348,6 +378,10 @@ packages:
cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
cookie@0.6.0:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
@@ -518,6 +552,10 @@ packages:
express-rate-limit@5.5.1:
resolution: {integrity: sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==}
express@4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'}
express@4.21.0:
resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==}
engines: {node: '>= 0.10.0'}
@@ -563,6 +601,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
finalhandler@1.2.0:
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
engines: {node: '>= 0.8'}
finalhandler@1.3.1:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'}
@@ -570,10 +612,6 @@ packages:
forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
form-data@2.3.3:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
@@ -678,9 +716,9 @@ packages:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
http-signature@1.2.0:
resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
engines: {node: '>=0.8', npm: '>=1.3.7'}
http-signature@1.3.6:
resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==}
engines: {node: '>=0.10'}
http-signature@1.4.0:
resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==}
@@ -818,10 +856,6 @@ packages:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
jsprim@1.4.2:
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
engines: {node: '>=0.6.0'}
jsprim@2.0.2:
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
engines: {'0': node >=0.6.0}
@@ -879,6 +913,9 @@ packages:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
merge-descriptors@1.0.3:
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
@@ -988,6 +1025,15 @@ packages:
encoding:
optional: true
node-fetch@2.6.8:
resolution: {integrity: sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
@@ -1076,6 +1122,9 @@ packages:
path-to-regexp@0.1.10:
resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==}
path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@@ -1118,6 +1167,10 @@ packages:
resolution: {integrity: sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ==}
engines: {node: '>= 0.4.0'}
postman-request@2.88.1-postman.40:
resolution: {integrity: sha512-uE4AiIqhjtHKp4pj9ei7fkdfNXEX9IqDBlK1plGAQne6y79UUlrTdtYLhwXoO0AMOvqyl9Ar+BU6Eo6P/MPgfg==}
engines: {node: '>= 16'}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -1148,6 +1201,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@@ -1156,6 +1213,9 @@ packages:
resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==}
engines: {node: '>=0.6'}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -1166,6 +1226,10 @@ packages:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
raw-body@2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'}
raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
@@ -1193,15 +1257,13 @@ packages:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
request@2.88.2:
resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
engines: {node: '>= 6'}
deprecated: request has been deprecated, see https://github.com/request/request/issues/3142
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
resolve@1.22.12:
resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
engines: {node: '>= 0.4'}
@@ -1255,10 +1317,18 @@ packages:
engines: {node: '>=10'}
hasBin: true
send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
serve-static@1.15.0:
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
engines: {node: '>= 0.8.0'}
serve-static@1.16.2:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'}
@@ -1348,6 +1418,9 @@ packages:
steno@0.4.4:
resolution: {integrity: sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==}
stream-length@1.0.2:
resolution: {integrity: sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==}
stream-shift@1.0.3:
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
@@ -1424,10 +1497,6 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tough-cookie@2.5.0:
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
engines: {node: '>=0.8'}
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
@@ -1467,6 +1536,10 @@ packages:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'}
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -1481,6 +1554,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -1488,13 +1564,9 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@3.4.0:
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
hasBin: true
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
validate-npm-package-license@3.0.4:
@@ -1511,10 +1583,18 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
verdaccio-audit@10.2.4:
resolution: {integrity: sha512-/0H6/JFVnhHwucUfMRVjL6gtGnB5gr3dDxq93Ja1Y0ob+2jxAfpqNMHg8c6/d/ZyHFf0y4tXzHESDruXCzTiaQ==}
engines: {node: '>=8'}
verdaccio-audit@13.0.0-next-8.1:
resolution: {integrity: sha512-EEfUeC1kHuErtwF9FC670W+EXHhcl+iuigONkcprwRfkPxmdBs+Hx36745hgAMZ9SCqedNECaycnGF3tZ3VYfw==}
engines: {node: '>=12'}
verdaccio-htpasswd@10.5.2:
resolution: {integrity: sha512-bO5Wm8w07pWswNvwFWjNEoznuUU37CcfblcrU0Ci8c038EgTu2V47uwh4AyZ4PTK6ps9oxHqA7a1b+83sY0OkA==}
engines: {node: '>=8'}
verdaccio-htpasswd@13.0.0-next-8.1:
resolution: {integrity: sha512-BfvmO+ZdbwfttOwrdTPD6Bccr1ZfZ9Tk/9wpXamxdWB/XPWlk3FtyGsvqCmxsInRLPhQ/FSk9c3zRCGvICTFYg==}
engines: {node: '>=12'}
@@ -1598,23 +1678,40 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@pnpm/registry-mock@3.50.0(typanion@3.14.0)':
'@pnpm/registry-mock@6.0.0(verdaccio@5.33.0(typanion@3.14.0))':
dependencies:
anonymous-npm-registry-client: 0.2.0
anonymous-npm-registry-client: 0.3.2
execa: 5.1.1
fs-extra: 11.3.4
read-yaml-file: 2.1.0
rimraf: 3.0.2
tempy: 1.0.1
verdaccio: 5.33.0(typanion@3.14.0)
verdaccio-audit: 10.2.4
verdaccio-htpasswd: 10.5.2
write-yaml-file: 4.2.0
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- encoding
- react-native-b4a
- supports-color
- typanion
'@postman/form-data@3.1.1':
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
'@postman/tough-cookie@4.1.3-postman.1':
dependencies:
psl: 1.15.0
punycode: 2.3.1
universalify: 0.2.0
url-parse: 1.5.10
'@postman/tunnel-agent@0.6.8':
dependencies:
safe-buffer: 5.2.1
'@qiwi/npm-types@1.0.3': {}
'@verdaccio/auth@8.0.0-next-8.1':
dependencies:
@@ -1655,6 +1752,10 @@ snapshots:
process-warning: 1.0.0
semver: 7.6.3
'@verdaccio/file-locking@10.3.0':
dependencies:
lockfile: 1.0.4
'@verdaccio/file-locking@10.3.1':
dependencies:
lockfile: 1.0.4
@@ -1813,14 +1914,15 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
anonymous-npm-registry-client@0.2.0:
anonymous-npm-registry-client@0.3.2:
dependencies:
concat-stream: 1.6.2
'@qiwi/npm-types': 1.0.3
concat-stream: 2.0.0
graceful-fs: 4.2.11
normalize-package-data: 2.5.0
npm-package-arg: 6.1.1
once: 1.4.0
request: 2.88.2
request: postman-request@2.88.1-postman.40
retry: 0.13.1
safe-buffer: 5.2.1
semver: 7.7.4
@@ -1911,6 +2013,25 @@ snapshots:
bcryptjs@2.4.3: {}
bluebird@2.11.0: {}
body-parser@1.20.1:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
raw-body: 2.5.1
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
body-parser@1.20.3:
dependencies:
bytes: 3.1.2
@@ -1941,6 +2062,10 @@ snapshots:
dependencies:
fill-range: 7.1.1
brotli@1.3.3:
dependencies:
base64-js: 1.5.1
browserify-zlib@0.1.4:
dependencies:
pako: 0.2.9
@@ -2003,11 +2128,11 @@ snapshots:
concat-map@0.0.1: {}
concat-stream@1.6.2:
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 2.3.8
readable-stream: 3.6.2
typedarray: 0.0.6
console-control-strings@1.1.0:
@@ -2021,6 +2146,8 @@ snapshots:
cookie-signature@1.0.6: {}
cookie@0.5.0: {}
cookie@0.6.0: {}
cookie@0.7.1: {}
@@ -2174,6 +2301,42 @@ snapshots:
express-rate-limit@5.5.1: {}
express@4.18.2:
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.1
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.5.0
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.2.0
fresh: 0.5.2
http-errors: 2.0.0
merge-descriptors: 1.0.1
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.7
proxy-addr: 2.0.7
qs: 6.11.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.18.0
serve-static: 1.15.0
setprototypeof: 1.2.0
statuses: 2.0.1
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
express@4.21.0:
dependencies:
accepts: 1.3.8
@@ -2278,6 +2441,18 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
finalhandler@1.2.0:
dependencies:
debug: 2.6.9
encodeurl: 1.0.2
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
finalhandler@1.3.1:
dependencies:
debug: 2.6.9
@@ -2292,12 +2467,6 @@ snapshots:
forever-agent@0.6.1: {}
form-data@2.3.3:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
@@ -2438,10 +2607,10 @@ snapshots:
statuses: 2.0.1
toidentifier: 1.0.1
http-signature@1.2.0:
http-signature@1.3.6:
dependencies:
assert-plus: 1.0.0
jsprim: 1.4.2
jsprim: 2.0.2
sshpk: 1.18.0
http-signature@1.4.0:
@@ -2558,14 +2727,7 @@ snapshots:
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.6.3
jsprim@1.4.2:
dependencies:
assert-plus: 1.0.0
extsprintf: 1.3.0
json-schema: 0.4.0
verror: 1.10.0
semver: 7.7.4
jsprim@2.0.2:
dependencies:
@@ -2621,6 +2783,8 @@ snapshots:
media-typer@0.3.0: {}
merge-descriptors@1.0.1: {}
merge-descriptors@1.0.3: {}
merge-stream@2.0.0: {}
@@ -2694,6 +2858,10 @@ snapshots:
dependencies:
whatwg-url: 5.0.0
node-fetch@2.6.8:
dependencies:
whatwg-url: 5.0.0
normalize-package-data@2.5.0:
dependencies:
hosted-git-info: 2.8.9
@@ -2772,6 +2940,8 @@ snapshots:
path-to-regexp@0.1.10: {}
path-to-regexp@0.1.7: {}
path-type@4.0.0: {}
peek-stream@1.1.3:
@@ -2830,6 +3000,31 @@ snapshots:
pkginfo@0.4.1: {}
postman-request@2.88.1-postman.40:
dependencies:
'@postman/form-data': 3.1.1
'@postman/tough-cookie': 4.1.3-postman.1
'@postman/tunnel-agent': 0.6.8
aws-sign2: 0.7.0
aws4: 1.13.2
brotli: 1.3.3
caseless: 0.12.0
combined-stream: 1.0.8
extend: 3.0.2
forever-agent: 0.6.1
har-validator: 5.1.5
http-signature: 1.3.6
is-typedarray: 1.0.0
isstream: 0.1.2
json-stringify-safe: 5.0.1
mime-types: 2.1.35
oauth-sign: 0.9.0
performance-now: 2.1.0
qs: 6.5.5
safe-buffer: 5.2.1
stream-length: 1.0.2
uuid: 8.3.2
process-nextick-args@2.0.1: {}
process-warning@1.0.0: {}
@@ -2860,18 +3055,31 @@ snapshots:
punycode@2.3.1: {}
qs@6.11.0:
dependencies:
side-channel: 1.1.0
qs@6.13.0:
dependencies:
side-channel: 1.1.0
qs@6.5.5: {}
querystringify@2.2.0: {}
queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
range-parser@1.2.1: {}
raw-body@2.5.1:
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
raw-body@2.5.2:
dependencies:
bytes: 3.1.2
@@ -2912,31 +3120,10 @@ snapshots:
real-require@0.2.0: {}
request@2.88.2:
dependencies:
aws-sign2: 0.7.0
aws4: 1.13.2
caseless: 0.12.0
combined-stream: 1.0.8
extend: 3.0.2
forever-agent: 0.6.1
form-data: 2.3.3
har-validator: 5.1.5
http-signature: 1.2.0
is-typedarray: 1.0.0
isstream: 0.1.2
json-stringify-safe: 5.0.1
mime-types: 2.1.35
oauth-sign: 0.9.0
performance-now: 2.1.0
qs: 6.5.5
safe-buffer: 5.2.1
tough-cookie: 2.5.0
tunnel-agent: 0.6.0
uuid: 3.4.0
require-from-string@2.0.2: {}
requires-port@1.0.0: {}
resolve@1.22.12:
dependencies:
es-errors: 1.3.0
@@ -2974,6 +3161,24 @@ snapshots:
semver@7.7.4: {}
send@0.18.0:
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -2992,6 +3197,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
serve-static@1.15.0:
dependencies:
encodeurl: 1.0.2
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.18.0
transitivePeerDependencies:
- supports-color
serve-static@1.16.2:
dependencies:
encodeurl: 2.0.0
@@ -3098,6 +3312,10 @@ snapshots:
dependencies:
graceful-fs: 4.2.11
stream-length@1.0.2:
dependencies:
bluebird: 2.11.0
stream-shift@1.0.3: {}
streamx@2.25.0:
@@ -3196,11 +3414,6 @@ snapshots:
toidentifier@1.0.1: {}
tough-cookie@2.5.0:
dependencies:
psl: 1.15.0
punycode: 2.3.1
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
@@ -3235,6 +3448,8 @@ snapshots:
dependencies:
crypto-random-string: 2.0.0
universalify@0.2.0: {}
universalify@2.0.1: {}
unix-crypt-td-js@1.1.4: {}
@@ -3245,12 +3460,15 @@ snapshots:
dependencies:
punycode: 2.3.1
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
uuid@3.4.0: {}
uuid@8.3.2: {}
validate-npm-package-license@3.0.4:
@@ -3266,6 +3484,16 @@ snapshots:
vary@1.1.2: {}
verdaccio-audit@10.2.4:
dependencies:
body-parser: 1.20.1
express: 4.18.2
https-proxy-agent: 5.0.1
node-fetch: 2.6.8
transitivePeerDependencies:
- encoding
- supports-color
verdaccio-audit@13.0.0-next-8.1:
dependencies:
'@verdaccio/config': 8.0.0-next-8.1
@@ -3277,6 +3505,14 @@ snapshots:
- encoding
- supports-color
verdaccio-htpasswd@10.5.2:
dependencies:
'@verdaccio/file-locking': 10.3.0
apache-md5: 1.1.8
bcryptjs: 2.4.3
http-errors: 2.0.0
unix-crypt-td-js: 1.1.4
verdaccio-htpasswd@13.0.0-next-8.1:
dependencies:
'@verdaccio/core': 8.0.0-next-8.1

View File

@@ -1,2 +1,12 @@
allowBuilds:
core-js: false
# Verdaccio's CJS deps (notably `@verdaccio/signature` requiring
# `@verdaccio/config`) don't resolve correctly under pnpm's strict
# isolation when verdaccio is invoked from a nested install. The
# Rust launcher spawns `node launch.mjs` which calls
# `@pnpm/registry-mock`'s programmatic `start()` API, which in turn
# spawns verdaccio — a flat `node_modules` ensures verdaccio's
# transitive CJS requires resolve no matter where in the tree
# they're loaded from.
nodeLinker: hoisted

View File

@@ -9,7 +9,7 @@ use reqwest::Client;
use std::{
fs::File,
path::Path,
process::{Child, Command, Stdio},
process::{Child, Stdio},
};
use sysinfo::{Pid, Signal};
use tokio::time::{Duration, sleep};
@@ -79,7 +79,6 @@ impl<'a> MockInstanceOptions<'a> {
eprintln!("Preparing...");
node_registry_mock()
.pipe(Command::new)
.arg("prepare")
.env("PNPM_REGISTRY_MOCK_PORT", &port)
.stdin(Stdio::null())
@@ -95,7 +94,6 @@ impl<'a> MockInstanceOptions<'a> {
File::create(stderr).expect("create file for stderr").into()
});
let process = node_registry_mock()
.pipe(Command::new)
.env("PNPM_REGISTRY_MOCK_PORT", &port)
.stdin(Stdio::null())
.stdout(stdout)

View File

@@ -1,25 +1,25 @@
use crate::registry_mock;
use pipe_trait::Pipe;
use std::{
env, iter,
path::{Path, PathBuf},
sync::OnceLock,
};
use which::which_in;
use std::{path::PathBuf, process::Command, sync::OnceLock};
static NODE_REGISTRY_MOCK: OnceLock<PathBuf> = OnceLock::new();
static LAUNCH_SCRIPT: OnceLock<PathBuf> = OnceLock::new();
fn init() -> PathBuf {
let bin = registry_mock().join("node_modules").join(".bin");
let paths = env::var_os("PATH")
.unwrap_or_default()
.pipe_ref(env::split_paths)
.chain(iter::once(bin))
.pipe(env::join_paths)
.expect("append node_modules/.bin to PATH");
which_in("registry-mock", Some(paths), ".").expect("find registry-mock binary")
/// Path to the `launch.mjs` wrapper that drives `@pnpm/registry-mock`
/// via its programmatic API. The wrapper is required because the
/// package's default CLI export does not thread `useNodeVersion`
/// through — and verdaccio 5.33 (the version v6 of `@pnpm/registry-mock`
/// bundles) rejects its 64-character storage secret on Node 22+, so
/// running it under the host Node fails. Pacquet pins
/// `useNodeVersion: '20.16.0'` in the wrapper to match pnpm's jest
/// `globalSetup` shape.
fn launch_script() -> &'static PathBuf {
LAUNCH_SCRIPT.get_or_init(|| registry_mock().join("launch.mjs"))
}
pub fn node_registry_mock() -> &'static Path {
NODE_REGISTRY_MOCK.get_or_init(init)
/// Returns a [`Command`] pre-populated with `node <launch.mjs>`. The
/// caller appends `prepare` (to publish fixtures) or omits the arg
/// (to launch the server) and any environment / stdio setup.
pub fn node_registry_mock() -> Command {
let mut cmd = Command::new("node");
cmd.arg(launch_script());
cmd
}