Repository: railwayapp/nixpacks
Pull request #1235 (2024-12-03 20:32:38)

Add .node-version file basic support
Noticed that the `.node-version` convention is not currently supported.

> [!NOTE]
> This is my first Rust PR, if you have suggestion, tips or remarks 😉 

- [x] Tests are added/updated if needed


MatteoGauthier: Just a small question, when I run :

```
cargo run -- build examples/node-node-version --name node-node-version
```

I got this error... It's not breaking the tests but its weird, do you have any idea ?

<details><summary>Details</summary>
<p>

```
20.52 unpacking source archive /nix/store/6rn31a96dly28rhgli9nninq7dsaky9c-node-v14.21.3.tar.xz
22.29 source root is node-v14.21.3
22.39 setting SOURCE_DATE_EPOCH to timestamp 1676561550 of file node-v14.21.3/vcbuild.bat
22.40 patching sources
22.40 updateAutotoolsGnuConfigScriptsPhase
22.42 Updating Autotools / GNU config script to a newer upstream version: ./deps/cares/config.sub
22.44 Updating Autotools / GNU config script to a newer upstream version: ./deps/cares/config.guess
22.44 configuring
22.46 fixing libtool script ./deps/cares/ltmain.sh
22.47 ./deps/cares/configure
22.50 configure flags: --prefix=/nix/store/m65ddhqa3i1c4fvykqfk8vkwsk32crkf-nodejs-14.21.3 --shared-libuv --shared-libuv-libpath=/nix/store/kfdvif4lanw5zfdbhnp7hrjlrsxbzal9-libuv-1.46.0/lib --shared-openssl --shared-openssl-libpath=/nix/store/fyic70v28145nnr4s3ff03svr5w6gync-openssl-1.1.1w/lib --shared-zlib --shared-zlib-libpath=/nix/store/i6w8i3ps7x9cyyyvfif55rf1plvprp2f-zlib-1.3/lib --with-intl=system-icu --without-dtrace
22.52 Node.js configure: Found Python 3.11.5...
22.52 Please use python3.10 or python3.9 or python3.8 or python3.7 or python3.6 or python3.5 or python2.7.
23.05 error: builder for '/nix/store/fs2bjgbwk2ivs0h9cljih0j9n25nxhxf-nodejs-14.21.3.drv' failed with exit code 1
24.42 error: 1 dependencies of derivation '/nix/store/8b4ixp1kx3v239p0k1m6rh46p99qjf0l-bf744fe90419885eefced41b3e5ae442d732712d-env.drv' failed to build
------

 1 warning found (use docker --debug to expand):
 - UndefinedVar: Usage of undefined variable '$NIXPACKS_PATH' (line 18)
Dockerfile:8
--------------------
   6 |
   7 |     COPY .nixpacks/nixpkgs-bf744fe90419885eefced41b3e5ae442d732712d.nix .nixpacks/nixpkgs-bf744fe90419885eefced41b3e5ae442d732712d.nix
   8 | >>> RUN nix-env -if .nixpacks/nixpkgs-bf744fe90419885eefced41b3e5ae442d732712d.nix && nix-collect-garbage -d
   9 |
  10 |
--------------------
ERROR: failed to solve: process "/bin/bash -ol pipefail -c nix-env -if .nixpacks/nixpkgs-bf744fe90419885eefced41b3e5ae442d732712d.nix && nix-collect-garbage -d" did not complete successfully: exit code: 100
Error: Docker build failed
```

</p>
</details> 
iloveitaly: Check out some of the commands in the makefile to get better errors, the default errors from running rust tests are not great

https://github.com/railwayapp/nixpacks/blob/main/Makefile
coffee-cup: The error is happening because there are no prebuilt binaries for Node 14 because it is such an old version and it is attempting to build it from source and there are missing dependencies needed. Using a newer version of Node should work.
MatteoGauthier: Hey @coffee-cup, I've updated the PR 
- The version in the example is now v22 (LTS)
- Tests updated
- Example node-node-version is working well
github-actions[bot]: This pull request is stale because it has been open 10 days with no activity. Remove stale label or comment or this will be closed in 5 days
github-actions[bot]: This pull request was closed for lack of activity. Feel free to reopen later.

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/examples/node-node-version/.node-version b/examples/node-node-version/.node-version
new file mode 100644
index 0000000..dc0bb0f
--- /dev/null
+++ b/examples/node-node-version/.node-version
@@ -0,0 +1,1 @@
+v22.12.0
diff --git a/examples/node-node-version/index.js b/examples/node-node-version/index.js
new file mode 100644
index 0000000..140fc15
--- /dev/null
+++ b/examples/node-node-version/index.js
@@ -0,0 +1,2 @@
+console.log("Hello from Node");
+console.log(`Node version: ${process.version}`);
diff --git a/examples/node-node-version/package-lock.json b/examples/node-node-version/package-lock.json
new file mode 100644
index 0000000..d18e0e0
--- /dev/null
+++ b/examples/node-node-version/package-lock.json
@@ -0,0 +1,12 @@
+{
+  "name": "node",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "node",
+      "version": "1.0.0"
+    }
+  }
+}
diff --git a/examples/node-node-version/package.json b/examples/node-node-version/package.json
new file mode 100644
index 0000000..2f12d8f
--- /dev/null
+++ b/examples/node-node-version/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "node",
+  "version": "1.0.0",
+  "main": "index.js",
+  "scripts": {
+    "start": "node index.js"
+  }
+}
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..49e727f 100644
--- a/src/providers/node/mod.rs
+++ b/src/providers/node/mod.rs
@@ -340,7 +340,18 @@ impl NodeProvider {
             None
         };
 
-        let node_version = env_node_version.or(pkg_node_version).or(nvmrc_node_version);
+        let dot_node_version = if app.includes_file(".node-version") {
+            let node_version_file = app.read_file(".node-version")?;
+            // Using simple string transform since .node-version don't currently have a convention around the use of lts/* implemented in parse_nvmrc method
+            Some(node_version_file.trim().replace('v', ""))
+        } else {
+            None
+        };
+
+        let node_version = env_node_version
+            .or(pkg_node_version)
+            .or(nvmrc_node_version)
+            .or(dot_node_version);
 
         let node_version = match node_version {
             Some(node_version) => node_version,
@@ -362,7 +373,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 +408,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();
         }
 
@@ -961,6 +972,23 @@ mod test {
         Ok(())
     }
 
+    #[test]
+    fn test_version_from_node_version_file() -> Result<()> {
+        assert_eq!(
+            NodeProvider::get_nix_node_pkg(
+                &PackageJson {
+                    name: Some(String::default()),
+                    ..Default::default()
+                },
+                &App::new("examples/node-node-version")?,
+                &Environment::default()
+            )?,
+            Pkg::new("nodejs_22")
+        );
+
+        Ok(())
+    }
+
     #[test]
     fn test_version_from_nvmrc_lts() -> Result<()> {
         assert_eq!(


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"
+  }
+}
diff --git a/tests/snapshots/generate_plan_tests__node_node_version.snap b/tests/snapshots/generate_plan_tests__node_node_version.snap
new file mode 100644
index 0000000..a7f82aa
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_node_version.snap
@@ -0,0 +1,54 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "providers": [],
+  "buildImage": "[build_image]",
+  "variables": {
+    "CI": "true",
+    "NIXPACKS_METADATA": "node",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "phases": {
+    "build": {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "cacheDirectories": [
+        "node_modules/.cache"
+      ]
+    },
+    "install": {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "cmds": [
+        "npm ci"
+      ],
+      "cacheDirectories": [
+        "/root/.npm"
+      ],
+      "paths": [
+        "/app/node_modules/.bin"
+      ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "nodejs_22",
+        "npm-8_x"
+      ],
+      "nixOverlays": [
+        "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      ],
+      "nixpkgsArchive": "[archive]"
+    }
+  },
+  "start": {
+    "cmd": "npm run start"
+  }
+}