create-interview-cockpit 0.7.0 → 0.8.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 +1 -1
- package/template/client/src/api.ts +96 -1
- package/template/client/src/components/CodeRunnerModal.tsx +545 -65
- package/template/client/src/reactLab.ts +38 -8
- package/template/client/tsconfig.tsbuildinfo +30 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/index.ts +376 -0
|
@@ -46,10 +46,13 @@ import {
|
|
|
46
46
|
type FrontendLabType,
|
|
47
47
|
} from "../reactLab";
|
|
48
48
|
import {
|
|
49
|
+
fetchModuleFederationGeneratedFile,
|
|
50
|
+
fetchModuleFederationGeneratedFiles,
|
|
49
51
|
fetchModuleFederationStatus,
|
|
50
52
|
startModuleFederationSandbox,
|
|
51
53
|
startNextjsSandbox,
|
|
52
54
|
stopModuleFederationSandbox,
|
|
55
|
+
streamModuleFederationCommand,
|
|
53
56
|
updateNextjsFiles,
|
|
54
57
|
updateModuleFederationFiles,
|
|
55
58
|
stopNextjsSandbox,
|
|
@@ -251,6 +254,31 @@ function buildFileTree(paths: string[]): FileTreeNode {
|
|
|
251
254
|
return root;
|
|
252
255
|
}
|
|
253
256
|
|
|
257
|
+
function isModuleFederationGeneratedPath(filePath: string): boolean {
|
|
258
|
+
return /(^|\/)dist\//.test(filePath);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getModuleFederationCommandRoots(
|
|
262
|
+
files: Record<string, string>,
|
|
263
|
+
): string[] {
|
|
264
|
+
const roots = new Set<string>(["."]);
|
|
265
|
+
|
|
266
|
+
for (const filePath of Object.keys(files)) {
|
|
267
|
+
const match = filePath.match(/^(apps\/[^/]+)/);
|
|
268
|
+
if (match) {
|
|
269
|
+
roots.add(match[1]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return Array.from(roots).sort((a, b) => {
|
|
274
|
+
if (a === ".") return -1;
|
|
275
|
+
if (b === ".") return 1;
|
|
276
|
+
return a.localeCompare(b);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type SbxBottomTab = "output" | "console" | "chat";
|
|
281
|
+
|
|
254
282
|
export default function CodeRunnerModal() {
|
|
255
283
|
const {
|
|
256
284
|
closeCodeRunner,
|
|
@@ -320,14 +348,23 @@ export default function CodeRunnerModal() {
|
|
|
320
348
|
const [mfStarting, setMfStarting] = useState(false);
|
|
321
349
|
const [mfError, setMfError] = useState<string | null>(null);
|
|
322
350
|
const [mfPreviewApp, setMfPreviewApp] = useState("host");
|
|
351
|
+
const [mfConsoleOutput, setMfConsoleOutput] = useState<OutputLine[]>([]);
|
|
352
|
+
const [mfConsoleCommand, setMfConsoleCommand] = useState("npm run build");
|
|
353
|
+
const [mfConsoleCwd, setMfConsoleCwd] = useState("apps/host");
|
|
354
|
+
const [mfConsoleRunning, setMfConsoleRunning] = useState(false);
|
|
355
|
+
const [mfGeneratedFiles, setMfGeneratedFiles] = useState<string[]>([]);
|
|
356
|
+
const [mfGeneratedFileContents, setMfGeneratedFileContents] = useState<
|
|
357
|
+
Record<string, string>
|
|
358
|
+
>({});
|
|
359
|
+
const [mfLoadingFile, setMfLoadingFile] = useState<string | null>(null);
|
|
323
360
|
// Simulated URL bar state for Next.js mode
|
|
324
361
|
const [reactPreviewPath, setReactPreviewPath] = useState("/");
|
|
325
362
|
const [reactNavInput, setReactNavInput] = useState("/");
|
|
326
363
|
const [reactNavHistory, setReactNavHistory] = useState<string[]>(["/"]);
|
|
327
364
|
const [reactNavIndex, setReactNavIndex] = useState(0);
|
|
328
365
|
|
|
329
|
-
// ── Sandbox output tab ("output" | "chat")
|
|
330
|
-
const [sbxBottomTab, setSbxBottomTab] = useState<
|
|
366
|
+
// ── Sandbox output tab ("output" | "console" | "chat") ─────────────
|
|
367
|
+
const [sbxBottomTab, setSbxBottomTab] = useState<SbxBottomTab>("output");
|
|
331
368
|
|
|
332
369
|
// ── Sandbox panel sizes ─────────────────────────────────────────
|
|
333
370
|
// sbxSplit: server pane width as % of the editor row (0–100)
|
|
@@ -499,7 +536,9 @@ export default function CodeRunnerModal() {
|
|
|
499
536
|
};
|
|
500
537
|
|
|
501
538
|
const outputEndRef = useRef<HTMLDivElement>(null);
|
|
539
|
+
const mfConsoleEndRef = useRef<HTMLDivElement>(null);
|
|
502
540
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
541
|
+
const mfGeneratedFileRequestRef = useRef<string | null>(null);
|
|
503
542
|
// Tracks how many server log lines have already been flushed to sandboxOutput
|
|
504
543
|
const sandboxLogOffsetRef = useRef(0);
|
|
505
544
|
// Stable ref so unmount cleanup can stop sandbox without stale closure
|
|
@@ -553,6 +592,12 @@ export default function CodeRunnerModal() {
|
|
|
553
592
|
setServerCollapsed(true);
|
|
554
593
|
setClientCollapsed(false);
|
|
555
594
|
setMfPreviewApp("host");
|
|
595
|
+
setMfConsoleCommand("npm run build");
|
|
596
|
+
setMfConsoleCwd("apps/host");
|
|
597
|
+
setMfConsoleOutput([]);
|
|
598
|
+
setMfGeneratedFiles([]);
|
|
599
|
+
setMfGeneratedFileContents({});
|
|
600
|
+
setMfLoadingFile(null);
|
|
556
601
|
}
|
|
557
602
|
}
|
|
558
603
|
}, [runnerInitialSandbox]);
|
|
@@ -564,6 +609,20 @@ export default function CodeRunnerModal() {
|
|
|
564
609
|
outputEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
565
610
|
}, [output]);
|
|
566
611
|
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
if (sbxBottomTab === "console") {
|
|
614
|
+
mfConsoleEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
615
|
+
}
|
|
616
|
+
}, [mfConsoleOutput, mfConsoleRunning, sbxBottomTab]);
|
|
617
|
+
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
if (clientType !== "module-federation") return;
|
|
620
|
+
const roots = getModuleFederationCommandRoots(reactFiles);
|
|
621
|
+
if (!roots.includes(mfConsoleCwd)) {
|
|
622
|
+
setMfConsoleCwd(roots.find((root) => root !== ".") ?? ".");
|
|
623
|
+
}
|
|
624
|
+
}, [clientType, reactFiles, mfConsoleCwd]);
|
|
625
|
+
|
|
567
626
|
// ── Sandbox divider drag handlers ────────────────────────────────
|
|
568
627
|
useEffect(() => {
|
|
569
628
|
const onMove = (e: MouseEvent) => {
|
|
@@ -1038,10 +1097,51 @@ export default function CodeRunnerModal() {
|
|
|
1038
1097
|
}
|
|
1039
1098
|
}, [nxStarting, reactFiles]);
|
|
1040
1099
|
|
|
1100
|
+
const refreshModuleFederationGeneratedFiles = useCallback(
|
|
1101
|
+
async (sandboxId: string | null = mfSandboxId): Promise<string[]> => {
|
|
1102
|
+
if (!sandboxId) {
|
|
1103
|
+
setMfGeneratedFiles([]);
|
|
1104
|
+
setMfGeneratedFileContents({});
|
|
1105
|
+
setMfLoadingFile(null);
|
|
1106
|
+
return [];
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
try {
|
|
1110
|
+
const files = await fetchModuleFederationGeneratedFiles(sandboxId);
|
|
1111
|
+
setMfGeneratedFiles(files);
|
|
1112
|
+
setMfGeneratedFileContents((prev) =>
|
|
1113
|
+
Object.fromEntries(
|
|
1114
|
+
Object.entries(prev).filter(([filePath]) =>
|
|
1115
|
+
files.includes(filePath),
|
|
1116
|
+
),
|
|
1117
|
+
),
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
if (
|
|
1121
|
+
reactActiveFile &&
|
|
1122
|
+
!Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile) &&
|
|
1123
|
+
!files.includes(reactActiveFile)
|
|
1124
|
+
) {
|
|
1125
|
+
setReactActiveFile(Object.keys(reactFiles)[0] ?? files[0] ?? "");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return files;
|
|
1129
|
+
} catch (err: any) {
|
|
1130
|
+
setMfError(err?.message ?? String(err));
|
|
1131
|
+
return [];
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
[mfSandboxId, reactActiveFile, reactFiles],
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1041
1137
|
const startModuleFederationServer = useCallback(async () => {
|
|
1042
1138
|
if (mfStarting) return;
|
|
1043
1139
|
setMfStarting(true);
|
|
1044
1140
|
setMfError(null);
|
|
1141
|
+
setMfGeneratedFiles([]);
|
|
1142
|
+
setMfGeneratedFileContents({});
|
|
1143
|
+
setMfLoadingFile(null);
|
|
1144
|
+
setMfConsoleOutput([]);
|
|
1045
1145
|
setSbxBottomTab("output");
|
|
1046
1146
|
setSandboxOutput([
|
|
1047
1147
|
{
|
|
@@ -1061,6 +1161,7 @@ export default function CodeRunnerModal() {
|
|
|
1061
1161
|
setReactClientTab("preview");
|
|
1062
1162
|
setServerCollapsed(true);
|
|
1063
1163
|
setClientCollapsed(false);
|
|
1164
|
+
void refreshModuleFederationGeneratedFiles(info.id);
|
|
1064
1165
|
setSandboxOutput((prev) => [
|
|
1065
1166
|
...prev,
|
|
1066
1167
|
{
|
|
@@ -1079,7 +1180,7 @@ export default function CodeRunnerModal() {
|
|
|
1079
1180
|
} finally {
|
|
1080
1181
|
setMfStarting(false);
|
|
1081
1182
|
}
|
|
1082
|
-
}, [mfStarting, reactFiles]);
|
|
1183
|
+
}, [mfStarting, reactFiles, refreshModuleFederationGeneratedFiles]);
|
|
1083
1184
|
|
|
1084
1185
|
const stopModuleFederationServer = useCallback(async () => {
|
|
1085
1186
|
if (!mfSandboxId) return;
|
|
@@ -1088,6 +1189,10 @@ export default function CodeRunnerModal() {
|
|
|
1088
1189
|
setMfHostUrl(null);
|
|
1089
1190
|
setMfAppUrls({});
|
|
1090
1191
|
setMfError(null);
|
|
1192
|
+
setMfGeneratedFiles([]);
|
|
1193
|
+
setMfGeneratedFileContents({});
|
|
1194
|
+
setMfLoadingFile(null);
|
|
1195
|
+
setMfConsoleRunning(false);
|
|
1091
1196
|
setSandboxOutput((prev) => [
|
|
1092
1197
|
...prev,
|
|
1093
1198
|
{
|
|
@@ -1148,6 +1253,10 @@ export default function CodeRunnerModal() {
|
|
|
1148
1253
|
setMfHostUrl(null);
|
|
1149
1254
|
setMfAppUrls({});
|
|
1150
1255
|
setMfError(null);
|
|
1256
|
+
setMfGeneratedFiles([]);
|
|
1257
|
+
setMfGeneratedFileContents({});
|
|
1258
|
+
setMfLoadingFile(null);
|
|
1259
|
+
setMfConsoleRunning(false);
|
|
1151
1260
|
return;
|
|
1152
1261
|
}
|
|
1153
1262
|
if (status.hostUrl) setMfHostUrl(status.hostUrl);
|
|
@@ -1180,6 +1289,143 @@ export default function CodeRunnerModal() {
|
|
|
1180
1289
|
return () => clearInterval(interval);
|
|
1181
1290
|
}, [mfSandboxId]);
|
|
1182
1291
|
|
|
1292
|
+
useEffect(() => {
|
|
1293
|
+
if (clientType !== "module-federation" || !mfSandboxId) return;
|
|
1294
|
+
if (!reactActiveFile) return;
|
|
1295
|
+
if (Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile)) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
if (!mfGeneratedFiles.includes(reactActiveFile)) return;
|
|
1299
|
+
if (mfGeneratedFileContents[reactActiveFile] !== undefined) return;
|
|
1300
|
+
if (mfGeneratedFileRequestRef.current === reactActiveFile) return;
|
|
1301
|
+
|
|
1302
|
+
const filePath = reactActiveFile;
|
|
1303
|
+
let cancelled = false;
|
|
1304
|
+
mfGeneratedFileRequestRef.current = filePath;
|
|
1305
|
+
setMfLoadingFile(filePath);
|
|
1306
|
+
|
|
1307
|
+
void fetchModuleFederationGeneratedFile(mfSandboxId, filePath)
|
|
1308
|
+
.then(({ content }) => {
|
|
1309
|
+
if (cancelled) return;
|
|
1310
|
+
setMfGeneratedFileContents((prev) => ({
|
|
1311
|
+
...prev,
|
|
1312
|
+
[filePath]: content,
|
|
1313
|
+
}));
|
|
1314
|
+
})
|
|
1315
|
+
.catch((error: any) => {
|
|
1316
|
+
if (cancelled) return;
|
|
1317
|
+
const message = error?.message ?? String(error);
|
|
1318
|
+
setMfError(message);
|
|
1319
|
+
setMfGeneratedFileContents((prev) => ({
|
|
1320
|
+
...prev,
|
|
1321
|
+
[filePath]: `Failed to load generated file.\n\n${message}`,
|
|
1322
|
+
}));
|
|
1323
|
+
})
|
|
1324
|
+
.finally(() => {
|
|
1325
|
+
if (mfGeneratedFileRequestRef.current === filePath) {
|
|
1326
|
+
mfGeneratedFileRequestRef.current = null;
|
|
1327
|
+
}
|
|
1328
|
+
if (cancelled) return;
|
|
1329
|
+
setMfLoadingFile((current) => (current === filePath ? null : current));
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
return () => {
|
|
1333
|
+
cancelled = true;
|
|
1334
|
+
};
|
|
1335
|
+
}, [
|
|
1336
|
+
clientType,
|
|
1337
|
+
mfGeneratedFileContents,
|
|
1338
|
+
mfGeneratedFiles,
|
|
1339
|
+
mfSandboxId,
|
|
1340
|
+
reactActiveFile,
|
|
1341
|
+
reactFiles,
|
|
1342
|
+
]);
|
|
1343
|
+
|
|
1344
|
+
const runModuleFederationCommand = useCallback(async () => {
|
|
1345
|
+
if (!mfSandboxId || mfConsoleRunning) return;
|
|
1346
|
+
const command = mfConsoleCommand.trim();
|
|
1347
|
+
if (!command) return;
|
|
1348
|
+
|
|
1349
|
+
setMfError(null);
|
|
1350
|
+
setMfConsoleRunning(true);
|
|
1351
|
+
setSbxBottomTab("console");
|
|
1352
|
+
|
|
1353
|
+
let streamError: string | null = null;
|
|
1354
|
+
|
|
1355
|
+
try {
|
|
1356
|
+
await streamModuleFederationCommand(
|
|
1357
|
+
{
|
|
1358
|
+
id: mfSandboxId,
|
|
1359
|
+
command,
|
|
1360
|
+
cwd: mfConsoleCwd === "." ? undefined : mfConsoleCwd,
|
|
1361
|
+
},
|
|
1362
|
+
(message) => {
|
|
1363
|
+
if (message.type === "output") {
|
|
1364
|
+
setMfConsoleOutput((prev) => [
|
|
1365
|
+
...prev,
|
|
1366
|
+
{
|
|
1367
|
+
kind: message.kind,
|
|
1368
|
+
text: message.text,
|
|
1369
|
+
source: "server",
|
|
1370
|
+
},
|
|
1371
|
+
]);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (message.type === "error") {
|
|
1376
|
+
streamError = message.error;
|
|
1377
|
+
setMfConsoleOutput((prev) => [
|
|
1378
|
+
...prev,
|
|
1379
|
+
{
|
|
1380
|
+
kind: "stderr",
|
|
1381
|
+
text: message.error,
|
|
1382
|
+
source: "server",
|
|
1383
|
+
},
|
|
1384
|
+
]);
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1389
|
+
if (streamError) {
|
|
1390
|
+
throw new Error(streamError);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const files = await refreshModuleFederationGeneratedFiles(mfSandboxId);
|
|
1394
|
+
setMfConsoleOutput((prev) => [
|
|
1395
|
+
...prev,
|
|
1396
|
+
{
|
|
1397
|
+
kind: "info",
|
|
1398
|
+
text:
|
|
1399
|
+
files.length > 0
|
|
1400
|
+
? `Refreshed ${files.length} generated file${files.length === 1 ? "" : "s"}.`
|
|
1401
|
+
: "No generated dist files were found.",
|
|
1402
|
+
source: "server",
|
|
1403
|
+
},
|
|
1404
|
+
]);
|
|
1405
|
+
} catch (err: any) {
|
|
1406
|
+
const message = err?.message ?? String(err);
|
|
1407
|
+
setMfError(message);
|
|
1408
|
+
if (streamError !== message) {
|
|
1409
|
+
setMfConsoleOutput((prev) => [
|
|
1410
|
+
...prev,
|
|
1411
|
+
{
|
|
1412
|
+
kind: "stderr",
|
|
1413
|
+
text: message,
|
|
1414
|
+
source: "server",
|
|
1415
|
+
},
|
|
1416
|
+
]);
|
|
1417
|
+
}
|
|
1418
|
+
} finally {
|
|
1419
|
+
setMfConsoleRunning(false);
|
|
1420
|
+
}
|
|
1421
|
+
}, [
|
|
1422
|
+
mfConsoleCommand,
|
|
1423
|
+
mfConsoleCwd,
|
|
1424
|
+
mfConsoleRunning,
|
|
1425
|
+
mfSandboxId,
|
|
1426
|
+
refreshModuleFederationGeneratedFiles,
|
|
1427
|
+
]);
|
|
1428
|
+
|
|
1183
1429
|
// Clean up Next.js server when the modal is closed or mode changes away from nextjs
|
|
1184
1430
|
const prevClientTypeRef = useRef(clientType);
|
|
1185
1431
|
useEffect(() => {
|
|
@@ -1199,6 +1445,13 @@ export default function CodeRunnerModal() {
|
|
|
1199
1445
|
setMfSandboxId(null);
|
|
1200
1446
|
setMfHostUrl(null);
|
|
1201
1447
|
setMfAppUrls({});
|
|
1448
|
+
setMfGeneratedFiles([]);
|
|
1449
|
+
setMfGeneratedFileContents({});
|
|
1450
|
+
setMfLoadingFile(null);
|
|
1451
|
+
setMfConsoleRunning(false);
|
|
1452
|
+
setSbxBottomTab((current) =>
|
|
1453
|
+
current === "console" ? "output" : current,
|
|
1454
|
+
);
|
|
1202
1455
|
}
|
|
1203
1456
|
}, [clientType, nxSandboxId, mfSandboxId]);
|
|
1204
1457
|
|
|
@@ -1215,6 +1468,16 @@ export default function CodeRunnerModal() {
|
|
|
1215
1468
|
(ct: FrontendClientType) => {
|
|
1216
1469
|
if (ct === clientType) return;
|
|
1217
1470
|
setClientType(ct);
|
|
1471
|
+
if (ct !== "module-federation") {
|
|
1472
|
+
setMfGeneratedFiles([]);
|
|
1473
|
+
setMfGeneratedFileContents({});
|
|
1474
|
+
setMfLoadingFile(null);
|
|
1475
|
+
setMfConsoleOutput([]);
|
|
1476
|
+
setMfConsoleRunning(false);
|
|
1477
|
+
setSbxBottomTab((current) =>
|
|
1478
|
+
current === "console" ? "output" : current,
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1218
1481
|
if (ct !== "script") {
|
|
1219
1482
|
const defs = defaultForType(ct);
|
|
1220
1483
|
setReactFiles(defs.files);
|
|
@@ -1233,6 +1496,10 @@ export default function CodeRunnerModal() {
|
|
|
1233
1496
|
setMfPreviewApp("host");
|
|
1234
1497
|
setMfError(null);
|
|
1235
1498
|
}
|
|
1499
|
+
if (ct === "module-federation") {
|
|
1500
|
+
setMfConsoleCommand("npm run build");
|
|
1501
|
+
setMfConsoleCwd("apps/host");
|
|
1502
|
+
}
|
|
1236
1503
|
}
|
|
1237
1504
|
},
|
|
1238
1505
|
[clientType],
|
|
@@ -1482,6 +1749,27 @@ export default function CodeRunnerModal() {
|
|
|
1482
1749
|
minHeight: MIN_H,
|
|
1483
1750
|
};
|
|
1484
1751
|
|
|
1752
|
+
const moduleFederationCommandRoots =
|
|
1753
|
+
getModuleFederationCommandRoots(reactFiles);
|
|
1754
|
+
const moduleFederationGeneratedFiles = mfGeneratedFiles.filter(
|
|
1755
|
+
(filePath) => !Object.prototype.hasOwnProperty.call(reactFiles, filePath),
|
|
1756
|
+
);
|
|
1757
|
+
const moduleFederationGeneratedFileSet = new Set(
|
|
1758
|
+
moduleFederationGeneratedFiles,
|
|
1759
|
+
);
|
|
1760
|
+
const visibleReactFiles =
|
|
1761
|
+
clientType === "module-federation"
|
|
1762
|
+
? Array.from(
|
|
1763
|
+
new Set([
|
|
1764
|
+
...Object.keys(reactFiles),
|
|
1765
|
+
...moduleFederationGeneratedFiles,
|
|
1766
|
+
]),
|
|
1767
|
+
)
|
|
1768
|
+
: Object.keys(reactFiles);
|
|
1769
|
+
const isActiveModuleFederationGeneratedFile =
|
|
1770
|
+
clientType === "module-federation" &&
|
|
1771
|
+
moduleFederationGeneratedFileSet.has(reactActiveFile);
|
|
1772
|
+
|
|
1485
1773
|
return (
|
|
1486
1774
|
<div
|
|
1487
1775
|
className="fixed z-[60] flex flex-col bg-slate-900 border border-slate-700 rounded-xl shadow-2xl overflow-hidden select-none"
|
|
@@ -2564,9 +2852,18 @@ export default function CodeRunnerModal() {
|
|
|
2564
2852
|
{/* Tree nodes */}
|
|
2565
2853
|
<div className="flex-1 py-1">
|
|
2566
2854
|
{(() => {
|
|
2567
|
-
const tree = buildFileTree(
|
|
2855
|
+
const tree = buildFileTree(visibleReactFiles);
|
|
2568
2856
|
|
|
2569
|
-
const fileIcon = (
|
|
2857
|
+
const fileIcon = (
|
|
2858
|
+
name: string,
|
|
2859
|
+
isGenerated = false,
|
|
2860
|
+
) => {
|
|
2861
|
+
if (isGenerated)
|
|
2862
|
+
return (
|
|
2863
|
+
<span className="text-emerald-400 mr-1 text-[8px] font-semibold uppercase">
|
|
2864
|
+
dist
|
|
2865
|
+
</span>
|
|
2866
|
+
);
|
|
2570
2867
|
if (name.endsWith(".tsx") || name.endsWith(".jsx"))
|
|
2571
2868
|
return (
|
|
2572
2869
|
<span className="text-cyan-400 mr-1 text-[9px]">
|
|
@@ -2588,47 +2885,74 @@ export default function CodeRunnerModal() {
|
|
|
2588
2885
|
|
|
2589
2886
|
const renderFile = (path: string, indent = 0) => (
|
|
2590
2887
|
<div key={path} className="group flex items-center">
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2888
|
+
{(() => {
|
|
2889
|
+
const isGenerated =
|
|
2890
|
+
moduleFederationGeneratedFileSet.has(path);
|
|
2891
|
+
return (
|
|
2892
|
+
<>
|
|
2893
|
+
<button
|
|
2894
|
+
type="button"
|
|
2895
|
+
onClick={() => {
|
|
2896
|
+
setReactActiveFile(path);
|
|
2897
|
+
setReactClientTab("edit");
|
|
2898
|
+
}}
|
|
2899
|
+
style={{
|
|
2900
|
+
paddingLeft: `${8 + indent * 10}px`,
|
|
2901
|
+
}}
|
|
2902
|
+
className={`flex-1 flex items-center gap-0.5 py-0.5 pr-1 text-left text-[10px] font-mono truncate transition-colors ${
|
|
2903
|
+
path === reactActiveFile &&
|
|
2904
|
+
reactClientTab === "edit"
|
|
2905
|
+
? isGenerated
|
|
2906
|
+
? "bg-slate-700 text-emerald-100"
|
|
2907
|
+
: "bg-slate-700 text-slate-100"
|
|
2908
|
+
: isGenerated
|
|
2909
|
+
? "text-emerald-300/80 hover:bg-slate-800 hover:text-emerald-100"
|
|
2910
|
+
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
2911
|
+
}`}
|
|
2912
|
+
title={
|
|
2913
|
+
isGenerated
|
|
2914
|
+
? `${path} (generated)`
|
|
2915
|
+
: path
|
|
2916
|
+
}
|
|
2917
|
+
>
|
|
2918
|
+
{fileIcon(
|
|
2919
|
+
path.split("/").pop() ?? path,
|
|
2920
|
+
isGenerated,
|
|
2921
|
+
)}
|
|
2922
|
+
<span className="truncate">
|
|
2923
|
+
{path.split("/").pop()}
|
|
2924
|
+
</span>
|
|
2925
|
+
</button>
|
|
2926
|
+
{!isGenerated && (
|
|
2927
|
+
<button
|
|
2928
|
+
type="button"
|
|
2929
|
+
onClick={() => {
|
|
2930
|
+
if (
|
|
2931
|
+
Object.keys(reactFiles).length <= 1
|
|
2932
|
+
)
|
|
2933
|
+
return;
|
|
2934
|
+
const remaining = Object.keys(
|
|
2935
|
+
reactFiles,
|
|
2936
|
+
).filter((f) => f !== path);
|
|
2937
|
+
setReactFiles((prev) => {
|
|
2938
|
+
const next = { ...prev };
|
|
2939
|
+
delete next[path];
|
|
2940
|
+
return next;
|
|
2941
|
+
});
|
|
2942
|
+
if (reactActiveFile === path)
|
|
2943
|
+
setReactActiveFile(
|
|
2944
|
+
remaining[0] ?? "",
|
|
2945
|
+
);
|
|
2946
|
+
}}
|
|
2947
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 mr-1 rounded text-slate-600 hover:text-red-400 transition-all shrink-0"
|
|
2948
|
+
title="Delete file"
|
|
2949
|
+
>
|
|
2950
|
+
<X className="w-2.5 h-2.5" />
|
|
2951
|
+
</button>
|
|
2952
|
+
)}
|
|
2953
|
+
</>
|
|
2954
|
+
);
|
|
2955
|
+
})()}
|
|
2632
2956
|
</div>
|
|
2633
2957
|
);
|
|
2634
2958
|
|
|
@@ -2728,25 +3052,48 @@ export default function CodeRunnerModal() {
|
|
|
2728
3052
|
}
|
|
2729
3053
|
/>
|
|
2730
3054
|
) : reactClientTab === "edit" ? (
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
3055
|
+
isActiveModuleFederationGeneratedFile ? (
|
|
3056
|
+
<div className="absolute inset-0 flex flex-col bg-slate-950">
|
|
3057
|
+
<div className="shrink-0 border-b border-slate-800 bg-slate-900/80 px-3 py-1.5 text-[10px] font-mono text-emerald-300">
|
|
3058
|
+
Read-only build artifact from dist/. Run the webpack
|
|
3059
|
+
console again to refresh it.
|
|
3060
|
+
</div>
|
|
3061
|
+
<div className="flex-1 overflow-auto px-3 py-3 font-mono text-[12px] leading-relaxed text-slate-200">
|
|
3062
|
+
{mfLoadingFile === reactActiveFile &&
|
|
3063
|
+
mfGeneratedFileContents[reactActiveFile] ===
|
|
3064
|
+
undefined ? (
|
|
3065
|
+
<div className="flex h-full items-center justify-center gap-2 text-slate-500">
|
|
3066
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
3067
|
+
<span>Loading generated file…</span>
|
|
3068
|
+
</div>
|
|
3069
|
+
) : (
|
|
3070
|
+
<pre className="m-0 min-w-max whitespace-pre">
|
|
3071
|
+
{mfGeneratedFileContents[reactActiveFile] ?? ""}
|
|
3072
|
+
</pre>
|
|
3073
|
+
)}
|
|
3074
|
+
</div>
|
|
3075
|
+
</div>
|
|
3076
|
+
) : (
|
|
3077
|
+
<SyntaxEditor
|
|
3078
|
+
key={reactActiveFile}
|
|
3079
|
+
value={reactFiles[reactActiveFile] ?? ""}
|
|
3080
|
+
onChange={(val) =>
|
|
3081
|
+
setReactFiles((prev) => ({
|
|
3082
|
+
...prev,
|
|
3083
|
+
[reactActiveFile]: val,
|
|
3084
|
+
}))
|
|
3085
|
+
}
|
|
3086
|
+
language={
|
|
3087
|
+
reactActiveFile.endsWith(".ts") ||
|
|
3088
|
+
reactActiveFile.endsWith(".tsx")
|
|
3089
|
+
? "typescript"
|
|
3090
|
+
: "javascript"
|
|
3091
|
+
}
|
|
3092
|
+
fontSize="12px"
|
|
3093
|
+
focusRingClass="ring-cyan-500/30"
|
|
3094
|
+
placeholder={`// ${reactActiveFile}\n`}
|
|
3095
|
+
/>
|
|
3096
|
+
)
|
|
2750
3097
|
) : (
|
|
2751
3098
|
<div className="w-full h-full flex flex-col">
|
|
2752
3099
|
{clientType === "nextjs" && (
|
|
@@ -3029,6 +3376,20 @@ export default function CodeRunnerModal() {
|
|
|
3029
3376
|
) : null}
|
|
3030
3377
|
Output
|
|
3031
3378
|
</button>
|
|
3379
|
+
{clientType === "module-federation" && (
|
|
3380
|
+
<button
|
|
3381
|
+
type="button"
|
|
3382
|
+
onClick={() => setSbxBottomTab("console")}
|
|
3383
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider font-medium border-b-2 transition-colors ${
|
|
3384
|
+
sbxBottomTab === "console"
|
|
3385
|
+
? "border-cyan-500 text-cyan-300"
|
|
3386
|
+
: "border-transparent text-slate-500 hover:text-slate-300"
|
|
3387
|
+
}`}
|
|
3388
|
+
>
|
|
3389
|
+
<Terminal className="w-3 h-3" />
|
|
3390
|
+
Console
|
|
3391
|
+
</button>
|
|
3392
|
+
)}
|
|
3032
3393
|
<button
|
|
3033
3394
|
type="button"
|
|
3034
3395
|
onClick={() => {
|
|
@@ -3049,6 +3410,9 @@ export default function CodeRunnerModal() {
|
|
|
3049
3410
|
(serverStarting || clientRunning) && (
|
|
3050
3411
|
<Loader2 className="w-3 h-3 text-emerald-400 animate-spin mr-1" />
|
|
3051
3412
|
)}
|
|
3413
|
+
{sbxBottomTab === "console" && mfConsoleRunning && (
|
|
3414
|
+
<Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
|
|
3415
|
+
)}
|
|
3052
3416
|
{sbxBottomTab === "output" && sandboxOutput.length > 0 && (
|
|
3053
3417
|
<div className="flex items-center gap-1 mr-1">
|
|
3054
3418
|
<button
|
|
@@ -3073,6 +3437,30 @@ export default function CodeRunnerModal() {
|
|
|
3073
3437
|
</button>
|
|
3074
3438
|
</div>
|
|
3075
3439
|
)}
|
|
3440
|
+
{sbxBottomTab === "console" && mfConsoleOutput.length > 0 && (
|
|
3441
|
+
<div className="flex items-center gap-1 mr-1">
|
|
3442
|
+
<button
|
|
3443
|
+
type="button"
|
|
3444
|
+
onClick={() =>
|
|
3445
|
+
navigator.clipboard.writeText(
|
|
3446
|
+
mfConsoleOutput.map((line) => line.text).join("\n"),
|
|
3447
|
+
)
|
|
3448
|
+
}
|
|
3449
|
+
className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
|
|
3450
|
+
title="Copy console output"
|
|
3451
|
+
>
|
|
3452
|
+
<Copy className="w-3 h-3" />
|
|
3453
|
+
</button>
|
|
3454
|
+
<button
|
|
3455
|
+
type="button"
|
|
3456
|
+
onClick={() => setMfConsoleOutput([])}
|
|
3457
|
+
className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
|
|
3458
|
+
title="Clear console output"
|
|
3459
|
+
>
|
|
3460
|
+
<Trash2 className="w-3 h-3" />
|
|
3461
|
+
</button>
|
|
3462
|
+
</div>
|
|
3463
|
+
)}
|
|
3076
3464
|
{sbxBottomTab === "chat" && sbxChatMessages.length > 0 && (
|
|
3077
3465
|
<button
|
|
3078
3466
|
type="button"
|
|
@@ -3144,6 +3532,98 @@ export default function CodeRunnerModal() {
|
|
|
3144
3532
|
</div>
|
|
3145
3533
|
)}
|
|
3146
3534
|
|
|
3535
|
+
{sbxBottomTab === "console" && (
|
|
3536
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
3537
|
+
<div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">
|
|
3538
|
+
<select
|
|
3539
|
+
value={mfConsoleCwd}
|
|
3540
|
+
onChange={(e) => setMfConsoleCwd(e.target.value)}
|
|
3541
|
+
disabled={!mfSandboxId || mfConsoleRunning}
|
|
3542
|
+
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"
|
|
3543
|
+
>
|
|
3544
|
+
{moduleFederationCommandRoots.map((root) => (
|
|
3545
|
+
<option key={root} value={root}>
|
|
3546
|
+
{root === "." ? "root" : root}
|
|
3547
|
+
</option>
|
|
3548
|
+
))}
|
|
3549
|
+
</select>
|
|
3550
|
+
<input
|
|
3551
|
+
value={mfConsoleCommand}
|
|
3552
|
+
onChange={(e) => setMfConsoleCommand(e.target.value)}
|
|
3553
|
+
onKeyDown={(e) => {
|
|
3554
|
+
if (e.key === "Enter") {
|
|
3555
|
+
e.preventDefault();
|
|
3556
|
+
void runModuleFederationCommand();
|
|
3557
|
+
}
|
|
3558
|
+
}}
|
|
3559
|
+
disabled={!mfSandboxId || mfConsoleRunning}
|
|
3560
|
+
placeholder="npm run build"
|
|
3561
|
+
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"
|
|
3562
|
+
spellCheck={false}
|
|
3563
|
+
/>
|
|
3564
|
+
<button
|
|
3565
|
+
type="button"
|
|
3566
|
+
onClick={() => void runModuleFederationCommand()}
|
|
3567
|
+
disabled={
|
|
3568
|
+
!mfSandboxId ||
|
|
3569
|
+
mfConsoleRunning ||
|
|
3570
|
+
!mfConsoleCommand.trim()
|
|
3571
|
+
}
|
|
3572
|
+
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"
|
|
3573
|
+
>
|
|
3574
|
+
{mfConsoleRunning ? (
|
|
3575
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
3576
|
+
) : (
|
|
3577
|
+
<Play className="w-3 h-3" />
|
|
3578
|
+
)}
|
|
3579
|
+
Run
|
|
3580
|
+
</button>
|
|
3581
|
+
<button
|
|
3582
|
+
type="button"
|
|
3583
|
+
onClick={() => void refreshModuleFederationGeneratedFiles()}
|
|
3584
|
+
disabled={!mfSandboxId || mfConsoleRunning}
|
|
3585
|
+
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"
|
|
3586
|
+
>
|
|
3587
|
+
Refresh dist
|
|
3588
|
+
</button>
|
|
3589
|
+
</div>
|
|
3590
|
+
<div className="shrink-0 px-3 py-1.5 text-[10px] text-slate-500 border-b border-slate-800">
|
|
3591
|
+
Run npm scripts in the selected webpack app. Generated dist
|
|
3592
|
+
files appear in the explorer as read-only artifacts.
|
|
3593
|
+
</div>
|
|
3594
|
+
<div className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[12px] leading-relaxed">
|
|
3595
|
+
{mfConsoleOutput.length === 0 && !mfConsoleRunning && (
|
|
3596
|
+
<span className="text-slate-600">
|
|
3597
|
+
{mfSandboxId
|
|
3598
|
+
? "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
|
|
3599
|
+
: "Start webpack first, then run npm commands here."}
|
|
3600
|
+
</span>
|
|
3601
|
+
)}
|
|
3602
|
+
{mfConsoleOutput.map((line, index) => (
|
|
3603
|
+
<div key={index} className="flex items-start gap-2">
|
|
3604
|
+
<span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
|
|
3605
|
+
cmd
|
|
3606
|
+
</span>
|
|
3607
|
+
<span
|
|
3608
|
+
className={
|
|
3609
|
+
line.kind === "stderr"
|
|
3610
|
+
? "text-red-400 whitespace-pre-wrap"
|
|
3611
|
+
: line.kind === "warn"
|
|
3612
|
+
? "text-amber-400 whitespace-pre-wrap"
|
|
3613
|
+
: line.kind === "info"
|
|
3614
|
+
? "text-slate-500 italic whitespace-pre-wrap"
|
|
3615
|
+
: "text-slate-200 whitespace-pre-wrap"
|
|
3616
|
+
}
|
|
3617
|
+
>
|
|
3618
|
+
{line.text}
|
|
3619
|
+
</span>
|
|
3620
|
+
</div>
|
|
3621
|
+
))}
|
|
3622
|
+
<div ref={mfConsoleEndRef} />
|
|
3623
|
+
</div>
|
|
3624
|
+
</div>
|
|
3625
|
+
)}
|
|
3626
|
+
|
|
3147
3627
|
{/* Chat tab */}
|
|
3148
3628
|
{sbxBottomTab === "chat" && (
|
|
3149
3629
|
<>
|