All files / src/core/normalize normalize-path.ts

100% Statements 30/30
87.5% Branches 14/16
100% Functions 4/4
100% Lines 30/30

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                                      42x         42x     42x     42x 1x       41x 16x 2x   14x     41x 2x   39x 2x     39x               37x 37x     37x 15x     37x                   70x       70x   70x     70x     70x 70x     70x 70x   70x 70x     70x      
/**
 * Path normalization for API routes
 * Core logic - no Vite dependencies
 */
 
import { normalizePath as normalizePathUtil } from '../../shared/utils.js';
import { PARAM_PATTERN } from '../../shared/constants.js';
 
/**
 * Converts a file path to a normalized HTTP route
 *
 * Examples:
 * - users/[id].get.ts -> users/:id
 * - posts/[...ids].get.ts -> posts/*ids
 * - health.get.ts -> health
 * - execute/index.post.ts -> execute (index treated as directory index)
 */
export function normalizeRoutePath(filePath: string): string {
  // Remove extensions (.ts, .js) and HTTP method
  let path = filePath
    .replace(/\.(ts|js)$/, '')
    .replace(/\.(get|post|put|patch|delete|head|options)$/, '');
  
  // Normalize separators
  path = path.replace(/\\/g, '/');
  
  // Remove leading/trailing slashes (but preserve empty string for root route)
  path = path.replace(/^\/+|\/+$/g, '');
  
  // If path is empty, it means root route (e.g., health.get.ts)
  if (!path) {
    return '';
  }
  
  // Convert [id] to :id and [...ids] to *ids (catch-all)
  path = path.replace(PARAM_PATTERN, (match, isCatchAll, paramName) => {
    if (isCatchAll) {
      return `*${paramName}`;
    }
    return `:${paramName}`;
  });
 
  if (path === 'index') {
    return '';
  }
  if (path.endsWith('/index')) {
    path = path.slice(0, -6);
  }
 
  return path;
}
 
/**
 * Extracts parameters from a path pattern
 * Example: "users/:id/posts/:postId" -> ["id", "postId"]
 */
export function extractParamsFromPattern(pattern: string): string[] {
  const params: string[] = [];
  const paramRegex = /[:*]([^/]+)/g;
  let match;
  
  while ((match = paramRegex.exec(pattern)) !== null) {
    params.push(match[1]);
  }
  
  return params;
}
 
/**
 * Converts a path pattern to regex for matching
 * Example: "users/:id" -> /^\/users\/([^/]+)$/
 * Example: "posts/*ids" -> /^\/posts\/(.*)$/
 */
export function patternToRegex(pattern: string): RegExp {
  // Normalize pattern: empty route becomes '/', others add / at the beginning
  let normalizedPattern = pattern === '' ? '/' : (pattern.startsWith('/') ? pattern : `/${pattern}`);
  
  // First replace placeholders (before escaping)
  // Replace *param (catch-all) with temporary placeholder
  normalizedPattern = normalizedPattern.replace(/\*(\w+)/g, '__CATCHALL_$1__');
  // Replace :param with temporary placeholder
  normalizedPattern = normalizedPattern.replace(/:(\w+)/g, '__PARAM_$1__');
  
  // Escape special characters
  let regexStr = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
  
  // Restore placeholders as capture groups
  regexStr = regexStr.replace(/__PARAM_\w+__/g, '([^/]+)');
  regexStr = regexStr.replace(/__CATCHALL_(\w+)__/g, '(.*)');
  
  // Ensure it starts and ends correctly
  Eif (!regexStr.startsWith('^')) {
    regexStr = '^' + regexStr;
  }
  Eif (!regexStr.endsWith('$')) {
    regexStr = regexStr + '$';
  }
  
  return new RegExp(regexStr);
}