React Component な npm のパッケージを作って TypeScript のプロジェクトで使う

# はじめに

TypeScript のプロジェクトでいい感じのテキストフィールドをatomsディレクトリの下に用意して作っていましたが、機能が多くなったり、コンポーネントが分かれてくるとmoleculesに移動させたりと、めんどくさいので、npmのパッケージ化してしまえと思ったことが始まりです。

今回は create-react-app で作成したプロジェクトに取り込ませて、実際に私が行った、npmのパッケージ開発方法、手順を紹介します。

# パッケージを取り込む用のプロジェクトを作る

とりあえず [create-react-app](https://create-react-app.dev/docs/adding-typescript/) でベースのプロジェクトを作成します。

ベースのプロジェクトが存在する方は不要です。

```sh
$ yarn create react-app my-app --template typescript
$ cd my-app
$ yarn start
```

![スクリーンショット 2020-04-11 14.21.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/cf3b9bc6-a5cc-db8d-135b-4840dc525a58.png)

おなじみの画面が表示されましたね。

この画面の真ん中くらいにいい感じのテキストフィールドを表示しながら開発していきましょう。

### npm のパッケージの雛形を作成

`npm`のアカウント作成は[3分でできるnpmモジュール](https://qiita.com/fnobi/items/f6b1574fb9f4518ed520)を見ながら作りました。

ディレクトリと雛形を作成します。

```sh
$ mkdir react-custom-text-field
$ cd react-custom-text-field
$ yarn init
yarn init v1.19.0
question name (react-custom-text-field): 
question version (1.0.0): 0.0.1
question description: 
question entry point (index.js): ./dist/index.js
question repository url: 
question author: pure-adachi
question license (MIT): 
question private:
```

`version`は開発なのでメジャーバージョンは`0`です。
とりあえず`0.0.1`にしておきました。

`entry point`はビルドで生成する予定のファイルです。

## 必要なモジュールをインストール

- React関連

```sh
$ yarn add -D react react-dom @types/react @types/react-dom
```

- tscでコンパイルするので

```sh
$ yarn add -D typescript
```

## package.json

ビルドのコマンドを用意します。

```json:package.json
"scripts": {
  "build": "tsc"
}
```

## tsconfig.json

`tsc`でのコンパイルの設定を用意します。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}
```

## メインのファイルを作成

```tsx:src/index.tsx
import React from "react";

const ReactCustomTextField = () => {
  return (
    <div>
      <input type="text" />
    </div>
  );
};

export default ReactCustomTextField;
```

とりあえずベースとして、シンプルな入力フィールドにしました。

# ビルド

```sh
$ yarn build
```

`dist`ディレクトリが出来て、その中に`index.js`ファイルが生成されれば成功です。

```sh
$ tree dist/
dist/
└── index.js
```

# パッケージをプロジェクトに取り込み

ベースのプロジェクトと、開発パッケージのディレクトリ構成を以下のように配置しています。

```sh
$ tree
.
├── my-app
〜〜〜
〜〜〜
└── react-custom-text-field
〜〜〜
〜〜〜
```

最初に `create-react-app` で作成したプロジェクト側で、 `react-custom-text-field` を取り込みます。

ローカルにあるパッケージなので、ディレクトリ指定で取り込めます。

```sh
$ yarn add ../react-custom-text-field/
```

`App.tsx`に導入

```diff:App.tsx
 import React from "react";
 import logo from "./logo.svg";
 import "./App.css";
+import ReactCustomTextField from "react-custom-text-field";

 function App() {
   return (
     <div className="App">
       <header className="App-header">
         <img src={logo} className="App-logo" alt="logo" />

+        <ReactCustomTextField />

        <p>
           Edit <code>src/App.tsx</code> and save to reload.
         </p>
         <a
           className="App-link"
           href="https://reactjs.org"
           target="_blank"
           rel="noopener noreferrer"
         >
           Learn React
         </a>
       </header>
     </div>
   );
 }

 export default App;
```

パッケージの型定義していないので、エラーが発生しました。

```sh
my-app/src/App.tsx
TypeScript error in /my-app/src/App.tsx(4,34):
Could not find a declaration file for module 'react-custom-text-field'. '/my-app/node_modules/react-custom-text-field/dist/index.js' implicitly has an 'any' type.
  Try `npm install @types/react-custom-text-field` if it exists or add a new declaration (.d.ts) file containing `declare module 'react-custom-text-field';`  TS7016

    2 | import logo from "./logo.svg";
    3 | import "./App.css";
  > 4 | import ReactCustomTextField from "react-custom-text-field";
      |                                  ^
    5 | 
    6 | function App() {
    7 |   return (
```

# 型解決をパッケージ側でする

## `.d.ts`ファイルを作成

ルートディレクトリに`types`ディレクトリを作成して、その下に`.d.ts`ファイルを作成します。

```ts:types/react-custom-text-field/index.d.ts
declare const _default: React.ComponentType;
export default _default;
```

## 型ファイルを参照させる

`package.json`に型ファイルのパスを追加します。

```json:package.json
"types": "./types/react-custom-text-field/index.d.ts"
```

## 取り込んでいるプロジェクトでインストールし直す

```sh
$ yarn remove react-custom-text-field
$ yarn add ../react-custom-text-field/
$ yarn start
```

![スクリーンショット 2020-04-11 17.12.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/9f5492ac-85ba-ca61-ac9e-e43b680db7ed.png)

テキストフィールドが表示されました。

# パッケージの更新を監視できるようにする

パッケージの編集をして、プロジェクト側でインストールし直すのはかなりめんどくさいです。

パッケージ側のファイルを保存した際に、自動でビルドされ、ビルドで変更されたら、プロジェクト側にも反映されるようにします。
（`node_modules`内にインストールされた`react-custom-text-field`が開発しているディレクトリのシンボリックリンクになるイメージ）

## パッケージ側の設定

ウォッチオプションをつけた`tsc`コマンドを実行できるようにします。

```json:package.json
"scripts": {
  "build": "tsc",
  "build:watch": "tsc -w"
}
```

`yarn link`を実行して、このパッケージにリンク付可能とさせます。

```sh
$ yarn link
yarn link v1.19.0
success Registered "react-custom-text-field".
info You can now run `yarn link "react-custom-text-field"` in the projects where you want to use this package and it will be used instead.
✨  Done in 0.05s.
```

ソースの変更を監視するビルドを実行させておきます。

```sh
$ yarn build:watch
```

## プロジェクト側の設定

パッケージをシンボリックリンクさせる

```sh
$ yarn link react-custom-text-field
```

`node_modules`の中身を見てみるとシンボリックリンクが作成されていることがわかります。

```sh
$ ls -l node_modules/ | grep react-custom-text-field
lrwxr-xr-x    1 adachi  staff     56  4 11 21:46 react-custom-text-field -> ../../../../../.config/yarn/link/react-custom-tex
t-field
```

## ホットロードの確認

```sh:プロジェクト側
$ yarn start
```

と

```sh:パッケージ側
$ yarn build:watch
```

が起動されている状態で、パッケージファイルの更新をします。

```diff:src/index.tsx
 const ReactCustomTextField = () => {
   return (
     <div>
-      <input type="text" />
+      <input type="text" value="hoge" />
     </div>
   );
 };
```

![スクリーンショット 2020-04-11 21.52.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/9ee6df56-f45c-a890-4bb5-4e084989f66e.png)

勝手に反映されました。

# リリース 第１段

コマンド打つだけです。

```sh
$ yarn publish
yarn publish v1.19.0
[1/4] Bumping version...
info Current version: 0.0.1
question New version: 0.0.1
[2/4] Logging in...
[3/4] Publishing...
success Published.
[4/4] Revoking token...
info Not revoking login token, specified via config file.
✨  Done in 8.34s.
```

![スクリーンショット 2020-04-11 22.00.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/bc868d30-7864-7fdc-e6c1-40c708041d42.png)

`npm`のサイトで確認できました。

# リリースしたパッケージをプロジェクトで使う

ディレクトリ指定でインストールしていたパッケージを`npm`からインストール

```sh
$ yarn unlink react-custom-text-field
$ yarn add react-custom-text-field
$ yarn start
```

コンパイルに失敗しました。

```sh
Failed to compile.

./src/App.tsx
Module not found: Can't resolve 'react-custom-text-field' in '/my-app/src'
```

実際にインストールされたファイルを確認してみます。

```sh

$ ls -l node_modules/react-custom-text-field/
total 24
-rw-r--r--  1 adachi  staff   26  4 11 21:59 README.md
-rw-r--r--  1 adachi  staff  439  4 11 21:59 package.json
drwxr-xr-x  3 adachi  staff   96  4 11 22:05 src
-rw-r--r--  1 adachi  staff  370  4 11 21:59 tsconfig.json
drwxr-xr-x  3 adachi  staff   96  4 11 22:05 types
```

`dist`ディレクトリがない。。。。

ここに`dist`ディレクトも含まれるようにパッケージ側を修正します。

# リリース 第2段

`package.json`に`files`を追加

```json:package.json
"files": [
  "dist"
]
```

そしてリリース

```sh
$ yarn publish
yarn publish v1.19.0
[1/4] Bumping version...
info Current version: 0.0.1
question New version: 0.0.2
info New version: 0.0.2
[2/4] Logging in...
[3/4] Publishing...
success Published.
[4/4] Revoking token...
info Not revoking login token, specified via config file.
✨  Done in 6.86s.
```

# もう一度インストールしなおして動作確認

```sh
$ yarn remove react-custom-text-field
$ yarn add react-custom-text-field
$ yarn start
```

見覚えのあるエラーが出ました。。。

```sh
my-app/src/App.tsx
TypeScript error in /my-app/src/App.tsx(4,34):
Could not find a declaration file for module 'react-custom-text-field'. '/my-app/node_modules/react-custom-text-field/dist/index.js' implicitly has an 'any' type.
  Try `npm install @types/react-custom-text-field` if it exists or add a new declaration (.d.ts) file containing `declare module 'react-custom-text-field';`  TS7016

    2 | import logo from "./logo.svg";
    3 | import "./App.css";
  > 4 | import ReactCustomTextField from "react-custom-text-field";
      |                                  ^
    5 | 
    6 | function App() {
    7 |   return (
```

`node_modules/react-custom-text-field`の中身を確認してみます。

```sh
$ ls -l node_modules/react-custom-text-field/
total 16
-rw-r--r--  1 adachi  staff   26  4 11 22:15 README.md
drwxr-xr-x  3 adachi  staff   96  4 11 22:15 dist
-rw-r--r--  1 adachi  staff  468  4 11 22:15 package.json
```

むしろ`dist`しかない。。。

発生したエラー、最初に見たエラーと同様、型定義されてないエラーですね。

`types`ディレクトリに型があるので、`types`も含まれるように修正します。

# リリース 第3段

`package.json`に`files`を追加

```json:package.json
"files": [
  "dist",
  "types"
]
```

そしてリリース

```sh
$ yarn publish
yarn publish v1.19.0
[1/4] Bumping version...
info Current version: 0.0.2
question New version: 0.0.3
info New version: 0.0.3
[2/4] Logging in...
[3/4] Publishing...
success Published.
[4/4] Revoking token...
info Not revoking login token, specified via config file.
✨  Done in 6.64s.
```

# 更にもう一度インストールしなおして動作確認

```sh
$ yarn remove react-custom-text-field
$ yarn add react-custom-text-field
$ yarn start
```

![スクリーンショット 2020-04-11 22.23.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/0307316b-7641-559f-3c4d-8c1f049cad3d.png)

ようやく表示されましたね。

# 終わりに

今回作成したnpmのパッケージとGitHubのリンクはこちらです。

- [react-custom-text-field - npm](https://www.npmjs.com/package/react-custom-text-field)
- [react-custom-text-field - GitHub](https://github.com/etiopiamokamame/react-custom-text-field)

ゴリゴリ開発する部分を楽しみにしていたので、環境を整える部分でずっこけまくり、開発着手するときにはウキウキ感は薄くなってました。

`@types/react-custom-text-field`でインストールさせる方法は[DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/)にPull Request投げたり、取り込んでもらうまで時間がかかったりと大変です。
自分で解決できるようにする方法があったのでこの方法でやりました。

`yarn publish`する度に、勝手にそのバージョンでコミットされて、タグも生成されるので、`Github`等へのプッシュ（タグも）はかなり楽でした。
(`yarn pusblish` → `git push origin [ブランチ名]` → `git push origin --tags`して、後は`GitHub`でぽちぽち、かたかたするだけ)

# 参考文献

- [3分でできるnpmモジュール - Qiita](https://qiita.com/fnobi/items/f6b1574fb9f4518ed520)
- [TypeScript チュートリアル - Qiita](https://qiita.com/EBIHARA_kenji/items/31b7c1c62426bdabd263)
- [TypeScriptプロジェクトに独自の型定義を配置する方法 - Qiita](https://qiita.com/mtgto/items/e30d1529ca298e49557e)
- [TypeScript で型定義ファイル( d.ts )がないときの対処法 - Qiita](https://qiita.com/Nossa/items/726cc3e67527e896ed1e)
- [ちゃんと使い分けてる? dependenciesいろいろ。 - Qiita](https://qiita.com/cognitom/items/acc3ffcbca4c56cf2b95)
- [「セマンティック バージョニング」を読んだのでバージョニングについてまとめた - Qiita](https://qiita.com/soyanchu/items/72095a7c9fdb7f71b2c3)