mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-19 04:02:32 -04:00
feat(pacquet): implement native recursive command (#13129)
Implemented native recursive command in Rust for the pacquet CLI. It prints the help and exits with status 1, matching the pnpm legacy behavior. Related to: #11633.
This commit is contained in:
5
.changeset/pacquet-recursive.md
Normal file
5
.changeset/pacquet-recursive.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"pacquet": minor
|
||||
---
|
||||
|
||||
Implemented native `recursive`, `multi`, and `m` commands in the Rust CLI.
|
||||
@@ -267,6 +267,9 @@ pub enum CliCommand {
|
||||
Access(AccessArgs),
|
||||
/// Initialize a package.json
|
||||
Init,
|
||||
/// Concurrently runs a command in all subdirectory projects.
|
||||
#[clap(visible_aliases = ["multi", "m"])]
|
||||
Recursive,
|
||||
/// Add a package
|
||||
Add(AddArgs),
|
||||
/// Install packages
|
||||
|
||||
@@ -316,6 +316,7 @@ fn route<'a>(command: CliCommand, ctx: &RunCtx<'a>) -> miette::Result<CommandFut
|
||||
match command {
|
||||
CliCommand::Access(args) => dispatch_query::access(ctx, args),
|
||||
CliCommand::Init => dispatch_script::init(ctx),
|
||||
CliCommand::Recursive => dispatch_query::recursive(ctx),
|
||||
CliCommand::Add(args) => dispatch_install::add(ctx, args),
|
||||
CliCommand::Install(args) => dispatch_install::install(ctx, args),
|
||||
CliCommand::InstallTest(args) => dispatch_install::install_test(ctx, args),
|
||||
|
||||
@@ -47,10 +47,20 @@ use super::{
|
||||
why::WhyArgs,
|
||||
with::WithArgs,
|
||||
};
|
||||
use clap::CommandFactory;
|
||||
use pacquet_config::Config;
|
||||
use pacquet_default_reporter::DefaultReporter;
|
||||
use pacquet_reporter::{NdjsonReporter, SilentReporter};
|
||||
|
||||
pub(super) fn recursive<'a>(_ctx: &RunCtx<'a>) -> miette::Result<CommandFuture<'a>> {
|
||||
Ok(Box::pin(async move {
|
||||
let mut cmd = crate::cli_args::CliArgs::command();
|
||||
let _ = cmd.find_subcommand_mut("recursive").expect("recursive subcommand").print_help();
|
||||
#[expect(clippy::exit, reason = "`recursive` exits non-zero, mirroring pnpm")]
|
||||
std::process::exit(1);
|
||||
}))
|
||||
}
|
||||
|
||||
// `outdated` is a read-only query: it prints a report to stdout and never
|
||||
// installs, so it has no reporter-typed install pipeline to dispatch on. It
|
||||
// reports back whether any dependency was outdated; process termination stays
|
||||
|
||||
@@ -457,6 +457,7 @@ fn command_name(command: &CliCommand) -> &'static str {
|
||||
match command {
|
||||
CliCommand::Access(_) => "access",
|
||||
CliCommand::Init => "init",
|
||||
CliCommand::Recursive => "recursive",
|
||||
CliCommand::Add(_) => "add",
|
||||
CliCommand::Install(_) => "install",
|
||||
CliCommand::InstallTest(_) => "install-test",
|
||||
|
||||
@@ -35,18 +35,78 @@ use std::{
|
||||
/// `cmd` must be the same [`Command`] the returned argv is parsed with
|
||||
/// (including the [`crate::boolean_negations`] augmentation), so the
|
||||
/// hidden `--no-<flag>` negations relocate like their positive forms.
|
||||
pub fn relocate_pre_subcommand_flags(cmd: &Command, argv: Vec<OsString>) -> Vec<OsString> {
|
||||
fn find_positional(
|
||||
argv: &[OsString],
|
||||
mut index: usize,
|
||||
top_level: &ArgTable,
|
||||
subcommand_union: &ArgTable,
|
||||
) -> Option<usize> {
|
||||
loop {
|
||||
let token = argv.get(index).and_then(|token| token.to_str())?;
|
||||
if token == "--" {
|
||||
return None;
|
||||
}
|
||||
if let Some(rest) = token.strip_prefix("--") {
|
||||
let (name, has_inline_value) =
|
||||
rest.split_once('=').map_or((rest, false), |(name, _)| (name, true));
|
||||
if let Some(consumes_value) = top_level.long_consumes_value(name) {
|
||||
index += token_width(consumes_value, has_inline_value);
|
||||
} else {
|
||||
let consumes_value = subcommand_union.long_consumes_value(name).unwrap_or(false);
|
||||
index += token_width(consumes_value, has_inline_value);
|
||||
}
|
||||
} else if let Some(rest) = token.strip_prefix('-').filter(|rest| !rest.is_empty()) {
|
||||
let short = rest.chars().next().expect("checked non-empty");
|
||||
let is_bare_short = rest.chars().count() == 1;
|
||||
if let Some(consumes_value) = top_level.short_consumes_value(short) {
|
||||
index += token_width(consumes_value && is_bare_short, false);
|
||||
} else {
|
||||
let consumes_value = subcommand_union.short_consumes_value(short).unwrap_or(false);
|
||||
index += token_width(consumes_value && is_bare_short, false);
|
||||
}
|
||||
} else {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Move pre-subcommand option tokens that belong to a subcommand's
|
||||
/// grammar to directly after the subcommand token. See the module docs.
|
||||
///
|
||||
/// `cmd` must be the same [`Command`] the returned argv is parsed with
|
||||
/// (including the [`crate::boolean_negations`] augmentation), so the
|
||||
/// hidden `--no-<flag>` negations relocate like their positive forms.
|
||||
pub fn relocate_pre_subcommand_flags(cmd: &Command, mut argv: Vec<OsString>) -> Vec<OsString> {
|
||||
let top_level = ArgTable::top_level(cmd);
|
||||
let subcommand_union = ArgTable::subcommand_union(cmd);
|
||||
|
||||
let mut current_idx = 1;
|
||||
while let Some(pos_idx) = find_positional(&argv, current_idx, &top_level, &subcommand_union) {
|
||||
if let Some(token) = argv.get(pos_idx).and_then(|t| t.to_str())
|
||||
&& matches!(token, "recursive" | "multi" | "m")
|
||||
&& find_positional(&argv, pos_idx + 1, &top_level, &subcommand_union).is_some()
|
||||
{
|
||||
argv[pos_idx] = OsString::from("--recursive");
|
||||
current_idx = pos_idx + 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let mut moved_indexes: HashSet<usize> = HashSet::new();
|
||||
let subcommand_index = find_positional(&argv, 1, &top_level, &subcommand_union);
|
||||
let Some(subcommand_index) = subcommand_index else {
|
||||
return argv;
|
||||
};
|
||||
|
||||
// Now we must re-calculate moved_indexes, because find_positional just skipped.
|
||||
let mut index = 1;
|
||||
let subcommand_index = loop {
|
||||
let Some(token) = argv.get(index).and_then(|token| token.to_str()) else {
|
||||
return argv;
|
||||
while index < subcommand_index {
|
||||
let Some(token) = argv.get(index).and_then(|t| t.to_str()) else {
|
||||
break;
|
||||
};
|
||||
if token == "--" {
|
||||
return argv;
|
||||
break;
|
||||
}
|
||||
if let Some(rest) = token.strip_prefix("--") {
|
||||
let (name, has_inline_value) =
|
||||
@@ -63,9 +123,6 @@ pub fn relocate_pre_subcommand_flags(cmd: &Command, argv: Vec<OsString>) -> Vec<
|
||||
}
|
||||
} else if let Some(rest) = token.strip_prefix('-').filter(|rest| !rest.is_empty()) {
|
||||
let short = rest.chars().next().expect("checked non-empty");
|
||||
// A multi-character short token (`-Cdir`, combined bools) has
|
||||
// its value / siblings attached, so it never consumes the
|
||||
// next token regardless of the short's arity.
|
||||
let is_bare_short = rest.chars().count() == 1;
|
||||
if let Some(consumes_value) = top_level.short_consumes_value(short) {
|
||||
index += token_width(consumes_value && is_bare_short, false);
|
||||
@@ -78,9 +135,9 @@ pub fn relocate_pre_subcommand_flags(cmd: &Command, argv: Vec<OsString>) -> Vec<
|
||||
index += width;
|
||||
}
|
||||
} else {
|
||||
break index;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if moved_indexes.is_empty() || cmd.find_subcommand(&argv[subcommand_index]).is_none() {
|
||||
return argv;
|
||||
|
||||
Reference in New Issue
Block a user