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(`