Skip to content

Commit 4658afa

Browse files
committed
fix: further improve relative routing
1 parent a492e39 commit 4658afa

File tree

3 files changed

+204
-21
lines changed

3 files changed

+204
-21
lines changed

packages/react-router/src/link.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
FullSearchSchema,
2323
FullSearchSchemaInput,
2424
ParentPath,
25+
RootPath,
2526
RouteByPath,
2627
RouteByToPath,
2728
RoutePaths,
@@ -78,15 +79,18 @@ export type RemoveLeadingSlashes<T> = T extends `/${string}`
7879
export type FindDescendantPaths<
7980
TRouter extends AnyRouter,
8081
TPrefix extends string,
81-
> = (TPrefix | `${TPrefix}/${string}`) & RouteToPath<TRouter>
82+
TCleanedPrefix extends string = RemoveTrailingSlashes<TPrefix>,
83+
> = (TPrefix | `${TCleanedPrefix}/${string}`) & RouteToPath<TRouter>
8284

8385
export type SearchPaths<
8486
TRouter extends AnyRouter,
8587
TPrefix extends string,
8688
TPaths = FindDescendantPaths<TRouter, TPrefix>,
89+
TCleanedPrefix extends string = RemoveTrailingSlashes<TPrefix>,
90+
TRootPath = RootPath<TrailingSlashOptionByRouter<TRouter>>,
8791
> = TPaths extends TPrefix
88-
? ''
89-
: TPaths extends `${TPrefix}/${infer TRest}`
92+
? TRootPath
93+
: TPaths extends `${TCleanedPrefix}/${infer TRest}`
9094
? `/${TRest}`
9195
: never
9296

@@ -100,25 +104,22 @@ export type RelativeToParentPathAutoComplete<
100104
TRouter extends AnyRouter,
101105
TFrom extends string,
102106
TTo extends string,
103-
TResolvedPath extends string = RemoveTrailingSlashes<
104-
ResolveRelativePath<TFrom, TTo>
105-
>,
107+
TResolvedPath extends string = ResolveRelativePath<TFrom, TTo>,
106108
> =
107109
| SearchRelativePathAutoComplete<TRouter, TTo, TResolvedPath>
108110
| (TTo extends `${string}..` | `${string}../`
109-
? FindDescendantPaths<TRouter, TResolvedPath> &
110-
TResolvedPath extends TResolvedPath
111+
? TResolvedPath extends '/' | ''
111112
? never
112-
: `${TTo}/${ParentPath<TrailingSlashOptionByRouter<TRouter>>}`
113+
: FindDescendantPaths<TRouter, TResolvedPath> extends never
114+
? never
115+
: `${TTo}/${ParentPath<TrailingSlashOptionByRouter<TRouter>>}`
113116
: never)
114117

115118
export type RelativeToCurrentPathAutoComplete<
116119
TRouter extends AnyRouter,
117120
TFrom extends string,
118121
TTo extends string,
119-
TResolvedPath extends string = RemoveTrailingSlashes<
120-
ResolveRelativePath<TFrom, TTo>
121-
>,
122+
TResolvedPath extends string = ResolveRelativePath<TFrom, TTo>,
122123
> =
123124
| SearchRelativePathAutoComplete<TRouter, TTo, TResolvedPath>
124125
| CurrentPath<TrailingSlashOptionByRouter<TRouter>>
@@ -513,12 +514,12 @@ export type ResolveParentPath<
513514
TFrom extends string,
514515
TTo extends string,
515516
> = TTo extends '../' | '..'
516-
? TFrom extends ''
517+
? TFrom extends '' | '/'
517518
? never
518519
: AddLeadingSlash<RemoveLastSegment<TFrom>>
519520
: TTo & `../${string}` extends never
520521
? AddLeadingSlash<JoinPath<TFrom, TTo>>
521-
: TFrom extends ''
522+
: TFrom extends '' | '/'
522523
? never
523524
: TTo extends `../${infer ToRest}`
524525
? ResolveParentPath<RemoveLastSegment<TFrom>, ToRest>

packages/react-router/src/routeInfo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export type CurrentPath<TOption> = 'always' extends TOption
7171
? '.'
7272
: './' | '.'
7373

74+
export type RootPath<TOption> = 'always' extends TOption
75+
? '/'
76+
: 'never' extends TOption
77+
? ''
78+
: '' | '/'
79+
7480
export type CatchAllPaths<TOption> = CurrentPath<TOption> | ParentPath<TOption>
7581

7682
export type CodeRoutesByPath<TRouteTree extends AnyRoute> =

packages/react-router/tests/link.test-d.tsx

Lines changed: 183 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ test('when navigating from a route with no params and no search to the current r
728728
})
729729

730730
test('when navigating from a route with no params and no search to the parent route', () => {
731-
expectTypeOf(Link<DefaultRouter, '/posts', '../'>)
731+
expectTypeOf(Link<DefaultRouter, '/posts', '..'>)
732732
.parameter(0)
733733
.toHaveProperty('to')
734734
.toEqualTypeOf<
@@ -740,11 +740,11 @@ test('when navigating from a route with no params and no search to the parent ro
740740
| '../invoices/$invoiceId/details/$detailId'
741741
| '../invoices/$invoiceId/details/$detailId/lines'
742742
| '../invoices'
743-
| '../'
743+
| '..'
744744
| undefined
745745
>()
746746

747-
expectTypeOf(Link<DefaultRouterObjects, '/posts', '../'>)
747+
expectTypeOf(Link<DefaultRouterObjects, '/posts', '..'>)
748748
.parameter(0)
749749
.toHaveProperty('to')
750750
.toEqualTypeOf<
@@ -756,7 +756,7 @@ test('when navigating from a route with no params and no search to the parent ro
756756
| '../invoices/$invoiceId/details/$detailId'
757757
| '../invoices/$invoiceId/details/$detailId/lines'
758758
| '../invoices'
759-
| '../'
759+
| '..'
760760
| undefined
761761
>()
762762

@@ -776,7 +776,7 @@ test('when navigating from a route with no params and no search to the parent ro
776776
| undefined
777777
>()
778778

779-
expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts', '../'>)
779+
expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts', '..'>)
780780
.parameter(0)
781781
.toHaveProperty('to')
782782
.toEqualTypeOf<
@@ -788,11 +788,11 @@ test('when navigating from a route with no params and no search to the parent ro
788788
| '../invoices/$invoiceId/details/$detailId'
789789
| '../invoices/$invoiceId/details/$detailId/lines'
790790
| '../invoices'
791-
| '../'
791+
| '..'
792792
| undefined
793793
>()
794794

795-
expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts', '../'>)
795+
expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts', '..'>)
796796
.parameter(0)
797797
.toHaveProperty('to')
798798
.toEqualTypeOf<
@@ -813,6 +813,7 @@ test('when navigating from a route with no params and no search to the parent ro
813813
| '../invoices'
814814
| '../invoices/'
815815
| '../'
816+
| '..'
816817
| undefined
817818
>()
818819
})
@@ -3855,10 +3856,18 @@ test('ResolveRelativePath', () => {
38553856
ResolveRelativePath<'/posts/1/comments', '..'>
38563857
>().toEqualTypeOf<'/posts/1'>()
38573858

3859+
expectTypeOf<
3860+
ResolveRelativePath<'/posts/1/comments/', '..'>
3861+
>().toEqualTypeOf<'/posts/1/'>()
3862+
38583863
expectTypeOf<
38593864
ResolveRelativePath<'/posts/1/comments', '../..'>
38603865
>().toEqualTypeOf<'/posts'>()
38613866

3867+
expectTypeOf<
3868+
ResolveRelativePath<'/posts/1/comments/', '../..'>
3869+
>().toEqualTypeOf<'/posts/'>()
3870+
38623871
expectTypeOf<
38633872
ResolveRelativePath<'/posts/1/comments', '../../..'>
38643873
>().toEqualTypeOf<'/'>()
@@ -3875,6 +3884,10 @@ test('ResolveRelativePath', () => {
38753884
ResolveRelativePath<'/posts/1/comments', '../edit'>
38763885
>().toEqualTypeOf<'/posts/1/edit'>()
38773886

3887+
expectTypeOf<
3888+
ResolveRelativePath<'/posts/1/comments/', '../edit'>
3889+
>().toEqualTypeOf<'/posts/1/edit'>()
3890+
38783891
expectTypeOf<
38793892
ResolveRelativePath<'/posts/1/comments', '1'>
38803893
>().toEqualTypeOf<'/posts/1/comments/1'>()
@@ -3888,6 +3901,169 @@ test('ResolveRelativePath', () => {
38883901
>().toEqualTypeOf<'/posts/1/comments/1/2'>()
38893902
})
38903903

3904+
test('navigation edge cases', () => {
3905+
expectTypeOf(Link<DefaultRouter, '/', '..'>)
3906+
.parameter(0)
3907+
.toHaveProperty('to')
3908+
.toEqualTypeOf<undefined>()
3909+
3910+
expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/', '../'>)
3911+
.parameter(0)
3912+
.toHaveProperty('to')
3913+
.toEqualTypeOf<undefined>()
3914+
3915+
expectTypeOf(Link<RouterNeverTrailingSlashes, '/', '..'>)
3916+
.parameter(0)
3917+
.toHaveProperty('to')
3918+
.toEqualTypeOf<undefined>()
3919+
3920+
expectTypeOf(Link<RouterPreserveTrailingSlashes, '/', '..' | '../'>)
3921+
.parameter(0)
3922+
.toHaveProperty('to')
3923+
.toEqualTypeOf<undefined>()
3924+
3925+
expectTypeOf(Link<DefaultRouter, '', '..'>)
3926+
.parameter(0)
3927+
.toHaveProperty('to')
3928+
.toEqualTypeOf<undefined>()
3929+
3930+
expectTypeOf(Link<RouterAlwaysTrailingSlashes, '', '../'>)
3931+
.parameter(0)
3932+
.toHaveProperty('to')
3933+
.toEqualTypeOf<undefined>()
3934+
3935+
expectTypeOf(Link<RouterNeverTrailingSlashes, '', '..'>)
3936+
.parameter(0)
3937+
.toHaveProperty('to')
3938+
.toEqualTypeOf<undefined>()
3939+
3940+
expectTypeOf(Link<RouterPreserveTrailingSlashes, '', '..' | '../'>)
3941+
.parameter(0)
3942+
.toHaveProperty('to')
3943+
.toEqualTypeOf<undefined>()
3944+
3945+
expectTypeOf(Link<DefaultRouter, '/posts', '...'>)
3946+
.parameter(0)
3947+
.toHaveProperty('to')
3948+
.toEqualTypeOf<undefined>()
3949+
3950+
expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts', '.../'>)
3951+
.parameter(0)
3952+
.toHaveProperty('to')
3953+
.toEqualTypeOf<undefined>()
3954+
3955+
expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts', '...'>)
3956+
.parameter(0)
3957+
.toHaveProperty('to')
3958+
.toEqualTypeOf<undefined>()
3959+
3960+
expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts', '...' | '.../'>)
3961+
.parameter(0)
3962+
.toHaveProperty('to')
3963+
.toEqualTypeOf<undefined>()
3964+
3965+
expectTypeOf(Link<DefaultRouter, '/posts/$postId', '../../..'>)
3966+
.parameter(0)
3967+
.toHaveProperty('to')
3968+
.toEqualTypeOf<undefined>()
3969+
3970+
expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts/$postId', '../../../'>)
3971+
.parameter(0)
3972+
.toHaveProperty('to')
3973+
.toEqualTypeOf<undefined>()
3974+
3975+
expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts/$postId', '../../..'>)
3976+
.parameter(0)
3977+
.toHaveProperty('to')
3978+
.toEqualTypeOf<undefined>()
3979+
3980+
expectTypeOf(
3981+
Link<
3982+
RouterPreserveTrailingSlashes,
3983+
'/posts/$postId',
3984+
'../../..' | '../../../'
3985+
>,
3986+
)
3987+
.parameter(0)
3988+
.toHaveProperty('to')
3989+
.toEqualTypeOf<undefined>()
3990+
3991+
expectTypeOf(Link<DefaultRouter, '/posts/$postId', '../..'>)
3992+
.parameter(0)
3993+
.toHaveProperty('to')
3994+
.toEqualTypeOf<
3995+
| '../..'
3996+
| '../../posts'
3997+
| '../../posts/$postId'
3998+
| '../../invoices'
3999+
| '../../invoices/$invoiceId'
4000+
| '../../invoices/$invoiceId/edit'
4001+
| '../../invoices/$invoiceId/details'
4002+
| '../../invoices/$invoiceId/details/$detailId'
4003+
| '../../invoices/$invoiceId/details/$detailId/lines'
4004+
| undefined
4005+
>()
4006+
4007+
expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts/$postId', '../../'>)
4008+
.parameter(0)
4009+
.toHaveProperty('to')
4010+
.toEqualTypeOf<
4011+
| '../../'
4012+
| '../../posts/'
4013+
| '../../posts/$postId/'
4014+
| '../../invoices/'
4015+
| '../../invoices/$invoiceId/'
4016+
| '../../invoices/$invoiceId/edit/'
4017+
| '../../invoices/$invoiceId/details/'
4018+
| '../../invoices/$invoiceId/details/$detailId/'
4019+
| '../../invoices/$invoiceId/details/$detailId/lines/'
4020+
| undefined
4021+
>()
4022+
4023+
expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts/$postId', '../../'>)
4024+
.parameter(0)
4025+
.toHaveProperty('to')
4026+
.toEqualTypeOf<
4027+
| '../..'
4028+
| '../../posts'
4029+
| '../../posts/$postId'
4030+
| '../../invoices'
4031+
| '../../invoices/$invoiceId'
4032+
| '../../invoices/$invoiceId/edit'
4033+
| '../../invoices/$invoiceId/details'
4034+
| '../../invoices/$invoiceId/details/$detailId'
4035+
| '../../invoices/$invoiceId/details/$detailId/lines'
4036+
| undefined
4037+
>()
4038+
4039+
expectTypeOf(
4040+
Link<RouterPreserveTrailingSlashes, '/posts/$postId', '../../' | '../..'>,
4041+
)
4042+
.parameter(0)
4043+
.toHaveProperty('to')
4044+
.toEqualTypeOf<
4045+
| '../..'
4046+
| '../../'
4047+
| '../../posts'
4048+
| '../../posts/$postId'
4049+
| '../../invoices'
4050+
| '../../invoices/$invoiceId'
4051+
| '../../invoices/$invoiceId/edit'
4052+
| '../../invoices/$invoiceId/details'
4053+
| '../../invoices/$invoiceId/details/$detailId'
4054+
| '../../invoices/$invoiceId/details/$detailId/lines'
4055+
| '../../posts/'
4056+
| '../../posts/$postId/'
4057+
| '../../invoices/'
4058+
| '../../invoices/$invoiceId/'
4059+
| '../../invoices/$invoiceId/edit/'
4060+
| '../../invoices/$invoiceId/details/'
4061+
| '../../invoices/$invoiceId/details/$detailId/'
4062+
| '../../invoices/$invoiceId/details/$detailId/lines/'
4063+
| undefined
4064+
>()
4065+
})
4066+
38914067
test('linkOptions', () => {
38924068
const defaultRouterLinkOptions = linkOptions<
38934069
{ label: string },

0 commit comments

Comments
 (0)