create-interview-cockpit 0.13.0 → 0.14.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-interview-cockpit",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Scaffold a personal AI-powered interview prep cockpit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,10 @@
1
1
  import { useState } from "react";
2
2
  import { useStore } from "../store";
3
3
  import { parseInfraLabWorkspace } from "../infraLab";
4
- import { parseFrontendLabWorkspace } from "../reactLab";
4
+ import {
5
+ parseFrontendLabWorkspace,
6
+ ISOLATED_MODULE_FEDERATION_LAB,
7
+ } from "../reactLab";
5
8
  import type { ContextFile } from "../types";
6
9
  import {
7
10
  Plus,
@@ -379,6 +382,7 @@ export default function LabsPanel() {
379
382
  emptyText,
380
383
  onNewLab,
381
384
  newLabTitle,
385
+ newLabMenu,
382
386
  onOpen,
383
387
  openTitle,
384
388
  accentClass,
@@ -391,11 +395,17 @@ export default function LabsPanel() {
391
395
  emptyText: string;
392
396
  onNewLab?: () => void;
393
397
  newLabTitle?: string;
398
+ newLabMenu?: Array<{
399
+ label: string;
400
+ description: string;
401
+ onClick: () => void;
402
+ }>;
394
403
  onOpen: (cf: ContextFile) => void;
395
404
  openTitle: string;
396
405
  accentClass: string;
397
406
  bgClass: string;
398
407
  }) {
408
+ const [menuOpen, setMenuOpen] = useState(false);
399
409
  const items = byOrigin(origin);
400
410
  if (!currentQuestion) return null;
401
411
 
@@ -408,7 +418,45 @@ export default function LabsPanel() {
408
418
  {title} ({items.length})
409
419
  </span>
410
420
  </div>
411
- {onNewLab && (
421
+ {newLabMenu ? (
422
+ <div className="relative">
423
+ <button
424
+ onClick={() => setMenuOpen((o) => !o)}
425
+ className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
426
+ title="New lab"
427
+ >
428
+ <Plus className="w-3.5 h-3.5" />
429
+ </button>
430
+ {menuOpen && (
431
+ <>
432
+ {/* backdrop to close on outside click */}
433
+ <div
434
+ className="fixed inset-0 z-40"
435
+ onClick={() => setMenuOpen(false)}
436
+ />
437
+ <div className="absolute right-0 top-full mt-1 z-50 bg-slate-800 border border-slate-600 rounded-lg shadow-xl w-52 overflow-hidden">
438
+ {newLabMenu.map((item) => (
439
+ <button
440
+ key={item.label}
441
+ onClick={() => {
442
+ item.onClick();
443
+ setMenuOpen(false);
444
+ }}
445
+ className="w-full text-left px-3 py-2 hover:bg-slate-700 transition-colors group"
446
+ >
447
+ <div className="text-[11px] font-medium text-slate-200 group-hover:text-cyan-300">
448
+ {item.label}
449
+ </div>
450
+ <div className="text-[10px] text-slate-500 mt-0.5 leading-snug">
451
+ {item.description}
452
+ </div>
453
+ </button>
454
+ ))}
455
+ </div>
456
+ </>
457
+ )}
458
+ </div>
459
+ ) : onNewLab ? (
412
460
  <button
413
461
  onClick={onNewLab}
414
462
  className="p-0.5 rounded text-slate-600 hover:text-cyan-400 hover:bg-slate-700 transition-colors"
@@ -416,7 +464,7 @@ export default function LabsPanel() {
416
464
  >
417
465
  <Plus className="w-3.5 h-3.5" />
418
466
  </button>
419
- )}
467
+ ) : null}
420
468
  </div>
421
469
  <div className="space-y-0.5 max-h-40 overflow-y-auto">
422
470
  {items.map((cf) => (
@@ -528,8 +576,21 @@ export default function LabsPanel() {
528
576
  iconColor="text-emerald-400/70"
529
577
  origin="module-federation"
530
578
  emptyText="Save a webpack module federation lab to reopen it here"
531
- onNewLab={() => openModuleFederationLab()}
532
- newLabTitle="Open Webpack Module Federation Lab"
579
+ newLabMenu={[
580
+ {
581
+ label: "Shared React Tree",
582
+ description:
583
+ "Shell & remote share one React runtime — classic federation",
584
+ onClick: () => openModuleFederationLab(),
585
+ },
586
+ {
587
+ label: "Isolated Mount / Unmount",
588
+ description:
589
+ "Remote owns its own React root via mount(el) / unmount(el)",
590
+ onClick: () =>
591
+ openModuleFederationLab(ISOLATED_MODULE_FEDERATION_LAB),
592
+ },
593
+ ]}
533
594
  onOpen={openMFFile}
534
595
  openTitle="Open in Webpack Module Federation Lab"
535
596
  accentClass="text-emerald-200"
@@ -1143,6 +1143,414 @@ export const DEFAULT_MODULE_FEDERATION_LAB: FrontendLabWorkspace = {
1143
1143
  files: MODULE_FEDERATION_DEFAULT_FILES,
1144
1144
  };
1145
1145
 
1146
+ // ─── Isolated Mount/Unmount MFE workspace ───────────────────────────────────
1147
+ // Each remote manages its own React root — no shared React tree with the host.
1148
+ // The remote exposes mount(el, props) / unmount(el) instead of a React component.
1149
+ const MODULE_FEDERATION_ISOLATED_FILES: Record<string, string> = {
1150
+ "README.md": `# Isolated Mount/Unmount Module Federation Lab
1151
+
1152
+ ## Pattern overview
1153
+
1154
+ In the classic "shared React tree" pattern the host imports a remote React component
1155
+ and renders it inside its own tree - both share one React runtime.
1156
+
1157
+ In this **isolated** pattern:
1158
+ - The remote exposes \`mount(el, props)\` and \`unmount(el)\` functions
1159
+ - The host calls those functions with a DOM element
1160
+ - Each remote creates its own \`ReactDOM.createRoot\` — it is NOT in the host's tree
1161
+ - The remote can use a different React major version from the host
1162
+
1163
+ ## Structure
1164
+
1165
+ - \`apps/host\` — the shell app (React 18)
1166
+ - \`apps/mfe-auth\` — the micro-frontend (could be React 18 or 19)
1167
+
1168
+ ## Key files
1169
+
1170
+ - \`apps/mfe-auth/src/mount.jsx\` — exposes \`mount\` / \`unmount\`
1171
+ - \`apps/mfe-auth/webpack.config.js\` — exposes \`./mount\` (not a React component)
1172
+ - \`apps/host/src/MfeContainer.jsx\` — host side: DOM ref + useEffect calling mount/unmount
1173
+ - \`apps/host/src/App.jsx\` — renders \`<MfeContainer />\` like any normal component
1174
+
1175
+ ## What to experiment with
1176
+
1177
+ 1. Try passing different versions of React to host vs mfe-auth and see they don't conflict
1178
+ 2. Add a second MFE (\`apps/mfe-dashboard\`) following the same mount/unmount contract
1179
+ 3. Pass props through \`mount(el, { user, theme })\` and handle updates in the remote
1180
+ 4. Observe that React context from the host does NOT flow into the remote
1181
+ `,
1182
+ "package.json": `{
1183
+ "name": "mf-isolated-lab",
1184
+ "private": true,
1185
+ "workspaces": [
1186
+ "apps/host",
1187
+ "apps/mfe-auth"
1188
+ ],
1189
+ "scripts": {
1190
+ "dev": "concurrently -k -n host,mfe-auth -c cyan,magenta 'npm run dev --workspace=@mf-isolated/host' 'npm run dev --workspace=@mf-isolated/mfe-auth'",
1191
+ "build": "npm run build --workspace=@mf-isolated/host && npm run build --workspace=@mf-isolated/mfe-auth"
1192
+ },
1193
+ "devDependencies": {
1194
+ "concurrently": "^9.2.1"
1195
+ }
1196
+ }
1197
+ `,
1198
+ "apps/host/package.json": `{
1199
+ "name": "@mf-isolated/host",
1200
+ "version": "1.0.0",
1201
+ "private": true,
1202
+ "scripts": {
1203
+ "dev": "webpack serve --config webpack.config.js",
1204
+ "build": "webpack --config webpack.config.js"
1205
+ },
1206
+ "dependencies": {
1207
+ "react": "^19.0.0",
1208
+ "react-dom": "^19.0.0"
1209
+ },
1210
+ "devDependencies": {
1211
+ "esbuild": "^0.28.0",
1212
+ "esbuild-loader": "^4.4.3",
1213
+ "html-webpack-plugin": "^5.6.7",
1214
+ "webpack": "^5.106.2",
1215
+ "webpack-cli": "^7.0.2",
1216
+ "webpack-dev-server": "^5.2.3"
1217
+ }
1218
+ }
1219
+ `,
1220
+ "apps/host/webpack.config.js": `const HtmlWebpackPlugin = require("html-webpack-plugin");
1221
+ const { ModuleFederationPlugin } = require("webpack").container;
1222
+ const deps = require("./package.json").dependencies;
1223
+
1224
+ const HOST_PORT = parseInt(process.env.HOST_PORT || "3001");
1225
+ const MFE_AUTH_PORT = parseInt(process.env.MFE_AUTH_PORT || "3002");
1226
+
1227
+ module.exports = {
1228
+ mode: "development",
1229
+ entry: "./src/index.js",
1230
+ output: {
1231
+ publicPath: "auto",
1232
+ },
1233
+ resolve: { extensions: [".js", ".jsx"] },
1234
+ module: {
1235
+ rules: [
1236
+ {
1237
+ test: /\\.(js|jsx)$/,
1238
+ exclude: /node_modules/,
1239
+ use: {
1240
+ loader: "esbuild-loader",
1241
+ options: { loader: "jsx", jsx: "automatic", target: "es2020" },
1242
+ },
1243
+ },
1244
+ ],
1245
+ },
1246
+ plugins: [
1247
+ new ModuleFederationPlugin({
1248
+ name: "host",
1249
+ remotes: {
1250
+ // The remote exposes mount/unmount — NOT a React component
1251
+ mfeAuth: \`mfeAuth@http://localhost:\${MFE_AUTH_PORT}/remoteEntry.js\`,
1252
+ },
1253
+ // Host does NOT share React with the remote.
1254
+ // Each app brings its own copy.
1255
+ shared: {
1256
+ react: { requiredVersion: deps.react },
1257
+ "react-dom": { requiredVersion: deps["react-dom"] },
1258
+ },
1259
+ }),
1260
+ new HtmlWebpackPlugin({ template: "./public/index.html" }),
1261
+ ],
1262
+ devServer: {
1263
+ port: HOST_PORT,
1264
+ headers: { "Access-Control-Allow-Origin": "*" },
1265
+ },
1266
+ };
1267
+ `,
1268
+ "apps/host/public/index.html": `<!doctype html>
1269
+ <html lang="en">
1270
+ <head>
1271
+ <meta charset="UTF-8" />
1272
+ <title>Host (Isolated MFE)</title>
1273
+ <style>
1274
+ body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; }
1275
+ h1 { padding: 1rem 1.5rem 0; font-size: 1.1rem; color: #94a3b8; }
1276
+ .mfe-slot {
1277
+ margin: 1rem 1.5rem;
1278
+ border: 1px solid #334155;
1279
+ border-radius: 8px;
1280
+ padding: 1rem;
1281
+ min-height: 80px;
1282
+ background: #1e293b;
1283
+ }
1284
+ </style>
1285
+ </head>
1286
+ <body>
1287
+ <div id="root"></div>
1288
+ </body>
1289
+ </html>
1290
+ `,
1291
+ "apps/host/src/index.js": `// Async boundary: required for Module Federation dynamic imports
1292
+ import("./bootstrap");
1293
+ `,
1294
+ "apps/host/src/bootstrap.jsx": `import React from "react";
1295
+ import { createRoot } from "react-dom/client";
1296
+ import App from "./App";
1297
+
1298
+ createRoot(document.getElementById("root")).render(<App />);
1299
+ `,
1300
+ "apps/host/src/App.jsx": `import React from "react";
1301
+ import MfeContainer from "./MfeContainer";
1302
+
1303
+ export default function App() {
1304
+ return (
1305
+ <div>
1306
+ <h1>Host App — Isolated MFE demo</h1>
1307
+ <p style={{ padding: "0 1.5rem", color: "#64748b", fontSize: "0.8rem" }}>
1308
+ The auth widget below is rendered by mfe-auth into its own React root.
1309
+ The host and the remote do NOT share a React tree.
1310
+ </p>
1311
+
1312
+ {/* Host renders a plain DOM container.
1313
+ MfeContainer calls mount/unmount on that element. */}
1314
+ <MfeContainer user={{ name: "Alice", role: "admin" }} />
1315
+ </div>
1316
+ );
1317
+ }
1318
+ `,
1319
+ "apps/host/src/MfeContainer.jsx": `import React, { useRef, useEffect } from "react";
1320
+
1321
+ // Lazy-load the remote's mount/unmount contract — not a React component.
1322
+ const mfeAuthMountPromise = import("mfeAuth/mount");
1323
+
1324
+ export default function MfeContainer({ user }) {
1325
+ const elRef = useRef(null);
1326
+ const mountedRef = useRef(null); // holds { unmount } returned by remote
1327
+
1328
+ useEffect(() => {
1329
+ let cancelled = false;
1330
+ let cleanup = null;
1331
+
1332
+ mfeAuthMountPromise.then(({ mount }) => {
1333
+ if (cancelled || !elRef.current) return;
1334
+ // mount() creates a new ReactDOM.createRoot inside mfe-auth.
1335
+ // It returns an object with an unmount() method.
1336
+ cleanup = mount(elRef.current, { user });
1337
+ mountedRef.current = cleanup;
1338
+ });
1339
+
1340
+ return () => {
1341
+ cancelled = true;
1342
+ // On cleanup, call the remote's own unmount so it can tear down its root.
1343
+ cleanup?.unmount();
1344
+ };
1345
+ }, []); // mount once — treat like a portal
1346
+
1347
+ // If props change, tell the remote to update.
1348
+ // The remote decides how to handle prop updates.
1349
+ useEffect(() => {
1350
+ mountedRef.current?.update?.({ user });
1351
+ }, [user]);
1352
+
1353
+ return (
1354
+ <div
1355
+ ref={elRef}
1356
+ className="mfe-slot"
1357
+ style={{
1358
+ margin: "1rem 1.5rem",
1359
+ border: "1px solid #334155",
1360
+ borderRadius: "8px",
1361
+ padding: "1rem",
1362
+ minHeight: "80px",
1363
+ background: "#1e293b",
1364
+ }}
1365
+ />
1366
+ );
1367
+ }
1368
+ `,
1369
+ "apps/mfe-auth/package.json": `{
1370
+ "name": "@mf-isolated/mfe-auth",
1371
+ "version": "1.0.0",
1372
+ "private": true,
1373
+ "scripts": {
1374
+ "dev": "webpack serve --config webpack.config.js",
1375
+ "build": "webpack --config webpack.config.js"
1376
+ },
1377
+ "dependencies": {
1378
+ "react": "^19.0.0",
1379
+ "react-dom": "^19.0.0"
1380
+ },
1381
+ "devDependencies": {
1382
+ "esbuild": "^0.28.0",
1383
+ "esbuild-loader": "^4.4.3",
1384
+ "html-webpack-plugin": "^5.6.7",
1385
+ "webpack": "^5.106.2",
1386
+ "webpack-cli": "^7.0.2",
1387
+ "webpack-dev-server": "^5.2.3"
1388
+ }
1389
+ }
1390
+ `,
1391
+ "apps/mfe-auth/webpack.config.js": `const HtmlWebpackPlugin = require("html-webpack-plugin");
1392
+ const { ModuleFederationPlugin } = require("webpack").container;
1393
+ const deps = require("./package.json").dependencies;
1394
+
1395
+ const MFE_AUTH_PORT = parseInt(process.env.MFE_AUTH_PORT || "3002");
1396
+
1397
+ module.exports = {
1398
+ mode: "development",
1399
+ entry: "./src/index.js",
1400
+ output: {
1401
+ publicPath: "auto",
1402
+ },
1403
+ resolve: { extensions: [".js", ".jsx"] },
1404
+ module: {
1405
+ rules: [
1406
+ {
1407
+ test: /\\.(js|jsx)$/,
1408
+ exclude: /node_modules/,
1409
+ use: {
1410
+ loader: "esbuild-loader",
1411
+ options: { loader: "jsx", jsx: "automatic", target: "es2020" },
1412
+ },
1413
+ },
1414
+ ],
1415
+ },
1416
+ plugins: [
1417
+ new ModuleFederationPlugin({
1418
+ name: "mfeAuth",
1419
+ filename: "remoteEntry.js",
1420
+ exposes: {
1421
+ // Key difference: we expose the MOUNT MODULE, not a React component.
1422
+ // The host never imports a JSX element from us directly.
1423
+ "./mount": "./src/mount.jsx",
1424
+ },
1425
+ // This remote brings its OWN React copy.
1426
+ // No singleton sharing with the host here.
1427
+ shared: {
1428
+ react: { requiredVersion: deps.react },
1429
+ "react-dom": { requiredVersion: deps["react-dom"] },
1430
+ },
1431
+ }),
1432
+ new HtmlWebpackPlugin({ template: "./public/index.html" }),
1433
+ ],
1434
+ devServer: {
1435
+ port: MFE_AUTH_PORT,
1436
+ headers: { "Access-Control-Allow-Origin": "*" },
1437
+ },
1438
+ };
1439
+ `,
1440
+ "apps/mfe-auth/public/index.html": `<!doctype html>
1441
+ <html lang="en">
1442
+ <head><meta charset="UTF-8" /><title>MFE Auth (standalone)</title></head>
1443
+ <body><div id="root"></div></body>
1444
+ </html>
1445
+ `,
1446
+ "apps/mfe-auth/src/index.js": `import("./bootstrap");
1447
+ `,
1448
+ "apps/mfe-auth/src/bootstrap.jsx": `// Standalone entry — only used when running mfe-auth on its own for development.
1449
+ import React from "react";
1450
+ import { createRoot } from "react-dom/client";
1451
+ import App from "./App";
1452
+
1453
+ createRoot(document.getElementById("root")).render(
1454
+ <App user={{ name: "Dev User", role: "developer" }} />
1455
+ );
1456
+ `,
1457
+ "apps/mfe-auth/src/App.jsx": `import React from "react";
1458
+
1459
+ // Standalone view — used for local development of the MFE in isolation.
1460
+ export default function App({ user = {} }) {
1461
+ return (
1462
+ <div style={{ padding: "1rem", fontFamily: "system-ui, sans-serif", background: "#0f172a", color: "#e2e8f0", minHeight: "100vh" }}>
1463
+ <h2 style={{ color: "#a78bfa", fontSize: "0.9rem", margin: "0 0 0.5rem" }}>
1464
+ mfe-auth — standalone dev view
1465
+ </h2>
1466
+ <AuthWidget user={user} />
1467
+ </div>
1468
+ );
1469
+ }
1470
+
1471
+ // The actual React component that this MFE renders.
1472
+ export function AuthWidget({ user = {} }) {
1473
+ return (
1474
+ <div style={{ padding: "0.75rem 1rem", background: "#1e293b", borderRadius: "6px", border: "1px solid #334155" }}>
1475
+ <p style={{ margin: 0, fontSize: "0.8rem", color: "#94a3b8" }}>Auth MFE — isolated React root</p>
1476
+ <p style={{ margin: "0.5rem 0 0", fontSize: "0.75rem", color: "#64748b" }}>
1477
+ Logged in as: <strong style={{ color: "#e2e8f0" }}>{user.name ?? "Guest"}</strong>
1478
+ {user.role && <span style={{ marginLeft: "0.5rem", color: "#6366f1" }}>({user.role})</span>}
1479
+ </p>
1480
+ <p style={{ margin: "0.5rem 0 0", fontSize: "0.7rem", color: "#334155", fontStyle: "italic" }}>
1481
+ This component lives in its own React root — not in the host tree.
1482
+ React context from the host does not reach here.
1483
+ </p>
1484
+ </div>
1485
+ );
1486
+ }
1487
+ `,
1488
+ "apps/mfe-auth/src/mount.jsx": `// ─────────────────────────────────────────────────────────────
1489
+ // mount.jsx — the public contract exposed via Module Federation
1490
+ //
1491
+ // The host imports THIS file, not a React component.
1492
+ // It calls mount(domElement, props) to render, unmount(domElement) to tear down.
1493
+ //
1494
+ // This is the core of the isolated MFE pattern:
1495
+ // 1. We create our OWN React root (not the host's)
1496
+ // 2. We own our own lifecycle
1497
+ // 3. The host never sees our React instance
1498
+ // ─────────────────────────────────────────────────────────────
1499
+ import React from "react";
1500
+ import { createRoot } from "react-dom/client";
1501
+ import { AuthWidget } from "./App";
1502
+
1503
+ // Keep track of roots by element so we can update or unmount them.
1504
+ const roots = new WeakMap();
1505
+
1506
+ /**
1507
+ * Mount the MFE into the given DOM element.
1508
+ *
1509
+ * @param {HTMLElement} el - the host-provided DOM node
1510
+ * @param {object} props - initial props from the host
1511
+ * @returns {{ unmount: () => void, update: (props: object) => void }}
1512
+ */
1513
+ export function mount(el, props = {}) {
1514
+ const root = createRoot(el);
1515
+ roots.set(el, root);
1516
+
1517
+ root.render(<AuthWidget {...props} />);
1518
+
1519
+ return {
1520
+ /** Call this to pass updated props from the host without remounting. */
1521
+ update(newProps) {
1522
+ root.render(<AuthWidget {...newProps} />);
1523
+ },
1524
+ /** Tear down the React root when the host removes this MFE. */
1525
+ unmount() {
1526
+ root.unmount();
1527
+ roots.delete(el);
1528
+ },
1529
+ };
1530
+ }
1531
+
1532
+ /**
1533
+ * Convenience function: unmount using only the element reference.
1534
+ * Useful if the host didn't store the return value of mount().
1535
+ */
1536
+ export function unmount(el) {
1537
+ const root = roots.get(el);
1538
+ if (root) {
1539
+ root.unmount();
1540
+ roots.delete(el);
1541
+ }
1542
+ }
1543
+ `,
1544
+ };
1545
+
1546
+ export const ISOLATED_MODULE_FEDERATION_LAB: FrontendLabWorkspace = {
1547
+ version: 1,
1548
+ label: "Webpack MF — Isolated Mount/Unmount",
1549
+ type: "module-federation",
1550
+ activeFile: "apps/mfe-auth/src/mount.jsx",
1551
+ files: MODULE_FEDERATION_ISOLATED_FILES,
1552
+ };
1553
+
1146
1554
  export function defaultForType(type: FrontendLabType): FrontendLabWorkspace {
1147
1555
  if (type === "nextjs") return DEFAULT_NEXTJS_LAB;
1148
1556
  if (type === "module-federation") return DEFAULT_MODULE_FEDERATION_LAB;
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.12.0"
2
+ "version": "0.13.0"
3
3
  }
@@ -2589,17 +2589,19 @@ function getModuleFederationCommandEnv(
2589
2589
  sandbox: ModuleFederationSandboxEntry,
2590
2590
  ): NodeJS.ProcessEnv {
2591
2591
  const hostPort = new URL(sandbox.appUrls.host).port;
2592
- const profilePort = new URL(sandbox.appUrls.profile).port;
2593
- const checkoutPort = new URL(sandbox.appUrls.checkout).port;
2594
-
2595
- return {
2592
+ const env: NodeJS.ProcessEnv = {
2596
2593
  ...process.env,
2597
2594
  HOST_PORT: hostPort,
2598
- PROFILE_PORT: profilePort,
2599
- CHECKOUT_PORT: checkoutPort,
2600
2595
  MF_SANDBOX_ID: sandbox.id,
2601
2596
  npm_config_update_notifier: "false",
2602
2597
  };
2598
+ if (sandbox.appUrls.profile)
2599
+ env.PROFILE_PORT = new URL(sandbox.appUrls.profile).port;
2600
+ if (sandbox.appUrls.checkout)
2601
+ env.CHECKOUT_PORT = new URL(sandbox.appUrls.checkout).port;
2602
+ if (sandbox.appUrls.mfeAuth)
2603
+ env.MFE_AUTH_PORT = new URL(sandbox.appUrls.mfeAuth).port;
2604
+ return env;
2603
2605
  }
2604
2606
 
2605
2607
  async function runStreamedCommand(
@@ -2934,24 +2936,42 @@ app.post("/api/module-federation/start", async (req, res) => {
2934
2936
  logs,
2935
2937
  );
2936
2938
 
2937
- const [hostPort, profilePort, checkoutPort] = await getDistinctPorts(3);
2938
- const appUrls = {
2939
+ // Detect isolated 2-app pattern (host + mfe-auth, no profile/checkout).
2940
+ const isIsolated =
2941
+ typeof files["apps/mfe-auth/package.json"] === "string" &&
2942
+ typeof files["apps/checkout/package.json"] !== "string";
2943
+
2944
+ const ports = await getDistinctPorts(isIsolated ? 2 : 3);
2945
+ const [hostPort] = ports;
2946
+
2947
+ const appUrls: Record<string, string> = {
2939
2948
  host: `http://localhost:${hostPort}`,
2940
- profile: `http://localhost:${profilePort}`,
2941
- checkout: `http://localhost:${checkoutPort}`,
2942
2949
  };
2950
+ const spawnEnv: NodeJS.ProcessEnv = {
2951
+ ...process.env,
2952
+ HOST_PORT: String(hostPort),
2953
+ MF_SANDBOX_ID: id,
2954
+ npm_config_update_notifier: "false",
2955
+ };
2956
+
2957
+ if (isIsolated) {
2958
+ const [, mfeAuthPort] = ports;
2959
+ appUrls.mfeAuth = `http://localhost:${mfeAuthPort}`;
2960
+ spawnEnv.MFE_AUTH_PORT = String(mfeAuthPort);
2961
+ } else {
2962
+ const [, profilePort, checkoutPort] = ports;
2963
+ appUrls.profile = `http://localhost:${profilePort}`;
2964
+ appUrls.checkout = `http://localhost:${checkoutPort}`;
2965
+ spawnEnv.PROFILE_PORT = String(profilePort);
2966
+ spawnEnv.CHECKOUT_PORT = String(checkoutPort);
2967
+ }
2968
+
2943
2969
  const readyPorts = new Set<string>();
2970
+ const requiredPorts = isIsolated ? 2 : 3;
2944
2971
 
2945
2972
  const child = spawn(npmCommand(), ["run", "dev"], {
2946
2973
  cwd: dir,
2947
- env: {
2948
- ...process.env,
2949
- HOST_PORT: String(hostPort),
2950
- PROFILE_PORT: String(profilePort),
2951
- CHECKOUT_PORT: String(checkoutPort),
2952
- MF_SANDBOX_ID: id,
2953
- npm_config_update_notifier: "false",
2954
- },
2974
+ env: spawnEnv,
2955
2975
  });
2956
2976
 
2957
2977
  const entry: ModuleFederationSandboxEntry = {
@@ -2966,11 +2986,10 @@ app.post("/api/module-federation/start", async (req, res) => {
2966
2986
  };
2967
2987
 
2968
2988
  const markReady = (text: string) => {
2969
- if (text.includes(`localhost:${hostPort}`)) readyPorts.add("host");
2970
- if (text.includes(`localhost:${profilePort}`)) readyPorts.add("profile");
2971
- if (text.includes(`localhost:${checkoutPort}`))
2972
- readyPorts.add("checkout");
2973
- if (readyPorts.size === 3) {
2989
+ for (const [key, url] of Object.entries(appUrls)) {
2990
+ if (text.includes(new URL(url).host)) readyPorts.add(key);
2991
+ }
2992
+ if (readyPorts.size >= requiredPorts) {
2974
2993
  entry.ready = true;
2975
2994
  }
2976
2995
  };