|
1 | 1 | import type { Route } from '@rspress/shared'; |
2 | | -import { matchPath, matchRoutes } from 'react-router-dom'; |
3 | 2 | import { routes } from 'virtual-routes'; |
4 | 3 |
|
| 4 | +/** |
| 5 | + * Normalize route path by: |
| 6 | + * 1. Decoding URI components |
| 7 | + * 2. Removing .html suffix |
| 8 | + * 3. Converting /index to / |
| 9 | + * |
| 10 | + * Examples: |
| 11 | + * - /api/config → /api/config |
| 12 | + * - /api/config.html → /api/config |
| 13 | + * - /api/config/index → /api/config/ |
| 14 | + * - /api/config/index.html → /api/config/ |
| 15 | + * - /index.html → / |
| 16 | + */ |
5 | 17 | function normalizeRoutePath(routePath: string) { |
6 | 18 | return decodeURIComponent(routePath) |
7 | 19 | .replace(/\.html$/, '') |
8 | 20 | .replace(/\/index$/, '/'); |
9 | 21 | } |
10 | 22 |
|
| 23 | +/** |
| 24 | + * Simple implementation of matchPath to check if a pattern matches a pathname |
| 25 | + * Better performance alternative of `import { matchPath } from 'react-router-dom'` |
| 26 | + * @param pattern - The route pattern to match against |
| 27 | + * @param pathname - The pathname to check |
| 28 | + * @returns Match object if matched, null otherwise |
| 29 | + * |
| 30 | + * @example |
| 31 | + * matchPath('/api/config', '/api/config') // { path: '/api/config' } |
| 32 | + * matchPath('/api/config/', '/api/config') // { path: '/api/config/' } |
| 33 | + * matchPath('/api/config', '/api/other') // null |
| 34 | + */ |
| 35 | +export function matchPath( |
| 36 | + pattern: string, |
| 37 | + pathname: string, |
| 38 | +): { path: string } | null { |
| 39 | + // Normalize both pattern and pathname for comparison |
| 40 | + // Always add trailing slash for consistent comparison |
| 41 | + const _pathname = normalizeRoutePath(pathname); |
| 42 | + const normalizedPattern = pattern.endsWith('/') ? pattern : `${pattern}/`; |
| 43 | + const normalizedPathname = _pathname.endsWith('/') |
| 44 | + ? _pathname |
| 45 | + : `${_pathname}/`; |
| 46 | + |
| 47 | + // Exact match |
| 48 | + if (normalizedPattern === normalizedPathname) { |
| 49 | + return { path: pattern }; |
| 50 | + } |
| 51 | + |
| 52 | + return null; |
| 53 | +} |
| 54 | + |
| 55 | +// Sort routes by path length (longest first) to match most specific routes first |
| 56 | +const sortedRoutes = [...routes].sort((a, b) => { |
| 57 | + const pathA = a.path || ''; |
| 58 | + const pathB = b.path || ''; |
| 59 | + return pathB.length - pathA.length; |
| 60 | +}); |
| 61 | + |
| 62 | +/** |
| 63 | + * Simple implementation of matchRoutes to find matching routes |
| 64 | + * Better performance alternative of `import { matchRoutes } from 'react-router-dom'` |
| 65 | + * @param _routes - Array of routes (unused, uses pre-sorted sortedRoutes) |
| 66 | + * @param pathname - The pathname to match |
| 67 | + * @returns Array of matched routes with route object, or null if no match |
| 68 | + */ |
| 69 | +function matchRoutes( |
| 70 | + _routes: Route[], |
| 71 | + pathname: string, |
| 72 | +): Array<{ route: Route }> | null { |
| 73 | + for (const route of sortedRoutes) { |
| 74 | + const routePath = route.path || ''; |
| 75 | + const match = matchPath(routePath, pathname); |
| 76 | + |
| 77 | + if (match) { |
| 78 | + return [{ route }]; |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + return null; |
| 83 | +} |
| 84 | + |
11 | 85 | const cache = new Map<string, Route>(); |
12 | 86 | /** |
13 | 87 | * this is a bridge of two core features Sidebar and RouteService |
|
0 commit comments