Live
Black Hat USADark ReadingBlack Hat AsiaAI BusinessDutchess to host artificial intelligence summit at Marist in Poughkeepsie - Daily FreemanGoogle News: AIAnthropic’s Catastrophic Leak May Have Just Handed China the Blueprints to Claude Al - TipRanksGoogle News: ClaudeMeta's AI push is reshaping how work gets done inside the companyBusiness InsiderOpenAI's Fidji Simo Is Taking Medical Leave Amid an Executive Shake-Up - WIREDGoogle News: OpenAIAI & Tech brief: Ireland ascendant - The Washington PostGNews AI EUPeople would rather have an Amazon warehouse in their backyard than a data centerTechCrunch AITake-Two lays off its head of AI and several team members just two months after the CEO said it was embracing Gen AI - TweakTownGoogle News: Generative AIOpenAI Buys TBPN Tech Talk Show for Enterprise Client Outreach - News and Statistics - IndexBoxGoogle News: OpenAILenovo Legion Go 2 suddenly costs $650 more as RAMageddon lays waste to gaming hardwareThe VergeResearchers Discover How to Add Psilocybin, DMT, and Other Psychedelics to TobaccoGizmodoOpenAI’s Top Executive Fidji Simo To Take Medical Leave from Company - WSJGoogle News: OpenAIBlack Hat USADark ReadingBlack Hat AsiaAI BusinessDutchess to host artificial intelligence summit at Marist in Poughkeepsie - Daily FreemanGoogle News: AIAnthropic’s Catastrophic Leak May Have Just Handed China the Blueprints to Claude Al - TipRanksGoogle News: ClaudeMeta's AI push is reshaping how work gets done inside the companyBusiness InsiderOpenAI's Fidji Simo Is Taking Medical Leave Amid an Executive Shake-Up - WIREDGoogle News: OpenAIAI & Tech brief: Ireland ascendant - The Washington PostGNews AI EUPeople would rather have an Amazon warehouse in their backyard than a data centerTechCrunch AITake-Two lays off its head of AI and several team members just two months after the CEO said it was embracing Gen AI - TweakTownGoogle News: Generative AIOpenAI Buys TBPN Tech Talk Show for Enterprise Client Outreach - News and Statistics - IndexBoxGoogle News: OpenAILenovo Legion Go 2 suddenly costs $650 more as RAMageddon lays waste to gaming hardwareThe VergeResearchers Discover How to Add Psilocybin, DMT, and Other Psychedelics to TobaccoGizmodoOpenAI’s Top Executive Fidji Simo To Take Medical Leave from Company - WSJGoogle News: OpenAI
AI NEWS HUBbyEIGENVECTOREigenvector

Migrating a Webpack-Era Federated Module to Vite Without Breaking the Host Contract

DEV Communityby Mateus OliveiraApril 2, 202611 min read1 views
Source Quiz

A practical guide to migrating a federated remote to Vite, based on lessons from a real migration. I was tasked with updating a legacy React application that did not support Module Federation. That integration was added first so the app could run as a remote inside a larger host application. Later, the remote needed to migrate from Create React App (CRA) to Vite. By that point, the host already depended on the remote's loading behavior. The tricky part was not replacing CRA with Vite. It was preserving the runtime contract while only the remote changed bundlers. If you own a CRA or webpack-era remote that still has to load cleanly inside an existing host, this post covers the cleanup work beforehand, the core CRA-to-Vite swap, the federation-specific deployment fixes, and a local dev harne

A practical guide to migrating a federated remote to Vite, based on lessons from a real migration.

I was tasked with updating a legacy React application that did not support Module Federation. That integration was added first so the app could run as a remote inside a larger host application. Later, the remote needed to migrate from Create React App (CRA) to Vite. By that point, the host already depended on the remote's loading behavior. The tricky part was not replacing CRA with Vite. It was preserving the runtime contract while only the remote changed bundlers.

If you own a CRA or webpack-era remote that still has to load cleanly inside an existing host, this post covers the cleanup work beforehand, the core CRA-to-Vite swap, the federation-specific deployment fixes, and a local dev harness for debugging the full host loading sequence without redeploying every change.

Terms for reference

  • CRA: Create React App. For years it was the default easy on-ramp for React apps before being deprecated in 2025.

  • CRACO: Create React App Configuration Override

  • Module Federation: A way for one application to load code from another at runtime instead of bundling everything together up front.

  • Host: The application that loads another app at runtime.

  • Remote: The application that exposes code for the host to load.

  • Runtime contract: The files and exported APIs the host already expects.

Why migrate?

  • Dependabot alerts. The biggest issue was that the CRA dependency tree had kept accumulating a number of high-risk Dependabot alerts, and patching around them was getting harder to justify.

  • Slow builds. CRA and webpack took over a minute for a cold-start build.

  • Too many config layers. CRACO was overriding CRA's webpack config, plus custom build scripts for module federation.

  • Stale tooling. ESLint was still on the legacy .eslintrc format. Jest had its own separate config.

  • Dependency rot. Years of Dependabot patches left dozens of manual resolutions in the dependency manifest that nobody fully understood anymore.

The goal was not just "swap the build tool." It was to reduce dependency risk, simplify the toolchain, and leave the project in a state that another engineer could pick up. Vite had already earned a strong reputation. What was different now was that there was finally enough maintenance pressure to justify spending sprint time on the migration.

Step 1: Remove dead weight

Before touching the build tool, everything that would conflict with Vite or had become dead weight needed to go.

Remove webpack and Babel dependencies

Some dependencies werent really "dependencies" so much as assumptions about the old toolchain:

  • Babel macros like preval.macro that ran at compile time. Vite doesnt run your app through the same pipeline that a CRA stack does.

  • CRA-specific packages like react-scripts, craco, react-app-rewired

  • Packages like jsonwebtoken that were built for Node.js and rely on polyfills that webpack injected automatically. Vite does not do this, so if anything in the browser code imports Node.js built-ins like crypto or Buffer, it will break.

Remove stale deps and manual resolutions

The package dependencies were audited and around a dozen were removed. Then the pile of old manual resolutions that had accumulated from years of Dependabot fixes was cleared out. Most of those overrides were for transitive deps of packages that were already gone.

Check for Sass compatibility

Worth checking early: a shared design system was still using deprecated Sass @import patterns, and it had to be updated before the new toolchain would build cleanly.

Step 2: The CRA-to-Vite swap

With the codebase cleaned up, the core migration came down to a few straightforward steps:

  • Replace CRA/CRACO config with a single vite.config.ts

  • Move index.html from public/ to the project root and point it at the module entry

  • Rename REACT_APP_* env vars to VITE_*; in application code, replace process.env usage with import.meta.env

  • Update any legacy ReactDOM.render calls to createRoot

  • Modernize surrounding tooling where it made sense, like moving ESLint to flat config

  • Update scripts for vite, vite build, vite preview, and vitest

Replace Jest with Vitest

Once Vite was the build tool, Vitest was the obvious test runner. It shares the same config file, understands the same path aliases, and removed a lot of separate config glue.

Add the test config directly to vite.config.ts:

import { defineConfig } from 'vite';

export default defineConfig({ // ...build config above... test: { globals: true, environment: 'jsdom', setupFiles: './src/test/setup.ts', coverage: { reporter: ['text', 'html'], include: ['src/**/.{ts,tsx}'], }, }, });`

Enter fullscreen mode

Exit fullscreen mode

No separate jest.config.js. No babel-jest transform. No moduleNameMapper to keep in sync with path aliases.

Step 3: Module Federation with Vite

This is where the migration stopped being a normal bundler swap. The host still ran webpack and expected all of this to keep working:

host -> fetch asset-manifest.json host -> load remoteEntry.js host -> init shared scope host -> get exposed module host -> call inject(container, props) host -> later call unmount()

Enter fullscreen mode

Exit fullscreen mode

Configuring the federation plugin

Install @module-federation/vite and add it to your Vite config:

import react from '@vitejs/plugin-react'; import { federation } from '@module-federation/vite'; import { defineConfig } from 'vite';

export default defineConfig({ plugins: [ react(), federation({ name: 'remoteApp', filename: 'remoteEntry.js', exposes: { './RemoteModule': './src/remote/entry.ts', }, }), ], // ... });`

Enter fullscreen mode

Exit fullscreen mode

The exposed entry file should export the lifecycle functions the host expects:

// src/remote/entry.ts export { inject, unmount } from './RemoteModule'; export { default } from './RemoteModule';

Enter fullscreen mode

Exit fullscreen mode

import { MemoryRouter } from 'react-router-dom'; import { createRoot, type Root } from 'react-dom/client'; import App from '../App';

let root: Root | null = null;

export const inject = ( container: string | HTMLElement, props?: Record ): void => { const element = typeof container === 'string' ? document.getElementById(container) : container; if (!element) return;

// Guard against duplicate roots if the host mounts twice. root?.unmount();

root = createRoot(element); root.render(

); };

export const unmount = (): void => { if (root) { root.unmount(); root = null; } };`

Enter fullscreen mode

Exit fullscreen mode

Note: The inject(container, props) and unmount() API here is host-specific. MemoryRouter made sense because the embedded remote needed internal navigation but not deep-linkable standalone URLs. Standalone development used BrowserRouter instead.

Generating a host-compatible asset manifest

The host fetched asset-manifest.json and expected specific keys for remoteEntry.js and main.css. Vite produced a different file (manifest.json) with a different shape, so even after renaming the file, the host couldnt parse it.

The fix was a small Vite plugin that generates a compatible manifest after the build:

import { promises as fs } from 'node:fs'; import path from 'node:path'; import type { Plugin } from 'vite';

export const rewriteHostManifest = (): Plugin => ({ name: 'rewrite-host-manifest', async writeBundle(options, bundle) { const outputDir = options.dir || 'dist'; const files = Object.keys(bundle); const remoteEntry = files.find((file) => file.endsWith('remoteEntry.js')); const mainCss = files.find((file) => file.endsWith('.css')); if (!remoteEntry || !mainCss) { throw new Error('remoteEntry.js not found in bundle output'); } const manifest = { files: { 'remoteEntry.js': /${remoteEntry}, 'main.css': /${mainCss}, }, }; await fs.writeFile( path.join(outputDir, 'asset-manifest.json'), JSON.stringify(manifest, null, 2) ); }, });`

Enter fullscreen mode

Exit fullscreen mode

Add it to the plugins:

plugins: [  react(),  federation({ /* ... */ }),  rewriteHostManifest(), ],

Enter fullscreen mode

Exit fullscreen mode

Adapt the manifest shape to whatever the host actually reads. This was specific to this setup.

Confirm the base path

If the built assets are served from a CDN or cloud storage bucket, you need to tell Vite:

export default defineConfig({  base: process.env.ASSET_BASE_PATH || '/',  // ... });

Enter fullscreen mode

Exit fullscreen mode

Without this, Vite generates root-relative paths like /assets/chunk-abc123.js. The host resolves those relative to its own origin, which in this case served index.html instead of the JS file, producing MIME type errors. Setting base to the bucket or CDN path fixed it.

Split fonts from the main bundle (if applicable)

The module bundled custom fonts, but the host already loaded the same fonts globally. The fix was to move the @font-face declarations into a separate SCSS file and only import it in standalone mode, not in the federated entry.

Step 4: Local federation dev harness

This was the biggest QOL improvement, and probably the most reusable part of the migration. Testing a federated module usually means deploying to a test environment and loading it through the host. That's a slow feedback loop. Instead, a local dev harness was built to replicate the host's loading sequence.

The harness used vite build --watch plus vite preview instead of the normal dev server because the goal was to validate the real emitted artifacts: asset-manifest.json, remoteEntry.js, built CSS, and chunk URLs. The standard dev server is great for app development, but it doesnt produce the same output the host will actually fetch in production.

The harness did the following:

  • Build the module in development mode with vite build

  • Keep rebuilding with vite build --watch

  • Serve the output with vite preview

  • Use a simple intermediary UI to collect runtime props (locale, auth token, environment details)

  • Fetch asset-manifest.json from the local preview server

  • Load remoteEntry.js

  • Call container.init() and container.get()

  • Call inject() with configurable props and verify unmount() cleanup

That made it possible to test the full federation lifecycle locally, including script loading, module init, prop injection, CSS loading, auth handling, and unmount cleanup, without deploying anything.

The entry point ended up with three runtime modes:

// src/main.tsx if (import.meta.env.VITE_USE_FEDERATION_HARNESS === 'true') {  const { FederationHarness } = await import('./dev/FederationHarness');  root.render(); } else if (import.meta.env.VITE_EMBEDDED_MODE === 'true') {  const { FederatedEntry } = await import('./remote/FederatedEntry');  root.render(); } else {  const { StandaloneEntry } = await import('./standalone/StandaloneEntry');  root.render(); }

Enter fullscreen mode

Exit fullscreen mode

  • start runs standalone app development

  • dev runs federation development against a local preview server

  • build produces the production remote for the real host

Pitfalls to watch out for

  • Vite's manifest is not webpack's manifest. Dont assume the formats will match.

  • base matters for remote hosting. Forget it and every chunk import will 404 or return HTML instead of JavaScript.

  • Shared dependencies are not automatic wins. They are one of the biggest selling points of Module Federation, but cross-bundler setups and older integration contracts can make them risky to use.

  • Suppress lint rules temporarily. A build tool migration will surface new lint errors from updated configs. Add temporary warn overrides and fix them in separate PRs and keep momentum.

  • Fix things at the source. For example, dont patch CI when the build config is wrong :)

Verification

These were the checks that mattered more than "the build passed":

  • Standalone development still worked with the app's normal router and env vars

  • The local federation harness could fetch asset-manifest.json, load remoteEntry.js, and mount the module

  • CSS loaded correctly from the built output

  • Production hosting used the correct base path and chunk URLs all resolved correctly

  • Full regression test of all features

Results

  • Resolved all the open dependabot alerts

  • Removed .babelrc, craco.config.js, jest.config.js, and custom webpack overrides

  • Consolidated build, dev, preview, and test config into vite.config.ts

  • Cold-start build time went from 63.4s in CRA/webpack to 9.3s in Vite

  • The lockfile diff had a reduction of ~10k lines

If you're maintaining a federated micro frontend on CRA, the path to Vite is worth the effort. Just remember to analyze the host's loading contract and build yourself a local harness that exercises the real federation lifecycle.

A note on Vite 8: Vite 8 shipped recently, after this migration was already complete. Its release notes mention Module Federation support as one of the capabilities unlocked by the new Rolldown-based architecture, which looks promising. If I were starting today, I would look into this first.

References

  • React: Sunsetting Create React App

  • Vite docs: Building for Production

  • Sass docs: @import deprecation

  • Module Federation Vite plugin repository

  • Migrating from Create React App to Vite: A Modern Approach

  • Module Federation Vite Plugin

  • Vite 8 announcement

Was this article helpful?

Sign in to highlight and annotate this article

AI
Ask AI about this article
Powered by Eigenvector · full article context loaded
Ready

Conversation starters

Ask anything about this article…

Daily AI Digest

Get the top 5 AI stories delivered to your inbox every morning.

More about

releaseannounceupdate

Knowledge Map

Knowledge Map
TopicsEntitiesSource
Migrating a…releaseannounceupdateproductapplicationfeatureDEV Communi…

Connected Articles — Knowledge Graph

This article is connected to other articles through shared AI topics and tags.

Knowledge Graph100 articles · 158 connections
Scroll to zoom · drag to pan · click to open

Discussion

Sign in to join the discussion

No comments yet — be the first to share your thoughts!

More in Products