configurations.
* <program>/ - program-specific dotfiles.
+Updating npm-based packages
+---------------------------
+
+Some packages are built from pre-built npm trees because their dependency
+trees are too large for recursive Guix packaging.
+``scripts/generate-npm-hash`` handles the update:
+
+ $ scripts/generate-npm-hash <name> <version>
+
+This npm-installs the named package at the given version and prints the
+``guix hash`` for the resulting ``node_modules/`` tree. Copy that hash
+into the package definition's ``local-file`` reference.
+
+You only need to run this script when bumping the npm package version
+-- ``guix home reconfigure`` uses the hash baked into the definition and
+never runs ``npm install`` itself.
+
TODO
----
2. Or use EnterWorktree for automatic isolation.
3. Or add a PreToolUse hook that auto-creates/switches to a per-session
branch before Edit/Write.
+
--- /dev/null
+;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;; License: GPL-3.0 or later.
+;;
+;; npm.scm — npm-install-build-system for npm/Node.js packages.
+;;
+;; This module provides a build system that installs npm packages
+;; inside a fixed-output derivation and wraps them with a shell
+;; script. The two-phase design avoids running npm during normal
+;; guix builds:
+;;
+;; 1. Fixed-output derivation — npm install fetches the package
+;; and its dependency tree into a content-addressed directory.
+;; The hash pins the exact dependency set; network access is
+;; granted because the output hash is declared up front.
+;;
+;; 2. Wrapper script — a minimal shell script that invokes Node.js
+;; on the installed entry point. This runs in a normal
+;; (sandboxed) derivation.
+
+(define-module (conf home npm)
+ #:use-module (guix gexp)
+ #:use-module (guix build-system)
+ #:use-module (guix build-system trivial)
+ #:use-module (guix utils)
+ #:use-module (gnu packages bash)
+ #:use-module (gnu packages base)
+ #:use-module (gnu packages node)
+ #:export (npm-install-build-system))
+
+(define (npm-prebuilt-drv bin-name npm-package version hash)
+ "Return a computed-file that npm-installs NPM-PACKAGE at VERSION."
+ (computed-file (string-append bin-name "-built-" version)
+ #~(begin
+ (use-modules (guix build utils))
+ (setenv "HOME" "/tmp")
+ (let* ((out #$output)
+ (node #$(file-append node-lts "/bin/node"))
+ (npm-cli #$(file-append node-lts
+ "/lib/node_modules/npm/bin/npm-cli.js"))
+ (bash #$(file-append bash-minimal "/bin/bash"))
+ (build-dir "/tmp/npm-build")
+ (bin-dir "/tmp/bin"))
+ ;; Phase 1: npm install (fixed-output).
+ (mkdir-p build-dir)
+ (with-output-to-file (string-append build-dir
+ "/package.json")
+ (lambda ()
+ (format #t "{\"dependencies\": {\"~a\": \"~a\"}}~%"
+ #$npm-package
+ #$version)))
+ ;; sh symlink for lifecycle scripts.
+ (mkdir-p bin-dir)
+ (symlink bash
+ (string-append bin-dir "/sh"))
+ ;; PATH must include node, bash, npm itself,
+ ;; and the sh symlink so lifecycle scripts
+ ;; that run before npm's bin/ is populated
+ ;; can find a shell.
+ (setenv "PATH"
+ (string-append (dirname (dirname npm-cli))
+ "/bin:"
+ (dirname node)
+ ":"
+ (dirname bash)
+ ":"
+ bin-dir
+ ":"
+ (getenv "PATH")))
+ (with-directory-excursion build-dir
+ (invoke node npm-cli "install" "--production"
+ "--ignore-scripts"))
+ (mkdir-p out)
+ (copy-recursively (string-append build-dir
+ "/node_modules")
+ (string-append out "/node_modules"))))
+ #:options `(#:hash ,hash
+ #:hash-algo sha256
+ #:recursive? #t
+ #:leaked-env-vars ("http_proxy" "https_proxy")
+ #:modules ((guix build utils)))))
+
+(define (npm-build-gexp bin-name entry hash npm-package version)
+ "Return a builder gexp for an npm-install-build-system package."
+ (let ((built (npm-prebuilt-drv bin-name npm-package version hash))
+ (mkdir (file-append coreutils "/bin/mkdir"))
+ (chmod (file-append coreutils "/bin/chmod")))
+ #~(begin
+ ;; Phase 2: generate shell wrapper script.
+ (let* ((out (getenv "out"))
+ (bindir (string-append out "/bin"))
+ (script (string-append bindir "/"
+ #$bin-name)))
+ (system (string-append #$mkdir " -p " bindir))
+ (with-output-to-file script
+ (lambda ()
+ (format #t "#!~a~%"
+ #$(file-append bash-minimal "/bin/bash"))
+ (format #t "exec ~a ~a \"$@\"~%"
+ #$(file-append node-lts "/bin/node")
+ (string-append #$built "/node_modules/"
+ #$entry))))
+ (system (string-append #$chmod " +x " script))))))
+
+(define* (lower-npm-install name
+ #:key npm-package
+ version
+ entry
+ hash
+ (bin-name name)
+ (wrapper-name name)
+ source
+ target
+ system
+ inputs
+ native-inputs
+ outputs
+ #:allow-other-keys #:rest arguments)
+ "Lower an npm-install-build-system package to a bag."
+ (let ((builder (npm-build-gexp wrapper-name entry hash npm-package version)))
+ ((build-system-lower trivial-build-system)
+ name
+ #:source source
+ #:target target
+ #:system system
+ #:inputs inputs
+ #:native-inputs `(("coreutils" ,coreutils)
+ ,@native-inputs)
+ #:outputs outputs
+ #:builder builder)))
+
+(define npm-install-build-system
+ (build-system (name 'npm-install)
+ (description
+ "npm install in a fixed-output derivation, wrap with a Node.js
+shell script")
+ (lower lower-npm-install)))
--- /dev/null
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# generate-npm-hash — produce a pre-built npm tree and print its guix hash.
+#
+# Usage: generate-npm-hash <name> <version>
+#
+# npm-installs the named package at the given version, creates a
+# <name>-built-<version>/ directory with node_modules/, and prints the
+# guix hash for a local-file reference.
+#
+# Run this only when bumping the npm package version — guix home
+# reconfigure uses the hash baked into the package definition and never
+# runs npm install itself.
+
+set -o errexit -o nounset
+
+if [ $# -ne 2 ]; then
+ echo "Usage: generate-npm-hash <name> <version>" >&2
+ exit 1
+fi
+
+name=$1
+version=$2
+
+case $name in
+ *)
+ echo "Unknown package: $name" >&2
+ exit 1
+ ;;
+esac
+
+dir="${name}-built-${version}"
+tmp="${name}-tmp"
+
+rm -rf "$dir" "$tmp"
+mkdir "$tmp"
+echo "{\"dependencies\": {\"$npm_pkg\": \"$version\"}}" > "$tmp/package.json"
+(cd "$tmp" && npm install --production)
+mkdir -p "$dir"
+cp --recursive "$tmp/node_modules" "$dir/"
+rm -rf "$tmp"
+
+# Verify that npm's .bin/<name> symlink exists.
+if [ ! -e "$dir/node_modules/.bin/$name" ]; then
+ echo "ERROR: entry point .bin/$name missing in ${dir}/node_modules/" >&2
+ exit 1
+fi
+
+echo "Done. guix hash: $(guix hash --recursive "$dir")"