CoffeeScriptなどの別言語からコンパイルしたり、ClosureコンパイラなどでMinifyしたりしたソースをデバッグしているときなどに、生成されたJavaScriptソースコードから変換前のオリジナルソースの場所を知りたいときがあります。
それを知るための技術がSource Mapです。これがどのようにオリジナルのソースを参照しているのか気になったので調べてみました。
CoffeeScriptをコンパイルしたときのソースマップ
簡単な例として、フィボナッチのCoffeeScript版をソースとして用います (fibonacci.coffee)。
1 2 3 4 5 6 7 | fib = (n) -> if n == 0 or n == 1 n else (fib n - 1) + (fib n - 2) console.log fib 10 |
これを-cm
でマップ付きでコンパイルした結果のfibonacci.jsが次になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // Generated by CoffeeScript 1.6.3 ( function () { var fib; fib = function (n) { if (n === 0 || n === 1) { return n; } else { return (fib(n - 1)) + (fib(n - 2)); } }; console.log(fib(10)); }).call( this ); /* //@ sourceMappingURL=fibonacci.map */ |
そのソースマップfibonacci.mapは次のようになりました。
1 2 3 4 5 6 7 8 9 10 | { "version" : 3, "file" : "fibonacci.js" , "sourceRoot" : "" , "sources" : [ "fibonacci.coffee" ], "names" : [], "mappings" : ";AAAA;CAAA,EAAA,GAAA;;CAAA,CAAA,CAAA,MAAO;CACH,GAAA,CAAQ;CAAR,YACI;MADJ;CAGK,EAAA,UAAD;MAJF;CAAN,EAAM;;CAAN,CAMA,CAAA,IAAO;CANP" } |
mapping
の部分がそのマッピング情報データで、「生成コード → 元ソース」の対応情報になっています。
このデータは、生成ファイルの各行ごとに ;
で区切られ、それぞれの行の各セグメントごとに ,
で区切られたデータとなっています。
そこで、生成ファイルとマッピングデータとの対応をわかりやすく行ごとに示したら、次のようになります。
行によっては、セグメントがなかったり、複数あったりすることがわかります。
セグメント情報
次はそれぞれのセグメントが、どのように元ソースを示しているのかを説明します。
まず、各セグメントはBase64でエンコードされていますので、これをデコードします。例えば、AAAA
というセグメントなら、0, 0, 0, 0という4つの数値が得られます。
この4つの値はそれぞれ次のように意味を持っています。
ひとつめの値は生成コードの列を示します。上で示したように、セグメントは生成コードのどの行を示しているかは判るので、あとは列がわかればよいというわけです。
ふたつめ以降が元ソースの情報になります。ふたつめはソースIDで、ソースマップファイルの配列sources
の添字を指定します。みっつめとよっつめでそれぞれソースの行と列を指定します。
というわけで、先ほどの例に戻ると、2行目のAAAA
によって1行目1列目を指定していることになります。
また別の例として、5行目最後のMAAO
を見てみましょう。
これはBase64では12, 0, 0, 14となります。ただし実際には、これをそのまま利用するのではなくて、次のような変換を行います。
12を2進数表現した001100
において、最上位ビットと最下位ビットは特別な意味を持ちます。
最上位ビットはcontinuation bitですが、0
なのでここではあまり気にしなくてよいのでよいです。
最下上位ビットはsign bitで、符号を示します (0なら + 、1なら −)。
残りのビット群はそのまま数値として利用します。
というわけで、最終的に得られる数値は6, 0, 0, 7という4つの数値が得られます。
これに基づいてマッピング位置を決めるわけですが、実はセグメント情報の数値はどれも現在値からの相対値になっています (だからsign bitで負の値が取れるようになっている)。
なお、ひとつめの生成コードの列は行ごとにクリアされて1列目に初期化されますが、ソースIDは初期値0、元ソースの行と列はどちらも初期値1です。
というわけで、先ほどの例に戻ると、5行目のMAAO
より前では後ろ3文字がAAA
のみなので、元ソースの添字、行、列のどれも変化していないことがわかります。
5行目について言えば、CAAA
が3つあるので生成コードの列が1つずつ右に行くのを3回行ってから、MAAO
を適用するので次のようなマッピングになります。
なんでこういうマッピングをしているのかはわかりません。CoffeeScriptのマッピングはちょっとおかしい気がします。
セグメント情報 詳細
セグメント情報の別のデコード例としてQAA2BH
を見てみましょう。
continuation bitは可変長数値表現 (Variable-length quantity; VLQ)であり、大きい値を指定したい場合に利用します。
なお、デコードについてはソースコードを見たほうが速いです。
また、5つめの値はこのセグメントと関連のある名前 (識別子など) を示すもので、ソースマップファイルの配列name
の添字を指定します。
jQueryをminifyしたときのソースマップ
長くなったので、jQueryをminifyしたときのソースマップのマッピングを図示したものを示して終わりにします。
1 2 3 4 5 6 7 | { "version" :3, "file" : "jquery-2.0.3.min.js" , "sources" :[ "jquery-2.0.3.js" ], "names" :[ "window" , "undefined" , "rootjQuery" , "readyList" , "core_strundefined" , "location" , "document" , ... , "jQuery" , "_$" , "$" , ... ], "mappings" : ";;;CAaA,SAAWA,EAAQC,WAOnB,GAECC,GAGAC,EAIAC,QAA2BH,WAG3BI,EAAWL,EAAOK,SAClBC,EAAWN,EAAOM,SAClBC,EAAUD,EAASE,gBAGnBC,..." } |
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。