one
Version:
One is a new React Framework that makes Vite serve both native and web.
220 lines (191 loc) • 5.96 kB
text/typescript
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
}