Skip to content

Commit bca6c91

Browse files
feat: pass in router instance in client function middleware
this adds a new package `@tanstack/start-storage-context` to break up a circular dependency between start-client-core and start-server-core
1 parent a1d54c4 commit bca6c91

File tree

25 files changed

+413
-55
lines changed

25 files changed

+413
-55
lines changed

e2e/react-start/server-functions/src/routeTree.gen.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserv
2222
import { Route as ConsistentRouteImport } from './routes/consistent'
2323
import { Route as AbortSignalRouteImport } from './routes/abort-signal'
2424
import { Route as IndexRouteImport } from './routes/index'
25+
import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index'
2526
import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index'
2627
import { Route as CookiesIndexRouteImport } from './routes/cookies/index'
28+
import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router'
2729
import { Route as CookiesSetRouteImport } from './routes/cookies/set'
2830
import { Route as FormdataRedirectTargetNameRouteImport } from './routes/formdata-redirect/target.$name'
2931

@@ -92,6 +94,11 @@ const IndexRoute = IndexRouteImport.update({
9294
path: '/',
9395
getParentRoute: () => rootRouteImport,
9496
} as any)
97+
const MiddlewareIndexRoute = MiddlewareIndexRouteImport.update({
98+
id: '/middleware/',
99+
path: '/middleware/',
100+
getParentRoute: () => rootRouteImport,
101+
} as any)
95102
const FormdataRedirectIndexRoute = FormdataRedirectIndexRouteImport.update({
96103
id: '/formdata-redirect/',
97104
path: '/formdata-redirect/',
@@ -102,6 +109,12 @@ const CookiesIndexRoute = CookiesIndexRouteImport.update({
102109
path: '/cookies/',
103110
getParentRoute: () => rootRouteImport,
104111
} as any)
112+
const MiddlewareClientMiddlewareRouterRoute =
113+
MiddlewareClientMiddlewareRouterRouteImport.update({
114+
id: '/middleware/client-middleware-router',
115+
path: '/middleware/client-middleware-router',
116+
getParentRoute: () => rootRouteImport,
117+
} as any)
105118
const CookiesSetRoute = CookiesSetRouteImport.update({
106119
id: '/cookies/set',
107120
path: '/cookies/set',
@@ -129,8 +142,10 @@ export interface FileRoutesByFullPath {
129142
'/status': typeof StatusRoute
130143
'/submit-post-formdata': typeof SubmitPostFormdataRoute
131144
'/cookies/set': typeof CookiesSetRoute
145+
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
132146
'/cookies': typeof CookiesIndexRoute
133147
'/formdata-redirect': typeof FormdataRedirectIndexRoute
148+
'/middleware': typeof MiddlewareIndexRoute
134149
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
135150
}
136151
export interface FileRoutesByTo {
@@ -148,8 +163,10 @@ export interface FileRoutesByTo {
148163
'/status': typeof StatusRoute
149164
'/submit-post-formdata': typeof SubmitPostFormdataRoute
150165
'/cookies/set': typeof CookiesSetRoute
166+
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
151167
'/cookies': typeof CookiesIndexRoute
152168
'/formdata-redirect': typeof FormdataRedirectIndexRoute
169+
'/middleware': typeof MiddlewareIndexRoute
153170
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
154171
}
155172
export interface FileRoutesById {
@@ -168,8 +185,10 @@ export interface FileRoutesById {
168185
'/status': typeof StatusRoute
169186
'/submit-post-formdata': typeof SubmitPostFormdataRoute
170187
'/cookies/set': typeof CookiesSetRoute
188+
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
171189
'/cookies/': typeof CookiesIndexRoute
172190
'/formdata-redirect/': typeof FormdataRedirectIndexRoute
191+
'/middleware/': typeof MiddlewareIndexRoute
173192
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
174193
}
175194
export interface FileRouteTypes {
@@ -189,8 +208,10 @@ export interface FileRouteTypes {
189208
| '/status'
190209
| '/submit-post-formdata'
191210
| '/cookies/set'
211+
| '/middleware/client-middleware-router'
192212
| '/cookies'
193213
| '/formdata-redirect'
214+
| '/middleware'
194215
| '/formdata-redirect/target/$name'
195216
fileRoutesByTo: FileRoutesByTo
196217
to:
@@ -208,8 +229,10 @@ export interface FileRouteTypes {
208229
| '/status'
209230
| '/submit-post-formdata'
210231
| '/cookies/set'
232+
| '/middleware/client-middleware-router'
211233
| '/cookies'
212234
| '/formdata-redirect'
235+
| '/middleware'
213236
| '/formdata-redirect/target/$name'
214237
id:
215238
| '__root__'
@@ -227,8 +250,10 @@ export interface FileRouteTypes {
227250
| '/status'
228251
| '/submit-post-formdata'
229252
| '/cookies/set'
253+
| '/middleware/client-middleware-router'
230254
| '/cookies/'
231255
| '/formdata-redirect/'
256+
| '/middleware/'
232257
| '/formdata-redirect/target/$name'
233258
fileRoutesById: FileRoutesById
234259
}
@@ -247,8 +272,10 @@ export interface RootRouteChildren {
247272
StatusRoute: typeof StatusRoute
248273
SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute
249274
CookiesSetRoute: typeof CookiesSetRoute
275+
MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
250276
CookiesIndexRoute: typeof CookiesIndexRoute
251277
FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute
278+
MiddlewareIndexRoute: typeof MiddlewareIndexRoute
252279
FormdataRedirectTargetNameRoute: typeof FormdataRedirectTargetNameRoute
253280
}
254281

@@ -345,6 +372,13 @@ declare module '@tanstack/react-router' {
345372
preLoaderRoute: typeof IndexRouteImport
346373
parentRoute: typeof rootRouteImport
347374
}
375+
'/middleware/': {
376+
id: '/middleware/'
377+
path: '/middleware'
378+
fullPath: '/middleware'
379+
preLoaderRoute: typeof MiddlewareIndexRouteImport
380+
parentRoute: typeof rootRouteImport
381+
}
348382
'/formdata-redirect/': {
349383
id: '/formdata-redirect/'
350384
path: '/formdata-redirect'
@@ -359,6 +393,13 @@ declare module '@tanstack/react-router' {
359393
preLoaderRoute: typeof CookiesIndexRouteImport
360394
parentRoute: typeof rootRouteImport
361395
}
396+
'/middleware/client-middleware-router': {
397+
id: '/middleware/client-middleware-router'
398+
path: '/middleware/client-middleware-router'
399+
fullPath: '/middleware/client-middleware-router'
400+
preLoaderRoute: typeof MiddlewareClientMiddlewareRouterRouteImport
401+
parentRoute: typeof rootRouteImport
402+
}
362403
'/cookies/set': {
363404
id: '/cookies/set'
364405
path: '/cookies/set'
@@ -391,8 +432,10 @@ const rootRouteChildren: RootRouteChildren = {
391432
StatusRoute: StatusRoute,
392433
SubmitPostFormdataRoute: SubmitPostFormdataRoute,
393434
CookiesSetRoute: CookiesSetRoute,
435+
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
394436
CookiesIndexRoute: CookiesIndexRoute,
395437
FormdataRedirectIndexRoute: FormdataRedirectIndexRoute,
438+
MiddlewareIndexRoute: MiddlewareIndexRoute,
396439
FormdataRedirectTargetNameRoute: FormdataRedirectTargetNameRoute,
397440
}
398441
export const routeTree = rootRouteImport

e2e/react-start/server-functions/src/router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export function createRouter() {
1010
defaultErrorComponent: DefaultCatchBoundary,
1111
defaultNotFoundComponent: () => <NotFound />,
1212
scrollRestoration: true,
13+
context: {
14+
foo: {
15+
bar: 'baz',
16+
},
17+
},
1318
})
1419

1520
return router

e2e/react-start/server-functions/src/routes/__root.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import {
55
Link,
66
Outlet,
77
Scripts,
8-
createRootRoute,
8+
createRootRouteWithContext,
99
} from '@tanstack/react-router'
1010

1111
import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
1212
import { NotFound } from '~/components/NotFound'
1313
import appCss from '~/styles/app.css?url'
1414

15-
export const Route = createRootRoute({
15+
export const Route = createRootRouteWithContext<{ foo: { bar: string } }>()({
1616
head: () => ({
1717
meta: [
1818
{

e2e/react-start/server-functions/src/routes/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ function Home() {
8282
server function redirects when FormData is submitted (via no-JS)
8383
</Link>
8484
</li>
85+
<li>
86+
<Link to="/middleware">Server Functions Middlware E2E tests</Link>
87+
</li>
8588
</ul>
8689
</div>
8790
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { createFileRoute, useRouter } from '@tanstack/react-router'
2+
import { createMiddleware, createServerFn } from '@tanstack/react-start'
3+
import React from 'react'
4+
5+
const middleware = createMiddleware({ type: 'function' }).client(
6+
async ({ router, next }) => {
7+
return next({
8+
sendContext: {
9+
routerContext: router.options.context,
10+
},
11+
})
12+
},
13+
)
14+
15+
const serverFn = createServerFn()
16+
.middleware([middleware])
17+
.handler(({ context }) => {
18+
return context.routerContext
19+
})
20+
export const Route = createFileRoute('/middleware/client-middleware-router')({
21+
component: RouteComponent,
22+
})
23+
24+
function RouteComponent() {
25+
const [serverFnResult, setServerFnResult] = React.useState({})
26+
const router = useRouter()
27+
return (
28+
<div
29+
className="p-2 m-2 grid gap-2"
30+
data-testid="client-middleware-router-route-component"
31+
>
32+
<h3>Client Middleware has access to router instance</h3>
33+
<p>
34+
This component checks that the client middleware has access to the
35+
router instance and thus its context.
36+
</p>
37+
<div>
38+
It should return{' '}
39+
<code>
40+
<pre data-testid="expected-server-fn-result">
41+
{JSON.stringify(router.options.context)}
42+
</pre>
43+
</code>
44+
</div>
45+
<p>
46+
serverFn returns:
47+
<br />
48+
<span data-testid="serverFn-response">
49+
{JSON.stringify(serverFnResult)}
50+
</span>
51+
</p>
52+
<button
53+
data-testid="btn-serverFn"
54+
type="button"
55+
className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
56+
onClick={() => {
57+
serverFn().then(setServerFnResult)
58+
}}
59+
>
60+
Invoke Server Function
61+
</button>
62+
</div>
63+
)
64+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/middleware/')({
4+
component: RouteComponent,
5+
})
6+
7+
function RouteComponent() {
8+
return (
9+
<div className="p-8">
10+
<h1 className="font-bold text-lg">
11+
Server functions middleware E2E tests
12+
</h1>
13+
<ul className="list-disc p-4">
14+
<li>
15+
<Route.Link to="./client-middleware-router">
16+
Client Middleware has access to router instance
17+
</Route.Link>
18+
</li>
19+
</ul>
20+
</div>
21+
)
22+
}

e2e/react-start/server-functions/tests/server-functions.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,20 @@ test('raw response', async ({ page }) => {
339339
expect(page.url().endsWith(`/formdata-redirect/target/${expected}`))
340340
})
341341
})
342+
343+
test.describe('middleware', () => {
344+
test('client middleware should have access to router context via the router instance', async ({
345+
page,
346+
}) => {
347+
await page.goto('/middleware/client-middleware-router')
348+
await page.waitForLoadState('networkidle')
349+
350+
const expected =
351+
(await page.getByTestId('expected-server-fn-result').textContent()) || ''
352+
expect(expected).not.toBe('')
353+
354+
await page.getByTestId('btn-serverFn').click()
355+
await page.waitForLoadState('networkidle')
356+
await expect(page.getByTestId('serverFn-response')).toContainText(expected)
357+
})
358+
})

packages/router-core/src/global.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { AnyRouter } from './router'
2+
3+
declare global {
4+
interface Window {
5+
__TSR_ROUTER__?: AnyRouter
6+
}
7+
}
8+
9+
export {}

packages/router-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from './global'
2+
13
export { TSR_DEFERRED_PROMISE, defer } from './defer'
24
export type { DeferredPromiseState, DeferredPromise } from './defer'
35
export { preloadWarning } from './link'

packages/router-core/src/router.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,6 @@ import type { AnySchema, AnyValidator } from './validators'
8686
import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
8787
import type { NotFoundError } from './not-found'
8888

89-
declare global {
90-
interface Window {
91-
__TSR_ROUTER__?: AnyRouter
92-
}
93-
}
94-
9589
export type ControllablePromise<T = any> = Promise<T> & {
9690
resolve: (value: T) => void
9791
reject: (value?: any) => void

0 commit comments

Comments
 (0)