Typescriptで書いたAngularJSのControllerをgulpで自動登録する


Typescriptを使うとAngularJSのControllerをクラスとして開発することができる。クラスとしてきれいに開発できるのでTypescriptは気に入っている。

```typescript:navbar.controller.ts
module b2 {

  class NavbarController {
    constructor ($scope: ng.IScope) {
      $scope.date = new Date();
    }
  }

  angular.module('b2')
  .controller('NavbarController',['$scope',NavbarController]);

}
```
## でも、AngularJSへの登録コードは残ってしまう
AngularJSにクラスをコントローラーとして登録するコードを毎回書かなければならない。

```typescript
  angular.module('b2')
  .controller('NavbarController',['$scope',NavbarController]);
```
せっかく、TypeScriptでクラスとして開発しているのに残念だ。

## gulpのスクリプトでAngularJSへの登録コードを自動生成する
gulpでTypescriptをビルドするタイミングで、JavaScritpのASTを解析して、50行くらいのコードで実現できた。gulpはデータの流れが分かりやすいので、こういう処理を追加しやすい。
### 環境セットアップ
` yo gulp-angular` を実行しTypescriptを選び、gulp & AngularJS & Typescriptのプロジェクトを作る。


` npm install --save-dev through2 falafel ` を実行し、JavaScriptnのASTを解析するライブラリーをインストールする。

### gulp スクリプトの開発
コンパイル用のgulpのスクリプトファイルを編集していく。

``` js:gulp/scripts.js
'use strict';

var gulp = require('gulp');
var paths = gulp.paths;

var $ = require('gulp-load-plugins')();

gulp.task('scripts', function () {
  return gulp.src(paths.src + '/{app,components}/**/*.ts')
    .pipe($.typescript())
    .on('error', function handleError(err) {
      console.error(err.toString());
      this.emit('end');
    })
    .pipe(gulp.dest(paths.tmp + '/serve/'))
    .pipe($.size())
});
```

#### npmで取得したライブラリーを読み込む
through2とfalafelを読み込む。

``` js
'use strict';

var gulp = require('gulp');
// 追加したライブラリーを読み込む
var through = require('through2');
var falafel = require('falafel');
var paths = gulp.paths;
```

#### typescriptのコンパイル後に処理を挟み込む
angularifyメソッドをpipeで追加する。

``` js
gulp.task('scripts', function () {
  return gulp.src(paths.src + '/{app,components}/**/*.ts')
    .pipe($.typescript())
    .on('error', function handleError(err) {
      console.error(err.toString());
      this.emit('end');
    })
    .pipe(angularify({  /* 追加 */
      moduleName:'b2'   
    }))                 
    .pipe(gulp.dest(paths.tmp + '/serve/'))
    .pipe($.size())
});
```

#### JavaScriptにコンパイルされた後のファイルごとに処理をする
angularifyメソッドのなかでthroughを使うことでJavaScriptにコンパイルされた後のコードをファイルごとに操作することができる。

``` js
function angularify(opts) {

  var stream = through.obj(function(file, enc, cb) {
    if (file.isNull()) {
      // do nothing
    }

    if (file.isBuffer()) {
      // ファイルごとに中身を見て、変換することができる
      var contents = file.contents.toString();
      contents = transform(contents,opts.moduleName);
      file.contents = new Buffer(contents.toString());
    }

    this.push(file);

    return cb();
  });

  // returning the file stream
  return stream;
};
```

#### JavaScriptを解析し、AngularJSへの登録処理を追加する
falafelを利用しASTに変換されたJavaScriptを解析しソースコードを変換する。

``` js
function transform(contents,moduleName){
   // JavaScriptをASTに変換する
   return falafel(contents, {tolerant: true}, function (node) {
     registerControllerToModule(node,moduleName);
   }).toString();
}

function registerControllerToModule(node,moduleName){
 var decls, decl;
 // Typescriptのクラスは一定のルールでJavaScriptに変換されるので、
 // クラス宣言のパターンにマッチングして、コントローラーのクラス宣言に
 // ひっかける
 if (node.type === 'VariableDeclaration' &&
     (decls = node.declarations) && decls.length === 1 &&
     (decl = decls[0]) && decl.id.name.match(/.*Controller/)) {
     if(decl.init.type === 'CallExpression'){
       // コンストラクタのFunction
       var constructor = decl.init.callee.body.body[0];
       // コンストラクタの引数名
       var constructorParams = constructor.params.map(function(param){
         return '\''+param.name + '\'';
       });
　　　　// コントローラー名
       var controllerName = decl.id.name;
       // コンストラクタの引数の後にFunctionを追加する
       constructorParams.push(controllerName);
		
       // AngularJSへの登録コードを組み立てる
       var source = '\n    ';
       source += 'angular.module(\''+ moduleName +'\')';
       source += '.controller(\''+controllerName+'\',['+constructorParams.join('\,')+']);';

       // クラス宣言の後にAngularJSへの登録コードを追加する
       node.update(node.source()+source);
     }
   return true;
 }
 return false;
}
```

### gulp スクリプトの動作確認

AngularJSへの登録コードを削除したTypeScriptのクラスを用意する。

``` typescript
'use strict';

module b2 {

  interface INavbarScope extends ng.IScope {
    date: Date
  }

  class NavbarController {
    constructor ($scope: INavbarScope) {
      $scope.date = new Date();
    }
  }

}

```

gulp の実行後に生成されるファイルには、AngularJSへの登録コードが追加されている。

``` javascript
'use strict';
var b2;
(function (b2) {
    var NavbarController = (function () {
        function NavbarController($scope) {
            $scope.date = new Date();
        }
        return NavbarController;
    })();
    angular.module('b2').controller('NavbarController',['$scope',NavbarController]);
})(b2 || (b2 = {}));
```

## 作った gulp スクリプト

``` js:gulp/scripts.js
'use strict';

var gulp = require('gulp');
var through = require('through2');
var falafel = require('falafel');
var paths = gulp.paths;

var $ = require('gulp-load-plugins')();

gulp.task('scripts', function () {
  return gulp.src(paths.src + '/{app,components}/**/*.ts')
    .pipe($.typescript())
    .on('error', function handleError(err) {
      console.error(err.toString());
      this.emit('end');
    })
    .pipe(angularify({
      moduleName:'b2'
    }))
    .pipe(gulp.dest(paths.tmp + '/serve/'))
    .pipe($.size())
});

function angularify(opts) {

  // creating a stream through which each file will pass
  var stream = through.obj(function(file, enc, cb) {
    if (file.isNull()) {
      // do nothing
    }

    if (file.isBuffer()) {
      var contents = file.contents.toString();
      contents = transform(contents,opts.moduleName);
      // console.log(contents);
      file.contents = new Buffer(contents.toString());
    }

    this.push(file);

    return cb();
  });

  // returning the file stream
  return stream;
};

function transform(contents,moduleName){
   return falafel(contents, {tolerant: true}, function (node) {
     registerControllerToModule(node,moduleName);
   }).toString();
}

function registerControllerToModule(node,moduleName){
 var decls, decl;
 if (node.type === 'VariableDeclaration' &&
     (decls = node.declarations) && decls.length === 1 &&
     (decl = decls[0]) && decl.id.name.match(/.*Controller/)) {
     if(decl.init.type === 'CallExpression'){
       var constructor = decl.init.callee.body.body[0];
       var constructorParams = constructor.params.map(function(param){
         return '\''+param.name + '\'';
       });
       var controllerName = decl.id.name;
       constructorParams.push(controllerName);
       var source = '\n    ';
       source += 'angular.module(\''+ moduleName +'\')';
       source += '.controller(\''+controllerName+'\',['+constructorParams.join('\,')+']);';
       node.update(node.source()+source);
     }
   return true;
 }
 return false;
}
```