Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6a85968

Browse filesBrowse files
authored
feat(pacquet): port the stage command and add its -/stage endpoints to pnpr (#12922)
Port `pnpm stage` (publish, list, view, approve, reject, download) to the Rust CLI, mirroring the TypeScript command's flags, error codes, endpoints, and output. `stage publish` reuses the publish pipeline (PublishArgs is split into a reusable PublishFlags, and the pipeline returns summaries instead of printing); approve/reject drive the shared OTP / web-auth flow; download summarizes the tarball with the same traversal protections as the TypeScript implementation. Also fixes the staged-publish route in pacquet-publish to POST -/stage/package/:pkg (libnpmpublish's stage route) instead of the regular packument PUT — previously unreachable because the CLI hardcoded stage: false. pnpr grows the server half: POST /-/stage/package/:pkg validates and authorizes like a direct publish and holds the document under a UUID; list/view/tarball inspect held records; approve replays the document through the regular validate/stage/commit flow; reject deletes it. Records persist under a reserved .staged/ namespace on the fs and S3 backends, with stage-id validation ahead of any path or object key. Shared plumbing: the capped response-body reader moves from the dist-tag command into pacquet-network, and pacquet-pack exports its en-locale path sort for the tarball summary. Also fixes a broken test on main (semantic conflict between #12910 and #12914): State::init no longer persists a scaffolded root package.json when a pnpm-workspace.yaml sits next to the missing manifest, so a verify-deps-before-run install can't turn the workspace root into a selectable project with the init template's failing test script.
1 parent 84e3d3b commit 6a85968
Copy full SHA for 6a85968

32 files changed

+3,559-218Lines changed: 3559 additions & 218 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.changeset/stage-list-page-cap.md‎

Copy file name to clipboard
+6Lines changed: 6 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@pnpm/releasing.commands": patch
3+
"pnpm": patch
4+
---
5+
6+
`pnpm stage list` now stops paginating after a fail-safe cap of 1000 pages, so a misbehaving registry cannot keep the command looping forever.
Collapse file

‎pnpm/crates/cli/Cargo.toml‎

Copy file name to clipboardExpand all lines: pnpm/crates/cli/Cargo.toml
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pacquet-hooks = { workspace = true }
3131
pacquet-lockfile = { workspace = true }
3232
pacquet-modules-yaml = { workspace = true }
3333
pacquet-network = { workspace = true }
34+
pacquet-network-web-auth = { workspace = true }
3435
pacquet-config = { workspace = true }
3536
pacquet-config-parse-overrides = { workspace = true }
3637
pacquet-default-reporter = { workspace = true }
@@ -61,6 +62,7 @@ clap = { workspace = true }
6162
derive_more = { workspace = true }
6263
dialoguer = { workspace = true }
6364
dunce = { workspace = true }
65+
flate2 = { workspace = true }
6466
futures-util = { workspace = true }
6567
home = { workspace = true }
6668
indexmap = { workspace = true }
@@ -76,6 +78,7 @@ rmp-serde = { workspace = true }
7678
serde = { workspace = true }
7779
serde_json = { workspace = true }
7880
tabled = { workspace = true }
81+
tar = { workspace = true }
7982
tempfile = { workspace = true }
8083
tokio = { workspace = true }
8184
tracing = { workspace = true }
@@ -91,20 +94,17 @@ wax = { workspace = true }
9194
windows-sys = { workspace = true, features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
9295

9396
[dev-dependencies]
94-
pacquet-network-web-auth = { workspace = true }
9597
pacquet-network-web-auth-testing = { workspace = true }
9698
pacquet-testing-utils = { workspace = true }
9799
pnpr = { workspace = true }
98100

99101
assert_cmd = { workspace = true }
100102
command-extra = { workspace = true }
101103
dialoguer = { workspace = true }
102-
flate2 = { workspace = true }
103104
insta = { workspace = true }
104105
mockito = { workspace = true }
105106
pretty_assertions = { workspace = true }
106107
serde-saphyr = { workspace = true }
107-
tar = { workspace = true }
108108
text-block-macros = { workspace = true }
109109
walkdir = { workspace = true }
110110

Collapse file

‎pnpm/crates/cli/src/cli_args.rs‎

Copy file name to clipboardExpand all lines: pnpm/crates/cli/src/cli_args.rs
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub mod search;
5555
pub mod self_update;
5656
pub mod set_script;
5757
pub mod setup;
58+
pub mod stage;
5859
pub mod stop;
5960
pub mod store;
6061
pub mod supported_architectures;
Collapse file

‎pnpm/crates/cli/src/cli_args/cli_command.rs‎

Copy file name to clipboardExpand all lines: pnpm/crates/cli/src/cli_args/cli_command.rs
+5-1Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use super::{
5151
self_update::SelfUpdateArgs,
5252
set_script::SetScriptArgs,
5353
setup::SetupArgs,
54+
stage::StageArgs,
5455
stop::StopArgs,
5556
store::StoreCommand,
5657
unlink::UnlinkArgs,
@@ -232,7 +233,7 @@ impl CliArgs {
232233
}
233234

234235
fn validate_report_summary_global_option(&self) -> Result<(), clap::Error> {
235-
if matches!(self.command, CliCommand::Publish(_)) {
236+
if matches!(self.command, CliCommand::Publish(_) | CliCommand::Stage(_)) {
236237
return Ok(());
237238
}
238239
self.validate_run_scoped_global_option("--report-summary")
@@ -287,6 +288,9 @@ pub enum CliCommand {
287288
Pack(PackArgs),
288289
/// Publish a package to the registry
289290
Publish(PublishArgs),
291+
/// Stage packages for publishing, deferring proof-of-presence (2FA) to a
292+
/// later point in time.
293+
Stage(StageArgs),
290294
/// Removes packages from `node_modules` and from the project's `package.json`.
291295
// Unlike npm, pnpm does not treat "r" as an alias of "remove" to avoid
292296
// confusion with "run" and "recursive".
Collapse file

‎pnpm/crates/cli/src/cli_args/dispatch.rs‎

Copy file name to clipboardExpand all lines: pnpm/crates/cli/src/cli_args/dispatch.rs
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ fn route<'a>(command: CliCommand, ctx: &RunCtx<'a>) -> miette::Result<CommandFut
308308
CliCommand::Rebuild(args) => dispatch_install::rebuild(ctx, args),
309309
CliCommand::Pack(args) => dispatch_query::pack(ctx, &args),
310310
CliCommand::Publish(args) => dispatch_query::publish(ctx, args),
311+
CliCommand::Stage(args) => dispatch_query::stage(ctx, args),
311312
CliCommand::Remove(args) => dispatch_install::remove(ctx, args),
312313
CliCommand::Patch(args) => dispatch_install::patch(ctx, args),
313314
CliCommand::PatchCommit(args) => dispatch_install::patch_commit(ctx, args),
Collapse file

‎pnpm/crates/cli/src/cli_args/dispatch_query.rs‎

Copy file name to clipboardExpand all lines: pnpm/crates/cli/src/cli_args/dispatch_query.rs
+41-1Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use super::{
2929
search::SearchArgs,
3030
self_update::SelfUpdateArgs,
3131
setup::SetupArgs,
32+
stage::StageArgs,
3233
store::StoreCommand,
3334
why::WhyArgs,
3435
with::WithArgs,
@@ -224,7 +225,7 @@ pub(super) fn publish<'a>(
224225
let config = (ctx.config)()?;
225226
let dir = ctx.dir;
226227
let recursive = ctx.recursive;
227-
args.report_summary |= ctx.recursive_report_summary;
228+
args.flags.report_summary |= ctx.recursive_report_summary;
228229
Ok(match ctx.reporter {
229230
ReporterType::Default | ReporterType::AppendOnly => {
230231
Box::pin(args.run::<DefaultReporter>(dir, config, recursive))
@@ -234,6 +235,45 @@ pub(super) fn publish<'a>(
234235
})
235236
}
236237

238+
/// `stage` shares `publish`'s dispatch shape: the values are read off `ctx`
239+
/// before the boxed future so it captures only owned/concrete values (see
240+
/// [`publish`]), and the subcommand's output is printed here, sanitized,
241+
/// mirroring pnpm's `handler` → CLI print split.
242+
pub(super) fn stage<'a>(
243+
ctx: &RunCtx<'a>,
244+
mut args: StageArgs,
245+
) -> miette::Result<CommandFuture<'a>> {
246+
let config = (ctx.config)()?;
247+
let dir = ctx.dir;
248+
let recursive = ctx.recursive;
249+
args.flags.report_summary |= ctx.recursive_report_summary;
250+
async fn print_output<Reporter: pacquet_reporter::Reporter>(
251+
args: StageArgs,
252+
dir: &std::path::Path,
253+
config: &Config,
254+
recursive: bool,
255+
) -> miette::Result<()> {
256+
if let Some(output) = args.run::<Reporter>(dir, config, recursive).await? {
257+
let output = super::sanitize::sanitize(&output);
258+
if !output.is_empty() {
259+
println!("{output}");
260+
}
261+
}
262+
Ok(())
263+
}
264+
Ok(match ctx.reporter {
265+
ReporterType::Default | ReporterType::AppendOnly => {
266+
Box::pin(print_output::<DefaultReporter>(args, dir, config, recursive))
267+
}
268+
ReporterType::Ndjson => {
269+
Box::pin(print_output::<NdjsonReporter>(args, dir, config, recursive))
270+
}
271+
ReporterType::Silent => {
272+
Box::pin(print_output::<SilentReporter>(args, dir, config, recursive))
273+
}
274+
})
275+
}
276+
237277
pub(super) fn bin<'a>(ctx: &RunCtx<'a>, args: BinArgs) -> miette::Result<CommandFuture<'a>> {
238278
args.run(ctx.dir, (ctx.config)()?)?;
239279
Ok(Box::pin(std::future::ready(Ok(()))))
Collapse file

‎pnpm/crates/cli/src/cli_args/dist_tag.rs‎

Copy file name to clipboardExpand all lines: pnpm/crates/cli/src/cli_args/dist_tag.rs
+13-41Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use super::sanitize;
22
use clap::Args;
33
use derive_more::{Display, Error};
4-
use futures_util::StreamExt as _;
54
use miette::{Context, Diagnostic, IntoDiagnostic};
65
use node_semver::Version;
76
use pacquet_config::Config;
87
use pacquet_network::{
9-
NetworkSettings, RetryOpts, ThrottledClient, encode_uri_component, redact_url_credentials,
10-
retry_async, send_with_retry,
8+
LimitedBody, NetworkSettings, RetryOpts, ThrottledClient, encode_uri_component,
9+
read_limited_body, redact_url_credentials, retry_async, send_with_retry,
1110
};
1211
use pacquet_resolving_npm_resolver::pick_registry_for_package;
1312
use pacquet_resolving_parse_wanted_dependency::parse_wanted_dependency;
@@ -428,7 +427,7 @@ async fn write_error_from_response(response: Response, action: String) -> miette
428427
})?;
429428
let web_otp_challenge =
430429
if body.truncated { None } else { parse_web_otp_challenge(&body.bytes) };
431-
let body = body.into_display_string();
430+
let body = body_display_string(&body);
432431
if status == StatusCode::UNAUTHORIZED {
433432
if let Some(challenge) = web_otp_challenge {
434433
return Err(DistTagError::WebOtpRequired {
@@ -501,45 +500,18 @@ where
501500
.into()
502501
}
503502

504-
struct LimitedBody {
505-
bytes: Vec<u8>,
506-
truncated: bool,
507-
}
508-
509-
impl LimitedBody {
510-
fn into_display_string(self) -> String {
511-
let body = String::from_utf8_lossy(&self.bytes);
512-
let mut body = sanitize::sanitize(&body).into_owned();
513-
if self.truncated {
514-
if !body.is_empty() && !body.chars().next_back().is_some_and(char::is_whitespace) {
515-
body.push(' ');
516-
}
517-
body.push_str("(response body truncated)");
518-
}
519-
body
520-
}
521-
}
522-
523-
async fn read_limited_body(
524-
response: Response,
525-
limit: usize,
526-
) -> Result<LimitedBody, reqwest::Error> {
527-
let header_exceeds_limit =
528-
response.content_length().is_some_and(|length| length > limit as u64);
529-
let mut bytes = Vec::new();
530-
let mut truncated = header_exceeds_limit;
531-
let mut stream = response.bytes_stream();
532-
while let Some(chunk) = stream.next().await {
533-
let chunk = chunk?;
534-
let remaining = limit.saturating_sub(bytes.len());
535-
if chunk.len() > remaining {
536-
bytes.extend_from_slice(&chunk[..remaining]);
537-
truncated = true;
538-
break;
503+
/// Render a capped response body for an error message: lossy UTF-8,
504+
/// sanitized, with a truncation note when the cap was hit.
505+
pub(super) fn body_display_string(body: &LimitedBody) -> String {
506+
let text = String::from_utf8_lossy(&body.bytes);
507+
let mut text = sanitize::sanitize(&text).into_owned();
508+
if body.truncated {
509+
if !text.is_empty() && !text.chars().next_back().is_some_and(char::is_whitespace) {
510+
text.push(' ');
539511
}
540-
bytes.extend_from_slice(&chunk);
512+
text.push_str("(response body truncated)");
541513
}
542-
Ok(LimitedBody { bytes, truncated })
514+
text
543515
}
544516

545517
#[derive(Deserialize)]

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.