🦔

monorepo で開発時にパッケージの変更を build なしで他のパッケージに反映する方法

2023/11/18に公開

JavaScript のプロジェクトにおいて複数のパッケージを1つのリポジトリで管理する monorepo 形式を選択する場合があります。
monorepo 内部の各パッケージに依存関係があり、依存元の変更に応じて依存先の実装やテストを変更したときに、依存元の build を実行なしでその変更を反映するには工夫が必要です。

monorepo を採用しているプロジェクトでこの問題に対してどういう対処をしているかをまとめます。

問題点

次のような monorepo を考えます。
lib-b は lib-a に依存しています。

packages
├── lib-a
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   └── tsconfig.json
└── lib-b
    ├── package.json
    ├── src
    │   └── index.ts
    └── tsconfig.json

lib-a の package.json の main には build 後の dist ディレクトリにある index.js ファイルが指定されています。

{
  "name": "lib-a",
  "version": "0.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "rm -rf dist && tsc"
  }
}

この場合、lib-a の src/index.ts を変更だけしても lib-b には反映されません。なぜなら lib-b は lib-a の dist/index.js を参照しているため、build を実行して dist ディレクトリの中を更新する必要があるからです。

またこのときに tsserver の機能を使って lib-b から lib-a の参照にジャンプしても src/index.ts に飛ばないという問題点もあります。

解決策

問題の原因は依存元の lib-a の main に build によって生成されるファイルへのパスを書いていることです。npm publish を実行してレジストリにアップロードされるときの package.json はこの内容でなければなりません。しかし開発時は main に src/index.ts を指定して、publish 前に書き換えることでこの問題を解決できます。

monorepo を採用しているプロジェクトでの具体例を紹介します。

chakra-ui/zag の場合

git にコミットされている package.json の main フィールドには src/index.ts が指定されています。
https://github.com/chakra-ui/zag/blob/3c3a207f7d1240989f8b681437ca4e64582c362b/packages/anatomy/package.json#L36

npm scripts の prepack で clean-package を実行しています。

この clean-package の config ファイルの内容です。

https://github.com/chakra-ui/zag/blob/3c3a207f7d1240989f8b681437ca4e64582c362b/clean-package.config.json

main や module や exports などのフィールドの内容を build 後のファイルへのパスに書き換えています。

同じ chakra-ui org の chakra-ui/ark も npm scripts の before:packprepare-release.ts を実行して package.json の main フィールドを書き換えています。

mui/material-ui の場合

このリポジトリも同様に package.json の main に src/index.ts が指定されています。
https://github.com/mui/material-ui/blob/d2aeadcadf9d9265bf3f9ae347604872b046c09f/packages/mui-material/package.json#L7

npm run build を実行したときに build ディレクトリを作成して、その中に package.json を生成しています。

https://github.com/mui/material-ui/blob/d2aeadcadf9d9265bf3f9ae347604872b046c09f/scripts/copyFiles.mjs#L53

まとめ

npm publish したときのことを想定して package.json の main フィールドに build 後のファイルへのパスを書くと、monorepo 内部で依存関係が存在する場合に開発体験を損ねます。

これに対応するために開発中の main フィールドには src/index.ts のような形で build 前の TypeScript ファイルへのパスを書くと、build を実行することなく依存元の変更が反映されることを紹介しました。
いくつかのプロジェクトではこの方式を採用していて、npm publish 直前に main フィールドを書き換えていることを確認しました。

おまけ

main フィールドには dist/index.js を書いておき、tsconfig.json の paths や vite の alias で解決する方法も存在します。

他に方法を知っている方がいれば教えてください。

Discussion