create-interview-cockpit 0.5.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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/template/client/package-lock.json +734 -1
  3. package/template/client/package.json +1 -0
  4. package/template/client/src/App.tsx +3 -0
  5. package/template/client/src/api.ts +384 -4
  6. package/template/client/src/components/AiSettingsModal.tsx +818 -425
  7. package/template/client/src/components/ChatMessage.tsx +34 -12
  8. package/template/client/src/components/ChatView.tsx +298 -121
  9. package/template/client/src/components/CodeContextPanel.tsx +530 -2
  10. package/template/client/src/components/CodeRunnerModal.tsx +1895 -120
  11. package/template/client/src/components/DocRefModal.tsx +55 -6
  12. package/template/client/src/components/FileAttachments.tsx +20 -4
  13. package/template/client/src/components/InfraLabModal.tsx +1706 -0
  14. package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
  15. package/template/client/src/components/MarkdownRenderer.tsx +22 -8
  16. package/template/client/src/components/NotesModal.tsx +977 -0
  17. package/template/client/src/components/PlotEmbed.tsx +173 -0
  18. package/template/client/src/components/Sidebar.tsx +184 -0
  19. package/template/client/src/components/VizCraftEmbed.tsx +257 -13
  20. package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
  21. package/template/client/src/infraLab.ts +124 -0
  22. package/template/client/src/reactLab.ts +960 -0
  23. package/template/client/src/store.ts +250 -6
  24. package/template/client/src/types.ts +36 -3
  25. package/template/client/tsconfig.tsbuildinfo +1 -1
  26. package/template/cockpit.json +1 -1
  27. package/template/server/src/google-drive.ts +39 -3
  28. package/template/server/src/index.ts +954 -52
  29. package/template/server/src/infra-runner.ts +1104 -0
  30. package/template/server/src/storage.ts +22 -3
@@ -0,0 +1,960 @@
1
+ import type { FrontendLabWorkspace } from "./types";
2
+
3
+ export type FrontendLabType = FrontendLabWorkspace["type"];
4
+
5
+ // ── Default file contents ────────────────────────────────────────────────────
6
+
7
+ const REACT_DEFAULT_FILES: Record<string, string> = {
8
+ "App.tsx": `import { useState } from "react";
9
+ import { Counter } from "./Counter";
10
+ import type { User } from "./types";
11
+
12
+ const user: User = { name: "Alice", age: 28 };
13
+
14
+ export default function App() {
15
+ const [count, setCount] = useState(0);
16
+
17
+ return (
18
+ <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
19
+ <h1 style={{ fontSize: "1.5rem", fontWeight: "bold", marginBottom: "0.5rem" }}>
20
+ React + TypeScript Lab
21
+ </h1>
22
+ <p style={{ color: "#64748b", marginBottom: "1.5rem" }}>
23
+ Welcome, {user.name}! Practice React fundamentals here.
24
+ </p>
25
+ <Counter initialCount={count} onCountChange={setCount} />
26
+ <p style={{ marginTop: "1rem", color: "#94a3b8", fontSize: "0.875rem" }}>
27
+ Parent count: {count}
28
+ </p>
29
+ </div>
30
+ );
31
+ }
32
+ `,
33
+ "Counter.tsx": `import { useState, useCallback } from "react";
34
+ import type { CounterProps } from "./types";
35
+
36
+ // Stateful child component — receives props from App
37
+ export function Counter({ initialCount = 0, onCountChange }: CounterProps) {
38
+ const [count, setCount] = useState(initialCount);
39
+
40
+ const increment = useCallback(() => {
41
+ setCount((c) => {
42
+ const next = c + 1;
43
+ onCountChange?.(next);
44
+ return next;
45
+ });
46
+ }, [onCountChange]);
47
+
48
+ const decrement = useCallback(() => {
49
+ setCount((c) => {
50
+ const next = c - 1;
51
+ onCountChange?.(next);
52
+ return next;
53
+ });
54
+ }, [onCountChange]);
55
+
56
+ return (
57
+ <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
58
+ <button
59
+ onClick={decrement}
60
+ style={{
61
+ padding: "0.5rem 1.25rem",
62
+ fontSize: "1.25rem",
63
+ cursor: "pointer",
64
+ borderRadius: "0.375rem",
65
+ border: "1px solid #cbd5e1",
66
+ background: "#f8fafc",
67
+ }}
68
+ >
69
+
70
+ </button>
71
+ <span style={{ fontSize: "2rem", fontWeight: "bold", minWidth: "3rem", textAlign: "center" }}>
72
+ {count}
73
+ </span>
74
+ <button
75
+ onClick={increment}
76
+ style={{
77
+ padding: "0.5rem 1.25rem",
78
+ fontSize: "1.25rem",
79
+ cursor: "pointer",
80
+ borderRadius: "0.375rem",
81
+ border: "1px solid #cbd5e1",
82
+ background: "#f8fafc",
83
+ }}
84
+ >
85
+ +
86
+ </button>
87
+ </div>
88
+ );
89
+ }
90
+ `,
91
+ "types.ts": `// Type definitions — shared across components
92
+
93
+ export interface User {
94
+ name: string;
95
+ age: number;
96
+ }
97
+
98
+ export interface CounterProps {
99
+ initialCount?: number;
100
+ /** Callback that fires whenever the count changes */
101
+ onCountChange?: (count: number) => void;
102
+ }
103
+ `,
104
+ };
105
+
106
+ const NEXTJS_DEFAULT_FILES: Record<string, string> = {
107
+ "app/page.tsx": `// Server Component (default in App Router — no "use client" needed)
108
+ // In real Next.js this could be async and fetch data directly
109
+ import { Counter } from "../components/Counter";
110
+
111
+ export default function HomePage() {
112
+ // In real Next.js: const data = await fetch('/api/...').then(r => r.json())
113
+ const message = "Server Components render on the server — no useState here!";
114
+
115
+ return (
116
+ <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
117
+ <h1 style={{ fontSize: "1.5rem", fontWeight: "bold", marginBottom: "0.5rem" }}>
118
+ Next.js App Router Lab
119
+ </h1>
120
+ <p style={{ color: "#64748b", marginBottom: "1.5rem" }}>{message}</p>
121
+ {/* Counter is a Client Component — it can use useState */}
122
+ <Counter />
123
+ </div>
124
+ );
125
+ }
126
+ `,
127
+ "app/layout.tsx": `// Root Layout — always a Server Component
128
+ // Wraps ALL pages; persists across navigations without re-mounting
129
+
130
+ export default function RootLayout({
131
+ children,
132
+ }: {
133
+ children: React.ReactNode;
134
+ }) {
135
+ return (
136
+ <html lang="en">
137
+ <body style={{ margin: 0, background: "#f8fafc", fontFamily: "system-ui, sans-serif" }}>
138
+ <nav
139
+ style={{
140
+ padding: "0.75rem 2rem",
141
+ background: "#0070f3",
142
+ color: "#fff",
143
+ marginBottom: "0",
144
+ }}
145
+ >
146
+ <strong>My Next.js App</strong>
147
+ </nav>
148
+ <main>{children}</main>
149
+ </body>
150
+ </html>
151
+ );
152
+ }
153
+ `,
154
+ "app/loading.tsx": `// loading.tsx — shown while the page is fetching data (Suspense boundary)
155
+ // Next.js displays this automatically while the page component awaits
156
+
157
+ export default function Loading() {
158
+ return (
159
+ <div style={{ padding: "2rem", color: "#64748b" }}>
160
+ Loading…
161
+ </div>
162
+ );
163
+ }
164
+ `,
165
+ "components/Counter.tsx": `"use client";
166
+ // "use client" — marks this as a Client Component
167
+ // Only Client Components can use useState, useEffect, and browser APIs
168
+
169
+ import { useState } from "react";
170
+
171
+ export function Counter() {
172
+ const [count, setCount] = useState(0);
173
+
174
+ return (
175
+ <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
176
+ <button
177
+ onClick={() => setCount((c) => c - 1)}
178
+ style={{ padding: "0.5rem 1.25rem", fontSize: "1.25rem", cursor: "pointer",
179
+ borderRadius: "0.375rem", border: "1px solid #cbd5e1", background: "#f8fafc" }}
180
+ >
181
+
182
+ </button>
183
+ <span style={{ fontSize: "2rem", fontWeight: "bold", minWidth: "3rem", textAlign: "center" }}>
184
+ {count}
185
+ </span>
186
+ <button
187
+ onClick={() => setCount((c) => c + 1)}
188
+ style={{ padding: "0.5rem 1.25rem", fontSize: "1.25rem", cursor: "pointer",
189
+ borderRadius: "0.375rem", border: "1px solid #cbd5e1", background: "#f8fafc" }}
190
+ >
191
+ +
192
+ </button>
193
+ </div>
194
+ );
195
+ }
196
+ `,
197
+ "types.ts": `// Shared TypeScript types
198
+
199
+ export interface PageProps {
200
+ params: { slug: string };
201
+ searchParams: Record<string, string | string[] | undefined>;
202
+ }
203
+
204
+ export interface User {
205
+ id: string;
206
+ name: string;
207
+ email: string;
208
+ }
209
+ `,
210
+ };
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
+
663
+ // ── Lab workspace constructors ────────────────────────────────────────────────
664
+
665
+ export const DEFAULT_REACT_LAB: FrontendLabWorkspace = {
666
+ version: 1,
667
+ label: "React Lab",
668
+ type: "react",
669
+ activeFile: "App.tsx",
670
+ files: REACT_DEFAULT_FILES,
671
+ };
672
+
673
+ export const DEFAULT_NEXTJS_LAB: FrontendLabWorkspace = {
674
+ version: 1,
675
+ label: "Next.js Lab",
676
+ type: "nextjs",
677
+ activeFile: "app/page.tsx",
678
+ files: NEXTJS_DEFAULT_FILES,
679
+ };
680
+
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;
693
+ }
694
+
695
+ export function cloneFrontendLabWorkspace(
696
+ workspace?: FrontendLabWorkspace | null,
697
+ type?: FrontendLabType,
698
+ ): FrontendLabWorkspace {
699
+ const resolvedType = workspace?.type ?? type ?? "react";
700
+ const defaults = defaultForType(resolvedType);
701
+ const source = workspace ?? defaults;
702
+ const files =
703
+ source.files && Object.keys(source.files).length > 0
704
+ ? { ...source.files }
705
+ : { ...defaults.files };
706
+ const activeFile = files[source.activeFile]
707
+ ? source.activeFile
708
+ : (Object.keys(files)[0] ?? defaults.activeFile);
709
+
710
+ return {
711
+ version: 1,
712
+ label: source.label?.trim() || defaults.label,
713
+ type: resolvedType,
714
+ activeFile,
715
+ files,
716
+ };
717
+ }
718
+
719
+ export function serializeFrontendLabWorkspace(
720
+ workspace: FrontendLabWorkspace,
721
+ ): string {
722
+ return JSON.stringify(cloneFrontendLabWorkspace(workspace), null, 2);
723
+ }
724
+
725
+ export function parseFrontendLabWorkspace(
726
+ raw: string,
727
+ ): FrontendLabWorkspace | null {
728
+ try {
729
+ const parsed = JSON.parse(raw) as Partial<FrontendLabWorkspace> & {
730
+ files?: Record<string, unknown>;
731
+ };
732
+ if (!parsed || typeof parsed !== "object") return null;
733
+ if (!parsed.files || typeof parsed.files !== "object") return null;
734
+
735
+ const files = Object.fromEntries(
736
+ Object.entries(parsed.files).filter(
737
+ (e): e is [string, string] => typeof e[1] === "string",
738
+ ),
739
+ );
740
+ if (Object.keys(files).length === 0) return null;
741
+
742
+ const type: FrontendLabType =
743
+ parsed.type === "nextjs"
744
+ ? "nextjs"
745
+ : parsed.type === "module-federation"
746
+ ? "module-federation"
747
+ : "react";
748
+
749
+ return cloneFrontendLabWorkspace({
750
+ version: 1,
751
+ type,
752
+ label:
753
+ typeof parsed.label === "string" && parsed.label.trim()
754
+ ? parsed.label.trim()
755
+ : defaultForType(type).label,
756
+ activeFile:
757
+ typeof parsed.activeFile === "string"
758
+ ? parsed.activeFile
759
+ : defaultForType(type).activeFile,
760
+ files,
761
+ });
762
+ } catch {
763
+ return null;
764
+ }
765
+ }
766
+
767
+ /** Returns the canonical entry file for "Run" → preview. */
768
+ export function getEntryFile(workspace: FrontendLabWorkspace): string {
769
+ if (workspace.type === "nextjs") {
770
+ return workspace.files["app/page.tsx"]
771
+ ? "app/page.tsx"
772
+ : Object.keys(workspace.files)[0];
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
+ }
779
+ return workspace.files["App.tsx"]
780
+ ? "App.tsx"
781
+ : Object.keys(workspace.files)[0];
782
+ }
783
+
784
+ /** Preferred display order for the file tree. */
785
+ export function getFrontendLabFileOrder(
786
+ workspace: FrontendLabWorkspace,
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
+ }
799
+ const allFiles = Object.keys(workspace.files).sort((a, b) => {
800
+ // Sort by folder depth first, then alphabetically
801
+ const ad = a.split("/").length;
802
+ const bd = b.split("/").length;
803
+ return ad !== bd ? ad - bd : a.localeCompare(b);
804
+ });
805
+ return allFiles;
806
+ }
807
+
808
+ // ── Preview HTML generator ────────────────────────────────────────────────────
809
+
810
+ /**
811
+ * Resolves which page.tsx file corresponds to a Next.js route path.
812
+ * Returns null if no matching file exists in `files`.
813
+ */
814
+ export function resolveNextjsEntry(
815
+ files: Record<string, string>,
816
+ routePath: string,
817
+ ): string | null {
818
+ const segments = routePath.replace(/^\//, "").split("/").filter(Boolean);
819
+ const base =
820
+ segments.length === 0 ? "app/page" : `app/${segments.join("/")}/page`;
821
+ for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
822
+ if (files[base + ext] !== undefined) return base + ext;
823
+ }
824
+ return null;
825
+ }
826
+
827
+ /**
828
+ * Generates a self-contained HTML page for the preview iframe.
829
+ *
830
+ * Approach: loads React 18 UMD + Babel standalone from CDN, runs a
831
+ * custom module system built on top of Babel's CJS transform plugin,
832
+ * then renders the default export from `entryFile`.
833
+ *
834
+ * CDN URLs are version-pinned so the preview is reproducible.
835
+ */
836
+ export function generatePreviewHTML(
837
+ files: Record<string, string>,
838
+ entryFile: string,
839
+ sandboxUrl?: string,
840
+ isNextjs?: boolean,
841
+ ): string {
842
+ const filesJSON = JSON.stringify(files);
843
+ const entryJSON = JSON.stringify(entryFile);
844
+ const sandboxJSON = JSON.stringify(sandboxUrl ?? "");
845
+ const isNextjsJSON = isNextjs ? "true" : "false";
846
+ // _i breaks up the 'import' keyword so Vite/Babel doesn't misparse
847
+ // the template literal below as containing real module import declarations
848
+ const _i = "import";
849
+
850
+ return `<!DOCTYPE html>
851
+ <html>
852
+ <head>
853
+ <meta charset="utf-8">
854
+ <meta name="viewport" content="width=device-width, initial-scale=1">
855
+ <script>window.__F__=${filesJSON};window.__E__=${entryJSON};window.SANDBOX_URL=${sandboxJSON};window.__NX__=${isNextjsJSON};</script>
856
+ <script src="https://unpkg.com/@babel/standalone@7.26.10/babel.min.js"></script>
857
+ <style>
858
+ *{box-sizing:border-box}
859
+ body{margin:0;background:#fff;font-family:system-ui,sans-serif}
860
+ #__err{display:none;position:fixed;bottom:0;left:0;right:0;padding:0.75rem 1rem;background:#fef2f2;color:#991b1b;font:12px/1.5 monospace;white-space:pre-wrap;border-top:2px solid #fca5a5;max-height:50%;overflow:auto;z-index:9999}
861
+ </style>
862
+ </head>
863
+ <body>
864
+ <div id="root"></div>
865
+ <div id="__err"></div>
866
+ <script type="module">
867
+ ${_i} React from 'https://esm.sh/react@19.1.0';
868
+ ${_i} * as ReactDOM from 'https://esm.sh/react-dom@19.1.0/client?deps=react@19.1.0';
869
+ window.React = React;
870
+ window.ReactDOM = ReactDOM;
871
+ (function(){
872
+ var files=window.__F__,entry=window.__E__,reg={};
873
+ function norm(from,id){
874
+ if(id==='react'||id==='react/jsx-runtime'||id==='react/jsx-dev-runtime')return'__react__';
875
+ if(id==='react-dom'||id==='react-dom/server')return'__reactdom__';
876
+ if(id==='react-dom/client')return'__reactdomclient__';
877
+ if(!id.startsWith('.')){return'__ext__:'+id;}
878
+ var dir=from.includes('/')?from.slice(0,from.lastIndexOf('/')+1):'';
879
+ var parts=(dir+id).split('/').reduce(function(a,p){
880
+ if(p==='..')a.pop();else if(p&&p!=='.')a.push(p);return a;
881
+ },[]);
882
+ var base=parts.join('/');
883
+ var exts=['','.tsx','.ts','.jsx','.js'];
884
+ for(var i=0;i<exts.length;i++){if(files[base+exts[i]]!=null)return base+exts[i];}
885
+ return base;
886
+ }
887
+ function makeReq(from){
888
+ return function(id){
889
+ if(id==='react'||id==='react/jsx-runtime'||id==='react/jsx-dev-runtime')return window.React;
890
+ if(id==='react-dom/client')return{createRoot:window.ReactDOM.createRoot.bind(window.ReactDOM)};
891
+ if(id==='react-dom')return window.ReactDOM;
892
+ var key=norm(from,id);
893
+ if(key.startsWith('__ext__:'))return{};
894
+ if(reg[key])return reg[key].exports;
895
+ for(var e of['.tsx','.ts','.jsx','.js']){if(reg[key+e])return reg[key+e].exports;}
896
+ console.warn('Module not found:',id,'from',from);return{};
897
+ };
898
+ }
899
+ function loadMod(name){
900
+ if(reg[name])return;
901
+ var src=files[name];if(src==null)return;
902
+ var m={exports:{}};
903
+ reg[name]=m;
904
+ try{
905
+ var out=Babel.transform(src,{
906
+ presets:[['react',{runtime:'classic'}],['typescript',{allExtensions:true,isTSX:true}]],
907
+ plugins:['transform-modules-commonjs','transform-dynamic-import'],
908
+ filename:name,sourceType:'module'
909
+ }).code;
910
+ (new Function('require','module','exports',out))(makeReq(name),m,m.exports);
911
+ }catch(e){throw new Error(name+': '+e.message);}
912
+ }
913
+ function deps(name){
914
+ var src=files[name]||'',re=/from\\s+['""]([^'"]+)['"]/g,d=[],m;
915
+ while((m=re.exec(src))!==null){
916
+ var k=norm(name,m[1]);
917
+ if(k&&!k.startsWith('__')&&files[k])d.push(k);
918
+ }
919
+ return d;
920
+ }
921
+ var vis=new Set(),order=[];
922
+ function visit(n){if(vis.has(n))return;vis.add(n);deps(n).forEach(visit);order.push(n);}
923
+ Object.keys(files).forEach(visit);
924
+ function showErr(msg){
925
+ var el=document.getElementById('__err');
926
+ el.style.display='block';el.innerText=msg;
927
+ try{parent.postMessage({type:'rlab-err',error:msg},'*');}catch(e){}
928
+ }
929
+ window.onerror=function(msg,s,l,c,err){showErr(err?err.message+'\\n'+(err.stack||''):String(msg));return true;};
930
+ window.addEventListener('unhandledrejection',function(e){showErr(e.reason&&e.reason.message?e.reason.message:String(e.reason));});
931
+ try{
932
+ order.forEach(loadMod);
933
+ var em=reg[entry];
934
+ if(!em)throw new Error('Entry not found: '+entry);
935
+ var Comp=em.exports.default;
936
+ if(typeof Comp!=='function')throw new Error('No default export (function/component) in '+entry);
937
+ // Expose a navigate helper so in-preview code can trigger URL bar changes:
938
+ // window.__nxNavigate('/dashboard')
939
+ window.__nxNavigate=function(to){try{parent.postMessage({type:'rlab-nav',to:to},'*');}catch(e){}};
940
+ var pageEl=React.createElement(Comp,null);
941
+ // In Next.js mode: wrap the page in app/layout.tsx if it exists
942
+ if(window.__NX__){
943
+ var lk=null;
944
+ for(var _le of['app/layout.tsx','app/layout.ts','app/layout.jsx','app/layout.js']){
945
+ if(reg[_le]){lk=_le;break;}
946
+ }
947
+ if(lk&&typeof reg[lk].exports.default==='function'){
948
+ pageEl=React.createElement(reg[lk].exports.default,null,pageEl);
949
+ }
950
+ }
951
+ ReactDOM.createRoot(document.getElementById('root')).render(
952
+ React.createElement(React.StrictMode,null,pageEl)
953
+ );
954
+ try{parent.postMessage({type:'rlab-ready'},'*');}catch(e){}
955
+ }catch(err){showErr(err.message+(err.stack?'\\n\\n'+err.stack:''));}
956
+ })();
957
+ </script>
958
+ </body>
959
+ </html>`;
960
+ }