Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions e2e/react-start/serialization-adapters/src/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,38 @@ export function makeData() {
},
}
}
export class NestedOuter {
constructor(public inner: NestedInner) {}
whisper() {
return this.inner.value.toLowerCase()
}
}

export class NestedInner {
constructor(public value: string) {}
shout() {
return this.value.toUpperCase()
}
}

export const nestedInnerAdapter = createSerializationAdapter({
key: 'nestedInner',
test: (value): value is NestedInner => value instanceof NestedInner,
toSerializable: (inner) => inner.value,
fromSerializable: (value) => new NestedInner(value),
})

export const nestedOuterAdapter = createSerializationAdapter({
key: 'nestedOuter',
extends: [nestedInnerAdapter],
test: (value) => value instanceof NestedOuter,
toSerializable: (outer) => outer.inner,
fromSerializable: (value) => new NestedOuter(value),
})

export function makeNested() {
return new NestedOuter(new NestedInner('Hello World'))
}

export function RenderData({
id,
Expand Down
27 changes: 26 additions & 1 deletion e2e/react-start/serialization-adapters/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SsrStreamRouteImport } from './routes/ssr/stream'
import { Route as SsrNestedRouteImport } from './routes/ssr/nested'
import { Route as SsrDataOnlyRouteImport } from './routes/ssr/data-only'
import { Route as ServerFunctionCustomErrorRouteImport } from './routes/server-function/custom-error'

Expand All @@ -24,6 +25,11 @@ const SsrStreamRoute = SsrStreamRouteImport.update({
path: '/ssr/stream',
getParentRoute: () => rootRouteImport,
} as any)
const SsrNestedRoute = SsrNestedRouteImport.update({
id: '/ssr/nested',
path: '/ssr/nested',
getParentRoute: () => rootRouteImport,
} as any)
const SsrDataOnlyRoute = SsrDataOnlyRouteImport.update({
id: '/ssr/data-only',
path: '/ssr/data-only',
Expand All @@ -40,19 +46,22 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
'/ssr/data-only': typeof SsrDataOnlyRoute
'/ssr/nested': typeof SsrNestedRoute
'/ssr/stream': typeof SsrStreamRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
'/ssr/data-only': typeof SsrDataOnlyRoute
'/ssr/nested': typeof SsrNestedRoute
'/ssr/stream': typeof SsrStreamRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
'/ssr/data-only': typeof SsrDataOnlyRoute
'/ssr/nested': typeof SsrNestedRoute
'/ssr/stream': typeof SsrStreamRoute
}
export interface FileRouteTypes {
Expand All @@ -61,21 +70,29 @@ export interface FileRouteTypes {
| '/'
| '/server-function/custom-error'
| '/ssr/data-only'
| '/ssr/nested'
| '/ssr/stream'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/server-function/custom-error' | '/ssr/data-only' | '/ssr/stream'
to:
| '/'
| '/server-function/custom-error'
| '/ssr/data-only'
| '/ssr/nested'
| '/ssr/stream'
id:
| '__root__'
| '/'
| '/server-function/custom-error'
| '/ssr/data-only'
| '/ssr/nested'
| '/ssr/stream'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ServerFunctionCustomErrorRoute: typeof ServerFunctionCustomErrorRoute
SsrDataOnlyRoute: typeof SsrDataOnlyRoute
SsrNestedRoute: typeof SsrNestedRoute
SsrStreamRoute: typeof SsrStreamRoute
}

Expand All @@ -95,6 +112,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SsrStreamRouteImport
parentRoute: typeof rootRouteImport
}
'/ssr/nested': {
id: '/ssr/nested'
path: '/ssr/nested'
fullPath: '/ssr/nested'
preLoaderRoute: typeof SsrNestedRouteImport
parentRoute: typeof rootRouteImport
}
'/ssr/data-only': {
id: '/ssr/data-only'
path: '/ssr/data-only'
Expand All @@ -116,6 +140,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ServerFunctionCustomErrorRoute: ServerFunctionCustomErrorRoute,
SsrDataOnlyRoute: SsrDataOnlyRoute,
SsrNestedRoute: SsrNestedRoute,
SsrStreamRoute: SsrStreamRoute,
}
export const routeTree = rootRouteImport
Expand Down
56 changes: 56 additions & 0 deletions e2e/react-start/serialization-adapters/src/routes/ssr/nested.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createFileRoute } from '@tanstack/react-router'
import { makeNested } from '~/data'

export const Route = createFileRoute('/ssr/nested')({
beforeLoad: () => {
return { nested: makeNested() }
},
loader: ({ context }) => {
return context
},
component: () => {
const loaderData = Route.useLoaderData()

const localData = makeNested()
const expectedShoutState = localData.inner.shout()
const expectedWhisperState = localData.whisper()
const shoutState = loaderData.nested.inner.shout()
const whisperState = loaderData.nested.whisper()

return (
<div data-testid="data-only-container">
<h2 data-testid="data-only-heading">data-only</h2>
<div data-testid="shout-container">
<h3>shout</h3>
<div>
expected:{' '}
<div data-testid="shout-expected-state">
{JSON.stringify(expectedShoutState)}
</div>
</div>
<div>
actual:{' '}
<div data-testid="shout-actual-state">
{JSON.stringify(shoutState)}
</div>
</div>
</div>
<div data-testid="whisper-container">
<h3>whisper</h3>
<div>
expected:{' '}
<div data-testid="whisper-expected-state">
{JSON.stringify(expectedWhisperState)}
</div>
</div>
<div>
actual:{' '}
<div data-testid="whisper-actual-state">
{JSON.stringify(whisperState)}
</div>
</div>
</div>
</div>
)
},
})
10 changes: 8 additions & 2 deletions e2e/react-start/serialization-adapters/src/start.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { createStart } from '@tanstack/react-start'
import { carAdapter, fooAdapter } from './data'
import { carAdapter, fooAdapter, nestedOuterAdapter } from './data'
import { customErrorAdapter } from './CustomError'

export const startInstance = createStart(() => {
return {
defaultSsr: true,
serializationAdapters: [fooAdapter, carAdapter, customErrorAdapter],
serializationAdapters: [
fooAdapter,
carAdapter,
customErrorAdapter,
// only register nestedOuterAdapter here, nestedInnerAdapter is registered as an "extends" of nestedOuterAdapter
nestedOuterAdapter,
],
}
})
21 changes: 21 additions & 0 deletions e2e/react-start/serialization-adapters/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ test.describe('SSR serialization adapters', () => {
await awaitPageLoaded(page)
await checkData(page, 'stream')
})

test('nested', async ({ page }) => {
await page.goto('/ssr/nested')
await awaitPageLoaded(page)

const expectedShout = await page
.getByTestId(`shout-expected-state`)
.textContent()
expect(expectedShout).not.toBeNull()
await expect(page.getByTestId(`shout-actual-state`)).toContainText(
expectedShout!,
)

const expectedWhisper = await page
.getByTestId(`whisper-expected-state`)
.textContent()
expect(expectedWhisper).not.toBeNull()
await expect(page.getByTestId(`whisper-actual-state`)).toContainText(
expectedWhisper!,
)
})
})

test.describe('server functions serialization adapters', () => {
Expand Down
84 changes: 61 additions & 23 deletions packages/router-core/src/ssr/serializer/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,40 @@ export interface SerializableExtensions extends DefaultSerializable {}

export type Serializable = SerializableExtensions[keyof SerializableExtensions]

export type UnionizeSerializationAdaptersInput<
TAdapters extends ReadonlyArray<AnySerializationAdapter>,
> = TAdapters[number]['~types']['input']

export function createSerializationAdapter<
TInput = unknown,
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!!! */,
TOutput = unknown,
const TExtendsAdapters extends
| ReadonlyArray<AnySerializationAdapter>
| never = never,
>(
opts: CreateSerializationAdapterOptions<TInput, TOutput>,
): SerializationAdapter<TInput, TOutput> {
return opts as unknown as SerializationAdapter<TInput, TOutput>
opts: CreateSerializationAdapterOptions<TInput, TOutput, TExtendsAdapters>,
): SerializationAdapter<TInput, TOutput, TExtendsAdapters> {
return opts as unknown as SerializationAdapter<
TInput,
TOutput,
TExtendsAdapters
>
}

export interface CreateSerializationAdapterOptions<TInput, TOutput> {
export interface CreateSerializationAdapterOptions<
TInput,
TOutput,
TExtendsAdapters extends ReadonlyArray<AnySerializationAdapter> | never,
> {
key: string
extends?: TExtendsAdapters
test: (value: unknown) => value is TInput
toSerializable: (value: TInput) => ValidateSerializable<TOutput, Serializable>
toSerializable: (
value: TInput,
) => ValidateSerializable<
TOutput,
Serializable | UnionizeSerializationAdaptersInput<TExtendsAdapters>
>
fromSerializable: (value: TOutput) => TInput
}

Expand Down Expand Up @@ -90,28 +111,42 @@ export interface DefaultSerializerExtensions {

export interface SerializerExtensions extends DefaultSerializerExtensions {}

export interface SerializationAdapter<TInput, TOutput> {
'~types': SerializationAdapterTypes<TInput, TOutput>
export interface SerializationAdapter<
TInput,
TOutput,
TExtendsAdapters extends ReadonlyArray<AnySerializationAdapter>,
> {
'~types': SerializationAdapterTypes<TInput, TOutput, TExtendsAdapters>
key: string
extends?: TExtendsAdapters
test: (value: unknown) => value is TInput
toSerializable: (value: TInput) => TOutput
fromSerializable: (value: TOutput) => TInput
makePlugin: (options: { didRun: boolean }) => Plugin<TInput, SerovalNode>
}

export interface SerializationAdapterTypes<TInput, TOutput> {
input: TInput
export interface SerializationAdapterTypes<
TInput,
TOutput,
TExtendsAdapters extends ReadonlyArray<AnySerializationAdapter>,
> {
input: TInput | UnionizeSerializationAdaptersInput<TExtendsAdapters>
output: TOutput
extends: TExtendsAdapters
}

export type AnySerializationAdapter = SerializationAdapter<any, any>
export type AnySerializationAdapter = SerializationAdapter<any, any, any>

export function makeSsrSerovalPlugin<TInput, TOutput>(
serializationAdapter: SerializationAdapter<TInput, TOutput>,
export function makeSsrSerovalPlugin(
serializationAdapter: AnySerializationAdapter,
options: { didRun: boolean },
) {
return createPlugin<TInput, SerovalNode>({
): Plugin<any, SerovalNode> {
return createPlugin<any, SerovalNode>({
tag: '$TSR/t/' + serializationAdapter.key,
extends: serializationAdapter.extends
? (serializationAdapter.extends as Array<AnySerializationAdapter>).map(
(ext) => makeSsrSerovalPlugin(ext, options),
)
: undefined,
test: serializationAdapter.test,
parse: {
stream(value, ctx) {
Expand All @@ -134,11 +169,16 @@ export function makeSsrSerovalPlugin<TInput, TOutput>(
})
}

export function makeSerovalPlugin<TInput, TOutput>(
serializationAdapter: SerializationAdapter<TInput, TOutput>,
) {
return createPlugin<TInput, SerovalNode>({
export function makeSerovalPlugin(
serializationAdapter: AnySerializationAdapter,
): Plugin<any, SerovalNode> {
return createPlugin<any, SerovalNode>({
tag: '$TSR/t/' + serializationAdapter.key,
extends: serializationAdapter.extends
? (serializationAdapter.extends as Array<AnySerializationAdapter>).map(
makeSerovalPlugin,
)
: undefined,
test: serializationAdapter.test,
parse: {
sync(value, ctx) {
Expand All @@ -154,9 +194,7 @@ export function makeSerovalPlugin<TInput, TOutput>(
// we don't generate JS code outside of SSR (for now)
serialize: undefined as never,
deserialize(node, ctx) {
return serializationAdapter.fromSerializable(
ctx.deserialize(node) as TOutput,
)
return serializationAdapter.fromSerializable(ctx.deserialize(node))
},
})
}
Expand Down
5 changes: 1 addition & 4 deletions packages/start-client-core/src/createMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,8 @@ export type AssignAllServerRequestContext<
> = Assign<
// Fetch Request Context
GlobalFetchRequestContext,
// AnyContext,
Assign<
GlobalServerRequestContext<TRegister>, // TODO: This enabled global middleware
// type inference, but creates a circular types issue. No idea how to fix this.
// AnyContext,
GlobalServerRequestContext<TRegister>,
__AssignAllServerRequestContext<TMiddlewares, TSendContext, TServerContext>
>
>
Expand Down
Loading
Loading