From 552138186d9794fda48e38dc2a549a5639baa0ec Mon Sep 17 00:00:00 2001 From: chorobin Date: Wed, 20 Nov 2024 00:34:03 +0100 Subject: [PATCH 1/7] fix(start): allow functions in types --- packages/start/src/client/createServerFn.ts | 14 +++++--------- ...rFn.test-d.ts => createServerFn.test-d.tsx} | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) rename packages/start/src/client/tests/{createServerFn.test-d.ts => createServerFn.test-d.tsx} (89%) diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index d450abcfb80..a1d1ebad0d6 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -65,15 +65,11 @@ export type FetcherData = WrapRSCs< TResponse extends JsonResponse ? TData : TResponse > -export type WrapRSCs = T extends JSX.Element - ? ReadableStream - : T extends Record - ? { - [K in keyof T]: WrapRSCs - } - : T extends Array - ? Array> - : T +export type WrapRSCs = T extends (...args: any[]) => any + ? T + : T extends JSX.Element + ? ReadableStream + : { [K in keyof T]: WrapRSCs } export type RscStream = { __cacheState: T diff --git a/packages/start/src/client/tests/createServerFn.test-d.ts b/packages/start/src/client/tests/createServerFn.test-d.tsx similarity index 89% rename from packages/start/src/client/tests/createServerFn.test-d.ts rename to packages/start/src/client/tests/createServerFn.test-d.tsx index 3ef1f893059..48f4106c018 100644 --- a/packages/start/src/client/tests/createServerFn.test-d.ts +++ b/packages/start/src/client/tests/createServerFn.test-d.tsx @@ -173,3 +173,21 @@ 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>() +}) + +test('createServerFn returns RSC', () => { + const fn = createServerFn().handler(() => ({ + rscs: [
I'm an RSC
,
I'm an RSC
] as const, + })) + + expectTypeOf(fn()).toEqualTypeOf< + Promise<{ rscs: readonly [ReadableStream, ReadableStream] }> + >() +}) From fd359a2d66aaaeacc0ea840dddf0ef31cbc78b1e Mon Sep 17 00:00:00 2001 From: chorobin Date: Wed, 20 Nov 2024 00:44:34 +0100 Subject: [PATCH 2/7] fix(start): implement start serialization on the type level --- packages/start/src/client/createServerFn.ts | 10 ++++++++-- .../start/src/client/tests/createServerFn.test-d.tsx | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index a1d1ebad0d6..904c7527cb7 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -65,11 +65,17 @@ export type FetcherData = WrapRSCs< TResponse extends JsonResponse ? TData : TResponse > -export type WrapRSCs = T extends (...args: any[]) => any +export type NonSerializable = (...args: any[]) => any + +export type Serializable = Date | undefined + +export type WrapRSCs = T extends Serializable ? T : T extends JSX.Element ? ReadableStream - : { [K in keyof T]: WrapRSCs } + : T extends NonSerializable + ? never + : { [K in keyof T]: WrapRSCs } export type RscStream = { __cacheState: T diff --git a/packages/start/src/client/tests/createServerFn.test-d.tsx b/packages/start/src/client/tests/createServerFn.test-d.tsx index 48f4106c018..cc6178dc9f6 100644 --- a/packages/start/src/client/tests/createServerFn.test-d.tsx +++ b/packages/start/src/client/tests/createServerFn.test-d.tsx @@ -191,3 +191,11 @@ test('createServerFn returns RSC', () => { Promise<{ rscs: readonly [ReadableStream, ReadableStream] }> >() }) + +test('createServerFn cannot return function', () => { + const fn = createServerFn().handler(() => ({ + func: () => 'func', + })) + + expectTypeOf(fn()).toEqualTypeOf>() +}) From 11f1743b9161555b5b99401afd1c5e3b4544213e Mon Sep 17 00:00:00 2001 From: chorobin Date: Wed, 20 Nov 2024 01:04:01 +0100 Subject: [PATCH 3/7] fix(start): validate createServerFn to not allow functions --- packages/start/src/client/createServerFn.ts | 12 ++++++++---- .../src/client/tests/createServerFn.test-d.tsx | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index 904c7527cb7..be75727e1eb 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -73,9 +73,13 @@ export type WrapRSCs = T extends Serializable ? T : T extends JSX.Element ? ReadableStream - : T extends NonSerializable - ? never - : { [K in keyof T]: WrapRSCs } + : { [K in keyof T]: WrapRSCs } + +export type ValidateRSCs = T extends Serializable + ? T + : T extends NonSerializable + ? 'Function is not serializable' + : { [K in keyof T]: ValidateRSCs } export type RscStream = { __cacheState: T @@ -85,7 +89,7 @@ export type Method = 'GET' | 'POST' export type ServerFn = ( ctx: ServerFnCtx, -) => Promise | TResponse +) => Promise> | ValidateRSCs export type ServerFnCtx = { method: TMethod diff --git a/packages/start/src/client/tests/createServerFn.test-d.tsx b/packages/start/src/client/tests/createServerFn.test-d.tsx index cc6178dc9f6..ae3bda80587 100644 --- a/packages/start/src/client/tests/createServerFn.test-d.tsx +++ b/packages/start/src/client/tests/createServerFn.test-d.tsx @@ -192,10 +192,19 @@ test('createServerFn returns RSC', () => { >() }) -test('createServerFn cannot return function', () => { +test('createServerFn returns undefined', () => { const fn = createServerFn().handler(() => ({ - func: () => 'func', + nothing: undefined, })) - expectTypeOf(fn()).toEqualTypeOf>() + expectTypeOf(fn()).toEqualTypeOf>() +}) + +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' }> + >() }) From 7dbf06dda71c8d40f8af05f814639416a5de4b00 Mon Sep 17 00:00:00 2001 From: chorobin Date: Wed, 20 Nov 2024 21:48:36 +0100 Subject: [PATCH 4/7] refactor(start): make transformer types generic --- packages/react-router/src/index.tsx | 8 +++++- packages/react-router/src/transformer.ts | 19 +++++++++++++++ packages/start/src/client/createServerFn.ts | 27 +++++++++------------ packages/start/src/client/index.tsx | 1 - 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx index 02a2b230b05..bda48bbecf9 100644 --- a/packages/react-router/src/index.tsx +++ b/packages/react-router/src/index.tsx @@ -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' diff --git a/packages/react-router/src/transformer.ts b/packages/react-router/src/transformer.ts index 2277a3dbb8d..15a6af55e77 100644 --- a/packages/react-router/src/transformer.ts +++ b/packages/react-router/src/transformer.ts @@ -47,3 +47,22 @@ const transformers = [ parse: () => undefined, }, ] as const + +export type TransformerStringify = T extends TSerializable + ? T + : T extends (...args: Array) => any + ? 'Function is not serializable' + : { [K in keyof T]: TransformerStringify } + +export type TransformerParse = T extends TSerializable + ? T + : T extends JSX.Element + ? ReadableStream + : { [K in keyof T]: TransformerParse } + +export type DefaultTransformerStringify = TransformerStringify< + T, + Date | undefined +> + +export type DefaultTransformerParse = TransformerParse diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index be75727e1eb..c6b3b04622b 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -1,7 +1,12 @@ 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, +} from '@tanstack/react-router' import type { AnyMiddleware, MergeAllServerContext, @@ -61,26 +66,14 @@ export interface OptionalFetcherDataOptions extends FetcherBaseOptions { data?: TInput } -export type FetcherData = WrapRSCs< +export type FetcherData = DefaultTransformerParse< TResponse extends JsonResponse ? TData : TResponse > -export type NonSerializable = (...args: any[]) => any +export type NonSerializable = (...args: Array) => any export type Serializable = Date | undefined -export type WrapRSCs = T extends Serializable - ? T - : T extends JSX.Element - ? ReadableStream - : { [K in keyof T]: WrapRSCs } - -export type ValidateRSCs = T extends Serializable - ? T - : T extends NonSerializable - ? 'Function is not serializable' - : { [K in keyof T]: ValidateRSCs } - export type RscStream = { __cacheState: T } @@ -89,7 +82,9 @@ export type Method = 'GET' | 'POST' export type ServerFn = ( ctx: ServerFnCtx, -) => Promise> | ValidateRSCs +) => + | Promise> + | DefaultTransformerStringify export type ServerFnCtx = { method: TMethod diff --git a/packages/start/src/client/index.tsx b/packages/start/src/client/index.tsx index bdedb47f085..5a2361e8e66 100644 --- a/packages/start/src/client/index.tsx +++ b/packages/start/src/client/index.tsx @@ -9,7 +9,6 @@ export { type CompiledFetcherFn, type Fetcher, type RscStream, - type WrapRSCs, type FetcherImpl, type FetcherData, type FetcherBaseOptions, From d60c86c18f0c1f1bbcea8529a613c0d0c9ad2999 Mon Sep 17 00:00:00 2001 From: chorobin Date: Wed, 20 Nov 2024 22:58:56 +0100 Subject: [PATCH 5/7] feat(start): add type checking of validator and context --- packages/start/src/client/createMiddleware.ts | 11 ++--- packages/start/src/client/createServerFn.ts | 16 +++++++- .../client/tests/createServerFn.test-d.tsx | 19 ++++++++- .../tests/createServerMiddleware.test-d.ts | 40 ++++++++++++++++++- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/packages/start/src/client/createMiddleware.ts b/packages/start/src/client/createMiddleware.ts index 2351d9c5325..f59f02b7068 100644 --- a/packages/start/src/client/createMiddleware.ts +++ b/packages/start/src/client/createMiddleware.ts @@ -1,7 +1,8 @@ -import type { Method } from './createServerFn' +import type { ConstrainValidator, Method } from './createServerFn' import type { AnyValidator, Constrain, + DefaultTransformerStringify, Expand, MergeAll, ResolveValidatorInput, @@ -129,7 +130,7 @@ export interface MiddlewareOptions< > { validateClient?: boolean middleware?: TMiddlewares - validator?: Constrain + validator?: ConstrainValidator client?: MiddlewareClientFn< TMiddlewares, TValidator, @@ -166,7 +167,7 @@ export type MiddlewareServerFn< TNewClientAfterContext = undefined, >(ctx?: { context?: TNewServerContext - sendContext?: TNewClientAfterContext + sendContext?: DefaultTransformerStringify }) => Promise< ServerResultWithContext > @@ -186,7 +187,7 @@ export type MiddlewareClientFn< method: Method next: (ctx?: { context?: TNewClientContext - sendContext?: TNewServerContext + sendContext?: DefaultTransformerStringify headers?: HeadersInit }) => Promise> }) => @@ -272,7 +273,7 @@ export interface MiddlewareValidator< TClientAfterContext, > { validator: ( - input: TNewValidator, + input: ConstrainValidator, ) => MiddlewareAfterMiddleware< TId, TMiddlewares, diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index c6b3b04622b..c159a463e11 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -6,6 +6,8 @@ import type { Constrain, DefaultTransformerParse, DefaultTransformerStringify, + ResolveValidatorInput, + Validator, } from '@tanstack/react-router' import type { AnyMiddleware, @@ -106,13 +108,23 @@ type ServerFnBaseOptions< method: TMethod validateClient?: boolean middleware?: Constrain> - validator?: Constrain + validator?: ConstrainValidator extractedFn?: CompiledFetcherFn serverFn?: ServerFn filename: string functionId: string } +export type ConstrainValidator = unknown extends TValidator + ? TValidator + : Constrain< + TValidator, + Validator< + DefaultTransformerStringify>, + any + > + > + type ServerFnBase< TMethod extends Method = 'GET', TResponse = unknown, @@ -127,7 +139,7 @@ type ServerFnBase< 'validator' | 'handler' > validator: ( - validator: Constrain, + validator: ConstrainValidator, ) => Pick< ServerFnBase, 'handler' | 'middleware' diff --git a/packages/start/src/client/tests/createServerFn.test-d.tsx b/packages/start/src/client/tests/createServerFn.test-d.tsx index ae3bda80587..1bf0222c13b 100644 --- a/packages/start/src/client/tests/createServerFn.test-d.tsx +++ b/packages/start/src/client/tests/createServerFn.test-d.tsx @@ -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) => { @@ -184,7 +185,10 @@ test('createServerFn returns Date', () => { test('createServerFn returns RSC', () => { const fn = createServerFn().handler(() => ({ - rscs: [
I'm an RSC
,
I'm an RSC
] as const, + rscs: [ +
I'm an RSC
, +
I'm an RSC
, + ] as const, })) expectTypeOf(fn()).toEqualTypeOf< @@ -208,3 +212,16 @@ test('createServerFn cannot return function', () => { | Promise<{ func: 'Function is not serializable' }> >() }) + +test('createServerFn cannot validate function', () => { + const validator = createServerFn().validator<() => { func: () => 'string' }> + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain< + () => { func: () => 'string' }, + Validator<{ func: 'Function is not serializable' }, any> + > + >() +}) diff --git a/packages/start/src/client/tests/createServerMiddleware.test-d.ts b/packages/start/src/client/tests/createServerMiddleware.test-d.ts index d3f464e9957..0ac62d58d8a 100644 --- a/packages/start/src/client/tests/createServerMiddleware.test-d.ts +++ b/packages/start/src/client/tests/createServerMiddleware.test-d.ts @@ -1,5 +1,6 @@ import { expectTypeOf, test } from 'vitest' import { createMiddleware } from '../createMiddleware' +import { Constrain, Validator } from '@tanstack/react-router' test('createServeMiddleware removes middleware after middleware,', () => { const middleware = createMiddleware() @@ -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' }, @@ -244,8 +245,43 @@ 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() + .toHaveProperty('sendContext') + .toEqualTypeOf<{ func: 'Function is not serializable' } | undefined>() + + return next() + }) + .server(({ next }) => { + expectTypeOf(next 'func' }>) + .parameter(0) + .exclude() + .toHaveProperty('sendContext') + .toEqualTypeOf<{ func: 'Function is not serializable' } | undefined>() + + return next() + }) +}) + +test('createMiddleware cannot validate function', () => { + const validator = createMiddleware().validator<() => { func: () => 'string' }> + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain< + () => { func: () => 'string' }, + Validator<{ func: 'Function is not serializable' }, any> + > + >() +}) From dfa4135b7e45a4b863cdfd4d2a0588c91b5d6ef3 Mon Sep 17 00:00:00 2001 From: chorobin Date: Wed, 20 Nov 2024 23:11:37 +0100 Subject: [PATCH 6/7] chore: fix linting --- .../start/src/client/tests/createServerMiddleware.test-d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start/src/client/tests/createServerMiddleware.test-d.ts b/packages/start/src/client/tests/createServerMiddleware.test-d.ts index 0ac62d58d8a..5e52b9a4203 100644 --- a/packages/start/src/client/tests/createServerMiddleware.test-d.ts +++ b/packages/start/src/client/tests/createServerMiddleware.test-d.ts @@ -1,6 +1,6 @@ import { expectTypeOf, test } from 'vitest' import { createMiddleware } from '../createMiddleware' -import { Constrain, Validator } from '@tanstack/react-router' +import type { Constrain, Validator } from '@tanstack/react-router' test('createServeMiddleware removes middleware after middleware,', () => { const middleware = createMiddleware() From ae0914f95c8c386d4c48588bad8878068edf766f Mon Sep 17 00:00:00 2001 From: chorobin Date: Thu, 21 Nov 2024 19:31:18 +0100 Subject: [PATCH 7/7] chore: remove serializable types --- packages/start/src/client/createServerFn.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index c159a463e11..1bc91498126 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -72,10 +72,6 @@ export type FetcherData = DefaultTransformerParse< TResponse extends JsonResponse ? TData : TResponse > -export type NonSerializable = (...args: Array) => any - -export type Serializable = Date | undefined - export type RscStream = { __cacheState: T }