@nexus_js/cli 0.6.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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/add.d.ts +36 -0
- package/dist/add.d.ts.map +1 -0
- package/dist/add.js +342 -0
- package/dist/add.js.map +1 -0
- package/dist/analyzer.d.ts +70 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +247 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/audit.d.ts +35 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +383 -0
- package/dist/audit.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +367 -0
- package/dist/bin.js.map +1 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/create.d.ts +7 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +256 -0
- package/dist/create.js.map +1 -0
- package/dist/fix.d.ts +22 -0
- package/dist/fix.d.ts.map +1 -0
- package/dist/fix.js +199 -0
- package/dist/fix.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/studio.d.ts +136 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +721 -0
- package/dist/studio.js.map +1 -0
- package/package.json +63 -0
package/dist/studio.js
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Studio — Real-time developer dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Launched via `nexus studio` or automatically as a panel within `nexus dev`.
|
|
5
|
+
* Runs a lightweight WebSocket server alongside the main dev server.
|
|
6
|
+
* The browser UI connects to ws://localhost:${STUDIO_PORT}/_nexus/studio
|
|
7
|
+
*
|
|
8
|
+
* Panels:
|
|
9
|
+
* 1. Layout Tree — Visual nested layout hierarchy for the current route.
|
|
10
|
+
* 2. Island Map — All live islands on screen, their state, hydration strategy.
|
|
11
|
+
* 3. Action Log — Real-time stream of Server Action calls, payloads, timings.
|
|
12
|
+
* 4. JS Cost — Bundle analysis for the current route (mirrors nexus analyze).
|
|
13
|
+
* 5. Cache Inspector — Current cache entries, TTLs, hit/miss ratio.
|
|
14
|
+
* 6. Store Viewer — Live snapshot of the Global State Store.
|
|
15
|
+
*/
|
|
16
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
17
|
+
import { createServer as createNetServer } from 'node:net';
|
|
18
|
+
export const STUDIO_PORT = 4000;
|
|
19
|
+
export const STUDIO_WS_PATH = '/_nexus/studio';
|
|
20
|
+
const OPEN = 1;
|
|
21
|
+
const clients = new Set();
|
|
22
|
+
export function broadcast(event) {
|
|
23
|
+
const payload = JSON.stringify(event);
|
|
24
|
+
for (const client of clients) {
|
|
25
|
+
if (client.readyState === OPEN) {
|
|
26
|
+
try {
|
|
27
|
+
client.send(payload);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
clients.delete(client);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ── WebSocket handshake (no external dependency, pure Node.js) ────────────────
|
|
36
|
+
function wsHandshake(req, socket) {
|
|
37
|
+
const key = req.headers['sec-websocket-key'];
|
|
38
|
+
if (!key) {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { createHash } = require('node:crypto');
|
|
43
|
+
const accept = createHash('sha1')
|
|
44
|
+
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
45
|
+
.digest('base64');
|
|
46
|
+
socket.write([
|
|
47
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
48
|
+
'Upgrade: websocket',
|
|
49
|
+
'Connection: Upgrade',
|
|
50
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
51
|
+
'\r\n',
|
|
52
|
+
].join('\r\n'));
|
|
53
|
+
const ws = {
|
|
54
|
+
readyState: OPEN,
|
|
55
|
+
send(data) {
|
|
56
|
+
const buf = Buffer.from(data);
|
|
57
|
+
const len = buf.length;
|
|
58
|
+
const header = Buffer.alloc(len < 126 ? 2 : 4);
|
|
59
|
+
header[0] = 0x81; // FIN + text opcode
|
|
60
|
+
if (len < 126) {
|
|
61
|
+
header[1] = len;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
header[1] = 126;
|
|
65
|
+
header.writeUInt16BE(len, 2);
|
|
66
|
+
}
|
|
67
|
+
socket.write(Buffer.concat([header, buf]));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
clients.add(ws);
|
|
71
|
+
socket.on('close', () => {
|
|
72
|
+
ws.readyState = 3;
|
|
73
|
+
clients.delete(ws);
|
|
74
|
+
});
|
|
75
|
+
// Send a welcome snapshot so the client panel shows current state immediately
|
|
76
|
+
ws.send(JSON.stringify({ type: 'studio:connected', timestamp: Date.now() }));
|
|
77
|
+
}
|
|
78
|
+
// ── HTML Dashboard (single-file, no bundler needed) ───────────────────────────
|
|
79
|
+
function studioHtml(port) {
|
|
80
|
+
return `<!DOCTYPE html>
|
|
81
|
+
<html lang="en">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="utf-8">
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
85
|
+
<title>Nexus Studio</title>
|
|
86
|
+
<style>
|
|
87
|
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Syne:wght@700;800&display=swap');
|
|
88
|
+
|
|
89
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
90
|
+
|
|
91
|
+
:root {
|
|
92
|
+
--bg: #0a0a0f;
|
|
93
|
+
--surface: #111118;
|
|
94
|
+
--border: #1e1e2e;
|
|
95
|
+
--accent: #7c3aed;
|
|
96
|
+
--accent2: #06b6d4;
|
|
97
|
+
--green: #10b981;
|
|
98
|
+
--red: #ef4444;
|
|
99
|
+
--amber: #f59e0b;
|
|
100
|
+
--text: #e2e8f0;
|
|
101
|
+
--muted: #64748b;
|
|
102
|
+
--mono: 'JetBrains Mono', monospace;
|
|
103
|
+
--sans: 'Syne', system-ui, sans-serif;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
body {
|
|
107
|
+
background: var(--bg);
|
|
108
|
+
color: var(--text);
|
|
109
|
+
font-family: var(--mono);
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
height: 100vh;
|
|
112
|
+
display: grid;
|
|
113
|
+
grid-template-rows: 48px 1fr;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Header */
|
|
118
|
+
header {
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 16px;
|
|
122
|
+
padding: 0 20px;
|
|
123
|
+
background: var(--surface);
|
|
124
|
+
border-bottom: 1px solid var(--border);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.logo {
|
|
128
|
+
font-family: var(--sans);
|
|
129
|
+
font-weight: 800;
|
|
130
|
+
font-size: 18px;
|
|
131
|
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
132
|
+
-webkit-background-clip: text;
|
|
133
|
+
-webkit-text-fill-color: transparent;
|
|
134
|
+
background-clip: text;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.status-dot {
|
|
138
|
+
width: 8px; height: 8px;
|
|
139
|
+
border-radius: 50%;
|
|
140
|
+
background: var(--muted);
|
|
141
|
+
transition: background 0.3s;
|
|
142
|
+
}
|
|
143
|
+
.status-dot.connected { background: var(--green); box-shadow: 0 0 8px var(--green); }
|
|
144
|
+
|
|
145
|
+
.status-label { color: var(--muted); font-size: 11px; }
|
|
146
|
+
|
|
147
|
+
nav { display: flex; gap: 4px; margin-left: auto; }
|
|
148
|
+
nav button {
|
|
149
|
+
padding: 4px 12px;
|
|
150
|
+
border: 1px solid var(--border);
|
|
151
|
+
border-radius: 6px;
|
|
152
|
+
background: transparent;
|
|
153
|
+
color: var(--muted);
|
|
154
|
+
font-family: var(--mono);
|
|
155
|
+
font-size: 11px;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
transition: all 0.15s;
|
|
158
|
+
}
|
|
159
|
+
nav button.active, nav button:hover {
|
|
160
|
+
background: var(--accent);
|
|
161
|
+
border-color: var(--accent);
|
|
162
|
+
color: #fff;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Main layout */
|
|
166
|
+
.workspace {
|
|
167
|
+
display: grid;
|
|
168
|
+
grid-template-columns: 280px 1fr 300px;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Panels */
|
|
173
|
+
.panel {
|
|
174
|
+
border-right: 1px solid var(--border);
|
|
175
|
+
overflow-y: auto;
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.panel:last-child { border-right: none; }
|
|
181
|
+
|
|
182
|
+
.panel-header {
|
|
183
|
+
padding: 10px 14px;
|
|
184
|
+
border-bottom: 1px solid var(--border);
|
|
185
|
+
font-family: var(--sans);
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
font-weight: 700;
|
|
188
|
+
color: var(--muted);
|
|
189
|
+
text-transform: uppercase;
|
|
190
|
+
letter-spacing: 0.08em;
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: space-between;
|
|
194
|
+
position: sticky;
|
|
195
|
+
top: 0;
|
|
196
|
+
background: var(--surface);
|
|
197
|
+
z-index: 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.panel-content { padding: 10px; flex: 1; }
|
|
201
|
+
|
|
202
|
+
/* Island card */
|
|
203
|
+
.island-card {
|
|
204
|
+
border: 1px solid var(--border);
|
|
205
|
+
border-radius: 8px;
|
|
206
|
+
padding: 10px;
|
|
207
|
+
margin-bottom: 8px;
|
|
208
|
+
background: rgba(124, 58, 237, 0.04);
|
|
209
|
+
transition: border-color 0.2s;
|
|
210
|
+
}
|
|
211
|
+
.island-card:hover { border-color: var(--accent); }
|
|
212
|
+
|
|
213
|
+
.island-name {
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
color: var(--accent2);
|
|
216
|
+
margin-bottom: 4px;
|
|
217
|
+
font-size: 12px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.island-meta { color: var(--muted); font-size: 11px; }
|
|
221
|
+
|
|
222
|
+
.badge {
|
|
223
|
+
display: inline-block;
|
|
224
|
+
padding: 1px 6px;
|
|
225
|
+
border-radius: 4px;
|
|
226
|
+
font-size: 10px;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
letter-spacing: 0.04em;
|
|
229
|
+
}
|
|
230
|
+
.badge-load { background: rgba(16,185,129,0.15); color: var(--green); }
|
|
231
|
+
.badge-idle { background: rgba(245,158,11,0.15); color: var(--amber); }
|
|
232
|
+
.badge-visible { background: rgba(6,182,212,0.15); color: var(--accent2); }
|
|
233
|
+
.badge-server { background: rgba(100,116,139,0.15); color: var(--muted); }
|
|
234
|
+
|
|
235
|
+
/* Action log */
|
|
236
|
+
.action-entry {
|
|
237
|
+
border-bottom: 1px solid var(--border);
|
|
238
|
+
padding: 8px 0;
|
|
239
|
+
font-size: 11px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.action-name { color: var(--accent); font-weight: 600; margin-bottom: 2px; }
|
|
243
|
+
.action-duration { color: var(--green); }
|
|
244
|
+
.action-error { color: var(--red); }
|
|
245
|
+
|
|
246
|
+
.action-payload {
|
|
247
|
+
margin-top: 4px;
|
|
248
|
+
padding: 6px 8px;
|
|
249
|
+
background: var(--bg);
|
|
250
|
+
border-radius: 4px;
|
|
251
|
+
overflow-x: auto;
|
|
252
|
+
white-space: pre;
|
|
253
|
+
max-height: 80px;
|
|
254
|
+
font-size: 10px;
|
|
255
|
+
color: var(--text);
|
|
256
|
+
display: none;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.action-entry.expanded .action-payload { display: block; }
|
|
260
|
+
.action-entry { cursor: pointer; }
|
|
261
|
+
|
|
262
|
+
/* Route info */
|
|
263
|
+
.route-path {
|
|
264
|
+
font-size: 18px;
|
|
265
|
+
font-weight: 600;
|
|
266
|
+
color: var(--text);
|
|
267
|
+
margin-bottom: 12px;
|
|
268
|
+
word-break: break-all;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.metric-row {
|
|
272
|
+
display: flex;
|
|
273
|
+
justify-content: space-between;
|
|
274
|
+
align-items: center;
|
|
275
|
+
padding: 6px 0;
|
|
276
|
+
border-bottom: 1px solid var(--border);
|
|
277
|
+
font-size: 11px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.metric-label { color: var(--muted); }
|
|
281
|
+
|
|
282
|
+
.metric-value { font-weight: 600; }
|
|
283
|
+
.metric-value.good { color: var(--green); }
|
|
284
|
+
.metric-value.warn { color: var(--amber); }
|
|
285
|
+
.metric-value.bad { color: var(--red); }
|
|
286
|
+
|
|
287
|
+
/* Progress bar */
|
|
288
|
+
.budget-bar {
|
|
289
|
+
height: 4px;
|
|
290
|
+
border-radius: 2px;
|
|
291
|
+
background: var(--border);
|
|
292
|
+
margin-top: 4px;
|
|
293
|
+
overflow: hidden;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.budget-fill {
|
|
297
|
+
height: 100%;
|
|
298
|
+
border-radius: 2px;
|
|
299
|
+
background: var(--green);
|
|
300
|
+
transition: width 0.4s ease;
|
|
301
|
+
}
|
|
302
|
+
.budget-fill.over { background: var(--red); }
|
|
303
|
+
.budget-fill.warn { background: var(--amber); }
|
|
304
|
+
|
|
305
|
+
/* Cache inspector */
|
|
306
|
+
.cache-entry {
|
|
307
|
+
padding: 6px 0;
|
|
308
|
+
border-bottom: 1px solid var(--border);
|
|
309
|
+
font-size: 11px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.cache-key { color: var(--accent2); margin-bottom: 2px; }
|
|
313
|
+
.cache-ttl { color: var(--muted); }
|
|
314
|
+
|
|
315
|
+
/* Store viewer */
|
|
316
|
+
.store-entry {
|
|
317
|
+
padding: 6px 0;
|
|
318
|
+
border-bottom: 1px solid var(--border);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.store-key { color: var(--accent); font-size: 11px; margin-bottom: 2px; }
|
|
322
|
+
.store-value {
|
|
323
|
+
font-size: 10px;
|
|
324
|
+
color: var(--text);
|
|
325
|
+
background: var(--bg);
|
|
326
|
+
padding: 4px 6px;
|
|
327
|
+
border-radius: 4px;
|
|
328
|
+
white-space: pre-wrap;
|
|
329
|
+
max-height: 60px;
|
|
330
|
+
overflow-y: auto;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Layout tree */
|
|
334
|
+
.layout-node {
|
|
335
|
+
padding: 5px 8px;
|
|
336
|
+
margin: 2px 0;
|
|
337
|
+
border-left: 2px solid var(--border);
|
|
338
|
+
font-size: 11px;
|
|
339
|
+
color: var(--muted);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.layout-node.active {
|
|
343
|
+
border-color: var(--accent);
|
|
344
|
+
color: var(--text);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* Scrollbars */
|
|
348
|
+
::-webkit-scrollbar { width: 4px; height: 4px; }
|
|
349
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
350
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
351
|
+
|
|
352
|
+
.empty-state {
|
|
353
|
+
text-align: center;
|
|
354
|
+
padding: 32px;
|
|
355
|
+
color: var(--muted);
|
|
356
|
+
font-size: 11px;
|
|
357
|
+
}
|
|
358
|
+
</style>
|
|
359
|
+
</head>
|
|
360
|
+
<body>
|
|
361
|
+
<header>
|
|
362
|
+
<span class="logo">Nexus Studio</span>
|
|
363
|
+
<div class="status-dot" id="statusDot"></div>
|
|
364
|
+
<span class="status-label" id="statusLabel">Connecting...</span>
|
|
365
|
+
<nav>
|
|
366
|
+
<button class="active" data-panel="overview">Overview</button>
|
|
367
|
+
<button data-panel="islands">Islands</button>
|
|
368
|
+
<button data-panel="actions">Actions</button>
|
|
369
|
+
<button data-panel="cache">Cache</button>
|
|
370
|
+
<button data-panel="store">Store</button>
|
|
371
|
+
</nav>
|
|
372
|
+
</header>
|
|
373
|
+
|
|
374
|
+
<div class="workspace">
|
|
375
|
+
<!-- Left: Layout Tree -->
|
|
376
|
+
<div class="panel" id="panelLeft">
|
|
377
|
+
<div class="panel-header">
|
|
378
|
+
<span>Layout Tree</span>
|
|
379
|
+
<span id="routeCount">0 islands</span>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="panel-content" id="layoutTree">
|
|
382
|
+
<div class="empty-state">Navigate to a page to see the layout tree.</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<!-- Center: Main Content -->
|
|
387
|
+
<div class="panel" id="panelCenter" style="border-right:none">
|
|
388
|
+
<div class="panel-header">
|
|
389
|
+
<span id="centerPanelTitle">Route Overview</span>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="panel-content" id="centerContent">
|
|
392
|
+
<div class="empty-state">No route loaded yet. Open your browser to a Nexus page.</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Right: Context -->
|
|
397
|
+
<div class="panel" id="panelRight">
|
|
398
|
+
<div class="panel-header">
|
|
399
|
+
<span>Action Log</span>
|
|
400
|
+
<button onclick="clearActions()" style="font-size:10px;padding:2px 6px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--muted);cursor:pointer">Clear</button>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="panel-content" id="actionLog">
|
|
403
|
+
<div class="empty-state">No actions fired yet.</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<script>
|
|
409
|
+
const WS_PORT = ${port};
|
|
410
|
+
const state = {
|
|
411
|
+
connected: false,
|
|
412
|
+
route: null,
|
|
413
|
+
islands: new Map(),
|
|
414
|
+
actions: [],
|
|
415
|
+
cache: new Map(),
|
|
416
|
+
store: new Map(),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// ── WebSocket ────────────────────────────────────────────────────────────
|
|
420
|
+
let ws;
|
|
421
|
+
let reconnectTimer;
|
|
422
|
+
|
|
423
|
+
function connect() {
|
|
424
|
+
ws = new WebSocket('ws://localhost:' + WS_PORT + '/_nexus/studio');
|
|
425
|
+
|
|
426
|
+
ws.onopen = () => {
|
|
427
|
+
state.connected = true;
|
|
428
|
+
document.getElementById('statusDot').classList.add('connected');
|
|
429
|
+
document.getElementById('statusLabel').textContent = 'Connected';
|
|
430
|
+
clearTimeout(reconnectTimer);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
ws.onmessage = (e) => {
|
|
434
|
+
const event = JSON.parse(e.data);
|
|
435
|
+
handleEvent(event);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
ws.onclose = () => {
|
|
439
|
+
state.connected = false;
|
|
440
|
+
document.getElementById('statusDot').classList.remove('connected');
|
|
441
|
+
document.getElementById('statusLabel').textContent = 'Disconnected — retrying...';
|
|
442
|
+
reconnectTimer = setTimeout(connect, 2000);
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
connect();
|
|
447
|
+
|
|
448
|
+
// ── Event handlers ────────────────────────────────────────────────────────
|
|
449
|
+
function handleEvent(event) {
|
|
450
|
+
switch (event.type) {
|
|
451
|
+
case 'route:change':
|
|
452
|
+
state.route = event.payload;
|
|
453
|
+
state.islands.clear();
|
|
454
|
+
renderLayoutTree();
|
|
455
|
+
renderOverview();
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case 'island:mounted':
|
|
459
|
+
state.islands.set(event.payload.id, event.payload);
|
|
460
|
+
document.getElementById('routeCount').textContent = state.islands.size + ' islands';
|
|
461
|
+
renderIslandPanel();
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
case 'island:destroyed':
|
|
465
|
+
state.islands.delete(event.payload.id);
|
|
466
|
+
document.getElementById('routeCount').textContent = state.islands.size + ' islands';
|
|
467
|
+
renderIslandPanel();
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case 'island:state':
|
|
471
|
+
const isl = state.islands.get(event.payload.id);
|
|
472
|
+
if (isl) { isl.state = event.payload.state; renderIslandPanel(); }
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
case 'action:call':
|
|
476
|
+
state.actions.unshift({ ...event.payload, status: 'pending' });
|
|
477
|
+
if (state.actions.length > 100) state.actions.pop();
|
|
478
|
+
renderActionLog();
|
|
479
|
+
break;
|
|
480
|
+
|
|
481
|
+
case 'action:result':
|
|
482
|
+
const a = state.actions.find(x => x.id === event.payload.id);
|
|
483
|
+
if (a) { Object.assign(a, event.payload, { status: 'success' }); renderActionLog(); }
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'action:error':
|
|
487
|
+
const ae = state.actions.find(x => x.id === event.payload.id);
|
|
488
|
+
if (ae) { Object.assign(ae, event.payload, { status: 'error' }); renderActionLog(); }
|
|
489
|
+
break;
|
|
490
|
+
|
|
491
|
+
case 'cache:set':
|
|
492
|
+
state.cache.set(event.payload.key, event.payload);
|
|
493
|
+
renderCachePanel();
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case 'cache:hit':
|
|
497
|
+
case 'cache:miss':
|
|
498
|
+
const ce = state.cache.get(event.payload.key);
|
|
499
|
+
if (ce) { ce.lastAccess = event.type === 'cache:hit' ? 'HIT' : 'MISS'; renderCachePanel(); }
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
case 'store:update':
|
|
503
|
+
state.store.set(event.payload.key, event.payload.value);
|
|
504
|
+
renderStorePanel();
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ── Renderers ─────────────────────────────────────────────────────────────
|
|
510
|
+
function renderLayoutTree() {
|
|
511
|
+
const el = document.getElementById('layoutTree');
|
|
512
|
+
if (!state.route) { el.innerHTML = '<div class="empty-state">No route.</div>'; return; }
|
|
513
|
+
|
|
514
|
+
const layouts = state.route.layouts ?? [];
|
|
515
|
+
let html = layouts.map((l, i) =>
|
|
516
|
+
'<div class="layout-node" style="padding-left:' + (8 + i * 12) + 'px">' +
|
|
517
|
+
'┣ ' + l.replace(/.*\\//, '') + '</div>'
|
|
518
|
+
).join('');
|
|
519
|
+
html += '<div class="layout-node active" style="padding-left:' + (8 + layouts.length * 12) + 'px">◆ ' +
|
|
520
|
+
(state.route.page ?? 'page').replace(/.*\\//, '') + '</div>';
|
|
521
|
+
|
|
522
|
+
el.innerHTML = html;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function renderOverview() {
|
|
526
|
+
const el = document.getElementById('centerContent');
|
|
527
|
+
if (!state.route) return;
|
|
528
|
+
const r = state.route;
|
|
529
|
+
const jsCost = r.jsCost ?? {};
|
|
530
|
+
const raw = jsCost.raw ?? 0;
|
|
531
|
+
const budget = jsCost.budget ?? 50000;
|
|
532
|
+
const pct = Math.min((raw / budget) * 100, 100);
|
|
533
|
+
const colorClass = pct > 100 ? 'bad' : pct > 70 ? 'warn' : 'good';
|
|
534
|
+
|
|
535
|
+
el.innerHTML = \`
|
|
536
|
+
<div class="route-path">\${r.path || '/'}</div>
|
|
537
|
+
<div class="metric-row">
|
|
538
|
+
<span class="metric-label">Cache Strategy</span>
|
|
539
|
+
<span class="metric-value">\${r.cacheStrategy || '—'}</span>
|
|
540
|
+
</div>
|
|
541
|
+
<div class="metric-row">
|
|
542
|
+
<span class="metric-label">Cache TTL</span>
|
|
543
|
+
<span class="metric-value \${r.cacheTtl > 0 ? 'good' : 'warn'}">\${r.cacheTtl > 0 ? r.cacheTtl + 's' : 'no-store'}</span>
|
|
544
|
+
</div>
|
|
545
|
+
<div class="metric-row">
|
|
546
|
+
<span class="metric-label">JS Budget</span>
|
|
547
|
+
<span class="metric-value \${colorClass}">\${(raw/1024).toFixed(1)}kb / \${(budget/1024).toFixed(0)}kb</span>
|
|
548
|
+
</div>
|
|
549
|
+
<div class="budget-bar">
|
|
550
|
+
<div class="budget-fill \${colorClass !== 'good' ? colorClass : ''}" style="width:\${pct}%"></div>
|
|
551
|
+
</div>
|
|
552
|
+
<div class="metric-row" style="margin-top:12px">
|
|
553
|
+
<span class="metric-label">Active Islands</span>
|
|
554
|
+
<span class="metric-value">\${state.islands.size}</span>
|
|
555
|
+
</div>
|
|
556
|
+
<div class="metric-row">
|
|
557
|
+
<span class="metric-label">Params</span>
|
|
558
|
+
<span class="metric-value">\${Object.entries(r.params || {}).map(([k,v]) => k + '=' + v).join(', ') || '—'}</span>
|
|
559
|
+
</div>
|
|
560
|
+
\`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function renderIslandPanel() {
|
|
564
|
+
const el = document.getElementById('centerContent');
|
|
565
|
+
const title = document.getElementById('centerPanelTitle');
|
|
566
|
+
if (document.querySelector('nav button.active')?.dataset.panel !== 'islands') return;
|
|
567
|
+
title.textContent = 'Live Islands (' + state.islands.size + ')';
|
|
568
|
+
|
|
569
|
+
if (state.islands.size === 0) {
|
|
570
|
+
el.innerHTML = '<div class="empty-state">No islands active on this page.</div>';
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
el.innerHTML = Array.from(state.islands.values()).map(isl => \`
|
|
575
|
+
<div class="island-card">
|
|
576
|
+
<div class="island-name">\${isl.component.replace(/.*\\//, '')}</div>
|
|
577
|
+
<div class="island-meta" style="margin-bottom:6px">
|
|
578
|
+
<span class="badge badge-\${isl.strategy?.replace('client:', '') ?? 'load'}">\${isl.strategy ?? 'client:load'}</span>
|
|
579
|
+
<span style="margin-left:6px;color:var(--muted)">#\${isl.id?.slice(0,8)}</span>
|
|
580
|
+
</div>
|
|
581
|
+
<div style="font-size:10px;color:var(--muted)">State:</div>
|
|
582
|
+
<div class="store-value">\${JSON.stringify(isl.state ?? {}, null, 2)}</div>
|
|
583
|
+
</div>
|
|
584
|
+
\`).join('');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function renderActionLog() {
|
|
588
|
+
const el = document.getElementById('actionLog');
|
|
589
|
+
if (state.actions.length === 0) {
|
|
590
|
+
el.innerHTML = '<div class="empty-state">No actions fired yet.</div>';
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
el.innerHTML = state.actions.slice(0, 30).map(a => \`
|
|
595
|
+
<div class="action-entry" onclick="this.classList.toggle('expanded')">
|
|
596
|
+
<div class="action-name">\${a.name}</div>
|
|
597
|
+
<div class="island-meta">
|
|
598
|
+
\${a.status === 'pending' ? '<span style="color:var(--amber)">⏳ pending</span>' : ''}
|
|
599
|
+
\${a.status === 'success' ? '<span class="action-duration">✓ ' + a.duration + 'ms</span>' : ''}
|
|
600
|
+
\${a.status === 'error' ? '<span class="action-error">✗ ' + (a.error ?? 'error') + '</span>' : ''}
|
|
601
|
+
\${a.islandId ? ' · ' + a.islandId.slice(0,8) : ''}
|
|
602
|
+
</div>
|
|
603
|
+
<div class="action-payload">\${JSON.stringify(a.input ?? {}, null, 2)}</div>
|
|
604
|
+
</div>
|
|
605
|
+
\`).join('');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function renderCachePanel() {
|
|
609
|
+
const el = document.getElementById('centerContent');
|
|
610
|
+
if (document.querySelector('nav button.active')?.dataset.panel !== 'cache') return;
|
|
611
|
+
|
|
612
|
+
if (state.cache.size === 0) {
|
|
613
|
+
el.innerHTML = '<div class="empty-state">No cache entries yet.</div>';
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
el.innerHTML = Array.from(state.cache.values()).map(c => \`
|
|
618
|
+
<div class="cache-entry">
|
|
619
|
+
<div class="cache-key">\${c.key}</div>
|
|
620
|
+
<div class="cache-ttl">TTL: \${c.ttl}s · Size: \${(c.size/1024).toFixed(1)}kb \${c.lastAccess ? '· ' + c.lastAccess : ''}</div>
|
|
621
|
+
\${c.tags?.length ? '<div class="cache-ttl">Tags: ' + c.tags.join(', ') + '</div>' : ''}
|
|
622
|
+
</div>
|
|
623
|
+
\`).join('');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function renderStorePanel() {
|
|
627
|
+
const el = document.getElementById('centerContent');
|
|
628
|
+
if (document.querySelector('nav button.active')?.dataset.panel !== 'store') return;
|
|
629
|
+
|
|
630
|
+
if (state.store.size === 0) {
|
|
631
|
+
el.innerHTML = '<div class="empty-state">No store entries yet.</div>';
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
el.innerHTML = Array.from(state.store.entries()).map(([k, v]) => \`
|
|
636
|
+
<div class="store-entry">
|
|
637
|
+
<div class="store-key">\${k}</div>
|
|
638
|
+
<div class="store-value">\${JSON.stringify(v, null, 2)}</div>
|
|
639
|
+
</div>
|
|
640
|
+
\`).join('');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ── Nav ───────────────────────────────────────────────────────────────────
|
|
644
|
+
const panelRenderers = {
|
|
645
|
+
overview: renderOverview,
|
|
646
|
+
islands: renderIslandPanel,
|
|
647
|
+
actions: () => {},
|
|
648
|
+
cache: renderCachePanel,
|
|
649
|
+
store: renderStorePanel,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
document.querySelectorAll('nav button').forEach(btn => {
|
|
653
|
+
btn.addEventListener('click', () => {
|
|
654
|
+
document.querySelectorAll('nav button').forEach(b => b.classList.remove('active'));
|
|
655
|
+
btn.classList.add('active');
|
|
656
|
+
const panel = btn.dataset.panel;
|
|
657
|
+
document.getElementById('centerPanelTitle').textContent = btn.textContent;
|
|
658
|
+
|
|
659
|
+
if (panel === 'actions') {
|
|
660
|
+
document.getElementById('panelRight').style.display = '';
|
|
661
|
+
document.getElementById('centerContent').innerHTML = '<div class="empty-state">Click an action to expand its payload.</div>';
|
|
662
|
+
} else {
|
|
663
|
+
panelRenderers[panel]?.();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
function clearActions() {
|
|
669
|
+
state.actions = [];
|
|
670
|
+
renderActionLog();
|
|
671
|
+
}
|
|
672
|
+
</script>
|
|
673
|
+
</body>
|
|
674
|
+
</html>`;
|
|
675
|
+
}
|
|
676
|
+
export async function startStudio(preferredPort = STUDIO_PORT) {
|
|
677
|
+
const port = await findFreePort(preferredPort);
|
|
678
|
+
const server = createHttpServer((req, res) => {
|
|
679
|
+
if (req.url === '/_nexus/studio' && req.headers.upgrade?.toLowerCase() === 'websocket') {
|
|
680
|
+
return; // Handled by upgrade event
|
|
681
|
+
}
|
|
682
|
+
if (req.url === '/' || req.url === '/studio') {
|
|
683
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
684
|
+
res.end(studioHtml(port));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
res.writeHead(404).end();
|
|
688
|
+
});
|
|
689
|
+
server.on('upgrade', (req, socket, head) => {
|
|
690
|
+
if (req.url === STUDIO_WS_PATH) {
|
|
691
|
+
wsHandshake(req, socket);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
socket.destroy();
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
await new Promise((resolve) => server.listen(port, '127.0.0.1', resolve));
|
|
698
|
+
console.log(`\n ◆ Nexus Studio http://localhost:${port}\n`);
|
|
699
|
+
return {
|
|
700
|
+
port,
|
|
701
|
+
close: () => server.close(),
|
|
702
|
+
broadcast,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async function findFreePort(start) {
|
|
706
|
+
for (let port = start; port < start + 100; port++) {
|
|
707
|
+
const free = await isPortFree(port);
|
|
708
|
+
if (free)
|
|
709
|
+
return port;
|
|
710
|
+
}
|
|
711
|
+
throw new Error('No free port found for Nexus Studio');
|
|
712
|
+
}
|
|
713
|
+
function isPortFree(port) {
|
|
714
|
+
return new Promise((resolve) => {
|
|
715
|
+
const server = createNetServer();
|
|
716
|
+
server.once('error', () => resolve(false));
|
|
717
|
+
server.once('listening', () => { server.close(); resolve(true); });
|
|
718
|
+
server.listen(port, '127.0.0.1');
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
//# sourceMappingURL=studio.js.map
|