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.
- package/dist/analyzer-BqIe1p0R.js +35 -0
- package/dist/analyzer-BqIe1p0R.js.map +1 -0
- package/dist/analyzer-CKLGLFtx.cjs +35 -0
- package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
- package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
- package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
- package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +19 -7
- package/dist/http-server-BEMPIs33.cjs.map +1 -1
- package/dist/http-server-CCeagTyU.js.map +1 -1
- package/dist/index.cjs +1500 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1482 -256
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
- package/dist/plugins/application/api-explorer/static/style.css +767 -0
- package/dist/plugins/application/api-explorer/static/theme.css +128 -0
- package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
- package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
- package/dist/plugins/application/asyncapi/static/style.css +565 -0
- package/dist/plugins/application/asyncapi/static/theme.css +128 -0
- package/dist/plugins/application/auth.d.ts +3 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +3 -1
- package/dist/plugins/application/dashboard/plugin.d.ts +13 -3
- package/dist/plugins/application/dashboard/static/registry.css +0 -53
- package/dist/plugins/application/dashboard/static/styles.css +29 -20
- package/dist/plugins/application/dashboard/static/tabulator.css +83 -31
- package/dist/plugins/application/dashboard/static/theme.css +128 -0
- package/dist/plugins/application/graphql-apollo.d.ts +33 -0
- package/dist/plugins/application/graphql-yoga.d.ts +25 -0
- package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
- package/dist/plugins/application/scalar.d.ts +9 -2
- package/dist/router.d.ts +80 -51
- package/dist/shokupan.d.ts +14 -8
- package/dist/util/datastore.d.ts +71 -7
- package/dist/util/decorators.d.ts +2 -2
- package/dist/util/types.d.ts +96 -3
- package/package.json +32 -12
- package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
- package/dist/analyzer-Ce_7JxZh.js.map +0 -1
- package/dist/plugins/application/dashboard/static/scrollbar.css +0 -24
- 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
|
+
};
|