From da658300e3f122ae0bce61e4033b0bda7028a101 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Sat, 31 Jan 2026 22:10:33 -0600 Subject: [PATCH 01/21] feat: add eval framework for testing installer agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a structured evaluation system to validate the WorkOS installer agent against framework fixtures. Phase 1 includes: - Core types and interfaces for grading results - File and build graders with pattern matching - Next.js-specific grader checking AuthKit integration - Fixture manager for temp dir setup/cleanup - Eval runner orchestrating fixture → agent → grade flow - CLI entry point with --framework and --verbose flags - Minimal Next.js 14 App Router fixture The agent executor is stubbed to validate framework structure first. Run with: pnpm eval --- .gitignore | 3 + package.json | 5 +- src/utils/exec-file.ts | 59 +++++++++++++++++ tests/evals/agent-executor.ts | 51 +++++++++++++++ tests/evals/fixture-manager.ts | 51 +++++++++++++++ tests/evals/graders/build-grader.ts | 46 +++++++++++++ tests/evals/graders/file-grader.ts | 58 +++++++++++++++++ tests/evals/graders/nextjs.grader.ts | 55 ++++++++++++++++ tests/evals/index.ts | 28 ++++++++ tests/evals/runner.ts | 71 +++++++++++++++++++++ tests/evals/types.ts | 31 +++++++++ tests/fixtures/nextjs/fresh/app/layout.tsx | 11 ++++ tests/fixtures/nextjs/fresh/app/page.tsx | 7 ++ tests/fixtures/nextjs/fresh/next.config.mjs | 4 ++ tests/fixtures/nextjs/fresh/package.json | 21 ++++++ tests/fixtures/nextjs/fresh/tsconfig.json | 21 ++++++ 16 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 src/utils/exec-file.ts create mode 100644 tests/evals/agent-executor.ts create mode 100644 tests/evals/fixture-manager.ts create mode 100644 tests/evals/graders/build-grader.ts create mode 100644 tests/evals/graders/file-grader.ts create mode 100644 tests/evals/graders/nextjs.grader.ts create mode 100644 tests/evals/index.ts create mode 100644 tests/evals/runner.ts create mode 100644 tests/evals/types.ts create mode 100644 tests/fixtures/nextjs/fresh/app/layout.tsx create mode 100644 tests/fixtures/nextjs/fresh/app/page.tsx create mode 100644 tests/fixtures/nextjs/fresh/next.config.mjs create mode 100644 tests/fixtures/nextjs/fresh/package.json create mode 100644 tests/fixtures/nextjs/fresh/tsconfig.json diff --git a/.gitignore b/.gitignore index 785f59f..92aa0ee 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ src/version.ts .idea *.sublime-* dist/ + +# Eval results +tests/eval-results/ diff --git a/package.json b/package.json index 6ac44c4..fd33a52 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "engines": { "node": ">=20.20" }, - "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b", + "packageManager": "pnpm@10.28.2", "scripts": { "clean": "rm -rf ./dist", "prebuild": "pnpm clean", @@ -85,7 +85,8 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "eval": "tsx tests/evals/index.ts" }, "author": "WorkOS", "license": "MIT" diff --git a/src/utils/exec-file.ts b/src/utils/exec-file.ts new file mode 100644 index 0000000..5c81b6a --- /dev/null +++ b/src/utils/exec-file.ts @@ -0,0 +1,59 @@ +import { spawn } from 'node:child_process'; + +export interface ExecResult { + status: number; + stdout: string; + stderr: string; +} + +export interface ExecOptions { + cwd?: string; + timeout?: number; + env?: NodeJS.ProcessEnv; +} + +/** + * Execute a command without throwing on non-zero exit codes. + * Returns { status, stdout, stderr } for all outcomes. + */ +export function execFileNoThrow( + command: string, + args: string[], + options: ExecOptions = {} +): Promise { + return new Promise((resolve) => { + const child = spawn(command, args, { + cwd: options.cwd, + env: options.env ?? process.env, + timeout: options.timeout, + shell: false, + }); + + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr?.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + resolve({ + status: code ?? 1, + stdout, + stderr, + }); + }); + + child.on('error', (err) => { + resolve({ + status: 1, + stdout, + stderr: err.message, + }); + }); + }); +} diff --git a/tests/evals/agent-executor.ts b/tests/evals/agent-executor.ts new file mode 100644 index 0000000..5b45a68 --- /dev/null +++ b/tests/evals/agent-executor.ts @@ -0,0 +1,51 @@ +import { Integration } from '../../src/lib/constants.js'; + +export interface AgentResult { + success: boolean; + output: string; + toolCalls: Array<{ tool: string; args: unknown; result: unknown }>; +} + +export class AgentExecutor { + constructor( + private workDir: string, + private framework: string + ) {} + + async run(): Promise { + const toolCalls: AgentResult['toolCalls'] = []; + + // Map framework to Integration enum + const integration = this.getIntegration(); + + // TODO: In a real implementation, this would: + // 1. Build the prompt using existing infrastructure + // 2. Initialize and run the agent + // 3. Capture tool calls and output + // + // For now, we stub this to focus on the eval framework structure. + // The agent runner integration will be completed once we validate + // the fixture/grader flow works correctly. + + console.log(` Running agent for ${integration} in ${this.workDir}...`); + + // Placeholder - actual agent execution would go here + // This allows us to test the eval framework structure + return { + success: true, + output: `[Placeholder] Agent would run ${integration} installation here`, + toolCalls, + }; + } + + private getIntegration(): Integration { + const map: Record = { + nextjs: Integration.nextjs, + react: Integration.react, + 'react-router': Integration.reactRouter, + 'tanstack-start': Integration.tanstackStart, + 'vanilla-js': Integration.vanillaJs, + }; + return map[this.framework]; + } +} diff --git a/tests/evals/fixture-manager.ts b/tests/evals/fixture-manager.ts new file mode 100644 index 0000000..387a7ea --- /dev/null +++ b/tests/evals/fixture-manager.ts @@ -0,0 +1,51 @@ +import { cp, rm, mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { execFileNoThrow } from '../../src/utils/exec-file.js'; + +export class FixtureManager { + private tempDir: string | null = null; + + constructor( + private framework: string, + private state: string + ) {} + + async setup(): Promise { + // Create temp directory + this.tempDir = await mkdtemp(join(tmpdir(), `eval-${this.framework}-`)); + + // Copy fixture files + const fixtureSource = join( + process.cwd(), + 'tests/fixtures', + this.framework, + this.state + ); + + await cp(fixtureSource, this.tempDir, { recursive: true }); + + // Install dependencies using safe exec + console.log(' Installing dependencies...'); + const result = await execFileNoThrow('pnpm', ['install'], { + cwd: this.tempDir, + }); + + if (result.status !== 0) { + throw new Error(`pnpm install failed: ${result.stderr}`); + } + + return this.tempDir; + } + + async cleanup(): Promise { + if (this.tempDir) { + await rm(this.tempDir, { recursive: true, force: true }); + this.tempDir = null; + } + } + + getTempDir(): string | null { + return this.tempDir; + } +} diff --git a/tests/evals/graders/build-grader.ts b/tests/evals/graders/build-grader.ts new file mode 100644 index 0000000..9834ac1 --- /dev/null +++ b/tests/evals/graders/build-grader.ts @@ -0,0 +1,46 @@ +import { execFileNoThrow } from '../../../src/utils/exec-file.js'; +import type { GradeCheck } from '../types.js'; + +export class BuildGrader { + constructor(protected workDir: string) {} + + async checkBuild(): Promise { + const result = await execFileNoThrow('pnpm', ['build'], { + cwd: this.workDir, + timeout: 120000, // 2 minute timeout + }); + + if (result.status === 0) { + return { + name: 'Build succeeds', + passed: true, + }; + } + + return { + name: 'Build succeeds', + passed: false, + message: `Build failed: ${result.stderr.slice(0, 500)}`, + }; + } + + async checkTypecheck(): Promise { + const result = await execFileNoThrow('pnpm', ['tsc', '--noEmit'], { + cwd: this.workDir, + timeout: 60000, + }); + + if (result.status === 0) { + return { + name: 'Type check passes', + passed: true, + }; + } + + return { + name: 'Type check passes', + passed: false, + message: `Type errors: ${result.stderr.slice(0, 500)}`, + }; + } +} diff --git a/tests/evals/graders/file-grader.ts b/tests/evals/graders/file-grader.ts new file mode 100644 index 0000000..d6860cb --- /dev/null +++ b/tests/evals/graders/file-grader.ts @@ -0,0 +1,58 @@ +import { access, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import type { GradeCheck } from '../types.js'; + +export class FileGrader { + constructor(protected workDir: string) {} + + async checkFileExists(relativePath: string): Promise { + const fullPath = join(this.workDir, relativePath); + try { + await access(fullPath); + return { + name: `File exists: ${relativePath}`, + passed: true, + }; + } catch { + return { + name: `File exists: ${relativePath}`, + passed: false, + message: `File not found: ${relativePath}`, + }; + } + } + + async checkFileContains( + relativePath: string, + patterns: (string | RegExp)[] + ): Promise { + const fullPath = join(this.workDir, relativePath); + const checks: GradeCheck[] = []; + + let content: string; + try { + content = await readFile(fullPath, 'utf-8'); + } catch { + return patterns.map((p) => ({ + name: `Pattern in ${relativePath}: ${p}`, + passed: false, + message: `Cannot read file: ${relativePath}`, + })); + } + + for (const pattern of patterns) { + const matches = + typeof pattern === 'string' + ? content.includes(pattern) + : pattern.test(content); + + checks.push({ + name: `Pattern in ${relativePath}: ${pattern}`, + passed: matches, + message: matches ? undefined : `Pattern not found: ${pattern}`, + }); + } + + return checks; + } +} diff --git a/tests/evals/graders/nextjs.grader.ts b/tests/evals/graders/nextjs.grader.ts new file mode 100644 index 0000000..d371f03 --- /dev/null +++ b/tests/evals/graders/nextjs.grader.ts @@ -0,0 +1,55 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +export class NextjsGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const checks: GradeCheck[] = []; + + // Check callback route exists (App Router) + checks.push( + await this.fileGrader.checkFileExists('app/api/auth/callback/route.ts') + ); + + // Check middleware exists + checks.push(await this.fileGrader.checkFileExists('middleware.ts')); + + // Check middleware imports authkit + checks.push( + ...(await this.fileGrader.checkFileContains('middleware.ts', [ + '@workos-inc/authkit-nextjs', + 'authkitMiddleware', + ])) + ); + + // Check AuthKitProvider in layout + checks.push( + ...(await this.fileGrader.checkFileContains('app/layout.tsx', [ + 'AuthKitProvider', + ])) + ); + + // Check environment variables used correctly + checks.push( + ...(await this.fileGrader.checkFileContains('middleware.ts', [ + /process\.env\.WORKOS_/, + ])) + ); + + // Check build succeeds + checks.push(await this.buildGrader.checkBuild()); + + return { + passed: checks.every((c) => c.passed), + checks, + }; + } +} diff --git a/tests/evals/index.ts b/tests/evals/index.ts new file mode 100644 index 0000000..f2b76bd --- /dev/null +++ b/tests/evals/index.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import { runEvals } from './runner.js'; + +async function main() { + const args = process.argv.slice(2); + + // Parse basic flags (expand in Phase 2) + const options = { + framework: args.find((a) => a.startsWith('--framework='))?.split('=')[1], + verbose: args.includes('--verbose'), + }; + + try { + const results = await runEvals(options); + + // Print summary + const passed = results.filter((r) => r.passed).length; + console.log(`\n${passed}/${results.length} scenarios passed`); + + // Exit code + process.exit(results.every((r) => r.passed) ? 0 : 1); + } catch (error) { + console.error('Eval failed:', error); + process.exit(1); + } +} + +main(); diff --git a/tests/evals/runner.ts b/tests/evals/runner.ts new file mode 100644 index 0000000..845e503 --- /dev/null +++ b/tests/evals/runner.ts @@ -0,0 +1,71 @@ +import { FixtureManager } from './fixture-manager.js'; +import { AgentExecutor } from './agent-executor.js'; +import { NextjsGrader } from './graders/nextjs.grader.js'; +import type { EvalResult, EvalOptions, Grader } from './types.js'; + +interface Scenario { + framework: string; + state: string; + grader: new (workDir: string) => Grader; +} + +const SCENARIOS: Scenario[] = [ + { framework: 'nextjs', state: 'fresh', grader: NextjsGrader }, + // More scenarios added in Phase 2 +]; + +export async function runEvals(options: EvalOptions): Promise { + const results: EvalResult[] = []; + + const scenarios = SCENARIOS.filter( + (s) => !options.framework || s.framework === options.framework + ); + + for (const scenario of scenarios) { + console.log(`\nRunning: ${scenario.framework}/${scenario.state}`); + const startTime = Date.now(); + + const fixtureManager = new FixtureManager(scenario.framework, scenario.state); + + try { + // Setup fixture in temp directory + const workDir = await fixtureManager.setup(); + + // Run agent against fixture + const executor = new AgentExecutor(workDir, scenario.framework); + const agentResult = await executor.run(); + + // Grade the result + const grader = new scenario.grader(workDir); + const gradeResult = await grader.grade(); + + results.push({ + scenario: `${scenario.framework}/${scenario.state}`, + passed: gradeResult.passed, + duration: Date.now() - startTime, + checks: gradeResult.checks, + agentOutput: agentResult.output, + }); + + console.log(gradeResult.passed ? '✓ PASSED' : '✗ FAILED'); + + if (!gradeResult.passed) { + for (const check of gradeResult.checks.filter((c) => !c.passed)) { + console.log(` - ${check.name}: ${check.message}`); + } + } + } catch (error) { + results.push({ + scenario: `${scenario.framework}/${scenario.state}`, + passed: false, + duration: Date.now() - startTime, + error: error instanceof Error ? error.message : String(error), + }); + console.log('✗ ERROR:', error); + } finally { + await fixtureManager.cleanup(); + } + } + + return results; +} diff --git a/tests/evals/types.ts b/tests/evals/types.ts new file mode 100644 index 0000000..59c1844 --- /dev/null +++ b/tests/evals/types.ts @@ -0,0 +1,31 @@ +export interface GradeCheck { + name: string; + passed: boolean; + message?: string; + expected?: string; + actual?: string; +} + +export interface GradeResult { + passed: boolean; + checks: GradeCheck[]; +} + +export interface Grader { + grade(): Promise; +} + +export interface EvalResult { + scenario: string; + passed: boolean; + duration: number; + checks?: GradeCheck[]; + agentOutput?: string; + error?: string; +} + +export interface EvalOptions { + framework?: string; + state?: string; + verbose?: boolean; +} diff --git a/tests/fixtures/nextjs/fresh/app/layout.tsx b/tests/fixtures/nextjs/fresh/app/layout.tsx new file mode 100644 index 0000000..225b603 --- /dev/null +++ b/tests/fixtures/nextjs/fresh/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/tests/fixtures/nextjs/fresh/app/page.tsx b/tests/fixtures/nextjs/fresh/app/page.tsx new file mode 100644 index 0000000..9584f50 --- /dev/null +++ b/tests/fixtures/nextjs/fresh/app/page.tsx @@ -0,0 +1,7 @@ +export default function Home() { + return ( +
+

Welcome

+
+ ); +} diff --git a/tests/fixtures/nextjs/fresh/next.config.mjs b/tests/fixtures/nextjs/fresh/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/tests/fixtures/nextjs/fresh/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/tests/fixtures/nextjs/fresh/package.json b/tests/fixtures/nextjs/fresh/package.json new file mode 100644 index 0000000..0016e7d --- /dev/null +++ b/tests/fixtures/nextjs/fresh/package.json @@ -0,0 +1,21 @@ +{ + "name": "nextjs-fresh-fixture", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^14.2.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "typescript": "^5.4.0" + } +} diff --git a/tests/fixtures/nextjs/fresh/tsconfig.json b/tests/fixtures/nextjs/fresh/tsconfig.json new file mode 100644 index 0000000..6420eed --- /dev/null +++ b/tests/fixtures/nextjs/fresh/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 7d83b35d127f9d37c561041a50a1a57ec0868d99 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Sat, 31 Jan 2026 22:25:00 -0600 Subject: [PATCH 02/21] feat: expand eval framework to full 15-scenario test matrix Add CLI with filtering (--framework, --state, --json), matrix reporter, and graders for all 5 frameworks. Create fixtures for fresh, existing, and existing-auth0 states across Next.js, React SPA, React Router, TanStack Start, and Vanilla JS. --- tests/evals/cli.ts | 68 ++++++++++++++++ tests/evals/graders/react-router.grader.ts | 80 +++++++++++++++++++ tests/evals/graders/react.grader.ts | 52 ++++++++++++ tests/evals/graders/tanstack.grader.ts | 78 ++++++++++++++++++ tests/evals/graders/vanilla.grader.ts | 59 ++++++++++++++ tests/evals/index.ts | 22 ++--- tests/evals/reporter.ts | 61 ++++++++++++++ tests/evals/runner.ts | 32 +++++++- .../nextjs/existing-auth0/.env.example | 5 ++ .../nextjs/existing-auth0/app/about/page.tsx | 8 ++ .../app/api/auth/[auth0]/route.ts | 3 + .../existing-auth0/app/dashboard/page.tsx | 26 ++++++ .../nextjs/existing-auth0/app/layout.tsx | 23 ++++++ .../nextjs/existing-auth0/app/page.tsx | 8 ++ .../nextjs/existing-auth0/next.config.mjs | 4 + .../nextjs/existing-auth0/package.json | 22 +++++ .../nextjs/existing-auth0/tsconfig.json | 26 ++++++ .../nextjs/existing/app/about/page.tsx | 8 ++ .../nextjs/existing/app/api/hello/route.ts | 5 ++ .../nextjs/existing/app/dashboard/page.tsx | 8 ++ tests/fixtures/nextjs/existing/app/layout.tsx | 19 +++++ tests/fixtures/nextjs/existing/app/page.tsx | 8 ++ .../fixtures/nextjs/existing/next.config.mjs | 4 + tests/fixtures/nextjs/existing/package.json | 21 +++++ tests/fixtures/nextjs/existing/tsconfig.json | 26 ++++++ .../react-router/existing-auth0/.env.example | 2 + .../react-router/existing-auth0/app/root.tsx | 36 +++++++++ .../react-router/existing-auth0/app/routes.ts | 7 ++ .../existing-auth0/app/routes/about.tsx | 8 ++ .../existing-auth0/app/routes/dashboard.tsx | 25 ++++++ .../existing-auth0/app/routes/home.tsx | 17 ++++ .../react-router/existing-auth0/package.json | 26 ++++++ .../existing-auth0/react-router.config.ts | 5 ++ .../react-router/existing-auth0/tsconfig.json | 19 +++++ .../existing-auth0/vite.config.ts | 12 +++ .../react-router/existing/app/root.tsx | 27 +++++++ .../react-router/existing/app/routes.ts | 7 ++ .../existing/app/routes/about.tsx | 8 ++ .../existing/app/routes/dashboard.tsx | 8 ++ .../react-router/existing/app/routes/home.tsx | 8 ++ .../react-router/existing/package.json | 25 ++++++ .../existing/react-router.config.ts | 5 ++ .../react-router/existing/tsconfig.json | 19 +++++ .../react-router/existing/vite.config.ts | 12 +++ .../fixtures/react-router/fresh/app/root.tsx | 23 ++++++ .../fixtures/react-router/fresh/app/routes.ts | 3 + .../react-router/fresh/app/routes/home.tsx | 8 ++ .../fixtures/react-router/fresh/package.json | 25 ++++++ .../react-router/fresh/react-router.config.ts | 5 ++ .../fixtures/react-router/fresh/tsconfig.json | 16 ++++ .../react-router/fresh/vite.config.ts | 6 ++ .../react/existing-auth0/.env.example | 2 + .../fixtures/react/existing-auth0/index.html | 12 +++ .../react/existing-auth0/package.json | 24 ++++++ .../fixtures/react/existing-auth0/src/App.tsx | 30 +++++++ .../react/existing-auth0/src/main.tsx | 21 +++++ .../react/existing-auth0/src/pages/About.tsx | 8 ++ .../existing-auth0/src/pages/Dashboard.tsx | 20 +++++ .../react/existing-auth0/src/pages/Home.tsx | 8 ++ .../react/existing-auth0/src/vite-env.d.ts | 10 +++ .../react/existing-auth0/tsconfig.json | 23 ++++++ .../react/existing-auth0/vite.config.ts | 12 +++ tests/fixtures/react/existing/index.html | 12 +++ tests/fixtures/react/existing/package.json | 23 ++++++ tests/fixtures/react/existing/src/App.tsx | 22 +++++ tests/fixtures/react/existing/src/main.tsx | 12 +++ .../react/existing/src/pages/About.tsx | 8 ++ .../react/existing/src/pages/Dashboard.tsx | 8 ++ .../react/existing/src/pages/Home.tsx | 8 ++ .../fixtures/react/existing/src/vite-env.d.ts | 1 + tests/fixtures/react/existing/tsconfig.json | 23 ++++++ tests/fixtures/react/existing/vite.config.ts | 12 +++ tests/fixtures/react/fresh/index.html | 12 +++ tests/fixtures/react/fresh/package.json | 22 +++++ tests/fixtures/react/fresh/src/App.tsx | 10 +++ tests/fixtures/react/fresh/src/main.tsx | 9 +++ tests/fixtures/react/fresh/src/vite-env.d.ts | 1 + tests/fixtures/react/fresh/tsconfig.json | 20 +++++ tests/fixtures/react/fresh/vite.config.ts | 6 ++ .../existing-auth0/.env.example | 2 + .../existing-auth0/app.config.ts | 3 + .../existing-auth0/app/client.tsx | 7 ++ .../existing-auth0/app/router.tsx | 16 ++++ .../existing-auth0/app/routes/__root.tsx | 33 ++++++++ .../existing-auth0/app/routes/about.tsx | 14 ++++ .../existing-auth0/app/routes/dashboard.tsx | 30 +++++++ .../existing-auth0/app/routes/index.tsx | 22 +++++ .../tanstack-start/existing-auth0/app/ssr.tsx | 6 ++ .../existing-auth0/package.json | 24 ++++++ .../existing-auth0/tsconfig.json | 22 +++++ .../tanstack-start/existing/app.config.ts | 3 + .../tanstack-start/existing/app/client.tsx | 7 ++ .../tanstack-start/existing/app/router.tsx | 16 ++++ .../existing/app/routes/__root.tsx | 24 ++++++ .../existing/app/routes/about.tsx | 14 ++++ .../existing/app/routes/dashboard.tsx | 14 ++++ .../existing/app/routes/index.tsx | 14 ++++ .../tanstack-start/existing/app/ssr.tsx | 6 ++ .../tanstack-start/existing/package.json | 23 ++++++ .../tanstack-start/existing/tsconfig.json | 22 +++++ .../tanstack-start/fresh/app.config.ts | 3 + .../tanstack-start/fresh/app/client.tsx | 7 ++ .../tanstack-start/fresh/app/router.tsx | 16 ++++ .../fresh/app/routes/__root.tsx | 20 +++++ .../tanstack-start/fresh/app/routes/index.tsx | 14 ++++ .../fixtures/tanstack-start/fresh/app/ssr.tsx | 6 ++ .../tanstack-start/fresh/package.json | 23 ++++++ .../tanstack-start/fresh/tsconfig.json | 19 +++++ .../vanilla-js/existing-auth0/.env.example | 2 + .../vanilla-js/existing-auth0/about.html | 23 ++++++ .../vanilla-js/existing-auth0/dashboard.html | 25 ++++++ .../vanilla-js/existing-auth0/dashboard.js | 55 +++++++++++++ .../vanilla-js/existing-auth0/index.html | 23 ++++++ .../vanilla-js/existing-auth0/main.js | 60 ++++++++++++++ .../vanilla-js/existing-auth0/package.json | 16 ++++ .../vanilla-js/existing-auth0/styles.css | 33 ++++++++ tests/fixtures/vanilla-js/existing/about.html | 19 +++++ .../vanilla-js/existing/dashboard.html | 19 +++++ tests/fixtures/vanilla-js/existing/index.html | 19 +++++ tests/fixtures/vanilla-js/existing/main.js | 10 +++ .../fixtures/vanilla-js/existing/package.json | 13 +++ tests/fixtures/vanilla-js/existing/styles.css | 21 +++++ tests/fixtures/vanilla-js/fresh/index.html | 15 ++++ tests/fixtures/vanilla-js/fresh/main.js | 1 + tests/fixtures/vanilla-js/fresh/package.json | 13 +++ 125 files changed, 2202 insertions(+), 12 deletions(-) create mode 100644 tests/evals/cli.ts create mode 100644 tests/evals/graders/react-router.grader.ts create mode 100644 tests/evals/graders/react.grader.ts create mode 100644 tests/evals/graders/tanstack.grader.ts create mode 100644 tests/evals/graders/vanilla.grader.ts create mode 100644 tests/evals/reporter.ts create mode 100644 tests/fixtures/nextjs/existing-auth0/.env.example create mode 100644 tests/fixtures/nextjs/existing-auth0/app/about/page.tsx create mode 100644 tests/fixtures/nextjs/existing-auth0/app/api/auth/[auth0]/route.ts create mode 100644 tests/fixtures/nextjs/existing-auth0/app/dashboard/page.tsx create mode 100644 tests/fixtures/nextjs/existing-auth0/app/layout.tsx create mode 100644 tests/fixtures/nextjs/existing-auth0/app/page.tsx create mode 100644 tests/fixtures/nextjs/existing-auth0/next.config.mjs create mode 100644 tests/fixtures/nextjs/existing-auth0/package.json create mode 100644 tests/fixtures/nextjs/existing-auth0/tsconfig.json create mode 100644 tests/fixtures/nextjs/existing/app/about/page.tsx create mode 100644 tests/fixtures/nextjs/existing/app/api/hello/route.ts create mode 100644 tests/fixtures/nextjs/existing/app/dashboard/page.tsx create mode 100644 tests/fixtures/nextjs/existing/app/layout.tsx create mode 100644 tests/fixtures/nextjs/existing/app/page.tsx create mode 100644 tests/fixtures/nextjs/existing/next.config.mjs create mode 100644 tests/fixtures/nextjs/existing/package.json create mode 100644 tests/fixtures/nextjs/existing/tsconfig.json create mode 100644 tests/fixtures/react-router/existing-auth0/.env.example create mode 100644 tests/fixtures/react-router/existing-auth0/app/root.tsx create mode 100644 tests/fixtures/react-router/existing-auth0/app/routes.ts create mode 100644 tests/fixtures/react-router/existing-auth0/app/routes/about.tsx create mode 100644 tests/fixtures/react-router/existing-auth0/app/routes/dashboard.tsx create mode 100644 tests/fixtures/react-router/existing-auth0/app/routes/home.tsx create mode 100644 tests/fixtures/react-router/existing-auth0/package.json create mode 100644 tests/fixtures/react-router/existing-auth0/react-router.config.ts create mode 100644 tests/fixtures/react-router/existing-auth0/tsconfig.json create mode 100644 tests/fixtures/react-router/existing-auth0/vite.config.ts create mode 100644 tests/fixtures/react-router/existing/app/root.tsx create mode 100644 tests/fixtures/react-router/existing/app/routes.ts create mode 100644 tests/fixtures/react-router/existing/app/routes/about.tsx create mode 100644 tests/fixtures/react-router/existing/app/routes/dashboard.tsx create mode 100644 tests/fixtures/react-router/existing/app/routes/home.tsx create mode 100644 tests/fixtures/react-router/existing/package.json create mode 100644 tests/fixtures/react-router/existing/react-router.config.ts create mode 100644 tests/fixtures/react-router/existing/tsconfig.json create mode 100644 tests/fixtures/react-router/existing/vite.config.ts create mode 100644 tests/fixtures/react-router/fresh/app/root.tsx create mode 100644 tests/fixtures/react-router/fresh/app/routes.ts create mode 100644 tests/fixtures/react-router/fresh/app/routes/home.tsx create mode 100644 tests/fixtures/react-router/fresh/package.json create mode 100644 tests/fixtures/react-router/fresh/react-router.config.ts create mode 100644 tests/fixtures/react-router/fresh/tsconfig.json create mode 100644 tests/fixtures/react-router/fresh/vite.config.ts create mode 100644 tests/fixtures/react/existing-auth0/.env.example create mode 100644 tests/fixtures/react/existing-auth0/index.html create mode 100644 tests/fixtures/react/existing-auth0/package.json create mode 100644 tests/fixtures/react/existing-auth0/src/App.tsx create mode 100644 tests/fixtures/react/existing-auth0/src/main.tsx create mode 100644 tests/fixtures/react/existing-auth0/src/pages/About.tsx create mode 100644 tests/fixtures/react/existing-auth0/src/pages/Dashboard.tsx create mode 100644 tests/fixtures/react/existing-auth0/src/pages/Home.tsx create mode 100644 tests/fixtures/react/existing-auth0/src/vite-env.d.ts create mode 100644 tests/fixtures/react/existing-auth0/tsconfig.json create mode 100644 tests/fixtures/react/existing-auth0/vite.config.ts create mode 100644 tests/fixtures/react/existing/index.html create mode 100644 tests/fixtures/react/existing/package.json create mode 100644 tests/fixtures/react/existing/src/App.tsx create mode 100644 tests/fixtures/react/existing/src/main.tsx create mode 100644 tests/fixtures/react/existing/src/pages/About.tsx create mode 100644 tests/fixtures/react/existing/src/pages/Dashboard.tsx create mode 100644 tests/fixtures/react/existing/src/pages/Home.tsx create mode 100644 tests/fixtures/react/existing/src/vite-env.d.ts create mode 100644 tests/fixtures/react/existing/tsconfig.json create mode 100644 tests/fixtures/react/existing/vite.config.ts create mode 100644 tests/fixtures/react/fresh/index.html create mode 100644 tests/fixtures/react/fresh/package.json create mode 100644 tests/fixtures/react/fresh/src/App.tsx create mode 100644 tests/fixtures/react/fresh/src/main.tsx create mode 100644 tests/fixtures/react/fresh/src/vite-env.d.ts create mode 100644 tests/fixtures/react/fresh/tsconfig.json create mode 100644 tests/fixtures/react/fresh/vite.config.ts create mode 100644 tests/fixtures/tanstack-start/existing-auth0/.env.example create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app.config.ts create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app/client.tsx create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app/router.tsx create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app/routes/__root.tsx create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app/routes/about.tsx create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app/routes/dashboard.tsx create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app/routes/index.tsx create mode 100644 tests/fixtures/tanstack-start/existing-auth0/app/ssr.tsx create mode 100644 tests/fixtures/tanstack-start/existing-auth0/package.json create mode 100644 tests/fixtures/tanstack-start/existing-auth0/tsconfig.json create mode 100644 tests/fixtures/tanstack-start/existing/app.config.ts create mode 100644 tests/fixtures/tanstack-start/existing/app/client.tsx create mode 100644 tests/fixtures/tanstack-start/existing/app/router.tsx create mode 100644 tests/fixtures/tanstack-start/existing/app/routes/__root.tsx create mode 100644 tests/fixtures/tanstack-start/existing/app/routes/about.tsx create mode 100644 tests/fixtures/tanstack-start/existing/app/routes/dashboard.tsx create mode 100644 tests/fixtures/tanstack-start/existing/app/routes/index.tsx create mode 100644 tests/fixtures/tanstack-start/existing/app/ssr.tsx create mode 100644 tests/fixtures/tanstack-start/existing/package.json create mode 100644 tests/fixtures/tanstack-start/existing/tsconfig.json create mode 100644 tests/fixtures/tanstack-start/fresh/app.config.ts create mode 100644 tests/fixtures/tanstack-start/fresh/app/client.tsx create mode 100644 tests/fixtures/tanstack-start/fresh/app/router.tsx create mode 100644 tests/fixtures/tanstack-start/fresh/app/routes/__root.tsx create mode 100644 tests/fixtures/tanstack-start/fresh/app/routes/index.tsx create mode 100644 tests/fixtures/tanstack-start/fresh/app/ssr.tsx create mode 100644 tests/fixtures/tanstack-start/fresh/package.json create mode 100644 tests/fixtures/tanstack-start/fresh/tsconfig.json create mode 100644 tests/fixtures/vanilla-js/existing-auth0/.env.example create mode 100644 tests/fixtures/vanilla-js/existing-auth0/about.html create mode 100644 tests/fixtures/vanilla-js/existing-auth0/dashboard.html create mode 100644 tests/fixtures/vanilla-js/existing-auth0/dashboard.js create mode 100644 tests/fixtures/vanilla-js/existing-auth0/index.html create mode 100644 tests/fixtures/vanilla-js/existing-auth0/main.js create mode 100644 tests/fixtures/vanilla-js/existing-auth0/package.json create mode 100644 tests/fixtures/vanilla-js/existing-auth0/styles.css create mode 100644 tests/fixtures/vanilla-js/existing/about.html create mode 100644 tests/fixtures/vanilla-js/existing/dashboard.html create mode 100644 tests/fixtures/vanilla-js/existing/index.html create mode 100644 tests/fixtures/vanilla-js/existing/main.js create mode 100644 tests/fixtures/vanilla-js/existing/package.json create mode 100644 tests/fixtures/vanilla-js/existing/styles.css create mode 100644 tests/fixtures/vanilla-js/fresh/index.html create mode 100644 tests/fixtures/vanilla-js/fresh/main.js create mode 100644 tests/fixtures/vanilla-js/fresh/package.json diff --git a/tests/evals/cli.ts b/tests/evals/cli.ts new file mode 100644 index 0000000..f8e5ece --- /dev/null +++ b/tests/evals/cli.ts @@ -0,0 +1,68 @@ +export interface CliOptions { + framework?: string; + state?: string; + verbose: boolean; + json: boolean; + help: boolean; +} + +const FRAMEWORKS = ['nextjs', 'react', 'react-router', 'tanstack-start', 'vanilla-js']; +const STATES = ['fresh', 'existing', 'existing-auth0']; + +export function parseArgs(args: string[]): CliOptions { + const options: CliOptions = { + verbose: false, + json: false, + help: false, + }; + + for (const arg of args) { + if (arg === '--help' || arg === '-h') { + options.help = true; + } else if (arg === '--verbose' || arg === '-v') { + options.verbose = true; + } else if (arg === '--json') { + options.json = true; + } else if (arg.startsWith('--framework=')) { + const framework = arg.split('=')[1]; + if (!FRAMEWORKS.includes(framework)) { + throw new Error(`Unknown framework: ${framework}. Valid: ${FRAMEWORKS.join(', ')}`); + } + options.framework = framework; + } else if (arg.startsWith('--state=')) { + const state = arg.split('=')[1]; + if (!STATES.includes(state)) { + throw new Error(`Unknown state: ${state}. Valid: ${STATES.join(', ')}`); + } + options.state = state; + } + } + + return options; +} + +export function printHelp(): void { + console.log(` +Usage: pnpm eval [options] + +Options: + --framework= Run only scenarios for this framework + Valid: ${FRAMEWORKS.join(', ')} + + --state= Run only scenarios for this project state + Valid: ${STATES.join(', ')} + + --verbose, -v Show detailed output including agent tool calls + + --json Output results as JSON (for scripting) + + --help, -h Show this help message + +Examples: + pnpm eval # Run all 15 scenarios + pnpm eval --framework=nextjs # Run only Next.js scenarios + pnpm eval --state=fresh # Run only fresh app scenarios + pnpm eval --framework=react --state=existing-auth0 + # Run specific scenario +`); +} diff --git a/tests/evals/graders/react-router.grader.ts b/tests/evals/graders/react-router.grader.ts new file mode 100644 index 0000000..b4b077a --- /dev/null +++ b/tests/evals/graders/react-router.grader.ts @@ -0,0 +1,80 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +export class ReactRouterGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const checks: GradeCheck[] = []; + + // Check auth route/loader exists (v7 flat routes style) + const v7RouteExists = await this.fileGrader.checkFileExists('app/routes/auth.callback.tsx'); + if (!v7RouteExists.passed) { + // Try v6 nested style + const v6RouteExists = await this.fileGrader.checkFileExists('src/routes/callback.tsx'); + if (!v6RouteExists.passed) { + // Try alternate v7 pattern + checks.push(await this.fileGrader.checkFileExists('app/routes/callback.tsx')); + } else { + checks.push(v6RouteExists); + } + } else { + checks.push(v7RouteExists); + } + + // Check loader/action for auth - try multiple possible locations + const authRoutePatterns = await this.checkAuthRoute(); + checks.push(...authRoutePatterns); + + // Check protected route wrapper or middleware + const hasProtectedRoute = await this.fileGrader.checkFileExists( + 'app/components/ProtectedRoute.tsx' + ); + if (!hasProtectedRoute.passed) { + // Try alternate locations + checks.push(await this.fileGrader.checkFileExists('app/utils/auth.ts')); + } else { + checks.push(hasProtectedRoute); + } + + // Check build succeeds + checks.push(await this.buildGrader.checkBuild()); + + return { + passed: checks.every((c) => c.passed), + checks, + }; + } + + private async checkAuthRoute(): Promise { + // Try v7 flat routes first + const v7Checks = await this.fileGrader.checkFileContains('app/routes/auth.callback.tsx', [ + 'loader', + '@workos-inc/authkit', + ]); + + if (v7Checks.every((c) => c.passed)) { + return v7Checks; + } + + // Try alternate callback location + const altChecks = await this.fileGrader.checkFileContains('app/routes/callback.tsx', [ + 'loader', + '@workos-inc/authkit', + ]); + + if (altChecks.every((c) => c.passed)) { + return altChecks; + } + + // Return the v7 checks as the expected pattern + return v7Checks; + } +} diff --git a/tests/evals/graders/react.grader.ts b/tests/evals/graders/react.grader.ts new file mode 100644 index 0000000..b11428d --- /dev/null +++ b/tests/evals/graders/react.grader.ts @@ -0,0 +1,52 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +export class ReactGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const checks: GradeCheck[] = []; + + // Check AuthKitProvider wrapper exists + checks.push( + ...(await this.fileGrader.checkFileContains('src/main.tsx', [ + 'AuthKitProvider', + '@workos-inc/authkit-react', + ])) + ); + + // Check callback component exists + const callbackExists = await this.fileGrader.checkFileExists('src/pages/callback.tsx'); + if (!callbackExists.passed) { + // Try alternate location + checks.push(await this.fileGrader.checkFileExists('src/components/Callback.tsx')); + } else { + checks.push(callbackExists); + } + + // Check useAuth hook usage somewhere + checks.push(...(await this.fileGrader.checkFileContains('src/App.tsx', ['useAuth']))); + + // Check environment config + checks.push( + ...(await this.fileGrader.checkFileContains('src/main.tsx', [ + /VITE_WORKOS_CLIENT_ID|import\.meta\.env/, + ])) + ); + + // Check build succeeds + checks.push(await this.buildGrader.checkBuild()); + + return { + passed: checks.every((c) => c.passed), + checks, + }; + } +} diff --git a/tests/evals/graders/tanstack.grader.ts b/tests/evals/graders/tanstack.grader.ts new file mode 100644 index 0000000..182acba --- /dev/null +++ b/tests/evals/graders/tanstack.grader.ts @@ -0,0 +1,78 @@ +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +export class TanstackGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + + constructor(workDir: string) { + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const checks: GradeCheck[] = []; + + // Check auth callback route + const callbackRoute = await this.fileGrader.checkFileExists('app/routes/auth/callback.tsx'); + if (!callbackRoute.passed) { + // Try alternate pattern + checks.push(await this.fileGrader.checkFileExists('app/routes/callback.tsx')); + } else { + checks.push(callbackRoute); + } + + // Check server functions for auth + const serverAuth = await this.fileGrader.checkFileExists('app/server/auth.ts'); + if (!serverAuth.passed) { + // Try alternate locations + checks.push(await this.fileGrader.checkFileExists('app/lib/auth.ts')); + } else { + checks.push(serverAuth); + } + + // Check auth server function content + const authContent = await this.checkAuthServerContent(); + checks.push(...authContent); + + // Check provider setup in root + const rootProviderChecks = await this.fileGrader.checkFileContains('app/routes/__root.tsx', [ + 'AuthKitProvider', + ]); + if (!rootProviderChecks.every((c) => c.passed)) { + // Try alternate root location + checks.push( + ...(await this.fileGrader.checkFileContains('app/root.tsx', ['AuthKitProvider'])) + ); + } else { + checks.push(...rootProviderChecks); + } + + // Check build succeeds + checks.push(await this.buildGrader.checkBuild()); + + return { + passed: checks.every((c) => c.passed), + checks, + }; + } + + private async checkAuthServerContent(): Promise { + // Try primary location + const primaryChecks = await this.fileGrader.checkFileContains('app/server/auth.ts', [ + '@workos-inc/authkit', + 'createServerFn', + ]); + + if (primaryChecks.every((c) => c.passed)) { + return primaryChecks; + } + + // Try alternate location + return this.fileGrader.checkFileContains('app/lib/auth.ts', [ + '@workos-inc/authkit', + 'createServerFn', + ]); + } +} diff --git a/tests/evals/graders/vanilla.grader.ts b/tests/evals/graders/vanilla.grader.ts new file mode 100644 index 0000000..5fa2adf --- /dev/null +++ b/tests/evals/graders/vanilla.grader.ts @@ -0,0 +1,59 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { FileGrader } from './file-grader.js'; +import { BuildGrader } from './build-grader.js'; +import type { Grader, GradeResult, GradeCheck } from '../types.js'; + +export class VanillaGrader implements Grader { + private fileGrader: FileGrader; + private buildGrader: BuildGrader; + private workDir: string; + + constructor(workDir: string) { + this.workDir = workDir; + this.fileGrader = new FileGrader(workDir); + this.buildGrader = new BuildGrader(workDir); + } + + async grade(): Promise { + const checks: GradeCheck[] = []; + + // Check callback page exists + const callbackHtml = await this.fileGrader.checkFileExists('callback.html'); + const callbackJs = await this.fileGrader.checkFileExists('callback.js'); + checks.push(callbackHtml.passed ? callbackHtml : callbackJs); + + // Check auth script + checks.push(await this.fileGrader.checkFileExists('auth.js')); + + // Check auth script content + checks.push( + ...(await this.fileGrader.checkFileContains('auth.js', ['workos', 'getAuthorizationUrl'])) + ); + + // Check index.html includes auth + checks.push(...(await this.fileGrader.checkFileContains('index.html', [/auth\.js|workos/i]))); + + // Vanilla JS may not have build step - check if build script exists + const hasBuildScript = await this.checkHasBuildScript(); + if (hasBuildScript) { + checks.push(await this.buildGrader.checkBuild()); + } + + return { + passed: checks.every((c) => c.passed), + checks, + }; + } + + private async checkHasBuildScript(): Promise { + try { + const pkgPath = join(this.workDir, 'package.json'); + const content = await readFile(pkgPath, 'utf-8'); + const pkg = JSON.parse(content); + return !!pkg.scripts?.build; + } catch { + return false; + } + } +} diff --git a/tests/evals/index.ts b/tests/evals/index.ts index f2b76bd..3df7b33 100644 --- a/tests/evals/index.ts +++ b/tests/evals/index.ts @@ -1,23 +1,25 @@ #!/usr/bin/env node +import { parseArgs, printHelp } from './cli.js'; import { runEvals } from './runner.js'; +import { printMatrix, printJson } from './reporter.js'; async function main() { - const args = process.argv.slice(2); + const options = parseArgs(process.argv.slice(2)); - // Parse basic flags (expand in Phase 2) - const options = { - framework: args.find((a) => a.startsWith('--framework='))?.split('=')[1], - verbose: args.includes('--verbose'), - }; + if (options.help) { + printHelp(); + process.exit(0); + } try { const results = await runEvals(options); - // Print summary - const passed = results.filter((r) => r.passed).length; - console.log(`\n${passed}/${results.length} scenarios passed`); + if (options.json) { + printJson(results); + } else { + printMatrix(results); + } - // Exit code process.exit(results.every((r) => r.passed) ? 0 : 1); } catch (error) { console.error('Eval failed:', error); diff --git a/tests/evals/reporter.ts b/tests/evals/reporter.ts new file mode 100644 index 0000000..cae1cd9 --- /dev/null +++ b/tests/evals/reporter.ts @@ -0,0 +1,61 @@ +import type { EvalResult } from './types.js'; + +const FRAMEWORKS = ['nextjs', 'react', 'react-router', 'tanstack-start', 'vanilla-js']; +const STATES = ['fresh', 'existing', 'existing-auth0']; + +export function printMatrix(results: EvalResult[]): void { + const resultMap = new Map(results.map((r) => [r.scenario, r])); + + // Header + console.log('\n┌─────────────────┬─────────┬──────────┬───────────────┐'); + console.log('│ Framework │ Fresh │ Existing │ Existing+Auth │'); + console.log('├─────────────────┼─────────┼──────────┼───────────────┤'); + + for (const framework of FRAMEWORKS) { + const cells = STATES.map((state) => { + const key = `${framework}/${state}`; + const result = resultMap.get(key); + if (!result) return ' - '; + return result.passed ? ' ✓ ' : ' ✗ '; + }); + + const name = framework.padEnd(15); + console.log(`│ ${name} │${cells[0]}│${cells[1]} │${cells[2]} │`); + } + + console.log('└─────────────────┴─────────┴──────────┴───────────────┘'); + + // Summary + const passed = results.filter((r) => r.passed).length; + const total = results.length; + const rate = ((passed / total) * 100).toFixed(1); + console.log(`\nResults: ${passed}/${total} passed (${rate}%)`); + + if (passed < total) { + console.log('\nFailed scenarios:'); + for (const result of results.filter((r) => !r.passed)) { + console.log(` • ${result.scenario}`); + if (result.error) { + console.log(` Error: ${result.error}`); + } else if (result.checks) { + for (const check of result.checks.filter((c) => !c.passed)) { + console.log(` - ${check.name}: ${check.message}`); + } + } + } + } +} + +export function printJson(results: EvalResult[]): void { + const output = { + timestamp: new Date().toISOString(), + summary: { + total: results.length, + passed: results.filter((r) => r.passed).length, + failed: results.filter((r) => !r.passed).length, + passRate: results.filter((r) => r.passed).length / results.length, + }, + results, + }; + console.log(JSON.stringify(output, null, 2)); +} diff --git a/tests/evals/runner.ts b/tests/evals/runner.ts index 845e503..3bc7330 100644 --- a/tests/evals/runner.ts +++ b/tests/evals/runner.ts @@ -1,6 +1,10 @@ import { FixtureManager } from './fixture-manager.js'; import { AgentExecutor } from './agent-executor.js'; import { NextjsGrader } from './graders/nextjs.grader.js'; +import { ReactGrader } from './graders/react.grader.js'; +import { ReactRouterGrader } from './graders/react-router.grader.js'; +import { TanstackGrader } from './graders/tanstack.grader.js'; +import { VanillaGrader } from './graders/vanilla.grader.js'; import type { EvalResult, EvalOptions, Grader } from './types.js'; interface Scenario { @@ -10,15 +14,39 @@ interface Scenario { } const SCENARIOS: Scenario[] = [ + // Next.js { framework: 'nextjs', state: 'fresh', grader: NextjsGrader }, - // More scenarios added in Phase 2 + { framework: 'nextjs', state: 'existing', grader: NextjsGrader }, + { framework: 'nextjs', state: 'existing-auth0', grader: NextjsGrader }, + + // React SPA + { framework: 'react', state: 'fresh', grader: ReactGrader }, + { framework: 'react', state: 'existing', grader: ReactGrader }, + { framework: 'react', state: 'existing-auth0', grader: ReactGrader }, + + // React Router + { framework: 'react-router', state: 'fresh', grader: ReactRouterGrader }, + { framework: 'react-router', state: 'existing', grader: ReactRouterGrader }, + { framework: 'react-router', state: 'existing-auth0', grader: ReactRouterGrader }, + + // TanStack Start + { framework: 'tanstack-start', state: 'fresh', grader: TanstackGrader }, + { framework: 'tanstack-start', state: 'existing', grader: TanstackGrader }, + { framework: 'tanstack-start', state: 'existing-auth0', grader: TanstackGrader }, + + // Vanilla JS + { framework: 'vanilla-js', state: 'fresh', grader: VanillaGrader }, + { framework: 'vanilla-js', state: 'existing', grader: VanillaGrader }, + { framework: 'vanilla-js', state: 'existing-auth0', grader: VanillaGrader }, ]; export async function runEvals(options: EvalOptions): Promise { const results: EvalResult[] = []; const scenarios = SCENARIOS.filter( - (s) => !options.framework || s.framework === options.framework + (s) => + (!options.framework || s.framework === options.framework) && + (!options.state || s.state === options.state) ); for (const scenario of scenarios) { diff --git a/tests/fixtures/nextjs/existing-auth0/.env.example b/tests/fixtures/nextjs/existing-auth0/.env.example new file mode 100644 index 0000000..6d28d93 --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/.env.example @@ -0,0 +1,5 @@ +AUTH0_SECRET='use-a-long-random-string' +AUTH0_BASE_URL='http://localhost:3000' +AUTH0_ISSUER_BASE_URL='https://your-tenant.auth0.com' +AUTH0_CLIENT_ID='your-client-id' +AUTH0_CLIENT_SECRET='your-client-secret' diff --git a/tests/fixtures/nextjs/existing-auth0/app/about/page.tsx b/tests/fixtures/nextjs/existing-auth0/app/about/page.tsx new file mode 100644 index 0000000..48cc7a4 --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/app/about/page.tsx @@ -0,0 +1,8 @@ +export default function About() { + return ( +
+

About

+

This is an existing Next.js application with Auth0.

+
+ ); +} diff --git a/tests/fixtures/nextjs/existing-auth0/app/api/auth/[auth0]/route.ts b/tests/fixtures/nextjs/existing-auth0/app/api/auth/[auth0]/route.ts new file mode 100644 index 0000000..1d2e4e4 --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/app/api/auth/[auth0]/route.ts @@ -0,0 +1,3 @@ +import { handleAuth } from '@auth0/nextjs-auth0'; + +export const GET = handleAuth(); diff --git a/tests/fixtures/nextjs/existing-auth0/app/dashboard/page.tsx b/tests/fixtures/nextjs/existing-auth0/app/dashboard/page.tsx new file mode 100644 index 0000000..0917d50 --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/app/dashboard/page.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { useUser } from '@auth0/nextjs-auth0/client'; + +export default function Dashboard() { + const { user, isLoading, error } = useUser(); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + if (!user) { + return ( +
+

Dashboard

+

Please log in to view the dashboard.

+
+ ); + } + + return ( +
+

Dashboard

+

Welcome, {user.name}!

+
+ ); +} diff --git a/tests/fixtures/nextjs/existing-auth0/app/layout.tsx b/tests/fixtures/nextjs/existing-auth0/app/layout.tsx new file mode 100644 index 0000000..947036b --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/app/layout.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link'; +import { UserProvider } from '@auth0/nextjs-auth0/client'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + {children} + + + + ); +} diff --git a/tests/fixtures/nextjs/existing-auth0/app/page.tsx b/tests/fixtures/nextjs/existing-auth0/app/page.tsx new file mode 100644 index 0000000..12c593e --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/app/page.tsx @@ -0,0 +1,8 @@ +export default function Home() { + return ( +
+

Home

+

Welcome to the home page.

+
+ ); +} diff --git a/tests/fixtures/nextjs/existing-auth0/next.config.mjs b/tests/fixtures/nextjs/existing-auth0/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/tests/fixtures/nextjs/existing-auth0/package.json b/tests/fixtures/nextjs/existing-auth0/package.json new file mode 100644 index 0000000..5d2912f --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/package.json @@ -0,0 +1,22 @@ +{ + "name": "nextjs-existing-auth0-fixture", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@auth0/nextjs-auth0": "^3.5.0", + "next": "^14.2.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "typescript": "^5.4.0" + } +} diff --git a/tests/fixtures/nextjs/existing-auth0/tsconfig.json b/tests/fixtures/nextjs/existing-auth0/tsconfig.json new file mode 100644 index 0000000..e7ff90f --- /dev/null +++ b/tests/fixtures/nextjs/existing-auth0/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/tests/fixtures/nextjs/existing/app/about/page.tsx b/tests/fixtures/nextjs/existing/app/about/page.tsx new file mode 100644 index 0000000..b680e06 --- /dev/null +++ b/tests/fixtures/nextjs/existing/app/about/page.tsx @@ -0,0 +1,8 @@ +export default function About() { + return ( +
+

About

+

This is an existing Next.js application.

+
+ ); +} diff --git a/tests/fixtures/nextjs/existing/app/api/hello/route.ts b/tests/fixtures/nextjs/existing/app/api/hello/route.ts new file mode 100644 index 0000000..6c6a098 --- /dev/null +++ b/tests/fixtures/nextjs/existing/app/api/hello/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ message: 'Hello, World!' }); +} diff --git a/tests/fixtures/nextjs/existing/app/dashboard/page.tsx b/tests/fixtures/nextjs/existing/app/dashboard/page.tsx new file mode 100644 index 0000000..3481984 --- /dev/null +++ b/tests/fixtures/nextjs/existing/app/dashboard/page.tsx @@ -0,0 +1,8 @@ +export default function Dashboard() { + return ( +
+

Dashboard

+

Protected content would go here.

+
+ ); +} diff --git a/tests/fixtures/nextjs/existing/app/layout.tsx b/tests/fixtures/nextjs/existing/app/layout.tsx new file mode 100644 index 0000000..0cf48dd --- /dev/null +++ b/tests/fixtures/nextjs/existing/app/layout.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + ); +} diff --git a/tests/fixtures/nextjs/existing/app/page.tsx b/tests/fixtures/nextjs/existing/app/page.tsx new file mode 100644 index 0000000..12c593e --- /dev/null +++ b/tests/fixtures/nextjs/existing/app/page.tsx @@ -0,0 +1,8 @@ +export default function Home() { + return ( +
+

Home

+

Welcome to the home page.

+
+ ); +} diff --git a/tests/fixtures/nextjs/existing/next.config.mjs b/tests/fixtures/nextjs/existing/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/tests/fixtures/nextjs/existing/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/tests/fixtures/nextjs/existing/package.json b/tests/fixtures/nextjs/existing/package.json new file mode 100644 index 0000000..e5de786 --- /dev/null +++ b/tests/fixtures/nextjs/existing/package.json @@ -0,0 +1,21 @@ +{ + "name": "nextjs-existing-fixture", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^14.2.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "typescript": "^5.4.0" + } +} diff --git a/tests/fixtures/nextjs/existing/tsconfig.json b/tests/fixtures/nextjs/existing/tsconfig.json new file mode 100644 index 0000000..e7ff90f --- /dev/null +++ b/tests/fixtures/nextjs/existing/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/tests/fixtures/react-router/existing-auth0/.env.example b/tests/fixtures/react-router/existing-auth0/.env.example new file mode 100644 index 0000000..6a3c194 --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/.env.example @@ -0,0 +1,2 @@ +VITE_AUTH0_DOMAIN=your-tenant.auth0.com +VITE_AUTH0_CLIENT_ID=your-client-id diff --git a/tests/fixtures/react-router/existing-auth0/app/root.tsx b/tests/fixtures/react-router/existing-auth0/app/root.tsx new file mode 100644 index 0000000..637ffd6 --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/app/root.tsx @@ -0,0 +1,36 @@ +import { Links, Meta, NavLink, Outlet, Scripts, ScrollRestoration } from 'react-router'; +import { Auth0Provider } from '@auth0/auth0-react'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + + {children} + + + + + + ); +} + +export default function Root() { + return ; +} diff --git a/tests/fixtures/react-router/existing-auth0/app/routes.ts b/tests/fixtures/react-router/existing-auth0/app/routes.ts new file mode 100644 index 0000000..3e22bc7 --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/app/routes.ts @@ -0,0 +1,7 @@ +import { type RouteConfig, index, route } from '@react-router/dev/routes'; + +export default [ + index('routes/home.tsx'), + route('about', 'routes/about.tsx'), + route('dashboard', 'routes/dashboard.tsx'), +] satisfies RouteConfig; diff --git a/tests/fixtures/react-router/existing-auth0/app/routes/about.tsx b/tests/fixtures/react-router/existing-auth0/app/routes/about.tsx new file mode 100644 index 0000000..7e1421d --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/app/routes/about.tsx @@ -0,0 +1,8 @@ +export default function About() { + return ( +
+

About

+

This is an existing React Router application with Auth0.

+
+ ); +} diff --git a/tests/fixtures/react-router/existing-auth0/app/routes/dashboard.tsx b/tests/fixtures/react-router/existing-auth0/app/routes/dashboard.tsx new file mode 100644 index 0000000..d6232ea --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/app/routes/dashboard.tsx @@ -0,0 +1,25 @@ +import { useAuth0 } from '@auth0/auth0-react'; + +export default function Dashboard() { + const { isAuthenticated, isLoading, user } = useAuth0(); + + if (isLoading) { + return
Loading...
; + } + + if (!isAuthenticated) { + return ( +
+

Dashboard

+

Please log in to view the dashboard.

+
+ ); + } + + return ( +
+

Dashboard

+

Welcome, {user?.name}!

+
+ ); +} diff --git a/tests/fixtures/react-router/existing-auth0/app/routes/home.tsx b/tests/fixtures/react-router/existing-auth0/app/routes/home.tsx new file mode 100644 index 0000000..505cc55 --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/app/routes/home.tsx @@ -0,0 +1,17 @@ +import { useAuth0 } from '@auth0/auth0-react'; + +export default function Home() { + const { isAuthenticated, loginWithRedirect, logout } = useAuth0(); + + return ( +
+

Home

+

Welcome to the home page.

+ {isAuthenticated ? ( + + ) : ( + + )} +
+ ); +} diff --git a/tests/fixtures/react-router/existing-auth0/package.json b/tests/fixtures/react-router/existing-auth0/package.json new file mode 100644 index 0000000..3e4e849 --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/package.json @@ -0,0 +1,26 @@ +{ + "name": "react-router-existing-auth0-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "react-router dev", + "build": "react-router build", + "start": "react-router-serve ./build/server/index.js" + }, + "dependencies": { + "@auth0/auth0-react": "^2.2.4", + "@react-router/node": "^7.1.1", + "@react-router/serve": "^7.1.1", + "isbot": "^5.1.17", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.1.1" + }, + "devDependencies": { + "@react-router/dev": "^7.1.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.2", + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/react-router/existing-auth0/react-router.config.ts b/tests/fixtures/react-router/existing-auth0/react-router.config.ts new file mode 100644 index 0000000..51e8967 --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + ssr: true, +} satisfies Config; diff --git a/tests/fixtures/react-router/existing-auth0/tsconfig.json b/tests/fixtures/react-router/existing-auth0/tsconfig.json new file mode 100644 index 0000000..b092946 --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": ["**/*", ".react-router/types/**/*"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true, + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + } + } +} diff --git a/tests/fixtures/react-router/existing-auth0/vite.config.ts b/tests/fixtures/react-router/existing-auth0/vite.config.ts new file mode 100644 index 0000000..aa78dec --- /dev/null +++ b/tests/fixtures/react-router/existing-auth0/vite.config.ts @@ -0,0 +1,12 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [reactRouter()], + resolve: { + alias: { + '~': resolve(__dirname, './app'), + }, + }, +}); diff --git a/tests/fixtures/react-router/existing/app/root.tsx b/tests/fixtures/react-router/existing/app/root.tsx new file mode 100644 index 0000000..5471bee --- /dev/null +++ b/tests/fixtures/react-router/existing/app/root.tsx @@ -0,0 +1,27 @@ +import { Links, Meta, NavLink, Outlet, Scripts, ScrollRestoration } from 'react-router'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + {children} + + + + + ); +} + +export default function Root() { + return ; +} diff --git a/tests/fixtures/react-router/existing/app/routes.ts b/tests/fixtures/react-router/existing/app/routes.ts new file mode 100644 index 0000000..3e22bc7 --- /dev/null +++ b/tests/fixtures/react-router/existing/app/routes.ts @@ -0,0 +1,7 @@ +import { type RouteConfig, index, route } from '@react-router/dev/routes'; + +export default [ + index('routes/home.tsx'), + route('about', 'routes/about.tsx'), + route('dashboard', 'routes/dashboard.tsx'), +] satisfies RouteConfig; diff --git a/tests/fixtures/react-router/existing/app/routes/about.tsx b/tests/fixtures/react-router/existing/app/routes/about.tsx new file mode 100644 index 0000000..31a98f4 --- /dev/null +++ b/tests/fixtures/react-router/existing/app/routes/about.tsx @@ -0,0 +1,8 @@ +export default function About() { + return ( +
+

About

+

This is an existing React Router application.

+
+ ); +} diff --git a/tests/fixtures/react-router/existing/app/routes/dashboard.tsx b/tests/fixtures/react-router/existing/app/routes/dashboard.tsx new file mode 100644 index 0000000..70f9ece --- /dev/null +++ b/tests/fixtures/react-router/existing/app/routes/dashboard.tsx @@ -0,0 +1,8 @@ +export default function Dashboard() { + return ( +
+

Dashboard

+

Protected content would go here.

+
+ ); +} diff --git a/tests/fixtures/react-router/existing/app/routes/home.tsx b/tests/fixtures/react-router/existing/app/routes/home.tsx new file mode 100644 index 0000000..219c3ec --- /dev/null +++ b/tests/fixtures/react-router/existing/app/routes/home.tsx @@ -0,0 +1,8 @@ +export default function Home() { + return ( +
+

Home

+

Welcome to the home page.

+
+ ); +} diff --git a/tests/fixtures/react-router/existing/package.json b/tests/fixtures/react-router/existing/package.json new file mode 100644 index 0000000..c690c67 --- /dev/null +++ b/tests/fixtures/react-router/existing/package.json @@ -0,0 +1,25 @@ +{ + "name": "react-router-existing-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "react-router dev", + "build": "react-router build", + "start": "react-router-serve ./build/server/index.js" + }, + "dependencies": { + "@react-router/node": "^7.1.1", + "@react-router/serve": "^7.1.1", + "isbot": "^5.1.17", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.1.1" + }, + "devDependencies": { + "@react-router/dev": "^7.1.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.2", + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/react-router/existing/react-router.config.ts b/tests/fixtures/react-router/existing/react-router.config.ts new file mode 100644 index 0000000..51e8967 --- /dev/null +++ b/tests/fixtures/react-router/existing/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + ssr: true, +} satisfies Config; diff --git a/tests/fixtures/react-router/existing/tsconfig.json b/tests/fixtures/react-router/existing/tsconfig.json new file mode 100644 index 0000000..b092946 --- /dev/null +++ b/tests/fixtures/react-router/existing/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": ["**/*", ".react-router/types/**/*"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true, + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + } + } +} diff --git a/tests/fixtures/react-router/existing/vite.config.ts b/tests/fixtures/react-router/existing/vite.config.ts new file mode 100644 index 0000000..aa78dec --- /dev/null +++ b/tests/fixtures/react-router/existing/vite.config.ts @@ -0,0 +1,12 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [reactRouter()], + resolve: { + alias: { + '~': resolve(__dirname, './app'), + }, + }, +}); diff --git a/tests/fixtures/react-router/fresh/app/root.tsx b/tests/fixtures/react-router/fresh/app/root.tsx new file mode 100644 index 0000000..dea8bc3 --- /dev/null +++ b/tests/fixtures/react-router/fresh/app/root.tsx @@ -0,0 +1,23 @@ +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function Root() { + return ; +} diff --git a/tests/fixtures/react-router/fresh/app/routes.ts b/tests/fixtures/react-router/fresh/app/routes.ts new file mode 100644 index 0000000..205ff3c --- /dev/null +++ b/tests/fixtures/react-router/fresh/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from '@react-router/dev/routes'; + +export default [index('routes/home.tsx')] satisfies RouteConfig; diff --git a/tests/fixtures/react-router/fresh/app/routes/home.tsx b/tests/fixtures/react-router/fresh/app/routes/home.tsx new file mode 100644 index 0000000..6e60f0f --- /dev/null +++ b/tests/fixtures/react-router/fresh/app/routes/home.tsx @@ -0,0 +1,8 @@ +export default function Home() { + return ( +
+

Welcome to React Router

+

This is a fresh React Router application.

+
+ ); +} diff --git a/tests/fixtures/react-router/fresh/package.json b/tests/fixtures/react-router/fresh/package.json new file mode 100644 index 0000000..398ca86 --- /dev/null +++ b/tests/fixtures/react-router/fresh/package.json @@ -0,0 +1,25 @@ +{ + "name": "react-router-fresh-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "react-router dev", + "build": "react-router build", + "start": "react-router-serve ./build/server/index.js" + }, + "dependencies": { + "@react-router/node": "^7.1.1", + "@react-router/serve": "^7.1.1", + "isbot": "^5.1.17", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.1.1" + }, + "devDependencies": { + "@react-router/dev": "^7.1.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.2", + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/react-router/fresh/react-router.config.ts b/tests/fixtures/react-router/fresh/react-router.config.ts new file mode 100644 index 0000000..51e8967 --- /dev/null +++ b/tests/fixtures/react-router/fresh/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + ssr: true, +} satisfies Config; diff --git a/tests/fixtures/react-router/fresh/tsconfig.json b/tests/fixtures/react-router/fresh/tsconfig.json new file mode 100644 index 0000000..d063681 --- /dev/null +++ b/tests/fixtures/react-router/fresh/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["**/*", ".react-router/types/**/*"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true, + "rootDirs": [".", "./.react-router/types"] + } +} diff --git a/tests/fixtures/react-router/fresh/vite.config.ts b/tests/fixtures/react-router/fresh/vite.config.ts new file mode 100644 index 0000000..68ba30d --- /dev/null +++ b/tests/fixtures/react-router/fresh/vite.config.ts @@ -0,0 +1,6 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [reactRouter()], +}); diff --git a/tests/fixtures/react/existing-auth0/.env.example b/tests/fixtures/react/existing-auth0/.env.example new file mode 100644 index 0000000..6a3c194 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/.env.example @@ -0,0 +1,2 @@ +VITE_AUTH0_DOMAIN=your-tenant.auth0.com +VITE_AUTH0_CLIENT_ID=your-client-id diff --git a/tests/fixtures/react/existing-auth0/index.html b/tests/fixtures/react/existing-auth0/index.html new file mode 100644 index 0000000..6fac552 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/index.html @@ -0,0 +1,12 @@ + + + + + + React App with Auth0 + + +
+ + + diff --git a/tests/fixtures/react/existing-auth0/package.json b/tests/fixtures/react/existing-auth0/package.json new file mode 100644 index 0000000..fed077b --- /dev/null +++ b/tests/fixtures/react/existing-auth0/package.json @@ -0,0 +1,24 @@ +{ + "name": "react-existing-auth0-fixture", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@auth0/auth0-react": "^2.2.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.6.2", + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/react/existing-auth0/src/App.tsx b/tests/fixtures/react/existing-auth0/src/App.tsx new file mode 100644 index 0000000..0d0d707 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/src/App.tsx @@ -0,0 +1,30 @@ +import { Routes, Route, Link } from 'react-router-dom'; +import { useAuth0 } from '@auth0/auth0-react'; +import { Home } from './pages/Home'; +import { About } from './pages/About'; +import { Dashboard } from './pages/Dashboard'; + +function App() { + const { isAuthenticated, loginWithRedirect, logout } = useAuth0(); + + return ( +
+ + + } /> + } /> + } /> + +
+ ); +} + +export default App; diff --git a/tests/fixtures/react/existing-auth0/src/main.tsx b/tests/fixtures/react/existing-auth0/src/main.tsx new file mode 100644 index 0000000..ce10051 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/src/main.tsx @@ -0,0 +1,21 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { Auth0Provider } from '@auth0/auth0-react'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + + + + + +); diff --git a/tests/fixtures/react/existing-auth0/src/pages/About.tsx b/tests/fixtures/react/existing-auth0/src/pages/About.tsx new file mode 100644 index 0000000..310df85 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/src/pages/About.tsx @@ -0,0 +1,8 @@ +export function About() { + return ( +
+

About

+

This is an existing React application with Auth0.

+
+ ); +} diff --git a/tests/fixtures/react/existing-auth0/src/pages/Dashboard.tsx b/tests/fixtures/react/existing-auth0/src/pages/Dashboard.tsx new file mode 100644 index 0000000..6afddd8 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/src/pages/Dashboard.tsx @@ -0,0 +1,20 @@ +import { useAuth0 } from '@auth0/auth0-react'; + +export function Dashboard() { + const { isAuthenticated, isLoading, user } = useAuth0(); + + if (isLoading) { + return
Loading...
; + } + + if (!isAuthenticated) { + return
Please log in to view the dashboard.
; + } + + return ( +
+

Dashboard

+

Welcome, {user?.name}!

+
+ ); +} diff --git a/tests/fixtures/react/existing-auth0/src/pages/Home.tsx b/tests/fixtures/react/existing-auth0/src/pages/Home.tsx new file mode 100644 index 0000000..ed07ba1 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/src/pages/Home.tsx @@ -0,0 +1,8 @@ +export function Home() { + return ( +
+

Home

+

Welcome to the home page.

+
+ ); +} diff --git a/tests/fixtures/react/existing-auth0/src/vite-env.d.ts b/tests/fixtures/react/existing-auth0/src/vite-env.d.ts new file mode 100644 index 0000000..e882ef9 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_AUTH0_DOMAIN: string; + readonly VITE_AUTH0_CLIENT_ID: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/tests/fixtures/react/existing-auth0/tsconfig.json b/tests/fixtures/react/existing-auth0/tsconfig.json new file mode 100644 index 0000000..d541922 --- /dev/null +++ b/tests/fixtures/react/existing-auth0/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/tests/fixtures/react/existing-auth0/vite.config.ts b/tests/fixtures/react/existing-auth0/vite.config.ts new file mode 100644 index 0000000..d192dba --- /dev/null +++ b/tests/fixtures/react/existing-auth0/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, +}); diff --git a/tests/fixtures/react/existing/index.html b/tests/fixtures/react/existing/index.html new file mode 100644 index 0000000..5da656c --- /dev/null +++ b/tests/fixtures/react/existing/index.html @@ -0,0 +1,12 @@ + + + + + + React App + + +
+ + + diff --git a/tests/fixtures/react/existing/package.json b/tests/fixtures/react/existing/package.json new file mode 100644 index 0000000..ab4bf32 --- /dev/null +++ b/tests/fixtures/react/existing/package.json @@ -0,0 +1,23 @@ +{ + "name": "react-existing-fixture", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.6.2", + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/react/existing/src/App.tsx b/tests/fixtures/react/existing/src/App.tsx new file mode 100644 index 0000000..b6b0d61 --- /dev/null +++ b/tests/fixtures/react/existing/src/App.tsx @@ -0,0 +1,22 @@ +import { Routes, Route, Link } from 'react-router-dom'; +import { Home } from './pages/Home'; +import { About } from './pages/About'; +import { Dashboard } from './pages/Dashboard'; + +function App() { + return ( +
+ + + } /> + } /> + } /> + +
+ ); +} + +export default App; diff --git a/tests/fixtures/react/existing/src/main.tsx b/tests/fixtures/react/existing/src/main.tsx new file mode 100644 index 0000000..a127f29 --- /dev/null +++ b/tests/fixtures/react/existing/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + + + +); diff --git a/tests/fixtures/react/existing/src/pages/About.tsx b/tests/fixtures/react/existing/src/pages/About.tsx new file mode 100644 index 0000000..9c57a60 --- /dev/null +++ b/tests/fixtures/react/existing/src/pages/About.tsx @@ -0,0 +1,8 @@ +export function About() { + return ( +
+

About

+

This is an existing React application.

+
+ ); +} diff --git a/tests/fixtures/react/existing/src/pages/Dashboard.tsx b/tests/fixtures/react/existing/src/pages/Dashboard.tsx new file mode 100644 index 0000000..42409fd --- /dev/null +++ b/tests/fixtures/react/existing/src/pages/Dashboard.tsx @@ -0,0 +1,8 @@ +export function Dashboard() { + return ( +
+

Dashboard

+

Protected content would go here.

+
+ ); +} diff --git a/tests/fixtures/react/existing/src/pages/Home.tsx b/tests/fixtures/react/existing/src/pages/Home.tsx new file mode 100644 index 0000000..ed07ba1 --- /dev/null +++ b/tests/fixtures/react/existing/src/pages/Home.tsx @@ -0,0 +1,8 @@ +export function Home() { + return ( +
+

Home

+

Welcome to the home page.

+
+ ); +} diff --git a/tests/fixtures/react/existing/src/vite-env.d.ts b/tests/fixtures/react/existing/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/tests/fixtures/react/existing/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tests/fixtures/react/existing/tsconfig.json b/tests/fixtures/react/existing/tsconfig.json new file mode 100644 index 0000000..d541922 --- /dev/null +++ b/tests/fixtures/react/existing/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/tests/fixtures/react/existing/vite.config.ts b/tests/fixtures/react/existing/vite.config.ts new file mode 100644 index 0000000..d192dba --- /dev/null +++ b/tests/fixtures/react/existing/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, +}); diff --git a/tests/fixtures/react/fresh/index.html b/tests/fixtures/react/fresh/index.html new file mode 100644 index 0000000..5da656c --- /dev/null +++ b/tests/fixtures/react/fresh/index.html @@ -0,0 +1,12 @@ + + + + + + React App + + +
+ + + diff --git a/tests/fixtures/react/fresh/package.json b/tests/fixtures/react/fresh/package.json new file mode 100644 index 0000000..b24e566 --- /dev/null +++ b/tests/fixtures/react/fresh/package.json @@ -0,0 +1,22 @@ +{ + "name": "react-fresh-fixture", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.6.2", + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/react/fresh/src/App.tsx b/tests/fixtures/react/fresh/src/App.tsx new file mode 100644 index 0000000..725bea2 --- /dev/null +++ b/tests/fixtures/react/fresh/src/App.tsx @@ -0,0 +1,10 @@ +function App() { + return ( +
+

React App

+

Welcome to your React application.

+
+ ); +} + +export default App; diff --git a/tests/fixtures/react/fresh/src/main.tsx b/tests/fixtures/react/fresh/src/main.tsx new file mode 100644 index 0000000..3a8bd35 --- /dev/null +++ b/tests/fixtures/react/fresh/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/tests/fixtures/react/fresh/src/vite-env.d.ts b/tests/fixtures/react/fresh/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/tests/fixtures/react/fresh/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tests/fixtures/react/fresh/tsconfig.json b/tests/fixtures/react/fresh/tsconfig.json new file mode 100644 index 0000000..109f0ac --- /dev/null +++ b/tests/fixtures/react/fresh/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tests/fixtures/react/fresh/vite.config.ts b/tests/fixtures/react/fresh/vite.config.ts new file mode 100644 index 0000000..0466183 --- /dev/null +++ b/tests/fixtures/react/fresh/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/tests/fixtures/tanstack-start/existing-auth0/.env.example b/tests/fixtures/tanstack-start/existing-auth0/.env.example new file mode 100644 index 0000000..6a3c194 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/.env.example @@ -0,0 +1,2 @@ +VITE_AUTH0_DOMAIN=your-tenant.auth0.com +VITE_AUTH0_CLIENT_ID=your-client-id diff --git a/tests/fixtures/tanstack-start/existing-auth0/app.config.ts b/tests/fixtures/tanstack-start/existing-auth0/app.config.ts new file mode 100644 index 0000000..ac46b90 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from '@tanstack/start/config'; + +export default defineConfig({}); diff --git a/tests/fixtures/tanstack-start/existing-auth0/app/client.tsx b/tests/fixtures/tanstack-start/existing-auth0/app/client.tsx new file mode 100644 index 0000000..0af9ea0 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app/client.tsx @@ -0,0 +1,7 @@ +import { StartClient } from '@tanstack/start'; +import { hydrateRoot } from 'react-dom/client'; +import { createRouter } from './router'; + +const router = createRouter(); + +hydrateRoot(document, ); diff --git a/tests/fixtures/tanstack-start/existing-auth0/app/router.tsx b/tests/fixtures/tanstack-start/existing-auth0/app/router.tsx new file mode 100644 index 0000000..4487607 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app/router.tsx @@ -0,0 +1,16 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + }); + + return router; +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType; + } +} diff --git a/tests/fixtures/tanstack-start/existing-auth0/app/routes/__root.tsx b/tests/fixtures/tanstack-start/existing-auth0/app/routes/__root.tsx new file mode 100644 index 0000000..c270a51 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app/routes/__root.tsx @@ -0,0 +1,33 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router'; +import { Auth0Provider } from '@auth0/auth0-react'; + +export const Route = createRootRoute({ + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + + TanStack Start with Auth0 + + + + + + + + + ); +} diff --git a/tests/fixtures/tanstack-start/existing-auth0/app/routes/about.tsx b/tests/fixtures/tanstack-start/existing-auth0/app/routes/about.tsx new file mode 100644 index 0000000..196b6a6 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/about')({ + component: About, +}); + +function About() { + return ( +
+

About

+

This is an existing TanStack Start application with Auth0.

+
+ ); +} diff --git a/tests/fixtures/tanstack-start/existing-auth0/app/routes/dashboard.tsx b/tests/fixtures/tanstack-start/existing-auth0/app/routes/dashboard.tsx new file mode 100644 index 0000000..ec67bb1 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app/routes/dashboard.tsx @@ -0,0 +1,30 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { useAuth0 } from '@auth0/auth0-react'; + +export const Route = createFileRoute('/dashboard')({ + component: Dashboard, +}); + +function Dashboard() { + const { isAuthenticated, isLoading, user } = useAuth0(); + + if (isLoading) { + return
Loading...
; + } + + if (!isAuthenticated) { + return ( +
+

Dashboard

+

Please log in to view the dashboard.

+
+ ); + } + + return ( +
+

Dashboard

+

Welcome, {user?.name}!

+
+ ); +} diff --git a/tests/fixtures/tanstack-start/existing-auth0/app/routes/index.tsx b/tests/fixtures/tanstack-start/existing-auth0/app/routes/index.tsx new file mode 100644 index 0000000..cf655ff --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app/routes/index.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { useAuth0 } from '@auth0/auth0-react'; + +export const Route = createFileRoute('/')({ + component: Home, +}); + +function Home() { + const { isAuthenticated, loginWithRedirect, logout } = useAuth0(); + + return ( +
+

Home

+

Welcome to the home page.

+ {isAuthenticated ? ( + + ) : ( + + )} +
+ ); +} diff --git a/tests/fixtures/tanstack-start/existing-auth0/app/ssr.tsx b/tests/fixtures/tanstack-start/existing-auth0/app/ssr.tsx new file mode 100644 index 0000000..2a5ee26 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/app/ssr.tsx @@ -0,0 +1,6 @@ +import { createStartHandler, defaultStreamHandler } from '@tanstack/start/server'; +import { createRouter } from './router'; + +export default createStartHandler({ + createRouter, +})(defaultStreamHandler); diff --git a/tests/fixtures/tanstack-start/existing-auth0/package.json b/tests/fixtures/tanstack-start/existing-auth0/package.json new file mode 100644 index 0000000..de863b8 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/package.json @@ -0,0 +1,24 @@ +{ + "name": "tanstack-start-existing-auth0-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@auth0/auth0-react": "^2.2.4", + "@tanstack/react-router": "^1.95.1", + "@tanstack/start": "^1.95.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "vinxi": "^0.5.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.2" + } +} diff --git a/tests/fixtures/tanstack-start/existing-auth0/tsconfig.json b/tests/fixtures/tanstack-start/existing-auth0/tsconfig.json new file mode 100644 index 0000000..5125a86 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing-auth0/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "~/*": ["./app/*"] + } + }, + "include": ["app"] +} diff --git a/tests/fixtures/tanstack-start/existing/app.config.ts b/tests/fixtures/tanstack-start/existing/app.config.ts new file mode 100644 index 0000000..ac46b90 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from '@tanstack/start/config'; + +export default defineConfig({}); diff --git a/tests/fixtures/tanstack-start/existing/app/client.tsx b/tests/fixtures/tanstack-start/existing/app/client.tsx new file mode 100644 index 0000000..0af9ea0 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app/client.tsx @@ -0,0 +1,7 @@ +import { StartClient } from '@tanstack/start'; +import { hydrateRoot } from 'react-dom/client'; +import { createRouter } from './router'; + +const router = createRouter(); + +hydrateRoot(document, ); diff --git a/tests/fixtures/tanstack-start/existing/app/router.tsx b/tests/fixtures/tanstack-start/existing/app/router.tsx new file mode 100644 index 0000000..4487607 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app/router.tsx @@ -0,0 +1,16 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + }); + + return router; +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType; + } +} diff --git a/tests/fixtures/tanstack-start/existing/app/routes/__root.tsx b/tests/fixtures/tanstack-start/existing/app/routes/__root.tsx new file mode 100644 index 0000000..d6ab330 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app/routes/__root.tsx @@ -0,0 +1,24 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router'; + +export const Route = createRootRoute({ + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + + TanStack Start + + + + + + + ); +} diff --git a/tests/fixtures/tanstack-start/existing/app/routes/about.tsx b/tests/fixtures/tanstack-start/existing/app/routes/about.tsx new file mode 100644 index 0000000..54c6263 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/about')({ + component: About, +}); + +function About() { + return ( +
+

About

+

This is an existing TanStack Start application.

+
+ ); +} diff --git a/tests/fixtures/tanstack-start/existing/app/routes/dashboard.tsx b/tests/fixtures/tanstack-start/existing/app/routes/dashboard.tsx new file mode 100644 index 0000000..4456b66 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app/routes/dashboard.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/dashboard')({ + component: Dashboard, +}); + +function Dashboard() { + return ( +
+

Dashboard

+

Protected content would go here.

+
+ ); +} diff --git a/tests/fixtures/tanstack-start/existing/app/routes/index.tsx b/tests/fixtures/tanstack-start/existing/app/routes/index.tsx new file mode 100644 index 0000000..a3fba46 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/')({ + component: Home, +}); + +function Home() { + return ( +
+

Home

+

Welcome to the home page.

+
+ ); +} diff --git a/tests/fixtures/tanstack-start/existing/app/ssr.tsx b/tests/fixtures/tanstack-start/existing/app/ssr.tsx new file mode 100644 index 0000000..2a5ee26 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/app/ssr.tsx @@ -0,0 +1,6 @@ +import { createStartHandler, defaultStreamHandler } from '@tanstack/start/server'; +import { createRouter } from './router'; + +export default createStartHandler({ + createRouter, +})(defaultStreamHandler); diff --git a/tests/fixtures/tanstack-start/existing/package.json b/tests/fixtures/tanstack-start/existing/package.json new file mode 100644 index 0000000..9d207e5 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-start-existing-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@tanstack/react-router": "^1.95.1", + "@tanstack/start": "^1.95.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "vinxi": "^0.5.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.2" + } +} diff --git a/tests/fixtures/tanstack-start/existing/tsconfig.json b/tests/fixtures/tanstack-start/existing/tsconfig.json new file mode 100644 index 0000000..5125a86 --- /dev/null +++ b/tests/fixtures/tanstack-start/existing/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "~/*": ["./app/*"] + } + }, + "include": ["app"] +} diff --git a/tests/fixtures/tanstack-start/fresh/app.config.ts b/tests/fixtures/tanstack-start/fresh/app.config.ts new file mode 100644 index 0000000..ac46b90 --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/app.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from '@tanstack/start/config'; + +export default defineConfig({}); diff --git a/tests/fixtures/tanstack-start/fresh/app/client.tsx b/tests/fixtures/tanstack-start/fresh/app/client.tsx new file mode 100644 index 0000000..0af9ea0 --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/app/client.tsx @@ -0,0 +1,7 @@ +import { StartClient } from '@tanstack/start'; +import { hydrateRoot } from 'react-dom/client'; +import { createRouter } from './router'; + +const router = createRouter(); + +hydrateRoot(document, ); diff --git a/tests/fixtures/tanstack-start/fresh/app/router.tsx b/tests/fixtures/tanstack-start/fresh/app/router.tsx new file mode 100644 index 0000000..4487607 --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/app/router.tsx @@ -0,0 +1,16 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + }); + + return router; +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType; + } +} diff --git a/tests/fixtures/tanstack-start/fresh/app/routes/__root.tsx b/tests/fixtures/tanstack-start/fresh/app/routes/__root.tsx new file mode 100644 index 0000000..914c089 --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/app/routes/__root.tsx @@ -0,0 +1,20 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router'; + +export const Route = createRootRoute({ + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + + TanStack Start + + + + + + ); +} diff --git a/tests/fixtures/tanstack-start/fresh/app/routes/index.tsx b/tests/fixtures/tanstack-start/fresh/app/routes/index.tsx new file mode 100644 index 0000000..f0d362c --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/app/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/')({ + component: Home, +}); + +function Home() { + return ( +
+

Welcome to TanStack Start

+

This is a fresh TanStack Start application.

+
+ ); +} diff --git a/tests/fixtures/tanstack-start/fresh/app/ssr.tsx b/tests/fixtures/tanstack-start/fresh/app/ssr.tsx new file mode 100644 index 0000000..2a5ee26 --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/app/ssr.tsx @@ -0,0 +1,6 @@ +import { createStartHandler, defaultStreamHandler } from '@tanstack/start/server'; +import { createRouter } from './router'; + +export default createStartHandler({ + createRouter, +})(defaultStreamHandler); diff --git a/tests/fixtures/tanstack-start/fresh/package.json b/tests/fixtures/tanstack-start/fresh/package.json new file mode 100644 index 0000000..518bb25 --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-start-fresh-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@tanstack/react-router": "^1.95.1", + "@tanstack/start": "^1.95.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "vinxi": "^0.5.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.2" + } +} diff --git a/tests/fixtures/tanstack-start/fresh/tsconfig.json b/tests/fixtures/tanstack-start/fresh/tsconfig.json new file mode 100644 index 0000000..23e2bdb --- /dev/null +++ b/tests/fixtures/tanstack-start/fresh/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["app"] +} diff --git a/tests/fixtures/vanilla-js/existing-auth0/.env.example b/tests/fixtures/vanilla-js/existing-auth0/.env.example new file mode 100644 index 0000000..6a3c194 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/.env.example @@ -0,0 +1,2 @@ +VITE_AUTH0_DOMAIN=your-tenant.auth0.com +VITE_AUTH0_CLIENT_ID=your-client-id diff --git a/tests/fixtures/vanilla-js/existing-auth0/about.html b/tests/fixtures/vanilla-js/existing-auth0/about.html new file mode 100644 index 0000000..a5ac958 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/about.html @@ -0,0 +1,23 @@ + + + + + + About - Vanilla JS App + + + + +
+

About

+

This is an existing vanilla JavaScript application with Auth0.

+
+ + + diff --git a/tests/fixtures/vanilla-js/existing-auth0/dashboard.html b/tests/fixtures/vanilla-js/existing-auth0/dashboard.html new file mode 100644 index 0000000..543e3be --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/dashboard.html @@ -0,0 +1,25 @@ + + + + + + Dashboard - Vanilla JS App + + + + +
+

Dashboard

+
+

Loading...

+
+
+ + + diff --git a/tests/fixtures/vanilla-js/existing-auth0/dashboard.js b/tests/fixtures/vanilla-js/existing-auth0/dashboard.js new file mode 100644 index 0000000..9f257a1 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/dashboard.js @@ -0,0 +1,55 @@ +import { createAuth0Client } from '@auth0/auth0-spa-js'; + +async function initDashboard() { + const auth0Client = await createAuth0Client({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + }); + + const isAuthenticated = await auth0Client.isAuthenticated(); + const content = document.getElementById('dashboard-content'); + + // Update nav buttons + const loginBtn = document.getElementById('login-btn'); + const logoutBtn = document.getElementById('logout-btn'); + + if (loginBtn && logoutBtn) { + loginBtn.style.display = isAuthenticated ? 'none' : 'inline-block'; + logoutBtn.style.display = isAuthenticated ? 'inline-block' : 'none'; + + loginBtn.addEventListener('click', async () => { + await auth0Client.loginWithRedirect({ + authorizationParams: { + redirect_uri: window.location.origin + '/dashboard.html', + }, + }); + }); + + logoutBtn.addEventListener('click', async () => { + await auth0Client.logout({ + logoutParams: { + returnTo: window.location.origin, + }, + }); + }); + } + + // Clear existing content + if (content) { + content.textContent = ''; + + if (!isAuthenticated) { + const p = document.createElement('p'); + p.textContent = 'Please log in to view the dashboard.'; + content.appendChild(p); + return; + } + + const user = await auth0Client.getUser(); + const p = document.createElement('p'); + p.textContent = `Welcome, ${user?.name || 'User'}!`; + content.appendChild(p); + } +} + +initDashboard().catch(console.error); diff --git a/tests/fixtures/vanilla-js/existing-auth0/index.html b/tests/fixtures/vanilla-js/existing-auth0/index.html new file mode 100644 index 0000000..7b65792 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/index.html @@ -0,0 +1,23 @@ + + + + + + Vanilla JS App with Auth0 + + + + +
+

Home

+

Welcome to the home page.

+
+ + + diff --git a/tests/fixtures/vanilla-js/existing-auth0/main.js b/tests/fixtures/vanilla-js/existing-auth0/main.js new file mode 100644 index 0000000..2316b8d --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/main.js @@ -0,0 +1,60 @@ +import { createAuth0Client } from '@auth0/auth0-spa-js'; + +let auth0Client = null; + +async function initAuth0() { + auth0Client = await createAuth0Client({ + domain: import.meta.env.VITE_AUTH0_DOMAIN, + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + }); + + // Check if user is authenticated + const isAuthenticated = await auth0Client.isAuthenticated(); + updateUI(isAuthenticated); + + // Handle callback + if (window.location.search.includes('code=')) { + await auth0Client.handleRedirectCallback(); + window.history.replaceState({}, document.title, window.location.pathname); + updateUI(true); + } +} + +function updateUI(isAuthenticated) { + const loginBtn = document.getElementById('login-btn'); + const logoutBtn = document.getElementById('logout-btn'); + + if (loginBtn && logoutBtn) { + loginBtn.style.display = isAuthenticated ? 'none' : 'inline-block'; + logoutBtn.style.display = isAuthenticated ? 'inline-block' : 'none'; + } +} + +async function login() { + await auth0Client.loginWithRedirect({ + authorizationParams: { + redirect_uri: window.location.origin, + }, + }); +} + +async function logout() { + await auth0Client.logout({ + logoutParams: { + returnTo: window.location.origin, + }, + }); +} + +// Initialize +initAuth0().catch(console.error); + +// Expose functions globally +window.auth0Login = login; +window.auth0Logout = logout; + +// Attach event listeners +document.getElementById('login-btn')?.addEventListener('click', login); +document.getElementById('logout-btn')?.addEventListener('click', logout); + +export { auth0Client, login, logout }; diff --git a/tests/fixtures/vanilla-js/existing-auth0/package.json b/tests/fixtures/vanilla-js/existing-auth0/package.json new file mode 100644 index 0000000..f82d489 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/package.json @@ -0,0 +1,16 @@ +{ + "name": "vanilla-js-existing-auth0-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@auth0/auth0-spa-js": "^2.1.3" + }, + "devDependencies": { + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/vanilla-js/existing-auth0/styles.css b/tests/fixtures/vanilla-js/existing-auth0/styles.css new file mode 100644 index 0000000..c25d3ee --- /dev/null +++ b/tests/fixtures/vanilla-js/existing-auth0/styles.css @@ -0,0 +1,33 @@ +body { + font-family: system-ui, -apple-system, sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +nav { + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + gap: 10px; +} + +nav a { + color: #333; + text-decoration: none; +} + +nav a:hover { + text-decoration: underline; +} + +#auth-buttons { + margin-left: auto; +} + +button { + padding: 8px 16px; + cursor: pointer; +} diff --git a/tests/fixtures/vanilla-js/existing/about.html b/tests/fixtures/vanilla-js/existing/about.html new file mode 100644 index 0000000..214ae3d --- /dev/null +++ b/tests/fixtures/vanilla-js/existing/about.html @@ -0,0 +1,19 @@ + + + + + + About - Vanilla JS App + + + + +
+

About

+

This is an existing vanilla JavaScript application.

+
+ + + diff --git a/tests/fixtures/vanilla-js/existing/dashboard.html b/tests/fixtures/vanilla-js/existing/dashboard.html new file mode 100644 index 0000000..d625f10 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing/dashboard.html @@ -0,0 +1,19 @@ + + + + + + Dashboard - Vanilla JS App + + + + +
+

Dashboard

+

Protected content would go here.

+
+ + + diff --git a/tests/fixtures/vanilla-js/existing/index.html b/tests/fixtures/vanilla-js/existing/index.html new file mode 100644 index 0000000..fd6c3a7 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing/index.html @@ -0,0 +1,19 @@ + + + + + + Vanilla JS App + + + + +
+

Home

+

Welcome to the home page.

+
+ + + diff --git a/tests/fixtures/vanilla-js/existing/main.js b/tests/fixtures/vanilla-js/existing/main.js new file mode 100644 index 0000000..8e1b2a0 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing/main.js @@ -0,0 +1,10 @@ +console.log('Vanilla JS app loaded'); + +// Simple utilities +export function $(selector) { + return document.querySelector(selector); +} + +export function $$(selector) { + return document.querySelectorAll(selector); +} diff --git a/tests/fixtures/vanilla-js/existing/package.json b/tests/fixtures/vanilla-js/existing/package.json new file mode 100644 index 0000000..4da60e9 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing/package.json @@ -0,0 +1,13 @@ +{ + "name": "vanilla-js-existing-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^6.0.5" + } +} diff --git a/tests/fixtures/vanilla-js/existing/styles.css b/tests/fixtures/vanilla-js/existing/styles.css new file mode 100644 index 0000000..5f65646 --- /dev/null +++ b/tests/fixtures/vanilla-js/existing/styles.css @@ -0,0 +1,21 @@ +body { + font-family: system-ui, -apple-system, sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +nav { + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +nav a { + color: #333; + text-decoration: none; +} + +nav a:hover { + text-decoration: underline; +} diff --git a/tests/fixtures/vanilla-js/fresh/index.html b/tests/fixtures/vanilla-js/fresh/index.html new file mode 100644 index 0000000..2776313 --- /dev/null +++ b/tests/fixtures/vanilla-js/fresh/index.html @@ -0,0 +1,15 @@ + + + + + + Vanilla JS App + + +
+

Vanilla JS App

+

Welcome to your vanilla JavaScript application.

+
+ + + diff --git a/tests/fixtures/vanilla-js/fresh/main.js b/tests/fixtures/vanilla-js/fresh/main.js new file mode 100644 index 0000000..83a130d --- /dev/null +++ b/tests/fixtures/vanilla-js/fresh/main.js @@ -0,0 +1 @@ +console.log('Vanilla JS app loaded'); diff --git a/tests/fixtures/vanilla-js/fresh/package.json b/tests/fixtures/vanilla-js/fresh/package.json new file mode 100644 index 0000000..22b1db0 --- /dev/null +++ b/tests/fixtures/vanilla-js/fresh/package.json @@ -0,0 +1,13 @@ +{ + "name": "vanilla-js-fresh-fixture", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^6.0.5" + } +} From 570cf511741e5c2223a0947c96497ea11926cecc Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Sat, 31 Jan 2026 22:36:21 -0600 Subject: [PATCH 03/21] feat: add retry logic, debug tooling, and history tracking to evals - Add history.ts for results persistence with compare functionality - Extend CLI with --debug, --keep-on-fail, --retry, --no-retry flags - Add history and compare subcommands (pnpm eval:history, eval:compare) - Implement retry loop in runner for handling LLM non-determinism - Add verbose failure output with expected/actual values - Create README documentation for eval framework usage --- package.json | 4 +- tests/evals/README.md | 169 +++++++++++++++++++++++++++++++++ tests/evals/agent-executor.ts | 13 ++- tests/evals/cli.ts | 57 ++++++++++- tests/evals/fixture-manager.ts | 12 ++- tests/evals/history.ts | 129 +++++++++++++++++++++++++ tests/evals/index.ts | 50 ++++++++-- tests/evals/runner.ts | 127 ++++++++++++++++++------- tests/evals/types.ts | 1 + 9 files changed, 515 insertions(+), 47 deletions(-) create mode 100644 tests/evals/README.md create mode 100644 tests/evals/history.ts diff --git a/package.json b/package.json index fd33a52..c697b0b 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,9 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", - "eval": "tsx tests/evals/index.ts" + "eval": "tsx tests/evals/index.ts", + "eval:history": "tsx tests/evals/index.ts history", + "eval:compare": "tsx tests/evals/index.ts compare" }, "author": "WorkOS", "license": "MIT" diff --git a/tests/evals/README.md b/tests/evals/README.md new file mode 100644 index 0000000..85d508b --- /dev/null +++ b/tests/evals/README.md @@ -0,0 +1,169 @@ +# Installer Evaluations + +Automated evaluation framework for testing WorkOS AuthKit installer skills against realistic project scenarios. + +## Quick Start + +```bash +# Run all evaluations +pnpm eval + +# Run specific framework +pnpm eval --framework=nextjs + +# Run specific scenario +pnpm eval --framework=react --state=existing-auth0 +``` + +## Test Matrix + +The framework tests 15 scenarios (5 frameworks × 3 project states): + +| State | Description | +|-------|-------------| +| `fresh` | Minimal scaffold from `create-*` tools | +| `existing` | Project with routes, components, custom config | +| `existing-auth0` | Project with Auth0 authentication already integrated | + +| Framework | Skill | Key Checks | +|-----------|-------|------------| +| `nextjs` | workos-authkit-nextjs | middleware.ts, callback route, AuthKitProvider | +| `react` | workos-authkit-react | AuthKitProvider, callback component, useAuth | +| `react-router` | workos-authkit-react-router | Auth loader, protected routes | +| `tanstack-start` | workos-authkit-tanstack-start | Server functions, callback route | +| `vanilla-js` | workos-authkit-vanilla-js | Auth script, callback page | + +## CLI Options + +``` +--framework= Filter by framework +--state= Filter by project state +--verbose, -v Show agent tool calls and detailed output +--debug Extra verbose, preserve temp dirs on failure +--keep-on-fail Don't cleanup temp directory when scenario fails +--retry= Number of retry attempts (default: 2) +--no-retry Disable retries +--json Output results as JSON +--help, -h Show help +``` + +## Debugging Failures + +### 1. Inspect the failure details + +```bash +pnpm eval --framework=react --state=existing-auth0 --verbose +``` + +### 2. Preserve the temp directory + +```bash +pnpm eval --framework=react --state=existing-auth0 --keep-on-fail +# Output will show: "Temp directory preserved: /tmp/eval-react-xxxxx" +``` + +### 3. Manually inspect the project state + +```bash +cd /tmp/eval-react-xxxxx +ls -la +cat middleware.ts +``` + +### 4. Compare with previous runs + +```bash +# List recent runs +pnpm eval:history + +# Compare two runs +pnpm eval:compare 2024-01-15T10-30-00 2024-01-16T14-45-00 +``` + +## Adding a New Fixture + +1. Create directory: `tests/fixtures/{framework}/{state}/` + +2. Add minimal project files: + - `package.json` with dependencies + - `tsconfig.json` (if TypeScript) + - Framework config file + - Basic app structure + +3. Verify fixture works standalone: + ```bash + cd tests/fixtures/{framework}/{state} + pnpm install + pnpm build + ``` + +4. Add scenario to `tests/evals/runner.ts` SCENARIOS array + +## Adding/Modifying Graders + +Graders live in `tests/evals/graders/{framework}.grader.ts`. + +Each grader implements: +```typescript +interface Grader { + grade(): Promise; +} +``` + +Use the helper classes: +- `FileGrader` - Check file existence and content patterns +- `BuildGrader` - Run build commands and check exit codes + +Example: +```typescript +const checks: GradeCheck[] = []; + +// File must exist +checks.push(await this.fileGrader.checkFileExists('middleware.ts')); + +// File must contain patterns +checks.push(...await this.fileGrader.checkFileContains('middleware.ts', [ + '@workos-inc/authkit', + 'authkitMiddleware', +])); + +// Build must succeed +checks.push(await this.buildGrader.checkBuild()); + +return { passed: checks.every(c => c.passed), checks }; +``` + +## Results Storage + +Results are saved to `tests/eval-results/`: +- Each run creates `{timestamp}.json` +- `latest.json` symlinks to most recent +- Use `pnpm eval:history` to list runs +- Use `pnpm eval:compare` to diff runs + +## Troubleshooting + +### "pnpm install failed" + +The fixture's dependencies may have version conflicts. Check: +```bash +cd tests/fixtures/{framework}/{state} +pnpm install +``` + +### "Build failed" but files look correct + +The agent may have created correct files but with syntax errors. Use `--keep-on-fail` to inspect: +```bash +pnpm eval --framework=nextjs --keep-on-fail +# Then run build manually in temp dir to see full error +``` + +### Flaky passes/failures + +LLM responses vary. Use `--retry=3` for more attempts: +```bash +pnpm eval --retry=3 +``` + +If a scenario is consistently flaky, check if the skill instructions are ambiguous. diff --git a/tests/evals/agent-executor.ts b/tests/evals/agent-executor.ts index 5b45a68..c94881b 100644 --- a/tests/evals/agent-executor.ts +++ b/tests/evals/agent-executor.ts @@ -6,11 +6,20 @@ export interface AgentResult { toolCalls: Array<{ tool: string; args: unknown; result: unknown }>; } +export interface AgentExecutorOptions { + verbose?: boolean; +} + export class AgentExecutor { + private options: AgentExecutorOptions; + constructor( private workDir: string, - private framework: string - ) {} + private framework: string, + options: AgentExecutorOptions = {} + ) { + this.options = options; + } async run(): Promise { const toolCalls: AgentResult['toolCalls'] = []; diff --git a/tests/evals/cli.ts b/tests/evals/cli.ts index f8e5ece..4cf743d 100644 --- a/tests/evals/cli.ts +++ b/tests/evals/cli.ts @@ -2,8 +2,14 @@ export interface CliOptions { framework?: string; state?: string; verbose: boolean; + debug: boolean; json: boolean; help: boolean; + keepOnFail: boolean; + retry: number; + noRetry: boolean; + command?: 'run' | 'history' | 'compare'; + compareIds?: [string, string]; } const FRAMEWORKS = ['nextjs', 'react', 'react-router', 'tanstack-start', 'vanilla-js']; @@ -12,17 +18,45 @@ const STATES = ['fresh', 'existing', 'existing-auth0']; export function parseArgs(args: string[]): CliOptions { const options: CliOptions = { verbose: false, + debug: false, json: false, help: false, + keepOnFail: false, + retry: 2, + noRetry: false, }; + // Check for subcommands + if (args[0] === 'history') { + options.command = 'history'; + return options; + } + + if (args[0] === 'compare' && args.length >= 3) { + options.command = 'compare'; + options.compareIds = [args[1], args[2]]; + return options; + } + + options.command = 'run'; + for (const arg of args) { if (arg === '--help' || arg === '-h') { options.help = true; } else if (arg === '--verbose' || arg === '-v') { options.verbose = true; + } else if (arg === '--debug') { + options.debug = true; + options.verbose = true; + options.keepOnFail = true; } else if (arg === '--json') { options.json = true; + } else if (arg === '--keep-on-fail') { + options.keepOnFail = true; + } else if (arg === '--no-retry') { + options.noRetry = true; + } else if (arg.startsWith('--retry=')) { + options.retry = parseInt(arg.split('=')[1], 10); } else if (arg.startsWith('--framework=')) { const framework = arg.split('=')[1]; if (!FRAMEWORKS.includes(framework)) { @@ -38,12 +72,21 @@ export function parseArgs(args: string[]): CliOptions { } } + if (options.noRetry) { + options.retry = 0; + } + return options; } export function printHelp(): void { console.log(` -Usage: pnpm eval [options] +Usage: pnpm eval [command] [options] + +Commands: + run (default) Run evaluations + history List recent eval runs + compare Compare two eval runs Options: --framework= Run only scenarios for this framework @@ -54,6 +97,14 @@ Options: --verbose, -v Show detailed output including agent tool calls + --debug Extra verbose, preserve temp dirs on failure + + --keep-on-fail Don't cleanup temp directory when scenario fails + + --retry= Number of retry attempts (default: 2) + + --no-retry Disable retries + --json Output results as JSON (for scripting) --help, -h Show this help message @@ -64,5 +115,9 @@ Examples: pnpm eval --state=fresh # Run only fresh app scenarios pnpm eval --framework=react --state=existing-auth0 # Run specific scenario + pnpm eval --debug # Verbose output, keep failed dirs + pnpm eval --retry=3 # More retry attempts + pnpm eval:history # List recent runs + pnpm eval:compare # Compare two runs `); } diff --git a/tests/evals/fixture-manager.ts b/tests/evals/fixture-manager.ts index 387a7ea..c5cbafd 100644 --- a/tests/evals/fixture-manager.ts +++ b/tests/evals/fixture-manager.ts @@ -3,13 +3,21 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { execFileNoThrow } from '../../src/utils/exec-file.js'; +export interface FixtureOptions { + keepOnFail?: boolean; +} + export class FixtureManager { private tempDir: string | null = null; + private options: FixtureOptions; constructor( private framework: string, - private state: string - ) {} + private state: string, + options: FixtureOptions = {} + ) { + this.options = options; + } async setup(): Promise { // Create temp directory diff --git a/tests/evals/history.ts b/tests/evals/history.ts new file mode 100644 index 0000000..79780e4 --- /dev/null +++ b/tests/evals/history.ts @@ -0,0 +1,129 @@ +import { writeFile, readFile, readdir, symlink, unlink, mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; +import type { EvalResult } from './types.js'; + +const RESULTS_DIR = join(process.cwd(), 'tests/eval-results'); + +export interface EvalRun { + id: string; + timestamp: string; + summary: { + total: number; + passed: number; + failed: number; + passRate: number; + }; + options: { + framework?: string; + state?: string; + }; + results: EvalResult[]; +} + +export async function saveResults( + results: EvalResult[], + options: { framework?: string; state?: string } +): Promise { + // Ensure results directory exists + await mkdir(RESULTS_DIR, { recursive: true }); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `${timestamp}.json`; + const filepath = join(RESULTS_DIR, filename); + + const run: EvalRun = { + id: timestamp, + timestamp: new Date().toISOString(), + summary: { + total: results.length, + passed: results.filter((r) => r.passed).length, + failed: results.filter((r) => !r.passed).length, + passRate: results.length > 0 ? results.filter((r) => r.passed).length / results.length : 0, + }, + options, + results, + }; + + await writeFile(filepath, JSON.stringify(run, null, 2)); + + // Update latest symlink + const latestPath = join(RESULTS_DIR, 'latest.json'); + try { + await unlink(latestPath); + } catch { + // Ignore if doesn't exist + } + await symlink(filename, latestPath); + + return filepath; +} + +export async function loadRun(id: string): Promise { + const filepath = + id === 'latest' ? join(RESULTS_DIR, 'latest.json') : join(RESULTS_DIR, `${id}.json`); + + const content = await readFile(filepath, 'utf-8'); + return JSON.parse(content); +} + +export async function listRuns(): Promise { + try { + const files = await readdir(RESULTS_DIR); + return files + .filter((f) => f.endsWith('.json') && f !== 'latest.json') + .sort() + .reverse(); + } catch { + return []; + } +} + +export function compareRuns(run1: EvalRun, run2: EvalRun): void { + console.log(`\nComparing ${run1.id} vs ${run2.id}\n`); + + console.log('Summary:'); + console.log( + ` Run 1: ${run1.summary.passed}/${run1.summary.total} (${(run1.summary.passRate * 100).toFixed(1)}%)` + ); + console.log( + ` Run 2: ${run2.summary.passed}/${run2.summary.total} (${(run2.summary.passRate * 100).toFixed(1)}%)` + ); + + const diff = run2.summary.passRate - run1.summary.passRate; + const trend = diff > 0 ? '↑' : diff < 0 ? '↓' : '→'; + console.log(` Trend: ${trend} ${Math.abs(diff * 100).toFixed(1)}%`); + + // Find regressions and improvements + const results1 = new Map(run1.results.map((r) => [r.scenario, r.passed])); + const results2 = new Map(run2.results.map((r) => [r.scenario, r.passed])); + + const regressions: string[] = []; + const improvements: string[] = []; + + for (const [scenario, passed2] of results2) { + const passed1 = results1.get(scenario); + if (passed1 === true && passed2 === false) { + regressions.push(scenario); + } else if (passed1 === false && passed2 === true) { + improvements.push(scenario); + } + } + + if (regressions.length > 0) { + console.log('\nRegressions (was passing, now failing):'); + for (const s of regressions) { + console.log(` ✗ ${s}`); + } + } + + if (improvements.length > 0) { + console.log('\nImprovements (was failing, now passing):'); + for (const s of improvements) { + console.log(` ✓ ${s}`); + } + } + + if (regressions.length === 0 && improvements.length === 0) { + console.log('\nNo changes in pass/fail status.'); + } +} diff --git a/tests/evals/index.ts b/tests/evals/index.ts index 3df7b33..1609edf 100644 --- a/tests/evals/index.ts +++ b/tests/evals/index.ts @@ -2,6 +2,7 @@ import { parseArgs, printHelp } from './cli.js'; import { runEvals } from './runner.js'; import { printMatrix, printJson } from './reporter.js'; +import { listRuns, loadRun, compareRuns } from './history.js'; async function main() { const options = parseArgs(process.argv.slice(2)); @@ -12,15 +13,50 @@ async function main() { } try { - const results = await runEvals(options); + switch (options.command) { + case 'history': { + const runs = await listRuns(); + if (runs.length === 0) { + console.log('No eval runs found. Run `pnpm eval` first.'); + break; + } + console.log('Recent eval runs:'); + for (const run of runs.slice(0, 10)) { + const data = await loadRun(run.replace('.json', '')); + console.log( + ` ${run.replace('.json', '')} - ${data.summary.passed}/${data.summary.total} passed` + ); + } + break; + } - if (options.json) { - printJson(results); - } else { - printMatrix(results); - } + case 'compare': { + const [id1, id2] = options.compareIds!; + const run1 = await loadRun(id1); + const run2 = await loadRun(id2); + compareRuns(run1, run2); + break; + } + + case 'run': + default: { + const results = await runEvals({ + framework: options.framework, + state: options.state, + verbose: options.verbose, + keepOnFail: options.keepOnFail, + retry: options.retry, + }); - process.exit(results.every((r) => r.passed) ? 0 : 1); + if (options.json) { + printJson(results); + } else { + printMatrix(results); + } + + process.exit(results.every((r) => r.passed) ? 0 : 1); + } + } } catch (error) { console.error('Eval failed:', error); process.exit(1); diff --git a/tests/evals/runner.ts b/tests/evals/runner.ts index 3bc7330..01db25e 100644 --- a/tests/evals/runner.ts +++ b/tests/evals/runner.ts @@ -5,7 +5,8 @@ import { ReactGrader } from './graders/react.grader.js'; import { ReactRouterGrader } from './graders/react-router.grader.js'; import { TanstackGrader } from './graders/tanstack.grader.js'; import { VanillaGrader } from './graders/vanilla.grader.js'; -import type { EvalResult, EvalOptions, Grader } from './types.js'; +import { saveResults } from './history.js'; +import type { EvalResult, EvalOptions, Grader, GradeCheck } from './types.js'; interface Scenario { framework: string; @@ -40,8 +41,14 @@ const SCENARIOS: Scenario[] = [ { framework: 'vanilla-js', state: 'existing-auth0', grader: VanillaGrader }, ]; -export async function runEvals(options: EvalOptions): Promise { +export interface ExtendedEvalOptions extends EvalOptions { + keepOnFail?: boolean; + retry?: number; +} + +export async function runEvals(options: ExtendedEvalOptions): Promise { const results: EvalResult[] = []; + const maxAttempts = (options.retry ?? 2) + 1; const scenarios = SCENARIOS.filter( (s) => @@ -51,49 +58,101 @@ export async function runEvals(options: EvalOptions): Promise { for (const scenario of scenarios) { console.log(`\nRunning: ${scenario.framework}/${scenario.state}`); - const startTime = Date.now(); - const fixtureManager = new FixtureManager(scenario.framework, scenario.state); + let lastResult: EvalResult | null = null; + let attempt = 0; - try { - // Setup fixture in temp directory - const workDir = await fixtureManager.setup(); + while (attempt < maxAttempts) { + attempt++; + if (attempt > 1) { + console.log(` Retry attempt ${attempt}/${maxAttempts}...`); + } - // Run agent against fixture - const executor = new AgentExecutor(workDir, scenario.framework); - const agentResult = await executor.run(); + const startTime = Date.now(); + const fixtureManager = new FixtureManager(scenario.framework, scenario.state, { + keepOnFail: options.keepOnFail, + }); - // Grade the result - const grader = new scenario.grader(workDir); - const gradeResult = await grader.grade(); + try { + const workDir = await fixtureManager.setup(); - results.push({ - scenario: `${scenario.framework}/${scenario.state}`, - passed: gradeResult.passed, - duration: Date.now() - startTime, - checks: gradeResult.checks, - agentOutput: agentResult.output, - }); + const executor = new AgentExecutor(workDir, scenario.framework, { + verbose: options.verbose, + }); + const agentResult = await executor.run(); - console.log(gradeResult.passed ? '✓ PASSED' : '✗ FAILED'); + const grader = new scenario.grader(workDir); + const gradeResult = await grader.grade(); - if (!gradeResult.passed) { - for (const check of gradeResult.checks.filter((c) => !c.passed)) { - console.log(` - ${check.name}: ${check.message}`); + lastResult = { + scenario: `${scenario.framework}/${scenario.state}`, + passed: gradeResult.passed, + duration: Date.now() - startTime, + checks: gradeResult.checks, + agentOutput: agentResult.output, + attempts: attempt, + }; + + if (gradeResult.passed) { + break; // Success, no more retries needed + } + } catch (error) { + lastResult = { + scenario: `${scenario.framework}/${scenario.state}`, + passed: false, + duration: Date.now() - startTime, + error: error instanceof Error ? error.message : String(error), + attempts: attempt, + }; + } finally { + if (!options.keepOnFail || lastResult?.passed) { + await fixtureManager.cleanup(); + } else { + console.log(` Temp directory preserved: ${fixtureManager.getTempDir()}`); } } - } catch (error) { - results.push({ - scenario: `${scenario.framework}/${scenario.state}`, - passed: false, - duration: Date.now() - startTime, - error: error instanceof Error ? error.message : String(error), - }); - console.log('✗ ERROR:', error); - } finally { - await fixtureManager.cleanup(); + } + + if (lastResult) { + results.push(lastResult); + const status = lastResult.passed ? '✓ PASSED' : '✗ FAILED'; + const attemptInfo = + lastResult.attempts && lastResult.attempts > 1 + ? ` (attempt ${lastResult.attempts}/${maxAttempts})` + : ''; + console.log(`${status}${attemptInfo}`); + + if (!lastResult.passed && !options.verbose) { + printFailureDetails(lastResult, false); + } else if (!lastResult.passed && options.verbose) { + printFailureDetails(lastResult, true); + } } } + // Save results (skip in json mode since caller handles output) + const filepath = await saveResults(results, { + framework: options.framework, + state: options.state, + }); + console.log(`\nResults saved to: ${filepath}`); + return results; } + +function printFailureDetails(result: EvalResult, verbose: boolean): void { + if (result.error) { + console.log(` Error: ${result.error}`); + } else if (result.checks) { + const failedChecks = result.checks.filter((c: GradeCheck) => !c.passed); + for (const check of failedChecks) { + console.log(` - ${check.name}: ${check.message}`); + if (verbose && check.expected) { + console.log(` Expected: ${check.expected}`); + } + if (verbose && check.actual) { + console.log(` Actual: ${check.actual}`); + } + } + } +} diff --git a/tests/evals/types.ts b/tests/evals/types.ts index 59c1844..32732ad 100644 --- a/tests/evals/types.ts +++ b/tests/evals/types.ts @@ -22,6 +22,7 @@ export interface EvalResult { checks?: GradeCheck[]; agentOutput?: string; error?: string; + attempts?: number; } export interface EvalOptions { From d9b35aab1463f4917f04bad9bb02000485dc2bf0 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Sat, 31 Jan 2026 22:57:23 -0600 Subject: [PATCH 04/21] feat(evals): wire AgentExecutor to use Claude Agent SDK Replace stub implementation with real agent execution: - Add env-loader for credentials from .env.local - Configure SDK with direct auth mode (bypasses gateway) - Capture tool calls and output from message stream - Add ToolCall interface to types --- .env.local.example | 8 +- tests/evals/agent-executor.ts | 172 +++++++++++++++++++++++++++++----- tests/evals/env-loader.ts | 32 +++++++ tests/evals/types.ts | 7 ++ 4 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 tests/evals/env-loader.ts diff --git a/.env.local.example b/.env.local.example index d3c8c50..e542e1b 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,5 +1,9 @@ # Local development configuration # Copy this to .env.local and fill in values -# WorkOS Client ID for local development -WORKOS_CLIENT_ID=client_xxx +# Required for running evals +ANTHROPIC_API_KEY=sk-ant-... + +# WorkOS credentials (optional for evals - placeholders used if missing) +WORKOS_API_KEY=sk_test_... +WORKOS_CLIENT_ID=client_... diff --git a/tests/evals/agent-executor.ts b/tests/evals/agent-executor.ts index c94881b..4976cd5 100644 --- a/tests/evals/agent-executor.ts +++ b/tests/evals/agent-executor.ts @@ -1,17 +1,34 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { Integration } from '../../src/lib/constants.js'; +import { loadCredentials } from './env-loader.js'; +import { writeEnvLocal } from '../../src/lib/env-writer.js'; +import { getConfig } from '../../src/lib/settings.js'; +import type { ToolCall } from './types.js'; export interface AgentResult { success: boolean; output: string; - toolCalls: Array<{ tool: string; args: unknown; result: unknown }>; + toolCalls: ToolCall[]; + error?: string; } export interface AgentExecutorOptions { verbose?: boolean; } +// Skill name mapping for each framework +const SKILL_NAMES: Record = { + [Integration.nextjs]: 'workos-authkit-nextjs', + [Integration.react]: 'workos-authkit-react', + [Integration.reactRouter]: 'workos-authkit-react-router', + [Integration.tanstackStart]: 'workos-authkit-tanstack-start', + [Integration.vanillaJs]: 'workos-authkit-vanilla-js', +}; + export class AgentExecutor { private options: AgentExecutorOptions; + private credentials: ReturnType; constructor( private workDir: string, @@ -19,32 +36,143 @@ export class AgentExecutor { options: AgentExecutorOptions = {} ) { this.options = options; + this.credentials = loadCredentials(); } async run(): Promise { - const toolCalls: AgentResult['toolCalls'] = []; - - // Map framework to Integration enum const integration = this.getIntegration(); + const toolCalls: ToolCall[] = []; + const collectedOutput: string[] = []; - // TODO: In a real implementation, this would: - // 1. Build the prompt using existing infrastructure - // 2. Initialize and run the agent - // 3. Capture tool calls and output - // - // For now, we stub this to focus on the eval framework structure. - // The agent runner integration will be completed once we validate - // the fixture/grader flow works correctly. - - console.log(` Running agent for ${integration} in ${this.workDir}...`); - - // Placeholder - actual agent execution would go here - // This allows us to test the eval framework structure - return { - success: true, - output: `[Placeholder] Agent would run ${integration} installation here`, - toolCalls, - }; + if (this.options.verbose) { + console.log(` Initializing agent for ${integration}...`); + } + + // Write .env.local to fixture directory + writeEnvLocal(this.workDir, { + WORKOS_API_KEY: this.credentials.workosApiKey, + WORKOS_CLIENT_ID: this.credentials.workosClientId, + WORKOS_REDIRECT_URI: 'http://localhost:3000/callback', + }); + + // Build prompt + const skillName = SKILL_NAMES[integration]; + const prompt = this.buildPrompt(skillName); + + // Initialize and run agent + try { + const { query } = await import('@anthropic-ai/claude-agent-sdk'); + + // Build SDK environment for direct mode + const sdkEnv: Record = { + ...process.env, + ANTHROPIC_API_KEY: this.credentials.anthropicApiKey, + CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'true', + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'true', + }; + // Remove gateway config to use direct API + delete sdkEnv.ANTHROPIC_BASE_URL; + delete sdkEnv.ANTHROPIC_AUTH_TOKEN; + + // Get plugin path for skills + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const pluginPath = path.join(__dirname, '../..'); + + const response = query({ + prompt: prompt, + options: { + model: getConfig().model, + cwd: this.workDir, + permissionMode: 'acceptEdits', + mcpServers: { + workos: { + command: 'npx', + args: ['-y', '@workos/mcp-docs-server'], + }, + }, + env: sdkEnv, + tools: { type: 'preset', preset: 'claude_code' }, + allowedTools: ['Skill', 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebFetch'], + plugins: [{ type: 'local', path: pluginPath }], + }, + }); + + // Process message stream + for await (const message of response) { + this.handleMessage(message, toolCalls, collectedOutput); + } + + return { + success: true, + output: collectedOutput.join('\n'), + toolCalls, + }; + } catch (error) { + return { + success: false, + output: collectedOutput.join('\n'), + toolCalls, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + private buildPrompt(skillName: string): string { + return `You are integrating WorkOS AuthKit into this application. + +## Project Context +- Framework: ${this.framework} +- Working directory: ${this.workDir} + +## Environment +The following environment variables have been configured in .env.local: +- WORKOS_API_KEY +- WORKOS_CLIENT_ID +- WORKOS_REDIRECT_URI + +## Your Task +Use the \`${skillName}\` skill to integrate WorkOS AuthKit into this application. + +Begin by invoking the ${skillName} skill.`; + } + + private handleMessage( + message: any, + toolCalls: ToolCall[], + collectedOutput: string[] + ): void { + if (message.type === 'assistant') { + const content = message.message?.content; + if (Array.isArray(content)) { + for (const block of content) { + // Capture text output + if (block.type === 'text' && typeof block.text === 'string') { + collectedOutput.push(block.text); + if (this.options.verbose) { + console.log(` Agent: ${block.text.slice(0, 100)}...`); + } + } + // Capture tool calls + if (block.type === 'tool_use') { + const call: ToolCall = { + tool: block.name, + input: block.input as Record, + }; + toolCalls.push(call); + if (this.options.verbose) { + console.log(` Tool: ${block.name}`); + } + } + } + } + } + + if (message.type === 'result') { + if (message.subtype !== 'success' && message.errors?.length > 0) { + collectedOutput.push(`Error: ${message.errors.join(', ')}`); + } + } } private getIntegration(): Integration { diff --git a/tests/evals/env-loader.ts b/tests/evals/env-loader.ts new file mode 100644 index 0000000..63944fe --- /dev/null +++ b/tests/evals/env-loader.ts @@ -0,0 +1,32 @@ +import { config } from 'dotenv'; +import { join } from 'node:path'; + +export interface EvalCredentials { + anthropicApiKey: string; + workosApiKey: string; + workosClientId: string; +} + +export function loadCredentials(): EvalCredentials { + // Load from project root .env.local + config({ path: join(process.cwd(), '.env.local') }); + + const anthropicApiKey = process.env.ANTHROPIC_API_KEY; + const workosApiKey = process.env.WORKOS_API_KEY; + const workosClientId = process.env.WORKOS_CLIENT_ID; + + if (!anthropicApiKey) { + throw new Error( + 'ANTHROPIC_API_KEY not found.\n' + + 'Copy .env.local.example to .env.local and add your key:\n' + + ' ANTHROPIC_API_KEY=sk-ant-...' + ); + } + + // WorkOS credentials can be placeholder values for evals + return { + anthropicApiKey, + workosApiKey: workosApiKey || 'sk_test_placeholder', + workosClientId: workosClientId || 'client_placeholder', + }; +} diff --git a/tests/evals/types.ts b/tests/evals/types.ts index 32732ad..bc336ff 100644 --- a/tests/evals/types.ts +++ b/tests/evals/types.ts @@ -30,3 +30,10 @@ export interface EvalOptions { state?: string; verbose?: boolean; } + +export interface ToolCall { + tool: string; + input: Record; + output?: string; + durationMs?: number; +} From a9fd654ab6c119019a27b808e781615b5041cbb2 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Sat, 31 Jan 2026 22:58:35 -0600 Subject: [PATCH 05/21] style: format eval files with prettier --- src/utils/exec-file.ts | 6 +-- tests/evals/README.md | 39 +++++++++++-------- tests/evals/agent-executor.ts | 8 +--- tests/evals/env-loader.ts | 2 +- tests/evals/fixture-manager.ts | 9 +---- tests/evals/graders/file-grader.ts | 10 +---- tests/evals/graders/nextjs.grader.ts | 18 ++------- tests/evals/graders/react-router.grader.ts | 4 +- tests/evals/graders/react.grader.ts | 9 +---- tests/evals/graders/tanstack.grader.ts | 13 ++----- tests/evals/graders/vanilla.grader.ts | 4 +- tests/evals/history.ts | 13 ++----- tests/evals/index.ts | 4 +- tests/evals/runner.ts | 8 +--- .../nextjs/existing-auth0/app/layout.tsx | 11 ++---- tests/fixtures/nextjs/existing/app/layout.tsx | 9 +---- tests/fixtures/nextjs/fresh/app/layout.tsx | 6 +-- .../fixtures/react/existing-auth0/src/App.tsx | 3 +- .../react/existing-auth0/src/main.tsx | 2 +- tests/fixtures/react/existing/src/App.tsx | 3 +- tests/fixtures/react/existing/src/main.tsx | 2 +- tests/fixtures/react/fresh/src/main.tsx | 2 +- .../existing-auth0/app/routes/__root.tsx | 3 +- .../existing/app/routes/__root.tsx | 3 +- .../vanilla-js/existing-auth0/styles.css | 5 ++- tests/fixtures/vanilla-js/existing/about.html | 4 +- .../vanilla-js/existing/dashboard.html | 4 +- tests/fixtures/vanilla-js/existing/index.html | 4 +- tests/fixtures/vanilla-js/existing/styles.css | 5 ++- 29 files changed, 73 insertions(+), 140 deletions(-) diff --git a/src/utils/exec-file.ts b/src/utils/exec-file.ts index 5c81b6a..38d5e46 100644 --- a/src/utils/exec-file.ts +++ b/src/utils/exec-file.ts @@ -16,11 +16,7 @@ export interface ExecOptions { * Execute a command without throwing on non-zero exit codes. * Returns { status, stdout, stderr } for all outcomes. */ -export function execFileNoThrow( - command: string, - args: string[], - options: ExecOptions = {} -): Promise { +export function execFileNoThrow(command: string, args: string[], options: ExecOptions = {}): Promise { return new Promise((resolve) => { const child = spawn(command, args, { cwd: options.cwd, diff --git a/tests/evals/README.md b/tests/evals/README.md index 85d508b..8eb2f7f 100644 --- a/tests/evals/README.md +++ b/tests/evals/README.md @@ -19,19 +19,19 @@ pnpm eval --framework=react --state=existing-auth0 The framework tests 15 scenarios (5 frameworks × 3 project states): -| State | Description | -|-------|-------------| -| `fresh` | Minimal scaffold from `create-*` tools | -| `existing` | Project with routes, components, custom config | +| State | Description | +| ---------------- | ---------------------------------------------------- | +| `fresh` | Minimal scaffold from `create-*` tools | +| `existing` | Project with routes, components, custom config | | `existing-auth0` | Project with Auth0 authentication already integrated | -| Framework | Skill | Key Checks | -|-----------|-------|------------| -| `nextjs` | workos-authkit-nextjs | middleware.ts, callback route, AuthKitProvider | -| `react` | workos-authkit-react | AuthKitProvider, callback component, useAuth | -| `react-router` | workos-authkit-react-router | Auth loader, protected routes | -| `tanstack-start` | workos-authkit-tanstack-start | Server functions, callback route | -| `vanilla-js` | workos-authkit-vanilla-js | Auth script, callback page | +| Framework | Skill | Key Checks | +| ---------------- | ----------------------------- | ---------------------------------------------- | +| `nextjs` | workos-authkit-nextjs | middleware.ts, callback route, AuthKitProvider | +| `react` | workos-authkit-react | AuthKitProvider, callback component, useAuth | +| `react-router` | workos-authkit-react-router | Auth loader, protected routes | +| `tanstack-start` | workos-authkit-tanstack-start | Server functions, callback route | +| `vanilla-js` | workos-authkit-vanilla-js | Auth script, callback page | ## CLI Options @@ -91,6 +91,7 @@ pnpm eval:compare 2024-01-15T10-30-00 2024-01-16T14-45-00 - Basic app structure 3. Verify fixture works standalone: + ```bash cd tests/fixtures/{framework}/{state} pnpm install @@ -104,6 +105,7 @@ pnpm eval:compare 2024-01-15T10-30-00 2024-01-16T14-45-00 Graders live in `tests/evals/graders/{framework}.grader.ts`. Each grader implements: + ```typescript interface Grader { grade(): Promise; @@ -111,10 +113,12 @@ interface Grader { ``` Use the helper classes: + - `FileGrader` - Check file existence and content patterns - `BuildGrader` - Run build commands and check exit codes Example: + ```typescript const checks: GradeCheck[] = []; @@ -122,20 +126,20 @@ const checks: GradeCheck[] = []; checks.push(await this.fileGrader.checkFileExists('middleware.ts')); // File must contain patterns -checks.push(...await this.fileGrader.checkFileContains('middleware.ts', [ - '@workos-inc/authkit', - 'authkitMiddleware', -])); +checks.push( + ...(await this.fileGrader.checkFileContains('middleware.ts', ['@workos-inc/authkit', 'authkitMiddleware'])), +); // Build must succeed checks.push(await this.buildGrader.checkBuild()); -return { passed: checks.every(c => c.passed), checks }; +return { passed: checks.every((c) => c.passed), checks }; ``` ## Results Storage Results are saved to `tests/eval-results/`: + - Each run creates `{timestamp}.json` - `latest.json` symlinks to most recent - Use `pnpm eval:history` to list runs @@ -146,6 +150,7 @@ Results are saved to `tests/eval-results/`: ### "pnpm install failed" The fixture's dependencies may have version conflicts. Check: + ```bash cd tests/fixtures/{framework}/{state} pnpm install @@ -154,6 +159,7 @@ pnpm install ### "Build failed" but files look correct The agent may have created correct files but with syntax errors. Use `--keep-on-fail` to inspect: + ```bash pnpm eval --framework=nextjs --keep-on-fail # Then run build manually in temp dir to see full error @@ -162,6 +168,7 @@ pnpm eval --framework=nextjs --keep-on-fail ### Flaky passes/failures LLM responses vary. Use `--retry=3` for more attempts: + ```bash pnpm eval --retry=3 ``` diff --git a/tests/evals/agent-executor.ts b/tests/evals/agent-executor.ts index 4976cd5..5568fc4 100644 --- a/tests/evals/agent-executor.ts +++ b/tests/evals/agent-executor.ts @@ -33,7 +33,7 @@ export class AgentExecutor { constructor( private workDir: string, private framework: string, - options: AgentExecutorOptions = {} + options: AgentExecutorOptions = {}, ) { this.options = options; this.credentials = loadCredentials(); @@ -137,11 +137,7 @@ Use the \`${skillName}\` skill to integrate WorkOS AuthKit into this application Begin by invoking the ${skillName} skill.`; } - private handleMessage( - message: any, - toolCalls: ToolCall[], - collectedOutput: string[] - ): void { + private handleMessage(message: any, toolCalls: ToolCall[], collectedOutput: string[]): void { if (message.type === 'assistant') { const content = message.message?.content; if (Array.isArray(content)) { diff --git a/tests/evals/env-loader.ts b/tests/evals/env-loader.ts index 63944fe..b7276f7 100644 --- a/tests/evals/env-loader.ts +++ b/tests/evals/env-loader.ts @@ -19,7 +19,7 @@ export function loadCredentials(): EvalCredentials { throw new Error( 'ANTHROPIC_API_KEY not found.\n' + 'Copy .env.local.example to .env.local and add your key:\n' + - ' ANTHROPIC_API_KEY=sk-ant-...' + ' ANTHROPIC_API_KEY=sk-ant-...', ); } diff --git a/tests/evals/fixture-manager.ts b/tests/evals/fixture-manager.ts index c5cbafd..30e7364 100644 --- a/tests/evals/fixture-manager.ts +++ b/tests/evals/fixture-manager.ts @@ -14,7 +14,7 @@ export class FixtureManager { constructor( private framework: string, private state: string, - options: FixtureOptions = {} + options: FixtureOptions = {}, ) { this.options = options; } @@ -24,12 +24,7 @@ export class FixtureManager { this.tempDir = await mkdtemp(join(tmpdir(), `eval-${this.framework}-`)); // Copy fixture files - const fixtureSource = join( - process.cwd(), - 'tests/fixtures', - this.framework, - this.state - ); + const fixtureSource = join(process.cwd(), 'tests/fixtures', this.framework, this.state); await cp(fixtureSource, this.tempDir, { recursive: true }); diff --git a/tests/evals/graders/file-grader.ts b/tests/evals/graders/file-grader.ts index d6860cb..68542e6 100644 --- a/tests/evals/graders/file-grader.ts +++ b/tests/evals/graders/file-grader.ts @@ -22,10 +22,7 @@ export class FileGrader { } } - async checkFileContains( - relativePath: string, - patterns: (string | RegExp)[] - ): Promise { + async checkFileContains(relativePath: string, patterns: (string | RegExp)[]): Promise { const fullPath = join(this.workDir, relativePath); const checks: GradeCheck[] = []; @@ -41,10 +38,7 @@ export class FileGrader { } for (const pattern of patterns) { - const matches = - typeof pattern === 'string' - ? content.includes(pattern) - : pattern.test(content); + const matches = typeof pattern === 'string' ? content.includes(pattern) : pattern.test(content); checks.push({ name: `Pattern in ${relativePath}: ${pattern}`, diff --git a/tests/evals/graders/nextjs.grader.ts b/tests/evals/graders/nextjs.grader.ts index d371f03..887035f 100644 --- a/tests/evals/graders/nextjs.grader.ts +++ b/tests/evals/graders/nextjs.grader.ts @@ -15,9 +15,7 @@ export class NextjsGrader implements Grader { const checks: GradeCheck[] = []; // Check callback route exists (App Router) - checks.push( - await this.fileGrader.checkFileExists('app/api/auth/callback/route.ts') - ); + checks.push(await this.fileGrader.checkFileExists('app/api/auth/callback/route.ts')); // Check middleware exists checks.push(await this.fileGrader.checkFileExists('middleware.ts')); @@ -27,22 +25,14 @@ export class NextjsGrader implements Grader { ...(await this.fileGrader.checkFileContains('middleware.ts', [ '@workos-inc/authkit-nextjs', 'authkitMiddleware', - ])) + ])), ); // Check AuthKitProvider in layout - checks.push( - ...(await this.fileGrader.checkFileContains('app/layout.tsx', [ - 'AuthKitProvider', - ])) - ); + checks.push(...(await this.fileGrader.checkFileContains('app/layout.tsx', ['AuthKitProvider']))); // Check environment variables used correctly - checks.push( - ...(await this.fileGrader.checkFileContains('middleware.ts', [ - /process\.env\.WORKOS_/, - ])) - ); + checks.push(...(await this.fileGrader.checkFileContains('middleware.ts', [/process\.env\.WORKOS_/]))); // Check build succeeds checks.push(await this.buildGrader.checkBuild()); diff --git a/tests/evals/graders/react-router.grader.ts b/tests/evals/graders/react-router.grader.ts index b4b077a..86dc12a 100644 --- a/tests/evals/graders/react-router.grader.ts +++ b/tests/evals/graders/react-router.grader.ts @@ -34,9 +34,7 @@ export class ReactRouterGrader implements Grader { checks.push(...authRoutePatterns); // Check protected route wrapper or middleware - const hasProtectedRoute = await this.fileGrader.checkFileExists( - 'app/components/ProtectedRoute.tsx' - ); + const hasProtectedRoute = await this.fileGrader.checkFileExists('app/components/ProtectedRoute.tsx'); if (!hasProtectedRoute.passed) { // Try alternate locations checks.push(await this.fileGrader.checkFileExists('app/utils/auth.ts')); diff --git a/tests/evals/graders/react.grader.ts b/tests/evals/graders/react.grader.ts index b11428d..77802ea 100644 --- a/tests/evals/graders/react.grader.ts +++ b/tests/evals/graders/react.grader.ts @@ -16,10 +16,7 @@ export class ReactGrader implements Grader { // Check AuthKitProvider wrapper exists checks.push( - ...(await this.fileGrader.checkFileContains('src/main.tsx', [ - 'AuthKitProvider', - '@workos-inc/authkit-react', - ])) + ...(await this.fileGrader.checkFileContains('src/main.tsx', ['AuthKitProvider', '@workos-inc/authkit-react'])), ); // Check callback component exists @@ -36,9 +33,7 @@ export class ReactGrader implements Grader { // Check environment config checks.push( - ...(await this.fileGrader.checkFileContains('src/main.tsx', [ - /VITE_WORKOS_CLIENT_ID|import\.meta\.env/, - ])) + ...(await this.fileGrader.checkFileContains('src/main.tsx', [/VITE_WORKOS_CLIENT_ID|import\.meta\.env/])), ); // Check build succeeds diff --git a/tests/evals/graders/tanstack.grader.ts b/tests/evals/graders/tanstack.grader.ts index 182acba..5c749c0 100644 --- a/tests/evals/graders/tanstack.grader.ts +++ b/tests/evals/graders/tanstack.grader.ts @@ -37,14 +37,10 @@ export class TanstackGrader implements Grader { checks.push(...authContent); // Check provider setup in root - const rootProviderChecks = await this.fileGrader.checkFileContains('app/routes/__root.tsx', [ - 'AuthKitProvider', - ]); + const rootProviderChecks = await this.fileGrader.checkFileContains('app/routes/__root.tsx', ['AuthKitProvider']); if (!rootProviderChecks.every((c) => c.passed)) { // Try alternate root location - checks.push( - ...(await this.fileGrader.checkFileContains('app/root.tsx', ['AuthKitProvider'])) - ); + checks.push(...(await this.fileGrader.checkFileContains('app/root.tsx', ['AuthKitProvider']))); } else { checks.push(...rootProviderChecks); } @@ -70,9 +66,6 @@ export class TanstackGrader implements Grader { } // Try alternate location - return this.fileGrader.checkFileContains('app/lib/auth.ts', [ - '@workos-inc/authkit', - 'createServerFn', - ]); + return this.fileGrader.checkFileContains('app/lib/auth.ts', ['@workos-inc/authkit', 'createServerFn']); } } diff --git a/tests/evals/graders/vanilla.grader.ts b/tests/evals/graders/vanilla.grader.ts index 5fa2adf..4646d3c 100644 --- a/tests/evals/graders/vanilla.grader.ts +++ b/tests/evals/graders/vanilla.grader.ts @@ -27,9 +27,7 @@ export class VanillaGrader implements Grader { checks.push(await this.fileGrader.checkFileExists('auth.js')); // Check auth script content - checks.push( - ...(await this.fileGrader.checkFileContains('auth.js', ['workos', 'getAuthorizationUrl'])) - ); + checks.push(...(await this.fileGrader.checkFileContains('auth.js', ['workos', 'getAuthorizationUrl']))); // Check index.html includes auth checks.push(...(await this.fileGrader.checkFileContains('index.html', [/auth\.js|workos/i]))); diff --git a/tests/evals/history.ts b/tests/evals/history.ts index 79780e4..34f9e2a 100644 --- a/tests/evals/history.ts +++ b/tests/evals/history.ts @@ -22,7 +22,7 @@ export interface EvalRun { export async function saveResults( results: EvalResult[], - options: { framework?: string; state?: string } + options: { framework?: string; state?: string }, ): Promise { // Ensure results directory exists await mkdir(RESULTS_DIR, { recursive: true }); @@ -59,8 +59,7 @@ export async function saveResults( } export async function loadRun(id: string): Promise { - const filepath = - id === 'latest' ? join(RESULTS_DIR, 'latest.json') : join(RESULTS_DIR, `${id}.json`); + const filepath = id === 'latest' ? join(RESULTS_DIR, 'latest.json') : join(RESULTS_DIR, `${id}.json`); const content = await readFile(filepath, 'utf-8'); return JSON.parse(content); @@ -82,12 +81,8 @@ export function compareRuns(run1: EvalRun, run2: EvalRun): void { console.log(`\nComparing ${run1.id} vs ${run2.id}\n`); console.log('Summary:'); - console.log( - ` Run 1: ${run1.summary.passed}/${run1.summary.total} (${(run1.summary.passRate * 100).toFixed(1)}%)` - ); - console.log( - ` Run 2: ${run2.summary.passed}/${run2.summary.total} (${(run2.summary.passRate * 100).toFixed(1)}%)` - ); + console.log(` Run 1: ${run1.summary.passed}/${run1.summary.total} (${(run1.summary.passRate * 100).toFixed(1)}%)`); + console.log(` Run 2: ${run2.summary.passed}/${run2.summary.total} (${(run2.summary.passRate * 100).toFixed(1)}%)`); const diff = run2.summary.passRate - run1.summary.passRate; const trend = diff > 0 ? '↑' : diff < 0 ? '↓' : '→'; diff --git a/tests/evals/index.ts b/tests/evals/index.ts index 1609edf..9dd99fa 100644 --- a/tests/evals/index.ts +++ b/tests/evals/index.ts @@ -23,9 +23,7 @@ async function main() { console.log('Recent eval runs:'); for (const run of runs.slice(0, 10)) { const data = await loadRun(run.replace('.json', '')); - console.log( - ` ${run.replace('.json', '')} - ${data.summary.passed}/${data.summary.total} passed` - ); + console.log(` ${run.replace('.json', '')} - ${data.summary.passed}/${data.summary.total} passed`); } break; } diff --git a/tests/evals/runner.ts b/tests/evals/runner.ts index 01db25e..1c90c30 100644 --- a/tests/evals/runner.ts +++ b/tests/evals/runner.ts @@ -51,9 +51,7 @@ export async function runEvals(options: ExtendedEvalOptions): Promise - (!options.framework || s.framework === options.framework) && - (!options.state || s.state === options.state) + (s) => (!options.framework || s.framework === options.framework) && (!options.state || s.state === options.state), ); for (const scenario of scenarios) { @@ -117,9 +115,7 @@ export async function runEvals(options: ExtendedEvalOptions): Promise 1 - ? ` (attempt ${lastResult.attempts}/${maxAttempts})` - : ''; + lastResult.attempts && lastResult.attempts > 1 ? ` (attempt ${lastResult.attempts}/${maxAttempts})` : ''; console.log(`${status}${attemptInfo}`); if (!lastResult.passed && !options.verbose) { diff --git a/tests/fixtures/nextjs/existing-auth0/app/layout.tsx b/tests/fixtures/nextjs/existing-auth0/app/layout.tsx index 947036b..69bcb6b 100644 --- a/tests/fixtures/nextjs/existing-auth0/app/layout.tsx +++ b/tests/fixtures/nextjs/existing-auth0/app/layout.tsx @@ -1,19 +1,14 @@ import Link from 'next/link'; import { UserProvider } from '@auth0/nextjs-auth0/client'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} diff --git a/tests/fixtures/nextjs/existing/app/layout.tsx b/tests/fixtures/nextjs/existing/app/layout.tsx index 0cf48dd..9d0c20c 100644 --- a/tests/fixtures/nextjs/existing/app/layout.tsx +++ b/tests/fixtures/nextjs/existing/app/layout.tsx @@ -1,16 +1,11 @@ import Link from 'next/link'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} diff --git a/tests/fixtures/nextjs/fresh/app/layout.tsx b/tests/fixtures/nextjs/fresh/app/layout.tsx index 225b603..f3ef34c 100644 --- a/tests/fixtures/nextjs/fresh/app/layout.tsx +++ b/tests/fixtures/nextjs/fresh/app/layout.tsx @@ -1,8 +1,4 @@ -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} diff --git a/tests/fixtures/react/existing-auth0/src/App.tsx b/tests/fixtures/react/existing-auth0/src/App.tsx index 0d0d707..d90d760 100644 --- a/tests/fixtures/react/existing-auth0/src/App.tsx +++ b/tests/fixtures/react/existing-auth0/src/App.tsx @@ -10,8 +10,7 @@ function App() { return (