Package npm/TypeScript/Bun CLI tools for Nix. Use when creating Nix derivations for JavaScript/TypeScript tools from npm registry or GitHub sources, handling pre-built packages or source builds with dependency management.
Install
npx skillscat add ypares/agent-skills/package-npm-nix Install via the SkillsCat registry.
For tools already built and published to npm (fastest approach):{ lib, stdenv, fetchzip, nodejs, }: stdenv.mkDerivation rec { pname = "tool-name"; version = "1.0.0"; src = fetchzip { url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz"; hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; nativeBuildInputs = [ nodejs ]; installPhase = '' runHook preInstall mkdir -p $out/bin cp $src/dist/cli.js $out/bin/tool-name chmod +x $out/bin/tool-name # Fix shebang substituteInPlace $out/bin/tool-name \ --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node" runHook postInstall ''; meta = with lib; { description = "Tool description"; homepage = "https://github.com/org/repo"; license = licenses.mit; sourceProvenance = with lib.sourceTypes; [ binaryBytecode ]; maintainers = with maintainers; [ ]; mainProgram = "tool-name"; platforms = platforms.all; }; }Get the hash:
nix-prefetch-url --unpack https://registry.npmjs.org/tool-name/-/tool-name-1.0.0.tgz # Convert to SRI format: nix hash convert --to sri --hash-algo sha256 <hash-output></pre_built_from_npm>
For tools that need to be built from source using Bun:</source_build_with_bun> </quick_start> **Determine build approach**:{ lib, stdenv, stdenvNoCC, fetchFromGitHub, bun, makeBinaryWrapper, nodejs, autoPatchelfHook, }: let fetchBunDeps = { src, hash, ... }@args: stdenvNoCC.mkDerivation { pname = args.pname or "${src.name or "source"}-bun-deps"; version = args.version or src.version or "unknown"; inherit src; nativeBuildInputs = [ bun ]; buildPhase = '' export HOME=$TMPDIR export npm_config_ignore_scripts=true bun install --no-progress --frozen-lockfile --ignore-scripts ''; installPhase = '' mkdir -p $out cp -R ./node_modules $out cp ./bun.lock $out/ ''; dontFixup = true; outputHash = hash; outputHashAlgo = "sha256"; outputHashMode = "recursive"; }; version = "1.0.0"; src = fetchFromGitHub { owner = "org"; repo = "repo"; rev = "v${version}"; hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; node_modules = fetchBunDeps { pname = "tool-name-bun-deps"; inherit version src; hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="; }; in stdenv.mkDerivation rec { pname = "tool-name"; inherit version src; nativeBuildInputs = [ bun nodejs makeBinaryWrapper autoPatchelfHook ]; buildInputs = [ stdenv.cc.cc.lib ]; buildPhase = '' # Verify lockfile match diff -q ./bun.lock ${node_modules}/bun.lock || exit 1 # Copy and patch node_modules cp -R ${node_modules}/node_modules . chmod -R u+w node_modules patchShebangs node_modules autoPatchelf node_modules export HOME=$TMPDIR export npm_config_ignore_scripts=true bun run build ''; installPhase = '' mkdir -p $out/bin cp dist/tool-name $out/bin/tool-name chmod +x $out/bin/tool-name ''; dontStrip = true; meta = with lib; { description = "Tool description"; homepage = "https://github.com/org/repo"; license = licenses.mit; sourceProvenance = with lib.sourceTypes; [ fromSource ]; maintainers = with maintainers; [ ]; mainProgram = "tool-name"; platforms = [ "x86_64-linux" ]; }; }Check the npm package:
# Download and inspect nix-prefetch-url --unpack https://registry.npmjs.org/package/-/package-1.0.0.tgz ls -la /nix/store/<hash>-package-1.0.0.tgz/If
dist/directory exists with built files:
→ Use pre-built approach (simpler, faster)If only
src/exists or package.json has build scripts:
→ Use source build approachCheck package.json for:
"bin"field: Shows what executables are provided"type": "module": ES modules (common in modern packages)"scripts": Build commands (indicates source build needed)- Runtime: Look for bun, node, or specific version requirements</step_1_identify_package_type>
For pre-built packages:
# Fetch npm tarball
nix-prefetch-url --unpack https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz
# Output: 1abc... (base32 format)
# Convert to SRI format
nix hash convert --to sri --hash-algo sha256 1abc...
# Output: sha256-xyz...For source builds:
# Get GitHub source hash
nix-prefetch-url --unpack https://github.com/org/repo/archive/v1.0.0.tar.gz
# Get dependencies hash (requires iteration):
# 1. Use lib.fakeHash in fetchBunDeps
# 2. Try to build
# 3. Nix will show expected hash in error
# 4. Update hash and rebuild</step_2_fetch_hashes>
**Create package structure**:
mkdir -p packages/tool-nameCreate packages/tool-name/package.nix with full derivation (see quick_start).
Create packages/tool-name/default.nix:
{ pkgs }: pkgs.callPackage ./package.nix { }This two-file pattern allows the package to be used standalone or integrated into a flake.
</step_3_create_package_files>
WASM files or other assets:
installPhase = ''
mkdir -p $out/bin
cp $src/dist/cli.js $out/bin/tool
cp $src/dist/*.wasm $out/bin/ # Copy WASM alongside
chmod +x $out/bin/tool
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
'';Multiple executables:
# package.json might have:
# "bin": {
# "tool": "dist/cli.js",
# "tool-admin": "dist/admin.js"
# }
installPhase = ''
mkdir -p $out/bin
for exe in tool tool-admin; do
cp $src/dist/$exe.js $out/bin/$exe
chmod +x $out/bin/$exe
substituteInPlace $out/bin/$exe \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
done
'';
meta.mainProgram = "tool"; # Primary commandPlatform-specific binaries:
meta = {
platforms = [ "x86_64-linux" ]; # Bun-compiled binaries often Linux-only
# or
platforms = platforms.all; # Pure JS works everywhere
};</step_4_handle_special_cases>
**Build and test**:
# Build
nix build .#tool-name
# Test the binary
./result/bin/tool-name --version
./result/bin/tool-name --help
# Check dependencies (Linux)
ldd ./result/bin/tool-name # Should show all deps resolved
# Format
nix fmt
# Run flake checks
nix flake check</step_5_test_build>
Every package must have complete metadata:
meta = with lib; {
description = "Clear, concise description";
homepage = "https://project-homepage.com";
changelog = "https://github.com/org/repo/releases"; # Optional but nice
license = licenses.mit; # or licenses.unfree for proprietary
sourceProvenance = with lib.sourceTypes; [
fromSource # Built from source
# or
binaryBytecode # Pre-built JS/TS (npm dist/)
# or
binaryNativeCode # Compiled binaries
];
maintainers = with maintainers; [ ]; # Empty OK for community packages
mainProgram = "binary-name";
platforms = platforms.all; # or specific: [ "x86_64-linux" ]
};</essential_fields>
**Choose based on what you're packaging**:
fromSource: Built from TypeScript/source during derivationbinaryBytecode: Pre-compiled JS from npm registrybinaryNativeCode: Native binaries (Rust, Go, Bun-compiled)
This affects security auditing and reproducibility expectations.
</source_provenance_guide>
</metadata_requirements>
# Single file
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
# Multiple files
find $out/bin -type f -exec substituteInPlace {} \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node" \;The --replace-quiet flag suppresses warnings if pattern not found.
</shebang_replacement>
nativeBuildInputs = [
bun
nodejs
makeBinaryWrapper
autoPatchelfHook # Linux: patches ELF binaries
];
buildInputs = [
stdenv.cc.cc.lib # Provides libgcc_s.so.1, libstdc++.so.6
];
autoPatchelfIgnoreMissingDeps = [
"libc.musl-x86_64.so.1" # Ignore musl if not available
];autoPatchelf runs automatically on Linux, fixing RPATH for .so files.
</native_dependencies>
# Bun embeds JavaScript in the binary
dontStrip = true;Stripping would remove the embedded JS, breaking the program.
</bun_compiled_binaries>
# After nix-prefetch-url
ls -la /nix/store/*-pkg-1.0.0.tgz/
# Common layouts:
# dist/cli.js → Pre-built, use directly
# dist/index.js → Main entry, check package.json "bin"
# src/index.ts → Source only, need to build
# lib/ → Built CommonJS
# esm/ → Built ES modulesCheck package.json to find the correct entry point.
</checking_tarball_contents>
</common_patterns>
❌ Hardcode node paths:
# Bad
"#!/usr/bin/node" # Won't work on NixOS✅ Use substituteInPlace:
# Good
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"❌ Skip hash verification:
# Bad - insecure
hash = lib.fakeHash;✅ Get real hash:
# Good - reproducible and secure
hash = "sha256-actual-hash-here";❌ Forget to make executable:
# Bad - won't run
cp $src/dist/cli.js $out/bin/tool✅ Set executable bit:
# Good
cp $src/dist/cli.js $out/bin/tool
chmod +x $out/bin/tool❌ Strip Bun binaries:
# Bad - breaks Bun-compiled executables
# (default behavior strips binaries)✅ Disable stripping:
# Good
dontStrip = true;</avoid_these>
</anti_patterns>
**Error: "hash mismatch in fixed-output derivation"**
The hash you provided doesn't match what Nix fetched.
Solution:
- Nix error shows "got: sha256-XYZ..."
- Copy that hash into your derivation
- Rebuild
For fetchBunDeps, this is expected the first time—use the error output to get the correct hash.
</hash_mismatch>
Check:
# List what was actually built
ls -R result/
# Check package.json "bin" field
cat /nix/store/*-source/package.json | jq .bin
# Check build output location
cat /nix/store/*-source/package.json | jq .scripts.buildThe build might output to a different directory than expected.
</missing_executable>
The binary needs ELF patching for native dependencies.
Solution:
nativeBuildInputs = [
autoPatchelfHook
];
buildInputs = [
stdenv.cc.cc.lib
];For node_modules with native addons:
buildPhase = ''
cp -R ${node_modules}/node_modules .
chmod -R u+w node_modules
autoPatchelf node_modules # Patch .node files
'';</elf_interpreter_error>
**Error: "bun.lock mismatch"**
The lockfile in your source doesn't match the cached dependencies.
This happens when:
- Source version updated but dependency hash not updated
- Source repo has uncommitted lockfile changes
Solution:
- Update source hash to match new version
- Set dependency hash to
lib.fakeHash - Build to get correct dependency hash
- Update dependency hash
- Rebuild</bun_lock_mismatch>
-
nix build .#package-namesucceeds -
./result/bin/tool --versionworks -
./result/bin/tool --helpworks -
nix flake checkpasses -
meta.descriptionis clear and concise -
meta.homepagepoints to project site -
meta.licenseis correct -
meta.sourceProvenancematches what you packaged -
meta.mainProgramis set -
meta.platformsis appropriate for the tool - All hashes are real (no
lib.fakeHash) - Shebangs use Nix store paths, not /usr/bin
- File is formatted with
nix fmt</build_checklist>
Consider asking maintainers with macOS/ARM to test, or:
- Mark platforms conservatively based on what you can test
- Note in package that other platforms are untested
- Let CI or other contributors expand platform support</testing_on_other_platforms>
- Clean build with no warnings or errors
- Working executable in
result/bin/ - Complete and accurate metadata
- Proper source provenance classification
- All dependencies resolved (no missing libraries)
- Reproducible builds (real hashes, no network access during build)
- Follows Nix packaging conventions (shebang patching, proper phases)</success_criteria>