❄️

dream2nixでElm開発環境を構築する方法

2024/04/09に公開

本記事では、Elmを使ってアプリケーションを開発したいと考えているNix/NixOSユーザーに向けて、開発環境を構築する方法について説明します。紹介する手順では、dream2nixという、さまざまなプログラミング言語のパッケージ管理(npm、pip、cargoなど)を統一的なインターフェースによって扱うためのフレームワークによって、Elmで開発したアプリケーションをビルドできる開発環境を構築します。

また、実行ファイルをビルドするだけではなく、Elmで開発したアプリケーションをデバッグする際には、elm-watchの開発サーバーをつかい、Elm Debuggerを活用できます。
記事の後半では、Google CloudのCloud RunなどのDockerイメージが必要となる状況に対応するため、DockerイメージをNixパッケージとしてビルドする方法も紹介します。

本記事で扱うツール一覧

この記事では、以下のツールを扱うことでElmの開発環境を構築します。

ツール名称 本記事で想定するバージョン 説明
flake-parts - Nix Flakesを再利用可能なモジュールに分割するためのフレームワークです。
dream2nix - さまざまなプログラミング言語のパッケージ管理(pip, npm, cargo など)を統一的なインターフェースによって扱うためのフレームワークです。
elm2nix 0.3.0 Elm パッケージの指定を Nix 式に変換するツールです。
elm-watch 2.0.0-beta.2 elm makeに近い体験を提供しつつ、ホットリロード機能をつかえるツールです。バージョン2のベータ版では、試験的な開発サーバーが付属しています。
esbuild 0.20.2 JavaScript/TypeScript のバンドラーです。

DevshellでElmプロジェクト作成

まずは、Elmの開発環境に必要なNixパッケージをDevshellとして設定しましょう。

  1. シェルでプロジェクトとなるベースディレクトリ example-elm を作成します。
  2. ベースディレクトリ example-elm をカレントディレクトリに設定します。
  3. git init を実行してGitリポジトリを初期化します。
  4. NodeJSとElmに関するgitignoreファイルを取得します。
nix run nixpkgs#curl -- -sL https://www.toptal.com/developers/gitignore/api/node,elm >> .gitignore
  1. Nix Flakesファイルとして flake.nix を作成します。
{
  description = "Elm Example";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    flake-parts.url = "github:hercules-ci/flake-parts";
  };

  outputs = inputs @ {self, ...}:
    inputs.flake-parts.lib.mkFlake {inherit inputs;} {
      systems = ["x86_64-linux"];
      perSystem = {
        config,
        self',
        pkgs,
        system,
        ...
      }: {
        devShells.default = with pkgs;
          mkShell {
            nativeBuildInputs =
              [
                nodejs_21
                nodePackages.npm
              ]
              ++ (with elmPackages; [
                elm
              ]);
          };
      };
    };
}
  1. git add . を実行します。
  2. nix develop を実行して、Devshellに入ります。
  3. elm init を実行して、Elmプロジェクトを作成します。
  4. Elmのソースコードとしてファイルを src/Main.elm に配置して、その内容を以下のように記述します。
module Main exposing (..)

import Browser
import Html exposing (div, h1, text)

main : Program () () ()
main =
    Browser.element
        { init = always ( (), Cmd.none )
        , update = always (\() -> ( (), Cmd.none ))
        , view = always (h1 [] [ text "Elm Example" ])
        , subscriptions = always Sub.none
        }

これでElmプロジェクトを作成できました。

elm-watchで開発サーバーを設定

Elmで構築したアプリケーションをデバッグしやすいように、elm-watchで開発サーバーを設定しましょう。

  1. NodeJSのパッケージファイルとして package.jsonを作成し、ファイルの内容を以下のように記述します。
{
  "name": "example-elm",
  "version": "0.1.0",
  "scripts": {
    "start": "npx elm-watch hot"
  }
}
  1. シェルで npm install elm-watch@2.0.0-beta.2 を実行してelm-watchをインストールします。
  2. HTMLファイルとして public/index.html を配置し、ファイルの内容を以下のように記述します。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="build/main.js" defer></script>
  <title>Elm Example</title>
</head>

<body>
  <div id="app"></div>
  <script type="module">
    const app = Elm.Main.init({
      node: document.getElementById('app')
    });
  </script>
</body>
</html>

  1. elm-watchの設定ファイルとして、JSONファイル elm-watch.json を作成し、ファイルの内容を以下のように記述します。
{
    "targets": {
        "Example Elm": {
            "inputs": [
                "src/Main.elm"
            ],
            "output": "public/build/main.js"
        }
    },
    "serve": "public/"
}

  1. 続いて、シェルに下記のコマンドを実行すると、elm-watchの開発サーバーが起動します。開発サーバーのポートは自動的に割り当てられます。標準出力に含まれるURLへアクセスして、動作を確認してください。
npm run start

elm2nixでElmのパッケージ指定をNix式に変換

実行ファイルをビルドするためには、Elmのパッケージ指定をNix式で表現しなければいけません。elm2nixというツールを使ってelm.jsonファイルからNix式へ変換しましょう。

  1. シェルで下記のコマンドを実行します。
nix run nixpkgs#elm2nix convert > elm-srcs.nix
nix run nixpkgs#elm2nix snapshot

これによって、 elm-srcs.nix ファイルにelm.jsonのパッケージ指定がNix式として記述されます。開発中の際に、elmのパッケージを新しくインストールやアンインストールした場合は、再度この手順を実施してください。

dream2nixでNodeJSプロジェクトを追加

dream2nixを使ってNodeJSプロジェクトを追加します。

  1. ファイル flake.nixinputs 項目にdream2nixを追加します。
    dream2nix = {
      url = "github:nix-community/dream2nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  1. ファイル package.jsonを編集し、以下のスクリプトを追加します。start コマンドあとに追加する場合は、行末に , をつけてください。
    "build": "elm make src/Main.elm --optimize --output=public/build/main.js && npm run esbuild -- --minify",
    "esbuild": "npx esbuild app.ts --bundle --outdir=public/build --public-path=/build/"
  1. シェルで npm install esbuild を実行して、esbuildをインストールします。
  2. esbuildのエントリポイントファイルとして app.ts を作成して、ファイルの内容を以下のように記述します。
function run(): void {
  window.Elm?.ApplicationMain?.init();
}

run();

export {};
  1. ファイル flake.nixperSystem 項目に下記を追加します。
        packages.default = inputs.dream2nix.lib.evalModules {
          packageSets.nixpkgs = pkgs;
          modules = [
            ./default.nix
            {
              paths.projectRoot = ./.;
              paths.package = ./.;
            }
          ];
        };
  1. Nixパッケージのビルドのため Nixファイル default.nix を作成し、ファイルの内容を以下のように記述します。
{
  lib,
  config,
  dream2nix,
  ...
}: rec {
  imports = [
    dream2nix.modules.dream2nix.nodejs-package-lock-v3
    dream2nix.modules.dream2nix.nodejs-granular-v3
  ];

  name = "example-elm";
  version = "0.1.0";

  deps = {nixpkgs, ...}: {
    inherit
      (nixpkgs)
      stdenv
      nodejs_21
      esbuild
      caddy
      ;
    inherit (nixpkgs.nodePackages) npm;
    inherit
      (nixpkgs.elmPackages)
      elm
      fetchElmDeps
      ;
  };

  mkDerivation = {
    src = ./.;
    buildInputs = with config.deps; [
      elm
      caddy
    ];
    postConfigure = config.deps.fetchElmDeps {
      elmPackages = import ./elm-srcs.nix;
      elmVersion = "0.19.1";
      registryDat = ./registry.dat;
    };
    installPhase = ''
      cp -R $out/lib/node_modules/${name}/public $out/public
      rm -rf $out/lib
      mkdir $out/bin

      makeWrapper $(type -p caddy) $out/bin/${name} \
        --add-flags "file-server --listen :\"\''${PORT:-8000}\" --root $out/public"
    '';
  };

  nodejs-granular-v3 = {
    installMethod = "symlink";
  };

  nodejs-package-lock-v3 = {
    packageLockFile = "${config.mkDerivation.src}/package-lock.json";
  };
}

default.nixの内容について説明します。

  • imports 項目では、NodeJSのパッケージを扱うために、dream2nixのnodejs-package-lock-v3nodejs-granular-v3モジュールをインポートしています。
  • deps 項目では、このプロジェクトで使用する依存パッケージを記述しています。
  • mkDerivation 項目では、プロジェクトのビルド手順を記述しています。buildInputs 項目で必要なパッケージを指定し、postConfigure 項目でElmの依存関係を取得しています。installPhase 項目では、esbuildによってバンドルされた成果物をcaddyで配信できるようにパッケージ化しています。
  • nodejs-granular-v3 項目とnodejs-package-lock-v3 項目では、それぞれNix Flakesの出力へのインストール方法とパッケージロックファイルの場所を指定しています。

これで、dream2nixを使ってNodeJSプロジェクトが追加できました。シェルで git add . したのち、 nix run を実行して、ウェブブラウザー上で http://localhost:8000 にアクセスして動作を確認してください。

DockerイメージのNixパッケージを追加

Google CloudのCloud Runにデプロイする必要がある場合は、Dockerイメージが必要となるので、実行ファイルをDockerイメージで扱えるように、新しいNixパッケージ ociを追加しましょう。ファイルflake.nixperSystem 項目に以下の内容を記述します。

        packages.oci = with pkgs;
          dockerTools.buildLayeredImage rec {
            name = "example-elm";
            tag = "latest";
            config = {
              Env = [
                # https://gist.github.com/CMCDragonkai/1ae4f4b5edeb021ca7bb1d271caca999
                "SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt"
              ];
              Cmd = [
                "${self.packages.${system}.default}/bin/${name}"
              ];
            };
          };

シェルで git add . したのち、nix build .#oci を実行すると、Dockerイメージをビルドできます。ローカルもしくはサーバーのシェルで docker load -i result && docker run -p 8000:8000 example-elm を実行して、ウェブブラウザー上で http://localhost:8000 にアクセスして動作を確認してください。

まとめ

Elmの開発環境を構築する手順は以上となります。本記事では扱いませんでしたが、NixOS Testing libraryという仕組みで結合テストを扱ったり、バックエンドをモノレポとして含めることで、フロントエンドとバックエンド間でのNixパッケージに関する依存関係もNixで扱うことができます。

また、サンプルプロジェクトをGitHubで公開しています。このリポジトリには、terranixによるInfrastructure as Codeや結合テストといった応用の例も含んでおりますので、よろしければご活用ください。

https://github.com/cachet-jp/example-elm

本記事の内容やサンプルプロジェクトは、さまざまなNixの活用例のほんのひとつですが、少しでも、みなさまの快適な開発環境を構築するために役立てば幸いです。お目通しくださり、ありがとうございました。

株式会社カシェイ

Discussion