@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.11 → 3.9.0-ultramodern.12

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 (68) hide show
  1. package/dist/cjs/cli/artifacts.js +2 -1
  2. package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
  3. package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
  4. package/dist/cjs/cli/tanstackTypes/source.js +28 -63
  5. package/dist/cjs/runtime/lifecycle.js +0 -3
  6. package/dist/cjs/runtime/loaderBridge.js +10 -4
  7. package/dist/cjs/runtime/navigation.js +102 -0
  8. package/dist/cjs/runtime/plugin.js +6 -1
  9. package/dist/cjs/runtime/plugin.node.js +82 -64
  10. package/dist/cjs/runtime/routeTree/construction.js +19 -61
  11. package/dist/cjs/runtime/routeTree/loaders.js +1 -1
  12. package/dist/cjs/runtime/routeTree/paths.js +0 -6
  13. package/dist/cjs/runtime/utils.js +1 -0
  14. package/dist/cjs/shared/routeDescriptor.js +60 -0
  15. package/dist/esm/cli/artifacts.mjs +2 -1
  16. package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  17. package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
  18. package/dist/esm/cli/tanstackTypes/source.mjs +29 -64
  19. package/dist/esm/runtime/lifecycle.mjs +1 -1
  20. package/dist/esm/runtime/loaderBridge.mjs +8 -5
  21. package/dist/esm/runtime/navigation.mjs +64 -0
  22. package/dist/esm/runtime/plugin.mjs +6 -1
  23. package/dist/esm/runtime/plugin.node.mjs +83 -65
  24. package/dist/esm/runtime/routeTree/construction.mjs +19 -61
  25. package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
  26. package/dist/esm/runtime/routeTree/paths.mjs +1 -4
  27. package/dist/esm/runtime/utils.mjs +1 -0
  28. package/dist/esm/shared/routeDescriptor.mjs +22 -0
  29. package/dist/esm-node/cli/artifacts.mjs +2 -1
  30. package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  31. package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
  32. package/dist/esm-node/cli/tanstackTypes/source.mjs +29 -64
  33. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  34. package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
  35. package/dist/esm-node/runtime/navigation.mjs +65 -0
  36. package/dist/esm-node/runtime/plugin.mjs +6 -1
  37. package/dist/esm-node/runtime/plugin.node.mjs +83 -65
  38. package/dist/esm-node/runtime/routeTree/construction.mjs +19 -61
  39. package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
  40. package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
  41. package/dist/esm-node/runtime/utils.mjs +1 -0
  42. package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
  43. package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
  44. package/dist/types/runtime/lifecycle.d.ts +1 -1
  45. package/dist/types/runtime/loaderBridge.d.ts +1 -0
  46. package/dist/types/runtime/navigation.d.ts +4 -0
  47. package/dist/types/runtime/routeTree/paths.d.ts +0 -2
  48. package/dist/types/shared/routeDescriptor.d.ts +26 -0
  49. package/package.json +12 -12
  50. package/src/cli/artifacts.ts +6 -1
  51. package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
  52. package/src/cli/tanstackTypes/shared.ts +0 -14
  53. package/src/cli/tanstackTypes/source.ts +43 -119
  54. package/src/runtime/lifecycle.ts +0 -1
  55. package/src/runtime/loaderBridge.ts +9 -9
  56. package/src/runtime/navigation.ts +78 -0
  57. package/src/runtime/plugin.node.tsx +111 -89
  58. package/src/runtime/plugin.tsx +6 -0
  59. package/src/runtime/routeTree/construction.ts +32 -112
  60. package/src/runtime/routeTree/loaders.ts +2 -4
  61. package/src/runtime/routeTree/paths.ts +0 -6
  62. package/src/runtime/utils.tsx +1 -0
  63. package/src/shared/routeDescriptor.ts +46 -0
  64. package/tests/router/generateRouteArtifacts.test.ts +36 -2
  65. package/tests/router/localisedIdentity.test.tsx +1 -0
  66. package/tests/router/prefetchLink.test.tsx +29 -0
  67. package/tests/router/serverPlugin.test.ts +145 -0
  68. package/tests/router/tanstackTypes.test.ts +120 -2
@@ -1,25 +1,22 @@
1
1
  // @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
2
2
 
3
- import type {
4
- AppNormalizedConfig,
5
- AppTools,
6
- AppToolsContext,
7
- } from '@modern-js/app-tools';
8
- import { canonicaliseLocalisedRoutes } from '@modern-js/i18n-runtime-extensions';
3
+ import type { AppToolsContext } from '@modern-js/app-tools';
9
4
  import { makeLegalIdentifier } from '@modern-js/runtime/cli';
10
5
  import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
11
6
  import { upath as path } from '@modern-js/utils';
12
7
 
8
+ import {
9
+ describeRouteTree,
10
+ type RouteDescriptor,
11
+ } from '../../shared/routeDescriptor';
12
+
13
13
  import {
14
14
  createRouteStaticDataSnippet,
15
- isIndexRoute,
16
- isPathlessLayout,
17
15
  normalizeRelativeImport,
18
16
  pickModernLoaderModule,
19
17
  pickRouteSearchContractModules,
20
18
  quote,
21
19
  resolveFileNoExt,
22
- toTanstackPath,
23
20
  } from './shared';
24
21
 
25
22
  type RouteForCli = NestedRouteForCli | PageRoute;
@@ -46,22 +43,15 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
46
43
  appContext,
47
44
  entryName,
48
45
  generatedDirName = 'modern-tanstack',
49
- routes: rawRoutes,
46
+ routes,
50
47
  } = opts;
51
- const routes: RouteForCli[] = canonicaliseLocalisedRoutes(rawRoutes);
52
48
  const outDir = path.join(
53
49
  appContext.srcDirectory,
54
50
  generatedDirName,
55
51
  entryName,
56
52
  );
57
53
 
58
- const rootModern = routes.find(r => {
59
- const extras = r as RouteExtras;
60
- return r && extras.type === 'nested' && extras.isRoot;
61
- }) as NestedRouteForCli | undefined;
62
-
63
- const rootExtras = rootModern as RouteExtras | undefined;
64
- const topLevel = rootModern ? rootExtras?.children || [] : routes;
54
+ const { root: rootModern, children: topLevel } = describeRouteTree(routes);
65
55
 
66
56
  const imports: string[] = [];
67
57
  const statements: string[] = [];
@@ -92,15 +82,11 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
92
82
  // value carries the internal `@_modern_js_src` alias, which the app's
93
83
  // tsconfig does not map — the generated file must use relative
94
84
  // imports.
95
- const resolvedNoExt = await resolveRouteModuleNoExt(componentPath);
96
- if (!resolvedNoExt) {
85
+ const relImport = await resolveRouteImport(componentPath);
86
+ if (!relImport) {
97
87
  return null;
98
88
  }
99
89
 
100
- const relImport = normalizeRelativeImport(
101
- path.relative(outDir, resolvedNoExt),
102
- );
103
-
104
90
  const componentName = `component_${componentIndex++}`;
105
91
  imports.push(`import ${componentName} from ${quote(relImport)};`);
106
92
  return componentName;
@@ -111,7 +97,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
111
97
  return pendingImportName;
112
98
  };
113
99
 
114
- const resolveRouteModuleNoExt = async (aliasedNoExtPath: string) => {
100
+ const resolveRouteImport = async (aliasedNoExtPath: string) => {
115
101
  const prefix = `${appContext.internalSrcAlias}/`;
116
102
  let absNoExt: string;
117
103
  if (aliasedNoExtPath.startsWith(prefix)) {
@@ -124,7 +110,10 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
124
110
  absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
125
111
  }
126
112
 
127
- return resolveFileNoExt(absNoExt);
113
+ const resolvedNoExt = await resolveFileNoExt(absNoExt);
114
+ return resolvedNoExt
115
+ ? normalizeRelativeImport(path.relative(outDir, resolvedNoExt))
116
+ : null;
128
117
  };
129
118
 
130
119
  const getImportNamesForLoader = async (
@@ -143,15 +132,11 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
143
132
  };
144
133
  }
145
134
 
146
- const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
147
- if (!resolvedNoExt) {
135
+ const relImport = await resolveRouteImport(aliasedNoExtPath);
136
+ if (!relImport) {
148
137
  return null;
149
138
  }
150
139
 
151
- const relImport = normalizeRelativeImport(
152
- path.relative(outDir, resolvedNoExt),
153
- );
154
-
155
140
  const importName = `loader_${loaderIndex++}`;
156
141
  const actionName = hasAction
157
142
  ? importName.replace(/^loader_/, 'action_')
@@ -182,14 +167,10 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
182
167
  return existing;
183
168
  }
184
169
 
185
- const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
186
- if (!resolvedNoExt) {
170
+ const relImport = await resolveRouteImport(aliasedNoExtPath);
171
+ if (!relImport) {
187
172
  return null;
188
173
  }
189
-
190
- const relImport = normalizeRelativeImport(
191
- path.relative(outDir, resolvedNoExt),
192
- );
193
174
  const importName =
194
175
  exportName === 'validateSearch'
195
176
  ? `validateSearch_${validateSearchIndex++}`
@@ -217,15 +198,8 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
217
198
  return reserveRouteVarName(`route_${base}`);
218
199
  };
219
200
 
220
- const buildRoute = async (opts: {
221
- parentVar: string;
222
- route: NestedRouteForCli | PageRoute;
223
- }): Promise<string> => {
224
- const { parentVar, route } = opts;
225
-
226
- const varName = createRouteVarName(route);
201
+ const createRouteOptions = async (route: RouteForCli, hasSplat: boolean) => {
227
202
  const routeExtras = route as RouteExtras;
228
-
229
203
  const loaderInfo = pickModernLoaderModule(route);
230
204
  const routeAction = routeExtras.action;
231
205
  const loaderImports = loaderInfo
@@ -251,10 +225,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
251
225
  )
252
226
  : null;
253
227
 
254
- const rawPath = routeExtras.path;
255
- const hasSplat = typeof rawPath === 'string' && rawPath.includes('*');
256
-
257
- const routeOpts: string[] = [`getParentRoute: () => ${parentVar},`];
228
+ const routeOpts: string[] = [];
258
229
 
259
230
  const componentName = await getImportNameForComponent(
260
231
  routeExtras._component,
@@ -263,14 +234,6 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
263
234
  routeOpts.push(`component: ${componentName},`);
264
235
  }
265
236
 
266
- if (isPathlessLayout(route)) {
267
- const id = routeExtras.id;
268
- routeOpts.push(`id: ${quote(id || 'pathless')},`);
269
- } else {
270
- const p = isIndexRoute(route) ? '/' : toTanstackPath(rawPath || '');
271
- routeOpts.push(`path: ${quote(p)},`);
272
- }
273
-
274
237
  if (loaderName) {
275
238
  routeOpts.push(
276
239
  `loader: modernLoaderToTanstack({ hasSplat: ${hasSplat} }, ${loaderName}),`,
@@ -292,7 +255,22 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
292
255
  routeOpts.push(staticDataSnippet);
293
256
  }
294
257
 
295
- const children = routeExtras.children;
258
+ return routeOpts;
259
+ };
260
+
261
+ const buildRoute = async (
262
+ descriptor: RouteDescriptor<RouteForCli>,
263
+ parentVar: string,
264
+ ): Promise<string> => {
265
+ const { source: route, location, hasSplat, children } = descriptor;
266
+ const varName = createRouteVarName(route);
267
+ const routeOpts = [
268
+ `getParentRoute: () => ${parentVar},`,
269
+ ...Object.entries(location).map(
270
+ ([key, value]) => `${key}: ${quote(value)},`,
271
+ ),
272
+ ...(await createRouteOptions(route, hasSplat)),
273
+ ];
296
274
  const hasChildren = Boolean(children && children.length > 0);
297
275
  const routeCtorVarName = hasChildren
298
276
  ? reserveRouteVarName(`${varName}__base`)
@@ -305,9 +283,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
305
283
  if (children && children.length > 0) {
306
284
  const childVars: string[] = [];
307
285
  for (const child of children) {
308
- childVars.push(
309
- await buildRoute({ parentVar: routeCtorVarName, route: child }),
310
- );
286
+ childVars.push(await buildRoute(child, routeCtorVarName));
311
287
  }
312
288
  statements.push(
313
289
  `const ${varName} = ${routeCtorVarName}.addChildren([${childVars.join(', ')}]);`,
@@ -317,60 +293,14 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
317
293
  return varName;
318
294
  };
319
295
 
320
- const rootLoaderInfo = rootModern ? pickModernLoaderModule(rootModern) : null;
321
- const rootAction = rootExtras?.action;
322
- const rootLoaderImports = rootLoaderInfo?.loaderPath
323
- ? await getImportNamesForLoader(
324
- rootLoaderInfo.loaderPath,
325
- rootLoaderInfo.inline,
326
- Boolean(
327
- rootLoaderInfo.inline && rootAction === rootLoaderInfo.loaderPath,
328
- ),
329
- )
330
- : null;
331
- const rootLoaderName = rootLoaderImports?.loaderName || null;
332
- const rootActionName = rootLoaderImports?.actionName || null;
333
- const rootSearchContractInfo = rootModern
334
- ? pickRouteSearchContractModules(rootModern)
335
- : null;
336
- const rootValidateSearchName = rootSearchContractInfo?.validateSearchPath
337
- ? await getImportNameForSearchContract(
338
- rootSearchContractInfo.validateSearchPath,
339
- 'validateSearch',
340
- )
341
- : null;
342
- const rootLoaderDepsName = rootSearchContractInfo?.loaderDepsPath
343
- ? await getImportNameForSearchContract(
344
- rootSearchContractInfo.loaderDepsPath,
345
- 'loaderDeps',
346
- )
347
- : null;
348
-
349
296
  const topLevelVars: string[] = [];
350
297
  for (const route of topLevel) {
351
- topLevelVars.push(await buildRoute({ parentVar: 'rootRoute', route }));
298
+ topLevelVars.push(await buildRoute(route, 'rootRoute'));
352
299
  }
353
300
 
354
- const rootOpts: string[] = [];
355
-
356
- const rootComponentName = await getImportNameForComponent(
357
- rootExtras?._component,
358
- );
359
- if (rootComponentName) {
360
- rootOpts.push(`component: ${rootComponentName},`);
361
- }
362
-
363
- if (rootLoaderName) {
364
- rootOpts.push(
365
- `loader: modernLoaderToTanstack({ hasSplat: false }, ${rootLoaderName}),`,
366
- );
367
- }
368
- if (rootValidateSearchName) {
369
- rootOpts.push(`validateSearch: ${rootValidateSearchName},`);
370
- }
371
- if (rootLoaderDepsName) {
372
- rootOpts.push(`loaderDeps: ${rootLoaderDepsName},`);
373
- }
301
+ const rootOpts = rootModern
302
+ ? await createRouteOptions(rootModern, false)
303
+ : [];
374
304
 
375
305
  const routerGenTs = `// This file is auto-generated by Modern.js. Do not edit manually.
376
306
 
@@ -388,13 +318,7 @@ ${imports.join('\n')}
388
318
 
389
319
  export const rootRoute = createRootRouteWithContext<ModernRouterContext>()({
390
320
  ${rootOpts.join('\n ')}
391
- ${
392
- createRouteStaticDataSnippet({
393
- modernRouteId: rootExtras?.id,
394
- loaderName: rootLoaderName,
395
- actionName: rootActionName,
396
- }) || ''
397
- }
321
+
398
322
  });
399
323
 
400
324
  ${statements.join('\n\n')}
@@ -19,7 +19,6 @@ export type RouterLifecycleContext = NativeRouterLifecycleContext & {
19
19
  export {
20
20
  applyRouterRuntimeState,
21
21
  applyRouterServerPrepareResult,
22
- createRouterRuntimeState,
23
22
  createRouterServerSnapshot,
24
23
  getRouterRuntimeState,
25
24
  getRouterServerSnapshot,
@@ -98,15 +98,15 @@ type LoaderLikeContext = {
98
98
  params?: Record<string, string>;
99
99
  };
100
100
 
101
- function getLoaderSignal(ctx: LoaderLikeContext | undefined): AbortSignal {
102
- const abortSignal = ctx?.abortController?.signal;
103
- if (abortSignal instanceof AbortSignal) {
104
- return abortSignal;
105
- }
106
- if (ctx?.signal instanceof AbortSignal) {
107
- return ctx.signal;
108
- }
109
- return new AbortController().signal;
101
+ export function getLoaderSignal(
102
+ ctx: LoaderLikeContext | undefined,
103
+ ): AbortSignal {
104
+ const matchSignal = ctx?.abortController?.signal ?? ctx?.signal;
105
+ const requestSignal = ctx?.context?.request?.signal;
106
+ const signals = [matchSignal, requestSignal].filter(
107
+ (signal): signal is AbortSignal => signal instanceof AbortSignal,
108
+ );
109
+ return AbortSignal.any(signals);
110
110
  }
111
111
 
112
112
  export function getLoaderHref(ctx: LoaderLikeContext | undefined): string {
@@ -0,0 +1,78 @@
1
+ import type {
2
+ RouterNavigationCapability,
3
+ RouterNavigationSnapshot,
4
+ } from '@modern-js/runtime-extensions/router-state';
5
+ import type { AnyRouter } from '@tanstack/react-router';
6
+ import { Link } from './prefetchLink';
7
+
8
+ /** Native TanStack navigation, exposed through the provider contract. */
9
+ export function createTanstackNavigation(
10
+ router: AnyRouter,
11
+ ): RouterNavigationCapability {
12
+ let location: AnyRouter['state']['location'] | undefined;
13
+ let matches: AnyRouter['state']['matches'] | undefined;
14
+ let snapshot: RouterNavigationSnapshot;
15
+ return {
16
+ Link: Link as RouterNavigationCapability['Link'],
17
+ getSnapshot() {
18
+ const state = router.state;
19
+ if (
20
+ location !== state.location ||
21
+ matches === undefined ||
22
+ matches.length !== state.matches.length ||
23
+ matches.some(
24
+ (match, index) => match.params !== state.matches[index]?.params,
25
+ )
26
+ ) {
27
+ location = state.location;
28
+ matches = state.matches;
29
+ snapshot = {
30
+ location: {
31
+ pathname: location.pathname,
32
+ search: location.searchStr,
33
+ hash:
34
+ location.hash !== '' ? `#${location.hash.replace(/^#/, '')}` : '',
35
+ },
36
+ params: Object.assign({}, ...matches.map(match => match.params)),
37
+ };
38
+ }
39
+ return snapshot;
40
+ },
41
+ subscribe(listener) {
42
+ const stops = (
43
+ ['onBeforeNavigate', 'onBeforeLoad', 'onLoad', 'onResolved'] as const
44
+ ).map(event => router.subscribe(event, listener));
45
+ return () => {
46
+ for (const stop of stops) stop();
47
+ };
48
+ },
49
+ navigate(href, options) {
50
+ return router.navigate({
51
+ to: href,
52
+ replace: options?.replace,
53
+ ...(options?.state === undefined
54
+ ? {}
55
+ : { state: options.state as never }),
56
+ });
57
+ },
58
+ createLinkProps(target) {
59
+ const preload =
60
+ target.preload !== undefined
61
+ ? target.preload
62
+ : target.prefetch === 'none'
63
+ ? false
64
+ : target.prefetch;
65
+ return {
66
+ to: target.pathname,
67
+ ...(target.search !== undefined ? { search: target.search } : {}),
68
+ ...(target.hash !== undefined && target.hash !== ''
69
+ ? { hash: target.hash }
70
+ : {}),
71
+ ...(target.hashScrollIntoView === undefined
72
+ ? {}
73
+ : { hashScrollIntoView: target.hashScrollIntoView }),
74
+ ...(preload === undefined ? {} : { preload }),
75
+ };
76
+ },
77
+ };
78
+ }
@@ -23,11 +23,13 @@ import { createModernBasepathRewrite } from './basepathRewrite';
23
23
  import { routerProviderRegistryHooks } from './hooks';
24
24
  import { wrapTanstackSsrHydrationBoundary } from './hydrationBoundary';
25
25
  import {
26
+ applyRouterRuntimeState,
26
27
  applyRouterServerPrepareResult,
27
- createRouterServerSnapshot,
28
28
  getRouterRuntimeState,
29
+ getRouterServerSnapshot,
29
30
  type RouterLifecycleContext,
30
31
  } from './lifecycle';
32
+ import { createTanstackNavigation } from './navigation';
31
33
  import {
32
34
  createTanstackRouteObjects,
33
35
  getTanstackRouteConfig,
@@ -168,116 +170,136 @@ export const tanstackRouterPlugin = (
168
170
  setTanstackRscRouter(serverRouter);
169
171
  }
170
172
 
171
- await attachServerSsrUtils(serverRouter);
172
-
173
- const end = time();
174
-
173
+ let disposed = false;
174
+ let prepared = false;
175
+ const onAbort = () => controller.abort(rawRequest.signal.reason);
176
+ const cleanup = () => {
177
+ if (disposed) return;
178
+ disposed = true;
179
+ rawRequest.signal.removeEventListener('abort', onAbort);
180
+ controller.abort();
181
+ serverRouter.serverSsr?.cleanup?.();
182
+ };
183
+ // Acquire the disposer before attachment: every subsequent failure,
184
+ // including partial attachment, has an owner. Successful preparation
185
+ // leaves disposal to the response lifetime in the SSR pipeline.
186
+ applyRouterRuntimeState(context, {
187
+ framework: 'tanstack',
188
+ basename: _basename,
189
+ instance: serverRouter,
190
+ cleanup,
191
+ });
192
+ rawRequest.signal.addEventListener('abort', onAbort, { once: true });
175
193
  try {
176
- await tanstackRouter.load({ sync: true });
177
- } finally {
178
- const cost = end();
179
- context.ssrContext?.onTiming?.(LOADER_REPORTER_NAME, cost);
180
- }
194
+ rawRequest.signal.throwIfAborted();
195
+ await attachServerSsrUtils(serverRouter);
196
+ rawRequest.signal.throwIfAborted();
197
+
198
+ const end = time();
181
199
 
182
- const serverLoadResult = serverRouter._serverResult;
183
- if (!serverLoadResult) {
184
200
  try {
185
- serverRouter.serverSsr?.cleanup?.();
186
- } catch {}
187
- throw new Error(
188
- 'TanStack Router completed an SSR load without a server result.',
189
- );
190
- }
201
+ await tanstackRouter.load({ sync: true });
202
+ } finally {
203
+ const cost = end();
204
+ context.ssrContext?.onTiming?.(LOADER_REPORTER_NAME, cost);
205
+ }
191
206
 
192
- if (serverLoadResult.type === 'redirect') {
193
- const { redirect } = serverLoadResult;
207
+ rawRequest.signal.throwIfAborted();
208
+ const serverLoadResult = serverRouter._serverResult;
209
+ if (!serverLoadResult) {
210
+ throw new Error(
211
+ 'TanStack Router completed an SSR load without a server result.',
212
+ );
213
+ }
194
214
 
195
- try {
196
- serverRouter.serverSsr?.cleanup?.();
197
- } catch {}
198
-
199
- return interrupt(
200
- isRSCNavigation
201
- ? handleTanstackRscRedirect(
202
- redirect.headers,
203
- _basename,
204
- redirect.status,
205
- )
206
- : redirect,
207
- );
208
- }
215
+ if (serverLoadResult.type === 'redirect') {
216
+ const { redirect } = serverLoadResult;
217
+
218
+ return interrupt(
219
+ isRSCNavigation
220
+ ? handleTanstackRscRedirect(
221
+ redirect.headers,
222
+ _basename,
223
+ redirect.status,
224
+ )
225
+ : redirect,
226
+ );
227
+ }
228
+
229
+ const routerErrors = collectRouterErrors(tanstackRouter);
230
+ if (routerErrors && loaderFailureMode === 'clientRender') {
231
+ (
232
+ context.ssrContext?.response as
233
+ | { status: (code: number) => void }
234
+ | undefined
235
+ )?.status(200);
236
+ throw Object.values(routerErrors)[0];
237
+ }
238
+
239
+ await preloadMatchedRouteComponents(serverRouter);
209
240
 
210
- const routerErrors = collectRouterErrors(tanstackRouter);
211
- if (routerErrors && loaderFailureMode === 'clientRender') {
212
241
  (
213
242
  context.ssrContext?.response as
214
243
  | { status: (code: number) => void }
215
244
  | undefined
216
- )?.status(200);
217
- try {
218
- serverRouter.serverSsr?.cleanup?.();
219
- } catch {}
220
- throw Object.values(routerErrors)[0];
221
- }
222
-
223
- await preloadMatchedRouteComponents(serverRouter);
245
+ )?.status(serverLoadResult.status);
224
246
 
225
- (
226
- context.ssrContext?.response as
227
- | { status: (code: number) => void }
228
- | undefined
229
- )?.status(serverLoadResult.status);
247
+ await serverRouter.serverSsr?.dehydrate?.();
230
248
 
231
- await serverRouter.serverSsr?.dehydrate?.();
249
+ if (enableRsc) {
250
+ if (isRSCNavigation) {
251
+ // RSC navigations consume the server payload directly. Normal HTML SSR
252
+ // emits the buffered bootstrap script below and must not wait here
253
+ // because Modern's non-streaming hook has not rendered the app yet.
254
+ await waitForRouterSerialization(serverRouter);
255
+ }
232
256
 
233
- if (enableRsc) {
234
- if (isRSCNavigation) {
235
- // RSC navigations consume the server payload directly. Normal HTML SSR
236
- // emits the buffered bootstrap script below and must not wait here
237
- // because Modern's non-streaming hook has not rendered the app yet.
238
- await waitForRouterSerialization(serverRouter);
257
+ setTanstackRscServerPayload(
258
+ createTanstackRscServerPayload(serverRouter, {
259
+ omitClientLoaderData: isRSCNavigation,
260
+ }),
261
+ );
239
262
  }
240
263
 
241
- setTanstackRscServerPayload(
242
- createTanstackRscServerPayload(serverRouter, {
243
- omitClientLoaderData: isRSCNavigation,
244
- }),
245
- );
246
- }
247
-
248
- const ssrScriptTags = serverRouter.serverSsr?.takeBufferedScripts?.();
249
- const hydrationScripts = routerManagedTagsToHtml(ssrScriptTags);
250
- const matchedRouteIds = getModernRouteIdsFromMatches(serverRouter);
251
- const routerServerSnapshot: InternalRouterServerSnapshot =
252
- createRouterServerSnapshot({
264
+ const ssrScriptTags = serverRouter.serverSsr?.takeBufferedScripts?.();
265
+ const hydrationScripts = routerManagedTagsToHtml(ssrScriptTags);
266
+ const matchedRouteIds = getModernRouteIdsFromMatches(serverRouter);
267
+ const routerServerSnapshot: InternalRouterServerSnapshot = {
253
268
  framework: 'tanstack',
254
269
  basename: _basename,
255
270
  statusCode: serverLoadResult.status,
256
271
  errors: routerErrors,
257
272
  matchedRouteIds,
258
273
  hydrationScripts,
259
- });
260
- const runtimeContext = applyRouterServerPrepareResult(
261
- context as TInternalRuntimeContext,
262
- {
263
- snapshot: routerServerSnapshot,
264
- cleanup: () => serverRouter.serverSsr?.cleanup?.(),
265
- state: {
266
- framework: 'tanstack',
267
- basename: _basename,
268
- instance: serverRouter,
269
- hydrationScripts,
270
- matchedRouteIds,
271
- serverSnapshot: routerServerSnapshot,
274
+ };
275
+ const runtimeContext = applyRouterServerPrepareResult(
276
+ context as TInternalRuntimeContext,
277
+ {
278
+ snapshot: routerServerSnapshot,
279
+ cleanup,
280
+ state: {
281
+ framework: 'tanstack',
282
+ basename: _basename,
283
+ instance: serverRouter,
284
+ navigation: createTanstackNavigation(tanstackRouter),
285
+ },
272
286
  },
273
- },
274
- );
275
- hooks.onAfterCreateRouter.call({
276
- ...routerLifecycleContext,
277
- router: serverRouter,
278
- serverSnapshot: routerServerSnapshot,
279
- runtimeContext,
280
- });
287
+ );
288
+ rawRequest.signal.throwIfAborted();
289
+ hooks.onAfterCreateRouter.call({
290
+ ...routerLifecycleContext,
291
+ router: serverRouter,
292
+ serverSnapshot: getRouterServerSnapshot(runtimeContext),
293
+ runtimeContext,
294
+ });
295
+ prepared = true;
296
+ } finally {
297
+ if (!prepared) {
298
+ try {
299
+ cleanup();
300
+ } catch {}
301
+ }
302
+ }
281
303
  });
282
304
 
283
305
  api.wrapRoot(App => {
@@ -32,6 +32,7 @@ import {
32
32
  applyRouterRuntimeState,
33
33
  type RouterLifecycleContext,
34
34
  } from './lifecycle';
35
+ import { createTanstackNavigation } from './navigation';
35
36
  import {
36
37
  createTanstackRouteObjects,
37
38
  getMergedRouterConfig,
@@ -259,10 +260,15 @@ export const tanstackRouterPlugin = (
259
260
  if (!router) {
260
261
  return App ? <App /> : null;
261
262
  }
263
+ const navigation = useMemo(
264
+ () => createTanstackNavigation(router),
265
+ [router],
266
+ );
262
267
  const runtimeState = applyRouterRuntimeState(runtimeContext, {
263
268
  framework: 'tanstack',
264
269
  basename: _basename,
265
270
  instance: router,
271
+ navigation,
266
272
  });
267
273
  const lifecycleContext: RouterLifecycleContext = {
268
274
  framework: 'tanstack',