🧹

workspaceをつかったモノレポ構成のNext.jsをAWS Amplifyでホスティングするときに必要だった後処理

2023/01/25に公開

npmやyarnのworkspaceをつかったmonorepoになっているNext.jsのアプリをAWS Amplifyでホスティングするのに少し手こずったので、メモとして残しておきます。

TL;DR

outputをstandalone, outputFileTracingRootを設定して、最後にpostBuildのフェーズで

shopt -s dotglob
rm .next/standalone/package.json
cp -r .next/standalone/packages/appA/ .next/standalone
rm -rf .next/standalone/packages

して.next/standaloneの中の構造を整えよう

Next.jsがAmplifyでHostingされるときに期待されている構造

昨年、AmplifyでのNext.js 12.x/13.xのホスティングがサポートされました
https://aws.amazon.com/jp/blogs/mobile/amplify-next-js-13/

Amplify HostingでNext.js 12以降ののアプリをホスティングするときには
NEXT_PRIVATE_STANDALONEという環境変数がtrueに設定されてoutput: "standalone"が有効になった状態でビルドされるようです。

そして、その際の結果の .next/standalone ディレクトリの構造は主に以下のような構造が期待されています

.next/standalone
├── .next
│   ├── package.json
│   └── server
├── node_modules
│   ├── ...
│   ├── next
│   └── ...
├── package.json
└── server.js

このように

  • .next/standalone直下に.nextが存在すること
  • .next/standalone直下に起動用のserver.jsが存在すること
  • .next/standalone直下に必要な依存ライブラリを全て含んだnode_modulesが存在すること

などが期待されています。(Next.jsの自然なstandaloneの形式です)

workspaceでnode_modulesがインストールされる場所

yarnやnpmなどのパッケージ管理ツールではpackage.jsonに"workspace"を記載することで
monorepoの各パッケージ(以下 サブアプリ)を認識して依存関係の管理などをしてくれます

{
  "private": true,
  "workspaces": [
      "packages/*"
  ],
  ....
}

このようになっているときに、パッケージをインストールすると、各サブアプリの依存関係を分析して適宜各サブアプリ内のディレクトリに保存するか、ルートディレクトリで共通化するかなどの対応をしてくれます。

これは、hoistingという共通化できるnode_modulesをルートで共有することでnode_modulesのサイズを削減する機構です、詳しくは他の記事の方にゆずります。
私は以下の記事を参考にさせていただきました
https://tars0x9752.com/posts/yarn-hoisting

そのため、単純に通常のビルドと同様に、ルートレベルでパッケージのインストールコマンドを実行すると、設定によっては依存するライブラリがルートレベルのnode_modulesと各アプリのディレクトリ内のnode_modulesに分かれて保存される可能性があります。

outputFileTracingRootでルートレベルに保存されているライブラリを正しく認識する

上記のような形で実行に必要なnode_modulesがルートレベル側に保存されているかもしれない関係から、
Next.jsではoutputFileTracingRootというオプションをnext.conf.jsに設定することで、ルートレベルのnode_modulesに保存されている依存パッケージも追跡してstandalone内にきちんとコピーするように設定ができます。
https://nextjs.org/docs/advanced-features/output-file-tracing#caveats

例えば以下の設定だと、サブアプリから見て2つ上のディレクトリがルートレベルになっているという想定になっています( ../../の箇所)

outputFileTracingRoot: path.join(__dirname, '../../'),

この設定を忘れてしまうと、実行時に MODULE_NOT_FOUND のエラーが発生してしまいます。

outputFileTracingRootの設定のみだと、.next/standaloneが期待した形にならない

上記のようにnode_modulesを集めてくれるoutputFileTracingRootですが、これを設定した状態でoutpout: "standalone"なビルドをすると、.next/standalone ディレクトリが以下のような構造になってしまいます

.next/standalone
├── node_modules
│   ├── ...
│   ├── next
│   └── ...
├── package.json <- ルートレベルのpackage.json
└── packages
    └── appA
        ├── .next
        ├── package.json <- サブアプリのpackage.json
        └── server.js

このように、outputFileTracingRootを設定していなかった時とは違い、.next/standalone直下ではなく、packages/appAルートレベルから見たときのサブアプリの相対パスに期待している構造が保存されてしまいます

コマンドをつかってファイルを移動して期待している形に修正する

そこで、冒頭に紹介したコマンドをつかって、packages/appAからファイルを.next/standalone直下に移動してやることで期待されている構造に変換することができます

# *を書いたときの対象に隠しファイルやディレクトリも含める(.nextを移動するため)
shopt -s dotglob
# ルートレベルのではなく、サブアプリのpackage.jsonを使いたいためまずは削除
rm .next/standalone/package.json
# サブアプリのパスから.next/standalone直下に中身を全てコピーする
cp -r .next/standalone/packages/appA/ .next/standalone
# 不要になったpackagesディレクトリは削除する
rm -rf .next/standalone/packages

まとめ

いろいろと試行錯誤を経て、依存ライブラリがどこに保存されるべきなのかなどを探ってようやく無事設定できました。
もっと簡単な方法や公式でちゃんとオプションが実は提供されていたりしたら是非コメントでおしえてくださいませ!

Discussion