2023-05-04
Source Map はよく使っているんですが、どういう風に動いているのか分かっていなかったので調べました。
Source maps are a way to map a combined/minified file back to an unbuilt state. When you build for production, along with minifying and combining your JavaScript files, you generate a source map which holds information about your original files.
ChatGPT に翻訳してもらうと以下のようになります。
ソースマップは、結合された/最小化されたファイルを元の未ビルド状態にマッピングする方法です。本番環境用にビルドする際に、JavaScriptファイルを最小化・結合すると同時に、元のファイルに関する情報を保持するソースマップを生成します。
背景として、一般的に JavaScript は本番で配信する場合は、Webpack のようなバンドラを通して1つのファイルにまとめたり、配信する容量を減らすため最小化(minify)したり、TypeScript からトランスパイルしたりします。
そのため、実行されている箇所と書いたコードの場所が異なるため、効率よくデバッグするためにこういった仕組みが必要になります。
試す&解説のために GitHub リポジトリを作成しました: mryhryki/example-sourcemap
ビルドツールは esbuild を使いました。
主なファイルは以下の4つです。
src/datetime.ts
: ビルドの起点となるファイルsrc/main.ts
: モジュールファイルdist/index.js
: ビルドした出力ファイル(src/datetime.ts
+ src/main.ts
の内容をバンドル&ミニファイしたファイル)dist/index.js.map
: ソースマップファイル(今回の本題)今回の本題なので、この中身だけ記載しておきます。
{
"version": 3,
"sources": ["../src/main.ts", "../src/datetime.ts"],
"sourcesContent": ["export function main(): void {\n console.log('START')\n throw new Error('DUMMY')\n}\n\n", "import {main} from \"./main\";\n\ntry {\n main()\n} catch (err) {\n console.error('ERROR:', err)\n}\n"],
"mappings": "AAAO,SAASA,GAAa,CAC3B,cAAQ,IAAI,OAAO,EACb,IAAI,MAAM,OAAO,CACzB,CCDA,GAAI,CACFC,EAAK,CACP,OAASC,EAAP,CACA,QAAQ,MAAM,SAAUA,CAAG,CAC7B",
"names": ["main", "main", "err"]
}
https://github.com/mryhryki/example-sourcemap/blob/blog/dist/index.js.map
Source Map の仕様はこちらの GitHub リポジトリにあります。
source-map/source-map-spec: The specification of the source map format
この中で特に mappings は分かりにくいので解説します。
Base64 VLQ という方法で、ビルドしたファイルと元ファイルの位置をマッピングしています。
Base64 を活用して数値の配列を少ない文字数で表現するための手法のようです。
Base64 VLQ概要 - Speaker Deck. というスライドの解説がわかりやすかったです。
この方式を使う主な理由は Source Map のファイル容量を削減することが目的のようです。
そもそもソースコード自体をすべて含んでいて大きいので、他はなるべく容量を減らしたいというのは分かりますね。
parse.ts というファイルでどういう構成になっているのか分析してみました。
結果は README に載せていますので、気になる方は見ると参考になるかも知れません。
相対パスでもURLでも指定可能です。
//# sourceMappingURL=index.js.map
//# sourceMappingURL=https://example.com/index.js.map
データ URL でファイルに含めることもできます。
もちろんファイルの容量はかなり大きくなります。
//# sourceMappingURL=data:application/json;base64,ewogICJ...
SourceMap というヘッダーで指定することもできるようです。
HTTP で配信している場合のみ使える方法です。
HTTP/1.1 200 OK
SourceMap: index.js.map
...
--enable-source-maps
をつけると有効になるようです。
$ node --version
v18.16.0
$ node --help | grep 'enable-source-maps'
--enable-source-maps Source Map V3 support for stack traces
実際に index.js を実行すると、元のソースコードの箇所を示してくれました。
$ node --enable-source-maps dist/index.js
START
ERROR: Error: DUMMY
at main (/xxx/example-sourcemap/src/main.ts:3:9)
at Object.<anonymous> (/xxx/example-sourcemap/src/get_secret.ts:4:3)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
at Module.load (node:internal/modules/cjs/loader:1117:32)
at Module._load (node:internal/modules/cjs/loader:958:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:23:47
基本的に何もしなくても有効になっていました。
(サンプリリポジトリの serve.ts を実行して試しました)