JSXそのものにはPyPIやCPANのようなモジュールリポジトリはありません。ですが、npmを使って管理することができます。JSXではJavaScriptのコードのラッパーを定義したりもするので、npmで対象となるJSモジュールも一緒に管理できるようになるので、npmを使うのが一番理にかなっています。このエントリーでは、npmを使ってモジュールを管理する方法について説明します。JSXを対象に説明しますが、npmを使いたいaltJS全般、あるいはスのnode.jsでも役に立つかもしれないお話です。
やり方は簡単です。package.jsonを使って必要なモジュールを書いていくだけです。
{
"name": "your project name",
"version": "0.1.0",
"dependencies": {
"getopt.jsx": "~0.1.0"
}
}
すべてを定義したら、 package.json
が置いてあるフォルダで
$ npm install
とタイプすると必要なモジュールが現在のフォルダの node_modules
以下にインストールされます。
nameとversionだけ書いた package.json
、以下のコマンドを使えば package.json
への行の追加とインストールが同時に行えます。
$ npm install getopt.jsx --save
JSXの処理系を実行するときは、各モジュールのjsxコードが入っているフォルダを検索パスに追加します。
jsx --release --add-search-path node_modules/getopt.jsx/src
grunt-jsxを使っているなら、パスの展開ができるので node_modules/*/src
で一括指定できます。こちらが全体像のサンプルです。AMD形式のモジュール、CommonJS形式、Closure形式、グローバル変数にクラスを定義する方式と何種類ものjsを生成するためのgrunt定義ファイルなのでちょっと行が多いのですが、ミニマムな部分を抜き出すとこのようになっています。
{
jsx: {
build: {
src: ['src/*.jsx'],
add_search_path: ['lib', 'node_modules/*/src'],
dest: 'bin/',
executable: 'node'
}
}
}
作っているのがアプリであればこれで問題ありません。
npmの問題点 - 重複インストール
npmは必要なモジュールはnode_modules以下に集めてインストールします。そのモジュールがさらに依存しているモジュールがあれば、そのフォルダの中にまたnode_modulesフォルダができて、その中にインストールされます。Pythonの場合はsite-packagesというフォルダにフラットにすべてのモジュールが格納されます。npmは、複数のモジュールが同じモジュールを利用していたとすると、このドキュメントのアルゴリズムの説明にある方法でなるべく共有されるようにインストールしますが、状況によっては同じモジュール名のフォルダが何箇所も登場してしまいます。
何箇所も登場するということは、node.jsやJSXで、何度もrequireで読み込まれてしまうということです。もちろん、それぞれメモリを消費しますし、JITに必要な実行情報の統計も正しく取れないでしょう(V8はそれでも早いと思いますが)。a.jsとa''.jsのそれぞれのファイルでクラスAが定義されていたとします。a.jsで作ったAのオブジェクトはa''.jsのAのインスタンスか?という判定式は失敗しますし、それぞれのバージョンが微妙に違う場合はさらに分かりにくい問題が発生する可能性が増えます。
あと、JSXはnode.jsと違い、インポート先のnode_modulesを自動参照はしてくれないため、依存の依存を正しく処理することができません(今のところは。pull-requestは出しています)。
状況次第、というのは具体的には次の2通りで発生することが確認できています。
後からnpm installした場合
A→B、B→Cと依存していたとします。Aのpackage.jsonにBが書かれていたら、A→B→Cと処理されてインストールされます。ですが、あとからAもCに依存するようになって、Aでpackage.jsonにCを追加してからnpm installしたり、npm install --saveするとAの下にもBの下にもCができてしまいます。
これは下記のコマンドをタイプすることで重複を削除することはできます。
npm dedupe
packageのインストール元にgitリポジトリを指定した場合
npmはgitリポジトリを指定することができますが、この場合は問答無用で重複インストールされますし、dedupeも効きません。この場合はどうしようもないので、今のところgitでパッケージを分けて管理するのは推奨しません。公開できないケースで、どうしてもprivateなモジュールを作りたいというのであれば、privateなnpmリポジトリを立てるしかなさげでした。それか、npmのパッケージとしてリポジトリに入れるのではなくて、通常のgit submodule運用をするか・・・ですね。
package.jsonを作成する場合はこの点に注意する必要があります。次に具体的な対処法について説明します。
dependenciesとdevDependenciesとpeerDependenciesを使い分ける
dependenciesは「実行時に必ず必要になる」ものを、devDependenciesは「実行時にも開発時にも必要になるもの」を書きます。peerDependenciesは「親のパッケージで、このバージョンが指定されてないとエラーにする」という用途で使います。開発しているものがアプリか、ライブラリかで方針が変わってきますので、それぞれ分けて説明します。
アプリケーション開発
アプリの例だと、検索エンジンのOktaviaなどがこれにあたります。基本的に、JSXのモジュールはビルド時にのみ必要なのでdevDependenciesに書いていきます。
このサンプルにはありませんが、JSXがラッパーライブラリになっていて、JSのモジュールが実行時に必要、というものがあれば、それだけをdependenciesに書きます。
ライブラリ開発
ライブラリの場合は、親との重複インストールを避けるため、dependenciesには何も書かないようにします。ただし、何も書かないとライブラリ単体でユニットテストを実行することもできないので、必要なモジュールはすべてdevDependenciesに書きます。このライブラリを使ったアプリケーションをビルドする時には、このモジュールで必要なパッケージもインストールされてないと困るので、開発ツール(gruntなど)を除いたパッケージ群は、peerDependenciesにも書きます。親のpackage.jsonに必要なモジュールが定義されていなければ、親のディレクトリでnpm installを実行したときに厳しくエラーにすることができます。
Oktaviaの内部で使っているFM-indexというアルゴリズムのpackage.jsonが参考になるでしょう。
node.jsの探索パスについて
最後に、node.jsの探索パスについておさらいです。
0.6だか古いnode.jsの場合はrequire.pathsだかが配列になっていて、そこにパスを追加することで読み込み先のディレクトリを追加することができましたが、現行バージョンではできないようになっています。
今の実装は、/home/shibu/myapp/app.jsというソースから"lib.js"をrequireをした場合には(フォルダ名の先頭がピリオドでもスラッシュでもない場合)、以下順序でフォルダを探索します。
- /home/shibu/myapp/node_modules/lib.js
- /home/shibu/node_modules/lib.js
- /home/node_modules/lib.js
- /node_modules/lib.js
ルートまで行って見つからなければエラーです。また、jsファイルではなくて、ディレクトリ名が指定されて、指定されたディレクトリが見つかった場合はそのフォルダのpackage.jsonを調べます。もし、mainという項目でファイル名が指定されていれば、そのファイルを読みに行きます。あるいは、index.jsというファイルがあればそれを読みます。ルートフォルダまでたどるなかで、どちらも見つからなければエラーになります。
npmはこのように探索するnode.jsのためのツールです。インストールするときも「上位フォルダにマッチするバージョンがなければインストールする」というシンプルなルールで処理します。npmも重複はなるべくしないようにちょびっとだけ努力はしてくれますが、そこまではがんばらない、というのを理解しておくことが大切です。