import type { JsonSerialize } from "../serialize.js"; import type { TypedRouteItem, TypedIncludeItem, TypedLayoutItem, TypedCacheItem, TypedTransitionItem, } from "../route-types.js"; import type { LocalOnlyInclude, UnnamedRoute } from "./pattern-types.js"; // ============================================================================ // Route Type Extraction Utilities // ============================================================================ /** * Prefix route names with a given prefix (e.g., "blog" + "post" = "blog.post") * * Filters out plain `string` index signatures to prevent dynamically-generated * routes from poisoning the route map. When TypeScript encounters very large * route sets (5000+ routes via Array.from), it may give up computing specific * types and fall back to Record. Without filtering, PrefixRoutes * would map `string` to `${prefix}.${string}`, creating an index signature that * accepts ANY prefixed name and defeats type-safe route checking. * * Uses `string extends K` (conservative filter): * - Drops `string` keys (TypeScript fallback) -> prevents `[x: `site.${string}`]` * - Keeps template literal patterns like `item${number}` from Array.from loops, * which are imprecise but still allow writing paths like `/shop/product/1` * * A more aggressive alternative (`{} extends Record`) would also drop * template literal patterns. We chose conservative because loop-generated routes * with `${number}` patterns still provide some value: they don't appear in * named-routes.gen.ts or IDE autocomplete, but they do let you manually write * valid paths without type errors. */ type PrefixRoutes< TRoutes extends Record, TPrefix extends string, > = TPrefix extends "" ? TRoutes : { [K in keyof TRoutes as K extends string ? string extends K ? never : `${TPrefix}.${K}` : never]: TRoutes[K]; }; /** * Prefix route patterns with a URL prefix (e.g., "/blog" + "/:slug" = "/blog/:slug") */ type PrefixPatterns< TRoutes extends Record, TUrlPrefix extends string, > = { [K in keyof TRoutes]: TRoutes[K] extends string ? `${TUrlPrefix}${TRoutes[K]}` : TRoutes[K] extends { readonly path: infer P extends string; readonly search: infer S; } ? { readonly path: `${TUrlPrefix}${P}`; readonly search: S } : TRoutes[K]; }; /** * Convert a union type to an intersection type. * Used to combine route maps from multiple siblings without recursive tuple processing. */ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( k: infer I, ) => void ? I : never; /** * Extract routes from a single item (path, include, layout, cache with children) */ type ExtractRoutesFromItem = // TypedRouteItem: extract name -> pattern (exclude unnamed routes) // When search schema is non-empty, value becomes { path, search } object T extends TypedRouteItem ? TName extends string ? TName extends UnnamedRoute ? {} // Exclude unnamed routes from type map : {} extends TSearch ? { [K in TName]: TPattern } : { [K in TName]: { readonly path: TPattern; readonly search: TSearch; }; } : {} : // TypedIncludeItem: extract prefixed routes (both name and URL prefix) T extends TypedIncludeItem< infer TRoutes, infer TNamePrefix, infer TUrlPrefix > ? TNamePrefix extends LocalOnlyInclude ? {} : TNamePrefix extends string ? TUrlPrefix extends string ? PrefixRoutes, TNamePrefix> : PrefixRoutes : TUrlPrefix extends string ? PrefixPatterns : TRoutes : // TypedLayoutItem: extract child routes from phantom type T extends TypedLayoutItem ? TChildRoutes : // TypedCacheItem: extract child routes from phantom type T extends TypedCacheItem ? TChildRoutes : // TypedTransitionItem: extract child routes from phantom type T extends TypedTransitionItem ? TChildRoutes : // Fallback (won't extract routes) {}; /** * Extract routes from an array of items using mapped types. * Uses UnionToIntersection to combine routes without recursive tuple processing, * removing the sibling limit that was caused by TypeScript recursion limits. */ type ExtractRoutesFromItems = T extends readonly any[] ? UnionToIntersection< { [K in keyof T]: ExtractRoutesFromItem }[number] > extends infer R ? R extends Record ? R : {} : {} : {}; /** * Main utility: extract route map from urls() callback return type * Uses mapped types for sibling processing (no sibling limit). */ export type ExtractRoutes = ExtractRoutesFromItems; // ============================================================================ // Response Type Extraction Utilities // ============================================================================ /** * Prefix keys of a Record with a dot-separated prefix. * Used for response type maps through include(). * Same index signature filter as PrefixRoutes (see comment there). */ type PrefixKeys< T extends Record, TPrefix extends string, > = TPrefix extends "" ? T : { [K in keyof T as K extends string ? string extends K ? never : `${TPrefix}.${K}` : never]: T[K]; }; /** * Extract response data types from a single item. * Parallel to ExtractRoutesFromItem but extracts name -> TData mapping. */ type ExtractResponsesFromItem = T extends TypedRouteItem ? TName extends string ? TName extends UnnamedRoute ? {} : { [K in TName]: TData } : {} : T extends TypedIncludeItem ? TNamePrefix extends LocalOnlyInclude ? {} : TNamePrefix extends string ? TResponses extends Record ? PrefixKeys : {} : TResponses extends Record ? TResponses : {} : T extends TypedLayoutItem ? TChildResponses extends Record ? TChildResponses : {} : T extends TypedCacheItem ? TChildResponses extends Record ? TChildResponses : {} : T extends TypedTransitionItem ? TChildResponses extends Record ? TChildResponses : {} : {}; /** * Extract responses from an array of items using mapped types. * Parallel to ExtractRoutesFromItems. */ type ExtractResponsesFromItems = T extends readonly any[] ? UnionToIntersection< { [K in keyof T]: ExtractResponsesFromItem }[number] > extends infer R ? R extends Record ? R : {} : {} : {}; /** * Main utility: extract response data type map from urls() callback return type. * Parallel to ExtractRoutes. */ export type ExtractResponses = ExtractResponsesFromItems; // ============================================================================ // Response Envelope Types // ============================================================================ /** * Error shape returned in the `{ error }` side of a JSON response envelope. */ export interface ResponseError { message: string; code?: string; type?: string; stack?: string; } /** * Discriminated union envelope for JSON response routes. * Consumers check `result.error` to discriminate between success and failure. * * @example * ```typescript * const result: ResponseEnvelope = await fetch(url).then(r => r.json()); * if (result.error) { * console.log(result.error.message, result.error.code); * return; * } * result.data.name // fully typed * ``` */ export type ResponseEnvelope = | { data: T; error?: undefined } | { data?: undefined; error: ResponseError }; // ============================================================================ // Response Type Consumer Utilities // ============================================================================ /** * Extract the response data type for a named route from a UrlPatterns instance. * Wraps in ResponseEnvelope since JSON response routes return enveloped data. * * @example * ```typescript * const apiPatterns = urls(({ path }) => [ * path.json("/health", (ctx) => ({ status: "ok", timestamp: Date.now() }), { name: "health" }), * ]); * * type HealthData = RouteResponse; * // ResponseEnvelope<{ status: string; timestamp: number }> * ``` * * The payload is the JSON wire shape (via `Rango.JsonSerialize`), matching * `Rango.PathResponse` and what `fetch().then(r => r.json())` actually yields — * e.g. a `Date` field resolves as `string`. */ export type RouteResponse = TPatterns extends { readonly _responses?: infer R; } ? TName extends keyof R ? ResponseEnvelope>> : never : never;