react-routes-forge 1.1.0 → 1.1.3
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/README.md +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ One source of truth for your routes — templates for `<Route path={...} />` and
|
|
|
6
6
|
|
|
7
7
|
[](#license)
|
|
8
8
|
[](#)
|
|
9
|
+
[](#requirements)
|
|
10
|
+
[](https://github.com/mhsmustafa84/react-routes-forge/actions/workflows/ci-security.yml)
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ type RouteMap = {
|
|
|
25
25
|
* Extracts param names from a path template string.
|
|
26
26
|
* e.g. '/users/:id/posts/:postId' → 'id' | 'postId'
|
|
27
27
|
*/
|
|
28
|
-
type
|
|
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;
|
|
29
30
|
/**
|
|
30
31
|
* Builds a params object type from a path template.
|
|
31
32
|
* e.g. '/users/:id' → { id: RouteParam }
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var l=()=>/:([^/]+)/g,x=()=>/[.*+?^${}()|[\]\\]/g;function f(t){return t.replace(x(),"\\$&")}function R(t){return f(t).replace(l(),"([^/]+)")}function P(t,e){if(!e)return t;let r=new URLSearchParams;for(let[s,
|
|
1
|
+
var l=()=>/:([^/]+)/g,x=()=>/[.*+?^${}()|[\]\\]/g;function f(t){return t.replace(x(),"\\$&")}function R(t){return f(t).replace(l(),"([^/]+)")}function P(t,e){if(!e)return t;let r=new URLSearchParams;for(let[s,n]of Object.entries(e))n!=null&&(Array.isArray(n)?n.forEach(i=>{i!=null&&r.append(s,String(i))}):r.append(s,String(n)));let a=r.toString();return a?t+(t.includes("?")?"&":"?")+a:t}function p(t,e,r,a){let s=u(t),n=s.filter(o=>e[o]===void 0||e[o]===null),i=s.reduce((o,c)=>{let m=e[c],h=m==null?`:${c}`:String(m);return o.replace(new RegExp(`:${f(c)}\\??(?=/|$)`,"g"),h)},t);if(n.length>0){if(a?.strict)throw new RangeError(`[route-forge] Missing required param(s) ${n.map(c=>`":${c}"`).join(", ")} in template "${t}".`);globalThis.process?.env?.NODE_ENV!=="production"&&console.warn(`[route-forge] Unresolved params in path "${i}". Check that all :param segments have matching keys.`)}return P(i,r)}function u(t){return[...t.matchAll(l())].map(e=>e[1].replace(/\?$/,""))}function g(t){return l().test(t)}function T(t,e,r={exact:!0}){let a=t.split("?")[0]??"",s=R(e);return(r.exact?new RegExp(`^${s}$`):new RegExp(`^${s}`)).test(a)}function v(t,e){let r=e.split("?")[0]??"",a=u(t),s=new RegExp(`^${R(t)}$`),n=r.match(s);return n?Object.fromEntries(a.map((i,o)=>[i,n[o+1]??""])):{}}function b(...t){return"/"+t.map(e=>e.replace(/^\/+|\/+$/g,"")).filter(Boolean).join("/")}function E(t,e,r,a){return p(t,e,r,a)}function N(t){return u(t)}function d(t,e=""){let r=[];for(let a of Object.keys(t)){let s=e?`${e}.${a}`:a,n=t[a];typeof n=="string"?r.push({key:s,path:n}):n instanceof String?r.push({key:s,path:n.valueOf()}):typeof n=="object"&&n!==null&&r.push(...d(n,s))}return r}var O=t=>typeof t=="object"&&t!==null;function $(t){let e=u(t),r=new String(t);return r.build=(a,s,n)=>p(t,a,s,n),r.paramNames=e,r}function y(t){let e={};for(let r in t){if(!Object.prototype.hasOwnProperty.call(t,r))continue;let a=t[r];typeof a=="string"?e[r]=g(a)?$(a):a:O(a)&&(e[r]=y(a))}return e}function k(t){return y(t)}import{useParams as w,useNavigate as A,generatePath as Q}from"react-router-dom";function j(){return w()}function B(){let t=A();return(e,r)=>{t(e,r)}}function S(t,e,r,a){if(!u(t).every(o=>e[o]!==void 0&&e[o]!==null))return p(t,e,r,a);let i=Q(t,Object.fromEntries(Object.entries(e).map(([o,c])=>[o,String(c)])));return P(i,r)}export{E as build,p as buildPath,k as defineRoutes,u as extractParamNames,v as extractParamsFromPath,d as flattenRoutes,N as getParamNames,T as isActivePath,g as isDynamic,b as joinPaths,B as useNavigateTo,S as useResolvedPath,j as useRouteParams};
|
|
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 { 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): string {\n if (!query) return path;\n\n const searchParams = new URLSearchParams();\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 const queryString = searchParams.toString();\n if (!queryString) return path;\n\n return path + (path.includes(\"?\") ? \"&\" : \"?\") + queryString;\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);\n}\n\nexport function extractParamNames(template: string): string[] {\n return [...template.matchAll(PATH_PARAM_RE())].map(\n (match) => match[1] as string,\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 pattern = createTemplatePattern(template);\n const regex = options.exact\n ? new RegExp(`^${pattern}$`)\n : new RegExp(`^${pattern}`);\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 regex = new RegExp(`^${createTemplatePattern(template)}$`);\n const match = pathWithoutSearch.match(regex);\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 return (\n \"/\" +\n segments\n .map((segment) => segment.replace(/^\\/+|\\/+$/g, \"\"))\n .filter(Boolean)\n .join(\"/\")\n );\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 * 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\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);\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,EAA6B,CACrE,GAAI,CAACA,EAAO,OAAOD,EAEnB,IAAME,EAAe,IAAI,gBACzB,OAAW,CAACC,EAAKP,CAAK,IAAK,OAAO,QAAQK,CAAK,EAClBL,GAAU,OACjC,MAAM,QAAQA,CAAK,EACrBA,EAAM,QAASQ,GAAM,CACIA,GAAM,MAAMF,EAAa,OAAOC,EAAK,OAAOC,CAAC,CAAC,CACvE,CAAC,EAEDF,EAAa,OAAOC,EAAK,OAAOP,CAAK,CAAC,GAI1C,IAAMS,EAAcH,EAAa,SAAS,EAC1C,OAAKG,EAEEL,GAAQA,EAAK,SAAS,GAAG,EAAI,IAAM,KAAOK,EAFxBL,CAG3B,CAEO,SAASM,EACdR,EACAS,EACAN,EACAO,EACQ,CACR,IAAMC,EAAaC,EAAkBZ,CAAQ,EACvCa,EAAaF,EAAW,OAC3BG,GAASL,EAAOK,CAAI,IAAM,QAAaL,EAAOK,CAAI,IAAM,IAC3D,EAEMC,EAAWJ,EAAW,OAAO,CAACT,EAAMY,IAAS,CACjD,IAAMhB,EAAQW,EAAOK,CAAI,EACnBE,EAAqClB,GAAU,KAAO,IAAIgB,CAAI,GAAK,OAAOhB,CAAK,EACrF,OAAOI,EAAK,QACV,IAAI,OAAO,IAAIL,EAAYiB,CAAI,CAAC,UAAW,GAAG,EAC9CE,CACF,CACF,EAAGhB,CAAQ,EAEX,GAAIa,EAAW,OAAS,EAAG,CACzB,GAAIH,GAAS,OACX,MAAM,IAAI,WACR,2CAA2CG,EAAW,IAAKI,GAAM,KAAKA,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,iBAAiBjB,CAAQ,IACjH,EAIA,WAKA,SAEkB,KAAK,WAAa,cACpC,QAAQ,KACN,4CAA4Ce,CAAQ,uDAEtD,CAEJ,CAEA,OAAOd,EAAYc,EAAUZ,CAAK,CACpC,CAEO,SAASS,EAAkBZ,EAA4B,CAC5D,MAAO,CAAC,GAAGA,EAAS,SAASL,EAAc,CAAC,CAAC,EAAE,IAC5CuB,GAAUA,EAAM,CAAC,CACpB,CACF,CAEO,SAASC,EAAUjB,EAAuB,CAC/C,OAAOP,EAAc,EAAE,KAAKO,CAAI,CAClC,CAEO,SAASkB,EACdC,EACArB,EACAU,EAA+B,CAAE,MAAO,EAAK,EACpC,CACT,IAAMY,EAAoBD,EAAY,MAAM,GAAG,EAAE,CAAC,GAAK,GACjDE,EAAUxB,EAAsBC,CAAQ,EAK9C,OAJcU,EAAQ,MAClB,IAAI,OAAO,IAAIa,CAAO,GAAG,EACzB,IAAI,OAAO,IAAIA,CAAO,EAAE,GAEf,KAAKD,CAAiB,CACrC,CAEO,SAASE,EACdxB,EACAyB,EACwB,CACxB,IAAMH,EAAoBG,EAAa,MAAM,GAAG,EAAE,CAAC,GAAK,GAClDd,EAAaC,EAAkBZ,CAAQ,EACvC0B,EAAQ,IAAI,OAAO,IAAI3B,EAAsBC,CAAQ,CAAC,GAAG,EACzDkB,EAAQI,EAAkB,MAAMI,CAAK,EAE3C,OAAKR,EAEE,OAAO,YACZP,EAAW,IAAI,CAACG,EAAMa,IAAU,CAACb,EAAMI,EAAMS,EAAQ,CAAC,GAAK,EAAE,CAAC,CAChE,EAJmB,CAAC,CAKtB,CAEO,SAASC,KAAaC,EAA4B,CACvD,MACE,IACAA,EACG,IAAKC,GAAYA,EAAQ,QAAQ,aAAc,EAAE,CAAC,EAClD,OAAO,OAAO,EACd,KAAK,GAAG,CAEf,CAEO,SAASC,EACd/B,EACAS,EACAN,EACAO,EACQ,CACR,OAAOF,EAAUR,EAAUS,EAAQN,EAAOO,CAAO,CACnD,CAEO,SAASsB,EAAchC,EAA4B,CACxD,OAAOY,EAAkBZ,CAAQ,CACnC,CAkBO,SAASiC,EACdC,EACAC,EAAS,GACI,CACb,IAAMC,EAAuB,CAAC,EAE9B,QAAW/B,KAAO,OAAO,KAAK6B,CAAM,EAAG,CACrC,IAAMG,EAAUF,EAAS,GAAGA,CAAM,IAAI9B,CAAG,GAAKA,EACxCP,EAAQoC,EAAO7B,CAAG,EAEpB,OAAOP,GAAU,SAEnBsC,EAAQ,KAAK,CAAE,IAAKC,EAAS,KAAMvC,CAAM,CAAC,EACjCA,aAAiB,OAE1BsC,EAAQ,KAAK,CAAE,IAAKC,EAAS,KAAMvC,EAAM,QAAQ,CAAE,CAAC,EAC3C,OAAOA,GAAU,UAAYA,IAAU,MAEhDsC,EAAQ,KACN,GAAGH,EAAcnC,EAAkCuC,CAAO,CAC5D,CAGJ,CAEA,OAAOD,CACT,CC3JA,IAAME,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,CAAK,CAChC","names":["PATH_PARAM_RE","ESCAPE_RE","escapeRegex","value","createTemplatePattern","template","appendQuery","path","query","searchParams","key","v","queryString","buildPath","params","options","paramNames","extractParamNames","unresolved","name","resolved","replacement","p","match","isDynamic","isActivePath","currentPath","pathWithoutSearch","pattern","extractParamsFromPath","resolvedPath","regex","index","joinPaths","segments","segment","build","getParamNames","flattenRoutes","routes","prefix","entries","fullKey","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/utils.ts","../src/core/defineRoutes.ts","../src/hooks/index.ts"],"sourcesContent":["import type { 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): string {\n if (!query) return path;\n\n const searchParams = new URLSearchParams();\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 const queryString = searchParams.toString();\n if (!queryString) return path;\n\n return path + (path.includes(\"?\") ? \"&\" : \"?\") + queryString;\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);\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 pattern = createTemplatePattern(template);\n const regex = options.exact\n ? new RegExp(`^${pattern}$`)\n : new RegExp(`^${pattern}`);\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 regex = new RegExp(`^${createTemplatePattern(template)}$`);\n const match = pathWithoutSearch.match(regex);\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 return (\n \"/\" +\n segments\n .map((segment) => segment.replace(/^\\/+|\\/+$/g, \"\"))\n .filter(Boolean)\n .join(\"/\")\n );\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 * 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\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);\n}\n"],"mappings":"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","names":["PATH_PARAM_RE","ESCAPE_RE","escapeRegex","value","createTemplatePattern","template","appendQuery","path","query","searchParams","key","v","queryString","buildPath","params","options","paramNames","extractParamNames","unresolved","name","resolved","replacement","p","match","isDynamic","isActivePath","currentPath","pathWithoutSearch","pattern","extractParamsFromPath","resolvedPath","regex","index","joinPaths","segments","segment","build","getParamNames","flattenRoutes","routes","prefix","entries","fullKey","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"]}
|