🚀

node2nix で Nixpkgs 非登録の Node ライブラリを管理する

に公開

はじめに

パッケージマネージャ Nix の利点の 1 つは対応しているパッケージの豊富さです

「最近流行っているツールをちょっと動かしてみたい」と思った時、大抵は Nix で使えます。
例えば、OpenAI Codex CLI を試したい時、以下のコマンドだけで実現できます。

Bash
nix-shell -p codex

利用可能なパッケージには Node.js に依存するモノ(e.g. Zenn CLI)も多く含まれます。
しかし、マイナーなパッケージは Nixpkgs 未登録な場合も多いです


node2nix は Node.js パッケージを Nix で扱えるように変換するためのツールです

https://github.com/svanderburg/node2nix

node2nix には大きく分けて 2 つの使い方があり、それぞれ挙動が異なります。

  • package.json からプロジェクト全体を変換する方法
  • パッケージリストからツールを個別に変換する方法

本記事では、各方法の利用方法・使い分けを解説します。

https://github.com/ryuryu333/nodejs_nix_env_template

https://zenn.dev/trifolium/articles/6678b0c0fb0d27

想定読者

  • Nix 経験者
    • flake.nix の devShell で環境構築している方
  • Node.js パッケージを Nix で管理したい方
    • pkgs に未登録のパッケージ含む
  • node2nix の使い方を知りたい方

各方法の特徴

A. プロジェクトベース(package.json + lockfile)

npm install の挙動を Nix で再現する方法です

package.jsonpackage-lock.json を元に、単一の node_modules を持つ derivation を生成します。

ESLint とそのプラグインのように、パッケージ同士が互いを参照する必要がある場合に最適です。

また、NPM の依存関係解決アルゴリズムで lockfile を作成し、lockfile の依存関係を元に Nix ファイルへ変換するため、再現性が高いです。

Bash
node2nix -l package-lock.json
package.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 のような単体で完結するツールを入れる場合に最適です。

設定がシンプルで、他のパッケージへの影響を気にせず追加・更新できます

Bash
node2nix -i node-packages.json
node-packages.json
[
  { "eslint" : "9.34.0" },
  { "typescript-eslint" : "8.41.0" }
]

使い分け・ユースケース

Case 1. 単体で完結するツールを入れたい

e.g. Zenn CLI
-> B. リストベース(個別パッケージ)

Case 2. 互いに無関係な複数ツールを入れたい

e.g. Zenn CLICodex CLI
-> B. リストベース(個別パッケージ)

Case 3. 本体とプラグインのように、相互参照が必要なツール群を入れたい

e.g. ESLinttypescript-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 に導入するまでの具体的な手順を解説します。

どちらの方法も作業の流れは同じです。

    1. node2nix で作成する Nix ファイルを保管する用のフォルダとして node-pkgs を作成
    1. node-pkgs フォルダの中に json ファイルを作成
    1. json ファイルに利用したい Node.js パッケージを記述
    1. node2nix コマンドを実行
    1. default.nixnode-env.nixnode-packages.nix が自動生成
    1. flake.nix を記述
    1. 完了

主な違いは、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 を作成

Bash
cd your-repo
mkdir node-pkgs && cd node-pkgs
touch package.json
package.json
{
  "name": "node-eslint-env",
  "version": "1.0.0",
  "private": "true",
  "devDependencies": {
    "eslint": "9.34.0",
    "typescript-eslint": "8.41.0"
  }
}
バージョンの指定方法

全てを検証したわけではありませんが、npm のドキュメントに記載されている方法が利用可能なはずです。

https://docs.npmjs.com/cli/v11/configuring-npm/package-json#dependencies

package.json
{
  "name": "node-eslint-env",
  "version": "1.0.0",
  "private": "true",
  "devDependencies": {
    "eslint": "9.34.0",
    "typescript-eslint": "*"
  }
}

node2nix で Nix ファイルを作成

Bash
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 を作成
Bash
nix-shell -p nodejs_24 --run 'npm install --package-lock-only --lockfile-version=2'
  • node2nix で package-lock.json の依存関係を元に Nix ファイルを作成
Bash
nix-shell -p node2nix --run 'node2nix -d -l package-lock.json'

flake.nix を作成

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 のシンボリックリンクを作成することで対策できます。

flake.nix
in
{
  devShells.default = pkgs.mkShell {
    packages = [
      nodeDependencies            
    ];
    shellHook = ''
      ln -sfn ${nodeDependencies}/lib/node_modules ./node_modules
    '';
  };
}

仮に、シンボリックリンクを作っても正常に動作しない場合、以下の設定を試すと動くかもしれません。

flake.nix
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

https://github.com/svanderburg/node2nix?tab=readme-ov-file#using-the-nodejs-environment-in-other-nix-derivations

B. リストベース(個別パッケージ)

node-packages.json を作成

Bash
cd your-repo
mkdir node-pkgs && cd node-pkgs
touch node-packages.json
node-packages.json
[
  { "eslint": "9.34.0" },
  { "typescript-eslint": "8.41.0" }
]
バージョンの指定方法

node2nix 公式ドキュメントをご確認ください。

https://github.com/svanderburg/node2nix?tab=readme-ov-file#deploying-a-collection-of-npm-packages-from-the-npm-registry

node-packages.json
[
  "eslint",
  { "typescript-eslint": "1.0.0" },
  { "zenn-cli": "1.0.x" },
  { "node2nix": "git://github.com/svanderburg/node2nix.git" }
]

node2nix で Nix ファイルを作成

Bash
nix-shell -p node2nix --run \
'node2nix -i node-packages.json'

flake.nix を作成

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 の記述方法について

以下の様な記述が可能です。

flake.nix
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 公式ドキュメントをご確認ください。

https://github.com/svanderburg/node2nix?tab=readme-ov-file#deploying-a-collection-of-npm-packages-from-the-npm-registry

トラブルシューティング

ここまでの解説で紹介した手順は、いくつかのエラーを乗り越えた末にたどり着いた方法です。

誰かの参考になるかもしれないので、本項では、node2nix を使う過程で遭遇したエラーと対処法を紹介します。

error: attribute 'nodejs_14' missing

発生事象

flake.nix にて node2nix で作成した Nix ファイルを使用する際、ビルド時に nodejs_14 が無いとエラーになりました。

Bash
       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?
ログ

実行時のログです。

flake.nix
{
  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"
          ];
        };
      }
    );
}
Bash
$ 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 ファイルを使ってビルドしてみると、同じエラーが発生。

Bash
$ 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 の引数定義を確認してみます。

default.nix
{pkgs ? import <nixpkgs> {
    inherit system;
  }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs_14"}:

nodejs が未指定の場合は pkgs."nodejs_14" が使用されることが分かります。

pkgs."nodejs_14" は 2023 年頃に pkgs から除外されているため、エラーが起きたようです。

https://github.com/NixOS/nixpkgs/pull/264358

https://github.com/NixOS/nixpkgs/pull/258427

対策

default.nix の引数 nodejs を指定する。

flake.nix
my-node-pkgs = import ./node-pkgs/default.nix {
  inherit pkgs system;
  nodejs = pkgs.nodejs_24;
};

参考資料

この方も同じ対策をされていました。

https://blog.yasunori0418.dev/p/fix-node2nix-build/

textlint 使用時 - Error: Failed to load packages

発生事象

方法 B. リストベース(個別パッケージ)node-packages.json を使って textlinttextlint-rule-preset-ja-spacing を導入しました。

textlint を実行すると、エラーが発生しました。

Bash
$ 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 を指定するだけで解消する。

flake.nix
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 は動作しませんでした。
(リアルタイムで編集中のファイルをチェック・警告表示されるはずが、実施されず。)

拡張の動作ログを確認すると、以下のエラーになっていました。

VSCode > 出力 > 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.

https://marketplace.visualstudio.com/items?itemName=3w36zj6.textlint

対策

方法 A. プロジェクトベース(package.json + lockfile) を使用する。

そして、node_modules をプロジェクトディレクトリ直下に用意する。

node2nix 公式ドキュメントの Using the Node.js environment in other Nix derivations を参考にして、シンボリックリンクを作成します。

https://github.com/svanderburg/node2nix?tab=readme-ov-file#using-the-nodejs-environment-in-other-nix-derivations

flake.nix
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
    '';
  };
}

発生事象

上記対策で node_modules のシンボリックリンクを作成する仕組みを導入しました。

しかし、環境起動時にシンボリック作成が失敗していました。

(筆者の場合、direnvnix develop を自動化しているので、VSCode で該当プロジェクトを開くと nix develop が実行される。この時に、エラーが起きていた。)

Bash
ln: failed to create symbolic link './node_modules/node_modules': Permission denied

対策

ln コマンドのオプションを変更し -sfn を指定する。

flake.nix
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
    '';
  };
}

参考資料

https://github.com/svanderburg/node2nix

https://zenn.dev/pandaman64/articles/zenn-built-with-nix

https://www.takeokunn.org/posts/fleeting/20250622133346-how_to_use_node2nix/

https://blog.yasunori0418.dev/p/fix-node2nix-build/

Discussion