create-interview-cockpit 0.10.0 → 0.11.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-interview-cockpit",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Scaffold a personal AI-powered interview prep cockpit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2398,6 +2398,161 @@ export default function CodeRunnerModal() {
2398
2398
  const mfInspectorIssueCount =
2399
2399
  mfInspectorDifferentRemoteCount + mfInspectorErroredRemoteCount;
2400
2400
  const mfInspectorViewCopy = MF_INSPECTOR_VIEW_COPY[mfInspectorView];
2401
+ const mfInspectorWorkspacePackageMap = new Map<
2402
+ string,
2403
+ {
2404
+ apps: string[];
2405
+ requiredVersions: Array<{ app: string; version: string | false | null }>;
2406
+ }
2407
+ >();
2408
+
2409
+ for (const [filePath, fileContents] of Object.entries(reactFiles)) {
2410
+ const match = filePath.match(/^apps\/([^/]+)\/package\.json$/);
2411
+ if (!match) continue;
2412
+
2413
+ try {
2414
+ const packageJson = JSON.parse(fileContents);
2415
+ const appName = match[1];
2416
+ const dependencyVersions = asInspectorRecord(packageJson.dependencies);
2417
+
2418
+ for (const [packageName, versionValue] of Object.entries(
2419
+ dependencyVersions,
2420
+ )) {
2421
+ const existing = mfInspectorWorkspacePackageMap.get(packageName) ?? {
2422
+ apps: [],
2423
+ requiredVersions: [],
2424
+ };
2425
+
2426
+ if (!existing.apps.includes(appName)) {
2427
+ existing.apps.push(appName);
2428
+ }
2429
+ existing.requiredVersions.push({
2430
+ app: appName,
2431
+ version: typeof versionValue === "string" ? versionValue : null,
2432
+ });
2433
+ mfInspectorWorkspacePackageMap.set(packageName, existing);
2434
+ }
2435
+ } catch {
2436
+ // Ignore malformed package.json edits while the user is typing.
2437
+ }
2438
+ }
2439
+
2440
+ const mfInspectorDeclaredSharedPackageMap = new Map<
2441
+ string,
2442
+ {
2443
+ apps: string[];
2444
+ requiredVersions: Array<{ app: string; version: string | false | null }>;
2445
+ singletonPreferred: boolean;
2446
+ }
2447
+ >();
2448
+
2449
+ for (const [packageName, packageInfo] of mfInspectorWorkspacePackageMap) {
2450
+ mfInspectorDeclaredSharedPackageMap.set(packageName, {
2451
+ apps: [...packageInfo.apps],
2452
+ requiredVersions: [...packageInfo.requiredVersions],
2453
+ singletonPreferred: false,
2454
+ });
2455
+ }
2456
+
2457
+ for (const { app, declaredConfig } of mfInspectorDeclaredConfigs) {
2458
+ const sharedPackages = asInspectorRecord(declaredConfig.shared);
2459
+ for (const packageName of Object.keys(sharedPackages)) {
2460
+ const packageConfig = asInspectorRecord(sharedPackages[packageName]);
2461
+ const existing = mfInspectorDeclaredSharedPackageMap.get(packageName) ?? {
2462
+ apps: [],
2463
+ requiredVersions: [],
2464
+ singletonPreferred: false,
2465
+ };
2466
+
2467
+ if (!existing.apps.includes(app)) {
2468
+ existing.apps.push(app);
2469
+ }
2470
+ existing.requiredVersions.push({
2471
+ app,
2472
+ version:
2473
+ typeof packageConfig.requiredVersion === "string"
2474
+ ? packageConfig.requiredVersion
2475
+ : packageConfig.requiredVersion === false
2476
+ ? false
2477
+ : null,
2478
+ });
2479
+ existing.singletonPreferred =
2480
+ existing.singletonPreferred || packageConfig.singleton === true;
2481
+ mfInspectorDeclaredSharedPackageMap.set(packageName, existing);
2482
+ }
2483
+ }
2484
+
2485
+ const mfInspectorShareScopeNames = Array.from(
2486
+ new Set([
2487
+ ...Object.keys(mfShareScopeForMatrix?.shareScopes ?? {}).filter(
2488
+ (scopeName) => scopeName !== "__error",
2489
+ ),
2490
+ ...(mfInspectorDeclaredSharedPackageMap.size > 0 ? ["default"] : []),
2491
+ ]),
2492
+ );
2493
+ const mfInspectorRuntimeSharedPackageMap = new Map<
2494
+ string,
2495
+ { registeredVersionCount: number; loaded: boolean }
2496
+ >();
2497
+
2498
+ for (const [scopeName, scopeValue] of Object.entries(
2499
+ mfShareScopeForMatrix?.shareScopes ?? {},
2500
+ )) {
2501
+ if (scopeName === "__error") continue;
2502
+ for (const [packageName, versionValue] of Object.entries(
2503
+ asInspectorRecord(scopeValue),
2504
+ )) {
2505
+ const versionEntries = Array.isArray(versionValue) ? versionValue : [];
2506
+ const existing = mfInspectorRuntimeSharedPackageMap.get(packageName) ?? {
2507
+ registeredVersionCount: 0,
2508
+ loaded: false,
2509
+ };
2510
+ existing.registeredVersionCount = Math.max(
2511
+ existing.registeredVersionCount,
2512
+ versionEntries.length,
2513
+ );
2514
+ existing.loaded =
2515
+ existing.loaded ||
2516
+ versionEntries.some(
2517
+ (entry) => asInspectorRecord(entry).loaded === true,
2518
+ );
2519
+ mfInspectorRuntimeSharedPackageMap.set(packageName, existing);
2520
+ }
2521
+ }
2522
+
2523
+ const mfInspectorSharedPackagesByApp = new Map<
2524
+ string,
2525
+ Array<{
2526
+ packageName: string;
2527
+ singletonPreferred: boolean;
2528
+ runtimeRegistered: boolean;
2529
+ runtimeLoaded: boolean;
2530
+ }>
2531
+ >();
2532
+
2533
+ for (const [
2534
+ packageName,
2535
+ packageInfo,
2536
+ ] of mfInspectorDeclaredSharedPackageMap) {
2537
+ for (const appName of packageInfo.apps) {
2538
+ const appPackages = mfInspectorSharedPackagesByApp.get(appName) ?? [];
2539
+ const runtimePackageInfo =
2540
+ mfInspectorRuntimeSharedPackageMap.get(packageName) ?? null;
2541
+ appPackages.push({
2542
+ packageName,
2543
+ singletonPreferred: packageInfo.singletonPreferred,
2544
+ runtimeRegistered: runtimePackageInfo != null,
2545
+ runtimeLoaded: runtimePackageInfo?.loaded === true,
2546
+ });
2547
+ mfInspectorSharedPackagesByApp.set(appName, appPackages);
2548
+ }
2549
+ }
2550
+
2551
+ for (const packages of mfInspectorSharedPackagesByApp.values()) {
2552
+ packages.sort((left, right) =>
2553
+ left.packageName.localeCompare(right.packageName),
2554
+ );
2555
+ }
2401
2556
  const mfInspectorSummaryCards = [
2402
2557
  {
2403
2558
  label: "Host page",
@@ -2442,6 +2597,64 @@ export default function CodeRunnerModal() {
2442
2597
  },
2443
2598
  ];
2444
2599
 
2600
+ const getMfInspectorAppSharedPackages = (appName: string) =>
2601
+ mfInspectorSharedPackagesByApp.get(appName) ?? [];
2602
+
2603
+ const getMfInspectorAppSharedPackageSummary = (appName: string) => {
2604
+ const packages = getMfInspectorAppSharedPackages(appName);
2605
+ const previewItems = packages.slice(0, 2).map((entry) => entry.packageName);
2606
+ const hiddenCount = packages.length - previewItems.length;
2607
+
2608
+ return {
2609
+ count: packages.length,
2610
+ countLabel:
2611
+ packages.length === 1
2612
+ ? "1 shared package"
2613
+ : `${packages.length} shared packages`,
2614
+ previewText:
2615
+ packages.length === 0
2616
+ ? "none declared"
2617
+ : previewItems.join(", ") +
2618
+ (hiddenCount > 0 ? ` +${hiddenCount}` : ""),
2619
+ };
2620
+ };
2621
+
2622
+ const renderMfInspectorPackageBadges = (appName: string) => {
2623
+ const packages = getMfInspectorAppSharedPackages(appName);
2624
+
2625
+ if (packages.length === 0) {
2626
+ return <span className="text-slate-600">none declared</span>;
2627
+ }
2628
+
2629
+ return (
2630
+ <div className="flex flex-wrap gap-1">
2631
+ {packages.map((entry) => {
2632
+ const toneClass = entry.runtimeLoaded
2633
+ ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-200"
2634
+ : entry.runtimeRegistered
2635
+ ? "border-cyan-500/30 bg-cyan-500/10 text-cyan-200"
2636
+ : "border-slate-700 bg-slate-800/50 text-slate-300";
2637
+ return (
2638
+ <span
2639
+ key={`${appName}-${entry.packageName}`}
2640
+ className={`rounded border px-1.5 py-0.5 text-[10px] font-mono ${toneClass}`}
2641
+ title={
2642
+ entry.runtimeLoaded
2643
+ ? "Seen and loaded at runtime"
2644
+ : entry.runtimeRegistered
2645
+ ? "Seen in webpack runtime registry"
2646
+ : "Declared in package.json but not yet seen at runtime"
2647
+ }
2648
+ >
2649
+ {entry.packageName}
2650
+ {entry.singletonPreferred ? " · one copy" : ""}
2651
+ </span>
2652
+ );
2653
+ })}
2654
+ </div>
2655
+ );
2656
+ };
2657
+
2445
2658
  const renderMfInspectorTruthCell = (
2446
2659
  sameInstance: boolean | null,
2447
2660
  error: string | null,
@@ -4526,9 +4739,11 @@ export default function CodeRunnerModal() {
4526
4739
  const remoteY = 156;
4527
4740
  const nodeW = 96;
4528
4741
  const nodeH = 46;
4529
- const svgH = 220;
4742
+ const svgH = 250;
4530
4743
  const hostBooted = mfInspectorBootMap.has("host");
4531
4744
  const hostData = mfInspectorBootMap.get("host");
4745
+ const hostPackageSummary =
4746
+ getMfInspectorAppSharedPackageSummary("host");
4532
4747
 
4533
4748
  const getConnectionStatus = (
4534
4749
  remoteName: string,
@@ -4716,6 +4931,30 @@ export default function CodeRunnerModal() {
4716
4931
  {"▣ DOM " + hostData.reactDomVersion}
4717
4932
  </text>
4718
4933
  )}
4934
+ {hostPackageSummary.count > 0 && (
4935
+ <>
4936
+ <text
4937
+ x={W / 2}
4938
+ y={hostY + nodeH / 2 + 16}
4939
+ textAnchor="middle"
4940
+ fill="#cbd5e1"
4941
+ fontSize={8}
4942
+ fontFamily="ui-sans-serif, sans-serif"
4943
+ >
4944
+ {hostPackageSummary.countLabel}
4945
+ </text>
4946
+ <text
4947
+ x={W / 2}
4948
+ y={hostY + nodeH / 2 + 27}
4949
+ textAnchor="middle"
4950
+ fill="#64748b"
4951
+ fontSize={7.5}
4952
+ fontFamily="ui-monospace, monospace"
4953
+ >
4954
+ {hostPackageSummary.previewText}
4955
+ </text>
4956
+ </>
4957
+ )}
4719
4958
 
4720
4959
  {/* REMOTE nodes */}
4721
4960
  {displayRemotes.map((remoteName, i) => {
@@ -4723,6 +4962,10 @@ export default function CodeRunnerModal() {
4723
4962
  const status = isPlaceholder
4724
4963
  ? "idle"
4725
4964
  : getConnectionStatus(remoteName);
4965
+ const packageSummary =
4966
+ getMfInspectorAppSharedPackageSummary(
4967
+ remoteName,
4968
+ );
4726
4969
  const remoteData =
4727
4970
  mfInspectorBootMap.get(remoteName);
4728
4971
  const keys =
@@ -4958,6 +5201,30 @@ export default function CodeRunnerModal() {
4958
5201
  )}
4959
5202
  </g>
4960
5203
  )}
5204
+ {packageSummary.count > 0 && (
5205
+ <g>
5206
+ <text
5207
+ x={rx}
5208
+ y={remoteY + nodeH / 2 + 26}
5209
+ textAnchor="middle"
5210
+ fill="#cbd5e1"
5211
+ fontSize={8}
5212
+ fontFamily="ui-sans-serif, sans-serif"
5213
+ >
5214
+ {packageSummary.countLabel}
5215
+ </text>
5216
+ <text
5217
+ x={rx}
5218
+ y={remoteY + nodeH / 2 + 37}
5219
+ textAnchor="middle"
5220
+ fill="#64748b"
5221
+ fontSize={7.5}
5222
+ fontFamily="ui-monospace, monospace"
5223
+ >
5224
+ {packageSummary.previewText}
5225
+ </text>
5226
+ </g>
5227
+ )}
4961
5228
  </g>
4962
5229
  );
4963
5230
  })}
@@ -5036,7 +5303,9 @@ export default function CodeRunnerModal() {
5036
5303
  <div className="text-[11px] text-slate-500 mb-2 leading-relaxed">
5037
5304
  "Same copy" is the healthy result for React and
5038
5305
  ReactDOM. "Different copy" often leads to hook,
5039
- context, or shared-state bugs.
5306
+ context, or shared-state bugs. The package list
5307
+ column reflects the current app package.json
5308
+ files.
5040
5309
  </div>
5041
5310
  <div className="rounded-lg border border-slate-800 overflow-hidden">
5042
5311
  <table className="w-full text-[11px]">
@@ -5051,6 +5320,9 @@ export default function CodeRunnerModal() {
5051
5320
  <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5052
5321
  DOM copy
5053
5322
  </th>
5323
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5324
+ Current shared packages
5325
+ </th>
5054
5326
  <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5055
5327
  Version seen
5056
5328
  </th>
@@ -5066,6 +5338,10 @@ export default function CodeRunnerModal() {
5066
5338
  mfInspectorIdentityResultMap.get(
5067
5339
  remoteKey,
5068
5340
  ) ?? null;
5341
+ const remoteAppName =
5342
+ result?.remoteApp ??
5343
+ remoteKey.split("/")[0] ??
5344
+ "unknown";
5069
5345
  const loadInfo =
5070
5346
  mfInspectorRemoteLoadMap.get(
5071
5347
  remoteKey,
@@ -5092,6 +5368,11 @@ export default function CodeRunnerModal() {
5092
5368
  loadInfo?.status ?? null,
5093
5369
  )}
5094
5370
  </td>
5371
+ <td className="px-3 py-2 align-top">
5372
+ {renderMfInspectorPackageBadges(
5373
+ remoteAppName,
5374
+ )}
5375
+ </td>
5095
5376
  <td className="px-3 py-2 font-mono text-slate-400 text-[10px]">
5096
5377
  {result?.reactVersion ??
5097
5378
  (loadInfo?.status ===
@@ -5257,13 +5538,13 @@ export default function CodeRunnerModal() {
5257
5538
  <>
5258
5539
  <div className="rounded-lg border border-slate-800 bg-slate-900/50 px-3 py-2.5 mb-3">
5259
5540
  <div className="text-[12px] font-semibold text-slate-100">
5260
- Live shared package list
5541
+ Shared package picture
5261
5542
  </div>
5262
5543
  <p className="mt-1 text-[11px] text-slate-400 leading-relaxed">
5263
- This is webpack's runtime list of libraries that
5264
- apps can reuse. It is more trustworthy than config
5265
- alone because it shows what actually happened in
5266
- the browser.
5544
+ This tab merges two sources: the packages found in
5545
+ the current app package.json files in the editor,
5546
+ and the packages webpack has actually registered
5547
+ in its live runtime share scope.
5267
5548
  </p>
5268
5549
  <p className="mt-1 text-[10px] text-slate-500">
5269
5550
  Snapshot source:{" "}
@@ -5276,168 +5557,239 @@ export default function CodeRunnerModal() {
5276
5557
  )}
5277
5558
  </p>
5278
5559
  </div>
5279
- {Object.entries(mfShareScopeForMatrix.shareScopes)
5280
- .filter(([sn]) => sn !== "__error")
5281
- .map(([scopeName, scopeValue]) => {
5282
- const packages = Object.entries(
5283
- asInspectorRecord(scopeValue),
5284
- );
5285
- return (
5286
- <div key={scopeName} className="mb-4">
5287
- <div className="text-[10px] text-slate-500 uppercase tracking-wider mb-2 font-mono">
5288
- sharing group: {scopeName}
5289
- </div>
5290
- <div className="rounded-lg border border-slate-800 overflow-hidden">
5291
- <table className="w-full text-[11px]">
5292
- <thead>
5293
- <tr className="bg-slate-900/80 border-b border-slate-800">
5294
- <th className="text-left px-3 py-1.5 text-slate-400 font-medium w-32">
5295
- Package
5296
- </th>
5297
- <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5298
- Versions webpack can reuse
5299
- </th>
5300
- </tr>
5301
- </thead>
5302
- <tbody>
5303
- {packages.map(
5304
- ([pkgName, versionValue]) => {
5305
- const versionEntries =
5306
- Array.isArray(versionValue)
5307
- ? versionValue
5308
- : [];
5309
- const declaredSingleton =
5310
- mfInspectorDeclaredConfigs.some(
5311
- (cfg) => {
5312
- const sh = asInspectorRecord(
5313
- asInspectorRecord(
5314
- cfg.declaredConfig,
5315
- ).shared,
5316
- );
5317
- return (
5318
- asInspectorRecord(
5319
- sh[pkgName],
5320
- ).singleton === true
5321
- );
5322
- },
5323
- );
5324
- const loadedCount =
5325
- versionEntries.filter(
5326
- (ve) =>
5327
- asInspectorRecord(ve)
5328
- .loaded === true,
5329
- ).length;
5330
- return (
5331
- <tr
5332
- key={pkgName}
5333
- className="border-b border-slate-800/50 last:border-0"
5334
- >
5335
- <td className="px-3 py-2.5 align-top">
5336
- <div className="font-mono text-slate-100 font-medium">
5337
- {pkgName}
5338
- </div>
5339
- <div className="flex flex-wrap gap-1 mt-1">
5340
- {declaredSingleton && (
5341
- <span className="rounded border border-amber-500/30 bg-amber-500/10 px-1 py-0.5 text-[9px] text-amber-300">
5342
- one copy preferred
5343
- </span>
5560
+ {mfInspectorShareScopeNames.map((scopeName) => {
5561
+ const scopeValue =
5562
+ scopeName === "default"
5563
+ ? asInspectorRecord(
5564
+ mfShareScopeForMatrix.shareScopes.default,
5565
+ )
5566
+ : asInspectorRecord(
5567
+ mfShareScopeForMatrix.shareScopes[
5568
+ scopeName
5569
+ ],
5570
+ );
5571
+ const packageNames = Array.from(
5572
+ new Set([
5573
+ ...Object.keys(scopeValue),
5574
+ ...(scopeName === "default"
5575
+ ? Array.from(
5576
+ mfInspectorDeclaredSharedPackageMap.keys(),
5577
+ )
5578
+ : []),
5579
+ ]),
5580
+ ).sort((left, right) => left.localeCompare(right));
5581
+
5582
+ if (packageNames.length === 0) return null;
5583
+
5584
+ return (
5585
+ <div key={scopeName} className="mb-4">
5586
+ <div className="text-[10px] text-slate-500 uppercase tracking-wider mb-2 font-mono">
5587
+ sharing group: {scopeName}
5588
+ </div>
5589
+ <div className="rounded-lg border border-slate-800 overflow-hidden">
5590
+ <table className="w-full text-[11px]">
5591
+ <thead>
5592
+ <tr className="bg-slate-900/80 border-b border-slate-800">
5593
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium w-32">
5594
+ Package
5595
+ </th>
5596
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium w-56">
5597
+ Declared by apps
5598
+ </th>
5599
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5600
+ Versions webpack can reuse
5601
+ </th>
5602
+ </tr>
5603
+ </thead>
5604
+ <tbody>
5605
+ {packageNames.map((pkgName) => {
5606
+ const versionValue =
5607
+ scopeValue[pkgName];
5608
+ const versionEntries = Array.isArray(
5609
+ versionValue,
5610
+ )
5611
+ ? versionValue
5612
+ : [];
5613
+ const declaredInfo =
5614
+ mfInspectorDeclaredSharedPackageMap.get(
5615
+ pkgName,
5616
+ ) ?? null;
5617
+ const declaredSingleton =
5618
+ declaredInfo?.singletonPreferred ===
5619
+ true;
5620
+ const declaredVersionSummary =
5621
+ declaredInfo?.requiredVersions
5622
+ .filter(
5623
+ ({ version }) => version !== null,
5624
+ )
5625
+ .map(({ app, version }) =>
5626
+ version === false
5627
+ ? `${app}: any`
5628
+ : `${app}: ${version}`,
5629
+ ) ?? [];
5630
+ const loadedCount =
5631
+ versionEntries.filter(
5632
+ (ve) =>
5633
+ asInspectorRecord(ve).loaded ===
5634
+ true,
5635
+ ).length;
5636
+ return (
5637
+ <tr
5638
+ key={pkgName}
5639
+ className="border-b border-slate-800/50 last:border-0"
5640
+ >
5641
+ <td className="px-3 py-2.5 align-top">
5642
+ <div className="font-mono text-slate-100 font-medium">
5643
+ {pkgName}
5644
+ </div>
5645
+ <div className="flex flex-wrap gap-1 mt-1">
5646
+ {declaredSingleton && (
5647
+ <span className="rounded border border-amber-500/30 bg-amber-500/10 px-1 py-0.5 text-[9px] text-amber-300">
5648
+ one copy preferred
5649
+ </span>
5650
+ )}
5651
+ {declaredInfo ? (
5652
+ <span className="rounded border border-cyan-500/30 bg-cyan-500/10 px-1 py-0.5 text-[9px] text-cyan-300">
5653
+ from current package.json
5654
+ </span>
5655
+ ) : (
5656
+ <span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-500">
5657
+ runtime only
5658
+ </span>
5659
+ )}
5660
+ <span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-400">
5661
+ {loadedCount}/
5662
+ {versionEntries.length}{" "}
5663
+ already used
5664
+ </span>
5665
+ </div>
5666
+ </td>
5667
+ <td className="px-3 py-2.5 align-top">
5668
+ {declaredInfo ? (
5669
+ <div>
5670
+ <div className="flex flex-wrap gap-1">
5671
+ {declaredInfo.apps.map(
5672
+ (appName) => (
5673
+ <span
5674
+ key={appName}
5675
+ className="rounded border border-slate-700 bg-slate-800/50 px-1.5 py-0.5 text-[9px] text-slate-300 font-mono"
5676
+ >
5677
+ {appName}
5678
+ </span>
5679
+ ),
5344
5680
  )}
5345
- <span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-400">
5346
- {loadedCount}/
5347
- {versionEntries.length}{" "}
5348
- already used
5349
- </span>
5350
5681
  </div>
5351
- </td>
5352
- <td className="px-3 py-2.5">
5353
- <div className="flex flex-wrap gap-2">
5354
- {versionEntries.length ===
5355
- 0 ? (
5356
- <span className="text-slate-600">
5357
- none yet
5358
- </span>
5359
- ) : (
5360
- versionEntries.map(
5361
- (ve, vi) => {
5362
- const vr =
5363
- asInspectorRecord(
5364
- ve,
5365
- );
5366
- const version =
5367
- typeof vr.version ===
5368
- "string"
5369
- ? vr.version
5370
- : "?";
5371
- const from =
5372
- typeof vr.from ===
5373
- "string"
5374
- ? vr.from
5375
- : null;
5376
- const loaded =
5377
- vr.loaded === true;
5378
- const eager =
5379
- vr.eager === true;
5380
- return (
5381
- <div
5382
- key={vi}
5383
- className={`rounded-lg border px-2 py-1.5 font-mono text-[10px] ${
5682
+ {declaredVersionSummary.length >
5683
+ 0 && (
5684
+ <div className="mt-1 text-[9px] text-slate-500 leading-relaxed">
5685
+ required versions:{" "}
5686
+ {declaredVersionSummary.join(
5687
+ ", ",
5688
+ )}
5689
+ </div>
5690
+ )}
5691
+ </div>
5692
+ ) : (
5693
+ <span className="text-slate-600">
5694
+ not declared in current app
5695
+ config
5696
+ </span>
5697
+ )}
5698
+ </td>
5699
+ <td className="px-3 py-2.5">
5700
+ <div className="flex flex-wrap gap-2">
5701
+ {versionEntries.length === 0 ? (
5702
+ declaredInfo ? (
5703
+ <div className="rounded-lg border border-slate-700 bg-slate-800/40 px-2 py-1.5 text-[10px] text-slate-400 leading-relaxed max-w-[22rem]">
5704
+ Declared for sharing, but
5705
+ webpack has not put it in
5706
+ the live runtime registry
5707
+ yet.
5708
+ </div>
5709
+ ) : (
5710
+ <span className="text-slate-600">
5711
+ none yet
5712
+ </span>
5713
+ )
5714
+ ) : (
5715
+ versionEntries.map(
5716
+ (ve, vi) => {
5717
+ const vr =
5718
+ asInspectorRecord(ve);
5719
+ const version =
5720
+ typeof vr.version ===
5721
+ "string"
5722
+ ? vr.version
5723
+ : "?";
5724
+ const from =
5725
+ typeof vr.from ===
5726
+ "string"
5727
+ ? vr.from
5728
+ : null;
5729
+ const loaded =
5730
+ vr.loaded === true;
5731
+ const eager =
5732
+ vr.eager === true;
5733
+ return (
5734
+ <div
5735
+ key={vi}
5736
+ className={`rounded-lg border px-2 py-1.5 font-mono text-[10px] ${
5737
+ loaded
5738
+ ? "border-emerald-500/30 bg-emerald-500/5"
5739
+ : "border-slate-700 bg-slate-800/40"
5740
+ }`}
5741
+ >
5742
+ <div
5743
+ className={`font-semibold ${loaded ? "text-emerald-200" : "text-slate-400"}`}
5744
+ >
5745
+ {version}
5746
+ </div>
5747
+ {from && (
5748
+ <div className="text-slate-500 mt-0.5 text-[9px]">
5749
+ registered by{" "}
5750
+ {from}
5751
+ </div>
5752
+ )}
5753
+ <div className="flex gap-1 mt-1">
5754
+ <span
5755
+ className={`rounded px-1 py-0.5 text-[8px] ${
5384
5756
  loaded
5385
- ? "border-emerald-500/30 bg-emerald-500/5"
5386
- : "border-slate-700 bg-slate-800/40"
5757
+ ? "bg-emerald-500/20 text-emerald-300"
5758
+ : "bg-slate-700 text-slate-500"
5387
5759
  }`}
5388
5760
  >
5389
- <div
5390
- className={`font-semibold ${loaded ? "text-emerald-200" : "text-slate-400"}`}
5391
- >
5392
- {version}
5393
- </div>
5394
- {from && (
5395
- <div className="text-slate-500 mt-0.5 text-[9px]">
5396
- registered by{" "}
5397
- {from}
5398
- </div>
5399
- )}
5400
- <div className="flex gap-1 mt-1">
5401
- <span
5402
- className={`rounded px-1 py-0.5 text-[8px] ${
5403
- loaded
5404
- ? "bg-emerald-500/20 text-emerald-300"
5405
- : "bg-slate-700 text-slate-500"
5406
- }`}
5407
- >
5408
- {loaded
5409
- ? "in use"
5410
- : "registered"}
5411
- </span>
5412
- <span
5413
- className={`rounded px-1 py-0.5 text-[8px] ${
5414
- eager
5415
- ? "bg-amber-500/20 text-amber-300"
5416
- : "bg-slate-800 text-slate-600"
5417
- }`}
5418
- >
5419
- {eager
5420
- ? "load now"
5421
- : "load later"}
5422
- </span>
5423
- </div>
5424
- </div>
5425
- );
5426
- },
5427
- )
5428
- )}
5429
- </div>
5430
- </td>
5431
- </tr>
5432
- );
5433
- },
5434
- )}
5435
- </tbody>
5436
- </table>
5437
- </div>
5761
+ {loaded
5762
+ ? "in use"
5763
+ : "registered"}
5764
+ </span>
5765
+ <span
5766
+ className={`rounded px-1 py-0.5 text-[8px] ${
5767
+ eager
5768
+ ? "bg-amber-500/20 text-amber-300"
5769
+ : "bg-slate-800 text-slate-600"
5770
+ }`}
5771
+ >
5772
+ {eager
5773
+ ? "load now"
5774
+ : "load later"}
5775
+ </span>
5776
+ </div>
5777
+ </div>
5778
+ );
5779
+ },
5780
+ )
5781
+ )}
5782
+ </div>
5783
+ </td>
5784
+ </tr>
5785
+ );
5786
+ })}
5787
+ </tbody>
5788
+ </table>
5438
5789
  </div>
5439
- );
5440
- })}
5790
+ </div>
5791
+ );
5792
+ })}
5441
5793
  {typeof mfShareScopeForMatrix.shareScopes.__error ===
5442
5794
  "string" && (
5443
5795
  <div className="rounded border border-red-500/20 bg-red-500/10 px-3 py-2 text-[11px] text-red-200">
@@ -216,10 +216,12 @@ This lab uses real webpack 5 + webpack-dev-server + Module Federation.
216
216
 
217
217
  ## What is here
218
218
 
219
- - package.json runs three apps together: a host plus two remotes
219
+ - the root package.json orchestrates three npm workspaces: a host plus two remotes
220
+ - each app owns its own dependencies in its local package.json
220
221
  - apps/host consumes federated modules from the remotes
221
222
  - apps/profile exposes a profile widget
222
223
  - apps/checkout exposes a checkout widget
224
+ - react-router-dom is declared in each app package.json so routing experiments work across apps
223
225
 
224
226
  ## Good experiments
225
227
 
@@ -231,33 +233,29 @@ This lab uses real webpack 5 + webpack-dev-server + Module Federation.
231
233
  ## Notes
232
234
 
233
235
  - Ports are injected by the lab runner through environment variables.
234
- - If you change package.json, restart the webpack lab so dependencies/scripts are re-read.
236
+ - Each webpack config builds its shared dependency list from that app's runtime dependencies.
237
+ - If you add or remove dependencies in an app package.json, restart the webpack lab so npm workspaces reinstall them.
235
238
  `,
236
239
  "package.json": `{
237
240
  "name": "webpack-module-federation-lab",
238
241
  "private": true,
242
+ "workspaces": [
243
+ "apps/host",
244
+ "apps/profile",
245
+ "apps/checkout"
246
+ ],
239
247
  "scripts": {
240
- "dev": "concurrently -k -n host,profile,checkout -c cyan,magenta,yellow 'npm --prefix apps/host run dev' 'npm --prefix apps/profile run dev' 'npm --prefix apps/checkout run dev'",
241
- "dev:host": "npm --prefix apps/host run dev",
242
- "dev:profile": "npm --prefix apps/profile run dev",
243
- "dev:checkout": "npm --prefix apps/checkout run dev",
244
- "build": "npm --prefix apps/host run build && npm --prefix apps/profile run build && npm --prefix apps/checkout run build",
245
- "build:host": "npm --prefix apps/host run build",
246
- "build:profile": "npm --prefix apps/profile run build",
247
- "build:checkout": "npm --prefix apps/checkout run build"
248
- },
249
- "dependencies": {
250
- "react": "^19.0.0",
251
- "react-dom": "^19.0.0"
248
+ "dev": "concurrently -k -n host,profile,checkout -c cyan,magenta,yellow 'npm run dev --workspace=@mf-lab/host' 'npm run dev --workspace=@mf-lab/profile' 'npm run dev --workspace=@mf-lab/checkout'",
249
+ "dev:host": "npm run dev --workspace=@mf-lab/host",
250
+ "dev:profile": "npm run dev --workspace=@mf-lab/profile",
251
+ "dev:checkout": "npm run dev --workspace=@mf-lab/checkout",
252
+ "build": "npm run build --workspace=@mf-lab/host && npm run build --workspace=@mf-lab/profile && npm run build --workspace=@mf-lab/checkout",
253
+ "build:host": "npm run build --workspace=@mf-lab/host",
254
+ "build:profile": "npm run build --workspace=@mf-lab/profile",
255
+ "build:checkout": "npm run build --workspace=@mf-lab/checkout"
252
256
  },
253
257
  "devDependencies": {
254
- "concurrently": "^9.2.1",
255
- "esbuild": "^0.28.0",
256
- "esbuild-loader": "^4.4.3",
257
- "html-webpack-plugin": "^5.6.7",
258
- "webpack": "^5.106.2",
259
- "webpack-cli": "^7.0.2",
260
- "webpack-dev-server": "^5.2.3"
258
+ "concurrently": "^9.2.1"
261
259
  }
262
260
  }
263
261
  `,
@@ -279,6 +277,19 @@ This lab uses real webpack 5 + webpack-dev-server + Module Federation.
279
277
  "scripts": {
280
278
  "dev": "webpack serve --config webpack.config.js",
281
279
  "build": "webpack --config webpack.config.js"
280
+ },
281
+ "dependencies": {
282
+ "react": "^19.0.0",
283
+ "react-dom": "^19.0.0",
284
+ "react-router-dom": "^7.6.1"
285
+ },
286
+ "devDependencies": {
287
+ "esbuild": "^0.28.0",
288
+ "esbuild-loader": "^4.4.3",
289
+ "html-webpack-plugin": "^5.6.7",
290
+ "webpack": "^5.106.2",
291
+ "webpack-cli": "^7.0.2",
292
+ "webpack-dev-server": "^5.2.3"
282
293
  }
283
294
  }
284
295
  `,
@@ -503,6 +514,44 @@ export function makeInspectableLazy(remoteKey, componentLoad, debugLoad) {
503
514
  }
504
515
  };
505
516
  }
517
+ `,
518
+ "apps/shared/buildSharedConfig.js": `function createSharedConfig(packageJson) {
519
+ const dependencyVersions =
520
+ packageJson && typeof packageJson === "object"
521
+ ? packageJson.dependencies || {}
522
+ : {};
523
+
524
+ const sharedOverrides = {
525
+ react: {
526
+ singleton: true,
527
+ requiredVersion: dependencyVersions.react || false,
528
+ },
529
+ "react-dom": {
530
+ singleton: true,
531
+ requiredVersion: dependencyVersions["react-dom"] || false,
532
+ },
533
+ "react-router-dom": {
534
+ singleton: true,
535
+ requiredVersion: dependencyVersions["react-router-dom"] || false,
536
+ },
537
+ };
538
+
539
+ // Add package names here if you want them installed but NOT shared.
540
+ const unsharedPackages = new Set([]);
541
+
542
+ return Object.fromEntries(
543
+ Object.keys(dependencyVersions)
544
+ .filter((packageName) => !unsharedPackages.has(packageName))
545
+ .map((packageName) => [
546
+ packageName,
547
+ sharedOverrides[packageName] ?? {
548
+ requiredVersion: dependencyVersions[packageName] || false,
549
+ },
550
+ ]),
551
+ );
552
+ }
553
+
554
+ module.exports = { createSharedConfig };
506
555
  `,
507
556
  "apps/host/src/index.jsx": `import("./bootstrap");
508
557
  `,
@@ -607,14 +656,13 @@ export default function App() {
607
656
  const webpack = require("webpack");
608
657
  const HtmlWebpackPlugin = require("html-webpack-plugin");
609
658
  const { ModuleFederationPlugin } = webpack.container;
659
+ const packageJson = require("./package.json");
660
+ const { createSharedConfig } = require("../shared/buildSharedConfig");
610
661
 
611
662
  const hostPort = Number(process.env.HOST_PORT || 3100);
612
663
  const profilePort = Number(process.env.PROFILE_PORT || 3101);
613
664
  const checkoutPort = Number(process.env.CHECKOUT_PORT || 3102);
614
- const sharedConfig = {
615
- react: { singleton: true, requiredVersion: false },
616
- "react-dom": { singleton: true, requiredVersion: false },
617
- };
665
+ const sharedConfig = createSharedConfig(packageJson);
618
666
  const remoteConfig = {
619
667
  profile: "profile@http://localhost:" + profilePort + "/remoteEntry.js",
620
668
  checkout: "checkout@http://localhost:" + checkoutPort + "/remoteEntry.js",
@@ -695,6 +743,19 @@ module.exports = {
695
743
  "scripts": {
696
744
  "dev": "webpack serve --config webpack.config.js",
697
745
  "build": "webpack --config webpack.config.js"
746
+ },
747
+ "dependencies": {
748
+ "react": "^19.0.0",
749
+ "react-dom": "^19.0.0",
750
+ "react-router-dom": "^7.6.1"
751
+ },
752
+ "devDependencies": {
753
+ "esbuild": "^0.28.0",
754
+ "esbuild-loader": "^4.4.3",
755
+ "html-webpack-plugin": "^5.6.7",
756
+ "webpack": "^5.106.2",
757
+ "webpack-cli": "^7.0.2",
758
+ "webpack-dev-server": "^5.2.3"
698
759
  }
699
760
  }
700
761
  `,
@@ -758,12 +819,11 @@ export default function ProfileCard() {
758
819
  const webpack = require("webpack");
759
820
  const HtmlWebpackPlugin = require("html-webpack-plugin");
760
821
  const { ModuleFederationPlugin } = webpack.container;
822
+ const packageJson = require("./package.json");
823
+ const { createSharedConfig } = require("../shared/buildSharedConfig");
761
824
 
762
825
  const profilePort = Number(process.env.PROFILE_PORT || 3101);
763
- const sharedConfig = {
764
- react: { singleton: true, requiredVersion: false },
765
- "react-dom": { singleton: true, requiredVersion: false },
766
- };
826
+ const sharedConfig = createSharedConfig(packageJson);
767
827
  const exposeConfig = {
768
828
  "./ProfileCard": path.resolve(__dirname, "./src/ProfileCard.jsx"),
769
829
  "./InspectorBridge": path.resolve(__dirname, "./src/inspectorBridge.js"),
@@ -803,6 +863,7 @@ module.exports = {
803
863
  },
804
864
  devServer: {
805
865
  port: profilePort,
866
+ historyApiFallback: true,
806
867
  hot: true,
807
868
  headers: {
808
869
  "Access-Control-Allow-Origin": "*",
@@ -844,6 +905,19 @@ module.exports = {
844
905
  "scripts": {
845
906
  "dev": "webpack serve --config webpack.config.js",
846
907
  "build": "webpack --config webpack.config.js"
908
+ },
909
+ "dependencies": {
910
+ "react": "^19.0.0",
911
+ "react-dom": "^19.0.0",
912
+ "react-router-dom": "^7.6.1"
913
+ },
914
+ "devDependencies": {
915
+ "esbuild": "^0.28.0",
916
+ "esbuild-loader": "^4.4.3",
917
+ "html-webpack-plugin": "^5.6.7",
918
+ "webpack": "^5.106.2",
919
+ "webpack-cli": "^7.0.2",
920
+ "webpack-dev-server": "^5.2.3"
847
921
  }
848
922
  }
849
923
  `,
@@ -915,12 +989,11 @@ export default function CheckoutPanel() {
915
989
  const webpack = require("webpack");
916
990
  const HtmlWebpackPlugin = require("html-webpack-plugin");
917
991
  const { ModuleFederationPlugin } = webpack.container;
992
+ const packageJson = require("./package.json");
993
+ const { createSharedConfig } = require("../shared/buildSharedConfig");
918
994
 
919
995
  const checkoutPort = Number(process.env.CHECKOUT_PORT || 3102);
920
- const sharedConfig = {
921
- react: { singleton: true, requiredVersion: false },
922
- "react-dom": { singleton: true, requiredVersion: false },
923
- };
996
+ const sharedConfig = createSharedConfig(packageJson);
924
997
  const exposeConfig = {
925
998
  "./CheckoutPanel": path.resolve(__dirname, "./src/CheckoutPanel.jsx"),
926
999
  "./InspectorBridge": path.resolve(__dirname, "./src/inspectorBridge.js"),
@@ -960,6 +1033,7 @@ module.exports = {
960
1033
  },
961
1034
  devServer: {
962
1035
  port: checkoutPort,
1036
+ historyApiFallback: true,
963
1037
  hot: true,
964
1038
  headers: {
965
1039
  "Access-Control-Allow-Origin": "*",
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.8.0"
2
+ "version": "0.9.0"
3
3
  }