このプロジェクトは以前のプロジェクトにmochaとReactの対応、自動ビルド時間短縮を施した置き換え版です。
概要
このプロジェクトは、AltJS(TypeScript & CoffeeScript) & Browserify & mocha & React構成の雛形プロジェクトです、以下の機能を持っています。
- TypeScript、Browserifyの差分ビルド
- gulp.watch、Watchifyの変更監視による自動ビルド
- TypeScript、CoffeeScriptのソースファイルを混在出来る
- ファイルごとに型が欲しいか、短く書きたいか、で好きな方を選べばいい
- CoffeeScriptで書いたクラスをTypeScriptでrequireして使う、等
- ※当然ですが、型チェックをするならTypeScript同士じゃないとダメ
- mochaによるテスト
また、独自拡張として以下の対応をしています。
- require時にaliasを指定出来る
- 従来の相対パス指定も出来る
- TypeScriptにて、ユーザ外部モジュール及び型定義ファイルの自動生成(import hoge = require(alias名);と書ける)
- 多段ソースマップの問題を解決し、Browserify生成のjsファイルからでもAltJSのソースにbreakpointを貼れる
- Reactに対応したmochaによるコンソール、ブラウザ両対応のテスト
- Watchifyによる自動ビルド時間を極力短縮
また、gulpのbuildやwatch中にエラーが発生するとエラー通知がされるようにしています。
Usage
- npm install
- tsd update -s
- gulp
- gulp (build | watch) [--env production]
- gulp (test | test:watch)
- gulp clean
--env productionオプション有りだと圧縮した公開用、無しだとソースマップ付きの開発用としてbundleファイルを生成します。
ファイル構成
root
├── src - ソース置き場
│ ├── index.html
│ └── scripts
├── test_src - テストソース置き場
│ ├── test.html
│ └── scripts
├── gulpscripts - gulp用の自作スクリプト
│ ├── debug - --env production関係
│ │ └── debug.coffee
│ ├── tasks - 各ビルド関係
│ │ ├── browserify-task.coffee - Browserifyのbundle処理
│ │ ├── coffee-task.coffee - CoffeeScriptのトランスパイル処理
│ │ ├── mocha-task.coffee - mochaのnode用・ブラウザ用各ファイルの生成
│ │ ├── react-jade-task.coffee - react-jadeのトランスパイル処理
│ │ └── ts-task.coffee - TypeScriptのトランスパイル処理
│ ├── ambient-external-module.ts - ソース内の専用タグ収集(alias、外部モジュール化の補助)
│ ├── ast-parser.coffee - ソース文字列のrequire一覧を取得する(get-requires-from-files.coffeeで使用)
│ ├── error-log.coffee - エラーログ
│ ├── forked-gulp-react-jade.coffee - gulp-react-jadeでエラー発生時にplumberでキャッチ出来るようにしたもの
│ ├── get-file-name.coffee - パスからファイル名だけ取得
│ ├── get-requires-from-files.coffee - ソース内のrequire一覧を取得する
│ ├── grep-sync.coffee - 同期版のgrep
│ ├── gulp-callback.coffee - stream内の任意の場所でコールバックを発生させる
│ ├── merge-multi-sourcemap.coffee - 多段ソースマップの合成
│ ├── notify-error.coffee - エラーのデスクトップ通知
│ ├── same-path.coffee - 複数のパスから一致する部分の取得
│ └── to-relative-path.coffee - パスを相対パスへ
├── gulpfile.coffee - gulpメインスクリプト
├── package.json - nodeパッケージ
└── tsd.json - TypeScript型定義ファイルパッケージ
下記のディレクトリ一覧は自動生成されます
root
├── typings - 公式のDefinitelyTypedによる型定義ファイル(tsd update -sで生成)
├── src_typings - srcを対象としたユーザ生成型定義ファイル (gulp (build | watch)で生成)
├── test_src_typings - test_srcを対象としたユーザ生成型定義ファイル (gulp (test | test:watch)で生成)
├── public - src成果物
├── test_public - test_src成果物
ここからは一時ディレクトリ(無視しても良い)
├── lib
├── src_typings_tmp
├── test_lib
└── test_src_typings_tmp
gulpfileについて
この環境はかなり規模が大きくなってきたため、各トランスパイル、Browserifyのbundle、mocha関係をgulpscripts/tasksに分割しています。
gulpfileに書かれているのは、起点となる各gulp.taskと、各taskのメイン処理が書かれているgulpscripts/tasksへと入出力、各オプションを渡してるだけになってます。
(短いコードは直接gulpfileに書いてます)
例えば、CoffeeSriptの場合は
# srcディレクトリのCoffeeScriptをトランスパイルしてlibディレクトリに出力
gulp.task 'build:coffee', () -> coffeeTask.createCoffeeStream('src', 'lib')
# srcディレクトリのcoffee-react(jsx記法対応CoffeeScript)をトランスパイルしてlibディレクトリに出力
gulp.task 'build:cjsx', () -> coffeeTask.createCjsxStream('src', 'lib')
_createStream = (src_root, out_root, config) ->
gulp.src("#{src_root}/**/*#{config.ext}")
.pipe(config.compiler())
.pipe(gulp.dest out_root)
createCoffeeStream = (src_root, out_root) ->
_createStream(src_root, out_root, {ext: '.coffee', compiler: $.coffee})
createCjsxStream = (src_root, out_root) ->
_createStream(src_root, out_root, {ext: '.cjsx', compiler: $.coffeeReact})
module.exports =
createCoffeeStream: createCoffeeStream
createCjsxStream: createCjsxStream
という感じになってます、他のtaskも似たような書き方です、ここまで来るとTypeScriptにしてinterfaceを導入した方が良いんですが(型による表現はやっぱり便利)、書ける環境だけ整えて結局CoffeeScriptで書いています、ambient-external-module.tsだけTypeScriptで書いているのは試験的に書いてみた名残です。
ソースへのrequire用のalias、ユーザ外部モジュール化について
ソース中に独自タグである
// TypeScript
/// <ambient-external-module alias="{filename}" />
# CoffeeScript
###
<ambient-external-module alias="{filename}" />
###
を埋め込むことにより、
gulpscripts/ambient-external-module.coffee
がソース中のタグを収集し、browserifyにソースを追加する際にrequireメソッドによりaliasが定義されます
b.require('lib/path/to/hoge.js', expose: 'hoge')
また、TypeScriptの場合はdts-bundleにより外部モジュール化されsrc_typingsディレクトリ内にユーザ型定義ファイルが作成されるため、
/// <reference path="root/src_typings/tsd.d.ts" />
import Hoge = require('hoge');
Hoge.foo();
という書き方をする事が出来ます。
多段ソースマップの解決について
AltJSからbrowserifyによるbundleファイル生成までの流れは、以下のようになっています。
- 1.AltJSのトランスパイル
- hoge.ts -> tsc -> hoge.js & hoge.js.map
- foo.coffee -> coffee -c -> foo.js & foo.js.map
- 2.browserifyによるbundle
- (hoge.js & hoge.js.map) & (foo.js & foo.js.map) -> browserify -> bundle.js & bundle.js.map
中間ファイルであるhoge.jsやfoo.jsのそれぞれのソースマップファイルはAltJSとの紐づけ、
生成物であるbundle.jsのソースマップファイルは中間ファイルとの紐づけがされた状態であり、
bundle.jsのソースマップファイルから、AltJSへと直接紐づける必要があります。
紐づけ方法ですが、mozilla/source-mapによりソースマップ内の対応した位置情報をプロットしてみると、
中間ファイルのソースマップファイルであるhoge.js.mapやfoo.js.mapのgeneratedの位置情報と、
生成物のソースマップファイルであるbundle.js.mapのoriginalの位置情報が対になっていると読み取れます。
この対になっている位置情報を基に、AltJSのoriginalの位置情報と、生成物のbundle.jsのgeneratedの位置情報を取り出せれば、多段ソースマップの問題が解決出来る事になります。
この問題を解決するスクリプトgulpscripts/merge-multi-sourcemap.coffeeを作成し、browserify実行後に走らせることでこの問題を解決しています。
- ※スクリプト内では、さらに細かく紐づけをしています。
- ※browserifyでuglifyによる圧縮後のソースマップに試しましたが、列の位置が微妙にずれる結果となってしまった為、uglifyと併用した場合は上手く動かない可能性があります。
mochaのReact対応について
Reactでテストをする際はReact.addons.TestUtilを利用しますが、その前にDOMの取得と、
globalスコープの幾つかの変数に値をセットする必要があります。
if (コンソールの場合)
jsdon = require('jsdom').jsdom
global.document = jsdon('<html><body></body></html>') # DOMを構築しセット
else # ブラウザの場合
$ = require('jquery')
global.document = $('html') # html上のDOMを取得してセット
global.window = document.defaultView;
global.navigator
# テスト開始
React.addons.TestUtils.hogehoge
コンソール側、ブラウザ側で処理が変わってきますので、環境ごとに利用するjsを切り替えることによって対応しています。
ファイル構成について
テスト用のjsファイルは、下記の構成になっています。
コンソール用
├── get-document.js - jsdomにより構築されたDOMの取得
└── run-source-map-support.js - source-map-support.js実行時のソースマップ解決
ブラウザ用
├── get-document-bundle.js - jqueryによるHTML上のDOM取得
└── run-browser-source-map-support.js - browser-source-map-support.js実行時のソースマップ解決
共通
└── test-bundle.js - test_srcのソースのbundleファイル
コンソール側、ブラウザ側で差異のあるDOM取得処理を別ファイルに切りだし、それぞれの環境で読み込んでいます。
Browserifyのtest-bundle.jsのbundle時はその別ファイルを
b.exclude('./get-document')
とexclude指定しておき、ブラウザ用のget-document-bundle.jsのbundle時に
b.require('get-document-bundle.js', expose: './get-document')
とrequire名を定義して実行時にtest-bundle.jsから見えるようにしています。
コンソール側で同名となるget-document.jsのままにしているのは、Node.jsのrequireを利用してファイルを直接指定するやや裏技的な方法で対処している為です。
なお、get-document.jsやget-document-bundle.jsは、gulpscripts/tasks/mocha-task.coffeeのcreateGetDocumentStreamで行っています。
run-(browser-)source-map-support.jsについて
テスト中にエラーが発生した時のソースマップ対応にはsource-map-supportを利用しています。
AltJSと紐づける為には、sourceMapSupport.install()のretrieveSourceMapメソッド内で、各ファイルに対応したソースマップデータ(.map内の文字列)を返す必要があります。
sourceMapSupport.install({
retrieveSourceMap: function(source) {
if (source === "test-bundle.js") {
return test-bundle.js.mapデータ
}
...
}
})
このデータは、gulpscripts/tasks/mocha-task.coffeeのcreateRunSourceMapSupportでrun-(browser-)source-map-support.jsを生成する際にソースマップファイルを読み込んで埋め込む事により対処しています。
bundleファイルの構成について
bundleファイルは、React等の共通moduleをbundleしたcommon-bundle.jsと、開発時のsrcディレクトリをbundleしたbundle.jsに分けています。
public
├── bundle.js - srcディレクトリのbundle
└── common-module.js - 共通moduleのbundle(React等)
開発中にgulp watchを開始し、srcディレクトリのコーディングをした際にbundle.jsだけを自動ビルドの対象とする事で再bundle時間の短縮を図っています。
注意点としてはbundleファイルが複数ある事への認識と、gulpfileにある設定で明示的に切り出す指定しないと、bundle.js側にmoduleが含まれてしまう事です。
また、切り出す指定をしたままだと、未使用でもcommon-bundle.js側に含まれたままになってしまいます。
その為、common-module.jsへ切り出す時は、使用する状態が不変である(ReactでWebサイトを作る場合はReactを必ず使う為、Reactを切り出すのは妥当)moduleを選んだ方が、チーム内でもどれを切り出すべきかの混乱が少なく済みます。
未使用が含まれる事はなくなりました。
# reactモジュールを、common-bundle.jsへ
gulp.task 'browserify-requireonly', () ->
browserifyTask.browserifyBundleStreamRequireOnly('lib', 'public', {
requires: ['react']
bundle_name: 'common-bundle.js'
})
# reactモジュールを、bundle.jsへ含めない
gulp.task 'browserify', () ->
browserifyTask.browserifyBundleStream('lib', 'public', {
excludes: ['react']
bundle_name: 'bundle.js'
})
TypeScriptの定義ファイルについて
定義ファイルは、DefinitelyTypedにより公開されているモジュール用・srcディレクトリ用・test_srcディレクトリ用の3種類があり、
src用、test_src用はgulpで自動生成され、TypeScriptを編集した際にも自動更新されます。
これらはDefinitelyTypedと同様にルート用の定義ファイル(tsd.d.ts)を用意している為、それぞれの定義ファイルを参照するだけでよいです。
-
DefinitelyTyped用の定義ファイル
- typings/tsd.d.ts
-
srcディレクトリ用の定義ファイル
- src_typings/tsd.d.ts
-
test_srcディレクトリ用の定義ファイル
- test_src_typings/tsd.d.ts
/// srcディレクトリの場合
/// <reference path="../../typings/tsd.d.ts" />
/// <reference path="../../src_typings/tsd.d.ts" />
/// test_srcディレクトリの場合
/// <reference path="../../typings/tsd.d.ts" />
/// <reference path="../../test_src_typings/tsd.d.ts" />
参考
所感
AltJS対応は色々面倒でした(主にソースマップ絡み)、Reactはjsx記法使えないと辛い為AltJS対応が半ば強制されており、環境を構築するのはほぼ必須です、それなら最初から環境構築せずにJavaScriptとBackbone.jsやAngular.jsで書いた方が色々準備が省けて楽かもです。
ただ、型が無いのもそれはそれで辛いし、開発段階に入ると色々自動化してくれる環境を作っておいた方が楽なのは確か。
環境構築中に嵌った所
- gulp-mocha-phantomとsource-map-supportについて
- PhantomJS上だと、sourceMapSupport.install()内のretrieveSourceMapメソッドが呼ばれなかったため、mochaでエラー発生時にエラー行のAltJSへの紐づけが出来なかった。
やり残し的な
- React等の必須 & 巨大なページ毎の共通moduleを別bundle.jsにする仕組みを作った方が良い
- 後々ページ別のbundle.js生成時にその仕組み必要だし、差分ビルドをしているとはいえ初期段階で再bundleに1秒前後かかってる、別bundle.jsに切り出したらその時間を無くせる。
共通moduleをtest-common-bundle.jsに分けた、ただ、未使用の場合に残ったままになってしまうので、そこの検知、もしくは残らないようにする仕組みが必要、それに開発中にgulpfileを触らないようにもしたい。
browserify-maybe-multi-requireを使い、未使用moduleが[test-]common-bundle.jsに含まれないようにした。