binja 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,34 @@ export interface ContextValue {
18
18
  expandable: boolean;
19
19
  children?: Record<string, ContextValue>;
20
20
  }
21
+ export interface QueryInfo {
22
+ /** SQL query or operation name */
23
+ sql: string;
24
+ /** Query parameters */
25
+ params?: any[];
26
+ /** Duration in milliseconds */
27
+ duration: number;
28
+ /** Number of rows returned */
29
+ rows?: number;
30
+ /** Source ORM (drizzle, prisma, etc.) */
31
+ source?: string;
32
+ /** Timestamp when query was executed */
33
+ timestamp: number;
34
+ /** Whether this query was detected as part of N+1 */
35
+ isN1?: boolean;
36
+ }
37
+ export interface QueryStats {
38
+ /** Total number of queries */
39
+ count: number;
40
+ /** Total duration of all queries */
41
+ totalDuration: number;
42
+ /** Number of slow queries (> 100ms) */
43
+ slowCount: number;
44
+ /** Number of N+1 queries detected */
45
+ n1Count: number;
46
+ /** Queries grouped by similar SQL for N+1 detection */
47
+ queryCounts: Map<string, number>;
48
+ }
21
49
  export interface DebugData {
22
50
  startTime: number;
23
51
  endTime?: number;
@@ -35,6 +63,8 @@ export interface DebugData {
35
63
  testsUsed: Map<string, number>;
36
64
  cacheHits: number;
37
65
  cacheMisses: number;
66
+ queries: QueryInfo[];
67
+ queryStats: QueryStats;
38
68
  warnings: string[];
39
69
  }
40
70
  export declare class DebugCollector {
@@ -59,6 +89,19 @@ export declare class DebugCollector {
59
89
  recordCacheHit(): void;
60
90
  recordCacheMiss(): void;
61
91
  addWarning(message: string): void;
92
+ /**
93
+ * Record a database query for telemetry
94
+ * @param query Query information
95
+ */
96
+ recordQuery(query: Omit<QueryInfo, 'timestamp' | 'isN1'>): void;
97
+ /**
98
+ * Normalize SQL query for comparison (removes specific values)
99
+ */
100
+ private normalizeQuery;
101
+ /**
102
+ * Get query statistics
103
+ */
104
+ getQueryStats(): QueryStats;
62
105
  getData(): DebugData;
63
106
  getSummary(): {
64
107
  totalTime: number;
@@ -1 +1 @@
1
- {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../src/debug/collector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAA;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,SAAS;IAExB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,aAAa,EAAE,YAAY,EAAE,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,IAAI,EAAE,SAAS,GAAG,KAAK,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAGhB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IAG7C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAG9B,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IAGnB,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAW;;IAmBvB,UAAU,IAAI,IAAI;IAIlB,QAAQ,IAAI,IAAI;IAMhB,WAAW,IAAI,IAAI;IAInB,SAAS,IAAI,IAAI;IAMjB,WAAW,IAAI,IAAI;IAInB,SAAS,IAAI,IAAI;IASjB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAQtF,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,IAAI;IAItC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKhC,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAOlD,OAAO,CAAC,YAAY;IAwBpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,UAAU;IAmClB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIhC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK9B,cAAc,IAAI,IAAI;IAItB,eAAe,IAAI,IAAI;IAKvB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKjC,OAAO,IAAI,SAAS;IAKpB,UAAU,IAAI;QACZ,SAAS,EAAE,MAAM,CAAA;QACjB,aAAa,EAAE,MAAM,CAAA;QACrB,WAAW,EAAE,MAAM,CAAA;QACnB,IAAI,EAAE,MAAM,CAAA;KACb;CAQF;AAKD,wBAAgB,oBAAoB,IAAI,cAAc,CAGrD;AAED,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAEzD;AAED,wBAAgB,kBAAkB,IAAI,SAAS,GAAG,IAAI,CAOrD"}
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../src/debug/collector.ts"],"names":[],"mappings":"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"}
@@ -13,9 +13,10 @@
13
13
  import type { Environment } from '../index';
14
14
  import { PanelOptions } from './panel';
15
15
  export { DebugCollector, startDebugCollection, endDebugCollection, getDebugCollector } from './collector';
16
- export type { DebugData, ContextValue } from './collector';
16
+ export type { DebugData, ContextValue, QueryInfo, QueryStats } from './collector';
17
17
  export { generateDebugPanel } from './panel';
18
18
  export type { PanelOptions } from './panel';
19
+ export { recordQuery, createPrismaMiddleware, setupPrismaLogging, createDrizzleLogger, wrapDrizzleQuery, wrapBunSQL, wrapQuery, createQueryWrapper, } from './integrations';
19
20
  export interface DebugRenderOptions {
20
21
  /** Panel options */
21
22
  panel?: PanelOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/debug/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,OAAO,EAAsB,YAAY,EAAE,MAAM,SAAS,CAAA;AAG1D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACzG,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,MAAM,WAAW,kBAAkB;IACjC,oBAAoB;IACpB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CAgCjB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB;yBAEvD,MAAM,YAAW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAQ,OAAO,CAAC,MAAM,CAAC;yBAG3D,MAAM,YAAW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAQ,OAAO,CAAC,MAAM,CAAC;EAIzF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB;IAE9E;;OAEG;aAEa,GAAG,GAAG,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC;IAyBjD;;OAEG;gBAEO,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,MAAM,IAAI;EA2BjD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/debug/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,OAAO,EAAsB,YAAY,EAAE,MAAM,SAAS,CAAA;AAG1D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACzG,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAG3C,OAAO,EACL,WAAW,EACX,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,kBAAkB,GACnB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,WAAW,kBAAkB;IACjC,oBAAoB;IACpB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CAgCjB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB;yBAEvD,MAAM,YAAW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAQ,OAAO,CAAC,MAAM,CAAC;yBAG3D,MAAM,YAAW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAQ,OAAO,CAAC,MAAM,CAAC;EAIzF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB;IAE9E;;OAEG;aAEa,GAAG,GAAG,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC;IAyBjD;;OAEG;gBAEO,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,MAAM,IAAI;EA2BjD"}
@@ -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
  }
@@ -315,6 +350,25 @@ function generateStyles(id, c, opts) {
315
350
  #${id} .dbg-warning { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(234,179,8,0.1); border-radius: 6px; border-left: 3px solid ${c.warning}; }
316
351
  #${id} .dbg-warning-icon { color: ${c.warning}; flex-shrink: 0; margin-top: 1px; }
317
352
  #${id} .dbg-warning-text { color: ${c.text}; font-size: 12px; }
353
+ #${id} .dbg-queries { display: flex; flex-direction: column; gap: 8px; }
354
+ #${id} .dbg-query { background: ${c.bg}; border-radius: 6px; overflow: hidden; }
355
+ #${id} .dbg-query.n1 { border-left: 3px solid ${c.error}; }
356
+ #${id} .dbg-query.slow { border-left: 3px solid ${c.warning}; }
357
+ #${id} .dbg-query-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; gap: 8px; }
358
+ #${id} .dbg-query-sql { flex: 1; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; color: ${c.text}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
359
+ #${id} .dbg-query-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
360
+ #${id} .dbg-query-time { font-family: 'SF Mono', Monaco, monospace; font-size: 10px; padding: 2px 6px; border-radius: 3px; background: ${c.bgTertiary}; }
361
+ #${id} .dbg-query-time.slow { background: rgba(234,179,8,0.15); color: ${c.warning}; }
362
+ #${id} .dbg-query-rows { font-size: 10px; color: ${c.textMuted}; }
363
+ #${id} .dbg-query-source { font-size: 9px; padding: 2px 5px; border-radius: 3px; background: rgba(59,130,246,0.1); color: ${c.accent}; text-transform: uppercase; }
364
+ #${id} .dbg-query-badge { font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600; }
365
+ #${id} .dbg-query-badge.n1 { background: rgba(239,68,68,0.15); color: ${c.error}; }
366
+ #${id} .dbg-query-stats { display: flex; gap: 12px; margin-bottom: 12px; padding: 10px; background: ${c.bg}; border-radius: 6px; }
367
+ #${id} .dbg-query-stat { flex: 1; text-align: center; }
368
+ #${id} .dbg-query-stat-num { font-size: 18px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; color: ${c.text}; }
369
+ #${id} .dbg-query-stat-num.warning { color: ${c.warning}; }
370
+ #${id} .dbg-query-stat-num.error { color: ${c.error}; }
371
+ #${id} .dbg-query-stat-label { font-size: 10px; color: ${c.textMuted}; margin-top: 2px; }
318
372
  `;
319
373
  }
320
374
  function getPosition(pos) {
@@ -339,7 +393,8 @@ var icons = {
339
393
  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>`,
340
394
  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
395
  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>`
396
+ 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>`,
397
+ 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>`
343
398
  };
344
399
  function generateToggle(id, data, c) {
345
400
  const time = (data.totalTime || 0).toFixed(1);
@@ -368,6 +423,7 @@ function generatePanel(id, data, c, opts) {
368
423
  ${generateTemplatesSection(data)}
369
424
  ${generateContextSection(data)}
370
425
  ${generateFiltersSection(data)}
426
+ ${generateQueriesSection(data)}
371
427
  ${generateCacheSection(data)}
372
428
  ${generateWarningsSection(data)}
373
429
  </div>
@@ -531,6 +587,70 @@ function generateWarningsSection(data) {
531
587
  </div>
532
588
  </div>`;
533
589
  }
590
+ function generateQueriesSection(data) {
591
+ if (data.queries.length === 0)
592
+ return "";
593
+ const stats = data.queryStats;
594
+ const hasIssues = stats.slowCount > 0 || stats.n1Count > 0;
595
+ const statsHtml = `
596
+ <div class="dbg-query-stats">
597
+ <div class="dbg-query-stat">
598
+ <div class="dbg-query-stat-num">${stats.count}</div>
599
+ <div class="dbg-query-stat-label">Queries</div>
600
+ </div>
601
+ <div class="dbg-query-stat">
602
+ <div class="dbg-query-stat-num">${stats.totalDuration.toFixed(1)}ms</div>
603
+ <div class="dbg-query-stat-label">Total Time</div>
604
+ </div>
605
+ <div class="dbg-query-stat">
606
+ <div class="dbg-query-stat-num ${stats.slowCount > 0 ? "warning" : ""}">${stats.slowCount}</div>
607
+ <div class="dbg-query-stat-label">Slow (&gt;100ms)</div>
608
+ </div>
609
+ <div class="dbg-query-stat">
610
+ <div class="dbg-query-stat-num ${stats.n1Count > 0 ? "error" : ""}">${stats.n1Count}</div>
611
+ <div class="dbg-query-stat-label">N+1</div>
612
+ </div>
613
+ </div>`;
614
+ const queries = data.queries.map((q) => {
615
+ const isSlow = q.duration > 100;
616
+ const classes = [
617
+ "dbg-query",
618
+ q.isN1 ? "n1" : "",
619
+ isSlow ? "slow" : ""
620
+ ].filter(Boolean).join(" ");
621
+ const badges = [];
622
+ if (q.isN1) {
623
+ badges.push('<span class="dbg-query-badge n1">N+1</span>');
624
+ }
625
+ const rowsText = q.rows !== undefined ? `<span class="dbg-query-rows">${q.rows} rows</span>` : "";
626
+ const sourceText = q.source ? `<span class="dbg-query-source">${escapeHtml(q.source)}</span>` : "";
627
+ return `
628
+ <div class="${classes}">
629
+ <div class="dbg-query-header">
630
+ <span class="dbg-query-sql" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>
631
+ <div class="dbg-query-meta">
632
+ ${badges.join("")}
633
+ ${sourceText}
634
+ ${rowsText}
635
+ <span class="dbg-query-time ${isSlow ? "slow" : ""}">${q.duration.toFixed(1)}ms</span>
636
+ </div>
637
+ </div>
638
+ </div>`;
639
+ }).join("");
640
+ const metaColor = hasIssues ? 'style="color:#ef4444"' : "";
641
+ return `
642
+ <div class="dbg-section ${hasIssues ? "open" : ""}">
643
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
644
+ <span class="dbg-section-title">${icons.database} Queries</span>
645
+ <span class="dbg-section-meta" ${metaColor}>${stats.count} (${stats.totalDuration.toFixed(1)}ms)</span>
646
+ <span class="dbg-chevron">${icons.chevron}</span>
647
+ </div>
648
+ <div class="dbg-section-content">
649
+ ${statsHtml}
650
+ <div class="dbg-queries">${queries}</div>
651
+ </div>
652
+ </div>`;
653
+ }
534
654
  function generateScript(id) {
535
655
  return `
536
656
  (function(){
@@ -568,6 +688,155 @@ function generateScript(id) {
568
688
  function escapeHtml(str) {
569
689
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
570
690
  }
691
+ // src/debug/integrations.ts
692
+ function recordQuery(query) {
693
+ const collector = getDebugCollector();
694
+ if (collector) {
695
+ collector.recordQuery(query);
696
+ }
697
+ }
698
+ function createPrismaMiddleware() {
699
+ return async (params, next) => {
700
+ const collector = getDebugCollector();
701
+ const start = performance.now();
702
+ const result = await next(params);
703
+ const duration = performance.now() - start;
704
+ if (collector) {
705
+ const sql = `${params.action} ${params.model}`;
706
+ collector.recordQuery({
707
+ sql,
708
+ params: params.args ? [JSON.stringify(params.args)] : undefined,
709
+ duration,
710
+ rows: Array.isArray(result) ? result.length : result ? 1 : 0,
711
+ source: "prisma"
712
+ });
713
+ }
714
+ return result;
715
+ };
716
+ }
717
+ function setupPrismaLogging(prisma) {
718
+ prisma.$on("query", (e) => {
719
+ const collector = getDebugCollector();
720
+ if (collector) {
721
+ collector.recordQuery({
722
+ sql: e.query,
723
+ params: e.params ? JSON.parse(e.params) : undefined,
724
+ duration: e.duration,
725
+ source: "prisma"
726
+ });
727
+ }
728
+ });
729
+ }
730
+ function createDrizzleLogger() {
731
+ return {
732
+ logQuery(query, params) {
733
+ const collector = getDebugCollector();
734
+ if (collector) {
735
+ collector.recordQuery({
736
+ sql: query,
737
+ params,
738
+ duration: 0,
739
+ source: "drizzle"
740
+ });
741
+ }
742
+ }
743
+ };
744
+ }
745
+ async function wrapDrizzleQuery(query, sql) {
746
+ const collector = getDebugCollector();
747
+ const start = performance.now();
748
+ const result = await query;
749
+ const duration = performance.now() - start;
750
+ if (collector) {
751
+ collector.recordQuery({
752
+ sql: sql || "Drizzle Query",
753
+ duration,
754
+ rows: Array.isArray(result) ? result.length : result ? 1 : 0,
755
+ source: "drizzle"
756
+ });
757
+ }
758
+ return result;
759
+ }
760
+ function wrapBunSQL(db) {
761
+ const handler = {
762
+ get(target, prop) {
763
+ const value = target[prop];
764
+ if (typeof value === "function") {
765
+ if (prop === "query" || prop === "run" || prop === "prepare") {
766
+ return function(...args) {
767
+ const collector = getDebugCollector();
768
+ const sql = typeof args[0] === "string" ? args[0] : "SQL Query";
769
+ const start = performance.now();
770
+ const result = value.apply(target, args);
771
+ if (result && typeof result.all === "function") {
772
+ return wrapPreparedStatement(result, sql);
773
+ }
774
+ const duration = performance.now() - start;
775
+ if (collector) {
776
+ collector.recordQuery({
777
+ sql,
778
+ params: args.slice(1),
779
+ duration,
780
+ rows: Array.isArray(result) ? result.length : undefined,
781
+ source: "bun:sqlite"
782
+ });
783
+ }
784
+ return result;
785
+ };
786
+ }
787
+ }
788
+ return value;
789
+ }
790
+ };
791
+ return new Proxy(db, handler);
792
+ }
793
+ function wrapPreparedStatement(stmt, sql) {
794
+ const handler = {
795
+ get(target, prop) {
796
+ const value = target[prop];
797
+ if (typeof value === "function" && (prop === "all" || prop === "get" || prop === "run")) {
798
+ return function(...args) {
799
+ const collector = getDebugCollector();
800
+ const start = performance.now();
801
+ const result = value.apply(target, args);
802
+ const duration = performance.now() - start;
803
+ if (collector) {
804
+ collector.recordQuery({
805
+ sql,
806
+ params: args,
807
+ duration,
808
+ rows: Array.isArray(result) ? result.length : result ? 1 : 0,
809
+ source: "bun:sqlite"
810
+ });
811
+ }
812
+ return result;
813
+ };
814
+ }
815
+ return value;
816
+ }
817
+ };
818
+ return new Proxy(stmt, handler);
819
+ }
820
+ async function wrapQuery(sql, queryFn, source = "sql") {
821
+ const collector = getDebugCollector();
822
+ const start = performance.now();
823
+ const result = await queryFn();
824
+ const duration = performance.now() - start;
825
+ if (collector) {
826
+ collector.recordQuery({
827
+ sql,
828
+ duration,
829
+ rows: Array.isArray(result) ? result.length : result ? 1 : 0,
830
+ source
831
+ });
832
+ }
833
+ return result;
834
+ }
835
+ function createQueryWrapper(source) {
836
+ return (sql, queryFn) => {
837
+ return wrapQuery(sql, queryFn, source);
838
+ };
839
+ }
571
840
 
572
841
  // src/debug/index.ts
573
842
  async function renderWithDebug(env, templateName, context = {}, options = {}) {
@@ -666,13 +935,21 @@ function debugMiddleware(env, options = {}) {
666
935
  };
667
936
  }
668
937
  export {
938
+ wrapQuery,
939
+ wrapDrizzleQuery,
940
+ wrapBunSQL,
669
941
  startDebugCollection,
942
+ setupPrismaLogging,
670
943
  renderWithDebug,
671
944
  renderStringWithDebug,
945
+ recordQuery,
672
946
  getDebugCollector,
673
947
  generateDebugPanel,
674
948
  endDebugCollection,
675
949
  debugMiddleware,
950
+ createQueryWrapper,
951
+ createPrismaMiddleware,
952
+ createDrizzleLogger,
676
953
  createDebugRenderer,
677
954
  DebugCollector
678
955
  };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * ORM Integrations for Query Telemetry
3
+ *
4
+ * Provides helpers for Prisma, Drizzle, and Bun SQL
5
+ */
6
+ import type { QueryInfo } from './collector';
7
+ type QueryInput = Omit<QueryInfo, 'timestamp' | 'isN1'>;
8
+ /**
9
+ * Record a query manually (works with any database)
10
+ */
11
+ export declare function recordQuery(query: QueryInput): void;
12
+ /**
13
+ * Create Prisma middleware for query telemetry
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { PrismaClient } from '@prisma/client'
18
+ * import { createPrismaMiddleware } from 'binja/debug'
19
+ *
20
+ * const prisma = new PrismaClient()
21
+ * prisma.$use(createPrismaMiddleware())
22
+ * ```
23
+ */
24
+ export declare function createPrismaMiddleware(): (params: any, next: (params: any) => Promise<any>) => Promise<any>;
25
+ /**
26
+ * Setup Prisma query event logging
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { PrismaClient } from '@prisma/client'
31
+ * import { setupPrismaLogging } from 'binja/debug'
32
+ *
33
+ * const prisma = new PrismaClient({ log: [{ emit: 'event', level: 'query' }] })
34
+ * setupPrismaLogging(prisma)
35
+ * ```
36
+ */
37
+ export declare function setupPrismaLogging(prisma: any): void;
38
+ /**
39
+ * Create Drizzle logger for query telemetry
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * import { drizzle } from 'drizzle-orm/...'
44
+ * import { createDrizzleLogger } from 'binja/debug'
45
+ *
46
+ * const db = drizzle(client, { logger: createDrizzleLogger() })
47
+ * ```
48
+ */
49
+ export declare function createDrizzleLogger(): {
50
+ logQuery(query: string, params: unknown[]): void;
51
+ };
52
+ /**
53
+ * Wrap a Drizzle query for accurate timing
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * import { wrapDrizzleQuery } from 'binja/debug'
58
+ *
59
+ * const users = await wrapDrizzleQuery(
60
+ * db.select().from(usersTable).where(eq(usersTable.id, 1))
61
+ * )
62
+ * ```
63
+ */
64
+ export declare function wrapDrizzleQuery<T>(query: Promise<T>, sql?: string): Promise<T>;
65
+ /**
66
+ * Wrap Bun SQL database for query telemetry
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * import { Database } from 'bun:sqlite'
71
+ * import { wrapBunSQL } from 'binja/debug'
72
+ *
73
+ * const db = wrapBunSQL(new Database('mydb.sqlite'))
74
+ * ```
75
+ */
76
+ export declare function wrapBunSQL<T extends object>(db: T): T;
77
+ /**
78
+ * Wrap any async query function for timing
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { wrapQuery } from 'binja/debug'
83
+ *
84
+ * const result = await wrapQuery(
85
+ * 'SELECT * FROM users WHERE id = ?',
86
+ * () => db.query('SELECT * FROM users WHERE id = ?', [1])
87
+ * )
88
+ * ```
89
+ */
90
+ export declare function wrapQuery<T>(sql: string, queryFn: () => Promise<T>, source?: string): Promise<T>;
91
+ /**
92
+ * Create a query wrapper for any database client
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * import { createQueryWrapper } from 'binja/debug'
97
+ *
98
+ * const query = createQueryWrapper('mysql')
99
+ * const users = await query('SELECT * FROM users', () => mysql.query('SELECT * FROM users'))
100
+ * ```
101
+ */
102
+ export declare function createQueryWrapper(source: string): <T>(sql: string, queryFn: () => Promise<T>) => Promise<T>;
103
+ export {};
104
+ //# sourceMappingURL=integrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../src/debug/integrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,KAAK,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC,CAAA;AAEvD;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAKnD;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,KACtB,QAAQ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,kBAqB/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAYpD;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB;oBAEf,MAAM,UAAU,OAAO,EAAE,GAAG,IAAI;EAcnD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EACjB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,CAAC,CAAC,CAkBZ;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CA0CrD;AA0CD;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,MAAM,SAAQ,GACb,OAAO,CAAC,CAAC,CAAC,CAkBZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,IACvC,CAAC,EAAE,KAAK,MAAM,EAAE,SAAS,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAG/D"}
package/dist/index.js CHANGED
@@ -5081,6 +5081,14 @@ class DebugCollector {
5081
5081
  testsUsed: new Map,
5082
5082
  cacheHits: 0,
5083
5083
  cacheMisses: 0,
5084
+ queries: [],
5085
+ queryStats: {
5086
+ count: 0,
5087
+ totalDuration: 0,
5088
+ slowCount: 0,
5089
+ n1Count: 0,
5090
+ queryCounts: new Map
5091
+ },
5084
5092
  warnings: []
5085
5093
  };
5086
5094
  }
@@ -5221,6 +5229,33 @@ class DebugCollector {
5221
5229
  addWarning(message) {
5222
5230
  this.data.warnings.push(message);
5223
5231
  }
5232
+ recordQuery(query) {
5233
+ const normalizedSql = this.normalizeQuery(query.sql);
5234
+ const currentCount = this.data.queryStats.queryCounts.get(normalizedSql) || 0;
5235
+ this.data.queryStats.queryCounts.set(normalizedSql, currentCount + 1);
5236
+ const isN1 = currentCount >= 2;
5237
+ const queryInfo = {
5238
+ ...query,
5239
+ timestamp: performance.now(),
5240
+ isN1
5241
+ };
5242
+ this.data.queries.push(queryInfo);
5243
+ this.data.queryStats.count++;
5244
+ this.data.queryStats.totalDuration += query.duration;
5245
+ if (query.duration > 100) {
5246
+ this.data.queryStats.slowCount++;
5247
+ }
5248
+ if (isN1 && currentCount === 2) {
5249
+ this.data.queryStats.n1Count++;
5250
+ this.addWarning(`N+1 query detected: ${normalizedSql.slice(0, 50)}...`);
5251
+ }
5252
+ }
5253
+ normalizeQuery(sql) {
5254
+ return sql.replace(/\s+/g, " ").replace(/= \?/g, "= ?").replace(/= \$\d+/g, "= ?").replace(/= '\w+'/g, "= '?'").replace(/= \d+/g, "= ?").replace(/IN \([^)]+\)/gi, "IN (?)").trim();
5255
+ }
5256
+ getQueryStats() {
5257
+ return this.data.queryStats;
5258
+ }
5224
5259
  getData() {
5225
5260
  return this.data;
5226
5261
  }
@@ -5379,6 +5414,25 @@ function generateStyles(id, c2, opts) {
5379
5414
  #${id} .dbg-warning { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(234,179,8,0.1); border-radius: 6px; border-left: 3px solid ${c2.warning}; }
5380
5415
  #${id} .dbg-warning-icon { color: ${c2.warning}; flex-shrink: 0; margin-top: 1px; }
5381
5416
  #${id} .dbg-warning-text { color: ${c2.text}; font-size: 12px; }
5417
+ #${id} .dbg-queries { display: flex; flex-direction: column; gap: 8px; }
5418
+ #${id} .dbg-query { background: ${c2.bg}; border-radius: 6px; overflow: hidden; }
5419
+ #${id} .dbg-query.n1 { border-left: 3px solid ${c2.error}; }
5420
+ #${id} .dbg-query.slow { border-left: 3px solid ${c2.warning}; }
5421
+ #${id} .dbg-query-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; gap: 8px; }
5422
+ #${id} .dbg-query-sql { flex: 1; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; color: ${c2.text}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
5423
+ #${id} .dbg-query-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
5424
+ #${id} .dbg-query-time { font-family: 'SF Mono', Monaco, monospace; font-size: 10px; padding: 2px 6px; border-radius: 3px; background: ${c2.bgTertiary}; }
5425
+ #${id} .dbg-query-time.slow { background: rgba(234,179,8,0.15); color: ${c2.warning}; }
5426
+ #${id} .dbg-query-rows { font-size: 10px; color: ${c2.textMuted}; }
5427
+ #${id} .dbg-query-source { font-size: 9px; padding: 2px 5px; border-radius: 3px; background: rgba(59,130,246,0.1); color: ${c2.accent}; text-transform: uppercase; }
5428
+ #${id} .dbg-query-badge { font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600; }
5429
+ #${id} .dbg-query-badge.n1 { background: rgba(239,68,68,0.15); color: ${c2.error}; }
5430
+ #${id} .dbg-query-stats { display: flex; gap: 12px; margin-bottom: 12px; padding: 10px; background: ${c2.bg}; border-radius: 6px; }
5431
+ #${id} .dbg-query-stat { flex: 1; text-align: center; }
5432
+ #${id} .dbg-query-stat-num { font-size: 18px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; color: ${c2.text}; }
5433
+ #${id} .dbg-query-stat-num.warning { color: ${c2.warning}; }
5434
+ #${id} .dbg-query-stat-num.error { color: ${c2.error}; }
5435
+ #${id} .dbg-query-stat-label { font-size: 10px; color: ${c2.textMuted}; margin-top: 2px; }
5382
5436
  `;
5383
5437
  }
5384
5438
  function getPosition(pos) {
@@ -5403,7 +5457,8 @@ var icons = {
5403
5457
  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>`,
5404
5458
  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>`,
5405
5459
  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>`,
5406
- 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>`
5460
+ 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>`,
5461
+ 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>`
5407
5462
  };
5408
5463
  function generateToggle(id, data, c2) {
5409
5464
  const time2 = (data.totalTime || 0).toFixed(1);
@@ -5432,6 +5487,7 @@ function generatePanel(id, data, c2, opts) {
5432
5487
  ${generateTemplatesSection(data)}
5433
5488
  ${generateContextSection(data)}
5434
5489
  ${generateFiltersSection(data)}
5490
+ ${generateQueriesSection(data)}
5435
5491
  ${generateCacheSection(data)}
5436
5492
  ${generateWarningsSection(data)}
5437
5493
  </div>
@@ -5595,6 +5651,70 @@ function generateWarningsSection(data) {
5595
5651
  </div>
5596
5652
  </div>`;
5597
5653
  }
5654
+ function generateQueriesSection(data) {
5655
+ if (data.queries.length === 0)
5656
+ return "";
5657
+ const stats = data.queryStats;
5658
+ const hasIssues = stats.slowCount > 0 || stats.n1Count > 0;
5659
+ const statsHtml = `
5660
+ <div class="dbg-query-stats">
5661
+ <div class="dbg-query-stat">
5662
+ <div class="dbg-query-stat-num">${stats.count}</div>
5663
+ <div class="dbg-query-stat-label">Queries</div>
5664
+ </div>
5665
+ <div class="dbg-query-stat">
5666
+ <div class="dbg-query-stat-num">${stats.totalDuration.toFixed(1)}ms</div>
5667
+ <div class="dbg-query-stat-label">Total Time</div>
5668
+ </div>
5669
+ <div class="dbg-query-stat">
5670
+ <div class="dbg-query-stat-num ${stats.slowCount > 0 ? "warning" : ""}">${stats.slowCount}</div>
5671
+ <div class="dbg-query-stat-label">Slow (&gt;100ms)</div>
5672
+ </div>
5673
+ <div class="dbg-query-stat">
5674
+ <div class="dbg-query-stat-num ${stats.n1Count > 0 ? "error" : ""}">${stats.n1Count}</div>
5675
+ <div class="dbg-query-stat-label">N+1</div>
5676
+ </div>
5677
+ </div>`;
5678
+ const queries = data.queries.map((q) => {
5679
+ const isSlow = q.duration > 100;
5680
+ const classes = [
5681
+ "dbg-query",
5682
+ q.isN1 ? "n1" : "",
5683
+ isSlow ? "slow" : ""
5684
+ ].filter(Boolean).join(" ");
5685
+ const badges = [];
5686
+ if (q.isN1) {
5687
+ badges.push('<span class="dbg-query-badge n1">N+1</span>');
5688
+ }
5689
+ const rowsText = q.rows !== undefined ? `<span class="dbg-query-rows">${q.rows} rows</span>` : "";
5690
+ const sourceText = q.source ? `<span class="dbg-query-source">${escapeHtml(q.source)}</span>` : "";
5691
+ return `
5692
+ <div class="${classes}">
5693
+ <div class="dbg-query-header">
5694
+ <span class="dbg-query-sql" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>
5695
+ <div class="dbg-query-meta">
5696
+ ${badges.join("")}
5697
+ ${sourceText}
5698
+ ${rowsText}
5699
+ <span class="dbg-query-time ${isSlow ? "slow" : ""}">${q.duration.toFixed(1)}ms</span>
5700
+ </div>
5701
+ </div>
5702
+ </div>`;
5703
+ }).join("");
5704
+ const metaColor = hasIssues ? 'style="color:#ef4444"' : "";
5705
+ return `
5706
+ <div class="dbg-section ${hasIssues ? "open" : ""}">
5707
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
5708
+ <span class="dbg-section-title">${icons.database} Queries</span>
5709
+ <span class="dbg-section-meta" ${metaColor}>${stats.count} (${stats.totalDuration.toFixed(1)}ms)</span>
5710
+ <span class="dbg-chevron">${icons.chevron}</span>
5711
+ </div>
5712
+ <div class="dbg-section-content">
5713
+ ${statsHtml}
5714
+ <div class="dbg-queries">${queries}</div>
5715
+ </div>
5716
+ </div>`;
5717
+ }
5598
5718
  function generateScript(id) {
5599
5719
  return `
5600
5720
  (function(){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "binja",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "High-performance Jinja2/Django template engine for Bun",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",