UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

220 lines (191 loc) 5.96 kB
import { getRoutes } from '../router/getRoutes' import { isTypedRoute, removeSupportedExtensions } from '../router/matchers' import type { RouteNode } from '../router/Route' import type { One } from '../vite/types' // /[...param1]/ - Match [...param1] const CATCH_ALL = /\[\.\.\..+?\]/g // /[param1] - Match [param1] const SLUG = /\[.+?\]/g export function getTypedRoutesDeclarationFile(ctx: One.RouteContext) { const staticRoutes = new Set<string>() const dynamicRoutes = new Set<string>() const dynamicRouteContextKeys = new Set<string>() walkRouteNode( getRoutes(ctx, { platformRoutes: false, // We don't need to generate platform specific routes ignoreEntryPoints: true, ignoreRequireErrors: true, // importMode: 'async', }), '', staticRoutes, dynamicRoutes, dynamicRouteContextKeys ) const hasRoutes = dynamicRouteContextKeys.size > 0 return `// deno-lint-ignore-file /* eslint-disable */ // biome-ignore: needed import import type { OneRouter } from 'one' declare module 'one' { export namespace OneRouter { export interface __routes<T extends string = string> extends Record<string, unknown> { StaticRoutes: ${setToUnionType(staticRoutes)} DynamicRoutes: ${setToUnionType(dynamicRoutes)} DynamicRouteTemplate: ${setToUnionType(dynamicRouteContextKeys)} IsTyped: true ${hasRoutes ? `RouteTypes: ${generateRouteTypesMap(dynamicRouteContextKeys)}` : ''} } } } ${ hasRoutes ? ` /** * Helper type for route information */ type RouteInfo<Params = Record<string, never>> = { Params: Params LoaderProps: { path: string; params: Params; request?: Request } }` : '' } `.trim() } /** * Generates a mapped type for all routes with their expanded types * This improves intellisense by showing actual param types instead of aliases */ function generateRouteTypesMap(dynamicRouteContextKeys: Set<string>): string { if (dynamicRouteContextKeys.size === 0) { return '{}' } const routes = [...dynamicRouteContextKeys].sort() const entries = routes .map((routePath) => { // Generate the param type inline for better intellisense const params = extractParams(routePath) const paramsType = params.length === 0 ? '{}' : generateInlineParamsType(params) return ` '${routePath}': RouteInfo<${paramsType}>` }) .join('\n') return `{\n${entries}\n }` } /** * Extract parameter names from a route path * e.g., "/docs/[slug]/[id]" -> ["slug", "id"] */ function extractParams(routePath: string): Array<{ name: string; isCatchAll: boolean }> { const params: Array<{ name: string; isCatchAll: boolean }> = [] const paramRegex = /\[(\.\.\.)?([\w]+)\]/g let match while ((match = paramRegex.exec(routePath)) !== null) { params.push({ name: match[2], isCatchAll: match[1] === '...', }) } return params } /** * Generate inline params type for better intellisense * e.g., [{ name: "slug", isCatchAll: false }] -> "{ slug: string }" */ function generateInlineParamsType( params: Array<{ name: string; isCatchAll: boolean }> ): string { const entries = params.map((p) => { const type = p.isCatchAll ? 'string[]' : 'string' return `${p.name}: ${type}` }) return `{ ${entries.join('; ')} }` } /** * Walks a RouteNode tree and adds the routes to the provided sets */ function walkRouteNode( routeNode: RouteNode | null, parentRoutePath: string, staticRoutes: Set<string>, dynamicRoutes: Set<string>, dynamicRouteContextKeys: Set<string> ) { if (!routeNode) return addRouteNode( routeNode, parentRoutePath, staticRoutes, dynamicRoutes, dynamicRouteContextKeys ) parentRoutePath = `${removeSupportedExtensions(`${parentRoutePath}/${routeNode.route}`).replace(/\/?index$/, '')}` // replace /index with / for (const child of routeNode.children) { walkRouteNode( child, parentRoutePath, staticRoutes, dynamicRoutes, dynamicRouteContextKeys ) } } /** * Given a RouteNode, adds the route to the correct sets * Modifies the RouteNode.route to be a typed-route string */ function addRouteNode( routeNode: RouteNode | null, parentRoutePath: string, staticRoutes: Set<string>, dynamicRoutes: Set<string>, dynamicRouteContextKeys: Set<string> ) { if (!routeNode?.route) return if (!isTypedRoute(routeNode.route)) return let routePath = `${parentRoutePath}/${removeSupportedExtensions(routeNode.route).replace(/\/?index$/, '')}` // replace /index with / if (!routePath.startsWith('/')) { routePath = `/${routePath}` } if (routeNode.dynamic) { for (const path of generateCombinations(routePath)) { dynamicRouteContextKeys.add(path) dynamicRoutes.add( // biome-ignore lint/suspicious/noTemplateCurlyInString: intentionally generating type string `${path.replaceAll(CATCH_ALL, '${string}').replaceAll(SLUG, '${OneRouter.SingleRoutePart<T>}')}` ) } } else { for (const combination of generateCombinations(routePath)) { staticRoutes.add(combination) } } } /** * Converts a Set to a TypeScript union type */ const setToUnionType = <T>(set: Set<T>) => { return set.size > 0 ? [...set] .sort() .map((s) => `\`${s}\``) .join(' | ') : 'never' } function generateCombinations(pathname) { const groups = pathname .split('/') .filter((part) => part.startsWith('(') && part.endsWith(')')) const combinations: string[] = [] function generate(currentIndex, currentPath) { if (currentIndex === groups.length) { combinations.push(currentPath.replace(/\/{2,}/g, '/')) return } const group = groups[currentIndex] const withoutGroup = currentPath.replace(`/${group}`, '') generate(currentIndex + 1, withoutGroup) generate(currentIndex + 1, currentPath) } generate(0, pathname) return combinations }