create-interview-cockpit 0.6.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 +160 -2
- package/template/client/src/components/CodeContextPanel.tsx +111 -0
- package/template/client/src/components/CodeRunnerModal.tsx +1000 -226
- package/template/client/src/reactLab.ts +518 -5
- package/template/client/src/store.ts +35 -4
- package/template/client/src/types.ts +3 -2
- package/template/client/tsconfig.tsbuildinfo +30 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/google-drive.ts +2 -0
- package/template/server/src/index.ts +642 -5
- package/template/server/src/storage.ts +11 -2
|
@@ -43,10 +43,18 @@ import {
|
|
|
43
43
|
generatePreviewHTML,
|
|
44
44
|
defaultForType,
|
|
45
45
|
resolveNextjsEntry,
|
|
46
|
+
type FrontendLabType,
|
|
46
47
|
} from "../reactLab";
|
|
47
48
|
import {
|
|
49
|
+
fetchModuleFederationGeneratedFile,
|
|
50
|
+
fetchModuleFederationGeneratedFiles,
|
|
51
|
+
fetchModuleFederationStatus,
|
|
52
|
+
startModuleFederationSandbox,
|
|
48
53
|
startNextjsSandbox,
|
|
54
|
+
stopModuleFederationSandbox,
|
|
55
|
+
streamModuleFederationCommand,
|
|
49
56
|
updateNextjsFiles,
|
|
57
|
+
updateModuleFederationFiles,
|
|
50
58
|
stopNextjsSandbox,
|
|
51
59
|
} from "../api";
|
|
52
60
|
import ReactMarkdown from "react-markdown";
|
|
@@ -67,6 +75,7 @@ interface OutputLine {
|
|
|
67
75
|
|
|
68
76
|
const LANG_OPTIONS = ["typescript", "javascript"] as const;
|
|
69
77
|
type Lang = (typeof LANG_OPTIONS)[number];
|
|
78
|
+
type FrontendClientType = "script" | FrontendLabType;
|
|
70
79
|
|
|
71
80
|
// ── Sandbox default snippets ─────────────────────────────────────────
|
|
72
81
|
const DEFAULT_SERVER_CODE = `import express from 'express';
|
|
@@ -202,6 +211,74 @@ function SyntaxEditor({
|
|
|
202
211
|
);
|
|
203
212
|
}
|
|
204
213
|
|
|
214
|
+
interface FileTreeNode {
|
|
215
|
+
name: string;
|
|
216
|
+
path: string;
|
|
217
|
+
children: FileTreeNode[];
|
|
218
|
+
files: string[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildFileTree(paths: string[]): FileTreeNode {
|
|
222
|
+
const root: FileTreeNode = {
|
|
223
|
+
name: "",
|
|
224
|
+
path: "",
|
|
225
|
+
children: [],
|
|
226
|
+
files: [],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
for (const filePath of paths) {
|
|
230
|
+
const parts = filePath.split("/");
|
|
231
|
+
let node = root;
|
|
232
|
+
|
|
233
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
234
|
+
const name = parts[index];
|
|
235
|
+
const path = parts.slice(0, index + 1).join("/");
|
|
236
|
+
let child = node.children.find((entry) => entry.path === path);
|
|
237
|
+
|
|
238
|
+
if (!child) {
|
|
239
|
+
child = {
|
|
240
|
+
name,
|
|
241
|
+
path,
|
|
242
|
+
children: [],
|
|
243
|
+
files: [],
|
|
244
|
+
};
|
|
245
|
+
node.children.push(child);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
node = child;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
node.files.push(filePath);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return root;
|
|
255
|
+
}
|
|
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
|
+
|
|
205
282
|
export default function CodeRunnerModal() {
|
|
206
283
|
const {
|
|
207
284
|
closeCodeRunner,
|
|
@@ -246,9 +323,7 @@ export default function CodeRunnerModal() {
|
|
|
246
323
|
const [clientRunning, setClientRunning] = useState(false);
|
|
247
324
|
|
|
248
325
|
// ── React/Next.js client state ──────────────────────────────
|
|
249
|
-
const [clientType, setClientType] = useState<"script"
|
|
250
|
-
"script",
|
|
251
|
-
);
|
|
326
|
+
const [clientType, setClientType] = useState<FrontendClientType>("script");
|
|
252
327
|
const [reactFiles, setReactFiles] = useState<Record<string, string>>({});
|
|
253
328
|
const [reactActiveFile, setReactActiveFile] = useState<string>("");
|
|
254
329
|
const [reactClientTab, setReactClientTab] = useState<"edit" | "preview">(
|
|
@@ -267,14 +342,29 @@ export default function CodeRunnerModal() {
|
|
|
267
342
|
const [nxStarting, setNxStarting] = useState(false);
|
|
268
343
|
const [nxError, setNxError] = useState<string | null>(null);
|
|
269
344
|
const nxIframeRef = useRef<HTMLIFrameElement>(null);
|
|
345
|
+
const [mfSandboxId, setMfSandboxId] = useState<string | null>(null);
|
|
346
|
+
const [mfHostUrl, setMfHostUrl] = useState<string | null>(null);
|
|
347
|
+
const [mfAppUrls, setMfAppUrls] = useState<Record<string, string>>({});
|
|
348
|
+
const [mfStarting, setMfStarting] = useState(false);
|
|
349
|
+
const [mfError, setMfError] = useState<string | null>(null);
|
|
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);
|
|
270
360
|
// Simulated URL bar state for Next.js mode
|
|
271
361
|
const [reactPreviewPath, setReactPreviewPath] = useState("/");
|
|
272
362
|
const [reactNavInput, setReactNavInput] = useState("/");
|
|
273
363
|
const [reactNavHistory, setReactNavHistory] = useState<string[]>(["/"]);
|
|
274
364
|
const [reactNavIndex, setReactNavIndex] = useState(0);
|
|
275
365
|
|
|
276
|
-
// ── Sandbox output tab ("output" | "chat")
|
|
277
|
-
const [sbxBottomTab, setSbxBottomTab] = useState<
|
|
366
|
+
// ── Sandbox output tab ("output" | "console" | "chat") ─────────────
|
|
367
|
+
const [sbxBottomTab, setSbxBottomTab] = useState<SbxBottomTab>("output");
|
|
278
368
|
|
|
279
369
|
// ── Sandbox panel sizes ─────────────────────────────────────────
|
|
280
370
|
// sbxSplit: server pane width as % of the editor row (0–100)
|
|
@@ -367,7 +457,9 @@ export default function CodeRunnerModal() {
|
|
|
367
457
|
? "react"
|
|
368
458
|
: clientType === "nextjs"
|
|
369
459
|
? "nextjs"
|
|
370
|
-
: "
|
|
460
|
+
: clientType === "module-federation"
|
|
461
|
+
? "module-federation"
|
|
462
|
+
: "sandbox";
|
|
371
463
|
const payload = JSON.stringify(
|
|
372
464
|
clientType === "script"
|
|
373
465
|
? { serverCode, serverLang, clientCode, clientLang }
|
|
@@ -444,7 +536,9 @@ export default function CodeRunnerModal() {
|
|
|
444
536
|
};
|
|
445
537
|
|
|
446
538
|
const outputEndRef = useRef<HTMLDivElement>(null);
|
|
539
|
+
const mfConsoleEndRef = useRef<HTMLDivElement>(null);
|
|
447
540
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
541
|
+
const mfGeneratedFileRequestRef = useRef<string | null>(null);
|
|
448
542
|
// Tracks how many server log lines have already been flushed to sandboxOutput
|
|
449
543
|
const sandboxLogOffsetRef = useRef(0);
|
|
450
544
|
// Stable ref so unmount cleanup can stop sandbox without stale closure
|
|
@@ -474,10 +568,9 @@ export default function CodeRunnerModal() {
|
|
|
474
568
|
setActiveSandboxId(runnerInitialSandbox.fileId ?? null);
|
|
475
569
|
// Restore client type and React/Next.js files
|
|
476
570
|
const ct =
|
|
477
|
-
(runnerInitialSandbox.clientType as
|
|
478
|
-
"script";
|
|
571
|
+
(runnerInitialSandbox.clientType as FrontendClientType) ?? "script";
|
|
479
572
|
setClientType(ct);
|
|
480
|
-
if (ct
|
|
573
|
+
if (ct !== "script") {
|
|
481
574
|
if (
|
|
482
575
|
runnerInitialSandbox.reactFiles &&
|
|
483
576
|
Object.keys(runnerInitialSandbox.reactFiles).length > 0
|
|
@@ -495,6 +588,17 @@ export default function CodeRunnerModal() {
|
|
|
495
588
|
}
|
|
496
589
|
setReactPreviewSrc(null);
|
|
497
590
|
setReactClientTab("edit");
|
|
591
|
+
if (ct === "module-federation") {
|
|
592
|
+
setServerCollapsed(true);
|
|
593
|
+
setClientCollapsed(false);
|
|
594
|
+
setMfPreviewApp("host");
|
|
595
|
+
setMfConsoleCommand("npm run build");
|
|
596
|
+
setMfConsoleCwd("apps/host");
|
|
597
|
+
setMfConsoleOutput([]);
|
|
598
|
+
setMfGeneratedFiles([]);
|
|
599
|
+
setMfGeneratedFileContents({});
|
|
600
|
+
setMfLoadingFile(null);
|
|
601
|
+
}
|
|
498
602
|
}
|
|
499
603
|
}, [runnerInitialSandbox]);
|
|
500
604
|
|
|
@@ -505,6 +609,20 @@ export default function CodeRunnerModal() {
|
|
|
505
609
|
outputEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
506
610
|
}, [output]);
|
|
507
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
|
+
|
|
508
626
|
// ── Sandbox divider drag handlers ────────────────────────────────
|
|
509
627
|
useEffect(() => {
|
|
510
628
|
const onMove = (e: MouseEvent) => {
|
|
@@ -979,6 +1097,112 @@ export default function CodeRunnerModal() {
|
|
|
979
1097
|
}
|
|
980
1098
|
}, [nxStarting, reactFiles]);
|
|
981
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
|
+
|
|
1137
|
+
const startModuleFederationServer = useCallback(async () => {
|
|
1138
|
+
if (mfStarting) return;
|
|
1139
|
+
setMfStarting(true);
|
|
1140
|
+
setMfError(null);
|
|
1141
|
+
setMfGeneratedFiles([]);
|
|
1142
|
+
setMfGeneratedFileContents({});
|
|
1143
|
+
setMfLoadingFile(null);
|
|
1144
|
+
setMfConsoleOutput([]);
|
|
1145
|
+
setSbxBottomTab("output");
|
|
1146
|
+
setSandboxOutput([
|
|
1147
|
+
{
|
|
1148
|
+
kind: "info",
|
|
1149
|
+
text: "Installing dependencies and starting webpack apps…",
|
|
1150
|
+
source: "server",
|
|
1151
|
+
},
|
|
1152
|
+
]);
|
|
1153
|
+
try {
|
|
1154
|
+
const info = await startModuleFederationSandbox(reactFiles);
|
|
1155
|
+
setMfSandboxId(info.id);
|
|
1156
|
+
setMfHostUrl(info.hostUrl);
|
|
1157
|
+
setMfAppUrls(info.appUrls);
|
|
1158
|
+
setMfPreviewApp(
|
|
1159
|
+
info.appUrls.host ? "host" : (Object.keys(info.appUrls)[0] ?? "host"),
|
|
1160
|
+
);
|
|
1161
|
+
setReactClientTab("preview");
|
|
1162
|
+
setServerCollapsed(true);
|
|
1163
|
+
setClientCollapsed(false);
|
|
1164
|
+
void refreshModuleFederationGeneratedFiles(info.id);
|
|
1165
|
+
setSandboxOutput((prev) => [
|
|
1166
|
+
...prev,
|
|
1167
|
+
{
|
|
1168
|
+
kind: "info",
|
|
1169
|
+
text: `✓ Webpack host running at ${info.hostUrl}`,
|
|
1170
|
+
source: "server",
|
|
1171
|
+
},
|
|
1172
|
+
]);
|
|
1173
|
+
} catch (err: any) {
|
|
1174
|
+
const message = err?.message ?? String(err);
|
|
1175
|
+
setMfError(message);
|
|
1176
|
+
setSandboxOutput((prev) => [
|
|
1177
|
+
...prev,
|
|
1178
|
+
{ kind: "stderr", text: message, source: "server" },
|
|
1179
|
+
]);
|
|
1180
|
+
} finally {
|
|
1181
|
+
setMfStarting(false);
|
|
1182
|
+
}
|
|
1183
|
+
}, [mfStarting, reactFiles, refreshModuleFederationGeneratedFiles]);
|
|
1184
|
+
|
|
1185
|
+
const stopModuleFederationServer = useCallback(async () => {
|
|
1186
|
+
if (!mfSandboxId) return;
|
|
1187
|
+
await stopModuleFederationSandbox(mfSandboxId).catch(() => {});
|
|
1188
|
+
setMfSandboxId(null);
|
|
1189
|
+
setMfHostUrl(null);
|
|
1190
|
+
setMfAppUrls({});
|
|
1191
|
+
setMfError(null);
|
|
1192
|
+
setMfGeneratedFiles([]);
|
|
1193
|
+
setMfGeneratedFileContents({});
|
|
1194
|
+
setMfLoadingFile(null);
|
|
1195
|
+
setMfConsoleRunning(false);
|
|
1196
|
+
setSandboxOutput((prev) => [
|
|
1197
|
+
...prev,
|
|
1198
|
+
{
|
|
1199
|
+
kind: "info",
|
|
1200
|
+
text: "Webpack module federation lab stopped.",
|
|
1201
|
+
source: "server",
|
|
1202
|
+
},
|
|
1203
|
+
]);
|
|
1204
|
+
}, [mfSandboxId]);
|
|
1205
|
+
|
|
982
1206
|
/** Push updated files to the running Next.js server (HMR picks them up). */
|
|
983
1207
|
const pushNextjsFiles = useCallback(
|
|
984
1208
|
async (files: Record<string, string>) => {
|
|
@@ -992,6 +1216,18 @@ export default function CodeRunnerModal() {
|
|
|
992
1216
|
[nxSandboxId],
|
|
993
1217
|
);
|
|
994
1218
|
|
|
1219
|
+
const pushModuleFederationFiles = useCallback(
|
|
1220
|
+
async (files: Record<string, string>) => {
|
|
1221
|
+
if (!mfSandboxId) return;
|
|
1222
|
+
try {
|
|
1223
|
+
await updateModuleFederationFiles(mfSandboxId, files);
|
|
1224
|
+
} catch (err: any) {
|
|
1225
|
+
setMfError(err?.message ?? String(err));
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
[mfSandboxId],
|
|
1229
|
+
);
|
|
1230
|
+
|
|
995
1231
|
// Auto-push file changes to the running Next.js server
|
|
996
1232
|
const nxFilesRef = useRef(reactFiles);
|
|
997
1233
|
useEffect(() => {
|
|
@@ -1000,6 +1236,196 @@ export default function CodeRunnerModal() {
|
|
|
1000
1236
|
void pushNextjsFiles(reactFiles);
|
|
1001
1237
|
}, [reactFiles, nxSandboxId, pushNextjsFiles]);
|
|
1002
1238
|
|
|
1239
|
+
const mfFilesRef = useRef(reactFiles);
|
|
1240
|
+
useEffect(() => {
|
|
1241
|
+
if (!mfSandboxId || reactFiles === mfFilesRef.current) return;
|
|
1242
|
+
mfFilesRef.current = reactFiles;
|
|
1243
|
+
void pushModuleFederationFiles(reactFiles);
|
|
1244
|
+
}, [reactFiles, mfSandboxId, pushModuleFederationFiles]);
|
|
1245
|
+
|
|
1246
|
+
useEffect(() => {
|
|
1247
|
+
if (!mfSandboxId) return;
|
|
1248
|
+
const interval = setInterval(async () => {
|
|
1249
|
+
try {
|
|
1250
|
+
const status = await fetchModuleFederationStatus(mfSandboxId);
|
|
1251
|
+
if (!status.running) {
|
|
1252
|
+
setMfSandboxId(null);
|
|
1253
|
+
setMfHostUrl(null);
|
|
1254
|
+
setMfAppUrls({});
|
|
1255
|
+
setMfError(null);
|
|
1256
|
+
setMfGeneratedFiles([]);
|
|
1257
|
+
setMfGeneratedFileContents({});
|
|
1258
|
+
setMfLoadingFile(null);
|
|
1259
|
+
setMfConsoleRunning(false);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (status.hostUrl) setMfHostUrl(status.hostUrl);
|
|
1263
|
+
if (status.appUrls) {
|
|
1264
|
+
setMfAppUrls(status.appUrls);
|
|
1265
|
+
setMfPreviewApp((prev) =>
|
|
1266
|
+
status.appUrls?.[prev]
|
|
1267
|
+
? prev
|
|
1268
|
+
: (Object.keys(status.appUrls ?? {})[0] ?? "host"),
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (status.logs) {
|
|
1272
|
+
setSandboxOutput(
|
|
1273
|
+
status.logs.flatMap((chunk) =>
|
|
1274
|
+
chunk
|
|
1275
|
+
.split("\n")
|
|
1276
|
+
.filter(Boolean)
|
|
1277
|
+
.map((text) => ({
|
|
1278
|
+
kind: "stdout" as const,
|
|
1279
|
+
text,
|
|
1280
|
+
source: "server" as const,
|
|
1281
|
+
})),
|
|
1282
|
+
),
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
} catch {
|
|
1286
|
+
/* ignore transient network errors */
|
|
1287
|
+
}
|
|
1288
|
+
}, 1000);
|
|
1289
|
+
return () => clearInterval(interval);
|
|
1290
|
+
}, [mfSandboxId]);
|
|
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
|
+
|
|
1003
1429
|
// Clean up Next.js server when the modal is closed or mode changes away from nextjs
|
|
1004
1430
|
const prevClientTypeRef = useRef(clientType);
|
|
1005
1431
|
useEffect(() => {
|
|
@@ -1010,21 +1436,49 @@ export default function CodeRunnerModal() {
|
|
|
1010
1436
|
setNxSandboxId(null);
|
|
1011
1437
|
setNxSandboxUrl(null);
|
|
1012
1438
|
}
|
|
1013
|
-
|
|
1439
|
+
if (
|
|
1440
|
+
prev === "module-federation" &&
|
|
1441
|
+
clientType !== "module-federation" &&
|
|
1442
|
+
mfSandboxId
|
|
1443
|
+
) {
|
|
1444
|
+
void stopModuleFederationSandbox(mfSandboxId);
|
|
1445
|
+
setMfSandboxId(null);
|
|
1446
|
+
setMfHostUrl(null);
|
|
1447
|
+
setMfAppUrls({});
|
|
1448
|
+
setMfGeneratedFiles([]);
|
|
1449
|
+
setMfGeneratedFileContents({});
|
|
1450
|
+
setMfLoadingFile(null);
|
|
1451
|
+
setMfConsoleRunning(false);
|
|
1452
|
+
setSbxBottomTab((current) =>
|
|
1453
|
+
current === "console" ? "output" : current,
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
}, [clientType, nxSandboxId, mfSandboxId]);
|
|
1014
1457
|
|
|
1015
1458
|
// Clean up on unmount
|
|
1016
1459
|
useEffect(() => {
|
|
1017
1460
|
return () => {
|
|
1018
1461
|
if (nxSandboxId) void stopNextjsSandbox(nxSandboxId);
|
|
1462
|
+
if (mfSandboxId) void stopModuleFederationSandbox(mfSandboxId);
|
|
1019
1463
|
};
|
|
1020
1464
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1021
|
-
}, [nxSandboxId]);
|
|
1465
|
+
}, [nxSandboxId, mfSandboxId]);
|
|
1022
1466
|
|
|
1023
1467
|
const handleClientTypeChange = useCallback(
|
|
1024
|
-
(ct:
|
|
1468
|
+
(ct: FrontendClientType) => {
|
|
1025
1469
|
if (ct === clientType) return;
|
|
1026
1470
|
setClientType(ct);
|
|
1027
|
-
if (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
|
+
}
|
|
1481
|
+
if (ct !== "script") {
|
|
1028
1482
|
const defs = defaultForType(ct);
|
|
1029
1483
|
setReactFiles(defs.files);
|
|
1030
1484
|
setReactActiveFile(defs.activeFile);
|
|
@@ -1036,6 +1490,16 @@ export default function CodeRunnerModal() {
|
|
|
1036
1490
|
setReactNavHistory(["/"]);
|
|
1037
1491
|
setReactNavIndex(0);
|
|
1038
1492
|
}
|
|
1493
|
+
if (ct === "module-federation") {
|
|
1494
|
+
setServerCollapsed(true);
|
|
1495
|
+
setClientCollapsed(false);
|
|
1496
|
+
setMfPreviewApp("host");
|
|
1497
|
+
setMfError(null);
|
|
1498
|
+
}
|
|
1499
|
+
if (ct === "module-federation") {
|
|
1500
|
+
setMfConsoleCommand("npm run build");
|
|
1501
|
+
setMfConsoleCwd("apps/host");
|
|
1502
|
+
}
|
|
1039
1503
|
}
|
|
1040
1504
|
},
|
|
1041
1505
|
[clientType],
|
|
@@ -1061,12 +1525,19 @@ export default function CodeRunnerModal() {
|
|
|
1061
1525
|
...prev,
|
|
1062
1526
|
{ id: aId, role: "assistant", content: "" },
|
|
1063
1527
|
]);
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1528
|
+
const isFrontendMode =
|
|
1529
|
+
clientType === "react" ||
|
|
1530
|
+
clientType === "nextjs" ||
|
|
1531
|
+
clientType === "module-federation";
|
|
1532
|
+
const workspaceFiles = isFrontendMode
|
|
1066
1533
|
? reactFiles
|
|
1067
1534
|
: { "client.js": clientCode, "server.ts": serverCode };
|
|
1068
|
-
const labType:
|
|
1069
|
-
clientType === "nextjs"
|
|
1535
|
+
const labType: FrontendLabType =
|
|
1536
|
+
clientType === "nextjs"
|
|
1537
|
+
? "nextjs"
|
|
1538
|
+
: clientType === "module-federation"
|
|
1539
|
+
? "module-federation"
|
|
1540
|
+
: "react";
|
|
1070
1541
|
try {
|
|
1071
1542
|
const history = [...sbxChatMessages, userMsg].map((m) => ({
|
|
1072
1543
|
role: m.role,
|
|
@@ -1278,6 +1749,27 @@ export default function CodeRunnerModal() {
|
|
|
1278
1749
|
minHeight: MIN_H,
|
|
1279
1750
|
};
|
|
1280
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
|
+
|
|
1281
1773
|
return (
|
|
1282
1774
|
<div
|
|
1283
1775
|
className="fixed z-[60] flex flex-col bg-slate-900 border border-slate-700 rounded-xl shadow-2xl overflow-hidden select-none"
|
|
@@ -1944,7 +2436,14 @@ export default function CodeRunnerModal() {
|
|
|
1944
2436
|
</span>
|
|
1945
2437
|
{/* Client type selector: JS / React / Next */}
|
|
1946
2438
|
<div className="flex items-center rounded overflow-hidden border border-slate-700 text-[9px] ml-1 shrink-0">
|
|
1947
|
-
{(
|
|
2439
|
+
{(
|
|
2440
|
+
[
|
|
2441
|
+
"script",
|
|
2442
|
+
"react",
|
|
2443
|
+
"nextjs",
|
|
2444
|
+
"module-federation",
|
|
2445
|
+
] as const
|
|
2446
|
+
).map((ct) => (
|
|
1948
2447
|
<button
|
|
1949
2448
|
key={ct}
|
|
1950
2449
|
type="button"
|
|
@@ -1959,7 +2458,9 @@ export default function CodeRunnerModal() {
|
|
|
1959
2458
|
? "JS"
|
|
1960
2459
|
: ct === "react"
|
|
1961
2460
|
? "React"
|
|
1962
|
-
: "
|
|
2461
|
+
: ct === "nextjs"
|
|
2462
|
+
? "Next"
|
|
2463
|
+
: "Webpack"}
|
|
1963
2464
|
</button>
|
|
1964
2465
|
))}
|
|
1965
2466
|
</div>
|
|
@@ -2023,9 +2524,11 @@ export default function CodeRunnerModal() {
|
|
|
2023
2524
|
</>
|
|
2024
2525
|
)}
|
|
2025
2526
|
{/* React/Next mode: optional URL + Preview button + edit/preview toggle for Next */}
|
|
2026
|
-
{(clientType === "react" ||
|
|
2527
|
+
{(clientType === "react" ||
|
|
2528
|
+
clientType === "nextjs" ||
|
|
2529
|
+
clientType === "module-federation") && (
|
|
2027
2530
|
<>
|
|
2028
|
-
{sandboxUrl && (
|
|
2531
|
+
{clientType !== "module-federation" && sandboxUrl && (
|
|
2029
2532
|
<span
|
|
2030
2533
|
className="text-[9px] font-mono text-slate-600 truncate max-w-[80px]"
|
|
2031
2534
|
title={sandboxUrl}
|
|
@@ -2037,7 +2540,7 @@ export default function CodeRunnerModal() {
|
|
|
2037
2540
|
{clientType === "react" && (
|
|
2038
2541
|
<button
|
|
2039
2542
|
type="button"
|
|
2040
|
-
onClick={refreshPreview}
|
|
2543
|
+
onClick={() => refreshPreview()}
|
|
2041
2544
|
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
2545
|
title="Render preview"
|
|
2043
2546
|
>
|
|
@@ -2093,6 +2596,73 @@ export default function CodeRunnerModal() {
|
|
|
2093
2596
|
)}
|
|
2094
2597
|
</>
|
|
2095
2598
|
)}
|
|
2599
|
+
{clientType === "module-federation" && (
|
|
2600
|
+
<>
|
|
2601
|
+
{mfSandboxId && mfHostUrl && (
|
|
2602
|
+
<span
|
|
2603
|
+
className="text-[9px] font-mono text-slate-600 truncate max-w-[110px]"
|
|
2604
|
+
title={mfHostUrl}
|
|
2605
|
+
>
|
|
2606
|
+
{mfHostUrl.replace(/^https?:\/\//, "")}
|
|
2607
|
+
</span>
|
|
2608
|
+
)}
|
|
2609
|
+
{!mfSandboxId ? (
|
|
2610
|
+
<button
|
|
2611
|
+
type="button"
|
|
2612
|
+
onClick={() => void startModuleFederationServer()}
|
|
2613
|
+
disabled={mfStarting}
|
|
2614
|
+
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"
|
|
2615
|
+
title="Start real webpack module federation dev servers"
|
|
2616
|
+
>
|
|
2617
|
+
{mfStarting ? (
|
|
2618
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
2619
|
+
) : (
|
|
2620
|
+
<Play className="w-3 h-3" />
|
|
2621
|
+
)}
|
|
2622
|
+
{mfStarting ? "Starting…" : "Run Webpack"}
|
|
2623
|
+
</button>
|
|
2624
|
+
) : (
|
|
2625
|
+
<>
|
|
2626
|
+
<div className="flex items-center rounded overflow-hidden border border-slate-700/50 text-[9px] shrink-0">
|
|
2627
|
+
<button
|
|
2628
|
+
type="button"
|
|
2629
|
+
onClick={() => setReactClientTab("edit")}
|
|
2630
|
+
className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
|
|
2631
|
+
reactClientTab === "edit"
|
|
2632
|
+
? "bg-slate-700 text-slate-200"
|
|
2633
|
+
: "text-slate-500 hover:text-slate-400"
|
|
2634
|
+
}`}
|
|
2635
|
+
title="Edit code"
|
|
2636
|
+
>
|
|
2637
|
+
<Code2 className="w-2.5 h-2.5" />
|
|
2638
|
+
</button>
|
|
2639
|
+
<button
|
|
2640
|
+
type="button"
|
|
2641
|
+
onClick={() => setReactClientTab("preview")}
|
|
2642
|
+
className={`flex items-center gap-0.5 px-1.5 py-0.5 transition-colors ${
|
|
2643
|
+
reactClientTab === "preview"
|
|
2644
|
+
? "bg-slate-700 text-slate-200"
|
|
2645
|
+
: "text-slate-500 hover:text-slate-400"
|
|
2646
|
+
}`}
|
|
2647
|
+
title="Live preview"
|
|
2648
|
+
>
|
|
2649
|
+
<Eye className="w-2.5 h-2.5" />
|
|
2650
|
+
</button>
|
|
2651
|
+
</div>
|
|
2652
|
+
<button
|
|
2653
|
+
type="button"
|
|
2654
|
+
onClick={() =>
|
|
2655
|
+
void stopModuleFederationServer()
|
|
2656
|
+
}
|
|
2657
|
+
className="p-0.5 rounded text-slate-600 hover:text-red-400 transition-colors shrink-0"
|
|
2658
|
+
title="Stop webpack lab"
|
|
2659
|
+
>
|
|
2660
|
+
<StopCircle className="w-3 h-3" />
|
|
2661
|
+
</button>
|
|
2662
|
+
</>
|
|
2663
|
+
)}
|
|
2664
|
+
</>
|
|
2665
|
+
)}
|
|
2096
2666
|
</>
|
|
2097
2667
|
)}
|
|
2098
2668
|
</div>
|
|
@@ -2215,10 +2785,11 @@ export default function CodeRunnerModal() {
|
|
|
2215
2785
|
|
|
2216
2786
|
{/* Client body */}
|
|
2217
2787
|
<div
|
|
2218
|
-
className={`flex-1 min-h-0 ${clientType === "nextjs" ? "flex flex-row" : "relative"}`}
|
|
2788
|
+
className={`flex-1 min-h-0 ${clientType === "nextjs" || clientType === "module-federation" ? "flex flex-row" : "relative"}`}
|
|
2219
2789
|
>
|
|
2220
2790
|
{/* ── Next.js VS Code-style file tree sidebar ── */}
|
|
2221
|
-
{clientType === "nextjs"
|
|
2791
|
+
{(clientType === "nextjs" ||
|
|
2792
|
+
clientType === "module-federation") && (
|
|
2222
2793
|
<div className="w-36 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto">
|
|
2223
2794
|
{/* Sidebar header */}
|
|
2224
2795
|
<div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-700/60">
|
|
@@ -2256,7 +2827,11 @@ export default function CodeRunnerModal() {
|
|
|
2256
2827
|
setReactNewFileName("");
|
|
2257
2828
|
}
|
|
2258
2829
|
}}
|
|
2259
|
-
placeholder=
|
|
2830
|
+
placeholder={
|
|
2831
|
+
clientType === "module-federation"
|
|
2832
|
+
? "apps/orders/src/App.jsx"
|
|
2833
|
+
: "app/new.tsx"
|
|
2834
|
+
}
|
|
2260
2835
|
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
2836
|
/>
|
|
2262
2837
|
) : (
|
|
@@ -2264,7 +2839,11 @@ export default function CodeRunnerModal() {
|
|
|
2264
2839
|
type="button"
|
|
2265
2840
|
onClick={() => setReactAddingFile(true)}
|
|
2266
2841
|
className="p-0.5 rounded text-slate-600 hover:text-cyan-400 transition-colors"
|
|
2267
|
-
title=
|
|
2842
|
+
title={
|
|
2843
|
+
clientType === "module-federation"
|
|
2844
|
+
? "New file (use paths like apps/orders/src/App.jsx)"
|
|
2845
|
+
: "New file (use paths like app/dashboard/page.tsx)"
|
|
2846
|
+
}
|
|
2268
2847
|
>
|
|
2269
2848
|
<FilePlus className="w-3 h-3" />
|
|
2270
2849
|
</button>
|
|
@@ -2273,27 +2852,18 @@ export default function CodeRunnerModal() {
|
|
|
2273
2852
|
{/* Tree nodes */}
|
|
2274
2853
|
<div className="flex-1 py-1">
|
|
2275
2854
|
{(() => {
|
|
2276
|
-
|
|
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
|
-
);
|
|
2855
|
+
const tree = buildFileTree(visibleReactFiles);
|
|
2295
2856
|
|
|
2296
|
-
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
|
+
);
|
|
2297
2867
|
if (name.endsWith(".tsx") || name.endsWith(".jsx"))
|
|
2298
2868
|
return (
|
|
2299
2869
|
<span className="text-cyan-400 mr-1 text-[9px]">
|
|
@@ -2315,84 +2885,117 @@ export default function CodeRunnerModal() {
|
|
|
2315
2885
|
|
|
2316
2886
|
const renderFile = (path: string, indent = 0) => (
|
|
2317
2887
|
<div key={path} className="group flex items-center">
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
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
|
+
})()}
|
|
2359
2956
|
</div>
|
|
2360
2957
|
);
|
|
2361
2958
|
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2959
|
+
const renderNode = (
|
|
2960
|
+
node: FileTreeNode,
|
|
2961
|
+
indent = 0,
|
|
2962
|
+
): React.ReactNode => {
|
|
2963
|
+
if (!node.path) {
|
|
2964
|
+
return (
|
|
2965
|
+
<>
|
|
2966
|
+
{node.children
|
|
2967
|
+
.sort((a, b) =>
|
|
2968
|
+
a.name.localeCompare(b.name),
|
|
2969
|
+
)
|
|
2970
|
+
.map((child) => renderNode(child, 0))}
|
|
2971
|
+
{node.files
|
|
2972
|
+
.sort((a, b) => a.localeCompare(b))
|
|
2973
|
+
.map((path) => renderFile(path, 0))}
|
|
2974
|
+
</>
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
const isOpen = !collapsedFolders.has(node.path);
|
|
2381
2979
|
|
|
2382
2980
|
return (
|
|
2383
|
-
<div key={
|
|
2384
|
-
{/* Folder row */}
|
|
2981
|
+
<div key={node.path}>
|
|
2385
2982
|
<button
|
|
2386
2983
|
type="button"
|
|
2387
2984
|
onClick={() =>
|
|
2388
2985
|
setCollapsedFolders((prev) => {
|
|
2389
2986
|
const next = new Set(prev);
|
|
2390
|
-
if (next.has(
|
|
2391
|
-
|
|
2987
|
+
if (next.has(node.path)) {
|
|
2988
|
+
next.delete(node.path);
|
|
2989
|
+
} else {
|
|
2990
|
+
next.add(node.path);
|
|
2991
|
+
}
|
|
2392
2992
|
return next;
|
|
2393
2993
|
})
|
|
2394
2994
|
}
|
|
2395
|
-
|
|
2995
|
+
style={{
|
|
2996
|
+
paddingLeft: `${8 + indent * 10}px`,
|
|
2997
|
+
}}
|
|
2998
|
+
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
2999
|
>
|
|
2397
3000
|
{isOpen ? (
|
|
2398
3001
|
<ChevronDown className="w-2.5 h-2.5 shrink-0 text-slate-500" />
|
|
@@ -2402,68 +3005,29 @@ export default function CodeRunnerModal() {
|
|
|
2402
3005
|
<span className="text-yellow-300/80 mr-0.5">
|
|
2403
3006
|
📁
|
|
2404
3007
|
</span>
|
|
2405
|
-
<span className="truncate">{
|
|
3008
|
+
<span className="truncate">{node.name}/</span>
|
|
2406
3009
|
</button>
|
|
2407
|
-
{/* Children */}
|
|
2408
3010
|
{isOpen && (
|
|
2409
3011
|
<div>
|
|
2410
|
-
{
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
)
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
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))}
|
|
3012
|
+
{node.children
|
|
3013
|
+
.sort((a, b) =>
|
|
3014
|
+
a.name.localeCompare(b.name),
|
|
3015
|
+
)
|
|
3016
|
+
.map((child) =>
|
|
3017
|
+
renderNode(child, indent + 1),
|
|
3018
|
+
)}
|
|
3019
|
+
{node.files
|
|
3020
|
+
.sort((a, b) => a.localeCompare(b))
|
|
3021
|
+
.map((path) =>
|
|
3022
|
+
renderFile(path, indent + 1),
|
|
3023
|
+
)}
|
|
2455
3024
|
</div>
|
|
2456
3025
|
)}
|
|
2457
3026
|
</div>
|
|
2458
3027
|
);
|
|
2459
3028
|
};
|
|
2460
3029
|
|
|
2461
|
-
return (
|
|
2462
|
-
<>
|
|
2463
|
-
{folders.map(renderFolder)}
|
|
2464
|
-
{rootFiles.map((f) => renderFile(f, 0))}
|
|
2465
|
-
</>
|
|
2466
|
-
);
|
|
3030
|
+
return renderNode(tree);
|
|
2467
3031
|
})()}
|
|
2468
3032
|
</div>
|
|
2469
3033
|
</div>
|
|
@@ -2471,7 +3035,7 @@ export default function CodeRunnerModal() {
|
|
|
2471
3035
|
|
|
2472
3036
|
{/* ── Editor / Preview area ── */}
|
|
2473
3037
|
<div
|
|
2474
|
-
className={`${clientType === "nextjs" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
|
|
3038
|
+
className={`${clientType === "nextjs" || clientType === "module-federation" ? "flex-1 min-w-0 relative" : "absolute inset-0"}`}
|
|
2475
3039
|
>
|
|
2476
3040
|
{clientType === "script" ? (
|
|
2477
3041
|
<SyntaxEditor
|
|
@@ -2488,31 +3052,52 @@ export default function CodeRunnerModal() {
|
|
|
2488
3052
|
}
|
|
2489
3053
|
/>
|
|
2490
3054
|
) : reactClientTab === "edit" ? (
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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
|
+
)
|
|
2510
3097
|
) : (
|
|
2511
|
-
// Preview area — URL bar for Next.js, plain iframe for React
|
|
2512
3098
|
<div className="w-full h-full flex flex-col">
|
|
2513
3099
|
{clientType === "nextjs" && (
|
|
2514
3100
|
<div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0">
|
|
2515
|
-
{/* Back */}
|
|
2516
3101
|
<button
|
|
2517
3102
|
type="button"
|
|
2518
3103
|
disabled={reactNavIndex <= 0}
|
|
@@ -2535,7 +3120,6 @@ export default function CodeRunnerModal() {
|
|
|
2535
3120
|
>
|
|
2536
3121
|
<ChevronLeft className="w-3.5 h-3.5" />
|
|
2537
3122
|
</button>
|
|
2538
|
-
{/* Forward */}
|
|
2539
3123
|
<button
|
|
2540
3124
|
type="button"
|
|
2541
3125
|
disabled={
|
|
@@ -2560,7 +3144,6 @@ export default function CodeRunnerModal() {
|
|
|
2560
3144
|
>
|
|
2561
3145
|
<ChevronRight className="w-3.5 h-3.5" />
|
|
2562
3146
|
</button>
|
|
2563
|
-
{/* Refresh */}
|
|
2564
3147
|
<button
|
|
2565
3148
|
type="button"
|
|
2566
3149
|
onClick={() => {
|
|
@@ -2582,7 +3165,6 @@ export default function CodeRunnerModal() {
|
|
|
2582
3165
|
<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
3166
|
</svg>
|
|
2584
3167
|
</button>
|
|
2585
|
-
{/* URL bar */}
|
|
2586
3168
|
<form
|
|
2587
3169
|
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
3170
|
onSubmit={(e) => {
|
|
@@ -2621,7 +3203,6 @@ export default function CodeRunnerModal() {
|
|
|
2621
3203
|
spellCheck={false}
|
|
2622
3204
|
/>
|
|
2623
3205
|
</form>
|
|
2624
|
-
{/* Status indicator */}
|
|
2625
3206
|
{nxSandboxUrl ? (
|
|
2626
3207
|
<span className="text-[9px] font-mono text-green-400 shrink-0">
|
|
2627
3208
|
● live
|
|
@@ -2648,49 +3229,103 @@ export default function CodeRunnerModal() {
|
|
|
2648
3229
|
)}
|
|
2649
3230
|
</div>
|
|
2650
3231
|
)}
|
|
2651
|
-
{
|
|
2652
|
-
|
|
3232
|
+
{clientType === "module-federation" && (
|
|
3233
|
+
<div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0 overflow-x-auto">
|
|
3234
|
+
{Object.entries(mfAppUrls).map(([name, url]) => (
|
|
3235
|
+
<button
|
|
3236
|
+
key={name}
|
|
3237
|
+
type="button"
|
|
3238
|
+
onClick={() => setMfPreviewApp(name)}
|
|
3239
|
+
className={`px-2 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 ${
|
|
3240
|
+
mfPreviewApp === name
|
|
3241
|
+
? "bg-slate-700 text-slate-100"
|
|
3242
|
+
: "text-slate-500 hover:text-slate-300 hover:bg-slate-700/40"
|
|
3243
|
+
}`}
|
|
3244
|
+
title={url}
|
|
3245
|
+
>
|
|
3246
|
+
{name}
|
|
3247
|
+
</button>
|
|
3248
|
+
))}
|
|
3249
|
+
<div className="ml-auto text-[9px] font-mono text-slate-600 shrink-0">
|
|
3250
|
+
{mfAppUrls[mfPreviewApp] ??
|
|
3251
|
+
mfHostUrl ??
|
|
3252
|
+
"Start webpack to preview"}
|
|
3253
|
+
</div>
|
|
3254
|
+
</div>
|
|
3255
|
+
)}
|
|
3256
|
+
{((clientType === "module-federation" && mfError) ||
|
|
3257
|
+
(clientType !== "module-federation" && nxError)) && (
|
|
2653
3258
|
<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
|
-
{
|
|
3259
|
+
{clientType === "module-federation"
|
|
3260
|
+
? mfError
|
|
3261
|
+
: nxError}
|
|
2655
3262
|
</div>
|
|
2656
3263
|
)}
|
|
2657
|
-
{
|
|
2658
|
-
{nxStarting && (
|
|
3264
|
+
{(nxStarting || mfStarting) && (
|
|
2659
3265
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-slate-400 text-sm bg-slate-950">
|
|
2660
3266
|
<Loader2 className="w-8 h-8 animate-spin text-cyan-400" />
|
|
2661
3267
|
<p className="text-[12px]">
|
|
2662
|
-
|
|
3268
|
+
{clientType === "module-federation"
|
|
3269
|
+
? "Installing dependencies and starting webpack apps…"
|
|
3270
|
+
: "Starting Next.js dev server…"}
|
|
2663
3271
|
</p>
|
|
2664
|
-
<p className="text-[10px] text-slate-600">
|
|
2665
|
-
|
|
3272
|
+
<p className="text-[10px] text-slate-600 max-w-md text-center px-4">
|
|
3273
|
+
{clientType === "module-federation"
|
|
3274
|
+
? "The first run can take a little longer because npm install runs inside the lab sandbox."
|
|
3275
|
+
: "This takes ~10 seconds on the first run"}
|
|
2666
3276
|
</p>
|
|
2667
3277
|
</div>
|
|
2668
3278
|
)}
|
|
2669
|
-
{
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
3279
|
+
{!nxStarting &&
|
|
3280
|
+
clientType === "nextjs" &&
|
|
3281
|
+
nxSandboxUrl && (
|
|
3282
|
+
<iframe
|
|
3283
|
+
ref={nxIframeRef}
|
|
3284
|
+
src={nxSandboxUrl + reactPreviewPath}
|
|
3285
|
+
className="flex-1 min-h-0 w-full border-0 bg-white"
|
|
3286
|
+
title="Next.js Preview"
|
|
3287
|
+
onLoad={() => {
|
|
3288
|
+
try {
|
|
3289
|
+
const p =
|
|
3290
|
+
nxIframeRef.current?.contentWindow?.location
|
|
3291
|
+
.pathname;
|
|
3292
|
+
if (p) {
|
|
3293
|
+
setReactPreviewPath(p);
|
|
3294
|
+
setReactNavInput(p);
|
|
3295
|
+
}
|
|
3296
|
+
} catch {
|
|
3297
|
+
// cross-origin — ignore
|
|
2685
3298
|
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
3299
|
+
}}
|
|
3300
|
+
/>
|
|
3301
|
+
)}
|
|
3302
|
+
{!mfStarting &&
|
|
3303
|
+
clientType === "module-federation" &&
|
|
3304
|
+
mfSandboxId &&
|
|
3305
|
+
(mfAppUrls[mfPreviewApp] ?? mfHostUrl) && (
|
|
3306
|
+
<iframe
|
|
3307
|
+
src={mfAppUrls[mfPreviewApp] ?? mfHostUrl ?? ""}
|
|
3308
|
+
className="flex-1 min-h-0 w-full border-0 bg-white"
|
|
3309
|
+
title="Webpack Module Federation Preview"
|
|
3310
|
+
/>
|
|
3311
|
+
)}
|
|
3312
|
+
{!mfStarting &&
|
|
3313
|
+
clientType === "module-federation" &&
|
|
3314
|
+
!mfSandboxId && (
|
|
3315
|
+
<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">
|
|
3316
|
+
<Server className="w-8 h-8 text-cyan-400/70" />
|
|
3317
|
+
<p className="text-[12px]">
|
|
3318
|
+
Start the webpack lab to launch the host and
|
|
3319
|
+
remote dev servers.
|
|
3320
|
+
</p>
|
|
3321
|
+
<p className="text-[10px] text-slate-600 max-w-md">
|
|
3322
|
+
The preview lets you switch between the host and
|
|
3323
|
+
each remote so you can inspect their output
|
|
3324
|
+
independently.
|
|
3325
|
+
</p>
|
|
3326
|
+
</div>
|
|
3327
|
+
)}
|
|
3328
|
+
{!nxStarting && clientType === "react" && (
|
|
2694
3329
|
<iframe
|
|
2695
3330
|
srcDoc={reactPreviewSrc ?? ""}
|
|
2696
3331
|
sandbox="allow-scripts"
|
|
@@ -2741,6 +3376,20 @@ export default function CodeRunnerModal() {
|
|
|
2741
3376
|
) : null}
|
|
2742
3377
|
Output
|
|
2743
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
|
+
)}
|
|
2744
3393
|
<button
|
|
2745
3394
|
type="button"
|
|
2746
3395
|
onClick={() => {
|
|
@@ -2761,6 +3410,9 @@ export default function CodeRunnerModal() {
|
|
|
2761
3410
|
(serverStarting || clientRunning) && (
|
|
2762
3411
|
<Loader2 className="w-3 h-3 text-emerald-400 animate-spin mr-1" />
|
|
2763
3412
|
)}
|
|
3413
|
+
{sbxBottomTab === "console" && mfConsoleRunning && (
|
|
3414
|
+
<Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
|
|
3415
|
+
)}
|
|
2764
3416
|
{sbxBottomTab === "output" && sandboxOutput.length > 0 && (
|
|
2765
3417
|
<div className="flex items-center gap-1 mr-1">
|
|
2766
3418
|
<button
|
|
@@ -2785,6 +3437,30 @@ export default function CodeRunnerModal() {
|
|
|
2785
3437
|
</button>
|
|
2786
3438
|
</div>
|
|
2787
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
|
+
)}
|
|
2788
3464
|
{sbxBottomTab === "chat" && sbxChatMessages.length > 0 && (
|
|
2789
3465
|
<button
|
|
2790
3466
|
type="button"
|
|
@@ -2815,7 +3491,9 @@ export default function CodeRunnerModal() {
|
|
|
2815
3491
|
!serverStarting &&
|
|
2816
3492
|
!clientRunning && (
|
|
2817
3493
|
<span className="text-slate-600">
|
|
2818
|
-
|
|
3494
|
+
{clientType === "module-federation"
|
|
3495
|
+
? "Run webpack to start the host and remotes"
|
|
3496
|
+
: "Start the server, then run the client"}
|
|
2819
3497
|
</span>
|
|
2820
3498
|
)}
|
|
2821
3499
|
{sandboxOutput.map((line, i) => (
|
|
@@ -2854,6 +3532,98 @@ export default function CodeRunnerModal() {
|
|
|
2854
3532
|
</div>
|
|
2855
3533
|
)}
|
|
2856
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
|
+
|
|
2857
3627
|
{/* Chat tab */}
|
|
2858
3628
|
{sbxBottomTab === "chat" && (
|
|
2859
3629
|
<>
|
|
@@ -2864,9 +3634,13 @@ export default function CodeRunnerModal() {
|
|
|
2864
3634
|
{sbxChatMessages.length === 0 && (
|
|
2865
3635
|
<p className="text-xs text-slate-600 pt-1">
|
|
2866
3636
|
Ask anything about your code —{" "}
|
|
2867
|
-
{clientType === "react" ||
|
|
3637
|
+
{clientType === "react" ||
|
|
3638
|
+
clientType === "nextjs" ||
|
|
3639
|
+
clientType === "module-federation" ? (
|
|
2868
3640
|
<span className="text-slate-500">
|
|
2869
|
-
|
|
3641
|
+
{clientType === "module-federation"
|
|
3642
|
+
? '"Why is remoteEntry.js 404ing?"'
|
|
3643
|
+
: '"Why does my useEffect run twice?"'}
|
|
2870
3644
|
</span>
|
|
2871
3645
|
) : (
|
|
2872
3646
|
<span className="text-slate-500">
|
|
@@ -3002,7 +3776,7 @@ export default function CodeRunnerModal() {
|
|
|
3002
3776
|
void handleSbxChatSend();
|
|
3003
3777
|
}
|
|
3004
3778
|
}}
|
|
3005
|
-
placeholder={`Ask about your ${clientType === "react" ? "React" : clientType === "nextjs" ? "Next.js" : "sandbox"} code…`}
|
|
3779
|
+
placeholder={`Ask about your ${clientType === "react" ? "React" : clientType === "nextjs" ? "Next.js" : clientType === "module-federation" ? "webpack module federation" : "sandbox"} code…`}
|
|
3006
3780
|
disabled={sbxChatLoading}
|
|
3007
3781
|
className="flex-1 bg-transparent text-xs text-slate-200 placeholder-slate-600 outline-none resize-none disabled:opacity-50 max-h-20"
|
|
3008
3782
|
/>
|