@tanstack/router-core 0.0.1-alpha.10 → 0.0.1-alpha.11

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-alpha.10",
4
+ "version": "0.0.1-alpha.11",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
package/src/frameworks.ts CHANGED
@@ -4,7 +4,6 @@ export interface FrameworkGenerics {
4
4
  // pre-defined as constraints:
5
5
  //
6
6
  // Element: any
7
- // AsyncElement: any
8
7
  // SyncOrAsyncElement?: any
9
8
  }
10
9
 
package/src/link.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  RouteInfoByPath,
6
6
  } from './routeInfo'
7
7
  import { Location } from './router'
8
- import { NoInfer, PickAsRequired, PickRequired, Updater } from './utils'
8
+ import { Expand, NoInfer, PickAsRequired, PickRequired, Updater } from './utils'
9
9
 
10
10
  export type LinkInfo =
11
11
  | {
@@ -35,23 +35,25 @@ type CleanPath<T extends string> = T extends `${infer L}//${infer R}`
35
35
  ? `/${CleanPath<L>}`
36
36
  : T
37
37
 
38
- export type Split<S, TTrailing = true> = S extends unknown
38
+ export type Split<S, TIncludeTrailingSlash = true> = S extends unknown
39
39
  ? string extends S
40
40
  ? string[]
41
41
  : S extends string
42
42
  ? CleanPath<S> extends ''
43
43
  ? []
44
- : TTrailing extends true
44
+ : TIncludeTrailingSlash extends true
45
45
  ? CleanPath<S> extends `${infer T}/`
46
- ? [T, '/']
46
+ ? [...Split<T>, '/']
47
47
  : CleanPath<S> extends `/${infer U}`
48
- ? ['/', U]
48
+ ? Split<U>
49
49
  : CleanPath<S> extends `${infer T}/${infer U}`
50
- ? [T, ...Split<U>]
50
+ ? [...Split<T>, ...Split<U>]
51
51
  : [S]
52
52
  : CleanPath<S> extends `${infer T}/${infer U}`
53
- ? [T, ...Split<U>]
54
- : [S]
53
+ ? [...Split<T>, ...Split<U>]
54
+ : S extends string
55
+ ? [S]
56
+ : never
55
57
  : never
56
58
  : never
57
59
 
@@ -128,7 +130,7 @@ export type ToOptions<
128
130
  from?: TFrom
129
131
  // // When using relative route paths, this option forces resolution from the current path, instead of the route API's path or `from` path
130
132
  // fromCurrent?: boolean
131
- } & CheckPath<TAllRouteInfo, NoInfer<TResolvedTo>> &
133
+ } & CheckPath<TAllRouteInfo, NoInfer<TResolvedTo>, {}> &
132
134
  SearchParamOptions<TAllRouteInfo, TFrom, TResolvedTo> &
133
135
  PathParamOptions<TAllRouteInfo, TFrom, TResolvedTo>
134
136
 
@@ -248,19 +250,41 @@ export type CheckRelativePath<
248
250
  : {}
249
251
  : {}
250
252
 
251
- export type CheckPath<TAllRouteInfo extends AnyAllRouteInfo, TPath> = Exclude<
253
+ export type CheckPath<
254
+ TAllRouteInfo extends AnyAllRouteInfo,
252
255
  TPath,
253
- TAllRouteInfo['routePaths']
254
- > extends never
255
- ? {}
256
+ TPass,
257
+ > = Exclude<TPath, TAllRouteInfo['routePaths']> extends never
258
+ ? TPass
256
259
  : CheckPathError<TAllRouteInfo, Exclude<TPath, TAllRouteInfo['routePaths']>>
257
260
 
258
- export type CheckPathError<TAllRouteInfo extends AnyAllRouteInfo, TInvalids> = {
261
+ export type CheckPathError<
262
+ TAllRouteInfo extends AnyAllRouteInfo,
263
+ TInvalids,
264
+ > = Expand<{
259
265
  Error: `${TInvalids extends string
260
266
  ? TInvalids
261
267
  : never} is not a valid route path.`
262
268
  'Valid Route Paths': TAllRouteInfo['routePaths']
263
- }
269
+ }>
270
+
271
+ export type CheckId<
272
+ TAllRouteInfo extends AnyAllRouteInfo,
273
+ TPath,
274
+ TPass,
275
+ > = Exclude<TPath, TAllRouteInfo['routeIds']> extends never
276
+ ? TPass
277
+ : CheckIdError<TAllRouteInfo, Exclude<TPath, TAllRouteInfo['routeIds']>>
278
+
279
+ export type CheckIdError<
280
+ TAllRouteInfo extends AnyAllRouteInfo,
281
+ TInvalids,
282
+ > = Expand<{
283
+ Error: `${TInvalids extends string
284
+ ? TInvalids
285
+ : never} is not a valid route ID.`
286
+ 'Valid Route IDs': TAllRouteInfo['routeIds']
287
+ }>
264
288
 
265
289
  export type ResolveRelativePath<TFrom, TTo = '.'> = TFrom extends string
266
290
  ? TTo extends string
@@ -274,10 +298,14 @@ export type ResolveRelativePath<TFrom, TTo = '.'> = TFrom extends string
274
298
  ? TTo
275
299
  : Split<TTo> extends ['..', ...infer ToRest]
276
300
  ? Split<TFrom> extends [...infer FromRest, infer FromTail]
277
- ? ResolveRelativePath<Join<FromRest>, Join<ToRest>>
301
+ ? ToRest extends ['/']
302
+ ? Join<[...FromRest, '/']>
303
+ : ResolveRelativePath<Join<FromRest>, Join<ToRest>>
278
304
  : never
279
305
  : Split<TTo> extends ['.', ...infer ToRest]
280
- ? ResolveRelativePath<TFrom, Join<ToRest>>
306
+ ? ToRest extends ['/']
307
+ ? Join<[TFrom, '/']>
308
+ : ResolveRelativePath<TFrom, Join<ToRest>>
281
309
  : CleanPath<Join<['/', ...Split<TFrom>, ...Split<TTo>]>>
282
310
  : never
283
311
  : never
package/src/route.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  ResolveRelativePath,
6
6
  ToOptions,
7
7
  } from './link'
8
- import { RouteConfig, RouteOptions } from './routeConfig'
8
+ import { LoaderContext, RouteConfig, RouteOptions } from './routeConfig'
9
9
  import {
10
10
  AnyAllRouteInfo,
11
11
  AnyRouteInfo,
@@ -14,7 +14,14 @@ import {
14
14
  RouteInfoByPath,
15
15
  } from './routeInfo'
16
16
  import { RouteMatch } from './routeMatch'
17
- import { Action, ActionState, MatchRouteOptions, Router } from './router'
17
+ import {
18
+ Action,
19
+ ActionState,
20
+ Loader,
21
+ LoaderState,
22
+ MatchRouteOptions,
23
+ Router,
24
+ } from './router'
18
25
  import { NoInfer, replaceEqualDeep } from './utils'
19
26
 
20
27
  export interface AnyRoute extends Route<any, any> {}
@@ -57,6 +64,21 @@ export interface Route<
57
64
  | Action<TRouteInfo['actionPayload'], TRouteInfo['actionResponse']>
58
65
  | undefined
59
66
  : Action<TRouteInfo['actionPayload'], TRouteInfo['actionResponse']>
67
+ loader: unknown extends TRouteInfo['routeLoaderData']
68
+ ?
69
+ | Action<
70
+ LoaderContext<
71
+ TRouteInfo['fullSearchSchema'],
72
+ TRouteInfo['allParams']
73
+ >,
74
+ TRouteInfo['routeLoaderData']
75
+ >
76
+ | undefined
77
+ : Loader<
78
+ TRouteInfo['fullSearchSchema'],
79
+ TRouteInfo['allParams'],
80
+ TRouteInfo['routeLoaderData']
81
+ >
60
82
  }
61
83
 
62
84
  export function createRoute<
@@ -126,6 +148,45 @@ export function createRoute<
126
148
  return router.state.actions[id]!
127
149
  })()
128
150
 
151
+ const loader =
152
+ router.state.loaders[id] ||
153
+ (() => {
154
+ router.state.loaders[id] = {
155
+ pending: [],
156
+ fetch: (async (loaderContext: LoaderContext<any, any>) => {
157
+ if (!route) {
158
+ return
159
+ }
160
+
161
+ const loaderState: LoaderState<any, any> = {
162
+ loadedAt: Date.now(),
163
+ loaderContext,
164
+ }
165
+
166
+ loader.current = loaderState
167
+ loader.latest = loaderState
168
+ loader.pending.push(loaderState)
169
+
170
+ // router.state = {
171
+ // ...router.state,
172
+ // currentAction: loaderState,
173
+ // latestAction: loaderState,
174
+ // }
175
+
176
+ router.notify()
177
+
178
+ try {
179
+ return await route.options.loader?.(loaderContext)
180
+ } finally {
181
+ loader.pending = loader.pending.filter((d) => d !== loaderState)
182
+ // router.removeActionQueue.push({ loader, loaderState })
183
+ router.notify()
184
+ }
185
+ }) as any,
186
+ }
187
+ return router.state.loaders[id]!
188
+ })()
189
+
129
190
  let route: Route<TAllRouteInfo, TRouteInfo> = {
130
191
  routeId: id,
131
192
  routeRouteId: routeId,
@@ -136,6 +197,7 @@ export function createRoute<
136
197
  childRoutes: undefined!,
137
198
  parentRoute: parent,
138
199
  action,
200
+ loader: loader as any,
139
201
 
140
202
  buildLink: (options) => {
141
203
  return router.buildLink({
@@ -53,11 +53,18 @@ export type LoaderFn<
53
53
  TRouteLoaderData extends AnyLoaderData,
54
54
  TFullSearchSchema extends AnySearchSchema = {},
55
55
  TAllParams extends AnyPathParams = {},
56
- > = (loaderContext: {
56
+ > = (
57
+ loaderContext: LoaderContext<TFullSearchSchema, TAllParams>,
58
+ ) => Promise<TRouteLoaderData>
59
+
60
+ export interface LoaderContext<
61
+ TFullSearchSchema extends AnySearchSchema = {},
62
+ TAllParams extends AnyPathParams = {},
63
+ > {
57
64
  params: TAllParams
58
65
  search: TFullSearchSchema
59
66
  signal?: AbortSignal
60
- }) => Promise<TRouteLoaderData>
67
+ }
61
68
 
62
69
  export type ActionFn<TActionPayload = unknown, TActionResponse = unknown> = (
63
70
  submission: TActionPayload,
@@ -105,58 +112,6 @@ export type RouteOptions<
105
112
  pendingMs?: number
106
113
  // _If the `pendingElement` is shown_, the minimum duration for which it will be visible.
107
114
  pendingMinMs?: number
108
- // // An array of child routes
109
- // children?: Route<any, any, any, any>[]
110
- } & (
111
- | {
112
- parseParams?: never
113
- stringifyParams?: never
114
- }
115
- | {
116
- // Parse params optionally receives path params as strings and returns them in a parsed format (like a number or boolean)
117
- parseParams: (
118
- rawParams: IsAny<TPath, any, Record<ParsePathParams<TPath>, string>>,
119
- ) => TParams
120
- stringifyParams: (
121
- params: TParams,
122
- ) => Record<ParsePathParams<TPath>, string>
123
- }
124
- ) &
125
- RouteLoaders<
126
- // Route Loaders (see below) can be inline on the route, or resolved async
127
- TRouteLoaderData,
128
- TLoaderData,
129
- TActionPayload,
130
- TActionResponse,
131
- TFullSearchSchema,
132
- TAllParams
133
- > & {
134
- // If `import` is defined, this route can resolve its elements and loaders in a single asynchronous call
135
- // This is particularly useful for code-splitting or module federation
136
- import?: (opts: {
137
- params: AnyPathParams
138
- }) => Promise<
139
- RouteLoaders<
140
- TRouteLoaderData,
141
- TLoaderData,
142
- TActionPayload,
143
- TActionResponse,
144
- TFullSearchSchema,
145
- TAllParams
146
- >
147
- >
148
- } & (PickUnsafe<TParentParams, ParsePathParams<TPath>> extends never // Detect if an existing path param is being redefined
149
- ? {}
150
- : 'Cannot redefined path params in child routes!')
151
-
152
- export interface RouteLoaders<
153
- TRouteLoaderData extends AnyLoaderData = {},
154
- TLoaderData extends AnyLoaderData = {},
155
- TActionPayload = unknown,
156
- TActionResponse = unknown,
157
- TFullSearchSchema extends AnySearchSchema = {},
158
- TAllParams extends AnyPathParams = {},
159
- > {
160
115
  // The content to be rendered when the route is matched. If no element is provided, defaults to `<Outlet />`
161
116
  element?: GetFrameworkGeneric<'SyncOrAsyncElement'> // , NoInfer<TLoaderData>>
162
117
  // The content to be rendered when `loader` encounters an error
@@ -196,7 +151,24 @@ export interface RouteLoaders<
196
151
  }) => void
197
152
  // An object of whatever you want! This object is accessible anywhere matches are.
198
153
  meta?: RouteMeta // TODO: Make this nested and mergeable
199
- }
154
+ } & (
155
+ | {
156
+ parseParams?: never
157
+ stringifyParams?: never
158
+ }
159
+ | {
160
+ // Parse params optionally receives path params as strings and returns them in a parsed format (like a number or boolean)
161
+ parseParams: (
162
+ rawParams: IsAny<TPath, any, Record<ParsePathParams<TPath>, string>>,
163
+ ) => TParams
164
+ stringifyParams: (
165
+ params: TParams,
166
+ ) => Record<ParsePathParams<TPath>, string>
167
+ }
168
+ ) &
169
+ (PickUnsafe<TParentParams, ParsePathParams<TPath>> extends never // Detect if an existing path param is being redefined
170
+ ? {}
171
+ : 'Cannot redefined path params in child routes!')
200
172
 
201
173
  export type SearchFilter<T, U = T> = (prev: T) => U
202
174
 
package/src/routeMatch.ts CHANGED
@@ -16,7 +16,7 @@ export interface RouteMatch<
16
16
  > extends Route<TAllRouteInfo, TRouteInfo> {
17
17
  matchId: string
18
18
  pathname: string
19
- params: AnyPathParams
19
+ params: TRouteInfo['params']
20
20
  parentMatch?: RouteMatch
21
21
  childMatches: RouteMatch[]
22
22
  routeSearch: TRouteInfo['searchSchema']
@@ -60,7 +60,13 @@ export interface RouteMatch<
60
60
  resolve: () => void
61
61
  }
62
62
  cancel: () => void
63
- load: (opts?: { maxAge?: number }) => Promise<void>
63
+ load: (
64
+ loaderOpts?: { withPending?: boolean } & (
65
+ | { preload: true; maxAge: number; gcMaxAge: number }
66
+ | { preload?: false; maxAge?: never; gcMaxAge?: never }
67
+ ),
68
+ ) => Promise<TRouteInfo['routeLoaderData']>
69
+ fetch: (opts?: { maxAge?: number }) => Promise<TRouteInfo['routeLoaderData']>
64
70
  invalidate: () => void
65
71
  hasLoaders: () => boolean
66
72
  }
@@ -208,7 +214,39 @@ export function createRouteMatch<
208
214
  elementTypes.some((d) => typeof route.options[d] === 'function')
209
215
  )
210
216
  },
211
- load: async (opts) => {
217
+ load: async (loaderOpts) => {
218
+ const now = Date.now()
219
+ const minMaxAge = loaderOpts?.preload
220
+ ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge)
221
+ : 0
222
+
223
+ // If this is a preload, add it to the preload cache
224
+ if (loaderOpts?.preload && minMaxAge > 0) {
225
+ // If the match is currently active, don't preload it
226
+ if (
227
+ router.state.matches.find((d) => d.matchId === routeMatch.matchId)
228
+ ) {
229
+ return
230
+ }
231
+
232
+ router.matchCache[routeMatch.matchId] = {
233
+ gc: now + loaderOpts.gcMaxAge,
234
+ match: routeMatch as RouteMatch<any, any>,
235
+ }
236
+ }
237
+
238
+ // If the match is invalid, errored or idle, trigger it to load
239
+ if (
240
+ (routeMatch.status === 'success' && routeMatch.getIsInvalid()) ||
241
+ routeMatch.status === 'error' ||
242
+ routeMatch.status === 'idle'
243
+ ) {
244
+ const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
245
+
246
+ routeMatch.fetch({ maxAge })
247
+ }
248
+ },
249
+ fetch: async (opts) => {
212
250
  const id = '' + Date.now() + Math.random()
213
251
  routeMatch.__.latestId = id
214
252
 
@@ -243,13 +281,9 @@ export function createRouteMatch<
243
281
  return
244
282
  }
245
283
 
246
- if (typeof routeElement === 'function') {
247
- const res = await (routeElement as any)(routeMatch)
248
-
249
- routeMatch.__[type] = res
250
- } else {
251
- routeMatch.__[type] = routeMatch.options[type] as any
252
- }
284
+ routeMatch.__[type] = await router.options.createElement!(
285
+ routeElement,
286
+ )
253
287
  }),
254
288
  )
255
289
  })()
package/src/router.ts CHANGED
@@ -25,8 +25,11 @@ import {
25
25
  } from './path'
26
26
  import { AnyRoute, cascadeLoaderData, createRoute, Route } from './route'
27
27
  import {
28
+ AnyLoaderData,
29
+ AnyPathParams,
28
30
  AnyRouteConfig,
29
31
  AnySearchSchema,
32
+ LoaderContext,
30
33
  RouteConfig,
31
34
  SearchFilter,
32
35
  } from './routeConfig'
@@ -43,6 +46,7 @@ import {
43
46
  functionalUpdate,
44
47
  last,
45
48
  PickAsRequired,
49
+ PickRequired,
46
50
  replaceEqualDeep,
47
51
  Timeout,
48
52
  Updater,
@@ -99,6 +103,11 @@ export interface RouterOptions<TRouteConfig extends AnyRouteConfig> {
99
103
  basepath?: string
100
104
  createRouter?: (router: Router<any, any>) => void
101
105
  createRoute?: (opts: { route: AnyRoute; router: Router<any, any> }) => void
106
+ createElement?: (
107
+ element:
108
+ | GetFrameworkGeneric<'Element'>
109
+ | (() => Promise<GetFrameworkGeneric<'Element'>>),
110
+ ) => Promise<GetFrameworkGeneric<'Element'>>
102
111
  }
103
112
 
104
113
  export interface Action<
@@ -124,6 +133,42 @@ export interface ActionState<
124
133
  error?: unknown
125
134
  }
126
135
 
136
+ export interface Loader<
137
+ TFullSearchSchema extends AnySearchSchema = {},
138
+ TAllParams extends AnyPathParams = {},
139
+ TRouteLoaderData = AnyLoaderData,
140
+ > {
141
+ fetch: keyof PickRequired<TFullSearchSchema> extends never
142
+ ? keyof TAllParams extends never
143
+ ? (loaderContext: { signal?: AbortSignal }) => Promise<TRouteLoaderData>
144
+ : (loaderContext: {
145
+ params: TAllParams
146
+ search?: TFullSearchSchema
147
+ signal?: AbortSignal
148
+ }) => Promise<TRouteLoaderData>
149
+ : keyof TAllParams extends never
150
+ ? (loaderContext: {
151
+ search: TFullSearchSchema
152
+ params: TAllParams
153
+ signal?: AbortSignal
154
+ }) => Promise<TRouteLoaderData>
155
+ : (loaderContext: {
156
+ search: TFullSearchSchema
157
+ signal?: AbortSignal
158
+ }) => Promise<TRouteLoaderData>
159
+ current?: LoaderState<TFullSearchSchema, TAllParams>
160
+ latest?: LoaderState<TFullSearchSchema, TAllParams>
161
+ pending: LoaderState<TFullSearchSchema, TAllParams>[]
162
+ }
163
+
164
+ export interface LoaderState<
165
+ TFullSearchSchema = unknown,
166
+ TAllParams = unknown,
167
+ > {
168
+ loadedAt: number
169
+ loaderContext: LoaderContext<TFullSearchSchema, TAllParams>
170
+ }
171
+
127
172
  export interface RouterState {
128
173
  status: 'idle' | 'loading'
129
174
  location: Location
@@ -133,6 +178,7 @@ export interface RouterState {
133
178
  currentAction?: ActionState
134
179
  latestAction?: ActionState
135
180
  actions: Record<string, Action>
181
+ loaders: Record<string, Loader>
136
182
  pending?: PendingState
137
183
  isFetching: boolean
138
184
  isPreloading: boolean
@@ -322,6 +368,7 @@ export function createRouter<
322
368
  location: null!,
323
369
  matches: [],
324
370
  actions: {},
371
+ loaders: {},
325
372
  loaderData: {} as any,
326
373
  lastUpdated: Date.now(),
327
374
  isFetching: false,
@@ -705,38 +752,10 @@ export function createRouter<
705
752
  },
706
753
 
707
754
  loadMatches: async (resolvedMatches, loaderOpts) => {
708
- const now = Date.now()
709
- const minMaxAge = loaderOpts?.preload
710
- ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge)
711
- : 0
712
-
713
755
  const matchPromises = resolvedMatches.map(async (match) => {
714
756
  // Validate the match (loads search params etc)
715
757
  match.__.validate()
716
-
717
- // If this is a preload, add it to the preload cache
718
- if (loaderOpts?.preload && minMaxAge > 0) {
719
- // If the match is currently active, don't preload it
720
- if (router.state.matches.find((d) => d.matchId === match.matchId)) {
721
- return
722
- }
723
-
724
- router.matchCache[match.matchId] = {
725
- gc: now + loaderOpts.gcMaxAge,
726
- match,
727
- }
728
- }
729
-
730
- // If the match is invalid, errored or idle, trigger it to load
731
- if (
732
- (match.status === 'success' && match.getIsInvalid()) ||
733
- match.status === 'error' ||
734
- match.status === 'idle'
735
- ) {
736
- const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
737
-
738
- match.load({ maxAge })
739
- }
758
+ match.load(loaderOpts)
740
759
 
741
760
  if (match.status === 'loading') {
742
761
  // If requested, start the pending timers