@tanstack/router-core 1.120.4 → 1.121.0-alpha.1

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 (51) hide show
  1. package/dist/cjs/fileRoute.d.cts +6 -2
  2. package/dist/cjs/index.cjs +3 -0
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/cjs/index.d.cts +6 -6
  5. package/dist/cjs/link.cjs.map +1 -1
  6. package/dist/cjs/link.d.cts +18 -1
  7. package/dist/cjs/path.cjs +130 -16
  8. package/dist/cjs/path.cjs.map +1 -1
  9. package/dist/cjs/path.d.cts +17 -0
  10. package/dist/cjs/redirect.cjs +17 -14
  11. package/dist/cjs/redirect.cjs.map +1 -1
  12. package/dist/cjs/redirect.d.cts +13 -7
  13. package/dist/cjs/route.cjs +12 -1
  14. package/dist/cjs/route.cjs.map +1 -1
  15. package/dist/cjs/route.d.cts +17 -18
  16. package/dist/cjs/router.cjs +290 -211
  17. package/dist/cjs/router.cjs.map +1 -1
  18. package/dist/cjs/router.d.cts +46 -3
  19. package/dist/cjs/typePrimitives.d.cts +2 -2
  20. package/dist/cjs/utils.cjs.map +1 -1
  21. package/dist/cjs/utils.d.cts +2 -0
  22. package/dist/esm/fileRoute.d.ts +6 -2
  23. package/dist/esm/index.d.ts +6 -6
  24. package/dist/esm/index.js +5 -2
  25. package/dist/esm/link.d.ts +18 -1
  26. package/dist/esm/link.js.map +1 -1
  27. package/dist/esm/path.d.ts +17 -0
  28. package/dist/esm/path.js +130 -16
  29. package/dist/esm/path.js.map +1 -1
  30. package/dist/esm/redirect.d.ts +13 -7
  31. package/dist/esm/redirect.js +17 -14
  32. package/dist/esm/redirect.js.map +1 -1
  33. package/dist/esm/route.d.ts +17 -18
  34. package/dist/esm/route.js +12 -1
  35. package/dist/esm/route.js.map +1 -1
  36. package/dist/esm/router.d.ts +46 -3
  37. package/dist/esm/router.js +293 -214
  38. package/dist/esm/router.js.map +1 -1
  39. package/dist/esm/typePrimitives.d.ts +2 -2
  40. package/dist/esm/utils.d.ts +2 -0
  41. package/dist/esm/utils.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/fileRoute.ts +90 -1
  44. package/src/index.ts +14 -6
  45. package/src/link.ts +97 -11
  46. package/src/path.ts +181 -16
  47. package/src/redirect.ts +37 -22
  48. package/src/route.ts +119 -39
  49. package/src/router.ts +393 -269
  50. package/src/typePrimitives.ts +2 -2
  51. package/src/utils.ts +14 -0
package/src/path.ts CHANGED
@@ -5,6 +5,9 @@ import type { AnyPathParams } from './route'
5
5
  export interface Segment {
6
6
  type: 'pathname' | 'param' | 'wildcard'
7
7
  value: string
8
+ // Add a new property to store the static segment if present
9
+ prefixSegment?: string
10
+ suffixSegment?: string
8
11
  }
9
12
 
10
13
  export function joinPaths(paths: Array<string | undefined>) {
@@ -137,10 +140,52 @@ export function resolvePath({
137
140
  }
138
141
  }
139
142
 
140
- const joined = joinPaths([basepath, ...baseSegments.map((d) => d.value)])
143
+ const segmentValues = baseSegments.map((segment) => {
144
+ if (segment.type === 'param') {
145
+ const param = segment.value.substring(1)
146
+ if (segment.prefixSegment && segment.suffixSegment) {
147
+ return `${segment.prefixSegment}{$${param}}${segment.suffixSegment}`
148
+ } else if (segment.prefixSegment) {
149
+ return `${segment.prefixSegment}{$${param}}`
150
+ } else if (segment.suffixSegment) {
151
+ return `{$${param}}${segment.suffixSegment}`
152
+ }
153
+ }
154
+ if (segment.type === 'wildcard') {
155
+ if (segment.prefixSegment && segment.suffixSegment) {
156
+ return `${segment.prefixSegment}{$}${segment.suffixSegment}`
157
+ } else if (segment.prefixSegment) {
158
+ return `${segment.prefixSegment}{$}`
159
+ } else if (segment.suffixSegment) {
160
+ return `{$}${segment.suffixSegment}`
161
+ }
162
+ }
163
+ return segment.value
164
+ })
165
+ const joined = joinPaths([basepath, ...segmentValues])
141
166
  return cleanPath(joined)
142
167
  }
143
168
 
169
+ const PARAM_RE = /^\$.{1,}$/ // $paramName
170
+ const PARAM_W_CURLY_BRACES_RE = /^(.*?)\{(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{$paramName}suffix
171
+ const WILDCARD_RE = /^\$$/ // $
172
+ const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
173
+
174
+ /**
175
+ * Required: `/foo/$bar` ✅
176
+ * Prefix and Suffix: `/foo/prefix${bar}suffix` ✅
177
+ * Wildcard: `/foo/$` ✅
178
+ * Wildcard with Prefix and Suffix: `/foo/prefix{$}suffix` ✅
179
+ *
180
+ * Future:
181
+ * Optional: `/foo/{-bar}`
182
+ * Optional named segment: `/foo/{bar}`
183
+ * Optional named segment with Prefix and Suffix: `/foo/prefix{-bar}suffix`
184
+ * Escape special characters:
185
+ * - `/foo/[$]` - Static route
186
+ * - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
187
+ * - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
188
+ */
144
189
  export function parsePathname(pathname?: string): Array<Segment> {
145
190
  if (!pathname) {
146
191
  return []
@@ -167,20 +212,55 @@ export function parsePathname(pathname?: string): Array<Segment> {
167
212
 
168
213
  segments.push(
169
214
  ...split.map((part): Segment => {
170
- if (part === '$' || part === '*') {
215
+ // Check for wildcard with curly braces: prefix{$}suffix
216
+ const wildcardBracesMatch = part.match(WILDCARD_W_CURLY_BRACES_RE)
217
+ if (wildcardBracesMatch) {
218
+ const prefix = wildcardBracesMatch[1]
219
+ const suffix = wildcardBracesMatch[2]
171
220
  return {
172
221
  type: 'wildcard',
173
- value: part,
222
+ value: '$',
223
+ prefixSegment: prefix || undefined,
224
+ suffixSegment: suffix || undefined,
174
225
  }
175
226
  }
176
227
 
177
- if (part.charAt(0) === '$') {
228
+ // Check for the new parameter format: prefix{$paramName}suffix
229
+ const paramBracesMatch = part.match(PARAM_W_CURLY_BRACES_RE)
230
+ if (paramBracesMatch) {
231
+ const prefix = paramBracesMatch[1]
232
+ const paramName = paramBracesMatch[2]
233
+ const suffix = paramBracesMatch[3]
178
234
  return {
179
235
  type: 'param',
180
- value: part,
236
+ value: '' + paramName,
237
+ prefixSegment: prefix || undefined,
238
+ suffixSegment: suffix || undefined,
181
239
  }
182
240
  }
183
241
 
242
+ // Check for bare parameter format: $paramName (without curly braces)
243
+ if (PARAM_RE.test(part)) {
244
+ const paramName = part.substring(1)
245
+ return {
246
+ type: 'param',
247
+ value: '$' + paramName,
248
+ prefixSegment: undefined,
249
+ suffixSegment: undefined,
250
+ }
251
+ }
252
+
253
+ // Check for bare wildcard: $ (without curly braces)
254
+ if (WILDCARD_RE.test(part)) {
255
+ return {
256
+ type: 'wildcard',
257
+ value: '$',
258
+ prefixSegment: undefined,
259
+ suffixSegment: undefined,
260
+ }
261
+ }
262
+
263
+ // Handle regular pathname segment
184
264
  return {
185
265
  type: 'pathname',
186
266
  value: part.includes('%25')
@@ -248,9 +328,13 @@ export function interpolatePath({
248
328
  interpolatedPathSegments.map((segment) => {
249
329
  if (segment.type === 'wildcard') {
250
330
  usedParams._splat = params._splat
331
+ const segmentPrefix = segment.prefixSegment || ''
332
+ const segmentSuffix = segment.suffixSegment || ''
251
333
  const value = encodeParam('_splat')
252
- if (leaveWildcards) return `${segment.value}${value ?? ''}`
253
- return value
334
+ if (leaveWildcards) {
335
+ return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
336
+ }
337
+ return `${segmentPrefix}${value}${segmentSuffix}`
254
338
  }
255
339
 
256
340
  if (segment.type === 'param') {
@@ -259,11 +343,14 @@ export function interpolatePath({
259
343
  isMissingParams = true
260
344
  }
261
345
  usedParams[key] = params[key]
346
+
347
+ const segmentPrefix = segment.prefixSegment || ''
348
+ const segmentSuffix = segment.suffixSegment || ''
262
349
  if (leaveParams) {
263
350
  const value = encodeParam(segment.value)
264
- return `${segment.value}${value ?? ''}`
351
+ return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
265
352
  }
266
- return encodeParam(key) ?? 'undefined'
353
+ return `${segmentPrefix}${encodeParam(key) ?? 'undefined'}${segmentSuffix}`
267
354
  }
268
355
 
269
356
  return segment.value
@@ -390,9 +477,57 @@ export function matchByPath(
390
477
 
391
478
  if (routeSegment) {
392
479
  if (routeSegment.type === 'wildcard') {
393
- const _splat = decodeURI(
394
- joinPaths(baseSegments.slice(i).map((d) => d.value)),
395
- )
480
+ // Capture all remaining segments for a wildcard
481
+ const remainingBaseSegments = baseSegments.slice(i)
482
+
483
+ let _splat: string
484
+
485
+ // If this is a wildcard with prefix/suffix, we need to handle the first segment specially
486
+ if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
487
+ if (!baseSegment) return false
488
+
489
+ const prefix = routeSegment.prefixSegment || ''
490
+ const suffix = routeSegment.suffixSegment || ''
491
+
492
+ // Check if the base segment starts with prefix and ends with suffix
493
+ const baseValue = baseSegment.value
494
+ if ('prefixSegment' in routeSegment) {
495
+ if (!baseValue.startsWith(prefix)) {
496
+ return false
497
+ }
498
+ }
499
+ if ('suffixSegment' in routeSegment) {
500
+ if (
501
+ !baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
502
+ ) {
503
+ return false
504
+ }
505
+ }
506
+
507
+ let rejoinedSplat = decodeURI(
508
+ joinPaths(remainingBaseSegments.map((d) => d.value)),
509
+ )
510
+
511
+ // Remove the prefix and suffix from the rejoined splat
512
+ if (prefix && rejoinedSplat.startsWith(prefix)) {
513
+ rejoinedSplat = rejoinedSplat.slice(prefix.length)
514
+ }
515
+
516
+ if (suffix && rejoinedSplat.endsWith(suffix)) {
517
+ rejoinedSplat = rejoinedSplat.slice(
518
+ 0,
519
+ rejoinedSplat.length - suffix.length,
520
+ )
521
+ }
522
+
523
+ _splat = rejoinedSplat
524
+ } else {
525
+ // If no prefix/suffix, just rejoin the remaining segments
526
+ _splat = decodeURI(
527
+ joinPaths(remainingBaseSegments.map((d) => d.value)),
528
+ )
529
+ }
530
+
396
531
  // TODO: Deprecate *
397
532
  params['*'] = _splat
398
533
  params['_splat'] = _splat
@@ -426,11 +561,41 @@ export function matchByPath(
426
561
  if (baseSegment.value === '/') {
427
562
  return false
428
563
  }
429
- if (baseSegment.value.charAt(0) !== '$') {
430
- params[routeSegment.value.substring(1)] = decodeURIComponent(
431
- baseSegment.value,
432
- )
564
+
565
+ let _paramValue: string
566
+
567
+ // If this param has prefix/suffix, we need to extract the actual parameter value
568
+ if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
569
+ const prefix = routeSegment.prefixSegment || ''
570
+ const suffix = routeSegment.suffixSegment || ''
571
+
572
+ // Check if the base segment starts with prefix and ends with suffix
573
+ const baseValue = baseSegment.value
574
+ if (prefix && !baseValue.startsWith(prefix)) {
575
+ return false
576
+ }
577
+ if (suffix && !baseValue.endsWith(suffix)) {
578
+ return false
579
+ }
580
+
581
+ let paramValue = baseValue
582
+ if (prefix && paramValue.startsWith(prefix)) {
583
+ paramValue = paramValue.slice(prefix.length)
584
+ }
585
+ if (suffix && paramValue.endsWith(suffix)) {
586
+ paramValue = paramValue.slice(
587
+ 0,
588
+ paramValue.length - suffix.length,
589
+ )
590
+ }
591
+
592
+ _paramValue = decodeURIComponent(paramValue)
593
+ } else {
594
+ // If no prefix/suffix, just decode the base segment value
595
+ _paramValue = decodeURIComponent(baseSegment.value)
433
596
  }
597
+
598
+ params[routeSegment.value.substring(1)] = _paramValue
434
599
  }
435
600
  }
436
601
 
package/src/redirect.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { NavigateOptions } from './link'
2
2
  import type { AnyRouter, RegisteredRouter } from './router'
3
- import type { PickAsRequired } from './utils'
4
3
 
5
4
  export type AnyRedirect = Redirect<any, any, any, any, any>
6
5
 
@@ -13,6 +12,17 @@ export type Redirect<
13
12
  TTo extends string | undefined = undefined,
14
13
  TMaskFrom extends string = TFrom,
15
14
  TMaskTo extends string = '.',
15
+ > = Response & {
16
+ options: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
17
+ redirectHandled?: boolean
18
+ }
19
+
20
+ export type RedirectOptions<
21
+ TRouter extends AnyRouter = RegisteredRouter,
22
+ TFrom extends string = string,
23
+ TTo extends string | undefined = undefined,
24
+ TMaskFrom extends string = TFrom,
25
+ TMaskTo extends string = '.',
16
26
  > = {
17
27
  href?: string
18
28
  /**
@@ -42,12 +52,7 @@ export type ResolvedRedirect<
42
52
  TTo extends string = '',
43
53
  TMaskFrom extends string = TFrom,
44
54
  TMaskTo extends string = '',
45
- > = PickAsRequired<
46
- Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
47
- 'code' | 'statusCode' | 'headers'
48
- > & {
49
- href: string
50
- }
55
+ > = Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
51
56
 
52
57
  export function redirect<
53
58
  TRouter extends AnyRouter = RegisteredRouter,
@@ -56,30 +61,40 @@ export function redirect<
56
61
  const TMaskFrom extends string = TFrom,
57
62
  const TMaskTo extends string = '',
58
63
  >(
59
- opts: Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
64
+ opts: RedirectOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
60
65
  ): Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> {
61
- ;(opts as any).isRedirect = true
62
66
  opts.statusCode = opts.statusCode || opts.code || 307
63
- opts.headers = opts.headers || {}
64
- if (!opts.reloadDocument) {
65
- opts.reloadDocument = false
66
- try {
67
- new URL(`${opts.href}`)
68
- opts.reloadDocument = true
69
- } catch {}
70
- }
67
+ const headers = new Headers(opts.headers || {})
68
+
69
+ const response = new Response(null, {
70
+ status: opts.statusCode,
71
+ headers,
72
+ })
73
+
74
+ ;(response as Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>).options =
75
+ opts
71
76
 
72
77
  if (opts.throw) {
73
- throw opts
78
+ throw response
74
79
  }
75
80
 
76
- return opts
81
+ return response as Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
77
82
  }
78
83
 
79
84
  export function isRedirect(obj: any): obj is AnyRedirect {
80
- return !!obj?.isRedirect
85
+ return obj instanceof Response && !!(obj as any).options
81
86
  }
82
87
 
83
- export function isResolvedRedirect(obj: any): obj is ResolvedRedirect {
84
- return !!obj?.isRedirect && obj.href
88
+ export function isResolvedRedirect(
89
+ obj: any,
90
+ ): obj is AnyRedirect & { options: { href: string } } {
91
+ return isRedirect(obj) && !!obj.options.href
92
+ }
93
+
94
+ export function parseRedirect(obj: any) {
95
+ if (typeof obj === 'object' && obj.isSerializedRedirect) {
96
+ return redirect(obj)
97
+ }
98
+
99
+ return undefined
85
100
  }
package/src/route.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import invariant from 'tiny-invariant'
1
2
  import { joinPaths, trimPathLeft } from './path'
2
3
  import { notFound } from './not-found'
3
4
  import { rootRouteId } from './root'
@@ -20,6 +21,8 @@ import type {
20
21
  Constrain,
21
22
  Expand,
22
23
  IntersectAssign,
24
+ LooseAsyncReturnType,
25
+ LooseReturnType,
23
26
  NoInfer,
24
27
  } from './utils'
25
28
  import type {
@@ -150,27 +153,24 @@ export type ResolveSearchSchema<TSearchValidator> =
150
153
  ? ResolveSearchSchemaFn<TSearchValidator['parse']>
151
154
  : ResolveSearchSchemaFn<TSearchValidator>
152
155
 
153
- export type ParseSplatParams<TPath extends string> = TPath &
154
- `${string}$` extends never
155
- ? TPath & `${string}$/${string}` extends never
156
- ? never
157
- : '_splat'
158
- : '_splat'
156
+ export type ResolveRequiredParams<TPath extends string, T> = {
157
+ [K in ParsePathParams<TPath>['required']]: T
158
+ }
159
159
 
160
- export interface SplatParams {
161
- _splat?: string
160
+ export type ResolveOptionalParams<TPath extends string, T> = {
161
+ [K in ParsePathParams<TPath>['optional']]?: T
162
162
  }
163
163
 
164
- export type ResolveParams<TPath extends string> =
165
- ParseSplatParams<TPath> extends never
166
- ? Record<ParsePathParams<TPath>, string>
167
- : Record<ParsePathParams<TPath>, string> & SplatParams
164
+ export type ResolveParams<
165
+ TPath extends string,
166
+ T = string,
167
+ > = ResolveRequiredParams<TPath, T> & ResolveOptionalParams<TPath, T>
168
168
 
169
169
  export type ParseParamsFn<in out TPath extends string, in out TParams> = (
170
- rawParams: ResolveParams<TPath>,
171
- ) => TParams extends Record<ParsePathParams<TPath>, any>
170
+ rawParams: Expand<ResolveParams<TPath>>,
171
+ ) => TParams extends ResolveParams<TPath, any>
172
172
  ? TParams
173
- : Record<ParsePathParams<TPath>, any>
173
+ : ResolveParams<TPath, any>
174
174
 
175
175
  export type StringifyParamsFn<in out TPath extends string, in out TParams> = (
176
176
  params: TParams,
@@ -270,20 +270,6 @@ export type TrimPathRight<T extends string> = T extends '/'
270
270
  ? TrimPathRight<U>
271
271
  : T
272
272
 
273
- export type LooseReturnType<T> = T extends (
274
- ...args: Array<any>
275
- ) => infer TReturn
276
- ? TReturn
277
- : never
278
-
279
- export type LooseAsyncReturnType<T> = T extends (
280
- ...args: Array<any>
281
- ) => infer TReturn
282
- ? TReturn extends Promise<infer TReturn>
283
- ? TReturn
284
- : TReturn
285
- : never
286
-
287
273
  export type ContextReturnType<TContextFn> = unknown extends TContextFn
288
274
  ? TContextFn
289
275
  : LooseReturnType<TContextFn> extends never
@@ -448,7 +434,7 @@ export interface RouteExtensions<in out TId, in out TFullPath> {
448
434
  }
449
435
 
450
436
  export type RouteLazyFn<TRoute extends AnyRoute> = (
451
- lazyFn: () => Promise<LazyRoute>,
437
+ lazyFn: () => Promise<LazyRoute<TRoute>>,
452
438
  ) => TRoute
453
439
 
454
440
  export type RouteAddChildrenFn<
@@ -602,7 +588,26 @@ export interface Route<
602
588
  >
603
589
  isRoot: TParentRoute extends AnyRoute ? true : false
604
590
  _componentsPromise?: Promise<Array<void>>
605
- lazyFn?: () => Promise<LazyRoute>
591
+ lazyFn?: () => Promise<
592
+ LazyRoute<
593
+ Route<
594
+ TParentRoute,
595
+ TPath,
596
+ TFullPath,
597
+ TCustomId,
598
+ TId,
599
+ TSearchValidator,
600
+ TParams,
601
+ TRouterContext,
602
+ TRouteContextFn,
603
+ TBeforeLoadFn,
604
+ TLoaderDeps,
605
+ TLoaderFn,
606
+ TChildren,
607
+ TFileRouteTypes
608
+ >
609
+ >
610
+ >
606
611
  _lazyPromise?: Promise<void>
607
612
  rank: number
608
613
  to: TrimPathRight<TFullPath>
@@ -621,7 +626,24 @@ export interface Route<
621
626
  TBeforeLoadFn
622
627
  >,
623
628
  ) => this
624
- lazy: RouteLazyFn<this>
629
+ lazy: RouteLazyFn<
630
+ Route<
631
+ TParentRoute,
632
+ TPath,
633
+ TFullPath,
634
+ TCustomId,
635
+ TId,
636
+ TSearchValidator,
637
+ TParams,
638
+ TRouterContext,
639
+ TRouteContextFn,
640
+ TBeforeLoadFn,
641
+ TLoaderDeps,
642
+ TLoaderFn,
643
+ TChildren,
644
+ TFileRouteTypes
645
+ >
646
+ >
625
647
  addChildren: RouteAddChildrenFn<
626
648
  TParentRoute,
627
649
  TPath,
@@ -960,7 +982,7 @@ type AssetFnContextOptions<
960
982
  TLoaderDeps
961
983
  >
962
984
  params: ResolveAllParamsFromParent<TParentRoute, TParams>
963
- loaderData: ResolveLoaderData<TLoaderFn>
985
+ loaderData?: ResolveLoaderData<TLoaderFn>
964
986
  }
965
987
 
966
988
  export interface DefaultUpdatableRouteOptionsExtensions {
@@ -1070,9 +1092,20 @@ export interface UpdatableRouteOptions<
1070
1092
  TLoaderDeps
1071
1093
  >,
1072
1094
  ) => void
1073
- headers?: (ctx: {
1074
- loaderData: ResolveLoaderData<TLoaderFn>
1075
- }) => Record<string, string>
1095
+ headers?: (
1096
+ ctx: AssetFnContextOptions<
1097
+ TRouteId,
1098
+ TFullPath,
1099
+ TParentRoute,
1100
+ TParams,
1101
+ TSearchValidator,
1102
+ TLoaderFn,
1103
+ TRouterContext,
1104
+ TRouteContextFn,
1105
+ TBeforeLoadFn,
1106
+ TLoaderDeps
1107
+ >,
1108
+ ) => Record<string, string>
1076
1109
  head?: (
1077
1110
  ctx: AssetFnContextOptions<
1078
1111
  TRouteId,
@@ -1336,7 +1369,26 @@ export class BaseRoute<
1336
1369
  children?: TChildren
1337
1370
  originalIndex?: number
1338
1371
  rank!: number
1339
- lazyFn?: () => Promise<LazyRoute>
1372
+ lazyFn?: () => Promise<
1373
+ LazyRoute<
1374
+ Route<
1375
+ TParentRoute,
1376
+ TPath,
1377
+ TFullPath,
1378
+ TCustomId,
1379
+ TId,
1380
+ TSearchValidator,
1381
+ TParams,
1382
+ TRouterContext,
1383
+ TRouteContextFn,
1384
+ TBeforeLoadFn,
1385
+ TLoaderDeps,
1386
+ TLoaderFn,
1387
+ TChildren,
1388
+ TFileRouteTypes
1389
+ >
1390
+ >
1391
+ >
1340
1392
  _lazyPromise?: Promise<void>
1341
1393
  _componentsPromise?: Promise<Array<void>>
1342
1394
 
@@ -1409,7 +1461,8 @@ export class BaseRoute<
1409
1461
  if (isRoot) {
1410
1462
  this._path = rootRouteId as TPath
1411
1463
  } else if (!this.parentRoute) {
1412
- throw new Error(
1464
+ invariant(
1465
+ false,
1413
1466
  `Child Route instances must pass a 'getParentRoute: () => ParentRoute' option that returns a Route instance.`,
1414
1467
  )
1415
1468
  }
@@ -1449,6 +1502,16 @@ export class BaseRoute<
1449
1502
  this._ssr = options?.ssr ?? opts.defaultSsr ?? true
1450
1503
  }
1451
1504
 
1505
+ clone = (other: typeof this) => {
1506
+ this._path = other._path
1507
+ this._id = other._id
1508
+ this._fullPath = other._fullPath
1509
+ this._to = other._to
1510
+ this._ssr = other._ssr
1511
+ this.options.getParentRoute = other.options.getParentRoute
1512
+ this.children = other.children
1513
+ }
1514
+
1452
1515
  addChildren: RouteAddChildrenFn<
1453
1516
  TParentRoute,
1454
1517
  TPath,
@@ -1562,7 +1625,24 @@ export class BaseRoute<
1562
1625
  return this
1563
1626
  }
1564
1627
 
1565
- lazy: RouteLazyFn<this> = (lazyFn) => {
1628
+ lazy: RouteLazyFn<
1629
+ Route<
1630
+ TParentRoute,
1631
+ TPath,
1632
+ TFullPath,
1633
+ TCustomId,
1634
+ TId,
1635
+ TSearchValidator,
1636
+ TParams,
1637
+ TRouterContext,
1638
+ TRouteContextFn,
1639
+ TBeforeLoadFn,
1640
+ TLoaderDeps,
1641
+ TLoaderFn,
1642
+ TChildren,
1643
+ TFileRouteTypes
1644
+ >
1645
+ > = (lazyFn) => {
1566
1646
  this.lazyFn = lazyFn
1567
1647
  return this
1568
1648
  }