Skip to content

[Pro 1.3.0] nativeResolver fails to resolve react-native from nested deps under the Expo dev server (pnpm monorepo); expo export / EAS unaffected #569

@istarkov

Description

@istarkov

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):

  1. Downgrade to uniwind-pro@1.2.1.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions