-
Notifications
You must be signed in to change notification settings - Fork 153
Description
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: falseRe.Pack Version
5.0.0-rc.12
Reproduction
Tricky to create a reproduction URL
Steps to reproduce
Tricky to create a reproduction URL