import { createContext , useContext , createMemo , createSignal , createRenderEffect , useTransition , untrack } from 'solid-js' ; import { createStore , reconcile } from 'solid-js/store' ; import { createMatcher , parseQuery , resolvePath , renderPath , warnOnce } from './utils' ; import type { RouteUpdateSignal , RouteState , RouterState , RouterLocation , RouteUpdateMode , RouterUtils , RouteMatch , RouterIntegration , RedirectOptions } from './types' ; const MAX_REDIRECTS = 100 ; interface Referrer { ref : string ; mode : RouteUpdateMode ; } export const RouterContext = createContext < RouterState > ( ) ; export const RouteContext = createContext < RouteState > ( ) ; export const useRouter = ( ) => { const router = useContext ( RouterContext ) ; if ( ! router ) { throw new Error ( 'No router context defined - ensure your application is wrapped with a Router component' ) ; } return router ; } ; export const useRoute = ( ) => useContext ( RouteContext ) || useRouter ( ) . base ; export const defaultUtils = { resolvePath , createMatcher , parseQuery , renderPath } ; function normalizeIntegration ( integration ) { if ( ! integration ) { return { signal : createSignal ( { value : '' } ) } ; } else if ( Array . isArray ( integration ) ) { return { signal : integration } ; } return integration ; } export function createRouter ( integration ? , basePath = '' , overrides ? ) { const { signal : [ source , setSource ] , utils : intUtils } = normalizeIntegration ( integration ) ; const utils = { ... defaultUtils , ... intUtils , ... overrides } ; const path = utils . resolvePath ( '' , basePath ) ; if ( path === undefined ) { throw new Error ( `${ basePath } is not a valid base path` ) ; } else if ( path && ! source ( ) . value ) { setSource ( { value : path , mode : 'init' } ) ; } const baseRoute = createRouteState ( utils , path , path , false , ( ) => [ path , { } ] ) ; const referrers = [ ] ; const [ isRouting , start ] = useTransition ( ) ; const [ reference , setReference ] = createSignal ( source ( ) . value ) ; const [ location ] = createStore < RouterLocation > ( { get path ( ) { return reference ( ) . split ( '?' , 1 ) [ 0 ] ; } , get queryString ( ) { return reference ( ) . split ( '?' , 2 ) [ 1 ] || '' ; } } ) ; function redirect ( mode , to , options = { resolve : false } ) { const currentRoute = useContext ( RouteContext ) || baseRoute ; const resolvedTo = options . resolve ? currentRoute . resolvePath ( to ) : utils . resolvePath ( '' , to ) ; if ( resolvedTo === undefined ) { throw new Error ( `Path '${ path }' is not a routable path` ) ; } const redirectCount = referrers . push ( { ref : untrack ( reference ) , mode } ) ; if ( redirectCount > MAX_REDIRECTS ) { throw new Error ( 'Too many redirects' ) ; } start ( ( ) => setReference ( resolvedTo ) ) ; } function handleRouteEnd ( nextRef ) { const first = referrers . shift ( ) ; if ( first ) { if ( nextRef !== first . ref ) { setSource ( { value : nextRef , mode : first . mode } ) ; } referrers . length = 0 ; } } createRenderEffect ( ( ) => { start ( ( ) => setReference ( source ( ) . value ) ) ; } ) ; createRenderEffect ( ( ) => { handleRouteEnd ( reference ( ) ) ; } ) ; return { base : baseRoute , location , query : createMapMemo ( ( ) => location . queryString ? utils . parseQuery ( location . queryString ) : { } ) , isRouting , utils , push ( to , options ) { redirect ( 'push' , to , options ) ; } , replace ( to , options ) { redirect ( 'replace' , to , options ) ; } } ; } export function createRoute ( pattern = '' , end = false ) { const router = useRouter ( ) ; const parent = useRoute ( ) ; const path = parent . resolvePath ( pattern ) ; if ( path === undefined ) { throw new Error ( `${ pattern } is not a valid path` ) ; } if ( process . env . NODE_ENV !== 'production' ) { warnOnce ( `terminal-route-${ path }` , parent . end && ! end , `Route '${ path }' is being defined under parent route '${ parent . path }' which was marked with 'end'. This route will not match` ) ; } const matcher = router . utils . createMatcher ( path , { end } ) ; const match = createMemo ( ( ) => matcher ( router . location . path ) ) ; return createRouteState ( router . utils , router . base . path , path , end , match ) ; } export function createRouteState ( utils , basePath , path , end , matchSignal ) { const match = createMemo ( ( ) => { const routeMatch = matchSignal ( ) ; return routeMatch ? routeMatch [ 0 ] : undefined ; } ) ; return { path , end , match , params : createMapMemo ( ( ) => { const routeMatch = matchSignal ( ) ; return routeMatch ? routeMatch [ 1 ] : { } ; } ) , resolvePath ( path ) { const matchPath = match ( ) ; return matchPath !== undefined ? utils . resolvePath ( basePath , path , matchPath ) : undefined ; } } ; } function createMapMemo < T > ( fn ) { const map = createMemo ( fn ) ; const data = createMemo ( map , undefined , { equals : ( a , b ) => { reconcile ( b , { key : null } ) ( a as any ) ; return true ; } } ) ; const [ state ] = createStore ( { get map ( ) { return data ( ) ; } } ) ; return new Proxy ( { } , { get ( _ , key ) { return state . map [ key as any ] ; } , ownKeys ( ) { return Reflect . ownKeys ( map ( ) ) ; } , getOwnPropertyDescriptor ( ) { return { enumerable : true , configurable : true } ; } } ) ; }