@tanstack/router-core 1.171.15 → 1.171.16-pre.0

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 (134) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +3 -12
  3. package/dist/cjs/await-signal.cjs +21 -0
  4. package/dist/cjs/await-signal.cjs.map +1 -0
  5. package/dist/cjs/await-signal.d.cts +1 -0
  6. package/dist/cjs/index.cjs +3 -1
  7. package/dist/cjs/index.d.cts +3 -2
  8. package/dist/cjs/isServer/client.cjs +2 -0
  9. package/dist/cjs/isServer/client.cjs.map +1 -1
  10. package/dist/cjs/isServer/client.d.cts +1 -0
  11. package/dist/cjs/isServer/development.cjs +2 -0
  12. package/dist/cjs/isServer/development.cjs.map +1 -1
  13. package/dist/cjs/isServer/development.d.cts +1 -0
  14. package/dist/cjs/isServer/server.cjs +2 -0
  15. package/dist/cjs/isServer/server.cjs.map +1 -1
  16. package/dist/cjs/isServer/server.d.cts +1 -0
  17. package/dist/cjs/load-client.cjs +1470 -0
  18. package/dist/cjs/load-client.cjs.map +1 -0
  19. package/dist/cjs/load-client.d.cts +147 -0
  20. package/dist/cjs/load-server.cjs +565 -0
  21. package/dist/cjs/load-server.cjs.map +1 -0
  22. package/dist/cjs/load-server.d.cts +16 -0
  23. package/dist/cjs/redirect.cjs.map +1 -1
  24. package/dist/cjs/redirect.d.cts +0 -1
  25. package/dist/cjs/route.cjs.map +1 -1
  26. package/dist/cjs/router.cjs +187 -442
  27. package/dist/cjs/router.cjs.map +1 -1
  28. package/dist/cjs/router.d.cts +61 -51
  29. package/dist/cjs/ssr/client.cjs +2 -2
  30. package/dist/cjs/ssr/createRequestHandler.cjs +60 -9
  31. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  32. package/dist/cjs/ssr/createRequestHandler.d.cts +1 -0
  33. package/dist/cjs/ssr/handlerCallback.cjs +49 -4
  34. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
  35. package/dist/cjs/ssr/handlerCallback.d.cts +3 -0
  36. package/dist/cjs/ssr/server.cjs +4 -0
  37. package/dist/cjs/ssr/server.d.cts +2 -2
  38. package/dist/cjs/ssr/ssr-client.d.cts +1 -10
  39. package/dist/cjs/ssr/ssr-match-id.cjs +2 -2
  40. package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
  41. package/dist/cjs/ssr/ssr-server.cjs +6 -6
  42. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  43. package/dist/cjs/ssr/transformStreamWithRouter.cjs +27 -1
  44. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  45. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +2 -0
  46. package/dist/cjs/ssr/types.d.cts +0 -1
  47. package/dist/cjs/stores.cjs +30 -104
  48. package/dist/cjs/stores.cjs.map +1 -1
  49. package/dist/cjs/stores.d.cts +7 -32
  50. package/dist/cjs/utils.cjs +0 -4
  51. package/dist/cjs/utils.cjs.map +1 -1
  52. package/dist/esm/Matches.d.ts +3 -12
  53. package/dist/esm/Matches.js.map +1 -1
  54. package/dist/esm/await-signal.d.ts +1 -0
  55. package/dist/esm/await-signal.js +21 -0
  56. package/dist/esm/await-signal.js.map +1 -0
  57. package/dist/esm/index.d.ts +3 -2
  58. package/dist/esm/index.js +3 -2
  59. package/dist/esm/isServer/client.d.ts +1 -0
  60. package/dist/esm/isServer/client.js +2 -1
  61. package/dist/esm/isServer/client.js.map +1 -1
  62. package/dist/esm/isServer/development.d.ts +1 -0
  63. package/dist/esm/isServer/development.js +2 -1
  64. package/dist/esm/isServer/development.js.map +1 -1
  65. package/dist/esm/isServer/server.d.ts +1 -0
  66. package/dist/esm/isServer/server.js +2 -1
  67. package/dist/esm/isServer/server.js.map +1 -1
  68. package/dist/esm/load-client.d.ts +147 -0
  69. package/dist/esm/load-client.js +1462 -0
  70. package/dist/esm/load-client.js.map +1 -0
  71. package/dist/esm/load-server.d.ts +16 -0
  72. package/dist/esm/load-server.js +565 -0
  73. package/dist/esm/load-server.js.map +1 -0
  74. package/dist/esm/redirect.d.ts +0 -1
  75. package/dist/esm/redirect.js.map +1 -1
  76. package/dist/esm/route.js.map +1 -1
  77. package/dist/esm/router.d.ts +61 -51
  78. package/dist/esm/router.js +188 -444
  79. package/dist/esm/router.js.map +1 -1
  80. package/dist/esm/ssr/client.js +1 -1
  81. package/dist/esm/ssr/createRequestHandler.d.ts +1 -0
  82. package/dist/esm/ssr/createRequestHandler.js +61 -11
  83. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  84. package/dist/esm/ssr/handlerCallback.d.ts +3 -0
  85. package/dist/esm/ssr/handlerCallback.js +47 -5
  86. package/dist/esm/ssr/handlerCallback.js.map +1 -1
  87. package/dist/esm/ssr/server.d.ts +2 -2
  88. package/dist/esm/ssr/server.js +3 -3
  89. package/dist/esm/ssr/ssr-client.d.ts +1 -10
  90. package/dist/esm/ssr/ssr-match-id.js +2 -2
  91. package/dist/esm/ssr/ssr-match-id.js.map +1 -1
  92. package/dist/esm/ssr/ssr-server.js +6 -6
  93. package/dist/esm/ssr/ssr-server.js.map +1 -1
  94. package/dist/esm/ssr/transformStreamWithRouter.d.ts +2 -0
  95. package/dist/esm/ssr/transformStreamWithRouter.js +27 -1
  96. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  97. package/dist/esm/ssr/types.d.ts +0 -1
  98. package/dist/esm/stores.d.ts +7 -32
  99. package/dist/esm/stores.js +30 -104
  100. package/dist/esm/stores.js.map +1 -1
  101. package/dist/esm/utils.js +1 -4
  102. package/dist/esm/utils.js.map +1 -1
  103. package/package.json +3 -3
  104. package/src/Matches.ts +6 -21
  105. package/src/await-signal.ts +27 -0
  106. package/src/index.ts +1 -3
  107. package/src/isServer/client.ts +1 -0
  108. package/src/isServer/development.ts +1 -0
  109. package/src/isServer/server.ts +1 -0
  110. package/src/load-client.ts +2701 -0
  111. package/src/load-server.ts +849 -0
  112. package/src/redirect.ts +0 -1
  113. package/src/route.ts +2 -11
  114. package/src/router.ts +379 -769
  115. package/src/ssr/createRequestHandler.ts +109 -16
  116. package/src/ssr/handlerCallback.ts +76 -10
  117. package/src/ssr/server.ts +4 -1
  118. package/src/ssr/ssr-client.ts +1 -310
  119. package/src/ssr/ssr-match-id.ts +11 -2
  120. package/src/ssr/ssr-server.ts +9 -8
  121. package/src/ssr/transformStreamWithRouter.ts +37 -1
  122. package/src/ssr/types.ts +0 -1
  123. package/src/stores.ts +46 -208
  124. package/dist/cjs/load-matches.cjs +0 -658
  125. package/dist/cjs/load-matches.cjs.map +0 -1
  126. package/dist/cjs/load-matches.d.cts +0 -18
  127. package/dist/cjs/ssr/ssr-client.cjs +0 -183
  128. package/dist/cjs/ssr/ssr-client.cjs.map +0 -1
  129. package/dist/esm/load-matches.d.ts +0 -18
  130. package/dist/esm/load-matches.js +0 -656
  131. package/dist/esm/load-matches.js.map +0 -1
  132. package/dist/esm/ssr/ssr-client.js +0 -183
  133. package/dist/esm/ssr/ssr-client.js.map +0 -1
  134. package/src/load-matches.ts +0 -1278
@@ -1,11 +1,15 @@
1
1
  import { createMemoryHistory } from '@tanstack/history'
2
+ import { _getRenderedMatches } from '../load-client'
2
3
  import { mergeHeaders } from './headers'
3
4
  import {
4
5
  attachRouterServerSsrUtils,
5
6
  getNormalizedURL,
6
7
  getOrigin,
7
8
  } from './ssr-server'
8
- import { normalizeSsrResponse } from './handlerCallback'
9
+ import {
10
+ bindSsrResponseToRequest,
11
+ disposeSsrResponseDetached,
12
+ } from './handlerCallback'
9
13
  import type { HandlerCallback } from './handlerCallback'
10
14
  import type { AnyHeaders } from './headers'
11
15
  import type { AnyRouter } from '../router'
@@ -15,6 +19,79 @@ export type RequestHandler<TRouter extends AnyRouter> = (
15
19
  cb: HandlerCallback<TRouter>,
16
20
  ) => Promise<Response>
17
21
 
22
+ type RequestWaiter = ((reason: unknown) => void) | undefined
23
+
24
+ const requestWaiters = new WeakMap<AbortSignal, Array<RequestWaiter>>()
25
+
26
+ function removeRequestWaiter(
27
+ waiters: Array<RequestWaiter>,
28
+ index: number,
29
+ reject: (reason: unknown) => void,
30
+ ) {
31
+ if (waiters[index] !== reject) {
32
+ return
33
+ }
34
+ if (index !== waiters.length - 1) {
35
+ waiters[index] = undefined
36
+ return
37
+ }
38
+
39
+ waiters.pop()
40
+ while (waiters.length && waiters[waiters.length - 1] === undefined) {
41
+ waiters.pop()
42
+ }
43
+ }
44
+
45
+ export function waitForRequest<T>(
46
+ value: T | PromiseLike<T>,
47
+ signal: AbortSignal,
48
+ onLate?: (value: T) => void,
49
+ ): Promise<T> {
50
+ const promise = Promise.resolve(value)
51
+ if (signal.aborted) {
52
+ void promise.then(onLate, () => {})
53
+ return Promise.reject(signal.reason)
54
+ }
55
+
56
+ return new Promise<T>((resolve, reject) => {
57
+ let waiters = requestWaiters.get(signal)
58
+ let index: number
59
+ if (waiters) {
60
+ index = waiters.push(reject) - 1
61
+ } else {
62
+ const newWaiters: Array<RequestWaiter> = [reject]
63
+ waiters = newWaiters
64
+ index = 0
65
+ requestWaiters.set(signal, newWaiters)
66
+ signal.addEventListener(
67
+ 'abort',
68
+ () => {
69
+ requestWaiters.delete(signal)
70
+ for (const rejectWaiter of newWaiters) {
71
+ rejectWaiter?.(signal.reason)
72
+ }
73
+ newWaiters.length = 0
74
+ },
75
+ { once: true },
76
+ )
77
+ }
78
+ void promise.then(
79
+ (result) => {
80
+ removeRequestWaiter(waiters, index, reject)
81
+ if (signal.aborted) {
82
+ onLate?.(result)
83
+ } else {
84
+ resolve(result)
85
+ }
86
+ },
87
+ (error) => {
88
+ removeRequestWaiter(waiters, index, reject)
89
+ reject(error)
90
+ },
91
+ )
92
+ })
93
+ }
94
+
18
95
  export function createRequestHandler<TRouter extends AnyRouter>({
19
96
  createRouter,
20
97
  request,
@@ -25,13 +102,14 @@ export function createRequestHandler<TRouter extends AnyRouter>({
25
102
  getRouterManifest?: () => ServerManifest | Promise<ServerManifest>
26
103
  }): RequestHandler<TRouter> {
27
104
  return async (cb) => {
105
+ request.signal.throwIfAborted()
28
106
  const router = createRouter()
29
107
  let responseOwnsCleanup = false
30
108
 
31
109
  try {
32
110
  attachRouterServerSsrUtils({
33
111
  router,
34
- manifest: await getRouterManifest?.(),
112
+ manifest: await waitForRequest(getRouterManifest?.(), request.signal),
35
113
  })
36
114
 
37
115
  // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.
@@ -50,20 +128,41 @@ export function createRequestHandler<TRouter extends AnyRouter>({
50
128
  origin: router.options.origin ?? origin,
51
129
  })
52
130
 
53
- await router.load()
131
+ await router.load({
132
+ _signal: request.signal,
133
+ })
134
+ request.signal.throwIfAborted()
135
+
136
+ const result = router._serverResult
137
+ if (result?.type === 'redirect') {
138
+ return result.redirect
139
+ }
54
140
 
55
- await router.serverSsr?.dehydrate()
141
+ await waitForRequest(router.serverSsr?.dehydrate(), request.signal)
142
+ request.signal.throwIfAborted()
56
143
 
57
144
  const responseHeaders = getRequestHeaders({
58
145
  router,
59
146
  })
60
147
 
61
- const response = await cb({
62
- request,
148
+ request.signal.throwIfAborted()
149
+ const response = await waitForRequest(
150
+ cb({
151
+ request,
152
+ router,
153
+ responseHeaders,
154
+ }),
155
+ request.signal,
156
+ (late) => {
157
+ disposeSsrResponseDetached(late, request.signal.reason)
158
+ },
159
+ )
160
+ const ssrResponse = bindSsrResponseToRequest(
63
161
  router,
64
- responseHeaders,
65
- })
66
- const ssrResponse = normalizeSsrResponse(response)
162
+ response,
163
+ request.signal,
164
+ )
165
+ request.signal.throwIfAborted()
67
166
  responseOwnsCleanup = ssrResponse.serverSsrCleanup === 'stream'
68
167
  return ssrResponse.response
69
168
  } finally {
@@ -79,16 +178,10 @@ export function createRequestHandler<TRouter extends AnyRouter>({
79
178
 
80
179
  function getRequestHeaders(opts: { router: AnyRouter }): Headers {
81
180
  const matchHeaders: Array<AnyHeaders> = []
82
- for (const match of opts.router.stores.matches.get()) {
181
+ for (const match of _getRenderedMatches(opts.router.stores.matches.get())) {
83
182
  matchHeaders.push(match.headers)
84
183
  }
85
184
 
86
- // Handle Redirects
87
- const redirect = opts.router.stores.redirect.get()
88
- if (redirect) {
89
- matchHeaders.push(redirect.headers)
90
- }
91
-
92
185
  return mergeHeaders(
93
186
  {
94
187
  'Content-Type': 'text/html; charset=UTF-8',
@@ -30,6 +30,40 @@ export function normalizeSsrResponse(
30
30
  : { response: result, serverSsrCleanup: 'none' }
31
31
  }
32
32
 
33
+ export function disposeSsrResponse(
34
+ response: SsrResponse,
35
+ reason?: unknown,
36
+ ): Promise<void> {
37
+ if (response.serverSsrCleanup !== 'stream') {
38
+ return Promise.resolve()
39
+ }
40
+ try {
41
+ return Promise.resolve(response.dispose(reason))
42
+ } catch (error) {
43
+ return Promise.reject(error)
44
+ }
45
+ }
46
+
47
+ export function disposeSsrResponseDetached(
48
+ result: HandlerCallbackResult,
49
+ reason?: unknown,
50
+ onError: (error: unknown) => void = console.error,
51
+ ): void {
52
+ const ssrResponse = normalizeSsrResponse(result)
53
+ if (ssrResponse.serverSsrCleanup === 'stream') {
54
+ void disposeSsrResponse(ssrResponse, reason).catch(onError)
55
+ return
56
+ }
57
+
58
+ if (ssrResponse.response.body) {
59
+ try {
60
+ void ssrResponse.response.body.cancel(reason).catch(onError)
61
+ } catch (error) {
62
+ onError(error)
63
+ }
64
+ }
65
+ }
66
+
33
67
  export function createSsrStreamResponse<TRouter extends AnyRouter>(
34
68
  router: TRouter,
35
69
  response: Response,
@@ -43,29 +77,63 @@ export function createSsrStreamResponse<TRouter extends AnyRouter>(
43
77
  response,
44
78
  serverSsrCleanup: 'stream',
45
79
  async dispose(reason?: unknown) {
46
- if (disposed) return
80
+ if (disposed) {
81
+ return
82
+ }
47
83
  disposed = true
48
84
 
85
+ // Sever router ownership before asking user/renderer stream machinery to
86
+ // cancel. A custom stream is allowed to ignore cancellation forever.
87
+ router.serverSsr?.cleanup()
88
+
49
89
  try {
50
90
  await response.body!.cancel(reason)
51
91
  } catch {
52
- // ignore; fallback cleanup below still releases router SSR state
92
+ // Cleanup above already released router SSR state.
53
93
  }
54
-
55
- router.serverSsr?.cleanup()
56
94
  },
57
95
  }
58
96
  }
59
97
 
98
+ export function bindSsrResponseToRequest(
99
+ router: AnyRouter | undefined,
100
+ result: HandlerCallbackResult,
101
+ signal: AbortSignal,
102
+ ): SsrResponse {
103
+ const ssrResponse = normalizeSsrResponse(result)
104
+ if (ssrResponse.serverSsrCleanup !== 'stream') {
105
+ if (signal.aborted) {
106
+ disposeSsrResponseDetached(result, signal.reason)
107
+ }
108
+ return ssrResponse
109
+ }
110
+
111
+ const failed = (error: unknown) => {
112
+ router?.serverSsr?.cleanup()
113
+ console.error(error)
114
+ }
115
+ const abort = () => {
116
+ disposeSsrResponseDetached(ssrResponse, signal.reason, failed)
117
+ }
118
+ if (signal.aborted) {
119
+ abort()
120
+ return ssrResponse
121
+ }
122
+
123
+ signal.addEventListener('abort', abort, { once: true })
124
+ router?.serverSsr?.onCleanup(() => {
125
+ signal.removeEventListener('abort', abort)
126
+ })
127
+ return ssrResponse
128
+ }
129
+
60
130
  export async function replaceSsrResponse(
61
131
  result: HandlerCallbackResult,
62
132
  response: Response,
63
133
  reason?: unknown,
64
134
  ): Promise<SsrResponse> {
65
135
  const ssrResponse = normalizeSsrResponse(result)
66
- if (ssrResponse.serverSsrCleanup === 'stream') {
67
- await ssrResponse.dispose(reason)
68
- }
136
+ await disposeSsrResponse(ssrResponse, reason)
69
137
  return { response, serverSsrCleanup: 'none' }
70
138
  }
71
139
 
@@ -74,9 +142,7 @@ export async function stripSsrResponseBody(
74
142
  reason?: unknown,
75
143
  ): Promise<SsrResponse> {
76
144
  const ssrResponse = normalizeSsrResponse(result)
77
- if (ssrResponse.serverSsrCleanup === 'stream') {
78
- await ssrResponse.dispose(reason)
79
- }
145
+ await disposeSsrResponse(ssrResponse, reason)
80
146
  return {
81
147
  response: new Response(null, ssrResponse.response),
82
148
  serverSsrCleanup: 'none',
package/src/ssr/server.ts CHANGED
@@ -1,8 +1,11 @@
1
- export { createRequestHandler } from './createRequestHandler'
1
+ export { createRequestHandler, waitForRequest } from './createRequestHandler'
2
2
  export type { RequestHandler } from './createRequestHandler'
3
3
  export {
4
+ bindSsrResponseToRequest,
4
5
  createSsrStreamResponse,
5
6
  defineHandlerCallback,
7
+ disposeSsrResponse,
8
+ disposeSsrResponseDetached,
6
9
  isSsrResponse,
7
10
  normalizeSsrResponse,
8
11
  replaceSsrResponse,
@@ -1,310 +1 @@
1
- import { invariant } from '../invariant'
2
- import { isNotFound } from '../not-found'
3
- import { createControlledPromise } from '../utils'
4
- import { hydrateSsrMatchId } from './ssr-match-id'
5
- import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants'
6
- import type { DehydratedMatch, TsrSsrGlobal } from './types'
7
- import type { AnyRouteMatch } from '../Matches'
8
- import type { AnyRouter } from '../router'
9
- import type { RouteContextOptions } from '../route'
10
- import type { AnySerializationAdapter } from './serializer/transformer'
11
-
12
- declare global {
13
- interface Window {
14
- [GLOBAL_TSR]?: TsrSsrGlobal
15
- [GLOBAL_SEROVAL]?: any
16
- }
17
- }
18
-
19
- function hydrateMatch(
20
- match: AnyRouteMatch,
21
- deyhydratedMatch: DehydratedMatch,
22
- ): void {
23
- match.id = deyhydratedMatch.i
24
- match.__beforeLoadContext = deyhydratedMatch.b
25
- match.loaderData = deyhydratedMatch.l
26
- match.status = deyhydratedMatch.s
27
- match.ssr = deyhydratedMatch.ssr
28
- match.updatedAt = deyhydratedMatch.u
29
- match.error = deyhydratedMatch.e
30
- // Only hydrate global-not-found when a defined value is present in the
31
- // dehydrated payload. If omitted, preserve the value computed from the
32
- // current client location (important for SPA fallback HTML served at unknown
33
- // URLs, where dehydrated matches may come from `/` but client matching marks
34
- // root as globalNotFound).
35
- if (deyhydratedMatch.g !== undefined) {
36
- match.globalNotFound = deyhydratedMatch.g
37
- }
38
- }
39
-
40
- export async function hydrate(router: AnyRouter): Promise<any> {
41
- if (!window.$_TSR) {
42
- if (process.env.NODE_ENV !== 'production') {
43
- throw new Error(
44
- 'Invariant failed: Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',
45
- )
46
- }
47
-
48
- invariant()
49
- }
50
-
51
- const serializationAdapters = router.options.serializationAdapters as
52
- | Array<AnySerializationAdapter>
53
- | undefined
54
-
55
- if (serializationAdapters?.length) {
56
- const fromSerializableMap = new Map()
57
- serializationAdapters.forEach((adapter) => {
58
- fromSerializableMap.set(adapter.key, adapter.fromSerializable)
59
- })
60
- window.$_TSR.t = fromSerializableMap
61
- window.$_TSR.buffer.forEach((script) => script())
62
- }
63
- window.$_TSR.initialized = true
64
-
65
- if (!window.$_TSR.router) {
66
- if (process.env.NODE_ENV !== 'production') {
67
- throw new Error(
68
- 'Invariant failed: Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
69
- )
70
- }
71
-
72
- invariant()
73
- }
74
-
75
- const dehydratedRouter = window.$_TSR.router
76
- dehydratedRouter.matches.forEach((dehydratedMatch) => {
77
- dehydratedMatch.i = hydrateSsrMatchId(dehydratedMatch.i)
78
- })
79
- if (dehydratedRouter.lastMatchId) {
80
- dehydratedRouter.lastMatchId = hydrateSsrMatchId(
81
- dehydratedRouter.lastMatchId,
82
- )
83
- }
84
- const { manifest, dehydratedData, lastMatchId } = dehydratedRouter
85
-
86
- router.ssr = {
87
- manifest,
88
- }
89
- const meta = document.querySelector('meta[property="csp-nonce"]') as
90
- | HTMLMetaElement
91
- | undefined
92
- const nonce = meta?.content
93
- router.options.ssr = {
94
- nonce,
95
- }
96
-
97
- // Allow the user to handle custom hydration data before matching routes.
98
- // This lets hydration install router config that affects matching, e.g. rewrites.
99
- await router.options.hydrate?.(dehydratedData)
100
-
101
- // Hydrate the router state
102
- const matches = router.matchRoutes(router.stores.location.get())
103
-
104
- // kick off loading the route chunks
105
- const routeChunkPromise = Promise.all(
106
- matches.map((match) =>
107
- router.loadRouteChunk(router.looseRoutesById[match.routeId]!),
108
- ),
109
- )
110
-
111
- function setMatchForcePending(match: AnyRouteMatch) {
112
- // usually the minPendingPromise is created in the Match component if a pending match is rendered
113
- // however, this might be too late if the match synchronously resolves
114
- const route = router.looseRoutesById[match.routeId]!
115
- const pendingMinMs =
116
- route.options.pendingMinMs ?? router.options.defaultPendingMinMs
117
- if (pendingMinMs) {
118
- const minPendingPromise = createControlledPromise<void>()
119
- match._nonReactive.minPendingPromise = minPendingPromise
120
- match._forcePending = true
121
-
122
- setTimeout(() => {
123
- minPendingPromise.resolve()
124
- // We've handled the minPendingPromise, so we can delete it
125
- router.updateMatch(match.id, (prev) => {
126
- prev._nonReactive.minPendingPromise = undefined
127
- return {
128
- ...prev,
129
- _forcePending: undefined,
130
- }
131
- })
132
- }, pendingMinMs)
133
- }
134
- }
135
-
136
- function setRouteSsr(match: AnyRouteMatch) {
137
- const route = router.looseRoutesById[match.routeId]
138
- if (route) {
139
- route.options.ssr = match.ssr
140
- }
141
- }
142
- // Right after hydration and before the first render, we need to rehydrate each match
143
- // First step is to reyhdrate loaderData and __beforeLoadContext
144
- let firstNonSsrMatchIndex: number | undefined = undefined
145
- matches.forEach((match) => {
146
- const dehydratedMatch = dehydratedRouter.matches.find(
147
- (d) => d.i === match.id,
148
- )
149
- if (!dehydratedMatch) {
150
- match._nonReactive.dehydrated = false
151
- match.ssr = false
152
- setRouteSsr(match)
153
- return
154
- }
155
-
156
- hydrateMatch(match, dehydratedMatch)
157
- setRouteSsr(match)
158
-
159
- match._nonReactive.dehydrated = match.ssr !== false
160
-
161
- if (match.ssr === 'data-only' || match.ssr === false) {
162
- if (firstNonSsrMatchIndex === undefined) {
163
- firstNonSsrMatchIndex = match.index
164
- setMatchForcePending(match)
165
- }
166
- }
167
- })
168
-
169
- router.stores.setMatches(matches)
170
-
171
- // now that all necessary data is hydrated:
172
- // 1) fully reconstruct the route context
173
- // 2) execute `head()` and `scripts()` for each match
174
- const activeMatches = router.stores.matches.get()
175
- const location = router.stores.location.get()
176
- await Promise.all(
177
- activeMatches.map(async (match) => {
178
- try {
179
- const route = router.looseRoutesById[match.routeId]!
180
-
181
- const parentMatch = activeMatches[match.index - 1]
182
- const parentContext = parentMatch?.context ?? router.options.context
183
-
184
- // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
185
- // so run it again and merge route context
186
- if (route.options.context) {
187
- const contextFnContext: RouteContextOptions<any, any, any, any, any> =
188
- {
189
- deps: match.loaderDeps,
190
- params: match.params,
191
- context: parentContext ?? {},
192
- location,
193
- navigate: (opts: any) =>
194
- router.navigate({
195
- ...opts,
196
- _fromLocation: location,
197
- }),
198
- buildLocation: router.buildLocation,
199
- cause: match.cause,
200
- abortController: match.abortController,
201
- preload: false,
202
- matches,
203
- routeId: route.id,
204
- }
205
- match.__routeContext =
206
- route.options.context(contextFnContext) ?? undefined
207
- }
208
-
209
- match.context = {
210
- ...parentContext,
211
- ...match.__routeContext,
212
- ...match.__beforeLoadContext,
213
- }
214
-
215
- const assetContext = {
216
- ssr: router.options.ssr,
217
- matches: activeMatches,
218
- match,
219
- params: match.params,
220
- loaderData: match.loaderData,
221
- }
222
- const headFnContent = await route.options.head?.(assetContext)
223
-
224
- const scripts = await route.options.scripts?.(assetContext)
225
-
226
- match.meta = headFnContent?.meta
227
- match.links = headFnContent?.links
228
- match.headScripts = headFnContent?.scripts
229
- match.styles = headFnContent?.styles
230
- match.scripts = scripts
231
- } catch (err) {
232
- if (isNotFound(err)) {
233
- match.error = { isNotFound: true }
234
- console.error(
235
- `NotFound error during hydration for routeId: ${match.routeId}`,
236
- err,
237
- )
238
- } else {
239
- match.error = err as any
240
- console.error(
241
- `Error during hydration for route ${match.routeId}:`,
242
- err,
243
- )
244
- throw err
245
- }
246
- }
247
- }),
248
- )
249
-
250
- const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
251
- const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
252
- // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()
253
- if (!hasSsrFalseMatches && !isSpaMode) {
254
- matches.forEach((match) => {
255
- // remove the dehydrated flag since we won't run router.load() which would remove it
256
- match._nonReactive.dehydrated = undefined
257
- })
258
- // Mark the current location as resolved so that later load cycles
259
- // (e.g. preloads, invalidations) don't mistakenly detect a href change
260
- // (resolvedLocation defaults to undefined and router.load() is skipped
261
- // in the normal SSR hydration path).
262
- router.stores.resolvedLocation.set(router.stores.location.get())
263
- return routeChunkPromise
264
- }
265
-
266
- // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts
267
- const loadPromise = Promise.resolve()
268
- .then(() => router.load())
269
- .catch((err) => {
270
- console.error('Error during router hydration:', err)
271
- })
272
-
273
- // in SPA mode we need to keep the first match below the root route pending until router.load() is finished
274
- // this will prevent that other pending components are rendered but hydration is not blocked
275
- if (isSpaMode) {
276
- const match = matches[1]
277
- if (!match) {
278
- if (process.env.NODE_ENV !== 'production') {
279
- throw new Error(
280
- 'Invariant failed: Expected to find a match below the root match in SPA mode.',
281
- )
282
- }
283
-
284
- invariant()
285
- }
286
- setMatchForcePending(match)
287
-
288
- match._displayPending = true
289
- match._nonReactive.displayPendingPromise = loadPromise
290
-
291
- loadPromise.then(() => {
292
- router.batch(() => {
293
- // ensure router is not in status 'pending' anymore
294
- // this usually happens in Transitioner but if loading synchronously resolves,
295
- // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false
296
- if (router.stores.status.get() === 'pending') {
297
- router.stores.status.set('idle')
298
- router.stores.resolvedLocation.set(router.stores.location.get())
299
- }
300
- // hide the pending component once the load is finished
301
- router.updateMatch(match.id, (prev) => ({
302
- ...prev,
303
- _displayPending: undefined,
304
- displayPendingPromise: undefined,
305
- }))
306
- })
307
- })
308
- }
309
- return routeChunkPromise
310
- }
1
+ export { hydrate } from '../load-client'
@@ -1,7 +1,16 @@
1
1
  export function dehydrateSsrMatchId(id: string): string {
2
- return id.replaceAll('/', '\0')
2
+ return id
3
+ .replaceAll('~', '~~')
4
+ .replaceAll('\0', '~0')
5
+ .replaceAll('\uFFFD', '~r')
6
+ .replaceAll('/', '\0')
3
7
  }
4
8
 
5
9
  export function hydrateSsrMatchId(id: string): string {
6
- return id.replaceAll('\0', '/').replaceAll('\uFFFD', '/')
10
+ return id
11
+ .replaceAll('\0', '/')
12
+ .replaceAll('\uFFFD', '/')
13
+ .replace(/~([~0r])/g, (_, code) =>
14
+ code === '0' ? '\0' : code === 'r' ? '\uFFFD' : code,
15
+ )
7
16
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { decodePath } from '../utils'
9
9
  import { createLRUCache } from '../lru-cache'
10
10
  import { rootRouteId } from '../root'
11
+ import { _getRenderedMatches } from '../load-client'
11
12
  import minifiedTsrBootStrapScript from './tsrScript?script-string'
12
13
  import { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'
13
14
  import { dehydrateSsrMatchId } from './ssr-match-id'
@@ -51,7 +52,7 @@ export function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {
51
52
  dehydratedMatch[shorthand] = match[key]
52
53
  }
53
54
  }
54
- if (match.globalNotFound) {
55
+ if (match._notFound) {
55
56
  dehydratedMatch.g = true
56
57
  }
57
58
  return dehydratedMatch
@@ -389,7 +390,7 @@ export function attachRouterServerSsrUtils({
389
390
  if (!manifest) return manifest
390
391
 
391
392
  const requestAssets = getRequestAssets?.()
392
- const matches = router.stores.matches.get()
393
+ const matches = _getRenderedMatches(router.stores.matches.get())
393
394
  const hasAssets = hasRequestAssets(requestAssets)
394
395
 
395
396
  if (!hasAssets && !manifest.inlineCss) {
@@ -495,8 +496,9 @@ export function attachRouterServerSsrUtils({
495
496
 
496
497
  invariant()
497
498
  }
498
- let matchesToDehydrate = router.stores.matches.get()
499
- if (router.isShell()) {
499
+ let matchesToDehydrate = _getRenderedMatches(router.stores.matches.get())
500
+ const isShell = router.isShell()
501
+ if (isShell) {
500
502
  // In SPA mode we only want to dehydrate the root match
501
503
  matchesToDehydrate = matchesToDehydrate.slice(0, 1)
502
504
  }
@@ -540,11 +542,10 @@ export function attachRouterServerSsrUtils({
540
542
  manifest: manifestToDehydrate,
541
543
  matches,
542
544
  }
543
- const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
544
- if (lastMatchId) {
545
- dehydratedRouter.lastMatchId = dehydrateSsrMatchId(lastMatchId)
546
- }
547
545
  const dehydratedData = await router.options.dehydrate?.()
546
+ if (cleanupStarted) {
547
+ return
548
+ }
548
549
  if (dehydratedData) {
549
550
  dehydratedRouter.dehydratedData = dehydratedData
550
551
  }