Titanium + Alloy + TypeScript で、iOS & Android アプリを作る

# 願望

[Titanium](http://www.appcelerator.com/platform/titanium-platform/)(Alloy)を使って、iOSとAndroid両方のアプリを同時に作成したい。ついでに、TypeScriptを使いたい！

というわけで、いろいろやってみました。

# 準備

TypeScriptはインストールしておいてください。

```bash:
$ npm install -g typescript
```

Macの場合は、npmの前にsudoが必要かもしれません。

# alloy.jmk

参考：
http://k0suke.be/post/36585786781/alloy-with-coffeescript

TitaniumでAlloyプロジェクトを使っているときには、alloy.jmkというファイルを書いてやると、コンパイル前とコンパイル後にしたい処理を記述できるようです。

```javascript:alloy.jmk
task("pre:compile", function(event,logger) {
    // コンパイル前処理
});

task("post:compile",function(event,logger){
    // コンパイル後処理
});
```

というふうに書けば、あとはTitaniumが自動で処理してくれます。

# alloy.jmkでTypeScriptをコンパイル出来るように設定する

[TypeScriptのソースを読む](http://qiita.com/JunSuzukiJapan@github/items/708bc21fca88f6ee6f8b)をいう記事の「まとめ」のところで書いたcompile関数をちょっと変更して、alloy.jmkファイルから呼び出すようにうまく書いてやればtypescriptで記述されたファイルを自動でjavascriptファイルに変換してビルド出来るでしょう。

Titaniumuで、"File" -> "New" -> "File"で、プロジェクトのルートに、"alloy.jmk"というファイルを作成します。
![スクリーンショット 0025-07-23 12.57.37.png](https://qiita-image-store.s3.amazonaws.com/0/20722/ccedc673-3a27-3e52-31bd-7c781e76c457.png)

その中の記述は以下を参考に。

>gistにもアップしました。
>[https://gist.github.com/JunSuzukiJapan/6062233](https://gist.github.com/JunSuzukiJapan/6062233)

```javascript:alloy.jmk
function compile(TypeScript, logger, sourcefilename){
	var _this = this;

	var ByteOrderMark = {};
    ByteOrderMark[ByteOrderMark["None"] = 0] = "None";
    ByteOrderMark[ByteOrderMark["Utf8"] = 1] = "Utf8";
    ByteOrderMark[ByteOrderMark["Utf16BigEndian"] = 2] = "Utf16BigEndian";
    ByteOrderMark[ByteOrderMark["Utf16LittleEndian"] = 3] = "Utf16LittleEndian";
	
	var FileInformation = (function () {
	    function FileInformation(contents, byteOrderMark) {
	        this._contents = contents;
	        this._byteOrderMark = byteOrderMark;
	    }
	    FileInformation.prototype.contents = function () {
	        return this._contents;
	    };
	
	    FileInformation.prototype.byteOrderMark = function () {
	        return this._byteOrderMark;
	    };
	    return FileInformation;
	})();
	
	var Environment = (function () {
	    function getNodeEnvironment() {
	        var _fs = require('fs');
	        var _path = require('path');
	        var _module = require('module');
	
	        return {
	            currentDirectory: function () {
	                return (process).cwd();
	            },
	            readFile: function (file) {
	                var buffer = _fs.readFileSync(file);
	                switch (buffer[0]) {
	                    case 0xFE:
	                        if (buffer[1] === 0xFF) {
	                            var i = 0;
	                            while ((i + 1) < buffer.length) {
	                                var temp = buffer[i];
	                                buffer[i] = buffer[i + 1];
	                                buffer[i + 1] = temp;
	                                i += 2;
	                            }
	                            return new FileInformation(buffer.toString("ucs2", 2), ByteOrderMark.Utf16BigEndian);
	                        }
	                        break;
	                    case 0xFF:
	                        if (buffer[1] === 0xFE) {
	                            return new FileInformation(buffer.toString("ucs2", 2), ByteOrderMark.Utf16LittleEndian);
	                        }
	                        break;
	                    case 0xEF:
	                        if (buffer[1] === 0xBB) {
	                            return new FileInformation(buffer.toString("utf8", 3), ByteOrderMark.Utf8);
	                        }
	                }
	
	                return new FileInformation(buffer.toString("utf8", 0), ByteOrderMark.None);
	            },
	            writeFile: function (path, contents, writeByteOrderMark) {
	                function mkdirRecursiveSync(path) {
	                    var stats = _fs.statSync(path);
	                    if (stats.isFile()) {
	                        throw "\"" + path + "\" exists but isn't a directory.";
	                    } else if (stats.isDirectory()) {
	                        return;
	                    } else {
	                        mkdirRecursiveSync(_path.dirname(path));
	                        _fs.mkdirSync(path, 0775);
	                    }
	                }
	                mkdirRecursiveSync(_path.dirname(path));
	
	                if (writeByteOrderMark) {
	                    contents = '\uFEFF' + contents;
	                }
	                _fs.writeFileSync(path, contents, "utf8");
	            },
	            fileExists: function (path) {
	                return _fs.existsSync(path);
	            },
	            deleteFile: function (path) {
	                try  {
	                    _fs.unlinkSync(path);
	                } catch (e) {
	                }
	            },
	            directoryExists: function (path) {
	                return _fs.existsSync(path) && _fs.statSync(path).isDirectory();
	            },
	            listFiles: function dir(path, spec, options) {
	                options = options || {};
	
	                function filesInFolder(folder) {
	                    var paths = [];
	
	                    var files = _fs.readdirSync(folder);
	                    for (var i = 0; i < files.length; i++) {
	                        var stat = _fs.statSync(folder + "\\" + files[i]);
	                        if (options.recursive && stat.isDirectory()) {
	                            paths = paths.concat(filesInFolder(folder + "\\" + files[i]));
	                        } else if (stat.isFile() && (!spec || files[i].match(spec))) {
	                            paths.push(folder + "\\" + files[i]);
	                        }
	                    }
	
	                    return paths;
	                }
	
	                return filesInFolder(path);
	            },
	            arguments: process.argv.slice(2),
	            standardOut: {
	                Write: function (str) {
	                    process.stdout.write(str);
	                },
	                WriteLine: function (str) {
	                    process.stdout.write(str + '\n');
	                },
	                Close: function () {
	                }
	            }
	        };
	    }
	    ;
	
        return getNodeEnvironment();
	})();
	
	var IOUtils;
	(function (IOUtils) {
	    function createDirectoryStructure(ioHost, dirName) {
	        if (ioHost.directoryExists(dirName)) {
	            return;
	        }
	
	        var parentDirectory = ioHost.dirName(dirName);
	        if (parentDirectory != "") {
	            createDirectoryStructure(ioHost, parentDirectory);
	        }
	        ioHost.createDirectory(dirName);
	    }
	
	    function writeFileAndFolderStructure(ioHost, fileName, contents, writeByteOrderMark) {
	        var path = ioHost.resolvePath(fileName);
	        var dirName = ioHost.dirName(path);
	        createDirectoryStructure(ioHost, dirName);
	        return ioHost.writeFile(path, contents, writeByteOrderMark);
	    }
	    IOUtils.writeFileAndFolderStructure = writeFileAndFolderStructure;
	
	    function throwIOError(message, error) {
	        var errorMessage = message;
	        if (error && error.message) {
	            errorMessage += (" " + error.message);
	        }
	        throw new Error(errorMessage);
	    }
	    IOUtils.throwIOError = throwIOError;
	
	    var BufferedTextWriter = (function () {
	        function BufferedTextWriter(writer, capacity) {
	            if (typeof capacity === "undefined") { capacity = 1024; }
	            this.writer = writer;
	            this.capacity = capacity;
	            this.buffer = "";
	        }
	        BufferedTextWriter.prototype.Write = function (str) {
	            this.buffer += str;
	            if (this.buffer.length >= this.capacity) {
	                this.writer.Write(this.buffer);
	                this.buffer = "";
	            }
	        };
	        BufferedTextWriter.prototype.WriteLine = function (str) {
	            this.Write(str + '\r\n');
	        };
	        BufferedTextWriter.prototype.Close = function () {
	            this.writer.Write(this.buffer);
	            this.writer.Close();
	            this.buffer = null;
	        };
	        return BufferedTextWriter;
	    })();
	    IOUtils.BufferedTextWriter = BufferedTextWriter;
	})(IOUtils || (IOUtils = {}));

	var IO = (function () {
	    function getNodeIO() {
	        var _fs = require('fs');
	        var _path = require('path');
	        var _module = require('module');
	
	        return {
	            readFile: function (file) {
	                return Environment.readFile(file);
	            },
	            writeFile: function (path, contents, writeByteOrderMark) {
	                Environment.writeFile(path, contents, writeByteOrderMark);
	            },
	            deleteFile: function (path) {
	                try  {
	                    _fs.unlinkSync(path);
	                } catch (e) {
	                    IOUtils.throwIOError("Couldn't delete file '" + path + "'.", e);
	                }
	            },
	            fileExists: function (path) {
	                return _fs.existsSync(path);
	            },
	            dir: function dir(path, spec, options) {
	                options = options || {};
	
	                function filesInFolder(folder) {
	                    var paths = [];
	
	                    try  {
	                        var files = _fs.readdirSync(folder);
	                        for (var i = 0; i < files.length; i++) {
	                            var stat = _fs.statSync(folder + "/" + files[i]);
	                            if (options.recursive && stat.isDirectory()) {
	                                paths = paths.concat(filesInFolder(folder + "/" + files[i]));
	                            } else if (stat.isFile() && (!spec || files[i].match(spec))) {
	                                paths.push(folder + "/" + files[i]);
	                            }
	                        }
	                    } catch (err) {
	                    }
	
	                    return paths;
	                }
	
	                return filesInFolder(path);
	            },
	            createDirectory: function (path) {
	                try  {
	                    if (!this.directoryExists(path)) {
	                        _fs.mkdirSync(path);
	                    }
	                } catch (e) {
	                    IOUtils.throwIOError("Couldn't create directory '" + path + "'.", e);
	                }
	            },
	            directoryExists: function (path) {
	                return _fs.existsSync(path) && _fs.statSync(path).isDirectory();
	            },
	            resolvePath: function (path) {
	                return _path.resolve(path);
	            },
	            dirName: function (path) {
	                return _path.dirname(path);
	            },
	            findFile: function (rootPath, partialFilePath) {
	                var path = rootPath + "/" + partialFilePath;
	
	                while (true) {
	                    if (_fs.existsSync(path)) {
	                        return { fileInformation: this.readFile(path), path: path };
	                    } else {
	                        var parentPath = _path.resolve(rootPath, "..");
	
	                        if (rootPath === parentPath) {
	                            return null;
	                        } else {
	                            rootPath = parentPath;
	                            path = _path.resolve(rootPath, partialFilePath);
	                        }
	                    }
	                }
	            },
	            print: function (str) {
	                process.stdout.write(str);
	            },
	            printLine: function (str) {
	                process.stdout.write(str + '\n');
	            },
	            arguments: process.argv.slice(2),
	            stderr: {
	                Write: function (str) {
	                    process.stderr.write(str);
	                },
	                WriteLine: function (str) {
	                    process.stderr.write(str + '\n');
	                },
	                Close: function () {
	                }
	            },
	            stdout: {
	                Write: function (str) {
	                    process.stdout.write(str);
	                },
	                WriteLine: function (str) {
	                    process.stdout.write(str + '\n');
	                },
	                Close: function () {
	                }
	            },
	            watchFile: function (fileName, callback) {
	                var firstRun = true;
	                var processingChange = false;
	
	                var fileChanged = function (curr, prev) {
	                    if (!firstRun) {
	                        if (curr.mtime < prev.mtime) {
	                            return;
	                        }
	
	                        _fs.unwatchFile(fileName, fileChanged);
	                        if (!processingChange) {
	                            processingChange = true;
	                            callback(fileName);
	                            setTimeout(function () {
	                                processingChange = false;
	                            }, 100);
	                        }
	                    }
	                    firstRun = false;
	                    _fs.watchFile(fileName, { persistent: true, interval: 500 }, fileChanged);
	                };
	
	                fileChanged();
	                return {
	                    fileName: fileName,
	                    close: function () {
	                        _fs.unwatchFile(fileName, fileChanged);
	                    }
	                };
	            },
	            run: function (source, fileName) {
	                require.main.fileName = fileName;
	                require.main.paths = _module._nodeModulePaths(_path.dirname(_fs.realpathSync(fileName)));
	                require.main._compile(source, fileName);
	            },
	            getExecutingFilePath: function () {
	                return process.mainModule.filename;
	            },
	            quit: process.exit
	        };
	    }
	    ;
	
	    return getNodeIO();
	})();

	var ErrorReporter = (function() {
		function ErrorReporter(ioHost, compilationEnvironment) {
			this.ioHost = ioHost;
			this.hasErrors = false;
			this.setCompilationEnvironment(compilationEnvironment);
		}

		ErrorReporter.prototype.addDiagnostic = function(diagnostic) {
			this.hasErrors = true;

			if (diagnostic.fileName()) {
				var soruceUnit = this.compilationEnvironment.getSourceUnit(diagnostic.fileName());
				if (!soruceUnit) {
					soruceUnit = new TypeScript.SourceUnit(diagnostic.fileName(), this.ioHost.readFile(diagnostic.fileName()));
				}
				var lineMap = new TypeScript.LineMap(soruceUnit.getLineStartPositions(), soruceUnit.getLength());
				var lineCol = {
					line : -1,
					character : -1
				};
				lineMap.fillLineAndCharacterFromPosition(diagnostic.start(), lineCol);

				this.ioHost.stderr.Write(diagnostic.fileName() + "(" + (lineCol.line + 1) + "," + (lineCol.character + 1) + "): ");
			}

			this.ioHost.stderr.WriteLine(diagnostic.message());
		};

		ErrorReporter.prototype.setCompilationEnvironment = function(compilationEnvironment) {
			this.compilationEnvironment = compilationEnvironment;
		};

		ErrorReporter.prototype.reset = function() {
			this.hasErrors = false;
		};
		return ErrorReporter;
	})(); 

	this.ioHost = IO;
    this.hasResolveErrors = false;
    this.compilerVersion = "0.9.0.1";
    this.printedVersion = false;
    this.errorReporter = null;
	this.compilationSettings = new TypeScript.CompilationSettings();
	this.compilationEnvironment = new TypeScript.CompilationEnvironment(compilationSettings, ioHost);
    this.resolvedEnvironment = this.compilationEnvironment;
    this.errorReporter = new ErrorReporter(this.ioHost, this.compilationEnvironment);

	var compiler = new TypeScript.TypeScriptCompiler(logger, this.compilationSettings, null);

	//
	// ソースファイルの設定
	//
	var fileInformation = ioHost.readFile(sourcefilename);
	var code = new TypeScript.SourceUnit(sourcefilename, fileInformation);

	compiler.addSourceUnit(
	    code.path,
    	TypeScript.ScriptSnapshot.fromString(code.fileInformation.contents()),
	    code.fileInformation.byteOrderMark(),
    	0,
	    false,
	    code.referencedFiles);

	//
	// シンタックスエラーのチェック？
	//
	var syntacticDiagnostics = compiler.getSyntacticDiagnostics(code.path);
	compiler.reportDiagnostics(syntacticDiagnostics, this.errorReporter);

	var anySyntacticErrors = false;
	var anySemanticErrors = false;


	if (syntacticDiagnostics.length > 0) {
	    anySyntacticErrors = true;  // シンタックスエラー有り
	}

	if (anySyntacticErrors) {
	    return true; // エラーで終了
	}

	//
	// 型チェック
	//
	compiler.pullTypeCheck();

	//
	// コンパイル結果の書出し
	//

	// ファイル書出し用オブジェクト
	var emitterIOHost = {
    	writeFile: function (fileName, contents, writeByteOrderMark) {
        	return IOUtils.writeFileAndFolderStructure(_this.ioHost, fileName, contents, writeByteOrderMark);
	    },
    	directoryExists: this.ioHost.directoryExists,
	    fileExists: this.ioHost.fileExists,
    	resolvePath: this.ioHost.resolvePath
	};

	// よくわからない何か（ソースファイル名と出力ファイル名がどうしたこうした？）
	var mapInputToOutput = function (inputFile, outputFile) {
    	_this.resolvedEnvironment.inputFileNameToOutputFileName.addOrUpdate(inputFile, outputFile);
	};

	// 発行（ファイルに書出し？）
	var emitDiagnostics = compiler.emitAll(emitterIOHost, mapInputToOutput);
	compiler.reportDiagnostics(emitDiagnostics, this.errorReporter);
	if (emitDiagnostics.length > 0) {
    	return true; // エラーで終了
	}

	if (anySemanticErrors) {
    	return true; // エラーで終了
	}

	// 発行（ファイルに書出し？）　その２
	var emitDeclarationsDiagnostics = compiler.emitAllDeclarations();
	compiler.reportDiagnostics(emitDeclarationsDiagnostics, this.errorReporter);
	if (emitDeclarationsDiagnostics.length > 0) {
    	return true; // エラーで終了
	}

	return false; // 無事コンパイル完了
}

// コンパイル前処理
task("pre:compile", function(event,logger) {
	var wrench = require("wrench"),
		fs = require("fs"),
		path = require("path");
 
	var code = [
		fs.readFileSync("/usr/local/lib/node_modules/typescript/bin/typescript.js"), // <-各自の環境に合わせて設定する。
		"module.exports = TypeScript;"
	].join("");
	fs.writeFileSync(process.env.TMPDIR + "typescript.js", code);
	var TypeScript = require(process.env.TMPDIR + "typescript.js");
	fs.unlinkSync(process.env.TMPDIR + "typescript.js");
 
	logger.information = logger.info;
	logger.warning = logger.warn;
	logger.fatal = logger.info;
	logger.log = logger.info;

	event.alloyConfig.tsc = [];
 
	wrench.readdirSyncRecursive(event.dir.home).forEach(function(target){
		if (target.match(/\.ts$/) && ! target.match(/\.d\.ts$/)) { // .d.tsファイルはコンパイルしない。
			var filename = path.join(event.dir.home + "/" + target);
			compile(TypeScript, logger, filename);
      		event.alloyConfig.tsc.push(target.replace(/.ts$/, ".js"));
		}
	});

 });

// コンパイル後処理
task("post:compile",function(event,logger){
  var fs = require("fs");

  event.alloyConfig.tsc.forEach(function(target){
    fs.unlinkSync(event.dir.home + "/" + target);
  });
});
```

あとは、TypeScriptでcontrollerなりmodelなりを書いて、Titaniumの"Run"コマンド、あるいは"Debug"コマンドを実行すればオッケーです。

TypeScriptのコンパイル時にエラーが表示されることがありますが、エラーがあってもJavaScriptに変換されていれば、そのまま処理が進みます。

>wrenchまわりでひっかかる場合は、ターミナルで
>
> npm install wrench
>
>してやる必要があるかも…。

### 追記
2013/07/29
　・*.d.tsファイルはコンパイルしないようにした(node.d.tsなどをコンパイルしたくないから）。
　・意味解析部分を削除（エラーになることがあるので…）。