Repository: denoland/deno
Pull request: fix(install): clean up associated config and lock files during uninstall

This PR fixes an issue where `deno uninstall` would successfully remove the executable but leave behind the associated hidden configuration files (like `.<name>.deno.json`) in the bin directory.

**Changes:**

* Updated the uninstall logic to detect and remove associated `.deno.json` and `.lock.json` files alongside the shim.
* Added a regression test to verify that these hidden files are created during setup and correctly removed upon uninstallation.

Closes #31861

Changed files:
filename: cli/tools/installer/mod.rs
patch: @@ -401,9 +401,28 @@ pub async fn uninstall(
   // There might be some extra files to delete
   // Note: tsconfig.json is legacy. We renamed it to deno.json.
   // Remove cleaning it up after January 2024
+  // Use the base file path (without extension) to compute related files
+  let base_file = installation_dir.join(&uninstall_flags.name);
+
   for ext in ["tsconfig.json", "deno.json", "lock.json"] {
-    let file_path = file_path.with_extension(ext);
-    remove_file_if_exists(&file_path)?;
+    // remove the plain extension files (e.g., name.deno.json, name.lock.json)
+    let file_path_ext = base_file.with_extension(ext);
+    remove_file_if_exists(&file_path_ext)?;
+
+    // also remove the hidden per-command copies created at install time
+    // (e.g., .name.deno.json)
+    let hidden_file = get_hidden_file_with_ext(&base_file, ext);
+    remove_file_if_exists(&hidden_file)?;
+
+    // On Windows, installs use a shim with a .cmd extension, which means the
+    // hidden files might be named like `.name.cmd.deno.json`. Attempt to remove
+    // those as well to be thorough.
+    #[cfg(windows)]
+    {
+      let base_with_cmd = base_file.with_extension("cmd");
+      let hidden_cmd_file = get_hidden_file_with_ext(&base_with_cmd, ext);
+      remove_file_if_exists(&hidden_cmd_file)?;
+    }
   }
 
   log::info!("✅ Successfully uninstalled {}", uninstall_flags.name);
@@ -1922,6 +1941,7 @@ mod tests {
       file_path = file_path.with_extension("cmd");
       File::create(&file_path).unwrap();
     }
+    let shim_path = file_path.clone();
 
     // create extra files
     {
@@ -1938,6 +1958,18 @@ mod tests {
       File::create(file_path).unwrap();
     }
 
+    // create hidden per-command copies as produced by install
+    {
+      let hidden_file =
+        get_hidden_file_with_ext(shim_path.as_path(), "deno.json");
+      File::create(hidden_file).unwrap();
+    }
+    {
+      let hidden_file =
+        get_hidden_file_with_ext(shim_path.as_path(), "lock.json");
+      File::create(hidden_file).unwrap();
+    }
+
     uninstall(
       Default::default(),
       UninstallFlags {
@@ -1955,6 +1987,14 @@ mod tests {
     assert!(!file_path.with_extension("deno.json").exists());
     assert!(!file_path.with_extension("lock.json").exists());
 
+    // hidden per-command files should also be removed
+    assert!(
+      !get_hidden_file_with_ext(shim_path.as_path(), "deno.json").exists()
+    );
+    assert!(
+      !get_hidden_file_with_ext(shim_path.as_path(), "lock.json").exists()
+    );
+
     if cfg!(windows) {
       file_path = file_path.with_extension("cmd");
       assert!(!file_path.exists());