package-npm-nix

Original🇺🇸 English
Translated

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.

2installs
Added on

NPX Install

npx skill4agent add ypares/agent-skills package-npm-nix

Tags

Translated version includes tags in frontmatter
<objective> Create Nix packages for npm-based CLI tools, covering both pre-built packages from npm registry and source builds with proper dependency management. This skill provides patterns for fetching, building, and packaging JavaScript/TypeScript/Bun tools in Nix environments. </objective>
<quick_start> <pre_built_from_npm> For tools already built and published to npm (fastest approach):
nix
{
  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:
bash
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:
nix
{
  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>
<workflow> <step_1_identify_package_type> **Determine build approach**:
Check the npm package:
bash
# 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)
  • Runtime: Look for bun, node, or specific version requirements </step_1_identify_package_type>
<step_2_fetch_hashes> Get source and dependency hashes:
For pre-built packages:
bash
# 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:
bash
# 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:
bash
mkdir -p packages/tool-name
Create
packages/tool-name/package.nix
with full derivation (see quick_start).
Create
packages/tool-name/default.nix
:
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:
nix
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:
nix
# 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 command
Platform-specific binaries:
nix
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:
bash
# 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> </workflow>
<metadata_requirements> <essential_fields> Every package must have complete metadata:
nix
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 derivation
  • binaryBytecode
    : Pre-compiled JS from npm registry
  • binaryNativeCode
    : 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:
nix
# 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):
nix
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:
nix
# 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:
bash
# 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 modules
Check package.json to find the correct entry point. </checking_tarball_contents> </common_patterns>
<anti_patterns> <avoid_these> Don't do this:
❌ Hardcode node paths:
nix
# Bad
"#!/usr/bin/node"  # Won't work on NixOS
✅ Use substituteInPlace:
nix
# Good
substituteInPlace $out/bin/tool \
  --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
❌ Skip hash verification:
nix
# Bad - insecure
hash = lib.fakeHash;
✅ Get real hash:
nix
# Good - reproducible and secure
hash = "sha256-actual-hash-here";
❌ Forget to make executable:
nix
# Bad - won't run
cp $src/dist/cli.js $out/bin/tool
✅ Set executable bit:
nix
# Good
cp $src/dist/cli.js $out/bin/tool
chmod +x $out/bin/tool
❌ Strip Bun binaries:
nix
# Bad - breaks Bun-compiled executables
# (default behavior strips binaries)
✅ Disable stripping:
nix
# Good
dontStrip = true;
</avoid_these> </anti_patterns>
<troubleshooting> <hash_mismatch> **Error: "hash mismatch in fixed-output derivation"**
The hash you provided doesn't match what Nix fetched.
Solution:
  1. Nix error shows "got: sha256-XYZ..."
  2. Copy that hash into your derivation
  3. Rebuild
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:
bash
# 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.build
The 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:
nix
nativeBuildInputs = [
  autoPatchelfHook
];

buildInputs = [
  stdenv.cc.cc.lib
];
For node_modules with native addons:
nix
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:
  • Source version updated but dependency hash not updated
  • Source repo has uncommitted lockfile changes
Solution:
  1. Update source hash to match new version
  2. Set dependency hash to
    lib.fakeHash
  3. Build to get correct dependency hash
  4. Update dependency hash
  5. Rebuild </bun_lock_mismatch> </troubleshooting>
<validation> <build_checklist> Before considering the package done:
  • nix build .#package-name
    succeeds
  • ./result/bin/tool --version
    works
  • ./result/bin/tool --help
    works
  • nix flake check
    passes
  • meta.description
    is clear and concise
  • meta.homepage
    points to project site
  • meta.license
    is correct
  • meta.sourceProvenance
    matches what you packaged
  • meta.mainProgram
    is set
  • meta.platforms
    is 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>
<testing_on_other_platforms> If you only have Linux but package claims
platforms.all
:
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> </validation>
<success_criteria> A well-packaged npm tool has:
  • 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>