Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/react-router/src/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -290,7 +290,13 @@ export {
export type { SearchSerializer, SearchParser } from './searchParams'

export { defaultTransformer } from './transformer'
export type { RouterTransformer } from './transformer'
export type {
RouterTransformer,
TransformerParse,
TransformerStringify,
DefaultTransformerParse,
DefaultTransformerStringify,
} from './transformer'

export { useBlocker, Block } from './useBlocker'

Expand Down
19 changes: 19 additions & 0 deletions packages/react-router/src/transformer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,3 +47,22 @@ const transformers = [
parse: () => undefined,
},
] as const

export type TransformerStringify<T, TSerializable> = T extends TSerializable
? T
: T extends (...args: Array<any>) => any
? 'Function is not serializable'
: { [K in keyof T]: TransformerStringify<T[K], TSerializable> }

export type TransformerParse<T, TSerializable> = T extends TSerializable
? T
: T extends JSX.Element
? ReadableStream
: { [K in keyof T]: TransformerParse<T[K], TSerializable> }

export type DefaultTransformerStringify<T> = TransformerStringify<
T,
Date | undefined
>

export type DefaultTransformerParse<T> = TransformerParse<T, Date | undefined>
12 changes: 6 additions & 6 deletions packages/start/src/client/createMiddleware.ts
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
import type { Method } from './createServerFn'
import type { ConstrainValidator, Method } from './createServerFn'
import type {
AnyValidator,
Constrain,
DefaultTransformerStringify,
Expand,
MergeAll,
ResolveValidatorInput,
Expand DownExpand Up@@ -129,7 +129,7 @@ export interface MiddlewareOptions<
> {
validateClient?: boolean
middleware?: TMiddlewares
validator?: Constrain<TValidator, AnyValidator>
validator?: ConstrainValidator<TValidator>
client?: MiddlewareClientFn<
TMiddlewares,
TValidator,
Expand DownExpand Up@@ -166,7 +166,7 @@ export type MiddlewareServerFn<
TNewClientAfterContext = undefined,
>(ctx?: {
context?: TNewServerContext
sendContext?: TNewClientAfterContext
sendContext?: DefaultTransformerStringify<TNewClientAfterContext>
}) => Promise<
ServerResultWithContext<TNewServerContext, TNewClientAfterContext>
>
Expand All@@ -186,7 +186,7 @@ export type MiddlewareClientFn<
method: Method
next: <TNewServerContext = undefined, TNewClientContext = undefined>(ctx?: {
context?: TNewClientContext
sendContext?: TNewServerContext
sendContext?: DefaultTransformerStringify<TNewServerContext>
headers?: HeadersInit
}) => Promise<ClientResultWithContext<TNewServerContext, TNewClientContext>>
}) =>
Expand DownExpand Up@@ -272,7 +272,7 @@ export interface MiddlewareValidator<
TClientAfterContext,
> {
validator: <TNewValidator>(
input: TNewValidator,
input: ConstrainValidator<TNewValidator>,
) => MiddlewareAfterMiddleware<
TId,
TMiddlewares,
Expand Down
39 changes: 24 additions & 15 deletions packages/start/src/client/createServerFn.ts
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
import invariant from 'tiny-invariant'
import { defaultTransformer } from '@tanstack/react-router'
import { mergeHeaders } from './headers'
import type { AnyValidator, Constrain } from '@tanstack/react-router'
import type {
AnyValidator,
Constrain,
DefaultTransformerParse,
DefaultTransformerStringify,
ResolveValidatorInput,
Validator,
} from '@tanstack/react-router'
import type {
AnyMiddleware,
MergeAllServerContext,
Expand DownExpand Up@@ -61,20 +68,10 @@ export interface OptionalFetcherDataOptions<TInput> extends FetcherBaseOptions {
data?: TInput
}

export type FetcherData<TResponse> = WrapRSCs<
export type FetcherData<TResponse> = DefaultTransformerParse<
TResponse extends JsonResponse<infer TData> ? TData : TResponse
>

export type WrapRSCs<T> = T extends JSX.Element
? ReadableStream
: T extends Record<string, any>
? {
[K in keyof T]: WrapRSCs<T[K]>
}
: T extends Array<infer U>
? Array<WrapRSCs<U>>
: T

export type RscStream<T> = {
__cacheState: T
}
Expand All@@ -83,7 +80,9 @@ export type Method = 'GET' | 'POST'

export type ServerFn<TMethod, TMiddlewares, TValidator, TResponse> = (
ctx: ServerFnCtx<TMethod, TMiddlewares, TValidator>,
) => Promise<TResponse> | TResponse
) =>
| Promise<DefaultTransformerStringify<TResponse>>
| DefaultTransformerStringify<TResponse>

export type ServerFnCtx<TMethod, TMiddlewares, TValidator> = {
method: TMethod
Expand All@@ -105,13 +104,23 @@ type ServerFnBaseOptions<
method: TMethod
validateClient?: boolean
middleware?: Constrain<TMiddlewares, ReadonlyArray<AnyMiddleware>>
validator?: Constrain<TInput, AnyValidator>
validator?: ConstrainValidator<TInput>
extractedFn?: CompiledFetcherFn<TResponse>
serverFn?: ServerFn<TMethod, TMiddlewares, TInput, TResponse>
filename: string
functionId: string
}

export type ConstrainValidator<TValidator> = unknown extends TValidator
? TValidator
: Constrain<
TValidator,
Validator<
DefaultTransformerStringify<ResolveValidatorInput<TValidator>>,
any
>
>

type ServerFnBase<
TMethod extends Method = 'GET',
TResponse = unknown,
Expand All@@ -126,7 +135,7 @@ type ServerFnBase<
'validator' | 'handler'
>
validator: <TValidator>(
validator: Constrain<TValidator, AnyValidator>,
validator: ConstrainValidator<TValidator>,
) => Pick<
ServerFnBase<TMethod, TResponse, TMiddlewares, TValidator>,
'handler' | 'middleware'
Expand Down
1 change: 0 additions & 1 deletion packages/start/src/client/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@ export {
type CompiledFetcherFn,
type Fetcher,
type RscStream,
type WrapRSCs,
type FetcherImpl,
type FetcherData,
type FetcherBaseOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { expectTypeOf, test } from 'vitest'
import { createServerFn } from '../createServerFn'
import { createMiddleware } from '../createMiddleware'
import type { Constrain, Validator } from '@tanstack/react-router'

test('createServerFn without middleware', () => {
createServerFn({ method: 'GET' }).handler((options) => {
Expand DownExpand Up@@ -178,3 +179,56 @@ test('createServerFn where data is optional if there is no validator', () => {
| undefined
>()
})

test('createServerFn returns Date', () => {
const fn = createServerFn().handler(() => ({
dates: [new Date(), new Date()] as const,
}))

expectTypeOf(fn()).toEqualTypeOf<Promise<{ dates: readonly [Date, Date] }>>()
})

test('createServerFn returns RSC', () => {
const fn = createServerFn().handler(() => ({
rscs: [
<div key="0">I'm an RSC</div>,
<div key="1">I'm an RSC</div>,
] as const,
}))

expectTypeOf(fn()).toEqualTypeOf<
Promise<{ rscs: readonly [ReadableStream, ReadableStream] }>
>()
})

test('createServerFn returns undefined', () => {
const fn = createServerFn().handler(() => ({
nothing: undefined,
}))

expectTypeOf(fn()).toEqualTypeOf<Promise<{ nothing: undefined }>>()
})

test('createServerFn cannot return function', () => {
expectTypeOf(createServerFn().handler<{ func: () => 'func' }>)
.parameter(0)
.returns.toEqualTypeOf<
| { func: 'Function is not serializable' }
| Promise<{ func: 'Function is not serializable' }>
>()
})

test('createServerFn cannot validate function', () => {
const validator = createServerFn().validator<
(input: { func: () => 'string' }) => { output: 'string' }
>

expectTypeOf(validator)
.parameter(0)
.toEqualTypeOf<
Constrain<
(input: { func: () => 'string' }) => { output: 'string' },
Validator<{ func: 'Function is not serializable' }, any>
>
>()
})
42 changes: 40 additions & 2 deletions packages/start/src/client/tests/createServerMiddleware.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { expectTypeOf, test } from 'vitest'
import { createMiddleware } from '../createMiddleware'
import type { Constrain, Validator } from '@tanstack/react-router'

test('createServeMiddleware removes middleware after middleware,', () => {
const middleware = createMiddleware()
Expand DownExpand Up@@ -228,7 +229,7 @@ test('createMiddleware merges server context and client context, sends server co
fromServer1: string
fromServer2: string
fromServer3: string
toServer1: string
toServer1: 'toServer1'
}>()
return next({
context: { fromServer4: 'fromServer4' },
Expand All@@ -244,8 +245,45 @@ test('createMiddleware merges server context and client context, sends server co
fromClient3: string
clientAfter3: string
fromClient4: string
toClient1: string
toClient1: 'toClient1'
}>
return next({ context: { clientAfter4: 'clientAfter4' } })
})
})

test('createMiddleware sendContext cannot send a function', () => {
createMiddleware()
.client(({ next }) => {
expectTypeOf(next<{ func: () => 'func' }>)
.parameter(0)
.exclude<undefined>()
.toHaveProperty('sendContext')
.toEqualTypeOf<{ func: 'Function is not serializable' } | undefined>()

return next()
})
.server(({ next }) => {
expectTypeOf(next<undefined, { func: () => 'func' }>)
.parameter(0)
.exclude<undefined>()
.toHaveProperty('sendContext')
.toEqualTypeOf<{ func: 'Function is not serializable' } | undefined>()

return next()
})
})

test('createMiddleware cannot validate function', () => {
const validator = createMiddleware().validator<
(input: { func: () => 'string' }) => { output: 'string' }
>

expectTypeOf(validator)
.parameter(0)
.toEqualTypeOf<
Constrain<
(input: { func: () => 'string' }) => { output: 'string' },
Validator<{ func: 'Function is not serializable' }, any>
>
>()
})