Vite + Webpack + Rspack with Module Federation

Vite is the first build tool Zephyr is able to handle Module Federation configuration directly. This guide aims to walk you through how you can deploy a Micro-Frontend application using the Official Vite Plugin from Module Federation. After this guide, you will have a React application consuming remote applications bundled by Vite, Rspack and Webpack deployed through Zephyr Cloud.

Prerequisites

If you don't have nvm to manage your node version yet, head to the official nvm guide to install it.

nvm use 20

Install Zephyr Plugins

For applications built with Vite:

Terminal
npm i vite-plugin-zephyr@latest

For applications built with Webpack and Rspack:

Terminal
npm i zephyr-webpack-plugin@latest

Example configuration

Four example build configuration for Vite, Rspack and Webpack.

Vite Host

vite.config.ts
1import { defineConfig } from 'vite';
2import react from '@vitejs/plugin-react';
3import { withZephyr } from 'vite-plugin-zephyr';
4
5const mfConfig = {
6  name: 'vite-host',
7  filename: 'remoteEntry.js',
8  remotes: {
9    'vite-remote': {
10      entry: 'http://localhost:5174/remoteEntry.js',
11      type: 'module',
12    },
13    vite_webpack: {
14      entry: 'http://localhost:8080/remoteEntry.js',
15      type: 'var',
16    },
17    vite_rspack: {
18      entry: 'http://localhost:8081/remoteEntry.js',
19      type: 'var',
20    },
21  },
22  shared: {
23    react: {
24      singleton: true,
25    },
26    'react-dom': {
27      singleton: true,
28    },
29  },
30};
31
32export default defineConfig({
33  plugins: [
34    react(), 
35    withZephyr({mfConfig}), // sequence matters
36    svgr({
37        svgrOptions: {
38          // svgr options
39        },
40      }),
41  ],
42  build: {
43    target: 'chrome89',
44    modulePreload: {
45        resolveDependencies: (_, deps: string[]) => {
46          // Only preload React packages and non-federated modules
47          return deps.filter((dep) => {
48            const isReactPackage = dep.includes('react') || dep.includes('react-dom');
49            const isNotRemoteEntry = !dep.includes('remoteEntry.js');
50
51            return isReactPackage && isNotRemoteEntry;
52          });
53        },
54      },
55  },
56});
Known issue

1. Enable top level await For vite-plugin-zephyr to work properly with your remotes, you need to set your build target to chrome89 to enable top level await, alternatively you can install vite-plugin-top-level-await plugin to enable it and use it in configuration:

vite.config.ts
plugins: [
    react(),
    withZephyr({
      name: 'viteViteHost',
      remotes: {
       ...
      },
      filename: 'remoteEntry-[hash].js',
      manifest: true,
      shared: {
        vue: {},
        'react/': {
          requiredVersion: '18',
        },
        'react-dom': {},
      ...
      },
      runtimePlugins: ['./src/mfPlugins'],
    }),
    // If you set build.target: "chrome89", you can remove this plugin
    false && topLevelAwait(),
  ],
    build: {
    target: 'chrome89',
  },

2. Sequence of plugins

Because of how Vite and Rollup are exposing hooks and processing modules in plugins, the sequence of plugins matters where withZephyr() should be after react() but before any other plugins. For example, if svgr() is before withZephyr() it would unexpectedly interrupt the transformed output bundle.

3. Shared dependencies

If the application doesn't show up in the browser, you might be experiencing problems related to dependencies (one of the potential issues), please rememeber to configure modulePreload in your vite.config.ts:

vite.config.ts
modulePreload: {
        resolveDependencies: (_, deps: string[]) => {
          // Only preload React packages and non-federated modules
          return deps.filter((dep) => {
            const isReactPackage = dep.includes('react') || dep.includes('react-dom');
            const isNotRemoteEntry = !dep.includes('remoteEntry.js');

            return isReactPackage && isNotRemoteEntry;
          });
        },
      },

Vite Remote

vite.config.ts
1import { defineConfig } from 'vite';
2import react from '@vitejs/plugin-react';
3import { withZephyr } from 'vite-plugin-zephyr';
4
5const mfConfig = {
6  name: 'vite-remote',
7  filename: 'remoteEntry.js',
8  exposes: {
9    './Button': './src/Button',
10  },
11  shared: ['react', 'react-dom'],
12};
13
14// https://vitejs.dev/config/
15export default defineConfig({
16  plugins: [
17    react(), 
18    withZephyr({ mfConfig })
19  ],
20  experimental: {
21    renderBuiltUrl() {
22      return { relative: true };
23    },
24  },
25  build: {
26    target: 'chrome89',
27  },
28});

Rspack Remote

Example project created via npx create-mf-app

rspack.config.js
1const rspack = require('@rspack/core');
2const refreshPlugin = require('@rspack/plugin-react-refresh');
3const isDev = process.env.NODE_ENV === 'development';
4const path = require('path');
5const { withZephyr } = require('zephyr-webpack-plugin');
6
7const printCompilationMessage = require('./compilation.config.js');
8
9/**
10 * @type {import('@rspack/cli').Configuration}
11 */
12module.exports = withZephyr()({
13  context: __dirname,
14  entry: {
15    main: './src/index.tsx',
16  },
17
18  devServer: {
19    port: 8081,
20    historyApiFallback: true,
21    watchFiles: [path.resolve(__dirname, 'src')],
22    onListening: function (devServer) {
23      const port = devServer.server.address().port;
24
25      printCompilationMessage('compiling', port);
26
27      devServer.compiler.hooks.done.tap('OutputMessagePlugin', (stats) => {
28        setImmediate(() => {
29          if (stats.hasErrors()) {
30            printCompilationMessage('failure', port);
31          } else {
32            printCompilationMessage('success', port);
33          }
34        });
35      });
36    },
37  },
38  experiments: {
39    css: true,
40  },
41  resolve: {
42    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
43  },
44  module: {
45    rules: [
46      {
47        test: /\.(svg|png)$/,
48        type: 'asset',
49      },
50      {
51        test: /\.css$/,
52        use: [
53          {
54            loader: 'postcss-loader',
55            options: {
56              postcssOptions: {
57                plugins: {
58                  tailwindcss: {},
59                  autoprefixer: {},
60                },
61              },
62            },
63          },
64        ],
65        type: 'css',
66      },
67      {
68        test: /\.(jsx?|tsx?)$/,
69        use: [
70          {
71            loader: 'builtin:swc-loader',
72            options: {
73              sourceMap: true,
74              jsc: {
75                parser: {
76                  syntax: 'typescript',
77                  tsx: true,
78                },
79                transform: {
80                  react: {
81                    runtime: 'automatic',
82                    development: isDev,
83                    refresh: isDev,
84                  },
85                },
86                target: 'es2020',
87              },
88            },
89          },
90        ],
91      },
92    ],
93  },
94  plugins: [
95    new rspack.container.ModuleFederationPlugin({
96      name: 'vite_rspack',
97      filename: 'remoteEntry.js',
98      exposes: {
99        './Image': './src/Image',
100      },
101      shared: ['react', 'react-dom'],
102    }),
103    new rspack.DefinePlugin({
104      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
105    }),
106    new rspack.ProgressPlugin({}),
107    new rspack.HtmlRspackPlugin({
108      template: './src/index.html',
109    }),
110    isDev ? new refreshPlugin() : null,
111  ].filter(Boolean),
112});

Webpack Remote

Example project created via npx create-mf-app

webpack.config.js
1const HtmlWebPackPlugin = require('html-webpack-plugin');
2//const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
3const path = require('path');
4const Dotenv = require('dotenv-webpack');
5const { withZephyr } = require('zephyr-webpack-plugin');
6const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
7
8const deps = require('./package.json').dependencies;
9
10const printCompilationMessage = require('./compilation.config.js');
11
12module.exports = (_, argv) =>
13  withZephyr()({
14    output: {
15      publicPath: 'auto',
16    },
17
18    resolve: {
19      extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
20    },
21
22    devServer: {
23      port: 8080,
24      historyApiFallback: true,
25      watchFiles: [path.resolve(__dirname, 'src')],
26      onListening: function (devServer) {
27        const port = devServer.server.address().port;
28
29        printCompilationMessage('compiling', port);
30
31        devServer.compiler.hooks.done.tap('OutputMessagePlugin', (stats) => {
32          setImmediate(() => {
33            if (stats.hasErrors()) {
34              printCompilationMessage('failure', port);
35            } else {
36              printCompilationMessage('success', port);
37            }
38          });
39        });
40      },
41    },
42
43    module: {
44      rules: [
45        {
46          test: /\.(svg|png)$/,
47          type: 'asset',
48        },
49        {
50          test: /\.m?js/,
51          type: 'javascript/auto',
52          resolve: {
53            fullySpecified: false,
54          },
55        },
56        {
57          test: /\.(css|s[ac]ss)$/i,
58          use: ['style-loader', 'css-loader', 'postcss-loader'],
59        },
60        {
61          test: /\.(ts|tsx|js|jsx)$/,
62          exclude: /node_modules/,
63          use: {
64            loader: 'babel-loader',
65          },
66        },
67      ],
68    },
69
70    plugins: [
71      new ModuleFederationPlugin({
72        name: 'vite_webpack',
73        filename: 'remoteEntry.js',
74        exposes: {
75          './Image': './src/Image',
76        },
77        shared: {
78          react: {
79            singleton: true,
80          },
81          'react-dom': {
82            singleton: true,
83          },
84        },
85      }),
86      new HtmlWebPackPlugin({
87        template: './src/index.html',
88      }),
89      new Dotenv(),
90    ],
91  });