@tanstack/react-router 0.0.1-alpha.0

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/src/index.tsx ADDED
@@ -0,0 +1,568 @@
1
+ import * as React from 'react'
2
+
3
+ import { useSyncExternalStore } from 'use-sync-external-store/shim'
4
+
5
+ import {
6
+ AnyRoute,
7
+ RootRouteId,
8
+ rootRouteId,
9
+ Route,
10
+ Router,
11
+ } from '@tanstack/router-core'
12
+ import {
13
+ warning,
14
+ RouterOptions,
15
+ RouteMatch,
16
+ MatchRouteOptions,
17
+ RouteConfig,
18
+ AnyRouteConfig,
19
+ AnyAllRouteInfo,
20
+ DefaultAllRouteInfo,
21
+ functionalUpdate,
22
+ createRouter,
23
+ AnyRouteInfo,
24
+ AllRouteInfo,
25
+ RouteInfo,
26
+ ValidFromPath,
27
+ LinkOptions,
28
+ RouteInfoByPath,
29
+ ResolveRelativePath,
30
+ NoInfer,
31
+ ToOptions,
32
+ } from '@tanstack/router-core'
33
+
34
+ export * from '@tanstack/router-core'
35
+
36
+ declare module '@tanstack/router-core' {
37
+ interface FrameworkGenerics {
38
+ Element: React.ReactNode
39
+ AsyncElement: (opts: {
40
+ params: Record<string, string>
41
+ }) => Promise<React.ReactNode>
42
+ SyncOrAsyncElement: React.ReactNode | FrameworkGenerics['AsyncElement']
43
+ }
44
+
45
+ interface Router<
46
+ TRouteConfig extends AnyRouteConfig = RouteConfig,
47
+ TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
48
+ > extends Pick<
49
+ Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][RootRouteId]>,
50
+ 'linkProps' | 'Link' | 'MatchRoute'
51
+ > {
52
+ useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
53
+ routeId: TId,
54
+ ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
55
+ useMatch: <TId extends keyof TAllRouteInfo['routeInfoById']>(
56
+ routeId: TId,
57
+ ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
58
+ // linkProps: <TTo extends string = '.'>(
59
+ // props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
60
+ // React.AnchorHTMLAttributes<HTMLAnchorElement>,
61
+ // ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
62
+ // Link: <TTo extends string = '.'>(
63
+ // props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
64
+ // React.AnchorHTMLAttributes<HTMLAnchorElement> &
65
+ // Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
66
+ // // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
67
+ // children?:
68
+ // | React.ReactNode
69
+ // | ((state: { isActive: boolean }) => React.ReactNode)
70
+ // },
71
+ // ) => JSX.Element
72
+ // MatchRoute: <TTo extends string = '.'>(
73
+ // props: ToOptions<TAllRouteInfo, '/', TTo> &
74
+ // MatchRouteOptions & {
75
+ // // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
76
+ // children?:
77
+ // | React.ReactNode
78
+ // | ((
79
+ // params: RouteInfoByPath<
80
+ // TAllRouteInfo,
81
+ // ResolveRelativePath<'/', NoInfer<TTo>>
82
+ // >['allParams'],
83
+ // ) => React.ReactNode)
84
+ // },
85
+ // ) => JSX.Element
86
+ }
87
+
88
+ interface Route<
89
+ TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
90
+ TRouteInfo extends AnyRouteInfo = RouteInfo,
91
+ > {
92
+ linkProps: <TTo extends string = '.'>(
93
+ props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
94
+ React.AnchorHTMLAttributes<HTMLAnchorElement>,
95
+ ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
96
+ Link: <TTo extends string = '.'>(
97
+ props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
98
+ React.AnchorHTMLAttributes<HTMLAnchorElement> &
99
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
100
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
101
+ children?:
102
+ | React.ReactNode
103
+ | ((state: { isActive: boolean }) => React.ReactNode)
104
+ },
105
+ ) => JSX.Element
106
+ MatchRoute: <TTo extends string = '.'>(
107
+ props: ToOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
108
+ MatchRouteOptions & {
109
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
110
+ children?:
111
+ | React.ReactNode
112
+ | ((
113
+ params: RouteInfoByPath<
114
+ TAllRouteInfo,
115
+ ResolveRelativePath<TRouteInfo['fullPath'], NoInfer<TTo>>
116
+ >['allParams'],
117
+ ) => React.ReactNode)
118
+ },
119
+ ) => JSX.Element
120
+ }
121
+ }
122
+
123
+ type LinkPropsOptions<
124
+ TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
125
+ TFrom extends ValidFromPath<TAllRouteInfo> = '/',
126
+ TTo extends string = '.',
127
+ > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
128
+ // A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
129
+ activeProps?:
130
+ | React.AnchorHTMLAttributes<HTMLAnchorElement>
131
+ | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
132
+ // A function that returns additional props for the `inactive` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
133
+ inactiveProps?:
134
+ | React.AnchorHTMLAttributes<HTMLAnchorElement>
135
+ | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
136
+ }
137
+
138
+ export type PromptProps = {
139
+ message: string
140
+ when?: boolean | any
141
+ children?: React.ReactNode
142
+ }
143
+
144
+ //
145
+
146
+ const matchesContext = React.createContext<RouteMatch[]>(null!)
147
+ const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
148
+
149
+ // Detect if we're in the DOM
150
+ const isDOM = Boolean(
151
+ typeof window !== 'undefined' &&
152
+ window.document &&
153
+ window.document.createElement,
154
+ )
155
+
156
+ const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
157
+
158
+ export type MatchesProviderProps = {
159
+ value: RouteMatch[]
160
+ children: React.ReactNode
161
+ }
162
+
163
+ export function MatchesProvider(props: MatchesProviderProps) {
164
+ return <matchesContext.Provider {...props} />
165
+ }
166
+
167
+ const useRouterSubscription = (router: Router<any, any>) => {
168
+ useSyncExternalStore(
169
+ (cb) => router.subscribe(() => cb()),
170
+ () => router.state,
171
+ )
172
+ }
173
+
174
+ export function createReactRouter<
175
+ TRouteConfig extends AnyRouteConfig = RouteConfig,
176
+ >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
177
+ const coreRouter = createRouter<TRouteConfig>({
178
+ ...opts,
179
+ createRouter: (router) => {
180
+ const routerExt: Pick<Router<any, any>, 'useRoute' | 'useMatch'> = {
181
+ useRoute: (routeId) => {
182
+ const route = router.getRoute(routeId)
183
+ useRouterSubscription(router)
184
+ if (!route) {
185
+ throw new Error(
186
+ `Could not find a route for route "${
187
+ routeId as string
188
+ }"! Did you forget to add it to your route config?`,
189
+ )
190
+ }
191
+ return route
192
+ },
193
+ useMatch: (routeId) => {
194
+ if (routeId === rootRouteId) {
195
+ throw new Error(
196
+ `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
197
+ )
198
+ }
199
+ const runtimeMatch = useMatch()
200
+ const match = router.state.matches.find((d) => d.routeId === routeId)
201
+
202
+ if (!match) {
203
+ throw new Error(
204
+ `Could not find a match for route "${
205
+ routeId as string
206
+ }" being rendered in this component!`,
207
+ )
208
+ }
209
+
210
+ if (runtimeMatch.routeId !== match?.routeId) {
211
+ throw new Error(
212
+ `useMatch('${
213
+ match?.routeId as string
214
+ }') is being called in a component that is meant to render the '${
215
+ runtimeMatch.routeId
216
+ }' route. Did you mean to 'useRoute(${
217
+ match?.routeId as string
218
+ })' instead?`,
219
+ )
220
+ }
221
+
222
+ useRouterSubscription(router)
223
+
224
+ if (!match) {
225
+ throw new Error('Match not found!')
226
+ }
227
+
228
+ return match
229
+ },
230
+ }
231
+
232
+ Object.assign(router, routerExt)
233
+ },
234
+ createRoute: ({ router, route }) => {
235
+ const routeExt: Pick<AnyRoute, 'linkProps' | 'Link' | 'MatchRoute'> = {
236
+ linkProps: (options) => {
237
+ const {
238
+ // custom props
239
+ type,
240
+ children,
241
+ target,
242
+ activeProps = () => ({ className: 'active' }),
243
+ inactiveProps = () => ({}),
244
+ activeOptions,
245
+ disabled,
246
+ // fromCurrent,
247
+ hash,
248
+ search,
249
+ params,
250
+ to,
251
+ preload,
252
+ preloadDelay,
253
+ preloadMaxAge,
254
+ replace,
255
+ // element props
256
+ style,
257
+ className,
258
+ onClick,
259
+ onFocus,
260
+ onMouseEnter,
261
+ onMouseLeave,
262
+ onTouchStart,
263
+ onTouchEnd,
264
+ ...rest
265
+ } = options
266
+
267
+ const linkInfo = route.buildLink(options)
268
+
269
+ if (linkInfo.type === 'external') {
270
+ const { href } = linkInfo
271
+ return { href }
272
+ }
273
+
274
+ const {
275
+ handleClick,
276
+ handleFocus,
277
+ handleEnter,
278
+ handleLeave,
279
+ isActive,
280
+ next,
281
+ } = linkInfo
282
+
283
+ const composeHandlers =
284
+ (handlers: (undefined | ((e: any) => void))[]) =>
285
+ (e: React.SyntheticEvent) => {
286
+ e.persist()
287
+ handlers.forEach((handler) => {
288
+ if (handler) handler(e)
289
+ })
290
+ }
291
+
292
+ // Get the active props
293
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
294
+ isActive ? functionalUpdate(activeProps) ?? {} : {}
295
+
296
+ // Get the inactive props
297
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
298
+ isActive ? {} : functionalUpdate(inactiveProps) ?? {}
299
+
300
+ return {
301
+ ...resolvedActiveProps,
302
+ ...resolvedInactiveProps,
303
+ ...rest,
304
+ href: disabled ? undefined : next.href,
305
+ onClick: composeHandlers([handleClick, onClick]),
306
+ onFocus: composeHandlers([handleFocus, onFocus]),
307
+ onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
308
+ onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
309
+ target,
310
+ style: {
311
+ ...style,
312
+ ...resolvedActiveProps.style,
313
+ ...resolvedInactiveProps.style,
314
+ },
315
+ className:
316
+ [
317
+ className,
318
+ resolvedActiveProps.className,
319
+ resolvedInactiveProps.className,
320
+ ]
321
+ .filter(Boolean)
322
+ .join(' ') || undefined,
323
+ ...(disabled
324
+ ? {
325
+ role: 'link',
326
+ 'aria-disabled': true,
327
+ }
328
+ : undefined),
329
+ ['data-status']: isActive ? 'active' : undefined,
330
+ }
331
+ },
332
+ Link: React.forwardRef((props: any, ref) => {
333
+ const linkProps = route.linkProps(props)
334
+
335
+ useRouterSubscription(router)
336
+
337
+ return (
338
+ <a
339
+ {...{
340
+ ref: ref as any,
341
+ ...linkProps,
342
+ children:
343
+ typeof props.children === 'function'
344
+ ? props.children({
345
+ isActive:
346
+ (linkProps as any)['data-status'] === 'active',
347
+ })
348
+ : props.children,
349
+ }}
350
+ />
351
+ )
352
+ }) as any,
353
+ MatchRoute: (opts) => {
354
+ const { pending, caseSensitive, children, ...rest } = opts
355
+
356
+ const params = route.matchRoute(rest as any, {
357
+ pending,
358
+ caseSensitive,
359
+ })
360
+
361
+ // useRouterSubscription(router)
362
+
363
+ if (!params) {
364
+ return null
365
+ }
366
+
367
+ return typeof opts.children === 'function'
368
+ ? opts.children(params as any)
369
+ : (opts.children as any)
370
+ },
371
+ }
372
+
373
+ Object.assign(route, routeExt)
374
+ },
375
+ })
376
+
377
+ return coreRouter as any
378
+ }
379
+
380
+ export type RouterProps<
381
+ TRouteConfig extends AnyRouteConfig = RouteConfig,
382
+ TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
383
+ > = RouterOptions<TRouteConfig> & {
384
+ router: Router<TRouteConfig, TAllRouteInfo>
385
+ // Children will default to `<Outlet />` if not provided
386
+ children?: React.ReactNode
387
+ }
388
+
389
+ export function RouterProvider<
390
+ TRouteConfig extends AnyRouteConfig = RouteConfig,
391
+ TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
392
+ >({ children, router, ...rest }: RouterProps<TRouteConfig, TAllRouteInfo>) {
393
+ router.update(rest)
394
+
395
+ useSyncExternalStore(
396
+ (cb) => router.subscribe(() => cb()),
397
+ () => router.state,
398
+ )
399
+
400
+ useLayoutEffect(() => {
401
+ router.mount()
402
+ }, [])
403
+
404
+ return (
405
+ <routerContext.Provider value={{ router }}>
406
+ <MatchesProvider value={router.state.matches}>
407
+ {children ?? <Outlet />}
408
+ </MatchesProvider>
409
+ </routerContext.Provider>
410
+ )
411
+ }
412
+
413
+ export function useRouter(): Router {
414
+ const value = React.useContext(routerContext)
415
+ warning(!value, 'useRouter must be used inside a <Router> component!')
416
+
417
+ useRouterSubscription(value.router)
418
+
419
+ return value.router as Router
420
+ }
421
+
422
+ export function useMatches(): RouteMatch[] {
423
+ return React.useContext(matchesContext)
424
+ }
425
+
426
+ export function useParentMatches(): RouteMatch[] {
427
+ const router = useRouter()
428
+ const match = useMatch()
429
+ const matches = router.state.matches
430
+ return matches.slice(
431
+ 0,
432
+ matches.findIndex((d) => d.matchId === match.matchId) - 1,
433
+ )
434
+ }
435
+
436
+ export function useMatch<T>(): RouteMatch {
437
+ return useMatches()?.[0] as RouteMatch
438
+ }
439
+
440
+ export function Outlet() {
441
+ const router = useRouter()
442
+ const [, ...matches] = useMatches()
443
+
444
+ const childMatch = matches[0]
445
+
446
+ if (!childMatch) return null
447
+
448
+ const element = (((): React.ReactNode => {
449
+ if (!childMatch) {
450
+ return null
451
+ }
452
+
453
+ const errorElement =
454
+ childMatch.__.errorElement ?? router.options.defaultErrorElement
455
+
456
+ if (childMatch.status === 'error') {
457
+ if (errorElement) {
458
+ return errorElement as any
459
+ }
460
+
461
+ if (
462
+ childMatch.options.useErrorBoundary ||
463
+ router.options.useErrorBoundary
464
+ ) {
465
+ throw childMatch.error
466
+ }
467
+
468
+ return <DefaultCatchBoundary error={childMatch.error} />
469
+ }
470
+
471
+ if (childMatch.status === 'loading' || childMatch.status === 'idle') {
472
+ if (childMatch.isPending) {
473
+ const pendingElement =
474
+ childMatch.__.pendingElement ?? router.options.defaultPendingElement
475
+
476
+ if (childMatch.options.pendingMs || pendingElement) {
477
+ return (pendingElement as any) ?? null
478
+ }
479
+ }
480
+
481
+ return null
482
+ }
483
+
484
+ return (childMatch.__.element as any) ?? router.options.defaultElement
485
+ })() as JSX.Element) ?? <Outlet />
486
+
487
+ const catchElement =
488
+ childMatch?.options.catchElement ?? router.options.defaultCatchElement
489
+
490
+ return (
491
+ <MatchesProvider value={matches} key={childMatch.matchId}>
492
+ <CatchBoundary catchElement={catchElement}>{element}</CatchBoundary>
493
+ </MatchesProvider>
494
+ )
495
+ }
496
+
497
+ class CatchBoundary extends React.Component<{
498
+ children: any
499
+ catchElement: any
500
+ }> {
501
+ state = {
502
+ error: false,
503
+ }
504
+ componentDidCatch(error: any, info: any) {
505
+ console.error(error)
506
+
507
+ this.setState({
508
+ error,
509
+ info,
510
+ })
511
+ }
512
+ reset = () => {
513
+ this.setState({
514
+ error: false,
515
+ info: false,
516
+ })
517
+ }
518
+ render() {
519
+ const catchElement = this.props.catchElement ?? DefaultCatchBoundary
520
+
521
+ if (this.state.error) {
522
+ return typeof catchElement === 'function'
523
+ ? catchElement(this.state)
524
+ : catchElement
525
+ }
526
+
527
+ return this.props.children
528
+ }
529
+ }
530
+
531
+ export function DefaultCatchBoundary({ error }: { error: any }) {
532
+ return (
533
+ <div style={{ padding: '.5rem', maxWidth: '100%' }}>
534
+ <strong style={{ fontSize: '1.2rem' }}>Something went wrong!</strong>
535
+ <div style={{ height: '.5rem' }} />
536
+ <div>
537
+ <pre>
538
+ {error.message ? (
539
+ <code
540
+ style={{
541
+ fontSize: '.7em',
542
+ border: '1px solid red',
543
+ borderRadius: '.25rem',
544
+ padding: '.5rem',
545
+ color: 'red',
546
+ }}
547
+ >
548
+ {error.message}
549
+ </code>
550
+ ) : null}
551
+ </pre>
552
+ </div>
553
+ <div style={{ height: '1rem' }} />
554
+ <div
555
+ style={{
556
+ fontSize: '.8em',
557
+ borderLeft: '3px solid rgba(127, 127, 127, 1)',
558
+ paddingLeft: '.5rem',
559
+ opacity: 0.5,
560
+ }}
561
+ >
562
+ If you are the owner of this website, it's highly recommended that you
563
+ configure your own custom Catch/Error boundaries for the router. You can
564
+ optionally configure a boundary for each route.
565
+ </div>
566
+ </div>
567
+ )
568
+ }
package/src/qss.ts ADDED
@@ -0,0 +1,53 @@
1
+ // @ts-nocheck
2
+
3
+ // We're inlining qss here for compression's sake, but we've included it as a hard dependency for the MIT license it requires.
4
+
5
+ export function encode(obj, pfx?: string) {
6
+ var k,
7
+ i,
8
+ tmp,
9
+ str = ''
10
+
11
+ for (k in obj) {
12
+ if ((tmp = obj[k]) !== void 0) {
13
+ if (Array.isArray(tmp)) {
14
+ for (i = 0; i < tmp.length; i++) {
15
+ str && (str += '&')
16
+ str += encodeURIComponent(k) + '=' + encodeURIComponent(tmp[i])
17
+ }
18
+ } else {
19
+ str && (str += '&')
20
+ str += encodeURIComponent(k) + '=' + encodeURIComponent(tmp)
21
+ }
22
+ }
23
+ }
24
+
25
+ return (pfx || '') + str
26
+ }
27
+
28
+ function toValue(mix) {
29
+ if (!mix) return ''
30
+ var str = decodeURIComponent(mix)
31
+ if (str === 'false') return false
32
+ if (str === 'true') return true
33
+ return +str * 0 === 0 ? +str : str
34
+ }
35
+
36
+ export function decode(str) {
37
+ var tmp,
38
+ k,
39
+ out = {},
40
+ arr = str.split('&')
41
+
42
+ while ((tmp = arr.shift())) {
43
+ tmp = tmp.split('=')
44
+ k = tmp.shift()
45
+ if (out[k] !== void 0) {
46
+ out[k] = [].concat(out[k], toValue(tmp.shift()))
47
+ } else {
48
+ out[k] = toValue(tmp.shift())
49
+ }
50
+ }
51
+
52
+ return out
53
+ }