はじめに

Promises Bookという薄い本を書いているのですが、書籍中に出てくるサンプルコードはテストが必須であるという原則があります。

サンプルコードは必ずテストコードが必要となる。(読者がコピペして実行するようなコードをテストを書くべきである)
promises-book/CONTRIBUTE.md at master · azu/promises-book

サンプルコードとテストはNode(といってもほぼCommonJSというだけ)で書いています。

ECMAScript6 Promisesについての内容なので、ブラウザ/Node.js どちらの実行環境でもいいのですが、閲覧するのはブラウザが基本になると思うのでブラウザ向けのコードを表示したいという感じになると思います。

CommonJS -> ブラウザ用のJS といえば、browserifyCommonJS Everywhere等がありますが、
これらは書き方次第で実行できないような状況ができてしまうと困るので、module.exportrequire 等をエミュレートするコードを入れることで解決しています。

そのため、元のコードが小さくても、元コード + エミュレートする関数等が入って表示用としてはあまりキレイではありません。

複雑なreuqireによる依存の解決等を捨てて、書き方にある程度制約を持てば単純にrequireをインライン化したり、不要な exportを削除して結合しただけのコードでもそのまま動くように出来るはず という感じで表題にある事をするNodeモジュールを書きました

inlining-node-require

azu/inlining-node-requireは先ほども述べたように、 require("./add"); というようにモジュールを読み込んでいた場合は、add.js をその場に展開して単一のファイルにするという感じのモジュールです。

下記のような、var addmodule.exports = add といいうように、識別子が一致してる場合はその辺を丸ごと削ったりしてより短くなるようにしてます。

add.js

function add(x, y) {
    return x + y;
}
module.exports = add;

index.js

var add = require("./add");
add(1, 2);

index.js をエントリーポイントに指定して、そこを中心にrequire先をインライン化していきます。

$ inlining-node-require index.js

出力結果は以下のようになります。

function add(x, y) {
    return x + y;
}
add(1, 2);

remove-use-strict

azu/remove-use-strict は不必要な "use strict";という宣言となるリテラルを取り除くNodeモジュールです。

上記の azu/inlining-node-require は簡単にいればただのconcatです。
一般的に "use strict"; は各モジュールに宣言して使うと思うので、結合すると無意味な "use strict";が出来てしまうかもしれません。

無意味な "use strict";というのは、そもそも "use strict";というのはディレクティブプロローグという関数やプログラムbodyの先頭に書かないと行けないので、それ以外の位置に出てきてたらおかしいという感じで消すようにしてます。

ディレクティブプロローグやstrict modeについては下記を読むといいです。

実際に以下のように、ディレクティブプロローグ以外の "use strict"; を削除してくれます。(オプションで全ての "use strict"; を削除するモードもあります)

var removeUst = require("remove-use-strict");
var code = 'var a = 1;' +
    '"use strict";';// unnecessary use strict...
removeUst(code); // => 'var a = 1;'

azu/inlining-node-require にこの機能を含めてしまうのでも良かったのですが、それぞれ単体のモジュールで用意したほうが柔軟性があるので単機能モジュールとして分けてます。

ユースケース

どちらもPromises Bookという電子書籍的なものを書いてて欲しくなったので書きました。

先ほど書いたように Promises Book では基本的にコードはNodeで書かれていて(ただしブラウザで動くようにpolyfillを使う)、またサンプルコードはテストするべきであるという方針を持っています。

また、文章はAsciidoctorで書かれてるので、ソースコードはそのままInclude Directiveを使って埋め込めます。

例えば、Promise.all とXHRを使ったサンプルコードのソースは以下のようになってます。

promise-all-xhr.js

"use strict";
var getURL = require("../../Ch1_WhatsPromises/src/xhr-promise").getURL;
var request = {
    comment: function getComment() {
        return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
    },
    people: function getPeople() {
        return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
    }
};
function main() {
    return Promise.all([request.comment(), request.people()]);
}

module.exports.main = main;
module.exports.request = request;

テストするpromise-all-xhr-test.js用に関数等をexportsしてあったり、以前書いたコードを再利用したいので require などもしています。

上記のようなコードはサンプルコードとして表示するには、他のコードに依存してたり(require)、無意味な exports があったりするので向いてないです(またコピペで動かない…)

これをブラウザで見るように、azu/inlining-node-requireazu/remove-use-strictを使って変換すると以下のようになります。

embed-promise-all-xhr.js

'use strict';
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, false);
        req.onload = function () {
            if (req.status == 200) {
                resolve(req.response);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    return Promise.all([request.comment(), request.people()]);
}

こうすることで、書く時はテストしやすいようにして、表示するときは表示用に結合/無駄な部分を削ったものを使うようにして、サンプルコードもテストしやすい状況を作っています。
(まだ、表示用のコードの動作をどう担保するかは考え中)

変換はどちらもgulpを書いたのでそれを使って変換してます。

gulp pluginはsindresorhus/generator-gulp-plugin-boilerplatehparra/generator-gulp-pluginなんかを使うと5分ぐらいで書けます。

物理本ではないので、紙面のサイズはそこまで気にしなくていいのでこういう方針にしてますが、
世の中の書籍はどうやってサンプルコードを管理してるんだろ?

仕組み

ここまで読んだ人は気づいてるかもしれませんが、これらのモジュールはJavaScript ASTを変換する事で行っています。

例えば、azu/remove-use-strictでは以下のような ディレクティブプロローグの文字列変数に代入してる文字列 の区別が付けられるようにしているため、誤爆して削除されません。(この場合は削除対象がないですね)

"use strict";
function a(){
    "use strict";
    var a = "use strict";
}

ただ、JavaScript ASTはただの木構造のオブジェクトと言っても書き換えは色々手順が必要なので面倒な部分が多いです。

そこで、azu/inlining-node-requireではコード表現を直に使って書き換えを行えられる falafel というモジュールを使っています。
falafelはtraverse関数も持ってるので、ASTのTreeを走査しながら、require 関数のノードを見つけたら、指定ファイルの中身を取ってきてfalafelを使って埋め込むように書き換えるということを繰り返しています。

これにより、ASTの書き換えに必要なコード量はかなり減ります。

falafel はASTをrangeを持つマッピングデータとしてだけ扱ってて、実際に書き換えるのは文字列(chunk)にしておいたソースコードなため、直感的に置換する文字列のソースをそのまま書き換えに指定出来ます。

一方、コードを書き換える という事をASTでやろうとすると、書き換え後のコードをASTとして作り、
その作成したASTを書き換えたい所へ置換したり追加したりする必要があります。

azu/remove-use-strict の方では、特定のnodeを削除したいだけだったので、直接ASTからnodeを削除していく感じにしてます。

そのnode(StringのLiteral)がディレクティブプロローグであるかどうかを知る必要があるので、
関数のスコープごとにスタックを積んでいく感じのよくある実装をしています。

走査に使ってるEstraverseでは、rootノードから子ノードを見ていく enter と 逆に葉ノードから親ノードをたどっていく leave が一緒に書けるので、こういうスコープを分けて考える書き方がやりやすいです。

単純にスコープを知るだけなら、escopeast-scope を使ったほうが楽かもしれません。


仕組みの話が何かごちゃつきましたが、JavaScript ASTについては JSオジサン というイベントで少し話すことになってるので、それまでに何かもう少し整理したいですね。

LTなのでそこまで色々話せるわけじゃないですが。

browserifyみたいに大きなものとかじゃなくて、ちょっとしたものならJavaScript ASTを使って書くというのは、ものすごく難しいというレベルではないので(基本的に木構造のオブジェクトを見ていくという話なので)、何か作ってみるといいかもしれません。

初めてJavaScript ASTを触る時はESLintのプラグインを書いてあたりからやってみるのをオススメします。