TypeScriptでもリリースビルドする
TypeScriptの8つめ。
Productionをやります。
// webpack.config.production.ts import * as path from 'path'; import * as webpack from 'webpack'; export default { entry: './ts/index.tsx', output: { path: path.resolve('static/js'), filename: 'bundle.js', }, module: { rules: [{ test: /\.(ts|tsx)$/, use: [ 'awesome-typescript-loader', 'tslint-loader', ], }], }, resolve: { extensions: ['.ts', '.tsx', '.js'], }, plugins: [ new webpack.optimize.UglifyJsPlugin({ sourceMap: false, comments: false }), ], };
特に論点ないですね。
$ yarn add rimraf -D
rimrafをいつも使ってます。
// package.jsonの一部 "scripts": { "build": "rimraf ./static/js/*; webpack -p --config webpack.config.production.ts" },
yarn run buildもショートハンドがあります。これはnpmと同じ。
$ yarn build
Atomを入れてTypeScriptを書く
TypeScriptの7つめ。
愛用のWebStormでTypeScriptを研究しつつ、おまけでAtomをインスコしてサンプルを作るときの清書プラットフォームとして使って見ました。
私はHomebrewでNodeもYarnも入れてますので、Atomも。MacOS向けGUIインストーラーアプリをHomebrewでいれる拡張として、このHomebrew Caskを使います。
$ brew tap caskroom/cask $ brew cask install atom
プラグインをガシガシいれます。入れたのは以下のものと、それらが依存するもの。
他にはちょこちょこ入れ出ししてみたけど、良いものが見つからず、上のでほぼいい感じ。
課題
「Go to Declaration」で型定義に飛んだりすることができなかった。調べるとバグなんだか、そうじゃないんだか。結構長い間苦労されているのかな?Atomでまだ解決できてない。
しかし、WebStormはザクザク型定義で飛んだり跳ねたりできる!すげーや商用(なのかな?)。
$ brew cask uninstall atom $ rm -rf ~/.atom
一旦、Atomはアンインストール。また時機をみて試してみます。おそらくWebStormのサブスクリプションが切れるころ。
TypeScriptでもReduxのHMR対応しておきましょう
TypeScriptの6つめ。
HMRには3段階あって、非Reactアプリ、Reactアプリと続いて最後にReact+Reduxアプリに到達します。
// /store/Store.ts import { createStore, combineReducers, applyMiddleware } from 'redux'; import { routerReducer as router, routerMiddleware } from 'react-router-redux'; import { History } from 'history'; import { composeWithDevTools } from 'redux-devtools-extension'; import { app } from './AppReducer'; export default (history: History) => { const store = createStore( combineReducers({ router, app }), composeWithDevTools( applyMiddleware( routerMiddleware(history), ), ), ); if (module.hot) { module.hot.accept('./AppReducer.ts', () => { const nextReducer = { router, app: require<{app: typeof app}>('./AppReducer').app }; store.replaceReducer(combineReducers(nextReducer)); }); } return store; };
もう派生論点なんでフルにコードは示さずとも、ポイントだけ。requireを使って更新Reducerを別名で読み込んで、store.replaceReducerを呼ぶことです。
上記コードではChromeプラグインのRedux DevToolsに対応させるため、react-router-reduxを入れました。
TypeScriptでもHMRを実現する
TypeScriptの5つめ。まだまだ続きます。
$ yarn add react-hot-loader@next
$ yarn add @types/webpack-env -D
react-hot-loaderは3.0.0-beta.7が入りました。そして@types/webpack-envを!これが自分的には一番ヤバい。見つけるのに苦労した。
// webpack.config.development.ts import * as path from 'path'; import * as webpack from 'webpack'; export default { entry: [ 'react-hot-loader/patch', './ts/index.tsx', ], output: { filename: 'bundle.js', publicPath: '/js/', }, devtool: 'inline-source-map', module: { rules: [{ test: /\.(ts|tsx)$/, use: [ 'react-hot-loader/webpack', 'awesome-typescript-loader', 'tslint-loader', ], }], }, resolve: { extensions: ['.ts', '.tsx', '.js'], }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), new webpack.NoEmitOnErrorsPlugin(), ], devServer: { host: 'localhost', port: 9000, hot: true, historyApiFallback: true, contentBase: path.resolve('static'), }, };
いろいろ出し入れしてみて、一番シンプルな作りがこれかなあ。historyApiFallbackは今は必要ないですが、React RouterすなわちHistoryを用いてかつProxyでワークアラウンドしないアプリを作るときに必須です。
- entryにreact-hot-loader/patchを入れる
- ローダーチェーンの先頭にreact-hot-loader/webpackを入れる
- HotModuleReplacementPluginを登録する
- devServer.hotをtrueにする
// tsconfig.json { "compilerOptions": { "module": "commonjs", "target": "es5", "jsx": "react", "lib": ["es6", "dom"], "types": ["webpack-env", "jest"] } }
ここで、webpack-envが出てくる!これが無いとHMRの定石である、module.hotの名前解決ができない。いやーグローバル名前空間でいろいろやれるのはダメだと思いますが、JavaScriptの黒歴史としてこんなのが山ほどこれからも出てくることでしょう。TypeScriptはJavaScriptとそのまま地続きだからグローバルで自由なユーザー定義を禁止するような言語仕様にできなかったか。。。
// /components/App.tsx import * as React from 'react'; import { Hello } from './Hello'; import { Counter } from './Counter'; export const App = () => ( <div> <Hello compiler="TypeScript" framework="React" /> <Counter /> </div> ); // /index.tsx import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { AppContainer } from 'react-hot-loader'; import { App } from './components/App'; const render = () => ReactDOM.render( <AppContainer> <App /> </AppContainer>, document.getElementById('application'), ); render(); if (module.hot) { module.hot.accept('./components/App', () => { const NextApp = require<{App: typeof App}>('./components/App').App; ReactDOM.render( <AppContainer><NextApp /></AppContainer>, document.getElementById('application'), ); }); }
Hotにする対象として、App.tsxに受け直しておきます。これをmodule.hot.acceptを用いて監視するのですが、ES6でやったときのようにWebpack2以降のモジュール読み込みシステムなるものは使えない。tsconfig.jsonのほうでモジュールシステムをcommonjsにしているのでこうなったかなと推測してます。かといってcommonjs以外の選択肢はTypeScriptのコンパイラオプションのドキュメント読んでいろいろ研究して見たけどどうもぴったりの解がほかになかったです。
requireを使ってNextAppと別名で読み込み直すのはコードもトリッキーな印象のものになります。型を解決するために、require<{App: typeof App}>なんて書き方してますからね。もしAppをexport defaultしていると、require<default: typeof App}>(‘./components/App’).default; なんてことになってエディタによっては「defaultは予約語だ!」というようにLintで引っかかったりします。汚いが、しょうがない。いまのところ他にやり方が思いつかない。
これで、TypeScriptでReactのHMRもできました!
TypeScriptでもJest+Enzyme
TypeScriptの4つめ。
Lintができたらテストもちゃんとやっとく。最近覚えたJestそしてEnzymeでやります。
$ yarn add jest enzyme react-test-renderer ts-jest @types/jest -D
jestとenzymeを加える他に、@types/jestが必須です。必須ではないですが@types/enzymeも加えておいたほうが今後にはより良いですがとりあえず加えなくても動きます。react-test-rendererはenzymeのpeerDependenciesなため。
// package.jsonの一部 "scripts": { "test": "jest" }, "jest": { "moduleFileExtensions": ["ts", "tsx", "js"], "transform": { "^.+\\.(ts|tsx)$": "./node_modules/ts-jest/preprocessor.js" }, "testMatch": [ "**/__tests__/*Test.(ts|tsx)" ] },
package.jsonにscripts.testを追加するのとjestエントリを書きます。jest.moduleFileExtensionsはtsとtsxに加えて、たとえJSでテストを書かずともjsを加えなければなりません。これも裏で処理パイプの中で.jsが出てきてそれらを解決できなくなるのを避けるためです。webpackのresolveと同様な感じ。
transformerでts-jestを設定しています。TypeScriptファイルのトランスパイルを行うのを下記URLのように自分でコード書くのもOKなようですが、ts-jsonはより重厚なことをしています。
https://github.com/facebook/jest/blob/master/examples/typescript/preprocessor.js
// tsconfig.json { "compilerOptions": { "module": "commonjs", "target": "es5", "jsx": "react", "lib": ["es6", "dom"], "types": ["jest"] } }
@types/jestを入れ、tsconfig.jsonのcompilerOptions.typesにその型情報を読み込む設定をしないと、Jestテスト中で書くdescribeとtestの両グローバル関数が名前解決できません。またcompilerOptions.libにes6とdomを追加します。これはTypeScriptのコンパイラオプションのページ、
https://www.typescriptlang.org/docs/handbook/compiler-options.html
ここの、–libスイッチの説明にある、
- Note: If –lib is not specified a default library is injected. The default library injected is:
- For –target ES5: DOM,ES5,ScriptHost
- For –target ES6: DOM,ES6,DOM.Iterable,ScriptHost
これがぶつかります。ブラウザで動かすからES5をターゲットとしてるため、Nodeで動かすJestテストやLint実行では問題が起きるということ。前にBabel環境でHMRをやるときにも似たようなことがありましたね(似て非なるのですが)。Babelではモジュール機能をON/OFFにするために面倒なことがありましたが、こちらはトランスパイル時にリンク(?)する標準モジュールの選択でということ。
// /components/__tests__/HelloTest.tsx import * as React from 'react'; import { shallow } from 'enzyme'; import { Hello } from '../Hello'; describe('HelloComponent', () => { test('<Hello TypeScript>', () => { const hello = shallow(<Hello compiler="TypeScript" framework="React" />); expect(hello.find('h1').text()).toBe('Hello from TypeScript and React!'); }); test('<Hello JavaScript>', () => { const hello = shallow(<Hello framework="React" />); expect(hello.find('h1').text()).toBe('Hello from JavaScript and React!'); }); });
テストはEnzymeのshallowでシンプルに書いてみました。$yarn testと、これもまたyarn run testのショートハンドが用意されています。
TypeScriptでもLintを張る
TypeScriptの3つめ。
やはり大人の嗜みとしてLintを設定したいと思います。ES6ではEslintでAirbnbをやってました。TypeScriptでもAirbnbを張り付けときます。
$ yarn add tslint tslint-config-airbnb tslint-react tslint-loader -D
名前もそのまま、Tslintというのがあります。設定もJSのAirbnbがTslint版で移植されてましたが、こちらはReact関係が無いので、補いでtslint-reactを。webpackに差し込み用にtslint-loaderを入れます。
{ "extends": ["tslint-config-airbnb", "tslint-react"], "rules": { "variable-name": false, "ter-indent": [true, 4], "max-line-length": [true, 120] } }
Tslintの設定は、プロジェクトルートにtslint.jsonを置きます。私の好みとしてインデント4空白、一行最大120字というのに変えたほか、ReactでStateless Componentで先頭大文字の名前の関数を書くとひっかかかるのを緩和するために、variable-name=falseにしました。それだけ。
// package.jsonの一部 "scripts": { "lint": "tslint --project ./ --type-check './ts/**/*.{ts,tsx}'" },
package.jsonのscripts.lintにコマンドを書きますが、型チェック(=文法チェック)も同時にがっつりやってくれるように、–projectと–type-checkスイッチをつけます。–projectはスイッチの引数にルートフォルダを指定。これはtsconfig.jsonのある場所を設定するとのヘルプ説明でした。
$ yarn lint
yarn run lintもショートハンドでyarn lintがあります。本家Eslint版のAirbnbと比べて、このTslint版はなんか緩くあまり怒られない。
// webpack.config.development.tsの一部 module: { rules: [{ test: /\.(ts|tsx)$/, use: [ 'awesome-typescript-loader', 'tslint-loader', ], }], },
tslint-loaderをローダーチェーンの一番下に入れます。これで一番最初にLintフィルターしてくれる。
設定のベスト
Tslintの設定で、まだまだ何がベストが見えてないです。JSX構文について緩い感じがするので、Eslintのほうを見つつTslintを試行錯誤するってのがおいおいやってきます。
そのココロは、TypeScriptと生きてく決心がついてきた。まだ二日の"にわか"ですけど。
Webpackの設定をTypeScriptで書く
TypeScriptの2つめ。前記事の続き。
Webpack DevServerを動かしてTypeScriptで書いたReactアプリを動かしましたが、このWebpackの設定をTypeScriptで書くことも可能。Babel使ってES6で書くこともできましたので驚くことではない。
$ yarn add ts-node -D
yarnでts-nodeをいれます。あとはwebpack.config.development.tsと拡張子を.jsから変更。
// webpack.config.development.ts import * as path from 'path'; export default { entry: './ts/index.tsx', output: { filename: 'bundle.js', publicPath: '/js/', }, devtool: 'inline-source-map', module: { rules: [{ test: /\.(ts|tsx)$/, use: 'awesome-typescript-loader', }], }, resolve: { extensions: ['.ts', '.tsx', '.js'], }, devServer: { host: 'localhost', port: 9000, contentBase: path.resolve('static'), }, };
ほとんど何も変わらない。。。importがFlow風な、exportがES6風な。これでpackage.jsonのscripts.startを「webpack-dev-server –config webpack.config.development.ts」とあわせる。