What happened?
After uniwind-pro resolved to 1.3.0 (1.2.1 was fine), the Metro dev server (expo start / expo run:ios) red-screens with:
Unable to resolve module react-native from
.../node_modules/.pnpm/expo-constants@.../node_modules/expo-constants/build/Constants.js
react-native could not be found within the project or in these directories:
../../node_modules/.pnpm/expo-constants@.../node_modules
../../node_modules/.pnpm/node_modules
...
Import stack: src/app/(drawer)/_layout.tsx → expo-router → exports.js → views/Sitemap.js → expo-constants → react-native (also reproduces from expo-router/renderRootComponent.js).
The install is healthy: node -e "require.resolve('react-native', {paths:['<.../expo-constants/build>']})" resolves fine, and react-native is symlinked into every consumer's .pnpm dir and hoisted to .pnpm/node_modules/.
The failure is dev-server-only. expo export --platform ios (and therefore EAS builds) on the same 1.3.0 bundles cleanly — a 7.7 MB Hermes bundle, exit 0, zero resolution errors.
Reproduction
- pnpm workspace monorepo, app under
apps/native, Expo SDK 56.
uniwind: npm:uniwind-pro@1.3.0, standard withUniwindConfig(getDefaultConfig(__dirname), {...}).
expo start --dev-client (or expo run:ios) → red screen above.
expo export --platform ios → succeeds.
Two independent things flip it back to working (both confirm the resolver is the cause):
- Downgrade to
uniwind-pro@1.2.1.
- Keep 1.3.0 but null out uniwind's resolver after wrapping:
const c = withUniwindConfig(getDefaultConfig(__dirname), {...}); c.resolver.resolveRequest = undefined;
Expected
react-native resolves under the dev server, same as 1.2.1 and same as expo export.
What's exactly broken
In uniwind/dist/metro/index.cjs, nativeResolver's first action eagerly resolves the incoming literal moduleName through the captured resolver, before any guard:
const nativeResolver = ({ context, moduleName, platform, resolver }) => {
const resolution = resolver(context, moduleName, platform); // ← throws for moduleName === "react-native"
...
if (moduleName === "react-native") return resolver(context, "uniwind/components", platform);
...
};
and the wrapper captures the config's existing resolver rather than delegating through Metro:
resolveRequest: (context, moduleName, platform) => {
const resolver = config$1.resolver?.resolveRequest ?? context.resolveRequest; // ← prefers pre-set resolver
return (platform === Platform.Web ? webResolver : nativeResolver)({ context, moduleName, platform, resolver });
}
Under expo start, Expo has already populated config.resolver.resolveRequest with its composed dev-only chain (createExpoAutolinkingResolver, createTypescriptResolver, createExpoFallbackResolver, withMetroErrorReportingResolver — all under @expo/cli/.../start/server/metro/). uniwind captures that and re-invokes it out-of-band for the raw react-native request. In that re-entrant dev chain, react-native mis-resolves from a nested .pnpm consumer and throws UnableToResolveError. expo export composes a shallower pipeline (none of those dev-only layers), so the identical call resolves — hence dev-only.
Stack (trimmed):
buildFailedToResolveNameError (metro-resolver/resolve.js:269)
← resolve (metro-resolver/resolve.js:210)
← withMetroResolvers.js:75
← nativeResolver (uniwind-pro/dist/metro/index.cjs:97) // the eager resolver(context, moduleName, platform)
← resolveRequest (uniwind-pro/dist/metro/index.cjs:161)
← withMetroErrorReportingResolver.js:88 // dev-server-only layer
Suggested direction
- Don't eagerly resolve the literal
moduleName before the redirect decision — for moduleName === "react-native" (non-internal origin) the intent is to return uniwind/components, so resolving the raw react-native first is wasted work that can fail.
- Prefer delegating through
context.resolveRequest instead of capturing config$1.resolver.resolveRequest, so the resolver composes correctly inside Expo's dev chain.
Environment
uniwind-pro 1.3.0 (broken) vs 1.2.1 (works)
expo@56.0.8, @expo/cli@56.1.13, metro@0.84.4, expo-router@56.2.8, expo-constants@56.0.16
react-native@0.85.3, react@19.2.3, New Architecture, React Compiler on
pnpm@10.22.0 workspace monorepo, macOS, iOS simulator
What happened?
After
uniwind-proresolved to 1.3.0 (1.2.1 was fine), the Metro dev server (expo start/expo run:ios) red-screens with:Import stack:
src/app/(drawer)/_layout.tsx → expo-router → exports.js → views/Sitemap.js → expo-constants → react-native(also reproduces fromexpo-router/renderRootComponent.js).The install is healthy:
node -e "require.resolve('react-native', {paths:['<.../expo-constants/build>']})"resolves fine, andreact-nativeis symlinked into every consumer's.pnpmdir and hoisted to.pnpm/node_modules/.The failure is dev-server-only.
expo export --platform ios(and therefore EAS builds) on the same 1.3.0 bundles cleanly — a 7.7 MB Hermes bundle, exit 0, zero resolution errors.Reproduction
apps/native, Expo SDK 56.uniwind: npm:uniwind-pro@1.3.0, standardwithUniwindConfig(getDefaultConfig(__dirname), {...}).expo start --dev-client(orexpo run:ios) → red screen above.expo export --platform ios→ succeeds.Two independent things flip it back to working (both confirm the resolver is the cause):
uniwind-pro@1.2.1.const c = withUniwindConfig(getDefaultConfig(__dirname), {...}); c.resolver.resolveRequest = undefined;Expected
react-nativeresolves under the dev server, same as 1.2.1 and same asexpo export.What's exactly broken
In
uniwind/dist/metro/index.cjs,nativeResolver's first action eagerly resolves the incoming literalmoduleNamethrough the captured resolver, before any guard:and the wrapper captures the config's existing resolver rather than delegating through Metro:
Under
expo start, Expo has already populatedconfig.resolver.resolveRequestwith its composed dev-only chain (createExpoAutolinkingResolver,createTypescriptResolver,createExpoFallbackResolver,withMetroErrorReportingResolver— all under@expo/cli/.../start/server/metro/). uniwind captures that and re-invokes it out-of-band for the rawreact-nativerequest. In that re-entrant dev chain,react-nativemis-resolves from a nested.pnpmconsumer and throwsUnableToResolveError.expo exportcomposes a shallower pipeline (none of those dev-only layers), so the identical call resolves — hence dev-only.Stack (trimmed):
Suggested direction
moduleNamebefore the redirect decision — formoduleName === "react-native"(non-internal origin) the intent is to returnuniwind/components, so resolving the rawreact-nativefirst is wasted work that can fail.context.resolveRequestinstead of capturingconfig$1.resolver.resolveRequest, so the resolver composes correctly inside Expo's dev chain.Environment
uniwind-pro1.3.0 (broken) vs 1.2.1 (works)expo@56.0.8,@expo/cli@56.1.13,metro@0.84.4,expo-router@56.2.8,expo-constants@56.0.16react-native@0.85.3,react@19.2.3, New Architecture, React Compiler onpnpm@10.22.0workspace monorepo, macOS, iOS simulator