babel-loader+webpackでreact開発環境を整える(ただし型チェック...)


TL; DR.

@babel/preset-typescript があるので ts-loader (webpackのTypeScript用プラグイン)はいらない子になったと言いたいです。のでbabel-loader+webpackでTypeScriptを使ったreactの開発環境を作るのが今回のゴールです。ネタバレするとトランスパイルはbabel-loader+webpackで問題ないですが、型チェックが必要なのでtscで型チェックします。

Installing

$ npm install --save-dev \
  @babel/cli \
  @babel/core \
  @babel/preset-env \
  @babel/preset-react \
  @babel/preset-typescript \
  @types/react \
  @types/react-dom \
  babel-loader \
  typescript \
  webpack \
  webpack-cli
$ npm install --save \
  react \
  react-dom

babelの設定

そもそもコヤツがMicroSoftのブログでpublishされたのが2018年8月27日。
ref: https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/

@babel/preset-typescript@babel/preset-envを使えばTypeScript->任意のversionのESに変換してくれます。

// babel.config.js
'use strict';

const presets = [
  // 必要に応じて browserslist(対象のブラウザ) とか useBuiltIns (polyfill) の設定を入れていこうな
  // ['@babel/preset-env', {browserslist: '> 0.25%, not dead'}]的な
  ['@babel/preset-env'],
  ['@babel/preset-typescript', {
    // 強制的にjsxのパースを行うオプション。
    // e.g: var hoge = <string>fuga; みたいなコードがパースできる
    isTSX: true,
    // isTSX: trueにするときは常に必須のオプション
    allExtensions: true
  }],
  ['@babel/preset-react', {
    // WIP: 後半でここ、NODE_ENVで切り替えられるように変更します
    development: true
  }]
];

module.exports = {presets}

まずはこれだけでbundleはされませんが、

// src/Index.tsx
import * as React from 'react';
import {render} from 'react-dom';

render(
  <h1>Hello World!!</h1>,
  document.querySelector('#root')
);

上記のようなコードが書けるようになります。んでもって

$ babel src/Index.tsx -o dist/index.js

こうすれば、babelによるコードのトランスパイルは完了。

webpackの設定

次はjsをbundleして単体のファイルで動くようにしていきます。っつてもここは普段のwebpackの設定とそんなに変わりません。

// webpack.config.js
'use strict';

const path = require('path');

module.exports = {
  target: 'web',
  // entry pointをrepository root からの src/Index.tsxを想定
  context: path.join(__dirname, 'src'),
  entry: './Index',
  // 出力先は dist/index.js です
  output: {
    path: path.join(__dirname, 'dist'),
    filename: './index.js'
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  module: {
    rules: [{
      test: /\.tsx?$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
    }]
  }
};

ここは皆さんの普段のbabel-loader+webpackと変わらんのジャマイカでしょうか。

型チェックの問題

さて実はここで静的型チェックの問題があります。
例えば

// src/Index.tsx
import * as React from 'react';
import {render} from 'react-dom';

// number型にstringを入れてる
const content: number = 'Hello World!!';

render(
  <h1>{content}</h1>,
  document.querySelector('#root')
);

number型にstringを入れてるの明らかに間違ってるんですが、@babel/preset-typescriptでは型チェックをしないので(トランスパイルするだけ)、これは通っちゃってdist/index.jsに成果物が吐き出されます。のでトランスパイルはbabelを使い、静的型チェックは従来どおりtscを使うという戦略で行きます。

型チェックとしてのtscの導入

まずはtsconfig.jsonをば。

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "allowJs": false,
    "jsx": "react",
    "declaration": false,
    "noEmit": true,
    "strict": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}

意図としては、コードのトランスパイルはbabelがやってくれるので型チェック(厳密にはimport先が間違ってないかとかも)だけやってtscコマンドを実行した後は何も生成しないという意図になります。んでは一個ずつオプションの解説をば。

target

コンパイル後のEcmaScriptのバージョン指定です。
実際はtscで成果物作るわけではないのでなんでもいいと思いますが、一応バージョン固定したかった(のでESNEXTは指定してません)のと、現時点(2019年2月)で最新のes2018を指定しました。

module

moduleのimport方式をどうするか。ここも成果物を作るわけではないので、なんでもいいと思います。

allowJs

tsxとtsしか使わないよという前提であれば(import先を除く)でfalseにしてCIフェーズとかで弾いてもいいのではと考えました。

declaration

falseにすることでd.tsの型定義ファイルの生成をさせないようにします。

noEmit

trueにすることで置換後のjsファイルを吐き出さないようにします。(src/Index.tsxに対してsrc/Index.jsみたいな)

strict

trueにすることで、nullとかanyとかimplicit returnを厳格にチェックしてくれます。

react.production.min.jsをバンドルに使う

@babel/preset-reactにはdevelopmentというオプション(ref: https://babeljs.io/docs/en/babel-preset-react#development)があって、これの切り替えによってdevelopmentモードをtrueにできます。また、react/index.jsの配下には


// node_modules/react/index.js
'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

というコードが入っていて、環境変数NODE_ENVによる切り替えが可能です。ので、ここではbabel.config.jsでNODE_ENVをコントロールできるように

// babel.config.js
'use strict';

// NODE_ENVがproductionかどうかの判定
const isDev = process.env.NODE_ENV !== 'production'

const presets = [
  ['@babel/preset-env'],
  ['@babel/preset-typescript', {
    isTSX: true,
    allExtensions: true
  }],
  ['@babel/preset-react', {
    development: isDev
  }]
];

module.exports = {presets}

NODE_ENVの判定を入れました。

仕上げ

さて、これでビルドの材料は揃ったので、仕上げにnpm-scriptsを仕込んでいきましょう。
以下はpackage.jsonのscripts部分の抜粋です。

"scripts": {
  "build:production": "NODE_ENV=production; tsc && webpack --mode=production",
  "build:development": "tsc && webpack --mode=development"
}

例えばですが、ガンガン開発するときはtsc無視してciとかテストフェーズだけtsc回して型ミスってるところだけ直していくっていうスタイルも取れるのかなーと考えています。
(まぁparcel.js使えと言われれば元も子もないんですけどねw)

参考