diff --git a/.gitignore b/.gitignore index 528e53f21..9424004b1 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,7 @@ out # Nuxt.js build / generate output .nuxt dist +build # Gatsby files .cache/ diff --git a/examples/react/todo/package.json b/examples/react/todo/package.json index b8c287f10..5df2a8f61 100644 --- a/examples/react/todo/package.json +++ b/examples/react/todo/package.json @@ -3,24 +3,28 @@ "private": true, "version": "0.1.1", "dependencies": { + "@tanstack/db-devtools": "workspace:*", "@tanstack/electric-db-collection": "^0.1.0", "@tanstack/query-core": "^5.75.7", "@tanstack/query-db-collection": "^0.2.0", "@tanstack/react-db": "^0.1.0", + "@tanstack/react-db-devtools": "workspace:*", + "@tanstack/react-devtools": "^0.3.0", "@tanstack/react-router": "^1.125.6", + "@tanstack/react-router-devtools": "^1.130.2", "@tanstack/react-start": "^1.126.1", - "@tanstack/trailbase-db-collection": "^0.1.0", + "@tanstack/trailbase-db-collection": "^0.1.2", "cors": "^2.8.5", "drizzle-orm": "^0.40.1", "drizzle-zod": "^0.8.3", - "zod": "^4.0.17", "express": "^4.19.2", "postgres": "^3.4.7", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.1.11", "trailbase": "^0.7.1", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "zod": "^4.0.17" }, "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/examples/react/todo/src/lib/collections.ts b/examples/react/todo/src/lib/collections.ts index 5e41bac92..7ba1cd161 100644 --- a/examples/react/todo/src/lib/collections.ts +++ b/examples/react/todo/src/lib/collections.ts @@ -1,4 +1,5 @@ import { createCollection } from "@tanstack/react-db" +import { initializeDbDevtools } from "@tanstack/react-db-devtools" import { electricCollectionOptions } from "@tanstack/electric-db-collection" import { queryCollectionOptions } from "@tanstack/query-db-collection" import { trailBaseCollectionOptions } from "@tanstack/trailbase-db-collection" @@ -11,13 +12,16 @@ import type { SelectConfig, SelectTodo } from "../db/validation" // Create a query client for query collections const queryClient = new QueryClient() +// Initialize DB devtools early (idempotent - safe to call multiple times) +initializeDbDevtools() + // Create a TrailBase client. const trailBaseClient = initClient(`http://localhost:4000`) // Electric Todo Collection export const electricTodoCollection = createCollection( electricCollectionOptions({ - id: `todos`, + id: `electric-todos`, shapeOptions: { url: `http://localhost:3003/v1/shape`, params: { @@ -71,7 +75,7 @@ export const electricTodoCollection = createCollection( // Query Todo Collection export const queryTodoCollection = createCollection( queryCollectionOptions({ - id: `todos`, + id: `query-todos`, queryKey: [`todos`], refetchInterval: 3000, queryFn: async () => { @@ -130,7 +134,7 @@ type Todo = { // TrailBase Todo Collection export const trailBaseTodoCollection = createCollection( trailBaseCollectionOptions({ - id: `todos`, + id: `trailbase-todos`, getKey: (item) => item.id, schema: selectTodoSchema, recordApi: trailBaseClient.records(`todos`), @@ -149,7 +153,7 @@ export const trailBaseTodoCollection = createCollection( // Electric Config Collection export const electricConfigCollection = createCollection( electricCollectionOptions({ - id: `config`, + id: `electric-config`, shapeOptions: { url: `http://localhost:3003/v1/shape`, params: { @@ -185,7 +189,7 @@ export const electricConfigCollection = createCollection( // Query Config Collection export const queryConfigCollection = createCollection( queryCollectionOptions({ - id: `config`, + id: `query-config`, queryKey: [`config`], refetchInterval: 3000, queryFn: async () => { @@ -231,7 +235,7 @@ type Config = { // TrailBase Config Collection export const trailBaseConfigCollection = createCollection( trailBaseCollectionOptions({ - id: `config`, + id: `trailbase-config`, getKey: (item) => item.id, schema: selectConfigSchema, recordApi: trailBaseClient.records(`config`), diff --git a/examples/react/todo/src/routes/__root.tsx b/examples/react/todo/src/routes/__root.tsx index 12b4b2be4..446009dca 100644 --- a/examples/react/todo/src/routes/__root.tsx +++ b/examples/react/todo/src/routes/__root.tsx @@ -4,6 +4,10 @@ import { Scripts, createRootRoute, } from "@tanstack/react-router" +import { TanstackDevtools } from "@tanstack/react-devtools" +import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools" +import { TanStackReactDbDevtoolsPanel } from "@tanstack/react-db-devtools" +// import { TanStackReactDbDevtools } from "@tanstack/react-db-devtools" import appCss from "../styles.css?url" @@ -32,6 +36,20 @@ export const Route = createRootRoute({ component: () => ( + , + }, + { + name: "Tanstack DB", + render: , + }, + ]} + /> + {/* Alternative standalone component */} + {/* */} ), }) diff --git a/packages/db-devtools/README.md b/packages/db-devtools/README.md new file mode 100644 index 000000000..edba5e573 --- /dev/null +++ b/packages/db-devtools/README.md @@ -0,0 +1,64 @@ +# @tanstack/db-devtools + +Developer tools for TanStack DB that provide real-time insights into your collections, live queries, and transactions. + +## Installation + +```bash +npm install @tanstack/db-devtools +npm install @tanstack/react-db-devtools +``` + +## Usage + +### With TanStack Devtools (Recommended) + +```tsx +import { TanstackDevtools } from "@tanstack/react-devtools" +import { TanStackReactDbDevtoolsPanel } from "@tanstack/react-db-devtools" + +function App() { + return ( + , + }, + ]} + /> + ) +} +``` + +### Standalone Component + +```tsx +import { ReactDbDevtools } from "@tanstack/react-db-devtools" + +function App() { + return ( +
+

My App

+ +
+ ) +} +``` + +## Features + +- **Collection Monitoring**: View all active collections with real-time status updates +- **Live Query Insights**: Special handling for live queries with performance metrics +- **Transaction Tracking**: Monitor all database transactions and their states +- **Development Only**: Automatically tree-shaken in production builds + +## What You Can See + +- Collection status, size, and transaction count +- Live query performance metrics +- Transaction details and states +- Real-time data inspection +- Collection metadata and settings + +Collections automatically register themselves with the devtools when created - no additional setup required. diff --git a/packages/db-devtools/package.json b/packages/db-devtools/package.json new file mode 100644 index 000000000..f10389574 --- /dev/null +++ b/packages/db-devtools/package.json @@ -0,0 +1,103 @@ +{ + "name": "@tanstack/db-devtools", + "version": "0.0.1", + "description": "Developer tools for TanStack DB", + "author": "tanstack", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/db.git", + "directory": "packages/db-devtools" + }, + "homepage": "https://tanstack.com/db", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "browser": {}, + "exports": { + ".": { + "@tanstack/custom-condition": "./src/index.ts", + "solid": { + "development": "./dist/index.js", + "import": "./dist/index.js" + }, + "development": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src", + "!src/__tests__" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "clean": "premove ./build ./coverage ./dist-ts", + "compile": "tsc --build", + "test:eslint": "eslint ./src", + "test:types": "npm-run-all --serial test:types:*", + "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", + "test:types:tscurrent": "tsc --build", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict && attw --pack", + "lint": "eslint . --fix", + "build": "vite build", + "build:dev": "tsup --watch" + }, + "dependencies": { + "@tanstack/db": "workspace:*", + "@tanstack/react-table": "^8.13.2", + "@tanstack/react-virtual": "^3.0.0", + "@tanstack/solid-db": "workspace:*", + "@tanstack/solid-table": "^8.21.3", + "@tanstack/solid-virtual": "^3.13.12", + "@types/prismjs": "^1.26.5", + "prismjs": "^1.30.0" + }, + "devDependencies": { + "@kobalte/core": "^0.13.4", + "@solid-primitives/keyed": "^1.2.2", + "@solid-primitives/resize-observer": "^2.0.26", + "@solid-primitives/storage": "^1.3.11", + "@tanstack/match-sorter-utils": "^8.19.4", + "clsx": "^2.1.1", + "goober": "^2.1.16", + "npm-run-all2": "^5.0.0", + "solid-js": "^1.9.5", + "solid-transition-group": "^0.2.3", + "superjson": "^2.2.1", + "tsup-preset-solid": "^2.2.0", + "vite-plugin-dts": "^4.5.4", + "vite-plugin-solid": "^2.11.6" + } +} diff --git a/packages/db-devtools/src/BaseTanStackDbDevtoolsPanel.tsx b/packages/db-devtools/src/BaseTanStackDbDevtoolsPanel.tsx new file mode 100644 index 000000000..96a1ff896 --- /dev/null +++ b/packages/db-devtools/src/BaseTanStackDbDevtoolsPanel.tsx @@ -0,0 +1,429 @@ +import { clsx as cx } from "clsx" +import { Show, createEffect, createMemo, createSignal } from "solid-js" +import { useLiveQuery } from "@tanstack/solid-db" +import { + createCollection, + createLiveQueryCollection, + eq, + localOnlyCollectionOptions, +} from "@tanstack/db" +import { useDevtoolsOnClose } from "./contexts" +import { useStyles } from "./useStyles" +import { useLocalStorage } from "./useLocalStorage" +import { + CollectionDetailsPanel, + CollectionsPanel, + GenericDetailsPanel, + Logo, + TabNavigation, + TransactionsPanel, +} from "./components" +import type { Accessor, JSX } from "solid-js" +import type { DbDevtoolsRegistry } from "./types" + +export interface BaseDbDevtoolsPanelOptions { + /** + * The standard React style object used to style a component with inline styles + */ + style?: Accessor + /** + * The standard React class property used to style a component with classes + */ + className?: Accessor + /** + * A boolean variable indicating whether the panel is open or closed + */ + isOpen?: boolean + /** + * A function that toggles the open and close state of the panel + */ + setIsOpen?: (isOpen: boolean) => void + /** + * Handles the opening and closing the devtools panel + */ + handleDragStart?: (e: any) => void + /** + * The DB devtools registry instance + */ + registry: Accessor + /** + * Use this to attach the devtool's styles to specific element in the DOM. + */ + shadowDOMTarget?: ShadowRoot +} + +export const BaseTanStackDbDevtoolsPanel = + function BaseTanStackDbDevtoolsPanel({ + ...props + }: BaseDbDevtoolsPanelOptions): JSX.Element { + const { setIsOpen, handleDragStart, registry, ...panelProps } = props + + const { onCloseClick } = useDevtoolsOnClose() + const styles = useStyles() + const { className, style, ...otherPanelProps } = panelProps + + // Simple local state - no navigation store complexity + const [selectedView, setSelectedView] = createSignal< + `collections` | `transactions` + >(`collections`) + const [activeCollectionId, setActiveCollectionId] = useLocalStorage( + `tanstackDbDevtoolsActiveCollectionId`, + `` + ) + const [selectedTransaction, setSelectedTransaction] = createSignal< + string | null + >(null) + + // Reactive views derived from live queries + // We'll compute these as memos that create fresh arrays so Solid tracks per-update + + // Use useLiveQuery for reactive data from devtools collections + // Wrap in try-catch to prevent crashes if collections are not properly initialized + let collectionsQuery: any + let transactionsQuery: any + let collectionsLQ: any + let transactionsLQ: any + // Note: selectedCollectionLQ is currently unused, kept for potential future detail views + + let transactionsForCollectionLQ: any + let selectedTransactionLQ: any + // Local-only empty placeholders for early-render fallbacks + let emptyCollectionsCol: any + let emptyTransactionsCol: any + + try { + // Ensure empty placeholder collections exist for any fallback paths + if (!emptyCollectionsCol) { + emptyCollectionsCol = createCollection( + localOnlyCollectionOptions({ + id: `__devtools_empty_collections`, + __devtoolsInternal: true, + getKey: (entry: any) => entry.id ?? Math.random().toString(36), + }) + ) + } + if (!emptyTransactionsCol) { + emptyTransactionsCol = createCollection( + localOnlyCollectionOptions({ + id: `__devtools_empty_transactions`, + __devtoolsInternal: true, + getKey: (entry: any) => entry.id ?? Math.random().toString(36), + }) + ) + } + + // Live collections query + collectionsQuery = useLiveQuery(() => { + const reg = registry() + if (!collectionsLQ) { + collectionsLQ = createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_collections`, + startSync: true, + gcTime: 5000, + query: (q: any) => + q + .from({ collections: reg.store.collections }) + .select(({ collections }: any) => ({ + id: collections.id, + type: collections.metadata.type, + status: collections.metadata.status, + size: collections.metadata.size, + hasTransactions: collections.metadata.hasTransactions, + transactionCount: collections.metadata.transactionCount, + createdAt: collections.metadata.createdAt, + lastUpdated: collections.metadata.lastUpdated, + gcTime: collections.metadata.gcTime, + timings: collections.metadata.timings, + })), + } as any) + } + return collectionsLQ + }) + + transactionsQuery = useLiveQuery(() => { + const reg = registry() + if (!transactionsLQ) { + transactionsLQ = createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_transactions`, + startSync: true, + gcTime: 5000, + query: (q: any) => + q + .from({ transactions: reg.store.transactions }) + .orderBy( + ({ transactions }: any) => transactions.createdAt, + `desc` + ) + .select(({ transactions }: any) => ({ + id: transactions.id, + collectionId: transactions.collectionId, + state: transactions.state, + mutations: transactions.mutations, + createdAt: transactions.createdAt, + updatedAt: transactions.updatedAt, + isPersisted: transactions.isPersisted, + })), + } as any) + } + return transactionsLQ + }) + + // Selected collection live query not required for current UI; rely on collectionsArray + + // Transactions filtered by selected collection id + transactionsForCollectionLQ = useLiveQuery(() => { + const reg = registry() + const id = activeCollectionId() + if (!id) { + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_transactions_for_collection_empty`, + startSync: true, + gcTime: 3000, + query: (q: any) => q.from({ transactions: emptyTransactionsCol }), + } as any) + } + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_transactions_for_collection_${id}`, + startSync: true, + gcTime: 5000, + query: (q: any) => + q + .from({ transactions: reg.store.transactions }) + .where(({ transactions }: any) => + eq(transactions.collectionId, id) + ) + .orderBy( + ({ transactions }: any) => transactions.createdAt, + `desc` + ) + .select(({ transactions }: any) => ({ + id: transactions.id, + collectionId: transactions.collectionId, + state: transactions.state, + mutations: transactions.mutations, + createdAt: transactions.createdAt, + updatedAt: transactions.updatedAt, + isPersisted: transactions.isPersisted, + })), + } as any) + }) + + // Selected transaction via live query with where by id + selectedTransactionLQ = useLiveQuery(() => { + const reg = registry() + const id = selectedTransaction() + if (!id) { + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_selected_transaction_empty`, + startSync: true, + gcTime: 3000, + query: (q: any) => q.from({ transactions: emptyTransactionsCol }), + } as any) + } + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_selected_transaction_${id}`, + startSync: true, + gcTime: 5000, + query: (q: any) => + q + .from({ transactions: reg.store.transactions }) + .where(({ transactions }: any) => eq(transactions.id, id)) + .select(({ transactions }: any) => ({ + id: transactions.id, + collectionId: transactions.collectionId, + state: transactions.state, + mutations: transactions.mutations, + createdAt: transactions.createdAt, + updatedAt: transactions.updatedAt, + isPersisted: transactions.isPersisted, + })), + } as any) + }) + + // No explicit effects needed; we'll read from queries directly in memos below + } catch (error) { + console.error(`Error initializing useLiveQuery:`, error) + collectionsQuery = { data: [] as Array } + transactionsQuery = { data: [] as Array } + } + + // Reactive arrays derived from live queries (copy to trigger tracking) + const collectionsArray = createMemo(() => + Array.isArray(collectionsQuery.data) + ? (collectionsQuery.data as Array).slice() + : [] + ) + const transactions = createMemo(() => { + const raw = Array.isArray(transactionsQuery.data) + ? (transactionsQuery.data as Array).slice() + : [] + return raw.map((entry: any) => ({ + id: entry.id, + collectionId: entry.collectionId, + state: entry.state, + mutations: entry.mutations, + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + isPersisted: entry.isPersisted, + })) + }) + + const transactionsForActiveCollection = createMemo(() => { + const raw = Array.isArray(transactionsForCollectionLQ?.data) + ? (transactionsForCollectionLQ.data as Array).slice() + : [] + return raw.map((entry: any) => ({ + id: entry.id, + collectionId: entry.collectionId, + state: entry.state, + mutations: entry.mutations, + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + isPersisted: entry.isPersisted, + })) + }) + + // Computed values + const activeCollection = createMemo(() => { + const data = collectionsArray() + const id = activeCollectionId() + if (!Array.isArray(data)) return undefined + return data.find((c: any) => c.id === id) + }) + + const activeTransaction = createMemo(() => { + const data = selectedTransactionLQ?.data as Array + if (!Array.isArray(data)) return undefined + return data[0] + }) + + // Use reactive data for immediate updates + createEffect(() => { + const newCollections = collectionsArray() + if (!Array.isArray(newCollections)) return + if (activeCollectionId() === `` && newCollections.length > 0) { + setActiveCollectionId(newCollections[0]?.id ?? ``) + } + }) + + // Note: Transactions are handled reactively through useLiveQuery + + return ( +
+ {handleDragStart ? ( +
+ ) : null} + + + +
+
+
+ + collectionsArray().length} + transactionsCount={() => { + try { + return transactions().length + } catch (error) { + console.error(`Error getting transactions count:`, error) + return 0 + } + }} + onSelectView={setSelectedView} + /> +
+
+
+ {/* Content based on selected view */} +
+ + setActiveCollectionId(c.id)} + /> + + + + { + try { + const id = activeCollectionId() + if (!id) return transactions() + return transactionsForActiveCollection() + } catch (error) { + console.error( + `Error getting transactions for panel:`, + error + ) + return [] + } + }} + selectedTransaction={selectedTransaction} + onSelectTransaction={setSelectedTransaction} + /> + +
+
+
+ +
+ + + + + + +
+
+ ) + } + +export default BaseTanStackDbDevtoolsPanel diff --git a/packages/db-devtools/src/FloatingTanStackDbDevtools.tsx b/packages/db-devtools/src/FloatingTanStackDbDevtools.tsx new file mode 100644 index 000000000..7dc489d8a --- /dev/null +++ b/packages/db-devtools/src/FloatingTanStackDbDevtools.tsx @@ -0,0 +1,274 @@ +import { clsx as cx } from "clsx" +import { createEffect, createMemo, createSignal } from "solid-js" +import { Dynamic } from "solid-js/web" +import { DevtoolsOnCloseContext } from "./contexts" +import { BaseTanStackDbDevtoolsPanel } from "./BaseTanStackDbDevtoolsPanel" +import { useLocalStorage } from "./useLocalStorage" +import { TanStackLogo } from "./logo" +import { useStyles } from "./useStyles" +import type { Accessor, JSX } from "solid-js" +import type { DbDevtoolsRegistry } from "./types" + +export interface FloatingDbDevtoolsOptions { + /** + * Set this true if you want the dev tools to default to being open + */ + initialIsOpen?: boolean + /** + * Use this to add props to the panel. For example, you can add class, style (merge and override default style), etc. + */ + panelProps?: any & { + ref?: any + } + /** + * Use this to add props to the close button. For example, you can add class, style (merge and override default style), onClick (extend default handler), etc. + */ + closeButtonProps?: any & { + ref?: any + } + /** + * Use this to add props to the toggle button. For example, you can add class, style (merge and override default style), onClick (extend default handler), etc. + */ + toggleButtonProps?: any & { + ref?: any + } + /** + * The position of the TanStack DB logo to open and close the devtools panel. + * Defaults to 'bottom-left'. + */ + position?: `top-left` | `top-right` | `bottom-left` | `bottom-right` + /** + * Use this to render the devtools inside a different type of container element for a11y purposes. + * Any string which corresponds to a valid intrinsic JSX element is allowed. + * Defaults to 'footer'. + */ + containerElement?: string | any + /** + * The DB devtools registry instance + */ + registry: Accessor + /** + * Use this to attach the devtool's styles to specific element in the DOM. + */ + shadowDOMTarget?: ShadowRoot +} + +export function FloatingTanStackDbDevtools({ + initialIsOpen, + panelProps = {}, + closeButtonProps = {}, + toggleButtonProps = {}, + position = `bottom-left`, + containerElement: Container = `footer`, + registry, + shadowDOMTarget, +}: FloatingDbDevtoolsOptions): JSX.Element | null { + const [rootEl, setRootEl] = createSignal() + + // eslint-disable-next-line prefer-const + let panelRef: HTMLDivElement | undefined = undefined + + const [isOpen, setIsOpen] = useLocalStorage( + `tanstackDbDevtoolsOpen`, + initialIsOpen + ) + + const [devtoolsHeight, setDevtoolsHeight] = useLocalStorage( + `tanstackDbDevtoolsHeight`, + null + ) + + const [isResolvedOpen, setIsResolvedOpen] = createSignal(false) + const [isResizing, setIsResizing] = createSignal(false) + const styles = useStyles() + + const handleDragStart = ( + panelElement: HTMLDivElement | undefined, + startEvent: any + ) => { + if (startEvent.button !== 0) return // Only allow left click for drag + + setIsResizing(true) + + const dragInfo = { + originalHeight: panelElement?.getBoundingClientRect().height || 0, + pageY: startEvent.pageY, + } + + const run = (moveEvent: MouseEvent) => { + const delta = dragInfo.pageY - moveEvent.pageY + const newHeight = dragInfo.originalHeight + delta + + setDevtoolsHeight(newHeight) + + if (newHeight < 70) { + setIsOpen(false) + } else { + setIsOpen(true) + } + } + + const unsub = () => { + setIsResizing(false) + document.removeEventListener(`mousemove`, run) + document.removeEventListener(`mouseUp`, unsub) + } + + document.addEventListener(`mousemove`, run) + document.addEventListener(`mouseup`, unsub) + } + + createEffect(() => { + setIsResolvedOpen(isOpen()) + }) + + createEffect(() => { + if (isResolvedOpen()) { + const previousValue = rootEl()?.parentElement?.style.paddingBottom + + const run = () => { + const containerHeight = panelRef!.getBoundingClientRect().height + if (rootEl()?.parentElement) { + setRootEl((prev) => { + if (prev?.parentElement) { + prev.parentElement.style.paddingBottom = `${containerHeight}px` + } + return prev + }) + } + } + + run() + + if (typeof window !== `undefined`) { + window.addEventListener(`resize`, run) + + return () => { + window.removeEventListener(`resize`, run) + if (rootEl()?.parentElement && typeof previousValue === `string`) { + setRootEl((prev) => { + prev!.parentElement!.style.paddingBottom = previousValue + return prev + }) + } + } + } + } else { + // Reset padding when devtools are closed + if (rootEl()?.parentElement) { + setRootEl((prev) => { + if (prev?.parentElement) { + prev.parentElement.removeAttribute(`style`) + } + return prev + }) + } + } + return + }) + + createEffect(() => { + if (rootEl()) { + const el = rootEl() + const fontSize = getComputedStyle(el!).fontSize + el?.style.setProperty(`--tsdb-font-size`, fontSize) + } + }) + + const { style: panelStyle = {}, ...otherPanelProps } = panelProps as { + style?: Record + } + + const { onClick: onCloseClick } = closeButtonProps + + const { + onClick: onToggleClick, + class: toggleButtonClassName, + ...otherToggleButtonProps + } = toggleButtonProps + + // Always render when called (we're already in a client-side React environment) + // The original isMounted() check was preventing rendering when embedded in React + // if (!isMounted()) return null + + const resolvedHeight = createMemo(() => { + const h = devtoolsHeight() + return typeof h === `number` ? h : 500 + }) + + const basePanelClass = createMemo(() => { + return cx( + styles().devtoolsPanelContainer, + styles().devtoolsPanelContainerVisibility(!!isOpen()), + styles().devtoolsPanelContainerResizing(isResizing), + styles().devtoolsPanelContainerAnimation( + isResolvedOpen(), + resolvedHeight() + 16 + ) + ) + }) + + const basePanelStyle = createMemo(() => { + return { + height: `${resolvedHeight()}px`, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ...(panelStyle || {}), + } + }) + + const buttonStyle = createMemo(() => { + return cx( + styles().mainCloseBtn, + styles().mainCloseBtnPosition(position), + styles().mainCloseBtnAnimation(!!isOpen()), + toggleButtonClassName + ) + }) + + return ( + + {}, + }} + > + handleDragStart(panelRef, e)} + shadowDOMTarget={shadowDOMTarget} + /> + + + + + ) +} + +export default FloatingTanStackDbDevtools diff --git a/packages/db-devtools/src/TanstackDbDevtools.tsx b/packages/db-devtools/src/TanstackDbDevtools.tsx new file mode 100644 index 000000000..8bd47ed29 --- /dev/null +++ b/packages/db-devtools/src/TanstackDbDevtools.tsx @@ -0,0 +1,131 @@ +/** @jsxImportSource solid-js */ +import { render } from "solid-js/web" +import { createSignal } from "solid-js" +import { initializeDevtoolsRegistry } from "./registry" +import { FloatingTanStackDbDevtools } from "./FloatingTanStackDbDevtools" +import type { DbDevtoolsConfig, DbDevtoolsRegistry } from "./types" +import type { Signal } from "solid-js" + +export interface TanstackDbDevtoolsConfig extends DbDevtoolsConfig { + styleNonce?: string + shadowDOMTarget?: ShadowRoot +} + +class TanstackDbDevtools { + #registry: DbDevtoolsRegistry + #isMounted = false + #shadowDOMTarget?: ShadowRoot + #initialIsOpen: Signal + #position: Signal + #panelProps: Signal | undefined> + #toggleButtonProps: Signal | undefined> + #closeButtonProps: Signal | undefined> + #storageKey: Signal + #panelState: Signal + #onPanelStateChange: Signal<((isOpen: boolean) => void) | undefined> + #dispose?: () => void + + constructor(config: TanstackDbDevtoolsConfig) { + const { + initialIsOpen, + position, + panelProps, + toggleButtonProps, + closeButtonProps, + storageKey, + panelState, + onPanelStateChange, + styleNonce: _styleNonce, + shadowDOMTarget, + } = config + + // Only initialize on the client side + if (typeof window === `undefined`) { + throw new Error(`TanstackDbDevtools cannot be instantiated during SSR`) + } + + this.#registry = initializeDevtoolsRegistry() + this.#shadowDOMTarget = shadowDOMTarget + this.#initialIsOpen = createSignal(initialIsOpen) + this.#position = createSignal(position) + this.#panelProps = createSignal(panelProps) + this.#toggleButtonProps = createSignal(toggleButtonProps) + this.#closeButtonProps = createSignal(closeButtonProps) + this.#storageKey = createSignal(storageKey) + this.#panelState = createSignal(panelState) + this.#onPanelStateChange = createSignal(onPanelStateChange) + } + + setInitialIsOpen(isOpen: boolean) { + this.#initialIsOpen[1](isOpen) + } + + setPosition(position: DbDevtoolsConfig[`position`]) { + this.#position[1](position) + } + + setPanelProps(props: Record) { + this.#panelProps[1](props) + } + + setToggleButtonProps(props: Record) { + this.#toggleButtonProps[1](props) + } + + setCloseButtonProps(props: Record) { + this.#closeButtonProps[1](props) + } + + setStorageKey(key: string) { + this.#storageKey[1](key) + } + + setPanelState(state: DbDevtoolsConfig[`panelState`]) { + this.#panelState[1](state) + } + + setOnPanelStateChange(callback: (isOpen: boolean) => void) { + this.#onPanelStateChange[1](() => callback) + } + + mount(el: T) { + if (this.#isMounted) { + throw new Error(`DB Devtools is already mounted`) + } + + const getValidPosition = (pos: DbDevtoolsConfig[`position`]) => { + if (pos === `relative` || pos === undefined) { + return `bottom-left` as const + } + return pos + } + + const dispose = render( + () => ( + this.#registry} + shadowDOMTarget={this.#shadowDOMTarget} + /> + ), + el + ) + + this.#isMounted = true + this.#dispose = dispose + } + + unmount() { + if (!this.#isMounted) { + throw new Error(`DB Devtools is not mounted`) + } + this.#dispose?.() + this.#isMounted = false + } +} + +export { TanstackDbDevtools } diff --git a/packages/db-devtools/src/components/CollectionDataView.tsx b/packages/db-devtools/src/components/CollectionDataView.tsx new file mode 100644 index 000000000..ed9f8fa9a --- /dev/null +++ b/packages/db-devtools/src/components/CollectionDataView.tsx @@ -0,0 +1,72 @@ +import { Show, createMemo } from "solid-js" +import { useLiveQuery } from "@tanstack/solid-db" +import { createLiveQueryCollection } from "@tanstack/db" +import { useStyles } from "../useStyles" +import { getDevtoolsRegistry } from "../devtools" +import { DataTable } from "./DataTable" +import type { CollectionMetadata } from "../types" + +interface CollectionDataViewProps { + collectionMetadata: CollectionMetadata +} + +export function CollectionDataView(props: CollectionDataViewProps) { + const styles = useStyles() + + // Create a live query to get all data from the collection + const dataQuery = useLiveQuery(() => { + // Get the collection from the registry + const registry = getDevtoolsRegistry() + if (!registry) { + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_collection_data_empty`, + startSync: true, + gcTime: 3000, + query: (q: any) => q.from({ empty: [] }), + } as any) + } + + const collection = registry.getCollection(props.collectionMetadata.id) + if (!collection) { + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_collection_data_empty`, + startSync: true, + gcTime: 3000, + query: (q: any) => q.from({ empty: [] }), + } as any) + } + + console.log({ collection }) + + // Query the collection directly + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_collection_data_${props.collectionMetadata.id}`, + startSync: true, + gcTime: 5000, + query: (q: any) => q.from({ data: collection }), + } as any) + }) + + console.log({ dataQuery }) + + const data = createMemo(() => { + const raw = Array.isArray(dataQuery.data) + ? (dataQuery.data as Array).slice() + : [] + return raw + }) + + return ( +
+ 0} + fallback={
No data available
} + > + +
+
+ ) +} diff --git a/packages/db-devtools/src/components/CollectionDetailsPanel.tsx b/packages/db-devtools/src/components/CollectionDetailsPanel.tsx new file mode 100644 index 000000000..4f006bae2 --- /dev/null +++ b/packages/db-devtools/src/components/CollectionDetailsPanel.tsx @@ -0,0 +1,393 @@ +import { + Show, + createEffect, + createMemo, + createSignal, + onCleanup, +} from "solid-js" +import { useLiveQuery } from "@tanstack/solid-db" +import { + createCollection, + createLiveQueryCollection, + eq, + localOnlyCollectionOptions, +} from "@tanstack/db" +import { clsx as cx } from "clsx" +import { useStyles } from "../useStyles" +import { getDevtoolsRegistry } from "../devtools" +import { convertQueryIRToString } from "../utils/queryToString" +import { Explorer } from "./Explorer" +import { TransactionsPanel } from "./TransactionsPanel" +import { GenericDetailsPanel } from "./DetailsPanel" +import { SyntaxHighlighter } from "./SyntaxHighlighter" +import { CollectionDataView } from "./CollectionDataView" +import type { CollectionMetadata } from "../types" +import type { Accessor } from "solid-js" + +export interface CollectionDetailsPanelProps { + activeCollection: Accessor +} + +type CollectionTab = + | `summary` + | `config` + | `state` + | `transactions` + | `data` + | `query-ir` + +export function CollectionDetailsPanel({ + activeCollection, +}: CollectionDetailsPanelProps) { + const styles = useStyles() + const [selectedTab, setSelectedTab] = createSignal(`summary`) + + // Reset selected tab if it's not available for the current collection + createEffect(() => { + const currentCollection = collection() + const availableTabs = tabs() + const currentTab = selectedTab() + + if ( + currentCollection && + !availableTabs.find((tab) => tab.id === currentTab) + ) { + setSelectedTab(`summary`) + } + }) + const [selectedTransaction, setSelectedTransaction] = createSignal< + string | null + >(null) + const registry = getDevtoolsRegistry() + + const collection = createMemo(() => { + const metadata = activeCollection() + if (!metadata || !registry) return null + + // Get the actual collection instance + const collectionInstance = registry.getCollection(metadata.id) + return { metadata, instance: collectionInstance } + }) + + // Bump a reactive tick when the underlying collection emits changes + const [stateVersion, setStateVersion] = createSignal(0) + createEffect(() => { + const current = collection() + if (!current || !current.instance) return + const unsubscribe = current.instance.subscribeChanges(() => { + // Any change to the collection should retrigger the state view + setStateVersion((v) => v + 1) + }) + onCleanup(() => { + unsubscribe() + }) + }) + + // Live query for transactions filtered to the active collection + let emptyTransactionsCol: any + const transactionsForCollectionQuery: any = useLiveQuery(() => { + const metadata = activeCollection() + if (!emptyTransactionsCol) { + emptyTransactionsCol = createCollection( + localOnlyCollectionOptions({ + id: `__devtools_empty_transactions_for_collection`, + __devtoolsInternal: true, + getKey: (entry: any) => entry.id ?? Math.random().toString(36), + }) + ) + } + + if (!registry || !metadata) { + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_transactions_for_collection_empty_local`, + startSync: true, + gcTime: 3000, + query: (q: any) => q.from({ transactions: emptyTransactionsCol }), + } as any) + } + + return createLiveQueryCollection({ + __devtoolsInternal: true, + id: `__devtools_view_transactions_for_collection_${metadata.id}`, + startSync: true, + gcTime: 5000, + query: (q: any) => + q + .from({ transactions: registry.store.transactions }) + .where(({ transactions }: any) => + eq(transactions.collectionId, metadata.id) + ) + .orderBy(({ transactions }: any) => transactions.createdAt, `desc`) + .select(({ transactions }: any) => ({ + id: transactions.id, + collectionId: transactions.collectionId, + state: transactions.state, + mutations: transactions.mutations, + createdAt: transactions.createdAt, + updatedAt: transactions.updatedAt, + isPersisted: transactions.isPersisted, + })), + } as any) + }) + + const collectionTransactions = createMemo(() => { + const raw = Array.isArray(transactionsForCollectionQuery.data) + ? (transactionsForCollectionQuery.data as Array).slice() + : [] + return raw + }) + + const activeTransaction = createMemo(() => { + const txs = collectionTransactions() + const selectedId = selectedTransaction() + return txs.find((t) => t.id === selectedId) + }) + + const tabs = createMemo(() => { + const currentCollection = collection() + const baseTabs: Array<{ id: CollectionTab; label: string }> = [ + { id: `summary`, label: `Summary` }, + { id: `config`, label: `Config` }, + { id: `state`, label: `State` }, + { id: `transactions`, label: `Transactions` }, + { id: `data`, label: `Data` }, + ] + + // Only add Query IR tab for live query collections + if (currentCollection?.metadata.type === `live-query`) { + baseTabs.push({ id: `query-ir`, label: `Query IR` }) + } + + return baseTabs + }) + + const renderTabContent = () => { + const currentCollection = collection() + if (!currentCollection) return null + + const { metadata, instance } = currentCollection + + switch (selectedTab()) { + case `summary`: { + return ( +
+ {/* Show query string for live query collections */} + {metadata.type === `live-query` && + (() => { + const queryIR = instance?.config.__devtoolsQueryIR + if (queryIR) { + const queryString = convertQueryIRToString( + queryIR.unoptimized + ) + return ( + + ) + } + return null + })()} + + metadata} + defaultExpanded={{}} + /> +
+ ) + } + + case `config`: { + return instance ? ( + { + // Filter out devtools internal properties + const config = { ...instance.config } + delete config.__devtoolsInternal + delete config.__devtoolsQueryIR + return config + }} + defaultExpanded={{}} + /> + ) : ( +
+ Collection instance not available +
+ ) + } + + case `state`: { + if (!instance) { + return ( +
+ Collection instance not available +
+ ) + } + + // Depend on stateVersion so updates re-render this block + stateVersion() + const stateData = { + syncedData: instance.syncedData, + optimisticUpserts: instance.optimisticUpserts, + optimisticDeletes: instance.optimisticDeletes, + } + + return ( + stateData} + defaultExpanded={{}} + /> + ) + } + + case `transactions`: { + const transactions = collectionTransactions() + return ( +
+
+ transactions} + selectedTransaction={selectedTransaction} + onSelectTransaction={setSelectedTransaction} + /> +
+
+ `transactions` as const} + activeCollection={() => undefined} + activeTransaction={activeTransaction} + isSubPanel={true} + /> +
+
+ ) + } + + case `data`: { + const collectionInstance = collection() + if (!collectionInstance?.metadata) { + return ( +
+ Collection metadata not available +
+ ) + } + + return ( + + ) + } + + case `query-ir`: { + const collectionInstance = collection() + if (!collectionInstance?.instance) { + return ( +
+ Collection instance not available +
+ ) + } + + // Only show for live query collections + if (collectionInstance.metadata.type !== `live-query`) { + return ( +
+ Query IR is only available for live query collections +
+ ) + } + + const queryIR = collectionInstance.instance.config.__devtoolsQueryIR + if (!queryIR) { + return ( +
+ Query IR not available for this collection +
+ ) + } + + return ( +
+ queryIR.unoptimized} + defaultExpanded={{}} + /> + queryIR.optimized} + defaultExpanded={{}} + /> +
+ ) + } + + default: + return null + } + } + + return ( + +
+ Select a collection to view details +
+ + } + > + {(collectionMetadata) => ( +
+
+
{collectionMetadata().id}
+
+ {tabs().map((tab) => ( + + ))} +
+
+ + {/* Tab Content */} +
+ {renderTabContent()} +
+
+ )} +
+ ) +} diff --git a/packages/db-devtools/src/components/CollectionItem.tsx b/packages/db-devtools/src/components/CollectionItem.tsx new file mode 100644 index 000000000..a3d7fd148 --- /dev/null +++ b/packages/db-devtools/src/components/CollectionItem.tsx @@ -0,0 +1,32 @@ +import { clsx as cx } from "clsx" +import { useStyles } from "../useStyles" +import { CollectionStats } from "./CollectionStats" +import type { Accessor } from "solid-js" +import type { CollectionMetadata } from "../types" + +interface CollectionItemProps { + collection: CollectionMetadata + isActive: Accessor + onSelect: (collection: CollectionMetadata) => void +} + +export function CollectionItem({ + collection, + isActive, + onSelect, +}: CollectionItemProps) { + const styles = useStyles() + + return ( +
onSelect(collection)} + > +
{collection.id}
+ +
+ ) +} diff --git a/packages/db-devtools/src/components/CollectionStats.tsx b/packages/db-devtools/src/components/CollectionStats.tsx new file mode 100644 index 000000000..eb1abcc83 --- /dev/null +++ b/packages/db-devtools/src/components/CollectionStats.tsx @@ -0,0 +1,52 @@ +import { clsx as cx } from "clsx" +import { useStyles } from "../useStyles" +import { formatTime } from "../utils/formatTime" +import type { CollectionMetadata } from "../types" + +interface CollectionStatsProps { + collection: CollectionMetadata +} + +export function CollectionStats({ collection }: CollectionStatsProps) { + const styles = useStyles() + + if (collection.type === `collection`) { + // Standard collection stats + return ( +
+
{collection.size}
+
/
+
{collection.transactionCount}
+
/
+
{formatTime(collection.gcTime || 0)}
+
/
+
+ {collection.status} +
+
+ ) + } else { + // Live query collection stats + return ( +
+
{collection.size}
+
/
+
{formatTime(collection.gcTime || 0)}
+
/
+
+ {collection.status} +
+
+ ) + } +} diff --git a/packages/db-devtools/src/components/CollectionsPanel.tsx b/packages/db-devtools/src/components/CollectionsPanel.tsx new file mode 100644 index 000000000..fa4bd0f53 --- /dev/null +++ b/packages/db-devtools/src/components/CollectionsPanel.tsx @@ -0,0 +1,136 @@ +import { For, Show, createMemo } from "solid-js" +import { useStyles } from "../useStyles" +import { multiSortBy } from "../utils" +import { CollectionItem } from "./CollectionItem" +import type { Accessor } from "solid-js" +import type { CollectionMetadata } from "../types" + +interface CollectionsPanelProps { + collections: Accessor> + activeCollectionId: Accessor + onSelectCollection: (collection: CollectionMetadata) => void +} + +export function CollectionsPanel({ + collections, + activeCollectionId, + onSelectCollection, +}: CollectionsPanelProps) { + const styles = useStyles() + + const sortedCollections = createMemo(() => { + return multiSortBy(collections(), [ + (c) => (c.status === `error` ? 0 : 1), // Errors first + (c) => c.id.toLowerCase(), // Then alphabetically by ID + ]) + }) + + // Group collections by type + const groupedCollections = createMemo(() => { + const groups: Record> = {} + + sortedCollections().forEach((collection) => { + const type = collection.type + if (!groups[type]) { + groups[type] = [] + } + const targetGroup = groups[type] + targetGroup.push(collection) + }) + + // Sort collections within each group alphabetically + Object.keys(groups).forEach((key) => { + const group = groups[key] + if (group) { + group.sort((a, b) => + a.id.toLowerCase().localeCompare(b.id.toLowerCase()) + ) + } + }) + + return groups + }) + + const getGroupDisplayName = (type: string): string => { + switch (type) { + case `live-query`: + return `Live Queries` + case `electric`: + return `Electric Collections` + case `query`: + return `Query Collections` + case `local-only`: + return `Local-Only Collections` + case `local-storage`: + return `Local Storage Collections` + case `generic`: + return `Generic Collections` + default: + return `${type.charAt(0).toUpperCase() + type.slice(1)} Collections` + } + } + + const getGroupStats = (type: string): Array => { + switch (type) { + case `live-query`: + return [`Items`, `/`, `GC`, `/`, `Status`] + default: + return [`Items`, `/`, `GC`, `/`, `Status`] + } + } + + // Get sorted group entries with live-query first, then alphabetical + const sortedGroupEntries = createMemo(() => { + const entries = Object.entries(groupedCollections()) + return entries.sort(([a], [b]) => { + // Live-query always comes first + if (a === `live-query`) return -1 + if (b === `live-query`) return 1 + // Others are sorted alphabetically + return a.localeCompare(b) + }) + }) + + return ( +
+
+ 0} + fallback={ +
+ No collections found +
+ } + > + + {([type, groupCollections]) => ( + 0}> +
+
+
+ {getGroupDisplayName(type)} ({groupCollections.length}) +
+
+ + {(stat) => {stat}} + +
+
+ + {(collection) => ( + collection.id === activeCollectionId()} + onSelect={onSelectCollection} + /> + )} + +
+
+ )} +
+
+
+
+ ) +} diff --git a/packages/db-devtools/src/components/DataTable.tsx b/packages/db-devtools/src/components/DataTable.tsx new file mode 100644 index 000000000..aa36f483a --- /dev/null +++ b/packages/db-devtools/src/components/DataTable.tsx @@ -0,0 +1,124 @@ +import { For, createEffect, createMemo } from "solid-js" +import { createSolidTable, getCoreRowModel } from "@tanstack/solid-table" +// import { createVirtualizer } from "@tanstack/solid-virtual" +import { useStyles } from "../useStyles" +import { + extractKeysFromData, + formatValueForTable, + getFullValue, +} from "../utils/dataFormatting" +import type { ColumnDef } from "@tanstack/solid-table" + +interface DataTableProps { + data: Array + class?: string +} + +export function DataTable(props: DataTableProps) { + const styles = useStyles() + let tableContainerRef: HTMLDivElement | undefined + let headerRef: HTMLDivElement | undefined + + // Extract columns from data + const columns = createMemo(() => { + const keys = extractKeysFromData(props.data) + return keys.map((key) => ({ + id: key, + header: key, + accessorKey: key, + cell: ({ getValue }: any) => { + const value = getValue() + return ( +
+ {formatValueForTable(value)} +
+ ) + }, + })) as Array> + }) + + // Create table instance + const table = createMemo(() => { + return createSolidTable({ + data: props.data, + columns: columns(), + getCoreRowModel: getCoreRowModel(), + }) + }) + + // Temporarily render without row virtualization for correctness. + // We'll re-enable once stable across proxies and Solid integration. + // let rowVirtualizer: any = null + // createEffect(() => { ... }) + + // Column model (no virtualization yet for columns to simplify) + const leafColumns = createMemo(() => table().getAllColumns()) + + // Update virtualizers when data changes + createEffect(() => { + props.data + }) + + // Keep header horizontal scroll in sync with body + createEffect(() => { + if (!tableContainerRef || !headerRef) return + const onScroll = () => { + headerRef.scrollLeft = tableContainerRef.scrollLeft + } + tableContainerRef.addEventListener(`scroll`, onScroll) + return () => tableContainerRef.removeEventListener(`scroll`, onScroll) + }) + + return ( +
+ {/* Table Header */} +
+
+ + {(column) => ( +
+ {typeof column.columnDef.header === `string` + ? column.columnDef.header + : column.id} +
+ )} +
+
+
+ + {/* Table Body */} +
+
+ + {(row) => ( +
+ + {(column) => { + const value = row.getValue(column.id) + return ( +
+ {formatValueForTable(value)} +
+ ) + }} +
+
+ )} +
+
+
+
+ ) +} diff --git a/packages/db-devtools/src/components/DetailsPanel.tsx b/packages/db-devtools/src/components/DetailsPanel.tsx new file mode 100644 index 000000000..7feda39ec --- /dev/null +++ b/packages/db-devtools/src/components/DetailsPanel.tsx @@ -0,0 +1,168 @@ +import { Show, createMemo } from "solid-js" +import { useStyles } from "../useStyles" +import { Explorer } from "./Explorer" +import type { CollectionMetadata, TransactionDetails } from "../types" +import type { Accessor } from "solid-js" + +export interface DetailsPanelProps { + selectedView: Accessor<`collections` | `transactions`> + activeCollection: Accessor + activeTransaction: Accessor + isSubPanel?: boolean +} + +export function DetailsPanel({ + selectedView, + activeCollection, + activeTransaction: _activeTransaction, +}: DetailsPanelProps) { + const styles = useStyles() + + return ( + + +
+ Select a collection to view details +
+ + } + > + {(collection) => ( +
+
{collection().id}
+
+ collection()} + defaultExpanded={{}} + /> +
+
+ )} +
+
+ ) +} + +export function TransactionDetailsPanel({ + selectedView, + activeTransaction: _activeTransaction, +}: DetailsPanelProps) { + const styles = useStyles() + + return ( + + +
+ Select a transaction to view details +
+ + } + > + {(transaction) => ( +
+
+ Transaction {transaction().id} +
+
+ transaction()} + defaultExpanded={{}} + /> +
+
+ )} +
+
+ ) +} + +export function GenericDetailsPanel({ + selectedView, + activeCollection, + activeTransaction, + isSubPanel = false, +}: DetailsPanelProps) { + const styles = useStyles() + + // Create stable value functions using createMemo to prevent unnecessary re-renders + const collectionValue = createMemo(() => activeCollection()) + const transactionValue = createMemo(() => activeTransaction()) + + return ( + <> + + +
+ Select a collection to view details +
+ + } + > + {(collection) => ( +
+
{collection().id}
+
+ +
+
+ )} +
+
+ + + +
+ Select a transaction to view details +
+ + } + > + {(transaction) => ( +
+
+ Transaction {transaction().id} +
+
+ +
+
+ )} +
+
+ + ) +} diff --git a/packages/db-devtools/src/components/Explorer.tsx b/packages/db-devtools/src/components/Explorer.tsx new file mode 100644 index 000000000..e1a3b97ad --- /dev/null +++ b/packages/db-devtools/src/components/Explorer.tsx @@ -0,0 +1,361 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import { clsx as cx } from "clsx" +import * as goober from "goober" +import { createMemo, createSignal, useContext } from "solid-js" +import { tokens } from "../tokens" +import { ShadowDomTargetContext } from "../contexts" +import type { Accessor, JSX } from "solid-js" + +type ExpanderProps = { + expanded: boolean + style?: JSX.CSSProperties +} + +export const Expander = ({ expanded, style: _style = {} }: ExpanderProps) => { + const styles = useStyles() + return ( + + + + + + ) +} + +type Entry = { + label: string +} + +type RendererProps = { + handleEntry: HandleEntryFn + label?: JSX.Element + value: Accessor + subEntries: Array + subEntryPages: Array> + type: string + expanded: Accessor + toggleExpanded: () => void + pageSize: number + filterSubEntries?: (subEntries: Array) => Array +} + +/** + * Chunk elements in the array by size + * + * when the array cannot be chunked evenly by size, the last chunk will be + * filled with the remaining elements + * + * @example + * chunkArray(['a','b', 'c', 'd', 'e'], 2) // returns [['a','b'], ['c', 'd'], ['e']] + */ +export function chunkArray(array: Array, size: number): Array> { + if (size < 1) return [] + let i = 0 + const result: Array> = [] + while (i < array.length) { + result.push(array.slice(i, i + size)) + i = i + size + } + return result +} + +type HandleEntryFn = (entry: Entry) => JSX.Element + +type ExplorerProps = Partial & { + defaultExpanded?: true | Record + value: Accessor +} + +type Property = { + defaultExpanded?: boolean | Record + label: string + value: unknown +} + +function isIterable(x: any): x is Iterable { + return Symbol.iterator in x +} + +function displayValue(value: unknown): string { + if (value === null) return `null` + if (value === undefined) return `undefined` + if (typeof value === `string`) return `"${value}"` + if (typeof value === `number`) return value.toString() + if (typeof value === `boolean`) return value.toString() + if (typeof value === `function`) return `function` + if (value instanceof Date) return `Date('${value.toISOString()}')` + if (value instanceof Map) return `Map(${value.size})` + if (value instanceof Set) return `Set(${value.size})` + if (Array.isArray(value)) return `Array(${value.length})` + if (typeof value === `object`) return `Object` + return String(value) +} + +export function Explorer({ + value, + defaultExpanded, + pageSize = 100, + filterSubEntries, + ...rest +}: ExplorerProps) { + const [expanded, setExpanded] = createSignal(Boolean(defaultExpanded)) + const toggleExpanded = () => setExpanded((old) => !old) + + const type = createMemo(() => typeof value()) + const subEntries = createMemo(() => { + let entries: Array = [] + + const makeProperty = (sub: { label: string; value: unknown }): Property => { + const subDefaultExpanded = + defaultExpanded === true + ? { [sub.label]: true } + : defaultExpanded?.[sub.label] + return { + ...sub, + value: () => sub.value, + defaultExpanded: subDefaultExpanded, + } + } + + if (Array.isArray(value())) { + // any[] + entries = (value() as Array).map((d, i) => + makeProperty({ + label: i.toString(), + value: d, + }) + ) + } else if (value() instanceof Map) { + // Map + entries = Array.from((value() as Map).entries()).map( + ([key, val]) => + makeProperty({ + label: String(key), + value: val, + }) + ) + } else if (value() instanceof Set) { + // Set + entries = Array.from(value() as Set, (val, i) => + makeProperty({ + label: i.toString(), + value: val, + }) + ) + } else if ( + value() !== null && + typeof value() === `object` && + isIterable(value()) && + typeof (value() as Iterable)[Symbol.iterator] === `function` + ) { + // Iterable + entries = Array.from(value() as Iterable, (val, i) => + makeProperty({ + label: i.toString(), + value: val, + }) + ) + } else if (typeof value() === `object` && value() !== null) { + // object + entries = Object.entries(value() as object).map(([key, val]) => + makeProperty({ + label: key, + value: val, + }) + ) + } + + return filterSubEntries ? filterSubEntries(entries) : entries + }) + + const subEntryPages = createMemo(() => chunkArray(subEntries(), pageSize)) + + const [expandedPages, setExpandedPages] = createSignal>([]) + const styles = useStyles() + + // const refreshValueSnapshot = () => { + // setValueSnapshot((value() as () => any)()) + // } + + const handleEntry = (entry: Entry) => ( + + ) + + return ( +
+ {subEntryPages().length ? ( + <> + + {(expanded() ?? false) ? ( + subEntryPages().length === 1 ? ( +
+ {subEntries().map((entry, _index) => handleEntry(entry))} +
+ ) : ( +
+ {subEntryPages().map((entries, index) => { + return ( +
+
+ + {expandedPages().includes(index) ? ( +
+ {entries.map((entry) => handleEntry(entry))} +
+ ) : null} +
+
+ ) + })} +
+ ) + ) : null} + + ) : type() === `function` ? ( + <> + {rest.label}: + {` `} + {displayValue(value())} + + ) : ( + <> + {rest.label}: + {` `} + {displayValue(value())} + + )} +
+ ) +} + +const stylesFactory = (shadowDOMTarget?: ShadowRoot) => { + const { colors, font, size } = tokens + const { fontFamily, lineHeight, size: fontSize } = font + const css = shadowDOMTarget + ? goober.css.bind({ target: shadowDOMTarget }) + : goober.css + + return { + entry: css` + font-family: ${fontFamily.mono}; + font-size: ${fontSize.xs}; + line-height: ${lineHeight.sm}; + outline: none; + word-break: break-word; + `, + labelButton: css` + cursor: pointer; + color: inherit; + font: inherit; + outline: inherit; + background: transparent; + border: none; + padding: 0; + `, + expander: css` + display: inline-flex; + align-items: center; + justify-content: center; + width: ${size[3]}; + height: ${size[3]}; + padding-left: 3px; + box-sizing: content-box; + `, + expanderIcon: (expanded: boolean) => { + if (expanded) { + return css` + transform: rotate(90deg); + transition: transform 0.1s ease; + ` + } + return css` + transform: rotate(0deg); + transition: transform 0.1s ease; + ` + }, + expandButton: css` + display: flex; + gap: ${size[1]}; + align-items: center; + cursor: pointer; + color: inherit; + font: inherit; + outline: inherit; + background: transparent; + border: none; + padding: 0; + `, + value: css` + color: ${colors.purple[400]}; + `, + subEntries: css` + margin-left: ${size[2]}; + padding-left: ${size[2]}; + border-left: 2px solid ${colors.darkGray[400]}; + `, + info: css` + color: ${colors.gray[500]}; + font-size: ${fontSize.xs}; + padding-left: ${size[1]}; + `, + refreshValueBtn: css` + appearance: none; + border: 0; + cursor: pointer; + background: transparent; + color: inherit; + padding: 0; + font-family: ${fontFamily.mono}; + font-size: ${fontSize.xs}; + `, + } +} + +function useStyles() { + const shadowDOMTarget = useContext(ShadowDomTargetContext) + const styles = stylesFactory(shadowDOMTarget) + return () => styles +} diff --git a/packages/db-devtools/src/components/Logo.tsx b/packages/db-devtools/src/components/Logo.tsx new file mode 100644 index 000000000..6087c9578 --- /dev/null +++ b/packages/db-devtools/src/components/Logo.tsx @@ -0,0 +1,18 @@ +import { clsx as cx } from "clsx" +import { useStyles } from "../useStyles" + +interface LogoProps { + className?: () => string + [key: string]: any +} + +export function Logo(props: LogoProps) { + const { className, ...rest } = props + const styles = useStyles() + return ( + + ) +} diff --git a/packages/db-devtools/src/components/SyntaxHighlighter.tsx b/packages/db-devtools/src/components/SyntaxHighlighter.tsx new file mode 100644 index 000000000..68af5401b --- /dev/null +++ b/packages/db-devtools/src/components/SyntaxHighlighter.tsx @@ -0,0 +1,35 @@ +import { createEffect, onMount } from "solid-js" +import Prism from "prismjs" +import "prismjs/components/prism-javascript" +import "prismjs/components/prism-typescript" + +interface SyntaxHighlighterProps { + code: string + language?: string + class?: string +} + +export function SyntaxHighlighter(props: SyntaxHighlighterProps) { + let preRef: HTMLPreElement | undefined + + onMount(() => { + if (preRef) { + Prism.highlightElement(preRef) + } + }) + + createEffect(() => { + if (preRef && props.code) { + Prism.highlightElement(preRef) + } + }) + + return ( +
+      {props.code}
+    
+ ) +} diff --git a/packages/db-devtools/src/components/TabNavigation.tsx b/packages/db-devtools/src/components/TabNavigation.tsx new file mode 100644 index 000000000..ebb8d49e5 --- /dev/null +++ b/packages/db-devtools/src/components/TabNavigation.tsx @@ -0,0 +1,46 @@ +import { clsx as cx } from "clsx" +import { useStyles } from "../useStyles" +import type { Accessor } from "solid-js" + +interface TabNavigationProps { + selectedView: Accessor<`collections` | `transactions`> + collectionsCount: Accessor + transactionsCount: Accessor + onSelectView: (view: `collections` | `transactions`) => void +} + +export function TabNavigation({ + selectedView, + collectionsCount, + transactionsCount, + onSelectView, +}: TabNavigationProps) { + const styles = useStyles() + + return ( +
+ + +
+ ) +} diff --git a/packages/db-devtools/src/components/TransactionItem.tsx b/packages/db-devtools/src/components/TransactionItem.tsx new file mode 100644 index 000000000..182bd4f58 --- /dev/null +++ b/packages/db-devtools/src/components/TransactionItem.tsx @@ -0,0 +1,33 @@ +import { clsx as cx } from "clsx" +import { useStyles } from "../useStyles" +import { TransactionStats } from "./TransactionStats" +import type { Accessor } from "solid-js" +import type { TransactionDetails } from "../types" + +interface TransactionItemProps { + transaction: TransactionDetails + isActive: Accessor + onSelect: (transactionId: string) => void +} + +export function TransactionItem({ + transaction, + isActive, + onSelect, +}: TransactionItemProps) { + const styles = useStyles() + + return ( +
onSelect(transaction.id)} + > + {isActive() ?
: null} +
{transaction.id}
+ +
+ ) +} diff --git a/packages/db-devtools/src/components/TransactionStats.tsx b/packages/db-devtools/src/components/TransactionStats.tsx new file mode 100644 index 000000000..92b363be7 --- /dev/null +++ b/packages/db-devtools/src/components/TransactionStats.tsx @@ -0,0 +1,33 @@ +import { clsx as cx } from "clsx" +import { useStyles } from "../useStyles" +import { formatTime } from "../utils/formatTime" +import type { TransactionDetails } from "../types" + +interface TransactionStatsProps { + transaction: TransactionDetails +} + +export function TransactionStats({ transaction }: TransactionStatsProps) { + const styles = useStyles() + + const age = Date.now() - transaction.createdAt.getTime() + + return ( +
+
{transaction.mutations.length}
+
/
+
1
+
/
+
{formatTime(age)}
+
/
+
+ {transaction.state} +
+
+ ) +} diff --git a/packages/db-devtools/src/components/TransactionsPanel.tsx b/packages/db-devtools/src/components/TransactionsPanel.tsx new file mode 100644 index 000000000..11b09421e --- /dev/null +++ b/packages/db-devtools/src/components/TransactionsPanel.tsx @@ -0,0 +1,58 @@ +import { For, Show } from "solid-js" +import { useStyles } from "../useStyles" +import { TransactionItem } from "./TransactionItem" +import type { Accessor } from "solid-js" +import type { TransactionDetails } from "../types" + +interface TransactionsPanelProps { + transactions: Accessor> + selectedTransaction: Accessor + onSelectTransaction: (transactionId: string) => void +} + +export function TransactionsPanel({ + transactions, + selectedTransaction, + onSelectTransaction, +}: TransactionsPanelProps) { + const styles = useStyles() + + return ( +
+
+
+
+
Transactions
+
+ Mutations + / + Collections + / + Age + / + Status +
+
+ 0} + fallback={ +
+ No transactions found +
+ } + > + + {(transaction) => ( + selectedTransaction() === transaction.id} + onSelect={onSelectTransaction} + /> + )} + +
+
+
+
+ ) +} diff --git a/packages/db-devtools/src/components/index.ts b/packages/db-devtools/src/components/index.ts new file mode 100644 index 000000000..8eafa2f35 --- /dev/null +++ b/packages/db-devtools/src/components/index.ts @@ -0,0 +1,13 @@ +export { CollectionsPanel } from "./CollectionsPanel" +export { DetailsPanel, GenericDetailsPanel } from "./DetailsPanel" +export { Explorer } from "./Explorer" +export { Logo } from "./Logo" +export { TabNavigation } from "./TabNavigation" +export { TransactionItem } from "./TransactionItem" +export { TransactionStats } from "./TransactionStats" +export { TransactionsPanel } from "./TransactionsPanel" +export { CollectionItem } from "./CollectionItem" +export { CollectionStats } from "./CollectionStats" +export { CollectionDetailsPanel } from "./CollectionDetailsPanel" +export { CollectionDataView } from "./CollectionDataView" +export { DataTable } from "./DataTable" diff --git a/packages/db-devtools/src/constants.ts b/packages/db-devtools/src/constants.ts new file mode 100644 index 000000000..9ebb21f9a --- /dev/null +++ b/packages/db-devtools/src/constants.ts @@ -0,0 +1,30 @@ +export const DEFAULT_HEIGHT = 500 +export const DEFAULT_WIDTH = 500 +export const POSITION = `bottom-right` +export const BUTTON_POSITION = `bottom-right` +export const INITIAL_IS_OPEN = false +export const DEFAULT_SORT_ORDER = 1 +export const DEFAULT_SORT_FN_NAME = `Status > Last Updated` +export const DEFAULT_MUTATION_SORT_FN_NAME = `Status > Last Updated` + +export const firstBreakpoint = 1024 +export const secondBreakpoint = 796 +export const thirdBreakpoint = 700 + +export type DevtoolsPosition = + | `top-left` + | `top-right` + | `bottom-left` + | `bottom-right` + | `top` + | `bottom` + | `left` + | `right` +export type DevtoolsButtonPosition = + | `top-left` + | `top-right` + | `bottom-left` + | `bottom-right` + | `relative` + +export const isServer = typeof window === `undefined` diff --git a/packages/db-devtools/src/contexts/NavigationContext.tsx b/packages/db-devtools/src/contexts/NavigationContext.tsx new file mode 100644 index 000000000..839e26bf9 --- /dev/null +++ b/packages/db-devtools/src/contexts/NavigationContext.tsx @@ -0,0 +1,108 @@ +import { createContext, createSignal, useContext } from "solid-js" +import type { Accessor, Setter } from "solid-js" +import type { CollectionMetadata, TransactionDetails } from "../types" + +export interface NavigationState { + selectedView: Accessor<`collections` | `transactions`> + setSelectedView: Setter<`collections` | `transactions`> + activeCollectionId: Accessor + setActiveCollectionId: Setter + selectedTransaction: Accessor + setSelectedTransaction: Setter + activeCollection: Accessor + activeTransaction: Accessor + collections: Accessor> + setCollections: Setter> + transactions: Accessor> + setTransactions: Setter> +} + +const NavigationContext = createContext() + +export function createNavigationStore(): NavigationState { + const [selectedView, setSelectedView] = createSignal< + `collections` | `transactions` + >(`collections`) + const [activeCollectionId, setActiveCollectionId] = createSignal(``) + const [selectedTransaction, setSelectedTransaction] = createSignal< + string | null + >(null) + + // These will be set by the parent component + const [collections, setCollections] = createSignal>( + [] + ) + const [transactions, setTransactions] = createSignal< + Array + >([]) + + const activeCollection = () => { + const active = collections().find((c) => c.id === activeCollectionId()) + return active + } + + const activeTransaction = () => { + const active = transactions().find((t) => t.id === selectedTransaction()) + return active + } + + // Debug logging + const debugSetSelectedView: Setter<`collections` | `transactions`> = ( + value + ) => { + setSelectedView(value) + } + + const debugSetActiveCollectionId: Setter = (value) => { + setActiveCollectionId(value) + } + + const debugSetSelectedTransaction: Setter = (value) => { + setSelectedTransaction(value) + } + + const debugSetCollections: Setter> = (value) => { + setCollections(value) + } + + const debugSetTransactions: Setter> = (value) => { + setTransactions(value) + } + + const store: NavigationState = { + selectedView, + setSelectedView: debugSetSelectedView, + activeCollectionId, + setActiveCollectionId: debugSetActiveCollectionId, + selectedTransaction, + setSelectedTransaction: debugSetSelectedTransaction, + activeCollection, + activeTransaction, + // Internal state setters for parent component + collections, + setCollections: debugSetCollections, + transactions, + setTransactions: debugSetTransactions, + } + + return store +} + +export function useNavigation() { + const context = useContext(NavigationContext) + if (!context) { + throw new Error(`useNavigation must be used within a NavigationProvider`) + } + return context +} + +export function NavigationProvider(props: { + children: any + store: NavigationState +}) { + return ( + + {props.children} + + ) +} diff --git a/packages/db-devtools/src/contexts/index.tsx b/packages/db-devtools/src/contexts/index.tsx new file mode 100644 index 000000000..518b202ff --- /dev/null +++ b/packages/db-devtools/src/contexts/index.tsx @@ -0,0 +1,18 @@ +import { createContext, useContext } from "solid-js" + +// Devtools On Close Context - matches Router devtools pattern +export const DevtoolsOnCloseContext = createContext<{ + onCloseClick: (e: any) => void +}>({ + onCloseClick: () => {}, +}) + +export const useDevtoolsOnClose = () => useContext(DevtoolsOnCloseContext) + +// Shadow DOM Target Context - matches Router devtools pattern +export const ShadowDomTargetContext = createContext( + undefined +) + +// Navigation Context +export * from "./NavigationContext" diff --git a/packages/db-devtools/src/devtools-store.ts b/packages/db-devtools/src/devtools-store.ts new file mode 100644 index 000000000..29460ca5f --- /dev/null +++ b/packages/db-devtools/src/devtools-store.ts @@ -0,0 +1,383 @@ +import { createCollection, localOnlyCollectionOptions } from "@tanstack/db" +import type { CollectionImpl } from "../../db/src/collection" +import type { Transaction } from "../../db/src/transactions" +import type { + CollectionMetadata, + DevtoolsCollectionEntry, + DevtoolsStore, + DevtoolsTransactionEntry, +} from "./types" + +// Collections collection - stores devtools collection entries +const devtoolsCollectionsCollection = createCollection( + localOnlyCollectionOptions({ + id: `__devtools_collections`, + __devtoolsInternal: true, // Prevent self-registration + getKey: (entry: DevtoolsCollectionEntry) => entry.id, + }) +) + +// Transactions collection - stores devtools transaction entries +const devtoolsTransactionsCollection = createCollection( + localOnlyCollectionOptions({ + id: `__devtools_transactions`, + __devtoolsInternal: true, // Prevent self-registration + getKey: (entry: DevtoolsTransactionEntry) => entry.id, + }) +) + +class DevtoolsStoreImpl implements DevtoolsStore { + public collections = devtoolsCollectionsCollection as any + public transactions = devtoolsTransactionsCollection as any + + private countTransactionsForCollection = (collectionId: string): number => { + let count = 0 + for (const entry of this.transactions.values()) { + if (entry.collectionId === collectionId) count++ + } + return count + } + + private hasTransactionsForCollection = (collectionId: string): boolean => { + for (const entry of this.transactions.values()) { + if (entry.collectionId === collectionId) return true + } + return false + } + + registerCollection = ( + collection: CollectionImpl + ): (() => void) | undefined => { + // Check if collection is already registered + const existingEntry = this.collections.get(collection.id) + if (existingEntry) { + // Collection already exists, just update the weak ref and return existing callback + existingEntry.weakRef = new WeakRef(collection) + return existingEntry.updateCallback + } + + const metadata: CollectionMetadata = { + id: collection.id, + type: this.detectCollectionType(collection), + status: collection.status, + size: collection.size, + hasTransactions: this.hasTransactionsForCollection(collection.id), + transactionCount: this.countTransactionsForCollection(collection.id), + createdAt: new Date(), + lastUpdated: new Date(), + gcTime: collection.config.gcTime, + timings: this.isLiveQuery(collection) + ? { + totalIncrementalRuns: 0, + } + : undefined, + } + + // Create a callback that updates metadata for this specific collection + const updateCallback = () => { + this.updateCollection(collection.id) + } + + // Create a callback that updates only transactions for this collection + const updateTransactionsCallback = () => { + this.updateTransactions(collection.id) + } + + const entry: DevtoolsCollectionEntry = { + id: collection.id, + weakRef: new WeakRef(collection), + metadata, + isActive: false, + updateCallback, + updateTransactionsCallback, + } + + // Insert into collections collection + this.collections.insert(entry) + + // Track performance for live queries + if (this.isLiveQuery(collection)) { + this.instrumentLiveQuery(collection, entry) + } + + // Call the update callback immediately so devtools UI updates right away + queueMicrotask(updateCallback) + + // Return the update callback for the collection to use + return updateCallback + } + + unregisterCollection = (id: string): void => { + const entry = this.collections.get(id) + if (entry) { + // Release any hard reference + entry.hardRef = undefined + entry.isActive = false + this.collections.delete(id) + } + } + + registerTransaction = ( + transaction: Transaction, + collectionId: string + ): void => { + // Check if transaction is already registered + const existingEntry = this.transactions.get(transaction.id) + if (existingEntry) { + // Transaction already exists, just update the weak ref and state + existingEntry.weakRef = new WeakRef(transaction) + existingEntry.state = transaction.state + existingEntry.isPersisted = transaction.state === `completed` + existingEntry.updatedAt = new Date() + return + } + + const entry: DevtoolsTransactionEntry = { + id: transaction.id, + collectionId, + state: transaction.state, + mutations: transaction.mutations.map((m: any) => ({ + id: m.mutationId, + type: m.type, + key: m.key, + optimistic: m.optimistic, + createdAt: m.createdAt, + original: m.original, + modified: m.modified, + changes: m.changes, + })), + createdAt: transaction.createdAt, + updatedAt: transaction.createdAt, + isPersisted: transaction.state === `completed`, + weakRef: new WeakRef(transaction), + } + + // Insert into transactions collection + this.transactions.insert(entry) + // Bump the parent collection metadata to reflect transaction counts immediately + this.updateCollection(collectionId) + } + + getCollection = (id: string): CollectionImpl | undefined => { + const entry = this.collections.get(id) + if (!entry) return undefined + + const collection = entry.weakRef.deref() + if (collection && !entry.isActive) { + // Create hard reference + entry.hardRef = collection + entry.isActive = true + } + + return collection + } + + releaseCollection = (id: string): void => { + const entry = this.collections.get(id) + if (entry && entry.isActive) { + // Release hard reference + entry.hardRef = undefined + entry.isActive = false + } + } + + getAllCollectionMetadata = (): Array => { + const results: Array = [] + + for (const entry of this.collections.values()) { + const collection = entry.weakRef.deref() + if (collection) { + // Compute fresh metadata snapshot without mutating stored entry in-place + const snapshot: CollectionMetadata = { + ...entry.metadata, + status: collection.status, + size: collection.size, + hasTransactions: this.hasTransactionsForCollection(entry.id), + transactionCount: this.countTransactionsForCollection(entry.id), + lastUpdated: new Date(), + } + results.push(snapshot) + } else { + // Collection was garbage collected, report cleaned-up snapshot (do not mutate entry) + const snapshot: CollectionMetadata = { + ...entry.metadata, + status: `cleaned-up`, + lastUpdated: new Date(), + } + results.push(snapshot) + } + } + + return results + } + + getTransactions = ( + collectionId?: string + ): Array => { + const transactions: Array = [] + + for (const entry of this.transactions.values()) { + if (collectionId && entry.collectionId !== collectionId) continue + + // Update transaction state from weak ref if available + const transaction = entry.weakRef.deref() + if (transaction) { + entry.state = transaction.state + entry.isPersisted = transaction.state === `completed` + entry.updatedAt = new Date() + } + + transactions.push({ ...entry }) + } + + return transactions.sort( + (a, b) => b.createdAt.getTime() - a.createdAt.getTime() + ) + } + + updateCollection = (id: string): void => { + const entry = this.collections.get(id) + if (!entry) return + + const collection = entry.weakRef.deref() + if (collection) { + // Build fresh metadata snapshot (avoid mutating stored entry to ensure change detection) + const newMetadata: CollectionMetadata = { + ...entry.metadata, + status: collection.status, + size: collection.size, + hasTransactions: this.hasTransactionsForCollection(id), + transactionCount: this.countTransactionsForCollection(id), + lastUpdated: new Date(), + } + + this.collections.update(id, (draft: any) => { + draft.metadata = newMetadata + }) + } + } + + updateTransactions = (collectionId?: string): void => { + // Update all transactions for the collection + for (const entry of this.transactions.values()) { + if (collectionId && entry.collectionId !== collectionId) continue + + const transaction = entry.weakRef.deref() + if (transaction) { + const newState = transaction.state + const newPersisted = transaction.state === `completed` + const newUpdatedAt = new Date() + + this.transactions.update(entry.id, (draft: any) => { + draft.state = newState + draft.isPersisted = newPersisted + draft.updatedAt = newUpdatedAt + }) + + // Optional: when a transaction completes, ensure parent metadata updates + this.updateCollection(entry.collectionId) + } + } + } + + cleanup = (): void => { + // Release all hard references + for (const entry of this.collections.values()) { + if (entry.isActive) { + entry.hardRef = undefined + entry.isActive = false + } + } + } + + garbageCollect = (): void => { + // Remove entries for collections that have been garbage collected + const collectionsToRemove: Array = [] + for (const entry of this.collections.values()) { + const collection = entry.weakRef.deref() + if (!collection) { + collectionsToRemove.push(entry.id) + } + } + + // Remove dead collections + for (const id of collectionsToRemove) { + this.collections.delete(id) + } + + // Remove entries for transactions that have been garbage collected + const transactionsToRemove: Array = [] + for (const entry of this.transactions.values()) { + const transaction = entry.weakRef.deref() + if (!transaction) { + transactionsToRemove.push(entry.id) + } + } + + // Remove dead transactions + for (const id of transactionsToRemove) { + this.transactions.delete(id) + } + } + + private detectCollectionType = (collection: any): string => { + // Check the new collection type marker first + if (collection.config.collectionType) { + return collection.config.collectionType + } + + // Default to generic collection + return `generic` + } + + private isLiveQuery = (collection: any): boolean => { + return this.detectCollectionType(collection) === `live-query` + } + + private instrumentLiveQuery = ( + collection: any, + entry: DevtoolsCollectionEntry + ): void => { + // This is where we would add performance tracking for live queries + // We'll need to hook into the query execution pipeline to track timings + // For now, this is a placeholder + if (!entry.metadata.timings) { + entry.metadata.timings = { + totalIncrementalRuns: 0, + } + } + } +} + +// Create and export the devtools store +export function createDevtoolsStore(): DevtoolsStore { + return new DevtoolsStoreImpl() as any +} + +// Initialize the global devtools store +export function initializeDevtoolsStore(): DevtoolsStore { + // SSR safety check - return a no-op store for server-side rendering + if (typeof window === `undefined`) { + return { + collections: {} as any, + transactions: {} as any, + registerCollection: () => undefined, + unregisterCollection: () => {}, + registerTransaction: () => {}, + getCollection: () => undefined, + releaseCollection: () => {}, + getAllCollectionMetadata: () => [], + getTransactions: () => [], + updateCollection: () => {}, + updateTransactions: () => {}, + cleanup: () => {}, + garbageCollect: () => {}, + } as DevtoolsStore + } + + // Only create real store on the client side + if (!(window as any).__TANSTACK_DB_DEVTOOLS_STORE__) { + ;(window as any).__TANSTACK_DB_DEVTOOLS_STORE__ = createDevtoolsStore() + } + return (window as any).__TANSTACK_DB_DEVTOOLS_STORE__ as DevtoolsStore +} diff --git a/packages/db-devtools/src/devtools.ts b/packages/db-devtools/src/devtools.ts new file mode 100644 index 000000000..2d790801d --- /dev/null +++ b/packages/db-devtools/src/devtools.ts @@ -0,0 +1,136 @@ +import { initializeDevtoolsRegistry } from "./registry" +import { initializeDevtoolsStore } from "./devtools-store" +import { getDevtools } from "./global-types" +import type { CollectionImpl } from "../../db/src/collection" +import type { DbDevtoolsRegistry } from "./types" + +/** + * Initialize the DB devtools registry. + * This should be called once in your application, typically in your main entry point. + * Collections will automatically register themselves if this registry is present. + */ +export function initializeDbDevtools(): void { + // SSR safety check + if (typeof window === `undefined`) { + return + } + + // Check if devtools are already initialized + const hasGlobal = Object.prototype.hasOwnProperty.call( + window, + `__TANSTACK_DB_DEVTOOLS__` + ) + if (hasGlobal) return + + // Initialize the registry and store + const registry = initializeDevtoolsRegistry() + const store = initializeDevtoolsStore() + + // Store the registry globally with proper typing + window.__TANSTACK_DB_DEVTOOLS__ = { + ...registry, + // Keep store available for registerTransaction from core package + store, + } + + // Flush any transactions that were queued before devtools initialized + const w: any = window as any + const pending = w.__TANSTACK_DB_PENDING_TRANSACTIONS__ as + | Array<{ transaction: any; collectionId: string }> + | undefined + if (Array.isArray(pending) && pending.length) { + for (const { transaction, collectionId } of pending) { + store.registerTransaction(transaction, collectionId) + } + w.__TANSTACK_DB_PENDING_TRANSACTIONS__ = [] + } +} + +/** + * Manually register a collection with the devtools. + * This is automatically called by collections when they are created if devtools are enabled. + */ +export function registerCollection( + collection: CollectionImpl | undefined +): void { + if (typeof window === `undefined`) return + + const devtools = getDevtools() + if (devtools?.registerCollection && collection) { + const updateCallback = devtools.registerCollection(collection) + if (updateCallback) { + ;(collection as any).__devtoolsUpdateCallback = updateCallback + } + } +} + +/** + * Manually unregister a collection from the devtools. + * This is automatically called when collections are garbage collected. + */ +export function unregisterCollection(id: string): void { + if (typeof window === `undefined`) return + + const devtools = getDevtools() + devtools?.unregisterCollection(id) +} + +/** + * Check if devtools are currently enabled (registry is present). + */ +export function isDevtoolsEnabled(): boolean { + if (typeof window === `undefined`) return false + return !!getDevtools() +} + +export function getDevtoolsRegistry(): DbDevtoolsRegistry | undefined { + if (typeof window === `undefined`) return undefined + const devtools = getDevtools() + if (!devtools) return undefined + + // Return the actual registry instance that has the store property + return devtools as unknown as DbDevtoolsRegistry +} + +/** + * Trigger a metadata update for a collection in the devtools. + * This should be called by collections when their state changes significantly. + */ +export function triggerCollectionUpdate( + collection: CollectionImpl +): void { + if (typeof window === `undefined`) return + + const updateCallback = (collection as any).__devtoolsUpdateCallback + if (typeof updateCallback === `function`) { + updateCallback() + } +} + +/** + * Trigger a transaction update for a collection in the devtools. + * This should be called by collections when their transactions change. + */ +export function triggerTransactionUpdate( + collection: CollectionImpl +): void { + if (typeof window === `undefined`) return + + const devtools = getDevtools() + // Delegate to store/registry through the public API + devtools?.store.updateTransactions(collection.id) +} + +/** + * Clean up the devtools registry and all references. + * This is useful for testing or when you want to completely reset the devtools state. + */ +export function cleanupDevtools(): void { + if (typeof window === `undefined`) return + + const devtools = getDevtools() + if (devtools) { + devtools.store.cleanup() + delete window.__TANSTACK_DB_DEVTOOLS__ + } +} diff --git a/packages/db-devtools/src/global-types.ts b/packages/db-devtools/src/global-types.ts new file mode 100644 index 000000000..b540b4ceb --- /dev/null +++ b/packages/db-devtools/src/global-types.ts @@ -0,0 +1,34 @@ +import type { CollectionImpl } from "../../db/src/collection" +import type { DevtoolsStore } from "./types" + +// Global type definitions for devtools +declare global { + interface Window { + __TANSTACK_DB_DEVTOOLS__?: { + // Minimal surface area exposed globally + registerCollection: ( + collection: CollectionImpl + ) => (() => void) | undefined + unregisterCollection: (id: string) => void + registerTransaction?: (transaction: any, collectionId: string) => void + updateTransactions?: (collectionId?: string) => void + // Core package may call this while devtools are initializing + store: DevtoolsStore + } + + // Queue used before devtools initialize (read/written by core) + __TANSTACK_DB_PENDING_TRANSACTIONS__?: Array<{ + transaction: any + collectionId: string + }> + } +} + +// Export the type for use in other files +export type TanStackDbDevtools = NonNullable + +// Helper function for accessing devtools with proper typing +export function getDevtools(): TanStackDbDevtools | undefined { + if (typeof window === `undefined`) return undefined + return window.__TANSTACK_DB_DEVTOOLS__ +} diff --git a/packages/db-devtools/src/index.ts b/packages/db-devtools/src/index.ts new file mode 100644 index 000000000..0768420a1 --- /dev/null +++ b/packages/db-devtools/src/index.ts @@ -0,0 +1,19 @@ +// Re-export all public APIs +export * from "./devtools" +export * from "./devtools-store" +export * from "./global-types" +export * from "./registry" +export * from "./types" + +// Re-export components +export { BaseTanStackDbDevtoolsPanel } from "./BaseTanStackDbDevtoolsPanel" +export { + TanstackDbDevtools, + type TanstackDbDevtoolsConfig, +} from "./TanstackDbDevtools" +export { FloatingTanStackDbDevtools } from "./FloatingTanStackDbDevtools" + +// Re-export utilities +export { useLocalStorage } from "./useLocalStorage" +export { useStyles } from "./useStyles" +export { useDevtoolsOnClose } from "./contexts" diff --git a/packages/db-devtools/src/logo.tsx b/packages/db-devtools/src/logo.tsx new file mode 100644 index 000000000..09ca2fb5f --- /dev/null +++ b/packages/db-devtools/src/logo.tsx @@ -0,0 +1,817 @@ +import { createUniqueId } from "solid-js" + +export function TanStackLogo() { + const id = createUniqueId() + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/db-devtools/src/registry.ts b/packages/db-devtools/src/registry.ts new file mode 100644 index 000000000..8553c6853 --- /dev/null +++ b/packages/db-devtools/src/registry.ts @@ -0,0 +1,182 @@ +import { createSignal } from "solid-js" +import { initializeDevtoolsStore } from "./devtools-store" +import { getDevtools } from "./global-types" +import type { + CollectionMetadata, + DbDevtoolsRegistry, + TransactionDetails, +} from "./types" + +class DbDevtoolsRegistryImpl implements DbDevtoolsRegistry { + public store = initializeDevtoolsStore() + + // SolidJS signals for reactive updates (kept for backward compatibility) + private _collectionsSignal = createSignal>([]) + private _transactionsSignal = createSignal>([]) + + // Expose collections map for backward compatibility + public get collections() { + return new Map() // Empty map since we're using collections now + } + + constructor() {} + + // Expose signals for reactive UI updates + public get collectionsSignal() { + return this._collectionsSignal[0] + } + + public get transactionsSignal() { + return this._transactionsSignal[0] + } + + private triggerUpdate = () => { + // Update collections signal with a fresh array reference + const collectionsData = this.getAllCollectionMetadata() + this._collectionsSignal[1]([...collectionsData]) + + // Update transactions signal + const transactionsData = this.store.getTransactions() + this._transactionsSignal[1]( + transactionsData.map((entry) => ({ + id: entry.id, + collectionId: entry.collectionId, + state: entry.state, + mutations: entry.mutations, + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + isPersisted: entry.isPersisted, + })) + ) + } + + private triggerCollectionUpdate = (id: string) => { + const updatedMetadata = this.getCollectionMetadata(id) + if (!updatedMetadata) return + const currentCollections = this._collectionsSignal[0]() + const next = currentCollections.map((c) => + c.id === id ? updatedMetadata : c + ) + this._collectionsSignal[1](next) + } + + private triggerTransactionUpdate = (collectionId?: string) => { + // Get updated transactions data + const updatedTransactions = this.store.getTransactions(collectionId) + this._transactionsSignal[1]( + updatedTransactions.map((entry) => ({ + id: entry.id, + collectionId: entry.collectionId, + state: entry.state, + mutations: entry.mutations, + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + isPersisted: entry.isPersisted, + })) + ) + } + + registerCollection = (collection: any): (() => void) | undefined => { + const updateCallback = this.store.registerCollection(collection) + + // Set the update callback on the collection for future updates + if (updateCallback && collection) { + collection.__devtoolsUpdateCallback = updateCallback + } + + // Trigger reactive update for immediate UI refresh + this.triggerUpdate() + + // Return the update callback for the collection to use + return updateCallback + } + + unregisterCollection = (id: string): void => { + this.store.unregisterCollection(id) + + // Trigger reactive update for immediate UI refresh + this.triggerUpdate() + } + + getCollectionMetadata = (id: string): CollectionMetadata | undefined => { + const entry = this.store.collections.get(id) + if (!entry) return undefined + // Delegate to store snapshot logic to avoid mutating entry metadata here + const all = this.store.getAllCollectionMetadata() + return all.find((c) => c.id === id) + } + + getAllCollectionMetadata = (): Array => { + return this.store.getAllCollectionMetadata() + } + + updateCollectionMetadata = (id: string): void => { + this.store.updateCollection(id) + + // Use efficient update that only changes the specific collection + this.triggerCollectionUpdate(id) + + // Also update transactions since they may have changed + this.triggerTransactionUpdate(id) + } + + updateTransactions = (collectionId?: string): void => { + this.store.updateTransactions(collectionId) + this.triggerTransactionUpdate(collectionId) + } + + getCollection = (id: string): any => { + return this.store.getCollection(id) + } + + releaseCollection = (id: string): void => { + this.store.releaseCollection(id) + } + + cleanup = (): void => { + this.store.cleanup() + } + + garbageCollect = (): void => { + this.store.garbageCollect() + } +} + +// Create and export the global registry +export function createDbDevtoolsRegistry(): DbDevtoolsRegistry { + return new DbDevtoolsRegistryImpl() +} + +// Initialize the global registry if not already present +export function initializeDevtoolsRegistry(): DbDevtoolsRegistry { + // SSR safety check - return a no-op registry for server-side rendering + if (typeof window === `undefined`) { + // Create dummy signals that won't be used during SSR + const dummySignal = () => [] + dummySignal.set = () => {} + + return { + collections: new Map(), + store: initializeDevtoolsStore(), + collectionsSignal: dummySignal as any, + transactionsSignal: dummySignal as any, + registerCollection: () => undefined, + unregisterCollection: () => {}, + getCollection: () => undefined, + releaseCollection: () => {}, + getAllCollectionMetadata: () => [], + getCollectionMetadata: () => undefined, + updateCollectionMetadata: () => {}, + updateTransactions: () => {}, + getTransactions: () => [], + cleanup: () => {}, + garbageCollect: () => {}, + } as DbDevtoolsRegistry + } + + // Only create real signals on the client side + if (!getDevtools()) { + window.__TANSTACK_DB_DEVTOOLS__ = createDbDevtoolsRegistry() + } + return getDevtools() as unknown as DbDevtoolsRegistry +} diff --git a/packages/db-devtools/src/tokens.ts b/packages/db-devtools/src/tokens.ts new file mode 100644 index 000000000..655bd7936 --- /dev/null +++ b/packages/db-devtools/src/tokens.ts @@ -0,0 +1,264 @@ +export const tokens = { + colors: { + inherit: `inherit`, + current: `currentColor`, + transparent: `transparent`, + black: `#000000`, + white: `#ffffff`, + neutral: { + 50: `#f9fafb`, + 100: `#f2f4f7`, + 200: `#eaecf0`, + 300: `#d0d5dd`, + 400: `#98a2b3`, + 500: `#667085`, + 600: `#475467`, + 700: `#344054`, + 800: `#1d2939`, + 900: `#101828`, + }, + darkGray: { + 50: `#525c7a`, + 100: `#49536e`, + 200: `#414962`, + 300: `#394056`, + 400: `#313749`, + 500: `#292e3d`, + 600: `#212530`, + 700: `#191c24`, + 800: `#111318`, + 900: `#0b0d10`, + }, + gray: { + 50: `#f9fafb`, + 100: `#f2f4f7`, + 200: `#eaecf0`, + 300: `#d0d5dd`, + 400: `#98a2b3`, + 500: `#667085`, + 600: `#475467`, + 700: `#344054`, + 800: `#1d2939`, + 900: `#101828`, + }, + blue: { + 25: `#F5FAFF`, + 50: `#EFF8FF`, + 100: `#D1E9FF`, + 200: `#B2DDFF`, + 300: `#84CAFF`, + 400: `#53B1FD`, + 500: `#2E90FA`, + 600: `#1570EF`, + 700: `#175CD3`, + 800: `#1849A9`, + 900: `#194185`, + }, + green: { + 25: `#F6FEF9`, + 50: `#ECFDF3`, + 100: `#D1FADF`, + 200: `#A6F4C5`, + 300: `#6CE9A6`, + 400: `#32D583`, + 500: `#12B76A`, + 600: `#039855`, + 700: `#027A48`, + 800: `#05603A`, + 900: `#054F31`, + }, + red: { + 50: `#fef2f2`, + 100: `#fee2e2`, + 200: `#fecaca`, + 300: `#fca5a5`, + 400: `#f87171`, + 500: `#ef4444`, + 600: `#dc2626`, + 700: `#b91c1c`, + 800: `#991b1b`, + 900: `#7f1d1d`, + 950: `#450a0a`, + }, + yellow: { + 25: `#FFFCF5`, + 50: `#FFFAEB`, + 100: `#FEF0C7`, + 200: `#FEDF89`, + 300: `#FEC84B`, + 400: `#FDB022`, + 500: `#F79009`, + 600: `#DC6803`, + 700: `#B54708`, + 800: `#93370D`, + 900: `#7A2E0E`, + }, + purple: { + 25: `#FAFAFF`, + 50: `#F4F3FF`, + 100: `#EBE9FE`, + 200: `#D9D6FE`, + 300: `#BDB4FE`, + 400: `#9B8AFB`, + 500: `#7A5AF8`, + 600: `#6328EF`, + 700: `#5912D3`, + 800: `#4A0FB0`, + 900: `#3E0C8E`, + }, + orange: { + 25: `#FFFAF5`, + 50: `#FFF4ED`, + 100: `#FFE6D5`, + 200: `#FFD6AE`, + 300: `#FF9C66`, + 400: `#FF692E`, + 500: `#FF4405`, + 600: `#E62E05`, + 700: `#BC1B06`, + 800: `#97180C`, + 900: `#771A0D`, + }, + }, + size: { + px: `1px`, + 0: `0px`, + 0.5: `0.125rem`, + 1: `0.25rem`, + 1.5: `0.375rem`, + 2: `0.5rem`, + 2.5: `0.625rem`, + 3: `0.75rem`, + 3.5: `0.875rem`, + 4: `1rem`, + 5: `1.25rem`, + 6: `1.5rem`, + 7: `1.75rem`, + 8: `2rem`, + 9: `2.25rem`, + 10: `2.5rem`, + 11: `2.75rem`, + 12: `3rem`, + 14: `3.5rem`, + 16: `4rem`, + 20: `5rem`, + 24: `6rem`, + 28: `7rem`, + 32: `8rem`, + 36: `9rem`, + 40: `10rem`, + 44: `11rem`, + 48: `12rem`, + 52: `13rem`, + 56: `14rem`, + 60: `15rem`, + 64: `16rem`, + 72: `18rem`, + 80: `20rem`, + 96: `24rem`, + }, + alpha: { + 5: `0D`, + 10: `1A`, + 20: `33`, + 30: `4D`, + 40: `66`, + 50: `80`, + 60: `99`, + 70: `B3`, + 80: `CC`, + 90: `E6`, + 95: `F2`, + }, + font: { + size: { + xs: `0.75rem`, + sm: `0.875rem`, + md: `1rem`, + lg: `1.125rem`, + xl: `1.25rem`, + "2xl": `1.5rem`, + "3xl": `1.875rem`, + "4xl": `2.25rem`, + }, + lineHeight: { + xs: `1rem`, + sm: `1.25rem`, + md: `1.5rem`, + lg: `1.75rem`, + xl: `1.75rem`, + "2xl": `2rem`, + "3xl": `2.25rem`, + "4xl": `2.5rem`, + }, + weight: { + thin: `100`, + extralight: `200`, + light: `300`, + normal: `400`, + medium: `500`, + semibold: `600`, + bold: `700`, + extrabold: `800`, + black: `900`, + }, + fontFamily: { + sans: [ + `ui-sans-serif`, + `system-ui`, + `-apple-system`, + `BlinkMacSystemFont`, + `"Segoe UI"`, + `Roboto`, + `"Helvetica Neue"`, + `Arial`, + `"Noto Sans"`, + `sans-serif`, + `"Apple Color Emoji"`, + `"Segoe UI Emoji"`, + `"Segoe UI Symbol"`, + `"Noto Color Emoji"`, + ].join(`, `), + serif: [ + `ui-serif`, + `Georgia`, + `Cambria`, + `"Times New Roman"`, + `Times`, + `serif`, + ].join(`, `), + mono: [ + `ui-monospace`, + `SFMono-Regular`, + `"Menlo"`, + `Monaco`, + `Consolas`, + `"Liberation Mono"`, + `"Courier New"`, + `monospace`, + ].join(`, `), + }, + }, + shadow: { + xs: `0 1px 2px 0 rgb(0 0 0 / 0.05)`, + sm: `0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)`, + md: `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)`, + lg: `0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)`, + xl: `0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)`, + "2xl": `0 25px 50px -12px rgb(0 0 0 / 0.25)`, + inner: `inset 0 2px 4px 0 rgb(0 0 0 / 0.05)`, + }, + border: { + radius: { + none: `0px`, + sm: `0.125rem`, + md: `0.375rem`, + lg: `0.5rem`, + xl: `0.75rem`, + "2xl": `1rem`, + "3xl": `1.5rem`, + full: `9999px`, + xs: `0.0625rem`, + }, + }, +} diff --git a/packages/db-devtools/src/types.ts b/packages/db-devtools/src/types.ts new file mode 100644 index 000000000..acfd54ca5 --- /dev/null +++ b/packages/db-devtools/src/types.ts @@ -0,0 +1,192 @@ +import type { Collection, CollectionImpl } from "../../db/src/collection" +import type { CollectionStatus } from "../../db/src/types" + +export interface DbDevtoolsConfig { + /** + * Set this true if you want the dev tools to default to being open + */ + initialIsOpen?: boolean + /** + * The position of the TanStack logo to open and close the devtools panel. + * 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'relative' + * Defaults to 'bottom-right' + */ + position?: + | `top-left` + | `top-right` + | `bottom-left` + | `bottom-right` + | `relative` + /** + * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc. + */ + panelProps?: Record + /** + * Use this to add props to the close button. For example, you can add className, style (merge and override default style), etc. + */ + closeButtonProps?: Record + /** + * Use this to add props to the toggle button. For example, you can add className, style (merge and override default style), etc. + */ + toggleButtonProps?: Record + /** + * The prefix for the localStorage keys used to store the open state and position of the devtools panel. + * Defaults to 'tanstackDbDevtools' + */ + storageKey?: string + /** + * A boolean variable indicating whether the pannel is open or closed. + * If defined, the open state will be controlled by this variable, otherwise it will be controlled by the component's internal state. + */ + panelState?: `open` | `closed` + /** + * A callback function which will be called when the open state changes. + * If panelState is defined, this callback will be called when the user toggles the panel. + */ + onPanelStateChange?: (isOpen: boolean) => void +} + +export interface CollectionMetadata { + id: string + type: string + status: CollectionStatus + size: number + hasTransactions: boolean + transactionCount: number + createdAt: Date + lastUpdated: Date + gcTime?: number + + // Performance tracking for live queries + timings?: { + initialRunTime?: number + lastIncrementalRunTime?: number + totalIncrementalRuns: number + averageIncrementalRunTime?: number + } + + // Additional metadata + schema?: any + syncConfig?: any +} + +export interface DevtoolsCollectionEntry { + id: string + weakRef: WeakRef> + metadata: CollectionMetadata + isActive: boolean // Whether we're currently viewing this collection (hard ref held) + hardRef?: CollectionImpl // Only set when actively viewing + updateCallback?: () => void // Callback to trigger metadata update (doesn't hold strong refs) + updateTransactionsCallback?: () => void // Callback to trigger transaction update only (doesn't hold strong refs) +} + +export interface DevtoolsTransactionEntry { + id: string + collectionId: string + state: string + mutations: Array<{ + id: string + type: `insert` | `update` | `delete` + key: any + optimistic: boolean + createdAt: Date + original?: any + modified?: any + changes?: any + }> + createdAt: Date + updatedAt: Date + isPersisted: boolean + // Keep reference to actual transaction for real-time updates + weakRef: WeakRef // Transaction +} + +export interface DevtoolsStore { + collections: Collection + transactions: Collection + + // Registration methods + registerCollection: ( + collection: CollectionImpl + ) => (() => void) | undefined + unregisterCollection: (id: string) => void + registerTransaction: (transaction: any, collectionId: string) => void + + // Access methods + getCollection: (id: string) => CollectionImpl | undefined + releaseCollection: (id: string) => void + + // Metadata access + getAllCollectionMetadata: () => Array + getTransactions: (collectionId?: string) => Array + + // Update methods + updateCollection: (id: string) => void + updateTransactions: (collectionId?: string) => void + + // Utility methods + cleanup: () => void + garbageCollect: () => void +} + +export interface CollectionRegistryEntry { + weakRef: WeakRef> + metadata: CollectionMetadata + isActive: boolean // Whether we're currently viewing this collection (hard ref held) + hardRef?: CollectionImpl // Only set when actively viewing + updateCallback?: () => void // Callback to trigger metadata update (doesn't hold strong refs) + updateTransactionsCallback?: () => void // Callback to trigger transaction update only (doesn't hold strong refs) +} + +export interface TransactionDetails { + id: string + collectionId: string + state: string + mutations: Array<{ + id: string + type: `insert` | `update` | `delete` + key: any + optimistic: boolean + createdAt: Date + original?: any + modified?: any + changes?: any + }> + createdAt: Date + updatedAt: Date + isPersisted: boolean +} + +export interface DbDevtoolsRegistry { + collections: Map + + // Store for the new local-only collections implementation + store: DevtoolsStore + + // SolidJS signals for reactive UI updates + collectionsSignal: () => Array + transactionsSignal: () => Array + + // Registration methods + registerCollection: ( + collection: CollectionImpl + ) => (() => void) | undefined + unregisterCollection: (id: string) => void + + // Metadata access + getCollectionMetadata: (id: string) => CollectionMetadata | undefined + getAllCollectionMetadata: () => Array + updateCollectionMetadata: (id: string) => void // Trigger immediate metadata update + updateTransactions: (collectionId?: string) => void // Trigger immediate transaction update + + // Collection access (creates hard refs) + getCollection: (id: string) => CollectionImpl | undefined + releaseCollection: (id: string) => void + + // Cleanup utilities + cleanup: () => void + garbageCollect: () => void +} + +// Window global interface is already declared in @tanstack/db +// The DbDevtoolsRegistry interface extends the base interface used there diff --git a/packages/db-devtools/src/useLocalStorage.ts b/packages/db-devtools/src/useLocalStorage.ts new file mode 100644 index 000000000..6359c36b7 --- /dev/null +++ b/packages/db-devtools/src/useLocalStorage.ts @@ -0,0 +1,41 @@ +import { createEffect, createSignal } from "solid-js" +import type { Accessor, Setter } from "solid-js" + +export function useLocalStorage( + key: string, + defaultValue?: T +): [Accessor, Setter] { + // Initialize with default value or try to get from localStorage + const getInitialValue = (): T => { + if (typeof window === `undefined`) { + return defaultValue as T + } + + try { + const item = window.localStorage.getItem(key) + return item ? JSON.parse(item) : (defaultValue as T) + } catch { + return defaultValue as T + } + } + + const [value, setValue] = createSignal(getInitialValue()) + + // Update localStorage when value changes + createEffect(() => { + if (typeof window === `undefined`) return + + try { + const currentValue = value() + if (currentValue === undefined) { + window.localStorage.removeItem(key) + } else { + window.localStorage.setItem(key, JSON.stringify(currentValue)) + } + } catch {} + }) + + return [value, setValue] +} + +export default useLocalStorage diff --git a/packages/db-devtools/src/useStyles.tsx b/packages/db-devtools/src/useStyles.tsx new file mode 100644 index 000000000..6061d8599 --- /dev/null +++ b/packages/db-devtools/src/useStyles.tsx @@ -0,0 +1,774 @@ +import * as goober from "goober" +import { createSignal, useContext } from "solid-js" +import { tokens } from "./tokens" +import { ShadowDomTargetContext } from "./contexts" +import type { Accessor } from "solid-js" + +const stylesFactory = (shadowDOMTarget?: ShadowRoot) => { + const { colors, font, size, alpha, border } = tokens + const { fontFamily, size: fontSize } = font + const css = shadowDOMTarget + ? goober.css.bind({ target: shadowDOMTarget }) + : goober.css + + return { + devtoolsPanelContainer: css` + direction: ltr; + position: fixed; + bottom: 0; + right: 0; + z-index: 99999; + width: 100%; + max-height: 90%; + border-top: 1px solid ${colors.gray[700]}; + transform-origin: top; + `, + devtoolsPanelContainerVisibility: (isOpen: boolean) => { + return css` + visibility: ${isOpen ? `visible` : `hidden`}; + ` + }, + devtoolsPanelContainerResizing: (isResizing: Accessor) => { + if (isResizing()) { + return css` + transition: none; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + ` + } + + return css` + transition: all 0.4s ease; + ` + }, + devtoolsPanelContainerAnimation: (isOpen: boolean, height: number) => { + if (isOpen) { + return css` + pointer-events: auto; + transform: translateY(0); + ` + } + return css` + pointer-events: none; + transform: translateY(${height}px); + ` + }, + logo: css` + cursor: pointer; + display: flex; + flex-direction: column; + background-color: transparent; + border: none; + font-family: ${fontFamily.sans}; + gap: ${tokens.size[0.5]}; + padding: 0px; + &:hover { + opacity: 0.7; + } + &:focus-visible { + outline-offset: 4px; + border-radius: ${border.radius.xs}; + outline: 2px solid ${colors.blue[800]}; + } + `, + tanstackLogo: css` + font-size: ${font.size.md}; + font-weight: ${font.weight.bold}; + line-height: ${font.lineHeight.xs}; + white-space: nowrap; + color: ${colors.gray[300]}; + `, + dbLogo: css` + font-weight: ${font.weight.semibold}; + font-size: ${font.size.xs}; + background: linear-gradient( + to right, + rgb(249, 115, 22), + rgb(194, 65, 12) + ); + background-clip: text; + -webkit-background-clip: text; + line-height: 1; + -webkit-text-fill-color: transparent; + white-space: nowrap; + `, + devtoolsPanel: css` + display: flex; + font-size: ${fontSize.sm}; + font-family: ${fontFamily.sans}; + background-color: ${colors.darkGray[700]}; + color: ${colors.gray[300]}; + flex-grow: 0; + + @media (max-width: 700px) { + flex-direction: column; + } + @media (max-width: 600px) { + font-size: ${fontSize.xs}; + } + `, + dragHandle: css` + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 4px; + cursor: row-resize; + z-index: 100000; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + &:hover { + background-color: ${colors.purple[400]}${alpha[90]}; + } + `, + firstContainer: css` + flex: 0 0 35%; + min-height: 40%; + max-height: 100%; + overflow: auto; + border-right: 1px solid ${colors.gray[700]}; + display: flex; + flex-direction: column; + min-width: 0; /* prevent content from forcing panel expansion */ + `, + secondContainer: css` + flex: 1 1 500px; + min-height: 40%; + max-height: 100%; + overflow: auto; + display: flex; + flex-direction: column; + min-width: 0; /* allow shrinking inside parent */ + `, + collectionsList: css` + overflow-y: auto; + flex: 1; + `, + collectionsHeader: css` + display: flex; + align-items: center; + padding: ${size[2]} ${size[2.5]}; + gap: ${size[2.5]}; + border-bottom: ${colors.darkGray[500]} 1px solid; + align-items: center; + `, + mainCloseBtn: css` + background: ${colors.darkGray[700]}; + padding: ${size[1]} ${size[2]} ${size[1]} ${size[1.5]}; + border-radius: ${border.radius.md}; + position: fixed; + z-index: 99999; + display: inline-flex; + width: fit-content; + cursor: pointer; + appearance: none; + border: 0; + gap: 8px; + align-items: center; + border: 1px solid ${colors.gray[500]}; + font-size: ${font.size.xs}; + cursor: pointer; + transition: all 0.25s ease-out; + + &:hover { + background: ${colors.darkGray[500]}; + } + `, + mainCloseBtnPosition: ( + position: `top-left` | `top-right` | `bottom-left` | `bottom-right` + ) => { + const base = css` + ${position === `top-left` ? `top: ${size[2]}; left: ${size[2]};` : ``} + ${position === `top-right` ? `top: ${size[2]}; right: ${size[2]};` : ``} + ${position === `bottom-left` + ? `bottom: ${size[2]}; left: ${size[2]};` + : ``} + ${position === `bottom-right` + ? `bottom: ${size[2]}; right: ${size[2]};` + : ``} + ` + return base + }, + mainCloseBtnAnimation: (isOpen: boolean) => { + if (!isOpen) { + return css` + opacity: 1; + pointer-events: auto; + visibility: visible; + ` + } + return css` + opacity: 0; + pointer-events: none; + visibility: hidden; + ` + }, + dbLogoCloseButton: css` + font-weight: ${font.weight.semibold}; + font-size: ${font.size.xs}; + background: linear-gradient( + to right, + rgb(249, 115, 22), + rgb(194, 65, 12) + ); + background-clip: text; + -webkit-background-clip: text; + line-height: 1; + -webkit-text-fill-color: transparent; + white-space: nowrap; + `, + mainCloseBtnDivider: css` + width: 1px; + background: ${tokens.colors.gray[600]}; + height: 100%; + border-radius: 999999px; + color: transparent; + `, + mainCloseBtnIconContainer: css` + position: relative; + width: ${size[5]}; + height: ${size[5]}; + background: linear-gradient(45deg, #06b6d4, #3b82f6); + border-radius: 999999px; + overflow: hidden; + `, + mainCloseBtnIconOuter: css` + width: ${size[5]}; + height: ${size[5]}; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + filter: blur(3px) saturate(1.8) contrast(2); + `, + mainCloseBtnIconInner: css` + width: ${size[4]}; + height: ${size[4]}; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + `, + panelCloseBtn: css` + position: absolute; + cursor: pointer; + z-index: 100001; + display: flex; + align-items: center; + justify-content: center; + outline: none; + background-color: ${colors.darkGray[700]}; + &:hover { + background-color: ${colors.darkGray[500]}; + } + + top: 0; + right: ${size[2]}; + transform: translate(0, -100%); + border-right: ${colors.darkGray[300]} 1px solid; + border-left: ${colors.darkGray[300]} 1px solid; + border-top: ${colors.darkGray[300]} 1px solid; + border-bottom: none; + border-radius: ${border.radius.sm} ${border.radius.sm} 0px 0px; + padding: ${size[1]} ${size[1.5]} ${size[0.5]} ${size[1.5]}; + + &::after { + content: " "; + position: absolute; + top: 100%; + left: -${size[2.5]}; + height: ${size[1.5]}; + width: calc(100% + ${size[5]}); + } + `, + panelCloseBtnIcon: css` + color: ${colors.gray[400]}; + width: ${size[2]}; + height: ${size[2]}; + `, + collectionItem: css` + display: flex; + align-items: center; + padding: ${size[2]}; + border-bottom: 1px solid ${colors.gray[700]}; + cursor: pointer; + background-color: ${colors.darkGray[700]}; + transition: all 0.2s ease; + position: relative; + + &:hover { + background-color: ${colors.darkGray[600]}; + } + `, + collectionItemActive: css` + background-color: ${colors.darkGray[600]}; + border-left: 3px solid ${colors.blue[500]}; + `, + activeIndicator: css` + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 3px; + background-color: ${colors.blue[500]}; + pointer-events: none; + `, + collectionName: css` + font-weight: ${font.weight.medium}; + color: ${colors.gray[200]}; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + `, + collectionStatus: css` + font-size: ${fontSize.xs}; + padding: ${size[0.5]} ${size[1]}; + border-radius: ${border.radius.sm}; + font-weight: ${font.weight.medium}; + background-color: ${colors.green[900]}; + color: ${colors.green[300]}; + border: 1px solid ${colors.green[700]}; + `, + collectionStatusError: css` + background-color: ${colors.red[900]}; + color: ${colors.red[300]}; + border: 1px solid ${colors.red[700]}; + `, + collectionCount: css` + font-size: ${fontSize.xs}; + color: ${colors.gray[400]}; + margin-left: ${size[2]}; + `, + collectionStats: css` + display: flex; + gap: ${size[1]}; + font-size: ${fontSize.xs}; + color: ${colors.gray[400]}; + font-variant-numeric: tabular-nums; + line-height: ${font.lineHeight.xs}; + align-items: center; + `, + detailsPanel: css` + display: flex; + flex-direction: column; + background-color: ${colors.darkGray[700]}; + color: ${colors.gray[300]}; + width: 100%; + height: 100%; + overflow: hidden; /* prevent children from expanding container */ + min-width: 0; /* allow shrink inside flex parent */ + `, + detailsHeader: css` + display: flex; + align-items: center; + font-weight: ${font.weight.semibold}; + color: ${colors.gray[200]}; + font-size: ${fontSize.sm}; + `, + transactionHeader: css` + display: flex; + align-items: center; + padding: ${size[1.5]} ${size[2]}; + background-color: ${colors.darkGray[600]}; + border-bottom: 1px solid ${colors.gray[700]}; + font-weight: ${font.weight.semibold}; + color: ${colors.gray[200]}; + font-size: ${fontSize.sm}; + `, + transactionSubHeader: css` + display: flex; + align-items: center; + padding: ${size[1.5]} ${size[2]}; + background-color: ${colors.darkGray[600]}; + border-bottom: 1px solid ${colors.gray[700]}; + font-weight: ${font.weight.semibold}; + color: ${colors.gray[200]}; + font-size: ${fontSize.xs}; + `, + detailsHeaderRow: css` + display: flex; + align-items: center; + justify-content: space-between; + padding: ${size[2]}; + background-color: ${colors.darkGray[600]}; + border-bottom: 1px solid ${colors.gray[700]}; + font-weight: ${font.weight.semibold}; + color: ${colors.gray[200]}; + font-size: ${fontSize.sm}; + width: 100%; + `, + detailsContent: css` + flex: 1; + padding: ${size[2]}; + overflow: hidden; + min-width: 0; + `, + detailsContentNoPadding: css` + flex: 1; + overflow: hidden; /* Let inner table manage scrolling */ + padding: 0; + margin: 0; + min-width: 0; + position: relative; + width: 100%; + height: 100%; + `, + explorerContainer: css` + font-family: ${fontFamily.mono}; + font-size: ${fontSize.xs}; + color: ${colors.gray[300]}; + overflow-y: auto; + `, + row: css` + display: flex; + align-items: center; + padding: ${size[2]} ${size[2.5]}; + gap: ${size[2.5]}; + border-bottom: ${colors.gray[700]} 1px solid; + align-items: center; + `, + headerContainer: css` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: ${size[0.5]} ${size[2]}; + `, + collectionsExplorerContainer: css` + overflow-y: auto; + flex: 1 0; + `, + collectionsExplorer: css` + /* Removed padding to use full width and height */ + `, + collectionGroup: css` + /* Removed margin to eliminate extra spacing */ + `, + collectionGroupHeader: css` + padding: ${size[1.5]} ${size[2]}; + font-size: ${fontSize.xs}; + font-weight: ${font.weight.semibold}; + color: ${colors.gray[400]}; + background: ${colors.darkGray[600]}; + border-bottom: 1px solid ${colors.gray[700]}; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + justify-content: space-between; + align-items: center; + `, + collectionGroupStats: css` + display: flex; + gap: ${size[1]}; + font-size: ${fontSize.xs}; + color: ${colors.gray[500]}; + font-variant-numeric: tabular-nums; + line-height: ${font.lineHeight.xs}; + align-items: center; + font-weight: ${font.weight.normal}; + text-transform: none; + letter-spacing: normal; + `, + tabNav: css` + display: flex; + gap: ${size[1]}; + `, + tabBtn: css` + padding: ${size[1]} ${size[2]}; + background: transparent; + border: 1px solid ${colors.gray[600]}; + border-radius: ${border.radius.sm}; + color: ${colors.gray[400]}; + cursor: pointer; + font-size: ${fontSize.xs}; + font-weight: ${font.weight.medium}; + + &:hover { + background: ${colors.darkGray[500]}; + border-color: ${colors.gray[500]}; + } + `, + tabBtnActive: css` + background: ${colors.blue[500]}; + color: ${colors.white}; + border-color: ${colors.blue[400]}; + + &:hover { + background: ${colors.blue[600]}; + border-color: ${colors.blue[500]}; + } + `, + sidebarContent: css` + flex: 1; + overflow-y: auto; + `, + transactionsExplorer: css` + display: flex; + flex-direction: column; + flex: 1; + `, + noDataMessage: css` + padding: ${size[4]}; + text-align: center; + color: ${colors.gray[500]}; + font-style: italic; + `, + collectionTabNav: css` + display: flex; + gap: ${size[1]}; + `, + collectionTabBtn: css` + padding: ${size[1]} ${size[2]}; + background: transparent; + border: 1px solid ${colors.gray[600]}; + border-radius: ${border.radius.sm}; + color: ${colors.gray[400]}; + cursor: pointer; + font-size: ${fontSize.xs}; + font-weight: ${font.weight.medium}; + position: relative; + + &:hover { + background: ${colors.darkGray[500]}; + border-color: ${colors.gray[500]}; + } + `, + collectionTabBtnActive: css` + background: ${colors.blue[500]} !important; + color: ${colors.white} !important; + border-color: ${colors.blue[400]} !important; + + &:hover { + background: ${colors.blue[600]} !important; + border-color: ${colors.blue[500]} !important; + } + `, + tabBadge: css` + position: absolute; + top: -${size[0.5]}; + right: -${size[0.5]}; + background: ${colors.red[500]}; + color: ${colors.white}; + border-radius: 50%; + width: ${size[3]}; + height: ${size[3]}; + display: flex; + align-items: center; + justify-content: center; + font-size: ${fontSize.xs}; + font-weight: ${font.weight.bold}; + line-height: 1; + `, + splitPanelContainer: css` + display: flex; + width: 100%; + height: 100%; + `, + splitPanelLeft: css` + flex: 0 0 50%; + height: 100%; + border-right: 1px solid ${colors.gray[700]}; + overflow-y: auto; + `, + splitPanelRight: css` + flex: 1; + height: 100%; + overflow-y: auto; + `, + queryStringContainer: css` + padding: ${size[2]}; + background-color: ${colors.darkGray[800]}; + border-radius: ${border.radius.sm}; + border: 1px solid ${colors.gray[700]}; + margin: ${size[2]}; + `, + queryString: css` + font-size: ${fontSize.xs}; + margin: 0 0 ${size[3]} 0; + padding: 0 0 ${size[2]} 0; + border-bottom: 1px solid ${colors.gray[700]}; + background: transparent !important; + + /* Prism.js syntax highlighting styles for dark theme */ + & .token.comment, + & .token.prolog, + & .token.doctype, + & .token.cdata { + color: ${colors.gray[500]}; + } + + & .token.punctuation { + color: ${colors.gray[400]}; + } + + & .token.namespace { + opacity: 0.7; + } + + & .token.property, + & .token.tag, + & .token.boolean, + & .token.number, + & .token.constant, + & .token.symbol, + & .token.deleted { + color: ${colors.blue[400]}; + } + + & .token.selector, + & .token.attr-name, + & .token.string, + & .token.char, + & .token.builtin, + & .token.inserted { + color: ${colors.green[400]}; + } + + & .token.operator, + & .token.entity, + & .token.url, + & .language-css .token.string, + & .style .token.string { + color: ${colors.yellow[400]}; + } + + & .token.atrule, + & .token.attr-value, + & .token.keyword { + color: ${colors.purple[400]}; + } + + & .token.function, + & .token.class-name { + color: ${colors.orange[400]}; + } + + & .token.regex, + & .token.important, + & .token.variable { + color: ${colors.red[400]}; + } + + & .token.important, + & .token.bold { + font-weight: ${font.weight.bold}; + } + + & .token.italic { + font-style: italic; + } + + & .token.entity { + cursor: help; + } + + /* Override Prism.js default styles to match our theme */ + & .token { + font-family: ${fontFamily.mono}; + } + + & pre { + background: transparent !important; + margin: 0; + padding: 0; + } + + & code { + font-family: ${fontFamily.mono}; + font-size: ${fontSize.xs}; + line-height: ${font.lineHeight.md}; + white-space: pre-wrap; + word-break: break-word; + } + `, + queryStringHeader: css` + font-size: ${fontSize.sm}; + font-weight: ${font.weight.semibold}; + color: ${colors.gray[200]}; + margin-bottom: ${size[1]}; + padding-bottom: ${size[1]}; + border-bottom: 1px solid ${colors.gray[700]}; + `, + dataTableContainer: css` + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + min-width: 0; /* allow shrink within flex parent */ + background-color: ${colors.darkGray[700]}; + overflow: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + `, + tableHeaderContainer: css` + height: 32px; + background-color: ${colors.darkGray[600]}; + border-bottom: 1px solid ${colors.gray[700]}; + overflow-x: hidden; /* hide header scrollbar, sync with body */ + overflow-y: hidden; + flex-shrink: 0; + padding: 0; + margin: 0; + width: 100%; + min-width: 0; + `, + tableHeaderCell: css` + display: flex; + align-items: center; + padding: ${size[1]} ${size[2]}; + font-size: ${fontSize.xs}; + font-weight: ${font.weight.semibold}; + color: ${colors.gray[200]}; + background-color: ${colors.darkGray[600]}; + border-right: 1px solid ${colors.gray[700]}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + user-select: none; + `, + tableBodyContainer: css` + flex: 1 1 auto; + overflow-x: auto; + overflow-y: auto; + /* Enable horizontal scroll for wide tables */ + white-space: nowrap; + position: relative; + width: 100%; + min-width: 0; /* prevent flex overflow expanding parent */ + max-height: 100%; /* ensure the body can scroll vertically */ + `, + tableRow: css` + position: relative; + border-bottom: 1px solid ${colors.gray[700]}; + `, + tableCell: css` + display: flex; + align-items: center; + padding: ${size[1]} ${size[2]}; + font-size: ${fontSize.xs}; + font-family: ${fontFamily.mono}; + color: ${colors.gray[300]}; + border-right: 1px solid ${colors.gray[700]}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-height: 32px; + box-sizing: border-box; + background: transparent; + `, + } +} + +export function useStyles() { + const shadowDomTarget = useContext(ShadowDomTargetContext) + const [_styles] = createSignal(stylesFactory(shadowDomTarget)) + return _styles +} diff --git a/packages/db-devtools/src/utils.tsx b/packages/db-devtools/src/utils.tsx new file mode 100644 index 000000000..a2116a068 --- /dev/null +++ b/packages/db-devtools/src/utils.tsx @@ -0,0 +1,102 @@ +import { createSignal, onMount } from "solid-js" +import type { Accessor } from "solid-js" + +export function useIsMounted(): Accessor { + const [isMounted, setIsMounted] = createSignal(false) + + onMount(() => { + setIsMounted(true) + }) + + return isMounted +} + +export function multiSortBy( + items: Array, + sorters: Array<(item: T) => any> +): Array { + return [...items].sort((a, b) => { + for (const sorter of sorters) { + const aVal = sorter(a) + const bVal = sorter(b) + if (aVal < bVal) return -1 + if (aVal > bVal) return 1 + } + return 0 + }) +} + +export function getStatusColor(status: string): string { + switch (status) { + case `active`: + case `success`: + return `green` + case `error`: + case `failed`: + return `red` + case `pending`: + case `loading`: + return `yellow` + case `idle`: + default: + return `gray` + } +} + +export function displayValue(value: any, space?: number): string { + if (typeof value === `string`) { + return JSON.stringify(value) + } + + if (typeof value === `number` || typeof value === `boolean`) { + return String(value) + } + + if (value === null) { + return `null` + } + + if (value === undefined) { + return `undefined` + } + + if (typeof value === `object`) { + return JSON.stringify(value, null, space) + } + + return String(value) +} + +export function formatTimestamp(timestamp: number): string { + const date = new Date(timestamp) + return date.toLocaleTimeString() +} + +export function truncate(str: string, length: number): string { + if (str.length <= length) return str + return str.slice(0, length) + `...` +} + +export function isObject(value: any): value is object { + return value !== null && typeof value === `object` +} + +export function isArray(value: any): value is Array { + return Array.isArray(value) +} + +export function getKeys(obj: any): Array { + if (!isObject(obj)) return [] + return Object.keys(obj) +} + +export function sortBy(items: Array, sorter: (item: T) => any): Array { + return [...items].sort((a, b) => { + const aVal = sorter(a) + const bVal = sorter(b) + + if (aVal < bVal) return -1 + if (aVal > bVal) return 1 + return 0 + }) +} diff --git a/packages/db-devtools/src/utils/dataFormatting.ts b/packages/db-devtools/src/utils/dataFormatting.ts new file mode 100644 index 000000000..9649b0420 --- /dev/null +++ b/packages/db-devtools/src/utils/dataFormatting.ts @@ -0,0 +1,99 @@ +/** + * Formats a value for display in the data table + * - Primitives are displayed as-is + * - Objects and arrays are converted to short representations (max 40 chars) + * - Long strings are truncated + * - Date objects show the full ISO string + */ +export function formatValueForTable(value: any): string { + if (value === null) return `null` + if (value === undefined) return `undefined` + + const type = typeof value + + switch (type) { + case `string`: + return value.length > 40 ? value.substring(0, 37) + `...` : value + case `number`: + case `boolean`: + return String(value) + case `object`: + if (value instanceof Date) { + return value.toISOString() + } + if (Array.isArray(value)) { + const arrayStr = `[${value.length} items]` + return arrayStr.length > 40 + ? arrayStr.substring(0, 37) + `...` + : arrayStr + } else { + const keys = Object.keys(value) + const objectStr = `{${keys.length} keys}` + return objectStr.length > 40 + ? objectStr.substring(0, 37) + `...` + : objectStr + } + default: + return String(value) + } +} + +/** + * Gets the full value for tooltip display + */ +export function getFullValue(value: any): string { + if (value === null) return `null` + if (value === undefined) return `undefined` + + const type = typeof value + + switch (type) { + case `string`: + return value + case `number`: + case `boolean`: + return String(value) + case `object`: + if (value instanceof Date) { + return value.toISOString() + } + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } + default: + return String(value) + } +} + +/** + * Extracts all unique keys from an array of objects + */ +export function extractKeysFromData(data: Array): Array { + const keys = new Set() + + for (const item of data) { + if (!item || typeof item !== `object`) continue + + // Try multiple strategies to handle proxies and non-plain objects + try { + const own = Reflect.ownKeys(item) + for (const key of own) { + if (typeof key === `string`) keys.add(key) + } + } catch {} + + try { + Object.getOwnPropertyNames(item).forEach((k) => keys.add(k)) + } catch {} + + // Fallback to enumerable keys (works well with many proxies) + + for (const k in item) { + keys.add(k) + } + } + + return Array.from(keys).sort() +} diff --git a/packages/db-devtools/src/utils/formatTime.ts b/packages/db-devtools/src/utils/formatTime.ts new file mode 100644 index 000000000..0e591e07d --- /dev/null +++ b/packages/db-devtools/src/utils/formatTime.ts @@ -0,0 +1,20 @@ +export function formatTime(ms: number): string { + if (ms === 0) return `0s` + + const units = [`s`, `min`, `h`, `d`] + const values = [ms / 1000, ms / 60000, ms / 3600000, ms / 86400000] + + let chosenUnitIndex = 0 + for (let i = 1; i < values.length; i++) { + if (values[i]! < 1) break + chosenUnitIndex = i + } + + const formatter = new Intl.NumberFormat(navigator.language, { + compactDisplay: `short`, + notation: `compact`, + maximumFractionDigits: 0, + }) + + return formatter.format(values[chosenUnitIndex]!) + units[chosenUnitIndex] +} diff --git a/packages/db-devtools/src/utils/index.ts b/packages/db-devtools/src/utils/index.ts new file mode 100644 index 000000000..1e7072760 --- /dev/null +++ b/packages/db-devtools/src/utils/index.ts @@ -0,0 +1 @@ +export { formatTime } from "./formatTime" diff --git a/packages/db-devtools/src/utils/queryToString.ts b/packages/db-devtools/src/utils/queryToString.ts new file mode 100644 index 000000000..61d943f4b --- /dev/null +++ b/packages/db-devtools/src/utils/queryToString.ts @@ -0,0 +1,168 @@ +/** + * Converts Query IR back to a string representation that looks like the original query builder code + */ + +export function convertQueryIRToString(queryIR: any): string { + if (!queryIR) return `No query available` + + const lines: Array = [`query`] + + // FROM clause + if (queryIR.from) { + const fromClause = convertFromToString(queryIR.from) + lines.push(` .from(${fromClause})`) + } else { + return `Invalid query: missing FROM clause` + } + + // JOIN clauses + if (queryIR.join && queryIR.join.length > 0) { + for (const join of queryIR.join) { + const joinClause = convertJoinToString(join) + lines.push(` .${join.type}Join(${joinClause})`) + } + } + + // WHERE clauses + if (queryIR.where && queryIR.where.length > 0) { + for (const where of queryIR.where) { + const whereClause = convertExpressionToString(where) + lines.push(` .where(({${getTableAliases(queryIR)}}) => ${whereClause})`) + } + } + + // GROUP BY clause + if (queryIR.groupBy && queryIR.groupBy.length > 0) { + const groupByClauses = queryIR.groupBy.map((gb: any) => + convertExpressionToString(gb) + ) + lines.push( + ` .groupBy(({${getTableAliases(queryIR)}}) => [${groupByClauses.join(`, `)}])` + ) + } + + // HAVING clauses + if (queryIR.having && queryIR.having.length > 0) { + for (const having of queryIR.having) { + const havingClause = convertExpressionToString(having) + lines.push( + ` .having(({${getTableAliases(queryIR)}}) => ${havingClause})` + ) + } + } + + // ORDER BY clause + if (queryIR.orderBy && queryIR.orderBy.length > 0) { + const orderByClauses = queryIR.orderBy.map((ob: any) => { + const expr = convertExpressionToString(ob.expression) + const direction = ob.compareOptions?.direction || `asc` + return `${expr}, '${direction}'` + }) + lines.push( + ` .orderBy(({${getTableAliases(queryIR)}}) => [${orderByClauses.join(`, `)}])` + ) + } + + // LIMIT clause + if (queryIR.limit !== undefined) { + lines.push(` .limit(${queryIR.limit})`) + } + + // OFFSET clause + if (queryIR.offset !== undefined) { + lines.push(` .offset(${queryIR.offset})`) + } + + // SELECT clause + if (queryIR.select) { + const selectClauses = Object.entries(queryIR.select).map( + ([alias, expr]: [string, any]) => { + const expression = convertExpressionToString(expr) + return `${alias}: ${expression}` + } + ) + lines.push(` .select(({${getTableAliases(queryIR)}}) => ({`) + lines.push(` ${selectClauses.join(`,\n `)}`) + lines.push(` }))`) + } + + // DISTINCT clause + if (queryIR.distinct) { + lines.push(` .distinct()`) + } + + return lines.join(`\n`) +} + +function convertFromToString(from: any): string { + if (from.type === `collectionRef`) { + return `{ ${from.alias}: /* ${from.collection.id} */ }` + } else if (from.type === `queryRef`) { + // TODO: add support for subqueries, recursively convert the subquery + return `{ ${from.alias}: subquery }` + } + return `unknown` +} + +function convertJoinToString(join: any): string { + const fromClause = convertFromToString(join.from) + const leftExpr = convertExpressionToString(join.left) + const rightExpr = convertExpressionToString(join.right) + return `${fromClause}, ({${getJoinAliases(join)}}) => eq(${leftExpr}, ${rightExpr})` +} + +function convertExpressionToString(expr: any): string { + if (!expr) return `undefined` + + switch (expr.type) { + case `ref`: + return expr.path.join(`.`) + case `val`: + if (typeof expr.value === `string`) { + return `'${expr.value}'` + } else if (typeof expr.value === `boolean`) { + return expr.value ? `true` : `false` + } else if (expr.value === null) { + return `null` + } else if (expr.value === undefined) { + return `undefined` + } + return String(expr.value) + case `func`: { + const args = expr.args.map((arg: any) => convertExpressionToString(arg)) + return `${expr.name}(${args.join(`, `)})` + } + case `agg`: { + const aggArgs = expr.args.map((arg: any) => + convertExpressionToString(arg) + ) + return `${expr.name}(${aggArgs.join(`, `)})` + } + default: + return `unknown` + } +} + +function getTableAliases(queryIR: any): string { + const aliases: Array = [] + + // Add FROM alias + if (queryIR.from) { + aliases.push(queryIR.from.alias) + } + + // Add JOIN aliases + if (queryIR.join) { + for (const join of queryIR.join) { + aliases.push(join.from.alias) + } + } + + return aliases.join(`, `) +} + +function getJoinAliases(join: any): string { + // For joins, we need to include both the joined table and any existing aliases + // This is a simplified version - in practice we'd need to track all available aliases + return `${join.from.alias}` +} diff --git a/packages/db-devtools/tsconfig.json b/packages/db-devtools/tsconfig.json new file mode 100644 index 000000000..e98dd302f --- /dev/null +++ b/packages/db-devtools/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "declaration": true, + "declarationMap": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM"] + }, + "include": ["src/**/*", "*.config.ts"], + "exclude": ["build", "dist", "node_modules"] +} diff --git a/packages/db-devtools/tsconfig.prod.json b/packages/db-devtools/tsconfig.prod.json new file mode 100644 index 000000000..961414205 --- /dev/null +++ b/packages/db-devtools/tsconfig.prod.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "target": "ES2021", + "lib": ["ES2021", "DOM"] + }, + "exclude": ["src/__tests__", "**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/db-devtools/tsup.config.ts b/packages/db-devtools/tsup.config.ts new file mode 100644 index 000000000..ab62ae712 --- /dev/null +++ b/packages/db-devtools/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: [`src/index.ts`], + format: [`esm`, `cjs`], + dts: true, + sourcemap: true, + clean: true, + outDir: `build`, + external: [`solid-js`, `solid-js/web`, `@tanstack/db`], + esbuildOptions(options) { + // Use SolidJS-compatible JSX settings + options.jsx = `automatic` + options.jsxImportSource = `solid-js` + // Add SolidJS runtime helpers + options.banner = { + js: `import{template as _$template,delegateEvents as _$delegateEvents,addEventListener as _$addEventListener,classList as _$classList,style as _$style,setAttribute as _$setAttribute,setProperty as _$setProperty,className as _$className,textContent as _$textContent,innerHTML as _$innerHTML}from"solid-js/web";`, + } + }, +}) diff --git a/packages/db-devtools/vite.config.ts b/packages/db-devtools/vite.config.ts new file mode 100644 index 000000000..7ff1ef8c4 --- /dev/null +++ b/packages/db-devtools/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vite" +import solid from "vite-plugin-solid" +import dts from "vite-plugin-dts" + +export default defineConfig({ + plugins: [ + solid(), + dts({ + insertTypesEntry: true, + include: [`src/**/*`], + exclude: [`src/**/*.test.*`, `src/__tests__/**/*`], + }), + ], + build: { + target: `esnext`, + lib: { + entry: `src/index.ts`, + formats: [`es`, `cjs`], + fileName: (format) => `index.${format === `es` ? `js` : `cjs`}`, + }, + rollupOptions: { + external: [`solid-js`, `solid-js/web`, `@tanstack/db`], + }, + }, +}) diff --git a/packages/db/src/collection.ts b/packages/db/src/collection.ts index eea7905cd..d1ab37301 100644 --- a/packages/db/src/collection.ts +++ b/packages/db/src/collection.ts @@ -62,6 +62,49 @@ import type { import type { IndexOptions } from "./indexes/index-options.js" import type { BaseIndex, IndexResolver } from "./indexes/base-index.js" +// Check for devtools registry and register collection if available +function registerWithDevtools(collection: CollectionImpl): void { + // Skip registration if this is a devtools internal collection + if (collection.config.__devtoolsInternal) { + return + } + + // Skip if already registered + if ((collection as any).isRegisteredWithDevtools) { + return + } + + if (typeof window !== `undefined`) { + const devtools = window.__TANSTACK_DB_DEVTOOLS__ as any + if (devtools?.registerCollection) { + const updateCallback = devtools.registerCollection(collection) + if (updateCallback) { + ;(collection as any).__devtoolsUpdateCallback = updateCallback + ;(collection as any).isRegisteredWithDevtools = true + } else { + ;(collection as any).isRegisteredWithDevtools = false + } + } else { + ;(collection as any).isRegisteredWithDevtools = false + } + } +} + +// Helper function to trigger devtools updates +function triggerDevtoolsUpdate( + collection: CollectionImpl +): void { + if (typeof window !== `undefined`) { + const updateCallback = (collection as any).__devtoolsUpdateCallback + if (typeof updateCallback === `function`) { + updateCallback() + } + } +} + +// Import global devtools types (will be available when devtools are loaded) +// The actual types are declared in @tanstack/db-devtools + interface PendingSyncedTransaction> { committed: boolean operations: Array> @@ -331,6 +374,7 @@ export class CollectionImpl< private gcTimeoutId: ReturnType | null = null private preloadPromise: Promise | null = null private syncCleanupFn: (() => void) | null = null + private isRegisteredWithDevtools = false /** * Register a callback to be executed when the collection first becomes ready @@ -462,6 +506,9 @@ export class CollectionImpl< this.validateStatusTransition(this._status, newStatus) this._status = newStatus + // Trigger devtools update when status changes + triggerDevtoolsUpdate(this) + // Resolve indexes when collection becomes ready if (newStatus === `ready` && !this.isIndexesResolved) { // Resolve indexes asynchronously without blocking @@ -510,6 +557,9 @@ export class CollectionImpl< this.syncedData = new Map() } + // Register with devtools if available + registerWithDevtools(this) + // Only start sync immediately if explicitly enabled if (config.startSync === true) { this.startSync() @@ -681,6 +731,15 @@ export class CollectionImpl< }) } + // Unregister from devtools if available + if ( + typeof window !== `undefined` && + window.__TANSTACK_DB_DEVTOOLS__?.unregisterCollection + ) { + window.__TANSTACK_DB_DEVTOOLS__.unregisterCollection(this.id) + this.isRegisteredWithDevtools = false + } + // Clear data this.syncedData.clear() this.syncedMetadata.clear() @@ -737,6 +796,11 @@ export class CollectionImpl< this.activeSubscribersCount++ this.cancelGCTimer() + // Re-register with devtools if not already registered (handles timing issues) + if (!this.isRegisteredWithDevtools) { + registerWithDevtools(this) + } + // Start sync if collection was cleaned up if (this._status === `cleaned-up` || this._status === `idle`) { this.startSync() @@ -872,6 +936,9 @@ export class CollectionImpl< // Emit all events if no pending sync transactions this.emitEvents(filteredEventsBySyncStatus, triggeredByUserAction) } + + // Trigger devtools update after optimistic state changes + triggerDevtoolsUpdate(this) } /** @@ -1347,6 +1414,9 @@ export class CollectionImpl< this.onFirstReadyCallbacks = [] callbacks.forEach((callback) => callback()) } + + // Trigger devtools update after sync operations + triggerDevtoolsUpdate(this) } } @@ -1355,6 +1425,17 @@ export class CollectionImpl< * @private */ private scheduleTransactionCleanup(transaction: Transaction): void { + // Skip cleanup for devtools internal collections + if (this.config.__devtoolsInternal) { + return + } + + // Register transaction with devtools + if (typeof window !== `undefined`) { + const devtools = window.__TANSTACK_DB_DEVTOOLS__ + devtools?.store?.registerTransaction?.(transaction, this.id) + } + // Only schedule cleanup for transactions that aren't already completed if (transaction.state === `completed`) { this.transactions.delete(transaction.id) @@ -2369,5 +2450,16 @@ export class CollectionImpl< this.capturePreSyncVisibleState() this.recomputeOptimisticState(false) + + // Trigger devtools update after transaction state changes + triggerDevtoolsUpdate(this) + + // Also trigger transaction list/state refresh in devtools + if (typeof window !== `undefined`) { + const devtools = window.__TANSTACK_DB_DEVTOOLS__ as any + try { + devtools?.updateTransactions?.(this.id) + } catch {} + } } } diff --git a/packages/db/src/devtools-globals.d.ts b/packages/db/src/devtools-globals.d.ts new file mode 100644 index 000000000..6dd23400a --- /dev/null +++ b/packages/db/src/devtools-globals.d.ts @@ -0,0 +1,25 @@ +// Ambient declaration to provide the global devtools type for the db package +// This avoids depending on the db-devtools package while allowing typed access + +export {} + +declare global { + interface Window { + // Minimal subset needed by the core db package + __TANSTACK_DB_DEVTOOLS__?: { + registerCollection?: (collection: any) => (() => void) | undefined + unregisterCollection?: (id: string) => void + registerTransaction?: (transaction: any, collectionId: string) => void + updateTransactions?: (collectionId?: string) => void + store?: { + registerTransaction?: (transaction: any, collectionId: string) => void + } + } + + // Queue used before devtools initialize (read/written by core) + __TANSTACK_DB_PENDING_TRANSACTIONS__?: Array<{ + transaction: any + collectionId: string + }> + } +} diff --git a/packages/db/src/local-only.ts b/packages/db/src/local-only.ts index d6590c610..b37bf15de 100644 --- a/packages/db/src/local-only.ts +++ b/packages/db/src/local-only.ts @@ -38,6 +38,12 @@ export interface LocalOnlyCollectionConfig< schema?: TSchema getKey: (item: ResolveType) => TKey + /** + * Internal flag to prevent devtools registration for devtools-owned collections + * @internal + */ + __devtoolsInternal?: boolean + /** * Optional initial data to populate the collection with on creation * This data will be applied during the initial sync process @@ -216,6 +222,7 @@ export function localOnlyCollectionOptions< utils: {} as LocalOnlyCollectionUtils, startSync: true, gcTime: 0, + collectionType: `local-only` as const, } } diff --git a/packages/db/src/local-storage.ts b/packages/db/src/local-storage.ts index 43dfc5afa..2564e2d2d 100644 --- a/packages/db/src/local-storage.ts +++ b/packages/db/src/local-storage.ts @@ -440,6 +440,7 @@ export function localStorageCollectionOptions< clearStorage, getStorageSize, }, + collectionType: `local-storage` as const, } } diff --git a/packages/db/src/proxy.ts b/packages/db/src/proxy.ts index 2c0050b1a..253245164 100644 --- a/packages/db/src/proxy.ts +++ b/packages/db/src/proxy.ts @@ -159,7 +159,12 @@ function deepClone( /** * Deep equality check that handles special types like Date, RegExp, Map, and Set */ -function deepEqual(a: T, b: T): boolean { +function deepEqual( + a: T, + b: T, + // Track visited object pairs to avoid infinite recursion on cyclic structures + visited: WeakMap> = new WeakMap() +): boolean { // Handle primitive types if (a === b) return true @@ -173,6 +178,25 @@ function deepEqual(a: T, b: T): boolean { return false } + // Before descending into object comparisons, guard against cycles by + // memoizing object pairs that have already been compared + const objA = a as unknown as object + const objB = b as unknown as object + const isObjA = typeof a === `object` + const isObjB = typeof b === `object` + + if (isObjA && isObjB) { + const seenForA = visited.get(objA) + if (seenForA && seenForA.has(objB)) { + return true + } + const setForA = seenForA ?? new Set() + setForA.add(objB) + if (!seenForA) { + visited.set(objA, setForA) + } + } + // Handle Date objects if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime() @@ -189,7 +213,7 @@ function deepEqual(a: T, b: T): boolean { const entries = Array.from(a.entries()) for (const [key, val] of entries) { - if (!b.has(key) || !deepEqual(val, b.get(key))) { + if (!b.has(key) || !deepEqual(val, b.get(key), visited)) { return false } } @@ -220,7 +244,7 @@ function deepEqual(a: T, b: T): boolean { if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { - if (!deepEqual(a[i], b[i])) return false + if (!deepEqual(a[i], b[i], visited)) return false } return true @@ -253,7 +277,7 @@ function deepEqual(a: T, b: T): boolean { return keysA.every( (key) => Object.prototype.hasOwnProperty.call(b, key) && - deepEqual((a as any)[key], (b as any)[key]) + deepEqual((a as any)[key], (b as any)[key], visited) ) } diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index 364670b49..84f3e1273 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -33,6 +33,8 @@ export interface CompilationResult { pipeline: ResultStream /** Map of collection aliases to their WHERE clauses for index optimization */ collectionWhereClauses: Map> + /** The optimized query IR (for devtools) */ + optimizedQueryIR: QueryIR } /** @@ -251,6 +253,7 @@ export function compileQuery( const compilationResult = { pipeline: result, collectionWhereClauses, + optimizedQueryIR: query, } cache.set(rawQuery, compilationResult) @@ -277,6 +280,7 @@ export function compileQuery( const compilationResult = { pipeline: result, collectionWhereClauses, + optimizedQueryIR: query, } cache.set(rawQuery, compilationResult) diff --git a/packages/db/src/query/live-query-collection.ts b/packages/db/src/query/live-query-collection.ts index 9d7877f5c..c9219a7a5 100644 --- a/packages/db/src/query/live-query-collection.ts +++ b/packages/db/src/query/live-query-collection.ts @@ -88,6 +88,12 @@ export interface LiveQueryCollectionConfig< * GC time for the collection */ gcTime?: number + + /** + * Marks this live query as internal to devtools so it won't register itself + * with the devtools registry (prevents circular references in the UI) + */ + __devtoolsInternal?: boolean } /** @@ -175,6 +181,7 @@ export function liveQueryCollectionOptions< let collectionWhereClausesCache: | Map> | undefined + let optimizedQueryIRCache: any | undefined const compileBasePipeline = () => { graphCache = new D2() @@ -189,6 +196,7 @@ export function liveQueryCollectionOptions< ;({ pipeline: pipelineCache, collectionWhereClauses: collectionWhereClausesCache, + optimizedQueryIR: optimizedQueryIRCache, } = compileQuery(query, inputsCache as Record)) } @@ -383,6 +391,15 @@ export function liveQueryCollectionOptions< onUpdate: config.onUpdate, onDelete: config.onDelete, startSync: config.startSync, + // Mark as live query for devtools + collectionType: `live-query` as const, + // Propagate devtools-internal marker to avoid self-registration + __devtoolsInternal: config.__devtoolsInternal, + // Store query IR for devtools access + __devtoolsQueryIR: { + unoptimized: query, + optimized: optimizedQueryIRCache, + }, } } diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index 28b25e0ef..e3ff8bdba 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -397,6 +397,24 @@ export interface CollectionConfig< * compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime() */ compare?: (x: T, y: T) => number + /** + * Collection type for devtools grouping and identification + * @internal + */ + collectionType?: string + /** + * Internal flag to prevent devtools registration for devtools-owned collections + * @internal + */ + __devtoolsInternal?: boolean + /** + * Internal query IR storage for devtools access + * @internal + */ + __devtoolsQueryIR?: { + unoptimized: any + optimized: any + } /** * Optional asynchronous handler function called before an insert operation * @param params Object containing transaction and collection information diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 23bb3f28c..376b90610 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "declaration": true, @@ -17,6 +17,6 @@ "@tanstack/db-ivm": ["../db-ivm/src"] } }, - "include": ["src", "tests", "vite.config.ts"], + "include": ["src", "tests", "vite.config.ts", "../../shared/**/*.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index d80f46bcd..ce7a0a34a 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -423,6 +423,7 @@ export function electricCollectionOptions< utils: { awaitTxId, }, + collectionType: `electric` as const, } } diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 1a3074fc9..7f3653a16 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -333,6 +333,7 @@ export function queryCollectionOptions< config: QueryCollectionConfig ): CollectionConfig & { utils: QueryCollectionUtils + collectionType: `query` } { const { queryKey, @@ -594,5 +595,6 @@ export function queryCollectionOptions< refetch, ...writeUtils, }, + collectionType: `query` as const, } } diff --git a/packages/react-db-devtools/package.json b/packages/react-db-devtools/package.json new file mode 100644 index 000000000..7984acf6d --- /dev/null +++ b/packages/react-db-devtools/package.json @@ -0,0 +1,98 @@ +{ + "name": "@tanstack/react-db-devtools", + "version": "0.0.1", + "description": "React wrapper for TanStack DB Devtools", + "author": "tanstack", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/db.git", + "directory": "packages/react-db-devtools" + }, + "homepage": "https://tanstack.com/db", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "type": "module", + "types": "build/legacy/index.d.ts", + "main": "build/legacy/index.cjs", + "module": "build/legacy/index.js", + "exports": { + ".": { + "@tanstack/custom-condition": "./src/index.ts", + "import": { + "types": "./build/modern/index.d.ts", + "default": "./build/modern/index.js" + }, + "require": { + "types": "./build/modern/index.d.cts", + "default": "./build/modern/index.cjs" + } + }, + "./production": { + "import": { + "types": "./build/modern/production.d.ts", + "default": "./build/modern/production.js" + }, + "require": { + "types": "./build/modern/production.d.cts", + "default": "./build/modern/production.cjs" + } + }, + "./build/modern/production.js": { + "import": { + "types": "./build/modern/production.d.ts", + "default": "./build/modern/production.js" + }, + "require": { + "types": "./build/modern/production.d.cts", + "default": "./build/modern/production.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "build", + "src", + "!src/__tests__" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "clean": "premove ./build ./coverage ./dist-ts", + "compile": "tsc --build", + "test:eslint": "eslint ./src", + "test:types": "npm-run-all --serial test:types:*", + "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", + "test:types:tscurrent": "tsc --build", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict && attw --pack", + "lint": "eslint . --fix", + "build": "tsup --tsconfig tsconfig.prod.json", + "build:dev": "tsup --watch" + }, + "dependencies": { + "@tanstack/db-devtools": "workspace:*", + "solid-js": "^1.9.5" + }, + "devDependencies": { + "@tanstack/react-db": "workspace:*", + "@testing-library/react": "^16.1.0", + "@types/react": "^19.0.1", + "@vitejs/plugin-react": "^4.3.4", + "npm-run-all2": "^5.0.0", + "react": "^19.0.0" + }, + "peerDependencies": { + "@tanstack/react-db": "workspace:^", + "react": "^18 || ^19" + } +} diff --git a/packages/react-db-devtools/src/ReactDbDevtools.tsx b/packages/react-db-devtools/src/ReactDbDevtools.tsx new file mode 100644 index 000000000..1214ee654 --- /dev/null +++ b/packages/react-db-devtools/src/ReactDbDevtools.tsx @@ -0,0 +1,103 @@ +"use client" +import { useEffect, useRef, useState } from "react" +import { TanstackDbDevtools } from "@tanstack/db-devtools" +import { initializeDbDevtools } from "./index" +import type { TanstackDbDevtoolsConfig } from "@tanstack/db-devtools" + +export interface TanStackReactDbDevtoolsProps extends TanstackDbDevtoolsConfig { + // Additional React-specific props if needed +} + +export function TanStackReactDbDevtools( + props: TanStackReactDbDevtoolsProps = {} +) { + const ref = useRef(null) + const [devtools, setDevtools] = useState(null) + const initializingRef = useRef(false) + + // Initialize devtools only on client side + useEffect(() => { + if ( + typeof window === `undefined` || + !ref.current || + initializingRef.current + ) { + return + } + + // Set flag to prevent multiple initializations + initializingRef.current = true + + // Note: Devtools registry is now initialized in collections.ts before collections are created + initializeDbDevtools() + const devtoolsInstance = new TanstackDbDevtools(props) + + try { + // Mount the devtools to the DOM element + devtoolsInstance.mount(ref.current) + setDevtools(devtoolsInstance) + } catch { + initializingRef.current = false // Reset flag on error + } + + return () => { + try { + // Only unmount if the devtools were successfully mounted + devtoolsInstance.unmount() + } catch { + // Ignore unmount errors if devtools weren't mounted + } + initializingRef.current = false + } + }, []) // Empty dependency array to prevent remounting + + // Update devtools when props change + useEffect(() => { + if (!devtools) return + + if (props.initialIsOpen !== undefined) { + devtools.setInitialIsOpen(props.initialIsOpen) + } + + if (props.position !== undefined) { + devtools.setPosition(props.position) + } + + if (props.panelProps !== undefined) { + devtools.setPanelProps(props.panelProps) + } + + if (props.toggleButtonProps !== undefined) { + devtools.setToggleButtonProps(props.toggleButtonProps) + } + + if (props.closeButtonProps !== undefined) { + devtools.setCloseButtonProps(props.closeButtonProps) + } + + if (props.storageKey !== undefined) { + devtools.setStorageKey(props.storageKey) + } + + if (props.panelState !== undefined) { + devtools.setPanelState(props.panelState) + } + + if (props.onPanelStateChange !== undefined) { + devtools.setOnPanelStateChange(props.onPanelStateChange) + } + }, [ + devtools, + props.initialIsOpen, + props.position, + props.panelProps, + props.toggleButtonProps, + props.closeButtonProps, + props.storageKey, + props.panelState, + props.onPanelStateChange, + ]) + + // Render a container div for the devtools + return
+} diff --git a/packages/react-db-devtools/src/ReactDbDevtoolsPanel.tsx b/packages/react-db-devtools/src/ReactDbDevtoolsPanel.tsx new file mode 100644 index 000000000..23608dc27 --- /dev/null +++ b/packages/react-db-devtools/src/ReactDbDevtoolsPanel.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useRef, useState } from "react" + +export interface ReactDbDevtoolsPanelOptions { + // Additional React-specific props if needed +} + +export const TanStackReactDbDevtoolsPanel: React.FC< + ReactDbDevtoolsPanelOptions +> = (props): React.ReactElement | null => { + const { ...rest } = props + + // SSR safety check - return null during SSR + if (typeof window === `undefined`) { + return null + } + + const [isInitialized, setIsInitialized] = useState(false) + const [error, setError] = useState(null) + const [registry, setRegistry] = useState(null) + const containerRef = useRef(null) + const solidRootRef = useRef(null) + + useEffect(() => { + // Only initialize on the client side + if (typeof window === `undefined`) return + + async function init() { + try { + // Dynamically import the devtools to avoid SSR issues + const { getDevtoolsRegistry } = await import(`@tanstack/db-devtools`) + + // Get the existing registry (should already be initialized) + const existingRegistry = getDevtoolsRegistry() + + if (!existingRegistry) { + throw new Error( + `DB devtools registry not found. Make sure initializeDbDevtools() was called.` + ) + } + + setRegistry(existingRegistry) + setIsInitialized(true) + } catch (initError) { + console.error(`Failed to initialize DB devtools:`, initError) + setError( + initError instanceof Error ? initError.message : `Unknown error` + ) + } + } + + init() + }, []) // Empty dependency array to run only once + + useEffect(() => { + if (!isInitialized || !registry || !containerRef.current) return + + async function mountSolidComponent() { + try { + // Import SolidJS render and the base panel component + const { render } = await import(`solid-js/web`) + const { BaseTanStackDbDevtoolsPanel } = await import( + `@tanstack/db-devtools` + ) + + // Clean up any existing component + if (solidRootRef.current) { + solidRootRef.current() + solidRootRef.current = null + } + + // Create a SolidJS component that renders the base panel + const SolidComponent = () => { + // Filter out React-specific props that might not be compatible + const { children: _children, ...solidProps } = rest as any + return BaseTanStackDbDevtoolsPanel({ + registry: () => registry, + style: () => ({ + height: `100%`, + width: `100%`, + display: `flex`, + flexDirection: `column`, + overflow: `hidden`, + }), + ...solidProps, + }) + } + + // Render the SolidJS component into the container + const dispose = render(SolidComponent, containerRef.current!) + solidRootRef.current = dispose + } catch (mountError) { + console.error(`Failed to mount SolidJS component:`, mountError) + setError( + mountError instanceof Error ? mountError.message : `Unknown error` + ) + } + } + + mountSolidComponent() + + // Cleanup function + return () => { + if (solidRootRef.current) { + try { + solidRootRef.current() + } catch (disposeError) { + console.error(`Error disposing SolidJS component:`, disposeError) + } + solidRootRef.current = null + } + } + }, [isInitialized, registry, rest]) + + // Don't render anything until we're on the client + if (typeof window === `undefined`) { + return null + } + + // Show error state if initialization failed + if (error) { + return ( +
+

Failed to load DB Devtools

+

Error: {error}

+
+ ) + } + + // Show loading state while initializing + if (!isInitialized || !registry) { + return ( +
+

Loading DB Devtools...

+
+ ) + } + + // Render a container div that the SolidJS component will be mounted into + // Use flexbox to ensure it takes up the full available height + return ( +
+ ) +} diff --git a/packages/react-db-devtools/src/index.ts b/packages/react-db-devtools/src/index.ts new file mode 100644 index 000000000..4649cb366 --- /dev/null +++ b/packages/react-db-devtools/src/index.ts @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" + +export { TanStackReactDbDevtoolsPanel } from "./ReactDbDevtoolsPanel" + +function DevtoolsWrapper(props: any) { + const [isClient, setIsClient] = React.useState(false) + const [DevtoolsComponent, setDevtoolsComponent] = + React.useState | null>(null) + + React.useEffect(() => { + // Only run on client after hydration + setIsClient(true) + + // Dynamically import the devtools component + import(`./ReactDbDevtools`).then((module) => { + setDevtoolsComponent(() => module.TanStackReactDbDevtools) + }) + }, []) + + // Always render null during SSR and initial client render to prevent hydration mismatch + if (!isClient || !DevtoolsComponent) { + return null + } + + return React.createElement(DevtoolsComponent, props) +} + +// Follow TanStack Query devtools pattern exactly +export const TanStackReactDbDevtools: React.ComponentType = + typeof window === `undefined` || process.env.NODE_ENV !== `development` + ? () => null + : DevtoolsWrapper + +export type { TanStackReactDbDevtoolsProps } from "./ReactDbDevtools" + +// SSR-safe initialization function - just call the core devtools +export function initializeDbDevtools(): void { + // SSR safety check + if (typeof window === `undefined`) { + return + } + + // Just import and call the core devtools initialization + import(`@tanstack/db-devtools`) + .then((module) => { + module.initializeDbDevtools() + }) + .catch(() => { + // Silently fail if core devtools module can't be loaded + }) +} diff --git a/packages/react-db-devtools/tsconfig.json b/packages/react-db-devtools/tsconfig.json new file mode 100644 index 000000000..ea37177e5 --- /dev/null +++ b/packages/react-db-devtools/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2020" + }, + "include": ["src/**/*", "*.config.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/react-db-devtools/tsconfig.prod.json b/packages/react-db-devtools/tsconfig.prod.json new file mode 100644 index 000000000..829981cef --- /dev/null +++ b/packages/react-db-devtools/tsconfig.prod.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2020" + }, + "exclude": ["src/__tests__", "**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/react-db-devtools/tsup.config.ts b/packages/react-db-devtools/tsup.config.ts new file mode 100644 index 000000000..c9becf00f --- /dev/null +++ b/packages/react-db-devtools/tsup.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from "tsup" + +export default defineConfig([ + { + entry: [`src/*.ts`, `src/*.tsx`], + format: [`esm`, `cjs`], + dts: true, + sourcemap: true, + clean: true, + external: [ + `react`, + `react-dom`, + `@tanstack/react-db`, + `@tanstack/db-devtools`, + ], + outDir: `build/modern`, + }, + { + entry: [`src/*.ts`, `src/*.tsx`], + format: [`esm`, `cjs`], + dts: true, + sourcemap: true, + clean: false, + external: [ + `react`, + `react-dom`, + `@tanstack/react-db`, + `@tanstack/db-devtools`, + ], + outDir: `build/legacy`, + }, +]) diff --git a/packages/react-db/tsconfig.json b/packages/react-db/tsconfig.json index feba9330f..0ef350e28 100644 --- a/packages/react-db/tsconfig.json +++ b/packages/react-db/tsconfig.json @@ -17,6 +17,6 @@ "@tanstack/db-ivm": ["../db-ivm/src"] } }, - "include": ["src/**/*", "tests", "vite.config.ts"], + "include": ["src/**/*", "tests", "vite.config.ts", "../../shared/**/*.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/packages/solid-db/tsconfig.json b/packages/solid-db/tsconfig.json index 8f54dac68..1bc643818 100644 --- a/packages/solid-db/tsconfig.json +++ b/packages/solid-db/tsconfig.json @@ -18,6 +18,6 @@ "@tanstack/db-ivm": ["../db-ivm/src"] } }, - "include": ["src/**/*", "tests", "vite.config.ts"], + "include": ["src/**/*", "tests", "vite.config.ts", "../../shared/**/*.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index 1d1e982fb..50c137174 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -1044,7 +1044,7 @@ describe(`Query Collections`, () => { }) }) - it(`should handle isReady with parameterized queries`, async () => { + it(`should handle isReady with parameterized queries`, () => { let beginFn: (() => void) | undefined let commitFn: (() => void) | undefined diff --git a/packages/svelte-db/tsconfig.json b/packages/svelte-db/tsconfig.json index fc534e3d5..3632e7f2a 100644 --- a/packages/svelte-db/tsconfig.json +++ b/packages/svelte-db/tsconfig.json @@ -16,6 +16,6 @@ "@tanstack/db-ivm": ["../db-ivm/src"] } }, - "include": ["src/**/*", "tests", "vite.config.ts", "svelte.config.js"], + "include": ["src/**/*", "tests", "vite.config.ts", "svelte.config.js", "../../shared/**/*.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/packages/trailbase-db-collection/src/trailbase.ts b/packages/trailbase-db-collection/src/trailbase.ts index 88519ec1c..c41c3b089 100644 --- a/packages/trailbase-db-collection/src/trailbase.ts +++ b/packages/trailbase-db-collection/src/trailbase.ts @@ -291,6 +291,7 @@ export function trailBaseCollectionOptions< return { ...config, + collectionType: `trailbase` as const, sync, getKey, onInsert: async ( diff --git a/packages/vue-db/tsconfig.json b/packages/vue-db/tsconfig.json index 0b61f8cc7..7a15916c7 100644 --- a/packages/vue-db/tsconfig.json +++ b/packages/vue-db/tsconfig.json @@ -16,6 +16,6 @@ "@tanstack/db-ivm": ["../db-ivm/src"] } }, - "include": ["src/**/*", "tests", "vite.config.ts"], + "include": ["src/**/*", "tests", "vite.config.ts", "../../shared/**/*.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc024c9bb..5bfe15382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,7 +88,7 @@ importers: version: 0.4.0 tsup: specifier: ^8.0.2 - version: 8.5.0(@microsoft/api-extractor@7.47.7(@types/node@22.17.0))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) + version: 8.5.0(@microsoft/api-extractor@7.52.10(@types/node@22.17.0))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) typescript: specifier: ^5.8.2 version: 5.8.3 @@ -243,6 +243,9 @@ importers: examples/react/todo: dependencies: + '@tanstack/db-devtools': + specifier: workspace:* + version: link:../../../packages/db-devtools '@tanstack/electric-db-collection': specifier: ^0.1.0 version: link:../../../packages/electric-db-collection @@ -255,14 +258,23 @@ importers: '@tanstack/react-db': specifier: ^0.1.0 version: link:../../../packages/react-db + '@tanstack/react-db-devtools': + specifier: workspace:* + version: link:../../../packages/react-db-devtools + '@tanstack/react-devtools': + specifier: ^0.3.0 + version: 0.3.0(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.7) '@tanstack/react-router': specifier: ^1.125.6 version: 1.130.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/react-router-devtools': + specifier: ^1.130.2 + version: 1.130.2(@tanstack/react-router@1.130.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.130.2)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.7)(tiny-invariant@1.3.3) '@tanstack/react-start': specifier: ^1.126.1 version: 1.130.3(@netlify/blobs@9.1.2)(@tanstack/react-router@1.130.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)))(drizzle-orm@0.40.1(@types/pg@8.15.5)(gel@2.1.1)(kysely@0.28.3)(pg@8.16.3)(postgres@3.4.7))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(vite-plugin-solid@2.11.8(@testing-library/jest-dom@6.6.4)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)))(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) '@tanstack/trailbase-db-collection': - specifier: ^0.1.0 + specifier: ^0.1.2 version: link:../../../packages/trailbase-db-collection cors: specifier: ^2.8.5 @@ -487,6 +499,76 @@ importers: specifier: ^2.1.20 version: 2.1.20 + packages/db-devtools: + dependencies: + '@tanstack/db': + specifier: workspace:* + version: link:../db + '@tanstack/react-table': + specifier: ^8.13.2 + version: 8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/react-virtual': + specifier: ^3.0.0 + version: 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/solid-db': + specifier: workspace:* + version: link:../solid-db + '@tanstack/solid-table': + specifier: ^8.21.3 + version: 8.21.3(solid-js@1.9.7) + '@tanstack/solid-virtual': + specifier: ^3.13.12 + version: 3.13.12(solid-js@1.9.7) + '@types/prismjs': + specifier: ^1.26.5 + version: 1.26.5 + prismjs: + specifier: ^1.30.0 + version: 1.30.0 + devDependencies: + '@kobalte/core': + specifier: ^0.13.4 + version: 0.13.11(solid-js@1.9.7) + '@solid-primitives/keyed': + specifier: ^1.2.2 + version: 1.5.2(solid-js@1.9.7) + '@solid-primitives/resize-observer': + specifier: ^2.0.26 + version: 2.1.3(solid-js@1.9.7) + '@solid-primitives/storage': + specifier: ^1.3.11 + version: 1.3.11(solid-js@1.9.7) + '@tanstack/match-sorter-utils': + specifier: ^8.19.4 + version: 8.19.4 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + goober: + specifier: ^2.1.16 + version: 2.1.16(csstype@3.1.3) + npm-run-all2: + specifier: ^5.0.0 + version: 5.0.2 + solid-js: + specifier: ^1.9.5 + version: 1.9.7 + solid-transition-group: + specifier: ^0.2.3 + version: 0.2.3(solid-js@1.9.7) + superjson: + specifier: ^2.2.1 + version: 2.2.2 + tsup-preset-solid: + specifier: ^2.2.0 + version: 2.2.0(esbuild@0.25.8)(solid-js@1.9.7)(tsup@8.5.0(@microsoft/api-extractor@7.52.10(@types/node@22.17.0))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@22.17.0)(rollup@4.46.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + vite-plugin-solid: + specifier: ^2.11.6 + version: 2.11.8(@testing-library/jest-dom@6.6.4)(solid-js@1.9.7)(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + packages/db-ivm: dependencies: fractional-indexing: @@ -590,6 +672,34 @@ importers: specifier: ^19.0.0 version: 19.1.1(react@19.1.1) + packages/react-db-devtools: + dependencies: + '@tanstack/db-devtools': + specifier: workspace:* + version: link:../db-devtools + solid-js: + specifier: ^1.9.5 + version: 1.9.7 + devDependencies: + '@tanstack/react-db': + specifier: workspace:* + version: link:../react-db + '@testing-library/react': + specifier: ^16.1.0 + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@types/react': + specifier: ^19.0.1 + version: 19.1.9 + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + npm-run-all2: + specifier: ^5.0.0 + version: 5.0.2 + react: + specifier: ^19.0.0 + version: 19.1.1 + packages/solid-db: dependencies: '@solid-primitives/map': @@ -948,6 +1058,11 @@ packages: resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} engines: {node: '>=v18'} + '@corvu/utils@0.4.2': + resolution: {integrity: sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==} + peerDependencies: + solid-js: ^1.8 + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -1635,6 +1750,15 @@ packages: '@fastify/busboy@3.1.1': resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.3': + resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@gerrit0/mini-shiki@1.27.2': resolution: {integrity: sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==} @@ -1661,6 +1785,12 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@internationalized/date@3.8.2': + resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} + + '@internationalized/number@3.6.4': + resolution: {integrity: sha512-P+/h+RDaiX8EGt3shB9AYM1+QgkvHmJ5rKi4/59k4sg9g58k9rqsRW0WxRO7jCoHyvVbFRRFKmVTdFYdehrxHg==} + '@ioredis/commands@1.3.0': resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==} @@ -1700,6 +1830,16 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@kobalte/core@0.13.11': + resolution: {integrity: sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ==} + peerDependencies: + solid-js: ^1.8.15 + + '@kobalte/utils@0.9.1': + resolution: {integrity: sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w==} + peerDependencies: + solid-js: ^1.8.8 + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -1723,10 +1863,17 @@ packages: '@microsoft/api-extractor-model@7.29.6': resolution: {integrity: sha512-gC0KGtrZvxzf/Rt9oMYD2dHvtN/1KPEYsrQPyMKhLHnlVuO/f4AFN3E4toqZzD2pt4LhkKoYmL2H9tX3yCOyRw==} + '@microsoft/api-extractor-model@7.30.7': + resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==} + '@microsoft/api-extractor@7.47.7': resolution: {integrity: sha512-fNiD3G55ZJGhPOBPMKD/enozj8yxJSYyVJWxRWdcUtw842rvthDHJgUWq9gXQTensFlMHv2wGuCjjivPv53j0A==} hasBin: true + '@microsoft/api-extractor@7.52.10': + resolution: {integrity: sha512-LhKytJM5ZJkbHQVfW/3o747rZUNs/MGg6j/wt/9qwwqEOfvUDTYXXxIBuMgrRXhJ528p41iyz4zjBVHZU74Odg==} + hasBin: true + '@microsoft/tsdoc-config@0.17.1': resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} @@ -2112,6 +2259,14 @@ packages: cpu: [x64] os: [win32] + '@rushstack/node-core-library@5.14.0': + resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + '@rushstack/node-core-library@5.7.0': resolution: {integrity: sha512-Ff9Cz/YlWu9ce4dmqNBZpA45AEya04XaBFIjV7xTVeEf+y/kTjEasmozqFELXlNG4ROdevss75JrrZ5WgufDkQ==} peerDependencies: @@ -2131,9 +2286,20 @@ packages: '@types/node': optional: true + '@rushstack/terminal@0.15.4': + resolution: {integrity: sha512-OQSThV0itlwVNHV6thoXiAYZlQh4Fgvie2CzxFABsbO2MWQsI4zOh3LRNigYSTrmS+ba2j0B3EObakPzf/x6Zg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + '@rushstack/ts-command-line@4.22.6': resolution: {integrity: sha512-QSRqHT/IfoC5nk9zn6+fgyqOPXHME0BfchII9EUPR19pocsNp/xSbeBCbD3PIR2Lg+Q5qk7OFqk1VhWPMdKHJg==} + '@rushstack/ts-command-line@5.0.2': + resolution: {integrity: sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ==} + '@shikijs/engine-oniguruma@1.29.2': resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==} @@ -2188,6 +2354,16 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/keyed@1.5.2': + resolution: {integrity: sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/map@0.4.13': + resolution: {integrity: sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew==} + peerDependencies: + solid-js: ^1.6.12 + '@solid-primitives/map@0.7.2': resolution: {integrity: sha512-sXK/rS68B4oq3XXNyLrzVhLtT1pnimmMUahd2FqhtYUuyQsCfnW058ptO1s+lWc2k8F/3zQSNVkZ2ifJjlcNbQ==} peerDependencies: @@ -2198,6 +2374,11 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/props@3.2.2': + resolution: {integrity: sha512-lZOTwFJajBrshSyg14nBMEP0h8MXzPowGO0s3OeiR3z6nXHTfj0FhzDtJMv+VYoRJKQHG2QRnJTgCzK6erARAw==} + peerDependencies: + solid-js: ^1.6.12 + '@solid-primitives/refs@1.1.2': resolution: {integrity: sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg==} peerDependencies: @@ -2223,11 +2404,21 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/storage@1.3.11': + resolution: {integrity: sha512-PpQWR3TaTxHIJFbI9ZssYTM4Aa67g1vJIgps4TPhcXzHqqomrPAIveFC2FG7SDQoi9YQia8FVBjigELziJpfIg==} + peerDependencies: + solid-js: ^1.6.12 + '@solid-primitives/styles@0.1.2': resolution: {integrity: sha512-7iX5K+J5b1PRrbgw3Ki92uvU2LgQ0Kd/QMsrAZxDg5dpUBwMyTijZkA3bbs1ikZsT1oQhS41bTyKbjrXeU0Awg==} peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/transition-group@1.1.2': + resolution: {integrity: sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA==} + peerDependencies: + solid-js: ^1.6.12 + '@solid-primitives/trigger@1.2.2': resolution: {integrity: sha512-IWoptVc0SWYgmpBPpCMehS5b07+tpFcvw15tOQ3QbXedSYn6KP8zCjPkHNzMxcOvOicTneleeZDP7lqmz+PQ6g==} peerDependencies: @@ -2302,6 +2493,9 @@ packages: resolution: {integrity: sha512-08eKiDAjj4zLug1taXSIJ0kGL5cawjVCyJkBb6EWSg5fEPX6L+Wtr0CH2If4j5KYylz85iaZiFlUItvgJvll5g==} engines: {node: ^14.13.1 || ^16.0.0 || >=18} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@tailwindcss/node@4.1.11': resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} @@ -2406,6 +2600,22 @@ packages: peerDependencies: typescript: '>=4.7' + '@tanstack/devtools-event-bus@0.2.1': + resolution: {integrity: sha512-JMq3AmrQR2LH9P8Rcj1MTq8Iq/mPk/PyuqSw1L0hO2Wl8G1oz5ue31fS8u8lIgOCVR/mGdJah18p+Pj5OosRJA==} + engines: {node: '>=18'} + + '@tanstack/devtools-ui@0.3.0': + resolution: {integrity: sha512-lyP0eM6juIWn8zgI8xI32Lh86gCnjUyNePE9F7Bfgkv5taILmmJAHW5Mme4T2ufv7L8NLwOiBY/bZYnP4zev0w==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + + '@tanstack/devtools@0.3.1': + resolution: {integrity: sha512-dqUpPbB4CWvNsXEOyS0H/6elp/QVetvHz3f51B96zl+b5294gUjGwSMI4eNVrmBQPPmqkdkeGsbbkgrlJ1VhNQ==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + '@tanstack/directive-functions-plugin@1.129.7': resolution: {integrity: sha512-2VvlVmDvwHOnDAXQQa+gnhDnWPW59JcqePFf1ujOG0QGv+pw1G+JzHpiLZs4Dwr4myMxMGzFp5AWtvF96rpE7Q==} engines: {node: '>=12'} @@ -2426,6 +2636,10 @@ packages: resolution: {integrity: sha512-I3YTkbe4RZQN54Qw4+IUhOjqG2DdbG2+EBWuQfew4MEk0eddLYAQVa50BZVww4/D2eh5I9vEk2Fd1Y0Wty7pug==} engines: {node: '>=12'} + '@tanstack/match-sorter-utils@8.19.4': + resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} + engines: {node: '>=12'} + '@tanstack/publish-config@0.2.0': resolution: {integrity: sha512-RC0yRBFJvGuR58tKQUIkMXVEiATXgESIc+3/NTqoCC7D2YOF4fZGmHGYIanFEPQH7EGfQ5+Bwi+H6BOtKnymtw==} engines: {node: '>=18'} @@ -2448,6 +2662,15 @@ packages: peerDependencies: react: '>=16.8.0' + '@tanstack/react-devtools@0.3.0': + resolution: {integrity: sha512-16Bfxdb6lxekwY1Nl7UOzfKAIqSB2kw1neX5WUFa1g2SIZLyb3gYiSn1a42RaiJaZOAuHBOxhmad8ZBsetMumQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': '>=16.8' + '@types/react-dom': '>=16.8' + react: '>=16.8' + react-dom: '>=16.8' + '@tanstack/react-query@5.83.0': resolution: {integrity: sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==} peerDependencies: @@ -2514,6 +2737,19 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/router-core@1.130.2': resolution: {integrity: sha512-d5hYEEAvNUImpoomTlP2tRelX4JiNx3g2uk6xAO/aPKuMYdfntBSV7xbKuWZEhSwqeN2Z4qD3YyQEXBa4Fu7Mg==} engines: {node: '>=12'} @@ -2606,6 +2842,17 @@ packages: peerDependencies: solid-js: ^1.6.0 + '@tanstack/solid-table@8.21.3': + resolution: {integrity: sha512-PmhfSLBxVKiFs01LtYOYrCRhCyTUjxmb4KlxRQiqcALtip8+DOJeeezQM4RSX/GUS0SMVHyH/dNboCpcO++k2A==} + engines: {node: '>=12'} + peerDependencies: + solid-js: '>=1.3' + + '@tanstack/solid-virtual@3.13.12': + resolution: {integrity: sha512-0dS8GkBTmbuM9cUR6Jni0a45eJbd32CAEbZj8HrZMWIj3lu974NpGz5ywcomOGJ9GdeHuDaRzlwtonBbKV1ihQ==} + peerDependencies: + solid-js: ^1.3.0 + '@tanstack/start-client-core@1.130.2': resolution: {integrity: sha512-5uiMGabPZS1XVyrk1D9yK1Zk7DQdGXCaNXWj40dFoxP+hViFPJobYnW9qQHuMm5j7AAWDpqOqCOjvD2nruZ2FQ==} engines: {node: '>=12'} @@ -2642,6 +2889,10 @@ packages: '@tanstack/store@0.7.2': resolution: {integrity: sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==} + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@tanstack/trailbase-db-collection@0.0.3': resolution: {integrity: sha512-iQDNTKIP1b0+h/qAkHg66CdKwKOC2fvhbdQ9IO8lCsv4tWS9zWgtYYCHUPyigZecYCp2ankqpOixlJumC1lTWg==} peerDependencies: @@ -2651,6 +2902,9 @@ packages: resolution: {integrity: sha512-1ak0ZirlLRxd3dNNOFnMoYORBeC83nK4C+OiXpE0dxsO8ZVrBqCtNCKr8SG+W9zICXcWGiFu9qYLsgNKTayOqw==} engines: {node: '>=18'} + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tanstack/virtual-file-routes@1.129.7': resolution: {integrity: sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==} engines: {node: '>=12'} @@ -2777,6 +3031,9 @@ packages: '@types/pg@8.15.5': resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -3050,6 +3307,14 @@ packages: typescript: optional: true + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@vue/reactivity@3.5.18': resolution: {integrity: sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==} @@ -3147,6 +3412,9 @@ packages: ajv@8.13.0: resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3609,6 +3877,10 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + copy-file@11.0.0: resolution: {integrity: sha512-mFsNh/DIANLqFt5VHZoGirdg7bK5+oTWlhnGu6tgRhzBlnEKWaPX2xrFaLltii/6rmhqFMJqffUgknuRdpYlHw==} engines: {node: '>=18'} @@ -4155,6 +4427,9 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -4193,6 +4468,12 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild-plugin-solid@0.5.0: + resolution: {integrity: sha512-ITK6n+0ayGFeDVUZWNMxX+vLsasEN1ILrg4pISsNOQ+mq4ljlJJiuXotInd+HE0MzwTcA9wExT1yzDE2hsqPsg==} + peerDependencies: + esbuild: '>=0.12' + solid-js: '>= 1.0' + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -4552,6 +4833,10 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -4733,6 +5018,9 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -4857,6 +5145,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -5159,6 +5450,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -5452,6 +5746,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -5680,6 +5978,9 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} hasBin: true + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} @@ -5692,6 +5993,11 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + npm-run-all2@5.0.2: + resolution: {integrity: sha512-S2G6FWZ3pNWAAKm2PFSOtEAG/N+XO/kz3+9l6V91IY+Y3XFSt7Lp7DV92KCgEboEW0hRTu0vFaMe4zXDZYaOyA==} + engines: {node: '>= 10'} + hasBin: true + npm-run-path@2.0.2: resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} engines: {node: '>=4'} @@ -5858,6 +6164,10 @@ packages: resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} engines: {node: '>=14'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse-json@8.3.0: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} @@ -5980,6 +6290,11 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pidtree@0.5.0: + resolution: {integrity: sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==} + engines: {node: '>=0.10'} + hasBin: true + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -6085,6 +6400,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -6179,6 +6498,10 @@ packages: resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} engines: {node: '>=18'} + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + read-pkg@9.0.1: resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} engines: {node: '>=18'} @@ -6237,6 +6560,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + remove-trailing-separator@1.1.0: resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} @@ -6508,11 +6834,27 @@ packages: solid-js@1.9.7: resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} + solid-presence@0.1.8: + resolution: {integrity: sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==} + peerDependencies: + solid-js: ^1.8 + + solid-prevent-scroll@0.1.10: + resolution: {integrity: sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw==} + peerDependencies: + solid-js: ^1.8 + solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: solid-js: ^1.3 + solid-transition-group@0.2.3: + resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} + engines: {node: '>=18.0.0', pnpm: '>=8.6.0'} + peerDependencies: + solid-js: ^1.6.12 + sorted-btree@1.8.1: resolution: {integrity: sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ==} @@ -6669,6 +7011,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + supports-color@10.0.0: resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} engines: {node: '>=18'} @@ -6873,6 +7219,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup-preset-solid@2.2.0: + resolution: {integrity: sha512-sPAzeArmYkVAZNRN+m4tkiojdd0GzW/lCwd4+TQDKMENe8wr2uAuro1s0Z59ASmdBbkXoxLgCiNcuQMyiidMZg==} + peerDependencies: + tsup: ^8.0.0 + tsup@8.5.0: resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} engines: {node: '>=18'} @@ -6901,6 +7252,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -6955,6 +7310,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -7164,6 +7524,15 @@ packages: vite: optional: true + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite-plugin-externalize-deps@0.9.0: resolution: {integrity: sha512-wg3qb5gCy2d1KpPKyD9wkXMcYJ84yjgziHrStq9/8R7chhUC73mhQz+tVtvhFiICQHsBn1pnkY4IBbPqF9JHNw==} peerDependencies: @@ -7889,6 +8258,11 @@ snapshots: '@types/conventional-commits-parser': 5.0.1 chalk: 5.4.1 + '@corvu/utils@0.4.2(solid-js@1.9.7)': + dependencies: + '@floating-ui/dom': 1.7.3 + solid-js: 1.9.7 + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -8296,6 +8670,17 @@ snapshots: '@fastify/busboy@3.1.1': {} + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.3': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + '@gerrit0/mini-shiki@1.27.2': dependencies: '@shikijs/engine-oniguruma': 1.29.2 @@ -8317,6 +8702,14 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@internationalized/date@3.8.2': + dependencies: + '@swc/helpers': 0.5.17 + + '@internationalized/number@3.6.4': + dependencies: + '@swc/helpers': 0.5.17 + '@ioredis/commands@1.3.0': {} '@isaacs/balanced-match@4.0.1': {} @@ -8359,6 +8752,29 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 + '@kobalte/core@0.13.11(solid-js@1.9.7)': + dependencies: + '@floating-ui/dom': 1.7.3 + '@internationalized/date': 3.8.2 + '@internationalized/number': 3.6.4 + '@kobalte/utils': 0.9.1(solid-js@1.9.7) + '@solid-primitives/props': 3.2.2(solid-js@1.9.7) + '@solid-primitives/resize-observer': 2.1.3(solid-js@1.9.7) + solid-js: 1.9.7 + solid-presence: 0.1.8(solid-js@1.9.7) + solid-prevent-scroll: 0.1.10(solid-js@1.9.7) + + '@kobalte/utils@0.9.1(solid-js@1.9.7)': + dependencies: + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.7) + '@solid-primitives/keyed': 1.5.2(solid-js@1.9.7) + '@solid-primitives/map': 0.4.13(solid-js@1.9.7) + '@solid-primitives/media': 2.3.3(solid-js@1.9.7) + '@solid-primitives/props': 3.2.2(solid-js@1.9.7) + '@solid-primitives/refs': 1.1.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) + solid-js: 1.9.7 + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.1 @@ -8406,6 +8822,14 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor-model@7.30.7(@types/node@22.17.0)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@22.17.0) + transitivePeerDependencies: + - '@types/node' + '@microsoft/api-extractor@7.47.7(@types/node@22.17.0)': dependencies: '@microsoft/api-extractor-model': 7.29.6(@types/node@22.17.0) @@ -8424,6 +8848,24 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor@7.52.10(@types/node@22.17.0)': + dependencies: + '@microsoft/api-extractor-model': 7.30.7(@types/node@22.17.0) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.14.0(@types/node@22.17.0) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.15.4(@types/node@22.17.0) + '@rushstack/ts-command-line': 5.0.2(@types/node@22.17.0) + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.10 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + '@microsoft/tsdoc-config@0.17.1': dependencies: '@microsoft/tsdoc': 0.15.1 @@ -8807,6 +9249,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.1': optional: true + '@rushstack/node-core-library@5.14.0(@types/node@22.17.0)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.10 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.17.0 + '@rushstack/node-core-library@5.7.0(@types/node@22.17.0)': dependencies: ajv: 8.13.0 @@ -8832,6 +9287,13 @@ snapshots: optionalDependencies: '@types/node': 22.17.0 + '@rushstack/terminal@0.15.4(@types/node@22.17.0)': + dependencies: + '@rushstack/node-core-library': 5.14.0(@types/node@22.17.0) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.17.0 + '@rushstack/ts-command-line@4.22.6(@types/node@22.17.0)': dependencies: '@rushstack/terminal': 0.14.0(@types/node@22.17.0) @@ -8841,6 +9303,15 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@rushstack/ts-command-line@5.0.2(@types/node@22.17.0)': + dependencies: + '@rushstack/terminal': 0.15.4(@types/node@22.17.0) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@shikijs/engine-oniguruma@1.29.2': dependencies: '@shikijs/types': 1.29.2 @@ -8923,6 +9394,15 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 + '@solid-primitives/keyed@1.5.2(solid-js@1.9.7)': + dependencies: + solid-js: 1.9.7 + + '@solid-primitives/map@0.4.13(solid-js@1.9.7)': + dependencies: + '@solid-primitives/trigger': 1.2.2(solid-js@1.9.7) + solid-js: 1.9.7 + '@solid-primitives/map@0.7.2(solid-js@1.9.7)': dependencies: '@solid-primitives/trigger': 1.2.2(solid-js@1.9.7) @@ -8936,6 +9416,11 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 + '@solid-primitives/props@3.2.2(solid-js@1.9.7)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) + solid-js: 1.9.7 + '@solid-primitives/refs@1.1.2(solid-js@1.9.7)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) @@ -8963,12 +9448,21 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 + '@solid-primitives/storage@1.3.11(solid-js@1.9.7)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) + solid-js: 1.9.7 + '@solid-primitives/styles@0.1.2(solid-js@1.9.7)': dependencies: '@solid-primitives/rootless': 1.5.2(solid-js@1.9.7) '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.7)': + dependencies: + solid-js: 1.9.7 + '@solid-primitives/trigger@1.2.2(solid-js@1.9.7)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) @@ -9057,6 +9551,10 @@ snapshots: transitivePeerDependencies: - encoding + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.1.11': dependencies: '@ampproject/remapping': 2.3.0 @@ -9156,6 +9654,33 @@ snapshots: '@standard-schema/spec': 1.0.0 typescript: 5.8.3 + '@tanstack/devtools-event-bus@0.2.1': + dependencies: + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tanstack/devtools-ui@0.3.0(csstype@3.1.3)(solid-js@1.9.7)': + dependencies: + goober: 2.1.16(csstype@3.1.3) + solid-js: 1.9.7 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools@0.3.1(csstype@3.1.3)(solid-js@1.9.7)': + dependencies: + '@solid-primitives/keyboard': 1.3.3(solid-js@1.9.7) + '@tanstack/devtools-event-bus': 0.2.1 + '@tanstack/devtools-ui': 0.3.0(csstype@3.1.3)(solid-js@1.9.7) + clsx: 2.1.1 + goober: 2.1.16(csstype@3.1.3) + solid-js: 1.9.7 + transitivePeerDependencies: + - bufferutil + - csstype + - utf-8-validate + '@tanstack/directive-functions-plugin@1.129.7(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@babel/code-frame': 7.27.1 @@ -9201,6 +9726,10 @@ snapshots: '@tanstack/history@1.129.7': {} + '@tanstack/match-sorter-utils@8.19.4': + dependencies: + remove-accents: 0.5.0 + '@tanstack/publish-config@0.2.0': dependencies: '@commitlint/parse': 19.8.1 @@ -9232,6 +9761,19 @@ snapshots: transitivePeerDependencies: - typescript + '@tanstack/react-devtools@0.3.0(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.7)': + dependencies: + '@tanstack/devtools': 0.3.1(csstype@3.1.3)(solid-js@1.9.7) + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + transitivePeerDependencies: + - bufferutil + - csstype + - solid-js + - utf-8-validate + '@tanstack/react-query@5.83.0(react@19.1.1)': dependencies: '@tanstack/query-core': 5.83.0 @@ -9462,6 +10004,18 @@ snapshots: react-dom: 19.1.1(react@19.1.1) use-sync-external-store: 1.5.0(react@19.1.1) + '@tanstack/react-table@8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + + '@tanstack/react-virtual@3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + '@tanstack/router-core@1.130.2': dependencies: '@tanstack/history': 1.129.7 @@ -9668,6 +10222,16 @@ snapshots: '@tanstack/store': 0.7.0 solid-js: 1.9.7 + '@tanstack/solid-table@8.21.3(solid-js@1.9.7)': + dependencies: + '@tanstack/table-core': 8.21.3 + solid-js: 1.9.7 + + '@tanstack/solid-virtual@3.13.12(solid-js@1.9.7)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + solid-js: 1.9.7 + '@tanstack/start-client-core@1.130.2': dependencies: '@tanstack/router-core': 1.130.2 @@ -9825,6 +10389,8 @@ snapshots: '@tanstack/store@0.7.2': {} + '@tanstack/table-core@8.21.3': {} + '@tanstack/trailbase-db-collection@0.0.3(typescript@5.8.3)': dependencies: '@standard-schema/spec': 1.0.0 @@ -9844,6 +10410,8 @@ snapshots: transitivePeerDependencies: - typescript + '@tanstack/virtual-core@3.13.12': {} + '@tanstack/virtual-file-routes@1.129.7': {} '@tanstack/vite-config@0.2.0(@types/node@22.17.0)(rollup@4.46.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': @@ -10002,6 +10570,8 @@ snapshots: pg-protocol: 1.10.3 pg-types: 2.2.0 + '@types/prismjs@1.26.5': {} + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -10344,6 +10914,19 @@ snapshots: optionalDependencies: typescript: 5.8.3 + '@vue/language-core@2.2.0(typescript@5.8.3)': + dependencies: + '@volar/language-core': 2.4.22 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.18 + alien-signals: 0.4.14 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.8.3 + '@vue/reactivity@3.5.18': dependencies: '@vue/shared': 3.5.18 @@ -10453,6 +11036,8 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + alien-signals@0.4.14: {} + ansi-colors@4.1.3: {} ansi-escapes@7.0.0: @@ -10977,6 +11562,10 @@ snapshots: cookie@1.0.2: {} + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + copy-file@11.0.0: dependencies: graceful-fs: 4.2.11 @@ -11333,6 +11922,10 @@ snapshots: environment@1.1.0: {} + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} es-abstract@1.24.0: @@ -11438,6 +12031,16 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild-plugin-solid@0.5.0(esbuild@0.25.8)(solid-js@1.9.7): + dependencies: + '@babel/core': 7.28.0 + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + babel-preset-solid: 1.9.6(@babel/core@7.28.0) + esbuild: 0.25.8 + solid-js: 1.9.7 + transitivePeerDependencies: + - supports-color + esbuild-register@3.6.0(esbuild@0.19.12): dependencies: debug: 4.4.1 @@ -11985,6 +12588,12 @@ snapshots: fresh@2.0.0: {} + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -12201,6 +12810,8 @@ snapshots: hookable@5.5.3: {} + hosted-git-info@2.8.9: {} + hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -12321,6 +12932,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} is-async-function@2.1.1: @@ -12614,6 +13227,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -12907,6 +13522,8 @@ snapshots: media-typer@0.3.0: {} + memorystream@0.3.1: {} + meow@12.1.1: {} merge-anything@5.1.7: @@ -13274,6 +13891,13 @@ snapshots: dependencies: abbrev: 3.0.1 + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.10 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 @@ -13286,6 +13910,16 @@ snapshots: normalize-path@3.0.0: {} + npm-run-all2@5.0.2: + dependencies: + ansi-styles: 5.2.0 + cross-spawn: 7.0.6 + memorystream: 0.3.1 + minimatch: 3.1.2 + pidtree: 0.5.0 + read-pkg: 5.2.0 + shell-quote: 1.8.3 + npm-run-path@2.0.2: dependencies: path-key: 2.0.1 @@ -13459,9 +14093,16 @@ snapshots: parse-gitignore@2.0.0: {} + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse-json@8.3.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 index-to-position: 1.1.0 type-fest: 4.41.0 @@ -13561,6 +14202,8 @@ snapshots: picomatch@4.0.3: {} + pidtree@0.5.0: {} + pidtree@0.6.0: {} pify@4.0.1: {} @@ -13657,6 +14300,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + prismjs@1.30.0: {} + process-nextick-args@2.0.1: {} process@0.11.10: {} @@ -13747,6 +14392,13 @@ snapshots: read-pkg: 9.0.1 type-fest: 4.41.0 + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + read-pkg@9.0.1: dependencies: '@types/normalize-package-data': 2.4.4 @@ -13839,6 +14491,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remove-accents@0.5.0: {} + remove-trailing-separator@1.1.0: {} require-directory@2.1.1: {} @@ -14162,6 +14816,16 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.2(seroval@1.3.2) + solid-presence@0.1.8(solid-js@1.9.7): + dependencies: + '@corvu/utils': 0.4.2(solid-js@1.9.7) + solid-js: 1.9.7 + + solid-prevent-scroll@0.1.10(solid-js@1.9.7): + dependencies: + '@corvu/utils': 0.4.2(solid-js@1.9.7) + solid-js: 1.9.7 + solid-refresh@0.6.3(solid-js@1.9.7): dependencies: '@babel/generator': 7.28.0 @@ -14171,6 +14835,12 @@ snapshots: transitivePeerDependencies: - supports-color + solid-transition-group@0.2.3(solid-js@1.9.7): + dependencies: + '@solid-primitives/refs': 1.1.2(solid-js@1.9.7) + '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.7) + solid-js: 1.9.7 + sorted-btree@1.8.1: {} source-map-js@1.2.1: {} @@ -14347,6 +15017,10 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + supports-color@10.0.0: {} supports-color@7.2.0: @@ -14540,7 +15214,16 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(@microsoft/api-extractor@7.47.7(@types/node@22.17.0))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): + tsup-preset-solid@2.2.0(esbuild@0.25.8)(solid-js@1.9.7)(tsup@8.5.0(@microsoft/api-extractor@7.52.10(@types/node@22.17.0))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)): + dependencies: + esbuild-plugin-solid: 0.5.0(esbuild@0.25.8)(solid-js@1.9.7) + tsup: 8.5.0(@microsoft/api-extractor@7.52.10(@types/node@22.17.0))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) + transitivePeerDependencies: + - esbuild + - solid-js + - supports-color + + tsup@8.5.0(@microsoft/api-extractor@7.52.10(@types/node@22.17.0))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.8) cac: 6.7.14 @@ -14560,7 +15243,7 @@ snapshots: tinyglobby: 0.2.14 tree-kill: 1.2.2 optionalDependencies: - '@microsoft/api-extractor': 7.47.7(@types/node@22.17.0) + '@microsoft/api-extractor': 7.52.10(@types/node@22.17.0) postcss: 8.5.6 typescript: 5.8.3 transitivePeerDependencies: @@ -14580,6 +15263,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.6.0: {} + type-fest@4.41.0: {} type-is@1.6.18: @@ -14651,6 +15336,8 @@ snapshots: typescript@5.4.2: {} + typescript@5.8.2: {} + typescript@5.8.3: {} uc.micro@2.1.0: {} @@ -14894,6 +15581,25 @@ snapshots: - rollup - supports-color + vite-plugin-dts@4.5.4(@types/node@22.17.0)(rollup@4.46.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)): + dependencies: + '@microsoft/api-extractor': 7.52.10(@types/node@22.17.0) + '@rollup/pluginutils': 5.2.0(rollup@4.46.1) + '@volar/typescript': 2.4.22 + '@vue/language-core': 2.2.0(typescript@5.8.3) + compare-versions: 6.1.1 + debug: 4.4.1 + kolorist: 1.8.0 + local-pkg: 1.1.1 + magic-string: 0.30.17 + typescript: 5.8.3 + optionalDependencies: + vite: 6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite-plugin-externalize-deps@0.9.0(vite@6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)): dependencies: vite: 6.3.5(@types/node@22.17.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) diff --git a/tsconfig.json b/tsconfig.json index e54c55d56..ab474f8a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,7 +35,8 @@ "examples/**/*.ts", "examples/**/*.tsx", "eslint.config.mjs", - "scripts/verify-links.ts" + "scripts/verify-links.ts", + "shared/**/*.d.ts" ], "exclude": ["node_modules"] }