React Hooksのライブラリ開発(web向け)をしたい人のためのテンプレート
TL; DR
React HooksのCustom Hooksをnpmのライブラリとして切り出して開発したい人用のGitHub Repository Templateです.スターもらえるとめっちゃ喜びます.
対応してるもの
- TypeScript (
.ts
,.tsx
) - React v17+ (jsxの変換の都合上)
- Jestでのunit test
- その他諸々 (下記参照)
概要
Reactを触ったことのある方なら,Hooksはご存知だと思います.
一応めちゃくちゃざっくりとした解説をいれておくと,useXXX
みたいなやつです.知らない方は1度触ってください.これはお願いです.
そして,useXXX
みたいなのは自分でも定義できます.これをCustom Hooksといいます.
Custom Hooksでいい感じにまとめられるようになると,コードの見通しが一気にすごくよくなるので積極的に使いましょう.
そして,この “いい感じにまとめる” のがうまくなってくると,もはやプロダクトを越えて共有したくなってきます.そういう例はこのへんにたくさんあります.
しかし,npmへのライブラリの公開というのは,プロダクトをつくることとはまた別次元の難しさがあります. (実は今回のものはCDまでは対応していません.Kitをnpmに公開したくないため,それに関してはまた違うところでやるためです.構想は下のほうにあるので,そのへんは各々でやってみてください.気が向いたら私もテンプレートとしてコードを追加するかもしれません.)
実は,私はいくつかこうしたKitをprivateで持っています.今回その1つの hookskit
を公開したのは,npmのライブラリ作成に関する公開されている知見が本当に少ないと感じたためです.
本記事では,その有象無象 (私の甘い理解含む) と紆余曲折を記すことで,なにか少しでも役に立てば,と思い書くことにしました.
なお,GitHubでTemplateとして公開しているのは,単純にCRAのようなものがあまり好きではないというだけのことです.
説明
ビルドツール
最初これを作り始めたときはtsc
にすべて頼っていました.しかしtsc
はbuild後の成果物のpaths
とbaseUrl
を解決してくれないので,私の好む
import { useExample } from '/example'
みたいな /
で始まるスタイルをうまく解決することができませんでした.
いろいろと見て回ったところ,tscを最後のbuildに使う世の中のライブラリの大体が
- tscを使う場合,相対パスでのimportでやりくりしている
- 絶対パスでのimportにする場合,tscを最終buildに使っていない
のどちらかのパターンでした.
また,tscでbuildする場合,ESMやCJSスタイルでの書き出しをするのが一度にできないため結局いろいろと面倒な点があり,違う方法を探すことにしました.
そこで,次はesbuild
を使うことにしたのですが,esbuild
は型情報を落としてしまうだけなのでd.ts
を生成するには結局tsc
を使う必要があります.すると結局ビルド速度が速い利点が霞んでしまうのと,ビルドスクリプトもTypeScriptで書きたいのでそうするとts-node
を使ってビルドスクリプトを走らせる必要がありますが,そうすると (なんらかの理由で) tsconfigを3つか4つ持っておく必要が出てきてしまい,これでは面倒だなということでesbuild
を外しました.
途中でtsdxも使いましたが,ちょっとほしいものと違ったのでやめました.
結果,microbundle
を使いました.このツールについては日本語で唯一?の文献がこれです.
元々preact
のためのbuildツールで,package.json
の中の設定をみて書き出してくれます.import pathのresolveなども勝手にやってくれるのでいいのですが,現状
- 複数のinputファイル → 複数のoutputファイルができない
-
d.ts
の書き出しが完全にtsc
依存
であるので,必ずindex.ts
のようなものを持っておく必要があります.
そしてmicrobundle
の注意点として,d.ts
に関してはtsc
に完全依存であるためpackage.json
のtypes
にindex.d.ts
と書いていても,source
がexport.ts
のような場合,吐き出されるのはexport.d.ts
です.ここが結構ハマりポイントでした.
またtsc
にはconcatenate機能もないため (module
オプションがamd
かsystem
のときはoutFile
が使えるが今回はだめ),要するにsrcの中のtsファイルそれぞれに対応する形でd.ts
が吐き出され,d.ts
内のimport pathがresolveされず,importする側で期待する挙動をしません.
この問題は結局の所webpack
やrollup
を使ってpluginでどうにかするか,ttypescript
を使うしかないのですが,microbundle
の作者はttypescript
の導入には反対のようで (わかる),こうなるともう後付けでやるか,webpack
やrollup
のconfigを書くしかありません(やりたくない).
ここで使えるのがtsc-alias
です.
これはtsc
の成果物に対してimport pathをtsconfig
を見て解決してくれます.はっきり言って神です.この作者のGitHub Sponsorがオンだったら速攻でSponsoringしてました.
これで晴れてビルドできるようになりました.
テスト
私はGoを書くときはTDDが大好きですが,フロントのHooks以外の開発ではTDDはあまり使いません.理由はUIまわりのテストは労多くして功少なしな気がしてしまうからなのですが (なのでやるとしたらVRTとかでしょうか),この辺はまたの機会に.
一方,HooksはUIと疎結合にしてあればテストしやすいはずです.ただし,ものによってはpreconditionを用意しにくいため,このへんはケースバイケースでしょう.
というわけで,unit testは書いていきたいのでJest
を使うことにしました.src
ディレクトリに*.test.{ts,tsx}
の形で書いていきます.
ここで若干面倒だったことが2点あり,
-
d.ts
の生成にtsc
を使っているため,build時にtestの型情報が生成されないようにするにはtsconfigのexclude
にtest
を除外するよう指定する必要がある.しかしそうすると開発時にtest
をかけないため,build用のtsconfigと分ける必要がある.(私はすでにオプションが多すぎるので分けていたので問題なし) -
microbundle
にはpackage.json
に"type": "module"
を記述するようにとある(ESMをデフォルトにするオプション)が,これをするとts-jest
が動かない (https://github.com/facebook/jest/issues/9430 このへんに解決策の提案は色々あるが,現状自分の環境ではどれも動かなかった).package.json
の"type": "module"
を外してもとりあえずmicrobundle
の動作には問題がなさそうなので,外すことで対応
というわけで,unit testは書けるようになりました.
ただし私の経験上,npmのライブラリ開発は大抵importしてみないと見えないエラーも多々ある (実はうまくimportできない,d.tsが適切ではないなどなど) ので,絶対にimportしてみるテストは書いたほうがいいです.
最低限ということで,今回はexamples
ディレクトリにbasic
というごく単純な例をおき実験しています.本来ならbuild可能を確かめるだけでなく,こっち側でもJestを使い,最低限renderできるかはテストすべきだが今回は省いてます.
なお,このexampleをtestに使うのはGoの発想です.
今回はどちらかというとintegration testっぽいけれども.
この “importしてみる” には,yalc
を使ってます.yarn link
だとエラーが出るようになってしまったのと,より実際の状況に近づけるためです.
これはとくに引っかかる点なくできると思うのでとくに解説はありませんが,敷いて言えば,今はまだpublishingしていないためyalcをgitに含めてます.publishing後はできればexampleの中のdependencyをnpmの方に向けて,yalc link
でGitにはyalc
を含めなくていいようにしたいです (とはいえyalcは必要なのでpackage.jsonのdependencyには残るはず).
継続性
最後に継続性の話です.
私のKit全般に言えることなのですが,必ずRenovatebotを入れるようにしています.これはdependencyを最新にするPRを自動的に作成するサービスで,templateに限らず入れておいたほうがいい物ですが,特にtemplate repoに入れておくことでUse Templateしたときにいつでも最新である安心感があります.
さらにdependencyを最新にしたときに落ちないことを確認するため,GitHub Actionsでテストをかけています.これはもちろんRenovatebotのPRに対しても有向なため,今後Reactがアップデートしたときも安心です.
GitHub Templateの残念なところとして,Repoを作ったあとTemplateに入ったアップデートは自動的に反映されません (そりゃそう).
また,Templateも気をつけていないとすぐに廃れてしまいます (よくあるのが手元の開発中のものだけでeslintの設定を変えて,templateには反映しないとか).
今回はこれに対する解決策ではないですが,なるべくTemplateを継続したいので,せめてRenovatebotを利用しているところです.
いずれ,templateとの差分を出す機構とかを作っていきたい所存です.
あとは,JavaScriptがGitHubの
ここに出るのが嫌だったので,eslintのconfigやprettierのconfigは全部yamlで書いてます.
CD
今回は書いてないですが,CDに関して,
- npmのtokenをGitHubのSecretsに書く
- ローカルの手作業でpackage.jsonのversionをあげる.
- リリースすると決まったら,tagを打つ (
v1.0.0
とか) - tagのpushがあったら,GitHub Actionsで
- tagのバージョンとpackage.jsonのバージョンが一致するか確認
-
can-npm-publish
でpublishできるかチェック - OKだったらpublish
というフローが1番いいと思ってます.
昔はmasterにmergeされたら〜とかがいいと思ってたんですが,あっ…無理…ってなったのが,
これです.できればpackage.jsonのバージョンを外部から埋め込む,とかすればいいんですけどねぇ… 開発するときに支障が出そうでやってないです.
わからないこととか
- なんとなくRenovatebotでいつもやるようにPin Dependencyしてるんですが,ライブラリ用途だとこれはやめておくべきな気がしてます (その場合はRenovatebotはどう更新するんだろう,lockだけの更新になるのかしら…) → 書いてありました https://docs.renovatebot.com/dependency-pinning/ とはいえfirebaseのsdkとかはpinしてるんですよね.
- 次にやりたいことに,ReactのUIライブラリの作成のためのtemplate作成があります.そこではjsxの変換をemotionを使う予定なのですが (
css
propsを使いたい),emotionをdependencyとするべきなのか,peerDependencyとするべきなのか…. - Tree Shakingがあるから1ファイルにbundleしていいのか,それともファイルを分割すると利点があるのか (例:
@material-ui/core/Button
的な) - どこまでbabelかけるべきなのか.IEとかで動かしたい用途の場合はプロジェクトもとでnode_modules向けにbabelをかけるべきだとは思うが,どうサポートすればいいのかが難しい.この辺あまり深く考えたことがないのでちゃんと調べたい
Discussion