create-interview-cockpit 0.10.0 → 0.12.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.
@@ -56,6 +56,11 @@ import {
56
56
  updateNextjsFiles,
57
57
  updateModuleFederationFiles,
58
58
  stopNextjsSandbox,
59
+ startReactLabSandbox,
60
+ updateReactLabFiles,
61
+ streamReactLabCommand,
62
+ stopReactLabSandbox,
63
+ readReactLabFile,
59
64
  } from "../api";
60
65
  import ReactMarkdown from "react-markdown";
61
66
  import remarkGfm from "remark-gfm";
@@ -646,6 +651,15 @@ export default function CodeRunnerModal() {
646
651
  const [reactNavInput, setReactNavInput] = useState("/");
647
652
  const [reactNavHistory, setReactNavHistory] = useState<string[]>(["/"]);
648
653
  const [reactNavIndex, setReactNavIndex] = useState(0);
654
+ // Real Vite dev-server state (React lab)
655
+ const [viteSandboxId, setViteSandboxId] = useState<string | null>(null);
656
+ const [viteSandboxUrl, setViteSandboxUrl] = useState<string | null>(null);
657
+ const [viteStarting, setViteStarting] = useState(false);
658
+ const [viteError, setViteError] = useState<string | null>(null);
659
+ const [viteConsoleCommand, setViteConsoleCommand] = useState("npm install");
660
+ const [viteConsoleOutput, setViteConsoleOutput] = useState<OutputLine[]>([]);
661
+ const [viteConsoleRunning, setViteConsoleRunning] = useState(false);
662
+ const viteIframeRef = useRef<HTMLIFrameElement>(null);
649
663
 
650
664
  // ── Sandbox output tab ("output" | "console" | "chat") ─────────────
651
665
  const [sbxBottomTab, setSbxBottomTab] = useState<SbxBottomTab>("output");
@@ -873,6 +887,13 @@ export default function CodeRunnerModal() {
873
887
  }
874
888
  setReactPreviewSrc(null);
875
889
  setReactClientTab("edit");
890
+ if (ct === "react") {
891
+ setViteSandboxId(null);
892
+ setViteSandboxUrl(null);
893
+ setViteError(null);
894
+ setViteConsoleOutput([]);
895
+ setViteConsoleRunning(false);
896
+ }
876
897
  if (ct === "module-federation") {
877
898
  setServerCollapsed(true);
878
899
  setClientCollapsed(false);
@@ -901,7 +922,13 @@ export default function CodeRunnerModal() {
901
922
  if (sbxBottomTab === "console") {
902
923
  mfConsoleEndRef.current?.scrollIntoView({ behavior: "smooth" });
903
924
  }
904
- }, [mfConsoleOutput, mfConsoleRunning, sbxBottomTab]);
925
+ }, [
926
+ mfConsoleOutput,
927
+ viteConsoleOutput,
928
+ mfConsoleRunning,
929
+ viteConsoleRunning,
930
+ sbxBottomTab,
931
+ ]);
905
932
 
906
933
  useEffect(() => {
907
934
  if (clientType !== "module-federation") return;
@@ -1278,7 +1305,11 @@ export default function CodeRunnerModal() {
1278
1305
  return files["app/page.tsx"]
1279
1306
  ? "app/page.tsx"
1280
1307
  : (Object.keys(files)[0] ?? "");
1281
- return files["App.tsx"] ? "App.tsx" : (Object.keys(files)[0] ?? "");
1308
+ return files["main.tsx"]
1309
+ ? "main.tsx"
1310
+ : files["App.tsx"]
1311
+ ? "App.tsx"
1312
+ : (Object.keys(files)[0] ?? "");
1282
1313
  };
1283
1314
 
1284
1315
  const refreshPreview = useCallback(
@@ -1325,6 +1356,15 @@ export default function CodeRunnerModal() {
1325
1356
  entry = resolved;
1326
1357
  } else {
1327
1358
  entry = getReactEntry(reactFiles, type);
1359
+ if (
1360
+ entry === "main.tsx" &&
1361
+ !/export\s+function\s+mount\s*\(/.test(reactFiles[entry] ?? "") &&
1362
+ reactFiles["App.tsx"]
1363
+ ) {
1364
+ // Compatibility path for older React labs created before main.tsx
1365
+ // exported an explicit mount() entry contract.
1366
+ entry = "App.tsx";
1367
+ }
1328
1368
  }
1329
1369
  if (!entry) return;
1330
1370
  const html = generatePreviewHTML(
@@ -1349,6 +1389,19 @@ export default function CodeRunnerModal() {
1349
1389
  }
1350
1390
  }, [reactFiles, reactClientTab, reactPreviewSrc, refreshPreview]);
1351
1391
 
1392
+ // Auto-sync file edits to the running Vite server (HMR picks them up)
1393
+ const viteFileSyncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
1394
+ useEffect(() => {
1395
+ if (!viteSandboxId) return;
1396
+ if (viteFileSyncTimer.current) clearTimeout(viteFileSyncTimer.current);
1397
+ viteFileSyncTimer.current = setTimeout(() => {
1398
+ void updateReactLabFiles(viteSandboxId, reactFiles);
1399
+ }, 400);
1400
+ return () => {
1401
+ if (viteFileSyncTimer.current) clearTimeout(viteFileSyncTimer.current);
1402
+ };
1403
+ }, [reactFiles, viteSandboxId]);
1404
+
1352
1405
  /** Navigate to a new path (updates URL bar + history + re-renders preview). */
1353
1406
  const navigatePreview = useCallback(
1354
1407
  (to: string) => {
@@ -1365,12 +1418,23 @@ export default function CodeRunnerModal() {
1365
1418
  [reactNavIndex, refreshPreview],
1366
1419
  );
1367
1420
 
1368
- // Listen for rlab-nav messages from the preview iframe
1421
+ // Listen for rlab-nav and rlab-err messages from the preview iframe
1369
1422
  useEffect(() => {
1370
1423
  const handler = (e: MessageEvent) => {
1371
1424
  if (e.data?.type === "rlab-nav" && typeof e.data.to === "string") {
1372
1425
  navigatePreview(e.data.to);
1373
1426
  }
1427
+ if (e.data?.type === "rlab-err" && typeof e.data.error === "string") {
1428
+ setOutput((prev) => [
1429
+ ...prev,
1430
+ {
1431
+ kind: "stderr",
1432
+ text: `Preview error: ${e.data.error}`,
1433
+ source: "client",
1434
+ },
1435
+ ]);
1436
+ setSbxBottomTab("output");
1437
+ }
1374
1438
  };
1375
1439
  window.addEventListener("message", handler);
1376
1440
  return () => window.removeEventListener("message", handler);
@@ -1782,16 +1846,84 @@ export default function CodeRunnerModal() {
1782
1846
  current === "console" || current === "inspector" ? "output" : current,
1783
1847
  );
1784
1848
  }
1785
- }, [clientType, nxSandboxId, mfSandboxId]);
1849
+ if (prev === "react" && clientType !== "react" && viteSandboxId) {
1850
+ void stopReactLabSandbox(viteSandboxId);
1851
+ setViteSandboxId(null);
1852
+ setViteSandboxUrl(null);
1853
+ setViteConsoleRunning(false);
1854
+ setSbxBottomTab((current) =>
1855
+ current === "console" ? "output" : current,
1856
+ );
1857
+ }
1858
+ }, [clientType, nxSandboxId, mfSandboxId, viteSandboxId]);
1786
1859
 
1787
1860
  // Clean up on unmount
1788
1861
  useEffect(() => {
1789
1862
  return () => {
1790
1863
  if (nxSandboxId) void stopNextjsSandbox(nxSandboxId);
1791
1864
  if (mfSandboxId) void stopModuleFederationSandbox(mfSandboxId);
1865
+ if (viteSandboxId) void stopReactLabSandbox(viteSandboxId);
1792
1866
  };
1793
1867
  // eslint-disable-next-line react-hooks/exhaustive-deps
1794
- }, [nxSandboxId, mfSandboxId]);
1868
+ }, [nxSandboxId, mfSandboxId, viteSandboxId]);
1869
+
1870
+ const startViteServer = useCallback(async () => {
1871
+ if (viteStarting) return;
1872
+ setViteStarting(true);
1873
+ setViteError(null);
1874
+ setViteConsoleOutput([]);
1875
+ try {
1876
+ const info = await startReactLabSandbox(reactFiles);
1877
+ setViteSandboxId(info.id);
1878
+ setViteSandboxUrl(info.url);
1879
+ setReactClientTab("preview");
1880
+ } catch (err: any) {
1881
+ setViteError(err?.message ?? String(err));
1882
+ } finally {
1883
+ setViteStarting(false);
1884
+ }
1885
+ }, [viteStarting, reactFiles]);
1886
+
1887
+ const runViteCommand = useCallback(async () => {
1888
+ if (!viteSandboxId || viteConsoleRunning) return;
1889
+ const command = viteConsoleCommand.trim();
1890
+ if (!command) return;
1891
+ setViteError(null);
1892
+ setViteConsoleRunning(true);
1893
+ setSbxBottomTab("console");
1894
+ try {
1895
+ await streamReactLabCommand({ id: viteSandboxId, command }, (message) => {
1896
+ if (message.type === "output") {
1897
+ setViteConsoleOutput((prev) => [
1898
+ ...prev,
1899
+ { kind: message.kind, text: message.text, source: "server" },
1900
+ ]);
1901
+ } else if (message.type === "error") {
1902
+ setViteConsoleOutput((prev) => [
1903
+ ...prev,
1904
+ { kind: "stderr", text: message.error, source: "server" },
1905
+ ]);
1906
+ }
1907
+ });
1908
+ // After any npm command that mutates dependencies, read package.json back
1909
+ // from disk so the editor reflects what npm actually wrote.
1910
+ const tokens = command.trim().split(/\s+/);
1911
+ const sub = tokens[1];
1912
+ if (
1913
+ sub &&
1914
+ /^(install|i|uninstall|un|remove|rm|r|update|up|upgrade|add)$/.test(sub)
1915
+ ) {
1916
+ const updated = await readReactLabFile(viteSandboxId, "package.json");
1917
+ if (updated !== null) {
1918
+ setReactFiles((prev) => ({ ...prev, "package.json": updated }));
1919
+ }
1920
+ }
1921
+ } catch (err: any) {
1922
+ setViteError(err?.message ?? String(err));
1923
+ } finally {
1924
+ setViteConsoleRunning(false);
1925
+ }
1926
+ }, [viteConsoleCommand, viteConsoleRunning, viteSandboxId]);
1795
1927
 
1796
1928
  const handleClientTypeChange = useCallback(
1797
1929
  (ct: FrontendClientType) => {
@@ -1808,6 +1940,10 @@ export default function CodeRunnerModal() {
1808
1940
  current === "console" || current === "inspector" ? "output" : current,
1809
1941
  );
1810
1942
  }
1943
+ if (ct !== "react") {
1944
+ setViteConsoleOutput([]);
1945
+ setViteConsoleRunning(false);
1946
+ }
1811
1947
  if (ct !== "script") {
1812
1948
  const defs = defaultForType(ct);
1813
1949
  setReactFiles(defs.files);
@@ -2111,6 +2247,10 @@ export default function CodeRunnerModal() {
2111
2247
  const isActiveModuleFederationGeneratedFile =
2112
2248
  clientType === "module-federation" &&
2113
2249
  moduleFederationGeneratedFileSet.has(reactActiveFile);
2250
+ const usesClientExplorer =
2251
+ clientType === "react" ||
2252
+ clientType === "nextjs" ||
2253
+ clientType === "module-federation";
2114
2254
  const mfInspectorRuntimeCount = new Set(
2115
2255
  mfInspectorEvents.map((event) => event.runtimeId),
2116
2256
  ).size;
@@ -2398,6 +2538,161 @@ export default function CodeRunnerModal() {
2398
2538
  const mfInspectorIssueCount =
2399
2539
  mfInspectorDifferentRemoteCount + mfInspectorErroredRemoteCount;
2400
2540
  const mfInspectorViewCopy = MF_INSPECTOR_VIEW_COPY[mfInspectorView];
2541
+ const mfInspectorWorkspacePackageMap = new Map<
2542
+ string,
2543
+ {
2544
+ apps: string[];
2545
+ requiredVersions: Array<{ app: string; version: string | false | null }>;
2546
+ }
2547
+ >();
2548
+
2549
+ for (const [filePath, fileContents] of Object.entries(reactFiles)) {
2550
+ const match = filePath.match(/^apps\/([^/]+)\/package\.json$/);
2551
+ if (!match) continue;
2552
+
2553
+ try {
2554
+ const packageJson = JSON.parse(fileContents);
2555
+ const appName = match[1];
2556
+ const dependencyVersions = asInspectorRecord(packageJson.dependencies);
2557
+
2558
+ for (const [packageName, versionValue] of Object.entries(
2559
+ dependencyVersions,
2560
+ )) {
2561
+ const existing = mfInspectorWorkspacePackageMap.get(packageName) ?? {
2562
+ apps: [],
2563
+ requiredVersions: [],
2564
+ };
2565
+
2566
+ if (!existing.apps.includes(appName)) {
2567
+ existing.apps.push(appName);
2568
+ }
2569
+ existing.requiredVersions.push({
2570
+ app: appName,
2571
+ version: typeof versionValue === "string" ? versionValue : null,
2572
+ });
2573
+ mfInspectorWorkspacePackageMap.set(packageName, existing);
2574
+ }
2575
+ } catch {
2576
+ // Ignore malformed package.json edits while the user is typing.
2577
+ }
2578
+ }
2579
+
2580
+ const mfInspectorDeclaredSharedPackageMap = new Map<
2581
+ string,
2582
+ {
2583
+ apps: string[];
2584
+ requiredVersions: Array<{ app: string; version: string | false | null }>;
2585
+ singletonPreferred: boolean;
2586
+ }
2587
+ >();
2588
+
2589
+ for (const [packageName, packageInfo] of mfInspectorWorkspacePackageMap) {
2590
+ mfInspectorDeclaredSharedPackageMap.set(packageName, {
2591
+ apps: [...packageInfo.apps],
2592
+ requiredVersions: [...packageInfo.requiredVersions],
2593
+ singletonPreferred: false,
2594
+ });
2595
+ }
2596
+
2597
+ for (const { app, declaredConfig } of mfInspectorDeclaredConfigs) {
2598
+ const sharedPackages = asInspectorRecord(declaredConfig.shared);
2599
+ for (const packageName of Object.keys(sharedPackages)) {
2600
+ const packageConfig = asInspectorRecord(sharedPackages[packageName]);
2601
+ const existing = mfInspectorDeclaredSharedPackageMap.get(packageName) ?? {
2602
+ apps: [],
2603
+ requiredVersions: [],
2604
+ singletonPreferred: false,
2605
+ };
2606
+
2607
+ if (!existing.apps.includes(app)) {
2608
+ existing.apps.push(app);
2609
+ }
2610
+ existing.requiredVersions.push({
2611
+ app,
2612
+ version:
2613
+ typeof packageConfig.requiredVersion === "string"
2614
+ ? packageConfig.requiredVersion
2615
+ : packageConfig.requiredVersion === false
2616
+ ? false
2617
+ : null,
2618
+ });
2619
+ existing.singletonPreferred =
2620
+ existing.singletonPreferred || packageConfig.singleton === true;
2621
+ mfInspectorDeclaredSharedPackageMap.set(packageName, existing);
2622
+ }
2623
+ }
2624
+
2625
+ const mfInspectorShareScopeNames = Array.from(
2626
+ new Set([
2627
+ ...Object.keys(mfShareScopeForMatrix?.shareScopes ?? {}).filter(
2628
+ (scopeName) => scopeName !== "__error",
2629
+ ),
2630
+ ...(mfInspectorDeclaredSharedPackageMap.size > 0 ? ["default"] : []),
2631
+ ]),
2632
+ );
2633
+ const mfInspectorRuntimeSharedPackageMap = new Map<
2634
+ string,
2635
+ { registeredVersionCount: number; loaded: boolean }
2636
+ >();
2637
+
2638
+ for (const [scopeName, scopeValue] of Object.entries(
2639
+ mfShareScopeForMatrix?.shareScopes ?? {},
2640
+ )) {
2641
+ if (scopeName === "__error") continue;
2642
+ for (const [packageName, versionValue] of Object.entries(
2643
+ asInspectorRecord(scopeValue),
2644
+ )) {
2645
+ const versionEntries = Array.isArray(versionValue) ? versionValue : [];
2646
+ const existing = mfInspectorRuntimeSharedPackageMap.get(packageName) ?? {
2647
+ registeredVersionCount: 0,
2648
+ loaded: false,
2649
+ };
2650
+ existing.registeredVersionCount = Math.max(
2651
+ existing.registeredVersionCount,
2652
+ versionEntries.length,
2653
+ );
2654
+ existing.loaded =
2655
+ existing.loaded ||
2656
+ versionEntries.some(
2657
+ (entry) => asInspectorRecord(entry).loaded === true,
2658
+ );
2659
+ mfInspectorRuntimeSharedPackageMap.set(packageName, existing);
2660
+ }
2661
+ }
2662
+
2663
+ const mfInspectorSharedPackagesByApp = new Map<
2664
+ string,
2665
+ Array<{
2666
+ packageName: string;
2667
+ singletonPreferred: boolean;
2668
+ runtimeRegistered: boolean;
2669
+ runtimeLoaded: boolean;
2670
+ }>
2671
+ >();
2672
+
2673
+ for (const [
2674
+ packageName,
2675
+ packageInfo,
2676
+ ] of mfInspectorDeclaredSharedPackageMap) {
2677
+ for (const appName of packageInfo.apps) {
2678
+ const appPackages = mfInspectorSharedPackagesByApp.get(appName) ?? [];
2679
+ const runtimePackageInfo =
2680
+ mfInspectorRuntimeSharedPackageMap.get(packageName) ?? null;
2681
+ appPackages.push({
2682
+ packageName,
2683
+ singletonPreferred: packageInfo.singletonPreferred,
2684
+ runtimeRegistered: runtimePackageInfo != null,
2685
+ runtimeLoaded: runtimePackageInfo?.loaded === true,
2686
+ });
2687
+ mfInspectorSharedPackagesByApp.set(appName, appPackages);
2688
+ }
2689
+ }
2690
+
2691
+ for (const packages of mfInspectorSharedPackagesByApp.values()) {
2692
+ packages.sort((left, right) =>
2693
+ left.packageName.localeCompare(right.packageName),
2694
+ );
2695
+ }
2401
2696
  const mfInspectorSummaryCards = [
2402
2697
  {
2403
2698
  label: "Host page",
@@ -2442,6 +2737,64 @@ export default function CodeRunnerModal() {
2442
2737
  },
2443
2738
  ];
2444
2739
 
2740
+ const getMfInspectorAppSharedPackages = (appName: string) =>
2741
+ mfInspectorSharedPackagesByApp.get(appName) ?? [];
2742
+
2743
+ const getMfInspectorAppSharedPackageSummary = (appName: string) => {
2744
+ const packages = getMfInspectorAppSharedPackages(appName);
2745
+ const previewItems = packages.slice(0, 2).map((entry) => entry.packageName);
2746
+ const hiddenCount = packages.length - previewItems.length;
2747
+
2748
+ return {
2749
+ count: packages.length,
2750
+ countLabel:
2751
+ packages.length === 1
2752
+ ? "1 shared package"
2753
+ : `${packages.length} shared packages`,
2754
+ previewText:
2755
+ packages.length === 0
2756
+ ? "none declared"
2757
+ : previewItems.join(", ") +
2758
+ (hiddenCount > 0 ? ` +${hiddenCount}` : ""),
2759
+ };
2760
+ };
2761
+
2762
+ const renderMfInspectorPackageBadges = (appName: string) => {
2763
+ const packages = getMfInspectorAppSharedPackages(appName);
2764
+
2765
+ if (packages.length === 0) {
2766
+ return <span className="text-slate-600">none declared</span>;
2767
+ }
2768
+
2769
+ return (
2770
+ <div className="flex flex-wrap gap-1">
2771
+ {packages.map((entry) => {
2772
+ const toneClass = entry.runtimeLoaded
2773
+ ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-200"
2774
+ : entry.runtimeRegistered
2775
+ ? "border-cyan-500/30 bg-cyan-500/10 text-cyan-200"
2776
+ : "border-slate-700 bg-slate-800/50 text-slate-300";
2777
+ return (
2778
+ <span
2779
+ key={`${appName}-${entry.packageName}`}
2780
+ className={`rounded border px-1.5 py-0.5 text-[10px] font-mono ${toneClass}`}
2781
+ title={
2782
+ entry.runtimeLoaded
2783
+ ? "Seen and loaded at runtime"
2784
+ : entry.runtimeRegistered
2785
+ ? "Seen in webpack runtime registry"
2786
+ : "Declared in package.json but not yet seen at runtime"
2787
+ }
2788
+ >
2789
+ {entry.packageName}
2790
+ {entry.singletonPreferred ? " · one copy" : ""}
2791
+ </span>
2792
+ );
2793
+ })}
2794
+ </div>
2795
+ );
2796
+ };
2797
+
2445
2798
  const renderMfInspectorTruthCell = (
2446
2799
  sameInstance: boolean | null,
2447
2800
  error: string | null,
@@ -3239,7 +3592,7 @@ export default function CodeRunnerModal() {
3239
3592
  </button>
3240
3593
  </>
3241
3594
  )}
3242
- {/* React/Next mode: optional URL + Preview button + edit/preview toggle for Next */}
3595
+ {/* React/Next/Webpack mode controls */}
3243
3596
  {(clientType === "react" ||
3244
3597
  clientType === "nextjs" ||
3245
3598
  clientType === "module-federation") && (
@@ -3252,17 +3605,69 @@ export default function CodeRunnerModal() {
3252
3605
  {sandboxUrl}
3253
3606
  </span>
3254
3607
  )}
3255
- {/* React mode: simple preview button */}
3608
+ {/* React mode: Run Vite OR edit/preview toggle */}
3256
3609
  {clientType === "react" && (
3257
- <button
3258
- type="button"
3259
- onClick={() => refreshPreview()}
3260
- className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 hover:bg-cyan-600/40 text-cyan-400 transition-colors shrink-0"
3261
- title="Render preview"
3262
- >
3263
- <Eye className="w-3 h-3" />
3264
- Preview
3265
- </button>
3610
+ <>
3611
+ {!viteSandboxUrl ? (
3612
+ <button
3613
+ type="button"
3614
+ onClick={() => void startViteServer()}
3615
+ disabled={viteStarting}
3616
+ className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 hover:bg-cyan-600/40 text-cyan-400 disabled:opacity-50 transition-colors shrink-0"
3617
+ title="Install dependencies and start Vite dev server"
3618
+ >
3619
+ {viteStarting ? (
3620
+ <Loader2 className="w-3 h-3 animate-spin" />
3621
+ ) : (
3622
+ <Play className="w-3 h-3" />
3623
+ )}
3624
+ {viteStarting ? "Starting…" : "Run Vite"}
3625
+ </button>
3626
+ ) : (
3627
+ <>
3628
+ <div className="flex items-center rounded overflow-hidden border border-slate-700/50 text-[9px] shrink-0">
3629
+ <button
3630
+ type="button"
3631
+ onClick={() => setReactClientTab("edit")}
3632
+ className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3633
+ reactClientTab === "edit"
3634
+ ? "bg-slate-700 text-slate-200"
3635
+ : "text-slate-500 hover:text-slate-400"
3636
+ }`}
3637
+ title="Edit code"
3638
+ >
3639
+ <Code2 className="w-2.5 h-2.5" />
3640
+ </button>
3641
+ <button
3642
+ type="button"
3643
+ onClick={() => setReactClientTab("preview")}
3644
+ className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3645
+ reactClientTab === "preview"
3646
+ ? "bg-slate-700 text-slate-200"
3647
+ : "text-slate-500 hover:text-slate-400"
3648
+ }`}
3649
+ title="Live preview"
3650
+ >
3651
+ <Eye className="w-2.5 h-2.5" />
3652
+ </button>
3653
+ </div>
3654
+ <button
3655
+ type="button"
3656
+ onClick={() => {
3657
+ if (viteSandboxId)
3658
+ void stopReactLabSandbox(viteSandboxId);
3659
+ setViteSandboxId(null);
3660
+ setViteSandboxUrl(null);
3661
+ setReactClientTab("edit");
3662
+ }}
3663
+ className="p-0.5 rounded text-slate-600 hover:text-red-400 transition-colors shrink-0"
3664
+ title="Stop Vite lab"
3665
+ >
3666
+ <StopCircle className="w-3 h-3" />
3667
+ </button>
3668
+ </>
3669
+ )}
3670
+ </>
3266
3671
  )}
3267
3672
  {/* Next.js mode: start real server OR edit/preview toggle */}
3268
3673
  {clientType === "nextjs" && (
@@ -3383,129 +3788,12 @@ export default function CodeRunnerModal() {
3383
3788
  )}
3384
3789
  </div>
3385
3790
 
3386
- {/* File tabs row (React only — Next.js uses the tree sidebar) */}
3387
- {clientType === "react" && (
3388
- <div className="flex items-center gap-0.5 px-2 py-1 bg-slate-800/40 border-b border-slate-700 shrink-0 overflow-x-auto">
3389
- {Object.keys(reactFiles).map((fname) => (
3390
- <button
3391
- key={fname}
3392
- type="button"
3393
- onClick={() => {
3394
- setReactActiveFile(fname);
3395
- setReactClientTab("edit");
3396
- }}
3397
- className={`flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-mono whitespace-nowrap transition-colors ${
3398
- fname === reactActiveFile && reactClientTab === "edit"
3399
- ? "bg-slate-900 text-slate-200 border border-slate-600"
3400
- : "text-slate-500 hover:text-slate-300 hover:bg-slate-800/50"
3401
- }`}
3402
- >
3403
- {fname.includes("/") ? fname.split("/").pop() : fname}
3404
- <span
3405
- role="button"
3406
- onClick={(e) => {
3407
- e.stopPropagation();
3408
- if (Object.keys(reactFiles).length <= 1) return;
3409
- const remaining = Object.keys(reactFiles).filter(
3410
- (f) => f !== fname,
3411
- );
3412
- setReactFiles((prev) => {
3413
- const next = { ...prev };
3414
- delete next[fname];
3415
- return next;
3416
- });
3417
- if (reactActiveFile === fname)
3418
- setReactActiveFile(remaining[0] ?? "");
3419
- }}
3420
- className="w-3 h-3 flex items-center justify-center text-slate-600 hover:text-red-400 rounded transition-colors"
3421
- title="Delete file"
3422
- >
3423
- <X className="w-2.5 h-2.5" />
3424
- </span>
3425
- </button>
3426
- ))}
3427
- {/* Add new file */}
3428
- {reactAddingFile ? (
3429
- <input
3430
- autoFocus
3431
- value={reactNewFileName}
3432
- onChange={(e) => setReactNewFileName(e.target.value)}
3433
- onBlur={() => {
3434
- setReactAddingFile(false);
3435
- setReactNewFileName("");
3436
- }}
3437
- onKeyDown={(e) => {
3438
- if (e.key === "Enter") {
3439
- e.preventDefault();
3440
- const name = reactNewFileName.trim();
3441
- if (name && !reactFiles[name]) {
3442
- setReactFiles((prev) => ({
3443
- ...prev,
3444
- [name]: newFileContent(name),
3445
- }));
3446
- setReactActiveFile(name);
3447
- setReactClientTab("edit");
3448
- }
3449
- setReactAddingFile(false);
3450
- setReactNewFileName("");
3451
- } else if (e.key === "Escape") {
3452
- setReactAddingFile(false);
3453
- setReactNewFileName("");
3454
- }
3455
- }}
3456
- placeholder="filename.tsx"
3457
- className="w-28 bg-slate-900 border border-cyan-600/50 rounded px-1.5 py-0.5 text-[10px] font-mono text-slate-200 placeholder-slate-600 outline-none focus:border-cyan-500"
3458
- />
3459
- ) : (
3460
- <button
3461
- type="button"
3462
- onClick={() => setReactAddingFile(true)}
3463
- className="p-0.5 rounded text-slate-600 hover:text-cyan-400 transition-colors shrink-0"
3464
- title="New file"
3465
- >
3466
- <FilePlus className="w-3 h-3" />
3467
- </button>
3468
- )}
3469
- {/* Edit / Preview tab toggle */}
3470
- <div className="ml-auto flex items-center rounded overflow-hidden border border-slate-700/50 text-[9px] shrink-0">
3471
- <button
3472
- type="button"
3473
- onClick={() => setReactClientTab("edit")}
3474
- className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3475
- reactClientTab === "edit"
3476
- ? "bg-slate-700 text-slate-200"
3477
- : "text-slate-500 hover:text-slate-400"
3478
- }`}
3479
- title="Edit code"
3480
- >
3481
- <Code2 className="w-2.5 h-2.5" />
3482
- </button>
3483
- <button
3484
- type="button"
3485
- onClick={() => {
3486
- if (!reactPreviewSrc) refreshPreview();
3487
- else setReactClientTab("preview");
3488
- }}
3489
- className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
3490
- reactClientTab === "preview"
3491
- ? "bg-slate-700 text-slate-200"
3492
- : "text-slate-500 hover:text-slate-400"
3493
- }`}
3494
- title="Live preview"
3495
- >
3496
- <Eye className="w-2.5 h-2.5" />
3497
- </button>
3498
- </div>
3499
- </div>
3500
- )}
3501
-
3502
3791
  {/* Client body */}
3503
3792
  <div
3504
- className={`flex-1 min-h-0 ${clientType === "nextjs" || clientType === "module-federation" ? "flex flex-row" : "relative"}`}
3793
+ className={`flex-1 min-h-0 ${usesClientExplorer ? "flex flex-row" : "relative"}`}
3505
3794
  >
3506
- {/* ── Next.js VS Code-style file tree sidebar ── */}
3507
- {(clientType === "nextjs" ||
3508
- clientType === "module-federation") && (
3795
+ {/* ── VS Code-style file tree sidebar ── */}
3796
+ {usesClientExplorer && (
3509
3797
  <div className="w-36 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto">
3510
3798
  {/* Sidebar header */}
3511
3799
  <div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-700/60">
@@ -3546,7 +3834,9 @@ export default function CodeRunnerModal() {
3546
3834
  placeholder={
3547
3835
  clientType === "module-federation"
3548
3836
  ? "apps/orders/src/App.jsx"
3549
- : "app/new.tsx"
3837
+ : clientType === "nextjs"
3838
+ ? "app/new.tsx"
3839
+ : "components/NewWidget.tsx"
3550
3840
  }
3551
3841
  className="w-full bg-slate-800 border border-cyan-600/50 rounded px-1 py-0.5 text-[9px] font-mono text-slate-200 placeholder-slate-600 outline-none focus:border-cyan-500"
3552
3842
  />
@@ -3558,7 +3848,9 @@ export default function CodeRunnerModal() {
3558
3848
  title={
3559
3849
  clientType === "module-federation"
3560
3850
  ? "New file (use paths like apps/orders/src/App.jsx)"
3561
- : "New file (use paths like app/dashboard/page.tsx)"
3851
+ : clientType === "nextjs"
3852
+ ? "New file (use paths like app/dashboard/page.tsx)"
3853
+ : "New file (use paths like components/Button.tsx)"
3562
3854
  }
3563
3855
  >
3564
3856
  <FilePlus className="w-3 h-3" />
@@ -3751,7 +4043,7 @@ export default function CodeRunnerModal() {
3751
4043
 
3752
4044
  {/* ── Editor / Preview area ── */}
3753
4045
  <div
3754
- className={`${clientType === "nextjs" || clientType === "module-federation" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
4046
+ className={`${usesClientExplorer ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
3755
4047
  >
3756
4048
  {clientType === "script" ? (
3757
4049
  <SyntaxEditor
@@ -4047,23 +4339,29 @@ export default function CodeRunnerModal() {
4047
4339
  </div>
4048
4340
  )}
4049
4341
  {((clientType === "module-federation" && mfError) ||
4050
- (clientType !== "module-federation" && nxError)) && (
4342
+ (clientType === "nextjs" && nxError) ||
4343
+ (clientType === "react" && viteError)) && (
4051
4344
  <div className="text-[10px] text-red-400 bg-red-950/40 border-b border-red-800 px-3 py-1.5 shrink-0 font-mono">
4052
4345
  {clientType === "module-federation"
4053
4346
  ? mfError
4054
- : nxError}
4347
+ : clientType === "react"
4348
+ ? viteError
4349
+ : nxError}
4055
4350
  </div>
4056
4351
  )}
4057
- {(nxStarting || mfStarting) && (
4352
+ {(nxStarting || mfStarting || viteStarting) && (
4058
4353
  <div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950">
4059
4354
  <Loader2 className="w-8 h-8 animate-spin text-cyan-400" />
4060
4355
  <p className="text-[12px]">
4061
4356
  {clientType === "module-federation"
4062
4357
  ? "Installing dependencies and starting webpack apps…"
4063
- : "Starting Next.js dev server…"}
4358
+ : clientType === "react"
4359
+ ? "Installing dependencies and starting Vite…"
4360
+ : "Starting Next.js dev server…"}
4064
4361
  </p>
4065
4362
  <p className="text-[10px] text-slate-600 max-w-md text-center px-4">
4066
- {clientType === "module-federation"
4363
+ {clientType === "module-federation" ||
4364
+ clientType === "react"
4067
4365
  ? "The first run can take a little longer because npm install runs inside the lab sandbox."
4068
4366
  : "This takes ~10 seconds on the first run"}
4069
4367
  </p>
@@ -4129,14 +4427,47 @@ export default function CodeRunnerModal() {
4129
4427
  </p>
4130
4428
  </div>
4131
4429
  )}
4132
- {!nxStarting && clientType === "react" && (
4133
- <iframe
4134
- srcDoc={reactPreviewSrc ?? ""}
4135
- sandbox="allow-scripts"
4136
- className="flex-1 min-h-0 w-full border-0 bg-white"
4137
- title="React Preview"
4138
- />
4139
- )}
4430
+ {!viteStarting &&
4431
+ clientType === "react" &&
4432
+ viteSandboxUrl && (
4433
+ <iframe
4434
+ ref={viteIframeRef}
4435
+ src={viteSandboxUrl}
4436
+ className="flex-1 min-h-0 w-full border-0 bg-white"
4437
+ style={
4438
+ isDraggingResize
4439
+ ? { pointerEvents: "none" }
4440
+ : undefined
4441
+ }
4442
+ title="React Lab Preview"
4443
+ />
4444
+ )}
4445
+ {!viteStarting &&
4446
+ clientType === "react" &&
4447
+ !viteSandboxUrl && (
4448
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950 px-6 text-center">
4449
+ <Server className="w-8 h-8 text-cyan-400/70" />
4450
+ <p className="text-[12px]">
4451
+ Click{" "}
4452
+ <span className="text-cyan-400 font-medium">
4453
+ Run Vite
4454
+ </span>{" "}
4455
+ to install dependencies and start the dev
4456
+ server.
4457
+ </p>
4458
+ <p className="text-[10px] text-slate-600 max-w-md">
4459
+ Edit{" "}
4460
+ <code className="text-slate-400">
4461
+ package.json
4462
+ </code>{" "}
4463
+ to add packages, then use the Console tab to run{" "}
4464
+ <code className="text-slate-400">
4465
+ npm install
4466
+ </code>
4467
+ .
4468
+ </p>
4469
+ </div>
4470
+ )}
4140
4471
  </div>
4141
4472
  )}
4142
4473
  </div>
@@ -4183,7 +4514,8 @@ export default function CodeRunnerModal() {
4183
4514
  ) : null}
4184
4515
  Output
4185
4516
  </button>
4186
- {clientType === "module-federation" && (
4517
+ {(clientType === "module-federation" ||
4518
+ clientType === "react") && (
4187
4519
  <button
4188
4520
  type="button"
4189
4521
  onClick={() => setSbxBottomTab("console")}
@@ -4231,9 +4563,10 @@ export default function CodeRunnerModal() {
4231
4563
  (serverStarting || clientRunning) && (
4232
4564
  <Loader2 className="w-3 h-3 text-emerald-400 animate-spin mr-1" />
4233
4565
  )}
4234
- {sbxBottomTab === "console" && mfConsoleRunning && (
4235
- <Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
4236
- )}
4566
+ {sbxBottomTab === "console" &&
4567
+ (mfConsoleRunning || viteConsoleRunning) && (
4568
+ <Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
4569
+ )}
4237
4570
  {sbxBottomTab === "inspector" && mfInspectorEvents.length > 0 && (
4238
4571
  <div className="flex items-center gap-1 mr-1">
4239
4572
  <button
@@ -4282,30 +4615,39 @@ export default function CodeRunnerModal() {
4282
4615
  </button>
4283
4616
  </div>
4284
4617
  )}
4285
- {sbxBottomTab === "console" && mfConsoleOutput.length > 0 && (
4286
- <div className="flex items-center gap-1 mr-1">
4287
- <button
4288
- type="button"
4289
- onClick={() =>
4290
- navigator.clipboard.writeText(
4291
- mfConsoleOutput.map((line) => line.text).join("\n"),
4292
- )
4293
- }
4294
- className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4295
- title="Copy console output"
4296
- >
4297
- <Copy className="w-3 h-3" />
4298
- </button>
4299
- <button
4300
- type="button"
4301
- onClick={() => setMfConsoleOutput([])}
4302
- className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4303
- title="Clear console output"
4304
- >
4305
- <Trash2 className="w-3 h-3" />
4306
- </button>
4307
- </div>
4308
- )}
4618
+ {sbxBottomTab === "console" &&
4619
+ (mfConsoleOutput.length > 0 ||
4620
+ viteConsoleOutput.length > 0) && (
4621
+ <div className="flex items-center gap-1 mr-1">
4622
+ <button
4623
+ type="button"
4624
+ onClick={() => {
4625
+ const out =
4626
+ clientType === "react"
4627
+ ? viteConsoleOutput
4628
+ : mfConsoleOutput;
4629
+ navigator.clipboard.writeText(
4630
+ out.map((line) => line.text).join("\n"),
4631
+ );
4632
+ }}
4633
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4634
+ title="Copy console output"
4635
+ >
4636
+ <Copy className="w-3 h-3" />
4637
+ </button>
4638
+ <button
4639
+ type="button"
4640
+ onClick={() => {
4641
+ if (clientType === "react") setViteConsoleOutput([]);
4642
+ else setMfConsoleOutput([]);
4643
+ }}
4644
+ className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
4645
+ title="Clear console output"
4646
+ >
4647
+ <Trash2 className="w-3 h-3" />
4648
+ </button>
4649
+ </div>
4650
+ )}
4309
4651
  {sbxBottomTab === "chat" && sbxChatMessages.length > 0 && (
4310
4652
  <button
4311
4653
  type="button"
@@ -4338,7 +4680,11 @@ export default function CodeRunnerModal() {
4338
4680
  <span className="text-slate-600">
4339
4681
  {clientType === "module-federation"
4340
4682
  ? "Run webpack to start the host and remotes"
4341
- : "Start the server, then run the client"}
4683
+ : clientType === "nextjs"
4684
+ ? "Start Next.js to launch the live preview"
4685
+ : clientType === "react"
4686
+ ? "Click Run Vite to start the dev server"
4687
+ : "Start the server, then run the client"}
4342
4688
  </span>
4343
4689
  )}
4344
4690
  {sandboxOutput.map((line, i) => (
@@ -4526,9 +4872,11 @@ export default function CodeRunnerModal() {
4526
4872
  const remoteY = 156;
4527
4873
  const nodeW = 96;
4528
4874
  const nodeH = 46;
4529
- const svgH = 220;
4875
+ const svgH = 250;
4530
4876
  const hostBooted = mfInspectorBootMap.has("host");
4531
4877
  const hostData = mfInspectorBootMap.get("host");
4878
+ const hostPackageSummary =
4879
+ getMfInspectorAppSharedPackageSummary("host");
4532
4880
 
4533
4881
  const getConnectionStatus = (
4534
4882
  remoteName: string,
@@ -4716,6 +5064,30 @@ export default function CodeRunnerModal() {
4716
5064
  {"▣ DOM " + hostData.reactDomVersion}
4717
5065
  </text>
4718
5066
  )}
5067
+ {hostPackageSummary.count > 0 && (
5068
+ <>
5069
+ <text
5070
+ x={W / 2}
5071
+ y={hostY + nodeH / 2 + 16}
5072
+ textAnchor="middle"
5073
+ fill="#cbd5e1"
5074
+ fontSize={8}
5075
+ fontFamily="ui-sans-serif, sans-serif"
5076
+ >
5077
+ {hostPackageSummary.countLabel}
5078
+ </text>
5079
+ <text
5080
+ x={W / 2}
5081
+ y={hostY + nodeH / 2 + 27}
5082
+ textAnchor="middle"
5083
+ fill="#64748b"
5084
+ fontSize={7.5}
5085
+ fontFamily="ui-monospace, monospace"
5086
+ >
5087
+ {hostPackageSummary.previewText}
5088
+ </text>
5089
+ </>
5090
+ )}
4719
5091
 
4720
5092
  {/* REMOTE nodes */}
4721
5093
  {displayRemotes.map((remoteName, i) => {
@@ -4723,6 +5095,10 @@ export default function CodeRunnerModal() {
4723
5095
  const status = isPlaceholder
4724
5096
  ? "idle"
4725
5097
  : getConnectionStatus(remoteName);
5098
+ const packageSummary =
5099
+ getMfInspectorAppSharedPackageSummary(
5100
+ remoteName,
5101
+ );
4726
5102
  const remoteData =
4727
5103
  mfInspectorBootMap.get(remoteName);
4728
5104
  const keys =
@@ -4958,6 +5334,30 @@ export default function CodeRunnerModal() {
4958
5334
  )}
4959
5335
  </g>
4960
5336
  )}
5337
+ {packageSummary.count > 0 && (
5338
+ <g>
5339
+ <text
5340
+ x={rx}
5341
+ y={remoteY + nodeH / 2 + 26}
5342
+ textAnchor="middle"
5343
+ fill="#cbd5e1"
5344
+ fontSize={8}
5345
+ fontFamily="ui-sans-serif, sans-serif"
5346
+ >
5347
+ {packageSummary.countLabel}
5348
+ </text>
5349
+ <text
5350
+ x={rx}
5351
+ y={remoteY + nodeH / 2 + 37}
5352
+ textAnchor="middle"
5353
+ fill="#64748b"
5354
+ fontSize={7.5}
5355
+ fontFamily="ui-monospace, monospace"
5356
+ >
5357
+ {packageSummary.previewText}
5358
+ </text>
5359
+ </g>
5360
+ )}
4961
5361
  </g>
4962
5362
  );
4963
5363
  })}
@@ -5036,7 +5436,9 @@ export default function CodeRunnerModal() {
5036
5436
  <div className="text-[11px] text-slate-500 mb-2 leading-relaxed">
5037
5437
  "Same copy" is the healthy result for React and
5038
5438
  ReactDOM. "Different copy" often leads to hook,
5039
- context, or shared-state bugs.
5439
+ context, or shared-state bugs. The package list
5440
+ column reflects the current app package.json
5441
+ files.
5040
5442
  </div>
5041
5443
  <div className="rounded-lg border border-slate-800 overflow-hidden">
5042
5444
  <table className="w-full text-[11px]">
@@ -5051,6 +5453,9 @@ export default function CodeRunnerModal() {
5051
5453
  <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5052
5454
  DOM copy
5053
5455
  </th>
5456
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5457
+ Current shared packages
5458
+ </th>
5054
5459
  <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5055
5460
  Version seen
5056
5461
  </th>
@@ -5066,6 +5471,10 @@ export default function CodeRunnerModal() {
5066
5471
  mfInspectorIdentityResultMap.get(
5067
5472
  remoteKey,
5068
5473
  ) ?? null;
5474
+ const remoteAppName =
5475
+ result?.remoteApp ??
5476
+ remoteKey.split("/")[0] ??
5477
+ "unknown";
5069
5478
  const loadInfo =
5070
5479
  mfInspectorRemoteLoadMap.get(
5071
5480
  remoteKey,
@@ -5092,6 +5501,11 @@ export default function CodeRunnerModal() {
5092
5501
  loadInfo?.status ?? null,
5093
5502
  )}
5094
5503
  </td>
5504
+ <td className="px-3 py-2 align-top">
5505
+ {renderMfInspectorPackageBadges(
5506
+ remoteAppName,
5507
+ )}
5508
+ </td>
5095
5509
  <td className="px-3 py-2 font-mono text-slate-400 text-[10px]">
5096
5510
  {result?.reactVersion ??
5097
5511
  (loadInfo?.status ===
@@ -5257,13 +5671,13 @@ export default function CodeRunnerModal() {
5257
5671
  <>
5258
5672
  <div className="rounded-lg border border-slate-800 bg-slate-900/50 px-3 py-2.5 mb-3">
5259
5673
  <div className="text-[12px] font-semibold text-slate-100">
5260
- Live shared package list
5674
+ Shared package picture
5261
5675
  </div>
5262
5676
  <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.
5677
+ This tab merges two sources: the packages found in
5678
+ the current app package.json files in the editor,
5679
+ and the packages webpack has actually registered
5680
+ in its live runtime share scope.
5267
5681
  </p>
5268
5682
  <p className="mt-1 text-[10px] text-slate-500">
5269
5683
  Snapshot source:{" "}
@@ -5276,168 +5690,239 @@ export default function CodeRunnerModal() {
5276
5690
  )}
5277
5691
  </p>
5278
5692
  </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>
5693
+ {mfInspectorShareScopeNames.map((scopeName) => {
5694
+ const scopeValue =
5695
+ scopeName === "default"
5696
+ ? asInspectorRecord(
5697
+ mfShareScopeForMatrix.shareScopes.default,
5698
+ )
5699
+ : asInspectorRecord(
5700
+ mfShareScopeForMatrix.shareScopes[
5701
+ scopeName
5702
+ ],
5703
+ );
5704
+ const packageNames = Array.from(
5705
+ new Set([
5706
+ ...Object.keys(scopeValue),
5707
+ ...(scopeName === "default"
5708
+ ? Array.from(
5709
+ mfInspectorDeclaredSharedPackageMap.keys(),
5710
+ )
5711
+ : []),
5712
+ ]),
5713
+ ).sort((left, right) => left.localeCompare(right));
5714
+
5715
+ if (packageNames.length === 0) return null;
5716
+
5717
+ return (
5718
+ <div key={scopeName} className="mb-4">
5719
+ <div className="text-[10px] text-slate-500 uppercase tracking-wider mb-2 font-mono">
5720
+ sharing group: {scopeName}
5721
+ </div>
5722
+ <div className="rounded-lg border border-slate-800 overflow-hidden">
5723
+ <table className="w-full text-[11px]">
5724
+ <thead>
5725
+ <tr className="bg-slate-900/80 border-b border-slate-800">
5726
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium w-32">
5727
+ Package
5728
+ </th>
5729
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium w-56">
5730
+ Declared by apps
5731
+ </th>
5732
+ <th className="text-left px-3 py-1.5 text-slate-400 font-medium">
5733
+ Versions webpack can reuse
5734
+ </th>
5735
+ </tr>
5736
+ </thead>
5737
+ <tbody>
5738
+ {packageNames.map((pkgName) => {
5739
+ const versionValue =
5740
+ scopeValue[pkgName];
5741
+ const versionEntries = Array.isArray(
5742
+ versionValue,
5743
+ )
5744
+ ? versionValue
5745
+ : [];
5746
+ const declaredInfo =
5747
+ mfInspectorDeclaredSharedPackageMap.get(
5748
+ pkgName,
5749
+ ) ?? null;
5750
+ const declaredSingleton =
5751
+ declaredInfo?.singletonPreferred ===
5752
+ true;
5753
+ const declaredVersionSummary =
5754
+ declaredInfo?.requiredVersions
5755
+ .filter(
5756
+ ({ version }) => version !== null,
5757
+ )
5758
+ .map(({ app, version }) =>
5759
+ version === false
5760
+ ? `${app}: any`
5761
+ : `${app}: ${version}`,
5762
+ ) ?? [];
5763
+ const loadedCount =
5764
+ versionEntries.filter(
5765
+ (ve) =>
5766
+ asInspectorRecord(ve).loaded ===
5767
+ true,
5768
+ ).length;
5769
+ return (
5770
+ <tr
5771
+ key={pkgName}
5772
+ className="border-b border-slate-800/50 last:border-0"
5773
+ >
5774
+ <td className="px-3 py-2.5 align-top">
5775
+ <div className="font-mono text-slate-100 font-medium">
5776
+ {pkgName}
5777
+ </div>
5778
+ <div className="flex flex-wrap gap-1 mt-1">
5779
+ {declaredSingleton && (
5780
+ <span className="rounded border border-amber-500/30 bg-amber-500/10 px-1 py-0.5 text-[9px] text-amber-300">
5781
+ one copy preferred
5782
+ </span>
5783
+ )}
5784
+ {declaredInfo ? (
5785
+ <span className="rounded border border-cyan-500/30 bg-cyan-500/10 px-1 py-0.5 text-[9px] text-cyan-300">
5786
+ from current package.json
5787
+ </span>
5788
+ ) : (
5789
+ <span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-500">
5790
+ runtime only
5791
+ </span>
5792
+ )}
5793
+ <span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-400">
5794
+ {loadedCount}/
5795
+ {versionEntries.length}{" "}
5796
+ already used
5797
+ </span>
5798
+ </div>
5799
+ </td>
5800
+ <td className="px-3 py-2.5 align-top">
5801
+ {declaredInfo ? (
5802
+ <div>
5803
+ <div className="flex flex-wrap gap-1">
5804
+ {declaredInfo.apps.map(
5805
+ (appName) => (
5806
+ <span
5807
+ key={appName}
5808
+ className="rounded border border-slate-700 bg-slate-800/50 px-1.5 py-0.5 text-[9px] text-slate-300 font-mono"
5809
+ >
5810
+ {appName}
5811
+ </span>
5812
+ ),
5344
5813
  )}
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
5814
  </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] ${
5815
+ {declaredVersionSummary.length >
5816
+ 0 && (
5817
+ <div className="mt-1 text-[9px] text-slate-500 leading-relaxed">
5818
+ required versions:{" "}
5819
+ {declaredVersionSummary.join(
5820
+ ", ",
5821
+ )}
5822
+ </div>
5823
+ )}
5824
+ </div>
5825
+ ) : (
5826
+ <span className="text-slate-600">
5827
+ not declared in current app
5828
+ config
5829
+ </span>
5830
+ )}
5831
+ </td>
5832
+ <td className="px-3 py-2.5">
5833
+ <div className="flex flex-wrap gap-2">
5834
+ {versionEntries.length === 0 ? (
5835
+ declaredInfo ? (
5836
+ <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]">
5837
+ Declared for sharing, but
5838
+ webpack has not put it in
5839
+ the live runtime registry
5840
+ yet.
5841
+ </div>
5842
+ ) : (
5843
+ <span className="text-slate-600">
5844
+ none yet
5845
+ </span>
5846
+ )
5847
+ ) : (
5848
+ versionEntries.map(
5849
+ (ve, vi) => {
5850
+ const vr =
5851
+ asInspectorRecord(ve);
5852
+ const version =
5853
+ typeof vr.version ===
5854
+ "string"
5855
+ ? vr.version
5856
+ : "?";
5857
+ const from =
5858
+ typeof vr.from ===
5859
+ "string"
5860
+ ? vr.from
5861
+ : null;
5862
+ const loaded =
5863
+ vr.loaded === true;
5864
+ const eager =
5865
+ vr.eager === true;
5866
+ return (
5867
+ <div
5868
+ key={vi}
5869
+ className={`rounded-lg border px-2 py-1.5 font-mono text-[10px] ${
5870
+ loaded
5871
+ ? "border-emerald-500/30 bg-emerald-500/5"
5872
+ : "border-slate-700 bg-slate-800/40"
5873
+ }`}
5874
+ >
5875
+ <div
5876
+ className={`font-semibold ${loaded ? "text-emerald-200" : "text-slate-400"}`}
5877
+ >
5878
+ {version}
5879
+ </div>
5880
+ {from && (
5881
+ <div className="text-slate-500 mt-0.5 text-[9px]">
5882
+ registered by{" "}
5883
+ {from}
5884
+ </div>
5885
+ )}
5886
+ <div className="flex gap-1 mt-1">
5887
+ <span
5888
+ className={`rounded px-1 py-0.5 text-[8px] ${
5384
5889
  loaded
5385
- ? "border-emerald-500/30 bg-emerald-500/5"
5386
- : "border-slate-700 bg-slate-800/40"
5890
+ ? "bg-emerald-500/20 text-emerald-300"
5891
+ : "bg-slate-700 text-slate-500"
5387
5892
  }`}
5388
5893
  >
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>
5894
+ {loaded
5895
+ ? "in use"
5896
+ : "registered"}
5897
+ </span>
5898
+ <span
5899
+ className={`rounded px-1 py-0.5 text-[8px] ${
5900
+ eager
5901
+ ? "bg-amber-500/20 text-amber-300"
5902
+ : "bg-slate-800 text-slate-600"
5903
+ }`}
5904
+ >
5905
+ {eager
5906
+ ? "load now"
5907
+ : "load later"}
5908
+ </span>
5909
+ </div>
5910
+ </div>
5911
+ );
5912
+ },
5913
+ )
5914
+ )}
5915
+ </div>
5916
+ </td>
5917
+ </tr>
5918
+ );
5919
+ })}
5920
+ </tbody>
5921
+ </table>
5438
5922
  </div>
5439
- );
5440
- })}
5923
+ </div>
5924
+ );
5925
+ })}
5441
5926
  {typeof mfShareScopeForMatrix.shareScopes.__error ===
5442
5927
  "string" && (
5443
5928
  <div className="rounded border border-red-500/20 bg-red-500/10 px-3 py-2 text-[11px] text-red-200">
@@ -5533,90 +6018,143 @@ export default function CodeRunnerModal() {
5533
6018
  {sbxBottomTab === "console" && (
5534
6019
  <div className="flex-1 min-h-0 flex flex-col">
5535
6020
  <div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">
5536
- <select
5537
- value={mfConsoleCwd}
5538
- onChange={(e) => setMfConsoleCwd(e.target.value)}
5539
- disabled={!mfSandboxId || mfConsoleRunning}
5540
- className="bg-slate-950 border border-slate-700 rounded px-2 py-1 text-[11px] font-mono text-slate-200 outline-none disabled:opacity-50"
5541
- >
5542
- {moduleFederationCommandRoots.map((root) => (
5543
- <option key={root} value={root}>
5544
- {root === "." ? "root" : root}
5545
- </option>
5546
- ))}
5547
- </select>
6021
+ {clientType === "module-federation" && (
6022
+ <select
6023
+ value={mfConsoleCwd}
6024
+ onChange={(e) => setMfConsoleCwd(e.target.value)}
6025
+ disabled={!mfSandboxId || mfConsoleRunning}
6026
+ className="bg-slate-950 border border-slate-700 rounded px-2 py-1 text-[11px] font-mono text-slate-200 outline-none disabled:opacity-50"
6027
+ >
6028
+ {moduleFederationCommandRoots.map((root) => (
6029
+ <option key={root} value={root}>
6030
+ {root === "." ? "root" : root}
6031
+ </option>
6032
+ ))}
6033
+ </select>
6034
+ )}
5548
6035
  <input
5549
- value={mfConsoleCommand}
5550
- onChange={(e) => setMfConsoleCommand(e.target.value)}
6036
+ value={
6037
+ clientType === "react"
6038
+ ? viteConsoleCommand
6039
+ : mfConsoleCommand
6040
+ }
6041
+ onChange={(e) =>
6042
+ clientType === "react"
6043
+ ? setViteConsoleCommand(e.target.value)
6044
+ : setMfConsoleCommand(e.target.value)
6045
+ }
5551
6046
  onKeyDown={(e) => {
5552
6047
  if (e.key === "Enter") {
5553
6048
  e.preventDefault();
5554
- void runModuleFederationCommand();
6049
+ if (clientType === "react") void runViteCommand();
6050
+ else void runModuleFederationCommand();
5555
6051
  }
5556
6052
  }}
5557
- disabled={!mfSandboxId || mfConsoleRunning}
5558
- placeholder="npm run build"
6053
+ disabled={
6054
+ clientType === "react"
6055
+ ? !viteSandboxId || viteConsoleRunning
6056
+ : !mfSandboxId || mfConsoleRunning
6057
+ }
6058
+ placeholder={
6059
+ clientType === "react"
6060
+ ? "npm install <package>"
6061
+ : "npm run build"
6062
+ }
5559
6063
  className="flex-1 bg-slate-950 border border-slate-700 rounded px-2 py-1 text-[11px] font-mono text-slate-200 placeholder-slate-600 outline-none disabled:opacity-50"
5560
6064
  spellCheck={false}
5561
6065
  />
5562
6066
  <button
5563
6067
  type="button"
5564
- onClick={() => void runModuleFederationCommand()}
6068
+ onClick={() => {
6069
+ if (clientType === "react") void runViteCommand();
6070
+ else void runModuleFederationCommand();
6071
+ }}
5565
6072
  disabled={
5566
- !mfSandboxId ||
5567
- mfConsoleRunning ||
5568
- !mfConsoleCommand.trim()
6073
+ clientType === "react"
6074
+ ? !viteSandboxId ||
6075
+ viteConsoleRunning ||
6076
+ !viteConsoleCommand.trim()
6077
+ : !mfSandboxId ||
6078
+ mfConsoleRunning ||
6079
+ !mfConsoleCommand.trim()
5569
6080
  }
5570
6081
  className="flex items-center gap-1 px-2.5 py-1 rounded text-[11px] font-medium bg-cyan-600/20 hover:bg-cyan-600/35 text-cyan-300 disabled:opacity-50 transition-colors shrink-0"
5571
6082
  >
5572
- {mfConsoleRunning ? (
6083
+ {(
6084
+ clientType === "react"
6085
+ ? viteConsoleRunning
6086
+ : mfConsoleRunning
6087
+ ) ? (
5573
6088
  <Loader2 className="w-3 h-3 animate-spin" />
5574
6089
  ) : (
5575
6090
  <Play className="w-3 h-3" />
5576
6091
  )}
5577
6092
  Run
5578
6093
  </button>
5579
- <button
5580
- type="button"
5581
- onClick={() => void refreshModuleFederationGeneratedFiles()}
5582
- disabled={!mfSandboxId || mfConsoleRunning}
5583
- className="px-2 py-1 rounded text-[10px] font-medium text-slate-400 hover:text-slate-200 hover:bg-slate-800 disabled:opacity-50 transition-colors shrink-0"
5584
- >
5585
- Refresh dist
5586
- </button>
6094
+ {clientType === "module-federation" && (
6095
+ <button
6096
+ type="button"
6097
+ onClick={() =>
6098
+ void refreshModuleFederationGeneratedFiles()
6099
+ }
6100
+ disabled={!mfSandboxId || mfConsoleRunning}
6101
+ className="px-2 py-1 rounded text-[10px] font-medium text-slate-400 hover:text-slate-200 hover:bg-slate-800 disabled:opacity-50 transition-colors shrink-0"
6102
+ >
6103
+ Refresh dist
6104
+ </button>
6105
+ )}
5587
6106
  </div>
5588
6107
  <div className="shrink-0 px-3 py-1.5 text-[10px] text-slate-500 border-b border-slate-800">
5589
- Run npm scripts in the selected webpack app. Generated dist
5590
- files appear in the explorer as read-only artifacts.
6108
+ {clientType === "react"
6109
+ ? "Run npm commands in the React lab e.g. npm install lodash. Vite HMR reloads automatically."
6110
+ : "Run npm scripts in the selected webpack app. Generated dist files appear in the explorer as read-only artifacts."}
5591
6111
  </div>
5592
6112
  <div className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[12px] leading-relaxed">
5593
- {mfConsoleOutput.length === 0 && !mfConsoleRunning && (
5594
- <span className="text-slate-600">
5595
- {mfSandboxId
5596
- ? "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
5597
- : "Start webpack first, then run npm commands here."}
5598
- </span>
5599
- )}
5600
- {mfConsoleOutput.map((line, index) => (
5601
- <div key={index} className="flex items-start gap-2">
5602
- <span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
5603
- cmd
5604
- </span>
5605
- <span
5606
- className={
5607
- line.kind === "stderr"
5608
- ? "text-red-400 whitespace-pre-wrap"
5609
- : line.kind === "warn"
5610
- ? "text-amber-400 whitespace-pre-wrap"
5611
- : line.kind === "info"
5612
- ? "text-slate-500 italic whitespace-pre-wrap"
5613
- : "text-slate-200 whitespace-pre-wrap"
5614
- }
5615
- >
5616
- {line.text}
5617
- </span>
5618
- </div>
5619
- ))}
6113
+ {(() => {
6114
+ const out =
6115
+ clientType === "react"
6116
+ ? viteConsoleOutput
6117
+ : mfConsoleOutput;
6118
+ const running =
6119
+ clientType === "react"
6120
+ ? viteConsoleRunning
6121
+ : mfConsoleRunning;
6122
+ const sandboxActive =
6123
+ clientType === "react" ? !!viteSandboxId : !!mfSandboxId;
6124
+ if (out.length === 0 && !running) {
6125
+ return (
6126
+ <span className="text-slate-600">
6127
+ {sandboxActive
6128
+ ? clientType === "react"
6129
+ ? "Run npm install <pkg> to add a package."
6130
+ : "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
6131
+ : clientType === "react"
6132
+ ? "Start Vite first, then run npm commands here."
6133
+ : "Start webpack first, then run npm commands here."}
6134
+ </span>
6135
+ );
6136
+ }
6137
+ return out.map((line, index) => (
6138
+ <div key={index} className="flex items-start gap-2">
6139
+ <span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
6140
+ cmd
6141
+ </span>
6142
+ <span
6143
+ className={
6144
+ line.kind === "stderr"
6145
+ ? "text-red-400 whitespace-pre-wrap"
6146
+ : line.kind === "warn"
6147
+ ? "text-amber-400 whitespace-pre-wrap"
6148
+ : line.kind === "info"
6149
+ ? "text-slate-500 italic whitespace-pre-wrap"
6150
+ : "text-slate-200 whitespace-pre-wrap"
6151
+ }
6152
+ >
6153
+ {line.text}
6154
+ </span>
6155
+ </div>
6156
+ ));
6157
+ })()}
5620
6158
  <div ref={mfConsoleEndRef} />
5621
6159
  </div>
5622
6160
  </div>