pnpm + Turborepo + Expo で環境構築
はじめに
筆者は最近 TypeScript + モノレポ(というか Turborepo)の構成にハマっており、モノレポにする必要があるかまだ分からないプロジェクトでもとりあえず Turborepo でモノレポ構成で作り始めるようにしています。
今回、サイドプロジェクトで「モバイルアプリはマスト。できれば Web アプリもほしい」という要件が降ってきたのでどんな感じで作るか考えていたのですが、ふと「Turborepo + Expo + Next.js という技術スタックで作れば Web アプリとモバイルアプリ(React Native)でコンポーネント共有したりできて生産性最強なんじゃね!?」と思ったので開発環境の構築をしてみたログです。
この記事のゴールは Expo Go 上でモバイルアプリを立ち上げてみるところまでですので、実際に開発を進めていくとさらにいろいろ調整が必要になる可能性が高いですが、そこまではカバーしていません。あらかじめご了承ください。
手順
大まかな手順は次の通りです:
- pnpm + create-turbo で Turborepo のセットアップをする
- create-expo-app でモバイルアプリのパッケージを作成する
- Metro Bundler の設定を調整する
- モバイルアプリのエントリポイントを変更する
- pnpm の依存関係巻き上げ設定を調整する
以下の Turborepo と Expo のドキュメントを総合した感じになります:
pnpm + create-turbo で Turborepo のセットアップをする
Turborepo + Next.js の構成については Trurborepo の公式サイトにある create-turbo というツールを使うことで難なくできます。
プロジェクトのディレクトリを置きたい場所で npx create-turbo@latest
を実行し、プロンプトに適当に答えれば、2つの Next.js アプリと共有パッケージのサンプル + ESList 等が設定されたプロジェクトが作成されます。
詳しい手順は上記の記事を参照してください。
.
├── .eslintrc.js
├── README.md
├── apps
│ ├── docs
│ ├── mobile
│ └── web
├── package.json
├── packages
│ ├── eslint-config-custom
│ ├── tsconfig
│ └── ui
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
現時点で上記のようなディレクトリ構造になっていると思います。
create-expo-app でモバイルアプリのパッケージを作成する
create-expo-app
コマンドを使って apps
ディレクトリ内に Expo アプリのパッケージを作成します。
手順は以下の公式サイトに従います:
公式サイトでは npx
が使用されていますが pnpm dlx
を使用することもできるので、ここでは pnpm dlx
を使ってみます:
pnpm dlx create-expo-app apps/mobile
引数にパスを渡すことで指定したディレクトリにアプリを作成することができるので、今回は apps/mobile
にしています。
Metro Bundler の設定を調整する
このまま pnpm start
して Expo アプリを立ち上げたいところですが、立ち上げても node_modules
の依存関係が解決できないというエラーが発生します。
このエラーとその解決方法については上記のドキュメントに記載されており、どうやら Metro Bundler という React Native 用のバンドラー(正直よく分かっていません)がデフォルトでは monorepo に対応していないため、ワークスペースルートの node_modules
とプロジェクトルートの node_modules
それぞれにインストールされたライブラリをうまく見つけることができず起きるようです。
よって、Metro Bundler がプロジェクトルートとワークスペースルートそれぞれの node_modules
にインストールされているパッケージを解決できるように設定をカスタムする必要があります。
手順は簡単で apps/mobile
に metro.config.js
という設定ファイルを作成し、先掲のページに載っている設定例をまるごとコピーします。
// Learn more https://docs.expo.dev/guides/monorepos
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
// Find the project and workspace directories
const projectRoot = __dirname;
// This can be replaced with `find-yarn-workspace-root`
const workspaceRoot = path.resolve(projectRoot, '../..');
const config = getDefaultConfig(projectRoot);
// 1. Watch all files within the monorepo
config.watchFolders = [workspaceRoot];
// 2. Let Metro know where to resolve packages and in what order
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;
module.exports = config;
ここで設定していることととしては以下の3点のようです:
- ワークスペース全体のファイルの変更を監視する
- プロジェクトルート (
apps/mobile/node_modules
) とワークスペースルート (node_modules
) それぞれのディレクトリから依存関係を解決する - パッケージ毎にインストールされた依存パッケージのバージョンが異なる場合でも、ワークスペースルートの依存関係を使わないようにする
それぞれの詳細は先掲のページに詳しく解説されていますので、必要に応じて確認してください。
モバイルアプリのエントリポイントを変更する
これで起動できるかと思いきや、今度は「エントリポイントが見つからない」というエラーが発生します。
apps/mobile/package.json
を見てみると、デフォルトのエントリポイントは node_modules/expo/AppEntry.js
となっています。
なぜこうなっているのかはよく分かりませんが、monorepo ではこのエントリポイントがうまく解決できないようですので apps/mobile
ディレクトリ内に手動で作成して変更します。
多分名前は何でも良いと思いますが、上記ドキュメントに従い apps/mobile/index.js
というベーシックな名前で作成しました。
例によって中身は Expo のドキュメント から丸コピです:
import { registerRootComponent } from "expo";
import App from "./App";
// custom entrypoint for monorepo
// see: https://docs.expo.dev/guides/monorepos/#change-default-entrypoint
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
pnpm の依存関係巻き上げ設定を調整する
さて、今度こそ!と思うのですが、最後に1つ pnpm 特有の設定変更が必要です。
pnpm はデフォルトで依存関係の実体を node_modules/.pnpm
ディレクトリ内に配置し、各ワークスペース内の node_modules
には実体へのシンボリックリンクを配置することでデータ量を削減したり、プロジェクトの package.json
にない依存パッケージへのアクセスを禁止したりできるという革新的な仕組みになっています。
が、しかし、、、残念なことに React Native はそのようなディレクトリ構造をサポートしていないらしく、pnpm の動作を npm と同じものに変更する必要があります。せっかく pnpm なのに...という気持ちはありますが、とりあえず動かしたいので涙をのんで以下のコマンドを実行します:
# in the workspace root
pnpm config set node-linker hoisted
上記コマンドは .npmrc
という設定ファイルに設定を書き込むもので、手作業で .npmrc
ファイルを以下の内容で作成するのと同じです:
node-linker=hoisted
node-linker
というオプションの詳細は pnpm のドキュメントを参照してください。この設定により、すべての依存関係がワークスペースルートに巻き上げられるみたいです。
最後に node_modules
を再構築するため、以下を実行します:
pnpm install
Expo を起動させてみる
ちょっと長い道のりでしたが、これでやっと Expo アプリを起動できます 🎉
apps/mobile
ディレクトリ内で pnpm start
を実行します。
問題なければ Metro が起動し、QR コードが表示されると思います。
iOS デバイスにインストールした Expo Go アプリ内で mobile
アプリを起動させ App.js
のテキストを編集してみます:
\ジャーン/
ホットリロードも問題なく動いているようなので、本記事のゴールは達成です!やったね!!
まとめ
この後は Tailwind を使って Next.js と モバイルアプリ間でコンポーネントのスタイルごと共有したいとかさらなる野望もありますが、ちょっと調べた感じそれはまた大変そうです。おとなしくモバイルアプリは Flutter とかで独立させて開発した方がかえって簡単だったりするかもしれませんが、まあ、とりあえずやってみます...
Expo というか React Native は Next.js ほどモノレポ対応に注力していない(特に pnpm を使う場合は)ようで少しいろいろ対応することがあったのですが、ネット上には日本語の情報があまりなかったので、本記事がどなたかの参考になれば幸いです!
Discussion