vitest-plugin-module-federation
v1.0.0
Published
Auto-mock Module Federation remote imports in Vitest — no more unresolvable 'remoteApp/Component' imports in unit tests.
Maintainers
Readme
Vitest Plugin Module Federation
Vitest plugin to auto-mock Module Federation remote imports in unit tests.
The Problem
If your app consumes federated remotes, imports like shop/Button only exist at runtime:
Error: Failed to resolve import "shop/Button" from "src/Basket.tsx"The usual fix is a vi.mock('shop/Button', ...) in every test file that touches a remote. That gets old quickly.
The Solution
Register the plugin once and tell it which remotes you have:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import moduleFederationMock from 'vitest-plugin-module-federation';
export default defineConfig({
plugins: [
moduleFederationMock({
remotes: ['shop', 'checkout'],
}),
],
test: {
environment: 'jsdom',
},
});Anything imported from shop/* or checkout/* now resolves to a generated mock. By default that's a React component which renders its props and children with a predictable test id:
import Button from 'shop/Button';
render(<Button aria-label="checkout">Go</Button>);
screen.getByTestId('mock-shop-button');No vi.mock calls and no changes to your components.
Requirements
- Node.js >= 20
- Vitest 2, 3 or 4 (it's a plain Vite plugin, so any Vite-based runner works)
Installation
pnpm add -D vitest-plugin-module-federationnpm install --save-dev vitest-plugin-module-federationUsage
Telling the plugin about your remotes
Pass remote names directly, a RegExp, or reuse the federation config you already have. At least one of remotes or federationConfig is required and they merge when both are given:
moduleFederationMock({ remotes: ['shop', 'checkout'] });
// or match specifiers with a RegExp
moduleFederationMock({ remotes: /^apps_[a-z]+\// });
// or read the remote names from your Module Federation config
import { federationConfig } from './module-federation.config';
moduleFederationMock({ federationConfig });Choosing the default mock
defaultMock controls what gets generated for a matched import:
'react-component'(the default) renders<div data-testid="mock-<remote>-<module>" {...props}>{children}</div>. It's exported asdefaultand as a PascalCased named export derived from the module path, soshop/user-profilealso exportsUserProfile. Notereactneeds to be installed; it isn't a dependency of this plugin.'empty'gives youexport default {}with no React involved.- A function gives you full control and should return the module source:
moduleFederationMock({
remotes: ['shop'],
defaultMock: ({ remote, modulePath }) => `export default () => null;`,
});Overriding individual modules
Sometimes a generated component isn't enough, for example a remote that exports utilities. Use mocks to override specific imports:
moduleFederationMock({
remotes: ['shop', 'checkout'],
mocks: {
// inline module source, served as-is
'checkout/config': `export const currency = 'GBP';`,
// point the import at a real file (relative to the Vite root)
'checkout/formatPrice': { file: './test/mocks/formatPrice.ts' },
// keep the default mock but add named exports
'shop/Header': { namedExports: ['HeaderNav'] },
},
});TypeScript
Your app probably declares its remote modules already. If not:
// remotes.d.ts
declare module 'shop/Button' {
import type { ComponentType, PropsWithChildren } from 'react';
const Button: ComponentType<PropsWithChildren<Record<string, unknown>>>;
export default Button;
}How it works
The plugin runs before Vite's own resolver (enforce: 'pre') and matches import specifiers against your remote names. Matches resolve to a virtual module (or to your file override) and the mock source is generated at load time. Because the interception happens at the resolver level, it makes no difference which bundler builds the app itself.
Credits
MIT © Chris Boakes
