diff --git a/examples/preact/authenticated-routes/index.html b/examples/preact/authenticated-routes/index.html new file mode 100644 index 00000000000..5c7fd8a77e9 --- /dev/null +++ b/examples/preact/authenticated-routes/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Router - Preact Authenticated Routes + + +
+ + + diff --git a/examples/preact/authenticated-routes/package.json b/examples/preact/authenticated-routes/package.json new file mode 100644 index 00000000000..54a467d4caa --- /dev/null +++ b/examples/preact/authenticated-routes/package.json @@ -0,0 +1,27 @@ +{ + "name": "tanstack-router-preact-example-authenticated-routes", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/preact-router-devtools": "workspace:*", + "@tanstack/router-plugin": "workspace:*", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.2" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/examples/preact/authenticated-routes/src/auth.tsx b/examples/preact/authenticated-routes/src/auth.tsx new file mode 100644 index 00000000000..478e4ebbf98 --- /dev/null +++ b/examples/preact/authenticated-routes/src/auth.tsx @@ -0,0 +1,68 @@ +import { createContext } from 'preact' +import { useContext, useState, useEffect, useCallback } from 'preact/hooks' + +import { sleep } from './utils' + +export interface AuthContext { + isAuthenticated: boolean + login: (username: string) => Promise + logout: () => Promise + user: string | null +} + +const AuthContext = createContext(null) + +const key = 'tanstack.auth.user' + +function getStoredUser() { + return localStorage.getItem(key) +} + +function setStoredUser(user: string | null) { + if (user) { + localStorage.setItem(key, user) + } else { + localStorage.removeItem(key) + } +} + +export function AuthProvider({ + children, +}: { + children: preact.ComponentChildren +}) { + const [user, setUser] = useState(getStoredUser()) + const isAuthenticated = !!user + + const logout = useCallback(async () => { + await sleep(250) + + setStoredUser(null) + setUser(null) + }, []) + + const login = useCallback(async (username: string) => { + await sleep(500) + + setStoredUser(username) + setUser(username) + }, []) + + useEffect(() => { + setUser(getStoredUser()) + }, []) + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} diff --git a/examples/preact/authenticated-routes/src/main.tsx b/examples/preact/authenticated-routes/src/main.tsx new file mode 100644 index 00000000000..26f8f235e9c --- /dev/null +++ b/examples/preact/authenticated-routes/src/main.tsx @@ -0,0 +1,42 @@ +import { render } from 'preact' +import { RouterProvider, createRouter } from '@tanstack/preact-router' + +import { routeTree } from './routeTree.gen' +import { AuthProvider, useAuth } from './auth' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, + context: { + auth: undefined!, // This will be set after we wrap the app in an AuthProvider + }, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +function InnerApp() { + const auth = useAuth() + return +} + +function App() { + return ( + + + + ) +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} diff --git a/examples/preact/authenticated-routes/src/posts.ts b/examples/preact/authenticated-routes/src/posts.ts new file mode 100644 index 00000000000..3c74418441c --- /dev/null +++ b/examples/preact/authenticated-routes/src/posts.ts @@ -0,0 +1,23 @@ +import axios from 'redaxios' + +export type InvoiceType = { + id: number + title: string + body: string +} + +export const fetchInvoices = async () => { + console.info('Fetching invoices...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +export const fetchInvoiceById = async (id: number) => { + console.info(`Fetching invoice with id ${id}...`) + await new Promise((r) => setTimeout(r, 500)) + return axios + .get(`https://jsonplaceholder.typicode.com/posts/${id}`) + .then((r) => r.data) +} diff --git a/examples/preact/authenticated-routes/src/routes/__root.tsx b/examples/preact/authenticated-routes/src/routes/__root.tsx new file mode 100644 index 00000000000..6222423d6cb --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/__root.tsx @@ -0,0 +1,17 @@ +import { Outlet, createRootRouteWithContext } from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' + +import type { AuthContext } from '../auth' + +interface MyRouterContext { + auth: AuthContext +} + +export const Route = createRootRouteWithContext()({ + component: () => ( + <> + + + + ), +}) diff --git a/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx b/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx new file mode 100644 index 00000000000..1aec8e70f49 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.dashboard.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/preact-router' + +import { useAuth } from '../auth' + +export const Route = createFileRoute('/_auth/dashboard')({ + component: DashboardPage, +}) + +function DashboardPage() { + const auth = useAuth() + + return ( +
+

Hi {auth.user}!

+

You are currently on the dashboard route.

+
+ ) +} diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx new file mode 100644 index 00000000000..3fb0458ee4a --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.$invoiceId.tsx @@ -0,0 +1,30 @@ +import { createFileRoute } from '@tanstack/preact-router' + +import { fetchInvoiceById } from '../posts' + +export const Route = createFileRoute('/_auth/invoices/$invoiceId')({ + loader: async ({ params: { invoiceId } }) => { + return { + invoice: await fetchInvoiceById(parseInt(invoiceId)), + } + }, + component: InvoicePage, +}) + +function InvoicePage() { + const { invoice } = Route.useLoaderData() + + return ( +
+

+ Invoice No. #{invoice.id.toString().padStart(2, '0')} +

+

+ Invoice title: {invoice.title} +

+

+ Invoice body: {invoice.body} +

+
+ ) +} diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx new file mode 100644 index 00000000000..ca17732f950 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/preact-router' + +export const Route = createFileRoute('/_auth/invoices/')({ + component: () =>
Select an invoice to view it!
, +}) diff --git a/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx b/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx new file mode 100644 index 00000000000..e46c564b21c --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.invoices.tsx @@ -0,0 +1,42 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/preact-router' + +import { fetchInvoices } from '../posts' + +export const Route = createFileRoute('/_auth/invoices')({ + loader: async () => ({ + invoices: await fetchInvoices(), + }), + component: InvoicesRoute, +}) + +function InvoicesRoute() { + const { invoices } = Route.useLoaderData() + + return ( +
+
+

Choose an invoice from the list below.

+
    + {invoices.map((invoice) => ( +
  1. + + + #{invoice.id.toString().padStart(2, '0')} + {' '} + - {invoice.title.slice(0, 16)}... + +
  2. + ))} +
+
+
+ +
+
+ ) +} diff --git a/examples/preact/authenticated-routes/src/routes/_auth.tsx b/examples/preact/authenticated-routes/src/routes/_auth.tsx new file mode 100644 index 00000000000..324b5fab794 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/_auth.tsx @@ -0,0 +1,75 @@ +import { + Link, + Outlet, + createFileRoute, + redirect, + useRouter, +} from '@tanstack/preact-router' + +import { useAuth } from '../auth' + +export const Route = createFileRoute('/_auth')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ + to: '/login', + search: { + redirect: location.href, + }, + }) + } + }, + component: AuthLayout, +}) + +function AuthLayout() { + const router = useRouter() + const navigate = Route.useNavigate() + const auth = useAuth() + + const handleLogout = () => { + if (window.confirm('Are you sure you want to logout?')) { + auth.logout().then(() => { + router.invalidate().finally(() => { + navigate({ to: '/' }) + }) + }) + } + } + + return ( +
+

Authenticated Route

+

This route's content is only visible to authenticated users.

+
    +
  • + + Dashboard + +
  • +
  • + + Invoices + +
  • +
  • + +
  • +
+
+ +
+ ) +} diff --git a/examples/preact/authenticated-routes/src/routes/index.tsx b/examples/preact/authenticated-routes/src/routes/index.tsx new file mode 100644 index 00000000000..bd4578caa23 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/index.tsx @@ -0,0 +1,46 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link } from '@tanstack/preact-router' + +export const Route = createFileRoute('/')({ + component: HomeComponent, +}) + +function HomeComponent() { + return ( +
+

Welcome!

+

+ IMPORTANT!!! This is just an + example of how to use authenticated routes with TanStack Router. +
+ This is NOT an example how you'd write a production-level authentication + system. +
+ You'll need to take the concepts and patterns used in this example and + adapt then to work with your authentication flow/system for your app. +

+

+ You are currently on the index route of the{' '} + authenticated-routes example. +

+

You can try going through these options.

+
    +
  1. + + Go to the public login page. + +
  2. +
  3. + + Go to the auth-only dashboard page. + +
  4. +
  5. + + Go to the auth-only invoices page. + +
  6. +
+
+ ) +} diff --git a/examples/preact/authenticated-routes/src/routes/login.tsx b/examples/preact/authenticated-routes/src/routes/login.tsx new file mode 100644 index 00000000000..0694775e4b8 --- /dev/null +++ b/examples/preact/authenticated-routes/src/routes/login.tsx @@ -0,0 +1,94 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { useState } from 'preact/hooks' +import { redirect, useRouter, useRouterState } from '@tanstack/preact-router' +import { z } from 'zod' + +import { useAuth } from '../auth' +import { sleep } from '../utils' + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion +const fallback = '/dashboard' as const + +export const Route = createFileRoute('/login')({ + validateSearch: z.object({ + redirect: z.string().optional().catch(''), + }), + beforeLoad: ({ context, search }) => { + if (context.auth.isAuthenticated) { + throw redirect({ to: search.redirect || fallback }) + } + }, + component: LoginComponent, +}) + +function LoginComponent() { + const auth = useAuth() + const router = useRouter() + const isLoading = useRouterState({ select: (s) => s.isLoading }) + const navigate = Route.useNavigate() + const [isSubmitting, setIsSubmitting] = useState(false) + + const search = Route.useSearch() + + const onFormSubmit = async (evt: Event) => { + setIsSubmitting(true) + try { + evt.preventDefault() + const form = evt.target as HTMLFormElement + const data = new FormData(form) + const fieldValue = data.get('username') + + if (!fieldValue) return + const username = fieldValue.toString() + await auth.login(username) + + await router.invalidate() + + // This is just a hack being used to wait for the auth state to update + // in a real app, you'd want to use a more robust solution + await sleep(1) + + await navigate({ to: search.redirect || fallback }) + } catch (error) { + console.error('Error logging in: ', error) + } finally { + setIsSubmitting(false) + } + } + + const isLoggingIn = isLoading || isSubmitting + + return ( +
+

Login page

+ {search.redirect ? ( +

You need to login to access this page.

+ ) : ( +

Login to see all the cool content in here.

+ )} +
+
+
+ + +
+ +
+
+
+ ) +} diff --git a/examples/preact/authenticated-routes/src/styles.css b/examples/preact/authenticated-routes/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/examples/preact/authenticated-routes/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/examples/preact/authenticated-routes/src/utils.ts b/examples/preact/authenticated-routes/src/utils.ts new file mode 100644 index 00000000000..4f3faad1343 --- /dev/null +++ b/examples/preact/authenticated-routes/src/utils.ts @@ -0,0 +1,3 @@ +export async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/examples/preact/authenticated-routes/tsconfig.json b/examples/preact/authenticated-routes/tsconfig.json new file mode 100644 index 00000000000..3b34947304d --- /dev/null +++ b/examples/preact/authenticated-routes/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} diff --git a/examples/preact/authenticated-routes/vite.config.js b/examples/preact/authenticated-routes/vite.config.js new file mode 100644 index 00000000000..b239d60f4f7 --- /dev/null +++ b/examples/preact/authenticated-routes/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'preact', + autoCodeSplitting: true, + }), + preact(), + ], +}) diff --git a/examples/preact/basic-file-based/index.html b/examples/preact/basic-file-based/index.html new file mode 100644 index 00000000000..f9d481eb748 --- /dev/null +++ b/examples/preact/basic-file-based/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Router - Preact File-Based + + +
+ + + diff --git a/examples/preact/basic-file-based/package.json b/examples/preact/basic-file-based/package.json new file mode 100644 index 00000000000..fb0590ff52a --- /dev/null +++ b/examples/preact/basic-file-based/package.json @@ -0,0 +1,27 @@ +{ + "name": "tanstack-router-preact-example-basic-file-based", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/preact-router-devtools": "workspace:*", + "@tanstack/router-plugin": "workspace:*", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17", + "zod": "^3.24.2" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/examples/preact/basic-file-based/postcss.config.mjs b/examples/preact/basic-file-based/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/examples/preact/basic-file-based/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/preact/basic-file-based/src/main.tsx b/examples/preact/basic-file-based/src/main.tsx new file mode 100644 index 00000000000..cddc50db860 --- /dev/null +++ b/examples/preact/basic-file-based/src/main.tsx @@ -0,0 +1,25 @@ +import { render } from 'preact' +import { RouterProvider, createRouter } from '@tanstack/preact-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} diff --git a/examples/preact/basic-file-based/src/posts.tsx b/examples/preact/basic-file-based/src/posts.tsx new file mode 100644 index 00000000000..e07ecd42326 --- /dev/null +++ b/examples/preact/basic-file-based/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/preact-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/examples/preact/basic-file-based/src/routes/__root.tsx b/examples/preact/basic-file-based/src/routes/__root.tsx new file mode 100644 index 00000000000..c679b9db8ae --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import { createFileRoute, Link, Outlet } from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' + +export const Route = createFileRoute('/__root')({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Pathless Layout + {' '} + + Anchor + {' '} + + This Route Does Not Exist + +
+
+ + {/* Start rendering router matches */} + + + ) +} diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx new file mode 100644 index 00000000000..a96fbbda14a --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Outlet } from '@tanstack/preact-router' + +export const Route = createFileRoute('/_pathlessLayout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a pathless layout
+
+ +
+
+ ) +} diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx new file mode 100644 index 00000000000..b7110984deb --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout.tsx @@ -0,0 +1,35 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link, Outlet } from '@tanstack/preact-router' + +export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested pathless layout
+
+ + Go to route A + + + Go to route B + +
+
+ +
+
+ ) +} diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx new file mode 100644 index 00000000000..957fa5f89bd --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-a.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/preact-router' +export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')( + { + component: LayoutAComponent, + }, +) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx new file mode 100644 index 00000000000..0adb241a1da --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/_pathlessLayout/_nested-layout/route-b.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/preact-router' +export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')( + { + component: LayoutBComponent, + }, +) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/examples/preact/basic-file-based/src/routes/anchor.tsx b/examples/preact/basic-file-based/src/routes/anchor.tsx new file mode 100644 index 00000000000..1c7b9419e2f --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/anchor.tsx @@ -0,0 +1,209 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { useLayoutEffect, useRef, useState } from 'preact/hooks' +import { Link, useLocation, useNavigate } from '@tanstack/preact-router' + +export const Route = createFileRoute('/anchor')({ + component: AnchorComponent, +}) + +const anchors: Array<{ + id: string + title: string + hashScrollIntoView?: boolean | ScrollIntoViewOptions +}> = [ + { + id: 'default-anchor', + title: 'Default Anchor', + }, + { + id: 'false-anchor', + title: 'No Scroll Into View', + hashScrollIntoView: false, + }, + { + id: 'smooth-scroll', + title: 'Smooth Scroll', + hashScrollIntoView: { behavior: 'smooth' }, + }, +] as const + +function AnchorSection({ id, title }: { id: string; title: string }) { + const [hasShown, setHasShown] = useState(false) + const elementRef = useRef(null) + + useLayoutEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (!hasShown && entry.isIntersecting) { + setHasShown(true) + } + }, + { threshold: 0.01 }, + ) + + const currentRef = elementRef.current + if (currentRef) { + observer.observe(currentRef) + } + + return () => { + if (currentRef) { + observer.unobserve(currentRef) + } + } + }, [hasShown]) + + return ( +
+

+ {title} + {hasShown ? ' (shown)' : ''} +

+
+ ) +} + +function AnchorComponent() { + const navigate = useNavigate() + const location = useLocation() + const [withScroll, setWithScroll] = useState(true) + + return ( +
+ +
+
{ + event.preventDefault() + event.stopPropagation() + const formData = new FormData(event.target as HTMLFormElement) + + const toHash = formData.get('hash') as string + + if (!toHash) { + return + } + + const hashScrollIntoView = withScroll + ? ({ + behavior: formData.get('scrollBehavior') as ScrollBehavior, + block: formData.get('scrollBlock') as ScrollLogicalPosition, + inline: formData.get('scrollInline') as ScrollLogicalPosition, + } satisfies ScrollIntoViewOptions) + : false + + navigate({ hash: toHash, hashScrollIntoView }) + }} + > +

Scroll with navigate

+
+ +
+ +
+
+ {withScroll ? ( + <> +
+ +
+ +
+ +
+ +
+ +
+ + ) : null} +
+ +
+
+ + {anchors.map((anchor) => ( + + ))} +
+
+ ) +} diff --git a/examples/preact/basic-file-based/src/routes/index.tsx b/examples/preact/basic-file-based/src/routes/index.tsx new file mode 100644 index 00000000000..ebf57a66ddb --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/preact-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!

+
+ ) +} diff --git a/examples/preact/basic-file-based/src/routes/posts.$postId.tsx b/examples/preact/basic-file-based/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..22f006148c0 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/posts.$postId.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { ErrorComponent } from '@tanstack/preact-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post.title}

+
{post.body}
+
+ ) +} diff --git a/examples/preact/basic-file-based/src/routes/posts.index.tsx b/examples/preact/basic-file-based/src/routes/posts.index.tsx new file mode 100644 index 00000000000..b9404656d90 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/preact-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/examples/preact/basic-file-based/src/routes/posts.route.tsx b/examples/preact/basic-file-based/src/routes/posts.route.tsx new file mode 100644 index 00000000000..07fac1f75c5 --- /dev/null +++ b/examples/preact/basic-file-based/src/routes/posts.route.tsx @@ -0,0 +1,39 @@ +import { createFileRoute } from '@tanstack/preact-router' +import { Link, Outlet } from '@tanstack/preact-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/examples/preact/basic-file-based/src/styles.css b/examples/preact/basic-file-based/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/examples/preact/basic-file-based/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/examples/preact/basic-file-based/tailwind.config.mjs b/examples/preact/basic-file-based/tailwind.config.mjs new file mode 100644 index 00000000000..7141e4528c6 --- /dev/null +++ b/examples/preact/basic-file-based/tailwind.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/examples/preact/basic-file-based/tsconfig.json b/examples/preact/basic-file-based/tsconfig.json new file mode 100644 index 00000000000..3b34947304d --- /dev/null +++ b/examples/preact/basic-file-based/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} diff --git a/examples/preact/basic-file-based/vite.config.js b/examples/preact/basic-file-based/vite.config.js new file mode 100644 index 00000000000..b239d60f4f7 --- /dev/null +++ b/examples/preact/basic-file-based/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'preact', + autoCodeSplitting: true, + }), + preact(), + ], +}) diff --git a/examples/preact/basic-preact-query/index.html b/examples/preact/basic-preact-query/index.html new file mode 100644 index 00000000000..63fbc3049ab --- /dev/null +++ b/examples/preact/basic-preact-query/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Router - Preact Query + + +
+ + + diff --git a/examples/preact/basic-preact-query/package.json b/examples/preact/basic-preact-query/package.json new file mode 100644 index 00000000000..4e6bc03752b --- /dev/null +++ b/examples/preact/basic-preact-query/package.json @@ -0,0 +1,27 @@ +{ + "name": "tanstack-router-preact-example-preact-query", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/preact-router-devtools": "workspace:*", + "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/examples/preact/basic-preact-query/src/main.tsx b/examples/preact/basic-preact-query/src/main.tsx new file mode 100644 index 00000000000..3691b7939ba --- /dev/null +++ b/examples/preact/basic-preact-query/src/main.tsx @@ -0,0 +1,278 @@ +import { render } from 'preact' +import { useEffect } from 'preact/hooks' +import { + ErrorComponent, + Link, + Outlet, + RouterProvider, + createRootRouteWithContext, + createRoute, + createRouter, + useRouter, +} from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { + QueryClient, + QueryClientProvider, + useQueryErrorResetBoundary, + useSuspenseQuery, +} from '@tanstack/react-query' +import { NotFoundError, postQueryOptions, postsQueryOptions } from './posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' +import './styles.css' + +const rootRoute = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Pathless Layout + {' '} + + This Route Does Not Exist + +
+
+ + + + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexRouteComponent, +}) + +function IndexRouteComponent() { + return ( +
+

Welcome Home!

+
+ ) +} + +const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: ({ context: { queryClient } }) => + queryClient.ensureQueryData(postsQueryOptions), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexRouteComponent, +}) + +function PostsIndexRouteComponent() { + return
Select a post.
+} + +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ context: { queryClient }, params: { postId } }) => + queryClient.ensureQueryData(postQueryOptions(postId)), + component: PostRouteComponent, +}) + +function PostErrorComponent({ error }: ErrorComponentProps) { + const router = useRouter() + if (error instanceof NotFoundError) { + return
{error.message}
+ } + const queryErrorResetBoundary = useQueryErrorResetBoundary() + + useEffect(() => { + queryErrorResetBoundary.reset() + }, [queryErrorResetBoundary]) + + return ( +
+ + +
+ ) +} + +function PostRouteComponent() { + const { postId } = postRoute.useParams() + const postQuery = useSuspenseQuery(postQueryOptions(postId)) + const post = postQuery.data + + return ( +
+

{post.title}

+
{post.body}
+
+ ) +} + +const pathlessLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_pathlessLayout', + component: PathlessLayoutComponent, +}) + +function PathlessLayoutComponent() { + return ( +
+
I'm a pathless layout
+
+ +
+
+ ) +} + +const nestedPathlessLayoutRoute = createRoute({ + getParentRoute: () => pathlessLayoutRoute, + id: '_nestedPathlessLayout', + component: Layout2Component, +}) + +function Layout2Component() { + return ( +
+
I'm a nested pathless layout
+
+ + Go to route A + + + Go to route B + +
+
+ +
+
+ ) +} + +const pathlessLayoutARoute = createRoute({ + getParentRoute: () => nestedPathlessLayoutRoute, + path: '/route-a', + component: PathlessLayoutAComponent, +}) + +function PathlessLayoutAComponent() { + return
I'm layout A!
+} + +const pathlessLayoutBRoute = createRoute({ + getParentRoute: () => nestedPathlessLayoutRoute, + path: '/route-b', + component: PathlessLayoutBComponent, +}) + +function PathlessLayoutBComponent() { + return
I'm layout B!
+} + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + pathlessLayoutRoute.addChildren([ + nestedPathlessLayoutRoute.addChildren([ + pathlessLayoutARoute, + pathlessLayoutBRoute, + ]), + ]), + indexRoute, +]) + +const queryClient = new QueryClient() + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + // Since we're using React Query, we don't want loader calls to ever be stale + // This will ensure that the loader is always called when the route is preloaded or visited + defaultPreloadStaleTime: 0, + scrollRestoration: true, + context: { + queryClient, + }, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render( + + + , + rootElement, + ) +} diff --git a/examples/preact/basic-preact-query/src/posts.lazy.tsx b/examples/preact/basic-preact-query/src/posts.lazy.tsx new file mode 100644 index 00000000000..13c8a7be6c8 --- /dev/null +++ b/examples/preact/basic-preact-query/src/posts.lazy.tsx @@ -0,0 +1,39 @@ +import { Link, Outlet, createLazyRoute } from '@tanstack/preact-router' +import { useSuspenseQuery } from '@tanstack/react-query' +import { postsQueryOptions } from './posts' + +export const Route = createLazyRoute('/posts')({ + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const postsQuery = useSuspenseQuery(postsQueryOptions) + + const posts = postsQuery.data + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+ +
+ ) +} diff --git a/examples/preact/basic-preact-query/src/posts.ts b/examples/preact/basic-preact-query/src/posts.ts new file mode 100644 index 00000000000..54e126683e3 --- /dev/null +++ b/examples/preact/basic-preact-query/src/posts.ts @@ -0,0 +1,44 @@ +import axios from 'redaxios' +import { queryOptions } from '@tanstack/react-query' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} + +export const postQueryOptions = (postId: string) => + queryOptions({ + queryKey: ['posts', { postId }], + queryFn: () => fetchPost(postId), + }) + +export const postsQueryOptions = queryOptions({ + queryKey: ['posts'], + queryFn: () => fetchPosts(), +}) diff --git a/examples/preact/basic-preact-query/src/styles.css b/examples/preact/basic-preact-query/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/examples/preact/basic-preact-query/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/examples/preact/basic-preact-query/tsconfig.json b/examples/preact/basic-preact-query/tsconfig.json new file mode 100644 index 00000000000..3b34947304d --- /dev/null +++ b/examples/preact/basic-preact-query/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} diff --git a/examples/preact/basic-preact-query/vite.config.js b/examples/preact/basic-preact-query/vite.config.js new file mode 100644 index 00000000000..29b326faf09 --- /dev/null +++ b/examples/preact/basic-preact-query/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()], +}) diff --git a/examples/preact/basic/.gitignore b/examples/preact/basic/.gitignore new file mode 100644 index 00000000000..8354e4d50d5 --- /dev/null +++ b/examples/preact/basic/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/examples/preact/basic/.vscode/settings.json b/examples/preact/basic/.vscode/settings.json new file mode 100644 index 00000000000..00b5278e580 --- /dev/null +++ b/examples/preact/basic/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/examples/preact/basic/README.md b/examples/preact/basic/README.md new file mode 100644 index 00000000000..115199d292c --- /dev/null +++ b/examples/preact/basic/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm start` or `yarn start` diff --git a/examples/preact/basic/index.html b/examples/preact/basic/index.html new file mode 100644 index 00000000000..4dcb5c0570b --- /dev/null +++ b/examples/preact/basic/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/examples/preact/basic/package.json b/examples/preact/basic/package.json new file mode 100644 index 00000000000..afbb1505e0a --- /dev/null +++ b/examples/preact/basic/package.json @@ -0,0 +1,25 @@ +{ + "name": "tanstack-router-preact-example-basic", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "autoprefixer": "^10.4.20", + "express": "^4.21.2", + "postcss": "^8.5.1", + "preact": "^10.24.3", + "redaxios": "^0.5.1", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/examples/preact/basic/postcss.config.mjs b/examples/preact/basic/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/examples/preact/basic/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/preact/basic/server.js b/examples/preact/basic/server.js new file mode 100644 index 00000000000..fe12afcd7c3 --- /dev/null +++ b/examples/preact/basic/server.js @@ -0,0 +1,46 @@ +import express from 'express' +import { createServer as createViteServer } from 'vite' + +const app = express() + +const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: 'custom', +}) + +app.use(vite.middlewares) + +app.use('*', async (req, res) => { + try { + const url = req.originalUrl + + let viteHead = await vite.transformIndexHtml( + url, + `
`, + ) + + viteHead = viteHead.substring( + viteHead.indexOf('') + 6, + viteHead.indexOf(''), + ) + + const entry = await vite.ssrLoadModule('/src/entry-server.tsx') + + // Test streaming by checking query param + const useStreaming = req.query.stream === 'true' + + console.info('Rendering:', url, useStreaming ? '(streaming)' : '(string)') + await entry.render({ req, res, head: viteHead, useStreaming }) + } catch (e) { + vite.ssrFixStacktrace(e) + console.error(e.stack) + res.status(500).end(e.stack) + } +}) + +const port = 3000 +app.listen(port, () => { + console.info(`Server running at http://localhost:${port}`) + console.info(`Test SSR: http://localhost:${port}/`) + console.info(`Test Streaming: http://localhost:${port}/?stream=true`) +}) diff --git a/examples/preact/basic/src/entry-client.tsx b/examples/preact/basic/src/entry-client.tsx new file mode 100644 index 00000000000..00743fde176 --- /dev/null +++ b/examples/preact/basic/src/entry-client.tsx @@ -0,0 +1,7 @@ +import { render } from 'preact' +import { RouterClient } from '@tanstack/preact-router/ssr/client' +import { createRouterInstance } from './router' + +const router = createRouterInstance() + +render(, document) diff --git a/examples/preact/basic/src/entry-server.tsx b/examples/preact/basic/src/entry-server.tsx new file mode 100644 index 00000000000..1d46e6f63a4 --- /dev/null +++ b/examples/preact/basic/src/entry-server.tsx @@ -0,0 +1,83 @@ +import { pipeline } from 'node:stream/promises' +import { + RouterServer, + createRequestHandler, + renderRouterToString, + renderRouterToStream, +} from '@tanstack/preact-router/ssr/server' +import { createRouterInstance } from './router' +import type express from 'express' + +export async function render({ + req, + res, + head, + useStreaming = false, +}: { + head: string + req: express.Request + res: express.Response + useStreaming?: boolean +}) { + // Convert the express request to a fetch request + const url = new URL(req.originalUrl || req.url, 'http://localhost:3000').href + + const request = new Request(url, { + method: req.method, + headers: (() => { + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + if (typeof value === 'string') { + headers.set(key, value) + } else if (Array.isArray(value)) { + value.forEach((v) => headers.append(key, String(v))) + } + } + return headers + })(), + }) + + // Create a request handler + const handler = createRequestHandler({ + request, + createRouter: () => { + const router = createRouterInstance() + + // Update each router instance with the head info from vite + router.update({ + context: { + ...router.options.context, + head: head, + }, + }) + return router + }, + }) + + // Use either streaming or string rendering + const response = await handler(({ responseHeaders, router }) => + useStreaming + ? renderRouterToStream({ + request, + responseHeaders, + router, + children: , + }) + : renderRouterToString({ + responseHeaders, + router, + children: , + }), + ) + + // Convert the fetch response back to an express response + res.statusMessage = response.statusText + res.status(response.status) + + response.headers.forEach((value, name) => { + res.setHeader(name, value) + }) + + // Stream the response body + return pipeline(response.body as any, res) +} diff --git a/examples/preact/basic/src/main.tsx b/examples/preact/basic/src/main.tsx new file mode 100644 index 00000000000..e335054ac39 --- /dev/null +++ b/examples/preact/basic/src/main.tsx @@ -0,0 +1,230 @@ +import { render } from 'preact' +import { + ErrorComponent, + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/preact-router' +import { NotFoundError, fetchPost, fetchPosts } from './posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' +import './styles.css' + +const rootRoute = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} + + Pathless Layout + {' '} + + This Route Does Not Exist + +
+ + + ) +} +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+

Welcome Home!

+
+ ) +} + +export const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} + +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ params }) => fetchPost(params.postId), + component: PostComponent, +}) + +function PostErrorComponent({ error }: ErrorComponentProps) { + if (error instanceof NotFoundError) { + return
{error.message}
+ } + + return +} + +function PostComponent() { + const post = postRoute.useLoaderData() + + return ( +
+

{post.title}

+
+
{post.body}
+
+ ) +} + +const pathlessLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_pathlessLayout', + component: PathlessLayoutComponent, +}) + +function PathlessLayoutComponent() { + return ( +
+
I'm a pathless layout
+
+ +
+
+ ) +} + +const nestedPathlessLayout2Route = createRoute({ + getParentRoute: () => pathlessLayoutRoute, + id: '_nestedPathlessLayout', + component: PathlessLayout2Component, +}) + +function PathlessLayout2Component() { + return ( +
+
I'm a nested pathless layout
+
+ + Go to Route A + + + Go to Route B + +
+
+ +
+
+ ) +} + +const pathlessLayoutARoute = createRoute({ + getParentRoute: () => nestedPathlessLayout2Route, + path: '/route-a', + component: PathlessLayoutAComponent, +}) + +function PathlessLayoutAComponent() { + return
I'm route A!
+} + +const pathlessLayoutBRoute = createRoute({ + getParentRoute: () => nestedPathlessLayout2Route, + path: '/route-b', + component: PathlessLayoutBComponent, +}) + +function PathlessLayoutBComponent() { + return
I'm route B!
+} + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + pathlessLayoutRoute.addChildren([ + nestedPathlessLayout2Route.addChildren([ + pathlessLayoutARoute, + pathlessLayoutBRoute, + ]), + ]), + indexRoute, +]) + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, +}) + +// Register things for typesafety +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} diff --git a/examples/preact/basic/src/posts.lazy.tsx b/examples/preact/basic/src/posts.lazy.tsx new file mode 100644 index 00000000000..1e4a4daf6fa --- /dev/null +++ b/examples/preact/basic/src/posts.lazy.tsx @@ -0,0 +1,35 @@ +import { Link, Outlet, createLazyRoute } from '@tanstack/preact-router' + +export const Route = createLazyRoute('/posts')({ + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+ +
+ ) +} diff --git a/examples/preact/basic/src/posts.ts b/examples/preact/basic/src/posts.ts new file mode 100644 index 00000000000..54d62e57886 --- /dev/null +++ b/examples/preact/basic/src/posts.ts @@ -0,0 +1,32 @@ +import axios from 'redaxios' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} diff --git a/examples/preact/basic/src/preact.d.ts b/examples/preact/basic/src/preact.d.ts new file mode 100644 index 00000000000..fc03c2efc84 --- /dev/null +++ b/examples/preact/basic/src/preact.d.ts @@ -0,0 +1,6 @@ +/// + +declare namespace JSX { + type Element = preact.JSX.Element + type IntrinsicElements = preact.JSX.IntrinsicElements +} diff --git a/examples/preact/basic/src/router.tsx b/examples/preact/basic/src/router.tsx new file mode 100644 index 00000000000..a65a254c190 --- /dev/null +++ b/examples/preact/basic/src/router.tsx @@ -0,0 +1,110 @@ +import { + createRouter, + createRootRoute, + createRoute, + Outlet, +} from '@tanstack/preact-router' +import { NotFoundError, fetchPost, fetchPosts } from './posts' +import type { ErrorComponentProps } from '@tanstack/preact-router' + +const rootRoute = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ Home + Posts +
+ + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+

Welcome Home!

+
+ ) +} + +export const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} + +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: PostErrorComponent, + loader: ({ params }) => fetchPost(params.postId), + component: PostComponent, +}) + +function PostErrorComponent({ error }: ErrorComponentProps) { + if (error instanceof NotFoundError) { + return
{error.message}
+ } + return
Error: {String(error)}
+} + +function PostComponent() { + const post = postRoute.useLoaderData() + return ( +
+

{post.title}

+
+
{post.body}
+
+ ) +} + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + indexRoute, +]) + +export function createRouterInstance() { + return createRouter({ + routeTree, + context: { + head: '', + }, + defaultPreload: 'intent', + scrollRestoration: true, + }) +} + +declare module '@tanstack/preact-router' { + interface Register { + router: ReturnType + } +} diff --git a/examples/preact/basic/src/styles.css b/examples/preact/basic/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/examples/preact/basic/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/examples/preact/basic/tailwind.config.mjs b/examples/preact/basic/tailwind.config.mjs new file mode 100644 index 00000000000..4986094b9d5 --- /dev/null +++ b/examples/preact/basic/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'], +} diff --git a/examples/preact/basic/tsconfig.json b/examples/preact/basic/tsconfig.json new file mode 100644 index 00000000000..a4510fc5fc0 --- /dev/null +++ b/examples/preact/basic/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} diff --git a/examples/preact/basic/vite.config.js b/examples/preact/basic/vite.config.js new file mode 100644 index 00000000000..29b326faf09 --- /dev/null +++ b/examples/preact/basic/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()], +}) diff --git a/examples/preact/scroll-restoration/index.html b/examples/preact/scroll-restoration/index.html new file mode 100644 index 00000000000..8d033fabc8f --- /dev/null +++ b/examples/preact/scroll-restoration/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Router - Preact Scroll Restoration + + +
+ + + diff --git a/examples/preact/scroll-restoration/package.json b/examples/preact/scroll-restoration/package.json new file mode 100644 index 00000000000..c4458772d58 --- /dev/null +++ b/examples/preact/scroll-restoration/package.json @@ -0,0 +1,25 @@ +{ + "name": "tanstack-router-preact-example-scroll-restoration", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/preact-router": "workspace:*", + "@tanstack/react-virtual": "^3.13.0", + "@tanstack/preact-router-devtools": "workspace:*", + "preact": "^10.24.3", + "postcss": "^8.5.1", + "autoprefixer": "^10.4.20", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/examples/preact/scroll-restoration/src/main.tsx b/examples/preact/scroll-restoration/src/main.tsx new file mode 100644 index 00000000000..c794b2401c6 --- /dev/null +++ b/examples/preact/scroll-restoration/src/main.tsx @@ -0,0 +1,210 @@ +import { render } from 'preact' +import { useRef } from 'preact/hooks' +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, + useElementScrollRestoration, +} from '@tanstack/preact-router' +import { TanStackRouterDevtools } from '@tanstack/preact-router-devtools' +import { useVirtualizer } from '@tanstack/react-virtual' +import './styles.css' + +const rootRoute = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + About + + + About (No Reset) + + + By-Element + +
+ + + + ) +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+

Welcome Home!

+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ Home Item {i + 1} +
+ ))} +
+
+ ) +} + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: AboutComponent, +}) + +function AboutComponent() { + return ( +
+
Hello from About!
+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ About Item {i + 1} +
+ ))} +
+
+ ) +} + +const byElementRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/by-element', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: ByElementComponent, +}) + +function ByElementComponent() { + // We need a unique ID for manual scroll restoration on a specific element + // It should be as unique as possible for this element across your app + const scrollRestorationId = 'myVirtualizedContent' + + // We use that ID to get the scroll entry for this element + const scrollEntry = useElementScrollRestoration({ + id: scrollRestorationId, + }) + + // Let's use TanStack Virtual to virtualize some content! + const virtualizerParentRef = useRef(null) + const virtualizer = useVirtualizer({ + count: 10000, + getScrollElement: () => virtualizerParentRef.current, + estimateSize: () => 100, + // We pass the scrollY from the scroll restoration entry to the virtualizer + // as the initial offset + initialOffset: scrollEntry?.scrollY, + }) + + return ( +
+
Hello from By-Element!
+
+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ About Item {i + 1} +
+ ))} +
+
+ {Array.from({ length: 2 }).map((_, i) => ( +
+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ About Item {i + 1} +
+ ))} +
+
+ ))} +
+
Virtualized
+
+
+ {virtualizer.getVirtualItems().map((item) => ( +
+
+ Virtualized Item {item.index + 1} +
+
+ ))} +
+
+
+
+
+
+ ) +} + +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + byElementRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, +}) + +declare module '@tanstack/preact-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(, rootElement) +} diff --git a/examples/preact/scroll-restoration/src/styles.css b/examples/preact/scroll-restoration/src/styles.css new file mode 100644 index 00000000000..0b8e317099c --- /dev/null +++ b/examples/preact/scroll-restoration/src/styles.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} diff --git a/examples/preact/scroll-restoration/tsconfig.json b/examples/preact/scroll-restoration/tsconfig.json new file mode 100644 index 00000000000..3b34947304d --- /dev/null +++ b/examples/preact/scroll-restoration/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true + } +} diff --git a/examples/preact/scroll-restoration/vite.config.js b/examples/preact/scroll-restoration/vite.config.js new file mode 100644 index 00000000000..29b326faf09 --- /dev/null +++ b/examples/preact/scroll-restoration/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()], +}) diff --git a/examples/vanilla/authenticated-routes/index.html b/examples/vanilla/authenticated-routes/index.html new file mode 100644 index 00000000000..59b39a7167c --- /dev/null +++ b/examples/vanilla/authenticated-routes/index.html @@ -0,0 +1,111 @@ + + + + + + TanStack Router - Vanilla Authenticated Routes + + + +
+ + + diff --git a/examples/vanilla/authenticated-routes/package.json b/examples/vanilla/authenticated-routes/package.json new file mode 100644 index 00000000000..55606e0913e --- /dev/null +++ b/examples/vanilla/authenticated-routes/package.json @@ -0,0 +1,18 @@ +{ + "name": "vanilla-authenticated-routes-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3005", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^7.1.7" + } +} diff --git a/examples/vanilla/authenticated-routes/src/main.ts b/examples/vanilla/authenticated-routes/src/main.ts new file mode 100644 index 00000000000..cf939d54d8d --- /dev/null +++ b/examples/vanilla/authenticated-routes/src/main.ts @@ -0,0 +1,240 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + getMatchesHtml, + vanillaRouter, + redirect, +} from '@tanstack/vanilla-router' + +// Simple auth state management +let currentUser: { username: string } | null = null + +function login(username: string, password: string): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (username === 'admin' && password === 'password') { + currentUser = { username } + resolve() + } else { + reject(new Error('Invalid credentials')) + } + }, 500) + }) +} + +function logout() { + currentUser = null +} + +function isAuthenticated(): boolean { + return currentUser !== null +} + +function getCurrentUser() { + return currentUser +} + +// Root component +const RootComponent = (router: ReturnType) => { + const currentPath = router.state.location.pathname + const user = getCurrentUser() + + return ` + +
+ ${outlet()} +
+ ` +} + +const rootRoute = createRootRoute({ + component: RootComponent, + context: () => ({ + auth: { + isAuthenticated: isAuthenticated(), + user: getCurrentUser(), + }, + }), +}) + +// Index component (public) +const IndexComponent = (router: ReturnType) => { + const user = getCurrentUser() + return ` +
+

Welcome Home!

+

This is a public page. Anyone can access it.

+ ${ + user + ? ` +

You are logged in as ${user.username}.

+ Go to Dashboard + ` + : ` +

You are not logged in.

+ Login + ` + } +
+ ` +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +// Login component (public, but redirects if already authenticated) +const LoginComponent = (router: ReturnType) => { + return ` +
+

Login

+
+ + + + + +
+ +

+ Demo credentials:
+ Username: admin
+ Password: password +

+
+ ` +} + +const loginRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/login', + beforeLoad: ({ context }) => { + // Redirect to dashboard if already authenticated + if (context.auth.isAuthenticated) { + throw redirect({ to: '/dashboard' }) + } + }, + component: LoginComponent, +}) + +// Dashboard component (protected) +const DashboardComponent = (router: ReturnType) => { + const user = getCurrentUser() + return ` +
+

Dashboard

+

Welcome, ${user?.username}!

+

This is a protected route. Only authenticated users can access it.

+

If you try to access this page while logged out, you'll be redirected to the login page.

+ +
+ ` +} + +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + beforeLoad: ({ context }) => { + // Redirect to login if not authenticated + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } + }, + component: DashboardComponent, +}) + +// Create router +const routeTree = rootRoute.addChildren([ + indexRoute, + loginRoute, + dashboardRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + context: { + auth: { + isAuthenticated: false, + user: null, + }, + }, +}) + +// Render function +const rootElement = document.getElementById('app') +if (!rootElement) throw new Error('App element not found') + +function render() { + if (!rootElement) return + + // Update router context with current auth state + router.options.context = { + auth: { + isAuthenticated: isAuthenticated(), + user: getCurrentUser(), + }, + } + + const htmlParts = getMatchesHtml(router, router.state.matches) + rootElement.innerHTML = htmlParts.join('') + + // Update active links + const currentPath = router.state.location.pathname + const links = rootElement.querySelectorAll('nav a') + links.forEach((link) => { + const href = link.getAttribute('href') + if (href === currentPath) { + link.classList.add('active') + } else { + link.classList.remove('active') + } + }) + + // Setup login form handler + const loginForm = rootElement.querySelector('#login-form') as HTMLFormElement + if (loginForm) { + loginForm.onsubmit = async (e) => { + e.preventDefault() + const formData = new FormData(loginForm) + const username = formData.get('username') as string + const password = formData.get('password') as string + const errorDiv = rootElement.querySelector( + '#login-error', + ) as HTMLDivElement + + try { + await login(username, password) + errorDiv.style.display = 'none' + // Navigate to dashboard after successful login + await router.navigate({ to: '/dashboard' }) + } catch (error) { + errorDiv.style.display = 'block' + errorDiv.className = 'error' + errorDiv.textContent = + error instanceof Error ? error.message : 'Login failed' + } + } + } +} + +// Setup logout function +;(window as any).logout = async () => { + logout() + await router.navigate({ to: '/' }) +} + +// Setup router +vanillaRouter(router, render).catch(console.error) diff --git a/examples/vanilla/authenticated-routes/tsconfig.json b/examples/vanilla/authenticated-routes/tsconfig.json new file mode 100644 index 00000000000..1b0362ce9bf --- /dev/null +++ b/examples/vanilla/authenticated-routes/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/vanilla/authenticated-routes/vite.config.ts b/examples/vanilla/authenticated-routes/vite.config.ts new file mode 100644 index 00000000000..9306ffab9eb --- /dev/null +++ b/examples/vanilla/authenticated-routes/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve( + __dirname, + '../../../packages/vanilla-router/src', + ), + }, + }, +}) diff --git a/examples/vanilla/basic/index.html b/examples/vanilla/basic/index.html new file mode 100644 index 00000000000..8bf7c2fc097 --- /dev/null +++ b/examples/vanilla/basic/index.html @@ -0,0 +1,71 @@ + + + + + + TanStack Router - Vanilla JS + + + +
+ + + diff --git a/examples/vanilla/basic/package.json b/examples/vanilla/basic/package.json new file mode 100644 index 00000000000..ebb2894bd09 --- /dev/null +++ b/examples/vanilla/basic/package.json @@ -0,0 +1,19 @@ +{ + "name": "tanstack-router-vanilla-example-basic", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3001", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*", + "redaxios": "^0.5.1" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/examples/vanilla/basic/src/main.ts b/examples/vanilla/basic/src/main.ts new file mode 100644 index 00000000000..c2296de30df --- /dev/null +++ b/examples/vanilla/basic/src/main.ts @@ -0,0 +1,162 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + getMatchesHtml, + vanillaRouter, +} from '@tanstack/vanilla-router' +import { NotFoundError, fetchPost, fetchPosts } from './posts' + +// Root component +const RootComponent = (router: ReturnType) => { + return ` +
+ Home + Posts +
+ ${outlet()} + ` +} + +const rootRoute = createRootRoute({ + component: RootComponent, + notFoundComponent: () => (router: ReturnType) => { + return ` +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ` + }, +}) + +// Index component +const IndexComponent = (router: ReturnType) => { + return ` +
+

Welcome Home!

+
+ ` +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +// Posts layout route with component +export const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), + component: (router: ReturnType) => { + // Access data via route getters - pass router explicitly + const posts = postsLayoutRoute.getLoaderData(router) as + | Array<{ id: string; title: string }> + | undefined + + if (!posts) { + return `
Loading posts...
` + } + + return ` +
+
    + ${posts + .map( + (post) => ` +
  • + ${post.title} +
  • + `, + ) + .join('')} +
+ ${outlet()} +
+ ` + }, +}) + +// Posts index component +const PostsIndexComponent = (router: ReturnType) => { + return `
Select a post.
` +} + +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: PostsIndexComponent, +}) + +// Post route +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + errorComponent: + ({ error }) => + (router: ReturnType) => { + if (error instanceof NotFoundError) { + return `
${error.message}
` + } + return `
Error: ${String(error)}
` + }, + loader: ({ params }) => fetchPost(params.postId), + component: (router: ReturnType) => { + // Access data via route getters - pass router explicitly + const post = postRoute.getLoaderData(router) as + | { title: string; body: string } + | undefined + + if (!post) { + return `
Loading...
` + } + + return ` +
+

${post.title}

+
+
${post.body}
+
+ ` + }, +}) + +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + indexRoute, +]) + +// Create router +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, +}) + +// Register router for type safety +declare module '@tanstack/vanilla-router' { + interface Register { + router: typeof router + } +} + +// Initialize router +const rootElement = document.getElementById('app')! + +// Render function - direct DOM manipulation +function render() { + rootElement.innerHTML = '' + + const state = router.state + + // Use getMatchesHtml utility to get nested HTML strings (outlet replacement is handled internally) + const htmlParts = getMatchesHtml(router, state.matches) + rootElement.innerHTML = htmlParts.join('') +} + +// Setup router with state subscription and link handlers +await vanillaRouter(router, render) diff --git a/examples/vanilla/basic/src/posts.ts b/examples/vanilla/basic/src/posts.ts new file mode 100644 index 00000000000..13620952577 --- /dev/null +++ b/examples/vanilla/basic/src/posts.ts @@ -0,0 +1,31 @@ +import axios from 'redaxios' + +export class NotFoundError extends Error {} + +type PostType = { + id: string + title: string + body: string +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 500)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 500)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + + if (!post) { + throw new NotFoundError(`Post with id "${postId}" not found!`) + } + + return post +} diff --git a/examples/vanilla/basic/tsconfig.json b/examples/vanilla/basic/tsconfig.json new file mode 100644 index 00000000000..1b0362ce9bf --- /dev/null +++ b/examples/vanilla/basic/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/vanilla/basic/vite.config.js b/examples/vanilla/basic/vite.config.js new file mode 100644 index 00000000000..78dda83ec90 --- /dev/null +++ b/examples/vanilla/basic/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +// https://vitejs.dev/config/ +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve( + __dirname, + '../../../packages/vanilla-router/src', + ), + }, + }, +}) diff --git a/examples/vanilla/jsx-router/index.html b/examples/vanilla/jsx-router/index.html new file mode 100644 index 00000000000..ed4acadfb74 --- /dev/null +++ b/examples/vanilla/jsx-router/index.html @@ -0,0 +1,85 @@ + + + + + + TanStack Router - Vanilla JSX + + + +
+ + + diff --git a/examples/vanilla/jsx-router/package.json b/examples/vanilla/jsx-router/package.json new file mode 100644 index 00000000000..7432919f77f --- /dev/null +++ b/examples/vanilla/jsx-router/package.json @@ -0,0 +1,18 @@ +{ + "name": "vanilla-jsx-router-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^7.1.7" + } +} diff --git a/examples/vanilla/jsx-router/src/main.ts b/examples/vanilla/jsx-router/src/main.ts new file mode 100644 index 00000000000..373f9c4e539 --- /dev/null +++ b/examples/vanilla/jsx-router/src/main.ts @@ -0,0 +1,208 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + vanillaRouter, +} from '@tanstack/vanilla-router' +import { VanillaRenderer, jsx } from './renderer' + +// Mock data fetching +async function fetchPosts() { + await new Promise((resolve) => setTimeout(resolve, 500)) + return [ + { id: '1', title: 'Getting Started with Vanilla Router' }, + { id: '2', title: 'JSX Rendering Made Simple' }, + { id: '3', title: 'Building Modern Web Apps' }, + ] +} + +async function fetchPost(id: string) { + await new Promise((resolve) => setTimeout(resolve, 300)) + const posts: Record = { + '1': { + title: 'Getting Started with Vanilla Router', + body: 'Vanilla Router is a powerful routing solution for vanilla JavaScript applications. It provides type-safe routing, nested routes, and excellent developer experience.', + }, + '2': { + title: 'JSX Rendering Made Simple', + body: 'Combine the power of JSX with vanilla JavaScript. The JSX renderer makes it easy to build component-based UIs without a framework.', + }, + '3': { + title: 'Building Modern Web Apps', + body: 'Modern web applications require modern tools. Vanilla Router and JSX renderer provide a lightweight alternative to heavy frameworks.', + }, + } + if (!posts[id]) throw new Error('Post not found') + return posts[id] +} + +// Root component using JSX +const RootComponent = (router: ReturnType) => { + return () => { + return jsx( + 'div', + {}, + jsx( + 'nav', + {}, + jsx('a', { href: buildHref(router, { to: '/' }) }, 'Home'), + jsx('a', { href: buildHref(router, { to: '/posts' }) }, 'Posts'), + ), + jsx('main', {}, outlet()), + ) + } +} + +const rootRoute = createRootRoute({ + component: RootComponent, +}) + +// Index component +const IndexComponent = (router: ReturnType) => { + return () => { + return jsx( + 'div', + {}, + jsx('h1', {}, 'Welcome Home!'), + jsx( + 'p', + {}, + 'This is the home page using Vanilla Router with JSX rendering.', + ), + ) + } +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: IndexComponent, +}) + +// Posts layout route +const postsLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => fetchPosts(), + component: (router: ReturnType) => { + return () => { + const posts = postsLayoutRoute.getLoaderData(router) + + if (!posts) { + return jsx('div', { className: 'loading' }, 'Loading posts...') + } + + return jsx( + 'div', + {}, + jsx('h1', {}, 'Posts'), + jsx( + 'ul', + { className: 'post-list' }, + ...posts.map((post) => + jsx( + 'li', + { className: 'post-item' }, + jsx( + 'a', + { + href: buildHref(router, { + to: '/posts/$postId', + params: { postId: post.id }, + }), + }, + post.title, + ), + ), + ), + ), + outlet(), + ) + } + }, +}) + +// Posts index +const postsIndexRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '/', + component: (router: ReturnType) => { + return () => { + return jsx('div', {}, jsx('p', {}, 'Select a post to view details.')) + } + }, +}) + +// Post detail route +const postRoute = createRoute({ + getParentRoute: () => postsLayoutRoute, + path: '$postId', + loader: ({ params }: { params: { postId: string } }) => + fetchPost(params.postId), + component: (router: ReturnType) => { + return () => { + const post = postRoute.getLoaderData(router) + + if (!post) { + return jsx('div', { className: 'loading' }, 'Loading...') + } + + return jsx( + 'div', + { className: 'post-detail' }, + jsx('h1', { className: 'post-title' }, post.title), + jsx('div', { className: 'post-body' }, post.body), + ) + } + }, +}) + +// Create router +const routeTree = rootRoute.addChildren([ + postsLayoutRoute.addChildren([postRoute, postsIndexRoute]), + indexRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', +}) + +// Render function using VanillaRenderer +const renderer = new VanillaRenderer() +const rootElement = document.getElementById('app') +if (!rootElement) throw new Error('App element not found') + +function render() { + if (!rootElement) return + const state = router.state + + // Convert router matches to render contexts + const contexts = state.matches.map((match) => { + const route = router.routesById[match.routeId] + const matchState = router.getMatch(match.id) + + return { + component: route.options.component, + errorComponent: route.options.errorComponent, + pendingComponent: route.options.pendingComponent, + error: matchState?.error, + isPending: matchState?._displayPending, + data: { + loaderData: matchState?.loaderData, + params: matchState?.params, + search: matchState?.search, + routeId: match.routeId, + }, + } + }) + + // Render using JSX renderer with router + const html = renderer.render(contexts, router) + rootElement.innerHTML = html +} + +// Setup router +vanillaRouter(router, render).catch(console.error) diff --git a/examples/vanilla/jsx-router/src/renderer.ts b/examples/vanilla/jsx-router/src/renderer.ts new file mode 100644 index 00000000000..2a3b1b9f565 --- /dev/null +++ b/examples/vanilla/jsx-router/src/renderer.ts @@ -0,0 +1,319 @@ +// Copy of the renderer from packages/vanilla-router/examples/renderer/renderer.ts +// This allows the example to work independently + +import type { + VanillaRouteComponent, + VanillaErrorRouteComponent, +} from '@tanstack/vanilla-router' + +interface ComponentInstance { + cleanup?: () => void + getHtml: () => string +} + +export interface RenderContext { + component: VanillaRouteComponent | undefined + pendingComponent?: VanillaRouteComponent + errorComponent?: VanillaErrorRouteComponent + error?: Error + isPending?: boolean + data?: any +} + +export class VanillaRenderer { + private cleanupFunctions: Map void> = new Map() + private componentInstances: Map = new Map() + private currentContexts: Array = [] + private currentIndex: number = -1 + private childHtmlCache: Map = new Map() + + render(contexts: Array, router?: any): string { + this.cleanupFunctions.forEach((cleanup) => { + try { + cleanup() + } catch (error) { + console.error('Error during component cleanup:', error) + } + }) + this.cleanupFunctions.clear() + this.componentInstances.clear() + this.childHtmlCache.clear() + + if (contexts.length === 0) { + return '' + } + + this.currentContexts = contexts + + if (router) { + ;(globalThis as any).__tanstackRouter = router + } + + try { + return this.renderContexts(contexts, 0, router) + } catch (error) { + return this.renderError( + error as Error, + contexts[0]?.errorComponent, + contexts[0]?.data, + router, + ) + } finally { + if (router) { + delete (globalThis as any).__tanstackRouter + } + } + } + + private renderContexts( + contexts: Array, + index: number, + router?: any, + ): string { + if (index >= contexts.length) { + return '' + } + + const context = contexts[index] + if (!context) { + return '' + } + + const previousIndex = this.currentIndex + this.currentIndex = index + + try { + if (context.isPending && context.pendingComponent) { + const pendingInstance = this.createComponentInstance( + context.pendingComponent, + index, + router, + ) + return pendingInstance.getHtml() + } + + if (context.error && context.errorComponent) { + const errorFactory = context.errorComponent({ error: context.error }) + const errorInstance = this.createComponentInstance( + router ? errorFactory(router) : errorFactory, + index, + router, + ) + return errorInstance.getHtml() + } + + const Component = context.component + if (!Component) { + return this.renderContexts(contexts, index + 1, router) + } + + const instance = this.createComponentInstance(Component, index, router) + this.componentInstances.set(String(index), instance) + + if (instance.cleanup) { + this.cleanupFunctions.set(String(index), instance.cleanup) + } + + const childHtml = this.renderContexts(contexts, index + 1, router) + this.childHtmlCache.set(index, childHtml) + + const getContextData = () => this.currentContexts[index]?.data + const getChildHtmlFn = () => this.childHtmlCache.get(index) || '' + const renderContext = { data: getContextData, childHtml: getChildHtmlFn } + ;(globalThis as any).__vanillaRendererContext = renderContext + + try { + let html = instance.getHtml() + const OUTLET_MARKER = '__TANSTACK_ROUTER_OUTLET__' + if (html.includes(OUTLET_MARKER)) { + html = html.replace(OUTLET_MARKER, childHtml) + } + return html + } finally { + delete (globalThis as any).__vanillaRendererContext + } + } finally { + this.currentIndex = previousIndex + } + } + + private createComponentInstance( + component: VanillaRouteComponent, + index: number, + router?: any, + ): ComponentInstance { + const getContext = () => this.currentContexts[index]?.data + const getChildHtml = () => this.childHtmlCache.get(index) || '' + + const context = { + data: getContext, + childHtml: getChildHtml, + } + + ;(globalThis as any).__vanillaRendererContext = context + + const routerInstance = router || (globalThis as any).__tanstackRouter + const result = routerInstance ? component(routerInstance) : component() + + delete (globalThis as any).__vanillaRendererContext + + if (Array.isArray(result)) { + const [cleanup, getHtml] = result + return { cleanup, getHtml } + } else { + return { getHtml: result } + } + } + + private renderError( + error: Error, + errorComponent?: VanillaErrorRouteComponent, + data?: any, + router?: any, + ): string { + if (errorComponent) { + const context = { data: () => data, childHtml: () => '' } + ;(globalThis as any).__vanillaRendererContext = context + + const routerInstance = router || (globalThis as any).__tanstackRouter + const errorFactory = errorComponent({ error }) + const instance = this.createComponentInstance( + routerInstance ? errorFactory(routerInstance) : errorFactory, + 0, + routerInstance, + ) + delete (globalThis as any).__vanillaRendererContext + + return instance.getHtml() + } else { + return `
Error: ${error.message || String(error)}
` + } + } + + destroy() { + this.cleanupFunctions.forEach((cleanup) => { + try { + cleanup() + } catch (error) { + console.error('Error during component cleanup:', error) + } + }) + this.cleanupFunctions.clear() + this.componentInstances.clear() + this.childHtmlCache.clear() + this.currentContexts = [] + } +} + +function escapeHtml(text: string): string { + if (typeof document === 'undefined') { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } + const div = document.createElement('div') + div.textContent = String(text) + return div.innerHTML +} + +function renderChild(child: any): string { + if (child === null || child === undefined || child === false) return '' + if (typeof child === 'string' && child.trim().startsWith('<')) { + return child + } + if (typeof child === 'string' || typeof child === 'number') + return escapeHtml(String(child)) + if (Array.isArray(child)) return child.map(renderChild).join('') + if ( + typeof child === 'object' && + child.$$typeof === Symbol.for('react.element') + ) { + return jsx(child.type, child.props) + } + return String(child) +} + +export function jsx(type: any, props: any, ...children: any[]): string { + const normalizedChildren = children.filter( + (child) => child !== undefined && child !== null, + ) + + if (type === Symbol.for('react.fragment') || type === null) { + return normalizedChildren.map((child) => renderChild(child)).join('') + } + + if (typeof type === 'function') { + const componentResult = type(props || {}) + if (typeof componentResult === 'function') { + return componentResult( + normalizedChildren.map((child) => renderChild(child)).join(''), + ) + } + return renderChild(componentResult) + } + + const tagName = String(type).toLowerCase() + const attrs = props + ? Object.entries(props) + .filter(([key]) => key !== 'children') + .map(([key, value]) => { + if (key === 'className') { + return `class="${escapeHtml(String(value))}"` + } + const attrName = key.replace(/([A-Z])/g, '-$1').toLowerCase() + if (value === true) return attrName + if (value === false || value === null || value === undefined) + return '' + return `${attrName}="${escapeHtml(String(value))}"` + }) + .filter(Boolean) + .join(' ') + : '' + + const childrenHtml = + normalizedChildren.length > 0 + ? normalizedChildren + .map((child) => { + if (child && typeof child === 'object' && child.__html) { + return child.__html + } + return renderChild(child) + }) + .join('') + : '' + + const selfClosingTags = [ + 'input', + 'img', + 'br', + 'hr', + 'meta', + 'link', + 'area', + 'base', + 'col', + 'embed', + 'source', + 'track', + 'wbr', + ] + if (selfClosingTags.includes(tagName)) { + return `<${tagName}${attrs ? ' ' + attrs : ''} />` + } + + return `<${tagName}${attrs ? ' ' + attrs : ''}>${childrenHtml}` +} + +export function jsxs(type: any, props: any, ...children: any[]): string { + return jsx(type, props, ...children) +} + +export function Fragment({ children = [] }: { children?: any[] }): string { + return (Array.isArray(children) ? children : [children]) + .filter((child) => child !== undefined && child !== null) + .map((child) => renderChild(child)) + .join('') +} diff --git a/examples/vanilla/jsx-router/tsconfig.json b/examples/vanilla/jsx-router/tsconfig.json new file mode 100644 index 00000000000..de6b7eedefd --- /dev/null +++ b/examples/vanilla/jsx-router/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "jsxFactory": "jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/vanilla/jsx-router/vite.config.ts b/examples/vanilla/jsx-router/vite.config.ts new file mode 100644 index 00000000000..9306ffab9eb --- /dev/null +++ b/examples/vanilla/jsx-router/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve( + __dirname, + '../../../packages/vanilla-router/src', + ), + }, + }, +}) diff --git a/examples/vanilla/scroll-restoration/index.html b/examples/vanilla/scroll-restoration/index.html new file mode 100644 index 00000000000..cbafc9fda24 --- /dev/null +++ b/examples/vanilla/scroll-restoration/index.html @@ -0,0 +1,84 @@ + + + + + + TanStack Router - Vanilla Scroll Restoration + + + +
+ + + diff --git a/examples/vanilla/scroll-restoration/package.json b/examples/vanilla/scroll-restoration/package.json new file mode 100644 index 00000000000..922203540de --- /dev/null +++ b/examples/vanilla/scroll-restoration/package.json @@ -0,0 +1,18 @@ +{ + "name": "vanilla-scroll-restoration-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3004", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/vanilla-router": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^7.1.7" + } +} diff --git a/examples/vanilla/scroll-restoration/src/main.ts b/examples/vanilla/scroll-restoration/src/main.ts new file mode 100644 index 00000000000..043b2b28a4c --- /dev/null +++ b/examples/vanilla/scroll-restoration/src/main.ts @@ -0,0 +1,145 @@ +import { + createRouter, + createRootRoute, + createRoute, + buildHref, + outlet, + getMatchesHtml, + vanillaRouter, +} from '@tanstack/vanilla-router' + +// Root component +const RootComponent = (router: ReturnType) => { + const currentPath = router.state.location.pathname + return ` + +
+ ${outlet()} +
+ ` +} + +const rootRoute = createRootRoute({ + component: RootComponent, +}) + +// Index component with scrollable content +const IndexComponent = (router: ReturnType) => { + return ` +
+ Scroll Restoration Demo: Scroll down on this page, then navigate to another page and come back. + Your scroll position should be restored automatically! +
+

Welcome Home!

+
+ ${Array.from( + { length: 50 }, + (_, i) => ` +
Home Item ${i + 1}
+ `, + ).join('')} +
+ ` +} + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: IndexComponent, +}) + +// About component with scrollable content +const AboutComponent = (router: ReturnType) => { + return ` +
+ Scroll Restoration Demo: Scroll down on this page, navigate away, and come back. + Your scroll position should be restored! +
+

About Page

+
+ ${Array.from( + { length: 50 }, + (_, i) => ` +
About Item ${i + 1}
+ `, + ).join('')} +
+ ` +} + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: AboutComponent, +}) + +// Scrollable container component (demonstrates element-level scroll restoration) +const ScrollableComponent = (router: ReturnType) => { + return ` +
+ Element-Level Scroll Restoration: Scroll within the container below, navigate away, and come back. + The scroll position within the container should be restored! +
+

Scrollable Container Demo

+
+ ${Array.from( + { length: 50 }, + (_, i) => ` +
Scrollable Item ${i + 1}
+ `, + ).join('')} +
+ ` +} + +const scrollableRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/scrollable', + loader: () => new Promise((r) => setTimeout(r, 500)), + component: ScrollableComponent, +}) + +// Create router with scroll restoration enabled +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + scrollableRoute, +]) + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + scrollRestoration: true, // Enable scroll restoration +}) + +// Render function +const rootElement = document.getElementById('app') +if (!rootElement) throw new Error('App element not found') + +function render() { + if (!rootElement) return + const htmlParts = getMatchesHtml(router, router.state.matches) + rootElement.innerHTML = htmlParts.join('') + + // Update active links + const currentPath = router.state.location.pathname + const links = rootElement.querySelectorAll('nav a') + links.forEach((link) => { + const href = link.getAttribute('href') + if (href === currentPath) { + link.classList.add('active') + } else { + link.classList.remove('active') + } + }) +} + +// Setup router +vanillaRouter(router, render).catch(console.error) diff --git a/examples/vanilla/scroll-restoration/tsconfig.json b/examples/vanilla/scroll-restoration/tsconfig.json new file mode 100644 index 00000000000..1b0362ce9bf --- /dev/null +++ b/examples/vanilla/scroll-restoration/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/vanilla/scroll-restoration/vite.config.ts b/examples/vanilla/scroll-restoration/vite.config.ts new file mode 100644 index 00000000000..9306ffab9eb --- /dev/null +++ b/examples/vanilla/scroll-restoration/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + resolve: { + alias: { + '@tanstack/vanilla-router': path.resolve( + __dirname, + '../../../packages/vanilla-router/src', + ), + }, + }, +}) diff --git a/labeler-config.yml b/labeler-config.yml index bf86504942f..76ad32ce7f8 100644 --- a/labeler-config.yml +++ b/labeler-config.yml @@ -13,6 +13,9 @@ 'package: nitro-v2-vite-plugin': - changed-files: - any-glob-to-any-file: 'packages/nitro-v2-vite-plugin/**/*' +'package: preact-router': + - changed-files: + - any-glob-to-any-file: 'packages/preact-router/**/*' 'package: react-router': - changed-files: - any-glob-to-any-file: 'packages/react-router/**/*' @@ -97,6 +100,9 @@ 'package: valibot-adapter': - changed-files: - any-glob-to-any-file: 'packages/valibot-adapter/**/*' +'package: vanilla-router': + - changed-files: + - any-glob-to-any-file: 'packages/vanilla-router/**/*' 'package: virtual-file-routes': - changed-files: - any-glob-to-any-file: 'packages/virtual-file-routes/**/*' diff --git a/packages/preact-router/README.md b/packages/preact-router/README.md new file mode 100644 index 00000000000..d83bf5fdf43 --- /dev/null +++ b/packages/preact-router/README.md @@ -0,0 +1,31 @@ + + +# TanStack React Router + +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) + +🤖 Type-safe router w/ built-in caching & URL state management for React! + + + #TanStack + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + + + +Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual) + +## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more! diff --git a/packages/preact-router/build-no-check.js b/packages/preact-router/build-no-check.js new file mode 100644 index 00000000000..08e3c75359f --- /dev/null +++ b/packages/preact-router/build-no-check.js @@ -0,0 +1,9 @@ +import { build } from 'vite' +build({ + mode: 'production', + build: { emptyOutDir: true }, + logLevel: 'warn', +}).catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/preact-router/eslint.config.ts b/packages/preact-router/eslint.config.ts new file mode 100644 index 00000000000..a2f17156678 --- /dev/null +++ b/packages/preact-router/eslint.config.ts @@ -0,0 +1,24 @@ +import pluginReact from '@eslint-react/eslint-plugin' +// @ts-expect-error +import pluginReactHooks from 'eslint-plugin-react-hooks' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], + }, + { + plugins: { + 'react-hooks': pluginReactHooks, + '@eslint-react': pluginReact, + }, + rules: { + '@eslint-react/no-unstable-context-value': 'off', + '@eslint-react/no-unstable-default-props': 'off', + '@eslint-react/dom/no-missing-button-type': 'off', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/rules-of-hooks': 'error', + }, + }, +] diff --git a/packages/preact-router/package.json b/packages/preact-router/package.json new file mode 100644 index 00000000000..f7399658098 --- /dev/null +++ b/packages/preact-router/package.json @@ -0,0 +1,117 @@ +{ + "name": "@tanstack/preact-router", + "version": "1.133.13", + "description": "Modern and scalable routing for Preact applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/preact-router" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "preact", + "location", + "router", + "routing", + "async", + "async router", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js -p tsconfig.legacy.json", + "test:types:ts59": "tsc -p tsconfig.legacy.json", + "test:unit": "vitest", + "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests", + "test:perf": "vitest bench", + "test:perf:dev": "pnpm run test:perf --watch --hideSkippedTests", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "pnpm run build:lib", + "build:lib": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./ssr/server": { + "import": { + "types": "./dist/esm/ssr/server.d.ts", + "default": "./dist/esm/ssr/server.js" + }, + "require": { + "types": "./dist/cjs/ssr/server.d.cts", + "default": "./dist/cjs/ssr/server.cjs" + } + }, + "./ssr/client": { + "import": { + "types": "./dist/esm/ssr/client.d.ts", + "default": "./dist/esm/ssr/client.js" + }, + "require": { + "types": "./dist/cjs/ssr/client.d.cts", + "default": "./dist/cjs/ssr/client.cjs" + } + }, + "./package.json": "./package.json", + "./llms": { + "import": { + "types": "./dist/llms/index.d.ts", + "default": "./dist/llms/index.js" + } + } + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "@tanstack/history": "workspace:*", + "@tanstack/react-store": "^0.7.0", + "@tanstack/router-core": "workspace:*", + "@tanstack/store": "^0.7.0", + "isbot": "^5.1.22", + "preact-render-to-string": "^6.3.1", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "devDependencies": { + "@preact/preset-vite": "^2.9.3", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/preact": "^3.2.4", + "combinate": "^1.1.11", + "preact": "^10.24.3", + "vibe-rules": "^0.2.57", + "zod": "^3.24.2" + }, + "peerDependencies": { + "preact": ">=10.0.0" + } +} diff --git a/packages/preact-router/src/Asset.tsx b/packages/preact-router/src/Asset.tsx new file mode 100644 index 00000000000..e4356c92f7a --- /dev/null +++ b/packages/preact-router/src/Asset.tsx @@ -0,0 +1,155 @@ +import type { ComponentChildren } from 'preact' +import { useEffect } from 'preact/hooks' +import { useRouter } from './useRouter' +import type { RouterManagedTag } from '@tanstack/router-core' + +interface ScriptAttrs { + [key: string]: string | boolean | undefined + src?: string +} + +export function Asset({ + tag, + attrs, + children, + nonce, +}: RouterManagedTag & { nonce?: string }): any { + switch (tag) { + case 'title': + return {children} + case 'meta': + return + case 'link': + return + case 'style': + return ( + + + +
+ + + + diff --git a/packages/vanilla-router/examples/vanilla-dom-example.ts b/packages/vanilla-router/examples/vanilla-dom-example.ts new file mode 100644 index 00000000000..bf11c489df5 --- /dev/null +++ b/packages/vanilla-router/examples/vanilla-dom-example.ts @@ -0,0 +1,140 @@ +/** + * Example: Using headless router with direct DOM manipulation + */ + +import { createRouter, createRoute } from '@tanstack/vanilla-router' +import { + buildHref, + outlet, + getMatchesHtml, + setupLinkHandlers, + setRouter, + getRouter, +} from '@tanstack/vanilla-router' + +// Define routes - components are simple functions that return HTML strings +// Note: Routes can reference each other, but for simple paths you can use strings +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => { + return '

Home Page

Welcome to the home page!

' + }, +}) + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + component: () => { + return '

About

This is the about page.

' + }, +}) + +const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '/posts/$postId', + component: () => { + // Use route getter to access params + const params = postRoute.getParams() + return `

Post ${params.postId}

This is post ${params.postId}

` + }, +}) + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/posts', + component: () => { + const posts = [ + { id: 1, title: 'Post 1' }, + { id: 2, title: 'Post 2' }, + ] + // Use buildHref utility for type-safe hrefs with params (like navigate() and ) + const router = getRouter() + return ` +

Posts

+ + ` + }, +}) + +const rootRoute = createRoute({ + getParentRoute: () => rootRoute, + id: 'root', + component: () => { + // Use outlet() function to mark where child routes should render + const router = getRouter() + return ` +
+ +
${outlet()}
+
+ ` + }, +}) + +// Create router +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + postsRoute.addChildren([postRoute]), +]) + +const router = createRouter({ + routeTree, +}) + +// Initialize router +const rootElement = document.getElementById('app')! +setRouter(router) // Set global router context for route getters + +// Render function - direct DOM manipulation +function renderToDOM(router: typeof router, rootElement: HTMLElement) { + rootElement.innerHTML = '' + + const state = router.state + + // Check for not found + if (state.matches.some((m) => m.status === 'notFound' || m.globalNotFound)) { + rootElement.innerHTML = '
404 - Not Found
' + return + } + + // Use getMatchesHtml utility to get nested HTML strings (outlet replacement is handled internally) + const htmlParts = getMatchesHtml(router, state.matches) + rootElement.innerHTML = htmlParts.join('') +} + +// Subscribe to router events and render on changes +async function init() { + // Load initial matches if needed + if (router.state.matches.length === 0) { + try { + await router.load() + } catch (error) { + console.error('Error loading router:', error) + } + } + + // Initial render + renderToDOM(router, rootElement) + + // Subscribe to router state changes + router.subscribe('onResolved', () => { + renderToDOM(router, rootElement) + }) + + router.subscribe('onLoad', () => { + renderToDOM(router, rootElement) + }) +} + +init() + +// Setup link handlers (returns cleanup function) +const cleanupLinkHandlers = setupLinkHandlers(router, rootElement) diff --git a/packages/vanilla-router/examples/vanilla-jsx-example.ts b/packages/vanilla-router/examples/vanilla-jsx-example.ts new file mode 100644 index 00000000000..5938ca5cb0f --- /dev/null +++ b/packages/vanilla-router/examples/vanilla-jsx-example.ts @@ -0,0 +1,161 @@ +/** + * Example: Using headless router with component renderer (JSX) + * + * NOTE: This example is outdated - HeadlessRouter was refactored into utilities. + * The renderer is now located in examples/renderer/ for example use only. + */ + +import { createRouter, createRoute } from '../../src' +import { VanillaRenderer, type RenderContext } from './renderer/renderer' +import { jsx } from './renderer/jsx-runtime' + +// Define routes with JSX components +const rootRoute = createRoute({ + getParentRoute: () => rootRoute, + id: 'root', + component: () => { + return () => { + return jsx( + 'div', + {}, + jsx( + 'nav', + {}, + jsx('a', { href: '/' }, 'Home'), + jsx('a', { href: '/about' }, 'About'), + jsx('a', { href: '/posts' }, 'Posts'), + ), + jsx('main', { id: 'outlet' }), + ) + } + }, +}) + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => { + return () => { + return jsx( + 'div', + {}, + jsx('h1', {}, 'Home Page'), + jsx('p', {}, 'Welcome to the home page!'), + ) + } + }, +}) + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + component: () => { + return () => { + return jsx( + 'div', + {}, + jsx('h1', {}, 'About'), + jsx('p', {}, 'This is the about page.'), + ) + } + }, +}) + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/posts', + component: () => { + return () => { + const posts = [ + { id: 1, title: 'Post 1' }, + { id: 2, title: 'Post 2' }, + ] + return jsx( + 'div', + {}, + jsx('h1', {}, 'Posts'), + jsx( + 'ul', + {}, + ...posts.map((post) => + jsx('li', {}, jsx('a', { href: `/posts/${post.id}` }, post.title)), + ), + ), + ) + } + }, +}) + +const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '/posts/$postId', + component: () => { + return () => { + // Use route getter to access params + const params = postRoute.getParams() + return jsx( + 'div', + {}, + jsx('h1', {}, `Post ${params.postId}`), + jsx('p', {}, `This is post ${params.postId}`), + ) + } + }, +}) + +// Create router +const routeTree = rootRoute.addChildren([ + indexRoute, + aboutRoute, + postsRoute.addChildren([postRoute]), +]) + +const router = createRouter({ + routeTree, +}) + +// Render function - using component renderer +const renderer = new VanillaRenderer() + +function renderWithComponentRenderer( + state: RouterRenderState, + rootElement: HTMLElement, +) { + if (state.isNotFound) { + rootElement.innerHTML = '
404 - Not Found
' + return + } + + if (state.globalError) { + rootElement.innerHTML = `
Error: ${state.globalError.message}
` + return + } + + // Convert router state to render contexts + const contexts: RenderContext[] = state.matches.map((match) => ({ + component: match.component, + pendingComponent: match.pendingComponent, + errorComponent: match.errorComponent, + error: match.error, + isPending: match.isPending, + data: { + loaderData: match.loaderData, + params: match.params, + search: match.search, + routeId: match.routeId, + }, + })) + + // Use component renderer + const html = renderer.render(contexts) + rootElement.innerHTML = html +} + +// Initialize headless router +const rootElement = document.getElementById('app')! +const headlessRouter = new HeadlessRouter(router, (state) => { + renderWithComponentRenderer(state, rootElement) +}) + +// Setup link handlers +headlessRouter.setupLinkHandlers(rootElement) diff --git a/packages/vanilla-router/package.json b/packages/vanilla-router/package.json new file mode 100644 index 00000000000..ef0214aa02e --- /dev/null +++ b/packages/vanilla-router/package.json @@ -0,0 +1,76 @@ +{ + "name": "@tanstack/vanilla-router", + "version": "1.133.13", + "description": "Modern and scalable routing for vanilla JavaScript applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/router.git", + "directory": "packages/vanilla-router" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "vanilla", + "javascript", + "location", + "router", + "routing", + "async", + "async router", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint", + "test:types": "tsc -p tsconfig.json --noEmit", + "test:unit": "vitest", + "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "pnpm run build:lib", + "build:lib": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "@tanstack/history": "workspace:*", + "@tanstack/router-core": "workspace:*", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "devDependencies": { + "combinate": "^1.1.11", + "typescript": "^5.7.2", + "vibe-rules": "^0.2.57", + "vite-plugin-dts": "^4.3.0", + "vitest": "^2.1.8", + "zod": "^3.24.2" + } +} diff --git a/packages/vanilla-router/src/error-handling.ts b/packages/vanilla-router/src/error-handling.ts new file mode 100644 index 00000000000..d883f58c283 --- /dev/null +++ b/packages/vanilla-router/src/error-handling.ts @@ -0,0 +1,80 @@ +import type { AnyRouter, AnyRouteMatch } from '@tanstack/router-core' +import { isNotFound, isRedirect } from '@tanstack/router-core' +import type { + VanillaErrorRouteComponent, + VanillaNotFoundRouteComponent, +} from './types' + +/** + * Check if an error is a NotFoundError + * + * @param error - The error to check + * @returns True if the error is a NotFoundError + */ +export function checkIsNotFound( + error: unknown, +): error is import('@tanstack/router-core').NotFoundError { + return isNotFound(error) +} + +/** + * Check if an error is a Redirect + * + * @param error - The error to check + * @returns True if the error is a Redirect + */ +export function checkIsRedirect( + error: unknown, +): error is import('@tanstack/router-core').Redirect { + return isRedirect(error) +} + +/** + * Get the error component for a match + * This checks the route's errorComponent option and falls back to router's defaultErrorComponent + * + * @param router - The router instance + * @param match - The match to get error component for + * @returns The error component factory function, or undefined if none configured + */ +export function getErrorComponent( + router: TRouter, + match: AnyRouteMatch, +): VanillaErrorRouteComponent | undefined { + const route = router.routesById[match.routeId] + if (!route) return undefined + + const errorComponent = + route.options.errorComponent === false + ? undefined + : (route.options.errorComponent ?? router.options.defaultErrorComponent) + + return errorComponent +} + +/** + * Get the not found component for a match + * This checks the route's notFoundComponent option and falls back to router's defaultNotFoundComponent + * + * @param router - The router instance + * @param match - The match to get not found component for + * @returns The not found component factory function, or undefined if none configured + */ +export function getNotFoundComponent( + router: TRouter, + match: AnyRouteMatch, +): VanillaNotFoundRouteComponent | undefined { + const route = router.routesById[match.routeId] + if (!route) return undefined + + const notFoundComponent = + route.options.notFoundComponent === false + ? undefined + : (route.options.notFoundComponent ?? + router.options.defaultNotFoundComponent) + + return notFoundComponent +} + +// Re-export isNotFound and isRedirect for convenience +export { isNotFound, isRedirect } from '@tanstack/router-core' diff --git a/packages/vanilla-router/src/fileRoute.ts b/packages/vanilla-router/src/fileRoute.ts new file mode 100644 index 00000000000..814ba85ab37 --- /dev/null +++ b/packages/vanilla-router/src/fileRoute.ts @@ -0,0 +1,276 @@ +import warning from 'tiny-warning' +import { createRoute } from './route' +import type { + AnyContext, + AnyRoute, + AnyRouter, + Constrain, + ConstrainLiteral, + FileBaseRouteOptions, + FileRoutesByPath, + LazyRouteOptions, + Register, + RegisteredRouter, + ResolveParams, + Route, + RouteById, + RouteConstraints, + RouteIds, + RouteLoaderFn, + UpdatableRouteOptions, +} from '@tanstack/router-core' +import type { VanillaRouteComponent } from './types' + +/** + * Create a file route for vanilla JS + * This is adapted from React/Preact fileRoute but without hooks + * Instead, route getters are used (e.g., route.getLoaderData(router)) + * + * @param path - The file path (e.g., '/posts/$postId') + * @returns A function that creates a route with the given options + */ +export function createFileRoute< + TFilePath extends keyof FileRoutesByPath, + TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'], + TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'], + TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'], + TFullPath extends + RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'], +>( + path?: TFilePath, +): FileRoute['createRoute'] { + if (typeof path === 'object') { + return new FileRoute(path, { + silent: true, + }).createRoute(path) as any + } + return new FileRoute(path, { + silent: true, + }).createRoute +} + +/** + * FileRoute class for vanilla JS + * Provides route creation without hooks - use route getters instead + * + * @deprecated It's no longer recommended to use the `FileRoute` class directly. + * Instead, use `createFileRoute('/path/to/file')(options)` to create a file route. + */ +export class FileRoute< + TFilePath extends keyof FileRoutesByPath, + TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]['parentRoute'], + TId extends RouteConstraints['TId'] = FileRoutesByPath[TFilePath]['id'], + TPath extends RouteConstraints['TPath'] = FileRoutesByPath[TFilePath]['path'], + TFullPath extends + RouteConstraints['TFullPath'] = FileRoutesByPath[TFilePath]['fullPath'], +> { + silent?: boolean + + constructor( + public path?: TFilePath, + _opts?: { silent: boolean }, + ) { + this.silent = _opts?.silent + } + + createRoute = < + TRegister = Register, + TSearchValidator = undefined, + TParams = ResolveParams, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, + TLoaderDeps extends Record = {}, + TLoaderFn = undefined, + TChildren = unknown, + TSSR = unknown, + const TMiddlewares = unknown, + THandlers = undefined, + >( + options?: FileBaseRouteOptions< + TRegister, + TParentRoute, + TId, + TPath, + TSearchValidator, + TParams, + TLoaderDeps, + TLoaderFn, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + AnyContext, + TSSR, + TMiddlewares, + THandlers + > & + UpdatableRouteOptions< + TParentRoute, + TId, + TFullPath, + TParams, + TSearchValidator, + TLoaderFn, + TLoaderDeps, + AnyContext, + TRouteContextFn, + TBeforeLoadFn + >, + ): ReturnType => { + warning( + this.silent, + 'FileRoute is deprecated and will be removed in the next major version. Use the createFileRoute(path)(options) function instead.', + ) + const route = createRoute(options as any) + ;(route as any).isRoot = false + return route as any + } +} + +/** + * FileRouteLoader for vanilla JS + * Note: In vanilla JS, loaders should be defined directly in route options + * This is provided for compatibility but is deprecated + * + * @deprecated It's recommended not to split loaders into separate files. + * Instead, place the loader function in the main route file, inside the + * `createFileRoute('/path/to/file')(options)` options. + */ +export function FileRouteLoader< + TFilePath extends keyof FileRoutesByPath, + TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'], +>( + _path: TFilePath, +): ( + loaderFn: Constrain< + TLoaderFn, + RouteLoaderFn< + Register, + TRoute['parentRoute'], + TRoute['types']['id'], + TRoute['types']['params'], + TRoute['types']['loaderDeps'], + TRoute['types']['routerContext'], + TRoute['types']['routeContextFn'], + TRoute['types']['beforeLoadFn'] + > + >, +) => TLoaderFn { + warning( + false, + `FileRouteLoader is deprecated and will be removed in the next major version. Please place the loader function in the main route file, inside the \`createFileRoute('/path/to/file')(options)\` options`, + ) + return (loaderFn) => loaderFn as any +} + +/** + * Create a lazy route for vanilla JS + * Note: Lazy routes in vanilla JS don't use hooks - use route getters instead + */ +export function createLazyRoute< + TRouter extends AnyRouter = RegisteredRouter, + TId extends string = string, + TRoute extends AnyRoute = RouteById, +>(id: ConstrainLiteral>) { + return (opts: LazyRouteOptions) => { + return new LazyRoute({ + id: id, + ...opts, + }) + } +} + +/** + * Create a lazy file route for vanilla JS + */ +export function createLazyFileRoute< + TFilePath extends keyof FileRoutesByPath, + TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'], +>(id: TFilePath): (opts: LazyRouteOptions) => LazyRoute { + if (typeof id === 'object') { + return new LazyRoute(id) as any + } + + return (opts: LazyRouteOptions) => new LazyRoute({ id, ...opts }) +} + +/** + * LazyRoute class for vanilla JS + * Note: In vanilla JS, route getters should be used instead of hooks + */ +export class LazyRoute { + options: { + id: string + } & LazyRouteOptions + + constructor( + opts: { + id: string + } & LazyRouteOptions, + ) { + this.options = opts + } + + /** + * Get match data for this lazy route + * Use route.getMatch(router) instead of this method + */ + getMatch(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find( + (m) => m.routeId === this.options.id, + ) + if (!match) return undefined + return router.getMatch(match.id) + } + + /** + * Get loader data for this lazy route + * Use route.getLoaderData(router) instead of this method + */ + getLoaderData(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find( + (m) => m.routeId === this.options.id, + ) + if (!match) return undefined + const matchState = router.getMatch(match.id) + return matchState?.loaderData + } + + /** + * Get params for this lazy route + * Use route.getParams(router) instead of this method + */ + getParams(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find( + (m) => m.routeId === this.options.id, + ) + if (!match) return {} + const matchState = router.getMatch(match.id) + return matchState?._strictParams ?? matchState?.params ?? {} + } + + /** + * Get search params for this lazy route + * Use route.getSearch(router) instead of this method + */ + getSearch(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find( + (m) => m.routeId === this.options.id, + ) + if (!match) return {} + const matchState = router.getMatch(match.id) + return matchState?._strictSearch ?? matchState?.search ?? {} + } + + /** + * Get route context for this lazy route + * Use route.getRouteContext(router) instead of this method + */ + getRouteContext(router: import('@tanstack/router-core').AnyRouter) { + const match = router.state.matches.find( + (m) => m.routeId === this.options.id, + ) + if (!match) return {} + const matchState = router.getMatch(match.id) + return matchState?.context ?? {} + } +} diff --git a/packages/vanilla-router/src/index.ts b/packages/vanilla-router/src/index.ts new file mode 100644 index 00000000000..e6d41574ce0 --- /dev/null +++ b/packages/vanilla-router/src/index.ts @@ -0,0 +1,84 @@ +export { createRouter } from './router' +export type { Router } from './router' + +export { createRoute, createRootRoute, Route, RootRoute } from './route' +export type { + VanillaRouteComponent, + VanillaErrorRouteComponent, + VanillaNotFoundRouteComponent, +} from './types' + +export { + buildHref, + getMatchesHtml, + outlet, + setupLinkHandlers, + vanillaRouter, +} from './vanilla-router' + +// Router state utilities +export { + subscribeRouterState, + getRouterState, + getLocation, + getMatches, +} from './utils' + +// Navigation utilities +export { navigate, canGoBack, goBack, goForward, go } from './navigation' + +// Route data utilities +export { + getMatchData, + getParams, + getSearch, + getLoaderData, + getRouteContext, + getLoaderDeps, +} from './route-data' + +// Error handling utilities +export { + checkIsNotFound, + checkIsRedirect, + getErrorComponent, + getNotFoundComponent, + isNotFound, + isRedirect, +} from './error-handling' + +// Scroll restoration utilities +export { + setupScrollRestorationUtil as setupScrollRestoration, + getScrollPosition, + saveScrollPosition, +} from './scroll-restoration' + +// File route utilities +export { + createFileRoute, + FileRoute, + FileRouteLoader, + createLazyRoute, + createLazyFileRoute, + LazyRoute, +} from './fileRoute' + +// Re-export core types and utilities +export type { + AnyRouter, + AnyRoute, + RouterState, + NavigateOptions, + ParsedLocation, +} from '@tanstack/router-core' + +export { redirect, notFound } from '@tanstack/router-core' + +export { + createBrowserHistory, + createHashHistory, + createMemoryHistory, +} from '@tanstack/history' + +export type { RouterHistory, HistoryLocation } from '@tanstack/history' diff --git a/packages/vanilla-router/src/navigation.ts b/packages/vanilla-router/src/navigation.ts new file mode 100644 index 00000000000..32693792443 --- /dev/null +++ b/packages/vanilla-router/src/navigation.ts @@ -0,0 +1,70 @@ +import type { AnyRouter, NavigateOptions } from '@tanstack/router-core' + +/** + * Navigate programmatically + * This is a vanilla JS equivalent of useNavigate hook + * + * @param router - The router instance + * @param options - Navigation options (to, params, search, hash, replace, etc.) + * @returns Promise that resolves when navigation completes + */ +export function navigate( + router: TRouter, + options: NavigateOptions, +): Promise { + return router.navigate(options) +} + +/** + * Check if the router can go back in history + * This is a vanilla JS equivalent of useCanGoBack hook + * + * @param router - The router instance + * @returns True if can go back, false otherwise + */ +export function canGoBack(router: TRouter): boolean { + return router.history.canGoBack() +} + +/** + * Go back in history + * Equivalent to browser back button + * + * @param router - The router instance + * @param options - Optional navigation options (e.g., ignoreBlocker) + */ +export function goBack( + router: TRouter, + options?: { ignoreBlocker?: boolean }, +): void { + router.history.back(options) +} + +/** + * Go forward in history + * Equivalent to browser forward button + * + * @param router - The router instance + * @param options - Optional navigation options (e.g., ignoreBlocker) + */ +export function goForward( + router: TRouter, + options?: { ignoreBlocker?: boolean }, +): void { + router.history.forward(options) +} + +/** + * Go to a specific index in history + * + * @param router - The router instance + * @param index - The index to navigate to (negative goes back, positive goes forward) + * @param options - Optional navigation options (e.g., ignoreBlocker) + */ +export function go( + router: TRouter, + index: number, + options?: { ignoreBlocker?: boolean }, +): void { + router.history.go(index, options) +} diff --git a/packages/vanilla-router/src/route-data.ts b/packages/vanilla-router/src/route-data.ts new file mode 100644 index 00000000000..4405f6c7049 --- /dev/null +++ b/packages/vanilla-router/src/route-data.ts @@ -0,0 +1,115 @@ +import type { AnyRouter } from '@tanstack/router-core' + +/** + * Get match data for a specific route by routeId + * This is a vanilla JS equivalent of useMatch hook + * + * @param router - The router instance + * @param routeId - The route ID to get match data for + * @returns Match data for the route, or undefined if not found + */ +export function getMatchData( + router: TRouter, + routeId: string, +): TRouter['state']['matches'][0] | undefined { + return router.state.matches.find((match) => match.routeId === routeId) +} + +/** + * Get params for a specific route by routeId + * This is a vanilla JS equivalent of useParams hook + * + * @param router - The router instance + * @param routeId - The route ID to get params for + * @param strict - Whether to use strict params (default: true) + * @returns Params for the route, or undefined if route not found + */ +export function getParams( + router: TRouter, + routeId: string, + strict: boolean = true, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + if (!matchState) return undefined + + return strict ? matchState._strictParams : matchState.params +} + +/** + * Get search params for a specific route by routeId + * This is a vanilla JS equivalent of useSearch hook + * + * @param router - The router instance + * @param routeId - The route ID to get search params for + * @returns Search params for the route, or undefined if route not found + */ +export function getSearch( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.search +} + +/** + * Get loader data for a specific route by routeId + * This is a vanilla JS equivalent of useLoaderData hook + * + * @param router - The router instance + * @param routeId - The route ID to get loader data for + * @returns Loader data for the route, or undefined if route not found or loader hasn't run + */ +export function getLoaderData( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.loaderData +} + +/** + * Get route context for a specific route by routeId + * This is a vanilla JS equivalent of useRouteContext hook + * + * @param router - The router instance + * @param routeId - The route ID to get context for + * @returns Route context for the route, or undefined if route not found + */ +export function getRouteContext( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.context +} + +/** + * Get loader dependencies for a specific route by routeId + * This is a vanilla JS equivalent of useLoaderDeps hook + * + * @param router - The router instance + * @param routeId - The route ID to get loader deps for + * @returns Loader dependencies for the route, or undefined if route not found + */ +export function getLoaderDeps( + router: TRouter, + routeId: string, +): any { + const match = getMatchData(router, routeId) + if (!match) return undefined + + const matchState = router.getMatch(match.id) + return matchState?.loaderDeps +} diff --git a/packages/vanilla-router/src/route.ts b/packages/vanilla-router/src/route.ts new file mode 100644 index 00000000000..e95d0e11231 --- /dev/null +++ b/packages/vanilla-router/src/route.ts @@ -0,0 +1,435 @@ +import { BaseRootRoute, BaseRoute, notFound } from '@tanstack/router-core' +import type { + AnyContext, + AnyRoute, + AnyRouter, + NotFoundError, + Register, + ResolveFullPath, + ResolveId, + ResolveParams, + RootRouteOptions, + RouteConstraints, + RouteOptions, +} from '@tanstack/router-core' + +// Router context for route getters - deprecated, router should be passed as parameter +declare global { + var __tanstackRouter: import('@tanstack/router-core').AnyRouter | undefined +} + +declare module '@tanstack/router-core' { + export interface UpdatableRouteOptionsExtensions { + component?: VanillaRouteComponent + errorComponent?: false | null | undefined | VanillaErrorRouteComponent + notFoundComponent?: VanillaNotFoundRouteComponent + pendingComponent?: VanillaRouteComponent + } + + export interface RootRouteOptionsExtensions { + shellComponent?: VanillaRouteComponent + } +} + +export class Route< + in out TRegister = unknown, + in out TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute, + in out TPath extends RouteConstraints['TPath'] = '/', + in out TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath< + TParentRoute, + TPath + >, + in out TCustomId extends RouteConstraints['TCustomId'] = string, + in out TId extends RouteConstraints['TId'] = ResolveId< + TParentRoute, + TCustomId, + TPath + >, + in out TSearchValidator = undefined, + in out TParams = ResolveParams, + in out TRouterContext = AnyContext, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, + in out TLoaderDeps extends Record = {}, + in out TLoaderFn = undefined, + in out TChildren = unknown, + in out TFileRouteTypes = unknown, + in out TSSR = unknown, + in out TServerMiddlewares = unknown, + in out THandlers = undefined, +> extends BaseRoute< + TRegister, + TParentRoute, + TPath, + TFullPath, + TCustomId, + TId, + TSearchValidator, + TParams, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TFileRouteTypes, + TSSR, + TServerMiddlewares, + THandlers +> { + constructor( + options?: RouteOptions< + TRegister, + TParentRoute, + TId, + TCustomId, + TFullPath, + TPath, + TSearchValidator, + TParams, + TLoaderDeps, + TLoaderFn, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TSSR, + TServerMiddlewares, + THandlers + >, + ) { + super(options) + } + + notFound = (opts?: NotFoundError) => { + return notFound({ routeId: this.id as string, ...opts }) + } + + /** + * Get the loader data for this route + * @param router - The router instance (required) + */ + getLoaderData = ( + router: import('@tanstack/router-core').AnyRouter, + ): TLoaderFn extends undefined ? undefined : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined as any + const matchState = router.getMatch(match.id) + return matchState?.loaderData as any + } + + /** + * Get the params for this route + * @param router - The router instance (required) + */ + getParams = (router: import('@tanstack/router-core').AnyRouter): TParams => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as TParams + const matchState = router.getMatch(match.id) + return (matchState?._strictParams ?? matchState?.params ?? {}) as TParams + } + + /** + * Get the search params for this route + * @param router - The router instance (required) + */ + getSearch = ( + router: import('@tanstack/router-core').AnyRouter, + ): TSearchValidator extends undefined ? Record : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return (matchState?._strictSearch ?? matchState?.search ?? {}) as any + } + + /** + * Get the route context for this route + * @param router - The router instance (required) + */ + getRouteContext = ( + router: import('@tanstack/router-core').AnyRouter, + ): TRouteContextFn extends AnyContext ? AnyContext : TRouteContextFn => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return matchState?.context as any + } + + /** + * Get the loader dependencies for this route + * @param router - The router instance (required) + */ + getLoaderDeps = ( + router: import('@tanstack/router-core').AnyRouter, + ): TLoaderDeps => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as TLoaderDeps + const matchState = router.getMatch(match.id) + return matchState?.loaderDeps ?? ({} as TLoaderDeps) + } + + /** + * Get the full match data for this route + * @param router - The router instance (required) + */ + getMatch = (router: import('@tanstack/router-core').AnyRouter) => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined + return router.getMatch(match.id) + } +} + +export function createRoute< + TRegister = unknown, + TParentRoute extends RouteConstraints['TParentRoute'] = AnyRoute, + TPath extends RouteConstraints['TPath'] = '/', + TFullPath extends RouteConstraints['TFullPath'] = ResolveFullPath< + TParentRoute, + TPath + >, + TCustomId extends RouteConstraints['TCustomId'] = string, + TId extends RouteConstraints['TId'] = ResolveId< + TParentRoute, + TCustomId, + TPath + >, + TSearchValidator = undefined, + TParams = ResolveParams, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, + TLoaderDeps extends Record = {}, + TLoaderFn = undefined, + TChildren = unknown, + TSSR = unknown, + const TServerMiddlewares = unknown, +>( + options: RouteOptions< + TRegister, + TParentRoute, + TId, + TCustomId, + TFullPath, + TPath, + TSearchValidator, + TParams, + TLoaderDeps, + TLoaderFn, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + TSSR, + TServerMiddlewares + >, +): Route< + TRegister, + TParentRoute, + TPath, + TFullPath, + TCustomId, + TId, + TSearchValidator, + TParams, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TSSR, + TServerMiddlewares +> { + return new Route< + TRegister, + TParentRoute, + TPath, + TFullPath, + TCustomId, + TId, + TSearchValidator, + TParams, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TSSR, + TServerMiddlewares + >(options as any) +} + +export class RootRoute< + in out TRegister = unknown, + in out TSearchValidator = undefined, + in out TRouterContext = {}, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, + in out TLoaderDeps extends Record = {}, + in out TLoaderFn = undefined, + in out TChildren = unknown, + in out TFileRouteTypes = unknown, + in out TSSR = unknown, + in out TServerMiddlewares = unknown, + in out THandlers = undefined, +> extends BaseRootRoute< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TChildren, + TFileRouteTypes, + TSSR, + TServerMiddlewares, + THandlers +> { + constructor( + options?: RootRouteOptions< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TSSR, + TServerMiddlewares, + THandlers + >, + ) { + super(options) + } + + /** + * Get the loader data for this route + * @param router - The router instance (required) + */ + getLoaderData = ( + router: import('@tanstack/router-core').AnyRouter, + ): TLoaderFn extends undefined ? undefined : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined as any + const matchState = router.getMatch(match.id) + return matchState?.loaderData as any + } + + /** + * Get the params for this route + * @param router - The router instance (required) + */ + getParams = ( + router: import('@tanstack/router-core').AnyRouter, + ): Record => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as Record + const matchState = router.getMatch(match.id) + return (matchState?._strictParams ?? matchState?.params ?? {}) as Record< + string, + never + > + } + + /** + * Get the search params for this route + * @param router - The router instance (required) + */ + getSearch = ( + router: import('@tanstack/router-core').AnyRouter, + ): TSearchValidator extends undefined ? Record : any => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return (matchState?._strictSearch ?? matchState?.search ?? {}) as any + } + + /** + * Get the route context for this route + * @param router - The router instance (required) + */ + getRouteContext = ( + router: import('@tanstack/router-core').AnyRouter, + ): TRouteContextFn extends AnyContext ? AnyContext : TRouteContextFn => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as any + const matchState = router.getMatch(match.id) + return matchState?.context as any + } + + /** + * Get the loader dependencies for this route + * @param router - The router instance (required) + */ + getLoaderDeps = ( + router: import('@tanstack/router-core').AnyRouter, + ): TLoaderDeps => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return {} as TLoaderDeps + const matchState = router.getMatch(match.id) + return matchState?.loaderDeps ?? ({} as TLoaderDeps) + } + + /** + * Get the full match data for this route + * @param router - The router instance (required) + */ + getMatch = (router: import('@tanstack/router-core').AnyRouter) => { + const match = router.state.matches.find((m) => m.routeId === this.id) + if (!match) return undefined + return router.getMatch(match.id) + } +} + +export function createRootRoute< + TRegister = Register, + TSearchValidator = undefined, + TRouterContext = {}, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, + TLoaderDeps extends Record = {}, + TLoaderFn = undefined, + TSSR = unknown, + const TServerMiddlewares = unknown, + THandlers = undefined, +>( + options?: RootRouteOptions< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + TSSR, + TServerMiddlewares, + THandlers + >, +): RootRoute< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + unknown, + unknown, + TSSR, + TServerMiddlewares, + THandlers +> { + return new RootRoute< + TRegister, + TSearchValidator, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TLoaderDeps, + TLoaderFn, + unknown, + unknown, + TSSR, + TServerMiddlewares, + THandlers + >(options as any) +} diff --git a/packages/vanilla-router/src/router.ts b/packages/vanilla-router/src/router.ts new file mode 100644 index 00000000000..1442a3ceacd --- /dev/null +++ b/packages/vanilla-router/src/router.ts @@ -0,0 +1,85 @@ +import { RouterCore } from '@tanstack/router-core' +import type { RouterHistory } from '@tanstack/history' +import type { + AnyRoute, + CreateRouterFn, + RouterConstructorOptions, + RouterState, + TrailingSlashOption, +} from '@tanstack/router-core' + +import type { + VanillaErrorRouteComponent, + VanillaNotFoundRouteComponent, + VanillaRouteComponent, +} from './types' + +declare module '@tanstack/router-core' { + export interface RouterOptionsExtensions { + /** + * The default `component` a route should use if no component is provided. + * + * @default Outlet + */ + defaultComponent?: VanillaRouteComponent + /** + * The default `errorComponent` a route should use if no error component is provided. + */ + defaultErrorComponent?: VanillaErrorRouteComponent + /** + * The default `pendingComponent` a route should use if no pending component is provided. + */ + defaultPendingComponent?: VanillaRouteComponent + /** + * The default `notFoundComponent` a route should use if no notFound component is provided. + */ + defaultNotFoundComponent?: VanillaNotFoundRouteComponent + } +} + +export const createRouter: CreateRouterFn = (options) => { + return new Router(options) +} + +export class Router< + in out TRouteTree extends AnyRoute, + in out TTrailingSlashOption extends TrailingSlashOption = 'never', + in out TDefaultStructuralSharingOption extends boolean = false, + in out TRouterHistory extends RouterHistory = RouterHistory, + in out TDehydrated extends Record = Record, +> extends RouterCore< + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated +> { + constructor( + options: RouterConstructorOptions< + TRouteTree, + TTrailingSlashOption, + TDefaultStructuralSharingOption, + TRouterHistory, + TDehydrated + >, + ) { + super(options) + } + + /** + * Subscribe to router state changes + * This is the recommended way to react to router state updates in vanilla JS + * Similar to React Router's useRouterState hook + * + * @param callback - Function called whenever router state changes + * @returns Unsubscribe function + */ + subscribeState( + callback: (state: RouterState) => void, + ): () => void { + // Subscribe directly to the router's store (same as React Router does internally) + return this.__store.subscribe(() => { + callback(this.state) + }) + } +} diff --git a/packages/vanilla-router/src/scroll-restoration.ts b/packages/vanilla-router/src/scroll-restoration.ts new file mode 100644 index 00000000000..586072adf35 --- /dev/null +++ b/packages/vanilla-router/src/scroll-restoration.ts @@ -0,0 +1,127 @@ +import type { + AnyRouter, + ParsedLocation, + ScrollRestorationEntry, +} from '@tanstack/router-core' +import { + defaultGetScrollRestorationKey, + getCssSelector, + scrollRestorationCache, + setupScrollRestoration, +} from '@tanstack/router-core' + +/** + * Setup scroll restoration for the router + * This is typically called automatically when scrollRestoration: true is set in router options + * But can be called manually if needed + * + * @param router - The router instance + * @param force - Force setup even if scrollRestoration is false in options + */ +export function setupScrollRestorationUtil( + router: TRouter, + force?: boolean, +): void { + setupScrollRestoration(router, force) +} + +/** + * Get scroll position for a specific element or window + * This is a vanilla JS equivalent of useElementScrollRestoration hook + * + * @param router - The router instance + * @param options - Options for getting scroll position + * @param options.id - Unique ID for the element (must match data-scroll-restoration-id attribute) + * @param options.getElement - Function that returns the element to get scroll position for + * @param options.getKey - Optional function to get the cache key (defaults to location.href) + * @returns Scroll restoration entry with scrollX and scrollY, or undefined if not found + */ +export function getScrollPosition( + router: TRouter, + options: { + id?: string + getElement?: () => Window | Element | undefined | null + getKey?: (location: ParsedLocation) => string + }, +): ScrollRestorationEntry | undefined { + if (!scrollRestorationCache) return undefined + + const getKey = options.getKey || defaultGetScrollRestorationKey + const restoreKey = getKey(router.latestLocation) + const byKey = scrollRestorationCache.state[restoreKey] + + if (!byKey) return undefined + + let elementSelector = '' + + if (options.id) { + elementSelector = `[data-scroll-restoration-id="${options.id}"]` + } else { + const element = options.getElement?.() + if (!element) { + return undefined + } + elementSelector = + element instanceof Window ? 'window' : getCssSelector(element) + } + + return byKey[elementSelector] +} + +/** + * Save scroll position for a specific element or window + * This is typically handled automatically by setupScrollRestoration + * But can be called manually if needed + * + * @param router - The router instance + * @param options - Options for saving scroll position + * @param options.id - Unique ID for the element (must match data-scroll-restoration-id attribute) + * @param options.getElement - Function that returns the element to save scroll position for + * @param options.getKey - Optional function to get the cache key (defaults to location.href) + */ +export function saveScrollPosition( + router: TRouter, + options: { + id?: string + getElement?: () => Window | Element | undefined | null + getKey?: (location: ParsedLocation) => string + }, +): void { + if (!scrollRestorationCache) return + + const getKey = options.getKey || defaultGetScrollRestorationKey + const restoreKey = getKey(router.latestLocation) + + let elementSelector = '' + + if (options.id) { + elementSelector = `[data-scroll-restoration-id="${options.id}"]` + } else { + const element = options.getElement?.() + if (!element) { + return + } + elementSelector = + element instanceof Window ? 'window' : getCssSelector(element) + } + + scrollRestorationCache.set((state) => { + const keyEntry = (state[restoreKey] ||= {} as any) + + const elementEntry = (keyEntry[elementSelector] ||= + {} as ScrollRestorationEntry) + + if (elementSelector === 'window') { + elementEntry.scrollX = window.scrollX || 0 + elementEntry.scrollY = window.scrollY || 0 + } else if (elementSelector) { + const element = document.querySelector(elementSelector) + if (element) { + elementEntry.scrollX = element.scrollLeft || 0 + elementEntry.scrollY = element.scrollTop || 0 + } + } + + return state + }) +} diff --git a/packages/vanilla-router/src/types.ts b/packages/vanilla-router/src/types.ts new file mode 100644 index 00000000000..fbb97bc2aef --- /dev/null +++ b/packages/vanilla-router/src/types.ts @@ -0,0 +1,13 @@ +// Vanilla component types +// Components receive router as parameter and return a render function with no parameters +export type VanillaComponent = ( + router: import('@tanstack/router-core').AnyRouter, +) => (() => string) | [() => void, () => string] + +export type VanillaRouteComponent = VanillaComponent +export type VanillaErrorRouteComponent = (props: { + error: Error +}) => VanillaComponent +export type VanillaNotFoundRouteComponent = (props: { + data?: any +}) => VanillaComponent diff --git a/packages/vanilla-router/src/utils.ts b/packages/vanilla-router/src/utils.ts new file mode 100644 index 00000000000..f649f1bd537 --- /dev/null +++ b/packages/vanilla-router/src/utils.ts @@ -0,0 +1,89 @@ +import type { AnyRouter, RouterState } from '@tanstack/router-core' +import { replaceEqualDeep } from '@tanstack/router-core' + +/** + * Subscribe to router state changes with optional selector + * This is a vanilla JS equivalent of useRouterState hook + * + * @param router - The router instance + * @param callback - Function called whenever router state changes (or selected state changes) + * @param selector - Optional function to select a portion of the router state + * @param structuralSharing - Whether to use structural sharing (deep equality check) for the selected value + * @returns Unsubscribe function + */ +export function subscribeRouterState( + router: TRouter, + callback: (state: RouterState) => void, + selector?: (state: RouterState) => any, + structuralSharing?: boolean, +): () => void { + let previousResult: any = undefined + + const unsubscribe = router.subscribeState((state) => { + if (selector) { + const newSlice = selector(state) + + if (structuralSharing ?? router.options.defaultStructuralSharing) { + const sharedSlice = replaceEqualDeep(previousResult, newSlice) + previousResult = sharedSlice + + // Always call callback - replaceEqualDeep handles equality checking + callback(sharedSlice) + } else { + callback(newSlice) + } + } else { + callback(state) + } + }) + + return unsubscribe +} + +/** + * Get current router state (synchronous) + * This is a vanilla JS equivalent of useRouterState hook without subscription + * + * @param router - The router instance + * @param selector - Optional function to select a portion of the router state + * @returns Current router state or selected portion + */ +export function getRouterState( + router: TRouter, + selector?: (state: RouterState) => any, +): any { + const state = router.state + return selector ? selector(state) : state +} + +/** + * Get current location from router state + * This is a vanilla JS equivalent of useLocation hook + * + * @param router - The router instance + * @param selector - Optional function to select a portion of the location + * @returns Current location or selected portion + */ +export function getLocation( + router: TRouter, + selector?: (location: RouterState['location']) => any, +): any { + const location = router.state.location + return selector ? selector(location) : location +} + +/** + * Get current matches from router state + * This is a vanilla JS equivalent of useMatches hook + * + * @param router - The router instance + * @param selector - Optional function to select/transform matches + * @returns Current matches or selected/transformed matches + */ +export function getMatches( + router: TRouter, + selector?: (matches: RouterState['matches']) => any, +): any { + const matches = router.state.matches + return selector ? selector(matches) : matches +} diff --git a/packages/vanilla-router/src/vanilla-router.ts b/packages/vanilla-router/src/vanilla-router.ts new file mode 100644 index 00000000000..3f03c5bd0d0 --- /dev/null +++ b/packages/vanilla-router/src/vanilla-router.ts @@ -0,0 +1,256 @@ +import type { AnyRouter, AnyRoute } from '@tanstack/router-core' +import { isNotFound } from '@tanstack/router-core' +import { setupScrollRestorationUtil } from './scroll-restoration' + +/** + * Internal outlet marker - used by outlet() function + */ +const OUTLET_MARKER = '__TANSTACK_ROUTER_OUTLET__' + +/** + * Function to mark where child routes should be rendered + * Returns a special marker that will be replaced with child content + */ +export function outlet(): string { + return OUTLET_MARKER +} + +/** + * Process router matches and return nested HTML strings + * Handles outlet replacement for nested routes automatically + * Returns an array of HTML strings in render order + * + * @param router - The router instance + * @param matches - Array of matches from router.state.matches + * @returns Array of HTML strings with nested routes properly composed + */ +export function getMatchesHtml( + router: AnyRouter, + matches: AnyRouter['state']['matches'], +): string[] { + const htmlParts: string[] = [] + + matches.forEach((match, index) => { + // Get HTML for this match + const componentHtml = getMatchHtml(router, match) + + // For nested routes, replace outlet marker in parent + if (index > 0 && htmlParts.length > 0) { + const lastHtml = htmlParts[htmlParts.length - 1] + if (lastHtml.includes(OUTLET_MARKER)) { + htmlParts[htmlParts.length - 1] = lastHtml.replace( + OUTLET_MARKER, + componentHtml, + ) + } else { + htmlParts.push(componentHtml) + } + } else { + htmlParts.push(componentHtml) + } + }) + + // Clean up any remaining outlet markers (e.g., when root route has outlet but no children) + return htmlParts.map((html) => html.replace(OUTLET_MARKER, '')) +} + +/** + * Get HTML for a single match (handles error, pending, not found, and component states) + */ +function getMatchHtml( + router: AnyRouter, + match: AnyRouter['state']['matches'][0], +): string { + const route: AnyRoute = router.routesById[match.routeId] + const matchState = router.getMatch(match.id) + + try { + // Check for not found status first (like React/Preact adapters) + if (match.status === 'notFound') { + const notFoundComponent = + route.options.notFoundComponent === false + ? undefined + : (route.options.notFoundComponent ?? + router.options.defaultNotFoundComponent) + + if ( + notFoundComponent && + matchState?.error && + isNotFound(matchState.error) + ) { + const notFoundFactory = notFoundComponent({ data: matchState.error }) + const notFoundHtml = notFoundFactory(router) + return typeof notFoundHtml === 'string' ? notFoundHtml : notFoundHtml() + } + + // Fallback if no notFoundComponent configured + return '
Not Found
' + } + + // Get components from route options + const errorComponent = + route.options.errorComponent === false + ? undefined + : (route.options.errorComponent ?? router.options.defaultErrorComponent) + const pendingComponent = + route.options.pendingComponent ?? router.options.defaultPendingComponent + const component = route.options.component ?? router.options.defaultComponent + + // Check for error state + if (matchState?.error && errorComponent) { + const errorFactory = errorComponent({ error: matchState.error }) + const errorHtml = errorFactory(router) + return typeof errorHtml === 'string' ? errorHtml : errorHtml() + } + + // Check for pending state + if (matchState?._displayPending && pendingComponent) { + const pendingHtml = pendingComponent(router) + return typeof pendingHtml === 'string' ? pendingHtml : pendingHtml() + } + + // Render component + if (component) { + const componentHtml = component(router) + return typeof componentHtml === 'string' ? componentHtml : componentHtml() + } + + return '' + } catch (error) { + // If component throws, return empty string + console.error('Error rendering component:', error) + return '' + } +} + +/** + * Build a type-safe href for navigation + * Useful for direct DOM manipulation where you don't have Link components + * Behaves like navigate() and APIs - accepts a 'to' path string + */ +export function buildHref( + router: AnyRouter, + options: { + to?: string + params?: Record + search?: Record + hash?: string + }, +): string { + const location = router.buildLocation({ + to: options.to, + params: options.params, + search: options.search, + hash: options.hash, + }) + return location.href +} + +/** + * Enable automatic link handling for the entire document + * Returns a cleanup function to remove event listeners + */ +export function setupLinkHandlers(router: AnyRouter): () => void { + // Use event delegation for link clicks on the entire document + const linkClickHandler = (e: Event) => { + const target = e.target as HTMLElement + const link = target.closest('a[href]') as HTMLAnchorElement + if (!link) return + + const href = link.getAttribute('href') + if (!href) return + + // Skip external links and links with target + if (href.startsWith('http://') || href.startsWith('https://')) return + if (link.target && link.target !== '_self') return + + // Skip if modifier keys are pressed + if ( + (e as MouseEvent).metaKey || + (e as MouseEvent).ctrlKey || + (e as MouseEvent).altKey || + (e as MouseEvent).shiftKey + ) + return + + e.preventDefault() + e.stopPropagation() + + const replace = link.hasAttribute('data-replace') + const resetScroll = link.hasAttribute('data-reset-scroll') + const hashScroll = link.getAttribute('data-hash-scroll') + + router + .navigate({ + to: href, + replace, + resetScroll: resetScroll !== null, + hashScrollIntoView: + hashScroll === 'true' + ? true + : hashScroll === 'false' + ? false + : undefined, + }) + .then(() => { + // Router state change will trigger render via subscription + }) + .catch((err) => { + console.error('Navigation error:', err) + }) + } + + document.addEventListener('click', linkClickHandler, true) + + // Return cleanup function + return () => { + document.removeEventListener('click', linkClickHandler, true) + } +} + +/** + * Setup router with automatic state subscription and link handling + * This is a convenience function that combines subscribeState and setupLinkHandlers + * Also handles initial loading and rendering + * + * @param router - The router instance + * @param renderCallback - Function called whenever router state changes to render the UI + * @returns Cleanup function that unsubscribes from state changes and removes link handlers + */ +export async function vanillaRouter( + router: AnyRouter, + renderCallback: () => void, +): Promise<() => void> { + // Load initial matches if needed + if (router.state.matches.length === 0) { + try { + await router.load() + } catch (error) { + console.error('Error loading router:', error) + } + } + + // Setup scroll restoration if enabled + if (router.options.scrollRestoration) { + setupScrollRestorationUtil(router) + } + + // Initial render + renderCallback() + + // Subscribe to router state changes + // Use subscribeState if available (vanilla Router), otherwise fall back to __store + const unsubscribeState = + typeof (router as any).subscribeState === 'function' + ? (router as any).subscribeState(renderCallback) + : router.__store.subscribe(renderCallback) + + // Setup link handlers on document + const cleanupLinkHandlers = setupLinkHandlers(router) + + // Return combined cleanup function + return () => { + unsubscribeState() + cleanupLinkHandlers() + } +} diff --git a/packages/vanilla-router/tests/router.test.ts b/packages/vanilla-router/tests/router.test.ts new file mode 100644 index 00000000000..de8854081bb --- /dev/null +++ b/packages/vanilla-router/tests/router.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { createRouter, createRootRoute, createRoute } from '../src' +import { getMatchesHtml, buildHref, outlet } from '../src' + +describe('Vanilla Router', () => { + it('should create a router', () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
Root
` + }, + }) + + const router = createRouter({ + routeTree: rootRoute, + }) + + expect(router).toBeDefined() + expect(router.state).toBeDefined() + }) + + it('should render root component', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
Root Component
` + }, + }) + + const router = createRouter({ + routeTree: rootRoute, + }) + + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + expect(htmlParts.join('')).toContain('Root Component') + }) + + it('should render nested routes', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
Root ${outlet()}
` + }, + }) + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: (router) => { + return `
Index
` + }, + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute]), + }) + + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + const html = htmlParts.join('') + expect(html).toContain('Root') + expect(html).toContain('Index') + }) + + it('should build hrefs', () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
Root
` + }, + }) + + const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + }) + + const postRoute = createRoute({ + getParentRoute: () => postsRoute, + path: '$postId', + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([postsRoute.addChildren([postRoute])]), + }) + + const href = buildHref(router, { + to: '/posts/$postId', + params: { postId: '123' }, + }) + + expect(href).toBe('/posts/123') + }) + + it('should use route getters', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
Root
` + }, + }) + + const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', + loader: () => { + return [{ id: '1', title: 'Post 1' }] + }, + component: (router) => { + const posts = postsRoute.getLoaderData(router) + return `
Posts: ${JSON.stringify(posts)}
` + }, + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([postsRoute]), + }) + + await router.navigate({ to: '/posts' }) + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + const html = htmlParts.join('') + expect(html).toContain('Posts:') + expect(html).toContain('Post 1') + }) + + it('should handle route params', async () => { + const rootRoute = createRootRoute({ + component: (router) => { + return `
Root
` + }, + }) + + const postRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts/$postId', + component: (router) => { + const params = postRoute.getParams(router) + return `
Post ID: ${params.postId}
` + }, + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([postRoute]), + }) + + await router.navigate({ to: '/posts/123' }) + await router.load() + + const htmlParts = getMatchesHtml(router, router.state.matches) + const html = htmlParts.join('') + expect(html).toContain('Post ID: 123') + }) +}) diff --git a/packages/vanilla-router/tsconfig.json b/packages/vanilla-router/tsconfig.json new file mode 100644 index 00000000000..65fd562dd68 --- /dev/null +++ b/packages/vanilla-router/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/vanilla-router/vite.config.ts b/packages/vanilla-router/vite.config.ts new file mode 100644 index 00000000000..0331779af54 --- /dev/null +++ b/packages/vanilla-router/vite.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from 'vitest/config' +import dts from 'vite-plugin-dts' +import packageJson from './package.json' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const config = defineConfig({ + plugins: [ + dts({ + entryRoot: 'src', + tsconfigPath: path.join(__dirname, 'tsconfig.json'), + outDir: 'dist', + }), + ], + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + typecheck: { enabled: true }, + }, + build: { + lib: { + entry: ['./src/index.ts'], + formats: ['es', 'cjs'], + fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs'), + }, + rollupOptions: { + // Bundle router-core and history instead of externalizing them + external: [], + }, + }, +}) + +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c48463509cc..c55fbc571ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7715,6 +7715,55 @@ importers: specifier: ^7.1.7 version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + packages/preact-router: + dependencies: + '@tanstack/history': + specifier: workspace:* + version: link:../history + '@tanstack/react-store': + specifier: ^0.7.0 + version: 0.7.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + '@tanstack/store': + specifier: ^0.7.0 + version: 0.7.7 + isbot: + specifier: ^5.1.22 + version: 5.1.28 + preact-render-to-string: + specifier: ^6.3.1 + version: 6.6.3(preact@10.27.2) + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + tiny-warning: + specifier: ^1.0.3 + version: 1.0.3 + devDependencies: + '@preact/preset-vite': + specifier: ^2.9.3 + version: 2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/preact': + specifier: ^3.2.4 + version: 3.2.4(preact@10.27.2) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + preact: + specifier: ^10.24.3 + version: 10.27.2 + vibe-rules: + specifier: ^0.2.57 + version: 0.2.57 + zod: + specifier: ^3.24.2 + version: 3.25.57 + packages/react-router: dependencies: '@tanstack/history': @@ -8573,6 +8622,40 @@ importers: specifier: 1.0.0-beta.15 version: 1.0.0-beta.15(typescript@5.9.2) + packages/vanilla-router: + dependencies: + '@tanstack/history': + specifier: workspace:* + version: link:../history + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 + tiny-warning: + specifier: ^1.0.3 + version: 1.0.3 + devDependencies: + combinate: + specifier: ^1.1.11 + version: 1.1.11 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vibe-rules: + specifier: ^0.2.57 + version: 0.2.57 + vite-plugin-dts: + specifier: 4.0.3 + version: 4.0.3(@types/node@22.10.2)(rollup@4.52.5)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + zod: + specifier: ^3.24.2 + version: 3.25.57 + packages/virtual-file-routes: {} packages/zod-adapter: @@ -8802,6 +8885,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.25.9': resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} engines: {node: '>=6.9.0'} @@ -8826,6 +8915,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@7.27.1': + resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.27.1': resolution: {integrity: sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==} engines: {node: '>=6.9.0'} @@ -10903,6 +10998,29 @@ packages: '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + '@preact/preset-vite@2.10.2': + resolution: {integrity: sha512-K9wHlJOtkE+cGqlyQ5v9kL3Ge0Ql4LlIZjkUTL+1zf3nNdF88F9UZN6VTV8jdzBX9Fl7WSzeNMSDG7qECPmSmg==} + peerDependencies: + '@babel/core': 7.x + vite: ^7.1.7 + + '@prefresh/babel-plugin@0.5.2': + resolution: {integrity: sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==} + + '@prefresh/core@1.5.8': + resolution: {integrity: sha512-T7HMpakS1iPVCFZvfDLMGyrWAcO3toUN9/RkJUqqoRr/vNhQrZgHjidfhq3awDzAQtw1emDWH8dsOeu0DWqtgA==} + peerDependencies: + preact: ^10.0.0 || ^11.0.0-0 + + '@prefresh/utils@1.2.1': + resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} + + '@prefresh/vite@2.4.11': + resolution: {integrity: sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==} + peerDependencies: + preact: ^10.4.0 || ^11.0.0-0 + vite: ^7.1.7 + '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} @@ -11704,6 +11822,10 @@ packages: rollup: optional: true + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -12762,6 +12884,12 @@ packages: peerDependencies: react: ^19.0.0 + '@tanstack/react-store@0.7.7': + resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + peerDependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + '@tanstack/react-store@0.8.0': resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==} peerDependencies: @@ -12801,6 +12929,9 @@ packages: peerDependencies: solid-js: 1.9.10 + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tanstack/store@0.8.0': resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} @@ -12822,10 +12953,20 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + '@testing-library/jest-dom@6.6.3': resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/preact@3.2.4': + resolution: {integrity: sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==} + engines: {node: '>= 12'} + peerDependencies: + preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0' + '@testing-library/react@16.2.0': resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} engines: {node: '>=18'} @@ -13335,9 +13476,23 @@ packages: webdriverio: optional: true + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^7.1.7 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.0.6': resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==} peerDependencies: @@ -13360,18 +13515,30 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.0.6': resolution: {integrity: sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==} '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.0.6': resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==} @@ -13383,6 +13550,9 @@ packages: peerDependencies: vitest: 3.0.6 + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.0.6': resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==} @@ -13721,6 +13891,9 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -13731,6 +13904,10 @@ packages: arktype@2.1.7: resolution: {integrity: sha512-RpczU+Ny4g4BqeVu9v2o288A5p8DQ8w8kJuFcD3okCT+oHP8C9YDTkj2kJG2Vz3XQAN2O9Aw06RNoJV0UZ8m6A==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -13772,6 +13949,10 @@ packages: peerDependencies: postcss: ^8.1.0 + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + axios@1.9.0: resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} @@ -13790,6 +13971,11 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} + babel-plugin-transform-hook-names@1.0.2: + resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} + peerDependencies: + '@babel/core': ^7.12.10 + babel-preset-solid@1.9.3: resolution: {integrity: sha512-jvlx5wDp8s+bEF9sGFw/84SInXOA51ttkUEroQziKMbxplXThVKt83qB6bDTa1HuLNatdU9FHpFOiQWs1tLQIg==} peerDependencies: @@ -13909,10 +14095,22 @@ packages: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + call-bound@1.0.3: resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} @@ -14460,6 +14658,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -14478,6 +14680,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -14486,6 +14692,10 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -14762,6 +14972,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -15207,6 +15420,10 @@ packages: debug: optional: true + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -15278,6 +15495,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -15294,6 +15514,10 @@ packages: resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -15426,6 +15650,10 @@ packages: handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -15434,10 +15662,17 @@ packages: resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -15645,6 +15880,10 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + interpret@3.1.1: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} @@ -15689,20 +15928,44 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -15744,6 +16007,10 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -15754,6 +16021,10 @@ packages: is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -15795,6 +16066,18 @@ packages: is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -15807,6 +16090,14 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + is-text-path@2.0.0: resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} engines: {node: '>=8'} @@ -15826,6 +16117,14 @@ packages: is-url@1.2.4: resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -15845,6 +16144,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbot@5.1.28: resolution: {integrity: sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==} engines: {node: '>=18'} @@ -16743,6 +17045,9 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} @@ -16824,6 +17129,18 @@ packages: resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -17092,6 +17409,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -17149,6 +17470,14 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@6.6.3: + resolution: {integrity: sha512-7oHG7jzjriqsFPkSPiPnzrQ0GcxFm6wOkYWNdStK5Ks9YlWSQQXKGBRAX4nKDdqX7HAQuRvI4pZNZMycK4WwDw==} + peerDependencies: + preact: '>=10 || >= 11.0.0-0' + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + precinct@12.2.0: resolution: {integrity: sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==} engines: {node: '>=18'} @@ -17440,6 +17769,10 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + relateurl@0.2.7: resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} engines: {node: '>= 0.10'} @@ -17585,6 +17918,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -17670,6 +18007,14 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} @@ -17726,6 +18071,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-code-frame@1.3.0: + resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} + simple-git@3.28.0: resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} @@ -17839,6 +18187,10 @@ packages: stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stack-trace@1.0.0-pre2: + resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} + engines: {node: '>=16'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -17862,6 +18214,10 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} @@ -18083,6 +18439,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -18599,11 +18959,6 @@ packages: '@types/react': optional: true - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^19.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -18657,6 +19012,11 @@ packages: resolution: {integrity: sha512-CVGXHyKRvDeC3S6SywxTcNGuckmSjwB+2q/v8eDSmwDBTlz0ziRqm49eI5ELLy4djKq6DdCSYvV4EGcwzsHRog==} hasBin: true + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -18687,6 +19047,11 @@ packages: '@testing-library/jest-dom': optional: true + vite-prerender-plugin@0.5.12: + resolution: {integrity: sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==} + peerDependencies: + vite: ^7.1.7 + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -18743,6 +19108,31 @@ packages: vite: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': 22.10.2 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -18914,6 +19304,18 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -19432,6 +19834,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.27.4)': dependencies: '@babel/core': 7.27.4 @@ -19462,6 +19871,17 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.4)': dependencies: '@babel/core': 7.27.4 @@ -21653,11 +22073,47 @@ snapshots: '@poppinss/exception@1.2.2': {} - '@prisma/client@5.22.0(prisma@5.22.0)': - optionalDependencies: - prisma: 5.22.0 - - '@prisma/debug@5.22.0': {} + '@preact/preset-vite@2.10.2(@babel/core@7.28.4)(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.4) + '@prefresh/vite': 2.4.11(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@rollup/pluginutils': 4.2.1 + babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.4) + debug: 4.4.3 + picocolors: 1.1.1 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-prerender-plugin: 0.5.12(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + transitivePeerDependencies: + - preact + - supports-color + + '@prefresh/babel-plugin@0.5.2': {} + + '@prefresh/core@1.5.8(preact@10.27.2)': + dependencies: + preact: 10.27.2 + + '@prefresh/utils@1.2.1': {} + + '@prefresh/vite@2.4.11(preact@10.27.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.4 + '@prefresh/babel-plugin': 0.5.2 + '@prefresh/core': 1.5.8(preact@10.27.2) + '@prefresh/utils': 1.2.1 + '@rollup/pluginutils': 4.2.1 + preact: 10.27.2 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + '@prisma/client@5.22.0(prisma@5.22.0)': + optionalDependencies: + prisma: 5.22.0 + + '@prisma/debug@5.22.0': {} '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} @@ -22479,6 +22935,11 @@ snapshots: optionalDependencies: rollup: 4.52.2 + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + '@rollup/pluginutils@5.1.4(rollup@4.52.2)': dependencies: '@types/estree': 1.0.8 @@ -23497,6 +23958,13 @@ snapshots: '@tanstack/query-core': 5.90.6 react: 19.0.0 + '@tanstack/react-store@0.7.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/store': 0.7.7 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + use-sync-external-store: 1.6.0(react@19.0.0) + '@tanstack/react-store@0.8.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@tanstack/store': 0.8.0 @@ -23537,6 +24005,8 @@ snapshots: '@tanstack/virtual-core': 3.13.12 solid-js: 1.9.10 + '@tanstack/store@0.7.7': {} + '@tanstack/store@0.8.0': {} '@tanstack/typedoc-config@0.3.0(typescript@5.9.2)': @@ -23575,6 +24045,17 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.26.7 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/jest-dom@6.6.3': dependencies: '@adobe/css-tools': 4.4.1 @@ -23585,6 +24066,11 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 + '@testing-library/preact@3.2.4(preact@10.27.2)': + dependencies: + '@testing-library/dom': 8.20.1 + preact: 10.27.2 + '@testing-library/react@16.2.0(@testing-library/dom@10.4.1)(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.7 @@ -24214,6 +24700,13 @@ snapshots: - utf-8-validate - vite + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -24222,6 +24715,15 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + msw: 2.7.0(@types/node@22.10.2)(typescript@5.9.2) + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + '@vitest/mocker@3.0.6(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.0.6 @@ -24240,6 +24742,10 @@ snapshots: msw: 2.7.0(@types/node@22.10.2)(typescript@5.9.2) vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.0.6': dependencies: tinyrainbow: 2.0.0 @@ -24248,18 +24754,33 @@ snapshots: dependencies: tinyrainbow: 2.0.0 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 pathe: 2.0.3 strip-literal: 3.0.0 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.0.6': dependencies: tinyspy: 3.0.2 @@ -24279,6 +24800,12 @@ snapshots: tinyrainbow: 2.0.0 vitest: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@3.0.6': dependencies: '@vitest/pretty-format': 3.0.6 @@ -24700,6 +25227,10 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -24711,6 +25242,11 @@ snapshots: '@ark/schema': 0.44.2 '@ark/util': 0.44.2 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.3 + is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} array-ify@1.0.0: {} @@ -24747,6 +25283,10 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + axios@1.9.0: dependencies: follow-redirects: 1.15.9(debug@4.4.3) @@ -24792,6 +25332,10 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.10 + babel-plugin-transform-hook-names@1.0.2(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + babel-preset-solid@1.9.3(@babel/core@7.27.4): dependencies: '@babel/core': 7.27.4 @@ -24951,11 +25495,28 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.7 + set-function-length: 1.2.2 + call-bound@1.0.3: dependencies: call-bind-apply-helpers: 1.0.1 get-intrinsic: 1.2.7 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsite@1.0.0: {} callsites@3.1.0: {} @@ -25433,6 +25994,27 @@ snapshots: deep-eql@5.0.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.7 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -25448,10 +26030,22 @@ snapshots: dependencies: clone: 1.0.4 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@2.0.0: {} define-lazy-prop@3.0.0: {} + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + defu@6.1.4: {} delayed-stream@1.0.0: {} @@ -25702,6 +26296,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.2.7 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@1.6.0: {} es-module-lexer@1.7.0: {} @@ -26416,6 +27022,10 @@ snapshots: optionalDependencies: debug: 4.4.3 + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 @@ -26477,6 +27087,8 @@ snapshots: function-bind@1.1.2: {} + functions-have-names@1.2.3: {} + gensync@1.0.0-beta.2: {} get-amd-module-type@6.0.1: @@ -26499,6 +27111,19 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-port-please@3.2.0: {} @@ -26639,12 +27264,22 @@ snapshots: handle-thing@2.0.1: {} + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-own-prop@2.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -26856,6 +27491,12 @@ snapshots: inline-style-parser@0.2.4: {} + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + interpret@3.1.1: {} ioredis@5.8.0: @@ -26939,18 +27580,45 @@ snapshots: iron-webcrypto@1.2.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + is-arrayish@0.2.1: {} is-arrayish@0.3.4: {} + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -26983,12 +27651,19 @@ snapshots: is-interactive@1.0.0: {} + is-map@2.0.3: {} + is-module@1.0.0: {} is-network-error@1.1.0: {} is-node-process@1.2.0: {} + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-obj@2.0.0: {} @@ -27015,12 +27690,36 @@ snapshots: dependencies: '@types/estree': 1.0.7 + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.3 + is-stream@2.0.1: {} is-stream@3.0.0: {} is-stream@4.0.1: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.3 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 @@ -27033,6 +27732,13 @@ snapshots: is-url@1.2.4: {} + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + is-what@4.1.16: {} is-wsl@2.2.0: @@ -27049,6 +27755,8 @@ snapshots: isarray@1.0.0: {} + isarray@2.0.5: {} + isbot@5.1.28: {} isexe@2.0.0: {} @@ -28028,6 +28736,11 @@ snapshots: node-gyp-build@4.8.4: {} + node-html-parser@6.1.13: + dependencies: + css-select: 5.1.0 + he: 1.2.0 + node-machine-id@1.1.12: {} node-mock-http@1.0.3: {} @@ -28138,6 +28851,22 @@ snapshots: object-inspect@1.13.3: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + obuf@1.1.2: {} ofetch@1.4.1: @@ -28394,6 +29123,8 @@ snapshots: pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} + postcss-import@15.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -28446,6 +29177,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@6.6.3(preact@10.27.2): + dependencies: + preact: 10.27.2 + + preact@10.27.2: {} + precinct@12.2.0: dependencies: '@dependents/detective-less': 5.0.1 @@ -28807,6 +29544,15 @@ snapshots: regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + relateurl@0.2.7: {} remove-trailing-separator@1.1.0: {} @@ -28983,6 +29729,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -29103,6 +29855,22 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + setprototypeof@1.1.0: {} setprototypeof@1.2.0: {} @@ -29208,6 +29976,10 @@ snapshots: signal-exit@4.1.0: {} + simple-code-frame@1.3.0: + dependencies: + kolorist: 1.8.0 + simple-git@3.28.0: dependencies: '@kwsites/file-exists': 1.1.1 @@ -29332,6 +30104,8 @@ snapshots: stack-trace@0.0.10: {} + stack-trace@1.0.0-pre2: {} + stackback@0.0.2: {} stackframe@1.3.4: {} @@ -29349,6 +30123,11 @@ snapshots: std-env@3.9.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + stoppable@1.1.0: {} streamx@2.22.0: @@ -29459,7 +30238,7 @@ snapshots: dependencies: dequal: 2.0.3 react: 19.0.0 - use-sync-external-store: 1.5.0(react@19.0.0) + use-sync-external-store: 1.6.0(react@19.0.0) symbol-tree@3.2.4: {} @@ -29601,6 +30380,8 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@3.0.2: {} @@ -30009,10 +30790,6 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 - use-sync-external-store@1.5.0(react@19.0.0): - dependencies: - react: 19.0.0 - use-sync-external-store@1.6.0(react@19.0.0): dependencies: react: 19.0.0 @@ -30056,6 +30833,27 @@ snapshots: import-meta-resolve: 4.1.0 zod: 3.25.57 + vite-node@2.1.9(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -30156,6 +30954,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite-prerender-plugin@0.5.12(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): + dependencies: + kolorist: 1.8.0 + magic-string: 0.30.19 + node-html-parser: 6.1.13 + simple-code-frame: 1.3.0 + source-map: 0.7.6 + stack-trace: 1.0.0-pre2 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: debug: 4.4.0 @@ -30227,6 +31035,47 @@ snapshots: optionalDependencies: vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vitest@2.1.9(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-node: 2.1.9(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.10.2 + '@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.52.0)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/ui': 3.0.6(vitest@3.2.4) + jsdom: 27.0.0(postcss@8.5.6) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 @@ -30551,6 +31400,31 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0