Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

先日、TypeScript + jspm 触って消耗した話にangular2をjspm + TypeScriptの環境でコンパイルしようとして上手くいかなかった話を書きました。

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

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

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

作成したブツは Quramy/TypeScript#jspm-module-resolutionにpushしてあります。

改造内容

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

件のangular2であれば、

jspm install angular2

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

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

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

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

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

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対象の補完も効きます。

2__app_ts______workspaces_javascript_ts-jspm-ng2__-_VIM__vim__と_1__zsh.png

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

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

どうやったか

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

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しても大差はないのですが。。。)

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も挙がっている」と書いてあるので、これが実装されるのを待ちたいですね。

Quramy
Front-end web developer. TypeScript, Angular and Vim, weapon of choice.
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away