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のショートハンドが用意されています。

f:id:masataka_k:20170906080833p:plain

AtomはテストのGUIを入れてないから地味。WebStormには素敵なUIがあります。

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」とあわせる。

TypeScriptのReact-Webpack DevServer実行

TypeScriptのReactアプリを作るに際し、一番シンプルなのはWebpack DevServerで実行だけするというものかなと思います。NodeとYarnはすでに入ってること前提で、あとはテキストエディタ&WEBブラウザだけでOK。

$ mkdir TsReact
$ cd TsReact
$ yarn init
$ yarn add react react-dom
$ yarn add @types/react @types/react-dom -D
$ yarn add typescript webpack webpack-dev-server path awesome-typescript-loader -D

まずターミナルからプロジェクトの基盤を作ります。フォルダ作ってYarnでやるべきことをやる。reactとwebpack関係はES6のプロジェクトと一緒ですが、Babelの一群がtypescriptひとつで代替されるので必要なものが格段に少なくなります。

// tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "jsx": "react"
    }
}

TypeScriptのコンパイラ(=トランスパイラ)の設定をプロジェクトルートのtsconfig.jsonに書きます。まずは最低限で上記のとおり。ブラウザで実行するのでes5がターゲット。スキーマを見るとjsxはreact-domかreact-nativeか処理しないか(preserve)。例のFacebook OSSライセンスが揉めれば、いずれはpreserveで非Reactなものに対応させるようなことあるかもしれませんが、今はReact以外はありません。

このtsconfig.jsonの内容をpackage.jsonに格納する方法を探したけど見つからなく、どうもできないみたい。BabelやEslintみたいに格納させて欲しいなあ。

http://json.schemastore.org/tsconfig

// webpack.config.development.js
const path = require('path');

module.exports = {
    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'),
    },
};

Webpack DevServerの設定ですが、こちらはプロジェクトルートのwebpack.config.development.jsへ。あとでproduction環境も作りますから、はじめからdevelopmentをいれたファイル名にしておきました。/tsフォルダにソースコードを置き、/js/bundle.jsという名前で配信するようにしています。/static/index.htmlに静的なエントリHTMLをおいて、これはdevServer.contentBaseで静的ファイルサービスの設定をしました。bundle.jsの読み込みとアプリケーションホルダーのdivを持つだけのHTMLです。

注意としては、resolveに、TypeScriptのソース拡張子である.tsおよび.tsxに加えて、.jsも並べること。Webpack DevServerが背後でjsをステルスに提供しているのでそれも解決できるようにしないとトランスパイルエラーが出ます。TypeScriptは、awesome-typescript-loaderが処理します。同じ役割でts-loaderというのも世にありますがドキュメントを読んでみた結果、後に行うHMR環境を作るのはこちらが良いとのことなのでawesomeで。

// /components/Hello.tsx
import * as React from 'react';

export type Props = {
    compiler?: string;
    framework: string;
};

export const Hello = (props: Props) => (
    <h1>Hello from {props.compiler || 'JavaScript'} and {props.framework}!</h1>
);



// /components/Counter.tsx
import * as React from 'react';

export class Counter extends React.Component<{}, { counter: number }> {
    constructor(props) {
        super(props);
        this.state = { counter: 0 };
    }

    private interval: number;

    componentDidMount() {
        this.interval = window.setInterval(
            () => this.setState({ counter: this.state.counter + 1 }),
            1000,
        );
    }

    componentWillUnmount() {
        window.clearInterval(this.interval);
    }

    render() {
        return <h2>{this.state.counter}</h2>;
    }
}



// /index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';

import { Hello } from './components/Hello';
import { Counter } from './components/Counter';

ReactDOM.render(
    <div>
        <Hello compiler="TypeScript" framework="React" />
        <Counter />
    </div>,
    document.getElementById('application'),
);

簡単なReactアプリとして、二つのコンポーネント(Hello.tsxとCounter.tsx)をindex.tsxでまとめるものです。HelloがStateless Componentでプロパティを設定するもの、CounterがClass Componentでステートを持つものにしました。Flowを書いてみた後でTypeScriptを触ると、なんて一緒なのだろうと。探せば細かく違いはありながらも概ね違和感なく、TypeScript初手のはずなのに特に疑問なくすぐ慣れます。まさにES6+FlowでTypeScriptだわ。

// package.json
{
    "name": "TsReact",
    "version": "1.0.0",
    "private": true,
    "scripts": {
        "start": "webpack-dev-server --config webpack.config.development.js"
    },
    "dependencies": {
        "react": "^15.6.1",
        "react-dom": "^15.6.1"
    },
    "devDependencies": {
        "@types/react": "^16.0.5",
        "@types/react-dom": "^15.5.4",
        "awesome-typescript-loader": "^3.2.3",
        "path": "^0.12.7",
        "typescript": "^2.5.2",
        "webpack": "^3.5.5",
        "webpack-dev-server": "^2.7.1"
    }
}

package.jsonにscripts.startを足します。おまけでprivateをtrueにして、ライセンス等の情報は削りました。private=trueにしておくと削っても警告されません。そうでなければ折に触れてnpmやyarnより警告表示されます。

$ yarn start

yarn startでWebpack DevServerが起動します。yarn run startのショートハンドでyarn startが有効。

f:id:masataka_k:20170906061030p:plain

動かすとこんな感じ。今回、同時にAtomを入れてみました。AtomでTypeScript環境つくるのも簡単。結構成熟している。

TypeScriptのReact開発環境を作った

Labor Dayの連休を利用して、TypeScriptでReact開発環境を作ることをやってみました。

  • (TypeScriptで書いたReactアプリを、以下同じ) Webpack DevServerで実行する
  • 構文および書き癖チェックする
  • Jest+Enzymeでテストする
  • Webpack DevServerでHMR実行する
  • Webpackでproductionビルドする

結果として、あまりはまらずに全部できた。できてみると現金ですがTypeScript良いように思います。これまでAlt-JSの走りとしてCoffeeScriptをスルーし、DartもTypeScriptも喰わず嫌いで来てましたが、JS界隈がBabelでトランスパイルし、WEBフロントはWebpackでバンドルすること前提でECMA Script 6が共通言語となりつつある雰囲気の中、さらに新しいもの好きにFlowを入れて型を付けてみようとしたら、それは只のTypeScriptだったというオチです。言語的にいくつか確認すべきことはありながら、これまで書いて来たソースコードのほとんどが修正すくなく移行できました。

要点たくさんあるので、ぽつぽつ書いていきます。今日はお呼ばれBBQ。

Flowの速い環境変化

Flowの0.54.0が出てたのでアップグレードしたら、挙動が変わった。

# .flowconfig
[ignore]
.*/sass/*
.*/static/*
.*/bin/*
.*/pkg/*
.*/src/*
.*/vendor/*

[include]

[libs]

[lints]

[options]
suppress_comment= \\(.\\|\n\\)*\\flow-suppress
esproposal.decorators=ignore

設定がスッキリしてしまいました。Immutable.jsの型定義ファイルは、libsエントリに入れずともnode_modulesの下から検索してくれるようになったようです。エントリを残しておくと、Flow組み込みのReactのほうにもある「Iterable」型定義を上書きしてしまう不都合が発生します。また、node_modulesの下は型チェックに行かなくなったみたい。ignoreエントリに明示しなくても動きが変わらなくなった。

History、material-ui、react-hot-loader、react-router-redux、redux-form、redux-form-material-uiの型定義スタブがなくてもエラーにならなくなった。現在唯一にスタブが必要なのはbrowser-cookies、マイナーだからか?

EslintにFlowを一緒にやってもらう

WebStormの機能をもちいてFlowの型チェックエラーをエディタに反映してもらってましたが、バックグラウンドでFlowサービスをずっと動かして重いせいなのか、今日はベイエリアも珍しく摂氏34度を超える猛暑になってるのでPCのファンが止まらない。WebStormじゃないところで動かすことも考えて、FlowをEslintに組み込みます。

github.com

すでに入れてるeslint-plugin-flowtypeはユーザーにFlowの型定義を利用したプログラムを書くように促すもので、eslint-plugin-flowtype-errorsはFlowの型チェック機能を随時呼び出しては出たエラーや警告をEslintのエラーや警告としてくれるものです。Eslintとエディタの連携はAtomSublimeなど既に多くで提供されていて、もちろんWebStormでもあります。

// package.json
  "babel": {
    "presets": [["es2015", { "modules": false }], "react", "stage-1", "flow"],
    "plugins": ["transform-decorators-legacy", "react-hot-loader/babel"],
    "env": {
      "JEST": { "plugins": ["transform-es2015-modules-commonjs"] }
    }
  },
  "eslintConfig": {
    "parser": "babel-eslint",
    "plugins": ["flowtype", "flowtype-errors"],
    "extends": ["airbnb", "plugin:flowtype/recommended"],
    "env": {
      "browser": true,
      "jest": true
    },
    "rules": {
      "indent": ["warn", 4, { "SwitchCase": 1 }],
      "max-len": ["warn", 120],
      "react/jsx-indent": ["warn", 4],
      "react/jsx-indent-props": ["warn", 4],
      "flowtype/require-valid-file-annotation": "error",
      "flowtype/semi": "error",
      "flowtype-errors/show-errors": "error"
    }
  },

ドキュメントのとおりに何の疑問もなく動き、完了。WebStormでのFlowサーバー常駐を解きました。

しかし、どうなんだろう

Flowへ全部のソースコードを対応してみると…これってTypeScriptなんじゃね?という疑問が。。。今週はLabor Dayのお休みで連休なのだけど、普段は涼しいために家庭にクーラーが無い地域を襲った猛暑で外に行く気が起きない。喰わず嫌いをやめてTypeScriptの研究もしてみるとかしてみないとか。TypeScriptはApache License Version 2ですしね。とにかく型は正義。

yarnに乗り換えてみた

Facebook OSSの話題に触れてるうちに、yarnにしてみっかなと。yarnのライセンスはFacebook OSSの特徴となったBSD-3-ClauseではなくBSD-2-Clauseですけど、そこはかとなくFacebookな匂いの子です。今までニーズがなかったから入れ替えてなかったけど、むしろ今こそ楽しげかなとノリで。

まず、node関連を大掃除しました。私はnodeをHomeBrewや公式配布インストーラやnやらでその時々適当にやってたのでゴミだらけです。まずHomeBrewから抜く。私は抜いてあったので結果としておまじない。

$ brew unlink node
$ brew uninstall node

/usr/local/bin、/usr/local/lib、/usr/local/include、/usr/local/Celler、/var/db/receiptsの下のnode、n、npm、npxの関係者とわかるものを削除し、npmのグローバルリポジトリのロック&キャッシュである ~/.npmとプロジェクトリポジトリのnode_modulesとpackage-lock.jsonを削ります。/usr/local/includeにはがっつり何時のかわからんnodeのcヘッダファイル群がハードコピーされてた。HomeBrewで管理するとこれはCellerへのリンクになります。

$ brew install yarn
$ brew link node

あとはプロジェクトのフォルダに移ってモジュールの取り込み直し。

$ yarn install
$ git add yarn.lock

https://yarnpkg.com/en/docs/yarn-lock#toc-check-into-source-control

yarn.lockファイルは上記URLのドキュメントによるとバージョン管理にぶっ込むことが常道らしい。should be checked into source controlって書いてある。

豆として、yarnはツールだけでリポジトリはnpmjs.orgを見てるのでnpmと同じ。loginとかcreateとかのライブラリ公開関係はことごとくnpm.orgをターゲットとしているとドキュメントに書いてある。npmはグローバルモジュールリポジトリが~/.npmでした。yarnは、~/config/yarn/globalになります。私はグローバルにはもともとnpmとnしか入ってなかったので、このyarnのグローバルには何も入れるものがなくなっちゃった。今後のnodeのバージョン管理(もちろんyarnも)はnをやめてHomeBrewでやります。

超いらない豆としては、yarnはML言語なるもので書かれてますね。関数型言語らしいんだけど言語の名前自体初めて知ったぐらいなので読めない。

yarn runがちょっと違う

yarn run <スクリプト名> で、package.jsonのscriptsエントリに登録されたものを実行するのはnpmと変わらないのですが、yarn run <./node_modules/.bin/以下の実行ファイル> で実行できるようになっていました。私はdirenvで./node_modules/.binにパスを通していましたがこれは不要になるな。

Flow-bin問題

yarnでインストールするとflow-binのプラットフォーム別バイナリのユーザー実行権限が落ちてるということはなかった。いいがかりかもしれないけど、冒頭でそこはかとなくFacebookな匂いがすると言ったことの一つ。ほか公式WEBドキュメントがFlowとyarnでまるっきり一緒の作りなことも一つ。

WebStormでの設定

Configuring Node.js Interpreters - Help | WebStorm

愛用するWebStormではサイレントにyarn対応していました。現行バージョンでは偽npmとして取り扱います。

f:id:masataka_k:20170902073617p:plain

WebStormのnodeインタプリタの設定のところで、NPM Packageをyarnに切り替えると、もうnpmは一切いらない。間違って使わないように/usr/local/lib/node_modules/npmは削ってしまった。

yarnでnpmをインストールし、そのまますぐアンインストールした

その後のたった半日ではありますが一連の運用で不都合はないけど、あとで万一npmが必要になったら「$ yarn global add npm」すれば良いと思う。思ったのですぐやってみた。

$ yarn global add npm
yarn global v0.27.5
warning package.json: No license field
warning No license field
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "npm@5.4.0" with binaries:
      - npm
      - npx
warning No license field
Done in 6.18s.
$ ls -l /usr/local/bin|grep npm
lrwxr-xr-x  1 masataka_k  admin       77  9  1 16:56 npm -> ../../../Users/masataka_k/.config/yarn/global/node_modules/npm/bin/npm-cli.js
lrwxr-xr-x  1 masataka_k  admin       77  9  1 16:56 npx -> ../../../Users/masataka_k/.config/yarn/global/node_modules/npm/bin/npx-cli.js

ちゃんとインストールされています。もちろん使えますが、使わないのでアンインストール。

$ yarn global remove npm

跡形もなくなりました。

yarnの効能

コンソールに流れるメッセージがきちんとしたサマリーになってわかりやすくなったこと。地味に「yarn why」が楽しい。速くなったかどうだかは、並べて比べてないのでわかんないな。