Skip to content

Commit

Permalink
serialize non-native objects via their enumerable string properties
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori committed Aug 27, 2024
1 parent e872696 commit e2083ad
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 10 deletions.
5 changes: 4 additions & 1 deletion packages/remix-react/single-fetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,13 @@ export function decodeViaTurboStream(
return { value: { [SingleFetchRedirectSymbol]: rest[0] } };
}
},
(type) => {
(type, value) => {
if (type === "SingleFetchFallback") {
return { value: undefined };
}
if (type === "SingleFetchClassInstance") {
return { value };
}
},
],
});
Expand Down
14 changes: 6 additions & 8 deletions packages/remix-server-runtime/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ import { type Expect, type Equal } from "./typecheck";
import { type SerializeFrom as SingleFetch_SerializeFrom } from "./single-fetch";
import type { Future } from "./future";

type SingleFetchEnabled = Future extends {
singleFetch: infer T extends boolean;
}
? T
: false;
// prettier-ignore
type SingleFetchEnabled =
Future extends { singleFetch: infer T extends boolean } ? T : false

// prettier-ignore
/**
Expand All @@ -26,8 +24,9 @@ type SingleFetchEnabled = Future extends {
*/
export type SerializeFrom<T> =
SingleFetchEnabled extends true ?
T extends (...args: []) => unknown ? SingleFetch_SerializeFrom<T> :
never
T extends (...args: []) => unknown ?
SingleFetch_SerializeFrom<T> :
never
:
T extends (...args: any[]) => infer Output ?
Parameters<T> extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ?
Expand All @@ -39,7 +38,6 @@ export type SerializeFrom<T> =
:
// Back compat: manually defined data type, not inferred from loader nor action
Jsonify<Awaited<T>>
;

// note: cannot be inlined as logic requires union distribution
// prettier-ignore
Expand Down
166 changes: 165 additions & 1 deletion packages/remix-server-runtime/single-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@remix-run/router";
import { encode } from "turbo-stream";

import { type Expect, type Equal } from "./typecheck";
import type { ServerBuild } from "./build";
import type { AppLoadContext } from "./data";
import { sanitizeError, sanitizeErrors } from "./errors";
Expand All @@ -20,6 +21,11 @@ import { ServerMode } from "./mode";
import type { TypedResponse } from "./responses";
import { isRedirectStatusCode, isResponse } from "./responses";
import type { Jsonify } from "./jsonify";
import type {
ClientActionFunctionArgs,
ClientLoaderFunctionArgs,
LoaderFunctionArgs,
} from "./routeModules";

export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect");

Expand Down Expand Up @@ -350,7 +356,18 @@ export function encodeViaTurboStream(
}
},
],
postPlugins: [() => ["SingleFetchFallback"]],
postPlugins: [
(value) => {
if (!value) return;
if (typeof value !== "object") return;

return [
"SingleFetchClassInstance",
Object.fromEntries(Object.entries(value)),
];
},
() => ["SingleFetchFallback"],
],
});
}

Expand Down Expand Up @@ -417,6 +434,153 @@ type Fn = (...args: any[]) => unknown;
// json/defer will be removed so everything will use the new simplified typings.
// prettier-ignore
export type SerializeFrom<T extends Fn> =
Parameters<T> extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ?
ReturnType<T> extends TypedResponse<infer U> ? Jsonify<U> :
Awaited<ReturnType<T>>
:
Awaited<ReturnType<T>> extends TypedResponse<Record<string, unknown>> ? Jsonify<T> :
Awaited<ReturnType<T>> extends DataWithResponseInit<infer D> ? Serialize<D> :
Serialize<Awaited<ReturnType<T>>>;

type ServerLoader<T> = (args: LoaderFunctionArgs) => T;
type ClientLoader<T> = (args: ClientLoaderFunctionArgs) => T;

class TestClass {
constructor(public a: string, public b: Date) {
this.a = a;
this.b = b;
}

testmethod() {}
}

interface TestInterface {
undefined: undefined;
null: null;
boolean: boolean;
string: string;
symbol: symbol;
number: number;
bigint: bigint;
Date: Date;
URL: URL;
RegExp: RegExp;
Error: Error;
Set: Set<Error>;
Map: Map<Date, RegExp>;
}

type Recursive = {
a: string;
b: Date;
recursive?: Recursive;
};

// prettier-ignore
// eslint-disable-next-line
type _tests = [
Expect<Equal<SerializeFrom<ServerLoader<void>>, undefined>>,
Expect<Equal<SerializeFrom<ServerLoader<{
undefined: undefined,
null: null,
boolean: boolean,
string: string,
symbol: symbol,
number: number,
bigint: bigint,
Date: Date,
URL: URL,
RegExp: RegExp,
Error: Error,
Set: Set<Error>,
Map: Map<Date, RegExp>,
TestInterface: TestInterface,
Recursive: Recursive
}>>, {
undefined: undefined,
null: null,
boolean: boolean,
string: string,
symbol: symbol,
number: number,
bigint: bigint,
Date: Date,
URL: URL,
RegExp: RegExp,
Error: Error,
Set: Set<Error>,
Map: Map<Date, RegExp>,
TestInterface: TestInterface
Recursive: Recursive
}>>,
Expect<Equal<SerializeFrom<ServerLoader<[
undefined,
null,
boolean,
string,
symbol,
number,
bigint,
Date,
URL,
RegExp,
Error
]>>, [
undefined,
null,
boolean,
string,
symbol,
number,
bigint,
Date,
URL,
RegExp,
Error
]>>,
Expect<Equal<SerializeFrom<ServerLoader<Promise<[
undefined,
null,
boolean,
string,
symbol,
number,
bigint,
Date,
URL,
RegExp,
Error
]>>>, [
undefined,
null,
boolean,
string,
symbol,
number,
bigint,
Date,
URL,
RegExp,
Error
]>>,

Expect<Equal<SerializeFrom<ServerLoader<{
function: () => void,
class: TestClass
}>>, {
function: undefined,
class: {
a: string
b: Date,
testmethod: undefined
},
}>>,

Expect<Equal<SerializeFrom<ClientLoader<{
function: () => void,
class: TestClass
}>>, {
function: () => void,
class: TestClass
}>>,
]

0 comments on commit e2083ad

Please sign in to comment.