Skip to content

Commit

Permalink
refactor: finalize Resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
Lodin committed Oct 1, 2024
1 parent e074f67 commit c2b6144
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 332 deletions.
40 changes: 20 additions & 20 deletions src/resolver/generateUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
import { parse, type ParseOptions, type Token, tokensToFunction, type TokensToFunctionOptions } from 'path-to-regexp';
import type { EmptyObject, Writable } from 'type-fest';
import Resolver from './resolver.js';
import type { AnyObject, ChildrenCallback, Route, Params } from './types.js';
import type { AnyObject, ChildrenCallback, IndexedParams, Params, Route } from './types.js';
import { getRoutePath, isString } from './utils.js';

export type UrlParams = Readonly<Record<string, ReadonlyArray<number | string> | number | string>>;

function cacheRoutes<T, R extends AnyObject = EmptyObject>(
routesByName: Map<string, Array<Route<T, R>>>,
route: Writable<Route<T, R>>,
routes?: ChildrenCallback<T, R> | ReadonlyArray<Route<T, R>>,
cacheKeyProvider?: (route: Route<T, R>) => string,
function cacheRoutes<T, R extends AnyObject, C extends AnyObject>(
routesByName: Map<string, Array<Route<T, R, C>>>,
route: Writable<Route<T, R, C>>,
routes?: ReadonlyArray<Route<T, R, C>> | ChildrenCallback<T, R, C>,
cacheKeyProvider?: (route: Route<T, R, C>) => string,
): void {
const name = route.name ?? cacheKeyProvider?.(route);
if (name) {
Expand All @@ -30,18 +30,18 @@ function cacheRoutes<T, R extends AnyObject = EmptyObject>(
}
}

if (Array.isArray<ReadonlyArray<Writable<Route<T, R>>>>(routes)) {
if (Array.isArray<ReadonlyArray<Writable<Route<T, R, C>>>>(routes)) {
for (const childRoute of routes) {
childRoute.parent = route;
cacheRoutes(routesByName, childRoute, childRoute.__children ?? childRoute.children, cacheKeyProvider);
}
}
}

function getRouteByName<R extends AnyObject = EmptyObject>(
routesByName: Map<string, Array<Route<T, R>>>,
function getRouteByName<T, R extends AnyObject, C extends AnyObject>(
routesByName: Map<string, Array<Route<T, R, C>>>,
routeName: string,
): Route<T, R> | undefined {
): Route<T, R, C> | undefined {
const routes = routesByName.get(routeName);

if (routes) {
Expand All @@ -57,7 +57,7 @@ function getRouteByName<R extends AnyObject = EmptyObject>(

export type StringifyQueryParams = (params: UrlParams) => string;

export type GenerateUrlOptions<T, R extends AnyObject> = ParseOptions &
export type GenerateUrlOptions<T, R extends AnyObject, C extends AnyObject> = ParseOptions &
Readonly<{
/**
* Add a query string to generated url based on unknown route params.
Expand All @@ -67,7 +67,7 @@ export type GenerateUrlOptions<T, R extends AnyObject> = ParseOptions &
* Generates a unique route name based on all parent routes with the specified separator.
*/
uniqueRouteNameSep?: string;
cacheKeyProvider?(route: Route<T, R>): string | undefined;
cacheKeyProvider?(route: Route<T, R, C>): string;
}> &
TokensToFunctionOptions;

Expand All @@ -78,24 +78,24 @@ type RouteCacheRecord = Readonly<{

export type UrlGenerator = (routeName: string, params?: Params) => string;

function generateUrls<T, R extends AnyObject = EmptyObject>(
resolver: Resolver<R>,
options: GenerateUrlOptions<T, R> = {},
function generateUrls<T = unknown, R extends AnyObject = EmptyObject, C extends AnyObject = EmptyObject>(
resolver: Resolver<T, R, C>,
options: GenerateUrlOptions<T, R, C> = {},
): UrlGenerator {
if (!(resolver instanceof Resolver)) {
throw new TypeError('An instance of Resolver is expected');
}

const cache = new Map<string, RouteCacheRecord>();
const routesByName = new Map<string, Array<Route<T, R>>>();
const routesByName = new Map<string, Array<Route<T, R, C>>>();

return (routeName, params) => {
let route = getRouteByName(routesByName, routeName);
if (!route) {
routesByName.clear(); // clear cache
cacheRoutes(
cacheRoutes<T, R, C>(
routesByName,
resolver.root as Writable<Route<T, R>>,
resolver.root as Writable<Route<T, R, C>>,
resolver.root.__children,
options.cacheKeyProvider,
);
Expand Down Expand Up @@ -133,13 +133,13 @@ function generateUrls<T, R extends AnyObject = EmptyObject>(
let url = toPath(params) || '/';

if (options.stringifyQueryParams && params) {
const queryParams: Record<string, string | readonly string[]> = {};
const queryParams: Writable<IndexedParams> = {};
for (const [key, value] of Object.entries(params)) {
if (!(key in cached.keys) && value) {
queryParams[key] = value;
}
}
const query = options.stringifyQueryParams(queryParams);
const query = options.stringifyQueryParams(queryParams as UrlParams);
if (query) {
url += query.startsWith('?') ? query : `?${query}`;
}
Expand Down
25 changes: 14 additions & 11 deletions src/resolver/matchRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
*/

import type { Key } from 'path-to-regexp';
import type { Writable } from 'type-fest';
import matchPath, { type Match } from './matchPath.js';
import type { AnyObject, IndexedParams, Route } from './types.js';
import { getRoutePath, unwrapChildren } from './utils.js';

export type MatchWithRoute<T, R extends AnyObject> = Match &
export type MatchWithRoute<T, R extends AnyObject, C extends AnyObject> = Match &
Readonly<{
route: Route<T, R>;
route: Route<T, R, C>;
}>;

type RouteMatchIterator<T, R extends AnyObject> = Iterator<MatchWithRoute<T, R>, undefined, Route<T, R> | undefined>;
type RouteMatchIterator<T, R extends AnyObject, C extends AnyObject> = Iterator<
MatchWithRoute<T, R, C>,
undefined,
Route<T, R, C> | undefined
>;

/**
* Traverses the routes tree and matches its nodes to the given pathname from
Expand Down Expand Up @@ -66,15 +69,15 @@ type RouteMatchIterator<T, R extends AnyObject> = Iterator<MatchWithRoute<T, R>,
* Prefix matching can be enabled also by `children: true`.
*/
// eslint-disable-next-line @typescript-eslint/max-params
function matchRoute<T, R extends AnyObject>(
route: Route<T, R>,
function matchRoute<T, R extends AnyObject, C extends AnyObject>(
route: Route<T, R, C>,
pathname: string,
ignoreLeadingSlash?: boolean,
parentKeys?: readonly Key[],
parentParams?: IndexedParams,
): Iterator<MatchWithRoute<T, R>, undefined, Route<T, R> | undefined> {
): Iterator<MatchWithRoute<T, R, C>, undefined, Route<T, R, C> | undefined> {
let match: Match | null;
let childMatches: RouteMatchIterator<T, R> | null;
let childMatches: RouteMatchIterator<T, R, C> | null;
let childIndex = 0;
let routepath = getRoutePath(route);
if (routepath.startsWith('/')) {
Expand All @@ -86,12 +89,12 @@ function matchRoute<T, R extends AnyObject>(
}

return {
next(routeToSkip?: Route<T, R>): IteratorResult<MatchWithRoute<T, R>, undefined> {
next(routeToSkip?: Route<T, R, C>): IteratorResult<MatchWithRoute<T, R, C>, undefined> {
if (route === routeToSkip) {
return { done: true, value: undefined };
}

(route as Writable<Route<T, R>>).__children ??= unwrapChildren(route.children);
route.__children ??= unwrapChildren(route.children);
const children = route.__children ?? [];
const exact = !route.__children && !route.children;

Expand All @@ -114,7 +117,7 @@ function matchRoute<T, R extends AnyObject>(
while (childIndex < children.length) {
if (!childMatches) {
const childRoute = children[childIndex];
(childRoute as Writable<Route<T, R>>).parent = route;
childRoute.parent = route;

let matchedLength = match.path.length;
if (matchedLength > 0 && pathname.charAt(matchedLength) === '/') {
Expand Down
4 changes: 2 additions & 2 deletions src/resolver/resolveRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
* LICENSE.txt file in the root directory of this source tree.
*/
import type { EmptyObject } from 'type-fest';
import type { ActionResult, AnyObject, RouteContext } from './types.js';
import type { ActionResult, AnyObject, MaybePromise, RouteContext } from './types.js';
import { isFunction } from './utils.js';

// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
export default function resolveRoute<T, R extends AnyObject = EmptyObject>(
context: RouteContext<T, R>,
): ActionResult<T> {
): MaybePromise<ActionResult<T>> {
if (isFunction(context.route?.action)) {
return context.route.action(context);
}
Expand Down
Loading

0 comments on commit c2b6144

Please sign in to comment.