import type { ExtractParams } from "./types.js"; import type { SearchSchema, ResolveSearchSchema } from "./search-params.js"; import { serializeSearchParams } from "./search-params.js"; import { substitutePatternParams } from "./router/substitute-pattern-params.js"; /** * Sanitize prefix string by removing leading slash * "/shop" -> "shop", "blog" -> "blog", "" -> "" */ export type SanitizePrefix = T extends `/${infer P}` ? P : T; /** * Helper type to merge multiple route definitions into a single accumulated type. * * @example * ```typescript * type AppRoutes = MergeRoutes<[typeof siteRoutes, typeof apiRoutes]>; * ``` */ export type MergeRoutes = T extends [ infer First, ...infer Rest, ] ? First & MergeRoutes : {}; /** * Helper to safely extract route patterns from a routes object * Handles string values, { path, response } objects, and interface types (like RegisteredRoutes) */ type RoutePatternFor< TRoutes, TName extends keyof TRoutes, > = TRoutes[TName] extends string ? TRoutes[TName] : TRoutes[TName] extends { readonly path: infer P extends string } ? P : string; /** * Extract params type for a route */ export type ParamsFor = ExtractParams< RoutePatternFor >; /** * Check if an object type has any keys */ type IsEmptyObject = keyof T extends never ? true : false; /** * Extract search schema from a route entry. * Returns {} if no search schema is defined. */ type ExtractSearchSchema< TRoutes, TName extends keyof TRoutes, > = TRoutes[TName] extends { readonly search: infer S extends SearchSchema } ? S : {}; /** * Type-safe reverse function signature (Django-style URL reversal) * * Validates route names and params at compile time. * Use route names instead of raw paths for full type safety. * * @example * ```typescript * reverse("cart") // ✓ Validates route exists * reverse("product.detail", { id: "123" }) // ✓ Validates route + params * ``` */ export type ReverseFunction = { /** * Route without params - validates route name exists */ ( name: IsEmptyObject< ExtractParams> > extends true ? TName : never, ): string; /** * Route with params - validates both route name and params */ ( name: TName, params: ExtractParams>, ): string; /** * Route with params and search - validates route name, params, and search */ ( name: TName, params: ExtractParams>, search: ResolveSearchSchema>, ): string; /** * Dot-prefixed route without params - strictly local resolution */ ( name: IsEmptyObject< ExtractParams> > extends true ? `.${TName}` : never, ): string; /** * Dot-prefixed route with params - strictly local resolution */ ( name: `.${TName}`, params: ExtractParams>, ): string; /** * Dot-prefixed route with params and search - strictly local resolution */ ( name: `.${TName}`, params: ExtractParams>, search: ResolveSearchSchema>, ): string; }; /** * Type-safe scoped reverse function with separate local and global namespaces. * * - `.name` — local resolution within the current include() scope * - `name` — global resolution against the named-routes definition * * @example * ```typescript * reverse(".article", { slug: "hello" }) // ✓ Local route (resolves with mount prefix) * reverse(".index") // ✓ Local route (no params) * reverse("magazine.index") // ✓ Global route (fully qualified) * reverse("blog.post", { slug: "hello" }) // ✓ Global route + params * reverse(".typo") // ✗ Compile error (not in local routes) * reverse("typo") // ✗ Compile error (not in global routes) * ``` */ export type ScopedReverseFunction< TLocalRoutes, TGlobalRoutes = TLocalRoutes, > = { /** * Global route without params */ ( name: IsEmptyObject< ExtractParams> > extends true ? TName : never, ): string; /** * Global route with params */ ( name: TName, params: ExtractParams>, ): string; /** * Global route with params and search */ ( name: TName, params: ExtractParams>, search: ResolveSearchSchema>, ): string; /** * Dot-prefixed local route without params */ ( name: IsEmptyObject< ExtractParams> > extends true ? `.${TName}` : never, ): string; /** * Dot-prefixed local route with params */ ( name: `.${TName}`, params: ExtractParams>, ): string; /** * Dot-prefixed local route with params and search */ ( name: `.${TName}`, params: ExtractParams>, search: ResolveSearchSchema>, ): string; }; /** * Extract local routes type from UrlPatterns * Used with scopedReverse() to get the routes type from patterns */ export type ExtractLocalRoutes = TPatterns extends { readonly _routes?: infer TRoutes; } ? TRoutes : TPatterns extends Record ? TPatterns : Record; /** * Params accepted by `useReverse(routes)`. The route's own params are * required, and additional string keys are permitted so callers can * override values that would otherwise be auto-filled from the matched * route's `useParams()` (e.g. an enclosing `:tenantId` mount segment). */ export type LocalReverseParams = ExtractParams & { readonly [extra: string]: string | undefined; }; /** * Type-safe local reverse function with dot-prefixed names only. * * Returned by `useReverse(routes)` on the client. The route map is the * exposure boundary (a generated `routes` from a `urls()` module) and the * scope is implicit from that import — there is no global namespace, so * names must be dot-prefixed to mirror `ctx.reverse(".name")`. * * @example * ```typescript * const reverse = useReverse(blogRoutes); * reverse(".index"); // ✓ no params * reverse(".post", { postId: "hello" }); // ✓ with params * reverse(".search", {}, { q: "hi" }); // ✓ with search schema * reverse(".typo"); // ✗ compile error * ``` */ export type LocalReverseFunction = { /** * Dot-prefixed local route without params */ ( name: IsEmptyObject< ExtractParams> > extends true ? `.${TName}` : never, ): string; /** * Dot-prefixed local route with params */ ( name: `.${TName}`, params: LocalReverseParams>, ): string; /** * Dot-prefixed local route with params and search */ ( name: `.${TName}`, params: LocalReverseParams>, search: ResolveSearchSchema>, ): string; }; /** * Extract the response data type for a named route from a UrlPatterns instance. * Re-exported from urls.ts for consumer convenience. */ export type { RouteResponse } from "./urls.js"; /** * Get a locally-typed reverse function from ctx.reverse for composable modules. * * This is a type-only cast - ctx.reverse already resolves names at runtime. * Provides type safety: `.name` validates against local routes, * `name` validates against global named-routes. * * @param reverse - The ctx.reverse function from HandlerContext * @returns The same reverse function, typed with local + global routes * * @example * ```typescript * // urls/blog.tsx * export const blogPatterns = urls(({ path }) => [ * path("/", (ctx) => { * const reverse = scopedReverse(ctx.reverse); * * reverse(".index"); // ✓ Local route * reverse(".post", { slug: "x" }); // ✓ Local with params * reverse("shop.cart"); // ✓ Global route * * return ; * }, { name: "index" }), * * path("/:slug", BlogPost, { name: "post" }), * ]); * ``` */ export function scopedReverse( reverse: (...args: any[]) => string, ): ScopedReverseFunction> { return reverse as ScopedReverseFunction>; } /** * Create a type-safe reverse function for URL generation * * @param routeMap - Flattened route map with all registered routes * @returns Type-safe reverse function * * @example * ```typescript * // Given routes: { cart: "/shop/cart", detail: "/shop/product/:slug" } * const reverse = createReverse(routeMap); * reverse("cart"); // "/shop/cart" * reverse("detail", { slug: "my-product" }); // "/shop/product/my-product" * ``` */ type RouteMapEntry = string | { path: string; search?: Record }; function resolveRoutePattern( entry: RouteMapEntry | undefined, ): string | undefined { if (!entry) return undefined; return typeof entry === "string" ? entry : entry.path; } export function createReverse>( routeMap: TRoutes, ): ReverseFunction> { return (( name: string, params?: Record, search?: Record, ) => { const pattern = resolveRoutePattern( routeMap[name] as unknown as RouteMapEntry, ); if (!pattern) { // During build-time discovery, lazy includes haven't resolved yet. // Return a placeholder instead of crashing the build. if ((globalThis as any).__rscRouterDiscoveryActive) { return `/__unresolved_reverse/${name}`; } throw new Error(`Unknown route: ${name}`); } let result = params ? substitutePatternParams(pattern, params, name) : pattern; // Append search params as query string if (search) { const qs = serializeSearchParams(search); if (qs) { result += `?${qs}`; } } return result; }) as ReverseFunction; }