@mf-toolkit/shared-inspector 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -102
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -6,81 +6,108 @@
|
|
|
6
6
|
|
|
7
7
|
**Stop debugging Module Federation in production.**
|
|
8
8
|
|
|
9
|
-
`shared` config breaks in silence — wrong versions ship,
|
|
9
|
+
`shared` config breaks in silence — wrong versions ship, duplicate React copies can end up in the bundle, singleton negotiation fails, and teams get paged for "Invalid hook call" on Friday night. `shared-inspector` catches these mistakes at build time. Every finding comes with a risk score and a ready-to-paste fix.
|
|
10
10
|
|
|
11
11
|
## The problem
|
|
12
12
|
|
|
13
13
|
Module Federation teams manually manage `shared` config and make three kinds of mistakes:
|
|
14
14
|
|
|
15
15
|
- **Over-sharing** — packages listed in `shared` that the microfrontend never imports. Creates artificial version coupling between independent teams.
|
|
16
|
-
- **Under-sharing** — packages used by both host and remote but missing from `shared`. Each microfrontend
|
|
16
|
+
- **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).
|
|
17
17
|
- **Version mismatch** — `requiredVersion` doesn'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.
|
|
18
18
|
|
|
19
19
|
Existing tools (webpack-bundle-analyzer, source-map-explorer) show *what ended up in the bundle*, not *why shared config is suboptimal*. Different questions.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Why not bundle analyzer?
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
npm install --save-dev @mf-toolkit/shared-inspector
|
|
25
|
-
```
|
|
23
|
+
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.
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
| Question | Bundle analyzer | shared-inspector |
|
|
26
|
+
|----------|----------------|-----------------|
|
|
27
|
+
| Which packages are large? | ✅ | — |
|
|
28
|
+
| Is React duplicated across MFs? | Visible after the fact | ✅ Detected before build ships |
|
|
29
|
+
| Is `requiredVersion` out of sync with the installed version? | ✗ | ✅ |
|
|
30
|
+
| Is a package marked `singleton` in one MF but not another? | ✗ | ✅ |
|
|
31
|
+
| Which packages are declared `shared` but never imported? | ✗ | ✅ |
|
|
32
|
+
| Which used packages are missing from `shared` entirely? | ✗ | ✅ |
|
|
33
|
+
| Cross-MF version conflicts across teams? | ✗ | ✅ via federation manifests |
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
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.
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
# Analyse the current project (auto-reads name from package.json)
|
|
33
|
-
npx @mf-toolkit/shared-inspector
|
|
37
|
+
## Example
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
npx @mf-toolkit/shared-inspector --interactive
|
|
39
|
+
A `shell` host app (React 18) and a `checkout` remote have been developed by separate teams. Their `shared` configs have drifted:
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
```js
|
|
42
|
+
// shell — webpack.config.js
|
|
43
|
+
shared: {
|
|
44
|
+
react: { singleton: true, requiredVersion: '^18.2.0' },
|
|
45
|
+
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
|
|
46
|
+
zustand: { singleton: true },
|
|
47
|
+
}
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
// checkout — webpack.config.js
|
|
50
|
+
shared: {
|
|
51
|
+
react: { singleton: true, requiredVersion: '^17.0.2' }, // ← stale version
|
|
52
|
+
'react-dom': { singleton: true, requiredVersion: '^17.0.2' },
|
|
53
|
+
lodash: {}, // ← never imported
|
|
54
|
+
// zustand: missing — checkout imports it, but it's not in shared
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Running `npx @mf-toolkit/shared-inspector` in the `checkout` project:
|
|
43
59
|
|
|
44
|
-
# Cross-MF federation analysis from saved manifests
|
|
45
|
-
npx @mf-toolkit/shared-inspector federation checkout.json catalog.json cart.json
|
|
46
60
|
```
|
|
61
|
+
[MfSharedInspector] checkout (depth: local-graph, 34 files scanned)
|
|
62
|
+
────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
⚠ Version Mismatch — react
|
|
65
|
+
configured: ^17.0.2 | installed: 17.0.2
|
|
66
|
+
→ Risk: Invalid hook call, broken context across MFs
|
|
67
|
+
💡 Fix:
|
|
68
|
+
shared: {
|
|
69
|
+
react: { singleton: true, requiredVersion: "^18.2.0" }
|
|
70
|
+
}
|
|
47
71
|
|
|
48
|
-
|
|
72
|
+
→ Not Shared — zustand (8 imports in 5 files)
|
|
73
|
+
→ Risk: Each MF may get its own store instance — state changes may not propagate across MFs
|
|
74
|
+
💡 Fix:
|
|
75
|
+
shared: {
|
|
76
|
+
zustand: { singleton: true }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
✗ Unused Shared — lodash
|
|
80
|
+
0 imports, shared without singleton
|
|
81
|
+
→ Wastes bundle negotiation overhead with no benefit
|
|
82
|
+
💡 Fix: Remove "lodash" from shared config
|
|
49
83
|
|
|
84
|
+
────────────────────────────────────────────────────────────
|
|
85
|
+
Score: 69/100 🟠 RISKY
|
|
50
86
|
```
|
|
51
|
-
$ npx @mf-toolkit/shared-inspector --interactive
|
|
52
87
|
|
|
53
|
-
|
|
88
|
+
**After manually updating the config based on the suggestions above** — `react` version aligned, `zustand` added to `shared`, `lodash` removed:
|
|
54
89
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Shared packages — comma-separated names or path to .json (empty to skip): react,react-dom,mobx
|
|
58
|
-
Path to tsconfig.json for alias resolution (empty to skip):
|
|
59
|
-
Workspace packages to exclude, comma-separated (empty to skip):
|
|
60
|
-
Fail build on findings — mismatch / unused / any / none (default: none): mismatch
|
|
61
|
-
Write project-manifest.json? (y/N): n
|
|
90
|
+
```
|
|
91
|
+
Score: 100/100 ✅ HEALTHY
|
|
62
92
|
```
|
|
63
93
|
|
|
64
|
-
|
|
94
|
+
The cross-team federation report also clears: `shell` and `checkout` now negotiate a single React instance and a single Zustand store at runtime.
|
|
65
95
|
|
|
66
|
-
|
|
67
|
-
|------|---------|-------------|
|
|
68
|
-
| `--source, -s <dirs>` | `./src` | Source dirs to scan, comma-separated |
|
|
69
|
-
| `--depth <depth>` | `local-graph` | Scan depth: `direct` \| `local-graph` |
|
|
70
|
-
| `--shared <packages\|file>` | — | Comma-separated package names or path to `.json` config |
|
|
71
|
-
| `--tsconfig <path>` | — | tsconfig.json for path alias resolution |
|
|
72
|
-
| `--workspace-packages <pkgs>` | — | Comma-separated workspace packages to exclude |
|
|
73
|
-
| `--name <name>` | auto from `package.json` | Project name |
|
|
74
|
-
| `--fail-on <rule>` | — | Exit 1 when findings match: `mismatch` \| `unused` \| `any` |
|
|
75
|
-
| `--write-manifest` | `false` | Write `project-manifest.json` to output dir |
|
|
76
|
-
| `--output-dir <dir>` | `.` | Output directory for manifest |
|
|
77
|
-
| `--interactive, -i` | — | Launch step-by-step wizard |
|
|
78
|
-
| `--version, -v` | — | Print version and exit |
|
|
79
|
-
| `--help, -h` | — | Show help |
|
|
96
|
+
## Installation
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
```bash
|
|
99
|
+
npm install --save-dev @mf-toolkit/shared-inspector
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Quick start
|
|
82
103
|
|
|
83
|
-
|
|
104
|
+
Run against any MF project — no config file needed:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx @mf-toolkit/shared-inspector
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The 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:
|
|
84
111
|
|
|
85
112
|
```
|
|
86
113
|
mf-inspector v0.4.0
|
|
@@ -104,7 +131,7 @@ Each finding is rendered as a diagnostic card: what's wrong, what breaks at runt
|
|
|
104
131
|
💡 Fix: Remove "lodash" from shared config
|
|
105
132
|
|
|
106
133
|
→ Not Shared — mobx (12 imports in 8 files via re-export in src/shared/index.ts)
|
|
107
|
-
→ Risk: Each MF
|
|
134
|
+
→ Risk: Each MF may get its own MobX instance — observables and reactions can fail to sync between MFs
|
|
108
135
|
💡 Fix:
|
|
109
136
|
shared: {
|
|
110
137
|
mobx: { singleton: true }
|
|
@@ -123,7 +150,7 @@ Total: 12 shared, 10 used, 1 unused, 1 candidates, 1 mismatch, 0 eager risks
|
|
|
123
150
|
|
|
124
151
|
Colors are auto-applied in TTY terminals and disabled in CI / piped output (`NO_COLOR` / `TERM=dumb` respected).
|
|
125
152
|
|
|
126
|
-
|
|
153
|
+
### Risk scoring
|
|
127
154
|
|
|
128
155
|
Every report ends with a score out of 100:
|
|
129
156
|
|
|
@@ -140,7 +167,43 @@ Every report ends with a score out of 100:
|
|
|
140
167
|
| 40–69 | 🟠 RISKY |
|
|
141
168
|
| 0–39 | 🔴 CRITICAL |
|
|
142
169
|
|
|
143
|
-
|
|
170
|
+
## CI mode
|
|
171
|
+
|
|
172
|
+
Integrate into build pipelines to fail on findings, gate on score, or emit manifests for later federation analysis.
|
|
173
|
+
|
|
174
|
+
### Failing the build
|
|
175
|
+
|
|
176
|
+
Use `--fail-on` to exit with code 1 when specific findings are detected:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx @mf-toolkit/shared-inspector --source ./src --shared react,react-dom --fail-on mismatch
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With the webpack plugin:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { MfSharedInspectorPlugin } from '@mf-toolkit/shared-inspector/webpack';
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
plugins: [
|
|
189
|
+
new ModuleFederationPlugin({
|
|
190
|
+
name: 'checkout',
|
|
191
|
+
shared: { react: { singleton: true }, mobx: { singleton: true } },
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
// sharedConfig not needed — auto-extracted from ModuleFederationPlugin above
|
|
195
|
+
new MfSharedInspectorPlugin({
|
|
196
|
+
sourceDirs: ['./src'],
|
|
197
|
+
failOn: 'mismatch', // 'mismatch' | 'unused' | 'any'
|
|
198
|
+
warn: true,
|
|
199
|
+
}),
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Gating on score
|
|
205
|
+
|
|
206
|
+
Use `scoreProjectReport` programmatically to set custom thresholds:
|
|
144
207
|
|
|
145
208
|
```typescript
|
|
146
209
|
import { analyzeProject, scoreProjectReport } from '@mf-toolkit/shared-inspector';
|
|
@@ -154,9 +217,67 @@ if (score < 70) {
|
|
|
154
217
|
}
|
|
155
218
|
```
|
|
156
219
|
|
|
157
|
-
|
|
220
|
+
### Writing manifests for federation analysis
|
|
221
|
+
|
|
222
|
+
Each MF can emit a `project-manifest.json` as a build artifact to enable cross-team analysis in a later step:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npx @mf-toolkit/shared-inspector --shared ./shared-config.json --write-manifest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
With the webpack plugin:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
new MfSharedInspectorPlugin({
|
|
232
|
+
sourceDirs: ['./src'],
|
|
233
|
+
writeManifest: true, // writes project-manifest.json for CI aggregation
|
|
234
|
+
warn: true,
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Upload the manifest as a CI artifact:
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
# .github/workflows/build.yml
|
|
242
|
+
jobs:
|
|
243
|
+
build-checkout:
|
|
244
|
+
steps:
|
|
245
|
+
- run: npm run build # MfSharedInspectorPlugin writes project-manifest.json
|
|
246
|
+
- uses: actions/upload-artifact@v4
|
|
247
|
+
with: { name: manifest-checkout, path: project-manifest.json }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Federation mode
|
|
251
|
+
|
|
252
|
+
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.
|
|
253
|
+
|
|
254
|
+
### CLI
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npx @mf-toolkit/shared-inspector federation checkout.json catalog.json cart.json
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Programmatic
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { analyzeFederation, formatFederationReport, scoreFederationReport } from '@mf-toolkit/shared-inspector';
|
|
264
|
+
|
|
265
|
+
const report = analyzeFederation([checkoutManifest, catalogManifest, cartManifest]);
|
|
266
|
+
const { score, label } = scoreFederationReport(report);
|
|
158
267
|
|
|
159
|
-
|
|
268
|
+
console.log(formatFederationReport(report));
|
|
269
|
+
// ⚠ Version Conflict — react
|
|
270
|
+
// checkout: ^17.0.0
|
|
271
|
+
// catalog: ^18.0.0
|
|
272
|
+
// → Risk: MF singleton negotiation may silently load the wrong version → Invalid hook call
|
|
273
|
+
// 💡 Fix: shared: { react: { singleton: true, requiredVersion: "^18.0.0" } }
|
|
274
|
+
//
|
|
275
|
+
// Score: 60/100 🟠 RISKY
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Programmatic API
|
|
279
|
+
|
|
280
|
+
### Two-phase API
|
|
160
281
|
|
|
161
282
|
```typescript
|
|
162
283
|
import { buildProjectManifest, analyzeProject, formatReport } from '@mf-toolkit/shared-inspector';
|
|
@@ -203,30 +324,6 @@ const report = await inspect({
|
|
|
203
324
|
});
|
|
204
325
|
```
|
|
205
326
|
|
|
206
|
-
### Webpack plugin
|
|
207
|
-
|
|
208
|
-
`sharedConfig` is optional — the plugin auto-extracts it from `ModuleFederationPlugin` when not provided:
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
import { MfSharedInspectorPlugin } from '@mf-toolkit/shared-inspector/webpack';
|
|
212
|
-
|
|
213
|
-
module.exports = {
|
|
214
|
-
plugins: [
|
|
215
|
-
new ModuleFederationPlugin({
|
|
216
|
-
name: 'checkout',
|
|
217
|
-
shared: { react: { singleton: true }, mobx: { singleton: true } },
|
|
218
|
-
}),
|
|
219
|
-
|
|
220
|
-
// sharedConfig not needed — auto-extracted from ModuleFederationPlugin above
|
|
221
|
-
new MfSharedInspectorPlugin({
|
|
222
|
-
sourceDirs: ['./src'],
|
|
223
|
-
warn: true,
|
|
224
|
-
writeManifest: true, // writes project-manifest.json for CI aggregation
|
|
225
|
-
}),
|
|
226
|
-
],
|
|
227
|
-
};
|
|
228
|
-
```
|
|
229
|
-
|
|
230
327
|
## Analysis depth
|
|
231
328
|
|
|
232
329
|
| Depth | What it finds | Speed |
|
|
@@ -262,41 +359,40 @@ await buildProjectManifest({
|
|
|
262
359
|
|
|
263
360
|
Without `tsconfigPath`, `@components/Button` is treated as an external package and packages imported inside it are invisible in `local-graph` mode.
|
|
264
361
|
|
|
265
|
-
##
|
|
362
|
+
## Interactive wizard
|
|
266
363
|
|
|
267
|
-
|
|
364
|
+
Not sure which flags to pass? Run the step-by-step wizard:
|
|
268
365
|
|
|
269
|
-
```yaml
|
|
270
|
-
# .github/workflows/build.yml
|
|
271
|
-
jobs:
|
|
272
|
-
build-checkout:
|
|
273
|
-
steps:
|
|
274
|
-
- run: npm run build # MfSharedInspectorPlugin writes project-manifest.json
|
|
275
|
-
- uses: actions/upload-artifact@v4
|
|
276
|
-
with: { name: manifest-checkout, path: project-manifest.json }
|
|
277
366
|
```
|
|
367
|
+
$ npx @mf-toolkit/shared-inspector --interactive
|
|
278
368
|
|
|
279
|
-
|
|
280
|
-
import { analyzeFederation, formatFederationReport, scoreFederationReport } from '@mf-toolkit/shared-inspector';
|
|
281
|
-
|
|
282
|
-
const report = analyzeFederation([checkoutManifest, catalogManifest, cartManifest]);
|
|
283
|
-
const { score, label } = scoreFederationReport(report);
|
|
369
|
+
[MfSharedInspector] Interactive setup
|
|
284
370
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Score: 60/100 🟠 RISKY
|
|
371
|
+
Source directories to scan (default: ./src):
|
|
372
|
+
Scan depth — direct or local-graph (default: local-graph):
|
|
373
|
+
Shared packages — comma-separated names or path to .json (empty to skip): react,react-dom,mobx
|
|
374
|
+
Path to tsconfig.json for alias resolution (empty to skip):
|
|
375
|
+
Workspace packages to exclude, comma-separated (empty to skip):
|
|
376
|
+
Fail build on findings — mismatch / unused / any / none (default: none): mismatch
|
|
377
|
+
Write project-manifest.json? (y/N): n
|
|
293
378
|
```
|
|
294
379
|
|
|
295
|
-
|
|
380
|
+
## CLI reference
|
|
296
381
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
382
|
+
| Flag | Default | Description |
|
|
383
|
+
|------|---------|-------------|
|
|
384
|
+
| `--source, -s <dirs>` | `./src` | Source dirs to scan, comma-separated |
|
|
385
|
+
| `--depth <depth>` | `local-graph` | Scan depth: `direct` \| `local-graph` |
|
|
386
|
+
| `--shared <packages\|file>` | — | Comma-separated package names or path to `.json` config |
|
|
387
|
+
| `--tsconfig <path>` | — | tsconfig.json for path alias resolution |
|
|
388
|
+
| `--workspace-packages <pkgs>` | — | Comma-separated workspace packages to exclude |
|
|
389
|
+
| `--name <name>` | auto from `package.json` | Project name |
|
|
390
|
+
| `--fail-on <rule>` | — | Exit 1 when findings match: `mismatch` \| `unused` \| `any` |
|
|
391
|
+
| `--write-manifest` | `false` | Write `project-manifest.json` to output dir |
|
|
392
|
+
| `--output-dir <dir>` | `.` | Output directory for manifest |
|
|
393
|
+
| `--interactive, -i` | — | Launch step-by-step wizard |
|
|
394
|
+
| `--version, -v` | — | Print version and exit |
|
|
395
|
+
| `--help, -h` | — | Show help |
|
|
300
396
|
|
|
301
397
|
## API reference
|
|
302
398
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mf-toolkit/shared-inspector",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Stop debugging Module Federation in production. Audit shared config at build time.",
|
|
5
5
|
"author": "Vitaly Zheltko",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
"typescript": ">=4.7"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
|
-
"typescript": {
|
|
51
|
+
"typescript": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@types/semver": "^7.5.8",
|