diff --git a/examples/react/start-basic-static/.gitignore b/examples/react/start-basic-static/.gitignore new file mode 100644 index 0000000000..be342025da --- /dev/null +++ b/examples/react/start-basic-static/.gitignore @@ -0,0 +1,22 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +.vinxi + +/build/ +/api/ +/server/build +/public/build +.vinxi +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/examples/react/start-basic-static/.prettierignore b/examples/react/start-basic-static/.prettierignore new file mode 100644 index 0000000000..2be5eaa6ec --- /dev/null +++ b/examples/react/start-basic-static/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/examples/react/start-basic-static/README.md b/examples/react/start-basic-static/README.md new file mode 100644 index 0000000000..eb580a5bf8 --- /dev/null +++ b/examples/react/start-basic-static/README.md @@ -0,0 +1,72 @@ +# Welcome to TanStack.com! + +This site is built with TanStack Router! + +- [TanStack Router Docs](https://tanstack.com/router) + +It's deployed automagically with Vercel! + +- [Vercel](https://vercel.com/) + +## Development + +From your terminal: + +```sh +pnpm install +pnpm dev +``` + +This starts your app in development mode, rebuilding assets on file changes. + +## Editing and previewing the docs of TanStack projects locally + +The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app. +In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system. + +Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally : + +1. Create a new directory called `tanstack`. + +```sh +mkdir tanstack +``` + +2. Enter the directory and clone this repo and the repo of the project there. + +```sh +cd tanstack +git clone git@github.com:TanStack/tanstack.com.git +git clone git@github.com:TanStack/form.git +``` + +> [!NOTE] +> Your `tanstack` directory should look like this: +> +> ``` +> tanstack/ +> | +> +-- form/ +> | +> +-- tanstack.com/ +> ``` + +> [!WARNING] +> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found. + +3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: + +```sh +cd tanstack.com +pnpm i +# The app will run on https://localhost:3000 by default +pnpm dev +``` + +4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`. + +> [!NOTE] +> The updated pages need to be manually reloaded in the browser. + +> [!WARNING] +> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page! diff --git a/examples/react/start-basic-static/app.config.ts b/examples/react/start-basic-static/app.config.ts new file mode 100644 index 0000000000..78c6c550a6 --- /dev/null +++ b/examples/react/start-basic-static/app.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@tanstack/start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + vite: { + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + ], + }, + server: { + preset: 'netlify', + prerender: { + routes: ['/'], + crawlLinks: true, + }, + }, +}) diff --git a/examples/react/start-basic-static/app/client.tsx b/examples/react/start-basic-static/app/client.tsx new file mode 100644 index 0000000000..b14d8aac68 --- /dev/null +++ b/examples/react/start-basic-static/app/client.tsx @@ -0,0 +1,8 @@ +/// +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/start' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document, ) diff --git a/examples/react/start-basic-static/app/components/DefaultCatchBoundary.tsx b/examples/react/start-basic-static/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000000..f750e7bd2b --- /dev/null +++ b/examples/react/start-basic-static/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error('DefaultCatchBoundary Error:', error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/examples/react/start-basic-static/app/components/NotFound.tsx b/examples/react/start-basic-static/app/components/NotFound.tsx new file mode 100644 index 0000000000..7b54fa5680 --- /dev/null +++ b/examples/react/start-basic-static/app/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/react-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/examples/react/start-basic-static/app/routeTree.gen.ts b/examples/react/start-basic-static/app/routeTree.gen.ts new file mode 100644 index 0000000000..9f7ab8107b --- /dev/null +++ b/examples/react/start-basic-static/app/routeTree.gen.ts @@ -0,0 +1,470 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as UsersImport } from './routes/users' +import { Route as RedirectImport } from './routes/redirect' +import { Route as PostsImport } from './routes/posts' +import { Route as DeferredImport } from './routes/deferred' +import { Route as LayoutImport } from './routes/_layout' +import { Route as IndexImport } from './routes/index' +import { Route as UsersIndexImport } from './routes/users.index' +import { Route as PostsIndexImport } from './routes/posts.index' +import { Route as UsersUserIdImport } from './routes/users.$userId' +import { Route as PostsPostIdImport } from './routes/posts.$postId' +import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as PostsPostIdDeepImport } from './routes/posts_.$postId.deep' +import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b' +import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a' + +// Create/Update Routes + +const UsersRoute = UsersImport.update({ + id: '/users', + path: '/users', + getParentRoute: () => rootRoute, +} as any) + +const RedirectRoute = RedirectImport.update({ + id: '/redirect', + path: '/redirect', + getParentRoute: () => rootRoute, +} as any) + +const PostsRoute = PostsImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRoute, +} as any) + +const DeferredRoute = DeferredImport.update({ + id: '/deferred', + path: '/deferred', + getParentRoute: () => rootRoute, +} as any) + +const LayoutRoute = LayoutImport.update({ + id: '/_layout', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const UsersIndexRoute = UsersIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => UsersRoute, +} as any) + +const PostsIndexRoute = PostsIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => PostsRoute, +} as any) + +const UsersUserIdRoute = UsersUserIdImport.update({ + id: '/$userId', + path: '/$userId', + getParentRoute: () => UsersRoute, +} as any) + +const PostsPostIdRoute = PostsPostIdImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => PostsRoute, +} as any) + +const LayoutLayout2Route = LayoutLayout2Import.update({ + id: '/_layout-2', + getParentRoute: () => LayoutRoute, +} as any) + +const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({ + id: '/posts_/$postId/deep', + path: '/posts/$postId/deep', + getParentRoute: () => rootRoute, +} as any) + +const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({ + id: '/layout-b', + path: '/layout-b', + getParentRoute: () => LayoutLayout2Route, +} as any) + +const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({ + id: '/layout-a', + path: '/layout-a', + getParentRoute: () => LayoutLayout2Route, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutImport + parentRoute: typeof rootRoute + } + '/deferred': { + id: '/deferred' + path: '/deferred' + fullPath: '/deferred' + preLoaderRoute: typeof DeferredImport + parentRoute: typeof rootRoute + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsImport + parentRoute: typeof rootRoute + } + '/redirect': { + id: '/redirect' + path: '/redirect' + fullPath: '/redirect' + preLoaderRoute: typeof RedirectImport + parentRoute: typeof rootRoute + } + '/users': { + id: '/users' + path: '/users' + fullPath: '/users' + preLoaderRoute: typeof UsersImport + parentRoute: typeof rootRoute + } + '/_layout/_layout-2': { + id: '/_layout/_layout-2' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutLayout2Import + parentRoute: typeof LayoutImport + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdImport + parentRoute: typeof PostsImport + } + '/users/$userId': { + id: '/users/$userId' + path: '/$userId' + fullPath: '/users/$userId' + preLoaderRoute: typeof UsersUserIdImport + parentRoute: typeof UsersImport + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexImport + parentRoute: typeof PostsImport + } + '/users/': { + id: '/users/' + path: '/' + fullPath: '/users/' + preLoaderRoute: typeof UsersIndexImport + parentRoute: typeof UsersImport + } + '/_layout/_layout-2/layout-a': { + id: '/_layout/_layout-2/layout-a' + path: '/layout-a' + fullPath: '/layout-a' + preLoaderRoute: typeof LayoutLayout2LayoutAImport + parentRoute: typeof LayoutLayout2Import + } + '/_layout/_layout-2/layout-b': { + id: '/_layout/_layout-2/layout-b' + path: '/layout-b' + fullPath: '/layout-b' + preLoaderRoute: typeof LayoutLayout2LayoutBImport + parentRoute: typeof LayoutLayout2Import + } + '/posts_/$postId/deep': { + id: '/posts_/$postId/deep' + path: '/posts/$postId/deep' + fullPath: '/posts/$postId/deep' + preLoaderRoute: typeof PostsPostIdDeepImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +interface LayoutLayout2RouteChildren { + LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute + LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute +} + +const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = { + LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute, + LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute, +} + +const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren( + LayoutLayout2RouteChildren, +) + +interface LayoutRouteChildren { + LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutLayout2Route: LayoutLayout2RouteWithChildren, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface PostsRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteChildren: PostsRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) + +interface UsersRouteChildren { + UsersUserIdRoute: typeof UsersUserIdRoute + UsersIndexRoute: typeof UsersIndexRoute +} + +const UsersRouteChildren: UsersRouteChildren = { + UsersUserIdRoute: UsersUserIdRoute, + UsersIndexRoute: UsersIndexRoute, +} + +const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '': typeof LayoutLayout2RouteWithChildren + '/deferred': typeof DeferredRoute + '/posts': typeof PostsRouteWithChildren + '/redirect': typeof RedirectRoute + '/users': typeof UsersRouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/users/$userId': typeof UsersUserIdRoute + '/posts/': typeof PostsIndexRoute + '/users/': typeof UsersIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts/$postId/deep': typeof PostsPostIdDeepRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute + '': typeof LayoutLayout2RouteWithChildren + '/deferred': typeof DeferredRoute + '/redirect': typeof RedirectRoute + '/posts/$postId': typeof PostsPostIdRoute + '/users/$userId': typeof UsersUserIdRoute + '/posts': typeof PostsIndexRoute + '/users': typeof UsersIndexRoute + '/layout-a': typeof LayoutLayout2LayoutARoute + '/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts/$postId/deep': typeof PostsPostIdDeepRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/deferred': typeof DeferredRoute + '/posts': typeof PostsRouteWithChildren + '/redirect': typeof RedirectRoute + '/users': typeof UsersRouteWithChildren + '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/posts/$postId': typeof PostsPostIdRoute + '/users/$userId': typeof UsersUserIdRoute + '/posts/': typeof PostsIndexRoute + '/users/': typeof UsersIndexRoute + '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute + '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute + '/posts_/$postId/deep': typeof PostsPostIdDeepRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '' + | '/deferred' + | '/posts' + | '/redirect' + | '/users' + | '/posts/$postId' + | '/users/$userId' + | '/posts/' + | '/users/' + | '/layout-a' + | '/layout-b' + | '/posts/$postId/deep' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '' + | '/deferred' + | '/redirect' + | '/posts/$postId' + | '/users/$userId' + | '/posts' + | '/users' + | '/layout-a' + | '/layout-b' + | '/posts/$postId/deep' + id: + | '__root__' + | '/' + | '/_layout' + | '/deferred' + | '/posts' + | '/redirect' + | '/users' + | '/_layout/_layout-2' + | '/posts/$postId' + | '/users/$userId' + | '/posts/' + | '/users/' + | '/_layout/_layout-2/layout-a' + | '/_layout/_layout-2/layout-b' + | '/posts_/$postId/deep' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + DeferredRoute: typeof DeferredRoute + PostsRoute: typeof PostsRouteWithChildren + RedirectRoute: typeof RedirectRoute + UsersRoute: typeof UsersRouteWithChildren + PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + DeferredRoute: DeferredRoute, + PostsRoute: PostsRouteWithChildren, + RedirectRoute: RedirectRoute, + UsersRoute: UsersRouteWithChildren, + PostsPostIdDeepRoute: PostsPostIdDeepRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/_layout", + "/deferred", + "/posts", + "/redirect", + "/users", + "/posts_/$postId/deep" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/_layout": { + "filePath": "_layout.tsx", + "children": [ + "/_layout/_layout-2" + ] + }, + "/deferred": { + "filePath": "deferred.tsx" + }, + "/posts": { + "filePath": "posts.tsx", + "children": [ + "/posts/$postId", + "/posts/" + ] + }, + "/redirect": { + "filePath": "redirect.tsx" + }, + "/users": { + "filePath": "users.tsx", + "children": [ + "/users/$userId", + "/users/" + ] + }, + "/_layout/_layout-2": { + "filePath": "_layout/_layout-2.tsx", + "parent": "/_layout", + "children": [ + "/_layout/_layout-2/layout-a", + "/_layout/_layout-2/layout-b" + ] + }, + "/posts/$postId": { + "filePath": "posts.$postId.tsx", + "parent": "/posts" + }, + "/users/$userId": { + "filePath": "users.$userId.tsx", + "parent": "/users" + }, + "/posts/": { + "filePath": "posts.index.tsx", + "parent": "/posts" + }, + "/users/": { + "filePath": "users.index.tsx", + "parent": "/users" + }, + "/_layout/_layout-2/layout-a": { + "filePath": "_layout/_layout-2/layout-a.tsx", + "parent": "/_layout/_layout-2" + }, + "/_layout/_layout-2/layout-b": { + "filePath": "_layout/_layout-2/layout-b.tsx", + "parent": "/_layout/_layout-2" + }, + "/posts_/$postId/deep": { + "filePath": "posts_.$postId.deep.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/examples/react/start-basic-static/app/router.tsx b/examples/react/start-basic-static/app/router.tsx new file mode 100644 index 0000000000..0886de701f --- /dev/null +++ b/examples/react/start-basic-static/app/router.tsx @@ -0,0 +1,21 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + }) + + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/examples/react/start-basic-static/app/routes/__root.tsx b/examples/react/start-basic-static/app/routes/__root.tsx new file mode 100644 index 0000000000..a2376f8364 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/__root.tsx @@ -0,0 +1,140 @@ +import { + Link, + Outlet, + ScrollRestoration, + createRootRoute, +} from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { Meta, Scripts } from '@tanstack/start' +import * as React from 'react' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: + 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework', + description: `TanStack Start is a type-safe, client-first, full-stack React framework. `, + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: (props) => { + return ( + + + + ) + }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + {' '} + + Posts + {' '} + + Users + {' '} + + Layout + {' '} + + Deferred + {' '} + + This Route Does Not Exist + +
+
+ {children} + + + + + + ) +} diff --git a/examples/react/start-basic-static/app/routes/_layout.tsx b/examples/react/start-basic-static/app/routes/_layout.tsx new file mode 100644 index 0000000000..02ddbb1cd9 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/_layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/examples/react/start-basic-static/app/routes/_layout/_layout-2.tsx b/examples/react/start-basic-static/app/routes/_layout/_layout-2.tsx new file mode 100644 index 0000000000..3b7dbf2903 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/_layout/_layout-2.tsx @@ -0,0 +1,34 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/_layout-2')({ + component: LayoutComponent, +}) + +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Layout A + + + Layout B + +
+
+ +
+
+ ) +} diff --git a/examples/react/start-basic-static/app/routes/_layout/_layout-2/layout-a.tsx b/examples/react/start-basic-static/app/routes/_layout/_layout-2/layout-a.tsx new file mode 100644 index 0000000000..61e19b4d9f --- /dev/null +++ b/examples/react/start-basic-static/app/routes/_layout/_layout-2/layout-a.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-a')({ + component: LayoutAComponent, +}) + +function LayoutAComponent() { + return
I'm layout A!
+} diff --git a/examples/react/start-basic-static/app/routes/_layout/_layout-2/layout-b.tsx b/examples/react/start-basic-static/app/routes/_layout/_layout-2/layout-b.tsx new file mode 100644 index 0000000000..cceed1fb9a --- /dev/null +++ b/examples/react/start-basic-static/app/routes/_layout/_layout-2/layout-b.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/_layout-2/layout-b')({ + component: LayoutBComponent, +}) + +function LayoutBComponent() { + return
I'm layout B!
+} diff --git a/examples/react/start-basic-static/app/routes/deferred.tsx b/examples/react/start-basic-static/app/routes/deferred.tsx new file mode 100644 index 0000000000..dd5511f692 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/deferred.tsx @@ -0,0 +1,62 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/start' +import { Suspense, useState } from 'react' + +const personServerFn = createServerFn({ method: 'GET', type: 'static' }) + .validator((d: string) => d) + .handler(({ data: name }) => { + return { name, randomNumber: Math.floor(Math.random() * 100) } + }) + +const slowServerFn = createServerFn({ method: 'GET', type: 'static' }) + .validator((d: string) => d) + .handler(async ({ data: name }) => { + await new Promise((r) => setTimeout(r, 1000)) + return { name, randomNumber: Math.floor(Math.random() * 100) } + }) + +export const Route = createFileRoute('/deferred')({ + loader: async () => { + return { + deferredStuff: new Promise((r) => + setTimeout(() => r('Hello deferred!'), 2000), + ), + deferredPerson: slowServerFn({ data: 'Tanner Linsley' }), + person: await personServerFn({ data: 'John Doe' }), + } + }, + component: Deferred, +}) + +function Deferred() { + const [count, setCount] = useState(0) + const { deferredStuff, deferredPerson, person } = Route.useLoaderData() + + return ( +
+
+ {person.name} - {person.randomNumber} +
+ Loading person...
}> + ( +
+ {data.name} - {data.randomNumber} +
+ )} + /> + + Loading stuff...}> +

{data}

} + /> +
+
Count: {count}
+
+ +
+ + ) +} diff --git a/examples/react/start-basic-static/app/routes/index.tsx b/examples/react/start-basic-static/app/routes/index.tsx new file mode 100644 index 0000000000..09a907cb18 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome Home!!!

+
+ ) +} diff --git a/examples/react/start-basic-static/app/routes/posts.$postId.tsx b/examples/react/start-basic-static/app/routes/posts.$postId.tsx new file mode 100644 index 0000000000..0d4d2de8eb --- /dev/null +++ b/examples/react/start-basic-static/app/routes/posts.$postId.tsx @@ -0,0 +1,38 @@ +import { ErrorComponent, Link, createFileRoute } from '@tanstack/react-router' +import { fetchPost } from '../utils/posts' +import type { ErrorComponentProps } from '@tanstack/react-router' +import { NotFound } from '~/components/NotFound' + +export const Route = createFileRoute('/posts/$postId')({ + loader: ({ params: { postId } }) => fetchPost({ data: postId }), + errorComponent: PostErrorComponent, + component: PostComponent, + notFoundComponent: () => { + return Post not found + }, +}) + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post.title}

+
{post.body}
+ + Deep View + +
+ ) +} diff --git a/examples/react/start-basic-static/app/routes/posts.index.tsx b/examples/react/start-basic-static/app/routes/posts.index.tsx new file mode 100644 index 0000000000..5b5f08f95b --- /dev/null +++ b/examples/react/start-basic-static/app/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/examples/react/start-basic-static/app/routes/posts.tsx b/examples/react/start-basic-static/app/routes/posts.tsx new file mode 100644 index 0000000000..ae49032459 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '../utils/posts' + +export const Route = createFileRoute('/posts')({ + loader: async () => fetchPosts(), + component: PostsComponent, +}) + +function PostsComponent() { + 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/react/start-basic-static/app/routes/posts_.$postId.deep.tsx b/examples/react/start-basic-static/app/routes/posts_.$postId.deep.tsx new file mode 100644 index 0000000000..a82d2e3211 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/posts_.$postId.deep.tsx @@ -0,0 +1,29 @@ +import { Link, createFileRoute } from '@tanstack/react-router' +import { fetchPost } from '../utils/posts' +import { PostErrorComponent } from './posts.$postId' + +export const Route = createFileRoute('/posts_/$postId/deep')({ + loader: ({ params: { postId } }) => + fetchPost({ + data: postId, + }), + errorComponent: PostErrorComponent, + component: PostDeepComponent, +}) + +function PostDeepComponent() { + const post = Route.useLoaderData() + + return ( +
+ + ← All Posts + +

{post.title}

+
{post.body}
+
+ ) +} diff --git a/examples/react/start-basic-static/app/routes/redirect.tsx b/examples/react/start-basic-static/app/routes/redirect.tsx new file mode 100644 index 0000000000..c9286de13d --- /dev/null +++ b/examples/react/start-basic-static/app/routes/redirect.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect')({ + beforeLoad: async () => { + throw redirect({ + to: '/posts', + }) + }, +}) diff --git a/examples/react/start-basic-static/app/routes/users.$userId.tsx b/examples/react/start-basic-static/app/routes/users.$userId.tsx new file mode 100644 index 0000000000..1a797d0978 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/users.$userId.tsx @@ -0,0 +1,55 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/react-router' +import axios from 'redaxios' +import { setResponseStatus } from 'vinxi/http' +import { createServerFn } from '@tanstack/start' +import type { ErrorComponentProps } from '@tanstack/react-router' +import type { User } from '~/utils/users' +import { NotFound } from '~/components/NotFound' + +const fetchUser = createServerFn({ method: 'GET' }) + .validator((d: string) => d) + .type(({ data }) => (+data % 2 === 0 ? 'static' : 'dynamic')) + .handler(async ({ data: userId }) => { + try { + return axios + .get('https://jsonplaceholder.typicode.com/users/' + userId) + .then((d) => ({ + id: d.data.id, + name: d.data.name, + email: d.data.email, + })) + } catch (e) { + console.error(e) + setResponseStatus(404) + + return { error: 'User not found' } + } + }) + +export const Route = createFileRoute('/users/$userId')({ + loader: ({ params: { userId } }) => fetchUser({ data: userId }), + errorComponent: UserErrorComponent, + component: UserComponent, + notFoundComponent: () => { + return User not found + }, +}) + +export function UserErrorComponent({ error }: ErrorComponentProps) { + return +} + +function UserComponent() { + const user = Route.useLoaderData() + + if ('error' in user) { + return User not found + } + + return ( +
+

{user.name}

+
{user.email}
+
+ ) +} diff --git a/examples/react/start-basic-static/app/routes/users.index.tsx b/examples/react/start-basic-static/app/routes/users.index.tsx new file mode 100644 index 0000000000..b6b0ee67fb --- /dev/null +++ b/examples/react/start-basic-static/app/routes/users.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/users/')({ + component: UsersIndexComponent, +}) + +function UsersIndexComponent() { + return
Select a user.
+} diff --git a/examples/react/start-basic-static/app/routes/users.tsx b/examples/react/start-basic-static/app/routes/users.tsx new file mode 100644 index 0000000000..2ebf718a75 --- /dev/null +++ b/examples/react/start-basic-static/app/routes/users.tsx @@ -0,0 +1,54 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import axios from 'redaxios' +import { createServerFn } from '@tanstack/start' +import type { User } from '../utils/users' + +const fetchUsers = createServerFn({ method: 'GET', type: 'static' }).handler( + async () => { + console.info('Fetching users...') + const res = await axios.get>( + 'https://jsonplaceholder.typicode.com/users', + ) + + return res.data + .slice(0, 10) + .map((u) => ({ id: u.id, name: u.name, email: u.email })) + }, +) + +export const Route = createFileRoute('/users')({ + loader: async () => fetchUsers(), + component: UsersComponent, +}) + +function UsersComponent() { + const users = Route.useLoaderData() + + return ( +
+
    + {[ + ...users, + { id: 'i-do-not-exist', name: 'Non-existent User', email: '' }, + ].map((user) => { + return ( +
  • + +
    {user.name}
    + +
  • + ) + })} +
+
+ +
+ ) +} diff --git a/examples/react/start-basic-static/app/ssr.tsx b/examples/react/start-basic-static/app/ssr.tsx new file mode 100644 index 0000000000..f2d33f9030 --- /dev/null +++ b/examples/react/start-basic-static/app/ssr.tsx @@ -0,0 +1,13 @@ +/// +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/start/server' +import { getRouterManifest } from '@tanstack/start/router-manifest' + +import { createRouter } from './router' + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler) diff --git a/examples/react/start-basic-static/app/styles/app.css b/examples/react/start-basic-static/app/styles/app.css new file mode 100644 index 0000000000..d6426ccb72 --- /dev/null +++ b/examples/react/start-basic-static/app/styles/app.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/examples/react/start-basic-static/app/utils/loggingMiddleware.tsx b/examples/react/start-basic-static/app/utils/loggingMiddleware.tsx new file mode 100644 index 0000000000..52c90185d0 --- /dev/null +++ b/examples/react/start-basic-static/app/utils/loggingMiddleware.tsx @@ -0,0 +1,37 @@ +import { createMiddleware } from '@tanstack/start' + +export const logMiddleware = createMiddleware() + .client(async (ctx) => { + const clientTime = new Date() + + return ctx.next({ + context: { + clientTime, + }, + sendContext: { + clientTime, + }, + }) + }) + .server(async (ctx) => { + const serverTime = new Date() + + return ctx.next({ + sendContext: { + serverTime, + durationToServer: + serverTime.getTime() - ctx.context.clientTime.getTime(), + }, + }) + }) + .clientAfter(async (ctx) => { + const now = new Date() + + console.log('Client Req/Res:', { + duration: ctx.context.clientTime.getTime() - now.getTime(), + durationToServer: ctx.context.durationToServer, + durationFromServer: now.getTime() - ctx.context.serverTime.getTime(), + }) + + return ctx.next() + }) diff --git a/examples/react/start-basic-static/app/utils/posts.tsx b/examples/react/start-basic-static/app/utils/posts.tsx new file mode 100644 index 0000000000..050ec0d86c --- /dev/null +++ b/examples/react/start-basic-static/app/utils/posts.tsx @@ -0,0 +1,37 @@ +import { notFound } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/start' +import axios from 'redaxios' +import { logMiddleware } from './loggingMiddleware' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = createServerFn({ method: 'GET', type: 'static' }) + .middleware([logMiddleware]) + .validator((d: string) => d) + .handler(async ({ data }) => { + console.info(`Fetching post with id ${data}...`) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${data}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post + }) + +export const fetchPosts = createServerFn({ method: 'GET', type: 'static' }) + .middleware([logMiddleware]) + .handler(async () => { + console.info('Fetching posts...') + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) + }) diff --git a/examples/react/start-basic-static/app/utils/seo.ts b/examples/react/start-basic-static/app/utils/seo.ts new file mode 100644 index 0000000000..d18ad84b74 --- /dev/null +++ b/examples/react/start-basic-static/app/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/examples/react/start-basic-static/app/utils/users.tsx b/examples/react/start-basic-static/app/utils/users.tsx new file mode 100644 index 0000000000..b810f455fe --- /dev/null +++ b/examples/react/start-basic-static/app/utils/users.tsx @@ -0,0 +1,7 @@ +export type User = { + id: number + name: string + email: string +} + +export const DEPLOY_URL = 'http://localhost:3000' diff --git a/examples/react/start-basic-static/package.json b/examples/react/start-basic-static/package.json new file mode 100644 index 0000000000..78aa67a6e4 --- /dev/null +++ b/examples/react/start-basic-static/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-start-example-basic-static", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@tanstack/react-router": "^1.84.4", + "@tanstack/router-devtools": "^1.84.4", + "@tanstack/start": "^1.84.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.5.5", + "vinxi": "0.4.3" + }, + "devDependencies": { + "@types/node": "^22.5.4", + "@types/react": "^18.2.65", + "@types/react-dom": "^18.2.21", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.15", + "typescript": "^5.6.2", + "vite-tsconfig-paths": "^5.1.3" + } +} diff --git a/examples/react/start-basic-static/postcss.config.cjs b/examples/react/start-basic-static/postcss.config.cjs new file mode 100644 index 0000000000..8e638a6bcd --- /dev/null +++ b/examples/react/start-basic-static/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require('tailwindcss/nesting'), + require('tailwindcss'), + require('autoprefixer'), + ], +} diff --git a/examples/react/start-basic-static/public/android-chrome-192x192.png b/examples/react/start-basic-static/public/android-chrome-192x192.png new file mode 100644 index 0000000000..09c8324f8c Binary files /dev/null and b/examples/react/start-basic-static/public/android-chrome-192x192.png differ diff --git a/examples/react/start-basic-static/public/android-chrome-512x512.png b/examples/react/start-basic-static/public/android-chrome-512x512.png new file mode 100644 index 0000000000..11d626ea3d Binary files /dev/null and b/examples/react/start-basic-static/public/android-chrome-512x512.png differ diff --git a/examples/react/start-basic-static/public/apple-touch-icon.png b/examples/react/start-basic-static/public/apple-touch-icon.png new file mode 100644 index 0000000000..5a9423cc02 Binary files /dev/null and b/examples/react/start-basic-static/public/apple-touch-icon.png differ diff --git a/examples/react/start-basic-static/public/favicon-16x16.png b/examples/react/start-basic-static/public/favicon-16x16.png new file mode 100644 index 0000000000..e3389b0044 Binary files /dev/null and b/examples/react/start-basic-static/public/favicon-16x16.png differ diff --git a/examples/react/start-basic-static/public/favicon-32x32.png b/examples/react/start-basic-static/public/favicon-32x32.png new file mode 100644 index 0000000000..900c77d444 Binary files /dev/null and b/examples/react/start-basic-static/public/favicon-32x32.png differ diff --git a/examples/react/start-basic-static/public/favicon.ico b/examples/react/start-basic-static/public/favicon.ico new file mode 100644 index 0000000000..1a1751676f Binary files /dev/null and b/examples/react/start-basic-static/public/favicon.ico differ diff --git a/examples/react/start-basic-static/public/favicon.png b/examples/react/start-basic-static/public/favicon.png new file mode 100644 index 0000000000..1e77bc0609 Binary files /dev/null and b/examples/react/start-basic-static/public/favicon.png differ diff --git a/examples/react/start-basic-static/public/site.webmanifest b/examples/react/start-basic-static/public/site.webmanifest new file mode 100644 index 0000000000..fa99de77db --- /dev/null +++ b/examples/react/start-basic-static/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/examples/react/start-basic-static/tailwind.config.cjs b/examples/react/start-basic-static/tailwind.config.cjs new file mode 100644 index 0000000000..75fe25dbf7 --- /dev/null +++ b/examples/react/start-basic-static/tailwind.config.cjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./app/**/*.{js,ts,jsx,tsx}'], +} diff --git a/examples/react/start-basic-static/tsconfig.json b/examples/react/start-basic-static/tsconfig.json new file mode 100644 index 0000000000..d1b5b77660 --- /dev/null +++ b/examples/react/start-basic-static/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true + } +} diff --git a/examples/react/start-basic/app/routes/deferred.tsx b/examples/react/start-basic/app/routes/deferred.tsx index 7cc7f8b9b5..606ab69b80 100644 --- a/examples/react/start-basic/app/routes/deferred.tsx +++ b/examples/react/start-basic/app/routes/deferred.tsx @@ -3,13 +3,13 @@ import { createServerFn } from '@tanstack/start' import { Suspense, useState } from 'react' const personServerFn = createServerFn({ method: 'GET' }) - .validator((d) => d as string) + .validator((d: string) => d) .handler(({ data: name }) => { return { name, randomNumber: Math.floor(Math.random() * 100) } }) const slowServerFn = createServerFn({ method: 'GET' }) - .validator((d) => d as string) + .validator((d: string) => d) .handler(async ({ data: name }) => { await new Promise((r) => setTimeout(r, 1000)) return { name, randomNumber: Math.floor(Math.random() * 100) } diff --git a/examples/react/start-basic/app/utils/posts.tsx b/examples/react/start-basic/app/utils/posts.tsx index 196498f8c9..99e43f5197 100644 --- a/examples/react/start-basic/app/utils/posts.tsx +++ b/examples/react/start-basic/app/utils/posts.tsx @@ -11,7 +11,7 @@ export type PostType = { export const fetchPost = createServerFn({ method: 'GET' }) .middleware([logMiddleware]) - .validator((d) => d as string) + .validator((d: string) => d) .handler(async ({ data }) => { console.info(`Fetching post with id ${data}...`) const post = await axios diff --git a/examples/react/start-supabase-basic/app/utils/posts.ts b/examples/react/start-supabase-basic/app/utils/posts.ts index 3c8b8d2434..6eb213af1f 100644 --- a/examples/react/start-supabase-basic/app/utils/posts.ts +++ b/examples/react/start-supabase-basic/app/utils/posts.ts @@ -9,7 +9,7 @@ export type PostType = { } export const fetchPost = createServerFn({ method: 'GET' }) - .validator((d) => d as string) + .validator((d: string) => d) .handler(async ({ data: postId }) => { console.info(`Fetching post with id ${postId}...`) const post = await axios diff --git a/examples/react/with-framer-motion/src/main.tsx b/examples/react/with-framer-motion/src/main.tsx index 3ce3a89ea1..e636ddca43 100644 --- a/examples/react/with-framer-motion/src/main.tsx +++ b/examples/react/with-framer-motion/src/main.tsx @@ -35,7 +35,6 @@ const fetchPost = async (postId: string) => { const post = await axios .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) .then((r) => r.data) - .catch(console.log) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!post) { diff --git a/packages/start/src/client-runtime/fetcher.tsx b/packages/start/src/client-runtime/fetcher.tsx index 70eea4b9ae..ce00aab5a5 100644 --- a/packages/start/src/client-runtime/fetcher.tsx +++ b/packages/start/src/client-runtime/fetcher.tsx @@ -5,7 +5,7 @@ import { isPlainObject, isRedirect, } from '@tanstack/react-router' -import type { MiddlewareOptions } from '../client/createServerFn' +import type { MiddlewareCtx } from '../client/createServerFn' export async function fetcher( base: string, @@ -17,7 +17,7 @@ export async function fetcher( // If createServerFn was used to wrap the fetcher, // We need to handle the arguments differently if (isPlainObject(_first) && _first.method) { - const first = _first as MiddlewareOptions + const first = _first as MiddlewareCtx const type = first.data instanceof FormData ? 'formData' : 'payload' // Arrange the headers @@ -30,7 +30,7 @@ export async function fetcher( : {}), ...(first.headers instanceof Headers ? Object.fromEntries(first.headers.entries()) - : first.headers || {}), + : first.headers), }) // If the method is GET, we need to move the payload to the query string @@ -40,6 +40,7 @@ export async function fetcher( payload: defaultTransformer.stringify({ data: first.data, context: first.context, + ...(typeof first.type !== 'function' && { type: first.type }), }), }) @@ -101,7 +102,7 @@ export async function fetcher( } } -function getFetcherRequestOptions(opts: MiddlewareOptions) { +function getFetcherRequestOptions(opts: MiddlewareCtx) { if (opts.method === 'POST') { if (opts.data instanceof FormData) { opts.data.set('__TSR_CONTEXT', defaultTransformer.stringify(opts.context)) diff --git a/packages/start/src/client/createMiddleware.ts b/packages/start/src/client/createMiddleware.ts index 6eefb7751f..762310b130 100644 --- a/packages/start/src/client/createMiddleware.ts +++ b/packages/start/src/client/createMiddleware.ts @@ -1,4 +1,8 @@ -import type { ConstrainValidator, Method } from './createServerFn' +import type { + ConstrainValidator, + Method, + ServerFnTypeOrTypeFn, +} from './createServerFn' import type { Assign, Constrain, @@ -156,6 +160,7 @@ export interface MiddlewareClientFnOptions< sendContext?: unknown // cc Chris Horobin method: Method next: MiddlewareClientNextFn + type: ServerFnTypeOrTypeFn } export type MiddlewareClientFn< diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index 6408e783cd..a772fb4676 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -1,5 +1,4 @@ -import invariant from 'tiny-invariant' -import { defaultTransformer } from '@tanstack/react-router' +import { defaultTransformer, invariant, warning } from '@tanstack/react-router' import { mergeHeaders } from './headers' import { globalMiddleware } from './registerGlobalMiddleware' import type { @@ -53,8 +52,11 @@ export type FetcherImpl = export type FetcherBaseOptions = { headers?: HeadersInit + type?: ServerFnType } +export type ServerFnType = 'static' | 'dynamic' + export interface RequiredFetcherDataOptions extends FetcherBaseOptions { data: TInput } @@ -86,11 +88,11 @@ export interface ServerFnCtx { } export type CompiledFetcherFn = { - (opts: CompiledFetcherFnOptions & ServerFnBaseOptions): Promise + (opts: CompiledFetcherFnOptions & ServerFnInternalOptions): Promise url: string } -type ServerFnBaseOptions< +type ServerFnInternalOptions< TMethod extends Method = 'GET', TResponse = unknown, TMiddlewares = unknown, @@ -104,6 +106,7 @@ type ServerFnBaseOptions< serverFn?: ServerFn filename: string functionId: string + type: ServerFnTypeOrTypeFn } export type ConstrainValidator = unknown extends TValidator @@ -119,6 +122,22 @@ export type ConstrainValidator = unknown extends TValidator > > +// Validator +export interface ServerFnValidator { + validator: ( + validator: ConstrainValidator, + ) => ServerFnAfterValidator +} + +export interface ServerFnAfterValidator< + TMethod extends Method, + TMiddlewares, + TValidator, +> extends ServerFnMiddleware, + ServerFnTyper, + ServerFnHandler {} + +// Middleware export interface ServerFnMiddleware { middleware: ( middlewares: Constrain>, @@ -130,21 +149,35 @@ export interface ServerFnAfterMiddleware< TMiddlewares, TValidator, > extends ServerFnValidator, + ServerFnTyper, ServerFnHandler {} -export interface ServerFnValidator { - validator: ( - validator: ConstrainValidator, - ) => ServerFnAfterValidator +// Typer +export interface ServerFnTyper< + TMethod extends Method, + TMiddlewares, + TValidator, +> { + type: ( + typer: ServerFnTypeOrTypeFn, + ) => ServerFnAfterTyper } -export interface ServerFnAfterValidator< +export type ServerFnTypeOrTypeFn< TMethod extends Method, TMiddlewares, TValidator, -> extends ServerFnMiddleware, - ServerFnHandler {} +> = + | ServerFnType + | ((ctx: ServerFnCtx) => ServerFnType) +export interface ServerFnAfterTyper< + TMethod extends Method, + TMiddlewares, + TValidator, +> extends ServerFnHandler {} + +// Handler export interface ServerFnHandler< TMethod extends Method, TMiddlewares, @@ -160,12 +193,145 @@ export interface ServerFnBuilder< TResponse = unknown, TMiddlewares = unknown, TValidator = unknown, -> extends ServerFnMiddleware, - ServerFnValidator, +> extends ServerFnValidator, + ServerFnMiddleware, + ServerFnTyper, ServerFnHandler { - options: ServerFnBaseOptions + options: ServerFnInternalOptions +} + +type StaticCachedResult = { + ctx?: { + result: any + context: any + } + error?: any +} + +export type ServerFnStaticCache = { + getItem: ( + ctx: MiddlewareCtx, + ) => StaticCachedResult | Promise + setItem: (ctx: MiddlewareCtx, response: StaticCachedResult) => Promise + fetchItem: ( + ctx: MiddlewareCtx, + ) => StaticCachedResult | Promise } +let serverFnStaticCache: ServerFnStaticCache | undefined + +export function setServerFnStaticCache( + cache?: ServerFnStaticCache | (() => ServerFnStaticCache | undefined), +) { + const previousCache = serverFnStaticCache + serverFnStaticCache = typeof cache === 'function' ? cache() : cache + + return () => { + serverFnStaticCache = previousCache + } +} + +export function createServerFnStaticCache( + serverFnStaticCache: ServerFnStaticCache, +) { + return serverFnStaticCache +} + +setServerFnStaticCache(() => { + const getStaticCacheUrl = (options: MiddlewareCtx, hash: string) => { + return `/__tsr/staticServerFnCache/${options.filename}__${options.functionId}__${hash}.json` + } + + const jsonToFilenameSafeString = (json: any) => { + // Custom replacer to sort keys + const sortedKeysReplacer = (key: string, value: any) => + value && typeof value === 'object' && !Array.isArray(value) + ? Object.keys(value) + .sort() + .reduce((acc: any, curr: string) => { + acc[curr] = value[curr] + return acc + }, {}) + : value + + // Convert JSON to string with sorted keys + const jsonString = JSON.stringify(json ?? '', sortedKeysReplacer) + + // Replace characters invalid in filenames + return jsonString + .replace(/[/\\?%*:|"<>]/g, '-') // Replace invalid characters with a dash + .replace(/\s+/g, '_') // Optionally replace whitespace with underscores + } + + const staticClientCache = + typeof document !== 'undefined' ? new Map() : null + + return createServerFnStaticCache({ + getItem: async (ctx) => { + if (typeof document === 'undefined') { + const hash = jsonToFilenameSafeString(ctx.data) + const url = getStaticCacheUrl(ctx, hash) + const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR! + + // Use fs instead of fetch to read from filesystem + const fs = await import('node:fs/promises') + const path = await import('node:path') + const filePath = path.join(publicUrl, url) + + const [cachedResult, readError] = await fs + .readFile(filePath, 'utf-8') + .then((c) => [ + defaultTransformer.parse(c) as { + ctx: unknown + error: any + }, + null, + ]) + .catch((e) => [null, e]) + + if (readError && readError.code !== 'ENOENT') { + throw readError + } + + return cachedResult as StaticCachedResult + } + }, + setItem: async (ctx, response) => { + const fs = await import('node:fs/promises') + const path = await import('node:path') + + const hash = jsonToFilenameSafeString(ctx.data) + const url = getStaticCacheUrl(ctx, hash) + const publicUrl = process.env.TSS_OUTPUT_PUBLIC_DIR! + const filePath = path.join(publicUrl, url) + + // Ensure the directory exists + await fs.mkdir(path.dirname(filePath), { recursive: true }) + + // Store the result with fs + await fs.writeFile(filePath, defaultTransformer.stringify(response)) + }, + fetchItem: async (ctx) => { + const hash = jsonToFilenameSafeString(ctx.data) + const url = getStaticCacheUrl(ctx, hash) + + let result: any = staticClientCache?.get(url) + + if (!result) { + result = await fetch(url, { + method: 'GET', + }) + .then((r) => r.text()) + .then((d) => defaultTransformer.parse(d)) + + staticClientCache?.set(url, result) + } + + return result + }, + }) +}) + export function createServerFn< TMethod extends Method, TResponse = unknown, @@ -173,11 +339,16 @@ export function createServerFn< TValidator = undefined, >( options?: { - method: TMethod + method?: TMethod }, - __opts?: ServerFnBaseOptions, + __opts?: ServerFnInternalOptions< + TMethod, + TResponse, + TMiddlewares, + TValidator + >, ): ServerFnBuilder { - const resolvedOptions = (__opts || options || {}) as ServerFnBaseOptions< + const resolvedOptions = (__opts || options || {}) as ServerFnInternalOptions< TMethod, TResponse, TMiddlewares, @@ -202,6 +373,12 @@ export function createServerFn< Object.assign(resolvedOptions, { validator }), ) as any }, + type: (type) => { + return createServerFn( + undefined, + Object.assign(resolvedOptions, { type }), + ) as any + }, handler: (...args) => { // This function signature changes due to AST transformations // in the babel plugin. We need to cast it to the correct @@ -219,11 +396,6 @@ export function createServerFn< serverFn, }) - invariant( - extractedFn.url, - `createServerFn must be called with a function that is marked with the 'use server' pragma. Are you using the @tanstack/start-vite-plugin ?`, - ) - const resolvedMiddleware = [ ...(resolvedOptions.middleware || []), serverFnBaseToMiddleware(resolvedOptions), @@ -235,10 +407,8 @@ export function createServerFn< async (opts?: CompiledFetcherFnOptions) => { // Start by executing the client-side middleware chain return executeMiddleware(resolvedMiddleware, 'client', { - ...extractedFn, - method: resolvedOptions.method, - data: opts?.data as any, - headers: opts?.headers, + ...resolvedOptions, + ...(opts as any), context: Object.assign({}, extractedFn), }).then((d) => d.result) }, @@ -247,18 +417,76 @@ export function createServerFn< ...extractedFn, // The extracted function on the server-side calls // this function - __executeServer: (opts: any) => { - const parsedOpts = - opts instanceof FormData ? extractFormDataContext(opts) : opts - - return executeMiddleware(resolvedMiddleware, 'server', { - ...extractedFn, - ...parsedOpts, - }).then((d) => ({ - // Only send the result and sendContext back to the client - result: d.result, - context: d.sendContext, - })) + __executeServer: async (opts: any) => { + if (process.env.ROUTER !== 'client') { + const parsedOpts = + opts instanceof FormData ? extractFormDataContext(opts) : opts + + const ctx = { + ...resolvedOptions, + ...parsedOpts, + } + + ctx.type = + typeof resolvedOptions.type === 'function' + ? resolvedOptions.type(ctx) + : resolvedOptions.type + + const run = async () => + executeMiddleware(resolvedMiddleware, 'server', ctx).then( + (d) => ({ + // Only send the result and sendContext back to the client + result: d.result, + context: d.sendContext, + }), + ) + + if (ctx.type === 'static') { + let response: StaticCachedResult | undefined + + // If we can get the cached item, try to get it + if (serverFnStaticCache?.getItem) { + // If this throws, it's okay to let it bubble up + response = await serverFnStaticCache.getItem(ctx) + } + + if (!response) { + // If there's no cached item, execute the server function + response = await run() + .then((d) => { + return { + ctx: d, + error: null, + } + }) + .catch((e) => { + return { + ctx: undefined, + error: e, + } + }) + + if (serverFnStaticCache?.setItem) { + await serverFnStaticCache.setItem(ctx, response) + } + } + + invariant( + response, + 'No response from both server and static cache!', + ) + + if (response.error) { + throw response.error + } + + return response.ctx + } + + return run() + } + + return undefined }, }, ) as any @@ -283,7 +511,7 @@ function extractFormDataContext(formData: FormData) { context, data: formData, } - } catch (e) { + } catch { return { data: formData, } @@ -314,19 +542,16 @@ function flattenMiddlewares( return flattened } -export type MiddlewareOptions = { - method: Method - data: any - headers?: HeadersInit - sendContext?: any - context?: any -} - -export type MiddlewareResult = { +export type MiddlewareCtx = { context: any sendContext: any data: any result: unknown + method: Method + type: 'static' | 'dynamic' + headers: HeadersInit + filename: string + functionId: string } const applyMiddleware = ( @@ -335,14 +560,11 @@ const applyMiddleware = ( | AnyMiddleware['options']['server'] | AnyMiddleware['options']['clientAfter'] >, - mCtx: MiddlewareOptions, - nextFn: (ctx: MiddlewareOptions) => Promise, + mCtx: MiddlewareCtx, + nextFn: (ctx: MiddlewareCtx) => Promise, ) => { return middlewareFn({ - data: mCtx.data, - context: mCtx.context, - sendContext: mCtx.sendContext, - method: mCtx.method, + ...mCtx, next: ((userResult: any) => { // Take the user provided context // and merge it with the current context @@ -360,17 +582,14 @@ const applyMiddleware = ( // Return the next middleware return nextFn({ - method: mCtx.method, - data: mCtx.data, + ...mCtx, context, sendContext, headers, result: userResult?.result ?? (mCtx as any).result, - } as MiddlewareResult & { - method: Method }) }) as any, - }) + } as any) } function execValidator(validator: AnyValidator, input: unknown): unknown { @@ -402,14 +621,14 @@ function execValidator(validator: AnyValidator, input: unknown): unknown { async function executeMiddleware( middlewares: Array, env: 'client' | 'server', - opts: MiddlewareOptions, -): Promise { + ctx: Omit & { headers?: HeadersInit }, +): Promise { const flattenedMiddlewares = flattenMiddlewares([ ...globalMiddleware, ...middlewares, ]) - const next = async (ctx: MiddlewareOptions): Promise => { + const next = async (ctx: MiddlewareCtx): Promise => { // Get the next middleware const nextMiddleware = flattenedMiddlewares.shift() @@ -436,15 +655,15 @@ async function executeMiddleware( return applyMiddleware( middlewareFn, ctx, - async (userCtx): Promise => { + async (userCtx): Promise => { // If there is a clientAfter function and we are on the client if (env === 'client' && nextMiddleware.options.clientAfter) { // We need to await the next middleware and get the result - const result = await next(userCtx) + const resultCtx = await next(userCtx) // Then we can execute the clientAfter function return applyMiddleware( nextMiddleware.options.clientAfter, - result as any, + resultCtx as any, // Identity, because there "next" is just returning (d: any) => d, ) as any @@ -460,15 +679,15 @@ async function executeMiddleware( // Start the middleware chain return next({ - ...opts, - headers: opts.headers || {}, - sendContext: (opts as any).sendContext || {}, - context: opts.context || {}, + ...ctx, + headers: ctx.headers || {}, + sendContext: (ctx as any).sendContext || {}, + context: ctx.context || {}, }) } function serverFnBaseToMiddleware( - options: ServerFnBaseOptions, + options: ServerFnInternalOptions, ): AnyMiddleware { return { _types: undefined!, @@ -476,23 +695,52 @@ function serverFnBaseToMiddleware( validator: options.validator, validateClient: options.validateClient, client: async ({ next, sendContext, ...ctx }) => { - // Execute the extracted function - // but not before serializing the context - const res = await options.extractedFn?.({ + const payload = { ...ctx, // switch the sendContext over to context context: sendContext, - } as any) + type: typeof ctx.type === 'function' ? ctx.type(ctx) : ctx.type, + } as any + + if ( + ctx.type === 'static' && + process.env.NODE_ENV === 'production' && + typeof document !== 'undefined' && + serverFnStaticCache?.fetchItem + ) { + const result = await serverFnStaticCache.fetchItem(payload) + + if (result) { + if (result.error) { + throw result.error + } + + return next(result.ctx) + } + + warning( + result, + `No static cache item found for ${payload.filename}__${payload.functionId}__${JSON.stringify(payload.data)}, falling back to server function...`, + ) + } + + // Execute the extracted function + // but not before serializing the context + const res = await options.extractedFn?.(payload) return next(res) }, server: async ({ next, ...ctx }) => { - // Execute the server function - const result = await options.serverFn?.(ctx as any) + if (process.env.ROUTER !== 'client') { + // Execute the server function + const result = await options.serverFn?.(ctx as any) + + return next({ + result, + } as any) + } - return next({ - result, - } as any) + throw new Error('Server function called from the client!') }, }, } diff --git a/packages/start/src/client/index.tsx b/packages/start/src/client/index.tsx index 08f8b6f2df..71807b74e2 100644 --- a/packages/start/src/client/index.tsx +++ b/packages/start/src/client/index.tsx @@ -21,6 +21,7 @@ export { type FetcherBaseOptions, type ServerFn, type ServerFnCtx, + type MiddlewareCtx, } from './createServerFn' export { createMiddleware, diff --git a/packages/start/src/client/tests/createServerFn.test-d.tsx b/packages/start/src/client/tests/createServerFn.test-d.tsx index b2736e359d..0c1d18828d 100644 --- a/packages/start/src/client/tests/createServerFn.test-d.tsx +++ b/packages/start/src/client/tests/createServerFn.test-d.tsx @@ -47,6 +47,7 @@ test('createServerFn with validator', () => { expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: { input: string } headers?: HeadersInit + type?: 'static' | 'dynamic' }>() }) @@ -147,6 +148,7 @@ test('createServerFn with middleware and validator', () => { readonly inputC: 'inputC' } headers?: HeadersInit + type?: 'static' | 'dynamic' }>() expectTypeOf(fn).returns.resolves.toEqualTypeOf<'data'>() @@ -258,6 +260,7 @@ test('createServerFn where validator is optional if object is optional', () => { | { data?: 'c' | undefined headers?: HeadersInit + type?: 'static' | 'dynamic' } | undefined >() @@ -276,6 +279,7 @@ test('createServerFn where data is optional if there is no validator', () => { | { data?: undefined headers?: HeadersInit + type?: 'static' | 'dynamic' } | undefined >() diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index f371997c8e..1b581239b2 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -18,6 +18,7 @@ import { config } from 'vinxi/plugins/config' import { serverFunctions } from '@vinxi/server-functions/plugin' // @ts-expect-error import { serverTransform } from '@vinxi/server-functions/server' +import { createNitro } from 'nitropack' import { tanstackStartVinxiFileRouter } from './vinxi-file-router.js' import { checkDeploymentPresetInput, @@ -88,9 +89,9 @@ function mergeSsrOptions(options: Array) { return ssrOptions } -export function defineConfig( +export async function defineConfig( inlineConfig: TanStackStartInputConfig = {}, -): VinxiApp { +): Promise { const opts = inlineConfigSchema.parse(inlineConfig) const { preset: configDeploymentPreset, ...serverOptions } = @@ -123,6 +124,14 @@ export function defineConfig( const apiEntryExists = existsSync(apiEntry) + // Create a dummy nitro app to get the resolved public output path + const dummyNitroApp = await createNitro({ + preset: deploymentPreset, + }) + + const nitroOutputPublicDir = dummyNitroApp.options.output.publicDir + await dummyNitroApp.close() + let vinxiApp = createApp({ server: { ...serverOptions, @@ -162,10 +171,15 @@ export function defineConfig( define: { ...(viteConfig.userConfig.define || {}), ...(clientViteConfig.userConfig.define || {}), + ...injectDefineEnv('ROUTER_BASE', 'client'), ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), ...injectDefineEnv('TSS_SERVER_BASE', serverBase), ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv( + 'TSS_OUTPUT_PUBLIC_DIR', + nitroOutputPublicDir, + ), }, }), ...(viteConfig.plugins || []), @@ -199,10 +213,15 @@ export function defineConfig( define: { ...(viteConfig.userConfig.define || {}), ...(ssrViteConfig.userConfig.define || {}), + ...injectDefineEnv('ROUTER_BASE', 'ssr'), ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), ...injectDefineEnv('TSS_SERVER_BASE', serverBase), ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv( + 'TSS_OUTPUT_PUBLIC_DIR', + nitroOutputPublicDir, + ), }, }), tsrRoutesManifest({ @@ -248,10 +267,15 @@ export function defineConfig( define: { ...(viteConfig.userConfig.define || {}), ...(serverViteConfig.userConfig.define || {}), + ...injectDefineEnv('ROUTER_BASE', 'server'), ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), ...injectDefineEnv('TSS_SERVER_BASE', serverBase), ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv( + 'TSS_OUTPUT_PUBLIC_DIR', + nitroOutputPublicDir, + ), }, }), serverFunctions.server({ @@ -313,10 +337,12 @@ export function defineConfig( define: { ...(viteConfig.userConfig.define || {}), ...(apiViteConfig.userConfig.define || {}), + ...injectDefineEnv('ROUTER_BASE', 'api'), ...injectDefineEnv('TSS_PUBLIC_BASE', publicBase), ...injectDefineEnv('TSS_CLIENT_BASE', clientBase), ...injectDefineEnv('TSS_SERVER_BASE', serverBase), ...injectDefineEnv('TSS_API_BASE', apiBase), + ...injectDefineEnv('TSS_OUTPUT_PUBLIC_DIR', nitroOutputPublicDir), }, }), TanStackRouterVite({ diff --git a/packages/start/src/react-server-runtime/index.tsx b/packages/start/src/react-server-runtime/index.tsx index d7fbea3fe7..6c083c77f8 100644 --- a/packages/start/src/react-server-runtime/index.tsx +++ b/packages/start/src/react-server-runtime/index.tsx @@ -10,5 +10,7 @@ export function createServerReference( return Object.assign(fn, { url: functionUrl, + filename: id, + functionId: name, }) } diff --git a/packages/start/src/server-runtime/index.tsx b/packages/start/src/server-runtime/index.tsx index eda1e96e91..84a0759e6d 100644 --- a/packages/start/src/server-runtime/index.tsx +++ b/packages/start/src/server-runtime/index.tsx @@ -152,7 +152,9 @@ export function createServerReference(_fn: any, id: string, name: string) { } return Object.assign(proxyFn, { - url: functionUrl.replace(fakeHost, ''), + url: functionUrl + .replace(fakeHost, '') + .replace(fakeHost.replace('http:', 'https:'), ''), filename: id, functionId: name, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c470e9231..bfcad36ac8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1924,10 +1924,10 @@ importers: version: 18.3.1 html-webpack-plugin: specifier: ^5.6.3 - version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.0) + version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.9.3(@swc/helpers@0.5.15))(webpack@5.97.0) + version: 0.2.6(@swc/core@1.9.3(@swc/helpers@0.5.15))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -2613,6 +2613,110 @@ importers: specifier: ^5.1.3 version: 5.1.3(typescript@5.7.2)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + examples/react/start-basic-static: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-devtools': + specifier: workspace:* + version: link:../../../packages/router-devtools + '@tanstack/start': + specifier: workspace:* + version: link:../../../packages/start + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + tailwind-merge: + specifier: ^2.5.5 + version: 2.5.5 + vinxi: + specifier: 0.4.3 + version: 0.4.3(@types/node@22.10.1)(ioredis@5.4.1)(terser@5.36.0)(typescript@5.7.2) + devDependencies: + '@types/node': + specifier: ^22.5.4 + version: 22.10.1 + '@types/react': + specifier: ^18.2.65 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.2.21 + version: 18.3.1 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.49) + postcss: + specifier: ^8.4.49 + version: 8.4.49 + tailwindcss: + specifier: ^3.4.15 + version: 3.4.16 + typescript: + specifier: ^5.6.2 + version: 5.7.2 + vite-tsconfig-paths: + specifier: ^5.1.3 + version: 5.1.3(typescript@5.7.2)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + + examples/react/start-basic-static: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-devtools': + specifier: workspace:* + version: link:../../../packages/router-devtools + '@tanstack/start': + specifier: workspace:* + version: link:../../../packages/start + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + tailwind-merge: + specifier: ^2.5.5 + version: 2.5.5 + vinxi: + specifier: 0.4.3 + version: 0.4.3(@types/node@22.10.1)(ioredis@5.4.1)(terser@5.36.0)(typescript@5.7.2) + devDependencies: + '@types/node': + specifier: ^22.5.4 + version: 22.10.1 + '@types/react': + specifier: ^18.2.65 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.2.21 + version: 18.3.1 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.49) + postcss: + specifier: ^8.4.49 + version: 8.4.49 + tailwindcss: + specifier: ^3.4.15 + version: 3.4.16 + typescript: + specifier: ^5.6.2 + version: 5.7.2 + vite-tsconfig-paths: + specifier: ^5.1.3 + version: 5.1.3(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.1)(terser@5.36.0)) + examples/react/start-clerk-basic: dependencies: '@clerk/tanstack-start': @@ -3154,7 +3258,7 @@ importers: version: 4.3.4(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.0) + version: 5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) react: specifier: ^18.3.1 version: 18.3.1 @@ -3163,7 +3267,7 @@ importers: version: 18.3.1(react@18.3.1) swc-loader: specifier: ^0.2.6 - version: 0.2.6(@swc/core@1.9.3(@swc/helpers@0.5.15))(webpack@5.97.0) + version: 0.2.6(@swc/core@1.9.3(@swc/helpers@0.5.15))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -6971,6 +7075,14 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.2.4: + resolution: {integrity: sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==} + peerDependencies: + uWebSockets.js: '*' + peerDependenciesMeta: + uWebSockets.js: + optional: true + crossws@0.3.1: resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} @@ -7910,6 +8022,9 @@ packages: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + h3@1.11.1: + resolution: {integrity: sha512-AbaH6IDnZN6nmbnJOH72y3c5Wwh9P97soSVdGSBbcDACRdkC0FEWf25pzx4f/NuOCK6quHmW18yF2Wx+G4Zi1A==} + h3@1.13.0: resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==} @@ -10545,6 +10660,10 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vinxi@0.4.3: + resolution: {integrity: sha512-RgJz7RWftML5h/qfPsp3QKVc2FSlvV4+HevpE0yEY2j+PS/I2ULjoSsZDXaR8Ks2WYuFFDzQr8yrox7v8aqkng==} + hasBin: true + vinxi@0.5.1: resolution: {integrity: sha512-jvl2hJ0fyWwfDVQdDDHCJiVxqU4k0A6kFAnljS0kIjrGfhdTvKEWIoj0bcJgMyrKhxNMoZZGmHZsstQgjDIL3g==} hasBin: true @@ -13566,17 +13685,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.97.0)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.97.0)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.0)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.0))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4))': dependencies: webpack: 5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0) @@ -14260,6 +14379,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.2.4: {} + crossws@0.3.1: dependencies: uncrypto: 0.1.3 @@ -15432,6 +15553,21 @@ snapshots: dependencies: duplexer: 0.1.2 + h3@1.11.1: + dependencies: + cookie-es: 1.2.2 + crossws: 0.2.4 + defu: 6.1.4 + destr: 2.0.3 + iron-webcrypto: 1.2.1 + ohash: 1.1.4 + radix3: 1.1.2 + ufo: 1.5.4 + uncrypto: 0.1.3 + unenv: 1.10.0 + transitivePeerDependencies: + - uWebSockets.js + h3@1.13.0: dependencies: cookie-es: 1.2.2 @@ -15520,7 +15656,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.0): + html-webpack-plugin@5.6.3(@rspack/core@1.1.5(@swc/helpers@0.5.15))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -17666,7 +17802,7 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - swc-loader@0.2.6(@swc/core@1.9.3(@swc/helpers@0.5.15))(webpack@5.97.0): + swc-loader@0.2.6(@swc/core@1.9.3(@swc/helpers@0.5.15))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@swc/core': 1.9.3(@swc/helpers@0.5.15) '@swc/counter': 0.1.3 @@ -17736,19 +17872,19 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - terser-webpack-plugin@5.3.10(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)): + terser-webpack-plugin@5.3.10(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0) + webpack: 5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.9.3(@swc/helpers@0.5.15) esbuild: 0.24.0 - terser-webpack-plugin@5.3.10(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.0): + terser-webpack-plugin@5.3.10(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -17760,6 +17896,18 @@ snapshots: '@swc/core': 1.9.3(@swc/helpers@0.5.15) esbuild: 0.24.0 + terser-webpack-plugin@5.3.10(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.36.0 + webpack: 5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0) + optionalDependencies: + '@swc/core': 1.9.3(@swc/helpers@0.5.15) + esbuild: 0.24.0 + terser@5.36.0: dependencies: '@jridgewell/source-map': 0.3.6 @@ -18174,6 +18322,76 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + vinxi@0.4.3(@types/node@22.10.1)(ioredis@5.4.1)(terser@5.36.0)(typescript@5.7.2): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@types/micromatch': 4.0.9 + '@vinxi/listhen': 1.5.6 + boxen: 7.1.1 + chokidar: 3.6.0 + citty: 0.1.6 + consola: 3.2.3 + crossws: 0.2.4 + dax-sh: 0.39.2 + defu: 6.1.4 + es-module-lexer: 1.5.4 + esbuild: 0.20.2 + fast-glob: 3.3.2 + get-port-please: 3.1.2 + h3: 1.11.1 + hookable: 5.5.3 + http-proxy: 1.18.1 + micromatch: 4.0.8 + nitropack: 2.10.4(typescript@5.7.2) + node-fetch-native: 1.6.4 + path-to-regexp: 6.3.0 + pathe: 1.1.2 + radix3: 1.1.2 + resolve: 1.22.8 + serve-placeholder: 2.0.2 + serve-static: 1.16.2 + ufo: 1.5.4 + unctx: 2.3.1 + unenv: 1.10.0 + unstorage: 1.13.1(ioredis@5.4.1) + vite: 5.4.11(@types/node@22.10.1)(terser@5.36.0) + zod: 3.23.8 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/kv' + - better-sqlite3 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - less + - lightningcss + - mysql2 + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - typescript + - uWebSockets.js + - xml2js + vinxi@0.5.1(@types/node@22.10.1)(ioredis@5.4.1)(jiti@2.4.1)(terser@5.36.0)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.6.1): dependencies: '@babel/core': 7.26.0 @@ -18412,9 +18630,9 @@ snapshots: webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.97.0) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.97.0) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.97.0) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.97.0))(webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.97.0))(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -18428,7 +18646,7 @@ snapshots: optionalDependencies: webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.97.0) - webpack-dev-middleware@7.4.2(webpack@5.97.0): + webpack-dev-middleware@7.4.2(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)): dependencies: colorette: 2.0.20 memfs: 4.14.1 @@ -18467,7 +18685,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.97.0) + webpack-dev-middleware: 7.4.2(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) ws: 8.18.0 optionalDependencies: webpack: 5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) @@ -18540,7 +18758,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.0) + terser-webpack-plugin: 5.3.10(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack@5.97.0(@swc/core@1.9.3(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4)) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: