node2nix で Nixpkgs 非登録の Node ライブラリを管理する
はじめに
パッケージマネージャ Nix の利点の 1 つは対応しているパッケージの豊富さです。
「最近流行っているツールをちょっと動かしてみたい」と思った時、大抵は Nix で使えます。
例えば、OpenAI Codex CLI
を試したい時、以下のコマンドだけで実現できます。
nix-shell -p codex
利用可能なパッケージには Node.js に依存するモノ(e.g. Zenn CLI
)も多く含まれます。
しかし、マイナーなパッケージは Nixpkgs 未登録な場合も多いです。
node2nix は Node.js パッケージを Nix で扱えるように変換するためのツールです。
node2nix には大きく分けて 2 つの使い方があり、それぞれ挙動が異なります。
- package.json からプロジェクト全体を変換する方法
- パッケージリストからツールを個別に変換する方法
本記事では、各方法の利用方法・使い分けを解説します。
想定読者
- Nix 経験者
- flake.nix の devShell で環境構築している方
- Node.js パッケージを Nix で管理したい方
- pkgs に未登録のパッケージ含む
- node2nix の使い方を知りたい方
各方法の特徴
A. プロジェクトベース(package.json + lockfile)
npm install
の挙動を Nix で再現する方法です。
package.json
と package-lock.json
を元に、単一の node_modules を持つ derivation を生成します。
ESLint とそのプラグインのように、パッケージ同士が互いを参照する必要がある場合に最適です。
また、NPM の依存関係解決アルゴリズムで lockfile を作成し、lockfile の依存関係を元に Nix ファイルへ変換するため、再現性が高いです。
node2nix -l package-lock.json
{
"name": "node-eslint-env",
"version": "1.0.0",
"private": "true",
"devDependencies": {
"eslint": "9.34.0",
"typescript-eslint": "8.41.0"
}
}
B. リストベース(個別パッケージ)
NPM レジストリから、個々のパッケージの依存関係を取得し、Nix ファイルへ変換する方法です。
指定した各パッケージが、それぞれ独立した derivation として生成されます。
Zenn CLI
のような単体で完結するツールを入れる場合に最適です。
設定がシンプルで、他のパッケージへの影響を気にせず追加・更新できます。
node2nix -i node-packages.json
[
{ "eslint" : "9.34.0" },
{ "typescript-eslint" : "8.41.0" }
]
使い分け・ユースケース
Case 1. 単体で完結するツールを入れたい
e.g. Zenn CLI
-> B. リストベース(個別パッケージ)
Case 2. 互いに無関係な複数ツールを入れたい
e.g. Zenn CLI
、Codex CLI
-> B. リストベース(個別パッケージ)
Case 3. 本体とプラグインのように、相互参照が必要なツール群を入れたい
e.g. ESLint
、typescript-eslint
-> A. プロジェクトベース(package.json + lockfile)
Case 4. VSCode 拡張機能との連携が必要なツールを入れたい
e.g. textlint
、拡張 3w36zj6.textlint
-> A. プロジェクトベース(package.json + lockfile)
具体的な使い方 - flake.nix への導入手順
ここからは、実際に node2nix を使って Node.js パッケージを flake.nix の devShell に導入するまでの具体的な手順を解説します。
どちらの方法も作業の流れは同じです。
-
- node2nix で作成する Nix ファイルを保管する用のフォルダとして
node-pkgs
を作成
- node2nix で作成する Nix ファイルを保管する用のフォルダとして
-
-
node-pkgs
フォルダの中に json ファイルを作成
-
-
- json ファイルに利用したい Node.js パッケージを記述
-
-
node2nix
コマンドを実行
-
-
-
default.nix
、node-env.nix
、node-packages.nix
が自動生成
-
-
-
flake.nix
を記述
-
-
- 完了
主な違いは、json の種類・書き方、node2nix
のコマンド、node-packages.nix
の中身、flake.nix
の記述、です。
your-repo
├─ flake.lock
├─ flake.nix
└─ node-pkgs
├─ default.nix
├─ node-env.nix
├─ node-packages.nix
├─ package.json // 方法 A のみ
├─ package-lock.json // 方法 A のみ
└─ node-packages.json // 方法 B のみ
A. プロジェクトベース(package.json + lockfile)
package.json
を作成
cd your-repo
mkdir node-pkgs && cd node-pkgs
touch package.json
{
"name": "node-eslint-env",
"version": "1.0.0",
"private": "true",
"devDependencies": {
"eslint": "9.34.0",
"typescript-eslint": "8.41.0"
}
}
バージョンの指定方法
全てを検証したわけではありませんが、npm のドキュメントに記載されている方法が利用可能なはずです。
{
"name": "node-eslint-env",
"version": "1.0.0",
"private": "true",
"devDependencies": {
"eslint": "9.34.0",
"typescript-eslint": "*"
}
}
node2nix で Nix ファイルを作成
nix-shell -p nodejs_24 node2nix --run \
'npm install --package-lock-only --lockfile-version=2 && node2nix -d -l package-lock.json'
コマンドの補足解説
上記コマンドは 2 つの処理を行っています。
- npm で
package.json
からpackage-lock.json
を作成
nix-shell -p nodejs_24 --run 'npm install --package-lock-only --lockfile-version=2'
- node2nix で
package-lock.json
の依存関係を元に Nix ファイルを作成
nix-shell -p node2nix --run 'node2nix -d -l package-lock.json'
flake.nix を作成
{
description = "Node pkgs environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
my-node-pkgs = import ./node-pkgs/default.nix {
inherit pkgs system;
nodejs = pkgs.nodejs_24;
};
inherit (my-node-pkgs) nodeDependencies;
in
{
devShells.default = pkgs.mkShell {
packages = [
nodeDependencies
];
};
}
);
}
node_modules を必要とする場合
以下の場合、プロジェクトディレクトリ直下に node_modules
が存在する必要があります。
- 一部のツール(e.g.
ESLint
+ プラグイン) - 一部の VSCode 拡張(e.g.
3w36zj6.textlint
)
次の様に node_modules
のシンボリックリンクを作成することで対策できます。
in
{
devShells.default = pkgs.mkShell {
packages = [
nodeDependencies
];
shellHook = ''
ln -sfn ${nodeDependencies}/lib/node_modules ./node_modules
'';
};
}
仮に、シンボリックリンクを作っても正常に動作しない場合、以下の設定を試すと動くかもしれません。
in
{
devShells.default = pkgs.mkShell {
packages = [
nodeDependencies
];
shellHook = ''
ln -s ${nodeDependencies}/lib/node_modules ./node_modules
export PATH="${nodeDependencies}/bin:$PATH"
ln -s $NODE_PATH node_modules
'';
};
}
詳細は公式ドキュメントをご確認ください。
Using the Node.js environment in other Nix derivations
Creating a symlink to the node_modules folder in a shell session
B. リストベース(個別パッケージ)
node-packages.json
を作成
cd your-repo
mkdir node-pkgs && cd node-pkgs
touch node-packages.json
[
{ "eslint": "9.34.0" },
{ "typescript-eslint": "8.41.0" }
]
バージョンの指定方法
node2nix 公式ドキュメントをご確認ください。
[
"eslint",
{ "typescript-eslint": "1.0.0" },
{ "zenn-cli": "1.0.x" },
{ "node2nix": "git://github.com/svanderburg/node2nix.git" }
]
node2nix で Nix ファイルを作成
nix-shell -p node2nix --run \
'node2nix -i node-packages.json'
flake.nix を作成
{
description = "Node pkgs environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
my-node-pkgs = import ./node-pkgs/default.nix {
inherit pkgs system;
nodejs = pkgs.nodejs_24;
};
in
{
devShells.default = pkgs.mkShell {
packages = [
my-node-pkgs."eslint-9.34.0"
my-node-pkgs."typescript-eslint-8.41.0"
];
};
}
);
}
packages の記述方法について
以下の様な記述が可能です。
devShells.default = pkgs.mkShell {
packages = [
# json での記述:
# "eslint"
my-node-pkgs.eslint
# json での記述:
# { "typescript-eslint": "1.0.0" }
my-node-pkgs."typescript-eslint-1.0.0"
# json での記述:
# { "typescript-eslint": "1.0.0" }
my-node-pkgs."zenn-cli-1.0.x"
# json での記述:
# { "node2nix": "git://github.com/svanderburg/node2nix.git" }
my-node-pkgs."node2nix-git://github.com/svanderburg/node2nix.git"
];
};
詳細は node2nix 公式ドキュメントをご確認ください。
トラブルシューティング
ここまでの解説で紹介した手順は、いくつかのエラーを乗り越えた末にたどり着いた方法です。
誰かの参考になるかもしれないので、本項では、node2nix を使う過程で遭遇したエラーと対処法を紹介します。
error: attribute 'nodejs_14' missing
発生事象
flake.nix にて node2nix で作成した Nix ファイルを使用する際、ビルド時に nodejs_14
が無いとエラーになりました。
error: attribute 'nodejs_14' missing
at /home/ryu/dev/zenn_contents/node-pkgs/default.nix:5:48:
4| inherit system;
5| }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs_14"}:
| ^
6|
Did you mean one of nodejs_18, nodejs_24, nodejs_20 or nodejs_22?
ログ
実行時のログです。
{
description = "Zenn CLI environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
my-node-pkgs = import ./node-pkgs/default.nix {};
in
{
devShells.default = pkgs.mkShell {
packages = [
my-node-pkgs."textlint-15.2.2"
];
};
}
);
}
$ nix flake check
warning: Git tree '/home/ryu/dev/zenn_contents' has uncommitted changes
error:
… while checking flake output 'devShells'
at «github:numtide/flake-utils/11707dc2f618dd54ca8739b309ec4fc024de578b?narHash=sha256-l0KFg5HjrsfsO/JpG%2Br7fRrqm12kzFHyUHqHCVpMMbI%3D»/lib.nix:43:9:
42| // {
43| ${key} = (attrs.${key} or { }) // {
| ^
44| ${system} = ret.${key};
… while checking the derivation 'devShells.x86_64-linux.default'
at /home/ryu/dev/zenn_contents/flake.nix:24:9:
23| {
24| devShells.default = pkgs.mkShell {
| ^
25| packages = [
(stack trace truncated; use '--show-trace' to show the full, detailed trace)
error: attribute 'nodejs_14' missing
at /home/ryu/dev/zenn_contents/node-pkgs/default.nix:5:48:
4| inherit system;
5| }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs_14"}:
| ^
6|
Did you mean one of nodejs_18, nodejs_24, nodejs_20 or nodejs_22?
原因調査
node2nix で作成した Nix ファイルを使ってビルドしてみると、同じエラーが発生。
$ nix-build -A textlint
error:
… in the condition of the assert statement
at /nix/store/84n5cvdb684m7zzc9hfmd5ii8f5cscvn-source/lib/customisation.nix:410:9:
409| drvPath =
410| assert condition;
| ^
411| drv.drvPath;
… while evaluating a branch condition
at /nix/store/84n5cvdb684m7zzc9hfmd5ii8f5cscvn-source/pkgs/stdenv/generic/check-meta.nix:644:5:
643| in
644| if validity ? handled then
| ^
645| validity
(stack trace truncated; use '--show-trace' to show the full, detailed trace)
error: attribute 'nodejs_14' missing
at /home/ryu/dev/zenn_contents/node-pkgs/default.nix:5:48:
4| inherit system;
5| }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs_14"}:
| ^
6|
Did you mean one of nodejs_18, nodejs_24, nodejs_20 or nodejs_22?
default.nix
の引数定義を確認してみます。
{pkgs ? import <nixpkgs> {
inherit system;
}, system ? builtins.currentSystem, nodejs ? pkgs."nodejs_14"}:
nodejs
が未指定の場合は pkgs."nodejs_14"
が使用されることが分かります。
pkgs."nodejs_14"
は 2023 年頃に pkgs から除外されているため、エラーが起きたようです。
対策
default.nix
の引数 nodejs
を指定する。
my-node-pkgs = import ./node-pkgs/default.nix {
inherit pkgs system;
nodejs = pkgs.nodejs_24;
};
参考資料
この方も同じ対策をされていました。
textlint 使用時 - Error: Failed to load packages
発生事象
方法 B. リストベース(個別パッケージ)
で node-packages.json
を使って textlint
と textlint-rule-preset-ja-spacing
を導入しました。
textlint
を実行すると、エラーが発生しました。
$ textlint --preset preset-ja-spacing README.md
Error
Failed to load packages
Stack trace
Error: Failed to load packages
at loadCliDescriptor (/nix/store/xr3scyk65srk3cppm2dvbv8xfdcfm34p-textlint-15.2.2/lib/node_modules/textlint/lib/src/loader/CliLoader.js:44:15)
at async loadDescriptor (/nix/store/xr3scyk65srk3cppm2dvbv8xfdcfm34p-textlint-15.2.2/lib/node_modules/textlint/lib/src/cli.js:25:27)
at async Object.executeWithOptions (/nix/store/xr3scyk65srk3cppm2dvbv8xfdcfm34p-textlint-15.2.2/lib/node_modules/textlint/lib/src/cli.js:133:28)
対策
方法 A. プロジェクトベース(package.json + lockfile)
を使用する。
nodeDependencies
を指定するだけで解消する。
let
pkgs = nixpkgs.legacyPackages.${system};
my-node-pkgs = import ./node-pkgs/default.nix {
inherit pkgs system;
nodejs = pkgs.nodejs_24;
};
inherit (my-node-pkgs) nodeDependencies;
in
{
devShells.default = pkgs.mkShell {
packages = [
nodeDependencies
];
};
}
VSCode 拡張 textlint 使用時 - Failed to load the textlint library
上記の対策で CLI で textlint
をプリセット含めて動作可能になりました。
しかし、VSCode 拡張 3w36zj6.textlint
は動作しませんでした。
(リアルタイムで編集中のファイルをチェック・警告表示されるはずが、実施されず。)
拡張の動作ログを確認すると、以下のエラーになっていました。
Failed to load the textlint library in /home/ryu/dev/zenn_contents .
To use textlint in this workspace please install textlint using 'npm install textlint' or globally using 'npm install -g textlint'.
You need to reopen the workspace after installing textlint.
原因調査
textlint
拡張のドキュメントを読むと、開いているワークスペースのフォルダから textlint
を探し、無ければグローバルから探す、と書いてあります。
おそらく、前者は node_modules
、後者は ~/.local/bin/
を探していると思われます。
プルジェクト直下に node_modules
を置いてしまうのが楽に対処できそうです。
The extension uses the textlint library installed in the opened workspace folder. If the folder doesn't provide one, the extension looks for a global install version. If you haven't installed textlint either locally or globally, you can do so by running npm install textlint in the workspace folder for a local install or npm install -g textlint for a global install.
対策
方法 A. プロジェクトベース(package.json + lockfile)
を使用する。
そして、node_modules
をプロジェクトディレクトリ直下に用意する。
node2nix 公式ドキュメントの Using the Node.js environment in other Nix derivations
を参考にして、シンボリックリンクを作成します。
let
pkgs = nixpkgs.legacyPackages.${system};
my-node-pkgs = import ./node-pkgs/default.nix {
inherit pkgs system;
nodejs = pkgs.nodejs_24;
};
inherit (my-node-pkgs) nodeDependencies;
in
{
devShells.default = pkgs.mkShell {
packages = [
nodeDependencies
];
shellHook = ''
ln -s ${nodeDependencies}/lib/node_modules ./node_modules
'';
};
}
VSCode 起動時 - ln: failed to create symbolic link
発生事象
上記対策で node_modules
のシンボリックリンクを作成する仕組みを導入しました。
しかし、環境起動時にシンボリック作成が失敗していました。
(筆者の場合、direnv
で nix develop
を自動化しているので、VSCode で該当プロジェクトを開くと nix develop
が実行される。この時に、エラーが起きていた。)
ln: failed to create symbolic link './node_modules/node_modules': Permission denied
対策
ln
コマンドのオプションを変更し -sfn
を指定する。
let
pkgs = nixpkgs.legacyPackages.${system};
my-node-pkgs = import ./node-pkgs/default.nix {
inherit pkgs system;
nodejs = pkgs.nodejs_24;
};
inherit (my-node-pkgs) nodeDependencies;
in
{
devShells.default = pkgs.mkShell {
packages = [
nodeDependencies
];
shellHook = ''
ln -sfn ${nodeDependencies}/lib/node_modules ./node_modules
'';
};
}
参考資料
Discussion