|
import { A } from '@mobily/ts-belt'; |
|
import { stringify as queryStringify } from 'querystring'; |
|
import { useParams } from 'react-router-dom'; |
|
import { ConditionalKeys, Opaque, Schema } from 'type-fest'; |
|
|
|
export type WithLooseAutocomplete< |
|
AcceptedType, |
|
Suggestions extends AcceptedType |
|
> = |
|
| Suggestions |
|
| Exclude<Omit<AcceptedType, Suggestions & keyof AcceptedType>, Suggestions>; |
|
|
|
type RequiredParam = Opaque<string, 'requiredParam'>; |
|
type OptionalParam = Opaque<string, 'optionalParam'>; |
|
type ParamFieldDefinition = RequiredParam | OptionalParam; |
|
|
|
type PartialByValue<Obj, ValuesToMakeOptional> = Omit< |
|
Obj, |
|
ConditionalKeys<Obj, ValuesToMakeOptional> |
|
> & |
|
Partial<Pick<Obj, ConditionalKeys<Obj, ValuesToMakeOptional>>>; |
|
|
|
type ParamObject< |
|
ParamDefinition extends Record<string, ParamFieldDefinition> |
|
> = Schema<PartialByValue<ParamDefinition, OptionalParam>, string>; |
|
|
|
export type RouteDefinition< |
|
DynamicSegmentName extends string, |
|
ParamsDefinition extends Record<string, ParamFieldDefinition> = Record< |
|
never, |
|
unknown |
|
> |
|
> = { |
|
path: string; |
|
generatePath: ( |
|
segmentValues: Record<DynamicSegmentName, string>, |
|
paramValues: ParamObject<ParamsDefinition> |
|
) => string; |
|
extend: < |
|
ExtendedDynamicSegmentName extends string, |
|
ExtendedParams extends Record< |
|
string, |
|
RequiredParam | OptionalParam |
|
> = Record<never, unknown> |
|
>( |
|
extensionSegmentValues: WithLooseAutocomplete< |
|
string, |
|
`:${DynamicSegmentName | ExtendedDynamicSegmentName}` |
|
>[] |
|
) => RouteDefinition< |
|
DynamicSegmentName | ExtendedDynamicSegmentName, |
|
ExtendedParams |
|
>; |
|
useParams: () => Partial< |
|
ParamObject<ParamsDefinition> & Record<DynamicSegmentName, string> |
|
>; |
|
}; |
|
|
|
// Infer the type of an existing route |
|
// definition's params |
|
export type InferRouteParams< |
|
RouteDef extends RouteDefinition<any, any> |
|
> = RouteDef extends RouteDefinition<any, infer ParamDef> |
|
? ParamObject<ParamDef> |
|
: never; |
|
|
|
// Infer the type of an existing route |
|
// definition's dynamic segments |
|
export type InferRouteDynamicSegments< |
|
RouteDef extends RouteDefinition<any, any> |
|
> = RouteDef extends RouteDefinition<infer SegmentName, any> |
|
? SegmentName |
|
: never; |
|
|
|
const composePath = A.join('/'); |
|
const defineRoute = < |
|
DynamicSegmentName extends string, |
|
ParamsDefinition extends Record<string, ParamFieldDefinition> = Record< |
|
never, |
|
unknown |
|
> |
|
>( |
|
segments: WithLooseAutocomplete<string, `:${DynamicSegmentName}`>[] |
|
): RouteDefinition<DynamicSegmentName, ParamsDefinition> => |
|
({ |
|
path: composePath(segments), |
|
generatePath: (segmentValues, params) => { |
|
const replacedSegments = segments.map((val) => { |
|
const withoutParamPrefix = (val as string).replace(/^:/, ''); |
|
const replacement = |
|
withoutParamPrefix in segmentValues |
|
? segmentValues[withoutParamPrefix as DynamicSegmentName] |
|
: (val as string); |
|
return replacement; |
|
}); |
|
const paramQuery = queryStringify(params as any); |
|
const pathWithoutParams = composePath(replacedSegments); |
|
const paramSeparator = paramQuery ? '?' : ''; |
|
const path = `${pathWithoutParams}${paramSeparator}${paramQuery}`; |
|
return path; |
|
}, |
|
extend: (extendedSegments) => |
|
defineRoute([...segments, ...extendedSegments] as any[]), |
|
useParams: () => useParams() as any, |
|
} as RouteDefinition<DynamicSegmentName, ParamsDefinition>); |
|
|
|
export default defineRoute; |