UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

486 lines (485 loc) • 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: !0 }); var jsxRuntime = require("react/jsx-runtime"), React = require("react"), difference = require("lodash/difference.js"), intersection = require("lodash/intersection.js"), isPlainObject = require("lodash/isPlainObject.js"), pick = require("lodash/pick.js"), debug$1 = require("debug"), identity = require("lodash/identity.js"); function _interopDefaultCompat(e) { return e && typeof e == "object" && "default" in e ? e : { default: e }; } var difference__default = /* @__PURE__ */ _interopDefaultCompat(difference), intersection__default = /* @__PURE__ */ _interopDefaultCompat(intersection), isPlainObject__default = /* @__PURE__ */ _interopDefaultCompat(isPlainObject), pick__default = /* @__PURE__ */ _interopDefaultCompat(pick), debug__default = /* @__PURE__ */ _interopDefaultCompat(debug$1), identity__default = /* @__PURE__ */ _interopDefaultCompat(identity); const RouterContext = React.createContext(null); function useRouter() { const router = React.useContext(RouterContext); if (!router) throw new Error("Router: missing context value"); return router; } function isLeftClickEvent(event) { return event.button === 0; } function isModifiedEvent(event) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); } function useLink(options) { const { onClick: onClickProp, href, target, replace = !1 } = options, { navigateUrl } = useRouter(); return { onClick: React.useCallback( (event) => { event.isDefaultPrevented() || href && (onClickProp && onClickProp(event), !(isModifiedEvent(event) || !isLeftClickEvent(event)) && (target || (event.preventDefault(), navigateUrl({ path: href, replace })))); }, [href, navigateUrl, onClickProp, replace, target] ) }; } function useIntentLink(options) { const { intent, onClick: onClickProp, params, replace, target } = options, { resolveIntentLink } = useRouter(), href = React.useMemo(() => resolveIntentLink(intent, params), [intent, params, resolveIntentLink]), { onClick } = useLink({ href, onClick: onClickProp, replace, target }); return { onClick, href }; } const IntentLink = React.forwardRef(function(props, ref) { const { intent, params, target, ...restProps } = props, { onClick, href } = useIntentLink({ intent, params, target, onClick: props.onClick }); return /* @__PURE__ */ jsxRuntime.jsx("a", { ...restProps, href, onClick, ref, target }); }), Link = React.forwardRef(function(props, ref) { const { onClick: onClickProp, href, target, replace, ...restProps } = props, { onClick } = useLink({ onClick: onClickProp, href, target, replace }); return /* @__PURE__ */ jsxRuntime.jsx("a", { ...restProps, onClick, href, target, ref }); }), VALID_PARAM_SEGMENT = /^[a-zA-Z0-9_-]+$/; function createSegment(segment) { if (!segment) return null; if (segment.startsWith(":")) { const paramName = segment.substring(1); if (!VALID_PARAM_SEGMENT.test(paramName)) { const addendum = segment.includes("*") ? " Splats are not supported. Consider using child routes instead" : ""; console.error( new Error(`Warning: Param segments "${segment}" includes invalid characters.${addendum}`) ); } return { type: "param", name: paramName }; } return { type: "dir", name: segment }; } function _parseRoute(route2) { const [pathname] = route2.split("?"), segments = pathname.split("/").map(createSegment).filter(Boolean); return { raw: route2, segments }; } function arrayify(val) { return Array.isArray(val) ? val : val ? [val] : []; } function isRecord(value) { return isPlainObject__default.default(value); } function createMatchError(node, missingKeys, unmappableStateKeys) { return { type: "error", node, missingKeys, unmappableStateKeys }; } function createMatchOk(node, matchedState, searchParams, child) { return { type: "ok", node, matchedState, searchParams, child }; } function _findMatchingRoutes(node, _state) { if (!_state) return createMatchOk(node, {}, []); const scopedState = node.scope ? _state[node.scope] : _state, { _searchParams: searchParams = [], ...state } = scopedState || {}, requiredParams = node.route.segments.filter((seg) => seg.type === "param").map((seg) => seg.name), stateKeys = isRecord(state) ? Object.keys(state) : [], consumedParams = intersection__default.default(stateKeys, requiredParams), missingParams = difference__default.default(requiredParams, consumedParams), remainingParams = difference__default.default(stateKeys, consumedParams); if (missingParams.length > 0) return createMatchError(node, missingParams, []); const scopedParams = searchParams.map(([key, value]) => [[key], value]), consumedState = pick__default.default(state, consumedParams); if (remainingParams.length === 0) return createMatchOk(node, consumedState, scopedParams); const children = arrayify( (typeof node.children == "function" ? node.children(isRecord(state) ? state : {}) : node.children) || [] ); if (remainingParams.length > 0 && children.length === 0) return createMatchError(node, [], remainingParams); const remainingState = pick__default.default(state, remainingParams), found = children.map((childNode) => _findMatchingRoutes(childNode, remainingState)).find((res) => res.type === "ok"); return found ? createMatchOk(node, consumedState, scopedParams, found) : createMatchError(node, [], remainingParams); } function encodeURIComponentExcept(uriComponent, unescaped) { const chars = [...String(uriComponent)]; let res = ""; for (let i = 0; i < chars.length; i++) { const char = chars[i]; unescaped.includes(char) ? res += char : res += encodeURIComponent(char); } return res; } const debug = debug__default.default("state-router"); function _resolvePathFromState(node, _state) { debug("Resolving path from state %o", _state); const match = _findMatchingRoutes(node, _state); if (match.type === "error") { const unmappable = match.unmappableStateKeys; if (unmappable.length > 0) throw new Error( `Unable to find matching route for state. Could not map the following state key${unmappable.length == 1 ? "" : "s"} to a valid url: ${unmappable.map(quote).join(", ")}` ); const missingKeys = match.missingKeys; throw new Error( `Unable to find matching route for state. State object is missing the following key${missingKeys.length == 1 ? "" : "s"} defined in route: ${missingKeys.map(quote).join(", ")}` ); } const { path, searchParams } = pathFromMatchResult(match), search = searchParams.length > 0 ? encodeParams$1(searchParams) : ""; return `/${path.join("/")}${search ? `?${search}` : ""}`; } function bracketify(value) { return `[${value}]`; } function encodeParams$1(params) { return params.flatMap(([key, value]) => value === void 0 ? [] : [encodeSearchParamKey(serializeScopedPath(key)), encodeSearchParamValue(value)].join( "=" )).join("&"); } function serializeScopedPath(scopedPath) { const [head, ...tail] = scopedPath; return tail.length > 0 ? [head, ...tail.map(bracketify)].join("") : head; } function encodeSearchParamValue(value) { return encodeURIComponentExcept(value, "/"); } function encodeSearchParamKey(value) { return encodeURIComponentExcept(value, "[]"); } function pathFromMatchResult(match) { const matchedState = match.matchedState, base = match.node.route.segments.map((segment) => { if (segment.type === "dir") return segment.name; const transform = match.node.transform && match.node.transform[segment.name]; return transform ? transform.toPath(matchedState[segment.name]) : matchedState[segment.name]; }), childMatch = match.child ? pathFromMatchResult(match.child) : void 0, searchParams = childMatch != null && childMatch.searchParams ? [...match.searchParams, ...childMatch.searchParams] : match.searchParams; return { searchParams: addNodeScope(match.node, searchParams), path: [...base || [], ...(childMatch == null ? void 0 : childMatch.path) || []] }; } function addNodeScope(node, searchParams) { const scope = node.scope; return scope && !node.__unsafe_disableScopedSearchParams ? searchParams.map(([namespaces, value]) => [[scope, ...namespaces], value]) : searchParams; } function quote(value) { return `"${value}"`; } function parseScopedParams(params) { return params.map(([key, value]) => [parse(key), value]); } const OPEN = 1, CLOSED = 0; function parse(str) { const result = []; let i = 0, state = CLOSED; for (; i < str.length; ) { const nextBracketIdx = str.indexOf("[", i); if (nextBracketIdx === -1) { result.push(str.slice(i, str.length)); break; } if (state === OPEN) throw new Error("Nested brackets not supported"); state = OPEN, nextBracketIdx > i && (result.push(str.slice(i, nextBracketIdx)), i = nextBracketIdx); const nextClosing = str.indexOf("]", nextBracketIdx); if (nextClosing === -1) { if (state === OPEN) throw new Error("Unclosed bracket"); break; } state = CLOSED, result.push(str.slice(i + 1, nextClosing)), i = nextClosing + 1; } return result; } function matchPath(node, path, searchParams) { const parts = path.split("/").filter(Boolean), segmentsLength = node.route.segments.length; if (parts.length < segmentsLength) return null; const state = {}; if (!node.route.segments.every((segment, i) => { if (segment.type === "dir") return segment.name === parts[i]; const transform = node.transform && node.transform[segment.name]; return state[segment.name] = transform ? transform.toState(parts[i]) : parts[i], !0; })) return null; const rest = parts.slice(segmentsLength); let childState = null; const children = typeof node.children == "function" ? arrayify(node.children(state)) : node.children, unscopedParams = removeScope(node.scope, searchParams); if (children.some((childNode) => { if (childNode) { const childParams = childNode.scope ? unscopedParams.filter(([namespaces]) => childNode.scope === namespaces[0]) : unscopedParams; return childState = matchPath(childNode, rest.join("/"), childParams), childState; } }), rest.length > 0 && !childState) return null; const selfParams = unscopedParams.flatMap(([namespace, value]) => namespace.length === 1 ? [[namespace[0], value]] : []), mergedState = { ...state, ...childState || {}, ...selfParams.length > 0 ? { _searchParams: selfParams } : {} }; return node.scope ? { [node.scope]: mergedState } : mergedState; } function _resolveStateFromPath(node, path) { debug("resolving state from path %s", path); const [pathname, search] = path.split("?"), urlSearchParams = Array.from(new URLSearchParams(search).entries()), pathMatch = matchPath(node, pathname, parseScopedParams(urlSearchParams)); return debug("resolved: %o", pathMatch || null), pathMatch || null; } function removeScope(scope, searchParams) { return scope ? searchParams.map(([namespaces, value]) => [ namespaces[0] === scope ? namespaces.slice(1) : namespaces, value ]) : searchParams; } function encodeBase64Url(str) { return encodeBase64(str).replace(/\//g, "_").replace(/\+/g, "-").replace(/[=]+$/, ""); } function decodeBase64Url(str) { return decodeBase64(str.replace(/-/g, "+").replace(/_/g, "/")); } function percentToByte(p) { return String.fromCharCode(parseInt(p.slice(1), 16)); } function encodeBase64(str) { return btoa(encodeURIComponent(str).replace(/%[0-9A-F]{2}/g, percentToByte)); } function byteToPercent(b) { return `%${`00${b.charCodeAt(0).toString(16)}`.slice(-2)}`; } function decodeBase64(str) { return decodeURIComponent(Array.from(atob(str), byteToPercent).join("")); } function decodeJsonParams(pathSegment = "") { const segment = decodeURIComponent(pathSegment); if (!segment) return {}; try { return JSON.parse(decodeBase64Url(segment)); } catch { } try { return JSON.parse(atob(segment)); } catch { } try { return JSON.parse(segment); } catch { console.warn("Failed to parse JSON parameters"); } return {}; } function encodeJsonParams(params) { return params ? encodeBase64Url(JSON.stringify(params)) : ""; } function decodeParams(pathSegment) { return pathSegment.split(";").reduce((params, pair) => { const [key, value] = pair.split("="); return params[decodeURIComponent(key)] = decodeURIComponent(value), params; }, {}); } function encodeParams(params) { return Object.entries(params).filter(([, value]) => value != null).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join(";"); } const route = { create: (routeOrOpts, childrenOrOpts, children) => _createNode(normalizeArgs(routeOrOpts, childrenOrOpts, children)), intents: (base) => { const basePath = normalize(base).join("/"); return route.create(`${basePath}/:intent`, [ route.create( ":params", { transform: { params: { toState: decodeParams, toPath: encodeParams } } }, [ route.create(":payload", { transform: { payload: { toState: decodeJsonParams, toPath: encodeJsonParams } } }) ] ) ]); }, scope(scopeName, routeOrOpts, childrenOrOpts, children) { const options = normalizeArgs(routeOrOpts, childrenOrOpts, children); return _createNode({ ...options, scope: scopeName }); } }; function normalizeChildren(children) { return Array.isArray(children) || typeof children == "function" ? children : children ? [children] : []; } function isRoute(val) { return val && "_isRoute" in val; } function normalizeArgs(path, childrenOrOpts, children) { return typeof path == "object" ? path : Array.isArray(childrenOrOpts) || typeof childrenOrOpts == "function" || isRoute(childrenOrOpts) ? { path, children: normalizeChildren(childrenOrOpts) } : children ? { path, ...childrenOrOpts, children: normalizeChildren(children) } : { path, ...childrenOrOpts }; } function normalize(...paths) { return paths.reduce((acc, path) => acc.concat(path.split("/")), []).filter(Boolean); } const EMPTY_STATE$1 = {}; function isRoot(pathname) { return pathname.split("/").every((segment) => !segment); } function _createNode(options) { const { path, scope, transform, children, __unsafe_disableScopedSearchParams } = options; if (!path) throw new TypeError("Missing path"); const parsedRoute = _parseRoute(path); return { _isRoute: !0, // todo: make a Router class instead scope, // eslint-disable-next-line camelcase __unsafe_disableScopedSearchParams, route: parsedRoute, children: children || [], transform, encode(state) { return _resolvePathFromState(this, state); }, decode(_path) { return _resolveStateFromPath(this, _path); }, isRoot, isNotFound(pathname) { return this.decode(pathname) === null; }, getBasePath() { return this.encode(EMPTY_STATE$1); }, getRedirectBase(pathname) { if (isRoot(pathname)) { const basePath = this.getBasePath(); if (pathname !== basePath) return basePath; } return null; } }; } function RouterProvider(props) { const { onNavigate, router: routerProp, state } = props, resolveIntentLink = React.useCallback( (intentName, parameters) => { const [params, payload] = Array.isArray(parameters) ? parameters : [parameters]; return routerProp.encode({ intent: intentName, params, payload }); }, [routerProp] ), resolvePathFromState = React.useCallback( (nextState) => routerProp.encode(nextState), [routerProp] ), navigate = React.useCallback( (nextState, options = {}) => { onNavigate({ path: resolvePathFromState(nextState), replace: options.replace }); }, [onNavigate, resolvePathFromState] ), navigateIntent = React.useCallback( (intentName, params, options = {}) => { onNavigate({ path: resolveIntentLink(intentName, params), replace: options.replace }); }, [onNavigate, resolveIntentLink] ), router = React.useMemo( () => ({ navigate, navigateIntent, navigateUrl: onNavigate, resolveIntentLink, resolvePathFromState, state }), [navigate, navigateIntent, onNavigate, resolveIntentLink, resolvePathFromState, state] ); return /* @__PURE__ */ jsxRuntime.jsx(RouterContext.Provider, { value: router, children: props.children }); } function addScope(routerState, scope, scopedState) { return scopedState && { ...routerState, [scope]: scopedState }; } function RouteScope(props) { const { children, scope, __unsafe_disableScopedSearchParams } = props, parentRouter = useRouter(), { resolvePathFromState: parent_resolvePathFromState, navigate: parent_navigate } = parentRouter, parentStateRef = React.useRef(parentRouter.state); parentStateRef.current = parentRouter.state; const resolveNextParentState = React.useCallback( (_nextState) => { const { _searchParams, ...nextState } = _nextState, nextParentState = addScope(parentStateRef.current, scope, nextState); return __unsafe_disableScopedSearchParams ? nextParentState._searchParams = _searchParams : nextParentState[scope]._searchParams = _searchParams, nextParentState; }, [scope, __unsafe_disableScopedSearchParams] ), resolvePathFromState = React.useCallback( (nextState) => parent_resolvePathFromState(resolveNextParentState(nextState)), [parent_resolvePathFromState, resolveNextParentState] ), navigate = React.useCallback( (nextState) => parent_navigate(resolveNextParentState(nextState)), [parent_navigate, resolveNextParentState] ), childRouter = React.useMemo(() => { const parentState = parentRouter.state, childState = { ...parentState[scope] || {} }; return __unsafe_disableScopedSearchParams && (childState._searchParams = parentState._searchParams), { ...parentRouter, navigate, resolvePathFromState, state: childState }; }, [scope, parentRouter, navigate, resolvePathFromState, __unsafe_disableScopedSearchParams]); return /* @__PURE__ */ jsxRuntime.jsx(RouterContext.Provider, { value: childRouter, children }); } const EMPTY_STATE = {}; function useStateLink(options) { const { onClick: onClickProp, replace, state, target, toIndex = !1 } = options; if (state && toIndex) throw new Error("Passing both `state` and `toIndex={true}` as props to StateLink is invalid"); !state && !toIndex && console.error( new Error( "No state passed to StateLink. If you want to link to an empty state, its better to use the the `toIndex` property" ) ); const { resolvePathFromState } = useRouter(), href = React.useMemo( () => resolvePathFromState(toIndex ? EMPTY_STATE : state || EMPTY_STATE), [resolvePathFromState, state, toIndex] ), { onClick } = useLink({ href, onClick: onClickProp, replace, target }); return { onClick, href }; } const StateLink = React.forwardRef(function(props, ref) { const { onClick: onClickProp, replace, state, target, toIndex = !1, ...restProps } = props, { onClick, href } = useStateLink({ onClick: onClickProp, replace, state, target, toIndex }); return /* @__PURE__ */ jsxRuntime.jsx("a", { ...restProps, href, onClick, ref }); }); function useRouterState(selector = identity__default.default) { const { state } = useRouter(); return React.useMemo(() => selector(state), [selector, state]); } function withRouter(Component) { function WithRouter2(props) { const router = useRouter(); return /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props, router }); } return WithRouter2.displayName = `withRouter(${Component.displayName || Component.name})`, WithRouter2; } const WithRouter = withRouter((props) => props.children(props.router)); exports.IntentLink = IntentLink; exports.Link = Link; exports.RouteScope = RouteScope; exports.RouterContext = RouterContext; exports.RouterProvider = RouterProvider; exports.StateLink = StateLink; exports.WithRouter = WithRouter; exports._createNode = _createNode; exports.decodeJsonParams = decodeJsonParams; exports.encodeJsonParams = encodeJsonParams; exports.route = route; exports.useIntentLink = useIntentLink; exports.useLink = useLink; exports.useRouter = useRouter; exports.useRouterState = useRouterState; exports.useStateLink = useStateLink; exports.withRouter = withRouter; //# sourceMappingURL=router.js.map