Open9

Next.jsに`shiki`を導入すると、プレビューモード やISRでエラーが発生する原因を調査する

先日Next.js+Vercelで作ったブログで、プレビューモードを使おうとするとエラーになるので、その原因の調査内容をメモしていきたいと思います。

まずはVercelに本番環境しか用意していなかったので、原因調査用の環境(プレビューデプロイメント)を作る。デプロイ元のGitHubリポジトリにmainブランチしかなかったので、新たにdevelopブランチを作成。

今後はdevelopブランチへのプッシュ以外はプレビューデプロイを行いたくないので、そのように設定。やり方は、以下の記事を参考にさせていただきました。

https://zenn.dev/catnose99/articles/b37104fc7ef214

これで準備は完了。

現在本番環境で起こっているエラーは以下の2つ。

  • microCMSの「画面プレビュー」ボタンを押してプレビュー画面を開くと500エラーとなる。
  • ISRを使おうとすると、ブログ記事のページがすべて500エラーになる。(なので現在はSSGを使用)

面白いことに、ローカルでnext buildnext startで起動した環境では、プレビューモードもISRも問題なく使える

プレビューモードや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も使えるようになった…

shikiが原因であることはこれで確定したけど、問題はどうしてローカルでは発生せずVercelの環境でのみ発生するのかってことですね…(´ω`)

現在わかっていることは、

  • ローカルでビルド&起動した環境ではプレビューモードもISRも問題なく使える
  • Vercelの環境でのみshikiが見つからず、そのせいでプレビューモードもISRもエラーになる
  • Vercelの環境でも、SSGを使っている場合は問題は発生しない(shikiでハイライトもされている)

要は、ビルド時にgetStaticPropsでデータをフェッチしてページをレンダーする時はshikiが見つかるけど、プレビューモードやISRのようにリクエスト時にgetStaticPropsでデータをフェッチする場合はshikiが見つからない…ということか…?🤔

スクラップのタイトルをちょっと変更。

今回、プレビューモード やISRがコケるのはNext.jsやVercelが原因というより、自分がshikiをちゃんと理解していなかったことが原因でした。

原因はまさにこれだった…

https://github.com/shikijs/shiki/issues/138

↑のスレッドの初めのコメントで指摘されている通り、shikiがVecelの環境で動かなかった原因は、shikiがページ内のコードブロックをハイライトする際、Node.jsのfsパッケージを使って動的theme(ハイライトのテーマ)languages(言語の解析)の設定ファイルにアクセスしようとしていたことが原因でした。

npm install shikiでNext.jsプロジェクト内のnode_modulesにインストールされたshikiパッケージは、以下のような内容になっています。

shiki package

このshikiの中にあるthemeslanguagesの中にあるファイルが、next buildでビルドされた結果には含まれておらず、プレビューモード やISRでページをレンダーする際にshiki.highlighterがそれらのファイルを読み込もうとして失敗し、エラーが発生していたんですね。

ローカルでnext build&next startした場合は動いてなかった?

next buildでビルドした結果に必要なファイルが含まれてなかったなら、なんでローカルでnext build&next startした場合は普通に動いたのさ?」と思われたかもしれません。

これはnext buildでNext.jsプロジェクト内に生成された.nextshiki.highlighter同じプロジェクト内にあるnode_modulesから、必要な設定ファイルにアクセスしていたから、と考えられます。

local dev

試しにnext buildでビルドした後に、npm uninstall shikishikiパッケージを一旦Next.jsプロジェクトのnode_modulesから削除してからnext startしたところ、Vercelの環境と全く同じエラーが発生しました

対応策

とりあえずの応急処置として、自分のブログで普段使うハイライトのテーマや言語の設定ファイルのみをNext.jsプロジェクト内にそのまま引っ張ってきて、そこを参照するようにshiki.highlighterを設定し直しました。

↓Next.jsプロジェクト内にこんな感じで設定ファイルを置いて、

lib_shiki

↓こんな感じで読み込みます。

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がバンドルする際に使っているnftshikiが検知されるよう対応してもらえたら嬉しいな」との要望が上がっていますが、自分も是非対応に期待したいところです。

shikiの製作者さんより、「shikiのthemelanguageのJSONはバンドルに含めず、静的アセットとして扱った方がいい」と言われたのですが、いろいろ悩んだ末、これまで通りpublicではなくsrcの中にディレクトリを置いて、そこで管理することにしました。

themelanguageの設定JSONをフォントファイルなどと同じものとして扱うなら、publicに置くのが正しそうですが、publicの方に置いちゃうとまたプレビューモード やISRの処理がややこしくなっちゃいそうなので…

Next.jsをv10.2.1以降へアップデートすると、再び同じエラーが発現しました

症状自体は、これまでと全く同じです。

  • shikinode_modulesにある設定ファイルを読み込もうとして失敗し、エラーとなる。
  • SSGでレンダリングする場合は問題ない。SSRやISRなど、リクエスト時にサーバーサイドが動く場合にのみエラーが発生する。

node_modulesではなく、src内にある設定ファイルを参照するよう設定しているはずなのですが、なぜかそれを無視して、node_modulesの設定ファイルを読み込もうとしてエラーが発生しているようです。一体何故だ…😇

ちなみにNext.jsをv10.2.0へ戻すと直ります。ということは、バージョンアップによってNext.jsのビルドの仕方がなにか代わったのでしょうか?

Next.jsでshikiを使うとこういう問題があることは、以前からGitHubのIssuesへ報告している人がちらほらいるのですが、shikiの作者さんはそれらの声に対し、「getStaticPropsを使えば問題ありません」と回答するに留めています。

https://github.com/shikijs/shiki/issues/161

これは逆に言えば、「SSRやISRでは使うなよ」ということなのかもしれません。

以前、PrismaのSchickling氏が「(Next.jsがバンドルに使っている)nftに検知されるようshikiを改良してもらえたら嬉しい」と要望をだしていましたが、どうもshikiの作者さんはそれに積極的に対応するつもりはないような雰囲気です…

残念ながら、shikiは今後Next.jsで積極的に使わない方がいいかもしれません…

ログインするとコメントできます