Repository: railwayapp/nixpacks (rust)

Issue:
Nixpacks Bug Report: jsonc is parsed as json
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

Nixpacks parses `deno.jsonc` as json instead of jsonc.

### To reproduce

`deno.jsonc`:
```jsonc
{
	"tasks": {
		// This comment is valid in jsonc.
		"start": "dist/start"
	}
}
```

```sh
% nixpacks build .
Error: Error reading deno.jsonc as JSON

Caused by:
    key must be a string at line 3 column 3
```


### Expected behavior

`deno.jsonc` should be parsed as jsonc.

### Environment

nixpacks 1.28.1 on macOS 14.6.1 (23G93).

Pull request description:
feat(jsonc): added jsonc support
Fixes #1189 

Added jsonc support for deno.jsonc

PR Checklist
- [x] Docs are updated if needed
- [x] Tests are added/updated if needed 


Patch:
diff --git a/examples/deno-jsonc/deno.jsonc b/examples/deno-jsonc/deno.jsonc
new file mode 100644
index 0000000..9064591
--- /dev/null
+++ b/examples/deno-jsonc/deno.jsonc
@@ -0,0 +1,15 @@
+// THIS FILE HAS ALL KINDS OF COMMENTS POSSIBLE TO TEST JSONC
+{
+  //some random comment
+  "tasks": {
+    // random afterline comment
+    /* :) */ "dev": "deno run --watch main.ts",
+    "start": "deno start main.ts"
+  } /*
+    This 
+    is some multiline comment
+    */,
+  "imports": {
+    "@std/assert": "jsr:@std/assert@1"
+  }
+}
diff --git a/examples/deno-jsonc/deno.lock b/examples/deno-jsonc/deno.lock
new file mode 100644
index 0000000..3513c0f
--- /dev/null
+++ b/examples/deno-jsonc/deno.lock
@@ -0,0 +1,18 @@
+{
+  "version": "4",
+  "specifiers": {
+    "jsr:@std/assert@1": "1.0.8",
+    "jsr:@std/internal@^1.0.5": "1.0.5"
+  },
+  "jsr": {
+    "@std/assert@1.0.8": {
+      "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
+      "dependencies": [
+        "jsr:@std/internal"
+      ]
+    },
+    "@std/internal@1.0.5": {
+      "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
+    }
+  }
+}
diff --git a/examples/deno-jsonc/main.ts b/examples/deno-jsonc/main.ts
new file mode 100644
index 0000000..bdf52b9
--- /dev/null
+++ b/examples/deno-jsonc/main.ts
@@ -0,0 +1,7 @@
+export function add(a: number, b: number): number {
+  return a + b;
+}
+// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
+if (import.meta.main) {
+  console.log("Add 2 + 3 =", add(2, 3));
+}
\ No newline at end of file
diff --git a/src/nixpacks/app.rs b/src/nixpacks/app.rs
index d94e841..7aea9a1 100644
--- a/src/nixpacks/app.rs
+++ b/src/nixpacks/app.rs
@@ -190,6 +190,47 @@ impl App {
         Ok(toml_file)
     }
 
+    /// Parse jsonc files as json by ignoring all kinds of comments
+    pub fn read_jsonc<T>(&self, name: &str) -> Result<T>
+    where
+        T: DeserializeOwned,
+    {
+        let mut cleaned_jsonc = String::new();
+        let contents = self.read_file(name)?;
+        let mut chars = contents.chars().peekable();
+        while let Some(current_char) = chars.next() {
+            match current_char {
+                '/' if chars.peek() == Some(&'/') => {
+                    while let Some(&next_char) = chars.peek() {
+                        chars.next();
+                        if next_char == '\n' {
+                            break;
+                        }
+                    }
+                }
+                '/' if chars.peek() == Some(&'*') => {
+                    chars.next();
+                    loop {
+                        match chars.next() {
+                            Some('*') if chars.peek() == Some(&'/') => {
+                                chars.next();
+                                break;
+                            }
+                            None => break,
+                            _ => continue,
+                        }
+                    }
+                }
+                _ => cleaned_jsonc.push(current_char),
+            }
+        }
+        let value: T = serde_json::from_str(cleaned_jsonc.as_str()).with_context(|| {
+            let relative_path = self.strip_source_path(Path::new(name)).unwrap();
+            format!("Error reading {} as JSONC", relative_path.to_str().unwrap())
+        })?;
+        Ok(value)
+    }
+
     /// Try to yaml-parse a file.
     pub fn read_yaml<T>(&self, name: &str) -> Result<T>
     where
@@ -281,6 +322,14 @@ mod tests {
         Ok(())
     }
 
+    #[test]
+    fn test_read_jsonc_file() -> Result<()> {
+        let app = App::new("./examples/deno-jsonc")?;
+        let value: Map<String, Value> = app.read_jsonc("deno.jsonc")?;
+        assert!(value.get("tasks").is_some());
+        Ok(())
+    }
+
     #[test]
     fn test_read_toml_file() -> Result<()> {
         let app = App::new("./examples/rust-rocket")?;
diff --git a/src/providers/deno.rs b/src/providers/deno.rs
index a5289ec..3deef7b 100644
--- a/src/providers/deno.rs
+++ b/src/providers/deno.rs
@@ -82,7 +82,7 @@ impl DenoProvider {
         if app.includes_file("deno.json") || app.includes_file("deno.jsonc") {
             let deno_json: DenoJson = app
                 .read_json("deno.json")
-                .or_else(|_| app.read_json("deno.jsonc"))?;
+                .or_else(|_| app.read_jsonc("deno.jsonc"))?;
 
             if let Some(tasks) = deno_json.tasks {
                 if let Some(start) = tasks.start {
diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs
index e4e72e3..4536c9b 100644
--- a/src/providers/node/mod.rs
+++ b/src/providers/node/mod.rs
@@ -362,7 +362,7 @@ impl NodeProvider {
             pkg_manager = "pnpm";
         } else if app.includes_file("yarn.lock") {
             pkg_manager = "yarn";
-        } else if app.includes_file("bun.lockb") {
+        } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") {
             pkg_manager = "bun";
         }
         pkg_manager.to_string()
@@ -397,7 +397,7 @@ impl NodeProvider {
             }
         } else if app.includes_file("package-lock.json") {
             install_cmd = "npm ci".to_string();
-        } else if app.includes_file("bun.lockb") {
+        } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") {
             install_cmd = "bun i --no-save".to_string();
         }
 


Test patch:
diff --git a/examples/deno-jsonc/main_test.ts b/examples/deno-jsonc/main_test.ts
new file mode 100644
index 0000000..3d981e9
--- /dev/null
+++ b/examples/deno-jsonc/main_test.ts
@@ -0,0 +1,6 @@
+import { assertEquals } from "@std/assert";
+import { add } from "./main.ts";
+
+Deno.test(function addTest() {
+  assertEquals(add(2, 3), 5);
+});
diff --git a/tests/snapshots/generate_plan_tests__deno_jsonc.snap b/tests/snapshots/generate_plan_tests__deno_jsonc.snap
new file mode 100644
index 0000000..dd51556
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__deno_jsonc.snap
@@ -0,0 +1,24 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "providers": [],
+  "buildImage": "[build_image]",
+  "variables": {
+    "NIXPACKS_METADATA": "deno"
+  },
+  "phases": {
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "deno"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
+    }
+  },
+  "start": {
+    "cmd": "deno start main.ts"
+  }
+}