開発中のnpmパッケージをローカルで参照する際に気をつけること

npmパッケージを開発中、デモページも同じリポジトリで開発してたら思わぬところでハマったので経緯と解決法を記録しておく。

# 経緯

ReactでシンプルなJSONエディタがほしくて作って

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Reactでシンプル(by Plain Text)なJSON Editorを作ったデモ。<br>入力された文字列をそのページ自体のCSS Propertiesとして使用している。<a href="https://t.co/lLXlXGmCzO">https://t.co/lLXlXGmCzO</a><a href="https://t.co/pYLba2zmTS">https://t.co/pYLba2zmTS</a><a href="https://twitter.com/hashtag/react?src=hash&amp;ref_src=twsrc%5Etfw">#react</a> <a href="https://twitter.com/hashtag/react?src=hash&amp;ref_src=twsrc%5Etfw">#react</a>-hooks <a href="https://twitter.com/hashtag/jsoneditor?src=hash&amp;ref_src=twsrc%5Etfw">#jsoneditor</a> <a href="https://twitter.com/hashtag/json?src=hash&amp;ref_src=twsrc%5Etfw">#json</a> <a href="https://twitter.com/hashtag/npm?src=hash&amp;ref_src=twsrc%5Etfw">#npm</a> <a href="https://t.co/mOIawqyiTi">pic.twitter.com/mOIawqyiTi</a></p>&mdash; 岩谷成晃 (@nariakiiwatani) <a href="https://twitter.com/nariakiiwatani/status/1292824028332224515?ref_src=twsrc%5Etfw">August 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

次回別のコンポーネントを作って公開する際にやることを最小化できるように、boilerplateとして切り出してまとめて

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">git pushでnpmパッケージとデモページを更新する（React&amp;TypeScript用のboilerplate付き） <a href="https://t.co/mJrzV9ssxA">https://t.co/mJrzV9ssxA</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; 岩谷成晃 (@nariakiiwatani) <a href="https://twitter.com/nariakiiwatani/status/1293198003293978629?ref_src=twsrc%5Etfw">August 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

せっかくなのでreact-plain-json-editorもこのboilerplateにしたがって編集しなおしたところ、デモページが動かなくなったので

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">live demoが死んでる</p>&mdash; 岩谷成晃 (@nariakiiwatani) <a href="https://twitter.com/nariakiiwatani/status/1293298713369042944?ref_src=twsrc%5Etfw">August 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

修正した！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">修正した！</p>&mdash; 岩谷成晃 (@nariakiiwatani) <a href="https://twitter.com/nariakiiwatani/status/1293355338410766338?ref_src=twsrc%5Etfw">August 12, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# 現象

デモページを開くとブラウザで下記のエラーが出現した。
（非常に丁寧なエラーメッセージですね）

```
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
    at resolveDispatcher (react.development.js:1465)
    at Object.useState (react.development.js:1496)
    at ../dist/PlainJsonEditor.js.exports.PlainJsonEditor (PlainJsonEditor.js:38)
    at renderWithHooks (react-dom.development.js:14803)
    at mountIndeterminateComponent (react-dom.development.js:17482)
    at beginWork (react-dom.development.js:18596)
    at HTMLUnknownElement.callCallback (react-dom.development.js:188)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
    at invokeGuardedCallback (react-dom.development.js:292)
    at beginWork$1 (react-dom.development.js:23203)
```

よくある原因として挙げてくれている3点のうち、1,2は大丈夫そうだったので、3が怪しい。

`3. You might have more than one copy of React in the same app`
`（意訳）Reactが複数含まれているかもしれません`

ざっくり現象の概要をまとめてしまうと、「__ライブラリ本体とデモページとで別々のReactを参照している__ので不整合が起きている」ということになるかと思う。

とりあえず結論だけ知りたい方は[こちら](#まとめ)へ。

ちなみに、現象としてはこちらのページに書かれているものとおそらく同じだが、少し違う対処をした部分もあるのでこちらはこちらで記しておく。
[Reactコンポーネントをnpmパッケージとして開発する](https://developer.hatenastaff.com/entry/2016/04/09/180000#reactはpeerDependenciesに入れる)

# 状況の整理

- ライブラリ本体の開発
    - 開発フォルダをルートディレクトリ(`/`)と呼ぶことにする
    - ビルドツールは`tsc`
    - ビルドに関連するファイルは`package.json`
- デモページの開発
    - 開発フォルダは`demo`ディレクトリ
    - ビルドツールは`webpack`
    - ビルドに関連するファイルは`demo/package.json`と`demo/webpack.config.js`

# 問題と対処

## 問題1: ライブラリ本体のdependenciesにreactが入っている

これはそもそもよくない。普通は

- 動作に必要なパッケージ => `dependencies`
- 開発に必要なパッケージ => `devDependencies`

だが、パッケージとして公開することが前提であれば、使用側との競合を避けるため

- 動作に必要なパッケージ => `peerDependencies`
- 開発に必要なパッケージ => `devDependencies`

とするべきだ。
ただし、これだけでは`peerDependencies`は`yarn install`（または`npm install`）に無視されるので、開発時にビルドできなくなる。

### 問題1への対処

- 動作に必要なパッケージ => `peerDependencies`と`devDependencies`の両方
- 開発に必要なパッケージ => `devDependencies`

とするのが良さそうだ。

## 問題2: `node_modules`と`demo/node_modules`の両方に`react`が含まれている

問題1に対処しても、まだ`react`の競合は避けられていないので、
どちらかだけに含むようにする（またはどちらかだけを参照するようにする）必要がある。

### 修正前

```json:demo/package.json(修正前/一部抜粋)
"dependencies": {
	"react": "^16.13.1"
}
```

```react:/demo/src/component/App.tsx(修正前/一部抜粋)
// ルートディレクトリを直接参照
import { PlainJsonEditor } from '../../../'
```

```js:demo/webpack.config.js(修正前/一部抜粋)
// 実際にはこれは書いていなかったが、説明のためにデフォルト値で記載。
resolve: {
	modules: ['node_modules']
}
```

これらの指定により、デモページ側では`demo/node_modules`にある`react`が参照されてしまう。

### 修正方針

実際にパッケージが使用される環境を想定すると、`react`はライブラリ側ではなくdemo側にインストールされる方が正しくはあるが、開発環境が不便になるので、__ライブラリ側だけにインストールし、demo側からルートの`node_modules`を参照できるようにする。__

### 問題2への対処

`yarn link`または`npm link`を使っておいてから`"react-plain-json-editor": "latest"`などを指定しても良いが、誰か別の人が使うときに管理する状態を極力増やさないようにしたいので今回は`"link:../"`を使った。

```json:demo/package.json(修正後/一部抜粋)
"dependencies": {
	"react-plain-json-editor": "link:../"
}
```

```js:demo/src/components/App.tsx(修正後/一部抜粋)
// dependenciesでlinkしたので、通常のパッケージをimportする時と同じ記法で使用可能
import { PlainJsonEditor } from 'react-plain-json-editor'
```

```js:demo/webpack.config.js(修正後/一部抜粋)
resolve: {
	// ルート側を優先して参照するようにする
	modules: ['../node_modules', 'node_modules']
}
```

ちなみにこの`demo/webpack.config.json`の修正後も、`demo/node_modules`に`react`が含まれていると同様のエラーが起きた。
ここはwebpackの仕様の話になるのだろうが、これ以上は調べていない。


## 問題3: `package.json`にバージョン指定がない

※今回の問題とは直接の関係はない

パッケージのリリースはGitHub Actionsに登録した`semantic-release`に任せており、バージョン番号は`semantic-release`が管理するので、`package.json`にバージョンを書くと冗長だったり公開バージョンとの間に見た目上の不整合が生じたりすると思って、`version`の指定自体を削除していた。

するとdemoの`yarn install`時に下記エラーが発生した。

```
error Can't add "react-plain-json-editor": invalid package version "".
```

### 問題3への対処

`package.json`に`"version": "0.0.0"`を追加

---

以上で問題なくデモページが動くようになった。

成果物はこちらです。
[react-plain-json-editor](https://github.com/nariakiiwatani/react-plain-json-editor)
[dev-npm-package-react-typescript-boilerplate](https://github.com/nariakiiwatani/dev-npm-package-react-typescript-boilerplate)


# まとめ

### `react`(及びライブラリ本体が他に依存するパッケージ)は`packege.json`の`peerDependencies`と`devDependencies`の両方に記載し、`dependencies`には記載しない。

- `peerDependencies`: このパッケージをinstallする際に必要に応じてWarningを出す
- `devDependencies`: 開発時のために`node_modules`にインストールされる

### `demo/node_modules`に`react`を含めない

- `demo/package.json`の`dependencies`に`react`を書かない

### デモページのビルド時にルートの`node_modules`を参照するようにする

- `demo/webpack.config.js`の`resolve.modules`に`['../node_modules', 'node_modules']`を指定する
    - `../node_modules`: ルートディレクトリ側の`node_modules`のこと
    - `node_modules`: デモページ側の`demo/node_modules`のこと

### デモページから開発中のライブラリを使用できるようにする
- `demo/package.json`の`dependencies`に`"react-plain-json-editor": "link:../"`を記載
- または`yarn link`を設定後`"react-plain-json-editor": "latest"`を記載
    
```shell:ex)yarn/linkを使う方法
# まずはルートディレクトリで
yarn link # このパッケージをlinkコマンドで参照可能にする(`~/.config/yarn/link`にシンボリックリンクが貼られる)
cd demo # demoへ移動
yarn link react-plain-json-editor # linkで登録したパッケージをこのプロジェクト(demo)で使えるようにする
```


# 参考にしたページ

[Reactコンポーネントをnpmパッケージとして開発する](https://developer.hatenastaff.com/entry/2016/04/09/180000#reactはpeerDependenciesに入れる)
[Yarnでローカルのパッケージをaddする方法](https://qiita.com/suin/items/409dc019de605830b4c2)
[npm linkの基本的な使い方まとめ](https://qiita.com/103ma2/items/284b3f00948121f23ee4)
[0000-link-dependency-type.md](https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-link-dependency-type.md)
[依存関係の種類](https://classic.yarnpkg.com/ja/docs/dependency-types/)
[Webpackを使う時は、resolve.modulesの設定に気を付けよう。(特にModule not found: Error: Can't resolve～が表示された時)](https://qiita.com/toris-birds/items/c0874a66865f7e3a09ee)
[Resolve](https://webpack.js.org/configuration/resolve/)