From ab11042ad3a4504ba7c720b7dbf4a022f989c7af Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Fri, 27 Feb 2026 01:45:28 +0100 Subject: [PATCH 1/4] feat(pnpm): Add filter argument support - Add support for pnpm global filter arguments (`--filter`) across all pnpm commands (`list`, `outdated`, `install`, etc.). - Merge pnpm filters with native command arguments (`--filter` + custom args like `--depth`). - Implement proper handling for `Vec` (text filters) and `Vec` (OS-specific filters). - Add unit tests for `merge_pnpm_args` and CLI parsing to validate expected behavior. This enables users to apply workspace-specific filtering directly via `--filter @scope` before running pnpm commands, reducing token overhead by excluding irrelevant packages/dependencies from output. Fixes: #259 FIXME: Add notes for typechecking/building with workspaces when filters are applied. --- get | 12 +++++ package-lock.json | 6 +++ src/main.rs | 101 +++++++++++++++++++++++++++++++++--- test-results/.last-run.json | 4 ++ 4 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 get create mode 100644 package-lock.json create mode 100644 test-results/.last-run.json diff --git a/get b/get new file mode 100644 index 00000000..d0f30953 --- /dev/null +++ b/get @@ -0,0 +1,12 @@ +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "identity", + "Host": "httpbin.org", + "User-Agent": "Wget/1.25.0", + "X-Amzn-Trace-Id": "Root=1-69b2dfc9-1caefd3629f546f2584d3aef" + }, + "origin": "90.110.73.127", + "url": "https://httpbin.org/get" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..da32b56f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "rtk", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/main.rs b/src/main.rs index 289de863..898c5616 100644 --- a/src/main.rs +++ b/src/main.rs @@ -204,6 +204,10 @@ enum Commands { /// pnpm commands with ultra-compact output Pnpm { + /// pnpm filter arguments (can be repeated: --filter @app1 --filter @app2) + #[arg(long, short = 'F')] + filter: Vec, + #[command(subcommand)] command: PnpmCommands, }, @@ -1186,6 +1190,24 @@ fn shell_split(input: &str) -> Vec { tokens } +/// Merge pnpm global filters args with other ones +fn merge_pnpm_args(filters: &[String], args: &[String]) -> Vec { + filters + .iter() + .map(|filter| format!("--filter={}", filter)) + .chain(args.iter().map(|arg| arg.to_string())) + .collect() +} + +/// Merge pnpm global filters args with other ones +fn merge_pnpm_args_os(filters: &[String], args: &[OsString]) -> Vec { + filters + .iter() + .map(|filter| OsString::from(format!("--filter={}", filter))) + .chain(args.iter().map(|arg| arg.to_os_string())) + .collect() +} + fn main() -> Result<()> { // Fire-and-forget telemetry ping (1/day, non-blocking) telemetry::maybe_ping(); @@ -1408,17 +1430,25 @@ fn main() -> Result<()> { psql_cmd::run(&args, cli.verbose)?; } - Commands::Pnpm { command } => match command { + Commands::Pnpm { filter, command } => match command { PnpmCommands::List { depth, args } => { - pnpm_cmd::run(pnpm_cmd::PnpmCommand::List { depth }, &args, cli.verbose)?; + pnpm_cmd::run( + pnpm_cmd::PnpmCommand::List { depth }, + &merge_pnpm_args(&filter, &args), + cli.verbose, + )?; } PnpmCommands::Outdated { args } => { - pnpm_cmd::run(pnpm_cmd::PnpmCommand::Outdated, &args, cli.verbose)?; + pnpm_cmd::run( + pnpm_cmd::PnpmCommand::Outdated, + &merge_pnpm_args(&filter, &args), + cli.verbose, + )?; } PnpmCommands::Install { packages, args } => { pnpm_cmd::run( pnpm_cmd::PnpmCommand::Install { packages }, - &args, + &merge_pnpm_args(&filter, &args), cli.verbose, )?; } @@ -1426,13 +1456,14 @@ fn main() -> Result<()> { let mut build_args: Vec = vec!["build".into()]; build_args.extend(args); let os_args: Vec = build_args.into_iter().map(OsString::from).collect(); - pnpm_cmd::run_passthrough(&os_args, cli.verbose)?; + pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &os_args), cli.verbose)?; } PnpmCommands::Typecheck { args } => { + // FIXME: if filters are present, we should find out which workspaces are typechecked before running tsc tsc_cmd::run(&args, cli.verbose)?; } PnpmCommands::Other(args) => { - pnpm_cmd::run_passthrough(&args, cli.verbose)?; + pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &args), cli.verbose)?; } }, @@ -2487,4 +2518,62 @@ mod tests { } } } + + #[test] + fn test_merge_filters_with_args() { + let filters = vec!["@app1".to_string(), "@app2".to_string()]; + let args = vec![ + "--filter=@app3".to_string(), + "--depth=0".to_string(), + "--no-verbose".to_string(), + ]; + let expected_args = vec![ + "--filter=@app1", + "--filter=@app2", + "--filter=@app3", + "--depth=0", + "--no-verbose", + ]; + assert_eq!(merge_pnpm_args(&filters, &args), expected_args); + } + + #[test] + fn test_merge_filters_with_args_os() { + let filters = vec!["@app1".to_string()]; + let args = vec![OsString::from("--depth=0")]; + let expected_args = vec![ + OsString::from("--filter=@app1"), + OsString::from("--depth=0"), + ]; + assert_eq!(merge_pnpm_args_os(&filters, &args), expected_args); + } + + #[test] + fn test_pnpm_subcommand_with_filter() { + let cli = Cli::try_parse_from(["rtk", "pnpm", "--filter", "@app1", "list"]).unwrap(); + match cli.command { + Commands::Pnpm { + filter, + command: PnpmCommands::List { depth, args }, + } => { + assert_eq!(depth, 0); + assert_eq!(filter, vec!["@app1"]); + assert!(args.is_empty()); + } + _ => panic!("Expected Pnpm List command"), + } + } + + #[test] + fn test_pnpm_subcommand_with_short_filter() { + // -F is the short form of --filter in pnpm + let cli = + Cli::try_parse_from(["rtk", "pnpm", "-F", "@app1", "-F", "@app2", "list"]).unwrap(); + match cli.command { + Commands::Pnpm { filter, .. } => { + assert_eq!(filter, vec!["@app1", "@app2"]); + } + _ => panic!("Expected Pnpm command"), + } + } } diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 00000000..5fca3f84 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file From 13e57a0e0054203b40eda47da7795d92aaac3ac7 Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Sat, 7 Mar 2026 00:00:46 +0100 Subject: [PATCH 2/4] feat: take review into account - Add validate_pnpm_filters() to warn when filters used with unsupported commands - Add comprehensive tests for all scenarios - Improve comments and use idiomatic rust --- src/main.rs | 222 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 181 insertions(+), 41 deletions(-) diff --git a/src/main.rs b/src/main.rs index 898c5616..5b62e0fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1190,24 +1190,49 @@ fn shell_split(input: &str) -> Vec { tokens } -/// Merge pnpm global filters args with other ones +/// Merge pnpm global filters args with other ones for standard String-based commands fn merge_pnpm_args(filters: &[String], args: &[String]) -> Vec { filters .iter() .map(|filter| format!("--filter={}", filter)) - .chain(args.iter().map(|arg| arg.to_string())) + .chain(args.iter().cloned()) .collect() } -/// Merge pnpm global filters args with other ones +/// Merge pnpm global filters args with other ones, using OsString for passthrough compatibility fn merge_pnpm_args_os(filters: &[String], args: &[OsString]) -> Vec { filters .iter() .map(|filter| OsString::from(format!("--filter={}", filter))) - .chain(args.iter().map(|arg| arg.to_os_string())) + .chain(args.iter().cloned()) .collect() } +/// Validate that pnpm filters are only used in the global context, not before subcommands like build or tsc. +fn validate_pnpm_filters(filters: &[String], command: &PnpmCommands) -> Option { + // Check if this is a Build or Typecheck command with filters + match command { + PnpmCommands::Build { .. } | PnpmCommands::Typecheck { .. } => { + // FIXME: if filters are present, we should find out which workspaces are build before running build + if !filters.is_empty() { + let cmd_name = match command { + PnpmCommands::Build { .. } => "build", + PnpmCommands::Typecheck { .. } => "tsc", + _ => unreachable!(), + }; + let msg = format!( + "[rtk] warning: --filter is not yet supported for pnpm {}, filters preceding the subcommand will be ignored", + cmd_name + ); + eprintln!("{}", msg); + return Some(msg); + } + None + } + _ => None, + } +} + fn main() -> Result<()> { // Fire-and-forget telemetry ping (1/day, non-blocking) telemetry::maybe_ping(); @@ -1430,42 +1455,49 @@ fn main() -> Result<()> { psql_cmd::run(&args, cli.verbose)?; } - Commands::Pnpm { filter, command } => match command { - PnpmCommands::List { depth, args } => { - pnpm_cmd::run( - pnpm_cmd::PnpmCommand::List { depth }, - &merge_pnpm_args(&filter, &args), - cli.verbose, - )?; - } - PnpmCommands::Outdated { args } => { - pnpm_cmd::run( - pnpm_cmd::PnpmCommand::Outdated, - &merge_pnpm_args(&filter, &args), - cli.verbose, - )?; + Commands::Pnpm { filter, command } => { + // Warns user if filters are used with unsupported subcommands like build or typecheck + if let Some(warning) = validate_pnpm_filters(&filter, &command) { + eprintln!("{}", warning); } - PnpmCommands::Install { packages, args } => { - pnpm_cmd::run( - pnpm_cmd::PnpmCommand::Install { packages }, - &merge_pnpm_args(&filter, &args), - cli.verbose, - )?; - } - PnpmCommands::Build { args } => { - let mut build_args: Vec = vec!["build".into()]; - build_args.extend(args); - let os_args: Vec = build_args.into_iter().map(OsString::from).collect(); - pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &os_args), cli.verbose)?; - } - PnpmCommands::Typecheck { args } => { - // FIXME: if filters are present, we should find out which workspaces are typechecked before running tsc - tsc_cmd::run(&args, cli.verbose)?; - } - PnpmCommands::Other(args) => { - pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &args), cli.verbose)?; + + match command { + PnpmCommands::List { depth, args } => { + pnpm_cmd::run( + pnpm_cmd::PnpmCommand::List { depth }, + &merge_pnpm_args(&filter, &args), + cli.verbose, + )?; + } + PnpmCommands::Outdated { args } => { + pnpm_cmd::run( + pnpm_cmd::PnpmCommand::Outdated, + &merge_pnpm_args(&filter, &args), + cli.verbose, + )?; + } + PnpmCommands::Install { packages, args } => { + pnpm_cmd::run( + pnpm_cmd::PnpmCommand::Install { packages }, + &merge_pnpm_args(&filter, &args), + cli.verbose, + )?; + } + PnpmCommands::Build { args } => { + let mut build_args: Vec = vec!["build".into()]; + build_args.extend(args); + let os_args: Vec = + build_args.into_iter().map(OsString::from).collect(); + pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &os_args), cli.verbose)?; + } + PnpmCommands::Typecheck { args } => { + tsc_cmd::run(&args, cli.verbose)?; + } + PnpmCommands::Other(args) => { + pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &args), cli.verbose)?; + } } - }, + } Commands::Err { command } => { let cmd = command.join(" "); @@ -2519,6 +2551,14 @@ mod tests { } } + #[test] + fn test_merge_filters_with_no_args() { + let filters = vec![]; + let args = vec!["--depth=0".to_string(), "--no-verbose".to_string()]; + let expected_args = vec!["--depth=0", "--no-verbose"]; + assert_eq!(merge_pnpm_args(&filters, &args), expected_args); + } + #[test] fn test_merge_filters_with_args() { let filters = vec!["@app1".to_string(), "@app2".to_string()]; @@ -2537,6 +2577,14 @@ mod tests { assert_eq!(merge_pnpm_args(&filters, &args), expected_args); } + #[test] + fn test_merge_filters_with_no_args_os() { + let filters = vec![]; + let args = vec![OsString::from("--depth=0")]; + let expected_args = vec![OsString::from("--depth=0")]; + assert_eq!(merge_pnpm_args_os(&filters, &args), expected_args); + } + #[test] fn test_merge_filters_with_args_os() { let filters = vec!["@app1".to_string()]; @@ -2550,15 +2598,22 @@ mod tests { #[test] fn test_pnpm_subcommand_with_filter() { - let cli = Cli::try_parse_from(["rtk", "pnpm", "--filter", "@app1", "list"]).unwrap(); + let cli = Cli::try_parse_from([ + "rtk", "pnpm", "--filter", "@app1", "--filter", "@app2", "list", "--filter", "@app3", + "--filter", "@app4", "--prod", + ]) + .unwrap(); match cli.command { Commands::Pnpm { filter, command: PnpmCommands::List { depth, args }, } => { assert_eq!(depth, 0); - assert_eq!(filter, vec!["@app1"]); - assert!(args.is_empty()); + assert_eq!(filter, vec!["@app1", "@app2"]); + assert_eq!( + args, + vec!["--filter", "@app3", "--filter", "@app4", "--prod"] + ); } _ => panic!("Expected Pnpm List command"), } @@ -2576,4 +2631,89 @@ mod tests { _ => panic!("Expected Pnpm command"), } } + + #[test] + fn test_pnpm_build_without_filters() { + let cli = Cli::try_parse_from([ + "rtk", "pnpm", "build", "--filter", "@app3", "--filter", "@app4", + ]) + .unwrap(); + match cli.command { + Commands::Pnpm { filter, command } => { + let warning = validate_pnpm_filters(&filter, &command); + + assert!(filter.is_empty()); + assert!(warning.is_none()) + } + _ => panic!("Expected Pnpm Build command"), + } + } + + #[test] + fn test_pnpm_build_with_filters() { + let cli = Cli::try_parse_from([ + "rtk", "pnpm", "--filter", "@app1", "--filter", "@app2", "build", "--filter", "@app3", + "--filter", "@app4", + ]) + .unwrap(); + match cli.command { + Commands::Pnpm { filter, command } => { + let warning = validate_pnpm_filters(&filter, &command).unwrap(); + + assert_eq!(filter, vec!["@app1", "@app2"]); + assert_eq!(warning, "[rtk] warning: --filter is not yet supported for pnpm build, filters preceding the subcommand will be ignored") + } + _ => panic!("Expected Pnpm Build command"), + } + } + + #[test] + fn test_pnpm_typecheck_without_filters() { + let cli = Cli::try_parse_from([ + "rtk", + "pnpm", + "typecheck", + "--filter", + "@app3", + "--filter", + "@app4", + ]) + .unwrap(); + match cli.command { + Commands::Pnpm { filter, command } => { + let warning = validate_pnpm_filters(&filter, &command); + + assert!(filter.is_empty()); + assert!(warning.is_none()) + } + _ => panic!("Expected Pnpm Build command"), + } + } + + #[test] + fn test_pnpm_typecheck_with_filters() { + let cli = Cli::try_parse_from([ + "rtk", + "pnpm", + "--filter", + "@app1", + "--filter", + "@app2", + "typecheck", + "--filter", + "@app3", + "--filter", + "@app4", + ]) + .unwrap(); + match cli.command { + Commands::Pnpm { filter, command } => { + let warning = validate_pnpm_filters(&filter, &command).unwrap(); + + assert_eq!(filter, vec!["@app1", "@app2"]); + assert_eq!(warning, "[rtk] warning: --filter is not yet supported for pnpm tsc, filters preceding the subcommand will be ignored") + } + _ => panic!("Expected Pnpm Build command"), + } + } } From 2e6cd7d40678e113a52c655426ea4d84d535d60e Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Thu, 12 Mar 2026 23:42:18 +0100 Subject: [PATCH 3/4] refactor(pnpm): consolidate build into generic passthrough - Remove explicit PnpmCommands::Build variant (routed through Other) - Simplify filter validation for Typecheck only - Remove redundant warning eprintln!() call --- src/main.rs | 60 ++++++----------------------------------------------- 1 file changed, 6 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5b62e0fe..fc53e307 100644 --- a/src/main.rs +++ b/src/main.rs @@ -764,12 +764,6 @@ enum PnpmCommands { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, }, - /// Build (generic passthrough, no framework-specific filter) - Build { - /// Additional build arguments - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - args: Vec, - }, /// Typecheck (delegates to tsc filter) Typecheck { /// Additional typecheck arguments @@ -1208,15 +1202,14 @@ fn merge_pnpm_args_os(filters: &[String], args: &[OsString]) -> Vec { .collect() } -/// Validate that pnpm filters are only used in the global context, not before subcommands like build or tsc. +/// Validate that pnpm filters are only used in the global context, not before subcommands like tsc. fn validate_pnpm_filters(filters: &[String], command: &PnpmCommands) -> Option { // Check if this is a Build or Typecheck command with filters match command { - PnpmCommands::Build { .. } | PnpmCommands::Typecheck { .. } => { - // FIXME: if filters are present, we should find out which workspaces are build before running build + PnpmCommands::Typecheck { .. } => { + // FIXME: if filters are present, we should find out which workspaces are selected before running rtk dedicated commands if !filters.is_empty() { let cmd_name = match command { - PnpmCommands::Build { .. } => "build", PnpmCommands::Typecheck { .. } => "tsc", _ => unreachable!(), }; @@ -1224,7 +1217,6 @@ fn validate_pnpm_filters(filters: &[String], command: &PnpmCommands) -> Option Result<()> { } Commands::Pnpm { filter, command } => { - // Warns user if filters are used with unsupported subcommands like build or typecheck + // Warns user if filters are used with unsupported subcommands like typecheck if let Some(warning) = validate_pnpm_filters(&filter, &command) { eprintln!("{}", warning); } @@ -1483,14 +1475,9 @@ fn main() -> Result<()> { cli.verbose, )?; } - PnpmCommands::Build { args } => { - let mut build_args: Vec = vec!["build".into()]; - build_args.extend(args); - let os_args: Vec = - build_args.into_iter().map(OsString::from).collect(); - pnpm_cmd::run_passthrough(&merge_pnpm_args_os(&filter, &os_args), cli.verbose)?; - } PnpmCommands::Typecheck { args } => { + // FIXME: Currently ignores global filters (warned via validate_pnpm_filters). + // Future: Auto-detect which workspaces would be typechecked and apply filters appropriately. tsc_cmd::run(&args, cli.verbose)?; } PnpmCommands::Other(args) => { @@ -2632,41 +2619,6 @@ mod tests { } } - #[test] - fn test_pnpm_build_without_filters() { - let cli = Cli::try_parse_from([ - "rtk", "pnpm", "build", "--filter", "@app3", "--filter", "@app4", - ]) - .unwrap(); - match cli.command { - Commands::Pnpm { filter, command } => { - let warning = validate_pnpm_filters(&filter, &command); - - assert!(filter.is_empty()); - assert!(warning.is_none()) - } - _ => panic!("Expected Pnpm Build command"), - } - } - - #[test] - fn test_pnpm_build_with_filters() { - let cli = Cli::try_parse_from([ - "rtk", "pnpm", "--filter", "@app1", "--filter", "@app2", "build", "--filter", "@app3", - "--filter", "@app4", - ]) - .unwrap(); - match cli.command { - Commands::Pnpm { filter, command } => { - let warning = validate_pnpm_filters(&filter, &command).unwrap(); - - assert_eq!(filter, vec!["@app1", "@app2"]); - assert_eq!(warning, "[rtk] warning: --filter is not yet supported for pnpm build, filters preceding the subcommand will be ignored") - } - _ => panic!("Expected Pnpm Build command"), - } - } - #[test] fn test_pnpm_typecheck_without_filters() { let cli = Cli::try_parse_from([ From 3e5a70c6b5eaffb2e47cfff590db8255ba6fd458 Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Fri, 13 Mar 2026 00:34:01 +0100 Subject: [PATCH 4/4] chore: cleanup files added inadvertently --- get | 12 ------------ package-lock.json | 6 ------ test-results/.last-run.json | 4 ---- 3 files changed, 22 deletions(-) delete mode 100644 get delete mode 100644 package-lock.json delete mode 100644 test-results/.last-run.json diff --git a/get b/get deleted file mode 100644 index d0f30953..00000000 --- a/get +++ /dev/null @@ -1,12 +0,0 @@ -{ - "args": {}, - "headers": { - "Accept": "*/*", - "Accept-Encoding": "identity", - "Host": "httpbin.org", - "User-Agent": "Wget/1.25.0", - "X-Amzn-Trace-Id": "Root=1-69b2dfc9-1caefd3629f546f2584d3aef" - }, - "origin": "90.110.73.127", - "url": "https://httpbin.org/get" -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index da32b56f..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "rtk", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/test-results/.last-run.json b/test-results/.last-run.json deleted file mode 100644 index 5fca3f84..00000000 --- a/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "failed", - "failedTests": [] -} \ No newline at end of file