Next.jsに`shiki`を導入すると、プレビューモード やISRでエラーが発生する原因を調査する
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
先日Next.js+Vercelで作ったブログで、プレビューモードを使おうとするとエラーになるので、その原因の調査内容をメモしていきたいと思います。
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
まずはVercelに本番環境しか用意していなかったので、原因調査用の環境(プレビューデプロイメント)を作る。デプロイ元のGitHubリポジトリにmain
ブランチしかなかったので、新たにdevelop
ブランチを作成。
今後はdevelop
ブランチへのプッシュ以外はプレビューデプロイを行いたくないので、そのように設定。やり方は、以下の記事を参考にさせていただきました。
これで準備は完了。
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
現在本番環境で起こっているエラーは以下の2つ。
- microCMSの「画面プレビュー」ボタンを押してプレビュー画面を開くと500エラーとなる。
- ISRを使おうとすると、ブログ記事のページがすべて500エラーになる。(なので現在はSSGを使用)
面白いことに、ローカルでnext build
しnext start
で起動した環境では、プレビューモードもISRも問題なく使える…
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
プレビューモードやISRを使用した状態で、ブログ記事のページにアクセスすると500エラーとなり、VercelのFunctions Logに、以下のエラーが表示される。
[GET] /blog/particular
21:51:45:68
2021-04-05T12:51:46.703Z 166ce334-29cd-4060-8628-58cc4877f0c6 ERROR [Error: ENOENT: no such file or directory, open '/var/task/node_modules/shiki/themes/nord.json'] {
errno: -2,
syscall: 'open',
path: '/var/task/node_modules/shiki/themes/nord.json'
}
RequestId: 166ce334-29cd-4060-8628-58cc4877f0c6 Error: Runtime exited with error: exit status 1
Runtime.ExitError
コードブロックをハイライトするのに使っているshiki
が見つからず、そのせいでページをうまくレンダリングできなくて500エラーになっている模様。
試しにshiki
でコードブロックをハイライトする処理をコメントアウトしてデプロイし直すと、プレビューモードもISRも使えるようになった…
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
shiki
が原因であることはこれで確定したけど、問題はどうしてローカルでは発生せずVercelの環境でのみ発生するのかってことですね…(´ω`)
現在わかっていることは、
- ローカルでビルド&起動した環境ではプレビューモードもISRも問題なく使える
- Vercelの環境でのみ
shiki
が見つからず、そのせいでプレビューモードもISRもエラーになる - Vercelの環境でも、SSGを使っている場合は問題は発生しない(
shiki
でハイライトもされている)
要は、ビルド時にgetStaticProps
でデータをフェッチしてページをレンダーする時はshiki
が見つかるけど、プレビューモードやISRのようにリクエスト時にgetStaticProps
でデータをフェッチする場合はshiki
が見つからない…ということか…?🤔
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
スクラップのタイトルをちょっと変更。
今回、プレビューモード やISRがコケるのはNext.jsやVercelが原因というより、自分がshiki
をちゃんと理解していなかったことが原因でした。
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
原因はまさにこれだった…
↑のスレッドの初めのコメントで指摘されている通り、shiki
がVecelの環境で動かなかった原因は、shiki
がページ内のコードブロックをハイライトする際、Node.jsのfs
パッケージを使って動的にtheme(ハイライトのテーマ)
やlanguages(言語の解析)
の設定ファイルにアクセスしようとしていたことが原因でした。
npm install shiki
でNext.jsプロジェクト内のnode_modules
にインストールされたshiki
パッケージは、以下のような内容になっています。
このshiki
の中にあるthemes
やlanguages
の中にあるファイルが、next build
でビルドされた結果には含まれておらず、プレビューモード やISRでページをレンダーする際にshiki.highlighter
がそれらのファイルを読み込もうとして失敗し、エラーが発生していたんですね。
next build
&next start
した場合は動いてなかった?
ローカルで「next build
でビルドした結果に必要なファイルが含まれてなかったなら、なんでローカルでnext build
&next start
した場合は普通に動いたのさ?」と思われたかもしれません。
これはnext build
でNext.jsプロジェクト内に生成された.next
のshiki.highlighter
が同じプロジェクト内にあるnode_modules
から、必要な設定ファイルにアクセスしていたから、と考えられます。
試しにnext build
でビルドした後に、npm uninstall shiki
でshiki
パッケージを一旦Next.jsプロジェクトのnode_modules
から削除してからnext start
したところ、Vercelの環境と全く同じエラーが発生しました。
対応策
とりあえずの応急処置として、自分のブログで普段使うハイライトのテーマや言語の設定ファイルのみをNext.jsプロジェクト内にそのまま引っ張ってきて、そこを参照するようにshiki.highlighter
を設定し直しました。
↓Next.jsプロジェクト内にこんな感じで設定ファイルを置いて、
↓こんな感じで読み込みます。
import path from 'path';
import unified from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypeShiki from '@leafac/rehype-shiki';
import { loadTheme, getHighlighter } from 'shiki';
const shikiDirectory = path.join(process.cwd(), 'src/lib/shiki');
const theme = loadTheme(path.join(shikiDirectory, '/themes/nord.json'));
const tsPath = path.join(shikiDirectory, '/languages/typescript.tmLanguage.json');
const tsxPath = path.join(shikiDirectory, '/languages/tsx.tmLanguage.json');
const languages = [
{
id: 'typescript',
scopeName: 'source.ts',
path: tsPath,
aliases: ['ts'],
},
{
id: 'tsx',
scopeName: 'source.tsx',
path: tsxPath,
},
];
export const markdownToHtml = async (markdown: string) =>
unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeShiki, {
highlighter: await getHighlighter({
theme: await theme,
langs: languages,
}),
})
.use(rehypeStringify)
.processSync(markdown);
手段としては少々泥臭いですが…😓
とりあえずこれでVercelの環境でshiki
を使いつつ、プレビューモードやISRも使えるようになりました。
最初に紹介したスレッド内では「Next.jsがバンドルする際に使っているnftにshiki
が検知されるよう対応してもらえたら嬉しいな」との要望が上がっていますが、自分も是非対応に期待したいところです。
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
shikiの製作者さんより、「shikiのtheme
やlanguage
のJSONはバンドルに含めず、静的アセットとして扱った方がいい」と言われたのですが、いろいろ悩んだ末、これまで通りpublic
ではなくsrc
の中にディレクトリを置いて、そこで管理することにしました。
theme
やlanguage
の設定JSONをフォントファイルなどと同じものとして扱うなら、public
に置くのが正しそうですが、public
の方に置いちゃうとまたプレビューモード やISRの処理がややこしくなっちゃいそうなので…
![taka_h](https://res.cloudinary.com/zenn/image/fetch/s--DgxcUlPI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/4039b5ee23.jpeg)
Next.jsをv10.2.1以降へアップデートすると、再び同じエラーが発現しました
症状自体は、これまでと全く同じです。
-
shiki
がnode_modules
にある設定ファイルを読み込もうとして失敗し、エラーとなる。 - SSGでレンダリングする場合は問題ない。SSRやISRなど、リクエスト時にサーバーサイドが動く場合にのみエラーが発生する。
node_modules
ではなく、src
内にある設定ファイルを参照するよう設定しているはずなのですが、なぜかそれを無視して、node_modules
の設定ファイルを読み込もうとしてエラーが発生しているようです。一体何故だ…😇
ちなみにNext.jsをv10.2.0へ戻すと直ります。ということは、バージョンアップによってNext.jsのビルドの仕方がなにか代わったのでしょうか?
Next.jsでshiki
を使うとこういう問題があることは、以前からGitHubのIssuesへ報告している人がちらほらいるのですが、shiki
の作者さんはそれらの声に対し、「getStaticProps
を使えば問題ありません」と回答するに留めています。
これは逆に言えば、「SSRやISRでは使うなよ」ということなのかもしれません。
以前、PrismaのSchickling氏が「(Next.jsがバンドルに使っている)nft
に検知されるようshiki
を改良してもらえたら嬉しい」と要望をだしていましたが、どうもshiki
の作者さんはそれに積極的に対応するつもりはないような雰囲気です…
残念ながら、shiki
は今後Next.jsで積極的に使わない方がいいかもしれません…