create-interview-cockpit 0.7.0 → 0.9.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 +637 -73
- 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,37 @@ 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 normalizeModuleFederationPreviewPath(input: string): string {
|
|
262
|
+
const trimmed = input.trim();
|
|
263
|
+
if (!trimmed) return "/";
|
|
264
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getModuleFederationCommandRoots(
|
|
268
|
+
files: Record<string, string>,
|
|
269
|
+
): string[] {
|
|
270
|
+
const roots = new Set<string>(["."]);
|
|
271
|
+
|
|
272
|
+
for (const filePath of Object.keys(files)) {
|
|
273
|
+
const match = filePath.match(/^(apps\/[^/]+)/);
|
|
274
|
+
if (match) {
|
|
275
|
+
roots.add(match[1]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return Array.from(roots).sort((a, b) => {
|
|
280
|
+
if (a === ".") return -1;
|
|
281
|
+
if (b === ".") return 1;
|
|
282
|
+
return a.localeCompare(b);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
type SbxBottomTab = "output" | "console" | "chat";
|
|
287
|
+
|
|
254
288
|
export default function CodeRunnerModal() {
|
|
255
289
|
const {
|
|
256
290
|
closeCodeRunner,
|
|
@@ -314,20 +348,32 @@ export default function CodeRunnerModal() {
|
|
|
314
348
|
const [nxStarting, setNxStarting] = useState(false);
|
|
315
349
|
const [nxError, setNxError] = useState<string | null>(null);
|
|
316
350
|
const nxIframeRef = useRef<HTMLIFrameElement>(null);
|
|
351
|
+
const mfIframeRef = useRef<HTMLIFrameElement>(null);
|
|
317
352
|
const [mfSandboxId, setMfSandboxId] = useState<string | null>(null);
|
|
318
353
|
const [mfHostUrl, setMfHostUrl] = useState<string | null>(null);
|
|
319
354
|
const [mfAppUrls, setMfAppUrls] = useState<Record<string, string>>({});
|
|
320
355
|
const [mfStarting, setMfStarting] = useState(false);
|
|
321
356
|
const [mfError, setMfError] = useState<string | null>(null);
|
|
322
357
|
const [mfPreviewApp, setMfPreviewApp] = useState("host");
|
|
358
|
+
const [mfPreviewPath, setMfPreviewPath] = useState("/");
|
|
359
|
+
const [mfNavInput, setMfNavInput] = useState("/");
|
|
360
|
+
const [mfConsoleOutput, setMfConsoleOutput] = useState<OutputLine[]>([]);
|
|
361
|
+
const [mfConsoleCommand, setMfConsoleCommand] = useState("npm run build");
|
|
362
|
+
const [mfConsoleCwd, setMfConsoleCwd] = useState("apps/host");
|
|
363
|
+
const [mfConsoleRunning, setMfConsoleRunning] = useState(false);
|
|
364
|
+
const [mfGeneratedFiles, setMfGeneratedFiles] = useState<string[]>([]);
|
|
365
|
+
const [mfGeneratedFileContents, setMfGeneratedFileContents] = useState<
|
|
366
|
+
Record<string, string>
|
|
367
|
+
>({});
|
|
368
|
+
const [mfLoadingFile, setMfLoadingFile] = useState<string | null>(null);
|
|
323
369
|
// Simulated URL bar state for Next.js mode
|
|
324
370
|
const [reactPreviewPath, setReactPreviewPath] = useState("/");
|
|
325
371
|
const [reactNavInput, setReactNavInput] = useState("/");
|
|
326
372
|
const [reactNavHistory, setReactNavHistory] = useState<string[]>(["/"]);
|
|
327
373
|
const [reactNavIndex, setReactNavIndex] = useState(0);
|
|
328
374
|
|
|
329
|
-
// ── Sandbox output tab ("output" | "chat")
|
|
330
|
-
const [sbxBottomTab, setSbxBottomTab] = useState<
|
|
375
|
+
// ── Sandbox output tab ("output" | "console" | "chat") ─────────────
|
|
376
|
+
const [sbxBottomTab, setSbxBottomTab] = useState<SbxBottomTab>("output");
|
|
331
377
|
|
|
332
378
|
// ── Sandbox panel sizes ─────────────────────────────────────────
|
|
333
379
|
// sbxSplit: server pane width as % of the editor row (0–100)
|
|
@@ -499,7 +545,9 @@ export default function CodeRunnerModal() {
|
|
|
499
545
|
};
|
|
500
546
|
|
|
501
547
|
const outputEndRef = useRef<HTMLDivElement>(null);
|
|
548
|
+
const mfConsoleEndRef = useRef<HTMLDivElement>(null);
|
|
502
549
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
550
|
+
const mfGeneratedFileRequestRef = useRef<string | null>(null);
|
|
503
551
|
// Tracks how many server log lines have already been flushed to sandboxOutput
|
|
504
552
|
const sandboxLogOffsetRef = useRef(0);
|
|
505
553
|
// Stable ref so unmount cleanup can stop sandbox without stale closure
|
|
@@ -553,6 +601,14 @@ export default function CodeRunnerModal() {
|
|
|
553
601
|
setServerCollapsed(true);
|
|
554
602
|
setClientCollapsed(false);
|
|
555
603
|
setMfPreviewApp("host");
|
|
604
|
+
setMfPreviewPath("/");
|
|
605
|
+
setMfNavInput("/");
|
|
606
|
+
setMfConsoleCommand("npm run build");
|
|
607
|
+
setMfConsoleCwd("apps/host");
|
|
608
|
+
setMfConsoleOutput([]);
|
|
609
|
+
setMfGeneratedFiles([]);
|
|
610
|
+
setMfGeneratedFileContents({});
|
|
611
|
+
setMfLoadingFile(null);
|
|
556
612
|
}
|
|
557
613
|
}
|
|
558
614
|
}, [runnerInitialSandbox]);
|
|
@@ -564,6 +620,20 @@ export default function CodeRunnerModal() {
|
|
|
564
620
|
outputEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
565
621
|
}, [output]);
|
|
566
622
|
|
|
623
|
+
useEffect(() => {
|
|
624
|
+
if (sbxBottomTab === "console") {
|
|
625
|
+
mfConsoleEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
626
|
+
}
|
|
627
|
+
}, [mfConsoleOutput, mfConsoleRunning, sbxBottomTab]);
|
|
628
|
+
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
if (clientType !== "module-federation") return;
|
|
631
|
+
const roots = getModuleFederationCommandRoots(reactFiles);
|
|
632
|
+
if (!roots.includes(mfConsoleCwd)) {
|
|
633
|
+
setMfConsoleCwd(roots.find((root) => root !== ".") ?? ".");
|
|
634
|
+
}
|
|
635
|
+
}, [clientType, reactFiles, mfConsoleCwd]);
|
|
636
|
+
|
|
567
637
|
// ── Sandbox divider drag handlers ────────────────────────────────
|
|
568
638
|
useEffect(() => {
|
|
569
639
|
const onMove = (e: MouseEvent) => {
|
|
@@ -1038,10 +1108,51 @@ export default function CodeRunnerModal() {
|
|
|
1038
1108
|
}
|
|
1039
1109
|
}, [nxStarting, reactFiles]);
|
|
1040
1110
|
|
|
1111
|
+
const refreshModuleFederationGeneratedFiles = useCallback(
|
|
1112
|
+
async (sandboxId: string | null = mfSandboxId): Promise<string[]> => {
|
|
1113
|
+
if (!sandboxId) {
|
|
1114
|
+
setMfGeneratedFiles([]);
|
|
1115
|
+
setMfGeneratedFileContents({});
|
|
1116
|
+
setMfLoadingFile(null);
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
try {
|
|
1121
|
+
const files = await fetchModuleFederationGeneratedFiles(sandboxId);
|
|
1122
|
+
setMfGeneratedFiles(files);
|
|
1123
|
+
setMfGeneratedFileContents((prev) =>
|
|
1124
|
+
Object.fromEntries(
|
|
1125
|
+
Object.entries(prev).filter(([filePath]) =>
|
|
1126
|
+
files.includes(filePath),
|
|
1127
|
+
),
|
|
1128
|
+
),
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
if (
|
|
1132
|
+
reactActiveFile &&
|
|
1133
|
+
!Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile) &&
|
|
1134
|
+
!files.includes(reactActiveFile)
|
|
1135
|
+
) {
|
|
1136
|
+
setReactActiveFile(Object.keys(reactFiles)[0] ?? files[0] ?? "");
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return files;
|
|
1140
|
+
} catch (err: any) {
|
|
1141
|
+
setMfError(err?.message ?? String(err));
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
[mfSandboxId, reactActiveFile, reactFiles],
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1041
1148
|
const startModuleFederationServer = useCallback(async () => {
|
|
1042
1149
|
if (mfStarting) return;
|
|
1043
1150
|
setMfStarting(true);
|
|
1044
1151
|
setMfError(null);
|
|
1152
|
+
setMfGeneratedFiles([]);
|
|
1153
|
+
setMfGeneratedFileContents({});
|
|
1154
|
+
setMfLoadingFile(null);
|
|
1155
|
+
setMfConsoleOutput([]);
|
|
1045
1156
|
setSbxBottomTab("output");
|
|
1046
1157
|
setSandboxOutput([
|
|
1047
1158
|
{
|
|
@@ -1058,9 +1169,12 @@ export default function CodeRunnerModal() {
|
|
|
1058
1169
|
setMfPreviewApp(
|
|
1059
1170
|
info.appUrls.host ? "host" : (Object.keys(info.appUrls)[0] ?? "host"),
|
|
1060
1171
|
);
|
|
1172
|
+
setMfPreviewPath("/");
|
|
1173
|
+
setMfNavInput("/");
|
|
1061
1174
|
setReactClientTab("preview");
|
|
1062
1175
|
setServerCollapsed(true);
|
|
1063
1176
|
setClientCollapsed(false);
|
|
1177
|
+
void refreshModuleFederationGeneratedFiles(info.id);
|
|
1064
1178
|
setSandboxOutput((prev) => [
|
|
1065
1179
|
...prev,
|
|
1066
1180
|
{
|
|
@@ -1079,7 +1193,7 @@ export default function CodeRunnerModal() {
|
|
|
1079
1193
|
} finally {
|
|
1080
1194
|
setMfStarting(false);
|
|
1081
1195
|
}
|
|
1082
|
-
}, [mfStarting, reactFiles]);
|
|
1196
|
+
}, [mfStarting, reactFiles, refreshModuleFederationGeneratedFiles]);
|
|
1083
1197
|
|
|
1084
1198
|
const stopModuleFederationServer = useCallback(async () => {
|
|
1085
1199
|
if (!mfSandboxId) return;
|
|
@@ -1088,6 +1202,10 @@ export default function CodeRunnerModal() {
|
|
|
1088
1202
|
setMfHostUrl(null);
|
|
1089
1203
|
setMfAppUrls({});
|
|
1090
1204
|
setMfError(null);
|
|
1205
|
+
setMfGeneratedFiles([]);
|
|
1206
|
+
setMfGeneratedFileContents({});
|
|
1207
|
+
setMfLoadingFile(null);
|
|
1208
|
+
setMfConsoleRunning(false);
|
|
1091
1209
|
setSandboxOutput((prev) => [
|
|
1092
1210
|
...prev,
|
|
1093
1211
|
{
|
|
@@ -1148,6 +1266,10 @@ export default function CodeRunnerModal() {
|
|
|
1148
1266
|
setMfHostUrl(null);
|
|
1149
1267
|
setMfAppUrls({});
|
|
1150
1268
|
setMfError(null);
|
|
1269
|
+
setMfGeneratedFiles([]);
|
|
1270
|
+
setMfGeneratedFileContents({});
|
|
1271
|
+
setMfLoadingFile(null);
|
|
1272
|
+
setMfConsoleRunning(false);
|
|
1151
1273
|
return;
|
|
1152
1274
|
}
|
|
1153
1275
|
if (status.hostUrl) setMfHostUrl(status.hostUrl);
|
|
@@ -1180,6 +1302,143 @@ export default function CodeRunnerModal() {
|
|
|
1180
1302
|
return () => clearInterval(interval);
|
|
1181
1303
|
}, [mfSandboxId]);
|
|
1182
1304
|
|
|
1305
|
+
useEffect(() => {
|
|
1306
|
+
if (clientType !== "module-federation" || !mfSandboxId) return;
|
|
1307
|
+
if (!reactActiveFile) return;
|
|
1308
|
+
if (Object.prototype.hasOwnProperty.call(reactFiles, reactActiveFile)) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (!mfGeneratedFiles.includes(reactActiveFile)) return;
|
|
1312
|
+
if (mfGeneratedFileContents[reactActiveFile] !== undefined) return;
|
|
1313
|
+
if (mfGeneratedFileRequestRef.current === reactActiveFile) return;
|
|
1314
|
+
|
|
1315
|
+
const filePath = reactActiveFile;
|
|
1316
|
+
let cancelled = false;
|
|
1317
|
+
mfGeneratedFileRequestRef.current = filePath;
|
|
1318
|
+
setMfLoadingFile(filePath);
|
|
1319
|
+
|
|
1320
|
+
void fetchModuleFederationGeneratedFile(mfSandboxId, filePath)
|
|
1321
|
+
.then(({ content }) => {
|
|
1322
|
+
if (cancelled) return;
|
|
1323
|
+
setMfGeneratedFileContents((prev) => ({
|
|
1324
|
+
...prev,
|
|
1325
|
+
[filePath]: content,
|
|
1326
|
+
}));
|
|
1327
|
+
})
|
|
1328
|
+
.catch((error: any) => {
|
|
1329
|
+
if (cancelled) return;
|
|
1330
|
+
const message = error?.message ?? String(error);
|
|
1331
|
+
setMfError(message);
|
|
1332
|
+
setMfGeneratedFileContents((prev) => ({
|
|
1333
|
+
...prev,
|
|
1334
|
+
[filePath]: `Failed to load generated file.\n\n${message}`,
|
|
1335
|
+
}));
|
|
1336
|
+
})
|
|
1337
|
+
.finally(() => {
|
|
1338
|
+
if (mfGeneratedFileRequestRef.current === filePath) {
|
|
1339
|
+
mfGeneratedFileRequestRef.current = null;
|
|
1340
|
+
}
|
|
1341
|
+
if (cancelled) return;
|
|
1342
|
+
setMfLoadingFile((current) => (current === filePath ? null : current));
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
return () => {
|
|
1346
|
+
cancelled = true;
|
|
1347
|
+
};
|
|
1348
|
+
}, [
|
|
1349
|
+
clientType,
|
|
1350
|
+
mfGeneratedFileContents,
|
|
1351
|
+
mfGeneratedFiles,
|
|
1352
|
+
mfSandboxId,
|
|
1353
|
+
reactActiveFile,
|
|
1354
|
+
reactFiles,
|
|
1355
|
+
]);
|
|
1356
|
+
|
|
1357
|
+
const runModuleFederationCommand = useCallback(async () => {
|
|
1358
|
+
if (!mfSandboxId || mfConsoleRunning) return;
|
|
1359
|
+
const command = mfConsoleCommand.trim();
|
|
1360
|
+
if (!command) return;
|
|
1361
|
+
|
|
1362
|
+
setMfError(null);
|
|
1363
|
+
setMfConsoleRunning(true);
|
|
1364
|
+
setSbxBottomTab("console");
|
|
1365
|
+
|
|
1366
|
+
let streamError: string | null = null;
|
|
1367
|
+
|
|
1368
|
+
try {
|
|
1369
|
+
await streamModuleFederationCommand(
|
|
1370
|
+
{
|
|
1371
|
+
id: mfSandboxId,
|
|
1372
|
+
command,
|
|
1373
|
+
cwd: mfConsoleCwd === "." ? undefined : mfConsoleCwd,
|
|
1374
|
+
},
|
|
1375
|
+
(message) => {
|
|
1376
|
+
if (message.type === "output") {
|
|
1377
|
+
setMfConsoleOutput((prev) => [
|
|
1378
|
+
...prev,
|
|
1379
|
+
{
|
|
1380
|
+
kind: message.kind,
|
|
1381
|
+
text: message.text,
|
|
1382
|
+
source: "server",
|
|
1383
|
+
},
|
|
1384
|
+
]);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (message.type === "error") {
|
|
1389
|
+
streamError = message.error;
|
|
1390
|
+
setMfConsoleOutput((prev) => [
|
|
1391
|
+
...prev,
|
|
1392
|
+
{
|
|
1393
|
+
kind: "stderr",
|
|
1394
|
+
text: message.error,
|
|
1395
|
+
source: "server",
|
|
1396
|
+
},
|
|
1397
|
+
]);
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
if (streamError) {
|
|
1403
|
+
throw new Error(streamError);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const files = await refreshModuleFederationGeneratedFiles(mfSandboxId);
|
|
1407
|
+
setMfConsoleOutput((prev) => [
|
|
1408
|
+
...prev,
|
|
1409
|
+
{
|
|
1410
|
+
kind: "info",
|
|
1411
|
+
text:
|
|
1412
|
+
files.length > 0
|
|
1413
|
+
? `Refreshed ${files.length} generated file${files.length === 1 ? "" : "s"}.`
|
|
1414
|
+
: "No generated dist files were found.",
|
|
1415
|
+
source: "server",
|
|
1416
|
+
},
|
|
1417
|
+
]);
|
|
1418
|
+
} catch (err: any) {
|
|
1419
|
+
const message = err?.message ?? String(err);
|
|
1420
|
+
setMfError(message);
|
|
1421
|
+
if (streamError !== message) {
|
|
1422
|
+
setMfConsoleOutput((prev) => [
|
|
1423
|
+
...prev,
|
|
1424
|
+
{
|
|
1425
|
+
kind: "stderr",
|
|
1426
|
+
text: message,
|
|
1427
|
+
source: "server",
|
|
1428
|
+
},
|
|
1429
|
+
]);
|
|
1430
|
+
}
|
|
1431
|
+
} finally {
|
|
1432
|
+
setMfConsoleRunning(false);
|
|
1433
|
+
}
|
|
1434
|
+
}, [
|
|
1435
|
+
mfConsoleCommand,
|
|
1436
|
+
mfConsoleCwd,
|
|
1437
|
+
mfConsoleRunning,
|
|
1438
|
+
mfSandboxId,
|
|
1439
|
+
refreshModuleFederationGeneratedFiles,
|
|
1440
|
+
]);
|
|
1441
|
+
|
|
1183
1442
|
// Clean up Next.js server when the modal is closed or mode changes away from nextjs
|
|
1184
1443
|
const prevClientTypeRef = useRef(clientType);
|
|
1185
1444
|
useEffect(() => {
|
|
@@ -1199,6 +1458,13 @@ export default function CodeRunnerModal() {
|
|
|
1199
1458
|
setMfSandboxId(null);
|
|
1200
1459
|
setMfHostUrl(null);
|
|
1201
1460
|
setMfAppUrls({});
|
|
1461
|
+
setMfGeneratedFiles([]);
|
|
1462
|
+
setMfGeneratedFileContents({});
|
|
1463
|
+
setMfLoadingFile(null);
|
|
1464
|
+
setMfConsoleRunning(false);
|
|
1465
|
+
setSbxBottomTab((current) =>
|
|
1466
|
+
current === "console" ? "output" : current,
|
|
1467
|
+
);
|
|
1202
1468
|
}
|
|
1203
1469
|
}, [clientType, nxSandboxId, mfSandboxId]);
|
|
1204
1470
|
|
|
@@ -1215,6 +1481,16 @@ export default function CodeRunnerModal() {
|
|
|
1215
1481
|
(ct: FrontendClientType) => {
|
|
1216
1482
|
if (ct === clientType) return;
|
|
1217
1483
|
setClientType(ct);
|
|
1484
|
+
if (ct !== "module-federation") {
|
|
1485
|
+
setMfGeneratedFiles([]);
|
|
1486
|
+
setMfGeneratedFileContents({});
|
|
1487
|
+
setMfLoadingFile(null);
|
|
1488
|
+
setMfConsoleOutput([]);
|
|
1489
|
+
setMfConsoleRunning(false);
|
|
1490
|
+
setSbxBottomTab((current) =>
|
|
1491
|
+
current === "console" ? "output" : current,
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1218
1494
|
if (ct !== "script") {
|
|
1219
1495
|
const defs = defaultForType(ct);
|
|
1220
1496
|
setReactFiles(defs.files);
|
|
@@ -1231,8 +1507,14 @@ export default function CodeRunnerModal() {
|
|
|
1231
1507
|
setServerCollapsed(true);
|
|
1232
1508
|
setClientCollapsed(false);
|
|
1233
1509
|
setMfPreviewApp("host");
|
|
1510
|
+
setMfPreviewPath("/");
|
|
1511
|
+
setMfNavInput("/");
|
|
1234
1512
|
setMfError(null);
|
|
1235
1513
|
}
|
|
1514
|
+
if (ct === "module-federation") {
|
|
1515
|
+
setMfConsoleCommand("npm run build");
|
|
1516
|
+
setMfConsoleCwd("apps/host");
|
|
1517
|
+
}
|
|
1236
1518
|
}
|
|
1237
1519
|
},
|
|
1238
1520
|
[clientType],
|
|
@@ -1482,6 +1764,37 @@ export default function CodeRunnerModal() {
|
|
|
1482
1764
|
minHeight: MIN_H,
|
|
1483
1765
|
};
|
|
1484
1766
|
|
|
1767
|
+
const moduleFederationCommandRoots =
|
|
1768
|
+
getModuleFederationCommandRoots(reactFiles);
|
|
1769
|
+
const moduleFederationGeneratedFiles = mfGeneratedFiles.filter(
|
|
1770
|
+
(filePath) => !Object.prototype.hasOwnProperty.call(reactFiles, filePath),
|
|
1771
|
+
);
|
|
1772
|
+
const moduleFederationGeneratedFileSet = new Set(
|
|
1773
|
+
moduleFederationGeneratedFiles,
|
|
1774
|
+
);
|
|
1775
|
+
const moduleFederationPreviewBaseUrl =
|
|
1776
|
+
mfAppUrls[mfPreviewApp] ?? mfHostUrl ?? "";
|
|
1777
|
+
const moduleFederationPreviewPath =
|
|
1778
|
+
normalizeModuleFederationPreviewPath(mfPreviewPath);
|
|
1779
|
+
const moduleFederationPreviewUrl = moduleFederationPreviewBaseUrl
|
|
1780
|
+
? `${moduleFederationPreviewBaseUrl}${moduleFederationPreviewPath}`
|
|
1781
|
+
: "";
|
|
1782
|
+
const moduleFederationPreviewHostLabel = moduleFederationPreviewBaseUrl
|
|
1783
|
+
? moduleFederationPreviewBaseUrl.replace(/^https?:\/\//, "")
|
|
1784
|
+
: "localhost";
|
|
1785
|
+
const visibleReactFiles =
|
|
1786
|
+
clientType === "module-federation"
|
|
1787
|
+
? Array.from(
|
|
1788
|
+
new Set([
|
|
1789
|
+
...Object.keys(reactFiles),
|
|
1790
|
+
...moduleFederationGeneratedFiles,
|
|
1791
|
+
]),
|
|
1792
|
+
)
|
|
1793
|
+
: Object.keys(reactFiles);
|
|
1794
|
+
const isActiveModuleFederationGeneratedFile =
|
|
1795
|
+
clientType === "module-federation" &&
|
|
1796
|
+
moduleFederationGeneratedFileSet.has(reactActiveFile);
|
|
1797
|
+
|
|
1485
1798
|
return (
|
|
1486
1799
|
<div
|
|
1487
1800
|
className="fixed z-[60] flex flex-col bg-slate-900 border border-slate-700 rounded-xl shadow-2xl overflow-hidden select-none"
|
|
@@ -2564,9 +2877,18 @@ export default function CodeRunnerModal() {
|
|
|
2564
2877
|
{/* Tree nodes */}
|
|
2565
2878
|
<div className="flex-1 py-1">
|
|
2566
2879
|
{(() => {
|
|
2567
|
-
const tree = buildFileTree(
|
|
2880
|
+
const tree = buildFileTree(visibleReactFiles);
|
|
2568
2881
|
|
|
2569
|
-
const fileIcon = (
|
|
2882
|
+
const fileIcon = (
|
|
2883
|
+
name: string,
|
|
2884
|
+
isGenerated = false,
|
|
2885
|
+
) => {
|
|
2886
|
+
if (isGenerated)
|
|
2887
|
+
return (
|
|
2888
|
+
<span className="text-emerald-400 mr-1 text-[8px] font-semibold uppercase">
|
|
2889
|
+
dist
|
|
2890
|
+
</span>
|
|
2891
|
+
);
|
|
2570
2892
|
if (name.endsWith(".tsx") || name.endsWith(".jsx"))
|
|
2571
2893
|
return (
|
|
2572
2894
|
<span className="text-cyan-400 mr-1 text-[9px]">
|
|
@@ -2588,47 +2910,74 @@ export default function CodeRunnerModal() {
|
|
|
2588
2910
|
|
|
2589
2911
|
const renderFile = (path: string, indent = 0) => (
|
|
2590
2912
|
<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
|
-
|
|
2913
|
+
{(() => {
|
|
2914
|
+
const isGenerated =
|
|
2915
|
+
moduleFederationGeneratedFileSet.has(path);
|
|
2916
|
+
return (
|
|
2917
|
+
<>
|
|
2918
|
+
<button
|
|
2919
|
+
type="button"
|
|
2920
|
+
onClick={() => {
|
|
2921
|
+
setReactActiveFile(path);
|
|
2922
|
+
setReactClientTab("edit");
|
|
2923
|
+
}}
|
|
2924
|
+
style={{
|
|
2925
|
+
paddingLeft: `${8 + indent * 10}px`,
|
|
2926
|
+
}}
|
|
2927
|
+
className={`flex-1 flex items-center gap-0.5 py-0.5 pr-1 text-left text-[10px] font-mono truncate transition-colors ${
|
|
2928
|
+
path === reactActiveFile &&
|
|
2929
|
+
reactClientTab === "edit"
|
|
2930
|
+
? isGenerated
|
|
2931
|
+
? "bg-slate-700 text-emerald-100"
|
|
2932
|
+
: "bg-slate-700 text-slate-100"
|
|
2933
|
+
: isGenerated
|
|
2934
|
+
? "text-emerald-300/80 hover:bg-slate-800 hover:text-emerald-100"
|
|
2935
|
+
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
2936
|
+
}`}
|
|
2937
|
+
title={
|
|
2938
|
+
isGenerated
|
|
2939
|
+
? `${path} (generated)`
|
|
2940
|
+
: path
|
|
2941
|
+
}
|
|
2942
|
+
>
|
|
2943
|
+
{fileIcon(
|
|
2944
|
+
path.split("/").pop() ?? path,
|
|
2945
|
+
isGenerated,
|
|
2946
|
+
)}
|
|
2947
|
+
<span className="truncate">
|
|
2948
|
+
{path.split("/").pop()}
|
|
2949
|
+
</span>
|
|
2950
|
+
</button>
|
|
2951
|
+
{!isGenerated && (
|
|
2952
|
+
<button
|
|
2953
|
+
type="button"
|
|
2954
|
+
onClick={() => {
|
|
2955
|
+
if (
|
|
2956
|
+
Object.keys(reactFiles).length <= 1
|
|
2957
|
+
)
|
|
2958
|
+
return;
|
|
2959
|
+
const remaining = Object.keys(
|
|
2960
|
+
reactFiles,
|
|
2961
|
+
).filter((f) => f !== path);
|
|
2962
|
+
setReactFiles((prev) => {
|
|
2963
|
+
const next = { ...prev };
|
|
2964
|
+
delete next[path];
|
|
2965
|
+
return next;
|
|
2966
|
+
});
|
|
2967
|
+
if (reactActiveFile === path)
|
|
2968
|
+
setReactActiveFile(
|
|
2969
|
+
remaining[0] ?? "",
|
|
2970
|
+
);
|
|
2971
|
+
}}
|
|
2972
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 mr-1 rounded text-slate-600 hover:text-red-400 transition-all shrink-0"
|
|
2973
|
+
title="Delete file"
|
|
2974
|
+
>
|
|
2975
|
+
<X className="w-2.5 h-2.5" />
|
|
2976
|
+
</button>
|
|
2977
|
+
)}
|
|
2978
|
+
</>
|
|
2979
|
+
);
|
|
2980
|
+
})()}
|
|
2632
2981
|
</div>
|
|
2633
2982
|
);
|
|
2634
2983
|
|
|
@@ -2728,25 +3077,48 @@ export default function CodeRunnerModal() {
|
|
|
2728
3077
|
}
|
|
2729
3078
|
/>
|
|
2730
3079
|
) : reactClientTab === "edit" ? (
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
3080
|
+
isActiveModuleFederationGeneratedFile ? (
|
|
3081
|
+
<div className="absolute inset-0 flex flex-col bg-slate-950">
|
|
3082
|
+
<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">
|
|
3083
|
+
Read-only build artifact from dist/. Run the webpack
|
|
3084
|
+
console again to refresh it.
|
|
3085
|
+
</div>
|
|
3086
|
+
<div className="flex-1 overflow-auto px-3 py-3 font-mono text-[12px] leading-relaxed text-slate-200">
|
|
3087
|
+
{mfLoadingFile === reactActiveFile &&
|
|
3088
|
+
mfGeneratedFileContents[reactActiveFile] ===
|
|
3089
|
+
undefined ? (
|
|
3090
|
+
<div className="flex h-full items-center justify-center gap-2 text-slate-500">
|
|
3091
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
3092
|
+
<span>Loading generated file…</span>
|
|
3093
|
+
</div>
|
|
3094
|
+
) : (
|
|
3095
|
+
<pre className="m-0 min-w-max whitespace-pre">
|
|
3096
|
+
{mfGeneratedFileContents[reactActiveFile] ?? ""}
|
|
3097
|
+
</pre>
|
|
3098
|
+
)}
|
|
3099
|
+
</div>
|
|
3100
|
+
</div>
|
|
3101
|
+
) : (
|
|
3102
|
+
<SyntaxEditor
|
|
3103
|
+
key={reactActiveFile}
|
|
3104
|
+
value={reactFiles[reactActiveFile] ?? ""}
|
|
3105
|
+
onChange={(val) =>
|
|
3106
|
+
setReactFiles((prev) => ({
|
|
3107
|
+
...prev,
|
|
3108
|
+
[reactActiveFile]: val,
|
|
3109
|
+
}))
|
|
3110
|
+
}
|
|
3111
|
+
language={
|
|
3112
|
+
reactActiveFile.endsWith(".ts") ||
|
|
3113
|
+
reactActiveFile.endsWith(".tsx")
|
|
3114
|
+
? "typescript"
|
|
3115
|
+
: "javascript"
|
|
3116
|
+
}
|
|
3117
|
+
fontSize="12px"
|
|
3118
|
+
focusRingClass="ring-cyan-500/30"
|
|
3119
|
+
placeholder={`// ${reactActiveFile}\n`}
|
|
3120
|
+
/>
|
|
3121
|
+
)
|
|
2750
3122
|
) : (
|
|
2751
3123
|
<div className="w-full h-full flex flex-col">
|
|
2752
3124
|
{clientType === "nextjs" && (
|
|
@@ -2883,7 +3255,7 @@ export default function CodeRunnerModal() {
|
|
|
2883
3255
|
</div>
|
|
2884
3256
|
)}
|
|
2885
3257
|
{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
|
|
3258
|
+
<div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0 min-w-0">
|
|
2887
3259
|
{Object.entries(mfAppUrls).map(([name, url]) => (
|
|
2888
3260
|
<button
|
|
2889
3261
|
key={name}
|
|
@@ -2899,11 +3271,69 @@ export default function CodeRunnerModal() {
|
|
|
2899
3271
|
{name}
|
|
2900
3272
|
</button>
|
|
2901
3273
|
))}
|
|
2902
|
-
<
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3274
|
+
<button
|
|
3275
|
+
type="button"
|
|
3276
|
+
onClick={() => {
|
|
3277
|
+
if (
|
|
3278
|
+
mfIframeRef.current &&
|
|
3279
|
+
moduleFederationPreviewUrl
|
|
3280
|
+
) {
|
|
3281
|
+
mfIframeRef.current.src =
|
|
3282
|
+
moduleFederationPreviewUrl;
|
|
3283
|
+
}
|
|
3284
|
+
}}
|
|
3285
|
+
className="p-0.5 rounded text-slate-500 hover:text-slate-200 disabled:opacity-30 disabled:cursor-not-allowed transition-colors shrink-0"
|
|
3286
|
+
title="Refresh"
|
|
3287
|
+
disabled={!moduleFederationPreviewUrl}
|
|
3288
|
+
>
|
|
3289
|
+
<svg
|
|
3290
|
+
className="w-3 h-3"
|
|
3291
|
+
viewBox="0 0 16 16"
|
|
3292
|
+
fill="currentColor"
|
|
3293
|
+
>
|
|
3294
|
+
<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" />
|
|
3295
|
+
</svg>
|
|
3296
|
+
</button>
|
|
3297
|
+
<form
|
|
3298
|
+
className="flex-1 flex items-center gap-1 bg-slate-900 border border-slate-600 rounded px-2 py-0.5 focus-within:border-cyan-500/60 transition-colors min-w-0"
|
|
3299
|
+
onSubmit={(e) => {
|
|
3300
|
+
e.preventDefault();
|
|
3301
|
+
const nextPath =
|
|
3302
|
+
normalizeModuleFederationPreviewPath(
|
|
3303
|
+
mfNavInput,
|
|
3304
|
+
);
|
|
3305
|
+
setMfPreviewPath(nextPath);
|
|
3306
|
+
setMfNavInput(nextPath);
|
|
3307
|
+
if (
|
|
3308
|
+
nextPath === moduleFederationPreviewPath &&
|
|
3309
|
+
mfIframeRef.current &&
|
|
3310
|
+
moduleFederationPreviewBaseUrl
|
|
3311
|
+
) {
|
|
3312
|
+
mfIframeRef.current.src = `${moduleFederationPreviewBaseUrl}${nextPath}`;
|
|
3313
|
+
}
|
|
3314
|
+
}}
|
|
3315
|
+
>
|
|
3316
|
+
<span className="text-slate-600 text-[9px] font-mono select-none shrink-0">
|
|
3317
|
+
{moduleFederationPreviewHostLabel}
|
|
3318
|
+
</span>
|
|
3319
|
+
<input
|
|
3320
|
+
value={mfNavInput}
|
|
3321
|
+
onChange={(e) => setMfNavInput(e.target.value)}
|
|
3322
|
+
onFocus={(e) => e.target.select()}
|
|
3323
|
+
className="flex-1 bg-transparent text-[11px] font-mono text-slate-200 outline-none placeholder-slate-600 min-w-0"
|
|
3324
|
+
placeholder="/"
|
|
3325
|
+
spellCheck={false}
|
|
3326
|
+
/>
|
|
3327
|
+
</form>
|
|
3328
|
+
{moduleFederationPreviewUrl ? (
|
|
3329
|
+
<span className="text-[9px] font-mono text-green-400 shrink-0">
|
|
3330
|
+
● live
|
|
3331
|
+
</span>
|
|
3332
|
+
) : (
|
|
3333
|
+
<span className="text-[9px] font-mono text-slate-600 shrink-0">
|
|
3334
|
+
Start webpack to preview
|
|
3335
|
+
</span>
|
|
3336
|
+
)}
|
|
2907
3337
|
</div>
|
|
2908
3338
|
)}
|
|
2909
3339
|
{((clientType === "module-federation" && mfError) ||
|
|
@@ -2955,9 +3385,10 @@ export default function CodeRunnerModal() {
|
|
|
2955
3385
|
{!mfStarting &&
|
|
2956
3386
|
clientType === "module-federation" &&
|
|
2957
3387
|
mfSandboxId &&
|
|
2958
|
-
|
|
3388
|
+
moduleFederationPreviewUrl && (
|
|
2959
3389
|
<iframe
|
|
2960
|
-
|
|
3390
|
+
ref={mfIframeRef}
|
|
3391
|
+
src={moduleFederationPreviewUrl}
|
|
2961
3392
|
className="flex-1 min-h-0 w-full border-0 bg-white"
|
|
2962
3393
|
title="Webpack Module Federation Preview"
|
|
2963
3394
|
/>
|
|
@@ -3029,6 +3460,20 @@ export default function CodeRunnerModal() {
|
|
|
3029
3460
|
) : null}
|
|
3030
3461
|
Output
|
|
3031
3462
|
</button>
|
|
3463
|
+
{clientType === "module-federation" && (
|
|
3464
|
+
<button
|
|
3465
|
+
type="button"
|
|
3466
|
+
onClick={() => setSbxBottomTab("console")}
|
|
3467
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider font-medium border-b-2 transition-colors ${
|
|
3468
|
+
sbxBottomTab === "console"
|
|
3469
|
+
? "border-cyan-500 text-cyan-300"
|
|
3470
|
+
: "border-transparent text-slate-500 hover:text-slate-300"
|
|
3471
|
+
}`}
|
|
3472
|
+
>
|
|
3473
|
+
<Terminal className="w-3 h-3" />
|
|
3474
|
+
Console
|
|
3475
|
+
</button>
|
|
3476
|
+
)}
|
|
3032
3477
|
<button
|
|
3033
3478
|
type="button"
|
|
3034
3479
|
onClick={() => {
|
|
@@ -3049,6 +3494,9 @@ export default function CodeRunnerModal() {
|
|
|
3049
3494
|
(serverStarting || clientRunning) && (
|
|
3050
3495
|
<Loader2 className="w-3 h-3 text-emerald-400 animate-spin mr-1" />
|
|
3051
3496
|
)}
|
|
3497
|
+
{sbxBottomTab === "console" && mfConsoleRunning && (
|
|
3498
|
+
<Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
|
|
3499
|
+
)}
|
|
3052
3500
|
{sbxBottomTab === "output" && sandboxOutput.length > 0 && (
|
|
3053
3501
|
<div className="flex items-center gap-1 mr-1">
|
|
3054
3502
|
<button
|
|
@@ -3073,6 +3521,30 @@ export default function CodeRunnerModal() {
|
|
|
3073
3521
|
</button>
|
|
3074
3522
|
</div>
|
|
3075
3523
|
)}
|
|
3524
|
+
{sbxBottomTab === "console" && mfConsoleOutput.length > 0 && (
|
|
3525
|
+
<div className="flex items-center gap-1 mr-1">
|
|
3526
|
+
<button
|
|
3527
|
+
type="button"
|
|
3528
|
+
onClick={() =>
|
|
3529
|
+
navigator.clipboard.writeText(
|
|
3530
|
+
mfConsoleOutput.map((line) => line.text).join("\n"),
|
|
3531
|
+
)
|
|
3532
|
+
}
|
|
3533
|
+
className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
|
|
3534
|
+
title="Copy console output"
|
|
3535
|
+
>
|
|
3536
|
+
<Copy className="w-3 h-3" />
|
|
3537
|
+
</button>
|
|
3538
|
+
<button
|
|
3539
|
+
type="button"
|
|
3540
|
+
onClick={() => setMfConsoleOutput([])}
|
|
3541
|
+
className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
|
|
3542
|
+
title="Clear console output"
|
|
3543
|
+
>
|
|
3544
|
+
<Trash2 className="w-3 h-3" />
|
|
3545
|
+
</button>
|
|
3546
|
+
</div>
|
|
3547
|
+
)}
|
|
3076
3548
|
{sbxBottomTab === "chat" && sbxChatMessages.length > 0 && (
|
|
3077
3549
|
<button
|
|
3078
3550
|
type="button"
|
|
@@ -3144,6 +3616,98 @@ export default function CodeRunnerModal() {
|
|
|
3144
3616
|
</div>
|
|
3145
3617
|
)}
|
|
3146
3618
|
|
|
3619
|
+
{sbxBottomTab === "console" && (
|
|
3620
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
3621
|
+
<div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">
|
|
3622
|
+
<select
|
|
3623
|
+
value={mfConsoleCwd}
|
|
3624
|
+
onChange={(e) => setMfConsoleCwd(e.target.value)}
|
|
3625
|
+
disabled={!mfSandboxId || mfConsoleRunning}
|
|
3626
|
+
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"
|
|
3627
|
+
>
|
|
3628
|
+
{moduleFederationCommandRoots.map((root) => (
|
|
3629
|
+
<option key={root} value={root}>
|
|
3630
|
+
{root === "." ? "root" : root}
|
|
3631
|
+
</option>
|
|
3632
|
+
))}
|
|
3633
|
+
</select>
|
|
3634
|
+
<input
|
|
3635
|
+
value={mfConsoleCommand}
|
|
3636
|
+
onChange={(e) => setMfConsoleCommand(e.target.value)}
|
|
3637
|
+
onKeyDown={(e) => {
|
|
3638
|
+
if (e.key === "Enter") {
|
|
3639
|
+
e.preventDefault();
|
|
3640
|
+
void runModuleFederationCommand();
|
|
3641
|
+
}
|
|
3642
|
+
}}
|
|
3643
|
+
disabled={!mfSandboxId || mfConsoleRunning}
|
|
3644
|
+
placeholder="npm run build"
|
|
3645
|
+
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"
|
|
3646
|
+
spellCheck={false}
|
|
3647
|
+
/>
|
|
3648
|
+
<button
|
|
3649
|
+
type="button"
|
|
3650
|
+
onClick={() => void runModuleFederationCommand()}
|
|
3651
|
+
disabled={
|
|
3652
|
+
!mfSandboxId ||
|
|
3653
|
+
mfConsoleRunning ||
|
|
3654
|
+
!mfConsoleCommand.trim()
|
|
3655
|
+
}
|
|
3656
|
+
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"
|
|
3657
|
+
>
|
|
3658
|
+
{mfConsoleRunning ? (
|
|
3659
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
3660
|
+
) : (
|
|
3661
|
+
<Play className="w-3 h-3" />
|
|
3662
|
+
)}
|
|
3663
|
+
Run
|
|
3664
|
+
</button>
|
|
3665
|
+
<button
|
|
3666
|
+
type="button"
|
|
3667
|
+
onClick={() => void refreshModuleFederationGeneratedFiles()}
|
|
3668
|
+
disabled={!mfSandboxId || mfConsoleRunning}
|
|
3669
|
+
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"
|
|
3670
|
+
>
|
|
3671
|
+
Refresh dist
|
|
3672
|
+
</button>
|
|
3673
|
+
</div>
|
|
3674
|
+
<div className="shrink-0 px-3 py-1.5 text-[10px] text-slate-500 border-b border-slate-800">
|
|
3675
|
+
Run npm scripts in the selected webpack app. Generated dist
|
|
3676
|
+
files appear in the explorer as read-only artifacts.
|
|
3677
|
+
</div>
|
|
3678
|
+
<div className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[12px] leading-relaxed">
|
|
3679
|
+
{mfConsoleOutput.length === 0 && !mfConsoleRunning && (
|
|
3680
|
+
<span className="text-slate-600">
|
|
3681
|
+
{mfSandboxId
|
|
3682
|
+
? "Run npm run build in apps/host, apps/profile, or apps/checkout to inspect dist/."
|
|
3683
|
+
: "Start webpack first, then run npm commands here."}
|
|
3684
|
+
</span>
|
|
3685
|
+
)}
|
|
3686
|
+
{mfConsoleOutput.map((line, index) => (
|
|
3687
|
+
<div key={index} className="flex items-start gap-2">
|
|
3688
|
+
<span className="shrink-0 text-[9px] font-bold mt-0.5 w-7 text-right text-cyan-600">
|
|
3689
|
+
cmd
|
|
3690
|
+
</span>
|
|
3691
|
+
<span
|
|
3692
|
+
className={
|
|
3693
|
+
line.kind === "stderr"
|
|
3694
|
+
? "text-red-400 whitespace-pre-wrap"
|
|
3695
|
+
: line.kind === "warn"
|
|
3696
|
+
? "text-amber-400 whitespace-pre-wrap"
|
|
3697
|
+
: line.kind === "info"
|
|
3698
|
+
? "text-slate-500 italic whitespace-pre-wrap"
|
|
3699
|
+
: "text-slate-200 whitespace-pre-wrap"
|
|
3700
|
+
}
|
|
3701
|
+
>
|
|
3702
|
+
{line.text}
|
|
3703
|
+
</span>
|
|
3704
|
+
</div>
|
|
3705
|
+
))}
|
|
3706
|
+
<div ref={mfConsoleEndRef} />
|
|
3707
|
+
</div>
|
|
3708
|
+
</div>
|
|
3709
|
+
)}
|
|
3710
|
+
|
|
3147
3711
|
{/* Chat tab */}
|
|
3148
3712
|
{sbxBottomTab === "chat" && (
|
|
3149
3713
|
<>
|