From 1419b8b44934164897713001ae377277046883a5 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Thu, 4 Jun 2026 03:37:51 +0300 Subject: [PATCH 1/3] add support for buildNpmPackage --- CHANGELOG.md | 6 +++++ README.md | 1 + src/builder.rs | 5 ++++ src/cli.rs | 1 + src/lang/mod.rs | 1 + src/lang/node/mod.rs | 32 ++++++++++++++++++++++ src/main.rs | 51 +++++++++++++++++++++++++++++++++++ tests/cmd/npm.out/default.nix | 34 +++++++++++++++++++++++ tests/cmd/npm.toml | 1 + 9 files changed, 132 insertions(+) create mode 100644 src/lang/node/mod.rs create mode 100644 tests/cmd/npm.out/default.nix create mode 100644 tests/cmd/npm.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7450c273..337f854c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add support for `buildNpmPackage` + ## v0.3.6 - 2026-05-15 ### Fixes diff --git a/README.md b/README.md index 1b725834..b4dba50f 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Options: - `buildRustPackage` - `buildPythonApplication` and `buildPythonPackage` - `buildGoModule` +- `buildNpmPackage` ### Supported fetchers diff --git a/src/builder.rs b/src/builder.rs index 5992b5ce..c61dcf7d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -9,6 +9,7 @@ pub enum Builder { application: bool, rust: Option, }, + BuildNpmPackage, BuildRustPackage { vendor: CargoVendor, }, @@ -40,6 +41,10 @@ impl Display for Builder { } } + Builder::BuildNpmPackage => { + write!(f, "buildNpmPackage")?; + } + Builder::BuildRustPackage { vendor } => { write!( f, diff --git a/src/cli.rs b/src/cli.rs index ca3f7eb0..dc63d2f8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -74,6 +74,7 @@ pub struct Opts { #[clap(rename_all = "camelCase")] pub enum BuilderFunction { BuildGoModule, + BuildNpmPackage, BuildPythonApplication, BuildPythonPackage, BuildRustPackage, diff --git a/src/lang/mod.rs b/src/lang/mod.rs index f263af52..71c1ef13 100644 --- a/src/lang/mod.rs +++ b/src/lang/mod.rs @@ -1,3 +1,4 @@ pub mod go; +pub mod node; pub mod python; pub mod rust; diff --git a/src/lang/node/mod.rs b/src/lang/node/mod.rs new file mode 100644 index 00000000..5aebe4f2 --- /dev/null +++ b/src/lang/node/mod.rs @@ -0,0 +1,32 @@ +use std::{fs::File, io::BufReader, path::Path}; + +use serde::Deserialize; +use tracing::warn; + +use crate::utils::ResultExt; + +#[derive(Deserialize)] +struct PackageJson { + #[serde(default)] + scripts: Scripts, +} + +#[derive(Default, Deserialize)] +struct Scripts { + build: Option, +} + +// assumes a build script exists when package.json can't be read or parsed +pub fn npm_lacks_build_script(src_dir: &Path) -> bool { + let Some(file) = File::open(src_dir.join("package.json")).ok_inspect(|e| warn!("{e}")) else { + return false; + }; + + match serde_json::from_reader::<_, PackageJson>(BufReader::new(file)) { + Ok(package) => package.scripts.build.is_none(), + Err(e) => { + warn!("{e}"); + false + } + } +} diff --git a/src/main.rs b/src/main.rs index e3e95a00..d04e2b2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,6 +49,7 @@ use crate::{ inputs::{AllInputs, write_all_lambda_inputs, write_inputs, write_lambda_input}, lang::{ go::{load_go_dependencies, write_ldflags}, + node::npm_lacks_build_script, python::{Pyproject, PythonDependencies, parse_requirements_txt}, rust::{cargo_deps_hash, load_cargo_lock, write_cargo_lock}, }, @@ -396,6 +397,9 @@ async fn run() -> Result<()> { let has_cmake = src_dir.join("CMakeLists.txt").is_file(); let has_go = src_dir.join("go.mod").is_file(); let has_meson = src_dir.join("meson.build").is_file(); + let has_npm = src_dir.join("package.json").is_file(); + let has_npm_lock = src_dir.join("package-lock.json").is_file() + || src_dir.join("npm-shrinkwrap.json").is_file(); let has_zig = src_dir.join("build.zig").is_file(); let pyproject_toml = src_dir.join("pyproject.toml"); let has_python = pyproject_toml.is_file() || src_dir.join("setup.py").is_file(); @@ -403,6 +407,7 @@ async fn run() -> Result<()> { let builder = match (opts.builder, opts.cargo_vendor) { (Some(builder), rust @ Some(vendor)) if has_cargo => match builder { BuilderFunction::BuildGoModule => Builder::BuildGoModule, + BuilderFunction::BuildNpmPackage => Builder::BuildNpmPackage, BuilderFunction::BuildPythonApplication => Builder::BuildPythonPackage { application: true, rust, @@ -419,6 +424,7 @@ async fn run() -> Result<()> { let rust = has_cargo.then_some(CargoVendor::FetchCargoVendor); match builder { BuilderFunction::BuildGoModule => Builder::BuildGoModule, + BuilderFunction::BuildNpmPackage => Builder::BuildNpmPackage, BuilderFunction::BuildPythonApplication => Builder::BuildPythonPackage { application: true, rust, @@ -471,6 +477,10 @@ async fn run() -> Result<()> { } } + if has_npm { + builders.push(Builder::BuildNpmPackage); + } + builders.push(Builder::MkDerivation { rust: None }); builders.push(Builder::MkDerivationNoCC); @@ -512,6 +522,9 @@ async fn run() -> Result<()> { Builder::BuildGoModule => { writeln!(out, " buildGoModule,")?; } + Builder::BuildNpmPackage => { + writeln!(out, " buildNpmPackage,")?; + } Builder::BuildPythonPackage { application, rust } => { writeln!( out, @@ -637,6 +650,44 @@ async fn run() -> Result<()> { res } + Builder::BuildNpmPackage => { + let hash = if has_npm_lock + && let Some(hash) = fod_hash(format!( + r#"(import({nixpkgs}){{}}).fetchNpmDeps{{src={src};hash="{FAKE_HASH}";}}"#, + )) + .await + { + hash + } else { + FAKE_HASH.into() + }; + + let res = write_all_lambda_inputs(&mut out, &inputs, &mut BTreeSet::new())?; + if nix_update_script { + writeln!(out, " nix-update-script,")?; + } + + writedoc! {out, r#" + }}: + + buildNpmPackage (finalAttrs: {{ + pname = {pname:?}; + version = {version:?}; + __structuredAttrs = true; + + src = {src_expr}; + + npmDepsHash = "{hash}"; + + "#}?; + + if npm_lacks_build_script(&src_dir) { + writeln!(out, " dontNpmBuild = true;\n")?; + } + + res + } + Builder::BuildPythonPackage { application, rust } => { enum CargoVendorData { Hash(String), diff --git a/tests/cmd/npm.out/default.nix b/tests/cmd/npm.out/default.nix new file mode 100644 index 00000000..81b86339 --- /dev/null +++ b/tests/cmd/npm.out/default.nix @@ -0,0 +1,34 @@ +{ + lib, + buildNpmPackage, + fetchFromGitHub, + nix-update-script, +}: + +buildNpmPackage (finalAttrs: { + pname = "cowsay"; + version = "1.5.0"; + __structuredAttrs = true; + + src = fetchFromGitHub { + owner = "piuccio"; + repo = "cowsay"; + tag = "v${finalAttrs.version}"; + hash = "sha256-TZ3EQGzVptNqK3cNrkLnyP1FzBd81XaszVucEnmBy4Y="; + }; + + npmDepsHash = "sha256-MIvLeuElaN9IbdB+SMgOLNTeycaK0k/M/R+xRxSD4U8="; + + dontNpmBuild = true; + + passthru.updateScript = nix-update-script { }; + + meta = { + description = "[..]"; + homepage = "https://github.com/piuccio/cowsay"; + changelog = "https://github.com/piuccio/cowsay/releases/tag/${finalAttrs.src.tag}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ alice ]; + mainProgram = "cowsay"; + }; +}) diff --git a/tests/cmd/npm.toml b/tests/cmd/npm.toml new file mode 100644 index 00000000..b7cd266e --- /dev/null +++ b/tests/cmd/npm.toml @@ -0,0 +1 @@ +args = ["--headless", "--url=https://github.com/piuccio/cowsay", "--rev=v1.5.0"] From 9e444fe4f7829520db147d29e69b9bc6215d9415 Mon Sep 17 00:00:00 2001 From: figsoda Date: Sun, 5 Jul 2026 21:17:39 -0400 Subject: [PATCH 2/3] node -> npm --- src/lang/mod.rs | 2 +- src/lang/{node => npm}/mod.rs | 0 src/main.rs | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/lang/{node => npm}/mod.rs (100%) diff --git a/src/lang/mod.rs b/src/lang/mod.rs index 71c1ef13..ce0c9ed9 100644 --- a/src/lang/mod.rs +++ b/src/lang/mod.rs @@ -1,4 +1,4 @@ pub mod go; -pub mod node; +pub mod npm; pub mod python; pub mod rust; diff --git a/src/lang/node/mod.rs b/src/lang/npm/mod.rs similarity index 100% rename from src/lang/node/mod.rs rename to src/lang/npm/mod.rs diff --git a/src/main.rs b/src/main.rs index 3db06898..53b735f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ use crate::{ inputs::{AllInputs, write_all_lambda_inputs, write_inputs, write_lambda_input}, lang::{ go::{load_go_dependencies, write_ldflags}, - node::npm_lacks_build_script, + npm::npm_lacks_build_script, python::{Pyproject, PythonDependencies, parse_requirements_txt}, rust::{cargo_deps_hash, load_cargo_lock, write_cargo_lock}, }, From fb4cbf5fa1b0bc2d0ffcf07e33058b7e43f6fe3b Mon Sep 17 00:00:00 2001 From: figsoda Date: Sun, 5 Jul 2026 21:23:42 -0400 Subject: [PATCH 3/3] npm: small refactors `npm_lacks_build_script` -> `npm_has_build_script` `serde_json::from_reader` -> `serde_json::from_str`: [1] [1]: https://docs.rs/serde_json/1.0.150/serde_json/fn.from_reader.html > Note that counter to intuition, this function is usually slower than > reading a file completely into memory and then applying `from_str` or > `from_slice` on it. --- src/lang/npm/mod.rs | 16 +++++++++------- src/main.rs | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/lang/npm/mod.rs b/src/lang/npm/mod.rs index 5aebe4f2..c53b0fea 100644 --- a/src/lang/npm/mod.rs +++ b/src/lang/npm/mod.rs @@ -1,4 +1,4 @@ -use std::{fs::File, io::BufReader, path::Path}; +use std::{fs::read_to_string, path::Path}; use serde::Deserialize; use tracing::warn; @@ -17,16 +17,18 @@ struct Scripts { } // assumes a build script exists when package.json can't be read or parsed -pub fn npm_lacks_build_script(src_dir: &Path) -> bool { - let Some(file) = File::open(src_dir.join("package.json")).ok_inspect(|e| warn!("{e}")) else { - return false; +pub fn npm_has_build_script(src_dir: &Path) -> bool { + let Some(package_json) = + read_to_string(src_dir.join("package.json")).ok_inspect(|e| warn!("{e}")) + else { + return true; }; - match serde_json::from_reader::<_, PackageJson>(BufReader::new(file)) { - Ok(package) => package.scripts.build.is_none(), + match serde_json::from_str::(&package_json) { + Ok(package) => package.scripts.build.is_some(), Err(e) => { warn!("{e}"); - false + true } } } diff --git a/src/main.rs b/src/main.rs index 53b735f4..7e23f81f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ use crate::{ inputs::{AllInputs, write_all_lambda_inputs, write_inputs, write_lambda_input}, lang::{ go::{load_go_dependencies, write_ldflags}, - npm::npm_lacks_build_script, + npm::npm_has_build_script, python::{Pyproject, PythonDependencies, parse_requirements_txt}, rust::{cargo_deps_hash, load_cargo_lock, write_cargo_lock}, }, @@ -681,7 +681,7 @@ async fn run() -> Result<()> { "#}?; - if npm_lacks_build_script(&src_dir) { + if !npm_has_build_script(&src_dir) { writeln!(out, " dontNpmBuild = true;\n")?; }