From b525b0d0de08cc0608b0ea300ffa0f18b216c394 Mon Sep 17 00:00:00 2001 From: Bare7a Date: Tue, 21 Jul 2026 21:14:17 +0300 Subject: [PATCH 1/9] perf(XS-43): Optimize SQL suggestions --- .../editor/lib/createSqlHoverProvider.ts | 9 +- .../src/features/editor/lib/sqlCache.test.ts | 60 +++++++++++++ frontend/src/features/editor/lib/sqlCache.ts | 33 +++++++ .../features/editor/lib/sqlCompletion.test.ts | 87 +++++++++++++++++++ .../src/features/editor/lib/sqlCompletion.ts | 26 +++--- .../src/features/editor/lib/sqlContext.ts | 10 +++ .../src/features/editor/lib/sqlHover.test.ts | 32 ++++++- frontend/src/features/editor/lib/sqlHover.ts | 23 ++++- .../src/features/editor/lib/sqlQueryParse.ts | 21 +++++ .../src/features/editor/lib/sqlStatements.ts | 10 +++ .../src/features/editor/lib/sqlSuggestions.ts | 82 +++++++++++------ frontend/src/features/editor/lib/sqlTokens.ts | 12 ++- 12 files changed, 359 insertions(+), 46 deletions(-) create mode 100644 frontend/src/features/editor/lib/sqlCache.test.ts create mode 100644 frontend/src/features/editor/lib/sqlCache.ts diff --git a/frontend/src/features/editor/lib/createSqlHoverProvider.ts b/frontend/src/features/editor/lib/createSqlHoverProvider.ts index f8b31b3..65336d4 100644 --- a/frontend/src/features/editor/lib/createSqlHoverProvider.ts +++ b/frontend/src/features/editor/lib/createSqlHoverProvider.ts @@ -1,5 +1,5 @@ import type { editor, languages } from 'monaco-editor'; -import { analyzeHover, columnHoverLines } from '@/features/editor/lib/sqlHover'; +import { analyzeHover, columnHoverLines, tableColumnsMarkdown } from '@/features/editor/lib/sqlHover'; import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse'; import { currentStatementRange, parseSqlStatements } from '@/features/editor/lib/sqlStatements'; import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types'; @@ -38,7 +38,12 @@ export function createSqlHoverProvider( }; if (query.lines) { - return { range, contents: query.lines.map((value) => ({ value })) }; + const contents = query.lines.map((value) => ({ value })); + if (query.tableColumns) { + const cols = await onLoadColumns(query.tableColumns.schema, query.tableColumns.table).catch(() => []); + contents.push(...tableColumnsMarkdown(cols).map((value) => ({ value }))); + } + return { range, contents }; } if (query.columnLookup) { for (const binding of query.columnLookup.bindings) { diff --git a/frontend/src/features/editor/lib/sqlCache.test.ts b/frontend/src/features/editor/lib/sqlCache.test.ts new file mode 100644 index 0000000..3aac729 --- /dev/null +++ b/frontend/src/features/editor/lib/sqlCache.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest'; +import { LruCache } from '@/features/editor/lib/sqlCache'; +import { analyzeSqlCursor } from '@/features/editor/lib/sqlContext'; +import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse'; +import { parseSqlStatements } from '@/features/editor/lib/sqlStatements'; +import { tokenizeSql } from '@/features/editor/lib/sqlTokens'; +import type { SchemaInfo, TableInfo } from '@/types'; + +describe('LruCache', () => { + it('evicts the least recently used entry at capacity', () => { + const cache = new LruCache(2); + cache.set('a', 1); + cache.set('b', 2); + cache.get('a'); // refresh: 'b' is now the oldest + cache.set('c', 3); + expect(cache.get('a')).toBe(1); + expect(cache.get('b')).toBeUndefined(); + expect(cache.get('c')).toBe(3); + }); + + it('overwrites an existing key without growing', () => { + const cache = new LruCache(2); + cache.set('a', 1); + cache.set('a', 2); + cache.set('b', 3); + expect(cache.get('a')).toBe(2); + expect(cache.get('b')).toBe(3); + }); +}); + +// One keystroke fans out to several providers analyzing the same strings; these must be shared. +describe('per-keystroke analysis is memoized', () => { + const schemas: SchemaInfo[] = [{ name: 'public' }]; + const tables: TableInfo[] = [{ schema: 'public', name: 'users', type: 'table' }]; + + it('tokenizeSql returns the cached tokens for identical text + driver', () => { + expect(tokenizeSql('SELECT * FROM users', 'postgres')).toBe(tokenizeSql('SELECT * FROM users', 'postgres')); + expect(tokenizeSql('SELECT * FROM users', 'postgres')).not.toBe(tokenizeSql('SELECT * FROM users', 'mysql')); + }); + + it('analyzeSqlCursor returns the cached analysis for identical before-text', () => { + expect(analyzeSqlCursor('SELECT * FROM users WHERE ', 'postgres')).toBe( + analyzeSqlCursor('SELECT * FROM users WHERE ', 'postgres'), + ); + }); + + it('parseSqlStatements returns the cached split for an identical buffer', () => { + expect(parseSqlStatements('SELECT 1;\nSELECT 2;', 'postgres')).toBe( + parseSqlStatements('SELECT 1;\nSELECT 2;', 'postgres'), + ); + }); + + it('parseQueryContext hits only while the schema arrays keep their identity', () => { + const first = parseQueryContext('SELECT * FROM users', tables, schemas, 'postgres'); + expect(parseQueryContext('SELECT * FROM users', tables, schemas, 'postgres')).toBe(first); + // A schema reload swaps the arrays; equal content is not enough for a hit. + const reloaded: TableInfo[] = [{ schema: 'public', name: 'users', type: 'table' }]; + expect(parseQueryContext('SELECT * FROM users', reloaded, schemas, 'postgres')).not.toBe(first); + }); +}); diff --git a/frontend/src/features/editor/lib/sqlCache.ts b/frontend/src/features/editor/lib/sqlCache.ts new file mode 100644 index 0000000..17c0378 --- /dev/null +++ b/frontend/src/features/editor/lib/sqlCache.ts @@ -0,0 +1,33 @@ +// Tiny LRU used to memoize the pure per-keystroke analysis entry points (tokenize, cursor +// analysis, query/statement parsing). One keystroke fans out to several Monaco providers that +// re-analyze identical strings; the cache collapses those to a single computation. +// Pattern borrowed from potygen's inspect cache (packages/potygen/src/inspect/cache.ts). +export class LruCache { + private map = new Map(); + + constructor(private readonly limit: number) {} + + get(key: string): V | undefined { + const value = this.map.get(key); + if (value !== undefined) { + this.map.delete(key); + this.map.set(key, value); + } + return value; + } + + set(key: string, value: V): V { + if (this.map.has(key)) this.map.delete(key); + this.map.set(key, value); + if (this.map.size > this.limit) { + const oldest = this.map.keys().next().value; + if (oldest !== undefined) this.map.delete(oldest); + } + return value; + } +} + +// Cache keys join the dialect and source text; '\0' cannot appear in either side of the key. +export function cacheKey(driver: string | undefined, text: string): string { + return `${driver ?? ''}\0${text}`; +} diff --git a/frontend/src/features/editor/lib/sqlCompletion.test.ts b/frontend/src/features/editor/lib/sqlCompletion.test.ts index e8f02ef..ab53995 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.test.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.test.ts @@ -883,6 +883,93 @@ describe('column suggestions carry PK / NOT NULL hints', () => { }); }); +// Borrowed from potygen: a column name projected by several in-scope sources is ambiguous bare +// (the server rejects it), so it is offered qualified per source instead. +describe('ambiguous columns across joined sources are offered qualified', () => { + const cols: Record = { + 'public.users': [ + { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false }, + { name: 'email', dataType: 'text', isNullable: false, isPrimary: false, isForeign: false }, + ], + 'public.orders': [ + { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false }, + { name: 'total', dataType: 'numeric', isNullable: true, isPrimary: false, isForeign: false }, + ], + }; + const ambComplete = (text: string) => { + const ctx: CompletionContext = { + schemas, + tables, + columns: [], + tablesBySchema: { public: tables }, + columnsByTable: cols, + driver: 'postgres', + }; + const parsed = parseQueryContext(text, tables, schemas, 'postgres'); + return buildCompletionItems({ ctx, text, position: text.length, parsed }); + }; + + it('qualifies the shared column per source and keeps unique columns bare', () => { + const labels = ambComplete('SELECT * FROM users u JOIN orders o ON u.id = o.id WHERE ').map((i) => i.label); + expect(labels).toEqual(expect.arrayContaining(['u.id', 'o.id', 'email', 'total'])); + expect(labels).not.toContain('id'); + }); + + it('uses the table name as qualifier when there is no alias', () => { + const labels = ambComplete('SELECT * FROM users JOIN orders ON ').map((i) => i.label); + expect(labels).toEqual(expect.arrayContaining(['users.id', 'orders.id'])); + expect(labels).not.toContain('id'); + }); + + it('qualifies every column of a self-join per alias', () => { + const labels = ambComplete('SELECT * FROM users a JOIN users b ON ').map((i) => i.label); + expect(labels).toEqual(expect.arrayContaining(['a.id', 'b.id', 'a.email', 'b.email'])); + expect(labels).not.toContain('id'); + expect(labels).not.toContain('email'); + }); + + it('keeps single-table statements fully bare', () => { + expect(columnLabels('SELECT * FROM users WHERE ')).toEqual(expect.arrayContaining(ALL_COLS)); + }); + + it('driver-quotes the qualifier in the inserted text', () => { + const capTables: TableInfo[] = [ + { schema: 'public', name: 'Users', type: 'table' }, + { schema: 'public', name: 'orders', type: 'table' }, + ]; + const shared: ColumnInfo[] = [{ name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false }]; + const ctx: CompletionContext = { + schemas, + tables: capTables, + columns: [], + tablesBySchema: { public: capTables }, + columnsByTable: { 'public.Users': shared, 'public.orders': shared }, + driver: 'postgres', + }; + const text = 'SELECT * FROM "Users" JOIN orders ON '; + const items = buildCompletionItems({ + ctx, + text, + position: text.length, + parsed: parseQueryContext(text, capTables, schemas, 'postgres'), + }); + expect(items.find((i) => i.label === 'Users.id')?.insertText).toBe('"Users".id'); + expect(items.find((i) => i.label === 'orders.id')?.insertText).toBe('orders.id'); + }); +}); + +// Borrowed from potygen: its column completions include the query's sources. The statement is +// parsed whole, so a mid-statement edit of the select list knows the FROM/JOIN refs after the caret. +describe('SELECT list offers the statement’s table refs (mid-statement edit)', () => { + it('offers alias, table and columns while editing the select list', () => { + const text = 'SELECT FROM users u'; + const position = 'SELECT '.length; + const parsed = parseQueryContext(text, tables, schemas, 'postgres'); + const labels = buildCompletionItems({ ctx: makeCtx(), text, position, parsed }).map((i) => i.label); + expect(labels).toEqual(expect.arrayContaining(['u', 'users', 'id', 'email', 'name'])); + }); +}); + describe('keywords are offered by position', () => { it('offers statement starters only at the very start', () => { expect(labelsOf('SEL')).toEqual(expect.arrayContaining(['SELECT'])); diff --git a/frontend/src/features/editor/lib/sqlCompletion.ts b/frontend/src/features/editor/lib/sqlCompletion.ts index 6901aa4..84ebe4c 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.ts @@ -18,7 +18,7 @@ import { keywordsForShape, rank, suggestColumnsForTable, - suggestColumnsFromBindings, + suggestColumnsInScope, suggestCteItems, suggestQueryTableRefs, suggestSchemas, @@ -190,13 +190,12 @@ function orderGroupItems( ctx: CompletionContext, slot: Extract, queryTables: QueryTableRef[], - bindings: Map, ): CompletionItem[] { // Columns/tables only at the start of a sort term (after BY/comma), not after a finished one. const items: CompletionItem[] = slot.expectsExpr ? [ ...suggestQueryTableRefs(ctx, queryTables, slot.prefix), - ...suggestColumnsFromBindings(ctx, bindings, slot.prefix), + ...suggestColumnsInScope(ctx, queryTables, slot.prefix), ] : []; const keywords = slot.directionAllowed ? ['ASC', 'DESC', ...slot.trailingKeywords] : slot.trailingKeywords; @@ -213,13 +212,13 @@ function generalItems( shape: StatementShape, parsed: ParsedQuery, ): CompletionItem[] { - const { queryTables, bindings } = parsed; + const { queryTables } = parsed; const items: CompletionItem[] = []; // Identifiers only when the preceding token expects one; keywords always flow through. if (slot.inFilter && slot.expectsExpr) { if (shape.afterOnKeyword && slot.prefix === '') items.push(...fkJoinItems(ctx, queryTables)); items.push(...suggestQueryTableRefs(ctx, queryTables, slot.prefix)); - items.push(...suggestColumnsFromBindings(ctx, bindings, slot.prefix)); + items.push(...suggestColumnsInScope(ctx, queryTables, slot.prefix)); items.push(...inScopeVirtualColumnItems(ctx, parsed, slot.prefix)); } @@ -229,7 +228,10 @@ function generalItems( } if (slot.expectsExpr && shape.inSelectList) { - items.push(...suggestColumnsFromBindings(ctx, bindings, slot.prefix)); + // The statement is parsed whole, so FROM/JOIN sources after the caret are known here. + // Offer them alongside their columns (potygen does the same) to make qualifying easy. + items.push(...suggestQueryTableRefs(ctx, queryTables, slot.prefix)); + items.push(...suggestColumnsInScope(ctx, queryTables, slot.prefix)); } if (slot.expectsExpr && !shape.hasFrom && !slot.inFilter) { items.push(...suggestSchemas(ctx, slot.prefix)); @@ -240,7 +242,7 @@ function generalItems( function completionItems(input: BuildCompletionInput, cursor: SqlCursor): CompletionItem[] { const { ctx, parsed } = input; const { slot, shape } = cursor; - const { queryTables, bindings } = parsed; + const { queryTables } = parsed; switch (slot.kind) { case 'none': @@ -253,28 +255,28 @@ function completionItems(input: BuildCompletionInput, cursor: SqlCursor): Comple } case 'insert-columns': { const used = new Set(slot.used); - return suggestColumnsFromBindings(ctx, bindings, slot.prefix).filter( + return suggestColumnsInScope(ctx, queryTables, slot.prefix).filter( (item) => !used.has(item.label.toLowerCase()), ); } case 'set-column': { - const items = suggestColumnsFromBindings(ctx, bindings, slot.prefix); + const items = suggestColumnsInScope(ctx, queryTables, slot.prefix); return slot.leadingSpace ? withLeadingSpace(items) : items; } case 'filter-start': return withLeadingSpace([ ...(shape.afterOnKeyword ? fkJoinItems(ctx, queryTables) : []), ...suggestQueryTableRefs(ctx, queryTables, ''), - ...suggestColumnsFromBindings(ctx, bindings, ''), + ...suggestColumnsInScope(ctx, queryTables, ''), ...inScopeVirtualColumnItems(ctx, parsed, ''), ]); case 'value': return [ - ...suggestValueItems(ctx, queryTables, bindings, slot.prefix), + ...suggestValueItems(ctx, queryTables, slot.prefix), ...inScopeVirtualColumnItems(ctx, parsed, slot.prefix), ]; case 'order-group': { - const items = orderGroupItems(ctx, slot, queryTables, bindings); + const items = orderGroupItems(ctx, slot, queryTables); if (slot.expectsExpr) items.push(...inScopeVirtualColumnItems(ctx, parsed, slot.prefix)); return slot.leadingSpace ? withLeadingSpace(items) : items; } diff --git a/frontend/src/features/editor/lib/sqlContext.ts b/frontend/src/features/editor/lib/sqlContext.ts index 8288c85..bc80699 100644 --- a/frontend/src/features/editor/lib/sqlContext.ts +++ b/frontend/src/features/editor/lib/sqlContext.ts @@ -1,3 +1,4 @@ +import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; import { isIdentLike, isKeyword, type SqlToken, tokenIdentText, tokenizeSql } from '@/features/editor/lib/sqlTokens'; import type { DriverType } from '@/types'; @@ -244,7 +245,16 @@ function lowerIdent(t: SqlToken): string { return tokenIdentText(t).toLowerCase(); } +// One completion request calls this three times with the same before-text (column prefetch, +// item building, replace range); the cache turns that into a single analysis. +const cursorCache = new LruCache(16); + export function analyzeSqlCursor(before: string, driver?: DriverType): SqlCursor { + const key = cacheKey(driver, before); + return cursorCache.get(key) ?? cursorCache.set(key, analyzeCursor(before, driver)); +} + +function analyzeCursor(before: string, driver?: DriverType): SqlCursor { const all = tokenizeSql(before, driver); const len = before.length; diff --git a/frontend/src/features/editor/lib/sqlHover.test.ts b/frontend/src/features/editor/lib/sqlHover.test.ts index 030f9e0..af0bed7 100644 --- a/frontend/src/features/editor/lib/sqlHover.test.ts +++ b/frontend/src/features/editor/lib/sqlHover.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { analyzeHover, columnHoverLines } from '@/features/editor/lib/sqlHover'; +import { analyzeHover, columnHoverLines, tableColumnsMarkdown } from '@/features/editor/lib/sqlHover'; import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse'; -import type { SchemaInfo, TableInfo } from '@/types'; +import type { ColumnInfo, SchemaInfo, TableInfo } from '@/types'; const schemas: SchemaInfo[] = [{ name: 'public' }]; const tables: TableInfo[] = [ @@ -70,4 +70,32 @@ describe('analyzeHover', () => { ); expect(lines).toEqual(['**email** · text · not null', 'column of public.users']); }); + + // Borrowed from potygen's quick info: a table hover carries its relation so the provider can + // append the column list. + it('marks table and alias hovers for a column-list append', () => { + expect(hoverAt('SELECT * FROM users', 'users')?.tableColumns).toEqual({ schema: 'public', table: 'users' }); + const alias = hoverAt('SELECT * FROM users u WHERE u.id = 1', 'u ', 1); + expect(alias?.tableColumns).toEqual({ schema: 'public', table: 'users' }); + }); + + it('renders the column list as a markdown table, capped', () => { + const cols: ColumnInfo[] = [ + { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false }, + { name: 'email', dataType: 'text', isNullable: false, isPrimary: false, isForeign: false }, + ]; + expect(tableColumnsMarkdown(cols)).toEqual([ + '| column | type |\n| --- | --- |\n| id | int · PK |\n| email | text · not null |', + ]); + expect(tableColumnsMarkdown([])).toEqual([]); + + const many: ColumnInfo[] = Array.from({ length: 35 }, (_, i) => ({ + name: `c${i}`, + dataType: 'int', + isNullable: true, + isPrimary: false, + isForeign: false, + })); + expect(tableColumnsMarkdown(many)[1]).toBe('… 5 more columns'); + }); }); diff --git a/frontend/src/features/editor/lib/sqlHover.ts b/frontend/src/features/editor/lib/sqlHover.ts index e02ec8c..d26df6f 100644 --- a/frontend/src/features/editor/lib/sqlHover.ts +++ b/frontend/src/features/editor/lib/sqlHover.ts @@ -10,6 +10,8 @@ export interface HoverQuery { end: number; lines?: string[]; columnLookup?: { bindings: TableBinding[]; name: string }; + // Table/alias hover: the caller appends this relation's column list (loaded lazily). + tableColumns?: TableBinding; } export function columnHoverLines(col: ColumnInfo, table: TableBinding): string[] { @@ -17,6 +19,15 @@ export function columnHoverLines(col: ColumnInfo, table: TableBinding): string[] return [`**${col.name}** · ${columnDetail(col)}`, `column of ${where}`]; } +// Markdown column table for the table-hover card (potygen-style quick info). +export function tableColumnsMarkdown(cols: ColumnInfo[], max = 30): string[] { + if (cols.length === 0) return []; + const shown = cols.slice(0, max); + const rows = shown.map((c) => `| ${c.name} | ${columnDetail(c)} |`); + const table = ['| column | type |', '| --- | --- |', ...rows].join('\n'); + return cols.length > shown.length ? [table, `… ${cols.length - shown.length} more columns`] : [table]; +} + function tableLines(t: TableInfo): string[] { return [`**${t.name}** · ${t.type || 'table'}`, t.schema ? `schema ${t.schema}` : ''].filter(Boolean); } @@ -75,9 +86,13 @@ export function analyzeHover( if (bound) { const info = tables.find((t) => t.name === bound.table && t.schema === bound.schema); if (bound.table.toLowerCase() !== nameLc) { - return { ...span, lines: [`**${name}** · alias for ${bound.table}`, ...(info ? tableLines(info).slice(1) : [])] }; + return { + ...span, + lines: [`**${name}** · alias for ${bound.table}`, ...(info ? tableLines(info).slice(1) : [])], + tableColumns: info ? bound : undefined, + }; } - if (info) return { ...span, lines: tableLines(info) }; + if (info) return { ...span, lines: tableLines(info), tableColumns: bound }; } // CTE / derived-table alias. @@ -90,7 +105,9 @@ export function analyzeHover( // Any table in the connected schema. const table = tables.find((t) => t.name.toLowerCase() === nameLc); - if (table) return { ...span, lines: tableLines(table) }; + if (table) { + return { ...span, lines: tableLines(table), tableColumns: { schema: table.schema, table: table.name } }; + } // A schema name. const schema = schemas.find((s) => s.name.toLowerCase() === nameLc); diff --git a/frontend/src/features/editor/lib/sqlQueryParse.ts b/frontend/src/features/editor/lib/sqlQueryParse.ts index 72316e2..001faaa 100644 --- a/frontend/src/features/editor/lib/sqlQueryParse.ts +++ b/frontend/src/features/editor/lib/sqlQueryParse.ts @@ -1,3 +1,4 @@ +import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; import { ALIAS_STOP_WORDS, unquoteIdent } from '@/features/editor/lib/sqlQuoting'; import { isIdentLike, @@ -238,12 +239,32 @@ function collectCtes(tokens: SqlToken[], virtualColumns: Map): return ctes; } +// Completion, hover and diagnostics each parse the current statement per keystroke; diagnostics +// re-parse every statement in the buffer, so unchanged statements should be cache hits. The +// schema arrays participate in resolution, so a hit also requires their identity to match. +interface ParseCacheEntry { + tables: TableInfo[]; + schemas: SchemaInfo[]; + result: ParsedQuery; +} + +const parseCache = new LruCache(64); + export function parseQueryContext( sql: string, tables: TableInfo[], schemas: SchemaInfo[], driver: DriverType, ): ParsedQuery { + const key = cacheKey(driver, sql); + const hit = parseCache.get(key); + if (hit && hit.tables === tables && hit.schemas === schemas) return hit.result; + const result = parseQuery(sql, tables, schemas, driver); + parseCache.set(key, { tables, schemas, result }); + return result; +} + +function parseQuery(sql: string, tables: TableInfo[], schemas: SchemaInfo[], driver: DriverType): ParsedQuery { const tokens = tokenizeSql(sql, driver); const queryTables: QueryTableRef[] = []; const virtualColumns = new Map(); diff --git a/frontend/src/features/editor/lib/sqlStatements.ts b/frontend/src/features/editor/lib/sqlStatements.ts index 20a98f7..a359a9f 100644 --- a/frontend/src/features/editor/lib/sqlStatements.ts +++ b/frontend/src/features/editor/lib/sqlStatements.ts @@ -1,3 +1,4 @@ +import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; import { findBlockCommentEnd, findLineCommentEnd, @@ -47,7 +48,16 @@ function firstCodeOffset(text: string, from: number, to: number, opts: SqlLexOpt // mysql-client `DELIMITER xx` line: switches the terminator so procedure bodies can contain `;`. const DELIMITER_LINE_RE = /DELIMITER[ \t]+(\S+)[ \t]*(?:\r?\n|$)/iy; +// Completion, hover, run glyphs and diagnostics all split the same (whole) buffer; entries are +// buffer-sized, so the cache stays small. +const statementCache = new LruCache(8); + export function parseSqlStatements(sql: string, driver?: DriverType): SqlStatement[] { + const key = cacheKey(driver, sql); + return statementCache.get(key) ?? statementCache.set(key, splitStatements(sql, driver)); +} + +function splitStatements(sql: string, driver?: DriverType): SqlStatement[] { const opts = lexOptionsFor(driver); const clientDelimiters = driver === 'mysql'; const statements: SqlStatement[] = []; diff --git a/frontend/src/features/editor/lib/sqlSuggestions.ts b/frontend/src/features/editor/lib/sqlSuggestions.ts index 77dbd8c..6ab1c51 100644 --- a/frontend/src/features/editor/lib/sqlSuggestions.ts +++ b/frontend/src/features/editor/lib/sqlSuggestions.ts @@ -319,33 +319,72 @@ export function suggestQueryTableRefs( return items; } -export function suggestColumnsFromBindings( +// Columns of every relation the statement binds, one source per FROM/JOIN/UPDATE/INSERT ref. +// A name projected by several sources is ambiguous bare - the server would reject it - so those +// are offered qualified per source (`alias.col`). Potygen drops ambiguous columns from its +// completions; qualifying keeps them reachable. +export function suggestColumnsInScope( ctx: CompletionContext, - bindings: Map, + queryTables: QueryTableRef[], lcPrefix: string, + seen?: Set, ): CompletionItem[] { - const items: CompletionItem[] = []; - const seen = new Set(); - const seenTables = new Set(); + const sources: { ref: QueryTableRef; cols: ColumnInfo[] }[] = []; + const seenRefs = new Set(); + for (const ref of queryTables) { + const refKey = `${columnCacheKey(ref.schema, ref.table)}|${(ref.alias ?? '').toLowerCase()}`; + if (seenRefs.has(refKey)) continue; + seenRefs.add(refKey); + sources.push({ ref, cols: ctx.columnsByTable[columnCacheKey(ref.schema, ref.table)] || [] }); + } - for (const binding of bindings.values()) { - const tk = columnCacheKey(binding.schema, binding.table); - if (seenTables.has(tk)) continue; - seenTables.add(tk); - pushColumnItems(items, ctx.columnsByTable[tk] || [], 0, lcPrefix, ctx.driver, seen); + // How many sources project each name; self-joins count once per alias, like the server does. + const nameCount = new Map(); + for (const s of sources) { + for (const name of new Set(s.cols.map((c) => c.name.toLowerCase()))) { + nameCount.set(name, (nameCount.get(name) ?? 0) + 1); + } } + const items: CompletionItem[] = []; + for (const s of sources) { + const emitted = new Set(); + for (const c of s.cols) { + const key = c.name.toLowerCase(); + if (emitted.has(key)) continue; + emitted.add(key); + const score = matchScore(c.name, lcPrefix); + if (score < 0) continue; + if ((nameCount.get(key) ?? 0) > 1) { + const qualifier = s.ref.alias ?? s.ref.table; + const label = `${qualifier}.${c.name}`; + seen?.add(key); + items.push({ + label, + kind: 'field', + detail: columnDetail(c), + insertText: `${formatSqlIdentifier(qualifier, ctx.driver)}.${formatSqlIdentifier(c.name, ctx.driver)}`, + sortText: rank(0, score, label), + }); + } else { + if (seen?.has(key)) continue; + seen?.add(key); + items.push({ + label: c.name, + kind: 'field', + detail: columnDetail(c), + insertText: formatSqlIdentifier(c.name, ctx.driver), + sortText: rank(0, score, c.name), + }); + } + } + } return items; } const VALUE_LITERALS = ['NULL', 'TRUE', 'FALSE', 'DEFAULT']; -export function suggestValueItems( - ctx: CompletionContext, - queryTables: QueryTableRef[], - bindings: Map, - lcPrefix: string, -): CompletionItem[] { +export function suggestValueItems(ctx: CompletionContext, queryTables: QueryTableRef[], lcPrefix: string): CompletionItem[] { const items: CompletionItem[] = []; const seenCols = new Set(); @@ -360,16 +399,7 @@ export function suggestValueItems( if (item) items.push(item); } - for (const binding of bindings.values()) { - pushColumnItems( - items, - ctx.columnsByTable[columnCacheKey(binding.schema, binding.table)] || [], - 0, - lcPrefix, - ctx.driver, - seenCols, - ); - } + items.push(...suggestColumnsInScope(ctx, queryTables, lcPrefix, seenCols)); pushColumnItems(items, ctx.columns, 1, lcPrefix, ctx.driver, seenCols); return items; diff --git a/frontend/src/features/editor/lib/sqlTokens.ts b/frontend/src/features/editor/lib/sqlTokens.ts index 724ecc9..5d1391e 100644 --- a/frontend/src/features/editor/lib/sqlTokens.ts +++ b/frontend/src/features/editor/lib/sqlTokens.ts @@ -1,3 +1,4 @@ +import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; import { findBlockCommentEnd, findLineCommentEnd, @@ -34,8 +35,17 @@ const WS_RE = /\s/; const TWO_CHAR_OPS = new Set(['<=', '>=', '<>', '!=', '::', '||', ':=']); const PUNCT = new Set(['.', ',', '(', ')', ';']); -// One pass; $tag$ delimiters are ops so dollar-quoted bodies stay tokenized (completion works inside them). +// Completion, hover and query parsing all tokenize the same statement text within one keystroke. +// The result is shared, so callers must treat the returned tokens as immutable. +const tokenCache = new LruCache(64); + export function tokenizeSql(text: string, driver?: DriverType): SqlToken[] { + const key = cacheKey(driver, text); + return tokenCache.get(key) ?? tokenCache.set(key, tokenize(text, driver)); +} + +// One pass; $tag$ delimiters are ops so dollar-quoted bodies stay tokenized (completion works inside them). +function tokenize(text: string, driver?: DriverType): SqlToken[] { const opts = lexOptionsFor(driver); const len = text.length; const tokens: SqlToken[] = []; From b08982636bba5d809bdd9812a7daf2b3f40a5d98 Mon Sep 17 00:00:00 2001 From: Bare7a Date: Tue, 21 Jul 2026 21:43:55 +0300 Subject: [PATCH 2/9] Updates LRU from 64 to 256 --- frontend/src/features/editor/lib/sqlQueryParse.ts | 4 +++- frontend/src/features/editor/lib/sqlTokens.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/editor/lib/sqlQueryParse.ts b/frontend/src/features/editor/lib/sqlQueryParse.ts index 001faaa..4c6d810 100644 --- a/frontend/src/features/editor/lib/sqlQueryParse.ts +++ b/frontend/src/features/editor/lib/sqlQueryParse.ts @@ -248,7 +248,9 @@ interface ParseCacheEntry { result: ParsedQuery; } -const parseCache = new LruCache(64); +// Capacity must exceed the statement count of a typical buffer (diagnostics parse every +// statement per pass); beyond it the cache degrades gracefully to recomputing. +const parseCache = new LruCache(256); export function parseQueryContext( sql: string, diff --git a/frontend/src/features/editor/lib/sqlTokens.ts b/frontend/src/features/editor/lib/sqlTokens.ts index 5d1391e..2e2e4b9 100644 --- a/frontend/src/features/editor/lib/sqlTokens.ts +++ b/frontend/src/features/editor/lib/sqlTokens.ts @@ -36,8 +36,9 @@ const TWO_CHAR_OPS = new Set(['<=', '>=', '<>', '!=', '::', '||', ':=']); const PUNCT = new Set(['.', ',', '(', ')', ';']); // Completion, hover and query parsing all tokenize the same statement text within one keystroke. -// The result is shared, so callers must treat the returned tokens as immutable. -const tokenCache = new LruCache(64); +// The result is shared, so callers must treat the returned tokens as immutable. Capacity must +// exceed the statement count of a typical buffer, or a diagnostics pass cycles the whole cache. +const tokenCache = new LruCache(256); export function tokenizeSql(text: string, driver?: DriverType): SqlToken[] { const key = cacheKey(driver, text); From 45eb153e8716ca4049cc95438574ab2e2d2aeb00 Mon Sep 17 00:00:00 2001 From: Bare7a Date: Wed, 22 Jul 2026 11:28:57 +0300 Subject: [PATCH 3/9] Adds translation for the tables description --- .../src/features/editor/SqlConditionInput.tsx | 3 +- frontend/src/features/editor/SqlEditor.tsx | 2 +- .../editor/hooks/useSqlDiagnostics.ts | 3 +- .../src/features/editor/lib/sqlCompletion.ts | 7 ++- .../src/features/editor/lib/sqlDiagnostics.ts | 3 +- frontend/src/features/editor/lib/sqlHover.ts | 59 ++++++++++++++----- .../src/features/editor/lib/sqlSuggestions.ts | 29 ++++++--- frontend/src/i18n/locales/bg.json | 26 +++++++- frontend/src/i18n/locales/de.json | 26 +++++++- frontend/src/i18n/locales/en.json | 26 +++++++- frontend/src/test/setupI18n.ts | 19 ++++++ frontend/vitest.config.ts | 1 + 12 files changed, 171 insertions(+), 33 deletions(-) create mode 100644 frontend/src/test/setupI18n.ts diff --git a/frontend/src/features/editor/SqlConditionInput.tsx b/frontend/src/features/editor/SqlConditionInput.tsx index 518bc10..721662f 100644 --- a/frontend/src/features/editor/SqlConditionInput.tsx +++ b/frontend/src/features/editor/SqlConditionInput.tsx @@ -5,6 +5,7 @@ import { MONACO_FONT_METRICS_OPTIONS } from '@/features/editor/lib/monacoFontMet import { getMonacoThemeName, setupMonacoBeforeMount } from '@/features/editor/lib/monacoTheme'; import { formatSqlIdentifier } from '@/features/editor/lib/sqlQuoting'; import { matchScore, rank } from '@/features/editor/lib/sqlSuggestions'; +import { t } from '@/i18n'; import { useAppTheme } from '@/shared/hooks/useAppTheme'; import { useUiZoom } from '@/shared/hooks/useUiZoom'; import { cx } from '@/shared/lib/cx'; @@ -184,7 +185,7 @@ export function SqlConditionInput({ suggestions.push({ label: col, kind: Kind.Field, - detail: types?.[i] || 'column', + detail: types?.[i] || t('editor.sql.column'), insertText: formatSqlIdentifier(col, drv), sortText: rank(0, score, col), range, diff --git a/frontend/src/features/editor/SqlEditor.tsx b/frontend/src/features/editor/SqlEditor.tsx index d136b5c..3f09a6b 100644 --- a/frontend/src/features/editor/SqlEditor.tsx +++ b/frontend/src/features/editor/SqlEditor.tsx @@ -173,7 +173,7 @@ export const SqlEditor = memo(function SqlEditor({ }, []); const { updateRunGlyphs, statementsRef } = useRunGlyphs(editorRef, monacoRef, sql, languageRevision, driver); - useSqlDiagnostics(editorRef, monacoRef, sql, allTables, schemas, driver); + useSqlDiagnostics(editorRef, monacoRef, sql, allTables, schemas, driver, languageRevision); const { bindEditorActions } = useEditorActions({ editorRef, diff --git a/frontend/src/features/editor/hooks/useSqlDiagnostics.ts b/frontend/src/features/editor/hooks/useSqlDiagnostics.ts index 37fbe38..9065c9c 100644 --- a/frontend/src/features/editor/hooks/useSqlDiagnostics.ts +++ b/frontend/src/features/editor/hooks/useSqlDiagnostics.ts @@ -14,6 +14,7 @@ export function useSqlDiagnostics( tables: TableInfo[], schemas: SchemaInfo[], driver: DriverType, + languageRevision = 0, ) { useEffect(() => { const ed = editorRef.current; @@ -46,5 +47,5 @@ export function useSqlDiagnostics( clearTimeout(handle); if (!model.isDisposed()) monaco.editor.setModelMarkers(model, SCHEMA_MARKER_OWNER, []); }; - }, [editorRef, monacoRef, sql, tables, schemas, driver]); + }, [editorRef, monacoRef, sql, tables, schemas, driver, languageRevision]); } diff --git a/frontend/src/features/editor/lib/sqlCompletion.ts b/frontend/src/features/editor/lib/sqlCompletion.ts index 84ebe4c..112b96f 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.ts @@ -27,6 +27,7 @@ import { suggestVirtualColumns, withLeadingSpace, } from '@/features/editor/lib/sqlSuggestions'; +import { t } from '@/i18n'; import type { DriverType, SchemaInfo, TableInfo } from '@/types'; export interface BindingsNeedingColumnsCtx { @@ -131,7 +132,7 @@ function fkJoinItems(ctx: CompletionContext, queryTables: QueryTableRef[]): Comp items.push({ label: expr, kind: 'field', - detail: 'foreign key', + detail: t('editor.sql.foreignKey'), insertText: expr, sortText: rank(0, 0, expr), }); @@ -141,7 +142,9 @@ function fkJoinItems(ctx: CompletionContext, queryTables: QueryTableRef[]): Comp } function virtualDetail(parsed: ParsedQuery, nameLc: string): string { - return parsed.ctes.some((c) => c.toLowerCase() === nameLc) ? 'CTE column' : 'subquery column'; + return parsed.ctes.some((c) => c.toLowerCase() === nameLc) + ? t('editor.sql.cteColumn') + : t('editor.sql.subqueryColumn'); } // Derived-table aliases are always in scope; CTEs only once referenced in FROM/JOIN. diff --git a/frontend/src/features/editor/lib/sqlDiagnostics.ts b/frontend/src/features/editor/lib/sqlDiagnostics.ts index 64f2abd..5affe81 100644 --- a/frontend/src/features/editor/lib/sqlDiagnostics.ts +++ b/frontend/src/features/editor/lib/sqlDiagnostics.ts @@ -1,5 +1,6 @@ import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse'; import { parseSqlStatements } from '@/features/editor/lib/sqlStatements'; +import { t } from '@/i18n'; import type { DriverType, SchemaInfo, TableInfo } from '@/types'; export interface SqlDiagnostic { @@ -24,7 +25,7 @@ export function collectSchemaDiagnostics( out.push({ start: stmt.start + ref.nameStart, end: stmt.start + ref.nameEnd, - message: `Unknown table "${ref.table}"`, + message: t('editor.sql.unknownTable', { table: ref.table }), }); } } diff --git a/frontend/src/features/editor/lib/sqlHover.ts b/frontend/src/features/editor/lib/sqlHover.ts index d26df6f..f9cd473 100644 --- a/frontend/src/features/editor/lib/sqlHover.ts +++ b/frontend/src/features/editor/lib/sqlHover.ts @@ -1,7 +1,8 @@ import { type ParsedQuery, resolveDotCompletion, type TableBinding } from '@/features/editor/lib/sqlQueryParse'; import { QUOTE_FORCING_KEYWORDS, unquoteIdent } from '@/features/editor/lib/sqlQuoting'; -import { columnDetail } from '@/features/editor/lib/sqlSuggestions'; +import { columnDetail, relationTypeLabel } from '@/features/editor/lib/sqlSuggestions'; import { isIdentLike, type SqlToken, tokenIdentText, tokenizeSql } from '@/features/editor/lib/sqlTokens'; +import { t } from '@/i18n'; import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types'; // Immediate answer (`lines`) or a column lookup the caller resolves via cached loads. @@ -16,7 +17,7 @@ export interface HoverQuery { export function columnHoverLines(col: ColumnInfo, table: TableBinding): string[] { const where = table.schema ? `${table.schema}.${table.table}` : table.table; - return [`**${col.name}** · ${columnDetail(col)}`, `column of ${where}`]; + return [`**${col.name}** · ${columnDetail(col)}`, t('editor.sql.columnOf', { target: where })]; } // Markdown column table for the table-hover card (potygen-style quick info). @@ -24,16 +25,25 @@ export function tableColumnsMarkdown(cols: ColumnInfo[], max = 30): string[] { if (cols.length === 0) return []; const shown = cols.slice(0, max); const rows = shown.map((c) => `| ${c.name} | ${columnDetail(c)} |`); - const table = ['| column | type |', '| --- | --- |', ...rows].join('\n'); - return cols.length > shown.length ? [table, `… ${cols.length - shown.length} more columns`] : [table]; + const table = [`| ${t('editor.sql.column')} | ${t('editor.sql.type')} |`, '| --- | --- |', ...rows].join('\n'); + return cols.length > shown.length + ? [table, t('editor.sql.moreColumns', { count: cols.length - shown.length })] + : [table]; } -function tableLines(t: TableInfo): string[] { - return [`**${t.name}** · ${t.type || 'table'}`, t.schema ? `schema ${t.schema}` : ''].filter(Boolean); +function tableLines(info: TableInfo): string[] { + return [ + t('editor.sql.nameKind', { name: `**${info.name}**`, kind: relationTypeLabel(info.type) }), + info.schema ? t('editor.sql.schemaName', { name: info.schema }) : '', + ].filter(Boolean); +} + +function virtualKindLabel(isCte: boolean): string { + return isCte ? t('editor.sql.cte') : t('editor.sql.subquery'); } function tokenAt(tokens: SqlToken[], offset: number): SqlToken | undefined { - return tokens.find((t) => offset >= t.start && offset < t.end && isIdentLike(t)); + return tokens.find((tok) => offset >= tok.start && offset < tok.end && isIdentLike(tok)); } export function analyzeHover( @@ -44,7 +54,7 @@ export function analyzeHover( schemas: SchemaInfo[], driver: DriverType, ): HoverQuery | null { - const tokens = tokenizeSql(stmtText, driver).filter((t) => t.kind !== 'comment'); + const tokens = tokenizeSql(stmtText, driver).filter((tok) => tok.kind !== 'comment'); const tok = tokenAt(tokens, offset); if (!tok) return null; // Bare keywords aren't identifiers; quoted tokens always are. @@ -73,8 +83,8 @@ export function analyzeHover( const virtual = parsed.virtualColumns.get(qualLc); if (virtual) { if (!virtual.some((c) => c.toLowerCase() === nameLc)) return null; - const kind = parsed.ctes.some((c) => c.toLowerCase() === qualLc) ? 'CTE' : 'subquery'; - return { ...span, lines: [`**${name}**`, `column of ${kind} ${qualLc}`] }; + const kind = virtualKindLabel(parsed.ctes.some((c) => c.toLowerCase() === qualLc)); + return { ...span, lines: [`**${name}**`, t('editor.sql.columnOf', { target: `${kind} ${qualLc}` })] }; } const binding = resolveDotCompletion({ segments }, parsed.bindings, tables, schemas, driver); if (binding) return { ...span, columnLookup: { bindings: [binding], name: nameLc } }; @@ -84,11 +94,17 @@ export function analyzeHover( // Alias or table referenced by this query. const bound = parsed.bindings.get(nameLc); if (bound) { - const info = tables.find((t) => t.name === bound.table && t.schema === bound.schema); + const info = tables.find((tbl) => tbl.name === bound.table && tbl.schema === bound.schema); if (bound.table.toLowerCase() !== nameLc) { return { ...span, - lines: [`**${name}** · alias for ${bound.table}`, ...(info ? tableLines(info).slice(1) : [])], + lines: [ + t('editor.sql.nameKind', { + name: `**${name}**`, + kind: t('editor.sql.aliasFor', { table: bound.table }), + }), + ...(info ? tableLines(info).slice(1) : []), + ], tableColumns: info ? bound : undefined, }; } @@ -98,20 +114,31 @@ export function analyzeHover( // CTE / derived-table alias. const virtual = parsed.virtualColumns.get(nameLc); if (virtual) { - const kind = parsed.ctes.some((c) => c.toLowerCase() === nameLc) ? 'CTE' : 'subquery'; + const kind = virtualKindLabel(parsed.ctes.some((c) => c.toLowerCase() === nameLc)); const cols = virtual.length > 0 ? virtual.slice(0, 8).join(', ') + (virtual.length > 8 ? ', …' : '') : ''; - return { ...span, lines: [`**${name}** · ${kind}`, cols && `columns: ${cols}`].filter(Boolean) as string[] }; + return { + ...span, + lines: [ + t('editor.sql.nameKind', { name: `**${name}**`, kind }), + cols ? t('editor.sql.columnsList', { cols }) : '', + ].filter(Boolean) as string[], + }; } // Any table in the connected schema. - const table = tables.find((t) => t.name.toLowerCase() === nameLc); + const table = tables.find((tbl) => tbl.name.toLowerCase() === nameLc); if (table) { return { ...span, lines: tableLines(table), tableColumns: { schema: table.schema, table: table.name } }; } // A schema name. const schema = schemas.find((s) => s.name.toLowerCase() === nameLc); - if (schema) return { ...span, lines: [`**${schema.name}** · schema`] }; + if (schema) { + return { + ...span, + lines: [t('editor.sql.nameKind', { name: `**${schema.name}**`, kind: t('editor.sql.schema') })], + }; + } // Bare column: search the query's in-scope tables (loaded lazily by the caller). const candidates = [...parsed.bindings.values()].filter( diff --git a/frontend/src/features/editor/lib/sqlSuggestions.ts b/frontend/src/features/editor/lib/sqlSuggestions.ts index 6ab1c51..69fc3a8 100644 --- a/frontend/src/features/editor/lib/sqlSuggestions.ts +++ b/frontend/src/features/editor/lib/sqlSuggestions.ts @@ -1,6 +1,7 @@ import type { StatementShape } from '@/features/editor/lib/sqlContext'; import type { QueryTableRef, TableBinding } from '@/features/editor/lib/sqlQueryParse'; import { columnCacheKey, formatSqlIdentifier } from '@/features/editor/lib/sqlQuoting'; +import { t } from '@/i18n'; import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types'; export interface CompletionContext { @@ -144,13 +145,22 @@ export function rank(tier: 0 | 1 | 2 | 3 | 4 | 5, score: number, label: string): // Column hint shown in the suggestion's detail line: type plus PK / FK / NOT NULL markers. export function columnDetail(c: ColumnInfo): string { const tags: string[] = []; - if (c.isPrimary) tags.push('PK'); - if (c.isForeign) tags.push('FK'); + if (c.isPrimary) tags.push(t('editor.sql.pk')); + if (c.isForeign) tags.push(t('editor.sql.fk')); if (tags.length > 0) return `${c.dataType} · ${tags.join(' · ')}`; - if (!c.isNullable) return `${c.dataType} · not null`; + if (!c.isNullable) return `${c.dataType} · ${t('editor.sql.notNull')}`; return c.dataType; } +// Localize common relation kinds from the schema catalog; unknown values pass through. +export function relationTypeLabel(type?: string): string { + const raw = (type || 'table').trim(); + const key = raw.toLowerCase(); + if (key === 'table' || key === 'base table') return t('editor.sql.table'); + if (key === 'view') return t('editor.sql.view'); + return raw; +} + export function keywordItem(kw: string, lcPrefix: string): CompletionItem | null { const score = matchScore(kw, lcPrefix); if (score < 0) return null; @@ -220,7 +230,7 @@ export function suggestCteItems(ctes: string[], lcPrefix: string, driver: Driver items.push({ label: name, kind: 'class', - detail: 'CTE', + detail: t('editor.sql.cte'), insertText: formatSqlIdentifier(name, driver), filterText: formatSqlIdentifier(name, driver), // Tier 0 (query-local): ranks above the table list so it survives the 100-item cap. @@ -241,10 +251,11 @@ export function suggestTables(ctx: CompletionContext, lcPrefix: string, schemaFi const score = matchScore(t.name, lcPrefix); if (score < 0) continue; const insert = formatSqlIdentifier(t.name, ctx.driver); + const kind = relationTypeLabel(schemaFilter ? 'table' : t.type || 'table'); items.push({ label: t.name, kind: 'class', - detail: schemaFilter ? 'table' : t.type || 'table', + detail: kind, insertText: insert, filterText: insert, sortText: rank(1, score, t.name), @@ -261,7 +272,7 @@ export function suggestSchemas(ctx: CompletionContext, lcPrefix: string): Comple items.push({ label: s.name, kind: 'module', - detail: 'schema', + detail: t('editor.sql.schema'), insertText: formatSqlIdentifier(s.name, ctx.driver), sortText: rank(3, score, s.name), }); @@ -288,7 +299,9 @@ export function suggestQueryTableRefs( items.push({ label: ref.table, kind: 'class', - detail: ref.schema ? `${ref.schema} · table` : 'table', + detail: ref.schema + ? t('editor.sql.schemaTable', { schema: ref.schema, type: t('editor.sql.table') }) + : t('editor.sql.table'), insertText: insert, filterText: insert, sortText: rank(0, score, ref.table), @@ -306,7 +319,7 @@ export function suggestQueryTableRefs( items.push({ label: ref.alias, kind: 'class', - detail: `alias → ${ref.table}`, + detail: t('editor.sql.aliasArrow', { table: ref.table }), insertText: insert, filterText: insert, sortText: rank(0, score, ref.alias), diff --git a/frontend/src/i18n/locales/bg.json b/frontend/src/i18n/locales/bg.json index 67d653b..bd8f030 100644 --- a/frontend/src/i18n/locales/bg.json +++ b/frontend/src/i18n/locales/bg.json @@ -196,7 +196,31 @@ "actionRunSelection": "Изпълни избраното", "actionRunAll": "Изпълни всичко", "actionSaveQuery": "Запази заявка", - "actionRenameSaved": "Преименувай запазена заявка" + "actionRenameSaved": "Преименувай запазена заявка", + "sql": { + "pk": "PK", + "fk": "FK", + "notNull": "не е null", + "table": "таблица", + "view": "изглед", + "schema": "схема", + "column": "колона", + "type": "тип", + "cte": "CTE", + "subquery": "подзаявка", + "cteColumn": "колона от CTE", + "subqueryColumn": "колона от подзаявка", + "foreignKey": "външен ключ", + "aliasArrow": "псевдоним → {{table}}", + "schemaTable": "{{schema}} · {{type}}", + "unknownTable": "Неизвестна таблица \"{{table}}\"", + "columnOf": "колона от {{target}}", + "schemaName": "схема {{name}}", + "nameKind": "{{name}} · {{kind}}", + "aliasFor": "псевдоним за {{table}}", + "columnsList": "колони: {{cols}}", + "moreColumns": "… още {{count}} колони" + } }, "results": { "noResults": "Няма резултати", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 5c404cb..1d6265b 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -196,7 +196,31 @@ "actionRunSelection": "Auswahl ausführen", "actionRunAll": "Alles ausführen", "actionSaveQuery": "Abfrage speichern", - "actionRenameSaved": "Gespeicherte Abfrage umbenennen" + "actionRenameSaved": "Gespeicherte Abfrage umbenennen", + "sql": { + "pk": "PK", + "fk": "FK", + "notNull": "nicht null", + "table": "Tabelle", + "view": "Sicht", + "schema": "Schema", + "column": "Spalte", + "type": "Typ", + "cte": "CTE", + "subquery": "Unterabfrage", + "cteColumn": "CTE-Spalte", + "subqueryColumn": "Unterabfrage-Spalte", + "foreignKey": "Fremdschlüssel", + "aliasArrow": "Alias → {{table}}", + "schemaTable": "{{schema}} · {{type}}", + "unknownTable": "Unbekannte Tabelle \"{{table}}\"", + "columnOf": "Spalte von {{target}}", + "schemaName": "Schema {{name}}", + "nameKind": "{{name}} · {{kind}}", + "aliasFor": "Alias für {{table}}", + "columnsList": "Spalten: {{cols}}", + "moreColumns": "… {{count}} weitere Spalten" + } }, "results": { "noResults": "Keine Ergebnisse", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index d0eb49c..f501afd 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -196,7 +196,31 @@ "actionRunSelection": "Run Selection", "actionRunAll": "Run All", "actionSaveQuery": "Save Query", - "actionRenameSaved": "Rename Saved Query" + "actionRenameSaved": "Rename Saved Query", + "sql": { + "pk": "PK", + "fk": "FK", + "notNull": "not null", + "table": "table", + "view": "view", + "schema": "schema", + "column": "column", + "type": "type", + "cte": "CTE", + "subquery": "subquery", + "cteColumn": "CTE column", + "subqueryColumn": "subquery column", + "foreignKey": "foreign key", + "aliasArrow": "alias → {{table}}", + "schemaTable": "{{schema}} · {{type}}", + "unknownTable": "Unknown table \"{{table}}\"", + "columnOf": "column of {{target}}", + "schemaName": "schema {{name}}", + "nameKind": "{{name}} · {{kind}}", + "aliasFor": "alias for {{table}}", + "columnsList": "columns: {{cols}}", + "moreColumns": "… {{count}} more columns" + } }, "results": { "noResults": "No results", diff --git a/frontend/src/test/setupI18n.ts b/frontend/src/test/setupI18n.ts new file mode 100644 index 0000000..5d8a5a5 --- /dev/null +++ b/frontend/src/test/setupI18n.ts @@ -0,0 +1,19 @@ +import i18n from 'i18next'; +import bg from '@/i18n/locales/bg.json'; +import de from '@/i18n/locales/de.json'; +import en from '@/i18n/locales/en.json'; + +// Node unit tests have no DOM; init a minimal i18n so SQL helpers that call t() resolve English copy. +if (!i18n.isInitialized) { + await i18n.init({ + resources: { + en: { translation: en }, + de: { translation: de }, + bg: { translation: bg }, + }, + lng: 'en', + fallbackLng: 'en', + interpolation: { escapeValue: false }, + returnEmptyString: false, + }); +} diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index a192e50..fd9b133 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ // Tests can live anywhere under src - co-located with the modules they // cover. Feature folders are about to hold their own *.test.ts files. include: ['src/**/*.test.ts'], + setupFiles: ['src/test/setupI18n.ts'], globals: false, }, }); From 918089983554caa1d1370c6f9ff04d17fe1ba15b Mon Sep 17 00:00:00 2001 From: Bare7a Date: Wed, 22 Jul 2026 11:38:14 +0300 Subject: [PATCH 4/9] Updated comments --- frontend/src/features/editor/lib/sqlCache.test.ts | 5 ++--- frontend/src/features/editor/lib/sqlCache.ts | 7 ++----- frontend/src/features/editor/lib/sqlCompletion.test.ts | 4 ---- frontend/src/features/editor/lib/sqlCompletion.ts | 3 +-- frontend/src/features/editor/lib/sqlContext.ts | 3 +-- frontend/src/features/editor/lib/sqlHover.test.ts | 2 -- frontend/src/features/editor/lib/sqlHover.ts | 4 +--- frontend/src/features/editor/lib/sqlQueryParse.ts | 7 ++----- frontend/src/features/editor/lib/sqlStatements.ts | 3 +-- frontend/src/features/editor/lib/sqlSuggestions.ts | 9 +++------ frontend/src/features/editor/lib/sqlTokens.ts | 5 ++--- frontend/src/test/setupI18n.ts | 2 +- 12 files changed, 16 insertions(+), 38 deletions(-) diff --git a/frontend/src/features/editor/lib/sqlCache.test.ts b/frontend/src/features/editor/lib/sqlCache.test.ts index 3aac729..533c9a6 100644 --- a/frontend/src/features/editor/lib/sqlCache.test.ts +++ b/frontend/src/features/editor/lib/sqlCache.test.ts @@ -11,7 +11,7 @@ describe('LruCache', () => { const cache = new LruCache(2); cache.set('a', 1); cache.set('b', 2); - cache.get('a'); // refresh: 'b' is now the oldest + cache.get('a'); // make 'a' most recent cache.set('c', 3); expect(cache.get('a')).toBe(1); expect(cache.get('b')).toBeUndefined(); @@ -28,7 +28,6 @@ describe('LruCache', () => { }); }); -// One keystroke fans out to several providers analyzing the same strings; these must be shared. describe('per-keystroke analysis is memoized', () => { const schemas: SchemaInfo[] = [{ name: 'public' }]; const tables: TableInfo[] = [{ schema: 'public', name: 'users', type: 'table' }]; @@ -53,7 +52,7 @@ describe('per-keystroke analysis is memoized', () => { it('parseQueryContext hits only while the schema arrays keep their identity', () => { const first = parseQueryContext('SELECT * FROM users', tables, schemas, 'postgres'); expect(parseQueryContext('SELECT * FROM users', tables, schemas, 'postgres')).toBe(first); - // A schema reload swaps the arrays; equal content is not enough for a hit. + // Equal content, new array identity → miss. const reloaded: TableInfo[] = [{ schema: 'public', name: 'users', type: 'table' }]; expect(parseQueryContext('SELECT * FROM users', reloaded, schemas, 'postgres')).not.toBe(first); }); diff --git a/frontend/src/features/editor/lib/sqlCache.ts b/frontend/src/features/editor/lib/sqlCache.ts index 17c0378..ddf5f7e 100644 --- a/frontend/src/features/editor/lib/sqlCache.ts +++ b/frontend/src/features/editor/lib/sqlCache.ts @@ -1,7 +1,4 @@ -// Tiny LRU used to memoize the pure per-keystroke analysis entry points (tokenize, cursor -// analysis, query/statement parsing). One keystroke fans out to several Monaco providers that -// re-analyze identical strings; the cache collapses those to a single computation. -// Pattern borrowed from potygen's inspect cache (packages/potygen/src/inspect/cache.ts). +// LRU for shared per-keystroke analysis (tokenize / cursor / parse). export class LruCache { private map = new Map(); @@ -27,7 +24,7 @@ export class LruCache { } } -// Cache keys join the dialect and source text; '\0' cannot appear in either side of the key. +// Dialect + SQL text; '\0' cannot appear in either side. export function cacheKey(driver: string | undefined, text: string): string { return `${driver ?? ''}\0${text}`; } diff --git a/frontend/src/features/editor/lib/sqlCompletion.test.ts b/frontend/src/features/editor/lib/sqlCompletion.test.ts index ab53995..cca5fb4 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.test.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.test.ts @@ -883,8 +883,6 @@ describe('column suggestions carry PK / NOT NULL hints', () => { }); }); -// Borrowed from potygen: a column name projected by several in-scope sources is ambiguous bare -// (the server rejects it), so it is offered qualified per source instead. describe('ambiguous columns across joined sources are offered qualified', () => { const cols: Record = { 'public.users': [ @@ -958,8 +956,6 @@ describe('ambiguous columns across joined sources are offered qualified', () => }); }); -// Borrowed from potygen: its column completions include the query's sources. The statement is -// parsed whole, so a mid-statement edit of the select list knows the FROM/JOIN refs after the caret. describe('SELECT list offers the statement’s table refs (mid-statement edit)', () => { it('offers alias, table and columns while editing the select list', () => { const text = 'SELECT FROM users u'; diff --git a/frontend/src/features/editor/lib/sqlCompletion.ts b/frontend/src/features/editor/lib/sqlCompletion.ts index 112b96f..7595906 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.ts @@ -231,8 +231,7 @@ function generalItems( } if (slot.expectsExpr && shape.inSelectList) { - // The statement is parsed whole, so FROM/JOIN sources after the caret are known here. - // Offer them alongside their columns (potygen does the same) to make qualifying easy. + // Include table refs so SELECT-list edits can qualify columns. items.push(...suggestQueryTableRefs(ctx, queryTables, slot.prefix)); items.push(...suggestColumnsInScope(ctx, queryTables, slot.prefix)); } diff --git a/frontend/src/features/editor/lib/sqlContext.ts b/frontend/src/features/editor/lib/sqlContext.ts index bc80699..ff8c81d 100644 --- a/frontend/src/features/editor/lib/sqlContext.ts +++ b/frontend/src/features/editor/lib/sqlContext.ts @@ -245,8 +245,7 @@ function lowerIdent(t: SqlToken): string { return tokenIdentText(t).toLowerCase(); } -// One completion request calls this three times with the same before-text (column prefetch, -// item building, replace range); the cache turns that into a single analysis. +// Same before-text is analyzed thrice per completion request. const cursorCache = new LruCache(16); export function analyzeSqlCursor(before: string, driver?: DriverType): SqlCursor { diff --git a/frontend/src/features/editor/lib/sqlHover.test.ts b/frontend/src/features/editor/lib/sqlHover.test.ts index af0bed7..9e9a32e 100644 --- a/frontend/src/features/editor/lib/sqlHover.test.ts +++ b/frontend/src/features/editor/lib/sqlHover.test.ts @@ -71,8 +71,6 @@ describe('analyzeHover', () => { expect(lines).toEqual(['**email** · text · not null', 'column of public.users']); }); - // Borrowed from potygen's quick info: a table hover carries its relation so the provider can - // append the column list. it('marks table and alias hovers for a column-list append', () => { expect(hoverAt('SELECT * FROM users', 'users')?.tableColumns).toEqual({ schema: 'public', table: 'users' }); const alias = hoverAt('SELECT * FROM users u WHERE u.id = 1', 'u ', 1); diff --git a/frontend/src/features/editor/lib/sqlHover.ts b/frontend/src/features/editor/lib/sqlHover.ts index f9cd473..8bb78be 100644 --- a/frontend/src/features/editor/lib/sqlHover.ts +++ b/frontend/src/features/editor/lib/sqlHover.ts @@ -11,8 +11,7 @@ export interface HoverQuery { end: number; lines?: string[]; columnLookup?: { bindings: TableBinding[]; name: string }; - // Table/alias hover: the caller appends this relation's column list (loaded lazily). - tableColumns?: TableBinding; + tableColumns?: TableBinding; // Lazy column list for table/alias hover. } export function columnHoverLines(col: ColumnInfo, table: TableBinding): string[] { @@ -20,7 +19,6 @@ export function columnHoverLines(col: ColumnInfo, table: TableBinding): string[] return [`**${col.name}** · ${columnDetail(col)}`, t('editor.sql.columnOf', { target: where })]; } -// Markdown column table for the table-hover card (potygen-style quick info). export function tableColumnsMarkdown(cols: ColumnInfo[], max = 30): string[] { if (cols.length === 0) return []; const shown = cols.slice(0, max); diff --git a/frontend/src/features/editor/lib/sqlQueryParse.ts b/frontend/src/features/editor/lib/sqlQueryParse.ts index 4c6d810..30c56ec 100644 --- a/frontend/src/features/editor/lib/sqlQueryParse.ts +++ b/frontend/src/features/editor/lib/sqlQueryParse.ts @@ -239,17 +239,14 @@ function collectCtes(tokens: SqlToken[], virtualColumns: Map): return ctes; } -// Completion, hover and diagnostics each parse the current statement per keystroke; diagnostics -// re-parse every statement in the buffer, so unchanged statements should be cache hits. The -// schema arrays participate in resolution, so a hit also requires their identity to match. +// Schema array identity is part of the hit check (resolution depends on it). interface ParseCacheEntry { tables: TableInfo[]; schemas: SchemaInfo[]; result: ParsedQuery; } -// Capacity must exceed the statement count of a typical buffer (diagnostics parse every -// statement per pass); beyond it the cache degrades gracefully to recomputing. +// Sized so a typical multi-statement diagnostics pass stays warm. const parseCache = new LruCache(256); export function parseQueryContext( diff --git a/frontend/src/features/editor/lib/sqlStatements.ts b/frontend/src/features/editor/lib/sqlStatements.ts index a359a9f..e72e64e 100644 --- a/frontend/src/features/editor/lib/sqlStatements.ts +++ b/frontend/src/features/editor/lib/sqlStatements.ts @@ -48,8 +48,7 @@ function firstCodeOffset(text: string, from: number, to: number, opts: SqlLexOpt // mysql-client `DELIMITER xx` line: switches the terminator so procedure bodies can contain `;`. const DELIMITER_LINE_RE = /DELIMITER[ \t]+(\S+)[ \t]*(?:\r?\n|$)/iy; -// Completion, hover, run glyphs and diagnostics all split the same (whole) buffer; entries are -// buffer-sized, so the cache stays small. +// Whole-buffer splits shared by glyphs / completion / diagnostics. const statementCache = new LruCache(8); export function parseSqlStatements(sql: string, driver?: DriverType): SqlStatement[] { diff --git a/frontend/src/features/editor/lib/sqlSuggestions.ts b/frontend/src/features/editor/lib/sqlSuggestions.ts index 69fc3a8..c6fda2d 100644 --- a/frontend/src/features/editor/lib/sqlSuggestions.ts +++ b/frontend/src/features/editor/lib/sqlSuggestions.ts @@ -152,7 +152,7 @@ export function columnDetail(c: ColumnInfo): string { return c.dataType; } -// Localize common relation kinds from the schema catalog; unknown values pass through. +// Known relation kinds; unknown catalog values pass through. export function relationTypeLabel(type?: string): string { const raw = (type || 'table').trim(); const key = raw.toLowerCase(); @@ -332,10 +332,7 @@ export function suggestQueryTableRefs( return items; } -// Columns of every relation the statement binds, one source per FROM/JOIN/UPDATE/INSERT ref. -// A name projected by several sources is ambiguous bare - the server would reject it - so those -// are offered qualified per source (`alias.col`). Potygen drops ambiguous columns from its -// completions; qualifying keeps them reachable. +// In-scope columns; names shared by multiple sources are offered as `alias.col`. export function suggestColumnsInScope( ctx: CompletionContext, queryTables: QueryTableRef[], @@ -351,7 +348,7 @@ export function suggestColumnsInScope( sources.push({ ref, cols: ctx.columnsByTable[columnCacheKey(ref.schema, ref.table)] || [] }); } - // How many sources project each name; self-joins count once per alias, like the server does. + // Count per source (self-joins: one per alias). const nameCount = new Map(); for (const s of sources) { for (const name of new Set(s.cols.map((c) => c.name.toLowerCase()))) { diff --git a/frontend/src/features/editor/lib/sqlTokens.ts b/frontend/src/features/editor/lib/sqlTokens.ts index 2e2e4b9..76f8c97 100644 --- a/frontend/src/features/editor/lib/sqlTokens.ts +++ b/frontend/src/features/editor/lib/sqlTokens.ts @@ -35,9 +35,8 @@ const WS_RE = /\s/; const TWO_CHAR_OPS = new Set(['<=', '>=', '<>', '!=', '::', '||', ':=']); const PUNCT = new Set(['.', ',', '(', ')', ';']); -// Completion, hover and query parsing all tokenize the same statement text within one keystroke. -// The result is shared, so callers must treat the returned tokens as immutable. Capacity must -// exceed the statement count of a typical buffer, or a diagnostics pass cycles the whole cache. +// Shared across providers per keystroke; treat returned tokens as immutable. +// Capacity covers a typical multi-statement diagnostics pass. const tokenCache = new LruCache(256); export function tokenizeSql(text: string, driver?: DriverType): SqlToken[] { diff --git a/frontend/src/test/setupI18n.ts b/frontend/src/test/setupI18n.ts index 5d8a5a5..a099c23 100644 --- a/frontend/src/test/setupI18n.ts +++ b/frontend/src/test/setupI18n.ts @@ -3,7 +3,7 @@ import bg from '@/i18n/locales/bg.json'; import de from '@/i18n/locales/de.json'; import en from '@/i18n/locales/en.json'; -// Node unit tests have no DOM; init a minimal i18n so SQL helpers that call t() resolve English copy. +// Node has no DOM; init i18n so SQL helpers that call t() resolve English copy. if (!i18n.isInitialized) { await i18n.init({ resources: { From 31d2dff234709510d67d25ec1e5c22e6f4603ebd Mon Sep 17 00:00:00 2001 From: Bare7a Date: Wed, 22 Jul 2026 11:46:03 +0300 Subject: [PATCH 5/9] Fix biome errors --- frontend/src/features/editor/lib/sqlCompletion.test.ts | 4 +++- frontend/src/features/editor/lib/sqlCompletion.ts | 9 ++------- frontend/src/features/editor/lib/sqlSuggestions.ts | 6 +++++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/features/editor/lib/sqlCompletion.test.ts b/frontend/src/features/editor/lib/sqlCompletion.test.ts index cca5fb4..20a5f4c 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.test.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.test.ts @@ -935,7 +935,9 @@ describe('ambiguous columns across joined sources are offered qualified', () => { schema: 'public', name: 'Users', type: 'table' }, { schema: 'public', name: 'orders', type: 'table' }, ]; - const shared: ColumnInfo[] = [{ name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false }]; + const shared: ColumnInfo[] = [ + { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false }, + ]; const ctx: CompletionContext = { schemas, tables: capTables, diff --git a/frontend/src/features/editor/lib/sqlCompletion.ts b/frontend/src/features/editor/lib/sqlCompletion.ts index 7595906..3e33b7e 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.ts @@ -196,10 +196,7 @@ function orderGroupItems( ): CompletionItem[] { // Columns/tables only at the start of a sort term (after BY/comma), not after a finished one. const items: CompletionItem[] = slot.expectsExpr - ? [ - ...suggestQueryTableRefs(ctx, queryTables, slot.prefix), - ...suggestColumnsInScope(ctx, queryTables, slot.prefix), - ] + ? [...suggestQueryTableRefs(ctx, queryTables, slot.prefix), ...suggestColumnsInScope(ctx, queryTables, slot.prefix)] : []; const keywords = slot.directionAllowed ? ['ASC', 'DESC', ...slot.trailingKeywords] : slot.trailingKeywords; for (const kw of keywords) { @@ -257,9 +254,7 @@ function completionItems(input: BuildCompletionInput, cursor: SqlCursor): Comple } case 'insert-columns': { const used = new Set(slot.used); - return suggestColumnsInScope(ctx, queryTables, slot.prefix).filter( - (item) => !used.has(item.label.toLowerCase()), - ); + return suggestColumnsInScope(ctx, queryTables, slot.prefix).filter((item) => !used.has(item.label.toLowerCase())); } case 'set-column': { const items = suggestColumnsInScope(ctx, queryTables, slot.prefix); diff --git a/frontend/src/features/editor/lib/sqlSuggestions.ts b/frontend/src/features/editor/lib/sqlSuggestions.ts index c6fda2d..2b4bef0 100644 --- a/frontend/src/features/editor/lib/sqlSuggestions.ts +++ b/frontend/src/features/editor/lib/sqlSuggestions.ts @@ -394,7 +394,11 @@ export function suggestColumnsInScope( const VALUE_LITERALS = ['NULL', 'TRUE', 'FALSE', 'DEFAULT']; -export function suggestValueItems(ctx: CompletionContext, queryTables: QueryTableRef[], lcPrefix: string): CompletionItem[] { +export function suggestValueItems( + ctx: CompletionContext, + queryTables: QueryTableRef[], + lcPrefix: string, +): CompletionItem[] { const items: CompletionItem[] = []; const seenCols = new Set(); From 4a65da10f7487fa9fa6318a3888e15b696b4d1be Mon Sep 17 00:00:00 2001 From: Bare7a Date: Wed, 22 Jul 2026 12:16:00 +0300 Subject: [PATCH 6/9] More optimizations --- .../src/features/editor/SqlConditionInput.tsx | 4 +- frontend/src/features/editor/lib/sqlCache.ts | 19 +++++-- .../src/features/editor/lib/sqlCompletion.ts | 8 ++- .../src/features/editor/lib/sqlContext.ts | 8 +-- frontend/src/features/editor/lib/sqlHover.ts | 7 +-- .../src/features/editor/lib/sqlQueryParse.ts | 10 ++-- .../src/features/editor/lib/sqlStatements.ts | 8 +-- .../src/features/editor/lib/sqlSuggestions.ts | 53 ++++++++++++------- frontend/src/features/editor/lib/sqlTokens.ts | 8 +-- 9 files changed, 75 insertions(+), 50 deletions(-) diff --git a/frontend/src/features/editor/SqlConditionInput.tsx b/frontend/src/features/editor/SqlConditionInput.tsx index 721662f..e44ce56 100644 --- a/frontend/src/features/editor/SqlConditionInput.tsx +++ b/frontend/src/features/editor/SqlConditionInput.tsx @@ -3,9 +3,9 @@ import type { editor, languages, Position } from 'monaco-editor'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import { MONACO_FONT_METRICS_OPTIONS } from '@/features/editor/lib/monacoFontMetrics'; import { getMonacoThemeName, setupMonacoBeforeMount } from '@/features/editor/lib/monacoTheme'; +import { sqlLabels } from '@/features/editor/lib/sqlLabels'; import { formatSqlIdentifier } from '@/features/editor/lib/sqlQuoting'; import { matchScore, rank } from '@/features/editor/lib/sqlSuggestions'; -import { t } from '@/i18n'; import { useAppTheme } from '@/shared/hooks/useAppTheme'; import { useUiZoom } from '@/shared/hooks/useUiZoom'; import { cx } from '@/shared/lib/cx'; @@ -185,7 +185,7 @@ export function SqlConditionInput({ suggestions.push({ label: col, kind: Kind.Field, - detail: types?.[i] || t('editor.sql.column'), + detail: types?.[i] || sqlLabels().column, insertText: formatSqlIdentifier(col, drv), sortText: rank(0, score, col), range, diff --git a/frontend/src/features/editor/lib/sqlCache.ts b/frontend/src/features/editor/lib/sqlCache.ts index ddf5f7e..6320528 100644 --- a/frontend/src/features/editor/lib/sqlCache.ts +++ b/frontend/src/features/editor/lib/sqlCache.ts @@ -24,7 +24,20 @@ export class LruCache { } } -// Dialect + SQL text; '\0' cannot appear in either side. -export function cacheKey(driver: string | undefined, text: string): string { - return `${driver ?? ''}\0${text}`; +// One LRU per dialect, keyed by the SQL text itself. Keying on the existing string +// avoids concatenating a fresh `driver + buffer` key (and re-hashing it) on every access. +export class DriverLruCache { + private byDriver = new Map>(); + + constructor(private readonly limit: number) {} + + of(driver: string | undefined): LruCache { + const key = driver ?? ''; + let cache = this.byDriver.get(key); + if (!cache) { + cache = new LruCache(this.limit); + this.byDriver.set(key, cache); + } + return cache; + } } diff --git a/frontend/src/features/editor/lib/sqlCompletion.ts b/frontend/src/features/editor/lib/sqlCompletion.ts index 3e33b7e..bd200e1 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.ts @@ -4,6 +4,7 @@ import { type SqlCursor, type StatementShape, } from '@/features/editor/lib/sqlContext'; +import { sqlLabels } from '@/features/editor/lib/sqlLabels'; import { type ParsedQuery, type QueryTableRef, @@ -27,7 +28,6 @@ import { suggestVirtualColumns, withLeadingSpace, } from '@/features/editor/lib/sqlSuggestions'; -import { t } from '@/i18n'; import type { DriverType, SchemaInfo, TableInfo } from '@/types'; export interface BindingsNeedingColumnsCtx { @@ -132,7 +132,7 @@ function fkJoinItems(ctx: CompletionContext, queryTables: QueryTableRef[]): Comp items.push({ label: expr, kind: 'field', - detail: t('editor.sql.foreignKey'), + detail: sqlLabels().foreignKey, insertText: expr, sortText: rank(0, 0, expr), }); @@ -142,9 +142,7 @@ function fkJoinItems(ctx: CompletionContext, queryTables: QueryTableRef[]): Comp } function virtualDetail(parsed: ParsedQuery, nameLc: string): string { - return parsed.ctes.some((c) => c.toLowerCase() === nameLc) - ? t('editor.sql.cteColumn') - : t('editor.sql.subqueryColumn'); + return parsed.ctes.some((c) => c.toLowerCase() === nameLc) ? sqlLabels().cteColumn : sqlLabels().subqueryColumn; } // Derived-table aliases are always in scope; CTEs only once referenced in FROM/JOIN. diff --git a/frontend/src/features/editor/lib/sqlContext.ts b/frontend/src/features/editor/lib/sqlContext.ts index ff8c81d..a4f362d 100644 --- a/frontend/src/features/editor/lib/sqlContext.ts +++ b/frontend/src/features/editor/lib/sqlContext.ts @@ -1,4 +1,4 @@ -import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; +import { DriverLruCache } from '@/features/editor/lib/sqlCache'; import { isIdentLike, isKeyword, type SqlToken, tokenIdentText, tokenizeSql } from '@/features/editor/lib/sqlTokens'; import type { DriverType } from '@/types'; @@ -246,11 +246,11 @@ function lowerIdent(t: SqlToken): string { } // Same before-text is analyzed thrice per completion request. -const cursorCache = new LruCache(16); +const cursorCache = new DriverLruCache(16); export function analyzeSqlCursor(before: string, driver?: DriverType): SqlCursor { - const key = cacheKey(driver, before); - return cursorCache.get(key) ?? cursorCache.set(key, analyzeCursor(before, driver)); + const cache = cursorCache.of(driver); + return cache.get(before) ?? cache.set(before, analyzeCursor(before, driver)); } function analyzeCursor(before: string, driver?: DriverType): SqlCursor { diff --git a/frontend/src/features/editor/lib/sqlHover.ts b/frontend/src/features/editor/lib/sqlHover.ts index 8bb78be..fad5222 100644 --- a/frontend/src/features/editor/lib/sqlHover.ts +++ b/frontend/src/features/editor/lib/sqlHover.ts @@ -1,3 +1,4 @@ +import { sqlLabels } from '@/features/editor/lib/sqlLabels'; import { type ParsedQuery, resolveDotCompletion, type TableBinding } from '@/features/editor/lib/sqlQueryParse'; import { QUOTE_FORCING_KEYWORDS, unquoteIdent } from '@/features/editor/lib/sqlQuoting'; import { columnDetail, relationTypeLabel } from '@/features/editor/lib/sqlSuggestions'; @@ -23,7 +24,7 @@ export function tableColumnsMarkdown(cols: ColumnInfo[], max = 30): string[] { if (cols.length === 0) return []; const shown = cols.slice(0, max); const rows = shown.map((c) => `| ${c.name} | ${columnDetail(c)} |`); - const table = [`| ${t('editor.sql.column')} | ${t('editor.sql.type')} |`, '| --- | --- |', ...rows].join('\n'); + const table = [`| ${sqlLabels().column} | ${sqlLabels().type} |`, '| --- | --- |', ...rows].join('\n'); return cols.length > shown.length ? [table, t('editor.sql.moreColumns', { count: cols.length - shown.length })] : [table]; @@ -37,7 +38,7 @@ function tableLines(info: TableInfo): string[] { } function virtualKindLabel(isCte: boolean): string { - return isCte ? t('editor.sql.cte') : t('editor.sql.subquery'); + return isCte ? sqlLabels().cte : sqlLabels().subquery; } function tokenAt(tokens: SqlToken[], offset: number): SqlToken | undefined { @@ -134,7 +135,7 @@ export function analyzeHover( if (schema) { return { ...span, - lines: [t('editor.sql.nameKind', { name: `**${schema.name}**`, kind: t('editor.sql.schema') })], + lines: [t('editor.sql.nameKind', { name: `**${schema.name}**`, kind: sqlLabels().schema })], }; } diff --git a/frontend/src/features/editor/lib/sqlQueryParse.ts b/frontend/src/features/editor/lib/sqlQueryParse.ts index 30c56ec..8b58f7c 100644 --- a/frontend/src/features/editor/lib/sqlQueryParse.ts +++ b/frontend/src/features/editor/lib/sqlQueryParse.ts @@ -1,4 +1,4 @@ -import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; +import { DriverLruCache } from '@/features/editor/lib/sqlCache'; import { ALIAS_STOP_WORDS, unquoteIdent } from '@/features/editor/lib/sqlQuoting'; import { isIdentLike, @@ -247,7 +247,7 @@ interface ParseCacheEntry { } // Sized so a typical multi-statement diagnostics pass stays warm. -const parseCache = new LruCache(256); +const parseCache = new DriverLruCache(256); export function parseQueryContext( sql: string, @@ -255,11 +255,11 @@ export function parseQueryContext( schemas: SchemaInfo[], driver: DriverType, ): ParsedQuery { - const key = cacheKey(driver, sql); - const hit = parseCache.get(key); + const cache = parseCache.of(driver); + const hit = cache.get(sql); if (hit && hit.tables === tables && hit.schemas === schemas) return hit.result; const result = parseQuery(sql, tables, schemas, driver); - parseCache.set(key, { tables, schemas, result }); + cache.set(sql, { tables, schemas, result }); return result; } diff --git a/frontend/src/features/editor/lib/sqlStatements.ts b/frontend/src/features/editor/lib/sqlStatements.ts index e72e64e..5c7af3a 100644 --- a/frontend/src/features/editor/lib/sqlStatements.ts +++ b/frontend/src/features/editor/lib/sqlStatements.ts @@ -1,4 +1,4 @@ -import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; +import { DriverLruCache } from '@/features/editor/lib/sqlCache'; import { findBlockCommentEnd, findLineCommentEnd, @@ -49,11 +49,11 @@ function firstCodeOffset(text: string, from: number, to: number, opts: SqlLexOpt const DELIMITER_LINE_RE = /DELIMITER[ \t]+(\S+)[ \t]*(?:\r?\n|$)/iy; // Whole-buffer splits shared by glyphs / completion / diagnostics. -const statementCache = new LruCache(8); +const statementCache = new DriverLruCache(8); export function parseSqlStatements(sql: string, driver?: DriverType): SqlStatement[] { - const key = cacheKey(driver, sql); - return statementCache.get(key) ?? statementCache.set(key, splitStatements(sql, driver)); + const cache = statementCache.of(driver); + return cache.get(sql) ?? cache.set(sql, splitStatements(sql, driver)); } function splitStatements(sql: string, driver?: DriverType): SqlStatement[] { diff --git a/frontend/src/features/editor/lib/sqlSuggestions.ts b/frontend/src/features/editor/lib/sqlSuggestions.ts index 2b4bef0..1caf01b 100644 --- a/frontend/src/features/editor/lib/sqlSuggestions.ts +++ b/frontend/src/features/editor/lib/sqlSuggestions.ts @@ -1,4 +1,5 @@ import type { StatementShape } from '@/features/editor/lib/sqlContext'; +import { sqlLabels } from '@/features/editor/lib/sqlLabels'; import type { QueryTableRef, TableBinding } from '@/features/editor/lib/sqlQueryParse'; import { columnCacheKey, formatSqlIdentifier } from '@/features/editor/lib/sqlQuoting'; import { t } from '@/i18n'; @@ -144,20 +145,22 @@ export function rank(tier: 0 | 1 | 2 | 3 | 4 | 5, score: number, label: string): // Column hint shown in the suggestion's detail line: type plus PK / FK / NOT NULL markers. export function columnDetail(c: ColumnInfo): string { - const tags: string[] = []; - if (c.isPrimary) tags.push(t('editor.sql.pk')); - if (c.isForeign) tags.push(t('editor.sql.fk')); - if (tags.length > 0) return `${c.dataType} · ${tags.join(' · ')}`; - if (!c.isNullable) return `${c.dataType} · ${t('editor.sql.notNull')}`; + const labels = sqlLabels(); + if (c.isPrimary || c.isForeign) { + const tags = c.isPrimary && c.isForeign ? `${labels.pk} · ${labels.fk}` : c.isPrimary ? labels.pk : labels.fk; + return `${c.dataType} · ${tags}`; + } + if (!c.isNullable) return `${c.dataType} · ${labels.notNull}`; return c.dataType; } // Known relation kinds; unknown catalog values pass through. export function relationTypeLabel(type?: string): string { - const raw = (type || 'table').trim(); + if (!type || type === 'table') return sqlLabels().table; + const raw = type.trim(); const key = raw.toLowerCase(); - if (key === 'table' || key === 'base table') return t('editor.sql.table'); - if (key === 'view') return t('editor.sql.view'); + if (key === 'table' || key === 'base table') return sqlLabels().table; + if (key === 'view') return sqlLabels().view; return raw; } @@ -230,7 +233,7 @@ export function suggestCteItems(ctes: string[], lcPrefix: string, driver: Driver items.push({ label: name, kind: 'class', - detail: t('editor.sql.cte'), + detail: sqlLabels().cte, insertText: formatSqlIdentifier(name, driver), filterText: formatSqlIdentifier(name, driver), // Tier 0 (query-local): ranks above the table list so it survives the 100-item cap. @@ -272,7 +275,7 @@ export function suggestSchemas(ctx: CompletionContext, lcPrefix: string): Comple items.push({ label: s.name, kind: 'module', - detail: t('editor.sql.schema'), + detail: sqlLabels().schema, insertText: formatSqlIdentifier(s.name, ctx.driver), sortText: rank(3, score, s.name), }); @@ -300,8 +303,8 @@ export function suggestQueryTableRefs( label: ref.table, kind: 'class', detail: ref.schema - ? t('editor.sql.schemaTable', { schema: ref.schema, type: t('editor.sql.table') }) - : t('editor.sql.table'), + ? t('editor.sql.schemaTable', { schema: ref.schema, type: sqlLabels().table }) + : sqlLabels().table, insertText: insert, filterText: insert, sortText: rank(0, score, ref.table), @@ -332,6 +335,21 @@ export function suggestQueryTableRefs( return items; } +function countColumnNames(sources: { cols: ColumnInfo[] }[]): Map { + const counts = new Map(); + const perSource = new Set(); + for (const s of sources) { + perSource.clear(); + for (const c of s.cols) { + const key = c.name.toLowerCase(); + if (perSource.has(key)) continue; + perSource.add(key); + counts.set(key, (counts.get(key) ?? 0) + 1); + } + } + return counts; +} + // In-scope columns; names shared by multiple sources are offered as `alias.col`. export function suggestColumnsInScope( ctx: CompletionContext, @@ -348,13 +366,8 @@ export function suggestColumnsInScope( sources.push({ ref, cols: ctx.columnsByTable[columnCacheKey(ref.schema, ref.table)] || [] }); } - // Count per source (self-joins: one per alias). - const nameCount = new Map(); - for (const s of sources) { - for (const name of new Set(s.cols.map((c) => c.name.toLowerCase()))) { - nameCount.set(name, (nameCount.get(name) ?? 0) + 1); - } - } + // Count per source (self-joins: one per alias). One source cannot be ambiguous with itself. + const nameCount = sources.length > 1 ? countColumnNames(sources) : null; const items: CompletionItem[] = []; for (const s of sources) { @@ -365,7 +378,7 @@ export function suggestColumnsInScope( emitted.add(key); const score = matchScore(c.name, lcPrefix); if (score < 0) continue; - if ((nameCount.get(key) ?? 0) > 1) { + if (nameCount !== null && (nameCount.get(key) ?? 0) > 1) { const qualifier = s.ref.alias ?? s.ref.table; const label = `${qualifier}.${c.name}`; seen?.add(key); diff --git a/frontend/src/features/editor/lib/sqlTokens.ts b/frontend/src/features/editor/lib/sqlTokens.ts index 76f8c97..2b25f72 100644 --- a/frontend/src/features/editor/lib/sqlTokens.ts +++ b/frontend/src/features/editor/lib/sqlTokens.ts @@ -1,4 +1,4 @@ -import { cacheKey, LruCache } from '@/features/editor/lib/sqlCache'; +import { DriverLruCache } from '@/features/editor/lib/sqlCache'; import { findBlockCommentEnd, findLineCommentEnd, @@ -37,11 +37,11 @@ const PUNCT = new Set(['.', ',', '(', ')', ';']); // Shared across providers per keystroke; treat returned tokens as immutable. // Capacity covers a typical multi-statement diagnostics pass. -const tokenCache = new LruCache(256); +const tokenCache = new DriverLruCache(256); export function tokenizeSql(text: string, driver?: DriverType): SqlToken[] { - const key = cacheKey(driver, text); - return tokenCache.get(key) ?? tokenCache.set(key, tokenize(text, driver)); + const cache = tokenCache.of(driver); + return cache.get(text) ?? cache.set(text, tokenize(text, driver)); } // One pass; $tag$ delimiters are ops so dollar-quoted bodies stay tokenized (completion works inside them). From aa5bb04ecb9685a79b65d6f5cf061065c310d3ec Mon Sep 17 00:00:00 2001 From: Bare7a Date: Wed, 22 Jul 2026 12:19:57 +0300 Subject: [PATCH 7/9] Fix --- frontend/bench/sqlPerf.bench.test.ts | 361 ++++++++++++++++++ .../src/features/editor/lib/sqlLabels.test.ts | 20 + frontend/src/features/editor/lib/sqlLabels.ts | 44 +++ frontend/vitest.bench.config.ts | 25 ++ 4 files changed, 450 insertions(+) create mode 100644 frontend/bench/sqlPerf.bench.test.ts create mode 100644 frontend/src/features/editor/lib/sqlLabels.test.ts create mode 100644 frontend/src/features/editor/lib/sqlLabels.ts create mode 100644 frontend/vitest.bench.config.ts diff --git a/frontend/bench/sqlPerf.bench.test.ts b/frontend/bench/sqlPerf.bench.test.ts new file mode 100644 index 0000000..e1d1226 --- /dev/null +++ b/frontend/bench/sqlPerf.bench.test.ts @@ -0,0 +1,361 @@ +/** + * SQL editor pipeline benchmark. + * + * Replays exactly what the editor runs at runtime, per keystroke: + * - run-glyphs: parseSqlStatements(fullText) (useRunGlyphs) + * - completion: parseSqlStatements -> currentStatementRange -> + * parseQueryContext(stmt) -> bindingsNeedingColumns -> + * buildCompletionItems -> completionReplaceRange (createSqlCompletionProvider) + * - diagnostics: collectSchemaDiagnostics(fullText) (useSqlDiagnostics, debounce fire) + * - hover: parseSqlStatements -> currentStatementRange -> + * parseQueryContext(stmt) -> analyzeHover (createSqlHoverProvider) + * + * Identical file is run on both branches; results go to BENCH_OUT as JSON. + */ +import fs from 'node:fs'; +import i18n from 'i18next'; +import { it } from 'vitest'; +import { + bindingsNeedingColumns, + buildCompletionItems, + completionReplaceRange, +} from '@/features/editor/lib/sqlCompletion'; +import { collectSchemaDiagnostics } from '@/features/editor/lib/sqlDiagnostics'; +import { analyzeHover } from '@/features/editor/lib/sqlHover'; +import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse'; +import { columnCacheKey } from '@/features/editor/lib/sqlQuoting'; +import { currentStatementRange, parseSqlStatements } from '@/features/editor/lib/sqlStatements'; +import type { CompletionContext } from '@/features/editor/lib/sqlSuggestions'; +import bg from '@/i18n/locales/bg.json'; +import de from '@/i18n/locales/de.json'; +import en from '@/i18n/locales/en.json'; +import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types'; + +// Same init as src/test/setupI18n.ts (the app inits the same singleton at boot). +if (!i18n.isInitialized) { + await i18n.init({ + resources: { en: { translation: en }, de: { translation: de }, bg: { translation: bg } }, + lng: 'en', + fallbackLng: 'en', + interpolation: { escapeValue: false }, + returnEmptyString: false, + }); +} + +const DRIVER: DriverType = 'postgres' as DriverType; + +// ---------- fixture: 250 tables x 40 columns, shared id/created_at/updated_at ---------- + +const TYPES = ['integer', 'text', 'numeric(12,2)', 'timestamptz', 'boolean', 'uuid', 'jsonb', 'bigint']; + +function makeColumns(table: string): ColumnInfo[] { + const cols: ColumnInfo[] = [ + { name: 'id', dataType: 'integer', isNullable: false, isPrimary: true, isForeign: false }, + { name: 'created_at', dataType: 'timestamptz', isNullable: false, isPrimary: false, isForeign: false }, + { name: 'updated_at', dataType: 'timestamptz', isNullable: true, isPrimary: false, isForeign: false }, + ]; + for (let i = 0; i < 37; i++) { + cols.push({ + name: `${table}_c${i}`, + dataType: TYPES[i % TYPES.length], + isNullable: i % 3 !== 0, + isPrimary: false, + isForeign: i === 5, + foreignTable: i === 5 ? 'users' : undefined, + foreignColumn: i === 5 ? 'id' : undefined, + }); + } + return cols; +} + +const NAMED = ['users', 'orders', 'order_items', 'products', 'customers']; +const tables: TableInfo[] = []; +for (const name of NAMED) tables.push({ schema: 'public', name, type: 'table' }); +for (let i = NAMED.length; i < 250; i++) + tables.push({ schema: 'public', name: `t_${String(i).padStart(3, '0')}`, type: i % 10 === 0 ? 'view' : 'table' }); + +const schemas: SchemaInfo[] = [{ name: 'public' }, { name: 'analytics' }, { name: 'audit' }]; +const tablesBySchema: Record = { public: tables, analytics: [], audit: [] }; + +const columnsByTable: Record = {}; +for (const tbl of tables) columnsByTable[columnCacheKey(tbl.schema, tbl.name)] = makeColumns(tbl.name); +// Domain columns used by the typed queries. +columnsByTable['public.users'].push( + { name: 'email', dataType: 'text', isNullable: false, isPrimary: false, isForeign: false }, + { name: 'full_name', dataType: 'text', isNullable: true, isPrimary: false, isForeign: false }, +); +columnsByTable['public.orders'].push( + { name: 'total', dataType: 'numeric(12,2)', isNullable: false, isPrimary: false, isForeign: false }, + { + name: 'user_id', + dataType: 'integer', + isNullable: false, + isPrimary: false, + isForeign: true, + foreignTable: 'users', + foreignColumn: 'id', + }, + { + name: 'customer_id', + dataType: 'integer', + isNullable: false, + isPrimary: false, + isForeign: true, + foreignTable: 'customers', + foreignColumn: 'id', + }, +); +columnsByTable['public.order_items'].push( + { + name: 'order_id', + dataType: 'integer', + isNullable: false, + isPrimary: false, + isForeign: true, + foreignTable: 'orders', + foreignColumn: 'id', + }, + { + name: 'product_id', + dataType: 'integer', + isNullable: false, + isPrimary: false, + isForeign: true, + foreignTable: 'products', + foreignColumn: 'id', + }, +); + +const ctx: CompletionContext = { schemas, tables, columns: [], tablesBySchema, columnsByTable, driver: DRIVER }; + +// ---------- measurement plumbing ---------- + +interface Stats { + n: number; + meanMs: number; + p50Ms: number; + p95Ms: number; + maxMs: number; + totalMs: number; +} + +function stats(samples: number[]): Stats { + const s = [...samples].sort((a, b) => a - b); + const total = s.reduce((a, b) => a + b, 0); + const q = (p: number) => s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0; + return { + n: s.length, + meanMs: total / (s.length || 1), + p50Ms: q(0.5), + p95Ms: q(0.95), + maxMs: s[s.length - 1] ?? 0, + totalMs: total, + }; +} + +let peakRss = 0; +function sampleRss(): void { + const rss = process.memoryUsage().rss; + if (rss > peakRss) peakRss = rss; +} + +function gcNow(): void { + (globalThis as { gc?: () => void }).gc?.(); +} + +// ---------- editor pipeline replicas ---------- + +function glyphPass(text: string): number { + return parseSqlStatements(text, DRIVER).length; +} + +function completionRequest(text: string, offset: number): number { + const { start, end } = currentStatementRange(parseSqlStatements(text, DRIVER), offset, text.length); + const textBefore = text.slice(start, offset); + const parsed = parseQueryContext(text.slice(start, end), tables, schemas, DRIVER); + bindingsNeedingColumns(textBefore, parsed, { tables, schemas, driver: DRIVER }); + const items = buildCompletionItems({ ctx, text, position: offset, parsed, statementStart: start }); + completionReplaceRange( + { lineNumber: 1, column: offset + 1 }, + textBefore, + { startColumn: offset + 1, endColumn: offset + 1 }, + DRIVER, + ); + return items.length; +} + +function diagnosticsFire(text: string): number { + return collectSchemaDiagnostics(text, tables, schemas, DRIVER).length; +} + +function hoverRequest(text: string, offset: number): boolean { + const { start, end } = currentStatementRange(parseSqlStatements(text, DRIVER), offset, text.length); + const stmt = text.slice(start, end); + const parsed = parseQueryContext(stmt, tables, schemas, DRIVER); + return analyzeHover(stmt, offset - start, parsed, tables, schemas, DRIVER) !== null; +} + +// ---------- workloads ---------- + +interface WorkloadResult { + buckets: Record; + wallMs: number; + cpuUserMs: number; + cpuSysMs: number; + checksum: number; +} + +function runWorkload(fn: (push: (bucket: string, ms: number) => void) => number): WorkloadResult { + const buckets: Record = {}; + const push = (bucket: string, ms: number) => { + buckets[bucket] ??= []; + buckets[bucket].push(ms); + }; + gcNow(); + const cpu0 = process.cpuUsage(); + const t0 = performance.now(); + const checksum = fn(push); + const wallMs = performance.now() - t0; + const cpu = process.cpuUsage(cpu0); + const out: Record = {}; + for (const [k, v] of Object.entries(buckets)) out[k] = stats(v); + return { buckets: out, wallMs, cpuUserMs: cpu.user / 1000, cpuSysMs: cpu.system / 1000, checksum }; +} + +function typedQuery(salt: string): string { + return ( + `SELECT u.id, u.email, o.total FROM users u JOIN orders o ON o.user_id = u.id ` + + `WHERE u.email LIKE 'a${salt}%' AND o.total > 100 ORDER BY o.created_at DESC` + ); +} + +// Simulates typing `typed` at the end of `prefix`, running the per-keystroke pipeline. +function typingSession(prefix: string, typed: string, push: (b: string, ms: number) => void, diagEvery = 12): number { + let checksum = 0; + for (let k = 1; k <= typed.length; k++) { + const text = prefix + typed.slice(0, k); + const offset = text.length; + + let t0 = performance.now(); + checksum += glyphPass(text); + push('glyphs', performance.now() - t0); + + t0 = performance.now(); + checksum += completionRequest(text, offset); + push('completion', performance.now() - t0); + + if (k % diagEvery === 0 || k === typed.length) { + t0 = performance.now(); + checksum += diagnosticsFire(text); + push('diagnostics', performance.now() - t0); + } + sampleRss(); + } + return checksum; +} + +const W1_SESSIONS = 8; +function w1(push: (b: string, ms: number) => void): number { + let checksum = 0; + for (let s = 0; s < W1_SESSIONS; s++) checksum += typingSession('', typedQuery(`s${s}`), push); + return checksum; +} + +const W2_SESSIONS = 6; +const W2_STATEMENTS = 150; +function bigFilePrefix(salt: string): string { + const lines: string[] = []; + for (let i = 0; i < W2_STATEMENTS; i++) { + const t = `t_${String((i % 245) + 5).padStart(3, '0')}`; + lines.push(`SELECT id, created_at, ${t}_c${i % 37} FROM ${t} WHERE ${t}_c3 = ${i} /*${salt}*/;`); + } + return `${lines.join('\n')}\n`; +} +function w2(push: (b: string, ms: number) => void): number { + let checksum = 0; + for (let s = 0; s < W2_SESSIONS; s++) checksum += typingSession(bigFilePrefix(`s${s}`), typedQuery(`s${s}`), push); + return checksum; +} + +const W3_SESSIONS = 8; +function w3(push: (b: string, ms: number) => void): number { + let checksum = 0; + for (let s = 0; s < W3_SESSIONS; s++) { + const fixed = + `/*s${s}*/ SELECT * FROM users u JOIN orders o ON o.user_id = u.id ` + + `JOIN order_items oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id ` + + `JOIN customers c ON c.id = o.customer_id WHERE `; + const typedTail = 'created_at > now() AND u.email = c.customers_c1'; + checksum += typingSession('', fixed, push, 1_000_000); // no diag churn here + for (let k = 1; k <= typedTail.length; k++) { + const text = fixed + typedTail.slice(0, k); + const t0 = performance.now(); + checksum += completionRequest(text, text.length); + push('completion-wide', performance.now() - t0); + sampleRss(); + } + } + return checksum; +} + +const W4_SESSIONS = 6; +const W4_SWEEPS = 40; +function w4(push: (b: string, ms: number) => void): number { + let checksum = 0; + for (let s = 0; s < W4_SESSIONS; s++) { + const text = typedQuery(`s${s}`); + for (let sweep = 0; sweep < W4_SWEEPS; sweep++) { + for (let offset = 0; offset < text.length; offset += 3) { + const t0 = performance.now(); + checksum += hoverRequest(text, offset) ? 1 : 0; + push('hover', performance.now() - t0); + } + sampleRss(); + } + } + return checksum; +} + +// ---------- main ---------- + +it('sql editor pipeline benchmark', () => { + // JIT warmup on salted inputs that are never measured. + { + const sink: string[] = []; + typingSession('', typedQuery('warm'), () => sink.push(''), 10); + typingSession(bigFilePrefix('warm'), typedQuery('warm'), () => sink.push(''), 10); + } + gcNow(); + const baselineHeap = process.memoryUsage().heapUsed; + peakRss = 0; + sampleRss(); + + const results: Record = {}; + results.typing_small = runWorkload(w1); + results.typing_bigfile = runWorkload(w2); + results.completion_wide = runWorkload(w3); + results.hover_sweep = runWorkload(w4); + + const finalRssBeforeGc = process.memoryUsage().rss; + gcNow(); + const retainedHeap = process.memoryUsage().heapUsed - baselineHeap; + + const out = { + node: process.version, + side: process.env.BENCH_SIDE ?? 'unknown', + run: Number(process.env.BENCH_RUN ?? 0), + workloads: results, + memory: { + baselineHeapMB: baselineHeap / 1048576, + retainedHeapMB: retainedHeap / 1048576, + peakRssMB: peakRss / 1048576, + finalRssMB: finalRssBeforeGc / 1048576, + // darwin reports maxRSS in bytes, linux in KiB. + maxRssMB: process.resourceUsage().maxRSS / (process.platform === 'darwin' ? 1048576 : 1024), + }, + }; + + const dest = process.env.BENCH_OUT; + if (dest) fs.writeFileSync(dest, JSON.stringify(out, null, 2)); + console.log(JSON.stringify(out)); +}, 900_000); diff --git a/frontend/src/features/editor/lib/sqlLabels.test.ts b/frontend/src/features/editor/lib/sqlLabels.test.ts new file mode 100644 index 0000000..71cf02d --- /dev/null +++ b/frontend/src/features/editor/lib/sqlLabels.test.ts @@ -0,0 +1,20 @@ +import i18n from 'i18next'; +import { describe, expect, it } from 'vitest'; +import { sqlLabels } from '@/features/editor/lib/sqlLabels'; + +describe('sqlLabels', () => { + it('returns the same memoized object until the language changes', async () => { + await i18n.changeLanguage('en'); + const first = sqlLabels(); + expect(first.notNull).toBe('not null'); + expect(sqlLabels()).toBe(first); + + await i18n.changeLanguage('de'); + const german = sqlLabels(); + expect(german).not.toBe(first); + expect(german.table).toBe('Tabelle'); + + await i18n.changeLanguage('en'); + expect(sqlLabels().table).toBe('table'); + }); +}); diff --git a/frontend/src/features/editor/lib/sqlLabels.ts b/frontend/src/features/editor/lib/sqlLabels.ts new file mode 100644 index 0000000..59e04fe --- /dev/null +++ b/frontend/src/features/editor/lib/sqlLabels.ts @@ -0,0 +1,44 @@ +import { subscribeLanguageChanged, t } from '@/i18n'; + +// Static suggestion/hover copy, resolved once per language instead of per item +// (completion emits hundreds of items per keystroke). +export interface SqlLabels { + pk: string; + fk: string; + notNull: string; + table: string; + view: string; + schema: string; + cte: string; + subquery: string; + column: string; + type: string; + foreignKey: string; + cteColumn: string; + subqueryColumn: string; +} + +let cached: SqlLabels | null = null; + +subscribeLanguageChanged(() => { + cached = null; +}); + +export function sqlLabels(): SqlLabels { + cached ??= { + pk: t('editor.sql.pk'), + fk: t('editor.sql.fk'), + notNull: t('editor.sql.notNull'), + table: t('editor.sql.table'), + view: t('editor.sql.view'), + schema: t('editor.sql.schema'), + cte: t('editor.sql.cte'), + subquery: t('editor.sql.subquery'), + column: t('editor.sql.column'), + type: t('editor.sql.type'), + foreignKey: t('editor.sql.foreignKey'), + cteColumn: t('editor.sql.cteColumn'), + subqueryColumn: t('editor.sql.subqueryColumn'), + }; + return cached; +} diff --git a/frontend/vitest.bench.config.ts b/frontend/vitest.bench.config.ts new file mode 100644 index 0000000..0ee5f7e --- /dev/null +++ b/frontend/vitest.bench.config.ts @@ -0,0 +1,25 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + '@bindings': path.resolve(__dirname, 'bindings'), + '@wailsio/runtime': path.resolve(__dirname, 'src/test/wailsioRuntimeStub.ts'), + }, + }, + test: { + environment: 'node', + include: ['bench/**/*.bench.test.ts'], + pool: 'forks', + maxWorkers: 1, + execArgv: ['--expose-gc'], + testTimeout: 900_000, + hookTimeout: 900_000, + globals: false, + }, +}); From 460676a7dd8670ab0df53238471876e5d1b2af3b Mon Sep 17 00:00:00 2001 From: Bare7a Date: Wed, 22 Jul 2026 13:51:48 +0300 Subject: [PATCH 8/9] Updated all dependencies --- build/config.yml | 2 +- build/darwin/Info.dev.plist | 4 +- build/darwin/Info.plist | 4 +- build/ios/Info.dev.plist | 4 +- build/ios/Info.plist | 4 +- build/linux/nfpm/nfpm.yaml | 2 +- build/windows/info.json | 4 +- build/windows/nsis/wails_tools.nsh | 2 +- build/windows/wails.exe.manifest | 2 +- frontend/package-lock.json | 703 +++++++++++++----- frontend/package.json | 30 +- .../features/editor/lib/monacoFontMetrics.ts | 2 +- .../src/features/editor/lib/monacoSetup.ts | 32 +- .../src/features/editor/lib/monacoTheme.ts | 2 +- frontend/src/shared/lib/appInfo.ts | 2 +- frontend/src/vite-env.d.ts | 11 - go.mod | 16 +- go.sum | 44 +- internal/app/version.go | 2 +- 19 files changed, 599 insertions(+), 273 deletions(-) diff --git a/build/config.yml b/build/config.yml index a3675d8..d4862ea 100644 --- a/build/config.yml +++ b/build/config.yml @@ -11,7 +11,7 @@ info: description: "A fast, native SQL client built with Go, Wails and React." # The application description copyright: "(c) 2026, Bare7a" # Copyright text comments: "A fast, native SQL client built with Go, Wails and React." # Comments - version: "1.4.2" # The application version + version: "1.4.3" # The application version # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) # # Should match the name of your .icon file without the extension # # If not set and Assets.car exists, defaults to "appicon" diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index df5da25..23fd15f 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -78,9 +78,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 1.4.2 + 1.4.3 LSMinimumSystemVersion 12.0.0 NSAppTransportSecurity diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index 0656e0a..8cd5b12 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -78,9 +78,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 1.4.2 + 1.4.3 LSMinimumSystemVersion 12.0.0 NSHighResolutionCapable diff --git a/build/ios/Info.dev.plist b/build/ios/Info.dev.plist index 72fce0a..60c5af1 100644 --- a/build/ios/Info.dev.plist +++ b/build/ios/Info.dev.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.2-dev + 1.4.3-dev CFBundleVersion - 1.4.2 + 1.4.3 LSRequiresIPhoneOS MinimumOSVersion diff --git a/build/ios/Info.plist b/build/ios/Info.plist index a0e09f5..3f825ef 100644 --- a/build/ios/Info.plist +++ b/build/ios/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 1.4.2 + 1.4.3 LSRequiresIPhoneOS MinimumOSVersion diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index f4e54ac..71b8bec 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -6,7 +6,7 @@ name: "XenSQL" arch: ${GOARCH} platform: "linux" -version: "1.4.2" +version: "1.4.3" section: "default" priority: "extra" maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> diff --git a/build/windows/info.json b/build/windows/info.json index 29e5922..5f5dc31 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,10 +1,10 @@ { "fixed": { - "file_version": "1.4.2" + "file_version": "1.4.3" }, "info": { "0000": { - "ProductVersion": "1.4.2", + "ProductVersion": "1.4.3", "CompanyName": "Bare7a", "FileDescription": "A fast, native SQL client built with Go, Wails and React.", "LegalCopyright": "(c) 2026, Bare7a", diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh index 305b877..97548e0 100644 --- a/build/windows/nsis/wails_tools.nsh +++ b/build/windows/nsis/wails_tools.nsh @@ -14,7 +14,7 @@ !define INFO_PRODUCTNAME "XenSQL" !endif !ifndef INFO_PRODUCTVERSION - !define INFO_PRODUCTVERSION "1.4.2" + !define INFO_PRODUCTVERSION "1.4.3" !endif !ifndef INFO_COPYRIGHT !define INFO_COPYRIGHT "(c) 2026, Bare7a" diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest index d54088f..def9f79 100644 --- a/build/windows/wails.exe.manifest +++ b/build/windows/wails.exe.manifest @@ -1,6 +1,6 @@ - + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1275739..f996d09 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,35 +1,35 @@ { "name": "xensql-frontend", - "version": "1.4.2", + "version": "1.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xensql-frontend", - "version": "1.4.2", + "version": "1.4.3", "dependencies": { - "@fontsource/fira-code": "^5.2.7", - "@fontsource/inter": "^5.2.8", + "@fontsource/fira-code": "^5.3.0", + "@fontsource/inter": "^5.3.0", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-virtual": "^3.14.5", + "@tanstack/react-virtual": "^3.14.7", "@wailsio/runtime": "latest", - "i18next": "^26.3.4", - "lucide-react": "^1.23.0", - "monaco-editor": "^0.55.1", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-i18next": "^17.0.8", + "i18next": "^26.3.6", + "lucide-react": "^1.25.0", + "monaco-editor": "^0.56.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-i18next": "^17.0.10", "sql-formatter": "^15.8.2", "zustand": "^5.0.14" }, "devDependencies": { - "@biomejs/biome": "2.5.2", - "@types/node": "^26.1.0", + "@biomejs/biome": "2.5.5", + "@types/node": "^26.1.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", - "typescript": "^6.0.3", - "vite": "^8.1.3", + "@vitejs/plugin-react": "^6.0.4", + "typescript": "^7.0.2", + "vite": "^8.1.5", "vitest": "^4.1.10" } }, @@ -43,9 +43,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.2.tgz", - "integrity": "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.5.tgz", + "integrity": "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -59,20 +59,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.2", - "@biomejs/cli-darwin-x64": "2.5.2", - "@biomejs/cli-linux-arm64": "2.5.2", - "@biomejs/cli-linux-arm64-musl": "2.5.2", - "@biomejs/cli-linux-x64": "2.5.2", - "@biomejs/cli-linux-x64-musl": "2.5.2", - "@biomejs/cli-win32-arm64": "2.5.2", - "@biomejs/cli-win32-x64": "2.5.2" + "@biomejs/cli-darwin-arm64": "2.5.5", + "@biomejs/cli-darwin-x64": "2.5.5", + "@biomejs/cli-linux-arm64": "2.5.5", + "@biomejs/cli-linux-arm64-musl": "2.5.5", + "@biomejs/cli-linux-x64": "2.5.5", + "@biomejs/cli-linux-x64-musl": "2.5.5", + "@biomejs/cli-win32-arm64": "2.5.5", + "@biomejs/cli-win32-x64": "2.5.5" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.2.tgz", - "integrity": "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.5.tgz", + "integrity": "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==", "cpu": [ "arm64" ], @@ -87,9 +87,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.2.tgz", - "integrity": "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.5.tgz", + "integrity": "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==", "cpu": [ "x64" ], @@ -104,9 +104,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.2.tgz", - "integrity": "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.5.tgz", + "integrity": "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==", "cpu": [ "arm64" ], @@ -124,9 +124,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.2.tgz", - "integrity": "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.5.tgz", + "integrity": "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==", "cpu": [ "arm64" ], @@ -144,9 +144,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.2.tgz", - "integrity": "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.5.tgz", + "integrity": "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==", "cpu": [ "x64" ], @@ -164,9 +164,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.2.tgz", - "integrity": "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.5.tgz", + "integrity": "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==", "cpu": [ "x64" ], @@ -184,9 +184,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.2.tgz", - "integrity": "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.5.tgz", + "integrity": "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==", "cpu": [ "arm64" ], @@ -201,9 +201,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.2.tgz", - "integrity": "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.5.tgz", + "integrity": "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==", "cpu": [ "x64" ], @@ -252,18 +252,18 @@ } }, "node_modules/@fontsource/fira-code": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.7.tgz", - "integrity": "sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.3.0.tgz", + "integrity": "sha512-EJL968RJRkakubAj/coU8pSUaeTE5UNoRjtzAr6kGiSZ3jWuN8/AKWHwym/PFUaQL1q7IL/H+EXs4358YhrTBQ==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, "node_modules/@fontsource/inter": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", - "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -319,9 +319,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.138.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", - "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", "funding": { @@ -329,9 +329,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", - "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -346,9 +346,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", - "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -363,9 +363,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", - "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -380,9 +380,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", - "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -397,9 +397,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", - "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -414,9 +414,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", - "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], @@ -434,9 +434,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", - "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], @@ -454,9 +454,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", - "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], @@ -474,9 +474,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", - "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], @@ -494,9 +494,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", - "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", - "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], @@ -534,9 +534,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", - "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -551,9 +551,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", - "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -570,9 +570,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", - "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -587,9 +587,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", - "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -618,12 +618,12 @@ "license": "MIT" }, "node_modules/@tanstack/react-virtual": { - "version": "3.14.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.5.tgz", - "integrity": "sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A==", + "version": "3.14.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.7.tgz", + "integrity": "sha512-11uSrj77IDijNBqizD4lY4y1laMyRrqMLSxjnWy5CvWkCjyRDW+gGmxYq0lwQKVas/sq7zyzYWXbL/BvBzR32g==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.17.3" + "@tanstack/virtual-core": "3.17.5" }, "funding": { "type": "github", @@ -635,9 +635,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.3.tgz", - "integrity": "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.5.tgz", + "integrity": "sha512-AXfBC3sq6PuYSwyxYORqqgHCNjPGAvKJvZuBBJ1klhztWBB5cgqgwsq8+fNfaQJG7/K4xYBja9S90QFn2zmQAg==", "license": "MIT", "funding": { "type": "github", @@ -681,9 +681,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", - "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", "dependencies": { @@ -717,10 +717,330 @@ "license": "MIT", "optional": true }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", "dev": true, "license": "MIT", "dependencies": { @@ -1003,9 +1323,9 @@ } }, "node_modules/i18next": { - "version": "26.3.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz", - "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==", + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", "funding": [ { "type": "individual", @@ -1022,7 +1342,7 @@ ], "license": "MIT", "peerDependencies": { - "typescript": "^5 || ^6" + "typescript": "^5 || ^6 || ^7" }, "peerDependenciesMeta": { "typescript": { @@ -1292,9 +1612,9 @@ } }, "node_modules/lucide-react": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", - "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz", + "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1323,12 +1643,12 @@ } }, "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", "license": "MIT", "dependencies": { - "dompurify": "3.2.7", + "dompurify": "3.4.8", "marked": "14.0.0" } }, @@ -1339,9 +1659,9 @@ "license": "BSD-3-Clause" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -1405,9 +1725,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -1418,9 +1738,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "dev": true, "funding": [ { @@ -1438,7 +1758,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1466,30 +1786,30 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-i18next": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", - "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz", + "integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", @@ -1499,7 +1819,7 @@ "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", - "typescript": "^5 || ^6" + "typescript": "^5 || ^6 || ^7" }, "peerDependenciesMeta": { "react-dom": { @@ -1523,13 +1843,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", - "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.138.0", + "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -1539,21 +1859,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.4", - "@rolldown/binding-darwin-arm64": "1.1.4", - "@rolldown/binding-darwin-x64": "1.1.4", - "@rolldown/binding-freebsd-x64": "1.1.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", - "@rolldown/binding-linux-arm64-gnu": "1.1.4", - "@rolldown/binding-linux-arm64-musl": "1.1.4", - "@rolldown/binding-linux-ppc64-gnu": "1.1.4", - "@rolldown/binding-linux-s390x-gnu": "1.1.4", - "@rolldown/binding-linux-x64-gnu": "1.1.4", - "@rolldown/binding-linux-x64-musl": "1.1.4", - "@rolldown/binding-openharmony-arm64": "1.1.4", - "@rolldown/binding-wasm32-wasi": "1.1.4", - "@rolldown/binding-win32-arm64-msvc": "1.1.4", - "@rolldown/binding-win32-x64-msvc": "1.1.4" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/scheduler": { @@ -1665,17 +1985,38 @@ "optional": true }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "devOptional": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" } }, "node_modules/undici-types": { @@ -1695,16 +2036,16 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { diff --git a/frontend/package.json b/frontend/package.json index 45da949..6145762 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "xensql-frontend", "private": true, - "version": "1.4.2", + "version": "1.4.3", "homepage": "https://xensql.bare7a.eu", "type": "module", "scripts": { @@ -18,28 +18,28 @@ "check:fix": "biome check --write ." }, "dependencies": { - "@fontsource/inter": "^5.2.8", - "@fontsource/fira-code": "^5.2.7", + "@fontsource/inter": "^5.3.0", + "@fontsource/fira-code": "^5.3.0", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-virtual": "^3.14.5", + "@tanstack/react-virtual": "^3.14.7", "@wailsio/runtime": "latest", - "i18next": "^26.3.4", - "lucide-react": "^1.23.0", - "monaco-editor": "^0.55.1", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-i18next": "^17.0.8", + "i18next": "^26.3.6", + "lucide-react": "^1.25.0", + "monaco-editor": "^0.56.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-i18next": "^17.0.10", "sql-formatter": "^15.8.2", "zustand": "^5.0.14" }, "devDependencies": { - "@biomejs/biome": "2.5.2", - "@types/node": "^26.1.0", + "@biomejs/biome": "2.5.5", + "@types/node": "^26.1.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", - "typescript": "^6.0.3", - "vite": "^8.1.3", + "@vitejs/plugin-react": "^6.0.4", + "typescript": "^7.0.2", + "vite": "^8.1.5", "vitest": "^4.1.10" }, "overrides": { diff --git a/frontend/src/features/editor/lib/monacoFontMetrics.ts b/frontend/src/features/editor/lib/monacoFontMetrics.ts index 23c0bb0..c579b08 100644 --- a/frontend/src/features/editor/lib/monacoFontMetrics.ts +++ b/frontend/src/features/editor/lib/monacoFontMetrics.ts @@ -1,4 +1,4 @@ -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import * as monaco from 'monaco-editor/editor/editor.api'; import { MONACO_FONT_FAMILY } from '@/shared/lib/appFonts'; export const MONACO_FONT_METRICS_OPTIONS = { diff --git a/frontend/src/features/editor/lib/monacoSetup.ts b/frontend/src/features/editor/lib/monacoSetup.ts index 9847485..23bf16c 100644 --- a/frontend/src/features/editor/lib/monacoSetup.ts +++ b/frontend/src/features/editor/lib/monacoSetup.ts @@ -1,25 +1,21 @@ -// Import the bare editor API, NOT the full 'monaco-editor' - the full entry statically -// registers every language service (TypeScript/CSS/HTML/JSON), which makes the bundler emit -// all their (large) workers regardless of the getWorker guard below. We register only the -// languages the app actually uses. +// Bare editor API + only the languages/features we use. Importing `monaco-editor` would +// pull in every language service/worker (TypeScript, CSS, …) even with the getWorker guard. import { loader } from '@monaco-editor/react'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import * as monaco from 'monaco-editor/editor/editor.api'; +import 'monaco-editor/editor/contrib/suggest/browser/suggestController'; // autocomplete widget +import 'monaco-editor/features/hover/register'; // SQL hover provider UI +import 'monaco-editor/features/find/register'; // Find / Find & Replace (context menu) +import 'monaco-editor/features/folding/register'; // JSON / cell viewer folding import { remeasureMonacoFonts } from '@/features/editor/lib/monacoFontMetrics'; -// JSON language service (needs a worker) - used by the row/cell JSON viewers. -import 'monaco-editor/esm/vs/language/json/monaco.contribution'; -// Syntax-highlighting-only languages (no worker): SQL editor/filter + cell viewer (xml/html). -import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution'; -import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution'; -import 'monaco-editor/esm/vs/basic-languages/html/html.contribution'; -import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; -import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; +import 'monaco-editor/languages/features/json/register'; // RowJsonViewer + cell JSON (worker) +import 'monaco-editor/languages/definitions/sql/register'; // SQL highlighting +import 'monaco-editor/languages/definitions/xml/register'; // cell viewer +import 'monaco-editor/languages/definitions/html/register'; // cell viewer +import editorWorker from 'monaco-editor/editor/editor.worker?worker'; +import jsonWorker from 'monaco-editor/languages/features/json/json.worker?worker'; -// Bundle Monaco and its workers locally and hand the instance to @monaco-editor/react, -// so the editor never fetches from a CDN - the app works fully offline. Only the base -// editor worker and JSON worker are bundled; the app uses SQL (custom) + JSON + plaintext, -// plus xml/html highlighting in the cell viewer - never the TypeScript/CSS/HTML language -// services, so their (large) workers stay out of the bundle. +// Local bundle for @monaco-editor/react (no CDN) — editor worker + JSON worker only. export function initMonaco(): void { remeasureMonacoFonts(); diff --git a/frontend/src/features/editor/lib/monacoTheme.ts b/frontend/src/features/editor/lib/monacoTheme.ts index f23cf81..a36d50c 100644 --- a/frontend/src/features/editor/lib/monacoTheme.ts +++ b/frontend/src/features/editor/lib/monacoTheme.ts @@ -1,6 +1,6 @@ import type { Monaco } from '@monaco-editor/react'; import type { editor } from 'monaco-editor'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import * as monaco from 'monaco-editor/editor/editor.api'; import type { AppTheme } from '@/shared/lib/theme'; const XENSQL_MONACO_THEME_DARK = 'xensql-dark'; diff --git a/frontend/src/shared/lib/appInfo.ts b/frontend/src/shared/lib/appInfo.ts index 81eb880..76a586b 100644 --- a/frontend/src/shared/lib/appInfo.ts +++ b/frontend/src/shared/lib/appInfo.ts @@ -11,7 +11,7 @@ export interface AppInfo { /** Fallback when Go binding is unavailable (dev in browser). */ export const DEFAULT_APP_INFO: AppInfo = { name: 'XenSQL', - version: '1.4.2', + version: '1.4.3', author: 'Bare7a', email: 'bare7a@gmail.com', website: 'https://xensql.bare7a.eu', diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index b85b3d4..11f02fe 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1,12 +1 @@ /// - -// Monaco ships no .d.ts for its deep ESM subpaths (we import them directly to keep -// unused language services/workers out of the bundle - see features/editor/lib/monacoSetup.ts). -// Vite resolves the real files at build time; these just satisfy tsc. -declare module 'monaco-editor/esm/vs/editor/editor.api' { - export * from 'monaco-editor'; -} -declare module 'monaco-editor/esm/vs/language/json/monaco.contribution'; -declare module 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution'; -declare module 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution'; -declare module 'monaco-editor/esm/vs/basic-languages/html/html.contribution'; diff --git a/go.mod b/go.mod index 7e2275d..0483a59 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/go-sql-driver/mysql v1.10.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.10.0 - github.com/wailsapp/wails/v3 v3.0.0-alpha2.115 - modernc.org/sqlite v1.53.0 + github.com/wailsapp/wails/v3 v3.0.0-alpha2.117 + modernc.org/sqlite v1.54.0 ) require ( @@ -22,14 +22,14 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-isatty v0.0.23 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect - modernc.org/libc v1.73.5 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + modernc.org/libc v1.74.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 3b8352c..53174e5 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ= +github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -52,28 +52,28 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/wailsapp/wails/v3 v3.0.0-alpha2.115 h1:AwVincOURTpVeSNWAdZ9/xXnfrsHCBtt6ry9I5L8SZM= -github.com/wailsapp/wails/v3 v3.0.0-alpha2.115/go.mod h1:74WH2FScMsgucZvHHvv7eOefDXCm/CjuIxqhhZgPhKg= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +github.com/wailsapp/wails/v3 v3.0.0-alpha2.117 h1:udyjqPG3AIgkod5QDR/WblCkpV8R86BFPSrsWxSyt5Y= +github.com/wailsapp/wails/v3 v3.0.0-alpha2.117/go.mod h1:74WH2FScMsgucZvHHvv7eOefDXCm/CjuIxqhhZgPhKg= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= -modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.5 h1:hcwnthv2/LBl+mRLOYwnQA/LuW44Oln1NQlWppNaS1Q= -modernc.org/ccgo/v4 v4.34.5/go.mod h1:aow0HNkO30OSA/2NrtDXkis92ff8ZFiDOmDOPhqhF8U= +modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= +modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= @@ -82,8 +82,8 @@ modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.73.5 h1:G34rN/cRqL+zOUnrbz9uPq/+OxJ8/vzQ2CQwTJ42Wmw= -modernc.org/libc v1.73.5/go.mod h1:+Aoyx4M0etg6GikzCrip1VtvAtUlMlo2Aq+GHwQSqOA= +modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws= +modernc.org/libc v1.74.3/go.mod h1:4H7h/MJ8wnjL8RAbp9v3OXgnk22X7MouHIhDbvP3gj4= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -92,8 +92,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= -modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= +modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/app/version.go b/internal/app/version.go index 4ab8023..4e95a6c 100644 --- a/internal/app/version.go +++ b/internal/app/version.go @@ -3,4 +3,4 @@ package app // Version is the source of truth for the application version. // In order to update it use the following command: // go run ./cmd/bump-version [-major, -minor, -patch, 1.2.0] -const Version = "1.4.2" +const Version = "1.4.3" From cfa941d1f2588772129a9f035a9da418e18b3a94 Mon Sep 17 00:00:00 2001 From: Bare7a Date: Wed, 22 Jul 2026 13:53:05 +0300 Subject: [PATCH 9/9] Updated dompurify --- frontend/package-lock.json | 6 +++--- frontend/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f996d09..352c9a4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1245,9 +1245,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", - "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" diff --git a/frontend/package.json b/frontend/package.json index 6145762..57a81bf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,6 +43,6 @@ "vitest": "^4.1.10" }, "overrides": { - "dompurify": "^3.4.11" + "dompurify": "^3.4.12" } }