@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.10 → 3.2.0-ultramodern.101

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 (60) hide show
  1. package/dist/cjs/cli/index.js +12 -0
  2. package/dist/cjs/cli/routeSplitting.js +83 -0
  3. package/dist/cjs/cli/tanstackTypes.js +146 -58
  4. package/dist/cjs/runtime/hydrationBoundary.js +44 -0
  5. package/dist/cjs/runtime/index.js +321 -69
  6. package/dist/cjs/runtime/outlet.js +54 -0
  7. package/dist/cjs/runtime/plugin.js +194 -90
  8. package/dist/cjs/runtime/plugin.node.js +29 -11
  9. package/dist/cjs/runtime/plugin.worker.js +49 -0
  10. package/dist/cjs/runtime/routeTree.js +72 -12
  11. package/dist/cjs/runtime/types.js +27 -1
  12. package/dist/esm/cli/index.mjs +4 -1
  13. package/dist/esm/cli/routeSplitting.mjs +43 -0
  14. package/dist/esm/cli/tanstackTypes.mjs +146 -58
  15. package/dist/esm/runtime/hydrationBoundary.mjs +10 -0
  16. package/dist/esm/runtime/index.mjs +3 -2
  17. package/dist/esm/runtime/outlet.mjs +17 -0
  18. package/dist/esm/runtime/plugin.mjs +197 -93
  19. package/dist/esm/runtime/plugin.node.mjs +30 -12
  20. package/dist/esm/runtime/plugin.worker.mjs +1 -0
  21. package/dist/esm/runtime/routeTree.mjs +73 -13
  22. package/dist/esm/runtime/types.mjs +7 -0
  23. package/dist/esm-node/cli/index.mjs +4 -1
  24. package/dist/esm-node/cli/routeSplitting.mjs +44 -0
  25. package/dist/esm-node/cli/tanstackTypes.mjs +146 -58
  26. package/dist/esm-node/runtime/hydrationBoundary.mjs +11 -0
  27. package/dist/esm-node/runtime/index.mjs +3 -2
  28. package/dist/esm-node/runtime/outlet.mjs +18 -0
  29. package/dist/esm-node/runtime/plugin.mjs +197 -93
  30. package/dist/esm-node/runtime/plugin.node.mjs +30 -12
  31. package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
  32. package/dist/esm-node/runtime/routeTree.mjs +73 -13
  33. package/dist/esm-node/runtime/types.mjs +7 -0
  34. package/dist/types/cli/index.d.ts +4 -0
  35. package/dist/types/cli/routeSplitting.d.ts +29 -0
  36. package/dist/types/runtime/hydrationBoundary.d.ts +2 -0
  37. package/dist/types/runtime/index.d.ts +5 -2
  38. package/dist/types/runtime/outlet.d.ts +2 -0
  39. package/dist/types/runtime/plugin.d.ts +1 -1
  40. package/dist/types/runtime/plugin.node.d.ts +1 -1
  41. package/dist/types/runtime/plugin.worker.d.ts +1 -0
  42. package/dist/types/runtime/types.d.ts +7 -0
  43. package/package.json +14 -14
  44. package/src/cli/index.ts +17 -0
  45. package/src/cli/routeSplitting.ts +81 -0
  46. package/src/cli/tanstackTypes.ts +216 -67
  47. package/src/runtime/hydrationBoundary.tsx +12 -0
  48. package/src/runtime/index.tsx +107 -2
  49. package/src/runtime/outlet.tsx +42 -0
  50. package/src/runtime/plugin.node.tsx +57 -8
  51. package/src/runtime/plugin.tsx +353 -149
  52. package/src/runtime/plugin.worker.tsx +4 -0
  53. package/src/runtime/routeTree.ts +194 -23
  54. package/src/runtime/ssr-shim.d.ts +1 -3
  55. package/src/runtime/types.ts +13 -0
  56. package/tests/router/cli.test.ts +239 -0
  57. package/tests/router/fastDefaults.test.ts +25 -0
  58. package/tests/router/hydrationBoundary.test.tsx +23 -0
  59. package/tests/router/routeTree.test.ts +416 -1
  60. package/tests/router/tanstackTypes.test.ts +184 -0
@@ -24,8 +24,7 @@ import {
24
24
  useNavigate,
25
25
  useRouter,
26
26
  } from '@tanstack/react-router';
27
- import { RouterClient } from '@tanstack/react-router/ssr/client';
28
- import * as React from 'react';
27
+ import { hydrate as hydrateTanstackRouter } from '@tanstack/react-router/ssr/client';
29
28
  import { useContext, useMemo } from 'react';
30
29
  import { createModernBasepathRewrite } from './basepathRewrite';
31
30
  import {
@@ -37,13 +36,19 @@ import {
37
36
  onBeforeHydrateRouter as onBeforeHydrateRouterHook,
38
37
  type RouterExtendsHooks,
39
38
  } from './hooks';
39
+ import { wrapTanstackSsrHydrationBoundary } from './hydrationBoundary';
40
40
  import {
41
41
  applyRouterRuntimeState,
42
42
  type RouterLifecycleContext,
43
43
  } from './lifecycle';
44
+ import { withModernRouteMatchContext } from './outlet';
45
+ import { Link } from './prefetchLink';
44
46
  import { createRouteTreeFromRouteObjects } from './routeTree';
45
47
  import { getTanstackRscSerializationAdapters } from './rsc/client';
46
- import type { RouterConfig } from './types';
48
+ import {
49
+ getModernTanstackRouterFastDefaults,
50
+ type RouterConfig,
51
+ } from './types';
47
52
  import { createRouteObjectsFromConfig, urlJoin } from './utils';
48
53
 
49
54
  const BLOCKING_SUBSCRIBE_SYMBOL = Symbol.for(
@@ -52,7 +57,6 @@ const BLOCKING_SUBSCRIBE_SYMBOL = Symbol.for(
52
57
  const BLOCKING_STATE_SYMBOL = Symbol.for(
53
58
  '@modern-js/plugin-tanstack:blocking-state',
54
59
  );
55
-
56
60
  type TanstackRouterWithSubscribe = {
57
61
  [BLOCKING_STATE_SYMBOL]?: () => boolean;
58
62
  [BLOCKING_SUBSCRIBE_SYMBOL]?: boolean;
@@ -66,6 +70,30 @@ type WindowWithTanstackSsr = Window & {
66
70
  $_TSR?: unknown;
67
71
  };
68
72
 
73
+ type RouteComponentPreloadable = {
74
+ load?: () => Promise<unknown>;
75
+ preload?: () => Promise<unknown>;
76
+ };
77
+
78
+ type ModernRouteModule = {
79
+ Component?: unknown;
80
+ default?: unknown;
81
+ };
82
+
83
+ type RouterWithPreloadableRoutes = AnyRouter & {
84
+ routesById?: Record<
85
+ string,
86
+ {
87
+ options?: {
88
+ component?: unknown;
89
+ staticData?: {
90
+ modernRouteId?: string;
91
+ };
92
+ };
93
+ }
94
+ >;
95
+ };
96
+
69
97
  type TanstackRouterRuntimeConfig = {
70
98
  plugins?: TanstackRouterRuntimePlugin[];
71
99
  router?: Partial<RouterConfig>;
@@ -129,6 +157,144 @@ function wrapRouterSubscribeWithBlockState(
129
157
  target[BLOCKING_SUBSCRIBE_SYMBOL] = true;
130
158
  }
131
159
 
160
+ type RouterHydrationRecord = {
161
+ error?: unknown;
162
+ promise: Promise<unknown>;
163
+ status: 'pending' | 'fulfilled' | 'rejected';
164
+ };
165
+
166
+ const routerHydrationRecords = new WeakMap<AnyRouter, RouterHydrationRecord>();
167
+ const routeModulesKey = '_routeModules';
168
+
169
+ function pickRouteModuleComponent(
170
+ routeModule: unknown,
171
+ seen: Set<unknown> = new Set(),
172
+ ): unknown {
173
+ if (
174
+ typeof routeModule === 'function' ||
175
+ (routeModule &&
176
+ typeof routeModule === 'object' &&
177
+ '$$typeof' in routeModule)
178
+ ) {
179
+ return routeModule;
180
+ }
181
+
182
+ if (!routeModule || typeof routeModule !== 'object') {
183
+ return undefined;
184
+ }
185
+ if (seen.has(routeModule)) {
186
+ return undefined;
187
+ }
188
+ seen.add(routeModule);
189
+
190
+ const module = routeModule as ModernRouteModule;
191
+ for (const candidate of [module.default, module.Component]) {
192
+ const component = pickRouteModuleComponent(candidate, seen);
193
+ if (component) {
194
+ return component;
195
+ }
196
+ }
197
+
198
+ return undefined;
199
+ }
200
+
201
+ function getCachedRouteModule(routeId: string) {
202
+ if (typeof window === 'undefined') {
203
+ return undefined;
204
+ }
205
+
206
+ return (window as unknown as Record<string, Record<string, unknown>>)[
207
+ routeModulesKey
208
+ ]?.[routeId];
209
+ }
210
+
211
+ async function preloadHydratedRouteComponents(router: AnyRouter) {
212
+ const preloadableRouter = router as RouterWithPreloadableRoutes;
213
+ const routesById = preloadableRouter.routesById || {};
214
+ const matches = preloadableRouter.stores.matches.get() as Array<{
215
+ routeId?: string;
216
+ }>;
217
+
218
+ await Promise.all(
219
+ matches.map(match => {
220
+ if (!match.routeId) {
221
+ return undefined;
222
+ }
223
+
224
+ const route = routesById[match.routeId];
225
+ const component = route?.options?.component as RouteComponentPreloadable;
226
+ const preload = component?.load || component?.preload;
227
+ if (typeof preload !== 'function') {
228
+ return undefined;
229
+ }
230
+
231
+ return Promise.resolve(preload.call(component)).then(routeModule => {
232
+ const modernRouteId = route?.options?.staticData?.modernRouteId;
233
+ const resolvedComponent = pickRouteModuleComponent(
234
+ (modernRouteId && getCachedRouteModule(modernRouteId)) || routeModule,
235
+ );
236
+ if (resolvedComponent && modernRouteId) {
237
+ route.options.component = withModernRouteMatchContext(
238
+ resolvedComponent,
239
+ modernRouteId,
240
+ );
241
+ }
242
+ });
243
+ }),
244
+ );
245
+ }
246
+
247
+ function getTanstackSsrHydrationRecord(router: AnyRouter) {
248
+ let hydrationRecord = routerHydrationRecords.get(router);
249
+ if (!hydrationRecord) {
250
+ hydrationRecord = {
251
+ promise: Promise.resolve(),
252
+ status: 'pending',
253
+ };
254
+ routerHydrationRecords.set(router, hydrationRecord);
255
+ try {
256
+ hydrationRecord.promise = hydrateTanstackRouter(router)
257
+ .then(async value => {
258
+ await preloadHydratedRouteComponents(router);
259
+ return value;
260
+ })
261
+ .then(
262
+ value => {
263
+ hydrationRecord.status = 'fulfilled';
264
+ return value;
265
+ },
266
+ error => {
267
+ hydrationRecord.status = 'rejected';
268
+ hydrationRecord.error = error;
269
+ throw error;
270
+ },
271
+ );
272
+ } catch (error) {
273
+ hydrationRecord.status = 'rejected';
274
+ hydrationRecord.error = error;
275
+ hydrationRecord.promise = Promise.reject(error);
276
+ hydrationRecord.promise.catch(() => {});
277
+ }
278
+ }
279
+ return hydrationRecord;
280
+ }
281
+
282
+ function getTanstackSsrHydrationPromise(router: AnyRouter) {
283
+ return getTanstackSsrHydrationRecord(router).promise;
284
+ }
285
+
286
+ function hasTanstackSsrHydrationRecord(router: AnyRouter) {
287
+ return routerHydrationRecords.has(router);
288
+ }
289
+
290
+ function ModernRouterClient({ router }: { router: AnyRouter }) {
291
+ const hydrationRecord = getTanstackSsrHydrationRecord(router);
292
+ if (hydrationRecord.status === 'rejected') {
293
+ throw hydrationRecord.error;
294
+ }
295
+ return <RouterProvider router={router} />;
296
+ }
297
+
132
298
  function stripSyntheticNotFoundRoute(routes: RouteObject[]): RouteObject[] {
133
299
  return routes
134
300
  .filter(route => !(route.path === '*' && !route.id && !route.loader))
@@ -157,14 +323,151 @@ export const tanstackRouterPlugin = (
157
323
  onBeforeHydrateRouter: onBeforeHydrateRouterHook,
158
324
  },
159
325
  setup: (api: TanstackRouterPluginAPI) => {
160
- api.onBeforeRender(context => {
326
+ const hooks = api.getHooks();
327
+ let cachedRouteObjects: RouteObject[] | undefined;
328
+ let cachedRouteTree: ReturnType<
329
+ typeof createRouteTreeFromRouteObjects
330
+ > | null = null;
331
+ let cachedRouter: AnyRouter | null = null;
332
+ let cachedRouterBasepath: string | null = null;
333
+
334
+ const getMergedConfig = () => {
161
335
  const pluginConfig = api.getRuntimeConfig() as {
162
336
  router?: Partial<RouterConfig>;
163
337
  };
164
- const mergedConfig = merge(
165
- pluginConfig.router || {},
166
- userConfig,
167
- ) as RouterConfig;
338
+ return merge(pluginConfig.router || {}, userConfig) as RouterConfig;
339
+ };
340
+
341
+ const getRouteObjects = () => {
342
+ if (typeof cachedRouteObjects !== 'undefined') {
343
+ return cachedRouteObjects;
344
+ }
345
+
346
+ const mergedConfig = getMergedConfig();
347
+ const { routesConfig, createRoutes } = mergedConfig;
348
+ const finalRouteConfig = {
349
+ routes: getGlobalRoutes(),
350
+ globalApp: getGlobalLayoutApp(),
351
+ ...routesConfig,
352
+ };
353
+
354
+ const routeObjects = createRoutes
355
+ ? createRoutes()
356
+ : createRouteObjectsFromConfig({
357
+ routesConfig: finalRouteConfig,
358
+ }) || [];
359
+
360
+ const normalizedRouteObjects = createRoutes
361
+ ? routeObjects
362
+ : stripSyntheticNotFoundRoute(routeObjects);
363
+
364
+ cachedRouteObjects = hooks.modifyRoutes.call(
365
+ normalizedRouteObjects,
366
+ ) as RouteObject[];
367
+ return cachedRouteObjects;
368
+ };
369
+
370
+ const getRouteTree = () => {
371
+ if (cachedRouteTree) {
372
+ return cachedRouteTree;
373
+ }
374
+
375
+ const routeObjects = getRouteObjects();
376
+ if (!routeObjects.length) {
377
+ return null;
378
+ }
379
+
380
+ cachedRouteTree = createRouteTreeFromRouteObjects(routeObjects, {
381
+ rscPayloadRouter: getGlobalEnableRsc(),
382
+ });
383
+ return cachedRouteTree;
384
+ };
385
+
386
+ const selectBasePath = (pathname: string) => {
387
+ const { serverBase = [] } = getMergedConfig();
388
+ const match = serverBase.find(baseUrl =>
389
+ isSegmentPrefix(pathname, baseUrl),
390
+ );
391
+ return match || '/';
392
+ };
393
+
394
+ const getClientBasename = (runtimeContext: TInternalRuntimeContext) => {
395
+ const { basename = '' } = getMergedConfig();
396
+ const baseUrl = selectBasePath(location.pathname).replace(/^\/*/, '/');
397
+ return baseUrl === '/'
398
+ ? urlJoin(
399
+ baseUrl,
400
+ runtimeContext._internalRouterBaseName || basename || '',
401
+ )
402
+ : baseUrl;
403
+ };
404
+
405
+ const getRouter = (
406
+ runtimeContext: TInternalRuntimeContext,
407
+ _basename: string,
408
+ ) => {
409
+ const routeTree = getRouteTree();
410
+ if (!routeTree) {
411
+ return null;
412
+ }
413
+
414
+ const lifecycleContext: RouterLifecycleContext = {
415
+ framework: 'tanstack',
416
+ phase: 'client-create',
417
+ routes: getRouteObjects(),
418
+ runtimeContext,
419
+ basename: _basename,
420
+ };
421
+ hooks.onBeforeCreateRouter.call(lifecycleContext);
422
+
423
+ if (cachedRouter && cachedRouterBasepath === _basename) {
424
+ wrapRouterSubscribeWithBlockState(
425
+ cachedRouter,
426
+ runtimeContext.unstable_getBlockNavState,
427
+ );
428
+ hooks.onAfterCreateRouter.call({
429
+ ...lifecycleContext,
430
+ router: cachedRouter,
431
+ runtimeContext,
432
+ });
433
+ return cachedRouter;
434
+ }
435
+
436
+ const mergedConfig = getMergedConfig();
437
+ const { supportHtml5History = true } = mergedConfig;
438
+ const history = supportHtml5History
439
+ ? createBrowserHistory()
440
+ : createHashHistory();
441
+ const rewrite = createModernBasepathRewrite(_basename);
442
+ const serializationAdapters = getGlobalEnableRsc()
443
+ ? getTanstackRscSerializationAdapters()
444
+ : undefined;
445
+
446
+ cachedRouter = createRouter({
447
+ ...getModernTanstackRouterFastDefaults(mergedConfig),
448
+ routeTree,
449
+ basepath: '/',
450
+ rewrite,
451
+ history,
452
+ context: {},
453
+ ...(serializationAdapters ? { serializationAdapters } : {}),
454
+ });
455
+ cachedRouterBasepath = _basename;
456
+ wrapRouterSubscribeWithBlockState(
457
+ cachedRouter,
458
+ runtimeContext.unstable_getBlockNavState,
459
+ );
460
+ hooks.onAfterCreateRouter.call({
461
+ ...lifecycleContext,
462
+ router: cachedRouter,
463
+ runtimeContext,
464
+ });
465
+
466
+ return cachedRouter;
467
+ };
468
+
469
+ api.onBeforeRender(async context => {
470
+ const mergedConfig = getMergedConfig();
168
471
  if (
169
472
  typeof window !== 'undefined' &&
170
473
  (window as { _SSR_DATA?: unknown })._SSR_DATA &&
@@ -184,104 +487,44 @@ export const tanstackRouterPlugin = (
184
487
  }
185
488
 
186
489
  context.router = {
490
+ Link,
187
491
  useMatches,
188
492
  useLocation,
189
493
  useNavigate,
190
494
  useRouter,
191
495
  };
192
- });
193
496
 
194
- api.wrapRoot(App => {
195
- const mergedConfig = merge(
196
- api.getRuntimeConfig().router || {},
197
- userConfig,
198
- ) as RouterConfig;
199
-
200
- const {
201
- serverBase = [],
202
- supportHtml5History = true,
203
- basename = '',
204
- routesConfig,
205
- createRoutes,
206
- } = mergedConfig;
497
+ const hasSSRBootstrap =
498
+ typeof window !== 'undefined' &&
499
+ Boolean((window as WindowWithTanstackSsr).$_TSR);
500
+ if (hasSSRBootstrap && getRouteObjects().length) {
501
+ const runtimeContext = context as TInternalRuntimeContext;
502
+ const router = getRouter(
503
+ runtimeContext,
504
+ getClientBasename(runtimeContext),
505
+ );
506
+ if (router) {
507
+ await getTanstackSsrHydrationPromise(router);
508
+ }
509
+ }
207
510
 
208
- const finalRouteConfig = {
209
- routes: getGlobalRoutes(),
210
- globalApp: getGlobalLayoutApp(),
211
- ...routesConfig,
212
- };
511
+ return;
512
+ });
213
513
 
214
- if (!finalRouteConfig.routes && !createRoutes) {
514
+ api.wrapRoot(App => {
515
+ if (!getRouteObjects().length) {
215
516
  return App;
216
517
  }
217
518
 
218
- const hooks = api.getHooks();
219
-
220
- let cachedRouteObjects: RouteObject[] | undefined;
221
-
222
- const getRouteObjects = () => {
223
- if (typeof cachedRouteObjects !== 'undefined') {
224
- return cachedRouteObjects;
225
- }
226
-
227
- const routeObjects = createRoutes
228
- ? createRoutes()
229
- : createRouteObjectsFromConfig({
230
- routesConfig: finalRouteConfig,
231
- }) || [];
232
-
233
- const normalizedRouteObjects = createRoutes
234
- ? routeObjects
235
- : stripSyntheticNotFoundRoute(routeObjects);
236
-
237
- cachedRouteObjects = hooks.modifyRoutes.call(
238
- normalizedRouteObjects,
239
- ) as RouteObject[];
240
- return cachedRouteObjects;
241
- };
242
-
243
- const selectBasePath = (pathname: string) => {
244
- const match = serverBase.find(baseUrl =>
245
- isSegmentPrefix(pathname, baseUrl),
246
- );
247
- return match || '/';
248
- };
249
-
250
- let cachedRouteTree: ReturnType<
251
- typeof createRouteTreeFromRouteObjects
252
- > | null = null;
253
- let cachedRouter: AnyRouter | null = null;
254
- let cachedRouterBasepath: string | null = null;
255
-
256
519
  const RouterWrapper = () => {
257
520
  const runtimeContext = useContext(
258
521
  InternalRuntimeContext,
259
522
  ) as TInternalRuntimeContext;
260
523
 
261
- const baseUrl = selectBasePath(location.pathname).replace(
262
- /^\/*/,
263
- '/',
264
- );
265
- const _basename =
266
- baseUrl === '/'
267
- ? urlJoin(
268
- baseUrl,
269
- runtimeContext._internalRouterBaseName || basename || '',
270
- )
271
- : baseUrl;
524
+ const _basename = getClientBasename(runtimeContext);
272
525
 
273
526
  const routeTree = useMemo(() => {
274
- if (cachedRouteTree) {
275
- return cachedRouteTree;
276
- }
277
- const routeObjects = getRouteObjects();
278
- if (!routeObjects.length) {
279
- return null;
280
- }
281
- cachedRouteTree = createRouteTreeFromRouteObjects(routeObjects, {
282
- rscPayloadRouter: getGlobalEnableRsc(),
283
- });
284
- return cachedRouteTree;
527
+ return getRouteTree();
285
528
  }, []);
286
529
 
287
530
  if (!routeTree) {
@@ -289,58 +532,11 @@ export const tanstackRouterPlugin = (
289
532
  }
290
533
 
291
534
  const router = useMemo(() => {
292
- const lifecycleContext: RouterLifecycleContext = {
293
- framework: 'tanstack',
294
- phase: 'client-create',
295
- routes: getRouteObjects(),
296
- runtimeContext,
297
- basename: _basename,
298
- };
299
- hooks.onBeforeCreateRouter.call(lifecycleContext);
300
-
301
- if (cachedRouter && cachedRouterBasepath === _basename) {
302
- wrapRouterSubscribeWithBlockState(
303
- cachedRouter,
304
- runtimeContext.unstable_getBlockNavState,
305
- );
306
- hooks.onAfterCreateRouter.call({
307
- ...lifecycleContext,
308
- router: cachedRouter,
309
- runtimeContext,
310
- });
311
- return cachedRouter;
312
- }
313
-
314
- const history = supportHtml5History
315
- ? createBrowserHistory()
316
- : createHashHistory();
317
-
318
- const rewrite = createModernBasepathRewrite(_basename);
319
- const serializationAdapters = getGlobalEnableRsc()
320
- ? getTanstackRscSerializationAdapters()
321
- : undefined;
322
-
323
- cachedRouter = createRouter({
324
- routeTree,
325
- basepath: '/',
326
- rewrite,
327
- history,
328
- context: {},
329
- ...(serializationAdapters ? { serializationAdapters } : {}),
330
- });
331
- cachedRouterBasepath = _basename;
332
- wrapRouterSubscribeWithBlockState(
333
- cachedRouter,
334
- runtimeContext.unstable_getBlockNavState,
335
- );
336
- hooks.onAfterCreateRouter.call({
337
- ...lifecycleContext,
338
- router: cachedRouter,
339
- runtimeContext,
340
- });
341
-
342
- return cachedRouter;
343
- }, [_basename, routeTree, supportHtml5History, runtimeContext]);
535
+ return getRouter(runtimeContext, _basename);
536
+ }, [_basename, routeTree, runtimeContext]);
537
+ if (!router) {
538
+ return App ? <App /> : null;
539
+ }
344
540
  const runtimeState = applyRouterRuntimeState(runtimeContext, {
345
541
  framework: 'tanstack',
346
542
  basename: _basename,
@@ -357,8 +553,10 @@ export const tanstackRouterPlugin = (
357
553
 
358
554
  const hasSSRBootstrap =
359
555
  typeof window !== 'undefined' &&
360
- Boolean((window as WindowWithTanstackSsr).$_TSR);
361
- if (hasSSRBootstrap) {
556
+ (Boolean((window as WindowWithTanstackSsr).$_TSR) ||
557
+ hasTanstackSsrHydrationRecord(router));
558
+ const needsRouterClient = hasSSRBootstrap;
559
+ if (needsRouterClient) {
362
560
  hooks.onBeforeHydrateRouter.call({
363
561
  ...lifecycleContext,
364
562
  phase: 'hydrate',
@@ -367,14 +565,16 @@ export const tanstackRouterPlugin = (
367
565
  });
368
566
  }
369
567
 
370
- const RouterContent = hasSSRBootstrap ? (
371
- <React.Suspense fallback={null}>
372
- <RouterClient router={router} />
373
- </React.Suspense>
568
+ const RouterContent = needsRouterClient ? (
569
+ <ModernRouterClient router={router} />
374
570
  ) : (
375
571
  <RouterProvider router={router} />
376
572
  );
377
- if (hasSSRBootstrap) {
573
+ const HydratableRouterContent = wrapTanstackSsrHydrationBoundary(
574
+ RouterContent,
575
+ hasSSRBootstrap,
576
+ );
577
+ if (needsRouterClient) {
378
578
  hooks.onAfterHydrateRouter.call({
379
579
  ...lifecycleContext,
380
580
  phase: 'hydrate',
@@ -383,7 +583,11 @@ export const tanstackRouterPlugin = (
383
583
  });
384
584
  }
385
585
 
386
- return App ? <App>{RouterContent}</App> : RouterContent;
586
+ return App ? (
587
+ <App>{HydratableRouterContent}</App>
588
+ ) : (
589
+ HydratableRouterContent
590
+ );
387
591
  };
388
592
 
389
593
  return RouterWrapper;
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ tanstackRouterPlugin,
4
+ } from './plugin.node';