From 76649229f879f02d56c66289aaddb0fdb612ecfa Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Sun, 25 Oct 2026 13:13:48 +0900 Subject: [PATCH] fix(dlx): let npm infer executables from package metadata Pass scoped and versioned package specs unchanged to npm exec when no extra packages are requested. This avoids guessing cli for packages such as @angular/cli, which exposes ng instead. Update resolver tests and reuse the local echo package through a scoped npm alias for a small execution regression snapshot. Keep explicit --package and shell-mode behavior unchanged. --- .../fixtures/command_dlx_npm10/snapshots.toml | 1 + .../snapshots/command_dlx_npm10.md | 8 +++ .../vp_pm_cli/src/resolution/commands/dlx.rs | 49 ++++--------------- 3 files changed, 19 insertions(+), 39 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots.toml index 568eefbc96..3f5ae84dc6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots.toml @@ -8,6 +8,7 @@ steps = [ { argv = ["vp", "dlx", "--help"], comment = "should show help message", continue-on-failure = true }, { argv = ["vp", "dlx", "-s", "vp-test-echo", "hello"], comment = "should run vp-test-echo with npm exec", continue-on-failure = true }, { argv = ["vp", "dlx", "-s", "vp-test-echo@1.0.0", "hello"], comment = "should run specific version", continue-on-failure = true }, + { argv = ["vp", "dlx", "-s", "@vp-test/cli@npm:vp-test-echo@1.0.0", "hello"], comment = "npm selects the binary rather than using the scoped alias name" }, { argv = ["vp", "dlx", "-s", "-p", "vp-test-echo", "-p", "vp-test-upper", "vp-test-echo", "hello"], comment = "should run with multiple packages", continue-on-failure = true }, { argv = ["vp", "dlx", "-s", "-p", "vp-test-echo", "-p", "vp-test-upper", "-c", "echo hello-from-shell | vp-test-echo | vp-test-upper"], comment = "should run shell mode command (vp's own shell handles the pipe)", continue-on-failure = true }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots/command_dlx_npm10.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots/command_dlx_npm10.md index 8afdc25a35..a6ffc5b35a 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots/command_dlx_npm10.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dlx_npm10/snapshots/command_dlx_npm10.md @@ -39,6 +39,14 @@ should run specific version hello ``` +## `vp dlx -s @vp-test/cli@npm:vp-test-echo@1.0.0 hello` + +npm selects the binary rather than using the scoped alias name + +``` +hello +``` + ## `vp dlx -s -p vp-test-echo -p vp-test-upper vp-test-echo hello` should run with multiple packages diff --git a/crates/vp_pm_cli/src/resolution/commands/dlx.rs b/crates/vp_pm_cli/src/resolution/commands/dlx.rs index f85cf95d8e..095d40a993 100644 --- a/crates/vp_pm_cli/src/resolution/commands/dlx.rs +++ b/crates/vp_pm_cli/src/resolution/commands/dlx.rs @@ -52,7 +52,7 @@ impl Resolve for Npm { for package in &args.package { cmd.arg(vt_str::format!("--package={package}")); } - if !args.shell_mode && (!args.package.is_empty() || package_spec.contains('@')) { + if !args.shell_mode && !args.package.is_empty() { cmd.arg(vt_str::format!("--package={package_spec}")); } cmd.arg("--yes"); @@ -62,7 +62,7 @@ impl Resolve for Npm { if args.shell_mode { cmd.arg("-c").arg(build_shell_command(package_spec, command_args)); } else { - let command = if args.package.is_empty() && !package_spec.contains('@') { + let command = if args.package.is_empty() { package_spec.to_string() } else { extract_command_from_spec(package_spec) @@ -256,23 +256,11 @@ mod tests { #[test] fn test_npm_exec_with_version() { - let command = expect_run( - resolve(&npm("11.0.0"), dlx_args("typescript@5.5.4", &["tsc", "--version"])).outcome, - ); + let command = + expect_run(resolve(&npm("11.0.0"), dlx_args("create-vue@3.10.0", &["my-app"])).outcome); assert_eq!(command.program, "npm"); - assert_eq!( - command.args, - vec![ - "exec", - "--package=typescript@5.5.4", - "--yes", - "--", - "typescript", - "tsc", - "--version" - ] - ); + assert_eq!(command.args, vec!["exec", "--yes", "--", "create-vue@3.10.0", "my-app"]); } #[test] @@ -345,37 +333,20 @@ mod tests { #[test] fn test_npm_exec_scoped_package_with_version() { let command = expect_run( - resolve(&npm("11.0.0"), dlx_args("@vue/cli@5.0.0", &["create", "my-app"])).outcome, + resolve(&npm("11.0.0"), dlx_args("@angular/cli@19.2.15", &["--version"])).outcome, ); assert_eq!(command.program, "npm"); - assert_eq!( - command.args, - vec!["exec", "--package=@vue/cli@5.0.0", "--yes", "--", "cli", "create", "my-app"] - ); + assert_eq!(command.args, vec!["exec", "--yes", "--", "@angular/cli@19.2.15", "--version"]); } #[test] fn test_npm_exec_scoped_package_without_version() { - let command = expect_run( - resolve(&npm("11.0.0"), dlx_args("@vue/cli", &["create", "my-app"])).outcome, - ); - - assert_eq!(command.program, "npm"); - assert_eq!( - command.args, - vec!["exec", "--package=@vue/cli", "--yes", "--", "cli", "create", "my-app"] - ); - } - - #[test] - fn test_npm_exec_version_requires_package_flag_and_extracted_command() { let command = - expect_run(resolve(&npm("11.0.0"), dlx_args("create-vue@3.10.0", &["my-app"])).outcome); + expect_run(resolve(&npm("11.0.0"), dlx_args("@angular/cli", &["--version"])).outcome); - assert!(command.args.contains(&"--package=create-vue@3.10.0".to_string())); - let separator_pos = command.args.iter().position(|arg| arg == "--").unwrap(); - assert_eq!(command.args[separator_pos + 1], "create-vue"); + assert_eq!(command.program, "npm"); + assert_eq!(command.args, vec!["exec", "--yes", "--", "@angular/cli", "--version"]); } #[test]