TypeScript + Webpack + Reactによるコンポーネントのnpmパッケージを作る

# 概要
ReactでWebアプリを開発しているときに、「このコンポーネントたちは再利用性が高いからnpmパッケージとして独立させたい」と思ったときにどうすればいいかを整理します。

こちらの記事を参考にしています。
[Reactコンポーネントをnpmで公開する（GitHub Pages付き、Babel7、webpack4）](https://qiita.com/Takumon/items/945335b0e0fa035f2201)

この記事と異なる事情として、

* TypeScriptを使いたい
* Babel CLIではなくWebpack CLIでコンポーネントをビルドしたい（sass-loaderとかも使いたい）

というのがありました。
案外いろいろなところで詰まったので、備忘録として書き残しておきます。

# 筆者の環境
* MacOS Catalina 10.15.3
* zsh 5.7.1
* npm 6.9.0
* yarn 1.19.0


# 完成品
https://github.com/kecbigmt/ts-webpack-react-components

# 手順
以下の手順で説明します。

1. 必要なパッケージのインストール
2. 必要な設定ファイルの追加・編集
3. コンポーネントを作成
4. トランスパイル
5. 他のReactアプリからパッケージを参照する

.npmignoreを用意してnpmに公開する部分は[Reactコンポーネントをnpmで公開する（GitHub Pages付き、Babel7、webpack4）](https://qiita.com/Takumon/items/945335b0e0fa035f2201)と同じなので、そちらに譲ります。


## 1. 必要なパッケージのインストール
`npm init` を済ませたNodeプロジェクトに必要なパッケージをインストールしていきます。

```zsh
% yarn add -D react react-dom #Reactのインストール
% yarn add -D webpack webpack-cli #Webpackのインストール
% yarn add -D typescript ts-loader @types/react @types/react-dom #TypeScript, ts-loader, Reactの型定義ファイルのインストール
```

## 2. 必要な設定ファイルの追加・編集
* webpack.config.jsの追加
* tsconfig.jsonの追加
* package.jsonの編集

をやっていきます。

### webpack.config.jsの追加
TypeScriptで書かれたReactコンポーネントをWebpackでトランスパイルできるように設定します。

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

module.exports = {
  mode: 'development',
  entry: './src/index.ts',
  output: {
    filename: 'index.js',
    path: path.join(__dirname, 'dist'),
    libraryTarget: 'commonjs2',
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modeules/,
        use: [
          'ts-loader',
        ],
      },
    ],
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
  },
}
```

`libraryTarget: 'commonjs2'`の部分が見落とされがちですが、これがないとnpmパッケージとして外から使ったときに以下のようなエラーで怒られます。

```
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
```


### tsconfig.jsonの追加

ts-loaderが.tsファイルや.tsxファイルをトランスパイルするときの設定をこちらに書きます。

```json:./tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "esModuleInterop": true,
    "module": "esnext",
    "target": "es6",
    "jsx": "react",
    "declaration": true
  }
}
```

`"declaration": true` がないと型定義ファイルが生成されないため、他のTypeScriptプロジェクトから参照するときに型定義を利用できなくなってしまいます。

### package.jsonの編集
編集する必要があるところだけ抜粋します。


```json:./package.json
{
  "name": "my-components",
  "main": "dist/index.js",
  "scripts": {
    "transpile": "webpack",
    "prepublishOnly": "yarn transpile"
  },
  "peerDependencies": {
    "react": "16.13.0",
    "react-dom": "16.13.0"
  }
}
```

nameは自由です。このパッケージを利用するときに指定する名前になるので、わかりやすい名前を付けましょう。

npmパッケージとして利用されるときの基準となるパスをmainで設定し、トランスパイルのためのコマンドをscriptsに設定します。

`yarn transpile`を実行すると、Webpackがwebpack.config.jsの設定を参照してトランスパイルしてくれます。 prepublishOnlyは`npm publish`でパッケージをnpmで公開するとき前に自動で実行されます。

また、このパッケージを使うときに前提とする依存関係として、peerDependenciesを追加します。

## 3. コンポーネントを作成
```zsh
% mkdir -p src/components
```

srcディレクトリと、その中にcomponentsディレクトリを作成します。この中に2つコンポーネントを記述していきます。

まずButton.tsx。便宜的に、tsx内でスタイルを定義して直接当てていますが、本来はCSS Modulesを利用して外部ファイルにスタイルを書くようにしましょう。

```tsx:./src/components/Button.tsx
import React, { FC } from 'react';

export type ButtonProps = {
  label: string;
}

const style = {
  backgroundColor: '#007bff',
  color: '#fff',
  border: '0',
  borderRadius: '.25rem',
  fontSize: '1rem',
}

const Button: FC<ButtonProps> = ({ label }) => (
  <button style={style}>{label}</button>
);

export default Button;
```

次にBadge.tsx。

```tsx:./src/components/Badge.tsx
import React, { FC } from 'react';

export type BadgeProps = {
  label: string;
}

const style = {
  backgroundColor: '#6c757d',
  color: '#fff',
  borderRadius: '.25rem',
  display: 'inline-block',
  padding: '.25rem .4rem',
  fontSize: '1rem',
};

const Badge: FC<BadgeProps> = ({ label }) => (
  <span style={style}>{label}</span>
);

export default Badge;
```

そして、srcディレクトリ直下に、2つのコンポーネントをexportするindex.tsを配置します。

```ts:./src/index.ts
export { default as Badge } from './components/Badge';
export * from './components/Badge';

export { default as Button } from './components/Button';
export * from './components/Button';
```

それぞれ以下のようなことをしています。

* 1行目：defult exportされている関数コンポーネントにそれぞれ名前を付けて改めてexport
* 2行目：named exportされているものすべてを同じ名前のまま改めてexport（interfaceなども外部から利用できるようにする）

これで必要なファイルは揃いました。

## 4. トランスパイル
いよいよJSに変換します。プロジェクトルートで以下を実行してください。

```zsh
% yarn transpile
```

`✨  Done in 3.48s`のように表示されれば成功です。distディレクトリが作成され、そのなかにtsxから変換されたjsファイルが入っているはずです。
また、`.d.ts`が末尾に付く型定義ファイルも一緒に生成されていると思います。

```
プロジェクトルート
  ├─dist
  │   ├─index.js
  │   ├─index.d.ts
  │   └─components
  │     ├─Badge.d.ts
  │     └─Button.d.ts
```

ここまででパッケージの作成は完了です。

あとは.npmignoreなどを用意してnpmに公開するだけです。そのあたりの説明はこちらの記事にお譲りします。

[Reactコンポーネントをnpmで公開する（GitHub Pages付き、Babel7、webpack4）](https://qiita.com/Takumon/items/945335b0e0fa035f2201)

### 5. 他のReactアプリからパッケージを参照する

4までで作成したパッケージを、外部のReactプロジェクトから参照してみましょう。`yarn link`を利用することで、実際にパッケージを公開しなくても試すことができます。

パッケージのプロジェクトルートから、以下の流れでコマンドを実行していきます。

```zsh
% yarn link
% cd ../
% npx create-react-app my-project --typescript
% cd my-project
% yarn link "my-components"
```

まず、`yarn link`でこのパッケージを別のプロジェクトから参照できるようにします。同じPCの中だけで有効です。

次に、いったんディレクトリを出てからcreate-react-appで新しいReactプロジェクトを作成します。せっかくなのでTypeScriptで作ります。

こうして作成されたプロジェクトのルートで`yarn link "パッケージの名前"`と実行すると、先ほどのパッケージを参照することができるようになります。

あとはApp.tsxを編集してパッケージを利用できるか試してみましょう。

```tsx:./src/App.tsx
import React from 'react';

import { Badge, Button } from 'my-components';

import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Badge label="My Badge"/>
        <Button label="My Button"/>
      </header>
    </div>
  );
}

export default App;
```

編集できたらブラウザで確認してみましょう。

```zsh
% yarn start
```

あんまり見栄えは良くないですが、こんなふうに表示されればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201031/725c6d4b-1852-b147-2d2f-435e51099469.png)


あとはこれにcss-loader, style-loader, sass-loader, url-loaderなどお好きなloaderを加えて使うことができます。