Skip to content

Commit 58a7c4d

Browse files
authored
Add Node 24 support and refactor version-to-archive mapping (#1373)
* Add Node 24 support and refactor version-to-archive mapping Refactors Node version management to use a tuple array mapping versions to nix archive hashes, similar to the Swift and Go providers. This makes it easier to maintain and add new Node versions. - Add Node 24 support with specific nix archive (23f9169c4ccce521379e602cc82ed873a1f1b52b) - Remove Node 23 (was unreachable due to LTS filtering) - Add Node 22-specific nix archive (e6f23dc08d3624daab7094b701aa3954923c6bbb) - Update Bun nixpkgs archive to 31fb21469e34b6b5c7be77b9a35bae43d0c598e9 - Refactor AVAILABLE_NODE_VERSIONS from array to tuple array with archive hashes - Replace if/else chain in get_nix_archive() with lookup function - Add version_number_to_archive() helper function - Add pnpm 10 support (lockfileVersion 9.0) - Add node-24-pnpm-10 example * lint fix * Fix pnpm version detection to respect packageManager field Both pnpm 9 and 10 use lockfileVersion '9.0', making them indistinguishable by lockfile alone. This fix prioritizes the packageManager field in package.json when determining which pnpm version to use. Changes: - Parse packageManager field (e.g., "[email protected]") to extract version - Map parsed version to appropriate nix package (pnpm-9_x, pnpm-10_x) - Fall back to lockfile detection only when packageManager is absent - Default lockfileVersion '9.0' to pnpm-9_x (when introduced) - Add packageManager field to node-24-pnpm-10 example for pnpm 10 - Update test snapshots This ensures pnpm 10 is only used when explicitly specified in the packageManager field, fixing the node-pnpm-corepack example which was incorrectly using pnpm-10_x instead of pnpm-9_x. * Fix clippy and dead_code warnings - Remove unused PackageJsonNx and PackageJsonNxTargets structs - Simplify pnpm lockfile detection by removing redundant else-if branch
1 parent 1234b3b commit 58a7c4d

File tree

13 files changed

+199
-44
lines changed

13 files changed

+199
-44
lines changed

examples/node-24-pnpm-10/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("Hello from Node");

examples/node-24-pnpm-10/node_modules/.pnpm-workspace-state.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "node",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"start": "node index.js"
7+
},
8+
"engines": {
9+
"node": "24.x"
10+
},
11+
"packageManager": "[email protected]"
12+
}

examples/node-24-pnpm-10/pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub mod nixpacks;
5050
pub mod providers;
5151

5252
/// Supplies all currently-defined providers to build plan generators and image builders.
53-
pub fn get_providers() -> &'static [&'static (dyn Provider)] {
53+
pub fn get_providers() -> &'static [&'static dyn Provider] {
5454
&[
5555
&CrystalProvider {},
5656
&CSharpProvider {},

src/nixpacks/plan/generator.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub struct GeneratePlanOptions {
2626

2727
/// Holds plan options and providers for a build.
2828
pub struct NixpacksBuildPlanGenerator<'a> {
29-
providers: &'a [&'a (dyn Provider)],
29+
providers: &'a [&'a dyn Provider],
3030
config: GeneratePlanOptions,
3131
}
3232

@@ -50,7 +50,7 @@ impl PlanGenerator for NixpacksBuildPlanGenerator<'_> {
5050

5151
impl NixpacksBuildPlanGenerator<'_> {
5252
pub fn new<'a>(
53-
providers: &'a [&'a (dyn Provider)],
53+
providers: &'a [&'a dyn Provider],
5454
config: GeneratePlanOptions,
5555
) -> NixpacksBuildPlanGenerator<'a> {
5656
NixpacksBuildPlanGenerator { providers, config }

src/providers/node/mod.rs

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,20 @@ mod turborepo;
2626
pub const NODE_OVERLAY: &str = "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz";
2727

2828
// unlike package managers, {node,bun} versions are pinned to a particular nixpacks release
29-
const NODE_NIXPKGS_ARCHIVE: &str = "ffeebf0acf3ae8b29f8c7049cd911b9636efd7e7";
30-
const BUN_NIXPKGS_ARCHIVE: &str = "5a0711127cd8b916c3d3128f473388c8c79df0da";
31-
32-
// We need to use a specific commit hash for Node versions <16 since it is EOL in the latest Nix packages
33-
const NODE_LT_16_ARCHIVE: &str = "bf744fe90419885eefced41b3e5ae442d732712d";
29+
const BUN_NIXPKGS_ARCHIVE: &str = "31fb21469e34b6b5c7be77b9a35bae43d0c598e9";
3430

3531
const DEFAULT_NODE_VERSION: u32 = 18;
36-
const AVAILABLE_NODE_VERSIONS: &[u32] = &[14, 16, 18, 20, 22, 23];
32+
33+
// From: https://lazamar.co.uk/nix-versions/?channel=nixpkgs-unstable&package=nodejs
34+
// Maps Node version to nixpkgs archive hash
35+
const AVAILABLE_NODE_VERSIONS: &[(u32, &str)] = &[
36+
(14, "bf744fe90419885eefced41b3e5ae442d732712d"), // EOL version, older nixpkgs
37+
(16, "bf744fe90419885eefced41b3e5ae442d732712d"), // EOL version, older nixpkgs
38+
(18, "ffeebf0acf3ae8b29f8c7049cd911b9636efd7e7"),
39+
(20, "ffeebf0acf3ae8b29f8c7049cd911b9636efd7e7"),
40+
(22, "e6f23dc08d3624daab7094b701aa3954923c6bbb"),
41+
(24, "23f9169c4ccce521379e602cc82ed873a1f1b52b"),
42+
];
3743

3844
const YARN_CACHE_DIR: &str = "/usr/local/share/.cache/yarn/v6";
3945
const PNPM_CACHE_DIR: &str = "/root/.local/share/pnpm/store/v3";
@@ -502,15 +508,27 @@ impl NodeProvider {
502508
let package_json: PackageJson = app.read_json("package.json").unwrap_or_default();
503509
let package_manager = NodeProvider::get_package_manager(app);
504510
let node_pkg = NodeProvider::get_nix_node_pkg(&package_json, app, &Environment::default())?;
505-
let uses_le_16 = node_pkg.name.contains("14") || node_pkg.name.contains("16");
506511

507-
if uses_le_16 {
508-
Ok(NODE_LT_16_ARCHIVE.to_string())
509-
} else if package_manager == "bun" {
510-
Ok(BUN_NIXPKGS_ARCHIVE.to_string())
511-
} else {
512-
Ok(NODE_NIXPKGS_ARCHIVE.to_string())
512+
// Bun uses a separate archive
513+
if package_manager == "bun" {
514+
return Ok(BUN_NIXPKGS_ARCHIVE.to_string());
513515
}
516+
517+
// Extract version number from package name (e.g., "nodejs_18" -> 18)
518+
let version = node_pkg
519+
.name
520+
.strip_prefix("nodejs_")
521+
.and_then(|v| v.parse::<u32>().ok())
522+
.unwrap_or(DEFAULT_NODE_VERSION);
523+
524+
// Look up the archive for this version
525+
let archive = version_number_to_archive(version).unwrap_or_else(|| {
526+
// Fallback to default version's archive
527+
version_number_to_archive(DEFAULT_NODE_VERSION)
528+
.expect("Default node version must exist in AVAILABLE_NODE_VERSIONS")
529+
});
530+
531+
Ok(archive.to_string())
514532
}
515533

516534
/// Returns the nodejs nix package and the appropriate package manager nix image.
@@ -529,16 +547,35 @@ impl NodeProvider {
529547
pkgs.push(node_pkg);
530548

531549
if package_manager == "pnpm" {
532-
let lockfile = app.read_file("pnpm-lock.yaml").unwrap_or_default();
533-
if lockfile.starts_with("lockfileVersion: 5.3") {
534-
pm_pkg = Pkg::new("pnpm-6_x");
535-
} else if lockfile.starts_with("lockfileVersion: 5.4") {
536-
pm_pkg = Pkg::new("pnpm-7_x");
537-
} else if lockfile.starts_with("lockfileVersion: '6.0'") {
538-
pm_pkg = Pkg::new("pnpm-8_x");
550+
// First, try to determine version from packageManager field (for corepack)
551+
if let Some(ref pkg_manager_field) = package_json.package_manager {
552+
if let Some(version_str) = pkg_manager_field.strip_prefix("pnpm@") {
553+
// Parse major version from "[email protected]" -> 9
554+
if let Some(major_version) = version_str.split('.').next() {
555+
if let Ok(major) = major_version.parse::<u32>() {
556+
pm_pkg = match major {
557+
6 => Pkg::new("pnpm-6_x"),
558+
7 => Pkg::new("pnpm-7_x"),
559+
8 => Pkg::new("pnpm-8_x"),
560+
9 => Pkg::new("pnpm-9_x"),
561+
10 => Pkg::new("pnpm-10_x"),
562+
_ => {
563+
// For unknown versions, try lockfile detection
564+
NodeProvider::get_pnpm_package_from_lockfile(app)
565+
}
566+
};
567+
} else {
568+
pm_pkg = NodeProvider::get_pnpm_package_from_lockfile(app);
569+
}
570+
} else {
571+
pm_pkg = NodeProvider::get_pnpm_package_from_lockfile(app);
572+
}
573+
} else {
574+
pm_pkg = NodeProvider::get_pnpm_package_from_lockfile(app);
575+
}
539576
} else {
540-
// Default to pnpm 9
541-
pm_pkg = Pkg::new("pnpm-9_x");
577+
// Fall back to lockfile-based detection
578+
pm_pkg = NodeProvider::get_pnpm_package_from_lockfile(app);
542579
}
543580
} else if package_manager == "yarn" {
544581
pm_pkg = Pkg::new("yarn-1_x");
@@ -647,6 +684,20 @@ impl NodeProvider {
647684
all_deps
648685
}
649686

687+
fn get_pnpm_package_from_lockfile(app: &App) -> Pkg {
688+
let lockfile = app.read_file("pnpm-lock.yaml").unwrap_or_default();
689+
if lockfile.starts_with("lockfileVersion: 5.3") {
690+
Pkg::new("pnpm-6_x")
691+
} else if lockfile.starts_with("lockfileVersion: 5.4") {
692+
Pkg::new("pnpm-7_x")
693+
} else if lockfile.starts_with("lockfileVersion: '6.0'") {
694+
Pkg::new("pnpm-8_x")
695+
} else {
696+
// lockfileVersion '9.0' and unknown versions default to pnpm 9
697+
Pkg::new("pnpm-9_x")
698+
}
699+
}
700+
650701
pub fn cache_tsbuildinfo_file(app: &App, build: &mut Phase) {
651702
let mut ts_config: TsConfigJson = app.read_json("tsconfig.json").unwrap_or_default();
652703
if let Some(ref extends) = ts_config.extends {
@@ -682,8 +733,19 @@ impl NodeProvider {
682733
}
683734
}
684735

736+
fn version_number_to_archive(version: u32) -> Option<&'static str> {
737+
AVAILABLE_NODE_VERSIONS
738+
.iter()
739+
.find(|(ver, _archive)| *ver == version)
740+
.map(|(_ver, archive)| *archive)
741+
}
742+
685743
fn version_number_to_pkg(version: u32) -> String {
686-
if AVAILABLE_NODE_VERSIONS.contains(&version) {
744+
let version_exists = AVAILABLE_NODE_VERSIONS
745+
.iter()
746+
.any(|(ver, _archive)| *ver == version);
747+
748+
if version_exists {
687749
format!("nodejs_{version}")
688750
} else {
689751
format!("nodejs_{DEFAULT_NODE_VERSION}")
@@ -698,16 +760,17 @@ fn parse_node_version_into_pkg(node_version: &str) -> String {
698760
});
699761
let mut available_lts_node_versions = AVAILABLE_NODE_VERSIONS
700762
.iter()
701-
.filter(|v| *v % 2 == 0)
702-
.collect::<Vec<_>>();
763+
.map(|(ver, _archive)| *ver)
764+
.filter(|v| v % 2 == 0)
765+
.collect::<Vec<u32>>();
703766

704767
// use newest node version first
705768
available_lts_node_versions.sort_by(|a, b| b.cmp(a));
706769
for version_number in available_lts_node_versions {
707770
let version_range_string = format!("{version_number}.x.x");
708771
let version_range: Range = version_range_string.parse().unwrap();
709772
if version_range.allows_any(&range) {
710-
return version_number_to_pkg(*version_number);
773+
return version_number_to_pkg(version_number);
711774
}
712775
}
713776
default_node_pkg_name
@@ -799,7 +862,7 @@ mod test {
799862
&App::new("examples/node")?,
800863
&Environment::default()
801864
)?,
802-
Pkg::new(version_number_to_pkg(22).as_str())
865+
Pkg::new(version_number_to_pkg(24).as_str())
803866
);
804867

805868
Ok(())

src/providers/node/nx.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,6 @@ pub struct Configuration {
4545
pub production: Option<Value>,
4646
}
4747

48-
#[derive(Debug, Serialize, PartialEq, Eq, Deserialize)]
49-
pub struct PackageJsonNx {
50-
pub nx: Option<PackageJsonNxTargets>,
51-
}
52-
53-
#[derive(Debug, Serialize, PartialEq, Eq, Deserialize)]
54-
pub struct PackageJsonNxTargets {
55-
pub targets: Option<Targets>,
56-
}
57-
5848
pub struct Nx {}
5949

6050
const NX_APP_NAME_ENV_VAR: &str = "NX_APP_NAME";
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
source: tests/generate_plan_tests.rs
3+
expression: plan
4+
---
5+
{
6+
"providers": [],
7+
"buildImage": "[build_image]",
8+
"variables": {
9+
"CI": "true",
10+
"NIXPACKS_METADATA": "node",
11+
"NODE_ENV": "production",
12+
"NPM_CONFIG_PRODUCTION": "false"
13+
},
14+
"phases": {
15+
"build": {
16+
"name": "build",
17+
"dependsOn": [
18+
"install"
19+
],
20+
"cacheDirectories": [
21+
"node_modules/.cache"
22+
]
23+
},
24+
"install": {
25+
"name": "install",
26+
"dependsOn": [
27+
"setup"
28+
],
29+
"cmds": [
30+
"npm install -g [email protected] && corepack enable",
31+
"pnpm i --frozen-lockfile"
32+
],
33+
"cacheDirectories": [
34+
"/root/.local/share/pnpm/store/v3"
35+
],
36+
"paths": [
37+
"/app/node_modules/.bin"
38+
]
39+
},
40+
"setup": {
41+
"name": "setup",
42+
"nixPkgs": [
43+
"nodejs_24",
44+
"pnpm-10_x"
45+
],
46+
"nixOverlays": [
47+
"https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
48+
],
49+
"nixpkgsArchive": "[archive]"
50+
}
51+
},
52+
"start": {
53+
"cmd": "pnpm run start"
54+
}
55+
}

tests/snapshots/generate_plan_tests__node_pnpm_corepack.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ expression: plan
4343
"setup": {
4444
"name": "setup",
4545
"nixPkgs": [
46-
"nodejs_22",
46+
"nodejs_24",
4747
"pnpm-9_x"
4848
],
4949
"nixOverlays": [

0 commit comments

Comments
 (0)