😫

大文字・小文字が引き起こすパス問題: Next.jsプロジェクトにおけるMacとWindowsの違い

2023/08/15に公開

概要

Next.jsを使ったプロジェクトにて、Macのローカル環境では問題なく表示される画面が、Windows環境では表示されないという事象に遭遇しました。

問題自体はお粗末な凡ミスだったのですが、事象解決の対応時にいくつかハマりごとがあったため一連の経緯を備忘録として残しておきます。

状況整理

  • 事象発生の対象は特定の1画面のみ
  • 両OSとも複数ブラウザで挙動確認(Chrome, Firefox, Safari, Edge)
  • ブラウザによる違いは見られず
  • console上で確認できるエラーメッセージはリソースが見つからないという情報のみ
Failed to load resource: the server responded with a status of 404 (Not Found)

以上のことから、実行環境もしくはOS依存による可能性が高いと考えました。

原因

エラー内容を見てもNot Found以外の情報が得られなかったため、改めてルーティング対象ページのパスを見てみたところ、遷移先ページファイルを配置していたディレクトリ名に問題があることがわかりました。

pages配下のパスはkebab-caseで定義していたつもりが、小文字の i で始まるディレクトリ名の先頭が大文字の I になっていたのです。

e.g.
正) /ai-identity/index.tsx
誤) /ai-Identity/index.tsx

この大文字/小文字の名称違いが原因で、該当ページのパスが見つからず、404エラーが発生していました。実際に大文字になっていた部分を小文字にリネームしてみると、Windows環境でも正常に画面表示されることが確認できました。

Macでは問題なく表示されていた画面だったので、これまで気付くことができなかったケアレスミスです…

問題①gitで大文字/小文字の変更が検知されない

事象解決のためディレクトリ名の大文字/小文字変更をしたところ「gitで変更検知されずステージングエリアにaddすることができない」という問題にぶつかりました。

git configを確認すると、実行環境で大文字/小文字の区別をしない設定となっていたため、今回のようなリネームの差分検知をしてくれなかった模様です。

If true, this option enables various workarounds to enable Git to work better on filesystems that are not case sensitive, like FAT. For example, if a directory listing finds "makefile" when Git expects "Makefile", Git will assume it is really the same file, and continue to remember it as "Makefile".

The default is false, except git-clone[1] or git-init[1] will probe and set core.ignoreCase true if appropriate when the repository is created.

core.ignoreCase | Git - git-config Documentation

以下コマンドで大文字/小文字の違いも検知するように設定変更。

$ git config core.ignorecase false

設定が反映されたことを確認します。

$ git config -l --local | grep core.ignorecase
core.ignorecase=false

これで対象ディレクトリの変更を検知し、ステージング&コミットできました。

ちなみにgit mvで一度tmp的な名称に置き換えてリネームする方法もありますが、設定変更しておくのが無難でしょう。

問題②なぜMacでは問題が顕在化しなかったのか

パス名の不一致により、not foundエラーが引き起こされたことはわかりましたが「なぜMac環境ではこの問題が顕在化しなかったのか?」という疑問が浮かびました。

結論から言うと、Macのファイルシステムに起因するものではないかと考えられます。

パスの大文字/小文字を区別しないAPFS

Macのディスク情報を確認したところ、ファイルシステムフォーマットが「APFS(暗号化)」となっており、パスの大文字/小文字を区別しない仕様になっていました。

APFS(Apple File System)は、macOS 10.13以降で採用されるようになったファイルシステムフォーマットです。

https://support.apple.com/ja-jp/guide/disk-utility/dsku19ed921c/mac

実はこのAPFS自体には複数の種類があり、使用環境によりパスの大文字/小文字を区別するか否かが変わってきます。

  • APFS: APFSフォーマットを使用します。暗号化や、大文字/小文字を区別するフォーマットが必要ない場合は、このオプションを選択します。
  • APFS(暗号化): APFSフォーマットを使用し、ボリュームを暗号化します。
  • APFS(大文字/小文字を区別): APFSフォーマットを使用し、ファイル名およびフォルダ名の大文字/小文字を区別します。例えば、「Homework」と「HOMEWORK」という名前のフォルダは、2つの異なるフォルダです。
  • APFS(大文字/小文字を区別、暗号化): APFSフォーマットを使用し、ファイル名およびフォルダ名の大文字/小文字を区別し、ボリュームを暗号化します。例えば、「Homework」と「HOMEWORK」という名前のフォルダは、2つの異なるフォルダです。

「APFS(大文字/小文字を区別)」もしくは「APFS(大文字/小文字を区別、暗号化)」ではないファイルシステムフォーマットの場合、パスの大文字/小文字の違いを吸収してしまうわけです。前述の通り、パスの大文字/小文字を厳密に区別するOS環境(Webサーバ)へデプロイした場合、本件と同様に404 not foundな問題が発生するものと思われます。

自分が使用しているMacのファイルシステムが、パスの大文字/小文字を区別しない仕様になっているとはまったく認識していませんでした。ファイルシステムの概念を把握していなければ、中々連想しづらい問題かもしれませんね。

再発防止策

APFSのファイルシステムフォーマット変更は影響度が大き過ぎるため、再発防止策として該当プロジェクトに大文字/小文字のミスを検知してくれるプラグイン case-sensitive-paths-webpack-plugin を導入することにしました。

This Webpack plugin enforces the entire path of all required modules match the exact case of the actual path on disk. Using this plugin helps alleviate cases where developers working on OSX, which does not follow strict path case sensitivity, will cause conflicts with other developers or build boxes running other operating systems which require correctly cased paths.

case-sensitive-paths-webpack-plugin - npm

ひとまずこれで同様の事象発生は軽減できるのではないかと。
(他に適切な対処法があればご教示いただきたいです)

不明瞭な点

以上で問題の解決には至りましたが、TypeScriptに関する疑問が1点残っています。

大文字/小文字の間違いを防ぐため、元々tsconfig.jsonforceConsistentCasingInFileNamestrueに設定していたのですが、今回のケースはディレクトリ名だったため検知の対象外となるのでしょうか…?

Force Consistent Casing In File Names - forceConsistentCasingInFileNames

TypeScript follows the case sensitivity rules of the file system it’s running on. This can be problematic if some developers are working in a case-sensitive file system and others aren’t. If a file attempts to import fileManager.ts by specifying ./FileManager.ts the file will be found in a case-insensitive file system, but not on a case-sensitive file system.

When this option is set, TypeScript will issue an error if a program tries to include a file by a casing different from the casing on disk.

TypeScript: TSConfig Reference - Docs on every TSConfig option

TypeScriptに知見のある方、もしご存知でしたらコメントやフィードバックをいただけると嬉しいです。

Discussion