拙作のWebGLフレームワーク**Grimoire.js**は、Typescriptで組まれていて、一方でユーザーはJavascript
からも扱うことができる。
これが案外難しいところがいくつもあってこれを解決した。実現のために色々とビルド環境を何回も作り変えた結果最終的にいい形に5回目の作り直しで落ち着いて纏まったのでこれについて書いていく。
また、誤解を招かぬよう先に注釈しておくが、Grimoire.jsのプラグイン作者は以下のことをほとんどきにする必要はない。grimoirejs-cauldron
というスキャフォールダーが存在する。 しかし、具体的に中身のビルド周りやプラグイン連携の仕組みを俯瞰的に見るための文章であることに留意していただきたい。あるいは、同じような仕組みを持つライブラリを作りたい人の方が対象読者かもしれない。
問題点その1(ファイルを分割してrequireしたい)
このフレームワークは、複数のファイルから成り立っていて、それぞれのファイルはユーザーから参照され利用されうる可能性がある。また、browserifyなどのバンドリング環境が常に利用可能かもわからない。
npmからアクセスされる場合
例えば、以下のファイルA.ts
,B.ts
があったとしよう。
export default class A{}
export default class B{}
npmから扱う場合は、require
あるいはimport
をする場合は通常、package.json
のmain
属性に、var lib = require("ライブラリ名")
あるいはimport lib from "ライブラリ名"
とすれば、mainに指定したファイルをユーザーは参照することができる。
しかし、あるライブラリが複数個のファイルを別々のファイルパスでrequire
するときはどうすればいいのだろうか。
実は、この解決法はある。例えば、src/**/*.ts
をそれぞれlib/**/*.js
にコンパイル結果として生成するなら、これらは以下のようにrequireすることができる。(package.json
のpublish内容の設定にlib
フォルダが含まれるよう設定する必要がある)
こうすれば、ユーザーはvar A = require("ライブラリ名/lib/A")
あるいは、import A from "ライブラリ名/lib/A"
とすればそれぞれのファイルを別々にrequireして参照することができる。
一般的にnpmに公開されている場合、単一のファイルをrequireして使う場合が多いが、このようにすれば、複数個のファイルを最終生成物にする場合でも用いれるし、ユーザーが自前でバンドリングするとしても、自分で使ったファイルだけをバンドリング対象に含めることができる。
scriptタグからアクセスする場合
僕のライブラリは、スクリプトタグから読み込まれた場合、window.gr
にメインとなるオブジェクトのデータを代入する。スクリプトタグからの参照の場合でも、require
のようなことをしたい。
となれば、何かしらのオブジェクトに代入する以外の手段はない。そこで、以下のように記述したら、src/A.ts
のデフォルトエクスポートを取れるようにした。
var A = gr.src.A;
やり方は単純だ。webpack
のShellCommandPlugin
を用いて、npmのコマンドを呼べるので、npm scriptに記述したスクリプトを呼びうる。
実際には以下のように記述してnpmのスクリプトを呼び出している。
const shell = require("webpack-shell-plugin");
// ...returnでオブジェクトを作る部分
plugins: [new shell({
onBuildStart: "npm run generate-expose"
})]
この、npm scriptで、srcの中身を読み取って**こんなtsファイル**を作成すれば、目的を達成できる。
気をつけなくてはならないのは、tsのdefault exportのpublic要素の中に、exportされていない型の参照が使われていると、エラーになる。そこで、多くの場合インターフェースも出力する必要がある。
問題点その2(バンドリングしても、中身を含めないようにしたい場合がある)
僕のライブラリは、プラグインによる拡張にかなり重きが置かれている。
ある人が簡単に作ったプラグインは、簡単にnpm
にでき、かつscript
タグで簡単に利用できなければならない。
この際問題になるのが、ライブラリ同士の依存関係だ。grimoirejs-A
,grimoirejs-B
というパッケージがあったとして、BがAを依存関係として参照してたとしよう。
この場合、もしgrimoirejs-B
をバンドリングしたものをスクリプトタグで読み込もうものなら、通常ならgrimoirejs-A
も読み込まれる。なぜなら、grimoirejs-B
のバンドリング結果にはgrimoirejs-A
も含まれるべきだからだ。
これだけならば問題は無い。しかし、ここでgrimoirejs-C
がgrimoirejs-A
に依存していたとしよう。
もしもユーザーがnpm
を用いてgrimoirejs-B
とgrimoirejs-C
をrequireした場合、BとCの依存関係のバージョンがsemvar的に共存しても問題ないなら、通常通りgrimoirejs-Aを含めることができる。
しかし、scriptタグでは意味がない。grimoirejs-B
から読み込んでいるgrimoirejs-A
とgrimoirejs-C
から読み込んでいるgrimoirejs-C
から読み込んだgrimoirejs-A
は全くの別物だ。
Aの中でシングルトンであることを保証したかったとすれば、その実装はすぐに崩壊するだろう。
refフォルダの生成
そこで、実際のコードはは参照しないが、ブラウザ上からwindow.GrimoireJS
利用して参照だけ取るようなコードを書いてみればいいことに気がつく。
refフォルダの中に、以下のようなA.jsとB.jsを生成してやる。
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default=window.gr.A;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default=window.gr.B;
こうしてやれば、実際に別のスクリプトタグで読み込まれた対象しか参照しないのでスクリプトタグでうまく動作するし、バンドリングしたとしても、依存ライブラリが存在すれば動作をする。
refフォルダのd.ts
もし、Typescriptを利用するなら、refフォルダの対象を参照した時の内容が厄介であると気がつくだろう。
しかし、tsconfigでdeclarationの設定を変えて、このrefフォルダに生成されるようにすれば、Typescriptからは実態にアクセスしているように見えるため、型的には問題がなくなる。
この仕組みが結構素晴らしく、ユーザーがTypescriptを使う場合でも問題なく同じ仕組みが適用できることがわかる。
register/index.jsの作成
一方で、バンドリングして用いるなら、参照ではなく実際に実体を含めたい場合があるだろう。
そういう際に、require("ライブラリ名/register")
とした時点で、バンドリングした内容に含まれるようにすればいい。
やり方は簡単で、register/index.js
として、**あのtsファイル**からコンパイルされ、バンドリングされた結果を出力すればいいのだ。
最後に
webpackなどに慣れていない人からしたらものすごく複雑だし、こんなユースケースはあまりないのかもしれないが、ハリボテのように見えて案外うまく動作するウマいシステムではある。
今回は抽象化した説明しかしていないが、実際にGrimoire.jsでは、今回の説明を少し拡張した**ある仕様**を満たせばプラグインとして読み込める。もちろん、npmもスクリプトタグも問題なく相互に連携して運用が可能だ。
もし、興味があったら是非実際のビルド環境も**レポジトリ**から見ていただきたい。