@ereo/dev-inspector 0.1.6

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/dist/index.js ADDED
@@ -0,0 +1,2538 @@
1
+ // @bun
2
+ // src/inspector.ts
3
+ function generateInspectorHTML(routes) {
4
+ return `<!DOCTYPE html>
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="UTF-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
+ <title>EreoJS Route Inspector</title>
10
+ <style>
11
+ * { box-sizing: border-box; margin: 0; padding: 0; }
12
+ body {
13
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
14
+ background: #0f172a;
15
+ color: #e2e8f0;
16
+ line-height: 1.6;
17
+ }
18
+ .container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
19
+ header {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 1rem;
23
+ margin-bottom: 2rem;
24
+ padding-bottom: 1rem;
25
+ border-bottom: 1px solid #334155;
26
+ }
27
+ .logo {
28
+ width: 40px; height: 40px;
29
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
30
+ border-radius: 8px;
31
+ display: flex; align-items: center; justify-content: center;
32
+ font-weight: bold; font-size: 1.2rem;
33
+ }
34
+ h1 { font-size: 1.5rem; font-weight: 600; }
35
+ .stats {
36
+ display: grid;
37
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
38
+ gap: 1rem;
39
+ margin-bottom: 2rem;
40
+ }
41
+ .stat-card {
42
+ background: #1e293b;
43
+ padding: 1.5rem;
44
+ border-radius: 12px;
45
+ border: 1px solid #334155;
46
+ }
47
+ .stat-value { font-size: 2rem; font-weight: 700; color: #3b82f6; }
48
+ .stat-label { font-size: 0.875rem; color: #94a3b8; }
49
+ .route-tree { background: #1e293b; border-radius: 12px; border: 1px solid #334155; }
50
+ .route-item {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 0.75rem;
54
+ padding: 1rem 1.5rem;
55
+ border-bottom: 1px solid #334155;
56
+ transition: background 0.2s;
57
+ }
58
+ .route-item:hover { background: #252f47; }
59
+ .route-item:last-child { border-bottom: none; }
60
+ .method-badge {
61
+ padding: 0.25rem 0.5rem;
62
+ border-radius: 4px;
63
+ font-size: 0.75rem;
64
+ font-weight: 600;
65
+ text-transform: uppercase;
66
+ }
67
+ .method-ssr { background: #3b82f6; color: white; }
68
+ .method-ssg { background: #10b981; color: white; }
69
+ .method-csr { background: #f59e0b; color: white; }
70
+ .method-api { background: #8b5cf6; color: white; }
71
+ .method-rsc { background: #ec4899; color: white; }
72
+ .route-path { font-family: 'Monaco', 'Menlo', monospace; font-size: 0.9rem; }
73
+ .route-file { font-size: 0.8rem; color: #64748b; margin-left: auto; }
74
+ .route-tags {
75
+ display: flex;
76
+ gap: 0.5rem;
77
+ margin-left: 1rem;
78
+ }
79
+ .tag {
80
+ padding: 0.125rem 0.375rem;
81
+ background: #334155;
82
+ border-radius: 4px;
83
+ font-size: 0.75rem;
84
+ }
85
+ .tag-islands { background: #059669; }
86
+ .tag-loader { background: #2563eb; }
87
+ .tag-action { background: #dc2626; }
88
+ .tag-auth { background: #f59e0b; }
89
+ .search-box {
90
+ width: 100%;
91
+ padding: 0.75rem 1rem;
92
+ margin-bottom: 1rem;
93
+ background: #1e293b;
94
+ border: 1px solid #334155;
95
+ border-radius: 8px;
96
+ color: #e2e8f0;
97
+ font-size: 1rem;
98
+ }
99
+ .search-box:focus { outline: none; border-color: #3b82f6; }
100
+ .section-title {
101
+ font-size: 1rem;
102
+ font-weight: 600;
103
+ margin-bottom: 1rem;
104
+ color: #94a3b8;
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.05em;
107
+ }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <div class="container">
112
+ <header>
113
+ <div class="logo">O</div>
114
+ <h1>Route Inspector</h1>
115
+ </header>
116
+
117
+ <div class="stats">
118
+ <div class="stat-card">
119
+ <div class="stat-value">${routes.length}</div>
120
+ <div class="stat-label">Total Routes</div>
121
+ </div>
122
+ <div class="stat-card">
123
+ <div class="stat-value">${routes.filter((r) => r.renderMode === "ssr").length}</div>
124
+ <div class="stat-label">SSR Routes</div>
125
+ </div>
126
+ <div class="stat-card">
127
+ <div class="stat-value">${routes.filter((r) => r.renderMode === "ssg").length}</div>
128
+ <div class="stat-label">SSG Routes</div>
129
+ </div>
130
+ <div class="stat-card">
131
+ <div class="stat-value">${routes.filter((r) => r.renderMode === "api" || r.file.includes(".api.")).length}</div>
132
+ <div class="stat-label">API Routes</div>
133
+ </div>
134
+ </div>
135
+
136
+ <div class="section-title">Route Tree</div>
137
+ <input type="text" class="search-box" placeholder="Search routes..." id="search">
138
+
139
+ <div class="route-tree">
140
+ ${routes.map((route) => `
141
+ <div class="route-item" data-path="${route.path.toLowerCase()}">
142
+ <span class="method-badge method-${route.renderMode}">${route.renderMode}</span>
143
+ <span class="route-path">${route.path}</span>
144
+ <div class="route-tags">
145
+ ${route.islandCount > 0 ? `<span class="tag tag-islands">${route.islandCount} islands</span>` : ""}
146
+ ${route.hasLoader ? `<span class="tag tag-loader">loader</span>` : ""}
147
+ ${route.hasAction ? `<span class="tag tag-action">action</span>` : ""}
148
+ ${route.authRequired ? `<span class="tag tag-auth">auth</span>` : ""}
149
+ </div>
150
+ <span class="route-file">${route.file}</span>
151
+ </div>
152
+ `).join("")}
153
+ </div>
154
+ </div>
155
+
156
+ <script>
157
+ const searchInput = document.getElementById('search');
158
+ const routeItems = document.querySelectorAll('.route-item');
159
+
160
+ searchInput.addEventListener('input', (e) => {
161
+ const query = e.target.value.toLowerCase();
162
+ routeItems.forEach(item => {
163
+ const path = item.getAttribute('data-path');
164
+ item.style.display = path.includes(query) ? 'flex' : 'none';
165
+ });
166
+ });
167
+ </script>
168
+ </body>
169
+ </html>`;
170
+ }
171
+ function createRouteInfo(routes) {
172
+ return routes.map((route) => {
173
+ const config = route.config || {};
174
+ const renderMode = config.render?.mode || "ssr";
175
+ return {
176
+ id: route.id,
177
+ path: route.path,
178
+ file: route.file,
179
+ renderMode,
180
+ islandCount: config.islands?.components?.length || 0,
181
+ hasLoader: route.module?.loader !== undefined,
182
+ hasAction: route.module?.action !== undefined,
183
+ middlewareCount: config.middleware?.length || 0,
184
+ cacheTags: config.cache?.data?.tags,
185
+ authRequired: config.auth?.required
186
+ };
187
+ });
188
+ }
189
+ function createDevInspector(config = {}) {
190
+ const mountPath = config.mountPath || "/__ereo";
191
+ return {
192
+ name: "@ereo/dev-inspector",
193
+ configureServer(server) {
194
+ let routeInfo = [];
195
+ server.middlewares.push(async (request, _ctx, next) => {
196
+ const url = new URL(request.url);
197
+ if (url.pathname === mountPath) {
198
+ const html = generateInspectorHTML(routeInfo);
199
+ return new Response(html, {
200
+ headers: { "Content-Type": "text/html" }
201
+ });
202
+ }
203
+ if (url.pathname === `${mountPath}/api/routes`) {
204
+ return new Response(JSON.stringify(routeInfo), {
205
+ headers: { "Content-Type": "application/json" }
206
+ });
207
+ }
208
+ return next();
209
+ });
210
+ console.log(`[inspector] Mounted at ${mountPath}`);
211
+ }
212
+ };
213
+ }
214
+ function formatRouteTree(routes) {
215
+ const lines = ["Route Tree:", ""];
216
+ routes.forEach((route) => {
217
+ const icon = getRenderModeIcon(route.renderMode);
218
+ const tags = [];
219
+ if (route.hasLoader)
220
+ tags.push("loader");
221
+ if (route.hasAction)
222
+ tags.push("action");
223
+ if (route.islandCount > 0)
224
+ tags.push(`${route.islandCount} islands`);
225
+ if (route.authRequired)
226
+ tags.push("auth");
227
+ const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
228
+ lines.push(` ${icon} ${route.path}${tagStr}`);
229
+ lines.push(` \u2192 ${route.file}`);
230
+ });
231
+ return lines.join(`
232
+ `);
233
+ }
234
+ function getRenderModeIcon(mode) {
235
+ switch (mode) {
236
+ case "ssr":
237
+ return "\u26A1";
238
+ case "ssg":
239
+ return "\uD83D\uDCC4";
240
+ case "csr":
241
+ return "\uD83D\uDCBB";
242
+ case "api":
243
+ return "\uD83D\uDD0C";
244
+ case "rsc":
245
+ return "\uD83D\uDE80";
246
+ default:
247
+ return "\u2022";
248
+ }
249
+ }
250
+ // src/devtools/DataPipelineTab.tsx
251
+ function generateDataPipelineHTML(data) {
252
+ const { route, totalTime, loaders, efficiency, waterfalls, timestamp } = data;
253
+ const timelineHTML = loaders.sort((a, b) => a.start - b.start).map((loader) => generateLoaderBar(loader, totalTime)).join(`
254
+ `);
255
+ const waterfallHTML = waterfalls.map((w) => `
256
+ <div class="waterfall-warning ${w.necessary ? "necessary" : "unnecessary"}">
257
+ <span class="icon">${w.necessary ? "\u26A0\uFE0F" : "\uD83D\uDCA1"}</span>
258
+ <span class="message">${w.suggestion || `'${w.loader}' waited for ${w.waitedFor.join(", ")}`}</span>
259
+ </div>
260
+ `).join(`
261
+ `);
262
+ return `
263
+ <div class="pipeline-container">
264
+ <div class="pipeline-header">
265
+ <h3>Data Pipeline</h3>
266
+ <div class="route-info">
267
+ <span class="route-path">${escapeHtml(route)}</span>
268
+ <span class="total-time">${totalTime.toFixed(1)}ms</span>
269
+ </div>
270
+ </div>
271
+
272
+ <div class="efficiency-meter">
273
+ <div class="efficiency-label">
274
+ Parallel Efficiency:
275
+ <span class="${getEfficiencyClass(efficiency)}">${(efficiency * 100).toFixed(0)}%</span>
276
+ </div>
277
+ <div class="efficiency-bar">
278
+ <div class="efficiency-fill" style="width: ${efficiency * 100}%"></div>
279
+ </div>
280
+ <div class="efficiency-hint">
281
+ ${getEfficiencyHint(efficiency)}
282
+ </div>
283
+ </div>
284
+
285
+ <div class="timeline-container">
286
+ <div class="timeline-header">
287
+ <span class="timeline-label">Loader</span>
288
+ <span class="timeline-bar-header">
289
+ <span>0ms</span>
290
+ <span>${(totalTime / 2).toFixed(0)}ms</span>
291
+ <span>${totalTime.toFixed(0)}ms</span>
292
+ </span>
293
+ <span class="timeline-duration">Duration</span>
294
+ </div>
295
+ <div class="timeline-body">
296
+ ${timelineHTML}
297
+ </div>
298
+ </div>
299
+
300
+ ${waterfalls.length > 0 ? `
301
+ <div class="waterfall-section">
302
+ <h4>Optimization Opportunities</h4>
303
+ ${waterfallHTML}
304
+ </div>
305
+ ` : `
306
+ <div class="all-good">
307
+ \u2705 No waterfall issues detected
308
+ </div>
309
+ `}
310
+
311
+ <div class="pipeline-footer">
312
+ <span class="timestamp">Recorded at ${new Date(timestamp).toLocaleTimeString()}</span>
313
+ </div>
314
+ </div>
315
+ `;
316
+ }
317
+ function generateLoaderBar(loader, totalTime) {
318
+ const startPercent = loader.start / totalTime * 100;
319
+ const widthPercent = loader.duration / totalTime * 100;
320
+ const sourceIcon = getSourceIcon(loader.source);
321
+ const cacheClass = loader.cacheHit ? "cache-hit" : "";
322
+ const waitingClass = loader.waitingFor.length > 0 ? "had-wait" : "";
323
+ return `
324
+ <div class="loader-row ${cacheClass} ${waitingClass}">
325
+ <span class="loader-name" title="${escapeHtml(loader.key)}">
326
+ ${escapeHtml(loader.key)}
327
+ </span>
328
+ <div class="loader-bar-container">
329
+ ${loader.waitingFor.length > 0 ? `
330
+ <div class="wait-indicator" style="left: 0; width: ${startPercent}%">
331
+ <span class="wait-arrow">\u2192</span>
332
+ </div>
333
+ ` : ""}
334
+ <div class="loader-bar ${getBarClass(loader)}"
335
+ style="left: ${startPercent}%; width: ${Math.max(widthPercent, 0.5)}%"
336
+ title="${loader.key}: ${loader.duration.toFixed(1)}ms${loader.cacheHit ? " (CACHE HIT)" : ""}">
337
+ </div>
338
+ </div>
339
+ <span class="loader-stats">
340
+ <span class="duration">${loader.duration.toFixed(1)}ms</span>
341
+ <span class="source-icon" title="${loader.source}">${sourceIcon}</span>
342
+ ${loader.cacheHit ? '<span class="cache-badge">CACHE</span>' : ""}
343
+ </span>
344
+ </div>
345
+ `;
346
+ }
347
+ function getEfficiencyClass(efficiency) {
348
+ if (efficiency >= 0.8)
349
+ return "excellent";
350
+ if (efficiency >= 0.5)
351
+ return "good";
352
+ if (efficiency >= 0.3)
353
+ return "fair";
354
+ return "poor";
355
+ }
356
+ function getEfficiencyHint(efficiency) {
357
+ if (efficiency >= 0.8)
358
+ return "Excellent! Your loaders are well parallelized.";
359
+ if (efficiency >= 0.5)
360
+ return "Good parallelization. Some room for improvement.";
361
+ if (efficiency >= 0.3)
362
+ return "Fair. Consider reducing dependencies between loaders.";
363
+ return "Poor. Significant waterfall detected. Check dependencies.";
364
+ }
365
+ function getSourceIcon(source) {
366
+ switch (source) {
367
+ case "db":
368
+ return "\uD83D\uDDC4\uFE0F";
369
+ case "api":
370
+ return "\uD83C\uDF10";
371
+ case "cache":
372
+ return "\u26A1";
373
+ case "compute":
374
+ return "\u2699\uFE0F";
375
+ default:
376
+ return "\uD83D\uDCE6";
377
+ }
378
+ }
379
+ function getBarClass(loader) {
380
+ if (loader.cacheHit)
381
+ return "bar-cache";
382
+ switch (loader.source) {
383
+ case "db":
384
+ return "bar-db";
385
+ case "api":
386
+ return "bar-api";
387
+ default:
388
+ return "bar-default";
389
+ }
390
+ }
391
+ function escapeHtml(str) {
392
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
393
+ }
394
+ var DATA_PIPELINE_STYLES = `
395
+ .pipeline-container {
396
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
397
+ background: #0f172a;
398
+ color: #e2e8f0;
399
+ padding: 1.5rem;
400
+ border-radius: 12px;
401
+ }
402
+
403
+ .pipeline-header {
404
+ display: flex;
405
+ justify-content: space-between;
406
+ align-items: center;
407
+ margin-bottom: 1.5rem;
408
+ padding-bottom: 1rem;
409
+ border-bottom: 1px solid #334155;
410
+ }
411
+
412
+ .pipeline-header h3 {
413
+ margin: 0;
414
+ font-size: 1.25rem;
415
+ font-weight: 600;
416
+ color: #f8fafc;
417
+ }
418
+
419
+ .route-info {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 1rem;
423
+ }
424
+
425
+ .route-path {
426
+ font-family: 'Monaco', 'Menlo', monospace;
427
+ font-size: 0.875rem;
428
+ color: #94a3b8;
429
+ background: #1e293b;
430
+ padding: 0.25rem 0.75rem;
431
+ border-radius: 4px;
432
+ }
433
+
434
+ .total-time {
435
+ font-size: 1.5rem;
436
+ font-weight: 700;
437
+ color: #3b82f6;
438
+ }
439
+
440
+ /* Efficiency Meter */
441
+ .efficiency-meter {
442
+ margin-bottom: 1.5rem;
443
+ padding: 1rem;
444
+ background: #1e293b;
445
+ border-radius: 8px;
446
+ }
447
+
448
+ .efficiency-label {
449
+ display: flex;
450
+ justify-content: space-between;
451
+ margin-bottom: 0.5rem;
452
+ font-size: 0.875rem;
453
+ }
454
+
455
+ .efficiency-label .excellent { color: #10b981; }
456
+ .efficiency-label .good { color: #3b82f6; }
457
+ .efficiency-label .fair { color: #f59e0b; }
458
+ .efficiency-label .poor { color: #ef4444; }
459
+
460
+ .efficiency-bar {
461
+ height: 8px;
462
+ background: #334155;
463
+ border-radius: 4px;
464
+ overflow: hidden;
465
+ }
466
+
467
+ .efficiency-fill {
468
+ height: 100%;
469
+ background: linear-gradient(90deg, #10b981, #3b82f6);
470
+ border-radius: 4px;
471
+ transition: width 0.3s ease;
472
+ }
473
+
474
+ .efficiency-hint {
475
+ margin-top: 0.5rem;
476
+ font-size: 0.75rem;
477
+ color: #64748b;
478
+ }
479
+
480
+ /* Timeline */
481
+ .timeline-container {
482
+ margin-bottom: 1.5rem;
483
+ }
484
+
485
+ .timeline-header {
486
+ display: grid;
487
+ grid-template-columns: 150px 1fr 100px;
488
+ gap: 1rem;
489
+ padding: 0.5rem 0;
490
+ font-size: 0.75rem;
491
+ color: #64748b;
492
+ text-transform: uppercase;
493
+ letter-spacing: 0.05em;
494
+ }
495
+
496
+ .timeline-bar-header {
497
+ display: flex;
498
+ justify-content: space-between;
499
+ }
500
+
501
+ .loader-row {
502
+ display: grid;
503
+ grid-template-columns: 150px 1fr 100px;
504
+ gap: 1rem;
505
+ align-items: center;
506
+ padding: 0.5rem 0;
507
+ border-bottom: 1px solid #1e293b;
508
+ }
509
+
510
+ .loader-row:hover {
511
+ background: #1e293b;
512
+ margin: 0 -1rem;
513
+ padding-left: 1rem;
514
+ padding-right: 1rem;
515
+ border-radius: 4px;
516
+ }
517
+
518
+ .loader-name {
519
+ font-family: 'Monaco', 'Menlo', monospace;
520
+ font-size: 0.875rem;
521
+ overflow: hidden;
522
+ text-overflow: ellipsis;
523
+ white-space: nowrap;
524
+ }
525
+
526
+ .loader-bar-container {
527
+ position: relative;
528
+ height: 24px;
529
+ background: #1e293b;
530
+ border-radius: 4px;
531
+ }
532
+
533
+ .wait-indicator {
534
+ position: absolute;
535
+ top: 50%;
536
+ transform: translateY(-50%);
537
+ height: 2px;
538
+ background: repeating-linear-gradient(
539
+ 90deg,
540
+ transparent,
541
+ transparent 4px,
542
+ #475569 4px,
543
+ #475569 8px
544
+ );
545
+ }
546
+
547
+ .wait-arrow {
548
+ position: absolute;
549
+ right: 0;
550
+ top: 50%;
551
+ transform: translateY(-50%);
552
+ color: #64748b;
553
+ font-size: 10px;
554
+ }
555
+
556
+ .loader-bar {
557
+ position: absolute;
558
+ top: 4px;
559
+ height: 16px;
560
+ border-radius: 4px;
561
+ min-width: 2px;
562
+ transition: all 0.2s ease;
563
+ }
564
+
565
+ .loader-bar:hover {
566
+ filter: brightness(1.2);
567
+ }
568
+
569
+ .bar-default { background: linear-gradient(90deg, #6366f1, #8b5cf6); }
570
+ .bar-db { background: linear-gradient(90deg, #3b82f6, #2563eb); }
571
+ .bar-api { background: linear-gradient(90deg, #f59e0b, #d97706); }
572
+ .bar-cache { background: linear-gradient(90deg, #10b981, #059669); }
573
+
574
+ .loader-stats {
575
+ display: flex;
576
+ align-items: center;
577
+ gap: 0.5rem;
578
+ font-size: 0.875rem;
579
+ }
580
+
581
+ .duration {
582
+ font-family: 'Monaco', 'Menlo', monospace;
583
+ color: #94a3b8;
584
+ }
585
+
586
+ .source-icon {
587
+ font-size: 0.875rem;
588
+ }
589
+
590
+ .cache-badge {
591
+ font-size: 0.625rem;
592
+ padding: 0.125rem 0.375rem;
593
+ background: #059669;
594
+ color: white;
595
+ border-radius: 2px;
596
+ font-weight: 600;
597
+ }
598
+
599
+ .cache-hit .loader-name {
600
+ color: #10b981;
601
+ }
602
+
603
+ /* Waterfall Warnings */
604
+ .waterfall-section {
605
+ margin-bottom: 1rem;
606
+ }
607
+
608
+ .waterfall-section h4 {
609
+ margin: 0 0 0.75rem;
610
+ font-size: 0.875rem;
611
+ color: #f59e0b;
612
+ }
613
+
614
+ .waterfall-warning {
615
+ display: flex;
616
+ align-items: flex-start;
617
+ gap: 0.75rem;
618
+ padding: 0.75rem;
619
+ background: #1e293b;
620
+ border-radius: 8px;
621
+ margin-bottom: 0.5rem;
622
+ border-left: 3px solid #f59e0b;
623
+ }
624
+
625
+ .waterfall-warning.unnecessary {
626
+ border-left-color: #10b981;
627
+ }
628
+
629
+ .waterfall-warning .icon {
630
+ font-size: 1rem;
631
+ }
632
+
633
+ .waterfall-warning .message {
634
+ font-size: 0.875rem;
635
+ line-height: 1.4;
636
+ color: #cbd5e1;
637
+ }
638
+
639
+ .all-good {
640
+ padding: 1rem;
641
+ background: rgba(16, 185, 129, 0.1);
642
+ border: 1px solid rgba(16, 185, 129, 0.2);
643
+ border-radius: 8px;
644
+ text-align: center;
645
+ color: #10b981;
646
+ font-size: 0.875rem;
647
+ }
648
+
649
+ .pipeline-footer {
650
+ text-align: right;
651
+ font-size: 0.75rem;
652
+ color: #475569;
653
+ margin-top: 1rem;
654
+ padding-top: 1rem;
655
+ border-top: 1px solid #1e293b;
656
+ }
657
+ `;
658
+ function DataPipelineTab({ data }) {
659
+ return null;
660
+ }
661
+ // src/devtools/RoutesTab.tsx
662
+ function generateRoutesTabHTML(routes) {
663
+ const stats = calculateRouteStats(routes);
664
+ const routeRows = routes.sort((a, b) => a.path.localeCompare(b.path)).map((route) => generateRouteRow(route)).join(`
665
+ `);
666
+ return `
667
+ <div class="routes-container">
668
+ <div class="routes-header">
669
+ <h3>Routes</h3>
670
+ <div class="route-stats">
671
+ <span class="stat">
672
+ <span class="stat-value">${stats.total}</span>
673
+ <span class="stat-label">Total</span>
674
+ </span>
675
+ <span class="stat">
676
+ <span class="stat-value">${stats.ssr}</span>
677
+ <span class="stat-label">SSR</span>
678
+ </span>
679
+ <span class="stat">
680
+ <span class="stat-value">${stats.ssg}</span>
681
+ <span class="stat-label">SSG</span>
682
+ </span>
683
+ <span class="stat">
684
+ <span class="stat-value">${stats.api}</span>
685
+ <span class="stat-label">API</span>
686
+ </span>
687
+ </div>
688
+ </div>
689
+
690
+ <div class="routes-filter">
691
+ <input type="text"
692
+ id="route-search"
693
+ placeholder="Search routes..."
694
+ class="search-input">
695
+ <div class="filter-buttons">
696
+ <button class="filter-btn active" data-filter="all">All</button>
697
+ <button class="filter-btn" data-filter="ssr">SSR</button>
698
+ <button class="filter-btn" data-filter="ssg">SSG</button>
699
+ <button class="filter-btn" data-filter="api">API</button>
700
+ </div>
701
+ </div>
702
+
703
+ <div class="routes-table">
704
+ <div class="routes-table-header">
705
+ <span>Path</span>
706
+ <span>Mode</span>
707
+ <span>Features</span>
708
+ <span>Timing</span>
709
+ </div>
710
+ <div class="routes-table-body">
711
+ ${routeRows}
712
+ </div>
713
+ </div>
714
+ </div>
715
+
716
+ <script>
717
+ (function() {
718
+ const searchInput = document.getElementById('route-search');
719
+ const rows = document.querySelectorAll('.route-row');
720
+ const filterBtns = document.querySelectorAll('.filter-btn');
721
+
722
+ searchInput.addEventListener('input', filterRoutes);
723
+ filterBtns.forEach(btn => btn.addEventListener('click', handleFilterClick));
724
+
725
+ function filterRoutes() {
726
+ const query = searchInput.value.toLowerCase();
727
+ const activeFilter = document.querySelector('.filter-btn.active').dataset.filter;
728
+
729
+ rows.forEach(row => {
730
+ const path = row.dataset.path.toLowerCase();
731
+ const mode = row.dataset.mode;
732
+ const matchesSearch = path.includes(query);
733
+ const matchesFilter = activeFilter === 'all' || mode === activeFilter;
734
+
735
+ row.style.display = matchesSearch && matchesFilter ? 'grid' : 'none';
736
+ });
737
+ }
738
+
739
+ function handleFilterClick(e) {
740
+ filterBtns.forEach(btn => btn.classList.remove('active'));
741
+ e.target.classList.add('active');
742
+ filterRoutes();
743
+ }
744
+ })();
745
+ </script>
746
+ `;
747
+ }
748
+ function generateRouteRow(route) {
749
+ const modeClass = `mode-${route.renderMode}`;
750
+ const features = [];
751
+ if (route.hasLoader)
752
+ features.push("loader");
753
+ if (route.hasAction)
754
+ features.push("action");
755
+ if (route.islandCount > 0)
756
+ features.push(`${route.islandCount} islands`);
757
+ if (route.authRequired)
758
+ features.push("auth");
759
+ if (route.middleware.length > 0)
760
+ features.push(`${route.middleware.length} middleware`);
761
+ return `
762
+ <div class="route-row"
763
+ data-path="${escapeHtml2(route.path)}"
764
+ data-mode="${route.renderMode}">
765
+ <div class="route-path-cell">
766
+ <span class="route-path">${escapeHtml2(route.path)}</span>
767
+ <span class="route-file">${escapeHtml2(route.file)}</span>
768
+ </div>
769
+ <div class="route-mode-cell">
770
+ <span class="mode-badge ${modeClass}">${route.renderMode.toUpperCase()}</span>
771
+ </div>
772
+ <div class="route-features-cell">
773
+ ${features.map((f) => `<span class="feature-tag">${f}</span>`).join("")}
774
+ </div>
775
+ <div class="route-timing-cell">
776
+ ${route.lastTiming ? `<span class="timing">${route.lastTiming.toFixed(0)}ms</span>` : '<span class="timing-na">-</span>'}
777
+ </div>
778
+ </div>
779
+ `;
780
+ }
781
+ function calculateRouteStats(routes) {
782
+ return {
783
+ total: routes.length,
784
+ ssr: routes.filter((r) => r.renderMode === "ssr").length,
785
+ ssg: routes.filter((r) => r.renderMode === "ssg").length,
786
+ api: routes.filter((r) => r.renderMode === "api").length,
787
+ csr: routes.filter((r) => r.renderMode === "csr").length,
788
+ rsc: routes.filter((r) => r.renderMode === "rsc").length
789
+ };
790
+ }
791
+ function escapeHtml2(str) {
792
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
793
+ }
794
+ var ROUTES_TAB_STYLES = `
795
+ .routes-container {
796
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
797
+ background: #0f172a;
798
+ color: #e2e8f0;
799
+ padding: 1.5rem;
800
+ border-radius: 12px;
801
+ }
802
+
803
+ .routes-header {
804
+ display: flex;
805
+ justify-content: space-between;
806
+ align-items: center;
807
+ margin-bottom: 1.5rem;
808
+ padding-bottom: 1rem;
809
+ border-bottom: 1px solid #334155;
810
+ }
811
+
812
+ .routes-header h3 {
813
+ margin: 0;
814
+ font-size: 1.25rem;
815
+ font-weight: 600;
816
+ }
817
+
818
+ .route-stats {
819
+ display: flex;
820
+ gap: 1.5rem;
821
+ }
822
+
823
+ .stat {
824
+ display: flex;
825
+ flex-direction: column;
826
+ align-items: center;
827
+ }
828
+
829
+ .stat-value {
830
+ font-size: 1.5rem;
831
+ font-weight: 700;
832
+ color: #3b82f6;
833
+ }
834
+
835
+ .stat-label {
836
+ font-size: 0.75rem;
837
+ color: #64748b;
838
+ text-transform: uppercase;
839
+ }
840
+
841
+ .routes-filter {
842
+ display: flex;
843
+ gap: 1rem;
844
+ margin-bottom: 1rem;
845
+ }
846
+
847
+ .search-input {
848
+ flex: 1;
849
+ padding: 0.5rem 1rem;
850
+ background: #1e293b;
851
+ border: 1px solid #334155;
852
+ border-radius: 6px;
853
+ color: #e2e8f0;
854
+ font-size: 0.875rem;
855
+ }
856
+
857
+ .search-input:focus {
858
+ outline: none;
859
+ border-color: #3b82f6;
860
+ }
861
+
862
+ .filter-buttons {
863
+ display: flex;
864
+ gap: 0.5rem;
865
+ }
866
+
867
+ .filter-btn {
868
+ padding: 0.5rem 1rem;
869
+ background: #1e293b;
870
+ border: 1px solid #334155;
871
+ border-radius: 6px;
872
+ color: #94a3b8;
873
+ font-size: 0.75rem;
874
+ cursor: pointer;
875
+ transition: all 0.2s;
876
+ }
877
+
878
+ .filter-btn:hover {
879
+ border-color: #3b82f6;
880
+ }
881
+
882
+ .filter-btn.active {
883
+ background: #3b82f6;
884
+ border-color: #3b82f6;
885
+ color: white;
886
+ }
887
+
888
+ .routes-table {
889
+ background: #1e293b;
890
+ border-radius: 8px;
891
+ overflow: hidden;
892
+ }
893
+
894
+ .routes-table-header {
895
+ display: grid;
896
+ grid-template-columns: 2fr 100px 1fr 80px;
897
+ gap: 1rem;
898
+ padding: 0.75rem 1rem;
899
+ background: #0f172a;
900
+ font-size: 0.75rem;
901
+ color: #64748b;
902
+ text-transform: uppercase;
903
+ letter-spacing: 0.05em;
904
+ }
905
+
906
+ .route-row {
907
+ display: grid;
908
+ grid-template-columns: 2fr 100px 1fr 80px;
909
+ gap: 1rem;
910
+ padding: 0.75rem 1rem;
911
+ border-bottom: 1px solid #0f172a;
912
+ transition: background 0.2s;
913
+ }
914
+
915
+ .route-row:hover {
916
+ background: #334155;
917
+ }
918
+
919
+ .route-path-cell {
920
+ display: flex;
921
+ flex-direction: column;
922
+ gap: 0.25rem;
923
+ }
924
+
925
+ .route-path {
926
+ font-family: 'Monaco', 'Menlo', monospace;
927
+ font-size: 0.875rem;
928
+ color: #f8fafc;
929
+ }
930
+
931
+ .route-file {
932
+ font-size: 0.75rem;
933
+ color: #64748b;
934
+ overflow: hidden;
935
+ text-overflow: ellipsis;
936
+ white-space: nowrap;
937
+ }
938
+
939
+ .mode-badge {
940
+ display: inline-block;
941
+ padding: 0.25rem 0.5rem;
942
+ border-radius: 4px;
943
+ font-size: 0.625rem;
944
+ font-weight: 600;
945
+ }
946
+
947
+ .mode-ssr { background: #3b82f6; color: white; }
948
+ .mode-ssg { background: #10b981; color: white; }
949
+ .mode-csr { background: #f59e0b; color: white; }
950
+ .mode-api { background: #8b5cf6; color: white; }
951
+ .mode-rsc { background: #ec4899; color: white; }
952
+
953
+ .route-features-cell {
954
+ display: flex;
955
+ flex-wrap: wrap;
956
+ gap: 0.25rem;
957
+ }
958
+
959
+ .feature-tag {
960
+ padding: 0.125rem 0.375rem;
961
+ background: #334155;
962
+ border-radius: 4px;
963
+ font-size: 0.625rem;
964
+ color: #94a3b8;
965
+ }
966
+
967
+ .route-timing-cell {
968
+ text-align: right;
969
+ }
970
+
971
+ .timing {
972
+ font-family: 'Monaco', 'Menlo', monospace;
973
+ font-size: 0.875rem;
974
+ color: #10b981;
975
+ }
976
+
977
+ .timing-na {
978
+ color: #475569;
979
+ }
980
+ `;
981
+ function RoutesTab({ routes }) {
982
+ return null;
983
+ }
984
+ // src/devtools/IslandsTab.tsx
985
+ function generateIslandsTabHTML(islands) {
986
+ const stats = calculateIslandStats(islands);
987
+ const islandRows = islands.sort((a, b) => a.component.localeCompare(b.component)).map((island) => generateIslandRow(island)).join(`
988
+ `);
989
+ return `
990
+ <div class="islands-container">
991
+ <div class="islands-header">
992
+ <h3>Islands</h3>
993
+ <div class="islands-stats">
994
+ <span class="stat">
995
+ <span class="stat-value">${stats.total}</span>
996
+ <span class="stat-label">Total</span>
997
+ </span>
998
+ <span class="stat">
999
+ <span class="stat-value">${stats.hydrated}</span>
1000
+ <span class="stat-label">Hydrated</span>
1001
+ </span>
1002
+ <span class="stat">
1003
+ <span class="stat-value">${stats.pending}</span>
1004
+ <span class="stat-label">Pending</span>
1005
+ </span>
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <div class="hydration-strategies">
1010
+ <div class="strategy-card">
1011
+ <div class="strategy-icon">\u26A1</div>
1012
+ <div class="strategy-info">
1013
+ <span class="strategy-name">load</span>
1014
+ <span class="strategy-count">${stats.byStrategy.load || 0}</span>
1015
+ </div>
1016
+ </div>
1017
+ <div class="strategy-card">
1018
+ <div class="strategy-icon">\uD83D\uDE34</div>
1019
+ <div class="strategy-info">
1020
+ <span class="strategy-name">idle</span>
1021
+ <span class="strategy-count">${stats.byStrategy.idle || 0}</span>
1022
+ </div>
1023
+ </div>
1024
+ <div class="strategy-card">
1025
+ <div class="strategy-icon">\uD83D\uDC41\uFE0F</div>
1026
+ <div class="strategy-info">
1027
+ <span class="strategy-name">visible</span>
1028
+ <span class="strategy-count">${stats.byStrategy.visible || 0}</span>
1029
+ </div>
1030
+ </div>
1031
+ <div class="strategy-card">
1032
+ <div class="strategy-icon">\uD83D\uDCF1</div>
1033
+ <div class="strategy-info">
1034
+ <span class="strategy-name">media</span>
1035
+ <span class="strategy-count">${stats.byStrategy.media || 0}</span>
1036
+ </div>
1037
+ </div>
1038
+ </div>
1039
+
1040
+ ${islands.length > 0 ? `
1041
+ <div class="islands-list">
1042
+ ${islandRows}
1043
+ </div>
1044
+ ` : `
1045
+ <div class="no-islands">
1046
+ <span class="no-islands-icon">\uD83C\uDFDD\uFE0F</span>
1047
+ <span class="no-islands-text">No islands on this page</span>
1048
+ </div>
1049
+ `}
1050
+
1051
+ <div class="islands-actions">
1052
+ <button class="action-btn" onclick="window.__EREO_DEVTOOLS__.highlightIslands()">
1053
+ Highlight Islands
1054
+ </button>
1055
+ <button class="action-btn" onclick="window.__EREO_DEVTOOLS__.hydrateAll()">
1056
+ Force Hydrate All
1057
+ </button>
1058
+ </div>
1059
+ </div>
1060
+ `;
1061
+ }
1062
+ function generateIslandRow(island) {
1063
+ const statusClass = island.hydrated ? "hydrated" : "pending";
1064
+ const statusIcon = island.hydrated ? "\u2705" : "\u23F3";
1065
+ return `
1066
+ <div class="island-row ${statusClass}" data-island-id="${escapeHtml3(island.id)}">
1067
+ <div class="island-main">
1068
+ <div class="island-status">${statusIcon}</div>
1069
+ <div class="island-info">
1070
+ <span class="island-component">${escapeHtml3(island.component)}</span>
1071
+ <span class="island-selector">${escapeHtml3(island.selector)}</span>
1072
+ </div>
1073
+ </div>
1074
+ <div class="island-strategy">
1075
+ <span class="strategy-badge strategy-${island.strategy}">
1076
+ ${getStrategyIcon(island.strategy)} ${island.strategy}
1077
+ </span>
1078
+ ${island.mediaQuery ? `<span class="media-query">${escapeHtml3(island.mediaQuery)}</span>` : ""}
1079
+ </div>
1080
+ <div class="island-metrics">
1081
+ ${island.hydrationTime !== undefined ? `
1082
+ <span class="hydration-time">${island.hydrationTime.toFixed(0)}ms</span>
1083
+ ` : ""}
1084
+ <span class="props-size">${formatBytes(island.propsSize)}</span>
1085
+ </div>
1086
+ <div class="island-actions">
1087
+ <button class="island-action" onclick="window.__EREO_DEVTOOLS__.inspectIsland('${island.id}')" title="Inspect">
1088
+ \uD83D\uDD0D
1089
+ </button>
1090
+ <button class="island-action" onclick="window.__EREO_DEVTOOLS__.scrollToIsland('${island.id}')" title="Scroll to">
1091
+ \uD83D\uDCCD
1092
+ </button>
1093
+ </div>
1094
+ </div>
1095
+ `;
1096
+ }
1097
+ function getStrategyIcon(strategy) {
1098
+ switch (strategy) {
1099
+ case "load":
1100
+ return "\u26A1";
1101
+ case "idle":
1102
+ return "\uD83D\uDE34";
1103
+ case "visible":
1104
+ return "\uD83D\uDC41\uFE0F";
1105
+ case "media":
1106
+ return "\uD83D\uDCF1";
1107
+ case "none":
1108
+ return "\uD83D\uDEAB";
1109
+ default:
1110
+ return "\u2753";
1111
+ }
1112
+ }
1113
+ function formatBytes(bytes) {
1114
+ if (bytes < 1024)
1115
+ return `${bytes}B`;
1116
+ if (bytes < 1024 * 1024)
1117
+ return `${(bytes / 1024).toFixed(1)}KB`;
1118
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1119
+ }
1120
+ function calculateIslandStats(islands) {
1121
+ const byStrategy = {};
1122
+ for (const island of islands) {
1123
+ byStrategy[island.strategy] = (byStrategy[island.strategy] || 0) + 1;
1124
+ }
1125
+ return {
1126
+ total: islands.length,
1127
+ hydrated: islands.filter((i) => i.hydrated).length,
1128
+ pending: islands.filter((i) => !i.hydrated).length,
1129
+ byStrategy
1130
+ };
1131
+ }
1132
+ function escapeHtml3(str) {
1133
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1134
+ }
1135
+ var ISLANDS_TAB_STYLES = `
1136
+ .islands-container {
1137
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1138
+ background: #0f172a;
1139
+ color: #e2e8f0;
1140
+ padding: 1.5rem;
1141
+ border-radius: 12px;
1142
+ }
1143
+
1144
+ .islands-header {
1145
+ display: flex;
1146
+ justify-content: space-between;
1147
+ align-items: center;
1148
+ margin-bottom: 1.5rem;
1149
+ padding-bottom: 1rem;
1150
+ border-bottom: 1px solid #334155;
1151
+ }
1152
+
1153
+ .islands-header h3 {
1154
+ margin: 0;
1155
+ font-size: 1.25rem;
1156
+ font-weight: 600;
1157
+ }
1158
+
1159
+ .islands-stats {
1160
+ display: flex;
1161
+ gap: 1.5rem;
1162
+ }
1163
+
1164
+ .stat {
1165
+ display: flex;
1166
+ flex-direction: column;
1167
+ align-items: center;
1168
+ }
1169
+
1170
+ .stat-value {
1171
+ font-size: 1.5rem;
1172
+ font-weight: 700;
1173
+ color: #3b82f6;
1174
+ }
1175
+
1176
+ .stat-label {
1177
+ font-size: 0.75rem;
1178
+ color: #64748b;
1179
+ text-transform: uppercase;
1180
+ }
1181
+
1182
+ .hydration-strategies {
1183
+ display: grid;
1184
+ grid-template-columns: repeat(4, 1fr);
1185
+ gap: 1rem;
1186
+ margin-bottom: 1.5rem;
1187
+ }
1188
+
1189
+ .strategy-card {
1190
+ display: flex;
1191
+ align-items: center;
1192
+ gap: 0.75rem;
1193
+ padding: 1rem;
1194
+ background: #1e293b;
1195
+ border-radius: 8px;
1196
+ }
1197
+
1198
+ .strategy-icon {
1199
+ font-size: 1.5rem;
1200
+ }
1201
+
1202
+ .strategy-info {
1203
+ display: flex;
1204
+ flex-direction: column;
1205
+ }
1206
+
1207
+ .strategy-name {
1208
+ font-size: 0.875rem;
1209
+ color: #94a3b8;
1210
+ }
1211
+
1212
+ .strategy-count {
1213
+ font-size: 1.25rem;
1214
+ font-weight: 700;
1215
+ color: #f8fafc;
1216
+ }
1217
+
1218
+ .islands-list {
1219
+ background: #1e293b;
1220
+ border-radius: 8px;
1221
+ overflow: hidden;
1222
+ margin-bottom: 1rem;
1223
+ }
1224
+
1225
+ .island-row {
1226
+ display: grid;
1227
+ grid-template-columns: 1fr 150px 100px 70px;
1228
+ gap: 1rem;
1229
+ padding: 0.75rem 1rem;
1230
+ border-bottom: 1px solid #0f172a;
1231
+ align-items: center;
1232
+ }
1233
+
1234
+ .island-row:hover {
1235
+ background: #334155;
1236
+ }
1237
+
1238
+ .island-row.pending {
1239
+ opacity: 0.7;
1240
+ }
1241
+
1242
+ .island-main {
1243
+ display: flex;
1244
+ align-items: center;
1245
+ gap: 0.75rem;
1246
+ }
1247
+
1248
+ .island-status {
1249
+ font-size: 1rem;
1250
+ }
1251
+
1252
+ .island-info {
1253
+ display: flex;
1254
+ flex-direction: column;
1255
+ gap: 0.25rem;
1256
+ }
1257
+
1258
+ .island-component {
1259
+ font-family: 'Monaco', 'Menlo', monospace;
1260
+ font-size: 0.875rem;
1261
+ color: #f8fafc;
1262
+ }
1263
+
1264
+ .island-selector {
1265
+ font-size: 0.75rem;
1266
+ color: #64748b;
1267
+ }
1268
+
1269
+ .strategy-badge {
1270
+ display: inline-flex;
1271
+ align-items: center;
1272
+ gap: 0.25rem;
1273
+ padding: 0.25rem 0.5rem;
1274
+ border-radius: 4px;
1275
+ font-size: 0.75rem;
1276
+ font-weight: 500;
1277
+ }
1278
+
1279
+ .strategy-load { background: #3b82f6; color: white; }
1280
+ .strategy-idle { background: #6366f1; color: white; }
1281
+ .strategy-visible { background: #10b981; color: white; }
1282
+ .strategy-media { background: #f59e0b; color: white; }
1283
+ .strategy-none { background: #64748b; color: white; }
1284
+
1285
+ .media-query {
1286
+ font-size: 0.625rem;
1287
+ color: #94a3b8;
1288
+ display: block;
1289
+ margin-top: 0.25rem;
1290
+ }
1291
+
1292
+ .island-metrics {
1293
+ display: flex;
1294
+ flex-direction: column;
1295
+ gap: 0.25rem;
1296
+ text-align: right;
1297
+ }
1298
+
1299
+ .hydration-time {
1300
+ font-family: 'Monaco', 'Menlo', monospace;
1301
+ font-size: 0.875rem;
1302
+ color: #10b981;
1303
+ }
1304
+
1305
+ .props-size {
1306
+ font-size: 0.75rem;
1307
+ color: #64748b;
1308
+ }
1309
+
1310
+ .island-actions {
1311
+ display: flex;
1312
+ gap: 0.25rem;
1313
+ }
1314
+
1315
+ .island-action {
1316
+ padding: 0.25rem;
1317
+ background: transparent;
1318
+ border: none;
1319
+ cursor: pointer;
1320
+ font-size: 1rem;
1321
+ border-radius: 4px;
1322
+ transition: background 0.2s;
1323
+ }
1324
+
1325
+ .island-action:hover {
1326
+ background: #334155;
1327
+ }
1328
+
1329
+ .no-islands {
1330
+ display: flex;
1331
+ flex-direction: column;
1332
+ align-items: center;
1333
+ padding: 3rem;
1334
+ color: #64748b;
1335
+ }
1336
+
1337
+ .no-islands-icon {
1338
+ font-size: 3rem;
1339
+ margin-bottom: 1rem;
1340
+ }
1341
+
1342
+ .islands-actions {
1343
+ display: flex;
1344
+ gap: 0.5rem;
1345
+ justify-content: flex-end;
1346
+ }
1347
+
1348
+ .action-btn {
1349
+ padding: 0.5rem 1rem;
1350
+ background: #3b82f6;
1351
+ border: none;
1352
+ border-radius: 6px;
1353
+ color: white;
1354
+ font-size: 0.875rem;
1355
+ cursor: pointer;
1356
+ transition: background 0.2s;
1357
+ }
1358
+
1359
+ .action-btn:hover {
1360
+ background: #2563eb;
1361
+ }
1362
+ `;
1363
+ function IslandsTab({ islands }) {
1364
+ return null;
1365
+ }
1366
+ // src/devtools/CacheTab.tsx
1367
+ function generateCacheTabHTML(cache) {
1368
+ const { entries, totalSize, hitRate, tagStats } = cache;
1369
+ const entryRows = entries.sort((a, b) => b.lastAccessed - a.lastAccessed).slice(0, 50).map((entry) => generateEntryRow(entry)).join(`
1370
+ `);
1371
+ const tagRows = Array.from(tagStats.entries()).sort((a, b) => b[1].count - a[1].count).map(([tag, stats]) => generateTagRow(tag, stats)).join(`
1372
+ `);
1373
+ return `
1374
+ <div class="cache-container">
1375
+ <div class="cache-header">
1376
+ <h3>Cache</h3>
1377
+ <div class="cache-stats">
1378
+ <span class="stat">
1379
+ <span class="stat-value">${entries.length}</span>
1380
+ <span class="stat-label">Entries</span>
1381
+ </span>
1382
+ <span class="stat">
1383
+ <span class="stat-value">${formatBytes2(totalSize)}</span>
1384
+ <span class="stat-label">Size</span>
1385
+ </span>
1386
+ <span class="stat">
1387
+ <span class="stat-value hit-rate ${getHitRateClass(hitRate)}">${(hitRate * 100).toFixed(0)}%</span>
1388
+ <span class="stat-label">Hit Rate</span>
1389
+ </span>
1390
+ </div>
1391
+ </div>
1392
+
1393
+ <div class="cache-tabs">
1394
+ <button class="cache-tab active" data-tab="entries">Entries</button>
1395
+ <button class="cache-tab" data-tab="tags">Tags</button>
1396
+ </div>
1397
+
1398
+ <div class="cache-content">
1399
+ <div class="tab-content active" id="entries-tab">
1400
+ ${entries.length > 0 ? `
1401
+ <div class="cache-table">
1402
+ <div class="cache-table-header">
1403
+ <span>Key</span>
1404
+ <span>Tags</span>
1405
+ <span>Size</span>
1406
+ <span>TTL</span>
1407
+ <span>Hits</span>
1408
+ </div>
1409
+ <div class="cache-table-body">
1410
+ ${entryRows}
1411
+ </div>
1412
+ </div>
1413
+ ` : `
1414
+ <div class="no-entries">
1415
+ <span class="no-entries-icon">\uD83D\uDCBE</span>
1416
+ <span class="no-entries-text">Cache is empty</span>
1417
+ </div>
1418
+ `}
1419
+ </div>
1420
+
1421
+ <div class="tab-content" id="tags-tab" style="display: none;">
1422
+ ${tagStats.size > 0 ? `
1423
+ <div class="tags-table">
1424
+ <div class="tags-table-header">
1425
+ <span>Tag</span>
1426
+ <span>Entries</span>
1427
+ <span>Hits</span>
1428
+ <span>Misses</span>
1429
+ <span>Hit Rate</span>
1430
+ </div>
1431
+ <div class="tags-table-body">
1432
+ ${tagRows}
1433
+ </div>
1434
+ </div>
1435
+ ` : `
1436
+ <div class="no-entries">
1437
+ <span class="no-entries-icon">\uD83C\uDFF7\uFE0F</span>
1438
+ <span class="no-entries-text">No cache tags</span>
1439
+ </div>
1440
+ `}
1441
+ </div>
1442
+ </div>
1443
+
1444
+ <div class="cache-actions">
1445
+ <button class="action-btn danger" onclick="window.__EREO_DEVTOOLS__.clearCache()">
1446
+ Clear All Cache
1447
+ </button>
1448
+ <button class="action-btn" onclick="window.__EREO_DEVTOOLS__.refreshCache()">
1449
+ Refresh
1450
+ </button>
1451
+ </div>
1452
+ </div>
1453
+
1454
+ <script>
1455
+ (function() {
1456
+ const tabs = document.querySelectorAll('.cache-tab');
1457
+ const contents = document.querySelectorAll('.tab-content');
1458
+
1459
+ tabs.forEach(tab => {
1460
+ tab.addEventListener('click', () => {
1461
+ tabs.forEach(t => t.classList.remove('active'));
1462
+ contents.forEach(c => c.style.display = 'none');
1463
+
1464
+ tab.classList.add('active');
1465
+ const tabId = tab.dataset.tab + '-tab';
1466
+ document.getElementById(tabId).style.display = 'block';
1467
+ });
1468
+ });
1469
+ })();
1470
+ </script>
1471
+ `;
1472
+ }
1473
+ function generateEntryRow(entry) {
1474
+ const ttlClass = getTTLClass(entry.ttl);
1475
+ const age = Date.now() - entry.created;
1476
+ return `
1477
+ <div class="cache-entry-row">
1478
+ <div class="entry-key" title="${escapeHtml4(entry.key)}">
1479
+ ${escapeHtml4(truncateKey(entry.key))}
1480
+ </div>
1481
+ <div class="entry-tags">
1482
+ ${entry.tags.map((tag) => `<span class="tag">${escapeHtml4(tag)}</span>`).join("")}
1483
+ </div>
1484
+ <div class="entry-size">
1485
+ ${formatBytes2(entry.size)}
1486
+ </div>
1487
+ <div class="entry-ttl ${ttlClass}">
1488
+ ${formatTTL(entry.ttl)}
1489
+ </div>
1490
+ <div class="entry-hits">
1491
+ ${entry.accessCount}
1492
+ </div>
1493
+ <div class="entry-actions">
1494
+ <button class="entry-action" onclick="window.__EREO_DEVTOOLS__.invalidateKey('${escapeHtml4(entry.key)}')" title="Invalidate">
1495
+ \uD83D\uDDD1\uFE0F
1496
+ </button>
1497
+ <button class="entry-action" onclick="window.__EREO_DEVTOOLS__.inspectEntry('${escapeHtml4(entry.key)}')" title="Inspect">
1498
+ \uD83D\uDD0D
1499
+ </button>
1500
+ </div>
1501
+ </div>
1502
+ `;
1503
+ }
1504
+ function generateTagRow(tag, stats) {
1505
+ const tagHitRate = stats.hits + stats.misses > 0 ? stats.hits / (stats.hits + stats.misses) : 0;
1506
+ return `
1507
+ <div class="tag-row">
1508
+ <div class="tag-name">
1509
+ <span class="tag-badge">${escapeHtml4(tag)}</span>
1510
+ </div>
1511
+ <div class="tag-count">${stats.count}</div>
1512
+ <div class="tag-hits">${stats.hits}</div>
1513
+ <div class="tag-misses">${stats.misses}</div>
1514
+ <div class="tag-hit-rate ${getHitRateClass(tagHitRate)}">
1515
+ ${(tagHitRate * 100).toFixed(0)}%
1516
+ </div>
1517
+ <div class="tag-actions">
1518
+ <button class="tag-action" onclick="window.__EREO_DEVTOOLS__.invalidateTag('${escapeHtml4(tag)}')" title="Invalidate tag">
1519
+ \uD83D\uDDD1\uFE0F
1520
+ </button>
1521
+ </div>
1522
+ </div>
1523
+ `;
1524
+ }
1525
+ function formatBytes2(bytes) {
1526
+ if (bytes < 1024)
1527
+ return `${bytes}B`;
1528
+ if (bytes < 1024 * 1024)
1529
+ return `${(bytes / 1024).toFixed(1)}KB`;
1530
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1531
+ }
1532
+ function formatTTL(ms) {
1533
+ if (ms < 0)
1534
+ return "Expired";
1535
+ if (ms < 1000)
1536
+ return `${ms}ms`;
1537
+ if (ms < 60000)
1538
+ return `${(ms / 1000).toFixed(0)}s`;
1539
+ if (ms < 3600000)
1540
+ return `${(ms / 60000).toFixed(0)}m`;
1541
+ return `${(ms / 3600000).toFixed(1)}h`;
1542
+ }
1543
+ function getTTLClass(ttl) {
1544
+ if (ttl < 0)
1545
+ return "expired";
1546
+ if (ttl < 60000)
1547
+ return "expiring-soon";
1548
+ return "healthy";
1549
+ }
1550
+ function getHitRateClass(rate) {
1551
+ if (rate >= 0.8)
1552
+ return "excellent";
1553
+ if (rate >= 0.5)
1554
+ return "good";
1555
+ if (rate >= 0.2)
1556
+ return "fair";
1557
+ return "poor";
1558
+ }
1559
+ function truncateKey(key, maxLength = 50) {
1560
+ if (key.length <= maxLength)
1561
+ return key;
1562
+ return key.slice(0, maxLength - 3) + "...";
1563
+ }
1564
+ function escapeHtml4(str) {
1565
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1566
+ }
1567
+ var CACHE_TAB_STYLES = `
1568
+ .cache-container {
1569
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1570
+ background: #0f172a;
1571
+ color: #e2e8f0;
1572
+ padding: 1.5rem;
1573
+ border-radius: 12px;
1574
+ }
1575
+
1576
+ .cache-header {
1577
+ display: flex;
1578
+ justify-content: space-between;
1579
+ align-items: center;
1580
+ margin-bottom: 1.5rem;
1581
+ padding-bottom: 1rem;
1582
+ border-bottom: 1px solid #334155;
1583
+ }
1584
+
1585
+ .cache-header h3 {
1586
+ margin: 0;
1587
+ font-size: 1.25rem;
1588
+ font-weight: 600;
1589
+ }
1590
+
1591
+ .cache-stats {
1592
+ display: flex;
1593
+ gap: 1.5rem;
1594
+ }
1595
+
1596
+ .stat {
1597
+ display: flex;
1598
+ flex-direction: column;
1599
+ align-items: center;
1600
+ }
1601
+
1602
+ .stat-value {
1603
+ font-size: 1.5rem;
1604
+ font-weight: 700;
1605
+ color: #3b82f6;
1606
+ }
1607
+
1608
+ .stat-label {
1609
+ font-size: 0.75rem;
1610
+ color: #64748b;
1611
+ text-transform: uppercase;
1612
+ }
1613
+
1614
+ .hit-rate.excellent { color: #10b981; }
1615
+ .hit-rate.good { color: #3b82f6; }
1616
+ .hit-rate.fair { color: #f59e0b; }
1617
+ .hit-rate.poor { color: #ef4444; }
1618
+
1619
+ .cache-tabs {
1620
+ display: flex;
1621
+ gap: 0.5rem;
1622
+ margin-bottom: 1rem;
1623
+ }
1624
+
1625
+ .cache-tab {
1626
+ padding: 0.5rem 1rem;
1627
+ background: #1e293b;
1628
+ border: none;
1629
+ border-radius: 6px;
1630
+ color: #94a3b8;
1631
+ font-size: 0.875rem;
1632
+ cursor: pointer;
1633
+ transition: all 0.2s;
1634
+ }
1635
+
1636
+ .cache-tab:hover {
1637
+ color: #f8fafc;
1638
+ }
1639
+
1640
+ .cache-tab.active {
1641
+ background: #3b82f6;
1642
+ color: white;
1643
+ }
1644
+
1645
+ .cache-table,
1646
+ .tags-table {
1647
+ background: #1e293b;
1648
+ border-radius: 8px;
1649
+ overflow: hidden;
1650
+ }
1651
+
1652
+ .cache-table-header,
1653
+ .tags-table-header {
1654
+ display: grid;
1655
+ grid-template-columns: 2fr 1fr 80px 80px 60px 70px;
1656
+ gap: 1rem;
1657
+ padding: 0.75rem 1rem;
1658
+ background: #0f172a;
1659
+ font-size: 0.75rem;
1660
+ color: #64748b;
1661
+ text-transform: uppercase;
1662
+ letter-spacing: 0.05em;
1663
+ }
1664
+
1665
+ .tags-table-header {
1666
+ grid-template-columns: 1fr 80px 80px 80px 80px 50px;
1667
+ }
1668
+
1669
+ .cache-entry-row,
1670
+ .tag-row {
1671
+ display: grid;
1672
+ grid-template-columns: 2fr 1fr 80px 80px 60px 70px;
1673
+ gap: 1rem;
1674
+ padding: 0.75rem 1rem;
1675
+ border-bottom: 1px solid #0f172a;
1676
+ align-items: center;
1677
+ font-size: 0.875rem;
1678
+ }
1679
+
1680
+ .tag-row {
1681
+ grid-template-columns: 1fr 80px 80px 80px 80px 50px;
1682
+ }
1683
+
1684
+ .cache-entry-row:hover,
1685
+ .tag-row:hover {
1686
+ background: #334155;
1687
+ }
1688
+
1689
+ .entry-key {
1690
+ font-family: 'Monaco', 'Menlo', monospace;
1691
+ font-size: 0.75rem;
1692
+ overflow: hidden;
1693
+ text-overflow: ellipsis;
1694
+ white-space: nowrap;
1695
+ }
1696
+
1697
+ .entry-tags {
1698
+ display: flex;
1699
+ flex-wrap: wrap;
1700
+ gap: 0.25rem;
1701
+ }
1702
+
1703
+ .tag {
1704
+ padding: 0.125rem 0.375rem;
1705
+ background: #334155;
1706
+ border-radius: 4px;
1707
+ font-size: 0.625rem;
1708
+ color: #94a3b8;
1709
+ }
1710
+
1711
+ .tag-badge {
1712
+ padding: 0.25rem 0.5rem;
1713
+ background: #3b82f6;
1714
+ border-radius: 4px;
1715
+ font-size: 0.75rem;
1716
+ color: white;
1717
+ }
1718
+
1719
+ .entry-ttl.expired { color: #ef4444; }
1720
+ .entry-ttl.expiring-soon { color: #f59e0b; }
1721
+ .entry-ttl.healthy { color: #10b981; }
1722
+
1723
+ .entry-actions,
1724
+ .tag-actions {
1725
+ display: flex;
1726
+ gap: 0.25rem;
1727
+ }
1728
+
1729
+ .entry-action,
1730
+ .tag-action {
1731
+ padding: 0.25rem;
1732
+ background: transparent;
1733
+ border: none;
1734
+ cursor: pointer;
1735
+ font-size: 0.875rem;
1736
+ border-radius: 4px;
1737
+ transition: background 0.2s;
1738
+ }
1739
+
1740
+ .entry-action:hover,
1741
+ .tag-action:hover {
1742
+ background: #334155;
1743
+ }
1744
+
1745
+ .no-entries {
1746
+ display: flex;
1747
+ flex-direction: column;
1748
+ align-items: center;
1749
+ padding: 3rem;
1750
+ color: #64748b;
1751
+ }
1752
+
1753
+ .no-entries-icon {
1754
+ font-size: 3rem;
1755
+ margin-bottom: 1rem;
1756
+ }
1757
+
1758
+ .cache-actions {
1759
+ display: flex;
1760
+ gap: 0.5rem;
1761
+ justify-content: flex-end;
1762
+ margin-top: 1rem;
1763
+ }
1764
+
1765
+ .action-btn {
1766
+ padding: 0.5rem 1rem;
1767
+ background: #3b82f6;
1768
+ border: none;
1769
+ border-radius: 6px;
1770
+ color: white;
1771
+ font-size: 0.875rem;
1772
+ cursor: pointer;
1773
+ transition: background 0.2s;
1774
+ }
1775
+
1776
+ .action-btn:hover {
1777
+ background: #2563eb;
1778
+ }
1779
+
1780
+ .action-btn.danger {
1781
+ background: #ef4444;
1782
+ }
1783
+
1784
+ .action-btn.danger:hover {
1785
+ background: #dc2626;
1786
+ }
1787
+ `;
1788
+ function CacheTab({ cache }) {
1789
+ return null;
1790
+ }
1791
+ // src/devtools/DevToolsPanel.tsx
1792
+ function generateDevToolsPanelHTML(data) {
1793
+ const { pipeline, routes, islands, cache, hmrEvents } = data;
1794
+ return `
1795
+ <!DOCTYPE html>
1796
+ <html lang="en">
1797
+ <head>
1798
+ <meta charset="UTF-8">
1799
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1800
+ <title>EreoJS DevTools</title>
1801
+ <style>
1802
+ ${DEVTOOLS_BASE_STYLES}
1803
+ ${DATA_PIPELINE_STYLES}
1804
+ ${ROUTES_TAB_STYLES}
1805
+ ${ISLANDS_TAB_STYLES}
1806
+ ${CACHE_TAB_STYLES}
1807
+ </style>
1808
+ </head>
1809
+ <body>
1810
+ <div class="devtools-container">
1811
+ <header class="devtools-header">
1812
+ <div class="logo">
1813
+ <span class="logo-icon">\u2B21</span>
1814
+ <span class="logo-text">EreoJS DevTools</span>
1815
+ </div>
1816
+ <nav class="devtools-nav">
1817
+ <button class="nav-btn active" data-tab="data">Data Pipeline</button>
1818
+ <button class="nav-btn" data-tab="routes">Routes</button>
1819
+ <button class="nav-btn" data-tab="islands">Islands</button>
1820
+ <button class="nav-btn" data-tab="cache">Cache</button>
1821
+ <button class="nav-btn" data-tab="hmr">HMR</button>
1822
+ </nav>
1823
+ <div class="devtools-actions">
1824
+ <button class="action-icon" onclick="window.__EREO_DEVTOOLS__.refresh()" title="Refresh">
1825
+ \uD83D\uDD04
1826
+ </button>
1827
+ <button class="action-icon" onclick="window.__EREO_DEVTOOLS__.togglePosition()" title="Toggle Position">
1828
+ \uD83D\uDCD0
1829
+ </button>
1830
+ <button class="action-icon" onclick="window.__EREO_DEVTOOLS__.close()" title="Close">
1831
+ \u2715
1832
+ </button>
1833
+ </div>
1834
+ </header>
1835
+
1836
+ <main class="devtools-content">
1837
+ <div class="tab-panel active" id="data-panel">
1838
+ ${pipeline ? generateDataPipelineHTML(pipeline) : `
1839
+ <div class="no-data">
1840
+ <span class="no-data-icon">\uD83D\uDCCA</span>
1841
+ <span class="no-data-text">No data pipeline metrics yet</span>
1842
+ <span class="no-data-hint">Navigate to a route with loaders to see the pipeline visualization</span>
1843
+ </div>
1844
+ `}
1845
+ </div>
1846
+
1847
+ <div class="tab-panel" id="routes-panel">
1848
+ ${generateRoutesTabHTML(routes)}
1849
+ </div>
1850
+
1851
+ <div class="tab-panel" id="islands-panel">
1852
+ ${generateIslandsTabHTML(islands)}
1853
+ </div>
1854
+
1855
+ <div class="tab-panel" id="cache-panel">
1856
+ ${generateCacheTabHTML(cache)}
1857
+ </div>
1858
+
1859
+ <div class="tab-panel" id="hmr-panel">
1860
+ ${generateHMRTabHTML(hmrEvents)}
1861
+ </div>
1862
+ </main>
1863
+ </div>
1864
+
1865
+ <script>
1866
+ ${DEVTOOLS_CLIENT_SCRIPT}
1867
+ </script>
1868
+ </body>
1869
+ </html>
1870
+ `;
1871
+ }
1872
+ function generateHMRTabHTML(events) {
1873
+ if (events.length === 0) {
1874
+ return `
1875
+ <div class="hmr-container">
1876
+ <div class="hmr-header">
1877
+ <h3>HMR Events</h3>
1878
+ <div class="hmr-status">
1879
+ <span class="status-dot connected"></span>
1880
+ <span>Connected</span>
1881
+ </div>
1882
+ </div>
1883
+ <div class="no-events">
1884
+ <span class="no-events-icon">\u26A1</span>
1885
+ <span class="no-events-text">No HMR events yet</span>
1886
+ <span class="no-events-hint">Make changes to your code to see HMR updates</span>
1887
+ </div>
1888
+ </div>
1889
+ `;
1890
+ }
1891
+ const eventRows = events.sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((event) => `
1892
+ <div class="hmr-event hmr-${event.type}">
1893
+ <span class="event-icon">${getEventIcon(event.type)}</span>
1894
+ <span class="event-type">${event.type}</span>
1895
+ <span class="event-path">${escapeHtml5(event.path)}</span>
1896
+ ${event.reason ? `<span class="event-reason">${escapeHtml5(event.reason)}</span>` : ""}
1897
+ ${event.duration ? `<span class="event-duration">${event.duration.toFixed(0)}ms</span>` : ""}
1898
+ <span class="event-time">${formatTime(event.timestamp)}</span>
1899
+ </div>
1900
+ `).join(`
1901
+ `);
1902
+ return `
1903
+ <div class="hmr-container">
1904
+ <div class="hmr-header">
1905
+ <h3>HMR Events</h3>
1906
+ <div class="hmr-status">
1907
+ <span class="status-dot connected"></span>
1908
+ <span>Connected</span>
1909
+ </div>
1910
+ </div>
1911
+ <div class="hmr-events-list">
1912
+ ${eventRows}
1913
+ </div>
1914
+ </div>
1915
+ `;
1916
+ }
1917
+ function getEventIcon(type) {
1918
+ switch (type) {
1919
+ case "full-reload":
1920
+ return "\uD83D\uDD04";
1921
+ case "css-update":
1922
+ return "\uD83C\uDFA8";
1923
+ case "island-update":
1924
+ return "\uD83C\uDFDD\uFE0F";
1925
+ case "loader-update":
1926
+ return "\uD83D\uDCE6";
1927
+ case "component-update":
1928
+ return "\u269B\uFE0F";
1929
+ default:
1930
+ return "\u26A1";
1931
+ }
1932
+ }
1933
+ function formatTime(timestamp) {
1934
+ return new Date(timestamp).toLocaleTimeString();
1935
+ }
1936
+ function escapeHtml5(str) {
1937
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1938
+ }
1939
+ var DEVTOOLS_BASE_STYLES = `
1940
+ * {
1941
+ box-sizing: border-box;
1942
+ margin: 0;
1943
+ padding: 0;
1944
+ }
1945
+
1946
+ body {
1947
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1948
+ background: #0f172a;
1949
+ color: #e2e8f0;
1950
+ overflow: hidden;
1951
+ }
1952
+
1953
+ .devtools-container {
1954
+ display: flex;
1955
+ flex-direction: column;
1956
+ height: 100vh;
1957
+ }
1958
+
1959
+ .devtools-header {
1960
+ display: flex;
1961
+ align-items: center;
1962
+ gap: 1rem;
1963
+ padding: 0.75rem 1rem;
1964
+ background: #1e293b;
1965
+ border-bottom: 1px solid #334155;
1966
+ }
1967
+
1968
+ .logo {
1969
+ display: flex;
1970
+ align-items: center;
1971
+ gap: 0.5rem;
1972
+ font-weight: 600;
1973
+ }
1974
+
1975
+ .logo-icon {
1976
+ color: #3b82f6;
1977
+ font-size: 1.25rem;
1978
+ }
1979
+
1980
+ .devtools-nav {
1981
+ display: flex;
1982
+ gap: 0.25rem;
1983
+ flex: 1;
1984
+ }
1985
+
1986
+ .nav-btn {
1987
+ padding: 0.5rem 1rem;
1988
+ background: transparent;
1989
+ border: none;
1990
+ border-radius: 6px;
1991
+ color: #94a3b8;
1992
+ font-size: 0.875rem;
1993
+ cursor: pointer;
1994
+ transition: all 0.2s;
1995
+ }
1996
+
1997
+ .nav-btn:hover {
1998
+ color: #f8fafc;
1999
+ background: #334155;
2000
+ }
2001
+
2002
+ .nav-btn.active {
2003
+ color: #3b82f6;
2004
+ background: rgba(59, 130, 246, 0.1);
2005
+ }
2006
+
2007
+ .devtools-actions {
2008
+ display: flex;
2009
+ gap: 0.5rem;
2010
+ }
2011
+
2012
+ .action-icon {
2013
+ width: 32px;
2014
+ height: 32px;
2015
+ display: flex;
2016
+ align-items: center;
2017
+ justify-content: center;
2018
+ background: transparent;
2019
+ border: none;
2020
+ border-radius: 6px;
2021
+ cursor: pointer;
2022
+ font-size: 1rem;
2023
+ transition: background 0.2s;
2024
+ }
2025
+
2026
+ .action-icon:hover {
2027
+ background: #334155;
2028
+ }
2029
+
2030
+ .devtools-content {
2031
+ flex: 1;
2032
+ overflow: auto;
2033
+ padding: 1rem;
2034
+ }
2035
+
2036
+ .tab-panel {
2037
+ display: none;
2038
+ }
2039
+
2040
+ .tab-panel.active {
2041
+ display: block;
2042
+ }
2043
+
2044
+ .no-data {
2045
+ display: flex;
2046
+ flex-direction: column;
2047
+ align-items: center;
2048
+ justify-content: center;
2049
+ height: 300px;
2050
+ color: #64748b;
2051
+ }
2052
+
2053
+ .no-data-icon {
2054
+ font-size: 4rem;
2055
+ margin-bottom: 1rem;
2056
+ }
2057
+
2058
+ .no-data-text {
2059
+ font-size: 1.25rem;
2060
+ margin-bottom: 0.5rem;
2061
+ }
2062
+
2063
+ .no-data-hint {
2064
+ font-size: 0.875rem;
2065
+ color: #475569;
2066
+ }
2067
+
2068
+ /* HMR Tab Styles */
2069
+ .hmr-container {
2070
+ background: #0f172a;
2071
+ padding: 1.5rem;
2072
+ border-radius: 12px;
2073
+ }
2074
+
2075
+ .hmr-header {
2076
+ display: flex;
2077
+ justify-content: space-between;
2078
+ align-items: center;
2079
+ margin-bottom: 1.5rem;
2080
+ padding-bottom: 1rem;
2081
+ border-bottom: 1px solid #334155;
2082
+ }
2083
+
2084
+ .hmr-header h3 {
2085
+ margin: 0;
2086
+ font-size: 1.25rem;
2087
+ }
2088
+
2089
+ .hmr-status {
2090
+ display: flex;
2091
+ align-items: center;
2092
+ gap: 0.5rem;
2093
+ font-size: 0.875rem;
2094
+ color: #94a3b8;
2095
+ }
2096
+
2097
+ .status-dot {
2098
+ width: 8px;
2099
+ height: 8px;
2100
+ border-radius: 50%;
2101
+ background: #64748b;
2102
+ }
2103
+
2104
+ .status-dot.connected {
2105
+ background: #10b981;
2106
+ box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
2107
+ }
2108
+
2109
+ .hmr-events-list {
2110
+ background: #1e293b;
2111
+ border-radius: 8px;
2112
+ overflow: hidden;
2113
+ }
2114
+
2115
+ .hmr-event {
2116
+ display: grid;
2117
+ grid-template-columns: 30px 120px 1fr auto auto;
2118
+ gap: 1rem;
2119
+ padding: 0.75rem 1rem;
2120
+ border-bottom: 1px solid #0f172a;
2121
+ align-items: center;
2122
+ font-size: 0.875rem;
2123
+ }
2124
+
2125
+ .hmr-event:hover {
2126
+ background: #334155;
2127
+ }
2128
+
2129
+ .hmr-full-reload { border-left: 3px solid #f59e0b; }
2130
+ .hmr-css-update { border-left: 3px solid #10b981; }
2131
+ .hmr-island-update { border-left: 3px solid #3b82f6; }
2132
+ .hmr-loader-update { border-left: 3px solid #8b5cf6; }
2133
+ .hmr-component-update { border-left: 3px solid #ec4899; }
2134
+
2135
+ .event-icon {
2136
+ font-size: 1rem;
2137
+ }
2138
+
2139
+ .event-type {
2140
+ font-family: 'Monaco', 'Menlo', monospace;
2141
+ font-size: 0.75rem;
2142
+ color: #94a3b8;
2143
+ }
2144
+
2145
+ .event-path {
2146
+ font-family: 'Monaco', 'Menlo', monospace;
2147
+ font-size: 0.75rem;
2148
+ color: #f8fafc;
2149
+ overflow: hidden;
2150
+ text-overflow: ellipsis;
2151
+ white-space: nowrap;
2152
+ }
2153
+
2154
+ .event-reason {
2155
+ font-size: 0.75rem;
2156
+ color: #64748b;
2157
+ max-width: 200px;
2158
+ overflow: hidden;
2159
+ text-overflow: ellipsis;
2160
+ white-space: nowrap;
2161
+ }
2162
+
2163
+ .event-duration {
2164
+ font-family: 'Monaco', 'Menlo', monospace;
2165
+ font-size: 0.75rem;
2166
+ color: #10b981;
2167
+ }
2168
+
2169
+ .event-time {
2170
+ font-size: 0.75rem;
2171
+ color: #475569;
2172
+ }
2173
+
2174
+ .no-events {
2175
+ display: flex;
2176
+ flex-direction: column;
2177
+ align-items: center;
2178
+ padding: 3rem;
2179
+ color: #64748b;
2180
+ }
2181
+
2182
+ .no-events-icon {
2183
+ font-size: 3rem;
2184
+ margin-bottom: 1rem;
2185
+ }
2186
+
2187
+ .no-events-hint {
2188
+ font-size: 0.75rem;
2189
+ color: #475569;
2190
+ margin-top: 0.5rem;
2191
+ }
2192
+ `;
2193
+ var DEVTOOLS_CLIENT_SCRIPT = `
2194
+ (function() {
2195
+ // Tab switching
2196
+ const navBtns = document.querySelectorAll('.nav-btn');
2197
+ const panels = document.querySelectorAll('.tab-panel');
2198
+
2199
+ navBtns.forEach(btn => {
2200
+ btn.addEventListener('click', () => {
2201
+ navBtns.forEach(b => b.classList.remove('active'));
2202
+ panels.forEach(p => p.classList.remove('active'));
2203
+
2204
+ btn.classList.add('active');
2205
+ const tabId = btn.dataset.tab + '-panel';
2206
+ document.getElementById(tabId).classList.add('active');
2207
+ });
2208
+ });
2209
+
2210
+ // DevTools API
2211
+ window.__EREO_DEVTOOLS__ = {
2212
+ refresh() {
2213
+ location.reload();
2214
+ },
2215
+ togglePosition() {
2216
+ // Send message to parent to toggle position
2217
+ window.parent?.postMessage({ type: 'ereo-devtools-toggle-position' }, '*');
2218
+ },
2219
+ close() {
2220
+ window.parent?.postMessage({ type: 'ereo-devtools-close' }, '*');
2221
+ },
2222
+ highlightIslands() {
2223
+ window.parent?.postMessage({ type: 'ereo-devtools-highlight-islands' }, '*');
2224
+ },
2225
+ hydrateAll() {
2226
+ window.parent?.postMessage({ type: 'ereo-devtools-hydrate-all' }, '*');
2227
+ },
2228
+ scrollToIsland(id) {
2229
+ window.parent?.postMessage({ type: 'ereo-devtools-scroll-to-island', id }, '*');
2230
+ },
2231
+ inspectIsland(id) {
2232
+ window.parent?.postMessage({ type: 'ereo-devtools-inspect-island', id }, '*');
2233
+ },
2234
+ inspectEntry(key) {
2235
+ window.parent?.postMessage({ type: 'ereo-devtools-inspect-entry', key }, '*');
2236
+ },
2237
+ clearCache() {
2238
+ window.parent?.postMessage({ type: 'ereo-devtools-clear-cache' }, '*');
2239
+ },
2240
+ refreshCache() {
2241
+ window.parent?.postMessage({ type: 'ereo-devtools-refresh-cache' }, '*');
2242
+ },
2243
+ invalidateKey(key) {
2244
+ window.parent?.postMessage({ type: 'ereo-devtools-invalidate-key', key }, '*');
2245
+ },
2246
+ invalidateTag(tag) {
2247
+ window.parent?.postMessage({ type: 'ereo-devtools-invalidate-tag', tag }, '*');
2248
+ },
2249
+ };
2250
+
2251
+ // Listen for updates from parent
2252
+ window.addEventListener('message', (event) => {
2253
+ if (event.data.type === 'ereo-devtools-update') {
2254
+ // Handle live updates
2255
+ console.log('[DevTools] Received update:', event.data);
2256
+ }
2257
+ });
2258
+ })();
2259
+ `;
2260
+ function DevToolsPanel({ data }) {
2261
+ return null;
2262
+ }
2263
+ // src/devtools/plugin.ts
2264
+ function createDevToolsPlugin(config = {}) {
2265
+ const {
2266
+ mountPath = "/__devtools",
2267
+ dataPipeline = true,
2268
+ routes: showRoutes = true,
2269
+ islands = true,
2270
+ cache = true,
2271
+ position = "bottom-right"
2272
+ } = config;
2273
+ const state = {
2274
+ routes: [],
2275
+ pipelineHistory: [],
2276
+ hmrEvents: []
2277
+ };
2278
+ return {
2279
+ name: "@ereo/dev-inspector:devtools",
2280
+ transformRoutes(routeList) {
2281
+ state.routes = routeList.filter((r) => !r.layout).map((route) => ({
2282
+ path: route.path,
2283
+ file: route.file,
2284
+ renderMode: route.config?.render?.mode || "ssr",
2285
+ hasLoader: !!route.module?.loader,
2286
+ hasAction: !!route.module?.action,
2287
+ middleware: (route.config?.middleware || []).map((m) => typeof m === "string" ? m : "inline"),
2288
+ islandCount: route.config?.islands?.components?.length || 0,
2289
+ cacheTags: route.config?.cache?.data?.tags || [],
2290
+ authRequired: route.config?.auth?.required || false
2291
+ }));
2292
+ return routeList;
2293
+ },
2294
+ configureServer(server) {
2295
+ server.middlewares.push(async (request, context, next) => {
2296
+ const url = new URL(request.url);
2297
+ if (url.pathname === mountPath) {
2298
+ const panelData = await collectPanelData(state);
2299
+ const html = generateDevToolsPanelHTML(panelData);
2300
+ return new Response(html, {
2301
+ headers: {
2302
+ "Content-Type": "text/html",
2303
+ "X-Frame-Options": "SAMEORIGIN"
2304
+ }
2305
+ });
2306
+ }
2307
+ if (url.pathname === `${mountPath}/api/pipeline`) {
2308
+ return new Response(JSON.stringify(state.pipelineHistory), {
2309
+ headers: { "Content-Type": "application/json" }
2310
+ });
2311
+ }
2312
+ if (url.pathname === `${mountPath}/api/routes`) {
2313
+ return new Response(JSON.stringify(state.routes), {
2314
+ headers: { "Content-Type": "application/json" }
2315
+ });
2316
+ }
2317
+ if (url.pathname === `${mountPath}/api/hmr`) {
2318
+ return new Response(JSON.stringify(state.hmrEvents), {
2319
+ headers: { "Content-Type": "application/json" }
2320
+ });
2321
+ }
2322
+ if (url.pathname === `${mountPath}/api/pipeline/record` && request.method === "POST") {
2323
+ try {
2324
+ const metrics2 = await request.json();
2325
+ state.pipelineHistory.unshift(metrics2);
2326
+ if (state.pipelineHistory.length > 100) {
2327
+ state.pipelineHistory.pop();
2328
+ }
2329
+ return new Response(JSON.stringify({ success: true }), {
2330
+ headers: { "Content-Type": "application/json" }
2331
+ });
2332
+ } catch {
2333
+ return new Response(JSON.stringify({ error: "Invalid data" }), {
2334
+ status: 400,
2335
+ headers: { "Content-Type": "application/json" }
2336
+ });
2337
+ }
2338
+ }
2339
+ const response = await next();
2340
+ const metrics = context.get("__pipeline_metrics");
2341
+ if (metrics) {
2342
+ const routePath = url.pathname;
2343
+ const visualization = {
2344
+ route: routePath,
2345
+ totalTime: metrics.total,
2346
+ loaders: Array.from(metrics.loaders.values()).map((m) => ({
2347
+ key: m.key,
2348
+ start: m.startTime,
2349
+ end: m.endTime,
2350
+ duration: m.duration,
2351
+ cacheHit: m.cacheHit,
2352
+ source: m.source || "unknown",
2353
+ waitingFor: m.waitingFor
2354
+ })),
2355
+ efficiency: metrics.parallelEfficiency,
2356
+ waterfalls: metrics.waterfalls,
2357
+ timestamp: Date.now()
2358
+ };
2359
+ state.pipelineHistory.unshift(visualization);
2360
+ if (state.pipelineHistory.length > 100) {
2361
+ state.pipelineHistory.pop();
2362
+ }
2363
+ }
2364
+ return response;
2365
+ });
2366
+ if (dataPipeline || islands || cache) {
2367
+ server.middlewares.push(async (request, context, next) => {
2368
+ const response = await next();
2369
+ if (response.headers.get("Content-Type")?.includes("text/html")) {
2370
+ let html = await response.text();
2371
+ const overlayScript = generateOverlayScript(mountPath, position);
2372
+ html = html.replace("</body>", `${overlayScript}</body>`);
2373
+ return new Response(html, {
2374
+ status: response.status,
2375
+ headers: response.headers
2376
+ });
2377
+ }
2378
+ return response;
2379
+ });
2380
+ }
2381
+ console.log(` \x1B[35m\u2B21\x1B[0m DevTools available at ${mountPath}`);
2382
+ }
2383
+ };
2384
+ }
2385
+ async function collectPanelData(state) {
2386
+ const pipeline = state.pipelineHistory[0];
2387
+ const islands = [];
2388
+ const cache = {
2389
+ entries: [],
2390
+ totalSize: 0,
2391
+ hitRate: 0,
2392
+ tagStats: new Map
2393
+ };
2394
+ return {
2395
+ pipeline,
2396
+ routes: state.routes,
2397
+ islands,
2398
+ cache,
2399
+ hmrEvents: state.hmrEvents
2400
+ };
2401
+ }
2402
+ function generateOverlayScript(mountPath, position) {
2403
+ return `
2404
+ <script>
2405
+ (function() {
2406
+ // DevTools toggle button
2407
+ const button = document.createElement('button');
2408
+ button.id = 'ereo-devtools-toggle';
2409
+ button.innerHTML = '\u2B21';
2410
+ button.title = 'Open EreoJS DevTools';
2411
+ button.style.cssText = \`
2412
+ position: fixed;
2413
+ ${position.includes("bottom") ? "bottom: 16px;" : "top: 16px;"}
2414
+ ${position.includes("right") ? "right: 16px;" : "left: 16px;"}
2415
+ width: 48px;
2416
+ height: 48px;
2417
+ border-radius: 50%;
2418
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
2419
+ border: none;
2420
+ color: white;
2421
+ font-size: 24px;
2422
+ cursor: pointer;
2423
+ z-index: 99998;
2424
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2425
+ transition: transform 0.2s, box-shadow 0.2s;
2426
+ \`;
2427
+ button.addEventListener('mouseenter', () => {
2428
+ button.style.transform = 'scale(1.1)';
2429
+ button.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.4)';
2430
+ });
2431
+ button.addEventListener('mouseleave', () => {
2432
+ button.style.transform = 'scale(1)';
2433
+ button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
2434
+ });
2435
+
2436
+ // DevTools iframe
2437
+ let iframe = null;
2438
+ let isOpen = false;
2439
+
2440
+ button.addEventListener('click', () => {
2441
+ if (isOpen) {
2442
+ closeDevTools();
2443
+ } else {
2444
+ openDevTools();
2445
+ }
2446
+ });
2447
+
2448
+ function openDevTools() {
2449
+ if (!iframe) {
2450
+ iframe = document.createElement('iframe');
2451
+ iframe.id = 'ereo-devtools-frame';
2452
+ iframe.src = '${mountPath}';
2453
+ iframe.style.cssText = \`
2454
+ position: fixed;
2455
+ ${position.includes("bottom") ? "bottom: 0;" : "top: 0;"}
2456
+ left: 0;
2457
+ right: 0;
2458
+ height: 400px;
2459
+ border: none;
2460
+ background: #0f172a;
2461
+ z-index: 99999;
2462
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
2463
+ \`;
2464
+ document.body.appendChild(iframe);
2465
+ }
2466
+ iframe.style.display = 'block';
2467
+ isOpen = true;
2468
+ button.innerHTML = '\u2715';
2469
+ button.style.bottom = '416px';
2470
+ }
2471
+
2472
+ function closeDevTools() {
2473
+ if (iframe) {
2474
+ iframe.style.display = 'none';
2475
+ }
2476
+ isOpen = false;
2477
+ button.innerHTML = '\u2B21';
2478
+ button.style.bottom = '16px';
2479
+ }
2480
+
2481
+ // Listen for messages from DevTools
2482
+ window.addEventListener('message', (event) => {
2483
+ if (event.data.type === 'ereo-devtools-close') {
2484
+ closeDevTools();
2485
+ } else if (event.data.type === 'ereo-devtools-toggle-position') {
2486
+ // Toggle between top and bottom
2487
+ } else if (event.data.type === 'ereo-devtools-highlight-islands') {
2488
+ highlightIslands();
2489
+ } else if (event.data.type === 'ereo-devtools-scroll-to-island') {
2490
+ scrollToIsland(event.data.id);
2491
+ }
2492
+ });
2493
+
2494
+ function highlightIslands() {
2495
+ const islands = document.querySelectorAll('[data-island]');
2496
+ islands.forEach(el => {
2497
+ el.style.outline = '2px dashed #3b82f6';
2498
+ el.style.outlineOffset = '2px';
2499
+ setTimeout(() => {
2500
+ el.style.outline = '';
2501
+ el.style.outlineOffset = '';
2502
+ }, 3000);
2503
+ });
2504
+ }
2505
+
2506
+ function scrollToIsland(id) {
2507
+ const island = document.querySelector(\`[data-island="\${id}"]\`);
2508
+ if (island) {
2509
+ island.scrollIntoView({ behavior: 'smooth', block: 'center' });
2510
+ island.style.outline = '3px solid #10b981';
2511
+ setTimeout(() => {
2512
+ island.style.outline = '';
2513
+ }, 2000);
2514
+ }
2515
+ }
2516
+
2517
+ document.body.appendChild(button);
2518
+ })();
2519
+ </script>
2520
+ `;
2521
+ }
2522
+ export {
2523
+ generateRoutesTabHTML,
2524
+ generateIslandsTabHTML,
2525
+ generateInspectorHTML,
2526
+ generateDevToolsPanelHTML,
2527
+ generateDataPipelineHTML,
2528
+ generateCacheTabHTML,
2529
+ formatRouteTree,
2530
+ createRouteInfo,
2531
+ createDevToolsPlugin,
2532
+ createDevInspector,
2533
+ RoutesTab,
2534
+ IslandsTab,
2535
+ DevToolsPanel,
2536
+ DataPipelineTab,
2537
+ CacheTab
2538
+ };