先日、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
を設定すれば、
{
"compilerOptions": {
"module": "system",
"target": "es5",
"noImplicitAny": false,
"outDir": ".",
"rootDir": ".",
"sourceMap": false,
"moduleResolution": "jspm"
},
"exclude": [
"node_modules", "jspm_packages"
]
}
以下のようなimport文のコンパイルが通るようになります。
import {bootstrap} from 'angular2/bootstrap';
より具体的に言うと、jspmが作成するSystemJSの定義ファイルを元に、angular2/bootstrap
のモジュール実体を探索し、必要なd.tsファイルを解決してくれます。
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対象の補完も効きます。
なお、大概の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をぶっこみました。
主な変更箇所を抜粋すると、以下となります。
// 中略
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しても大差はないのですが。。。)
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も挙がっている」と書いてあるので、これが実装されるのを待ちたいですね。