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
243 lines (221 loc) • 6.17 kB
text/typescript
import {_parseRoute} from './_parseRoute'
import {_resolvePathFromState} from './_resolvePathFromState'
import {_resolveStateFromPath} from './_resolveStateFromPath'
import {type RouteChildren, type Router, type RouteTransform} from './types'
import {decodeJsonParams, encodeJsonParams} from './utils/jsonParamsEncoding'
import {decodeParams, encodeParams} from './utils/paramsEncoding'
/**
* @public
*/
export interface RouteNodeOptions {
/**
* The path of the route node.
*/
path?: string
/**
* The children of the route node. See {@link RouteChildren}
*/
children?: RouteChildren
/**
* The transforms to apply to the route node. See {@link RouteTransform}
*/
transform?: {
[key: string]: RouteTransform<any>
}
/**
* The scope of the route node.
*/
scope?: string
/**
* Optionally disable scoping of search params
* Scoped search params will be represented as scope[param]=value in the url
* Disabling this will still scope search params based on any parent scope unless the parent scope also has disabled search params scoping
* Caution: enabling this can cause conflicts with multiple plugins defining search params with the same name
*/
__unsafe_disableScopedSearchParams?: boolean
}
/**
* Interface for the {@link route} object.
*
* @public
*/
export interface RouteObject {
/**
* Creates a new router.
* Returns {@link Router}
* See {@link RouteNodeOptions} and {@link RouteChildren}
*/
create: (
routeOrOpts: RouteNodeOptions | string,
childrenOrOpts?: RouteNodeOptions | RouteChildren | null,
children?: Router | RouteChildren,
) => Router
/**
* Creates a new router for handling intents.
* Returns {@link Router}
*/
intents: (base: string) => Router
/**
* Creates a new router scope.
* Returns {@link Router}
*/
scope(
scopeName: string,
routeOrOpts: RouteNodeOptions | string,
childrenOrOpts?: RouteNodeOptions | RouteChildren | null,
children?: Router | RouteChildren,
): Router
}
/**
* An object containing functions for creating routers and router scopes.
* See {@link RouteObject}
*
* @public
*
* @example
* ```ts
* const router = route.create({
* path: "/foo",
* children: [
* route.create({
* path: "/bar",
* children: [
* route.create({
* path: "/:baz",
* transform: {
* baz: {
* toState: (id) => ({ id }),
* toPath: (state) => state.id,
* },
* },
* }),
* ],
* }),
* ],
* });
* ```
*/
export const route: RouteObject = {
create: (routeOrOpts, childrenOrOpts, children) =>
_createNode(normalizeArgs(routeOrOpts, childrenOrOpts, children)),
intents: (base: string) => {
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: string,
routeOrOpts: RouteNodeOptions | string,
childrenOrOpts?: RouteNodeOptions | RouteChildren | null,
children?: Router | RouteChildren,
) {
const options = normalizeArgs(routeOrOpts, childrenOrOpts, children)
return _createNode({
...options,
scope: scopeName,
})
},
}
function normalizeChildren(children: any): RouteChildren {
if (Array.isArray(children) || typeof children === 'function') {
return children
}
return children ? [children] : []
}
function isRoute(val?: RouteNodeOptions | Router | RouteChildren) {
return val && '_isRoute' in val
}
function normalizeArgs(...args: any[]): RouteNodeOptions
function normalizeArgs(
path: string | RouteNodeOptions,
childrenOrOpts?: RouteNodeOptions | Router | RouteChildren,
children?: Router | RouteChildren,
): RouteNodeOptions {
if (typeof path === 'object') {
return path
}
if (
Array.isArray(childrenOrOpts) ||
typeof childrenOrOpts === 'function' ||
isRoute(childrenOrOpts)
) {
return {path, children: normalizeChildren(childrenOrOpts)}
}
if (children) {
return {path, ...childrenOrOpts, children: normalizeChildren(children)}
}
return {path, ...childrenOrOpts}
}
function normalize(...paths: string[]) {
return paths.reduce<string[]>((acc, path) => acc.concat(path.split('/')), []).filter(Boolean)
}
const EMPTY_STATE = {}
function isRoot(pathname: string): boolean {
// it is the root if every segment is an empty string
return pathname.split('/').every((segment) => !segment)
}
/**
* @internal
* @param options - Route node options
*/
export function _createNode(options: RouteNodeOptions): Router {
// eslint-disable-next-line camelcase
const {path, scope, transform, children, __unsafe_disableScopedSearchParams} = options
if (!path) {
throw new TypeError('Missing path')
}
const parsedRoute = _parseRoute(path)
return {
_isRoute: true, // 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: isRoot,
isNotFound(pathname: string): boolean {
return this.decode(pathname) === null
},
getBasePath(): string {
return this.encode(EMPTY_STATE)
},
getRedirectBase(pathname: string): string | null {
if (isRoot(pathname)) {
const basePath = this.getBasePath()
// Check if basepath is something different than given
if (pathname !== basePath) {
return basePath
}
}
return null
},
}
}