|
| 1 | +import { |
| 2 | + LoaderFn, |
| 3 | + Outlet, |
| 4 | + ReactLocation, |
| 5 | + Route, |
| 6 | + Router, |
| 7 | + RouterProps, |
| 8 | +} from '@tanstack/react-location'; |
| 9 | +import { Fragment } from 'react'; |
| 10 | + |
| 11 | +type Element = () => JSX.Element; |
| 12 | +type Module = { default: Element; Loader: LoaderFn; Pending: Element; Failure: Element }; |
| 13 | + |
| 14 | +const PRESERVED = import.meta.glob<Module>('/src/pages/(_app|404).tsx', { eager: true }); |
| 15 | +const ROUTES = import.meta.glob<Module>('/src/pages/**/[a-z[]*.tsx'); |
| 16 | + |
| 17 | +const preservedRoutes: Partial<Record<string, () => JSX.Element>> = Object.keys(PRESERVED).reduce( |
| 18 | + (routes, key) => { |
| 19 | + const path = key.replace(/\/src\/pages\/|\.tsx$/g, ''); |
| 20 | + return { ...routes, [path]: PRESERVED[key]?.default }; |
| 21 | + }, |
| 22 | + {}, |
| 23 | +); |
| 24 | + |
| 25 | +const regularRoutes = Object.keys(ROUTES).reduce<Route[]>((routes, key) => { |
| 26 | + const module = ROUTES[key]; |
| 27 | + const route: Route = { |
| 28 | + element: () => module().then(mod => (mod?.default ? <mod.default /> : <></>)), |
| 29 | + loader: async (...args) => module().then(mod => mod?.Loader?.(...args)), |
| 30 | + pendingElement: async () => module().then(mod => (mod?.Pending ? <mod.Pending /> : null)), |
| 31 | + errorElement: async () => module().then(mod => (mod?.Failure ? <mod.Failure /> : null)), |
| 32 | + }; |
| 33 | + |
| 34 | + const segments = key |
| 35 | + .replace(/\/src\/pages|\.tsx$/g, '') |
| 36 | + .replace(/\[\.{3}.+\]/, '*') |
| 37 | + .replace(/\[([^\]]+)\]/g, ':$1') |
| 38 | + .split('/') |
| 39 | + .filter(Boolean); |
| 40 | + |
| 41 | + segments.reduce((parent, segment, index) => { |
| 42 | + const path = segment.replace(/index|\./g, '/'); |
| 43 | + const root = index === 0; |
| 44 | + const leaf = index === segments.length - 1 && segments.length > 1; |
| 45 | + const node = !root && !leaf; |
| 46 | + const insert = /^\w|\//.test(path) ? 'unshift' : 'push'; |
| 47 | + |
| 48 | + if (root) { |
| 49 | + const dynamic = path.startsWith(':') || path === '*'; |
| 50 | + if (dynamic) return parent; |
| 51 | + |
| 52 | + const last = segments.length === 1; |
| 53 | + if (last) { |
| 54 | + routes.push({ path, ...route }); |
| 55 | + return parent; |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + if (root || node) { |
| 60 | + const current = root ? routes : parent.children; |
| 61 | + const found = current?.find(route => route.path === path); |
| 62 | + if (found) found.children ??= []; |
| 63 | + else current?.[insert]({ path, children: [] }); |
| 64 | + return found || (current?.[insert === 'unshift' ? 0 : current.length - 1] as Route); |
| 65 | + } |
| 66 | + |
| 67 | + if (leaf) { |
| 68 | + parent?.children?.[insert]({ path, ...route }); |
| 69 | + } |
| 70 | + |
| 71 | + return parent; |
| 72 | + }, {} as Route); |
| 73 | + |
| 74 | + return routes; |
| 75 | +}, []); |
| 76 | + |
| 77 | +const App = preservedRoutes?.['_app'] || Fragment; |
| 78 | +const NotFound = preservedRoutes?.['404'] || Fragment; |
| 79 | + |
| 80 | +const location = new ReactLocation(); |
| 81 | +const routes = [...regularRoutes, { path: '*', element: <NotFound /> }]; |
| 82 | + |
| 83 | +const Routes = (props: Omit<RouterProps, 'children' | 'location' | 'routes'> = {}) => { |
| 84 | + return ( |
| 85 | + <Router {...props} location={location} routes={routes}> |
| 86 | + <App> |
| 87 | + <Outlet /> |
| 88 | + </App> |
| 89 | + </Router> |
| 90 | + ); |
| 91 | +}; |
| 92 | + |
| 93 | +export default Routes; |
0 commit comments