binja 0.5.3 → 0.6.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/debug/collector.d.ts +43 -0
- package/dist/debug/collector.d.ts.map +1 -1
- package/dist/debug/index.d.ts +2 -1
- package/dist/debug/index.d.ts.map +1 -1
- package/dist/debug/index.js +628 -308
- package/dist/debug/integrations.d.ts +104 -0
- package/dist/debug/integrations.d.ts.map +1 -0
- package/dist/debug/panel.d.ts +10 -8
- package/dist/debug/panel.d.ts.map +1 -1
- package/dist/index.js +473 -310
- package/package.json +1 -1
package/dist/debug/index.js
CHANGED
|
@@ -14,6 +14,14 @@ class DebugCollector {
|
|
|
14
14
|
testsUsed: new Map,
|
|
15
15
|
cacheHits: 0,
|
|
16
16
|
cacheMisses: 0,
|
|
17
|
+
queries: [],
|
|
18
|
+
queryStats: {
|
|
19
|
+
count: 0,
|
|
20
|
+
totalDuration: 0,
|
|
21
|
+
slowCount: 0,
|
|
22
|
+
n1Count: 0,
|
|
23
|
+
queryCounts: new Map
|
|
24
|
+
},
|
|
17
25
|
warnings: []
|
|
18
26
|
};
|
|
19
27
|
}
|
|
@@ -154,6 +162,33 @@ class DebugCollector {
|
|
|
154
162
|
addWarning(message) {
|
|
155
163
|
this.data.warnings.push(message);
|
|
156
164
|
}
|
|
165
|
+
recordQuery(query) {
|
|
166
|
+
const normalizedSql = this.normalizeQuery(query.sql);
|
|
167
|
+
const currentCount = this.data.queryStats.queryCounts.get(normalizedSql) || 0;
|
|
168
|
+
this.data.queryStats.queryCounts.set(normalizedSql, currentCount + 1);
|
|
169
|
+
const isN1 = currentCount >= 2;
|
|
170
|
+
const queryInfo = {
|
|
171
|
+
...query,
|
|
172
|
+
timestamp: performance.now(),
|
|
173
|
+
isN1
|
|
174
|
+
};
|
|
175
|
+
this.data.queries.push(queryInfo);
|
|
176
|
+
this.data.queryStats.count++;
|
|
177
|
+
this.data.queryStats.totalDuration += query.duration;
|
|
178
|
+
if (query.duration > 100) {
|
|
179
|
+
this.data.queryStats.slowCount++;
|
|
180
|
+
}
|
|
181
|
+
if (isN1 && currentCount === 2) {
|
|
182
|
+
this.data.queryStats.n1Count++;
|
|
183
|
+
this.addWarning(`N+1 query detected: ${normalizedSql.slice(0, 50)}...`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
normalizeQuery(sql) {
|
|
187
|
+
return sql.replace(/\s+/g, " ").replace(/= \?/g, "= ?").replace(/= \$\d+/g, "= ?").replace(/= '\w+'/g, "= '?'").replace(/= \d+/g, "= ?").replace(/IN \([^)]+\)/gi, "IN (?)").trim();
|
|
188
|
+
}
|
|
189
|
+
getQueryStats() {
|
|
190
|
+
return this.data.queryStats;
|
|
191
|
+
}
|
|
157
192
|
getData() {
|
|
158
193
|
return this.data;
|
|
159
194
|
}
|
|
@@ -185,388 +220,665 @@ function endDebugCollection() {
|
|
|
185
220
|
|
|
186
221
|
// src/debug/panel.ts
|
|
187
222
|
var DEFAULT_OPTIONS = {
|
|
188
|
-
position: "bottom
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
position: "bottom",
|
|
224
|
+
height: 300,
|
|
225
|
+
width: 400,
|
|
226
|
+
open: false,
|
|
227
|
+
dark: true
|
|
192
228
|
};
|
|
193
229
|
function generateDebugPanel(data, options = {}) {
|
|
194
230
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
195
|
-
const id = `binja-
|
|
196
|
-
const
|
|
231
|
+
const id = `binja-dbg-${Date.now()}`;
|
|
232
|
+
const c = opts.dark ? darkTheme : lightTheme;
|
|
197
233
|
return `
|
|
198
234
|
<!-- Binja Debug Panel -->
|
|
199
|
-
<div id="${id}" class="binja-
|
|
200
|
-
<style>${generateStyles(id,
|
|
201
|
-
${
|
|
202
|
-
|
|
203
|
-
<script>${generateScript(id)}</script>
|
|
235
|
+
<div id="${id}" class="binja-devtools" data-position="${opts.position}" data-open="${opts.open}">
|
|
236
|
+
<style>${generateStyles(id, c, opts)}</style>
|
|
237
|
+
${generateHTML(id, data, c, opts)}
|
|
238
|
+
<script>${generateScript(id, data, opts)}</script>
|
|
204
239
|
</div>
|
|
205
240
|
<!-- /Binja Debug Panel -->
|
|
206
241
|
`;
|
|
207
242
|
}
|
|
208
243
|
var darkTheme = {
|
|
209
|
-
bg: "#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
text: "#
|
|
215
|
-
textSecondary: "#
|
|
216
|
-
textMuted: "#
|
|
217
|
-
accent: "#
|
|
218
|
-
accentHover: "#
|
|
219
|
-
success: "#
|
|
220
|
-
warning: "#
|
|
221
|
-
error: "#
|
|
222
|
-
info: "#
|
|
244
|
+
bg: "#1e1e1e",
|
|
245
|
+
bgPanel: "#252526",
|
|
246
|
+
bgHover: "#2a2d2e",
|
|
247
|
+
bgActive: "#37373d",
|
|
248
|
+
border: "#3c3c3c",
|
|
249
|
+
text: "#cccccc",
|
|
250
|
+
textSecondary: "#969696",
|
|
251
|
+
textMuted: "#6e6e6e",
|
|
252
|
+
accent: "#0078d4",
|
|
253
|
+
accentHover: "#1c86d8",
|
|
254
|
+
success: "#4ec9b0",
|
|
255
|
+
warning: "#dcdcaa",
|
|
256
|
+
error: "#f14c4c",
|
|
257
|
+
info: "#75beff",
|
|
258
|
+
string: "#ce9178",
|
|
259
|
+
number: "#b5cea8",
|
|
260
|
+
keyword: "#569cd6"
|
|
223
261
|
};
|
|
224
262
|
var lightTheme = {
|
|
225
|
-
bg: "#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
text: "#
|
|
231
|
-
textSecondary: "#
|
|
232
|
-
textMuted: "#
|
|
233
|
-
accent: "#
|
|
234
|
-
accentHover: "#
|
|
235
|
-
success: "#
|
|
236
|
-
warning: "#
|
|
237
|
-
error: "#
|
|
238
|
-
info: "#
|
|
263
|
+
bg: "#f3f3f3",
|
|
264
|
+
bgPanel: "#ffffff",
|
|
265
|
+
bgHover: "#e8e8e8",
|
|
266
|
+
bgActive: "#d4d4d4",
|
|
267
|
+
border: "#d4d4d4",
|
|
268
|
+
text: "#1e1e1e",
|
|
269
|
+
textSecondary: "#616161",
|
|
270
|
+
textMuted: "#a0a0a0",
|
|
271
|
+
accent: "#0078d4",
|
|
272
|
+
accentHover: "#106ebe",
|
|
273
|
+
success: "#16825d",
|
|
274
|
+
warning: "#bf8803",
|
|
275
|
+
error: "#cd3131",
|
|
276
|
+
info: "#0078d4",
|
|
277
|
+
string: "#a31515",
|
|
278
|
+
number: "#098658",
|
|
279
|
+
keyword: "#0000ff"
|
|
239
280
|
};
|
|
240
281
|
function generateStyles(id, c, opts) {
|
|
241
|
-
const pos = getPosition(opts.position);
|
|
242
282
|
return `
|
|
243
|
-
#${id} { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
283
|
+
#${id} { --bg: ${c.bg}; --bg-panel: ${c.bgPanel}; --bg-hover: ${c.bgHover}; --bg-active: ${c.bgActive}; --border: ${c.border}; --text: ${c.text}; --text-secondary: ${c.textSecondary}; --text-muted: ${c.textMuted}; --accent: ${c.accent}; --success: ${c.success}; --warning: ${c.warning}; --error: ${c.error}; --string: ${c.string}; --number: ${c.number}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-size: 12px; line-height: 1.4; color: var(--text); }
|
|
244
284
|
#${id} * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
#${id} .
|
|
248
|
-
#${id} .
|
|
249
|
-
#${id} .
|
|
250
|
-
#${id} .
|
|
251
|
-
#${id} .
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
#${id} .
|
|
255
|
-
#${id} .
|
|
256
|
-
#${id} .
|
|
257
|
-
#${id} .
|
|
258
|
-
#${id} .
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
#${id} .
|
|
262
|
-
#${id} .
|
|
263
|
-
#${id} .
|
|
264
|
-
#${id} .
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
#${id} .
|
|
268
|
-
#${id} .
|
|
269
|
-
#${id} .
|
|
270
|
-
#${id} .
|
|
271
|
-
#${id} .
|
|
272
|
-
#${id} .
|
|
273
|
-
#${id} .
|
|
274
|
-
#${id} .
|
|
275
|
-
#${id} .
|
|
276
|
-
#${id} .
|
|
277
|
-
#${id} .
|
|
278
|
-
#${id} .
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
#${id} .
|
|
282
|
-
#${id} .
|
|
283
|
-
#${id} .
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
#${id} .
|
|
287
|
-
#${id} .
|
|
288
|
-
#${id} .
|
|
289
|
-
#${id} .
|
|
290
|
-
#${id} .
|
|
291
|
-
#${id} .
|
|
292
|
-
#${id} .
|
|
293
|
-
#${id} .
|
|
294
|
-
#${id} .
|
|
295
|
-
#${id} .
|
|
296
|
-
#${id} .
|
|
297
|
-
#${id} .
|
|
298
|
-
#${id} .
|
|
299
|
-
#${id} .
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
#${id} .
|
|
303
|
-
#${id} .
|
|
304
|
-
#${id} .
|
|
305
|
-
#${id} .
|
|
306
|
-
#${id} .
|
|
307
|
-
#${id} .
|
|
308
|
-
#${id} .
|
|
309
|
-
#${id} .
|
|
310
|
-
#${id} .
|
|
311
|
-
#${id} .
|
|
312
|
-
#${id} .
|
|
313
|
-
#${id} .
|
|
314
|
-
#${id} .
|
|
315
|
-
#${id} .
|
|
316
|
-
#${id} .
|
|
317
|
-
#${id} .
|
|
285
|
+
|
|
286
|
+
/* Toggle Button */
|
|
287
|
+
#${id} .devtools-toggle { position: fixed; bottom: 10px; right: 10px; z-index: 2147483646; display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text); cursor: pointer; font-size: 11px; font-weight: 500; box-shadow: 0 2px 8px rgba(0,0,0,0.2); transition: all 0.15s; }
|
|
288
|
+
#${id} .devtools-toggle:hover { background: var(--bg-hover); border-color: var(--accent); }
|
|
289
|
+
#${id} .devtools-toggle svg { width: 14px; height: 14px; color: var(--accent); }
|
|
290
|
+
#${id} .devtools-toggle .badge { padding: 2px 6px; background: var(--accent); color: #fff; border-radius: 3px; font-size: 10px; }
|
|
291
|
+
#${id}[data-open="true"] .devtools-toggle { display: none; }
|
|
292
|
+
|
|
293
|
+
/* Panel Container */
|
|
294
|
+
#${id} .devtools-panel { display: none; position: fixed; z-index: 2147483647; background: var(--bg); border: 1px solid var(--border); box-shadow: 0 -2px 12px rgba(0,0,0,0.3); }
|
|
295
|
+
#${id}[data-open="true"] .devtools-panel { display: flex; flex-direction: column; }
|
|
296
|
+
#${id}[data-position="bottom"] .devtools-panel { left: 0; right: 0; bottom: 0; height: ${opts.height}px; border-left: none; border-right: none; border-bottom: none; }
|
|
297
|
+
#${id}[data-position="right"] .devtools-panel { top: 0; right: 0; bottom: 0; width: ${opts.width}px; border-top: none; border-right: none; border-bottom: none; }
|
|
298
|
+
#${id}[data-position="popup"] .devtools-panel { bottom: 50px; right: 20px; width: 700px; height: 500px; border-radius: 8px; }
|
|
299
|
+
|
|
300
|
+
/* Resize Handle */
|
|
301
|
+
#${id} .devtools-resize { position: absolute; background: transparent; }
|
|
302
|
+
#${id}[data-position="bottom"] .devtools-resize { top: 0; left: 0; right: 0; height: 4px; cursor: ns-resize; }
|
|
303
|
+
#${id}[data-position="right"] .devtools-resize { top: 0; left: 0; bottom: 0; width: 4px; cursor: ew-resize; }
|
|
304
|
+
#${id} .devtools-resize:hover { background: var(--accent); }
|
|
305
|
+
|
|
306
|
+
/* Toolbar */
|
|
307
|
+
#${id} .devtools-toolbar { display: flex; align-items: center; justify-content: space-between; height: 30px; padding: 0 8px; background: var(--bg-panel); border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
308
|
+
#${id} .devtools-tabs { display: flex; height: 100%; }
|
|
309
|
+
#${id} .devtools-tab { display: flex; align-items: center; gap: 4px; padding: 0 12px; border: none; background: none; color: var(--text-secondary); font-size: 11px; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.1s; }
|
|
310
|
+
#${id} .devtools-tab:hover { color: var(--text); background: var(--bg-hover); }
|
|
311
|
+
#${id} .devtools-tab.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
312
|
+
#${id} .devtools-tab svg { width: 12px; height: 12px; opacity: 0.7; }
|
|
313
|
+
#${id} .devtools-tab .count { margin-left: 4px; padding: 1px 5px; background: var(--bg-active); border-radius: 8px; font-size: 10px; }
|
|
314
|
+
#${id} .devtools-tab .count.warn { background: rgba(241,76,76,0.2); color: var(--error); }
|
|
315
|
+
#${id} .devtools-actions { display: flex; align-items: center; gap: 4px; }
|
|
316
|
+
#${id} .devtools-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; background: none; color: var(--text-secondary); cursor: pointer; border-radius: 3px; }
|
|
317
|
+
#${id} .devtools-btn:hover { background: var(--bg-hover); color: var(--text); }
|
|
318
|
+
#${id} .devtools-btn svg { width: 14px; height: 14px; }
|
|
319
|
+
|
|
320
|
+
/* Content Area */
|
|
321
|
+
#${id} .devtools-content { flex: 1; overflow: hidden; }
|
|
322
|
+
#${id} .devtools-pane { display: none; height: 100%; overflow: auto; padding: 8px; }
|
|
323
|
+
#${id} .devtools-pane.active { display: block; }
|
|
324
|
+
|
|
325
|
+
/* Performance Tab */
|
|
326
|
+
#${id} .perf-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 12px; }
|
|
327
|
+
#${id} .perf-card { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 10px; text-align: center; }
|
|
328
|
+
#${id} .perf-card-value { font-size: 20px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; color: var(--success); }
|
|
329
|
+
#${id} .perf-card-label { font-size: 10px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; }
|
|
330
|
+
#${id} .perf-breakdown { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
331
|
+
#${id} .perf-row { display: flex; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
|
332
|
+
#${id} .perf-row:last-child { border-bottom: none; }
|
|
333
|
+
#${id} .perf-row-label { flex: 1; font-size: 11px; color: var(--text-secondary); }
|
|
334
|
+
#${id} .perf-row-value { font-family: 'SF Mono', Monaco, monospace; font-size: 11px; min-width: 60px; text-align: right; }
|
|
335
|
+
#${id} .perf-row-bar { flex: 2; height: 4px; background: var(--bg-active); border-radius: 2px; margin: 0 12px; overflow: hidden; }
|
|
336
|
+
#${id} .perf-row-fill { height: 100%; border-radius: 2px; }
|
|
337
|
+
#${id} .perf-row-fill.lexer { background: ${c.info}; }
|
|
338
|
+
#${id} .perf-row-fill.parser { background: ${c.warning}; }
|
|
339
|
+
#${id} .perf-row-fill.render { background: ${c.success}; }
|
|
340
|
+
|
|
341
|
+
/* Context Tab - Tree View */
|
|
342
|
+
#${id} .tree { font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 11px; }
|
|
343
|
+
#${id} .tree-item { }
|
|
344
|
+
#${id} .tree-row { display: flex; align-items: center; padding: 2px 4px; cursor: default; border-radius: 2px; }
|
|
345
|
+
#${id} .tree-row:hover { background: var(--bg-hover); }
|
|
346
|
+
#${id} .tree-row.expandable { cursor: pointer; }
|
|
347
|
+
#${id} .tree-arrow { width: 12px; height: 12px; color: var(--text-muted); transition: transform 0.1s; flex-shrink: 0; }
|
|
348
|
+
#${id} .tree-item.open > .tree-row .tree-arrow { transform: rotate(90deg); }
|
|
349
|
+
#${id} .tree-key { color: var(--keyword); margin-right: 4px; }
|
|
350
|
+
#${id} .tree-colon { color: var(--text-muted); margin-right: 6px; }
|
|
351
|
+
#${id} .tree-value { color: var(--text); }
|
|
352
|
+
#${id} .tree-value.string { color: var(--string); }
|
|
353
|
+
#${id} .tree-value.number { color: var(--number); }
|
|
354
|
+
#${id} .tree-value.null { color: var(--text-muted); font-style: italic; }
|
|
355
|
+
#${id} .tree-type { color: var(--text-muted); margin-left: 6px; font-size: 10px; }
|
|
356
|
+
#${id} .tree-children { display: none; padding-left: 16px; }
|
|
357
|
+
#${id} .tree-item.open > .tree-children { display: block; }
|
|
358
|
+
|
|
359
|
+
/* Templates Tab */
|
|
360
|
+
#${id} .template-list { display: flex; flex-direction: column; gap: 4px; }
|
|
361
|
+
#${id} .template-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; }
|
|
362
|
+
#${id} .template-icon { width: 14px; height: 14px; color: var(--text-muted); }
|
|
363
|
+
#${id} .template-name { font-family: 'SF Mono', Monaco, monospace; font-size: 11px; }
|
|
364
|
+
#${id} .template-badge { font-size: 9px; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; font-weight: 500; }
|
|
365
|
+
#${id} .template-badge.root { background: rgba(0,120,212,0.15); color: var(--accent); }
|
|
366
|
+
#${id} .template-badge.extends { background: rgba(156,86,246,0.15); color: #9c56f6; }
|
|
367
|
+
#${id} .template-badge.include { background: rgba(78,201,176,0.15); color: var(--success); }
|
|
368
|
+
|
|
369
|
+
/* Queries Tab */
|
|
370
|
+
#${id} .queries-stats { display: flex; gap: 16px; padding: 8px 12px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; margin-bottom: 8px; }
|
|
371
|
+
#${id} .queries-stat { text-align: center; }
|
|
372
|
+
#${id} .queries-stat-value { font-size: 16px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; }
|
|
373
|
+
#${id} .queries-stat-value.warn { color: var(--warning); }
|
|
374
|
+
#${id} .queries-stat-value.error { color: var(--error); }
|
|
375
|
+
#${id} .queries-stat-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; }
|
|
376
|
+
#${id} .query-list { display: flex; flex-direction: column; gap: 4px; }
|
|
377
|
+
#${id} .query-item { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
378
|
+
#${id} .query-item.n1 { border-left: 3px solid var(--error); }
|
|
379
|
+
#${id} .query-item.slow { border-left: 3px solid var(--warning); }
|
|
380
|
+
#${id} .query-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; }
|
|
381
|
+
#${id} .query-sql { flex: 1; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
|
|
382
|
+
#${id} .query-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
383
|
+
#${id} .query-badge { font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600; }
|
|
384
|
+
#${id} .query-badge.n1 { background: rgba(241,76,76,0.15); color: var(--error); }
|
|
385
|
+
#${id} .query-source { font-size: 9px; padding: 2px 5px; border-radius: 3px; background: var(--bg-active); color: var(--text-muted); text-transform: uppercase; }
|
|
386
|
+
#${id} .query-time { font-family: 'SF Mono', Monaco, monospace; font-size: 10px; color: var(--text-secondary); }
|
|
387
|
+
#${id} .query-time.slow { color: var(--warning); }
|
|
388
|
+
#${id} .query-rows { font-size: 10px; color: var(--text-muted); }
|
|
389
|
+
|
|
390
|
+
/* Filters Tab */
|
|
391
|
+
#${id} .filter-grid { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
392
|
+
#${id} .filter-chip { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; }
|
|
393
|
+
#${id} .filter-count { color: var(--accent); font-weight: 600; font-size: 10px; }
|
|
394
|
+
|
|
395
|
+
/* Cache Tab */
|
|
396
|
+
#${id} .cache-stats { display: flex; gap: 20px; }
|
|
397
|
+
#${id} .cache-stat { flex: 1; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 16px; text-align: center; }
|
|
398
|
+
#${id} .cache-value { font-size: 32px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; }
|
|
399
|
+
#${id} .cache-value.hit { color: var(--success); }
|
|
400
|
+
#${id} .cache-value.miss { color: var(--error); }
|
|
401
|
+
#${id} .cache-label { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
402
|
+
|
|
403
|
+
/* Warnings Tab */
|
|
404
|
+
#${id} .warning-list { display: flex; flex-direction: column; gap: 6px; }
|
|
405
|
+
#${id} .warning-item { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(241,76,76,0.08); border: 1px solid rgba(241,76,76,0.2); border-radius: 4px; }
|
|
406
|
+
#${id} .warning-icon { color: var(--error); flex-shrink: 0; width: 14px; height: 14px; }
|
|
407
|
+
#${id} .warning-text { font-size: 11px; color: var(--text); }
|
|
408
|
+
|
|
409
|
+
/* Empty State */
|
|
410
|
+
#${id} .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-muted); font-size: 12px; }
|
|
411
|
+
#${id} .empty-state svg { width: 32px; height: 32px; margin-bottom: 8px; opacity: 0.5; }
|
|
318
412
|
`;
|
|
319
413
|
}
|
|
320
|
-
function getPosition(pos) {
|
|
321
|
-
switch (pos) {
|
|
322
|
-
case "bottom-left":
|
|
323
|
-
return "bottom: 16px; left: 16px;";
|
|
324
|
-
case "top-right":
|
|
325
|
-
return "top: 16px; right: 16px;";
|
|
326
|
-
case "top-left":
|
|
327
|
-
return "top: 16px; left: 16px;";
|
|
328
|
-
default:
|
|
329
|
-
return "bottom: 16px; right: 16px;";
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
414
|
var icons = {
|
|
333
415
|
logo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`,
|
|
334
416
|
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`,
|
|
335
|
-
|
|
417
|
+
minimize: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>`,
|
|
418
|
+
dock: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 15h18"/></svg>`,
|
|
419
|
+
dockRight: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/></svg>`,
|
|
420
|
+
popup: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v6h6"/></svg>`,
|
|
421
|
+
arrow: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>`,
|
|
336
422
|
perf: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>`,
|
|
337
|
-
template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>`,
|
|
338
423
|
context: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/></svg>`,
|
|
424
|
+
template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>`,
|
|
339
425
|
filter: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`,
|
|
426
|
+
database: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`,
|
|
340
427
|
cache: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1010 10H12V2z"/><path d="M12 2a10 10 0 00-8.66 15"/></svg>`,
|
|
341
|
-
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg
|
|
342
|
-
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg>`
|
|
428
|
+
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`
|
|
343
429
|
};
|
|
344
|
-
function
|
|
430
|
+
function generateHTML(id, data, c, opts) {
|
|
345
431
|
const time = (data.totalTime || 0).toFixed(1);
|
|
432
|
+
const queryCount = data.queries?.length || 0;
|
|
433
|
+
const hasWarnings = data.warnings.length > 0 || data.queryStats?.n1Count > 0;
|
|
434
|
+
const tabs = [
|
|
435
|
+
{ id: "perf", icon: icons.perf, label: "Performance", count: `${time}ms` },
|
|
436
|
+
{ id: "context", icon: icons.context, label: "Context", count: Object.keys(data.contextSnapshot).length || null },
|
|
437
|
+
{ id: "templates", icon: icons.template, label: "Templates", count: data.templateChain.length || null },
|
|
438
|
+
{ id: "filters", icon: icons.filter, label: "Filters", count: data.filtersUsed.size || null },
|
|
439
|
+
{ id: "queries", icon: icons.database, label: "Queries", count: queryCount || null, warn: data.queryStats?.n1Count > 0 },
|
|
440
|
+
{ id: "cache", icon: icons.cache, label: "Cache", count: data.cacheHits + data.cacheMisses || null },
|
|
441
|
+
{ id: "warnings", icon: icons.warning, label: "Warnings", count: data.warnings.length || null, warn: hasWarnings }
|
|
442
|
+
];
|
|
443
|
+
const tabsHtml = tabs.map((t, i) => {
|
|
444
|
+
const countHtml = t.count !== null ? `<span class="count${t.warn ? " warn" : ""}">${t.count}</span>` : "";
|
|
445
|
+
return `<button class="devtools-tab${i === 0 ? " active" : ""}" data-tab="${t.id}">${t.icon}${t.label}${countHtml}</button>`;
|
|
446
|
+
}).join("");
|
|
346
447
|
return `
|
|
347
|
-
<button class="
|
|
448
|
+
<button class="devtools-toggle" onclick="document.getElementById('${id}').dataset.open='true'">
|
|
348
449
|
${icons.logo}
|
|
349
450
|
<span>Binja</span>
|
|
350
|
-
<span class="
|
|
351
|
-
</button
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
<div class="
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<
|
|
362
|
-
<span class="dbg-badge time">${time}ms</span>
|
|
363
|
-
<button class="dbg-close" onclick="document.querySelector('#${id} .dbg-panel').classList.remove('open');document.querySelector('#${id} .dbg-toggle').style.display='inline-flex'">${icons.close}</button>
|
|
451
|
+
<span class="badge">${time}ms</span>
|
|
452
|
+
</button>
|
|
453
|
+
|
|
454
|
+
<div class="devtools-panel">
|
|
455
|
+
<div class="devtools-resize"></div>
|
|
456
|
+
<div class="devtools-toolbar">
|
|
457
|
+
<div class="devtools-tabs">${tabsHtml}</div>
|
|
458
|
+
<div class="devtools-actions">
|
|
459
|
+
<button class="devtools-btn" title="Dock to bottom" data-dock="bottom">${icons.dock}</button>
|
|
460
|
+
<button class="devtools-btn" title="Dock to right" data-dock="right">${icons.dockRight}</button>
|
|
461
|
+
<button class="devtools-btn" title="Popup" data-dock="popup">${icons.popup}</button>
|
|
462
|
+
<button class="devtools-btn" title="Close" onclick="document.getElementById('${id}').dataset.open='false'">${icons.close}</button>
|
|
364
463
|
</div>
|
|
365
464
|
</div>
|
|
366
|
-
<div class="
|
|
367
|
-
${
|
|
368
|
-
${
|
|
369
|
-
${
|
|
370
|
-
${
|
|
371
|
-
${
|
|
372
|
-
${
|
|
465
|
+
<div class="devtools-content">
|
|
466
|
+
${generatePerfPane(data)}
|
|
467
|
+
${generateContextPane(data)}
|
|
468
|
+
${generateTemplatesPane(data)}
|
|
469
|
+
${generateFiltersPane(data)}
|
|
470
|
+
${generateQueriesPane(data)}
|
|
471
|
+
${generateCachePane(data)}
|
|
472
|
+
${generateWarningsPane(data)}
|
|
373
473
|
</div>
|
|
374
474
|
</div>`;
|
|
375
475
|
}
|
|
376
|
-
function
|
|
476
|
+
function generatePerfPane(data) {
|
|
377
477
|
const total = data.totalTime || 0.01;
|
|
378
478
|
const lexer = data.lexerTime || 0;
|
|
379
479
|
const parser = data.parserTime || 0;
|
|
380
480
|
const render = data.renderTime || 0;
|
|
481
|
+
const mode = data.mode === "aot" ? "AOT" : "Runtime";
|
|
381
482
|
return `
|
|
382
|
-
<div class="
|
|
383
|
-
<div class="
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
<div class="dbg-section-content">
|
|
388
|
-
<div class="dbg-row">
|
|
389
|
-
<span class="dbg-label">Lexer</span>
|
|
390
|
-
<span class="dbg-value">${lexer.toFixed(2)}ms</span>
|
|
483
|
+
<div class="devtools-pane active" data-pane="perf">
|
|
484
|
+
<div class="perf-grid">
|
|
485
|
+
<div class="perf-card">
|
|
486
|
+
<div class="perf-card-value">${total.toFixed(2)}ms</div>
|
|
487
|
+
<div class="perf-card-label">Total Time</div>
|
|
391
488
|
</div>
|
|
392
|
-
<div class="
|
|
393
|
-
|
|
394
|
-
<
|
|
395
|
-
<span class="dbg-value">${parser.toFixed(2)}ms</span>
|
|
489
|
+
<div class="perf-card">
|
|
490
|
+
<div class="perf-card-value" style="color:var(--accent)">${mode}</div>
|
|
491
|
+
<div class="perf-card-label">Mode</div>
|
|
396
492
|
</div>
|
|
397
|
-
<div class="
|
|
398
|
-
|
|
399
|
-
<
|
|
400
|
-
<span class="dbg-value">${render.toFixed(2)}ms</span>
|
|
493
|
+
<div class="perf-card">
|
|
494
|
+
<div class="perf-card-value">${data.templateChain.length}</div>
|
|
495
|
+
<div class="perf-card-label">Templates</div>
|
|
401
496
|
</div>
|
|
402
|
-
<div class="
|
|
403
|
-
|
|
404
|
-
<
|
|
405
|
-
<span class="dbg-value" style="font-weight:600">${total.toFixed(2)}ms</span>
|
|
497
|
+
<div class="perf-card">
|
|
498
|
+
<div class="perf-card-value">${data.queries?.length || 0}</div>
|
|
499
|
+
<div class="perf-card-label">Queries</div>
|
|
406
500
|
</div>
|
|
407
501
|
</div>
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
<div class="
|
|
415
|
-
|
|
416
|
-
<
|
|
417
|
-
<span class="
|
|
502
|
+
<div class="perf-breakdown">
|
|
503
|
+
<div class="perf-row">
|
|
504
|
+
<span class="perf-row-label">Lexer</span>
|
|
505
|
+
<div class="perf-row-bar"><div class="perf-row-fill lexer" style="width:${lexer / total * 100}%"></div></div>
|
|
506
|
+
<span class="perf-row-value">${lexer.toFixed(2)}ms</span>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="perf-row">
|
|
509
|
+
<span class="perf-row-label">Parser</span>
|
|
510
|
+
<div class="perf-row-bar"><div class="perf-row-fill parser" style="width:${parser / total * 100}%"></div></div>
|
|
511
|
+
<span class="perf-row-value">${parser.toFixed(2)}ms</span>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="perf-row">
|
|
514
|
+
<span class="perf-row-label">Render</span>
|
|
515
|
+
<div class="perf-row-bar"><div class="perf-row-fill render" style="width:${render / total * 100}%"></div></div>
|
|
516
|
+
<span class="perf-row-value">${render.toFixed(2)}ms</span>
|
|
418
517
|
</div>
|
|
419
|
-
`).join("");
|
|
420
|
-
return `
|
|
421
|
-
<div class="dbg-section">
|
|
422
|
-
<div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
|
|
423
|
-
<span class="dbg-section-title">${icons.template} Templates</span>
|
|
424
|
-
<span class="dbg-section-meta">${data.templateChain.length}</span>
|
|
425
|
-
<span class="dbg-chevron">${icons.chevron}</span>
|
|
426
|
-
</div>
|
|
427
|
-
<div class="dbg-section-content">
|
|
428
|
-
<div class="dbg-templates">${templates}</div>
|
|
429
518
|
</div>
|
|
430
519
|
</div>`;
|
|
431
520
|
}
|
|
432
|
-
function
|
|
521
|
+
function generateContextPane(data) {
|
|
433
522
|
const keys = Object.keys(data.contextSnapshot);
|
|
434
|
-
if (keys.length === 0)
|
|
435
|
-
return ""
|
|
436
|
-
|
|
523
|
+
if (keys.length === 0) {
|
|
524
|
+
return `<div class="devtools-pane" data-pane="context"><div class="empty-state">${icons.context}<span>No context variables</span></div></div>`;
|
|
525
|
+
}
|
|
526
|
+
const items = keys.map((key) => renderTreeItem(key, data.contextSnapshot[key])).join("");
|
|
527
|
+
return `<div class="devtools-pane" data-pane="context"><div class="tree">${items}</div></div>`;
|
|
528
|
+
}
|
|
529
|
+
function renderTreeItem(key, ctx) {
|
|
530
|
+
const hasChildren = ctx.expandable && ctx.children && Object.keys(ctx.children).length > 0;
|
|
531
|
+
const arrowHtml = hasChildren ? `<span class="tree-arrow">${icons.arrow}</span>` : '<span class="tree-arrow" style="visibility:hidden">${icons.arrow}</span>';
|
|
532
|
+
const expandableClass = hasChildren ? "expandable" : "";
|
|
533
|
+
const valueClass = getValueClass(ctx.type);
|
|
534
|
+
let childrenHtml = "";
|
|
535
|
+
if (hasChildren && ctx.children) {
|
|
536
|
+
childrenHtml = `<div class="tree-children">${Object.entries(ctx.children).map(([k, v]) => renderTreeItem(k, v)).join("")}</div>`;
|
|
537
|
+
}
|
|
437
538
|
return `
|
|
438
|
-
<div class="
|
|
439
|
-
<div class="
|
|
440
|
-
|
|
441
|
-
<span class="
|
|
442
|
-
<span class="
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<div class="dbg-ctx-grid">${items}</div>
|
|
539
|
+
<div class="tree-item">
|
|
540
|
+
<div class="tree-row ${expandableClass}">
|
|
541
|
+
${arrowHtml}
|
|
542
|
+
<span class="tree-key">${escapeHtml(key)}</span>
|
|
543
|
+
<span class="tree-colon">:</span>
|
|
544
|
+
<span class="tree-value ${valueClass}">${escapeHtml(ctx.preview)}</span>
|
|
545
|
+
<span class="tree-type">${ctx.type}</span>
|
|
446
546
|
</div>
|
|
547
|
+
${childrenHtml}
|
|
447
548
|
</div>`;
|
|
448
549
|
}
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
550
|
+
function getValueClass(type) {
|
|
551
|
+
if (type === "string")
|
|
552
|
+
return "string";
|
|
553
|
+
if (type === "number" || type === "integer" || type === "float")
|
|
554
|
+
return "number";
|
|
555
|
+
if (type === "null" || type === "undefined")
|
|
556
|
+
return "null";
|
|
557
|
+
return "";
|
|
558
|
+
}
|
|
559
|
+
function generateTemplatesPane(data) {
|
|
560
|
+
if (data.templateChain.length === 0) {
|
|
561
|
+
return `<div class="devtools-pane" data-pane="templates"><div class="empty-state">${icons.template}<span>No templates loaded</span></div></div>`;
|
|
457
562
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
<span class="dbg-ctx-name">${escapeHtml(key)}</span>
|
|
464
|
-
<span class="dbg-ctx-type">${ctx.type}</span>
|
|
465
|
-
</div>
|
|
466
|
-
<span class="dbg-ctx-preview">${escapeHtml(ctx.preview)}</span>
|
|
563
|
+
const items = data.templateChain.map((t) => `
|
|
564
|
+
<div class="template-item">
|
|
565
|
+
<span class="template-icon">${icons.template}</span>
|
|
566
|
+
<span class="template-name">${escapeHtml(t.name)}</span>
|
|
567
|
+
<span class="template-badge ${t.type}">${t.type}</span>
|
|
467
568
|
</div>
|
|
468
|
-
|
|
469
|
-
</div>`;
|
|
569
|
+
`).join("");
|
|
570
|
+
return `<div class="devtools-pane" data-pane="templates"><div class="template-list">${items}</div></div>`;
|
|
470
571
|
}
|
|
471
|
-
function
|
|
572
|
+
function generateFiltersPane(data) {
|
|
472
573
|
const filters = Array.from(data.filtersUsed.entries());
|
|
473
|
-
if (filters.length === 0)
|
|
474
|
-
return ""
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
<
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
<span class="dbg-section-meta">${filters.length}</span>
|
|
481
|
-
<span class="dbg-chevron">${icons.chevron}</span>
|
|
482
|
-
</div>
|
|
483
|
-
<div class="dbg-section-content">
|
|
484
|
-
<div class="dbg-filters">${items}</div>
|
|
485
|
-
</div>
|
|
486
|
-
</div>`;
|
|
574
|
+
if (filters.length === 0) {
|
|
575
|
+
return `<div class="devtools-pane" data-pane="filters"><div class="empty-state">${icons.filter}<span>No filters used</span></div></div>`;
|
|
576
|
+
}
|
|
577
|
+
const items = filters.map(([name, count]) => `
|
|
578
|
+
<span class="filter-chip">${escapeHtml(name)}<span class="filter-count">\xD7${count}</span></span>
|
|
579
|
+
`).join("");
|
|
580
|
+
return `<div class="devtools-pane" data-pane="filters"><div class="filter-grid">${items}</div></div>`;
|
|
487
581
|
}
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
582
|
+
function generateQueriesPane(data) {
|
|
583
|
+
if (!data.queries || data.queries.length === 0) {
|
|
584
|
+
return `<div class="devtools-pane" data-pane="queries"><div class="empty-state">${icons.database}<span>No queries recorded</span></div></div>`;
|
|
585
|
+
}
|
|
586
|
+
const stats = data.queryStats;
|
|
587
|
+
const statsHtml = `
|
|
588
|
+
<div class="queries-stats">
|
|
589
|
+
<div class="queries-stat">
|
|
590
|
+
<div class="queries-stat-value">${stats.count}</div>
|
|
591
|
+
<div class="queries-stat-label">Queries</div>
|
|
592
|
+
</div>
|
|
593
|
+
<div class="queries-stat">
|
|
594
|
+
<div class="queries-stat-value">${stats.totalDuration.toFixed(1)}ms</div>
|
|
595
|
+
<div class="queries-stat-label">Total</div>
|
|
596
|
+
</div>
|
|
597
|
+
<div class="queries-stat">
|
|
598
|
+
<div class="queries-stat-value ${stats.slowCount > 0 ? "warn" : ""}">${stats.slowCount}</div>
|
|
599
|
+
<div class="queries-stat-label">Slow</div>
|
|
504
600
|
</div>
|
|
505
|
-
<div class="
|
|
506
|
-
<div class="
|
|
507
|
-
<div class="
|
|
601
|
+
<div class="queries-stat">
|
|
602
|
+
<div class="queries-stat-value ${stats.n1Count > 0 ? "error" : ""}">${stats.n1Count}</div>
|
|
603
|
+
<div class="queries-stat-label">N+1</div>
|
|
508
604
|
</div>
|
|
605
|
+
</div>`;
|
|
606
|
+
const queries = data.queries.map((q) => {
|
|
607
|
+
const isSlow = q.duration > 100;
|
|
608
|
+
const classes = ["query-item", q.isN1 ? "n1" : "", isSlow ? "slow" : ""].filter(Boolean).join(" ");
|
|
609
|
+
const badge = q.isN1 ? '<span class="query-badge n1">N+1</span>' : "";
|
|
610
|
+
const source = q.source ? `<span class="query-source">${escapeHtml(q.source)}</span>` : "";
|
|
611
|
+
const rows = q.rows !== undefined ? `<span class="query-rows">${q.rows} rows</span>` : "";
|
|
612
|
+
return `
|
|
613
|
+
<div class="${classes}">
|
|
614
|
+
<div class="query-header">
|
|
615
|
+
<span class="query-sql" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>
|
|
616
|
+
<div class="query-meta">
|
|
617
|
+
${badge}${source}${rows}
|
|
618
|
+
<span class="query-time ${isSlow ? "slow" : ""}">${q.duration.toFixed(1)}ms</span>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
</div>`;
|
|
622
|
+
}).join("");
|
|
623
|
+
return `<div class="devtools-pane" data-pane="queries">${statsHtml}<div class="query-list">${queries}</div></div>`;
|
|
624
|
+
}
|
|
625
|
+
function generateCachePane(data) {
|
|
626
|
+
const total = data.cacheHits + data.cacheMisses;
|
|
627
|
+
if (total === 0) {
|
|
628
|
+
return `<div class="devtools-pane" data-pane="cache"><div class="empty-state">${icons.cache}<span>No cache activity</span></div></div>`;
|
|
629
|
+
}
|
|
630
|
+
const hitRate = (data.cacheHits / total * 100).toFixed(0);
|
|
631
|
+
return `
|
|
632
|
+
<div class="devtools-pane" data-pane="cache">
|
|
633
|
+
<div class="cache-stats">
|
|
634
|
+
<div class="cache-stat">
|
|
635
|
+
<div class="cache-value hit">${data.cacheHits}</div>
|
|
636
|
+
<div class="cache-label">Cache Hits</div>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="cache-stat">
|
|
639
|
+
<div class="cache-value miss">${data.cacheMisses}</div>
|
|
640
|
+
<div class="cache-label">Cache Misses</div>
|
|
641
|
+
</div>
|
|
642
|
+
<div class="cache-stat">
|
|
643
|
+
<div class="cache-value">${hitRate}%</div>
|
|
644
|
+
<div class="cache-label">Hit Rate</div>
|
|
509
645
|
</div>
|
|
510
646
|
</div>
|
|
511
647
|
</div>`;
|
|
512
648
|
}
|
|
513
|
-
function
|
|
514
|
-
if (data.warnings.length === 0)
|
|
515
|
-
return ""
|
|
649
|
+
function generateWarningsPane(data) {
|
|
650
|
+
if (data.warnings.length === 0) {
|
|
651
|
+
return `<div class="devtools-pane" data-pane="warnings"><div class="empty-state">${icons.warning}<span>No warnings</span></div></div>`;
|
|
652
|
+
}
|
|
516
653
|
const items = data.warnings.map((w) => `
|
|
517
|
-
<div class="
|
|
518
|
-
|
|
519
|
-
<span class="
|
|
654
|
+
<div class="warning-item">
|
|
655
|
+
<span class="warning-icon">${icons.warning}</span>
|
|
656
|
+
<span class="warning-text">${escapeHtml(w)}</span>
|
|
520
657
|
</div>
|
|
521
658
|
`).join("");
|
|
522
|
-
return
|
|
523
|
-
<div class="dbg-section open">
|
|
524
|
-
<div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
|
|
525
|
-
<span class="dbg-section-title">${icons.warning} Warnings</span>
|
|
526
|
-
<span class="dbg-section-meta" style="color:#eab308">${data.warnings.length}</span>
|
|
527
|
-
<span class="dbg-chevron">${icons.chevron}</span>
|
|
528
|
-
</div>
|
|
529
|
-
<div class="dbg-section-content">
|
|
530
|
-
<div class="dbg-warnings">${items}</div>
|
|
531
|
-
</div>
|
|
532
|
-
</div>`;
|
|
659
|
+
return `<div class="devtools-pane" data-pane="warnings"><div class="warning-list">${items}</div></div>`;
|
|
533
660
|
}
|
|
534
|
-
function generateScript(id) {
|
|
661
|
+
function generateScript(id, data, opts) {
|
|
535
662
|
return `
|
|
536
|
-
(function(){
|
|
537
|
-
var
|
|
538
|
-
if (!
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
663
|
+
(function() {
|
|
664
|
+
var root = document.getElementById('${id}');
|
|
665
|
+
if (!root) return;
|
|
666
|
+
|
|
667
|
+
// Tab switching
|
|
668
|
+
root.querySelectorAll('.devtools-tab').forEach(function(tab) {
|
|
669
|
+
tab.addEventListener('click', function() {
|
|
670
|
+
root.querySelectorAll('.devtools-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
671
|
+
root.querySelectorAll('.devtools-pane').forEach(function(p) { p.classList.remove('active'); });
|
|
672
|
+
tab.classList.add('active');
|
|
673
|
+
var pane = root.querySelector('[data-pane="' + tab.dataset.tab + '"]');
|
|
674
|
+
if (pane) pane.classList.add('active');
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Tree expand/collapse
|
|
679
|
+
root.querySelectorAll('.tree-row.expandable').forEach(function(row) {
|
|
680
|
+
row.addEventListener('click', function() {
|
|
681
|
+
row.parentElement.classList.toggle('open');
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Dock position switching
|
|
686
|
+
root.querySelectorAll('[data-dock]').forEach(function(btn) {
|
|
687
|
+
btn.addEventListener('click', function() {
|
|
688
|
+
root.dataset.position = btn.dataset.dock;
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Resize functionality
|
|
693
|
+
var resize = root.querySelector('.devtools-resize');
|
|
694
|
+
var panel = root.querySelector('.devtools-panel');
|
|
695
|
+
if (resize && panel) {
|
|
696
|
+
var isResizing = false;
|
|
697
|
+
var startY, startX, startHeight, startWidth;
|
|
698
|
+
|
|
699
|
+
resize.addEventListener('mousedown', function(e) {
|
|
700
|
+
isResizing = true;
|
|
701
|
+
startY = e.clientY;
|
|
702
|
+
startX = e.clientX;
|
|
703
|
+
startHeight = panel.offsetHeight;
|
|
704
|
+
startWidth = panel.offsetWidth;
|
|
705
|
+
document.body.style.cursor = root.dataset.position === 'right' ? 'ew-resize' : 'ns-resize';
|
|
706
|
+
e.preventDefault();
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
document.addEventListener('mousemove', function(e) {
|
|
710
|
+
if (!isResizing) return;
|
|
711
|
+
if (root.dataset.position === 'bottom') {
|
|
712
|
+
var newHeight = startHeight - (e.clientY - startY);
|
|
713
|
+
if (newHeight > 100 && newHeight < window.innerHeight * 0.8) {
|
|
714
|
+
panel.style.height = newHeight + 'px';
|
|
715
|
+
}
|
|
716
|
+
} else if (root.dataset.position === 'right') {
|
|
717
|
+
var newWidth = startWidth - (e.clientX - startX);
|
|
718
|
+
if (newWidth > 200 && newWidth < window.innerWidth * 0.6) {
|
|
719
|
+
panel.style.width = newWidth + 'px';
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
document.addEventListener('mouseup', function() {
|
|
725
|
+
isResizing = false;
|
|
726
|
+
document.body.style.cursor = '';
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
})();`;
|
|
730
|
+
}
|
|
731
|
+
function escapeHtml(str) {
|
|
732
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
733
|
+
}
|
|
734
|
+
// src/debug/integrations.ts
|
|
735
|
+
function recordQuery(query) {
|
|
736
|
+
const collector = getDebugCollector();
|
|
737
|
+
if (collector) {
|
|
738
|
+
collector.recordQuery(query);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function createPrismaMiddleware() {
|
|
742
|
+
return async (params, next) => {
|
|
743
|
+
const collector = getDebugCollector();
|
|
744
|
+
const start = performance.now();
|
|
745
|
+
const result = await next(params);
|
|
746
|
+
const duration = performance.now() - start;
|
|
747
|
+
if (collector) {
|
|
748
|
+
const sql = `${params.action} ${params.model}`;
|
|
749
|
+
collector.recordQuery({
|
|
750
|
+
sql,
|
|
751
|
+
params: params.args ? [JSON.stringify(params.args)] : undefined,
|
|
752
|
+
duration,
|
|
753
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
754
|
+
source: "prisma"
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return result;
|
|
556
758
|
};
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
759
|
+
}
|
|
760
|
+
function setupPrismaLogging(prisma) {
|
|
761
|
+
prisma.$on("query", (e) => {
|
|
762
|
+
const collector = getDebugCollector();
|
|
763
|
+
if (collector) {
|
|
764
|
+
collector.recordQuery({
|
|
765
|
+
sql: e.query,
|
|
766
|
+
params: e.params ? JSON.parse(e.params) : undefined,
|
|
767
|
+
duration: e.duration,
|
|
768
|
+
source: "prisma"
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
function createDrizzleLogger() {
|
|
774
|
+
return {
|
|
775
|
+
logQuery(query, params) {
|
|
776
|
+
const collector = getDebugCollector();
|
|
777
|
+
if (collector) {
|
|
778
|
+
collector.recordQuery({
|
|
779
|
+
sql: query,
|
|
780
|
+
params,
|
|
781
|
+
duration: 0,
|
|
782
|
+
source: "drizzle"
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
561
786
|
};
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
787
|
+
}
|
|
788
|
+
async function wrapDrizzleQuery(query, sql) {
|
|
789
|
+
const collector = getDebugCollector();
|
|
790
|
+
const start = performance.now();
|
|
791
|
+
const result = await query;
|
|
792
|
+
const duration = performance.now() - start;
|
|
793
|
+
if (collector) {
|
|
794
|
+
collector.recordQuery({
|
|
795
|
+
sql: sql || "Drizzle Query",
|
|
796
|
+
duration,
|
|
797
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
798
|
+
source: "drizzle"
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
function wrapBunSQL(db) {
|
|
804
|
+
const handler = {
|
|
805
|
+
get(target, prop) {
|
|
806
|
+
const value = target[prop];
|
|
807
|
+
if (typeof value === "function") {
|
|
808
|
+
if (prop === "query" || prop === "run" || prop === "prepare") {
|
|
809
|
+
return function(...args) {
|
|
810
|
+
const collector = getDebugCollector();
|
|
811
|
+
const sql = typeof args[0] === "string" ? args[0] : "SQL Query";
|
|
812
|
+
const start = performance.now();
|
|
813
|
+
const result = value.apply(target, args);
|
|
814
|
+
if (result && typeof result.all === "function") {
|
|
815
|
+
return wrapPreparedStatement(result, sql);
|
|
816
|
+
}
|
|
817
|
+
const duration = performance.now() - start;
|
|
818
|
+
if (collector) {
|
|
819
|
+
collector.recordQuery({
|
|
820
|
+
sql,
|
|
821
|
+
params: args.slice(1),
|
|
822
|
+
duration,
|
|
823
|
+
rows: Array.isArray(result) ? result.length : undefined,
|
|
824
|
+
source: "bun:sqlite"
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
return result;
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
565
833
|
};
|
|
566
|
-
|
|
834
|
+
return new Proxy(db, handler);
|
|
567
835
|
}
|
|
568
|
-
function
|
|
569
|
-
|
|
836
|
+
function wrapPreparedStatement(stmt, sql) {
|
|
837
|
+
const handler = {
|
|
838
|
+
get(target, prop) {
|
|
839
|
+
const value = target[prop];
|
|
840
|
+
if (typeof value === "function" && (prop === "all" || prop === "get" || prop === "run")) {
|
|
841
|
+
return function(...args) {
|
|
842
|
+
const collector = getDebugCollector();
|
|
843
|
+
const start = performance.now();
|
|
844
|
+
const result = value.apply(target, args);
|
|
845
|
+
const duration = performance.now() - start;
|
|
846
|
+
if (collector) {
|
|
847
|
+
collector.recordQuery({
|
|
848
|
+
sql,
|
|
849
|
+
params: args,
|
|
850
|
+
duration,
|
|
851
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
852
|
+
source: "bun:sqlite"
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
return result;
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
return value;
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
return new Proxy(stmt, handler);
|
|
862
|
+
}
|
|
863
|
+
async function wrapQuery(sql, queryFn, source = "sql") {
|
|
864
|
+
const collector = getDebugCollector();
|
|
865
|
+
const start = performance.now();
|
|
866
|
+
const result = await queryFn();
|
|
867
|
+
const duration = performance.now() - start;
|
|
868
|
+
if (collector) {
|
|
869
|
+
collector.recordQuery({
|
|
870
|
+
sql,
|
|
871
|
+
duration,
|
|
872
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
873
|
+
source
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
}
|
|
878
|
+
function createQueryWrapper(source) {
|
|
879
|
+
return (sql, queryFn) => {
|
|
880
|
+
return wrapQuery(sql, queryFn, source);
|
|
881
|
+
};
|
|
570
882
|
}
|
|
571
883
|
|
|
572
884
|
// src/debug/index.ts
|
|
@@ -666,13 +978,21 @@ function debugMiddleware(env, options = {}) {
|
|
|
666
978
|
};
|
|
667
979
|
}
|
|
668
980
|
export {
|
|
981
|
+
wrapQuery,
|
|
982
|
+
wrapDrizzleQuery,
|
|
983
|
+
wrapBunSQL,
|
|
669
984
|
startDebugCollection,
|
|
985
|
+
setupPrismaLogging,
|
|
670
986
|
renderWithDebug,
|
|
671
987
|
renderStringWithDebug,
|
|
988
|
+
recordQuery,
|
|
672
989
|
getDebugCollector,
|
|
673
990
|
generateDebugPanel,
|
|
674
991
|
endDebugCollection,
|
|
675
992
|
debugMiddleware,
|
|
993
|
+
createQueryWrapper,
|
|
994
|
+
createPrismaMiddleware,
|
|
995
|
+
createDrizzleLogger,
|
|
676
996
|
createDebugRenderer,
|
|
677
997
|
DebugCollector
|
|
678
998
|
};
|