Skip to content

Commit

Permalink
fix(start): sync start serialization on the type level (#2809)
Browse files Browse the repository at this point in the history
* fix(start): allow functions in types

* fix(start): implement start serialization on the type level

* fix(start): validate createServerFn to not allow functions

* refactor(start): make transformer types generic

* feat(start): add type checking of validator and context

* chore: fix linting

* chore: remove serializable types
  • Loading branch information
chorobin authored Nov 21, 2024
1 parent 7f8199c commit 607ac2b
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 25 deletions.
8 changes: 7 additions & 1 deletion packages/react-router/src/index.tsx
Original file line number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand Up @@ -129,7 +129,7 @@ export interface MiddlewareOptions<
> {
validateClient?: boolean
middleware?: TMiddlewares
validator?: Constrain<TValidator, AnyValidator>
validator?: ConstrainValidator<TValidator>
client?: MiddlewareClientFn<
TMiddlewares,
TValidator,
Expand Down Expand 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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>
>
>()
})

0 comments on commit 607ac2b

Please sign in to comment.