Repository: denoland/deno
Pull request: fix(cli): avoid `deno add` and `deno vendor` errors when deno.json is empty

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->

Avoid "Failed updating config file due to no object" errors for the below commands when existing `deno.json` is empty. Added a test case for each.

- `deno add` (c0f410d50652acf9316fdca52e5cc919770ff419)
  - fix #23215
- `deno vendor` (46fdc7f9f46788c0cb384f8dccd50d30b90ecddf)
  - not listed in the issue, but errors occur in the same way



Changed files:
filename: cli/tools/registry/pm.rs
patch: @@ -109,8 +109,14 @@ pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> {
     }
   }
 
-  let config_file_contents =
-    tokio::fs::read_to_string(&config_file_path).await.unwrap();
+  let config_file_contents = {
+    let contents = tokio::fs::read_to_string(&config_file_path).await.unwrap();
+    if contents.trim().is_empty() {
+      "{}\n".into()
+    } else {
+      contents
+    }
+  };
   let ast = jsonc_parser::parse_to_ast(
     &config_file_contents,
     &Default::default(),

filename: cli/tools/vendor/mod.rs
patch: @@ -310,6 +310,7 @@ fn update_config_text(
 ) -> Result<ModifiedResult, AnyError> {
   use jsonc_parser::ast::ObjectProp;
   use jsonc_parser::ast::Value;
+  let text = if text.trim().is_empty() { "{}\n" } else { text };
   let ast =
     jsonc_parser::parse_to_ast(text, &Default::default(), &Default::default())?;
   let obj = match ast.value {

filename: tests/integration/pm_tests.rs
patch: @@ -49,6 +49,26 @@ fn add_basic_no_deno_json() {
   temp_dir.join("deno.json").assert_matches_text(expected);
 }
 
+#[test]
+fn add_basic_with_empty_deno_json() {
+  let context = pm_context_builder().build();
+  let temp_dir = context.temp_dir();
+  temp_dir.write("deno.json", "");
+
+  let output = context.new_command().args("add @denotest/add").run();
+  output.assert_exit_code(0);
+  let output = output.combined_output();
+  assert_contains!(output, "Add @denotest/add");
+  temp_dir
+    .path()
+    .join("deno.json")
+    .assert_matches_json(json!({
+      "imports": {
+        "@denotest/add": "jsr:@denotest/add@^1.0.0"
+      }
+    }));
+}
+
 #[test]
 fn add_version_contraint() {
   let context = pm_context_builder().build();

filename: tests/integration/vendor_tests.rs
patch: @@ -528,6 +528,39 @@ fn update_existing_config_test() {
   assert!(output.status.success());
 }
 
+#[test]
+fn update_existing_empty_config_test() {
+  let _server = http_server();
+  let t = TempDir::new();
+  t.write(
+    "my_app.ts",
+    "import {Logger} from 'http://localhost:4545/vendor/logger.ts'; new Logger().log('outputted');",
+  );
+  t.write("deno.json", "");
+
+  let deno = util::deno_cmd()
+    .current_dir(t.path())
+    .arg("vendor")
+    .arg("my_app.ts")
+    .arg("--output")
+    .arg("vendor2")
+    .env("NO_COLOR", "1")
+    .piped_output()
+    .spawn()
+    .unwrap();
+  let output = deno.wait_with_output().unwrap();
+  assert_eq!(
+    String::from_utf8_lossy(&output.stderr).trim(),
+    format!(
+      "Download http://localhost:4545/vendor/logger.ts\n{}\n\n{}",
+      vendored_text("1 module", "vendor2"),
+      success_text_updated_deno_json("vendor2",)
+    )
+  );
+  assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "");
+  assert!(output.status.success());
+}
+
 #[test]
 fn vendor_npm_node_specifiers() {
   let context = TestContextBuilder::for_npm().use_temp_cwd().build();