RORK LABEN
DEADLINE — Google Play の Android 16(API レベル36)必須化まで残り4日です。8月31日以降、新規アプリも既存アプリの更新も対象となりますRULES — 提出ルールと可視性ルールは別物です。更新を止めているアプリも、API 35 未満のままだと新しい端末で新規ユーザーに表示されなくなりますEXTENSION — 延長を申請すれば11月1日まで全ユーザーへの配信を続けられます。フォームは Play Console から出るため、締切前に済ませておく必要がありますEXPO — Expo SDK 57 は React Native を 0.85 から 0.86 へ引き上げ、React は 19.2 で据え置きです。0.86 は破壊的変更がない想定とされていますHERMES — 57.0.9 が React Native 0.86.2 に更新し、SDK 56 の Hermes V1 メモリ退行を解消しました。reanimated や worklets を使うアプリで効きますPREBUILD — expo prebuild が native の android・ios ディレクトリを既定でクリアして再生成します。手を入れた native 変更は、事前に洗い出さないと静かに失われますDEADLINE — Google Play の Android 16(API レベル36)必須化まで残り4日です。8月31日以降、新規アプリも既存アプリの更新も対象となりますRULES — 提出ルールと可視性ルールは別物です。更新を止めているアプリも、API 35 未満のままだと新しい端末で新規ユーザーに表示されなくなりますEXTENSION — 延長を申請すれば11月1日まで全ユーザーへの配信を続けられます。フォームは Play Console から出るため、締切前に済ませておく必要がありますEXPO — Expo SDK 57 は React Native を 0.85 から 0.86 へ引き上げ、React は 19.2 で据え置きです。0.86 は破壊的変更がない想定とされていますHERMES — 57.0.9 が React Native 0.86.2 に更新し、SDK 56 の Hermes V1 メモリ退行を解消しました。reanimated や worklets を使うアプリで効きますPREBUILD — expo prebuild が native の android・ios ディレクトリを既定でクリアして再生成します。手を入れた native 変更は、事前に洗い出さないと静かに失われます
記事一覧/Rork入門
Rork入門/2026-08-28初級

Sunset.png と sunset.png は、手元の Mac では同じファイル、ビルドサーバーでは別のファイルです

手元では表示される画像が、クラウドのビルドだけで見つからない。原因はファイル名の大文字小文字でした。再現手順と、コミット前に検出する短いスクリプトを実測つきで残します。

Rork545Expo187アセットGit2ビルドエラー9

手元のシミュレータでは背景画像がちゃんと出ているのに、クラウドのビルドだけが「モジュールが見つからない」で止まりました。

差分を見ても、直前に触ったのは画像を1枚差し替えただけです。ファイルは確かにあります。開いて確認もしました。

原因は、Sunset.pngsunset.png に直したことでした。私にとっては同じファイルの整理でしたが、ビルドサーバーにとっては別のファイルの話だったのです。

個人開発で壁紙アプリのように画像を何百枚も抱えていると、この種の取り違えは一定の割合で必ず起きます。私自身、レビュー担当が他にいるわけでもありませんので、運任せにしない形にしておきたい部分です。

手元のファイルシステムは、大文字と小文字を区別していません

まずここが出発点です。macOS の既定のファイルシステムは、大文字と小文字を「区別せずに扱う」設定になっています。Sunset.pngsunset.png を同じものとして解決します。

一方、Linux はそれらを別のファイルとして扱います。Rork や Expo のクラウドビルド、GitHub Actions、EAS Build — どれも中身は Linux です。Android 端末のファイルシステムも同様です。

自分の環境がどちら側にいるかは、2行で確かめられます。

: > probe_a.txt
[ -e PROBE_A.TXT ] && echo "区別しない" || echo "区別する"

同じ条件を Linux 側(Node.js v22.23.2)で作って、実際の挙動を確認しました。assets/Sunset.png を置いた状態で、コードからは小文字で参照します。

// app.js — 実体は assets/Sunset.png、参照は小文字
const p = require.resolve("./assets/sunset.png");
console.log("resolved:", p);

実行結果です。

Error: Cannot find module './assets/sunset.png'

手元では通り、ビルドサーバーでは落ちる。この非対称が、差分を何度見返しても原因にたどり着けない理由でした。

Metro のバンドラは、パスの綴りをそのままファイルシステムに問い合わせます。問い合わせ先の性質が違えば、答えも変わるという単純な話です。

もっと厄介なのは、git がリネームを記録しないほうです

ここからが、私が本当につまずいた場所です。

macOS の git は、既定で core.ignorecasetrue になります。大文字小文字だけが違うリネームを、git が変更として認識しません。

同じ設定を作って、何が起きるかを確かめました。

git config core.ignorecase true
mv assets/Sunset.png assets/tmp && mv assets/tmp assets/sunset.png
git status --porcelain

出力はこの1行だけでした。

 D assets/Sunset.png

削除だけが見えていて、新しく置いた sunset.png は未追跡としてすら現れません。この状態で git add -A を実行すると、ステージされるのは削除です。

D	assets/Sunset.png

コミットしたあと、リポジトリに何が残っているかを確認しました。

git ls-files assets   # → 何も出力されない
ls assets             # → sunset.png(作業ツリーには存在する)

作業ツリーには画像があるのに、リポジトリからは消えています。クリーンな clone を作ると、assets ディレクトリそのものが存在しませんでした。

つまり、綴りの不一致は入口にすぎず、実際に起きていたのはアセットがリポジトリから抜け落ちていたことでした。手元でだけ動いていたのは当然です。手元にしかファイルがなかったのですから。

正しい直し方は git mv -f を通すことです。

git mv -f assets/Sunset.png assets/sunset.png
git diff --cached --name-status
R100	assets/Sunset.png	assets/sunset.png

R100 はリネームとして記録されたという意味です。この状態で clone すると、sunset.png がきちんと入っていました。

Rork からエクスポートしたコードを最初に git 管理へ載せるときの注意は、Rork のコードをエクスポートしたら、git init より先に .gitignore を用意します にまとめています。あわせて core.ignorecase の扱いも決めておくと安全です。

綴りの不一致を、コミット前に見つける

原因が分かっても、人間の注意力で毎回防ぐのは無理があります。私は検出を短いスクリプトに任せることにしました。

やることは単純です。ソースから相対パスのアセット参照を集め、綴りどおりに存在するかを確認し、存在しなければ「小文字にすると一致するファイル」があるかを調べます。あれば大文字小文字の不一致、なければ本当に存在しないファイルです。

// asset-case-check.mjs — 依存なしで動きます
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
 
const ROOTS = process.argv.slice(2).length ? process.argv.slice(2) : ["src", "app"];
const CODE = /\.(t|j)sx?$/;
const ASSET = /\.(png|jpg|jpeg|gif|webp|svg|mp3|mp4|ttf|otf|json)$/i;
// require("./a.png") / from "./a.png" / import("./a.png") の3形をまとめて拾います
const REF = /(?:require\(|from\s+|import\()\s*["'](\.\.?\/[^"']+)["']/g;
 
async function walk(dir, out = []) {
  let items;
  try { items = await readdir(dir, { withFileTypes: true }); } catch { return out; }
  for (const it of items) {
    if (it.name === "node_modules" || it.name.startsWith(".")) continue;
    const full = path.join(dir, it.name);
    if (it.isDirectory()) await walk(full, out);
    else out.push(full);
  }
  return out;
}
 
// ディレクトリの実エントリ名を一度だけ読み、小文字キーで引けるようにします
const dirCache = new Map();
async function entriesOf(dir) {
  if (dirCache.has(dir)) return dirCache.get(dir);
  let names = [];
  try { names = await readdir(dir); } catch {}
  const map = new Map();
  for (const n of names) {
    const k = n.toLowerCase();
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(n);
  }
  dirCache.set(dir, map);
  return map;
}
 
async function exists(p) { try { await stat(p); return true; } catch { return false; } }
 
const findings = [];
let scanned = 0, refs = 0;
 
for (const root of ROOTS) {
  for (const file of await walk(root)) {
    if (!CODE.test(file)) continue;
    scanned++;
    const src = await readFile(file, "utf8");
    for (const m of src.matchAll(REF)) {
      const spec = m[1];
      if (!ASSET.test(spec)) continue;
      refs++;
      const abs = path.resolve(path.dirname(file), spec);
      if (await exists(abs)) continue;                       // 綴りどおりに存在する
      const map = await entriesOf(path.dirname(abs));
      const hit = map.get(path.basename(abs).toLowerCase()); // 小文字で一致するものがあるか
      findings.push({ file, spec, actual: hit ? hit[0] : null });
    }
  }
}
 
for (const f of findings) {
  console.log(`${f.actual ? "CASE" : "MISS"}  ${f.file}\n      参照: ${f.spec}` +
    (f.actual ? `\n      実体: ${f.actual}` : ""));
}
const caseCount = findings.filter((f) => f.actual).length;
console.log(`\nソース ${scanned} 件 / アセット参照 ${refs} 件 / ` +
  `大文字小文字の不一致 ${caseCount} 件 / 実体なし ${findings.length - caseCount} 件`);
process.exit(caseCount ? 1 : 0);

exists() の判定を先に置いているのが要点です。綴りどおりに見つかるものは、そこで終わります。残ったものだけディレクトリ一覧を読むので、正常なプロジェクトではほとんど何も読みません。

小さなフィクスチャで動かした結果です。Sunset.png hero-Banner.webp icons/Play.svg を置き、コードからは全て小文字で参照しました。

CASE  src/screens/Home.tsx
      参照: ../assets/sunset.png
      実体: Sunset.png
CASE  src/screens/Home.tsx
      参照: ../assets/hero-banner.webp
      実体: hero-Banner.webp
CASE  src/screens/Home.tsx
      参照: ../assets/icons/play.svg
      実体: Play.svg
MISS  src/screens/Home.tsx
      参照: ../assets/nothing-here.png

ソース 1 件 / アセット参照 5 件 / 大文字小文字の不一致 3 件 / 実体なし 1 件

CASEMISS を分けているのは、直し方が違うからです。CASE は参照側の綴りを実体に合わせるか、git mv -f で実体側を揃えます。MISS はファイルの置き忘れなので、探す場所がそもそも別です。

実プロジェクトの規模で、どのくらい待たされるか

検出は速くないと習慣になりません。壁紙アプリに近い規模を作って計測しました。アセット420枚、ソース120ファイル、アセット参照480件。そのうち9件だけ、意図的に小文字へ崩してあります。

ソース 120 件 / アセット参照 480 件 / 大文字小文字の不一致 9 件 / 実体なし 0 件

real	0m0.144s

仕込んだ9件をそのまま9件として検出しました。所要は0.144秒です。

項目
アセット枚数420
ソースファイル120
アセット参照480 件
仕込んだ不一致9 件
検出9 件(見逃し 0)
実行時間0.144 秒(Node.js v22.23.2)

0.1秒台なら、コミットのたびに走らせても邪魔になりません。不一致があるときだけ終了コード1を返すので、package.json に置いて CI からも同じコマンドを呼べます。

{
  "scripts": {
    "check:assets": "node asset-case-check.mjs src app"
  }
}

私はこれをビルド前の一行として入れています。クラウドのビルドが始まってから10分待って失敗を知るより、手元で0.1秒のほうが精神衛生上ずっと良いというだけの理由です。個人開発だと、待ち時間はそのまま作業の中断になりますので。

起きる前に決めておくと楽になること

3つだけ、先に決めておくと後が楽です。

1. アセットのファイル名は小文字とハイフンだけにする。 Hero Banner@2x.PNG のような名前を許すと、大文字小文字に加えて空白とアットマークの扱いも増えます。最初に決めてしまえば、迷う場面自体がなくなります。

2. 名前を直すときは必ず git mv -f を通す。 Finder やエディタのリネームは、core.ignorecase の下では git に届きません。ここだけはコマンドで通す、と決めておくのが確実です。

3. 検出を人ではなく仕組みに持たせる。 一括置換のように、成功したように見えて別の場所が壊れる作業は他にもあります。一括置換は全ファイル成功しました。壊れたのは、消さなかった行のほうです でも同じ構図を扱いました。検証は毎回同じ手順で機械に任せるほうが、結果として速く終わります。

最初の公開前に決めておくべき名前は、アセット以外にもあります。変えられる名前と、変えられない名前 — Rork で最初の1本を公開する前の30分 も、着手前に一度目を通しておくと手戻りが減ります。

次にビルドが「見つからない」で止まったら、まず git ls-files でそのファイルがリポジトリに入っているかを確かめてみてください。作業ツリーにあることと、リポジトリにあることは別だ — 今回の私は、そこを一日かけて学びました。

同じ場所で一日を落とす方が一人でも減れば、この記録を残した意味があります。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Rork 入門2026-08-22
変えられる名前と、変えられない名前 — Rork で最初の1本を公開する前の30分
アプリには名前が3種類あり、公開後に変えられるのは表示名だけです。Rork が既定で入れる ID をそのまま提出する前に、依存ゼロのスクリプトで検査する手順をまとめました。
Rork 入門2026-08-17
Rork のコードをエクスポートしたら、git init より先に .gitignore を用意します
Rork からエクスポートしたプロジェクトをそのまま git init すると、鍵や .env まで履歴に載ります。除外設定を先に置いた場合と後から足した場合の差を実際に測り、効く書き方と効かない書き方を確かめました。
Rork 入門2026-05-05
ネイティブアプリと PWA、どちらを選ぶべきか — Rork を使う前に整理しておきたい3つの判断軸
ネイティブアプリとPWAの違いを、プッシュ通知・カメラ・App Store公開といった機能差から整理します。PWAで十分なケースも正直に示しながら、Rorkでネイティブを選ぶ意味と、判断に迷ったときに立ち返る3つの質問を具体的なコード例とともに紹介します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →