@mf-toolkit/shared-inspector
v0.5.4
Published
Stop debugging Module Federation in production. Audit shared config at build time.
Maintainers
Readme
@mf-toolkit/shared-inspector

Validate Module Federation shared config at build time.
shared config errors are silent — mismatched versions, broken singleton negotiation, duplicate instances in the bundle. shared-inspector surfaces these issues before they reach production. Every finding includes a risk score and a ready-to-paste fix.
The problem
Module Federation teams manually manage shared config and make three kinds of mistakes:
- Over-sharing — packages listed in
sharedthat the microfrontend never imports. Creates artificial version coupling between independent teams. - Under-sharing — packages used by both host and remote but missing from
shared. Each microfrontend may bundle its own copy (e.g. multiple React instances, each ~130 KB). - Version mismatch —
requiredVersiondoesn't match the installed version. Module Federation silently falls back to a local bundle. For packages with global state (React, styled-components) this causes "Invalid hook call" in production.
Existing tools (webpack-bundle-analyzer, source-map-explorer) show what ended up in the bundle, not why shared config is suboptimal. Different questions.
Why not bundle analyzer?
Bundle analyzers (webpack-bundle-analyzer, source-map-explorer, stats.json inspection) answer a different question: what is in the output? They are useful for auditing final bundle size, but they don't model Module Federation's shared dependency negotiation.
| Question | Bundle analyzer | shared-inspector |
|----------|----------------|-----------------|
| Which packages are large? | ✅ | — |
| Is React duplicated across MFs? | Visible after the fact | ✅ Detected before build ships |
| Is requiredVersion out of sync with the installed version? | ✗ | ✅ |
| Is a package marked singleton in one MF but not another? | ✗ | ✅ |
| Which packages are declared shared but never imported? | ✗ | ✅ |
| Which used packages are missing from shared entirely? | ✗ | ✅ |
| Cross-MF version conflicts across teams? | ✗ | ✅ via federation manifests |
In short: bundle analyzers are useful for post-build inspection. shared-inspector is focused on the shared config itself — catching misconfiguration at build time and explaining what the runtime consequences would be.
Example
A shell host app (React 18) and a checkout remote have been developed by separate teams. Their shared configs have drifted:
// shell — webpack.config.js
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
zustand: { singleton: true },
}
// checkout — webpack.config.js
shared: {
react: { singleton: true, requiredVersion: '^17.0.2' }, // ← stale version
'react-dom': { singleton: true, requiredVersion: '^17.0.2' },
lodash: {}, // ← never imported
// zustand: missing — checkout imports it, but it's not in shared
}Running npx @mf-toolkit/shared-inspector in the checkout project:
[MfSharedInspector] checkout (depth: local-graph, 34 files scanned)
────────────────────────────────────────────────────────────
⚠ Version Mismatch — react
configured: ^17.0.2 | installed: 17.0.2
→ Risk: Invalid hook call, broken context across MFs
💡 Fix:
shared: {
react: { singleton: true, requiredVersion: "^18.2.0" }
}
→ Not Shared — zustand (8 imports in 5 files)
→ Risk: Each MF may get its own store instance — state changes may not propagate across MFs
💡 Fix:
shared: {
zustand: { singleton: true }
}
✗ Unused Shared — lodash
0 imports, shared without singleton
→ Wastes bundle negotiation overhead with no benefit
💡 Fix: Remove "lodash" from shared config
────────────────────────────────────────────────────────────
Score: 69/100 🟠 RISKYAfter manually updating the config based on the suggestions above — react version aligned, zustand added to shared, lodash removed:
Score: 100/100 ✅ HEALTHYThe cross-team federation report also clears: shell and checkout now negotiate a single React instance and a single Zustand store at runtime.
Installation
npm install --save-dev @mf-toolkit/shared-inspectorQuick start
Run against any MF project — no config file needed:
npx @mf-toolkit/shared-inspectorThe tool scans ./src, reads installed versions from package.json, and prints a diagnostic report. Each finding includes what's wrong, what breaks at runtime, and a ready-to-paste fix:
✓ Scanned 47 files
[MfSharedInspector] checkout (depth: local-graph, 47 files scanned)
────────────────────────────────────────────────────────────
⚠ Version Mismatch — react
configured: ^18.0.0 | installed: 17.0.2
→ Risk: Invalid hook call, broken context across MFs
💡 Fix:
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" }
}
✗ Unused Shared — lodash
0 imports, shared without singleton
→ Wastes bundle negotiation overhead with no benefit
💡 Fix: Remove "lodash" from shared config
→ Not Shared — mobx (12 imports in 8 files via re-export in src/shared/index.ts)
→ Risk: Each MF may get its own MobX instance — observables and reactions can fail to sync between MFs
💡 Fix:
shared: {
mobx: { singleton: true }
}
────────────────────────────────────────────────────────────
Score: 62/100 🟠 RISKY
Issues:
🔴 1 high — version mismatch
🟠 1 medium — singleton gaps, duplicate libs
🟡 1 low — over-sharing
Total: 12 shared, 10 used, 1 unused, 1 candidates, 1 mismatch, 0 eager risksColors are auto-applied in TTY terminals and disabled in CI / piped output (NO_COLOR / TERM=dumb respected).
Risk scoring
Every report ends with a score out of 100:
| Severity | Penalty | Covers | |----------|---------|--------| | 🔴 HIGH | −20 each | Version mismatches, cross-MF version conflicts | | 🟠 MEDIUM | −8 each | Singleton risks, eager risks, duplicate libs, host gaps | | 🟡 LOW | −3 each | Unused shared packages, ghost shares |
| Score | Label | |-------|-------| | 90–100 | ✅ HEALTHY | | 70–89 | 🟡 GOOD | | 40–69 | 🟠 RISKY | | 0–39 | 🔴 CRITICAL |
Demo
See the tool in action across four real-world scenarios — healthy baseline, config drift, hidden federation conflicts, and critical misconfiguration — in a fully runnable e-commerce microfrontend:
zvitaly7/mf-storefront-demo — three independent React 18 apps (shell, catalog, checkout) with Module Federation, demonstrating how shared-inspector catches issues that static review and bundle analyzers miss.
Advanced: CI integration
Integrate into build pipelines to fail on findings, gate on score, or emit manifests for later federation analysis.
Failing the build
Use --fail-on to exit with code 1 when specific findings are detected:
npx @mf-toolkit/shared-inspector --source ./src --shared react,react-dom --fail-on mismatchWith the webpack plugin:
import { MfSharedInspectorPlugin } from '@mf-toolkit/shared-inspector/webpack';
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'checkout',
shared: { react: { singleton: true }, mobx: { singleton: true } },
}),
// sharedConfig not needed — auto-extracted from ModuleFederationPlugin above
new MfSharedInspectorPlugin({
sourceDirs: ['./src'],
failOn: 'mismatch', // 'mismatch' | 'unused' | 'any'
warn: true,
}),
],
};Gating on score
Use scoreProjectReport programmatically to set custom thresholds:
import { analyzeProject, scoreProjectReport } from '@mf-toolkit/shared-inspector';
const report = analyzeProject(manifest);
const { score, label, high, medium, low } = scoreProjectReport(report);
if (score < 70) {
console.error(`Shared config score: ${score}/100 (${label})`);
process.exit(1);
}Writing manifests for federation analysis
Each MF can emit a project-manifest.json as a build artifact to enable cross-team analysis in a later step:
npx @mf-toolkit/shared-inspector --shared ./shared-config.json --write-manifestWith the webpack plugin:
new MfSharedInspectorPlugin({
sourceDirs: ['./src'],
writeManifest: true, // writes project-manifest.json for CI aggregation
warn: true,
})Upload the manifest as a CI artifact:
# .github/workflows/build.yml
jobs:
build-checkout:
steps:
- run: npm run build # MfSharedInspectorPlugin writes project-manifest.json
- uses: actions/upload-artifact@v4
with: { name: manifest-checkout, path: project-manifest.json }Advanced: Federation mode

Once each MF has emitted its manifest, aggregate them to detect cross-team conflicts: version mismatches, singleton inconsistencies, and shared-config gaps across host and remotes.
Manifests can be local files or HTTP/HTTPS URLs — making federation analysis work across separate repositories without manual file passing.
CLI
# Local files (monorepo or pre-downloaded)
npx @mf-toolkit/shared-inspector federation checkout.json catalog.json cart.json
# URLs — fetch manifests directly from remote storage
npx @mf-toolkit/shared-inspector federation \
https://storage.example.com/manifests/checkout.json \
https://storage.example.com/manifests/cart.json \
https://storage.example.com/manifests/catalog.json
# Mix of local files and URLs
npx @mf-toolkit/shared-inspector federation checkout.json https://storage.example.com/cart.jsonPolyrepo setup
In a polyrepo, each team owns a separate repository. The recommended workflow:
Step 1 — each MF repo publishes its manifest on every build:
# .github/workflows/build.yml (in each MF repo)
jobs:
build:
steps:
- run: npm run build # MfSharedInspectorPlugin writes project-manifest.json
- uses: actions/upload-artifact@v4
with: { name: manifest-${{ github.event.repository.name }}, path: project-manifest.json }Step 2 — a dedicated federation-check job downloads all manifests and runs analysis:
# .github/workflows/federation-check.yml (in a shared/platform repo)
jobs:
federation-check:
steps:
- uses: actions/download-artifact@v4
with: { name: manifest-checkout, github-token: ${{ secrets.GITHUB_TOKEN }}, repository: org/checkout, run-id: ... }
- uses: actions/download-artifact@v4
with: { name: manifest-cart, github-token: ${{ secrets.GITHUB_TOKEN }}, repository: org/cart, run-id: ... }
- run: |
npx @mf-toolkit/shared-inspector federation \
manifest-checkout/project-manifest.json \
manifest-cart/project-manifest.jsonAlternatively, upload manifests to a shared HTTP storage (S3, CDN, object store) and use URL inputs directly — no artifact coordination required:
- run: |
npx @mf-toolkit/shared-inspector federation \
https://manifests.internal/checkout/latest.json \
https://manifests.internal/cart/latest.jsonProgrammatic
import { analyzeFederation, formatFederationReport, scoreFederationReport } from '@mf-toolkit/shared-inspector';
const report = analyzeFederation([checkoutManifest, catalogManifest, cartManifest]);
const { score, label } = scoreFederationReport(report);
console.log(formatFederationReport(report));
// ⚠ Version Conflict — react
// checkout: ^17.0.0
// catalog: ^18.0.0
// → Risk: MF singleton negotiation may silently load the wrong version → Invalid hook call
// 💡 Fix: shared: { react: { singleton: true, requiredVersion: "^18.0.0" } }
//
// Score: 60/100 🟠 RISKYProgrammatic API
Two-phase API
import { buildProjectManifest, analyzeProject, formatReport } from '@mf-toolkit/shared-inspector';
// Phase 1: collect facts
const manifest = await buildProjectManifest({
name: 'checkout',
sourceDirs: ['./src'],
sharedConfig: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
lodash: {},
},
// depth: 'local-graph' ← default, follows barrel re-exports
// tsconfigPath: './tsconfig.json' ← optional, resolves @alias/* imports
// workspacePackages: ['@my-org/*'] ← optional, excludes local monorepo packages
});
// Phase 2: analyse facts
const report = analyzeProject(manifest);
console.log(report.mismatched);
// [{ package: 'react', configured: '^18.0.0', installed: '17.0.2' }]
console.log(report.candidates);
// [{ package: 'mobx', importCount: 12, via: 'reexport', files: ['src/shared/index.ts'] }]
console.log(report.unused);
// [{ package: 'lodash', singleton: false }]
// Human-readable output with risk descriptions and fix snippets
console.log(formatReport(report, { name: manifest.project.name }));Shortcut API
import { inspect } from '@mf-toolkit/shared-inspector';
const report = await inspect({
name: 'checkout',
sourceDirs: ['./src'],
sharedConfig: { /* ... */ },
});Analysis depth
| Depth | What it finds | Speed |
|-------|--------------|-------|
| 'direct' | Explicit import / require statements | Fast (ms) |
| 'local-graph' (default) | + packages reachable via barrel re-exports and local wrappers | Slower (seconds) |
The difference matters when your project uses barrel files:
// src/shared/index.ts
export { observer } from 'mobx-react'; // re-export
export { makeAutoObservable } from 'mobx'; // re-export
// src/app.tsx
import { observer } from './shared'; // relative import — direct mode stops heredepth: 'direct'sees./shared(relative) → skips.mobxnot found.depth: 'local-graph'follows./shared→ findsmobxandmobx-reactvia re-export.
TypeScript path aliases
// tsconfig.json
{ "compilerOptions": { "paths": { "@components/*": ["src/components/*"] } } }
await buildProjectManifest({
sourceDirs: ['./src'],
tsconfigPath: './tsconfig.json', // enables alias resolution
});Without tsconfigPath, @components/Button is treated as an external package and packages imported inside it are invisible in local-graph mode.
Interactive wizard
Not sure which flags to pass? Run the step-by-step wizard:
$ npx @mf-toolkit/shared-inspector --interactive
[MfSharedInspector] Interactive setup
Source directories to scan (default: ./src):
Scan depth — direct or local-graph (default: local-graph):
Shared packages — comma-separated names or path to .json (empty to skip): react,react-dom,mobx
Path to tsconfig.json for alias resolution (empty to skip):
Workspace packages to exclude, comma-separated (empty to skip):
Fail build on findings — mismatch / unused / any / none (default: none): mismatch
Write project-manifest.json? (y/N): nCLI reference
| Flag | Default | Description |
|------|---------|-------------|
| --source, -s <dirs> | ./src | Source dirs to scan, comma-separated |
| --depth <depth> | local-graph | Scan depth: direct | local-graph |
| --shared <packages\|file> | — | Comma-separated package names or path to .json config |
| --tsconfig <path> | — | tsconfig.json for path alias resolution |
| --workspace-packages <pkgs> | — | Comma-separated workspace packages to exclude |
| --name <name> | auto from package.json | Project name |
| --kind <kind> | unknown | Project role: host | remote | unknown |
| --fail-on <rule> | — | Exit 1 when findings match: mismatch | unused | any |
| --min-score <n> | — | Exit 1 when score is below n (0–100) |
| --json | false | Output findings as JSON (suppresses spinner and banner) |
| --write-manifest | false | Write project-manifest.json to output dir |
| --output-dir <dir> | . | Output directory for manifest |
| --interactive, -i | — | Launch step-by-step wizard |
| --version, -v | — | Print version and exit |
| --help, -h | — | Show help |
Federation subcommand:
mf-inspector federation <manifest1> [manifest2...] [--fail-on <rule>] [--min-score <n>] [--json]Each manifest can be a local file path or an http(s):// URL. Local paths are resolved relative to the current working directory.
| Flag | Description |
|------|-------------|
| --fail-on <rule> | Exit 1 when findings match: mismatch (version conflicts) | unused (ghost shares) | any |
| --min-score <n> | Exit 1 when federation score is below n |
| --json | Output findings as JSON |
API reference
buildProjectManifest(options)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| name | string | — | Project name |
| sourceDirs | string[] | — | Directories to scan |
| depth | 'direct' \| 'local-graph' | 'local-graph' | Scan depth |
| sharedConfig | Record<string, SharedDepConfig> | {} | MF shared config |
| packageJsonPath | string | './package.json' | Path to package.json |
| extensions | string[] | ['.ts','.tsx','.js','.jsx'] | File extensions |
| ignore | string[] | [] | Packages to exclude (supports @scope/*) |
| tsconfigPath | string | undefined | tsconfig.json for path alias resolution |
| workspacePackages | string[] | [] | Local monorepo packages to exclude |
| kind | 'host' \| 'remote' \| 'unknown' | 'unknown' | Project role |
analyzeProject(manifest, options?)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| alwaysShared | string[] | ['react','react-dom'] | Never flag as unused |
| additionalCandidates | string[] | [] | Extend built-in candidates list |
| additionalSingletonRisks | string[] | [] | Extend built-in singleton-risk list |
scoreProjectReport(report) / scoreFederationReport(report)
Returns a RiskScore:
interface RiskScore {
score: number; // 0–100, higher is better
label: 'HEALTHY' | 'GOOD' | 'RISKY' | 'CRITICAL';
high: number; // count of high-severity findings
medium: number; // count of medium-severity findings
low: number; // count of low-severity findings
}analyzeFederation(manifests, options?)
Accepts N ProjectManifest objects (one per microfrontend) and returns a FederationReport.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| alwaysShared | string[] | ['react','react-dom'] | Exclude from ghost/gap detection |
writeReport(report, outputPath) / writeManifest(manifest, outputPath)
Write a ProjectReport or ProjectManifest to a JSON file. Parent directories are created automatically.
import { writeReport, writeManifest } from '@mf-toolkit/shared-inspector';
await writeReport(report, './dist/shared-report.json');
await writeManifest(manifest, './dist/project-manifest.json');MfSharedInspectorPlugin options
Extends all buildProjectManifest options (except name, auto-resolved from compiler) plus:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| warn | boolean | true | Print findings to console |
| failOn | 'mismatch' \| 'unused' \| 'any' | undefined | Fail the build when findings match |
| writeManifest | boolean | false | Write project-manifest.json to outputDir |
| outputDir | string | '.' | Directory for manifest output |
| analysis | AnalysisOptions | {} | Options forwarded to analyzeProject |
Detection categories
Per-project (analyzeProject)
| Category | Severity | Description |
|----------|----------|-------------|
| mismatched | 🔴 HIGH | requiredVersion doesn't satisfy installed version |
| singletonRisks | 🟠 MEDIUM | Global-state packages shared without singleton: true |
| eagerRisks | 🟠 MEDIUM | eager: true without singleton: true |
| candidates | 🟠 MEDIUM | Used packages missing from shared (each MF bundles own copy) |
| unused | 🟡 LOW | In shared config but not observed in scanned sources |
Cross-MF (analyzeFederation)
| Category | Severity | Description |
|----------|----------|-------------|
| versionConflicts | 🔴 HIGH | requiredVersion ranges across MFs have no overlap |
| singletonMismatches | 🟠 MEDIUM | singleton: true in some MFs, absent in others |
| hostGaps | 🟠 MEDIUM | Package used by 2+ MFs but not declared in shared by anyone |
| ghostShares | 🟡 LOW | Package in shared of one MF, unused/unshared by all others |
How it works
Four steps, no magic:
- Scan — statically extracts import/require statements from source files
- Normalize — reads your declared
sharedconfig (explicit or auto-extracted fromModuleFederationPlugin) - Resolve — reads installed versions from
node_modulesto detectrequiredVersiondrift - Cross-reference — produces findings, a risk score, and optionally a
project-manifest.jsonfor federation analysis
No webpack build required. Runs in seconds on the source tree directly.
When not to use this tool
- You are not using Module Federation (webpack or rspack)
- Your
sharedconfig is intentionally empty or minimal by design - You only need bundle size analysis — use webpack-bundle-analyzer instead
- Your MF setup uses dynamic runtime sharing with non-standard orchestration that doesn't rely on the
sharedconfig
Known limitations
The tool works well for the vast majority of typical Module Federation setups. The limitations below only apply to edge cases or highly dynamic configurations:
- TypeScript path aliases without
tsconfigPath: aliased imports are treated as external package names. - Dynamic imports with variables (
import(moduleName)): not analysed — requires runtime information. - Exact tsconfig alias patterns (non-wildcard): only
"@alias/*"wildcard form is supported. - Subclassed
ModuleFederationPlugin: auto-extraction matches by constructor name — passsharedConfigexplicitly for custom subclasses.
License
MIT