Repository: denoland/deno
Pull request: feat(uninstall): alias to 'deno remove' if -g flag missing

Close https://github.com/denoland/deno/issues/25457

Changed files:
filename: cli/args/flags.rs
patch: @@ -253,7 +253,6 @@ pub enum InstallFlagsLocal {
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct InstallFlags {
-  pub global: bool,
   pub kind: InstallKind,
 }
 
@@ -277,14 +276,12 @@ pub struct UninstallFlagsGlobal {
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum UninstallKind {
-  #[allow(unused)]
-  Local,
+  Local(RemoveFlags),
   Global(UninstallFlagsGlobal),
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct UninstallFlags {
-  pub global: bool,
   pub kind: UninstallKind,
 }
 
@@ -2493,11 +2490,12 @@ fn jupyter_subcommand() -> Command {
 fn uninstall_subcommand() -> Command {
   command(
     "uninstall",
-    cstr!("Uninstalls an executable script in the installation root's bin directory.
-  <p(245)>deno uninstall serve</>
+    cstr!("Uninstalls a dependency or an executable script in the installation root's bin directory.
+  <p(245)>deno uninstall @std/dotenv chalk</>
+  <p(245)>deno uninstall --global file_server</>
 
 To change the installation root, use <c>--root</> flag:
-  <p(245)>deno uninstall --root /usr/local serve</>
+  <p(245)>deno uninstall --global --root /usr/local serve</>
 
 The installation root is determined, in order of precedence:
   - <p(245)>--root</> option
@@ -2507,11 +2505,12 @@ The installation root is determined, in order of precedence:
   )
   .defer(|cmd| {
     cmd
-      .arg(Arg::new("name").required_unless_present("help"))
+      .arg(Arg::new("name-or-package").required_unless_present("help"))
       .arg(
         Arg::new("root")
           .long("root")
           .help("Installation root")
+          .requires("global")
           .value_hint(ValueHint::DirPath),
       )
       .arg(
@@ -2521,6 +2520,13 @@ The installation root is determined, in order of precedence:
           .help("Remove globally installed package or module")
           .action(ArgAction::SetTrue),
       )
+      .arg(
+        Arg::new("additional-packages")
+          .help("List of additional packages to remove")
+          .conflicts_with("global")
+          .num_args(1..)
+          .action(ArgAction::Append)
+      )
   })
 }
 
@@ -4430,8 +4436,6 @@ fn install_parse(flags: &mut Flags, matches: &mut ArgMatches) {
     let args = cmd_values.collect();
 
     flags.subcommand = DenoSubcommand::Install(InstallFlags {
-      // TODO(bartlomieju): remove for 2.0
-      global,
       kind: InstallKind::Global(InstallFlagsGlobal {
         name,
         module_url,
@@ -4448,7 +4452,6 @@ fn install_parse(flags: &mut Flags, matches: &mut ArgMatches) {
   if matches.get_flag("entrypoint") {
     let entrypoints = matches.remove_many::<String>("cmd").unwrap_or_default();
     flags.subcommand = DenoSubcommand::Install(InstallFlags {
-      global,
       kind: InstallKind::Local(InstallFlagsLocal::Entrypoints(
         entrypoints.collect(),
       )),
@@ -4458,12 +4461,10 @@ fn install_parse(flags: &mut Flags, matches: &mut ArgMatches) {
     .map(|packages| add_parse_inner(matches, Some(packages)))
   {
     flags.subcommand = DenoSubcommand::Install(InstallFlags {
-      global,
       kind: InstallKind::Local(InstallFlagsLocal::Add(add_files)),
     })
   } else {
     flags.subcommand = DenoSubcommand::Install(InstallFlags {
-      global,
       kind: InstallKind::Local(InstallFlagsLocal::TopLevel),
     });
   }
@@ -4554,15 +4555,24 @@ fn jupyter_parse(flags: &mut Flags, matches: &mut ArgMatches) {
 }
 
 fn uninstall_parse(flags: &mut Flags, matches: &mut ArgMatches) {
-  let root = matches.remove_one::<String>("root");
-  let global = matches.get_flag("global");
-  let name = matches.remove_one::<String>("name").unwrap();
-  flags.subcommand = DenoSubcommand::Uninstall(UninstallFlags {
-    // TODO(bartlomieju): remove once `deno uninstall` supports both local and
-    // global installs
-    global,
-    kind: UninstallKind::Global(UninstallFlagsGlobal { name, root }),
-  });
+  let name = matches.remove_one::<String>("name-or-package").unwrap();
+
+  let kind = if matches.get_flag("global") {
+    let root = matches.remove_one::<String>("root");
+    UninstallKind::Global(UninstallFlagsGlobal { name, root })
+  } else {
+    let packages: Vec<_> = vec![name]
+      .into_iter()
+      .chain(
+        matches
+          .remove_many::<String>("additional-packages")
+          .unwrap_or_default(),
+      )
+      .collect();
+    UninstallKind::Local(RemoveFlags { packages })
+  };
+
+  flags.subcommand = DenoSubcommand::Uninstall(UninstallFlags { kind });
 }
 
 fn lsp_parse(flags: &mut Flags, _matches: &mut ArgMatches) {
@@ -7896,7 +7906,6 @@ mod tests {
             root: None,
             force: false,
           }),
-          global: true,
         }),
         ..Flags::default()
       }
@@ -7919,7 +7928,6 @@ mod tests {
             root: None,
             force: false,
           }),
-          global: true,
         }),
         ..Flags::default()
       }
@@ -7941,7 +7949,6 @@ mod tests {
             root: Some("/foo".to_string()),
             force: true,
           }),
-          global: true,
         }),
         import_map_path: Some("import_map.json".to_string()),
         no_remote: true,
@@ -7968,7 +7975,37 @@ mod tests {
 
   #[test]
   fn uninstall() {
-    let r = flags_from_vec(svec!["deno", "uninstall", "file_server"]);
+    let r = flags_from_vec(svec!["deno", "uninstall"]);
+    assert!(r.is_err(),);
+
+    let r = flags_from_vec(svec!["deno", "uninstall", "@std/load"]);
+    assert_eq!(
+      r.unwrap(),
+      Flags {
+        subcommand: DenoSubcommand::Uninstall(UninstallFlags {
+          kind: UninstallKind::Local(RemoveFlags {
+            packages: vec!["@std/load".to_string()],
+          }),
+        }),
+        ..Flags::default()
+      }
+    );
+
+    let r =
+      flags_from_vec(svec!["deno", "uninstall", "file_server", "@std/load"]);
+    assert_eq!(
+      r.unwrap(),
+      Flags {
+        subcommand: DenoSubcommand::Uninstall(UninstallFlags {
+          kind: UninstallKind::Local(RemoveFlags {
+            packages: vec!["file_server".to_string(), "@std/load".to_string()],
+          }),
+        }),
+        ..Flags::default()
+      }
+    );
+
+    let r = flags_from_vec(svec!["deno", "uninstall", "-g", "file_server"]);
     assert_eq!(
       r.unwrap(),
       Flags {
@@ -7977,22 +8014,27 @@ mod tests {
             name: "file_server".to_string(),
             root: None,
           }),
-          global: false,
         }),
         ..Flags::default()
       }
     );
 
-    let r = flags_from_vec(svec!["deno", "uninstall", "-g", "file_server"]);
+    let r = flags_from_vec(svec![
+      "deno",
+      "uninstall",
+      "-g",
+      "--root",
+      "/user/foo/bar",
+      "file_server"
+    ]);
     assert_eq!(
       r.unwrap(),
       Flags {
         subcommand: DenoSubcommand::Uninstall(UninstallFlags {
           kind: UninstallKind::Global(UninstallFlagsGlobal {
             name: "file_server".to_string(),
-            root: None,
+            root: Some("/user/foo/bar".to_string()),
           }),
-          global: true,
         }),
         ..Flags::default()
       }

filename: cli/main.rs
patch: @@ -161,7 +161,7 @@ async fn run_subcommand(flags: Arc<Flags>) -> Result<i32, AnyError> {
       tools::jupyter::kernel(flags, jupyter_flags).await
     }),
     DenoSubcommand::Uninstall(uninstall_flags) => spawn_subcommand(async {
-      tools::installer::uninstall(uninstall_flags)
+      tools::installer::uninstall(flags, uninstall_flags).await
     }),
     DenoSubcommand::Lsp => spawn_subcommand(async { lsp::start().await }),
     DenoSubcommand::Lint(lint_flags) => spawn_subcommand(async {

filename: cli/tools/installer.rs
patch: @@ -198,14 +198,15 @@ pub async fn infer_name_from_url(
   Some(stem.to_string())
 }
 
-pub fn uninstall(uninstall_flags: UninstallFlags) -> Result<(), AnyError> {
-  if !uninstall_flags.global {
-    log::warn!("⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.");
-  }
-
+pub async fn uninstall(
+  flags: Arc<Flags>,
+  uninstall_flags: UninstallFlags,
+) -> Result<(), AnyError> {
   let uninstall_flags = match uninstall_flags.kind {
     UninstallKind::Global(flags) => flags,
-    UninstallKind::Local => unreachable!(),
+    UninstallKind::Local(remove_flags) => {
+      return super::registry::remove(flags, remove_flags).await;
+    }
   };
 
   let cwd = std::env::current_dir().context("Unable to get CWD")?;
@@ -332,10 +333,6 @@ pub async fn install_command(
 ) -> Result<(), AnyError> {
   match install_flags.kind {
     InstallKind::Global(global_flags) => {
-      if !install_flags.global {
-        log::warn!("⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.");
-      }
-
       install_global(flags, global_flags).await
     }
     InstallKind::Local(local_flags) => {
@@ -1512,8 +1509,8 @@ mod tests {
     assert!(content.contains(&expected_string));
   }
 
-  #[test]
-  fn uninstall_basic() {
+  #[tokio::test]
+  async fn uninstall_basic() {
     let temp_dir = TempDir::new();
     let bin_dir = temp_dir.path().join("bin");
     std::fs::create_dir(&bin_dir).unwrap();
@@ -1540,13 +1537,16 @@ mod tests {
       File::create(file_path).unwrap();
     }
 
-    uninstall(UninstallFlags {
-      kind: UninstallKind::Global(UninstallFlagsGlobal {
-        name: "echo_test".to_string(),
-        root: Some(temp_dir.path().to_string()),
-      }),
-      global: false,
-    })
+    uninstall(
+      Default::default(),
+      UninstallFlags {
+        kind: UninstallKind::Global(UninstallFlagsGlobal {
+          name: "echo_test".to_string(),
+          root: Some(temp_dir.path().to_string()),
+        }),
+      },
+    )
+    .await
     .unwrap();
 
     assert!(!file_path.exists());

filename: tests/integration/install_tests.rs
patch: @@ -62,7 +62,7 @@ fn install_basic() {
   // now uninstall
   context
     .new_command()
-    .args("uninstall echo_test")
+    .args("uninstall -g echo_test")
     .envs([
       ("HOME", temp_dir_str.as_str()),
       ("USERPROFILE", temp_dir_str.as_str()),
@@ -139,7 +139,7 @@ fn install_basic_global() {
   // now uninstall
   context
     .new_command()
-    .args("uninstall echo_test")
+    .args("uninstall -g echo_test")
     .envs([
       ("HOME", temp_dir_str.as_str()),
       ("USERPROFILE", temp_dir_str.as_str()),
@@ -274,7 +274,7 @@ fn installer_test_remote_module_run() {
   // now uninstall with the relative path
   context
     .new_command()
-    .args("uninstall --root ./root echo_test")
+    .args("uninstall -g --root ./root echo_test")
     .run()
     .skip_output_check()
     .assert_exit_code(0);