babylon.js のサードパーティ npm パッケージを作成する方法

[babylon-vrm-loader](https://www.npmjs.com/package/babylon-vrm-loader) を作る際に参考にしたこと、注意したことをまとめておきます。

そもそも babylon.js のサードパーティパッケージ自体がほぼなく、参考資料なしで作る必要がありましたので、この手法を参考にして babylon.js の拡張パッケージを作ってくれる方がいれば嬉しい限りです。

webpack の設定は three.js など他のライブラリでも参考になるかもしれません。

## ゴール

- `npm install @babylonjs/core babylon-vrm-loader` で npm 経由で読み込めること
- npm, webpack を利用せずブラウザだけでも読み込めること

二種類の出力を用意する必要がありますね。

## 前提

- TypeScript で作ります
- webpack でビルドします

これは元の babylon.js を踏襲するためなので、拡張パッケージは別の方法で作成しても構いません。あくまで一例です。

## デバッグ用のページを作る

何はともあれブラウザで表示が確認出来ないと拡張のしようがありません。最初にテスト用ページを作成することにしました。

```shell
# 依存関係を用意する
$ yarn init -y
$ yarn add --dev @babylonjs/core@^4.0.0 @babylonjs/loaders@^4.0.0 @babylonjs/inspector@^4.0.0 ts-loader typescript webpack webpack-cli webpack-dev-server webpack-merge
```

#### `dependencies` と `peerDependencies`

`@babylonjs/core` パッケージが babylon.js のものですが、ここでは `dependencies` ではなく `devDependencies` に記載しています。
今回作る新しいパッケージは `@babylonjs/core` を依存関係として要求しますが、 `dependencies` に記載してしまうと `npm install @babylonjs/core babylon-vrm-loader` した時に、 下記のような構成になってしまいます。

```
- new-package/
    - node_modules/
        - @babylonjs/
            - core/
                - ...
        - babylon-vrm-loader/
            - node_modules/
                - @babylonjs/
                    - core/
                        - ...
```

この状態になると、 `new-package` のビルド時に TypeScript で型エラーが発生する場合があります。
`babylon-vrm-loader` は **自身を必要とするパッケージ(ここでは `new-package`)と同じ(peer)依存関係が必要である** だけなので、 `dependencies` ではなく `peerDependencies` に記述します。

```shell
$ yarn add --peer @babylonjs/core @babylonjs/loaders
```

こうすることで依存関係ツリーが正常になります。

```
- new-package/
    - node_modules/
        - @babylonjs/
            - core/
                - ...
        - babylon-vrm-loader/
            - node_modules/
                - ここには何も入らない!
```

`peerDependencies` に記述すると、 `npm install babylon-vrm-loader` しただけでは _WARNING_ 警告が出るだけで `@babylonjs/core` はインストールされません。ビルド時に依存関係が見つからないエラーになります。

開発中はインストールされていてほしいので、 `devDependencies` にも追加する必要があります。

#### webpack-dev-server で開発環境構築

[webpack-dev-server](https://github.com/webpack/webpack-dev-server) パッケージを利用すれば、 webpack による開発環境を簡単に構築することが出来ます。

まず適当に [test/index.html](https://github.com/virtual-cast/babylon-vrm-loader/blob/master/test/index.html) を作ります。

```html:test/index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Babylon.js VRM Loader Test Sandbox</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no">
<style>
html,
body {
    margin: 0;
    padding: 0;
    height: 100%;
}

body {
    overflow: hidden;
}

#wrapper {
    width: 100%;
    height: 100%;
}

#main-canvas {
    width: 100%;
    height: 100%;
}
</style>
    </head>
    <body>
        <main id="wrapper">
            <canvas id="main-canvas"></canvas>
        </main>
        <script src="vendors~main.js"></script><!-- 後述・ほとんど変わらない依存関係部分を別にして、リビルドを早くします -->
        <script src="main.js"></script>
    </body>
</html>
```

[エントリポイントとなる ts ファイル](https://github.com/virtual-cast/babylon-mtoon-material/blob/master/src/test/index.ts)を書きます。

```ts:src/test/index.ts
import { Engine } from '@babylonjs/core/Engines/engine';
import { Scene } from '@babylonjs/core/scene';

// scene.createDefaultEnvironment を呼ぶために import する必要がある
import '@babylonjs/core/Helpers/sceneHelpers';

async function main() {
    const canvas = document.getElementById('main-canvas') as HTMLCanvasElement;
    const engine = new Engine(
        canvas,
        true,
    );

    const scene = new Scene(engine);
    scene.createDefaultEnvironment({
        createGround: true,
        createSkybox: true,
    });

    engine.runRenderLoop(() => {
        scene.render();
    });
    window.addEventListener('resize', () => {
        engine.resize();
    });
    // デバッグ用にグローバル変数に露出させる
    (window as any).currentScene = scene;
}

main().catch((reason) => {
    console.error(reason);
});
```

[デバッグ用の webpack コンフィグ](https://github.com/virtual-cast/babylon-mtoon-material/blob/master/webpack.test.config.js)を書きます。

```js:webpack.test.config.js
const resolve = require('path').resolve;

module.exports = {
    mode: 'development',
    devtool: 'source-map',
    entry: resolve(__dirname, 'src', 'test', 'index'), // エントリポイントを src/test/index.ts にします
    output: {
        library: 'babylon-vrm-loader',
        libraryTarget: 'umd', // 一般的な umd 形式で出力します
        filename: '[name].js',
        path: resolve(__dirname, 'test'), // 出力先を test ディレクトリにします
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: 'ts-loader',
            },
        ],
    },
    resolve: {
        modules: [resolve(__dirname, 'node_modules')],
        extensions: ['.js', '.ts'],
    },
    devServer: {
        contentBase: resolve(__dirname, 'test'), // test 以下を開発HTTPサーバとします
        port: 8080,
    },
    optimization: {
        splitChunks: {
            chunks: 'all', // この設定で `vendors~main.js` が生成されます
        },
    },
    target: 'web',
};
```

この状態で `$ yarn webpack-dev-server --config webpack.test.config.js` を実行すると、 http://localhost:8080 にシーンが表示されるようになります。

後は、エントリポイントを起点に新しい Material を作ったり、自分なりの拡張をしていけば OK です。

拡張した新しいクラスは `src/` 直下において、 `src/index.ts` で `export * from './some-material.ts';` のような形で全てのファイルを export するようにすると、ビルドが楽になります。
 
## 公開用の webpack コンフィグを作る

babylon.js は Ver.4 から ESModules 形式に対応したので、 [Tree Shaking](https://webpack.js.org/guides/tree-shaking/) を意識して書くことが出来ます。必要な依存関係だけを含められるという webpack の機能です。

自分がビルドしたファイルの中に、依存である `@babylonjs` のコードは入れたくないので、 webpack の [externals](https://webpack.js.org/guides/author-libraries#externalize-lodash) 機能を使って依存部分をビルドに含めないようにします。

```js:webpack.config.js
const resolve = require('path').resolve;
const merge = require('webpack-merge');

// Web版・npm版両方同じの基本設定
const baseConfig = {
    mode: 'production',
    entry: resolve(__dirname, 'src', 'index'), // エントリポイントを src/index.ts に指定
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: 'ts-loader',
            },
        ],
    },
    resolve: {
        modules: [resolve(__dirname, 'node_modules')],
        extensions: ['.js', '.ts'],
    },
    target: 'web',
};

// 配列を export して二種類ビルドする
module.exports = [
    /**
     * UMD 形式で npm 用のビルド
     */
    merge(baseConfig, {
        output: {
            library: 'babylon-vrm-loader', // パッケージ名
            libraryTarget: 'umd',
            filename: 'index.module.js', // 出力するファイル名
            path: resolve(__dirname, 'dist'), // 出力先(gitignore しておきます)
        },
        externals: [
            /^@babylonjs.*$/, // この正規表現に引っかかる依存は全てビルドの対象外になります
        ],
    }),
    /**
     * ブラウザ用のビルド
     */
    merge(baseConfig, {
        output: {
            library: 'VRMLoader', // `window.VRMLoader` としてアクセス出来るようになります
            libraryTarget: 'window', // `window` グローバル変数に代入されるタイプです。
            filename: 'index.js',
            path: resolve(__dirname, 'dist'),
        },
        externals: [
            // ここが一番難しい所です。 externals に関数を渡し、場合に応じて「どのグローバル変数を利用するか」をわけています。
            function (context, request, callback) {
                // `import ... from '@babylonjs/core`; している所は `BABYLON` グローバル変数から取得する
                if (/^@babylonjs\/core.*$/.test(request)) {
                    return callback(null, 'var BABYLON');
                }
                // `import ... from '@babylonjs/loaders/glTF/2.0`; している所は `LOADERS.GLTF2` グローバル変数から取得する
                // @see https://github.com/BabylonJS/Babylon.js/blob/master/Tools/Config/config.json#L415
                if (/^@babylonjs\/loaders\/glTF\/2\.0.*$/.test(request)) {
                    return callback(null, 'var LOADERS.GLTF2');
                }
                // `import ... from '@babylonjs/loaders`; している所は `LOADERS` グローバル変数から取得する
                if (/^@babylonjs\/loaders.*$/.test(request)) {
                    return callback(null, 'var LOADERS');
                }
                callback();
            },
        ],
    }),
];
```

### ブラウザ用依存解決設定

注意したいのが最後の「ブラウザ用のビルド」時の `externals` の設定です。

ブラウザで `babylon-vrm-loader` を利用する場合は、下記のように `script` タグを構築します。

```html
<script src="https://preview.babylonjs.com/babylon.max.js"></script>
<script src="https://unpkg.com/babylon-vrm-loader/dist/index.js"></script>
```

つまり、 `require` だとか `import` だとかいった構文は通常のビルドだけでは利用できません。

そこで、 babylon.js がグローバル変数として出力する値で依存を解決するように書き換えてやる必要があります。

上記コードによって、例えば `import ... from '@babylonjs/core';` と書いた部分は、 `'@babylonjs/core'` の代わりに `BABYLON` グローバル変数から依存クラスを取得します。

このあたりは [Externals | webpack](https://webpack.js.org/configuration/externals/) に詳細が書かれているのですが、いまいちパッとしない説明だったので色々試行錯誤して結果上記になりました。パクってください。

## パッケージング

後は npm にビルドしたファイルを登録して終わりです。

今回は [semantic-release/npm](https://github.com/semantic-release/npm) パッケージを利用して自動化しています。

このパッケージに関しては、 [semantic-release + git-cz でライブラリを良い感じに Semantic Versioning する | ryonkmr.com](https://ryonkmr.com/155630461) を参考にしました。
一度構築してしまえばあとは自動で CHANGE LOG の発行、 GitHub/npm 連携のリリースなどが出来るので便利です。

コミットメッセージを元にバージョンアップするかどうか判定しているので、 `git commit` ではなく `git cz` を使うという所だけ気を付ける必要があります。

実際のパブリッシュは [CircleCI](https://circleci.com/gh/virtual-cast/babylon-vrm-loader) で行っています。これも 1 ファイルで構築でき、無償で利用を始められるので便利に使わせてもらっています。