# Expo + EAS builds in a pnpm monorepo

Fetch when the user says: "el build mobile falla", "como buildeo
forge mobile", "configuro EAS", "Metro can't resolve X", "Hermes
crash", "expo install rompe pnpm", "que pongo en eas.json".

Hard-won from the 2026-06-09 Forge Mobile setup (~10 failed
builds, ~$5 USD wasted). These are not in Expo docs. Apply
all of them before the next build.

## 1. NEVER `npx expo install <pkg>` in pnpm workspaces

`expo install` shells out to `npm install` which chokes on
`workspace:*` deps. Use `pnpm add <pkg>` (from the app dir) or
`pnpm --filter @yujin/forge-mobile add <pkg>` (from repo root).

## 2. Match Expo dep versions to the installed SDK

`pnpm add expo-splash-screen` grabs LATEST (e.g. 56.x), not
the version compatible with your SDK. For SDK 51 expo-splash-
screen must be `~0.27.7`. Wrong version causes
"Plugin [id: 'expo-module-gradle-plugin'] was not found" in
Gradle config. Cross-check with `npx expo install --check` or
the official Expo SDK versions table BEFORE pinning.

## 3. `metro.config.js`: NEVER set `disableHierarchicalLookup = true`

This is poison with pnpm. Metro then cannot walk into
`.pnpm/<pkg>@.../node_modules/<dep>` where pnpm stores every
transitive dep. Every single "Unable to resolve module X"
EAS error in this project traced back to this line. Keep
it at default (false).

## 4. `extraNodeModules` is FALLBACK only

If Metro can resolve a package name from any `node_modules`,
the `extraNodeModules` alias for that name is IGNORED -- it
does not override. So an alias for `@nac3/forge-rn` will not
fire while a real `@nac3/forge-rn` package is also in
node_modules.

## 5. Workspace packages with `main: dist/...` need PRE-BUILD on EAS

If a workspace package's `package.json` `main` field points to
a compiled artifact (e.g. `./dist/index.js`), EAS won't compile
it for you. Add a pre-install hook that runs the package's build:

```json
"eas-build-pre-install":
  "cd ../.. && npm install -g pnpm@<v> && pnpm install &&
   pnpm --filter <workspace-pkg> build"
```

## 6. No `.js` suffix imports in code Metro bundles

`import { X } from './foo.js'` fails when `foo` is `.ts`. Metro
doesn't auto-swap `.js` -> `.ts`. Drop the suffix in app code.
(Inside libraries that ship via `tsc` to `dist/`, KEEP the `.js`
suffix -- ESM requires it; `tsc` emits valid `.js` files there.)

## 7. `.easignore` must INCLUDE `pnpm-lock.yaml`

An older comment said "EAS uses npm" and excluded the lockfile.
That was wrong. EAS reads `packageManager` from root `package.json`
and uses the right manager. The lockfile guarantees reproducible
installs.

## 8. Node version: must match the pnpm engine

pnpm@11.3.0 requires Node `>=22.13`. EAS default Node is 20.x.
Add to every `eas.json` profile: `"node": "22.16.0"`. Otherwise
the pre-install hook crashes with
"No such built-in module: node:sqlite" when pnpm runs.

## 9. Corepack on EAS images has stale signing keys

`corepack prepare pnpm@<v> --activate` fails with
"Cannot find matching keyid" on EAS Node 20.18.0. Sidestep:
install pnpm via plain npm in the pre-install hook:
```
npm install -g pnpm@<version>
```

## 10. Reproduce the Metro phase locally to save $1.20

Before pushing any "fix" for a Metro / RN / pnpm resolver error,
verify the bundle locally:

```powershell
cd C:\yujin-forge\apps\forge-mobile
npx expo export:embed --platform android `
  --bundle-output /tmp/out.bundle `
  --assets-dest /tmp/assets `
  --entry-file node_modules/expo-router/entry.js `
  --dev false
```

This runs the same Metro phase EAS does. Success here = success
on EAS for the bundle step. Failure reproduces the EAS error
message for free. ALWAYS run this before `eas-cli build`.

## 11. Hermes (RN 0.74) lacks TextDecoder as a global

Bundle compiles fine, APK installs cleanly, then crashes at first
launch with:
```
ReferenceError: Property 'TextDecoder' doesn't exist
js engine: hermes
```

Hermes in RN 0.74 doesn't include TextDecoder. Modules in
`@nac3/forge-rn` (`keys_envelope.ts`, `pairing.ts`) call
`new TextDecoder()` at module load time, so the crash fires
during bundle eval before any React renders.

Fix: install `text-encoding-polyfill` and import it AS THE
FIRST LINE of `app/_layout.tsx`:
```ts
import 'text-encoding-polyfill';
import * as React from 'react';
// ... rest
```

## 12. Hermes + dynamic `await import('uninstalled-pkg')` crashes

Pattern:
```ts
let Foo: any;
try { Foo = (await import('react-native-foo')).default; }
catch { Foo = null; }
```

Looks safe -- try/catch absorbs the missing-dep error, right?
**Wrong on Hermes.** Metro at bundle time emits a module ID for
the dynamic import target. At runtime the resolver returns
`undefined` (the dep was never installed) and Hermes throws
**outside** the try block with:
```
Error: Requiring unknown module "undefined", js engine: hermes
```

Fix: either (a) install the dep for real, or (b) remove the
dynamic import entirely and rely on the fallback. Don't try
to make `await import` "optional" -- in RN/Hermes the try/catch
doesn't help.

We hit this with `react-native-zeroconf` in
`apps/forge-mobile/components/lan_adapter.ts` (z.190): swapped
to the `pollingFallback` unconditionally until zeroconf is
adopted as a real dep.

## 13. Working setup reference (Forge Mobile)

- Owner / Expo username: `pkuschniroff`
- EAS project ID: `76fbb53e-701b-4630-9f08-179ebdafdc6f`
- Android keystore: "Build Credentials I4ycEkDKiE (default)",
  generated 2026-06-08 on EAS Cloud, reused across builds.
- Working `eas-build-pre-install`:
  ```
  cd ../.. && npm install -g pnpm@11.3.0 && pnpm install &&
  pnpm --filter @nac3/forge-rn build
  ```
- Profiles in eas.json: `internal` (APK), `preview` (APK),
  `production` (APK), `production-store` (AAB for Play).

## 14. Diagnostic flow for app-side crashes

When a release-built APK installs but crashes immediately:
1. Sometimes Android shows "App has stopped" without details.
2. On the phone: Settings -> About phone -> tap Build number 7x
   to enable Developer mode -> open it -> "Take bug report".
3. Generates a ~100-150 MB .txt file in Downloads.
4. Grep that file for the app package name (`app.yujin.forge.mobile`)
   to find the `AndroidRuntime: FATAL EXCEPTION` block with the
   Hermes JS stack -- that's the actual JS error.

No need for ADB / USB / Android Studio for first-pass triage.
