import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web'; import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, sharedConfig, getListener, $TRACK, splitProps, createResource, catchError } from 'solid-js'; import { createStore, reconcile, unwrap } from 'solid-js/store'; function createBeforeLeave() { let listeners = new Set(); function subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } let ignore = false; function confirm(to, options) { if (ignore) return !(ignore = false); const e = { to, options, defaultPrevented: false, preventDefault: () => e.defaultPrevented = true }; for (const l of listeners) l.listener({ ...e, from: l.location, retry: force => { force && (ignore = true); l.navigate(to, { ...options, resolve: false }); } }); return !e.defaultPrevented; } return { subscribe, confirm }; } // The following supports browser initiated blocking (eg back/forward) let depth; function saveCurrentDepth() { if (!window.history.state || window.history.state._depth == null) { window.history.replaceState({ ...window.history.state, _depth: window.history.length - 1 }, ""); } depth = window.history.state._depth; } if (!isServer) { saveCurrentDepth(); } function keepDepth(state) { return { ...state, _depth: window.history.state && window.history.state._depth }; } function notifyIfNotBlocked(notify, block) { let ignore = false; return () => { const prevDepth = depth; saveCurrentDepth(); const delta = prevDepth == null ? null : depth - prevDepth; if (ignore) { ignore = false; return; } if (delta && block(delta)) { ignore = true; window.history.go(-delta); } else { notify(); } }; } const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i; const trimPathRegex = /^\/+|(\/)\/+$/g; const mockBase = "http://sr"; function normalizePath(path, omitSlash = false) { const s = path.replace(trimPathRegex, "$1"); return s ? omitSlash || /^[?#]/.test(s) ? s : "/" + s : ""; } function resolvePath(base, path, from) { if (hasSchemeRegex.test(path)) { return undefined; } const basePath = normalizePath(base); const fromPath = from && normalizePath(from); let result = ""; if (!fromPath || path.startsWith("/")) { result = basePath; } else if (fromPath.toLowerCase().indexOf(basePath.toLowerCase()) !== 0) { result = basePath + fromPath; } else { result = fromPath; } return (result || "/") + normalizePath(path, !result); } function invariant(value, message) { if (value == null) { throw new Error(message); } return value; } function joinPaths(from, to) { return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to); } function extractSearchParams(url) { const params = {}; url.searchParams.forEach((value, key) => { if (key in params) { if (Array.isArray(params[key])) params[key].push(value);else params[key] = [params[key], value]; } else params[key] = value; }); return params; } function createMatcher(path, partial, matchFilters) { const [pattern, splat] = path.split("/*", 2); const segments = pattern.split("/").filter(Boolean); const len = segments.length; return location => { const locSegments = location.split("/").filter(Boolean); const lenDiff = locSegments.length - len; if (lenDiff < 0 || lenDiff > 0 && splat === undefined && !partial) { return null; } const match = { path: len ? "" : "/", params: {} }; const matchFilter = s => matchFilters === undefined ? undefined : matchFilters[s]; for (let i = 0; i < len; i++) { const segment = segments[i]; const dynamic = segment[0] === ":"; const locSegment = dynamic ? locSegments[i] : locSegments[i].toLowerCase(); const key = dynamic ? segment.slice(1) : segment.toLowerCase(); if (dynamic && matchSegment(locSegment, matchFilter(key))) { match.params[key] = locSegment; } else if (dynamic || !matchSegment(locSegment, key)) { return null; } match.path += `/${locSegment}`; } if (splat) { const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : ""; if (matchSegment(remainder, matchFilter(splat))) { match.params[splat] = remainder; } else { return null; } } return match; }; } function matchSegment(input, filter) { const isEqual = s => s === input; if (filter === undefined) { return true; } else if (typeof filter === "string") { return isEqual(filter); } else if (typeof filter === "function") { return filter(input); } else if (Array.isArray(filter)) { return filter.some(isEqual); } else if (filter instanceof RegExp) { return filter.test(input); } return false; } function scoreRoute(route) { const [pattern, splat] = route.pattern.split("/*", 2); const segments = pattern.split("/").filter(Boolean); return segments.reduce((score, segment) => score + (segment.startsWith(":") ? 2 : 3), segments.length - (splat === undefined ? 0 : 1)); } function createMemoObject(fn) { const map = new Map(); const owner = getOwner(); return new Proxy({}, { get(_, property) { if (!map.has(property)) { runWithOwner(owner, () => map.set(property, createMemo(() => fn()[property]))); } return map.get(property)(); }, getOwnPropertyDescriptor() { return { enumerable: true, configurable: true }; }, ownKeys() { return Reflect.ownKeys(fn()); }, has(_, property) { return property in fn(); } }); } function mergeSearchString(search, params) { const merged = new URLSearchParams(search); Object.entries(params).forEach(([key, value]) => { if (value == null || value === "" || value instanceof Array && !value.length) { merged.delete(key); } else { if (value instanceof Array) { // Delete all instances of the key before appending merged.delete(key); value.forEach(v => { merged.append(key, String(v)); }); } else { merged.set(key, String(value)); } } }); const s = merged.toString(); return s ? `?${s}` : ""; } function expandOptionals(pattern) { let match = /(\/?\:[^\/]+)\?/.exec(pattern); if (!match) return [pattern]; let prefix = pattern.slice(0, match.index); let suffix = pattern.slice(match.index + match[0].length); const prefixes = [prefix, prefix += match[1]]; // This section handles adjacent optional params. We don't actually want all permuations since // that will lead to equivalent routes which have the same number of params. For example // `/:a?/:b?/:c`? only has the unique expansion: `/`, `/:a`, `/:a/:b`, `/:a/:b/:c` and we can // discard `/:b`, `/:c`, `/:b/:c` by building them up in order and not recursing. This also helps // ensure predictability where earlier params have precidence. while (match = /^(\/\:[^\/]+)\?/.exec(suffix)) { prefixes.push(prefix += match[1]); suffix = suffix.slice(match[0].length); } return expandOptionals(suffix).reduce((results, expansion) => [...results, ...prefixes.map(p => p + expansion)], []); } function setFunctionName(obj, value) { Object.defineProperty(obj, "name", { value, writable: false, configurable: false }); return obj; } const MAX_REDIRECTS = 100; /** Consider this API opaque and internal. It is likely to change in the future. */ const RouterContextObj = createContext(); const RouteContextObj = createContext(); const useRouter = () => invariant(useContext(RouterContextObj), " and 'use' router primitives can be only used inside a Route."); const useRoute = () => useContext(RouteContextObj) || useRouter().base; const useResolvedPath = path => { const route = useRoute(); return createMemo(() => route.resolvePath(path())); }; const useHref = to => { const router = useRouter(); return createMemo(() => { const to_ = to(); return to_ !== undefined ? router.renderPath(to_) : to_; }); }; /** * Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options: * * - resolve (*boolean*, default `true`): resolve the path against the current route * - replace (*boolean*, default `false`): replace the history entry * - scroll (*boolean*, default `true`): scroll to top after navigation * - state (*any*, default `undefined`): pass custom state to `location.state` * * **Note**: The state is serialized using the structured clone algorithm which does not support all object types. * * @example * ```js * const navigate = useNavigate(); * * if (unauthorized) { * navigate("/login", { replace: true }); * } * ``` */ const useNavigate = () => useRouter().navigatorFactory(); /** * Retrieves reactive `location` object useful for getting things like `pathname`. * * @example * ```js * const location = useLocation(); * * const pathname = createMemo(() => parsePath(location.pathname)); * ``` */ const useLocation = () => useRouter().location; /** * Retrieves signal that indicates whether the route is currently in a *Transition*. * Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering. * * @example * ```js * const isRouting = useIsRouting(); * * return ( *
* *
* ); * ``` */ const useIsRouting = () => useRouter().isRouting; /** * usePreloadRoute returns a function that can be used to preload a route manual. * This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API. * * @example * ```js * const preload = usePreloadRoute(); * * preload(`/users/settings`, { preloadData: true }); * ``` */ const usePreloadRoute = () => { const pre = useRouter().preloadRoute; return (url, options = {}) => pre(url instanceof URL ? url : new URL(url, mockBase), options.preloadData); }; /** * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path. * Useful for determining if a given path matches the current route. * * @example * ```js * const match = useMatch(() => props.href); * * return
; * ``` */ const useMatch = (path, matchFilters) => { const location = useLocation(); const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters))); return createMemo(() => { for (const matcher of matchers()) { const match = matcher(location.pathname); if (match) return match; } }); }; /** * `useCurrentMatches` returns all the matches for the current matched route. * Useful for getting all the route information. * * @example * ```js * const matches = useCurrentMatches(); * * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb)) * ``` */ const useCurrentMatches = () => useRouter().matches; /** * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route. * * @example * ```js * const params = useParams(); * * // fetch user based on the id path parameter * const [user] = createResource(() => params.id, fetchUser); * ``` */ const useParams = () => useRouter().params; /** * Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them. * The object is a proxy so you must access properties to subscribe to reactive updates. * **Note** that values will be strings and property names will retain their casing. * * The setter method accepts an object whose entries will be merged into the current query string. * Values `''`, `undefined` and `null` will remove the key from the resulting query string. * Updates will behave just like a navigation and the setter accepts the same optional second parameter as `navigate` and auto-scrolling is disabled by default. * * @examples * ```js * const [searchParams, setSearchParams] = useSearchParams(); * * return ( *
* Page: {searchParams.page} * *
* ); * ``` */ const useSearchParams = () => { const location = useLocation(); const navigate = useNavigate(); const setSearchParams = (params, options) => { const searchString = untrack(() => mergeSearchString(location.search, params) + location.hash); navigate(searchString, { scroll: false, resolve: false, ...options }); }; return [location.query, setSearchParams]; }; /** * useBeforeLeave takes a function that will be called prior to leaving a route. * The function will be called with: * * - from (*Location*): current location (before change). * - to (*string | number*): path passed to `navigate`. * - options (*NavigateOptions*): options passed to navigate. * - preventDefault (*function*): call to block the route change. * - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`. * - retry (*function*, force?: boolean ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming). * * @example * ```js * useBeforeLeave((e: BeforeLeaveEventArgs) => { * if (form.isDirty && !e.defaultPrevented) { * // preventDefault to block immediately and prompt user async * e.preventDefault(); * setTimeout(() => { * if (window.confirm("Discard unsaved changes - are you sure?")) { * // user wants to proceed anyway so retry with force=true * e.retry(true); * } * }, 100); * } * }); * ``` */ const useBeforeLeave = listener => { const s = useRouter().beforeLeave.subscribe({ listener, location: useLocation(), navigate: useNavigate() }); onCleanup(s); }; function createRoutes(routeDef, base = "") { const { component, preload, load, children, info } = routeDef; const isLeaf = !children || Array.isArray(children) && !children.length; const shared = { key: routeDef, component, preload: preload || load, info }; return asArray(routeDef.path).reduce((acc, originalPath) => { for (const expandedPath of expandOptionals(originalPath)) { const path = joinPaths(base, expandedPath); let pattern = isLeaf ? path : path.split("/*", 1)[0]; pattern = pattern.split("/").map(s => { return s.startsWith(":") || s.startsWith("*") ? s : encodeURIComponent(s); }).join("/"); acc.push({ ...shared, originalPath, pattern, matcher: createMatcher(pattern, !isLeaf, routeDef.matchFilters) }); } return acc; }, []); } function createBranch(routes, index = 0) { return { routes, score: scoreRoute(routes[routes.length - 1]) * 10000 - index, matcher(location) { const matches = []; for (let i = routes.length - 1; i >= 0; i--) { const route = routes[i]; const match = route.matcher(location); if (!match) { return null; } matches.unshift({ ...match, route }); } return matches; } }; } function asArray(value) { return Array.isArray(value) ? value : [value]; } function createBranches(routeDef, base = "", stack = [], branches = []) { const routeDefs = asArray(routeDef); for (let i = 0, len = routeDefs.length; i < len; i++) { const def = routeDefs[i]; if (def && typeof def === "object") { if (!def.hasOwnProperty("path")) def.path = ""; const routes = createRoutes(def, base); for (const route of routes) { stack.push(route); const isEmptyArray = Array.isArray(def.children) && def.children.length === 0; if (def.children && !isEmptyArray) { createBranches(def.children, route.pattern, stack, branches); } else { const branch = createBranch([...stack], branches.length); branches.push(branch); } stack.pop(); } } } // Stack will be empty on final return return stack.length ? branches : branches.sort((a, b) => b.score - a.score); } function getRouteMatches(branches, location) { for (let i = 0, len = branches.length; i < len; i++) { const match = branches[i].matcher(location); if (match) { return match; } } return []; } function createLocation(path, state, queryWrapper) { const origin = new URL(mockBase); const url = createMemo(prev => { const path_ = path(); try { return new URL(path_, origin); } catch (err) { console.error(`Invalid path ${path_}`); return prev; } }, origin, { equals: (a, b) => a.href === b.href }); const pathname = createMemo(() => url().pathname); const search = createMemo(() => url().search, true); const hash = createMemo(() => url().hash); const key = () => ""; const queryFn = on(search, () => extractSearchParams(url())); return { get pathname() { return pathname(); }, get search() { return search(); }, get hash() { return hash(); }, get state() { return state(); }, get key() { return key(); }, query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn) }; } let intent; function getIntent() { return intent; } let inPreloadFn = false; function getInPreloadFn() { return inPreloadFn; } function setInPreloadFn(value) { inPreloadFn = value; } function createRouterContext(integration, branches, getContext, options = {}) { const { signal: [source, setSource], utils = {} } = integration; const parsePath = utils.parsePath || (p => p); const renderPath = utils.renderPath || (p => p); const beforeLeave = utils.beforeLeave || createBeforeLeave(); const basePath = resolvePath("", options.base || ""); if (basePath === undefined) { throw new Error(`${basePath} is not a valid base path`); } else if (basePath && !source().value) { setSource({ value: basePath, replace: true, scroll: false }); } const [isRouting, setIsRouting] = createSignal(false); // Keep track of last target, so that last call to transition wins let lastTransitionTarget; // Transition the location to a new value const transition = (newIntent, newTarget) => { if (newTarget.value === reference() && newTarget.state === state()) return; if (lastTransitionTarget === undefined) setIsRouting(true); intent = newIntent; lastTransitionTarget = newTarget; startTransition(() => { if (lastTransitionTarget !== newTarget) return; setReference(lastTransitionTarget.value); setState(lastTransitionTarget.state); resetErrorBoundaries(); if (!isServer) submissions[1](subs => subs.filter(s => s.pending)); }).finally(() => { if (lastTransitionTarget !== newTarget) return; // Batch, in order for isRouting and final source update to happen together batch(() => { intent = undefined; if (newIntent === "navigate") navigateEnd(lastTransitionTarget); setIsRouting(false); lastTransitionTarget = undefined; }); }); }; const [reference, setReference] = createSignal(source().value); const [state, setState] = createSignal(source().state); const location = createLocation(reference, state, utils.queryWrapper); const referrers = []; const submissions = createSignal(isServer ? initFromFlash() : []); const matches = createMemo(() => { if (typeof options.transformUrl === "function") { return getRouteMatches(branches(), options.transformUrl(location.pathname)); } return getRouteMatches(branches(), location.pathname); }); const buildParams = () => { const m = matches(); const params = {}; for (let i = 0; i < m.length; i++) { Object.assign(params, m[i].params); } return params; }; const params = utils.paramsWrapper ? utils.paramsWrapper(buildParams, branches) : createMemoObject(buildParams); const baseRoute = { pattern: basePath, path: () => basePath, outlet: () => null, resolvePath(to) { return resolvePath(basePath, to); } }; // Create a native transition, when source updates createRenderEffect(on(source, source => transition("native", source), { defer: true })); return { base: baseRoute, location, params, isRouting, renderPath, parsePath, navigatorFactory, matches, beforeLeave, preloadRoute, singleFlight: options.singleFlight === undefined ? true : options.singleFlight, submissions }; function navigateFromRoute(route, to, options) { // Untrack in case someone navigates in an effect - don't want to track `reference` or route paths untrack(() => { if (typeof to === "number") { if (!to) ; else if (utils.go) { utils.go(to); } else { console.warn("Router integration does not support relative routing"); } return; } const queryOnly = !to || to[0] === "?"; const { replace, resolve, scroll, state: nextState } = { replace: false, resolve: !queryOnly, scroll: true, ...options }; const resolvedTo = resolve ? route.resolvePath(to) : resolvePath(queryOnly && location.pathname || "", to); if (resolvedTo === undefined) { throw new Error(`Path '${to}' is not a routable path`); } else if (referrers.length >= MAX_REDIRECTS) { throw new Error("Too many redirects"); } const current = reference(); if (resolvedTo !== current || nextState !== state()) { if (isServer) { const e = getRequestEvent(); e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) }); setSource({ value: resolvedTo, replace, scroll, state: nextState }); } else if (beforeLeave.confirm(resolvedTo, options)) { referrers.push({ value: current, replace, scroll, state: state() }); transition("navigate", { value: resolvedTo, state: nextState }); } } }); } function navigatorFactory(route) { // Workaround for vite issue (https://github.com/vitejs/vite/issues/3803) route = route || useContext(RouteContextObj) || baseRoute; return (to, options) => navigateFromRoute(route, to, options); } function navigateEnd(next) { const first = referrers[0]; if (first) { setSource({ ...next, replace: first.replace, scroll: first.scroll }); referrers.length = 0; } } function preloadRoute(url, preloadData) { const matches = getRouteMatches(branches(), url.pathname); const prevIntent = intent; intent = "preload"; for (let match in matches) { const { route, params } = matches[match]; route.component && route.component.preload && route.component.preload(); const { preload } = route; inPreloadFn = true; preloadData && preload && runWithOwner(getContext(), () => preload({ params, location: { pathname: url.pathname, search: url.search, hash: url.hash, query: extractSearchParams(url), state: null, key: "" }, intent: "preload" })); inPreloadFn = false; } intent = prevIntent; } function initFromFlash() { const e = getRequestEvent(); return e && e.router && e.router.submission ? [e.router.submission] : []; } } function createRouteContext(router, parent, outlet, match) { const { base, location, params } = router; const { pattern, component, preload } = match().route; const path = createMemo(() => match().path); component && component.preload && component.preload(); inPreloadFn = true; const data = preload ? preload({ params, location, intent: intent || "initial" }) : undefined; inPreloadFn = false; const route = { parent, pattern, path, outlet: () => component ? createComponent(component, { params, location, data, get children() { return outlet(); } }) : outlet(), resolvePath(to) { return resolvePath(base.path(), to, path()); } }; return route; } const createRouterComponent = router => props => { const { base } = props; const routeDefs = children(() => props.children); const branches = createMemo(() => createBranches(routeDefs(), props.base || "")); let context; const routerState = createRouterContext(router, branches, () => context, { base, singleFlight: props.singleFlight, transformUrl: props.transformUrl }); router.create && router.create(routerState); return createComponent$1(RouterContextObj.Provider, { value: routerState, get children() { return createComponent$1(Root, { routerState: routerState, get root() { return props.root; }, get preload() { return props.rootPreload || props.rootLoad; }, get children() { return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, { routerState: routerState, get branches() { return branches(); } })]; } }); } }); }; function Root(props) { const location = props.routerState.location; const params = props.routerState.params; const data = createMemo(() => props.preload && untrack(() => { setInPreloadFn(true); props.preload({ params, location, intent: getIntent() || "initial" }); setInPreloadFn(false); })); return createComponent$1(Show, { get when() { return props.root; }, keyed: true, get fallback() { return props.children; }, children: Root => createComponent$1(Root, { params: params, location: location, get data() { return data(); }, get children() { return props.children; } }) }); } function Routes(props) { if (isServer) { const e = getRequestEvent(); if (e && e.router && e.router.dataOnly) { dataOnly(e, props.routerState, props.branches); return; } e && ((e.router || (e.router = {})).matches || (e.router.matches = props.routerState.matches().map(({ route, path, params }) => ({ path: route.originalPath, pattern: route.pattern, match: path, params, info: route.info })))); } const disposers = []; let root; const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => { let equal = prevMatches && nextMatches.length === prevMatches.length; const next = []; for (let i = 0, len = nextMatches.length; i < len; i++) { const prevMatch = prevMatches && prevMatches[i]; const nextMatch = nextMatches[i]; if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) { next[i] = prev[i]; } else { equal = false; if (disposers[i]) { disposers[i](); } createRoot(dispose => { disposers[i] = dispose; next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => { const routeMatches = props.routerState.matches(); return routeMatches[i] ?? routeMatches[0]; }); }); } } disposers.splice(nextMatches.length).forEach(dispose => dispose()); if (prev && equal) { return prev; } root = next[0]; return next; })); return createOutlet(() => routeStates() && root)(); } const createOutlet = child => { return () => createComponent$1(Show, { get when() { return child(); }, keyed: true, children: child => createComponent$1(RouteContextObj.Provider, { value: child, get children() { return child.outlet(); } }) }); }; const Route = props => { const childRoutes = children(() => props.children); return mergeProps(props, { get children() { return childRoutes(); } }); }; // for data only mode with single flight mutations function dataOnly(event, routerState, branches) { const url = new URL(event.request.url); const prevMatches = getRouteMatches(branches, new URL(event.router.previousUrl || event.request.url).pathname); const matches = getRouteMatches(branches, url.pathname); for (let match = 0; match < matches.length; match++) { if (!prevMatches[match] || matches[match].route !== prevMatches[match].route) event.router.dataOnly = true; const { route, params } = matches[match]; route.preload && route.preload({ params, location: routerState.location, intent: "preload" }); } } function intercept([value, setValue], get, set) { return [value, set ? v => setValue(set(v)) : setValue]; } function createRouter(config) { let ignore = false; const wrap = value => typeof value === "string" ? { value } : value; const signal = intercept(createSignal(wrap(config.get()), { equals: (a, b) => a.value === b.value && a.state === b.state }), undefined, next => { !ignore && config.set(next); if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true; return next; }); config.init && onCleanup(config.init((value = config.get()) => { ignore = true; signal[1](wrap(value)); ignore = false; })); return createRouterComponent({ signal, create: config.create, utils: config.utils }); } function bindEvent(target, type, handler) { target.addEventListener(type, handler); return () => target.removeEventListener(type, handler); } function scrollToHash(hash, fallbackTop) { const el = hash && document.getElementById(hash); if (el) { el.scrollIntoView(); } else if (fallbackTop) { window.scrollTo(0, 0); } } function getPath(url) { const u = new URL(url); return u.pathname + u.search; } function StaticRouter(props) { let e; const obj = { value: props.url || (e = getRequestEvent()) && getPath(e.request.url) || "" }; return createRouterComponent({ signal: [() => obj, next => Object.assign(obj, next)] })(props); } const LocationHeader = "Location"; const PRELOAD_TIMEOUT = 5000; const CACHE_TIMEOUT = 180000; let cacheMap = new Map(); // cleanup forward/back cache if (!isServer) { setInterval(() => { const now = Date.now(); for (let [k, v] of cacheMap.entries()) { if (!v[4].count && now - v[0] > CACHE_TIMEOUT) { cacheMap.delete(k); } } }, 300000); } function getCache() { if (!isServer) return cacheMap; const req = getRequestEvent(); if (!req) throw new Error("Cannot find cache context"); return (req.router || (req.router = {})).cache || (req.router.cache = new Map()); } /** * Revalidates the given cache entry/entries. */ function revalidate(key, force = true) { return startTransition(() => { const now = Date.now(); cacheKeyOp(key, entry => { force && (entry[0] = 0); //force cache miss entry[4][1](now); // retrigger live signals }); }); } function cacheKeyOp(key, fn) { key && !Array.isArray(key) && (key = [key]); for (let k of cacheMap.keys()) { if (key === undefined || matchKey(k, key)) fn(cacheMap.get(k)); } } function query(fn, name) { // prioritize GET for server functions if (fn.GET) fn = fn.GET; const cachedFn = (...args) => { const cache = getCache(); const intent = getIntent(); const inPreloadFn = getInPreloadFn(); const owner = getOwner(); const navigate = owner ? useNavigate() : undefined; const now = Date.now(); const key = name + hashKey(args); let cached = cache.get(key); let tracking; if (isServer) { const e = getRequestEvent(); if (e) { const dataOnly = (e.router || (e.router = {})).dataOnly; if (dataOnly) { const data = e && (e.router.data || (e.router.data = {})); if (data && key in data) return data[key]; if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) { data[key] = undefined; return Promise.resolve(); } } } } if (getListener() && !isServer) { tracking = true; onCleanup(() => cached[4].count--); } if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) { if (tracking) { cached[4].count++; cached[4][0](); // track } if (cached[3] === "preload" && intent !== "preload") { cached[0] = now; } let res = cached[1]; if (intent !== "preload") { res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]); !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version } inPreloadFn && "then" in res && res.catch(() => {}); return res; } let res; if (!isServer && sharedConfig.has && sharedConfig.has(key)) { res = sharedConfig.load(key); // hydrating // @ts-ignore at least until we add a delete method to sharedConfig delete globalThis._$HY.r[key]; } else res = fn(...args); if (cached) { cached[0] = now; cached[1] = res; cached[3] = intent; !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version } else { cache.set(key, cached = [now, res,, intent, createSignal(now)]); cached[4].count = 0; } if (tracking) { cached[4].count++; cached[4][0](); // track } if (isServer) { const e = getRequestEvent(); if (e && e.router.dataOnly) return e.router.data[key] = res; } if (intent !== "preload") { res = "then" in res ? res.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(res); } inPreloadFn && "then" in res && res.catch(() => {}); // serialize on server if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) { const e = getRequestEvent(); (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res); } return res; function handleResponse(error) { return async v => { if (v instanceof Response) { const e = getRequestEvent(); if (e) { for (const [key, value] of v.headers) { if (key == "set-cookie") e.response.headers.append("set-cookie", value);else e.response.headers.set(key, value); } } const url = v.headers.get(LocationHeader); if (url !== null) { // client + server relative redirect if (navigate && url.startsWith("/")) startTransition(() => { navigate(url, { replace: true }); });else if (!isServer) window.location.href = url;else if (e) e.response.status = 302; return; } if (v.customBody) v = await v.customBody(); } if (error) throw v; cached[2] = v; return v; }; } }; cachedFn.keyFor = (...args) => name + hashKey(args); cachedFn.key = name; return cachedFn; } query.get = key => { const cached = getCache().get(key); return cached[2]; }; query.set = (key, value) => { const cache = getCache(); const now = Date.now(); let cached = cache.get(key); if (cached) { cached[0] = now; cached[1] = Promise.resolve(value); cached[2] = value; cached[3] = "preload"; } else { cache.set(key, cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]); cached[4].count = 0; } }; query.delete = key => getCache().delete(key); query.clear = () => getCache().clear(); /** @deprecated use query instead */ const cache = query; function matchKey(key, keys) { for (let k of keys) { if (k && key.startsWith(k)) return true; } return false; } // Modified from the amazing Tanstack Query library (MIT) // https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts#L168 function hashKey(args) { return JSON.stringify(args, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => { result[key] = val[key]; return result; }, {}) : val); } function isPlainObject(obj) { let proto; return obj != null && typeof obj === "object" && (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype); } const actions = /* #__PURE__ */new Map(); function useSubmissions(fn, filter) { const router = useRouter(); const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input)))); return new Proxy([], { get(_, property) { if (property === $TRACK) return subs(); if (property === "pending") return subs().some(sub => !sub.result); return subs()[property]; }, has(_, property) { return property in subs(); } }); } function useSubmission(fn, filter) { const submissions = useSubmissions(fn, filter); return new Proxy({}, { get(_, property) { if (submissions.length === 0 && property === "clear" || property === "retry") return () => {}; return submissions[submissions.length - 1]?.[property]; } }); } function useAction(action) { const r = useRouter(); return (...args) => action.apply({ r }, args); } function action(fn, options = {}) { function mutate(...variables) { const router = this.r; const form = this.f; const p = (router.singleFlight && fn.withOptions ? fn.withOptions({ headers: { "X-Single-Flight": "true" } }) : fn)(...variables); const [result, setResult] = createSignal(); let submission; function handler(error) { return async res => { const result = await handleResponse(res, error, router.navigatorFactory()); let retry = null; o.onComplete?.({ ...submission, result: result?.data, error: result?.error, pending: false, retry() { return retry = submission.retry(); } }); if (retry) return retry; if (!result) return submission.clear(); setResult(result); if (result.error && !form) throw result.error; return result.data; }; } router.submissions[1](s => [...s, submission = { input: variables, url, get result() { return result()?.data; }, get error() { return result()?.error; }, get pending() { return !result(); }, clear() { router.submissions[1](v => v.filter(i => i !== submission)); }, retry() { setResult(undefined); const p = fn(...variables); return p.then(handler(), handler(true)); } }]); return p.then(handler(), handler(true)); } const o = typeof options === "string" ? { name: options } : options; const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined); const url = fn.url || name && `https://action/${name}` || ""; mutate.base = url; if (name) setFunctionName(mutate, name); return toAction(mutate, url); } function toAction(fn, url) { fn.toString = () => { if (!url) throw new Error("Client Actions need explicit names if server rendered"); return url; }; fn.with = function (...args) { const newFn = function (...passedArgs) { return fn.call(this, ...args, ...passedArgs); }; newFn.base = fn.base; const uri = new URL(url, mockBase); uri.searchParams.set("args", hashKey(args)); return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search); }; fn.url = url; if (!isServer) { actions.set(url, fn); getOwner() && onCleanup(() => actions.delete(url)); } return fn; } const hashString = s => s.split("").reduce((a, b) => (a << 5) - a + b.charCodeAt(0) | 0, 0); async function handleResponse(response, error, navigate) { let data; let custom; let keys; let flightKeys; if (response instanceof Response) { if (response.headers.has("X-Revalidate")) keys = response.headers.get("X-Revalidate").split(","); if (response.customBody) { data = custom = await response.customBody(); if (response.headers.has("X-Single-Flight")) { data = data._$value; delete custom._$value; flightKeys = Object.keys(custom); } } if (response.headers.has("Location")) { const locationUrl = response.headers.get("Location") || "/"; if (locationUrl.startsWith("http")) { window.location.href = locationUrl; } else { navigate(locationUrl); } } } else if (error) return { error: response };else data = response; // invalidate cacheKeyOp(keys, entry => entry[0] = 0); // set cache flightKeys && flightKeys.forEach(k => query.set(k, custom[k])); // trigger revalidation await revalidate(keys, false); return data != null ? { data } : undefined; } function setupNativeEvents({ preload = true, explicitLinks = false, actionBase = "/_server", transformUrl } = {}) { return router => { const basePath = router.base.path(); const navigateFromRoute = router.navigatorFactory(router.base); let preloadTimeout; let lastElement; function isSvg(el) { return el.namespaceURI === "http://www.w3.org/2000/svg"; } function handleAnchor(evt) { if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return; const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A"); if (!a || explicitLinks && !a.hasAttribute("link")) return; const svg = isSvg(a); const href = svg ? a.href.baseVal : a.href; const target = svg ? a.target.baseVal : a.target; if (target || !href && !a.hasAttribute("state")) return; const rel = (a.getAttribute("rel") || "").split(/\s+/); if (a.hasAttribute("download") || rel && rel.includes("external")) return; const url = svg ? new URL(href, document.baseURI) : new URL(href); if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return; return [a, url]; } function handleAnchorClick(evt) { const res = handleAnchor(evt); if (!res) return; const [a, url] = res; const to = router.parsePath(url.pathname + url.search + url.hash); const state = a.getAttribute("state"); evt.preventDefault(); navigateFromRoute(to, { resolve: false, replace: a.hasAttribute("replace"), scroll: !a.hasAttribute("noscroll"), state: state ? JSON.parse(state) : undefined }); } function handleAnchorPreload(evt) { const res = handleAnchor(evt); if (!res) return; const [a, url] = res; transformUrl && (url.pathname = transformUrl(url.pathname)); router.preloadRoute(url, a.getAttribute("preload") !== "false"); } function handleAnchorMove(evt) { clearTimeout(preloadTimeout); const res = handleAnchor(evt); if (!res) return lastElement = null; const [a, url] = res; if (lastElement === a) return; transformUrl && (url.pathname = transformUrl(url.pathname)); preloadTimeout = setTimeout(() => { router.preloadRoute(url, a.getAttribute("preload") !== "false"); lastElement = a; }, 20); } function handleFormSubmit(evt) { if (evt.defaultPrevented) return; let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : evt.target.getAttribute("action"); if (!actionRef) return; if (!actionRef.startsWith("https://action/")) { // normalize server actions const url = new URL(actionRef, mockBase); actionRef = router.parsePath(url.pathname + url.search); if (!actionRef.startsWith(actionBase)) return; } if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions"); const handler = actions.get(actionRef); if (handler) { evt.preventDefault(); const data = new FormData(evt.target, evt.submitter); handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data" ? data : new URLSearchParams(data)); } } // ensure delegated event run first delegateEvents(["click", "submit"]); document.addEventListener("click", handleAnchorClick); if (preload) { document.addEventListener("mousemove", handleAnchorMove, { passive: true }); document.addEventListener("focusin", handleAnchorPreload, { passive: true }); document.addEventListener("touchstart", handleAnchorPreload, { passive: true }); } document.addEventListener("submit", handleFormSubmit); onCleanup(() => { document.removeEventListener("click", handleAnchorClick); if (preload) { document.removeEventListener("mousemove", handleAnchorMove); document.removeEventListener("focusin", handleAnchorPreload); document.removeEventListener("touchstart", handleAnchorPreload); } document.removeEventListener("submit", handleFormSubmit); }); }; } function Router(props) { if (isServer) return StaticRouter(props); const getSource = () => { const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search; const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state; return { value: url + window.location.hash, state }; }; const beforeLeave = createBeforeLeave(); return createRouter({ get: getSource, set({ value, replace, scroll, state }) { if (replace) { window.history.replaceState(keepDepth(state), "", value); } else { window.history.pushState(state, "", value); } scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll); saveCurrentDepth(); }, init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => { if (delta) { return !beforeLeave.confirm(delta); } else { const s = getSource(); return !beforeLeave.confirm(s.value, { state: s.state }); } })), create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase, transformUrl: props.transformUrl }), utils: { go: delta => window.history.go(delta), beforeLeave } })(props); } function hashParser(str) { const to = str.replace(/^.*?#/, ""); // Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to // `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash // to the current path so we can handle these in-page anchors correctly. if (!to.startsWith("/")) { const [, path = "/"] = window.location.hash.split("#", 2); return `${path}#${to}`; } return to; } function HashRouter(props) { const getSource = () => window.location.hash.slice(1); const beforeLeave = createBeforeLeave(); return createRouter({ get: getSource, set({ value, replace, scroll, state }) { if (replace) { window.history.replaceState(keepDepth(state), "", "#" + value); } else { window.history.pushState(state, "", "#" + value); } const hashIndex = value.indexOf("#"); const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : ""; scrollToHash(hash, scroll); saveCurrentDepth(); }, init: notify => bindEvent(window, "hashchange", notifyIfNotBlocked(notify, delta => !beforeLeave.confirm(delta && delta < 0 ? delta : getSource()))), create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase }), utils: { go: delta => window.history.go(delta), renderPath: path => `#${path}`, parsePath: hashParser, beforeLeave } })(props); } function createMemoryHistory() { const entries = ["/"]; let index = 0; const listeners = []; const go = n => { // https://github.com/remix-run/react-router/blob/682810ca929d0e3c64a76f8d6e465196b7a2ac58/packages/router/history.ts#L245 index = Math.max(0, Math.min(index + n, entries.length - 1)); const value = entries[index]; listeners.forEach(listener => listener(value)); }; return { get: () => entries[index], set: ({ value, scroll, replace }) => { if (replace) { entries[index] = value; } else { entries.splice(index + 1, entries.length - index, value); index++; } listeners.forEach(listener => listener(value)); setTimeout(() => { if (scroll) { scrollToHash(value.split("#")[1] || "", true); } }, 0); }, back: () => { go(-1); }, forward: () => { go(1); }, go, listen: listener => { listeners.push(listener); return () => { const index = listeners.indexOf(listener); listeners.splice(index, 1); }; } }; } function MemoryRouter(props) { const memoryHistory = props.history || createMemoryHistory(); return createRouter({ get: memoryHistory.get, set: memoryHistory.set, init: memoryHistory.listen, create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase }), utils: { go: memoryHistory.go } })(props); } var _tmpl$ = /*#__PURE__*/template(`
`); function A(props) { props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props); const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]); const to = useResolvedPath(() => props.href); const href = useHref(to); const location = useLocation(); const isActive = createMemo(() => { const to_ = to(); if (to_ === undefined) return [false, false]; const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase(); const loc = decodeURI(normalizePath(location.pathname).toLowerCase()); return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc]; }); return (() => { var _el$ = _tmpl$(); spread(_el$, mergeProps$1(rest, { get href() { return href() || props.href; }, get state() { return JSON.stringify(props.state); }, get classList() { return { ...(props.class && { [props.class]: true }), [props.inactiveClass]: !isActive()[0], [props.activeClass]: isActive()[0], ...rest.classList }; }, "link": "", get ["aria-current"]() { return isActive()[1] ? "page" : undefined; } }), false, false); return _el$; })(); } function Navigate(props) { const navigate = useNavigate(); const location = useLocation(); const { href, state } = props; const path = typeof href === "function" ? href({ navigate, location }) : href; navigate(path, { replace: true, state }); return null; } /** * This is mock of the eventual Solid 2.0 primitive. It is not fully featured. */ /** * As `createAsync` and `createAsyncStore` are wrappers for `createResource`, * this type allows to support `latest` field for these primitives. * It will be removed in the future. */ function createAsync(fn, options) { let resource; let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest; [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options); const resultAccessor = () => resource(); if (options?.name) setFunctionName(resultAccessor, options.name); Object.defineProperty(resultAccessor, "latest", { get() { return resource.latest; } }); return resultAccessor; } function createAsyncStore(fn, options = {}) { let resource; let prev = () => !resource || resource.state === "unresolved" ? undefined : unwrap(resource.latest); [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, { ...options, storage: init => createDeepSignal(init, options.reconcile) }); const resultAccessor = () => resource(); Object.defineProperty(resultAccessor, "latest", { get() { return resource.latest; } }); return resultAccessor; } function createDeepSignal(value, options) { const [store, setStore] = createStore({ value: structuredClone(value) }); return [() => store.value, v => { typeof v === "function" && (v = v()); setStore("value", reconcile(structuredClone(v), options)); return store.value; }]; } // mock promise while hydrating to prevent fetching class MockPromise { static all() { return new MockPromise(); } static allSettled() { return new MockPromise(); } static any() { return new MockPromise(); } static race() { return new MockPromise(); } static reject() { return new MockPromise(); } static resolve() { return new MockPromise(); } catch() { return new MockPromise(); } then() { return new MockPromise(); } finally() { return new MockPromise(); } } function subFetch(fn, prev) { if (isServer || !sharedConfig.context) return fn(prev); const ogFetch = fetch; const ogPromise = Promise; try { window.fetch = () => new MockPromise(); Promise = MockPromise; return fn(prev); } finally { window.fetch = ogFetch; Promise = ogPromise; } } function redirect(url, init = 302) { let responseInit; let revalidate; if (typeof init === "number") { responseInit = { status: init }; } else { ({ revalidate, ...responseInit } = init); if (typeof responseInit.status === "undefined") { responseInit.status = 302; } } const headers = new Headers(responseInit.headers); headers.set("Location", url); revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString()); const response = new Response(null, { ...responseInit, headers: headers }); return response; } function reload(init = {}) { const { revalidate, ...responseInit } = init; const headers = new Headers(responseInit.headers); revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString()); return new Response(null, { ...responseInit, headers }); } function json(data, init = {}) { const { revalidate, ...responseInit } = init; const headers = new Headers(responseInit.headers); revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString()); headers.set("Content-Type", "application/json"); const response = new Response(JSON.stringify(data), { ...responseInit, headers }); response.customBody = () => data; return response; } export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };