TypeScript + jspm 触って消耗した話

# TypeScript + jspm 触って消耗した話

ここ数日jspm良さそうだなーと思って触っていたのですが、途中から雲行きが怪しくなったというお話です。

僕が辿った経緯をそのまま垂れ流して長くなってしまったので、結論っぽいことだけ先に書いておきます。

このエントリを書いている時点(2015.12.25)においては、TypeScripterにとってはjspmは銀の弾丸にはなり得ないと考えています。
jspm適用可否は、使おうとしているライブラリ群と相談してからにしましょう。特にangular2のアプリをjspmの構成で試すのはお勧めしません。


## tl;dr

フロントエンドのモジュールローダ、パッケージマネージャは流行り廃りが激しい世界です。
思い返すと、RequireJS(AMD)が出てきたと思えば、Bowerでパッケージ持ってきてgruntやgulpでwiredepしてみたり、npmからrequireしてbrowserifyかましてbundle.js作ってみたり、さらに発展させてWebPack使ったりと本当に目紛るしい。
毎年のように、新しい開発環境の構成に慣れるように努力し、それまでに培った知識が陳腐化していきます。

今回のエントリではjspmというパッケージマネージャ、および付随するSystemJSについて書きますが、数ヶ月もしたら何の役にも立たなくなるかもしれません。

僕はメインではフロントのパッケージマネージャはBower, HTMLへのinjectを含め、諸々のタスクをgulpで書く、という構成を取っていました。
プロダクションレベルのプロジェクトでがっつりやる分にはそんなに文句ないですが、ちょっと何かを試したいときには結構面倒です。
具体的には、**フロントエンド向けのモダンなライブラリ、フレームワークが触れる環境をさくっと作りたい**というケースでの話です。

「TypeScriptやBabelでトランスパイルして、依存するパッケージをinjectして、ソースコードのwatchとwebサーバの起動を行う」っていうタスク、何も見ずに書けますか？[^1]
僕はとっととフレームワーク触って遊びたいのに、その手前のビルドスクリプトで消耗するのはウンザリです。
望みのタスクが入ってるBoilerplateをyeomanのgeneratorで見つけるのも手ですが、自分には不要なdevDependeciesが多かったりするとnpm installの時間にウンザリすることが増えてきました。何回SocketIOとphantomJSダウンロードすりゃ気が済むんだよ、と言いたくなる。[^2]

そのような経緯と、最近は開発環境系のキャッチアップが疎かになっていたということもあり、他の構成を試そうと思った次第です。

もう一度書きますが、今回の目的は、**フロントエンド向けのモダンなライブラリ、フレームワークが触れる環境をさくっと作りたい** です。
逆に言うと、「最終的な配布形態をどう作れば良いか」のようなテーマはガン無視します。


## jspmの良い所

jspmはモジュールローダにSystemJSを利用することを前提としたフロントエンド用のパッケージマネージャです。何が出来るか等の説明は下記のエントリ等、様々な所で紹介されているので、詳細は割愛します。

* [jspm で快適 javascript 生活（クライアントサイド JS の依存管理決定版）](http://qiita.com/hrsh7th@github/items/0a225c46ba17196b9a55)
* [jspmで始めるES6生活](http://qiita.com/ledmonster/items/6b4b44ccb72d5e48df9c)

僕が良いな、と思った点は下記です。

* 依存するパッケージ(lodashやreact)の実体と、ES6 Modulesの紐付を自分でメンテナンスしなくてよい
* borwserifyと違い、開発時は必要なモジュールに該当するコードをそのまま読み込める
* TypeScript, Babelの実行時transpileもサポートしている
* devDependeciesやビルドスクリプト相当が限りなく少ない(=暗記できる程度の設定)

### 本当に目的に沿っているのか確かめてみる

試しにTypeScript + Reactの開発環境を作ってみました。

出来上がる環境は下記の要件を備えています。

* TypeScriptソースファイルの実行時transpile
* ソースのwatch、live reloadあり

必要とするCLIは `npm -g install jspm typescript dtsm browser-sync` です。jspm以外は大概入ってるんじゃないかと思います。

ちょっと長いですが、以下に全手順を書き出してみました。

まずは片っ端からinit祭りです。

```sh
jspm init
```

最後の質問だけはデフォルトではなく、TypeScriptを選択します。

```text
Package.json file does not exist, create it? [yes]:
Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:
Enter server baseURL (public folder path) [./]:
Enter jspm packages folder [./jspm_packages]:
Enter config file path [./config.js]:
Configuration file config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:
Do you wish to use a transpiler? [yes]:
Which ES6 transpiler would you like to use, Babel, TypeScript or Traceur? [babel]:TypeScript
```

続いてtsconfig.jsonを作って、

```sh
tsc --init
```

.d.ts取得のためにdtsm.jsonを作り、

```sh
dtsm init
```

Webサーバ用にbs-config.jsを作ります。

```sh
browser-sync init
```

この段階のファイル構成：

```text
 |- jspm_packages/                    
  |+ npm/                             
  |  system-csp-production.js         
  |  system-csp-production.js.map     
  |  system-csp-production.src.js     
  |  system-polyfills.js              
  |  system-polyfills.js.map          
  |  system-polyfills.src.js          
  |  system.js                        
  |  system.js.map                    
  |  system.src.js                    
 |  bs-config.js                      
 |  config.js                         
 |  dtsm.json                         
 |  package.json                      
 |  tsconfig.json                     
```

続いてパッケージの取得. 今回はシンプルなReactということで下記.

```sh
jspm install react react-dom
dtsm install react react-dom
```

ここからはエディタの出番。

まず、ソースを書くに先んじてtsconfig.jsonを書き換えます。

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

* SystemJSということで`module`に`system`を指定
* React JSX使いたいので、`jsx` に`react`を指定
* `jspm_packages` フォルダ配下が探索されるのが嫌なので`exclude` に追記

といった具合です。

ここからはソースコードです。tsconfig.jsonが完成しているので、Visual Studio CodeやVim + tsuquyomi, emacs + tide等、TypeScriptのIDEがあれば補完が効くはず。

JSXのviewを書いて、

```ts:components/app.tsx
import * as React from 'react';

export class App extends React.Component<any, any> {
    render() {
        return (<div>Hello, React TSX!!</div>);
    }
}
```

bootstrapとなる起動コードを用意して、

```ts:app/bootstrap.ts
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {App} from '../components/app';

ReactDOM.render(React.createElement(App), document.getElementById('app'));
```

index.htmlです.

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
  <script src="jspm_packages/system.js"></script>
  <script src="config.js"></script>
  <script>System.import('app/bootstrap')</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
```

上記で読み込まれるconfig.jsは、多少手を加えてあげる必要があります：

```js:config.js
System.config({
  baseURL: "/",
  defaultJSExtensions: true,
  transpiler: "typescript",

  /* 追記ここから */
  typescriptOptions: {
    jsx: 2
  },
  packages: {
    app: {defaultExtension: 'ts'},
    components: {defaultExtension: 'tsx'}
  },
  /* 追記ここまで */

  paths: {
    "npm:*": "jspm_packages/npm/*"
  },
  /* 以下略 */
});
```

今回は実行時にbrowserでTypeScriptをトランスパイルする構成としているので、

* `packages` の箇所で`.ts`, や `.tsx` がロードされるようにする
*  JSX(.tsxファイル)はデフォルトではtranspileしてくれないため、 `typescriptOptions` に `jsx: 2` というパラメータを付ける[^3]

最後にWebサーバとして使うBrowser Syncの設定ファイルです。

```js:bs-config.js
module.exports = {
    "ui": {
        "port": 3001,
        "weinre": {
            "port": 8080
        }
    },
    
    /* 変更 */
    "files": ["index.html", "config.js", "app/**/*.ts", "components/**/*.tsx"],
    "watchOptions": {},
    "server": true,
    /* 変更ここまで */

    "proxy": false,
    "port": 3000,
    "middleware": false,
    /* 以下略*/

}
```

ここでやっていることは、

* プロジェクトルートをドキュメントルートとして扱う
* live reloadしたい対象のファイルのパターンを`files`に列挙する

程度なので、正直無くてもそんなに困りません。

`browser-sync start --config bs-config.js` でサーバを立ち上げれば、`Hello, React TSX!!` の文字が画面に表示される筈です。


実際に全手順と変更したファイルの中身を記述してきたため、結構な量に見えるかもしれません。
しかし、jspm らしい部分といえばindex.htmlのscriptタグ部分と、config.jsに追記したSystemJS用の設定だけです。
tsconfig.jsonはTypeScriptで開発をするのであれば、どのような開発環境であったとしても記述するでしょうし、bs-config.jsにしても、特に目新しい話ではありません。
ビルドスクリプトを記述したり、ビルドのために何かをローカルにインストールしたりすること一切無にここまでこれるのは結構感動的です。

## jspmがハマらないケース

ここまでの段階では、僕はjspmを真剣に使おうかな、という気持に傾いていました。

他のフレームワークも試そうと考え、つい先日にbetaがリリースされたangular2を試してみたのですが、冒頭で書いた通りこれが上手くいかない。。
[angular2の 5 MIN QUICKSTART](https://angular.io/docs/ts/latest/quickstart.html)をざっと見る通り、TypeScript + SystemJSで紹介されてますし、jspmで上手くいきそうな予感がプンプン漂ってたんですが。。。

実際、`jspm install angular2` でrxjs, zone.js, reflect-metadataといった依存モジュールも含めてごっそり落としてくれますし、SystemJSのマッピングも綺麗に書いてくれます。

いざソースを書こうとすると雲行きが怪しくなります。
先ほどのReactの例では、`dtsm install react-dom rect` を実行して、コンパイルに必要な.d.tsファイルをダウンロードしていました。
angular2を触るのに慣れている人にとっては周知の通りですが、`dtsm install angular2` をしても、コメントのみが記載されたtsファイルが返ってくるだけです。[^4]

```ts:typings/angular2/angular2.d.ts
// Type definitions for Angular 2
// Project: http://angular.io/
// Definitions by: angular team <https://github.com/angular/>
// Definitions: https://github.com/borisyankov/DefinitelyTyped

// Angular 2 distributes typings in our NPM package.
// To get the typings, simply:
// $ npm install angular2
// and use TypeScript 1.6.2 or later.
//
// Note that TypeScript must be configured with
// --moduleResolution node
// which is the default when --module commonjs
```

「angular2.d.tsはnpmのパッケージに入れてあるからそれ使えよ。tscの--moduleResolutionオプションにnodeって書くだけだぜ」と言ってますね。
しかしながら、今使っているのはnpmではなく、jspmです。 `node_modules` は存在せず、`jspm_packages/npm` であるため、moduleResolution に何を書いてもtscは読みにいってくれません。

下記のangular2 componentの.tsファイルを書いたとしても、1行目からコンパイルエラーとなり、補完もへったくれも無い状態です。

```ts:app/app.component.ts
import {Component} from 'angular2/core';
@Component({
    selector: 'my-app',
    template: '<h1>My First Angular 2 App</h1>'
})
export class AppComponent { }
```

無論、`npm install angular2 --save` を叩いて(`jspm_packages/npm`だけでなく)`node_modules`にもangular2を突っ込めば、tscやtsserverは動作してくれるんですが、そこまでするなら、最早jspmに頼る理由が大分薄れてきます。

同じような悩みを持つ人は結構いるようで、TypeScriptの[automatic module resolution for JPSM packages #6012](https://github.com/Microsoft/TypeScript/issues/6012)というissueに全く同じ内容があげられています。

issueの内容を読む限り、下記の理由から直近の対応は無いと見ていいでしょう。

* jspm, SystemJSにおける、モジュール名 <-> 実ファイル のマッピング機構が仕様として名文化されておらず、実装も安定したとはいえない段階であること
* ユーザ定義のmodule resolverをcompilerで使えるようにする、といったbacklogは挙がっているものの、tscに組み込むだけでなくIDE・エディタのサポートも行うことを考慮すると、大掛かりな活動となること

どうしてもjspmで取得したパッケージのモジュール解決をしたければ、TypeScriptのソースコードをイジって、自分でjspm用のモジュールローダを作るしかないんじゃないかな。
SystemJSLoaderの`normalizeSync()`とか使えば、モジュール名からファイルパス取れるし、実装するだけならそんなに難しくないかもしれない。[^5]

TypeScript 1.8では、SystemJSっぽい[Path mappings based module resolution](https://github.com/Microsoft/TypeScript/issues/5039)という機能が追加される見込みだけど、飽くまでtsconfig.jsonにマッピングを定義する話の模様なので、jspmの生成するconfig.jsと親和性があるのかどうか、まだ良く分かっていません。


## まとめ
jspm + TypeScriptが上手くいく例としてReactを、上手くいかなかった(途中でやる気なくした)例としてangular2を取り上げました。

* TypeScriptには現状jspm packagesからモジュールを読む仕組みが存在しない(すぐに出来る見通しも立ってない)
* したがって、`--moduleResolution node` を前提として公開されているライブラリをjspm installしても、コンパイルが困難

ライブラリ毎を試す前に、jspmが上手くいきそうかどうかを恐る恐る試さねばならないというの状況は、そもそもの目的であった「さくっと開発環境を用意する」を達成出来ているとは言えません。
後ろ髪を引かれる思いも強いですが、現状は素直にnpm + browserify(+ watchfy)の構成にしておこうと考えています。[^6]
何らかの解決策を知っている・思いついた等あれば、コメント頂けると嬉しいです。


## 注釈

[^1]: gulpなら、`npm install gulp gulp-typescript gulp-inject main-bower-files --save-dev` あたりかな？自信ない。
[^2]: この1年で、transpilerやビルドツールがどんどん出てきているお陰で、自分にあったgeneratorを探すのがどんどん難しくなってきる気がする。
[^3]: tsconfig.jsonでは、'"jsx": "react"' ですが、TypeScriptが内部的にreactのenumに2を割り当てているため。正直イケてないし、compilerOptionsの二重管理になってるので、transpileは`tsc --watch`とかに任せちゃうのも手かも。
[^4]: alpha-39まではDefinitelyTypedにangular2.d.tsがいました。この頃に書かかれているブログ記事等だとangular2 + jspm + TypeScriptの構成は「上手くいく」ことになります。
[^5]: [https://github.com/Microsoft/TypeScript/blob/v1.7.5/src%2Fcompiler%2Fprogram.ts#L39](https://github.com/Microsoft/TypeScript/blob/v1.7.5/src%2Fcompiler%2Fprogram.ts#L39) 辺りを読むと、nodeのmoduleResolutionの実装がSystemJSと比べるとどんだけシンプルか分かる
[^6]: 主にハマった内容がd.ts周りのため、jspm + Tracuerやjspm + Babelの構成を否定するつもりは特にないです。