Skip to content

fix(example): patch @expo/plist null-proto crash breaking iOS device install#63

Merged
LeslieOA merged 2 commits into
developfrom
fix/expo-plist-device-install
May 30, 2026
Merged

fix(example): patch @expo/plist null-proto crash breaking iOS device install#63
LeslieOA merged 2 commits into
developfrom
fix/expo-plist-device-install

Conversation

@LeslieOA

Copy link
Copy Markdown
Member

Problem

npm run mobile:ios:run:device crashed at the install step:

TypeError: Cannot convert object to primitive value
    at LockdowndClient.startSession (@expo/cli .../LockdowndClient.ts:119)

The app built fine every time — only the device install failed, so expo run:ios --device never completed.

Root cause

@expo/plist 0.5.4 builds parsed plist objects with Object.create(null) (null prototype):

0.5.4:  new_obj = Object.create(null);   // no toString → crashes on coercion
0.5.3:  new_obj = {};                    // works

@expo/cli's lockdownd handshake does debug(\startSession: ${pairRecord}`). The ${pairRecord}` template coercion of a null-prototype object throws "Cannot convert object to primitive value", aborting before install.

Confirmed it is not an @expo/cli regression — LockdowndClient.js is byte-identical between 55.0.30 and 55.0.32. Other local projects work only because they resolved @expo/plist 0.5.3. Not iOS-version-related, not device-related, not our code.

Why patch-package (not overrides)

npm overrides can't fix it here: @expo/cli pins @expo/plist to an exact 0.5.4 and npm refuses to override the nested copy (verified across many attempts, including nested-path override syntax + full lockfile regen — the latter also drifted 78 unrelated deps incl. worklets, breaking the native build). patch-package edits the resolved file directly and replays on every install, version-pin-agnostic.

The fix

  • patches/@expo+plist+0.5.4.patch — one line: Object.create(null){}
  • patch-package devDep + postinstall hook → auto-applies on every install / fresh clone

CNG-safe

This is JS tooling only — it runs on the dev's Mac during expo run:ios's install step. It does not touch Pods, ios/, or anything Expo CNG manages. We never pod install. ✅

Verification

  • npm ci from clean → postinstall reports @expo/plist@0.5.4 ✔
  • npm run mobile:ios:run:deviceBuild Succeeded → Installing → app launches on device (previously crashed pre-install)
  • worklets stays at 0.7.2 (no dependency drift, native build intact)

Cleanup later

Remove once @expo/plist restores Object.prototype or @expo/cli stops string-coercing the pair record.

🤖 Generated with Claude Code

LeslieOA and others added 2 commits May 30, 2026 22:54
…install

`npm run mobile:ios:run:device` (and any `expo run:ios --device`) crashed at
the install step with:

  TypeError: Cannot convert object to primitive value
    at LockdowndClient.startSession (@expo/cli .../LockdowndClient.ts:119)

Root cause: @expo/plist 0.5.4 builds parsed plist objects with
`Object.create(null)` (null prototype). @expo/cli's device lockdownd handshake
does `debug(`startSession: ${pairRecord}`)`, and template-string coercion of a
null-prototype object throws "Cannot convert object to primitive value",
aborting before the app installs. @expo/cli itself is unchanged across 55.0.30
↔ 55.0.32 (byte-identical) — the regression is entirely in @expo/plist; other
projects only work because they happened to resolve 0.5.3.

npm `overrides` can't fix this here: @expo/cli pins @expo/plist to an exact
0.5.4 and npm refuses to override the nested copy (confirmed across many
attempts, including nested-path override syntax). So patch the one line via
patch-package instead:

  - Object.create(null)  →  {}   (build/parse.js, parsePlistXML dict branch)

This is JS tooling only — it runs on the dev's Mac during `expo run:ios`'s
install step. It does NOT touch Pods, `ios/`, or anything Expo CNG manages, so
it's fully CNG-compatible (we never `pod install`).

Adds patch-package as a devDep + `postinstall` hook so the patch auto-applies
on every install / clean clone. Verified: a fresh `npm ci` reports
"@expo/plist@0.5.4 ✔" and the device build now reaches install/launch.

Remove once @expo/plist restores Object.prototype or @expo/cli stops
string-coercing the pair record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI installs the library only (`npm ci --workspaces=false`), so the example-app
dep @expo/plist isn't in the tree — and patch-package hard-fails on a patch
whose target package is missing ("Patch file found for package plist which is
not present"), breaking the CI install step.

Guard the hook so patch-package runs only when @expo/plist is actually
installed. When present (dev Mac, example installs) it still runs and still
fails loudly on a genuinely broken patch; when absent (CI lib-only) it skips
with exit 0. An `if [ -d ... ]` guard preserves real-failure visibility,
unlike `|| true` which would mask it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@LeslieOA LeslieOA merged commit 90e0c7b into develop May 30, 2026
1 check passed
@LeslieOA LeslieOA deleted the fix/expo-plist-device-install branch May 30, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant