What happened?
In a pnpm monorepo, one Expo app depends on Uniwind Pro ("uniwind": "npm:uniwind-pro@^1.7.0") while another app depends on public Uniwind ("uniwind": "^1.12.0"). Both packages publish as "name": "uniwind". With publicHoistPattern: "*", the public copy is hoisted to the workspace root.
That produces two Metro failures:
1. Bundling: missing main / index
Unable to resolve "uniwind" from "src/app/_layout.tsx"
The package `…/node_modules/uniwind/package.json` was successfully found. However, this package itself specifies a
`main` module field that could not be resolved (`…/node_modules/uniwind/index`).
Pro installs as a CLI stub with no exports / main until postinstall. Even after the payload is installed, package.json has an exports map (react-native → ./src/index.ts) but no top-level main or react-native field. If Metro skips exports (common when the file map cannot see the pnpm store path), it defaults main to index, which does not exist.
Related: #679 (closed as invalid — not filed with the template). Same class of failure as react/metro#1424.
2. Runtime: uniwind/components rewrite hits the public copy
withUniwindConfig rewrites react-native → uniwind/components using the importing file as originModulePath. Modules outside the Pro app (expo-router, reanimated, …) then resolve uniwind from the hoisted public copy. That copy’s own react-native imports are rewritten again → infinite recursion:
Maximum call stack size exceeded (native stack depth)
"main" has not been registered
This is the same class of bug as #353, fixed for OSS in #570 (pin originModulePath when resolving uniwind / uniwind/*). Pro 1.7 still ships the pre-#570 plugin, so the pin is missing.
require("uniwind/metro") from the Pro app can also walk past the installer stub and load the public plugin.
Steps to Reproduce
- pnpm workspace with two Expo apps.
- App A:
"uniwind": "npm:uniwind-pro@^1.7.0".
- App B:
"uniwind": "^1.12.0" (public).
publicHoistPattern: "*" (or otherwise hoist public uniwind to the workspace root).
- From App A,
expo start / expo export --platform ios.
- Bundling fails with the
uniwind/index error above. If the Pro payload is present but the plugin is the public 1.12 copy (or Pro 1.7 without the origin pin), the device then crashes on launch with a native stack overflow.
Workaround that unblocked us: resolve Pro by filesystem path (<app>/node_modules/uniwind), load withUniwindConfig from that copy’s exports["./metro"], map uniwind / uniwind/* to that copy’s exports targets on disk, and pin originModulePath to App A’s package.json.
Snack or Repository Link (Optional)
Private monorepo — cannot share a public repro. Happy to extract a minimal pnpm workspace if useful.
Uniwind version
uniwind-pro 1.7.0 (app A) + public uniwind 1.12.0 (app B, hoisted)
React Native Version
0.86.3 (Expo SDK 57)
Platforms
iOS
Expo
Yes
Additional information 〰
What happened?
In a pnpm monorepo, one Expo app depends on Uniwind Pro (
"uniwind": "npm:uniwind-pro@^1.7.0") while another app depends on public Uniwind ("uniwind": "^1.12.0"). Both packages publish as"name": "uniwind". WithpublicHoistPattern: "*", the public copy is hoisted to the workspace root.That produces two Metro failures:
1. Bundling: missing
main/indexPro installs as a CLI stub with no
exports/mainuntil postinstall. Even after the payload is installed,package.jsonhas anexportsmap (react-native→./src/index.ts) but no top-levelmainorreact-nativefield. If Metro skipsexports(common when the file map cannot see the pnpm store path), it defaultsmaintoindex, which does not exist.Related: #679 (closed as invalid — not filed with the template). Same class of failure as react/metro#1424.
2. Runtime:
uniwind/componentsrewrite hits the public copywithUniwindConfigrewritesreact-native→uniwind/componentsusing the importing file asoriginModulePath. Modules outside the Pro app (expo-router, reanimated, …) then resolveuniwindfrom the hoisted public copy. That copy’s ownreact-nativeimports are rewritten again → infinite recursion:This is the same class of bug as #353, fixed for OSS in #570 (pin
originModulePathwhen resolvinguniwind/uniwind/*). Pro 1.7 still ships the pre-#570 plugin, so the pin is missing.require("uniwind/metro")from the Pro app can also walk past the installer stub and load the public plugin.Steps to Reproduce
"uniwind": "npm:uniwind-pro@^1.7.0"."uniwind": "^1.12.0"(public).publicHoistPattern: "*"(or otherwise hoist publicuniwindto the workspace root).expo start/expo export --platform ios.uniwind/indexerror above. If the Pro payload is present but the plugin is the public 1.12 copy (or Pro 1.7 without the origin pin), the device then crashes on launch with a native stack overflow.Workaround that unblocked us: resolve Pro by filesystem path (
<app>/node_modules/uniwind), loadwithUniwindConfigfrom that copy’sexports["./metro"], mapuniwind/uniwind/*to that copy’sexportstargets on disk, and pinoriginModulePathto App A’spackage.json.Snack or Repository Link (Optional)
Private monorepo — cannot share a public repro. Happy to extract a minimal pnpm workspace if useful.
Uniwind version
uniwind-pro 1.7.0 (app A) + public uniwind 1.12.0 (app B, hoisted)
React Native Version
0.86.3 (Expo SDK 57)
Platforms
iOS
Expo
Yes
Additional information 〰