@v4fire/client
Version:
V4Fire client core library
491 lines (397 loc) • 10.7 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
import parsePattern, { parse, compile } from 'path-to-regexp';
import type { Key, RegExpOptions } from 'path-to-regexp';
import { concatURLs, toQueryString, fromQueryString } from 'core/url';
import { deprecate } from 'core/functools/deprecation';
import { qsClearFixRgxp, routeNames, defaultRouteNames, isExternal } from 'core/router/const';
import type {
Route,
AppliedRoute,
RouteAPI,
InitialRoute,
StaticRoutes,
RouteBlueprint,
RouteBlueprints,
TransitionOptions,
AdditionalGetRouteOpts,
CompileRoutesOpts,
PathParam
} from 'core/router/interface';
/**
* Returns a name of the specified route
* @param [route]
*/
export function getRouteName(route?: AppliedRoute | Route | RouteBlueprint | InitialRoute): CanUndef<string> {
if (Object.isPlainObject(route)) {
for (let i = 0; i < routeNames.length; i++) {
const
val = route[routeNames[i]];
if (val != null) {
return val;
}
}
return undefined;
}
return Object.isString(route) ? route : undefined;
}
/**
* Returns a route object by the specified name or path
*
* @param ref - route name or path
* @param routes - available routes to get the route object by a name or path
* @param [opts] - additional options
*
* @example
* ```js
* routes = {
* demo: {
* route: '/demo'
* }
* };
*
*
* getRoute('/demo', routes).name === 'demo';
* ```
*/
export function getRoute(ref: string, routes: RouteBlueprints, opts: AdditionalGetRouteOpts = {}): CanUndef<RouteAPI> {
const
{basePath, defaultRoute} = opts;
const
routeKeys = Object.keys(routes);
const
initialRef = ref,
initialRefQuery = ref.includes('?') ? fromQueryString(ref) : {};
let
resolvedRoute: Nullable<RouteBlueprint> = null,
initialRoute: Nullable<RouteBlueprint> = null,
alias: Nullable<RouteBlueprint> = null;
let
resolvedRef = ref,
refIsNormalized = true,
externalRedirect = false;
// eslint-disable-next-line no-constant-condition
while (true) {
// Reference to a route that passed as ID
if (resolvedRef in routes) {
resolvedRoute = routes[resolvedRef];
if (resolvedRoute == null) {
break;
}
const
{meta} = resolvedRoute;
if (meta.redirect == null && meta.alias == null) {
break;
}
if (meta.external) {
externalRedirect = true;
break;
}
// Reference to a route that passed as a path
} else {
if (Object.isString(basePath) && basePath !== '') {
// Resolve the situation when the passed path already has basePath
const v = basePath.replace(/(.*)?[\\/]+$/, (str, base) => `${RegExp.escape(base)}/*`);
resolvedRef = concatURLs(basePath, resolvedRef.replace(new RegExp(`^${v}`), ''));
// We need to normalize only a user "raw" ref
if (refIsNormalized) {
ref = resolvedRef;
refIsNormalized = false;
}
}
for (let i = 0; i < routeKeys.length; i++) {
const
route = routes[routeKeys[i]];
if (!route) {
continue;
}
// In this case, we have the full matching of a route ref by a name or pattern
if (getRouteName(route) === resolvedRef || route.pattern === resolvedRef) {
resolvedRoute = route;
break;
}
// Try to test the passed ref with a route pattern
if (route.rgxp?.test(resolvedRef)) {
if (resolvedRoute == null) {
resolvedRoute = route;
continue;
}
// If we have several matches with the provided ref,
// like routes '/foo" and "/foo/:id" are matched with "/foo/bar",
// we should prefer that pattern that has more length
if (route.pattern!.length > (resolvedRoute.pattern?.length ?? 0)) {
resolvedRoute = route;
}
}
}
}
if (resolvedRoute == null) {
break;
}
const
{meta} = resolvedRoute;
// If we haven't found a route that matches the provided ref or the founded route does not redirect or refer
// to another route, we can exit from the search loop. Otherwise, we need to resolve the redirect/alias.
if (meta.redirect == null && meta.alias == null) {
break;
}
if (meta.external) {
externalRedirect = true;
break;
}
// The alias should preserve the original route name and path
if (meta.alias != null) {
if (alias == null) {
alias = resolvedRoute;
}
resolvedRef = meta.alias;
} else {
resolvedRef = meta.redirect!;
ref = resolvedRef;
}
initialRoute = resolvedRoute;
// Continue of resolving
resolvedRoute = undefined;
}
// We haven't found a route by the provided ref,
// that why we need to find a "default" route as loopback
if (!resolvedRoute) {
resolvedRoute = defaultRoute;
// We have found a route by the provided ref, but it contains an alias
} else if (alias) {
resolvedRoute = {
...resolvedRoute,
...Object.select(alias, [
'name',
'pattern',
'rgxp',
'pathParams'
])
};
}
if (resolvedRoute == null) {
return;
}
const routeAPI: RouteAPI = Object.create({
...resolvedRoute,
meta: Object.mixin(true, {}, resolvedRoute.meta),
get page(): string {
return resolvedRoute!.name;
},
resolvePath(params: Dictionary = {}): string {
const
parameters = resolvePathParameters(resolvedRoute?.pathParams ?? [], params);
if (externalRedirect) {
return compile(resolvedRoute?.meta.redirect ?? ref)(parameters);
}
const routePattern = resolvedRoute?.pattern;
const pattern = Object.isFunction(routePattern) ?
routePattern(routeAPI) :
routePattern;
return compile(pattern ?? ref)(parameters);
},
toPath(params?: Dictionary): string {
deprecate({name: 'toPath', type: 'method', renamedTo: 'resolvePath'});
return this.resolvePath(params);
}
});
Object.assign(routeAPI, {
name: resolvedRoute.name,
params: {},
query: Object.isDictionary(initialRefQuery) ? initialRefQuery : {}
});
// Fill route parameters from URL
const tryFillParams = (route: Nullable<RouteBlueprint<Dictionary>>): void => {
if (route == null) {
return;
}
const
params = route.rgxp?.exec(initialRef);
if (params == null) {
return;
}
const
pattern = Object.isFunction(route.pattern) ? route.pattern(routeAPI) : route.pattern;
for (let o = parse(pattern ?? ''), i = 0, j = 0; i < o.length; i++) {
const
el = o[i];
if (Object.isSimpleObject(el)) {
routeAPI.params[el.name] = params[++j];
}
}
};
tryFillParams(initialRoute);
tryFillParams(resolvedRoute);
return routeAPI;
}
/**
* Returns a path of the specified route with padding of additional parameters
*
* @param ref - route name or path
* @param routes - available routes to get the route object by name or path
* @param [opts] - additional options
*
* @example
* ```js
* routes = {
* demo: {
* route: '/demo'
* }
* };
*
* getRoutePath('demo', routes) === '/demo';
* getRoutePath('/demo', routes, {query: {foo: 'bar'}}) === '/demo?foo=bar';
* ```
*/
export function getRoutePath(ref: string, routes: RouteBlueprints, opts: TransitionOptions = {}): CanUndef<string> {
const
route = getRoute(ref, routes);
if (!route) {
return;
}
let
res = route.resolvePath(opts.params);
if (opts.query) {
const
q = toQueryString(opts.query, false);
if (q !== '') {
res += `?${q}`;
}
}
return res.replace(qsClearFixRgxp, '');
}
/**
* Compiles the specified static routes and returns a new object
*
* @param routes
* @param [opts]
*/
export function compileStaticRoutes(routes: StaticRoutes, opts: CompileRoutesOpts = {}): RouteBlueprints {
const
{basePath = ''} = opts,
compiledRoutes = {};
for (let keys = Object.keys(routes), i = 0; i < keys.length; i++) {
const
name = keys[i],
route = routes[name] ?? {},
originalPathParams: Key[] = [];
if (Object.isString(route)) {
const
pattern = concatURLs(basePath, route),
rgxp = parsePattern(pattern, originalPathParams);
const pathParams: PathParam[] = originalPathParams.map((param) => ({
...param,
aliases: []
}));
compiledRoutes[name] = {
name,
pattern,
rgxp,
pathParams,
/** @deprecated */
get page(): string {
return this.name;
},
/** @deprecated */
get index(): boolean {
return this.meta.default;
},
get default(): boolean {
return this.meta.default;
},
meta: {
name,
external: isExternal.test(pattern),
/** @deprecated */
page: name
}
};
} else {
let
pattern: CanUndef<string>,
rgxp: CanUndef<RegExp>;
if (Object.isString(route.path)) {
pattern = concatURLs(basePath, route.path);
rgxp = parsePattern(pattern, originalPathParams, <RegExpOptions>route.pathOpts);
}
const pathParams: PathParam[] = originalPathParams.map((param) => ({
...param,
aliases: route.pathOpts?.aliases?.[param.name] ?? []
}));
compiledRoutes[name] = {
name,
pattern,
rgxp,
pathParams,
/** @deprecated */
get page(): string {
return this.name;
},
/** @deprecated */
get index(): boolean {
return this.meta.default;
},
get default(): boolean {
return this.meta.default;
},
meta: {
...route,
name,
default: Boolean(route.default ?? route.index ?? defaultRouteNames[name]),
external: route.external ?? (
isExternal.test(pattern ?? '') ||
isExternal.test(route.redirect ?? '')
),
/** @deprecated */
page: name
}
};
}
}
return compiledRoutes;
}
/**
* Resolves dynamic parameters from the path based on the parsed from the pattern `pathParams`
* and the user-provided parameters
*
* @see [[RouteBlueprint.pathParams]]
*
* @param pathParams - parameters after parsing the path
* @param params - user-provided parameters with possible aliases
*
* @example
* ```typescript
* const route = {
* path: '/foo/:bar',
* pathOpts: {
* aliases: {bar: ['Bar']}
* }
* }
*
* const pathParams = [];
* parsePattern(route.path, pathParams, route.pathOpts);
*
* const parameters = {Bar: 21};
* resolvePathParameters(pathParams, parameters); // {Bar: 21, bar: 21}
* ```
*/
export function resolvePathParameters(pathParams: PathParam[], params: Dictionary): Dictionary {
const
parameters = {...params};
pathParams.forEach((param) => {
if (parameters.hasOwnProperty(param.name)) {
return;
}
const
alias = param.aliases.find((e) => parameters.hasOwnProperty(e));
if (alias != null) {
parameters[param.name] = parameters[alias];
}
});
return parameters;
}