v2.2.0
Build system

Build system

Melange は、OCaml のビルドシステムとして最も広く使われているDune (opens in a new tab)と深く統合されています。この統合により、開発者は OCaml ネイティブ実行ファイルと Melange でビルドされたフロントエンド・アプリケーションの両方を含む単一のプロジェクトを作成し、両方のプラットフォーム間でコードを簡単に共有することができます。

Dune は、プロジェクトのコンパイルに必要な作業を計画し、必要に応じてファイルをコピーし、Melange が OCaml ソースファイルを受け取って JavaScript コードに変換できるようにすべてを準備します。

それでは、Melange のコンパイルモデルに飛び込み、Melange プロジェクトで Dune を使用する方法を簡単に説明します。

コンパイルモデル

Melange は 1 つのソースファイルを 1 つの JavaScript モジュールにコンパイルします。このコンパイルモデルは、生成された JavaScript コードのデバッグを簡素化し、JavaScript プロジェクトで行うのと同じ方法で CSS ファイルやフォントなどのアセットをインポートすることを可能にします。また、Webpack (opens in a new tab)などの JavaScript モジュールバンドルラーや、その他の代替ツール (opens in a new tab)との統合も容易になります。

Webpack との統合の例として、Melange opam template (opens in a new tab)を参照できます。このリンク (opens in a new tab)からテンプレートに基づいてリポジトリを作成できます。

Melange はどのように Dune と統合されているか

Dune は、Melange プロジェクトがライブラリやアプリケーションを指定するために使用する OCaml ビルドシステムです。monorepos に最適化されており、プロジェクトのメンテナンスが容易になります。このセクションでは、Dune の機能の概要と Melange アプリケーションのビルド方法について説明します。

Features

Dune は OCaml を念頭に設計されており、Melange 開発者にとって理想的なツールとなっています。以下のような利点があります:

新規プロジェクトを作成する

Dune の使い方を理解するために、小さな Melange アプリケーションを作ってみましょう。

まず、パッケージ管理のセクションで示したように、opam スイッチを作成します:

opam switch create . 5.1.1 --deps-only

Dune と Melange の最新バージョンを switch にインストールします:

opam update
opam install dune melange

Reason 構文を使う場合は、reasonパッケージもインストールします:

opam install reason

dune-projectという名前のファイルを作成します。このファイルはプロジェクトの設定について Dune にいくつかのことを伝えます:

(lang dune 3.8)
 
(using melange 0.1)

最初の行(lang dune 3.8)は Dune に「Dune 言語」(dune ファイルで使用される言語)のバージョンを伝えます。Dune の Melange サポートはバージョン 3.8 からです。

2 行目 (using melange 0.1) は Dune に、Dune 言語の Melange 拡張 (opens in a new tab)を使いたいことを伝えます。

ライブラリを追加する

次に libフォルダーを作成し、その中に dune ファイルを作成します。duneファイルの中に以下の内容を記述します:

(library
 (name lib)
 (modes melange))

同ディレクトリにlib.ml(Reason の場合はlib.re)ファイルを作成します:

let name = "Jane"

duneファイルに現れるlibraryのようなトップレベルのエントリーは stanzaと呼ばれ、namemodesのような内部のものは stanza のfieldsと呼ばれます。

すべての stanza は、Dune のドキュメントサイトでよくカバーされており、library stanza (opens in a new tab)にリファレンスがあります。

Dune は、プロジェクトのフォルダー構造を変更する際の設定変更の必要性を最小限に抑えるように設計されています。例えば、lib フォルダーをプロジェクト内の別の場所に移動しても、dune ファイルを更新することなく、すべてのビルドコマンドが動作し続けます。この機能は非常に便利です。

melange.emit によるエントリーポイント

ライブラリーは、アプリケーションの動作や論理コンポーネントをカプセル化するのに便利ですが、それだけでは JavaScript の成果物を生成しません。

JavaScript コードを生成するには、アプリケーションのエントリーポイントを定義する必要があります。ルートフォルダーに別のduneファイルを作成します:

(melange.emit
 (target app)
 (libraries lib))

そしてapp.ml(Reason の場合はapp.re)ファイルを作ります:

let () = Js.log Lib.name

melange.emit スタンザは Dune にライブラリとモジュールのセットから JavaScript ファイルを生成するよう指示します。このスタンザに関する詳しいドキュメントは Dune docs (opens in a new tab) を参照してください。

アプリのファイル構造は以下のようになります:

├── _opam
├── lib
 ├── dune
 └── lib.ml
├── dune-project
├── dune
└── app.ml

ビルド

Melange コンパイラーを使用して、ソースから JavaScript コードを生成します:

dune build @melange

このコマンドは Dune に melange というエイリアスが付加されているターゲットをすべてビルドするように指示します。 エイリアス (opens in a new tab)は、ファイルを生成せず、設定可能な依存関係を持つビルドターゲットです。

デフォルトでは、melange.emit stanza 内のすべてのターゲットと依存するライブラリがmelangeエイリアスにアタッチされます。後述するように、明示的にエイリアスを定義することもできます。

すべてがうまくいけば、出来上がった JavaScript を Node.js で実行できるはずです。 Dune の機能を説明したときに述べたように、Dune はすべての成果物を_buildフォルダーの中に置き、ソース・フォルダーを汚さないようにしています。そのため、そのフォルダーに置かれたスクリプトを Node に指し示し、期待される出力を確認します:

$ node _build/default/app/app.js
Jane

JavaScript アーティファクトのレイアウト

上のコマンドでは、appフォルダーの中にあるapp.jsファイルを探さなければなりませんでしたが、ソースにはそのようなフォルダーはありません。このフォルダはmelange.emitstanza のtargetフィールドで宣言されたもので、Dune は生成された JavaScript の成果物をどこに置くかを知るためにこれを使います。

より複雑な例として、以下のセットアップを考えてみましょう:

├── dune-project
├── lib
 ├── dune
 └── foo.ml
└── emit
└── dune

emit/duneは以下のようになります:

(melange.emit
 (target app)
 (libraries lib))

lib/dune

(library
 (name lib)
 (modes melange))

そして、foo.mlfoo.re)の JavaScript の成果物は、以下に置かれます:

_build/default/emit/app/lib/foo.js

もっと一般的に言えば:

  • ワークスペースの相対パス $melange-emit-folder にある dune ファイルで定義された melange.emit stanza
  • (target $target) のように、target フィールド $target を含みます
  • 相対ワークスペース・パス $path-to-source-file に置かれた $name.ml$name.ml$name.ml$name.re) というソース・ファイル

$name.ml$name.re)から生成される JavaScript ファイルへのパスは以下のようになります:

_build/default/$melange-emit-folder/$target/$path-to-source-file/$name.js

melange.emit のガイドライン

melange.emit に関する以下の推奨事項は、大規模な産業プロジェクトでテストされ、複雑さ、メンテナンス、構築パフォーマンスに対処するための有用なガイドラインであることが証明されています。

  • Webpack などのツールから生成された JavaScript ファイルにアクセスしやすくするため、melange.emit stanza を含む dune ファイルをプロジェクトのルートフォルダに置くことを推奨します。これにより、生成された JavaScript ファイルが _build/default/$target パスの下に直接置かれるようになります
  • バンドルサイズが不用意に大きくなるリスクを最小にするために、melange.emit stanza の数を最小にすることをお勧めします。複数の melange.emit stanza を持つと、同じライブラリから生成された JavaScript コードが複数コピーされる可能性があります。melange.emit stanza をまとめることで、この問題を軽減し、より効率的なバンドルサイズを確保することができます

エイリアスを使う

デフォルトの melange エイリアスはプロトタイピングや小規模なプロジェクトでは便利ですが、 大規模なプロジェクトでは複数のエントリーポイントやmelange.emit stanza を定義することがあります。 このような場合、個々の stanza をビルドする方法があると便利です。 そのためには、alias フィールドを使うことで、それぞれに明示的なエイリアスを定義することができます。

melange.emit stanza にカスタムエイリアス my-app を定義してみましょう:

(melange.emit
 (target app)
 (alias my-app)
 (libraries lib))

これで、この新しいエイリアスを参照できるようになりました:

$ dune build @my-app

デフォルトの melange エイリアスを使用して再度ビルドしようとすると、Dune はエラーを返します。

$ dune build @melange
Error: Alias "melange" specified on the command line is empty.
It is not defined in . or any of its descendants.

アセットを扱う

Melange プロジェクトでは、CSS ファイルやフォントなどのアセットを使用することがあります。 Dune の仕組み上、アセットを _build フォルダにコピーしてインストールする必要があります。 この作業をできるだけ簡単にするため、Dune では stanza によって依存関係を指定する方法を用意しています:

  • library stanza の場合、 melange.runtime_deps フィールド
  • melange.emit stanza の場合、runtime_deps フィールド

どちらのフィールドも Dune ドキュメントサイトの Melange ページ (opens in a new tab)に記載されています。

Melange プロジェクトでアセットを扱う方法を学ぶために、テキストファイルから Lib.name の文字列を読み込むとします。 melange.runtime_deps フィールドと Melange が提供する Node へのバインディングを組み合わせます。 バインディングがどのように機能するかについては、次のセクション [JavaScript とのコミュニケーション]を参照してください。

そこで、lib フォルダの中に、Jane という名前だけを含む新しいファイル name.txt を追加してみましょう。

次に lib/dune ファイルを適合させます。 環境変数 __dirname の値を取得するために、melange.runtime_deps フィールドと bs.raw 拡張子(これらの拡張子については「JavaScript とのコミュニケーション」のセクションで詳しく説明します)を 使用できるようにする processing (opens in a new tab) フィールドを追加する必要があります:

(library
 (name lib)
 (modes melange)
 (melange.runtime_deps name.txt)
 (preprocess (pps melange.ppx)))

最後に lib/lib.mllib/lib.re) を更新し、最近追加されたファイルから読み込むようにします:

let dir = [%mel.raw "__dirname"]
let file = "name.txt"
let name = Node.Fs.readFileSync (dir ^ "/" ^ file, `ascii);

これらの変更後、プロジェクトをビルドすると、Node でアプリケーション・ファイルを実行できるようになります:

$ dune build @my-app
$ node _build/default/app/app.js
Jane

同じ方法で、フォント、CSS、SVG ファイル、その他プロジェクト内のあらゆるアセットをコピーすることができます。

Dune は依存関係を非常に柔軟に指定できます。もうひとつの興味深い機能は globs で、複数のファイルに依存する場合の設定を簡略化できます。例えば:

(melange.runtime_deps
 (glob_files styles/*.css)
 (glob_files images/*.png)
 (glob_files static/*.{pdf,txt}))

詳しくは依存関係の仕様のドキュメント (opens in a new tab)を参照してください。

ランタイムの依存性については、Melange 開発者向けの Dune ガイドを終了しました。 Dune の動作や Melange との統合についての詳細は、Dune ドキュメント (opens in a new tab)Melange opam template (opens in a new tab)を参照してください。

CommonJS または ES6 modules

Melange は、宣言した関数をエクスポートし、依存する値やモジュールのインポートを宣言する JavaScript モジュールを作成します。

デフォルトでは、Melange は CommonJS (opens in a new tab) モジュールを生成しますが、ES6 (opens in a new tab) モジュールを生成するように設定することも可能です。

ES6 モジュールを生成するには、melange.emit stanza (opens in a new tab)module_systems フィールドを使用します:

(melange.emit
 (target app)
 (alias my-app)
 (libraries lib)
 (module_systems es6))

拡張子を指定しない場合、結果の JavaScript ファイルは .js を使用します。 異なる拡張子を指定するには、(<module_systems> <extension>)のペアを使います(例:(module_systems (es6 mjs)))。 拡張子が異なる限り、同じフィールドで複数のモジュールシステムを使うことができます。 例えば、(module_systems commonjs (es6 mjs)) は、CommonJS と拡張子 .js を使った JavaScript ファイルと、ES6 と拡張子 .mjs を使った JavaScript ファイルのセットを生成します。