Source: lib/core/runtime/RouteMatcher.js

/**
 * Pure environment-agnostic module for compiling route patterns and matching URLs.
 */
export class RouteMatcher {
  /**
   * Compiles a route pattern into a regular expression, tracking parameter names.
   * @param {string} routePattern - The route pattern (e.g. '/user/:id').
   * @returns {{regex: RegExp, paramNames: string[]}}
   */
  static compileRoute(routePattern) {
    const paramNames = [];
    const escaped = routePattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');

    const regexStr = escaped.replace(/(:[a-zA-Z0-9_$]+)|(\*)/g, (_match, param) => {
      if (param) {
        paramNames.push(param.slice(1));
        return '([^/]+)';
      }
      paramNames.push('wildcard');
      return '(.*)';
    });

    return { regex: new RegExp(`^${regexStr}$`), paramNames };
  }

  /**
   * Normalizes a hash string with an optional namespace prefix.
   * @param {string} hash - Raw hash string (e.g. '#/app1/home').
   * @param {string} [prefix] - Namespace prefix (e.g. '/app1').
   * @returns {string|null} Normalized hash (e.g. '#/home'), or null if prefix does not match.
   */
  static normalizeHash(hash, prefix) {
    let normalized = hash || '#/';
    const secondHashIndex = normalized.indexOf('#', 1);
    if (secondHashIndex !== -1) {
      normalized = normalized.substring(0, secondHashIndex);
    }

    if (prefix) {
      const expectedStart = '#' + prefix;
      if (!normalized.startsWith(expectedStart)) {
        return null;
      }
      normalized = '#' + normalized.substring(expectedStart.length);
      if (!normalized.startsWith('#/')) {
        normalized = '#/' + normalized.substring(1);
      }
    }
    return normalized;
  }

  /**
   * Recursively finds a route match.
   * @param {Object} routes - The routes configuration
   * @param {string} decodedPath - The decoded path to match against
   * @param {string} basePath - The accumulated base path
   * @param {Object|null} parentDef - The parent route definition, if any
   * @returns {Object|null} The match result or null if not found
   */
  static _findMatch(routes, decodedPath, basePath = '', parentDef = null) {
    for (const [routePattern, routeDef] of Object.entries(routes)) {
      if (routePattern === '*') continue;

      let fullPattern = routePattern;
      if (basePath) {
        if (basePath.endsWith('/') && routePattern.startsWith('/')) {
          fullPattern = basePath + routePattern.slice(1);
        } else if (!basePath.endsWith('/') && !routePattern.startsWith('/')) {
          fullPattern = basePath + '/' + routePattern;
        } else {
          fullPattern = basePath + routePattern;
        }
      }

      const { regex, paramNames } = RouteMatcher.compileRoute(fullPattern);
      const match = decodedPath.match(regex);
      if (match) {
        return {
          pattern: fullPattern,
          definition: routeDef,
          parent: parentDef,
          match,
          paramNames
        };
      }

      if (routeDef && typeof routeDef === 'object' && routeDef.children) {
        let childRoutes = routeDef.children;
        if (Array.isArray(childRoutes)) {
          childRoutes = childRoutes.reduce((acc, child) => {
            acc[child.path] = child;
            return acc;
          }, {});
        }

        const nestedMatch = this._findMatch(childRoutes, decodedPath, fullPattern, {
          pattern: fullPattern,
          definition: routeDef
        });

        if (nestedMatch) return nestedMatch;
      }
    }
    return null;
  }

  /**
   * Checks if the route definitions have a non-fallback match for the given hash.
   * @param {Object<string, any>} routes - Map of route patterns.
   * @param {string} hash - The URL hash.
   * @param {object} [options] - Router options (e.g. prefix).
   * @returns {boolean} True if a non-fallback route matches.
   */
  static matches(routes, hash, options = {}) {
    const normalizedHash = RouteMatcher.normalizeHash(hash, options.prefix);
    if (normalizedHash === null) {
      return false;
    }

    const [pathPart] = normalizedHash.split('?');
    let decodedPath;
    try {
      decodedPath = decodeURIComponent(pathPart);
    } catch {
      decodedPath = pathPart;
    }

    return !!this._findMatch(routes, decodedPath);
  }

  /**
   * Matches a hash string against a collection of routes.
   * @param {Object<string, any>} routes - Map of route patterns to definitions.
   * @param {string} hash - The URL hash.
   * @param {object} [options] - Router options (e.g. prefix).
   * @param {Iterable<object>} [activeRouters] - Active router instances to check for fallback wildcard resolution.
   * @param {object} [currentRouter] - The current router instance.
   * @returns {object}
   */
  static matchRoute(routes, hash, options = {}, activeRouters = [], currentRouter = null) {
    const normalizedHash = RouteMatcher.normalizeHash(hash, options.prefix);
    if (normalizedHash === null) {
      return { matchedRoute: null, params: {}, otherRouterMatches: false, normalizedHash: null };
    }

    let matchedRoute = null;
    const params = {};
    const query = {};

    const [pathPart, queryPart] = normalizedHash.split('?');
    let decodedPath;

    try {
      decodedPath = decodeURIComponent(pathPart);
    } catch {
      decodedPath = pathPart;
    }

    if (queryPart) {
      const queryParams = new URLSearchParams(queryPart);
      for (const [key, value] of queryParams.entries()) {
        if (value === 'true') {
          query[key] = true;
        } else if (value === 'false') {
          query[key] = false;
        } else if (/^\d+$/.test(value)) {
          query[key] = Number(value);
        } else {
          query[key] = value;
        }
      }
    }

    const found = this._findMatch(routes, decodedPath);
    if (found) {
      matchedRoute = { pattern: found.pattern, definition: found.definition };
      if (found.parent) {
        matchedRoute.parent = found.parent;
      }

      found.paramNames.forEach((name, idx) => {
        const value = found.match[idx + 1];
        try {
          params[name] = decodeURIComponent(value);
        } catch {
          params[name] = value;
        }
      });

      if (Object.keys(query).length > 0) {
        params.query = { ...query };
      }
    }

    let otherRouterMatches = false;
    if (!matchedRoute && routes['*']) {
      const rawHash = hash || '#/';
      otherRouterMatches = Array.from(activeRouters || []).some(
        (r) => r !== currentRouter && typeof r.matches === 'function' && r.matches(rawHash),
      );

      if (!otherRouterMatches) {
        matchedRoute = { pattern: '*', definition: routes['*'] };
      }
    }

    return { matchedRoute, params, query, path: pathPart, otherRouterMatches, normalizedHash };
  }
}