/** * Django-inspired URL patterns for @rangojs/router * * This module provides `urls()` and `path()` for defining routes with * URL patterns visible at the definition site. * * @example * ```typescript * // urls/blog.ts * export const blogPatterns = urls(({ path, layout, loader }) => [ * layout(BlogLayout, () => [ * path("/", BlogIndex, { name: "index" }), * path("/:slug", BlogPost, { name: "post" }, () => [ * loader(PostLoader), * ]), * ]), * ]); * * // urls/index.ts * export const urlpatterns = urls(({ path, layout, include }) => [ * layout(RootLayout, () => [ * path("/", HomePage, { name: "home" }), * include("/blog", blogPatterns, { name: "blog" }), * ]), * ]); * ``` */ import type { ReactNode } from "react"; import type { DefaultEnv, ErrorBoundaryHandler, ExtractParams, Handler, HandlerContext, LoaderDefinition, MiddlewareFn, NotFoundBoundaryHandler, PartialCacheOptions, RouterEnv, ShouldRevalidateFn, TrailingSlashMode } from "./types.js"; import type { CookieOptions } from "./router/middleware.js"; import type { AllUseItems, TypedLayoutItem, TypedRouteItem, ParallelItem, InterceptItem, MiddlewareItem, RevalidateItem, LoaderItem, LoadingItem, ErrorBoundaryItem, NotFoundBoundaryItem, LayoutUseItem, RouteUseItem, ResponseRouteUseItem, ParallelUseItem, InterceptUseItem, LoaderUseItem, WhenItem, TypedCacheItem, TypedIncludeItem, UrlPatternsBrand } from "./route-types.js"; import { type InterceptWhenFn } from "./server/context"; import { type PrerenderHandlerDefinition } from "./prerender.js"; import { type StaticHandlerDefinition } from "./static-handler.js"; import type { SearchSchema } from "./search-params.js"; /** * Symbol marking a route as a response route (non-RSC). * Stored on PathOptions and UrlPatterns to signal the trie to short-circuit. */ export declare const RESPONSE_TYPE: unique symbol; /** * Handler that must return Response (not ReactNode). * Used by path.image(), path.stream(), path.any() (binary/streaming data). */ export type ResponseHandler, TEnv = any> = (ctx: ResponseHandlerContext) => Response | Promise; /** * JSON-serializable value type for auto-wrap support. */ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue; }; /** * Handler for JSON response routes. * Can return a plain JSON-serializable value (auto-wrapped) or Response (pass-through). */ export type JsonResponseHandler, TEnv = any> = (ctx: ResponseHandlerContext) => JsonValue | Response | Promise; /** * Handler for text-based response routes (text, html, xml). * Can return a string (auto-wrapped) or Response (pass-through). */ export type TextResponseHandler, TEnv = any> = (ctx: ResponseHandlerContext) => string | Response | Promise; /** * Lighter handler context for response routes. * No ctx.use() (no loaders). Supports setting response headers and cookies * without constructing a full Response object. */ export interface ResponseHandlerContext, TEnv = any> { request: Request; params: TParams; /** @internal Phantom property for params type invariance. Prevents mounting handlers on wrong routes. */ readonly _paramCheck?: (params: TParams) => TParams; /** Platform bindings (DB, KV, secrets, etc.) extracted from RouterEnv. */ env: TEnv extends RouterEnv ? B : {}; /** Query parameters from the URL (system params like `_rsc*` are filtered). */ searchParams: URLSearchParams; /** The full URL object (with system params filtered). */ url: URL; /** The pathname portion of the request URL. */ pathname: string; reverse: (name: string, params?: Record) => string; /** Read a variable set by middleware via ctx.set(key, value). */ get: (key: string) => unknown; /** Set a response header. Merged into the auto-wrapped or pass-through Response. */ header: (name: string, value: string) => void; /** Set a cookie on the response. */ setCookie: (name: string, value: string, options?: CookieOptions) => void; } /** * Sentinel type for unnamed routes. * Using a branded string instead of `never` prevents TypeScript from * widening array type inference when mixing named and unnamed routes. */ export type UnnamedRoute = "$unnamed"; /** * Options for path() function */ export interface PathOptions { /** Route name for href() lookups */ name?: TName; /** Search param schema for typed query parameters */ search?: TSearch; /** Trailing slash behavior: "never" (redirect /path/ to /path), "always" (redirect /path to /path/), "ignore" (match both) */ trailingSlash?: TrailingSlashMode; /** Response type marker (set by path.json(), etc.) */ [RESPONSE_TYPE]?: string; } /** * Internal representation of a URL pattern definition */ export interface PathDefinition { pattern: string; name?: string; handler: ReactNode | Handler; use?: RouteUseItem[]; } /** * Result of urls() - contains the route definitions */ export interface UrlPatterns = Record, TResponses extends Record = Record> { /** Internal: route definitions */ readonly definitions: PathDefinition[]; /** Internal: compiled handler function */ readonly handler: () => AllUseItems[]; /** Internal: trailing slash config per route name */ readonly trailingSlash: Record; /** Brand for type checking */ readonly [UrlPatternsBrand]: void; /** Environment type brand (phantom) */ readonly _env?: TEnv; /** Routes type brand (phantom) - carries route name -> pattern mapping */ readonly _routes?: TRoutes; /** Responses type brand (phantom) - carries route name -> response data type mapping */ readonly _responses?: TResponses; } /** * Options for include() */ export interface IncludeOptions { /** Name prefix for all routes in this pattern set */ name?: TNamePrefix; } /** * 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, 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, 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) * D is the current depth level for nested layouts/caches */ type ExtractRoutesFromItem = [D] extends [never] ? {} : T extends TypedRouteItem ? TName extends string ? TName extends UnnamedRoute ? {} : {} extends TSearch ? { [K in TName]: TPattern; } : { [K in TName]: { readonly path: TPattern; readonly search: TSearch; }; } : {} : T extends TypedIncludeItem ? TNamePrefix extends string ? TUrlPrefix extends string ? PrefixRoutes, TNamePrefix> : PrefixRoutes : TUrlPrefix extends string ? PrefixPatterns : TRoutes : T extends TypedLayoutItem ? TChildRoutes : T extends TypedCacheItem ? TChildRoutes : {}; /** * 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. * D is passed to ExtractRoutesFromItem for nested depth tracking. */ 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). * Uses Simplify to force eager evaluation for interface extension compatibility. */ export type ExtractRoutes = ExtractRoutesFromItems; /** * 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, 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 = [D] extends [never] ? {} : T extends TypedRouteItem ? TName extends string ? TName extends UnnamedRoute ? {} : { [K in TName]: TData; } : {} : T extends TypedIncludeItem ? 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 : {} : {}; /** * 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; /** * Helpers provided by urls() */ /** * Base path function signature for defining routes with URL patterns. */ export type PathFn = = ExtractParams>(pattern: TPattern, handler: ReactNode | ((ctx: HandlerContext) => ReactNode | Promise | Response | Promise) | PrerenderHandlerDefinition | StaticHandlerDefinition, optionsOrUse?: PathOptions | (() => RouteUseItem[]), use?: () => RouteUseItem[]) => string extends keyof TParams ? TypedRouteItem : ExtractParams extends TParams ? TParams extends ExtractParams ? TypedRouteItem : { __error: `Handler params do not match pattern "${TPattern}"`; } : { __error: `Handler params do not match pattern "${TPattern}"`; }; /** * Path function for response routes that must return Response (image, stream, any). * Handler must return Response, not ReactNode. Uses lighter ResponseHandlerContext. * Use items restricted to middleware() and cache() only. */ export type ResponsePathFn = (pattern: TPattern, handler: ResponseHandler, TEnv>, optionsOrUse?: PathOptions | (() => ResponseRouteUseItem[]), use?: () => ResponseRouteUseItem[]) => TypedRouteItem; /** * Path function for JSON response routes (path.json()). * Handler can return plain JSON-serializable values or Response. * TData is inferred from the handler's return type (excluding Response/Promise wrappers). */ export type JsonResponsePathFn = (pattern: TPattern, handler: (ctx: ResponseHandlerContext, TEnv>) => TData | Response | Promise, optionsOrUse?: PathOptions | (() => ResponseRouteUseItem[]), use?: () => ResponseRouteUseItem[]) => TypedRouteItem; /** * Path function for text-based response routes (path.text(), path.html(), path.xml()). * Handler can return a string or Response. TData is always `string`. */ export type TextResponsePathFn = (pattern: TPattern, handler: TextResponseHandler, TEnv>, optionsOrUse?: PathOptions | (() => ResponseRouteUseItem[]), use?: () => ResponseRouteUseItem[]) => TypedRouteItem; /** * Base include function signature. */ export type IncludeFn = , const TUrlPrefix extends string, const TNamePrefix extends string = never, TResponses extends Record = Record>(prefix: TUrlPrefix, patterns: UrlPatterns, options?: IncludeOptions) => TypedIncludeItem; export type PathHelpers = { /** * Define a route with URL pattern at definition site * * @example * ```typescript * // Pattern and component only * path("/about", AboutPage) * * // With options * path("/:slug", PostPage, { name: "post" }) * * // With children (loaders, middleware, etc.) * path("/:slug", PostPage, { name: "post" }, () => [ * loader(PostLoader), * ]) * ``` */ path: PathFn & { json: JsonResponsePathFn; text: TextResponsePathFn; html: TextResponsePathFn; xml: TextResponsePathFn; md: TextResponsePathFn; image: ResponsePathFn; stream: ResponsePathFn; any: ResponsePathFn; }; /** * Define a layout that wraps child routes */ layout: { (component: ReactNode | Handler | StaticHandlerDefinition): TypedLayoutItem<{}, {}>; (component: ReactNode | Handler | StaticHandlerDefinition, use: () => TChildren): TypedLayoutItem, ExtractResponses>; }; /** * Include nested URL patterns with optional name prefix * * ```typescript * // Without name - routes keep local names * include("/blog", blogPatterns) * * // With name - routes are prefixed (e.g., "index" → "blog.index") * include("/blog", blogPatterns, { name: "blog" }) * ``` */ include: IncludeFn; /** * Define parallel routes that render simultaneously in named slots */ parallel: | ReactNode | StaticHandlerDefinition>>(slots: TSlots, use?: () => ParallelUseItem[]) => ParallelItem; /** * Define an intercepting route for soft navigation * Note: routeName must match a named path() in this urlpatterns */ intercept: (slotName: `@${string}`, routeName: string, handler: ReactNode | Handler, use?: () => InterceptUseItem[]) => InterceptItem; /** * Attach middleware to the current route/layout */ middleware: (...fns: MiddlewareFn[]) => MiddlewareItem; /** * Control when a segment should revalidate during navigation */ revalidate: (fn: ShouldRevalidateFn) => RevalidateItem; /** * Attach a data loader to the current route/layout */ loader: (loaderDef: LoaderDefinition, use?: () => LoaderUseItem[]) => LoaderItem; /** * Attach a loading component to the current route/layout */ loading: (component: ReactNode, options?: { ssr?: boolean; }) => LoadingItem; /** * Attach an error boundary to catch errors in this segment */ errorBoundary: (fallback: ReactNode | ErrorBoundaryHandler) => ErrorBoundaryItem; /** * Attach a not-found boundary to handle notFound() calls */ notFoundBoundary: (fallback: ReactNode | NotFoundBoundaryHandler) => NotFoundBoundaryItem; /** * Define a condition for when an intercept should activate */ when: (fn: InterceptWhenFn) => WhenItem; /** * Define cache configuration for segments */ cache: { (): TypedCacheItem<{}, {}>; (children: () => TChildren): TypedCacheItem, ExtractResponses>; (options: PartialCacheOptions | false): TypedCacheItem<{}, {}>; (options: PartialCacheOptions | false, use: () => TChildren): TypedCacheItem, ExtractResponses>; }; }; /** * Define URL patterns with Django-inspired syntax * * Replaces map() as the entry point for route definitions. * URL patterns are now visible at the definition site via path(). * * @example * ```typescript * export const blogPatterns = urls(({ path, layout, loader }) => [ * layout(BlogLayout, () => [ * path("/", BlogIndex, { name: "index" }), * path("/:slug", BlogPost, { name: "post" }, () => [ * loader(PostLoader), * ]), * ]), * ]); * ``` */ export declare function urls(builder: (helpers: PathHelpers) => TItems): UrlPatterns, ExtractResponses>; /** * Extract route names from a UrlPatterns result * Used for type-safe href() generation */ export type ExtractRouteNames> = T extends UrlPatterns ? string : never; /** * Extract params for a specific route name */ export type ExtractPathParams, K extends string> = ExtractParams; /** * 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; }; /** * 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 }> * ``` */ export type RouteResponse = TPatterns extends { readonly _responses?: infer R; } ? TName extends keyof R ? ResponseEnvelope> : never : never; export type { AllUseItems, IncludeItem, TypedRouteItem, TypedIncludeItem, TypedLayoutItem, TypedCacheItem, } from "./route-types.js"; //# sourceMappingURL=urls.d.ts.map