TypeScriptを魔改造してjspmのモジュールをimport出来るようにする

先日、[TypeScript + jspm 触って消耗した話](http://qiita.com/Quramy/items/33cf5991ad8a9b385024)にangular2をjspm + TypeScriptの環境でコンパイルしようとして上手くいかなかった話を書きました。

コンパイルが上手くいかない原因は:

* angular2はDefinitelyTyped等に.d.tsを提供しておらず、NPMに直接.d.tsを配置している
* TypeScriptには `"moduleResolution": "node"` で`node_modules` 配下のモジュール(d.ts)を読み込む仕組みがあるが、`jspm_packages` からモジュールを読み込む仕組みがない([issue#6012](https://github.com/Microsoft/TypeScript/issues/6012))。

なのですが、おめおめと引き下がるのも癪ですし、年末休暇に突入して暇ということもあり、TypeScriptを魔改造してみました。
改造内容は、上述のissue#6012で言われていた「jspmでinstallしたモジュール内のd.tsにreferenceを通す」です。

作成したブツは [Quramy/TypeScript#jspm-module-resolution](https://github.com/Quramy/TypeScript/tree/jspm-module-resolution)にpushしてあります。

## 改造内容

`npm install Quramy/TypeScript#jspm-module-resolution` で動作の確認が出来ます。

件のangular2であれば、

```sh
jspm install angular2
```

としたあとに、以下のようにtsconfig.jsonに`moduleResolution: jspm` を設定すれば、

```json:tsconfig.json
{
    "compilerOptions": {
        "module": "system",
        "target": "es5",
        "noImplicitAny": false,
        "outDir": ".",
        "rootDir": ".",
        "sourceMap": false,
        "moduleResolution": "jspm"
    },
    "exclude": [
        "node_modules", "jspm_packages"
    ]
}
```

以下のようなimport文のコンパイルが通るようになります。

```ts:app.ts
import {bootstrap} from 'angular2/bootstrap';
```

より具体的に言うと、jspmが作成するSystemJSの定義ファイルを元に、`angular2/bootstrap` のモジュール実体を探索し、必要なd.tsファイルを解決してくれます。

```js:config.js(jspmのCLIで生成)
System.config({
  baseURL: "/",
  defaultJSExtensions: true,
  transpiler: null,
  paths: {
    "github:*": "jspm_packages/github/*",
    "npm:*": "jspm_packages/npm/*"
  },

  map: {
    "angular2": "npm:angular2@2.0.0-beta.0",
    // :
  }
});
```

改造版TypeScriptに同梱したtsserverをエディタやIDEと連携させれば、import対象の補完も効きます。

<img width="356" alt="2__app_ts______workspaces_javascript_ts-jspm-ng2__-_VIM__vim__と_1__zsh.png" src="https://qiita-image-store.s3.amazonaws.com/0/8808/e71db52d-e5cc-fb67-4623-801d45ffb51d.png">


なお、大概のTypeScriptに対応したIDEやエディタプラグインは、TypeScriptのlib/tsserver.jsを経由して動きます。
このため、カスタマイズ版TypeScriptを使う場合は、IDE側でtsserver.jsのパスを設定しないと使いものになりません。
上記キャプチャのVim + tsuquyomiは、npm installしたTypeScriptを優先的に利用するので設定不要ですが、例えばVisualStudioCodeであればワークスペース設定の`typescript.tsdk` にカスタマイズしたTypeScriptの`lib` フォルダを設定する必要があります。

```js
  // Specifies the folder path containing the tsserver and lib*.d.ts files to use.
  "typescript.tsdk": null,
```

## どうやったか

moduleResolutionは、`src/compiler/program.ts` に記述されています。今回はここにJSPM用のresolverをぶっこみました。
主な変更箇所を抜粋すると、以下となります。

```ts:src/compiler/program.ts

    // 中略

    export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
        const moduleResolution = compilerOptions.moduleResolution !== undefined
            ? compilerOptions.moduleResolution
            : compilerOptions.module === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic;

        switch (moduleResolution) {
            case ModuleResolutionKind.NodeJs: return nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host);
            case ModuleResolutionKind.JSPM: return jspmModuleNameResolver(moduleName, containingFile, compilerOptions, host);
            case ModuleResolutionKind.Classic: return classicNameResolver(moduleName, containingFile, compilerOptions, host);
        }
    }

    export function jspmModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
        const containingDirectory = getDirectoryPath(containingFile);
        const supportedExtensions = getSupportedExtensions(compilerOptions);
        if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) {
            const failedLookupLocations: string[] = [];
            const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
            let resolvedFileName = loadNodeModuleFromFile(supportedExtensions, candidate, failedLookupLocations, host);

            if (resolvedFileName) {
                return { resolvedModule: { resolvedFileName }, failedLookupLocations };
            }

            resolvedFileName = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, host);
            return resolvedFileName
                ? { resolvedModule: { resolvedFileName }, failedLookupLocations }
                : { resolvedModule: undefined, failedLookupLocations };
        }
        else {
            return loadModuleFromJSPMModules(moduleName, containingDirectory, host);
        }
    }

    function loadModuleFromJSPMModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
        var loader = sys.getJspmLoader();
        var jspmNormalizedName = loader.normalizeSync(moduleName);
        if (loader.defaultJSExtensions) jspmNormalizedName = jspmNormalizedName.replace(/\.js$/, '');
        jspmNormalizedName = jspmNormalizedName.replace(/^\s*file:\/\//, '') + '.d.ts';
        if (host.fileExists(jspmNormalizedName)) {
            return {resolvedModule: {resolvedFileName: jspmNormalizedName, isExternalLibraryImport: true}, failedLookupLocations: []};
        }
        else {
            return loadModuleFromNodeModules(moduleName, directory, host);
        }
    }

    // 以下略

```

ポイントは、`loadModuleFromJSPMModules`にて、jspmのローダの`normalizeSync` を叩いて、モジュール名から実ファイル名を取り出す部分です。
`normalizeSync` jspmにラップされたSystemJSのAPIですが、こいつを叩くと、`'angular2/core'` を`'(プロジェクトルート)/jspm_packages/npm/angular2@2.0.0-beta.0/core.js'` のように展開してくれます.

jspmのローダは`require('jspm').Loader()`で取得することが出来ます。
program.tsに直接requireを記述するのはさすがに心が痛むので、sys.tsに下記を追記した上で`function getNodeSystem(): System` の戻り値にぶっこんでます。
(所詮はNPMのパッケージに依存するしている魔改造なので、どのコードでrequireしても大差はないのですが。。。)

```ts:src/compiler/sys.ts
            var jspmLoader: any;
            function getJspmLoader() {
                if (!jspmLoader) {
                    jspmLoader = require('jspm').Loader();
                }
                return jspmLoader;
            }
```

その他、compilerOptionsを増やす都合でenumやparserを多少書き換える必要がありますが、どうでもいいレベルなので割愛。

## まとめ

このエントリでは、以下について記載しました。

* どうしても`jspm_packages` からd.tsをロードしたければTypeScriptを魔改造すればよい。
* NodeJsで動けば良いレベルであれば、超簡単。

とはいえ、TypeScriptに手を突っ込んでまで実現すべき内容か？といわれると疑問符が付きます。
そこまでjspmに固執してないですし。

TypeScriptのissue#6012には、「バックログには、ユーザ定義moduleResolverも挙がっている」と書いてあるので、これが実装されるのを待ちたいですね。