create-interview-cockpit 0.12.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.
@@ -19,6 +19,7 @@ import {
19
19
  Globe,
20
20
  SlidersHorizontal,
21
21
  ArrowRightLeft,
22
+ MoreHorizontal,
22
23
  } from "lucide-react";
23
24
 
24
25
  const ROOT_PARENT_VALUE = "__root__";
@@ -75,6 +76,9 @@ export default function Sidebar() {
75
76
  const [collapsedQuestions, setCollapsedQuestions] = useState<Set<string>>(
76
77
  new Set(),
77
78
  );
79
+ const [openMenuQuestionId, setOpenMenuQuestionId] = useState<string | null>(
80
+ null,
81
+ );
78
82
  const [openTopicPrompts, setOpenTopicPrompts] = useState<Set<string>>(
79
83
  new Set(),
80
84
  );
@@ -320,6 +324,7 @@ export default function Sidebar() {
320
324
  ) => {
321
325
  // 12px base left padding + 16px per depth level
322
326
  const paddingLeft = 12 + depth * 16;
327
+ const isMenuOpen = openMenuQuestionId === q.id;
323
328
  return (
324
329
  <div
325
330
  key={q.id}
@@ -371,7 +376,8 @@ export default function Sidebar() {
371
376
  />
372
377
  ) : (
373
378
  <span
374
- className="text-xs text-slate-400 truncate flex-1"
379
+ className="text-xs text-slate-400 truncate flex-1 min-w-0"
380
+ title={q.title}
375
381
  onDoubleClick={(e) => {
376
382
  e.stopPropagation();
377
383
  setEditingQuestionId(q.id);
@@ -381,63 +387,99 @@ export default function Sidebar() {
381
387
  {q.title}
382
388
  </span>
383
389
  )}
384
- <span className="text-[10px] text-slate-700 shrink-0">
385
- {q.messages.length > 0 ? `${q.messages.length}` : ""}
386
- </span>
387
- {editingQuestionId !== q.id && (
388
- <button
389
- onClick={(e) => {
390
- e.stopPropagation();
391
- setAddingChildTo(q.id);
392
- setNewChildTitle("");
393
- }}
394
- className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
395
- title="Add child question"
396
- >
397
- <CornerDownRight className="w-2.5 h-2.5" />
398
- </button>
399
- )}
400
- {editingQuestionId !== q.id && (
401
- <button
402
- onClick={(e) => {
403
- e.stopPropagation();
404
- setEditingQuestionId(q.id);
405
- setEditingQuestionTitle(q.title);
406
- }}
407
- className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
408
- title="Rename"
409
- >
410
- <Pencil className="w-2.5 h-2.5" />
411
- </button>
412
- )}
390
+
391
+ {/* Right side: count fades on hover, replaced by "..." menu */}
413
392
  {editingQuestionId !== q.id && (
414
- <button
415
- onClick={(e) => {
416
- e.stopPropagation();
417
- setMovingQuestionId((prev) => (prev === q.id ? null : q.id));
418
- setMoveTargetParentId(q.parentQuestionId ?? ROOT_PARENT_VALUE);
419
- }}
420
- className={`p-0.5 rounded opacity-0 group-hover:opacity-100 transition-all ${
421
- movingQuestionId === q.id
422
- ? "opacity-100 text-cyan-400"
423
- : "text-slate-600 hover:text-cyan-400"
424
- }`}
425
- title="Move to a different parent"
393
+ <div
394
+ className="relative shrink-0 flex items-center"
395
+ onClick={(e) => e.stopPropagation()}
426
396
  >
427
- <ArrowRightLeft className="w-2.5 h-2.5" />
428
- </button>
397
+ {/* Count hidden while hovering or when menu is open */}
398
+ <span
399
+ className={`text-[10px] text-slate-700 ${
400
+ isMenuOpen ? "hidden" : "group-hover:hidden"
401
+ }`}
402
+ >
403
+ {q.messages.length > 0 ? `${q.messages.length}` : ""}
404
+ </span>
405
+
406
+ {/* "..." button — shown on hover or while menu is open */}
407
+ <button
408
+ onClick={() => setOpenMenuQuestionId(isMenuOpen ? null : q.id)}
409
+ className={`p-0.5 rounded transition-all ${
410
+ isMenuOpen
411
+ ? "text-cyan-400"
412
+ : "opacity-0 group-hover:opacity-100 text-slate-500 hover:text-slate-300"
413
+ }`}
414
+ title="More options"
415
+ >
416
+ <MoreHorizontal className="w-3.5 h-3.5" />
417
+ </button>
418
+
419
+ {/* Dropdown */}
420
+ {isMenuOpen && (
421
+ <>
422
+ {/* Backdrop — closes menu when clicking outside */}
423
+ <div
424
+ className="fixed inset-0 z-40"
425
+ onClick={() => setOpenMenuQuestionId(null)}
426
+ />
427
+ <div className="absolute right-0 top-full mt-0.5 z-50 bg-slate-800 border border-slate-700 rounded-md shadow-xl min-w-[140px] py-0.5">
428
+ <button
429
+ onClick={() => {
430
+ setOpenMenuQuestionId(null);
431
+ setEditingQuestionId(q.id);
432
+ setEditingQuestionTitle(q.title);
433
+ }}
434
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
435
+ >
436
+ <Pencil className="w-3 h-3" /> Rename
437
+ </button>
438
+ <button
439
+ onClick={() => {
440
+ setOpenMenuQuestionId(null);
441
+ setAddingChildTo(q.id);
442
+ setNewChildTitle("");
443
+ }}
444
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
445
+ >
446
+ <CornerDownRight className="w-3 h-3" /> Add child
447
+ </button>
448
+ <button
449
+ onClick={() => {
450
+ setOpenMenuQuestionId(null);
451
+ setMovingQuestionId((prev) =>
452
+ prev === q.id ? null : q.id,
453
+ );
454
+ setMoveTargetParentId(
455
+ q.parentQuestionId ?? ROOT_PARENT_VALUE,
456
+ );
457
+ }}
458
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
459
+ >
460
+ <ArrowRightLeft className="w-3 h-3" /> Move
461
+ </button>
462
+ <div className="border-t border-slate-700 my-0.5" />
463
+ <button
464
+ onClick={() => {
465
+ setOpenMenuQuestionId(null);
466
+ if (
467
+ window.confirm(
468
+ `Delete "${q.title}"? This cannot be undone.`,
469
+ )
470
+ ) {
471
+ removeQuestion(q.id, topicId);
472
+ }
473
+ }}
474
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-slate-700 hover:text-red-300 transition-colors"
475
+ >
476
+ <Trash2 className="w-3 h-3" /> Delete
477
+ </button>
478
+ </div>
479
+ </>
480
+ )}
481
+ </div>
429
482
  )}
430
- <button
431
- onClick={(e) => {
432
- e.stopPropagation();
433
- if (window.confirm(`Delete "${q.title}"? This cannot be undone.`)) {
434
- removeQuestion(q.id, topicId);
435
- }
436
- }}
437
- className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all"
438
- >
439
- <Trash2 className="w-2.5 h-2.5" />
440
- </button>
441
483
  </div>
442
484
  );
443
485
  };
@@ -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;