@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121

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 (136) hide show
  1. package/dist/cjs/cli/index.js +89 -31
  2. package/dist/cjs/cli/routeSplitting.js +55 -0
  3. package/dist/cjs/cli/tanstackTypes.js +172 -170
  4. package/dist/cjs/cli.js +12 -8
  5. package/dist/cjs/runtime/basepathRewrite.js +12 -8
  6. package/dist/cjs/runtime/dataMutation.js +9 -5
  7. package/dist/cjs/runtime/hooks.js +20 -19
  8. package/dist/cjs/runtime/hydrationBoundary.js +48 -0
  9. package/dist/cjs/runtime/index.js +79 -35
  10. package/dist/cjs/runtime/lifecycle.js +21 -91
  11. package/dist/cjs/runtime/loaderBridge.js +173 -0
  12. package/dist/cjs/runtime/outlet.js +58 -0
  13. package/dist/cjs/runtime/plugin.js +195 -114
  14. package/dist/cjs/runtime/plugin.node.js +45 -45
  15. package/dist/cjs/runtime/plugin.worker.js +53 -0
  16. package/dist/cjs/runtime/pluginCore.js +55 -0
  17. package/dist/cjs/runtime/prefetchLink.js +10 -6
  18. package/dist/cjs/runtime/register.js +56 -0
  19. package/dist/cjs/runtime/routeTree.js +74 -207
  20. package/dist/cjs/runtime/router.js +41 -0
  21. package/dist/cjs/runtime/rsc/ClientSlot.js +9 -5
  22. package/dist/cjs/runtime/rsc/CompositeComponent.js +9 -5
  23. package/dist/cjs/runtime/rsc/ReplayableStream.js +14 -9
  24. package/dist/cjs/runtime/rsc/RscNodeRenderer.js +9 -5
  25. package/dist/cjs/runtime/rsc/SlotContext.js +9 -5
  26. package/dist/cjs/runtime/rsc/client.js +9 -5
  27. package/dist/cjs/runtime/rsc/createRscProxy.js +9 -5
  28. package/dist/cjs/runtime/rsc/index.js +9 -5
  29. package/dist/cjs/runtime/rsc/payloadRouter.js +44 -6
  30. package/dist/cjs/runtime/rsc/server.js +9 -5
  31. package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +9 -5
  32. package/dist/cjs/runtime/rsc/symbols.js +20 -15
  33. package/dist/cjs/runtime/state.js +45 -0
  34. package/dist/cjs/runtime/types.js +31 -1
  35. package/dist/cjs/runtime/utils.js +9 -10
  36. package/dist/cjs/runtime.js +9 -5
  37. package/dist/esm/cli/index.mjs +75 -27
  38. package/dist/esm/cli/routeSplitting.mjs +14 -0
  39. package/dist/esm/cli/tanstackTypes.mjs +158 -160
  40. package/dist/esm/runtime/hooks.mjs +1 -8
  41. package/dist/esm/runtime/hydrationBoundary.mjs +10 -0
  42. package/dist/esm/runtime/index.mjs +5 -2
  43. package/dist/esm/runtime/lifecycle.mjs +1 -82
  44. package/dist/esm/runtime/loaderBridge.mjs +114 -0
  45. package/dist/esm/runtime/outlet.mjs +17 -0
  46. package/dist/esm/runtime/plugin.mjs +191 -114
  47. package/dist/esm/runtime/plugin.node.mjs +40 -44
  48. package/dist/esm/runtime/plugin.worker.mjs +1 -0
  49. package/dist/esm/runtime/pluginCore.mjs +14 -0
  50. package/dist/esm/runtime/prefetchLink.mjs +1 -1
  51. package/dist/esm/runtime/register.mjs +18 -0
  52. package/dist/esm/runtime/routeTree.mjs +59 -193
  53. package/dist/esm/runtime/router.mjs +2 -0
  54. package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
  55. package/dist/esm/runtime/state.mjs +7 -0
  56. package/dist/esm/runtime/types.mjs +7 -0
  57. package/dist/esm/runtime/utils.mjs +0 -5
  58. package/dist/esm-node/cli/index.mjs +75 -27
  59. package/dist/esm-node/cli/routeSplitting.mjs +15 -0
  60. package/dist/esm-node/cli/tanstackTypes.mjs +158 -160
  61. package/dist/esm-node/runtime/hooks.mjs +1 -8
  62. package/dist/esm-node/runtime/hydrationBoundary.mjs +11 -0
  63. package/dist/esm-node/runtime/index.mjs +5 -2
  64. package/dist/esm-node/runtime/lifecycle.mjs +1 -82
  65. package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
  66. package/dist/esm-node/runtime/outlet.mjs +18 -0
  67. package/dist/esm-node/runtime/plugin.mjs +191 -114
  68. package/dist/esm-node/runtime/plugin.node.mjs +40 -44
  69. package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
  70. package/dist/esm-node/runtime/pluginCore.mjs +15 -0
  71. package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
  72. package/dist/esm-node/runtime/register.mjs +19 -0
  73. package/dist/esm-node/runtime/routeTree.mjs +59 -193
  74. package/dist/esm-node/runtime/router.mjs +3 -0
  75. package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
  76. package/dist/esm-node/runtime/state.mjs +8 -0
  77. package/dist/esm-node/runtime/types.mjs +7 -0
  78. package/dist/esm-node/runtime/utils.mjs +0 -5
  79. package/dist/types/cli/index.d.ts +14 -1
  80. package/dist/types/cli/routeSplitting.d.ts +20 -0
  81. package/dist/types/cli/tanstackTypes.d.ts +21 -1
  82. package/dist/types/runtime/hooks.d.ts +8 -33
  83. package/dist/types/runtime/hydrationBoundary.d.ts +2 -0
  84. package/dist/types/runtime/index.d.ts +8 -3
  85. package/dist/types/runtime/lifecycle.d.ts +7 -22
  86. package/dist/types/runtime/loaderBridge.d.ts +48 -0
  87. package/dist/types/runtime/outlet.d.ts +2 -0
  88. package/dist/types/runtime/plugin.d.ts +2 -15
  89. package/dist/types/runtime/plugin.node.d.ts +2 -15
  90. package/dist/types/runtime/plugin.worker.d.ts +1 -0
  91. package/dist/types/runtime/pluginCore.d.ts +21 -0
  92. package/dist/types/runtime/register.d.ts +9 -0
  93. package/dist/types/runtime/routeTree.d.ts +0 -2
  94. package/dist/types/runtime/router.d.ts +14 -0
  95. package/dist/types/runtime/state.d.ts +16 -0
  96. package/dist/types/runtime/types.d.ts +14 -53
  97. package/package.json +42 -40
  98. package/rstest.config.mts +6 -0
  99. package/src/cli/index.ts +162 -23
  100. package/src/cli/routeSplitting.ts +43 -0
  101. package/src/cli/tanstackTypes.ts +331 -187
  102. package/src/runtime/hooks.ts +10 -27
  103. package/src/runtime/hydrationBoundary.tsx +12 -0
  104. package/src/runtime/index.tsx +17 -7
  105. package/src/runtime/lifecycle.ts +16 -151
  106. package/src/runtime/loaderBridge.ts +257 -0
  107. package/src/runtime/outlet.tsx +48 -0
  108. package/src/runtime/plugin.node.tsx +72 -85
  109. package/src/runtime/plugin.tsx +361 -206
  110. package/src/runtime/plugin.worker.tsx +4 -0
  111. package/src/runtime/pluginCore.ts +48 -0
  112. package/src/runtime/prefetchLink.tsx +1 -1
  113. package/src/runtime/register.ts +58 -0
  114. package/src/runtime/routeTree.ts +163 -354
  115. package/src/runtime/router.ts +15 -0
  116. package/src/runtime/rsc/payloadRouter.ts +45 -2
  117. package/src/runtime/ssr-shim.d.ts +1 -3
  118. package/src/runtime/state.ts +29 -0
  119. package/src/runtime/types.ts +32 -66
  120. package/src/runtime/utils.tsx +3 -6
  121. package/tests/router/cli.test.ts +586 -5
  122. package/tests/router/fastDefaults.test.ts +25 -0
  123. package/tests/router/hooks.test.ts +26 -0
  124. package/tests/router/hydrationBoundary.test.tsx +23 -0
  125. package/tests/router/loaderBridge.test.ts +211 -0
  126. package/tests/router/packageSurface.test.ts +24 -0
  127. package/tests/router/prefetchLink.test.tsx +43 -7
  128. package/tests/router/register.test.ts +46 -0
  129. package/tests/router/routeTree.test.ts +381 -81
  130. package/tests/router/rsc.test.tsx +70 -0
  131. package/tests/router/tanstackTypes.test.ts +573 -1
  132. package/dist/cjs/runtime/DefaultNotFound.js +0 -47
  133. package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
  134. package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
  135. package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
  136. package/src/runtime/DefaultNotFound.tsx +0 -15
@@ -1,31 +1,10 @@
1
1
  // @effect-diagnostics asyncFunction:off nodeBuiltinImport:off strictBooleanExpressions:off
2
2
  import type { AppToolsContext } from '@modern-js/app-tools';
3
+ import { getPathWithoutExt, makeLegalIdentifier } from '@modern-js/runtime/cli';
3
4
  import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
4
- import { findExists, formatImportPath, fs, slash } from '@modern-js/utils';
5
+ import { findExists, formatImportPath, slash } from '@modern-js/utils';
5
6
  import path from 'path';
6
7
 
7
- const reservedWords =
8
- 'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public';
9
- const builtins =
10
- 'arguments Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl';
11
- const forbidList = new Set<string>(`${reservedWords} ${builtins}`.split(' '));
12
-
13
- function makeLegalIdentifier(str: string) {
14
- const identifier = str
15
- .replace(/-(\w)/g, (_, letter) => letter.toUpperCase())
16
- .replace(/[^$_a-zA-Z0-9]/g, '_');
17
-
18
- if (/\d/.test(identifier[0]) || forbidList.has(identifier)) {
19
- return `_${identifier}`;
20
- }
21
- return identifier || '_';
22
- }
23
-
24
- function getPathWithoutExt(filename: string) {
25
- const extname = path.extname(filename);
26
- return extname ? filename.slice(0, -extname.length) : filename;
27
- }
28
-
29
8
  const JS_OR_TS_EXTS = [
30
9
  '.js',
31
10
  '.jsx',
@@ -86,6 +65,17 @@ function pickModernLoaderModule(route: NestedRouteForCli | PageRoute) {
86
65
  return { loaderPath, inline };
87
66
  }
88
67
 
68
+ function pickRouteSearchContractModules(route: NestedRouteForCli | PageRoute) {
69
+ const validateSearchPath = (route as any).validateSearch;
70
+ const loaderDepsPath = (route as any).loaderDeps;
71
+
72
+ return {
73
+ validateSearchPath:
74
+ typeof validateSearchPath === 'string' ? validateSearchPath : null,
75
+ loaderDepsPath: typeof loaderDepsPath === 'string' ? loaderDepsPath : null,
76
+ };
77
+ }
78
+
89
79
  function isPathlessLayout(route: NestedRouteForCli | PageRoute) {
90
80
  return (
91
81
  (route as any).type === 'nested' &&
@@ -126,27 +116,154 @@ function createRouteStaticDataSnippet(opts: {
126
116
  )}\n }),`;
127
117
  }
128
118
 
129
- export async function isTanstackRouterFrameworkEnabled(
130
- appContext: AppToolsContext,
131
- ): Promise<boolean> {
132
- const runtimeConfigBase = path.join(
133
- appContext.srcDirectory,
134
- appContext.runtimeConfigFile,
135
- );
136
- const runtimeConfigFile = findExists(
137
- JS_OR_TS_EXTS.map(ext => `${runtimeConfigBase}${ext}`),
138
- );
139
- if (!runtimeConfigFile) {
140
- return false;
119
+ const LOCALE_PARAM_SEGMENTS = new Set([
120
+ ':lang',
121
+ ':locale',
122
+ ':language',
123
+ '$lang',
124
+ '$locale',
125
+ '$language',
126
+ ]);
127
+
128
+ type CanonicalAwareRoute = (NestedRouteForCli | PageRoute) & {
129
+ modernCanonicalPath?: string;
130
+ index?: boolean;
131
+ isRoot?: boolean;
132
+ children?: CanonicalAwareRoute[];
133
+ };
134
+
135
+ function paramsTypeForCanonicalPath(canonicalPath: string): string {
136
+ const fields: string[] = [];
137
+
138
+ for (const segment of canonicalPath.split('/')) {
139
+ if (!segment) {
140
+ continue;
141
+ }
142
+ if (segment === '*' || segment === '$') {
143
+ fields.push(`'_splat'?: string`);
144
+ continue;
145
+ }
146
+ if (segment.startsWith('{-$') && segment.endsWith('}')) {
147
+ fields.push(`${JSON.stringify(segment.slice(3, -1))}?: string`);
148
+ continue;
149
+ }
150
+ if (segment.startsWith('$')) {
151
+ fields.push(`${JSON.stringify(segment.slice(1))}: string`);
152
+ continue;
153
+ }
154
+ if (segment.startsWith(':')) {
155
+ const optional = segment.endsWith('?');
156
+ const name = segment.slice(1, optional ? undefined : segment.length);
157
+ fields.push(
158
+ `${JSON.stringify(optional ? name.slice(0, -1) : name)}${
159
+ optional ? '?' : ''
160
+ }: string`,
161
+ );
162
+ }
141
163
  }
142
164
 
143
- try {
144
- const content = await fs.readFile(runtimeConfigFile, 'utf-8');
145
- // Heuristic: allow both single and double quotes, tolerate whitespace/newlines.
146
- return /framework\s*:\s*['"]tanstack['"]/.test(content);
147
- } catch {
148
- return false;
165
+ return fields.length > 0
166
+ ? `{ ${fields.join('; ')} }`
167
+ : 'Record<string, never>';
168
+ }
169
+
170
+ export type CollectCanonicalRoutesOptions = {
171
+ /**
172
+ * Whether a leading `:lang`/`:locale`/`:language` route param may be
173
+ * treated as an i18n locale prefix. This MUST only be enabled when
174
+ * `@modern-js/plugin-i18n` is actually installed: the emitted
175
+ * `declare module '@modern-js/plugin-i18n/runtime'` augmentation breaks
176
+ * typechecking (TS2664) for apps that hand-roll a `/:lang/` param without
177
+ * the plugin. Routes carrying `modernCanonicalPath` metadata are always
178
+ * honored — only plugin-i18n produces them.
179
+ */
180
+ localeParamHeuristic?: boolean;
181
+ };
182
+
183
+ /**
184
+ * Derive the canonical (language-agnostic) route map for an entry: the
185
+ * leading locale param is stripped and localized physical variants (routes
186
+ * carrying `modernCanonicalPath` metadata from `@modern-js/plugin-i18n`)
187
+ * collapse to their canonical pattern. Returns `null` when the entry has no
188
+ * i18n routing surface (no locale param and no localized variants), so plain
189
+ * TanStack apps never get a `@modern-js/plugin-i18n` module augmentation.
190
+ */
191
+ export function collectCanonicalRoutesForEntry(
192
+ routes: (NestedRouteForCli | PageRoute)[],
193
+ options: CollectCanonicalRoutesOptions = {},
194
+ ): Record<string, string> | null {
195
+ const { localeParamHeuristic = true } = options;
196
+ const canonicalParams = new Map<string, string>();
197
+ let hasI18nSurface = false;
198
+
199
+ const normalizeJoined = (joined: string): string => {
200
+ const collapsed = joined.replace(/\/+/g, '/');
201
+ const withLeading = collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
202
+ return withLeading.length > 1
203
+ ? withLeading.replace(/\/+$/, '')
204
+ : withLeading;
205
+ };
206
+
207
+ const record = (canonicalPath: string) => {
208
+ const normalized = normalizeJoined(canonicalPath || '/');
209
+ const key = toTanstackPath(normalized);
210
+ if (!canonicalParams.has(key)) {
211
+ canonicalParams.set(key, paramsTypeForCanonicalPath(normalized));
212
+ }
213
+ };
214
+
215
+ const visit = (route: CanonicalAwareRoute, parentPath: string) => {
216
+ let currentPath = parentPath;
217
+
218
+ if (typeof route.modernCanonicalPath === 'string') {
219
+ hasI18nSurface = true;
220
+ currentPath = normalizeJoined(route.modernCanonicalPath);
221
+ } else if (typeof route.path === 'string' && route.path.length > 0) {
222
+ const segments = route.path
223
+ .replace(/\[(.+?)\]/g, ':$1')
224
+ .split('/')
225
+ .filter(Boolean);
226
+ if (
227
+ localeParamHeuristic &&
228
+ parentPath === '' &&
229
+ LOCALE_PARAM_SEGMENTS.has(segments[0])
230
+ ) {
231
+ hasI18nSurface = true;
232
+ segments.shift();
233
+ }
234
+ currentPath = segments.length
235
+ ? normalizeJoined(`${parentPath}/${segments.join('/')}`)
236
+ : parentPath;
237
+ }
238
+
239
+ const children = route.children;
240
+ if (children && children.length > 0) {
241
+ for (const child of children) {
242
+ visit(child, currentPath);
243
+ }
244
+ return;
245
+ }
246
+
247
+ // Leaf page or index route: a navigable target.
248
+ record(currentPath || '/');
249
+ };
250
+
251
+ const rootModern = routes.find(
252
+ route => (route as CanonicalAwareRoute).isRoot,
253
+ ) as CanonicalAwareRoute | undefined;
254
+ const topLevel = rootModern ? (rootModern.children ?? []) : routes;
255
+
256
+ for (const route of topLevel) {
257
+ visit(route as CanonicalAwareRoute, '');
149
258
  }
259
+
260
+ if (!hasI18nSurface || canonicalParams.size === 0) {
261
+ return null;
262
+ }
263
+
264
+ return Object.fromEntries(
265
+ [...canonicalParams.entries()].sort(([a], [b]) => a.localeCompare(b)),
266
+ );
150
267
  }
151
268
 
152
269
  export async function generateTanstackRouterTypesSourceForEntry(opts: {
@@ -182,9 +299,66 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
182
299
  const statements: string[] = [];
183
300
 
184
301
  const loaderImportMap = new Map<string, string>();
302
+ const componentImportMap = new Map<string, Promise<string | null>>();
303
+ const searchContractImportMap = new Map<string, string>();
304
+ const usedRouteVarNames = new Set<string>();
185
305
  let loaderIndex = 0;
306
+ let componentIndex = 0;
307
+ let validateSearchIndex = 0;
308
+ let loaderDepsIndex = 0;
186
309
  let routeIndex = 0;
187
310
 
311
+ const getImportNameForComponent = (
312
+ componentPath: unknown,
313
+ ): Promise<string | null> => {
314
+ if (typeof componentPath !== 'string' || componentPath.length === 0) {
315
+ return Promise.resolve(null);
316
+ }
317
+
318
+ // Cache the in-flight promise: sibling routes sharing a component module
319
+ // are generated concurrently and must reuse one import.
320
+ let pendingImportName = componentImportMap.get(componentPath);
321
+ if (!pendingImportName) {
322
+ pendingImportName = (async () => {
323
+ // Resolve through the same machinery as loaders: the raw `_component`
324
+ // value carries the internal `@_modern_js_src` alias, which the app's
325
+ // tsconfig does not map — the generated file must use relative
326
+ // imports.
327
+ const resolvedNoExt = await resolveRouteModuleNoExt(componentPath);
328
+ if (!resolvedNoExt) {
329
+ return null;
330
+ }
331
+
332
+ const relImport = normalizeRelativeImport(
333
+ path.relative(outDir, resolvedNoExt),
334
+ );
335
+
336
+ const componentName = `component_${componentIndex++}`;
337
+ imports.push(`import ${componentName} from ${quote(relImport)};`);
338
+ return componentName;
339
+ })();
340
+ componentImportMap.set(componentPath, pendingImportName);
341
+ }
342
+
343
+ return pendingImportName;
344
+ };
345
+
346
+ const resolveRouteModuleNoExt = async (aliasedNoExtPath: string) => {
347
+ const prefix = `${appContext.internalSrcAlias}/`;
348
+ let absNoExt: string;
349
+ if (aliasedNoExtPath.startsWith(prefix)) {
350
+ const rel = aliasedNoExtPath.slice(prefix.length);
351
+ absNoExt = path.join(appContext.srcDirectory, rel);
352
+ } else if (path.isAbsolute(aliasedNoExtPath)) {
353
+ absNoExt = aliasedNoExtPath;
354
+ } else {
355
+ // Unknown format; treat as already relative to src.
356
+ absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
357
+ }
358
+
359
+ return resolveFileNoExt(absNoExt);
360
+ };
361
+
188
362
  const getImportNamesForLoader = async (
189
363
  aliasedNoExtPath: string,
190
364
  inline: boolean,
@@ -201,19 +375,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
201
375
  };
202
376
  }
203
377
 
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);
378
+ const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
217
379
  if (!resolvedNoExt) {
218
380
  return null;
219
381
  }
@@ -242,10 +404,49 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
242
404
  return { loaderName: importName, actionName };
243
405
  };
244
406
 
407
+ const getImportNameForSearchContract = async (
408
+ aliasedNoExtPath: string,
409
+ exportName: 'validateSearch' | 'loaderDeps',
410
+ ) => {
411
+ const key = `${exportName}:${aliasedNoExtPath}`;
412
+ const existing = searchContractImportMap.get(key);
413
+ if (existing) {
414
+ return existing;
415
+ }
416
+
417
+ const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
418
+ if (!resolvedNoExt) {
419
+ return null;
420
+ }
421
+
422
+ const relImport = normalizeRelativeImport(
423
+ path.relative(outDir, resolvedNoExt),
424
+ );
425
+ const importName =
426
+ exportName === 'validateSearch'
427
+ ? `validateSearch_${validateSearchIndex++}`
428
+ : `loaderDeps_${loaderDepsIndex++}`;
429
+ imports.push(
430
+ `import { ${exportName} as ${importName} } from ${quote(relImport)};`,
431
+ );
432
+ searchContractImportMap.set(key, importName);
433
+ return importName;
434
+ };
435
+
436
+ const reserveRouteVarName = (preferred: string) => {
437
+ let candidate = preferred;
438
+ let suffix = 1;
439
+ while (usedRouteVarNames.has(candidate)) {
440
+ candidate = `${preferred}_${suffix++}`;
441
+ }
442
+ usedRouteVarNames.add(candidate);
443
+ return candidate;
444
+ };
445
+
245
446
  const createRouteVarName = (route: NestedRouteForCli | PageRoute) => {
246
447
  const id = (route as any).id as string | undefined;
247
448
  const base = id ? makeLegalIdentifier(id) : `r_${routeIndex++}`;
248
- return `route_${base}`;
449
+ return reserveRouteVarName(`route_${base}`);
249
450
  };
250
451
 
251
452
  const buildRoute = async (opts: {
@@ -267,12 +468,32 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
267
468
  : null;
268
469
  const loaderName = loaderImports?.loaderName || null;
269
470
  const actionName = loaderImports?.actionName || null;
471
+ const searchContractInfo = pickRouteSearchContractModules(route);
472
+ const validateSearchName = searchContractInfo.validateSearchPath
473
+ ? await getImportNameForSearchContract(
474
+ searchContractInfo.validateSearchPath,
475
+ 'validateSearch',
476
+ )
477
+ : null;
478
+ const loaderDepsName = searchContractInfo.loaderDepsPath
479
+ ? await getImportNameForSearchContract(
480
+ searchContractInfo.loaderDepsPath,
481
+ 'loaderDeps',
482
+ )
483
+ : null;
270
484
 
271
485
  const rawPath = (route as any).path as string | undefined;
272
486
  const hasSplat = typeof rawPath === 'string' && rawPath.includes('*');
273
487
 
274
488
  const routeOpts: string[] = [`getParentRoute: () => ${parentVar},`];
275
489
 
490
+ const componentName = await getImportNameForComponent(
491
+ (route as any)._component,
492
+ );
493
+ if (componentName) {
494
+ routeOpts.push(`component: ${componentName},`);
495
+ }
496
+
276
497
  if (isPathlessLayout(route)) {
277
498
  const id = (route as any).id as string | undefined;
278
499
  routeOpts.push(`id: ${quote(id || 'pathless')},`);
@@ -286,6 +507,12 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
286
507
  `loader: modernLoaderToTanstack({ hasSplat: ${hasSplat} }, ${loaderName}),`,
287
508
  );
288
509
  }
510
+ if (validateSearchName) {
511
+ routeOpts.push(`validateSearch: ${validateSearchName},`);
512
+ }
513
+ if (loaderDepsName) {
514
+ routeOpts.push(`loaderDeps: ${loaderDepsName},`);
515
+ }
289
516
 
290
517
  const staticDataSnippet = createRouteStaticDataSnippet({
291
518
  modernRouteId: (route as any).id as string | undefined,
@@ -296,18 +523,27 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
296
523
  routeOpts.push(staticDataSnippet);
297
524
  }
298
525
 
299
- statements.push(
300
- `const ${varName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
301
- );
302
-
303
526
  const children = (route as any).children as
304
527
  | Array<NestedRouteForCli | PageRoute>
305
528
  | undefined;
529
+ const hasChildren = Boolean(children && children.length > 0);
530
+ const routeCtorVarName = hasChildren
531
+ ? reserveRouteVarName(`${varName}__base`)
532
+ : varName;
533
+
534
+ statements.push(
535
+ `const ${routeCtorVarName} = createRoute({\n ${routeOpts.join('\n ')}\n});`,
536
+ );
537
+
306
538
  if (children && children.length > 0) {
307
539
  const childVars = await Promise.all(
308
- children.map(child => buildRoute({ parentVar: varName, route: child })),
540
+ children.map(child =>
541
+ buildRoute({ parentVar: routeCtorVarName, route: child }),
542
+ ),
543
+ );
544
+ statements.push(
545
+ `const ${varName} = ${routeCtorVarName}.addChildren([${childVars.join(', ')}]);`,
309
546
  );
310
- statements.push(`${varName}.addChildren([${childVars.join(', ')}]);`);
311
547
  }
312
548
 
313
549
  return varName;
@@ -326,17 +562,46 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
326
562
  : null;
327
563
  const rootLoaderName = rootLoaderImports?.loaderName || null;
328
564
  const rootActionName = rootLoaderImports?.actionName || null;
565
+ const rootSearchContractInfo = rootModern
566
+ ? pickRouteSearchContractModules(rootModern)
567
+ : null;
568
+ const rootValidateSearchName = rootSearchContractInfo?.validateSearchPath
569
+ ? await getImportNameForSearchContract(
570
+ rootSearchContractInfo.validateSearchPath,
571
+ 'validateSearch',
572
+ )
573
+ : null;
574
+ const rootLoaderDepsName = rootSearchContractInfo?.loaderDepsPath
575
+ ? await getImportNameForSearchContract(
576
+ rootSearchContractInfo.loaderDepsPath,
577
+ 'loaderDeps',
578
+ )
579
+ : null;
329
580
 
330
581
  const topLevelVars = await Promise.all(
331
582
  topLevel.map(route => buildRoute({ parentVar: 'rootRoute', route })),
332
583
  );
333
584
 
334
585
  const rootOpts: string[] = [];
586
+
587
+ const rootComponentName = await getImportNameForComponent(
588
+ (rootModern as any)?._component,
589
+ );
590
+ if (rootComponentName) {
591
+ rootOpts.push(`component: ${rootComponentName},`);
592
+ }
593
+
335
594
  if (rootLoaderName) {
336
595
  rootOpts.push(
337
596
  `loader: modernLoaderToTanstack({ hasSplat: false }, ${rootLoaderName}),`,
338
597
  );
339
598
  }
599
+ if (rootValidateSearchName) {
600
+ rootOpts.push(`validateSearch: ${rootValidateSearchName},`);
601
+ }
602
+ if (rootLoaderDepsName) {
603
+ rootOpts.push(`loaderDeps: ${rootLoaderDepsName},`);
604
+ }
340
605
 
341
606
  const routerGenTs = `/* eslint-disable */
342
607
  // This file is auto-generated by Modern.js. Do not edit manually.
@@ -346,134 +611,12 @@ import {
346
611
  createRootRouteWithContext,
347
612
  createRoute,
348
613
  createRouter,
349
- notFound,
350
- redirect,
614
+ createRouteStaticData,
615
+ type ModernRouterContext,
616
+ modernLoaderToTanstack,
617
+ modernTanstackRouterFastDefaults,
351
618
  } from '@modern-js/plugin-tanstack/runtime';
352
619
 
353
- type ModernRouterContext = {
354
- request?: Request;
355
- requestContext?: unknown;
356
- };
357
-
358
- function isResponse(value: unknown): value is Response {
359
- return (
360
- value != null &&
361
- typeof value === 'object' &&
362
- typeof (value as any).status === 'number' &&
363
- typeof (value as any).headers === 'object'
364
- );
365
- }
366
-
367
- const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
368
- function isRedirectResponse(res: Response) {
369
- return redirectStatusCodes.has(res.status);
370
- }
371
-
372
- function throwTanstackRedirect(location: string) {
373
- const target = location || '/';
374
- try {
375
- void new URL(target);
376
- throw redirect({ href: target });
377
- } catch {
378
- throw redirect({ to: target });
379
- }
380
- }
381
-
382
- function mapParamsForModernLoader(params: Record<string, string>, hasSplat: boolean) {
383
- if (!hasSplat) {
384
- return params;
385
- }
386
-
387
- const { _splat, ...rest } = params as any;
388
- if (typeof _splat !== 'undefined') {
389
- return { ...rest, '*': _splat };
390
- }
391
- return rest;
392
- }
393
-
394
- function createRouteStaticData(opts: {
395
- modernRouteId?: string;
396
- modernRouteAction?: unknown;
397
- modernRouteLoader?: unknown;
398
- }) {
399
- const staticData: Record<string, unknown> = {};
400
-
401
- if (opts.modernRouteId) {
402
- staticData.modernRouteId = opts.modernRouteId;
403
- }
404
-
405
- if (opts.modernRouteLoader) {
406
- staticData.modernRouteLoader = opts.modernRouteLoader;
407
- }
408
-
409
- if (opts.modernRouteAction) {
410
- staticData.modernRouteAction = opts.modernRouteAction;
411
- }
412
-
413
- return Object.keys(staticData).length > 0 ? staticData : undefined;
414
- }
415
-
416
- function modernLoaderToTanstack<TLoader extends (args: any) => any>(
417
- opts: { hasSplat: boolean },
418
- modernLoader: TLoader,
419
- ) {
420
- type LoaderResult = Awaited<ReturnType<TLoader>>;
421
-
422
- return async (ctx: any): Promise<LoaderResult> => {
423
- try {
424
- const signal: AbortSignal =
425
- ctx?.abortController?.signal ||
426
- ctx?.signal ||
427
- new AbortController().signal;
428
- const baseRequest: Request | undefined =
429
- ctx?.context?.request instanceof Request ? ctx.context.request : undefined;
430
-
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
- '';
438
-
439
- const request = baseRequest
440
- ? new Request(baseRequest, { signal })
441
- : new Request(href, { signal });
442
-
443
- const params = mapParamsForModernLoader(ctx?.params || {}, opts.hasSplat);
444
-
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;
462
- } 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;
473
- }
474
- };
475
- }
476
-
477
620
  ${imports.join('\n')}
478
621
 
479
622
  export const rootRoute = createRootRouteWithContext<ModernRouterContext>()({
@@ -492,6 +635,7 @@ ${statements.join('\n\n')}
492
635
  export const routeTree = rootRoute.addChildren([${topLevelVars.join(', ')}]);
493
636
 
494
637
  export const router = createRouter({
638
+ ...modernTanstackRouterFastDefaults,
495
639
  routeTree,
496
640
  history: createMemoryHistory({
497
641
  initialEntries: ['/'],
@@ -1,20 +1,10 @@
1
- import { createSyncHook } from '@modern-js/plugin';
2
- import type { TRuntimeContext } from '@modern-js/runtime/context';
3
- import type { RouteObject } from '@modern-js/runtime-utils/router';
4
- import type { RouterLifecycleContext } from './lifecycle';
5
-
6
- const modifyRoutes = createSyncHook<(routes: RouteObject[]) => RouteObject[]>();
7
- const onBeforeCreateRoutes =
8
- createSyncHook<(context: TRuntimeContext) => void>();
9
- const onBeforeCreateRouter =
10
- createSyncHook<(context: RouterLifecycleContext) => void>();
11
- const onAfterCreateRouter =
12
- createSyncHook<(context: RouterLifecycleContext) => void>();
13
- const onBeforeHydrateRouter =
14
- createSyncHook<(context: RouterLifecycleContext) => void>();
15
- const onAfterHydrateRouter =
16
- createSyncHook<(context: RouterLifecycleContext) => void>();
17
-
1
+ /**
2
+ * The router hooks are owned by @modern-js/runtime — this module re-exports
3
+ * the canonical instances through the `/context` seam, so the TanStack
4
+ * provider taps and calls the exact same hooks the built-in router wrapper
5
+ * registers. Creating separate hook instances here would silently split the
6
+ * hook registry between the wrapper and this provider.
7
+ */
18
8
  export {
19
9
  modifyRoutes,
20
10
  onAfterCreateRouter,
@@ -22,13 +12,6 @@ export {
22
12
  onBeforeCreateRouter,
23
13
  onBeforeCreateRoutes,
24
14
  onBeforeHydrateRouter,
25
- };
26
-
27
- export type RouterExtendsHooks = {
28
- modifyRoutes: typeof modifyRoutes;
29
- onBeforeCreateRoutes: typeof onBeforeCreateRoutes;
30
- onBeforeCreateRouter: typeof onBeforeCreateRouter;
31
- onAfterCreateRouter: typeof onAfterCreateRouter;
32
- onBeforeHydrateRouter: typeof onBeforeHydrateRouter;
33
- onAfterHydrateRouter: typeof onAfterHydrateRouter;
34
- };
15
+ type RouterExtendsHooks,
16
+ routerProviderRegistryHooks,
17
+ } from '@modern-js/runtime/context';
@@ -0,0 +1,12 @@
1
+ import { type ReactElement, Suspense } from 'react';
2
+
3
+ export function wrapTanstackSsrHydrationBoundary(
4
+ routerContent: ReactElement,
5
+ shouldWrap: boolean,
6
+ ) {
7
+ if (shouldWrap) {
8
+ return <Suspense fallback={null}>{routerContent}</Suspense>;
9
+ }
10
+
11
+ return routerContent;
12
+ }