Open11

既存の Next.js 製アプリを Turborepo を使った monorepo に移行するメモ

izuminizumin

背景

  • 砂場代わりのポートフォリオサイトを Next.js で作っている
  • ここでよりオーバーエンジニアリングしていくにあたって、構成変更(バックエンド増やすとか、へんなプロトコルで通信させるとか)を気軽にできるようにしたい
    • → monorepo 化だ!
  • 現状でも一部のロジックを CLI とサイト本体で共有するために yarn workspace を使った雑 monorepo になっているが、ビルド時の依存関係の管理とかキツい
izuminizumin

現状

  • ルートディレクトリが Next.js アプリのパッケージになっている
  • packages/ 以下にいくつかサブパッケージがある

理想の状態

  • apps/ 以下にアプリケーション(デプロイするやつ?)がある
  • packages/ 以下にいくつかサブパッケージがある
  • (将来的には Node.js 以外のコードも置きたいが、初手は諸々 Turborepo の doc に合わせて移行プロセスをシンプルにする)
izuminizumin

そもそも、なにが実現したい?

  • 雑にマイクロサービスを切り出したい
    • 技術的な検証
    • アーキテクチャの最適化
      • e.g. 動的な OG 生成のために puppeteer を動かす必要があり、Next.js アプリに chrome が入ってる → ビルド・デプロイが遅くなるので分けたい
  • 処理や設定を雑に共通化したい
    • e.g.
      • tsconfig, jest 等をいい感じに共通化して管理する
      • ブログ記事のマークダウンを処理するロジックを webapp と CLI で共通化する
izuminizumin

ルートパッケージの Next.js アプリを apps/ 以下に移動

今回はシンプルに apps/web とした

  • apps/web に移動
    • .env*
    • .babelrc
    • jest.config.js
    • tsconfig.json
      • tsconfig.base.json とかを切り出していた場合はパスの修正が必要
    • GraphQL まわり: VSCode Extension の設定, graphql-codegen の設定
      • サーバから introspection をダンプしてる場合、apps/web 以下に置くほうが楽かな?
    • デプロイ周り: Dockerfile, .dockerignore
    • src, public, next-env.d.ts
    • その他 Next.js アプリを動かすのに必要なファイル
      • e.g. ブログ記事の markdown file, sentry の設定ファイル
    • 開発支援のスクリプトで、Next.js アプリ用のものがあれば
  • ルートと apps/web それぞれに置くように分離
    • .gitignore: Next.js 由来のものをルートから移動
    • package.json
      • パッケージ名を変えて apps に移動
      • リポジトリ内で広く使うツールの devDependencies(jest, prettier 等)はルートに残す
      • workspaces も設定変更が必要
izuminizumin

(冷静に考えると、この時点で turbo 入れたりせず main branch に取り込んでおけばよかった…)

izuminizumin

Turborepo 導入

  • https://turborepo.org/docs/getting-started/existing-monorepo を参考に突っ込んでいく
  • pipeline の設定について
    • キャッシュに関する設定は後回しでも動くので、inputsoutputs は一旦無視
    • 各タスクの dependsOn だけ設定して、yarn devyarn test で依存関係を見てサブパッケージのビルドとかやってくれれば成功
izuminizumin

Dockerfile 修正

  • アプリごとに必要な依存が変わるケースもあるので、理想的には Dockerfile はアプリごと(→ apps/<app> ごと)に置きたい
    • e.g. puppeteer 使うアプリにだけ chrome いれたい
  • が、いずれのアプリもビルドのためにルートの package.json や yarn.lock が必要
    • ひとつのアプリをビルドしたいだけなのに全体のビルドが走る(yarn install も全体分で走る)ので悲しい
  • Turborepo 0.4.0 からこの問題をいい感じにする turbo prune なる機能が入ったので使える
  • purne を使う・使わないに関わらず、docker build 時のパラメタは以下のようになる:
    • context: . (root dir)
    • file: ./apps/<app>/Dockerfile
izuminizumin

.git の対応

turbo prune.git に依存するらしい
(ちゃんと調べてないけどキャッシュのため?)

buildkit を使ってる場合、デフォルトでは .git を無視するらしいのでちょっと手を加えないといけない

https://github.com/docker/build-push-action/issues/513

なんか環境変数指定するか、GHA で build-push-action つかってる場合は context: . を入れてあげるといいらしい

(余談: build-push-action が暗黙的に checkout してるのをここで初めて知った。いままで無駄に checkout していた…)

izuminizumin

Next.js の standalone build を使ってるときの追加対応

Next.js の standalone build を使ってるとき、実はパッケージを移動した時点でぶっ壊れている。
現状(next@v12.3.0)では standalone build は Next.js アプリの package.json があるディレクトリをルートとしてファイルを集めるが、monorepo の場合でそれをやると node_modules が対象外になってしまう
なので、experimental.outputFileTracingRoot なるオプションを指定してルートの位置を動かさないといけない。

https://nextjs.org/docs/advanced-features/output-file-tracing

Dockerfile の最終ステージでファイルを COPY する先も変わる

# Copy build artifacts
COPY --from=builder /app/apps/web/.next/standalone /app/
COPY --from=builder /app/apps/web/.next/static /app/apps/web/.next/static

# Copy static files
COPY --from=builder /app/apps/web/public /app/apps/web/public

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "/app/apps/web/server.js"]
izuminizumin

v2 以降の yarn(berry) の対応

.yarnrc.yml.yarn を COPY しておくのを忘れない。
多分 builder と installer で必要。
雑に base につっこんでおいてもいいかも。

izuminizumin

ところで、turbo prune に使う Turborepo は RUN npm -i -g turbo@<version> するしかないんだろうか…
package.json にあるものと2重管理になるの嫌だな