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」が楽しい。速くなったかどうだかは、並べて比べてないのでわかんないな。

FlowにImmutable.jsの型定義を取り込む

Immutable.jsの型定義の取り込みできた。今後は応用して他のライブラリも手動で取り込みできよう。

# .flowconfig
[ignore]
# .*/node_modules/* こいつが邪魔してた
.*/node_modules/eslint-plugin-jsx-a11y/*
.*/node_modules/react-event-listener/*
.*/sass/*
.*/static/*
.*/bin/*
.*/pkg/*
.*/src/*
.*/vendor/*

[include]

[libs]
# Immutable.jsの型定義ファイル
./node_modules/immutable/dist/immutable.js.flow

[lints]

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

まず、「$ flow-typed create-stub immutable@3.8.1」ととりあえず通すために手動でつくっちゃった型スタブファイルは削除します。そして、.flowconfigのlibsに、node_modules以下にインストールされているImmutable.jsのフォルダを探して特定した.js.flow型定義ファイルを登録します。しかしignore設定でnode_modules全体を排除しているとlibsが効かない。うーん、バグとはいえないがバグっぽい動きだなあ。私の環境では、eslint-plugin-jsx-a11yとreact-event-listenerが引っかかってたのでそれだけを明示的に除外するようにしました。

.js.flow

今回みつけた型定義ファイルですが、拡張子が.js.flowとなってました。.js.flowファイルを私のnode_modules以下でfindするとfbjs・github・immutable・invariant・throatだけ、拡張子が.jsで「declare type」を含むものはなし。まだまだですな。redux-formは最新のv7でFlow対応しているけど、redux-form-material-uiがpeerDependenciesにredux-formをv6で書いているので、いずれ。

もうひといきFlowが成熟したら提供ライブラリも増え、この辺のワークは全自動に近い感じになってくるでしょうが、おそらく最低一年はかかるかな。しかし、数日前に気になる記事が…

Facebook OSSのライセンス問題

medium.com

日本語抄訳:スタートアップはReactを使うべきではない (BSD + patentsライセンスを考慮して) — もし、いつか大企業に買収されたいと望むなら | To Be Decided

React・Jest・Flow・Immutable.jsと、私はFacebookOSSをスタック一式で使ってますが、そのFacebookのライセンス戦術があからさまに攻撃的であるという内容。Facebookへの特許訴訟を要件とした反撃策なので、ほとんどのプログラマーは目先の充足を排してまで今に考えることじゃないかな。しかし業界内のOSS資本投下が可能な組織の取り組みが変わってFlowのエコシステムの成長が阻害されることがあると残念です。型定義ライブラリの充足には全体の協力が必要なので、穏便に計画を進捗させてほしい。

ところで、gitの登場でOSS利用者のメンタルが変わったと思います。gitはそれまでのFork equals Fake(OSSのコードの自家製分岐バージョンを作るのは嫌われる)というFSF的なOSS原理主義での考え方に反して、Cloneして書き換えてPull Requestを送るというForkを積極的に正統な開発作法として取り込むようになり、結果としてWebpack等のバンドルツールを正統な開発手法と認知しています。昔のままならバンドルツールはコードを書き換えるわ、削るわで、存在が怪しいものになってしまってたでしょう。とにかくgithubをザクッと検索してザクザクっとソースやドキュメントを読んでザクザクザクっとコードを取り込み時には書き換えてと、昔のタブーを忘れてカジュアルにスピード開発しています。

そしてgithubの登場でOSS提供者のメンタルも変えました。githubにフリーのアカウントを取ってプロジェクトを作ればそのままホスティングされるし、特にgoではそのままライブラリのセントラルリポジトリになっています(JavaScriptではnpmがその任を担って、githubはセントラルリポジトリとなってませんがそのうちgo getみたいになるんじゃないかな。ほとんど全部githubなんだし、記述の手間をかければやり方もすでにあるし)。このようにgithubが自分のソフトウェアをOSS化するコストを極限まで小さくしました。

食うに困らなくなった現代人は名声を求めるというのがOSSの文化背景を説明する理屈の一つとされてきましたが、フォークして書き換えてホスティング無料かつ作業も短時間となり、つまりはコストまで困らなくなると、その名声すら動機とならなくなってくることが予想されます。github登場以降のプログラマーOSSに対してポスト名声の動機付けは特に必要としてなく、git登場以降のプログラマーとして自分の開発プロセス上の課題解決を省力化するように選択を重ねた結果、いままで以上にOSSと日々関わっていくことになったと言えそうです。自分の成果物をクラウド保存していつでも再利用できるようにするだけ、他の人が使いたければ使ってもいいよってだけ。名声の名残はプログラマーの転職時にはアカウントでの成果がレジュメの一部となるぐらい。

今回の特許訴訟に組み合わされたFacebook OSSのライセンス戦術は、副作用として大企業のOSS開発&提供へ資本投下する動機付けを発明したかもしれないですね。ビジネスを守るために企業が大金を投じてがんばって流行るOSSを作ったり、流行ってるOSSの著作者をプロジェクトごと買って囲いこめば、世のプログラマーが極端に省力化しているのでこのパテントテクニック/スキームは野火のように広がる。昔のGPL汚染ガーとか言ってたレベルの拡大スピードではないと思います。しかし、そもそもの論点としてこのスキームは法廷的に有効なのか?誰かがReactアプリ作ってからFacebookを特許訴訟してみないことには、どうなるか正確に予想できる人はいないんじゃないかな。特許と、その特許にまったく関係ない著作物の再利用とで、緊密にリンクするって単純なことじゃないように思います。よってFacebookにとって、犬が散歩道でおしっこ撒き散らしてるぐらいにしかならないと薄々思う。

特許訴訟に有効だとしても、OSS提供企業と著作権がどうしたということになってビジネスにクリティカルな痛手を被るリスクがここまでカジュアルでスピードの速い開発環境を得ているコストと比較してどうなのか。やっぱり私の今日の結論としては、目先の充足を排してまで今に考えることじゃないかな。

Update, 9/23

code.facebook.com

React、Jest、Flow、Immutable.jsについて、Reactはv16から他も同様にMITライセンスにするという。