@tanstack/router-core 0.0.1-alpha.9 → 0.0.1-beta.2

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.9",
4
+ "version": "0.0.1-beta.2",
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']
@@ -38,7 +38,6 @@ export interface RouteMatch<
38
38
  pendingElement?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
39
39
  loadPromise?: Promise<void>
40
40
  loaderPromise?: Promise<void>
41
- importPromise?: Promise<void>
42
41
  elementsPromise?: Promise<void>
43
42
  dataPromise?: Promise<void>
44
43
  pendingTimeout?: Timeout
@@ -61,7 +60,13 @@ export interface RouteMatch<
61
60
  resolve: () => void
62
61
  }
63
62
  cancel: () => void
64
- 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']>
65
70
  invalidate: () => void
66
71
  hasLoaders: () => boolean
67
72
  }
@@ -206,11 +211,42 @@ export function createRouteMatch<
206
211
  hasLoaders: () => {
207
212
  return !!(
208
213
  route.options.loader ||
209
- route.options.import ||
210
214
  elementTypes.some((d) => typeof route.options[d] === 'function')
211
215
  )
212
216
  },
213
- 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) => {
214
250
  const id = '' + Date.now() + Math.random()
215
251
  routeMatch.__.latestId = id
216
252
 
@@ -231,26 +267,7 @@ export function createRouteMatch<
231
267
  routeMatch.__.resolve = resolve as () => void
232
268
 
233
269
  const loaderPromise = (async () => {
234
- const importer = routeMatch.options.import
235
-
236
- // First, run any importers
237
- if (importer) {
238
- routeMatch.__.importPromise = importer({
239
- params: routeMatch.params,
240
- // search: routeMatch.search,
241
- }).then((imported) => {
242
- routeMatch.__ = {
243
- ...routeMatch.__,
244
- ...imported,
245
- }
246
- })
247
- }
248
-
249
- // Wait for the importer to finish before
250
- // attempting to load elements and data
251
- await routeMatch.__.importPromise
252
-
253
- // Next, load the elements and data in parallel
270
+ // Load the elements and data in parallel
254
271
 
255
272
  routeMatch.__.elementsPromise = (async () => {
256
273
  // then run all element and data loaders in parallel
@@ -264,13 +281,9 @@ export function createRouteMatch<
264
281
  return
265
282
  }
266
283
 
267
- if (typeof routeElement === 'function') {
268
- const res = await (routeElement as any)(routeMatch)
269
-
270
- routeMatch.__[type] = res
271
- } else {
272
- routeMatch.__[type] = routeMatch.options[type] as any
273
- }
284
+ routeMatch.__[type] = await router.options.createElement!(
285
+ routeElement,
286
+ )
274
287
  }),
275
288
  )
276
289
  })()