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.
88
85%
Does it follow best practices?
Impact
100%
1.63xAverage score across 3 eval scenarios
Low
Low-risk findings worth noting
<quick_start> <pre_built_from_npm> 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>
<source_build_with_bun> For tools that need to be built from source using Bun:
{
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" ];
};
}</source_build_with_bun> </quick_start>
**Determine build approach**: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 approach
Check 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)<step_2_fetch_hashes> Get source and dependency hashes:
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>
<step_3_create_package_files> 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>
<step_4_handle_special_cases> Common additional requirements:
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>
<step_5_test_build> 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>
<metadata_requirements> <essential_fields> 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>
<source_provenance_guide> 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>
<common_patterns> <shebang_replacement> Always replace shebangs for reproducibility:
# 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>
<native_dependencies> Handle native modules (like sqlite, sharp):
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_compiled_binaries> Don't strip Bun-compiled executables:
# Bun embeds JavaScript in the binary
dontStrip = true;Stripping would remove the embedded JS, breaking the program. </bun_compiled_binaries>
<checking_tarball_contents> Inspect npm package structure:
# 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>
<anti_patterns> <avoid_these> Don't do this:
❌ 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:
For fetchBunDeps, this is expected the first time—use the error output to get the correct hash.
</hash_mismatch>
<missing_executable> Error: Binary not found after build
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>
<elf_interpreter_error> Error: "No such file or directory" when running binary (Linux)
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>
<bun_lock_mismatch> Error: "bun.lock mismatch"
The lockfile in your source doesn't match the cached dependencies.
This happens when:
Solution:
lib.fakeHashnix build .#package-name succeeds./result/bin/tool --version works./result/bin/tool --help worksnix flake check passesmeta.description is clear and concisemeta.homepage points to project sitemeta.license is correctmeta.sourceProvenance matches what you packagedmeta.mainProgram is setmeta.platforms is appropriate for the toollib.fakeHash)nix fmt
</build_checklist><testing_on_other_platforms>
If you only have Linux but package claims platforms.all:
Consider asking maintainers with macOS/ARM to test, or:
<success_criteria> A well-packaged npm tool has:
result/bin/YPares/agent-skills
e2b6eda
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.