@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.30 → 3.5.0-ultramodern.32

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 (221) hide show
  1. package/dist/cjs/cli/artifacts.js +157 -0
  2. package/dist/cjs/cli/index.js +6 -103
  3. package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +113 -0
  4. package/dist/cjs/cli/tanstackTypes/shared.js +130 -0
  5. package/dist/cjs/cli/tanstackTypes/source.js +251 -0
  6. package/dist/cjs/cli/tanstackTypes.js +4 -350
  7. package/dist/cjs/runtime/blockingSubscribe.js +64 -0
  8. package/dist/cjs/runtime/{pluginCore.js → clientBasepath.js} +12 -19
  9. package/dist/cjs/runtime/clientHydration.js +129 -0
  10. package/dist/cjs/runtime/dataMutation.js +11 -176
  11. package/dist/cjs/runtime/formData.js +116 -0
  12. package/dist/cjs/runtime/loaderBridge.js +4 -1
  13. package/dist/cjs/runtime/plugin.js +21 -125
  14. package/dist/cjs/runtime/plugin.node.js +38 -119
  15. package/dist/cjs/runtime/pluginShared.js +90 -0
  16. package/dist/cjs/runtime/routeTree/components.js +102 -0
  17. package/dist/cjs/runtime/routeTree/construction.js +152 -0
  18. package/dist/cjs/runtime/routeTree/index.js +47 -0
  19. package/dist/cjs/runtime/routeTree/loaders.js +158 -0
  20. package/dist/cjs/runtime/routeTree/paths.js +57 -0
  21. package/dist/cjs/runtime/routeTree/staticData.js +63 -0
  22. package/dist/cjs/runtime/routeTree/tanstackRoutes.js +57 -0
  23. package/dist/cjs/runtime/routeTree/types.js +18 -0
  24. package/dist/cjs/runtime/routeTree.js +7 -320
  25. package/dist/cjs/runtime/rsc/CompositeComponent.js +18 -13
  26. package/dist/cjs/runtime/rsc/RscNodeRenderer.js +5 -16
  27. package/dist/cjs/runtime/rsc/client.js +8 -25
  28. package/dist/cjs/runtime/rsc/flightSerialization.js +132 -0
  29. package/dist/cjs/runtime/rsc/payloadErrors.js +99 -0
  30. package/dist/cjs/runtime/rsc/payloadFetch.js +117 -0
  31. package/dist/cjs/runtime/rsc/payloadRouter.js +21 -142
  32. package/dist/cjs/runtime/rsc/payloadRoutes.js +106 -0
  33. package/dist/cjs/runtime/rsc/payloadStorage.js +58 -0
  34. package/dist/cjs/runtime/rsc/server.js +14 -18
  35. package/dist/cjs/runtime/rsc/shared.js +71 -0
  36. package/dist/cjs/runtime/rsc/treeGetter.js +72 -0
  37. package/dist/cjs/runtime/ssrManagedTags.js +103 -0
  38. package/dist/cjs/runtime/ssrPreload.js +89 -0
  39. package/dist/cjs/runtime/ssrTypes.js +18 -0
  40. package/dist/cjs/runtime/submitAction.js +161 -0
  41. package/dist/cjs/runtime/utils.js +49 -86
  42. package/dist/esm/cli/artifacts.mjs +103 -0
  43. package/dist/esm/cli/index.mjs +4 -101
  44. package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +75 -0
  45. package/dist/esm/cli/tanstackTypes/shared.mjs +68 -0
  46. package/dist/esm/cli/tanstackTypes/source.mjs +203 -0
  47. package/dist/esm/cli/tanstackTypes.mjs +2 -339
  48. package/dist/esm/runtime/blockingSubscribe.mjs +19 -0
  49. package/dist/esm/runtime/clientBasepath.mjs +10 -0
  50. package/dist/esm/runtime/clientHydration.mjs +70 -0
  51. package/dist/esm/runtime/dataMutation.mjs +2 -167
  52. package/dist/esm/runtime/formData.mjs +63 -0
  53. package/dist/esm/runtime/loaderBridge.mjs +4 -1
  54. package/dist/esm/runtime/plugin.mjs +14 -118
  55. package/dist/esm/runtime/plugin.node.mjs +33 -114
  56. package/dist/esm/runtime/pluginShared.mjs +40 -0
  57. package/dist/esm/runtime/routeTree/components.mjs +55 -0
  58. package/dist/esm/runtime/routeTree/construction.mjs +111 -0
  59. package/dist/esm/runtime/routeTree/index.mjs +2 -0
  60. package/dist/esm/runtime/routeTree/loaders.mjs +117 -0
  61. package/dist/esm/runtime/routeTree/paths.mjs +16 -0
  62. package/dist/esm/runtime/routeTree/staticData.mjs +22 -0
  63. package/dist/esm/runtime/routeTree/tanstackRoutes.mjs +13 -0
  64. package/dist/esm/runtime/routeTree/types.mjs +0 -0
  65. package/dist/esm/runtime/routeTree.mjs +1 -318
  66. package/dist/esm/runtime/rsc/CompositeComponent.mjs +18 -13
  67. package/dist/esm/runtime/rsc/RscNodeRenderer.mjs +4 -15
  68. package/dist/esm/runtime/rsc/client.mjs +6 -26
  69. package/dist/esm/runtime/rsc/flightSerialization.mjs +88 -0
  70. package/dist/esm/runtime/rsc/payloadErrors.mjs +49 -0
  71. package/dist/esm/runtime/rsc/payloadFetch.mjs +61 -0
  72. package/dist/esm/runtime/rsc/payloadRouter.mjs +12 -133
  73. package/dist/esm/runtime/rsc/payloadRoutes.mjs +50 -0
  74. package/dist/esm/runtime/rsc/payloadStorage.mjs +14 -0
  75. package/dist/esm/runtime/rsc/server.mjs +17 -21
  76. package/dist/esm/runtime/rsc/shared.mjs +21 -0
  77. package/dist/esm/runtime/rsc/treeGetter.mjs +34 -0
  78. package/dist/esm/runtime/ssrManagedTags.mjs +53 -0
  79. package/dist/esm/runtime/ssrPreload.mjs +51 -0
  80. package/dist/esm/runtime/ssrTypes.mjs +0 -0
  81. package/dist/esm/runtime/submitAction.mjs +120 -0
  82. package/dist/esm/runtime/utils.mjs +47 -75
  83. package/dist/esm-node/cli/artifacts.mjs +104 -0
  84. package/dist/esm-node/cli/index.mjs +4 -101
  85. package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +76 -0
  86. package/dist/esm-node/cli/tanstackTypes/shared.mjs +69 -0
  87. package/dist/esm-node/cli/tanstackTypes/source.mjs +204 -0
  88. package/dist/esm-node/cli/tanstackTypes.mjs +2 -339
  89. package/dist/esm-node/runtime/blockingSubscribe.mjs +20 -0
  90. package/dist/esm-node/runtime/clientBasepath.mjs +11 -0
  91. package/dist/esm-node/runtime/clientHydration.mjs +71 -0
  92. package/dist/esm-node/runtime/dataMutation.mjs +2 -167
  93. package/dist/esm-node/runtime/formData.mjs +64 -0
  94. package/dist/esm-node/runtime/loaderBridge.mjs +4 -1
  95. package/dist/esm-node/runtime/plugin.mjs +14 -118
  96. package/dist/esm-node/runtime/plugin.node.mjs +33 -114
  97. package/dist/esm-node/runtime/pluginShared.mjs +41 -0
  98. package/dist/esm-node/runtime/routeTree/components.mjs +56 -0
  99. package/dist/esm-node/runtime/routeTree/construction.mjs +112 -0
  100. package/dist/esm-node/runtime/routeTree/index.mjs +3 -0
  101. package/dist/esm-node/runtime/routeTree/loaders.mjs +118 -0
  102. package/dist/esm-node/runtime/routeTree/paths.mjs +17 -0
  103. package/dist/esm-node/runtime/routeTree/staticData.mjs +23 -0
  104. package/dist/esm-node/runtime/routeTree/tanstackRoutes.mjs +14 -0
  105. package/dist/esm-node/runtime/routeTree/types.mjs +1 -0
  106. package/dist/esm-node/runtime/routeTree.mjs +1 -318
  107. package/dist/esm-node/runtime/rsc/CompositeComponent.mjs +18 -13
  108. package/dist/esm-node/runtime/rsc/RscNodeRenderer.mjs +4 -15
  109. package/dist/esm-node/runtime/rsc/client.mjs +6 -26
  110. package/dist/esm-node/runtime/rsc/flightSerialization.mjs +89 -0
  111. package/dist/esm-node/runtime/rsc/payloadErrors.mjs +50 -0
  112. package/dist/esm-node/runtime/rsc/payloadFetch.mjs +62 -0
  113. package/dist/esm-node/runtime/rsc/payloadRouter.mjs +12 -133
  114. package/dist/esm-node/runtime/rsc/payloadRoutes.mjs +51 -0
  115. package/dist/esm-node/runtime/rsc/payloadStorage.mjs +15 -0
  116. package/dist/esm-node/runtime/rsc/server.mjs +17 -21
  117. package/dist/esm-node/runtime/rsc/shared.mjs +22 -0
  118. package/dist/esm-node/runtime/rsc/treeGetter.mjs +35 -0
  119. package/dist/esm-node/runtime/ssrManagedTags.mjs +54 -0
  120. package/dist/esm-node/runtime/ssrPreload.mjs +52 -0
  121. package/dist/esm-node/runtime/ssrTypes.mjs +1 -0
  122. package/dist/esm-node/runtime/submitAction.mjs +121 -0
  123. package/dist/esm-node/runtime/utils.mjs +47 -75
  124. package/dist/types/cli/artifacts.d.ts +33 -0
  125. package/dist/types/cli/index.d.ts +2 -33
  126. package/dist/types/cli/tanstackTypes/canonicalRoutes.d.ts +22 -0
  127. package/dist/types/cli/tanstackTypes/shared.d.ts +20 -0
  128. package/dist/types/cli/tanstackTypes/source.d.ts +10 -0
  129. package/dist/types/cli/tanstackTypes.d.ts +3 -31
  130. package/dist/types/runtime/blockingSubscribe.d.ts +3 -0
  131. package/dist/types/runtime/clientBasepath.d.ts +1 -0
  132. package/dist/types/runtime/clientHydration.d.ts +19 -0
  133. package/dist/types/runtime/dataMutation.d.ts +3 -12
  134. package/dist/types/runtime/formData.d.ts +23 -0
  135. package/dist/types/runtime/plugin.d.ts +1 -1
  136. package/dist/types/runtime/plugin.node.d.ts +1 -1
  137. package/dist/types/runtime/{pluginCore.d.ts → pluginShared.d.ts} +14 -0
  138. package/dist/types/runtime/routeTree/components.d.ts +7 -0
  139. package/dist/types/runtime/routeTree/construction.d.ts +5 -0
  140. package/dist/types/runtime/routeTree/index.d.ts +2 -0
  141. package/dist/types/runtime/routeTree/loaders.d.ts +4 -0
  142. package/dist/types/runtime/routeTree/paths.d.ts +3 -0
  143. package/dist/types/runtime/routeTree/staticData.d.ts +19 -0
  144. package/dist/types/runtime/routeTree/tanstackRoutes.d.ts +4 -0
  145. package/dist/types/runtime/routeTree/types.d.ts +75 -0
  146. package/dist/types/runtime/routeTree.d.ts +1 -9
  147. package/dist/types/runtime/rsc/CompositeComponent.d.ts +1 -2
  148. package/dist/types/runtime/rsc/RscNodeRenderer.d.ts +2 -3
  149. package/dist/types/runtime/rsc/client.d.ts +2 -0
  150. package/dist/types/runtime/rsc/flightSerialization.d.ts +3 -0
  151. package/dist/types/runtime/rsc/payloadErrors.d.ts +6 -0
  152. package/dist/types/runtime/rsc/payloadFetch.d.ts +10 -0
  153. package/dist/types/runtime/rsc/payloadRouter.d.ts +2 -10
  154. package/dist/types/runtime/rsc/payloadRoutes.d.ts +43 -0
  155. package/dist/types/runtime/rsc/payloadStorage.d.ts +5 -0
  156. package/dist/types/runtime/rsc/shared.d.ts +19 -0
  157. package/dist/types/runtime/rsc/treeGetter.d.ts +1 -0
  158. package/dist/types/runtime/ssrManagedTags.d.ts +7 -0
  159. package/dist/types/runtime/ssrPreload.d.ts +2 -0
  160. package/dist/types/runtime/ssrTypes.d.ts +43 -0
  161. package/dist/types/runtime/state.d.ts +1 -1
  162. package/dist/types/runtime/submitAction.d.ts +14 -0
  163. package/dist/types/runtime/types.d.ts +3 -46
  164. package/dist/types/runtime/utils.d.ts +1 -22
  165. package/package.json +30 -9
  166. package/rstest.config.mts +1 -0
  167. package/src/cli/artifacts.ts +255 -0
  168. package/src/cli/index.ts +6 -246
  169. package/src/cli/tanstackTypes/canonicalRoutes.ts +157 -0
  170. package/src/cli/tanstackTypes/shared.ts +119 -0
  171. package/src/cli/tanstackTypes/source.ts +405 -0
  172. package/src/cli/tanstackTypes.ts +4 -647
  173. package/src/runtime/blockingSubscribe.ts +48 -0
  174. package/src/runtime/clientBasepath.ts +12 -0
  175. package/src/runtime/clientHydration.ts +151 -0
  176. package/src/runtime/dataMutation.tsx +9 -306
  177. package/src/runtime/formData.ts +153 -0
  178. package/src/runtime/loaderBridge.ts +8 -3
  179. package/src/runtime/plugin.node.tsx +60 -284
  180. package/src/runtime/plugin.tsx +21 -257
  181. package/src/runtime/pluginShared.ts +112 -0
  182. package/src/runtime/routeTree/components.ts +130 -0
  183. package/src/runtime/routeTree/construction.ts +238 -0
  184. package/src/runtime/routeTree/index.ts +7 -0
  185. package/src/runtime/routeTree/loaders.ts +232 -0
  186. package/src/runtime/routeTree/paths.ts +36 -0
  187. package/src/runtime/routeTree/staticData.ts +66 -0
  188. package/src/runtime/routeTree/tanstackRoutes.ts +36 -0
  189. package/src/runtime/routeTree/types.ts +87 -0
  190. package/src/runtime/routeTree.ts +5 -750
  191. package/src/runtime/rsc/CompositeComponent.tsx +29 -20
  192. package/src/runtime/rsc/RscNodeRenderer.tsx +9 -27
  193. package/src/runtime/rsc/client.tsx +12 -40
  194. package/src/runtime/rsc/flightSerialization.ts +158 -0
  195. package/src/runtime/rsc/payloadErrors.ts +73 -0
  196. package/src/runtime/rsc/payloadFetch.ts +97 -0
  197. package/src/runtime/rsc/payloadRouter.ts +29 -234
  198. package/src/runtime/rsc/payloadRoutes.ts +122 -0
  199. package/src/runtime/rsc/payloadStorage.ts +35 -0
  200. package/src/runtime/rsc/server.tsx +20 -29
  201. package/src/runtime/rsc/shared.ts +70 -0
  202. package/src/runtime/rsc/treeGetter.tsx +52 -0
  203. package/src/runtime/ssrManagedTags.ts +107 -0
  204. package/src/runtime/ssrPreload.ts +125 -0
  205. package/src/runtime/ssrTypes.ts +51 -0
  206. package/src/runtime/state.ts +4 -2
  207. package/src/runtime/submitAction.ts +194 -0
  208. package/src/runtime/types.ts +14 -51
  209. package/src/runtime/utils.tsx +72 -108
  210. package/src/types/react-server-dom-rspack.d.ts +56 -0
  211. package/tests/castRatchet.test.ts +85 -0
  212. package/tests/router/dataMutation.test.tsx +59 -0
  213. package/tests/router/fixtures/router-gen.golden.txt +72 -0
  214. package/tests/router/loaderBridge.test.ts +21 -0
  215. package/tests/router/routeTree.test.ts +51 -11
  216. package/tests/router/rsc.test.tsx +189 -0
  217. package/tests/router/ssrPreload.test.ts +30 -0
  218. package/tests/router/tanstackTypes.test.ts +254 -1
  219. package/dist/esm/runtime/pluginCore.mjs +0 -14
  220. package/dist/esm-node/runtime/pluginCore.mjs +0 -15
  221. package/src/runtime/pluginCore.ts +0 -56
@@ -2,6 +2,10 @@ import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from '@modern-js/runtim
2
2
  import type React from 'react';
3
3
  import { isValidElement } from 'react';
4
4
  import { createRscProxy } from '../../src/runtime/rsc/createRscProxy';
5
+ import {
6
+ reviveTanstackRscFlightValues,
7
+ serializeTanstackRscFlightValues,
8
+ } from '../../src/runtime/rsc/flightSerialization';
5
9
  import {
6
10
  __setTanstackRscPayloadDecoderForTests,
7
11
  createTanstackRscServerPayload,
@@ -65,6 +69,54 @@ describe('tanstack rsc runtime helpers', () => {
65
69
  ]);
66
70
  });
67
71
 
72
+ test('revives shared structures that the Flight serializer preserves', async () => {
73
+ const shared = { label: 'shared' };
74
+ const serialized = serializeTanstackRscFlightValues({
75
+ left: shared,
76
+ right: shared,
77
+ }) as { left: unknown; right: unknown };
78
+
79
+ const revived = (await reviveTanstackRscFlightValues(serialized)) as {
80
+ left: unknown;
81
+ right: unknown;
82
+ };
83
+
84
+ expect(serialized.left).toBe(serialized.right);
85
+ expect(revived.left).toBe(revived.right);
86
+ });
87
+
88
+ test('serializes TanStack RSC flight values nested in map and set containers', () => {
89
+ const stream: ServerComponentStream = {
90
+ createReplayStream: () => new ReadableStream<Uint8Array>(),
91
+ };
92
+ const proxy = createRscProxy(() => null, {
93
+ renderable: true,
94
+ stream,
95
+ }) as React.ReactElement & Record<PropertyKey, unknown>;
96
+
97
+ const serialized = serializeTanstackRscFlightValues({
98
+ map: new Map([['slot', proxy]]),
99
+ set: new Set([proxy]),
100
+ }) as {
101
+ map: Map<string, Record<string, unknown>>;
102
+ set: Set<Record<string, unknown>>;
103
+ };
104
+
105
+ const mapValue = serialized.map.get('slot');
106
+ const [setValue] = serialized.set;
107
+
108
+ expect(mapValue).not.toBe(proxy);
109
+ expect(setValue).not.toBe(proxy);
110
+ expect(mapValue).toMatchObject({
111
+ __modernTanstackRsc: true,
112
+ kind: 'renderable',
113
+ });
114
+ expect(setValue).toMatchObject({
115
+ __modernTanstackRsc: true,
116
+ kind: 'renderable',
117
+ });
118
+ });
119
+
68
120
  test('renderable RSC proxies preserve React element behavior and metadata', () => {
69
121
  const stream: ServerComponentStream = {
70
122
  createReplayStream: () => new ReadableStream<Uint8Array>(),
@@ -219,6 +271,36 @@ describe('tanstack rsc runtime helpers', () => {
219
271
  expect(JSON.stringify(payload.errors)).not.toContain('stack secret');
220
272
  });
221
273
 
274
+ test('redacts production TanStack RSC non-Error server payload errors', () => {
275
+ const payload = withNodeEnv('production', () =>
276
+ createTanstackRscServerPayload({
277
+ state: {
278
+ location: { href: '/plain' },
279
+ matches: [
280
+ {
281
+ error: { message: 'plain secret', token: 'token secret' },
282
+ params: {},
283
+ pathname: '/plain',
284
+ pathnameBase: '/plain',
285
+ route: { id: '/plain' },
286
+ routeId: '/plain',
287
+ },
288
+ ],
289
+ },
290
+ }),
291
+ );
292
+
293
+ expect(payload.errors).toMatchObject({
294
+ '/plain': {
295
+ message: 'Unexpected Server Error',
296
+ stack: undefined,
297
+ __type: 'Error',
298
+ },
299
+ });
300
+ expect(JSON.stringify(payload.errors)).not.toContain('plain secret');
301
+ expect(JSON.stringify(payload.errors)).not.toContain('token secret');
302
+ });
303
+
222
304
  test('converts TanStack RSC redirects to Modern RSC navigation headers', () => {
223
305
  const response = handleTanstackRscRedirect(
224
306
  new Headers({ Location: '/base/login' }),
@@ -232,6 +314,54 @@ describe('tanstack rsc runtime helpers', () => {
232
314
  expect(response.headers.get('X-Modernjs-BaseUrl')).toBe('/base');
233
315
  });
234
316
 
317
+ test('preserves TanStack RSC redirect paths that do not start with the basename', () => {
318
+ const response = handleTanstackRscRedirect(
319
+ new Headers({ Location: '/shop/base/login' }),
320
+ '/base',
321
+ 302,
322
+ );
323
+
324
+ expect(response.headers.get('X-Modernjs-Redirect')).toBe(
325
+ '/shop/base/login',
326
+ );
327
+ });
328
+
329
+ test('preserves RSC redirect response status and headers in TanStack redirects', async () => {
330
+ rstest.stubGlobal(
331
+ 'fetch',
332
+ rstest.fn(() =>
333
+ Promise.resolve(
334
+ new Response(null, {
335
+ headers: {
336
+ 'X-Modernjs-Redirect': '/login',
337
+ 'X-Trace': 'preserved',
338
+ },
339
+ status: 308,
340
+ }),
341
+ ),
342
+ ),
343
+ );
344
+
345
+ const thrown = (await loadTanstackRscRouteData({
346
+ loadClientData: async () => ({ fallback: true }),
347
+ request: new Request('http://localhost/products'),
348
+ routeId: '/products',
349
+ }).then(
350
+ () => {
351
+ throw new Error('expected redirect');
352
+ },
353
+ err => err,
354
+ )) as Response & {
355
+ options?: { statusCode?: number; to?: string };
356
+ };
357
+
358
+ expect(thrown).toBeInstanceOf(Response);
359
+ expect(thrown.status).toBe(308);
360
+ expect(thrown.headers.get('X-Trace')).toBe('preserved');
361
+ expect(thrown.options?.statusCode).toBe(308);
362
+ expect(thrown.options?.to).toBe('/login');
363
+ });
364
+
235
365
  test('loads one RSC payload for multiple server route loaders', async () => {
236
366
  const payload = {
237
367
  type: 'render',
@@ -275,6 +405,65 @@ describe('tanstack rsc runtime helpers', () => {
275
405
  expect(decodeMock).toHaveBeenCalledTimes(1);
276
406
  });
277
407
 
408
+ test('separates RSC payload cache entries by forwarded request headers', async () => {
409
+ const payloadByUser: Record<string, unknown> = {
410
+ alice: {
411
+ type: 'render',
412
+ actionData: null,
413
+ errors: null,
414
+ loaderData: {
415
+ '/profile': { user: 'alice' },
416
+ },
417
+ location: { href: '/profile' },
418
+ routes: [{ id: '/profile', hasLoader: true }],
419
+ },
420
+ bob: {
421
+ type: 'render',
422
+ actionData: null,
423
+ errors: null,
424
+ loaderData: {
425
+ '/profile': { user: 'bob' },
426
+ },
427
+ location: { href: '/profile' },
428
+ routes: [{ id: '/profile', hasLoader: true }],
429
+ },
430
+ };
431
+ const fetchMock = rstest.fn(
432
+ (_url: string | URL | Request, init?: RequestInit) => {
433
+ const user =
434
+ init?.headers instanceof Headers ? init.headers.get('x-user') : null;
435
+ return Promise.resolve(
436
+ new Response(JSON.stringify(payloadByUser[user || ''])),
437
+ );
438
+ },
439
+ );
440
+ rstest.stubGlobal('fetch', fetchMock);
441
+ __setTanstackRscPayloadDecoderForTests(async stream =>
442
+ JSON.parse(await new Response(stream).text()),
443
+ );
444
+
445
+ const [aliceData, bobData] = await Promise.all([
446
+ loadTanstackRscRouteData({
447
+ loadClientData: async () => ({ fallback: 'alice' }),
448
+ request: new Request('http://localhost/profile', {
449
+ headers: { 'x-user': 'alice' },
450
+ }),
451
+ routeId: '/profile',
452
+ }),
453
+ loadTanstackRscRouteData({
454
+ loadClientData: async () => ({ fallback: 'bob' }),
455
+ request: new Request('http://localhost/profile', {
456
+ headers: { 'x-user': 'bob' },
457
+ }),
458
+ routeId: '/profile',
459
+ }),
460
+ ]);
461
+
462
+ expect(aliceData).toEqual({ user: 'alice' });
463
+ expect(bobData).toEqual({ user: 'bob' });
464
+ expect(fetchMock).toHaveBeenCalledTimes(2);
465
+ });
466
+
278
467
  test('uses client loader data without requesting an RSC payload for client-loader routes', async () => {
279
468
  const fetchMock = rstest.fn();
280
469
  const loadClientData = rstest.fn(async () => ({ client: true }));
@@ -0,0 +1,30 @@
1
+ import { preloadMatchedRouteComponents } from '../../src/runtime/ssrPreload';
2
+
3
+ describe('preloadMatchedRouteComponents', () => {
4
+ test('preloads each component identity once per SSR pass', async () => {
5
+ const sharedComponent = Object.assign(() => null, {
6
+ preload: rstest.fn(),
7
+ });
8
+
9
+ await preloadMatchedRouteComponents({
10
+ routesById: {
11
+ child: {
12
+ options: {
13
+ component: sharedComponent,
14
+ },
15
+ },
16
+ root: {
17
+ options: {
18
+ component: sharedComponent,
19
+ pendingComponent: sharedComponent,
20
+ },
21
+ },
22
+ },
23
+ state: {
24
+ matches: [{ routeId: 'root' }, { routeId: 'child' }],
25
+ },
26
+ } as never);
27
+
28
+ expect(sharedComponent.preload).toHaveBeenCalledTimes(1);
29
+ });
30
+ });
@@ -1,19 +1,163 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
+ import { isRedirect } from '@tanstack/react-router';
7
+
8
+ const resolverDelays = new Map<string, number>();
9
+
10
+ rstest.mock('../../src/cli/tanstackTypes/shared', () => {
11
+ const actual = rstest.requireActual(
12
+ '../../src/cli/tanstackTypes/shared',
13
+ ) as typeof import('../../src/cli/tanstackTypes/shared');
14
+
15
+ return {
16
+ ...actual,
17
+ resolveFileNoExt: async (inputNoExtPath: string) => {
18
+ const normalized = inputNoExtPath.replaceAll('\\', '/');
19
+ const delay =
20
+ [...resolverDelays.entries()].find(([suffix]) =>
21
+ normalized.endsWith(suffix),
22
+ )?.[1] ?? 0;
23
+
24
+ if (delay > 0) {
25
+ await new Promise(resolve => setTimeout(resolve, delay));
26
+ }
27
+
28
+ return actual.resolveFileNoExt(inputNoExtPath);
29
+ },
30
+ };
31
+ });
32
+
6
33
  import {
7
34
  collectCanonicalRoutesForEntry,
8
35
  generateTanstackRouterTypesSourceForEntry,
9
36
  } from '../../src/cli/tanstackTypes';
37
+ import { throwTanstackRedirect } from '../../src/runtime/loaderBridge';
10
38
 
11
39
  const execFileAsync = promisify(execFile);
40
+ const routerGenGoldenPath = path.join(
41
+ __dirname,
42
+ 'fixtures',
43
+ 'router-gen.golden.txt',
44
+ );
45
+
46
+ type RedirectLike = {
47
+ options?: {
48
+ href?: string;
49
+ to?: string;
50
+ };
51
+ };
52
+
53
+ function catchThrown(fn: () => unknown): unknown {
54
+ try {
55
+ fn();
56
+ } catch (err) {
57
+ return err;
58
+ }
59
+ throw new Error('expected function to throw');
60
+ }
61
+
62
+ function extractRouteGenerationOrder(source: string) {
63
+ return source.split('\n').filter(line => {
64
+ return (
65
+ line.startsWith('import component_') ||
66
+ line.startsWith('const route_') ||
67
+ line.includes('component: component_') ||
68
+ line.startsWith('export const routeTree')
69
+ );
70
+ });
71
+ }
72
+
73
+ async function writeGoldenRouterFixture(srcDirectory: string) {
74
+ const files = new Map([
75
+ [
76
+ 'routes/(app)/layout.tsx',
77
+ 'export default function AppLayout() { return null; }',
78
+ ],
79
+ [
80
+ 'routes/(app)/users/(userId)/page.tsx',
81
+ 'export default function UserPage() { return null; }',
82
+ ],
83
+ [
84
+ 'routes/(app)/users/(userId)/page.data.ts',
85
+ [
86
+ 'export const loader = () => ({ userId: "42" });',
87
+ 'export const action = () => Response.json({ ok: true });',
88
+ ].join('\n'),
89
+ ],
90
+ [
91
+ 'routes/(app)/docs/splat.tsx',
92
+ 'export default function DocsSplatPage() { return null; }',
93
+ ],
94
+ [
95
+ 'routes/search.contract.ts',
96
+ [
97
+ 'export const validateSearch = (search: { tab?: string }) => ({ tab: search.tab ?? "overview" });',
98
+ 'export const loaderDeps = ({ search }: { search: { tab: string } }) => ({ tab: search.tab });',
99
+ ].join('\n'),
100
+ ],
101
+ ]);
102
+
103
+ for (const [relativePath, contents] of files) {
104
+ const filePath = path.join(srcDirectory, relativePath);
105
+ await mkdir(path.dirname(filePath), { recursive: true });
106
+ await writeFile(filePath, contents);
107
+ }
108
+ }
109
+
110
+ async function generateGoldenRouterGen(srcDirectory: string) {
111
+ await writeGoldenRouterFixture(srcDirectory);
112
+
113
+ const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
114
+ appContext: {
115
+ srcDirectory,
116
+ internalSrcAlias: '@/_',
117
+ } as any,
118
+ entryName: 'golden',
119
+ routes: [
120
+ {
121
+ type: 'nested',
122
+ id: 'layout',
123
+ isRoot: true,
124
+ children: [
125
+ {
126
+ type: 'nested',
127
+ id: '(app)/layout',
128
+ _component: '@/_/routes/(app)/layout',
129
+ children: [
130
+ {
131
+ type: 'nested',
132
+ id: '(app)/users/(userId)/page',
133
+ path: 'users/:userId',
134
+ _component: '@/_/routes/(app)/users/(userId)/page',
135
+ data: '@/_/routes/(app)/users/(userId)/page.data',
136
+ action: '@/_/routes/(app)/users/(userId)/page.data',
137
+ validateSearch: '@/_/routes/search.contract',
138
+ loaderDeps: '@/_/routes/search.contract',
139
+ },
140
+ {
141
+ type: 'nested',
142
+ id: '(app)/docs/splat/page',
143
+ path: 'docs/*',
144
+ _component: '@/_/routes/(app)/docs/splat',
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ },
150
+ ] as any,
151
+ });
152
+
153
+ return routerGenTs;
154
+ }
12
155
 
13
156
  describe('tanstack router type generation', () => {
14
157
  let tempDir: string | undefined;
15
158
 
16
159
  afterEach(async () => {
160
+ resolverDelays.clear();
17
161
  if (tempDir) {
18
162
  await rm(tempDir, { recursive: true, force: true });
19
163
  tempDir = undefined;
@@ -77,6 +221,15 @@ describe('tanstack router type generation', () => {
77
221
  expect(routerGenTs).not.toContain('function modernLoaderToTanstack');
78
222
  expect(routerGenTs).not.toContain('function createRouteStaticData');
79
223
  expect(routerGenTs).not.toContain('function throwTanstackRedirect');
224
+ expect(routerGenTs).not.toContain('new URL(target)');
225
+ expect(routerGenTs).not.toContain('redirect({ to: target })');
226
+
227
+ const redirectError = catchThrown(() =>
228
+ throwTanstackRedirect('https://example.com/absolute'),
229
+ ) as RedirectLike;
230
+ expect(isRedirect(redirectError)).toBe(true);
231
+ expect(redirectError.options?.href).toBe('https://example.com/absolute');
232
+ expect(redirectError.options?.to).toBeUndefined();
80
233
  });
81
234
 
82
235
  test('emits resolvable relative component imports for routes carrying _component', async () => {
@@ -167,6 +320,106 @@ describe('tanstack router type generation', () => {
167
320
  expect(dataOnlyRoute).not.toContain('component:');
168
321
  });
169
322
 
323
+ test('keeps generated source stable when route module resolution races', async () => {
324
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
325
+ const srcDirectory = path.join(tempDir, 'src');
326
+
327
+ for (const componentFile of [
328
+ 'routes/slow-page.tsx',
329
+ 'routes/fast-page.tsx',
330
+ 'routes/shared-page.tsx',
331
+ ]) {
332
+ const componentPath = path.join(srcDirectory, componentFile);
333
+ await mkdir(path.dirname(componentPath), { recursive: true });
334
+ await writeFile(
335
+ componentPath,
336
+ 'export default function Page() { return null; }',
337
+ );
338
+ }
339
+
340
+ const routes = [
341
+ {
342
+ type: 'nested',
343
+ id: 'layout',
344
+ isRoot: true,
345
+ children: [
346
+ {
347
+ type: 'nested',
348
+ id: 'slow/page',
349
+ path: 'slow',
350
+ _component: '@/_/routes/slow-page',
351
+ },
352
+ {
353
+ type: 'nested',
354
+ id: 'fast/page',
355
+ path: 'fast',
356
+ _component: '@/_/routes/fast-page',
357
+ },
358
+ {
359
+ type: 'nested',
360
+ id: 'shared-a/page',
361
+ path: 'shared-a',
362
+ _component: '@/_/routes/shared-page',
363
+ },
364
+ {
365
+ type: 'nested',
366
+ id: 'shared-b/page',
367
+ path: 'shared-b',
368
+ _component: '@/_/routes/shared-page',
369
+ },
370
+ ],
371
+ },
372
+ ] as any;
373
+
374
+ const generateWithResolverDelays = async (
375
+ delays: Record<string, number>,
376
+ ) => {
377
+ resolverDelays.clear();
378
+ for (const [suffix, delay] of Object.entries(delays)) {
379
+ resolverDelays.set(suffix, delay);
380
+ }
381
+
382
+ const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
383
+ appContext: {
384
+ srcDirectory,
385
+ internalSrcAlias: '@/_',
386
+ } as any,
387
+ entryName: 'index',
388
+ routes,
389
+ });
390
+
391
+ return routerGenTs;
392
+ };
393
+
394
+ const sequentialOutput = await generateWithResolverDelays({});
395
+ const racedOutput = await generateWithResolverDelays({
396
+ '/routes/slow-page': 25,
397
+ });
398
+
399
+ expect(
400
+ racedOutput.match(/from "\.\.\/\.\.\/routes\/shared-page";/g),
401
+ ).toHaveLength(1);
402
+ expect(extractRouteGenerationOrder(racedOutput)).toEqual(
403
+ extractRouteGenerationOrder(sequentialOutput),
404
+ );
405
+ expect(racedOutput).toBe(sequentialOutput);
406
+ });
407
+
408
+ test('matches checked-in router.gen golden output', async () => {
409
+ tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
410
+ const srcDirectory = path.join(tempDir, 'src');
411
+ const routerGenTs = await generateGoldenRouterGen(srcDirectory);
412
+
413
+ if (process.env.UPDATE_TANSTACK_ROUTER_GOLDEN === '1') {
414
+ await mkdir(path.dirname(routerGenGoldenPath), { recursive: true });
415
+ await writeFile(routerGenGoldenPath, routerGenTs);
416
+ }
417
+
418
+ await expect(readFile(routerGenGoldenPath, 'utf8')).resolves.toBe(
419
+ routerGenTs,
420
+ );
421
+ });
422
+
170
423
  test('typechecks generated TanStack search contracts', async () => {
171
424
  tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
172
425
  const srcDirectory = path.join(tempDir, 'src');
@@ -1,14 +0,0 @@
1
- import { getGlobalLayoutApp, getGlobalRoutes } from "@modern-js/runtime/context";
2
- import { merge } from "@modern-js/runtime-utils/merge";
3
- function getMergedRouterConfig(api, userConfig) {
4
- const pluginConfig = api.getRuntimeConfig();
5
- return merge(pluginConfig.router || {}, userConfig);
6
- }
7
- function getFinalRouteConfig(mergedConfig) {
8
- return {
9
- routes: getGlobalRoutes(),
10
- globalApp: getGlobalLayoutApp(),
11
- ...mergedConfig.routesConfig
12
- };
13
- }
14
- export { getFinalRouteConfig, getMergedRouterConfig };
@@ -1,15 +0,0 @@
1
- import "node:module";
2
- import { getGlobalLayoutApp, getGlobalRoutes } from "@modern-js/runtime/context";
3
- import { merge } from "@modern-js/runtime-utils/merge";
4
- function getMergedRouterConfig(api, userConfig) {
5
- const pluginConfig = api.getRuntimeConfig();
6
- return merge(pluginConfig.router || {}, userConfig);
7
- }
8
- function getFinalRouteConfig(mergedConfig) {
9
- return {
10
- routes: getGlobalRoutes(),
11
- globalApp: getGlobalLayoutApp(),
12
- ...mergedConfig.routesConfig
13
- };
14
- }
15
- export { getFinalRouteConfig, getMergedRouterConfig };
@@ -1,56 +0,0 @@
1
- import {
2
- getGlobalLayoutApp,
3
- getGlobalRoutes,
4
- type TInternalRuntimeContext,
5
- } from '@modern-js/runtime/context';
6
- import { merge } from '@modern-js/runtime-utils/merge';
7
- import type { RouterExtendsHooks } from './hooks';
8
- import type { RouterConfig } from './types';
9
-
10
- export type TanstackRouterRuntimeConfig = {
11
- plugins?: TanstackRouterRuntimePlugin[];
12
- router?: Partial<RouterConfig>;
13
- [key: string]: unknown;
14
- };
15
-
16
- type RuntimeInterrupt = (value?: unknown) => unknown;
17
-
18
- export type TanstackRouterPluginAPI = {
19
- getRuntimeConfig: () => TanstackRouterRuntimeConfig;
20
- getHooks: () => RouterExtendsHooks;
21
- onBeforeRender: (
22
- listener: (
23
- context: TInternalRuntimeContext,
24
- interrupt: RuntimeInterrupt,
25
- ) => unknown,
26
- ) => void;
27
- wrapRoot: (listener: (App: any) => any) => void;
28
- [key: string]: any;
29
- };
30
-
31
- export type TanstackRouterRuntimePlugin = {
32
- name?: string;
33
- registryHooks?: RouterExtendsHooks;
34
- setup?: (api: TanstackRouterPluginAPI) => unknown;
35
- [key: string]: unknown;
36
- };
37
-
38
- export function getMergedRouterConfig(
39
- api: TanstackRouterPluginAPI,
40
- userConfig: Partial<RouterConfig>,
41
- ): RouterConfig {
42
- const pluginConfig = api.getRuntimeConfig() as {
43
- router?: Partial<RouterConfig>;
44
- };
45
- return merge(pluginConfig.router || {}, userConfig) as RouterConfig;
46
- }
47
-
48
- export function getFinalRouteConfig(
49
- mergedConfig: RouterConfig,
50
- ): RouterConfig['routesConfig'] {
51
- return {
52
- routes: getGlobalRoutes() as RouterConfig['routesConfig']['routes'],
53
- globalApp: getGlobalLayoutApp(),
54
- ...mergedConfig.routesConfig,
55
- };
56
- }