Skip to content

Commit f770758

Browse files
fix: allow to express dependencies between serialization adapters
1 parent 581941a commit f770758

File tree

9 files changed

+229
-32
lines changed

9 files changed

+229
-32
lines changed

e2e/react-start/serialization-adapters/src/data.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,38 @@ export function makeData() {
8888
},
8989
}
9090
}
91+
export class NestedOuter {
92+
constructor(public inner: NestedInner) {}
93+
whisper() {
94+
return this.inner.value.toLowerCase()
95+
}
96+
}
97+
98+
export class NestedInner {
99+
constructor(public value: string) {}
100+
shout() {
101+
return this.value.toUpperCase()
102+
}
103+
}
104+
105+
export const nestedInnerAdapter = createSerializationAdapter({
106+
key: 'nestedInner',
107+
test: (value): value is NestedInner => value instanceof NestedInner,
108+
toSerializable: (inner) => inner.value,
109+
fromSerializable: (value) => new NestedInner(value),
110+
})
111+
112+
export const nestedOuterAdapter = createSerializationAdapter({
113+
key: 'nestedOuter',
114+
extends: [nestedInnerAdapter],
115+
test: (value) => value instanceof NestedOuter,
116+
toSerializable: (outer) => outer.inner,
117+
fromSerializable: (value) => new NestedOuter(value),
118+
})
119+
120+
export function makeNested() {
121+
return new NestedOuter(new NestedInner('Hello World'))
122+
}
91123

92124
export function RenderData({
93125
id,

e2e/react-start/serialization-adapters/src/routeTree.gen.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as IndexRouteImport } from './routes/index'
1313
import { Route as SsrStreamRouteImport } from './routes/ssr/stream'
14+
import { Route as SsrNestedRouteImport } from './routes/ssr/nested'
1415
import { Route as SsrDataOnlyRouteImport } from './routes/ssr/data-only'
1516
import { Route as ServerFunctionCustomErrorRouteImport } from './routes/server-function/custom-error'
1617

@@ -24,6 +25,11 @@ const SsrStreamRoute = SsrStreamRouteImport.update({
2425
path: '/ssr/stream',
2526
getParentRoute: () => rootRouteImport,
2627
} as any)
28+
const SsrNestedRoute = SsrNestedRouteImport.update({
29+
id: '/ssr/nested',
30+
path: '/ssr/nested',
31+
getParentRoute: () => rootRouteImport,
32+
} as any)
2733
const SsrDataOnlyRoute = SsrDataOnlyRouteImport.update({
2834
id: '/ssr/data-only',
2935
path: '/ssr/data-only',
@@ -40,19 +46,22 @@ export interface FileRoutesByFullPath {
4046
'/': typeof IndexRoute
4147
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
4248
'/ssr/data-only': typeof SsrDataOnlyRoute
49+
'/ssr/nested': typeof SsrNestedRoute
4350
'/ssr/stream': typeof SsrStreamRoute
4451
}
4552
export interface FileRoutesByTo {
4653
'/': typeof IndexRoute
4754
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
4855
'/ssr/data-only': typeof SsrDataOnlyRoute
56+
'/ssr/nested': typeof SsrNestedRoute
4957
'/ssr/stream': typeof SsrStreamRoute
5058
}
5159
export interface FileRoutesById {
5260
__root__: typeof rootRouteImport
5361
'/': typeof IndexRoute
5462
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
5563
'/ssr/data-only': typeof SsrDataOnlyRoute
64+
'/ssr/nested': typeof SsrNestedRoute
5665
'/ssr/stream': typeof SsrStreamRoute
5766
}
5867
export interface FileRouteTypes {
@@ -61,21 +70,29 @@ export interface FileRouteTypes {
6170
| '/'
6271
| '/server-function/custom-error'
6372
| '/ssr/data-only'
73+
| '/ssr/nested'
6474
| '/ssr/stream'
6575
fileRoutesByTo: FileRoutesByTo
66-
to: '/' | '/server-function/custom-error' | '/ssr/data-only' | '/ssr/stream'
76+
to:
77+
| '/'
78+
| '/server-function/custom-error'
79+
| '/ssr/data-only'
80+
| '/ssr/nested'
81+
| '/ssr/stream'
6782
id:
6883
| '__root__'
6984
| '/'
7085
| '/server-function/custom-error'
7186
| '/ssr/data-only'
87+
| '/ssr/nested'
7288
| '/ssr/stream'
7389
fileRoutesById: FileRoutesById
7490
}
7591
export interface RootRouteChildren {
7692
IndexRoute: typeof IndexRoute
7793
ServerFunctionCustomErrorRoute: typeof ServerFunctionCustomErrorRoute
7894
SsrDataOnlyRoute: typeof SsrDataOnlyRoute
95+
SsrNestedRoute: typeof SsrNestedRoute
7996
SsrStreamRoute: typeof SsrStreamRoute
8097
}
8198

@@ -95,6 +112,13 @@ declare module '@tanstack/react-router' {
95112
preLoaderRoute: typeof SsrStreamRouteImport
96113
parentRoute: typeof rootRouteImport
97114
}
115+
'/ssr/nested': {
116+
id: '/ssr/nested'
117+
path: '/ssr/nested'
118+
fullPath: '/ssr/nested'
119+
preLoaderRoute: typeof SsrNestedRouteImport
120+
parentRoute: typeof rootRouteImport
121+
}
98122
'/ssr/data-only': {
99123
id: '/ssr/data-only'
100124
path: '/ssr/data-only'
@@ -116,6 +140,7 @@ const rootRouteChildren: RootRouteChildren = {
116140
IndexRoute: IndexRoute,
117141
ServerFunctionCustomErrorRoute: ServerFunctionCustomErrorRoute,
118142
SsrDataOnlyRoute: SsrDataOnlyRoute,
143+
SsrNestedRoute: SsrNestedRoute,
119144
SsrStreamRoute: SsrStreamRoute,
120145
}
121146
export const routeTree = rootRouteImport
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { makeNested } from '~/data'
3+
4+
export const Route = createFileRoute('/ssr/nested')({
5+
beforeLoad: () => {
6+
return { nested: makeNested() }
7+
},
8+
loader: ({ context }) => {
9+
return context
10+
},
11+
component: () => {
12+
const loaderData = Route.useLoaderData()
13+
14+
const localData = makeNested()
15+
const expectedShoutState = localData.inner.shout()
16+
const expectedWhisperState = localData.whisper()
17+
const shoutState = loaderData.nested.inner.shout()
18+
const whisperState = loaderData.nested.whisper()
19+
20+
return (
21+
<div data-testid="data-only-container">
22+
<h2 data-testid="data-only-heading">data-only</h2>
23+
<div data-testid="shout-container">
24+
<h3>shout</h3>
25+
<div>
26+
expected:{' '}
27+
<div data-testid="shout-expected-state">
28+
{JSON.stringify(expectedShoutState)}
29+
</div>
30+
</div>
31+
<div>
32+
actual:{' '}
33+
<div data-testid="shout-actual-state">
34+
{JSON.stringify(shoutState)}
35+
</div>
36+
</div>
37+
</div>
38+
<div data-testid="whisper-container">
39+
<h3>whisper</h3>
40+
<div>
41+
expected:{' '}
42+
<div data-testid="whisper-expected-state">
43+
{JSON.stringify(expectedWhisperState)}
44+
</div>
45+
</div>
46+
<div>
47+
actual:{' '}
48+
<div data-testid="whisper-actual-state">
49+
{JSON.stringify(whisperState)}
50+
</div>
51+
</div>
52+
</div>
53+
</div>
54+
)
55+
},
56+
})
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { createStart } from '@tanstack/react-start'
2-
import { carAdapter, fooAdapter } from './data'
2+
import { carAdapter, fooAdapter, nestedOuterAdapter } from './data'
33
import { customErrorAdapter } from './CustomError'
44

55
export const startInstance = createStart(() => {
66
return {
77
defaultSsr: true,
8-
serializationAdapters: [fooAdapter, carAdapter, customErrorAdapter],
8+
serializationAdapters: [
9+
fooAdapter,
10+
carAdapter,
11+
customErrorAdapter,
12+
// only register nestedOuterAdapter here, nestedInnerAdapter is registered as an "extends" of nestedOuterAdapter
13+
nestedOuterAdapter,
14+
],
915
}
1016
})

e2e/react-start/serialization-adapters/tests/app.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ test.describe('SSR serialization adapters', () => {
4949
await awaitPageLoaded(page)
5050
await checkData(page, 'stream')
5151
})
52+
53+
test('nested', async ({ page }) => {
54+
await page.goto('/ssr/nested')
55+
await awaitPageLoaded(page)
56+
57+
const expectedShout = await page
58+
.getByTestId(`shout-expected-state`)
59+
.textContent()
60+
expect(expectedShout).not.toBeNull()
61+
await expect(page.getByTestId(`shout-actual-state`)).toContainText(
62+
expectedShout!,
63+
)
64+
65+
const expectedWhisper = await page
66+
.getByTestId(`whisper-expected-state`)
67+
.textContent()
68+
expect(expectedWhisper).not.toBeNull()
69+
await expect(page.getByTestId(`whisper-actual-state`)).toContainText(
70+
expectedWhisper!,
71+
)
72+
})
5273
})
5374

5475
test.describe('server functions serialization adapters', () => {

packages/router-core/src/ssr/serializer/transformer.ts

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,40 @@ export interface SerializableExtensions extends DefaultSerializable {}
2323

2424
export type Serializable = SerializableExtensions[keyof SerializableExtensions]
2525

26+
export type UnionizeSerializationAdaptersInput<
27+
TAdapters extends ReadonlyArray<AnySerializationAdapter>,
28+
> = TAdapters[number]['~types']['input']
29+
2630
export function createSerializationAdapter<
2731
TInput = unknown,
28-
TOutput = unknown /* we need to check that this type is actually serializable taking into account all seroval native types and any custom plugin WE=router/start add!!! */,
32+
TOutput = unknown,
33+
const TExtendsAdapters extends
34+
| ReadonlyArray<AnySerializationAdapter>
35+
| never = never,
2936
>(
30-
opts: CreateSerializationAdapterOptions<TInput, TOutput>,
31-
): SerializationAdapter<TInput, TOutput> {
32-
return opts as unknown as SerializationAdapter<TInput, TOutput>
37+
opts: CreateSerializationAdapterOptions<TInput, TOutput, TExtendsAdapters>,
38+
): SerializationAdapter<TInput, TOutput, TExtendsAdapters> {
39+
return opts as unknown as SerializationAdapter<
40+
TInput,
41+
TOutput,
42+
TExtendsAdapters
43+
>
3344
}
3445

35-
export interface CreateSerializationAdapterOptions<TInput, TOutput> {
46+
export interface CreateSerializationAdapterOptions<
47+
TInput,
48+
TOutput,
49+
TExtendsAdapters extends ReadonlyArray<AnySerializationAdapter> | never,
50+
> {
3651
key: string
52+
extends?: TExtendsAdapters
3753
test: (value: unknown) => value is TInput
38-
toSerializable: (value: TInput) => ValidateSerializable<TOutput, Serializable>
54+
toSerializable: (
55+
value: TInput,
56+
) => ValidateSerializable<
57+
TOutput,
58+
Serializable | UnionizeSerializationAdaptersInput<TExtendsAdapters>
59+
>
3960
fromSerializable: (value: TOutput) => TInput
4061
}
4162

@@ -90,28 +111,42 @@ export interface DefaultSerializerExtensions {
90111

91112
export interface SerializerExtensions extends DefaultSerializerExtensions {}
92113

93-
export interface SerializationAdapter<TInput, TOutput> {
94-
'~types': SerializationAdapterTypes<TInput, TOutput>
114+
export interface SerializationAdapter<
115+
TInput,
116+
TOutput,
117+
TExtendsAdapters extends ReadonlyArray<AnySerializationAdapter>,
118+
> {
119+
'~types': SerializationAdapterTypes<TInput, TOutput, TExtendsAdapters>
95120
key: string
121+
extends?: TExtendsAdapters
96122
test: (value: unknown) => value is TInput
97123
toSerializable: (value: TInput) => TOutput
98124
fromSerializable: (value: TOutput) => TInput
99-
makePlugin: (options: { didRun: boolean }) => Plugin<TInput, SerovalNode>
100125
}
101126

102-
export interface SerializationAdapterTypes<TInput, TOutput> {
103-
input: TInput
127+
export interface SerializationAdapterTypes<
128+
TInput,
129+
TOutput,
130+
TExtendsAdapters extends ReadonlyArray<AnySerializationAdapter>,
131+
> {
132+
input: TInput | UnionizeSerializationAdaptersInput<TExtendsAdapters>
104133
output: TOutput
134+
extends: TExtendsAdapters
105135
}
106136

107-
export type AnySerializationAdapter = SerializationAdapter<any, any>
137+
export type AnySerializationAdapter = SerializationAdapter<any, any, any>
108138

109-
export function makeSsrSerovalPlugin<TInput, TOutput>(
110-
serializationAdapter: SerializationAdapter<TInput, TOutput>,
139+
export function makeSsrSerovalPlugin(
140+
serializationAdapter: AnySerializationAdapter,
111141
options: { didRun: boolean },
112-
) {
113-
return createPlugin<TInput, SerovalNode>({
142+
): Plugin<any, SerovalNode> {
143+
return createPlugin<any, SerovalNode>({
114144
tag: '$TSR/t/' + serializationAdapter.key,
145+
extends: serializationAdapter.extends
146+
? (serializationAdapter.extends as Array<AnySerializationAdapter>).map(
147+
(ext) => makeSsrSerovalPlugin(ext, options),
148+
)
149+
: undefined,
115150
test: serializationAdapter.test,
116151
parse: {
117152
stream(value, ctx) {
@@ -134,11 +169,16 @@ export function makeSsrSerovalPlugin<TInput, TOutput>(
134169
})
135170
}
136171

137-
export function makeSerovalPlugin<TInput, TOutput>(
138-
serializationAdapter: SerializationAdapter<TInput, TOutput>,
139-
) {
140-
return createPlugin<TInput, SerovalNode>({
172+
export function makeSerovalPlugin(
173+
serializationAdapter: AnySerializationAdapter,
174+
): Plugin<any, SerovalNode> {
175+
return createPlugin<any, SerovalNode>({
141176
tag: '$TSR/t/' + serializationAdapter.key,
177+
extends: serializationAdapter.extends
178+
? (serializationAdapter.extends as Array<AnySerializationAdapter>).map(
179+
makeSerovalPlugin,
180+
)
181+
: undefined,
142182
test: serializationAdapter.test,
143183
parse: {
144184
sync(value, ctx) {
@@ -154,9 +194,7 @@ export function makeSerovalPlugin<TInput, TOutput>(
154194
// we don't generate JS code outside of SSR (for now)
155195
serialize: undefined as never,
156196
deserialize(node, ctx) {
157-
return serializationAdapter.fromSerializable(
158-
ctx.deserialize(node) as TOutput,
159-
)
197+
return serializationAdapter.fromSerializable(ctx.deserialize(node))
160198
},
161199
})
162200
}

packages/start-client-core/src/createMiddleware.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,8 @@ export type AssignAllServerRequestContext<
292292
> = Assign<
293293
// Fetch Request Context
294294
GlobalFetchRequestContext,
295-
// AnyContext,
296295
Assign<
297-
GlobalServerRequestContext<TRegister>, // TODO: This enabled global middleware
298-
// type inference, but creates a circular types issue. No idea how to fix this.
299-
// AnyContext,
296+
GlobalServerRequestContext<TRegister>,
300297
__AssignAllServerRequestContext<TMiddlewares, TSendContext, TServerContext>
301298
>
302299
>

0 commit comments

Comments
 (0)