フォルダ構造をそのままマップモジュール化するCLIツール作った


タイトルでは今一つ伝わらない気がしますが、図式化すると以下のようなツールです。

|-src
  |-assets
    |- font
      |- aldrich.ttf
    |- image
      |- player.png
      |- enemy.png

このような構造のフォルダを対象とすると…

import mod_0 from "./font/aldrich.ttf";
import mod_1 from "./image/player.png";
import mod_2 from "./image/enemy.png";
export default {
  "font": {
    "aldrich": mod_0
  },
  "image": {
    "player": mod_1,
    "enemy": mod_2
  }
};

上記の内容のsrc/assets/index.jsというファイルを生成します。

詳しい概要

元はphina.jsでの開発で、アセットファイルを追加・削除する度にコード本体を修正するのが面倒くさかったのをどうにかするために作ったツールです。
ただ、別に依存パッケージとかではないのでphina.jsとは全く関係ない使い方もできます。

一例として生成したモジュールをphina.jsで使用する例を書いてみます。
先ほど示したディレクトリ構造のようにフォルダ名(fontimage)がphinaのアセットローダーに対応したフォーマットであれば、出力されたモジュールを何もいじらずそのままアプリ初期化処理に使えます。

// src/index.js
import assetMap from "./assets/index";

// アプリケーション作成時にアセットをロード
const app = phina.game.GameApp({
  asset: assetMap,
  /* その他のオプション */
});

app.run();

ツール名はdimicです。
ディレクトリ(directory)を模倣する(mimic)の組み合わせから。

githubリポジトリ

使い方

前提

本ツールを使うには予めnode.js v10.10以上をインストールしている必要があります。
npmのバージョンはツールと無関係ですが、以下の説明ではnpxコマンド導入済みバージョン(v5.2以上)を使っていることを前提とします。

前述の通り、デフォルトではsrc/assets/index.jsというファイルを出力しますので、もしすでに同名のファイルがあった場合、上書きしてしまうことにも注意してください。

インストールと実行

インストールはおなじみnpm install dimic
グローバルインストールする場合はさらに-gオプションを付けます。

そしてプロジェクトフォルダのsrc/assets以下に先の例のような適当なフォルダ・ファイルがある状態で、以下のコマンドを実行します。

npx dimic

(グローバルインストールしている場合はnpxは不要です)

処理が始まり、程なくしてsrc/assets/index.jsというファイルが生成されていればOKです。

オプション

よくあるCLIツールと同じく、オプションを付与することで色々挙動を変えることができます。以下では主要なオプションを紹介します。
(以下サンプルコードではnpxは省略します)

入出力フォルダ・ファイルの変更

dimic --input-dir static/assets --output-file index.ts

デフォルトではsrc/assets/が入力対象ディレクトリ、出力ファイルの名前がindex.jsになっていますが、これをそれぞれ以上のようなオプション指定で変更することができます。
オプション名は-i-oとそれぞれ省略できます。

ファイルの監視(watchオプション)

dimic --watch

対象ディレクトリを監視し、変更があったら都度モジュールファイルを更新します。
省略して-wと書くこともできます。

ちなみに内部的にはchokidarを使っています。

リスト化対象ファイルの選り分け

glob形式で対象ファイルを選別することができます。

dimic --match *

デフォルトでは!_*が指定されていて、これは「"_"(アンダースコア)から始まるファイル以外全て」が対象、つまり_player.pngのようなファイルやフォルダは無視される設定になっています。
上記のように*のみを設定すると全てのファイルをリストアップ対象とします。

パターンマッチにはminimatchを使っているので、指定方法は以下のようなチートシートが参考になるかと思います。

https://motemen.hatenablog.com/entry/2014/07/15/minimatch-cheat-sheet

出力形式を変える

--formatオプションで出力形式を変更できます。

dimic --format json

以上のコマンドでファイルをjson形式で出力します。

今のところオプションはesm(デフォルト)、jsonおよびnoneがあります。
esmは冒頭サンプルで示した通りで、noneは以下のようにシンプルにexport defaultするだけのオプションです。

export default {
  "font": {
    "aldrich": "./font/aldrich.ttf"
  }
  "image": {
    "player": "./image/player.png",
    "enemy": "./image/enemy.png"
  }
}

オプションは他にもありますが、とりあえず以上を覚えておけば十分な気がします。

その他

対象ファイルは一階層に限定されます。例えば

  • src/assets/image/foo/bar.pngにように一段深い階層にあったり
  • src/assets/bar.pngのように0階層目にあるファイル

は無視されます。(これは仕様変更するかも?)

応用:TypeScriptでの型定義とenum化について

これをお読みの貴方がもしTypeScriptユーザーでしたら、アセットの型を定義して関連する関数を使いやすくしたいと思うことかもしれません。
アセットの型定義はkeyof typeof構文を使って実現することができます。

// src/example.ts
import assetMap from "./assets/index";

type ImageKey = keyof typeof assetMap.image;
function createImg(key: ImageKey) {
  const img = document.createElement("img");
  img.src = assetMap.image[key];
  return img;
}

// VS Codeなら引数指定のときにインテリセンスが効くようになったり、
// 存在しないアセットキーを指定するとエラーを出してくれるようになります。
const img1 = createImg("player");
const img2 = createImg("palyer"); // エラー!

更にちょっとマニアックですが文字列ベースの列挙型(文字列Enum)にする方法も合わせて紹介します。

import assetMap from "./assets/index";

/**
 * 指定オブジェクトのキーから文字列enumを作成するutil関数
 */
function objectKeysToEnum<
  T extends Record<string | number, any>
>(o: T): { [K in keyof T]: K } {
  return Object.keys(o).reduce((accumulator, currentValue) => {
    accumulator[currentValue] = currentValue;
    return accumulator;
  }, Object.create(null));
}

const ImageKey = objectKeysToEnum(assetMap.image)
console.log(ImageKey.player); // "player"
console.log(ImageKey.enemy); // "enemy"

// 型定義
type ImageKey = keyof typeof ImageKey;

気持ちとか

  • node.js CLIツールは初制作です
  • 作っている途中「すでに似たようなツールがありそう(車輪の再発明なのでは?)」という懸念が湧きましたが、CLIツール作りは以前からやってみたかったのと、課題としても比較的良い題材だったので気にしないことにしました
  • オプション名を決めるのが何気にとても迷います。webpack、rollupなどの大手ツールをなるべく参考にして名付けていますが…
  • fs関係のテストはfs-mockでやりましたが、これもそこそこ大変だったので、いずれ記事にしてみたい

参考

  • How to build a CLI with Node.js
  • pheanut:phina.jsをラップしたフレームワーク? ちゃんと使ったわけではないですが、スクリプトでアセットを組み立てる機能があるようで、今回のツールの発案のヒントになったので紹介します。