elm-ssr 0.6.2 → 0.90.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.
@@ -250,13 +250,27 @@ function createIslandsRuntime(deps) {
250
250
  syncCollection(metaNodes(document.head), metaNodes(sourceDoc.head), metaKey);
251
251
  };
252
252
 
253
- const navigate = async (url, push = true) => {
253
+ const navigate = async (url, push = true, options = {}) => {
254
254
  try {
255
- const response = await window.fetch("/api/render?path=" + encodeURIComponent(url.pathname + url.search));
255
+ const renderUrl = "/api/render?path=" + encodeURIComponent(url.pathname + url.search);
256
+ const response = await window.fetch(renderUrl, {
257
+ method: options.method || "GET",
258
+ body: options.body,
259
+ headers: options.headers
260
+ });
256
261
  const result = await response.json();
257
262
 
263
+ if (result.debug) {
264
+ window.dispatchEvent(new window.CustomEvent("elm-ssr-debug-update", { detail: result.debug }));
265
+ }
266
+
258
267
  if (result.redirect) {
259
- window.location.href = result.redirect;
268
+ const redirectUrl = new URL(result.redirect, window.location.href);
269
+ if (redirectUrl.origin === window.location.origin) {
270
+ navigate(redirectUrl, push);
271
+ } else {
272
+ window.location.href = result.redirect;
273
+ }
260
274
  return;
261
275
  }
262
276
 
@@ -322,7 +336,59 @@ function createIslandsRuntime(deps) {
322
336
  navigate(url);
323
337
  };
324
338
 
325
- return { bootIslands, navigate, handleLinkClick, persistentIslands, cleanups };
339
+ const handleFormSubmit = (event) => {
340
+ const target = event.target;
341
+ let form = target && target.nodeType === 1 ? target : null;
342
+
343
+ while (form && form.tagName !== "FORM") {
344
+ form = form.parentElement;
345
+ }
346
+
347
+ if (!form) {
348
+ return;
349
+ }
350
+
351
+ const action = form.getAttribute("action") || window.location.pathname + window.location.search;
352
+ const actionUrl = new URL(action, window.location.href);
353
+
354
+ if (actionUrl.origin !== window.location.origin) {
355
+ return;
356
+ }
357
+
358
+ if (form.getAttribute("target") === "_blank" || form.getAttribute("download") !== null) {
359
+ return;
360
+ }
361
+
362
+ event.preventDefault();
363
+
364
+ const method = (form.getAttribute("method") || "GET").toUpperCase();
365
+ const formData = new window.FormData(form);
366
+
367
+ if (method === "GET") {
368
+ const params = new window.URLSearchParams(formData);
369
+ actionUrl.search = params.toString();
370
+ navigate(actionUrl);
371
+ } else {
372
+ const enctype = form.getAttribute("enctype") || "application/x-www-form-urlencoded";
373
+ let body;
374
+ let headers = {};
375
+
376
+ if (enctype === "multipart/form-data") {
377
+ body = formData;
378
+ } else {
379
+ body = new window.URLSearchParams(formData);
380
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
381
+ }
382
+
383
+ navigate(actionUrl, true, {
384
+ method: "POST",
385
+ body,
386
+ headers
387
+ });
388
+ }
389
+ };
390
+
391
+ return { bootIslands, navigate, handleLinkClick, handleFormSubmit, persistentIslands, cleanups };
326
392
  }
327
393
  `;
328
394
 
@@ -339,6 +405,7 @@ const runtime = createIslandsRuntime({
339
405
  });
340
406
 
341
407
  window.addEventListener("click", runtime.handleLinkClick);
408
+ window.addEventListener("submit", runtime.handleFormSubmit);
342
409
  window.addEventListener("popstate", () => runtime.navigate(new URL(window.location.href), false));
343
410
 
344
411
  runtime.bootIslands();
@@ -0,0 +1,535 @@
1
+ import type { EffectRunner } from "./effects";
2
+
3
+ export const instrumentEffects = (baseRunner: EffectRunner): EffectRunner => {
4
+ return async (effect, context) => {
5
+ const start = performance.now();
6
+ try {
7
+ const result = await baseRunner(effect, context);
8
+ const durationMs = performance.now() - start;
9
+ if (context.debugLogs) {
10
+ context.debugLogs.push({
11
+ kind: effect.kind,
12
+ payload: effect.payload,
13
+ ok: result.ok,
14
+ value: result.value,
15
+ error: result.error,
16
+ durationMs
17
+ });
18
+ }
19
+ return result;
20
+ } catch (err: unknown) {
21
+ const durationMs = performance.now() - start;
22
+ if (context.debugLogs) {
23
+ context.debugLogs.push({
24
+ kind: effect.kind,
25
+ payload: effect.payload,
26
+ ok: false,
27
+ error: String(err),
28
+ durationMs
29
+ });
30
+ }
31
+ throw err;
32
+ }
33
+ };
34
+ };
35
+
36
+ export const injectDebugger = (html: string, data: unknown): string => {
37
+ const scriptContent = `
38
+ (function() {
39
+ const data = ${JSON.stringify(data)};
40
+
41
+ // 1. Create stylesheet
42
+ const style = document.createElement('style');
43
+ style.textContent = \`
44
+ #elm-ssr-debug-panel {
45
+ position: fixed;
46
+ bottom: 0;
47
+ left: 0;
48
+ width: 100vw;
49
+ height: 350px;
50
+ background: rgba(17, 24, 39, 0.95);
51
+ backdrop-filter: blur(12px);
52
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
53
+ color: #e5e7eb;
54
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
55
+ z-index: 999999;
56
+ transform: translateY(100%);
57
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
58
+ display: flex;
59
+ flex-direction: column;
60
+ box-shadow: 0 -10px 25px -5px rgba(0, 0, 0, 0.3);
61
+ }
62
+ #elm-ssr-debug-panel.open {
63
+ transform: translateY(0);
64
+ }
65
+ #elm-ssr-debug-toggle {
66
+ position: fixed;
67
+ bottom: 20px;
68
+ right: 20px;
69
+ background: rgba(79, 70, 229, 0.9);
70
+ color: white;
71
+ border: none;
72
+ padding: 10px 16px;
73
+ border-radius: 9999px;
74
+ cursor: pointer;
75
+ font-weight: 600;
76
+ font-size: 0.85em;
77
+ box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4);
78
+ z-index: 999998;
79
+ backdrop-filter: blur(4px);
80
+ transition: all 0.2s;
81
+ }
82
+ #elm-ssr-debug-toggle:hover {
83
+ background: rgba(79, 70, 229, 1);
84
+ transform: scale(1.05);
85
+ }
86
+ .debug-header {
87
+ display: flex;
88
+ justify-content: space-between;
89
+ align-items: center;
90
+ padding: 8px 16px;
91
+ background: rgba(0, 0, 0, 0.2);
92
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
93
+ }
94
+ .debug-title {
95
+ font-weight: 700;
96
+ color: #818cf8;
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 6px;
100
+ font-size: 0.9em;
101
+ }
102
+ .debug-tabs {
103
+ display: flex;
104
+ gap: 4px;
105
+ }
106
+ .debug-tab {
107
+ background: transparent;
108
+ border: none;
109
+ color: #9ca3af;
110
+ padding: 6px 12px;
111
+ cursor: pointer;
112
+ font-size: 0.85em;
113
+ font-weight: 500;
114
+ border-radius: 4px;
115
+ transition: all 0.15s;
116
+ }
117
+ .debug-tab:hover {
118
+ color: #f3f4f6;
119
+ background: rgba(255, 255, 255, 0.05);
120
+ }
121
+ .debug-tab.active {
122
+ color: white;
123
+ background: rgba(79, 70, 229, 0.2);
124
+ border: 1px solid rgba(79, 70, 229, 0.4);
125
+ }
126
+ .debug-close {
127
+ background: transparent;
128
+ border: none;
129
+ color: #9ca3af;
130
+ cursor: pointer;
131
+ font-size: 1.2em;
132
+ }
133
+ .debug-close:hover {
134
+ color: white;
135
+ }
136
+ .debug-content {
137
+ flex: 1;
138
+ overflow-y: auto;
139
+ padding: 16px;
140
+ font-size: 0.85em;
141
+ }
142
+ .debug-pane {
143
+ display: none;
144
+ }
145
+ .debug-pane.active {
146
+ display: block;
147
+ }
148
+ .overview-grid {
149
+ display: grid;
150
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
151
+ gap: 12px;
152
+ }
153
+ .overview-card {
154
+ background: rgba(255, 255, 255, 0.03);
155
+ border: 1px solid rgba(255, 255, 255, 0.05);
156
+ border-radius: 6px;
157
+ padding: 12px;
158
+ }
159
+ .overview-label {
160
+ color: #9ca3af;
161
+ font-size: 0.8em;
162
+ margin-bottom: 4px;
163
+ }
164
+ .overview-val {
165
+ font-size: 1.1em;
166
+ font-weight: 600;
167
+ }
168
+ .status-ok { color: #34d399; }
169
+ .status-error { color: #f87171; }
170
+ .debug-list {
171
+ list-style: none;
172
+ padding: 0;
173
+ margin: 0;
174
+ }
175
+ .debug-item {
176
+ padding: 8px 12px;
177
+ background: rgba(255, 255, 255, 0.02);
178
+ border: 1px solid rgba(255, 255, 255, 0.04);
179
+ border-radius: 6px;
180
+ margin-bottom: 8px;
181
+ display: flex;
182
+ justify-content: space-between;
183
+ align-items: center;
184
+ transition: background 0.15s;
185
+ }
186
+ .debug-item:hover {
187
+ background: rgba(255, 255, 255, 0.06);
188
+ }
189
+ .debug-item-meta {
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 2px;
193
+ }
194
+ .debug-badge {
195
+ font-size: 0.75em;
196
+ padding: 2px 6px;
197
+ border-radius: 4px;
198
+ font-weight: 600;
199
+ }
200
+ .badge-primary { background: rgba(79, 70, 229, 0.2); color: #a5b4fc; border: 1px solid rgba(79, 70, 229, 0.4); }
201
+ .badge-success { background: rgba(52, 211, 153, 0.2); color: #a7f3d0; border: 1px solid rgba(52, 211, 153, 0.4); }
202
+ .badge-warn { background: rgba(251, 191, 36, 0.2); color: #fde68a; border: 1px solid rgba(251, 191, 36, 0.4); }
203
+ .debug-code {
204
+ background: rgba(0, 0, 0, 0.3);
205
+ padding: 10px;
206
+ border-radius: 6px;
207
+ overflow-x: auto;
208
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
209
+ color: #a7f3d0;
210
+ margin: 4px 0;
211
+ border: 1px solid rgba(255, 255, 255, 0.03);
212
+ }
213
+ #elm-ssr-debug-highlight {
214
+ position: absolute;
215
+ background: rgba(139, 92, 246, 0.15);
216
+ border: 2px dashed rgba(139, 92, 246, 0.8);
217
+ pointer-events: none;
218
+ z-index: 999997;
219
+ display: none;
220
+ box-sizing: border-box;
221
+ box-shadow: 0 0 12px rgba(139, 92, 246, 0.3);
222
+ border-radius: 4px;
223
+ transition: all 0.15s ease-out;
224
+ }
225
+ \`;
226
+ document.head.appendChild(style);
227
+
228
+ // 2. Create Highlight div
229
+ const highlight = document.createElement('div');
230
+ highlight.id = 'elm-ssr-debug-highlight';
231
+ document.body.appendChild(highlight);
232
+
233
+ // 3. Create Toggle Button
234
+ const toggleBtn = document.createElement('button');
235
+ toggleBtn.id = 'elm-ssr-debug-toggle';
236
+ toggleBtn.innerHTML = '⚡ Debug';
237
+ document.body.appendChild(toggleBtn);
238
+
239
+ // 4. Create Panel DOM
240
+ const panel = document.createElement('div');
241
+ panel.id = 'elm-ssr-debug-panel';
242
+
243
+ let effectsHtml = '';
244
+ if (data.effects && data.effects.length > 0) {
245
+ effectsHtml = data.effects.map(eff => {
246
+ const isSql = ['query', 'queryOne', 'execute'].includes(eff.kind);
247
+ const sqlSnippet = isSql ? \`<pre class="debug-code">\${eff.payload.sql}</pre>\` : '';
248
+ const paramsSnippet = isSql && eff.payload.params && eff.payload.params.length > 0 ?
249
+ \`<div><strong>Params:</strong> \${JSON.stringify(eff.payload.params)}</div>\` : '';
250
+ const outcome = eff.ok ? '<span class="status-ok">SUCCESS</span>' : '<span class="status-error">FAILED</span>';
251
+ const detailError = eff.error ? \`<div class="status-error">\${eff.error}</div>\` : '';
252
+ return \`
253
+ <li class="debug-item">
254
+ <div class="debug-item-meta" style="flex: 1;">
255
+ <div style="display: flex; gap: 8px; align-items: center;">
256
+ <span class="debug-badge badge-primary">\${eff.kind}</span>
257
+ \${outcome}
258
+ <span style="color: #9ca3af; font-size: 0.85em;">\${eff.durationMs.toFixed(1)}ms</span>
259
+ </div>
260
+ \${sqlSnippet}
261
+ \${paramsSnippet}
262
+ \${detailError}
263
+ </div>
264
+ </li>
265
+ \`;
266
+ }).join('');
267
+ } else {
268
+ effectsHtml = '<div style="color: #9ca3af; text-align: center; padding: 20px;">No server-side effects executed for this request.</div>';
269
+ }
270
+
271
+ panel.innerHTML = \`
272
+ <div class="debug-header">
273
+ <div class="debug-title">
274
+ <span>⚡</span> elm-ssr DevTools
275
+ </div>
276
+ <div class="debug-tabs">
277
+ <button class="debug-tab active" data-tab="overview">Overview</button>
278
+ <button class="debug-tab" data-tab="islands">Islands</button>
279
+ <button class="debug-tab" data-tab="effects">Effects</button>
280
+ <button class="debug-tab" data-tab="session">Session</button>
281
+ <button class="debug-tab" data-tab="bridges">Bridges</button>
282
+ </div>
283
+ <button class="debug-close">&times;</button>
284
+ </div>
285
+
286
+ <div class="debug-content">
287
+ <!-- OVERVIEW TAB -->
288
+ <div class="debug-pane active" id="pane-overview">
289
+ <div class="overview-grid">
290
+ <div class="overview-card">
291
+ <div class="overview-label">Request Method & Path</div>
292
+ <div class="overview-val">\${data.method} \${new URL(data.url).pathname}</div>
293
+ </div>
294
+ <div class="overview-card">
295
+ <div class="overview-label">HTTP Status</div>
296
+ <div class="overview-val \${data.status < 400 ? 'status-ok' : 'status-error'}">\${data.status}</div>
297
+ </div>
298
+ <div class="overview-card">
299
+ <div class="overview-label">Server Render Time</div>
300
+ <div class="overview-val">\${data.durationMs.toFixed(1)} ms</div>
301
+ </div>
302
+ <div class="overview-card">
303
+ <div class="overview-label">Database & Effects</div>
304
+ <div class="overview-val">\${(data.effects || []).length} calls</div>
305
+ </div>
306
+ </div>
307
+ </div>
308
+
309
+ <!-- ISLANDS TAB -->
310
+ <div class="debug-pane" id="pane-islands">
311
+ <ul class="debug-list" id="debug-islands-list">
312
+ <li style="color: #9ca3af; text-align: center; padding: 20px;">Scanning page for active islands...</li>
313
+ </ul>
314
+ </div>
315
+
316
+ <!-- EFFECTS TAB -->
317
+ <div class="debug-pane" id="pane-effects">
318
+ <ul class="debug-list">
319
+ \${effectsHtml}
320
+ </ul>
321
+ </div>
322
+
323
+ <!-- SESSION TAB -->
324
+ <div class="debug-pane" id="pane-session">
325
+ <div style="display: flex; flex-direction: column; gap: 12px;">
326
+ <div>
327
+ <strong>Session Data:</strong>
328
+ <pre class="debug-code" style="color: #818cf8;">\${JSON.stringify(data.session, null, 2) || 'null'}</pre>
329
+ </div>
330
+ </div>
331
+ </div>
332
+
333
+ <!-- BRIDGES TAB -->
334
+ <div class="debug-pane" id="pane-bridges">
335
+ <div style="margin-bottom: 8px; color: #9ca3af; font-size: 0.9em;">Live broadcast event listener. Send messages to see updates:</div>
336
+ <ul class="debug-list" id="debug-bridges-log">
337
+ <li style="color: #9ca3af; text-align: center; padding: 12px;">Listening for 'elm-ssr-broadcast' events...</li>
338
+ </ul>
339
+ </div>
340
+ </div>
341
+ \`;
342
+ document.body.appendChild(panel);
343
+
344
+ toggleBtn.addEventListener('click', () => {
345
+ panel.classList.add('open');
346
+ toggleBtn.style.display = 'none';
347
+ });
348
+
349
+ panel.querySelector('.debug-close').addEventListener('click', () => {
350
+ panel.classList.remove('open');
351
+ toggleBtn.style.display = 'block';
352
+ });
353
+
354
+ const tabs = panel.querySelectorAll('.debug-tab');
355
+ const panes = panel.querySelectorAll('.debug-pane');
356
+ tabs.forEach(tab => {
357
+ tab.addEventListener('click', () => {
358
+ tabs.forEach(t => t.classList.remove('active'));
359
+ panes.forEach(p => p.classList.remove('active'));
360
+
361
+ tab.classList.add('active');
362
+ panel.querySelector('#pane-' + tab.getAttribute('data-tab')).classList.add('active');
363
+ });
364
+ });
365
+
366
+ const highlightElement = (el, label) => {
367
+ if (!el) return;
368
+ const rect = el.getBoundingClientRect();
369
+ const scrollY = window.scrollY;
370
+ const scrollX = window.scrollX;
371
+ highlight.style.top = (rect.top + scrollY) + 'px';
372
+ highlight.style.left = (rect.left + scrollX) + 'px';
373
+ highlight.style.width = rect.width + 'px';
374
+ highlight.style.height = rect.height + 'px';
375
+ highlight.style.display = 'block';
376
+ };
377
+
378
+ const removeHighlight = () => {
379
+ highlight.style.display = 'none';
380
+ };
381
+
382
+ const scanIslands = () => {
383
+ const list = panel.querySelector('#debug-islands-list');
384
+ const markers = Array.from(document.getElementsByTagName('elm-ssr-island'));
385
+
386
+ if (markers.length === 0) {
387
+ list.innerHTML = '<li style="color: #9ca3af; text-align: center; padding: 20px;">No interactive islands found on this page.</li>';
388
+ return;
389
+ }
390
+
391
+ list.innerHTML = '';
392
+ markers.forEach((marker, index) => {
393
+ const name = marker.getAttribute('data-elmssr-island') || 'Unknown';
394
+ const props = marker.getAttribute('data-elmssr-props') || '{}';
395
+ const isBooted = marker.getAttribute('data-elmssr-booted') === 'true';
396
+ const id = marker.getAttribute('data-elmssr-id') || 'none';
397
+
398
+ const li = document.createElement('li');
399
+ li.className = 'debug-item';
400
+ li.style.cursor = 'pointer';
401
+ li.innerHTML = \`
402
+ <div class="debug-item-meta" style="flex: 1;">
403
+ <div style="display: flex; justify-content: space-between; align-items: center;">
404
+ <strong style="color: #c084fc;">\${name}</strong>
405
+ <span class="debug-badge \${isBooted ? 'badge-success' : 'badge-warn'}">
406
+ \${isBooted ? 'BOOTED' : 'INITIALIZING'}
407
+ </span>
408
+ </div>
409
+ <div style="font-size: 0.8em; margin-top: 4px; color: #9ca3af;">
410
+ <span><strong>ID:</strong> \${id}</span>
411
+ </div>
412
+ <pre class="debug-code" style="font-size: 0.85em; max-height: 80px; overflow-y: auto;">\${JSON.stringify(JSON.parse(props), null, 2)}</pre>
413
+ </div>
414
+ \`;
415
+
416
+ li.addEventListener('mouseenter', () => highlightElement(marker, name));
417
+ li.addEventListener('mouseleave', removeHighlight);
418
+ list.appendChild(li);
419
+ });
420
+ };
421
+
422
+ panel.querySelector('[data-tab="islands"]').addEventListener('click', scanIslands);
423
+
424
+ const bridgeLog = panel.querySelector('#debug-bridges-log');
425
+ let firstBridge = true;
426
+ window.addEventListener('elm-ssr-broadcast', (event) => {
427
+ if (firstBridge) {
428
+ bridgeLog.innerHTML = '';
429
+ firstBridge = false;
430
+ }
431
+
432
+ const tag = event.detail && event.detail.tag || 'Unknown';
433
+ const payload = event.detail && event.detail.payload;
434
+ const time = new Date().toLocaleTimeString();
435
+
436
+ const li = document.createElement('li');
437
+ li.className = 'debug-item';
438
+ li.innerHTML = \`
439
+ <div class="debug-item-meta" style="flex: 1;">
440
+ <div style="display: flex; justify-content: space-between; align-items: center;">
441
+ <span class="debug-badge badge-success">\${tag}</span>
442
+ <span style="color: #9ca3af; font-size: 0.85em;">\${time}</span>
443
+ </div>
444
+ <pre class="debug-code" style="color: #fbbf24;">\${JSON.stringify(payload, null, 2)}</pre>
445
+ </div>
446
+ \`;
447
+
448
+ bridgeLog.insertBefore(li, bridgeLog.firstChild);
449
+ });
450
+
451
+ // Handle SPA live updates
452
+ const updatePanel = (newData) => {
453
+ const overviewPane = panel.querySelector('#pane-overview');
454
+ overviewPane.innerHTML = \`
455
+ <div class="overview-grid">
456
+ <div class="overview-card">
457
+ <div class="overview-label">Request Method & Path</div>
458
+ <div class="overview-val">\${newData.method} \${new URL(newData.url).pathname}</div>
459
+ </div>
460
+ <div class="overview-card">
461
+ <div class="overview-label">HTTP Status</div>
462
+ <div class="overview-val \${newData.status < 400 ? 'status-ok' : 'status-error'}">\${newData.status}</div>
463
+ </div>
464
+ <div class="overview-card">
465
+ <div class="overview-label">Server Render Time</div>
466
+ <div class="overview-val">\${newData.durationMs.toFixed(1)} ms</div>
467
+ </div>
468
+ <div class="overview-card">
469
+ <div class="overview-label">Database & Effects</div>
470
+ <div class="overview-val">\${(newData.effects || []).length} calls</div>
471
+ </div>
472
+ </div>
473
+ \`;
474
+
475
+ const effectsPane = panel.querySelector('#pane-effects');
476
+ let newEffectsHtml = '';
477
+ if (newData.effects && newData.effects.length > 0) {
478
+ newEffectsHtml = newData.effects.map(eff => {
479
+ const isSql = ['query', 'queryOne', 'execute'].includes(eff.kind);
480
+ const sqlSnippet = isSql ? \`<pre class="debug-code">\${eff.payload.sql}</pre>\` : '';
481
+ const paramsSnippet = isSql && eff.payload.params && eff.payload.params.length > 0 ?
482
+ \`<div><strong>Params:</strong> \${JSON.stringify(eff.payload.params)}</div>\` : '';
483
+ const outcome = eff.ok ? '<span class="status-ok">SUCCESS</span>' : '<span class="status-error">FAILED</span>';
484
+ const detailError = eff.error ? \`<div class="status-error">\${eff.error}</div>\` : '';
485
+ return \`
486
+ <li class="debug-item">
487
+ <div class="debug-item-meta" style="flex: 1;">
488
+ <div style="display: flex; gap: 8px; align-items: center;">
489
+ <span class="debug-badge badge-primary">\${eff.kind}</span>
490
+ \${outcome}
491
+ <span style="color: #9ca3af; font-size: 0.85em;">\${eff.durationMs.toFixed(1)}ms</span>
492
+ </div>
493
+ \${sqlSnippet}
494
+ \${paramsSnippet}
495
+ \${detailError}
496
+ </div>
497
+ </li>
498
+ \`;
499
+ }).join('');
500
+ effectsPane.innerHTML = \`<ul class="debug-list">\${newEffectsHtml}</ul>\`;
501
+ } else {
502
+ effectsPane.innerHTML = '<div style="color: #9ca3af; text-align: center; padding: 20px;">No server-side effects executed for this request.</div>';
503
+ }
504
+
505
+ const sessionPane = panel.querySelector('#pane-session');
506
+ sessionPane.innerHTML = \`
507
+ <div style="display: flex; flex-direction: column; gap: 12px;">
508
+ <div>
509
+ <strong>Session Data:</strong>
510
+ <pre class="debug-code" style="color: #818cf8;">\${JSON.stringify(newData.session, null, 2) || 'null'}</pre>
511
+ </div>
512
+ </div>
513
+ \`;
514
+
515
+ const activeTab = panel.querySelector('.debug-tab.active').getAttribute('data-tab');
516
+ if (activeTab === 'islands') {
517
+ scanIslands();
518
+ }
519
+ };
520
+
521
+ window.addEventListener('elm-ssr-debug-update', (event) => {
522
+ if (event.detail) {
523
+ updatePanel(event.detail);
524
+ }
525
+ });
526
+ })();
527
+ `;
528
+
529
+ const injectSnippet = `<script type="text/javascript">${scriptContent}</script>`;
530
+ const bodyCloseIndex = html.lastIndexOf("</body>");
531
+ if (bodyCloseIndex !== -1) {
532
+ return html.slice(0, bodyCloseIndex) + injectSnippet + html.slice(bodyCloseIndex);
533
+ }
534
+ return html + injectSnippet;
535
+ };
package/src/effects.ts CHANGED
@@ -19,6 +19,14 @@ export interface EffectContext {
19
19
  waitUntil?: (promise: Promise<unknown>) => void;
20
20
  /** Populated by `sessionMiddleware`; consumed by `sessionEffects`. */
21
21
  session?: import("./sessions/types").RequestSession;
22
+ debugLogs?: Array<{
23
+ kind: string;
24
+ payload: Record<string, unknown>;
25
+ ok: boolean;
26
+ value?: unknown;
27
+ error?: string;
28
+ durationMs: number;
29
+ }>;
22
30
  }
23
31
 
24
32
  /**