Skip to content

babel import-plugin still rewrites react-native-web internals, causing a circular-import crash on web (incomplete fix of #196 / #202) #380

Description

@yucedemirayak

Before submitting a new issue

  • I tested using the latest version of the library, as the bug might be already fixed.
  • I tested using a supported version of react native.
  • I checked for possible duplicate issues, with possible answers.

Bug summary

This is a follow-up to #196 — the fix in #202 is incomplete, so the react-native-web circular-import crash still reproduces on web with react-native-css@3.0.7.

On web, the app crashes at bundle load time:

Web  ERROR  [TypeError: Cannot read properties of undefined (reading 'default')]

Code: index.js
 35 | export { default as Button } from './exports/Button';
 36 | export { default as CheckBox } from './exports/CheckBox';
> 37 | export { default as FlatList } from './exports/FlatList';

Call Stack
  <global> (react-native-web/dist/index.js:37)
  <global> (react-native-css/dist/commonjs/components/FlatList.js:17)
  <global> (react-native-web/dist/vendor/react-native/Animated/components/AnimatedFlatList.js:12)
  <global> (react-native-web/dist/vendor/react-native/Animated/Animated.js:13)

(In a production bundle this surfaces as can't access property "default", _exportsFlatList2 is undefined.)

Root cause

There are two layers that rewrite react-native(-web) imports to react-native-css/components/*: the metro webResolver (src/metro/resolver.ts) and the babel import-plugin (src/babel/import-plugin.ts). #202 only touched the resolver, and only by excluding resolution targets inside react-native-web/dist/vendor (plus VirtualizedList). Neither layer excludes origins inside react-native-web, and the babel plugin was not changed at all.

The babel plugin's parseReactNativeWebSource resolves relative import specifiers against the current filename and rewrites anything that lands inside react-native-web/dist. So react-native-web's own internal imports get rewritten, which creates the cycle:

  1. Something touches Animatedreact-native-web/dist/vendor/react-native/Animated/Animated.js loads AnimatedFlatList.js.
  2. AnimatedFlatList.js imports ../../../../exports/FlatList → resolves into react-native-web/dist → babel rewrites it to react-native-css/components/FlatList. (The resolver's vendor exclusion doesn't help: it checks the target path, and the target here is exports/FlatList, not a vendor file. The babel plugin rewrites the specifier before the resolver would matter anyway.)
  3. react-native-css/components/FlatList imports react-nativereact-native-web/dist/index.js.
  4. The index's own export { default as FlatList } from './exports/FlatList' was also babel-rewritten to react-native-css/components/FlatList — which is the module currently mid-initialization in step 3 → its partial exports have no default → crash.

Note the native side already has exactly the guard that's missing here: nativeResolver skips rewriting when the origin is react-native/index.js.

Suggested fix (verified locally via pnpm patch against the published dist):

  • src/babel/import-plugin.ts — extend the existing isFromThisModule skip so files inside react-native-web are never rewritten:
function isFromThisModule(filename: string) {
  return (
    filename.startsWith(thisModuleDist) ||
    filename.startsWith(thisModuleSrc) ||
    // react-native-web internals must keep their raw imports, otherwise
    // rnw/index -> css component -> "react-native" -> rnw/index cycles
    filename.includes(`${sep}react-native-web${sep}`)
  );
}
  • src/metro/resolver.ts — add the equivalent origin guard to webResolver (mirrors nativeResolver's isReactNativeIndex guard):
export function webResolver(resolver, context, moduleName, platform) {
  const resolution = resolver(context, moduleName, platform);
  if (
    isFromThisModule(context.originModulePath) ||
    // Don't rewrite react-native-web's own internal imports
    context.originModulePath.includes(`${sep}react-native-web${sep}`) ||
    resolution.type !== "sourceFile" ||
    ...

With both guards in place: userland imports of react-native / react-native-web still resolve to the wrapped components (via the resolver / babel rewrite of their imports), the wrappers still receive the raw react-native-web implementations, the cycle is gone, and the app renders on web. Android/native is unaffected. Happy to open a PR if that helps.

Library version

3.0.7

Environment info

expo: ~57.0.7 (SDK 57 default template + expo-router)
react-native: 0.86.0
react: 19.2.3
react-native-web: ~0.21.0 (0.21.2 installed)
nativewind: 5.0.0-preview.4
react-native-css: 3.0.7
node: 24.15.0, pnpm: 11.10.0, OS: Linux
React Compiler enabled, bundling via `expo start` (dev) and `expo export` (prod) — both crash identically

Steps to reproduce

  1. Create an Expo SDK 57 app (create-expo-app, default template — it already uses Animated via its demo screens; any code path that touches react-native-web's Animated export triggers the bug).
  2. Install NativeWind v5 preview (nativewind@5.0.0-preview.4 + react-native-css@3.0.7) with the standard withNativewind metro config and nativewind/babel preset.
  3. expo start, open the web target.
  4. The bundle crashes at load with the error above before anything renders.

Reproducible example repository

The full root-cause analysis and a verified fix are included above; my project is private, but I can extract a minimal public repro from the steps above if needed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    auto-triagedIssue has been automatically triaged by the auto-triage workflowbugSomething isn't workingconfirmedBug reproduced and confirmed by triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions