diff --git a/packages/remix-server-runtime/jsonify.ts b/packages/remix-server-runtime/jsonify.ts index 80a6c2b5a..80e524ff5 100644 --- a/packages/remix-server-runtime/jsonify.ts +++ b/packages/remix-server-runtime/jsonify.ts @@ -21,6 +21,9 @@ export type Jsonify = T extends Number ? number : T extends Boolean ? boolean : + // Promises JSON.stringify to an empty object + T extends Promise ? EmptyObject : + // Map & Set T extends Map ? EmptyObject : T extends Set ? EmptyObject : @@ -119,6 +122,7 @@ type _tests = [ Expect, string>>, Expect, number>>, Expect, boolean>>, + Expect>, EmptyObject>>, // Map & Set Expect>, EmptyObject>>, @@ -251,7 +255,7 @@ type NeverToNull = [T] extends [never] ? null : T; // adapted from https://github.com/sindresorhus/type-fest/blob/main/source/empty-object.d.ts declare const emptyObjectSymbol: unique symbol; -type EmptyObject = { [emptyObjectSymbol]?: never }; +export type EmptyObject = { [emptyObjectSymbol]?: never }; // adapted from https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts type IsAny = 0 extends 1 & T ? true : false; diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index f9bacaf54..807276a67 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -54,7 +54,7 @@ type ClientActionFunction = ( * Arguments passed to a route `clientAction` function * @private Public API is exported from @remix-run/react */ -type ClientActionFunctionArgs = RRActionFunctionArgs & { +export type ClientActionFunctionArgs = RRActionFunctionArgs & { serverAction: () => Promise>; }; @@ -87,7 +87,7 @@ type ClientLoaderFunction = (( * Arguments passed to a route `clientLoader` function * @private Public API is exported from @remix-run/react */ -type ClientLoaderFunctionArgs = RRLoaderFunctionArgs & { +export type ClientLoaderFunctionArgs = RRLoaderFunctionArgs & { serverLoader: () => Promise>; }; diff --git a/packages/remix-server-runtime/serialize.ts b/packages/remix-server-runtime/serialize.ts index 4a2a8a3a7..c3d4822f4 100644 --- a/packages/remix-server-runtime/serialize.ts +++ b/packages/remix-server-runtime/serialize.ts @@ -1,21 +1,60 @@ -import type { Jsonify } from "./jsonify"; +import type { EmptyObject, Jsonify } from "./jsonify"; import type { TypedDeferredData, TypedResponse } from "./responses"; +import type { + ClientActionFunctionArgs, + ClientLoaderFunctionArgs, +} from "./routeModules"; import { expectType } from "./typecheck"; import { type Expect, type Equal } from "./typecheck"; // prettier-ignore /** - * Infer JSON serialized data type returned by a loader or action. + * Infer JSON serialized data type returned by a loader or action, while + * avoiding deserialization if the input type if it's a clientLoader or + * clientAction that returns a non-Response * * For example: * `type LoaderData = SerializeFrom` */ export type SerializeFrom = - T extends (...args: any[]) => infer Output ? Serialize> : + T extends (...args: any[]) => infer Output ? + Parameters extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? + // Client data functions may not serialize + SerializeClient> + : + // Serialize responses + Serialize> + : // Back compat: manually defined data type, not inferred from loader nor action Jsonify> ; +// note: cannot be inlined as logic requires union distribution +// prettier-ignore +type SerializeClient = + Output extends TypedDeferredData ? + // top-level promises + & { + [K in keyof U as K extends symbol + ? never + : Promise extends U[K] + ? K + : never]: DeferValueClient; // use generic to distribute over union + } + // non-promises + & { + [K in keyof U as Promise extends U[K] ? never : K]: U[K]; + } + : + Output extends TypedResponse ? Jsonify : + Awaited + +// prettier-ignore +type DeferValueClient = + T extends undefined ? undefined : + T extends Promise ? Promise> : + T; + // note: cannot be inlined as logic requires union distribution // prettier-ignore type Serialize = @@ -49,16 +88,45 @@ type DeferValue = type Pretty = { [K in keyof T]: T[K] }; -type Loader = () => Promise< - | TypedResponse // returned responses - | TypedResponse // thrown responses ->; +type Loader = () => Promise>; type LoaderDefer> = () => Promise< - | TypedDeferredData // returned responses - | TypedResponse // thrown responses + TypedDeferredData +>; + +type LoaderBoth< + T1 extends Record, + T2 extends Record +> = () => Promise | TypedDeferredData>; + +type ClientLoaderRaw> = ({ + request, +}: ClientLoaderFunctionArgs) => Promise; // returned non-Response + +type ClientLoaderResponse> = ({ + request, +}: ClientLoaderFunctionArgs) => Promise>; // returned responses + +type ClientLoaderDefer> = ({ + request, +}: ClientLoaderFunctionArgs) => Promise>; // returned responses + +type ClientLoaderResponseAndDefer< + T1 extends Record, + T2 extends Record +> = ({ + request, +}: ClientLoaderFunctionArgs) => Promise< + TypedResponse | TypedDeferredData >; +type ClientLoaderRawAndDefer< + T1 extends Record, + T2 extends Record +> = ({ + request, +}: ClientLoaderFunctionArgs) => Promise>; + // prettier-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars type _tests = [ @@ -78,7 +146,27 @@ type _tests = [ Expect>>, {a: string, name: number, data: boolean}>>, // defer top-level promises - Expect}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false> + Expect}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false>, + + // conditional defer or json + Expect }, { c: string; lazy: Promise<{ d: number }>}>> extends { a: string, b: EmptyObject } | { c: string; lazy: Promise<{ d: number }> } ? true : false>, + + // clientLoader raw JSON + Expect>>, {a: string}>>, + Expect }>>>, {a: Date, b: Map}>>, + + // clientLoader json() Response + Expect>>, {a: string}>>, + Expect>>, {a: string}>>, + + // clientLoader defer() data + Expect}>> extends {a: string, lazy: Promise<{ b: number }>} ? true : false>, + + // clientLoader conditional defer or json + Expect }, { c: string; lazy: Promise<{ d: number }>}>> extends { a: string, b: EmptyObject } | { c: string; lazy: Promise<{ d: number }> } ? true : false>, + + // clientLoader conditional defer or raw + Expect }, { c: string; lazy: Promise<{ d: number }>}>> extends { a: string, b: Promise } | { c: string; lazy: Promise<{ d: number }> } ? true : false>, ]; // recursive