shokupan 0.9.0 → 0.10.1

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.
Files changed (49) hide show
  1. package/dist/analyzer-BqIe1p0R.js +35 -0
  2. package/dist/analyzer-BqIe1p0R.js.map +1 -0
  3. package/dist/analyzer-CKLGLFtx.cjs +35 -0
  4. package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
  5. package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
  6. package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
  7. package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
  8. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
  9. package/dist/cli.cjs +1 -1
  10. package/dist/cli.js +1 -1
  11. package/dist/context.d.ts +19 -7
  12. package/dist/http-server-BEMPIs33.cjs.map +1 -1
  13. package/dist/http-server-CCeagTyU.js.map +1 -1
  14. package/dist/index.cjs +1500 -275
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +1482 -256
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
  20. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
  21. package/dist/plugins/application/api-explorer/static/style.css +767 -0
  22. package/dist/plugins/application/api-explorer/static/theme.css +128 -0
  23. package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
  24. package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
  25. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
  26. package/dist/plugins/application/asyncapi/static/style.css +565 -0
  27. package/dist/plugins/application/asyncapi/static/theme.css +128 -0
  28. package/dist/plugins/application/auth.d.ts +3 -1
  29. package/dist/plugins/application/dashboard/metrics-collector.d.ts +3 -1
  30. package/dist/plugins/application/dashboard/plugin.d.ts +13 -3
  31. package/dist/plugins/application/dashboard/static/registry.css +0 -53
  32. package/dist/plugins/application/dashboard/static/styles.css +29 -20
  33. package/dist/plugins/application/dashboard/static/tabulator.css +83 -31
  34. package/dist/plugins/application/dashboard/static/theme.css +128 -0
  35. package/dist/plugins/application/graphql-apollo.d.ts +33 -0
  36. package/dist/plugins/application/graphql-yoga.d.ts +25 -0
  37. package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
  38. package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
  39. package/dist/plugins/application/scalar.d.ts +9 -2
  40. package/dist/router.d.ts +80 -51
  41. package/dist/shokupan.d.ts +14 -8
  42. package/dist/util/datastore.d.ts +71 -7
  43. package/dist/util/decorators.d.ts +2 -2
  44. package/dist/util/types.d.ts +96 -3
  45. package/package.json +32 -12
  46. package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
  47. package/dist/analyzer-Ce_7JxZh.js.map +0 -1
  48. package/dist/plugins/application/dashboard/static/scrollbar.css +0 -24
  49. package/dist/plugins/application/dashboard/template.eta +0 -246
@@ -0,0 +1,748 @@
1
+ const state = {
2
+ socket: null,
3
+ isConnected: false,
4
+ shouldAutoReconnect: true,
5
+ reconnectTimer: null,
6
+ protocol: 'ws',
7
+ spec: window.INITIAL_SPEC || null,
8
+ editor: null,
9
+ selectedEvent: null,
10
+ logEntries: [],
11
+ logAutoScroll: true,
12
+ isConsoleMaximized: false,
13
+ disableSourceView: !!window.DISABLE_SOURCE_VIEW
14
+ };
15
+
16
+ const els = {
17
+ url: document.getElementById('url'),
18
+ protocol: document.getElementById('protocol'),
19
+ connectBtn: document.getElementById('connect-btn'),
20
+ clearBtn: document.getElementById('clear-logs-btn'),
21
+ statusText: document.getElementById('connection-status'),
22
+ statusDot: document.getElementById('status-dot'),
23
+ logs: document.getElementById('logs'),
24
+ logShim: document.getElementById('log-shim'),
25
+ sendBtn: document.getElementById('send-btn'),
26
+ navList: document.getElementById('nav-list'),
27
+ docPanel: document.getElementById('doc-panel'),
28
+ targetEventLabel: document.getElementById('target-event'),
29
+ targetEventLabel: document.getElementById('target-event'),
30
+ showSourceToggle: document.getElementById('show-source-toggle'),
31
+ btnCollapseNav: document.getElementById('btn-collapse-nav'),
32
+ btnExpandNav: document.getElementById('btn-expand-nav'),
33
+ btnCollapseConsole: document.getElementById('btn-collapse-console'),
34
+ btnExpandConsole: document.getElementById('btn-expand-console'),
35
+ sidebar: document.getElementById('sidebar'),
36
+ resizerLeft: document.getElementById('resizer-left'),
37
+ resizerRight: document.getElementById('resizer-right'),
38
+ resizerRight: document.getElementById('resizer-right'),
39
+ consolePanel: document.getElementById('console-panel'),
40
+ mainWrapper: document.getElementById('main-wrapper'),
41
+ btnMaximizeConsole: document.getElementById('btn-maximize-console')
42
+ };
43
+
44
+ // Resizers
45
+ function initResizers() {
46
+ const setup = (id, varName, isLeft) => {
47
+ const el = document.getElementById(id);
48
+ if (!el) return;
49
+ el.addEventListener('mousedown', (e) => {
50
+ e.preventDefault();
51
+ const root = document.documentElement;
52
+ const startX = e.clientX;
53
+ const startW = parseInt(getComputedStyle(root).getPropertyValue(varName), 10);
54
+ document.body.style.cursor = 'col-resize';
55
+ el.classList.add('resizing');
56
+
57
+ const onMove = (em) => {
58
+ const diff = em.clientX - startX;
59
+ const newW = isLeft ? startW + diff : startW - diff;
60
+ if (newW > 100 && newW < 800) root.style.setProperty(varName, newW + 'px');
61
+ };
62
+ const onUp = () => {
63
+ document.removeEventListener('mousemove', onMove);
64
+ document.removeEventListener('mouseup', onUp);
65
+ document.body.style.cursor = '';
66
+ el.classList.remove('resizing');
67
+ };
68
+ document.addEventListener('mousemove', onMove);
69
+ document.addEventListener('mouseup', onUp);
70
+ });
71
+ };
72
+ setup('resizer-left', '--sidebar-width', true);
73
+ setup('resizer-right', '--console-width', false);
74
+ }
75
+
76
+ function toggleConsoleMaximize() {
77
+ state.isConsoleMaximized = !state.isConsoleMaximized;
78
+ const btn = els.btnMaximizeConsole;
79
+
80
+ if (state.isConsoleMaximized) {
81
+ // Maximize
82
+ els.mainWrapper.style.display = 'none';
83
+ els.resizerRight.style.display = 'none';
84
+ els.consolePanel.style.flex = '1';
85
+ els.consolePanel.style.width = 'auto'; // Reset width if resized
86
+
87
+ // Update Icon to Restore
88
+ btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 14 10 14 10 20"></polyline><polyline points="20 10 14 10 14 4"></polyline><line x1="14" y1="10" x2="21" y2="3"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg>`;
89
+ btn.title = "Restore Console";
90
+ } else {
91
+ // Restore
92
+ els.mainWrapper.style.display = 'block'; // Or flex/whatever logic
93
+ // Actually main-wrapper was display:flex via style attribute in HTML,
94
+ // but let's check if we hid it. display='none' hides it.
95
+ // We can just set it empty to revert to stylesheet or inline default.
96
+ els.mainWrapper.style.display = '';
97
+ els.resizerRight.style.display = 'block';
98
+ els.consolePanel.style.flex = ''; // Revert to CSS default
99
+ els.consolePanel.style.width = ''; // Revert width
100
+
101
+ // Update Icon to Maximize
102
+ btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>`;
103
+ btn.title = "Maximize Console";
104
+ }
105
+ }
106
+
107
+ // Hydrate Navigation
108
+ function hydrateNav() {
109
+ const items = document.querySelectorAll('.tree-item[data-event]');
110
+ items.forEach(el => {
111
+ el.addEventListener('click', () => {
112
+ const eventName = el.dataset.event;
113
+ selectEvent(eventName, el);
114
+ });
115
+ });
116
+ }
117
+
118
+ function resolveItem(name) {
119
+ if (!state.spec || !state.spec.channels) return null;
120
+ const ch = state.spec.channels[name];
121
+ if (!ch) return null;
122
+
123
+ // Logic matching buildNavTree:
124
+ const op = ch.publish || ch.subscribe;
125
+ const type = ch.publish ? 'publish' : 'subscribe';
126
+ return { name, op, type };
127
+ }
128
+
129
+ // Initialize Monaco
130
+ require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
131
+ require(['vs/editor/editor.main'], function () {
132
+ initResizers(); // Init resizers
133
+ hydrateNav(); // Hydrate pre-rendered nav
134
+
135
+ // Virtual scroll listener
136
+ els.logs.addEventListener('scroll', () => {
137
+ const diff = els.logs.scrollHeight - els.logs.scrollTop - els.logs.clientHeight;
138
+ // User requested 10px threshold for sticky behavior
139
+ state.logAutoScroll = diff <= 10;
140
+ renderLogs();
141
+ });
142
+
143
+ els.clearBtn.onclick = () => {
144
+ state.logEntries = [];
145
+ renderLogs();
146
+ };
147
+
148
+ state.editor = monaco.editor.create(document.getElementById('editor-container'), {
149
+ value: '{\n "key": "value"\n}',
150
+ language: 'json',
151
+ theme: 'vs-dark',
152
+ minimap: { enabled: false },
153
+ lineNumbers: 'off',
154
+ folding: false,
155
+ glyphMargin: false,
156
+ lineDecorationsWidth: 0,
157
+ lineNumbersMinChars: 0,
158
+ padding: { top: 10, bottom: 10 },
159
+ fontSize: 12,
160
+ scrollBeyondLastLine: false,
161
+ automaticLayout: true,
162
+ backgroundColor: 'transparent'
163
+ });
164
+
165
+ // Auto-connect if URL is present (or wait for user?)
166
+ // Original script called connect() immediately.
167
+ // Toggles
168
+ if (els.btnCollapseNav) els.btnCollapseNav.onclick = () => {
169
+ els.sidebar.style.display = 'none';
170
+ els.resizerLeft.style.display = 'none';
171
+ els.btnExpandNav.style.display = 'flex';
172
+ };
173
+ if (els.btnExpandNav) els.btnExpandNav.onclick = () => {
174
+ els.sidebar.style.display = 'flex';
175
+ els.resizerLeft.style.display = 'block';
176
+ els.btnExpandNav.style.display = 'none';
177
+ };
178
+
179
+ if (els.btnCollapseConsole) els.btnCollapseConsole.onclick = () => {
180
+ // If maximized, restore first to ensure main content/buttons are visible
181
+ if (state.isConsoleMaximized) toggleConsoleMaximize();
182
+
183
+ els.consolePanel.style.display = 'none';
184
+ els.resizerRight.style.display = 'none';
185
+ els.btnExpandConsole.style.display = 'flex';
186
+ };
187
+ if (els.btnExpandConsole) els.btnExpandConsole.onclick = () => {
188
+ els.consolePanel.style.display = 'flex';
189
+ els.resizerRight.style.display = 'block';
190
+ els.btnExpandConsole.style.display = 'none';
191
+
192
+ // Reset maximize state if it was maximized
193
+ if (state.isConsoleMaximized) toggleConsoleMaximize();
194
+ };
195
+
196
+ if (els.btnMaximizeConsole) els.btnMaximizeConsole.onclick = toggleConsoleMaximize;
197
+
198
+ connect();
199
+ });
200
+
201
+ /* ================= Targeted Highlighting Helper ================= */
202
+ function applyEmitHighlight(decorations, src) {
203
+ // Apply emit-specific highlighting if available
204
+ if (src.emitHighlightLines) {
205
+ let startLine = src.emitHighlightLines[0];
206
+ let endLine = src.emitHighlightLines[1];
207
+
208
+ if (startLine > 0) {
209
+ decorations.push({
210
+ range: new monaco.Range(startLine, 1, endLine, 1),
211
+ options: {
212
+ isWholeLine: true,
213
+ className: 'emit-highlight'
214
+ }
215
+ });
216
+ }
217
+ }
218
+ }
219
+
220
+ /* ================= Schema & Doc Rendering ================= */
221
+ async function selectEvent(name, el) {
222
+ const item = resolveItem(name);
223
+ if (!item) return;
224
+ document.querySelectorAll('.tree-item').forEach(n => n.classList.remove('active'));
225
+ if (el) el.classList.add('active');
226
+
227
+ state.selectedEvent = item;
228
+ els.targetEventLabel.innerText = item.name;
229
+
230
+ const op = item.op;
231
+ const isWarning = !!op['x-warning'];
232
+ // Fix: Leave description blank if missing, don't fallback to summary for body
233
+ const desc = op.description || '';
234
+ const payload = op.message?.payload;
235
+
236
+ if (isWarning) {
237
+ const sourceInfos = Array.isArray(op['x-source-info']) ? op['x-source-info'] : (op['x-source-info'] ? [op['x-source-info']] : []);
238
+
239
+ let sourceLinksHtml = '';
240
+ if (!state.disableSourceView && sourceInfos.length > 0) {
241
+ sourceLinksHtml = sourceInfos.map(s => {
242
+ const filename = s.file ? s.file.split('/').pop() : 'unknown';
243
+ return `<a href="vscode://file/${s.file}:${s.line}" style="color: #fbbf24; text-decoration: underline; font-family: monospace; display: block;">
244
+ ${filename}:${s.line}
245
+ </a>`;
246
+ }).join('');
247
+ }
248
+
249
+ els.docPanel.innerHTML = `
250
+ <div class="doc-header" style="border-bottom: 2px solid #fbbf24;">
251
+ <h1 class="doc-title" style="color: #fbbf24;">⚠️ ${item.name}</h1>
252
+ <div class="doc-meta">
253
+ <span class="badge warning" style="background: #fbbf24; color: #000; font-size: 0.8rem; padding: 4px 8px;">WARNING</span>
254
+ </div>
255
+ </div>
256
+ <div class="doc-body">
257
+ <div class="alert warning" style="background: rgba(251, 191, 36, 0.1); border: 1px solid rgba(251, 191, 36, 0.2); border-radius: 6px; padding: 16px; margin-bottom: 24px;">
258
+ <p style="margin: 0; color: #fbbf24; font-weight: 500;">
259
+ ${op.summary || 'Possible Issue Detected'}
260
+ </p>
261
+ <p style="margin: 8px 0 0 0; opacity: 0.8; line-height: 1.5;">
262
+ ${desc}
263
+ </p>
264
+ <p style="margin: 12px 0 0 0;">
265
+ ${sourceLinksHtml}
266
+ </p>
267
+ </div>
268
+
269
+ ${!state.disableSourceView && sourceInfos.length > 0 ? `
270
+ <div class="section-title">Source Context</div>
271
+ <div id="snippet-container"></div>
272
+ ` : ''}
273
+ </div>
274
+ `;
275
+
276
+ // Render snippet editors
277
+ if (!state.disableSourceView && window.monaco && sourceInfos.length > 0) {
278
+ const container = document.getElementById('snippet-container');
279
+ for (let i = 0; i < sourceInfos.length; i++) {
280
+ const src = sourceInfos[i];
281
+
282
+ let code = null;
283
+ if (src.file) {
284
+ try {
285
+ const res = await fetch(`./_code?file=${encodeURIComponent(src.file)}`);
286
+ if (res.ok) code = await res.text();
287
+ else code = `// Failed to load source: ${res.statusText}`;
288
+ } catch (e) { code = `// Error loading source: ${e.message}`; }
289
+ }
290
+
291
+ if (code) {
292
+ const wrapper = document.createElement('div');
293
+ wrapper.style.marginBottom = '16px';
294
+ wrapper.innerHTML = `<div style="font-size: 0.8rem; color: #888; margin-bottom: 4px;">${src.file.split('/').pop()}:${src.line}</div>
295
+ <div id="snippet-editor-${i}" style="height: 300px; border: 1px solid #333; border-radius: 6px; overflow: hidden;"></div>`;
296
+ container.appendChild(wrapper);
297
+
298
+ monaco.editor.colorize(code, 'typescript', {}).then(() => {
299
+ const el = document.getElementById(`snippet-editor-${i}`);
300
+ if (!el) return;
301
+
302
+ const model = monaco.editor.createModel(code, "typescript");
303
+
304
+ el.style.height = '400px';
305
+
306
+ const editor = monaco.editor.create(el, {
307
+ model: model,
308
+ readOnly: true,
309
+ theme: 'vs-dark',
310
+ minimap: { enabled: true },
311
+ glyphMargin: true,
312
+ folding: false,
313
+ lineNumbers: 'on',
314
+ fontSize: 12,
315
+ scrollBeyondLastLine: false,
316
+ automaticLayout: true,
317
+ backgroundColor: 'transparent'
318
+ });
319
+
320
+ // Apply highlighting
321
+ const decorations = [];
322
+
323
+ // Highlight the warning lines if specified
324
+ if (src.highlightLines) {
325
+ let startLine = src.highlightLines[0];
326
+ let endLine = src.highlightLines[1];
327
+
328
+ if (startLine > 0) {
329
+ decorations.push({
330
+ range: new monaco.Range(startLine, 1, endLine, 1),
331
+ options: {
332
+ isWholeLine: true,
333
+ className: 'warning-line-highlight',
334
+ glyphMarginClassName: 'warning-glyph'
335
+ }
336
+ });
337
+ editor.revealLineInCenter(startLine);
338
+ }
339
+ }
340
+
341
+ editor.deltaDecorations([], decorations);
342
+ });
343
+ }
344
+ }
345
+ }
346
+ return;
347
+ }
348
+
349
+ // Source Link for Doc Header
350
+ const sourceInfos = Array.isArray(op['x-source-info']) ? op['x-source-info'] : (op['x-source-info'] ? [op['x-source-info']] : []);
351
+
352
+ let sourceLinkHtml = '';
353
+ if (!state.disableSourceView && sourceInfos.length > 0) {
354
+ // Show only first one in header or a "View Sources" dropdown?
355
+ // For simplicity, let's show the first one if length is 1, else "x Sources"
356
+ if (sourceInfos.length === 1) {
357
+ const s = sourceInfos[0];
358
+ const filename = s.file.split('/').pop();
359
+ sourceLinkHtml = `<a href="vscode://file/${s.file}:${s.line}" class="doc-source-link" title="${s.file}:${s.line}">
360
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px">
361
+ <polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline>
362
+ </svg>
363
+ ${filename}:${s.line}
364
+ </a>`;
365
+ } else {
366
+ sourceLinkHtml = `<div class="doc-source-link" title="Multiple sources">
367
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px">
368
+ <polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline>
369
+ </svg>
370
+ ${sourceInfos.length} Locations
371
+ </div>`;
372
+ }
373
+ }
374
+
375
+ els.docPanel.innerHTML = `
376
+ <div class="doc-header">
377
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 0.5rem;">
378
+ <h1 class="doc-title" style="margin:0">${item.name}</h1>
379
+ ${sourceLinkHtml}
380
+ </div>
381
+ <div class="doc-meta">
382
+ <span class="badge badge-${item.type === 'publish' ? 'SEND' : 'RECV'}" style="font-size: 0.8rem; padding: 4px 8px;">${item.type === 'publish' ? 'SEND' : 'RECV'}</span>
383
+ <span>${op.operationId || ''}</span>
384
+ </div>
385
+ </div>
386
+ <div class="doc-body">
387
+ ${desc ? `<p style="line-height: 1.6; margin-bottom: 2rem;">${desc}</p>` : ''}
388
+
389
+ <div class="section-title">Payload Schema</div>
390
+ ${payload ? renderSchemaToDOM(payload) : '<div class="empty-state-text" style="color:var(--text-muted); font-style:italic;">No payload definition.</div>'}
391
+
392
+ ${!state.disableSourceView && sourceInfos.length > 0 ? `
393
+ <div class="section-title" style="margin-top: 24px;">Source Code</div>
394
+ <div id="source-viewer-container"></div>
395
+ ` : ''}
396
+ </div>
397
+ `;
398
+
399
+ // Render Source Viewers
400
+ if (!state.disableSourceView && sourceInfos.length > 0 && window.monaco) {
401
+ const container = document.getElementById('source-viewer-container');
402
+ container.innerHTML = '';
403
+
404
+ // Group by file
405
+ const grouped = {};
406
+ sourceInfos.forEach(s => {
407
+ if (!s.file) return;
408
+ if (!grouped[s.file]) grouped[s.file] = [];
409
+ grouped[s.file].push(s);
410
+ });
411
+ const files = Object.keys(grouped);
412
+
413
+ // Render Tabs if multiple files
414
+ if (files.length > 1) {
415
+ const tabBar = document.createElement('div');
416
+ tabBar.style.display = 'flex';
417
+ tabBar.style.gap = '8px';
418
+ tabBar.style.marginBottom = '12px';
419
+ tabBar.style.borderBottom = '1px solid var(--border-color)';
420
+ tabBar.style.paddingBottom = '8px';
421
+
422
+ files.forEach((f, idx) => {
423
+ const tab = document.createElement('button');
424
+ tab.className = idx === 0 ? 'btn' : 'btn secondary';
425
+ tab.style.padding = '4px 12px';
426
+ tab.style.fontSize = '0.8rem';
427
+ tab.innerText = f.split('/').pop();
428
+ tab.onclick = () => {
429
+ // Toggle active state
430
+ Array.from(tabBar.children).forEach(b => b.className = 'btn secondary');
431
+ tab.className = 'btn';
432
+ // Toggle visibility
433
+ files.forEach((_, otherIdx) => {
434
+ const el = document.getElementById(`source-group-${otherIdx}`);
435
+ if (el) el.style.display = otherIdx === idx ? 'flex' : 'none';
436
+ });
437
+ };
438
+ tabBar.appendChild(tab);
439
+ });
440
+ container.appendChild(tabBar);
441
+ }
442
+
443
+ // Render Editors
444
+ for (let i = 0; i < files.length; i++) {
445
+ const fileName = files[i];
446
+ const sources = grouped[fileName];
447
+
448
+ const wrapper = document.createElement('div');
449
+ wrapper.id = `source-group-${i}`;
450
+ wrapper.classList.add('source-group');
451
+ wrapper.style.display = i === 0 ? 'flex' : 'none';
452
+
453
+ wrapper.innerHTML = `<div class="source-header-actions" style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
454
+ <a href="vscode://file/${fileName}:${sources[0].line}" class="doc-source-link" title="${fileName}:${sources[0].line}">
455
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px">
456
+ <polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline>
457
+ </svg>
458
+ ${fileName.split('/').pop()}:${sources[0].line}
459
+ </a>
460
+ <button class="btn-icon" title="Toggle Fullscreen" onclick="toggleFullscreen('source-group-${i}', 'source-editor-${i}')">
461
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
462
+ <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path>
463
+ </svg>
464
+ </button>
465
+ </div>
466
+ <div id="source-editor-${i}" style="height: 100%; border: 1px solid #333; border-radius: 6px; overflow: hidden;"></div>`;
467
+ container.appendChild(wrapper);
468
+
469
+ (async () => {
470
+ let code = null;
471
+ try {
472
+ const res = await fetch(`./_code?file=${encodeURIComponent(fileName)}`);
473
+ if (res.ok) code = await res.text();
474
+ else code = `// Failed to load source: ${res.statusText}`;
475
+ } catch (e) { code = `// Error loading source: ${e.message}`; }
476
+
477
+ if (code) {
478
+ const el = document.getElementById(`source-editor-${i}`);
479
+ if (!el) return;
480
+ const model = monaco.editor.createModel(code, "typescript");
481
+ const editor = monaco.editor.create(el, {
482
+ model: model,
483
+ readOnly: true,
484
+ theme: 'vs-dark',
485
+ minimap: { enabled: true },
486
+ glyphMargin: false,
487
+ folding: false,
488
+ lineNumbers: 'on',
489
+ fontSize: 12,
490
+ scrollBeyondLastLine: false,
491
+ automaticLayout: true,
492
+ backgroundColor: 'transparent'
493
+ });
494
+
495
+ // Aggregate Highlights
496
+ const decorations = [];
497
+ let firstScrollLine = null;
498
+
499
+ sources.forEach(src => {
500
+ // Determine potential scroll target
501
+ const scrollLine = src.emitHighlightLines ? src.emitHighlightLines[0] : (src.highlightLines ? src.highlightLines[0] : 1);
502
+ if (!firstScrollLine && scrollLine > 1) {
503
+ firstScrollLine = scrollLine;
504
+ }
505
+
506
+ // Apply Context Highlight (if no emit highlight for this specific entry)
507
+ if (src.highlightLines && !src.emitHighlightLines) {
508
+ let startLine = src.highlightLines[0];
509
+ let endLine = src.highlightLines[1];
510
+ if (startLine > 0) {
511
+ decorations.push({
512
+ range: new monaco.Range(startLine, 1, endLine, 1),
513
+ options: {
514
+ isWholeLine: true,
515
+ className: 'closure-highlight'
516
+ }
517
+ });
518
+ }
519
+ }
520
+
521
+ // Apply Emit Highlight
522
+ applyEmitHighlight(decorations, src);
523
+ });
524
+
525
+ // Scroll to the first relevant line found
526
+ if (firstScrollLine) {
527
+ editor.revealLineInCenter(firstScrollLine);
528
+ }
529
+
530
+ editor.deltaDecorations([], decorations);
531
+ }
532
+ })();
533
+ }
534
+ }
535
+
536
+ // Scaffold Editor
537
+ if (item.type === 'publish') {
538
+ let scaffold = "{}";
539
+ if (payload && payload.properties) {
540
+ const obj = {};
541
+ Object.keys(payload.properties).forEach(k => {
542
+ obj[k] = payload.properties[k].example || (payload.properties[k].type === 'number' ? 0 : "");
543
+ });
544
+ scaffold = JSON.stringify(obj, null, 2);
545
+ }
546
+ if (state.editor) state.editor.setValue(scaffold);
547
+ }
548
+ }
549
+
550
+ function renderSchemaToDOM(schema) {
551
+ if (!schema || schema.type !== 'object') {
552
+ return `<div class="code-block">${JSON.stringify(schema, null, 2)}</div>`;
553
+ }
554
+
555
+ let html = '<div class="schema-root">';
556
+
557
+ function renderProps(props, required = []) {
558
+ let out = '';
559
+ Object.keys(props).forEach(key => {
560
+ const prop = props[key];
561
+ const isReq = required.includes(key);
562
+ const type = prop.type || 'any';
563
+ const desc = prop.description || '';
564
+
565
+ out += `
566
+ <div class="schema-row">
567
+ <div class="schema-prop">
568
+ ${key} ${isReq ? '<span class="prop-req">*</span>' : ''}
569
+ </div>
570
+ <div style="flex: 1;">
571
+ <div style="display:flex; align-items:baseline;">
572
+ <span class="schema-type">${type}</span>
573
+ <span class="schema-desc">${desc}</span>
574
+ </div>
575
+ ${prop.properties ? `<div class="nested-schema">${renderProps(prop.properties, prop.required)}</div>` : ''}
576
+ </div>
577
+ </div>`;
578
+ });
579
+ return out;
580
+ }
581
+
582
+ if (schema.properties) {
583
+ html += renderProps(schema.properties, schema.required);
584
+ }
585
+ html += '</div>';
586
+ return html;
587
+ }
588
+
589
+ /* ================= Console & Utils ================= */
590
+ const ROW_HEIGHT = 28;
591
+
592
+ function renderLogs() {
593
+ const container = els.logs;
594
+ const total = state.logEntries.length;
595
+ els.logShim.style.height = (total * ROW_HEIGHT) + 'px';
596
+
597
+ // Calculate visible range
598
+ const scrollTop = container.scrollTop;
599
+ const clientHeight = container.clientHeight;
600
+
601
+ const startNode = Math.floor(scrollTop / ROW_HEIGHT);
602
+ // Buffer of 2 items
603
+ const startIndex = Math.max(0, startNode - 2);
604
+ const endIndex = Math.min(total, Math.ceil((scrollTop + clientHeight) / ROW_HEIGHT) + 2);
605
+
606
+ // Remove existing entries
607
+ // Note: We keep log-shim (which usually has ID)
608
+ // We can select all .log-entry`
609
+ const entries = container.getElementsByClassName('log-entry');
610
+ while (entries.length > 0) {
611
+ entries[0].remove();
612
+ }
613
+
614
+ for (let i = startIndex; i < endIndex; i++) {
615
+ const entry = state.logEntries[i];
616
+ if (!entry) continue;
617
+
618
+ const div = document.createElement('div');
619
+ div.className = 'log-entry ' + entry.type;
620
+ div.style.top = (i * ROW_HEIGHT) + 'px';
621
+ div.style.height = ROW_HEIGHT + 'px';
622
+ div.style.overflow = 'hidden';
623
+ div.style.whiteSpace = 'nowrap';
624
+ div.style.textOverflow = 'ellipsis';
625
+ div.style.display = 'flex';
626
+ div.style.alignItems = 'center';
627
+
628
+ const icons = {
629
+ in: `<svg width="24px" height="24px" viewBox="0 0 24 24" fill="#7986cb"><path d="M17.707 6.293a1 1 0 0 1 0 1.414L9.414 16H15a1 1 0 1 1 0 2H7a1 1 0 0 1-1-1V9a1 1 0 1 1 2 0v5.586l8.293-8.293a1 1 0 0 1 1.414 0z"/></svg>`,
630
+ out: `<svg width="24px" height="24px" viewBox="0 0 24 24" fill="#4caf50"><path d="M8 7a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v8a1 1 0 1 1-2 0V9.414l-8.293 8.293a1 1 0 0 1-1.414-1.414L14.586 8H9a1 1 0 0 1-1-1z"/></svg>`,
631
+ error: `<svg width="24px" height="24px" viewBox="0 0 24 24" fill="#ff5722"><path d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12zm5.793-4.207a1 1 0 0 1 1.414 0L12 10.586l2.793-2.793a1 1 0 1 1 1.414 1.414L13.414 12l2.793 2.793a1 1 0 0 1-1.414 1.414L12 13.414l-2.793 2.793a1 1 0 0 1-1.414-1.414L10.586 12 7.793 9.207a1 1 0 0 1 0-1.414z"/></svg>`,
632
+ info: `<svg width="24px" height="24px" viewBox="0 0 24 24" fill="#03a9f4"><path d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12z"/><path d="M12 10a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-6a1 1 0 0 1 1-1zm1.5-2.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>`
633
+ };
634
+ // Escape HTML in msg
635
+ div.innerHTML = `<span>${icons[entry.type]}</span><span class="log-time">${entry.time}</span><span class="log-content"></span>`;
636
+
637
+ const logContent = div.querySelector('.log-content');
638
+ logContent.title = entry.msg;
639
+ logContent.innerText = entry.msg;
640
+
641
+ container.appendChild(div);
642
+ }
643
+
644
+ // Auto-scroll logic
645
+ if (state.logAutoScroll && total > 0) {
646
+ if (container.scrollTop + clientHeight < els.logShim.offsetHeight) {
647
+ container.scrollTop = els.logShim.offsetHeight - clientHeight;
648
+ }
649
+ }
650
+ }
651
+
652
+ function log(source, msg, type = 'info') {
653
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false });
654
+ state.logEntries.push({ source, msg, type, time });
655
+
656
+ // If we are already near bottom, keep auto-scroll true
657
+ const container = els.logs;
658
+ const diff = container.scrollHeight - container.scrollTop - container.clientHeight;
659
+ if (diff < 50) state.logAutoScroll = true;
660
+
661
+ renderLogs();
662
+ }
663
+
664
+ function updateStatus() {
665
+ if (state.isConnected) {
666
+ els.statusText.innerText = 'Connected';
667
+ els.statusText.style.color = '#10b981';
668
+ els.statusDot.className = 'dot connected';
669
+ els.connectBtn.innerText = 'Disconnect';
670
+ els.connectBtn.className = 'btn secondary';
671
+ } else {
672
+ els.statusText.innerText = 'Disconnected';
673
+ els.statusText.style.color = '#666';
674
+ els.statusDot.className = 'dot';
675
+ els.connectBtn.innerText = 'Connect';
676
+ els.connectBtn.className = 'btn';
677
+ }
678
+ }
679
+
680
+ function connect() {
681
+ const url = els.url.value;
682
+ state.protocol = els.protocol.value;
683
+ if (state.reconnectTimer) clearTimeout(state.reconnectTimer);
684
+
685
+ const isWs = state.protocol === 'ws' || state.protocol === 'wss';
686
+ const fullUrl = (isWs ? (url.startsWith('ws') ? url : state.protocol + '://' + url) : (url.startsWith('http') ? url : 'http://' + url));
687
+ log('System', `Connecting to ${fullUrl}...`);
688
+
689
+ if (isWs) {
690
+ try {
691
+ state.socket = new WebSocket(fullUrl);
692
+ state.socket.onopen = () => {
693
+ state.isConnected = true;
694
+ updateStatus();
695
+ log('System', 'Connected', 'in');
696
+ // No need to loadSpec again here, handled by initial load
697
+ };
698
+ state.socket.onclose = () => {
699
+ if (state.isConnected) log('System', 'Disconnected');
700
+ state.isConnected = false;
701
+ updateStatus();
702
+ if (state.shouldAutoReconnect) scheduleReconnect();
703
+ };
704
+ state.socket.onerror = () => log('System', 'Connection Error', 'error');
705
+ state.socket.onmessage = (e) => log('Server', e.data, 'in');
706
+ } catch (e) { log('System', e.message, 'error'); }
707
+ } else {
708
+ state.socket = io(fullUrl, { transports: ['websocket'] });
709
+ state.socket.on('connect', () => { state.isConnected = true; updateStatus(); log('System', `Connected (${state.socket.id})`, 'in'); });
710
+ state.socket.on('disconnect', () => { state.isConnected = false; updateStatus(); log('System', 'Disconnected'); });
711
+ state.socket.onAny((e, ...args) => log('Server', `${e}: ${JSON.stringify(args)}`, 'in'));
712
+ }
713
+ }
714
+
715
+ function disconnect() {
716
+ state.shouldAutoReconnect = false;
717
+ if (state.socket) {
718
+ (state.protocol === 'ws' || state.protocol === 'wss') ? state.socket.close() : state.socket.disconnect();
719
+ }
720
+ }
721
+
722
+ function scheduleReconnect() {
723
+ if (state.reconnectTimer) return;
724
+ els.statusText.innerText = 'Reconnecting...';
725
+ state.reconnectTimer = setTimeout(() => { state.reconnectTimer = null; connect(); }, 3000);
726
+ }
727
+
728
+ els.connectBtn.onclick = () => {
729
+ if (state.isConnected) disconnect();
730
+ else { state.shouldAutoReconnect = true; connect(); }
731
+ };
732
+
733
+ els.sendBtn.onclick = () => {
734
+ if (!state.isConnected) return log('System', 'Not connected', 'error');
735
+ if (!state.selectedEvent) return log('System', 'Select event', 'error');
736
+ try {
737
+ const body = JSON.parse(state.editor.getValue());
738
+ const evt = state.selectedEvent.name;
739
+ if (state.protocol === 'ws' || state.protocol === 'wss') {
740
+ const pay = JSON.stringify({ type: 'EVENT', event: evt, data: body });
741
+ state.socket.send(pay);
742
+ log('Client', pay, 'out');
743
+ } else {
744
+ state.socket.emit(evt, body);
745
+ log('Client', `${evt}: ${JSON.stringify(body)}`, 'out');
746
+ }
747
+ } catch (e) { log('System', 'Invalid JSON', 'error'); }
748
+ };