- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
1880 lines
56 KiB
JavaScript
1880 lines
56 KiB
JavaScript
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), "<A> 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 (
|
|
* <div classList={{ "grey-out": isRouting() }}>
|
|
* <MyAwesomeContent />
|
|
* </div>
|
|
* );
|
|
* ```
|
|
*/
|
|
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 <div classList={{ active: Boolean(match()) }} />;
|
|
* ```
|
|
*/
|
|
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 (
|
|
* <div>
|
|
* <span>Page: {searchParams.page}</span>
|
|
* <button
|
|
* onClick={() =>
|
|
* setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
|
|
* }
|
|
* >
|
|
* Next Page
|
|
* </button>
|
|
* </div>
|
|
* );
|
|
* ```
|
|
*/
|
|
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(`<a>`);
|
|
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 };
|