Source Map はどのように動いているのか?

2023-05-04

はじめに

Source Map はよく使っているんですが、どういう風に動いているのか分かっていなかったので調べました。

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.

https://developer.chrome.com/blog/sourcemaps/

ChatGPT に翻訳してもらうと以下のようになります。

ソースマップは、結合された/最小化されたファイルを元の未ビルド状態にマッピングする方法です。本番環境用にビルドする際に、JavaScriptファイルを最小化・結合すると同時に、元のファイルに関する情報を保持するソースマップを生成します。

背景として、一般的に JavaScript は本番で配信する場合は、Webpack のようなバンドラを通して1つのファイルにまとめたり、配信する容量を減らすため最小化(minify)したり、TypeScript からトランスパイルしたりします。
そのため、実行されている箇所と書いたコードの場所が異なるため、効率よくデバッグするためにこういった仕組みが必要になります。

サンプルリポジトリ

試す&解説のために GitHub リポジトリを作成しました: mryhryki/example-sourcemap

ビルドツールは esbuild を使いました。

主なファイルは以下の4つです。

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 の仕様

Source Map の仕様はこちらの GitHub リポジトリにあります。
source-map/source-map-spec: The specification of the source map format

この中で特に mappings は分かりにくいので解説します。

mappings

Base64 VLQ という方法で、ビルドしたファイルと元ファイルの位置をマッピングしています。

Base64 VLQ とは

Base64 を活用して数値の配列を少ない文字数で表現するための手法のようです。
Base64 VLQ概要 - Speaker Deck. というスライドの解説がわかりやすかったです。

この方式を使う主な理由は Source Map のファイル容量を削減することが目的のようです。
そもそもソースコード自体をすべて含んでいて大きいので、他はなるべく容量を減らしたいというのは分かりますね。

分析してみる

parse.ts というファイルでどういう構成になっているのか分析してみました。
結果は README に載せていますので、気になる方は見ると参考になるかも知れません。

指定方法

1. ファイル内に記載する方法

1-1. URL で指定する方法

相対パスでもURLでも指定可能です。

//# sourceMappingURL=index.js.map
//# sourceMappingURL=https://example.com/index.js.map

1-2. Data URL で指定する方法

データ URL でファイルに含めることもできます。
もちろんファイルの容量はかなり大きくなります。

//# sourceMappingURL=data:application/json;base64,ewogICJ...

2. HTTPヘッダーで指定する方法

SourceMap というヘッダーで指定することもできるようです。
HTTP で配信している場合のみ使える方法です。

HTTP/1.1 200 OK
SourceMap: index.js.map
...

実際に動かしてみる

Node.js で Source 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

Browser (Google Chrome) で Source Map を有効にする

基本的に何もしなくても有効になっていました。

Google Chrome DevTools

(サンプリリポジトリの serve.ts を実行して試しました)

参考リンク