diff --git a/packages/react-router/src/ScriptOnce.tsx b/packages/react-router/src/ScriptOnce.tsx index f00317d52fb..97609c6fc7b 100644 --- a/packages/react-router/src/ScriptOnce.tsx +++ b/packages/react-router/src/ScriptOnce.tsx @@ -12,9 +12,8 @@ export function ScriptOnce({ children }: { children: string }) { return ( ` + return `${script}` }) }, dehydrate: async () => { @@ -223,7 +222,7 @@ export function attachRouterServerSsrUtils({ }, scopeId: SCOPE_ID, onDone: () => { - scriptBuffer.enqueue(GLOBAL_TSR + '.streamEnd=true') + scriptBuffer.enqueue(GLOBAL_TSR + '.e()') p.resolve('') }, onError: (err) => p.reject(err), diff --git a/packages/router-core/src/ssr/tsrScript.ts b/packages/router-core/src/ssr/tsrScript.ts index 084638cfa4c..69c9abe05d5 100644 --- a/packages/router-core/src/ssr/tsrScript.ts +++ b/packages/router-core/src/ssr/tsrScript.ts @@ -1,25 +1,15 @@ self.$_TSR = { - c() { - // If Vue has set the defer flag, don't remove scripts yet - wait for Vue to call cleanup() - if (self.$_TSR_DEFER) { - return - } - document.querySelectorAll('.\\$tsr').forEach((o) => { - o.remove() - }) - if (this.hydrated && this.streamEnd) { - delete self.$_TSR - delete self.$R['tsr'] - } + h() { + this.hydrated = true + this.c() + }, + e() { + this.streamEnded = true + this.c() }, - // Called by Vue after hydration is complete to perform deferred cleanup - cleanup() { - document.querySelectorAll('.\\$tsr').forEach((o) => { - o.remove() - }) - if (this.hydrated && this.streamEnd) { + c() { + if (this.hydrated && this.streamEnded) { delete self.$_TSR - delete self.$_TSR_DEFER delete self.$R['tsr'] } }, diff --git a/packages/router-core/src/ssr/types.ts b/packages/router-core/src/ssr/types.ts new file mode 100644 index 00000000000..ec0feee41a4 --- /dev/null +++ b/packages/router-core/src/ssr/types.ts @@ -0,0 +1,40 @@ +import type { Manifest } from '../manifest' +import type { MakeRouteMatch } from '../Matches' + +export interface DehydratedMatch { + i: MakeRouteMatch['id'] + b?: MakeRouteMatch['__beforeLoadContext'] + l?: MakeRouteMatch['loaderData'] + e?: MakeRouteMatch['error'] + u: MakeRouteMatch['updatedAt'] + s: MakeRouteMatch['status'] + ssr?: MakeRouteMatch['ssr'] +} + +export interface DehydratedRouter { + manifest: Manifest | undefined + dehydratedData?: any + lastMatchId?: string + matches: Array +} + +export interface TsrSsrGlobal { + router?: DehydratedRouter + // Signal that router hydration is complete + h: () => void + // Signal that stream has ended + e: () => void + // Cleanup all hydration resources and scripts + c: () => void + // p: Push script into buffer or execute immediately + p: (script: () => void) => void + buffer: Array<() => void> + // custom transformers, shortened since this is sent for each streamed value that needs a custom transformer + t?: Map any> + // this flag indicates whether the transformers were initialized + initialized?: boolean + // router is hydrated and doesnt need the streamed values anymore + hydrated?: boolean + // stream has ended + streamEnded?: boolean +} diff --git a/packages/react-router/tests/hydrate.test.ts b/packages/router-core/tests/hydrate.test.ts similarity index 91% rename from packages/react-router/tests/hydrate.test.ts rename to packages/router-core/tests/hydrate.test.ts index b0d91530554..101ade6f571 100644 --- a/packages/react-router/tests/hydrate.test.ts +++ b/packages/router-core/tests/hydrate.test.ts @@ -1,13 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createMemoryHistory } from '@tanstack/history' import { hydrate } from '@tanstack/router-core/ssr/client' -import { - createMemoryHistory, - createRootRoute, - createRoute, - createRouter, - notFound, -} from '../src' -import type { TsrSsrGlobal } from '@tanstack/router-core/ssr/client' +import { BaseRootRoute, BaseRoute, RouterCore, notFound } from '../src' +import type { TsrSsrGlobal } from '../src/ssr/types' import type { AnyRouteMatch } from '../src' describe('hydrate', () => { @@ -25,9 +20,9 @@ describe('hydrate', () => { const history = createMemoryHistory({ initialEntries: ['/'] }) - const rootRoute = createRootRoute({}) + const rootRoute = new BaseRootRoute({}) - const indexRoute = createRoute({ + const indexRoute = new BaseRoute({ getParentRoute: () => rootRoute, path: '/', component: () => 'Index', @@ -35,7 +30,7 @@ describe('hydrate', () => { head: mockHead, }) - const otherRoute = createRoute({ + const otherRoute = new BaseRoute({ getParentRoute: () => indexRoute, path: '/other', component: () => 'Other', @@ -45,7 +40,7 @@ describe('hydrate', () => { indexRoute.addChildren([otherRoute]), ]) - mockRouter = createRouter({ routeTree, history, isServer: true }) + mockRouter = new RouterCore({ routeTree, history, isServer: true }) }) afterEach(() => { @@ -100,6 +95,8 @@ describe('hydrate', () => { lastMatchId: '/', matches: [], }, + h: vi.fn(), + e: vi.fn(), c: vi.fn(), p: vi.fn(), buffer: mockBuffer, @@ -127,6 +124,8 @@ describe('hydrate', () => { lastMatchId: '/', matches: [], }, + h: vi.fn(), + e: vi.fn(), c: vi.fn(), p: vi.fn(), buffer: [], @@ -148,6 +147,8 @@ describe('hydrate', () => { lastMatchId: '/', matches: [], }, + h: vi.fn(), + e: vi.fn(), c: vi.fn(), p: vi.fn(), buffer: [], @@ -199,6 +200,8 @@ describe('hydrate', () => { lastMatchId: '/', matches: dehydratedMatches, }, + h: vi.fn(), + e: vi.fn(), c: vi.fn(), p: vi.fn(), buffer: [], @@ -242,6 +245,8 @@ describe('hydrate', () => { }, ], }, + h: vi.fn(), + e: vi.fn(), c: vi.fn(), p: vi.fn(), buffer: [], diff --git a/packages/solid-router/src/ScriptOnce.tsx b/packages/solid-router/src/ScriptOnce.tsx index 572570c8ddd..1ad12ccb071 100644 --- a/packages/solid-router/src/ScriptOnce.tsx +++ b/packages/solid-router/src/ScriptOnce.tsx @@ -15,7 +15,7 @@ export function ScriptOnce({