TypeScriptのビルドからテスト・カバレッジレポート, そしてgulpでconnect+watch

## 概要
### はじめに
jQueryを使ってセコセコとクライアントサイドのJavaScriptを書いていた時代からすごい変化しています。浦島太郎状態から脱却するために、最近の動向に沿った開発ができるように環境構築してみました。
TypeScriptでコードを書いてユニットテスト・カバレッジレポート作成を行いつつ開発を進められるようにします。
まずは、各ステップの操作を手動で確認したあと、gulpで自動的に行えるようにします。

### 目標
1. コード本体は[TypeScript](https://www.typescriptlang.org)で書く,ユニットテストも書く
2. TypeScriptをECMAScript5のCommonJSにコンパイル
3. 生成されたJavaScriptをユニットテストする、カバレッジも出す。
4. webpackでまとめる。
5. ファイルを更新したら自動ビルドする。

以上の操作をgulpで一括して行いたいと思います。

より多くのブラウザを対象にするためにECMAScript5(IE9以上対応)を使用します。

## やってみた
### 前提
- npmは使える状態のであるとします。npmでTypeScriptのコンパイラをインストールします。
- グローバルでインストールしていない場合、node_modules/.bin以下に存在します。明記はしませんがコマンドのパスに注意してください。

### TypeScriptからの手動操作

#### gitリポジトリの初期化
```bash
$ git init ts-smaple
$ cd ts-sample
```

#### 初期化
npmや各コマンドの初期化処理を実行します。

```bash
$ npm init
$ npm install typescript typings --save-dev
$ tsc --init
$ typings init
$ typings install
```

ディレクトリ構成は最終的に次のようにします。

```bash:ディレクトリ構成
ts-sample/
├── README.md
├── gulpfile.js
├── htdocs
│   └── index.html
├── node_modules
│   ├── @types
│   ├── 
│
├── package.json
├── src
│   ├── Person.ts
│   └── main.ts
├── test
│   └── PersonTest.ts
├── tsconfig.json
└── typings.json
```

#### tscofing.jsonの記述
TyepScriptのコンパイルオプションであるtsconfig.jsonを記述します。

```json:tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": true,
        "sourceMap": true
    },
    "files":[
        "typings/index.d.ts",
        "src/main.ts"
    ],
    "exclude": [
        "node_modules/"
    ]    
}
```

CommonJS形式のECMAScript5を出力するように設定しています。


tsconfig.jsonは公式サイトの以下を参考にします。

- [TypeScript tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)
- [TypeScript Compiler Options](https://www.typescriptlang.org/docs/handbook/compiler-options.html)

コンパイラオプションは日本語翻訳された方いらっしゃいました。

- [TypeScriptのコンパイラのオプション一覧](http://qiita.com/IganinTea/items/3056f1b5d1c96aa733d4)


#### かんたんなTypeScriptを記述

今回はサンプルとして簡単なTypeScriptを書きました。

ありがちな内容ですが、Personクラスを定義し、main.tsから呼び出して、jQueryを介してbodyに書き込みます。

```typescript:src/Person.ts
export class Person {    
    readonly id: number;
    readonly name: string;    

    constructor(_id: number, _name: string) {
        this.id = _id;
        this.name = _name;        
    }
}
```

```typescript:src/main.ts
import {Person} from './Person';
import * as $ from 'jquery';

var person: Person = new Person(1, "yoko");
$(() => {
    $('body').html('彼は' + person.name + 'です。');
});

```

さてこのコードをビルドするためにjQueryが必要なのでインストールします。
jQuery本体とjQueryの型定義を指定します。（TypeScriptのコンパイル自体は@types/jqueryだけでOKですが。）

```bash
$ npm install jquery @types/jquery --save-dev
```


この段階でTypeScriptからJavaScriptへのコンパイルであればできるかと思います。

```
$ tsc
```

#### ユニットテストの記述
先ほどのPersonクラスのテストを書きます。
今回はテストフレームワークとしてmochaを使います。

まずは依存させるパッケージをインストールします。

```bash
$ npm install mocha @types/mocha --save-dev
$ npm install power-assert @types/power-assert --save-dev
```


次のテストコードを書きます。コンストラクタのアサーションテストです。

```typescript:test/Person.ts
import * as assert from 'power-assert';
import {Person} from '../src/Person';

describe('PersonTest', () => {
    it('コンストラクタのテスト', () => {
        const person:Person = new Person(10, 'hiroyky');
        assert.equal(person.id, 10);
        assert.equal(person.name, 'hiroyky');
    });
});
```

これをJavaScriptにコンパイルして、mochaでユニットテストを実行します。最初なので手動でやります。

```bash
$ tsc
$ mocha test/*.js


  PersonTest
    ✓ コンストラクタのテスト


  1 passing (8ms)
```

さて、``tsc``でコンパイルしてからmochaを実行しました。
これを次のようにすることで同時に実行します。また、jsファイルは生成されません。
後で登場するwebpackで１つのjsファイルのみを生成物とするため、途中のjsは邪魔なので生成しないようにします。

```bash
$ npm install ts-node --save-dev
$ mocha --compilers ts:ts-node/register test/**/*.ts
```

また、package.jsonのtestフィールドにテストコマンドを登録しておくことで``npm test``とするだけでテストが実行されます。

```json
"scripts": { 
    "test": "mocha --compilers ts:ts-node/register test/**/*.ts"
 },
```


#### カバレッジレポートの作成
せっかくなのでユニットテストのカバレッジレポートを生成してみましょう。

カバレッジレポートの作成に[istanbul](https://github.com/gotwarlost/istanbul)を使います。早速インストール。

```bash
$ npm install istanbul --save-dev
```

次のコマンドでテストとカバレッジレポート作成を行います。

```bash
$ istanbul cover _mocha
```
jsファイルに対してテストが実施され、coverageディレクトリ以下にレポートが作成されているはずです。

![Screenshot from 2016-12-29 17-23-29.png](https://qiita-image-store.s3.amazonaws.com/0/73358/1b412187-38e3-c25b-11ca-e67aa9c491f9.png)


``istanbul help``や``istanbul help cover``などとしてヘルプを見ることができます。

```bash
$ istanbul help cover                                                                                                                                                                                                                     
Usage: istanbul cover [<options>] <executable-js-file-or-command> [-- <arguments-to-jsfile>]
```
先ほどのコマンドは``instanbul cover``でテストランナに``_mocha``を指定しています。_mochaは``node_modules/.bin/_mocha``にあります。

と言っても、これはTypeScriptをコンパイルしたJavaScriptのカバレッジレポートです。次の手順でTypeScriptのレポートを作成します。

```bash
$ (sudo) npm install remap-istanbul --global
$ cd ./coverage
$ remap-istanbul -i coverage.json -o lcov-report -t html 
```


#### webpackでjsファイルを一つにまとめる
さて、TypeScriptから生成されたJavaScriptは複数個存在し、requireでインポートしています。ご存知の通りWebブラウザ側のJavaScriptでは外部ファイルをインポートする機能はありませんので、[webpack](https://webpack.github.io)を使って、予め１つにまとめたjsファイルを作成し、配布します。
他にAMDというscriptタグを動的に生成するとこによってインポートする方法もあるようですがここでは行いません。(CPUメーカのAMDではありません。)

まずはwebpackをインストールします。

```bash
$ npm install webpack --save-dev
$ (sudo) npm install webpack --global
```


早速コンパイルで生成されたjsを一つにまとめてみましょう。すべてをまとめたjsファイルbundle.jsをhtdocsディレクトリ以下に生成します。
bundle.jsはPerson.jsだけでなくjQueryなど外部のライブラリも含んでいます。

```bash
$ webpack src/main.js htdocs/bundle.js
```

ここで、webpackの設定ファイルを記述することで、単に``webpack``とコマンド実行するだけで良くなります。
ファイル名はwebpack.config.jsです。

```javascript:webpack.config.js
module.exports = {
    entry: "./src/main.js",
    output: {
        path: __dirname/build,
        filename: "bundle.js"
    }
};
```

更に、以下のようにすることでtsファイルを直接１つのjsファイルにまとめることができます。ts-loaderをインストールします。

また、``devtool: 'source-map'``を指定してすることで、デバッグ時にデバッグツールが元のTypeScriptのコードを参照できるようにしています。これが無いと不便なのでデバッグ時はつけておきます。

```javascript:webpack.config.js
var path = require('path');
module.exports = {
    entry: './src/main.ts',
    output: {
        filename: './bundle.js'
    },
    devtool: 'source-map',
    resolve: {
        root:[path.join(__dirname,'node_modules')],
        extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js']
    },
    module: {
        loaders: [
            { test: /\.ts$/, loader: 'ts-loader' }
        ]
    }
};
```

```bash
$ npm install ts-loader --save-dev
$ webpack
```

#### index.htmlを書く
webpackで生成されたjsを読み込むような簡単なhtmlを作成しておきます。
ビルドで生成されるbundle.jsを読み込むようにしています。

```html
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <script src="bundle.js"></script>
 </head>
 <body>
 </body>
</html>
```

### gulpの導入
ここまでは、TypeScriptのコンパイル, ユニットテスト,カバレッジレポート生成, webpackをそれぞれ行ってきました。
ここからは、これらの操作をgulpで一括して実行できるようにしつつ、開発用サーバをgulpで準備していきます。

#### gulpのインストール
まずはgulpをインストールします。これはグローバルでもインストールしておくと良いでしょう。

```bash
$ npm install "git://github.com/gulpjs/gulp.git#4.0" --save-dev
$ (sudo) npm install "git://github.com/gulpjs/gulp.git#4.0" --global
```

#### gulpfile.jsの作成
gulpfile.jsを作成します。gulpfile.jsの書き方は[公式のドキュメント](https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md)に譲るとして、以下のように記述しました。

```javascript:gulpfile.js
const gulp = require('gulp');
const mocha = require('gulp-mocha');
const typescript = require('ts-node/register');
const webpack = require('gulp-webpack');
const webpackConfig = require('./webpack.config.js');

/** TypeScriptのコンパイル, webpackを実行 */
gulp.task('build', (done) => {
    gulp.src(['./src/*.ts'])
        .pipe(webpack(webpackConfig))
        .pipe(gulp.dest('./htdocs'));
    done();
});

/** ユニットテストの実行 */
gulp.task('test', (done) => {
    gulp.src(['./test/**/*.ts'])
        .pipe(mocha({
            'compilers': {
                ts: typescript
            }
        }))
        .once('error', () => {
            process.exit(1);
        })
    done();
});

gulp.task('default', gulp.series(
    'test',
    'build'
));
```

これで``gulp test``、``gulp build``とすればそれぞれユニットテスト, ビルドが実行されます。
また、defualtのtaskにあるように

```bash
$ gulp
```
とすればユニットテストとビルドが一括して行われます。ユニットテストに失敗したらそこで止まります。

#### gulpでwebサーバを作成
静的ファイルのみのページの動作検証を行うためにサーバを用意します。本来Apacheでもnginxでも良いのですが、ここでは[gulp-connect](https://www.npmjs.com/package/gulp-connect)を使ってサクっと開発webサーバを立ててみます。

```bash
$ npm install gulp-connect --save-dev
```

gulpfile.jsに以下のタスクを追記します。

```javascript:gulpfile.js
const connect = require('gulp-connect');

gulp.task('server', (done) => {
    connect.server({
        root: [__dirname + '/htdocs']
    });
    done();
});

gulp.task('default', gulp.series(
    'test',
    'build',
    'server' // 追記
));
```

``gulp server``と実行することでWebサーバが起動します。ルートディレクトリはhtdocsと設定したため先ほど作成したindex.htmlが開かれます。
またdefaultタスクにも``server``を追加したため、単に``gulp``と実行するだけでサーバの起動までが行われるうようになりました。

```bash
$gulp server                                                                             [] Starting 'server'...
[] Finished 'server' after 23 ms
[] Server started http://localhost:8080
```
早速、http://localhost:8080 にブラウザでアクセスして確認してみましょう。


#### ファイルの更新検知とリビルド
tsファイルなどの更新を監視して、更新があったらリビルドするようにします。ctrl+sなどで保存する度にビルドが実行されて便利です。

gulpfile.jsに次のタスクを追記します。

```javascript:gulpfile.js
gulp.task('watch', (done) => {
    gulp.watch(['./src/**/*.ts', './test/**/*.ts'], gulp.series(
        'test',
        'build'
    ));
    done();
});

gulp.task('default', gulp.series(
    'test',
    'build',
    'server',
    'watch'  // 追記
));
```

``gulp``と実行するとサーバが起動しますが、その後tsファイルなどを更新するとビルド処理が自動的に実行されます。

#### gulpでカバレッジレポート

TypeScriptのコードをカバレッジレポートするには、gulpfileを次のように記述します。
tsファイルをjsファイルにsourcemap付きコンパイル、jsファイルでカバレッジレポートを生成したあと、remap-istanbulでTypeScriptのカバレッジレポートに変換する一連の作業を``task('coverage')``で行っています。

- https://github.com/hiroyky/ts-sample/blob/master/gulpfile.js#L32-L75

```javascript
const sourcemaps = require('gulp-sourcemaps');
const istanbul = require('gulp-istanbul');
const remapIstanbul = require('remap-istanbul/lib/gulpRemapIstanbul');

gulp.task('compile', (done) => {
    const options = {
        module: "commonjs",
        target: "es5",
        noImplicitAny: true,
        sourceMap: true
    };

    gulp.src(['./**/*.ts', '!node_modules/**'])
        .pipe(sourcemaps.init())
        .pipe(typescript(options))
        .js.pipe(sourcemaps.write('./'))
        .pipe(gulp.dest('./build/'))
        .on('end', done);    
});

gulp.task('coverage', gulp.series(
    'compile',
    (done) => {
        gulp.src('./build/src/**/*.js')
            .pipe(istanbul())
            .pipe(istanbul.hookRequire());
        done();
    },
    (done) => {
        gulp.src('./build/test/**/*.js')
            .pipe(mocha())
            .pipe(istanbul.writeReports())
            .on('end', done);
    },
    (done) => {
        gulp.src('./coverage/coverage-final.json')
            .pipe(remapIstanbul({
                reports: {
                'json': 'coverage/coverage.json',
                'html': 'coverage/html-report'
                }
            }))
        done();
    }));
```

#### defaultタスクの作成
defaultタスクはユニットテストとビルドを実行したあと、サーバを立ち上げてwatchするようにします。流石に毎回カバレッジレポートを作成する必要はないだろうと思い、coverageタスクは入れてないです。

```javascript
gulp.task('default', gulp.series(
    'test',
    'build',
    'server',
    'watch'
));
```

## まとめ
TypeScriptでコードを書いてビルド・テスト、デバッグするための環境を準備するところを振り返りました。一度構築すれば、今後は``gulp``と実行するだけでＯＫになります。

## リポジトリ
今回作成したコードは

- [Github hiroyky/ts-sample](https://github.com/hiroyky/ts-sample)

にアップロードしました。

## 参考文献
いずれも2016年12月29日現在

- TypeScriptのコンパイル設定
  - [TypeScript tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)
  - [TypeScript Compiler Optoins](https://www.typescriptlang.org/docs/handbook/compiler-options.html)
  - [TypeScriptのコンパイラのオプション一覧](http://qiita.com/IganinTea/items/3056f1b5d1c96aa733d4)
- ユニットテストとカバレッジレポート
  - [mocha](https://mochajs.org)
  - [Github istanbul](https://github.com/gotwarlost/istanbul)
  - [TypeScript でユニットテストする。カバレッジもねっ!!](http://blog.syati.info/post/typescript_coverage/)
  - [Write tests for TypeScript projects with mocha and chai — in TypeScript!](https://journal.artfuldev.com/write-tests-for-typescript-projects-with-mocha-and-chai-in-typescript-86e053bdb2b6)
  - [istanbul + mocha設定メモ](http://yukidarake.hateblo.jp/entry/2015/06/29/202652)
  - [npm module code coverage in 2 steps](http://www.clock.co.uk/blog/npm-module-code-coverage-in-2-simple-steps)
- webpack
  - [webpack cli](https://webpack.github.io/docs/cli.html)
  - [webpack get started](http://webpack.github.io/docs/tutorials/getting-started/)
  - [webpack で始めるイマドキのフロントエンド開発](http://qiita.com/yosisa/items/61cfd3ede598e194813b)
  - [Gulp+WebPackで作るTypeScript開発環境【5分でできる！】](https://liginc.co.jp/201842)
- gulp-server
  - [Github gulp-connect](https://github.com/avevlad/gulp-connect)
- watch
  - [Github gulp-watch](https://github.com/floatdrop/gulp-watch)