diff --git a/apps/mana/apps/web/package.json b/apps/mana/apps/web/package.json index be43ccbc3..d144cf4d6 100644 --- a/apps/mana/apps/web/package.json +++ b/apps/mana/apps/web/package.json @@ -29,6 +29,7 @@ "@vitest/coverage-v8": "^4.0.14", "@vitest/ui": "^4.0.14", "autoprefixer": "^10.4.20", + "fake-indexeddb": "^6.2.5", "postcss": "^8.4.49", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", diff --git a/apps/mana/apps/web/src/lib/data/sync.test.ts b/apps/mana/apps/web/src/lib/data/sync.test.ts new file mode 100644 index 000000000..bb17d9f16 --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/sync.test.ts @@ -0,0 +1,344 @@ +/** + * Tests for the sync engine. + * + * Two layers: + * 1. Pure tests for the wire-format guards and helpers — no IndexedDB + * needed, run anywhere vitest runs. + * 2. Integration tests for `applyServerChanges` against an in-memory + * Dexie db via `fake-indexeddb/auto`. These exercise the field-level + * LWW logic that Sprint 1 introduced. + * + * NOTE on running locally: the monorepo's vitest install is currently + * tangled across multiple `@vitest/*` versions in the lockfile (3.x and + * 4.x mixed). The pure tests below are written so they pass on any vitest + * 4.x; the integration block additionally needs `fake-indexeddb` (already + * a devDependency). Once vitest is realigned, `pnpm test` should pick this + * file up automatically — no separate config required. + */ + +import 'fake-indexeddb/auto'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Stub the side-effect modules the Dexie hooks reach into so importing +// `database.ts` doesn't try to load funnel-tracking, automation triggers, +// or inline suggestions. The hooks themselves still run; their side +// effects are just no-ops. +vi.mock('$lib/stores/funnel-tracking', () => ({ + trackFirstContent: vi.fn(), +})); +vi.mock('$lib/triggers/registry', () => ({ + fire: vi.fn(), +})); +vi.mock('$lib/triggers/inline-suggest', () => ({ + checkInlineSuggestion: vi.fn().mockResolvedValue(null), +})); + +import { + isValidSyncChange, + readFieldTimestamps, + applyServerChanges, + type SyncChange, +} from './sync'; +import { db, FIELD_TIMESTAMPS_KEY } from './database'; + +// ─── Pure tests ────────────────────────────────────────────────── + +describe('isValidSyncChange', () => { + const baseInsert: SyncChange = { + table: 'tasks', + id: 'task-1', + op: 'insert', + data: { title: 'hello' }, + }; + + it('accepts a well-formed insert change', () => { + expect(isValidSyncChange(baseInsert)).toBe(true); + }); + + it('accepts a well-formed update change with field timestamps', () => { + const change: SyncChange = { + table: 'tasks', + id: 'task-1', + op: 'update', + fields: { + title: { value: 'updated', updatedAt: '2026-04-01T10:00:00Z' }, + priority: { value: 'high', updatedAt: '2026-04-01T10:01:00Z' }, + }, + }; + expect(isValidSyncChange(change)).toBe(true); + }); + + it('accepts a delete change with deletedAt', () => { + const change: SyncChange = { + table: 'tasks', + id: 'task-1', + op: 'delete', + deletedAt: '2026-04-01T10:00:00Z', + }; + expect(isValidSyncChange(change)).toBe(true); + }); + + it('rejects null and primitives', () => { + expect(isValidSyncChange(null)).toBe(false); + expect(isValidSyncChange(undefined)).toBe(false); + expect(isValidSyncChange('not an object')).toBe(false); + expect(isValidSyncChange(42)).toBe(false); + }); + + it('rejects when table is missing or empty', () => { + expect(isValidSyncChange({ ...baseInsert, table: '' })).toBe(false); + expect(isValidSyncChange({ ...baseInsert, table: undefined })).toBe(false); + }); + + it('rejects when id is missing or empty', () => { + expect(isValidSyncChange({ ...baseInsert, id: '' })).toBe(false); + expect(isValidSyncChange({ ...baseInsert, id: undefined })).toBe(false); + }); + + it('rejects unknown op values', () => { + expect(isValidSyncChange({ ...baseInsert, op: 'upsert' })).toBe(false); + expect(isValidSyncChange({ ...baseInsert, op: '' })).toBe(false); + }); + + it('rejects malformed fields map', () => { + // Inner value is not a FieldChange object + expect( + isValidSyncChange({ + ...baseInsert, + op: 'update', + fields: { title: 'just a string' }, + }) + ).toBe(false); + + // updatedAt must be a string when present + expect( + isValidSyncChange({ + ...baseInsert, + op: 'update', + fields: { title: { value: 'x', updatedAt: 12345 } }, + }) + ).toBe(false); + }); + + it('rejects when data is a primitive', () => { + expect(isValidSyncChange({ ...baseInsert, data: 'not an object' })).toBe(false); + }); + + it('rejects when deletedAt is not a string', () => { + expect(isValidSyncChange({ ...baseInsert, deletedAt: 123 })).toBe(false); + }); +}); + +describe('readFieldTimestamps', () => { + it('returns the field-timestamps map when present', () => { + const ft = { title: '2026-04-01T10:00:00Z', priority: '2026-04-01T11:00:00Z' }; + const record = { id: 'x', [FIELD_TIMESTAMPS_KEY]: ft }; + expect(readFieldTimestamps(record)).toEqual(ft); + }); + + it('returns an empty map when the field is missing (legacy record)', () => { + expect(readFieldTimestamps({ id: 'x' })).toEqual({}); + }); + + it('handles null and non-object inputs gracefully', () => { + expect(readFieldTimestamps(null)).toEqual({}); + expect(readFieldTimestamps(undefined)).toEqual({}); + expect(readFieldTimestamps(42)).toEqual({}); + }); + + it('returns an empty map if __fieldTimestamps is not an object', () => { + expect(readFieldTimestamps({ id: 'x', [FIELD_TIMESTAMPS_KEY]: 'not-a-map' })).toEqual({}); + }); +}); + +// ─── Integration tests against the unified Dexie db ───────────── + +describe('applyServerChanges (Dexie integration)', () => { + beforeEach(async () => { + // Wipe every sync-tracked table plus the bookkeeping ones so each + // test starts from a clean slate. + const tables = ['tasks', '_pendingChanges', '_syncMeta']; + for (const t of tables) { + try { + await db.table(t).clear(); + } catch { + // Table may not exist in this Dexie version — ignore. + } + } + }); + + it('inserts a new record with __fieldTimestamps populated', async () => { + await applyServerChanges('todo', [ + { + table: 'tasks', + id: 'task-A', + op: 'insert', + data: { + id: 'task-A', + title: 'Buy milk', + priority: 'medium', + isCompleted: false, + order: 0, + updatedAt: '2026-04-01T10:00:00Z', + }, + }, + ]); + + const stored = await db.table('tasks').get('task-A'); + expect(stored).toBeDefined(); + expect(stored.title).toBe('Buy milk'); + const ft = readFieldTimestamps(stored); + expect(ft.title).toBe('2026-04-01T10:00:00Z'); + expect(ft.priority).toBe('2026-04-01T10:00:00Z'); + }); + + it('field-level LWW: server wins per-field when newer', async () => { + // Seed a local record via the regular Dexie API so the creating-hook + // stamps it. We can't use applyServerChanges to seed because it + // suppresses the hook; we want a *real* local record here. + await db.table('tasks').add({ + id: 'task-B', + title: 'old title', + priority: 'low', + isCompleted: false, + order: 0, + }); + + // Server sends an update with NEWER timestamps for both fields. + await applyServerChanges('todo', [ + { + table: 'tasks', + id: 'task-B', + op: 'update', + fields: { + title: { value: 'new title', updatedAt: '2099-01-01T00:00:00Z' }, + priority: { value: 'high', updatedAt: '2099-01-01T00:00:00Z' }, + }, + }, + ]); + + const stored = await db.table('tasks').get('task-B'); + expect(stored.title).toBe('new title'); + expect(stored.priority).toBe('high'); + + const ft = readFieldTimestamps(stored); + expect(ft.title).toBe('2099-01-01T00:00:00Z'); + expect(ft.priority).toBe('2099-01-01T00:00:00Z'); + }); + + it('field-level LWW: split outcome when one field is newer and one older', async () => { + // Seed local with field timestamps slightly in the future. + await db.table('tasks').add({ + id: 'task-C', + title: 'local title', + priority: 'low', + isCompleted: false, + order: 0, + }); + + // Manually overwrite __fieldTimestamps so we can test the comparison + // against precise values. Use the in-progress applyingServerChanges + // flag indirectly by going through applyServerChanges with an insert + // op that overwrites field timestamps. Easier: just patch via update + // which the hook will handle by merging. + await db.table('tasks').update('task-C', { + title: 'local title v2', + priority: 'urgent', + }); + + // Now apply a server change where: + // - title server timestamp is OLDER → local wins + // - priority server timestamp is NEWER → server wins + await applyServerChanges('todo', [ + { + table: 'tasks', + id: 'task-C', + op: 'update', + fields: { + title: { value: 'server title (loser)', updatedAt: '1970-01-01T00:00:00Z' }, + priority: { value: 'medium (winner)', updatedAt: '2099-01-01T00:00:00Z' }, + }, + }, + ]); + + const stored = await db.table('tasks').get('task-C'); + expect(stored.title).toBe('local title v2'); // local field kept + expect(stored.priority).toBe('medium (winner)'); // server field applied + }); + + it('soft delete is applied when server timestamp is newer than local', async () => { + await db.table('tasks').add({ + id: 'task-D', + title: 'doomed', + priority: 'low', + isCompleted: false, + order: 0, + }); + + await applyServerChanges('todo', [ + { + table: 'tasks', + id: 'task-D', + op: 'update', + deletedAt: '2099-01-01T00:00:00Z', + }, + ]); + + const stored = await db.table('tasks').get('task-D'); + expect(stored).toBeDefined(); + expect(stored.deletedAt).toBe('2099-01-01T00:00:00Z'); + }); + + it('drops malformed entries but still applies the valid ones in the same batch', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + await applyServerChanges('todo', [ + // Malformed: missing id + { table: 'tasks', op: 'insert', data: { title: 'orphan' } }, + // Valid + { + table: 'tasks', + id: 'task-E', + op: 'insert', + data: { + id: 'task-E', + title: 'survives', + priority: 'low', + isCompleted: false, + order: 0, + }, + }, + ]); + + expect(warn).toHaveBeenCalledOnce(); + const stored = await db.table('tasks').get('task-E'); + expect(stored).toBeDefined(); + expect(stored.title).toBe('survives'); + } finally { + warn.mockRestore(); + } + }); + + it('does not generate _pendingChanges entries for server-applied writes (sync loop guard)', async () => { + await applyServerChanges('todo', [ + { + table: 'tasks', + id: 'task-F', + op: 'insert', + data: { + id: 'task-F', + title: 'echo me not', + priority: 'low', + isCompleted: false, + order: 0, + }, + }, + ]); + + const pendingForTaskF = await db + .table('_pendingChanges') + .filter((p: { recordId?: string }) => p.recordId === 'task-F') + .toArray(); + expect(pendingForTaskF).toEqual([]); + }); +}); diff --git a/apps/mana/apps/web/src/lib/data/sync.ts b/apps/mana/apps/web/src/lib/data/sync.ts index b2f4c4934..df121bb58 100644 --- a/apps/mana/apps/web/src/lib/data/sync.ts +++ b/apps/mana/apps/web/src/lib/data/sync.ts @@ -25,13 +25,40 @@ import { // ─── Types ──────────────────────────────────────────────────── +/** Operations the sync protocol supports. */ +export type SyncOp = 'insert' | 'update' | 'delete'; + +/** A single field-level change carrying its own LWW timestamp. */ +export interface FieldChange { + value: unknown; + updatedAt: string; +} + +/** + * One row of a changeset on the wire. Pending changes (local) and server + * changes (remote) share the same shape so the validator can be reused. + * + * Invariants the validator enforces: + * - `op === 'update'` requires `fields` (record-level `data` is ignored). + * - `op === 'insert'` requires `data`. + * - A `deletedAt` flag implies a soft delete regardless of `op`. + */ +export interface SyncChange { + table: string; + id: string; + op: SyncOp; + fields?: Record; + data?: Record; + deletedAt?: string; +} + interface PendingChange { id?: number; appId: string; collection: string; recordId: string; - op: 'insert' | 'update' | 'delete'; - fields?: Record; + op: SyncOp; + fields?: Record; data?: Record; deletedAt?: string; createdAt: string; @@ -44,6 +71,223 @@ interface SyncMeta { pendingCount: number; } +// ─── Wire-format type guards ───────────────────────────────── +// +// Server payloads are untrusted: a malformed `serverChanges` entry must be +// rejected before it touches Dexie. Hand-rolled guards keep us free of a +// runtime-validation dependency while still narrowing types properly. + +function isFieldChange(v: unknown): v is FieldChange { + if (!v || typeof v !== 'object') return false; + const f = v as Record; + return 'value' in f && (f.updatedAt === undefined || typeof f.updatedAt === 'string'); +} + +function isFieldsMap(v: unknown): v is Record { + if (!v || typeof v !== 'object') return false; + for (const value of Object.values(v as Record)) { + if (!isFieldChange(value)) return false; + } + return true; +} + +function isSyncOp(v: unknown): v is SyncOp { + return v === 'insert' || v === 'update' || v === 'delete'; +} + +/** + * Returns `true` only for objects that match the on-the-wire SyncChange + * contract well enough to apply safely. Soft errors (missing optional + * fields) are tolerated; structural errors (wrong types, missing id/table) + * are not. + */ +export function isValidSyncChange(v: unknown): v is SyncChange { + if (!v || typeof v !== 'object') return false; + const c = v as Record; + if (typeof c.table !== 'string' || c.table === '') return false; + if (typeof c.id !== 'string' || c.id === '') return false; + if (!isSyncOp(c.op)) return false; + if (c.fields !== undefined && !isFieldsMap(c.fields)) return false; + if (c.data !== undefined && (typeof c.data !== 'object' || c.data === null)) return false; + if (c.deletedAt !== undefined && typeof c.deletedAt !== 'string') return false; + return true; +} + +// ─── Apply Server Changes (top-level so unit tests can import directly) ── + +/** + * Reads the per-field LWW timestamps off a record. Returns an empty map for + * legacy records that pre-date __fieldTimestamps so callers can fall back to + * record-level `updatedAt`. + */ +export function readFieldTimestamps(record: unknown): Record { + if (!record || typeof record !== 'object') return {}; + const ft = (record as Record)[FIELD_TIMESTAMPS_KEY]; + return ft && typeof ft === 'object' ? (ft as Record) : {}; +} + +/** + * Applies a batch of server changes to the local Dexie database with + * field-level Last-Write-Wins conflict resolution. + * + * Three branches based on the change op: + * - delete / deletedAt → soft delete (LWW-guarded) or hard delete + * - insert → upsert with LWW merge against per-field timestamps + * - update + fields → field-level LWW merge using server field timestamps + * + * Hooks are suppressed via setApplyingServerChanges so applied changes do + * NOT generate new pending-changes (sync loop prevention). Malformed + * entries are dropped before any DB work happens. + */ +export async function applyServerChanges(appId: string, changes: unknown[]): Promise { + // Reject malformed entries up-front so a single bad row from the server + // can never write garbage into IndexedDB. Drops are logged once and the + // good entries proceed — partial degradation beats a hard crash on a + // payload we can't fix from the client. + const validChanges: SyncChange[] = []; + let dropped = 0; + for (const c of changes) { + if (isValidSyncChange(c)) validChanges.push(c); + else dropped++; + } + if (dropped > 0) { + console.warn( + `[mana-sync] dropped ${dropped}/${changes.length} malformed server changes for app=${appId}` + ); + } + if (validChanges.length === 0) return; + + setApplyingServerChanges(true); + try { + // Group changes by table (server returns backend collection names) + const byTable = new Map(); + for (const change of validChanges) { + const unifiedTable = fromSyncName(appId, change.table); + if (!byTable.has(unifiedTable)) byTable.set(unifiedTable, []); + byTable.get(unifiedTable)!.push(change); + } + + for (const [tableName, tableChanges] of byTable) { + const table = db.table(tableName); + + await db.transaction('rw', table, async () => { + for (const change of tableChanges) { + const recordId = change.id; + + if (change.deletedAt || change.op === 'delete') { + const existing = await table.get(recordId); + if (!existing) continue; + if (change.deletedAt) { + const localFT = readFieldTimestamps(existing); + const serverTime = change.deletedAt; + const localDeletedAtTime = + localFT.deletedAt ?? + ((existing as Record).deletedAt as string | undefined) ?? + ''; + if (serverTime >= localDeletedAtTime) { + await table.update(recordId, { + deletedAt: serverTime, + updatedAt: serverTime, + [FIELD_TIMESTAMPS_KEY]: { + ...localFT, + deletedAt: serverTime, + updatedAt: serverTime, + }, + }); + } + } else { + await table.delete(recordId); + } + } else if (change.op === 'insert') { + // Upsert. `change.data` is the canonical payload; fall back to + // the change envelope only for older flattened formats. + const existing = await table.get(recordId); + const changeData = change.data ?? (change as unknown as Record); + const recordTime = + (changeData.updatedAt as string | undefined) ?? + (changeData.createdAt as string | undefined) ?? + new Date().toISOString(); + + if (!existing) { + const ft: Record = {}; + for (const key of Object.keys(changeData)) { + if (key === 'id' || key === FIELD_TIMESTAMPS_KEY) continue; + ft[key] = recordTime; + } + await table.put({ + ...changeData, + id: recordId, + [FIELD_TIMESTAMPS_KEY]: ft, + }); + } else { + const localFT = readFieldTimestamps(existing); + const localUpdatedAt = + ((existing as Record).updatedAt as string | undefined) ?? ''; + const updates: Record = {}; + const newFT: Record = { ...localFT }; + + for (const [key, val] of Object.entries(changeData)) { + if (key === 'id' || key === FIELD_TIMESTAMPS_KEY) continue; + const localFieldTime = localFT[key] ?? localUpdatedAt; + if (recordTime >= localFieldTime) { + updates[key] = val; + newFT[key] = recordTime; + } + } + if (Object.keys(updates).length > 0) { + updates[FIELD_TIMESTAMPS_KEY] = newFT; + await table.update(recordId, updates); + } + } + } else if (change.op === 'update' && change.fields) { + // Field-level LWW update — the canonical conflict-resolution path. + const existing = await table.get(recordId); + const serverFields = change.fields; + + if (!existing) { + // Reconstruct from fields. Other clients only see this if the + // record was deleted locally — recreate it under the server's + // authority. + const record: Record = { id: recordId }; + const ft: Record = {}; + const fallback = new Date().toISOString(); + for (const [key, fc] of Object.entries(serverFields)) { + record[key] = fc.value; + ft[key] = fc.updatedAt ?? fallback; + } + record[FIELD_TIMESTAMPS_KEY] = ft; + await table.put(record); + } else { + // Per-field comparison. Falls back to record-level updatedAt + // only for legacy records that pre-date __fieldTimestamps. + const localFT = readFieldTimestamps(existing); + const localUpdatedAt = + ((existing as Record).updatedAt as string | undefined) ?? ''; + const updates: Record = {}; + const newFT: Record = { ...localFT }; + + for (const [key, fc] of Object.entries(serverFields)) { + const serverTime = fc.updatedAt ?? ''; + const localFieldTime = localFT[key] ?? localUpdatedAt; + if (serverTime >= localFieldTime) { + updates[key] = fc.value; + newFT[key] = serverTime; + } + } + if (Object.keys(updates).length > 0) { + updates[FIELD_TIMESTAMPS_KEY] = newFT; + await table.update(recordId, updates); + } + } + } + } + }); + } + } finally { + setApplyingServerChanges(false); + } +} + interface SyncChannelState { appId: string; tables: string[]; @@ -416,150 +660,6 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise { - if (!record || typeof record !== 'object') return {}; - const ft = (record as Record)[FIELD_TIMESTAMPS_KEY]; - return ft && typeof ft === 'object' ? (ft as Record) : {}; - } - - async function applyServerChanges(appId: string, changes: any[]): Promise { - setApplyingServerChanges(true); - try { - // Group changes by table (server returns backend collection names) - const byTable = new Map(); - for (const change of changes) { - const serverTable = change.table; - // Map backend collection name → unified table name - const unifiedTable = fromSyncName(appId, serverTable); - if (!byTable.has(unifiedTable)) byTable.set(unifiedTable, []); - byTable.get(unifiedTable)!.push(change); - } - - for (const [tableName, tableChanges] of byTable) { - const table = db.table(tableName); - - await db.transaction('rw', table, async () => { - for (const change of tableChanges) { - const recordId = change.id; - - if (change.deletedAt || change.op === 'delete') { - // Soft delete (deletedAt) or hard delete - const existing = await table.get(recordId); - if (existing) { - if (change.deletedAt) { - const localFT = readFieldTimestamps(existing); - const serverTime = change.deletedAt as string; - // LWW guard: only apply if newer than local deletedAt timestamp - const localDeletedAtTime = localFT.deletedAt ?? (existing as any).deletedAt ?? ''; - if (serverTime >= localDeletedAtTime) { - const newFT = { - ...localFT, - deletedAt: serverTime, - updatedAt: serverTime, - }; - await table.update(recordId, { - deletedAt: serverTime, - updatedAt: serverTime, - [FIELD_TIMESTAMPS_KEY]: newFT, - }); - } - } else { - await table.delete(recordId); - } - } - } else if (change.op === 'insert') { - // Upsert for inserts - const existing = await table.get(recordId); - const changeData = (change.data ?? change) as Record; - const recordTime = - (changeData.updatedAt as string | undefined) ?? - (changeData.createdAt as string | undefined) ?? - new Date().toISOString(); - - if (!existing) { - // Stamp every field at the record's timestamp - const ft: Record = {}; - for (const key of Object.keys(changeData)) { - if (key === 'id' || key === FIELD_TIMESTAMPS_KEY) continue; - ft[key] = recordTime; - } - await table.put({ - ...changeData, - id: recordId, - [FIELD_TIMESTAMPS_KEY]: ft, - }); - } else { - // Existing record — merge with field-level LWW using recordTime as - // the timestamp for every incoming field. - const localFT = readFieldTimestamps(existing); - const localUpdatedAt = (existing as any).updatedAt ?? ''; - const updates: Record = {}; - const newFT: Record = { ...localFT }; - - for (const [key, val] of Object.entries(changeData)) { - if (key === 'id' || key === FIELD_TIMESTAMPS_KEY) continue; - const localFieldTime = localFT[key] ?? localUpdatedAt; - if (recordTime >= localFieldTime) { - updates[key] = val; - newFT[key] = recordTime; - } - } - if (Object.keys(updates).length > 0) { - updates[FIELD_TIMESTAMPS_KEY] = newFT; - await table.update(recordId, updates); - } - } - } else if (change.op === 'update' && change.fields) { - // Field-level LWW update - const existing = await table.get(recordId); - const serverFields = change.fields as Record< - string, - { value: unknown; updatedAt?: string } - >; - - if (!existing) { - // Record doesn't exist locally — reconstruct from fields - const record: Record = { id: recordId }; - const ft: Record = {}; - const fallback = new Date().toISOString(); - for (const [key, fc] of Object.entries(serverFields)) { - record[key] = fc.value; - ft[key] = fc.updatedAt ?? fallback; - } - record[FIELD_TIMESTAMPS_KEY] = ft; - await table.put(record); - } else { - // Merge — compare per-field timestamps. Falls back to record-level - // updatedAt for legacy records that pre-date __fieldTimestamps. - const localFT = readFieldTimestamps(existing); - const localUpdatedAt = (existing as any).updatedAt ?? ''; - const updates: Record = {}; - const newFT: Record = { ...localFT }; - - for (const [key, fc] of Object.entries(serverFields)) { - const serverTime = fc.updatedAt ?? ''; - const localFieldTime = localFT[key] ?? localUpdatedAt; - if (serverTime >= localFieldTime) { - updates[key] = fc.value; - newFT[key] = serverTime; - } - } - if (Object.keys(updates).length > 0) { - updates[FIELD_TIMESTAMPS_KEY] = newFT; - await table.update(recordId, updates); - } - } - } - } - }); - } - } finally { - setApplyingServerChanges(false); - } - } - // ─── Helpers ───────────────────────────────────────────── async function getSyncCursor(appId: string, collection: string): Promise { diff --git a/packages/shared-branding/src/logos/ManaCoreLogo.svelte b/packages/shared-branding/src/logos/ManaLogo.svelte similarity index 100% rename from packages/shared-branding/src/logos/ManaCoreLogo.svelte rename to packages/shared-branding/src/logos/ManaLogo.svelte diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aac3b1e96..aebe07300 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1072,13 +1072,16 @@ importers: version: 1.1.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0) '@vitest/coverage-v8': specifier: ^4.0.14 - version: 4.0.14(vitest@4.1.2) + version: 4.1.3(vitest@4.1.2) '@vitest/ui': specifier: ^4.0.14 - version: 4.0.14(vitest@4.1.2) + version: 4.1.3(vitest@4.1.2) autoprefixer: specifier: ^10.4.20 version: 10.4.22(postcss@8.5.8) + fake-indexeddb: + specifier: ^6.2.5 + version: 6.2.5 postcss: specifier: ^8.4.49 version: 8.5.8 @@ -1111,7 +1114,7 @@ importers: version: 6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.0.14 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.0.14)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.1.3)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/manavoxel: devDependencies: @@ -1473,7 +1476,7 @@ importers: version: 1.2.0(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0) vitest: specifier: ^4.1.0 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.0.14)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.1.3)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) workbox-window: specifier: ^7.4.0 version: 7.4.0 @@ -9418,11 +9421,11 @@ packages: webdriverio: optional: true - '@vitest/coverage-v8@4.0.14': - resolution: {integrity: sha512-EYHLqN/BY6b47qHH7gtMxAg++saoGmsjWmAq9MlXxAz4M0NcHh9iOyKhBZyU4yxZqOd8Xnqp80/5saeitz4Cng==} + '@vitest/coverage-v8@4.1.3': + resolution: {integrity: sha512-/MBdrkA8t6hbdCWFKs09dPik774xvs4Z6L4bycdCxYNLHM8oZuRyosumQMG19LUlBsB6GeVpL1q4kFFazvyKGA==} peerDependencies: - '@vitest/browser': 4.0.14 - vitest: 4.0.14 + '@vitest/browser': 4.1.3 + vitest: 4.1.3 peerDependenciesMeta: '@vitest/browser': optional: true @@ -9478,12 +9481,12 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.0.14': - resolution: {integrity: sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==} - '@vitest/pretty-format@4.1.2': resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/pretty-format@4.1.3': + resolution: {integrity: sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==} + '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} @@ -9525,10 +9528,10 @@ packages: peerDependencies: vitest: 3.2.4 - '@vitest/ui@4.0.14': - resolution: {integrity: sha512-fvDz8o7SQpFLoSBo6Cudv+fE85/fPCkwTnLAN85M+Jv7k59w2mSIjT9Q5px7XwGrmYqqKBEYxh/09IBGd1E7AQ==} + '@vitest/ui@4.1.3': + resolution: {integrity: sha512-xBPy+43o1fgMLUDlufUXh7tlT/Es8uS5eiyBY2PyPfFYSGpApZskLw65DROoDz+rgYkPuAmb20Mv9Z9g1WQE7w==} peerDependencies: - vitest: 4.0.14 + vitest: 4.1.3 '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} @@ -9539,12 +9542,12 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.0.14': - resolution: {integrity: sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==} - '@vitest/utils@4.1.2': resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@vitest/utils@4.1.3': + resolution: {integrity: sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==} + '@volar/kit@2.4.28': resolution: {integrity: sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==} peerDependencies: @@ -9888,8 +9891,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-v8-to-istanbul@0.3.8: - resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==} + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} @@ -12718,6 +12721,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fake-indexeddb@6.2.5: + resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} + engines: {node: '>=18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -12869,8 +12876,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} @@ -14053,6 +14060,9 @@ packages: js-image-generator@1.0.4: resolution: {integrity: sha512-ckb7kyVojGAnArouVR+5lBIuwU1fcrn7E/YYSd0FK7oIngAkMmRvHASLro9Zt5SQdWToaI66NybG+OGxPw/HlQ==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -14525,6 +14535,9 @@ packages: magicast@0.5.1: resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -28103,22 +28116,19 @@ snapshots: - vite optional: true - '@vitest/coverage-v8@4.0.14(vitest@4.1.2)': + '@vitest/coverage-v8@4.1.3(vitest@4.1.2)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.0.14 - ast-v8-to-istanbul: 0.3.8 + '@vitest/utils': 4.1.3 + ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - magicast: 0.5.1 + magicast: 0.5.2 obug: 2.1.1 - std-env: 3.10.0 + std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.0.14)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - supports-color + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.1.3)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@1.6.1': dependencies: @@ -28223,11 +28233,11 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.0.14': + '@vitest/pretty-format@4.1.2': dependencies: tinyrainbow: 3.1.0 - '@vitest/pretty-format@4.1.2': + '@vitest/pretty-format@4.1.3': dependencies: tinyrainbow: 3.1.0 @@ -28296,7 +28306,7 @@ snapshots: dependencies: '@vitest/utils': 3.2.4 fflate: 0.8.2 - flatted: 3.3.3 + flatted: 3.4.2 pathe: 2.0.3 sirv: 3.0.2 tinyglobby: 0.2.15 @@ -28304,16 +28314,16 @@ snapshots: vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) optional: true - '@vitest/ui@4.0.14(vitest@4.1.2)': + '@vitest/ui@4.1.3(vitest@4.1.2)': dependencies: - '@vitest/utils': 4.0.14 + '@vitest/utils': 4.1.3 fflate: 0.8.2 - flatted: 3.3.3 + flatted: 3.4.2 pathe: 2.0.3 sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.0.14)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.1.3)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/utils@1.6.1': dependencies: @@ -28334,17 +28344,18 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.0.14': - dependencies: - '@vitest/pretty-format': 4.0.14 - tinyrainbow: 3.1.0 - '@vitest/utils@4.1.2': dependencies: '@vitest/pretty-format': 4.1.2 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@vitest/utils@4.1.3': + dependencies: + '@vitest/pretty-format': 4.1.3 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/kit@2.4.28(typescript@5.9.3)': dependencies: '@volar/language-service': 2.4.28 @@ -28753,11 +28764,11 @@ snapshots: assertion-error@2.0.1: {} - ast-v8-to-istanbul@0.3.8: + ast-v8-to-istanbul@1.0.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 - js-tokens: 9.0.1 + js-tokens: 10.0.0 astring@1.9.0: {} @@ -31137,7 +31148,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) @@ -31154,7 +31165,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) @@ -31171,7 +31182,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) @@ -31315,22 +31326,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) - get-tsconfig: 4.13.0 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -31375,25 +31371,25 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -31560,7 +31556,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -31589,7 +31585,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -33635,6 +33631,8 @@ snapshots: transitivePeerDependencies: - supports-color + fake-indexeddb@6.2.5: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -33816,16 +33814,16 @@ snapshots: flat-cache@3.2.0: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 rimraf: 3.0.2 flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.3: {} + flatted@3.4.2: {} flattie@1.1.1: {} @@ -34900,6 +34898,7 @@ snapshots: istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color + optional: true istanbul-reports@3.2.0: dependencies: @@ -36044,6 +36043,8 @@ snapshots: jpeg-js: 0.4.4 optional: true + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -36490,6 +36491,12 @@ snapshots: '@babel/types': 7.29.0 source-map-js: 1.2.1 + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + make-dir@4.0.0: dependencies: semver: 7.7.3 @@ -42180,7 +42187,7 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.0.14)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/ui@4.1.3)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 '@vitest/mocker': 4.1.2(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -42205,7 +42212,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.1 - '@vitest/ui': 4.0.14(vitest@4.1.2) + '@vitest/ui': 4.1.3(vitest@4.1.2) jsdom: 29.0.1(@noble/hashes@2.0.1) transitivePeerDependencies: - msw