import React , { useMemo , useState , useLayoutEffect } from 'react' ; import { RouterProvider } from './context' ; import { isNode , setSsrPath , getSsrPath } from './node' ; import { getFormattedPath , usePath } from './path' ; import type { NonEmptyRecord , Split , ValueOf } from './types' ; const emptyPathResult = [ null , null ] ; export interface PathParamOptions { basePath ? : string ; matchTrailingSlash ? : boolean ; } export interface RouteOptionParams extends PathParamOptions { routeProps ? : { [ k ] : any ; } ; overridePathParams ? : boolean ; } interface RouteMatcher { path : string ; regex : RegExp ; props : string [ ] ; } type ExtractPathParams < Path extends string , Parts = Split < Path , '/' > > = Parts extends [ infer Head , ... infer Tail ] ? Head extends `:${ infer Name }` ? { [ N in Name ] : string ; } & ExtractPathParams < Path , Tail > : ExtractPathParams < Path , Tail > : unknown ; export type Routes < Path extends string > = { [ P in Path ] : ( params : NonEmptyRecord < ExtractPathParams < P extends `${ infer P1 }*` ? P1 : P > > ) => JSX . Element ; } ; export function useRoutes < Path extends string > ( routes , { basePath = '' , routeProps = { } , overridePathParams = true , matchTrailingSlash = true , } = { } ) { const path = usePath ( basePath ) && getFormattedPath ( basePath ) ; useRedirectDetection ( basePath , path ) ; const route = useMatchRoute ( routes , path , { routeProps , overridePathParams , matchTrailingSlash , } ) ; if ( ! route || path === null ) return null ; return ( < RouterProvider basePath = { basePath } path = { path } > { route } < /RouterProvider>); } function useMatchRoute ( routes , path , { routeProps , overridePathParams , matchTrailingSlash , } ) { path = trailingMatch ( path , matchTrailingSlash ) ; const matchers = useMatchers ( Object . keys ( routes ) ) ; if ( path === null ) return null ; const [ routeMatch , props ] = getMatchParams ( path , matchers ) ; if ( ! routeMatch ) return null ; return routes [ routeMatch . path ] ( overridePathParams ? { ... props , ... routeProps } : { ... routeProps , ... props } ) ; } export function usePathParams < Path extends string > ( route , options ? ) ; export function usePathParams < Path extends string > ( routes , options ? ) ; export function usePathParams < Params extends ReadonlyArray < string > | string > ( routes , options = { } ) { const isSingle = ! Array . isArray ( routes ) ; const [ path , matchers ] = usePathOptions ( routes as string | string [ ] , options ) ; if ( path === null ) return isSingle ? null : emptyPathResult ; const [ routeMatch , props ] = getMatchParams ( path , matchers ) ; if ( ! routeMatch ) return isSingle ? null : emptyPathResult ; return isSingle ? props : ( [ routeMatch . path , props ] as ValueOf < { [ P in typeof routes [ number ] ] : [ P , NonEmptyRecord < ExtractPathParams < P extends `${ infer P1 }*` ? P1 : P > > ] ; } > ) ; } export function useMatch ( routes , options = { } ) { const [ path , matchers ] = usePathOptions ( routes , options ) ; const match = matchers . find ( ( { regex } ) => path ?. match ( regex ) ) ; return match ?. path ?? null ; } function usePathOptions ( routeOrRoutes , { basePath , matchTrailingSlash = true } ) { const routes = ( ! Array . isArray ( routeOrRoutes ) ? [ routeOrRoutes ] : routeOrRoutes ) as string [ ] ; const matchers = useMatchers ( routes ) ; return [ trailingMatch ( usePath ( basePath ) , matchTrailingSlash ) , matchers ] ; } function useMatchers ( routes ) { return useMemo ( ( ) => routes . map ( createRouteMatcher ) , [ hashParams ( routes ) ] ) ; } function getMatchParams ( path , routeMatchers ) { let pathParams = null ; const routeMatch = routeMatchers . find ( ( { regex } ) => { pathParams = path . match ( regex ) ; return ! ! pathParams ; } ) ; if ( ! routeMatch || pathParams === null ) return emptyPathResult ; const props = routeMatch . props . reduce ( ( props , prop , i ) => { props [ prop ] = pathParams ! [ i + 1 ] ; return props ; } , { } ) ; return [ routeMatch , props ] ; } function createRouteMatcher ( path ) { return { path , regex : new RegExp ( `${ path . substr ( 0 , 1 ) === '*' ? '' : '^' }${ escapeRegExp ( path ) . replace ( /:[a-zA-Z]+/g , '([^/]+)' ) . replace ( /\*/g , '' ) }${ path . substr ( - 1 ) === '*' ? '' : '$' }` , 'i' ) , props : ( path . match ( /:[a-zA-Z]+/g ) ?? [ ] ) . map ( ( paramName ) => paramName . substr ( 1 ) ) , } ; } export function setPath ( path ) { if ( ! isNode ) { throw new Error ( 'This method should only be used in NodeJS environments' ) ; } const url = require ( 'url' ) ; setSsrPath ( url . resolve ( getSsrPath ( ) , path ) ) ; } function hashParams ( params ) { return [ ... params ] . sort ( ) . join ( ':' ) ; } function useRedirectDetection ( basePath , path ) { const [ , forceRender ] = useState ( false ) ; useLayoutEffect ( ( ) => { if ( path !== getFormattedPath ( basePath ) ) { forceRender ( ( s ) => ! s ) ; } } , [ basePath , path ] ) ; } function trailingMatch ( path , matchTrailingSlash ) { if ( path === null ) return path ; if ( matchTrailingSlash && path && path [ path . length - 1 ] === '/' && path . length > 1 ) { path = path . substring ( 0 , path . length - 1 ) ; } return path ; } function escapeRegExp ( string ) { return string . replace ( /[-\\^$+?.()|[\]{}]/g , '\\$&' ) ; }