メモログ

💡 Personal notes about somthing I'm interested in

JSONデータを読み込んで差し替える

Node.jsの中であれば単にconst config = require('config.json')すれば良いのだけれど、フロントエンド側のJavaScriptではそうはいかない。XMLHttpRequestとかFetch APIを使うか、requireで書いたものをwebpackでbundleするとかしないといけない。他にもいろいろ方法はあると思うけど、まあとにかく一手間が必要。

その読み出すJSONデータを環境ごとに変更したい場合はなおさら面倒で、Node.jsであればprocess.env.NODE_ENVなんかを使えばいいけど、フロントエンド側ではwebpackでバンドルするときの設定を変更するとか、uglifyでDEBUGみたいなglobal変数を入れておいて書き出すとかしつつ、異なるJSONデータをfetchするように実装しないといけない。いろいろ方法はあるから難しい問題ではないけど、面倒ではある。

だからフロントエンド側の実装で const config = require('config.json') とあったら、const config = {'foo': 'bar'}みたいに置き換えてくれたら良いなあと思って、retrieve-jsonというものを用意してみた。

npm install -g retrieve-json

でインストールしたら、

retrive-json -i input.js -o output.js

という風に実行する。するとファイル内のrequireメソッドを探して、requireしてるJSONデータがローカルに見つかったら、そのデータに置き換えてくれる。--prefix dev. みたいにprefixオプションを指定するとdev.config.jsonみたいなファイル名のデータで差し替えてくれるので、環境ごとに別のJSONファイルを用意して差し替えることができる。

このくらいのことであれば正規表現で置換してもいけると思うのだけど、今回はesprimaescodegenestraverseを使って、以下のような感じでJavaScriptをASTに変換して、そこからrequire部分を見つけてJSONデータ(のASTノード)に置き換えるということをしている(実装にはTypeScriptを使用しているけど、普通のJavaScriptと内容的には変わらない)。

const ast = esprima.parse(data);
const result = estraverse.replace(ast, {
  enter: (node, parent) => {
    if (node.type === 'VariableDeclaration') {
      const declarations = node.declarations || [];
      const declaration = declarations && declarations[0];
      let kind = node.kind;
      if (!kind && declarations.length !== 1) {
        return node;
      }

      const declareType = declaration.type;
      const id = declaration.id;
      const idName = id && id.name;
      const init = declaration.init;
      const initCallee = init && init.callee;
      const initCalleeName = (initCallee && initCallee.name) || '';
      if (declareType === 'VariableDeclarator' && initCalleeName === 'require') {
        const initArg = (init.arguments || [])[0];
        const initArgValue = initArg && initArg.value;
        if (initArgValue && /\.json$/i.test(initArgValue)) {
          const { dir } = path.parse(inputFilePath);
          const prefix = options.prefix || '';
          const paths = path.parse(initArgValue);
          const jsonFilePath = paths.dir + '/' + prefix + paths.name + paths.ext;
          const jsonPath = path.resolve(process.cwd(), dir, jsonFilePath);
          const exists = fs.existsSync(jsonPath);
          if (!exists) {
            return node;
          }
          const jsonString = fs.readFileSync(jsonPath, {
            encoding: 'utf8'
          });
          return esprima.parse(`${kind} ${idName} = ${jsonString}`);
        }
      }
    }

    return node;
  }
});

const output = escodegen.generate(result) + '\n';

今のところrequire('config.json')みたいに文字列でファイル名が渡されていないと変換できない。必要十分な状態ではあるのだけど、そのうち必要が出てきたらescopeなんかでスコープの変数値を取得して、変数で入ってる場合でもJSONデータを取得できるといいなと思っている。

以上。