ソースマップの自作は割とコワくない。


「めんどくさそう」先入観で敬遠していたのですが、仕様を見たらそんなでもなかったので書き残しておきます。手っ取り早くコードを見たい方は[こちら](https://github.com/cognitom/CAPITALS)をどうぞ。

![1489821845-94F3558D-871C-46DA-9EE7-560E45EB45E6.png](https://qiita-image-store.s3.amazonaws.com/0/1858/791606d6-dc91-ad77-47b4-8753b71c1758.png "1489821845-94F3558D-871C-46DA-9EE7-560E45EB45E6.png")

ユーザとして、ソースマップに接する機会は多いですが、AltJSを作るとかでもない限り、あまり提供側に回ることはないかもしれません。私自身、次のような疑問というか誤解を抱えたまま、深入りしなかったクチです...。

- **誤解1**: ソースマップを作るにはASTを作る必要がある → 実際はもっとシンプル
- **誤解2**: なんかすごいエンコード(`AAAA;AACA;AACA;AACA...`)がされてる → ただのBase64

よく見かけるソースマップの説明記事(というほど、記事自体ないけれど)では、「ソースを構文解析してASTを構築して...」という手順が出てくるのですが、 **ソースマップにASTは必要ありません**。

もちろん、実際にAltJSを作るならASTの作成は必須なのですが、ソースマップとは独立した話です。逆に、ソースマップでできるのはシンプルで、 **次のふたつをマップすることだけ** です。

- コンパイル前のコードの位置: ○○行・○○列
- コンパイル後のコードの位置: ○○行・○○列

本稿では、コードのコンパイル中にどのようにマッピングデータを構築するか、簡単に説明を試みたいと思います。

## ソースマップってこういうやつ

もし、ソースマップのファイルを開いたことがなければ、ぜひ開いてみてください。`.map`の拡張子で終わるJSONのファイルです。BabelやTypeScript、CoffeeScriptを利用していれば、途中で必ず生成しているはず...

以下、例。

```json
{
  "version": 3,
  "sources": ["../fixture/HELLO.js"],
  "names": [],
  "mappings": "AAAA;AACA;AACA;AACA",
  "file": "hello.js",
  "sourcesContent": ["CONST NAME = \"WORLD\"\nCONST MESSAGE = `HELLO ${NAME}!`\nCONSOLE.LOG(MESSAGE)\n"]
}
```

ざっくり、次の3つの情報が記載されています。

- ファイル情報: 元ファイル(`sources`)、生成ファイル(`file`)
- マッピング情報 (`mappings`)
- 元ソース (`sourcesContent`)

マッピング情報のところが「初見殺し」ですが、コードの位置情報をBase64(正確には可変長な「Base64 VLQ」)で保持しているだけ(マップファイルの容量圧縮のため)。詳しくは[こちら](https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-base64vlq)。

**メモ**: ちなみに、`version`はファイル自体のバージョンではなく、 **ソースマップの仕様のバージョン** を表しています。基本「3」を指定しておけばOK。

**メモ**: WebPackやRollupなどのバンドラを使う場合など、元ファイルは複数になる場合があります。なので、`sources`と`sourcesContent`は配列です。

## なんちゃってコンパイラを作る

超絶簡単なAltJSを考えます。例えばこんなの。

```
CONST NAME = "WORLD"
CONST MESSAGE = `HELLO ${NAME}!`
CONSOLE.LOG(MESSAGE)
```

これをコンパイル(トランスパイル)すると、こうなります。

```javascript
const name = "world"
const message = `hello ${name}!`
console.log(message)
```

そうですね、お察しの通り`.toLowerCase()`しているだけです。

## コンパイルしながらマッピング

コンパイルした後に、独立してマッピングするのは非効率かつ不可能な場合も多いです。パースして変換する際、同時にソースマップも付けていくのが楽です。

ソースマップなしであれば、先ほどのコンパイラ(?)は1行で実装可能です。

```javascript
function compile (srcCode) {
  return srcCode.toLowerCase()
}
```

一応、行ごとに処理するように書くならこうですね。

```javascript
function compile (srcCode) {
  return srcCode
    .split('\n')
    .map(line => line.toLowerCase())
    .join('\n')
}
```

ここに、マッピングの処理を加えるとこんな感じに。詳しくは次項。

```javascript
function compile (srcFile, srcCode, destFileName) {
  // 出力ファイル名の指定
  destFileName = destFileName || path.basename(srcFile).toLowerCase()

  // SourceMapGeneratorの初期化
  const gen = new SourceMapGenerator({ file: destFileName })
  gen.setSourceContent(srcFile, srcCode)
  
  // コンパイルしながらマッピング
  const code = srcCode
    .split('\n')
    .map((line, idx) => {
      // 一行ごと対応付け
      gen.addMapping({
        source: srcFile,
        original: { line: idx + 1, column: 0 },
        generated: { line: idx + 1, column: 0 }
      })
      
      // コンパイル...というか小文字に変換するだけ
      return line.toLowerCase()
    })
    .join('\n')

  // ソースマップを付属させるため、文字列ではなくオブジェクトで返す
  return {
    code: code + `\n//# sourceMappingURL=${destFileName}.map`,
    map: gen.toString()
  }
}
```

## SourceMapGenerator

幸い、ソースマップを操作するためのライブラリがMozillaから提供されています。

- https://github.com/mozilla/source-map

いくつかクラスがあるのですが、ここでは[SourceMapGenerator](https://github.com/mozilla/source-map#sourcemapgenerator)だけ使います。

マッピングデータは、普通に作ると重くなってしまうので`AAAA;AACA;AACA;AACA...`みたいな形にエンコードします。SourceMapGeneratorは、このあたりのことを自動でやってくれます。

インスタンスメソッドに`addMapping()`というそのものずばりのものがあり、元コード(original)と生成コード(genrated)の位置を対応づけます。たとえば、次のように書くと、3行目と3行目(のそれぞれ0列)がマップされます。

```javascript
gen.addMapping({
  source: 'hello.js',
  original: { line: 3, column: 0 },
  generated: { line: 3, column: 0 }
})
```

このマッピングが、1行にひとつずつ以上提供されていれば、デバッグの際に行を追うことができるわけです。

## 試してみる

実際にプログラムを動かしてみるのが、やはりわかりやすいと思います。GitHubにサンプルコードを用意したので、こちらをクローンしましょう。

- https://github.com/cognitom/CAPITALS

以下は、デスクトップに展開して試す場合です。

```bash
$ cd ~/Desktop/
$ git clone https://github.com/cognitom/CAPITALS.git
$ cd CAPITALS
```

CLIから動かしてみます。

```bash
$ ./cli.js test/fixture/HELLO.js ../hello.js
```

デスクトップに次のファイルができていれば成功です。

- `hello.js`: 小文字に変換されたスクリプト
- `hello.js.map`: ソースマップ

HTMLファイルから読み込んでブラウザで表示すると、本稿冒頭のスクリーンショットのようになります。正しく、元ファイル上でステップ実行できました。

コードの詳細は、[index.js](https://github.com/cognitom/CAPITALS/blob/master/index.js)をどうぞ。

## さらに調べるには

このあたりをどうぞ。

- [ソースマップ仕様 (v3)](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit)
- html5rockの記事「[Introduction to JavaScript Source Maps](https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/)」