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.
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.4.0"
2
+ "version": "0.6.0"
3
3
  }
@@ -360,6 +360,7 @@ export async function syncWorkspace(
360
360
  cs.origin === "sandbox" ||
361
361
  cs.origin === "react" ||
362
362
  cs.origin === "nextjs" ||
363
+ cs.origin === "module-federation" ||
363
364
  cs.origin === "infra")
364
365
  ) {
365
366
  try {
@@ -770,6 +771,7 @@ export async function exportWorkspace(
770
771
  cf.origin === "sandbox" ||
771
772
  cf.origin === "react" ||
772
773
  cf.origin === "nextjs" ||
774
+ cf.origin === "module-federation" ||
773
775
  cf.origin === "infra"
774
776
  ) {
775
777
  try {
@@ -690,7 +690,14 @@ app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
690
690
  code: string;
691
691
  language: string;
692
692
  label: string;
693
- origin: "user" | "ai" | "sandbox" | "infra" | "react" | "nextjs";
693
+ origin:
694
+ | "user"
695
+ | "ai"
696
+ | "sandbox"
697
+ | "infra"
698
+ | "react"
699
+ | "nextjs"
700
+ | "module-federation";
694
701
  };
695
702
  if (typeof code !== "string" || !code.trim()) {
696
703
  return res.status(400).json({ error: "code is required" });
@@ -701,11 +708,12 @@ app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
701
708
  origin !== "sandbox" &&
702
709
  origin !== "infra" &&
703
710
  origin !== "react" &&
704
- origin !== "nextjs"
711
+ origin !== "nextjs" &&
712
+ origin !== "module-federation"
705
713
  ) {
706
714
  return res.status(400).json({
707
715
  error:
708
- "origin must be 'user', 'ai', 'sandbox', 'infra', 'react', or 'nextjs'",
716
+ "origin must be 'user', 'ai', 'sandbox', 'infra', 'react', 'nextjs', or 'module-federation'",
709
717
  });
710
718
  }
711
719
  try {
@@ -994,7 +1002,7 @@ app.post("/api/frontend-lab/ask", async (req, res) => {
994
1002
  const { messages, workspace, labType, questionId } = req.body as {
995
1003
  messages?: Array<{ role: "user" | "assistant"; content: string }>;
996
1004
  workspace?: Record<string, string>;
997
- labType?: "react" | "nextjs";
1005
+ labType?: "react" | "nextjs" | "module-federation";
998
1006
  questionId?: string;
999
1007
  };
1000
1008
 
@@ -1008,7 +1016,11 @@ app.post("/api/frontend-lab/ask", async (req, res) => {
1008
1016
  const aiSettings = await storage.getAiSettings();
1009
1017
 
1010
1018
  const typeLabel =
1011
- labType === "nextjs" ? "Next.js App Router" : "React + TypeScript";
1019
+ labType === "nextjs"
1020
+ ? "Next.js App Router"
1021
+ : labType === "module-federation"
1022
+ ? "Webpack Module Federation"
1023
+ : "React + TypeScript";
1012
1024
 
1013
1025
  let workspaceBlock = "";
1014
1026
  if (workspace && typeof workspace === "object") {
@@ -2280,6 +2292,346 @@ const NEXT_MODULES_DIR = path.join(NEXT_NPX_DIR, "node_modules");
2280
2292
  // walking up the directory tree — no symlinks needed, no Turbopack restrictions.
2281
2293
  const NEXT_SANDBOX_BASE = path.join(NEXT_NPX_DIR, ".sandboxes");
2282
2294
 
2295
+ interface ModuleFederationSandboxEntry {
2296
+ child: ReturnType<typeof spawn>;
2297
+ dir: string;
2298
+ hostUrl: string;
2299
+ appUrls: Record<string, string>;
2300
+ workspaceFiles: Set<string>;
2301
+ logs: string[];
2302
+ ready: boolean;
2303
+ }
2304
+
2305
+ const moduleFederationSandboxes = new Map<
2306
+ string,
2307
+ ModuleFederationSandboxEntry
2308
+ >();
2309
+ const MODULE_FEDERATION_SANDBOX_BASE = path.join(
2310
+ os.tmpdir(),
2311
+ "interview-cockpit-module-federation",
2312
+ );
2313
+
2314
+ function appendSandboxLog(logs: string[], text: string): string {
2315
+ const clean = text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
2316
+ logs.push(clean);
2317
+ if (logs.length > 400) {
2318
+ logs.splice(0, logs.length - 400);
2319
+ }
2320
+ return clean;
2321
+ }
2322
+
2323
+ function npmCommand(): string {
2324
+ return process.platform === "win32" ? "npm.cmd" : "npm";
2325
+ }
2326
+
2327
+ async function writeModuleFederationSandboxFiles(
2328
+ dir: string,
2329
+ files: Record<string, string>,
2330
+ ) {
2331
+ for (const [filePath, content] of Object.entries(files)) {
2332
+ const fullPath = path.join(dir, filePath);
2333
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
2334
+ await fs.writeFile(fullPath, content, "utf8");
2335
+ }
2336
+ }
2337
+
2338
+ async function runLoggedCommand(
2339
+ command: string,
2340
+ args: string[],
2341
+ options: { cwd: string; env?: NodeJS.ProcessEnv },
2342
+ logs: string[],
2343
+ ): Promise<void> {
2344
+ await new Promise<void>((resolve, reject) => {
2345
+ const child = spawn(command, args, {
2346
+ cwd: options.cwd,
2347
+ env: options.env,
2348
+ });
2349
+
2350
+ child.stdout.on("data", (chunk: Buffer) => {
2351
+ appendSandboxLog(logs, chunk.toString());
2352
+ });
2353
+ child.stderr.on("data", (chunk: Buffer) => {
2354
+ appendSandboxLog(logs, chunk.toString());
2355
+ });
2356
+ child.on("error", reject);
2357
+ child.on("exit", (code) => {
2358
+ if (code === 0) {
2359
+ resolve();
2360
+ return;
2361
+ }
2362
+ reject(
2363
+ new Error(
2364
+ logs.join("").trim() ||
2365
+ `${command} ${args.join(" ")} exited with code ${String(code)}`,
2366
+ ),
2367
+ );
2368
+ });
2369
+ });
2370
+ }
2371
+
2372
+ type ModuleFederationCommandOutputKind = "stdout" | "stderr" | "info";
2373
+
2374
+ const MODULE_FEDERATION_SHELL_META_TOKENS = new Set([
2375
+ "&&",
2376
+ "||",
2377
+ ";",
2378
+ "|",
2379
+ ">",
2380
+ ">>",
2381
+ "<",
2382
+ "2>",
2383
+ "&",
2384
+ ]);
2385
+
2386
+ function splitShellLikeCommand(command: string): string[] {
2387
+ const tokens: string[] = [];
2388
+ let current = "";
2389
+ let quote: '"' | "'" | null = null;
2390
+ let escape = false;
2391
+
2392
+ for (const char of command.trim()) {
2393
+ if (escape) {
2394
+ current += char;
2395
+ escape = false;
2396
+ continue;
2397
+ }
2398
+
2399
+ if (char === "\\") {
2400
+ escape = true;
2401
+ continue;
2402
+ }
2403
+
2404
+ if (quote) {
2405
+ if (char === quote) {
2406
+ quote = null;
2407
+ } else {
2408
+ current += char;
2409
+ }
2410
+ continue;
2411
+ }
2412
+
2413
+ if (char === '"' || char === "'") {
2414
+ quote = char;
2415
+ continue;
2416
+ }
2417
+
2418
+ if (/\s/.test(char)) {
2419
+ if (current) {
2420
+ tokens.push(current);
2421
+ current = "";
2422
+ }
2423
+ continue;
2424
+ }
2425
+
2426
+ current += char;
2427
+ }
2428
+
2429
+ if (escape) current += "\\";
2430
+ if (quote) throw new Error("Command has an unclosed quote");
2431
+ if (current) tokens.push(current);
2432
+ return tokens;
2433
+ }
2434
+
2435
+ function normalizeSandboxRelativePath(filePath: string, label: string): string {
2436
+ const normalized = filePath.replace(/\\/g, "/").trim();
2437
+ if (!normalized || normalized === ".") {
2438
+ return "";
2439
+ }
2440
+ if (path.isAbsolute(normalized)) {
2441
+ throw new Error(`${label} must be relative to the webpack lab root`);
2442
+ }
2443
+
2444
+ const parts = normalized.split("/").filter(Boolean);
2445
+ if (parts.some((part) => part === "." || part === "..")) {
2446
+ throw new Error(`${label} must stay inside the webpack lab root`);
2447
+ }
2448
+
2449
+ return parts.join("/");
2450
+ }
2451
+
2452
+ function isPathInside(root: string, target: string): boolean {
2453
+ const relative = path.relative(root, target);
2454
+ return (
2455
+ relative === "" ||
2456
+ (!relative.startsWith("..") && !path.isAbsolute(relative))
2457
+ );
2458
+ }
2459
+
2460
+ function parseModuleFederationCommand(command: string): {
2461
+ args: string[];
2462
+ displayCommand: string;
2463
+ } {
2464
+ const tokens = splitShellLikeCommand(command);
2465
+ if (tokens.length < 3 || tokens[0] !== "npm" || tokens[1] !== "run") {
2466
+ throw new Error(
2467
+ "Only npm run <script> commands are allowed in the webpack lab console",
2468
+ );
2469
+ }
2470
+
2471
+ if (tokens.some((token) => MODULE_FEDERATION_SHELL_META_TOKENS.has(token))) {
2472
+ throw new Error(
2473
+ "Shell operators are not supported here. Run one npm command at a time.",
2474
+ );
2475
+ }
2476
+
2477
+ if (
2478
+ tokens.some(
2479
+ (token) => token === "--prefix" || token.startsWith("--prefix="),
2480
+ )
2481
+ ) {
2482
+ throw new Error(
2483
+ "Use the folder selector instead of npm --prefix in the webpack lab console.",
2484
+ );
2485
+ }
2486
+
2487
+ return {
2488
+ args: tokens.slice(1),
2489
+ displayCommand: `$ ${tokens.join(" ")}`,
2490
+ };
2491
+ }
2492
+
2493
+ async function resolveModuleFederationCommandCwd(
2494
+ sandboxDir: string,
2495
+ cwd: string | undefined,
2496
+ ): Promise<{ normalized: string; fullPath: string }> {
2497
+ const normalized = normalizeSandboxRelativePath(
2498
+ cwd ?? "",
2499
+ "Command directory",
2500
+ );
2501
+ const fullPath = path.resolve(sandboxDir, normalized || ".");
2502
+
2503
+ if (!isPathInside(path.resolve(sandboxDir), fullPath)) {
2504
+ throw new Error(
2505
+ "Command directory must stay inside the webpack lab sandbox",
2506
+ );
2507
+ }
2508
+
2509
+ const stats = await fs.stat(fullPath).catch(() => null);
2510
+ if (!stats?.isDirectory()) {
2511
+ throw new Error(`Command directory not found: ${normalized || "."}`);
2512
+ }
2513
+
2514
+ return { normalized, fullPath };
2515
+ }
2516
+
2517
+ function getModuleFederationCommandEnv(
2518
+ sandbox: ModuleFederationSandboxEntry,
2519
+ ): NodeJS.ProcessEnv {
2520
+ const hostPort = new URL(sandbox.appUrls.host).port;
2521
+ const profilePort = new URL(sandbox.appUrls.profile).port;
2522
+ const checkoutPort = new URL(sandbox.appUrls.checkout).port;
2523
+
2524
+ return {
2525
+ ...process.env,
2526
+ HOST_PORT: hostPort,
2527
+ PROFILE_PORT: profilePort,
2528
+ CHECKOUT_PORT: checkoutPort,
2529
+ npm_config_update_notifier: "false",
2530
+ };
2531
+ }
2532
+
2533
+ async function runStreamedCommand(
2534
+ command: string,
2535
+ args: string[],
2536
+ options: { cwd: string; env?: NodeJS.ProcessEnv },
2537
+ onChunk: (payload: {
2538
+ kind: ModuleFederationCommandOutputKind;
2539
+ text: string;
2540
+ }) => void,
2541
+ ): Promise<void> {
2542
+ await new Promise<void>((resolve, reject) => {
2543
+ const stdout: string[] = [];
2544
+ const stderr: string[] = [];
2545
+ const child = spawn(command, args, {
2546
+ cwd: options.cwd,
2547
+ env: options.env,
2548
+ });
2549
+
2550
+ child.stdout.on("data", (chunk: Buffer) => {
2551
+ const text = chunk.toString().replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
2552
+ stdout.push(text);
2553
+ onChunk({ kind: "stdout", text });
2554
+ });
2555
+ child.stderr.on("data", (chunk: Buffer) => {
2556
+ const text = chunk.toString().replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
2557
+ stderr.push(text);
2558
+ onChunk({ kind: "stderr", text });
2559
+ });
2560
+ child.on("error", (error) => {
2561
+ reject(new Error(`${command} launch failed: ${error.message}`));
2562
+ });
2563
+ child.on("close", (code) => {
2564
+ if (code === 0) {
2565
+ resolve();
2566
+ return;
2567
+ }
2568
+
2569
+ reject(
2570
+ new Error(
2571
+ stderr.join("").trim() ||
2572
+ stdout.join("").trim() ||
2573
+ `${command} ${args.join(" ")} exited with code ${String(code)}`,
2574
+ ),
2575
+ );
2576
+ });
2577
+ });
2578
+ }
2579
+
2580
+ async function walkAllFiles(dir: string, prefix = ""): Promise<string[]> {
2581
+ const entries = await fs
2582
+ .readdir(dir, { withFileTypes: true })
2583
+ .catch(() => []);
2584
+ const files: string[] = [];
2585
+
2586
+ for (const entry of entries) {
2587
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2588
+ if (entry.isDirectory()) {
2589
+ files.push(...(await walkAllFiles(path.join(dir, entry.name), rel)));
2590
+ } else {
2591
+ files.push(rel);
2592
+ }
2593
+ }
2594
+
2595
+ return files;
2596
+ }
2597
+
2598
+ async function listModuleFederationGeneratedFiles(
2599
+ dir: string,
2600
+ ): Promise<string[]> {
2601
+ const files: string[] = [];
2602
+
2603
+ const visit = async (currentDir: string, prefix = "") => {
2604
+ const entries = await fs
2605
+ .readdir(currentDir, { withFileTypes: true })
2606
+ .catch(() => []);
2607
+
2608
+ for (const entry of entries) {
2609
+ if (entry.name === "node_modules") continue;
2610
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2611
+ const fullPath = path.join(currentDir, entry.name);
2612
+
2613
+ if (entry.isDirectory()) {
2614
+ if (entry.name === "dist") {
2615
+ files.push(...(await walkAllFiles(fullPath, rel)));
2616
+ } else {
2617
+ await visit(fullPath, rel);
2618
+ }
2619
+ }
2620
+ }
2621
+ };
2622
+
2623
+ await visit(dir);
2624
+ return files.sort((a, b) => a.localeCompare(b));
2625
+ }
2626
+
2627
+ async function getDistinctPorts(count: number): Promise<number[]> {
2628
+ const ports = new Set<number>();
2629
+ while (ports.size < count) {
2630
+ ports.add(await getFreePort());
2631
+ }
2632
+ return Array.from(ports);
2633
+ }
2634
+
2283
2635
  /** Write all user files and necessary config into a sandbox directory. */
2284
2636
  async function writeNextSandboxFiles(
2285
2637
  dir: string,
@@ -2480,6 +2832,291 @@ app.delete("/api/nextjs/:id", async (req, res) => {
2480
2832
  res.json({ ok: true });
2481
2833
  });
2482
2834
 
2835
+ app.post("/api/module-federation/start", async (req, res) => {
2836
+ const { files } = req.body as { files?: Record<string, string> };
2837
+ if (!files || typeof files !== "object") {
2838
+ return res.status(400).json({ error: "files is required" });
2839
+ }
2840
+ if (typeof files["package.json"] !== "string") {
2841
+ return res.status(400).json({ error: "package.json is required" });
2842
+ }
2843
+
2844
+ const id = randomUUID();
2845
+ const dir = path.join(MODULE_FEDERATION_SANDBOX_BASE, id);
2846
+ const logs: string[] = [];
2847
+
2848
+ try {
2849
+ await fs.mkdir(dir, { recursive: true });
2850
+ await writeModuleFederationSandboxFiles(dir, files);
2851
+
2852
+ await runLoggedCommand(
2853
+ npmCommand(),
2854
+ ["install", "--no-audit", "--no-fund", "--prefer-offline"],
2855
+ {
2856
+ cwd: dir,
2857
+ env: {
2858
+ ...process.env,
2859
+ npm_config_update_notifier: "false",
2860
+ },
2861
+ },
2862
+ logs,
2863
+ );
2864
+
2865
+ const [hostPort, profilePort, checkoutPort] = await getDistinctPorts(3);
2866
+ const appUrls = {
2867
+ host: `http://localhost:${hostPort}`,
2868
+ profile: `http://localhost:${profilePort}`,
2869
+ checkout: `http://localhost:${checkoutPort}`,
2870
+ };
2871
+ const readyPorts = new Set<string>();
2872
+
2873
+ const child = spawn(npmCommand(), ["run", "dev"], {
2874
+ cwd: dir,
2875
+ env: {
2876
+ ...process.env,
2877
+ HOST_PORT: String(hostPort),
2878
+ PROFILE_PORT: String(profilePort),
2879
+ CHECKOUT_PORT: String(checkoutPort),
2880
+ npm_config_update_notifier: "false",
2881
+ },
2882
+ });
2883
+
2884
+ const entry: ModuleFederationSandboxEntry = {
2885
+ child,
2886
+ dir,
2887
+ hostUrl: appUrls.host,
2888
+ appUrls,
2889
+ workspaceFiles: new Set(Object.keys(files)),
2890
+ logs,
2891
+ ready: false,
2892
+ };
2893
+
2894
+ const markReady = (text: string) => {
2895
+ if (text.includes(`localhost:${hostPort}`)) readyPorts.add("host");
2896
+ if (text.includes(`localhost:${profilePort}`)) readyPorts.add("profile");
2897
+ if (text.includes(`localhost:${checkoutPort}`))
2898
+ readyPorts.add("checkout");
2899
+ if (readyPorts.size === 3) {
2900
+ entry.ready = true;
2901
+ }
2902
+ };
2903
+
2904
+ child.stdout.on("data", (chunk: Buffer) => {
2905
+ markReady(appendSandboxLog(logs, chunk.toString()));
2906
+ });
2907
+ child.stderr.on("data", (chunk: Buffer) => {
2908
+ markReady(appendSandboxLog(logs, chunk.toString()));
2909
+ });
2910
+ child.on("exit", () => {
2911
+ moduleFederationSandboxes.delete(id);
2912
+ fs.rm(dir, { recursive: true, force: true }).catch(() => {});
2913
+ });
2914
+
2915
+ moduleFederationSandboxes.set(id, entry);
2916
+
2917
+ const deadline = Date.now() + 90_000;
2918
+ while (!entry.ready && Date.now() < deadline) {
2919
+ await new Promise((resolve) => setTimeout(resolve, 500));
2920
+ if (!moduleFederationSandboxes.has(id)) {
2921
+ return res.status(500).json({
2922
+ error: logs.join("").trim() || "Webpack module federation lab exited",
2923
+ });
2924
+ }
2925
+ }
2926
+
2927
+ if (!entry.ready) {
2928
+ return res.status(504).json({
2929
+ error: "Webpack module federation lab did not start in time",
2930
+ logs,
2931
+ });
2932
+ }
2933
+
2934
+ res.json({
2935
+ id,
2936
+ hostUrl: entry.hostUrl,
2937
+ appUrls: entry.appUrls,
2938
+ });
2939
+ } catch (error: any) {
2940
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
2941
+ res.status(500).json({
2942
+ error:
2943
+ logs.join("").trim() ||
2944
+ error?.message ||
2945
+ "Failed to start webpack module federation lab",
2946
+ });
2947
+ }
2948
+ });
2949
+
2950
+ app.post("/api/module-federation/:id/update-files", async (req, res) => {
2951
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
2952
+ if (!sandbox) return res.status(404).json({ error: "Sandbox not found" });
2953
+
2954
+ const { files } = req.body as { files?: Record<string, string> };
2955
+ if (!files || typeof files !== "object") {
2956
+ return res.status(400).json({ error: "files is required" });
2957
+ }
2958
+
2959
+ const nextFiles = new Set(Object.keys(files));
2960
+ await Promise.all(
2961
+ Array.from(sandbox.workspaceFiles)
2962
+ .filter((filePath) => !nextFiles.has(filePath))
2963
+ .map((filePath) =>
2964
+ fs
2965
+ .rm(path.join(sandbox.dir, filePath), { force: true })
2966
+ .catch(() => {}),
2967
+ ),
2968
+ );
2969
+ await writeModuleFederationSandboxFiles(sandbox.dir, files);
2970
+ sandbox.workspaceFiles = nextFiles;
2971
+ res.json({ ok: true });
2972
+ });
2973
+
2974
+ app.get("/api/module-federation/:id/status", (req, res) => {
2975
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
2976
+ if (!sandbox) {
2977
+ return res.json({ running: false });
2978
+ }
2979
+
2980
+ res.setHeader("Cache-Control", "no-store");
2981
+
2982
+ res.json({
2983
+ running: true,
2984
+ ready: sandbox.ready,
2985
+ hostUrl: sandbox.hostUrl,
2986
+ appUrls: sandbox.appUrls,
2987
+ logs: sandbox.logs.slice(-80),
2988
+ });
2989
+ });
2990
+
2991
+ app.post("/api/module-federation/:id/command-stream", async (req, res) => {
2992
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
2993
+
2994
+ res.setHeader("Content-Type", "text/event-stream");
2995
+ res.setHeader("Cache-Control", "no-cache");
2996
+ res.setHeader("Connection", "keep-alive");
2997
+ res.flushHeaders();
2998
+
2999
+ const send = (payload: unknown) => {
3000
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
3001
+ };
3002
+
3003
+ if (!sandbox) {
3004
+ send({ type: "error", error: "Sandbox not found" });
3005
+ res.end();
3006
+ return;
3007
+ }
3008
+
3009
+ const { command, cwd } = req.body as { command?: string; cwd?: string };
3010
+
3011
+ if (typeof command !== "string" || !command.trim()) {
3012
+ send({ type: "error", error: "command is required" });
3013
+ res.end();
3014
+ return;
3015
+ }
3016
+
3017
+ try {
3018
+ const parsed = parseModuleFederationCommand(command);
3019
+ const resolvedCwd = await resolveModuleFederationCommandCwd(
3020
+ sandbox.dir,
3021
+ cwd,
3022
+ );
3023
+
3024
+ send({
3025
+ type: "output",
3026
+ kind: "info",
3027
+ text: `cwd: ${resolvedCwd.normalized || "."}\n${parsed.displayCommand}\n`,
3028
+ });
3029
+
3030
+ await runStreamedCommand(
3031
+ npmCommand(),
3032
+ parsed.args,
3033
+ {
3034
+ cwd: resolvedCwd.fullPath,
3035
+ env: getModuleFederationCommandEnv(sandbox),
3036
+ },
3037
+ ({ kind, text }) => {
3038
+ send({ type: "output", kind, text });
3039
+ },
3040
+ );
3041
+
3042
+ send({ type: "complete" });
3043
+ } catch (error: any) {
3044
+ send({
3045
+ type: "error",
3046
+ error: error?.message || "Failed to run webpack lab command",
3047
+ });
3048
+ }
3049
+
3050
+ res.end();
3051
+ });
3052
+
3053
+ app.get("/api/module-federation/:id/generated-files", async (req, res) => {
3054
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
3055
+ if (!sandbox) {
3056
+ return res.status(404).json({ error: "Sandbox not found" });
3057
+ }
3058
+
3059
+ res.setHeader("Cache-Control", "no-store");
3060
+
3061
+ try {
3062
+ const files = await listModuleFederationGeneratedFiles(sandbox.dir);
3063
+ res.json({ files });
3064
+ } catch (error: any) {
3065
+ res.status(500).json({
3066
+ error: error?.message || "Failed to list webpack lab generated files",
3067
+ });
3068
+ }
3069
+ });
3070
+
3071
+ app.get("/api/module-federation/:id/generated-file", async (req, res) => {
3072
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
3073
+ if (!sandbox) {
3074
+ return res.status(404).json({ error: "Sandbox not found" });
3075
+ }
3076
+
3077
+ res.setHeader("Cache-Control", "no-store");
3078
+
3079
+ const filePath =
3080
+ typeof req.query.path === "string" ? req.query.path : undefined;
3081
+ if (!filePath) {
3082
+ return res.status(400).json({ error: "path is required" });
3083
+ }
3084
+
3085
+ try {
3086
+ const normalized = normalizeSandboxRelativePath(
3087
+ filePath,
3088
+ "Generated file path",
3089
+ );
3090
+ if (!/(^|\/)dist\//.test(normalized)) {
3091
+ return res.status(400).json({
3092
+ error: "Only generated dist files can be read here",
3093
+ });
3094
+ }
3095
+
3096
+ const fullPath = path.resolve(sandbox.dir, normalized);
3097
+ if (!isPathInside(path.resolve(sandbox.dir), fullPath)) {
3098
+ return res.status(403).json({ error: "Access denied" });
3099
+ }
3100
+
3101
+ const content = await fs.readFile(fullPath, "utf8");
3102
+ res.json({ path: normalized, content });
3103
+ } catch (error: any) {
3104
+ res.status(500).json({
3105
+ error: error?.message || "Failed to read webpack lab generated file",
3106
+ });
3107
+ }
3108
+ });
3109
+
3110
+ app.delete("/api/module-federation/:id", async (req, res) => {
3111
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
3112
+ if (sandbox) {
3113
+ sandbox.child.kill("SIGTERM");
3114
+ moduleFederationSandboxes.delete(req.params.id);
3115
+ await fs.rm(sandbox.dir, { recursive: true, force: true }).catch(() => {});
3116
+ }
3117
+ res.json({ ok: true });
3118
+ });
3119
+
2483
3120
  async function getFreePort(): Promise<number> {
2484
3121
  return new Promise((resolve, reject) => {
2485
3122
  const srv = net.createServer();