create-interview-cockpit 0.6.0 → 0.7.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.
@@ -43,10 +43,15 @@ import {
43
43
  generatePreviewHTML,
44
44
  defaultForType,
45
45
  resolveNextjsEntry,
46
+ type FrontendLabType,
46
47
  } from "../reactLab";
47
48
  import {
49
+ fetchModuleFederationStatus,
50
+ startModuleFederationSandbox,
48
51
  startNextjsSandbox,
52
+ stopModuleFederationSandbox,
49
53
  updateNextjsFiles,
54
+ updateModuleFederationFiles,
50
55
  stopNextjsSandbox,
51
56
  } from "../api";
52
57
  import ReactMarkdown from "react-markdown";
@@ -67,6 +72,7 @@ interface OutputLine {
67
72
 
68
73
  const LANG_OPTIONS = ["typescript", "javascript"] as const;
69
74
  type Lang = (typeof LANG_OPTIONS)[number];
75
+ type FrontendClientType = "script" | FrontendLabType;
70
76
 
71
77
  // ── Sandbox default snippets ─────────────────────────────────────────
72
78
  const DEFAULT_SERVER_CODE = `import express from 'express';
@@ -202,6 +208,49 @@ function SyntaxEditor({
202
208
  );
203
209
  }
204
210
 
211
+ interface FileTreeNode {
212
+ name: string;
213
+ path: string;
214
+ children: FileTreeNode[];
215
+ files: string[];
216
+ }
217
+
218
+ function buildFileTree(paths: string[]): FileTreeNode {
219
+ const root: FileTreeNode = {
220
+ name: "",
221
+ path: "",
222
+ children: [],
223
+ files: [],
224
+ };
225
+
226
+ for (const filePath of paths) {
227
+ const parts = filePath.split("/");
228
+ let node = root;
229
+
230
+ for (let index = 0; index < parts.length - 1; index += 1) {
231
+ const name = parts[index];
232
+ const path = parts.slice(0, index + 1).join("/");
233
+ let child = node.children.find((entry) => entry.path === path);
234
+
235
+ if (!child) {
236
+ child = {
237
+ name,
238
+ path,
239
+ children: [],
240
+ files: [],
241
+ };
242
+ node.children.push(child);
243
+ }
244
+
245
+ node = child;
246
+ }
247
+
248
+ node.files.push(filePath);
249
+ }
250
+
251
+ return root;
252
+ }
253
+
205
254
  export default function CodeRunnerModal() {
206
255
  const {
207
256
  closeCodeRunner,
@@ -246,9 +295,7 @@ export default function CodeRunnerModal() {
246
295
  const [clientRunning, setClientRunning] = useState(false);
247
296
 
248
297
  // ── React/Next.js client state ──────────────────────────────
249
- const [clientType, setClientType] = useState<"script" | "react" | "nextjs">(
250
- "script",
251
- );
298
+ const [clientType, setClientType] = useState<FrontendClientType>("script");
252
299
  const [reactFiles, setReactFiles] = useState<Record<string, string>>({});
253
300
  const [reactActiveFile, setReactActiveFile] = useState<string>("");
254
301
  const [reactClientTab, setReactClientTab] = useState<"edit" | "preview">(
@@ -267,6 +314,12 @@ export default function CodeRunnerModal() {
267
314
  const [nxStarting, setNxStarting] = useState(false);
268
315
  const [nxError, setNxError] = useState<string | null>(null);
269
316
  const nxIframeRef = useRef<HTMLIFrameElement>(null);
317
+ const [mfSandboxId, setMfSandboxId] = useState<string | null>(null);
318
+ const [mfHostUrl, setMfHostUrl] = useState<string | null>(null);
319
+ const [mfAppUrls, setMfAppUrls] = useState<Record<string, string>>({});
320
+ const [mfStarting, setMfStarting] = useState(false);
321
+ const [mfError, setMfError] = useState<string | null>(null);
322
+ const [mfPreviewApp, setMfPreviewApp] = useState("host");
270
323
  // Simulated URL bar state for Next.js mode
271
324
  const [reactPreviewPath, setReactPreviewPath] = useState("/");
272
325
  const [reactNavInput, setReactNavInput] = useState("/");
@@ -367,7 +420,9 @@ export default function CodeRunnerModal() {
367
420
  ? "react"
368
421
  : clientType === "nextjs"
369
422
  ? "nextjs"
370
- : "sandbox";
423
+ : clientType === "module-federation"
424
+ ? "module-federation"
425
+ : "sandbox";
371
426
  const payload = JSON.stringify(
372
427
  clientType === "script"
373
428
  ? { serverCode, serverLang, clientCode, clientLang }
@@ -474,10 +529,9 @@ export default function CodeRunnerModal() {
474
529
  setActiveSandboxId(runnerInitialSandbox.fileId ?? null);
475
530
  // Restore client type and React/Next.js files
476
531
  const ct =
477
- (runnerInitialSandbox.clientType as "script" | "react" | "nextjs") ??
478
- "script";
532
+ (runnerInitialSandbox.clientType as FrontendClientType) ?? "script";
479
533
  setClientType(ct);
480
- if (ct === "react" || ct === "nextjs") {
534
+ if (ct !== "script") {
481
535
  if (
482
536
  runnerInitialSandbox.reactFiles &&
483
537
  Object.keys(runnerInitialSandbox.reactFiles).length > 0
@@ -495,6 +549,11 @@ export default function CodeRunnerModal() {
495
549
  }
496
550
  setReactPreviewSrc(null);
497
551
  setReactClientTab("edit");
552
+ if (ct === "module-federation") {
553
+ setServerCollapsed(true);
554
+ setClientCollapsed(false);
555
+ setMfPreviewApp("host");
556
+ }
498
557
  }
499
558
  }, [runnerInitialSandbox]);
500
559
 
@@ -979,6 +1038,66 @@ export default function CodeRunnerModal() {
979
1038
  }
980
1039
  }, [nxStarting, reactFiles]);
981
1040
 
1041
+ const startModuleFederationServer = useCallback(async () => {
1042
+ if (mfStarting) return;
1043
+ setMfStarting(true);
1044
+ setMfError(null);
1045
+ setSbxBottomTab("output");
1046
+ setSandboxOutput([
1047
+ {
1048
+ kind: "info",
1049
+ text: "Installing dependencies and starting webpack apps…",
1050
+ source: "server",
1051
+ },
1052
+ ]);
1053
+ try {
1054
+ const info = await startModuleFederationSandbox(reactFiles);
1055
+ setMfSandboxId(info.id);
1056
+ setMfHostUrl(info.hostUrl);
1057
+ setMfAppUrls(info.appUrls);
1058
+ setMfPreviewApp(
1059
+ info.appUrls.host ? "host" : (Object.keys(info.appUrls)[0] ?? "host"),
1060
+ );
1061
+ setReactClientTab("preview");
1062
+ setServerCollapsed(true);
1063
+ setClientCollapsed(false);
1064
+ setSandboxOutput((prev) => [
1065
+ ...prev,
1066
+ {
1067
+ kind: "info",
1068
+ text: `✓ Webpack host running at ${info.hostUrl}`,
1069
+ source: "server",
1070
+ },
1071
+ ]);
1072
+ } catch (err: any) {
1073
+ const message = err?.message ?? String(err);
1074
+ setMfError(message);
1075
+ setSandboxOutput((prev) => [
1076
+ ...prev,
1077
+ { kind: "stderr", text: message, source: "server" },
1078
+ ]);
1079
+ } finally {
1080
+ setMfStarting(false);
1081
+ }
1082
+ }, [mfStarting, reactFiles]);
1083
+
1084
+ const stopModuleFederationServer = useCallback(async () => {
1085
+ if (!mfSandboxId) return;
1086
+ await stopModuleFederationSandbox(mfSandboxId).catch(() => {});
1087
+ setMfSandboxId(null);
1088
+ setMfHostUrl(null);
1089
+ setMfAppUrls({});
1090
+ setMfError(null);
1091
+ setSandboxOutput((prev) => [
1092
+ ...prev,
1093
+ {
1094
+ kind: "info",
1095
+ text: "Webpack module federation lab stopped.",
1096
+ source: "server",
1097
+ },
1098
+ ]);
1099
+ }, [mfSandboxId]);
1100
+
982
1101
  /** Push updated files to the running Next.js server (HMR picks them up). */
983
1102
  const pushNextjsFiles = useCallback(
984
1103
  async (files: Record<string, string>) => {
@@ -992,6 +1111,18 @@ export default function CodeRunnerModal() {
992
1111
  [nxSandboxId],
993
1112
  );
994
1113
 
1114
+ const pushModuleFederationFiles = useCallback(
1115
+ async (files: Record<string, string>) => {
1116
+ if (!mfSandboxId) return;
1117
+ try {
1118
+ await updateModuleFederationFiles(mfSandboxId, files);
1119
+ } catch (err: any) {
1120
+ setMfError(err?.message ?? String(err));
1121
+ }
1122
+ },
1123
+ [mfSandboxId],
1124
+ );
1125
+
995
1126
  // Auto-push file changes to the running Next.js server
996
1127
  const nxFilesRef = useRef(reactFiles);
997
1128
  useEffect(() => {
@@ -1000,6 +1131,55 @@ export default function CodeRunnerModal() {
1000
1131
  void pushNextjsFiles(reactFiles);
1001
1132
  }, [reactFiles, nxSandboxId, pushNextjsFiles]);
1002
1133
 
1134
+ const mfFilesRef = useRef(reactFiles);
1135
+ useEffect(() => {
1136
+ if (!mfSandboxId || reactFiles === mfFilesRef.current) return;
1137
+ mfFilesRef.current = reactFiles;
1138
+ void pushModuleFederationFiles(reactFiles);
1139
+ }, [reactFiles, mfSandboxId, pushModuleFederationFiles]);
1140
+
1141
+ useEffect(() => {
1142
+ if (!mfSandboxId) return;
1143
+ const interval = setInterval(async () => {
1144
+ try {
1145
+ const status = await fetchModuleFederationStatus(mfSandboxId);
1146
+ if (!status.running) {
1147
+ setMfSandboxId(null);
1148
+ setMfHostUrl(null);
1149
+ setMfAppUrls({});
1150
+ setMfError(null);
1151
+ return;
1152
+ }
1153
+ if (status.hostUrl) setMfHostUrl(status.hostUrl);
1154
+ if (status.appUrls) {
1155
+ setMfAppUrls(status.appUrls);
1156
+ setMfPreviewApp((prev) =>
1157
+ status.appUrls?.[prev]
1158
+ ? prev
1159
+ : (Object.keys(status.appUrls ?? {})[0] ?? "host"),
1160
+ );
1161
+ }
1162
+ if (status.logs) {
1163
+ setSandboxOutput(
1164
+ status.logs.flatMap((chunk) =>
1165
+ chunk
1166
+ .split("\n")
1167
+ .filter(Boolean)
1168
+ .map((text) => ({
1169
+ kind: "stdout" as const,
1170
+ text,
1171
+ source: "server" as const,
1172
+ })),
1173
+ ),
1174
+ );
1175
+ }
1176
+ } catch {
1177
+ /* ignore transient network errors */
1178
+ }
1179
+ }, 1000);
1180
+ return () => clearInterval(interval);
1181
+ }, [mfSandboxId]);
1182
+
1003
1183
  // Clean up Next.js server when the modal is closed or mode changes away from nextjs
1004
1184
  const prevClientTypeRef = useRef(clientType);
1005
1185
  useEffect(() => {
@@ -1010,21 +1190,32 @@ export default function CodeRunnerModal() {
1010
1190
  setNxSandboxId(null);
1011
1191
  setNxSandboxUrl(null);
1012
1192
  }
1013
- }, [clientType, nxSandboxId]);
1193
+ if (
1194
+ prev === "module-federation" &&
1195
+ clientType !== "module-federation" &&
1196
+ mfSandboxId
1197
+ ) {
1198
+ void stopModuleFederationSandbox(mfSandboxId);
1199
+ setMfSandboxId(null);
1200
+ setMfHostUrl(null);
1201
+ setMfAppUrls({});
1202
+ }
1203
+ }, [clientType, nxSandboxId, mfSandboxId]);
1014
1204
 
1015
1205
  // Clean up on unmount
1016
1206
  useEffect(() => {
1017
1207
  return () => {
1018
1208
  if (nxSandboxId) void stopNextjsSandbox(nxSandboxId);
1209
+ if (mfSandboxId) void stopModuleFederationSandbox(mfSandboxId);
1019
1210
  };
1020
1211
  // eslint-disable-next-line react-hooks/exhaustive-deps
1021
- }, [nxSandboxId]);
1212
+ }, [nxSandboxId, mfSandboxId]);
1022
1213
 
1023
1214
  const handleClientTypeChange = useCallback(
1024
- (ct: "script" | "react" | "nextjs") => {
1215
+ (ct: FrontendClientType) => {
1025
1216
  if (ct === clientType) return;
1026
1217
  setClientType(ct);
1027
- if (ct === "react" || ct === "nextjs") {
1218
+ if (ct !== "script") {
1028
1219
  const defs = defaultForType(ct);
1029
1220
  setReactFiles(defs.files);
1030
1221
  setReactActiveFile(defs.activeFile);
@@ -1036,6 +1227,12 @@ export default function CodeRunnerModal() {
1036
1227
  setReactNavHistory(["/"]);
1037
1228
  setReactNavIndex(0);
1038
1229
  }
1230
+ if (ct === "module-federation") {
1231
+ setServerCollapsed(true);
1232
+ setClientCollapsed(false);
1233
+ setMfPreviewApp("host");
1234
+ setMfError(null);
1235
+ }
1039
1236
  }
1040
1237
  },
1041
1238
  [clientType],
@@ -1061,12 +1258,19 @@ export default function CodeRunnerModal() {
1061
1258
  ...prev,
1062
1259
  { id: aId, role: "assistant", content: "" },
1063
1260
  ]);
1064
- const isReactMode = clientType === "react" || clientType === "nextjs";
1065
- const workspaceFiles = isReactMode
1261
+ const isFrontendMode =
1262
+ clientType === "react" ||
1263
+ clientType === "nextjs" ||
1264
+ clientType === "module-federation";
1265
+ const workspaceFiles = isFrontendMode
1066
1266
  ? reactFiles
1067
1267
  : { "client.js": clientCode, "server.ts": serverCode };
1068
- const labType: "react" | "nextjs" =
1069
- clientType === "nextjs" ? "nextjs" : "react";
1268
+ const labType: FrontendLabType =
1269
+ clientType === "nextjs"
1270
+ ? "nextjs"
1271
+ : clientType === "module-federation"
1272
+ ? "module-federation"
1273
+ : "react";
1070
1274
  try {
1071
1275
  const history = [...sbxChatMessages, userMsg].map((m) => ({
1072
1276
  role: m.role,
@@ -1944,7 +2148,14 @@ export default function CodeRunnerModal() {
1944
2148
  </span>
1945
2149
  {/* Client type selector: JS / React / Next */}
1946
2150
  <div className="flex items-center rounded overflow-hidden border border-slate-700 text-[9px] ml-1 shrink-0">
1947
- {(["script", "react", "nextjs"] as const).map((ct) => (
2151
+ {(
2152
+ [
2153
+ "script",
2154
+ "react",
2155
+ "nextjs",
2156
+ "module-federation",
2157
+ ] as const
2158
+ ).map((ct) => (
1948
2159
  <button
1949
2160
  key={ct}
1950
2161
  type="button"
@@ -1959,7 +2170,9 @@ export default function CodeRunnerModal() {
1959
2170
  ? "JS"
1960
2171
  : ct === "react"
1961
2172
  ? "React"
1962
- : "Next"}
2173
+ : ct === "nextjs"
2174
+ ? "Next"
2175
+ : "Webpack"}
1963
2176
  </button>
1964
2177
  ))}
1965
2178
  </div>
@@ -2023,9 +2236,11 @@ export default function CodeRunnerModal() {
2023
2236
  </>
2024
2237
  )}
2025
2238
  {/* React/Next mode: optional URL + Preview button + edit/preview toggle for Next */}
2026
- {(clientType === "react" || clientType === "nextjs") && (
2239
+ {(clientType === "react" ||
2240
+ clientType === "nextjs" ||
2241
+ clientType === "module-federation") && (
2027
2242
  <>
2028
- {sandboxUrl && (
2243
+ {clientType !== "module-federation" && sandboxUrl && (
2029
2244
  <span
2030
2245
  className="text-[9px] font-mono text-slate-600 truncate max-w-[80px]"
2031
2246
  title={sandboxUrl}
@@ -2037,7 +2252,7 @@ export default function CodeRunnerModal() {
2037
2252
  {clientType === "react" && (
2038
2253
  <button
2039
2254
  type="button"
2040
- onClick={refreshPreview}
2255
+ onClick={() => refreshPreview()}
2041
2256
  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"
2042
2257
  title="Render preview"
2043
2258
  >
@@ -2093,6 +2308,73 @@ export default function CodeRunnerModal() {
2093
2308
  )}
2094
2309
  </>
2095
2310
  )}
2311
+ {clientType === "module-federation" && (
2312
+ <>
2313
+ {mfSandboxId && mfHostUrl && (
2314
+ <span
2315
+ className="text-[9px] font-mono text-slate-600 truncate max-w-[110px]"
2316
+ title={mfHostUrl}
2317
+ >
2318
+ {mfHostUrl.replace(/^https?:\/\//, "")}
2319
+ </span>
2320
+ )}
2321
+ {!mfSandboxId ? (
2322
+ <button
2323
+ type="button"
2324
+ onClick={() => void startModuleFederationServer()}
2325
+ disabled={mfStarting}
2326
+ 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"
2327
+ title="Start real webpack module federation dev servers"
2328
+ >
2329
+ {mfStarting ? (
2330
+ <Loader2 className="w-3 h-3 animate-spin" />
2331
+ ) : (
2332
+ <Play className="w-3 h-3" />
2333
+ )}
2334
+ {mfStarting ? "Starting…" : "Run Webpack"}
2335
+ </button>
2336
+ ) : (
2337
+ <>
2338
+ <div className="flex items-center rounded overflow-hidden border border-slate-700/50 text-[9px] shrink-0">
2339
+ <button
2340
+ type="button"
2341
+ onClick={() => setReactClientTab("edit")}
2342
+ className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
2343
+ reactClientTab === "edit"
2344
+ ? "bg-slate-700 text-slate-200"
2345
+ : "text-slate-500 hover:text-slate-400"
2346
+ }`}
2347
+ title="Edit code"
2348
+ >
2349
+ <Code2 className="w-2.5 h-2.5" />
2350
+ </button>
2351
+ <button
2352
+ type="button"
2353
+ onClick={() => setReactClientTab("preview")}
2354
+ className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
2355
+ reactClientTab === "preview"
2356
+ ? "bg-slate-700 text-slate-200"
2357
+ : "text-slate-500 hover:text-slate-400"
2358
+ }`}
2359
+ title="Live preview"
2360
+ >
2361
+ <Eye className="w-2.5 h-2.5" />
2362
+ </button>
2363
+ </div>
2364
+ <button
2365
+ type="button"
2366
+ onClick={() =>
2367
+ void stopModuleFederationServer()
2368
+ }
2369
+ className="p-0.5 rounded text-slate-600 hover:text-red-400 transition-colors shrink-0"
2370
+ title="Stop webpack lab"
2371
+ >
2372
+ <StopCircle className="w-3 h-3" />
2373
+ </button>
2374
+ </>
2375
+ )}
2376
+ </>
2377
+ )}
2096
2378
  </>
2097
2379
  )}
2098
2380
  </div>
@@ -2215,10 +2497,11 @@ export default function CodeRunnerModal() {
2215
2497
 
2216
2498
  {/* Client body */}
2217
2499
  <div
2218
- className={`flex-1 min-h-0 ${clientType === "nextjs" ? "flex flex-row" : "relative"}`}
2500
+ className={`flex-1 min-h-0 ${clientType === "nextjs" || clientType === "module-federation" ? "flex flex-row" : "relative"}`}
2219
2501
  >
2220
2502
  {/* ── Next.js VS Code-style file tree sidebar ── */}
2221
- {clientType === "nextjs" && (
2503
+ {(clientType === "nextjs" ||
2504
+ clientType === "module-federation") && (
2222
2505
  <div className="w-36 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto">
2223
2506
  {/* Sidebar header */}
2224
2507
  <div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-700/60">
@@ -2256,7 +2539,11 @@ export default function CodeRunnerModal() {
2256
2539
  setReactNewFileName("");
2257
2540
  }
2258
2541
  }}
2259
- placeholder="app/new.tsx"
2542
+ placeholder={
2543
+ clientType === "module-federation"
2544
+ ? "apps/orders/src/App.jsx"
2545
+ : "app/new.tsx"
2546
+ }
2260
2547
  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"
2261
2548
  />
2262
2549
  ) : (
@@ -2264,7 +2551,11 @@ export default function CodeRunnerModal() {
2264
2551
  type="button"
2265
2552
  onClick={() => setReactAddingFile(true)}
2266
2553
  className="p-0.5 rounded text-slate-600 hover:text-cyan-400 transition-colors"
2267
- title="New file (use paths like app/dashboard/page.tsx)"
2554
+ title={
2555
+ clientType === "module-federation"
2556
+ ? "New file (use paths like apps/orders/src/App.jsx)"
2557
+ : "New file (use paths like app/dashboard/page.tsx)"
2558
+ }
2268
2559
  >
2269
2560
  <FilePlus className="w-3 h-3" />
2270
2561
  </button>
@@ -2273,25 +2564,7 @@ export default function CodeRunnerModal() {
2273
2564
  {/* Tree nodes */}
2274
2565
  <div className="flex-1 py-1">
2275
2566
  {(() => {
2276
- // Build a folder → file[] map, plus root-level files
2277
- const allFiles = Object.keys(reactFiles).sort(
2278
- (a, b) => {
2279
- const ad = a.split("/").length;
2280
- const bd = b.split("/").length;
2281
- return ad !== bd ? ad - bd : a.localeCompare(b);
2282
- },
2283
- );
2284
- // Collect unique top-level folders (first path segment for nested files)
2285
- const folders = Array.from(
2286
- new Set(
2287
- allFiles
2288
- .filter((f) => f.includes("/"))
2289
- .map((f) => f.split("/")[0]),
2290
- ),
2291
- ).sort();
2292
- const rootFiles = allFiles.filter(
2293
- (f) => !f.includes("/"),
2294
- );
2567
+ const tree = buildFileTree(Object.keys(reactFiles));
2295
2568
 
2296
2569
  const fileIcon = (name: string) => {
2297
2570
  if (name.endsWith(".tsx") || name.endsWith(".jsx"))
@@ -2359,40 +2632,46 @@ export default function CodeRunnerModal() {
2359
2632
  </div>
2360
2633
  );
2361
2634
 
2362
- const renderFolder = (folder: string) => {
2363
- const isOpen = !collapsedFolders.has(folder);
2364
- const children = allFiles.filter((f) => {
2365
- const parts = f.split("/");
2366
- return parts[0] === folder && parts.length >= 2;
2367
- });
2368
- // Build sub-folder groups within this folder
2369
- const subFolders = Array.from(
2370
- new Set(
2371
- children
2372
- .filter((f) => f.split("/").length > 2)
2373
- .map((f) =>
2374
- f.split("/").slice(0, 2).join("/"),
2375
- ),
2376
- ),
2377
- ).sort();
2378
- const directFiles = children.filter(
2379
- (f) => f.split("/").length === 2,
2380
- );
2635
+ const renderNode = (
2636
+ node: FileTreeNode,
2637
+ indent = 0,
2638
+ ): React.ReactNode => {
2639
+ if (!node.path) {
2640
+ return (
2641
+ <>
2642
+ {node.children
2643
+ .sort((a, b) =>
2644
+ a.name.localeCompare(b.name),
2645
+ )
2646
+ .map((child) => renderNode(child, 0))}
2647
+ {node.files
2648
+ .sort((a, b) => a.localeCompare(b))
2649
+ .map((path) => renderFile(path, 0))}
2650
+ </>
2651
+ );
2652
+ }
2653
+
2654
+ const isOpen = !collapsedFolders.has(node.path);
2381
2655
 
2382
2656
  return (
2383
- <div key={folder}>
2384
- {/* Folder row */}
2657
+ <div key={node.path}>
2385
2658
  <button
2386
2659
  type="button"
2387
2660
  onClick={() =>
2388
2661
  setCollapsedFolders((prev) => {
2389
2662
  const next = new Set(prev);
2390
- if (next.has(folder)) next.delete(folder);
2391
- else next.add(folder);
2663
+ if (next.has(node.path)) {
2664
+ next.delete(node.path);
2665
+ } else {
2666
+ next.add(node.path);
2667
+ }
2392
2668
  return next;
2393
2669
  })
2394
2670
  }
2395
- className="w-full flex items-center gap-0.5 px-2 py-0.5 text-left text-[10px] font-mono text-slate-300 hover:bg-slate-800 transition-colors select-none"
2671
+ style={{
2672
+ paddingLeft: `${8 + indent * 10}px`,
2673
+ }}
2674
+ className="w-full flex items-center gap-0.5 pr-2 py-0.5 text-left text-[10px] font-mono text-slate-300 hover:bg-slate-800 transition-colors select-none"
2396
2675
  >
2397
2676
  {isOpen ? (
2398
2677
  <ChevronDown className="w-2.5 h-2.5 shrink-0 text-slate-500" />
@@ -2402,68 +2681,29 @@ export default function CodeRunnerModal() {
2402
2681
  <span className="text-yellow-300/80 mr-0.5">
2403
2682
  📁
2404
2683
  </span>
2405
- <span className="truncate">{folder}/</span>
2684
+ <span className="truncate">{node.name}/</span>
2406
2685
  </button>
2407
- {/* Children */}
2408
2686
  {isOpen && (
2409
2687
  <div>
2410
- {subFolders.map((sf) => {
2411
- const sfIsOpen =
2412
- !collapsedFolders.has(sf);
2413
- const sfChildren = allFiles.filter(
2414
- (f) =>
2415
- f.startsWith(sf + "/") &&
2416
- f.split("/").length ===
2417
- sf.split("/").length + 1,
2418
- );
2419
- const sfKey = sf.split("/").pop() ?? sf;
2420
- return (
2421
- <div key={sf}>
2422
- <button
2423
- type="button"
2424
- onClick={() =>
2425
- setCollapsedFolders((prev) => {
2426
- const next = new Set(prev);
2427
- if (next.has(sf))
2428
- next.delete(sf);
2429
- else next.add(sf);
2430
- return next;
2431
- })
2432
- }
2433
- className="w-full flex items-center gap-0.5 pl-[18px] pr-2 py-0.5 text-left text-[10px] font-mono text-slate-300 hover:bg-slate-800 transition-colors select-none"
2434
- >
2435
- {sfIsOpen ? (
2436
- <ChevronDown className="w-2.5 h-2.5 shrink-0 text-slate-500" />
2437
- ) : (
2438
- <ChevronRight className="w-2.5 h-2.5 shrink-0 text-slate-500" />
2439
- )}
2440
- <span className="text-yellow-300/80 mr-0.5">
2441
- 📁
2442
- </span>
2443
- <span className="truncate">
2444
- {sfKey}/
2445
- </span>
2446
- </button>
2447
- {sfIsOpen &&
2448
- sfChildren.map((f) =>
2449
- renderFile(f, 3),
2450
- )}
2451
- </div>
2452
- );
2453
- })}
2454
- {directFiles.map((f) => renderFile(f, 1))}
2688
+ {node.children
2689
+ .sort((a, b) =>
2690
+ a.name.localeCompare(b.name),
2691
+ )
2692
+ .map((child) =>
2693
+ renderNode(child, indent + 1),
2694
+ )}
2695
+ {node.files
2696
+ .sort((a, b) => a.localeCompare(b))
2697
+ .map((path) =>
2698
+ renderFile(path, indent + 1),
2699
+ )}
2455
2700
  </div>
2456
2701
  )}
2457
2702
  </div>
2458
2703
  );
2459
2704
  };
2460
2705
 
2461
- return (
2462
- <>
2463
- {folders.map(renderFolder)}
2464
- {rootFiles.map((f) => renderFile(f, 0))}
2465
- </>
2466
- );
2706
+ return renderNode(tree);
2467
2707
  })()}
2468
2708
  </div>
2469
2709
  </div>
@@ -2471,7 +2711,7 @@ export default function CodeRunnerModal() {
2471
2711
 
2472
2712
  {/* ── Editor / Preview area ── */}
2473
2713
  <div
2474
- className={`${clientType === "nextjs" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
2714
+ className={`${clientType === "nextjs" || clientType === "module-federation" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
2475
2715
  >
2476
2716
  {clientType === "script" ? (
2477
2717
  <SyntaxEditor
@@ -2508,11 +2748,9 @@ export default function CodeRunnerModal() {
2508
2748
  placeholder={`// ${reactActiveFile}\n`}
2509
2749
  />
2510
2750
  ) : (
2511
- // Preview area — URL bar for Next.js, plain iframe for React
2512
2751
  <div className="w-full h-full flex flex-col">
2513
2752
  {clientType === "nextjs" && (
2514
2753
  <div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0">
2515
- {/* Back */}
2516
2754
  <button
2517
2755
  type="button"
2518
2756
  disabled={reactNavIndex <= 0}
@@ -2535,7 +2773,6 @@ export default function CodeRunnerModal() {
2535
2773
  >
2536
2774
  <ChevronLeft className="w-3.5 h-3.5" />
2537
2775
  </button>
2538
- {/* Forward */}
2539
2776
  <button
2540
2777
  type="button"
2541
2778
  disabled={
@@ -2560,7 +2797,6 @@ export default function CodeRunnerModal() {
2560
2797
  >
2561
2798
  <ChevronRight className="w-3.5 h-3.5" />
2562
2799
  </button>
2563
- {/* Refresh */}
2564
2800
  <button
2565
2801
  type="button"
2566
2802
  onClick={() => {
@@ -2582,7 +2818,6 @@ export default function CodeRunnerModal() {
2582
2818
  <path d="M13.65 2.35A8 8 0 1 0 15 8h-2a6 6 0 1 1-1.1-3.48L10 6h5V1l-1.35 1.35z" />
2583
2819
  </svg>
2584
2820
  </button>
2585
- {/* URL bar */}
2586
2821
  <form
2587
2822
  className="flex-1 flex items-center gap-1 bg-slate-900 border border-slate-600 rounded px-2 py-0.5 focus-within:border-blue-500/60 transition-colors"
2588
2823
  onSubmit={(e) => {
@@ -2621,7 +2856,6 @@ export default function CodeRunnerModal() {
2621
2856
  spellCheck={false}
2622
2857
  />
2623
2858
  </form>
2624
- {/* Status indicator */}
2625
2859
  {nxSandboxUrl ? (
2626
2860
  <span className="text-[9px] font-mono text-green-400 shrink-0">
2627
2861
  ● live
@@ -2648,49 +2882,103 @@ export default function CodeRunnerModal() {
2648
2882
  )}
2649
2883
  </div>
2650
2884
  )}
2651
- {/* Error banner */}
2652
- {nxError && (
2885
+ {clientType === "module-federation" && (
2886
+ <div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0 overflow-x-auto">
2887
+ {Object.entries(mfAppUrls).map(([name, url]) => (
2888
+ <button
2889
+ key={name}
2890
+ type="button"
2891
+ onClick={() => setMfPreviewApp(name)}
2892
+ className={`px-2 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 ${
2893
+ mfPreviewApp === name
2894
+ ? "bg-slate-700 text-slate-100"
2895
+ : "text-slate-500 hover:text-slate-300 hover:bg-slate-700/40"
2896
+ }`}
2897
+ title={url}
2898
+ >
2899
+ {name}
2900
+ </button>
2901
+ ))}
2902
+ <div className="ml-auto text-[9px] font-mono text-slate-600 shrink-0">
2903
+ {mfAppUrls[mfPreviewApp] ??
2904
+ mfHostUrl ??
2905
+ "Start webpack to preview"}
2906
+ </div>
2907
+ </div>
2908
+ )}
2909
+ {((clientType === "module-federation" && mfError) ||
2910
+ (clientType !== "module-federation" && nxError)) && (
2653
2911
  <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">
2654
- {nxError}
2912
+ {clientType === "module-federation"
2913
+ ? mfError
2914
+ : nxError}
2655
2915
  </div>
2656
2916
  )}
2657
- {/* Starting overlay */}
2658
- {nxStarting && (
2917
+ {(nxStarting || mfStarting) && (
2659
2918
  <div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950">
2660
2919
  <Loader2 className="w-8 h-8 animate-spin text-cyan-400" />
2661
2920
  <p className="text-[12px]">
2662
- Starting Next.js dev server…
2921
+ {clientType === "module-federation"
2922
+ ? "Installing dependencies and starting webpack apps…"
2923
+ : "Starting Next.js dev server…"}
2663
2924
  </p>
2664
- <p className="text-[10px] text-slate-600">
2665
- This takes ~10 seconds on the first run
2925
+ <p className="text-[10px] text-slate-600 max-w-md text-center px-4">
2926
+ {clientType === "module-federation"
2927
+ ? "The first run can take a little longer because npm install runs inside the lab sandbox."
2928
+ : "This takes ~10 seconds on the first run"}
2666
2929
  </p>
2667
2930
  </div>
2668
2931
  )}
2669
- {/* Real Next.js iframe */}
2670
- {!nxStarting && nxSandboxUrl && (
2671
- <iframe
2672
- ref={nxIframeRef}
2673
- src={nxSandboxUrl + reactPreviewPath}
2674
- className="flex-1 min-h-0 w-full border-0 bg-white"
2675
- title="Next.js Preview"
2676
- onLoad={() => {
2677
- // Try to read the iframe path (may be blocked cross-origin)
2678
- try {
2679
- const p =
2680
- nxIframeRef.current?.contentWindow?.location
2681
- .pathname;
2682
- if (p) {
2683
- setReactPreviewPath(p);
2684
- setReactNavInput(p);
2932
+ {!nxStarting &&
2933
+ clientType === "nextjs" &&
2934
+ nxSandboxUrl && (
2935
+ <iframe
2936
+ ref={nxIframeRef}
2937
+ src={nxSandboxUrl + reactPreviewPath}
2938
+ className="flex-1 min-h-0 w-full border-0 bg-white"
2939
+ title="Next.js Preview"
2940
+ onLoad={() => {
2941
+ try {
2942
+ const p =
2943
+ nxIframeRef.current?.contentWindow?.location
2944
+ .pathname;
2945
+ if (p) {
2946
+ setReactPreviewPath(p);
2947
+ setReactNavInput(p);
2948
+ }
2949
+ } catch {
2950
+ // cross-origin — ignore
2685
2951
  }
2686
- } catch {
2687
- // cross-origin — ignore
2688
- }
2689
- }}
2690
- />
2691
- )}
2692
- {/* Simulation iframe (when no real server) */}
2693
- {!nxStarting && !nxSandboxUrl && (
2952
+ }}
2953
+ />
2954
+ )}
2955
+ {!mfStarting &&
2956
+ clientType === "module-federation" &&
2957
+ mfSandboxId &&
2958
+ (mfAppUrls[mfPreviewApp] ?? mfHostUrl) && (
2959
+ <iframe
2960
+ src={mfAppUrls[mfPreviewApp] ?? mfHostUrl ?? ""}
2961
+ className="flex-1 min-h-0 w-full border-0 bg-white"
2962
+ title="Webpack Module Federation Preview"
2963
+ />
2964
+ )}
2965
+ {!mfStarting &&
2966
+ clientType === "module-federation" &&
2967
+ !mfSandboxId && (
2968
+ <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">
2969
+ <Server className="w-8 h-8 text-cyan-400/70" />
2970
+ <p className="text-[12px]">
2971
+ Start the webpack lab to launch the host and
2972
+ remote dev servers.
2973
+ </p>
2974
+ <p className="text-[10px] text-slate-600 max-w-md">
2975
+ The preview lets you switch between the host and
2976
+ each remote so you can inspect their output
2977
+ independently.
2978
+ </p>
2979
+ </div>
2980
+ )}
2981
+ {!nxStarting && clientType === "react" && (
2694
2982
  <iframe
2695
2983
  srcDoc={reactPreviewSrc ?? ""}
2696
2984
  sandbox="allow-scripts"
@@ -2815,7 +3103,9 @@ export default function CodeRunnerModal() {
2815
3103
  !serverStarting &&
2816
3104
  !clientRunning && (
2817
3105
  <span className="text-slate-600">
2818
- Start the server, then run the client
3106
+ {clientType === "module-federation"
3107
+ ? "Run webpack to start the host and remotes"
3108
+ : "Start the server, then run the client"}
2819
3109
  </span>
2820
3110
  )}
2821
3111
  {sandboxOutput.map((line, i) => (
@@ -2864,9 +3154,13 @@ export default function CodeRunnerModal() {
2864
3154
  {sbxChatMessages.length === 0 && (
2865
3155
  <p className="text-xs text-slate-600 pt-1">
2866
3156
  Ask anything about your code —{" "}
2867
- {clientType === "react" || clientType === "nextjs" ? (
3157
+ {clientType === "react" ||
3158
+ clientType === "nextjs" ||
3159
+ clientType === "module-federation" ? (
2868
3160
  <span className="text-slate-500">
2869
- "Why does my useEffect run twice?"
3161
+ {clientType === "module-federation"
3162
+ ? '"Why is remoteEntry.js 404ing?"'
3163
+ : '"Why does my useEffect run twice?"'}
2870
3164
  </span>
2871
3165
  ) : (
2872
3166
  <span className="text-slate-500">
@@ -3002,7 +3296,7 @@ export default function CodeRunnerModal() {
3002
3296
  void handleSbxChatSend();
3003
3297
  }
3004
3298
  }}
3005
- placeholder={`Ask about your ${clientType === "react" ? "React" : clientType === "nextjs" ? "Next.js" : "sandbox"} code…`}
3299
+ placeholder={`Ask about your ${clientType === "react" ? "React" : clientType === "nextjs" ? "Next.js" : clientType === "module-federation" ? "webpack module federation" : "sandbox"} code…`}
3006
3300
  disabled={sbxChatLoading}
3007
3301
  className="flex-1 bg-transparent text-xs text-slate-200 placeholder-slate-600 outline-none resize-none disabled:opacity-50 max-h-20"
3008
3302
  />