Dynamic resolution of Tailwind (Nativewind) tva() utility causes app crash for MFE apps in Release / Production builds
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
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 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
@sahajarora1286 can you try updating to latest Re.Pack version and seeing if that helps?
5.0.0-rc.12is 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)
Could you maybe help with resolving this ? Thanks in advance!
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.
#1315 should fix your issues with nativewind on the latest version