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.
package/dist/index.d.ts CHANGED
@@ -1,139 +1,75 @@
1
+ import { Q as QueryParams, R as RouteParams, B as BuildPathOptions, F as FlatRoute, a as BreadcrumbOptions, b as BreadcrumbItem, P as PathParams, c as RoutePath, E as ExtractParams, d as RouteTree } from './index-Beg7xp8k.js';
2
+ export { M as MatchPathOptions, e as RouteBuilder, f as RouteLeaf, g as RouteMap, h as RouteParam } from './index-Beg7xp8k.js';
3
+
1
4
  /**
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;
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
- * Extracts param names from a path template string.
26
- * e.g. '/users/:id/posts/:postId' → 'id' | 'postId'
27
- */
28
- type StripOptional<S extends string> = S extends `${infer Name}?` ? Name : S;
29
- type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? StripOptional<Param> | ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? StripOptional<Param> : never;
30
- /**
31
- * Builds a params object type from a path template.
32
- * e.g. '/users/:id' → { id: RouteParam }
5
+ * Emits a `console.warn` in non-production environments.
6
+ * Shared by the core utilities and `defineRoutes()` so the production check
7
+ * lives in one place.
33
8
  */
34
- type PathParams<T extends string> = ExtractParams<T> extends never ? never : {
35
- [K in ExtractParams<T>]: RouteParam;
36
- };
9
+ declare function devWarn(message: string): void;
37
10
  /**
38
- * Acceptable query param value types for route builders.
11
+ * Clears internal regex cache maps (PREFIX_CACHE and PATH_CACHE).
12
+ * Useful in test suites to prevent cached patterns from leaking across test cases.
39
13
  */
40
- type QueryParams = Record<string, RouteParam | RouteParam[] | null | undefined>;
14
+ declare function clearPathCache(): void;
41
15
  /**
42
- * Options accepted by `buildPath` (4th positional argument).
16
+ * Append a query string and/or hash fragment to a path that may already
17
+ * contain a query or hash.
18
+ *
19
+ * - Existing query pairs are preserved; new ones are joined with `&`.
20
+ * - The query string is always inserted before any hash fragment, so an
21
+ * existing `#section` on `path` is kept unless a new `hash` is given.
43
22
  *
44
23
  * @example
45
- * // Throws a RangeError when a :param segment is missing rather than
46
- * // silently leaving the colon-placeholder in the output string.
47
- * buildPath('/users/:id', {}, undefined, { strict: true });
48
- */
49
- type BuildPathOptions = {
50
- /**
51
- * When `true`, `buildPath` throws a `RangeError` if any `:param`
52
- * placeholder is left unresolved instead of emitting a console.warn.
53
- * Useful in dev/test environments to catch missing params early.
54
- */
55
- strict?: boolean;
56
- /**
57
- * URL hash fragment to append after the query string (e.g. `"section"` → `#section`).
58
- * The leading `#` is added automatically.
59
- */
60
- hash?: string;
61
- };
62
- /**
63
- * A single entry produced by `flattenRoutes()`.
24
+ * ```ts
25
+ * appendQuery("/users?tab=list", { page: 2 }); // "/users?tab=list&page=2"
26
+ * appendQuery("/users#top", { tab: "list" }); // → "/users?tab=list#top"
27
+ * ```
64
28
  */
65
- type FlatRoute = {
66
- /** Dot-joined key path from the root, e.g. `"SERVICES.BCC.EDIT"`. */
67
- key: string;
68
- /** The raw path template string, e.g. `"/services/bcc/edit/:id"`. */
69
- path: string;
70
- };
29
+ declare function appendQuery(path: string, query?: QueryParams, hash?: string): string;
71
30
  /**
72
- * A single breadcrumb entry produced by `getBreadcrumbs()`.
31
+ * Parse the query string out of a path (or bare query string) into a plain
32
+ * object. Repeated keys become arrays; a single key is a scalar string.
73
33
  *
74
- * @see {@link getBreadcrumbs}
75
- */
76
- type BreadcrumbItem = {
77
- /** Dot-joined key from the route tree, e.g. `"USERS.EDIT"`. */
78
- key: string;
79
- /** Human-readable label derived from the key (or from `labelResolver`). */
80
- label: string;
81
- /** The resolved path (params filled in), e.g. `"/users/edit/42"`. */
82
- path: string;
83
- /** `true` if this is the current page (exact match). */
84
- isCurrent: boolean;
85
- };
86
- /**
87
- * Options for `getBreadcrumbs()`.
34
+ * With `{ coerceBooleans: true }`, the strings `"true"`/`"false"` are
35
+ * converted to actual booleans.
88
36
  *
89
- * @see {@link getBreadcrumbs}
37
+ * @example
38
+ * ```ts
39
+ * extractQueryFromPath("/users/42?tab=profile&tag=a&tag=b");
40
+ * // → { tab: "profile", tag: ["a", "b"] }
41
+ * extractQueryFromPath("/search?active=true", { coerceBooleans: true });
42
+ * // → { active: true }
43
+ * ```
90
44
  */
91
- type BreadcrumbOptions = {
92
- /**
93
- * Custom label resolver. Receives the dot-joined key
94
- * (e.g. `"USERS.BCC.EDIT"`) and returns the display label.
95
- *
96
- * @default
97
- * The default implementation takes the last key segment,
98
- * replaces underscores with spaces, and capitalises the
99
- * first letter (e.g. `"PRODUCT_DETAILS"` → `"Product Details"`).
100
- */
101
- labelResolver?: (key: string) => string;
102
- };
103
-
45
+ declare function extractQueryFromPath(path: string, options?: {
46
+ coerceBooleans?: boolean;
47
+ coerceNumbers?: boolean;
48
+ }): QueryParams;
104
49
  declare function buildPath(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
105
50
  declare function extractParamNames(template: string): string[];
106
51
  declare function isDynamic(path: string): boolean;
52
+ /**
53
+ * Test whether `currentPath` matches `template`, mirroring React Router's
54
+ * `NavLink` matching semantics:
55
+ *
56
+ * - Case-insensitive by default (pass `caseSensitive: true` to opt out).
57
+ * - Trailing slashes are tolerated (`/users/` matches `/users`).
58
+ * - `exact: true` (the default) requires a full match; `exact: false`
59
+ * matches any path that starts with the template.
60
+ */
107
61
  declare function isActivePath(currentPath: string, template: string, options?: {
108
62
  exact?: boolean;
63
+ caseSensitive?: boolean;
109
64
  }): boolean;
110
65
  declare function extractParamsFromPath(template: string, resolvedPath: string): Record<string, string>;
111
66
  declare function joinPaths(...segments: string[]): string;
112
67
  declare function build(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
113
68
  declare function getParamNames(template: string): string[];
114
- /**
115
- * Convert a route template string into an anchored `RegExp` for matching paths.
116
- *
117
- * Each `:param` segment becomes a capturing group so the returned regex
118
- * can be used with `.test()` or `.exec()`.
119
- *
120
- * Query strings are **not** stripped — callers should split on `"?"` first
121
- * (see {@link isActivePath} or {@link extractParamsFromPath} for higher-level
122
- * helpers that handle this automatically).
123
- *
124
- * @param template - A route template, e.g. `"/users/:id"` or `"/users/:id/posts/:postId"`.
125
- * @returns A `RegExp` anchored with `^` and `$` that captures param values.
126
- *
127
- * @example
128
- * ```ts
129
- * const re = matchPath("/users/:id");
130
- * re.test("/users/42"); // true
131
- * re.exec("/users/42"); // ["/users/42", "42"]
132
- * re.test("/users/42/posts"); // false (exact match)
133
- * re.test("/users/42?page=1"); // true (query is part of captured value)
134
- * ```
135
- */
136
- declare function matchPath(template: string): RegExp;
69
+ declare function matchPath(template: string, options?: {
70
+ end?: boolean;
71
+ caseSensitive?: boolean;
72
+ }): RegExp;
137
73
  /**
138
74
  * Walk a `defineRoutes` output tree and return a flat array of
139
75
  * `{ key, path }` entries where `key` is the dot-joined key path from
@@ -185,66 +121,22 @@ declare function flattenRoutes(routes: Record<string, unknown>, prefix?: string)
185
121
  */
186
122
  declare function getBreadcrumbs(routes: Record<string, unknown> | FlatRoute[], currentPath: string, options?: BreadcrumbOptions): BreadcrumbItem[];
187
123
 
188
- type RouteInput = {
189
- [key: string]: string | RouteInput;
190
- };
191
- type DynamicRoute<T extends string> = T extends `${string}:${string}` ? T & {
192
- build(params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions): RoutePath;
193
- paramNames: Array<ExtractParams<T>>;
194
- } : T;
195
- type ResolvedRoutes<T extends RouteInput> = {
196
- [K in keyof T]: T[K] extends RouteInput ? ResolvedRoutes<T[K]> : T[K] extends string ? DynamicRoute<T[K]> : never;
197
- };
198
- declare function defineRoutes<T extends RouteInput>(routes: T): ResolvedRoutes<T>;
199
-
200
124
  /**
201
- * React integration hooks for route-forge.
202
- * These are thin wrappers import only if you're using React Router.
203
- */
204
-
205
- /**
206
- * A typed wrapper around React Router's `useParams`.
125
+ * A dynamic route is a template string with a `:param` or trailing `/*` splat,
126
+ * augmented with a `.build()` helper and a `.paramNames` array.
207
127
  *
208
- * @example
209
- * ```tsx
210
- * // Route: '/a/:x/b/:y/c/:z'
211
- * const { x, y, z } = useRouteParams<'/a/:x/b/:y/c/:z'>();
212
- * ```
128
+ * Exported so consumers (e.g. the hooks entry) can type against it.
213
129
  */
214
- declare function useRouteParams<T extends string>(): Record<ExtractParams<T>, string>;
215
- type NavigateOptions = {
216
- replace?: boolean;
217
- state?: unknown;
130
+ type StaticRoute<T extends string> = T & {
131
+ build(query?: QueryParams, options?: BuildPathOptions): RoutePath;
218
132
  };
219
- /**
220
- * A typed `navigate` helper that accepts a resolved path (output of `.build()`)
221
- * or a plain static path, with optional navigation options.
222
- *
223
- * @example
224
- * ```tsx
225
- * const navigateTo = useNavigateTo();
226
- * navigateTo(PATHS.USERS.EDIT.build({ id: 42 }));
227
- * navigateTo(PATHS.HOME, { replace: true });
228
- * ```
229
- */
230
- declare function useNavigateTo(): (path: string, options?: NavigateOptions) => void;
231
- /**
232
- * Resolves a dynamic path template against params using React Router's
233
- * `generatePath`, with proper typing.
234
- *
235
- * Accepts the same `options` bag as `build()` / `buildPath()`:
236
- * - (default) soft-fail: `console.warn` and return the partial path with unresolved `:param` placeholders.
237
- * - `{ strict: true }`: throw a `RangeError` on missing params — matching `.build()`'s strict behaviour.
238
- *
239
- * When all params are present, resolution is delegated to React Router's `generatePath`,
240
- * which correctly handles splat (`*`) and optional (`:param?`) segments.
241
- *
242
- * @example
243
- * ```tsx
244
- * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'
245
- * const path = useResolvedPath('/users/:id', {}, undefined, { strict: true }); // throws RangeError
246
- * ```
247
- */
248
- declare function useResolvedPath(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
133
+ type DynamicRoute<T extends string> = T extends `${string}:${string}` | `${string}/*` ? T & {
134
+ build(params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions): RoutePath;
135
+ paramNames: Array<ExtractParams<T>>;
136
+ } : StaticRoute<T>;
137
+ type ResolvedRoutes<T extends RouteTree> = {
138
+ [K in keyof T]: T[K] extends RouteTree ? ResolvedRoutes<T[K]> : T[K] extends string ? DynamicRoute<T[K]> : never;
139
+ };
140
+ declare function defineRoutes<T extends RouteTree>(routes: T): ResolvedRoutes<T>;
249
141
 
250
- export { type BreadcrumbItem, type BreadcrumbOptions, type BuildPathOptions, type ExtractParams, type FlatRoute, type PathParams, type QueryParams, type RouteBuilder, type RouteLeaf, type RouteMap, type RouteParam, type RouteParams, type RoutePath, build, buildPath, defineRoutes, extractParamNames, extractParamsFromPath, flattenRoutes, getBreadcrumbs, getParamNames, isActivePath, isDynamic, joinPaths, matchPath, useNavigateTo, useResolvedPath, useRouteParams };
142
+ export { BreadcrumbItem, BreadcrumbOptions, BuildPathOptions, type DynamicRoute, ExtractParams, FlatRoute, PathParams, QueryParams, RouteParams, RoutePath, RouteTree, type StaticRoute, appendQuery, build, buildPath, clearPathCache, defineRoutes, devWarn, extractParamNames, extractParamsFromPath, extractQueryFromPath, flattenRoutes, getBreadcrumbs, getParamNames, isActivePath, isDynamic, joinPaths, matchPath };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- var R=()=>/:([^/]+)/g,w=()=>/[.*+?^${}()|[\]\\]/g;function v(t){return t.replace(w(),"\\$&")}function y(t){return v(t).replace(R(),"([^/]+)")}function x(t,e,a){let n=t,s=new URLSearchParams;if(e)for(let[u,r]of Object.entries(e))r!=null&&(Array.isArray(r)?r.forEach(i=>{i!=null&&s.append(u,String(i))}):s.append(u,String(r)));let o=s.toString();return o&&(n+=(n.includes("?")?"&":"?")+o),a&&(n+="#"+a),n}function c(t,e,a,n){let s=p(t),o=s.filter(r=>e[r]===void 0||e[r]===null),u=s.reduce((r,i)=>{let l=e[i],g=l==null?`:${i}`:String(l);return r.replace(new RegExp(`:${v(i)}\\??(?=/|$)`,"g"),g)},t);if(o.length>0){if(n?.strict)throw new RangeError(`[route-forge] Missing required param(s) ${o.map(i=>`":${i}"`).join(", ")} in template "${t}".`);globalThis.process?.env?.NODE_ENV!=="production"&&console.warn(`[route-forge] Unresolved params in path "${u}". Check that all :param segments have matching keys.`)}return x(u,a,n?.hash)}function p(t){return[...t.matchAll(R())].map(e=>e[1].replace(/\?$/,""))}function m(t){return R().test(t)}function E(t,e,a={exact:!0}){let n=t.split("?")[0]??"";return(a.exact?h(e):new RegExp(`^${y(e)}`)).test(n)}function f(t,e){let a=e.split("?")[0]??"",n=p(t),s=a.match(h(t));return s?Object.fromEntries(n.map((o,u)=>[o,s[u+1]??""])):{}}function N(...t){return"/"+t.map(n=>n.replace(/^\/+/,"").replace(/\/+$/,"")).filter(Boolean).join("/")}function B(t,e,a,n){return c(t,e,a,n)}function $(t){return p(t)}function h(t){return new RegExp(`^${y(t)}$`)}function T(t,e=""){let a=[];for(let n of Object.keys(t)){let s=e?`${e}.${n}`:n,o=t[n];typeof o=="string"?a.push({key:s,path:o}):o instanceof String?a.push({key:s,path:o.valueOf()}):typeof o=="object"&&o!==null&&a.push(...T(o,s))}return a}function A(t){let e=t.split("."),a=e[e.length-1]??t;return(a==="ROOT"&&e.length>1?e[e.length-2]:a).replace(/_/g," ").toLowerCase().replace(/^\w/,s=>s.toUpperCase())}function Q(t,e,a){let n=Array.isArray(t)?t:T(t),s=e.split("?")[0]??"",o=a?.labelResolver??A,u=[];for(let r of n){let i=h(r.path),l=s.match(i);if(l){let P=f(r.path,l[0]),d=m(r.path)?c(r.path,P):r.path;u.push({key:r.key,resolvedPath:d,template:r.path,isCurrent:!0});continue}let g=new RegExp(`^${y(r.path)}`),b=s.match(g);if(b){let P=b[0],d=f(r.path,P),O=m(r.path)?c(r.path,d):r.path;u.push({key:r.key,resolvedPath:O,template:r.path,isCurrent:!1})}}return u.sort((r,i)=>r.template.length-i.template.length),u.map(r=>({key:r.key,label:o(r.key),path:r.resolvedPath,isCurrent:r.isCurrent}))}var j=t=>typeof t=="object"&&t!==null;function S(t){let e=p(t),a=new String(t);return a.build=(n,s,o)=>c(t,n,s,o),a.paramNames=e,a}function k(t){let e={};for(let a in t){if(!Object.prototype.hasOwnProperty.call(t,a))continue;let n=t[a];typeof n=="string"?e[a]=m(n)?S(n):n:j(n)&&(e[a]=k(n))}return e}function D(t){return k(t)}import{useParams as I,useNavigate as C,generatePath as F}from"react-router-dom";function K(){return I()}function M(){let t=C();return(e,a)=>{t(e,a)}}function _(t,e,a,n){if(!p(t).every(r=>e[r]!==void 0&&e[r]!==null))return c(t,e,a,n);let u=F(t,Object.fromEntries(Object.entries(e).map(([r,i])=>[r,String(i)])));return x(u,a,n?.hash)}export{B as build,c as buildPath,D as defineRoutes,p as extractParamNames,f as extractParamsFromPath,T as flattenRoutes,Q as getBreadcrumbs,$ as getParamNames,E as isActivePath,m as isDynamic,N as joinPaths,h as matchPath,M as useNavigateTo,_ as useResolvedPath,K as useRouteParams};
1
+ import{a as n,b as R,c as f,d as l,e as c,f as d,g as u,h as m,i as T,j as g,k as x,l as w,m as b,n as P,o as k}from"./chunk-EFHUAXKE.js";var O=t=>typeof t=="object"&&t!==null&&!Array.isArray(t)&&Object.getPrototypeOf(t)===Object.prototype;function $(t,e){t.startsWith("/")||n(`[route-forge] Route "${e}" does not start with "/": "${t}".`),t.includes("*")&&!t.endsWith("/*")&&n(`[route-forge] Route "${e}" uses "*" outside a trailing "/*" splat; only a trailing splat is supported: "${t}".`)}function v(t){let e=new Map,o=new Set,a=P(t);for(let r of a){let s=e.get(r.path);s===void 0?e.set(r.path,r.key):o.has(r.path)||(o.add(r.path),n(`[route-forge] Duplicate route path "${r.path}" for "${s}" and "${r.key}". Only one of them will be reachable.`))}for(let r=0;r<a.length;r++){let s=a[r];if(u(s.path))for(let p=r+1;p<a.length;p++){let i=a[p];if(!u(i.path)&&m(i.path,s.path,{exact:!0})){let h=`${s.key}->${i.key}`;o.has(h)||(o.add(h),n(`[route-forge] Route "${i.key}" ("${i.path}") is shadowed by dynamic route "${s.key}" ("${s.path}"). Place static routes before dynamic parameters in route trees.`))}}}}function B(t){let e=new String(t);return e.build=(o,a)=>c(t,{},o,a),e}function D(t){let e=d(t),o=new String(t);return o.build=(a,r,s)=>c(t,a,r,s),o.paramNames=e,o}function y(t){let e={};for(let o in t){if(!Object.prototype.hasOwnProperty.call(t,o))continue;let a=t[o];typeof a=="string"?($(a,o),e[o]=u(a)?D(a):B(a)):O(a)&&(e[o]=y(a))}return e}function Q(t){let e=y(t);return v(e),e}export{f as appendQuery,x as build,c as buildPath,R as clearPathCache,Q as defineRoutes,n as devWarn,d as extractParamNames,T as extractParamsFromPath,l as extractQueryFromPath,P as flattenRoutes,k as getBreadcrumbs,w as getParamNames,m as isActivePath,u as isDynamic,g as joinPaths,b as matchPath};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/utils.ts","../src/core/defineRoutes.ts","../src/hooks/index.ts"],"sourcesContent":["import type { BreadcrumbItem, BreadcrumbOptions, QueryParams, RouteParam, RouteParams, BuildPathOptions, FlatRoute } from \"../types\";\n\n/** Returns a fresh RegExp each call — avoids shared `lastIndex` state on /g patterns. */\nconst PATH_PARAM_RE = () => /:([^/]+)/g;\nconst ESCAPE_RE = () => /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction escapeRegex(value: string): string {\n return value.replace(ESCAPE_RE(), \"\\\\$&\");\n}\n\nfunction createTemplatePattern(template: string): string {\n return escapeRegex(template).replace(PATH_PARAM_RE(), \"([^/]+)\");\n}\n\nexport function appendQuery(path: string, query?: QueryParams, hash?: string): string {\n let result = path;\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 const queryString = searchParams.toString();\n if (queryString) {\n result += (result.includes(\"?\") ? \"&\" : \"?\") + queryString;\n }\n\n if (hash) {\n result += \"#\" + hash;\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) => params[name] === undefined || params[name] === null,\n );\n\n const resolved = paramNames.reduce((path, name) => {\n const value = params[name];\n const replacement = value === undefined || value === null ? `:${name}` : String(value);\n return path.replace(\n new RegExp(`:${escapeRegex(name)}\\\\??(?=/|$)`, \"g\"),\n replacement,\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 const runtimeProcess = (\n globalThis as typeof globalThis & {\n process?: {\n env?: Record<string, string | undefined>;\n };\n }\n ).process;\n\n if (runtimeProcess?.env?.NODE_ENV !== \"production\") {\n console.warn(\n `[route-forge] Unresolved params in path \"${resolved}\". ` +\n `Check that all :param segments have matching keys.`,\n );\n }\n }\n\n return appendQuery(resolved, query, options?.hash);\n}\n\nexport function extractParamNames(template: string): string[] {\n return [...template.matchAll(PATH_PARAM_RE())].map(\n (match) => (match[1] as string).replace(/\\?$/, \"\"),\n );\n}\n\nexport function isDynamic(path: string): boolean {\n return PATH_PARAM_RE().test(path);\n}\n\nexport function isActivePath(\n currentPath: string,\n template: string,\n options: { exact?: boolean } = { exact: true },\n): boolean {\n const pathWithoutSearch = currentPath.split(\"?\")[0] ?? \"\";\n const regex = options.exact\n ? matchPath(template)\n : new RegExp(`^${createTemplatePattern(template)}`);\n\n return regex.test(pathWithoutSearch);\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 return Object.fromEntries(\n paramNames.map((name, index) => [name, match[index + 1] ?? \"\"]),\n );\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 */\nexport function matchPath(template: string): RegExp {\n return new RegExp(`^${createTemplatePattern(template)}$`);\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\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 = new RegExp(`^${createTemplatePattern(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: labelFn(item.key),\n path: item.resolvedPath,\n isCurrent: item.isCurrent,\n }));\n}\n\n","import { buildPath, extractParamNames, isDynamic } from \"./utils\";\nimport type {\n BuildPathOptions,\n ExtractParams,\n PathParams,\n QueryParams,\n RoutePath,\n} from \"../types\";\n\n// Re-export so consumers that import from 'core/defineRoutes' get the full surface\nexport { buildPath, extractParamNames, isDynamic } from \"./utils\";\n\ntype RouteInput = {\n [key: string]: string | RouteInput;\n};\n\ntype DynamicRoute<T extends string> = T extends `${string}:${string}`\n ? T & {\n build(params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions): RoutePath;\n paramNames: Array<ExtractParams<T>>;\n }\n : T;\n\ntype ResolvedRoutes<T extends RouteInput> = {\n [K in keyof T]: T[K] extends RouteInput\n ? ResolvedRoutes<T[K]>\n : T[K] extends string\n ? DynamicRoute<T[K]>\n : never;\n};\n\nconst isRouteGroup = (value: unknown): value is RouteInput =>\n typeof value === \"object\" && value !== null;\n\nfunction wrapDynamicPath<T extends string>(template: T): DynamicRoute<T> {\n const paramNames = extractParamNames(template);\n const wrapped = new String(template) as unknown as DynamicRoute<T> & {\n build: (params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions) => RoutePath;\n paramNames: Array<ExtractParams<T>>;\n };\n\n wrapped.build = (params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions) =>\n buildPath(template, params, query, options) as RoutePath;\n wrapped.paramNames = paramNames as Array<ExtractParams<T>>;\n\n return wrapped as unknown as DynamicRoute<T>;\n}\n\nfunction processRouteMap<T extends RouteInput>(routes: T): ResolvedRoutes<T> {\n const result = {} as ResolvedRoutes<T>;\n\n for (const key in routes) {\n if (!Object.prototype.hasOwnProperty.call(routes, key)) continue;\n\n const value = routes[key];\n\n if (typeof value === \"string\") {\n result[key] = (\n isDynamic(value) ? wrapDynamicPath(value) : value\n ) as ResolvedRoutes<T>[typeof key];\n } else if (isRouteGroup(value)) {\n result[key] = processRouteMap(\n value as RouteInput,\n ) as unknown as ResolvedRoutes<T>[typeof key];\n }\n }\n\n return result;\n}\n\nexport function defineRoutes<T extends RouteInput>(\n routes: T,\n): ResolvedRoutes<T> {\n return processRouteMap(routes);\n}\n","/**\n * React integration hooks for route-forge.\n * These are thin wrappers — import only if you're using React Router.\n */\n\nimport { useParams, useNavigate, generatePath } from \"react-router-dom\";\nimport type {\n ExtractParams,\n QueryParams,\n RouteParams,\n BuildPathOptions,\n} from \"../types\";\nimport { appendQuery, extractParamNames, buildPath } from \"../core/utils\";\n\n// ─── useRouteParams ──────────────────────────────────────────────────────────\n\n/**\n * A typed wrapper around React Router's `useParams`.\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 */\nexport function useRouteParams<T extends string>(): Record<\n ExtractParams<T>,\n string\n> {\n return useParams() as Record<ExtractParams<T>, string>;\n}\n\n// ─── useNavigateTo ──────────────────────────────────────────────────────────\n\ntype 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 plain static path, with optional navigation options.\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 (path: string, options?: NavigateOptions) => {\n navigate(path, options);\n };\n}\n\n// ─── useResolvedPath ─────────────────────────────────────────────────────────\n\n/**\n * Resolves a dynamic path template against params using React Router's\n * `generatePath`, with proper typing.\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 * When all params are present, resolution is delegated to React Router's `generatePath`,\n * which correctly handles splat (`*`) and optional (`:param?`) segments.\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 * ```\n */\nexport function useResolvedPath(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n const paramNames = extractParamNames(template);\n const hasAllParams = paramNames.every(\n (name) => params[name] !== undefined && params[name] !== null,\n );\n\n if (!hasAllParams) {\n // Let buildPath own all missing-param behaviour (warn/throw) —\n // avoids re-implementing (and duplicating) the same check here.\n return buildPath(template, params, query, options);\n }\n\n // All params present — use generatePath for correct splat / optional-segment handling.\n const path = generatePath(\n template,\n Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])),\n );\n\n return appendQuery(path, query, options?.hash);\n}\n"],"mappings":"AAGA,IAAMA,EAAgB,IAAM,YACtBC,EAAY,IAAM,sBAExB,SAASC,EAAYC,EAAuB,CAC1C,OAAOA,EAAM,QAAQF,EAAU,EAAG,MAAM,CAC1C,CAEA,SAASG,EAAsBC,EAA0B,CACvD,OAAOH,EAAYG,CAAQ,EAAE,QAAQL,EAAc,EAAG,SAAS,CACjE,CAEO,SAASM,EAAYC,EAAcC,EAAqBC,EAAuB,CACpF,IAAIC,EAASH,EAEPI,EAAe,IAAI,gBACzB,GAAIH,EACF,OAAW,CAACI,EAAKT,CAAK,IAAK,OAAO,QAAQK,CAAK,EAClBL,GAAU,OACjC,MAAM,QAAQA,CAAK,EACrBA,EAAM,QAASU,GAAM,CACIA,GAAM,MAAMF,EAAa,OAAOC,EAAK,OAAOC,CAAC,CAAC,CACvE,CAAC,EAEDF,EAAa,OAAOC,EAAK,OAAOT,CAAK,CAAC,GAK5C,IAAMW,EAAcH,EAAa,SAAS,EAC1C,OAAIG,IACFJ,IAAWA,EAAO,SAAS,GAAG,EAAI,IAAM,KAAOI,GAG7CL,IACFC,GAAU,IAAMD,GAGXC,CACT,CAEO,SAASK,EACdV,EACAW,EACAR,EACAS,EACQ,CACR,IAAMC,EAAaC,EAAkBd,CAAQ,EACvCe,EAAaF,EAAW,OAC3BG,GAASL,EAAOK,CAAI,IAAM,QAAaL,EAAOK,CAAI,IAAM,IAC3D,EAEMC,EAAWJ,EAAW,OAAO,CAACX,EAAMc,IAAS,CACjD,IAAMlB,EAAQa,EAAOK,CAAI,EACnBE,EAAqCpB,GAAU,KAAO,IAAIkB,CAAI,GAAK,OAAOlB,CAAK,EACrF,OAAOI,EAAK,QACV,IAAI,OAAO,IAAIL,EAAYmB,CAAI,CAAC,cAAe,GAAG,EAClDE,CACF,CACF,EAAGlB,CAAQ,EAEX,GAAIe,EAAW,OAAS,EAAG,CACzB,GAAIH,GAAS,OACX,MAAM,IAAI,WACR,2CAA2CG,EAAW,IAAKI,GAAM,KAAKA,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,iBAAiBnB,CAAQ,IACjH,EAIA,WAKA,SAEkB,KAAK,WAAa,cACpC,QAAQ,KACN,4CAA4CiB,CAAQ,uDAEtD,CAEJ,CAEA,OAAOhB,EAAYgB,EAAUd,EAAOS,GAAS,IAAI,CACnD,CAEO,SAASE,EAAkBd,EAA4B,CAC5D,MAAO,CAAC,GAAGA,EAAS,SAASL,EAAc,CAAC,CAAC,EAAE,IAC5CyB,GAAWA,EAAM,CAAC,EAAa,QAAQ,MAAO,EAAE,CACnD,CACF,CAEO,SAASC,EAAUnB,EAAuB,CAC/C,OAAOP,EAAc,EAAE,KAAKO,CAAI,CAClC,CAEO,SAASoB,EACdC,EACAvB,EACAY,EAA+B,CAAE,MAAO,EAAK,EACpC,CACT,IAAMY,EAAoBD,EAAY,MAAM,GAAG,EAAE,CAAC,GAAK,GAKvD,OAJcX,EAAQ,MAClBa,EAAUzB,CAAQ,EAClB,IAAI,OAAO,IAAID,EAAsBC,CAAQ,CAAC,EAAE,GAEvC,KAAKwB,CAAiB,CACrC,CAEO,SAASE,EACd1B,EACA2B,EACwB,CACxB,IAAMH,EAAoBG,EAAa,MAAM,GAAG,EAAE,CAAC,GAAK,GAClDd,EAAaC,EAAkBd,CAAQ,EACvCoB,EAAQI,EAAkB,MAAMC,EAAUzB,CAAQ,CAAC,EAEzD,OAAKoB,EAEE,OAAO,YACZP,EAAW,IAAI,CAACG,EAAMY,IAAU,CAACZ,EAAMI,EAAMQ,EAAQ,CAAC,GAAK,EAAE,CAAC,CAChE,EAJmB,CAAC,CAKtB,CAEO,SAASC,KAAaC,EAA4B,CAKvD,MAAO,IAJWA,EAAS,IAAKC,GAC9BA,EAAQ,QAAQ,OAAQ,EAAE,EAAE,QAAQ,OAAQ,EAAE,CAChD,EAC2B,OAAO,OAAO,EACnB,KAAK,GAAG,CAChC,CAEO,SAASC,EACdhC,EACAW,EACAR,EACAS,EACQ,CACR,OAAOF,EAAUV,EAAUW,EAAQR,EAAOS,CAAO,CACnD,CAEO,SAASqB,EAAcjC,EAA4B,CACxD,OAAOc,EAAkBd,CAAQ,CACnC,CAwBO,SAASyB,EAAUzB,EAA0B,CAClD,OAAO,IAAI,OAAO,IAAID,EAAsBC,CAAQ,CAAC,GAAG,CAC1D,CAkBO,SAASkC,EACdC,EACAC,EAAS,GACI,CACb,IAAMC,EAAuB,CAAC,EAE9B,QAAW9B,KAAO,OAAO,KAAK4B,CAAM,EAAG,CACrC,IAAMG,EAAUF,EAAS,GAAGA,CAAM,IAAI7B,CAAG,GAAKA,EACxCT,EAAQqC,EAAO5B,CAAG,EAEpB,OAAOT,GAAU,SAEnBuC,EAAQ,KAAK,CAAE,IAAKC,EAAS,KAAMxC,CAAM,CAAC,EACjCA,aAAiB,OAE1BuC,EAAQ,KAAK,CAAE,IAAKC,EAAS,KAAMxC,EAAM,QAAQ,CAAE,CAAC,EAC3C,OAAOA,GAAU,UAAYA,IAAU,MAEhDuC,EAAQ,KACN,GAAGH,EAAcpC,EAAkCwC,CAAO,CAC5D,CAGJ,CAEA,OAAOD,CACT,CAEA,SAASE,EAAsBhC,EAAqB,CAClD,IAAMiC,EAAQjC,EAAI,MAAM,GAAG,EACrBkC,EAAOD,EAAMA,EAAM,OAAS,CAAC,GAAKjC,EAIxC,OADYkC,IAAS,QAAUD,EAAM,OAAS,EAAIA,EAAMA,EAAM,OAAS,CAAC,EAAKC,GAE1E,QAAQ,KAAM,GAAG,EACjB,YAAY,EACZ,QAAQ,MAAQC,GAAMA,EAAE,YAAY,CAAC,CAC1C,CAkCO,SAASC,EACdR,EACAZ,EACAX,EACkB,CAClB,IAAMgC,EAAO,MAAM,QAAQT,CAAM,EAC7BA,EACAD,EAAcC,CAAM,EAClBU,EAAWtB,EAAY,MAAM,GAAG,EAAE,CAAC,GAAK,GACxCuB,EAAUlC,GAAS,eAAiB2B,EAEpCQ,EAKD,CAAC,EAEN,QAAWC,KAASJ,EAAM,CACxB,IAAMK,EAAUxB,EAAUuB,EAAM,IAAI,EAC9BE,EAAaL,EAAS,MAAMI,CAAO,EAEzC,GAAIC,EAAY,CACd,IAAMvC,EAASe,EAAsBsB,EAAM,KAAME,EAAW,CAAC,CAAC,EACxDjC,EAAWI,EAAU2B,EAAM,IAAI,EACjCtC,EAAUsC,EAAM,KAAMrC,CAAM,EAC5BqC,EAAM,KACVD,EAAM,KAAK,CACT,IAAKC,EAAM,IACX,aAAc/B,EACd,SAAU+B,EAAM,KAChB,UAAW,EACb,CAAC,EACD,QACF,CAEA,IAAMG,EAAW,IAAI,OAAO,IAAIpD,EAAsBiD,EAAM,IAAI,CAAC,EAAE,EAC7DI,EAAcP,EAAS,MAAMM,CAAQ,EAE3C,GAAIC,EAAa,CACf,IAAMC,EAAiBD,EAAY,CAAC,EAC9BzC,EAASe,EAAsBsB,EAAM,KAAMK,CAAc,EACzDpC,EAAWI,EAAU2B,EAAM,IAAI,EACjCtC,EAAUsC,EAAM,KAAMrC,CAAM,EAC5BqC,EAAM,KACVD,EAAM,KAAK,CACT,IAAKC,EAAM,IACX,aAAc/B,EACd,SAAU+B,EAAM,KAChB,UAAW,EACb,CAAC,CACH,CACF,CAEA,OAAAD,EAAM,KAAK,CAACO,EAAGC,IAAMD,EAAE,SAAS,OAASC,EAAE,SAAS,MAAM,EAEnDR,EAAM,IAAKS,IAAU,CAC1B,IAAKA,EAAK,IACV,MAAOV,EAAQU,EAAK,GAAG,EACvB,KAAMA,EAAK,aACX,UAAWA,EAAK,SAClB,EAAE,CACJ,CCrSA,IAAMC,EAAgBC,GACpB,OAAOA,GAAU,UAAYA,IAAU,KAEzC,SAASC,EAAkCC,EAA8B,CACvE,IAAMC,EAAaC,EAAkBF,CAAQ,EACvCG,EAAU,IAAI,OAAOH,CAAQ,EAKnC,OAAAG,EAAQ,MAAQ,CAACC,EAAuBC,EAAqBC,IAC3DC,EAAUP,EAAUI,EAAQC,EAAOC,CAAO,EAC5CH,EAAQ,WAAaF,EAEdE,CACT,CAEA,SAASK,EAAsCC,EAA8B,CAC3E,IAAMC,EAAS,CAAC,EAEhB,QAAWC,KAAOF,EAAQ,CACxB,GAAI,CAAC,OAAO,UAAU,eAAe,KAAKA,EAAQE,CAAG,EAAG,SAExD,IAAMb,EAAQW,EAAOE,CAAG,EAEpB,OAAOb,GAAU,SACnBY,EAAOC,CAAG,EACRC,EAAUd,CAAK,EAAIC,EAAgBD,CAAK,EAAIA,EAErCD,EAAaC,CAAK,IAC3BY,EAAOC,CAAG,EAAIH,EACZV,CACF,EAEJ,CAEA,OAAOY,CACT,CAEO,SAASG,EACdJ,EACmB,CACnB,OAAOD,EAAgBC,CAAM,CAC/B,CCrEA,OAAS,aAAAK,EAAW,eAAAC,EAAa,gBAAAC,MAAoB,mBAoB9C,SAASC,GAGd,CACA,OAAOC,EAAU,CACnB,CAoBO,SAASC,GAAgB,CAC9B,IAAMC,EAAWC,EAAY,EAE7B,MAAO,CAACC,EAAcC,IAA8B,CAClDH,EAASE,EAAMC,CAAO,CACxB,CACF,CAqBO,SAASC,EACdC,EACAC,EACAC,EACAJ,EACQ,CAMR,GAAI,CALeK,EAAkBH,CAAQ,EACb,MAC7BI,GAASH,EAAOG,CAAI,IAAM,QAAaH,EAAOG,CAAI,IAAM,IAC3D,EAKE,OAAOC,EAAUL,EAAUC,EAAQC,EAAOJ,CAAO,EAInD,IAAMD,EAAOS,EACXN,EACA,OAAO,YAAY,OAAO,QAAQC,CAAM,EAAE,IAAI,CAAC,CAACM,EAAGC,CAAC,IAAM,CAACD,EAAG,OAAOC,CAAC,CAAC,CAAC,CAAC,CAC3E,EAEA,OAAOC,EAAYZ,EAAMK,EAAOJ,GAAS,IAAI,CAC/C","names":["PATH_PARAM_RE","ESCAPE_RE","escapeRegex","value","createTemplatePattern","template","appendQuery","path","query","hash","result","searchParams","key","v","queryString","buildPath","params","options","paramNames","extractParamNames","unresolved","name","resolved","replacement","p","match","isDynamic","isActivePath","currentPath","pathWithoutSearch","matchPath","extractParamsFromPath","resolvedPath","index","joinPaths","segments","segment","build","getParamNames","flattenRoutes","routes","prefix","entries","fullKey","deriveBreadcrumbLabel","parts","last","c","getBreadcrumbs","flat","pathname","labelFn","items","route","exactRe","exactMatch","prefixRe","prefixMatch","matchedPortion","a","b","item","isRouteGroup","value","wrapDynamicPath","template","paramNames","extractParamNames","wrapped","params","query","options","buildPath","processRouteMap","routes","result","key","isDynamic","defineRoutes","useParams","useNavigate","generatePath","useRouteParams","useParams","useNavigateTo","navigate","useNavigate","path","options","useResolvedPath","template","params","query","extractParamNames","name","buildPath","generatePath","k","v","appendQuery"]}
1
+ {"version":3,"sources":["../src/core/defineRoutes.ts"],"sourcesContent":["import {\n buildPath,\n devWarn,\n extractParamNames,\n flattenRoutes,\n isActivePath,\n isDynamic,\n} from \"./utils\";\nimport type {\n BuildPathOptions,\n ExtractParams,\n PathParams,\n QueryParams,\n RoutePath,\n RouteTree,\n} from \"../types\";\n\n// Re-export so consumers that import from 'core/defineRoutes' get the full surface\nexport { buildPath, extractParamNames, isDynamic } from \"./utils\";\n\n/**\n * A dynamic route is a template string with a `:param` or trailing `/*` splat,\n * augmented with a `.build()` helper and a `.paramNames` array.\n *\n * Exported so consumers (e.g. the hooks entry) can type against it.\n */\nexport type StaticRoute<T extends string> = T & {\n build(query?: QueryParams, options?: BuildPathOptions): RoutePath;\n};\n\nexport type DynamicRoute<T extends string> =\n T extends `${string}:${string}` | `${string}/*`\n ? T & {\n build(params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions): RoutePath;\n paramNames: Array<ExtractParams<T>>;\n }\n : StaticRoute<T>;\n\ntype ResolvedRoutes<T extends RouteTree> = {\n [K in keyof T]: T[K] extends RouteTree\n ? ResolvedRoutes<T[K]>\n : T[K] extends string\n ? DynamicRoute<T[K]>\n : never;\n};\n\nconst isRouteGroup = (value: unknown): value is RouteTree =>\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype;\n\n/**\n * Validate a single route template and warn (dev-only) about common mistakes:\n * missing leading `/` and non-trailing `*` splats.\n */\nfunction validateTemplate(template: string, key: string): void {\n if (!template.startsWith(\"/\")) {\n devWarn(\n `[route-forge] Route \"${key}\" does not start with \"/\": \"${template}\".`,\n );\n }\n\n if (template.includes(\"*\") && !template.endsWith(\"/*\")) {\n devWarn(\n `[route-forge] Route \"${key}\" uses \"*\" outside a trailing \"/*\" splat; only a trailing splat is supported: \"${template}\".`,\n );\n }\n}\n\n/**\n * Warn (dev-only) when two routes resolve to the same path template or shadow each other.\n */\nfunction detectDuplicatePaths(routes: Record<string, unknown>): void {\n const seen = new Map<string, string>();\n const warned = new Set<string>();\n const flat = flattenRoutes(routes);\n\n for (const route of flat) {\n const existing = seen.get(route.path);\n if (existing === undefined) {\n seen.set(route.path, route.key);\n } else if (!warned.has(route.path)) {\n warned.add(route.path);\n devWarn(\n `[route-forge] Duplicate route path \"${route.path}\" for \"${existing}\" and \"${route.key}\". ` +\n `Only one of them will be reachable.`,\n );\n }\n }\n\n // Shadowing check: warn when a static path matches a dynamic path defined before it\n for (let i = 0; i < flat.length; i++) {\n const r1 = flat[i]!;\n if (!isDynamic(r1.path)) continue;\n\n for (let j = i + 1; j < flat.length; j++) {\n const r2 = flat[j]!;\n if (isDynamic(r2.path)) continue;\n\n if (isActivePath(r2.path, r1.path, { exact: true })) {\n const pairKey = `${r1.key}->${r2.key}`;\n if (!warned.has(pairKey)) {\n warned.add(pairKey);\n devWarn(\n `[route-forge] Route \"${r2.key}\" (\"${r2.path}\") is shadowed by dynamic route \"${r1.key}\" (\"${r1.path}\"). Place static routes before dynamic parameters in route trees.`,\n );\n }\n }\n }\n }\n}\n\nfunction wrapStaticPath<T extends string>(template: T): StaticRoute<T> {\n const wrapped = new String(template) as unknown as StaticRoute<T> & {\n build: (query?: QueryParams, options?: BuildPathOptions) => RoutePath;\n };\n wrapped.build = (query?: QueryParams, options?: BuildPathOptions) =>\n buildPath(template, {}, query, options) as RoutePath;\n\n return wrapped as unknown as StaticRoute<T>;\n}\n\nfunction wrapDynamicPath<T extends string>(template: T): DynamicRoute<T> {\n const paramNames = extractParamNames(template);\n const wrapped = new String(template) as unknown as DynamicRoute<T> & {\n build: (params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions) => RoutePath;\n paramNames: Array<ExtractParams<T>>;\n };\n\n wrapped.build = (params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions) =>\n buildPath(template, params, query, options) as RoutePath;\n wrapped.paramNames = paramNames as Array<ExtractParams<T>>;\n\n return wrapped as unknown as DynamicRoute<T>;\n}\n\nfunction processRouteMap<T extends RouteTree>(routes: T): ResolvedRoutes<T> {\n const result = {} as ResolvedRoutes<T>;\n\n for (const key in routes) {\n if (!Object.prototype.hasOwnProperty.call(routes, key)) continue;\n\n const value = routes[key];\n\n if (typeof value === \"string\") {\n validateTemplate(value, key);\n result[key] = (\n isDynamic(value) ? wrapDynamicPath(value) : wrapStaticPath(value)\n ) as ResolvedRoutes<T>[typeof key];\n } else if (isRouteGroup(value)) {\n result[key] = processRouteMap(\n value as RouteTree,\n ) as unknown as ResolvedRoutes<T>[typeof key];\n }\n }\n\n return result;\n}\n\nexport function defineRoutes<T extends RouteTree>(\n routes: T,\n): ResolvedRoutes<T> {\n const result = processRouteMap(routes);\n detectDuplicatePaths(result as unknown as Record<string, unknown>);\n return result;\n}\n"],"mappings":"0IA8CA,IAAMA,EAAgBC,GACpB,OAAOA,GAAU,UACjBA,IAAU,MACV,CAAC,MAAM,QAAQA,CAAK,GACpB,OAAO,eAAeA,CAAK,IAAM,OAAO,UAM1C,SAASC,EAAiBC,EAAkBC,EAAmB,CACxDD,EAAS,WAAW,GAAG,GAC1BE,EACE,wBAAwBD,CAAG,+BAA+BD,CAAQ,IACpE,EAGEA,EAAS,SAAS,GAAG,GAAK,CAACA,EAAS,SAAS,IAAI,GACnDE,EACE,wBAAwBD,CAAG,kFAAkFD,CAAQ,IACvH,CAEJ,CAKA,SAASG,EAAqBC,EAAuC,CACnE,IAAMC,EAAO,IAAI,IACXC,EAAS,IAAI,IACbC,EAAOC,EAAcJ,CAAM,EAEjC,QAAWK,KAASF,EAAM,CACxB,IAAMG,EAAWL,EAAK,IAAII,EAAM,IAAI,EAChCC,IAAa,OACfL,EAAK,IAAII,EAAM,KAAMA,EAAM,GAAG,EACpBH,EAAO,IAAIG,EAAM,IAAI,IAC/BH,EAAO,IAAIG,EAAM,IAAI,EACrBP,EACE,uCAAuCO,EAAM,IAAI,UAAUC,CAAQ,UAAUD,EAAM,GAAG,wCAExF,EAEJ,CAGA,QAASE,EAAI,EAAGA,EAAIJ,EAAK,OAAQI,IAAK,CACpC,IAAMC,EAAKL,EAAKI,CAAC,EACjB,GAAKE,EAAUD,EAAG,IAAI,EAEtB,QAASE,EAAIH,EAAI,EAAGG,EAAIP,EAAK,OAAQO,IAAK,CACxC,IAAMC,EAAKR,EAAKO,CAAC,EACjB,GAAI,CAAAD,EAAUE,EAAG,IAAI,GAEjBC,EAAaD,EAAG,KAAMH,EAAG,KAAM,CAAE,MAAO,EAAK,CAAC,EAAG,CACnD,IAAMK,EAAU,GAAGL,EAAG,GAAG,KAAKG,EAAG,GAAG,GAC/BT,EAAO,IAAIW,CAAO,IACrBX,EAAO,IAAIW,CAAO,EAClBf,EACE,wBAAwBa,EAAG,GAAG,OAAOA,EAAG,IAAI,oCAAoCH,EAAG,GAAG,OAAOA,EAAG,IAAI,mEACtG,EAEJ,CACF,CACF,CACF,CAEA,SAASM,EAAiClB,EAA6B,CACrE,IAAMmB,EAAU,IAAI,OAAOnB,CAAQ,EAGnC,OAAAmB,EAAQ,MAAQ,CAACC,EAAqBC,IACpCC,EAAUtB,EAAU,CAAC,EAAGoB,EAAOC,CAAO,EAEjCF,CACT,CAEA,SAASI,EAAkCvB,EAA8B,CACvE,IAAMwB,EAAaC,EAAkBzB,CAAQ,EACvCmB,EAAU,IAAI,OAAOnB,CAAQ,EAKnC,OAAAmB,EAAQ,MAAQ,CAACO,EAAuBN,EAAqBC,IAC3DC,EAAUtB,EAAU0B,EAAQN,EAAOC,CAAO,EAC5CF,EAAQ,WAAaK,EAEdL,CACT,CAEA,SAASQ,EAAqCvB,EAA8B,CAC1E,IAAMwB,EAAS,CAAC,EAEhB,QAAW3B,KAAOG,EAAQ,CACxB,GAAI,CAAC,OAAO,UAAU,eAAe,KAAKA,EAAQH,CAAG,EAAG,SAExD,IAAMH,EAAQM,EAAOH,CAAG,EAEpB,OAAOH,GAAU,UACnBC,EAAiBD,EAAOG,CAAG,EAC3B2B,EAAO3B,CAAG,EACRY,EAAUf,CAAK,EAAIyB,EAAgBzB,CAAK,EAAIoB,EAAepB,CAAK,GAEzDD,EAAaC,CAAK,IAC3B8B,EAAO3B,CAAG,EAAI0B,EACZ7B,CACF,EAEJ,CAEA,OAAO8B,CACT,CAEO,SAASC,EACdzB,EACmB,CACnB,IAAMwB,EAASD,EAAgBvB,CAAM,EACrC,OAAAD,EAAqByB,CAA4C,EAC1DA,CACT","names":["isRouteGroup","value","validateTemplate","template","key","devWarn","detectDuplicatePaths","routes","seen","warned","flat","flattenRoutes","route","existing","i","r1","isDynamic","j","r2","isActivePath","pairKey","wrapStaticPath","wrapped","query","options","buildPath","wrapDynamicPath","paramNames","extractParamNames","params","processRouteMap","result","defineRoutes"]}
package/package.json CHANGED
@@ -1,27 +1,66 @@
1
1
  {
2
2
  "name": "react-routes-forge",
3
- "version": "1.2.0",
4
- "description": "Type-safe route definitions with automatic path builders for React apps",
3
+ "version": "1.3.0",
4
+ "description": "Type-safe route definitions, automatic path builders, query parameter handling, and active route matching for React applications with zero duplication.",
5
5
  "type": "module",
6
6
  "author": {
7
7
  "name": "Mostafa Abdelhamid",
8
- "email": "mhsmustafa84@gmail.com"
8
+ "email": "mhsmustafa84@gmail.com",
9
+ "url": "https://github.com/mhsmustafa84"
9
10
  },
10
11
  "license": "MIT",
11
12
  "repository": {
12
13
  "type": "git",
13
- "url": "https://github.com/mhsmustafa84/react-routes-forge.git"
14
+ "url": "git+https://github.com/mhsmustafa84/react-routes-forge.git"
14
15
  },
15
- "homepage": "https://github.com/mhsmustafa84/react-routes-forge#readme",
16
- "bugs": "https://github.com/mhsmustafa84/react-routes-forge/issues",
16
+ "bugs": {
17
+ "url": "https://github.com/mhsmustafa84/react-routes-forge/issues"
18
+ },
19
+ "homepage": "https://mhsmustafa84.github.io/react-routes-forge",
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "require": "./dist/index.cjs"
28
+ },
29
+ "./hooks": {
30
+ "types": "./dist/hooks/index.d.ts",
31
+ "import": "./dist/hooks/index.js",
32
+ "require": "./dist/hooks/index.cjs"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
17
41
  "keywords": [
18
42
  "react",
19
43
  "router",
20
44
  "routes",
45
+ "routing",
21
46
  "typescript",
22
47
  "type-safe",
48
+ "typesafe",
49
+ "type-inference",
23
50
  "react-router",
24
- "path-builder"
51
+ "react-router-dom",
52
+ "path-builder",
53
+ "route-builder",
54
+ "typed-routes",
55
+ "url-builder",
56
+ "link-builder",
57
+ "navigation",
58
+ "query-params",
59
+ "search-params",
60
+ "url-params",
61
+ "deep-linking",
62
+ "spa",
63
+ "frontend"
25
64
  ],
26
65
  "publishConfig": {
27
66
  "access": "public"
@@ -30,27 +69,19 @@
30
69
  "node": ">=18.0.0"
31
70
  },
32
71
  "sideEffects": false,
33
- "exports": {
34
- ".": {
35
- "import": "./dist/index.js",
36
- "types": "./dist/index.d.ts"
37
- },
38
- "./package.json": "./package.json"
39
- },
40
- "types": "./dist/index.d.ts",
41
- "files": [
42
- "dist",
43
- "README.md",
44
- "LICENSE"
45
- ],
46
72
  "scripts": {
47
- "build": "tsup src/index.ts --format esm --dts --minify --sourcemap --clean",
73
+ "build": "tsup src/index.ts src/hooks/index.ts --format esm,cjs --dts --minify --sourcemap --clean",
48
74
  "lint": "tsc --noEmit",
75
+ "lint:eslint": "eslint . --ext .js,.cjs,.ts,.tsx",
49
76
  "test": "bun test",
50
77
  "test:watch": "bun test --watch",
78
+ "test:coverage": "vitest run --coverage",
51
79
  "prepare": "husky",
52
80
  "prepublishOnly": "bun run test && bun run build",
53
- "release": "standard-version"
81
+ "release": "standard-version",
82
+ "docs:dev": "vitepress dev docs",
83
+ "docs:build": "vitepress build docs",
84
+ "docs:preview": "vitepress preview docs"
54
85
  },
55
86
  "peerDependencies": {
56
87
  "react": ">=17",
@@ -64,17 +95,22 @@
64
95
  "devDependencies": {
65
96
  "@commitlint/cli": "^21.0.2",
66
97
  "@commitlint/config-conventional": "^21.0.2",
98
+ "@microsoft/eslint-formatter-sarif": "^3.1.0",
67
99
  "@testing-library/react": "^16.3.2",
68
- "@types/bun": "^1.3.14",
69
100
  "@types/jsdom": "^28.0.3",
70
101
  "@types/node": "^26.1.1",
71
102
  "@types/react": "^19.2.17",
103
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
104
+ "@typescript-eslint/parser": "^8.0.0",
105
+ "@vitest/coverage-v8": "^4.1.10",
106
+ "eslint": "^8.57.0",
72
107
  "husky": "^9.0.0",
73
108
  "jsdom": "^29.1.1",
74
109
  "react-router-dom": "^6.0.0",
75
110
  "standard-version": "^9.5.0",
76
111
  "tsup": "^8.0.0",
77
112
  "typescript": "^5.0.0",
113
+ "vitepress": "^1.6.4",
78
114
  "vitest": "^4.1.10"
79
115
  }
80
116
  }