repack icon indicating copy to clipboard operation
repack copied to clipboard

How to implement react-native-web + repack + Module Federation

Open msvargas opened this issue 3 years ago • 15 comments

Hi,

I want to know if it is possible to reuse the same code on web and mobile (iOS,Android) using react-native-web + Module Federation and what is the best approach or implementation, maybe some example, I really appreciate it!

thank you very much

msvargas avatar May 31 '22 03:05 msvargas

Maybe to help anyone, after research a lot, I found the way, the result:

image

I know we can improve the implementation but it works, maybes some comment regarding to the correct way to add this it would be great, thanks!

https://github.com/msvargas/microapps-poc/blob/main/WebHost/src/utils/loadComponent.js#L3

msvargas avatar May 31 '22 23:05 msvargas

repack v3 will bring better support to high level apis, so youll be able to just use import from, like youd do in a normal browser app. Wont be limited to only low-level api like in v2

ScriptedAlchemy avatar Jul 07 '22 11:07 ScriptedAlchemy

Hi @ScriptedAlchemy thanks for your answer, when will repack v3 be available? thanks

msvargas avatar Jul 11 '22 20:07 msvargas

Should already be on npm as a beta tag

ScriptedAlchemy avatar Jul 12 '22 01:07 ScriptedAlchemy

To be precise it's under next tag: https://www.npmjs.com/package/@callstack/repack

zamotany avatar Jul 12 '22 11:07 zamotany

Thanks @zamotany currently I implemented it with yarn v3 and it is awesome! good job and thank you for sharing, I have some issue at the moment but I everything looks good.


@zamotany I have issue with this line: https://github.com/callstack/repack-examples/blob/9be2bf7122c556635125c7282b9fdbb3698119f0/module-federation/app1/webpack.config.mjs#L43 Do you recommend to turn off the fast refresh for module federation? thanks

msvargas avatar Jul 13 '22 00:07 msvargas

Hot reloading doesnt work with module federation. It's insanely complex to support HMR since it would require all the modules graphs being aware of eachother and how the remote modules are used be eachother.

If possible, I usually just use live reload for remote changes. So just refresh the page if the remote was updated and only use hmr for the host app parts

ScriptedAlchemy avatar Jul 13 '22 05:07 ScriptedAlchemy

Hi guys @ScriptedAlchemy @zamotany, the new API ScriptManager and Federated is awesome, I'm trying to run Repack V3 on web I really appreciate any help or information, I get this error: image

Then, I've tried to fixed it, I've created new Webpack Plugin called, RepackWebTargetPlugin, but now I get this error image


const {
	RepackInitRuntimeModule,
} = require('@callstack/repack/dist/webpack/plugins/RepackTargetPlugin/runtime/RepackInitRuntimeModule');
const {
	RepackLoadScriptRuntimeModule,
} = require('@callstack/repack/dist/webpack/plugins/RepackTargetPlugin/runtime/RepackLoadScriptRuntimeModule');



class RepackWebTargetPlugin {
	apply(compiler) {
		const globalObject = compiler.options.output.globalObject || 'global';

		// Normalize global object.
		new webpack.BannerPlugin({
			raw: true,
			entryOnly: true,
			banner: webpack.Template.asString([
				`/******/ var ${globalObject} = ${globalObject} || this || new Function("return this")() || ({}); // repackGlobal'`,
				'/******/',
			]),
		}).apply(compiler);

		compiler.hooks.compilation.tap('RepackTargetPlugin', (compilation) => {
			compilation.hooks.additionalTreeRuntimeRequirements.tap('RepackTargetPlugin', (chunk, runtimeRequirements) => {
				runtimeRequirements.add(webpack.RuntimeGlobals.startupOnlyAfter);

				// Add code initialize Re.Pack's runtime logic.
				compilation.addRuntimeModule(
					chunk,
					new RepackInitRuntimeModule({
						chunkId: chunk.id ?? undefined,
						globalObject,
						chunkLoadingGlobal: compiler.options.output.chunkLoadingGlobal,
						hmrEnabled: compilation.options.mode === 'development' && this.config?.hmr,
					}),
				);
			});

			// Overwrite Webpack's default load script runtime code with Re.Pack's implementation
			// specific to React Native.
			compilation.hooks.runtimeRequirementInTree.for(webpack.RuntimeGlobals.loadScript).tap('RepackTargetPlugin', (chunk) => {
				compilation.addRuntimeModule(chunk, new RepackLoadScriptRuntimeModule(chunk.id ?? undefined));

				// Return `true` to make sure Webpack's default load script runtime is not added.
				return true;
			});
		});
	}
}
My webpack config for web:
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
const Dotenv = require('dotenv-webpack');
const path = require('path');
const Repack = require('@callstack/repack');
const {
	RepackInitRuntimeModule,
} = require('@callstack/repack/dist/webpack/plugins/RepackTargetPlugin/runtime/RepackInitRuntimeModule');
const {
	RepackLoadScriptRuntimeModule,
} = require('@callstack/repack/dist/webpack/plugins/RepackTargetPlugin/runtime/RepackLoadScriptRuntimeModule');

class RepackWebTargetPlugin {
	apply(compiler) {
		const globalObject = compiler.options.output.globalObject || 'global';

		// Normalize global object.
		new webpack.BannerPlugin({
			raw: true,
			entryOnly: true,
			banner: webpack.Template.asString([
				`/******/ var ${globalObject} = ${globalObject} || this || new Function("return this")() || ({}); // repackGlobal'`,
				'/******/',
			]),
		}).apply(compiler);

		compiler.hooks.compilation.tap('RepackTargetPlugin', (compilation) => {
			compilation.hooks.additionalTreeRuntimeRequirements.tap('RepackTargetPlugin', (chunk, runtimeRequirements) => {
				runtimeRequirements.add(webpack.RuntimeGlobals.startupOnlyAfter);

				// Add code initialize Re.Pack's runtime logic.
				compilation.addRuntimeModule(
					chunk,
					new RepackInitRuntimeModule({
						chunkId: chunk.id ?? undefined,
						globalObject,
						chunkLoadingGlobal: compiler.options.output.chunkLoadingGlobal,
						hmrEnabled: compilation.options.mode === 'development' && this.config?.hmr,
					}),
				);
			});

			// Overwrite Webpack's default load script runtime code with Re.Pack's implementation
			// specific to React Native.
			compilation.hooks.runtimeRequirementInTree.for(webpack.RuntimeGlobals.loadScript).tap('RepackTargetPlugin', (chunk) => {
				compilation.addRuntimeModule(chunk, new RepackLoadScriptRuntimeModule(chunk.id ?? undefined));

				// Return `true` to make sure Webpack's default load script runtime is not added.
				return true;
			});
		});
	}
}

const mode = process.env.NODE_ENV || 'development';
const flavor = process.env.APP_FLAVOR || 'development';
module.exports = {
	devtool: 'eval-source-map',
	entry: './index.js',
	mode: mode,
	devServer: {
		static: {
			directory: path.join(__dirname, 'dist'),
		},
		port: 8080,
		historyApiFallback: true,
	},
	output: {
		publicPath: 'auto',
	},
	resolve: {
		extensions: ['.web.ts', '.web.tsx', '.web.js', '.web.jsx', '.tsx', '.ts', '.jsx', '.js'],
		alias: {
			'@cp/common': [
				path.resolve(__dirname, '../common/src/modules'),
				path.resolve(__dirname, '../common/src/services/modules'),
				path.resolve(__dirname, '../common/src'),
			],
			'react-native$': 'react-native-web',
			'@sentry/react-native': '@sentry/react',
			'react-native-linear-gradient$': 'react-native-web-linear-gradient',
		},
	},
	module: {
		rules: [
			{
				test: /\.[jt]sx?$/,
				include: [
					/src/,
					/node_modules(.*[/\\])+@react-native/,
					/node_modules(.*[/\\])+@react-navigation/,
					/node_modules(.*[/\\])+@react-native-community/,
					/node_modules(.*[/\\])+react-native-animatable/,
					/node_modules(.*[/\\])+react-native-element-dropdown/,
					/node_modules(.*[/\\])+babel-plugin-jsx-control-statements[/\\]components/,
					/node_modules(.*[/\\])+react-native-screens/,
					/node_modules(.*[/\\])+@callstack[/\\]repack/,
					/node_modules(.*[/\\])+@native-html[/\\]/,
					/node_modules(.*[/\\])+apptentive-react-native/,
					/node_modules(.*[/\\])+@sentry/,
					/node_modules(.*[/\\])+@react-native-firebase/,
					/node_modules(.*[/\\])+react-native-skeleton-placeholder/,
					/@cp[/\\]system-ui[/\\]MaskedView/,
				],
				use: 'babel-loader',
			},
			{
				test: /\.m?js$/,
				type: 'javascript/auto',
				resolve: {
					fullySpecified: false,
				},
			},
			{
				test: /\.svg$/,
				use: [
					{
						loader: '@svgr/webpack',
					},
				],
			},
			{
				test: /\.css$/i,
				use: ['style-loader', 'css-loader'],
			},
			{
				test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
				use: [
					{
						loader: 'file-loader',
						options: {
							name: '[name].[ext]',
							outputPath: 'fonts/',
						},
					},
				],
			},
			// https://necolas.github.io/react-native-web/docs/multi-platform/
			{
				test: /\.(gif|jpe?g|png)$/,
				use: {
					loader: 'url-loader',
					options: {
						name: '[name].[ext]',
						esModule: false,
					},
				},
			},
		],
	},
	plugins: [
		new Dotenv({
			path: `../../env/.env.${flavor}`, // load this now instead of the ones in '.env'
			safe: '../../env/.env.example', // load '.env.example' to verify the '.env' variables are all set. Can also be a string to a different file.
			allowEmptyValues: true, // allow empty variables (e.g. `FOO=`) (treat it as empty string, rather than missing)
			systemvars: false, // load all the predefined 'process.env' variables which will trump anything local per dotenv specs.
			silent: false, // hide any errors
			defaults: false, // load '.env.defaults' as the default values if empty.
			prefix: 'process.env.', // reference your env variables as 'import.meta.env.ENV_VAR'.
		}),
		new webpack.DefinePlugin({
			__DEV__: JSON.stringify(mode === 'development'),
		}),
		// new RepackWebTargetPlugin({ hmr: false }),
		new ModuleFederationPlugin({
			name: 'web-host',
			shared: {
				'@reduxjs/toolkit': {
					singleton: true,
					eager: true,
					requiredVersion: '^1.7.2',
				},
				'redux-persist': {
					singleton: true,
					eager: true,
					requiredVersion: '^6.0.0',
				},
				'react-redux': {
					singleton: true,
					eager: true,
					requiredVersion: '^7.2.8',
				},
				react: {
					singleton: true,
					eager: true,
					requiredVersion: '^17.0.2',
				},
				'react-native': {
					singleton: true,
					eager: true,
					requiredVersion: '*',
				},
				'react-native-web': {
					singleton: true,
				},
				'react-native-svg': {
					singleton: true,
					eager: true,
					requiredVersion: '*',
				},
			},
		}),
		new HtmlWebpackPlugin({
			template: './public/index.html',
			favicon: './public/favicon.ico',
		}),
		new CopyPlugin({
			patterns: [{ from: 'public', to: 'public' }],
		}),
	],
};

Thanks

msvargas avatar Jul 24 '22 22:07 msvargas

@msvargas RepackWebTargetPlugin will not fix the problem. ScriptManager was never intended to runned in the browser, but given that it's used inside source code it would make sense to make it usable on web too. So, definitely remove RepackWebTargetPlugin and you'll have to wait for a web-compatible ScriptManager.

zamotany avatar Jul 25 '22 09:07 zamotany

Hey guys, @zamotany, for android the localhost doesn't work, I have to use 10.0.2.2, Is this the expected behavior? Thanks image

msvargas avatar Jul 25 '22 21:07 msvargas

Yes, localhost on Android points to the Android device itself. It's expected and we should be more clear about it in the docs.

zamotany avatar Jul 25 '22 22:07 zamotany

Thank for your answer @msvargas! I've tried your and @zamotany suggest, I change in Root.js the localhost, but now:

ChunkManager.configure({
  forceRemoteChunkResolution: true,
  resolveRemoteChunk: async (chunkId, parentId) => {
    let url;

    switch (parentId) {
      case 'app1':
        url = `http://10.0.2.2:9000/${chunkId}.chunk.bundle`;
        break;
      case 'app2':
        url = `http://10.0.2.2:9001/${chunkId}.chunk.bundle`;
        break;
      case 'main':
      default:
        url =
          {
            // containers
            app1: 'http://10.0.2.2:9000/app1.container.bundle',
            app2: 'http://10.0.2.2:9001/app2.container.bundle',
          }[chunkId] ?? `http://10.0.2.2:8081/${chunkId}.chunk.bundle`;
        break;
    }

if I use Android Studio the error is the same:

same_error

Am I doing something wrong?

n6fab avatar Aug 04 '22 11:08 n6fab

I'm not sure, it is very weird, what repack version are you using?

msvargas avatar Aug 25 '22 15:08 msvargas

@callstack/repack 2.5.1

Could it be related to this? https://github.com/callstack/repack/issues/138 (the solution doesn't work for me)

It happens when building app1 on first launch, is it perhaps the loading wait time too long that generates the error?

n6fab avatar Aug 26 '22 09:08 n6fab

How to build release file(apk)

yleley avatar Sep 21 '22 08:09 yleley

Hello @msvargas , this is exciting - to include two isolated react-native apps into a container react-native app; Can you please do a tutorial video on how you achieved this? or quote a reference material?

DivyatejaChe avatar Oct 27 '22 18:10 DivyatejaChe

Hello @msvargas , this is exciting - to include two isolated react-native apps into a container react-native app; Can you please do a tutorial video on how you achieved this? or quote a reference material?

Hi, Have you already check this video? https://youtu.be/2DEoB7Vylqo

msvargas avatar Nov 26 '22 21:11 msvargas

This issue has been marked as stale because it has been inactive for 30 days. Please update this issue or it will be automatically closed in 14 days.

github-actions[bot] avatar Feb 03 '24 00:02 github-actions[bot]

This issue has been automatically closed because it has been inactive for more than 14 days. Please reopen if you want to add more context.

github-actions[bot] avatar Feb 18 '24 00:02 github-actions[bot]