All files / router/src registry.ts

100% Statements 73/73
100% Branches 23/23
100% Functions 3/3
100% Lines 73/73

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 1431x                             1x           1x 1x 1x             1x 978x 978x 978x                                               1x 324x 324x 324x 324x 324x 324x   324x 143x 143x   322x 322x   324x 324x 324x   324x 277x   277x 174x 258x 103x 1x 1x   103x 103x 103x 103x 103x 103x 324x 23x 23x   23x 5x 23x 18x 1x 1x   18x 18x 18x 18x 18x 18x 45x 16x 16x   16x 1x 1x   16x 16x 16x 16x 16x 16x 324x 1x                       143x 143x 143x 143x 143x 143x  
import { DYNAMIC_ROUTE_KEY, ROUTE_MAP_LINK, WILDCARD_ROUTE_KEY } from './constant.js';
import type { MatchedRoute, TRec, UnknownRoute } from './types.js';
 
/**
 * A registry for organizing and matching routes.
 *
 * Extends Map to store child routes keyed by their segment names.
 * Supports static, dynamic (`:param`), and wildcard (`*`) route matching.
 *
 * @example
 * ```ts
 * const registry = new RouteRegistry(route);
 * const match = registry.match('/users/123');
 * ```
 */
export class RouteRegistry extends Map {
  /**
   * Gets the name of the route this registry is associated with.
   *
   * @returns The route name
   */
  public get name() {
    return this.route.name;
  }
 
  /**
   * Creates a new RouteRegistry instance.
   *
   * @param route - The route this registry is associated with
   */
  constructor(public route: UnknownRoute) {
    super();
    ROUTE_MAP_LINK.set(this.route, this);
  }
 
  /**
   * Matches a URL path against the registered routes.
   *
   * Recursively traverses the route tree to find the best match.
   * Supports static segments, dynamic parameters, and wildcards.
   *
   * @param urlSegments - The URL path to match, as a string or array of segments
   * @param segments - Accumulator for matched route segments (internal use)
   * @param params - Accumulator for extracted parameters (internal use)
   * @param index - Current segment index (internal use)
   * @returns A matched route with segments and params, or undefined if no match
   *
   * @example
   * ```ts
   * const match = registry.match('/users/123');
   * if (match) {
   *   console.log(match.route); // The matched route
   *   console.log(match.params); // { id: '123' }
   *   console.log(match.segments); // Array of matched routes
   * }
   * ```
   */
  public match(
    urlSegments: string | string[],
    segments: UnknownRoute[] = [],
    params: TRec = {},
    index = 0
  ): MatchedRoute | void {
    if (!urlSegments || !urlSegments.length) return;
 
    if (typeof urlSegments === 'string') {
      urlSegments = cleanPath(urlSegments).split('/');
    }
 
    const segment = urlSegments[index];
    const recursive = urlSegments.length > index + 1;
 
    const staticRoute = segment === '' ? this : (this.get(segment) as RouteRegistry);
    const dynamicRoute = this.get(DYNAMIC_ROUTE_KEY) as RouteRegistry;
    const wildcardRoute = this.get(WILDCARD_ROUTE_KEY) as RouteRegistry;
 
    if (staticRoute) {
      segments.push(staticRoute.route);
 
      if (recursive) {
        return staticRoute.match(urlSegments, segments, params, index + 1);
      } else {
        if (staticRoute.route.index) {
          segments.push(staticRoute.route.index);
        }
 
        return {
          route: staticRoute.route,
          segments,
          params,
        };
      }
    } else if (dynamicRoute) {
      params[dynamicRoute.name.replace(/^:/, '')] = segment;
      segments.push(dynamicRoute.route);
 
      if (recursive) {
        return dynamicRoute.match(urlSegments, segments, params, index + 1);
      } else {
        if (dynamicRoute.route.index) {
          segments.push(dynamicRoute.route.index);
        }
 
        return {
          route: dynamicRoute.route,
          segments,
          params,
        };
      }
    } else if (wildcardRoute) {
      params['*'] = urlSegments.slice(index);
      segments.push(wildcardRoute.route);
 
      if (wildcardRoute.route.index) {
        segments.push(wildcardRoute.route.index);
      }
 
      return {
        route: wildcardRoute.route,
        segments,
        params,
      };
    }
  }
}
 
/**
 * Cleans a path string by normalizing slashes.
 *
 * Removes leading, trailing, and duplicate slashes.
 *
 * @param path - The path string to clean
 * @returns The cleaned path string
 *
 * @internal
 */
function cleanPath(path: string) {
  return path
    .replace(/^[\/]+/, '/')
    .replace(/[\/]+/g, '/')
    .replace(/[\/]+$/, '');
}