gulp-typescript-angularでクラス名のネーミングルールでAngularJSへ自動登録できるようにしたが不要になりそうだ。TypeScript1.5でデコレーターが導入されて、@Quramy氏のTypeScriptのDecoratorメモのストックにあるように、@vvakame氏のgistのコードを使うと、DecoratorでAngularJSへの登録はできる。ただし、Decoratorでは、コンストラクタのパラメーター名がとれないので、勉強がてら$inject Property Annotationを自動的に付与する機能をgulp-typescript-angularに追加してみた。
DecoratorのJavaScriptの変換後の形
Decorator が付与された TypeScriptのクラスをコンパイルすると
module sample {
@sample.Service
class SampleService {
constructor(public $q: angular.IQService) {
}
}
}
__decorate
メソッドが追加され、クラス宣言の最後でDecoratorを呼び出すコードが追加される。
なので、SampleController = __decorate([sample.Controller], SampleController);
の部分を抽出すれば、クラスに付与されているDecoratorが取れる。
var __decorate = this.__decorate || function (decorators, target, key, value) {
var kind = typeof (arguments.length == 2 ? value = target : value);
for (var i = decorators.length - 1; i >= 0; --i) {
var decorator = decorators[i];
switch (kind) {
case "function": value = decorator(value) || value; break;
case "number": decorator(target, key, value); break;
case "undefined": decorator(target, key); break;
case "object": value = decorator(target, key, value) || value; break;
}
}
return value;
};
var sample;
(function (sample) {
var SampleController = (function () {
function SampleController($scope) {
this.$scope = $scope;
}
SampleController = __decorate([sample.Controller], SampleController);
return SampleController;
})();
})(sample || (sample = {}));
これをfalafelを使ってJavaScriptのASTを解析していくことで、クラスの書き換えます。
1. JavaScriptのASTからクラスを抽出する
TypeScriptから生成されるJavaScriptでは、クラスは下記のようになる。
var SampleClass = (function(){/*クラスの定義*/ return SampleClass})();
このパターンに対応するASTを絞り込む。
falafelを使うとASTのnodeごとに判定することができるので、
それを使って、書きのコードでClass宣言を絞り込む。
if (node.type === 'VariableDeclaration' &&
(decls = node.declarations) && decls.length === 1 &&
(decl = decls[0]) && decl.init && decl.init.type === 'CallExpression' )
if(!decl.init.callee.body){
return;
}
if文を分解していくと、最初の判定は変数宣言であること "var xxx,yyy...."
if (node.type === 'VariableDeclaration' &&
次の条件は、変数宣言の数が一つであること "var xxx"
(decls = node.declarations) && decls.length === 1 &&
次の条件が、初期化処理が関数呼び出しであること "var xxx = func()"
(decl = decls[0]) && decl.init && decl.init.type === 'CallExpression' )
最後の条件が、初期化関数が無名関数であること "var xxx = (function(){})()"
if(!decl.init.callee.body){
return;
}
TypeScriptでコーディングする場合には、 "var xxx = (function(){})()"のようなコードを書くことはないので、個人的にはTypeScriptのクラスを絞り込むのに十分だと思いますが、網羅的には検証できていないので、バグったら条件を厳しくしていく必要があります。
2. クラスの宣言から Decorator を取り出す
Decoratorのロジックはreturn文の直前に追加される。
SampleDirective = __decorate([sample.Directive], SampleDirective);
return SampleDirective;
decl.init.callee.body.body
にクラス宣言の命令がListで入っているので、最後から2番目を取るとdecoratorの命令が取れる。
var body = decl.init.callee.body;
var decoratorBlock = body.body[body.body.length-2];
decoratorBlockがSampleDirective = __decorate([sample.Directive], SampleDirective);
に対応する。
なので、decoratorBlockの右辺のメソッドの第一引数をとれば良い。
var decorate = decoratorBlock.expression.right;
var decorators = decorate[0].elements.map(function(element){
return element.source();
})
これでdecoratorsに["sample.Directive"]がはいる。
3. constructorの後にコードを追加する
Decoratorは、クラス宣言の最後に呼び出されるので、クラス宣言の後に$inject Propery Annotationを追加しても遅い。コンストラクタの後に追加する必要がある。
コンストラクタは、クラス宣言の最初の文なので、
var constructor = decl.init.callee.body.body[0];
で取得できる。
このコンストラクタに対して、updateメソッドを使って、コードを追加する。
var source = '/*<generated_code>*/';
...
constructor.update(constructor.source() + source);
下記のように、SampleControllerのコンストラクタの直後にコードを追加することができる。
var sample;
(function (sample) {
var SampleController = (function () {
function SampleController($scope) {
this.$scope = $scope;
}/*<auto_generate>*/SampleController.$inject = ['$scope'];SampleController.$componentName = 'SampleController'/*</auto_generate>*/
SampleController = __decorate([sample.Controller], SampleController);
return SampleController;
})();
})(sample || (sample = {}));
まとめ
falafelのASTはconsole.logで出力すると、ASTの構造が分かりやすく出力されるので、console.logでデバッグしながら開発していくと、ASTの解析は簡単に実装できました。