From nix-skills
Packaging Node.js apps (npm/pnpm/bun) as Nix derivations. Use when creating buildNpmPackage expressions, fetchPnpmDeps-based derivations, or integrating JavaScript/TypeScript CLI tools into nix-darwin or NixOS configurations.
npx claudepluginhub eotel/claude-marketplace --plugin nix-skillsThis skill uses the workspace's default tool permissions.
Identify the lockfile in the project root, then choose the approach:
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Identify the lockfile in the project root, then choose the approach:
| Lockfile | Package Manager | Nix Approach |
|---|---|---|
package-lock.json | npm | buildNpmPackage (preferred) or importNpmLock |
pnpm-lock.yaml | pnpm | stdenv.mkDerivation + fetchPnpmDeps + pnpmConfigHook |
bun.lockb / bun.lock | bun | No native Nix builder — use npx wrapper or convert to npm |
If the project has no lockfile, generate one first (npm install, pnpm install, or bun install) and commit it.
For projects with package-lock.json. This is the most mature path.
{ lib, buildNpmPackage, fetchFromGitHub }:
buildNpmPackage rec {
pname = "my-tool";
version = "1.0.0";
src = fetchFromGitHub {
owner = "owner";
repo = "repo";
rev = "v${version}";
hash = ""; # Build once to get hash
};
npmDepsHash = ""; # Build once to get hash
# If the project has a build step (TypeScript, bundler, etc.)
# it runs automatically via `npm run build` in the build phase.
# Set this to skip the build phase if there is no build script:
# dontNpmBuild = true;
meta = {
description = "Description";
homepage = "https://github.com/owner/repo";
license = lib.licenses.mit;
mainProgram = "my-tool";
};
}
npmDepsHash — Hash of the npm dependency tarball. Leave empty on first build, then copy from the error.dontNpmBuild — Set to true if package.json has no build script.npmFlags — Extra flags passed to npm ci (e.g. [ "--legacy-peer-deps" ]).makeCacheWritable — Set true if postinstall scripts write to the cache.NODE_OPTIONS — Set "--openssl-legacy-provider" for older webpack projects.For projects that need a more granular lock-based approach:
{ lib, stdenv, importNpmLock, fetchFromGitHub, nodejs }:
stdenv.mkDerivation {
pname = "my-tool";
version = "1.0.0";
src = fetchFromGitHub { /* ... */ };
npmDeps = importNpmLock.buildNodeModules {
npmRoot = ./.;
nodejs = nodejs;
};
# ...
}
Use importNpmLock when buildNpmPackage hash computation is unreliable (e.g. packages with platform-specific optional deps).
For projects with pnpm-lock.yaml. There is no buildPnpmPackage helper in nixpkgs yet, so assemble manually.
{
lib,
stdenv,
fetchFromGitHub,
fetchPnpmDeps,
nodejs,
pnpm_10, # Pin to the major version matching the project
pnpmConfigHook,
makeWrapper,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "my-tool";
version = "1.0.0";
src = fetchFromGitHub {
owner = "owner";
repo = "repo";
rev = "v${finalAttrs.version}";
hash = ""; # Build once to get hash
};
nativeBuildInputs = [
nodejs
pnpm_10
pnpmConfigHook
makeWrapper
];
pnpmDeps = fetchPnpmDeps {
inherit (finalAttrs) pname version src;
pnpm = pnpm_10;
hash = ""; # Build once to get hash
};
buildPhase = ''
runHook preBuild
pnpm build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib/${finalAttrs.pname} $out/bin
cp -r dist node_modules package.json $out/lib/${finalAttrs.pname}/
makeWrapper ${nodejs}/bin/node $out/bin/${finalAttrs.pname} \
--add-flags "$out/lib/${finalAttrs.pname}/dist/index.js"
runHook postInstall
'';
meta = {
description = "Description";
homepage = "https://github.com/owner/repo";
license = lib.licenses.mit;
mainProgram = "my-tool";
};
})
The pnpm version must match the lockfile format. Mismatches cause silent corruption.
Lockfile lockfileVersion | Nix attribute |
|---|---|
'6.0' or '6.1' | pnpm_8 |
'9.0' | pnpm_9 or pnpm_10 |
Check the first line of pnpm-lock.yaml for the version.
The buildPhase invokes whatever pnpm build (or pnpm run build) triggers in package.json. This typically runs tsc, tsup, esbuild, or a bundler. Do not call tsc directly — use the project's build script.
For pnpm workspaces, you may need to build sub-packages first:
buildPhase = ''
runHook preBuild
pnpm --filter ui build # Build dependency workspace first
pnpm build # Then build the main package
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib/${finalAttrs.pname} $out/bin
cp -r dist node_modules package.json $out/lib/${finalAttrs.pname}/
# Copy workspace packages if needed at runtime
if [ -d packages ]; then
cp -r packages $out/lib/${finalAttrs.pname}/
fi
makeWrapper ${nodejs}/bin/node $out/bin/${finalAttrs.pname} \
--add-flags "$out/lib/${finalAttrs.pname}/dist/entry.js"
runHook postInstall
'';
There is no native Nix builder for bun lockfiles. Options:
npm install to generate package-lock.json, then use buildNpmPackage.fetchurl/fetchzip and wire them up. Only viable for projects with few deps.Hashes for src, npmDepsHash, and pnpmDeps are unknown until the first build.
hash = "";nix build .#my-tool (or darwin-rebuild switch)got: sha256-XXXX...sha256-... value into the corresponding hash fieldsrc hash first, then deps hash)This is the standard Nix workflow. There is no shortcut.
Place the derivation in a dedicated file:
home/
packages/
my-tool.nix # The derivation
pkgs.nix # Package list
In pkgs.nix (or equivalent):
home.packages = [
# ... other packages ...
(pkgs.callPackage ./packages/my-tool.nix { })
];
callPackage automatically passes nixpkgs attributes matching the function arguments.
If the project depends on native addons (e.g. better-sqlite3, sharp, bcrypt):
nativeBuildInputs = [
nodejs
pnpm_10
pnpmConfigHook
makeWrapper
python3 # Required by node-gyp
pkg-config # For finding native libraries
];
buildInputs = [
# Add native dependencies here, e.g.:
# vips # for sharp
# sqlite # for better-sqlite3
];
| Problem | Cause | Fix |
|---|---|---|
hash mismatch after update | Upstream changed deps | Rebuild with hash = "" to get new hash |
EACCES in build | Sandbox blocks network | Ensure all deps are fetched via fetchPnpmDeps/npmDepsHash |
postinstall script fails | Scripts try to download binaries | Set npmFlags = [ "--ignore-scripts" ] or patch |
pnpm: command not found | Wrong pnpm version in nativeBuildInputs | Match pnpm_N to lockfile version |
tsc: not found in build | TypeScript is a devDep, not in PATH | Use pnpm build (or npm run build) which resolves local bins |
| Missing files at runtime | installPhase didn't copy enough | Check what the entrypoint imports and copy those dirs |
Not every Node.js tool needs a Nix derivation. Prefer lighter alternatives when:
# In shell aliases
alias my-tool="npx my-tool@latest"
Only build a Nix derivation when you need reproducible, globally-available CLI tools or system services.
node2nix — The tool is unmaintained and incompatible with modern Node.js/lockfile versions.pnpm2nix — Unmaintained, broken with lockfile versions > 5.0.yarn2nix — Only works with Yarn v1.makeWrapper to set up NODE_PATH and entry points.npm install or pnpm install in buildPhase — Deps must come from the fixed-output derivation (npmDepsHash/fetchPnpmDeps). Network access is blocked in the sandbox.nodejs to a specific major version unless the project requires it — use the default nodejs attribute.pnpm_10 everywhere (both nativeBuildInputs and fetchPnpmDeps).