import type { ReactNode } from "react"; /** * Props for the Document component that wraps the entire application. */ export type DocumentProps = { children: ReactNode; }; /** * Parse constraint values into a union type * "a|b|c" -> "a" | "b" | "c" */ type ParseConstraint = T extends `${infer First}|${infer Rest}` ? First | ParseConstraint : T; /** * Extract param info from a param segment * * Handles: * - :param -> { name: "param", optional: false, type: string } * - :param? -> { name: "param", optional: true, type: string } * - :param(a|b) -> { name: "param", optional: false, type: "a" | "b" } * - :param(a|b)? -> { name: "param", optional: true, type: "a" | "b" } */ type ExtractParamInfo = // Optional + constrained (with optional suffix): :param(a|b)?suffix T extends `${infer Name}(${infer Constraint})?${string}` ? { name: Name; optional: true; type: ParseConstraint } : // Constrained (with optional suffix): :param(a|b)suffix T extends `${infer Name}(${infer Constraint})${string}` ? { name: Name; optional: false; type: ParseConstraint } : // Optional (with optional suffix): :param?suffix T extends `${infer Name}?${string}` ? { name: Name; optional: true; type: string } : // Param with dot-suffix: :param.html T extends `${infer Name}.${string}` ? { name: Name; optional: false; type: string } : // Param with dash-suffix: :param-slug T extends `${infer Name}-${string}` ? { name: Name; optional: false; type: string } : // Param with tilde-suffix: :param~v2 T extends `${infer Name}~${string}` ? { name: Name; optional: false; type: string } : // Required: :param (no suffix) { name: T; optional: false; type: string }; /** * Build param object from info */ type ParamFromInfo = Info extends { name: infer N extends string; optional: true; type: infer V; } ? { [K in N]?: V } : Info extends { name: infer N extends string; optional: false; type: infer V; } ? { [K in N]: V } : never; /** * Merge two param objects preserving optionality * Uses Pick to preserve the modifiers from source types */ type MergeParams = Pick & Pick extends infer O ? { [K in keyof O]: O[K] } : never; /** * Extract route params from a pattern with depth limit to prevent infinite recursion * * Supports: * - Required params: /:slug -> { slug: string } * - Optional params: /:locale? -> { locale?: string } * - Constrained params: /:locale(en|gb) -> { locale: "en" | "gb" } * - Optional + constrained: /:locale(en|gb)? -> { locale?: "en" | "gb" } * * @example * ExtractParams<"/products/:id"> // { id: string } * ExtractParams<"/:locale?/blog/:slug"> // { locale?: string; slug: string } * ExtractParams<"/:locale(en|gb)/blog"> // { locale: "en" | "gb" } * ExtractParams<"/:locale(en|gb)?/blog/:slug"> // { locale?: "en" | "gb"; slug: string } */ export type ExtractParams< T extends string, Depth extends readonly unknown[] = [], > = Depth["length"] extends 10 ? { [key: string]: string | undefined } // Fallback to generic params if too deep : // Match param with remaining path: :param.../rest T extends `${infer _Start}:${infer Param}/${infer Rest}` ? MergeParams< ParamFromInfo>, ExtractParams<`/${Rest}`, readonly [...Depth, unknown]> > : // Match param at end: :param... T extends `${infer _Start}:${infer Param}` ? ParamFromInfo> : {}; /** * Trailing slash handling mode * - "never": Redirect URLs with trailing slash to without * - "always": Redirect URLs without trailing slash to with * - "ignore": Match both with and without trailing slash */ export type TrailingSlashMode = "never" | "always" | "ignore"; /** * Route configuration object (alternative to string path) */ export type RouteConfig = { path: string; trailingSlash?: TrailingSlashMode; }; /** * Route definition options (global defaults) */ export type RouteDefinitionOptions = { trailingSlash?: TrailingSlashMode; }; export type RouteDefinition = { [key: string]: string | RouteConfig | RouteDefinition; }; /** * Recursively flatten nested routes with depth limit to prevent infinite recursion * Transforms: { products: { detail: "/product/:slug" } } => { "products.detail": "/product/:slug" } * Also handles RouteConfig objects: { api: { path: "/api" } } => { "api": "/api" } */ type FlattenRoutes< T extends RouteDefinition, Prefix extends string = "", Depth extends readonly unknown[] = [], > = Depth["length"] extends 5 ? never : { [K in keyof T]: T[K] extends string ? Record<`${Prefix}${K & string}`, T[K]> : T[K] extends RouteConfig ? Record<`${Prefix}${K & string}`, T[K]["path"]> : T[K] extends RouteDefinition ? FlattenRoutes< T[K], `${Prefix}${K & string}.`, readonly [...Depth, unknown] > : never; }[keyof T]; /** * Union to intersection helper */ type UnionToIntersection = ( U extends unknown ? (k: U) => void : never ) extends (k: infer I) => void ? I : never; /** * Resolved route map - flattened route definitions with full paths */ export type ResolvedRouteMap = UnionToIntersection< FlattenRoutes >;