{"version":3,"file":"index.mjs","sources":["../src/utils.ts","../src/Router.tsx","../src/hooks.ts","../src/Link.tsx","../src/RouterView.tsx"],"sourcesContent":["import UrlPattern from 'url-pattern'\nimport {\n    IRoute,\n    TRouteState,\n    TRouteName,\n    TURL,\n    TRoutePath,\n    TQuery,\n    IQueryObject,\n    THook,\n} from './types'\n\nexport function getRouteFromUrl<T>(\n    routes: IRoute<T>[],\n    url: TURL,\n    Matcher: any = UrlPattern,\n) {\n    return (\n        routes.find(({ path }) => new Matcher(path).match(url)) ||\n        getRouteByName(routes, 'notFound')\n    )\n}\n\nexport function getRouteByName<T>(routes: IRoute<T>[], routeName: TRouteName) {\n    return routes.find(({ name }) => name === routeName)\n}\n\nexport function getStateFromUrl(\n    path: TRoutePath,\n    url: TURL,\n    Matcher: any = UrlPattern,\n): TRouteState {\n    return new Matcher(path).match(url)\n}\n\nexport function deriveUrlFromPathAndState(\n    path: TRoutePath,\n    state: TRouteState,\n    query?: TQuery,\n    Matcher: any = UrlPattern,\n) {\n    const pathname = new Matcher(path).stringify(state)\n    const search = parseObjectIntoQueryString(query)\n    return `${pathname}${search}`\n}\n\nfunction getRouteAndStateFromUrl<T = THook>(routes: IRoute<T>[], url: TURL) {\n    const route = getRouteFromUrl<T>(routes, url)!\n    const state = getStateFromUrl(route.path, url)\n    return { route, state }\n}\n\nexport function getMatchFromUrl<T = THook>(routes: IRoute<T>[], url: TURL): Promise<void> {\n    const {\n        route: { onEnter, ...route },\n        state,\n    } = getRouteAndStateFromUrl<T>(routes, url)\n    return onEnter && typeof onEnter === 'function' ? onEnter(route, state) : Promise.resolve()\n}\n\nexport function getMatchWithCalleeFromUrl<T = THook>(\n    hookCallee: (\n        route: IRoute<T>,\n        state: TRouteState,\n    ) => (hook: any) => Promise<any>,\n    routes: IRoute<T>[],\n    url: TURL,\n) {\n    const { route, state } = getRouteAndStateFromUrl<T>(routes, url)\n    return route.onEnter && hookCallee(route, state)(route.onEnter)\n}\n\nexport function parseQueryStringIntoObject(queryString?: string) {\n    if (!queryString || typeof queryString !== 'string') {\n        return {}\n    }\n    const params: TQuery = {}\n    queryString.split('&').forEach(segment => {\n        const [key, value] = segment.split('=')\n        params[key] = value\n    })\n    return params\n}\n\nexport function parseObjectIntoQueryString(queryString?: IQueryObject) {\n    if (\n        !queryString ||\n        typeof queryString !== 'object' ||\n        Object.keys(queryString).length === 0\n    ) {\n        return ''\n    }\n    return `?${Object.keys(queryString)\n        .map(key => (queryString[key] ? key + '=' + queryString[key] : key))\n        .join('&')}`\n}\n","import React, {\n    createContext,\n    useMemo,\n    useCallback,\n    useState,\n    useEffect,\n    ProviderExoticComponent,\n    ProviderProps,\n} from 'react'\nimport {\n    IContext,\n    IProvider,\n    TRouteName,\n    TRouteState,\n    IData,\n    IRoute,\n    THook,\n    TDefaultHookCallee,\n    TQuery,\n} from './types'\nimport {\n    getRouteFromUrl,\n    getStateFromUrl,\n    getRouteByName,\n    deriveUrlFromPathAndState,\n    parseQueryStringIntoObject,\n} from './utils'\n\nconst SERVER = typeof window === 'undefined'\n\nexport const Context = createContext<IContext>(null!)\n\nfunction defaultHookCallee(route: IRoute, state: TRouteState) {\n    return (hook: THook) => hook(route, state)\n}\n\nexport function Provider<HookCallee = TDefaultHookCallee, Hook = THook>({\n    children,\n    routes,\n    queryString,\n    url = window.location.pathname,\n    onBeforeExit,\n    onEnter,\n    // @ts-ignore\n    hookCallee = defaultHookCallee,\n}: IProvider<HookCallee, Hook>) {\n    if (!url && SERVER) {\n        throw new Error(\n            '[oatmilk] You must pass a URL when rendering on the server.',\n        )\n    } else if (SERVER && typeof url !== 'string') {\n        throw new Error(\n            '[oatmilk] You must pass a string as the URL when rendering on the server.',\n        )\n    }\n\n    if (routes.length === 0) {\n        throw new Error('[oatmilk] You must provide routes')\n    }\n\n    const firstRoute = getRouteFromUrl<Hook>(routes, url)\n\n    const [{ route, state, queryParams }, setData] = useState<IData<Hook>>({\n        route: firstRoute!,\n        queryParams: parseQueryStringIntoObject(\n            !SERVER && !queryString\n                ? window.location.search.substr(1)\n                : queryString,\n        ),\n        state: firstRoute ? getStateFromUrl(firstRoute.path, url) : {},\n    })\n\n    const goTo = useCallback(\n        async (\n            toRouteName: TRouteName,\n            toState: TRouteState = {},\n            toQuery: TQuery = {},\n            withPrefix?: string\n        ) => {\n            const toRoute = getRouteByName(routes, toRouteName)\n\n            if (!toRoute) {\n                throw new Error(`[oatmilk] Route ${toRouteName} does not exist`)\n            }\n\n            // allows overriding of path\n            // which helps when many urls share the same view\n            if (withPrefix) {\n                toRoute.path = `${withPrefix}${toRoute.path}`\n            }\n\n            await Promise.all([\n                route.onBeforeExit &&\n                    // @ts-ignore\n                    hookCallee(route, state, queryParams)(route.onBeforeExit),\n                onBeforeExit &&\n                    // @ts-ignore\n                    hookCallee(route, state, queryParams)(onBeforeExit),\n            ])\n\n            if (onEnter) {\n                // @ts-ignore\n                hookCallee(toRoute, toState, toQuery)(onEnter)\n            }\n            if (toRoute.onEnter) {\n                // @ts-ignore\n                hookCallee(toRoute, toState, toQuery)(toRoute.onEnter)\n            }\n\n            setData({ route: toRoute, state: toState, queryParams: toQuery })\n        },\n        [routes, route, hookCallee, state],\n    )\n\n    const getHref = useCallback(\n        (routeName: string, state: TRouteState = {}, queryParams?: TQuery) => {\n            const route = getRouteByName(routes, routeName)\n            if (!route) {\n                throw new Error(\n                    `[oatmilk] Failed to generate href: route \"${routeName}\" does not exist`,\n                )\n            }\n            return deriveUrlFromPathAndState(route.path, state, queryParams)\n        },\n        [routes],\n    )\n\n    useEffect(() => {\n        if (SERVER) return\n\n        if (onEnter) {\n            // @ts-ignore\n            hookCallee(route, state)(onEnter)\n        }\n        if (route.onEnter) {\n            // @ts-ignore\n            hookCallee(route, state)(route.onEnter)\n        }\n    }, [])\n\n    useEffect(() => {\n        if (SERVER) return\n\n        if (route.name === 'notFound') {\n            return window.history.replaceState(\n                { routeName: route.name },\n                '',\n                window.location.pathname,\n            )\n        }\n\n        const url = deriveUrlFromPathAndState(route.path, state, queryParams)\n        const historyState = {\n            routeName: route.name,\n            state,\n            queryParams,\n            _from_oatmilk: true,\n        }\n        if (url !== window.location.pathname) {\n            window.history.pushState(historyState, '', url)\n        } else {\n            // ensure's that the query params an hash are kept on first page load\n            window.history.replaceState(\n                historyState,\n                '',\n                `${url}${window.location.search}${window.location.hash}`,\n            )\n        }\n    }, [route.name, routes, route.path, route.view, queryParams, state])\n\n    useEffect(() => {\n        if (SERVER) return\n\n        function onUserNavigating({ state: historyState }: PopStateEvent) {\n            if (\n                historyState &&\n                historyState.routeName &&\n                historyState.state &&\n                historyState._from_oatmilk\n            ) {\n                goTo(historyState.routeName, historyState.state)\n            }\n        }\n\n        window.addEventListener('popstate', onUserNavigating)\n        return () => window.removeEventListener('popstate', onUserNavigating)\n    }, [])\n\n    const value = useMemo(\n        () => ({\n            goTo,\n            state,\n            queryParams,\n            route: {\n                name: route.name,\n                path: route.path,\n                view: route.view,\n            },\n            getHref,\n            routes,\n        }),\n        [route.name, route.path, routes, route.view, state],\n    )\n\n    return <Context.Provider value={value}>{children}</Context.Provider>\n}\n","import { useContext } from 'react'\nimport { Context } from './Router'\n\nexport function useOatmilk() {\n    return useContext(Context)\n}\n","import React, { forwardRef, MouseEvent } from 'react'\nimport { useOatmilk } from './hooks'\nimport { ILinkProps } from './types'\n\nconst Link = forwardRef<HTMLAnchorElement, ILinkProps>(\n    (\n        {\n            onClick: onClickFromUser,\n            children,\n            queryParams,\n            routeName,\n            state,\n            prefix = '',\n            suffix = '',\n            ...props\n        },\n        ref,\n    ) => {\n        const { goTo, getHref } = useOatmilk()\n\n        const onClick = (e: MouseEvent<HTMLAnchorElement>) => {\n            if (typeof onClickFromUser === 'function') {\n                onClickFromUser(e)\n            }\n            if (\n                e.button === 0 && // ignore right clicks\n                !props.target && // let browser handle \"target=_blank\"\n                !e.defaultPrevented && // onClick prevented default\n                !e.metaKey && // ignore clicks with modifier keys...\n                !e.altKey &&\n                !e.ctrlKey &&\n                !e.shiftKey\n            ) {\n                e.preventDefault()\n                goTo(routeName, state, queryParams, prefix)\n            }\n        }\n\n        const href = `${prefix}${getHref(\n            routeName,\n            state,\n            queryParams,\n        )}${suffix}`.trim()\n\n        return (\n            <a {...props} href={href} ref={ref} onClick={onClick}>\n                {children}\n            </a>\n        )\n    },\n)\n\nexport { Link }\n","import React from 'react'\nimport { useOatmilk } from './hooks'\n\nexport function RouterView() {\n    const { route } = useOatmilk()\n\n    if (!route || !route.view) {\n        return null\n    }\n\n    return <route.view />\n}\n"],"names":["getRouteFromUrl","routes","url","Matcher","UrlPattern","find","ref","match","getRouteByName","routeName","getStateFromUrl","path","deriveUrlFromPathAndState","state","query","stringify","queryString","Object","keys","length","map","key","join","getRouteAndStateFromUrl","route","getMatchFromUrl","onEnter","Promise","resolve","getMatchWithCalleeFromUrl","hookCallee","parseQueryStringIntoObject","params","split","forEach","segment","const","SERVER","window","Context","createContext","defaultHookCallee","hook","Provider","location","pathname","Error","firstRoute","useState","queryParams","search","substr","goTo","useCallback","toRouteName","toState","toQuery","withPrefix","toRoute","all","onBeforeExit","setData","getHref","useEffect","name","history","replaceState","historyState","_from_oatmilk","pushState","view","addEventListener","onUserNavigating","removeEventListener","value","useMemo","React","children","useOatmilk","useContext","Link","forwardRef","ref$1","href","prefix","suffix","trim","props","onClick","e","onClickFromUser","button","target","defaultPrevented","metaKey","altKey","ctrlKey","shiftKey","preventDefault","RouterView"],"mappings":"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"}