WebPack 2 対応！イマドキの TypeScript ビルド環境 2016 決定版

まだ枠あいてるよー！のりこめー＾＾

TypeScript も 2.1 になって、型が益々賢くなりましたね。
TypeScript を効率的にビルドするには WebPack が欠かせないですが、 WebPack は最近 2.2 の RC が公開され、もうじきリリースされます。
私の開発環境も元々 WebPack の 1.x 系を使っていましたが、これを機に[マイグレーションガイド](https://webpack.js.org/guides/migrating/)を見ながら 2.2 RC に乗り換えました。

皆さんも今年のうちにビルドを見直しませんか？
この記事では私が Grunt 時代からコツコツ積み上げてきた TypeScript ビルド環境を公開します。

全体像
====

リポジトリはこちらになります。
[github:progre/template-typescript](https://github.com/progre/typescript-boilerplate)

zip でダウンロードして使うなり、 Git で取得するなりご自由にどうぞ。
Unlicense なので LICENSE ファイルを削除してご自由にライセンスを表明してからお使いください。

今回は master ブランチについて解説しますが、 Electron のビルドに特化したブランチもあるので、そちらも合わせてどうぞ。

ファイル一覧
----

```
📂 lib // ここに展開される
📂 src
  📂 public
    📂 js
      📄 index.ts    // クライアント側エントリーポイント
    📄 index.html    // html 等はそのまま lib にコピーされます
  📂 test
    📄 test.ts       // テストコードエントリーポイント
  📄 index.ts        // サーバー側エントリーポイント
📄 package.json
📄 tsconfig.json     // TypeScript の設定
📄 tslint.json　      // ビルドには組んでませんが VSCode でチェックしています
📄 typings.json      // Typings の型定義
📄 webpack.config.js // WebPack の設定
```

コマンド
----

```bash
$ npm install         # typings install も実行されます
$ npm run debug-build # デバッグビルド
$ npm run watch       # ウォッチしながら開発ビルド
$ npm run build       # リリースビルド
$ npm test            # テスト
```

ビルドの内容
====

TypeScript から ESnext に変換して、 Babel で 各環境レベルの ES に変換します。

TypeScript からは型消去をするだけで、ダウンパイルは Babel に任せています。
ただし、 [Babel のモジュール解決は標準に沿っていない](https://docs.google.com/presentation/d/1J9mN3N1KZXGwn7ufrv9UAIiUzKo8zsQaQwIKseebjzo/edit#slide=id.g18cb3b1c5f_0_189)ので、 TypeScript 側で一旦 CommonJS 化を行っています。
このため、npm のモジュールのルートオブジェクトを取得するコードは以下のようになります。

```ts
import * as fs from "fs";
```

以下、各設定の内容です。ちなみに Babel でやることは決まり切っているので .babelrc は提供していません。

```js:tsconfig.json
{
    "compilerOptions": {
        "jsx": "react",           // React 対応
        "lib": [                  // 使用する標準型定義の指定（ target が ES5, ES2015 以外の場合に必要）
            "es2017",
            "dom",
            "dom.iterable",
            "scripthost"
        ],
        "module": "commonjs",     // ES2015 モジュールでなく、 commonjs モジュールに変換
        "noImplicitAny": true,    // any になる箇所で明示を強制
        "noUnusedLocals": true,   // 未使用変数禁止
        "skipLibCheck": true,     // ビルド高速化（型定義内の型チェック無効化）
        "strictNullChecks": true, // null 禁止
        "target": "esnext"        // 残りは Babel さんお願いします。
    },
    "exclude": [
        "node_modules",
        "typings"
    ]
}
```

```webpack.config.js
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const failPlugin = require("webpack-fail-plugin");
const uglifySaveLicense = require("uglify-save-license");

const isProduction = process.env.NODE_ENV === "production";

// クライアント向け / サーバー向け を同時にビルドします。
// デバッグ版 / リリース版 は isProduction フラグで分けます。

// 共通の設定
let common = {
    devtool: isProduction
        ? false
        : "inline-source-map",                      // デバッグ版のみ Source-map を出力
    plugins: isProduction
        ? [failPlugin]                              // リリースビルドのみビルド失敗時にビルドを停止
        : [],
    resolve: { extensions: [".ts", ".tsx", ".js"] }
};

// babel-preset-env に targets を指定した ルールセットを作る補助関数
// babel-preset-env は指定した環境に最大限対応した js を吐く Babel 公式の便利なプリセットです。
function tsModule(targets) {
    return {
        rules: [{
            test: /\.tsx?$/,
            use: [ // 適用する Loader は逆順で記載します。
                {
                    loader: "babel-loader",
                    options: { presets: [["env", { targets }]] }
                },
                {
                    loader: "ts-loader",
                    options: { compilerOptions: { sourceMap: !isProduction } } // デバッグ版のみ Source-map を出力
                }
            ]
        }]
    };
}

module.exports = [
    Object.assign({},
        common,
        {
            entry: {
                index: ["babel-polyfill", "./src/public/js/index.ts"] // クライアント側エントリーポイント
            },
            module: tsModule({ browsers: ["last 2 versions"] }),      // 各主要ブラウザーの直近2バージョンに対応できる js を出力
            output: {
                filename: "lib/public/js/[name].js"                   // 出力先
            },
            plugins: common.plugins.concat([
                new CopyWebpackPlugin(
                    [{ from: "src/public/", to: "lib/public/" }], 
// html 等のコード以外のファイルをコピー
                    {
                        ignore: [
                            "test/",
                            "*.ts",
                            "*.tsx"
                        ]
                    })
            ])
                .concat(isProduction
                    ? [
                        new webpack.optimize.UglifyJsPlugin({
                            output: { comments: uglifySaveLicense }   // リリースビルドのみ uglify する
                        })
                    ]
                    : []),
            target: "web"
        }
    ),
    Object.assign({},
        common,
        {
            entry: {
                index: ["babel-polyfill", "./src/index.ts"],          // サーバー側エントリーポイント
                "test/test": ["babel-polyfill", "./src/test/test.ts"] // テストエントリーポイント
            },
            externals: /^(?!\.)/,
            module: tsModule({ node: 6 }),                            // Node.js 6.x 以降に対応できる js を出力
            output: {
                filename: "lib/[name].js",                            // 出力先
                libraryTarget: "commonjs2"                            // 初期設定 (umd) では babel-polyfill をうまく展開できないので、 commonjs2 形式を明示
            },
            target: "node"
        }
    )
];
```

Grunt → Gulp → WebPack と渡り歩いてきましたが、どんどん便利になりつつ記述はシンプルになっていきますね。
まだまた WebPack の力を 100% 引き出せてはいないので、指摘・改善案等々あればよろしくお願いします🙇

来年はどんなビルドツールが流行るんでしょうか！？

**Merry WebPack 2 & Happy New Typing!**