@tanstack/router-core 0.0.1-beta.45 → 0.0.1-beta.49

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 (44) hide show
  1. package/build/cjs/actions.js +94 -0
  2. package/build/cjs/actions.js.map +1 -0
  3. package/build/cjs/history.js +163 -0
  4. package/build/cjs/history.js.map +1 -0
  5. package/build/cjs/index.js +18 -20
  6. package/build/cjs/index.js.map +1 -1
  7. package/build/cjs/interop.js +175 -0
  8. package/build/cjs/interop.js.map +1 -0
  9. package/build/cjs/path.js +4 -5
  10. package/build/cjs/path.js.map +1 -1
  11. package/build/cjs/route.js +16 -138
  12. package/build/cjs/route.js.map +1 -1
  13. package/build/cjs/routeConfig.js +1 -7
  14. package/build/cjs/routeConfig.js.map +1 -1
  15. package/build/cjs/routeMatch.js +194 -199
  16. package/build/cjs/routeMatch.js.map +1 -1
  17. package/build/cjs/router.js +726 -703
  18. package/build/cjs/router.js.map +1 -1
  19. package/build/cjs/store.js +54 -0
  20. package/build/cjs/store.js.map +1 -0
  21. package/build/esm/index.js +1305 -1114
  22. package/build/esm/index.js.map +1 -1
  23. package/build/stats-html.html +1 -1
  24. package/build/stats-react.json +229 -192
  25. package/build/types/index.d.ts +172 -109
  26. package/build/umd/index.development.js +1381 -2331
  27. package/build/umd/index.development.js.map +1 -1
  28. package/build/umd/index.production.js +1 -1
  29. package/build/umd/index.production.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/actions.ts +157 -0
  32. package/src/history.ts +199 -0
  33. package/src/index.ts +4 -7
  34. package/src/interop.ts +169 -0
  35. package/src/link.ts +2 -2
  36. package/src/route.ts +34 -239
  37. package/src/routeConfig.ts +3 -34
  38. package/src/routeInfo.ts +6 -21
  39. package/src/routeMatch.ts +270 -285
  40. package/src/router.ts +967 -963
  41. package/src/store.ts +52 -0
  42. package/build/cjs/sharedClone.js +0 -122
  43. package/build/cjs/sharedClone.js.map +0 -1
  44. package/src/sharedClone.ts +0 -118
package/src/routeMatch.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { boolean } from 'zod'
2
1
  import { GetFrameworkGeneric } from './frameworks'
3
2
  import { Route } from './route'
4
3
  import {
@@ -7,16 +6,15 @@ import {
7
6
  DefaultAllRouteInfo,
8
7
  RouteInfo,
9
8
  } from './routeInfo'
10
- import { Router } from './router'
11
- import { batch, createStore } from '@solidjs/reactivity'
9
+ import { AnyRouter, Router } from './router'
10
+ import { batch, createStore, Store } from './store'
12
11
  import { Expand } from './utils'
13
- import { sharedClone } from './sharedClone'
12
+ import { replaceEqualDeep } from './interop'
14
13
 
15
14
  export interface RouteMatchStore<
16
15
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
17
16
  TRouteInfo extends AnyRouteInfo = RouteInfo,
18
17
  > {
19
- parentMatch?: RouteMatch
20
18
  routeSearch: TRouteInfo['searchSchema']
21
19
  search: Expand<
22
20
  TAllRouteInfo['fullSearchSchema'] & TRouteInfo['fullSearchSchema']
@@ -25,337 +23,324 @@ export interface RouteMatchStore<
25
23
  updatedAt?: number
26
24
  error?: unknown
27
25
  invalid: boolean
28
- isInvalid: boolean
29
26
  loaderData: TRouteInfo['loaderData']
30
27
  routeLoaderData: TRouteInfo['routeLoaderData']
31
28
  isFetching: boolean
32
29
  invalidAt: number
33
30
  }
34
31
 
35
- export interface RouteMatch<
36
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
37
- TRouteInfo extends AnyRouteInfo = RouteInfo,
38
- > extends Route<TAllRouteInfo, TRouteInfo> {
39
- store: RouteMatchStore<TAllRouteInfo, TRouteInfo>
40
- // setStore: WritableStore<RouteMatchStore<TAllRouteInfo, TRouteInfo>>
41
- matchId: string
42
- pathname: string
43
- params: TRouteInfo['allParams']
44
- childMatches: RouteMatch[]
45
- cancel: () => void
46
- load: (
47
- loaderOpts?:
48
- | { preload: true; maxAge: number; gcMaxAge: number }
49
- | { preload?: false; maxAge?: never; gcMaxAge?: never },
50
- ) => Promise<TRouteInfo['routeLoaderData']>
51
- fetch: (opts?: { maxAge?: number }) => Promise<TRouteInfo['routeLoaderData']>
52
- invalidate: () => void
53
- hasLoaders: () => boolean
54
- __: {
55
- setParentMatch: (parentMatch?: RouteMatch) => void
56
- component?: GetFrameworkGeneric<'Component'>
57
- errorComponent?: GetFrameworkGeneric<'ErrorComponent'>
58
- pendingComponent?: GetFrameworkGeneric<'Component'>
59
- loadPromise?: Promise<void>
60
- onExit?:
61
- | void
62
- | ((matchContext: {
63
- params: TRouteInfo['allParams']
64
- search: TRouteInfo['fullSearchSchema']
65
- }) => void)
66
- abortController: AbortController
67
- validate: () => void
68
- }
69
- }
70
-
71
32
  const componentTypes = [
72
33
  'component',
73
34
  'errorComponent',
74
35
  'pendingComponent',
75
36
  ] as const
76
37
 
77
- export function createRouteMatch<
38
+ export class RouteMatch<
78
39
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
79
40
  TRouteInfo extends AnyRouteInfo = RouteInfo,
80
- >(
81
- router: Router<any, any, any>,
82
- route: Route<TAllRouteInfo, TRouteInfo>,
83
- opts: {
84
- parentMatch?: RouteMatch<any, any>
85
- matchId: string
86
- params: TRouteInfo['allParams']
87
- pathname: string
88
- },
89
- ): RouteMatch<TAllRouteInfo, TRouteInfo> {
90
- let componentsPromise: Promise<void>
91
- let dataPromise: Promise<TRouteInfo['routeLoaderData']>
92
- let latestId = ''
93
- let resolve = () => {}
94
-
95
- function setLoaderData(loaderData: TRouteInfo['routeLoaderData']) {
41
+ > {
42
+ route!: Route<TAllRouteInfo, TRouteInfo>
43
+ router!: Router<TAllRouteInfo['routeConfig'], TAllRouteInfo>
44
+ store!: Store<RouteMatchStore<TAllRouteInfo, TRouteInfo>>
45
+ id!: string
46
+ pathname!: string
47
+ params!: TRouteInfo['allParams']
48
+
49
+ component: GetFrameworkGeneric<'Component'>
50
+ errorComponent: GetFrameworkGeneric<'ErrorComponent'>
51
+ pendingComponent: GetFrameworkGeneric<'Component'>
52
+ abortController = new AbortController()
53
+ #latestId = ''
54
+ #resolve = () => {}
55
+ onLoaderDataListeners = new Set<() => void>()
56
+ parentMatch?: RouteMatch
57
+
58
+ __loadPromise?: Promise<void>
59
+ __onExit?:
60
+ | void
61
+ | ((matchContext: {
62
+ params: TRouteInfo['allParams']
63
+ search: TRouteInfo['fullSearchSchema']
64
+ }) => void)
65
+
66
+ constructor(
67
+ router: AnyRouter,
68
+ route: Route<TAllRouteInfo, TRouteInfo>,
69
+ opts: {
70
+ matchId: string
71
+ params: TRouteInfo['allParams']
72
+ pathname: string
73
+ },
74
+ ) {
75
+ Object.assign(this, {
76
+ route,
77
+ router,
78
+ matchId: opts.matchId,
79
+ pathname: opts.pathname,
80
+ params: opts.params,
81
+ store: createStore<RouteMatchStore<TAllRouteInfo, TRouteInfo>>({
82
+ routeSearch: {},
83
+ search: {} as any,
84
+ status: 'idle',
85
+ routeLoaderData: {} as TRouteInfo['routeLoaderData'],
86
+ loaderData: {} as TRouteInfo['loaderData'],
87
+ isFetching: false,
88
+ invalid: false,
89
+ invalidAt: Infinity,
90
+ }),
91
+ })
92
+
93
+ if (!this.__hasLoaders()) {
94
+ this.store.setState((s) => (s.status = 'success'))
95
+ }
96
+ }
97
+
98
+ #setLoaderData = (loaderData: TRouteInfo['routeLoaderData']) => {
96
99
  batch(() => {
97
- setStore((s) => {
98
- s.routeLoaderData = sharedClone(s.routeLoaderData, loaderData)
100
+ this.store.setState((s) => {
101
+ s.routeLoaderData = loaderData
99
102
  })
100
- updateLoaderData()
103
+ this.#updateLoaderData()
101
104
  })
102
105
  }
103
106
 
104
- function updateLoaderData() {
105
- setStore((s) => {
106
- s.loaderData = sharedClone(s.loaderData, {
107
- ...store.parentMatch?.store.loaderData,
108
- ...s.routeLoaderData,
109
- }) as TRouteInfo['loaderData']
110
- })
107
+ cancel = () => {
108
+ this.abortController?.abort()
111
109
  }
112
110
 
113
- const [store, setStore] = createStore<
114
- RouteMatchStore<TAllRouteInfo, TRouteInfo>
115
- >({
116
- routeSearch: {},
117
- search: {} as any,
118
- status: 'idle',
119
- routeLoaderData: {} as TRouteInfo['routeLoaderData'],
120
- loaderData: {} as TRouteInfo['loaderData'],
121
- isFetching: false,
122
- invalid: false,
123
- invalidAt: Infinity,
124
- get isInvalid(): boolean {
125
- const now = Date.now()
126
- return this.invalid || this.invalidAt < now
127
- },
128
- })
129
-
130
- const routeMatch: RouteMatch<TAllRouteInfo, TRouteInfo> = {
131
- ...route,
132
- ...opts,
133
- store,
134
- // setStore,
135
- router,
136
- childMatches: [],
137
- __: {
138
- setParentMatch: (parentMatch?: RouteMatch) => {
139
- batch(() => {
140
- setStore((s) => {
141
- s.parentMatch = parentMatch
142
- })
111
+ load = async (
112
+ loaderOpts?:
113
+ | { preload: true; maxAge: number; gcMaxAge: number }
114
+ | { preload?: false; maxAge?: never; gcMaxAge?: never },
115
+ ): Promise<void> => {
116
+ const now = Date.now()
117
+ const minMaxAge = loaderOpts?.preload
118
+ ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge)
119
+ : 0
120
+
121
+ // If this is a preload, add it to the preload cache
122
+ if (loaderOpts?.preload && minMaxAge > 0) {
123
+ // If the match is currently active, don't preload it
124
+ if (
125
+ this.router.store.state.currentMatches.find((d) => d.id === this.id)
126
+ ) {
127
+ return
128
+ }
143
129
 
144
- updateLoaderData()
145
- })
146
- },
147
- abortController: new AbortController(),
148
- validate: () => {
149
- // Validate the search params and stabilize them
150
- const parentSearch =
151
- store.parentMatch?.store.search ?? router.store.currentLocation.search
130
+ this.router.store.setState((s) => {
131
+ s.matchCache[this.id] = {
132
+ gc: now + loaderOpts.gcMaxAge,
133
+ match: this as RouteMatch<any, any>,
134
+ }
135
+ })
136
+ }
137
+
138
+ // If the match is invalid, errored or idle, trigger it to load
139
+ if (
140
+ (this.store.state.status === 'success' && this.getIsInvalid()) ||
141
+ this.store.state.status === 'error' ||
142
+ this.store.state.status === 'idle'
143
+ ) {
144
+ const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
145
+ await this.fetch({ maxAge })
146
+ }
147
+ }
152
148
 
153
- try {
154
- const prevSearch = store.routeSearch
155
-
156
- const validator =
157
- typeof routeMatch.options.validateSearch === 'object'
158
- ? routeMatch.options.validateSearch.parse
159
- : routeMatch.options.validateSearch
160
-
161
- let nextSearch = sharedClone(
162
- prevSearch,
163
- validator?.(parentSearch) ?? {},
164
- )
165
-
166
- batch(() => {
167
- // Invalidate route matches when search param stability changes
168
- if (prevSearch !== nextSearch) {
169
- setStore((s) => (s.invalid = true))
170
- }
149
+ fetch = async (opts?: {
150
+ maxAge?: number
151
+ }): Promise<TRouteInfo['routeLoaderData']> => {
152
+ this.__loadPromise = new Promise(async (resolve) => {
153
+ const loadId = '' + Date.now() + Math.random()
154
+ this.#latestId = loadId
171
155
 
172
- // TODO: Alright, do we need batch() here?
173
- setStore((s) => {
174
- s.routeSearch = nextSearch
175
- s.search = sharedClone(parentSearch, {
176
- ...parentSearch,
177
- ...nextSearch,
178
- })
179
- })
180
- })
156
+ const checkLatest = () =>
157
+ loadId !== this.#latestId
158
+ ? this.__loadPromise?.then(() => resolve())
159
+ : undefined
160
+
161
+ let latestPromise
181
162
 
163
+ batch(() => {
164
+ // If the match was in an error state, set it
165
+ // to a loading state again. Otherwise, keep it
166
+ // as loading or resolved
167
+ if (this.store.state.status === 'idle') {
168
+ this.store.setState((s) => (s.status = 'loading'))
169
+ }
170
+
171
+ // We started loading the route, so it's no longer invalid
172
+ this.store.setState((s) => (s.invalid = false))
173
+ })
174
+
175
+ // We are now fetching, even if it's in the background of a
176
+ // resolved state
177
+ this.store.setState((s) => (s.isFetching = true))
178
+ this.#resolve = resolve as () => void
179
+
180
+ const componentsPromise = (async () => {
181
+ // then run all component and data loaders in parallel
182
+ // For each component type, potentially load it asynchronously
183
+
184
+ await Promise.all(
182
185
  componentTypes.map(async (type) => {
183
- const component = routeMatch.options[type]
186
+ const component = this.route.options[type]
184
187
 
185
- if (typeof routeMatch.__[type] !== 'function') {
186
- routeMatch.__[type] = component
188
+ if (this[type]?.preload) {
189
+ this[type] = await this.router.options.loadComponent!(component)
187
190
  }
191
+ }),
192
+ )
193
+ })()
194
+
195
+ const dataPromise = Promise.resolve().then(async () => {
196
+ try {
197
+ if (this.route.options.loader) {
198
+ const data = await this.router.loadMatchData(this)
199
+ if ((latestPromise = checkLatest())) return latestPromise
200
+
201
+ this.#setLoaderData(data)
202
+ }
203
+
204
+ this.store.setState((s) => {
205
+ s.error = undefined
206
+ s.status = 'success'
207
+ s.updatedAt = Date.now()
208
+ s.invalidAt =
209
+ s.updatedAt +
210
+ (opts?.maxAge ??
211
+ this.route.options.loaderMaxAge ??
212
+ this.router.options.defaultLoaderMaxAge ??
213
+ 0)
188
214
  })
189
- } catch (err: any) {
190
- console.error(err)
191
- const error = new (Error as any)('Invalid search params found', {
192
- cause: err,
193
- })
194
- error.code = 'INVALID_SEARCH_PARAMS'
195
215
 
196
- setStore((s) => {
216
+ return this.store.state.routeLoaderData
217
+ } catch (err) {
218
+ if ((latestPromise = checkLatest())) return latestPromise
219
+
220
+ if (process.env.NODE_ENV !== 'production') {
221
+ console.error(err)
222
+ }
223
+
224
+ this.store.setState((s) => {
225
+ s.error = err
197
226
  s.status = 'error'
198
- s.error = error
227
+ s.updatedAt = Date.now()
199
228
  })
200
229
 
201
- // Do not proceed with loading the route
202
- return
203
- }
204
- },
205
- },
206
- cancel: () => {
207
- routeMatch.__.abortController?.abort()
208
- },
209
- invalidate: () => {
210
- setStore((s) => (s.invalid = true))
211
- },
212
- hasLoaders: () => {
213
- return !!(
214
- route.options.loader ||
215
- componentTypes.some((d) => route.options[d]?.preload)
216
- )
217
- },
218
- load: async (loaderOpts) => {
219
- const now = Date.now()
220
- const minMaxAge = loaderOpts?.preload
221
- ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge)
222
- : 0
223
-
224
- // If this is a preload, add it to the preload cache
225
- if (loaderOpts?.preload && minMaxAge > 0) {
226
- // If the match is currently active, don't preload it
227
- if (
228
- router.store.currentMatches.find(
229
- (d) => d.matchId === routeMatch.matchId,
230
- )
231
- ) {
232
- return
230
+ throw err
233
231
  }
232
+ })
234
233
 
235
- router.store.matchCache[routeMatch.matchId] = {
236
- gc: now + loaderOpts.gcMaxAge,
237
- match: routeMatch as RouteMatch<any, any>,
238
- }
234
+ const after = async () => {
235
+ if ((latestPromise = checkLatest())) return latestPromise
236
+ this.store.setState((s) => (s.isFetching = false))
237
+ this.#resolve()
238
+ delete this.__loadPromise
239
239
  }
240
240
 
241
- // If the match is invalid, errored or idle, trigger it to load
242
- if (
243
- (store.status === 'success' && store.isInvalid) ||
244
- store.status === 'error' ||
245
- store.status === 'idle'
246
- ) {
247
- const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
248
-
249
- await routeMatch.fetch({ maxAge })
250
- }
251
- },
252
- fetch: async (opts) => {
253
- const loadId = '' + Date.now() + Math.random()
254
- latestId = loadId
255
- const checkLatest = async () => {
256
- if (loadId !== latestId) {
257
- // warning(true, 'Data loader is out of date!')
258
- return new Promise(() => {})
259
- }
241
+ try {
242
+ await Promise.all([componentsPromise, dataPromise.catch(() => {})])
243
+ after()
244
+ } catch {
245
+ after()
260
246
  }
247
+ })
261
248
 
262
- batch(() => {
263
- // If the match was in an error state, set it
264
- // to a loading state again. Otherwise, keep it
265
- // as loading or resolved
266
- if (store.status === 'idle') {
267
- setStore((s) => (s.status = 'loading'))
268
- }
249
+ return this.__loadPromise
250
+ }
251
+ invalidate = async () => {
252
+ this.store.setState((s) => (s.invalid = true))
253
+ if (this.router.store.state.currentMatches.find((d) => d.id === this.id)) {
254
+ await this.load()
255
+ }
256
+ }
257
+ __hasLoaders = () => {
258
+ return !!(
259
+ this.route.options.loader ||
260
+ componentTypes.some((d) => this.route.options[d]?.preload)
261
+ )
262
+ }
263
+ getIsInvalid = () => {
264
+ const now = Date.now()
265
+ return this.store.state.invalid || this.store.state.invalidAt < now
266
+ }
269
267
 
270
- // We started loading the route, so it's no longer invalid
271
- setStore((s) => (s.invalid = false))
268
+ #updateLoaderData = () => {
269
+ this.store.setState((s) => {
270
+ s.loaderData = replaceEqualDeep(s.loaderData, {
271
+ ...this.parentMatch?.store.state.loaderData,
272
+ ...s.routeLoaderData,
273
+ }) as TRouteInfo['loaderData']
274
+ })
275
+ this.onLoaderDataListeners.forEach((listener) => listener())
276
+ }
277
+
278
+ __setParentMatch = (parentMatch?: RouteMatch) => {
279
+ if (!this.parentMatch && parentMatch) {
280
+ this.parentMatch = parentMatch
281
+ this.parentMatch.__onLoaderData(() => {
282
+ this.#updateLoaderData()
272
283
  })
284
+ }
285
+ }
273
286
 
274
- routeMatch.__.loadPromise = new Promise(async (r) => {
275
- // We are now fetching, even if it's in the background of a
276
- // resolved state
277
- setStore((s) => (s.isFetching = true))
278
- resolve = r as () => void
279
-
280
- componentsPromise = (async () => {
281
- // then run all component and data loaders in parallel
282
- // For each component type, potentially load it asynchronously
283
-
284
- await Promise.all(
285
- componentTypes.map(async (type) => {
286
- const component = routeMatch.options[type]
287
-
288
- if (routeMatch.__[type]?.preload) {
289
- routeMatch.__[type] = await router.options.loadComponent!(
290
- component,
291
- )
292
- }
293
- }),
294
- )
295
- })()
296
-
297
- dataPromise = Promise.resolve().then(async () => {
298
- try {
299
- if (routeMatch.options.loader) {
300
- const data = await router.loadMatchData(routeMatch)
301
- await checkLatest()
302
-
303
- setLoaderData(data)
304
- }
287
+ __onLoaderData = (listener: () => void) => {
288
+ this.onLoaderDataListeners.add(listener)
289
+ // return () => this.onLoaderDataListeners.delete(listener)
290
+ }
305
291
 
306
- setStore((s) => {
307
- s.error = undefined
308
- s.status = 'success'
309
- s.updatedAt = Date.now()
310
- s.invalidAt =
311
- s.updatedAt +
312
- (opts?.maxAge ??
313
- routeMatch.options.loaderMaxAge ??
314
- router.options.defaultLoaderMaxAge ??
315
- 0)
316
- })
317
-
318
- return store.routeLoaderData
319
- } catch (err) {
320
- await checkLatest()
321
-
322
- if (process.env.NODE_ENV !== 'production') {
323
- console.error(err)
324
- }
292
+ __validate = () => {
293
+ // Validate the search params and stabilize them
294
+ const parentSearch =
295
+ this.parentMatch?.store.state.search ??
296
+ this.router.store.state.latestLocation.search
325
297
 
326
- setStore((s) => {
327
- s.error = err
328
- s.status = 'error'
329
- s.updatedAt = Date.now()
330
- })
298
+ try {
299
+ const prevSearch = this.store.state.routeSearch
331
300
 
332
- throw err
333
- }
334
- })
301
+ const validator =
302
+ typeof this.route.options.validateSearch === 'object'
303
+ ? this.route.options.validateSearch.parse
304
+ : this.route.options.validateSearch
305
+
306
+ let nextSearch = validator?.(parentSearch) ?? {}
335
307
 
336
- const after = async () => {
337
- await checkLatest()
338
- setStore((s) => (s.isFetching = false))
339
- delete routeMatch.__.loadPromise
340
- resolve()
308
+ batch(() => {
309
+ // Invalidate route matches when search param stability changes
310
+ if (prevSearch !== nextSearch) {
311
+ this.store.setState((s) => (s.invalid = true))
341
312
  }
342
313
 
343
- try {
344
- await Promise.all([componentsPromise, dataPromise.catch(() => {})])
345
- after()
346
- } catch {
347
- after()
314
+ this.store.setState((s) => {
315
+ s.routeSearch = nextSearch
316
+ s.search = {
317
+ ...parentSearch,
318
+ ...nextSearch,
319
+ } as any
320
+ })
321
+ })
322
+
323
+ componentTypes.map(async (type) => {
324
+ const component = this.route.options[type]
325
+
326
+ if (typeof this[type] !== 'function') {
327
+ this[type] = component
348
328
  }
349
329
  })
330
+ } catch (err: any) {
331
+ console.error(err)
332
+ const error = new (Error as any)('Invalid search params found', {
333
+ cause: err,
334
+ })
335
+ error.code = 'INVALID_SEARCH_PARAMS'
350
336
 
351
- await routeMatch.__.loadPromise
352
- await checkLatest()
353
- },
354
- }
337
+ this.store.setState((s) => {
338
+ s.status = 'error'
339
+ s.error = error
340
+ })
355
341
 
356
- if (!routeMatch.hasLoaders()) {
357
- setStore((s) => (s.status = 'success'))
342
+ // Do not proceed with loading the route
343
+ return
344
+ }
358
345
  }
359
-
360
- return routeMatch
361
346
  }