diff --git a/packages/react-start-client/src/tests/createServerFn.test-d.tsx b/packages/react-start-client/src/tests/createServerFn.test-d.tsx index 77f39cbedae..e7d8c7be08e 100644 --- a/packages/react-start-client/src/tests/createServerFn.test-d.tsx +++ b/packages/react-start-client/src/tests/createServerFn.test-d.tsx @@ -1,6 +1,8 @@ import { expectTypeOf, test } from 'vitest' import { createServerFn } from '@tanstack/start-client-core' +/* +// disabled until we really support RSC test.skip('createServerFn returns RSC', () => { const fn = createServerFn().handler(() => ({ rscs: [ @@ -14,4 +16,40 @@ test.skip('createServerFn returns RSC', () => { rscs: readonly [ReadableStream, ReadableStream] }> >() +})*/ + +test('createServerFn returns async array', () => { + const result: Array<{ a: number }> = [{ a: 1 }] + const serverFn = createServerFn({ method: 'GET' }).handler(async () => { + return result + }) + + expectTypeOf(serverFn()).toEqualTypeOf>>() +}) + +test('createServerFn returns sync array', () => { + const result: Array<{ a: number }> = [{ a: 1 }] + const serverFn = createServerFn({ method: 'GET' }).handler(() => { + return result + }) + + expectTypeOf(serverFn()).toEqualTypeOf>>() +}) + +test('createServerFn returns async union', () => { + const result = '1' as string | number + const serverFn = createServerFn({ method: 'GET' }).handler(async () => { + return result + }) + + expectTypeOf(serverFn()).toEqualTypeOf>() +}) + +test('createServerFn returns sync union', () => { + const result = '1' as string | number + const serverFn = createServerFn({ method: 'GET' }).handler(() => { + return result + }) + + expectTypeOf(serverFn()).toEqualTypeOf>() }) diff --git a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts index 1611a4cba6e..25b38c10357 100644 --- a/packages/start-client-core/src/client-rpc/serverFnFetcher.ts +++ b/packages/start-client-core/src/client-rpc/serverFnFetcher.ts @@ -7,7 +7,11 @@ import { import { fromCrossJSON, toJSONAsync } from 'seroval' import invariant from 'tiny-invariant' import { getDefaultSerovalPlugins } from '../getDefaultSerovalPlugins' -import { TSS_FORMDATA_CONTEXT, X_TSS_SERIALIZED } from '../constants' +import { + TSS_FORMDATA_CONTEXT, + X_TSS_RAW_RESPONSE, + X_TSS_SERIALIZED, +} from '../constants' import type { FunctionMiddlewareClientFnOptions } from '../createMiddleware' import type { Plugin as SerovalPlugin } from 'seroval' @@ -156,6 +160,9 @@ async function getResponse(fn: () => Promise) { } })() + if (response.headers.get(X_TSS_RAW_RESPONSE) === 'true') { + return response + } const contentType = response.headers.get('content-type') invariant(contentType, 'expected content-type header to be set') const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED) diff --git a/packages/start-client-core/src/constants.ts b/packages/start-client-core/src/constants.ts index 0a39ffaeed3..1e541af3231 100644 --- a/packages/start-client-core/src/constants.ts +++ b/packages/start-client-core/src/constants.ts @@ -5,4 +5,5 @@ export const TSS_SERVER_FUNCTION_FACTORY = Symbol.for( ) export const X_TSS_SERIALIZED = 'x-tss-serialized' +export const X_TSS_RAW_RESPONSE = 'x-tss-raw' export {} diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 3ae9a3682cc..e4941c58715 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -14,7 +14,6 @@ import type { ResolveValidatorInput, ValidateSerializable, ValidateSerializableInput, - ValidateSerializableInputResult, Validator, } from '@tanstack/router-core' import type { JsonResponse } from '@tanstack/router-core/ssr/client' @@ -174,7 +173,7 @@ export async function executeMiddleware( env: 'client' | 'server', opts: ServerFnMiddlewareOptions, ): Promise { - const globalMiddlewares = getStartOptions().functionMiddleware || [] + const globalMiddlewares = getStartOptions()?.functionMiddleware || [] const flattenedMiddlewares = flattenMiddlewares([ ...globalMiddlewares, ...middlewares, @@ -243,10 +242,10 @@ export type CompiledFetcherFnOptions = { context?: any } -export type Fetcher = +export type Fetcher = undefined extends IntersectAllValidatorInputs - ? OptionalFetcher - : RequiredFetcher + ? OptionalFetcher + : RequiredFetcher export interface FetcherBase { [TSS_SERVER_FUNCTION]: true @@ -260,26 +259,18 @@ export interface FetcherBase { }) => Promise } -export interface OptionalFetcher< - TRegister, - TMiddlewares, - TInputValidator, - TResponse, -> extends FetcherBase { +export interface OptionalFetcher + extends FetcherBase { ( options?: OptionalFetcherDataOptions, - ): Promise> + ): Promise> } -export interface RequiredFetcher< - TRegister, - TMiddlewares, - TInputValidator, - TResponse, -> extends FetcherBase { +export interface RequiredFetcher + extends FetcherBase { ( opts: RequiredFetcherDataOptions, - ): Promise> + ): Promise> } export type FetcherBaseOptions = { @@ -297,22 +288,23 @@ export interface RequiredFetcherDataOptions data: Expand> } -export type FetcherData = TResponse extends Response - ? Response - : TResponse extends JsonResponse - ? ValidateSerializableInputResult> - : ValidateSerializableInputResult - export type RscStream = { __cacheState: T } export type Method = 'GET' | 'POST' +export type FetcherData = + Awaited extends Response + ? Awaited + : Awaited extends JsonResponse + ? ReturnType['json']> + : Awaited + export type ServerFnReturnType = - | Response - | Promise> - | ValidateSerializableInput + Awaited extends Response + ? TResponse + : ValidateSerializableInput export type ServerFn< TRegister, @@ -521,7 +513,7 @@ export interface ServerFnHandler< TInputValidator, TNewResponse >, - ) => Fetcher + ) => Fetcher } export interface ServerFnBuilder diff --git a/packages/start-client-core/src/getDefaultSerovalPlugins.ts b/packages/start-client-core/src/getDefaultSerovalPlugins.ts index 3c4fb6c3224..61e48b9d896 100644 --- a/packages/start-client-core/src/getDefaultSerovalPlugins.ts +++ b/packages/start-client-core/src/getDefaultSerovalPlugins.ts @@ -7,7 +7,7 @@ import type { AnySerializationAdapter } from '@tanstack/router-core' export function getDefaultSerovalPlugins() { const start = getStartOptions() - const adapters = start.serializationAdapters as + const adapters = start?.serializationAdapters as | Array | undefined return [ diff --git a/packages/start-client-core/src/getStartOptions.ts b/packages/start-client-core/src/getStartOptions.ts index d4ba38cf4e1..7c08e1f49a6 100644 --- a/packages/start-client-core/src/getStartOptions.ts +++ b/packages/start-client-core/src/getStartOptions.ts @@ -1,6 +1,8 @@ import { getStartContext } from '@tanstack/start-storage-context' import { createIsomorphicFn } from './createIsomorphicFn' +import type { AnyStartInstanceOptions } from './createStart' -export const getStartOptions = createIsomorphicFn() - .client(() => window.__TSS_START_OPTIONS__!) - .server(() => getStartContext().startOptions) +export const getStartOptions: () => AnyStartInstanceOptions | undefined = + createIsomorphicFn() + .client(() => window.__TSS_START_OPTIONS__) + .server(() => getStartContext().startOptions) diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx index 2552f31a156..abb082d02b7 100644 --- a/packages/start-client-core/src/index.tsx +++ b/packages/start-client-core/src/index.tsx @@ -85,6 +85,7 @@ export { TSS_FORMDATA_CONTEXT, TSS_SERVER_FUNCTION, X_TSS_SERIALIZED, + X_TSS_RAW_RESPONSE, } from './constants' export type * from './serverRoute' diff --git a/packages/start-client-core/src/tests/createServerFn.test-d.ts b/packages/start-client-core/src/tests/createServerFn.test-d.ts index 177c679774c..47718d74f2b 100644 --- a/packages/start-client-core/src/tests/createServerFn.test-d.ts +++ b/packages/start-client-core/src/tests/createServerFn.test-d.ts @@ -317,11 +317,7 @@ test('createServerFn returns undefined', () => { test('createServerFn cannot return function', () => { expectTypeOf(createServerFn().handler<{ func: () => 'func' }>) .parameter(0) - .returns.toEqualTypeOf< - | Response - | { func: 'Function is not serializable' } - | Promise<{ func: 'Function is not serializable' }> - >() + .returns.toEqualTypeOf<{ func: 'Function is not serializable' }>() }) test('createServerFn cannot validate function', () => { @@ -598,3 +594,47 @@ test('createServerFn fetcher itself is serializable', () => { const fn1 = createServerFn().handler(() => ({})) const fn2 = createServerFn().handler(() => fn1) }) + +test('createServerFn returns async Response', () => { + const serverFn = createServerFn().handler(async () => { + return new Response(new Blob([JSON.stringify({ a: 1 })]), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + }) + + expectTypeOf(serverFn()).toEqualTypeOf>() +}) + +test('createServerFn returns sync Response', () => { + const serverFn = createServerFn().handler(() => { + return new Response(new Blob([JSON.stringify({ a: 1 })]), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }) + }) + + expectTypeOf(serverFn()).toEqualTypeOf>() +}) + +test('createServerFn returns async array', () => { + const result: Array<{ a: number }> = [{ a: 1 }] + const serverFn = createServerFn({ method: 'GET' }).handler(async () => { + return result + }) + + expectTypeOf(serverFn()).toEqualTypeOf>>() +}) + +test('createServerFn returns sync array', () => { + const result: Array<{ a: number }> = [{ a: 1 }] + const serverFn = createServerFn({ method: 'GET' }).handler(() => { + return result + }) + + expectTypeOf(serverFn()).toEqualTypeOf>>() +}) diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts index 47cf996f75c..d6beb47ac98 100644 --- a/packages/start-server-core/src/server-functions-handler.ts +++ b/packages/start-server-core/src/server-functions-handler.ts @@ -2,6 +2,7 @@ import { isNotFound } from '@tanstack/router-core' import invariant from 'tiny-invariant' import { TSS_FORMDATA_CONTEXT, + X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED, getDefaultSerovalPlugins, } from '@tanstack/start-client-core' @@ -145,6 +146,7 @@ export const handleServerAction = async ({ // Any time we get a Response back, we should just // return it immediately. if (result.result instanceof Response) { + result.result.headers.set(X_TSS_RAW_RESPONSE, 'true') return result.result }