diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json index ea2af9d157..d04bb55d1a 100644 --- a/examples/react/start-basic-react-query/package.json +++ b/examples/react/start-basic-react-query/package.json @@ -10,12 +10,15 @@ "start": "pnpx srvx --prod -s ../client dist/server/server.js" }, "dependencies": { + "@tanstack/devtools-vite": "^0.3.3", + "@tanstack/react-devtools": "^0.7.0", "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", "@tanstack/react-router": "^1.166.7", "@tanstack/react-router-devtools": "^1.166.7", "@tanstack/react-router-ssr-query": "^1.166.7", "@tanstack/react-start": "^1.166.7", + "@tanstack/react-start-devtools": "0.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", @@ -32,4 +35,4 @@ "vite": "^7.3.1", "vite-tsconfig-paths": "^5.1.4" } -} +} \ No newline at end of file diff --git a/examples/react/start-basic-react-query/src/routes/__root.tsx b/examples/react/start-basic-react-query/src/routes/__root.tsx index bc278f8036..45bfeb62e0 100644 --- a/examples/react/start-basic-react-query/src/routes/__root.tsx +++ b/examples/react/start-basic-react-query/src/routes/__root.tsx @@ -6,9 +6,11 @@ import { Scripts, createRootRouteWithContext, } from '@tanstack/react-router' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import * as React from 'react' +import { TanStackDevtools } from '@tanstack/react-devtools' +import { StartDevtoolsPanel } from '@tanstack/react-start-devtools' import type { QueryClient } from '@tanstack/react-query' import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' import { NotFound } from '~/components/NotFound' @@ -136,8 +138,23 @@ function RootDocument({ children }: { children: React.ReactNode }) {
{children} - - + + }, + { + name: 'React Router', + render: () => + }, + { + name: "TanStack Start", + render: () => + } + ]} /> + diff --git a/examples/react/start-basic-react-query/vite.config.ts b/examples/react/start-basic-react-query/vite.config.ts index 843599315e..b319c6956e 100644 --- a/examples/react/start-basic-react-query/vite.config.ts +++ b/examples/react/start-basic-react-query/vite.config.ts @@ -2,6 +2,7 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite' import { defineConfig } from 'vite' import tsConfigPaths from 'vite-tsconfig-paths' import viteReact from '@vitejs/plugin-react' +import { devtools } from '@tanstack/devtools-vite' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ @@ -9,6 +10,7 @@ export default defineConfig({ port: 3000, }, plugins: [ + devtools(), tailwindcss(), tsConfigPaths({ projects: ['./tsconfig.json'], diff --git a/examples/react/start-devtools-showcase/src/start.ts b/examples/react/start-devtools-showcase/src/start.ts new file mode 100644 index 0000000000..60bdec4da9 --- /dev/null +++ b/examples/react/start-devtools-showcase/src/start.ts @@ -0,0 +1,52 @@ +import { createStart, createMiddleware } from '@tanstack/react-start' + +export const requestLoggingMiddleware = createMiddleware().server( + async ({ next, pathname }) => { + const start = performance.now() + console.log(`[request] → ${pathname}`) + + const result = await next({ + context: { + requestStartTime: start, + requestPathname: pathname, + }, + }) + + const duration = performance.now() - start + console.log(`[request] ← ${pathname} (${duration.toFixed(1)}ms)`) + + return result + }, +) + +export const rateLimitMiddleware = createMiddleware().server( + async ({ next }) => { + return next({ + context: { + rateLimited: false, + }, + }) + }, +) + +export const globalFunctionMiddleware = createMiddleware({ + type: 'function', +}).server(async ({ next }) => { + const start = performance.now() + + const result = await next({ + context: { + globalFnMiddlewareApplied: true, + }, + }) + + const duration = performance.now() - start + console.log(`[global-fn-middleware] executed in ${duration.toFixed(1)}ms`) + + return result +}) + +export const startInstance = createStart(() => ({ + requestMiddleware: [requestLoggingMiddleware, rateLimitMiddleware], + functionMiddleware: [globalFunctionMiddleware], +})) diff --git a/package.json b/package.json index 99d62684e6..34cb9fd4b7 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,9 @@ "@tanstack/vue-start-client": "workspace:*", "@tanstack/vue-start-server": "workspace:*", "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-devtools": "workspace:*", + "@tanstack/react-start-devtools": "workspace:*", + "@tanstack/solid-start-devtools": "workspace:*", "@tanstack/start-client-core": "workspace:*", "@tanstack/start-server-core": "workspace:*", "@tanstack/start-storage-context": "workspace:*", @@ -135,4 +138,4 @@ "@tanstack/nitro-v2-vite-plugin": "workspace:*" } } -} +} \ No newline at end of file diff --git a/packages/react-start-devtools/eslint.config.js b/packages/react-start-devtools/eslint.config.js new file mode 100644 index 0000000000..3b8dc15043 --- /dev/null +++ b/packages/react-start-devtools/eslint.config.js @@ -0,0 +1,32 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import pluginReactCompiler from 'eslint-plugin-react-compiler' +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['**/*.{ts,tsx}'], + ...pluginReact.configs.recommended, + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + 'react-compiler': pluginReactCompiler, + }, + rules: { + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-compiler/react-compiler': 'error', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, + { + files: ['**/__tests__/**'], + rules: { + // 'react-compiler/react-compiler': 'off', + }, + }, +] diff --git a/packages/react-start-devtools/package.json b/packages/react-start-devtools/package.json new file mode 100644 index 0000000000..d3a3f99e83 --- /dev/null +++ b/packages/react-start-devtools/package.json @@ -0,0 +1,70 @@ +{ + "name": "@tanstack/react-start-devtools", + "version": "0.0.1", + "description": "React adapter for devtools for Start.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/react-start-devtools" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [], + "type": "module", + "types": "dist/esm/index.d.ts", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./production": { + "import": { + "types": "./dist/esm/production.d.ts", + "default": "./dist/esm/production.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "files": [ + "dist/", + "src" + ], + "scripts": { + "clean": "rimraf ./build ./dist", + "lint:fix": "eslint ./src --fix", + "test:eslint": "eslint ./src", + "test:lib": "vitest", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc", + "test:build": "publint --strict", + "build": "vite build" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "@types/react-dom": ">=16.8", + "react": ">=16.8", + "react-dom": ">=16.8" + }, + "dependencies": { + "@tanstack/devtools-utils": "^0.0.3", + "@tanstack/start-devtools": "workspace:*" + }, + "devDependencies": { + "@eslint-react/eslint-plugin": "^1.53.1", + "@vitejs/plugin-react": "^5.0.2", + "eslint-plugin-react-compiler": "19.1.0-rc.2", + "eslint-plugin-react-hooks": "^5.2.0" + } +} \ No newline at end of file diff --git a/packages/react-start-devtools/src/ReactStartDevtools.tsx b/packages/react-start-devtools/src/ReactStartDevtools.tsx new file mode 100644 index 0000000000..18849cfd2c --- /dev/null +++ b/packages/react-start-devtools/src/ReactStartDevtools.tsx @@ -0,0 +1,10 @@ + +import { createReactPanel } from '@tanstack/devtools-utils/react' +import { StartDevtoolsCore } from "@tanstack/start-devtools" +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react'; + +export interface StartDevtoolsReactInit extends DevtoolsPanelProps { } + +const [StartDevtoolsPanel, StartDevtoolsPanelNoOp] = createReactPanel(StartDevtoolsCore) + +export { StartDevtoolsPanel, StartDevtoolsPanelNoOp } \ No newline at end of file diff --git a/packages/react-start-devtools/src/index.ts b/packages/react-start-devtools/src/index.ts new file mode 100644 index 0000000000..be08ae76a3 --- /dev/null +++ b/packages/react-start-devtools/src/index.ts @@ -0,0 +1,16 @@ +'use client' + +import * as Devtools from './ReactStartDevtools' +import * as plugin from './plugin' + +export const StartDevtoolsPanel = + process.env.NODE_ENV !== 'development' + ? Devtools.StartDevtoolsPanelNoOp + : Devtools.StartDevtoolsPanel + +export const startDevtoolsPlugin = + process.env.NODE_ENV !== 'development' + ? plugin.startDevtoolsNoOpPlugin + : plugin.startDevtoolsPlugin + +export type { StartDevtoolsReactInit } from './ReactStartDevtools' diff --git a/packages/react-start-devtools/src/plugin.tsx b/packages/react-start-devtools/src/plugin.tsx new file mode 100644 index 0000000000..f1073b77ae --- /dev/null +++ b/packages/react-start-devtools/src/plugin.tsx @@ -0,0 +1,6 @@ +import { createReactPlugin } from '@tanstack/devtools-utils/react' +import { StartDevtoolsPanel } from './ReactStartDevtools' + +const [startDevtoolsPlugin, startDevtoolsNoOpPlugin] = createReactPlugin("TanStack Start", StartDevtoolsPanel) + +export { startDevtoolsPlugin, startDevtoolsNoOpPlugin } \ No newline at end of file diff --git a/packages/react-start-devtools/src/production.ts b/packages/react-start-devtools/src/production.ts new file mode 100644 index 0000000000..0d434cb441 --- /dev/null +++ b/packages/react-start-devtools/src/production.ts @@ -0,0 +1,7 @@ +'use client' + +export { StartDevtoolsPanel } from './ReactStartDevtools' + +export type { StartDevtoolsReactInit } from './ReactStartDevtools' + +export { startDevtoolsPlugin } from './plugin' diff --git a/packages/react-start-devtools/tests/index.test.ts b/packages/react-start-devtools/tests/index.test.ts new file mode 100644 index 0000000000..350ef3adeb --- /dev/null +++ b/packages/react-start-devtools/tests/index.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from 'vitest' + +describe('test suite', () => { + it('should work', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/react-start-devtools/tests/test-setup.ts b/packages/react-start-devtools/tests/test-setup.ts new file mode 100644 index 0000000000..a9d0dd31aa --- /dev/null +++ b/packages/react-start-devtools/tests/test-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/packages/react-start-devtools/tsconfig.docs.json b/packages/react-start-devtools/tsconfig.docs.json new file mode 100644 index 0000000000..2880b4dfa6 --- /dev/null +++ b/packages/react-start-devtools/tsconfig.docs.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["tests", "src"] +} diff --git a/packages/react-start-devtools/tsconfig.json b/packages/react-start-devtools/tsconfig.json new file mode 100644 index 0000000000..bf8f85d34c --- /dev/null +++ b/packages/react-start-devtools/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "eslint.config.js", "vite.config.ts", "tests"], + "compilerOptions": { + "jsx": "react" + } +} diff --git a/packages/react-start-devtools/vite.config.ts b/packages/react-start-devtools/vite.config.ts new file mode 100644 index 0000000000..e0c1757b6d --- /dev/null +++ b/packages/react-start-devtools/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import react from '@vitejs/plugin-react' +import packageJson from './package.json' + +const config = defineConfig({ + plugins: [react()], + test: { + name: packageJson.name, + dir: './', + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/test-setup.ts'], + globals: true, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts', './src/production.ts'], + srcDir: './src', + cjs: false, + }), +) diff --git a/packages/solid-start-devtools/eslint.config.js b/packages/solid-start-devtools/eslint.config.js new file mode 100644 index 0000000000..e472c69e73 --- /dev/null +++ b/packages/solid-start-devtools/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + rules: {}, + }, +] diff --git a/packages/solid-start-devtools/package.json b/packages/solid-start-devtools/package.json new file mode 100644 index 0000000000..2a54449d06 --- /dev/null +++ b/packages/solid-start-devtools/package.json @@ -0,0 +1,64 @@ +{ + "name": "@tanstack/solid-start-devtools", + "version": "0.0.1", + "description": "Solid adapter for devtools for Start.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/solid-start-devtools" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [], + "type": "module", + "types": "dist/esm/index.d.ts", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./production": { + "import": { + "types": "./dist/esm/production.d.ts", + "default": "./dist/esm/production.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "files": [ + "dist/", + "src" + ], + "scripts": { + "clean": "rimraf ./build ./dist", + "lint:fix": "eslint ./src --fix", + "test:eslint": "eslint ./src", + "test:lib": "vitest", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc", + "test:build": "publint --strict", + "build": "vite build" + }, + "peerDependencies": { + "solid-js": ">=1.9.7" + }, + "dependencies": { + "@tanstack/devtools-utils": "^0.0.3", + "@tanstack/start-devtools": "workspace:*" + }, + "devDependencies": { + "vite-plugin-solid": "^2.11.8" + } +} \ No newline at end of file diff --git a/packages/solid-start-devtools/src/SolidStartDevtools.tsx b/packages/solid-start-devtools/src/SolidStartDevtools.tsx new file mode 100644 index 0000000000..d5caf8fb16 --- /dev/null +++ b/packages/solid-start-devtools/src/SolidStartDevtools.tsx @@ -0,0 +1,10 @@ + +import { createSolidPanel } from "@tanstack/devtools-utils/solid" +import { StartDevtoolsCore } from "@tanstack/start-devtools" +import type { DevtoolsPanelProps } from "@tanstack/devtools-utils/solid"; + +const [StartDevtoolsPanel, StartDevtoolsPanelNoOp] = createSolidPanel(StartDevtoolsCore) +export interface StartDevtoolsSolidInit extends DevtoolsPanelProps { +} + +export { StartDevtoolsPanel, StartDevtoolsPanelNoOp } \ No newline at end of file diff --git a/packages/solid-start-devtools/src/index.ts b/packages/solid-start-devtools/src/index.ts new file mode 100644 index 0000000000..a00a551190 --- /dev/null +++ b/packages/solid-start-devtools/src/index.ts @@ -0,0 +1,15 @@ + +import * as Devtools from './SolidStartDevtools' +import * as plugin from './plugin' + +export const StartDevtoolsPanel = + process.env.NODE_ENV !== 'development' + ? Devtools.StartDevtoolsPanelNoOp + : Devtools.StartDevtoolsPanel + +export const startDevtoolsPlugin = + process.env.NODE_ENV !== 'development' + ? plugin.startDevtoolsNoOpPlugin + : plugin.startDevtoolsPlugin + +export type { StartDevtoolsSolidInit } from './SolidStartDevtools' diff --git a/packages/solid-start-devtools/src/plugin.tsx b/packages/solid-start-devtools/src/plugin.tsx new file mode 100644 index 0000000000..d720a7396f --- /dev/null +++ b/packages/solid-start-devtools/src/plugin.tsx @@ -0,0 +1,6 @@ +import { createSolidPlugin } from '@tanstack/devtools-utils/solid' +import { StartDevtoolsPanel } from './SolidStartDevtools' + +const [startDevtoolsPlugin, startDevtoolsNoOpPlugin] = createSolidPlugin("TanStack Start", StartDevtoolsPanel) + +export { startDevtoolsPlugin, startDevtoolsNoOpPlugin } \ No newline at end of file diff --git a/packages/solid-start-devtools/src/production.ts b/packages/solid-start-devtools/src/production.ts new file mode 100644 index 0000000000..6579ba0825 --- /dev/null +++ b/packages/solid-start-devtools/src/production.ts @@ -0,0 +1,6 @@ + +export { StartDevtoolsPanel } from './SolidStartDevtools' + +export type { StartDevtoolsSolidInit } from './SolidStartDevtools' + +export { startDevtoolsPlugin } from './plugin' diff --git a/packages/solid-start-devtools/tests/index.test.ts b/packages/solid-start-devtools/tests/index.test.ts new file mode 100644 index 0000000000..350ef3adeb --- /dev/null +++ b/packages/solid-start-devtools/tests/index.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from 'vitest' + +describe('test suite', () => { + it('should work', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/solid-start-devtools/tests/test-setup.ts b/packages/solid-start-devtools/tests/test-setup.ts new file mode 100644 index 0000000000..a9d0dd31aa --- /dev/null +++ b/packages/solid-start-devtools/tests/test-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/packages/solid-start-devtools/tsconfig.docs.json b/packages/solid-start-devtools/tsconfig.docs.json new file mode 100644 index 0000000000..2880b4dfa6 --- /dev/null +++ b/packages/solid-start-devtools/tsconfig.docs.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["tests", "src"] +} diff --git a/packages/solid-start-devtools/tsconfig.json b/packages/solid-start-devtools/tsconfig.json new file mode 100644 index 0000000000..3ee4c951e3 --- /dev/null +++ b/packages/solid-start-devtools/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "eslint.config.js", "vite.config.ts", "tests"], + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js" + } +} diff --git a/packages/solid-start-devtools/vite.config.ts b/packages/solid-start-devtools/vite.config.ts new file mode 100644 index 0000000000..7473089b27 --- /dev/null +++ b/packages/solid-start-devtools/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import solid from 'vite-plugin-solid' +import packageJson from './package.json' + +const config = defineConfig({ + plugins: [solid()], + test: { + name: packageJson.name, + dir: './', + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/test-setup.ts'], + globals: true, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts', './src/production.ts'], + srcDir: './src', + cjs: false, + }), +) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 42eef35aec..a0491ff062 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -150,6 +150,8 @@ export const createServerFn: CreateServerFn = (options, __opts) => { // this function __executeServer: async (opts: any) => { const startContext = getStartContextServerOnly() + const { requestId, requestStartTime, eventClient } = + startContext ?? {} const serverContextAfterGlobalMiddlewares = startContext.contextAfterGlobalMiddlewares // Use safeObjectMerge for opts.context which comes from client @@ -168,6 +170,7 @@ export const createServerFn: CreateServerFn = (options, __opts) => { request: startContext.request, } + const mwStart = performance.now() const result = await executeMiddleware( resolvedMiddleware, 'server', @@ -178,6 +181,27 @@ export const createServerFn: CreateServerFn = (options, __opts) => { error: d.error, context: d.sendContext, })) + const mwDuration = performance.now() - mwStart + + if ( + process.env.NODE_ENV !== 'production' && + requestId && + eventClient + ) { + eventClient.emit('middleware-executed', { + requestId, + scope: 'server-fn', + chain: [ + { + name: 'server-fn-middleware-chain', + startTime: mwStart, + endTime: mwStart + mwDuration, + }, + ], + totalDuration: mwDuration, + startTime: mwStart - (requestStartTime ?? mwStart), + }) + } return result }, diff --git a/packages/start-devtools/CHANGELOG.md b/packages/start-devtools/CHANGELOG.md new file mode 100644 index 0000000000..04f439c113 --- /dev/null +++ b/packages/start-devtools/CHANGELOG.md @@ -0,0 +1,2 @@ +# @tanstack/start-devtools + \ No newline at end of file diff --git a/packages/start-devtools/eslint.config.js b/packages/start-devtools/eslint.config.js new file mode 100644 index 0000000000..e472c69e73 --- /dev/null +++ b/packages/start-devtools/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + rules: {}, + }, +] diff --git a/packages/start-devtools/package.json b/packages/start-devtools/package.json new file mode 100644 index 0000000000..88f5d9b215 --- /dev/null +++ b/packages/start-devtools/package.json @@ -0,0 +1,70 @@ +{ + "name": "@tanstack/start-devtools", + "version": "0.0.1", + "description": "Devtools for Start.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/start-devtools" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "devtools", + "start" + ], + "type": "module", + "types": "dist/esm/index.d.ts", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./production": { + "import": { + "types": "./dist/esm/production.d.ts", + "default": "./dist/esm/production.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "files": [ + "dist/", + "src" + ], + "scripts": { + "clean": "rimraf ./build ./dist", + "lint:fix": "eslint ./src --fix", + "test:eslint": "eslint ./src", + "test:lib": "vitest", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc", + "test:build": "publint --strict", + "build": "vite build" + }, + "dependencies": { + "@tanstack/start-server-core": "workspace:*", + "@tanstack/devtools-ui": "^0.3.5", + "@tanstack/devtools-utils": "^0.0.3", + "@tanstack/solid-store": "^0.7.5", + "clsx": "^2.1.1", + "dayjs": "^1.11.18", + "goober": "^2.1.16", + "solid-js": "^1.9.9" + }, + "devDependencies": { + "vite-plugin-solid": "^2.11.8" + } +} \ No newline at end of file diff --git a/packages/start-devtools/src/StartDevtools.tsx b/packages/start-devtools/src/StartDevtools.tsx new file mode 100644 index 0000000000..5e3fe7ac21 --- /dev/null +++ b/packages/start-devtools/src/StartDevtools.tsx @@ -0,0 +1,202 @@ +/** @jsxImportSource solid-js */ +import { createSignal, createMemo, For, Show, onCleanup } from 'solid-js' +import { MainPanel, Header, HeaderLogo, Button } from '@tanstack/devtools-ui' +import { createRequestStore, type RequestEntry } from './store' +import FilterBar, { type Filters } from './components/FilterBar' +import RequestRow, { RequestTableHeader } from './components/RequestRow' +import TimelineOverview from './components/TimelineOverview' +import DetailSidebar from './components/DetailSidebar' + +export default function StartDevtools() { + const store = createRequestStore() + onCleanup(() => store.cleanup()) + + const [selectedId, setSelectedId] = createSignal(null) + const [timelineRange, setTimelineRange] = createSignal<[number, number]>([ + 0, 1, + ]) + const [filters, setFilters] = createSignal({ + method: null, + statusRange: null, + type: null, + urlSearch: '', + }) + + const allEntries = createMemo(() => Array.from(store.entries.values())) + + // Time span across all entries (for timeline) + const timeSpan = createMemo(() => { + const entries = allEntries() + if (entries.length === 0) return { start: 0, end: 1000 } + let minStart = Infinity + let maxEnd = -Infinity + for (const e of entries) { + if (e.startTimestamp < minStart) minStart = e.startTimestamp + const end = e.startTimestamp + (e.duration ?? 0) + if (end > maxEnd) maxEnd = end + } + if (maxEnd <= minStart) maxEnd = minStart + 1000 + return { start: minStart, end: maxEnd } + }) + + // Filter by type/method/status/url, then by timeline range + const filteredEntries = createMemo(() => { + const f = filters() + const [rangeLeft, rangeRight] = timelineRange() + const span = timeSpan() + const totalMs = span.end - span.start + + return allEntries().filter((entry: RequestEntry) => { + // Standard filters + if (f.method && entry.method !== f.method) return false + if (f.statusRange && entry.status !== null) { + const range = String(entry.status)[0] + 'xx' + if (range !== f.statusRange) return false + } + if (f.type && entry.type !== f.type) return false + if (f.urlSearch && !entry.url.includes(f.urlSearch)) return false + + // Timeline range filter + const entryStart = (entry.startTimestamp - span.start) / totalMs + const entryEnd = + (entry.startTimestamp + (entry.duration ?? 0) - span.start) / totalMs + // Show entry if it overlaps with the range + if (entryEnd < rangeLeft || entryStart > rangeRight) return false + + return true + }) + }) + + const maxTime = createMemo(() => { + let max = 0 + for (const e of filteredEntries()) { + if (e.duration && e.duration > max) max = e.duration + } + return max || 100 + }) + + const selectedEntry = createMemo(() => { + const id = selectedId() + if (!id) return null + return store.entries.get(id) ?? null + }) + + return ( + +
+ + TanStack Start + +
+ +
+
+ + + + {/* Timeline overview */} + + + {/* Main content: request table + optional detail sidebar */} +
+ {/* Request table */} +
+ +
+ 0} + fallback={ +
+ No requests captured yet. Make a request to see it here. +
+ } + > + + {(entry) => ( + + setSelectedId( + selectedId() === entry.requestId + ? null + : entry.requestId, + ) + } + /> + )} + +
+
+
+ + {/* Detail sidebar */} + + {(entry) => ( +
+ setSelectedId(null)} + /> +
+ )} +
+
+ + {/* CSS keyframes for pulse animation */} + +
+ ) +} diff --git a/packages/start-devtools/src/components/DetailSidebar.tsx b/packages/start-devtools/src/components/DetailSidebar.tsx new file mode 100644 index 0000000000..bc46269f2b --- /dev/null +++ b/packages/start-devtools/src/components/DetailSidebar.tsx @@ -0,0 +1,695 @@ +/** @jsxImportSource solid-js */ +import { createSignal, Show, For } from 'solid-js' +import type { RequestEntry } from '../store' + +type Tab = 'timing' | 'middleware' | 'headers' | 'response' + +interface DetailSidebarProps { + entry: RequestEntry + onClose: () => void +} + +const STATUS_COLORS: Record = { + '2': '#22c55e', + '3': '#eab308', + '4': '#f97316', + '5': '#ef4444', +} + +const PHASE_COLORS: Record = { + 'request-middleware': '#3b82f6', + 'route-middleware': '#60a5fa', + 'server-fn-middleware': '#93c5fd', + 'server-fn': '#f97316', + ssr: '#a855f7', +} + +const MW_SCOPE_COLORS: Record = { + request: '#3b82f6', + route: '#60a5fa', + 'server-fn': '#93c5fd', + global: '#93c5fd', + function: '#93c5fd', +} + +function displayUrl(entry: RequestEntry): string { + if (entry.serverFn?.name) return entry.serverFn.name + try { + const u = new URL(entry.url, 'http://localhost') + return u.pathname + } catch { + return entry.url + } +} + +export default function DetailSidebar(props: DetailSidebarProps) { + const [tab, setTab] = createSignal('timing') + + const statusColor = () => { + if (!props.entry.status) return '#6b7280' + return STATUS_COLORS[String(props.entry.status)[0]!] ?? '#6b7280' + } + + const maxPhaseDuration = () => { + let max = 0 + for (const p of props.entry.phases) { + if (p.duration && p.duration > max) max = p.duration + } + return max || 1 + } + + return ( +
+ {/* Header */} +
+ + {props.entry.method} + + + {displayUrl(props.entry)} + + + {props.entry.status ?? '...'} + + + {props.entry.duration !== null + ? `${props.entry.duration.toFixed(1)}ms` + : 'pending'} + + +
+ + {/* Tab bar */} +
+ + {(t) => ( + + )} + +
+ + {/* Content */} +
+ + + + + + + + + + + + +
+
+ ) +} + +/* ── Timing Tab ─────────────────────────────────────── */ + +function TimingTab(props: { entry: RequestEntry; maxDuration: number }) { + return ( +
+ {/* Phase bars */} +
+ Request Phases +
+ + {(phase) => ( +
+ + {phase.name} + +
+
+
+ + {phase.duration !== null + ? `${phase.duration.toFixed(1)}ms` + : '...'} + +
+ )} + + + No phase data yet + +
+
+ + {/* Summary */} +
+ Summary +
+ URL + {props.entry.url} + Method + {props.entry.method} + Status + + {props.entry.status !== null ? String(props.entry.status) : '...'} + + Total Duration + + {props.entry.duration !== null + ? `${props.entry.duration.toFixed(1)}ms` + : 'pending'} + + + Type + {props.entry.type} + + + Server Function + {props.entry.serverFn!.name} + Filename + {props.entry.serverFn!.filename} + +
+
+
+ ) +} + +/* ── Middleware Tab ──────────────────────────────────── */ + +function MiddlewareTab(props: { entry: RequestEntry }) { + const middlewarePhases = () => + props.entry.phases.filter((p) => p.name.endsWith('-middleware')) + + return ( +
+ + {(phase) => ( +
+ {phase.name} + 0} + fallback={ +
+ {phase.duration !== null + ? `Total: ${phase.duration.toFixed(1)}ms` + : 'In progress...'} +
+ } + > +
+ + {(mw) => { + const scope = phase.name.replace('-middleware', '') + const borderColor = + MW_SCOPE_COLORS[scope] ?? '#6b7280' + return ( +
+ + {mw.name} + + + {scope} + + + {mw.exclusiveDuration.toFixed(1)}ms + +
+ ) + }} +
+
+
+
+ )} +
+ + No middleware executed for this request + +
+ ) +} + +/* ── Headers Tab ────────────────────────────────────── */ + +function HeadersTab(props: { entry: RequestEntry }) { + return ( +
+
+ Request Headers + +
+ +
+ Response Headers + +
+
+
+ ) +} + +function HeaderList(props: { headers: Record }) { + const entries = () => Object.entries(props.headers) + + return ( +
+ 0} + fallback={No headers} + > + + {([key, value]) => ( + <> + + {key} + + + {value} + + + )} + + +
+ ) +} + +/* ── Response Tab ───────────────────────────────────── */ + +function ResponseTab(props: { entry: RequestEntry }) { + return ( +
+ +
+ Server Function +
+ Name + {props.entry.serverFn!.name} + ID + {props.entry.serverFn!.id} + Filename + {props.entry.serverFn!.filename} + Input Type + {props.entry.serverFn!.inputType} + + Result Type + {props.entry.serverFn!.resultType} + +
+
+
+ + +
+ Serialization +
+ Format + {props.entry.serialization!.format} + Content-Type + + {props.entry.serialization!.contentType} + + Raw Streams + + {props.entry.serialization!.hasRawStreams ? 'Yes' : 'No'} + +
+
+
+ + +
+ Redirect +
+ From + {props.entry.redirect!.from} + To + {props.entry.redirect!.to} + Status + {String(props.entry.redirect!.status)} +
+
+
+ + 0}> +
+ Stream Progress + + {props.entry.streamChunks.length} chunks received + +
+
+ + 0}> +
+ Errors + + {(err) => ( +
+
+ [{err.phase}] {err.message} +
+ +
+                    {err.stack}
+                  
+
+
+ )} +
+
+
+ + + No response data available + +
+ ) +} + +/* ── Shared small components ────────────────────────── */ + +function SectionTitle(props: { + children: any + style?: Record +}) { + return ( +

+ {props.children} +

+ ) +} + +function DetailKey(props: { children: any }) { + return ( + + {props.children} + + ) +} + +function DetailVal(props: { children: any; mono?: boolean }) { + return ( + + {props.children} + + ) +} + +function EmptyMessage(props: { children: any }) { + return ( +
+ {props.children} +
+ ) +} diff --git a/packages/start-devtools/src/components/FilterBar.tsx b/packages/start-devtools/src/components/FilterBar.tsx new file mode 100644 index 0000000000..80b613f1c2 --- /dev/null +++ b/packages/start-devtools/src/components/FilterBar.tsx @@ -0,0 +1,101 @@ +/** @jsxImportSource solid-js */ +import { For } from 'solid-js' + +export interface Filters { + method: string | null + statusRange: string | null + type: string | null + urlSearch: string +} + +interface FilterBarProps { + filters: Filters + onFilterChange: (filters: Filters) => void +} + +const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] +const STATUS_RANGES = ['2xx', '3xx', '4xx', '5xx'] +const TYPES = ['ssr', 'server-fn', 'server-route'] + +export default function FilterBar(props: FilterBarProps) { + const selectStyle = { + 'background-color': 'var(--tsd-bg-secondary, #2a2a3e)', + color: 'var(--tsd-text, #e0e0e0)', + border: '1px solid var(--tsd-border, #444)', + 'border-radius': '4px', + padding: '4px 8px', + 'font-size': '12px', + } + + return ( +
+ + + + + + + + props.onFilterChange({ + ...props.filters, + urlSearch: e.currentTarget.value, + }) + } + style={{ + ...selectStyle, + 'flex-grow': '1', + 'min-width': '120px', + }} + /> +
+ ) +} diff --git a/packages/start-devtools/src/components/RequestRow.tsx b/packages/start-devtools/src/components/RequestRow.tsx new file mode 100644 index 0000000000..1b1b60cd1d --- /dev/null +++ b/packages/start-devtools/src/components/RequestRow.tsx @@ -0,0 +1,172 @@ +/** @jsxImportSource solid-js */ +import { Show } from 'solid-js' +import type { RequestEntry } from '../store' +import WaterfallBar from './WaterfallBar' + +interface RequestRowProps { + entry: RequestEntry + maxTime: number + selected: boolean + onSelect: () => void +} + +const STATUS_COLORS: Record = { + '2': '#22c55e', + '3': '#eab308', + '4': '#f97316', + '5': '#ef4444', +} + +const TYPE_COLORS: Record = { + 'server-fn': '#f97316', + ssr: '#a855f7', + 'server-route': '#22c55e', +} + +const GRID_COLUMNS = '52px 1fr 42px 60px 90px 2fr' + +function displayUrl(entry: RequestEntry): string { + if (entry.serverFn?.name) return entry.serverFn.name + try { + const u = new URL(entry.url, 'http://localhost') + return u.pathname + } catch { + return entry.url + } +} + +export function RequestTableHeader() { + return ( +
+ Method + Name + Status + Time + Type + Waterfall +
+ ) +} + +export default function RequestRow(props: RequestRowProps) { + const statusColor = () => { + if (!props.entry.status) return '#6b7280' + return STATUS_COLORS[String(props.entry.status)[0]!] || '#6b7280' + } + + const typeColor = () => { + if (!props.entry.type) return '#6b7280' + return TYPE_COLORS[props.entry.type] || '#6b7280' + } + + return ( +
{ + if (!props.selected) + e.currentTarget.style.backgroundColor = 'rgba(99, 102, 241, 0.08)' + }} + onMouseLeave={(e) => { + if (!props.selected) + e.currentTarget.style.backgroundColor = 'transparent' + }} + > + + {props.entry.method} + + + + {displayUrl(props.entry)} + + + + {props.entry.status ?? '...'} + + + + {props.entry.duration !== null + ? `${props.entry.duration.toFixed(1)}ms` + : 'pending'} + + + }> + + {props.entry.type} + + + +
+ +
+
+ ) +} diff --git a/packages/start-devtools/src/components/TimelineOverview.tsx b/packages/start-devtools/src/components/TimelineOverview.tsx new file mode 100644 index 0000000000..8dbee1a7e2 --- /dev/null +++ b/packages/start-devtools/src/components/TimelineOverview.tsx @@ -0,0 +1,289 @@ +/** @jsxImportSource solid-js */ +import { createSignal, createMemo, For, Show, onCleanup } from 'solid-js' +import type { RequestEntry } from '../store' + +const TYPE_COLORS: Record = { + 'server-fn': '#f97316', + ssr: '#a855f7', + 'server-route': '#22c55e', +} + +interface TimelineOverviewProps { + entries: Array + range: [number, number] + onRangeChange: (range: [number, number]) => void +} + +export default function TimelineOverview(props: TimelineOverviewProps) { + let trackRef: HTMLDivElement | undefined + + const timeSpan = createMemo(() => { + const entries = props.entries + if (entries.length === 0) return { start: 0, end: 1000 } + let minStart = Infinity + let maxEnd = -Infinity + for (const e of entries) { + if (e.startTimestamp < minStart) minStart = e.startTimestamp + const end = e.startTimestamp + (e.duration ?? 0) + if (end > maxEnd) maxEnd = end + } + if (maxEnd <= minStart) maxEnd = minStart + 1000 + return { start: minStart, end: maxEnd } + }) + + const totalMs = createMemo(() => timeSpan().end - timeSpan().start) + + const ticks = createMemo(() => { + const ms = totalMs() + let interval: number + if (ms <= 1000) interval = 200 + else if (ms <= 5000) interval = 1000 + else if (ms <= 20000) interval = 5000 + else interval = 10000 + + const result: Array<{ label: string; pct: number }> = [] + const span = timeSpan() + for (let t = 0; t <= ms; t += interval) { + result.push({ + label: ms <= 2000 ? `${t}ms` : `${(t / 1000).toFixed(1)}s`, + pct: (t / ms) * 100, + }) + } + // Always include the end + if (result.length > 0 && result[result.length - 1]!.pct < 99) { + result.push({ + label: ms <= 2000 ? `${ms.toFixed(0)}ms` : `${(ms / 1000).toFixed(1)}s`, + pct: 100, + }) + } + return result + }) + + // Mini bars: each entry gets a bar positioned by startTimestamp, sized by duration + const bars = createMemo(() => { + const span = timeSpan() + const ms = totalMs() + return props.entries.map((entry, i) => { + const left = + ((entry.startTimestamp - span.start) / ms) * 100 + const width = Math.max( + ((entry.duration ?? 0) / ms) * 100, + 0.5, + ) + const color = TYPE_COLORS[entry.type ?? ''] ?? '#6b7280' + // Stack rows vertically: cycle through 5 rows + const row = i % 5 + return { left, width, color, row } + }) + }) + + // Drag logic + const [dragging, setDragging] = createSignal< + 'left' | 'right' | 'body' | null + >(null) + const [dragStartX, setDragStartX] = createSignal(0) + const [dragStartRange, setDragStartRange] = createSignal<[number, number]>([ + 0, 1, + ]) + + function getTrackX(clientX: number): number { + if (!trackRef) return 0 + const rect = trackRef.getBoundingClientRect() + return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) + } + + function onPointerDown( + handle: 'left' | 'right' | 'body', + e: PointerEvent, + ) { + e.preventDefault() + ;(e.target as HTMLElement).setPointerCapture(e.pointerId) + setDragging(handle) + setDragStartX(e.clientX) + setDragStartRange([...props.range]) + } + + function onPointerMove(e: PointerEvent) { + const d = dragging() + if (!d || !trackRef) return + + const rect = trackRef.getBoundingClientRect() + const deltaNorm = (e.clientX - dragStartX()) / rect.width + const [startL, startR] = dragStartRange() + + if (d === 'left') { + const newLeft = Math.max(0, Math.min(startL + deltaNorm, props.range[1] - 0.02)) + props.onRangeChange([newLeft, props.range[1]]) + } else if (d === 'right') { + const newRight = Math.min(1, Math.max(startR + deltaNorm, props.range[0] + 0.02)) + props.onRangeChange([props.range[0], newRight]) + } else if (d === 'body') { + const rangeWidth = startR - startL + let newLeft = startL + deltaNorm + let newRight = startR + deltaNorm + if (newLeft < 0) { + newLeft = 0 + newRight = rangeWidth + } + if (newRight > 1) { + newRight = 1 + newLeft = 1 - rangeWidth + } + props.onRangeChange([newLeft, newRight]) + } + } + + function onPointerUp() { + setDragging(null) + } + + return ( +
+ {/* Time ruler */} +
+ + {(tick) => ( + + {tick.label} + + )} + +
+ + {/* Track with mini bars and range selector */} +
+ {/* Mini bars */} + + {(bar) => ( +
+ )} + + + {/* Dimmed areas outside range */} +
+
+ + {/* Range selector body (draggable) */} +
onPointerDown('body', e)} + > + {/* Left handle */} +
{ + e.stopPropagation() + onPointerDown('left', e) + }} + /> + {/* Right handle */} +
{ + e.stopPropagation() + onPointerDown('right', e) + }} + /> +
+
+
+ ) +} diff --git a/packages/start-devtools/src/components/WaterfallBar.tsx b/packages/start-devtools/src/components/WaterfallBar.tsx new file mode 100644 index 0000000000..4f7b0d6074 --- /dev/null +++ b/packages/start-devtools/src/components/WaterfallBar.tsx @@ -0,0 +1,113 @@ +/** @jsxImportSource solid-js */ +import { For, Show } from 'solid-js' + +const PHASE_COLORS: Record = { + 'request-middleware': '#3b82f6', + 'route-middleware': '#60a5fa', + 'server-fn-middleware': '#93c5fd', + 'server-fn': '#f97316', + ssr: '#a855f7', + routing: '#22c55e', + error: '#ef4444', +} + +interface Phase { + name: string + startTime: number + endTime: number | null + duration: number | null + children?: Array<{ + name: string + startTime: number + endTime: number + exclusiveDuration: number + }> +} + +interface WaterfallBarProps { + phases: Array + totalDuration: number | null + maxTime: number + onPhaseClick?: (phaseName: string) => void +} + +export default function WaterfallBar(props: WaterfallBarProps) { + const scale = () => (props.maxTime > 0 ? 100 / props.maxTime : 1) + + return ( +
+ + {(phase) => { + const left = () => phase.startTime * scale() + const width = () => + phase.duration !== null + ? phase.duration * scale() + : (props.maxTime - phase.startTime) * scale() + const color = () => PHASE_COLORS[phase.name] || '#6b7280' + + return ( +
{ + e.stopPropagation() + props.onPhaseClick?.(phase.name) + }} + > + 0}> + + {(child) => { + const childLeft = () => + ((child.startTime - phase.startTime) / + (phase.duration || 1)) * + 100 + const childWidth = () => + (child.exclusiveDuration / (phase.duration || 1)) * 100 + + return ( +
+ ) + }} + + +
+ ) + }} +
+
+ ) +} diff --git a/packages/start-devtools/src/core.tsx b/packages/start-devtools/src/core.tsx new file mode 100644 index 0000000000..0516c4086b --- /dev/null +++ b/packages/start-devtools/src/core.tsx @@ -0,0 +1,11 @@ + +import { constructCoreClass } from '@tanstack/devtools-utils/solid' +import { lazy } from 'solid-js' + +const Component = lazy(() => import('./StartDevtools')) + +export interface StartDevtoolsInit { } + +const [StartDevtoolsCore, StartDevtoolsCoreNoOp] = constructCoreClass(Component) + +export { StartDevtoolsCore, StartDevtoolsCoreNoOp } \ No newline at end of file diff --git a/packages/start-devtools/src/index.ts b/packages/start-devtools/src/index.ts new file mode 100644 index 0000000000..d6a9993189 --- /dev/null +++ b/packages/start-devtools/src/index.ts @@ -0,0 +1,10 @@ +'use client' + +import * as Devtools from './core' + +export const StartDevtoolsCore = + process.env.NODE_ENV !== 'development' + ? Devtools.StartDevtoolsCoreNoOp + : Devtools.StartDevtoolsCore + +export type { StartDevtoolsInit } from './core' diff --git a/packages/start-devtools/src/production.ts b/packages/start-devtools/src/production.ts new file mode 100644 index 0000000000..1ec1a938a0 --- /dev/null +++ b/packages/start-devtools/src/production.ts @@ -0,0 +1,5 @@ +'use client' + +export { StartDevtoolsCore } from './core' + +export type { StartDevtoolsInit } from './core' diff --git a/packages/start-devtools/src/store.ts b/packages/start-devtools/src/store.ts new file mode 100644 index 0000000000..b12022d618 --- /dev/null +++ b/packages/start-devtools/src/store.ts @@ -0,0 +1,324 @@ +import { createSignal } from 'solid-js' +import { startEventClient } from '@tanstack/start-server-core/event-client' + +export interface RequestEntry { + requestId: string + url: string + method: string + type: 'server-fn' | 'ssr' | 'server-route' | null + headers: Record + status: number | null + startTimestamp: number + duration: number | null + + phases: Array<{ + name: string + startTime: number + endTime: number | null + duration: number | null + children?: Array<{ + name: string + startTime: number + endTime: number + exclusiveDuration: number + }> + }> + + routeMatch?: { + routes: Array<{ id: string; path: string }> + params: Record + } + serverFn?: { + id: string + name: string + filename: string + inputType: string + resultType?: string + } + serialization?: { + format: string + contentType: string + hasRawStreams: boolean + } + streamChunks: Array<{ index: number; timestamp: number }> + redirect?: { from: string; to: string; status: number } + errors: Array<{ phase: string; message: string; stack?: string }> + responseHeaders: Record | null +} + +function createEmptyEntry( + requestId: string, + url: string, + method: string, + headers: Record, + timestamp: number, +): RequestEntry { + return { + requestId, + url, + method, + type: null, + headers, + status: null, + startTimestamp: timestamp, + duration: null, + phases: [], + streamChunks: [], + errors: [], + responseHeaders: null, + } +} + +function computeExclusiveDuration( + chain: Array<{ name: string; startTime: number; endTime: number }>, +): Array<{ + name: string + startTime: number + endTime: number + exclusiveDuration: number +}> { + return chain.map((mw) => { + const nested = chain.filter( + (other) => + other !== mw && + other.startTime >= mw.startTime && + other.endTime <= mw.endTime, + ) + const nestedTime = nested.reduce( + (sum, n) => sum + (n.endTime - n.startTime), + 0, + ) + return { + ...mw, + exclusiveDuration: mw.endTime - mw.startTime - nestedTime, + } + }) +} + +export function processEvent( + entries: Map, + event: { type: string; pluginId: string; payload: any }, +): void { + const suffix = event.type.replace(`${event.pluginId}:`, '') + const payload = event.payload + + switch (suffix) { + case 'request-start': { + if (!entries.has(payload.requestId)) { + entries.set( + payload.requestId, + createEmptyEntry( + payload.requestId, + payload.url, + payload.method, + payload.headers, + payload.timestamp, + ), + ) + } + break + } + + case 'request-end': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.type = payload.type + entry.status = payload.status + entry.duration = payload.duration + entry.responseHeaders = payload.responseHeaders + if (payload.error) { + entry.errors.push({ + phase: 'request', + message: payload.error.message, + stack: payload.error.stack, + }) + } + break + } + + case 'middleware-executed': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.phases.push({ + name: `${payload.scope}-middleware`, + startTime: payload.startTime, + endTime: payload.startTime + payload.totalDuration, + duration: payload.totalDuration, + children: computeExclusiveDuration(payload.chain), + }) + break + } + + case 'server-fn-start': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.serverFn = { + id: payload.serverFnId, + name: payload.serverFnName, + filename: payload.filename, + inputType: payload.inputPayloadType, + } + entry.phases.push({ + name: 'server-fn', + startTime: payload.startTime, + endTime: null, + duration: null, + }) + break + } + + case 'server-fn-end': { + const entry = entries.get(payload.requestId) + if (!entry) return + if (entry.serverFn) { + entry.serverFn.resultType = payload.resultType + } + const phase = entry.phases.find( + (p) => p.name === 'server-fn' && p.endTime === null, + ) + if (phase) { + phase.endTime = phase.startTime + payload.duration + phase.duration = payload.duration + } + if (payload.error) { + entry.errors.push({ + phase: 'server-fn', + message: payload.error.message, + stack: payload.error.stack, + }) + } + break + } + + case 'ssr-start': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.phases.push({ + name: 'ssr', + startTime: payload.startTime, + endTime: null, + duration: null, + }) + break + } + + case 'ssr-end': { + const entry = entries.get(payload.requestId) + if (!entry) return + const phase = entry.phases.find( + (p) => p.name === 'ssr' && p.endTime === null, + ) + if (phase) { + phase.endTime = phase.startTime + payload.duration + phase.duration = payload.duration + phase.children = [ + { + name: 'router.load()', + startTime: 0, + endTime: payload.routerLoadDuration, + exclusiveDuration: payload.routerLoadDuration, + }, + { + name: 'dehydrate()', + startTime: payload.routerLoadDuration, + endTime: payload.routerLoadDuration + payload.dehydrationDuration, + exclusiveDuration: payload.dehydrationDuration, + }, + { + name: 'render', + startTime: payload.routerLoadDuration + payload.dehydrationDuration, + endTime: + payload.routerLoadDuration + + payload.dehydrationDuration + + payload.renderDuration, + exclusiveDuration: payload.renderDuration, + }, + ] + } + break + } + + case 'route-matched': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.routeMatch = { + routes: payload.matchedRoutes, + params: payload.params, + } + break + } + + case 'serialization-result': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.serialization = { + format: payload.format, + contentType: payload.contentType, + hasRawStreams: payload.hasRawStreams, + } + break + } + + case 'stream-chunk': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.streamChunks.push({ + index: payload.chunkIndex, + timestamp: payload.timestamp, + }) + break + } + + case 'redirect': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.redirect = { + from: payload.from, + to: payload.to, + status: payload.status, + } + break + } + + case 'error': { + const entry = entries.get(payload.requestId) + if (!entry) return + entry.errors.push({ + phase: payload.phase, + message: payload.message, + stack: payload.stack, + }) + break + } + } +} + +const MAX_ENTRIES = 500 + +export function createRequestStore() { + const [entries, setEntries] = createSignal>( + new Map(), + ) + + const cleanup = startEventClient.onAllPluginEvents((event: any) => { + setEntries((prev) => { + const next = new Map(prev) + processEvent(next, event) + if (next.size > MAX_ENTRIES) { + const keysToRemove = [...next.keys()].slice(0, next.size - MAX_ENTRIES) + for (const key of keysToRemove) next.delete(key) + } + return next + }) + }) + + return { + get entries() { + return entries() + }, + clear() { + setEntries(new Map()) + }, + cleanup, + } +} diff --git a/packages/start-devtools/src/styles/tokens.ts b/packages/start-devtools/src/styles/tokens.ts new file mode 100644 index 0000000000..9d247cf184 --- /dev/null +++ b/packages/start-devtools/src/styles/tokens.ts @@ -0,0 +1,305 @@ +export const tokens = { + colors: { + inherit: 'inherit', + current: 'currentColor', + transparent: 'transparent', + black: '#000000', + white: '#ffffff', + neutral: { + 50: '#f9fafb', + 100: '#f2f4f7', + 200: '#eaecf0', + 300: '#d0d5dd', + 400: '#98a2b3', + 500: '#667085', + 600: '#475467', + 700: '#344054', + 800: '#1d2939', + 900: '#101828', + }, + darkGray: { + 50: '#525c7a', + 100: '#49536e', + 200: '#414962', + 300: '#394056', + 400: '#313749', + 500: '#292e3d', + 600: '#212530', + 700: '#191c24', + 800: '#111318', + 900: '#0b0d10', + }, + gray: { + 50: '#f9fafb', + 100: '#f2f4f7', + 200: '#eaecf0', + 300: '#d0d5dd', + 400: '#98a2b3', + 500: '#667085', + 600: '#475467', + 700: '#344054', + 800: '#1d2939', + 900: '#101828', + }, + blue: { + 25: '#F5FAFF', + 50: '#EFF8FF', + 100: '#D1E9FF', + 200: '#B2DDFF', + 300: '#84CAFF', + 400: '#53B1FD', + 500: '#2E90FA', + 600: '#1570EF', + 700: '#175CD3', + 800: '#1849A9', + 900: '#194185', + }, + green: { + 25: '#F6FEF9', + 50: '#ECFDF3', + 100: '#D1FADF', + 200: '#A6F4C5', + 300: '#6CE9A6', + 400: '#32D583', + 500: '#12B76A', + 600: '#039855', + 700: '#027A48', + 800: '#05603A', + 900: '#054F31', + }, + red: { + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + 950: '#450a0a', + }, + yellow: { + 25: '#FFFCF5', + 50: '#FFFAEB', + 100: '#FEF0C7', + 200: '#FEDF89', + 300: '#FEC84B', + 400: '#FDB022', + 500: '#F79009', + 600: '#DC6803', + 700: '#B54708', + 800: '#93370D', + 900: '#7A2E0E', + }, + purple: { + 25: '#FAFAFF', + 50: '#F4F3FF', + 100: '#EBE9FE', + 200: '#D9D6FE', + 300: '#BDB4FE', + 400: '#9B8AFB', + 500: '#7A5AF8', + 600: '#6938EF', + 700: '#5925DC', + 800: '#4A1FB8', + 900: '#3E1C96', + }, + teal: { + 25: '#F6FEFC', + 50: '#F0FDF9', + 100: '#CCFBEF', + 200: '#99F6E0', + 300: '#5FE9D0', + 400: '#2ED3B7', + 500: '#15B79E', + 600: '#0E9384', + 700: '#107569', + 800: '#125D56', + 900: '#134E48', + }, + pink: { + 25: '#fdf2f8', + 50: '#fce7f3', + 100: '#fbcfe8', + 200: '#f9a8d4', + 300: '#f472b6', + 400: '#ec4899', + 500: '#db2777', + 600: '#be185d', + 700: '#9d174d', + 800: '#831843', + 900: '#500724', + }, + cyan: { + 25: '#ecfeff', + 50: '#cffafe', + 100: '#a5f3fc', + 200: '#67e8f9', + 300: '#22d3ee', + 400: '#06b6d4', + 500: '#0891b2', + 600: '#0e7490', + 700: '#155e75', + 800: '#164e63', + 900: '#083344', + }, + }, + alpha: { + 100: 'ff', + 90: 'e5', + 80: 'cc', + 70: 'b3', + 60: '99', + 50: '80', + 40: '66', + 30: '4d', + 20: '33', + 10: '1a', + 0: '00', + }, + font: { + size: { + '2xs': 'calc(var(--tsrd-font-size) * 0.625)', + xs: 'calc(var(--tsrd-font-size) * 0.75)', + sm: 'calc(var(--tsrd-font-size) * 0.875)', + md: 'var(--tsrd-font-size)', + lg: 'calc(var(--tsrd-font-size) * 1.125)', + xl: 'calc(var(--tsrd-font-size) * 1.25)', + '2xl': 'calc(var(--tsrd-font-size) * 1.5)', + '3xl': 'calc(var(--tsrd-font-size) * 1.875)', + '4xl': 'calc(var(--tsrd-font-size) * 2.25)', + '5xl': 'calc(var(--tsrd-font-size) * 3)', + '6xl': 'calc(var(--tsrd-font-size) * 3.75)', + '7xl': 'calc(var(--tsrd-font-size) * 4.5)', + '8xl': 'calc(var(--tsrd-font-size) * 6)', + '9xl': 'calc(var(--tsrd-font-size) * 8)', + }, + lineHeight: { + '3xs': 'calc(var(--tsrd-font-size) * 0.75)', + '2xs': 'calc(var(--tsrd-font-size) * 0.875)', + xs: 'calc(var(--tsrd-font-size) * 1)', + sm: 'calc(var(--tsrd-font-size) * 1.25)', + md: 'calc(var(--tsrd-font-size) * 1.5)', + lg: 'calc(var(--tsrd-font-size) * 1.75)', + xl: 'calc(var(--tsrd-font-size) * 2)', + '2xl': 'calc(var(--tsrd-font-size) * 2.25)', + '3xl': 'calc(var(--tsrd-font-size) * 2.5)', + '4xl': 'calc(var(--tsrd-font-size) * 2.75)', + '5xl': 'calc(var(--tsrd-font-size) * 3)', + '6xl': 'calc(var(--tsrd-font-size) * 3.25)', + '7xl': 'calc(var(--tsrd-font-size) * 3.5)', + '8xl': 'calc(var(--tsrd-font-size) * 3.75)', + '9xl': 'calc(var(--tsrd-font-size) * 4)', + }, + weight: { + thin: '100', + extralight: '200', + light: '300', + normal: '400', + medium: '500', + semibold: '600', + bold: '700', + extrabold: '800', + black: '900', + }, + fontFamily: { + sans: 'ui-sans-serif, Inter, system-ui, sans-serif, sans-serif', + mono: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace`, + }, + }, + breakpoints: { + xs: '320px', + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + border: { + radius: { + none: '0px', + xs: 'calc(var(--tsrd-font-size) * 0.125)', + sm: 'calc(var(--tsrd-font-size) * 0.25)', + md: 'calc(var(--tsrd-font-size) * 0.375)', + lg: 'calc(var(--tsrd-font-size) * 0.5)', + xl: 'calc(var(--tsrd-font-size) * 0.75)', + '2xl': 'calc(var(--tsrd-font-size) * 1)', + '3xl': 'calc(var(--tsrd-font-size) * 1.5)', + full: '9999px', + }, + }, + size: { + 0: '0px', + 0.25: 'calc(var(--tsrd-font-size) * 0.0625)', + 0.5: 'calc(var(--tsrd-font-size) * 0.125)', + 1: 'calc(var(--tsrd-font-size) * 0.25)', + 1.5: 'calc(var(--tsrd-font-size) * 0.375)', + 2: 'calc(var(--tsrd-font-size) * 0.5)', + 2.5: 'calc(var(--tsrd-font-size) * 0.625)', + 3: 'calc(var(--tsrd-font-size) * 0.75)', + 3.5: 'calc(var(--tsrd-font-size) * 0.875)', + 4: 'calc(var(--tsrd-font-size) * 1)', + 4.5: 'calc(var(--tsrd-font-size) * 1.125)', + 5: 'calc(var(--tsrd-font-size) * 1.25)', + 5.5: 'calc(var(--tsrd-font-size) * 1.375)', + 6: 'calc(var(--tsrd-font-size) * 1.5)', + 6.5: 'calc(var(--tsrd-font-size) * 1.625)', + 7: 'calc(var(--tsrd-font-size) * 1.75)', + 8: 'calc(var(--tsrd-font-size) * 2)', + 9: 'calc(var(--tsrd-font-size) * 2.25)', + 10: 'calc(var(--tsrd-font-size) * 2.5)', + 11: 'calc(var(--tsrd-font-size) * 2.75)', + 12: 'calc(var(--tsrd-font-size) * 3)', + 14: 'calc(var(--tsrd-font-size) * 3.5)', + 16: 'calc(var(--tsrd-font-size) * 4)', + 20: 'calc(var(--tsrd-font-size) * 5)', + 24: 'calc(var(--tsrd-font-size) * 6)', + 28: 'calc(var(--tsrd-font-size) * 7)', + 32: 'calc(var(--tsrd-font-size) * 8)', + 36: 'calc(var(--tsrd-font-size) * 9)', + 40: 'calc(var(--tsrd-font-size) * 10)', + 44: 'calc(var(--tsrd-font-size) * 11)', + 48: 'calc(var(--tsrd-font-size) * 12)', + 52: 'calc(var(--tsrd-font-size) * 13)', + 56: 'calc(var(--tsrd-font-size) * 14)', + 60: 'calc(var(--tsrd-font-size) * 15)', + 64: 'calc(var(--tsrd-font-size) * 16)', + 72: 'calc(var(--tsrd-font-size) * 18)', + 80: 'calc(var(--tsrd-font-size) * 20)', + 96: 'calc(var(--tsrd-font-size) * 24)', + }, + shadow: { + xs: (_: string = 'rgb(0 0 0 / 0.1)') => + `0 1px 2px 0 rgb(0 0 0 / 0.05)` as const, + sm: (color: string = 'rgb(0 0 0 / 0.1)') => + `0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}` as const, + md: (color: string = 'rgb(0 0 0 / 0.1)') => + `0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}` as const, + lg: (color: string = 'rgb(0 0 0 / 0.1)') => + `0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}` as const, + xl: (color: string = 'rgb(0 0 0 / 0.1)') => + `0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}` as const, + '2xl': (color: string = 'rgb(0 0 0 / 0.25)') => + `0 25px 50px -12px ${color}` as const, + inner: (color: string = 'rgb(0 0 0 / 0.05)') => + `inset 0 2px 4px 0 ${color}` as const, + none: () => `none` as const, + }, + zIndices: { + hide: -1, + auto: 'auto', + base: 0, + docked: 10, + dropdown: 1000, + sticky: 1100, + banner: 1200, + overlay: 1300, + modal: 1400, + popover: 1500, + skipLink: 1600, + toast: 1700, + tooltip: 1800, + }, +} as const diff --git a/packages/start-devtools/src/styles/use-styles.ts b/packages/start-devtools/src/styles/use-styles.ts new file mode 100644 index 0000000000..3c472600d6 --- /dev/null +++ b/packages/start-devtools/src/styles/use-styles.ts @@ -0,0 +1,364 @@ +import * as goober from 'goober' +import { createEffect, createSignal } from 'solid-js' +import { useTheme } from '@tanstack/devtools-ui' +import { tokens } from './tokens' + +const stylesFactory = (theme: 'light' | 'dark') => { + const { colors, font, size, alpha, border } = tokens + const { fontFamily, size: fontSize } = font + const css = goober.css + const t = (light: string, dark: string) => (theme === 'light' ? light : dark) + + return { + mainContainer: css` + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; + padding: ${size[2]}; + padding-top: 0; + margin-top: ${size[2]}; + `, + dragHandle: css` + width: 8px; + background: ${t(colors.gray[300], colors.darkGray[600])}; + cursor: col-resize; + position: relative; + transition: all 0.2s ease; + user-select: none; + pointer-events: all; + margin: 0 ${size[1]}; + border-radius: 2px; + + &:hover { + background: ${t(colors.blue[600], colors.blue[500])}; + margin: 0 ${size[1]}; + } + + &.dragging { + background: ${t(colors.blue[700], colors.blue[600])}; + margin: 0 ${size[1]}; + } + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 20px; + background: ${t(colors.gray[400], colors.darkGray[400])}; + border-radius: 1px; + pointer-events: none; + } + + &:hover::after, + &.dragging::after { + background: ${t(colors.blue[500], colors.blue[300])}; + } + `, + leftPanel: css` + background: ${t(colors.gray[100], colors.darkGray[800])}; + border-radius: ${border.radius.lg}; + border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; + flex-shrink: 0; + `, + rightPanel: css` + background: ${t(colors.gray[100], colors.darkGray[800])}; + border-radius: ${border.radius.lg}; + border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; + flex: 1; + `, + panelHeader: css` + font-size: ${fontSize.md}; + font-weight: ${font.weight.bold}; + color: ${t(colors.blue[700], colors.blue[400])}; + padding: ${size[2]}; + border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; + background: ${t(colors.gray[100], colors.darkGray[800])}; + flex-shrink: 0; + `, + utilList: css` + flex: 1; + overflow-y: auto; + padding: ${size[1]}; + min-height: 0; + `, + utilGroup: css` + margin-bottom: ${size[2]}; + `, + utilGroupHeader: css` + font-size: ${fontSize.xs}; + font-weight: ${font.weight.semibold}; + color: ${t(colors.gray[600], colors.gray[400])}; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: ${size[1]}; + padding: ${size[1]} ${size[2]}; + background: ${t(colors.gray[200], colors.darkGray[700])}; + border-radius: ${border.radius.md}; + `, + utilRow: css` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${size[2]}; + margin-bottom: ${size[1]}; + background: ${t(colors.gray[200], colors.darkGray[700])}; + border-radius: ${border.radius.md}; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; + + &:hover { + background: ${t(colors.gray[300], colors.darkGray[600])}; + border-color: ${t(colors.gray[400], colors.darkGray[500])}; + } + `, + utilRowSelected: css` + background: ${t(colors.blue[100], colors.blue[900] + alpha[20])}; + border-color: ${t(colors.blue[600], colors.blue[500])}; + box-shadow: 0 0 0 1px + ${t(colors.blue[600] + alpha[30], colors.blue[500] + alpha[30])}; + `, + utilKey: css` + font-family: ${fontFamily.mono}; + font-size: ${fontSize.xs}; + color: ${t(colors.gray[900], colors.gray[100])}; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + `, + utilStatus: css` + font-size: ${fontSize.xs}; + color: ${t(colors.gray[600], colors.gray[400])}; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: ${size[1]} ${size[1]}; + background: ${t(colors.gray[300], colors.darkGray[600])}; + border-radius: ${border.radius.sm}; + margin-left: ${size[1]}; + `, + stateDetails: css` + flex: 1; + overflow-y: auto; + padding: ${size[2]}; + min-height: 0; + `, + stateHeader: css` + margin-bottom: ${size[2]}; + padding-bottom: ${size[2]}; + border-bottom: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; + `, + stateTitle: css` + font-size: ${fontSize.md}; + font-weight: ${font.weight.bold}; + color: ${t(colors.blue[700], colors.blue[400])}; + margin-bottom: ${size[1]}; + `, + stateKey: css` + font-family: ${fontFamily.mono}; + font-size: ${fontSize.xs}; + color: ${t(colors.gray[600], colors.gray[400])}; + word-break: break-all; + `, + stateContent: css` + background: ${t(colors.gray[100], colors.darkGray[700])}; + border-radius: ${border.radius.md}; + padding: ${size[2]}; + border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; + `, + detailsGrid: css` + display: grid; + grid-template-columns: 1fr; + gap: ${size[2]}; + align-items: start; + `, + detailSection: css` + background: ${t(colors.white, colors.darkGray[700])}; + border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; + border-radius: ${border.radius.md}; + padding: ${size[2]}; + `, + detailSectionHeader: css` + font-size: ${fontSize.sm}; + font-weight: ${font.weight.bold}; + color: ${t(colors.gray[800], colors.gray[200])}; + margin-bottom: ${size[1]}; + text-transform: uppercase; + letter-spacing: 0.04em; + `, + actionsRow: css` + display: flex; + flex-wrap: wrap; + gap: ${size[2]}; + `, + actionButton: css` + display: inline-flex; + align-items: center; + gap: ${size[1]}; + padding: ${size[1]} ${size[2]}; + border-radius: ${border.radius.md}; + border: 1px solid ${t(colors.gray[300], colors.darkGray[500])}; + background: ${t(colors.gray[200], colors.darkGray[600])}; + color: ${t(colors.gray[900], colors.gray[100])}; + font-size: ${fontSize.xs}; + cursor: pointer; + user-select: none; + transition: + background 0.15s, + border-color 0.15s; + &:hover { + background: ${t(colors.gray[300], colors.darkGray[500])}; + border-color: ${t(colors.gray[400], colors.darkGray[400])}; + } + &:disabled { + opacity: 0.5; + cursor: not-allowed; + &:hover { + background: ${t(colors.gray[200], colors.darkGray[600])}; + border-color: ${t(colors.gray[300], colors.darkGray[500])}; + } + } + `, + actionDotBlue: css` + width: 6px; + height: 6px; + border-radius: 9999px; + background: ${colors.blue[400]}; + `, + actionDotGreen: css` + width: 6px; + height: 6px; + border-radius: 9999px; + background: ${colors.green[400]}; + `, + actionDotRed: css` + width: 6px; + height: 6px; + border-radius: 9999px; + background: ${colors.red[400]}; + `, + actionDotYellow: css` + width: 6px; + height: 6px; + border-radius: 9999px; + background: ${colors.yellow[400]}; + `, + actionDotOrange: css` + width: 6px; + height: 6px; + border-radius: 9999px; + background: ${colors.pink[400]}; + `, + actionDotPurple: css` + width: 6px; + height: 6px; + border-radius: 9999px; + background: ${colors.purple[400]}; + `, + infoGrid: css` + display: grid; + grid-template-columns: auto 1fr; + gap: ${size[1]}; + row-gap: ${size[1]}; + align-items: center; + `, + infoLabel: css` + color: ${t(colors.gray[600], colors.gray[400])}; + font-size: ${fontSize.xs}; + text-transform: uppercase; + letter-spacing: 0.05em; + `, + infoValueMono: css` + font-family: ${fontFamily.mono}; + font-size: ${fontSize.xs}; + color: ${t(colors.gray[900], colors.gray[100])}; + word-break: break-all; + `, + noSelection: css` + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: ${t(colors.gray[500], colors.gray[500])}; + font-style: italic; + text-align: center; + padding: ${size[4]}; + `, + // Keep existing styles for backward compatibility + sectionContainer: css` + display: flex; + flex-wrap: wrap; + gap: ${size[4]}; + `, + section: css` + background: ${t(colors.gray[100], colors.darkGray[800])}; + border-radius: ${border.radius.lg}; + box-shadow: ${tokens.shadow.md( + t(colors.gray[400] + alpha[80], colors.black + alpha[80]), + )}; + padding: ${size[4]}; + margin-bottom: ${size[4]}; + border: 1px solid ${t(colors.gray[200], colors.darkGray[700])}; + min-width: 0; + max-width: 33%; + max-height: fit-content; + `, + sectionHeader: css` + font-size: ${fontSize.lg}; + font-weight: ${font.weight.bold}; + margin-bottom: ${size[2]}; + color: ${t(colors.blue[600], colors.blue[400])}; + letter-spacing: 0.01em; + display: flex; + align-items: center; + gap: ${size[2]}; + `, + sectionEmpty: css` + color: ${t(colors.gray[500], colors.gray[500])}; + font-size: ${fontSize.sm}; + font-style: italic; + margin: ${size[2]} 0; + `, + instanceList: css` + display: flex; + flex-direction: column; + gap: ${size[2]}; + background: ${t(colors.gray[200], colors.darkGray[700])}; + border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; + `, + instanceCard: css` + background: ${t(colors.gray[200], colors.darkGray[700])}; + border-radius: ${border.radius.md}; + padding: ${size[3]}; + border: 1px solid ${t(colors.gray[300], colors.darkGray[600])}; + font-size: ${fontSize.sm}; + color: ${t(colors.gray[900], colors.gray[100])}; + font-family: ${fontFamily.mono}; + overflow-x: auto; + transition: + box-shadow 0.3s, + background 0.3s; + `, + } +} + +export function useStyles() { + const { theme } = useTheme() + const [styles, setStyles] = createSignal(stylesFactory(theme())) + createEffect(() => { + setStyles(stylesFactory(theme())) + }) + return styles +} diff --git a/packages/start-devtools/tests/index.test.ts b/packages/start-devtools/tests/index.test.ts new file mode 100644 index 0000000000..350ef3adeb --- /dev/null +++ b/packages/start-devtools/tests/index.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from 'vitest' + +describe('test suite', () => { + it('should work', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/start-devtools/tests/jest-dom-setup.ts b/packages/start-devtools/tests/jest-dom-setup.ts new file mode 100644 index 0000000000..2237c00c24 --- /dev/null +++ b/packages/start-devtools/tests/jest-dom-setup.ts @@ -0,0 +1,2 @@ +// Test setup - no external dependencies needed for pure function tests +export {} diff --git a/packages/start-devtools/tests/store.test.ts b/packages/start-devtools/tests/store.test.ts new file mode 100644 index 0000000000..c9fe1a2131 --- /dev/null +++ b/packages/start-devtools/tests/store.test.ts @@ -0,0 +1,400 @@ +import { describe, expect, it } from 'vitest' +import { processEvent, type RequestEntry } from '../src/store' + +describe('processEvent', () => { + function makeEntries(): Map { + return new Map() + } + + it('creates a new entry on request-start', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/api/test', + method: 'GET', + headers: { 'content-type': 'application/json' }, + timestamp: 1000, + }, + }) + + expect(entries.has('r1')).toBe(true) + const entry = entries.get('r1')! + expect(entry.url).toBe('/api/test') + expect(entry.method).toBe('GET') + expect(entry.status).toBeNull() + expect(entry.duration).toBeNull() + }) + + it('updates entry on request-end', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/api/test', + method: 'GET', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:request-end', + pluginId: 'start', + payload: { + requestId: 'r1', + type: 'server-fn', + status: 200, + duration: 50, + responseHeaders: { 'x-custom': 'value' }, + }, + }) + + const entry = entries.get('r1')! + expect(entry.type).toBe('server-fn') + expect(entry.status).toBe(200) + expect(entry.duration).toBe(50) + expect(entry.responseHeaders).toEqual({ 'x-custom': 'value' }) + }) + + it('adds middleware phase on middleware-executed', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'GET', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:middleware-executed', + pluginId: 'start', + payload: { + requestId: 'r1', + scope: 'request', + chain: [{ name: 'auth', startTime: 100, endTime: 110 }], + totalDuration: 10, + startTime: 5, + }, + }) + + const entry = entries.get('r1')! + expect(entry.phases).toHaveLength(1) + expect(entry.phases[0]!.name).toBe('request-middleware') + expect(entry.phases[0]!.children).toHaveLength(1) + }) + + it('populates serverFn fields on server-fn-start', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'POST', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:server-fn-start', + pluginId: 'start', + payload: { + requestId: 'r1', + serverFnId: 'fn1', + serverFnName: 'getUser', + filename: 'src/api.ts', + httpMethod: 'POST', + inputPayloadType: 'json', + startTime: 10, + }, + }) + + const entry = entries.get('r1')! + expect(entry.serverFn).toBeDefined() + expect(entry.serverFn!.name).toBe('getUser') + }) + + it('records errors on error event', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'GET', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:error', + pluginId: 'start', + payload: { + requestId: 'r1', + phase: 'middleware', + message: 'Auth failed', + stack: 'Error: Auth failed\n at ...', + timestamp: 1001, + }, + }) + + const entry = entries.get('r1')! + expect(entry.errors).toHaveLength(1) + expect(entry.errors[0]!.message).toBe('Auth failed') + }) + + it('ignores events for unknown requestIds', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-end', + pluginId: 'start', + payload: { + requestId: 'unknown', + type: 'ssr', + status: 200, + duration: 50, + responseHeaders: {}, + }, + }) + + expect(entries.size).toBe(0) + }) + + it('closes open server-fn phase on server-fn-end', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'POST', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:server-fn-start', + pluginId: 'start', + payload: { + requestId: 'r1', + serverFnId: 'fn1', + serverFnName: 'getUser', + filename: 'api.ts', + httpMethod: 'POST', + inputPayloadType: 'json', + startTime: 10, + }, + }) + processEvent(entries, { + type: 'start:server-fn-end', + pluginId: 'start', + payload: { + requestId: 'r1', + serverFnId: 'fn1', + duration: 25, + resultType: 'json', + status: 200, + }, + }) + + const entry = entries.get('r1')! + const phase = entry.phases.find((p) => p.name === 'server-fn')! + expect(phase.duration).toBe(25) + expect(phase.endTime).toBe(35) + expect(entry.serverFn!.resultType).toBe('json') + }) + + it('populates ssr-end with sub-phase children', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'GET', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:ssr-start', + pluginId: 'start', + payload: { + requestId: 'r1', + matchedRoute: '/', + params: {}, + startTime: 5, + }, + }) + processEvent(entries, { + type: 'start:ssr-end', + pluginId: 'start', + payload: { + requestId: 'r1', + duration: 100, + routerLoadDuration: 40, + dehydrationDuration: 30, + renderDuration: 30, + hadRedirect: false, + }, + }) + + const entry = entries.get('r1')! + const phase = entry.phases.find((p) => p.name === 'ssr')! + expect(phase.duration).toBe(100) + expect(phase.children).toHaveLength(3) + expect(phase.children![0]!.name).toBe('router.load()') + expect(phase.children![1]!.name).toBe('dehydrate()') + expect(phase.children![2]!.name).toBe('render') + }) + + it('populates route match info', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'GET', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:route-matched', + pluginId: 'start', + payload: { + requestId: 'r1', + matchedRoutes: [ + { id: '__root__', path: '/' }, + { id: '/posts', path: '/posts' }, + ], + foundRoute: { id: '/posts', path: '/posts' }, + isExactMatch: true, + params: { id: '1' }, + hasServerHandlers: false, + timestamp: 1001, + }, + }) + + const entry = entries.get('r1')! + expect(entry.routeMatch).toBeDefined() + expect(entry.routeMatch!.routes).toHaveLength(2) + expect(entry.routeMatch!.params).toEqual({ id: '1' }) + }) + + it('tracks stream chunks', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'POST', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:stream-chunk', + pluginId: 'start', + payload: { + requestId: 'r1', + serverFnId: 'fn1', + chunkIndex: 1, + timestamp: 1001, + }, + }) + processEvent(entries, { + type: 'start:stream-chunk', + pluginId: 'start', + payload: { + requestId: 'r1', + serverFnId: 'fn1', + chunkIndex: 5, + totalChunks: 5, + timestamp: 1002, + }, + }) + + const entry = entries.get('r1')! + expect(entry.streamChunks).toHaveLength(2) + expect(entry.streamChunks[1]!.index).toBe(5) + }) + + it('populates serialization info', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/', + method: 'POST', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:serialization-result', + pluginId: 'start', + payload: { + requestId: 'r1', + format: 'ndjson', + status: 200, + contentType: 'application/x-ndjson', + hasRawStreams: false, + duration: 5, + }, + }) + + const entry = entries.get('r1')! + expect(entry.serialization).toBeDefined() + expect(entry.serialization!.format).toBe('ndjson') + expect(entry.serialization!.contentType).toBe('application/x-ndjson') + }) + + it('records redirect info', () => { + const entries = makeEntries() + processEvent(entries, { + type: 'start:request-start', + pluginId: 'start', + payload: { + requestId: 'r1', + url: '/old', + method: 'GET', + headers: {}, + timestamp: 1000, + }, + }) + processEvent(entries, { + type: 'start:redirect', + pluginId: 'start', + payload: { + requestId: 'r1', + from: '/old', + to: '/new', + status: 302, + isServerFn: false, + timestamp: 1001, + }, + }) + + const entry = entries.get('r1')! + expect(entry.redirect).toEqual({ from: '/old', to: '/new', status: 302 }) + }) +}) diff --git a/packages/start-devtools/tsconfig.docs.json b/packages/start-devtools/tsconfig.docs.json new file mode 100644 index 0000000000..2880b4dfa6 --- /dev/null +++ b/packages/start-devtools/tsconfig.docs.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["tests", "src"] +} diff --git a/packages/start-devtools/tsconfig.json b/packages/start-devtools/tsconfig.json new file mode 100644 index 0000000000..3ee4c951e3 --- /dev/null +++ b/packages/start-devtools/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "eslint.config.js", "vite.config.ts", "tests"], + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js" + } +} diff --git a/packages/start-devtools/vite.config.ts b/packages/start-devtools/vite.config.ts new file mode 100644 index 0000000000..cfa230b3e0 --- /dev/null +++ b/packages/start-devtools/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/config/vite' +import solid from 'vite-plugin-solid' +import packageJson from './package.json' + +const config = defineConfig({ + plugins: [solid()], + test: { + name: packageJson.name, + dir: './', + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/jest-dom-setup.ts'], + globals: true, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts', './src/production.ts'], + srcDir: './src', + cjs: false, + }), +) diff --git a/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts b/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts index ef53af07e5..eb29289b9e 100644 --- a/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts +++ b/packages/start-plugin-core/src/start-compiler-plugin/plugin.ts @@ -315,7 +315,6 @@ export function startCompilerPlugin( return result }, }, - hotUpdate(ctx) { const compiler = compilers[this.environment.name] diff --git a/packages/start-server-core/package.json b/packages/start-server-core/package.json index 94ab2f4f43..ded506937a 100644 --- a/packages/start-server-core/package.json +++ b/packages/start-server-core/package.json @@ -47,6 +47,12 @@ "default": "./dist/esm/index.js" } }, + "./event-client": { + "import": { + "types": "./dist/esm/event-client.d.ts", + "default": "./dist/esm/event-client.js" + } + }, "./createServerRpc": { "import": { "types": "./dist/esm/createServerRpc.d.ts", @@ -75,6 +81,7 @@ "node": ">=22.12.0" }, "dependencies": { + "@tanstack/devtools-event-client": "^0.3.2", "@tanstack/history": "workspace:*", "@tanstack/router-core": "workspace:*", "@tanstack/start-client-core": "workspace:*", @@ -89,4 +96,4 @@ "fetchdts": "^0.1.6", "vite": "*" } -} +} \ No newline at end of file diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index 625e9cc487..c298b08ed4 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -27,6 +27,8 @@ import { import { HEADERS } from './constants' import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter' +import { startEventClient } from './event-client' +import { instrumentMiddlewareArray } from './devtools-instrumentation' import type { AnyFunctionMiddleware, AnyRequestMiddleware, @@ -425,6 +427,23 @@ export function createStartHandler( request, requestOpts, ) => { + let requestId: string | undefined + let requestStartTime: number | undefined + let response: Response | undefined + let requestType: 'server-fn' | 'ssr' | 'server-route' = 'ssr' + + if (process.env.NODE_ENV !== 'production') { + requestId = crypto.randomUUID() + requestStartTime = performance.now() + startEventClient.emit('request-start', { + requestId, + url: request.url.replace(/https?:\/\/[^/]+/, ''), + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + timestamp: Date.now(), + }) + } + let router: AnyRouter | null = null as AnyRouter | null let cbWillCleanup = false as boolean @@ -500,6 +519,7 @@ export function createStartHandler( // Check for server function requests first (early exit) if (SERVER_FN_BASE && url.pathname.startsWith(SERVER_FN_BASE)) { + requestType = 'server-fn' const serverFnId = url.pathname .slice(SERVER_FN_BASE.length) .split('/')[0] @@ -516,6 +536,12 @@ export function createStartHandler( contextAfterGlobalMiddlewares: context, request, executedRequestMiddlewares, + requestId, + requestStartTime, + eventClient: + process.env.NODE_ENV !== 'production' + ? (startEventClient as any) + : undefined, }, () => handleServerAction({ @@ -529,13 +555,50 @@ export function createStartHandler( const middlewares = flattenedRequestMiddlewares.map( (d) => d.options.server, ) - const ctx = await executeMiddleware([...middlewares, serverFnHandler], { - request, - pathname: url.pathname, - context: createNullProtoObject(requestOpts?.context), - }) + const requestMwChain: Array<{ + name: string + startTime: number + endTime: number + }> = [] + const instrumentedRequestMws = + process.env.NODE_ENV !== 'production' + ? instrumentMiddlewareArray( + middlewares as Array, + requestMwChain, + ) + : middlewares + const ctx = await executeMiddleware( + [...instrumentedRequestMws, serverFnHandler], + { + request, + pathname: url.pathname, + context: createNullProtoObject(requestOpts?.context), + }, + ) - return handleRedirectResponse(ctx.response, request, getRouter) + if ( + process.env.NODE_ENV !== 'production' && + requestId && + requestMwChain.length > 0 + ) { + startEventClient.emit('middleware-executed', { + requestId, + scope: 'request', + chain: requestMwChain, + totalDuration: + Math.max(...requestMwChain.map((m) => m.endTime)) - + Math.min(...requestMwChain.map((m) => m.startTime)), + startTime: requestMwChain[0]!.startTime - requestStartTime!, + }) + } + + response = await handleRedirectResponse( + ctx.response, + request, + getRouter, + { requestId, requestType }, + ) + return response } // Router execution function @@ -571,24 +634,56 @@ export function createStartHandler( }) routerInstance.update({ additionalContext: { serverContext } }) + + const ssrPhaseStart = performance.now() + if (process.env.NODE_ENV !== 'production' && requestId) { + const lastRoute = matchedRoutes?.[matchedRoutes.length - 1] + startEventClient.emit('ssr-start', { + requestId, + matchedRoute: + (lastRoute as any)?.fullPath || (lastRoute as any)?.path || '/', + params: (routerInstance.state.location as any)?.params || {}, + startTime: ssrPhaseStart - requestStartTime!, + }) + } + + const loadStart = performance.now() await routerInstance.load() + const loadEnd = performance.now() if (routerInstance.state.redirect) { return routerInstance.state.redirect } + const dehydrateStart = performance.now() await routerInstance.serverSsr!.dehydrate() + const dehydrateEnd = performance.now() const responseHeaders = getStartResponseHeaders({ router: routerInstance, }) cbWillCleanup = true - return cb({ + const renderStart = performance.now() + const ssrResponse = await cb({ request, router: routerInstance, responseHeaders, }) + const renderEnd = performance.now() + + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('ssr-end', { + requestId, + duration: performance.now() - ssrPhaseStart, + routerLoadDuration: loadEnd - loadStart, + dehydrationDuration: dehydrateEnd - dehydrateStart, + renderDuration: renderEnd - renderStart, + hadRedirect: !!routerInstance.state.redirect, + }) + } + + return ssrResponse } // Main request handler @@ -600,6 +695,12 @@ export function createStartHandler( contextAfterGlobalMiddlewares: context, request, executedRequestMiddlewares, + requestId, + requestStartTime, + eventClient: + process.env.NODE_ENV !== 'production' + ? (startEventClient as any) + : undefined, }, async () => { try { @@ -610,8 +711,19 @@ export function createStartHandler( executeRouter, context, executedRequestMiddlewares, + requestId, + requestStartTime, }) } catch (err) { + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('error', { + requestId, + phase: 'routing', + message: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined, + timestamp: Date.now(), + }) + } if (err instanceof Response) { return err } @@ -624,8 +736,17 @@ export function createStartHandler( const middlewares = flattenedRequestMiddlewares.map( (d) => d.options.server, ) + const requestMwChain: Array<{ + name: string + startTime: number + endTime: number + }> = [] + const instrumentedRequestMws = + process.env.NODE_ENV !== 'production' + ? instrumentMiddlewareArray(middlewares as Array, requestMwChain) + : middlewares const ctx = await executeMiddleware( - [...middlewares, requestHandlerMiddleware], + [...instrumentedRequestMws, requestHandlerMiddleware], { request, pathname: url.pathname, @@ -633,8 +754,40 @@ export function createStartHandler( }, ) - return handleRedirectResponse(ctx.response, request, getRouter) + if ( + process.env.NODE_ENV !== 'production' && + requestId && + requestMwChain.length > 0 + ) { + startEventClient.emit('middleware-executed', { + requestId, + scope: 'request', + chain: requestMwChain, + totalDuration: requestMwChain.reduce( + (sum, m) => sum + (m.endTime - m.startTime), + 0, + ), + startTime: requestMwChain[0]!.startTime - requestStartTime!, + }) + } + + response = await handleRedirectResponse( + ctx.response, + request, + getRouter, + { requestId, requestType }, + ) + return response } finally { + if (process.env.NODE_ENV !== 'production' && requestId && response) { + startEventClient.emit('request-end', { + requestId, + type: requestType, + status: response.status, + duration: performance.now() - requestStartTime!, + responseHeaders: Object.fromEntries(response.headers.entries()), + }) + } if (router && !cbWillCleanup) { // Clean up router SSR state if it was set up but won't be cleaned up by the callback // (e.g., in redirect cases or early returns before the callback is invoked). @@ -653,6 +806,7 @@ async function handleRedirectResponse( response: Response, request: Request, getRouter: () => Promise, + devtoolsCtx?: { requestId?: string; requestType?: string }, ): Promise { if (!isRedirect(response)) { return response @@ -669,6 +823,20 @@ async function handleRedirectResponse( } const opts = response.options + + if (process.env.NODE_ENV !== 'production' && devtoolsCtx?.requestId) { + startEventClient.emit('redirect', { + requestId: devtoolsCtx.requestId, + from: request.url.replace(/https?:\/\/[^/]+/, ''), + to: (opts.href || + opts.to || + response.headers.get('Location') || + '') as string, + status: response.status || ((opts as any).status ?? 302), + isServerFn: devtoolsCtx.requestType === 'server-fn', + timestamp: Date.now(), + }) + } if (opts.to && typeof opts.to === 'string' && !opts.to.startsWith('/')) { throw new Error( `Server side redirects must use absolute paths via the 'href' or 'to' options. The redirect() method's "to" property accepts an internal path only. Use the "href" property to provide an external URL. Received: ${JSON.stringify(opts)}`, @@ -710,6 +878,8 @@ async function handleServerRoutes({ executeRouter, context, executedRequestMiddlewares, + requestId, + requestStartTime, }: { getRouter: () => Promise request: Request @@ -720,6 +890,8 @@ async function handleServerRoutes({ ) => Promise context: any executedRequestMiddlewares: Set + requestId?: string + requestStartTime?: number }): Promise { const router = await getRouter() const rewrittenUrl = executeRewriteInput(router.rewrite, url) @@ -753,6 +925,27 @@ async function handleServerRoutes({ // Add handler middleware if exact match const server = foundRoute?.options.server + + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('route-matched', { + requestId, + matchedRoutes: matchedRoutes.map((r: any) => ({ + id: r.id, + path: r.fullPath || r.path, + })), + foundRoute: foundRoute + ? { + id: foundRoute.id, + path: (foundRoute as any).fullPath || foundRoute.path, + } + : null, + isExactMatch: !!isExactMatch, + params: routeParams, + hasServerHandlers: !!server?.handlers, + timestamp: Date.now(), + }) + } + if (server?.handlers && isExactMatch) { const handlers = typeof server.handlers === 'function' @@ -786,12 +979,40 @@ async function handleServerRoutes({ executeRouter(ctx.context, matchedRoutes), ) - const ctx = await executeMiddleware(routeMiddlewares, { + const routeMwChain: Array<{ + name: string + startTime: number + endTime: number + }> = [] + const instrumentedRouteMws = + process.env.NODE_ENV !== 'production' + ? instrumentMiddlewareArray(routeMiddlewares as Array, routeMwChain) + : routeMiddlewares + + const ctx = await executeMiddleware(instrumentedRouteMws, { request, context, params: routeParams, pathname, }) + if ( + process.env.NODE_ENV !== 'production' && + requestId && + routeMwChain.length > 0 + ) { + startEventClient.emit('middleware-executed', { + requestId, + scope: 'route', + chain: routeMwChain, + totalDuration: + Math.max(...routeMwChain.map((m) => m.endTime)) - + Math.min(...routeMwChain.map((m) => m.startTime)), + startTime: requestStartTime + ? routeMwChain[0]!.startTime - requestStartTime + : routeMwChain[0]!.startTime, + }) + } + return ctx.response } diff --git a/packages/start-server-core/src/devtools-instrumentation.ts b/packages/start-server-core/src/devtools-instrumentation.ts new file mode 100644 index 0000000000..8e7127bd07 --- /dev/null +++ b/packages/start-server-core/src/devtools-instrumentation.ts @@ -0,0 +1,29 @@ +const GENERIC_FUNCTION_NAMES = new Set(['', 'Mock', 'mockConstructor']) + +export function instrumentMiddlewareArray< + T extends (...args: Array) => any, +>( + middlewares: Array, + chain: Array<{ name: string; startTime: number; endTime: number }>, +): Array { + return middlewares.map((mw, i) => { + const name = + mw.name && !GENERIC_FUNCTION_NAMES.has(mw.name) + ? mw.name + : `middleware-${i}` + const wrapped = async (ctx: any) => { + const mwStart = performance.now() + try { + return await mw(ctx) + } finally { + chain.push({ + name, + startTime: mwStart, + endTime: performance.now(), + }) + } + } + Object.defineProperty(wrapped, 'name', { value: name }) + return wrapped as unknown as T + }) +} diff --git a/packages/start-server-core/src/event-client.ts b/packages/start-server-core/src/event-client.ts new file mode 100644 index 0000000000..58365e525d --- /dev/null +++ b/packages/start-server-core/src/event-client.ts @@ -0,0 +1,133 @@ +import { EventClient } from '@tanstack/devtools-event-client' + +export interface StartEventMap { + // === Major Phase Pairs (waterfall bars) === + + 'request-start': { + requestId: string + url: string + method: string + headers: Record + timestamp: number + } + + 'request-end': { + requestId: string + type: 'server-fn' | 'ssr' | 'server-route' + status: number + duration: number + responseHeaders: Record + error?: { message: string; stack?: string } + } + + 'server-fn-start': { + requestId: string + serverFnId: string + serverFnName: string + filename: string + httpMethod: string + inputPayloadType: 'json' | 'formdata' | 'query-string' | 'none' + startTime: number + } + + 'server-fn-end': { + requestId: string + serverFnId: string + duration: number + resultType: + | 'json' + | 'ndjson-stream' + | 'framed-binary' + | 'raw-response' + | 'redirect' + | 'not-found' + | 'error' + status: number + error?: { message: string; stack?: string } + } + + 'ssr-start': { + requestId: string + matchedRoute: string + params: Record + startTime: number + } + + 'ssr-end': { + requestId: string + duration: number + routerLoadDuration: number + dehydrationDuration: number + renderDuration: number + hadRedirect: boolean + } + + // === Consolidated Events (rich payloads) === + + 'middleware-executed': { + requestId: string + scope: 'request' | 'route' | 'server-fn' + chain: Array<{ + name: string + startTime: number + endTime: number + }> + totalDuration: number + startTime: number + } + + 'route-matched': { + requestId: string + matchedRoutes: Array<{ id: string; path: string }> + foundRoute: { id: string; path: string } | null + isExactMatch: boolean + params: Record + hasServerHandlers: boolean + timestamp: number + } + + 'serialization-result': { + requestId: string + format: 'json' | 'ndjson' | 'framed-binary' | 'raw-response' + status: number + contentType: string + hasRawStreams: boolean + duration: number + } + + 'stream-chunk': { + requestId: string + serverFnId: string + chunkIndex: number + totalChunks?: number + timestamp: number + } + + redirect: { + requestId: string + from: string + to: string + status: number + isServerFn: boolean + timestamp: number + } + + error: { + requestId: string + phase: 'middleware' | 'routing' | 'server-fn' | 'ssr' | 'serialization' + message: string + stack?: string + timestamp: number + } +} + +class StartEventClient extends EventClient { + constructor() { + super({ + pluginId: 'start', + enabled: process.env.NODE_ENV !== 'production', + }) + } +} + +export const startEventClient = new StartEventClient() diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts index 4f5067a7eb..249aafb79e 100644 --- a/packages/start-server-core/src/server-functions-handler.ts +++ b/packages/start-server-core/src/server-functions-handler.ts @@ -4,6 +4,8 @@ import { isRedirect, } from '@tanstack/router-core' import invariant from 'tiny-invariant' +import { startEventClient } from './event-client' +import { getStartContext } from '@tanstack/start-storage-context' import { TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, @@ -50,6 +52,13 @@ export const handleServerAction = async ({ const action = await getServerFnById(serverFnId, { fromClient: true }) + const startCtx = getStartContext({ throwIfNotFound: false }) + const requestId = startCtx?.requestId + const requestStartTime = startCtx?.requestStartTime + const fnMeta = (action as any).serverFnMeta as + | { id: string; name?: string; filename?: string } + | undefined + // Early method check: reject mismatched HTTP methods before parsing // the request payload (FormData, JSON, query string, etc.) if (action.method && methodUpper !== action.method) { @@ -73,6 +82,28 @@ export const handleServerAction = async ({ const contentType = request.headers.get('Content-Type') + const serverFnStart = performance.now() + + if (process.env.NODE_ENV !== 'production' && requestId) { + const inputPayloadType = contentType?.includes('multipart/form-data') + ? 'formdata' + : contentType?.includes('application/json') + ? 'json' + : request.method === 'GET' + ? 'query-string' + : 'none' + + startEventClient.emit('server-fn-start', { + requestId, + serverFnId: fnMeta?.id || serverFnId, + serverFnName: fnMeta?.name || serverFnId, + filename: fnMeta?.filename || 'unknown', + httpMethod: request.method, + inputPayloadType, + startTime: serverFnStart - (requestStartTime ?? serverFnStart), + }) + } + function parsePayload(payload: any) { const parsedPayload = fromJSON(payload, { plugins: serovalPlugins }) return parsedPayload as any @@ -158,7 +189,9 @@ export const handleServerAction = async ({ const unwrapped = res.result || res.error + let wasNotFound = false if (isNotFound(res)) { + wasNotFound = true res = isNotFoundResponse(res) } @@ -177,6 +210,9 @@ export const handleServerAction = async ({ return serializeResult(res) function serializeResult(res: unknown): Response { + const serializationStart = performance.now() + let lastChunkEmitTime = 0 + let chunkIndex = 0 let nonStreamingBody: any = undefined const alsResponse = getResponse() @@ -225,6 +261,16 @@ export const handleServerAction = async ({ // If no raw streams and done synchronously, return simple JSON if (done && rawStreams.size === 0) { + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('serialization-result', { + requestId, + format: 'json', + status: alsResponse.status ?? 200, + contentType: 'application/json', + hasRawStreams: false, + duration: performance.now() - serializationStart, + }) + } return new Response( nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined, { @@ -244,9 +290,33 @@ export const handleServerAction = async ({ const jsonStream = new ReadableStream({ start(controller) { callbacks.onParse = (value) => { + chunkIndex++ + const now = performance.now() + if ( + process.env.NODE_ENV !== 'production' && + requestId && + now - lastChunkEmitTime >= 100 + ) { + startEventClient.emit('stream-chunk', { + requestId, + serverFnId, + chunkIndex, + timestamp: Date.now(), + }) + lastChunkEmitTime = now + } controller.enqueue(JSON.stringify(value) + '\n') } callbacks.onDone = () => { + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('stream-chunk', { + requestId, + serverFnId, + chunkIndex, + totalChunks: chunkIndex, + timestamp: Date.now(), + }) + } try { controller.close() } catch { @@ -267,6 +337,16 @@ export const handleServerAction = async ({ rawStreams, ) + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('serialization-result', { + requestId, + format: 'framed-binary', + status: alsResponse.status ?? 200, + contentType: 'application/x-tss-framed; v=1', + hasRawStreams: true, + duration: performance.now() - serializationStart, + }) + } return new Response(multiplexedStream, { status: alsResponse.status, statusText: alsResponse.statusText, @@ -280,11 +360,36 @@ export const handleServerAction = async ({ // No raw streams but not done yet - use standard NDJSON streaming const stream = new ReadableStream({ start(controller) { - callbacks.onParse = (value) => + callbacks.onParse = (value) => { + chunkIndex++ + const now = performance.now() + if ( + process.env.NODE_ENV !== 'production' && + requestId && + now - lastChunkEmitTime >= 100 + ) { + startEventClient.emit('stream-chunk', { + requestId, + serverFnId, + chunkIndex, + timestamp: Date.now(), + }) + lastChunkEmitTime = now + } controller.enqueue( textEncoder.encode(JSON.stringify(value) + '\n'), ) + } callbacks.onDone = () => { + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('stream-chunk', { + requestId, + serverFnId, + chunkIndex, + totalChunks: chunkIndex, + timestamp: Date.now(), + }) + } try { controller.close() } catch (error) { @@ -298,6 +403,16 @@ export const handleServerAction = async ({ } }, }) + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('serialization-result', { + requestId, + format: 'ndjson', + status: alsResponse.status ?? 200, + contentType: 'application/x-ndjson', + hasRawStreams: false, + duration: performance.now() - serializationStart, + }) + } return new Response(stream, { status: alsResponse.status, statusText: alsResponse.statusText, @@ -308,12 +423,32 @@ export const handleServerAction = async ({ }) } + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('serialization-result', { + requestId, + format: 'raw-response', + status: alsResponse.status ?? 200, + contentType: '', + hasRawStreams: false, + duration: performance.now() - serializationStart, + }) + } return new Response(undefined, { status: alsResponse.status, statusText: alsResponse.statusText, }) } } catch (error: any) { + if (process.env.NODE_ENV !== 'production' && requestId) { + startEventClient.emit('error', { + requestId, + phase: 'server-fn', + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + timestamp: Date.now(), + }) + } + if (error instanceof Response) { return error } @@ -360,6 +495,31 @@ export const handleServerAction = async ({ } })() + if (process.env.NODE_ENV !== 'production' && requestId && response) { + const respContentType = response.headers.get('Content-Type') || '' + const resultType = wasNotFound + ? 'not-found' + : response.status >= 400 + ? 'error' + : isRedirect(response) + ? 'redirect' + : respContentType.includes('x-tss-framed') + ? 'framed-binary' + : respContentType.includes('x-ndjson') + ? 'ndjson-stream' + : respContentType.includes('application/json') + ? 'json' + : 'raw-response' + + startEventClient.emit('server-fn-end', { + requestId, + serverFnId: fnMeta?.id || serverFnId, + duration: performance.now() - serverFnStart, + resultType, + status: response.status, + }) + } + return response } diff --git a/packages/start-server-core/tests/devtools-instrumentation.test.ts b/packages/start-server-core/tests/devtools-instrumentation.test.ts new file mode 100644 index 0000000000..06f6851a5c --- /dev/null +++ b/packages/start-server-core/tests/devtools-instrumentation.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from 'vitest' +import { instrumentMiddlewareArray } from '../src/devtools-instrumentation' + +describe('instrumentMiddlewareArray', () => { + it('returns a wrapped array of the same length', () => { + const mws = [vi.fn(), vi.fn(), vi.fn()] + const chain: Array<{ name: string; startTime: number; endTime: number }> = + [] + const wrapped = instrumentMiddlewareArray(mws, chain) + expect(wrapped).toHaveLength(3) + }) + + it('calls original middleware with the same ctx and returns its result', async () => { + const original = vi.fn().mockResolvedValue({ data: 42 }) + const chain: Array<{ name: string; startTime: number; endTime: number }> = + [] + const [wrapped] = instrumentMiddlewareArray([original], chain) + + const ctx = { request: 'test', next: vi.fn() } + const result = await wrapped!(ctx) + + expect(original).toHaveBeenCalledWith(ctx) + expect(result).toEqual({ data: 42 }) + }) + + it('records timing entries in the chain array', async () => { + const mw1 = vi.fn().mockResolvedValue(undefined) + const mw2 = vi.fn().mockResolvedValue(undefined) + const chain: Array<{ name: string; startTime: number; endTime: number }> = + [] + const wrapped = instrumentMiddlewareArray([mw1, mw2], chain) + + await wrapped[0]!({}) + await wrapped[1]!({}) + + expect(chain).toHaveLength(2) + expect(chain[0]!.name).toBe('middleware-0') + expect(chain[1]!.name).toBe('middleware-1') + expect(chain[0]!.startTime).toBeLessThanOrEqual(chain[0]!.endTime) + expect(chain[1]!.startTime).toBeLessThanOrEqual(chain[1]!.endTime) + }) + + it('preserves named middleware function names', async () => { + function authMiddleware(_ctx: any) { + return Promise.resolve() + } + const chain: Array<{ name: string; startTime: number; endTime: number }> = + [] + const wrapped = instrumentMiddlewareArray([authMiddleware], chain) + + await wrapped[0]!({}) + + expect(chain[0]!.name).toBe('authMiddleware') + expect(wrapped[0]!.name).toBe('authMiddleware') + }) + + it('records timing even when middleware throws', async () => { + const failing = vi.fn().mockRejectedValue(new Error('boom')) + const chain: Array<{ name: string; startTime: number; endTime: number }> = + [] + const [wrapped] = instrumentMiddlewareArray([failing], chain) + + await expect(wrapped!({})).rejects.toThrow('boom') + expect(chain).toHaveLength(1) + expect(chain[0]!.endTime).toBeGreaterThanOrEqual(chain[0]!.startTime) + }) + + it('returns empty array for empty input', () => { + const chain: Array<{ name: string; startTime: number; endTime: number }> = + [] + const wrapped = instrumentMiddlewareArray([], chain) + expect(wrapped).toHaveLength(0) + }) +}) diff --git a/packages/start-server-core/vite.config.ts b/packages/start-server-core/vite.config.ts index b2c5589b57..d07f9c22bf 100644 --- a/packages/start-server-core/vite.config.ts +++ b/packages/start-server-core/vite.config.ts @@ -20,6 +20,7 @@ export default mergeConfig( srcDir: './src', entry: [ './src/index.tsx', + './src/event-client.ts', './src/createServerRpc.ts', './src/createSsrRpc.ts', './src/fake-start-server-fn-resolver.ts', diff --git a/packages/start-storage-context/src/async-local-storage.ts b/packages/start-storage-context/src/async-local-storage.ts index 526c918266..224ac8eb2e 100644 --- a/packages/start-storage-context/src/async-local-storage.ts +++ b/packages/start-storage-context/src/async-local-storage.ts @@ -11,6 +11,11 @@ export interface StartStorageContext { // Track middlewares that have already executed in the request phase // to prevent duplicate execution executedRequestMiddlewares: Set + + // Devtools context threading (optional — never set in production) + requestId?: string + requestStartTime?: number + eventClient?: { emit: (event: string, payload: any) => void } } // Use a global symbol to ensure the same AsyncLocalStorage instance is shared diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 443d3728da..0d194b5606 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ overrides: '@tanstack/vue-start-client': workspace:* '@tanstack/vue-start-server': workspace:* '@tanstack/start-plugin-core': workspace:* + '@tanstack/start-devtools': workspace:* + '@tanstack/react-start-devtools': workspace:* + '@tanstack/solid-start-devtools': workspace:* '@tanstack/start-client-core': workspace:* '@tanstack/start-server-core': workspace:* '@tanstack/start-storage-context': workspace:* @@ -8515,6 +8518,12 @@ importers: examples/react/start-basic-react-query: dependencies: + '@tanstack/devtools-vite': + specifier: ^0.3.3 + version: 0.3.12(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@tanstack/react-devtools': + specifier: ^0.7.0 + version: 0.7.0(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-query': specifier: ^5.90.19 version: 5.90.19(react@19.2.3) @@ -8533,6 +8542,9 @@ importers: '@tanstack/react-start': specifier: workspace:* version: link:../../../packages/react-start + '@tanstack/react-start-devtools': + specifier: workspace:* + version: link:../../../packages/react-start-devtools react: specifier: ^19.2.3 version: 19.2.3 @@ -8755,7 +8767,7 @@ importers: version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17(vitest@4.0.17))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -11271,7 +11283,7 @@ importers: version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17(vitest@4.0.17))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -12098,6 +12110,40 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + packages/react-start-devtools: + dependencies: + '@tanstack/devtools-utils': + specifier: ^0.0.3 + version: 0.0.3(@types/react@19.2.8)(react@19.2.3)(solid-js@1.9.10) + '@tanstack/start-devtools': + specifier: workspace:* + version: link:../start-devtools + '@types/react': + specifier: ^19.2.8 + version: 19.2.8 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.8) + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@eslint-react/eslint-plugin': + specifier: ^1.53.1 + version: 1.53.1(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3) + '@vitejs/plugin-react': + specifier: ^5.0.2 + version: 5.1.0(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + eslint-plugin-react-compiler: + specifier: 19.1.0-rc.2 + version: 19.1.0-rc.2(eslint@9.22.0(jiti@2.6.1)) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.22.0(jiti@2.6.1)) + packages/react-start-server: dependencies: '@tanstack/history': @@ -12580,6 +12626,22 @@ importers: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + packages/solid-start-devtools: + dependencies: + '@tanstack/devtools-utils': + specifier: ^0.0.3 + version: 0.0.3(@types/react@19.2.8)(react@19.2.3)(solid-js@1.9.10) + '@tanstack/start-devtools': + specifier: workspace:* + version: link:../start-devtools + solid-js: + specifier: 1.9.10 + version: 1.9.10 + devDependencies: + vite-plugin-solid: + specifier: ^2.11.8 + version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + packages/solid-start-server: dependencies: '@solidjs/meta': @@ -12639,6 +12701,37 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + packages/start-devtools: + dependencies: + '@tanstack/devtools-ui': + specifier: ^0.3.5 + version: 0.3.5(csstype@3.2.3)(solid-js@1.9.10) + '@tanstack/devtools-utils': + specifier: ^0.0.3 + version: 0.0.3(@types/react@19.2.8)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10) + '@tanstack/solid-store': + specifier: ^0.7.5 + version: 0.7.7(solid-js@1.9.10) + '@tanstack/start-server-core': + specifier: workspace:* + version: link:../start-server-core + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dayjs: + specifier: ^1.11.18 + version: 1.11.19 + goober: + specifier: ^2.1.16 + version: 2.1.16(csstype@3.2.3) + solid-js: + specifier: 1.9.10 + version: 1.9.10 + devDependencies: + vite-plugin-solid: + specifier: ^2.11.8 + version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + packages/start-fn-stubs: devDependencies: vite: @@ -12726,6 +12819,9 @@ importers: packages/start-server-core: dependencies: + '@tanstack/devtools-event-client': + specifier: ^0.3.2 + version: 0.3.4 '@tanstack/history': specifier: workspace:* version: link:../history @@ -13217,6 +13313,13 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-proposal-private-methods@7.18.6': + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-decorators@7.25.9': resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} engines: {node: '>=6.9.0'} @@ -14566,14 +14669,26 @@ packages: resolution: {integrity: sha512-WuljGOJaaiehGkW0aAyuCZIGKfcv/Q1fSl4rvlfWohIDgpp5MFIkBa56drR75WUdNKrrUb3JirnVGIAhegUBIA==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} + '@eslint-react/ast@1.53.1': + resolution: {integrity: sha512-qvUC99ewtriJp9quVEOvZ6+RHcsMLfVQ0OhZ4/LupZUDhjW7GiX1dxJsFaxHdJ9rLNLhQyLSPmbAToeqUrSruQ==} + engines: {node: '>=18.18.0'} + '@eslint-react/core@1.26.2': resolution: {integrity: sha512-2mB5hZBL6XmOjDNL3o0h/qHQHuzxGQGYtQQHjD0Yddhde7NU/b4z/oxtrzEInc6Lk2Ry7Rhqi4S49EpwKXWJlQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} + '@eslint-react/core@1.53.1': + resolution: {integrity: sha512-8prroos5/Uvvh8Tjl1HHCpq4HWD3hV9tYkm7uXgKA6kqj0jHlgRcQzuO6ZPP7feBcK3uOeug7xrq03BuG8QKCA==} + engines: {node: '>=18.18.0'} + '@eslint-react/eff@1.26.2': resolution: {integrity: sha512-7ttz+DPNZl+cHdR5PwU9/ff95VHZmo10icGVX34HyRktJuU2boinWzib5KRg6V1jVwgWuzdvULNXyBd5NVMhhg==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} + '@eslint-react/eff@1.53.1': + resolution: {integrity: sha512-uq20lPRAmsWRjIZm+mAV/2kZsU2nDqn5IJslxGWe3Vfdw23hoyhEw3S1KKlxbftwbTvsZjKvVP0iw3bZo/NUpg==} + engines: {node: '>=18.18.0'} + '@eslint-react/eslint-plugin@1.26.2': resolution: {integrity: sha512-nTfR32jTLChc0RXKbks2Gf6seMYeqiCGj0qYq+yOmEn/XhcDWVQj86SHIJLFPwvH3LSwDUSgiQzdW9jn/rNv3A==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} @@ -14584,18 +14699,40 @@ packages: typescript: optional: true + '@eslint-react/eslint-plugin@1.53.1': + resolution: {integrity: sha512-JZ2ciXNCC9CtBBAqYtwWH+Jy/7ZzLw+whei8atP4Fxsbh+Scs30MfEwBzuiEbNw6uF9eZFfPidchpr5RaEhqxg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^9.22.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + '@eslint-react/jsx@1.26.2': resolution: {integrity: sha512-lldo9Sd/tZslBN8X7/ZAZXY7UccZZYctrNAoeR8DFMFWLxzvooykixLOl5YkRCWm4uaSmq3r3VNFZ35N2wcbyQ==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} + '@eslint-react/kit@1.53.1': + resolution: {integrity: sha512-zOi2le9V4rMrJvQV4OeedGvMGvDT46OyFPOwXKs7m0tQu5vXVJ8qwIPaVQT1n/WIuvOg49OfmAVaHpGxK++xLQ==} + engines: {node: '>=18.18.0'} + '@eslint-react/shared@1.26.2': resolution: {integrity: sha512-q/xrNkFe8sHAPjaAuvqyCl3Ls5ly9cfUpAfhAgxYtArNAtIZHvuwu0zrwoHMYk0ZpZi+VlQYwUCtKX8axPXoTw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} + '@eslint-react/shared@1.53.1': + resolution: {integrity: sha512-gomJQmFqQgQVI3Ra4vTMG/s6a4bx3JqeNiTBjxBJt4C9iGaBj458GkP4LJHX7TM6xUzX+fMSKOPX7eV3C/+UCw==} + engines: {node: '>=18.18.0'} + '@eslint-react/var@1.26.2': resolution: {integrity: sha512-9abwhGTd4DBxOy5jVF0CnjEYDiRTXg4cbbAulZ+MVqE03KZDWNAVYYEYI5e+YTOcyJbGYY/zPEYmB+c+cUEiyw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} + '@eslint-react/var@1.53.1': + resolution: {integrity: sha512-yzwopvPntcHU7mmDvWzRo1fb8QhjD8eDRRohD11rTV1u7nWO4QbJi0pOyugQakvte1/W11Y0Vr8Of0Ojk/A6zg==} + engines: {node: '>=18.18.0'} + '@eslint/config-array@0.19.2': resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -17964,6 +18101,10 @@ packages: resolution: {integrity: sha512-LefnH9KE9uRDEWifc3QDcooskA8ikfs41bybDTgpYQpyTUspZnaEdUdya9Hry0KYxZ8nos0S3nNbsP79KHqr6Q==} engines: {node: '>=18'} + '@tanstack/devtools-client@0.0.5': + resolution: {integrity: sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA==} + engines: {node: '>=18'} + '@tanstack/devtools-event-bus@0.3.2': resolution: {integrity: sha512-yJT2As/drc+Epu0nsqCsJaKaLcaNGufiNxSlp/+/oeTD0jsBxF9/PJBfh66XVpYXkKr97b8689mSu7QMef0Rrw==} engines: {node: '>=18'} @@ -17976,6 +18117,10 @@ packages: resolution: {integrity: sha512-eq+PpuutUyubXu+ycC1GIiVwBs86NF/8yYJJAKSpPcJLWl6R/761F1H4F/9ziX6zKezltFUH1ah3Cz8Ah+KJrw==} engines: {node: '>=18'} + '@tanstack/devtools-event-client@0.4.2': + resolution: {integrity: sha512-nerCPwV6RI4zQY+T5xxXEDOPgSF/gqf6dmCbDpTwkAvQJPHKgroHwKE5kvAcM3JC3ptdr5euwNV0//f8e+wmfQ==} + engines: {node: '>=18'} + '@tanstack/devtools-ui@0.3.5': resolution: {integrity: sha512-DU8OfLntngnph+Tb7ivQvh4F4w+rDu6r01fXlhjq/Nmgdr0gtsOox4kdmyq5rCs+C6aPgP3M7+BE+fv4dN+VvA==} engines: {node: '>=18'} @@ -17988,6 +18133,27 @@ packages: peerDependencies: solid-js: 1.9.10 + '@tanstack/devtools-utils@0.0.3': + resolution: {integrity: sha512-7FIIOIuMPQZ42FpXureF4vdMbjyTLCfVIEhSDInjDjSOtoEbkIUtw+HK25GyldOG2uLs1iYNij+Ln675KT0iBw==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': ^19.2.8 + react: ^19.2.3 + solid-js: 1.9.10 + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + solid-js: + optional: true + + '@tanstack/devtools-vite@0.3.12': + resolution: {integrity: sha512-fGJgu4xUhKmGk+a+/aHD8l5HKVk6+ObA+6D3YC3xCXbai/YmaGhztqcZf1tKUqjZyYyQLHsjqmKzvJgVpQP1jw==} + engines: {node: '>=18'} + peerDependencies: + vite: ^7.3.1 + '@tanstack/devtools@0.6.14': resolution: {integrity: sha512-dOtHoeLjjcHeNscu+ZEf89EFboQsy0ggb6pf8Sha59qBUeQbjUsaAvwP8Ogwg89oJxFQbTP7DKYNBNw5CxlNEA==} engines: {node: '>=18'} @@ -18079,6 +18245,11 @@ packages: peerDependencies: solid-js: 1.9.10 + '@tanstack/solid-store@0.7.7': + resolution: {integrity: sha512-DnEZbqQ+pg68BguHz17VFukfp+6JaTk8nE2MhdVliU8bhsOFlTMsmVHp/4gMoQ1AkmAOMFiBsSliROCaaeJzvg==} + peerDependencies: + solid-js: 1.9.10 + '@tanstack/solid-store@0.9.1': resolution: {integrity: sha512-gx7ToM+Yrkui36NIj0HjAufzv1Dg8usjtVFy5H3Ll52Xjuz+eliIJL+ihAr4LRuWh3nDPBR+nCLW0ShFrbE5yw==} peerDependencies: @@ -18089,6 +18260,9 @@ packages: peerDependencies: solid-js: 1.9.10 + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tanstack/store@0.9.1': resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} @@ -19516,6 +19690,10 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -20019,6 +20197,9 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + db0@0.3.4: resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} peerDependencies: @@ -20593,6 +20774,12 @@ packages: peerDependencies: eslint: ^9.22.0 + eslint-plugin-react-compiler@19.1.0-rc.2: + resolution: {integrity: sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==} + engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} + peerDependencies: + eslint: ^9.22.0 + eslint-plugin-react-debug@1.26.2: resolution: {integrity: sha512-UKCXj090YGXYmVLfZ8yZh09RLPlMfhJFYRXGUL4i/IHal22PO7kNTwNSHw105THVJXTiKPxuj/dDbII3H2C+7w==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} @@ -20603,6 +20790,16 @@ packages: typescript: optional: true + eslint-plugin-react-debug@1.53.1: + resolution: {integrity: sha512-WNOiQ6jhodJE88VjBU/IVDM+2Zr9gKHlBFDUSA3fQ0dMB5RiBVj5wMtxbxRuipK/GqNJbteqHcZoYEod7nfddg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^9.22.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + eslint-plugin-react-dom@1.26.2: resolution: {integrity: sha512-W4PLB5+zRt+Ceewtwf2tobEPBF+Pvl5ycElRPeKT9VOpn6IAqk0i5JqCVu7mPvPruLFbUDlGaHK769aZhqLyjA==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} @@ -20613,6 +20810,16 @@ packages: typescript: optional: true + eslint-plugin-react-dom@1.53.1: + resolution: {integrity: sha512-UYrWJ2cS4HpJ1A5XBuf1HfMpPoLdfGil+27g/ldXfGemb4IXqlxHt4ANLyC8l2CWcE3SXGJW7mTslL34MG0qTQ==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^9.22.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + eslint-plugin-react-hooks-extra@1.26.2: resolution: {integrity: sha512-Xh1Pp6lvYDI8aOFDvtd1E6WzBE3QVk2cV48pmKQOWzzL25Tod/7kk4pOXoML1t1rqRQW8xcoL7UmrlR8IMQh+w==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} @@ -20623,12 +20830,28 @@ packages: typescript: optional: true + eslint-plugin-react-hooks-extra@1.53.1: + resolution: {integrity: sha512-fshTnMWNn9NjFLIuy7HzkRgGK29vKv4ZBO9UMr+kltVAfKLMeXXP6021qVKk66i/XhQjbktiS+vQsu1Rd3ZKvg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^9.22.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + eslint-plugin-react-hooks@5.1.0: resolution: {integrity: sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==} engines: {node: '>=10'} peerDependencies: eslint: ^9.22.0 + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^9.22.0 + eslint-plugin-react-naming-convention@1.26.2: resolution: {integrity: sha512-eiudTnDwwVpOY6K2g2fsoklG3x4X7N0X4+wFM2AE0+qSy4TCCFic+H+NRi3T53nL0pbvNawHkjS8sRSRRzOG3A==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} @@ -20639,6 +20862,16 @@ packages: typescript: optional: true + eslint-plugin-react-naming-convention@1.53.1: + resolution: {integrity: sha512-rvZ/B/CSVF8d34HQ4qIt90LRuxotVx+KUf3i1OMXAyhsagEFMRe4gAlPJiRufZ+h9lnuu279bEdd+NINsXOteA==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^9.22.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + eslint-plugin-react-web-api@1.26.2: resolution: {integrity: sha512-xu0QWvptg9zDaf/hfiJ02hTOd/soF10ww3h9wnJZ7ohbMclIA89ZRET6lXXXJNie3HrOLsB+HmOg/h/Rc7zL+A==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} @@ -20649,6 +20882,16 @@ packages: typescript: optional: true + eslint-plugin-react-web-api@1.53.1: + resolution: {integrity: sha512-INVZ3Cbl9/b+sizyb43ChzEPXXYuDsBGU9BIg7OVTNPyDPloCXdI+dQFAcSlDocZhPrLxhPV3eT6+gXbygzYXg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^9.22.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + typescript: + optional: true + eslint-plugin-react-x@1.26.2: resolution: {integrity: sha512-4wEHGPdCY8yNl0AYcZWDdQ6QyX7lRmjoaM7CSw3v9ZEHLh2u8ttKl2JJpx5mRKFWP0JxQ8YvbgLW8MovDAIWmw==} engines: {bun: '>=1.0.15', node: '>=18.18.0'} @@ -20662,6 +20905,19 @@ packages: typescript: optional: true + eslint-plugin-react-x@1.53.1: + resolution: {integrity: sha512-MwMNnVwiPem0U6SlejDF/ddA4h/lmP6imL1RDZ2m3pUBrcdcOwOx0gyiRVTA3ENnhRlWfHljHf5y7m8qDSxMEg==} + engines: {node: '>=18.18.0'} + peerDependencies: + eslint: ^9.22.0 + ts-api-utils: ^2.1.0 + typescript: ^4.9.5 || ^5.3.3 + peerDependenciesMeta: + ts-api-utils: + optional: true + typescript: + optional: true + eslint-plugin-solid@0.14.5: resolution: {integrity: sha512-nfuYK09ah5aJG/oEN6P1qziy1zLgW4PDWe75VNPi4CEFYk1x2AEqwFeQfEPR7gNn0F2jOeqKhx2E+5oNCOBYWQ==} engines: {node: '>=18.0.0'} @@ -21271,6 +21527,12 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hey-listen@1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} @@ -21969,6 +22231,9 @@ packages: engines: {node: '>=8'} hasBin: true + launch-editor@2.13.1: + resolution: {integrity: sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==} + launch-editor@2.9.1: resolution: {integrity: sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==} @@ -24514,6 +24779,9 @@ packages: ts-pattern@5.6.2: resolution: {integrity: sha512-d4IxJUXROL5NCa3amvMg6VQW2HVtZYmUTPfvVtO7zJWGYLJ+mry9v2OmYm+z67aniQoQ8/yFNadiEwtNS9qQiw==} + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} @@ -25567,6 +25835,12 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} + zod-validation-error@3.5.4: + resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.24.4 + zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} @@ -25812,6 +26086,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -26960,6 +27242,19 @@ snapshots: - supports-color - typescript + '@eslint-react/ast@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + '@eslint-react/core@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -26978,8 +27273,28 @@ snapshots: - supports-color - typescript + '@eslint-react/core@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + birecord: 0.1.1 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + '@eslint-react/eff@1.26.2': {} + '@eslint-react/eff@1.53.1': {} + '@eslint-react/eslint-plugin@1.26.2(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 1.26.2 @@ -27001,6 +27316,28 @@ snapshots: - supports-color - ts-api-utils + '@eslint-react/eslint-plugin@1.53.1(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.22.0(jiti@2.6.1) + eslint-plugin-react-debug: 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-dom: 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 1.53.1(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + - ts-api-utils + '@eslint-react/jsx@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -27015,6 +27352,17 @@ snapshots: - supports-color - typescript + '@eslint-react/kit@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + ts-pattern: 5.9.0 + zod: 4.1.12 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + '@eslint-react/shared@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 1.26.2 @@ -27026,6 +27374,18 @@ snapshots: - supports-color - typescript + '@eslint-react/shared@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + ts-pattern: 5.9.0 + zod: 4.1.12 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + '@eslint-react/var@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -27040,6 +27400,20 @@ snapshots: - supports-color - typescript + '@eslint-react/var@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + '@eslint/config-array@0.19.2': dependencies: '@eslint/object-schema': 2.1.6 @@ -30720,6 +31094,10 @@ snapshots: dependencies: '@tanstack/devtools-event-client': 0.3.4 + '@tanstack/devtools-client@0.0.5': + dependencies: + '@tanstack/devtools-event-client': 0.4.2 + '@tanstack/devtools-event-bus@0.3.2': dependencies: ws: 8.18.3 @@ -30736,6 +31114,8 @@ snapshots: '@tanstack/devtools-event-client@0.3.4': {} + '@tanstack/devtools-event-client@0.4.2': {} + '@tanstack/devtools-ui@0.3.5(csstype@3.2.3)(solid-js@1.9.10)': dependencies: clsx: 2.1.1 @@ -30744,6 +31124,14 @@ snapshots: transitivePeerDependencies: - csstype + '@tanstack/devtools-ui@0.3.5(solid-js@1.9.10)': + dependencies: + clsx: 2.1.1 + goober: 2.1.16(csstype@3.1.3) + solid-js: 1.9.10 + transitivePeerDependencies: + - csstype + '@tanstack/devtools-ui@0.4.4(csstype@3.2.3)(solid-js@1.9.10)': dependencies: clsx: 2.1.1 @@ -30752,6 +31140,44 @@ snapshots: transitivePeerDependencies: - csstype + '@tanstack/devtools-utils@0.0.3(@types/react@19.2.8)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)': + dependencies: + '@tanstack/devtools-ui': 0.3.5(csstype@3.2.3)(solid-js@1.9.10) + optionalDependencies: + '@types/react': 19.2.8 + react: 19.2.3 + solid-js: 1.9.10 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-utils@0.0.3(@types/react@19.2.8)(react@19.2.3)(solid-js@1.9.10)': + dependencies: + '@tanstack/devtools-ui': 0.3.5(solid-js@1.9.10) + optionalDependencies: + '@types/react': 19.2.8 + react: 19.2.3 + solid-js: 1.9.10 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-vite@0.3.12(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@tanstack/devtools-client': 0.0.5 + '@tanstack/devtools-event-bus': 0.3.3 + chalk: 5.6.2 + launch-editor: 2.13.1 + picomatch: 4.0.3 + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@tanstack/devtools@0.6.14(csstype@3.2.3)(solid-js@1.9.10)': dependencies: '@solid-primitives/keyboard': 1.3.0(solid-js@1.9.10) @@ -30897,6 +31323,11 @@ snapshots: '@tanstack/query-core': 5.90.19 solid-js: 1.9.10 + '@tanstack/solid-store@0.7.7(solid-js@1.9.10)': + dependencies: + '@tanstack/store': 0.7.7 + solid-js: 1.9.10 + '@tanstack/solid-store@0.9.1(solid-js@1.9.10)': dependencies: '@tanstack/store': 0.9.1 @@ -30907,6 +31338,8 @@ snapshots: '@tanstack/virtual-core': 3.13.12 solid-js: 1.9.10 + '@tanstack/store@0.7.7': {} + '@tanstack/store@0.9.1': {} '@tanstack/typedoc-config@0.3.0(typescript@5.9.3)': @@ -31940,7 +32373,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.17(@types/node@25.0.9)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vitest: 4.0.17(@types/node@25.0.9)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -32953,6 +33386,8 @@ snapshots: chalk@5.4.1: {} + chalk@5.6.2: {} + char-regex@1.0.2: {} char-spinner@1.0.1: {} @@ -33454,6 +33889,8 @@ snapshots: dependencies: '@babel/runtime': 7.26.7 + dayjs@1.11.19: {} + db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3): optionalDependencies: '@electric-sql/pglite': 0.3.2 @@ -34123,6 +34560,18 @@ snapshots: transitivePeerDependencies: - typescript + eslint-plugin-react-compiler@19.1.0-rc.2(eslint@9.22.0(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.28.5) + eslint: 9.22.0(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 3.25.57 + zod-validation-error: 3.5.4(zod@3.25.57) + transitivePeerDependencies: + - supports-color + eslint-plugin-react-debug@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -34143,6 +34592,26 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-react-debug@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.22.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + eslint-plugin-react-dom@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -34163,6 +34632,26 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-react-dom@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 9.22.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + eslint-plugin-react-hooks-extra@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -34183,10 +34672,34 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-react-hooks-extra@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.22.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + eslint-plugin-react-hooks@5.1.0(eslint@9.22.0(jiti@2.6.1)): dependencies: eslint: 9.22.0(jiti@2.6.1) + eslint-plugin-react-hooks@5.2.0(eslint@9.22.0(jiti@2.6.1)): + dependencies: + eslint: 9.22.0(jiti@2.6.1) + eslint-plugin-react-naming-convention@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -34206,6 +34719,26 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-react-naming-convention@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.22.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + eslint-plugin-react-web-api@1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -34225,6 +34758,25 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-react-web-api@1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.22.0(jiti@2.6.1) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + eslint-plugin-react-x@1.26.2(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3): dependencies: '@eslint-react/ast': 1.26.2(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -34248,6 +34800,29 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-react-x@1.53.1(eslint@9.22.0(jiti@2.6.1))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 1.53.1 + '@eslint-react/kit': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/shared': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 1.53.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + compare-versions: 6.1.1 + eslint: 9.22.0(jiti@2.6.1) + is-immutable-type: 5.0.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) + string-ts: 2.2.1 + ts-pattern: 5.9.0 + optionalDependencies: + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + eslint-plugin-solid@0.14.5(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.44.1(eslint@9.22.0(jiti@2.6.1))(typescript@5.9.3) @@ -34949,7 +35524,7 @@ snapshots: optionalDependencies: crossws: 0.4.3(srvx@0.10.1) - h3@2.0.1-rc.14(crossws@0.4.3(srvx@0.11.9)): + h3@2.0.1-rc.14(crossws@0.4.3(srvx@0.10.1)): dependencies: rou3: 0.7.12 srvx: 0.11.9 @@ -34999,6 +35574,12 @@ snapshots: headers-polyfill@4.0.3: {} + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + hey-listen@1.0.8: {} highlight.js@10.7.3: {} @@ -35714,6 +36295,11 @@ snapshots: dotenv: 16.6.1 winston: 3.18.3 + launch-editor@2.13.1: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + launch-editor@2.9.1: dependencies: picocolors: 1.1.1 @@ -36374,7 +36960,7 @@ snapshots: consola: 3.4.2 crossws: 0.4.3(srvx@0.10.1) db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) - h3: 2.0.1-rc.14(crossws@0.4.3(srvx@0.11.9)) + h3: 2.0.1-rc.14(crossws@0.4.3(srvx@0.10.1)) jiti: 2.6.1 nf3: 0.3.6 ofetch: 2.0.0-alpha.3 @@ -38679,6 +39265,8 @@ snapshots: ts-pattern@5.6.2: {} + ts-pattern@5.9.0: {} + tsconfck@3.1.4(typescript@5.8.2): optionalDependencies: typescript: 5.8.2 @@ -39289,7 +39877,7 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) - vitest@3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17(vitest@4.0.17))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + vitest@3.2.4(@types/node@25.0.9)(@vitest/browser@4.0.17(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@4.0.17))(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -39977,6 +40565,10 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 + zod-validation-error@3.5.4(zod@3.25.57): + dependencies: + zod: 3.25.57 + zod@3.22.3: {} zod@3.25.57: {}