From 2f64c727ea39dbd1fda0221430cbcc452f3a6a31 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Tue, 12 May 2026 12:59:04 +0200 Subject: [PATCH] feat(executor): swallow optional-dep build failures and report via pnpm:skipped-optional-dependency (#397) (#419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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[].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. --- .../install__should_install_dependencies.snap | 6 +- .../install__should_install_exec_files.snap | 6 +- .../install__should_install_index_files.snap | 31 +- pacquet/crates/executor/src/lifecycle.rs | 14 +- .../crates/executor/src/lifecycle/tests.rs | 84 ++++ pacquet/crates/lockfile/src/snapshot_entry.rs | 22 + .../lockfile/src/snapshot_entry/tests.rs | 44 ++ .../package-manager/src/build_modules.rs | 39 +- .../src/build_modules/tests.rs | 146 +++++- .../src/build_sequence/tests.rs | 1 + .../package-manager/src/build_snapshot.rs | 1 + .../package-manager/src/install/tests.rs | 76 ++++ pacquet/crates/reporter/src/lib.rs | 67 +++ pacquet/crates/reporter/src/tests.rs | 78 +++- pacquet/tasks/registry-mock/launch.mjs | 67 +++ pacquet/tasks/registry-mock/package.json | 2 +- pacquet/tasks/registry-mock/pnpm-lock.yaml | 420 ++++++++++++++---- .../tasks/registry-mock/pnpm-workspace.yaml | 10 + .../tasks/registry-mock/src/mock_instance.rs | 4 +- .../registry-mock/src/node_registry_mock.rs | 38 +- 20 files changed, 1018 insertions(+), 138 deletions(-) create mode 100644 pacquet/crates/lockfile/src/snapshot_entry/tests.rs create mode 100644 pacquet/tasks/registry-mock/launch.mjs diff --git a/pacquet/crates/cli/tests/snapshots/install__should_install_dependencies.snap b/pacquet/crates/cli/tests/snapshots/install__should_install_dependencies.snap index ca0b40c941..a53fdab9b7 100644 --- a/pacquet/crates/cli/tests/snapshots/install__should_install_dependencies.snap +++ b/pacquet/crates/cli/tests/snapshots/install__should_install_dependencies.snap @@ -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", ], ) diff --git a/pacquet/crates/cli/tests/snapshots/install__should_install_exec_files.snap b/pacquet/crates/cli/tests/snapshots/install__should_install_exec_files.snap index 3ff5ce21ab..5b2ffbbdd4 100644 --- a/pacquet/crates/cli/tests/snapshots/install__should_install_exec_files.snap +++ b/pacquet/crates/cli/tests/snapshots/install__should_install_exec_files.snap @@ -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", ] diff --git a/pacquet/crates/cli/tests/snapshots/install__should_install_index_files.snap b/pacquet/crates/cli/tests/snapshots/install__should_install_index_files.snap index a57cb02d10..3487693aaf 100644 --- a/pacquet/crates/cli/tests/snapshots/install__should_install_index_files.snap +++ b/pacquet/crates/cli/tests/snapshots/install__should_install_index_files.snap @@ -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 diff --git a/pacquet/crates/executor/src/lifecycle.rs b/pacquet/crates/executor/src/lifecycle.rs index 8eaf09ce9a..7be97e418d 100644 --- a/pacquet/crates/executor/src/lifecycle.rs +++ b/pacquet/crates/executor/src/lifecycle.rs @@ -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[].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 + /// . + /// 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( 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( 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, }, diff --git a/pacquet/crates/executor/src/lifecycle/tests.rs b/pacquet/crates/executor/src/lifecycle/tests.rs index 2eab7efb89..569511d791 100644 --- a/pacquet/crates/executor/src/lifecycle/tests.rs +++ b/pacquet/crates/executor/src/lifecycle/tests.rs @@ -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::(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 +/// +/// 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> = 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 = HashMap::new(); + let extra_bin_paths: Vec = 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::(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::(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::(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::(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::(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::(opts).expect_err("malformed JSON must fail"); diff --git a/pacquet/crates/lockfile/src/snapshot_entry.rs b/pacquet/crates/lockfile/src/snapshot_entry.rs index 99e58a20d0..88a1b07a45 100644 --- a/pacquet/crates/lockfile/src/snapshot_entry.rs +++ b/pacquet/crates/lockfile/src/snapshot_entry.rs @@ -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>, #[serde(skip_serializing_if = "Option::is_none")] pub patched: Option, + + /// `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[].optional`. Pacquet trusts the precomputed + /// flag rather than re-deriving from the importer graph, + /// matching upstream's `lockfileToDepGraph` at + /// . + /// + /// `BuildModules` consults this flag to decide whether a failed + /// build should be swallowed and reported via + /// `pnpm:skipped-optional-dependency` (mirrors + /// ). + #[serde(default, skip_serializing_if = "is_false")] + pub optional: bool, +} + +fn is_false(value: &bool) -> bool { + !*value } diff --git a/pacquet/crates/lockfile/src/snapshot_entry/tests.rs b/pacquet/crates/lockfile/src/snapshot_entry/tests.rs new file mode 100644 index 0000000000..e000671c57 --- /dev/null +++ b/pacquet/crates/lockfile/src/snapshot_entry/tests.rs @@ -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 +/// ; +/// see `snapshots[].optional` in the upstream type at +/// . +#[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}", + ); +} diff --git a/pacquet/crates/package-manager/src/build_modules.rs b/pacquet/crates/package-manager/src/build_modules.rs index b7e351b301..1da4c57fd1 100644 --- a/pacquet/crates/package-manager/src/build_modules.rs +++ b/pacquet/crates/package-manager/src/build_modules.rs @@ -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::(RunPostinstallHooks { + let optional = snapshots.get(&snapshot_key).is_some_and(|entry| entry.optional); + + let result = run_postinstall_hooks::(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)); + } } } diff --git a/pacquet/crates/package-manager/src/build_modules/tests.rs b/pacquet/crates/package-manager/src/build_modules/tests.rs index 4a5f7b1b58..360362b0f0 100644 --- a/pacquet/crates/package-manager/src/build_modules/tests.rs +++ b/pacquet/crates/package-manager/src/build_modules/tests.rs @@ -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 +/// . +/// +/// 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> = 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::() + .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 +/// . +/// +/// 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::() + .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 diff --git a/pacquet/crates/package-manager/src/build_sequence/tests.rs b/pacquet/crates/package-manager/src/build_sequence/tests.rs index c2125fe36d..08c49dd3e9 100644 --- a/pacquet/crates/package-manager/src/build_sequence/tests.rs +++ b/pacquet/crates/package-manager/src/build_sequence/tests.rs @@ -34,6 +34,7 @@ fn snap(deps: &[(&str, &str)]) -> SnapshotEntry { optional_dependencies: None, transitive_peer_dependencies: None, patched: None, + optional: false, } } diff --git a/pacquet/crates/package-manager/src/build_snapshot.rs b/pacquet/crates/package-manager/src/build_snapshot.rs index b012a4feba..89f0864eb9 100644 --- a/pacquet/crates/package-manager/src/build_snapshot.rs +++ b/pacquet/crates/package-manager/src/build_snapshot.rs @@ -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 }) diff --git a/pacquet/crates/package-manager/src/install/tests.rs b/pacquet/crates/package-manager/src/install/tests.rs index b0dd31ed4c..8b70093f24 100644 --- a/pacquet/crates/package-manager/src/install/tests.rs +++ b/pacquet/crates/package-manager/src/install/tests.rs @@ -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 +/// . +/// +/// 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::() + .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)); +} diff --git a/pacquet/crates/reporter/src/lib.rs b/pacquet/crates/reporter/src/lib.rs index 54bc727ee6..687b6cb21b 100644 --- a/pacquet/crates/reporter/src/lib.rs +++ b/pacquet/crates/reporter/src/lib.rs @@ -149,6 +149,19 @@ pub enum LogEvent { /// Emit site: . #[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: . + /// Emit site (build_failure): . + #[serde(rename = "pnpm:skipped-optional-dependency")] + SkippedOptionalDependency(SkippedOptionalDependencyLog), } /// `pnpm:context` payload. @@ -517,6 +530,60 @@ pub struct IgnoredScriptsLog { pub package_names: Vec, } +/// `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, + 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 +/// . +#[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 diff --git a/pacquet/crates/reporter/src/tests.rs b/pacquet/crates/reporter/src/tests.rs index ad1b421f03..3d4af336d5 100644 --- a/pacquet/crates/reporter/src/tests.rs +++ b/pacquet/crates/reporter/src/tests.rs @@ -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 +/// . +#[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() { diff --git a/pacquet/tasks/registry-mock/launch.mjs b/pacquet/tasks/registry-mock/launch.mjs new file mode 100644 index 0000000000..80c521a462 --- /dev/null +++ b/pacquet/tasks/registry-mock/launch.mjs @@ -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 +// . +// +// 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) + }) +} diff --git a/pacquet/tasks/registry-mock/package.json b/pacquet/tasks/registry-mock/package.json index 080ef15323..191e5d1999 100644 --- a/pacquet/tasks/registry-mock/package.json +++ b/pacquet/tasks/registry-mock/package.json @@ -1,5 +1,5 @@ { "devDependencies": { - "@pnpm/registry-mock": "^3.16.0" + "@pnpm/registry-mock": "^6.0.0" } } diff --git a/pacquet/tasks/registry-mock/pnpm-lock.yaml b/pacquet/tasks/registry-mock/pnpm-lock.yaml index c3a676bac3..2c0a64665d 100644 --- a/pacquet/tasks/registry-mock/pnpm-lock.yaml +++ b/pacquet/tasks/registry-mock/pnpm-lock.yaml @@ -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 diff --git a/pacquet/tasks/registry-mock/pnpm-workspace.yaml b/pacquet/tasks/registry-mock/pnpm-workspace.yaml index 10ed64a56b..665d63d185 100644 --- a/pacquet/tasks/registry-mock/pnpm-workspace.yaml +++ b/pacquet/tasks/registry-mock/pnpm-workspace.yaml @@ -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 diff --git a/pacquet/tasks/registry-mock/src/mock_instance.rs b/pacquet/tasks/registry-mock/src/mock_instance.rs index 2bcbb4167f..4d7c89f756 100644 --- a/pacquet/tasks/registry-mock/src/mock_instance.rs +++ b/pacquet/tasks/registry-mock/src/mock_instance.rs @@ -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) diff --git a/pacquet/tasks/registry-mock/src/node_registry_mock.rs b/pacquet/tasks/registry-mock/src/node_registry_mock.rs index 52b05829b0..3be017f768 100644 --- a/pacquet/tasks/registry-mock/src/node_registry_mock.rs +++ b/pacquet/tasks/registry-mock/src/node_registry_mock.rs @@ -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 = OnceLock::new(); +static LAUNCH_SCRIPT: OnceLock = 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 `. 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 }