create-interview-cockpit 0.8.0 → 0.10.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/App.tsx +1 -0
- package/template/client/src/components/CodeContextPanel.tsx +853 -850
- package/template/client/src/components/CodeRunnerModal.tsx +2013 -15
- package/template/client/src/reactLab.ts +323 -28
- package/template/client/tsconfig.tsbuildinfo +1 -30
- package/template/cockpit.json +1 -1
- package/template/server/src/index.ts +4 -0
|
@@ -73,10 +73,67 @@ interface OutputLine {
|
|
|
73
73
|
source?: "server" | "client";
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
type MfInspectorEventKind =
|
|
77
|
+
| "declared-config"
|
|
78
|
+
| "runtime-boot"
|
|
79
|
+
| "route-change"
|
|
80
|
+
| "share-snapshot"
|
|
81
|
+
| "remote-load-start"
|
|
82
|
+
| "remote-load-success"
|
|
83
|
+
| "remote-load-error"
|
|
84
|
+
| "identity-check"
|
|
85
|
+
| "identity-check-error"
|
|
86
|
+
| "share-init-error";
|
|
87
|
+
|
|
88
|
+
interface MfInspectorEvent {
|
|
89
|
+
type: "mf-inspector-event";
|
|
90
|
+
sandboxId: string;
|
|
91
|
+
app: string;
|
|
92
|
+
runtimeId: string;
|
|
93
|
+
kind: MfInspectorEventKind | string;
|
|
94
|
+
route: string;
|
|
95
|
+
timestamp: number;
|
|
96
|
+
payload?: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
const LANG_OPTIONS = ["typescript", "javascript"] as const;
|
|
77
100
|
type Lang = (typeof LANG_OPTIONS)[number];
|
|
78
101
|
type FrontendClientType = "script" | FrontendLabType;
|
|
79
102
|
|
|
103
|
+
const MF_INSPECTOR_VIEWS = ["map", "shares", "events"] as const;
|
|
104
|
+
|
|
105
|
+
const MF_INSPECTOR_VIEW_COPY: Record<
|
|
106
|
+
(typeof MF_INSPECTOR_VIEWS)[number],
|
|
107
|
+
{
|
|
108
|
+
label: string;
|
|
109
|
+
title: string;
|
|
110
|
+
description: string;
|
|
111
|
+
tip: string;
|
|
112
|
+
}
|
|
113
|
+
> = {
|
|
114
|
+
map: {
|
|
115
|
+
label: "overview",
|
|
116
|
+
title: "Overview: who loaded what",
|
|
117
|
+
description:
|
|
118
|
+
"Start here. The top box is the host page. The lower boxes are remote apps. Line color and table status tell you whether the host tried to load a remote, whether the load worked, and whether React was reused safely.",
|
|
119
|
+
tip: "A healthy path is usually: host starts, host loads a remote, then the React copy check says same copy.",
|
|
120
|
+
},
|
|
121
|
+
shares: {
|
|
122
|
+
label: "shared packages",
|
|
123
|
+
title: "Shared packages: webpack's live reuse list",
|
|
124
|
+
description:
|
|
125
|
+
"This is the runtime truth. It shows which library versions webpack currently knows about and whether any app has started using them yet.",
|
|
126
|
+
tip: "For React apps, the happy path is usually one loaded React version and one loaded ReactDOM version that every app reuses.",
|
|
127
|
+
},
|
|
128
|
+
events: {
|
|
129
|
+
label: "timeline",
|
|
130
|
+
title: "Timeline: one browser event per row",
|
|
131
|
+
description:
|
|
132
|
+
"Newest items appear first. Each row tells you which app reported something, what happened in plain English, and why that step matters.",
|
|
133
|
+
tip: "If you're debugging a remote, follow the sequence: app started, host began loading, host finished loading, then React copy comparison.",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
80
137
|
// ── Sandbox default snippets ─────────────────────────────────────────
|
|
81
138
|
const DEFAULT_SERVER_CODE = `import express from 'express';
|
|
82
139
|
import { Readable } from 'stream';
|
|
@@ -254,10 +311,226 @@ function buildFileTree(paths: string[]): FileTreeNode {
|
|
|
254
311
|
return root;
|
|
255
312
|
}
|
|
256
313
|
|
|
314
|
+
function isMfInspectorEvent(value: unknown): value is MfInspectorEvent {
|
|
315
|
+
if (!value || typeof value !== "object") return false;
|
|
316
|
+
const data = value as Record<string, unknown>;
|
|
317
|
+
return (
|
|
318
|
+
data.type === "mf-inspector-event" &&
|
|
319
|
+
typeof data.sandboxId === "string" &&
|
|
320
|
+
typeof data.app === "string" &&
|
|
321
|
+
typeof data.runtimeId === "string" &&
|
|
322
|
+
typeof data.kind === "string" &&
|
|
323
|
+
typeof data.route === "string" &&
|
|
324
|
+
typeof data.timestamp === "number"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function asInspectorRecord(value: unknown): Record<string, unknown> {
|
|
329
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
330
|
+
? (value as Record<string, unknown>)
|
|
331
|
+
: {};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function asInspectorNumber(value: unknown): number | null {
|
|
335
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function asInspectorStringArray(value: unknown): string[] {
|
|
339
|
+
return Array.isArray(value)
|
|
340
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
341
|
+
: [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function formatInspectorTimestamp(timestamp: number): string {
|
|
345
|
+
try {
|
|
346
|
+
return new Date(timestamp).toLocaleTimeString([], {
|
|
347
|
+
hour: "2-digit",
|
|
348
|
+
minute: "2-digit",
|
|
349
|
+
second: "2-digit",
|
|
350
|
+
hour12: false,
|
|
351
|
+
});
|
|
352
|
+
} catch {
|
|
353
|
+
return String(timestamp);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function stringifyInspectorValue(value: unknown): string {
|
|
358
|
+
if (typeof value === "string") return value;
|
|
359
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
360
|
+
return String(value);
|
|
361
|
+
}
|
|
362
|
+
if (value == null) return "null";
|
|
363
|
+
try {
|
|
364
|
+
return JSON.stringify(value);
|
|
365
|
+
} catch {
|
|
366
|
+
return String(value);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getMfInspectorBadgeClass(kind: string): string {
|
|
371
|
+
if (kind.includes("error")) {
|
|
372
|
+
return "border-red-500/30 bg-red-500/10 text-red-300";
|
|
373
|
+
}
|
|
374
|
+
if (kind === "identity-check") {
|
|
375
|
+
return "border-emerald-500/30 bg-emerald-500/10 text-emerald-300";
|
|
376
|
+
}
|
|
377
|
+
if (kind === "remote-load-success") {
|
|
378
|
+
return "border-cyan-500/30 bg-cyan-500/10 text-cyan-300";
|
|
379
|
+
}
|
|
380
|
+
if (kind === "share-snapshot" || kind === "declared-config") {
|
|
381
|
+
return "border-amber-500/30 bg-amber-500/10 text-amber-300";
|
|
382
|
+
}
|
|
383
|
+
return "border-slate-600 bg-slate-800/80 text-slate-300";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function formatMfInspectorAppLabel(app: string): string {
|
|
387
|
+
if (!app) return "App";
|
|
388
|
+
if (app === "host") return "Host app";
|
|
389
|
+
return `${app.charAt(0).toUpperCase()}${app.slice(1)} remote`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getMfInspectorFriendlyKindLabel(kind: string): string {
|
|
393
|
+
switch (kind) {
|
|
394
|
+
case "declared-config":
|
|
395
|
+
return "Setup announced";
|
|
396
|
+
case "runtime-boot":
|
|
397
|
+
return "App started in browser";
|
|
398
|
+
case "route-change":
|
|
399
|
+
return "Route changed";
|
|
400
|
+
case "share-snapshot":
|
|
401
|
+
return "Shared package snapshot";
|
|
402
|
+
case "remote-load-start":
|
|
403
|
+
return "Host started loading a remote";
|
|
404
|
+
case "remote-load-success":
|
|
405
|
+
return "Remote finished loading";
|
|
406
|
+
case "remote-load-error":
|
|
407
|
+
return "Remote load failed";
|
|
408
|
+
case "identity-check":
|
|
409
|
+
return "React copy comparison";
|
|
410
|
+
case "identity-check-error":
|
|
411
|
+
return "React comparison failed";
|
|
412
|
+
case "share-init-error":
|
|
413
|
+
return "Sharing setup failed";
|
|
414
|
+
default:
|
|
415
|
+
return kind;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function describeMfInspectorEvent(event: MfInspectorEvent): string {
|
|
420
|
+
const payload = asInspectorRecord(event.payload);
|
|
421
|
+
|
|
422
|
+
switch (event.kind) {
|
|
423
|
+
case "declared-config":
|
|
424
|
+
return "This app described its webpack setup: what it can expose, what other apps it can call, and which packages it wants to reuse.";
|
|
425
|
+
case "runtime-boot": {
|
|
426
|
+
const label =
|
|
427
|
+
typeof payload.label === "string" ? payload.label : "runtime boot";
|
|
428
|
+
const reactVersion =
|
|
429
|
+
typeof payload.reactVersion === "string"
|
|
430
|
+
? payload.reactVersion
|
|
431
|
+
: "unknown";
|
|
432
|
+
return `${formatMfInspectorAppLabel(event.app)} started running in the browser. Boot label: ${label}. React version seen: ${reactVersion}.`;
|
|
433
|
+
}
|
|
434
|
+
case "route-change": {
|
|
435
|
+
const label =
|
|
436
|
+
typeof payload.label === "string" ? payload.label : "route change";
|
|
437
|
+
return `The page route changed, so the inspector took another snapshot of the shared packages. Trigger: ${label}.`;
|
|
438
|
+
}
|
|
439
|
+
case "share-snapshot": {
|
|
440
|
+
const shareScopes = asInspectorRecord(payload.shareScopes);
|
|
441
|
+
const packageCount = Object.entries(shareScopes).reduce(
|
|
442
|
+
(count, [scopeName, scopeValue]) => {
|
|
443
|
+
if (scopeName === "__error") return count;
|
|
444
|
+
return count + Object.keys(asInspectorRecord(scopeValue)).length;
|
|
445
|
+
},
|
|
446
|
+
0,
|
|
447
|
+
);
|
|
448
|
+
return `The inspector took a fresh picture of webpack's shared-package list and saw ${packageCount} package${packageCount === 1 ? "" : "s"}.`;
|
|
449
|
+
}
|
|
450
|
+
case "remote-load-start":
|
|
451
|
+
return `The host started requesting ${typeof payload.remoteKey === "string" ? payload.remoteKey : "a remote module"}. This is the moment remote code begins loading into the host page.`;
|
|
452
|
+
case "remote-load-success": {
|
|
453
|
+
const remoteKey =
|
|
454
|
+
typeof payload.remoteKey === "string"
|
|
455
|
+
? payload.remoteKey
|
|
456
|
+
: "remote module";
|
|
457
|
+
const durationMs = asInspectorNumber(payload.durationMs);
|
|
458
|
+
return durationMs === null
|
|
459
|
+
? `The host finished loading ${remoteKey}. The remote code is now available to render.`
|
|
460
|
+
: `The host finished loading ${remoteKey} in ${durationMs}ms. The remote code is now available to render.`;
|
|
461
|
+
}
|
|
462
|
+
case "remote-load-error":
|
|
463
|
+
return `The host tried to load ${typeof payload.remoteKey === "string" ? payload.remoteKey : "a remote module"}, but it failed: ${typeof payload.message === "string" ? payload.message : "unknown error"}`;
|
|
464
|
+
case "identity-check": {
|
|
465
|
+
const remoteKey =
|
|
466
|
+
typeof payload.remoteKey === "string"
|
|
467
|
+
? payload.remoteKey
|
|
468
|
+
: "remote module";
|
|
469
|
+
const sameReact = payload.sameReactInstance === true;
|
|
470
|
+
const sameDom = payload.sameReactDomInstance === true;
|
|
471
|
+
return `After loading ${remoteKey}, the host compared its React copies with the remote's copies. Result: React ${sameReact ? "same copy" : "different copy"}, ReactDOM ${sameDom ? "same copy" : "different copy"}.`;
|
|
472
|
+
}
|
|
473
|
+
case "identity-check-error":
|
|
474
|
+
return `The remote loaded, but the follow-up React comparison could not finish for ${typeof payload.remoteKey === "string" ? payload.remoteKey : "that module"}: ${typeof payload.message === "string" ? payload.message : "unknown error"}`;
|
|
475
|
+
case "share-init-error":
|
|
476
|
+
return `Webpack could not prepare its shared-package system before the check ran: ${typeof payload.message === "string" ? payload.message : "unknown error"}`;
|
|
477
|
+
default:
|
|
478
|
+
return stringifyInspectorValue(payload);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function explainMfInspectorEvent(event: MfInspectorEvent): string {
|
|
483
|
+
switch (event.kind) {
|
|
484
|
+
case "declared-config":
|
|
485
|
+
return "This is the app's promise before runtime. It tells you what should be possible even before the browser loads anything.";
|
|
486
|
+
case "runtime-boot":
|
|
487
|
+
return "If you do not see this, that app never really started in the browser.";
|
|
488
|
+
case "route-change":
|
|
489
|
+
return "Some remotes load only after navigation, so route changes can explain why behavior changed later.";
|
|
490
|
+
case "share-snapshot":
|
|
491
|
+
return "This helps you compare what webpack is actually sharing right now versus what the config files claimed.";
|
|
492
|
+
case "remote-load-start":
|
|
493
|
+
return "If a later success row never appears, the host got stuck while trying to reach the remote.";
|
|
494
|
+
case "remote-load-success":
|
|
495
|
+
return "This proves the remote code arrived. It does not yet prove React is being shared safely.";
|
|
496
|
+
case "remote-load-error":
|
|
497
|
+
return "Common causes are a bad remote URL, a wrong expose name, or a remote build that is not serving correctly.";
|
|
498
|
+
case "identity-check":
|
|
499
|
+
return "The safest result is same copy for both React and ReactDOM. Different copies often lead to hooks, context, or state bugs.";
|
|
500
|
+
case "identity-check-error":
|
|
501
|
+
return "The remote may have loaded, but the inspector could not prove whether React was reused correctly.";
|
|
502
|
+
case "share-init-error":
|
|
503
|
+
return "Without this setup step, later sharing information may be incomplete or misleading.";
|
|
504
|
+
default:
|
|
505
|
+
return "This row is extra debug detail reported by the running apps.";
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function areMfInspectorEventsEquivalent(
|
|
510
|
+
left: MfInspectorEvent,
|
|
511
|
+
right: MfInspectorEvent,
|
|
512
|
+
): boolean {
|
|
513
|
+
return (
|
|
514
|
+
left.runtimeId === right.runtimeId &&
|
|
515
|
+
left.app === right.app &&
|
|
516
|
+
left.kind === right.kind &&
|
|
517
|
+
left.timestamp === right.timestamp &&
|
|
518
|
+
left.route === right.route &&
|
|
519
|
+
stringifyInspectorValue(left.payload) ===
|
|
520
|
+
stringifyInspectorValue(right.payload)
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
257
524
|
function isModuleFederationGeneratedPath(filePath: string): boolean {
|
|
258
525
|
return /(^|\/)dist\//.test(filePath);
|
|
259
526
|
}
|
|
260
527
|
|
|
528
|
+
function normalizeModuleFederationPreviewPath(input: string): string {
|
|
529
|
+
const trimmed = input.trim();
|
|
530
|
+
if (!trimmed) return "/";
|
|
531
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
261
534
|
function getModuleFederationCommandRoots(
|
|
262
535
|
files: Record<string, string>,
|
|
263
536
|
): string[] {
|
|
@@ -277,7 +550,7 @@ function getModuleFederationCommandRoots(
|
|
|
277
550
|
});
|
|
278
551
|
}
|
|
279
552
|
|
|
280
|
-
type SbxBottomTab = "output" | "console" | "chat";
|
|
553
|
+
type SbxBottomTab = "output" | "console" | "inspector" | "chat";
|
|
281
554
|
|
|
282
555
|
export default function CodeRunnerModal() {
|
|
283
556
|
const {
|
|
@@ -342,12 +615,15 @@ export default function CodeRunnerModal() {
|
|
|
342
615
|
const [nxStarting, setNxStarting] = useState(false);
|
|
343
616
|
const [nxError, setNxError] = useState<string | null>(null);
|
|
344
617
|
const nxIframeRef = useRef<HTMLIFrameElement>(null);
|
|
618
|
+
const mfIframeRef = useRef<HTMLIFrameElement>(null);
|
|
345
619
|
const [mfSandboxId, setMfSandboxId] = useState<string | null>(null);
|
|
346
620
|
const [mfHostUrl, setMfHostUrl] = useState<string | null>(null);
|
|
347
621
|
const [mfAppUrls, setMfAppUrls] = useState<Record<string, string>>({});
|
|
348
622
|
const [mfStarting, setMfStarting] = useState(false);
|
|
349
623
|
const [mfError, setMfError] = useState<string | null>(null);
|
|
350
624
|
const [mfPreviewApp, setMfPreviewApp] = useState("host");
|
|
625
|
+
const [mfPreviewPath, setMfPreviewPath] = useState("/");
|
|
626
|
+
const [mfNavInput, setMfNavInput] = useState("/");
|
|
351
627
|
const [mfConsoleOutput, setMfConsoleOutput] = useState<OutputLine[]>([]);
|
|
352
628
|
const [mfConsoleCommand, setMfConsoleCommand] = useState("npm run build");
|
|
353
629
|
const [mfConsoleCwd, setMfConsoleCwd] = useState("apps/host");
|
|
@@ -357,6 +633,14 @@ export default function CodeRunnerModal() {
|
|
|
357
633
|
Record<string, string>
|
|
358
634
|
>({});
|
|
359
635
|
const [mfLoadingFile, setMfLoadingFile] = useState<string | null>(null);
|
|
636
|
+
const [mfInspectorEvents, setMfInspectorEvents] = useState<
|
|
637
|
+
MfInspectorEvent[]
|
|
638
|
+
>([]);
|
|
639
|
+
const [mfInspectorView, setMfInspectorView] = useState<
|
|
640
|
+
"map" | "shares" | "events"
|
|
641
|
+
>("map");
|
|
642
|
+
const [mfInspectorGuideCollapsed, setMfInspectorGuideCollapsed] =
|
|
643
|
+
useState(false);
|
|
360
644
|
// Simulated URL bar state for Next.js mode
|
|
361
645
|
const [reactPreviewPath, setReactPreviewPath] = useState("/");
|
|
362
646
|
const [reactNavInput, setReactNavInput] = useState("/");
|
|
@@ -380,6 +664,7 @@ export default function CodeRunnerModal() {
|
|
|
380
664
|
containerW: number;
|
|
381
665
|
} | null>(null);
|
|
382
666
|
const sbxOutputDrag = useRef<{ startY: number; startH: number } | null>(null);
|
|
667
|
+
const [isDraggingResize, setIsDraggingResize] = useState(false);
|
|
383
668
|
const sbxEditorRowRef = useRef<HTMLDivElement>(null);
|
|
384
669
|
|
|
385
670
|
// ── Sandbox save state (single combined save) ──────────────────
|
|
@@ -592,12 +877,15 @@ export default function CodeRunnerModal() {
|
|
|
592
877
|
setServerCollapsed(true);
|
|
593
878
|
setClientCollapsed(false);
|
|
594
879
|
setMfPreviewApp("host");
|
|
880
|
+
setMfPreviewPath("/");
|
|
881
|
+
setMfNavInput("/");
|
|
595
882
|
setMfConsoleCommand("npm run build");
|
|
596
883
|
setMfConsoleCwd("apps/host");
|
|
597
884
|
setMfConsoleOutput([]);
|
|
598
885
|
setMfGeneratedFiles([]);
|
|
599
886
|
setMfGeneratedFileContents({});
|
|
600
887
|
setMfLoadingFile(null);
|
|
888
|
+
setMfInspectorEvents([]);
|
|
601
889
|
}
|
|
602
890
|
}
|
|
603
891
|
}, [runnerInitialSandbox]);
|
|
@@ -642,14 +930,22 @@ export default function CodeRunnerModal() {
|
|
|
642
930
|
}
|
|
643
931
|
};
|
|
644
932
|
const onUp = () => {
|
|
645
|
-
sbxDividerDrag.current
|
|
646
|
-
|
|
933
|
+
if (sbxDividerDrag.current || sbxOutputDrag.current) {
|
|
934
|
+
sbxDividerDrag.current = null;
|
|
935
|
+
sbxOutputDrag.current = null;
|
|
936
|
+
document.body.style.userSelect = "";
|
|
937
|
+
document.body.style.cursor = "";
|
|
938
|
+
setIsDraggingResize(false);
|
|
939
|
+
}
|
|
647
940
|
};
|
|
648
941
|
document.addEventListener("mousemove", onMove);
|
|
649
942
|
document.addEventListener("mouseup", onUp);
|
|
650
943
|
return () => {
|
|
651
944
|
document.removeEventListener("mousemove", onMove);
|
|
652
945
|
document.removeEventListener("mouseup", onUp);
|
|
946
|
+
document.body.style.userSelect = "";
|
|
947
|
+
document.body.style.cursor = "";
|
|
948
|
+
setIsDraggingResize(false);
|
|
653
949
|
};
|
|
654
950
|
}, []);
|
|
655
951
|
|
|
@@ -1142,6 +1438,7 @@ export default function CodeRunnerModal() {
|
|
|
1142
1438
|
setMfGeneratedFileContents({});
|
|
1143
1439
|
setMfLoadingFile(null);
|
|
1144
1440
|
setMfConsoleOutput([]);
|
|
1441
|
+
setMfInspectorEvents([]);
|
|
1145
1442
|
setSbxBottomTab("output");
|
|
1146
1443
|
setSandboxOutput([
|
|
1147
1444
|
{
|
|
@@ -1158,6 +1455,8 @@ export default function CodeRunnerModal() {
|
|
|
1158
1455
|
setMfPreviewApp(
|
|
1159
1456
|
info.appUrls.host ? "host" : (Object.keys(info.appUrls)[0] ?? "host"),
|
|
1160
1457
|
);
|
|
1458
|
+
setMfPreviewPath("/");
|
|
1459
|
+
setMfNavInput("/");
|
|
1161
1460
|
setReactClientTab("preview");
|
|
1162
1461
|
setServerCollapsed(true);
|
|
1163
1462
|
setClientCollapsed(false);
|
|
@@ -1193,6 +1492,7 @@ export default function CodeRunnerModal() {
|
|
|
1193
1492
|
setMfGeneratedFileContents({});
|
|
1194
1493
|
setMfLoadingFile(null);
|
|
1195
1494
|
setMfConsoleRunning(false);
|
|
1495
|
+
setMfInspectorEvents([]);
|
|
1196
1496
|
setSandboxOutput((prev) => [
|
|
1197
1497
|
...prev,
|
|
1198
1498
|
{
|
|
@@ -1289,6 +1589,34 @@ export default function CodeRunnerModal() {
|
|
|
1289
1589
|
return () => clearInterval(interval);
|
|
1290
1590
|
}, [mfSandboxId]);
|
|
1291
1591
|
|
|
1592
|
+
useEffect(() => {
|
|
1593
|
+
if (clientType !== "module-federation" || !mfSandboxId) return;
|
|
1594
|
+
|
|
1595
|
+
const handler = (event: MessageEvent) => {
|
|
1596
|
+
if (!isMfInspectorEvent(event.data)) return;
|
|
1597
|
+
if (event.data.sandboxId !== mfSandboxId) return;
|
|
1598
|
+
|
|
1599
|
+
const payload = asInspectorRecord(event.data.payload);
|
|
1600
|
+
const nextEvent: MfInspectorEvent = {
|
|
1601
|
+
...event.data,
|
|
1602
|
+
payload,
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
setMfInspectorEvents((prev) => {
|
|
1606
|
+
const duplicate = prev.some((entry) =>
|
|
1607
|
+
areMfInspectorEventsEquivalent(entry, nextEvent),
|
|
1608
|
+
);
|
|
1609
|
+
if (duplicate) return prev;
|
|
1610
|
+
|
|
1611
|
+
const next = [...prev, nextEvent];
|
|
1612
|
+
return next.length > 250 ? next.slice(next.length - 250) : next;
|
|
1613
|
+
});
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
window.addEventListener("message", handler);
|
|
1617
|
+
return () => window.removeEventListener("message", handler);
|
|
1618
|
+
}, [clientType, mfSandboxId]);
|
|
1619
|
+
|
|
1292
1620
|
useEffect(() => {
|
|
1293
1621
|
if (clientType !== "module-federation" || !mfSandboxId) return;
|
|
1294
1622
|
if (!reactActiveFile) return;
|
|
@@ -1449,8 +1777,9 @@ export default function CodeRunnerModal() {
|
|
|
1449
1777
|
setMfGeneratedFileContents({});
|
|
1450
1778
|
setMfLoadingFile(null);
|
|
1451
1779
|
setMfConsoleRunning(false);
|
|
1780
|
+
setMfInspectorEvents([]);
|
|
1452
1781
|
setSbxBottomTab((current) =>
|
|
1453
|
-
current === "console" ? "output" : current,
|
|
1782
|
+
current === "console" || current === "inspector" ? "output" : current,
|
|
1454
1783
|
);
|
|
1455
1784
|
}
|
|
1456
1785
|
}, [clientType, nxSandboxId, mfSandboxId]);
|
|
@@ -1474,8 +1803,9 @@ export default function CodeRunnerModal() {
|
|
|
1474
1803
|
setMfLoadingFile(null);
|
|
1475
1804
|
setMfConsoleOutput([]);
|
|
1476
1805
|
setMfConsoleRunning(false);
|
|
1806
|
+
setMfInspectorEvents([]);
|
|
1477
1807
|
setSbxBottomTab((current) =>
|
|
1478
|
-
current === "console" ? "output" : current,
|
|
1808
|
+
current === "console" || current === "inspector" ? "output" : current,
|
|
1479
1809
|
);
|
|
1480
1810
|
}
|
|
1481
1811
|
if (ct !== "script") {
|
|
@@ -1494,6 +1824,8 @@ export default function CodeRunnerModal() {
|
|
|
1494
1824
|
setServerCollapsed(true);
|
|
1495
1825
|
setClientCollapsed(false);
|
|
1496
1826
|
setMfPreviewApp("host");
|
|
1827
|
+
setMfPreviewPath("/");
|
|
1828
|
+
setMfNavInput("/");
|
|
1497
1829
|
setMfError(null);
|
|
1498
1830
|
}
|
|
1499
1831
|
if (ct === "module-federation") {
|
|
@@ -1757,6 +2089,16 @@ export default function CodeRunnerModal() {
|
|
|
1757
2089
|
const moduleFederationGeneratedFileSet = new Set(
|
|
1758
2090
|
moduleFederationGeneratedFiles,
|
|
1759
2091
|
);
|
|
2092
|
+
const moduleFederationPreviewBaseUrl =
|
|
2093
|
+
mfAppUrls[mfPreviewApp] ?? mfHostUrl ?? "";
|
|
2094
|
+
const moduleFederationPreviewPath =
|
|
2095
|
+
normalizeModuleFederationPreviewPath(mfPreviewPath);
|
|
2096
|
+
const moduleFederationPreviewUrl = moduleFederationPreviewBaseUrl
|
|
2097
|
+
? `${moduleFederationPreviewBaseUrl}${moduleFederationPreviewPath}`
|
|
2098
|
+
: "";
|
|
2099
|
+
const moduleFederationPreviewHostLabel = moduleFederationPreviewBaseUrl
|
|
2100
|
+
? moduleFederationPreviewBaseUrl.replace(/^https?:\/\//, "")
|
|
2101
|
+
: "localhost";
|
|
1760
2102
|
const visibleReactFiles =
|
|
1761
2103
|
clientType === "module-federation"
|
|
1762
2104
|
? Array.from(
|
|
@@ -1769,6 +2111,377 @@ export default function CodeRunnerModal() {
|
|
|
1769
2111
|
const isActiveModuleFederationGeneratedFile =
|
|
1770
2112
|
clientType === "module-federation" &&
|
|
1771
2113
|
moduleFederationGeneratedFileSet.has(reactActiveFile);
|
|
2114
|
+
const mfInspectorRuntimeCount = new Set(
|
|
2115
|
+
mfInspectorEvents.map((event) => event.runtimeId),
|
|
2116
|
+
).size;
|
|
2117
|
+
const mfInspectorLastEvent =
|
|
2118
|
+
mfInspectorEvents[mfInspectorEvents.length - 1] ?? null;
|
|
2119
|
+
const mfInspectorDeclaredConfigMap = new Map<
|
|
2120
|
+
string,
|
|
2121
|
+
{
|
|
2122
|
+
app: string;
|
|
2123
|
+
declaredConfig: Record<string, unknown>;
|
|
2124
|
+
timestamp: number;
|
|
2125
|
+
source: string;
|
|
2126
|
+
}
|
|
2127
|
+
>();
|
|
2128
|
+
const mfInspectorShareScopeMap = new Map<
|
|
2129
|
+
string,
|
|
2130
|
+
{
|
|
2131
|
+
app: string;
|
|
2132
|
+
shareScopes: Record<string, unknown>;
|
|
2133
|
+
timestamp: number;
|
|
2134
|
+
route: string;
|
|
2135
|
+
source: string;
|
|
2136
|
+
runtimeId: string;
|
|
2137
|
+
}
|
|
2138
|
+
>();
|
|
2139
|
+
const mfInspectorIdentityEvents: MfInspectorEvent[] = [];
|
|
2140
|
+
|
|
2141
|
+
for (const event of mfInspectorEvents) {
|
|
2142
|
+
const payload = asInspectorRecord(event.payload);
|
|
2143
|
+
const declaredConfig = asInspectorRecord(payload.declaredConfig);
|
|
2144
|
+
if (Object.keys(declaredConfig).length > 0) {
|
|
2145
|
+
const configApp =
|
|
2146
|
+
event.kind === "identity-check" && typeof payload.remoteApp === "string"
|
|
2147
|
+
? payload.remoteApp
|
|
2148
|
+
: event.app;
|
|
2149
|
+
mfInspectorDeclaredConfigMap.set(configApp, {
|
|
2150
|
+
app: configApp,
|
|
2151
|
+
declaredConfig,
|
|
2152
|
+
timestamp: event.timestamp,
|
|
2153
|
+
source: event.kind,
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const shareScopes = asInspectorRecord(payload.shareScopes);
|
|
2158
|
+
if (Object.keys(shareScopes).length > 0) {
|
|
2159
|
+
mfInspectorShareScopeMap.set(event.app, {
|
|
2160
|
+
app: event.app,
|
|
2161
|
+
shareScopes,
|
|
2162
|
+
timestamp: event.timestamp,
|
|
2163
|
+
route: event.route,
|
|
2164
|
+
source: event.kind,
|
|
2165
|
+
runtimeId: event.runtimeId,
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
if (
|
|
2170
|
+
event.kind === "identity-check" ||
|
|
2171
|
+
event.kind === "identity-check-error"
|
|
2172
|
+
) {
|
|
2173
|
+
mfInspectorIdentityEvents.push(event);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
const mfInspectorDeclaredConfigs = Array.from(
|
|
2178
|
+
mfInspectorDeclaredConfigMap.values(),
|
|
2179
|
+
).sort((left, right) => left.app.localeCompare(right.app));
|
|
2180
|
+
const mfInspectorShareSnapshots = Array.from(
|
|
2181
|
+
mfInspectorShareScopeMap.values(),
|
|
2182
|
+
).sort((left, right) => left.app.localeCompare(right.app));
|
|
2183
|
+
const mfInspectorRecentEvents = [...mfInspectorEvents].slice(-60).reverse();
|
|
2184
|
+
|
|
2185
|
+
// ── Inspector topology derived data ──────────────────────────────────────
|
|
2186
|
+
const mfInspectorBootMap = new Map<
|
|
2187
|
+
string,
|
|
2188
|
+
{
|
|
2189
|
+
reactVersion: string | null;
|
|
2190
|
+
reactDomVersion: string | null;
|
|
2191
|
+
timestamp: number;
|
|
2192
|
+
}
|
|
2193
|
+
>();
|
|
2194
|
+
const mfInspectorRemoteLoadMap = new Map<
|
|
2195
|
+
string,
|
|
2196
|
+
{
|
|
2197
|
+
status: "loading" | "success" | "error";
|
|
2198
|
+
durationMs: number | null;
|
|
2199
|
+
message: string | null;
|
|
2200
|
+
}
|
|
2201
|
+
>();
|
|
2202
|
+
const mfInspectorIdentityResultMap = new Map<
|
|
2203
|
+
string,
|
|
2204
|
+
{
|
|
2205
|
+
sameReact: boolean;
|
|
2206
|
+
sameReactDom: boolean;
|
|
2207
|
+
remoteApp: string;
|
|
2208
|
+
reactVersion: string | null;
|
|
2209
|
+
reactDomVersion: string | null;
|
|
2210
|
+
error: string | null;
|
|
2211
|
+
}
|
|
2212
|
+
>();
|
|
2213
|
+
|
|
2214
|
+
for (const ev of mfInspectorEvents) {
|
|
2215
|
+
const ep = asInspectorRecord(ev.payload);
|
|
2216
|
+
if (ev.kind === "runtime-boot") {
|
|
2217
|
+
mfInspectorBootMap.set(ev.app, {
|
|
2218
|
+
reactVersion:
|
|
2219
|
+
typeof ep.reactVersion === "string" ? ep.reactVersion : null,
|
|
2220
|
+
reactDomVersion:
|
|
2221
|
+
typeof ep.reactDomVersion === "string" ? ep.reactDomVersion : null,
|
|
2222
|
+
timestamp: ev.timestamp,
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
if (ev.kind === "remote-load-start") {
|
|
2226
|
+
const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
|
|
2227
|
+
// Always reset to "loading" — a fresh load-start means a retry is in
|
|
2228
|
+
// progress (e.g. after the user reloads the preview), so any previous
|
|
2229
|
+
// error/success state should be replaced.
|
|
2230
|
+
mfInspectorRemoteLoadMap.set(rk, {
|
|
2231
|
+
status: "loading",
|
|
2232
|
+
durationMs: null,
|
|
2233
|
+
message: null,
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
if (ev.kind === "remote-load-success") {
|
|
2237
|
+
const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
|
|
2238
|
+
mfInspectorRemoteLoadMap.set(rk, {
|
|
2239
|
+
status: "success",
|
|
2240
|
+
durationMs: asInspectorNumber(ep.durationMs),
|
|
2241
|
+
message: null,
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
if (ev.kind === "remote-load-error") {
|
|
2245
|
+
const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
|
|
2246
|
+
mfInspectorRemoteLoadMap.set(rk, {
|
|
2247
|
+
status: "error",
|
|
2248
|
+
durationMs: null,
|
|
2249
|
+
message: typeof ep.message === "string" ? ep.message : "error",
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
for (const ev of mfInspectorIdentityEvents) {
|
|
2255
|
+
const ep = asInspectorRecord(ev.payload);
|
|
2256
|
+
const rk = typeof ep.remoteKey === "string" ? ep.remoteKey : "unknown";
|
|
2257
|
+
if (ev.kind === "identity-check") {
|
|
2258
|
+
mfInspectorIdentityResultMap.set(rk, {
|
|
2259
|
+
sameReact: ep.sameReactInstance === true,
|
|
2260
|
+
sameReactDom: ep.sameReactDomInstance === true,
|
|
2261
|
+
remoteApp:
|
|
2262
|
+
typeof ep.remoteApp === "string"
|
|
2263
|
+
? ep.remoteApp
|
|
2264
|
+
: rk.split("/")[0] || "unknown",
|
|
2265
|
+
reactVersion:
|
|
2266
|
+
typeof ep.reactVersion === "string" ? ep.reactVersion : null,
|
|
2267
|
+
reactDomVersion:
|
|
2268
|
+
typeof ep.reactDomVersion === "string" ? ep.reactDomVersion : null,
|
|
2269
|
+
error: null,
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
if (ev.kind === "identity-check-error") {
|
|
2273
|
+
mfInspectorIdentityResultMap.set(rk, {
|
|
2274
|
+
sameReact: false,
|
|
2275
|
+
sameReactDom: false,
|
|
2276
|
+
remoteApp: rk.split("/")[0] || "unknown",
|
|
2277
|
+
reactVersion: null,
|
|
2278
|
+
reactDomVersion: null,
|
|
2279
|
+
error: typeof ep.message === "string" ? ep.message : "error",
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// Remote app names for the topology
|
|
2285
|
+
const mfTopologyRemoteNames = Array.from(
|
|
2286
|
+
new Set([
|
|
2287
|
+
...mfInspectorDeclaredConfigs
|
|
2288
|
+
.filter((c) => c.app !== "host")
|
|
2289
|
+
.map((c) => c.app),
|
|
2290
|
+
...Array.from(mfInspectorBootMap.keys()).filter((k) => k !== "host"),
|
|
2291
|
+
...Array.from(mfInspectorShareScopeMap.keys()).filter(
|
|
2292
|
+
(k) => k !== "host",
|
|
2293
|
+
),
|
|
2294
|
+
]),
|
|
2295
|
+
);
|
|
2296
|
+
|
|
2297
|
+
// Remote keys grouped by remote app name (e.g. "profile" → ["profile/ProfileCard"])
|
|
2298
|
+
// Sources: load events (from host loading), identity events (from host identity check),
|
|
2299
|
+
// and declared config (so declared-but-unloaded remotes are always wired up).
|
|
2300
|
+
const mfTopologyRemoteKeysByApp = new Map<string, string[]>();
|
|
2301
|
+
const _addRemoteKey = (remoteKey: string) => {
|
|
2302
|
+
const appName = remoteKey.split("/")[0] || remoteKey;
|
|
2303
|
+
if (!mfTopologyRemoteKeysByApp.has(appName)) {
|
|
2304
|
+
mfTopologyRemoteKeysByApp.set(appName, []);
|
|
2305
|
+
}
|
|
2306
|
+
const existing = mfTopologyRemoteKeysByApp.get(appName)!;
|
|
2307
|
+
if (!existing.includes(remoteKey)) existing.push(remoteKey);
|
|
2308
|
+
};
|
|
2309
|
+
// From load events
|
|
2310
|
+
for (const [remoteKey] of mfInspectorRemoteLoadMap) {
|
|
2311
|
+
_addRemoteKey(remoteKey);
|
|
2312
|
+
}
|
|
2313
|
+
// From identity events (covers the case where load events were missed)
|
|
2314
|
+
for (const [remoteKey] of mfInspectorIdentityResultMap) {
|
|
2315
|
+
_addRemoteKey(remoteKey);
|
|
2316
|
+
}
|
|
2317
|
+
// From declared config — so every declared remote gets a key even before any events
|
|
2318
|
+
for (const { app: cfgApp, declaredConfig } of mfInspectorDeclaredConfigs) {
|
|
2319
|
+
if (cfgApp !== "host") continue; // only the host declares remotes it consumes
|
|
2320
|
+
const remotes = asInspectorRecord(declaredConfig.remotes);
|
|
2321
|
+
for (const remoteName of Object.keys(remotes)) {
|
|
2322
|
+
// Derive the canonical remote key from declared exposes, or fall back to
|
|
2323
|
+
// the key pattern seen in load/identity events for this remote app.
|
|
2324
|
+
const existingKeys = mfTopologyRemoteKeysByApp.get(remoteName);
|
|
2325
|
+
if (!existingKeys || existingKeys.length === 0) {
|
|
2326
|
+
// Find any declared config for the remote itself to get its expose list
|
|
2327
|
+
const remoteCfg = mfInspectorDeclaredConfigs.find(
|
|
2328
|
+
(c) => c.app === remoteName,
|
|
2329
|
+
);
|
|
2330
|
+
const exposes = asInspectorRecord(
|
|
2331
|
+
remoteCfg ? remoteCfg.declaredConfig.exposes : {},
|
|
2332
|
+
);
|
|
2333
|
+
const exposeKeys = Object.keys(exposes);
|
|
2334
|
+
if (exposeKeys.length > 0) {
|
|
2335
|
+
for (const exposeKey of exposeKeys) {
|
|
2336
|
+
// exposeKey is like "./ProfileCard"; canonical remote key is "profile/ProfileCard"
|
|
2337
|
+
const moduleName = exposeKey.replace(/^\.\//, "");
|
|
2338
|
+
_addRemoteKey(`${remoteName}/${moduleName}`);
|
|
2339
|
+
}
|
|
2340
|
+
} else {
|
|
2341
|
+
// No expose info yet — create a placeholder key so the node appears
|
|
2342
|
+
_addRemoteKey(`${remoteName}/__placeholder`);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// Best share scope snapshot for the matrix (host has the most complete view)
|
|
2349
|
+
const mfShareScopeForMatrix =
|
|
2350
|
+
mfInspectorShareScopeMap.get("host") ??
|
|
2351
|
+
(mfInspectorShareSnapshots.length > 0
|
|
2352
|
+
? mfInspectorShareSnapshots[mfInspectorShareSnapshots.length - 1]
|
|
2353
|
+
: null);
|
|
2354
|
+
const mfInspectorRemoteTableKeys = Array.from(
|
|
2355
|
+
new Set([
|
|
2356
|
+
...Array.from(mfInspectorRemoteLoadMap.keys()),
|
|
2357
|
+
...Array.from(mfInspectorIdentityResultMap.keys()),
|
|
2358
|
+
]),
|
|
2359
|
+
)
|
|
2360
|
+
.filter((remoteKey) => !remoteKey.endsWith("/__placeholder"))
|
|
2361
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2362
|
+
const mfInspectorLoadAttemptCount = mfInspectorRemoteTableKeys.filter(
|
|
2363
|
+
(remoteKey) => mfInspectorRemoteLoadMap.has(remoteKey),
|
|
2364
|
+
).length;
|
|
2365
|
+
const mfInspectorSharedRemoteCount = mfInspectorRemoteTableKeys.filter(
|
|
2366
|
+
(remoteKey) => {
|
|
2367
|
+
const result = mfInspectorIdentityResultMap.get(remoteKey);
|
|
2368
|
+
return Boolean(
|
|
2369
|
+
result && !result.error && result.sameReact && result.sameReactDom,
|
|
2370
|
+
);
|
|
2371
|
+
},
|
|
2372
|
+
).length;
|
|
2373
|
+
const mfInspectorDifferentRemoteCount = mfInspectorRemoteTableKeys.filter(
|
|
2374
|
+
(remoteKey) => {
|
|
2375
|
+
const result = mfInspectorIdentityResultMap.get(remoteKey);
|
|
2376
|
+
return Boolean(
|
|
2377
|
+
result && !result.error && (!result.sameReact || !result.sameReactDom),
|
|
2378
|
+
);
|
|
2379
|
+
},
|
|
2380
|
+
).length;
|
|
2381
|
+
const mfInspectorErroredRemoteCount = mfInspectorRemoteTableKeys.filter(
|
|
2382
|
+
(remoteKey) => {
|
|
2383
|
+
const result = mfInspectorIdentityResultMap.get(remoteKey);
|
|
2384
|
+
const loadInfo = mfInspectorRemoteLoadMap.get(remoteKey);
|
|
2385
|
+
return Boolean(result?.error) || loadInfo?.status === "error";
|
|
2386
|
+
},
|
|
2387
|
+
).length;
|
|
2388
|
+
const mfInspectorPendingRemoteCount = mfInspectorRemoteTableKeys.filter(
|
|
2389
|
+
(remoteKey) => {
|
|
2390
|
+
const result = mfInspectorIdentityResultMap.get(remoteKey);
|
|
2391
|
+
const loadInfo = mfInspectorRemoteLoadMap.get(remoteKey);
|
|
2392
|
+
return (
|
|
2393
|
+
!result &&
|
|
2394
|
+
(loadInfo?.status === "loading" || loadInfo?.status === "success")
|
|
2395
|
+
);
|
|
2396
|
+
},
|
|
2397
|
+
).length;
|
|
2398
|
+
const mfInspectorIssueCount =
|
|
2399
|
+
mfInspectorDifferentRemoteCount + mfInspectorErroredRemoteCount;
|
|
2400
|
+
const mfInspectorViewCopy = MF_INSPECTOR_VIEW_COPY[mfInspectorView];
|
|
2401
|
+
const mfInspectorSummaryCards = [
|
|
2402
|
+
{
|
|
2403
|
+
label: "Host page",
|
|
2404
|
+
value: mfInspectorBootMap.has("host") ? "running" : "waiting",
|
|
2405
|
+
toneClass: mfInspectorBootMap.has("host")
|
|
2406
|
+
? "text-sky-300"
|
|
2407
|
+
: "text-amber-300",
|
|
2408
|
+
hint: "The host is the page that loads remotes.",
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
label: "Remote modules",
|
|
2412
|
+
value: String(mfInspectorRemoteTableKeys.length),
|
|
2413
|
+
toneClass: "text-slate-100",
|
|
2414
|
+
hint: "Separate pieces of remote code the host has seen.",
|
|
2415
|
+
},
|
|
2416
|
+
{
|
|
2417
|
+
label: "Load attempts",
|
|
2418
|
+
value: String(mfInspectorLoadAttemptCount),
|
|
2419
|
+
toneClass:
|
|
2420
|
+
mfInspectorLoadAttemptCount > 0 ? "text-cyan-300" : "text-slate-400",
|
|
2421
|
+
hint: "How many remote modules the host actually tried to fetch.",
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
label: "Same React copy",
|
|
2425
|
+
value: String(mfInspectorSharedRemoteCount),
|
|
2426
|
+
toneClass:
|
|
2427
|
+
mfInspectorSharedRemoteCount > 0
|
|
2428
|
+
? "text-emerald-300"
|
|
2429
|
+
: "text-slate-400",
|
|
2430
|
+
hint: "Healthy result after the comparison step.",
|
|
2431
|
+
},
|
|
2432
|
+
{
|
|
2433
|
+
label: "Waiting / issues",
|
|
2434
|
+
value: `${mfInspectorPendingRemoteCount} / ${mfInspectorIssueCount}`,
|
|
2435
|
+
toneClass:
|
|
2436
|
+
mfInspectorIssueCount > 0
|
|
2437
|
+
? "text-red-300"
|
|
2438
|
+
: mfInspectorPendingRemoteCount > 0
|
|
2439
|
+
? "text-amber-300"
|
|
2440
|
+
: "text-emerald-300",
|
|
2441
|
+
hint: "First number is still loading/checking. Second number needs attention.",
|
|
2442
|
+
},
|
|
2443
|
+
];
|
|
2444
|
+
|
|
2445
|
+
const renderMfInspectorTruthCell = (
|
|
2446
|
+
sameInstance: boolean | null,
|
|
2447
|
+
error: string | null,
|
|
2448
|
+
loadState: "loading" | "success" | "error" | null,
|
|
2449
|
+
) => {
|
|
2450
|
+
let label = "not requested";
|
|
2451
|
+
let toneClass = "text-slate-400";
|
|
2452
|
+
let dotClass = "bg-slate-500";
|
|
2453
|
+
|
|
2454
|
+
if (error || loadState === "error") {
|
|
2455
|
+
label = "load failed";
|
|
2456
|
+
toneClass = "text-red-400";
|
|
2457
|
+
dotClass = "bg-red-400";
|
|
2458
|
+
} else if (sameInstance === true) {
|
|
2459
|
+
label = "same copy";
|
|
2460
|
+
toneClass = "text-emerald-400";
|
|
2461
|
+
dotClass = "bg-emerald-400";
|
|
2462
|
+
} else if (sameInstance === false) {
|
|
2463
|
+
label = "different copy";
|
|
2464
|
+
toneClass = "text-red-400";
|
|
2465
|
+
dotClass = "bg-red-400";
|
|
2466
|
+
} else if (loadState === "loading") {
|
|
2467
|
+
label = "loading";
|
|
2468
|
+
toneClass = "text-amber-400";
|
|
2469
|
+
dotClass = "bg-amber-400";
|
|
2470
|
+
} else if (loadState === "success") {
|
|
2471
|
+
label = "checking React";
|
|
2472
|
+
toneClass = "text-amber-400";
|
|
2473
|
+
dotClass = "bg-amber-400";
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
return (
|
|
2477
|
+
<span
|
|
2478
|
+
className={`inline-flex items-center gap-1 text-[10px] font-medium ${toneClass}`}
|
|
2479
|
+
>
|
|
2480
|
+
<span className={`w-1.5 h-1.5 rounded-full ${dotClass}`} />
|
|
2481
|
+
{label}
|
|
2482
|
+
</span>
|
|
2483
|
+
);
|
|
2484
|
+
};
|
|
1772
2485
|
|
|
1773
2486
|
return (
|
|
1774
2487
|
<div
|
|
@@ -2398,6 +3111,9 @@ export default function CodeRunnerModal() {
|
|
|
2398
3111
|
startPct: sbxSplit,
|
|
2399
3112
|
containerW,
|
|
2400
3113
|
};
|
|
3114
|
+
document.body.style.userSelect = "none";
|
|
3115
|
+
document.body.style.cursor = "col-resize";
|
|
3116
|
+
setIsDraggingResize(true);
|
|
2401
3117
|
}}
|
|
2402
3118
|
/>
|
|
2403
3119
|
)}
|
|
@@ -3230,12 +3946,31 @@ export default function CodeRunnerModal() {
|
|
|
3230
3946
|
</div>
|
|
3231
3947
|
)}
|
|
3232
3948
|
{clientType === "module-federation" && (
|
|
3233
|
-
<div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0
|
|
3949
|
+
<div className="flex items-center gap-1 px-2 py-1 bg-slate-800 border-b border-slate-700 shrink-0 min-w-0">
|
|
3234
3950
|
{Object.entries(mfAppUrls).map(([name, url]) => (
|
|
3235
3951
|
<button
|
|
3236
3952
|
key={name}
|
|
3237
3953
|
type="button"
|
|
3238
|
-
onClick={() =>
|
|
3954
|
+
onClick={() => {
|
|
3955
|
+
setMfPreviewApp(name);
|
|
3956
|
+
// Always imperatively set the iframe src so the
|
|
3957
|
+
// page reloads even if the React-controlled src
|
|
3958
|
+
// didn't change (e.g. clicking the active tab,
|
|
3959
|
+
// or switching back to the same URL after SPA
|
|
3960
|
+
// navigation). This lets React.lazy retry any
|
|
3961
|
+
// previously-failed remote loads.
|
|
3962
|
+
if (mfIframeRef.current && url) {
|
|
3963
|
+
const target = url.endsWith("/")
|
|
3964
|
+
? url
|
|
3965
|
+
: url + "/";
|
|
3966
|
+
mfIframeRef.current.src =
|
|
3967
|
+
target +
|
|
3968
|
+
moduleFederationPreviewPath.replace(
|
|
3969
|
+
/^\//,
|
|
3970
|
+
"",
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3973
|
+
}}
|
|
3239
3974
|
className={`px-2 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 ${
|
|
3240
3975
|
mfPreviewApp === name
|
|
3241
3976
|
? "bg-slate-700 text-slate-100"
|
|
@@ -3246,11 +3981,69 @@ export default function CodeRunnerModal() {
|
|
|
3246
3981
|
{name}
|
|
3247
3982
|
</button>
|
|
3248
3983
|
))}
|
|
3249
|
-
<
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3984
|
+
<button
|
|
3985
|
+
type="button"
|
|
3986
|
+
onClick={() => {
|
|
3987
|
+
if (
|
|
3988
|
+
mfIframeRef.current &&
|
|
3989
|
+
moduleFederationPreviewUrl
|
|
3990
|
+
) {
|
|
3991
|
+
mfIframeRef.current.src =
|
|
3992
|
+
moduleFederationPreviewUrl;
|
|
3993
|
+
}
|
|
3994
|
+
}}
|
|
3995
|
+
className="p-0.5 rounded text-slate-500 hover:text-slate-200 disabled:opacity-30 disabled:cursor-not-allowed transition-colors shrink-0"
|
|
3996
|
+
title="Refresh"
|
|
3997
|
+
disabled={!moduleFederationPreviewUrl}
|
|
3998
|
+
>
|
|
3999
|
+
<svg
|
|
4000
|
+
className="w-3 h-3"
|
|
4001
|
+
viewBox="0 0 16 16"
|
|
4002
|
+
fill="currentColor"
|
|
4003
|
+
>
|
|
4004
|
+
<path d="M13.65 2.35A8 8 0 1 0 15 8h-2a6 6 0 1 1-1.1-3.48L10 6h5V1l-1.35 1.35z" />
|
|
4005
|
+
</svg>
|
|
4006
|
+
</button>
|
|
4007
|
+
<form
|
|
4008
|
+
className="flex-1 flex items-center gap-1 bg-slate-900 border border-slate-600 rounded px-2 py-0.5 focus-within:border-cyan-500/60 transition-colors min-w-0"
|
|
4009
|
+
onSubmit={(e) => {
|
|
4010
|
+
e.preventDefault();
|
|
4011
|
+
const nextPath =
|
|
4012
|
+
normalizeModuleFederationPreviewPath(
|
|
4013
|
+
mfNavInput,
|
|
4014
|
+
);
|
|
4015
|
+
setMfPreviewPath(nextPath);
|
|
4016
|
+
setMfNavInput(nextPath);
|
|
4017
|
+
if (
|
|
4018
|
+
nextPath === moduleFederationPreviewPath &&
|
|
4019
|
+
mfIframeRef.current &&
|
|
4020
|
+
moduleFederationPreviewBaseUrl
|
|
4021
|
+
) {
|
|
4022
|
+
mfIframeRef.current.src = `${moduleFederationPreviewBaseUrl}${nextPath}`;
|
|
4023
|
+
}
|
|
4024
|
+
}}
|
|
4025
|
+
>
|
|
4026
|
+
<span className="text-slate-600 text-[9px] font-mono select-none shrink-0">
|
|
4027
|
+
{moduleFederationPreviewHostLabel}
|
|
4028
|
+
</span>
|
|
4029
|
+
<input
|
|
4030
|
+
value={mfNavInput}
|
|
4031
|
+
onChange={(e) => setMfNavInput(e.target.value)}
|
|
4032
|
+
onFocus={(e) => e.target.select()}
|
|
4033
|
+
className="flex-1 bg-transparent text-[11px] font-mono text-slate-200 outline-none placeholder-slate-600 min-w-0"
|
|
4034
|
+
placeholder="/"
|
|
4035
|
+
spellCheck={false}
|
|
4036
|
+
/>
|
|
4037
|
+
</form>
|
|
4038
|
+
{moduleFederationPreviewUrl ? (
|
|
4039
|
+
<span className="text-[9px] font-mono text-green-400 shrink-0">
|
|
4040
|
+
● live
|
|
4041
|
+
</span>
|
|
4042
|
+
) : (
|
|
4043
|
+
<span className="text-[9px] font-mono text-slate-600 shrink-0">
|
|
4044
|
+
Start webpack to preview
|
|
4045
|
+
</span>
|
|
4046
|
+
)}
|
|
3254
4047
|
</div>
|
|
3255
4048
|
)}
|
|
3256
4049
|
{((clientType === "module-federation" && mfError) ||
|
|
@@ -3283,6 +4076,11 @@ export default function CodeRunnerModal() {
|
|
|
3283
4076
|
ref={nxIframeRef}
|
|
3284
4077
|
src={nxSandboxUrl + reactPreviewPath}
|
|
3285
4078
|
className="flex-1 min-h-0 w-full border-0 bg-white"
|
|
4079
|
+
style={
|
|
4080
|
+
isDraggingResize
|
|
4081
|
+
? { pointerEvents: "none" }
|
|
4082
|
+
: undefined
|
|
4083
|
+
}
|
|
3286
4084
|
title="Next.js Preview"
|
|
3287
4085
|
onLoad={() => {
|
|
3288
4086
|
try {
|
|
@@ -3302,10 +4100,16 @@ export default function CodeRunnerModal() {
|
|
|
3302
4100
|
{!mfStarting &&
|
|
3303
4101
|
clientType === "module-federation" &&
|
|
3304
4102
|
mfSandboxId &&
|
|
3305
|
-
|
|
4103
|
+
moduleFederationPreviewUrl && (
|
|
3306
4104
|
<iframe
|
|
3307
|
-
|
|
4105
|
+
ref={mfIframeRef}
|
|
4106
|
+
src={moduleFederationPreviewUrl}
|
|
3308
4107
|
className="flex-1 min-h-0 w-full border-0 bg-white"
|
|
4108
|
+
style={
|
|
4109
|
+
isDraggingResize
|
|
4110
|
+
? { pointerEvents: "none" }
|
|
4111
|
+
: undefined
|
|
4112
|
+
}
|
|
3309
4113
|
title="Webpack Module Federation Preview"
|
|
3310
4114
|
/>
|
|
3311
4115
|
)}
|
|
@@ -3351,12 +4155,15 @@ export default function CodeRunnerModal() {
|
|
|
3351
4155
|
startH: outputCollapsed ? 0 : sbxOutputH,
|
|
3352
4156
|
};
|
|
3353
4157
|
if (outputCollapsed) setOutputCollapsed(false);
|
|
4158
|
+
document.body.style.userSelect = "none";
|
|
4159
|
+
document.body.style.cursor = "row-resize";
|
|
4160
|
+
setIsDraggingResize(true);
|
|
3354
4161
|
}}
|
|
3355
4162
|
/>
|
|
3356
4163
|
|
|
3357
4164
|
{/* Sandbox output pane */}
|
|
3358
4165
|
<div
|
|
3359
|
-
className="bg-slate-950 flex flex-col overflow-hidden shrink-0
|
|
4166
|
+
className="bg-slate-950 flex flex-col overflow-hidden shrink-0"
|
|
3360
4167
|
style={{ height: outputCollapsed ? 0 : sbxOutputH }}
|
|
3361
4168
|
>
|
|
3362
4169
|
{/* Tab bar */}
|
|
@@ -3390,6 +4197,20 @@ export default function CodeRunnerModal() {
|
|
|
3390
4197
|
Console
|
|
3391
4198
|
</button>
|
|
3392
4199
|
)}
|
|
4200
|
+
{clientType === "module-federation" && (
|
|
4201
|
+
<button
|
|
4202
|
+
type="button"
|
|
4203
|
+
onClick={() => setSbxBottomTab("inspector")}
|
|
4204
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider font-medium border-b-2 transition-colors ${
|
|
4205
|
+
sbxBottomTab === "inspector"
|
|
4206
|
+
? "border-amber-500 text-amber-300"
|
|
4207
|
+
: "border-transparent text-slate-500 hover:text-slate-300"
|
|
4208
|
+
}`}
|
|
4209
|
+
>
|
|
4210
|
+
<Eye className="w-3 h-3" />
|
|
4211
|
+
Inspector
|
|
4212
|
+
</button>
|
|
4213
|
+
)}
|
|
3393
4214
|
<button
|
|
3394
4215
|
type="button"
|
|
3395
4216
|
onClick={() => {
|
|
@@ -3413,6 +4234,30 @@ export default function CodeRunnerModal() {
|
|
|
3413
4234
|
{sbxBottomTab === "console" && mfConsoleRunning && (
|
|
3414
4235
|
<Loader2 className="w-3 h-3 text-cyan-400 animate-spin mr-1" />
|
|
3415
4236
|
)}
|
|
4237
|
+
{sbxBottomTab === "inspector" && mfInspectorEvents.length > 0 && (
|
|
4238
|
+
<div className="flex items-center gap-1 mr-1">
|
|
4239
|
+
<button
|
|
4240
|
+
type="button"
|
|
4241
|
+
onClick={() =>
|
|
4242
|
+
navigator.clipboard.writeText(
|
|
4243
|
+
JSON.stringify(mfInspectorEvents, null, 2),
|
|
4244
|
+
)
|
|
4245
|
+
}
|
|
4246
|
+
className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
|
|
4247
|
+
title="Copy inspector events"
|
|
4248
|
+
>
|
|
4249
|
+
<Copy className="w-3 h-3" />
|
|
4250
|
+
</button>
|
|
4251
|
+
<button
|
|
4252
|
+
type="button"
|
|
4253
|
+
onClick={() => setMfInspectorEvents([])}
|
|
4254
|
+
className="p-0.5 rounded text-slate-600 hover:text-slate-300 transition-colors"
|
|
4255
|
+
title="Clear inspector events"
|
|
4256
|
+
>
|
|
4257
|
+
<Trash2 className="w-3 h-3" />
|
|
4258
|
+
</button>
|
|
4259
|
+
</div>
|
|
4260
|
+
)}
|
|
3416
4261
|
{sbxBottomTab === "output" && sandboxOutput.length > 0 && (
|
|
3417
4262
|
<div className="flex items-center gap-1 mr-1">
|
|
3418
4263
|
<button
|
|
@@ -3532,6 +4377,1159 @@ export default function CodeRunnerModal() {
|
|
|
3532
4377
|
</div>
|
|
3533
4378
|
)}
|
|
3534
4379
|
|
|
4380
|
+
{sbxBottomTab === "inspector" && (
|
|
4381
|
+
<div className="flex-1 overflow-hidden flex flex-col text-[12px]">
|
|
4382
|
+
{/* Sub-view selector + meta bar */}
|
|
4383
|
+
<div className="shrink-0 flex items-center gap-2 px-3 py-1.5 border-b border-slate-800 bg-slate-900/80">
|
|
4384
|
+
<div className="flex items-center rounded overflow-hidden border border-slate-700 text-[10px]">
|
|
4385
|
+
{MF_INSPECTOR_VIEWS.map((view) => (
|
|
4386
|
+
<button
|
|
4387
|
+
key={view}
|
|
4388
|
+
type="button"
|
|
4389
|
+
onClick={() => setMfInspectorView(view)}
|
|
4390
|
+
title={MF_INSPECTOR_VIEW_COPY[view].description}
|
|
4391
|
+
className={`px-2.5 py-1 capitalize transition-colors ${
|
|
4392
|
+
mfInspectorView === view
|
|
4393
|
+
? "bg-amber-500/20 text-amber-200"
|
|
4394
|
+
: "text-slate-500 hover:text-slate-300"
|
|
4395
|
+
}`}
|
|
4396
|
+
>
|
|
4397
|
+
{MF_INSPECTOR_VIEW_COPY[view].label}
|
|
4398
|
+
</button>
|
|
4399
|
+
))}
|
|
4400
|
+
</div>
|
|
4401
|
+
<div className="flex items-center gap-2 ml-1 text-[10px] text-slate-500">
|
|
4402
|
+
<span>{mfInspectorEvents.length} timeline entries</span>
|
|
4403
|
+
<span>·</span>
|
|
4404
|
+
<span>
|
|
4405
|
+
{mfInspectorRuntimeCount} browser session
|
|
4406
|
+
{mfInspectorRuntimeCount !== 1 ? "s" : ""}
|
|
4407
|
+
</span>
|
|
4408
|
+
{mfInspectorSharedRemoteCount > 0 && (
|
|
4409
|
+
<>
|
|
4410
|
+
<span>·</span>
|
|
4411
|
+
<span className="text-emerald-400">
|
|
4412
|
+
{mfInspectorSharedRemoteCount} same React copy
|
|
4413
|
+
</span>
|
|
4414
|
+
</>
|
|
4415
|
+
)}
|
|
4416
|
+
</div>
|
|
4417
|
+
{mfPreviewApp !== "host" && mfInspectorView === "map" && (
|
|
4418
|
+
<span className="ml-auto text-[10px] text-amber-400/80 shrink-0 italic">
|
|
4419
|
+
Open the host preview to watch the host load remotes
|
|
4420
|
+
</span>
|
|
4421
|
+
)}
|
|
4422
|
+
</div>
|
|
4423
|
+
|
|
4424
|
+
<div
|
|
4425
|
+
className={`shrink-0 border-b border-slate-800 bg-slate-950/60 px-3 ${
|
|
4426
|
+
mfInspectorGuideCollapsed ? "py-2" : "py-3"
|
|
4427
|
+
}`}
|
|
4428
|
+
>
|
|
4429
|
+
<div className="rounded-xl border border-slate-800 bg-slate-900/60 px-3 py-3">
|
|
4430
|
+
<div className="flex items-start justify-between gap-3">
|
|
4431
|
+
<div className="min-w-0">
|
|
4432
|
+
<div className="text-[10px] uppercase tracking-wider text-sky-300/80 mb-1">
|
|
4433
|
+
Beginner guide
|
|
4434
|
+
</div>
|
|
4435
|
+
<div className="text-[13px] font-semibold text-slate-100">
|
|
4436
|
+
{mfInspectorViewCopy.title}
|
|
4437
|
+
</div>
|
|
4438
|
+
{mfInspectorGuideCollapsed && (
|
|
4439
|
+
<p className="mt-1 text-[10px] text-slate-500 leading-relaxed">
|
|
4440
|
+
Guide hidden. Expand it if you want the beginner
|
|
4441
|
+
explanation and summary cards back.
|
|
4442
|
+
</p>
|
|
4443
|
+
)}
|
|
4444
|
+
</div>
|
|
4445
|
+
<button
|
|
4446
|
+
type="button"
|
|
4447
|
+
onClick={() => setMfInspectorGuideCollapsed((v) => !v)}
|
|
4448
|
+
className="shrink-0 inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-950/70 px-2 py-1 text-[10px] text-slate-300 hover:text-slate-100 hover:border-slate-600 transition-colors"
|
|
4449
|
+
title={
|
|
4450
|
+
mfInspectorGuideCollapsed
|
|
4451
|
+
? "Show beginner guide"
|
|
4452
|
+
: "Hide beginner guide"
|
|
4453
|
+
}
|
|
4454
|
+
>
|
|
4455
|
+
{mfInspectorGuideCollapsed ? (
|
|
4456
|
+
<ChevronDown className="w-3 h-3" />
|
|
4457
|
+
) : (
|
|
4458
|
+
<ChevronUp className="w-3 h-3" />
|
|
4459
|
+
)}
|
|
4460
|
+
{mfInspectorGuideCollapsed ? "Show" : "Hide"}
|
|
4461
|
+
</button>
|
|
4462
|
+
</div>
|
|
4463
|
+
{!mfInspectorGuideCollapsed && (
|
|
4464
|
+
<>
|
|
4465
|
+
<p className="mt-1 text-[11px] text-slate-400 leading-relaxed max-w-3xl">
|
|
4466
|
+
{mfInspectorViewCopy.description}
|
|
4467
|
+
</p>
|
|
4468
|
+
<div className="mt-2 rounded-lg border border-slate-700/80 bg-slate-950/70 px-2.5 py-2 text-[10px] text-slate-300 leading-relaxed">
|
|
4469
|
+
<span className="font-semibold text-slate-100">
|
|
4470
|
+
What good looks like:
|
|
4471
|
+
</span>{" "}
|
|
4472
|
+
{mfPreviewApp !== "host" && mfInspectorView === "map"
|
|
4473
|
+
? "You are previewing a remote directly right now. Switch back to the host page if you want to watch the host-to-remote lines move and update."
|
|
4474
|
+
: mfInspectorViewCopy.tip}
|
|
4475
|
+
</div>
|
|
4476
|
+
<div
|
|
4477
|
+
className="mt-3 grid gap-2"
|
|
4478
|
+
style={{
|
|
4479
|
+
gridTemplateColumns:
|
|
4480
|
+
"repeat(auto-fit, minmax(120px, 1fr))",
|
|
4481
|
+
}}
|
|
4482
|
+
>
|
|
4483
|
+
{mfInspectorSummaryCards.map((card) => (
|
|
4484
|
+
<div
|
|
4485
|
+
key={card.label}
|
|
4486
|
+
className="rounded-lg border border-slate-800 bg-slate-950/70 px-2.5 py-2"
|
|
4487
|
+
>
|
|
4488
|
+
<div className="text-[9px] uppercase tracking-wider text-slate-500">
|
|
4489
|
+
{card.label}
|
|
4490
|
+
</div>
|
|
4491
|
+
<div
|
|
4492
|
+
className={`mt-1 text-[13px] font-semibold ${card.toneClass}`}
|
|
4493
|
+
>
|
|
4494
|
+
{card.value}
|
|
4495
|
+
</div>
|
|
4496
|
+
<div className="mt-1 text-[10px] text-slate-500 leading-snug">
|
|
4497
|
+
{card.hint}
|
|
4498
|
+
</div>
|
|
4499
|
+
</div>
|
|
4500
|
+
))}
|
|
4501
|
+
</div>
|
|
4502
|
+
</>
|
|
4503
|
+
)}
|
|
4504
|
+
</div>
|
|
4505
|
+
</div>
|
|
4506
|
+
|
|
4507
|
+
{/* View content */}
|
|
4508
|
+
<div className="flex-1 overflow-y-auto">
|
|
4509
|
+
{/* ── MAP VIEW ── */}
|
|
4510
|
+
{mfInspectorView === "map" && (
|
|
4511
|
+
<div>
|
|
4512
|
+
{mfInspectorEvents.length === 0 && !mfSandboxId ? (
|
|
4513
|
+
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-slate-500 px-4 text-[11px]">
|
|
4514
|
+
<Eye className="w-6 h-6 opacity-30" />
|
|
4515
|
+
<p>
|
|
4516
|
+
Start the webpack lab, then open the host page to
|
|
4517
|
+
watch the host load remotes step by step.
|
|
4518
|
+
</p>
|
|
4519
|
+
</div>
|
|
4520
|
+
) : (
|
|
4521
|
+
<>
|
|
4522
|
+
{/* SVG Topology diagram */}
|
|
4523
|
+
{(() => {
|
|
4524
|
+
const W = 500;
|
|
4525
|
+
const hostY = 46;
|
|
4526
|
+
const remoteY = 156;
|
|
4527
|
+
const nodeW = 96;
|
|
4528
|
+
const nodeH = 46;
|
|
4529
|
+
const svgH = 220;
|
|
4530
|
+
const hostBooted = mfInspectorBootMap.has("host");
|
|
4531
|
+
const hostData = mfInspectorBootMap.get("host");
|
|
4532
|
+
|
|
4533
|
+
const getConnectionStatus = (
|
|
4534
|
+
remoteName: string,
|
|
4535
|
+
):
|
|
4536
|
+
| "idle"
|
|
4537
|
+
| "loading"
|
|
4538
|
+
| "success-shared"
|
|
4539
|
+
| "success-different"
|
|
4540
|
+
| "error" => {
|
|
4541
|
+
const keys =
|
|
4542
|
+
mfTopologyRemoteKeysByApp.get(remoteName) ?? [];
|
|
4543
|
+
type IdentityEntry = {
|
|
4544
|
+
sameReact: boolean;
|
|
4545
|
+
sameReactDom: boolean;
|
|
4546
|
+
error: string | null;
|
|
4547
|
+
};
|
|
4548
|
+
type LoadEntry = {
|
|
4549
|
+
status: string;
|
|
4550
|
+
durationMs: number | null;
|
|
4551
|
+
};
|
|
4552
|
+
const identityResults = keys
|
|
4553
|
+
.map((k) => mfInspectorIdentityResultMap.get(k))
|
|
4554
|
+
.filter(Boolean) as IdentityEntry[];
|
|
4555
|
+
const loadResults = keys
|
|
4556
|
+
.map((k) => mfInspectorRemoteLoadMap.get(k))
|
|
4557
|
+
.filter(Boolean) as LoadEntry[];
|
|
4558
|
+
if (identityResults.some((r) => r.error))
|
|
4559
|
+
return "error";
|
|
4560
|
+
if (identityResults.length > 0)
|
|
4561
|
+
return identityResults.every(
|
|
4562
|
+
(r) => r.sameReact && r.sameReactDom,
|
|
4563
|
+
)
|
|
4564
|
+
? "success-shared"
|
|
4565
|
+
: "success-different";
|
|
4566
|
+
if (loadResults.some((r) => r.status === "error"))
|
|
4567
|
+
return "error";
|
|
4568
|
+
if (
|
|
4569
|
+
loadResults.some(
|
|
4570
|
+
(r) =>
|
|
4571
|
+
r.status === "loading" ||
|
|
4572
|
+
r.status === "success",
|
|
4573
|
+
)
|
|
4574
|
+
)
|
|
4575
|
+
return "loading";
|
|
4576
|
+
// Only show "loading" (amber) if the host has
|
|
4577
|
+
// actually attempted to load this remote. A
|
|
4578
|
+
// standalone boot event from the remote's own
|
|
4579
|
+
// dev server doesn't mean the HOST is loading it.
|
|
4580
|
+
return "idle";
|
|
4581
|
+
};
|
|
4582
|
+
|
|
4583
|
+
const statusLineColors: Record<string, string> = {
|
|
4584
|
+
idle: "#334155",
|
|
4585
|
+
loading: "#d97706",
|
|
4586
|
+
"success-shared": "#10b981",
|
|
4587
|
+
"success-different": "#ef4444",
|
|
4588
|
+
error: "#ef4444",
|
|
4589
|
+
};
|
|
4590
|
+
|
|
4591
|
+
const displayRemotes =
|
|
4592
|
+
mfTopologyRemoteNames.length > 0
|
|
4593
|
+
? mfTopologyRemoteNames
|
|
4594
|
+
: ["profile", "checkout"];
|
|
4595
|
+
const isPlaceholder =
|
|
4596
|
+
mfTopologyRemoteNames.length === 0;
|
|
4597
|
+
|
|
4598
|
+
const remotePositions = displayRemotes.map(
|
|
4599
|
+
(_, i) => {
|
|
4600
|
+
const step = W / (displayRemotes.length + 1);
|
|
4601
|
+
return step * (i + 1);
|
|
4602
|
+
},
|
|
4603
|
+
);
|
|
4604
|
+
|
|
4605
|
+
return (
|
|
4606
|
+
<div className="px-2 pt-3 pb-1">
|
|
4607
|
+
<svg
|
|
4608
|
+
viewBox={`0 0 ${W} ${svgH}`}
|
|
4609
|
+
width="100%"
|
|
4610
|
+
style={{ height: svgH, display: "block" }}
|
|
4611
|
+
>
|
|
4612
|
+
{/* Connection lines */}
|
|
4613
|
+
{displayRemotes.map((remoteName, i) => {
|
|
4614
|
+
const rx = remotePositions[i];
|
|
4615
|
+
const status = isPlaceholder
|
|
4616
|
+
? "idle"
|
|
4617
|
+
: getConnectionStatus(remoteName);
|
|
4618
|
+
const lineColor =
|
|
4619
|
+
statusLineColors[status] ?? "#334155";
|
|
4620
|
+
const isDashed = status === "idle";
|
|
4621
|
+
const keys =
|
|
4622
|
+
mfTopologyRemoteKeysByApp.get(
|
|
4623
|
+
remoteName,
|
|
4624
|
+
) ?? [];
|
|
4625
|
+
const loadResult = keys
|
|
4626
|
+
.map((k) =>
|
|
4627
|
+
mfInspectorRemoteLoadMap.get(k),
|
|
4628
|
+
)
|
|
4629
|
+
.find((r) => r?.durationMs != null);
|
|
4630
|
+
const durationLabel =
|
|
4631
|
+
loadResult?.durationMs != null
|
|
4632
|
+
? `${loadResult.durationMs}ms`
|
|
4633
|
+
: "";
|
|
4634
|
+
const midX = (W / 2 + rx) / 2;
|
|
4635
|
+
const midY =
|
|
4636
|
+
(hostY +
|
|
4637
|
+
nodeH / 2 +
|
|
4638
|
+
remoteY -
|
|
4639
|
+
nodeH / 2) /
|
|
4640
|
+
2;
|
|
4641
|
+
return (
|
|
4642
|
+
<g key={remoteName}>
|
|
4643
|
+
<line
|
|
4644
|
+
x1={W / 2}
|
|
4645
|
+
y1={hostY + nodeH / 2}
|
|
4646
|
+
x2={rx}
|
|
4647
|
+
y2={remoteY - nodeH / 2}
|
|
4648
|
+
stroke={lineColor}
|
|
4649
|
+
strokeWidth={isDashed ? 1.5 : 2}
|
|
4650
|
+
strokeDasharray={
|
|
4651
|
+
isDashed ? "5 4" : undefined
|
|
4652
|
+
}
|
|
4653
|
+
strokeLinecap="round"
|
|
4654
|
+
opacity={isPlaceholder ? 0.3 : 1}
|
|
4655
|
+
/>
|
|
4656
|
+
{durationLabel && (
|
|
4657
|
+
<text
|
|
4658
|
+
x={midX}
|
|
4659
|
+
y={midY - 5}
|
|
4660
|
+
textAnchor="middle"
|
|
4661
|
+
fill={lineColor}
|
|
4662
|
+
fontSize={9}
|
|
4663
|
+
fontFamily="ui-monospace, monospace"
|
|
4664
|
+
>
|
|
4665
|
+
{durationLabel}
|
|
4666
|
+
</text>
|
|
4667
|
+
)}
|
|
4668
|
+
</g>
|
|
4669
|
+
);
|
|
4670
|
+
})}
|
|
4671
|
+
|
|
4672
|
+
{/* HOST node */}
|
|
4673
|
+
<rect
|
|
4674
|
+
x={W / 2 - nodeW / 2}
|
|
4675
|
+
y={hostY - nodeH / 2}
|
|
4676
|
+
width={nodeW}
|
|
4677
|
+
height={nodeH}
|
|
4678
|
+
rx={7}
|
|
4679
|
+
fill={hostBooted ? "#0c2a3d" : "#0f172a"}
|
|
4680
|
+
stroke={hostBooted ? "#38bdf8" : "#475569"}
|
|
4681
|
+
strokeWidth={hostBooted ? 2 : 1}
|
|
4682
|
+
opacity={isPlaceholder ? 0.5 : 1}
|
|
4683
|
+
/>
|
|
4684
|
+
<text
|
|
4685
|
+
x={W / 2}
|
|
4686
|
+
y={hostY - 12}
|
|
4687
|
+
textAnchor="middle"
|
|
4688
|
+
fill={hostBooted ? "#e0f2fe" : "#94a3b8"}
|
|
4689
|
+
fontSize={11}
|
|
4690
|
+
fontWeight="700"
|
|
4691
|
+
fontFamily="ui-monospace, monospace"
|
|
4692
|
+
>
|
|
4693
|
+
host
|
|
4694
|
+
</text>
|
|
4695
|
+
{hostData?.reactVersion && (
|
|
4696
|
+
<text
|
|
4697
|
+
x={W / 2}
|
|
4698
|
+
y={hostY + 1}
|
|
4699
|
+
textAnchor="middle"
|
|
4700
|
+
fill="#7dd3fc"
|
|
4701
|
+
fontSize={8}
|
|
4702
|
+
fontFamily="ui-monospace, monospace"
|
|
4703
|
+
>
|
|
4704
|
+
{"⚛ React " + hostData.reactVersion}
|
|
4705
|
+
</text>
|
|
4706
|
+
)}
|
|
4707
|
+
{hostData?.reactDomVersion && (
|
|
4708
|
+
<text
|
|
4709
|
+
x={W / 2}
|
|
4710
|
+
y={hostY + 13}
|
|
4711
|
+
textAnchor="middle"
|
|
4712
|
+
fill="#7dd3fc"
|
|
4713
|
+
fontSize={8}
|
|
4714
|
+
fontFamily="ui-monospace, monospace"
|
|
4715
|
+
>
|
|
4716
|
+
{"▣ DOM " + hostData.reactDomVersion}
|
|
4717
|
+
</text>
|
|
4718
|
+
)}
|
|
4719
|
+
|
|
4720
|
+
{/* REMOTE nodes */}
|
|
4721
|
+
{displayRemotes.map((remoteName, i) => {
|
|
4722
|
+
const rx = remotePositions[i];
|
|
4723
|
+
const status = isPlaceholder
|
|
4724
|
+
? "idle"
|
|
4725
|
+
: getConnectionStatus(remoteName);
|
|
4726
|
+
const remoteData =
|
|
4727
|
+
mfInspectorBootMap.get(remoteName);
|
|
4728
|
+
const keys =
|
|
4729
|
+
mfTopologyRemoteKeysByApp.get(
|
|
4730
|
+
remoteName,
|
|
4731
|
+
) ?? [];
|
|
4732
|
+
const identityResult = keys
|
|
4733
|
+
.map((k) =>
|
|
4734
|
+
mfInspectorIdentityResultMap.get(k),
|
|
4735
|
+
)
|
|
4736
|
+
.find((r) => r != null);
|
|
4737
|
+
const reactShared = identityResult
|
|
4738
|
+
? identityResult.sameReact
|
|
4739
|
+
: null;
|
|
4740
|
+
const domShared = identityResult
|
|
4741
|
+
? identityResult.sameReactDom
|
|
4742
|
+
: null;
|
|
4743
|
+
const bothShared =
|
|
4744
|
+
reactShared === true &&
|
|
4745
|
+
domShared === true;
|
|
4746
|
+
const eitherDifferent =
|
|
4747
|
+
reactShared === false ||
|
|
4748
|
+
domShared === false;
|
|
4749
|
+
const nodeFill = bothShared
|
|
4750
|
+
? "#052e16"
|
|
4751
|
+
: eitherDifferent || status === "error"
|
|
4752
|
+
? "#450a0a"
|
|
4753
|
+
: status === "loading"
|
|
4754
|
+
? "#1c1917"
|
|
4755
|
+
: "#0f172a";
|
|
4756
|
+
const nodeStroke = bothShared
|
|
4757
|
+
? "#16a34a"
|
|
4758
|
+
: eitherDifferent || status === "error"
|
|
4759
|
+
? "#dc2626"
|
|
4760
|
+
: status === "loading"
|
|
4761
|
+
? "#d97706"
|
|
4762
|
+
: remoteData
|
|
4763
|
+
? "#818cf8"
|
|
4764
|
+
: "#334155";
|
|
4765
|
+
// Build per-library status label
|
|
4766
|
+
const reactLabel =
|
|
4767
|
+
reactShared === true
|
|
4768
|
+
? "\u2713 React"
|
|
4769
|
+
: reactShared === false
|
|
4770
|
+
? "\u2717 React"
|
|
4771
|
+
: status === "loading"
|
|
4772
|
+
? "React\u2026"
|
|
4773
|
+
: "";
|
|
4774
|
+
const domLabel =
|
|
4775
|
+
domShared === true
|
|
4776
|
+
? "\u2713 DOM"
|
|
4777
|
+
: domShared === false
|
|
4778
|
+
? "\u2717 DOM"
|
|
4779
|
+
: status === "loading"
|
|
4780
|
+
? "DOM\u2026"
|
|
4781
|
+
: "";
|
|
4782
|
+
const statusLabel =
|
|
4783
|
+
status === "error"
|
|
4784
|
+
? "\u2717 error"
|
|
4785
|
+
: [reactLabel, domLabel]
|
|
4786
|
+
.filter(Boolean)
|
|
4787
|
+
.join(" ");
|
|
4788
|
+
const reactLabelColor =
|
|
4789
|
+
reactShared === true
|
|
4790
|
+
? "#4ade80"
|
|
4791
|
+
: reactShared === false
|
|
4792
|
+
? "#f87171"
|
|
4793
|
+
: "#fbbf24";
|
|
4794
|
+
const domLabelColor =
|
|
4795
|
+
domShared === true
|
|
4796
|
+
? "#4ade80"
|
|
4797
|
+
: domShared === false
|
|
4798
|
+
? "#f87171"
|
|
4799
|
+
: "#fbbf24";
|
|
4800
|
+
const reactVer =
|
|
4801
|
+
remoteData?.reactVersion ??
|
|
4802
|
+
identityResult?.reactVersion;
|
|
4803
|
+
const reactDomVer =
|
|
4804
|
+
remoteData?.reactDomVersion ??
|
|
4805
|
+
identityResult?.reactDomVersion;
|
|
4806
|
+
|
|
4807
|
+
return (
|
|
4808
|
+
<g
|
|
4809
|
+
key={remoteName}
|
|
4810
|
+
opacity={isPlaceholder ? 0.35 : 1}
|
|
4811
|
+
>
|
|
4812
|
+
<rect
|
|
4813
|
+
x={rx - nodeW / 2}
|
|
4814
|
+
y={remoteY - nodeH / 2}
|
|
4815
|
+
width={nodeW}
|
|
4816
|
+
height={nodeH}
|
|
4817
|
+
rx={7}
|
|
4818
|
+
fill={nodeFill}
|
|
4819
|
+
stroke={nodeStroke}
|
|
4820
|
+
strokeWidth={
|
|
4821
|
+
status !== "idle" ? 2 : 1
|
|
4822
|
+
}
|
|
4823
|
+
/>
|
|
4824
|
+
<text
|
|
4825
|
+
x={rx}
|
|
4826
|
+
y={remoteY - 13}
|
|
4827
|
+
textAnchor="middle"
|
|
4828
|
+
fill="#e2e8f0"
|
|
4829
|
+
fontSize={11}
|
|
4830
|
+
fontWeight="700"
|
|
4831
|
+
fontFamily="ui-monospace, monospace"
|
|
4832
|
+
>
|
|
4833
|
+
{remoteName}
|
|
4834
|
+
</text>
|
|
4835
|
+
{/* React row */}
|
|
4836
|
+
<text
|
|
4837
|
+
x={rx - nodeW / 2 + 8}
|
|
4838
|
+
y={remoteY + 2}
|
|
4839
|
+
fill={
|
|
4840
|
+
reactShared === true
|
|
4841
|
+
? "#4ade80"
|
|
4842
|
+
: reactShared === false
|
|
4843
|
+
? "#f87171"
|
|
4844
|
+
: "#64748b"
|
|
4845
|
+
}
|
|
4846
|
+
fontSize={8}
|
|
4847
|
+
fontFamily="ui-monospace, monospace"
|
|
4848
|
+
>
|
|
4849
|
+
⚛
|
|
4850
|
+
</text>
|
|
4851
|
+
<text
|
|
4852
|
+
x={rx - nodeW / 2 + 19}
|
|
4853
|
+
y={remoteY + 2}
|
|
4854
|
+
fill="#94a3b8"
|
|
4855
|
+
fontSize={8}
|
|
4856
|
+
fontFamily="ui-monospace, monospace"
|
|
4857
|
+
>
|
|
4858
|
+
{reactVer
|
|
4859
|
+
? "React " + reactVer
|
|
4860
|
+
: "React"}
|
|
4861
|
+
</text>
|
|
4862
|
+
{reactShared !== null && (
|
|
4863
|
+
<text
|
|
4864
|
+
x={rx + nodeW / 2 - 6}
|
|
4865
|
+
y={remoteY + 2}
|
|
4866
|
+
textAnchor="end"
|
|
4867
|
+
fill={
|
|
4868
|
+
reactShared
|
|
4869
|
+
? "#4ade80"
|
|
4870
|
+
: "#f87171"
|
|
4871
|
+
}
|
|
4872
|
+
fontSize={8}
|
|
4873
|
+
fontFamily="ui-monospace, monospace"
|
|
4874
|
+
>
|
|
4875
|
+
{reactShared ? "\u2713" : "\u2717"}
|
|
4876
|
+
</text>
|
|
4877
|
+
)}
|
|
4878
|
+
{/* ReactDOM row */}
|
|
4879
|
+
<text
|
|
4880
|
+
x={rx - nodeW / 2 + 8}
|
|
4881
|
+
y={remoteY + 14}
|
|
4882
|
+
fill={
|
|
4883
|
+
domShared === true
|
|
4884
|
+
? "#4ade80"
|
|
4885
|
+
: domShared === false
|
|
4886
|
+
? "#f87171"
|
|
4887
|
+
: "#64748b"
|
|
4888
|
+
}
|
|
4889
|
+
fontSize={8}
|
|
4890
|
+
fontFamily="ui-monospace, monospace"
|
|
4891
|
+
>
|
|
4892
|
+
▣
|
|
4893
|
+
</text>
|
|
4894
|
+
<text
|
|
4895
|
+
x={rx - nodeW / 2 + 19}
|
|
4896
|
+
y={remoteY + 14}
|
|
4897
|
+
fill="#94a3b8"
|
|
4898
|
+
fontSize={8}
|
|
4899
|
+
fontFamily="ui-monospace, monospace"
|
|
4900
|
+
>
|
|
4901
|
+
{reactDomVer
|
|
4902
|
+
? "DOM " + reactDomVer
|
|
4903
|
+
: "ReactDOM"}
|
|
4904
|
+
</text>
|
|
4905
|
+
{domShared !== null && (
|
|
4906
|
+
<text
|
|
4907
|
+
x={rx + nodeW / 2 - 6}
|
|
4908
|
+
y={remoteY + 14}
|
|
4909
|
+
textAnchor="end"
|
|
4910
|
+
fill={
|
|
4911
|
+
domShared ? "#4ade80" : "#f87171"
|
|
4912
|
+
}
|
|
4913
|
+
fontSize={8}
|
|
4914
|
+
fontFamily="ui-monospace, monospace"
|
|
4915
|
+
>
|
|
4916
|
+
{domShared ? "\u2713" : "\u2717"}
|
|
4917
|
+
</text>
|
|
4918
|
+
)}
|
|
4919
|
+
{/* Status label below node — show per-lib check marks */}
|
|
4920
|
+
{(statusLabel ||
|
|
4921
|
+
status === "loading") && (
|
|
4922
|
+
<g>
|
|
4923
|
+
{reactLabel && (
|
|
4924
|
+
<text
|
|
4925
|
+
x={rx - 20}
|
|
4926
|
+
y={remoteY + nodeH / 2 + 14}
|
|
4927
|
+
textAnchor="middle"
|
|
4928
|
+
fill={reactLabelColor}
|
|
4929
|
+
fontSize={8.5}
|
|
4930
|
+
fontFamily="ui-monospace, monospace"
|
|
4931
|
+
>
|
|
4932
|
+
{reactLabel}
|
|
4933
|
+
</text>
|
|
4934
|
+
)}
|
|
4935
|
+
{domLabel && (
|
|
4936
|
+
<text
|
|
4937
|
+
x={rx + 22}
|
|
4938
|
+
y={remoteY + nodeH / 2 + 14}
|
|
4939
|
+
textAnchor="middle"
|
|
4940
|
+
fill={domLabelColor}
|
|
4941
|
+
fontSize={8.5}
|
|
4942
|
+
fontFamily="ui-monospace, monospace"
|
|
4943
|
+
>
|
|
4944
|
+
{domLabel}
|
|
4945
|
+
</text>
|
|
4946
|
+
)}
|
|
4947
|
+
{status === "error" && (
|
|
4948
|
+
<text
|
|
4949
|
+
x={rx}
|
|
4950
|
+
y={remoteY + nodeH / 2 + 14}
|
|
4951
|
+
textAnchor="middle"
|
|
4952
|
+
fill="#f87171"
|
|
4953
|
+
fontSize={8.5}
|
|
4954
|
+
fontFamily="ui-monospace, monospace"
|
|
4955
|
+
>
|
|
4956
|
+
✕ error
|
|
4957
|
+
</text>
|
|
4958
|
+
)}
|
|
4959
|
+
</g>
|
|
4960
|
+
)}
|
|
4961
|
+
</g>
|
|
4962
|
+
);
|
|
4963
|
+
})}
|
|
4964
|
+
|
|
4965
|
+
{/* Legend strip */}
|
|
4966
|
+
{(
|
|
4967
|
+
[
|
|
4968
|
+
{
|
|
4969
|
+
color: "#334155",
|
|
4970
|
+
label: "not asked yet",
|
|
4971
|
+
dash: true,
|
|
4972
|
+
},
|
|
4973
|
+
{
|
|
4974
|
+
color: "#d97706",
|
|
4975
|
+
label: "loading / checking",
|
|
4976
|
+
dash: false,
|
|
4977
|
+
},
|
|
4978
|
+
{
|
|
4979
|
+
color: "#10b981",
|
|
4980
|
+
label: "same React copy",
|
|
4981
|
+
dash: false,
|
|
4982
|
+
},
|
|
4983
|
+
{
|
|
4984
|
+
color: "#ef4444",
|
|
4985
|
+
label: "different / error",
|
|
4986
|
+
dash: false,
|
|
4987
|
+
},
|
|
4988
|
+
] as Array<{
|
|
4989
|
+
color: string;
|
|
4990
|
+
label: string;
|
|
4991
|
+
dash: boolean;
|
|
4992
|
+
}>
|
|
4993
|
+
).map(
|
|
4994
|
+
({ color, label, dash: isDash }, li) => (
|
|
4995
|
+
<g
|
|
4996
|
+
key={li}
|
|
4997
|
+
transform={`translate(${li * 126}, ${svgH - 11})`}
|
|
4998
|
+
>
|
|
4999
|
+
<line
|
|
5000
|
+
x1={0}
|
|
5001
|
+
y1={0}
|
|
5002
|
+
x2={14}
|
|
5003
|
+
y2={0}
|
|
5004
|
+
stroke={color}
|
|
5005
|
+
strokeWidth={2}
|
|
5006
|
+
strokeDasharray={
|
|
5007
|
+
isDash ? "4 3" : undefined
|
|
5008
|
+
}
|
|
5009
|
+
strokeLinecap="round"
|
|
5010
|
+
/>
|
|
5011
|
+
<text
|
|
5012
|
+
x={18}
|
|
5013
|
+
y={1}
|
|
5014
|
+
fill="#475569"
|
|
5015
|
+
fontSize={8.5}
|
|
5016
|
+
fontFamily="ui-sans-serif, sans-serif"
|
|
5017
|
+
dominantBaseline="middle"
|
|
5018
|
+
>
|
|
5019
|
+
{label}
|
|
5020
|
+
</text>
|
|
5021
|
+
</g>
|
|
5022
|
+
),
|
|
5023
|
+
)}
|
|
5024
|
+
</svg>
|
|
5025
|
+
</div>
|
|
5026
|
+
);
|
|
5027
|
+
})()}
|
|
5028
|
+
|
|
5029
|
+
{/* Instance-sharing truth table */}
|
|
5030
|
+
{mfInspectorRemoteTableKeys.length > 0 && (
|
|
5031
|
+
<div className="px-3 pb-3">
|
|
5032
|
+
<div className="text-[10px] uppercase tracking-wider text-slate-500 mb-2">
|
|
5033
|
+
Did the host and remote reuse the same React
|
|
5034
|
+
copy?
|
|
5035
|
+
</div>
|
|
5036
|
+
<div className="text-[11px] text-slate-500 mb-2 leading-relaxed">
|
|
5037
|
+
"Same copy" is the healthy result for React and
|
|
5038
|
+
ReactDOM. "Different copy" often leads to hook,
|
|
5039
|
+
context, or shared-state bugs.
|
|
5040
|
+
</div>
|
|
5041
|
+
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
|
5042
|
+
<table className="w-full text-[11px]">
|
|
5043
|
+
<thead>
|
|
5044
|
+
<tr className="border-b border-slate-800 bg-slate-900/80">
|
|
5045
|
+
<th className="text-left px-3 py-1.5 text-slate-400 font-medium">
|
|
5046
|
+
Remote module
|
|
5047
|
+
</th>
|
|
5048
|
+
<th className="text-left px-3 py-1.5 text-slate-400 font-medium">
|
|
5049
|
+
React copy
|
|
5050
|
+
</th>
|
|
5051
|
+
<th className="text-left px-3 py-1.5 text-slate-400 font-medium">
|
|
5052
|
+
DOM copy
|
|
5053
|
+
</th>
|
|
5054
|
+
<th className="text-left px-3 py-1.5 text-slate-400 font-medium">
|
|
5055
|
+
Version seen
|
|
5056
|
+
</th>
|
|
5057
|
+
<th className="text-left px-3 py-1.5 text-slate-400 font-medium">
|
|
5058
|
+
Load time
|
|
5059
|
+
</th>
|
|
5060
|
+
</tr>
|
|
5061
|
+
</thead>
|
|
5062
|
+
<tbody>
|
|
5063
|
+
{mfInspectorRemoteTableKeys.map(
|
|
5064
|
+
(remoteKey) => {
|
|
5065
|
+
const result =
|
|
5066
|
+
mfInspectorIdentityResultMap.get(
|
|
5067
|
+
remoteKey,
|
|
5068
|
+
) ?? null;
|
|
5069
|
+
const loadInfo =
|
|
5070
|
+
mfInspectorRemoteLoadMap.get(
|
|
5071
|
+
remoteKey,
|
|
5072
|
+
);
|
|
5073
|
+
return (
|
|
5074
|
+
<tr
|
|
5075
|
+
key={remoteKey}
|
|
5076
|
+
className="border-b border-slate-800/50 last:border-0"
|
|
5077
|
+
>
|
|
5078
|
+
<td className="px-3 py-2 font-mono text-slate-200">
|
|
5079
|
+
{remoteKey}
|
|
5080
|
+
</td>
|
|
5081
|
+
<td className="px-3 py-2">
|
|
5082
|
+
{renderMfInspectorTruthCell(
|
|
5083
|
+
result?.sameReact ?? null,
|
|
5084
|
+
result?.error ?? null,
|
|
5085
|
+
loadInfo?.status ?? null,
|
|
5086
|
+
)}
|
|
5087
|
+
</td>
|
|
5088
|
+
<td className="px-3 py-2">
|
|
5089
|
+
{renderMfInspectorTruthCell(
|
|
5090
|
+
result?.sameReactDom ?? null,
|
|
5091
|
+
result?.error ?? null,
|
|
5092
|
+
loadInfo?.status ?? null,
|
|
5093
|
+
)}
|
|
5094
|
+
</td>
|
|
5095
|
+
<td className="px-3 py-2 font-mono text-slate-400 text-[10px]">
|
|
5096
|
+
{result?.reactVersion ??
|
|
5097
|
+
(loadInfo?.status ===
|
|
5098
|
+
"loading" ||
|
|
5099
|
+
loadInfo?.status === "success"
|
|
5100
|
+
? "checking..."
|
|
5101
|
+
: "\u2014")}
|
|
5102
|
+
</td>
|
|
5103
|
+
<td className="px-3 py-2 font-mono text-slate-400 text-[10px]">
|
|
5104
|
+
{loadInfo?.durationMs != null
|
|
5105
|
+
? `${loadInfo.durationMs}ms`
|
|
5106
|
+
: loadInfo?.status === "loading"
|
|
5107
|
+
? "loading..."
|
|
5108
|
+
: "\u2014"}
|
|
5109
|
+
</td>
|
|
5110
|
+
</tr>
|
|
5111
|
+
);
|
|
5112
|
+
},
|
|
5113
|
+
)}
|
|
5114
|
+
</tbody>
|
|
5115
|
+
</table>
|
|
5116
|
+
</div>
|
|
5117
|
+
</div>
|
|
5118
|
+
)}
|
|
5119
|
+
|
|
5120
|
+
{/* Declared config cards */}
|
|
5121
|
+
{mfInspectorDeclaredConfigs.length > 0 && (
|
|
5122
|
+
<div className="px-3 pb-3">
|
|
5123
|
+
<div className="text-[10px] uppercase tracking-wider text-slate-500 mb-2">
|
|
5124
|
+
What each app promised in webpack config
|
|
5125
|
+
</div>
|
|
5126
|
+
<div className="text-[11px] text-slate-500 mb-2 leading-relaxed">
|
|
5127
|
+
This is the static setup from config files,
|
|
5128
|
+
before the browser proves anything at runtime.
|
|
5129
|
+
</div>
|
|
5130
|
+
<div
|
|
5131
|
+
className="grid gap-2"
|
|
5132
|
+
style={{
|
|
5133
|
+
gridTemplateColumns:
|
|
5134
|
+
"repeat(auto-fill, minmax(160px, 1fr))",
|
|
5135
|
+
}}
|
|
5136
|
+
>
|
|
5137
|
+
{mfInspectorDeclaredConfigs.map(
|
|
5138
|
+
({ app, declaredConfig }) => {
|
|
5139
|
+
const remotes = Object.keys(
|
|
5140
|
+
asInspectorRecord(declaredConfig.remotes),
|
|
5141
|
+
);
|
|
5142
|
+
const exposes = asInspectorStringArray(
|
|
5143
|
+
declaredConfig.exposes,
|
|
5144
|
+
);
|
|
5145
|
+
const sharedPkgs = Object.keys(
|
|
5146
|
+
asInspectorRecord(declaredConfig.shared),
|
|
5147
|
+
);
|
|
5148
|
+
return (
|
|
5149
|
+
<div
|
|
5150
|
+
key={app}
|
|
5151
|
+
className="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2.5 text-[11px]"
|
|
5152
|
+
>
|
|
5153
|
+
<div className="font-semibold text-slate-100 font-mono mb-2">
|
|
5154
|
+
{app}
|
|
5155
|
+
</div>
|
|
5156
|
+
{remotes.length > 0 && (
|
|
5157
|
+
<div className="mb-1.5">
|
|
5158
|
+
<div className="text-[9px] text-slate-500 uppercase tracking-wider mb-1">
|
|
5159
|
+
can call these remotes
|
|
5160
|
+
</div>
|
|
5161
|
+
<div className="flex flex-wrap gap-1">
|
|
5162
|
+
{remotes.map((r) => (
|
|
5163
|
+
<span
|
|
5164
|
+
key={r}
|
|
5165
|
+
className="rounded bg-cyan-500/10 border border-cyan-500/20 px-1.5 py-0.5 text-cyan-300 text-[10px] font-mono"
|
|
5166
|
+
>
|
|
5167
|
+
{r}
|
|
5168
|
+
</span>
|
|
5169
|
+
))}
|
|
5170
|
+
</div>
|
|
5171
|
+
</div>
|
|
5172
|
+
)}
|
|
5173
|
+
{exposes.length > 0 && (
|
|
5174
|
+
<div className="mb-1.5">
|
|
5175
|
+
<div className="text-[9px] text-slate-500 uppercase tracking-wider mb-1">
|
|
5176
|
+
offers these modules
|
|
5177
|
+
</div>
|
|
5178
|
+
<div className="flex flex-wrap gap-1">
|
|
5179
|
+
{exposes.map((e) => (
|
|
5180
|
+
<span
|
|
5181
|
+
key={e}
|
|
5182
|
+
className="rounded bg-violet-500/10 border border-violet-500/20 px-1.5 py-0.5 text-violet-300 text-[10px] font-mono"
|
|
5183
|
+
>
|
|
5184
|
+
{e}
|
|
5185
|
+
</span>
|
|
5186
|
+
))}
|
|
5187
|
+
</div>
|
|
5188
|
+
</div>
|
|
5189
|
+
)}
|
|
5190
|
+
{sharedPkgs.length > 0 && (
|
|
5191
|
+
<div>
|
|
5192
|
+
<div className="text-[9px] text-slate-500 uppercase tracking-wider mb-1">
|
|
5193
|
+
wants to share (
|
|
5194
|
+
{sharedPkgs.length})
|
|
5195
|
+
</div>
|
|
5196
|
+
<div className="flex flex-wrap gap-1">
|
|
5197
|
+
{sharedPkgs.map((p) => {
|
|
5198
|
+
const cfg = asInspectorRecord(
|
|
5199
|
+
asInspectorRecord(
|
|
5200
|
+
declaredConfig.shared,
|
|
5201
|
+
)[p],
|
|
5202
|
+
);
|
|
5203
|
+
const singleton =
|
|
5204
|
+
cfg.singleton === true;
|
|
5205
|
+
return (
|
|
5206
|
+
<span
|
|
5207
|
+
key={p}
|
|
5208
|
+
className={`rounded border px-1.5 py-0.5 text-[10px] font-mono ${
|
|
5209
|
+
singleton
|
|
5210
|
+
? "border-amber-500/30 bg-amber-500/10 text-amber-200"
|
|
5211
|
+
: "border-slate-700 bg-slate-800/50 text-slate-300"
|
|
5212
|
+
}`}
|
|
5213
|
+
>
|
|
5214
|
+
{p}
|
|
5215
|
+
{singleton
|
|
5216
|
+
? " \u00b7 one copy"
|
|
5217
|
+
: ""}
|
|
5218
|
+
</span>
|
|
5219
|
+
);
|
|
5220
|
+
})}
|
|
5221
|
+
</div>
|
|
5222
|
+
</div>
|
|
5223
|
+
)}
|
|
5224
|
+
</div>
|
|
5225
|
+
);
|
|
5226
|
+
},
|
|
5227
|
+
)}
|
|
5228
|
+
</div>
|
|
5229
|
+
</div>
|
|
5230
|
+
)}
|
|
5231
|
+
</>
|
|
5232
|
+
)}
|
|
5233
|
+
</div>
|
|
5234
|
+
)}
|
|
5235
|
+
|
|
5236
|
+
{/* ── SHARES VIEW ── */}
|
|
5237
|
+
{mfInspectorView === "shares" && (
|
|
5238
|
+
<div className="px-3 py-3">
|
|
5239
|
+
{!mfShareScopeForMatrix ? (
|
|
5240
|
+
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-slate-500 text-[11px]">
|
|
5241
|
+
<div className="opacity-20 text-3xl select-none">
|
|
5242
|
+
⚛
|
|
5243
|
+
</div>
|
|
5244
|
+
<p>
|
|
5245
|
+
Open the host preview first to populate webpack's
|
|
5246
|
+
live shared-package list.
|
|
5247
|
+
</p>
|
|
5248
|
+
<p className="text-[10px] text-slate-600">
|
|
5249
|
+
The inspector reads webpack's live{" "}
|
|
5250
|
+
<code className="font-mono">
|
|
5251
|
+
__webpack_share_scopes__
|
|
5252
|
+
</code>{" "}
|
|
5253
|
+
registry at runtime.
|
|
5254
|
+
</p>
|
|
5255
|
+
</div>
|
|
5256
|
+
) : (
|
|
5257
|
+
<>
|
|
5258
|
+
<div className="rounded-lg border border-slate-800 bg-slate-900/50 px-3 py-2.5 mb-3">
|
|
5259
|
+
<div className="text-[12px] font-semibold text-slate-100">
|
|
5260
|
+
Live shared package list
|
|
5261
|
+
</div>
|
|
5262
|
+
<p className="mt-1 text-[11px] text-slate-400 leading-relaxed">
|
|
5263
|
+
This is webpack's runtime list of libraries that
|
|
5264
|
+
apps can reuse. It is more trustworthy than config
|
|
5265
|
+
alone because it shows what actually happened in
|
|
5266
|
+
the browser.
|
|
5267
|
+
</p>
|
|
5268
|
+
<p className="mt-1 text-[10px] text-slate-500">
|
|
5269
|
+
Snapshot source:{" "}
|
|
5270
|
+
<span className="text-amber-300 font-mono">
|
|
5271
|
+
{mfShareScopeForMatrix.app}
|
|
5272
|
+
</span>{" "}
|
|
5273
|
+
at{" "}
|
|
5274
|
+
{formatInspectorTimestamp(
|
|
5275
|
+
mfShareScopeForMatrix.timestamp,
|
|
5276
|
+
)}
|
|
5277
|
+
</p>
|
|
5278
|
+
</div>
|
|
5279
|
+
{Object.entries(mfShareScopeForMatrix.shareScopes)
|
|
5280
|
+
.filter(([sn]) => sn !== "__error")
|
|
5281
|
+
.map(([scopeName, scopeValue]) => {
|
|
5282
|
+
const packages = Object.entries(
|
|
5283
|
+
asInspectorRecord(scopeValue),
|
|
5284
|
+
);
|
|
5285
|
+
return (
|
|
5286
|
+
<div key={scopeName} className="mb-4">
|
|
5287
|
+
<div className="text-[10px] text-slate-500 uppercase tracking-wider mb-2 font-mono">
|
|
5288
|
+
sharing group: {scopeName}
|
|
5289
|
+
</div>
|
|
5290
|
+
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
|
5291
|
+
<table className="w-full text-[11px]">
|
|
5292
|
+
<thead>
|
|
5293
|
+
<tr className="bg-slate-900/80 border-b border-slate-800">
|
|
5294
|
+
<th className="text-left px-3 py-1.5 text-slate-400 font-medium w-32">
|
|
5295
|
+
Package
|
|
5296
|
+
</th>
|
|
5297
|
+
<th className="text-left px-3 py-1.5 text-slate-400 font-medium">
|
|
5298
|
+
Versions webpack can reuse
|
|
5299
|
+
</th>
|
|
5300
|
+
</tr>
|
|
5301
|
+
</thead>
|
|
5302
|
+
<tbody>
|
|
5303
|
+
{packages.map(
|
|
5304
|
+
([pkgName, versionValue]) => {
|
|
5305
|
+
const versionEntries =
|
|
5306
|
+
Array.isArray(versionValue)
|
|
5307
|
+
? versionValue
|
|
5308
|
+
: [];
|
|
5309
|
+
const declaredSingleton =
|
|
5310
|
+
mfInspectorDeclaredConfigs.some(
|
|
5311
|
+
(cfg) => {
|
|
5312
|
+
const sh = asInspectorRecord(
|
|
5313
|
+
asInspectorRecord(
|
|
5314
|
+
cfg.declaredConfig,
|
|
5315
|
+
).shared,
|
|
5316
|
+
);
|
|
5317
|
+
return (
|
|
5318
|
+
asInspectorRecord(
|
|
5319
|
+
sh[pkgName],
|
|
5320
|
+
).singleton === true
|
|
5321
|
+
);
|
|
5322
|
+
},
|
|
5323
|
+
);
|
|
5324
|
+
const loadedCount =
|
|
5325
|
+
versionEntries.filter(
|
|
5326
|
+
(ve) =>
|
|
5327
|
+
asInspectorRecord(ve)
|
|
5328
|
+
.loaded === true,
|
|
5329
|
+
).length;
|
|
5330
|
+
return (
|
|
5331
|
+
<tr
|
|
5332
|
+
key={pkgName}
|
|
5333
|
+
className="border-b border-slate-800/50 last:border-0"
|
|
5334
|
+
>
|
|
5335
|
+
<td className="px-3 py-2.5 align-top">
|
|
5336
|
+
<div className="font-mono text-slate-100 font-medium">
|
|
5337
|
+
{pkgName}
|
|
5338
|
+
</div>
|
|
5339
|
+
<div className="flex flex-wrap gap-1 mt-1">
|
|
5340
|
+
{declaredSingleton && (
|
|
5341
|
+
<span className="rounded border border-amber-500/30 bg-amber-500/10 px-1 py-0.5 text-[9px] text-amber-300">
|
|
5342
|
+
one copy preferred
|
|
5343
|
+
</span>
|
|
5344
|
+
)}
|
|
5345
|
+
<span className="rounded border border-slate-700 bg-slate-800/50 px-1 py-0.5 text-[9px] text-slate-400">
|
|
5346
|
+
{loadedCount}/
|
|
5347
|
+
{versionEntries.length}{" "}
|
|
5348
|
+
already used
|
|
5349
|
+
</span>
|
|
5350
|
+
</div>
|
|
5351
|
+
</td>
|
|
5352
|
+
<td className="px-3 py-2.5">
|
|
5353
|
+
<div className="flex flex-wrap gap-2">
|
|
5354
|
+
{versionEntries.length ===
|
|
5355
|
+
0 ? (
|
|
5356
|
+
<span className="text-slate-600">
|
|
5357
|
+
none yet
|
|
5358
|
+
</span>
|
|
5359
|
+
) : (
|
|
5360
|
+
versionEntries.map(
|
|
5361
|
+
(ve, vi) => {
|
|
5362
|
+
const vr =
|
|
5363
|
+
asInspectorRecord(
|
|
5364
|
+
ve,
|
|
5365
|
+
);
|
|
5366
|
+
const version =
|
|
5367
|
+
typeof vr.version ===
|
|
5368
|
+
"string"
|
|
5369
|
+
? vr.version
|
|
5370
|
+
: "?";
|
|
5371
|
+
const from =
|
|
5372
|
+
typeof vr.from ===
|
|
5373
|
+
"string"
|
|
5374
|
+
? vr.from
|
|
5375
|
+
: null;
|
|
5376
|
+
const loaded =
|
|
5377
|
+
vr.loaded === true;
|
|
5378
|
+
const eager =
|
|
5379
|
+
vr.eager === true;
|
|
5380
|
+
return (
|
|
5381
|
+
<div
|
|
5382
|
+
key={vi}
|
|
5383
|
+
className={`rounded-lg border px-2 py-1.5 font-mono text-[10px] ${
|
|
5384
|
+
loaded
|
|
5385
|
+
? "border-emerald-500/30 bg-emerald-500/5"
|
|
5386
|
+
: "border-slate-700 bg-slate-800/40"
|
|
5387
|
+
}`}
|
|
5388
|
+
>
|
|
5389
|
+
<div
|
|
5390
|
+
className={`font-semibold ${loaded ? "text-emerald-200" : "text-slate-400"}`}
|
|
5391
|
+
>
|
|
5392
|
+
{version}
|
|
5393
|
+
</div>
|
|
5394
|
+
{from && (
|
|
5395
|
+
<div className="text-slate-500 mt-0.5 text-[9px]">
|
|
5396
|
+
registered by{" "}
|
|
5397
|
+
{from}
|
|
5398
|
+
</div>
|
|
5399
|
+
)}
|
|
5400
|
+
<div className="flex gap-1 mt-1">
|
|
5401
|
+
<span
|
|
5402
|
+
className={`rounded px-1 py-0.5 text-[8px] ${
|
|
5403
|
+
loaded
|
|
5404
|
+
? "bg-emerald-500/20 text-emerald-300"
|
|
5405
|
+
: "bg-slate-700 text-slate-500"
|
|
5406
|
+
}`}
|
|
5407
|
+
>
|
|
5408
|
+
{loaded
|
|
5409
|
+
? "in use"
|
|
5410
|
+
: "registered"}
|
|
5411
|
+
</span>
|
|
5412
|
+
<span
|
|
5413
|
+
className={`rounded px-1 py-0.5 text-[8px] ${
|
|
5414
|
+
eager
|
|
5415
|
+
? "bg-amber-500/20 text-amber-300"
|
|
5416
|
+
: "bg-slate-800 text-slate-600"
|
|
5417
|
+
}`}
|
|
5418
|
+
>
|
|
5419
|
+
{eager
|
|
5420
|
+
? "load now"
|
|
5421
|
+
: "load later"}
|
|
5422
|
+
</span>
|
|
5423
|
+
</div>
|
|
5424
|
+
</div>
|
|
5425
|
+
);
|
|
5426
|
+
},
|
|
5427
|
+
)
|
|
5428
|
+
)}
|
|
5429
|
+
</div>
|
|
5430
|
+
</td>
|
|
5431
|
+
</tr>
|
|
5432
|
+
);
|
|
5433
|
+
},
|
|
5434
|
+
)}
|
|
5435
|
+
</tbody>
|
|
5436
|
+
</table>
|
|
5437
|
+
</div>
|
|
5438
|
+
</div>
|
|
5439
|
+
);
|
|
5440
|
+
})}
|
|
5441
|
+
{typeof mfShareScopeForMatrix.shareScopes.__error ===
|
|
5442
|
+
"string" && (
|
|
5443
|
+
<div className="rounded border border-red-500/20 bg-red-500/10 px-3 py-2 text-[11px] text-red-200">
|
|
5444
|
+
{mfShareScopeForMatrix.shareScopes.__error}
|
|
5445
|
+
</div>
|
|
5446
|
+
)}
|
|
5447
|
+
</>
|
|
5448
|
+
)}
|
|
5449
|
+
</div>
|
|
5450
|
+
)}
|
|
5451
|
+
|
|
5452
|
+
{/* ── EVENTS VIEW ── */}
|
|
5453
|
+
{mfInspectorView === "events" && (
|
|
5454
|
+
<div className="py-2 px-2">
|
|
5455
|
+
{mfInspectorRecentEvents.length === 0 ? (
|
|
5456
|
+
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-slate-500 text-[11px]">
|
|
5457
|
+
<Terminal className="w-5 h-5 opacity-30" />
|
|
5458
|
+
<p>
|
|
5459
|
+
No browser events yet. Start the webpack lab, then
|
|
5460
|
+
open the host page.
|
|
5461
|
+
</p>
|
|
5462
|
+
</div>
|
|
5463
|
+
) : (
|
|
5464
|
+
<div className="space-y-2">
|
|
5465
|
+
<div className="px-1 text-[10px] text-slate-500 leading-relaxed">
|
|
5466
|
+
Newest first. Read each row as: who said something,
|
|
5467
|
+
what happened, and why that step matters.
|
|
5468
|
+
</div>
|
|
5469
|
+
{mfInspectorRecentEvents.map((event) => {
|
|
5470
|
+
const leftBorderColor = event.kind.includes("error")
|
|
5471
|
+
? "border-l-red-500"
|
|
5472
|
+
: event.kind === "identity-check"
|
|
5473
|
+
? "border-l-emerald-500"
|
|
5474
|
+
: event.kind === "remote-load-success"
|
|
5475
|
+
? "border-l-cyan-500"
|
|
5476
|
+
: event.kind === "share-snapshot"
|
|
5477
|
+
? "border-l-amber-500"
|
|
5478
|
+
: event.kind === "runtime-boot"
|
|
5479
|
+
? "border-l-sky-500"
|
|
5480
|
+
: "border-l-slate-700";
|
|
5481
|
+
const appColor =
|
|
5482
|
+
event.app === "host"
|
|
5483
|
+
? "text-sky-400"
|
|
5484
|
+
: "text-violet-400";
|
|
5485
|
+
return (
|
|
5486
|
+
<div
|
|
5487
|
+
key={`${event.runtimeId}-${event.timestamp}-${event.kind}`}
|
|
5488
|
+
className={`flex gap-3 rounded-lg border border-slate-800 bg-slate-900/40 px-3 py-2 border-l-2 hover:bg-slate-800/30 transition-colors ${leftBorderColor}`}
|
|
5489
|
+
>
|
|
5490
|
+
<div className="shrink-0 text-[10px] font-mono text-slate-600 w-16 pt-0.5">
|
|
5491
|
+
{formatInspectorTimestamp(event.timestamp)}
|
|
5492
|
+
</div>
|
|
5493
|
+
<div className="min-w-0 flex-1">
|
|
5494
|
+
<div className="flex flex-wrap items-center gap-1.5 text-[10px]">
|
|
5495
|
+
<span
|
|
5496
|
+
className={`font-mono font-semibold ${appColor}`}
|
|
5497
|
+
>
|
|
5498
|
+
{formatMfInspectorAppLabel(event.app)}
|
|
5499
|
+
</span>
|
|
5500
|
+
<span
|
|
5501
|
+
title={`Raw event name: ${event.kind}`}
|
|
5502
|
+
className={`rounded px-1.5 py-0.5 ${getMfInspectorBadgeClass(event.kind)}`}
|
|
5503
|
+
>
|
|
5504
|
+
{getMfInspectorFriendlyKindLabel(
|
|
5505
|
+
event.kind,
|
|
5506
|
+
)}
|
|
5507
|
+
</span>
|
|
5508
|
+
{event.route && event.route !== "/" && (
|
|
5509
|
+
<span className="font-mono text-slate-600">
|
|
5510
|
+
on {event.route}
|
|
5511
|
+
</span>
|
|
5512
|
+
)}
|
|
5513
|
+
</div>
|
|
5514
|
+
<div className="mt-1 text-[11px] text-slate-300 leading-relaxed">
|
|
5515
|
+
{describeMfInspectorEvent(event)}
|
|
5516
|
+
</div>
|
|
5517
|
+
<div className="mt-1 text-[10px] text-slate-500 leading-relaxed">
|
|
5518
|
+
Why it matters:{" "}
|
|
5519
|
+
{explainMfInspectorEvent(event)}
|
|
5520
|
+
</div>
|
|
5521
|
+
</div>
|
|
5522
|
+
</div>
|
|
5523
|
+
);
|
|
5524
|
+
})}
|
|
5525
|
+
</div>
|
|
5526
|
+
)}
|
|
5527
|
+
</div>
|
|
5528
|
+
)}
|
|
5529
|
+
</div>
|
|
5530
|
+
</div>
|
|
5531
|
+
)}
|
|
5532
|
+
|
|
3535
5533
|
{sbxBottomTab === "console" && (
|
|
3536
5534
|
<div className="flex-1 min-h-0 flex flex-col">
|
|
3537
5535
|
<div className="shrink-0 border-b border-slate-800 bg-slate-900/70 px-3 py-2 flex items-center gap-2">
|