react-routes-forge 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ var C=()=>/[.*+?^${}()|[\]\\]/g,R=()=>/(^|\/):([A-Za-z0-9_]+)(\?)?/g;function A(){return globalThis.process?.env?.NODE_ENV==="production"}function O(e){A()||console.warn(e)}function Q(){m.clear(),P.clear()}function d(e){return e.replace(C(),"\\$&")}function w(e){let t=e.endsWith("/*"),r=t?e.slice(0,-2):e,s="",c=0;for(let o of r.matchAll(R())){let i=o.index??0,a=o[0];s+=d(r.slice(c,i));let[,n="",,u]=o;s+=u?`(?:${d(n)}([^/]+))?`:`${d(n)}([^/]+)`,c=i+(a?.length??0)}return s+d(r.slice(c))+(t?"(?:/(.*))?":"")}function N(e,t){return new RegExp(`:${d(t)}\\?(?![A-Za-z0-9_])`).test(e)}var m=new Map,B=/^/;function k(e){if(e==="/")return B;let t=m.get(e);if(t!==void 0)return t;let r=new RegExp(`^${w(e)}(?=/|$)`);return m.set(e,r),r}function _(e){try{return decodeURIComponent(e)}catch{return e}}function I(e,t,r){let s=e.indexOf("#"),c=s===-1?e:e.slice(0,s),o=s===-1?"":e.slice(s+1),i=new URLSearchParams;if(t)for(let[u,l]of Object.entries(t))l!=null&&(Array.isArray(l)?l.forEach(f=>{f!=null&&i.append(u,String(f))}):i.append(u,String(l)));let a=c,n=i.toString();return n&&(a+=(a.includes("?")?"&":"?")+n),r?a+="#"+r:o&&(a+="#"+o),a}function T(e,t){let r=e.indexOf("#"),s=r===-1?e:e.slice(0,r),c=s.indexOf("?");if(c===-1)return{};let o=new URLSearchParams(s.slice(c+1)),i={};for(let a of new Set(o.keys())){let u=o.getAll(a).map(l=>t?.coerceBooleans&&(l==="true"||l==="false")?l==="true":t?.coerceNumbers&&l.trim()!==""&&!isNaN(Number(l))?Number(l):l);i[a]=u.length>1?u:u[0]??""}return i}function x(e,t,r,s){let c=y(e),o=c.filter(a=>(t[a]===void 0||t[a]===null)&&!N(e,a)&&a!=="*"),i=c.reduce((a,n)=>{let u=t[n],l=u==null;if(n==="*"){if(l)return a.replace(/\/\*$/,"")||"/";let p=s?.encode===!1?String(u):String(u).split("/").map(g=>encodeURIComponent(g)).join("/");return a.replace(/\/\*$/,`/${p}`)}let f=new RegExp(`(^|/):${d(n)}\\??(?![A-Za-z0-9_])`,"g");return a.replace(f,(p,g)=>{if(l)return p.endsWith("?")?"":`${g}:${n}`;let h=s?.encode===!1?String(u):encodeURIComponent(String(u));return`${g}${h}`})},e);if(o.length>0){if(s?.strict)throw new RangeError(`[route-forge] Missing required param(s) ${o.map(a=>`":${a}"`).join(", ")} in template "${e}".`);O(`[route-forge] Unresolved params in path "${i}". Check that all :param segments have matching keys.`)}return I(i,r,s?.hash)}function y(e){let t=[...e.matchAll(R())].map(r=>r[2]);return e.endsWith("/*")&&t.push("*"),t}function $(e){return R().test(e)||e.endsWith("/*")}function j(e,t,r={exact:!0,caseSensitive:!1}){let s=(e.split("?")[0]??"").replace(/\/+$/,"")||"/",c=t.replace(/\/+$/,"")||"/",o=r.caseSensitive?c:c.toLowerCase(),i=r.caseSensitive?s:s.toLowerCase();return(r.exact?b(o):k(o)).test(i)}function E(e,t){let r=t.split("?")[0]??"",s=y(e),c=r.match(b(e));if(!c)return{};let o={};return s.forEach((i,a)=>{let n=c[a+1];n!==void 0&&(o[i]=_(n))}),o}function M(...e){return"/"+e.map(s=>s.replace(/^\/+/,"").replace(/\/+$/,"")).filter(Boolean).join("/")}function U(e,t,r,s){return x(e,t,r,s)}function L(e){return y(e)}var P=new Map;function b(e,t){let r=t?.end??!0,s=t?.caseSensitive?"":"i",c=`${e}:${r}:${s}`,o=P.get(c);if(o!==void 0)return o;let i=w(e),a=r?`^${i}$`:`^${i}(?=/|$)`,n=new RegExp(a,s);return P.set(c,n),n}function v(e,t=""){let r=[];for(let s of Object.keys(e)){let c=t?`${t}.${s}`:s,o=e[s];typeof o=="string"?r.push({key:c,path:o}):o instanceof String?r.push({key:c,path:o.valueOf()}):typeof o=="object"&&o!==null&&r.push(...v(o,c))}return r}function F(e){let t=e.split("."),r=t[t.length-1]??e;return(r==="ROOT"&&t.length>1?t[t.length-2]:r).replace(/_/g," ").toLowerCase().replace(/^\w/,c=>c.toUpperCase())}function W(e,t,r){let s=Array.isArray(e)?e:v(e),c=t.split("?")[0]??"",o=r?.labelResolver??F,i=r?.labels??{},a=[];for(let n of s){let u=b(n.path),l=c.match(u);if(l){let g=E(n.path,l[0]),h=$(n.path)?x(n.path,g):n.path;a.push({key:n.key,resolvedPath:h,template:n.path,isCurrent:!0});continue}let f=k(n.path),p=c.match(f);if(p){let g=p[0],h=E(n.path,g),S=$(n.path)?x(n.path,h):n.path;a.push({key:n.key,resolvedPath:S,template:n.path,isCurrent:!1})}}return a.sort((n,u)=>n.template.length-u.template.length),a.map(n=>({key:n.key,label:i[n.key]??o(n.key),path:n.resolvedPath,isCurrent:n.isCurrent}))}export{O as a,Q as b,I as c,T as d,x as e,y as f,$ as g,j as h,E as i,M as j,U as k,L as l,b as m,v as n,W as o};
2
+ //# sourceMappingURL=chunk-EFHUAXKE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/utils.ts"],"sourcesContent":["import type {\n BreadcrumbItem,\n BreadcrumbOptions,\n BuildPathOptions,\n FlatRoute,\n QueryParams,\n RouteParams,\n} from \"../types\";\n\n/** Returns a fresh RegExp each call — avoids shared `lastIndex` state on /g patterns. */\nconst ESCAPE_RE = () => /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Matches a `:param` token that starts a URL segment.\n *\n * A param is only recognized when its `:` sits at the very start of the\n * template or is immediately preceded by `/`, so a literal colon inside a\n * segment (e.g. `/users/foo:bar`) is not treated as a param. Param names are\n * restricted to `[A-Za-z0-9_]` (matching React Router), so any static suffix\n * (e.g. `.json` in `/files/:name.json`) stays a literal. The optional `?`\n * marker (`:param?`) is captured separately so it can be handled as a\n * whole-segment modifier.\n */\nconst PARAM_SEGMENT_RE = () => /(^|\\/):([A-Za-z0-9_]+)(\\?)?/g;\n\n/** Returns `true` when running in a production bundle (suppresses dev warnings). */\nfunction isProduction(): boolean {\n const runtimeProcess = (\n globalThis as typeof globalThis & {\n process?: { env?: Record<string, string | undefined> };\n }\n ).process;\n return runtimeProcess?.env?.NODE_ENV === \"production\";\n}\n\n/**\n * Emits a `console.warn` in non-production environments.\n * Shared by the core utilities and `defineRoutes()` so the production check\n * lives in one place.\n */\nexport function devWarn(message: string): void {\n if (!isProduction()) {\n console.warn(message);\n }\n}\n\n/**\n * Clears internal regex cache maps (PREFIX_CACHE and PATH_CACHE).\n * Useful in test suites to prevent cached patterns from leaking across test cases.\n */\nexport function clearPathCache(): void {\n PREFIX_CACHE.clear();\n PATH_CACHE.clear();\n}\n\nfunction escapeRegex(value: string): string {\n return value.replace(ESCAPE_RE(), \"\\\\$&\");\n}\n\n/**\n * Convert a route template into an unanchored `RegExp` source string.\n *\n * - Required `:name` segments become a capturing group `([^/]+)`.\n * - Optional `:name?` segments become `(?:([^/]+))?` — the whole segment is\n * optional, matching React Router semantics.\n * - A trailing splat (`/*`) becomes `(?:/(.*))?`, capturing the remainder of\n * the path (including slashes) or nothing at all.\n * - Everything else is regex-escaped literally.\n */\nfunction createTemplatePattern(template: string): string {\n const splat = template.endsWith(\"/*\");\n const base = splat ? template.slice(0, -2) : template;\n let pattern = \"\";\n let cursor = 0;\n\n for (const match of base.matchAll(PARAM_SEGMENT_RE())) {\n const start = match.index ?? 0;\n const token = match[0];\n\n pattern += escapeRegex(base.slice(cursor, start));\n\n const [, boundary = \"\", , optional] = match;\n pattern += optional\n ? `(?:${escapeRegex(boundary)}([^/]+))?`\n : `${escapeRegex(boundary)}([^/]+)`;\n\n cursor = start + (token?.length ?? 0);\n }\n\n return pattern + escapeRegex(base.slice(cursor)) + (splat ? \"(?:/(.*))?\" : \"\");\n}\n\n/** Returns `true` when `name` is an optional (`:name?`) param in `template`. */\nfunction isOptionalParam(template: string, name: string): boolean {\n return new RegExp(`:${escapeRegex(name)}\\\\?(?![A-Za-z0-9_])`).test(template);\n}\n\n/**\n * Returns a `RegExp` that matches `template` as a path prefix, honoring\n * segment boundaries (`/users` matches `/users/42` but not `/usersettings`).\n *\n * The root template `/` is a prefix of every path.\n *\n * Compiled patterns are cached per template: the resulting `RegExp` has no\n * `/g` flag, so repeated `.test()`/`.exec()` calls are side-effect free and\n * sharing instances across callers is safe.\n */\nconst PREFIX_CACHE = new Map<string, RegExp>();\nconst ROOT_PREFIX_RE = /^/;\n\nfunction matchPrefix(template: string): RegExp {\n if (template === \"/\") return ROOT_PREFIX_RE;\n const cached = PREFIX_CACHE.get(template);\n if (cached !== undefined) return cached;\n const re = new RegExp(`^${createTemplatePattern(template)}(?=/|$)`);\n PREFIX_CACHE.set(template, re);\n return re;\n}\n\n/** Decode a URL-encoded param value, falling back to the raw value on error. */\nfunction safeDecode(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/**\n * Append a query string and/or hash fragment to a path that may already\n * contain a query or hash.\n *\n * - Existing query pairs are preserved; new ones are joined with `&`.\n * - The query string is always inserted before any hash fragment, so an\n * existing `#section` on `path` is kept unless a new `hash` is given.\n *\n * @example\n * ```ts\n * appendQuery(\"/users?tab=list\", { page: 2 }); // → \"/users?tab=list&page=2\"\n * appendQuery(\"/users#top\", { tab: \"list\" }); // → \"/users?tab=list#top\"\n * ```\n */\nexport function appendQuery(path: string, query?: QueryParams, hash?: string): string {\n const hashIdx = path.indexOf(\"#\");\n const base = hashIdx === -1 ? path : path.slice(0, hashIdx);\n const existingHash = hashIdx === -1 ? \"\" : path.slice(hashIdx + 1);\n\n const searchParams = new URLSearchParams();\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n value.forEach((v) => {\n if (v !== undefined && v !== null) searchParams.append(key, String(v));\n });\n } else {\n searchParams.append(key, String(value));\n }\n }\n }\n\n let result = base;\n const queryString = searchParams.toString();\n if (queryString) {\n result += (result.includes(\"?\") ? \"&\" : \"?\") + queryString;\n }\n\n if (hash) {\n result += \"#\" + hash;\n } else if (existingHash) {\n result += \"#\" + existingHash;\n }\n\n return result;\n}\n\n/**\n * Parse the query string out of a path (or bare query string) into a plain\n * object. Repeated keys become arrays; a single key is a scalar string.\n *\n * With `{ coerceBooleans: true }`, the strings `\"true\"`/`\"false\"` are\n * converted to actual booleans.\n *\n * @example\n * ```ts\n * extractQueryFromPath(\"/users/42?tab=profile&tag=a&tag=b\");\n * // → { tab: \"profile\", tag: [\"a\", \"b\"] }\n * extractQueryFromPath(\"/search?active=true\", { coerceBooleans: true });\n * // → { active: true }\n * ```\n */\nexport function extractQueryFromPath(\n path: string,\n options?: { coerceBooleans?: boolean; coerceNumbers?: boolean },\n): QueryParams {\n const hashIdx = path.indexOf(\"#\");\n const noHash = hashIdx === -1 ? path : path.slice(0, hashIdx);\n const queryIdx = noHash.indexOf(\"?\");\n if (queryIdx === -1) return {};\n\n const params = new URLSearchParams(noHash.slice(queryIdx + 1));\n const result: QueryParams = {};\n\n for (const key of new Set(params.keys())) {\n const values = params.getAll(key);\n const parsed = values.map((v) => {\n if (options?.coerceBooleans && (v === \"true\" || v === \"false\")) {\n return v === \"true\";\n }\n if (options?.coerceNumbers && v.trim() !== \"\" && !isNaN(Number(v))) {\n return Number(v);\n }\n return v;\n });\n result[key] = parsed.length > 1 ? parsed : (parsed[0] ?? \"\");\n }\n\n return result;\n}\n\nexport function buildPath(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n const paramNames = extractParamNames(template);\n const unresolved = paramNames.filter(\n (name) =>\n (params[name] === undefined || params[name] === null) &&\n !isOptionalParam(template, name) &&\n // A missing splat simply drops the `/*` suffix (matching React Router,\n // where `/files/*` also matches `/files`).\n name !== \"*\",\n );\n\n const resolved = paramNames.reduce((path, name) => {\n const value = params[name];\n const missing = value === undefined || value === null;\n\n if (name === \"*\") {\n if (missing) return path.replace(/\\/\\*$/, \"\") || \"/\";\n // Splat values are path-like: preserve `/` separators but still\n // encode characters that could break the URL (`?`, `#`, spaces, …).\n const encoded =\n options?.encode === false\n ? String(value)\n : String(value)\n .split(\"/\")\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n return path.replace(/\\/\\*$/, `/${encoded}`);\n }\n\n const re = new RegExp(\n `(^|/):${escapeRegex(name)}\\\\??(?![A-Za-z0-9_])`,\n \"g\",\n );\n\n return path.replace(re, (match, boundary) => {\n if (missing) {\n // Optional segment: drop the whole `/segment`. Required: keep the\n // `:name` placeholder so strict mode can report it.\n return match.endsWith(\"?\") ? \"\" : `${boundary}:${name}`;\n }\n\n const encoded =\n options?.encode === false\n ? String(value)\n : encodeURIComponent(String(value));\n return `${boundary}${encoded}`;\n });\n }, template);\n\n if (unresolved.length > 0) {\n if (options?.strict) {\n throw new RangeError(\n `[route-forge] Missing required param(s) ${unresolved.map((p) => `\":${p}\"`).join(\", \")} in template \"${template}\".`,\n );\n }\n\n devWarn(\n `[route-forge] Unresolved params in path \"${resolved}\". ` +\n `Check that all :param segments have matching keys.`,\n );\n }\n\n return appendQuery(resolved, query, options?.hash);\n}\n\nexport function extractParamNames(template: string): string[] {\n const names = [...template.matchAll(PARAM_SEGMENT_RE())].map(\n (match) => match[2] as string,\n );\n if (template.endsWith(\"/*\")) names.push(\"*\");\n return names;\n}\n\nexport function isDynamic(path: string): boolean {\n return PARAM_SEGMENT_RE().test(path) || path.endsWith(\"/*\");\n}\n\n/**\n * Test whether `currentPath` matches `template`, mirroring React Router's\n * `NavLink` matching semantics:\n *\n * - Case-insensitive by default (pass `caseSensitive: true` to opt out).\n * - Trailing slashes are tolerated (`/users/` matches `/users`).\n * - `exact: true` (the default) requires a full match; `exact: false`\n * matches any path that starts with the template.\n */\nexport function isActivePath(\n currentPath: string,\n template: string,\n options: { exact?: boolean; caseSensitive?: boolean } = {\n exact: true,\n caseSensitive: false,\n },\n): boolean {\n const pathname = (currentPath.split(\"?\")[0] ?? \"\").replace(/\\/+$/, \"\") || \"/\";\n const normalizedTemplate = template.replace(/\\/+$/, \"\") || \"/\";\n const target = options.caseSensitive\n ? normalizedTemplate\n : normalizedTemplate.toLowerCase();\n const candidate = options.caseSensitive ? pathname : pathname.toLowerCase();\n const regex = options.exact ? matchPath(target) : matchPrefix(target);\n\n return regex.test(candidate);\n}\n\nexport function extractParamsFromPath(\n template: string,\n resolvedPath: string,\n): Record<string, string> {\n const pathWithoutSearch = resolvedPath.split(\"?\")[0] ?? \"\";\n const paramNames = extractParamNames(template);\n const match = pathWithoutSearch.match(matchPath(template));\n\n if (!match) return {};\n\n const result: Record<string, string> = {};\n paramNames.forEach((name, index) => {\n const raw = match[index + 1];\n if (raw !== undefined) result[name] = safeDecode(raw);\n });\n return result;\n}\n\nexport function joinPaths(...segments: string[]): string {\n const processed = segments.map((segment) =>\n segment.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\"),\n );\n const filtered = processed.filter(Boolean);\n return \"/\" + filtered.join(\"/\");\n}\n\nexport function build(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n return buildPath(template, params, query, options);\n}\n\nexport function getParamNames(template: string): string[] {\n return extractParamNames(template);\n}\n\n/**\n * Convert a route template string into an anchored `RegExp` for matching paths.\n *\n * Each `:param` segment becomes a capturing group so the returned regex\n * can be used with `.test()` or `.exec()`.\n *\n * Query strings are **not** stripped — callers should split on `\"?\"` first\n * (see {@link isActivePath} or {@link extractParamsFromPath} for higher-level\n * helpers that handle this automatically).\n *\n * @param template - A route template, e.g. `\"/users/:id\"` or `\"/users/:id/posts/:postId\"`.\n * @returns A `RegExp` anchored with `^` and `$` that captures param values.\n *\n * @example\n * ```ts\n * const re = matchPath(\"/users/:id\");\n * re.test(\"/users/42\"); // true\n * re.exec(\"/users/42\"); // [\"/users/42\", \"42\"]\n * re.test(\"/users/42/posts\"); // false (exact match)\n * re.test(\"/users/42?page=1\"); // true (query is part of captured value)\n * ```\n *\n * Compiled patterns are cached per template (the regex has no `/g` flag, so\n * sharing instances across callers is safe).\n */\nconst PATH_CACHE = new Map<string, RegExp>();\n\nexport function matchPath(\n template: string,\n options?: { end?: boolean; caseSensitive?: boolean },\n): RegExp {\n const end = options?.end ?? true;\n const flags = options?.caseSensitive ? \"\" : \"i\";\n const cacheKey = `${template}:${end}:${flags}`;\n\n const cached = PATH_CACHE.get(cacheKey);\n if (cached !== undefined) return cached;\n\n const basePattern = createTemplatePattern(template);\n const pattern = end ? `^${basePattern}$` : `^${basePattern}(?=/|$)`;\n const re = new RegExp(pattern, flags);\n PATH_CACHE.set(cacheKey, re);\n return re;\n}\n\n/**\n * Walk a `defineRoutes` output tree and return a flat array of\n * `{ key, path }` entries where `key` is the dot-joined key path from\n * the root (e.g. `\"SERVICES.BCC.EDIT\"`) and `path` is the raw template\n * string (e.g. `\"/services/bcc/edit/:id\"`).\n *\n * Useful for:\n * - Generating sitemaps from a single source of truth.\n * - Detecting duplicate path strings across branches at startup:\n *\n * @example\n * const flat = flattenRoutes(PATHS);\n * const paths = flat.map((r) => r.path);\n * const dupes = paths.filter((p, i) => paths.indexOf(p) !== i);\n * if (dupes.length) console.warn('Duplicate paths:', dupes);\n */\nexport function flattenRoutes(\n routes: Record<string, unknown>,\n prefix = \"\",\n): FlatRoute[] {\n const entries: FlatRoute[] = [];\n\n for (const key of Object.keys(routes)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n const value = routes[key];\n\n if (typeof value === \"string\") {\n // Plain static string leaf.\n entries.push({ key: fullKey, path: value });\n } else if (value instanceof String) {\n // String-object leaf (wrapped dynamic path from defineRoutes).\n entries.push({ key: fullKey, path: value.valueOf() });\n } else if (typeof value === \"object\" && value !== null) {\n // Nested route group — recurse.\n entries.push(\n ...flattenRoutes(value as Record<string, unknown>, fullKey),\n );\n }\n // Anything else (functions, numbers, …) is silently skipped.\n }\n\n return entries;\n}\n\nfunction deriveBreadcrumbLabel(key: string): string {\n const parts = key.split(\".\");\n const last = parts[parts.length - 1] ?? key;\n // Use the parent segment when the leaf is the conventional \"ROOT\" key,\n // so USERS.ROOT → \"Users\" rather than \"Root\".\n const raw = last === \"ROOT\" && parts.length > 1 ? parts[parts.length - 2]! : last;\n return raw\n .replace(/_/g, \" \")\n .toLowerCase()\n .replace(/^\\w/, (c) => c.toUpperCase());\n}\n\n/**\n * Build a breadcrumb trail from a route tree or a flat route list.\n *\n * For a given `currentPath`, it walks the route tree and returns every route\n * that is an ancestor of (or an exact match to) the current page. Ancestors\n * are matched by prefix (e.g. `/users` matches `/users/edit/42/posts`).\n *\n * Dynamic params in ancestor paths are automatically resolved from the\n * matched portion of the URL.\n *\n * @param routes - A route tree (output of `defineRoutes`) or a pre-flattened\n * array from `flattenRoutes()`.\n * @param currentPath - The current URL (with or without query string).\n * @param options - Optional label resolver.\n * @returns An array of {@link BreadcrumbItem} ordered by depth\n * (most general first), where the last item is the current page.\n *\n * @example\n * ```ts\n * const PATHS = defineRoutes({\n * HOME: \"/\",\n * USERS: { ROOT: \"/users\", EDIT: \"/users/edit/:id\" },\n * } as const);\n *\n * getBreadcrumbs(PATHS, \"/users/edit/42\");\n * // → [\n * // { key: \"HOME\", label: \"Home\", path: \"/\", isCurrent: false },\n * // { key: \"USERS.ROOT\", label: \"Users\", path: \"/users\", isCurrent: false },\n * // { key: \"USERS.EDIT\", label: \"Edit\", path: \"/users/edit/42\", isCurrent: true },\n * // ]\n * ```\n */\nexport function getBreadcrumbs(\n routes: Record<string, unknown> | FlatRoute[],\n currentPath: string,\n options?: BreadcrumbOptions,\n): BreadcrumbItem[] {\n const flat = Array.isArray(routes)\n ? routes\n : flattenRoutes(routes);\n const pathname = currentPath.split(\"?\")[0] ?? \"\";\n const labelFn = options?.labelResolver ?? deriveBreadcrumbLabel;\n const labels = options?.labels ?? {};\n\n const items: Array<{\n key: string;\n resolvedPath: string;\n template: string;\n isCurrent: boolean;\n }> = [];\n\n for (const route of flat) {\n const exactRe = matchPath(route.path);\n const exactMatch = pathname.match(exactRe);\n\n if (exactMatch) {\n const params = extractParamsFromPath(route.path, exactMatch[0]);\n const resolved = isDynamic(route.path)\n ? buildPath(route.path, params)\n : route.path;\n items.push({\n key: route.key,\n resolvedPath: resolved,\n template: route.path,\n isCurrent: true,\n });\n continue;\n }\n\n const prefixRe = matchPrefix(route.path);\n const prefixMatch = pathname.match(prefixRe);\n\n if (prefixMatch) {\n const matchedPortion = prefixMatch[0];\n const params = extractParamsFromPath(route.path, matchedPortion);\n const resolved = isDynamic(route.path)\n ? buildPath(route.path, params)\n : route.path;\n items.push({\n key: route.key,\n resolvedPath: resolved,\n template: route.path,\n isCurrent: false,\n });\n }\n }\n\n items.sort((a, b) => a.template.length - b.template.length);\n\n return items.map((item) => ({\n key: item.key,\n label: labels[item.key] ?? labelFn(item.key),\n path: item.resolvedPath,\n isCurrent: item.isCurrent,\n }));\n}\n\n"],"mappings":"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","names":["ESCAPE_RE","PARAM_SEGMENT_RE","isProduction","devWarn","message","clearPathCache","PREFIX_CACHE","PATH_CACHE","escapeRegex","value","createTemplatePattern","template","splat","base","pattern","cursor","match","start","token","boundary","optional","isOptionalParam","name","ROOT_PREFIX_RE","matchPrefix","cached","re","safeDecode","appendQuery","path","query","hash","hashIdx","existingHash","searchParams","key","v","result","queryString","extractQueryFromPath","options","noHash","queryIdx","params","parsed","buildPath","paramNames","extractParamNames","unresolved","resolved","missing","encoded","segment","p","names","isDynamic","isActivePath","currentPath","pathname","normalizedTemplate","target","candidate","matchPath","extractParamsFromPath","resolvedPath","pathWithoutSearch","index","raw","joinPaths","segments","build","getParamNames","end","flags","cacheKey","basePattern","flattenRoutes","routes","prefix","entries","fullKey","deriveBreadcrumbLabel","parts","last","getBreadcrumbs","flat","labelFn","labels","items","route","exactRe","exactMatch","prefixRe","prefixMatch","matchedPortion","a","b","item"]}
@@ -0,0 +1,2 @@
1
+ "use strict";var m=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var O=Object.prototype.hasOwnProperty;var C=(e,t)=>{for(var n in t)m(e,n,{get:t[n],enumerable:!0})},N=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of A(t))!O.call(e,a)&&a!==n&&m(e,a,{get:()=>t[a],enumerable:!(r=k(t,a))||r.enumerable});return e};var B=e=>N(m({},"__esModule",{value:!0}),e);var D={};C(D,{useActivePath:()=>z,useNavigateTo:()=>W,useResolvedPath:()=>H,useRouteParams:()=>M,useTypedSearchParams:()=>q});module.exports=B(D);var x=require("react"),p=require("react-router-dom");var Q=()=>/[.*+?^${}()|[\]\\]/g,b=()=>/(^|\/):([A-Za-z0-9_]+)(\?)?/g;function T(){return globalThis.process?.env?.NODE_ENV==="production"}function _(e){T()||console.warn(e)}function g(e){return e.replace(Q(),"\\$&")}function $(e){let t=e.endsWith("/*"),n=t?e.slice(0,-2):e,r="",a=0;for(let o of n.matchAll(b())){let c=o.index??0,s=o[0];r+=g(n.slice(a,c));let[,i="",,u]=o;r+=u?`(?:${g(i)}([^/]+))?`:`${g(i)}([^/]+)`,a=c+(s?.length??0)}return r+g(n.slice(a))+(t?"(?:/(.*))?":"")}function F(e,t){return new RegExp(`:${g(t)}\\?(?![A-Za-z0-9_])`).test(e)}var y=new Map,I=/^/;function L(e){if(e==="/")return I;let t=y.get(e);if(t!==void 0)return t;let n=new RegExp(`^${$(e)}(?=/|$)`);return y.set(e,n),n}function P(e,t,n){let r=e.indexOf("#"),a=r===-1?e:e.slice(0,r),o=r===-1?"":e.slice(r+1),c=new URLSearchParams;if(t)for(let[u,l]of Object.entries(t))l!=null&&(Array.isArray(l)?l.forEach(d=>{d!=null&&c.append(u,String(d))}):c.append(u,String(l)));let s=a,i=c.toString();return i&&(s+=(s.includes("?")?"&":"?")+i),n?s+="#"+n:o&&(s+="#"+o),s}function v(e,t){let n=e.indexOf("#"),r=n===-1?e:e.slice(0,n),a=r.indexOf("?");if(a===-1)return{};let o=new URLSearchParams(r.slice(a+1)),c={};for(let s of new Set(o.keys())){let u=o.getAll(s).map(l=>t?.coerceBooleans&&(l==="true"||l==="false")?l==="true":t?.coerceNumbers&&l.trim()!==""&&!isNaN(Number(l))?Number(l):l);c[s]=u.length>1?u:u[0]??""}return c}function S(e,t,n,r){let a=U(e),o=a.filter(s=>(t[s]===void 0||t[s]===null)&&!F(e,s)&&s!=="*"),c=a.reduce((s,i)=>{let u=t[i],l=u==null;if(i==="*"){if(l)return s.replace(/\/\*$/,"")||"/";let h=r?.encode===!1?String(u):String(u).split("/").map(f=>encodeURIComponent(f)).join("/");return s.replace(/\/\*$/,`/${h}`)}let d=new RegExp(`(^|/):${g(i)}\\??(?![A-Za-z0-9_])`,"g");return s.replace(d,(h,f)=>{if(l)return h.endsWith("?")?"":`${f}:${i}`;let w=r?.encode===!1?String(u):encodeURIComponent(String(u));return`${f}${w}`})},e);if(o.length>0){if(r?.strict)throw new RangeError(`[route-forge] Missing required param(s) ${o.map(s=>`":${s}"`).join(", ")} in template "${e}".`);_(`[route-forge] Unresolved params in path "${c}". Check that all :param segments have matching keys.`)}return P(c,n,r?.hash)}function U(e){let t=[...e.matchAll(b())].map(n=>n[2]);return e.endsWith("/*")&&t.push("*"),t}function E(e,t,n={exact:!0,caseSensitive:!1}){let r=(e.split("?")[0]??"").replace(/\/+$/,"")||"/",a=t.replace(/\/+$/,"")||"/",o=n.caseSensitive?a:a.toLowerCase(),c=n.caseSensitive?r:r.toLowerCase();return(n.exact?j(o):L(o)).test(c)}var R=new Map;function j(e,t){let n=t?.end??!0,r=t?.caseSensitive?"":"i",a=`${e}:${n}:${r}`,o=R.get(a);if(o!==void 0)return o;let c=$(e),s=n?`^${c}$`:`^${c}(?=/|$)`,i=new RegExp(s,r);return R.set(a,i),i}function M(e){return(0,p.useParams)()}function W(){let e=(0,p.useNavigate)();return(0,x.useCallback)((t,n)=>{e(String(t),n)},[e])}function H(e,t,n,r){return S(e,t,n,r)}function z(e,t={exact:!0,caseSensitive:!1}){let n=(0,p.useLocation)();return E(n.pathname,e,t)}function q(e){let[t,n]=(0,p.useSearchParams)(),r=(0,p.useLocation)(),a=v(`${r.pathname}?${t.toString()}`,e),o=(0,x.useCallback)((c,s)=>{let i=P("",c),u=new URLSearchParams(i.startsWith("?")?i.slice(1):i);n(u,s)},[n]);return[a,o]}0&&(module.exports={useActivePath,useNavigateTo,useResolvedPath,useRouteParams,useTypedSearchParams});
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/index.ts","../../src/core/utils.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useParams, useNavigate, useLocation, useSearchParams } from \"react-router-dom\";\nimport type {\n ExtractParams,\n QueryParams,\n RouteParams,\n BuildPathOptions,\n} from \"../types\";\nimport { buildPath, isActivePath, extractQueryFromPath, appendQuery } from \"../core/utils\";\n\n// ─── useRouteParams ──────────────────────────────────────────────────────────\n\n/**\n * A typed wrapper around React Router's `useParams`.\n *\n * Pass a route template as a type parameter, or a dynamic route value from a\n * `defineRoutes()` tree for automatic type inference:\n *\n * @example\n * ```tsx\n * // Route: '/a/:x/b/:y/c/:z'\n * const { x, y, z } = useRouteParams<'/a/:x/b/:y/c/:z'>();\n *\n * // Or pass a route from your PATHS tree — params are inferred from it:\n * const PATHS = defineRoutes({ USERS: { EDIT: '/users/edit/:id' } } as const);\n * const { id } = useRouteParams(PATHS.USERS.EDIT);\n * ```\n */\n// Overload 1: no-arg generic — caller provides the template literal as T\nexport function useRouteParams<T extends string = string>(): Record<\n ExtractParams<T>,\n string\n>;\n// Overload 2: pass a route from defineRoutes() — P is inferred from paramNames array element type\nexport function useRouteParams<P extends string>(\n route: { readonly paramNames: ReadonlyArray<P> | Array<P> },\n): Record<P, string>;\n// Implementation\nexport function useRouteParams<P extends string>(\n _route?: { readonly paramNames: ReadonlyArray<P> | Array<P> },\n): Record<string, string> {\n return useParams() as Record<string, string>;\n}\n\n// ─── useNavigateTo ──────────────────────────────────────────────────────────\n\nexport type NavigateOptions = {\n replace?: boolean;\n state?: unknown;\n};\n\n/**\n * A typed `navigate` helper that accepts a resolved path (output of `.build()`)\n * or a route value straight from `defineRoutes()` (e.g. a `String` object or a\n * primitive string), with optional navigation options.\n *\n * `String` objects (used by route values so they can carry `.build()`) are\n * coerced to primitives, since React Router's `navigate()` ignores them.\n *\n * @example\n * ```tsx\n * const navigateTo = useNavigateTo();\n * navigateTo(PATHS.USERS.EDIT.build({ id: 42 }));\n * navigateTo(PATHS.HOME, { replace: true });\n * ```\n */\nexport function useNavigateTo() {\n const navigate = useNavigate();\n\n return useCallback(\n (path: string, options?: NavigateOptions) => {\n navigate(String(path), options);\n },\n [navigate],\n );\n}\n\n// ─── useResolvedPath ─────────────────────────────────────────────────────────\n\n/**\n * Resolves a dynamic path template against params, mirroring `buildPath()`.\n *\n * Accepts the same `options` bag as `build()` / `buildPath()`:\n * - (default) soft-fail: `console.warn` and return the partial path with unresolved `:param` placeholders.\n * - `{ strict: true }`: throw a `RangeError` on missing params — matching `.build()`'s strict behaviour.\n *\n * @example\n * ```tsx\n * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'\n * const path = useResolvedPath('/users/:id', {}, undefined, { strict: true }); // throws RangeError\n * const path = useResolvedPath('/files/*', { \"*\": \"a/b\" }); // → '/files/a/b'\n * ```\n */\nexport function useResolvedPath(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n return buildPath(template, params, query, options);\n}\n\n// ─── useActivePath ───────────────────────────────────────────────────────────\n\n/**\n * A hook that checks whether the current location matches a route template or path.\n * Thin wrapper around `isActivePath(useLocation().pathname, template, options)`.\n *\n * @example\n * ```tsx\n * const isActive = useActivePath(PATHS.USERS.ROOT, { exact: false });\n * ```\n */\nexport function useActivePath(\n template: string,\n options: { exact?: boolean; caseSensitive?: boolean } = {\n exact: true,\n caseSensitive: false,\n },\n): boolean {\n const location = useLocation();\n return isActivePath(location.pathname, template, options);\n}\n\n// ─── useTypedSearchParams ────────────────────────────────────────────────────\n\n/**\n * A typed wrapper around React Router's `useSearchParams`.\n * Returns parsed query params object and a setter that updates query params.\n *\n * @example\n * ```tsx\n * const [query, setQuery] = useTypedSearchParams({ coerceBooleans: true, coerceNumbers: true });\n * setQuery({ tab: 'details', page: 2 });\n * ```\n */\nexport function useTypedSearchParams(options?: {\n coerceBooleans?: boolean;\n coerceNumbers?: boolean;\n}) {\n const [searchParams, setSearchParams] = useSearchParams();\n const location = useLocation();\n\n const queryParams = extractQueryFromPath(\n `${location.pathname}?${searchParams.toString()}`,\n options,\n );\n\n const setTypedQuery = useCallback(\n (\n newQuery: QueryParams,\n navigateOptions?: { replace?: boolean; state?: unknown },\n ) => {\n const updatedPath = appendQuery(\"\", newQuery);\n const updatedSearchParams = new URLSearchParams(\n updatedPath.startsWith(\"?\") ? updatedPath.slice(1) : updatedPath,\n );\n setSearchParams(updatedSearchParams, navigateOptions);\n },\n [setSearchParams],\n );\n\n return [queryParams, setTypedQuery] as const;\n}\n\n","import type {\n BreadcrumbItem,\n BreadcrumbOptions,\n BuildPathOptions,\n FlatRoute,\n QueryParams,\n RouteParams,\n} from \"../types\";\n\n/** Returns a fresh RegExp each call — avoids shared `lastIndex` state on /g patterns. */\nconst ESCAPE_RE = () => /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Matches a `:param` token that starts a URL segment.\n *\n * A param is only recognized when its `:` sits at the very start of the\n * template or is immediately preceded by `/`, so a literal colon inside a\n * segment (e.g. `/users/foo:bar`) is not treated as a param. Param names are\n * restricted to `[A-Za-z0-9_]` (matching React Router), so any static suffix\n * (e.g. `.json` in `/files/:name.json`) stays a literal. The optional `?`\n * marker (`:param?`) is captured separately so it can be handled as a\n * whole-segment modifier.\n */\nconst PARAM_SEGMENT_RE = () => /(^|\\/):([A-Za-z0-9_]+)(\\?)?/g;\n\n/** Returns `true` when running in a production bundle (suppresses dev warnings). */\nfunction isProduction(): boolean {\n const runtimeProcess = (\n globalThis as typeof globalThis & {\n process?: { env?: Record<string, string | undefined> };\n }\n ).process;\n return runtimeProcess?.env?.NODE_ENV === \"production\";\n}\n\n/**\n * Emits a `console.warn` in non-production environments.\n * Shared by the core utilities and `defineRoutes()` so the production check\n * lives in one place.\n */\nexport function devWarn(message: string): void {\n if (!isProduction()) {\n console.warn(message);\n }\n}\n\n/**\n * Clears internal regex cache maps (PREFIX_CACHE and PATH_CACHE).\n * Useful in test suites to prevent cached patterns from leaking across test cases.\n */\nexport function clearPathCache(): void {\n PREFIX_CACHE.clear();\n PATH_CACHE.clear();\n}\n\nfunction escapeRegex(value: string): string {\n return value.replace(ESCAPE_RE(), \"\\\\$&\");\n}\n\n/**\n * Convert a route template into an unanchored `RegExp` source string.\n *\n * - Required `:name` segments become a capturing group `([^/]+)`.\n * - Optional `:name?` segments become `(?:([^/]+))?` — the whole segment is\n * optional, matching React Router semantics.\n * - A trailing splat (`/*`) becomes `(?:/(.*))?`, capturing the remainder of\n * the path (including slashes) or nothing at all.\n * - Everything else is regex-escaped literally.\n */\nfunction createTemplatePattern(template: string): string {\n const splat = template.endsWith(\"/*\");\n const base = splat ? template.slice(0, -2) : template;\n let pattern = \"\";\n let cursor = 0;\n\n for (const match of base.matchAll(PARAM_SEGMENT_RE())) {\n const start = match.index ?? 0;\n const token = match[0];\n\n pattern += escapeRegex(base.slice(cursor, start));\n\n const [, boundary = \"\", , optional] = match;\n pattern += optional\n ? `(?:${escapeRegex(boundary)}([^/]+))?`\n : `${escapeRegex(boundary)}([^/]+)`;\n\n cursor = start + (token?.length ?? 0);\n }\n\n return pattern + escapeRegex(base.slice(cursor)) + (splat ? \"(?:/(.*))?\" : \"\");\n}\n\n/** Returns `true` when `name` is an optional (`:name?`) param in `template`. */\nfunction isOptionalParam(template: string, name: string): boolean {\n return new RegExp(`:${escapeRegex(name)}\\\\?(?![A-Za-z0-9_])`).test(template);\n}\n\n/**\n * Returns a `RegExp` that matches `template` as a path prefix, honoring\n * segment boundaries (`/users` matches `/users/42` but not `/usersettings`).\n *\n * The root template `/` is a prefix of every path.\n *\n * Compiled patterns are cached per template: the resulting `RegExp` has no\n * `/g` flag, so repeated `.test()`/`.exec()` calls are side-effect free and\n * sharing instances across callers is safe.\n */\nconst PREFIX_CACHE = new Map<string, RegExp>();\nconst ROOT_PREFIX_RE = /^/;\n\nfunction matchPrefix(template: string): RegExp {\n if (template === \"/\") return ROOT_PREFIX_RE;\n const cached = PREFIX_CACHE.get(template);\n if (cached !== undefined) return cached;\n const re = new RegExp(`^${createTemplatePattern(template)}(?=/|$)`);\n PREFIX_CACHE.set(template, re);\n return re;\n}\n\n/** Decode a URL-encoded param value, falling back to the raw value on error. */\nfunction safeDecode(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/**\n * Append a query string and/or hash fragment to a path that may already\n * contain a query or hash.\n *\n * - Existing query pairs are preserved; new ones are joined with `&`.\n * - The query string is always inserted before any hash fragment, so an\n * existing `#section` on `path` is kept unless a new `hash` is given.\n *\n * @example\n * ```ts\n * appendQuery(\"/users?tab=list\", { page: 2 }); // → \"/users?tab=list&page=2\"\n * appendQuery(\"/users#top\", { tab: \"list\" }); // → \"/users?tab=list#top\"\n * ```\n */\nexport function appendQuery(path: string, query?: QueryParams, hash?: string): string {\n const hashIdx = path.indexOf(\"#\");\n const base = hashIdx === -1 ? path : path.slice(0, hashIdx);\n const existingHash = hashIdx === -1 ? \"\" : path.slice(hashIdx + 1);\n\n const searchParams = new URLSearchParams();\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n value.forEach((v) => {\n if (v !== undefined && v !== null) searchParams.append(key, String(v));\n });\n } else {\n searchParams.append(key, String(value));\n }\n }\n }\n\n let result = base;\n const queryString = searchParams.toString();\n if (queryString) {\n result += (result.includes(\"?\") ? \"&\" : \"?\") + queryString;\n }\n\n if (hash) {\n result += \"#\" + hash;\n } else if (existingHash) {\n result += \"#\" + existingHash;\n }\n\n return result;\n}\n\n/**\n * Parse the query string out of a path (or bare query string) into a plain\n * object. Repeated keys become arrays; a single key is a scalar string.\n *\n * With `{ coerceBooleans: true }`, the strings `\"true\"`/`\"false\"` are\n * converted to actual booleans.\n *\n * @example\n * ```ts\n * extractQueryFromPath(\"/users/42?tab=profile&tag=a&tag=b\");\n * // → { tab: \"profile\", tag: [\"a\", \"b\"] }\n * extractQueryFromPath(\"/search?active=true\", { coerceBooleans: true });\n * // → { active: true }\n * ```\n */\nexport function extractQueryFromPath(\n path: string,\n options?: { coerceBooleans?: boolean; coerceNumbers?: boolean },\n): QueryParams {\n const hashIdx = path.indexOf(\"#\");\n const noHash = hashIdx === -1 ? path : path.slice(0, hashIdx);\n const queryIdx = noHash.indexOf(\"?\");\n if (queryIdx === -1) return {};\n\n const params = new URLSearchParams(noHash.slice(queryIdx + 1));\n const result: QueryParams = {};\n\n for (const key of new Set(params.keys())) {\n const values = params.getAll(key);\n const parsed = values.map((v) => {\n if (options?.coerceBooleans && (v === \"true\" || v === \"false\")) {\n return v === \"true\";\n }\n if (options?.coerceNumbers && v.trim() !== \"\" && !isNaN(Number(v))) {\n return Number(v);\n }\n return v;\n });\n result[key] = parsed.length > 1 ? parsed : (parsed[0] ?? \"\");\n }\n\n return result;\n}\n\nexport function buildPath(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n const paramNames = extractParamNames(template);\n const unresolved = paramNames.filter(\n (name) =>\n (params[name] === undefined || params[name] === null) &&\n !isOptionalParam(template, name) &&\n // A missing splat simply drops the `/*` suffix (matching React Router,\n // where `/files/*` also matches `/files`).\n name !== \"*\",\n );\n\n const resolved = paramNames.reduce((path, name) => {\n const value = params[name];\n const missing = value === undefined || value === null;\n\n if (name === \"*\") {\n if (missing) return path.replace(/\\/\\*$/, \"\") || \"/\";\n // Splat values are path-like: preserve `/` separators but still\n // encode characters that could break the URL (`?`, `#`, spaces, …).\n const encoded =\n options?.encode === false\n ? String(value)\n : String(value)\n .split(\"/\")\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n return path.replace(/\\/\\*$/, `/${encoded}`);\n }\n\n const re = new RegExp(\n `(^|/):${escapeRegex(name)}\\\\??(?![A-Za-z0-9_])`,\n \"g\",\n );\n\n return path.replace(re, (match, boundary) => {\n if (missing) {\n // Optional segment: drop the whole `/segment`. Required: keep the\n // `:name` placeholder so strict mode can report it.\n return match.endsWith(\"?\") ? \"\" : `${boundary}:${name}`;\n }\n\n const encoded =\n options?.encode === false\n ? String(value)\n : encodeURIComponent(String(value));\n return `${boundary}${encoded}`;\n });\n }, template);\n\n if (unresolved.length > 0) {\n if (options?.strict) {\n throw new RangeError(\n `[route-forge] Missing required param(s) ${unresolved.map((p) => `\":${p}\"`).join(\", \")} in template \"${template}\".`,\n );\n }\n\n devWarn(\n `[route-forge] Unresolved params in path \"${resolved}\". ` +\n `Check that all :param segments have matching keys.`,\n );\n }\n\n return appendQuery(resolved, query, options?.hash);\n}\n\nexport function extractParamNames(template: string): string[] {\n const names = [...template.matchAll(PARAM_SEGMENT_RE())].map(\n (match) => match[2] as string,\n );\n if (template.endsWith(\"/*\")) names.push(\"*\");\n return names;\n}\n\nexport function isDynamic(path: string): boolean {\n return PARAM_SEGMENT_RE().test(path) || path.endsWith(\"/*\");\n}\n\n/**\n * Test whether `currentPath` matches `template`, mirroring React Router's\n * `NavLink` matching semantics:\n *\n * - Case-insensitive by default (pass `caseSensitive: true` to opt out).\n * - Trailing slashes are tolerated (`/users/` matches `/users`).\n * - `exact: true` (the default) requires a full match; `exact: false`\n * matches any path that starts with the template.\n */\nexport function isActivePath(\n currentPath: string,\n template: string,\n options: { exact?: boolean; caseSensitive?: boolean } = {\n exact: true,\n caseSensitive: false,\n },\n): boolean {\n const pathname = (currentPath.split(\"?\")[0] ?? \"\").replace(/\\/+$/, \"\") || \"/\";\n const normalizedTemplate = template.replace(/\\/+$/, \"\") || \"/\";\n const target = options.caseSensitive\n ? normalizedTemplate\n : normalizedTemplate.toLowerCase();\n const candidate = options.caseSensitive ? pathname : pathname.toLowerCase();\n const regex = options.exact ? matchPath(target) : matchPrefix(target);\n\n return regex.test(candidate);\n}\n\nexport function extractParamsFromPath(\n template: string,\n resolvedPath: string,\n): Record<string, string> {\n const pathWithoutSearch = resolvedPath.split(\"?\")[0] ?? \"\";\n const paramNames = extractParamNames(template);\n const match = pathWithoutSearch.match(matchPath(template));\n\n if (!match) return {};\n\n const result: Record<string, string> = {};\n paramNames.forEach((name, index) => {\n const raw = match[index + 1];\n if (raw !== undefined) result[name] = safeDecode(raw);\n });\n return result;\n}\n\nexport function joinPaths(...segments: string[]): string {\n const processed = segments.map((segment) =>\n segment.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\"),\n );\n const filtered = processed.filter(Boolean);\n return \"/\" + filtered.join(\"/\");\n}\n\nexport function build(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n return buildPath(template, params, query, options);\n}\n\nexport function getParamNames(template: string): string[] {\n return extractParamNames(template);\n}\n\n/**\n * Convert a route template string into an anchored `RegExp` for matching paths.\n *\n * Each `:param` segment becomes a capturing group so the returned regex\n * can be used with `.test()` or `.exec()`.\n *\n * Query strings are **not** stripped — callers should split on `\"?\"` first\n * (see {@link isActivePath} or {@link extractParamsFromPath} for higher-level\n * helpers that handle this automatically).\n *\n * @param template - A route template, e.g. `\"/users/:id\"` or `\"/users/:id/posts/:postId\"`.\n * @returns A `RegExp` anchored with `^` and `$` that captures param values.\n *\n * @example\n * ```ts\n * const re = matchPath(\"/users/:id\");\n * re.test(\"/users/42\"); // true\n * re.exec(\"/users/42\"); // [\"/users/42\", \"42\"]\n * re.test(\"/users/42/posts\"); // false (exact match)\n * re.test(\"/users/42?page=1\"); // true (query is part of captured value)\n * ```\n *\n * Compiled patterns are cached per template (the regex has no `/g` flag, so\n * sharing instances across callers is safe).\n */\nconst PATH_CACHE = new Map<string, RegExp>();\n\nexport function matchPath(\n template: string,\n options?: { end?: boolean; caseSensitive?: boolean },\n): RegExp {\n const end = options?.end ?? true;\n const flags = options?.caseSensitive ? \"\" : \"i\";\n const cacheKey = `${template}:${end}:${flags}`;\n\n const cached = PATH_CACHE.get(cacheKey);\n if (cached !== undefined) return cached;\n\n const basePattern = createTemplatePattern(template);\n const pattern = end ? `^${basePattern}$` : `^${basePattern}(?=/|$)`;\n const re = new RegExp(pattern, flags);\n PATH_CACHE.set(cacheKey, re);\n return re;\n}\n\n/**\n * Walk a `defineRoutes` output tree and return a flat array of\n * `{ key, path }` entries where `key` is the dot-joined key path from\n * the root (e.g. `\"SERVICES.BCC.EDIT\"`) and `path` is the raw template\n * string (e.g. `\"/services/bcc/edit/:id\"`).\n *\n * Useful for:\n * - Generating sitemaps from a single source of truth.\n * - Detecting duplicate path strings across branches at startup:\n *\n * @example\n * const flat = flattenRoutes(PATHS);\n * const paths = flat.map((r) => r.path);\n * const dupes = paths.filter((p, i) => paths.indexOf(p) !== i);\n * if (dupes.length) console.warn('Duplicate paths:', dupes);\n */\nexport function flattenRoutes(\n routes: Record<string, unknown>,\n prefix = \"\",\n): FlatRoute[] {\n const entries: FlatRoute[] = [];\n\n for (const key of Object.keys(routes)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n const value = routes[key];\n\n if (typeof value === \"string\") {\n // Plain static string leaf.\n entries.push({ key: fullKey, path: value });\n } else if (value instanceof String) {\n // String-object leaf (wrapped dynamic path from defineRoutes).\n entries.push({ key: fullKey, path: value.valueOf() });\n } else if (typeof value === \"object\" && value !== null) {\n // Nested route group — recurse.\n entries.push(\n ...flattenRoutes(value as Record<string, unknown>, fullKey),\n );\n }\n // Anything else (functions, numbers, …) is silently skipped.\n }\n\n return entries;\n}\n\nfunction deriveBreadcrumbLabel(key: string): string {\n const parts = key.split(\".\");\n const last = parts[parts.length - 1] ?? key;\n // Use the parent segment when the leaf is the conventional \"ROOT\" key,\n // so USERS.ROOT → \"Users\" rather than \"Root\".\n const raw = last === \"ROOT\" && parts.length > 1 ? parts[parts.length - 2]! : last;\n return raw\n .replace(/_/g, \" \")\n .toLowerCase()\n .replace(/^\\w/, (c) => c.toUpperCase());\n}\n\n/**\n * Build a breadcrumb trail from a route tree or a flat route list.\n *\n * For a given `currentPath`, it walks the route tree and returns every route\n * that is an ancestor of (or an exact match to) the current page. Ancestors\n * are matched by prefix (e.g. `/users` matches `/users/edit/42/posts`).\n *\n * Dynamic params in ancestor paths are automatically resolved from the\n * matched portion of the URL.\n *\n * @param routes - A route tree (output of `defineRoutes`) or a pre-flattened\n * array from `flattenRoutes()`.\n * @param currentPath - The current URL (with or without query string).\n * @param options - Optional label resolver.\n * @returns An array of {@link BreadcrumbItem} ordered by depth\n * (most general first), where the last item is the current page.\n *\n * @example\n * ```ts\n * const PATHS = defineRoutes({\n * HOME: \"/\",\n * USERS: { ROOT: \"/users\", EDIT: \"/users/edit/:id\" },\n * } as const);\n *\n * getBreadcrumbs(PATHS, \"/users/edit/42\");\n * // → [\n * // { key: \"HOME\", label: \"Home\", path: \"/\", isCurrent: false },\n * // { key: \"USERS.ROOT\", label: \"Users\", path: \"/users\", isCurrent: false },\n * // { key: \"USERS.EDIT\", label: \"Edit\", path: \"/users/edit/42\", isCurrent: true },\n * // ]\n * ```\n */\nexport function getBreadcrumbs(\n routes: Record<string, unknown> | FlatRoute[],\n currentPath: string,\n options?: BreadcrumbOptions,\n): BreadcrumbItem[] {\n const flat = Array.isArray(routes)\n ? routes\n : flattenRoutes(routes);\n const pathname = currentPath.split(\"?\")[0] ?? \"\";\n const labelFn = options?.labelResolver ?? deriveBreadcrumbLabel;\n const labels = options?.labels ?? {};\n\n const items: Array<{\n key: string;\n resolvedPath: string;\n template: string;\n isCurrent: boolean;\n }> = [];\n\n for (const route of flat) {\n const exactRe = matchPath(route.path);\n const exactMatch = pathname.match(exactRe);\n\n if (exactMatch) {\n const params = extractParamsFromPath(route.path, exactMatch[0]);\n const resolved = isDynamic(route.path)\n ? buildPath(route.path, params)\n : route.path;\n items.push({\n key: route.key,\n resolvedPath: resolved,\n template: route.path,\n isCurrent: true,\n });\n continue;\n }\n\n const prefixRe = matchPrefix(route.path);\n const prefixMatch = pathname.match(prefixRe);\n\n if (prefixMatch) {\n const matchedPortion = prefixMatch[0];\n const params = extractParamsFromPath(route.path, matchedPortion);\n const resolved = isDynamic(route.path)\n ? buildPath(route.path, params)\n : route.path;\n items.push({\n key: route.key,\n resolvedPath: resolved,\n template: route.path,\n isCurrent: false,\n });\n }\n }\n\n items.sort((a, b) => a.template.length - b.template.length);\n\n return items.map((item) => ({\n key: item.key,\n label: labels[item.key] ?? labelFn(item.key),\n path: item.resolvedPath,\n isCurrent: item.isCurrent,\n }));\n}\n\n"],"mappings":"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","names":["hooks_exports","__export","useActivePath","useNavigateTo","useResolvedPath","useRouteParams","useTypedSearchParams","__toCommonJS","import_react","import_react_router_dom","ESCAPE_RE","PARAM_SEGMENT_RE","isProduction","devWarn","message","escapeRegex","value","ESCAPE_RE","createTemplatePattern","template","splat","base","pattern","cursor","match","PARAM_SEGMENT_RE","start","token","boundary","optional","isOptionalParam","name","PREFIX_CACHE","ROOT_PREFIX_RE","matchPrefix","cached","re","appendQuery","path","query","hash","hashIdx","base","existingHash","searchParams","key","value","v","result","queryString","extractQueryFromPath","options","noHash","queryIdx","params","parsed","buildPath","template","paramNames","extractParamNames","unresolved","name","isOptionalParam","resolved","missing","encoded","segment","re","escapeRegex","match","boundary","p","devWarn","names","PARAM_SEGMENT_RE","isActivePath","currentPath","template","options","pathname","normalizedTemplate","target","candidate","matchPath","matchPrefix","PATH_CACHE","matchPath","template","options","end","flags","cacheKey","cached","basePattern","createTemplatePattern","pattern","re","useRouteParams","_route","useNavigateTo","navigate","path","options","useResolvedPath","template","params","query","buildPath","useActivePath","location","isActivePath","useTypedSearchParams","searchParams","setSearchParams","queryParams","extractQueryFromPath","setTypedQuery","newQuery","navigateOptions","updatedPath","appendQuery","updatedSearchParams"]}
@@ -0,0 +1,89 @@
1
+ import { R as RouteParams, Q as QueryParams, B as BuildPathOptions, E as ExtractParams } from '../index-Beg7xp8k.cjs';
2
+
3
+ /**
4
+ * A typed wrapper around React Router's `useParams`.
5
+ *
6
+ * Pass a route template as a type parameter, or a dynamic route value from a
7
+ * `defineRoutes()` tree for automatic type inference:
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * // Route: '/a/:x/b/:y/c/:z'
12
+ * const { x, y, z } = useRouteParams<'/a/:x/b/:y/c/:z'>();
13
+ *
14
+ * // Or pass a route from your PATHS tree — params are inferred from it:
15
+ * const PATHS = defineRoutes({ USERS: { EDIT: '/users/edit/:id' } } as const);
16
+ * const { id } = useRouteParams(PATHS.USERS.EDIT);
17
+ * ```
18
+ */
19
+ declare function useRouteParams<T extends string = string>(): Record<ExtractParams<T>, string>;
20
+ declare function useRouteParams<P extends string>(route: {
21
+ readonly paramNames: ReadonlyArray<P> | Array<P>;
22
+ }): Record<P, string>;
23
+ type NavigateOptions = {
24
+ replace?: boolean;
25
+ state?: unknown;
26
+ };
27
+ /**
28
+ * A typed `navigate` helper that accepts a resolved path (output of `.build()`)
29
+ * or a route value straight from `defineRoutes()` (e.g. a `String` object or a
30
+ * primitive string), with optional navigation options.
31
+ *
32
+ * `String` objects (used by route values so they can carry `.build()`) are
33
+ * coerced to primitives, since React Router's `navigate()` ignores them.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * const navigateTo = useNavigateTo();
38
+ * navigateTo(PATHS.USERS.EDIT.build({ id: 42 }));
39
+ * navigateTo(PATHS.HOME, { replace: true });
40
+ * ```
41
+ */
42
+ declare function useNavigateTo(): (path: string, options?: NavigateOptions) => void;
43
+ /**
44
+ * Resolves a dynamic path template against params, mirroring `buildPath()`.
45
+ *
46
+ * Accepts the same `options` bag as `build()` / `buildPath()`:
47
+ * - (default) soft-fail: `console.warn` and return the partial path with unresolved `:param` placeholders.
48
+ * - `{ strict: true }`: throw a `RangeError` on missing params — matching `.build()`'s strict behaviour.
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'
53
+ * const path = useResolvedPath('/users/:id', {}, undefined, { strict: true }); // throws RangeError
54
+ * const path = useResolvedPath('/files/*', { "*": "a/b" }); // → '/files/a/b'
55
+ * ```
56
+ */
57
+ declare function useResolvedPath(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
58
+ /**
59
+ * A hook that checks whether the current location matches a route template or path.
60
+ * Thin wrapper around `isActivePath(useLocation().pathname, template, options)`.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const isActive = useActivePath(PATHS.USERS.ROOT, { exact: false });
65
+ * ```
66
+ */
67
+ declare function useActivePath(template: string, options?: {
68
+ exact?: boolean;
69
+ caseSensitive?: boolean;
70
+ }): boolean;
71
+ /**
72
+ * A typed wrapper around React Router's `useSearchParams`.
73
+ * Returns parsed query params object and a setter that updates query params.
74
+ *
75
+ * @example
76
+ * ```tsx
77
+ * const [query, setQuery] = useTypedSearchParams({ coerceBooleans: true, coerceNumbers: true });
78
+ * setQuery({ tab: 'details', page: 2 });
79
+ * ```
80
+ */
81
+ declare function useTypedSearchParams(options?: {
82
+ coerceBooleans?: boolean;
83
+ coerceNumbers?: boolean;
84
+ }): readonly [QueryParams, (newQuery: QueryParams, navigateOptions?: {
85
+ replace?: boolean;
86
+ state?: unknown;
87
+ }) => void];
88
+
89
+ export { type NavigateOptions, useActivePath, useNavigateTo, useResolvedPath, useRouteParams, useTypedSearchParams };
@@ -0,0 +1,89 @@
1
+ import { R as RouteParams, Q as QueryParams, B as BuildPathOptions, E as ExtractParams } from '../index-Beg7xp8k.js';
2
+
3
+ /**
4
+ * A typed wrapper around React Router's `useParams`.
5
+ *
6
+ * Pass a route template as a type parameter, or a dynamic route value from a
7
+ * `defineRoutes()` tree for automatic type inference:
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * // Route: '/a/:x/b/:y/c/:z'
12
+ * const { x, y, z } = useRouteParams<'/a/:x/b/:y/c/:z'>();
13
+ *
14
+ * // Or pass a route from your PATHS tree — params are inferred from it:
15
+ * const PATHS = defineRoutes({ USERS: { EDIT: '/users/edit/:id' } } as const);
16
+ * const { id } = useRouteParams(PATHS.USERS.EDIT);
17
+ * ```
18
+ */
19
+ declare function useRouteParams<T extends string = string>(): Record<ExtractParams<T>, string>;
20
+ declare function useRouteParams<P extends string>(route: {
21
+ readonly paramNames: ReadonlyArray<P> | Array<P>;
22
+ }): Record<P, string>;
23
+ type NavigateOptions = {
24
+ replace?: boolean;
25
+ state?: unknown;
26
+ };
27
+ /**
28
+ * A typed `navigate` helper that accepts a resolved path (output of `.build()`)
29
+ * or a route value straight from `defineRoutes()` (e.g. a `String` object or a
30
+ * primitive string), with optional navigation options.
31
+ *
32
+ * `String` objects (used by route values so they can carry `.build()`) are
33
+ * coerced to primitives, since React Router's `navigate()` ignores them.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * const navigateTo = useNavigateTo();
38
+ * navigateTo(PATHS.USERS.EDIT.build({ id: 42 }));
39
+ * navigateTo(PATHS.HOME, { replace: true });
40
+ * ```
41
+ */
42
+ declare function useNavigateTo(): (path: string, options?: NavigateOptions) => void;
43
+ /**
44
+ * Resolves a dynamic path template against params, mirroring `buildPath()`.
45
+ *
46
+ * Accepts the same `options` bag as `build()` / `buildPath()`:
47
+ * - (default) soft-fail: `console.warn` and return the partial path with unresolved `:param` placeholders.
48
+ * - `{ strict: true }`: throw a `RangeError` on missing params — matching `.build()`'s strict behaviour.
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'
53
+ * const path = useResolvedPath('/users/:id', {}, undefined, { strict: true }); // throws RangeError
54
+ * const path = useResolvedPath('/files/*', { "*": "a/b" }); // → '/files/a/b'
55
+ * ```
56
+ */
57
+ declare function useResolvedPath(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
58
+ /**
59
+ * A hook that checks whether the current location matches a route template or path.
60
+ * Thin wrapper around `isActivePath(useLocation().pathname, template, options)`.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const isActive = useActivePath(PATHS.USERS.ROOT, { exact: false });
65
+ * ```
66
+ */
67
+ declare function useActivePath(template: string, options?: {
68
+ exact?: boolean;
69
+ caseSensitive?: boolean;
70
+ }): boolean;
71
+ /**
72
+ * A typed wrapper around React Router's `useSearchParams`.
73
+ * Returns parsed query params object and a setter that updates query params.
74
+ *
75
+ * @example
76
+ * ```tsx
77
+ * const [query, setQuery] = useTypedSearchParams({ coerceBooleans: true, coerceNumbers: true });
78
+ * setQuery({ tab: 'details', page: 2 });
79
+ * ```
80
+ */
81
+ declare function useTypedSearchParams(options?: {
82
+ coerceBooleans?: boolean;
83
+ coerceNumbers?: boolean;
84
+ }): readonly [QueryParams, (newQuery: QueryParams, navigateOptions?: {
85
+ replace?: boolean;
86
+ state?: unknown;
87
+ }) => void];
88
+
89
+ export { type NavigateOptions, useActivePath, useNavigateTo, useResolvedPath, useRouteParams, useTypedSearchParams };
@@ -0,0 +1,2 @@
1
+ import{c as o,d as n,e as i,h as u}from"../chunk-EFHUAXKE.js";import{useCallback as c}from"react";import{useParams as y,useNavigate as h,useLocation as m,useSearchParams as x}from"react-router-dom";function v(e){return y()}function S(){let e=h();return c((t,a)=>{e(String(t),a)},[e])}function N(e,t,a,r){return i(e,t,a,r)}function A(e,t={exact:!0,caseSensitive:!1}){let a=m();return u(a.pathname,e,t)}function Q(e){let[t,a]=x(),r=m(),P=n(`${r.pathname}?${t.toString()}`,e),p=c((l,d)=>{let s=o("",l),g=new URLSearchParams(s.startsWith("?")?s.slice(1):s);a(g,d)},[a]);return[P,p]}export{A as useActivePath,S as useNavigateTo,N as useResolvedPath,v as useRouteParams,Q as useTypedSearchParams};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/index.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useParams, useNavigate, useLocation, useSearchParams } from \"react-router-dom\";\nimport type {\n ExtractParams,\n QueryParams,\n RouteParams,\n BuildPathOptions,\n} from \"../types\";\nimport { buildPath, isActivePath, extractQueryFromPath, appendQuery } from \"../core/utils\";\n\n// ─── useRouteParams ──────────────────────────────────────────────────────────\n\n/**\n * A typed wrapper around React Router's `useParams`.\n *\n * Pass a route template as a type parameter, or a dynamic route value from a\n * `defineRoutes()` tree for automatic type inference:\n *\n * @example\n * ```tsx\n * // Route: '/a/:x/b/:y/c/:z'\n * const { x, y, z } = useRouteParams<'/a/:x/b/:y/c/:z'>();\n *\n * // Or pass a route from your PATHS tree — params are inferred from it:\n * const PATHS = defineRoutes({ USERS: { EDIT: '/users/edit/:id' } } as const);\n * const { id } = useRouteParams(PATHS.USERS.EDIT);\n * ```\n */\n// Overload 1: no-arg generic — caller provides the template literal as T\nexport function useRouteParams<T extends string = string>(): Record<\n ExtractParams<T>,\n string\n>;\n// Overload 2: pass a route from defineRoutes() — P is inferred from paramNames array element type\nexport function useRouteParams<P extends string>(\n route: { readonly paramNames: ReadonlyArray<P> | Array<P> },\n): Record<P, string>;\n// Implementation\nexport function useRouteParams<P extends string>(\n _route?: { readonly paramNames: ReadonlyArray<P> | Array<P> },\n): Record<string, string> {\n return useParams() as Record<string, string>;\n}\n\n// ─── useNavigateTo ──────────────────────────────────────────────────────────\n\nexport type NavigateOptions = {\n replace?: boolean;\n state?: unknown;\n};\n\n/**\n * A typed `navigate` helper that accepts a resolved path (output of `.build()`)\n * or a route value straight from `defineRoutes()` (e.g. a `String` object or a\n * primitive string), with optional navigation options.\n *\n * `String` objects (used by route values so they can carry `.build()`) are\n * coerced to primitives, since React Router's `navigate()` ignores them.\n *\n * @example\n * ```tsx\n * const navigateTo = useNavigateTo();\n * navigateTo(PATHS.USERS.EDIT.build({ id: 42 }));\n * navigateTo(PATHS.HOME, { replace: true });\n * ```\n */\nexport function useNavigateTo() {\n const navigate = useNavigate();\n\n return useCallback(\n (path: string, options?: NavigateOptions) => {\n navigate(String(path), options);\n },\n [navigate],\n );\n}\n\n// ─── useResolvedPath ─────────────────────────────────────────────────────────\n\n/**\n * Resolves a dynamic path template against params, mirroring `buildPath()`.\n *\n * Accepts the same `options` bag as `build()` / `buildPath()`:\n * - (default) soft-fail: `console.warn` and return the partial path with unresolved `:param` placeholders.\n * - `{ strict: true }`: throw a `RangeError` on missing params — matching `.build()`'s strict behaviour.\n *\n * @example\n * ```tsx\n * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'\n * const path = useResolvedPath('/users/:id', {}, undefined, { strict: true }); // throws RangeError\n * const path = useResolvedPath('/files/*', { \"*\": \"a/b\" }); // → '/files/a/b'\n * ```\n */\nexport function useResolvedPath(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n return buildPath(template, params, query, options);\n}\n\n// ─── useActivePath ───────────────────────────────────────────────────────────\n\n/**\n * A hook that checks whether the current location matches a route template or path.\n * Thin wrapper around `isActivePath(useLocation().pathname, template, options)`.\n *\n * @example\n * ```tsx\n * const isActive = useActivePath(PATHS.USERS.ROOT, { exact: false });\n * ```\n */\nexport function useActivePath(\n template: string,\n options: { exact?: boolean; caseSensitive?: boolean } = {\n exact: true,\n caseSensitive: false,\n },\n): boolean {\n const location = useLocation();\n return isActivePath(location.pathname, template, options);\n}\n\n// ─── useTypedSearchParams ────────────────────────────────────────────────────\n\n/**\n * A typed wrapper around React Router's `useSearchParams`.\n * Returns parsed query params object and a setter that updates query params.\n *\n * @example\n * ```tsx\n * const [query, setQuery] = useTypedSearchParams({ coerceBooleans: true, coerceNumbers: true });\n * setQuery({ tab: 'details', page: 2 });\n * ```\n */\nexport function useTypedSearchParams(options?: {\n coerceBooleans?: boolean;\n coerceNumbers?: boolean;\n}) {\n const [searchParams, setSearchParams] = useSearchParams();\n const location = useLocation();\n\n const queryParams = extractQueryFromPath(\n `${location.pathname}?${searchParams.toString()}`,\n options,\n );\n\n const setTypedQuery = useCallback(\n (\n newQuery: QueryParams,\n navigateOptions?: { replace?: boolean; state?: unknown },\n ) => {\n const updatedPath = appendQuery(\"\", newQuery);\n const updatedSearchParams = new URLSearchParams(\n updatedPath.startsWith(\"?\") ? updatedPath.slice(1) : updatedPath,\n );\n setSearchParams(updatedSearchParams, navigateOptions);\n },\n [setSearchParams],\n );\n\n return [queryParams, setTypedQuery] as const;\n}\n\n"],"mappings":"8DAAA,OAAS,eAAAA,MAAmB,QAC5B,OAAS,aAAAC,EAAW,eAAAC,EAAa,eAAAC,EAAa,mBAAAC,MAAuB,mBAqC9D,SAASC,EACdC,EACwB,CACxB,OAAOC,EAAU,CACnB,CAwBO,SAASC,GAAgB,CAC9B,IAAMC,EAAWC,EAAY,EAE7B,OAAOC,EACL,CAACC,EAAcC,IAA8B,CAC3CJ,EAAS,OAAOG,CAAI,EAAGC,CAAO,CAChC,EACA,CAACJ,CAAQ,CACX,CACF,CAkBO,SAASK,EACdC,EACAC,EACAC,EACAJ,EACQ,CACR,OAAOK,EAAUH,EAAUC,EAAQC,EAAOJ,CAAO,CACnD,CAaO,SAASM,EACdJ,EACAF,EAAwD,CACtD,MAAO,GACP,cAAe,EACjB,EACS,CACT,IAAMO,EAAWC,EAAY,EAC7B,OAAOC,EAAaF,EAAS,SAAUL,EAAUF,CAAO,CAC1D,CAcO,SAASU,EAAqBV,EAGlC,CACD,GAAM,CAACW,EAAcC,CAAe,EAAIC,EAAgB,EAClDN,EAAWC,EAAY,EAEvBM,EAAcC,EAClB,GAAGR,EAAS,QAAQ,IAAII,EAAa,SAAS,CAAC,GAC/CX,CACF,EAEMgB,EAAgBlB,EACpB,CACEmB,EACAC,IACG,CACH,IAAMC,EAAcC,EAAY,GAAIH,CAAQ,EACtCI,EAAsB,IAAI,gBAC9BF,EAAY,WAAW,GAAG,EAAIA,EAAY,MAAM,CAAC,EAAIA,CACvD,EACAP,EAAgBS,EAAqBH,CAAe,CACtD,EACA,CAACN,CAAe,CAClB,EAEA,MAAO,CAACE,EAAaE,CAAa,CACpC","names":["useCallback","useParams","useNavigate","useLocation","useSearchParams","useRouteParams","_route","useParams","useNavigateTo","navigate","useNavigate","useCallback","path","options","useResolvedPath","template","params","query","buildPath","useActivePath","location","useLocation","isActivePath","useTypedSearchParams","searchParams","setSearchParams","useSearchParams","queryParams","extractQueryFromPath","setTypedQuery","newQuery","navigateOptions","updatedPath","appendQuery","updatedSearchParams"]}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * A static route string (template or resolved), e.g. '/users/:id' or '/users/42'
3
+ */
4
+ type RoutePath = `/${string}`;
5
+ /**
6
+ * A route builder function that accepts params and returns a resolved path.
7
+ */
8
+ type RouteBuilder<TParams extends RouteParams = RouteParams> = (params: TParams) => RoutePath;
9
+ /**
10
+ * Acceptable param value types for route builders.
11
+ */
12
+ type RouteParam = string | number | boolean;
13
+ type RouteParams = Record<string, RouteParam>;
14
+ /**
15
+ * A leaf node in a route definition: either a static path or a builder function.
16
+ */
17
+ type RouteLeaf = RoutePath | RouteBuilder;
18
+ /**
19
+ * Recursively defines a route map: each key is either a leaf or a nested map.
20
+ */
21
+ type RouteMap = {
22
+ [key: string]: RouteLeaf | RouteMap;
23
+ };
24
+ /**
25
+ * Annotatable shape of a route map for `defineRoutes()`.
26
+ * Each key is either a static path string or a nested route tree.
27
+ */
28
+ type RouteTree = {
29
+ [key: string]: string | RouteTree;
30
+ };
31
+ /**
32
+ * Matches a single ASCII word character (`[A-Za-z0-9_]`), mirroring the
33
+ * runtime param regex used by the path utilities.
34
+ */
35
+ type IsWordChar<C extends string> = C extends "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "_" ? true : false;
36
+ /**
37
+ * The leading run of word characters in a string, e.g. `"name.json"` → `"name"`
38
+ * and `"id?"` → `"id"`.
39
+ */
40
+ type WordRun<S extends string> = S extends `${infer C}${infer Rest}` ? IsWordChar<C> extends true ? `${C}${WordRun<Rest>}` : "" : "";
41
+ /**
42
+ * `WordRun` of a segment, mapped to `never` when empty so it never pollutes a
43
+ * param union. For non-literal input (e.g. a `DynamicRoute` value whose object
44
+ * members widen the captured segment to `string`) it falls back to `string`,
45
+ * keeping param access valid without attempting literal extraction.
46
+ */
47
+ type ExtractWord<S extends string> = string extends S ? string : WordRun<S> extends "" ? never : WordRun<S>;
48
+ /**
49
+ * Extracts param names from a path template string.
50
+ * e.g. '/users/:id/posts/:postId' → 'id' | 'postId'
51
+ */
52
+ type ExtractParams<T extends string> = T extends `${string}:${infer Segment}/${infer Rest}` ? ExtractWord<Segment> | ExtractParams<`/${Rest}`> : T extends `${string}:${infer Segment}` ? ExtractWord<Segment> : T extends `${infer Prefix}/*` ? "*" | ExtractParams<Prefix> : never;
53
+ /**
54
+ * Builds a params object type from a path template.
55
+ * e.g. '/users/:id' → { id: RouteParam }
56
+ */
57
+ type PathParams<T extends string> = ExtractParams<T> extends never ? never : {
58
+ [K in ExtractParams<T>]: RouteParam;
59
+ };
60
+ /**
61
+ * Acceptable query param value types for route builders.
62
+ */
63
+ type QueryParams = Record<string, RouteParam | RouteParam[] | null | undefined>;
64
+ /**
65
+ * Options accepted by `buildPath` (4th positional argument).
66
+ *
67
+ * @example
68
+ * // Throws a RangeError when a :param segment is missing rather than
69
+ * // silently leaving the colon-placeholder in the output string.
70
+ * buildPath('/users/:id', {}, undefined, { strict: true });
71
+ */
72
+ type BuildPathOptions = {
73
+ /**
74
+ * When `true`, `buildPath` throws a `RangeError` if any `:param`
75
+ * placeholder is left unresolved instead of emitting a console.warn.
76
+ * Useful in dev/test environments to catch missing params early.
77
+ */
78
+ strict?: boolean;
79
+ /**
80
+ * When `false`, param values are substituted verbatim instead of being
81
+ * URL-encoded. Defaults to `true` — param values are run through
82
+ * `encodeURIComponent` so characters like `/`, `?`, `#` cannot break the
83
+ * URL structure.
84
+ */
85
+ encode?: boolean;
86
+ /**
87
+ * URL hash fragment to append after the query string (e.g. `"section"` → `#section`).
88
+ * The leading `#` is added automatically.
89
+ */
90
+ hash?: string;
91
+ };
92
+ /**
93
+ * A single entry produced by `flattenRoutes()`.
94
+ */
95
+ type FlatRoute = {
96
+ /** Dot-joined key path from the root, e.g. `"SERVICES.BCC.EDIT"`. */
97
+ key: string;
98
+ /** The raw path template string, e.g. `"/services/bcc/edit/:id"`. */
99
+ path: string;
100
+ };
101
+ /**
102
+ * A single breadcrumb entry produced by `getBreadcrumbs()`.
103
+ *
104
+ * @see {@link getBreadcrumbs}
105
+ */
106
+ type BreadcrumbItem = {
107
+ /** Dot-joined key from the route tree, e.g. `"USERS.EDIT"`. */
108
+ key: string;
109
+ /** Human-readable label derived from the key (or from `labelResolver`). */
110
+ label: string;
111
+ /** The resolved path (params filled in), e.g. `"/users/edit/42"`. */
112
+ path: string;
113
+ /** `true` if this is the current page (exact match). */
114
+ isCurrent: boolean;
115
+ };
116
+ /**
117
+ * Options for `getBreadcrumbs()`.
118
+ *
119
+ * @see {@link getBreadcrumbs}
120
+ */
121
+ type BreadcrumbOptions = {
122
+ /**
123
+ * Static label map keyed by the dot-joined key (e.g. `"USERS.EDIT"`).
124
+ * Takes precedence over `labelResolver`.
125
+ */
126
+ labels?: Record<string, string>;
127
+ /**
128
+ * Custom label resolver. Receives the dot-joined key
129
+ * (e.g. `"USERS.BCC.EDIT"`) and returns the display label.
130
+ *
131
+ * @default
132
+ * The default implementation takes the last key segment,
133
+ * replaces underscores with spaces, and capitalises the
134
+ * first letter (e.g. `"PRODUCT_DETAILS"` → `"Product Details"`).
135
+ */
136
+ labelResolver?: (key: string) => string;
137
+ };
138
+ /**
139
+ * Options for `matchPath()`.
140
+ */
141
+ type MatchPathOptions = {
142
+ /** When true, match requires an exact end of path string (default: true). */
143
+ end?: boolean;
144
+ /** When true, perform case-sensitive matching (default: false). */
145
+ caseSensitive?: boolean;
146
+ };
147
+
148
+ export type { BuildPathOptions as B, ExtractParams as E, FlatRoute as F, MatchPathOptions as M, PathParams as P, QueryParams as Q, RouteParams as R, BreadcrumbOptions as a, BreadcrumbItem as b, RoutePath as c, RouteTree as d, RouteBuilder as e, RouteLeaf as f, RouteMap as g, RouteParam as h };