@tanstack/router-core 1.121.0-alpha.27 → 1.121.0-alpha.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +31 -1
  3. package/dist/cjs/RouterProvider.d.cts +2 -1
  4. package/dist/cjs/defer.cjs +1 -1
  5. package/dist/cjs/defer.cjs.map +1 -1
  6. package/dist/cjs/global.d.cts +7 -0
  7. package/dist/cjs/index.cjs +1 -2
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +6 -6
  10. package/dist/cjs/link.cjs.map +1 -1
  11. package/dist/cjs/link.d.cts +12 -0
  12. package/dist/cjs/lru-cache.cjs +62 -0
  13. package/dist/cjs/lru-cache.cjs.map +1 -0
  14. package/dist/cjs/lru-cache.d.cts +5 -0
  15. package/dist/cjs/not-found.cjs +1 -1
  16. package/dist/cjs/not-found.cjs.map +1 -1
  17. package/dist/cjs/path.cjs +316 -148
  18. package/dist/cjs/path.cjs.map +1 -1
  19. package/dist/cjs/path.d.cts +18 -24
  20. package/dist/cjs/qss.cjs.map +1 -1
  21. package/dist/cjs/redirect.cjs +3 -0
  22. package/dist/cjs/redirect.cjs.map +1 -1
  23. package/dist/cjs/route.cjs +6 -12
  24. package/dist/cjs/route.cjs.map +1 -1
  25. package/dist/cjs/route.d.cts +29 -9
  26. package/dist/cjs/router.cjs +453 -272
  27. package/dist/cjs/router.cjs.map +1 -1
  28. package/dist/cjs/router.d.cts +55 -85
  29. package/dist/cjs/scroll-restoration.cjs +20 -13
  30. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  31. package/dist/cjs/scroll-restoration.d.cts +9 -1
  32. package/dist/cjs/searchMiddleware.cjs.map +1 -1
  33. package/dist/cjs/searchParams.cjs.map +1 -1
  34. package/dist/cjs/ssr/client.cjs +10 -0
  35. package/dist/cjs/ssr/client.cjs.map +1 -0
  36. package/dist/cjs/ssr/client.d.cts +5 -0
  37. package/dist/cjs/ssr/createRequestHandler.cjs +50 -0
  38. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -0
  39. package/dist/cjs/ssr/createRequestHandler.d.cts +9 -0
  40. package/dist/cjs/ssr/handlerCallback.cjs +7 -0
  41. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -0
  42. package/dist/cjs/ssr/handlerCallback.d.cts +9 -0
  43. package/dist/cjs/ssr/headers.cjs +39 -0
  44. package/dist/cjs/ssr/headers.cjs.map +1 -0
  45. package/dist/cjs/ssr/headers.d.cts +5 -0
  46. package/dist/cjs/ssr/json.cjs +14 -0
  47. package/dist/cjs/ssr/json.cjs.map +1 -0
  48. package/dist/cjs/ssr/json.d.cts +4 -0
  49. package/dist/cjs/ssr/seroval-plugins.cjs +34 -0
  50. package/dist/cjs/ssr/seroval-plugins.cjs.map +1 -0
  51. package/dist/cjs/ssr/seroval-plugins.d.cts +10 -0
  52. package/dist/cjs/ssr/server.cjs +13 -0
  53. package/dist/cjs/ssr/server.cjs.map +1 -0
  54. package/dist/cjs/ssr/server.d.cts +6 -0
  55. package/dist/cjs/ssr/ssr-client.cjs +159 -0
  56. package/dist/cjs/ssr/ssr-client.cjs.map +1 -0
  57. package/dist/cjs/ssr/ssr-client.d.cts +29 -0
  58. package/dist/cjs/ssr/ssr-server.cjs +107 -0
  59. package/dist/cjs/ssr/ssr-server.cjs.map +1 -0
  60. package/dist/cjs/ssr/ssr-server.d.cts +18 -0
  61. package/dist/cjs/ssr/transformStreamWithRouter.cjs +183 -0
  62. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -0
  63. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +6 -0
  64. package/dist/cjs/ssr/tsrScript.cjs +4 -0
  65. package/dist/cjs/ssr/tsrScript.cjs.map +1 -0
  66. package/dist/cjs/ssr/tsrScript.d.cts +0 -0
  67. package/dist/cjs/utils.cjs +7 -25
  68. package/dist/cjs/utils.cjs.map +1 -1
  69. package/dist/cjs/utils.d.cts +1 -6
  70. package/dist/esm/Matches.d.ts +31 -1
  71. package/dist/esm/Matches.js.map +1 -1
  72. package/dist/esm/RouterProvider.d.ts +2 -1
  73. package/dist/esm/defer.js +1 -1
  74. package/dist/esm/defer.js.map +1 -1
  75. package/dist/esm/global.d.ts +7 -0
  76. package/dist/esm/index.d.ts +6 -6
  77. package/dist/esm/index.js +2 -3
  78. package/dist/esm/link.d.ts +12 -0
  79. package/dist/esm/link.js.map +1 -1
  80. package/dist/esm/lru-cache.d.ts +5 -0
  81. package/dist/esm/lru-cache.js +62 -0
  82. package/dist/esm/lru-cache.js.map +1 -0
  83. package/dist/esm/not-found.js +1 -1
  84. package/dist/esm/not-found.js.map +1 -1
  85. package/dist/esm/path.d.ts +18 -24
  86. package/dist/esm/path.js +316 -148
  87. package/dist/esm/path.js.map +1 -1
  88. package/dist/esm/qss.js.map +1 -1
  89. package/dist/esm/redirect.js +3 -0
  90. package/dist/esm/redirect.js.map +1 -1
  91. package/dist/esm/route.d.ts +29 -9
  92. package/dist/esm/route.js +6 -12
  93. package/dist/esm/route.js.map +1 -1
  94. package/dist/esm/router.d.ts +55 -85
  95. package/dist/esm/router.js +462 -281
  96. package/dist/esm/router.js.map +1 -1
  97. package/dist/esm/scroll-restoration.d.ts +9 -1
  98. package/dist/esm/scroll-restoration.js +20 -13
  99. package/dist/esm/scroll-restoration.js.map +1 -1
  100. package/dist/esm/searchMiddleware.js.map +1 -1
  101. package/dist/esm/searchParams.js.map +1 -1
  102. package/dist/esm/ssr/client.d.ts +5 -0
  103. package/dist/esm/ssr/client.js +10 -0
  104. package/dist/esm/ssr/client.js.map +1 -0
  105. package/dist/esm/ssr/createRequestHandler.d.ts +9 -0
  106. package/dist/esm/ssr/createRequestHandler.js +50 -0
  107. package/dist/esm/ssr/createRequestHandler.js.map +1 -0
  108. package/dist/esm/ssr/handlerCallback.d.ts +9 -0
  109. package/dist/esm/ssr/handlerCallback.js +7 -0
  110. package/dist/esm/ssr/handlerCallback.js.map +1 -0
  111. package/dist/esm/ssr/headers.d.ts +5 -0
  112. package/dist/esm/ssr/headers.js +39 -0
  113. package/dist/esm/ssr/headers.js.map +1 -0
  114. package/dist/esm/ssr/json.d.ts +4 -0
  115. package/dist/esm/ssr/json.js +14 -0
  116. package/dist/esm/ssr/json.js.map +1 -0
  117. package/dist/esm/ssr/seroval-plugins.d.ts +10 -0
  118. package/dist/esm/ssr/seroval-plugins.js +34 -0
  119. package/dist/esm/ssr/seroval-plugins.js.map +1 -0
  120. package/dist/esm/ssr/server.d.ts +6 -0
  121. package/dist/esm/ssr/server.js +13 -0
  122. package/dist/esm/ssr/server.js.map +1 -0
  123. package/dist/esm/ssr/ssr-client.d.ts +29 -0
  124. package/dist/esm/ssr/ssr-client.js +159 -0
  125. package/dist/esm/ssr/ssr-client.js.map +1 -0
  126. package/dist/esm/ssr/ssr-server.d.ts +18 -0
  127. package/dist/esm/ssr/ssr-server.js +107 -0
  128. package/dist/esm/ssr/ssr-server.js.map +1 -0
  129. package/dist/esm/ssr/transformStreamWithRouter.d.ts +6 -0
  130. package/dist/esm/ssr/transformStreamWithRouter.js +183 -0
  131. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -0
  132. package/dist/esm/ssr/tsrScript.d.ts +0 -0
  133. package/dist/esm/ssr/tsrScript.js +5 -0
  134. package/dist/esm/ssr/tsrScript.js.map +1 -0
  135. package/dist/esm/utils.d.ts +1 -6
  136. package/dist/esm/utils.js +8 -26
  137. package/dist/esm/utils.js.map +1 -1
  138. package/package.json +29 -2
  139. package/src/Matches.ts +40 -1
  140. package/src/RouterProvider.ts +2 -1
  141. package/src/global.ts +9 -0
  142. package/src/index.ts +12 -20
  143. package/src/link.ts +12 -0
  144. package/src/lru-cache.ts +68 -0
  145. package/src/path.ts +424 -174
  146. package/src/redirect.ts +3 -0
  147. package/src/route.ts +44 -13
  148. package/src/router.ts +580 -312
  149. package/src/scroll-restoration.ts +30 -18
  150. package/src/ssr/client.ts +5 -0
  151. package/src/ssr/createRequestHandler.ts +74 -0
  152. package/src/ssr/handlerCallback.ts +15 -0
  153. package/src/ssr/headers.ts +51 -0
  154. package/src/ssr/json.ts +18 -0
  155. package/src/ssr/seroval-plugins.ts +43 -0
  156. package/src/ssr/server.ts +10 -0
  157. package/src/ssr/ssr-client.ts +242 -0
  158. package/src/ssr/ssr-server.ts +132 -0
  159. package/src/ssr/transformStreamWithRouter.ts +259 -0
  160. package/src/ssr/tsrScript.ts +7 -0
  161. package/src/utils.ts +10 -39
  162. package/src/vite-env.d.ts +4 -0
  163. package/dist/cjs/serializer.d.cts +0 -22
  164. package/dist/esm/serializer.d.ts +0 -22
  165. package/src/serializer.ts +0 -32
@@ -2,6 +2,7 @@ import { functionalUpdate } from './utils'
2
2
  import type { AnyRouter } from './router'
3
3
  import type { ParsedLocation } from './location'
4
4
  import type { NonNullableUpdater } from './utils'
5
+ import type { HistoryLocation } from '@tanstack/history'
5
6
 
6
7
  export type ScrollRestorationEntry = { scrollX: number; scrollY: number }
7
8
 
@@ -79,7 +80,7 @@ export const scrollRestorationCache = createScrollRestorationCache()
79
80
  */
80
81
 
81
82
  export const defaultGetScrollRestorationKey = (location: ParsedLocation) => {
82
- return location.state.key! || location.href
83
+ return location.state.__TSR_key! || location.href
83
84
  }
84
85
 
85
86
  export function getCssSelector(el: any): string {
@@ -100,15 +101,21 @@ let ignoreScroll = false
100
101
  // unless they are passed in as arguments. Why? Because we need to be able to
101
102
  // toString() it into a script tag to execute as early as possible in the browser
102
103
  // during SSR. Additionally, we also call it from within the router lifecycle
103
- export function restoreScroll(
104
- storageKey: string,
105
- key: string | undefined,
106
- behavior: ScrollToOptions['behavior'] | undefined,
107
- shouldScrollRestoration: boolean | undefined,
108
- scrollToTopSelectors:
109
- | Array<string | (() => Element | null | undefined)>
110
- | undefined,
111
- ) {
104
+ export function restoreScroll({
105
+ storageKey,
106
+ key,
107
+ behavior,
108
+ shouldScrollRestoration,
109
+ scrollToTopSelectors,
110
+ location,
111
+ }: {
112
+ storageKey: string
113
+ key?: string
114
+ behavior?: ScrollToOptions['behavior']
115
+ shouldScrollRestoration?: boolean
116
+ scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>
117
+ location?: HistoryLocation
118
+ }) {
112
119
  let byKey: ScrollRestorationByKey
113
120
 
114
121
  try {
@@ -128,7 +135,11 @@ export function restoreScroll(
128
135
  ;(() => {
129
136
  // If we have a cached entry for this location state,
130
137
  // we always need to prefer that over the hash scroll.
131
- if (shouldScrollRestoration && elementEntries) {
138
+ if (
139
+ shouldScrollRestoration &&
140
+ elementEntries &&
141
+ Object.keys(elementEntries).length > 0
142
+ ) {
132
143
  for (const elementSelector in elementEntries) {
133
144
  const entry = elementEntries[elementSelector]!
134
145
  if (elementSelector === 'window') {
@@ -153,7 +164,7 @@ export function restoreScroll(
153
164
  // Which means we've never seen this location before,
154
165
  // we need to check if there is a hash in the URL.
155
166
  // If there is, we need to scroll it's ID into view.
156
- const hash = window.location.hash.split('#')[1]
167
+ const hash = (location ?? window.location).hash.split('#')[1]
157
168
 
158
169
  if (hash) {
159
170
  const hashScrollIntoViewOptions =
@@ -321,13 +332,14 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
321
332
  return
322
333
  }
323
334
 
324
- restoreScroll(
335
+ restoreScroll({
325
336
  storageKey,
326
- cacheKey,
327
- router.options.scrollRestorationBehavior || undefined,
328
- router.isScrollRestoring || undefined,
329
- router.options.scrollToTopSelectors || undefined,
330
- )
337
+ key: cacheKey,
338
+ behavior: router.options.scrollRestorationBehavior,
339
+ shouldScrollRestoration: router.isScrollRestoring,
340
+ scrollToTopSelectors: router.options.scrollToTopSelectors,
341
+ location: router.history.location,
342
+ })
331
343
 
332
344
  if (router.isScrollRestoring) {
333
345
  // Mark the location as having been seen
@@ -0,0 +1,5 @@
1
+ export { mergeHeaders, headersInitToObject } from './headers'
2
+ export { json } from './json'
3
+ export type { JsonResponse } from './json'
4
+ export { hydrate } from './ssr-client'
5
+ export * from './ssr-client'
@@ -0,0 +1,74 @@
1
+ import { createMemoryHistory } from '@tanstack/history'
2
+ import { mergeHeaders } from './headers'
3
+ import { attachRouterServerSsrUtils } from './ssr-server'
4
+ import type { HandlerCallback } from './handlerCallback'
5
+ import type { AnyRouter } from '../router'
6
+ import type { Manifest } from '../manifest'
7
+
8
+ export type RequestHandler<TRouter extends AnyRouter> = (
9
+ cb: HandlerCallback<TRouter>,
10
+ ) => Promise<Response>
11
+
12
+ export function createRequestHandler<TRouter extends AnyRouter>({
13
+ createRouter,
14
+ request,
15
+ getRouterManifest,
16
+ }: {
17
+ createRouter: () => TRouter
18
+ request: Request
19
+ getRouterManifest?: () => Manifest | Promise<Manifest>
20
+ }): RequestHandler<TRouter> {
21
+ return async (cb) => {
22
+ const router = createRouter()
23
+
24
+ attachRouterServerSsrUtils(router, await getRouterManifest?.())
25
+
26
+ const url = new URL(request.url, 'http://localhost')
27
+
28
+ const href = url.href.replace(url.origin, '')
29
+
30
+ // Create a history for the router
31
+ const history = createMemoryHistory({
32
+ initialEntries: [href],
33
+ })
34
+
35
+ // Update the router with the history and context
36
+ router.update({
37
+ history,
38
+ })
39
+
40
+ await router.load()
41
+
42
+ await router.serverSsr?.dehydrate()
43
+
44
+ const responseHeaders = getRequestHeaders({
45
+ router,
46
+ })
47
+
48
+ return cb({
49
+ request,
50
+ router,
51
+ responseHeaders,
52
+ } as any)
53
+ }
54
+ }
55
+
56
+ function getRequestHeaders(opts: { router: AnyRouter }): Headers {
57
+ let headers = mergeHeaders(
58
+ {
59
+ 'Content-Type': 'text/html; charset=UTF-8',
60
+ },
61
+ ...opts.router.state.matches.map((match) => {
62
+ return match.headers
63
+ }),
64
+ )
65
+
66
+ // Handle Redirects
67
+ const { redirect } = opts.router.state
68
+
69
+ if (redirect) {
70
+ headers = mergeHeaders(headers, redirect.headers)
71
+ }
72
+
73
+ return headers
74
+ }
@@ -0,0 +1,15 @@
1
+ import type { AnyRouter } from '../router'
2
+
3
+ export interface HandlerCallback<TRouter extends AnyRouter> {
4
+ (ctx: {
5
+ request: Request
6
+ router: TRouter
7
+ responseHeaders: Headers
8
+ }): Response | Promise<Response>
9
+ }
10
+
11
+ export function defineHandlerCallback<TRouter extends AnyRouter>(
12
+ handler: HandlerCallback<TRouter>,
13
+ ): HandlerCallback<TRouter> {
14
+ return handler
15
+ }
@@ -0,0 +1,51 @@
1
+ import { splitSetCookieString } from 'cookie-es'
2
+ import type { OutgoingHttpHeaders } from 'node:http2'
3
+
4
+ // A utility function to turn HeadersInit into an object
5
+ export function headersInitToObject(
6
+ headers: HeadersInit,
7
+ ): Record<keyof OutgoingHttpHeaders, string> {
8
+ const obj: Record<keyof OutgoingHttpHeaders, string> = {}
9
+ const headersInstance = new Headers(headers)
10
+ for (const [key, value] of headersInstance.entries()) {
11
+ obj[key] = value
12
+ }
13
+ return obj
14
+ }
15
+
16
+ type AnyHeaders =
17
+ | Headers
18
+ | HeadersInit
19
+ | Record<string, string>
20
+ | Array<[string, string]>
21
+ | OutgoingHttpHeaders
22
+ | undefined
23
+
24
+ // Helper function to convert various HeaderInit types to a Headers instance
25
+ function toHeadersInstance(init: AnyHeaders) {
26
+ if (init instanceof Headers) {
27
+ return new Headers(init)
28
+ } else if (Array.isArray(init)) {
29
+ return new Headers(init)
30
+ } else if (typeof init === 'object') {
31
+ return new Headers(init as HeadersInit)
32
+ } else {
33
+ return new Headers()
34
+ }
35
+ }
36
+
37
+ // Function to merge headers with proper overrides
38
+ export function mergeHeaders(...headers: Array<AnyHeaders>) {
39
+ return headers.reduce((acc: Headers, header) => {
40
+ const headersInstance = toHeadersInstance(header)
41
+ for (const [key, value] of headersInstance.entries()) {
42
+ if (key === 'set-cookie') {
43
+ const splitCookies = splitSetCookieString(value)
44
+ splitCookies.forEach((cookie) => acc.append('set-cookie', cookie))
45
+ } else {
46
+ acc.set(key, value)
47
+ }
48
+ }
49
+ return acc
50
+ }, new Headers())
51
+ }
@@ -0,0 +1,18 @@
1
+ import { mergeHeaders } from './headers'
2
+
3
+ export interface JsonResponse<TData> extends Response {
4
+ json: () => Promise<TData>
5
+ }
6
+
7
+ export function json<TData>(
8
+ payload: TData,
9
+ init?: ResponseInit,
10
+ ): JsonResponse<TData> {
11
+ return new Response(JSON.stringify(payload), {
12
+ ...init,
13
+ headers: mergeHeaders(
14
+ { 'content-type': 'application/json' },
15
+ init?.headers,
16
+ ),
17
+ })
18
+ }
@@ -0,0 +1,43 @@
1
+ import { createPlugin } from 'seroval'
2
+ import type { SerovalNode } from 'seroval'
3
+
4
+ interface ErrorNode {
5
+ message: SerovalNode
6
+ }
7
+
8
+ /**
9
+ * this plugin serializes only the `message` part of an Error
10
+ * this helps with serializing e.g. a ZodError which has functions attached that cannot be serialized
11
+ */
12
+ export const ShallowErrorPlugin = /* @__PURE__ */ createPlugin<
13
+ Error,
14
+ ErrorNode
15
+ >({
16
+ tag: 'tanstack-start:seroval-plugins/Error',
17
+ test(value) {
18
+ return value instanceof Error
19
+ },
20
+ parse: {
21
+ sync(value, ctx) {
22
+ return {
23
+ message: ctx.parse(value.message),
24
+ }
25
+ },
26
+ async async(value, ctx) {
27
+ return {
28
+ message: await ctx.parse(value.message),
29
+ }
30
+ },
31
+ stream(value, ctx) {
32
+ return {
33
+ message: ctx.parse(value.message),
34
+ }
35
+ },
36
+ },
37
+ serialize(node, ctx) {
38
+ return 'new Error(' + ctx.serialize(node.message) + ')'
39
+ },
40
+ deserialize(node, ctx) {
41
+ return new Error(ctx.deserialize(node.message) as string)
42
+ },
43
+ })
@@ -0,0 +1,10 @@
1
+ export { createRequestHandler } from './createRequestHandler'
2
+ export type { RequestHandler } from './createRequestHandler'
3
+ export { defineHandlerCallback } from './handlerCallback'
4
+ export type { HandlerCallback } from './handlerCallback'
5
+ export {
6
+ transformPipeableStreamWithRouter,
7
+ transformStreamWithRouter,
8
+ transformReadableStreamWithRouter,
9
+ } from './transformStreamWithRouter'
10
+ export { attachRouterServerSsrUtils } from './ssr-server'
@@ -0,0 +1,242 @@
1
+ import invariant from 'tiny-invariant'
2
+ import { batch } from '@tanstack/store'
3
+ import { createControlledPromise } from '../utils'
4
+ import type { AnyRouteMatch, MakeRouteMatch } from '../Matches'
5
+ import type { AnyRouter } from '../router'
6
+ import type { Manifest } from '../manifest'
7
+ import type { RouteContextOptions } from '../route'
8
+ import type { GLOBAL_TSR } from './ssr-server'
9
+
10
+ declare global {
11
+ interface Window {
12
+ [GLOBAL_TSR]?: TsrSsrGlobal
13
+ }
14
+ }
15
+
16
+ export interface TsrSsrGlobal {
17
+ router?: DehydratedRouter
18
+ // clean scripts, shortened since this is sent for each streamed script
19
+ c: () => void
20
+ }
21
+
22
+ function hydrateMatch(
23
+ deyhydratedMatch: DehydratedMatch,
24
+ ): Partial<MakeRouteMatch> {
25
+ return {
26
+ id: deyhydratedMatch.i,
27
+ __beforeLoadContext: deyhydratedMatch.b,
28
+ loaderData: deyhydratedMatch.l,
29
+ status: deyhydratedMatch.s,
30
+ ssr: deyhydratedMatch.ssr,
31
+ updatedAt: deyhydratedMatch.u,
32
+ error: deyhydratedMatch.e,
33
+ }
34
+ }
35
+ export interface DehydratedMatch {
36
+ i: MakeRouteMatch['id']
37
+ b?: MakeRouteMatch['__beforeLoadContext']
38
+ l?: MakeRouteMatch['loaderData']
39
+ e?: MakeRouteMatch['error']
40
+ u: MakeRouteMatch['updatedAt']
41
+ s: MakeRouteMatch['status']
42
+ ssr?: MakeRouteMatch['ssr']
43
+ }
44
+
45
+ export interface DehydratedRouter {
46
+ manifest: Manifest | undefined
47
+ dehydratedData?: any
48
+ lastMatchId?: string
49
+ matches: Array<DehydratedMatch>
50
+ }
51
+
52
+ export async function hydrate(router: AnyRouter): Promise<any> {
53
+ invariant(
54
+ window.$_TSR?.router,
55
+ 'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
56
+ )
57
+
58
+ const { manifest, dehydratedData, lastMatchId } = window.$_TSR.router
59
+
60
+ router.ssr = {
61
+ manifest,
62
+ }
63
+
64
+ // Hydrate the router state
65
+ const matches = router.matchRoutes(router.state.location)
66
+
67
+ // kick off loading the route chunks
68
+ const routeChunkPromise = Promise.all(
69
+ matches.map((match) => {
70
+ const route = router.looseRoutesById[match.routeId]!
71
+ return router.loadRouteChunk(route)
72
+ }),
73
+ )
74
+
75
+ function setMatchForcePending(match: AnyRouteMatch) {
76
+ // usually the minPendingPromise is created in the Match component if a pending match is rendered
77
+ // however, this might be too late if the match synchronously resolves
78
+ const route = router.looseRoutesById[match.routeId]!
79
+ const pendingMinMs =
80
+ route.options.pendingMinMs ?? router.options.defaultPendingMinMs
81
+ if (pendingMinMs) {
82
+ const minPendingPromise = createControlledPromise<void>()
83
+ match.minPendingPromise = minPendingPromise
84
+ match._forcePending = true
85
+
86
+ setTimeout(() => {
87
+ minPendingPromise.resolve()
88
+ // We've handled the minPendingPromise, so we can delete it
89
+ router.updateMatch(match.id, (prev) => ({
90
+ ...prev,
91
+ minPendingPromise: undefined,
92
+ _forcePending: undefined,
93
+ }))
94
+ }, pendingMinMs)
95
+ }
96
+ }
97
+
98
+ // Right after hydration and before the first render, we need to rehydrate each match
99
+ // First step is to reyhdrate loaderData and __beforeLoadContext
100
+ let firstNonSsrMatchIndex: number | undefined = undefined
101
+ matches.forEach((match) => {
102
+ const dehydratedMatch = window.$_TSR!.router!.matches.find(
103
+ (d) => d.i === match.id,
104
+ )
105
+ if (!dehydratedMatch) {
106
+ Object.assign(match, { dehydrated: false, ssr: false })
107
+ return
108
+ }
109
+
110
+ Object.assign(match, hydrateMatch(dehydratedMatch))
111
+
112
+ if (match.ssr === false) {
113
+ match._dehydrated = false
114
+ } else {
115
+ match._dehydrated = true
116
+ }
117
+
118
+ if (match.ssr === 'data-only' || match.ssr === false) {
119
+ if (firstNonSsrMatchIndex === undefined) {
120
+ firstNonSsrMatchIndex = match.index
121
+ setMatchForcePending(match)
122
+ }
123
+ }
124
+ })
125
+
126
+ router.__store.setState((s) => {
127
+ return {
128
+ ...s,
129
+ matches,
130
+ }
131
+ })
132
+
133
+ // Allow the user to handle custom hydration data
134
+ await router.options.hydrate?.(dehydratedData)
135
+
136
+ // now that all necessary data is hydrated:
137
+ // 1) fully reconstruct the route context
138
+ // 2) execute `head()` and `scripts()` for each match
139
+ await Promise.all(
140
+ router.state.matches.map(async (match) => {
141
+ const route = router.looseRoutesById[match.routeId]!
142
+
143
+ const parentMatch = router.state.matches[match.index - 1]
144
+ const parentContext = parentMatch?.context ?? router.options.context ?? {}
145
+
146
+ // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
147
+ // so run it again and merge route context
148
+ const contextFnContext: RouteContextOptions<any, any, any, any> = {
149
+ deps: match.loaderDeps,
150
+ params: match.params,
151
+ context: parentContext,
152
+ location: router.state.location,
153
+ navigate: (opts: any) =>
154
+ router.navigate({ ...opts, _fromLocation: router.state.location }),
155
+ buildLocation: router.buildLocation,
156
+ cause: match.cause,
157
+ abortController: match.abortController,
158
+ preload: false,
159
+ matches,
160
+ }
161
+ match.__routeContext = route.options.context?.(contextFnContext) ?? {}
162
+
163
+ match.context = {
164
+ ...parentContext,
165
+ ...match.__routeContext,
166
+ ...match.__beforeLoadContext,
167
+ }
168
+
169
+ const assetContext = {
170
+ matches: router.state.matches,
171
+ match,
172
+ params: match.params,
173
+ loaderData: match.loaderData,
174
+ }
175
+ const headFnContent = await route.options.head?.(assetContext)
176
+
177
+ const scripts = await route.options.scripts?.(assetContext)
178
+
179
+ match.meta = headFnContent?.meta
180
+ match.links = headFnContent?.links
181
+ match.headScripts = headFnContent?.scripts
182
+ match.styles = headFnContent?.styles
183
+ match.scripts = scripts
184
+ }),
185
+ )
186
+
187
+ const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
188
+ const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
189
+ // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()
190
+ if (!hasSsrFalseMatches && !isSpaMode) {
191
+ matches.forEach((match) => {
192
+ // remove the _dehydrate flag since we won't run router.load() which would remove it
193
+ match._dehydrated = undefined
194
+ })
195
+ return routeChunkPromise
196
+ }
197
+
198
+ // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts
199
+ const loadPromise = Promise.resolve()
200
+ .then(() => router.load())
201
+ .catch((err) => {
202
+ console.error('Error during router hydration:', err)
203
+ })
204
+
205
+ // in SPA mode we need to keep the first match below the root route pending until router.load() is finished
206
+ // this will prevent that other pending components are rendered but hydration is not blocked
207
+ if (isSpaMode) {
208
+ const match = matches[1]
209
+ invariant(
210
+ match,
211
+ 'Expected to find a match below the root match in SPA mode.',
212
+ )
213
+ setMatchForcePending(match)
214
+
215
+ match._displayPending = true
216
+ match.displayPendingPromise = loadPromise
217
+
218
+ loadPromise.then(() => {
219
+ batch(() => {
220
+ // ensure router is not in status 'pending' anymore
221
+ // this usually happens in Transitioner but if loading synchronously resolves,
222
+ // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false
223
+ if (router.__store.state.status === 'pending') {
224
+ router.__store.setState((s) => ({
225
+ ...s,
226
+ status: 'idle',
227
+ resolvedLocation: s.location,
228
+ }))
229
+ }
230
+ // hide the pending component once the load is finished
231
+ router.updateMatch(match.id, (prev) => {
232
+ return {
233
+ ...prev,
234
+ _displayPending: undefined,
235
+ displayPendingPromise: undefined,
236
+ }
237
+ })
238
+ })
239
+ })
240
+ }
241
+ return routeChunkPromise
242
+ }
@@ -0,0 +1,132 @@
1
+ import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'
2
+ import { ReadableStreamPlugin } from 'seroval-plugins/web'
3
+ import invariant from 'tiny-invariant'
4
+ import { createControlledPromise } from '../utils'
5
+ import minifiedTsrBootStrapScript from './tsrScript?script-string'
6
+ import { ShallowErrorPlugin } from './seroval-plugins'
7
+ import type { AnyRouter } from '../router'
8
+ import type { DehydratedMatch } from './ssr-client'
9
+ import type { DehydratedRouter } from './client'
10
+ import type { AnyRouteMatch } from '../Matches'
11
+ import type { Manifest } from '../manifest'
12
+
13
+ declare module '../router' {
14
+ interface ServerSsr {
15
+ setRenderFinished: () => void
16
+ }
17
+ interface RouterEvents {
18
+ onInjectedHtml: {
19
+ type: 'onInjectedHtml'
20
+ promise: Promise<string>
21
+ }
22
+ }
23
+ }
24
+
25
+ export const GLOBAL_TSR = '$_TSR'
26
+ const SCOPE_ID = 'tsr'
27
+
28
+ export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
29
+ const dehydratedMatch: DehydratedMatch = {
30
+ i: match.id,
31
+ u: match.updatedAt,
32
+ s: match.status,
33
+ }
34
+
35
+ const properties = [
36
+ ['__beforeLoadContext', 'b'],
37
+ ['loaderData', 'l'],
38
+ ['error', 'e'],
39
+ ['ssr', 'ssr'],
40
+ ] as const
41
+
42
+ for (const [key, shorthand] of properties) {
43
+ if (match[key] !== undefined) {
44
+ dehydratedMatch[shorthand] = match[key]
45
+ }
46
+ }
47
+ return dehydratedMatch
48
+ }
49
+
50
+ export function attachRouterServerSsrUtils(
51
+ router: AnyRouter,
52
+ manifest: Manifest | undefined,
53
+ ) {
54
+ router.ssr = {
55
+ manifest,
56
+ }
57
+ const serializationRefs = new Map<unknown, number>()
58
+
59
+ let initialScriptSent = false
60
+ const getInitialScript = () => {
61
+ if (initialScriptSent) {
62
+ return ''
63
+ }
64
+ initialScriptSent = true
65
+ return `${getCrossReferenceHeader(SCOPE_ID)};${minifiedTsrBootStrapScript};`
66
+ }
67
+ let _dehydrated = false
68
+ const listeners: Array<() => void> = []
69
+
70
+ router.serverSsr = {
71
+ injectedHtml: [],
72
+ injectHtml: (getHtml) => {
73
+ const promise = Promise.resolve().then(getHtml)
74
+ router.serverSsr!.injectedHtml.push(promise)
75
+ router.emit({
76
+ type: 'onInjectedHtml',
77
+ promise,
78
+ })
79
+
80
+ return promise.then(() => {})
81
+ },
82
+ injectScript: (getScript) => {
83
+ return router.serverSsr!.injectHtml(async () => {
84
+ const script = await getScript()
85
+ return `<script class='$tsr'>${getInitialScript()}${script};if (typeof $_TSR !== 'undefined') $_TSR.c()</script>`
86
+ })
87
+ },
88
+ dehydrate: async () => {
89
+ invariant(!_dehydrated, 'router is already dehydrated!')
90
+ let matchesToDehydrate = router.state.matches
91
+ if (router.isShell()) {
92
+ // In SPA mode we only want to dehydrate the root match
93
+ matchesToDehydrate = matchesToDehydrate.slice(0, 1)
94
+ }
95
+ const matches = matchesToDehydrate.map(dehydrateMatch)
96
+
97
+ const dehydratedRouter: DehydratedRouter = {
98
+ manifest: router.ssr!.manifest,
99
+ matches,
100
+ }
101
+ const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
102
+ if (lastMatchId) {
103
+ dehydratedRouter.lastMatchId = lastMatchId
104
+ }
105
+ dehydratedRouter.dehydratedData = await router.options.dehydrate?.()
106
+ _dehydrated = true
107
+
108
+ const p = createControlledPromise<string>()
109
+ crossSerializeStream(dehydratedRouter, {
110
+ refs: serializationRefs,
111
+ // TODO make plugins configurable
112
+ plugins: [ReadableStreamPlugin, ShallowErrorPlugin],
113
+ onSerialize: (data, initial) => {
114
+ const serialized = initial ? `${GLOBAL_TSR}["router"]=` + data : data
115
+ router.serverSsr!.injectScript(() => serialized)
116
+ },
117
+ scopeId: SCOPE_ID,
118
+ onDone: () => p.resolve(''),
119
+ onError: (err) => p.reject(err),
120
+ })
121
+ // make sure the stream is kept open until the promise is resolved
122
+ router.serverSsr!.injectHtml(() => p)
123
+ },
124
+ isDehydrated() {
125
+ return _dehydrated
126
+ },
127
+ onRenderFinished: (listener) => listeners.push(listener),
128
+ setRenderFinished: () => {
129
+ listeners.forEach((l) => l())
130
+ },
131
+ }
132
+ }