Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make to required if from is not set #3099

Merged
merged 2 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e/react-router/basic-file-based/src/routes/anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ function AnchorComponent() {
{anchors.map((anchor) => (
<li key={anchor.id}>
<Link
from={Route.fullPath}
data-testid={`link-${anchor.id}`}
hash={anchor.id}
activeOptions={{ includeHash: true }}
Expand Down
1 change: 1 addition & 0 deletions examples/react/basic-file-based/src/routes/anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function AnchorComponent() {
{anchors.map((anchor) => (
<li key={anchor.id}>
<Link
from={Route.fullPath}
hash={anchor.id}
activeOptions={{ includeHash: true }}
activeProps={{
Expand Down
33 changes: 16 additions & 17 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import type {
} from './structuralSharing'
import type { AnyRoute, ReactNode, StaticDataRouteOption } from './route'
import type { AnyRouter, RegisteredRouter, RouterState } from './router'
import type { ResolveRelativePath, ResolveRoute, ToOptions } from './link'
import type {
MakeOptionalPathParams,
MakeOptionalSearchParams,
MaskOptions,
ResolveRelativePath,
ResolveRoute,
ToSubOptionsProps,
} from './link'
import type {
AllContext,
AllLoaderData,
Expand All @@ -23,7 +30,6 @@ import type {
RouteById,
RouteByPath,
RouteIds,
RoutePaths,
} from './routeInfo'
import type {
Constrain,
Expand Down Expand Up @@ -270,22 +276,15 @@ export interface MatchRouteOptions {

export type UseMatchRouteOptions<
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends RoutePaths<TRouter['routeTree']> | string = RoutePaths<
TRouter['routeTree']
>,
TTo extends string | undefined = '',
TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
TFrom extends string = string,
TTo extends string | undefined = undefined,
TMaskFrom extends string = TFrom,
TMaskTo extends string = '',
TOptions extends ToOptions<
TRouter,
TFrom,
TTo,
TMaskFrom,
TMaskTo
> = ToOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
TRelaxedOptions = Omit<TOptions, 'search' | 'params'> &
DeepPartial<Pick<TOptions, 'search' | 'params'>>,
> = TRelaxedOptions & MatchRouteOptions
> = ToSubOptionsProps<TRouter, TFrom, TTo> &
DeepPartial<MakeOptionalSearchParams<TRouter, TFrom, TTo>> &
DeepPartial<MakeOptionalPathParams<TRouter, TFrom, TTo>> &
MaskOptions<TRouter, TMaskFrom, TMaskTo> &
MatchRouteOptions

export function useMatchRoute<TRouter extends AnyRouter = RegisteredRouter>() {
const router = useRouter()
Expand Down
10 changes: 5 additions & 5 deletions packages/react-router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export type {
ParsePathParams,
RemoveTrailingSlashes,
RemoveLeadingSlashes,
SearchPaths,
SearchRelativePathAutoComplete,
RelativeToParentPathAutoComplete,
RelativeToCurrentPathAutoComplete,
AbsolutePathAutoComplete,
InferDescendantToPaths,
RelativeToPath,
RelativeToParentPath,
RelativeToCurrentPath,
AbsoluteToPath,
RelativeToPathAutoComplete,
NavigateOptions,
ToOptions,
Expand Down
108 changes: 62 additions & 46 deletions packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import type {
RoutePaths,
RouteToPath,
ToPath,
TrailingSlashOptionByRouter,
} from './routeInfo'
import type {
AnyRouter,
Expand Down Expand Up @@ -80,78 +79,73 @@ export type RemoveLeadingSlashes<T> = T extends `/${string}`
: T
: T

export type FindDescendantPaths<
export type FindDescendantToPaths<
TRouter extends AnyRouter,
TPrefix extends string,
> = `${TPrefix}/${string}` & RouteToPath<TRouter>

export type SearchPaths<
export type InferDescendantToPaths<
TRouter extends AnyRouter,
TPrefix extends string,
TPaths = FindDescendantPaths<TRouter, TPrefix>,
TPaths = FindDescendantToPaths<TRouter, TPrefix>,
> = TPaths extends `${TPrefix}/`
? never
: TPaths extends `${TPrefix}/${infer TRest}`
? TRest
: never

export type SearchRelativePathAutoComplete<
export type RelativeToPath<
TRouter extends AnyRouter,
TTo extends string,
TSearchPath extends string,
TResolvedPath extends string,
> =
| (TSearchPath & RouteToPath<TRouter> extends never
| (TResolvedPath & RouteToPath<TRouter> extends never
? never
: ToPath<TrailingSlashOptionByRouter<TRouter>, TTo>)
| `${TTo}/${SearchPaths<TRouter, RemoveTrailingSlashes<TSearchPath>>}`
: ToPath<TRouter, TTo>)
| `${RemoveTrailingSlashes<TTo>}/${InferDescendantToPaths<TRouter, RemoveTrailingSlashes<TResolvedPath>>}`

export type RelativeToParentPathAutoComplete<
export type RelativeToParentPath<
TRouter extends AnyRouter,
TFrom extends string,
TTo extends string,
TResolvedPath extends string = ResolveRelativePath<TFrom, TTo>,
> =
| SearchRelativePathAutoComplete<TRouter, TTo, TResolvedPath>
| RelativeToPath<TRouter, TTo, TResolvedPath>
| (TTo extends `${string}..` | `${string}../`
? TResolvedPath extends '/' | ''
? never
: FindDescendantPaths<
: FindDescendantToPaths<
TRouter,
RemoveTrailingSlashes<TResolvedPath>
> extends never
? never
: `${TTo}/${ParentPath<TrailingSlashOptionByRouter<TRouter>>}`
: `${RemoveTrailingSlashes<TTo>}/${ParentPath<TRouter>}`
: never)

export type RelativeToCurrentPathAutoComplete<
export type RelativeToCurrentPath<
TRouter extends AnyRouter,
TFrom extends string,
TTo extends string,
TResolvedPath extends string = ResolveRelativePath<TFrom, TTo>,
> =
| SearchRelativePathAutoComplete<TRouter, TTo, TResolvedPath>
| CurrentPath<TrailingSlashOptionByRouter<TRouter>>
> = RelativeToPath<TRouter, TTo, TResolvedPath> | CurrentPath<TRouter>

export type AbsolutePathAutoComplete<
TRouter extends AnyRouter,
TFrom extends string,
> =
export type AbsoluteToPath<TRouter extends AnyRouter, TFrom extends string> =
| (string extends TFrom
? CurrentPath<TrailingSlashOptionByRouter<TRouter>>
? CurrentPath<TRouter>
: TFrom extends `/`
? never
: CurrentPath<TrailingSlashOptionByRouter<TRouter>>)
: CurrentPath<TRouter>)
| (string extends TFrom
? ParentPath<TrailingSlashOptionByRouter<TRouter>>
? ParentPath<TRouter>
: TFrom extends `/`
? never
: ParentPath<TrailingSlashOptionByRouter<TRouter>>)
: ParentPath<TRouter>)
| RouteToPath<TRouter>
| (TFrom extends '/'
? never
: string extends TFrom
? never
: SearchPaths<TRouter, RemoveTrailingSlashes<TFrom>>)
: InferDescendantToPaths<TRouter, RemoveTrailingSlashes<TFrom>>)

export type RelativeToPathAutoComplete<
TRouter extends AnyRouter,
Expand All @@ -160,20 +154,12 @@ export type RelativeToPathAutoComplete<
> = string extends TTo
? string
: string extends TFrom
? AbsolutePathAutoComplete<TRouter, TFrom>
? AbsoluteToPath<TRouter, TFrom>
: TTo & `..${string}` extends never
? TTo & `.${string}` extends never
? AbsolutePathAutoComplete<TRouter, TFrom>
: RelativeToCurrentPathAutoComplete<
TRouter,
TFrom,
RemoveTrailingSlashes<TTo>
>
: RelativeToParentPathAutoComplete<
TRouter,
TFrom,
RemoveTrailingSlashes<TTo>
>
? AbsoluteToPath<TRouter, TFrom>
: RelativeToCurrentPath<TRouter, TFrom, TTo>
: RelativeToParentPath<TRouter, TFrom, TTo>

export type NavigateOptions<
TRouter extends AnyRouter = RegisteredRouter,
Expand Down Expand Up @@ -234,15 +220,41 @@ export type ToSubOptions<
SearchParamOptions<TRouter, TFrom, TTo> &
PathParamOptions<TRouter, TFrom, TTo>

export interface ToSubOptionsProps<
in out TRouter extends AnyRouter = RegisteredRouter,
in out TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
in out TTo extends string | undefined = '.',
export interface RequiredToOptions<
in out TRouter extends AnyRouter,
in out TFrom extends string,
in out TTo extends string | undefined,
> {
to: ToPathOption<TRouter, TFrom, TTo> & {}
}

export interface OptionalToOptions<
in out TRouter extends AnyRouter,
in out TFrom extends string,
in out TTo extends string | undefined,
> {
to?: ToPathOption<TRouter, TFrom, TTo> & {}
}

export type MakeToRequired<
TRouter extends AnyRouter,
TFrom extends string,
TTo extends string | undefined,
> = string extends TFrom
? string extends TTo
? OptionalToOptions<TRouter, TFrom, TTo>
: TTo & CatchAllPaths<TRouter> extends never
? RequiredToOptions<TRouter, TFrom, TTo>
: OptionalToOptions<TRouter, TFrom, TTo>
: OptionalToOptions<TRouter, TFrom, TTo>

export type ToSubOptionsProps<
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
TTo extends string | undefined = '.',
> = MakeToRequired<TRouter, TFrom, TTo> & {
hash?: true | Updater<string>
state?: true | NonNullableUpdater<HistoryState>
// The source route path. This is automatically set when using route-level APIs, but for type-safe relative routing on the router itself, this is required
from?: FromPathOption<TRouter, TFrom> & {}
}

Expand Down Expand Up @@ -319,7 +331,7 @@ export type ResolveToParams<
? never
: string extends TPath
? ResolveAllToParams<TRouter, TParamVariant>
: TPath extends CatchAllPaths<TrailingSlashOptionByRouter<TRouter>>
: TPath extends CatchAllPaths<TRouter>
? ResolveAllToParams<TRouter, TParamVariant>
: ResolveRoute<
TRouter,
Expand Down Expand Up @@ -404,7 +416,7 @@ export type IsRequired<
ResolveRelativePath<TFrom, TTo> extends infer TPath
? undefined extends TPath
? never
: TPath extends CatchAllPaths<TrailingSlashOptionByRouter<TRouter>>
: TPath extends CatchAllPaths<TRouter>
? never
: IsRequiredParams<
ResolveRelativeToParams<TRouter, TParamVariant, TFrom, TTo>
Expand Down Expand Up @@ -1014,7 +1026,11 @@ export function createLink<const TComp>(
export const Link: LinkComponent<'a'> = React.forwardRef<Element, any>(
(props, ref) => {
const { _asChild, ...rest } = props
const { type: _type, ref: innerRef, ...linkProps } = useLinkProps(rest, ref)
const {
type: _type,
ref: innerRef,
...linkProps
} = useLinkProps(rest as any, ref)

const children =
typeof rest.children === 'function'
Expand Down
43 changes: 24 additions & 19 deletions packages/react-router/src/routeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,30 @@ export type RouteIds<TRouteTree extends AnyRoute> =
? CodeRouteIds<TRouteTree>
: InferFileRouteTypes<TRouteTree>['id']

export type ParentPath<TOption> = 'always' extends TOption
? '../'
: 'never' extends TOption
? '..'
: '../' | '..'

export type CurrentPath<TOption> = 'always' extends TOption
? './'
: 'never' extends TOption
? '.'
: './' | '.'

export type ToPath<TOption, TTo extends string> = 'always' extends TOption
? `${TTo}/`
: 'never' extends TOption
? TTo
: TTo | `${TTo}/`

export type CatchAllPaths<TOption> = CurrentPath<TOption> | ParentPath<TOption>
export type ParentPath<TRouter extends AnyRouter> =
TrailingSlashOptionByRouter<TRouter> extends 'always'
? '../'
: TrailingSlashOptionByRouter<TRouter> extends 'never'
? '..'
: '../' | '..'

export type CurrentPath<TRouter extends AnyRouter> =
TrailingSlashOptionByRouter<TRouter> extends 'always'
? './'
: TrailingSlashOptionByRouter<TRouter> extends 'never'
? '.'
: './' | '.'

export type ToPath<TRouter extends AnyRouter, TTo extends string> =
TrailingSlashOptionByRouter<TRouter> extends 'always'
? AddTrailingSlash<TTo>
: TrailingSlashOptionByRouter<TRouter> extends 'never'
? RemoveTrailingSlashes<TTo>
: AddTrailingSlash<TTo> | RemoveTrailingSlashes<TTo>

export type CatchAllPaths<TRouter extends AnyRouter> =
| CurrentPath<TRouter>
| ParentPath<TRouter>

export type CodeRoutesByPath<TRouteTree extends AnyRoute> =
ParseRoute<TRouteTree> extends infer TRoutes extends AnyRoute
Expand Down
18 changes: 3 additions & 15 deletions packages/react-router/tests/Matches.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,26 +155,14 @@ test('when matching a route with params', () => {
.parameter(0)
.toHaveProperty('to')
.toEqualTypeOf<
| '/'
| '.'
| '..'
| '/invoices'
| '/invoices/$invoiceId'
| '/comments/$id'
| undefined
'/' | '.' | '..' | '/invoices' | '/invoices/$invoiceId' | '/comments/$id'
>()

expectTypeOf(MatchRoute<DefaultRouter, any, '/invoices/$invoiceId'>)
expectTypeOf(MatchRoute<DefaultRouter, string, '/invoices/$invoiceId'>)
.parameter(0)
.toHaveProperty('to')
.toEqualTypeOf<
| '/'
| '.'
| '..'
| '/invoices'
| '/invoices/$invoiceId'
| '/comments/$id'
| undefined
'/' | '.' | '..' | '/invoices' | '/invoices/$invoiceId' | '/comments/$id'
>()

expectTypeOf(
Expand Down
Loading
Loading