@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.9 → 3.2.0-ultramodern.91

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 (48) 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/index.js +38 -6
  5. package/dist/cjs/runtime/plugin.js +6 -5
  6. package/dist/cjs/runtime/plugin.node.js +27 -10
  7. package/dist/cjs/runtime/plugin.worker.js +49 -0
  8. package/dist/cjs/runtime/routeTree.js +55 -4
  9. package/dist/cjs/runtime/types.js +27 -1
  10. package/dist/esm/cli/index.mjs +4 -1
  11. package/dist/esm/cli/routeSplitting.mjs +43 -0
  12. package/dist/esm/cli/tanstackTypes.mjs +146 -58
  13. package/dist/esm/runtime/index.mjs +2 -1
  14. package/dist/esm/runtime/plugin.mjs +10 -9
  15. package/dist/esm/runtime/plugin.node.mjs +28 -11
  16. package/dist/esm/runtime/plugin.worker.mjs +1 -0
  17. package/dist/esm/runtime/routeTree.mjs +55 -4
  18. package/dist/esm/runtime/types.mjs +7 -0
  19. package/dist/esm-node/cli/index.mjs +4 -1
  20. package/dist/esm-node/cli/routeSplitting.mjs +44 -0
  21. package/dist/esm-node/cli/tanstackTypes.mjs +146 -58
  22. package/dist/esm-node/runtime/index.mjs +2 -1
  23. package/dist/esm-node/runtime/plugin.mjs +10 -9
  24. package/dist/esm-node/runtime/plugin.node.mjs +28 -11
  25. package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
  26. package/dist/esm-node/runtime/routeTree.mjs +55 -4
  27. package/dist/esm-node/runtime/types.mjs +7 -0
  28. package/dist/types/cli/index.d.ts +4 -0
  29. package/dist/types/cli/routeSplitting.d.ts +29 -0
  30. package/dist/types/runtime/index.d.ts +3 -1
  31. package/dist/types/runtime/plugin.d.ts +1 -1
  32. package/dist/types/runtime/plugin.node.d.ts +1 -1
  33. package/dist/types/runtime/plugin.worker.d.ts +1 -0
  34. package/dist/types/runtime/types.d.ts +7 -0
  35. package/package.json +14 -14
  36. package/src/cli/index.ts +17 -0
  37. package/src/cli/routeSplitting.ts +81 -0
  38. package/src/cli/tanstackTypes.ts +216 -67
  39. package/src/runtime/index.tsx +13 -1
  40. package/src/runtime/plugin.node.tsx +54 -7
  41. package/src/runtime/plugin.tsx +8 -5
  42. package/src/runtime/plugin.worker.tsx +4 -0
  43. package/src/runtime/routeTree.ts +125 -8
  44. package/src/runtime/types.ts +13 -0
  45. package/tests/router/cli.test.ts +239 -0
  46. package/tests/router/fastDefaults.test.ts +25 -0
  47. package/tests/router/routeTree.test.ts +193 -1
  48. package/tests/router/tanstackTypes.test.ts +184 -0
@@ -86,6 +86,17 @@ function pickModernLoaderModule(route: NestedRouteForCli | PageRoute) {
86
86
  return { loaderPath, inline };
87
87
  }
88
88
 
89
+ function pickRouteSearchContractModules(route: NestedRouteForCli | PageRoute) {
90
+ const validateSearchPath = (route as any).validateSearch;
91
+ const loaderDepsPath = (route as any).loaderDeps;
92
+
93
+ return {
94
+ validateSearchPath:
95
+ typeof validateSearchPath === 'string' ? validateSearchPath : null,
96
+ loaderDepsPath: typeof loaderDepsPath === 'string' ? loaderDepsPath : null,
97
+ };
98
+ }
99
+
89
100
  function isPathlessLayout(route: NestedRouteForCli | PageRoute) {
90
101
  return (
91
102
  (route as any).type === 'nested' &&
@@ -182,9 +193,29 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
182
193
  const statements: string[] = [];
183
194
 
184
195
  const loaderImportMap = new Map<string, string>();
196
+ const searchContractImportMap = new Map<string, string>();
197
+ const usedRouteVarNames = new Set<string>();
185
198
  let loaderIndex = 0;
199
+ let validateSearchIndex = 0;
200
+ let loaderDepsIndex = 0;
186
201
  let routeIndex = 0;
187
202
 
203
+ const resolveRouteModuleNoExt = async (aliasedNoExtPath: string) => {
204
+ const prefix = `${appContext.internalSrcAlias}/`;
205
+ let absNoExt: string;
206
+ if (aliasedNoExtPath.startsWith(prefix)) {
207
+ const rel = aliasedNoExtPath.slice(prefix.length);
208
+ absNoExt = path.join(appContext.srcDirectory, rel);
209
+ } else if (path.isAbsolute(aliasedNoExtPath)) {
210
+ absNoExt = aliasedNoExtPath;
211
+ } else {
212
+ // Unknown format; treat as already relative to src.
213
+ absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
214
+ }
215
+
216
+ return resolveFileNoExt(absNoExt);
217
+ };
218
+
188
219
  const getImportNamesForLoader = async (
189
220
  aliasedNoExtPath: string,
190
221
  inline: boolean,
@@ -201,19 +232,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
201
232
  };
202
233
  }
203
234
 
204
- const prefix = `${appContext.internalSrcAlias}/`;
205
- let absNoExt: string;
206
- if (aliasedNoExtPath.startsWith(prefix)) {
207
- const rel = aliasedNoExtPath.slice(prefix.length);
208
- absNoExt = path.join(appContext.srcDirectory, rel);
209
- } else if (path.isAbsolute(aliasedNoExtPath)) {
210
- absNoExt = aliasedNoExtPath;
211
- } else {
212
- // Unknown format; treat as already relative to src.
213
- absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
214
- }
215
-
216
- const resolvedNoExt = await resolveFileNoExt(absNoExt);
235
+ const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
217
236
  if (!resolvedNoExt) {
218
237
  return null;
219
238
  }
@@ -242,10 +261,49 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
242
261
  return { loaderName: importName, actionName };
243
262
  };
244
263
 
264
+ const getImportNameForSearchContract = async (
265
+ aliasedNoExtPath: string,
266
+ exportName: 'validateSearch' | 'loaderDeps',
267
+ ) => {
268
+ const key = `${exportName}:${aliasedNoExtPath}`;
269
+ const existing = searchContractImportMap.get(key);
270
+ if (existing) {
271
+ return existing;
272
+ }
273
+
274
+ const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
275
+ if (!resolvedNoExt) {
276
+ return null;
277
+ }
278
+
279
+ const relImport = normalizeRelativeImport(
280
+ path.relative(outDir, resolvedNoExt),
281
+ );
282
+ const importName =
283
+ exportName === 'validateSearch'
284
+ ? `validateSearch_${validateSearchIndex++}`
285
+ : `loaderDeps_${loaderDepsIndex++}`;
286
+ imports.push(
287
+ `import { ${exportName} as ${importName} } from ${quote(relImport)};`,
288
+ );
289
+ searchContractImportMap.set(key, importName);
290
+ return importName;
291
+ };
292
+
293
+ const reserveRouteVarName = (preferred: string) => {
294
+ let candidate = preferred;
295
+ let suffix = 1;
296
+ while (usedRouteVarNames.has(candidate)) {
297
+ candidate = `${preferred}_${suffix++}`;
298
+ }
299
+ usedRouteVarNames.add(candidate);
300
+ return candidate;
301
+ };
302
+
245
303
  const createRouteVarName = (route: NestedRouteForCli | PageRoute) => {
246
304
  const id = (route as any).id as string | undefined;
247
305
  const base = id ? makeLegalIdentifier(id) : `r_${routeIndex++}`;
248
- return `route_${base}`;
306
+ return reserveRouteVarName(`route_${base}`);
249
307
  };
250
308
 
251
309
  const buildRoute = async (opts: {
@@ -267,6 +325,19 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
267
325
  : null;
268
326
  const loaderName = loaderImports?.loaderName || null;
269
327
  const actionName = loaderImports?.actionName || null;
328
+ const searchContractInfo = pickRouteSearchContractModules(route);
329
+ const validateSearchName = searchContractInfo.validateSearchPath
330
+ ? await getImportNameForSearchContract(
331
+ searchContractInfo.validateSearchPath,
332
+ 'validateSearch',
333
+ )
334
+ : null;
335
+ const loaderDepsName = searchContractInfo.loaderDepsPath
336
+ ? await getImportNameForSearchContract(
337
+ searchContractInfo.loaderDepsPath,
338
+ 'loaderDeps',
339
+ )
340
+ : null;
270
341
 
271
342
  const rawPath = (route as any).path as string | undefined;
272
343
  const hasSplat = typeof rawPath === 'string' && rawPath.includes('*');
@@ -286,6 +357,12 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
286
357
  `loader: modernLoaderToTanstack({ hasSplat: ${hasSplat} }, ${loaderName}),`,
287
358
  );
288
359
  }
360
+ if (validateSearchName) {
361
+ routeOpts.push(`validateSearch: ${validateSearchName},`);
362
+ }
363
+ if (loaderDepsName) {
364
+ routeOpts.push(`loaderDeps: ${loaderDepsName},`);
365
+ }
289
366
 
290
367
  const staticDataSnippet = createRouteStaticDataSnippet({
291
368
  modernRouteId: (route as any).id as string | undefined,
@@ -296,18 +373,27 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
296
373
  routeOpts.push(staticDataSnippet);
297
374
  }
298
375
 
299
- statements.push(
300
- `const ${varName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
301
- );
302
-
303
376
  const children = (route as any).children as
304
377
  | Array<NestedRouteForCli | PageRoute>
305
378
  | undefined;
379
+ const hasChildren = Boolean(children && children.length > 0);
380
+ const routeCtorVarName = hasChildren
381
+ ? reserveRouteVarName(`${varName}__base`)
382
+ : varName;
383
+
384
+ statements.push(
385
+ `const ${routeCtorVarName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
386
+ );
387
+
306
388
  if (children && children.length > 0) {
307
389
  const childVars = await Promise.all(
308
- children.map(child => buildRoute({ parentVar: varName, route: child })),
390
+ children.map(child =>
391
+ buildRoute({ parentVar: routeCtorVarName, route: child }),
392
+ ),
393
+ );
394
+ statements.push(
395
+ `const ${varName} = ${routeCtorVarName}.addChildren([${childVars.join(', ')}]);`,
309
396
  );
310
- statements.push(`${varName}.addChildren([${childVars.join(', ')}]);`);
311
397
  }
312
398
 
313
399
  return varName;
@@ -326,6 +412,21 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
326
412
  : null;
327
413
  const rootLoaderName = rootLoaderImports?.loaderName || null;
328
414
  const rootActionName = rootLoaderImports?.actionName || null;
415
+ const rootSearchContractInfo = rootModern
416
+ ? pickRouteSearchContractModules(rootModern)
417
+ : null;
418
+ const rootValidateSearchName = rootSearchContractInfo?.validateSearchPath
419
+ ? await getImportNameForSearchContract(
420
+ rootSearchContractInfo.validateSearchPath,
421
+ 'validateSearch',
422
+ )
423
+ : null;
424
+ const rootLoaderDepsName = rootSearchContractInfo?.loaderDepsPath
425
+ ? await getImportNameForSearchContract(
426
+ rootSearchContractInfo.loaderDepsPath,
427
+ 'loaderDeps',
428
+ )
429
+ : null;
329
430
 
330
431
  const topLevelVars = await Promise.all(
331
432
  topLevel.map(route => buildRoute({ parentVar: 'rootRoute', route })),
@@ -337,12 +438,19 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
337
438
  `loader: modernLoaderToTanstack({ hasSplat: false }, ${rootLoaderName}),`,
338
439
  );
339
440
  }
441
+ if (rootValidateSearchName) {
442
+ rootOpts.push(`validateSearch: ${rootValidateSearchName},`);
443
+ }
444
+ if (rootLoaderDepsName) {
445
+ rootOpts.push(`loaderDeps: ${rootLoaderDepsName},`);
446
+ }
340
447
 
341
448
  const routerGenTs = `/* eslint-disable */
342
449
  // This file is auto-generated by Modern.js. Do not edit manually.
343
450
 
344
451
  import {
345
452
  createMemoryHistory,
453
+ modernTanstackRouterFastDefaults,
346
454
  createRootRouteWithContext,
347
455
  createRoute,
348
456
  createRouter,
@@ -370,7 +478,7 @@ function isRedirectResponse(res: Response) {
370
478
  }
371
479
 
372
480
  function throwTanstackRedirect(location: string) {
373
- const target = location || '/';
481
+ const target = location.length > 0 ? location : '/';
374
482
  try {
375
483
  void new URL(target);
376
484
  throw redirect({ href: target });
@@ -396,21 +504,87 @@ function createRouteStaticData(opts: {
396
504
  modernRouteAction?: unknown;
397
505
  modernRouteLoader?: unknown;
398
506
  }) {
399
- const staticData: Record<string, unknown> = {};
507
+ const staticData: {
508
+ modernRouteId?: string;
509
+ modernRouteAction?: unknown;
510
+ modernRouteLoader?: unknown;
511
+ } = {};
400
512
 
401
- if (opts.modernRouteId) {
513
+ if (typeof opts.modernRouteId === 'string' && opts.modernRouteId.length > 0) {
402
514
  staticData.modernRouteId = opts.modernRouteId;
403
515
  }
404
516
 
405
- if (opts.modernRouteLoader) {
517
+ if (typeof opts.modernRouteLoader !== 'undefined') {
406
518
  staticData.modernRouteLoader = opts.modernRouteLoader;
407
519
  }
408
520
 
409
- if (opts.modernRouteAction) {
521
+ if (typeof opts.modernRouteAction !== 'undefined') {
410
522
  staticData.modernRouteAction = opts.modernRouteAction;
411
523
  }
412
524
 
413
- return Object.keys(staticData).length > 0 ? staticData : undefined;
525
+ return staticData;
526
+ }
527
+
528
+ function getLoaderSignal(ctx: any): AbortSignal {
529
+ const abortSignal = ctx?.abortController?.signal;
530
+ if (abortSignal instanceof AbortSignal) {
531
+ return abortSignal;
532
+ }
533
+ if (ctx?.signal instanceof AbortSignal) {
534
+ return ctx.signal;
535
+ }
536
+ return new AbortController().signal;
537
+ }
538
+
539
+ function getLoaderHref(ctx: any): string {
540
+ if (typeof ctx?.location === 'string') {
541
+ return ctx.location;
542
+ }
543
+
544
+ const publicHref = ctx?.location?.publicHref;
545
+ if (typeof publicHref === 'string') {
546
+ return publicHref;
547
+ }
548
+
549
+ const href = ctx?.location?.href;
550
+ if (typeof href === 'string') {
551
+ return href;
552
+ }
553
+
554
+ const urlHref = ctx?.location?.url?.href;
555
+ return typeof urlHref === 'string' ? urlHref : '';
556
+ }
557
+
558
+ function getLoaderParams(ctx: any): Record<string, string> {
559
+ return typeof ctx?.params === 'object' && ctx.params !== null ? ctx.params : {};
560
+ }
561
+
562
+ function handleModernLoaderResult<LoaderResult>(result: LoaderResult): LoaderResult {
563
+ if (isResponse(result)) {
564
+ if (isRedirectResponse(result)) {
565
+ const location = result.headers.get('Location') ?? '/';
566
+ throwTanstackRedirect(location);
567
+ }
568
+ if (result.status === 404) {
569
+ throw notFound();
570
+ }
571
+ }
572
+
573
+ return result;
574
+ }
575
+
576
+ function handleModernLoaderError(err: unknown): never {
577
+ if (isResponse(err)) {
578
+ if (isRedirectResponse(err)) {
579
+ const location = err.headers.get('Location') ?? '/';
580
+ throwTanstackRedirect(location);
581
+ }
582
+ if (err.status === 404) {
583
+ throw notFound();
584
+ }
585
+ }
586
+
587
+ throw err;
414
588
  }
415
589
 
416
590
  function modernLoaderToTanstack<TLoader extends (args: any) => any>(
@@ -419,57 +593,31 @@ function modernLoaderToTanstack<TLoader extends (args: any) => any>(
419
593
  ) {
420
594
  type LoaderResult = Awaited<ReturnType<TLoader>>;
421
595
 
422
- return async (ctx: any): Promise<LoaderResult> => {
596
+ return (ctx: any): Promise<LoaderResult> => {
423
597
  try {
424
- const signal: AbortSignal =
425
- ctx?.abortController?.signal ||
426
- ctx?.signal ||
427
- new AbortController().signal;
598
+ const signal = getLoaderSignal(ctx);
428
599
  const baseRequest: Request | undefined =
429
600
  ctx?.context?.request instanceof Request ? ctx.context.request : undefined;
430
601
 
431
- const href =
432
- typeof ctx?.location === 'string'
433
- ? ctx.location
434
- : ctx?.location?.publicHref ||
435
- ctx?.location?.href ||
436
- ctx?.location?.url?.href ||
437
- '';
602
+ const href = getLoaderHref(ctx);
438
603
 
439
- const request = baseRequest
604
+ const request = baseRequest !== undefined
440
605
  ? new Request(baseRequest, { signal })
441
606
  : new Request(href, { signal });
442
607
 
443
- const params = mapParamsForModernLoader(ctx?.params || {}, opts.hasSplat);
608
+ const params = mapParamsForModernLoader(getLoaderParams(ctx), opts.hasSplat);
444
609
 
445
- const result = await (modernLoader as any)({
446
- request,
447
- params,
448
- context: ctx?.context?.requestContext,
449
- });
450
-
451
- if (isResponse(result)) {
452
- if (isRedirectResponse(result)) {
453
- const location = result.headers.get('Location') || '/';
454
- throwTanstackRedirect(location);
455
- }
456
- if (result.status === 404) {
457
- throw notFound();
458
- }
459
- }
460
-
461
- return result as LoaderResult;
610
+ return Promise.resolve(
611
+ (modernLoader as any)({
612
+ request,
613
+ params,
614
+ context: ctx?.context?.requestContext,
615
+ }),
616
+ )
617
+ .then((result: LoaderResult) => handleModernLoaderResult(result))
618
+ .catch(handleModernLoaderError);
462
619
  } catch (err) {
463
- if (isResponse(err)) {
464
- if (isRedirectResponse(err)) {
465
- const location = err.headers.get('Location') || '/';
466
- throwTanstackRedirect(location);
467
- }
468
- if (err.status === 404) {
469
- throw notFound();
470
- }
471
- }
472
- throw err;
620
+ handleModernLoaderError(err);
473
621
  }
474
622
  };
475
623
  }
@@ -492,6 +640,7 @@ ${statements.join('\n\n')}
492
640
  export const routeTree = rootRoute.addChildren([${topLevelVars.join(', ')}]);
493
641
 
494
642
  export const router = createRouter({
643
+ ...modernTanstackRouterFastDefaults,
495
644
  routeTree,
496
645
  history: createMemoryHistory({
497
646
  initialEntries: ['/'],
@@ -1,5 +1,12 @@
1
1
  export * from '@tanstack/react-router';
2
- export { useMatch } from '@tanstack/react-router';
2
+ export {
3
+ Outlet,
4
+ useLocation,
5
+ useMatch,
6
+ useMatches,
7
+ useNavigate,
8
+ useRouter,
9
+ } from '@tanstack/react-router';
3
10
  export type {
4
11
  Fetcher,
5
12
  FetcherState,
@@ -28,3 +35,8 @@ export type {
28
35
  CompositeComponentProps,
29
36
  } from './rsc/client';
30
37
  export { CompositeComponent } from './rsc/client';
38
+ export type { RouterConfig } from './types';
39
+ export {
40
+ getModernTanstackRouterFastDefaults,
41
+ modernTanstackRouterFastDefaults,
42
+ } from './types';
@@ -26,9 +26,9 @@ import {
26
26
  createRouter,
27
27
  RouterProvider,
28
28
  } from '@tanstack/react-router';
29
- import { attachRouterServerSsrUtils } from '@tanstack/react-router/ssr/server';
29
+ import { attachRouterServerSsrUtils } from '@tanstack/router-core/ssr/server';
30
30
  import type React from 'react';
31
- import { Suspense, useContext } from 'react';
31
+ import { useContext } from 'react';
32
32
  import { createModernBasepathRewrite } from './basepathRewrite';
33
33
  import {
34
34
  modifyRoutes as modifyRoutesHook,
@@ -52,7 +52,11 @@ import {
52
52
  createTanstackRscServerPayload,
53
53
  handleTanstackRscRedirect,
54
54
  } from './rsc/payloadRouter';
55
- import type { InternalRouterServerSnapshot, RouterConfig } from './types';
55
+ import {
56
+ getModernTanstackRouterFastDefaults,
57
+ type InternalRouterServerSnapshot,
58
+ type RouterConfig,
59
+ } from './types';
56
60
  import { createRouteObjectsFromConfig, urlJoin } from './utils';
57
61
 
58
62
  type ModernTanstackRouterContext = {
@@ -119,6 +123,17 @@ type PreloadableRouteComponent = {
119
123
  preload?: (props?: Record<string, unknown>) => Promise<unknown> | unknown;
120
124
  };
121
125
 
126
+ type ReactLazyRouteComponent = {
127
+ _init?: (payload: unknown) => unknown;
128
+ _payload?: unknown;
129
+ };
130
+
131
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
132
+ return Boolean(
133
+ value && typeof (value as PromiseLike<unknown>).then === 'function',
134
+ );
135
+ }
136
+
122
137
  type TanstackRouterWithServerSsr = AnyRouter & {
123
138
  resolveRedirect?: (redirect: Response) => Response;
124
139
  routesById?: Record<string, RouterRouteWithOptions>;
@@ -149,7 +164,37 @@ function isPreloadableRouteComponent(
149
164
  );
150
165
  }
151
166
 
167
+ function isReactLazyRouteComponent(
168
+ component: unknown,
169
+ ): component is ReactLazyRouteComponent {
170
+ return (
171
+ Boolean(component) &&
172
+ typeof component === 'object' &&
173
+ typeof (component as ReactLazyRouteComponent)._init === 'function' &&
174
+ '_payload' in component
175
+ );
176
+ }
177
+
178
+ async function preloadReactLazyRouteComponent(
179
+ component: ReactLazyRouteComponent,
180
+ ) {
181
+ try {
182
+ component._init?.(component._payload);
183
+ } catch (thrown) {
184
+ if (!isPromiseLike(thrown)) {
185
+ throw thrown;
186
+ }
187
+ await thrown;
188
+ component._init?.(component._payload);
189
+ }
190
+ }
191
+
152
192
  async function preloadRouteComponent(component: unknown) {
193
+ if (isReactLazyRouteComponent(component)) {
194
+ await preloadReactLazyRouteComponent(component);
195
+ return;
196
+ }
197
+
153
198
  if (!isPreloadableRouteComponent(component)) {
154
199
  return;
155
200
  }
@@ -398,6 +443,7 @@ export const tanstackRouterPlugin = (
398
443
  hooks.onBeforeCreateRouter.call(routerLifecycleContext);
399
444
 
400
445
  const tanstackRouter = createRouter({
446
+ ...getModernTanstackRouterFastDefaults(mergedConfig),
401
447
  routeTree,
402
448
  history,
403
449
  basepath: '/',
@@ -458,9 +504,12 @@ export const tanstackRouterPlugin = (
458
504
  context.ssrContext?.response.status(tanstackRouter.state.statusCode);
459
505
 
460
506
  await serverRouter.serverSsr?.dehydrate?.();
461
- await waitForRouterSerialization(serverRouter);
462
507
 
463
508
  if (isRSCNavigation) {
509
+ // RSC navigations consume the server payload directly. Normal HTML SSR
510
+ // emits the buffered bootstrap script below and must not wait here
511
+ // because Modern's non-streaming hook has not rendered the app yet.
512
+ await waitForRouterSerialization(serverRouter);
464
513
  setTanstackRscServerPayload(
465
514
  createTanstackRscServerPayload(serverRouter, {
466
515
  omitClientLoaderData: true,
@@ -516,9 +565,7 @@ export const tanstackRouterPlugin = (
516
565
  }
517
566
 
518
567
  const routerWrapper = (
519
- <Suspense fallback={null}>
520
- <RouterProvider router={router as AnyRouter} />
521
- </Suspense>
568
+ <RouterProvider router={router as AnyRouter} />
522
569
  );
523
570
 
524
571
  return App ? <App>{routerWrapper}</App> : routerWrapper;
@@ -25,7 +25,6 @@ import {
25
25
  useRouter,
26
26
  } from '@tanstack/react-router';
27
27
  import { RouterClient } from '@tanstack/react-router/ssr/client';
28
- import * as React from 'react';
29
28
  import { useContext, useMemo } from 'react';
30
29
  import { createModernBasepathRewrite } from './basepathRewrite';
31
30
  import {
@@ -41,9 +40,13 @@ import {
41
40
  applyRouterRuntimeState,
42
41
  type RouterLifecycleContext,
43
42
  } from './lifecycle';
43
+ import { Link } from './prefetchLink';
44
44
  import { createRouteTreeFromRouteObjects } from './routeTree';
45
45
  import { getTanstackRscSerializationAdapters } from './rsc/client';
46
- import type { RouterConfig } from './types';
46
+ import {
47
+ getModernTanstackRouterFastDefaults,
48
+ type RouterConfig,
49
+ } from './types';
47
50
  import { createRouteObjectsFromConfig, urlJoin } from './utils';
48
51
 
49
52
  const BLOCKING_SUBSCRIBE_SYMBOL = Symbol.for(
@@ -184,6 +187,7 @@ export const tanstackRouterPlugin = (
184
187
  }
185
188
 
186
189
  context.router = {
190
+ Link,
187
191
  useMatches,
188
192
  useLocation,
189
193
  useNavigate,
@@ -321,6 +325,7 @@ export const tanstackRouterPlugin = (
321
325
  : undefined;
322
326
 
323
327
  cachedRouter = createRouter({
328
+ ...getModernTanstackRouterFastDefaults(mergedConfig),
324
329
  routeTree,
325
330
  basepath: '/',
326
331
  rewrite,
@@ -368,9 +373,7 @@ export const tanstackRouterPlugin = (
368
373
  }
369
374
 
370
375
  const RouterContent = hasSSRBootstrap ? (
371
- <React.Suspense fallback={null}>
372
- <RouterClient router={router} />
373
- </React.Suspense>
376
+ <RouterClient router={router} />
374
377
  ) : (
375
378
  <RouterProvider router={router} />
376
379
  );
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ tanstackRouterPlugin,
4
+ } from './plugin.node';