diff --git a/docs/framework/react/api/router/RouteMatchType.md b/docs/framework/react/api/router/RouteMatchType.md index 69cce37e3d9..12d69cca740 100644 --- a/docs/framework/react/api/router/RouteMatchType.md +++ b/docs/framework/react/api/router/RouteMatchType.md @@ -20,7 +20,6 @@ interface RouteMatch { updatedAt: number loadPromise?: Promise loaderData?: Route['loaderData'] - routeContext: Route['routeContext'] context: Route['allContext'] search: Route['fullSearchSchema'] fetchedAt: number diff --git a/docs/framework/react/guide/router-context.md b/docs/framework/react/guide/router-context.md index 48bfb628a63..e65f651f4e3 100644 --- a/docs/framework/react/guide/router-context.md +++ b/docs/framework/react/guide/router-context.md @@ -297,7 +297,7 @@ export const Route = createFileRoute('/todos')({ ## Processing Accumulated Route Context -Context, especially the isolated `routeContext` objects, make it trivial to accumulate and process the route context objects for all matched routes. Here's an example where we use all of the matched route contexts to generate a breadcrumb trail: +Context, especially the isolated route `context` objects, make it trivial to accumulate and process the route context objects for all matched routes. Here's an example where we use all of the matched route contexts to generate a breadcrumb trail: ```tsx // src/routes/__root.tsx @@ -306,9 +306,9 @@ export const Route = createRootRoute({ const router = useRouter() const breadcrumbs = router.state.matches.map((match) => { - const { routeContext } = match + const { context } = match return { - title: routeContext.getTitle(), + title: context.getTitle(), path: match.path, } }) @@ -328,9 +328,9 @@ export const Route = createRootRoute({ const matchWithTitle = [...router.state.matches] .reverse() - .find((d) => d.routeContext.getTitle) + .find((d) => d.context.getTitle) - const title = matchWithTitle?.routeContext.getTitle() || 'My App' + const title = matchWithTitle?.context.getTitle() || 'My App' return ( diff --git a/docs/framework/react/overview.md b/docs/framework/react/overview.md index 0ccae841066..dcf38f8ebd5 100644 --- a/docs/framework/react/overview.md +++ b/docs/framework/react/overview.md @@ -123,7 +123,7 @@ TanStack Router provides a light-weight built-in caching layer that works seamle ### Flexible & Powerful Data Lifecycle APIs -TanStack Router is designed with a flexible and powerful data loading API that more easily integrates with existing data fetching libraries like TanStack Query, SWR, Apollo, Relay, or even your own custom data fetching solution. Configurable APIs like `routeContext`, `beforeLoad`, `loaderDeps` and `loader` work in unison to make it easy to define declarative data dependencies, prefetch data, and manage the lifecycle of an external data source with ease. +TanStack Router is designed with a flexible and powerful data loading API that more easily integrates with existing data fetching libraries like TanStack Query, SWR, Apollo, Relay, or even your own custom data fetching solution. Configurable APIs like `context`, `beforeLoad`, `loaderDeps` and `loader` work in unison to make it easy to define declarative data dependencies, prefetch data, and manage the lifecycle of an external data source with ease. ## Inherited Route Context diff --git a/examples/react/start-basic-auth/.env b/examples/react/start-basic-auth/.env new file mode 100644 index 00000000000..c498ab59bf1 --- /dev/null +++ b/examples/react/start-basic-auth/.env @@ -0,0 +1,7 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="file:./dev.db" \ No newline at end of file diff --git a/examples/react/start-basic-auth/.eslintrc b/examples/react/start-basic-auth/.eslintrc new file mode 100644 index 00000000000..af7fa28f833 --- /dev/null +++ b/examples/react/start-basic-auth/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": ["react-app"], + "parser": "@typescript-eslint/parser", + "plugins": ["react-hooks"] +} diff --git a/examples/react/start-basic-auth/.gitignore b/examples/react/start-basic-auth/.gitignore new file mode 100644 index 00000000000..b15fed94e2a --- /dev/null +++ b/examples/react/start-basic-auth/.gitignore @@ -0,0 +1,22 @@ +node_modules +package-lock.json +yarn.lock + +!.env +.DS_Store +.cache +.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-auth/.prettierignore b/examples/react/start-basic-auth/.prettierignore new file mode 100644 index 00000000000..fd1b50a539c --- /dev/null +++ b/examples/react/start-basic-auth/.prettierignore @@ -0,0 +1,5 @@ +**/api +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/examples/react/start-basic-auth/.prettierrc b/examples/react/start-basic-auth/.prettierrc new file mode 100644 index 00000000000..aaf3357d4a0 --- /dev/null +++ b/examples/react/start-basic-auth/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": false, + "trailingComma": "all" +} diff --git a/examples/react/start-basic-auth/README.md b/examples/react/start-basic-auth/README.md new file mode 100644 index 00000000000..eb580a5bf88 --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/app.config.ts b/examples/react/start-basic-auth/app.config.ts new file mode 100644 index 00000000000..d1d9b04ded7 --- /dev/null +++ b/examples/react/start-basic-auth/app.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@tanstack/start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + vite: { + plugins: () => [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + ], + }, +}) diff --git a/examples/react/start-basic-auth/app/client.tsx b/examples/react/start-basic-auth/app/client.tsx new file mode 100644 index 00000000000..f16ba73f621 --- /dev/null +++ b/examples/react/start-basic-auth/app/client.tsx @@ -0,0 +1,7 @@ +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/start' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document.getElementById('root')!, ) diff --git a/examples/react/start-basic-auth/app/components/Auth.tsx b/examples/react/start-basic-auth/app/components/Auth.tsx new file mode 100644 index 00000000000..7504f8649b4 --- /dev/null +++ b/examples/react/start-basic-auth/app/components/Auth.tsx @@ -0,0 +1,57 @@ +export function Auth({ + actionText, + onSubmit, + status, + afterSubmit, +}: { + actionText: string + onSubmit: (e: React.FormEvent) => void + status: 'pending' | 'idle' | 'success' | 'error' + afterSubmit?: React.ReactNode +}) { + return ( +
+
+

{actionText}

+
{ + e.preventDefault() + onSubmit(e) + }} + className="space-y-4" + > +
+ + +
+
+ + +
+ + {afterSubmit ? afterSubmit : null} +
+
+
+ ) +} diff --git a/examples/react/start-basic-auth/app/components/DefaultCatchBoundary.tsx b/examples/react/start-basic-auth/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..f0ce51dc572 --- /dev/null +++ b/examples/react/start-basic-auth/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + ErrorComponentProps, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/examples/react/start-basic-auth/app/components/Login.tsx b/examples/react/start-basic-auth/app/components/Login.tsx new file mode 100644 index 00000000000..e330d3d93fc --- /dev/null +++ b/examples/react/start-basic-auth/app/components/Login.tsx @@ -0,0 +1,69 @@ +import { Link, useRouter } from '@tanstack/react-router' +import { useServerFn } from '@tanstack/start' +import { useMutation } from '../hooks/useMutation' +import { loginFn } from '../routes/_authed' +import { Auth } from './Auth' +import { signupFn } from '~/routes/signup' + +export function Login() { + const router = useRouter() + + const loginMutation = useMutation({ + fn: loginFn, + onSuccess: (ctx) => { + if (!ctx.data?.error) { + router.invalidate() + router.navigate({ + to: '/', + }) + return + } + }, + }) + + const signupMutation = useMutation({ + fn: useServerFn(signupFn), + }) + + return ( + { + const formData = new FormData(e.target as HTMLFormElement) + + loginMutation.mutate({ + email: formData.get('email') as string, + password: formData.get('password') as string, + }) + }} + afterSubmit={ + loginMutation.data ? ( + <> +
{loginMutation.data.message}
+ {loginMutation.data.userNotFound ? ( +
+ +
+ ) : null} + + ) : null + } + /> + ) +} diff --git a/examples/react/start-basic-auth/app/components/NotFound.tsx b/examples/react/start-basic-auth/app/components/NotFound.tsx new file mode 100644 index 00000000000..7b54fa56800 --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/app/hooks/useMutation.ts b/examples/react/start-basic-auth/app/hooks/useMutation.ts new file mode 100644 index 00000000000..1ff7a4653bd --- /dev/null +++ b/examples/react/start-basic-auth/app/hooks/useMutation.ts @@ -0,0 +1,44 @@ +import * as React from 'react' + +export function useMutation(opts: { + fn: (variables: TVariables) => Promise + onSuccess?: (ctx: { data: TData }) => void | Promise +}) { + const [submittedAt, setSubmittedAt] = React.useState() + const [variables, setVariables] = React.useState() + const [error, setError] = React.useState() + const [data, setData] = React.useState() + const [status, setStatus] = React.useState< + 'idle' | 'pending' | 'success' | 'error' + >('idle') + + const mutate = React.useCallback( + async (variables: TVariables): Promise => { + setStatus('pending') + setSubmittedAt(Date.now()) + setVariables(variables) + // + try { + const data = await opts.fn(variables) + await opts.onSuccess?.({ data }) + setStatus('success') + setError(undefined) + setData(data) + return data + } catch (err: any) { + setStatus('error') + setError(err) + } + }, + [opts.fn], + ) + + return { + status, + variables, + submittedAt, + mutate, + error, + data, + } +} diff --git a/examples/react/start-basic-auth/app/routeTree.gen.ts b/examples/react/start-basic-auth/app/routeTree.gen.ts new file mode 100644 index 00000000000..d63d8d3e4d9 --- /dev/null +++ b/examples/react/start-basic-auth/app/routeTree.gen.ts @@ -0,0 +1,194 @@ +/* prettier-ignore-start */ + +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file is auto-generated by TanStack Router + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as SignupImport } from './routes/signup' +import { Route as LogoutImport } from './routes/logout' +import { Route as LoginImport } from './routes/login' +import { Route as AuthedImport } from './routes/_authed' +import { Route as IndexImport } from './routes/index' +import { Route as AuthedPostsImport } from './routes/_authed/posts' +import { Route as AuthedPostsIndexImport } from './routes/_authed/posts.index' +import { Route as AuthedPostsPostIdImport } from './routes/_authed/posts.$postId' + +// Create/Update Routes + +const SignupRoute = SignupImport.update({ + path: '/signup', + getParentRoute: () => rootRoute, +} as any) + +const LogoutRoute = LogoutImport.update({ + path: '/logout', + getParentRoute: () => rootRoute, +} as any) + +const LoginRoute = LoginImport.update({ + path: '/login', + getParentRoute: () => rootRoute, +} as any) + +const AuthedRoute = AuthedImport.update({ + id: '/_authed', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const AuthedPostsRoute = AuthedPostsImport.update({ + path: '/posts', + getParentRoute: () => AuthedRoute, +} as any) + +const AuthedPostsIndexRoute = AuthedPostsIndexImport.update({ + path: '/', + getParentRoute: () => AuthedPostsRoute, +} as any) + +const AuthedPostsPostIdRoute = AuthedPostsPostIdImport.update({ + path: '/$postId', + getParentRoute: () => AuthedPostsRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_authed': { + id: '/_authed' + path: '' + fullPath: '' + preLoaderRoute: typeof AuthedImport + parentRoute: typeof rootRoute + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginImport + parentRoute: typeof rootRoute + } + '/logout': { + id: '/logout' + path: '/logout' + fullPath: '/logout' + preLoaderRoute: typeof LogoutImport + parentRoute: typeof rootRoute + } + '/signup': { + id: '/signup' + path: '/signup' + fullPath: '/signup' + preLoaderRoute: typeof SignupImport + parentRoute: typeof rootRoute + } + '/_authed/posts': { + id: '/_authed/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof AuthedPostsImport + parentRoute: typeof AuthedImport + } + '/_authed/posts/$postId': { + id: '/_authed/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof AuthedPostsPostIdImport + parentRoute: typeof AuthedPostsImport + } + '/_authed/posts/': { + id: '/_authed/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof AuthedPostsIndexImport + parentRoute: typeof AuthedPostsImport + } + } +} + +// Create and export the route tree + +export const routeTree = rootRoute.addChildren({ + IndexRoute, + AuthedRoute: AuthedRoute.addChildren({ + AuthedPostsRoute: AuthedPostsRoute.addChildren({ + AuthedPostsPostIdRoute, + AuthedPostsIndexRoute, + }), + }), + LoginRoute, + LogoutRoute, + SignupRoute, +}) + +/* prettier-ignore-end */ + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/_authed", + "/login", + "/logout", + "/signup" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/_authed": { + "filePath": "_authed.tsx", + "children": [ + "/_authed/posts" + ] + }, + "/login": { + "filePath": "login.tsx" + }, + "/logout": { + "filePath": "logout.tsx" + }, + "/signup": { + "filePath": "signup.tsx" + }, + "/_authed/posts": { + "filePath": "_authed/posts.tsx", + "parent": "/_authed", + "children": [ + "/_authed/posts/$postId", + "/_authed/posts/" + ] + }, + "/_authed/posts/$postId": { + "filePath": "_authed/posts.$postId.tsx", + "parent": "/_authed/posts" + }, + "/_authed/posts/": { + "filePath": "_authed/posts.index.tsx", + "parent": "/_authed/posts" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/examples/react/start-basic-auth/app/router.tsx b/examples/react/start-basic-auth/app/router.tsx new file mode 100644 index 00000000000..0886de701f0 --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/app/routes/__root.tsx b/examples/react/start-basic-auth/app/routes/__root.tsx new file mode 100644 index 00000000000..8f90c99d558 --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/__root.tsx @@ -0,0 +1,146 @@ +import { + Link, + Outlet, + ScrollRestoration, + createRootRoute, +} from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { + Body, + Head, + Html, + Meta, + Scripts, + createServerFn, +} from '@tanstack/start' +import * as React from 'react' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary.js' +import { NotFound } from '~/components/NotFound.js' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo.js' +import { sessionStorage } from '~/utils/session.js' + +const fetchUser = createServerFn('GET', async (_, { request }) => { + const cookie = request.headers.get('cookie') + const session = await sessionStorage.getSession(cookie) + const userEmail = session.get('userEmail') + + if (!userEmail) { + return null + } + + return { + email: userEmail, + } +}) + +export const Route = createRootRoute({ + 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' }, + ], + beforeLoad: async () => { + const user = await fetchUser() + + return { + user, + } + }, + errorComponent: (props) => { + return ( + + + + ) + }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: { children: React.ReactNode }) { + const { user } = Route.useRouteContext() + + return ( + + + + + +
+ + Home + {' '} + + Posts + +
+ {user ? ( + <> + {user.email} + Logout + + ) : ( + Login + )} +
+
+
+ {children} + + + + + + ) +} diff --git a/examples/react/start-basic-auth/app/routes/_authed.tsx b/examples/react/start-basic-auth/app/routes/_authed.tsx new file mode 100644 index 00000000000..e5cd33d7ae3 --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/_authed.tsx @@ -0,0 +1,72 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn, json } from '@tanstack/start' +import { Auth } from '../components/Auth' +import { sessionStorage } from '~/utils/session.js' +import { hashPassword, prismaClient } from '~/utils/prisma' +import { Login } from '~/components/Login' + +export const loginFn = createServerFn( + 'POST', + async ( + payload: { + email: string + password: string + }, + { request }, + ) => { + // Find the user + const user = await prismaClient.user.findUnique({ + where: { + email: payload.email, + }, + }) + + // Check if the user exists + if (!user) { + return { + error: true, + userNotFound: true, + message: 'User not found', + } + } + + // Check if the password is correct + const hashedPassword = await hashPassword(payload.password) + + if (user.password !== hashedPassword) { + return { + error: true, + message: 'Incorrect password', + } + } + + // Create a session + const session = await sessionStorage.getSession( + request.headers.get('cookie'), + ) + + // Store the user's email in the session + session.set('userEmail', user.email) + + return json(null, { + headers: { + 'Set-Cookie': await sessionStorage.commitSession(session), + }, + }) + }, +) + +export const Route = createFileRoute('/_authed')({ + beforeLoad: ({ context }) => { + if (!context.user) { + throw new Error('Not authenticated') + } + }, + errorComponent: ({ error }) => { + if (error.message === 'Not authenticated') { + return + } + + throw error + }, +}) diff --git a/examples/react/start-basic-auth/app/routes/_authed/posts.$postId.tsx b/examples/react/start-basic-auth/app/routes/_authed/posts.$postId.tsx new file mode 100644 index 00000000000..d6d12b3702e --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/_authed/posts.$postId.tsx @@ -0,0 +1,28 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' +import { NotFound } from '~/components/NotFound.js' +import { fetchPost } from '~/utils/posts.js' + +export const Route = createFileRoute('/_authed/posts/$postId')({ + loader: ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + component: PostComponent, + notFoundComponent: () => { + return Post not found + }, +}) + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post.title}

+
{post.body}
+
+ ) +} diff --git a/examples/react/start-basic-auth/app/routes/_authed/posts.index.tsx b/examples/react/start-basic-auth/app/routes/_authed/posts.index.tsx new file mode 100644 index 00000000000..ea9e667e540 --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/_authed/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authed/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/examples/react/start-basic-auth/app/routes/_authed/posts.tsx b/examples/react/start-basic-auth/app/routes/_authed/posts.tsx new file mode 100644 index 00000000000..86c8ef41389 --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/_authed/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '~/utils/posts.js' + +export const Route = createFileRoute('/_authed/posts')({ + loader: () => 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-auth/app/routes/index.tsx b/examples/react/start-basic-auth/app/routes/index.tsx new file mode 100644 index 00000000000..09a907cb18e --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/app/routes/login.tsx b/examples/react/start-basic-auth/app/routes/login.tsx new file mode 100644 index 00000000000..03ced208326 --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/login.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Login } from '~/components/Login' + +export const Route = createFileRoute('/login')({ + component: LoginComp, +}) + +function LoginComp() { + return +} diff --git a/examples/react/start-basic-auth/app/routes/logout.tsx b/examples/react/start-basic-auth/app/routes/logout.tsx new file mode 100644 index 00000000000..2ce954ded39 --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/logout.tsx @@ -0,0 +1,19 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/start' +import { sessionStorage } from '~/utils/session' + +const logoutFn = createServerFn('POST', async (_: void, { request }) => { + const session = await sessionStorage.getSession(request.headers.get('cookie')) + + throw redirect({ + href: '/', + headers: { + 'Set-Cookie': await sessionStorage.destroySession(session), + }, + }) +}) + +export const Route = createFileRoute('/logout')({ + preload: false, + loader: () => logoutFn(), +}) diff --git a/examples/react/start-basic-auth/app/routes/signup.tsx b/examples/react/start-basic-auth/app/routes/signup.tsx new file mode 100644 index 00000000000..1acb45cb5c5 --- /dev/null +++ b/examples/react/start-basic-auth/app/routes/signup.tsx @@ -0,0 +1,101 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' +import { createServerFn, useServerFn } from '@tanstack/start' +import { hashPassword, prismaClient } from '~/utils/prisma' +import { sessionStorage } from '~/utils/session' +import { useMutation } from '~/hooks/useMutation' +import { Auth } from '~/components/Auth' + +export const signupFn = createServerFn( + 'POST', + async ( + payload: { email: string; password: string; redirectUrl?: string }, + context, + ) => { + // Check if the user already exists + const found = await prismaClient.user.findUnique({ + where: { + email: payload.email, + }, + }) + + // Encrypt the password using Sha256 into plaintext + const password = await hashPassword(payload.password) + + // Create a session + const session = await sessionStorage.getSession( + context.request.headers.get('cookie'), + ) + + if (found) { + if (found.password !== password) { + return { + error: true, + userExists: true, + message: 'User already exists', + } + } + + // Store the user's email in the session + session.set('userEmail', found.email) + + // Redirect to the prev page stored in the "redirect" search param + throw redirect({ + href: payload.redirectUrl || '/', + headers: { + 'Set-Cookie': await sessionStorage.commitSession(session), + }, + }) + } + + // Create the user + const user = await prismaClient.user.create({ + data: { + email: payload.email, + password, + }, + }) + + // Store the user's email in the session + session.set('userEmail', user.email) + + // Redirect to the prev page stored in the "redirect" search param + throw redirect({ + href: payload.redirectUrl || '/', + headers: { + 'Set-Cookie': await sessionStorage.commitSession(session), + }, + }) + }, +) + +export const Route = createFileRoute('/signup')({ + component: SignupComp, +}) + +function SignupComp() { + const signupMutation = useMutation({ + fn: useServerFn(signupFn), + }) + + return ( + { + const formData = new FormData(e.target as HTMLFormElement) + + signupMutation.mutate({ + email: formData.get('email') as string, + password: formData.get('password') as string, + }) + }} + afterSubmit={ + signupMutation.data?.error ? ( + <> +
{signupMutation.data.message}
+ + ) : null + } + /> + ) +} diff --git a/examples/react/start-basic-auth/app/ssr.tsx b/examples/react/start-basic-auth/app/ssr.tsx new file mode 100644 index 00000000000..62572579acb --- /dev/null +++ b/examples/react/start-basic-auth/app/ssr.tsx @@ -0,0 +1,12 @@ +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-auth/app/styles/app.css b/examples/react/start-basic-auth/app/styles/app.css new file mode 100644 index 00000000000..d6426ccb723 --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/app/utils/posts.ts b/examples/react/start-basic-auth/app/utils/posts.ts new file mode 100644 index 00000000000..00fc9ae1437 --- /dev/null +++ b/examples/react/start-basic-auth/app/utils/posts.ts @@ -0,0 +1,33 @@ +import { notFound } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/start' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = createServerFn('GET', async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + console.error(err) + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +}) + +export const fetchPosts = createServerFn('GET', async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 1000)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +}) diff --git a/examples/react/start-basic-auth/app/utils/prisma.ts b/examples/react/start-basic-auth/app/utils/prisma.ts new file mode 100644 index 00000000000..74f5137b465 --- /dev/null +++ b/examples/react/start-basic-auth/app/utils/prisma.ts @@ -0,0 +1,16 @@ +import crypto from 'node:crypto' +import { PrismaClient } from '@prisma/client' + +export const prismaClient = new PrismaClient() + +export function hashPassword(password: string) { + return new Promise((resolve, reject) => { + crypto.pbkdf2(password, 'salt', 100000, 64, 'sha256', (err, derivedKey) => { + if (err) { + reject(err) + } else { + resolve(derivedKey.toString('hex')) + } + }) + }) +} diff --git a/examples/react/start-basic-auth/app/utils/seo.ts b/examples/react/start-basic-auth/app/utils/seo.ts new file mode 100644 index 00000000000..d18ad84b74e --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/app/utils/session.ts b/examples/react/start-basic-auth/app/utils/session.ts new file mode 100644 index 00000000000..bbf6cdc5ba2 --- /dev/null +++ b/examples/react/start-basic-auth/app/utils/session.ts @@ -0,0 +1,20 @@ +// app/services/session.server.ts +import { createCookieSessionStorage } from '@remix-run/node' + +import type { User } from '@prisma/client' + +type SessionUser = { + userEmail: User['email'] +} + +// export the whole sessionStorage object +export const sessionStorage = createCookieSessionStorage({ + cookie: { + name: '_session', // use any name you want here + sameSite: 'lax', // this helps with CSRF + path: '/', // remember to add this so the cookie will work in all routes + httpOnly: true, // for security reasons, make this cookie http only + secrets: ['s3cr3t'], // replace this with an actual secret + secure: process.env.NODE_ENV === 'production', // enable this in prod only + }, +}) diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json new file mode 100644 index 00000000000..cf88d4ca797 --- /dev/null +++ b/examples/react/start-basic-auth/package.json @@ -0,0 +1,52 @@ +{ + "name": "tanstack-router-example-react-start-basic-auth", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start", + "lint": "prettier --check '**/*' --ignore-unknown && eslint --ext .ts,.tsx ./app", + "format": "prettier --write '**/*' --ignore-unknown", + "prisma-generate": "prisma generate", + "test:e2e": "pnpm run prisma-generate && playwright test --project=chromium" + }, + "dependencies": { + "@prisma/client": "5.17.0", + "@remix-run/node": "^2.10.3", + "@remix-run/server-runtime": "^2.10.3", + "@tanstack/react-router": "^1.45.3", + "@tanstack/router-devtools": "^1.45.3", + "@tanstack/router-plugin": "^1.45.3", + "@tanstack/start": "^1.45.3", + "@typescript-eslint/parser": "^7.16.0", + "@vitejs/plugin-react": "^4.3.1", + "dotenv": "^16.4.5", + "isbot": "^5.1.12", + "prisma": "^5.17.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "redaxios": "^0.5.1", + "remix-auth-form": "^1.5.0", + "tailwind-merge": "^2.4.0", + "vinxi": "0.3.12" + }, + "devDependencies": { + "@playwright/test": "^1.45.1", + "@replayio/playwright": "^3.1.8", + "@types/node": "^20.12.11", + "@types/react": "^18.2.65", + "@types/react-dom": "^18.2.21", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-react-app": "^7.0.1", + "postcss": "^8.4.39", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.3", + "vite": "^5.3.3", + "vite-tsconfig-paths": "^4.3.2" + } +} diff --git a/examples/react/start-basic-auth/playwright.config.ts b/examples/react/start-basic-auth/playwright.config.ts new file mode 100644 index 00000000000..9af0a0860c3 --- /dev/null +++ b/examples/react/start-basic-auth/playwright.config.ts @@ -0,0 +1,44 @@ +import { defineConfig, devices } from '@playwright/test' + +import dotenv from 'dotenv' + +dotenv.config() + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3000/', + }, + + webServer: { + // TODO: build && start seems broken, use that if it's working + command: 'pnpm dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'mock-db-setup', + testMatch: 'tests/mock-db-setup.test.ts', + teardown: 'cleanup-mock-db', + }, + { + name: 'cleanup-mock-db', + testMatch: 'tests/mock-db-teardown.test.ts', + }, + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + dependencies: ['mock-db-setup'], + }, + ], +}) diff --git a/examples/react/start-basic-auth/postcss.config.cjs b/examples/react/start-basic-auth/postcss.config.cjs new file mode 100644 index 00000000000..8e638a6bcdb --- /dev/null +++ b/examples/react/start-basic-auth/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require('tailwindcss/nesting'), + require('tailwindcss'), + require('autoprefixer'), + ], +} diff --git a/examples/react/start-basic-auth/prisma/dev.db b/examples/react/start-basic-auth/prisma/dev.db new file mode 100644 index 00000000000..5f4ab51a029 Binary files /dev/null and b/examples/react/start-basic-auth/prisma/dev.db differ diff --git a/examples/react/start-basic-auth/prisma/migrations/20240811183753_init/migration.sql b/examples/react/start-basic-auth/prisma/migrations/20240811183753_init/migration.sql new file mode 100644 index 00000000000..4512a8f7823 --- /dev/null +++ b/examples/react/start-basic-auth/prisma/migrations/20240811183753_init/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "User" ( + "email" TEXT NOT NULL PRIMARY KEY, + "password" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/examples/react/start-basic-auth/prisma/migrations/migration_lock.toml b/examples/react/start-basic-auth/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000000..e5e5c4705ab --- /dev/null +++ b/examples/react/start-basic-auth/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/examples/react/start-basic-auth/prisma/schema.prisma b/examples/react/start-basic-auth/prisma/schema.prisma new file mode 100644 index 00000000000..3544834310f --- /dev/null +++ b/examples/react/start-basic-auth/prisma/schema.prisma @@ -0,0 +1,16 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + email String @id @unique + password String +} \ No newline at end of file diff --git a/examples/react/start-basic-auth/public/android-chrome-192x192.png b/examples/react/start-basic-auth/public/android-chrome-192x192.png new file mode 100644 index 00000000000..09c8324f8c6 Binary files /dev/null and b/examples/react/start-basic-auth/public/android-chrome-192x192.png differ diff --git a/examples/react/start-basic-auth/public/android-chrome-512x512.png b/examples/react/start-basic-auth/public/android-chrome-512x512.png new file mode 100644 index 00000000000..11d626ea3d0 Binary files /dev/null and b/examples/react/start-basic-auth/public/android-chrome-512x512.png differ diff --git a/examples/react/start-basic-auth/public/apple-touch-icon.png b/examples/react/start-basic-auth/public/apple-touch-icon.png new file mode 100644 index 00000000000..5a9423cc02c Binary files /dev/null and b/examples/react/start-basic-auth/public/apple-touch-icon.png differ diff --git a/examples/react/start-basic-auth/public/favicon-16x16.png b/examples/react/start-basic-auth/public/favicon-16x16.png new file mode 100644 index 00000000000..e3389b00443 Binary files /dev/null and b/examples/react/start-basic-auth/public/favicon-16x16.png differ diff --git a/examples/react/start-basic-auth/public/favicon-32x32.png b/examples/react/start-basic-auth/public/favicon-32x32.png new file mode 100644 index 00000000000..900c77d444c Binary files /dev/null and b/examples/react/start-basic-auth/public/favicon-32x32.png differ diff --git a/examples/react/start-basic-auth/public/favicon.ico b/examples/react/start-basic-auth/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/examples/react/start-basic-auth/public/favicon.ico differ diff --git a/examples/react/start-basic-auth/public/favicon.png b/examples/react/start-basic-auth/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/examples/react/start-basic-auth/public/favicon.png differ diff --git a/examples/react/start-basic-auth/public/site.webmanifest b/examples/react/start-basic-auth/public/site.webmanifest new file mode 100644 index 00000000000..fa99de77db6 --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/tailwind.config.cjs b/examples/react/start-basic-auth/tailwind.config.cjs new file mode 100644 index 00000000000..75fe25dbf79 --- /dev/null +++ b/examples/react/start-basic-auth/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-auth/tests/app.spec.ts b/examples/react/start-basic-auth/tests/app.spec.ts new file mode 100644 index 00000000000..1adf77ddc35 --- /dev/null +++ b/examples/react/start-basic-auth/tests/app.spec.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' + +async function signup(page: Page, email: string, password: string) { + await page.goto('/signup') + await page.fill('input[name="email"]', email) + await page.fill('input[name="password"]', password) + await page.click('button[type="submit"]') +} + +async function login( + page: Page, + email: string, + password: string, + signupOnFail = false, +) { + await page.goto('/login') + await page.fill('input[name="email"]', email) + await page.fill('input[name="password"]', password) + await page.click('button[type="submit"]') + + if (signupOnFail) { + await page.waitForSelector('text=User not found') + await page.click('button:has-text("Sign up instead?")') + await page.waitForSelector('text=Logout') + } +} + +test('Posts redirects to login when not authenticated', async ({ page }) => { + await page.goto('/posts') + await expect(page.locator('h1')).toContainText('Login') +}) + +test('Login fails with user not found', async ({ page }) => { + await login(page, 'bad@gmail.com', 'badpassword') + expect(page.getByText('User not found')).toBeTruthy() +}) + +test('Login fails with incorrect password', async ({ page }) => { + await signup(page, 'test@gmail.com', 'badpassword') + expect(page.getByText('Incorrect password')).toBeTruthy() +}) + +test('Can sign up from a not found user', async ({ page }) => { + await login(page, 'test2@gmail.com', 'badpassword', true) + expect(page.getByText('test@gmail.com')).toBeTruthy() +}) + +test('Navigating to post after logging in', async ({ page }) => { + await login(page, 'test@gmail.com', 'test') + await new Promise((r) => setTimeout(r, 1000)) + await page.getByRole('link', { name: 'Posts' }).click() + await page.getByRole('link', { name: 'sunt aut facere repe' }).click() + await expect(page.getByRole('heading')).toContainText('sunt aut facere') +}) diff --git a/examples/react/start-basic-auth/tests/mock-db-setup.test.ts b/examples/react/start-basic-auth/tests/mock-db-setup.test.ts new file mode 100644 index 00000000000..742381a0475 --- /dev/null +++ b/examples/react/start-basic-auth/tests/mock-db-setup.test.ts @@ -0,0 +1,21 @@ +import { test as setup } from '@playwright/test' + +import { PrismaClient } from '@prisma/client' + +const prismaClient = new PrismaClient() + +setup('create new database', async () => { + if ( + await prismaClient.user.findUnique({ + where: { + email: 'test2@gmail.com', + }, + }) + ) { + await prismaClient.user.delete({ + where: { + email: 'test2@gmail.com', + }, + }) + } +}) diff --git a/examples/react/start-basic-auth/tests/mock-db-teardown.test.ts b/examples/react/start-basic-auth/tests/mock-db-teardown.test.ts new file mode 100644 index 00000000000..d933c7cb089 --- /dev/null +++ b/examples/react/start-basic-auth/tests/mock-db-teardown.test.ts @@ -0,0 +1,21 @@ +import { test as teardown } from '@playwright/test' + +import { PrismaClient } from '@prisma/client' + +const prismaClient = new PrismaClient() + +teardown('create new database', async () => { + if ( + await prismaClient.user.findUnique({ + where: { + email: 'test2@gmail.com', + }, + }) + ) { + await prismaClient.user.delete({ + where: { + email: 'test2@gmail.com', + }, + }) + } +}) diff --git a/examples/react/start-basic-auth/tsconfig.json b/examples/react/start-basic-auth/tsconfig.json new file mode 100644 index 00000000000..d1b5b776601 --- /dev/null +++ b/examples/react/start-basic-auth/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-clerk-basic/.env b/examples/react/start-clerk-basic/.env new file mode 100644 index 00000000000..952a04d217f --- /dev/null +++ b/examples/react/start-clerk-basic/.env @@ -0,0 +1,2 @@ +CLERK_PUBLISHABLE_KEY=[YOUR_CLERK_PUBLISHABLE_KEY] +CLERK_SECRET_KEY=[YOUR_CLERK_SECRET_KEY] \ No newline at end of file diff --git a/examples/react/start-clerk-basic/.eslintrc b/examples/react/start-clerk-basic/.eslintrc new file mode 100644 index 00000000000..af7fa28f833 --- /dev/null +++ b/examples/react/start-clerk-basic/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": ["react-app"], + "parser": "@typescript-eslint/parser", + "plugins": ["react-hooks"] +} diff --git a/examples/react/start-clerk-basic/.gitignore b/examples/react/start-clerk-basic/.gitignore new file mode 100644 index 00000000000..b15fed94e2a --- /dev/null +++ b/examples/react/start-clerk-basic/.gitignore @@ -0,0 +1,22 @@ +node_modules +package-lock.json +yarn.lock + +!.env +.DS_Store +.cache +.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-clerk-basic/.prettierignore b/examples/react/start-clerk-basic/.prettierignore new file mode 100644 index 00000000000..fd1b50a539c --- /dev/null +++ b/examples/react/start-clerk-basic/.prettierignore @@ -0,0 +1,5 @@ +**/api +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/examples/react/start-clerk-basic/.prettierrc b/examples/react/start-clerk-basic/.prettierrc new file mode 100644 index 00000000000..aaf3357d4a0 --- /dev/null +++ b/examples/react/start-clerk-basic/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": false, + "trailingComma": "all" +} diff --git a/examples/react/start-clerk-basic/README.md b/examples/react/start-clerk-basic/README.md new file mode 100644 index 00000000000..eb580a5bf88 --- /dev/null +++ b/examples/react/start-clerk-basic/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-clerk-basic/app.config.ts b/examples/react/start-clerk-basic/app.config.ts new file mode 100644 index 00000000000..d1d9b04ded7 --- /dev/null +++ b/examples/react/start-clerk-basic/app.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@tanstack/start/config' +import tsConfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + vite: { + plugins: () => [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + ], + }, +}) diff --git a/examples/react/start-clerk-basic/app/client.tsx b/examples/react/start-clerk-basic/app/client.tsx new file mode 100644 index 00000000000..f16ba73f621 --- /dev/null +++ b/examples/react/start-clerk-basic/app/client.tsx @@ -0,0 +1,7 @@ +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/start' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document.getElementById('root')!, ) diff --git a/examples/react/start-clerk-basic/app/components/DefaultCatchBoundary.tsx b/examples/react/start-clerk-basic/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..f0ce51dc572 --- /dev/null +++ b/examples/react/start-clerk-basic/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + ErrorComponentProps, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/examples/react/start-clerk-basic/app/components/NotFound.tsx b/examples/react/start-clerk-basic/app/components/NotFound.tsx new file mode 100644 index 00000000000..7b54fa56800 --- /dev/null +++ b/examples/react/start-clerk-basic/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-clerk-basic/app/routeTree.gen.ts b/examples/react/start-clerk-basic/app/routeTree.gen.ts new file mode 100644 index 00000000000..7c12dcd2cae --- /dev/null +++ b/examples/react/start-clerk-basic/app/routeTree.gen.ts @@ -0,0 +1,159 @@ +/* prettier-ignore-start */ + +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file is auto-generated by TanStack Router + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as AuthedImport } from './routes/_authed' +import { Route as IndexImport } from './routes/index' +import { Route as AuthedPostsImport } from './routes/_authed/posts' +import { Route as AuthedPostsIndexImport } from './routes/_authed/posts.index' +import { Route as AuthedProfileSplatImport } from './routes/_authed/profile.$' +import { Route as AuthedPostsPostIdImport } from './routes/_authed/posts.$postId' + +// Create/Update Routes + +const AuthedRoute = AuthedImport.update({ + id: '/_authed', + getParentRoute: () => rootRoute, +} as any) + +const IndexRoute = IndexImport.update({ + path: '/', + getParentRoute: () => rootRoute, +} as any) + +const AuthedPostsRoute = AuthedPostsImport.update({ + path: '/posts', + getParentRoute: () => AuthedRoute, +} as any) + +const AuthedPostsIndexRoute = AuthedPostsIndexImport.update({ + path: '/', + getParentRoute: () => AuthedPostsRoute, +} as any) + +const AuthedProfileSplatRoute = AuthedProfileSplatImport.update({ + path: '/profile/$', + getParentRoute: () => AuthedRoute, +} as any) + +const AuthedPostsPostIdRoute = AuthedPostsPostIdImport.update({ + path: '/$postId', + getParentRoute: () => AuthedPostsRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/_authed': { + id: '/_authed' + path: '' + fullPath: '' + preLoaderRoute: typeof AuthedImport + parentRoute: typeof rootRoute + } + '/_authed/posts': { + id: '/_authed/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof AuthedPostsImport + parentRoute: typeof AuthedImport + } + '/_authed/posts/$postId': { + id: '/_authed/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof AuthedPostsPostIdImport + parentRoute: typeof AuthedPostsImport + } + '/_authed/profile/$': { + id: '/_authed/profile/$' + path: '/profile/$' + fullPath: '/profile/$' + preLoaderRoute: typeof AuthedProfileSplatImport + parentRoute: typeof AuthedImport + } + '/_authed/posts/': { + id: '/_authed/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof AuthedPostsIndexImport + parentRoute: typeof AuthedPostsImport + } + } +} + +// Create and export the route tree + +export const routeTree = rootRoute.addChildren({ + IndexRoute, + AuthedRoute: AuthedRoute.addChildren({ + AuthedPostsRoute: AuthedPostsRoute.addChildren({ + AuthedPostsPostIdRoute, + AuthedPostsIndexRoute, + }), + AuthedProfileSplatRoute, + }), +}) + +/* prettier-ignore-end */ + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/_authed" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/_authed": { + "filePath": "_authed.tsx", + "children": [ + "/_authed/posts", + "/_authed/profile/$" + ] + }, + "/_authed/posts": { + "filePath": "_authed/posts.tsx", + "parent": "/_authed", + "children": [ + "/_authed/posts/$postId", + "/_authed/posts/" + ] + }, + "/_authed/posts/$postId": { + "filePath": "_authed/posts.$postId.tsx", + "parent": "/_authed/posts" + }, + "/_authed/profile/$": { + "filePath": "_authed/profile.$.tsx", + "parent": "/_authed" + }, + "/_authed/posts/": { + "filePath": "_authed/posts.index.tsx", + "parent": "/_authed/posts" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/examples/react/start-clerk-basic/app/router.tsx b/examples/react/start-clerk-basic/app/router.tsx new file mode 100644 index 00000000000..0886de701f0 --- /dev/null +++ b/examples/react/start-clerk-basic/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-clerk-basic/app/routes/__root.tsx b/examples/react/start-clerk-basic/app/routes/__root.tsx new file mode 100644 index 00000000000..2c4e4d9ef20 --- /dev/null +++ b/examples/react/start-clerk-basic/app/routes/__root.tsx @@ -0,0 +1,139 @@ +import { + Link, + Outlet, + ScrollRestoration, + createRootRoute, +} from '@tanstack/react-router' +import { + ClerkProvider, + SignInButton, + SignedIn, + SignedOut, + UserButton, +} from '@clerk/tanstack-start' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { + Body, + Head, + Html, + Meta, + Scripts, + createServerFn, +} from '@tanstack/start' +import * as React from 'react' +import { getAuth } from '@clerk/tanstack-start/server' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary.js' +import { NotFound } from '~/components/NotFound.js' +import appCss from '~/styles/app.css?url' + +const fetchClerkAuth = createServerFn('GET', async (_, ctx) => { + const user = await getAuth(ctx) + + return { + user, + } +}) + +export const Route = createRootRoute({ + meta: () => [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ], + 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' }, + ], + beforeLoad: async () => { + const { user } = await fetchClerkAuth() + + return { + user, + } + }, + errorComponent: (props) => { + return ( + + + + ) + }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + ) +} + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + {' '} + + Posts + +
+ + + + + + +
+
+
+ {children} + + + + + + ) +} diff --git a/examples/react/start-clerk-basic/app/routes/_authed.tsx b/examples/react/start-clerk-basic/app/routes/_authed.tsx new file mode 100644 index 00000000000..0ab5eb05c87 --- /dev/null +++ b/examples/react/start-clerk-basic/app/routes/_authed.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { SignIn } from '@clerk/tanstack-start' + +export const Route = createFileRoute('/_authed')({ + beforeLoad: ({ context }) => { + if (!context.user.userId) { + throw new Error('Not authenticated') + } + }, + errorComponent: ({ error }) => { + if (error.message === 'Not authenticated') { + return ( +
+ +
+ ) + } + + throw error + }, +}) diff --git a/examples/react/start-clerk-basic/app/routes/_authed/posts.$postId.tsx b/examples/react/start-clerk-basic/app/routes/_authed/posts.$postId.tsx new file mode 100644 index 00000000000..d6d12b3702e --- /dev/null +++ b/examples/react/start-clerk-basic/app/routes/_authed/posts.$postId.tsx @@ -0,0 +1,28 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' +import { NotFound } from '~/components/NotFound.js' +import { fetchPost } from '~/utils/posts.js' + +export const Route = createFileRoute('/_authed/posts/$postId')({ + loader: ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent as any, + component: PostComponent, + notFoundComponent: () => { + return Post not found + }, +}) + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post.title}

+
{post.body}
+
+ ) +} diff --git a/examples/react/start-clerk-basic/app/routes/_authed/posts.index.tsx b/examples/react/start-clerk-basic/app/routes/_authed/posts.index.tsx new file mode 100644 index 00000000000..ea9e667e540 --- /dev/null +++ b/examples/react/start-clerk-basic/app/routes/_authed/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authed/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/examples/react/start-clerk-basic/app/routes/_authed/posts.tsx b/examples/react/start-clerk-basic/app/routes/_authed/posts.tsx new file mode 100644 index 00000000000..86c8ef41389 --- /dev/null +++ b/examples/react/start-clerk-basic/app/routes/_authed/posts.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '~/utils/posts.js' + +export const Route = createFileRoute('/_authed/posts')({ + loader: () => 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-clerk-basic/app/routes/_authed/profile.$.tsx b/examples/react/start-clerk-basic/app/routes/_authed/profile.$.tsx new file mode 100644 index 00000000000..208a38e2300 --- /dev/null +++ b/examples/react/start-clerk-basic/app/routes/_authed/profile.$.tsx @@ -0,0 +1,38 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '~/utils/posts.js' + +export const Route = createFileRoute('/_authed/profile/$')({ + loader: () => 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-clerk-basic/app/routes/index.tsx b/examples/react/start-clerk-basic/app/routes/index.tsx new file mode 100644 index 00000000000..6ae388a1785 --- /dev/null +++ b/examples/react/start-clerk-basic/app/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Hello Clerk!

+
+ ) +} diff --git a/examples/react/start-clerk-basic/app/ssr.tsx b/examples/react/start-clerk-basic/app/ssr.tsx new file mode 100644 index 00000000000..12446ca2c7f --- /dev/null +++ b/examples/react/start-clerk-basic/app/ssr.tsx @@ -0,0 +1,16 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/start/server' +import { getRouterManifest } from '@tanstack/start/router-manifest' +import { createRouter } from './router' +import { createClerkHandler } from '@clerk/tanstack-start/server' + +const handler = createStartHandler({ + createRouter, + getRouterManifest, +}) + +const clerkHandler = createClerkHandler(handler) + +export default clerkHandler(defaultStreamHandler) diff --git a/examples/react/start-clerk-basic/app/styles/app.css b/examples/react/start-clerk-basic/app/styles/app.css new file mode 100644 index 00000000000..d6426ccb723 --- /dev/null +++ b/examples/react/start-clerk-basic/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-clerk-basic/app/utils/posts.ts b/examples/react/start-clerk-basic/app/utils/posts.ts new file mode 100644 index 00000000000..00fc9ae1437 --- /dev/null +++ b/examples/react/start-clerk-basic/app/utils/posts.ts @@ -0,0 +1,33 @@ +import { notFound } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/start' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = createServerFn('GET', async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + console.error(err) + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +}) + +export const fetchPosts = createServerFn('GET', async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 1000)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +}) diff --git a/examples/react/start-clerk-basic/app/utils/seo.ts b/examples/react/start-clerk-basic/app/utils/seo.ts new file mode 100644 index 00000000000..d18ad84b74e --- /dev/null +++ b/examples/react/start-clerk-basic/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-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json new file mode 100644 index 00000000000..9f13def9272 --- /dev/null +++ b/examples/react/start-clerk-basic/package.json @@ -0,0 +1,50 @@ +{ + "name": "tanstack-router-example-react-start-clerk-basic", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start", + "lint": "prettier --check '**/*' --ignore-unknown && eslint --ext .ts,.tsx ./app", + "format": "prettier --write '**/*' --ignore-unknown", + "test:e2e": "exit 0; playwright test --project=chromium" + }, + "dependencies": { + "@clerk/tanstack-start": "0.3.0-snapshot.vdf04997", + "@remix-run/node": "^2.10.3", + "@remix-run/server-runtime": "^2.10.3", + "@tanstack/react-router": "^1.45.3", + "@tanstack/router-devtools": "^1.45.3", + "@tanstack/router-plugin": "^1.45.3", + "@tanstack/start": "^1.45.3", + "@typescript-eslint/parser": "^7.16.0", + "@vitejs/plugin-react": "^4.3.1", + "dotenv": "^16.4.5", + "isbot": "^5.1.12", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "redaxios": "^0.5.1", + "remix-auth-form": "^1.5.0", + "tailwind-merge": "^2.4.0", + "vinxi": "0.3.12" + }, + "devDependencies": { + "@playwright/test": "^1.45.1", + "@replayio/playwright": "^3.1.8", + "@types/node": "^20.12.11", + "@types/react": "^18.2.65", + "@types/react-dom": "^18.2.21", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-config-react-app": "^7.0.1", + "postcss": "^8.4.39", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.3", + "vite": "^5.3.3", + "vite-tsconfig-paths": "^4.3.2" + } +} diff --git a/examples/react/start-clerk-basic/playwright.config.ts b/examples/react/start-clerk-basic/playwright.config.ts new file mode 100644 index 00000000000..092048941c7 --- /dev/null +++ b/examples/react/start-clerk-basic/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' + +import dotenv from 'dotenv' + +dotenv.config() + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3000/', + }, + + webServer: { + // TODO: build && start seems broken, use that if it's working + command: 'pnpm dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/examples/react/start-clerk-basic/postcss.config.cjs b/examples/react/start-clerk-basic/postcss.config.cjs new file mode 100644 index 00000000000..8e638a6bcdb --- /dev/null +++ b/examples/react/start-clerk-basic/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require('tailwindcss/nesting'), + require('tailwindcss'), + require('autoprefixer'), + ], +} diff --git a/examples/react/start-clerk-basic/public/android-chrome-192x192.png b/examples/react/start-clerk-basic/public/android-chrome-192x192.png new file mode 100644 index 00000000000..09c8324f8c6 Binary files /dev/null and b/examples/react/start-clerk-basic/public/android-chrome-192x192.png differ diff --git a/examples/react/start-clerk-basic/public/android-chrome-512x512.png b/examples/react/start-clerk-basic/public/android-chrome-512x512.png new file mode 100644 index 00000000000..11d626ea3d0 Binary files /dev/null and b/examples/react/start-clerk-basic/public/android-chrome-512x512.png differ diff --git a/examples/react/start-clerk-basic/public/apple-touch-icon.png b/examples/react/start-clerk-basic/public/apple-touch-icon.png new file mode 100644 index 00000000000..5a9423cc02c Binary files /dev/null and b/examples/react/start-clerk-basic/public/apple-touch-icon.png differ diff --git a/examples/react/start-clerk-basic/public/favicon-16x16.png b/examples/react/start-clerk-basic/public/favicon-16x16.png new file mode 100644 index 00000000000..e3389b00443 Binary files /dev/null and b/examples/react/start-clerk-basic/public/favicon-16x16.png differ diff --git a/examples/react/start-clerk-basic/public/favicon-32x32.png b/examples/react/start-clerk-basic/public/favicon-32x32.png new file mode 100644 index 00000000000..900c77d444c Binary files /dev/null and b/examples/react/start-clerk-basic/public/favicon-32x32.png differ diff --git a/examples/react/start-clerk-basic/public/favicon.ico b/examples/react/start-clerk-basic/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/examples/react/start-clerk-basic/public/favicon.ico differ diff --git a/examples/react/start-clerk-basic/public/favicon.png b/examples/react/start-clerk-basic/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/examples/react/start-clerk-basic/public/favicon.png differ diff --git a/examples/react/start-clerk-basic/public/site.webmanifest b/examples/react/start-clerk-basic/public/site.webmanifest new file mode 100644 index 00000000000..fa99de77db6 --- /dev/null +++ b/examples/react/start-clerk-basic/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-clerk-basic/tailwind.config.cjs b/examples/react/start-clerk-basic/tailwind.config.cjs new file mode 100644 index 00000000000..75fe25dbf79 --- /dev/null +++ b/examples/react/start-clerk-basic/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-clerk-basic/tests/app.spec.ts b/examples/react/start-clerk-basic/tests/app.spec.ts new file mode 100644 index 00000000000..208a95cadd8 --- /dev/null +++ b/examples/react/start-clerk-basic/tests/app.spec.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' + +test('loads', async ({ page }) => { + await page.goto('http://localhost:3000') +}) diff --git a/examples/react/start-clerk-basic/tsconfig.json b/examples/react/start-clerk-basic/tsconfig.json new file mode 100644 index 00000000000..d1b5b776601 --- /dev/null +++ b/examples/react/start-clerk-basic/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/packages/history/src/index.ts b/packages/history/src/index.ts index 0c66ec93003..ec6f2d9aa77 100644 --- a/packages/history/src/index.ts +++ b/packages/history/src/index.ts @@ -393,7 +393,7 @@ export function createMemoryHistory( }) } -function parseHref( +export function parseHref( href: string, state: HistoryState | undefined, ): HistoryLocation { diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx index 738da53cf42..97d70020d3b 100644 --- a/packages/react-router/src/Matches.tsx +++ b/packages/react-router/src/Matches.tsx @@ -33,7 +33,6 @@ export interface RouteMatch< TFullSearchSchema, TLoaderData, TAllContext, - TRouteContext, TLoaderDeps, > { id: string @@ -52,7 +51,8 @@ export interface RouteMatch< beforeLoadPromise?: ControlledPromise loaderPromise?: ControlledPromise loaderData?: TLoaderData - routeContext: TRouteContext + __routeContext: Record + __beforeLoadContext: Record context: TAllContext search: TFullSearchSchema fetchCount: number @@ -88,7 +88,6 @@ export type MakeRouteMatch< TAllContext = TStrict extends false ? AllContext : TTypes['allContext'], - TRouteContext = TTypes['routeContext'], TLoaderDeps = TTypes['loaderDeps'], > = RouteMatch< TRouteId, @@ -96,11 +95,10 @@ export type MakeRouteMatch< TFullSearchSchema, TLoaderData, TAllContext, - TRouteContext, TLoaderDeps > -export type AnyRouteMatch = RouteMatch +export type AnyRouteMatch = RouteMatch export function Matches() { const router = useRouter() @@ -109,11 +107,17 @@ export function Matches() { ) : null + // Do not render a root Suspense during SSR or hydrating from SSR + const ResolvedSuspense = + router.isServer || (typeof document !== 'undefined' && window.__TSR__) + ? SafeFragment + : React.Suspense + const inner = ( - + - + ) return router.options.InnerWrap ? ( diff --git a/packages/react-router/src/fileRoute.ts b/packages/react-router/src/fileRoute.ts index db78b6d0ac1..80748b064c6 100644 --- a/packages/react-router/src/fileRoute.ts +++ b/packages/react-router/src/fileRoute.ts @@ -15,14 +15,13 @@ import type { AnySearchValidator, DefaultSearchValidator, FileBaseRouteOptions, - InferAllContext, - ResolveAllContext, ResolveAllParamsFromParent, ResolveLoaderData, ResolveRouteContext, Route, RouteConstraints, RouteContext, + RouteContextFn, RouteLoaderFn, UpdatableRouteOptions, } from './route' @@ -76,9 +75,8 @@ export class FileRoute< TSearchValidator extends AnySearchValidator = DefaultSearchValidator, TParams = Record, string>, TAllParams = ResolveAllParamsFromParent, - TRouteContextReturn = RouteContext, - TRouteContext = ResolveRouteContext, - TAllContext = ResolveAllContext, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, TLoaderData = ResolveLoaderData, @@ -89,12 +87,11 @@ export class FileRoute< TPath, TSearchValidator, TParams, - TAllParams, - TRouteContextReturn, - InferAllContext, - TAllContext, TLoaderDeps, - TLoaderDataReturn + TLoaderDataReturn, + AnyContext, + TRouteContextFn, + TBeforeLoadFn > & UpdatableRouteOptions< TParentRoute, @@ -102,9 +99,10 @@ export class FileRoute< TAllParams, TSearchValidator, TLoaderData, - TAllContext, - TRouteContext, - TLoaderDeps + TLoaderDeps, + AnyContext, + TRouteContextFn, + TBeforeLoadFn >, ): Route< TParentRoute, @@ -115,9 +113,9 @@ export class FileRoute< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - TAllContext, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData, @@ -145,15 +143,21 @@ export function FileRouteLoader< _path: TFilePath, ): ( loaderFn: RouteLoaderFn< - TRoute['types']['allParams'], + TRoute['parentRoute'], + TRoute['types']['params'], TRoute['types']['loaderDeps'], - TRoute['types']['allContext'], + TRoute['types']['routerContext'], + TRoute['types']['routeContextFn'], + TRoute['types']['beforeLoadFn'], TLoaderData >, ) => RouteLoaderFn< - TRoute['types']['allParams'], + TRoute['parentRoute'], + TRoute['types']['params'], TRoute['types']['loaderDeps'], - TRoute['types']['allContext'], + TRoute['types']['routerContext'], + TRoute['types']['routeContextFn'], + TRoute['types']['beforeLoadFn'], NoInfer > { warning( @@ -172,7 +176,8 @@ export type LazyRouteOptions = Pick< {}, AnyContext, AnyContext, - {} + AnyContext, + AnyContext >, 'component' | 'errorComponent' | 'pendingComponent' | 'notFoundComponent' > diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx index ea67ed7b920..42e5dbe131a 100644 --- a/packages/react-router/src/index.tsx +++ b/packages/react-router/src/index.tsx @@ -214,7 +214,6 @@ export type { RouterContextOptions, TrailingSlashOption, RouterOptions, - RouterTransformer, RouterErrorSerializer, RouterState, ListenerFn, @@ -227,6 +226,8 @@ export type { RouterEvent, RouterListener, AnyRouterWithContext, + ExtractedEntry, + StreamState, } from './router' export { RouterProvider, RouterContextProvider } from './RouterProvider' @@ -254,6 +255,9 @@ export { } from './searchParams' export type { SearchSerializer, SearchParser } from './searchParams' +export { defaultTransformer } from './transformer' +export type { RouterTransformer } from './transformer' + export { useBlocker, Block } from './useBlocker' export { useNavigate, Navigate } from './useNavigate' diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts index 376a3b3c8b6..ab2ab7a139d 100644 --- a/packages/react-router/src/route.ts +++ b/packages/react-router/src/route.ts @@ -16,7 +16,7 @@ import type { NavigateOptions, ParsePathParams, ToMaskOptions } from './link' import type { ParsedLocation } from './location' import type { RouteById, RouteIds, RoutePaths } from './routeInfo' import type { AnyRouter, RegisteredRouter, Router } from './router' -import type { Assign, Expand, NoInfer, PickRequired } from './utils' +import type { Assign, Constrain, Expand, NoInfer, PickRequired } from './utils' import type { BuildLocationFn, NavigateFn } from './RouterProvider' import type { NotFoundError } from './not-found' import type { LazyRoute } from './fileRoute' @@ -57,25 +57,23 @@ export type RouteOptions< TSearchValidator extends AnySearchValidator = DefaultSearchValidator, TParams = AnyPathParams, TAllParams = TParams, - TRouteContextReturn = RouteContext, - TRouteContext = RouteContext, - TParentAllContext = AnyContext, - TAllContext = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, TLoaderData = ResolveLoaderData, + TRouterContext = {}, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, > = BaseRouteOptions< TParentRoute, TCustomId, TPath, TSearchValidator, TParams, - TAllParams, - TRouteContextReturn, - TParentAllContext, - TAllContext, TLoaderDeps, - TLoaderDataReturn + TLoaderDataReturn, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn > & UpdatableRouteOptions< NoInfer, @@ -83,9 +81,10 @@ export type RouteOptions< NoInfer, NoInfer, NoInfer, - NoInfer, - NoInfer, - NoInfer + NoInfer, + NoInfer, + NoInfer, + NoInfer > export type ParseParamsFn = ( @@ -115,52 +114,103 @@ export type ParamsOptions = { stringifyParams?: StringifyParamsFn } -export interface FullSearchSchemaOption { - search: TFullSearchSchema +export interface FullSearchSchemaOption< + in out TParentRoute extends AnyRoute, + in out TSearchValidator extends AnySearchValidator, +> { + search: Expand> } +export type RouteContextFn< + in out TParentRoute extends AnyRoute, + in out TSearchValidator extends AnySearchValidator, + in out TParams, + in out TRouterContext, +> = ( + ctx: RouteContextOptions< + TParentRoute, + TSearchValidator, + TParams, + TRouterContext + >, +) => any + +export type BeforeLoadFn< + in out TParentRoute extends AnyRoute, + in out TSearchValidator extends AnySearchValidator, + in out TParams, + in out TRouterContext, + in out TRouteContextFn, +> = ( + ctx: BeforeLoadContextOptions< + TParentRoute, + TSearchValidator, + TParams, + TRouterContext, + TRouteContextFn + >, +) => any + export type FileBaseRouteOptions< TParentRoute extends AnyRoute = AnyRoute, TPath extends string = string, TSearchValidator extends AnySearchValidator = undefined, TParams = {}, - TAllParams = {}, - TRouteContextReturn = RouteContext, - TParentAllContext = AnyContext, - TAllContext = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, -> = { + TRouterContext = {}, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, +> = ParamsOptions & { validateSearch?: TSearchValidator shouldReload?: | boolean | (( match: LoaderFnContext< - TAllParams, - ResolveFullSearchSchema, - TAllContext + TParentRoute, + TParams, + TLoaderDeps, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn >, ) => any) + + context?: Constrain< + TRouteContextFn, + RouteContextFn + > + // This async function is called before a route is loaded. // If an error is thrown here, the route's loader will not be called. // If thrown during a navigation, the navigation will be cancelled and the error will be passed to the `onError` function. // If thrown during a preload event, the error will be logged to the console. - beforeLoad?: ( - ctx: BeforeLoadContext< - ResolveFullSearchSchema, - TAllParams, - TParentAllContext - >, - ) => Promise | TRouteContextReturn | void + beforeLoad?: Constrain< + TBeforeLoadFn, + BeforeLoadFn< + TParentRoute, + TSearchValidator, + TParams, + TRouterContext, + TRouteContextFn + > + > + loaderDeps?: ( - opts: FullSearchSchemaOption< - ResolveFullSearchSchema - >, + opts: FullSearchSchemaOption, ) => TLoaderDeps + loader?: ( - ctx: LoaderFnContext, + ctx: LoaderFnContext< + TParentRoute, + TParams, + TLoaderDeps, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + >, ) => TLoaderDataReturn | Promise -} & ParamsOptions +} export type BaseRouteOptions< TParentRoute extends AnyRoute = AnyRoute, @@ -168,37 +218,34 @@ export type BaseRouteOptions< TPath extends string = string, TSearchValidator extends AnySearchValidator = undefined, TParams = {}, - TAllParams = {}, - TRouteContextReturn = RouteContext, - TParentAllContext = AnyContext, - TAllContext = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, + TRouterContext = {}, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, > = RoutePathOptions & FileBaseRouteOptions< TParentRoute, TPath, TSearchValidator, TParams, - TAllParams, - TRouteContextReturn, - TParentAllContext, - TAllContext, TLoaderDeps, - TLoaderDataReturn + TLoaderDataReturn, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn > & { getParentRoute: () => TParentRoute } -export interface BeforeLoadContext< - TFullSearchSchema, - TAllParams, - TParentAllContext, -> extends FullSearchSchemaOption { +export interface ContextOptions< + in out TParentRoute extends AnyRoute, + in out TSearchValidator extends AnySearchValidator, + in out TParams, +> extends FullSearchSchemaOption { abortController: AbortController preload: boolean - params: Expand - context: TParentAllContext + params: Expand> location: ParsedLocation /** * @deprecated Use `throw redirect({ to: '/somewhere' })` instead @@ -208,15 +255,37 @@ export interface BeforeLoadContext< cause: 'preload' | 'enter' | 'stay' } +export interface RouteContextOptions< + in out TParentRoute extends AnyRoute, + in out TSearchValidator extends AnySearchValidator, + in out TParams, + in out TRouterContext, +> extends ContextOptions { + context: Expand> +} + +export interface BeforeLoadContextOptions< + in out TParentRoute extends AnyRoute, + in out TSearchValidator extends AnySearchValidator, + in out TParams, + in out TRouterContext, + in out TRouteContextFn, +> extends ContextOptions { + context: Expand< + BeforeLoadContextParameter + > +} + export type UpdatableRouteOptions< TParentRoute extends AnyRoute, TRouteId, TAllParams, TSearchValidator extends AnySearchValidator, TLoaderData, - TAllContext, - TRouteContext, TLoaderDeps, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, > = { // test?: (args: TAllContext) => void // If true, this route will be matched as case-sensitive @@ -232,6 +301,7 @@ export type UpdatableRouteOptions< pendingMinMs?: number staleTime?: number gcTime?: number + preload?: boolean preloadStaleTime?: number preloadGcTime?: number // Filter functions that can manipulate search params *before* they are passed to links and navigate @@ -254,8 +324,12 @@ export type UpdatableRouteOptions< TAllParams, ResolveFullSearchSchema, TLoaderData, - TAllContext, - TRouteContext, + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + >, TLoaderDeps >, ) => void @@ -265,8 +339,12 @@ export type UpdatableRouteOptions< TAllParams, ResolveFullSearchSchema, TLoaderData, - TAllContext, - TRouteContext, + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + >, TLoaderDeps >, ) => void @@ -276,8 +354,12 @@ export type UpdatableRouteOptions< TAllParams, ResolveFullSearchSchema, TLoaderData, - TAllContext, - TRouteContext, + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + >, TLoaderDeps >, ) => void @@ -288,8 +370,12 @@ export type UpdatableRouteOptions< TAllParams, ResolveFullSearchSchema, TLoaderData, - TAllContext, - TRouteContext, + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + >, TLoaderDeps > > @@ -298,8 +384,12 @@ export type UpdatableRouteOptions< TAllParams, ResolveFullSearchSchema, TLoaderData, - TAllContext, - TRouteContext, + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + >, TLoaderDeps > params: TAllParams @@ -372,24 +462,44 @@ export type DefaultSearchValidator = SearchValidator< > export type RouteLoaderFn< - in out TAllParams = {}, - in out TLoaderDeps extends Record = {}, - in out TAllContext = AnyContext, + in out TParentRoute extends AnyRoute = AnyRoute, + in out TParams = {}, + in out TLoaderDeps = {}, + in out TRouterContext = {}, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, TLoaderData = undefined, > = ( - match: LoaderFnContext, + match: LoaderFnContext< + TParentRoute, + TParams, + TLoaderDeps, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + >, ) => TLoaderData | Promise export interface LoaderFnContext< - in out TAllParams = {}, + in out TParentRoute extends AnyRoute = AnyRoute, + in out TParams = {}, in out TLoaderDeps = {}, - in out TAllContext = AnyContext, + in out TRouterContext = {}, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, > { abortController: AbortController preload: boolean - params: Expand + params: Expand> deps: TLoaderDeps - context: TAllContext + context: Expand< + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + > + > location: ParsedLocation // Do not supply search schema here so as to demotivate people from trying to shortcut loaderDeps /** * @deprecated Use `throw redirect({ to: '/somewhere' })` instead @@ -434,13 +544,15 @@ export type InferAllParams = TRoute extends { ? TAllParams : {} -export type InferAllContext = TRoute extends { - types: { - allContext: infer TAllContext - } -} - ? TAllContext - : {} +export type InferAllContext = unknown extends TRoute + ? TRoute + : TRoute extends { + types: { + allContext: infer TAllContext + } + } + ? TAllContext + : {} export type ResolveSearchSchemaFnInput< TSearchValidator extends AnySearchValidator, @@ -490,16 +602,61 @@ export type ResolveFullSearchSchemaInput< ResolveSearchSchemaInput > -export type ResolveRouteContext = [ - TRouteContextReturn, -] extends [never] - ? RouteContext - : TRouteContextReturn +export type LooseReturnType = T extends ( + ...args: Array +) => infer TReturn + ? TReturn + : never + +export type LooseAsyncReturnType = T extends ( + ...args: Array +) => infer TReturn + ? TReturn extends Promise + ? TReturn + : TReturn + : never + +export type ContextReturnType = unknown extends TContextFn + ? TContextFn + : LooseReturnType extends never + ? AnyContext + : LooseReturnType + +export type ContextAsyncReturnType = unknown extends TContextFn + ? TContextFn + : LooseAsyncReturnType extends never + ? AnyContext + : LooseAsyncReturnType + +export type RouteContextParameter< + TParentRoute extends AnyRoute, + TRouterContext, +> = unknown extends TParentRoute + ? TRouterContext + : Assign> + +export type ResolveRouteContext = Assign< + ContextReturnType, + ContextAsyncReturnType +> +export type BeforeLoadContextParameter< + TParentRoute extends AnyRoute, + TRouterContext, + TRouteContextFn, +> = Assign< + RouteContextParameter, + ContextReturnType +> export type ResolveAllContext< TParentRoute extends AnyRoute, - TRouteContext, -> = Assign, TRouteContext> + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, +> = Assign< + BeforeLoadContextParameter, + ContextAsyncReturnType +> export type ResolveLoaderData = [TLoaderDataReturn] extends [ never, @@ -526,23 +683,9 @@ export interface AnyRoute any > {} -export type AnyRouteWithContext = Route< - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - TContext, - any, - any -> +export type AnyRouteWithContext = AnyRoute & { + types: { allContext: TContext } +} export type ResolveAllParamsFromParent< TParentRoute extends AnyRoute, @@ -676,9 +819,9 @@ export class Route< in out TSearchValidator extends AnySearchValidator = DefaultSearchValidator, in out TParams = Record, string>, in out TAllParams = ResolveAllParamsFromParent, - TRouteContextReturn = RouteContext, - in out TRouteContext = ResolveRouteContext, - in out TAllContext = ResolveAllContext, + in out TRouterContext = AnyContext, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, in out TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, in out TLoaderData = ResolveLoaderData, @@ -692,13 +835,12 @@ export class Route< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - InferAllContext, - TAllContext, TLoaderDeps, TLoaderDataReturn, - TLoaderData + TLoaderData, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn > // Set up in this.init() @@ -728,13 +870,12 @@ export class Route< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - InferAllContext, - TAllContext, TLoaderDeps, TLoaderDataReturn, - TLoaderData + TLoaderData, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn >, ) { this.options = (options as any) || {} @@ -764,8 +905,16 @@ export class Route< > params: TParams allParams: TAllParams - routeContext: TRouteContext - allContext: TAllContext + routerContext: TRouterContext + routeContext: ResolveRouteContext + routeContextFn: TRouteContextFn + beforeLoadFn: TBeforeLoadFn + allContext: ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + > children: TChildren loaderData: TLoaderData loaderDeps: TLoaderDeps @@ -782,13 +931,12 @@ export class Route< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - InferAllContext, - TAllContext, TLoaderDeps, TLoaderDataReturn, - TLoaderData + TLoaderData, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn > & RoutePathOptionsIntersection) | undefined @@ -796,7 +944,7 @@ export class Route< const isRoot = !options?.path && !options?.id // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.parentRoute = this.options?.getParentRoute?.() + this.parentRoute = this.options.getParentRoute?.() if (isRoot) { this.path = rootRouteId as TPath @@ -857,9 +1005,9 @@ export class Route< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - TAllContext, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData, @@ -872,7 +1020,15 @@ export class Route< } updateLoader = (options: { - loader: RouteLoaderFn + loader: RouteLoaderFn< + TParentRoute, + TParams, + TLoaderDeps, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, + TNewLoaderData + > }) => { Object.assign(this.options, options) return this as unknown as Route< @@ -884,9 +1040,9 @@ export class Route< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - TAllContext, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TNewLoaderData, TChildren @@ -900,9 +1056,10 @@ export class Route< TAllParams, TSearchValidator, TLoaderData, - TAllContext, - TRouteContext, - TLoaderDeps + TLoaderDeps, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn >, ): this => { Object.assign(this.options, options) @@ -925,8 +1082,26 @@ export class Route< return useMatch({ ...opts, from: this.id }) } - useRouteContext = >(opts?: { - select?: (search: Expand) => TSelected + useRouteContext = < + TSelected = Expand< + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + > + >, + >(opts?: { + select?: ( + search: Expand< + ResolveAllContext< + TParentRoute, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn + > + >, + ) => TSelected }): TSelected => { return useMatch({ ...opts, @@ -984,9 +1159,8 @@ export function createRoute< TSearchValidator extends AnySearchValidator = DefaultSearchValidator, TParams = Record, string>, TAllParams = ResolveAllParamsFromParent, - TRouteContextReturn = RouteContext, - TRouteContext = ResolveRouteContext, - TAllContext = ResolveAllContext, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, TLoaderData = ResolveLoaderData, @@ -999,13 +1173,12 @@ export function createRoute< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - InferAllContext, - TAllContext, TLoaderDeps, TLoaderDataReturn, - TLoaderData + TLoaderData, + AnyContext, + TRouteContextFn, + TBeforeLoadFn >, ) { return new Route< @@ -1017,9 +1190,9 @@ export function createRoute< TSearchValidator, TParams, TAllParams, - TRouteContextReturn, - TRouteContext, - TAllContext, + AnyContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData, @@ -1031,9 +1204,9 @@ export type AnyRootRoute = RootRoute export type RootRouteOptions< TSearchValidator extends AnySearchValidator = DefaultSearchValidator, - TRouteContextReturn = RouteContext, - TRouteContext = ResolveRouteContext, TRouterContext = {}, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, TLoaderData = ResolveLoaderData, @@ -1045,13 +1218,12 @@ export type RootRouteOptions< TSearchValidator, {}, // TParams {}, // TAllParams - TRouteContextReturn, // TRouteContextReturn - TRouteContext, // TRouteContext - TRouterContext, // TParentAllContext - Assign, // TAllContext TLoaderDeps, TLoaderDataReturn, // TLoaderDataReturn, - TLoaderData // TLoaderData, + TLoaderData, // TLoaderData, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn >, | 'path' | 'id' @@ -1064,19 +1236,18 @@ export type RootRouteOptions< export function createRootRouteWithContext() { return < + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, TSearchValidator extends AnySearchValidator = DefaultSearchValidator, - TRouteContextReturn extends RouteContext = RouteContext, - TRouteContext extends - RouteContext = ResolveRouteContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, TLoaderData = ResolveLoaderData, >( options?: RootRouteOptions< TSearchValidator, - TRouteContextReturn, - TRouteContext, TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData @@ -1084,9 +1255,9 @@ export function createRootRouteWithContext() { ) => { return createRootRoute< TSearchValidator, - TRouteContextReturn, - TRouteContext, TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderData >(options as any) @@ -1100,9 +1271,9 @@ export const rootRouteWithContext = createRootRouteWithContext export class RootRoute< in out TSearchValidator extends AnySearchValidator = DefaultSearchValidator, - TRouteContextReturn = RouteContext, - in out TRouteContext = ResolveRouteContext, in out TRouterContext = {}, + in out TRouteContextFn = AnyContext, + in out TBeforeLoadFn = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, in out TLoaderData = ResolveLoaderData, @@ -1116,9 +1287,9 @@ export class RootRoute< TSearchValidator, // TSearchValidator {}, // TParams {}, // TAllParams - TRouteContextReturn, // TRouteContextReturn - TRouteContext, // TRouteContext - Assign, // TAllContext + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData, @@ -1130,9 +1301,9 @@ export class RootRoute< constructor( options?: RootRouteOptions< TSearchValidator, - TRouteContextReturn, - TRouteContext, TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData @@ -1149,9 +1320,9 @@ export class RootRoute< children: TNewChildren, ): RootRoute< TSearchValidator, - TRouteContextReturn, - TRouteContext, TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData, @@ -1163,18 +1334,18 @@ export class RootRoute< export function createRootRoute< TSearchValidator extends AnySearchValidator = DefaultSearchValidator, - TRouteContextReturn = RouteContext, - TRouteContext = ResolveRouteContext, TRouterContext = {}, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, TLoaderData = ResolveLoaderData, >( options?: RootRouteOptions< TSearchValidator, - TRouteContextReturn, - TRouteContext, TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData @@ -1182,9 +1353,9 @@ export function createRootRoute< ) { return new RootRoute< TSearchValidator, - TRouteContextReturn, - TRouteContext, TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData @@ -1289,10 +1460,10 @@ export type NotFoundRouteComponent = SyncRouteComponent export class NotFoundRoute< TParentRoute extends AnyRootRoute, + TRouterContext = AnyContext, + TRouteContextFn = AnyContext, + TBeforeLoadFn = AnyContext, TSearchValidator extends AnySearchValidator = DefaultSearchValidator, - TRouteContextReturn = AnyContext, - TRouteContext = RouteContext, - TAllContext = ResolveAllContext, TLoaderDeps extends Record = {}, TLoaderDataReturn = {}, TLoaderData = ResolveLoaderData, @@ -1306,9 +1477,9 @@ export class NotFoundRoute< TSearchValidator, {}, {}, - TRouteContextReturn, - TRouteContext, - TAllContext, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn, TLoaderDeps, TLoaderDataReturn, TLoaderData, @@ -1323,13 +1494,12 @@ export class NotFoundRoute< TSearchValidator, {}, {}, - TRouteContextReturn, - TRouteContext, - InferAllContext, - TAllContext, TLoaderDeps, TLoaderDataReturn, - TLoaderData + TLoaderData, + TRouterContext, + TRouteContextFn, + TBeforeLoadFn >, | 'caseSensitive' | 'parseParams' diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 859b4a48094..702adc475cc 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -1,4 +1,8 @@ -import { createBrowserHistory, createMemoryHistory } from '@tanstack/history' +import { + createBrowserHistory, + createMemoryHistory, + parseHref, +} from '@tanstack/history' import { Store } from '@tanstack/react-store' import invariant from 'tiny-invariant' import warning from 'tiny-warning' @@ -25,6 +29,7 @@ import { } from './path' import { isRedirect, isResolvedRedirect } from './redirects' import { isNotFound } from './not-found' +import { defaultTransformer } from './transformer' import type * as React from 'react' import type { HistoryLocation, @@ -37,12 +42,13 @@ import type { AnyContext, AnyRoute, AnyRouteWithContext, - AnySearchSchema, + BeforeLoadContextOptions, ErrorRouteComponent, LoaderFnContext, NotFoundRouteComponent, RootRoute, RouteComponent, + RouteContextOptions, RouteMask, } from './route' import type { @@ -73,13 +79,18 @@ import type { import type { AnyRedirect, ResolvedRedirect } from './redirects' import type { NotFoundError } from './not-found' import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link' +import type { RouterTransformer } from './transformer' // declare global { interface Window { __TSR__?: { - matches: Array + matches: Array<{ + __beforeLoadContext?: string + loaderData?: string + extracted?: Array + }> streamedValues: Record< string, { @@ -121,9 +132,9 @@ export type HydrationCtx = { export type InferRouterContext = TRouteTree extends RootRoute< any, + infer TRouterContext extends AnyContext, any, any, - infer TRouterContext extends AnyContext, any, any, any, @@ -132,6 +143,20 @@ export type InferRouterContext = ? TRouterContext : AnyContext +export type ExtractedEntry = { + dataType: '__beforeLoadContext' | 'loaderData' + type: 'promise' | 'stream' + path: Array + value: any + id: number + streamState?: StreamState + matchIndex: number +} + +export type StreamState = { + promises: Array> +} + export type RouterContextOptions = AnyContext extends InferRouterContext ? { @@ -428,10 +453,6 @@ export interface RouterOptions< isServer?: boolean } -export interface RouterTransformer { - stringify: (obj: unknown) => string - parse: (str: string) => unknown -} export interface RouterErrorSerializer { serialize: (err: unknown) => TSerializedError deserialize: (err: TSerializedError) => unknown @@ -591,7 +612,8 @@ export class Router< matchIndex: number }) => any serializeLoaderData?: ( - data: any, + type: '__beforeLoadContext' | 'loaderData', + loaderData: any, ctx: { router: AnyRouter match: AnyRouteMatch @@ -644,10 +666,7 @@ export class Router< notFoundMode: options.notFoundMode ?? 'fuzzy', stringifySearch: options.stringifySearch ?? defaultStringifySearch, parseSearch: options.parseSearch ?? defaultParseSearch, - transformer: options.transformer ?? { - parse: JSON.parse, - stringify: JSON.stringify, - }, + transformer: options.transformer ?? defaultTransformer, }) if (typeof document !== 'undefined') { @@ -932,8 +951,7 @@ export class Router< } matchRoutes = ( - pathname: string, - locationSearch: AnySearchSchema, + next: ParsedLocation, opts?: { preload?: boolean; throwOnError?: boolean }, ): Array => { let routeParams: Record = {} @@ -941,7 +959,7 @@ export class Router< const foundRoute = this.flatRoutes.find((route) => { const matchedParams = matchPathname( this.basepath, - trimPathRight(pathname), + trimPathRight(next.pathname), { to: route.fullPath, caseSensitive: @@ -971,7 +989,7 @@ export class Router< foundRoute ? foundRoute.path !== '/' && routeParams['**'] : // Or if we didn't find a route and we have left over path - trimPathRight(pathname) + trimPathRight(next.pathname) ) { // If the user has defined an (old) 404 route, use it if (this.options.notFoundRoute) { @@ -1048,7 +1066,7 @@ export class Router< const [preMatchSearch, searchError]: [Record, any] = (() => { // Validate the search params and stabilize them - const parentSearch = parentMatch?.search ?? locationSearch + const parentSearch = parentMatch?.search ?? next.search try { const validator = @@ -1138,8 +1156,9 @@ export class Router< isFetching: false, error: undefined, paramsError: parseErrors[index], - routeContext: undefined!, - context: undefined!, + __routeContext: {}, + __beforeLoadContext: {}, + context: {}, abortController: new AbortController(), fetchCount: 0, cause, @@ -1180,6 +1199,41 @@ export class Router< // And also update the searchError if there is one match.searchError = searchError + const parentMatchId = parentMatch?.id + + const parentContext = !parentMatchId + ? ((this.options.context as any) ?? {}) + : (parentMatch.context ?? this.options.context ?? {}) + + match.context = { + ...parentContext, + ...match.__routeContext, + ...match.__beforeLoadContext, + } + + // Update the match's context + const contextFnContext: RouteContextOptions = { + search: match.search, + params: match.params, + context: match.context, + location: next, + navigate: (opts: any) => + this.navigate({ ...opts, _fromLocation: next }), + buildLocation: this.buildLocation, + cause: match.cause, + abortController: match.abortController, + preload: !!match.preload, + } + + // Get the route context + match.__routeContext = route.options.context?.(contextFnContext) ?? {} + + match.context = { + ...parentContext, + ...match.__routeContext, + ...match.__beforeLoadContext, + } + matches.push(match) }) @@ -1210,10 +1264,10 @@ export class Router< ): ParsedLocation => { const fromMatches = dest._fromLocation != null - ? this.matchRoutes( - dest._fromLocation.pathname, - dest.fromSearch || dest._fromLocation.search, - ) + ? this.matchRoutes({ + ...dest._fromLocation, + search: dest.fromSearch || dest._fromLocation.search, + }) : this.state.matches const fromMatch = @@ -1389,9 +1443,9 @@ export class Router< } } - const nextMatches = this.matchRoutes(next.pathname, next.search) + const nextMatches = this.matchRoutes(next) const maskedMatches = maskedNext - ? this.matchRoutes(maskedNext.pathname, maskedNext.search) + ? this.matchRoutes(maskedNext) : undefined const maskedFinal = maskedNext ? build(maskedDest, maskedMatches) @@ -1501,6 +1555,14 @@ export class Router< ignoreBlocker, ...rest }: BuildNextOptions & CommitLocationOptions = {}) => { + const href = (rest as any).href + if (href) { + const parsed = parseHref(href, {}) + rest.to = parsed.pathname + rest.search = this.options.parseSearch(parsed.search) + rest.hash = parsed.hash + } + const location = this.buildLocation(rest as any) return this.commitLocation({ ...location, @@ -1511,14 +1573,13 @@ export class Router< }) } - navigate: NavigateFn = ({ from, to, __isRedirect, ...rest }) => { + navigate: NavigateFn = ({ to, __isRedirect, ...rest }) => { // If this link simply reloads the current route, // make sure it has a new key so it will trigger a data refresh // If this `to` is a valid external URL, return // null for LinkUtils const toString = String(to) - // const fromString = from !== undefined ? String(from) : from let isExternal try { @@ -1533,7 +1594,6 @@ export class Router< return this.buildAndCommitLocation({ ...rest, - from, to, // to: toString, }) @@ -1552,7 +1612,10 @@ export class Router< let redirect: ResolvedRedirect | undefined let notFound: NotFoundError | undefined - const loadPromise = new Promise((resolve) => { + let loadPromise: Promise + + // eslint-disable-next-line prefer-const + loadPromise = new Promise((resolve) => { this.startReactTransition(async () => { try { const next = this.latestLocation @@ -1570,7 +1633,7 @@ export class Router< // this.cleanCache() // Match the routes - pendingMatches = this.matchRoutes(next.pathname, next.search) + pendingMatches = this.matchRoutes(next) // Ingest the new matches this.__store.setState((s) => ({ @@ -1871,6 +1934,7 @@ export class Router< for (const [index, { id: matchId, routeId }] of matches.entries()) { const existingMatch = this.getMatch(matchId)! + const parentMatchId = matches[index - 1]?.id if ( // If we are in the middle of a load, either of these will be present @@ -1894,20 +1958,6 @@ export class Router< const route = this.looseRoutesById[routeId]! const abortController = new AbortController() - const parentMatchId = matches[index - 1]?.id - - const getParentContext = () => { - if (!parentMatchId) { - return (this.options.context as any) ?? {} - } - - return ( - this.getMatch(parentMatchId)!.context ?? - this.options.context ?? - {} - ) - } - const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs @@ -1946,30 +1996,39 @@ export class Router< handleSerialError(index, searchError, 'VALIDATE_SEARCH') } - const parentContext = getParentContext() + const getParentMatchContext = () => + parentMatchId + ? this.getMatch(parentMatchId)!.context + : (this.options.context ?? {}) updateMatch(matchId, (prev) => ({ ...prev, isFetching: 'beforeLoad', fetchCount: prev.fetchCount + 1, - routeContext: replaceEqualDeep( - prev.routeContext, - parentContext, - ), - context: replaceEqualDeep(prev.context, parentContext), abortController, pendingTimeout, + context: { + ...getParentMatchContext(), + ...prev.__routeContext, + ...prev.__beforeLoadContext, + }, })) - const { search, params, routeContext, cause } = + const { search, params, context, cause } = this.getMatch(matchId)! - const beforeLoadFnContext = { + const beforeLoadFnContext: BeforeLoadContextOptions< + any, + any, + any, + any, + any + > = { search, abortController, params, preload: !!preload, - context: routeContext, + context, location, navigate: (opts: any) => this.navigate({ ...opts, _fromLocation: location }), @@ -1977,10 +2036,21 @@ export class Router< cause: preload ? 'preload' : cause, } - const beforeLoadContext = + let beforeLoadContext = (await route.options.beforeLoad?.(beforeLoadFnContext)) ?? {} + if (this.serializeLoaderData) { + beforeLoadContext = this.serializeLoaderData( + '__beforeLoadContext', + beforeLoadContext, + { + router: this, + match: this.getMatch(matchId)!, + }, + ) + } + if ( isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext) @@ -1989,18 +2059,14 @@ export class Router< } updateMatch(matchId, (prev) => { - const routeContext = { - ...prev.routeContext, - ...beforeLoadContext, - } - return { ...prev, - routeContext: replaceEqualDeep( - prev.routeContext, - routeContext, - ), - context: replaceEqualDeep(prev.context, routeContext), + __beforeLoadContext: beforeLoadContext, + context: { + ...getParentMatchContext(), + ...prev.__routeContext, + ...beforeLoadContext, + }, abortController, } }) @@ -2150,10 +2216,14 @@ export class Router< await route.options.loader?.(getLoaderContext()) if (this.serializeLoaderData) { - loaderData = this.serializeLoaderData(loaderData, { - router: this, - match: this.getMatch(matchId)!, - }) + loaderData = this.serializeLoaderData( + 'loaderData', + loaderData, + { + router: this, + match: this.getMatch(matchId)!, + }, + ) } handleRedirectAndNotFound( @@ -2220,7 +2290,9 @@ export class Router< // If the route is successful and still fresh, just resolve const { status, invalid } = this.getMatch(matchId)! - if ( + if (preload && route.options.preload === false) { + // Do nothing + } else if ( status === 'success' && (invalid || (shouldReload ?? age > staleAge)) ) { @@ -2342,7 +2414,7 @@ export class Router< ): Promise | undefined> => { const next = this.buildLocation(opts as any) - let matches = this.matchRoutes(next.pathname, next.search, { + let matches = this.matchRoutes(next, { throwOnError: true, preload: true, }) @@ -2499,10 +2571,7 @@ export class Router< this.options.hydrate?.(ctx.payload as any) const dehydratedState = ctx.router.state - const matches = this.matchRoutes( - this.state.location.pathname, - this.state.location.search, - ).map((match) => { + const matches = this.matchRoutes(this.state.location).map((match) => { const dehydratedMatch = dehydratedState.dehydratedMatches.find( (d) => d.id === match.id, ) diff --git a/packages/start/src/client/defaultTransformer.ts b/packages/react-router/src/transformer.ts similarity index 84% rename from packages/start/src/client/defaultTransformer.ts rename to packages/react-router/src/transformer.ts index 59fd234e3ea..2277a3dbb8d 100644 --- a/packages/start/src/client/defaultTransformer.ts +++ b/packages/react-router/src/transformer.ts @@ -1,6 +1,11 @@ -import { isPlainObject } from '@tanstack/react-router' +import { isPlainObject } from './utils' -export const defaultTransformer = { +export interface RouterTransformer { + stringify: (obj: unknown) => string + parse: (str: string) => unknown +} + +export const defaultTransformer: RouterTransformer = { stringify: (value: any) => JSON.stringify(value, function replacer(key, value) { const keyVal = this[key] diff --git a/packages/react-router/src/utils.ts b/packages/react-router/src/utils.ts index 907f2f09059..ed10becd34e 100644 --- a/packages/react-router/src/utils.ts +++ b/packages/react-router/src/utils.ts @@ -4,6 +4,7 @@ export type NoInfer = [T][T extends any ? 0 : never] export type IsAny = 1 extends 0 & TValue ? TYesResult : TNoResult + export type PickAsRequired = Omit< TValue, TKey @@ -98,6 +99,10 @@ export type MergeUnion = | MergeUnionPrimitives | MergeUnionObject +export type Constrain = + | (T extends TConstaint ? T : never) + | TConstaint + export function last(arr: Array) { return arr[arr.length - 1] } diff --git a/packages/react-router/tests/route.test-d.tsx b/packages/react-router/tests/route.test-d.tsx index 4d8da3bbd63..94ea908aad7 100644 --- a/packages/react-router/tests/route.test-d.tsx +++ b/packages/react-router/tests/route.test-d.tsx @@ -4,8 +4,18 @@ import { createRootRouteWithContext, createRoute, createRouter, + redirect, +} from '../src' +import type { + AnyRouter, + BuildLocationFn, + ControlledPromise, + NavigateFn, + NavigateOptions, + ParsedLocation, + Route, + SearchSchemaInput, } from '../src' -import type { ControlledPromise, SearchSchemaInput } from '../src' test('when creating the root', () => { const rootRoute = createRootRoute() @@ -15,10 +25,42 @@ test('when creating the root', () => { expectTypeOf(rootRoute.path).toEqualTypeOf<'/'>() }) +test('when creating the root with routeContext', () => { + const rootRoute = createRootRoute({ + context: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: {} + search: {} + }>() + }, + }) + + expectTypeOf(rootRoute.fullPath).toEqualTypeOf<'/'>() + expectTypeOf(rootRoute.id).toEqualTypeOf<'__root__'>() + expectTypeOf(rootRoute.path).toEqualTypeOf<'/'>() +}) + test('when creating the root with beforeLoad', () => { const rootRoute = createRootRoute({ beforeLoad: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ context: {} }>() + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: {} + search: {} + }>() }, }) @@ -30,7 +72,18 @@ test('when creating the root with beforeLoad', () => { test('when creating the root with a loader', () => { const rootRoute = createRootRoute({ loader: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ context: {} }>() + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + deps: {} + context: {} + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route + }>() }, }) @@ -39,12 +92,22 @@ test('when creating the root with a loader', () => { expectTypeOf(rootRoute.path).toEqualTypeOf<'/'>() }) -test('when creating the root route with context and a loader', () => { +test('when creating the root route with context and routeContext', () => { const createRouteResult = createRootRouteWithContext<{ userId: string }>() const rootRoute = createRouteResult({ - loader: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ context: { userId: string } }>() + context: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: {} + }>() }, }) @@ -68,7 +131,52 @@ test('when creating the root route with context and beforeLoad', () => { const rootRoute = createRouteResult({ beforeLoad: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ context: { userId: string } }>() + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: {} + }>() + }, + }) + + expectTypeOf(rootRoute.fullPath).toEqualTypeOf<'/'>() + expectTypeOf(rootRoute.id).toEqualTypeOf<'__root__'>() + expectTypeOf(rootRoute.path).toEqualTypeOf<'/'>() + + expectTypeOf(rootRoute.useRouteContext()).toEqualTypeOf<{ + userId: string + }>() + + expectTypeOf(rootRoute.useRouteContext) + .parameter(0) + .toEqualTypeOf< + { select?: (search: { userId: string }) => string } | undefined + >() +}) + +test('when creating the root route with context and a loader', () => { + const createRouteResult = createRootRouteWithContext<{ userId: string }>() + + const rootRoute = createRouteResult({ + loader: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + deps: {} + context: { userId: string } + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route + }>() }, }) @@ -87,17 +195,53 @@ test('when creating the root route with context and beforeLoad', () => { >() }) -test('when creating the root route with context, beforeLoad and a loader', () => { +test('when creating the root route with context, routeContext, beforeLoad and a loader', () => { const createRouteResult = createRootRouteWithContext<{ userId: string }>() const rootRoute = createRouteResult({ + context: (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: {} + }>() + + return { + env: 'env1' as const, + } + }, beforeLoad: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ context: { userId: string } }>() - return { permission: 'view' } as const + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string; env: 'env1' } + search: {} + }>() + return { permission: 'view' as const } }, loader: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ - context: { userId: string; permission: 'view' } + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + deps: {} + context: { userId: string; permission: 'view'; env: 'env1' } + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route }>() }, }) @@ -108,7 +252,8 @@ test('when creating the root route with context, beforeLoad and a loader', () => expectTypeOf(rootRoute.useRouteContext()).toEqualTypeOf<{ userId: string - readonly permission: 'view' + permission: 'view' + env: 'env1' }>() expectTypeOf(rootRoute.useRouteContext) @@ -117,7 +262,8 @@ test('when creating the root route with context, beforeLoad and a loader', () => | { select?: (search: { userId: string - readonly permission: 'view' + permission: 'view' + env: 'env1' }) => string } | undefined @@ -156,6 +302,54 @@ test('when creating a child route from the root route with context', () => { >() }) +test('when creating a child route with routeContext from the root route with context', () => { + const rootRoute = createRootRouteWithContext<{ userId: string }>()() + + createRoute({ + path: 'invoices', + getParentRoute: () => rootRoute, + context: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: {} + }>() + + return { + env: 'env1' as const, + } + }, + }) +}) + +test('when creating a child route with beforeLoad from the root route with context', () => { + const rootRoute = createRootRouteWithContext<{ userId: string }>()() + + createRoute({ + path: 'invoices', + getParentRoute: () => rootRoute, + beforeLoad: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: {} + }>() + }, + }) +}) + test('when creating a child route with a loader from the root route', () => { const rootRoute = createRootRoute() @@ -163,7 +357,18 @@ test('when creating a child route with a loader from the root route', () => { path: 'invoices', getParentRoute: () => rootRoute, loader: async (opt) => { - expectTypeOf(opt).toMatchTypeOf<{ context: {} }>() + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + deps: {} + context: {} + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route + }>() return [{ id: 'invoice1' }, { id: 'invoice2' }] as const }, }) @@ -187,19 +392,6 @@ test('when creating a child route with a loader from the root route', () => { >() }) -test('when creating a child route with beforeLoad from the root route with context', () => { - const rootRoute = createRootRouteWithContext<{ userId: string }>()() - - createRoute({ - path: 'invoices', - getParentRoute: () => rootRoute, - beforeLoad: async (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ context: { userId: string } }>() - return [{ id: 'invoice1' }, { id: 'invoice2' }] as const - }, - }) -}) - test('when creating a child route with a loader from the root route with context', () => { const rootRoute = createRootRouteWithContext<{ userId: string }>()() @@ -207,7 +399,18 @@ test('when creating a child route with a loader from the root route with context path: 'invoices', getParentRoute: () => rootRoute, loader: async (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ context: { userId: string } }>() + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + deps: {} + context: { userId: string } + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route + }>() return [{ id: 'invoice1' }, { id: 'invoice2' }] as const }, }) @@ -243,6 +446,7 @@ test('when creating a child route with search params from the root route', () => expectTypeOf(invoicesRoute.useSearch()).toEqualTypeOf<{ page: number }>() + expectTypeOf(invoicesRoute.useSearch) .parameter(0) .toEqualTypeOf< @@ -290,11 +494,21 @@ test('when creating a child route with params, search and loader from the root r createRoute({ path: 'invoices/$invoiceId', getParentRoute: () => rootRoute, - loader: (opts) => - expectTypeOf(opts).toMatchTypeOf<{ - params: { invoiceId: string } - }>, validateSearch: () => ({ page: 0 }), + loader: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: { invoiceId: string } + deps: {} + context: {} + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route + }> + }, }) }) @@ -304,13 +518,21 @@ test('when creating a child route with params, search, loader and loaderDeps fro createRoute({ path: 'invoices/$invoiceId', getParentRoute: () => rootRoute, + validateSearch: () => ({ page: 0 }), loaderDeps: (deps) => ({ page: deps.search.page }), loader: (opts) => - expectTypeOf(opts).toMatchTypeOf<{ + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean params: { invoiceId: string } deps: { page: number } + context: {} + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route }>(), - validateSearch: () => ({ page: 0 }), }) }) @@ -320,14 +542,44 @@ test('when creating a child route with params, search, loader and loaderDeps fro createRoute({ path: 'invoices/$invoiceId', getParentRoute: () => rootRoute, + validateSearch: () => ({ page: 0 }), loaderDeps: (deps) => ({ page: deps.search.page }), loader: (opts) => - expectTypeOf(opts).toMatchTypeOf<{ + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean params: { invoiceId: string } deps: { page: number } context: { userId: string } + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route }>(), + }) +}) + +test('when creating a child route with params, search with routeContext from the root route with context', () => { + const rootRoute = createRootRouteWithContext<{ userId: string }>()() + + createRoute({ + path: 'invoices/$invoiceId', + getParentRoute: () => rootRoute, validateSearch: () => ({ page: 0 }), + context: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: { invoiceId: string } + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: { page: number } + }>() + }, }) }) @@ -338,34 +590,71 @@ test('when creating a child route with params, search with beforeLoad from the r path: 'invoices/$invoiceId', getParentRoute: () => rootRoute, validateSearch: () => ({ page: 0 }), - beforeLoad: (opts) => + beforeLoad: (opts) => { expectTypeOf(opts).toMatchTypeOf<{ + abortController: AbortController + preload: boolean params: { invoiceId: string } + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' context: { userId: string } search: { page: number } - }>(), + }>() + }, }) }) -test('when creating a child route with params, search with beforeLoad and a loader from the root route with context', () => { +test('when creating a child route with params, search with routeContext, beforeLoad and a loader from the root route with context', () => { const rootRoute = createRootRouteWithContext<{ userId: string }>()() createRoute({ path: 'invoices/$invoiceId', getParentRoute: () => rootRoute, validateSearch: () => ({ page: 0 }), - beforeLoad: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ + context: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean params: { invoiceId: string } + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' context: { userId: string } search: { page: number } }>() + return { + env: 'env1', + } + }, + beforeLoad: (opts) => { + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: { invoiceId: string } + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string; env: string } + search: { page: number } + }>() return { permission: 'view' } as const }, loader: (opts) => { - expectTypeOf(opts).toMatchTypeOf<{ + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean params: { invoiceId: string } - context: { userId: string; permission: 'view' } + deps: {} + context: { userId: string; env: string; readonly permission: 'view' } + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route }>() }, }) @@ -432,19 +721,47 @@ test('when creating a child route with search from a parent with search', () => >() }) -test('when creating a child route with context from a parent with context', () => { +test('when creating a child route with routeContext from a parent with routeContext', () => { const rootRoute = createRootRouteWithContext<{ userId: string }>()() const invoicesRoute = createRoute({ path: 'invoices', getParentRoute: () => rootRoute, - beforeLoad: async () => ({ invoiceId: 'invoiceId1' }), + context: (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: {} + }>() + + return { invoiceId: 'invoiceId1' } + }, }) const detailsRoute = createRoute({ path: 'details', getParentRoute: () => invoicesRoute, - beforeLoad: async () => ({ detailId: 'detailId1' }), + context: (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string; invoiceId: string } + search: {} + }>() + + return { detailId: 'detailId1' } + }, }) expectTypeOf(detailsRoute.useRouteContext()).toEqualTypeOf<{ @@ -467,14 +784,102 @@ test('when creating a child route with context from a parent with context', () = >() }) -test('when creating a child route with context, search, params, loaderDeps and loader', () => { +test('when creating a child route with beforeLoad from a parent with beforeLoad', () => { + const rootRoute = createRootRouteWithContext<{ userId: string }>()() + + const invoicesRoute = createRoute({ + path: 'invoices', + getParentRoute: () => rootRoute, + beforeLoad: async (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: {} + }>() + return { invoiceId: 'invoiceId1' } + }, + }) + + const detailsRoute = createRoute({ + path: 'details', + getParentRoute: () => invoicesRoute, + beforeLoad: async (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string; invoiceId: string } + search: {} + }>() + return { detailId: 'detailId1' } + }, + }) + + expectTypeOf(detailsRoute.useRouteContext()).toEqualTypeOf<{ + userId: string + invoiceId: string + detailId: string + }>() + + expectTypeOf(detailsRoute.useRouteContext) + .parameter(0) + .toEqualTypeOf< + | { + select?: (search: { + userId: string + invoiceId: string + detailId: string + }) => string + } + | undefined + >() +}) + +test('when creating a child route with routeContext, beforeLoad, search, params, loaderDeps and loader', () => { const rootRoute = createRootRouteWithContext<{ userId: string }>()() const invoicesRoute = createRoute({ path: 'invoices', getParentRoute: () => rootRoute, validateSearch: () => ({ page: 0 }), - beforeLoad: () => ({ invoicePermissions: ['view'] as const }), + context: (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string } + search: { page: number } + }>() + return { env: 'env1' } + }, + beforeLoad: (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: {} + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { userId: string; env: string } + search: { page: number } + }>() + return { invoicePermissions: ['view'] as const } + }, }) const invoiceRoute = createRoute({ @@ -486,7 +891,43 @@ test('when creating a child route with context, search, params, loaderDeps and l path: 'details', getParentRoute: () => invoiceRoute, validateSearch: () => ({ detailPage: 0 }), - beforeLoad: () => ({ detailsPermissions: ['view'] as const }), + context: (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: { invoiceId: string } + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { + userId: string + env: string + invoicePermissions: readonly ['view'] + } + search: { page: number; detailPage: number } + }>() + return { detailEnv: 'detailEnv' } + }, + beforeLoad: (opt) => { + expectTypeOf(opt).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: { invoiceId: string } + location: ParsedLocation + navigate: NavigateFn + buildLocation: BuildLocationFn + cause: 'preload' | 'enter' | 'stay' + context: { + detailEnv: string + userId: string + env: string + invoicePermissions: readonly ['view'] + } + search: { page: number; detailPage: number } + }>() + return { detailsPermissions: ['view'] as const } + }, }) const detailRoute = createRoute({ @@ -497,14 +938,23 @@ test('when creating a child route with context, search, params, loaderDeps and l invoicePage: deps.search.page, }), loader: (opts) => - expectTypeOf(opts).toMatchTypeOf<{ - params: { detailId: string; invoiceId: string } + expectTypeOf(opts).toEqualTypeOf<{ + abortController: AbortController + preload: boolean + params: { invoiceId: string; detailId: string } deps: { detailPage: number; invoicePage: number } context: { userId: string - detailsPermissions: readonly ['view'] + env: string invoicePermissions: readonly ['view'] + detailEnv: string + detailsPermissions: readonly ['view'] } + location: ParsedLocation + navigate: (opts: NavigateOptions) => Promise + parentMatchPromise?: Promise + cause: 'preload' | 'enter' | 'stay' + route: Route }>(), }) }) @@ -602,8 +1052,6 @@ test('when creating a child route with context, search, params, loader, loaderDe userId: string detailsPermissions: readonly ['view'] invoicePermissions: readonly ['view'] - } - type TExpectedRouteContext = { detailPermission: boolean } type TExpectedLoaderData = { detailLoader: 'detailResult' } @@ -616,7 +1064,6 @@ test('when creating a child route with context, search, params, loader, loaderDe loaderPromise?: ControlledPromise componentsPromise?: Promise> loaderData?: TExpectedLoaderData - routeContext: TExpectedRouteContext } createRoute({ @@ -885,6 +1332,32 @@ test('when creating a child route with params.parse and params.stringify with me }>() }) +test('when routeContext throws', () => { + const rootRoute = createRootRoute() + const invoicesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'invoices', + context: () => { + throw redirect({ to: '/somewhere' }) + }, + }) + + expectTypeOf(invoicesRoute.useRouteContext()).toEqualTypeOf<{}>() +}) + +test('when beforeLoad throws', () => { + const rootRoute = createRootRoute() + const invoicesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'invoices', + beforeLoad: () => { + throw redirect({ to: '/somewhere' }) + }, + }) + + expectTypeOf(invoicesRoute.useRouteContext()).toEqualTypeOf<{}>() +}) + test('when creating a child route with no explicit search input', () => { const rootRoute = createRootRoute({ validateSearch: (input) => { diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx index 0d1e7006218..40b4294ee9d 100644 --- a/packages/react-router/tests/routeContext.test.tsx +++ b/packages/react-router/tests/routeContext.test.tsx @@ -22,10 +22,340 @@ import { sleep } from './utils' afterEach(() => { window.history.replaceState(null, 'root', '/') cleanup() + vi.clearAllMocks() + vi.resetAllMocks() }) const WAIT_TIME = 150 +describe('context function', () => { + configure({ reactStrictMode: true }) + + describe('accessing values in the context function', () => { + test('receives an empty object', async () => { + const mockIndexContextFn = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => { + mockIndexContextFn(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree }) + + render() + + const rootElement = await screen.findByText('Index page') + expect(rootElement).toBeInTheDocument() + + expect(mockIndexContextFn).toHaveBeenCalledWith({}) + }) + + test('receives an empty object - with an empty object when creating the router', async () => { + const mockIndexContextFn = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => { + mockIndexContextFn(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: {} }) + + render() + + const rootElement = await screen.findByText('Index page') + expect(rootElement).toBeInTheDocument() + + expect(mockIndexContextFn).toHaveBeenCalledWith({}) + }) + + test('receives valid values - with values added when creating the router', async () => { + const mockIndexContextFn = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => { + mockIndexContextFn(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: { project: 'Router' } }) + + render() + + const rootElement = await screen.findByText('Index page') + expect(rootElement).toBeInTheDocument() + + expect(mockIndexContextFn).toHaveBeenCalledWith({ project: 'Router' }) + }) + + test('receives valid values when updating the values in the parent route to be read in the child route', async () => { + const mockIndexContextFn = vi.fn() + + const rootRoute = createRootRoute({ + context: () => ({ + project: 'Router', + }), + }) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => { + mockIndexContextFn(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree }) + + render() + + const rootElement = await screen.findByText('Index page') + expect(rootElement).toBeInTheDocument() + + expect(mockIndexContextFn).toHaveBeenCalledWith({ project: 'Router' }) + }) + }) + + describe('return values being available in beforeLoad', () => { + test('when returning an empty object in a regular route', async () => { + const mockIndexBeforeLoad = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: () => ({}), + beforeLoad: ({ context }) => { + mockIndexBeforeLoad(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: { project: 'foo' } }) + + render() + + const rootElement = await screen.findByText('Index page') + expect(rootElement).toBeInTheDocument() + + expect(mockIndexBeforeLoad).toHaveBeenCalledWith({ project: 'foo' }) + expect(mockIndexBeforeLoad).toHaveBeenCalledTimes(1) + }) + + test('when updating the initial router context value in a regular route', async () => { + const mockIndexBeforeLoad = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => ({ ...context, project: 'Query' }), + beforeLoad: ({ context }) => { + mockIndexBeforeLoad(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: { project: 'foo' } }) + + render() + + const indexElement = await screen.findByText('Index page') + expect(indexElement).toBeInTheDocument() + + expect(mockIndexBeforeLoad).toHaveBeenCalledWith({ project: 'Query' }) + expect(mockIndexBeforeLoad).toHaveBeenCalledTimes(1) + }) + + test('when returning an empty object in the root route', async () => { + const mock = vi.fn() + + const rootRoute = createRootRoute({ + context: () => ({}), + beforeLoad: ({ context }) => { + mock(context) + }, + component: () =>
Root page
, + }) + const routeTree = rootRoute.addChildren([]) + const router = createRouter({ routeTree, context: { project: 'foo' } }) + + render() + + const rootElement = await screen.findByText('Root page') + expect(rootElement).toBeInTheDocument() + + expect(mock).toHaveBeenCalledWith({ project: 'foo' }) + expect(mock).toHaveBeenCalledTimes(1) + }) + + test('when updating the initial router context value in the parent route and in the child route', async () => { + const mockRootBeforeLoad = vi.fn() + const mockIndexBeforeLoad = vi.fn() + + const rootRoute = createRootRoute({ + context: ({ context }) => ({ ...context, project: 'Router' }), + beforeLoad: ({ context }) => { + mockRootBeforeLoad(context) + }, + component: () => ( +
+ Root page +
+ ), + }) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => ({ ...context, project: 'Query' }), + beforeLoad: ({ context }) => { + mockIndexBeforeLoad(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: { project: 'bar' } }) + + render() + + const indexElement = await screen.findByText('Index page') + expect(indexElement).toBeInTheDocument() + + expect(mockRootBeforeLoad).toHaveBeenCalledWith({ project: 'Router' }) + expect(mockRootBeforeLoad).toHaveBeenCalledTimes(1) + + expect(mockIndexBeforeLoad).toHaveBeenCalledWith({ project: 'Query' }) + expect(mockIndexBeforeLoad).toHaveBeenCalledTimes(1) + }) + }) + + describe('return values being available in loader', () => { + test('when returning an empty object in a regular route', async () => { + const mockIndexLoader = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: () => ({}), + loader: ({ context }) => { + mockIndexLoader(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: { project: 'foo' } }) + + render() + + const rootElement = await screen.findByText('Index page') + expect(rootElement).toBeInTheDocument() + + expect(mockIndexLoader).toHaveBeenCalledWith({ project: 'foo' }) + expect(mockIndexLoader).toHaveBeenCalledTimes(1) + }) + + test('when updating the initial router context value in a regular route', async () => { + const mockIndexLoader = vi.fn() + + const rootRoute = createRootRoute() + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => ({ ...context, project: 'Query' }), + loader: ({ context }) => { + mockIndexLoader(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: { project: 'foo' } }) + + render() + + const indexElement = await screen.findByText('Index page') + expect(indexElement).toBeInTheDocument() + + expect(mockIndexLoader).toHaveBeenCalledWith({ project: 'Query' }) + expect(mockIndexLoader).toHaveBeenCalledTimes(1) + }) + + test('when returning an empty object in the root route', async () => { + const mockRootLoader = vi.fn() + + const rootRoute = createRootRoute({ + context: () => ({}), + loader: ({ context }) => { + mockRootLoader(context) + }, + component: () =>
Root page
, + }) + const routeTree = rootRoute.addChildren([]) + const router = createRouter({ routeTree, context: { project: 'foo' } }) + + render() + + const rootElement = await screen.findByText('Root page') + expect(rootElement).toBeInTheDocument() + + expect(mockRootLoader).toHaveBeenCalledWith({ project: 'foo' }) + expect(mockRootLoader).toHaveBeenCalledTimes(1) + }) + + test('when updating the initial router context value in the root route and in the child route', async () => { + const mockRootLoader = vi.fn() + const mockIndexLoader = vi.fn() + + const rootRoute = createRootRoute({ + context: ({ context }) => ({ ...context, project: 'Router' }), + loader: ({ context }) => { + mockRootLoader(context) + }, + component: () => ( +
+ Root page +
+ ), + }) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + context: ({ context }) => ({ ...context, project: 'Query' }), + loader: ({ context }) => { + mockIndexLoader(context) + }, + component: () =>
Index page
, + }) + const routeTree = rootRoute.addChildren([indexRoute]) + const router = createRouter({ routeTree, context: { project: 'bar' } }) + + render() + + const indexElement = await screen.findByText('Index page') + expect(indexElement).toBeInTheDocument() + + expect(mockRootLoader).toHaveBeenCalledWith({ project: 'Router' }) + expect(mockRootLoader).toHaveBeenCalledTimes(1) + + expect(mockIndexLoader).toHaveBeenCalledWith({ project: 'Query' }) + expect(mockIndexLoader).toHaveBeenCalledTimes(1) + }) + }) +}) + describe('beforeLoad in the route definition', () => { configure({ reactStrictMode: true }) diff --git a/packages/react-router/tests/transformer.test.tsx b/packages/react-router/tests/transformer.test.tsx new file mode 100644 index 00000000000..cf7e6c67a5c --- /dev/null +++ b/packages/react-router/tests/transformer.test.tsx @@ -0,0 +1,70 @@ +import { describe, expect, test } from 'vitest' + +import { defaultTransformer } from '../src/transformer' + +describe('transformer.stringify', () => { + test('should stringify dates', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + expect(defaultTransformer.stringify(date)).toMatchInlineSnapshot(` + "{"$date":"2021-08-19T20:00:00.000Z"}" + `) + }) + + test('should stringify undefined', () => { + expect(defaultTransformer.stringify(undefined)).toMatchInlineSnapshot(` + "{"$undefined":""}" + `) + }) + + test('should stringify object foo="bar"', () => { + expect(defaultTransformer.stringify({ foo: 'bar' })).toMatchInlineSnapshot(` + "{"foo":"bar"}" + `) + }) + + test('should stringify object foo=undefined', () => { + expect(defaultTransformer.stringify({ foo: undefined })) + .toMatchInlineSnapshot(` + "{"foo":{"$undefined":""}}" + `) + }) + + test('should stringify object foo=Date', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + expect(defaultTransformer.stringify({ foo: date })).toMatchInlineSnapshot(` + "{"foo":{"$date":"2021-08-19T20:00:00.000Z"}}" + `) + }) +}) + +describe('transformer.parse', () => { + test('should parse dates', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + const str = defaultTransformer.stringify(date) + expect(defaultTransformer.parse(str)).toEqual(date) + }) + + test('should parse undefined', () => { + const str = defaultTransformer.stringify(undefined) + expect(defaultTransformer.parse(str)).toBeUndefined() + }) + + test('should parse object foo="bar"', () => { + const obj = { foo: 'bar' } + const str = defaultTransformer.stringify(obj) + expect(defaultTransformer.parse(str)).toEqual(obj) + }) + + test('should parse object foo=undefined', () => { + const obj = { foo: undefined } + const str = defaultTransformer.stringify(obj) + expect(defaultTransformer.parse(str)).toEqual(obj) + }) + + test('should parse object foo=Date', () => { + const date = new Date('2021-08-19T20:00:00.000Z') + const obj = { foo: date } + const str = defaultTransformer.stringify(obj) + expect(defaultTransformer.parse(str)).toEqual(obj) + }) +}) diff --git a/packages/start/src/client/StartClient.tsx b/packages/start/src/client/StartClient.tsx index 53a39045c0d..ea8f8b19aac 100644 --- a/packages/start/src/client/StartClient.tsx +++ b/packages/start/src/client/StartClient.tsx @@ -1,5 +1,4 @@ import { RouterProvider } from '@tanstack/react-router' -import { defaultTransformer } from './defaultTransformer' import { afterHydrate } from './serialization' import type { AnyRouter } from '@tanstack/react-router' @@ -7,9 +6,6 @@ let cleaned = false export function StartClient(props: { router: AnyRouter }) { if (!props.router.state.matches.length) { - props.router.options.transformer = - (props.router.options as any).transformer || defaultTransformer - props.router.hydrate() afterHydrate({ router: props.router }) } diff --git a/packages/start/src/client/index.tsx b/packages/start/src/client/index.tsx index eaf4d7bc8be..f238d3fdabd 100644 --- a/packages/start/src/client/index.tsx +++ b/packages/start/src/client/index.tsx @@ -24,3 +24,4 @@ export { } from '../constants' export { mergeHeaders } from './headers' export { renderRsc } from './renderRSC' +export { useServerFn } from './useServerFn' diff --git a/packages/start/src/client/serialization.tsx b/packages/start/src/client/serialization.tsx index 29067c03b09..ccbfc5b7232 100644 --- a/packages/start/src/client/serialization.tsx +++ b/packages/start/src/client/serialization.tsx @@ -6,8 +6,8 @@ import { isPlainArray, isPlainObject, pick, - rootRouteId, useRouter, + warning, } from '@tanstack/react-router' import jsesc from 'jsesc' import invariant from 'tiny-invariant' @@ -15,32 +15,27 @@ import { Context } from '@tanstack/react-cross-context' import type { AnyRouteMatch, AnyRouter, - ControlledPromise, + ExtractedEntry, + StreamState, } from '@tanstack/react-router' -type Entry = { - type: 'promise' | 'stream' - path: Array - value: any - id: number - streamState?: StreamState - matchIndex: number -} - export function serializeLoaderData( - loaderData: any, + dataType: '__beforeLoadContext' | 'loaderData', + data: any, ctx: { match: AnyRouteMatch router: AnyRouter }, ) { if (!ctx.router.isServer) { - return loaderData + return data } - const extracted: Array = [] + ;(ctx.match as any).extracted = (ctx.match as any).extracted || [] - const replacedLoaderData = replaceBy(loaderData, (value, path) => { + const extracted = (ctx.match as any).extracted + + const replacedLoaderData = replaceBy(data, (value, path) => { const type = value instanceof ReadableStream ? 'stream' @@ -49,7 +44,8 @@ export function serializeLoaderData( : undefined if (type) { - const entry: Entry = { + const entry: ExtractedEntry = { + dataType, type, path, id: extracted.length, @@ -73,20 +69,33 @@ export function serializeLoaderData( return value }) - ;(ctx.match as any).extracted = extracted - return replacedLoaderData } +// Right after hydration and before the first render, we need to rehydrate each match +// This includes rehydrating the loaderData and also using the beforeLoadContext +// to reconstruct any context that was serialized on the server export function afterHydrate({ router }: { router: AnyRouter }) { router.state.matches.forEach((match) => { const route = router.looseRoutesById[match.routeId]! - if (window.__TSR__?.matches[match.index]) { - match.loaderData = router.options.transformer.parse( - window.__TSR__.matches[match.index].loaderData, - ) + const dMatch = window.__TSR__?.matches[match.index] + if (dMatch) { + if (dMatch.__beforeLoadContext) { + match.__beforeLoadContext = router.options.transformer.parse( + dMatch.__beforeLoadContext, + ) as any + + match.context = { + ...match.context, + ...match.__beforeLoadContext, + } + } + + if (dMatch.loaderData) { + match.loaderData = router.options.transformer.parse(dMatch.loaderData) + } - const extracted = window.__TSR__.matches[match.index].extracted + const extracted = dMatch.extracted if (extracted) { Object.entries(extracted).forEach(([_, ex]: any) => { @@ -123,28 +132,41 @@ export function AfterEachMatch(props: { match: any; matchIndex: number }) { return null } - const extracted = (fullMatch as any).extracted as undefined | Array - - // Remove the extracted values from the loaderData - const serializedLoaderData = extracted - ? extracted.reduce( - (acc: any, entry: Entry) => { - return deepImmutableSetByPath( - acc, - ['loaderData', ...entry.path], - undefined, - ) - }, - { loaderData: fullMatch.loaderData }, - ).loaderData - : fullMatch.loaderData + const extracted = (fullMatch as any).extracted as + | undefined + | Array + + const [serializedBeforeLoadData, serializedLoaderData] = ( + ['__beforeLoadContext', 'loaderData'] as const + ).map((dataType) => { + return extracted + ? extracted.reduce( + (acc: any, entry: ExtractedEntry) => { + if (entry.dataType !== dataType) { + return deepImmutableSetByPath( + acc, + ['temp', ...entry.path], + undefined, + ) + } + return acc + }, + { temp: fullMatch[dataType] }, + ).temp + : fullMatch[dataType] + }) return ( <> - {serializedLoaderData !== undefined || extracted?.length ? ( + {serializedBeforeLoadData !== undefined || + serializedLoaderData !== undefined || + extracted?.length ? ( { if (d.type === 'stream') { - return + return } - return + return }) : null} @@ -181,17 +203,12 @@ export function replaceBy( cb: (value: any, path: Array) => any, path: Array = [], ): T { - const newObj = cb(obj, path) - - if (newObj !== obj) { - return newObj - } - if (isPlainArray(obj)) { return obj.map((value, i) => replaceBy(value, cb, [...path, `${i}`])) as any } if (isPlainObject(obj)) { + // Do not allow objects with illegal const newObj: any = {} for (const key in obj) { @@ -201,10 +218,28 @@ export function replaceBy( return newObj } + // // Detect classes, functions, and other non-serializable objects + // // and return undefined. Exclude some known types that are serializable + // if ( + // typeof obj === 'function' || + // (typeof obj === 'object' && + // ![Object, Promise, ReadableStream].includes((obj as any)?.constructor)) + // ) { + // console.info(obj) + // warning(false, `Non-serializable value ☝️ found at ${path.join('.')}`) + // return undefined as any + // } + + const newObj = cb(obj, path) + + if (newObj !== obj) { + return newObj + } + return obj } -function DehydratePromise({ entry }: { entry: Entry }) { +function DehydratePromise({ entry }: { entry: ExtractedEntry }) { return (
@@ -214,7 +249,7 @@ function DehydratePromise({ entry }: { entry: Entry }) { ) } -function InnerDehydratePromise({ entry }: { entry: Entry }) { +function InnerDehydratePromise({ entry }: { entry: ExtractedEntry }) { if (entry.value.status === 'pending') { throw entry.value } @@ -233,7 +268,7 @@ function InnerDehydratePromise({ entry }: { entry: Entry }) { ) } -function DehydrateStream({ entry }: { entry: Entry }) { +function DehydrateStream({ entry }: { entry: ExtractedEntry }) { invariant(entry.streamState, 'StreamState should be defined') return ( @@ -259,10 +294,6 @@ function DehydrateStream({ entry }: { entry: Entry }) { ) } -type StreamState = { - promises: Array> -} - // Readable stream with state is a stream that has a promise that resolves to the next chunk function createStreamState({ stream, diff --git a/packages/start/src/client/useServerFn.ts b/packages/start/src/client/useServerFn.ts new file mode 100644 index 00000000000..1bdfa4bc5e4 --- /dev/null +++ b/packages/start/src/client/useServerFn.ts @@ -0,0 +1,30 @@ +import { isRedirect, useRouter } from '@tanstack/react-router' + +export function useServerFn) => Promise>( + serverFn: T, +): (...args: Parameters) => ReturnType { + const router = useRouter() + + return (async (...args: Array) => { + try { + const res = await serverFn(...args) + + if (isRedirect(res)) { + throw res + } + + return res + } catch (err) { + if (isRedirect(err)) { + router.navigate( + router.resolveRedirect({ + ...err, + _fromLocation: router.state.location, + }), + ) + } + + throw err + } + }) as any +} diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 6cd3275362d..955f2738cea 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -174,7 +174,10 @@ export async function handleServerRequest(request: Request, event?: H3Event) { } function redirectOrNotFoundResponse(error: any) { - return new Response(JSON.stringify(error), { + const { headers, ...rest } = error + + return new Response(JSON.stringify(rest), { + status: 200, headers: { 'Content-Type': 'application/json', [serverFnReturnTypeHeader]: 'json', diff --git a/packages/start/src/server/createRequestHandler.ts b/packages/start/src/server/createRequestHandler.ts index aba2abe60cc..5357d6cbdd6 100644 --- a/packages/start/src/server/createRequestHandler.ts +++ b/packages/start/src/server/createRequestHandler.ts @@ -9,7 +9,6 @@ import { serverFnPayloadTypeHeader, serverFnReturnTypeHeader, } from '../client' -import { defaultTransformer } from '../client/defaultTransformer' import type { HandlerCallback } from './defaultStreamHandler' export type RequestHandler = ( @@ -30,8 +29,6 @@ export function createRequestHandler({ // Inject a few of the SSR helpers and defaults router.serializeLoaderData = serializeLoaderData as any - router.options.transformer = - (router.options as any).transformer || defaultTransformer if (getRouterManifest) { router.manifest = getRouterManifest() diff --git a/packages/start/src/server/createStartHandler.ts b/packages/start/src/server/createStartHandler.ts index ebd0e699b3a..26071c55ed8 100644 --- a/packages/start/src/server/createStartHandler.ts +++ b/packages/start/src/server/createStartHandler.ts @@ -16,7 +16,6 @@ import { serverFnPayloadTypeHeader, serverFnReturnTypeHeader, } from '../constants' -import { defaultTransformer } from '../client/defaultTransformer' import type { HandlerCallback } from './defaultStreamHandler' export type CustomizeStartHandler = ( @@ -46,8 +45,6 @@ export function createStartHandler({ // Inject a few of the SSR helpers and defaults router.serializeLoaderData = serializeLoaderData as any - router.options.transformer = - (router.options as any).transformer || defaultTransformer if (getRouterManifest) { router.manifest = getRouterManifest() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 602608a663f..bce3b312969 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1145,6 +1145,106 @@ importers: specifier: ^4.3.2 version: 4.3.2(typescript@5.5.3)(vite@5.3.5(@types/node@20.14.9)(terser@5.31.1)) + examples/react/start-basic-auth: + dependencies: + '@prisma/client': + specifier: 5.17.0 + version: 5.17.0(prisma@5.18.0) + '@remix-run/node': + specifier: ^2.10.3 + version: 2.11.1(typescript@5.5.3) + '@remix-run/server-runtime': + specifier: ^2.10.3 + version: 2.11.1(typescript@5.5.3) + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-devtools': + specifier: workspace:* + version: link:../../../packages/router-devtools + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../../packages/router-plugin + '@tanstack/start': + specifier: workspace:* + version: link:../../../packages/start + '@typescript-eslint/parser': + specifier: ^7.16.0 + version: 7.18.0(eslint@8.57.0)(typescript@5.5.3) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.3.5(@types/node@20.14.9)(terser@5.31.1)) + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + isbot: + specifier: ^5.1.12 + version: 5.1.14 + prisma: + specifier: ^5.17.0 + version: 5.18.0 + 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 + remix-auth-form: + specifier: ^1.5.0 + version: 1.5.0(@remix-run/server-runtime@2.11.1(typescript@5.5.3))(remix-auth@3.7.0(@remix-run/react@2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/server-runtime@2.11.1(typescript@5.5.3))) + tailwind-merge: + specifier: ^2.4.0 + version: 2.4.0 + vinxi: + specifier: 0.3.12 + version: 0.3.12(@opentelemetry/api@1.8.0)(@types/node@20.14.9)(ioredis@5.4.1)(terser@5.31.1) + devDependencies: + '@playwright/test': + specifier: ^1.45.1 + version: 1.45.3 + '@replayio/playwright': + specifier: ^3.1.8 + version: 3.1.8(@playwright/test@1.45.3) + '@types/node': + specifier: ^20.12.11 + version: 20.14.9 + '@types/react': + specifier: ^18.2.65 + version: 18.3.3 + '@types/react-dom': + specifier: ^18.2.21 + version: 18.3.0 + autoprefixer: + specifier: ^10.4.19 + version: 10.4.20(postcss@8.4.40) + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-config-react-app: + specifier: ^7.0.1 + version: 7.0.1(@babel/plugin-syntax-flow@7.24.6(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.25.2))(eslint@8.57.0)(typescript@5.5.3) + postcss: + specifier: ^8.4.39 + version: 8.4.40 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + tailwindcss: + specifier: ^3.4.4 + version: 3.4.7 + typescript: + specifier: ^5.5.3 + version: 5.5.3 + vite: + specifier: ^5.3.3 + version: 5.3.5(@types/node@20.14.9)(terser@5.31.1) + vite-tsconfig-paths: + specifier: ^4.3.2 + version: 4.3.2(typescript@5.5.3)(vite@5.3.5(@types/node@20.14.9)(terser@5.31.1)) + examples/react/start-basic-counter: dependencies: '@tanstack/react-router': @@ -1343,6 +1443,103 @@ importers: specifier: ^4.3.2 version: 4.3.2(typescript@5.5.3)(vite@5.3.5(@types/node@20.14.9)(terser@5.31.1)) + examples/react/start-clerk-basic: + dependencies: + '@clerk/tanstack-start': + specifier: 0.3.0-snapshot.vdf04997 + version: 0.3.0-snapshot.vdf04997(@tanstack/react-router@packages+react-router)(@tanstack/start@packages+start)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remix-run/node': + specifier: ^2.10.3 + version: 2.11.1(typescript@5.5.3) + '@remix-run/server-runtime': + specifier: ^2.10.3 + version: 2.11.1(typescript@5.5.3) + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-devtools': + specifier: workspace:* + version: link:../../../packages/router-devtools + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../../packages/router-plugin + '@tanstack/start': + specifier: workspace:* + version: link:../../../packages/start + '@typescript-eslint/parser': + specifier: ^7.16.0 + version: 7.18.0(eslint@8.57.0)(typescript@5.5.3) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.3.5(@types/node@20.14.9)(terser@5.31.1)) + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + isbot: + specifier: ^5.1.12 + version: 5.1.14 + 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 + remix-auth-form: + specifier: ^1.5.0 + version: 1.5.0(@remix-run/server-runtime@2.11.1(typescript@5.5.3))(remix-auth@3.7.0(@remix-run/react@2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/server-runtime@2.11.1(typescript@5.5.3))) + tailwind-merge: + specifier: ^2.4.0 + version: 2.4.0 + vinxi: + specifier: 0.3.12 + version: 0.3.12(@opentelemetry/api@1.8.0)(@types/node@20.14.9)(ioredis@5.4.1)(terser@5.31.1) + devDependencies: + '@playwright/test': + specifier: ^1.45.1 + version: 1.45.3 + '@replayio/playwright': + specifier: ^3.1.8 + version: 3.1.8(@playwright/test@1.45.3) + '@types/node': + specifier: ^20.12.11 + version: 20.14.9 + '@types/react': + specifier: ^18.2.65 + version: 18.3.3 + '@types/react-dom': + specifier: ^18.2.21 + version: 18.3.0 + autoprefixer: + specifier: ^10.4.19 + version: 10.4.20(postcss@8.4.40) + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-config-react-app: + specifier: ^7.0.1 + version: 7.0.1(@babel/plugin-syntax-flow@7.24.6(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.25.2))(eslint@8.57.0)(typescript@5.5.3) + postcss: + specifier: ^8.4.39 + version: 8.4.40 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + tailwindcss: + specifier: ^3.4.4 + version: 3.4.7 + typescript: + specifier: ^5.5.3 + version: 5.5.3 + vite: + specifier: ^5.3.3 + version: 5.3.5(@types/node@20.14.9)(terser@5.31.1) + vite-tsconfig-paths: + specifier: ^4.3.2 + version: 4.3.2(typescript@5.5.3)(vite@5.3.5(@types/node@20.14.9)(terser@5.31.1)) + examples/react/start-convex-trellaux: dependencies: '@convex-dev/react-query': @@ -2792,10 +2989,50 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@clerk/backend@1.7.0-snapshot.vdf04997': + resolution: {integrity: sha512-yVKKF4H4pPXlH4xGuWhqQl8L8UfACQRyi6Bo0/n4SPI8xhfAUbI+xXgl+0ewa76ExdmAYqOpQ8Nokwf0LnYoaw==} + engines: {node: '>=18.17.0'} + + '@clerk/clerk-react@5.4.2-snapshot.vdf04997': + resolution: {integrity: sha512-QiWbY5uvwI/90IdhKviotiPOSaezWSivoEotzh3gawlRtR+InLur1JTfRHqAonqq5WGejAet3R+3DKQrC0lw6Q==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: '>=18 || >=19.0.0-beta' + react-dom: '>=18 || >=19.0.0-beta' + + '@clerk/shared@2.5.2-snapshot.vdf04997': + resolution: {integrity: sha512-JokAhs1CcZ4UDQwJJCoSWPSqy/gUV7N8aRrgp4XkUiAiF7jeUrxvD4C9FNXjAlfn01utjmk3HuCQ1FyRMaB9Rg==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: '>=18 || >=19.0.0-beta' + react-dom: '>=18 || >=19.0.0-beta' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/tanstack-start@0.3.0-snapshot.vdf04997': + resolution: {integrity: sha512-MR6uTPn2gUcQXd4iwVQnTz1C5IAPXbT96bRqjFyxqWWklmMCL55Z+AwqdkYWslRxrNjqWaS26VPZrZ3upOsMcg==} + engines: {node: '>=18.17.0'} + peerDependencies: + '@tanstack/react-router': workspace:* + '@tanstack/start': workspace:* + react: '>=18 || >=19.0.0-beta' + react-dom: '>=18 || >=19.0.0-beta' + + '@clerk/types@4.14.0-snapshot.vdf04997': + resolution: {integrity: sha512-X5OKm/AqWkdjPbTgP+0orYUB3Fa7Cky65LPhMN1Es5ATgQKxHgIfA5tetBdazqeK7kUL3eejSPQH/c03tksiaw==} + engines: {node: '>=18.17.0'} + '@cloudflare/kv-asset-handler@0.3.2': resolution: {integrity: sha512-EeEjMobfuJrwoctj7FA1y1KEbM0+Q1xSjobIEyie9k4haVEBB7vkDvsasw1pM3rO39mL2akxIAzLMUAtrMHZhA==} engines: {node: '>=16.13'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@commitlint/parse@19.0.3': resolution: {integrity: sha512-Il+tNyOb8VDxN3P6XoBBwWJtKKGzHlitEuXA5BP6ir/3loWlsSqDr5aecl6hZcC/spjq4pHqNh0qPlfeWu38QA==} engines: {node: '>=v18'} @@ -2810,6 +3047,9 @@ packages: '@tanstack/react-query': ^5.0.0 convex: ^1.13.0 + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@deno/shim-deno-test@0.5.0': resolution: {integrity: sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==} @@ -3433,6 +3673,84 @@ packages: resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} engines: {node: '>=18'} + '@napi-rs/snappy-android-arm-eabi@7.2.2': + resolution: {integrity: sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/snappy-android-arm64@7.2.2': + resolution: {integrity: sha512-2R/A3qok+nGtpVK8oUMcrIi5OMDckGYNoBLFyli3zp8w6IArPRfg1yOfVUcHvpUDTo9T7LOS1fXgMOoC796eQw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/snappy-darwin-arm64@7.2.2': + resolution: {integrity: sha512-USgArHbfrmdbuq33bD5ssbkPIoT7YCXCRLmZpDS6dMDrx+iM7eD2BecNbOOo7/v1eu6TRmQ0xOzeQ6I/9FIi5g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/snappy-darwin-x64@7.2.2': + resolution: {integrity: sha512-0APDu8iO5iT0IJKblk2lH0VpWSl9zOZndZKnBYIc+ei1npw2L5QvuErFOTeTdHBtzvUHASB+9bvgaWnQo4PvTQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/snappy-freebsd-x64@7.2.2': + resolution: {integrity: sha512-mRTCJsuzy0o/B0Hnp9CwNB5V6cOJ4wedDTWEthsdKHSsQlO7WU9W1yP7H3Qv3Ccp/ZfMyrmG98Ad7u7lG58WXA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/snappy-linux-arm-gnueabihf@7.2.2': + resolution: {integrity: sha512-v1uzm8+6uYjasBPcFkv90VLZ+WhLzr/tnfkZ/iD9mHYiULqkqpRuC8zvc3FZaJy5wLQE9zTDkTJN1IvUcZ+Vcg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/snappy-linux-arm64-gnu@7.2.2': + resolution: {integrity: sha512-LrEMa5pBScs4GXWOn6ZYXfQ72IzoolZw5txqUHVGs8eK4g1HR9HTHhb2oY5ySNaKakG5sOgMsb1rwaEnjhChmQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/snappy-linux-arm64-musl@7.2.2': + resolution: {integrity: sha512-3orWZo9hUpGQcB+3aTLW7UFDqNCQfbr0+MvV67x8nMNYj5eAeUtMmUE/HxLznHO4eZ1qSqiTwLbVx05/Socdlw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/snappy-linux-x64-gnu@7.2.2': + resolution: {integrity: sha512-jZt8Jit/HHDcavt80zxEkDpH+R1Ic0ssiVCoueASzMXa7vwPJeF4ZxZyqUw4qeSy7n8UUExomu8G8ZbP6VKhgw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/snappy-linux-x64-musl@7.2.2': + resolution: {integrity: sha512-Dh96IXgcZrV39a+Tej/owcd9vr5ihiZ3KRix11rr1v0MWtVb61+H1GXXlz6+Zcx9y8jM1NmOuiIuJwkV4vZ4WA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/snappy-win32-arm64-msvc@7.2.2': + resolution: {integrity: sha512-9No0b3xGbHSWv2wtLEn3MO76Yopn1U2TdemZpCaEgOGccz1V+a/1d16Piz3ofSmnA13HGFz3h9NwZH9EOaIgYA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/snappy-win32-ia32-msvc@7.2.2': + resolution: {integrity: sha512-QiGe+0G86J74Qz1JcHtBwM3OYdTni1hX1PFyLRo3HhQUSpmi13Bzc1En7APn+6Pvo7gkrcy81dObGLDSxFAkQQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/snappy-win32-x64-msvc@7.2.2': + resolution: {integrity: sha512-a43cyx1nK0daw6BZxVcvDEXxKMFLSBSDTAhsFD0VqSKcC7MGUBMaqyoWUcMiI7LBSz4bxUmxDWKfCYzpEmeb3w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} @@ -3704,6 +4022,60 @@ packages: engines: {node: '>=18'} hasBin: true + '@prisma/client@5.17.0': + resolution: {integrity: sha512-N2tnyKayT0Zf7mHjwEyE8iG7FwTmXDHFZ1GnNhQp0pJUObsuel4ZZ1XwfuAYkq5mRIiC/Kot0kt0tGCfLJ70Jw==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + + '@prisma/debug@5.18.0': + resolution: {integrity: sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==} + + '@prisma/engines-version@5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169': + resolution: {integrity: sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==} + + '@prisma/engines@5.18.0': + resolution: {integrity: sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==} + + '@prisma/fetch-engine@5.18.0': + resolution: {integrity: sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==} + + '@prisma/get-platform@5.18.0': + resolution: {integrity: sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} @@ -3866,6 +4238,60 @@ packages: '@types/react': optional: true + '@remix-run/node@2.11.1': + resolution: {integrity: sha512-KCQPLSd5Y3OLCoJUQxxTGswALL1gZ+OgL3bf2ap6kITIp1AUZz3T4jqCNVVyWllVAU9gpCtrONaI+SiWf+8b2w==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@remix-run/react@2.11.1': + resolution: {integrity: sha512-bXilQrHx5WVHsdA6UFkWxYVePZJ1kzwfa/KYMdbMZi6zsSlv2/N6ZbgNuoemt8oM8/YgCT6EOPITzCgz+zEMVw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@remix-run/router@1.19.0': + resolution: {integrity: sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==} + engines: {node: '>=14.0.0'} + + '@remix-run/server-runtime@2.11.1': + resolution: {integrity: sha512-j3AlrZul0javvPR6ZWdN32/l12t1E90sLeZI/k+4HpT0ifjqJVg8uG6alRJ0LLN9ae5BERYEslUebUqdfejSkQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@remix-run/web-blob@3.1.0': + resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} + + '@remix-run/web-fetch@4.4.2': + resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==} + engines: {node: ^10.17 || >=12.3} + + '@remix-run/web-file@3.1.0': + resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==} + + '@remix-run/web-form-data@3.1.0': + resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==} + + '@remix-run/web-stream@1.1.0': + resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} + + '@replayio/playwright@3.1.8': + resolution: {integrity: sha512-hRAjdPeC7kJYqus0za02nApfH3/f1yccXcf9qeqdlNPTVGeKTfFunTJCWVBxavwW6nEamKvsqzEoBHSk7+jwVg==} + peerDependencies: + '@playwright/test': ^1.34.0 + '@rollup/plugin-alias@5.1.0': resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==} engines: {node: '>=14.0.0'} @@ -4579,6 +5005,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/unist@3.0.2': resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} @@ -4868,6 +5297,9 @@ packages: '@vue/shared@3.4.27': resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} + '@web3-storage/multipart-parser@1.0.0': + resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -4955,6 +5387,9 @@ packages: resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} hasBin: true + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -5006,6 +5441,10 @@ packages: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -5181,6 +5620,10 @@ packages: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -5299,6 +5742,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -5402,6 +5850,10 @@ packages: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -5422,6 +5874,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clipboardy@4.0.0: resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} engines: {node: '>=18'} @@ -5459,13 +5914,22 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + combinate@1.1.11: resolution: {integrity: sha512-+2MNAQ29HtNejOxkgaTQPC2Bm+pQvFuqf7o18uObl/Bx3daX06kjLUNY/qa9f+YSqzqm/ic3SdrlfN0fvTlw2g==} @@ -5590,6 +6054,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} + cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -5608,8 +6076,8 @@ packages: core-js@3.32.2: resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==} - core-js@3.38.0: - resolution: {integrity: sha512-XPpwqEodRljce9KswjZShh95qJ1URisBeKCjUdq27YdenkslVe7OO0ZJhlYXAChW7OhXaRLl8AAba7IBfoIHug==} + core-js@3.38.1: + resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -5665,6 +6133,9 @@ packages: resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} engines: {node: '>=18'} + csstype@3.1.1: + resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -5674,6 +6145,10 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-uri-to-buffer@3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -5925,6 +6400,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -6027,6 +6505,10 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -6309,6 +6791,9 @@ packages: fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -6339,6 +6824,9 @@ packages: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -6393,6 +6881,9 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -6751,6 +7242,10 @@ packages: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + https-proxy-agent@5.0.0: + resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==} + engines: {node: '>= 6'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -6874,6 +7369,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} @@ -7067,6 +7565,9 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-uuid@1.0.2: + resolution: {integrity: sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -7145,6 +7646,10 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -7210,6 +7715,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsonata@1.8.7: + resolution: {integrity: sha512-tOW2/hZ+nR2bcQZs+0T62LVe5CHaNa3laFFWb/262r39utN6whJGBF7IR2Wq1QXrDbhftolk5gggW8uUJYlBTQ==} + engines: {node: '>= 8'} + jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} @@ -7247,6 +7756,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + ky@1.5.0: resolution: {integrity: sha512-bkQo+UqryW6Zmo/DsixYZE4Z9t2mzvNMhceyIhuMuInb3knm5Q+GNGMKveydJAj+Z6piN1SwI6eR/V0G+Z0BtA==} engines: {node: '>=18'} @@ -7261,6 +7773,17 @@ packages: launch-editor@2.8.0: resolution: {integrity: sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==} + launchdarkly-eventsource@2.0.3: + resolution: {integrity: sha512-VhFjppK7jXlcEKaS7bxdoibB5j01NKyeDR7a8XfssdDGNWCTsbF0/5IExSmPi44eDncPhkoPNxlSZhEZvrbD5w==} + engines: {node: '>=0.12.0'} + + launchdarkly-js-sdk-common@5.2.0: + resolution: {integrity: sha512-aLv2ZrUv229RIwLtFhdILu2aJS/fqGSJzTk4L/bCDZA8RuIh7PutI3ui/AJeNnzPzjKzdEQZw6wVhkVc84baog==} + + launchdarkly-node-client-sdk@3.2.1: + resolution: {integrity: sha512-vIn1kFCWSX83M2hHIQEw+TyEZFqcXn4DTKja2Vdt9NFgs0I2BA70ENA+zgz8OSt+VqvTciZV0l5X90Uyv+3vsQ==} + engines: {node: '>= 12.0.0'} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -7342,6 +7865,13 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + logform@2.6.1: + resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} + engines: {node: '>= 12.0.0'} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -7387,6 +7917,10 @@ packages: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -7499,6 +8033,10 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mixpanel@0.18.0: + resolution: {integrity: sha512-VyUoiLB/S/7abYYHGD5x0LijeuJCUabG8Hb+FvYU3Y99xHf1Qh+s4/pH9lt50fRitAHncWbU1FE01EknUfVVjQ==} + engines: {node: '>=10.0'} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -7511,6 +8049,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -7607,6 +8149,10 @@ packages: resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} hasBin: true + node-localstorage@1.3.1: + resolution: {integrity: sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ==} + engines: {node: '>=0.12'} + node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} @@ -7749,6 +8295,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -7804,6 +8353,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + p-retry@6.2.0: resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} engines: {node: '>=16.17'} @@ -8019,6 +8572,11 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + prisma@5.18.0: + resolution: {integrity: sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==} + engines: {node: '>=16.13'} + hasBin: true + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -8029,6 +8587,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protobufjs@7.3.2: + resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -8134,6 +8696,19 @@ packages: '@types/react': optional: true + react-router-dom@6.26.0: + resolution: {integrity: sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.26.0: + resolution: {integrity: sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -8228,6 +8803,18 @@ packages: remeda@2.7.0: resolution: {integrity: sha512-7q7Xthw/C6uZBk8W5BfXpp/8IjaP51IPUrBVRfSZ3GB9dZMZJEAwYmVxA+TptDmhwlGRw8jUqoo4hL5zU0aV5Q==} + remix-auth-form@1.5.0: + resolution: {integrity: sha512-xWM7T41vi4ZsIxL3f8gz/D6g2mxrnYF7LnG+rG3VqwHh6l13xCoKLraxzWRdbKMVKKQCMISKZRXAeJh9/PQwBA==} + peerDependencies: + '@remix-run/server-runtime': ^1.0.0 || ^2.0.0 + remix-auth: ^3.6.0 + + remix-auth@3.7.0: + resolution: {integrity: sha512-2QVjp2nJVaYxuFBecMQwzixCO7CLSssttLBU5eVlNcNlVeNMmY1g7OkmZ1Ogw9sBcoMXZ18J7xXSK0AISVFcfQ==} + peerDependencies: + '@remix-run/react': ^1.0.0 || ^2.0.0 + '@remix-run/server-runtime': ^1.0.0 || ^2.0.0 + renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} @@ -8347,6 +8934,10 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -8410,6 +9001,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.0: + resolution: {integrity: sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -8424,6 +9018,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha-1@1.0.0: + resolution: {integrity: sha512-qjFA/+LdT0Gvu/JcmYTGZMvVy6WXJOWv1KQuY7HvSr2oBrMxA8PnZu2mc1/ZS2EvLMokj7lIeQsNPjkRzXrImw==} + shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} @@ -8471,6 +9068,9 @@ packages: simple-git@3.25.0: resolution: {integrity: sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -8483,9 +9083,23 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + slide@1.1.6: + resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} + smob@1.4.1: resolution: {integrity: sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==} + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + snakecase-keys@5.4.4: + resolution: {integrity: sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==} + engines: {node: '>=12'} + + snappy@7.2.2: + resolution: {integrity: sha512-iADMq1kY0v3vJmGTuKcFWSXt15qYUz7wFkArOrsSg0IFfI3nJqIJvK2/ZbEIndg7erIJLtAVX2nSOqPz7DcwbA==} + engines: {node: '>= 10'} + sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} @@ -8524,6 +9138,13 @@ packages: stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -8544,6 +9165,9 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + stream-slice@0.1.2: + resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + streamx@2.16.1: resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} @@ -8641,6 +9265,10 @@ packages: resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} engines: {node: '>=16'} + superstruct@1.0.4: + resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} + engines: {node: '>=14.0.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -8671,6 +9299,11 @@ packages: '@swc/core': ^1.2.147 webpack: '>=2' + swr@2.2.5: + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -8730,6 +9363,9 @@ packages: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -8806,6 +9442,10 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -8843,6 +9483,9 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.4.1: + resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} + tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} @@ -8852,6 +9495,9 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + turbo-stream@2.2.0: + resolution: {integrity: sha512-FKFg7A0To1VU4CH9YmSMON5QphK0BXjSoiC7D9yMh+mEEbXLUP9qJ4hEt1qcjKtzncs1OpcnjZO8NgrlVbZH+g==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -8992,6 +9638,10 @@ packages: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} + undici@6.19.7: + resolution: {integrity: sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==} + engines: {node: '>=18.17'} + unenv@1.9.0: resolution: {integrity: sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g==} @@ -9034,10 +9684,6 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin@1.11.0: - resolution: {integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==} - engines: {node: '>=14.0.0'} - unplugin@1.12.2: resolution: {integrity: sha512-bEqQxeC7rxtxPZ3M5V4Djcc4lQqKPgGe3mAWZvxcSmX5jhGxll19NliaRzQSQPrk4xJZSGniK3puLWpRuZN7VQ==} engines: {node: '>=14.0.0'} @@ -9108,6 +9754,9 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url-polyfill@1.1.12: + resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==} + urlpattern-polyfill@8.0.2: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} @@ -9176,6 +9825,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vinxi@0.3.12: + resolution: {integrity: sha512-YU/Scild/Rdy6qwgdILYRlO99Wp8ti2CmlMlYioEg7lRtxAST5iCFjviDya+BYQDgc3Pugh4KzOypVwjZknF2A==} + hasBin: true + vinxi@0.4.1: resolution: {integrity: sha512-WGEYqIuJ2/P3sBoSVKsGvp/UKpW4wVSaAFdA18gthyMCEExN6nVteoA+Rv1wQFLKXTVL9JRpeGJjcLzcRRgGCA==} hasBin: true @@ -9290,6 +9943,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + web-streams-polyfill@3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} @@ -9348,9 +10004,6 @@ packages: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} - webpack-virtual-modules@0.6.1: - resolution: {integrity: sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==} - webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -9431,6 +10084,17 @@ packages: wildcard@2.0.1: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + winston-loki@6.1.2: + resolution: {integrity: sha512-l1iqDDaEUt63Q8arDsiVCXIrK3jLjPOEc5UTs+WMVnWf8D+A8ZRErAAXKDOduT240aNGzpTbCwe5zfdqqLlzMg==} + + winston-transport@4.7.1: + resolution: {integrity: sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==} + engines: {node: '>= 12.0.0'} + + winston@3.14.2: + resolution: {integrity: sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==} + engines: {node: '>= 12.0.0'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -9446,6 +10110,9 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@1.3.4: + resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -10479,10 +11146,58 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 + '@clerk/backend@1.7.0-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 2.5.2-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.14.0-snapshot.vdf04997 + cookie: 0.5.0 + snakecase-keys: 5.4.4 + tslib: 2.4.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/clerk-react@5.4.2-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 2.5.2-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.14.0-snapshot.vdf04997 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.4.1 + + '@clerk/shared@2.5.2-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/types': 4.14.0-snapshot.vdf04997 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.7.0 + swr: 2.2.5(react@18.3.1) + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@clerk/tanstack-start@0.3.0-snapshot.vdf04997(@tanstack/react-router@packages+react-router)(@tanstack/start@packages+start)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/backend': 1.7.0-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/clerk-react': 5.4.2-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/shared': 2.5.2-snapshot.vdf04997(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.14.0-snapshot.vdf04997 + '@tanstack/react-router': link:packages/react-router + '@tanstack/start': link:packages/start + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.4.1 + + '@clerk/types@4.14.0-snapshot.vdf04997': + dependencies: + csstype: 3.1.1 + '@cloudflare/kv-asset-handler@0.3.2': dependencies: mime: 3.0.0 + '@colors/colors@1.6.0': {} + '@commitlint/parse@19.0.3': dependencies: '@commitlint/types': 19.0.3 @@ -10499,6 +11214,12 @@ snapshots: '@tanstack/react-query': 5.51.21(react@18.3.1) convex: 1.13.2(eslint@8.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + '@deno/shim-deno-test@0.5.0': {} '@deno/shim-deno@0.19.1': @@ -11071,6 +11792,45 @@ snapshots: outvariant: 1.4.2 strict-event-emitter: 0.5.1 + '@napi-rs/snappy-android-arm-eabi@7.2.2': + optional: true + + '@napi-rs/snappy-android-arm64@7.2.2': + optional: true + + '@napi-rs/snappy-darwin-arm64@7.2.2': + optional: true + + '@napi-rs/snappy-darwin-x64@7.2.2': + optional: true + + '@napi-rs/snappy-freebsd-x64@7.2.2': + optional: true + + '@napi-rs/snappy-linux-arm-gnueabihf@7.2.2': + optional: true + + '@napi-rs/snappy-linux-arm64-gnu@7.2.2': + optional: true + + '@napi-rs/snappy-linux-arm64-musl@7.2.2': + optional: true + + '@napi-rs/snappy-linux-x64-gnu@7.2.2': + optional: true + + '@napi-rs/snappy-linux-x64-musl@7.2.2': + optional: true + + '@napi-rs/snappy-win32-arm64-msvc@7.2.2': + optional: true + + '@napi-rs/snappy-win32-ia32-msvc@7.2.2': + optional: true + + '@napi-rs/snappy-win32-x64-msvc@7.2.2': + optional: true + '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.2.0 @@ -11298,12 +12058,60 @@ snapshots: '@parcel/watcher-win32-ia32': 2.4.1 '@parcel/watcher-win32-x64': 2.4.1 - '@pkgjs/parseargs@0.11.0': - optional: true + '@pkgjs/parseargs@0.11.0': + optional: true + + '@playwright/test@1.45.3': + dependencies: + playwright: 1.45.3 + + '@prisma/client@5.17.0(prisma@5.18.0)': + optionalDependencies: + prisma: 5.18.0 + + '@prisma/debug@5.18.0': {} + + '@prisma/engines-version@5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169': {} + + '@prisma/engines@5.18.0': + dependencies: + '@prisma/debug': 5.18.0 + '@prisma/engines-version': 5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169 + '@prisma/fetch-engine': 5.18.0 + '@prisma/get-platform': 5.18.0 + + '@prisma/fetch-engine@5.18.0': + dependencies: + '@prisma/debug': 5.18.0 + '@prisma/engines-version': 5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169 + '@prisma/get-platform': 5.18.0 + + '@prisma/get-platform@5.18.0': + dependencies: + '@prisma/debug': 5.18.0 + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} - '@playwright/test@1.45.3': + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': dependencies: - playwright: 1.45.3 + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} '@radix-ui/primitive@1.1.0': {} @@ -11440,6 +12248,98 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@remix-run/node@2.11.1(typescript@5.5.3)': + dependencies: + '@remix-run/server-runtime': 2.11.1(typescript@5.5.3) + '@remix-run/web-fetch': 4.4.2 + '@web3-storage/multipart-parser': 1.0.0 + cookie-signature: 1.2.1 + source-map-support: 0.5.21 + stream-slice: 0.1.2 + undici: 6.19.7 + optionalDependencies: + typescript: 5.5.3 + + '@remix-run/react@2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3)': + dependencies: + '@remix-run/router': 1.19.0 + '@remix-run/server-runtime': 2.11.1(typescript@5.5.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.26.0(react@18.3.1) + react-router-dom: 6.26.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + turbo-stream: 2.2.0 + optionalDependencies: + typescript: 5.5.3 + + '@remix-run/router@1.19.0': {} + + '@remix-run/server-runtime@2.11.1(typescript@5.5.3)': + dependencies: + '@remix-run/router': 1.19.0 + '@types/cookie': 0.6.0 + '@web3-storage/multipart-parser': 1.0.0 + cookie: 0.6.0 + set-cookie-parser: 2.7.0 + source-map: 0.7.4 + turbo-stream: 2.2.0 + optionalDependencies: + typescript: 5.5.3 + + '@remix-run/web-blob@3.1.0': + dependencies: + '@remix-run/web-stream': 1.1.0 + web-encoding: 1.1.5 + + '@remix-run/web-fetch@4.4.2': + dependencies: + '@remix-run/web-blob': 3.1.0 + '@remix-run/web-file': 3.1.0 + '@remix-run/web-form-data': 3.1.0 + '@remix-run/web-stream': 1.1.0 + '@web3-storage/multipart-parser': 1.0.0 + abort-controller: 3.0.0 + data-uri-to-buffer: 3.0.1 + mrmime: 1.0.1 + + '@remix-run/web-file@3.1.0': + dependencies: + '@remix-run/web-blob': 3.1.0 + + '@remix-run/web-form-data@3.1.0': + dependencies: + web-encoding: 1.1.5 + + '@remix-run/web-stream@1.1.0': + dependencies: + web-streams-polyfill: 3.2.1 + + '@replayio/playwright@3.1.8(@playwright/test@1.45.3)': + dependencies: + '@playwright/test': 1.45.3 + chalk: 4.1.2 + debug: 4.3.5 + fs-extra: 11.2.0 + is-uuid: 1.0.2 + jsonata: 1.8.7 + launchdarkly-node-client-sdk: 3.2.1 + mixpanel: 0.18.0 + node-fetch: 2.7.0 + p-map: 4.0.0 + sha-1: 1.0.0 + stack-utils: 2.0.6 + superstruct: 1.0.4 + undici: 5.28.4 + uuid: 8.3.2 + winston: 3.14.2 + winston-loki: 6.1.2 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@rollup/plugin-alias@5.1.0(rollup@4.18.0)': dependencies: slash: 4.0.0 @@ -11588,7 +12488,7 @@ snapshots: '@rspack/lite-tapable': 1.0.0 '@swc/helpers': 0.5.11 caniuse-lite: 1.0.30001651 - core-js: 3.38.0 + core-js: 3.38.1 optionalDependencies: fsevents: 2.3.3 @@ -11601,7 +12501,7 @@ snapshots: '@rsbuild/shared@1.0.0': dependencies: '@rspack/core': 0.4.0 - caniuse-lite: 1.0.30001651 + caniuse-lite: 1.0.30001647 line-diff: 2.1.1 lodash: 4.17.21 postcss: 8.4.31 @@ -12163,6 +13063,8 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/triple-beam@1.3.5': {} + '@types/unist@3.0.2': {} '@types/wrap-ansi@3.0.0': {} @@ -12608,6 +13510,8 @@ snapshots: '@vue/shared@3.4.27': {} + '@web3-storage/multipart-parser@1.0.0': {} + '@webassemblyjs/ast@1.12.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.6 @@ -12716,6 +13620,9 @@ snapshots: dependencies: argparse: 2.0.1 + '@zxing/text-encoding@0.9.0': + optional: true + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -12764,6 +13671,11 @@ snapshots: transitivePeerDependencies: - supports-color + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv-formats@2.1.1(ajv@8.12.0): optionalDependencies: ajv: 8.12.0 @@ -12974,6 +13886,8 @@ snapshots: astring@1.8.6: {} + async-exit-hook@2.0.1: {} + async-sema@3.1.1: {} async@3.2.5: {} @@ -13153,6 +14067,8 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.23.3) + btoa@1.2.1: {} + buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} @@ -13273,6 +14189,8 @@ snapshots: dependencies: source-map: 0.6.1 + clean-stack@2.2.0: {} + cli-boxes@3.0.0: {} cli-cursor@3.1.0: @@ -13285,6 +14203,8 @@ snapshots: cli-width@4.1.0: {} + client-only@0.0.1: {} + clipboardy@4.0.0: dependencies: execa: 8.0.1 @@ -13321,10 +14241,25 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + color-support@1.1.3: {} + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + colorette@2.0.20: {} + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + combinate@1.1.11: {} combined-stream@1.0.8: @@ -13440,6 +14375,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.1: {} + cookie@0.5.0: {} cookie@0.6.0: {} @@ -13454,7 +14391,7 @@ snapshots: core-js@3.32.2: {} - core-js@3.38.0: {} + core-js@3.38.1: {} core-util-is@1.0.3: {} @@ -13507,6 +14444,8 @@ snapshots: dependencies: rrweb-cssom: 0.6.0 + csstype@3.1.1: {} + csstype@3.1.3: {} current-git-branch@1.1.0: @@ -13517,6 +14456,8 @@ snapshots: damerau-levenshtein@1.0.8: {} + data-uri-to-buffer@3.0.1: {} + data-uri-to-buffer@4.0.1: {} data-urls@5.0.0: @@ -13715,6 +14656,8 @@ snapshots: emoji-regex@9.2.2: {} + enabled@2.0.0: {} + encodeurl@1.0.2: {} end-of-stream@1.4.4: @@ -13933,6 +14876,8 @@ snapshots: escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -14405,6 +15350,8 @@ snapshots: fast-decode-uri-component@1.0.1: {} + fast-deep-equal@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -14435,6 +15382,8 @@ snapshots: dependencies: websocket-driver: 0.7.4 + fecha@4.2.3: {} + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -14503,6 +15452,8 @@ snapshots: flatted@3.3.1: {} + fn.name@1.1.0: {} + follow-redirects@1.15.6: {} for-each@0.3.3: @@ -14892,6 +15843,13 @@ snapshots: http-shutdown@1.2.2: {} + https-proxy-agent@5.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -15010,6 +15968,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-async-function@2.0.0: dependencies: has-tostringtag: 1.0.2 @@ -15170,6 +16130,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-uuid@1.0.2: {} + is-weakmap@2.0.2: {} is-weakref@1.0.2: @@ -15242,6 +16204,8 @@ snapshots: jju@1.4.0: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-tokens@9.0.0: {} @@ -15307,6 +16271,8 @@ snapshots: json5@2.2.3: {} + jsonata@1.8.7: {} + jsonc-parser@3.2.0: {} jsonfile@4.0.0: @@ -15342,6 +16308,8 @@ snapshots: kolorist@1.8.0: {} + kuler@2.0.0: {} + ky@1.5.0: {} language-subtag-registry@0.3.22: {} @@ -15355,6 +16323,20 @@ snapshots: picocolors: 1.0.1 shell-quote: 1.8.1 + launchdarkly-eventsource@2.0.3: {} + + launchdarkly-js-sdk-common@5.2.0: + dependencies: + base64-js: 1.5.1 + fast-deep-equal: 2.0.1 + uuid: 8.3.2 + + launchdarkly-node-client-sdk@3.2.1: + dependencies: + launchdarkly-eventsource: 2.0.3 + launchdarkly-js-sdk-common: 5.2.0 + node-localstorage: 1.3.1 + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -15449,6 +16431,17 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + logform@2.6.1: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.4.1 + + long@5.2.3: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -15496,6 +16489,8 @@ snapshots: map-cache@0.2.2: {} + map-obj@4.3.0: {} + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -15586,6 +16581,12 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mixpanel@0.18.0: + dependencies: + https-proxy-agent: 5.0.0 + transitivePeerDependencies: + - supports-color + mkdirp@1.0.4: {} mlly@1.7.0: @@ -15597,6 +16598,8 @@ snapshots: mri@1.2.0: {} + mrmime@1.0.1: {} + ms@2.0.0: {} ms@2.1.2: {} @@ -15765,6 +16768,10 @@ snapshots: node-gyp-build@4.8.1: {} + node-localstorage@1.3.1: + dependencies: + write-file-atomic: 1.3.4 + node-machine-id@1.1.12: {} node-releases@2.0.18: {} @@ -15956,6 +16963,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -16030,6 +17041,10 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + p-retry@6.2.0: dependencies: '@types/retry': 0.12.2 @@ -16212,6 +17227,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 + prisma@5.18.0: + dependencies: + '@prisma/engines': 5.18.0 + process-nextick-args@2.0.1: {} process@0.11.10: {} @@ -16222,6 +17241,21 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + protobufjs@7.3.2: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.14.9 + long: 5.2.3 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -16317,6 +17351,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-router-dom@6.26.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.19.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.26.0(react@18.3.1) + + react-router@6.26.0(react@18.3.1): + dependencies: + '@remix-run/router': 1.19.0 + react: 18.3.1 + react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -16439,6 +17485,17 @@ snapshots: dependencies: type-fest: 4.23.0 + remix-auth-form@1.5.0(@remix-run/server-runtime@2.11.1(typescript@5.5.3))(remix-auth@3.7.0(@remix-run/react@2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/server-runtime@2.11.1(typescript@5.5.3))): + dependencies: + '@remix-run/server-runtime': 2.11.1(typescript@5.5.3) + remix-auth: 3.7.0(@remix-run/react@2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/server-runtime@2.11.1(typescript@5.5.3)) + + remix-auth@3.7.0(@remix-run/react@2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3))(@remix-run/server-runtime@2.11.1(typescript@5.5.3)): + dependencies: + '@remix-run/react': 2.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.3) + '@remix-run/server-runtime': 2.11.1(typescript@5.5.3) + uuid: 8.3.2 + renderkid@3.0.0: dependencies: css-select: 4.3.0 @@ -16574,6 +17631,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safe-stable-stringify@2.4.3: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -16663,6 +17722,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -16683,6 +17744,8 @@ snapshots: setprototypeof@1.2.0: {} + sha-1@1.0.0: {} + shallow-clone@3.0.1: dependencies: kind-of: 6.0.3 @@ -16729,14 +17792,48 @@ snapshots: transitivePeerDependencies: - supports-color + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + slash@3.0.0: {} slash@4.0.0: {} slash@5.1.0: {} + slide@1.1.6: {} + smob@1.4.1: {} + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + + snakecase-keys@5.4.4: + dependencies: + map-obj: 4.3.0 + snake-case: 3.0.4 + type-fest: 2.19.0 + + snappy@7.2.2: + optionalDependencies: + '@napi-rs/snappy-android-arm-eabi': 7.2.2 + '@napi-rs/snappy-android-arm64': 7.2.2 + '@napi-rs/snappy-darwin-arm64': 7.2.2 + '@napi-rs/snappy-darwin-x64': 7.2.2 + '@napi-rs/snappy-freebsd-x64': 7.2.2 + '@napi-rs/snappy-linux-arm-gnueabihf': 7.2.2 + '@napi-rs/snappy-linux-arm64-gnu': 7.2.2 + '@napi-rs/snappy-linux-arm64-musl': 7.2.2 + '@napi-rs/snappy-linux-x64-gnu': 7.2.2 + '@napi-rs/snappy-linux-x64-musl': 7.2.2 + '@napi-rs/snappy-win32-arm64-msvc': 7.2.2 + '@napi-rs/snappy-win32-ia32-msvc': 7.2.2 + '@napi-rs/snappy-win32-x64-msvc': 7.2.2 + optional: true + sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 @@ -16783,6 +17880,12 @@ snapshots: stable-hash@0.0.4: {} + stack-trace@0.0.10: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + stackback@0.0.2: {} stackframe@1.3.4: {} @@ -16795,6 +17898,8 @@ snapshots: std-env@3.7.0: {} + stream-slice@0.1.2: {} + streamx@2.16.1: dependencies: fast-fifo: 1.3.2 @@ -16914,6 +18019,8 @@ snapshots: dependencies: copy-anything: 3.0.2 + superstruct@1.0.4: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -16941,6 +18048,12 @@ snapshots: '@swc/counter': 0.1.3 webpack: 5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.11))(esbuild@0.21.5)(webpack-cli@5.1.4) + swr@2.2.5(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) + symbol-tree@3.2.4: {} system-architecture@0.1.0: {} @@ -17037,6 +18150,8 @@ snapshots: text-extensions@2.4.0: {} + text-hex@1.0.0: {} + text-table@0.2.0: {} thenify-all@1.6.0: @@ -17094,6 +18209,8 @@ snapshots: tree-kill@1.2.2: {} + triple-beam@1.4.1: {} + ts-api-utils@1.3.0(typescript@5.5.3): dependencies: typescript: 5.5.3 @@ -17126,6 +18243,8 @@ snapshots: tslib@1.14.1: {} + tslib@2.4.1: {} + tslib@2.6.2: {} tsutils@3.21.0(typescript@5.5.3): @@ -17133,6 +18252,8 @@ snapshots: tslib: 1.14.1 typescript: 5.5.3 + turbo-stream@2.2.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -17249,7 +18370,7 @@ snapshots: acorn: 8.12.1 estree-walker: 3.0.3 magic-string: 0.30.10 - unplugin: 1.11.0 + unplugin: 1.12.2 undici-types@5.26.5: {} @@ -17259,6 +18380,8 @@ snapshots: dependencies: '@fastify/busboy': 2.1.0 + undici@6.19.7: {} + unenv@1.9.0: dependencies: consola: 3.2.3 @@ -17294,7 +18417,7 @@ snapshots: pkg-types: 1.1.1 scule: 1.3.0 strip-literal: 1.3.0 - unplugin: 1.11.0 + unplugin: 1.12.2 transitivePeerDependencies: - rollup @@ -17306,13 +18429,6 @@ snapshots: unpipe@1.0.0: {} - unplugin@1.11.0: - dependencies: - acorn: 8.12.1 - chokidar: 3.6.0 - webpack-sources: 3.2.3 - webpack-virtual-modules: 0.6.1 - unplugin@1.12.2: dependencies: acorn: 8.12.1 @@ -17350,7 +18466,7 @@ snapshots: mlly: 1.7.0 pathe: 1.1.2 pkg-types: 1.1.1 - unplugin: 1.11.0 + unplugin: 1.12.2 update-browserslist-db@1.1.0(browserslist@4.23.3): dependencies: @@ -17369,6 +18485,8 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + url-polyfill@1.1.12: {} + urlpattern-polyfill@8.0.2: {} use-callback-ref@1.3.0(@types/react@18.3.3)(react@18.3.1): @@ -17418,6 +18536,73 @@ snapshots: vary@1.1.2: {} + vinxi@0.3.12(@opentelemetry/api@1.8.0)(@types/node@20.14.9)(ioredis@5.4.1)(terser@5.31.1): + dependencies: + '@babel/core': 7.25.2 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.25.2) + '@types/micromatch': 4.0.7 + '@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.7 + nitropack: 2.9.6(@opentelemetry/api@1.8.0) + node-fetch-native: 1.6.4 + path-to-regexp: 6.2.2 + pathe: 1.1.2 + radix3: 1.1.2 + resolve: 1.22.8 + serve-placeholder: 2.0.1 + serve-static: 1.15.0 + ufo: 1.5.3 + unctx: 2.3.1 + unenv: 1.9.0 + unstorage: 1.10.2(ioredis@5.4.1) + vite: 5.3.5(@types/node@20.14.9)(terser@5.31.1) + zod: 3.23.8 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@libsql/client' + - '@netlify/blobs' + - '@opentelemetry/api' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/kv' + - better-sqlite3 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + - uWebSockets.js + - xml2js + vinxi@0.4.1(@opentelemetry/api@1.8.0)(@types/node@20.14.9)(ioredis@5.4.1)(terser@5.31.1): dependencies: '@babel/core': 7.25.2 @@ -17612,6 +18797,12 @@ snapshots: dependencies: defaults: 1.0.4 + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + web-streams-polyfill@3.2.1: {} webidl-conversions@3.0.1: {} @@ -17697,8 +18888,6 @@ snapshots: webpack-sources@3.2.3: {} - webpack-virtual-modules@0.6.1: {} - webpack-virtual-modules@0.6.2: {} webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.11))(esbuild@0.21.5)(webpack-cli@5.1.4): @@ -17854,6 +19043,36 @@ snapshots: wildcard@2.0.1: {} + winston-loki@6.1.2: + dependencies: + async-exit-hook: 2.0.1 + btoa: 1.2.1 + protobufjs: 7.3.2 + url-polyfill: 1.1.12 + winston-transport: 4.7.1 + optionalDependencies: + snappy: 7.2.2 + + winston-transport@4.7.1: + dependencies: + logform: 2.6.1 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.14.2: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.5 + is-stream: 2.0.1 + logform: 2.6.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.7.1 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -17874,6 +19093,12 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@1.3.4: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + slide: 1.1.6 + ws@8.18.0: {} xml-name-validator@5.0.0: {}