Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/gentle-hoops-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/router-core': patch
---

allow Temporal types in serializable data
1 change: 1 addition & 0 deletions e2e/react-start/serialization-adapters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@tanstack/react-start": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"temporal-polyfill": "^1.0.4",
"zod": "^4.4.3"
},
"devDependencies": {
Expand Down
15 changes: 15 additions & 0 deletions e2e/react-start/serialization-adapters/src/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { StrictMode, startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/react-start/client'
import { install as installTemporalPolyfill } from 'temporal-polyfill/shim'

installTemporalPolyfill()

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<StartClient />
</StrictMode>,
)
})
42 changes: 42 additions & 0 deletions e2e/react-start/serialization-adapters/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { Route as IndexRouteImport } from './routes/index'
import { Route as ServerFunctionCustomErrorRouteImport } from './routes/server-function/custom-error'
import { Route as ServerFunctionLateRawStreamRouteImport } from './routes/server-function/late-raw-stream'
import { Route as ServerFunctionNestedRouteImport } from './routes/server-function/nested'
import { Route as ServerFunctionTemporalRouteImport } from './routes/server-function/temporal'
import { Route as SsrDataOnlyRouteImport } from './routes/ssr/data-only'
import { Route as SsrNestedRouteImport } from './routes/ssr/nested'
import { Route as SsrStreamRouteImport } from './routes/ssr/stream'
import { Route as SsrTemporalRouteImport } from './routes/ssr/temporal'

const IndexRoute = IndexRouteImport.update({
id: '/',
Expand All @@ -39,6 +41,11 @@ const ServerFunctionNestedRoute = ServerFunctionNestedRouteImport.update({
path: '/server-function/nested',
getParentRoute: () => rootRouteImport,
} as any)
const ServerFunctionTemporalRoute = ServerFunctionTemporalRouteImport.update({
id: '/server-function/temporal',
path: '/server-function/temporal',
getParentRoute: () => rootRouteImport,
} as any)
const SsrDataOnlyRoute = SsrDataOnlyRouteImport.update({
id: '/ssr/data-only',
path: '/ssr/data-only',
Expand All @@ -54,34 +61,45 @@ const SsrStreamRoute = SsrStreamRouteImport.update({
path: '/ssr/stream',
getParentRoute: () => rootRouteImport,
} as any)
const SsrTemporalRoute = SsrTemporalRouteImport.update({
id: '/ssr/temporal',
path: '/ssr/temporal',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
'/server-function/late-raw-stream': typeof ServerFunctionLateRawStreamRoute
'/server-function/nested': typeof ServerFunctionNestedRoute
'/server-function/temporal': typeof ServerFunctionTemporalRoute
'/ssr/data-only': typeof SsrDataOnlyRoute
'/ssr/nested': typeof SsrNestedRoute
'/ssr/stream': typeof SsrStreamRoute
'/ssr/temporal': typeof SsrTemporalRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
'/server-function/late-raw-stream': typeof ServerFunctionLateRawStreamRoute
'/server-function/nested': typeof ServerFunctionNestedRoute
'/server-function/temporal': typeof ServerFunctionTemporalRoute
'/ssr/data-only': typeof SsrDataOnlyRoute
'/ssr/nested': typeof SsrNestedRoute
'/ssr/stream': typeof SsrStreamRoute
'/ssr/temporal': typeof SsrTemporalRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/server-function/custom-error': typeof ServerFunctionCustomErrorRoute
'/server-function/late-raw-stream': typeof ServerFunctionLateRawStreamRoute
'/server-function/nested': typeof ServerFunctionNestedRoute
'/server-function/temporal': typeof ServerFunctionTemporalRoute
'/ssr/data-only': typeof SsrDataOnlyRoute
'/ssr/nested': typeof SsrNestedRoute
'/ssr/stream': typeof SsrStreamRoute
'/ssr/temporal': typeof SsrTemporalRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
Expand All @@ -90,37 +108,45 @@ export interface FileRouteTypes {
| '/server-function/custom-error'
| '/server-function/late-raw-stream'
| '/server-function/nested'
| '/server-function/temporal'
| '/ssr/data-only'
| '/ssr/nested'
| '/ssr/stream'
| '/ssr/temporal'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/server-function/custom-error'
| '/server-function/late-raw-stream'
| '/server-function/nested'
| '/server-function/temporal'
| '/ssr/data-only'
| '/ssr/nested'
| '/ssr/stream'
| '/ssr/temporal'
id:
| '__root__'
| '/'
| '/server-function/custom-error'
| '/server-function/late-raw-stream'
| '/server-function/nested'
| '/server-function/temporal'
| '/ssr/data-only'
| '/ssr/nested'
| '/ssr/stream'
| '/ssr/temporal'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ServerFunctionCustomErrorRoute: typeof ServerFunctionCustomErrorRoute
ServerFunctionLateRawStreamRoute: typeof ServerFunctionLateRawStreamRoute
ServerFunctionNestedRoute: typeof ServerFunctionNestedRoute
ServerFunctionTemporalRoute: typeof ServerFunctionTemporalRoute
SsrDataOnlyRoute: typeof SsrDataOnlyRoute
SsrNestedRoute: typeof SsrNestedRoute
SsrStreamRoute: typeof SsrStreamRoute
SsrTemporalRoute: typeof SsrTemporalRoute
}

declare module '@tanstack/react-router' {
Expand Down Expand Up @@ -153,6 +179,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ServerFunctionNestedRouteImport
parentRoute: typeof rootRouteImport
}
'/server-function/temporal': {
id: '/server-function/temporal'
path: '/server-function/temporal'
fullPath: '/server-function/temporal'
preLoaderRoute: typeof ServerFunctionTemporalRouteImport
parentRoute: typeof rootRouteImport
}
'/ssr/data-only': {
id: '/ssr/data-only'
path: '/ssr/data-only'
Expand All @@ -174,6 +207,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SsrStreamRouteImport
parentRoute: typeof rootRouteImport
}
'/ssr/temporal': {
id: '/ssr/temporal'
path: '/ssr/temporal'
fullPath: '/ssr/temporal'
preLoaderRoute: typeof SsrTemporalRouteImport
parentRoute: typeof rootRouteImport
}
}
}

Expand All @@ -182,9 +222,11 @@ const rootRouteChildren: RootRouteChildren = {
ServerFunctionCustomErrorRoute: ServerFunctionCustomErrorRoute,
ServerFunctionLateRawStreamRoute: ServerFunctionLateRawStreamRoute,
ServerFunctionNestedRoute: ServerFunctionNestedRoute,
ServerFunctionTemporalRoute: ServerFunctionTemporalRoute,
SsrDataOnlyRoute: SsrDataOnlyRoute,
SsrNestedRoute: SsrNestedRoute,
SsrStreamRoute: SsrStreamRoute,
SsrTemporalRoute: SsrTemporalRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
16 changes: 16 additions & 0 deletions e2e/react-start/serialization-adapters/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ function Home() {
>
Nested Classes
</Link>
<br />
<Link
data-testid="ssr-temporal-link"
to="/ssr/temporal"
reloadDocument={true}
>
Temporal
</Link>
</div>
<div>
<h2>Server Functions</h2>
Expand All @@ -50,6 +58,14 @@ function Home() {
>
Nested Classes returned from Server Function
</Link>
<br />
<Link
data-testid="server-functions-temporal-link"
to="/server-function/temporal"
reloadDocument={true}
>
Temporal returned from Server Function
</Link>
</div>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { useState } from 'react'
import type { TemporalData } from '~/temporal'
import { RenderTemporalData, makeTemporalData } from '~/temporal'

const temporalFn = createServerFn().handler(() => {
return makeTemporalData()
})

export const Route = createFileRoute('/server-function/temporal')({
component: RouteComponent,
})

function RouteComponent() {
const [temporalResponse, setTemporalResponse] = useState<TemporalData>()

return (
<div>
<button
data-testid="server-function-trigger"
onClick={() => temporalFn().then(setTemporalResponse)}
>
trigger
</button>

{temporalResponse ? (
<RenderTemporalData id="server-fn" data={temporalResponse} />
) : (
<div data-testid="waiting-for-response">waiting for response...</div>
)}
</div>
)
}
11 changes: 11 additions & 0 deletions e2e/react-start/serialization-adapters/src/routes/ssr/temporal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/react-router'
import { RenderTemporalData, makeTemporalData } from '~/temporal'

export const Route = createFileRoute('/ssr/temporal')({
loader: () => makeTemporalData(),
component: () => {
const loaderData = Route.useLoaderData()

return <RenderTemporalData id="loader" data={loaderData} />
},
})
10 changes: 10 additions & 0 deletions e2e/react-start/serialization-adapters/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import handler from '@tanstack/react-start/server-entry'
import { install as installTemporalPolyfill } from 'temporal-polyfill/shim'

installTemporalPolyfill()

export default {
fetch(request: Request) {
return handler.fetch(request)
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import 'temporal-polyfill/types/global'
56 changes: 56 additions & 0 deletions e2e/react-start/serialization-adapters/src/temporal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export const temporalTypeNames = [
'Instant',
'Duration',
'PlainDate',
'PlainDateTime',
'PlainMonthDay',
'PlainTime',
'PlainYearMonth',
'ZonedDateTime',
] as const

export function makeTemporalData() {
return {
Instant: Temporal.Instant.from('2024-03-05T06:07:08.9Z'),
Duration: Temporal.Duration.from('P1Y2M3DT4H5M6.7S'),
PlainDate: Temporal.PlainDate.from('2024-03-05'),
PlainDateTime: Temporal.PlainDateTime.from('2024-03-05T06:07:08.9'),
PlainMonthDay: Temporal.PlainMonthDay.from('03-05'),
PlainTime: Temporal.PlainTime.from('06:07:08.9'),
PlainYearMonth: Temporal.PlainYearMonth.from('2024-03'),
ZonedDateTime: Temporal.ZonedDateTime.from(
'2024-03-05T06:07:08.9+01:00[Europe/Berlin]',
),
}
}

export type TemporalData = ReturnType<typeof makeTemporalData>

function formatTemporal(value: unknown) {
return `${Object.prototype.toString.call(value)} ${String(value)}`
}

export function RenderTemporalData({
id,
data,
}: {
id: string
data: TemporalData
}) {
const localData = makeTemporalData()
return (
<div data-testid={`${id}-container`}>
{temporalTypeNames.map((name) => (
<div key={name}>
<h4>{name}</h4>
<div data-testid={`${id}-${name}-expected`}>
{formatTemporal(localData[name])}
</div>
<div data-testid={`${id}-${name}-actual`}>
{formatTemporal(data[name])}
</div>
</div>
))}
</div>
)
}
42 changes: 42 additions & 0 deletions e2e/react-start/serialization-adapters/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ async function checkNestedData(page: Page) {
expectedWhisper!,
)
}

const temporalTypeNames = [
'Instant',
'Duration',
'PlainDate',
'PlainDateTime',
'PlainMonthDay',
'PlainTime',
'PlainYearMonth',
'ZonedDateTime',
] as const

async function checkTemporalData(page: Page, id: string) {
for (const name of temporalTypeNames) {
const expected = await page
.getByTestId(`${id}-${name}-expected`)
.textContent()
expect(expected).not.toBeNull()
expect(expected).toContain(`[object Temporal.${name}]`)
await expect(page.getByTestId(`${id}-${name}-actual`)).toHaveText(expected!)
}
}

test.use({
whitelistErrors: [
'Failed to load resource: the server responded with a status of 499',
Expand Down Expand Up @@ -74,6 +97,13 @@ test.describe('SSR serialization adapters', () => {

await checkNestedData(page)
})

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

await checkTemporalData(page, 'loader')
})
})

test.describe('server functions serialization adapters', () => {
Expand Down Expand Up @@ -109,6 +139,18 @@ test.describe('server functions serialization adapters', () => {
await page.getByTestId('server-function-trigger').click()
await checkNestedData(page)
})

test('temporal', async ({ page }) => {
await page.goto('/server-function/temporal')
await awaitPageLoaded(page)

await expect(page.getByTestId('waiting-for-response')).toContainText(
'waiting for response...',
)

await page.getByTestId('server-function-trigger').click()
await checkTemporalData(page, 'server-fn')
})
})

test.describe('late RawStream serialization', () => {
Expand Down
Loading