callstack / repack

A Webpack-based toolkit to build your React Native application with full support of Webpack ecosystem.
https://re-pack.dev
MIT License
1.48k stars 107 forks source link

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

Closed msvargas closed 8 months ago

msvargas commented 2 years ago

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 commented 2 years ago

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

ScriptedAlchemy commented 2 years ago

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

msvargas commented 2 years ago

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

ScriptedAlchemy commented 2 years ago

Should already be on npm as a beta tag

zamotany commented 2 years ago

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

msvargas commented 2 years ago

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

ScriptedAlchemy commented 2 years ago

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

msvargas commented 2 years ago

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: ```javascript 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

zamotany commented 2 years ago

@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.

msvargas commented 2 years ago

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

zamotany commented 2 years ago

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

n6fab commented 2 years ago

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?

msvargas commented 2 years ago

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

n6fab commented 2 years ago

@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?

yleley commented 2 years ago

How to build release file(apk)

DivyatejaChe commented 2 years ago

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?

msvargas commented 1 year ago

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

github-actions[bot] commented 9 months ago

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] commented 8 months ago

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.