Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
feat(tanstack-react-start): Introduce middleware and support for TanStack Start RC#6859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
1c717f5443e7cec0342c53f9b230793acc54f4ee0814a31e35c42683a0f598702a3269c93808941fe920951cc061b772e6d55720cbf496bcf77df91900ac8501ec8ee456c86c86cb369d8a96ab2de4a2c5427d6eda42e5a33b064b52459e080a3018ec718285848fa58ef7ac6525dd912c5f793a612233cc8e86d40cf986b971abcb19fa85047d256d5cf00bb2d7f25ea5e5872555c0567e99daFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| --- | ||
| "@clerk/tanstack-react-start": minor | ||
| --- | ||
| Added support for [TanStack Start v1 RC](https://tanstack.com/blog/announcing-tanstack-start-v1)! Includes a new `clerkMiddleware()` global middleware replacing the custom server handler. | ||
| Usage: | ||
| 1. Create a `src/start.ts` file and add `clerkMiddleware()` to the list of request middlewares: | ||
| ```ts | ||
| // src/start.ts | ||
| import { clerkMiddleware } from '@clerk/tanstack-react-start/server' | ||
| import { createStart } from '@tanstack/react-start' | ||
| export const startInstance = createStart(() => { | ||
| return { | ||
| requestMiddleware: [clerkMiddleware()], | ||
| } | ||
| }) | ||
| ``` | ||
| 2. Add `<ClerkProvider>` to your root route | ||
| ```tsx | ||
| // src/routes/__root.tsx | ||
| import { ClerkProvider } from '@clerk/tanstack-react-start' | ||
| export const Route = createRootRoute({...}) | ||
| function RootDocument({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <ClerkProvider> | ||
| <html> | ||
| <head> | ||
| <HeadContent /> | ||
| </head> | ||
| <body> | ||
| {children} | ||
| <Scripts /> | ||
| </body> | ||
| </html> | ||
| </ClerkProvider> | ||
| ) | ||
| } | ||
| ``` | ||
| The `getAuth()` helper is now `auth()` and can now be called within server routes and functions, without passing a Request object: | ||
| ```ts | ||
| import { auth } from '@clerk/tanstack-react-start/server' | ||
| const authStateFn = createServerFn().handler(async () => { | ||
| const { userId } = await auth() | ||
| if (!userId) { | ||
| throw redirect({ | ||
| to: '/sign-in', | ||
| }) | ||
| } | ||
| return { userId } | ||
| }) | ||
| export const Route = createFileRoute('/')({ | ||
| component: Home, | ||
| beforeLoad: async () => await authStateFn(), | ||
| loader: async ({ context }) => { | ||
| return { userId: context.userId } | ||
| }, | ||
| }) | ||
| ``` |
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { clerkMiddleware } from '@clerk/tanstack-react-start/server'; | ||
| import { createStart } from '@tanstack/react-start'; | ||
| export const startInstance = createStart(() => { | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return { | ||
| requestMiddleware: [clerkMiddleware()], | ||
| }; | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,6 +2,7 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite'; | ||
| import { defineConfig } from 'vite'; | ||
| import tsConfigPaths from 'vite-tsconfig-paths'; | ||
| import tailwindcss from '@tailwindcss/vite'; | ||
| import viteReact from '@vitejs/plugin-react'; | ||
wobsoriano marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| @@ -10,5 +11,6 @@ export default defineConfig({ | ||
| }), | ||
| tanstackStart(), | ||
| tailwindcss(), | ||
| viteReact(), | ||
| ], | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -76,13 +76,13 @@ | ||
| "tslib": "catalog:repo" | ||
| }, | ||
| "devDependencies": { | ||
| "@tanstack/react-router": "1.131.49", | ||
| "@tanstack/react-start": "1.131.49", | ||
| "@tanstack/react-router": "1.132.0", | ||
| "@tanstack/react-start": "1.132.0", | ||
| "esbuild-plugin-file-path-extensions": "^2.1.4" | ||
| }, | ||
| "peerDependencies": { | ||
| "@tanstack/react-router": "^1.131.0 <1.132.0", | ||
| "@tanstack/react-start": "^1.131.0 <1.132.0", | ||
| "@tanstack/react-router": "^1.132.0", | ||
| "@tanstack/react-start": "^1.132.0", | ||
Comment on lines
+84
to
+85
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Raising the peer dependency floor is a breaking change Moving the peer requirements to 🤖 Prompt for AI Agents | ||
| "react": "catalog:peer-react", | ||
| "react-dom": "catalog:peer-react" | ||
| }, | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; | ||
| import { ScriptOnce, useRouteContext } from '@tanstack/react-router'; | ||
| import { ScriptOnce } from '@tanstack/react-router'; | ||
| import { getGlobalStartContext } from '@tanstack/react-start'; | ||
wobsoriano marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| import { useEffect } from 'react'; | ||
| import { isClient } from '../utils'; | ||
| @@ -19,15 +20,14 @@ const awaitableNavigateRef: { current: ReturnType<typeof useAwaitableNavigate> | | ||
| export function ClerkProvider({ children, ...providerProps }: TanstackStartClerkProviderProps): JSX.Element { | ||
| const awaitableNavigate = useAwaitableNavigate(); | ||
| const routerContext = useRouteContext({ | ||
| strict: false, | ||
| }); | ||
| // @ts-expect-error: Untyped internal Clerk initial state | ||
| const clerkInitialState = getGlobalStartContext()?.clerkInitialState ?? {}; | ||
| useEffect(() => { | ||
| awaitableNavigateRef.current = awaitableNavigate; | ||
| }, [awaitableNavigate]); | ||
| const clerkInitState = isClient() ? (window as any).__clerk_init_state : routerContext?.clerkInitialState; | ||
| const clerkInitState = isClient() ? (window as any).__clerk_init_state : clerkInitialState; | ||
| const { clerkSsrState, ...restInitState } = pickFromClerkInitState(clerkInitState?.__internal_clerk_state); | ||
| @@ -38,7 +38,7 @@ export function ClerkProvider({ children, ...providerProps }: TanstackStartClerk | ||
| return ( | ||
| <> | ||
| <ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(routerContext?.clerkInitialState)};`}</ScriptOnce> | ||
| <ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(clerkInitialState)};`}</ScriptOnce> | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape serialized JSON to avoid XSS/script-breaking sequences. Embedding raw Apply this diff: - <ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(clerkInitialState)};`}</ScriptOnce>+ <ScriptOnce>{`window.__clerk_init_state = ${safeSerialize(clerkInitialState)};`}</ScriptOnce>Add this helper near the top of the file (outside the component): // Safe JSON serializer for embedding into inline <script> tagsconstsafeSerialize=(data: unknown): string=>JSON.stringify(data).replace(/</g,'\\u003c')// avoid </script.replace(/-->/g,'--\\u003e')// avoid HTML comment close.replace(/\u2028/g,'\\u2028')// line sep.replace(/\u2029/g,'\\u2029');// paragraph sepAs per coding guidelines: “Provide meaningful error messages” and “Validate and sanitize outputs.” 🤖 Prompt for AI Agents | ||
| <ClerkOptionsProvider options={mergedProps}> | ||
| <ReactClerkProvider | ||
| initialState={clerkSsrState} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import type { SessionAuthObject } from '@clerk/backend'; | ||
| import type { AuthOptions, GetAuthFnNoRequest } from '@clerk/backend/internal'; | ||
| import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'; | ||
| import { getGlobalStartContext } from '@tanstack/react-start'; | ||
| import { errorThrower } from '../utils'; | ||
| import { clerkMiddlewareNotConfigured } from '../utils/errors'; | ||
| export const auth: GetAuthFnNoRequest<SessionAuthObject, true> = (async (opts?: AuthOptions) => { | ||
| // @ts-expect-error: Untyped internal Clerk start context | ||
| const authObjectFn = getGlobalStartContext().auth; | ||
| if (!authObjectFn) { | ||
| return errorThrower.throw(clerkMiddlewareNotConfigured); | ||
| } | ||
| // We're keeping it a promise for now for future changes | ||
| const authObject = await Promise.resolve(authObjectFn({ treatPendingAsSignedOut: opts?.treatPendingAsSignedOut })); | ||
| return getAuthObjectForAcceptedToken({ authObject, acceptsToken: opts?.acceptsToken }); | ||
| }) as GetAuthFnNoRequest<SessionAuthObject, true>; |
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import type { RequestState } from '@clerk/backend/internal'; | ||
| import { AuthStatus, constants } from '@clerk/backend/internal'; | ||
| import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler'; | ||
| import type { PendingSessionOptions } from '@clerk/types'; | ||
| import type { AnyRequestMiddleware } from '@tanstack/react-start'; | ||
| import { createMiddleware, json } from '@tanstack/react-start'; | ||
| import { clerkClient } from './clerkClient'; | ||
| import { loadOptions } from './loadOptions'; | ||
| import type { ClerkMiddlewareOptions } from './types'; | ||
| import { getResponseClerkState } from './utils'; | ||
| export const clerkMiddleware = (options?: ClerkMiddlewareOptions): AnyRequestMiddleware => { | ||
| return createMiddleware().server(async args => { | ||
| const loadedOptions = loadOptions(args.request, options); | ||
| const requestState = await clerkClient().authenticateRequest(args.request, { | ||
| ...loadedOptions, | ||
| acceptsToken: 'any', | ||
| }); | ||
| const locationHeader = requestState.headers.get(constants.Headers.Location); | ||
| if (locationHeader) { | ||
| handleNetlifyCacheInDevInstance({ | ||
| locationHeader, | ||
| requestStateHeaders: requestState.headers, | ||
| publishableKey: requestState.publishableKey, | ||
| }); | ||
| // Trigger a handshake redirect | ||
| // eslint-disable-next-line @typescript-eslint/only-throw-error | ||
| throw json(null, { status: 307, headers: requestState.headers }); | ||
| } | ||
| if (requestState.status === AuthStatus.Handshake) { | ||
| throw new Error('Clerk: handshake status without redirect'); | ||
| } | ||
| const clerkInitialState = getResponseClerkState(requestState as RequestState, loadedOptions); | ||
| const result = await args.next({ | ||
| context: { | ||
| clerkInitialState, | ||
MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be accessed by | ||
| auth: (opts?: PendingSessionOptions) => requestState.toAuth(opts), | ||
MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is for | ||
| }, | ||
| }); | ||
| if (requestState.headers) { | ||
| requestState.headers.forEach((value, key) => { | ||
| result.response.headers.append(key, value); | ||
| }); | ||
| } | ||
| return result; | ||
| }); | ||
| }; | ||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.