@akb2/react-router 1.0.0 → 1.1.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.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var J=Object.create;var K=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var Z=Object.getPrototypeOf,V=Object.prototype.hasOwnProperty;var tt=(t,o)=>{for(var e in o)K(t,e,{get:o[e],enumerable:!0})},B=(t,o,e,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of H(o))!V.call(t,r)&&r!==e&&K(t,r,{get:()=>o[r],enumerable:!(n=z(o,r))||n.enumerable});return t};var ot=(t,o,e)=>(e=t!=null?J(Z(t)):{},B(o||!t||!t.__esModule?K(e,"default",{value:t,enumerable:!0}):e,t)),et=t=>B(K({},"__esModule",{value:!0}),t);var st={};tt(st,{routerContextFabric:()=>Y});module.exports=et(st);var p=ot(require("react"),1);var h=require("@akb2/types-tools");var w="@akb2/react/";var k=Symbol.for(w+"router-context");var U=require("react"),v=require("wouter");var _=(t,o,e)=>{let n=t.split("/"),r=o.split("/");if(n.length!==r.length)return!1;for(let i=0;i<n.length;i++)if(r[i].startsWith(":")){e[r[i].slice(1)]=n[i];continue}else if(n[i]!==r[i])return!1;return!0};var j=require("@akb2/types-tools");var T=(t,o,e=!1)=>{let r=(0,j.anyToArray)(t).reduce((C,a)=>{let m=a.split("/");for(let S of m)C.push(S.startsWith(":")&&!e?(0,j.anyToString)(o?.[S.slice(1)],"*"):S);return C},[]);return("/"+(0,j.smartJoin)("/",...r)).replace(/[\/\\]+/gim,"/")};var A=t=>Array.isArray(t)?t:t.split(".");var E=require("@akb2/types-tools"),N=t=>Object.entries(t??{}).filter(([,o])=>(0,E.isDefined)(o)).reduce((o,[e,n])=>o+`${o?"&":"?"}${encodeURIComponent(e)}=${encodeURIComponent((0,E.anyToString)(n))}`,"");var Q=(t,o,e,n,r=!1)=>{let i=A(o),C=[],a=t;for(let m of i){if(!a.hasOwnProperty(m))break;a=a[m],C.push(a?.path??a?.paths[0]),a=a.children}return T(C,e,r)+N(n)};var q=(t,o,e,n,r=!1)=>{let i=N(n),C=A(o),a=[],m=t;for(let s of C){if(!m.hasOwnProperty(s))break;m=m[s];let u=Array.isArray(m?.paths)?m.paths:[m?.path];a.push(u.filter(y=>typeof y=="string")),m=m.children}return a.length===0?[T([])]:a.reduce((s,u)=>s.flatMap(y=>u.map(l=>[...y,l])),[[]]).map(s=>T(s,e,r)+i)};var I=t=>Array.from(t).reduce((o,[e,n])=>(o[e]=n,o),{});var M=require("react"),W=(t,o=null)=>{let e=globalThis;return t in e||(e[t]=(0,M.createContext)(o)),e[t]};var G=require("@akb2/types-tools"),X=t=>{if(!t)return{};let o=(0,G.anyToArray)(t.split("&")),e={};for(let n of o){let[r,i]=n.split("=");r&&(e[r]=i??"")}return e};var D=require("react/jsx-runtime"),Y=t=>{let o=W(k),e=({children:s})=>(0,D.jsxs)(o.Provider,{value:t,children:[" ",s," "]}),n=Q.bind(null,t),r=q.bind(null,t),i=s=>(0,v.useRoute)(n(s,null,{},!0)),C=()=>{let[s,u]=(0,v.useLocation)();return[s,(l,f,R,d)=>{let c=r(l,f,R,!0),g=c.find(O=>{let L=O.split("/").filter(Boolean);return Object.keys(f??{}).every($=>L.includes(":"+$))})??c[0],[b,P]=T(g,f,!1).split("?"),F=X(P),x=b+N({...F,...R});return u(x,d)}]},a=()=>{let[s]=C(),[u]=(0,v.useSearchParams)(),y=(0,U.useMemo)(()=>{let l=s.split("/").filter(Boolean);return l.length>0?l:[""]},[s]);return(0,U.useMemo)(()=>{let l=[],f=[],R={},d=I(u.entries()),c=null,g=t,b="";for(let P of y){let F=b?b+"/"+P:P,x=Object.entries(g).filter(([,O])=>(0,h.isDefined)(O.path)?_(F,(0,h.anyToString)(O.path),R):(0,h.isDefined)(O.paths)?(0,h.anyToArray)(O.paths).some(L=>_(F,(0,h.anyToString)(L),R)):!1);if(x.length>1)throw new Error("Error in routes config: you have routes with similar pathes");x.length===0?b=b?b+"/"+P:P:((0,h.isDefined)(c)&&f.push(c),b="",c=x[0][1],g=c.children??{},l.push(x[0][0]))}return(b||!(0,h.isDefined)(c))&&(c=t.notFound),{activatedRoute:c,routeKey:l,parents:f,pathParams:R,queryParams:d}},[y,u])};return{RouterContext:o,RouterProvider:e,createUrlFromRouteConfig:n,createUrlsFromRouteConfig:r,useLocation:C,useActivatedRoute:a,useRoute:i,useRoutePathState:(s,u)=>{let[,y]=C(),{routeKey:l,pathParams:f,queryParams:R}=a(),d=f[s],c=(0,U.useCallback)(g=>{d!==g&&y(l,{...f,[s]:g},R,{replace:!0})},[d,l,f,R]);return[d??u,c]},useRouteQueryState:(s,u)=>{let[,y]=C(),{routeKey:l,pathParams:f,queryParams:R}=a(),d=R[s]??u,c=(0,U.useCallback)(g=>{if(d!==g){let b={...R};(0,h.isDefined)(g)&&g!==u&&(0,h.anyToString)(g).length>0?b[s]=(0,h.anyToString)(g):delete b[s],y(l,f,b,{replace:!0})}},[d,l,f,R,u]);return[d??u,c]}}};0&&(module.exports={routerContextFabric});
1
+ "use strict";var pt=Object.create;var k=Object.defineProperty;var ft=Object.getOwnPropertyDescriptor;var dt=Object.getOwnPropertyNames;var Rt=Object.getPrototypeOf,Ct=Object.prototype.hasOwnProperty;var ht=(t,o)=>{for(var e in o)k(t,e,{get:o[e],enumerable:!0})},z=(t,o,e,r)=>{if(o&&typeof o=="object"||typeof o=="function")for(let a of dt(o))!Ct.call(t,a)&&a!==e&&k(t,a,{get:()=>o[a],enumerable:!(r=ft(o,a))||r.enumerable});return t};var gt=(t,o,e)=>(e=t!=null?pt(Rt(t)):{},z(o||!t||!t.__esModule?k(e,"default",{value:t,enumerable:!0}):e,t)),bt=t=>z(k({},"__esModule",{value:!0}),t);var Et={};ht(Et,{componentsContextFabric:()=>$,reactRouterFabric:()=>et,routerContextFabric:()=>mt});module.exports=bt(Et);var h=gt(require("react"),1);var B="@akb2/react/";var H=Symbol.for(B+"components-context");var V=require("react"),D=(t,o=null)=>{let e=globalThis;return t in e||(e[t]=(0,V.createContext)(o)),e[t]};var Z=require("react"),tt=require("react/jsx-runtime"),$=()=>{let t=D(H);return{ComponentsContext:t,ComponentsProvider:({components:r,children:a})=>(0,tt.jsx)(t.Provider,{value:r,children:a}),useComponents:()=>{let r=(0,Z.useContext)(t);if(!r)throw new Error("useComponents must be used inside ComponentsProvider");return r}}};var N=require("@akb2/types-tools");var W=require("@akb2/types-tools"),ot=t=>{let o=(0,W.isDefined)(t.children)?{...t.children}:void 0;return(0,W.isDefined)(o)?{...t,children:o}:{...t}};function et(...t){let o=Array.isArray(t[0])?t[0]:t,e={},r=[],a=100;for(let d of o)for(let s in d){let l=d[s];if(!(0,N.isDefined)(l.putInKey)&&(0,N.isDefined)(e[s]))throw new Error(`Duplicate route key "${s}" found in module options.`);(0,N.isDefined)(l.putInKey)?r.push([s,l]):e[s]=l}for(;r.length>0&&a-- >0;){let d=r.findIndex(([,{putInKey:i}])=>(0,N.isDefined)(i)&&(0,N.isDefined)(e[i]));if(d===-1)throw new Error('Circular dependency detected in route configurations. Please check the "putInKey" properties.');let[s,l]=r[d],{putInKey:m,...P}=l;if(!(0,N.isDefined)(m))throw new Error('Invalid route configuration. "putInKey" is not defined.');let x=e[m],w=(0,N.isDefined)(x.children)?{...x.children}:{};if((0,N.isDefined)(w[s]))throw new Error(`Duplicate route key "${s}" found in module options.`);w[s]=ot(P),e[m]={...x,children:w},r.splice(d,1)}return e}var u=require("@akb2/types-tools");var rt=Symbol.for(B+"router-context");var nt=(t,o,e)=>{let r={...e},a=t.split("/"),d=o.split("/");if(a.length!==d.length)return!1;for(let s=0;s<a.length;s++)if(d[s].startsWith(":")){r[d[s].slice(1)]=a[s];continue}else if(a[s]!==d[s])return!1;return Object.assign(e,r),!0};var K=require("@akb2/types-tools");var S=(t,o,e=!1)=>{let a=(0,K.anyToArray)(t).reduce((s,l)=>{let m=l.split("/");for(let P of m)s.push(P.startsWith(":")&&!e?(0,K.anyToString)(o?.[P.slice(1)],"*"):P);return s},[]);return("/"+(0,K.smartJoin)("/",...a)).replace(/[\/\\]+/gim,"/")};var Q=t=>Array.isArray(t)?t:t.split(".");var X=require("@akb2/types-tools"),A=t=>Object.entries(t??{}).filter(([,o])=>(0,X.isDefined)(o)).reduce((o,[e,r])=>o+`${o?"&":"?"}${encodeURIComponent(e)}=${encodeURIComponent((0,X.anyToString)(r))}`,"");var st=(t,o,e,r,a=!1)=>{let d=Q(o),s=[],l=t;for(let m of d){if(!l.hasOwnProperty(m))break;l=l[m],s.push(l?.path??l?.paths[0]),l=l.children}return S(s,e,a)+A(r)};var it=(t,o,e,r,a=!1)=>{let d=A(r),s=Q(o),l=[],m=t;for(let x of s){if(!m.hasOwnProperty(x))break;m=m[x];let w=Array.isArray(m?.paths)?m.paths:[m?.path];l.push(w.filter(i=>typeof i=="string")),m=m.children}return l.length===0?[S([])]:l.reduce((x,w)=>x.flatMap(i=>w.map(R=>[...i,R])),[[]]).map(x=>S(x,e,a)+d)};var at=t=>Array.from(t).reduce((o,[e,r])=>(o[e]=r,o),{});var ut=require("@akb2/types-tools"),ct=t=>{if(!t)return{};let o=(0,ut.anyToArray)(t.split("&")),e={};for(let r of o){let[a,d]=r.split("=");a&&(e[a]=d??"")}return e};var j=require("react"),F=require("wouter");var U=require("react/jsx-runtime"),mt=t=>{let o=(i,R)=>{let g=i.split("/").filter(Boolean),p=R.split("/").filter(Boolean);for(let c=0;c<g.length;c++){let f=g[c].startsWith(":"),n=p[c].startsWith(":");if(f!==n)return f?1:-1}return 0},e=(i,R=[""],g=new Set)=>{if(R.length===0)throw new Error("Parent paths cannot be empty.");let p=g.size===0,c=!1;for(let f in i){let n=i[f];if([(0,u.isDefined)(n.Module),(0,u.isDefined)(n.Template),(0,u.isDefined)(n.redirectUrl)].filter(Boolean).length!==1)throw new Error(`Route with key "${f}" must have exactly one of Module, Template, or redirectUrl defined.`);if([(0,u.isDefined)(n.path),(0,u.isDefined)(n.paths),n.isNotFound===!0].filter(Boolean).length!==1)throw new Error(`Route with key "${f}" must have exactly one of path, paths, or isNotFound defined.`);if(n.isNotFound===!0)if(p){if(c)throw new Error("Only one not found route can be defined at the top level.");if((0,u.isDefined)(n.children))throw new Error("Not found route cannot have children.");c=!0;continue}else throw new Error("Not found route can only be defined at the top level.");let y=(0,u.isDefined)(n.path)?[n.path]:(0,u.isDefined)(n.paths)?n.paths:[];if(y.length===0)throw new Error(`Route with key "${f}" must have at least one path defined.`);let b=R.flatMap(E=>y.map(v=>S([E,v])));for(let E of b){if(g.has(E))throw new Error(`Duplicate route detected for URL: "${E}"`);g.add(E)}(0,u.isDefined)(n.children)&&e(n.children,b,g)}if(p&&!c)throw new Error("A not found route must be defined at the top level.")};e(t);let r=D(rt),a=()=>{let{activatedRoute:{Module:i,Template:R,redirectUrl:g,SuspenseTemplate:p,props:c}}=P(),{useComponents:f}=$(),{FullPageLoader:n}=f(),T=(0,u.isDefined)(i)?i:(0,u.isDefined)(R)?R:()=>(0,U.jsx)(F.Redirect,{to:g}),C=p?(0,U.jsx)(p,{}):(0,U.jsx)(n,{});return(0,U.jsx)(r.Provider,{value:t,children:(0,U.jsx)(j.Suspense,{fallback:C,children:(0,U.jsx)(T,{...c})})})},d=st.bind(null,t),s=it.bind(null,t),l=i=>(0,F.useRoute)(d(i,null,{},!0)),m=()=>{let[i,R]=(0,F.useLocation)();return[i,(p,c,f,n)=>{let T=s(p,c,f,!0),C=T.find(q=>{let L=q.split("/").filter(Boolean);return Object.keys(c??{}).every(_=>L.includes(":"+_))})??T[0],[y,b]=S(C,c,!1).split("?"),E=ct(b),v=y+A({...E,...f});return R(v,n)}]},P=()=>{let[i]=m(),[R]=(0,F.useSearchParams)(),g=(0,j.useMemo)(()=>{let p=i.split("/").filter(Boolean);return p.length>0?p:[""]},[i]);return(0,j.useMemo)(()=>{let p=[],c=[],f={},n=at(R.entries()),T=null,C=null,y=t,b="",E=0;for(let v of g){let q=b?b+"/"+v:v,L=new Map;for(let _ in y){let O=y[_];if(O.isNotFound===!0){if(E>0)throw new Error("Not found route must be defined at the root level");T=O;continue}((0,u.isDefined)(O.path)?[O.path]:(0,u.isDefined)(O.paths)?(0,u.anyToArray)(O.paths):[]).forEach((M,G)=>{let I={};nt(q,M,I)&&L.set((0,u.smartJoin)("/",_,(0,u.anyToString)(G)),{path:M,config:O,pathParams:I})})}if(L.size>0){let O=Array.from(L.entries()).reduce((I,J)=>o(J[1].path,I[1].path)<0?J:I),[Y,{config:M,pathParams:G}]=O;(0,u.isDefined)(C)&&c.push(C),b="",C=M,y=C.children??{},Object.assign(f,G),p.push(Y.split("/")[0]),E++}else b=b?b+"/"+v:v}return(b||!(0,u.isDefined)(C))&&(C=T),{activatedRoute:C,routeKey:p,parents:c,pathParams:f,queryParams:n,routeKeyString:p.join("."),routeUrlMask:b}},[g,R])};return{RouterContext:r,RouterProvider:a,createUrlFromRouteConfig:d,createUrlsFromRouteConfig:s,useLocation:m,useActivatedRoute:P,useRoute:l,useRoutePathState:(i,R)=>{let[,g]=m(),{routeKey:p,pathParams:c,queryParams:f}=P(),n=c[i],T=(0,j.useCallback)(C=>{n!==C&&g(p,{...c,[i]:C},f,{replace:!0})},[n,p,c,f]);return[n??R,T]},useRouteQueryState:(i,R)=>{let[,g]=m(),{routeKey:p,pathParams:c,queryParams:f}=P(),n=f[i]??R,T=(0,j.useCallback)(C=>{if(n!==C){let y={...f};(0,u.isDefined)(C)&&C!==R&&(0,u.anyToString)(C).length>0?y[i]=(0,u.anyToString)(C):delete y[i],g(p,c,y,{replace:!0})}},[n,p,c,f,R]);return[n??R,T]}}};0&&(module.exports={componentsContextFabric,reactRouterFabric,routerContextFabric});
package/.dist/index.d.cts CHANGED
@@ -1,10 +1,30 @@
1
- import { MemoExoticComponent, LazyExoticComponent, FC, PropsWithChildren, Context } from 'react';
2
1
  import { Nullable, CustomObject, Undefinable } from '@akb2/types-tools';
2
+ import { ReactNode, FC, PropsWithChildren, Context, MemoExoticComponent, LazyExoticComponent } from 'react';
3
3
  import { Match } from 'wouter';
4
4
 
5
- declare const TREE_CHILD_KEY: "children";
5
+ type AnyComponent = (props: any) => ReactNode;
6
6
 
7
- type CanBeMemoExoticComponent<C extends JSX.Element | string | number> = MemoExoticComponent<() => C> | (() => C);
7
+ type ReactComponents<Props extends object = Record<string, object>> = {
8
+ [K in keyof Props]: Props[K] extends AnyComponent ? Props[K] : FC<Props[K]>;
9
+ };
10
+
11
+ interface RouterComponents {
12
+ FullPageLoader: {};
13
+ }
14
+
15
+ interface ComponentsProviderProps<TComponents extends ReactComponents<RouterComponents> = ReactComponents<RouterComponents>> extends PropsWithChildren {
16
+ components: TComponents;
17
+ }
18
+
19
+ interface CreateComponentsContext<TComponents extends ReactComponents<RouterComponents> = ReactComponents<RouterComponents>> {
20
+ ComponentsContext: Context<Nullable<TComponents>>;
21
+ ComponentsProvider: FC<ComponentsProviderProps<TComponents>>;
22
+ useComponents: () => TComponents;
23
+ }
24
+
25
+ declare const componentsContextFabric: <TComponents extends ReactComponents<RouterComponents>>() => CreateComponentsContext<TComponents>;
26
+
27
+ declare const TREE_CHILD_KEY: "children";
8
28
 
9
29
  type TreeChildKey = typeof TREE_CHILD_KEY;
10
30
 
@@ -27,6 +47,35 @@ type RouteConfigInput = {
27
47
  * General types
28
48
  */
29
49
  type RoutesConfig<T extends Record<string, RouteConfigInput> = Record<string, RouteConfigInput>> = T;
50
+ type RoutesConfigKey<TConfig extends RoutesConfig> = Extract<keyof TConfig, string>;
51
+
52
+ type CanBeMemoExoticComponent<C extends JSX.Element | string | number> = MemoExoticComponent<() => C> | (() => C);
53
+
54
+ type RoutesDefinition<T extends Record<string, RouteDefinition<object, string>> = Record<string, RouteDefinition<{}, string>>> = T;
55
+
56
+ /**
57
+ * Config interface
58
+ */
59
+ interface RouteConfigAndDefinitionBase<Props extends object = {}> {
60
+ SuspenseTemplate?: CanBeMemoExoticComponent<JSX.Element | string | number>;
61
+ props?: Props;
62
+ putInKey?: string;
63
+ }
64
+ /**
65
+ * Children interface
66
+ */
67
+ interface RouteDefinitionBase<TRouteChildren extends Record<string, RouteDefinition> = Record<string, RouteDefinition>, Props extends object = {}> extends RouteConfigAndDefinitionBase<Props> {
68
+ [TREE_CHILD_KEY]?: RoutesDefinition<TRouteChildren>;
69
+ }
70
+ /**
71
+ * For extension
72
+ */
73
+ interface RouteDefinitionExtension {
74
+ }
75
+ /**
76
+ * Base type
77
+ */
78
+ type RouteDefinition<Props extends object = {}, Keys extends string = string> = RouteRenderConfig<Props> & RouteDefinitionBase<Record<Keys, RouteDefinition>, Props> & RouteDefinitionExtension;
30
79
 
31
80
  /**
32
81
  * Base models
@@ -81,32 +130,48 @@ interface RouteConfigExtension {
81
130
  * General model
82
131
  */
83
132
  type RouteConfig<Props extends object = {}, Keys extends string = string> = RouteConfigPath & RouteRenderConfig & RouteConfigBase<Record<Keys, RouteConfig<Props>>, Props> & RouteConfigExtension;
133
+ type RouteConfigChildren<TConfig extends RouteConfigInput> = TreeChildKey extends keyof TConfig ? TConfig[TreeChildKey] extends RoutesConfig<infer TChildren extends Record<string, RouteConfigInput>> ? TChildren : {} : {};
84
134
 
135
+ type RouteConfigTypeError<TMessage extends string> = {
136
+ readonly __routeConfigTypeError__: TMessage;
137
+ };
138
+ type RouteConfigMapTypeErrorsDeep<TConfig> = {
139
+ [K in keyof TConfig]: TConfig[K] extends RouteConfigTypeError<string> ? TConfig[K] : TConfig[K] extends RouteConfigInput ? RouteConfigMapTypeErrorsDeep<RouteConfigChildren<TConfig[K]>> : never;
140
+ }[keyof TConfig];
85
141
  /**
86
- * Config interface
87
- */
88
- interface RouteConfigAndDefinitionBase<Props extends object = {}> {
89
- SuspenseTemplate?: CanBeMemoExoticComponent<JSX.Element | string | number>;
90
- props?: Props;
91
- putInKey?: string;
92
- }
93
- /**
94
- * Children interface
95
- */
96
- interface RouteDefinitionBase<TRouteChildren extends Record<string, RouteDefinition> = Record<string, RouteDefinition>, Props extends object = {}> extends RouteConfigAndDefinitionBase<Props> {
97
- [TREE_CHILD_KEY]?: RoutesDefinition<TRouteChildren>;
98
- }
99
- /**
100
- * For extension
142
+ * Errors
101
143
  */
102
- interface RouteDefinitionExtension {
103
- }
104
- /**
105
- * Base type
106
- */
107
- type RouteDefinition<Props extends object = {}, Keys extends string = string> = RouteRenderConfig<Props> & RouteDefinitionBase<Record<Keys, RouteDefinition>, Props> & RouteDefinitionExtension;
144
+ type DuplicateRootRouteKeyError<TKey extends string> = RouteConfigTypeError<`Duplicate root route key: ${TKey}`>;
145
+ type MissingPutInKeyError<TKey extends string> = RouteConfigTypeError<`putInKey target not found: ${TKey}`>;
146
+ type DuplicatePutInRouteKeyError<TParent extends string, TKey extends string> = RouteConfigTypeError<`Duplicate child route key: ${TKey} for putInKey: ${TParent}`>;
108
147
 
109
- type RoutesDefinition<T extends Record<string, RouteDefinition<object, string>> = Record<string, RouteDefinition<{}, string>>> = T;
148
+ type NormalizeMergedRoutesConfig<TConfig> = {
149
+ [K in Extract<keyof TConfig, string>]: Extract<TConfig[K], RouteConfigInput>;
150
+ };
151
+ type NormalizeResolvedRoutesConfig<TConfig extends Record<string, RouteConfigInput | RouteConfigTypeError<string>>> = {
152
+ [K in keyof TConfig as TConfig[K] extends RouteConfigTypeError<string> ? never : K]: Extract<TConfig[K], RouteConfigInput>;
153
+ };
154
+ type NormalizeRoutesConfig<TConfig extends Record<string, RouteConfigInput>> = {
155
+ [K in keyof TConfig]: TConfig[K];
156
+ };
157
+
158
+ type RouteOwnKeys = Exclude<keyof RouteConfigInput, TreeChildKey>;
159
+ type HasOwnRouteConfig<T> = Extract<keyof T, RouteOwnKeys> extends never ? false : true;
160
+ type MergeRouteNodeSafe<TLeft extends RouteConfigInput, TRight extends RouteConfigInput> = Omit<TLeft & TRight, TreeChildKey> & {
161
+ [K in TreeChildKey]: MergeRoutesConfigsDeep<RouteConfigChildren<TLeft>, RouteConfigChildren<TRight>>;
162
+ };
163
+ type RootRouteKeys<TConfig extends RoutesConfig> = Exclude<RoutesConfigKey<TConfig>, DeferredRouteKeys<TConfig>>;
164
+ type RootRoutesConfigPart<TConfig extends RoutesConfig> = {
165
+ [K in RootRouteKeys<TConfig>]: TConfig[K];
166
+ };
167
+ type MergeRouteNode<TLeft extends RouteConfigInput, TRight extends RouteConfigInput, TKey extends string> = HasOwnRouteConfig<TLeft> extends true ? HasOwnRouteConfig<TRight> extends true ? DuplicateRootRouteKeyError<TKey> : MergeRouteNodeSafe<TLeft, TRight> : MergeRouteNodeSafe<TLeft, TRight>;
168
+ type MergeRoutesConfigsDeep<TLeft extends Record<string, RouteConfigInput>, TRight extends Record<string, RouteConfigInput>> = {
169
+ [K in keyof TLeft | keyof TRight]: K extends keyof TLeft ? K extends keyof TRight ? K extends string ? MergeRouteNode<Extract<TLeft[K], RouteConfigInput>, Extract<TRight[K], RouteConfigInput>, K> : never : TLeft[K] : K extends keyof TRight ? TRight[K] : never;
170
+ };
171
+ type MergeRouteChildren<TRouteConfig extends RouteConfigInput, TDeferredChildren extends Record<string, RouteConfigInput>, TParent extends string> = Extract<Extract<keyof RouteConfigChildren<TRouteConfig>, keyof TDeferredChildren>, string> extends infer TDuplicateKey extends string ? [TDuplicateKey] extends [never] ? Omit<TRouteConfig, TreeChildKey> & {
172
+ [K in TreeChildKey]: RoutesConfig<NormalizeRoutesConfig<RouteConfigChildren<TRouteConfig> & TDeferredChildren>>;
173
+ } : DuplicatePutInRouteKeyError<TParent, TDuplicateKey> : never;
174
+ type MergeRootRoutesConfigs<TConfigs extends readonly RoutesConfig[], TResult extends Record<string, RouteConfigInput> = {}> = TConfigs extends readonly [infer THead extends RoutesConfig, ...infer TTail extends readonly RoutesConfig[]] ? MergeRoutesConfigsDeep<TResult, RootRoutesConfigPart<THead>> extends infer TMerged ? RouteConfigMapTypeErrorsDeep<TMerged> extends infer TError extends RouteConfigTypeError<string> ? [TError] extends [never] ? MergeRootRoutesConfigs<TTail, NormalizeMergedRoutesConfig<TMerged>> : TError : never : never : TResult;
110
175
 
111
176
  type RemoveIndexSignature<T> = {
112
177
  [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K];
@@ -136,6 +201,47 @@ type RoutesPathKeys<TRoutes extends RouteTree, TPrefix extends string = '', TDep
136
201
  [K in RouteKey<TRoutes>]: JoinRouteKey<TPrefix, K> | (RouteTreeChildren<TRoutes[K]> extends infer TChildren extends RouteTree ? RoutesPathKeys<TChildren, JoinRouteKey<TPrefix, K>, [...TDepth, unknown]> : never);
137
202
  }[RouteKey<TRoutes>];
138
203
 
204
+ type ExplicitChildrenRouteKeys<TRootConfig extends Record<string, RouteConfigInput>> = {
205
+ [K in Extract<keyof TRootConfig, string>]: TreeChildKey extends keyof TRootConfig[K] ? K : never;
206
+ }[Extract<keyof TRootConfig, string>];
207
+ type NormalizePutInKey<TPutInKey, TKnownParents extends string> = TPutInKey extends string ? string extends TPutInKey ? [TKnownParents] extends [never] ? TPutInKey : TKnownParents : TPutInKey : never;
208
+ type RouteConfigPutInKey<TConfig extends RouteConfigInput> = TConfig extends {
209
+ putInKey: infer TPutInKey extends string;
210
+ } ? TPutInKey : never;
211
+ type PutInKeyFallbackParents<TRootConfig extends Record<string, RouteConfigInput>> = [
212
+ ExplicitChildrenRouteKeys<TRootConfig>
213
+ ] extends [never] ? Extract<keyof TRootConfig, string> : ExplicitChildrenRouteKeys<TRootConfig>;
214
+
215
+ type MergeDeferredRoutesForParent<TConfigs extends readonly RoutesConfig[], TRootConfig extends Record<string, RouteConfigInput>, TParent extends string, TResult extends Record<string, RouteConfigInput> = {}> = TConfigs extends readonly [infer THead extends RoutesConfig, ...infer TTail extends readonly RoutesConfig[]] ? DeferredRoutesForParentInConfig<THead, TParent, PutInKeyFallbackParents<TRootConfig>> extends infer TDeferredRoutes extends Record<string, RouteConfigInput> ? Extract<Extract<keyof TResult, keyof TDeferredRoutes>, string> extends infer TDuplicateKey extends string ? [TDuplicateKey] extends [never] ? MergeDeferredRoutesForParent<TTail, TRootConfig, TParent, TResult & TDeferredRoutes> : DuplicatePutInRouteKeyError<TParent, TDuplicateKey> : never : never : TResult;
216
+ type ApplyDeferredRouteForKey<TRootConfig extends Record<string, RouteConfigInput>, TConfigs extends readonly RoutesConfig[], TKey extends Extract<keyof TRootConfig, string>> = MergeDeferredRoutesForParent<TConfigs, TRootConfig, TKey> extends infer TDeferredChildren ? TDeferredChildren extends RouteConfigTypeError<string> ? TDeferredChildren : TDeferredChildren extends Record<string, RouteConfigInput> ? keyof TDeferredChildren extends never ? TRootConfig[TKey] : MergeRouteChildren<TRootConfig[TKey], TDeferredChildren, TKey> : never : never;
217
+ type RouteConfigWithoutPutInKey<TConfig extends RouteConfigInput> = Omit<TConfig, 'putInKey'>;
218
+ type DeferredRoutesForParentInConfig<TConfig extends RoutesConfig, TParent extends string, TKnownParents extends string = TParent> = {
219
+ [K in DeferredRouteKeys<TConfig> as Extract<NormalizePutInKey<RouteConfigPutInKey<TConfig[K]>, TKnownParents>, TParent> extends never ? never : K]: RouteConfigWithoutPutInKey<TConfig[K]>;
220
+ };
221
+ type DeferredRouteKeys<TConfig extends RoutesConfig> = {
222
+ [K in RoutesConfigKey<TConfig>]: TConfig[K] extends {
223
+ putInKey: string;
224
+ } ? K : never;
225
+ }[RoutesConfigKey<TConfig>];
226
+ type DeferredRouteParents<TConfig extends RoutesConfig, TKnownParents extends string> = {
227
+ [K in DeferredRouteKeys<TConfig>]: NormalizePutInKey<RouteConfigPutInKey<TConfig[K]>, TKnownParents>;
228
+ }[DeferredRouteKeys<TConfig>];
229
+ type ApplyDeferredRoutes<TRootConfig extends Record<string, RouteConfigInput>, TConfigs extends readonly RoutesConfig[]> = {
230
+ [K in Extract<keyof TRootConfig, string>]: ApplyDeferredRouteForKey<TRootConfig, TConfigs, K>;
231
+ };
232
+ type ResolveAppliedDeferredRoutes<TAppliedRoutes extends Record<string, RouteConfigInput | RouteConfigTypeError<string>>> = RouteConfigMapTypeErrorsDeep<TAppliedRoutes> extends infer TError extends RouteConfigTypeError<string> ? [TError] extends [never] ? RoutesConfig<NormalizeRoutesConfig<NormalizeResolvedRoutesConfig<TAppliedRoutes>>> : TError : never;
233
+
234
+ type MissingPutInKeys<TConfigs extends readonly RoutesConfig[], TRootConfig extends Record<string, RouteConfigInput>> = TConfigs extends readonly [infer THead extends RoutesConfig, ...infer TTail extends readonly RoutesConfig[]] ? Exclude<DeferredRouteParents<THead, PutInKeyFallbackParents<TRootConfig>>, Extract<keyof TRootConfig, string>> | MissingPutInKeys<TTail, TRootConfig> : never;
235
+ type ValidatePutInKeys<TConfigs extends readonly RoutesConfig[], TRootConfig extends Record<string, RouteConfigInput>> = MissingPutInKeys<TConfigs, TRootConfig> extends infer TMissingKey extends string ? [TMissingKey] extends [never] ? TRootConfig : MissingPutInKeyError<TMissingKey> : never;
236
+
237
+ /**
238
+ * General type
239
+ */
240
+ type UnionRoutesConfig<TRoutes extends readonly RoutesConfig[] = readonly RoutesConfig[]> = number extends TRoutes['length'] ? RoutesConfig : MergeRootRoutesConfigs<TRoutes> extends infer TRootRoutes ? TRootRoutes extends RouteConfigTypeError<string> ? TRootRoutes : TRootRoutes extends Record<string, RouteConfigInput> ? ValidatePutInKeys<TRoutes, TRootRoutes> extends infer TValidatedRoutes ? TValidatedRoutes extends RouteConfigTypeError<string> ? TValidatedRoutes : TValidatedRoutes extends Record<string, RouteConfigInput> ? ApplyDeferredRoutes<TValidatedRoutes, TRoutes> extends infer TAppliedRoutes extends Record<string, RouteConfigInput | RouteConfigTypeError<string>> ? ResolveAppliedDeferredRoutes<TAppliedRoutes> : never : never : never : never : never;
241
+
242
+ declare function reactRouterFabric<const TRoutes extends readonly RoutesConfig[]>(...mixedRoutesConfigs: TRoutes): UnionRoutesConfig<TRoutes>;
243
+ declare function reactRouterFabric<const TRoutes extends readonly RoutesConfig[]>(mixedRoutesConfigs: TRoutes[]): UnionRoutesConfig<TRoutes>;
244
+
139
245
  type CreateUrlFromRouteConfig<Routes extends RoutesConfig> = (mixedPath: RoutesKeys<Routes>, pathParams?: Nullable<CustomObject<string | number | boolean>>, queryParams?: CustomObject<Nullable<string | number | boolean>>, safePathParams?: boolean) => string;
140
246
 
141
247
  type CreateUrlsFromRouteConfig<Routes extends RoutesConfig> = (mixedPath: RoutesKeys<Routes>, pathParams?: Nullable<CustomObject<string | number | boolean>>, queryParams?: CustomObject<Nullable<string | number | boolean>>, safePathParams?: boolean) => string[];
@@ -147,6 +253,8 @@ interface UseActivatedRouteType<Routes extends RoutesConfig> {
147
253
  pathParams: Record<string, string>;
148
254
  queryParams: Record<string, string>;
149
255
  routeKey: ObjectChain<Routes>;
256
+ routeKeyString: RoutesPathKeys<Routes>;
257
+ routeUrlMask: string;
150
258
  parents: RouteConfig[];
151
259
  }
152
260
 
@@ -182,4 +290,4 @@ interface CreateRouterContext<Routes extends RoutesConfig = RoutesConfig> {
182
290
 
183
291
  declare const routerContextFabric: <Routes extends RoutesConfig = RoutesConfig>(routes: Routes) => CreateRouterContext<Routes>;
184
292
 
185
- export { type RoutesDefinition, type RoutesKeys, type RoutesPathKeys, routerContextFabric };
293
+ export { type ReactComponents, type RouterComponents, type RoutesDefinition, type RoutesKeys, type RoutesPathKeys, componentsContextFabric, reactRouterFabric, routerContextFabric };
package/.dist/index.d.ts CHANGED
@@ -1,10 +1,30 @@
1
- import { MemoExoticComponent, LazyExoticComponent, FC, PropsWithChildren, Context } from 'react';
2
1
  import { Nullable, CustomObject, Undefinable } from '@akb2/types-tools';
2
+ import { ReactNode, FC, PropsWithChildren, Context, MemoExoticComponent, LazyExoticComponent } from 'react';
3
3
  import { Match } from 'wouter';
4
4
 
5
- declare const TREE_CHILD_KEY: "children";
5
+ type AnyComponent = (props: any) => ReactNode;
6
6
 
7
- type CanBeMemoExoticComponent<C extends JSX.Element | string | number> = MemoExoticComponent<() => C> | (() => C);
7
+ type ReactComponents<Props extends object = Record<string, object>> = {
8
+ [K in keyof Props]: Props[K] extends AnyComponent ? Props[K] : FC<Props[K]>;
9
+ };
10
+
11
+ interface RouterComponents {
12
+ FullPageLoader: {};
13
+ }
14
+
15
+ interface ComponentsProviderProps<TComponents extends ReactComponents<RouterComponents> = ReactComponents<RouterComponents>> extends PropsWithChildren {
16
+ components: TComponents;
17
+ }
18
+
19
+ interface CreateComponentsContext<TComponents extends ReactComponents<RouterComponents> = ReactComponents<RouterComponents>> {
20
+ ComponentsContext: Context<Nullable<TComponents>>;
21
+ ComponentsProvider: FC<ComponentsProviderProps<TComponents>>;
22
+ useComponents: () => TComponents;
23
+ }
24
+
25
+ declare const componentsContextFabric: <TComponents extends ReactComponents<RouterComponents>>() => CreateComponentsContext<TComponents>;
26
+
27
+ declare const TREE_CHILD_KEY: "children";
8
28
 
9
29
  type TreeChildKey = typeof TREE_CHILD_KEY;
10
30
 
@@ -27,6 +47,35 @@ type RouteConfigInput = {
27
47
  * General types
28
48
  */
29
49
  type RoutesConfig<T extends Record<string, RouteConfigInput> = Record<string, RouteConfigInput>> = T;
50
+ type RoutesConfigKey<TConfig extends RoutesConfig> = Extract<keyof TConfig, string>;
51
+
52
+ type CanBeMemoExoticComponent<C extends JSX.Element | string | number> = MemoExoticComponent<() => C> | (() => C);
53
+
54
+ type RoutesDefinition<T extends Record<string, RouteDefinition<object, string>> = Record<string, RouteDefinition<{}, string>>> = T;
55
+
56
+ /**
57
+ * Config interface
58
+ */
59
+ interface RouteConfigAndDefinitionBase<Props extends object = {}> {
60
+ SuspenseTemplate?: CanBeMemoExoticComponent<JSX.Element | string | number>;
61
+ props?: Props;
62
+ putInKey?: string;
63
+ }
64
+ /**
65
+ * Children interface
66
+ */
67
+ interface RouteDefinitionBase<TRouteChildren extends Record<string, RouteDefinition> = Record<string, RouteDefinition>, Props extends object = {}> extends RouteConfigAndDefinitionBase<Props> {
68
+ [TREE_CHILD_KEY]?: RoutesDefinition<TRouteChildren>;
69
+ }
70
+ /**
71
+ * For extension
72
+ */
73
+ interface RouteDefinitionExtension {
74
+ }
75
+ /**
76
+ * Base type
77
+ */
78
+ type RouteDefinition<Props extends object = {}, Keys extends string = string> = RouteRenderConfig<Props> & RouteDefinitionBase<Record<Keys, RouteDefinition>, Props> & RouteDefinitionExtension;
30
79
 
31
80
  /**
32
81
  * Base models
@@ -81,32 +130,48 @@ interface RouteConfigExtension {
81
130
  * General model
82
131
  */
83
132
  type RouteConfig<Props extends object = {}, Keys extends string = string> = RouteConfigPath & RouteRenderConfig & RouteConfigBase<Record<Keys, RouteConfig<Props>>, Props> & RouteConfigExtension;
133
+ type RouteConfigChildren<TConfig extends RouteConfigInput> = TreeChildKey extends keyof TConfig ? TConfig[TreeChildKey] extends RoutesConfig<infer TChildren extends Record<string, RouteConfigInput>> ? TChildren : {} : {};
84
134
 
135
+ type RouteConfigTypeError<TMessage extends string> = {
136
+ readonly __routeConfigTypeError__: TMessage;
137
+ };
138
+ type RouteConfigMapTypeErrorsDeep<TConfig> = {
139
+ [K in keyof TConfig]: TConfig[K] extends RouteConfigTypeError<string> ? TConfig[K] : TConfig[K] extends RouteConfigInput ? RouteConfigMapTypeErrorsDeep<RouteConfigChildren<TConfig[K]>> : never;
140
+ }[keyof TConfig];
85
141
  /**
86
- * Config interface
87
- */
88
- interface RouteConfigAndDefinitionBase<Props extends object = {}> {
89
- SuspenseTemplate?: CanBeMemoExoticComponent<JSX.Element | string | number>;
90
- props?: Props;
91
- putInKey?: string;
92
- }
93
- /**
94
- * Children interface
95
- */
96
- interface RouteDefinitionBase<TRouteChildren extends Record<string, RouteDefinition> = Record<string, RouteDefinition>, Props extends object = {}> extends RouteConfigAndDefinitionBase<Props> {
97
- [TREE_CHILD_KEY]?: RoutesDefinition<TRouteChildren>;
98
- }
99
- /**
100
- * For extension
142
+ * Errors
101
143
  */
102
- interface RouteDefinitionExtension {
103
- }
104
- /**
105
- * Base type
106
- */
107
- type RouteDefinition<Props extends object = {}, Keys extends string = string> = RouteRenderConfig<Props> & RouteDefinitionBase<Record<Keys, RouteDefinition>, Props> & RouteDefinitionExtension;
144
+ type DuplicateRootRouteKeyError<TKey extends string> = RouteConfigTypeError<`Duplicate root route key: ${TKey}`>;
145
+ type MissingPutInKeyError<TKey extends string> = RouteConfigTypeError<`putInKey target not found: ${TKey}`>;
146
+ type DuplicatePutInRouteKeyError<TParent extends string, TKey extends string> = RouteConfigTypeError<`Duplicate child route key: ${TKey} for putInKey: ${TParent}`>;
108
147
 
109
- type RoutesDefinition<T extends Record<string, RouteDefinition<object, string>> = Record<string, RouteDefinition<{}, string>>> = T;
148
+ type NormalizeMergedRoutesConfig<TConfig> = {
149
+ [K in Extract<keyof TConfig, string>]: Extract<TConfig[K], RouteConfigInput>;
150
+ };
151
+ type NormalizeResolvedRoutesConfig<TConfig extends Record<string, RouteConfigInput | RouteConfigTypeError<string>>> = {
152
+ [K in keyof TConfig as TConfig[K] extends RouteConfigTypeError<string> ? never : K]: Extract<TConfig[K], RouteConfigInput>;
153
+ };
154
+ type NormalizeRoutesConfig<TConfig extends Record<string, RouteConfigInput>> = {
155
+ [K in keyof TConfig]: TConfig[K];
156
+ };
157
+
158
+ type RouteOwnKeys = Exclude<keyof RouteConfigInput, TreeChildKey>;
159
+ type HasOwnRouteConfig<T> = Extract<keyof T, RouteOwnKeys> extends never ? false : true;
160
+ type MergeRouteNodeSafe<TLeft extends RouteConfigInput, TRight extends RouteConfigInput> = Omit<TLeft & TRight, TreeChildKey> & {
161
+ [K in TreeChildKey]: MergeRoutesConfigsDeep<RouteConfigChildren<TLeft>, RouteConfigChildren<TRight>>;
162
+ };
163
+ type RootRouteKeys<TConfig extends RoutesConfig> = Exclude<RoutesConfigKey<TConfig>, DeferredRouteKeys<TConfig>>;
164
+ type RootRoutesConfigPart<TConfig extends RoutesConfig> = {
165
+ [K in RootRouteKeys<TConfig>]: TConfig[K];
166
+ };
167
+ type MergeRouteNode<TLeft extends RouteConfigInput, TRight extends RouteConfigInput, TKey extends string> = HasOwnRouteConfig<TLeft> extends true ? HasOwnRouteConfig<TRight> extends true ? DuplicateRootRouteKeyError<TKey> : MergeRouteNodeSafe<TLeft, TRight> : MergeRouteNodeSafe<TLeft, TRight>;
168
+ type MergeRoutesConfigsDeep<TLeft extends Record<string, RouteConfigInput>, TRight extends Record<string, RouteConfigInput>> = {
169
+ [K in keyof TLeft | keyof TRight]: K extends keyof TLeft ? K extends keyof TRight ? K extends string ? MergeRouteNode<Extract<TLeft[K], RouteConfigInput>, Extract<TRight[K], RouteConfigInput>, K> : never : TLeft[K] : K extends keyof TRight ? TRight[K] : never;
170
+ };
171
+ type MergeRouteChildren<TRouteConfig extends RouteConfigInput, TDeferredChildren extends Record<string, RouteConfigInput>, TParent extends string> = Extract<Extract<keyof RouteConfigChildren<TRouteConfig>, keyof TDeferredChildren>, string> extends infer TDuplicateKey extends string ? [TDuplicateKey] extends [never] ? Omit<TRouteConfig, TreeChildKey> & {
172
+ [K in TreeChildKey]: RoutesConfig<NormalizeRoutesConfig<RouteConfigChildren<TRouteConfig> & TDeferredChildren>>;
173
+ } : DuplicatePutInRouteKeyError<TParent, TDuplicateKey> : never;
174
+ type MergeRootRoutesConfigs<TConfigs extends readonly RoutesConfig[], TResult extends Record<string, RouteConfigInput> = {}> = TConfigs extends readonly [infer THead extends RoutesConfig, ...infer TTail extends readonly RoutesConfig[]] ? MergeRoutesConfigsDeep<TResult, RootRoutesConfigPart<THead>> extends infer TMerged ? RouteConfigMapTypeErrorsDeep<TMerged> extends infer TError extends RouteConfigTypeError<string> ? [TError] extends [never] ? MergeRootRoutesConfigs<TTail, NormalizeMergedRoutesConfig<TMerged>> : TError : never : never : TResult;
110
175
 
111
176
  type RemoveIndexSignature<T> = {
112
177
  [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K];
@@ -136,6 +201,47 @@ type RoutesPathKeys<TRoutes extends RouteTree, TPrefix extends string = '', TDep
136
201
  [K in RouteKey<TRoutes>]: JoinRouteKey<TPrefix, K> | (RouteTreeChildren<TRoutes[K]> extends infer TChildren extends RouteTree ? RoutesPathKeys<TChildren, JoinRouteKey<TPrefix, K>, [...TDepth, unknown]> : never);
137
202
  }[RouteKey<TRoutes>];
138
203
 
204
+ type ExplicitChildrenRouteKeys<TRootConfig extends Record<string, RouteConfigInput>> = {
205
+ [K in Extract<keyof TRootConfig, string>]: TreeChildKey extends keyof TRootConfig[K] ? K : never;
206
+ }[Extract<keyof TRootConfig, string>];
207
+ type NormalizePutInKey<TPutInKey, TKnownParents extends string> = TPutInKey extends string ? string extends TPutInKey ? [TKnownParents] extends [never] ? TPutInKey : TKnownParents : TPutInKey : never;
208
+ type RouteConfigPutInKey<TConfig extends RouteConfigInput> = TConfig extends {
209
+ putInKey: infer TPutInKey extends string;
210
+ } ? TPutInKey : never;
211
+ type PutInKeyFallbackParents<TRootConfig extends Record<string, RouteConfigInput>> = [
212
+ ExplicitChildrenRouteKeys<TRootConfig>
213
+ ] extends [never] ? Extract<keyof TRootConfig, string> : ExplicitChildrenRouteKeys<TRootConfig>;
214
+
215
+ type MergeDeferredRoutesForParent<TConfigs extends readonly RoutesConfig[], TRootConfig extends Record<string, RouteConfigInput>, TParent extends string, TResult extends Record<string, RouteConfigInput> = {}> = TConfigs extends readonly [infer THead extends RoutesConfig, ...infer TTail extends readonly RoutesConfig[]] ? DeferredRoutesForParentInConfig<THead, TParent, PutInKeyFallbackParents<TRootConfig>> extends infer TDeferredRoutes extends Record<string, RouteConfigInput> ? Extract<Extract<keyof TResult, keyof TDeferredRoutes>, string> extends infer TDuplicateKey extends string ? [TDuplicateKey] extends [never] ? MergeDeferredRoutesForParent<TTail, TRootConfig, TParent, TResult & TDeferredRoutes> : DuplicatePutInRouteKeyError<TParent, TDuplicateKey> : never : never : TResult;
216
+ type ApplyDeferredRouteForKey<TRootConfig extends Record<string, RouteConfigInput>, TConfigs extends readonly RoutesConfig[], TKey extends Extract<keyof TRootConfig, string>> = MergeDeferredRoutesForParent<TConfigs, TRootConfig, TKey> extends infer TDeferredChildren ? TDeferredChildren extends RouteConfigTypeError<string> ? TDeferredChildren : TDeferredChildren extends Record<string, RouteConfigInput> ? keyof TDeferredChildren extends never ? TRootConfig[TKey] : MergeRouteChildren<TRootConfig[TKey], TDeferredChildren, TKey> : never : never;
217
+ type RouteConfigWithoutPutInKey<TConfig extends RouteConfigInput> = Omit<TConfig, 'putInKey'>;
218
+ type DeferredRoutesForParentInConfig<TConfig extends RoutesConfig, TParent extends string, TKnownParents extends string = TParent> = {
219
+ [K in DeferredRouteKeys<TConfig> as Extract<NormalizePutInKey<RouteConfigPutInKey<TConfig[K]>, TKnownParents>, TParent> extends never ? never : K]: RouteConfigWithoutPutInKey<TConfig[K]>;
220
+ };
221
+ type DeferredRouteKeys<TConfig extends RoutesConfig> = {
222
+ [K in RoutesConfigKey<TConfig>]: TConfig[K] extends {
223
+ putInKey: string;
224
+ } ? K : never;
225
+ }[RoutesConfigKey<TConfig>];
226
+ type DeferredRouteParents<TConfig extends RoutesConfig, TKnownParents extends string> = {
227
+ [K in DeferredRouteKeys<TConfig>]: NormalizePutInKey<RouteConfigPutInKey<TConfig[K]>, TKnownParents>;
228
+ }[DeferredRouteKeys<TConfig>];
229
+ type ApplyDeferredRoutes<TRootConfig extends Record<string, RouteConfigInput>, TConfigs extends readonly RoutesConfig[]> = {
230
+ [K in Extract<keyof TRootConfig, string>]: ApplyDeferredRouteForKey<TRootConfig, TConfigs, K>;
231
+ };
232
+ type ResolveAppliedDeferredRoutes<TAppliedRoutes extends Record<string, RouteConfigInput | RouteConfigTypeError<string>>> = RouteConfigMapTypeErrorsDeep<TAppliedRoutes> extends infer TError extends RouteConfigTypeError<string> ? [TError] extends [never] ? RoutesConfig<NormalizeRoutesConfig<NormalizeResolvedRoutesConfig<TAppliedRoutes>>> : TError : never;
233
+
234
+ type MissingPutInKeys<TConfigs extends readonly RoutesConfig[], TRootConfig extends Record<string, RouteConfigInput>> = TConfigs extends readonly [infer THead extends RoutesConfig, ...infer TTail extends readonly RoutesConfig[]] ? Exclude<DeferredRouteParents<THead, PutInKeyFallbackParents<TRootConfig>>, Extract<keyof TRootConfig, string>> | MissingPutInKeys<TTail, TRootConfig> : never;
235
+ type ValidatePutInKeys<TConfigs extends readonly RoutesConfig[], TRootConfig extends Record<string, RouteConfigInput>> = MissingPutInKeys<TConfigs, TRootConfig> extends infer TMissingKey extends string ? [TMissingKey] extends [never] ? TRootConfig : MissingPutInKeyError<TMissingKey> : never;
236
+
237
+ /**
238
+ * General type
239
+ */
240
+ type UnionRoutesConfig<TRoutes extends readonly RoutesConfig[] = readonly RoutesConfig[]> = number extends TRoutes['length'] ? RoutesConfig : MergeRootRoutesConfigs<TRoutes> extends infer TRootRoutes ? TRootRoutes extends RouteConfigTypeError<string> ? TRootRoutes : TRootRoutes extends Record<string, RouteConfigInput> ? ValidatePutInKeys<TRoutes, TRootRoutes> extends infer TValidatedRoutes ? TValidatedRoutes extends RouteConfigTypeError<string> ? TValidatedRoutes : TValidatedRoutes extends Record<string, RouteConfigInput> ? ApplyDeferredRoutes<TValidatedRoutes, TRoutes> extends infer TAppliedRoutes extends Record<string, RouteConfigInput | RouteConfigTypeError<string>> ? ResolveAppliedDeferredRoutes<TAppliedRoutes> : never : never : never : never : never;
241
+
242
+ declare function reactRouterFabric<const TRoutes extends readonly RoutesConfig[]>(...mixedRoutesConfigs: TRoutes): UnionRoutesConfig<TRoutes>;
243
+ declare function reactRouterFabric<const TRoutes extends readonly RoutesConfig[]>(mixedRoutesConfigs: TRoutes[]): UnionRoutesConfig<TRoutes>;
244
+
139
245
  type CreateUrlFromRouteConfig<Routes extends RoutesConfig> = (mixedPath: RoutesKeys<Routes>, pathParams?: Nullable<CustomObject<string | number | boolean>>, queryParams?: CustomObject<Nullable<string | number | boolean>>, safePathParams?: boolean) => string;
140
246
 
141
247
  type CreateUrlsFromRouteConfig<Routes extends RoutesConfig> = (mixedPath: RoutesKeys<Routes>, pathParams?: Nullable<CustomObject<string | number | boolean>>, queryParams?: CustomObject<Nullable<string | number | boolean>>, safePathParams?: boolean) => string[];
@@ -147,6 +253,8 @@ interface UseActivatedRouteType<Routes extends RoutesConfig> {
147
253
  pathParams: Record<string, string>;
148
254
  queryParams: Record<string, string>;
149
255
  routeKey: ObjectChain<Routes>;
256
+ routeKeyString: RoutesPathKeys<Routes>;
257
+ routeUrlMask: string;
150
258
  parents: RouteConfig[];
151
259
  }
152
260
 
@@ -182,4 +290,4 @@ interface CreateRouterContext<Routes extends RoutesConfig = RoutesConfig> {
182
290
 
183
291
  declare const routerContextFabric: <Routes extends RoutesConfig = RoutesConfig>(routes: Routes) => CreateRouterContext<Routes>;
184
292
 
185
- export { type RoutesDefinition, type RoutesKeys, type RoutesPathKeys, routerContextFabric };
293
+ export { type ReactComponents, type RouterComponents, type RoutesDefinition, type RoutesKeys, type RoutesPathKeys, componentsContextFabric, reactRouterFabric, routerContextFabric };
package/.dist/index.js CHANGED
@@ -1 +1 @@
1
- import*as p from"react";import{anyToArray as z,anyToString as S,isDefined as N}from"@akb2/types-tools";var A="@akb2/react/";var E=Symbol.for(A+"router-context");import{useCallback as Q,useMemo as q}from"react";import{useSearchParams as H,useLocation as Z,useRoute as V}from"wouter";var K=(t,o,r)=>{let s=t.split("/"),a=o.split("/");if(s.length!==a.length)return!1;for(let i=0;i<s.length;i++)if(a[i].startsWith(":")){r[a[i].slice(1)]=s[i];continue}else if(s[i]!==a[i])return!1;return!0};import{anyToArray as W,anyToString as G,smartJoin as X}from"@akb2/types-tools";var d=(t,o,r=!1)=>{let a=W(t).reduce((h,n)=>{let m=n.split("/");for(let j of m)h.push(j.startsWith(":")&&!r?G(o?.[j.slice(1)],"*"):j);return h},[]);return("/"+X("/",...a)).replace(/[\/\\]+/gim,"/")};var v=t=>Array.isArray(t)?t:t.split(".");import{anyToString as Y,isDefined as D}from"@akb2/types-tools";var O=t=>Object.entries(t??{}).filter(([,o])=>D(o)).reduce((o,[r,s])=>o+`${o?"&":"?"}${encodeURIComponent(r)}=${encodeURIComponent(Y(s))}`,"");var L=(t,o,r,s,a=!1)=>{let i=v(o),h=[],n=t;for(let m of i){if(!n.hasOwnProperty(m))break;n=n[m],h.push(n?.path??n?.paths[0]),n=n.children}return d(h,r,a)+O(s)};var _=(t,o,r,s,a=!1)=>{let i=O(s),h=v(o),n=[],m=t;for(let e of h){if(!m.hasOwnProperty(e))break;m=m[e];let u=Array.isArray(m?.paths)?m.paths:[m?.path];n.push(u.filter(C=>typeof C=="string")),m=m.children}return n.length===0?[d([])]:n.reduce((e,u)=>e.flatMap(C=>u.map(l=>[...C,l])),[[]]).map(e=>d(e,r,a)+i)};var B=t=>Array.from(t).reduce((o,[r,s])=>(o[r]=s,o),{});import{createContext as $}from"react";var w=(t,o=null)=>{let r=globalThis;return t in r||(r[t]=$(o)),r[t]};import{anyToArray as J}from"@akb2/types-tools";var k=t=>{if(!t)return{};let o=J(t.split("&")),r={};for(let s of o){let[a,i]=s.split("=");a&&(r[a]=i??"")}return r};import{jsxs as ot}from"react/jsx-runtime";var tt=t=>{let o=w(E),r=({children:e})=>ot(o.Provider,{value:t,children:[" ",e," "]}),s=L.bind(null,t),a=_.bind(null,t),i=e=>V(s(e,null,{},!0)),h=()=>{let[e,u]=Z();return[e,(l,f,R,y)=>{let c=a(l,f,R,!0),g=c.find(x=>{let F=x.split("/").filter(Boolean);return Object.keys(f??{}).every(I=>F.includes(":"+I))})??c[0],[b,T]=d(g,f,!1).split("?"),U=k(T),P=b+O({...U,...R});return u(P,y)}]},n=()=>{let[e]=h(),[u]=H(),C=q(()=>{let l=e.split("/").filter(Boolean);return l.length>0?l:[""]},[e]);return q(()=>{let l=[],f=[],R={},y=B(u.entries()),c=null,g=t,b="";for(let T of C){let U=b?b+"/"+T:T,P=Object.entries(g).filter(([,x])=>N(x.path)?K(U,S(x.path),R):N(x.paths)?z(x.paths).some(F=>K(U,S(F),R)):!1);if(P.length>1)throw new Error("Error in routes config: you have routes with similar pathes");P.length===0?b=b?b+"/"+T:T:(N(c)&&f.push(c),b="",c=P[0][1],g=c.children??{},l.push(P[0][0]))}return(b||!N(c))&&(c=t.notFound),{activatedRoute:c,routeKey:l,parents:f,pathParams:R,queryParams:y}},[C,u])};return{RouterContext:o,RouterProvider:r,createUrlFromRouteConfig:s,createUrlsFromRouteConfig:a,useLocation:h,useActivatedRoute:n,useRoute:i,useRoutePathState:(e,u)=>{let[,C]=h(),{routeKey:l,pathParams:f,queryParams:R}=n(),y=f[e],c=Q(g=>{y!==g&&C(l,{...f,[e]:g},R,{replace:!0})},[y,l,f,R]);return[y??u,c]},useRouteQueryState:(e,u)=>{let[,C]=h(),{routeKey:l,pathParams:f,queryParams:R}=n(),y=R[e]??u,c=Q(g=>{if(y!==g){let b={...R};N(g)&&g!==u&&S(g).length>0?b[e]=S(g):delete b[e],C(l,f,b,{replace:!0})}},[y,l,f,R,u]);return[y??u,c]}}};export{tt as routerContextFabric};
1
+ import*as C from"react";var _="@akb2/react/";var q=Symbol.for(_+"components-context");import{createContext as rt}from"react";var I=(t,s=null)=>{let o=globalThis;return t in o||(o[t]=rt(s)),o[t]};import{useContext as nt}from"react";import{jsx as st}from"react/jsx-runtime";var D=()=>{let t=I(q);return{ComponentsContext:t,ComponentsProvider:({components:i,children:R})=>st(t.Provider,{value:i,children:R}),useComponents:()=>{let i=nt(t);if(!i)throw new Error("useComponents must be used inside ComponentsProvider");return i}}};import{isDefined as v}from"@akb2/types-tools";import{isDefined as G}from"@akb2/types-tools";var W=t=>{let s=G(t.children)?{...t.children}:void 0;return G(s)?{...t,children:s}:{...t}};function it(...t){let s=Array.isArray(t[0])?t[0]:t,o={},i=[],R=100;for(let p of s)for(let r in p){let u=p[r];if(!v(u.putInKey)&&v(o[r]))throw new Error(`Duplicate route key "${r}" found in module options.`);v(u.putInKey)?i.push([r,u]):o[r]=u}for(;i.length>0&&R-- >0;){let p=i.findIndex(([,{putInKey:n}])=>v(n)&&v(o[n]));if(p===-1)throw new Error('Circular dependency detected in route configurations. Please check the "putInKey" properties.');let[r,u]=i[p],{putInKey:c,...P}=u;if(!v(c))throw new Error('Invalid route configuration. "putInKey" is not defined.');let x=o[c],N=v(x.children)?{...x.children}:{};if(v(N[r]))throw new Error(`Duplicate route key "${r}" found in module options.`);N[r]=W(P),o[c]={...x,children:N},i.splice(p,1)}return o}import{anyToArray as ht,anyToString as $,isDefined as g,smartJoin as gt}from"@akb2/types-tools";var Y=Symbol.for(_+"router-context");var J=(t,s,o)=>{let i={...o},R=t.split("/"),p=s.split("/");if(R.length!==p.length)return!1;for(let r=0;r<R.length;r++)if(p[r].startsWith(":")){i[p[r].slice(1)]=R[r];continue}else if(R[r]!==p[r])return!1;return Object.assign(o,i),!0};import{anyToArray as ut,anyToString as ct,smartJoin as lt}from"@akb2/types-tools";var S=(t,s,o=!1)=>{let R=ut(t).reduce((r,u)=>{let c=u.split("/");for(let P of c)r.push(P.startsWith(":")&&!o?ct(s?.[P.slice(1)],"*"):P);return r},[]);return("/"+lt("/",...R)).replace(/[\/\\]+/gim,"/")};var M=t=>Array.isArray(t)?t:t.split(".");import{anyToString as mt,isDefined as pt}from"@akb2/types-tools";var j=t=>Object.entries(t??{}).filter(([,s])=>pt(s)).reduce((s,[o,i])=>s+`${s?"&":"?"}${encodeURIComponent(o)}=${encodeURIComponent(mt(i))}`,"");var z=(t,s,o,i,R=!1)=>{let p=M(s),r=[],u=t;for(let c of p){if(!u.hasOwnProperty(c))break;u=u[c],r.push(u?.path??u?.paths[0]),u=u.children}return S(r,o,R)+j(i)};var H=(t,s,o,i,R=!1)=>{let p=j(i),r=M(s),u=[],c=t;for(let x of r){if(!c.hasOwnProperty(x))break;c=c[x];let N=Array.isArray(c?.paths)?c.paths:[c?.path];u.push(N.filter(n=>typeof n=="string")),c=c.children}return u.length===0?[S([])]:u.reduce((x,N)=>x.flatMap(n=>N.map(f=>[...n,f])),[[]]).map(x=>S(x,o,R)+p)};var V=t=>Array.from(t).reduce((s,[o,i])=>(s[o]=i,s),{});import{anyToArray as ft}from"@akb2/types-tools";var Z=t=>{if(!t)return{};let s=ft(t.split("&")),o={};for(let i of s){let[R,p]=i.split("=");R&&(o[R]=p??"")}return o};import{Suspense as bt,useCallback as ot,useMemo as et}from"react";import{Redirect as Tt,useSearchParams as yt,useLocation as Pt,useRoute as xt}from"wouter";import{jsx as F}from"react/jsx-runtime";var Et=t=>{let s=(n,f)=>{let h=n.split("/").filter(Boolean),l=f.split("/").filter(Boolean);for(let a=0;a<h.length;a++){let m=h[a].startsWith(":"),e=l[a].startsWith(":");if(m!==e)return m?1:-1}return 0},o=(n,f=[""],h=new Set)=>{if(f.length===0)throw new Error("Parent paths cannot be empty.");let l=h.size===0,a=!1;for(let m in n){let e=n[m];if([g(e.Module),g(e.Template),g(e.redirectUrl)].filter(Boolean).length!==1)throw new Error(`Route with key "${m}" must have exactly one of Module, Template, or redirectUrl defined.`);if([g(e.path),g(e.paths),e.isNotFound===!0].filter(Boolean).length!==1)throw new Error(`Route with key "${m}" must have exactly one of path, paths, or isNotFound defined.`);if(e.isNotFound===!0)if(l){if(a)throw new Error("Only one not found route can be defined at the top level.");if(g(e.children))throw new Error("Not found route cannot have children.");a=!0;continue}else throw new Error("Not found route can only be defined at the top level.");let y=g(e.path)?[e.path]:g(e.paths)?e.paths:[];if(y.length===0)throw new Error(`Route with key "${m}" must have at least one path defined.`);let b=f.flatMap(E=>y.map(w=>S([E,w])));for(let E of b){if(h.has(E))throw new Error(`Duplicate route detected for URL: "${E}"`);h.add(E)}g(e.children)&&o(e.children,b,h)}if(l&&!a)throw new Error("A not found route must be defined at the top level.")};o(t);let i=I(Y),R=()=>{let{activatedRoute:{Module:n,Template:f,redirectUrl:h,SuspenseTemplate:l,props:a}}=P(),{useComponents:m}=D(),{FullPageLoader:e}=m(),T=g(n)?n:g(f)?f:()=>F(Tt,{to:h}),d=l?F(l,{}):F(e,{});return F(i.Provider,{value:t,children:F(bt,{fallback:d,children:F(T,{...a})})})},p=z.bind(null,t),r=H.bind(null,t),u=n=>xt(p(n,null,{},!0)),c=()=>{let[n,f]=Pt();return[n,(l,a,m,e)=>{let T=r(l,a,m,!0),d=T.find(k=>{let U=k.split("/").filter(Boolean);return Object.keys(a??{}).every(K=>U.includes(":"+K))})??T[0],[y,b]=S(d,a,!1).split("?"),E=Z(b),w=y+j({...E,...m});return f(w,e)}]},P=()=>{let[n]=c(),[f]=yt(),h=et(()=>{let l=n.split("/").filter(Boolean);return l.length>0?l:[""]},[n]);return et(()=>{let l=[],a=[],m={},e=V(f.entries()),T=null,d=null,y=t,b="",E=0;for(let w of h){let k=b?b+"/"+w:w,U=new Map;for(let K in y){let O=y[K];if(O.isNotFound===!0){if(E>0)throw new Error("Not found route must be defined at the root level");T=O;continue}(g(O.path)?[O.path]:g(O.paths)?ht(O.paths):[]).forEach((L,B)=>{let A={};J(k,L,A)&&U.set(gt("/",K,$(B)),{path:L,config:O,pathParams:A})})}if(U.size>0){let O=Array.from(U.entries()).reduce((A,X)=>s(X[1].path,A[1].path)<0?X:A),[Q,{config:L,pathParams:B}]=O;g(d)&&a.push(d),b="",d=L,y=d.children??{},Object.assign(m,B),l.push(Q.split("/")[0]),E++}else b=b?b+"/"+w:w}return(b||!g(d))&&(d=T),{activatedRoute:d,routeKey:l,parents:a,pathParams:m,queryParams:e,routeKeyString:l.join("."),routeUrlMask:b}},[h,f])};return{RouterContext:i,RouterProvider:R,createUrlFromRouteConfig:p,createUrlsFromRouteConfig:r,useLocation:c,useActivatedRoute:P,useRoute:u,useRoutePathState:(n,f)=>{let[,h]=c(),{routeKey:l,pathParams:a,queryParams:m}=P(),e=a[n],T=ot(d=>{e!==d&&h(l,{...a,[n]:d},m,{replace:!0})},[e,l,a,m]);return[e??f,T]},useRouteQueryState:(n,f)=>{let[,h]=c(),{routeKey:l,pathParams:a,queryParams:m}=P(),e=m[n]??f,T=ot(d=>{if(e!==d){let y={...m};g(d)&&d!==f&&$(d).length>0?y[n]=$(d):delete y[n],h(l,a,y,{replace:!0})}},[e,l,a,m,f]);return[e??f,T]}}};export{D as componentsContextFabric,it as reactRouterFabric,Et as routerContextFabric};
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # @akb2/react-router
2
+
3
+ [Русский](README.ru.md)
4
+
5
+ Route configuration composition and React routing utilities built on Wouter.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add @akb2/react-router react@^18.3.1 wouter@^3.13.0 @akb2/types-tools@^1.5.0
11
+ ```
12
+
13
+ React, Wouter, and `@akb2/types-tools` are required peers. This repository uses pnpm 12.5.1.
14
+
15
+ ## Public API
16
+
17
+ The package entry point currently exports:
18
+
19
+ | Export | Purpose |
20
+ | ------------------------- | -------------------------------------------------------------- |
21
+ | `reactRouterFabric` | Merge route configurations. |
22
+ | `routerContextFabric` | Create the router provider, hooks, and URL builders. |
23
+ | `componentsContextFabric` | Create the components provider and access hook. |
24
+ | `ReactComponents` | Map component props to a typed set of React components. |
25
+ | `RouterComponents` | Required router components: `FullPageLoader` with empty props. |
26
+ | `RoutesDefinition` | Type for rendering definitions. |
27
+ | `RoutesKeys` | Route keys as arrays or dot-separated strings. |
28
+ | `RoutesPathKeys` | Dot-separated route keys. |
29
+
30
+ Import all three factories directly from `@akb2/react-router`.
31
+
32
+ ## Compose route configurations
33
+
34
+ ```tsx
35
+ import { reactRouterFabric } from "@akb2/react-router";
36
+
37
+ const baseRoutes = {
38
+ home: { path: "", Template: () => <h1>Home</h1> },
39
+ users: { path: "users", Template: () => <h1>Users</h1> },
40
+ notFound: { isNotFound: true, Template: () => <h1>Not found</h1> },
41
+ };
42
+
43
+ const userRoutes = {
44
+ detail: {
45
+ putInKey: "users",
46
+ path: ":id",
47
+ Template: () => <h1>User details</h1>,
48
+ },
49
+ };
50
+
51
+ const routes = reactRouterFabric(baseRoutes, userRoutes);
52
+ // routes.users.children.detail → /users/:id
53
+ ```
54
+
55
+ The result is a configuration object, not a React router or provider. Without `putInKey`, routes are added at the root. With `putInKey`, a route is attached to `children` of the named root route. The target is a root key, not a dot-separated path. Duplicate keys and unresolved parent references throw errors; a missing parent currently uses the circular-dependency error message.
56
+
57
+ ## Route configuration
58
+
59
+ | Field | Meaning |
60
+ | ------------------ | ------------------------------------------------------ |
61
+ | `path` | One path; `""` represents the root. |
62
+ | `paths` | Alternative paths for the same route. |
63
+ | `isNotFound` | Marks the fallback route. |
64
+ | `children` | Nested routes appended to their parent path. |
65
+ | `Template` | Component rendered for the active route. |
66
+ | `Module` | Lazy React component rendered with Suspense. |
67
+ | `redirectUrl` | Destination passed to Wouter's `Redirect`. |
68
+ | `props` | Props passed to the active page. |
69
+ | `SuspenseTemplate` | Per-route loading component. |
70
+ | `putInKey` | Parent root key used during configuration composition. |
71
+
72
+ Dynamic path segments use `:name`, for example `users/:id`.
73
+
74
+ `routerContextFabric` validates configurations at creation time. Each route must specify exactly one of `path`, `paths`, or `isNotFound`, and exactly one of `Template`, `Module`, or `redirectUrl`. Alternative paths must be nonempty. A single fallback route is required at the root and cannot have children. Generated duplicate URLs are rejected.
75
+
76
+ ## Create and render a router
77
+
78
+ Using `routes` from the composition example:
79
+
80
+ ```tsx
81
+ import {
82
+ routerContextFabric,
83
+ componentsContextFabric,
84
+ type ReactComponents,
85
+ type RouterComponents,
86
+ } from "@akb2/react-router";
87
+
88
+ type AppComponents = ReactComponents<RouterComponents>;
89
+
90
+ const router = routerContextFabric(routes);
91
+ const { ComponentsProvider } = componentsContextFabric<AppComponents>();
92
+ const components: AppComponents = { FullPageLoader: () => <p>Loading…</p> };
93
+
94
+ export function App() {
95
+ return (
96
+ <ComponentsProvider components={components}>
97
+ <router.RouterProvider />
98
+ </ComponentsProvider>
99
+ );
100
+ }
101
+ ```
102
+
103
+ Create factories outside components. `ComponentsProvider` supplies `FullPageLoader`; it is required by `RouterProvider` even when the current page is synchronous. A route's `SuspenseTemplate` overrides that loader.
104
+
105
+ `RouterProvider` renders the active page and supplies the routes through `RouterContext`. It does not render arbitrary children as an application wrapper. Include navigation and layouts in route components as needed. Parent route templates are not automatically composed as nested layouts.
106
+
107
+ Wouter manages location. To customize its routing environment, place its `Router` above the consumers.
108
+
109
+ ### Shared components
110
+
111
+ `componentsContextFabric()` returns `ComponentsProvider`, `ComponentsContext`, and `useComponents`. Pass a component map containing `FullPageLoader` to the provider. Call `useComponents()` inside its descendants to read that map; calling it outside the provider throws an error.
112
+
113
+ `RouterComponents` describes the required router component props. `ReactComponents<T>` maps props objects to `FC` types while preserving entries that are already component function types. Pass the resulting type as the factory's generic argument.
114
+
115
+ Extend the props map to add shared components:
116
+
117
+ ```tsx
118
+ type ExtendedComponents = ReactComponents<
119
+ RouterComponents & {
120
+ PageTitle: { title: string };
121
+ }
122
+ >;
123
+
124
+ const shared = componentsContextFabric<ExtendedComponents>();
125
+ const extendedComponents: ExtendedComponents = {
126
+ FullPageLoader: () => <p>Loading…</p>,
127
+ PageTitle: ({ title }) => <h1>{title}</h1>,
128
+ };
129
+
130
+ function SharedPageTitle() {
131
+ const { PageTitle } = shared.useComponents();
132
+ return <PageTitle title="Users" />;
133
+ }
134
+
135
+ function SharedComponentsExample() {
136
+ return (
137
+ <shared.ComponentsProvider components={extendedComponents}>
138
+ <SharedPageTitle />
139
+ </shared.ComponentsProvider>
140
+ );
141
+ }
142
+ ```
143
+
144
+ In an application, pass this map to the provider wrapping `RouterProvider` and read it through `shared.useComponents()` inside page components.
145
+
146
+ ## Build URLs
147
+
148
+ ```ts
149
+ router.createUrlFromRouteConfig("users.detail", { id: 42 });
150
+ // "/users/42"
151
+ router.createUrlFromRouteConfig(["users", "detail"], { id: 42 }, { tab: "profile" });
152
+ // "/users/42?tab=profile"
153
+ router.createUrlsFromRouteConfig("users.detail", { id: 42 });
154
+ // ["/users/42"]
155
+ ```
156
+
157
+ Both builders accept `(key, pathParams?, queryParams?, safePathParams?)`. They can be called outside React.
158
+
159
+ - `createUrlFromRouteConfig` uses the first alternative path.
160
+ - `createUrlsFromRouteConfig` returns all combinations of parent and child path alternatives.
161
+ - Query keys and values are encoded; `null` values are omitted.
162
+ - Path values are substituted directly, without URL encoding. Missing values become `*`.
163
+ - Set `safePathParams` to `true` to keep placeholders such as `:id`.
164
+
165
+ ## Navigation and matching hooks
166
+
167
+ Call hooks inside components or custom hooks:
168
+
169
+ ```tsx
170
+ function UserNavigation() {
171
+ const [pathname, navigate] = router.useLocation<{ source: string }>();
172
+ const [matches, params] = router.useRoute("users.detail");
173
+ const active = router.useActivatedRoute();
174
+
175
+ return (
176
+ <button
177
+ onClick={() =>
178
+ navigate("users.detail", { id: 42 }, undefined, {
179
+ replace: true,
180
+ state: { source: "navigation" },
181
+ })
182
+ }
183
+ >
184
+ {pathname}: {matches ? params?.id : active.routeKeyString}
185
+ </button>
186
+ );
187
+ }
188
+ ```
189
+
190
+ | Hook | Result |
191
+ | --------------------- | ---------------------------------------------------------------------------------- |
192
+ | `useLocation<S>()` | `[pathname, navigate]`; pathname excludes the query string. |
193
+ | `useRoute(key)` | Wouter's match tuple with extracted parameters; checks the first path alternative. |
194
+ | `useActivatedRoute()` | Active route metadata, including parent configurations. |
195
+
196
+ `navigate(key, pathParams?, queryParams?, options?)` forwards `replace`, `state`, and `transition` options to Wouter. It selects the first path alternative containing all supplied path parameter keys, or the first alternative if none qualifies.
197
+
198
+ `useActivatedRoute()` returns `activatedRoute`, `routeKey`, `routeKeyString`, `routeUrlMask`, `parents`, `pathParams`, and `queryParams`. Parameters read from the URL are strings. Static segments take priority over parameterized segments when choosing among matching candidates. On fallback, key and parent metadata may retain a matched prefix. Currently `routeUrlMask` contains the unmatched accumulated path and is empty after a complete match; it does not provide the matched route's full mask.
199
+
200
+ ## URL state hooks
201
+
202
+ Inside a component:
203
+
204
+ ```tsx
205
+ function UserIdControl() {
206
+ const [id, setId] = router.useRoutePathState<string>("id", "unknown");
207
+
208
+ return <button onClick={() => setId("43")}>Current user: {id}</button>;
209
+ }
210
+
211
+ function TabControl() {
212
+ const [tab, setTab] = router.useRouteQueryState<string>("tab", "overview");
213
+
214
+ return (
215
+ <div>
216
+ <span>{tab}</span>
217
+ <button onClick={() => setTab("settings")}>Settings</button>
218
+ <button onClick={() => setTab("overview")}>Reset tab</button>
219
+ </div>
220
+ );
221
+ }
222
+ ```
223
+
224
+ `useRoutePathState` reads a path parameter, using the fallback when absent. Its setter affects the URL only if that parameter exists in the current route path.
225
+
226
+ `useRouteQueryState` reads a query parameter. Setting its fallback or an empty string removes that parameter; setting its current value skips navigation.
227
+
228
+ Both setters use history replacement and attempt to preserve other parameters. Both hooks accept `string | number`, but existing URL values remain strings: a numeric generic does not parse numbers.
229
+
230
+ ## Types
231
+
232
+ ```ts
233
+ import type { RoutesKeys, RoutesPathKeys } from "@akb2/react-router";
234
+
235
+ type Key = RoutesKeys<typeof routes>;
236
+ type PathKey = RoutesPathKeys<typeof routes>;
237
+ type Router = typeof router;
238
+ type ActiveRoute = ReturnType<typeof router.useActivatedRoute>;
239
+ ```
240
+
241
+ Keep the inferred route object type to preserve specific route keys. `RoutesDefinition` describes rendering definitions rather than the complete path configuration. Other internal types are not exported at the package root.
242
+
243
+ ## Known limitations
244
+
245
+ Four `it.fails` tests document two implementation defects:
246
+
247
+ - Combining dynamic path parameters with query parameters during navigation can replace the final path parameter with `*`. This also affects the state hooks.
248
+ - A shorter path alternative can match too early, causing active-route detection to return the fallback for a valid longer alternative.
249
+
250
+ Router contexts are shared globally across factory calls, while helpers capture each factory's configuration. Avoid nesting providers from unrelated configurations.
251
+
252
+ ## Development
253
+
254
+ ```sh
255
+ pnpm install
256
+ pnpm test
257
+ pnpm test:watch
258
+ pnpm exec tsc -p tsconfig.test.json --noEmit
259
+ pnpm build
260
+ ```
261
+
262
+ Tests in `tests/router-context-fabric` cover each feature in a separate file. Expected-failure tests execute normally and must lose their `it.fails` annotation once the corresponding defect is fixed.
263
+
264
+ `pnpm build` generates ESM, CommonJS, and TypeScript declarations in `.dist` from the public entry point.
265
+
266
+ ## License
267
+
268
+ [MPL-2.0](LICENSE)
package/README.ru.md ADDED
@@ -0,0 +1,268 @@
1
+ # @akb2/react-router
2
+
3
+ [English](README.md)
4
+
5
+ Объединение конфигураций маршрутов и инструменты React-навигации на основе Wouter.
6
+
7
+ ## Установка
8
+
9
+ ```sh
10
+ pnpm add @akb2/react-router react@^18.3.1 wouter@^3.13.0 @akb2/types-tools@^1.5.0
11
+ ```
12
+
13
+ React, Wouter и `@akb2/types-tools` обязательны как peer dependencies. В репозитории используется pnpm 12.5.1.
14
+
15
+ ## Публичный API
16
+
17
+ Точка входа пакета сейчас экспортирует:
18
+
19
+ | Экспорт | Назначение |
20
+ | ------------------------- | ----------------------------------------------------------------------- |
21
+ | `reactRouterFabric` | Объединение конфигураций маршрутов. |
22
+ | `routerContextFabric` | Создание провайдера роутера, хуков и функций построения URL. |
23
+ | `componentsContextFabric` | Создание провайдера компонентов и хука доступа к ним. |
24
+ | `ReactComponents` | Преобразование набора пропсов в типизированный набор React-компонентов. |
25
+ | `RouterComponents` | Обязательные компоненты роутера: `FullPageLoader` с пустыми пропсами. |
26
+ | `RoutesDefinition` | Тип определений отображения. |
27
+ | `RoutesKeys` | Ключи маршрутов в виде массивов или строк с точками. |
28
+ | `RoutesPathKeys` | Ключи маршрутов в виде строк с точками. |
29
+
30
+ Все три фабрики импортируются напрямую из `@akb2/react-router`.
31
+
32
+ ## Объединение конфигураций
33
+
34
+ ```tsx
35
+ import { reactRouterFabric } from "@akb2/react-router";
36
+
37
+ const baseRoutes = {
38
+ home: { path: "", Template: () => <h1>Home</h1> },
39
+ users: { path: "users", Template: () => <h1>Users</h1> },
40
+ notFound: { isNotFound: true, Template: () => <h1>Not found</h1> },
41
+ };
42
+
43
+ const userRoutes = {
44
+ detail: {
45
+ putInKey: "users",
46
+ path: ":id",
47
+ Template: () => <h1>User details</h1>,
48
+ },
49
+ };
50
+
51
+ const routes = reactRouterFabric(baseRoutes, userRoutes);
52
+ // routes.users.children.detail → /users/:id
53
+ ```
54
+
55
+ Результат — объект конфигурации. Без `putInKey` маршрут добавляется в корень. С `putInKey` маршрут помещается в `children` указанного корневого маршрута. Цель — корневой ключ, а не путь с точками. Повторяющиеся ключи и неразрешённые ссылки на родителей вызывают ошибки; для отсутствующего родителя сейчас используется сообщение о циклической зависимости.
56
+
57
+ ## Конфигурация маршрута
58
+
59
+ | Поле | Значение |
60
+ | ------------------ | ------------------------------------------------------------- |
61
+ | `path` | Один путь; `""` обозначает корень. |
62
+ | `paths` | Альтернативные пути одного маршрута. |
63
+ | `isNotFound` | Пометка запасного маршрута. |
64
+ | `children` | Вложенные маршруты, пути которых добавляются к родительскому. |
65
+ | `Template` | Компонент активной страницы. |
66
+ | `Module` | Ленивый React-компонент, отображаемый через Suspense. |
67
+ | `redirectUrl` | Адрес для `Redirect` из Wouter. |
68
+ | `props` | Пропсы активной страницы. |
69
+ | `SuspenseTemplate` | Компонент загрузки для конкретного маршрута. |
70
+ | `putInKey` | Корневой ключ родителя для объединения конфигураций. |
71
+
72
+ Динамические сегменты записываются как `:name`, например `users/:id`.
73
+
74
+ `routerContextFabric` проверяет конфигурацию при создании. Каждый маршрут должен содержать ровно одно из полей `path`, `paths`, `isNotFound` и ровно одно из полей `Template`, `Module`, `redirectUrl`. Массив альтернативных путей не должен быть пустым. В корне обязателен единственный запасной маршрут без дочерних элементов. Повторяющиеся сгенерированные URL запрещены.
75
+
76
+ ## Создание и отображение роутера
77
+
78
+ Используя `routes` из примера объединения:
79
+
80
+ ```tsx
81
+ import {
82
+ routerContextFabric,
83
+ componentsContextFabric,
84
+ type ReactComponents,
85
+ type RouterComponents,
86
+ } from "@akb2/react-router";
87
+
88
+ type AppComponents = ReactComponents<RouterComponents>;
89
+
90
+ const router = routerContextFabric(routes);
91
+ const { ComponentsProvider } = componentsContextFabric<AppComponents>();
92
+ const components: AppComponents = { FullPageLoader: () => <p>Loading…</p> };
93
+
94
+ export function App() {
95
+ return (
96
+ <ComponentsProvider components={components}>
97
+ <router.RouterProvider />
98
+ </ComponentsProvider>
99
+ );
100
+ }
101
+ ```
102
+
103
+ Создавайте фабрики вне компонентов. `ComponentsProvider` предоставляет `FullPageLoader`; он необходим для `RouterProvider`, даже когда текущая страница синхронная. Поле `SuspenseTemplate` маршрута заменяет этот загрузчик.
104
+
105
+ `RouterProvider` отображает активную страницу и предоставляет конфигурацию через `RouterContext`. Он не отображает произвольные дочерние элементы как обёртка приложения. Размещайте навигацию и макеты в компонентах маршрутов. Шаблоны родительских маршрутов автоматически не объединяются во вложенные макеты.
106
+
107
+ Адресом управляет Wouter. Для настройки окружения маршрутизации разместите его `Router` выше потребителей.
108
+
109
+ ### Общие компоненты
110
+
111
+ `componentsContextFabric()` возвращает `ComponentsProvider`, `ComponentsContext` и `useComponents`. Передайте провайдеру набор компонентов с обязательным `FullPageLoader`. Хук `useComponents()` читает этот набор внутри потомков провайдера; вызов вне провайдера вызывает ошибку.
112
+
113
+ `RouterComponents` описывает пропсы обязательных компонентов роутера. `ReactComponents<T>` преобразует объекты пропсов в типы `FC`, сохраняя уже заданные типы функций компонентов. Передайте результат как generic-аргумент фабрики.
114
+
115
+ Для дополнительных общих компонентов расширьте набор пропсов:
116
+
117
+ ```tsx
118
+ type ExtendedComponents = ReactComponents<
119
+ RouterComponents & {
120
+ PageTitle: { title: string };
121
+ }
122
+ >;
123
+
124
+ const shared = componentsContextFabric<ExtendedComponents>();
125
+ const extendedComponents: ExtendedComponents = {
126
+ FullPageLoader: () => <p>Загрузка…</p>,
127
+ PageTitle: ({ title }) => <h1>{title}</h1>,
128
+ };
129
+
130
+ function SharedPageTitle() {
131
+ const { PageTitle } = shared.useComponents();
132
+ return <PageTitle title="Пользователи" />;
133
+ }
134
+
135
+ function SharedComponentsExample() {
136
+ return (
137
+ <shared.ComponentsProvider components={extendedComponents}>
138
+ <SharedPageTitle />
139
+ </shared.ComponentsProvider>
140
+ );
141
+ }
142
+ ```
143
+
144
+ В приложении этот набор можно передать провайдеру, который оборачивает `RouterProvider`, и читать его через `shared.useComponents()` в компонентах страниц.
145
+
146
+ ## Построение URL
147
+
148
+ ```ts
149
+ router.createUrlFromRouteConfig("users.detail", { id: 42 });
150
+ // "/users/42"
151
+ router.createUrlFromRouteConfig(["users", "detail"], { id: 42 }, { tab: "profile" });
152
+ // "/users/42?tab=profile"
153
+ router.createUrlsFromRouteConfig("users.detail", { id: 42 });
154
+ // ["/users/42"]
155
+ ```
156
+
157
+ Обе функции принимают `(key, pathParams?, queryParams?, safePathParams?)`. Их можно вызывать вне React.
158
+
159
+ - `createUrlFromRouteConfig` использует первый альтернативный путь.
160
+ - `createUrlsFromRouteConfig` возвращает все комбинации альтернатив родителей и потомков.
161
+ - Ключи и значения query-параметров кодируются; значения `null` пропускаются.
162
+ - Значения path-параметров подставляются напрямую, без кодирования. Отсутствующие значения заменяются на `*`.
163
+ - При `safePathParams: true` сохраняются плейсхолдеры вроде `:id`.
164
+
165
+ ## Хуки навигации и сопоставления
166
+
167
+ Вызывайте хуки внутри компонентов или собственных хуков:
168
+
169
+ ```tsx
170
+ function UserNavigation() {
171
+ const [pathname, navigate] = router.useLocation<{ source: string }>();
172
+ const [matches, params] = router.useRoute("users.detail");
173
+ const active = router.useActivatedRoute();
174
+
175
+ return (
176
+ <button
177
+ onClick={() =>
178
+ navigate("users.detail", { id: 42 }, undefined, {
179
+ replace: true,
180
+ state: { source: "navigation" },
181
+ })
182
+ }
183
+ >
184
+ {pathname}: {matches ? params?.id : active.routeKeyString}
185
+ </button>
186
+ );
187
+ }
188
+ ```
189
+
190
+ | Хук | Результат |
191
+ | --------------------- | ---------------------------------------------------------------------------------------------- |
192
+ | `useLocation<S>()` | `[pathname, navigate]`; pathname не содержит query string. |
193
+ | `useRoute(key)` | Результат сопоставления Wouter с извлечёнными параметрами; проверяет первую альтернативу пути. |
194
+ | `useActivatedRoute()` | Метаданные активного маршрута, включая конфигурации родителей. |
195
+
196
+ `navigate(key, pathParams?, queryParams?, options?)` передаёт опции `replace`, `state`, `transition` в Wouter. Выбирается первая альтернатива пути со всеми переданными ключами path-параметров либо первая альтернатива, если подходящей нет.
197
+
198
+ `useActivatedRoute()` возвращает `activatedRoute`, `routeKey`, `routeKeyString`, `routeUrlMask`, `parents`, `pathParams`, `queryParams`. Параметры из URL являются строками. При выборе среди совпавших кандидатов статические сегменты приоритетнее параметризованных. При переходе к запасному маршруту метаданные ключей и родителей могут сохранять совпавшую часть дерева. Сейчас `routeUrlMask` содержит накопленный несовпавший путь и пуст после полного совпадения; полной маски найденного маршрута в нём нет.
199
+
200
+ ## Хуки состояния URL
201
+
202
+ Внутри компонента:
203
+
204
+ ```tsx
205
+ function UserIdControl() {
206
+ const [id, setId] = router.useRoutePathState<string>("id", "unknown");
207
+
208
+ return <button onClick={() => setId("43")}>Текущий пользователь: {id}</button>;
209
+ }
210
+
211
+ function TabControl() {
212
+ const [tab, setTab] = router.useRouteQueryState<string>("tab", "overview");
213
+
214
+ return (
215
+ <div>
216
+ <span>{tab}</span>
217
+ <button onClick={() => setTab("settings")}>Настройки</button>
218
+ <button onClick={() => setTab("overview")}>Сбросить вкладку</button>
219
+ </div>
220
+ );
221
+ }
222
+ ```
223
+
224
+ `useRoutePathState` читает параметр пути и возвращает fallback при его отсутствии. Сеттер влияет на URL только при наличии параметра в текущем пути маршрута.
225
+
226
+ `useRouteQueryState` читает query-параметр. Установка fallback или пустой строки удаляет параметр; установка текущего значения не вызывает навигацию.
227
+
228
+ Оба сеттера заменяют текущую запись истории и пытаются сохранить остальные параметры. Оба хука принимают `string | number`, но существующие значения URL остаются строками: числовой generic не преобразует их в числа.
229
+
230
+ ## Типы
231
+
232
+ ```ts
233
+ import type { RoutesKeys, RoutesPathKeys } from "@akb2/react-router";
234
+
235
+ type Key = RoutesKeys<typeof routes>;
236
+ type PathKey = RoutesPathKeys<typeof routes>;
237
+ type Router = typeof router;
238
+ type ActiveRoute = ReturnType<typeof router.useActivatedRoute>;
239
+ ```
240
+
241
+ Сохраняйте выведенный тип объекта маршрутов, чтобы не потерять конкретные ключи. `RoutesDefinition` описывает определения отображения, а не полную конфигурацию путей. Остальные внутренние типы не экспортируются из корня пакета.
242
+
243
+ ## Известные ограничения
244
+
245
+ Четыре теста с `it.fails` фиксируют две ошибки реализации:
246
+
247
+ - При навигации с динамическими path-параметрами и query-параметрами последний параметр пути может заменяться на `*`. Это также затрагивает хуки состояния.
248
+ - Короткая альтернатива пути может совпасть слишком рано, из-за чего определение активного маршрута возвращает запасной маршрут для корректной длинной альтернативы.
249
+
250
+ Контексты роутера глобально разделяются между вызовами фабрики, а функции замыкают конфигурацию своей фабрики. Избегайте вложенных провайдеров с несвязанными конфигурациями.
251
+
252
+ ## Разработка
253
+
254
+ ```sh
255
+ pnpm install
256
+ pnpm test
257
+ pnpm test:watch
258
+ pnpm exec tsc -p tsconfig.test.json --noEmit
259
+ pnpm build
260
+ ```
261
+
262
+ В `tests/router-context-fabric` каждая возможность проверяется в отдельном файле. Тесты ожидаемых ошибок выполняются; после исправления соответствующей ошибки нужно удалить пометку `it.fails`.
263
+
264
+ `pnpm build` создаёт ESM, CommonJS и декларации TypeScript в `.dist` из публичной точки входа.
265
+
266
+ ## Лицензия
267
+
268
+ [MPL-2.0](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akb2/react-router",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "The context for react router",
5
5
  "exports": {
6
6
  ".": {