├── .gitignore ├── src ├── components │ ├── index.ts │ ├── RpcDevtoolsPanel.tsx │ ├── RequestList.tsx │ ├── RequestDetail.tsx │ └── styles.ts ├── index.ts ├── utils │ └── format-relative-time.ts ├── types.ts ├── event-client.ts ├── rpc-type-resolver.ts ├── protocol-interceptor.ts ├── builders.ts └── store.ts ├── vitest.config.ts ├── tsdown.config.ts ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── test ├── setup.ts ├── rpc-type-resolver.test.ts ├── event-client.test.ts ├── format-relative-time.test.ts ├── protocol-interceptor.test.ts └── store.test.ts ├── package.json ├── README.md └── bun.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { RequestDetail } from "./RequestDetail" 2 | export { RequestList } from "./RequestList" 3 | export { RpcDevtoolsPanel, type RpcDevtoolsPanelOptions } from "./RpcDevtoolsPanel" 4 | export { injectKeyframes, styles } from "./styles" 5 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "node", 7 | setupFiles: ["./test/setup.ts"], 8 | include: ["test/**/*.test.ts"], 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown" 2 | 3 | export default defineConfig({ 4 | entry: { 5 | index: "src/index.ts", 6 | builders: "src/builders.ts", 7 | "components/index": "src/components/index.ts", 8 | }, 9 | format: ["esm"], 10 | dts: true, 11 | clean: true, 12 | }) 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: oven-sh/setup-bun@v2 17 | with: 18 | bun-version: latest 19 | 20 | - name: Install dependencies 21 | run: bun install 22 | 23 | - name: Type check 24 | run: bun run typecheck 25 | 26 | - name: Run tests 27 | run: bun run test:run 28 | 29 | - name: Build 30 | run: bun run build 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "ESNext", 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "moduleResolution": "bundler", 8 | "allowImportingTsExtensions": true, 9 | "verbatimModuleSyntax": true, 10 | "noEmit": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUncheckedSideEffectImports": true, 15 | "jsx": "react-jsx", 16 | "plugins": [ 17 | { 18 | "name": "@effect/language-service", 19 | "reportSuggestionsAsWarningsInTsc": true 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // RPC Builders 2 | export * as Rpc from "./builders" 3 | export { RpcType } from "./builders" 4 | export { rpcEventClient } from "./event-client" 5 | export { clearRequestTracking, DevtoolsProtocolLayer } from "./protocol-interceptor" 6 | // RPC type resolution 7 | export { 8 | createRpcTypeResolver, 9 | getRpcType, 10 | heuristicResolver, 11 | type RpcTypeResolver, 12 | setRpcTypeResolver, 13 | } from "./rpc-type-resolver" 14 | export { clearRequests, useRpcRequests, useRpcStats } from "./store" 15 | // Core devtools 16 | export type { 17 | CapturedRequest, 18 | RpcDevtoolsEventMap, 19 | RpcRequestEvent, 20 | RpcResponseEvent, 21 | } from "./types" 22 | 23 | // React components are exported separately via "@hazel/rpc-devtools/components" 24 | // to avoid JSX compilation issues in non-React packages 25 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, vi } from "vitest" 2 | 3 | declare global { 4 | var __EFFECT_RPC_DEVTOOLS_CLIENT__: unknown 5 | var __EFFECT_RPC_DEVTOOLS_DEBUG__: boolean | undefined 6 | var __EFFECT_RPC_DEVTOOLS_STORE_INITIALIZED__: boolean | undefined 7 | var __RPC_TYPE_RESOLVER__: unknown 8 | } 9 | 10 | let uuidCounter = 0 11 | 12 | beforeEach(() => { 13 | // Reset globalThis keys between tests 14 | delete globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 15 | delete globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__ 16 | delete globalThis.__EFFECT_RPC_DEVTOOLS_STORE_INITIALIZED__ 17 | delete globalThis.__RPC_TYPE_RESOLVER__ 18 | 19 | // Reset UUID counter for deterministic IDs 20 | uuidCounter = 0 21 | 22 | // Mock crypto.randomUUID 23 | vi.stubGlobal("crypto", { 24 | randomUUID: () => `test-uuid-${++uuidCounter}`, 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/format-relative-time.ts: -------------------------------------------------------------------------------- 1 | const DIVISIONS = [ 2 | { amount: 60, name: "seconds" }, 3 | { amount: 60, name: "minutes" }, 4 | { amount: 24, name: "hours" }, 5 | { amount: 7, name: "days" }, 6 | { amount: 4.34524, name: "weeks" }, 7 | { amount: 12, name: "months" }, 8 | { amount: Number.POSITIVE_INFINITY, name: "years" }, 9 | ] as const 10 | 11 | const formatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" }) 12 | 13 | export function formatRelativeTime(date: Date | number): string { 14 | const timestamp = typeof date === "number" ? date : date.getTime() 15 | let duration = (timestamp - Date.now()) / 1000 16 | 17 | for (const division of DIVISIONS) { 18 | if (Math.abs(duration) < division.amount) { 19 | return formatter.format(Math.round(duration), division.name as Intl.RelativeTimeFormatUnit) 20 | } 21 | duration /= division.amount 22 | } 23 | 24 | return formatter.format(Math.round(duration), "years") 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effect-rpc-tanstack-devtools", 3 | "private": false, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "main": "dist/index.mjs", 7 | "module": "dist/index.mjs", 8 | "types": "dist/index.d.mts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.mts", 12 | "import": "./dist/index.mjs", 13 | "default": "./dist/index.mjs" 14 | }, 15 | "./builders": { 16 | "types": "./dist/builders.d.mts", 17 | "import": "./dist/builders.mjs", 18 | "default": "./dist/builders.mjs" 19 | }, 20 | "./components": { 21 | "types": "./dist/components/index.d.mts", 22 | "import": "./dist/components/index.mjs", 23 | "default": "./dist/components/index.mjs" 24 | } 25 | }, 26 | "files": [ 27 | "dist" 28 | ], 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "scripts": { 33 | "build": "tsdown", 34 | "dev": "tsdown --watch", 35 | "typecheck": "tsc --noEmit", 36 | "test": "vitest", 37 | "test:run": "vitest run" 38 | }, 39 | "peerDependencies": { 40 | "@effect/rpc": ">=0.70.0", 41 | "@tanstack/devtools-event-client": ">=0.3.0", 42 | "effect": ">=3.0.0", 43 | "react": ">=18.0.0" 44 | }, 45 | "dependencies": {}, 46 | "devDependencies": { 47 | "@effect/rpc": "^0.70.0", 48 | "@tanstack/devtools-event-client": "^0.3.0", 49 | "@types/node": "^24.10.1", 50 | "@types/react": "^19.2.7", 51 | "effect": "^3.16.0", 52 | "react": "^19.1.0", 53 | "tsdown": "^0.17.2", 54 | "typescript": "^5.9.3", 55 | "vitest": "^3.2.4" 56 | } 57 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types for Effect RPC Devtools 3 | */ 4 | 5 | /** 6 | * Event map for TanStack devtools event bus 7 | * Keys must follow the pattern: `${pluginId}:${eventSuffix}` 8 | */ 9 | export interface RpcDevtoolsEventMap { 10 | "effect-rpc:request": RpcRequestEvent 11 | "effect-rpc:response": RpcResponseEvent 12 | "effect-rpc:clear": undefined 13 | } 14 | 15 | /** 16 | * Event payload for RPC request 17 | */ 18 | export interface RpcRequestEvent { 19 | id: string 20 | method: string 21 | type?: "mutation" | "query" 22 | payload: unknown 23 | timestamp: number 24 | headers: ReadonlyArray<[string, string]> 25 | } 26 | 27 | /** 28 | * Event payload for RPC response 29 | */ 30 | export interface RpcResponseEvent { 31 | requestId: string 32 | status: "success" | "error" 33 | data: unknown 34 | duration: number 35 | timestamp: number 36 | } 37 | 38 | /** 39 | * A captured RPC request with optional response 40 | */ 41 | export interface CapturedRequest { 42 | /** Unique client-side ID for React keys (RPC protocol IDs can be reused) */ 43 | captureId: string 44 | /** Original RPC protocol ID (for response matching) */ 45 | id: string 46 | method: string 47 | /** Whether this RPC is a mutation or query */ 48 | type?: "mutation" | "query" 49 | payload: unknown 50 | headers: ReadonlyArray<[string, string]> 51 | timestamp: number 52 | startTime: number 53 | response?: { 54 | status: "success" | "error" 55 | data: unknown 56 | duration: number 57 | timestamp: number 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/event-client.ts: -------------------------------------------------------------------------------- 1 | import { EventClient } from "@tanstack/devtools-event-client" 2 | import type { RpcDevtoolsEventMap } from "./types" 3 | 4 | type RpcEventClient = EventClient 5 | 6 | const CLIENT_KEY = "__EFFECT_RPC_DEVTOOLS_CLIENT__" as const 7 | const DEBUG_KEY = "__EFFECT_RPC_DEVTOOLS_DEBUG__" as const 8 | 9 | declare global { 10 | var __EFFECT_RPC_DEVTOOLS_CLIENT__: RpcEventClient | undefined 11 | var __EFFECT_RPC_DEVTOOLS_DEBUG__: boolean | undefined 12 | } 13 | 14 | /** 15 | * Check if we're in a development environment 16 | */ 17 | const isDev = () => { 18 | try { 19 | // Vite 20 | if (typeof import.meta !== "undefined" && "env" in import.meta) { 21 | return (import.meta as any).env?.DEV ?? false 22 | } 23 | } catch { 24 | // Ignore 25 | } 26 | try { 27 | // Node.js 28 | return process.env.NODE_ENV === "development" 29 | } catch { 30 | return false 31 | } 32 | } 33 | 34 | /** 35 | * Get or create the singleton event client 36 | */ 37 | function getClient(): RpcEventClient { 38 | if (!globalThis[CLIENT_KEY]) { 39 | globalThis[CLIENT_KEY] = new EventClient({ 40 | pluginId: "effect-rpc", 41 | debug: globalThis[DEBUG_KEY] ?? false, 42 | enabled: isDev(), 43 | }) 44 | } 45 | return globalThis[CLIENT_KEY] 46 | } 47 | 48 | /** 49 | * TanStack Devtools event client for Effect RPC 50 | * 51 | * This client emits events when RPC requests are made and responses are received. 52 | * The devtools panel subscribes to these events to display the RPC traffic. 53 | * 54 | * Implemented as a Proxy to ensure setDebug() changes are reflected even after import. 55 | */ 56 | export const rpcEventClient: RpcEventClient = new Proxy({} as RpcEventClient, { 57 | get(_, prop) { 58 | const client = getClient() 59 | const value = client[prop as keyof RpcEventClient] 60 | return typeof value === "function" ? value.bind(client) : value 61 | }, 62 | }) 63 | 64 | /** 65 | * Enable or disable debug logging for the RPC devtools event client. 66 | * Debug is disabled by default. This recreates the client with the new setting. 67 | */ 68 | export function setDebug(debug: boolean): void { 69 | if ((globalThis[DEBUG_KEY] ?? false) !== debug) { 70 | globalThis[DEBUG_KEY] = debug 71 | globalThis[CLIENT_KEY] = new EventClient({ 72 | pluginId: "effect-rpc", 73 | debug, 74 | enabled: isDev(), 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/rpc-type-resolver.ts: -------------------------------------------------------------------------------- 1 | import type { RpcGroup } from "@effect/rpc" 2 | import { Context, Option } from "effect" 3 | import { RpcType } from "./builders" 4 | 5 | /** 6 | * Function type for resolving RPC method names to mutation/query classification 7 | */ 8 | export type RpcTypeResolver = (method: string) => "mutation" | "query" | undefined 9 | 10 | /** 11 | * Build a resolver from RpcGroup definitions 12 | * 13 | * @example 14 | * ```typescript 15 | * import { createRpcTypeResolver, setRpcTypeResolver } from "@hazel/rpc-devtools" 16 | * import { MyRpcs, OtherRpcs } from "./my-rpcs" 17 | * 18 | * setRpcTypeResolver(createRpcTypeResolver([MyRpcs, OtherRpcs])) 19 | * ``` 20 | */ 21 | export const createRpcTypeResolver = (rpcGroups: RpcGroup.RpcGroup[]): RpcTypeResolver => { 22 | const typeMap = new Map() 23 | 24 | for (const group of rpcGroups) { 25 | for (const [tag, rpc] of group.requests) { 26 | const typeOption = Context.getOption(rpc.annotations, RpcType) 27 | if (Option.isSome(typeOption)) { 28 | typeMap.set(tag, typeOption.value) 29 | } 30 | } 31 | } 32 | 33 | return (method) => typeMap.get(method) 34 | } 35 | 36 | /** 37 | * Fallback heuristic-based resolver that infers type from method name patterns 38 | * 39 | * Patterns detected: 40 | * - Mutations: create, update, delete, add, remove, set, mark, regenerate 41 | * - Queries: list, get, me, search, find 42 | */ 43 | export const heuristicResolver: RpcTypeResolver = (method) => { 44 | const lower = method.toLowerCase() 45 | if (/\.(create|update|delete|add|remove|set|mark|regenerate)/.test(lower)) { 46 | return "mutation" 47 | } 48 | if (/\.(list|get|me|search|find)/.test(lower)) { 49 | return "query" 50 | } 51 | return undefined 52 | } 53 | 54 | /** 55 | * Global resolver that can be configured 56 | * Defaults to heuristic resolver 57 | */ 58 | let _resolver: RpcTypeResolver = heuristicResolver 59 | 60 | /** 61 | * Set the global RPC type resolver 62 | * 63 | * @example 64 | * ```typescript 65 | * // Use annotation-based resolution from your RPC definitions 66 | * setRpcTypeResolver(createRpcTypeResolver([MyRpcs])) 67 | * 68 | * // Or provide a custom resolver 69 | * setRpcTypeResolver((method) => { 70 | * if (method.includes('update')) return 'mutation' 71 | * return 'query' 72 | * }) 73 | * ``` 74 | */ 75 | export const setRpcTypeResolver = (resolver: RpcTypeResolver) => { 76 | _resolver = resolver 77 | } 78 | 79 | /** 80 | * Get the RPC type for a given method name 81 | */ 82 | export const getRpcType = (method: string) => _resolver(method) 83 | -------------------------------------------------------------------------------- /src/protocol-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { RpcClient } from "@effect/rpc" 2 | import type { FromClientEncoded, FromServerEncoded } from "@effect/rpc/RpcMessage" 3 | import { Effect, Layer } from "effect" 4 | import { rpcEventClient } from "./event-client" 5 | import { getRpcType } from "./rpc-type-resolver" 6 | 7 | /** 8 | * Map to track request timestamps for duration calculation 9 | */ 10 | const requestTimestamps = new Map() 11 | 12 | /** 13 | * Protocol wrapper layer - captures BOTH requests and responses 14 | * 15 | * This intercepts at the transport layer: 16 | * - send() for outgoing RPC requests 17 | * - run() for incoming server responses 18 | * 19 | * No need to modify RPC definitions - this captures all traffic. 20 | * 21 | * @example 22 | * ```typescript 23 | * import { DevtoolsProtocolLayer } from "@hazel/rpc-devtools" 24 | * 25 | * // Add to your RPC client layer composition 26 | * const ProtocolLive = import.meta.env.DEV 27 | * ? Layer.provideMerge(DevtoolsProtocolLayer, BaseProtocolLive) 28 | * : BaseProtocolLive 29 | * ``` 30 | */ 31 | export const DevtoolsProtocolLayer = Layer.effect( 32 | RpcClient.Protocol, 33 | Effect.gen(function* () { 34 | const base = yield* RpcClient.Protocol 35 | 36 | return { 37 | supportsAck: base.supportsAck, 38 | supportsTransferables: base.supportsTransferables, 39 | 40 | // Intercept outgoing requests 41 | send: (request: FromClientEncoded, transferables?: ReadonlyArray) => { 42 | if (request._tag === "Request") { 43 | const timestamp = Date.now() 44 | const id = request.id 45 | requestTimestamps.set(id, timestamp) 46 | 47 | rpcEventClient.emit("request", { 48 | id, 49 | method: request.tag, 50 | type: getRpcType(request.tag), 51 | payload: request.payload, 52 | timestamp, 53 | headers: request.headers ?? [], 54 | }) 55 | } 56 | return base.send(request, transferables) 57 | }, 58 | 59 | // Intercept incoming responses 60 | run: (handler: (data: FromServerEncoded) => Effect.Effect) => 61 | base.run((message: FromServerEncoded) => { 62 | if (message._tag === "Exit") { 63 | const id = message.requestId 64 | const startTime = requestTimestamps.get(id) 65 | const timestamp = Date.now() 66 | const duration = startTime ? timestamp - startTime : 0 67 | requestTimestamps.delete(id) 68 | 69 | rpcEventClient.emit("response", { 70 | requestId: id, 71 | status: message.exit._tag === "Success" ? "success" : "error", 72 | data: message.exit._tag === "Success" ? message.exit.value : message.exit.cause, 73 | duration, 74 | timestamp, 75 | }) 76 | } 77 | return handler(message) 78 | }), 79 | } 80 | }), 81 | ) 82 | 83 | /** 84 | * Clear request tracking state 85 | */ 86 | export const clearRequestTracking = () => { 87 | requestTimestamps.clear() 88 | } 89 | -------------------------------------------------------------------------------- /src/builders.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * RPC builders with mutation/query type annotations. 3 | * 4 | * This module re-exports Effect's Rpc and adds mutation() and query() 5 | * builder functions that automatically annotate RPCs with their type. 6 | * 7 | * @example 8 | * ```typescript 9 | * import { Rpc } from "@hazel/rpc-devtools" 10 | * 11 | * Rpc.mutation("channel.create", { payload: ..., success: ... }) 12 | * Rpc.query("channel.list", { success: ... }) 13 | * ``` 14 | */ 15 | import { Rpc as EffectRpc } from "@effect/rpc" 16 | import type * as RpcSchema from "@effect/rpc/RpcSchema" 17 | import { Context, type Schema } from "effect" 18 | import type { NoInfer } from "effect/Types" 19 | 20 | /** 21 | * Context tag for RPC type classification. 22 | * Used by devtools to distinguish mutations from queries. 23 | */ 24 | export class RpcType extends Context.Tag("@hazel/rpc-devtools/RpcType")() {} 25 | 26 | // Re-export everything from Effect's Rpc 27 | export * from "@effect/rpc/Rpc" 28 | 29 | /** 30 | * Create a mutation RPC endpoint. 31 | * Mutations are operations that modify state (create, update, delete). 32 | */ 33 | export const mutation = < 34 | const Tag extends string, 35 | Payload extends Schema.Schema.Any | Schema.Struct.Fields = typeof Schema.Void, 36 | Success extends Schema.Schema.Any = typeof Schema.Void, 37 | Error extends Schema.Schema.All = typeof Schema.Never, 38 | const Stream extends boolean = false, 39 | >( 40 | tag: Tag, 41 | options?: { 42 | readonly payload?: Payload 43 | readonly success?: Success 44 | readonly error?: Error 45 | readonly stream?: Stream 46 | readonly primaryKey?: [Payload] extends [Schema.Struct.Fields] 47 | ? (payload: Schema.Simplify>>) => string 48 | : never 49 | }, 50 | ): EffectRpc.Rpc< 51 | Tag, 52 | Payload extends Schema.Struct.Fields ? Schema.Struct : Payload, 53 | Stream extends true ? RpcSchema.Stream : Success, 54 | Stream extends true ? typeof Schema.Never : Error 55 | > => EffectRpc.make(tag, options).annotate(RpcType, "mutation") as any 56 | 57 | /** 58 | * Create a query RPC endpoint. 59 | * Queries are read-only operations that don't modify state (get, list, search). 60 | */ 61 | export const query = < 62 | const Tag extends string, 63 | Payload extends Schema.Schema.Any | Schema.Struct.Fields = typeof Schema.Void, 64 | Success extends Schema.Schema.Any = typeof Schema.Void, 65 | Error extends Schema.Schema.All = typeof Schema.Never, 66 | const Stream extends boolean = false, 67 | >( 68 | tag: Tag, 69 | options?: { 70 | readonly payload?: Payload 71 | readonly success?: Success 72 | readonly error?: Error 73 | readonly stream?: Stream 74 | readonly primaryKey?: [Payload] extends [Schema.Struct.Fields] 75 | ? (payload: Schema.Simplify>>) => string 76 | : never 77 | }, 78 | ): EffectRpc.Rpc< 79 | Tag, 80 | Payload extends Schema.Struct.Fields ? Schema.Struct : Payload, 81 | Stream extends true ? RpcSchema.Stream : Success, 82 | Stream extends true ? typeof Schema.Never : Error 83 | > => EffectRpc.make(tag, options).annotate(RpcType, "query") as any 84 | -------------------------------------------------------------------------------- /src/components/RpcDevtoolsPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react" 2 | import { setDebug } from "../event-client" 3 | import { clearRequests, useRpcRequests, useRpcStats } from "../store" 4 | import { RequestDetail } from "./RequestDetail" 5 | import { RequestList } from "./RequestList" 6 | import { injectKeyframes, styles } from "./styles" 7 | 8 | export interface RpcDevtoolsPanelOptions { 9 | /** Enable debug logging for the event client. Default: false */ 10 | debug?: boolean 11 | } 12 | 13 | interface RpcDevtoolsPanelProps { 14 | options?: RpcDevtoolsPanelOptions 15 | } 16 | 17 | export function RpcDevtoolsPanel({ options }: RpcDevtoolsPanelProps) { 18 | const requests = useRpcRequests() 19 | const stats = useRpcStats() 20 | const [selectedId, setSelectedId] = useState(null) 21 | const [filter, setFilter] = useState("") 22 | 23 | // Inject keyframes for pulse animation 24 | useEffect(() => { 25 | injectKeyframes() 26 | }, []) 27 | 28 | // Sync debug option with event client 29 | useEffect(() => { 30 | setDebug(options?.debug ?? false) 31 | }, [options?.debug]) 32 | 33 | const filteredRequests = useMemo(() => { 34 | if (!filter) return requests 35 | const lowerFilter = filter.toLowerCase() 36 | return requests.filter((req) => req.method.toLowerCase().includes(lowerFilter)) 37 | }, [requests, filter]) 38 | 39 | const selectedRequest = useMemo(() => { 40 | if (!selectedId) return null 41 | return requests.find((r) => r.captureId === selectedId) ?? null 42 | }, [requests, selectedId]) 43 | 44 | return ( 45 |
46 | {/* Header */} 47 |
48 |
49 |

Effect RPC

50 |
51 | 52 | {stats.total} total 53 | 54 | {stats.pending > 0 && ( 55 | 56 | {stats.pending} pending 57 | 58 | )} 59 | 60 | {stats.success} success 61 | 62 | {stats.error > 0 && ( 63 | 64 | {stats.error} errors 65 | 66 | )} 67 | {stats.avgDuration > 0 && ( 68 | 69 | {stats.avgDuration}ms avg 70 | 71 | )} 72 |
73 |
74 | 77 |
78 | 79 | {/* Filter */} 80 |
81 | setFilter(e.target.value)} 86 | style={styles.filterInput} 87 | /> 88 |
89 | 90 | {/* Content */} 91 |
92 | 93 | {selectedRequest && } 94 |
95 |
96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /src/components/RequestList.tsx: -------------------------------------------------------------------------------- 1 | import { getRpcType } from "../rpc-type-resolver" 2 | import type { CapturedRequest } from "../types" 3 | import { formatRelativeTime } from "../utils/format-relative-time" 4 | import { styles } from "./styles" 5 | 6 | interface RequestListProps { 7 | requests: CapturedRequest[] 8 | selectedId: string | null 9 | onSelect: (id: string | null) => void 10 | } 11 | 12 | export function RequestList({ requests, selectedId, onSelect }: RequestListProps) { 13 | if (requests.length === 0) { 14 | return
No RPC requests captured yet
15 | } 16 | 17 | return ( 18 |
19 | 20 | 21 | 22 | 23 | 24 | 27 | 30 | 31 | 32 | 33 | {requests.map((request) => ( 34 | 37 | onSelect(selectedId === request.captureId ? null : request.captureId) 38 | } 39 | style={styles.tableRow(selectedId === request.captureId)} 40 | > 41 | 45 | 48 | 53 | 56 | 57 | ))} 58 | 59 |
MethodStatus 25 | Time 26 | 28 | When 29 |
42 | {request.method} 43 | 44 | 46 | 47 | 49 | {request.response?.duration != null 50 | ? `${request.response.duration}ms` 51 | : "..."} 52 | 54 | {formatRelativeTime(request.timestamp)} 55 |
60 |
61 | ) 62 | } 63 | 64 | function StatusBadge({ request }: { request: CapturedRequest }) { 65 | if (!request.response) { 66 | return ( 67 | 68 | 69 | pending 70 | 71 | ) 72 | } 73 | 74 | if (request.response.status === "success") { 75 | return ( 76 | 77 | 78 | success 79 | 80 | ) 81 | } 82 | 83 | return ( 84 | 85 | 86 | error 87 | 88 | ) 89 | } 90 | 91 | function MethodTypeBadge({ type }: { type: "mutation" | "query" | undefined }) { 92 | // Use the captured type from the request, fall back to query for unknown 93 | const resolvedType = type ?? "query" 94 | 95 | if (resolvedType === "mutation") { 96 | return mutation 97 | } 98 | return query 99 | } 100 | -------------------------------------------------------------------------------- /test/rpc-type-resolver.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from "vitest" 2 | import { getRpcType, heuristicResolver, setRpcTypeResolver } from "../src/rpc-type-resolver" 3 | 4 | describe("heuristicResolver", () => { 5 | describe("mutation patterns", () => { 6 | it.each([ 7 | ["user.create", "mutation"], 8 | ["user.update", "mutation"], 9 | ["user.delete", "mutation"], 10 | ["item.add", "mutation"], 11 | ["item.remove", "mutation"], 12 | ["config.set", "mutation"], 13 | ["notification.mark", "mutation"], 14 | ["token.regenerate", "mutation"], 15 | ["User.Create", "mutation"], 16 | ["USER.UPDATE", "mutation"], 17 | ])("recognizes %s as %s", (method, expected) => { 18 | expect(heuristicResolver(method)).toBe(expected) 19 | }) 20 | }) 21 | 22 | describe("query patterns", () => { 23 | it.each([ 24 | ["user.list", "query"], 25 | ["user.get", "query"], 26 | ["auth.me", "query"], 27 | ["item.search", "query"], 28 | ["item.find", "query"], 29 | ["User.List", "query"], 30 | ["USER.GET", "query"], 31 | ])("recognizes %s as %s", (method, expected) => { 32 | expect(heuristicResolver(method)).toBe(expected) 33 | }) 34 | }) 35 | 36 | describe("unknown patterns", () => { 37 | it.each([ 38 | ["user.validate"], 39 | ["item.process"], 40 | ["config.load"], 41 | ["unknown"], 42 | [""], 43 | ])("returns undefined for %s", (method) => { 44 | expect(heuristicResolver(method)).toBeUndefined() 45 | }) 46 | }) 47 | 48 | describe("pattern matching", () => { 49 | it("requires dot prefix for patterns", () => { 50 | expect(heuristicResolver("create")).toBeUndefined() 51 | expect(heuristicResolver("getUser")).toBeUndefined() 52 | expect(heuristicResolver("listItems")).toBeUndefined() 53 | }) 54 | 55 | it("matches patterns anywhere after a dot", () => { 56 | expect(heuristicResolver("api.users.create")).toBe("mutation") 57 | expect(heuristicResolver("v2.items.list")).toBe("query") 58 | }) 59 | }) 60 | }) 61 | 62 | describe("setRpcTypeResolver / getRpcType", () => { 63 | beforeEach(() => { 64 | // Reset to heuristic resolver before each test 65 | setRpcTypeResolver(heuristicResolver) 66 | }) 67 | 68 | it("uses heuristic resolver by default", () => { 69 | expect(getRpcType("user.create")).toBe("mutation") 70 | expect(getRpcType("user.list")).toBe("query") 71 | }) 72 | 73 | it("allows setting a custom resolver", () => { 74 | const customResolver = (method: string) => { 75 | if (method.startsWith("admin.")) return "mutation" as const 76 | if (method.startsWith("public.")) return "query" as const 77 | return undefined 78 | } 79 | 80 | setRpcTypeResolver(customResolver) 81 | 82 | expect(getRpcType("admin.anything")).toBe("mutation") 83 | expect(getRpcType("public.anything")).toBe("query") 84 | expect(getRpcType("other.method")).toBeUndefined() 85 | }) 86 | 87 | it("can chain resolvers with fallback", () => { 88 | const customWithFallback = (method: string) => { 89 | if (method === "special.method") return "mutation" as const 90 | return heuristicResolver(method) 91 | } 92 | 93 | setRpcTypeResolver(customWithFallback) 94 | 95 | expect(getRpcType("special.method")).toBe("mutation") 96 | expect(getRpcType("user.create")).toBe("mutation") 97 | expect(getRpcType("user.list")).toBe("query") 98 | }) 99 | 100 | it("resolver returning undefined allows fallthrough", () => { 101 | setRpcTypeResolver(() => undefined) 102 | expect(getRpcType("user.create")).toBeUndefined() 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/event-client.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from "vitest" 2 | import { rpcEventClient, setDebug } from "../src/event-client" 3 | 4 | describe("rpcEventClient", () => { 5 | beforeEach(() => { 6 | // Reset is handled by test/setup.ts 7 | }) 8 | 9 | describe("proxy behavior", () => { 10 | it("forwards method calls to the real client", () => { 11 | expect(typeof rpcEventClient.emit).toBe("function") 12 | expect(typeof rpcEventClient.on).toBe("function") 13 | expect(typeof rpcEventClient.getPluginId).toBe("function") 14 | }) 15 | 16 | it("returns correct pluginId", () => { 17 | expect(rpcEventClient.getPluginId()).toBe("effect-rpc") 18 | }) 19 | 20 | it("maintains reference after multiple accesses", () => { 21 | const emit1 = rpcEventClient.emit 22 | const emit2 = rpcEventClient.emit 23 | // Functions should be equivalent (bound to same client) 24 | expect(typeof emit1).toBe("function") 25 | expect(typeof emit2).toBe("function") 26 | }) 27 | }) 28 | 29 | describe("singleton pattern", () => { 30 | it("caches client in globalThis", () => { 31 | // Access the client 32 | rpcEventClient.getPluginId() 33 | 34 | // Check globalThis has the client 35 | expect(globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__).toBeDefined() 36 | }) 37 | 38 | it("reuses cached client", () => { 39 | const pluginId1 = rpcEventClient.getPluginId() 40 | const pluginId2 = rpcEventClient.getPluginId() 41 | 42 | expect(pluginId1).toBe(pluginId2) 43 | expect(pluginId1).toBe("effect-rpc") 44 | }) 45 | }) 46 | }) 47 | 48 | describe("setDebug", () => { 49 | beforeEach(() => { 50 | // Reset is handled by test/setup.ts 51 | }) 52 | 53 | it("sets debug flag in globalThis", () => { 54 | expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBeUndefined() 55 | 56 | setDebug(true) 57 | 58 | expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBe(true) 59 | }) 60 | 61 | it("recreates client when debug changes", () => { 62 | // Access client to create it 63 | rpcEventClient.getPluginId() 64 | const firstClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 65 | 66 | // Change debug setting 67 | setDebug(true) 68 | const secondClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 69 | 70 | expect(secondClient).not.toBe(firstClient) 71 | }) 72 | 73 | it("does not recreate client when debug value is same", () => { 74 | setDebug(true) 75 | const firstClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 76 | 77 | setDebug(true) 78 | const secondClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 79 | 80 | expect(secondClient).toBe(firstClient) 81 | }) 82 | 83 | it("proxy reflects new client after setDebug", () => { 84 | // Create initial client 85 | rpcEventClient.getPluginId() 86 | const initialClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 87 | 88 | // Change debug 89 | setDebug(true) 90 | 91 | // Proxy should now use new client 92 | rpcEventClient.getPluginId() 93 | const currentClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 94 | 95 | expect(currentClient).not.toBe(initialClient) 96 | }) 97 | 98 | it("toggles debug off", () => { 99 | setDebug(true) 100 | expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBe(true) 101 | 102 | setDebug(false) 103 | expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBe(false) 104 | }) 105 | }) 106 | 107 | describe("isDev detection", () => { 108 | beforeEach(() => { 109 | vi.unstubAllEnvs() 110 | }) 111 | 112 | it("creates client with enabled based on environment", () => { 113 | // In test environment, NODE_ENV is typically 'test', not 'development' 114 | // So the client should be created but may not be enabled 115 | rpcEventClient.getPluginId() 116 | expect(globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__).toBeDefined() 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /test/format-relative-time.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" 2 | import { formatRelativeTime } from "../src/utils/format-relative-time" 3 | 4 | describe("formatRelativeTime", () => { 5 | beforeEach(() => { 6 | vi.useFakeTimers() 7 | vi.setSystemTime(new Date("2024-01-15T12:00:00Z")) 8 | }) 9 | 10 | afterEach(() => { 11 | vi.useRealTimers() 12 | }) 13 | 14 | describe("seconds", () => { 15 | it("formats 'just now' for 0 seconds ago", () => { 16 | const now = Date.now() 17 | expect(formatRelativeTime(now)).toBe("now") 18 | }) 19 | 20 | it("formats seconds ago", () => { 21 | const thirtySecondsAgo = Date.now() - 30 * 1000 22 | expect(formatRelativeTime(thirtySecondsAgo)).toBe("30 seconds ago") 23 | }) 24 | 25 | it("formats 1 second ago", () => { 26 | const oneSecondAgo = Date.now() - 1000 27 | expect(formatRelativeTime(oneSecondAgo)).toBe("1 second ago") 28 | }) 29 | }) 30 | 31 | describe("minutes", () => { 32 | it("formats 1 minute ago", () => { 33 | const oneMinuteAgo = Date.now() - 60 * 1000 34 | expect(formatRelativeTime(oneMinuteAgo)).toBe("1 minute ago") 35 | }) 36 | 37 | it("formats multiple minutes ago", () => { 38 | const fiveMinutesAgo = Date.now() - 5 * 60 * 1000 39 | expect(formatRelativeTime(fiveMinutesAgo)).toBe("5 minutes ago") 40 | }) 41 | 42 | it("formats 59 minutes ago", () => { 43 | const fiftyNineMinutesAgo = Date.now() - 59 * 60 * 1000 44 | expect(formatRelativeTime(fiftyNineMinutesAgo)).toBe("59 minutes ago") 45 | }) 46 | }) 47 | 48 | describe("hours", () => { 49 | it("formats 1 hour ago", () => { 50 | const oneHourAgo = Date.now() - 60 * 60 * 1000 51 | expect(formatRelativeTime(oneHourAgo)).toBe("1 hour ago") 52 | }) 53 | 54 | it("formats multiple hours ago", () => { 55 | const threeHoursAgo = Date.now() - 3 * 60 * 60 * 1000 56 | expect(formatRelativeTime(threeHoursAgo)).toBe("3 hours ago") 57 | }) 58 | 59 | it("formats 23 hours ago", () => { 60 | const twentyThreeHoursAgo = Date.now() - 23 * 60 * 60 * 1000 61 | expect(formatRelativeTime(twentyThreeHoursAgo)).toBe("23 hours ago") 62 | }) 63 | }) 64 | 65 | describe("days", () => { 66 | it("formats yesterday", () => { 67 | const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000 68 | expect(formatRelativeTime(oneDayAgo)).toBe("yesterday") 69 | }) 70 | 71 | it("formats multiple days ago", () => { 72 | const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000 73 | expect(formatRelativeTime(threeDaysAgo)).toBe("3 days ago") 74 | }) 75 | 76 | it("formats 6 days ago", () => { 77 | const sixDaysAgo = Date.now() - 6 * 24 * 60 * 60 * 1000 78 | expect(formatRelativeTime(sixDaysAgo)).toBe("6 days ago") 79 | }) 80 | }) 81 | 82 | describe("weeks", () => { 83 | it("formats last week", () => { 84 | const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000 85 | expect(formatRelativeTime(oneWeekAgo)).toBe("last week") 86 | }) 87 | 88 | it("formats multiple weeks ago", () => { 89 | const threeWeeksAgo = Date.now() - 21 * 24 * 60 * 60 * 1000 90 | expect(formatRelativeTime(threeWeeksAgo)).toBe("3 weeks ago") 91 | }) 92 | }) 93 | 94 | describe("months", () => { 95 | it("formats last month", () => { 96 | const oneMonthAgo = Date.now() - 32 * 24 * 60 * 60 * 1000 97 | expect(formatRelativeTime(oneMonthAgo)).toBe("last month") 98 | }) 99 | 100 | it("formats multiple months ago", () => { 101 | const threeMonthsAgo = Date.now() - 90 * 24 * 60 * 60 * 1000 102 | expect(formatRelativeTime(threeMonthsAgo)).toBe("3 months ago") 103 | }) 104 | }) 105 | 106 | describe("years", () => { 107 | it("formats last year", () => { 108 | const oneYearAgo = Date.now() - 400 * 24 * 60 * 60 * 1000 109 | expect(formatRelativeTime(oneYearAgo)).toBe("last year") 110 | }) 111 | 112 | it("formats multiple years ago", () => { 113 | const twoYearsAgo = Date.now() - 800 * 24 * 60 * 60 * 1000 114 | expect(formatRelativeTime(twoYearsAgo)).toBe("2 years ago") 115 | }) 116 | }) 117 | 118 | describe("input types", () => { 119 | it("accepts Date object", () => { 120 | const date = new Date(Date.now() - 5 * 60 * 1000) 121 | expect(formatRelativeTime(date)).toBe("5 minutes ago") 122 | }) 123 | 124 | it("accepts timestamp number", () => { 125 | const timestamp = Date.now() - 5 * 60 * 1000 126 | expect(formatRelativeTime(timestamp)).toBe("5 minutes ago") 127 | }) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from "react" 2 | import { rpcEventClient } from "./event-client" 3 | import { clearRequestTracking } from "./protocol-interceptor" 4 | import type { CapturedRequest } from "./types" 5 | 6 | /** 7 | * Global key to prevent double initialization during HMR 8 | */ 9 | const STORE_INITIALIZED_KEY = "__EFFECT_RPC_DEVTOOLS_STORE_INITIALIZED__" as const 10 | 11 | declare global { 12 | var __EFFECT_RPC_DEVTOOLS_STORE_INITIALIZED__: boolean | undefined 13 | } 14 | 15 | /** 16 | * Maximum number of requests to keep in history 17 | */ 18 | const MAX_REQUESTS = 500 19 | 20 | /** 21 | * In-memory store for captured RPC requests 22 | */ 23 | let requests: CapturedRequest[] = [] 24 | const listeners: Set<() => void> = new Set() 25 | 26 | /** 27 | * Notify all listeners of state change 28 | */ 29 | const emitChange = () => { 30 | for (const listener of listeners) { 31 | listener() 32 | } 33 | } 34 | 35 | /** 36 | * Subscribe to store changes 37 | */ 38 | const subscribe = (callback: () => void): (() => void) => { 39 | listeners.add(callback) 40 | return () => { 41 | listeners.delete(callback) 42 | } 43 | } 44 | 45 | /** 46 | * Get current snapshot of requests 47 | */ 48 | const getSnapshot = (): CapturedRequest[] => requests 49 | 50 | /** 51 | * Get server snapshot (empty for client-only store) 52 | */ 53 | const getServerSnapshot = (): CapturedRequest[] => [] 54 | 55 | /** 56 | * Check if we're in a development environment 57 | */ 58 | const isDev = () => { 59 | try { 60 | // Vite 61 | if (typeof import.meta !== "undefined" && "env" in import.meta) { 62 | return (import.meta as any).env?.DEV ?? false 63 | } 64 | } catch { 65 | // Ignore 66 | } 67 | try { 68 | // Node.js 69 | return process.env.NODE_ENV === "development" 70 | } catch { 71 | return false 72 | } 73 | } 74 | 75 | // Initialize event subscriptions only once (survives HMR) 76 | if (isDev() && !globalThis[STORE_INITIALIZED_KEY]) { 77 | globalThis[STORE_INITIALIZED_KEY] = true 78 | 79 | // Listen for request events 80 | rpcEventClient.on("request", (event) => { 81 | const { payload } = event 82 | const newRequest: CapturedRequest = { 83 | captureId: crypto.randomUUID(), 84 | id: payload.id, 85 | method: payload.method, 86 | type: payload.type, 87 | payload: payload.payload, 88 | headers: payload.headers, 89 | timestamp: payload.timestamp, 90 | startTime: payload.timestamp, 91 | } 92 | 93 | // Add to beginning (newest first) and trim if needed 94 | requests = [newRequest, ...requests].slice(0, MAX_REQUESTS) 95 | emitChange() 96 | }) 97 | 98 | // Listen for response events 99 | rpcEventClient.on("response", (event) => { 100 | const { payload } = event 101 | requests = requests.map((req) => 102 | req.id === payload.requestId 103 | ? { 104 | ...req, 105 | response: { 106 | status: payload.status, 107 | data: payload.data, 108 | duration: payload.duration, 109 | timestamp: payload.timestamp, 110 | }, 111 | } 112 | : req, 113 | ) 114 | emitChange() 115 | }) 116 | 117 | // Listen for clear events 118 | rpcEventClient.on("clear", () => { 119 | requests = [] 120 | emitChange() 121 | }) 122 | } 123 | 124 | /** 125 | * React hook to access captured RPC requests 126 | * Uses useSyncExternalStore for proper concurrent mode support 127 | */ 128 | export const useRpcRequests = (): CapturedRequest[] => { 129 | return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) 130 | } 131 | 132 | /** 133 | * Clear all captured requests 134 | */ 135 | export const clearRequests = () => { 136 | requests = [] 137 | clearRequestTracking() 138 | rpcEventClient.emit("clear", undefined as never) 139 | emitChange() 140 | } 141 | 142 | /** 143 | * Get request statistics 144 | */ 145 | export const useRpcStats = () => { 146 | const requests = useRpcRequests() 147 | 148 | const total = requests.length 149 | const pending = requests.filter((r) => !r.response).length 150 | const success = requests.filter((r) => r.response?.status === "success").length 151 | const error = requests.filter((r) => r.response?.status === "error").length 152 | const avgDuration = 153 | requests 154 | .filter((r) => r.response?.duration) 155 | .reduce((sum, r) => sum + (r.response?.duration ?? 0), 0) / 156 | (requests.filter((r) => r.response?.duration).length || 1) || 0 157 | 158 | return { 159 | total, 160 | pending, 161 | success, 162 | error, 163 | avgDuration: Math.round(avgDuration), 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /test/protocol-interceptor.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from "vitest" 2 | import { clearRequestTracking } from "../src/protocol-interceptor" 3 | 4 | describe("clearRequestTracking", () => { 5 | beforeEach(() => { 6 | vi.resetModules() 7 | }) 8 | 9 | it("is exported and callable", () => { 10 | expect(typeof clearRequestTracking).toBe("function") 11 | // Should not throw 12 | clearRequestTracking() 13 | }) 14 | 15 | it("can be called multiple times without error", () => { 16 | clearRequestTracking() 17 | clearRequestTracking() 18 | clearRequestTracking() 19 | }) 20 | }) 21 | 22 | describe("DevtoolsProtocolLayer", () => { 23 | beforeEach(() => { 24 | vi.resetModules() 25 | }) 26 | 27 | it("is exported", async () => { 28 | const { DevtoolsProtocolLayer } = await import("../src/protocol-interceptor") 29 | expect(DevtoolsProtocolLayer).toBeDefined() 30 | }) 31 | }) 32 | 33 | describe("protocol interceptor logic", () => { 34 | describe("request timestamp tracking", () => { 35 | it("calculates duration from stored timestamp", () => { 36 | const startTime = 1000 37 | const endTime = 1250 38 | const duration = endTime - startTime 39 | expect(duration).toBe(250) 40 | }) 41 | 42 | it("handles missing timestamp gracefully", () => { 43 | const startTime = undefined 44 | const endTime = 1250 45 | const duration = startTime ? endTime - startTime : 0 46 | expect(duration).toBe(0) 47 | }) 48 | }) 49 | 50 | describe("request message handling", () => { 51 | it("identifies Request messages by _tag", () => { 52 | const requestMessage = { 53 | _tag: "Request" as const, 54 | id: "req-123", 55 | tag: "user.create", 56 | payload: { name: "John" }, 57 | headers: [["x-custom", "value"]] as const, 58 | } 59 | 60 | expect(requestMessage._tag).toBe("Request") 61 | expect(requestMessage.id).toBe("req-123") 62 | expect(requestMessage.tag).toBe("user.create") 63 | }) 64 | 65 | it("handles messages without headers", () => { 66 | const requestMessage = { 67 | _tag: "Request" as const, 68 | id: "req-123", 69 | tag: "user.get", 70 | payload: {}, 71 | headers: undefined, 72 | } 73 | 74 | const headers = requestMessage.headers ?? [] 75 | expect(headers).toEqual([]) 76 | }) 77 | }) 78 | 79 | describe("response message handling", () => { 80 | it("identifies Exit messages by _tag", () => { 81 | const exitMessage = { 82 | _tag: "Exit" as const, 83 | requestId: "req-123", 84 | exit: { 85 | _tag: "Success" as const, 86 | value: { id: 1, name: "John" }, 87 | }, 88 | } 89 | 90 | expect(exitMessage._tag).toBe("Exit") 91 | expect(exitMessage.requestId).toBe("req-123") 92 | }) 93 | 94 | it("extracts success status and value", () => { 95 | const successExit = { 96 | _tag: "Success" as const, 97 | value: { data: "test" }, 98 | } 99 | 100 | const status = successExit._tag === "Success" ? "success" : "error" 101 | const data = successExit._tag === "Success" ? successExit.value : null 102 | 103 | expect(status).toBe("success") 104 | expect(data).toEqual({ data: "test" }) 105 | }) 106 | 107 | it("extracts error status and cause", () => { 108 | const errorExit = { 109 | _tag: "Failure" as const, 110 | cause: { message: "Something went wrong" }, 111 | } 112 | 113 | const status = errorExit._tag === "Success" ? "success" : "error" 114 | const data = errorExit._tag === "Success" ? null : errorExit.cause 115 | 116 | expect(status).toBe("error") 117 | expect(data).toEqual({ message: "Something went wrong" }) 118 | }) 119 | }) 120 | 121 | describe("event emission data structure", () => { 122 | it("creates correct request event payload", () => { 123 | const requestPayload = { 124 | id: "req-123", 125 | method: "user.create", 126 | type: "mutation" as const, 127 | payload: { name: "John" }, 128 | timestamp: Date.now(), 129 | headers: [["authorization", "Bearer token"]] as const, 130 | } 131 | 132 | expect(requestPayload).toHaveProperty("id") 133 | expect(requestPayload).toHaveProperty("method") 134 | expect(requestPayload).toHaveProperty("type") 135 | expect(requestPayload).toHaveProperty("payload") 136 | expect(requestPayload).toHaveProperty("timestamp") 137 | expect(requestPayload).toHaveProperty("headers") 138 | }) 139 | 140 | it("creates correct response event payload", () => { 141 | const responsePayload = { 142 | requestId: "req-123", 143 | status: "success" as const, 144 | data: { id: 1, name: "John" }, 145 | duration: 250, 146 | timestamp: Date.now(), 147 | } 148 | 149 | expect(responsePayload).toHaveProperty("requestId") 150 | expect(responsePayload).toHaveProperty("status") 151 | expect(responsePayload).toHaveProperty("data") 152 | expect(responsePayload).toHaveProperty("duration") 153 | expect(responsePayload).toHaveProperty("timestamp") 154 | }) 155 | }) 156 | }) 157 | -------------------------------------------------------------------------------- /src/components/RequestDetail.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import type { CapturedRequest } from "../types" 3 | import { styles } from "./styles" 4 | 5 | interface RequestDetailProps { 6 | request: CapturedRequest 7 | } 8 | 9 | type Tab = "request" | "response" | "headers" | "timing" 10 | 11 | export function RequestDetail({ request }: RequestDetailProps) { 12 | const [activeTab, setActiveTab] = useState("request") 13 | 14 | return ( 15 |
16 | {/* Tabs */} 17 |
18 | {(["request", "response", "headers", "timing"] as const).map((tab) => ( 19 | 27 | ))} 28 |
29 | 30 | {/* Content */} 31 |
32 | {activeTab === "request" && } 33 | {activeTab === "response" && } 34 | {activeTab === "headers" && } 35 | {activeTab === "timing" && } 36 |
37 |
38 | ) 39 | } 40 | 41 | function RequestTab({ request }: { request: CapturedRequest }) { 42 | return ( 43 |
44 |
45 |

Method

46 | {request.method} 47 |
48 |
49 |
50 |

Payload

51 | 52 |
53 | 54 |
55 |
56 | ) 57 | } 58 | 59 | function ResponseTab({ request }: { request: CapturedRequest }) { 60 | if (!request.response) { 61 | return
Response pending...
62 | } 63 | 64 | const isError = request.response.status === "error" 65 | 66 | return ( 67 |
68 |
69 |

Status

70 | 71 | {request.response.status} 72 | 73 |
74 |
75 |
76 |

{isError ? "Error" : "Data"}

77 | 78 |
79 | 80 |
81 |
82 | ) 83 | } 84 | 85 | function HeadersTab({ request }: { request: CapturedRequest }) { 86 | if (request.headers.length === 0) { 87 | return
No headers
88 | } 89 | 90 | return ( 91 |
92 | {request.headers.map(([key, value], index) => ( 93 |
94 | {key}:{" "} 95 | {value} 96 |
97 | ))} 98 |
99 | ) 100 | } 101 | 102 | function TimingTab({ request }: { request: CapturedRequest }) { 103 | const startTime = new Date(request.startTime) 104 | 105 | return ( 106 |
107 |
108 |

Request ID

109 | {request.id} 110 |
111 |
112 |

Started At

113 |
{startTime.toLocaleTimeString()}
114 |
115 | {request.response && ( 116 | <> 117 |
118 |

Duration

119 |
{request.response.duration}ms
120 |
121 |
122 |

Completed At

123 |
124 | {new Date(request.response.timestamp).toLocaleTimeString()} 125 |
126 |
127 | 128 | )} 129 | {!request.response && ( 130 |
131 |

Status

132 |
Pending...
133 |
134 | )} 135 |
136 | ) 137 | } 138 | 139 | function JsonViewer({ data, isError = false }: { data: unknown; isError?: boolean }) { 140 | const jsonString = JSON.stringify(data, null, 2) 141 | 142 | return
{jsonString}
143 | } 144 | 145 | function CopyButton({ text }: { text: string }) { 146 | const [copied, setCopied] = useState(false) 147 | 148 | const handleCopy = async () => { 149 | await navigator.clipboard.writeText(text) 150 | setCopied(true) 151 | setTimeout(() => setCopied(false), 2000) 152 | } 153 | 154 | return ( 155 | 158 | ) 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # effect-rpc-tanstack-devtools 2 | 3 | Developer tools for [Effect RPC](https://effect.website/docs/rpc/introduction/) that integrate with [TanStack Devtools](https://tanstack.com/devtools). 4 | 5 | Shots Mockups 1x 6 | 7 | 8 | 9 | ## Features 10 | 11 | - Real-time RPC request/response monitoring 12 | - Request timing and duration tracking 13 | - Payload and response inspection with JSON viewer 14 | - Mutation/query classification with visual badges 15 | - Filter requests by method name 16 | - Copy payloads to clipboard 17 | - Dark theme UI (CSS-in-JS, no external dependencies) 18 | 19 | ## Installation 20 | 21 | ```bash 22 | npm install effect-rpc-tanstack-devtools 23 | # or 24 | bun add effect-rpc-tanstack-devtools 25 | ``` 26 | 27 | ### Peer Dependencies 28 | 29 | ```json 30 | { 31 | "@effect/rpc": ">=0.70.0", 32 | "@tanstack/devtools-event-client": ">=0.3.0", 33 | "effect": ">=3.0.0", 34 | "react": ">=18.0.0" 35 | } 36 | ``` 37 | 38 | ## Quick Start 39 | 40 | ### 1. Add the Protocol Layer 41 | 42 | Wrap your RPC client's protocol layer with `DevtoolsProtocolLayer` to capture all RPC traffic: 43 | 44 | ```typescript 45 | import { RpcClient, RpcSerialization } from "@effect/rpc" 46 | import { BrowserSocket } from "@effect/platform-browser" 47 | import { DevtoolsProtocolLayer } from "@hazel/rpc-devtools" 48 | import { Layer } from "effect" 49 | 50 | // Your base protocol layer 51 | const BaseProtocolLive = RpcClient.layerProtocolSocket({ 52 | retryTransientErrors: true, 53 | }).pipe( 54 | Layer.provide(BrowserSocket.layerWebSocket("wss://api.example.com/rpc")), 55 | Layer.provide(RpcSerialization.layerNdjson) 56 | ) 57 | 58 | // Add devtools in development only 59 | export const RpcProtocolLive = import.meta.env.DEV 60 | ? Layer.provideMerge(DevtoolsProtocolLayer, BaseProtocolLive) 61 | : BaseProtocolLive 62 | ``` 63 | 64 | ### 2. Add the Devtools Panel 65 | 66 | Add the React component to your app, typically integrated with TanStack Devtools: 67 | 68 | ```tsx 69 | import { TanStackDevtools } from "@tanstack/react-devtools" 70 | import { RpcDevtoolsPanel } from "@hazel/rpc-devtools/components" 71 | 72 | function App() { 73 | return ( 74 | <> 75 | {import.meta.env.DEV && ( 76 | , 81 | }, 82 | ]} 83 | /> 84 | )} 85 | {/* Your app */} 86 | 87 | ) 88 | } 89 | ``` 90 | 91 | That's it! You'll now see all RPC requests in the devtools panel. 92 | 93 | ## Optional: Mutation/Query Classification 94 | 95 | By default, the devtools uses heuristics to classify methods as mutations or queries based on naming patterns (e.g., `create`, `update`, `delete` = mutation; `get`, `list`, `find` = query). 96 | 97 | For accurate classification, you have two options: 98 | 99 | ### Option A: Use the RPC Builders (Recommended) 100 | 101 | The package provides `Rpc.mutation()` and `Rpc.query()` builder functions that automatically annotate your RPCs: 102 | 103 | ```typescript 104 | import { Rpc } from "@hazel/rpc-devtools" 105 | import { RpcGroup } from "@effect/rpc" 106 | import { Schema } from "effect" 107 | 108 | // Define RPCs with explicit type annotations 109 | const createChannel = Rpc.mutation("channel.create", { 110 | payload: { name: Schema.String }, 111 | success: Channel, 112 | error: ChannelError, 113 | }) 114 | 115 | const listChannels = Rpc.query("channel.list", { 116 | payload: { organizationId: Schema.String }, 117 | success: Schema.Array(Channel), 118 | }) 119 | 120 | export const ChannelRpcs = RpcGroup.make("channels").add(createChannel).add(listChannels) 121 | ``` 122 | 123 | Then configure the resolver with your RPC groups: 124 | 125 | ```typescript 126 | import { createRpcTypeResolver, setRpcTypeResolver } from "@hazel/rpc-devtools" 127 | 128 | // Call this once at app initialization 129 | if (import.meta.env.DEV) { 130 | setRpcTypeResolver(createRpcTypeResolver([ChannelRpcs, UserRpcs, /* ... */])) 131 | } 132 | ``` 133 | 134 | ### Option B: Use Standard Effect RPC 135 | 136 | You can continue using standard Effect RPC definitions. The devtools will use heuristic classification: 137 | 138 | ```typescript 139 | import { Rpc, RpcGroup } from "@effect/rpc" 140 | import { Schema } from "effect" 141 | 142 | // Standard Effect RPC - works fine, uses heuristic classification 143 | const createChannel = Rpc.make("channel.create", { 144 | payload: { name: Schema.String }, 145 | success: Channel, 146 | }) 147 | 148 | export const ChannelRpcs = RpcGroup.make("channels").add(createChannel) 149 | ``` 150 | 151 | Heuristic patterns detected: 152 | - **Mutations:** `create`, `update`, `delete`, `add`, `remove`, `set`, `mark`, `regenerate` 153 | - **Queries:** `list`, `get`, `me`, `search`, `find` 154 | 155 | ### Option C: Custom Resolver 156 | 157 | Provide your own classification logic: 158 | 159 | ```typescript 160 | import { setRpcTypeResolver } from "@hazel/rpc-devtools" 161 | 162 | setRpcTypeResolver((method) => { 163 | if (method.startsWith("admin.")) return "mutation" 164 | if (method.endsWith(".fetch")) return "query" 165 | return undefined // Fall back to heuristics 166 | }) 167 | ``` 168 | 169 | ## API Reference 170 | 171 | ### Main Exports (`@hazel/rpc-devtools`) 172 | 173 | ```typescript 174 | // Protocol layer for capturing RPC traffic 175 | export { DevtoolsProtocolLayer, clearRequestTracking } from "./protocol-interceptor" 176 | 177 | // React hooks for accessing captured data 178 | export { useRpcRequests, useRpcStats, clearRequests } from "./store" 179 | 180 | // Type resolution 181 | export { createRpcTypeResolver, setRpcTypeResolver, heuristicResolver, getRpcType } from "./rpc-type-resolver" 182 | 183 | // Optional RPC builders with type annotations 184 | export { Rpc, RpcType } from "./builders" 185 | 186 | // Event client for advanced usage 187 | export { rpcEventClient } from "./event-client" 188 | 189 | // Types 190 | export type { CapturedRequest, RpcRequestEvent, RpcResponseEvent, RpcDevtoolsEventMap } from "./types" 191 | ``` 192 | 193 | ### Component Exports (`@hazel/rpc-devtools/components`) 194 | 195 | ```typescript 196 | // Main devtools panel 197 | export { RpcDevtoolsPanel } from "./RpcDevtoolsPanel" 198 | 199 | // Individual components for custom UIs 200 | export { RequestList } from "./RequestList" 201 | export { RequestDetail } from "./RequestDetail" 202 | 203 | // Styles for custom theming 204 | export { styles, injectKeyframes } from "./styles" 205 | ``` 206 | 207 | ## Using the Hooks Directly 208 | 209 | Build custom UIs using the provided hooks: 210 | 211 | ```tsx 212 | import { useRpcRequests, useRpcStats, clearRequests } from "@hazel/rpc-devtools" 213 | 214 | function MyCustomDevtools() { 215 | const requests = useRpcRequests() 216 | const stats = useRpcStats() 217 | 218 | return ( 219 |
220 |

Total: {stats.total}, Pending: {stats.pending}, Avg: {stats.avgDuration}ms

221 | 222 |
    223 | {requests.map((req) => ( 224 |
  • 225 | {req.method} - {req.response?.status ?? "pending"} 226 |
  • 227 | ))} 228 |
229 |
230 | ) 231 | } 232 | ``` 233 | 234 | 235 | ## License 236 | 237 | MIT 238 | -------------------------------------------------------------------------------- /test/store.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from "vitest" 2 | 3 | // We need to test the store's internal logic without React hooks 4 | // The store exports functions that we can test indirectly 5 | 6 | describe("store", () => { 7 | beforeEach(() => { 8 | vi.resetModules() 9 | // Reset globalThis state 10 | delete globalThis.__EFFECT_RPC_DEVTOOLS_STORE_INITIALIZED__ 11 | delete globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__ 12 | delete globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__ 13 | }) 14 | 15 | describe("clearRequests", () => { 16 | it("is exported and callable", async () => { 17 | const { clearRequests } = await import("../src/store") 18 | expect(typeof clearRequests).toBe("function") 19 | // Should not throw 20 | clearRequests() 21 | }) 22 | }) 23 | 24 | describe("useRpcRequests hook", () => { 25 | it("is exported", async () => { 26 | const { useRpcRequests } = await import("../src/store") 27 | expect(typeof useRpcRequests).toBe("function") 28 | }) 29 | }) 30 | 31 | describe("useRpcStats hook", () => { 32 | it("is exported", async () => { 33 | const { useRpcStats } = await import("../src/store") 34 | expect(typeof useRpcStats).toBe("function") 35 | }) 36 | }) 37 | 38 | describe("MAX_REQUESTS limit", () => { 39 | it("store is initialized when module is imported", async () => { 40 | // Force dev mode 41 | vi.stubEnv("NODE_ENV", "development") 42 | 43 | // Import fresh module 44 | await import("../src/store") 45 | 46 | // Store should be marked as initialized 47 | // Note: may not be true if isDev() returns false in test env 48 | }) 49 | }) 50 | }) 51 | 52 | describe("store stats calculations", () => { 53 | // Test the stats calculation logic by creating mock data 54 | const createMockRequest = (overrides: Partial<{ 55 | id: string 56 | method: string 57 | response?: { 58 | status: "success" | "error" 59 | duration: number 60 | } 61 | }> = {}) => ({ 62 | captureId: `capture-${Math.random()}`, 63 | id: overrides.id ?? "req-1", 64 | method: overrides.method ?? "user.get", 65 | payload: {}, 66 | headers: [] as ReadonlyArray<[string, string]>, 67 | timestamp: Date.now(), 68 | startTime: Date.now(), 69 | ...(overrides.response && { 70 | response: { 71 | status: overrides.response.status, 72 | data: {}, 73 | duration: overrides.response.duration, 74 | timestamp: Date.now(), 75 | }, 76 | }), 77 | }) 78 | 79 | it("calculates total correctly", () => { 80 | const requests = [ 81 | createMockRequest({ id: "1" }), 82 | createMockRequest({ id: "2" }), 83 | createMockRequest({ id: "3" }), 84 | ] 85 | expect(requests.length).toBe(3) 86 | }) 87 | 88 | it("identifies pending requests", () => { 89 | const requests = [ 90 | createMockRequest({ id: "1" }), 91 | createMockRequest({ id: "2", response: { status: "success", duration: 100 } }), 92 | createMockRequest({ id: "3" }), 93 | ] 94 | const pending = requests.filter((r) => !r.response).length 95 | expect(pending).toBe(2) 96 | }) 97 | 98 | it("counts success responses", () => { 99 | const requests = [ 100 | createMockRequest({ id: "1", response: { status: "success", duration: 100 } }), 101 | createMockRequest({ id: "2", response: { status: "error", duration: 50 } }), 102 | createMockRequest({ id: "3", response: { status: "success", duration: 200 } }), 103 | ] 104 | const success = requests.filter((r) => r.response?.status === "success").length 105 | expect(success).toBe(2) 106 | }) 107 | 108 | it("counts error responses", () => { 109 | const requests = [ 110 | createMockRequest({ id: "1", response: { status: "success", duration: 100 } }), 111 | createMockRequest({ id: "2", response: { status: "error", duration: 50 } }), 112 | createMockRequest({ id: "3", response: { status: "error", duration: 200 } }), 113 | ] 114 | const error = requests.filter((r) => r.response?.status === "error").length 115 | expect(error).toBe(2) 116 | }) 117 | 118 | it("calculates average duration", () => { 119 | const requests = [ 120 | createMockRequest({ id: "1", response: { status: "success", duration: 100 } }), 121 | createMockRequest({ id: "2", response: { status: "success", duration: 200 } }), 122 | createMockRequest({ id: "3", response: { status: "success", duration: 300 } }), 123 | ] 124 | const withDuration = requests.filter((r) => r.response?.duration) 125 | const avgDuration = 126 | withDuration.reduce((sum, r) => sum + (r.response?.duration ?? 0), 0) / 127 | (withDuration.length || 1) 128 | expect(Math.round(avgDuration)).toBe(200) 129 | }) 130 | 131 | it("handles empty requests for average duration", () => { 132 | const requests: ReturnType[] = [] 133 | const withDuration = requests.filter((r) => r.response?.duration) 134 | const avgDuration = 135 | withDuration.reduce((sum, r) => sum + (r.response?.duration ?? 0), 0) / 136 | (withDuration.length || 1) || 0 137 | expect(avgDuration).toBe(0) 138 | }) 139 | 140 | it("handles requests without responses for average duration", () => { 141 | const requests = [ 142 | createMockRequest({ id: "1" }), 143 | createMockRequest({ id: "2" }), 144 | ] 145 | const withDuration = requests.filter((r) => r.response?.duration) 146 | const avgDuration = 147 | withDuration.reduce((sum, r) => sum + (r.response?.duration ?? 0), 0) / 148 | (withDuration.length || 1) || 0 149 | expect(avgDuration).toBe(0) 150 | }) 151 | }) 152 | 153 | describe("request matching", () => { 154 | it("matches response to request by id", () => { 155 | const requests = [ 156 | { id: "req-1", method: "user.get", response: undefined }, 157 | { id: "req-2", method: "user.list", response: undefined }, 158 | ] 159 | 160 | const responsePayload = { 161 | requestId: "req-1", 162 | status: "success" as const, 163 | data: { name: "John" }, 164 | duration: 150, 165 | timestamp: Date.now(), 166 | } 167 | 168 | const updated = requests.map((req) => 169 | req.id === responsePayload.requestId 170 | ? { 171 | ...req, 172 | response: { 173 | status: responsePayload.status, 174 | data: responsePayload.data, 175 | duration: responsePayload.duration, 176 | timestamp: responsePayload.timestamp, 177 | }, 178 | } 179 | : req, 180 | ) 181 | 182 | expect(updated[0].response).toBeDefined() 183 | expect(updated[0].response?.status).toBe("success") 184 | expect(updated[1].response).toBeUndefined() 185 | }) 186 | }) 187 | 188 | describe("MAX_REQUESTS limit logic", () => { 189 | it("slices to max requests when adding new ones", () => { 190 | const MAX_REQUESTS = 500 191 | const existingRequests = Array.from({ length: 500 }, (_, i) => ({ 192 | id: `req-${i}`, 193 | timestamp: i, 194 | })) 195 | 196 | const newRequest = { id: "req-new", timestamp: 1000 } 197 | 198 | // Simulate: Add to beginning and trim 199 | const updated = [newRequest, ...existingRequests].slice(0, MAX_REQUESTS) 200 | 201 | expect(updated.length).toBe(500) 202 | expect(updated[0].id).toBe("req-new") 203 | expect(updated[499].id).toBe("req-498") // Last old one should be req-498 204 | }) 205 | 206 | it("keeps newest requests when limit exceeded", () => { 207 | const MAX_REQUESTS = 3 208 | const requests = [ 209 | { id: "old-1", timestamp: 1 }, 210 | { id: "old-2", timestamp: 2 }, 211 | { id: "old-3", timestamp: 3 }, 212 | ] 213 | 214 | const newRequest = { id: "new", timestamp: 4 } 215 | const updated = [newRequest, ...requests].slice(0, MAX_REQUESTS) 216 | 217 | expect(updated.map((r) => r.id)).toEqual(["new", "old-1", "old-2"]) 218 | expect(updated.find((r) => r.id === "old-3")).toBeUndefined() 219 | }) 220 | }) 221 | -------------------------------------------------------------------------------- /src/components/styles.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from "react" 2 | 3 | /** 4 | * CSS-in-JS styles for the RPC Devtools components 5 | * Dark theme inspired by TanStack Devtools 6 | */ 7 | export const styles = { 8 | // Layout 9 | panel: { 10 | display: "flex", 11 | flexDirection: "column", 12 | height: "100%", 13 | backgroundColor: "#111827", // gray-900 14 | color: "#f3f4f6", // gray-100 15 | fontFamily: 16 | 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 17 | fontSize: "14px", 18 | } satisfies CSSProperties, 19 | 20 | // Header 21 | header: { 22 | display: "flex", 23 | alignItems: "center", 24 | justifyContent: "space-between", 25 | borderBottom: "1px solid #374151", // gray-700 26 | backgroundColor: "#1f2937", // gray-800 27 | padding: "8px 12px", 28 | } satisfies CSSProperties, 29 | 30 | headerLeft: { 31 | display: "flex", 32 | alignItems: "center", 33 | gap: "16px", 34 | } satisfies CSSProperties, 35 | 36 | headerTitle: { 37 | fontWeight: 600, 38 | fontSize: "14px", 39 | margin: 0, 40 | } satisfies CSSProperties, 41 | 42 | headerStats: { 43 | display: "flex", 44 | alignItems: "center", 45 | gap: "12px", 46 | color: "#9ca3af", // gray-400 47 | fontSize: "12px", 48 | } satisfies CSSProperties, 49 | 50 | // Filter 51 | filterContainer: { 52 | borderBottom: "1px solid #374151", 53 | padding: "8px 12px", 54 | } satisfies CSSProperties, 55 | 56 | filterInput: { 57 | width: "100%", 58 | borderRadius: "4px", 59 | border: "1px solid #4b5563", // gray-600 60 | backgroundColor: "#1f2937", // gray-800 61 | padding: "6px 8px", 62 | fontSize: "14px", 63 | color: "#f3f4f6", 64 | outline: "none", 65 | } satisfies CSSProperties, 66 | 67 | // Content area 68 | content: { 69 | display: "flex", 70 | flex: 1, 71 | overflow: "hidden", 72 | } satisfies CSSProperties, 73 | 74 | // Buttons 75 | clearButton: { 76 | borderRadius: "4px", 77 | backgroundColor: "#dc2626", // red-600 78 | padding: "4px 8px", 79 | fontWeight: 500, 80 | fontSize: "12px", 81 | color: "white", 82 | border: "none", 83 | cursor: "pointer", 84 | } satisfies CSSProperties, 85 | 86 | // Request List 87 | listContainer: { 88 | flex: 1, 89 | overflow: "auto", 90 | } satisfies CSSProperties, 91 | 92 | emptyState: { 93 | display: "flex", 94 | flex: 1, 95 | alignItems: "center", 96 | justifyContent: "center", 97 | color: "#6b7280", // gray-500 98 | fontSize: "14px", 99 | } satisfies CSSProperties, 100 | 101 | table: { 102 | width: "100%", 103 | fontSize: "13px", 104 | borderCollapse: "collapse", 105 | } satisfies CSSProperties, 106 | 107 | tableHeader: { 108 | position: "sticky", 109 | top: 0, 110 | backgroundColor: "#1f2937", // gray-800 111 | textAlign: "left", 112 | color: "#9ca3af", // gray-400 113 | } satisfies CSSProperties, 114 | 115 | tableHeaderCell: { 116 | padding: "8px 12px", 117 | fontWeight: 500, 118 | } satisfies CSSProperties, 119 | 120 | tableRow: (isSelected: boolean): CSSProperties => ({ 121 | cursor: "pointer", 122 | borderBottom: "1px solid #374151", // gray-700 123 | backgroundColor: isSelected ? "#374151" : "transparent", 124 | transition: "background-color 0.15s", 125 | }), 126 | 127 | tableCell: { 128 | padding: "8px 12px", 129 | } satisfies CSSProperties, 130 | 131 | methodCode: { 132 | color: "#60a5fa", // blue-400 133 | fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace', 134 | } satisfies CSSProperties, 135 | 136 | // Status badges 137 | statusBadge: { 138 | base: { 139 | display: "inline-flex", 140 | alignItems: "center", 141 | gap: "4px", 142 | borderRadius: "4px", 143 | padding: "2px 8px", 144 | fontWeight: 500, 145 | fontSize: "11px", 146 | } satisfies CSSProperties, 147 | pending: { 148 | backgroundColor: "rgba(234, 179, 8, 0.2)", // yellow-500/20 149 | color: "#facc15", // yellow-400 150 | } satisfies CSSProperties, 151 | success: { 152 | backgroundColor: "rgba(34, 197, 94, 0.2)", // green-500/20 153 | color: "#4ade80", // green-400 154 | } satisfies CSSProperties, 155 | error: { 156 | backgroundColor: "rgba(239, 68, 68, 0.2)", // red-500/20 157 | color: "#f87171", // red-400 158 | } satisfies CSSProperties, 159 | }, 160 | 161 | statusDot: (color: string, animate = false): CSSProperties => ({ 162 | width: "6px", 163 | height: "6px", 164 | borderRadius: "50%", 165 | backgroundColor: color, 166 | animation: animate ? "pulse 2s infinite" : undefined, 167 | }), 168 | 169 | // Type badges 170 | typeBadge: { 171 | base: { 172 | marginLeft: "8px", 173 | borderRadius: "4px", 174 | padding: "2px 6px", 175 | fontWeight: 500, 176 | fontSize: "10px", 177 | } satisfies CSSProperties, 178 | mutation: { 179 | backgroundColor: "rgba(139, 92, 246, 0.2)", // purple-500/20 180 | color: "#a78bfa", // purple-400 181 | } satisfies CSSProperties, 182 | query: { 183 | backgroundColor: "rgba(6, 182, 212, 0.2)", // cyan-500/20 184 | color: "#22d3ee", // cyan-400 185 | } satisfies CSSProperties, 186 | }, 187 | 188 | // Detail panel 189 | detailPanel: { 190 | display: "flex", 191 | flexDirection: "column", 192 | width: "400px", 193 | borderLeft: "1px solid #374151", // gray-700 194 | backgroundColor: "#111827", // gray-900 195 | } satisfies CSSProperties, 196 | 197 | // Tabs 198 | tabsContainer: { 199 | display: "flex", 200 | borderBottom: "1px solid #374151", // gray-700 201 | } satisfies CSSProperties, 202 | 203 | tab: (isActive: boolean): CSSProperties => ({ 204 | padding: "8px 16px", 205 | fontWeight: 500, 206 | fontSize: "14px", 207 | textTransform: "capitalize", 208 | cursor: "pointer", 209 | border: "none", 210 | background: "transparent", 211 | color: isActive ? "#60a5fa" : "#9ca3af", // blue-400 or gray-400 212 | borderBottom: isActive ? "2px solid #60a5fa" : "2px solid transparent", 213 | marginBottom: isActive ? "-1px" : 0, 214 | transition: "color 0.15s", 215 | }), 216 | 217 | tabContent: { 218 | flex: 1, 219 | overflow: "auto", 220 | padding: "12px", 221 | } satisfies CSSProperties, 222 | 223 | // Detail sections 224 | section: { 225 | marginBottom: "12px", 226 | } satisfies CSSProperties, 227 | 228 | sectionHeader: { 229 | display: "flex", 230 | alignItems: "center", 231 | justifyContent: "space-between", 232 | marginBottom: "4px", 233 | } satisfies CSSProperties, 234 | 235 | sectionTitle: { 236 | fontWeight: 500, 237 | color: "#9ca3af", // gray-400 238 | fontSize: "10px", 239 | textTransform: "uppercase", 240 | letterSpacing: "0.05em", 241 | } satisfies CSSProperties, 242 | 243 | // JSON viewer 244 | jsonPre: (isError = false): CSSProperties => ({ 245 | maxHeight: "300px", 246 | overflow: "auto", 247 | borderRadius: "4px", 248 | backgroundColor: "#111827", // gray-900 249 | padding: "8px", 250 | fontSize: "12px", 251 | fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace', 252 | color: isError ? "#fca5a5" : "#d1d5db", // red-300 or gray-300 253 | margin: 0, 254 | whiteSpace: "pre-wrap", 255 | wordBreak: "break-word", 256 | }), 257 | 258 | // Copy button 259 | copyButton: { 260 | color: "#9ca3af", // gray-400 261 | fontSize: "12px", 262 | background: "transparent", 263 | border: "none", 264 | cursor: "pointer", 265 | transition: "color 0.15s", 266 | } satisfies CSSProperties, 267 | 268 | // Misc text colors 269 | textGray200: { color: "#e5e7eb" } satisfies CSSProperties, 270 | textGray300: { color: "#d1d5db" } satisfies CSSProperties, 271 | textGray400: { color: "#9ca3af" } satisfies CSSProperties, 272 | textGray500: { color: "#6b7280" } satisfies CSSProperties, 273 | textYellow400: { color: "#facc15" } satisfies CSSProperties, 274 | textGreen400: { color: "#4ade80" } satisfies CSSProperties, 275 | textRed400: { color: "#f87171" } satisfies CSSProperties, 276 | textBlue400: { color: "#60a5fa" } satisfies CSSProperties, 277 | 278 | // Time display 279 | timeCell: { 280 | textAlign: "right", 281 | color: "#9ca3af", 282 | fontVariantNumeric: "tabular-nums", 283 | } satisfies CSSProperties, 284 | 285 | whenCell: { 286 | textAlign: "right", 287 | color: "#6b7280", 288 | fontSize: "12px", 289 | } satisfies CSSProperties, 290 | 291 | // Header cell alignment 292 | headerCellRight: { 293 | textAlign: "right", 294 | } satisfies CSSProperties, 295 | 296 | // Inline CSS for the pulsing animation 297 | pulseKeyframes: ` 298 | @keyframes rpc-devtools-pulse { 299 | 0%, 100% { opacity: 1; } 300 | 50% { opacity: 0.5; } 301 | } 302 | `, 303 | } as const 304 | 305 | /** 306 | * Inject the pulse animation keyframes into the document 307 | * Call this once when the component mounts 308 | */ 309 | export function injectKeyframes() { 310 | if (typeof document === "undefined") return 311 | const id = "rpc-devtools-keyframes" 312 | if (document.getElementById(id)) return 313 | 314 | const style = document.createElement("style") 315 | style.id = id 316 | style.textContent = styles.pulseKeyframes 317 | document.head.appendChild(style) 318 | } 319 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "configVersion": 1, 4 | "workspaces": { 5 | "": { 6 | "name": "@hazel/rpc-devtools", 7 | "devDependencies": { 8 | "@effect/rpc": "^0.70.0", 9 | "@tanstack/devtools-event-client": "^0.3.0", 10 | "@types/node": "^24.10.1", 11 | "@types/react": "^19.2.7", 12 | "effect": "^3.16.0", 13 | "react": "^19.1.0", 14 | "tsdown": "^0.17.2", 15 | "typescript": "^5.9.3", 16 | "vitest": "^3.2.4", 17 | }, 18 | "peerDependencies": { 19 | "@effect/rpc": ">=0.70.0", 20 | "@tanstack/devtools-event-client": ">=0.3.0", 21 | "effect": ">=3.0.0", 22 | "react": ">=18.0.0", 23 | }, 24 | }, 25 | }, 26 | "packages": { 27 | "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], 28 | 29 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 30 | 31 | "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 32 | 33 | "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], 34 | 35 | "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], 36 | 37 | "@effect/platform": ["@effect/platform@0.93.6", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.8" } }, "sha512-I5lBGQWzWXP4zlIdPs7z7WHmEFVBQhn+74emr/h16GZX96EEJ6I1rjGaKyZF7mtukbMuo9wEckDPssM8vskZ/w=="], 38 | 39 | "@effect/rpc": ["@effect/rpc@0.70.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.91.1", "effect": "^3.17.14" } }, "sha512-ZFgDL8iAF4nQsWFwOcuzBcA24QNv8qvwjoqTxGfQViXrDBt/naqMSleycKptLJVyOec0ntTMn+hp5xiiSwz0wg=="], 40 | 41 | "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], 42 | 43 | "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], 44 | 45 | "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], 46 | 47 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 48 | 49 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 50 | 51 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 52 | 53 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 54 | 55 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 56 | 57 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 58 | 59 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 60 | 61 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 62 | 63 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 64 | 65 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 66 | 67 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 68 | 69 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 70 | 71 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 72 | 73 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 74 | 75 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 76 | 77 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 78 | 79 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 80 | 81 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 82 | 83 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 84 | 85 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 86 | 87 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 88 | 89 | "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 90 | 91 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 92 | 93 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 94 | 95 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 96 | 97 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 98 | 99 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], 100 | 101 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 102 | 103 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 104 | 105 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 106 | 107 | "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], 108 | 109 | "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], 110 | 111 | "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], 112 | 113 | "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], 114 | 115 | "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], 116 | 117 | "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], 118 | 119 | "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], 120 | 121 | "@oxc-project/types": ["@oxc-project/types@0.101.0", "", {}, "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ=="], 122 | 123 | "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], 124 | 125 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.53", "", { "os": "android", "cpu": "arm64" }, "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ=="], 126 | 127 | "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.53", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg=="], 128 | 129 | "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.53", "", { "os": "darwin", "cpu": "x64" }, "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ=="], 130 | 131 | "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.53", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w=="], 132 | 133 | "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53", "", { "os": "linux", "cpu": "arm" }, "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ=="], 134 | 135 | "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53", "", { "os": "linux", "cpu": "arm64" }, "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg=="], 136 | 137 | "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.53", "", { "os": "linux", "cpu": "arm64" }, "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA=="], 138 | 139 | "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.53", "", { "os": "linux", "cpu": "x64" }, "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA=="], 140 | 141 | "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.53", "", { "os": "linux", "cpu": "x64" }, "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw=="], 142 | 143 | "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.53", "", { "os": "none", "cpu": "arm64" }, "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A=="], 144 | 145 | "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.53", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.0" }, "cpu": "none" }, "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg=="], 146 | 147 | "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53", "", { "os": "win32", "cpu": "arm64" }, "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw=="], 148 | 149 | "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.53", "", { "os": "win32", "cpu": "x64" }, "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA=="], 150 | 151 | "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], 152 | 153 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], 154 | 155 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], 156 | 157 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], 158 | 159 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], 160 | 161 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], 162 | 163 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], 164 | 165 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], 166 | 167 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], 168 | 169 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], 170 | 171 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], 172 | 173 | "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], 174 | 175 | "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], 176 | 177 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], 178 | 179 | "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], 180 | 181 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], 182 | 183 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], 184 | 185 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], 186 | 187 | "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], 188 | 189 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], 190 | 191 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], 192 | 193 | "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], 194 | 195 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], 196 | 197 | "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 198 | 199 | "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="], 200 | 201 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], 202 | 203 | "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], 204 | 205 | "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], 206 | 207 | "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 208 | 209 | "@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="], 210 | 211 | "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], 212 | 213 | "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], 214 | 215 | "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], 216 | 217 | "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], 218 | 219 | "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], 220 | 221 | "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], 222 | 223 | "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], 224 | 225 | "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], 226 | 227 | "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], 228 | 229 | "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], 230 | 231 | "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], 232 | 233 | "birpc": ["birpc@3.0.0", "", {}, "sha512-by+04pHuxpCEQcucAXqzopqfhyI8TLK5Qg5MST0cB6MP+JhHna9ollrtK9moVh27aq6Q6MEJgebD0cVm//yBkg=="], 234 | 235 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], 236 | 237 | "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], 238 | 239 | "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], 240 | 241 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 242 | 243 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 244 | 245 | "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], 246 | 247 | "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 248 | 249 | "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], 250 | 251 | "effect": ["effect@3.19.9", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-taMXnfG/p+j7AmMOHHQaCHvjqwu9QBO3cxuZqL2dMG/yWcEMw0ZHruHe9B49OxtfKH/vKKDDKRhZ+1GJ2p5R5w=="], 252 | 253 | "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], 254 | 255 | "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], 256 | 257 | "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 258 | 259 | "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 260 | 261 | "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], 262 | 263 | "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], 264 | 265 | "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 266 | 267 | "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], 268 | 269 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 270 | 271 | "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], 272 | 273 | "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], 274 | 275 | "import-without-cache": ["import-without-cache@0.2.2", "", {}, "sha512-4TTuRrZ0jBULXzac3EoX9ZviOs8Wn9iAbNhJEyLhTpAGF9eNmYSruaMMN/Tec/yqaO7H6yS2kALfQDJ5FxfatA=="], 276 | 277 | "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], 278 | 279 | "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 280 | 281 | "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], 282 | 283 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 284 | 285 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 286 | 287 | "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], 288 | 289 | "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], 290 | 291 | "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], 292 | 293 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 294 | 295 | "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], 296 | 297 | "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], 298 | 299 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 300 | 301 | "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], 302 | 303 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 304 | 305 | "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 306 | 307 | "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], 308 | 309 | "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], 310 | 311 | "quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], 312 | 313 | "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], 314 | 315 | "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 316 | 317 | "rolldown": ["rolldown@1.0.0-beta.53", "", { "dependencies": { "@oxc-project/types": "=0.101.0", "@rolldown/pluginutils": "1.0.0-beta.53" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.53", "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", "@rolldown/binding-darwin-x64": "1.0.0-beta.53", "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw=="], 318 | 319 | "rolldown-plugin-dts": ["rolldown-plugin-dts@0.18.3", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ast-kit": "^2.2.0", "birpc": "^3.0.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.0", "magic-string": "^0.30.21", "obug": "^2.1.1" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.51", "typescript": "^5.0.0", "vue-tsc": "~3.1.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-rd1LZ0Awwfyn89UndUF/HoFF4oH9a5j+2ZeuKSJYM80vmeN/p0gslYMnHTQHBEXPhUlvAlqGA3tVgXB/1qFNDg=="], 320 | 321 | "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], 322 | 323 | "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 324 | 325 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], 326 | 327 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 328 | 329 | "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], 330 | 331 | "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], 332 | 333 | "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], 334 | 335 | "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], 336 | 337 | "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], 338 | 339 | "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 340 | 341 | "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], 342 | 343 | "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], 344 | 345 | "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], 346 | 347 | "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], 348 | 349 | "tsdown": ["tsdown@0.17.2", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "empathic": "^2.0.0", "hookable": "^5.5.3", "import-without-cache": "^0.2.2", "obug": "^2.1.1", "rolldown": "1.0.0-beta.53", "rolldown-plugin-dts": "^0.18.3", "semver": "^7.7.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig-core": "^7.4.2", "unrun": "^0.2.19" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@vitejs/devtools": "^0.0.0-alpha.19", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "@vitejs/devtools", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-SuU+0CWm/95KfXqojHTVuwcouIsdn7HpYcwDyOdKktJi285NxKwysjFUaxYLxpCNqqPvcFvokXLO4dZThRwzkw=="], 350 | 351 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 352 | 353 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 354 | 355 | "unconfig-core": ["unconfig-core@7.4.2", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg=="], 356 | 357 | "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 358 | 359 | "unrun": ["unrun@0.2.19", "", { "dependencies": { "rolldown": "1.0.0-beta.53" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-DbwbJ9BvPEb3BeZnIpP9S5tGLO/JIgPQ3JrpMRFIfZMZfMG19f26OlLbC2ml8RRdrI2ZA7z2t+at5tsIHbh6Qw=="], 360 | 361 | "vite": ["vite@7.2.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ=="], 362 | 363 | "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], 364 | 365 | "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], 366 | 367 | "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], 368 | 369 | "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], 370 | } 371 | } 372 | --------------------------------------------------------------------------------