/** * TanStack Router as a Screens app (`src/resolution/frameworks/tanstack-router.ts`, * `src/resolution/tanstack-router-synthesizer.ts`): routes declared file-based * (`createFileRoute('/posts/$postId')`) and code-based (`createRoute({ path, * getParentRoute })`), and the navigation between them — where the destination * is the route PATTERN rather than a filled URL, and rides under a `to` key. * * The fixture is the TanStack kitchen-sink and basic examples' shape. Mirrors * `react-router.test.ts`. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; import { initGrammars, loadAllGrammars } from '../src/extraction/grammars'; import { buildScreens } from '../src/ui-server/api/screens'; import { parseTanstackRoutes, tanstackPath, tanstackNavVerb, tanstackDestination, } from '../src/resolution/frameworks/tanstack-router'; import type { Node } from '../src/types'; // ============================================================================= // Paths // ============================================================================= describe('tanstack: tanstackPath', () => { it.each([ ['/', '/'], ['/login', '/login'], ['/posts/$postId', '/posts/:postId'], // A pathless layout is not in the URL; nor is a route group. ['/_auth/profile', '/profile'], ['/_pathlessLayout/route-a', '/route-a'], ['/(this-folder-is-not-in-the-url)/route-group', '/route-group'], // An index route's trailing slash is the address of its parent. ['/dashboard/', '/dashboard'], // A trailing `_` un-nests without changing the segment. ['/posts_/$postId/edit', '/posts/:postId/edit'], ['/files/$', '/files/:splat*'], ])('%s → %s', (raw, normalized) => { expect(tanstackPath(raw)).toBe(normalized); }); it('a path that names no address is nothing', () => { expect(tanstackPath('posts')).toBeNull(); }); }); // ============================================================================= // Reading the routes // ============================================================================= describe('tanstack: parseTanstackRoutes — file-based', () => { it('takes the path from the literal and the component from the options', () => { const src = "import { createFileRoute } from '@tanstack/react-router'\n" + "export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({\n" + ' params: { parse: (p) => ({ invoiceId: Number(p.invoiceId) }) },\n' + ' component: InvoiceComponent,\n' + '})\n'; expect(parseTanstackRoutes(src)).toEqual([ { path: '/dashboard/invoices/:invoiceId', component: 'InvoiceComponent', index: false, fileBased: true, line: 2 }, ]); }); it('finds a component written on a chained .update()', () => { const src = "export const Route = createFileRoute('/login')({\n" + ' validateSearch: z.object({ redirect: z.string().optional() }),\n' + '}).update({\n' + ' component: LoginComponent,\n' + '})\n'; expect(parseTanstackRoutes(src)[0]).toMatchObject({ path: '/login', component: 'LoginComponent' }); }); it('marks an index route, and drops a pathless layout that is no address of its own', () => { expect(parseTanstackRoutes("createFileRoute('/dashboard/')({ component: X })")[0]).toMatchObject({ path: '/dashboard', index: true, }); expect(parseTanstackRoutes("createFileRoute('/_auth')({ component: X })")).toEqual([]); // …but the index INSIDE a pathless layout is the page at that layout's // address — `_layout/index.tsx` is a project's home page. expect(parseTanstackRoutes("createFileRoute('/_layout/')({ component: Home })")[0]).toMatchObject({ path: '/', index: true, }); }); }); describe('tanstack: parseTanstackRoutes — code-based', () => { const src = "import { createRootRoute, createRoute } from '@tanstack/react-router'\n" + 'const rootRoute = createRootRoute({ component: RootComponent })\n' + 'const indexRoute = createRoute({\n' + ' getParentRoute: () => rootRoute,\n' + " path: '/',\n" + ' component: IndexComponent,\n' + '})\n' + 'const postsLayoutRoute = createRoute({\n' + ' getParentRoute: () => rootRoute,\n' + " path: 'posts',\n" + ' component: PostsLayoutComponent,\n' + '})\n' + 'const postsIndexRoute = createRoute({\n' + ' getParentRoute: () => postsLayoutRoute,\n' + " path: '/',\n" + ' component: PostsIndexComponent,\n' + '})\n' + 'const postRoute = createRoute({\n' + ' getParentRoute: () => postsLayoutRoute,\n' + " path: '$postId',\n" + ' component: PostComponent,\n' + '})\n' + 'const pathlessRoute = createRoute({\n' + ' getParentRoute: () => rootRoute,\n' + " id: 'pathless',\n" + ' component: PathlessComponent,\n' + '})\n' + 'const routeARoute = createRoute({\n' + ' getParentRoute: () => pathlessRoute,\n' + " path: '/route-a',\n" + ' component: RouteAComponent,\n' + '})\n'; it('composes a path through getParentRoute, and a pathless layout adds nothing to it', () => { expect(parseTanstackRoutes(src).map((r) => [r.path, r.component])).toEqual([ ['/', 'IndexComponent'], ['/posts', 'PostsIndexComponent'], ['/posts/:postId', 'PostComponent'], ['/route-a', 'RouteAComponent'], ]); }); it('a layout with children is not itself a page at that address', () => { // `postsLayoutRoute` sits at `/posts` and wraps the index that renders there. const posts = parseTanstackRoutes(src).filter((r) => r.path === '/posts'); expect(posts).toHaveLength(1); expect(posts[0]!.component).toBe('PostsIndexComponent'); }); }); // ============================================================================= // Destinations // ============================================================================= describe('tanstack: destinations', () => { it.each([ ['navigate', 'navigate'], ['redirect', 'redirect'], ['router.navigate', 'navigate'], ])('%s is a navigation', (name, verb) => { expect(tanstackNavVerb(name)).toBe(verb); }); it.each(['push', 'replace', 'paths.push', 'goto'])('%s is not', (name) => { expect(tanstackNavVerb(name)).toBeNull(); }); it('reads the `to` key, and normalises the pattern the way a route name is', () => { expect(tanstackDestination("{ to: '/posts/$postId' }")?.path).toBe('/posts/:postId'); expect(tanstackDestination("{ to: '/login', search: { redirect } }")?.path).toBe('/login'); expect(tanstackDestination("'/posts/$postId'")?.path).toBe('/posts/:postId'); }); it('a navigation with no destination changes the search on the page it is on', () => { expect(tanstackDestination('{ search: (old) => ({ ...old, page: 2 }) }')).toBeNull(); }); }); // ============================================================================= // The whole picture, indexed // ============================================================================= describe('tanstack: a routed app end to end', () => { let tmpDir: string; let cg: CodeGraph; function write(rel: string, content: string): void { const full = path.join(tmpDir, rel); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, content); } beforeAll(async () => { await initGrammars(); await loadAllGrammars(); tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-tanstack-')); write('package.json', JSON.stringify({ name: 'app', dependencies: { react: '19', '@tanstack/react-router': '1' } })); write( 'src/routes/index.tsx', "import { createFileRoute, Link } from '@tanstack/react-router'\n" + "export const Route = createFileRoute('/')({ component: IndexComponent })\n" + 'function IndexComponent() {\n' + ' return (\n' + '