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.
- package/package.json +1 -1
- package/template/client/package-lock.json +734 -1
- package/template/client/package.json +1 -0
- package/template/client/src/App.tsx +3 -0
- package/template/client/src/api.ts +384 -4
- package/template/client/src/components/AiSettingsModal.tsx +818 -425
- package/template/client/src/components/ChatMessage.tsx +34 -12
- package/template/client/src/components/ChatView.tsx +298 -121
- package/template/client/src/components/CodeContextPanel.tsx +530 -2
- package/template/client/src/components/CodeRunnerModal.tsx +1895 -120
- package/template/client/src/components/DocRefModal.tsx +55 -6
- package/template/client/src/components/FileAttachments.tsx +20 -4
- package/template/client/src/components/InfraLabModal.tsx +1706 -0
- package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
- package/template/client/src/components/MarkdownRenderer.tsx +22 -8
- package/template/client/src/components/NotesModal.tsx +977 -0
- package/template/client/src/components/PlotEmbed.tsx +173 -0
- package/template/client/src/components/Sidebar.tsx +184 -0
- package/template/client/src/components/VizCraftEmbed.tsx +257 -13
- package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
- package/template/client/src/infraLab.ts +124 -0
- package/template/client/src/reactLab.ts +960 -0
- package/template/client/src/store.ts +250 -6
- package/template/client/src/types.ts +36 -3
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/google-drive.ts +39 -3
- package/template/server/src/index.ts +954 -52
- package/template/server/src/infra-runner.ts +1104 -0
- 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
|
+
}
|