Simpacker 用の manifest.json を出力する Node.js スクリプト を作った

公開:2020/12/05
更新:2020/12/05
6 min読了の目安(約5800字TECH技術記事

最後に気づいたのですが、なぜか今回の記事は「だ、である調」で書いてました。
理由は不明です。

TL;DR

https://gist.github.com/JUNKI555/8f5a3616be630840b65dbb13769c51c3

Simpacker 用の manifest.json を出力したかった

Simpacker を Parcel で使うとかなり構成がシンプルになるが、
そもそも yarn だけでできないか考えていた。
PostCSS や TypeScript のトランスパイルは yarn だけでできる。
あとは Simpacker 用の manifest.json を自動生成できれば特に不便はなさそう。
つまり Rails+Simpacker+Parcel 環境時の
parcel-plugin-bundle-manifest に相当する物を
Node.js スクリプトで作ってやればできるはずだ。

処理の流れ

  • public/ 配下を対象として
  • public/ 直下のファイルを除外しつつ再帰でファイルを列挙して
  • パスから public/ は抜いてやりつつファイル名をKey, ファイルパスをValueにしたjsonを出力
  • ファイルパスにキャッシュバスター( ?v=HOGE とか)がついてるとなお良さそう

うん、いけそう。
キャッシュバスターの話はこことかに書いてあるが
要するにクエリ文字列つきのURL参照にしてやってブラウザキャッシュを強制的に効かなくする感じ

できた

できた。

使い方

  • 基本的には Rails+Simpacker プロジェクトのルートで
    • node output_manifest_js.js public でOK
  • コードの定数部分を変えればいいんだが、manifest.json のパスを変えたいときは
    • node output_manifest_js.js public public/packs/hoge-manifest.json とかでできるようにしてある
// ==================================================
// Simpacker 用 manifest.json 出力 Node.js スクリプト
// 基本の使い方:node output_manifest_js.js public
// ==================================================
const fs = require('fs');
const path = require('path');
const crypto = require('crypto')
// default ignore Path
const DEFAULT_IGNORE_PATH = 'public';
// default manifest.json path
const DEFAULT_OUTPUT_MANIFEST_PATH = 'public/packs/manifest.json';
// Cache Bustring String Source
const S = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// Number of Cache Bustring String
const N = 6;

// Check args
function showUsageAndExit() {
  const basename = path.basename(process.argv[1]);
  console.error(`Usage: node ${basename} <target directory path> <optionla:ignore path> <optional:manifest.json path>`);
  process.exit(1);
}

const getFiles = (dirpath, callback) => {
  fs.readdir(dirpath, {withFileTypes: true}, (err, dirents) => {
    if (err) {
      console.error(err);
      return;
    }

    for (const dirent of dirents) {
      const fp = path.join(dirpath, dirent.name);
      if (dirent.isDirectory()) {
        getFiles(fp, callback);
      } else if (ignorePath === dirpath) {
        // public ディレクトリ直下は処理対象外
        continue;
      } else if (fp === outputManifestPath) {
        // マニフェストファイルそのものは処理対象外
        continue;
      } else {
        cacheBustringString = Array.from(crypto.randomFillSync(new Uint8Array(N))).map((n)=>S[n%S.length]).join('');
        outputObject[dirent.name] = `${fp.replace(ignorePath, '')}?v=${cacheBustringString}`;
        callback(fp);
      }
    }
  });
}

// process.argv[0] : Node.jsの実行プロセスのフルパス
// process.argv[1] : スクリプトファイルのフルパス
// なので3番目以降をargsに取り出す
const args = process.argv.slice(2);
if (args.length < 1) {
  showUsageAndExit();
}

if (args.length < 2) {
  args.push(DEFAULT_OUTPUT_MANIFEST_PATH);
  args.push(DEFAULT_IGNORE_PATH);
}

if (args.length < 3) {
  args.push(DEFAULT_IGNORE_PATH);
}

const checkPath = args[0];
const outputManifestPath = args[1];
const ignorePath = args[2];
const outputObject = {};
getFiles(checkPath, console.log);

// getFiles の処理が終わり切ってから実行するために setTimeout
// Promise化すれば getFiles を同期実行できそう……
setTimeout(console.log, 500, `wait: output ${outputManifestPath} ...`);
setTimeout(()=>{ fs.writeFileSync(outputManifestPath, JSON.stringify(outputObject, null, 2)); }, 510);

実行。

node output_manifest_js.js public

出力例。
public/packs/manifest.json

{
  "baz.js": "/packs/js/baz.js?v=EDQ0N0",
  "sample.js": "/packs/js/sample.js?v=N7TG5H"
}

作るのに色々検索した履歴

Node.js でコマンドライン引数を取得する

// process.argv[0] は Node.jsの実行プロセスのフルパス
// process.argv[1] は スクリプトファイルのフルパス
// process.argv[2] 以降がコマンドライン引数
process.argv

ファイルパスを再帰で出力する

const fs = require('fs');
// fs.readdir(path[, options], callback)

ランダム文字列の生成

const S = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const N = 6;
Array.from(crypto.randomFillSync(new Uint8Array(N))).map((n)=>S[n%S.length]).join('');

json を出力する

fs.writeFileSync(outputManifestPath, JSON.stringify(outputObject, null, 2));

npm パッケージにしてみる?

モジュールバンドラに依存しない、似たようなものは存在しないみたいでした……。
YAML を読み込むパッケージなんかも取り入れて config/simpacker.yml を読み込めば
出力するべき manifest.json のファイルパスもわかりますし、
npm に公開しようかなとも考えたんですが、需要ありますかね……?
(需要なくても2021年初ぐらいに公開してみたいかも)