diff --git a/assets/CleanShot_2025-11-27_at_08.28.46_2x-4a965c47-7c20-4a7c-acd5-317a2c7876cd.png b/assets/CleanShot_2025-11-27_at_08.28.46_2x-4a965c47-7c20-4a7c-acd5-317a2c7876cd.png new file mode 100644 index 000000000..e69de29bb diff --git a/knip.json b/knip.json index 2a761388c..323240d7d 100644 --- a/knip.json +++ b/knip.json @@ -1,10 +1,31 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "ignoreDependencies": ["@faker-js/faker"], + "ignoreDependencies": ["@faker-js/faker", "@playwright/test"], "ignoreWorkspaces": ["examples/**"], + "ignore": [ + "packages/typescript/ai-openai/live-tests/**", + "packages/typescript/ai-openai/src/**/*.test.ts", + "packages/typescript/ai-openai/src/audio/audio-provider-options.ts", + "packages/typescript/ai-openai/src/audio/transcribe-provider-options.ts", + "packages/typescript/ai-openai/src/image/image-provider-options.ts", + "packages/typescript/smoke-tests/adapters/src/**", + "packages/typescript/smoke-tests/e2e/playwright.config.ts", + "packages/typescript/smoke-tests/e2e/src/**", + "packages/typescript/smoke-tests/e2e/vite.config.ts" + ], + "ignoreExportsUsedInFile": true, "workspaces": { "packages/react-ai": { "ignore": [] + }, + "packages/typescript/ai-anthropic": { + "ignore": ["src/tools/**"] + }, + "packages/typescript/ai-gemini": { + "ignore": ["src/tools/**"] + }, + "packages/typescript/ai-openai": { + "ignore": ["src/tools/**"] } } } diff --git a/package.json b/package.json index 862b2c3ab..744064820 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +", "clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +", "clean:all": "pnpm run clean && pnpm run clean:node_modules", - "copy:readme": "cp README.md packages/typescript/ai/README.md && cp README.md packages/typescript/ai-devtools/README.md && cp README.md packages/typescript/ai-client/README.md && cp README.md packages/typescript/ai-gemini/README.md && cp README.md packages/typescript/ai-ollama/README.md && cp README.md packages/typescript/ai-openai/README.md && cp README.md packages/typescript/ai-react/README.md && cp README.md packages/typescript/ai-react-ui/README.md && cp README.md packages/typescript/react-ai-devtools/README.md && cp README.md packages/typescript/solid-ai-devtools/README.md && cp README.md packages/typescript/tests-adapters/README.md", + "copy:readme": "cp README.md packages/typescript/ai/README.md && cp README.md packages/typescript/ai-devtools/README.md && cp README.md packages/typescript/ai-client/README.md && cp README.md packages/typescript/ai-gemini/README.md && cp README.md packages/typescript/ai-ollama/README.md && cp README.md packages/typescript/ai-openai/README.md && cp README.md packages/typescript/ai-react/README.md && cp README.md packages/typescript/ai-react-ui/README.md && cp README.md packages/typescript/react-ai-devtools/README.md && cp README.md packages/typescript/solid-ai-devtools/README.md", "dev": "pnpm run watch", "docs:generate": "node scripts/generateDocs.js && pnpm run copy:readme", "format": "pnpm run prettier:write", diff --git a/packages/typescript/ai-openai/live-tests/README.md b/packages/typescript/ai-openai/live-tests/README.md new file mode 100644 index 000000000..9da04c1ec --- /dev/null +++ b/packages/typescript/ai-openai/live-tests/README.md @@ -0,0 +1,59 @@ +# OpenAI Adapter Live Tests + +This directory contains live integration tests for the OpenAI adapter using the Responses API. + +## Setup + +1. Create a `.env.local` file in this directory with your OpenAI API key: + +``` +OPENAI_API_KEY=sk-... +``` + +2. Install dependencies from the workspace root: + +```bash +pnpm install +``` + +## Running Tests + +Run individual tests: + +```bash +pnpm test # Test with required parameters +pnpm test:optional # Test with optional parameters +``` + +Run all tests: + +```bash +pnpm test:all +``` + +## Test Scripts + +### `tool-test.ts` + +Tests tool calling with all required parameters. Verifies that: + +- Tool calls are properly detected in the stream +- Function names are correctly captured +- Arguments are passed as JSON strings +- Tools can be executed with the parsed arguments + +### `tool-test-optional.ts` + +Tests tool calling with optional parameters. Verifies that: + +- Tools with optional parameters work correctly +- The strict mode is disabled when not all parameters are required +- Default values can be applied for missing optional parameters + +## Key Findings + +The OpenAI Responses API has different behavior compared to the Chat Completions API: + +1. **Strict Mode**: When `strict: true`, ALL properties must be in the `required` array +2. **Tool Metadata**: Function names come from `response.output_item.added` events, not from `response.function_call_arguments.done` +3. **Finish Reason**: The Responses API doesn't have a `finish_reason` field; it must be inferred from the output content diff --git a/packages/typescript/ai-openai/live-tests/package.json b/packages/typescript/ai-openai/live-tests/package.json new file mode 100644 index 000000000..0df34e8bb --- /dev/null +++ b/packages/typescript/ai-openai/live-tests/package.json @@ -0,0 +1,18 @@ +{ + "name": "ai-openai-live-tests", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "tsx tool-test.ts", + "test:optional": "tsx tool-test-optional.ts", + "test:all": "tsx tool-test.ts && tsx tool-test-optional.ts" + }, + "dependencies": { + "@tanstack/ai": "workspace:*", + "@tanstack/ai-openai": "workspace:*" + }, + "devDependencies": { + "tsx": "^4.19.2" + } +} diff --git a/packages/typescript/ai-openai/live-tests/tool-test-optional.ts b/packages/typescript/ai-openai/live-tests/tool-test-optional.ts new file mode 100644 index 000000000..36a59f624 --- /dev/null +++ b/packages/typescript/ai-openai/live-tests/tool-test-optional.ts @@ -0,0 +1,182 @@ +import { createOpenAI } from '../src/index' +import { readFileSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +// Load environment variables from .env.local manually +const __dirname = dirname(fileURLToPath(import.meta.url)) +try { + const envContent = readFileSync(join(__dirname, '.env.local'), 'utf-8') + envContent.split('\n').forEach((line) => { + const match = line.match(/^([^=]+)=(.*)$/) + if (match) { + process.env[match[1].trim()] = match[2].trim() + } + }) +} catch (e) { + // .env.local not found, will use process.env +} + +const apiKey = process.env.OPENAI_API_KEY + +if (!apiKey) { + console.error('āŒ OPENAI_API_KEY not found in .env.local') + process.exit(1) +} + +async function testToolWithOptionalParameters() { + console.log('šŸš€ Testing OpenAI tool calling with OPTIONAL parameters\n') + + const adapter = createOpenAI(apiKey) + + // Create a tool with optional parameters (unit is optional) + const getTemperatureTool = { + type: 'function' as const, + function: { + name: 'get_temperature', + description: 'Get the current temperature for a specific location', + parameters: { + type: 'object', + properties: { + location: { + type: 'string', + description: 'The city or location to get the temperature for', + }, + unit: { + type: 'string', + enum: ['celsius', 'fahrenheit'], + description: + 'The temperature unit (optional, defaults to fahrenheit)', + }, + }, + required: ['location'], // unit is optional + }, + }, + execute: async (args: any) => { + console.log( + 'āœ… Tool executed with arguments:', + JSON.stringify(args, null, 2), + ) + + if (!args || !args.location) { + console.error('āŒ ERROR: Location argument is missing!') + return 'Error: Location is required' + } + + const unit = args.unit || 'fahrenheit' + console.log(` - location: "${args.location}"`) + console.log( + ` - unit: "${unit}" (${args.unit ? 'provided' : 'defaulted'})`, + ) + + return `The temperature in ${args.location} is 72°${unit === 'celsius' ? 'C' : 'F'}` + }, + } + + const messages = [ + { + role: 'user' as const, + content: + 'What is the temperature in Paris? Use the get_temperature tool.', + }, + ] + + console.log('šŸ“¤ Sending request with tool:') + console.log(' Tool name:', getTemperatureTool.function.name) + console.log( + ' Required params:', + getTemperatureTool.function.parameters.required, + ) + console.log(' Optional params:', ['unit']) + console.log(' User message:', messages[0].content) + console.log() + + try { + console.log('šŸ“„ Streaming response...\n') + + let toolCallFound = false + let toolCallArguments: any = null + let toolExecuted = false + let finalResponse = '' + + // @ts-ignore - using internal chat method + const stream = adapter.chatStream({ + model: 'gpt-4o-mini', + messages, + tools: [getTemperatureTool], + }) + + for await (const chunk of stream) { + if (chunk.type === 'tool_call') { + toolCallFound = true + toolCallArguments = chunk.toolCall.function.arguments + console.log('šŸ”§ Tool call detected!') + console.log(' Name:', chunk.toolCall.function.name) + console.log(' Arguments (raw):', toolCallArguments) + + // Parse if it's a string + if (typeof toolCallArguments === 'string') { + try { + const parsed = JSON.parse(toolCallArguments) + toolCallArguments = parsed + } catch (e) { + console.error(' āŒ Failed to parse arguments:', e) + } + } + + // Execute the tool + if (getTemperatureTool.execute) { + console.log('\nšŸ”Ø Executing tool...') + try { + const result = await getTemperatureTool.execute(toolCallArguments) + toolExecuted = true + console.log(' Result:', result) + } catch (error) { + console.error(' āŒ Tool execution error:', error) + } + } + } + + if (chunk.type === 'content') { + finalResponse += chunk.delta + } + } + + console.log('\n' + '='.repeat(60)) + console.log('šŸ“Š Test Summary:') + console.log(' Tool call found:', toolCallFound ? 'āœ…' : 'āŒ') + console.log(' Arguments received:', toolCallArguments ? 'āœ…' : 'āŒ') + console.log(' Tool executed:', toolExecuted ? 'āœ…' : 'āŒ') + console.log( + ' Location provided:', + toolCallArguments?.location ? 'āœ…' : 'āŒ', + ) + console.log('='.repeat(60)) + + if (!toolCallFound) { + console.error('\nāŒ FAIL: No tool call was detected') + process.exit(1) + } + + if (!toolCallArguments || !toolCallArguments.location) { + console.error('\nāŒ FAIL: Tool arguments missing or invalid') + process.exit(1) + } + + if (!toolExecuted) { + console.error('\nāŒ FAIL: Tool was not executed successfully') + process.exit(1) + } + + console.log( + '\nāœ… SUCCESS: Tool calling with optional parameters works correctly!', + ) + process.exit(0) + } catch (error: any) { + console.error('\nāŒ ERROR:', error.message) + console.error('Stack:', error.stack) + process.exit(1) + } +} + +testToolWithOptionalParameters() diff --git a/packages/typescript/ai-openai/live-tests/tool-test.ts b/packages/typescript/ai-openai/live-tests/tool-test.ts new file mode 100644 index 000000000..f8e4bb0f3 --- /dev/null +++ b/packages/typescript/ai-openai/live-tests/tool-test.ts @@ -0,0 +1,198 @@ +import { createOpenAI } from '../src/index' +import { readFileSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +// Load environment variables from .env.local manually +const __dirname = dirname(fileURLToPath(import.meta.url)) +try { + const envContent = readFileSync(join(__dirname, '.env.local'), 'utf-8') + envContent.split('\n').forEach((line) => { + const match = line.match(/^([^=]+)=(.*)$/) + if (match) { + process.env[match[1].trim()] = match[2].trim() + } + }) +} catch (e) { + // .env.local not found, will use process.env +} + +const apiKey = process.env.OPENAI_API_KEY + +if (!apiKey) { + console.error('āŒ OPENAI_API_KEY not found in .env.local') + process.exit(1) +} + +async function testToolCallingWithArguments() { + console.log('šŸš€ Testing OpenAI tool calling with arguments (Responses API)\n') + + const adapter = createOpenAI(apiKey) + + // Create a simple tool that requires arguments + // Note: Using strict mode which requires ALL properties to be in required array + const getTemperatureTool = { + type: 'function' as const, + function: { + name: 'get_temperature', + description: 'Get the current temperature for a specific location', + parameters: { + type: 'object', + properties: { + location: { + type: 'string', + description: 'The city or location to get the temperature for', + }, + unit: { + type: 'string', + enum: ['celsius', 'fahrenheit'], + description: 'The temperature unit', + }, + }, + required: ['location', 'unit'], // strict mode requires ALL properties in required + }, + }, + execute: async (args: any) => { + console.log( + 'āœ… Tool executed with arguments:', + JSON.stringify(args, null, 2), + ) + + // Validate arguments were passed correctly + if (!args) { + console.error('āŒ ERROR: Arguments are undefined!') + return 'Error: No arguments received' + } + + if (typeof args !== 'object') { + console.error('āŒ ERROR: Arguments are not an object:', typeof args) + return 'Error: Invalid arguments type' + } + + if (!args.location) { + console.error('āŒ ERROR: Location argument is missing!') + return 'Error: Location is required' + } + + console.log( + ` - location: "${args.location}" (type: ${typeof args.location})`, + ) + console.log(` - unit: "${args.unit}" (type: ${typeof args.unit})`) + + return `The temperature in ${args.location} is 72°${args.unit === 'celsius' ? 'C' : 'F'}` + }, + } + + const messages = [ + { + role: 'user' as const, + content: 'What is the temperature in San Francisco in fahrenheit?', + }, + ] + + console.log('šŸ“¤ Sending request with tool:') + console.log(' Tool name:', getTemperatureTool.function.name) + console.log( + ' Required params:', + getTemperatureTool.function.parameters.required, + ) + console.log(' User message:', messages[0].content) + console.log() + + try { + console.log('šŸ“„ Streaming response...\n') + + let toolCallFound = false + let toolCallArguments: any = null + let toolExecuted = false + let finalResponse = '' + + // @ts-ignore - using internal chat method + const stream = adapter.chatStream({ + model: 'gpt-4o-mini', + messages, + tools: [getTemperatureTool], + }) + + for await (const chunk of stream) { + console.log('Chunk:', JSON.stringify(chunk, null, 2)) + + if (chunk.type === 'tool_call') { + toolCallFound = true + toolCallArguments = chunk.toolCall.function.arguments + console.log('\nšŸ”§ Tool call detected!') + console.log(' Name:', chunk.toolCall.function.name) + console.log(' Arguments (raw):', toolCallArguments) + console.log(' Arguments (type):', typeof toolCallArguments) + + // Try to parse if it's a string + if (typeof toolCallArguments === 'string') { + try { + const parsed = JSON.parse(toolCallArguments) + console.log( + ' Arguments (parsed):', + JSON.stringify(parsed, null, 2), + ) + toolCallArguments = parsed + } catch (e) { + console.error(' āŒ Failed to parse arguments as JSON:', e) + } + } + + // Execute the tool + if (getTemperatureTool.execute) { + console.log('\nšŸ”Ø Executing tool...') + try { + const result = await getTemperatureTool.execute(toolCallArguments) + toolExecuted = true + console.log(' Result:', result) + } catch (error) { + console.error(' āŒ Tool execution error:', error) + } + } + } + + if (chunk.type === 'content') { + finalResponse += chunk.delta + } + } + + console.log('\n' + '='.repeat(60)) + console.log('šŸ“Š Test Summary:') + console.log(' Tool call found:', toolCallFound ? 'āœ…' : 'āŒ') + console.log(' Arguments received:', toolCallArguments ? 'āœ…' : 'āŒ') + console.log(' Arguments value:', JSON.stringify(toolCallArguments)) + console.log(' Tool executed:', toolExecuted ? 'āœ…' : 'āŒ') + console.log(' Final response:', finalResponse) + console.log('='.repeat(60)) + + if (!toolCallFound) { + console.error('\nāŒ FAIL: No tool call was detected in the stream') + process.exit(1) + } + + if (!toolCallArguments) { + console.error('\nāŒ FAIL: Tool call arguments are missing or null') + process.exit(1) + } + + if (typeof toolCallArguments === 'object' && !toolCallArguments.location) { + console.error('\nāŒ FAIL: Location parameter is missing from arguments') + process.exit(1) + } + + if (!toolExecuted) { + console.error('\nāŒ FAIL: Tool was not executed successfully') + process.exit(1) + } + + console.log('\nāœ… SUCCESS: Tool calling with arguments works correctly!') + process.exit(0) + } catch (error: any) { + console.error('\nāŒ ERROR:', error.message) + console.error('Stack:', error.stack) + process.exit(1) + } +} + +testToolCallingWithArguments() diff --git a/packages/typescript/ai-openai/src/openai-adapter.ts b/packages/typescript/ai-openai/src/openai-adapter.ts index ef2dc4fc7..2afe20493 100644 --- a/packages/typescript/ai-openai/src/openai-adapter.ts +++ b/packages/typescript/ai-openai/src/openai-adapter.ts @@ -189,26 +189,16 @@ export class OpenAI extends BaseAdapter< let accumulatedContent = '' let accumulatedReasoning = '' const timestamp = Date.now() - // let nextIndex = 0 let chunkCount = 0 - // Track accumulated function call arguments by call_id - const accumulatedFunctionCallArguments = new Map() - - // Map item_id (from delta events) to call_id (from function_call items) - // const itemIdToCallId = new Map() - // Preserve response metadata across events let responseId: string | null = null let model: string = options.model const eventTypeCounts = new Map() - // Track which item indices are reasoning items - // const reasoningItemIndices = new Set() try { for await (const chunk of stream) { - console.log(chunk) chunkCount++ const handleContentPart = ( contentPart: @@ -291,15 +281,27 @@ export class OpenAI extends BaseAdapter< yield handleContentPart(contentPart) } - if (chunk.type === 'response.function_call_arguments.done') { - const { name, item_id, output_index } = chunk - if (!toolCallMetadata.has(item_id)) { - toolCallMetadata.set(item_id, { - index: output_index, - name: name, - }) - accumulatedFunctionCallArguments.set(item_id, '') + // handle output_item.added to capture function call metadata (name) + if (chunk.type === 'response.output_item.added') { + const item = chunk.item + if (item.type === 'function_call' && item.id) { + // Store the function name for later use + if (!toolCallMetadata.has(item.id)) { + toolCallMetadata.set(item.id, { + index: chunk.output_index, + name: item.name || '', + }) + } } + } + + if (chunk.type === 'response.function_call_arguments.done') { + const { item_id, output_index } = chunk + + // Get the function name from metadata (captured in output_item.added) + const metadata = toolCallMetadata.get(item_id) + const name = metadata?.name || '' + yield { type: 'tool_call', id: responseId || generateId(), @@ -328,12 +330,18 @@ export class OpenAI extends BaseAdapter< } if (chunk.type === 'response.completed') { + // Determine finish reason based on output + // If there are function_call items in the output, it's a tool_calls finish + const hasFunctionCalls = chunk.response.output.some( + (item: any) => item.type === 'function_call', + ) + yield { type: 'done', id: responseId || generateId(), model: model || options.model, timestamp, - finishReason: 'stop', + finishReason: hasFunctionCalls ? 'tool_calls' : 'stop', } } diff --git a/packages/typescript/ai-openai/src/tools/function-tool.ts b/packages/typescript/ai-openai/src/tools/function-tool.ts index d7e324cef..006324f95 100644 --- a/packages/typescript/ai-openai/src/tools/function-tool.ts +++ b/packages/typescript/ai-openai/src/tools/function-tool.ts @@ -18,6 +18,20 @@ export function convertFunctionToolToAdapterFormat(tool: Tool): FunctionTool { // Otherwise, convert directly from tool.function (regular Tool structure) // For Responses API, FunctionTool has name at top level, with function containing description and parameters + + // Determine if we can use strict mode + // Strict mode requires all properties to be in the required array + const parameters = tool.function.parameters + const properties = parameters.properties || {} + const required = parameters.required || [] + const propertyNames = Object.keys(properties) + + // Only enable strict mode if all properties are required + // This ensures compatibility with tools that have optional parameters + const canUseStrict = + propertyNames.length > 0 && + propertyNames.every((prop) => required.includes(prop)) + return { type: 'function', name: tool.function.name, @@ -26,8 +40,7 @@ export function convertFunctionToolToAdapterFormat(tool: Tool): FunctionTool { ...tool.function.parameters, additionalProperties: false, }, - - strict: true, + strict: canUseStrict, } satisfies FunctionTool } diff --git a/packages/typescript/tests-adapters/README.md b/packages/typescript/smoke-tests/adapters/README.md similarity index 100% rename from packages/typescript/tests-adapters/README.md rename to packages/typescript/smoke-tests/adapters/README.md diff --git a/packages/typescript/tests-adapters/env.example b/packages/typescript/smoke-tests/adapters/env.example similarity index 100% rename from packages/typescript/tests-adapters/env.example rename to packages/typescript/smoke-tests/adapters/env.example diff --git a/packages/typescript/tests-adapters/package.json b/packages/typescript/smoke-tests/adapters/package.json similarity index 100% rename from packages/typescript/tests-adapters/package.json rename to packages/typescript/smoke-tests/adapters/package.json diff --git a/packages/typescript/tests-adapters/src/harness.ts b/packages/typescript/smoke-tests/adapters/src/harness.ts similarity index 100% rename from packages/typescript/tests-adapters/src/harness.ts rename to packages/typescript/smoke-tests/adapters/src/harness.ts diff --git a/packages/typescript/tests-adapters/src/index.ts b/packages/typescript/smoke-tests/adapters/src/index.ts similarity index 100% rename from packages/typescript/tests-adapters/src/index.ts rename to packages/typescript/smoke-tests/adapters/src/index.ts diff --git a/packages/typescript/tests-adapters/tsconfig.json b/packages/typescript/smoke-tests/adapters/tsconfig.json similarity index 100% rename from packages/typescript/tests-adapters/tsconfig.json rename to packages/typescript/smoke-tests/adapters/tsconfig.json diff --git a/packages/typescript/smoke-tests/e2e/.env.example b/packages/typescript/smoke-tests/e2e/.env.example new file mode 100644 index 000000000..3cac181df --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY=your_openai_api_key_here diff --git a/packages/typescript/smoke-tests/e2e/.gitignore b/packages/typescript/smoke-tests/e2e/.gitignore new file mode 100644 index 000000000..ed14b2afd --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/.gitignore @@ -0,0 +1,9 @@ +node_modules +.env +.DS_Store +dist +.vercel +.route +routeTree.gen.ts +test-results/ +playwright-report/ diff --git a/packages/typescript/smoke-tests/e2e/README.md b/packages/typescript/smoke-tests/e2e/README.md new file mode 100644 index 000000000..74d159413 --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/README.md @@ -0,0 +1,65 @@ +# TanStack AI E2E Tests + +End-to-end tests for TanStack AI chat functionality using Playwright. + +## Setup + +1. Install dependencies: + +```bash +pnpm install +``` + +2. Install Playwright browsers (required for running tests): + +```bash +pnpm exec playwright install --with-deps chromium +``` + +Note: This is also run automatically via the `postinstall` script, but you may need to run it manually if the browsers weren't installed correctly. + +3. Copy `.env.example` to `.env` and add your OpenAI API key: + +```bash +cp .env.example .env +``` + +4. Edit `.env` and add your `OPENAI_API_KEY`. + +## Running Tests + +Run the tests: + +```bash +pnpm test:e2e +``` + +Run tests with UI: + +```bash +pnpm test:e2e:ui +``` + +## Development + +Start the dev server: + +```bash +pnpm dev +``` + +The app will be available at `http://localhost:3100`. + +## Test Structure + +The tests use a simplified chat interface with: + +- Input field +- Submit button +- JSON messages display + +Tests verify that: + +1. The LLM responds correctly to prompts +2. Conversation context is maintained across multiple messages +3. Messages are properly structured in the JSON format diff --git a/packages/typescript/smoke-tests/e2e/package.json b/packages/typescript/smoke-tests/e2e/package.json new file mode 100644 index 000000000..17f705df7 --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/package.json @@ -0,0 +1,42 @@ +{ + "name": "@tanstack/smoke-tests-e2e", + "version": "0.1.0", + "description": "E2E tests for TanStack AI chat", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 3100", + "build": "vite build", + "serve": "vite preview", + "test:e2e": "npx playwright test", + "test:e2e:ui": "npx playwright test --ui", + "postinstall": "npx playwright install --with-deps chromium" + }, + "dependencies": { + "@ai-sdk/openai": "^2.0.52", + "@ai-sdk/provider": "^2.0.0", + "@ai-sdk/provider-utils": "^3.0.12", + "@tailwindcss/vite": "^4.0.6", + "@tanstack/ai": "workspace:*", + "@tanstack/ai-client": "workspace:*", + "@tanstack/ai-openai": "workspace:*", + "@tanstack/ai-react": "workspace:*", + "@tanstack/nitro-v2-vite-plugin": "^1.132.31", + "@tanstack/react-router": "^1.132.0", + "@tanstack/react-start": "^1.132.0", + "@tanstack/router-plugin": "^1.132.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.0.6", + "vite-tsconfig-paths": "^5.1.4" + }, + "devDependencies": { + "@playwright/test": "^1.57.0", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/packages/typescript/smoke-tests/e2e/playwright.config.ts b/packages/typescript/smoke-tests/e2e/playwright.config.ts new file mode 100644 index 000000000..1e8a27631 --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3100', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'pnpm run dev', + url: 'http://localhost:3100', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}) diff --git a/packages/typescript/smoke-tests/e2e/src/router.tsx b/packages/typescript/smoke-tests/e2e/src/router.tsx new file mode 100644 index 000000000..ee1edab88 --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/src/router.tsx @@ -0,0 +1,13 @@ +import { createRouter } from '@tanstack/react-router' + +// Import the generated route tree +import { routeTree } from './routeTree.gen' + +// Create a new router instance +export const getRouter = () => { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPreloadStaleTime: 0, + }) +} diff --git a/packages/typescript/smoke-tests/e2e/src/routes/__root.tsx b/packages/typescript/smoke-tests/e2e/src/routes/__root.tsx new file mode 100644 index 000000000..a427f536c --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/src/routes/__root.tsx @@ -0,0 +1,43 @@ +import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' +import appCss from '../styles.css?url' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'TanStack AI E2E Test', + }, + ], + links: [ + { + rel: 'stylesheet', + href: appCss, + }, + ], + }), + + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + + {children} + + + + ) +} diff --git a/packages/typescript/smoke-tests/e2e/src/routes/api.tanchat.ts b/packages/typescript/smoke-tests/e2e/src/routes/api.tanchat.ts new file mode 100644 index 000000000..a7ea69a77 --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/src/routes/api.tanchat.ts @@ -0,0 +1,58 @@ +import { createFileRoute } from '@tanstack/react-router' +import { chat, toStreamResponse, maxIterations } from '@tanstack/ai' +import { openai } from '@tanstack/ai-openai' + +export const Route = createFileRoute('/api/tanchat')({ + server: { + handlers: { + POST: async ({ request }) => { + const requestSignal = request.signal + + if (requestSignal?.aborted) { + return new Response(null, { status: 499 }) + } + + const abortController = new AbortController() + + const { messages } = await request.json() + try { + const stream = chat({ + adapter: openai(), + model: 'gpt-4o-mini', + systemPrompts: [ + 'You are a helpful assistant. Provide clear and concise answers.', + ], + agentLoopStrategy: maxIterations(20), + messages, + abortController, + }) + + return toStreamResponse(stream, { abortController }) + } catch (error: any) { + console.error('[API Route] Error in chat request:', { + message: error?.message, + name: error?.name, + status: error?.status, + statusText: error?.statusText, + code: error?.code, + type: error?.type, + stack: error?.stack, + error: error, + }) + if (error.name === 'AbortError' || abortController.signal.aborted) { + return new Response(null, { status: 499 }) + } + return new Response( + JSON.stringify({ + error: error.message || 'An error occurred', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ) + } + }, + }, + }, +}) diff --git a/packages/typescript/smoke-tests/e2e/src/routes/index.tsx b/packages/typescript/smoke-tests/e2e/src/routes/index.tsx new file mode 100644 index 000000000..57cd2b79d --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/src/routes/index.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { useChat, fetchServerSentEvents } from '@tanstack/ai-react' + +function ChatPage() { + const { messages, sendMessage, isLoading, stop } = useChat({ + connection: fetchServerSentEvents('/api/tanchat'), + }) + const [input, setInput] = useState('') + + return ( +
+ {/* Input area */} +
+ setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && input.trim() && !isLoading) { + sendMessage(input) + setInput('') + } + }} + placeholder="Type a message..." + disabled={isLoading} + style={{ + flex: 1, + padding: '10px', + fontSize: '14px', + border: '1px solid #ccc', + borderRadius: '4px', + }} + /> + + {isLoading && ( + + )} +
+ + {/* JSON Messages Display */} +
+
+          {JSON.stringify(messages, null, 2)}
+        
+
+
+ ) +} + +export const Route = createFileRoute('/')({ + component: ChatPage, +}) diff --git a/packages/typescript/smoke-tests/e2e/src/styles.css b/packages/typescript/smoke-tests/e2e/src/styles.css new file mode 100644 index 000000000..da493591c --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/src/styles.css @@ -0,0 +1,9 @@ +@import 'tailwindcss'; + +body { + margin: 0; + font-family: + system-ui, + -apple-system, + sans-serif; +} diff --git a/packages/typescript/smoke-tests/e2e/tests/chat.spec.ts b/packages/typescript/smoke-tests/e2e/tests/chat.spec.ts new file mode 100644 index 000000000..73298f9fa --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/tests/chat.spec.ts @@ -0,0 +1,193 @@ +import { test, expect } from '@playwright/test' + +test.describe('Chat E2E Tests', () => { + test('should handle two-prompt conversation with context', async ({ + page, + }) => { + await page.goto('/') + + // Take screenshot after navigation + await page.screenshot({ + path: 'test-results/01-after-navigation.png', + fullPage: true, + }) + + // Wait for the page to load with timeout and screenshot on failure + try { + await page.waitForSelector('#chat-input', { timeout: 10000 }) + } catch (error) { + await page.screenshot({ + path: 'test-results/02-wait-for-input-failed.png', + fullPage: true, + }) + console.log('Page content:', await page.content()) + console.log('Page URL:', page.url()) + throw error + } + + // Take screenshot after input is found + await page.screenshot({ + path: 'test-results/03-input-found.png', + fullPage: true, + }) + + // First prompt: Ask about the capital of France + const input = page.locator('#chat-input') + const submitButton = page.locator('#submit-button') + const messagesJson = page.locator('#messages-json-content') + + // Clear input and type with delay to trigger React events properly + await input.clear() + await input.pressSequentially('What is the capital of France?', { + delay: 50, + }) + // Small wait for React state to sync + await page.waitForTimeout(100) + // Click button (more reliable than Enter key) + await submitButton.click() + + // Take screenshot after submitting first message + await page.screenshot({ + path: 'test-results/04-first-message-sent.png', + fullPage: true, + }) + + // Wait for the response to appear in the JSON and verify Paris is in it + await page.waitForFunction( + () => { + const preElement = document.querySelector('#messages-json-content') + if (!preElement) return false + try { + const messages = JSON.parse(preElement.textContent || '[]') + const assistantMessages = messages.filter( + (m: any) => m.role === 'assistant', + ) + if (assistantMessages.length > 0) { + const lastMessage = assistantMessages[assistantMessages.length - 1] + const textParts = lastMessage.parts.filter( + (p: any) => p.type === 'text' && p.content, + ) + if (textParts.length > 0) { + const content = textParts.map((p: any) => p.content).join(' ') + return content.toLowerCase().includes('paris') + } + } + return false + } catch { + return false + } + }, + { timeout: 60000 }, + ) + + // Verify Paris is in the response + const messagesText1 = await messagesJson.textContent() + const messages1 = JSON.parse(messagesText1 || '[]') + const assistantMessage1 = messages1 + .filter((m: any) => m.role === 'assistant') + .pop() + const textContent1 = assistantMessage1.parts + .filter((p: any) => p.type === 'text' && p.content) + .map((p: any) => p.content) + .join(' ') + .toLowerCase() + + expect(textContent1).toContain('paris') + + // Take screenshot after first response received + await page.screenshot({ + path: 'test-results/05-first-response-received.png', + fullPage: true, + }) + + // Second prompt: Follow-up question about population + // Wait for loading to complete (isLoading becomes false) + await page.waitForFunction( + () => { + const button = document.querySelector( + '#submit-button', + ) as HTMLButtonElement + const isLoading = button?.getAttribute('data-is-loading') === 'true' + return button && !isLoading + }, + { timeout: 30000 }, + ) + // Clear input and type with delay to trigger React events properly + await input.clear() + await input.pressSequentially('What is the population of that city?', { + delay: 50, + }) + // Small wait for React state to sync + await page.waitForTimeout(100) + // Click button (more reliable than Enter key) + await submitButton.click() + + // Take screenshot after submitting second message + await page.screenshot({ + path: 'test-results/06-second-message-sent.png', + fullPage: true, + }) + + // Wait for the response to appear in the JSON and verify "million" is in it + await page.waitForFunction( + () => { + const preElement = document.querySelector('#messages-json-content') + if (!preElement) return false + try { + const messages = JSON.parse(preElement.textContent || '[]') + const assistantMessages = messages.filter( + (m: any) => m.role === 'assistant', + ) + // Should have at least 2 assistant messages now + if (assistantMessages.length >= 2) { + const lastMessage = assistantMessages[assistantMessages.length - 1] + const textParts = lastMessage.parts.filter( + (p: any) => p.type === 'text' && p.content, + ) + if (textParts.length > 0) { + const content = textParts.map((p: any) => p.content).join(' ') + return content.toLowerCase().includes('million') + } + } + return false + } catch { + return false + } + }, + { timeout: 60000 }, + ) + + // Verify "million" is in the response (indicating context was maintained) + const messagesText2 = await messagesJson.textContent() + const messages2 = JSON.parse(messagesText2 || '[]') + const assistantMessage2 = messages2 + .filter((m: any) => m.role === 'assistant') + .pop() + const textContent2 = assistantMessage2.parts + .filter((p: any) => p.type === 'text' && p.content) + .map((p: any) => p.content) + .join(' ') + .toLowerCase() + + expect(textContent2).toContain('million') + + // Verify we have the full conversation context + expect(messages2.length).toBeGreaterThanOrEqual(4) // At least 2 user + 2 assistant messages + + // Take final screenshot + await page.screenshot({ + path: 'test-results/07-test-complete.png', + fullPage: true, + }) + }) + + // Add a hook to take screenshot on test failure + test.afterEach(async ({ page }, testInfo) => { + if (testInfo.status !== testInfo.expectedStatus) { + await page.screenshot({ + path: `test-results/failure-${testInfo.title.replace(/\s+/g, '-')}.png`, + fullPage: true, + }) + } + }) +}) diff --git a/packages/typescript/smoke-tests/e2e/tsconfig.json b/packages/typescript/smoke-tests/e2e/tsconfig.json new file mode 100644 index 000000000..c7f7d55fc --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/tsconfig.json @@ -0,0 +1,28 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@playwright/test"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/typescript/smoke-tests/e2e/vite.config.ts b/packages/typescript/smoke-tests/e2e/vite.config.ts new file mode 100644 index 000000000..734b135c8 --- /dev/null +++ b/packages/typescript/smoke-tests/e2e/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' +import viteTsConfigPaths from 'vite-tsconfig-paths' +import tailwindcss from '@tailwindcss/vite' +import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin' + +const config = defineConfig({ + plugins: [ + nitroV2Plugin(), + viteTsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tailwindcss(), + tanstackStart(), + viteReact(), + ], +}) + +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 577d221cf..06198bfba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,10 +128,10 @@ importers: version: 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-router-devtools': specifier: ^1.139.7 - version: 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.7)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) '@tanstack/react-router-ssr-query': specifier: ^1.139.7 - version: 1.139.7(@tanstack/query-core@5.90.5)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.139.7(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-start': specifier: ^1.139.8 version: 1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -140,7 +140,7 @@ importers: version: 0.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/router-plugin': specifier: ^1.139.7 - version: 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@tanstack/store': specifier: ^0.8.0 version: 0.8.0 @@ -246,10 +246,10 @@ importers: version: 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-router-devtools': specifier: ^1.139.7 - version: 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.7)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) '@tanstack/react-router-ssr-query': specifier: ^1.139.7 - version: 1.139.7(@tanstack/query-core@5.90.5)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.139.7(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-start': specifier: ^1.139.8 version: 1.139.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -533,45 +533,23 @@ importers: specifier: ^7.2.4 version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - packages/typescript/solid-ai-devtools: - dependencies: - '@tanstack/ai-devtools-core': - specifier: workspace:* - version: link:../ai-devtools - '@tanstack/devtools-utils': - specifier: ^0.0.8 - version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.0)(solid-js@1.9.10) - solid-js: - specifier: '>=1.9.7' - version: 1.9.10 - devDependencies: - '@vitest/coverage-v8': - specifier: 4.0.14 - version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - vite: - specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-solid: - specifier: ^2.11.10 - version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - - packages/typescript/tests-adapters: + packages/typescript/smoke-tests/adapters: dependencies: '@tanstack/ai': specifier: workspace:* - version: link:../ai + version: link:../../ai '@tanstack/ai-anthropic': specifier: workspace:* - version: link:../ai-anthropic + version: link:../../ai-anthropic '@tanstack/ai-gemini': specifier: workspace:* - version: link:../ai-gemini + version: link:../../ai-gemini '@tanstack/ai-ollama': specifier: workspace:* - version: link:../ai-ollama + version: link:../../ai-ollama '@tanstack/ai-openai': specifier: workspace:* - version: link:../ai-openai + version: link:../../ai-openai devDependencies: '@types/node': specifier: ^24.10.1 @@ -586,6 +564,28 @@ importers: specifier: 5.9.3 version: 5.9.3 + packages/typescript/solid-ai-devtools: + dependencies: + '@tanstack/ai-devtools-core': + specifier: workspace:* + version: link:../ai-devtools + '@tanstack/devtools-utils': + specifier: ^0.0.8 + version: 0.0.8(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.0)(solid-js@1.9.10) + solid-js: + specifier: '>=1.9.7' + version: 1.9.10 + devDependencies: + '@vitest/coverage-v8': + specifier: 4.0.14 + version: 4.0.14(vitest@4.0.14(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.2.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite: + specifier: ^7.2.4 + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + packages: '@acemir/cssom@0.9.24': @@ -2055,6 +2055,9 @@ packages: resolution: {integrity: sha512-hTW2rLeZLBMmpwVzWmUJ5L50hPlf4Ea74rZtecbT6qQYCT16JEAbryfHm1u07KIICI2vEArsm2YguckjODqx0w==} engines: {node: '>=18'} + '@tanstack/query-core@5.90.11': + resolution: {integrity: sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==} + '@tanstack/query-core@5.90.5': resolution: {integrity: sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==} @@ -2129,6 +2132,10 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/router-core@1.139.10': + resolution: {integrity: sha512-gougqlYumNOn98d2ZhyoRJTNT8RvFip97z6T2T3/JTPrErwOsKaIA2FwlkfLJmJY1JQtUuF38IREJdfQrTJiqg==} + engines: {node: '>=12'} + '@tanstack/router-core@1.139.7': resolution: {integrity: sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA==} engines: {node: '>=12'} @@ -2144,10 +2151,35 @@ packages: csstype: optional: true + '@tanstack/router-generator@1.139.10': + resolution: {integrity: sha512-Uo0xmz6w1Ayv1AMyWLsT0ngXmjB8yAKv5khOaci/ZxAZNyvz3t84jqI7XXlG9fwtDRdTF4G/qBmXlPEmPk6Wfg==} + engines: {node: '>=12'} + '@tanstack/router-generator@1.139.7': resolution: {integrity: sha512-xnmF1poNH/dHtwFxecCcRsaLRIXVnXRZiWYUpvtyaPv4pQYayCrFQCg2ygDbCV0/8H7ctMBJh5MIL7GgPR7+xw==} engines: {node: '>=12'} + '@tanstack/router-plugin@1.139.10': + resolution: {integrity: sha512-0c9wzBKuz2U1jO+oAszT6VRaQDWPLfCJuPeXX7MCisM0nV2LVaxdb/y9YaWSKJ7zlQ7pwFkh37KYqcJhPXug/A==} + engines: {node: '>=12'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.139.10 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + '@tanstack/router-plugin@1.139.7': resolution: {integrity: sha512-sgB8nOoVKr0A2lw5p7kQ3MtEA03d1t+Qvqyy+f/QkHy5pGk8Yohg64TEX+2e98plfM3j5vAOu/JhAyoLLrp1Jw==} engines: {node: '>=12'} @@ -7570,6 +7602,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/query-core@5.90.11': {} + '@tanstack/query-core@5.90.5': {} '@tanstack/react-devtools@0.8.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)': @@ -7590,15 +7624,15 @@ snapshots: '@tanstack/query-core': 5.90.5 react: 19.2.0 - '@tanstack/react-router-devtools@1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.7)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/react-router-devtools@1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-devtools-core': 1.139.7(@tanstack/router-core@1.139.7)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@tanstack/router-devtools-core': 1.139.7(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: - '@tanstack/router-core': 1.139.7 + '@tanstack/router-core': 1.139.10 transitivePeerDependencies: - '@types/node' - csstype @@ -7614,12 +7648,12 @@ snapshots: - tsx - yaml - '@tanstack/react-router-ssr-query@1.139.7(@tanstack/query-core@5.90.5)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/react-router-ssr-query@1.139.7(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.5(react@19.2.0))(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@tanstack/query-core': 5.90.5 + '@tanstack/query-core': 5.90.11 '@tanstack/react-query': 5.90.5(react@19.2.0) '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-ssr-query-core': 1.139.7(@tanstack/query-core@5.90.5)(@tanstack/router-core@1.139.7) + '@tanstack/router-ssr-query-core': 1.139.7(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -7685,6 +7719,16 @@ snapshots: react-dom: 19.2.0(react@19.2.0) use-sync-external-store: 1.6.0(react@19.2.0) + '@tanstack/router-core@1.139.10': + dependencies: + '@tanstack/history': 1.139.0 + '@tanstack/store': 0.8.0 + cookie-es: 2.0.0 + seroval: 1.4.0 + seroval-plugins: 1.4.0(seroval@1.4.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + '@tanstack/router-core@1.139.7': dependencies: '@tanstack/history': 1.139.0 @@ -7695,9 +7739,9 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.139.7(@tanstack/router-core@1.139.7)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/router-devtools-core@1.139.7(@tanstack/router-core@1.139.10)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': dependencies: - '@tanstack/router-core': 1.139.7 + '@tanstack/router-core': 1.139.10 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) solid-js: 1.9.10 @@ -7718,6 +7762,19 @@ snapshots: - tsx - yaml + '@tanstack/router-generator@1.139.10': + dependencies: + '@tanstack/router-core': 1.139.10 + '@tanstack/router-utils': 1.139.0 + '@tanstack/virtual-file-routes': 1.139.0 + prettier: 3.6.2 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.20.6 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + '@tanstack/router-generator@1.139.7': dependencies: '@tanstack/router-core': 1.139.7 @@ -7731,6 +7788,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/router-plugin@1.139.10(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@tanstack/router-core': 1.139.10 + '@tanstack/router-generator': 1.139.10 + '@tanstack/router-utils': 1.139.0 + '@tanstack/virtual-file-routes': 1.139.0 + babel-dead-code-elimination: 1.0.10 + chokidar: 3.6.0 + unplugin: 2.3.10 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + transitivePeerDependencies: + - supports-color + '@tanstack/router-plugin@1.139.7(@tanstack/react-router@1.139.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 @@ -7754,10 +7834,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-ssr-query-core@1.139.7(@tanstack/query-core@5.90.5)(@tanstack/router-core@1.139.7)': + '@tanstack/router-ssr-query-core@1.139.7(@tanstack/query-core@5.90.11)(@tanstack/router-core@1.139.10)': dependencies: - '@tanstack/query-core': 5.90.5 - '@tanstack/router-core': 1.139.7 + '@tanstack/query-core': 5.90.11 + '@tanstack/router-core': 1.139.10 '@tanstack/router-utils@1.139.0': dependencies: