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
76 changes: 54 additions & 22 deletions packages/router-core/src/ssr/serializer/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,22 @@ export interface CreateSerializationAdapterOptions<TInput, TOutput> {
fromSerializable: (value: TOutput) => TInput
}

export type ValidateSerializable<T, TSerializable> = T extends TSerializable
? T
: T extends (...args: Array<any>) => any
? 'Function is not serializable'
: T extends Promise<any>
? ValidateSerializablePromise<T, TSerializable>
: T extends ReadableStream<any>
? ValidateReadableStream<T, TSerializable>
: T extends Set<any>
? ValidateSerializableSet<T, TSerializable>
: T extends Map<any, any>
? ValidateSerializableMap<T, TSerializable>
: {
[K in keyof T]: ValidateSerializable<T[K], TSerializable>
}
export type ValidateSerializable<T, TSerializable> =
T extends ReadonlyArray<unknown>
? ResolveArrayShape<T, TSerializable, 'input'>
: T extends TSerializable
? T
: T extends (...args: Array<any>) => any
? 'Function is not serializable'
: T extends Promise<any>
? ValidateSerializablePromise<T, TSerializable>
: T extends ReadableStream<any>
? ValidateReadableStream<T, TSerializable>
: T extends Set<any>
? ValidateSerializableSet<T, TSerializable>
: T extends Map<any, any>
? ValidateSerializableMap<T, TSerializable>
: { [K in keyof T]: ValidateSerializable<T[K], TSerializable> }

export type ValidateSerializablePromise<T, TSerializable> =
T extends Promise<infer TAwaited>
Expand Down Expand Up @@ -173,13 +174,15 @@ export type ValidateSerializableInputResult<TRegister, T> =
ValidateSerializableResult<T, RegisteredSerializableInput<TRegister>>

export type ValidateSerializableResult<T, TSerializable> =
T extends TSerializable
? T
: unknown extends SerializerExtensions['ReadableStream']
? { [K in keyof T]: ValidateSerializableResult<T[K], TSerializable> }
: T extends SerializerExtensions['ReadableStream']
? ReadableStream
: { [K in keyof T]: ValidateSerializableResult<T[K], TSerializable> }
T extends ReadonlyArray<unknown>
? ResolveArrayShape<T, TSerializable, 'result'>
: T extends TSerializable
? T
: unknown extends SerializerExtensions['ReadableStream']
? { [K in keyof T]: ValidateSerializableResult<T[K], TSerializable> }
: T extends SerializerExtensions['ReadableStream']
? ReadableStream
: { [K in keyof T]: ValidateSerializableResult<T[K], TSerializable> }

export type RegisteredSSROption<TRegister> =
unknown extends RegisteredConfigType<TRegister, 'defaultSsr'>
Expand Down Expand Up @@ -213,3 +216,32 @@ export type ValidateSerializableLifecycleResultSSR<
: RegisteredSSROption<TRegister> extends false
? any
: ValidateSerializableInput<TRegister, LooseReturnType<TFn>>

type ResolveArrayShape<
T extends ReadonlyArray<unknown>,
TSerializable,
TMode extends 'input' | 'result',
> = number extends T['length']
? T extends Array<infer U>
? Array<ArrayModeResult<TMode, U, TSerializable>>
: ReadonlyArray<ArrayModeResult<TMode, T[number], TSerializable>>
: ResolveTupleShape<T, TSerializable, TMode>

type ResolveTupleShape<
T extends ReadonlyArray<unknown>,
TSerializable,
TMode extends 'input' | 'result',
> = T extends readonly [infer THead, ...infer TTail]
? readonly [
ArrayModeResult<TMode, THead, TSerializable>,
...ResolveTupleShape<Readonly<TTail>, TSerializable, TMode>,
]
: T

type ArrayModeResult<
TMode extends 'input' | 'result',
TValue,
TSerializable,
> = TMode extends 'input'
? ValidateSerializable<TValue, TSerializable>
: ValidateSerializableResult<TValue, TSerializable>
80 changes: 80 additions & 0 deletions packages/router-core/tests/serializer-recursion.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expectTypeOf, it } from 'vitest'

import type {
Serializable,
ValidateSerializable,
ValidateSerializableResult,
} from '../src/ssr/serializer/transformer'

describe('ValidateSerializable array handling', () => {
it('preserves nested array payloads for input validation', () => {
type Input = Array<{ value: string; nested: Array<{ id: number }> }>
expectTypeOf<
ValidateSerializable<Input, Serializable>
>().branded.toEqualTypeOf<Input>()
})

it('preserves tuple structure for input validation', () => {
type InputTuple = readonly [{ name: string }, { count: number }]
expectTypeOf<
ValidateSerializable<InputTuple, Serializable>
>().branded.toEqualTypeOf<InputTuple>()
})

it('preserves readonly array structure for input validation', () => {
type InputReadonlyArray = ReadonlyArray<{ value: string }>
expectTypeOf<
ValidateSerializable<InputReadonlyArray, Serializable>
>().branded.toEqualTypeOf<InputReadonlyArray>()
})

it('preserves recursive payloads wrapped in Promise for input validation', () => {
type Recursive = { value: number; next?: Recursive }
type InputPromise = Promise<Recursive>
expectTypeOf<
ValidateSerializable<InputPromise, Serializable>
>().branded.toEqualTypeOf<InputPromise>()
})

it('preserves recursive payloads wrapped in Promise<Array> for input validation', () => {
type Recursive = { value: number; children?: Array<Recursive> }
type InputPromiseArray = Promise<Array<Recursive>>
expectTypeOf<
ValidateSerializable<InputPromiseArray, Serializable>
>().branded.toEqualTypeOf<InputPromiseArray>()
})

it('preserves recursive payloads wrapped in ReadableStream for input validation', () => {
type Recursive = { value: number; next?: Recursive }
type InputStream = ReadableStream<Recursive>
expectTypeOf<
ValidateSerializable<InputStream, Serializable>
>().branded.toEqualTypeOf<InputStream>()
})

it('should preserve recursive payload without infinite expansion', () => {
type Result = Array<Result> | { [key: string]: Result }
expectTypeOf<
ValidateSerializableResult<Result, Serializable>
>().branded.toEqualTypeOf<Result>()
})

it('should preserve recursive tuples without infinite expansion', () => {
type ResultTuple = readonly [
ReadonlyArray<ResultTuple>,
{ [key: string]: ResultTuple },
]
expectTypeOf<
ValidateSerializableResult<ResultTuple, Serializable>
>().branded.toEqualTypeOf<ResultTuple>()
})

it('should preserve readonly recursive arrays without infinite expansion', () => {
type ResultReadonlyArray = ReadonlyArray<
ResultReadonlyArray | { [key: string]: ResultReadonlyArray }
>
expectTypeOf<
ValidateSerializableResult<ResultReadonlyArray, Serializable>
>().branded.toEqualTypeOf<ResultReadonlyArray>()
})
})
Loading