create-interview-cockpit 0.6.0 → 0.7.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 +65 -2
- package/template/client/src/components/CodeContextPanel.tsx +111 -0
- package/template/client/src/components/CodeRunnerModal.tsx +457 -163
- package/template/client/src/reactLab.ts +488 -5
- package/template/client/src/store.ts +35 -4
- package/template/client/src/types.ts +3 -2
- package/template/cockpit.json +1 -1
- package/template/server/src/google-drive.ts +2 -0
- package/template/server/src/index.ts +266 -5
- package/template/server/src/storage.ts +11 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { FrontendLabWorkspace } from "./types";
|
|
2
2
|
|
|
3
|
+
export type FrontendLabType = FrontendLabWorkspace["type"];
|
|
4
|
+
|
|
3
5
|
// ── Default file contents ────────────────────────────────────────────────────
|
|
4
6
|
|
|
5
7
|
const REACT_DEFAULT_FILES: Record<string, string> = {
|
|
@@ -207,6 +209,457 @@ export interface User {
|
|
|
207
209
|
`,
|
|
208
210
|
};
|
|
209
211
|
|
|
212
|
+
const MODULE_FEDERATION_DEFAULT_FILES: Record<string, string> = {
|
|
213
|
+
"README.md": `# Webpack Module Federation Lab
|
|
214
|
+
|
|
215
|
+
This lab uses real webpack 5 + webpack-dev-server + Module Federation.
|
|
216
|
+
|
|
217
|
+
## What is here
|
|
218
|
+
|
|
219
|
+
- package.json runs three apps together: a host plus two remotes
|
|
220
|
+
- apps/host consumes federated modules from the remotes
|
|
221
|
+
- apps/profile exposes a profile widget
|
|
222
|
+
- apps/checkout exposes a checkout widget
|
|
223
|
+
|
|
224
|
+
## Good experiments
|
|
225
|
+
|
|
226
|
+
1. Break one remote URL in apps/host/webpack.config.js and see how the host fails.
|
|
227
|
+
2. Rename an exposed module in a remote without updating the host import.
|
|
228
|
+
3. Stop sharing React as a singleton and inspect the runtime behavior.
|
|
229
|
+
4. Add a new remote by copying an existing app and wiring it into the host.
|
|
230
|
+
|
|
231
|
+
## Notes
|
|
232
|
+
|
|
233
|
+
- Ports are injected by the lab runner through environment variables.
|
|
234
|
+
- If you change package.json, restart the webpack lab so dependencies/scripts are re-read.
|
|
235
|
+
`,
|
|
236
|
+
"package.json": `{
|
|
237
|
+
"name": "webpack-module-federation-lab",
|
|
238
|
+
"private": true,
|
|
239
|
+
"scripts": {
|
|
240
|
+
"dev": "concurrently -k -n host,profile,checkout -c cyan,magenta,yellow 'npm run dev:host' 'npm run dev:profile' 'npm run dev:checkout'",
|
|
241
|
+
"dev:host": "webpack serve --config apps/host/webpack.config.js",
|
|
242
|
+
"dev:profile": "webpack serve --config apps/profile/webpack.config.js",
|
|
243
|
+
"dev:checkout": "webpack serve --config apps/checkout/webpack.config.js",
|
|
244
|
+
"build": "npm run build:host && npm run build:profile && npm run build:checkout",
|
|
245
|
+
"build:host": "webpack --config apps/host/webpack.config.js",
|
|
246
|
+
"build:profile": "webpack --config apps/profile/webpack.config.js",
|
|
247
|
+
"build:checkout": "webpack --config apps/checkout/webpack.config.js"
|
|
248
|
+
},
|
|
249
|
+
"dependencies": {
|
|
250
|
+
"react": "^19.0.0",
|
|
251
|
+
"react-dom": "^19.0.0"
|
|
252
|
+
},
|
|
253
|
+
"devDependencies": {
|
|
254
|
+
"concurrently": "^9.2.1",
|
|
255
|
+
"esbuild": "^0.28.0",
|
|
256
|
+
"esbuild-loader": "^4.4.3",
|
|
257
|
+
"html-webpack-plugin": "^5.6.7",
|
|
258
|
+
"webpack": "^5.106.2",
|
|
259
|
+
"webpack-cli": "^7.0.2",
|
|
260
|
+
"webpack-dev-server": "^5.2.3"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
`,
|
|
264
|
+
"apps/host/public/index.html": `<!doctype html>
|
|
265
|
+
<html lang="en">
|
|
266
|
+
<head>
|
|
267
|
+
<meta charset="utf-8" />
|
|
268
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
269
|
+
<title>Host App</title>
|
|
270
|
+
</head>
|
|
271
|
+
<body>
|
|
272
|
+
<div id="root"></div>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|
|
275
|
+
`,
|
|
276
|
+
"apps/host/src/index.jsx": `import("./bootstrap");
|
|
277
|
+
`,
|
|
278
|
+
"apps/host/src/bootstrap.jsx": `import React from "react";
|
|
279
|
+
import { createRoot } from "react-dom/client";
|
|
280
|
+
import App from "./App";
|
|
281
|
+
|
|
282
|
+
const root = createRoot(document.getElementById("root"));
|
|
283
|
+
|
|
284
|
+
root.render(
|
|
285
|
+
<React.StrictMode>
|
|
286
|
+
<App />
|
|
287
|
+
</React.StrictMode>,
|
|
288
|
+
);
|
|
289
|
+
`,
|
|
290
|
+
"apps/host/src/App.jsx": `import React, { Suspense } from "react";
|
|
291
|
+
|
|
292
|
+
const ProfileCard = React.lazy(() => import("profile/ProfileCard"));
|
|
293
|
+
const CheckoutPanel = React.lazy(() => import("checkout/CheckoutPanel"));
|
|
294
|
+
|
|
295
|
+
function RemoteBoundary({ title, children }) {
|
|
296
|
+
return (
|
|
297
|
+
<div
|
|
298
|
+
style={{
|
|
299
|
+
border: "1px solid #cbd5e1",
|
|
300
|
+
borderRadius: "0.75rem",
|
|
301
|
+
padding: "1rem",
|
|
302
|
+
background: "#fff",
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
<div style={{ fontSize: "0.8rem", color: "#64748b", marginBottom: "0.75rem" }}>
|
|
306
|
+
{title}
|
|
307
|
+
</div>
|
|
308
|
+
<Suspense fallback={<p style={{ color: "#64748b" }}>Loading remote...</p>}>
|
|
309
|
+
{children}
|
|
310
|
+
</Suspense>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export default function App() {
|
|
316
|
+
return (
|
|
317
|
+
<main
|
|
318
|
+
style={{
|
|
319
|
+
minHeight: "100vh",
|
|
320
|
+
margin: 0,
|
|
321
|
+
padding: "2rem",
|
|
322
|
+
background: "linear-gradient(135deg, #e0f2fe 0%, #f8fafc 50%, #fef3c7 100%)",
|
|
323
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
<div style={{ maxWidth: "1100px", margin: "0 auto" }}>
|
|
327
|
+
<div style={{ marginBottom: "1.5rem" }}>
|
|
328
|
+
<p style={{ margin: 0, color: "#0369a1", fontSize: "0.8rem", letterSpacing: "0.08em", textTransform: "uppercase" }}>
|
|
329
|
+
Webpack 5 Host
|
|
330
|
+
</p>
|
|
331
|
+
<h1 style={{ margin: "0.35rem 0 0", fontSize: "2rem", color: "#0f172a" }}>
|
|
332
|
+
Module Federation Playground
|
|
333
|
+
</h1>
|
|
334
|
+
<p style={{ color: "#475569", maxWidth: "52rem" }}>
|
|
335
|
+
The host renders two independently built remotes. Change a remote expose, shared dependency,
|
|
336
|
+
or URL in the webpack configs to see the same failures you would hit in a real setup.
|
|
337
|
+
</p>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<section
|
|
341
|
+
style={{
|
|
342
|
+
display: "grid",
|
|
343
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
|
|
344
|
+
gap: "1rem",
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
<RemoteBoundary title="Remote: profile/ProfileCard">
|
|
348
|
+
<ProfileCard />
|
|
349
|
+
</RemoteBoundary>
|
|
350
|
+
<RemoteBoundary title="Remote: checkout/CheckoutPanel">
|
|
351
|
+
<CheckoutPanel />
|
|
352
|
+
</RemoteBoundary>
|
|
353
|
+
</section>
|
|
354
|
+
</div>
|
|
355
|
+
</main>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
`,
|
|
359
|
+
"apps/host/webpack.config.js": `const path = require("path");
|
|
360
|
+
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
|
361
|
+
const { ModuleFederationPlugin } = require("webpack").container;
|
|
362
|
+
|
|
363
|
+
const hostPort = Number(process.env.HOST_PORT || 3100);
|
|
364
|
+
const profilePort = Number(process.env.PROFILE_PORT || 3101);
|
|
365
|
+
const checkoutPort = Number(process.env.CHECKOUT_PORT || 3102);
|
|
366
|
+
|
|
367
|
+
module.exports = {
|
|
368
|
+
mode: "development",
|
|
369
|
+
entry: path.resolve(__dirname, "./src/index.jsx"),
|
|
370
|
+
output: {
|
|
371
|
+
publicPath: "http://localhost:" + hostPort + "/",
|
|
372
|
+
clean: true,
|
|
373
|
+
},
|
|
374
|
+
resolve: {
|
|
375
|
+
extensions: [".js", ".jsx"],
|
|
376
|
+
},
|
|
377
|
+
module: {
|
|
378
|
+
rules: [
|
|
379
|
+
{
|
|
380
|
+
test: /\\.(js|jsx)$/,
|
|
381
|
+
exclude: /node_modules/,
|
|
382
|
+
use: {
|
|
383
|
+
loader: "esbuild-loader",
|
|
384
|
+
options: {
|
|
385
|
+
loader: "jsx",
|
|
386
|
+
jsx: "automatic",
|
|
387
|
+
target: "es2020",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
devServer: {
|
|
394
|
+
port: hostPort,
|
|
395
|
+
historyApiFallback: true,
|
|
396
|
+
hot: true,
|
|
397
|
+
headers: {
|
|
398
|
+
"Access-Control-Allow-Origin": "*",
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
plugins: [
|
|
402
|
+
new ModuleFederationPlugin({
|
|
403
|
+
name: "host",
|
|
404
|
+
remotes: {
|
|
405
|
+
profile: "profile@http://localhost:" + profilePort + "/remoteEntry.js",
|
|
406
|
+
checkout: "checkout@http://localhost:" + checkoutPort + "/remoteEntry.js",
|
|
407
|
+
},
|
|
408
|
+
shared: {
|
|
409
|
+
react: { singleton: true, requiredVersion: false },
|
|
410
|
+
"react-dom": { singleton: true, requiredVersion: false },
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
new HtmlWebpackPlugin({
|
|
414
|
+
template: path.resolve(__dirname, "./public/index.html"),
|
|
415
|
+
}),
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
`,
|
|
419
|
+
"apps/profile/public/index.html": `<!doctype html>
|
|
420
|
+
<html lang="en">
|
|
421
|
+
<head>
|
|
422
|
+
<meta charset="utf-8" />
|
|
423
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
424
|
+
<title>Profile Remote</title>
|
|
425
|
+
</head>
|
|
426
|
+
<body>
|
|
427
|
+
<div id="root"></div>
|
|
428
|
+
</body>
|
|
429
|
+
</html>
|
|
430
|
+
`,
|
|
431
|
+
"apps/profile/src/index.jsx": `import("./bootstrap");
|
|
432
|
+
`,
|
|
433
|
+
"apps/profile/src/bootstrap.jsx": `import React from "react";
|
|
434
|
+
import { createRoot } from "react-dom/client";
|
|
435
|
+
import App from "./App";
|
|
436
|
+
|
|
437
|
+
const root = createRoot(document.getElementById("root"));
|
|
438
|
+
|
|
439
|
+
root.render(
|
|
440
|
+
<React.StrictMode>
|
|
441
|
+
<App />
|
|
442
|
+
</React.StrictMode>,
|
|
443
|
+
);
|
|
444
|
+
`,
|
|
445
|
+
"apps/profile/src/App.jsx": `import React from "react";
|
|
446
|
+
import ProfileCard from "./ProfileCard";
|
|
447
|
+
|
|
448
|
+
export default function App() {
|
|
449
|
+
return (
|
|
450
|
+
<main style={{ padding: "2rem", fontFamily: "ui-sans-serif, system-ui, sans-serif", background: "#f8fafc", minHeight: "100vh" }}>
|
|
451
|
+
<p style={{ margin: 0, color: "#7c3aed", fontSize: "0.8rem", letterSpacing: "0.08em", textTransform: "uppercase" }}>
|
|
452
|
+
Remote App
|
|
453
|
+
</p>
|
|
454
|
+
<h1 style={{ margin: "0.35rem 0 1rem", color: "#1e293b" }}>Profile</h1>
|
|
455
|
+
<ProfileCard />
|
|
456
|
+
</main>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
`,
|
|
460
|
+
"apps/profile/src/ProfileCard.jsx": `import React from "react";
|
|
461
|
+
|
|
462
|
+
export default function ProfileCard() {
|
|
463
|
+
return (
|
|
464
|
+
<section>
|
|
465
|
+
<h2 style={{ marginTop: 0, color: "#1e293b" }}>Federated profile card</h2>
|
|
466
|
+
<p style={{ color: "#475569" }}>
|
|
467
|
+
This component is exposed from the profile remote and consumed by the host at runtime.
|
|
468
|
+
</p>
|
|
469
|
+
<dl style={{ display: "grid", gridTemplateColumns: "max-content 1fr", gap: "0.5rem 1rem", margin: 0 }}>
|
|
470
|
+
<dt style={{ color: "#64748b" }}>Owner</dt>
|
|
471
|
+
<dd style={{ margin: 0, color: "#0f172a" }}>Composable Platform Team</dd>
|
|
472
|
+
<dt style={{ color: "#64748b" }}>Build</dt>
|
|
473
|
+
<dd style={{ margin: 0, color: "#0f172a" }}>profile/ProfileCard</dd>
|
|
474
|
+
</dl>
|
|
475
|
+
</section>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
`,
|
|
479
|
+
"apps/profile/webpack.config.js": `const path = require("path");
|
|
480
|
+
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
|
481
|
+
const { ModuleFederationPlugin } = require("webpack").container;
|
|
482
|
+
|
|
483
|
+
const profilePort = Number(process.env.PROFILE_PORT || 3101);
|
|
484
|
+
|
|
485
|
+
module.exports = {
|
|
486
|
+
mode: "development",
|
|
487
|
+
entry: path.resolve(__dirname, "./src/index.jsx"),
|
|
488
|
+
output: {
|
|
489
|
+
publicPath: "http://localhost:" + profilePort + "/",
|
|
490
|
+
clean: true,
|
|
491
|
+
},
|
|
492
|
+
resolve: {
|
|
493
|
+
extensions: [".js", ".jsx"],
|
|
494
|
+
},
|
|
495
|
+
module: {
|
|
496
|
+
rules: [
|
|
497
|
+
{
|
|
498
|
+
test: /\\.(js|jsx)$/,
|
|
499
|
+
exclude: /node_modules/,
|
|
500
|
+
use: {
|
|
501
|
+
loader: "esbuild-loader",
|
|
502
|
+
options: {
|
|
503
|
+
loader: "jsx",
|
|
504
|
+
jsx: "automatic",
|
|
505
|
+
target: "es2020",
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
},
|
|
511
|
+
devServer: {
|
|
512
|
+
port: profilePort,
|
|
513
|
+
hot: true,
|
|
514
|
+
headers: {
|
|
515
|
+
"Access-Control-Allow-Origin": "*",
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
plugins: [
|
|
519
|
+
new ModuleFederationPlugin({
|
|
520
|
+
name: "profile",
|
|
521
|
+
filename: "remoteEntry.js",
|
|
522
|
+
exposes: {
|
|
523
|
+
"./ProfileCard": path.resolve(__dirname, "./src/ProfileCard.jsx"),
|
|
524
|
+
},
|
|
525
|
+
shared: {
|
|
526
|
+
react: { singleton: true, requiredVersion: false },
|
|
527
|
+
"react-dom": { singleton: true, requiredVersion: false },
|
|
528
|
+
},
|
|
529
|
+
}),
|
|
530
|
+
new HtmlWebpackPlugin({
|
|
531
|
+
template: path.resolve(__dirname, "./public/index.html"),
|
|
532
|
+
}),
|
|
533
|
+
],
|
|
534
|
+
};
|
|
535
|
+
`,
|
|
536
|
+
"apps/checkout/public/index.html": `<!doctype html>
|
|
537
|
+
<html lang="en">
|
|
538
|
+
<head>
|
|
539
|
+
<meta charset="utf-8" />
|
|
540
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
541
|
+
<title>Checkout Remote</title>
|
|
542
|
+
</head>
|
|
543
|
+
<body>
|
|
544
|
+
<div id="root"></div>
|
|
545
|
+
</body>
|
|
546
|
+
</html>
|
|
547
|
+
`,
|
|
548
|
+
"apps/checkout/src/index.jsx": `import("./bootstrap");
|
|
549
|
+
`,
|
|
550
|
+
"apps/checkout/src/bootstrap.jsx": `import React from "react";
|
|
551
|
+
import { createRoot } from "react-dom/client";
|
|
552
|
+
import App from "./App";
|
|
553
|
+
|
|
554
|
+
const root = createRoot(document.getElementById("root"));
|
|
555
|
+
|
|
556
|
+
root.render(
|
|
557
|
+
<React.StrictMode>
|
|
558
|
+
<App />
|
|
559
|
+
</React.StrictMode>,
|
|
560
|
+
);
|
|
561
|
+
`,
|
|
562
|
+
"apps/checkout/src/App.jsx": `import React from "react";
|
|
563
|
+
import CheckoutPanel from "./CheckoutPanel";
|
|
564
|
+
|
|
565
|
+
export default function App() {
|
|
566
|
+
return (
|
|
567
|
+
<main style={{ padding: "2rem", fontFamily: "ui-sans-serif, system-ui, sans-serif", background: "#fff7ed", minHeight: "100vh" }}>
|
|
568
|
+
<p style={{ margin: 0, color: "#ea580c", fontSize: "0.8rem", letterSpacing: "0.08em", textTransform: "uppercase" }}>
|
|
569
|
+
Remote App
|
|
570
|
+
</p>
|
|
571
|
+
<h1 style={{ margin: "0.35rem 0 1rem", color: "#7c2d12" }}>Checkout</h1>
|
|
572
|
+
<CheckoutPanel />
|
|
573
|
+
</main>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
`,
|
|
577
|
+
"apps/checkout/src/CheckoutPanel.jsx": `import React from "react";
|
|
578
|
+
|
|
579
|
+
export default function CheckoutPanel() {
|
|
580
|
+
return (
|
|
581
|
+
<section>
|
|
582
|
+
<h2 style={{ marginTop: 0, color: "#7c2d12" }}>Federated checkout panel</h2>
|
|
583
|
+
<p style={{ color: "#9a3412" }}>
|
|
584
|
+
This remote can evolve independently from the host as long as the public module contract stays stable.
|
|
585
|
+
</p>
|
|
586
|
+
<button
|
|
587
|
+
type="button"
|
|
588
|
+
style={{
|
|
589
|
+
border: 0,
|
|
590
|
+
borderRadius: "999px",
|
|
591
|
+
background: "#fb923c",
|
|
592
|
+
color: "#431407",
|
|
593
|
+
padding: "0.65rem 1rem",
|
|
594
|
+
fontWeight: 700,
|
|
595
|
+
cursor: "pointer",
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
Ship order
|
|
599
|
+
</button>
|
|
600
|
+
</section>
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
`,
|
|
604
|
+
"apps/checkout/webpack.config.js": `const path = require("path");
|
|
605
|
+
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
|
606
|
+
const { ModuleFederationPlugin } = require("webpack").container;
|
|
607
|
+
|
|
608
|
+
const checkoutPort = Number(process.env.CHECKOUT_PORT || 3102);
|
|
609
|
+
|
|
610
|
+
module.exports = {
|
|
611
|
+
mode: "development",
|
|
612
|
+
entry: path.resolve(__dirname, "./src/index.jsx"),
|
|
613
|
+
output: {
|
|
614
|
+
publicPath: "http://localhost:" + checkoutPort + "/",
|
|
615
|
+
clean: true,
|
|
616
|
+
},
|
|
617
|
+
resolve: {
|
|
618
|
+
extensions: [".js", ".jsx"],
|
|
619
|
+
},
|
|
620
|
+
module: {
|
|
621
|
+
rules: [
|
|
622
|
+
{
|
|
623
|
+
test: /\\.(js|jsx)$/,
|
|
624
|
+
exclude: /node_modules/,
|
|
625
|
+
use: {
|
|
626
|
+
loader: "esbuild-loader",
|
|
627
|
+
options: {
|
|
628
|
+
loader: "jsx",
|
|
629
|
+
jsx: "automatic",
|
|
630
|
+
target: "es2020",
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
},
|
|
636
|
+
devServer: {
|
|
637
|
+
port: checkoutPort,
|
|
638
|
+
hot: true,
|
|
639
|
+
headers: {
|
|
640
|
+
"Access-Control-Allow-Origin": "*",
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
plugins: [
|
|
644
|
+
new ModuleFederationPlugin({
|
|
645
|
+
name: "checkout",
|
|
646
|
+
filename: "remoteEntry.js",
|
|
647
|
+
exposes: {
|
|
648
|
+
"./CheckoutPanel": path.resolve(__dirname, "./src/CheckoutPanel.jsx"),
|
|
649
|
+
},
|
|
650
|
+
shared: {
|
|
651
|
+
react: { singleton: true, requiredVersion: false },
|
|
652
|
+
"react-dom": { singleton: true, requiredVersion: false },
|
|
653
|
+
},
|
|
654
|
+
}),
|
|
655
|
+
new HtmlWebpackPlugin({
|
|
656
|
+
template: path.resolve(__dirname, "./public/index.html"),
|
|
657
|
+
}),
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
`,
|
|
661
|
+
};
|
|
662
|
+
|
|
210
663
|
// ── Lab workspace constructors ────────────────────────────────────────────────
|
|
211
664
|
|
|
212
665
|
export const DEFAULT_REACT_LAB: FrontendLabWorkspace = {
|
|
@@ -225,13 +678,23 @@ export const DEFAULT_NEXTJS_LAB: FrontendLabWorkspace = {
|
|
|
225
678
|
files: NEXTJS_DEFAULT_FILES,
|
|
226
679
|
};
|
|
227
680
|
|
|
228
|
-
export
|
|
229
|
-
|
|
681
|
+
export const DEFAULT_MODULE_FEDERATION_LAB: FrontendLabWorkspace = {
|
|
682
|
+
version: 1,
|
|
683
|
+
label: "Webpack Module Federation Lab",
|
|
684
|
+
type: "module-federation",
|
|
685
|
+
activeFile: "apps/host/src/App.jsx",
|
|
686
|
+
files: MODULE_FEDERATION_DEFAULT_FILES,
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
export function defaultForType(type: FrontendLabType): FrontendLabWorkspace {
|
|
690
|
+
if (type === "nextjs") return DEFAULT_NEXTJS_LAB;
|
|
691
|
+
if (type === "module-federation") return DEFAULT_MODULE_FEDERATION_LAB;
|
|
692
|
+
return DEFAULT_REACT_LAB;
|
|
230
693
|
}
|
|
231
694
|
|
|
232
695
|
export function cloneFrontendLabWorkspace(
|
|
233
696
|
workspace?: FrontendLabWorkspace | null,
|
|
234
|
-
type?:
|
|
697
|
+
type?: FrontendLabType,
|
|
235
698
|
): FrontendLabWorkspace {
|
|
236
699
|
const resolvedType = workspace?.type ?? type ?? "react";
|
|
237
700
|
const defaults = defaultForType(resolvedType);
|
|
@@ -276,8 +739,12 @@ export function parseFrontendLabWorkspace(
|
|
|
276
739
|
);
|
|
277
740
|
if (Object.keys(files).length === 0) return null;
|
|
278
741
|
|
|
279
|
-
const type:
|
|
280
|
-
parsed.type === "nextjs"
|
|
742
|
+
const type: FrontendLabType =
|
|
743
|
+
parsed.type === "nextjs"
|
|
744
|
+
? "nextjs"
|
|
745
|
+
: parsed.type === "module-federation"
|
|
746
|
+
? "module-federation"
|
|
747
|
+
: "react";
|
|
281
748
|
|
|
282
749
|
return cloneFrontendLabWorkspace({
|
|
283
750
|
version: 1,
|
|
@@ -304,6 +771,11 @@ export function getEntryFile(workspace: FrontendLabWorkspace): string {
|
|
|
304
771
|
? "app/page.tsx"
|
|
305
772
|
: Object.keys(workspace.files)[0];
|
|
306
773
|
}
|
|
774
|
+
if (workspace.type === "module-federation") {
|
|
775
|
+
return workspace.files["apps/host/src/App.jsx"]
|
|
776
|
+
? "apps/host/src/App.jsx"
|
|
777
|
+
: Object.keys(workspace.files)[0];
|
|
778
|
+
}
|
|
307
779
|
return workspace.files["App.tsx"]
|
|
308
780
|
? "App.tsx"
|
|
309
781
|
: Object.keys(workspace.files)[0];
|
|
@@ -313,6 +785,17 @@ export function getEntryFile(workspace: FrontendLabWorkspace): string {
|
|
|
313
785
|
export function getFrontendLabFileOrder(
|
|
314
786
|
workspace: FrontendLabWorkspace,
|
|
315
787
|
): string[] {
|
|
788
|
+
if (workspace.type === "module-federation") {
|
|
789
|
+
const preferred = ["README.md", "package.json"];
|
|
790
|
+
const rest = Object.keys(workspace.files)
|
|
791
|
+
.filter((name) => !preferred.includes(name))
|
|
792
|
+
.sort((a, b) => {
|
|
793
|
+
const ad = a.split("/").length;
|
|
794
|
+
const bd = b.split("/").length;
|
|
795
|
+
return ad !== bd ? ad - bd : a.localeCompare(b);
|
|
796
|
+
});
|
|
797
|
+
return preferred.filter((name) => workspace.files[name]).concat(rest);
|
|
798
|
+
}
|
|
316
799
|
const allFiles = Object.keys(workspace.files).sort((a, b) => {
|
|
317
800
|
// Sort by folder depth first, then alphabetically
|
|
318
801
|
const ad = a.split("/").length;
|
|
@@ -212,7 +212,14 @@ interface Store {
|
|
|
212
212
|
code: string,
|
|
213
213
|
language: string,
|
|
214
214
|
label: string,
|
|
215
|
-
origin:
|
|
215
|
+
origin:
|
|
216
|
+
| "user"
|
|
217
|
+
| "ai"
|
|
218
|
+
| "sandbox"
|
|
219
|
+
| "infra"
|
|
220
|
+
| "react"
|
|
221
|
+
| "nextjs"
|
|
222
|
+
| "module-federation",
|
|
216
223
|
) => Promise<import("./types").ContextFile>;
|
|
217
224
|
clearMessages: (questionId: string) => Promise<void>;
|
|
218
225
|
|
|
@@ -273,7 +280,7 @@ interface Store {
|
|
|
273
280
|
clientLang: string;
|
|
274
281
|
fileId?: string;
|
|
275
282
|
/** If set, the client panel opens in React or Next.js preview mode instead of script mode */
|
|
276
|
-
clientType?: "script" | "react" | "nextjs";
|
|
283
|
+
clientType?: "script" | "react" | "nextjs" | "module-federation";
|
|
277
284
|
reactFiles?: Record<string, string> | null;
|
|
278
285
|
reactActiveFile?: string | null;
|
|
279
286
|
} | null;
|
|
@@ -287,7 +294,7 @@ interface Store {
|
|
|
287
294
|
clientLang: string,
|
|
288
295
|
fileId?: string,
|
|
289
296
|
opts?: {
|
|
290
|
-
clientType?: "script" | "react" | "nextjs";
|
|
297
|
+
clientType?: "script" | "react" | "nextjs" | "module-federation";
|
|
291
298
|
reactFiles?: Record<string, string>;
|
|
292
299
|
reactActiveFile?: string;
|
|
293
300
|
},
|
|
@@ -311,7 +318,7 @@ interface Store {
|
|
|
311
318
|
openInfraLab: (workspace?: InfraLabWorkspace, fileId?: string) => void;
|
|
312
319
|
closeInfraLab: () => void;
|
|
313
320
|
|
|
314
|
-
// ── Frontend Labs (React / Next.js) — open inside the sandbox ──
|
|
321
|
+
// ── Frontend Labs (React / Next.js / Module Federation) — open inside the sandbox ──
|
|
315
322
|
openReactLab: (
|
|
316
323
|
workspace?: FrontendLabWorkspace,
|
|
317
324
|
fileId?: string,
|
|
@@ -324,6 +331,12 @@ interface Store {
|
|
|
324
331
|
serverCode?: string,
|
|
325
332
|
serverLang?: string,
|
|
326
333
|
) => void;
|
|
334
|
+
openModuleFederationLab: (
|
|
335
|
+
workspace?: FrontendLabWorkspace,
|
|
336
|
+
fileId?: string,
|
|
337
|
+
serverCode?: string,
|
|
338
|
+
serverLang?: string,
|
|
339
|
+
) => void;
|
|
327
340
|
}
|
|
328
341
|
|
|
329
342
|
export const useStore = create<Store>((set, get) => ({
|
|
@@ -959,6 +972,24 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
959
972
|
},
|
|
960
973
|
runnerInitialFileId: null,
|
|
961
974
|
}),
|
|
975
|
+
openModuleFederationLab: (workspace?, fileId?, serverCode?, serverLang?) =>
|
|
976
|
+
set({
|
|
977
|
+
showCodeRunner: true,
|
|
978
|
+
showInfraLab: false,
|
|
979
|
+
runnerInitialCode: "",
|
|
980
|
+
runnerInitialLanguage: "typescript",
|
|
981
|
+
runnerInitialSandbox: {
|
|
982
|
+
serverCode: serverCode ?? DEFAULT_SERVER_CODE,
|
|
983
|
+
serverLang: serverLang ?? "typescript",
|
|
984
|
+
clientCode: "",
|
|
985
|
+
clientLang: "javascript",
|
|
986
|
+
fileId,
|
|
987
|
+
clientType: "module-federation",
|
|
988
|
+
reactFiles: workspace?.files ?? null,
|
|
989
|
+
reactActiveFile: workspace?.activeFile ?? null,
|
|
990
|
+
},
|
|
991
|
+
runnerInitialFileId: null,
|
|
992
|
+
}),
|
|
962
993
|
|
|
963
994
|
overwriteContextFileContent: async (questionId, fileId, content) => {
|
|
964
995
|
await api.overwriteContextFileContent(questionId, fileId, content);
|
|
@@ -5,7 +5,8 @@ export type ContextFileOrigin =
|
|
|
5
5
|
| "sandbox"
|
|
6
6
|
| "infra"
|
|
7
7
|
| "react"
|
|
8
|
-
| "nextjs"
|
|
8
|
+
| "nextjs"
|
|
9
|
+
| "module-federation";
|
|
9
10
|
|
|
10
11
|
export interface ContextFile {
|
|
11
12
|
id: string;
|
|
@@ -28,7 +29,7 @@ export interface FrontendLabWorkspace {
|
|
|
28
29
|
version: 1;
|
|
29
30
|
label: string;
|
|
30
31
|
/** Determines defaults, entry file, and file-tree conventions. */
|
|
31
|
-
type: "react" | "nextjs";
|
|
32
|
+
type: "react" | "nextjs" | "module-federation";
|
|
32
33
|
activeFile: string;
|
|
33
34
|
files: Record<string, string>;
|
|
34
35
|
}
|
package/template/cockpit.json
CHANGED
|
@@ -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 {
|