repack icon indicating copy to clipboard operation
repack copied to clipboard

Dynamic resolution of Tailwind (Nativewind) tva() utility causes app crash for MFE apps in Release / Production builds

Open sahajarora1286 opened this issue 4 months ago • 4 comments

Describe the bug

Hello, I have a React Native Super App configured with Re.pack and Webpack. It contains a host app and some mini-apps (MFEs). It utilizes Nativewind / Tailwind and a UI library called Gluestack UI. Gluestack UI components use the tva() utility from tailwind to generate the resulting className strings based on component props (variants, primary, secondary, etc.).

The entire setup works perfectly fine with all Nativewind styling working as expected in dev builds of host and MFE apps. However, things change for release builds. The host app and its components render just fine in the release build, but when the host app loads an MFE app screen (MFE release build), the MFE app crashes because apparently the tva() utility's dynamic generation of className based on props returns undefined.

For e.g, this is a Gluestack UI component (ButtonText) used in both host and MFE app app1:

const ButtonText = React.forwardRef<
  React.ComponentRef<typeof UIButton.Text>,
  IButtonTextProps
>(function ButtonText({ className, variant, size, action, ...props }, ref) {
  const {
    variant: parentVariant,
    size: parentSize,
    action: parentAction,
  } = useStyleContext(SCOPE);

  return (
    <UIButton.Text
      ref={ref}
      {...props}
      className={buttonTextStyle({
        parentVariants: {
          variant: parentVariant,
          size: parentSize,
          action: parentAction,
        },
        variant,
        size,
        action,
        class: className,
      })}
    />
  );
});

const buttonTextStyle = tva({
  base: 'text-typography-0 font-semibold web:select-none',
  parentVariants: {
    action: {
      primary:
        'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
      secondary:
        'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
      positive:
        'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
      negative:
        'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
    },
    variant: {
      link: 'data-[hover=true]:underline data-[active=true]:underline',
      outline: '',
      solid:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    size: {
      xs: 'text-xs',
      sm: 'text-sm',
      md: 'text-base',
      lg: 'text-lg',
      xl: 'text-xl',
    },
  },
  parentCompoundVariants: [
    {
      variant: 'solid',
      action: 'primary',
      class:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    {
      variant: 'solid',
      action: 'secondary',
      class:
        'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
    },
    {
      variant: 'solid',
      action: 'positive',
      class:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    {
      variant: 'solid',
      action: 'negative',
      class:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    {
      variant: 'outline',
      action: 'primary',
      class:
        'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
    },
    {
      variant: 'outline',
      action: 'secondary',
      class:
        'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
    },
    {
      variant: 'outline',
      action: 'positive',
      class:
        'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
    },
    {
      variant: 'outline',
      action: 'negative',
      class:
        'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
    },
  ],
});

The crash causing code in this specific component is the className assignment to UIButton.Text via buttonTextStyle(...). buttonTextStyle(...) uses tva(...) to generate the resulting className string based on dynamic props received, e.g variant, action, size.

On further debugging I found that the real culprit seems to be the compoundVariants in the tva input object. When ButtonText is given a variant (e.g 'solid') and an action (e.g 'primary'), and if there is a compoundVariant in the tva input for 'solid' and 'primary', that's when the crash occurs. Seems like tva is unable to generate the className string for compoundVariants.

So I believe the real culprit here is tree-shaking that takes place when building the production version of the MFE app.

Here's the output and optimization configuration that gets applied to the MFE prod build in webpack:

 /**
     * Configures output.
     * It's recommended to leave it as it is unless you know what you're doing.
     * By default Webpack will emit files into the directory specified under `path`. In order for the
     * React Native app use them when bundling the `.ipa`/`.apk`, they need to be copied over with
     * `Repack.OutputPlugin`, which is configured by default inside `Repack.RepackPlugin`.
     */
    output: {
      clean: true,
      hashFunction: 'xxhash64',
      filename: 'index.bundle',
      chunkFilename: '[name].chunk.bundle',
    },
    /**
     * Configures optimization of the built bundle.
     */
    optimization: {
      /** Enables minification based on values passed from React Native CLI or from fallback. */
      minimize,
      /** Configure minimizer to process the bundle. */
      minimizer: [
        new TerserPlugin({
          test: /\.(js)?bundle(\?.*)?$/i,
          /**
           * Prevents emitting text file with comments, licenses etc.
           * If you want to gather in-file licenses, feel free to remove this line or configure it
           * differently.
           */
          extractComments: false,
          terserOptions: {
            format: {
              comments: false,
            },
          },
        }),
      ],
      chunkIds: 'named',
    },

The MFE prod build generates several build files, most important of them being app1.container.bundle, which is what the host app loads at runtime via ScriptManager.
On inspecting the contents of app1.container.bundle, I realized that it doesn't contain any tailwind classnames like text-primary-600 or typography-0. I also realized that among the built files are some other source files that are chunked, one of them being the Screen that imports and renders the ButtonText component. In this chunk file, I could find references to tailwind classes.

I don't understand how module federation works in the sense that host only loads app1.container.bundle, a file which does not have access to tailwind classes (due to which the tva() function call fails and leads to a crash). How do I make sure that the nativewind classes are generated for the MFE code in production builds and are available to the MFE bundle when host dynamically loads the MFE ? This should ensure that dynamic className generated via tva() no longer crashes.

Note: The app and MFE builds work just fine if they are built in production mode but with optimization.minimize = false in webpack config. So things really go wrong when minimizing the MFE build.

The crash log doesn't indicate much other than a cryptic error :

E/ReactNativeJS: TypeError: undefined is not a function
    
    This error is located at:
        in Unknown
        in Unknown
        in Unknown
        in Suspense
        in ErrorBoundary
        in RemoteComponent
        in RemoteAppEntryRouteHandler
        in RCTView
        in Unknown
        in Unknown
        in RemoteAppEntryScreen
        in StaticContainer
        in EnsureSingleNavigator
        in SceneView
        in RCTView
        in Unknown
        in RCTView
        in Unknown
        in Unknown
        in Unknown
        in Background
        in Screen
        in RNSScreen
        in Unknown
        in Suspender
        in Suspense
        in Freeze
        in DelayedFreeze
        in InnerScreen
        in Unknown
        in MaybeScreen
        in RNSScreenContainer
        in ScreenContainer
        in MaybeScreenContainer
        in RCTView
        in Unknown
        in SafeAreaProviderCompat
        in BottomTabView
        in PreventRemoveProvider
        in NavigationContent
        in Unknown
        in BottomTabNavigator
        in RCTView
        in Unknown
        in RCTView
        in Unknown
        in Unknown
        in AnimatedComponent(View)
        in Unknown
        in RCTView
        in Unknown
        in Unknown
        in AnimatedComponent(View)
        in Unknown
        in Wrap
        in AnimatedComponent(Wrap)
        in Unknown
        in GestureDetector
        in RNGestureHandlerRootView
        in GestureHandlerRootView
        in Drawer
        in ThemeProvider
        in EnsureSingleNavigator
        in BaseNavigationContainer
        in NavigationContainerInner
        in RCTView
        in Unknown
        in Unknown
        in RootNavigationMobile
        in AppNavigationContainer
        in PersistGate
        in Provider
        in ErrorBoundary
        in SafeAreaEnv
        in RNCSafeAreaProvider
        in SafeAreaProvider
        in SafeAreaProviderShim
        in ToastProvider
        in PortalProvider
        in RCTView
        in Unknown
        in Unknown
        in GluestackUIProvider
        in App
        in RCTView
        in Unknown
        in Unknown
        in AppContainer, js engine: hermes
Image

Any help with this would be super appreciated.

System Info

System:
  OS: macOS 15.5
  CPU: (10) arm64 Apple M1 Max
  Memory: 82.81 MB / 64.00 GB
  Shell:
    version: 3.2.57
    path: /bin/bash
Binaries:
  Node:
    version: 18.20.0
    path: ~/.nvm/versions/node/v18.20.0/bin/node
  Yarn:
    version: 1.19.0
    path: /opt/homebrew/bin/yarn
  npm:
    version: 10.5.0
    path: ~/.nvm/versions/node/v18.20.0/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.15.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK: Not Found
  Android SDK: Not Found
IDEs:
  Android Studio: 2021.3 AI-213.7172.25.2113.9123335
  Xcode:
    version: /undefined
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.5
    path: /usr/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react: Not Found
  react-native:
    installed: 0.74.6
    wanted: "0.74"
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: false

Re.Pack Version

5.0.0-rc.12

Reproduction

Tricky to create a reproduction URL

Steps to reproduce

Tricky to create a reproduction URL

sahajarora1286 avatar Sep 11 '25 20:09 sahajarora1286

@sahajarora1286 can you try updating to latest Re.Pack version and seeing if that helps? 5.0.0-rc.12 is a very old and not-stable version so it might be something that's likely fixed in next releases

jbroma avatar Sep 15 '25 08:09 jbroma

@sahajarora1286 can you try updating to latest Re.Pack version and seeing if that helps? 5.0.0-rc.12 is a very old and not-stable version so it might be something that's likely fixed in next releases

Thanks for responding, @jbroma !

After upgrading to @callstack/repack 5.2.1, I get an error when I launch the Android app in dev mode, before it even tries to hit the host webpack dev server:

Exception in native call from JS
                                                                                                    com.facebook.react.common.JavascriptException: TypeError: right operand of 'in' is not an object, js engine: hermes, stack:
                                                                                                    anonymous@41:3480392
                                                                                                    y@41:3480225
                                                                                                    h@41:3481309
                                                                                                    anonymous@61:19545
                                                                                                    86843@41:52037
                                                                                                    __webpack_require__@61:91314
                                                                                                    t@61:92396
                                                                                                    87631@41:51032
                                                                                                    __webpack_require__@61:91314
                                                                                                    t@61:92396
                                                                                                    45187@41:47959
                                                                                                    __webpack_require__@61:91314
                                                                                                    t@61:92396
                                                                                                    46708@41:66438
                                                                                                    __webpack_require__@61:91314
                                                                                                    t@61:92396
                                                                                                    12329@1:1711075
                                                                                                    __webpack_require__@61:91314
                                                                                                    t@61:92325
                                                                                                    anonymous@61:108450
                                                                                                    global@61:108459
                                                                                                    
                                                                                                    	at com.facebook.react.modules.core.ExceptionsManagerModule.reportException(ExceptionsManagerModule.java:65)
                                                                                                    	at java.lang.reflect.Method.invoke(Native Method)
                                                                                                    	at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
                                                                                                    	at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:146)
                                                                                                    	at com.facebook.jni.NativeRunnable.run(Native Method)
                                                                                                    	at android.os.Handler.handleCallback(Handler.java:942)
                                                                                                    	at android.os.Handler.dispatchMessage(Handler.java:99)
                                                                                                    	at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:27)
                                                                                                    	at android.os.Looper.loopOnce(Looper.java:201)
                                                                                                    	at android.os.Looper.loop(Looper.java:288)
                                                                                                    	at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:233)
Image

Could you maybe help with resolving this ? Thanks in advance!

sahajarora1286 avatar Sep 25 '25 03:09 sahajarora1286

Update:

I ended up migrating to RsPack and latest Repack (v5.2.1). Along with some other fixes, I was able to at-least the host app launching in dev mode without any errors.

However, Nativewind doesn't seem to be working at all now, i.e, nativewind styles are no longer applied. They worked when webpack was the bundler.

Is there any additional work that needs to be done to Nativewind configuration to get it working with RsPack ?

Snippets from my rspack.config.mjs :

  module: {
      rules: [
        ...Repack.getJsTransformRules(),
        ...Repack.getAssetTransformRules(),
      ],
    },
    plugins: [
      /**
       * Configure other required and additional plugins to make the bundle
       * work in React Native and provide good development experience with
       * sensible defaults.
       *
       * `Repack.RepackPlugin` provides some degree of customization, but if you
       * need more control, you can replace `Repack.RepackPlugin` with plugins
       * from `Repack.plugins`.
       */
      new Repack.RepackPlugin(),
      new ReanimatedPlugin(),
      new NativeWindPlugin(),
      new rspack.IgnorePlugin({
        resourceRegExp: /^@react-native-masked-view/,
      }),
    ],

After adding verifyInstallation from nativewind to my app entry component, here's a more specific error that I get from nativewind:

jsxImportSource was not set to 'nativewind'.

Nativewind's official setup involving adding the nativewind preset and some config to babel config. However now that babel is not being used due to SWC, we're probably missing that step ?

I tried updating Repack.getJsTransformRules as such:

...Repack.getJsTransformRules({
          swc: {
            jsxRuntime: "automatic",
            importSource: "nativewind",
          },
        }),

which led to another error:

Error: Your 'metro.config.js' has overridden the 'config.resolver.resolveRequest' config setting in a non-composable manner. Your styles will not work until this issue is resolved. Note that 'require('metro-config').mergeConfig' is a shallow merge and does not compose existing resolveRequest functions together.

I also do have "jsxImportSource": "nativewind" set in my tsconfig.

sahajarora1286 avatar Sep 25 '25 07:09 sahajarora1286

#1315 should fix your issues with nativewind on the latest version

dannyhw avatar Dec 09 '25 16:12 dannyhw