waymark 0.5.0 → 0.6.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/README.md +14 -13
- package/dist/index.d.ts +9 -9
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ Waymark is a routing library for React built around three core ideas: **type saf
|
|
|
79
79
|
| **Route match ranking**\* | ✅ | ✅ | ✅ | ❌ |
|
|
80
80
|
| **View transitions** | ✅ | ✅ | ✅ | ✅ |
|
|
81
81
|
| **Devtools** | ✅ | ⚠️ | ✅ | ❌ |
|
|
82
|
+
| **Navigation blockers** | 🔨 | ✅ | ✅ | ❌ |
|
|
82
83
|
| **File-based routing** | ❌ | ✅ | ✅ | ❌ |
|
|
83
84
|
| **React Native** | ❌ | ✅ | ❌ | ❌ |
|
|
84
85
|
|
|
@@ -1324,24 +1325,23 @@ function AppLayout() {
|
|
|
1324
1325
|
|
|
1325
1326
|
## Matching a route anywhere
|
|
1326
1327
|
|
|
1327
|
-
Use `useMatch` to check if a route
|
|
1328
|
+
Use `useMatch` to check if a route matches the current path from anywhere in your component tree. You can pass either a route pattern string or a route object, just like with `Link` and `navigate`. This is useful for conditional rendering, styling, access control, and more. It's also used internally by `useParams` and `Link`.
|
|
1328
1329
|
|
|
1329
|
-
The hook returns
|
|
1330
|
+
The hook returns a Match object (containing `route` and `params`) if there's a match, or `null` otherwise. There are two matching modes:
|
|
1330
1331
|
|
|
1331
|
-
- **Loose matching** (default): Matches if
|
|
1332
|
-
- **Strict matching** (`strict: true`): Matches only if
|
|
1332
|
+
- **Loose matching** (default): Matches if the path starts with the route pattern (e.g., `/dashboard` matches `/dashboard/settings`).
|
|
1333
|
+
- **Strict matching** (`strict: true`): Matches only if the path exactly matches the route pattern.
|
|
1333
1334
|
|
|
1334
1335
|
```tsx
|
|
1335
|
-
import {
|
|
1336
|
+
import { useMatch } from "waymark";
|
|
1336
1337
|
|
|
1337
1338
|
const dashboard = route("/dashboard").component(Dashboard);
|
|
1338
|
-
const settings = dashboard.route("/settings").component(Settings);
|
|
1339
1339
|
|
|
1340
1340
|
function Sidebar() {
|
|
1341
|
-
// Matches /dashboard
|
|
1341
|
+
// Matches /dashboard, /dashboard/anything, etc.
|
|
1342
1342
|
const match = useMatch({ from: dashboard });
|
|
1343
1343
|
|
|
1344
|
-
// Matches only /dashboard
|
|
1344
|
+
// Matches only /dashboard
|
|
1345
1345
|
const match = useMatch({ from: dashboard, strict: true });
|
|
1346
1346
|
|
|
1347
1347
|
return <nav>{match && <DashboardMenu />}</nav>;
|
|
@@ -1512,14 +1512,14 @@ const url = router.createUrl({ to: userProfile, params: { id: "42" } });
|
|
|
1512
1512
|
// Returns "/users/42"
|
|
1513
1513
|
```
|
|
1514
1514
|
|
|
1515
|
-
**`router.match(path,
|
|
1515
|
+
**`router.match(path, options)`** checks if a path matches a specific route.
|
|
1516
1516
|
|
|
1517
1517
|
- `path` - `string` - The path to match against
|
|
1518
|
-
- `
|
|
1518
|
+
- `options` - `MatchOptions` - Matching options
|
|
1519
1519
|
- Returns: `Match | null` - The match result or null if no match
|
|
1520
1520
|
|
|
1521
1521
|
```tsx
|
|
1522
|
-
const match = router.match("/users/42",
|
|
1522
|
+
const match = router.match("/users/42", { from: "/users/:id" });
|
|
1523
1523
|
// Returns { route, params: { id: "42" } }
|
|
1524
1524
|
```
|
|
1525
1525
|
|
|
@@ -1743,7 +1743,7 @@ setSearch({ page: 1 }, true); // Replace instead of push
|
|
|
1743
1743
|
**`useMatch(options)`** checks if a route matches the current path.
|
|
1744
1744
|
|
|
1745
1745
|
- `options` - `MatchOptions` - Matching options
|
|
1746
|
-
- Returns: `
|
|
1746
|
+
- Returns: `Match | null` - The match result or null if no match
|
|
1747
1747
|
|
|
1748
1748
|
```tsx
|
|
1749
1749
|
const match = useMatch({ from: "/users/:id" });
|
|
@@ -1910,7 +1910,7 @@ interface HistoryPushOptions {
|
|
|
1910
1910
|
```tsx
|
|
1911
1911
|
type MatchOptions = {
|
|
1912
1912
|
from: Pattern | Route; // The route to match against
|
|
1913
|
-
strict?: boolean; //
|
|
1913
|
+
strict?: boolean; // Require exact match (default: false, matches prefixes)
|
|
1914
1914
|
params?: Partial<Params>; // Optional param values to filter by
|
|
1915
1915
|
};
|
|
1916
1916
|
```
|
|
@@ -1963,6 +1963,7 @@ interface PreloadContext {
|
|
|
1963
1963
|
- Possibility to pass an arbitrary context to the Router instance for later use in preloads?
|
|
1964
1964
|
- Relative path navigation? Not sure it's worth the extra bundle size given that users can export/import route objects and pass them as navigation option.
|
|
1965
1965
|
- Document usage in test environments
|
|
1966
|
+
- Navigation blockers (`useBlocker`, etc.)
|
|
1966
1967
|
- Open to suggestions, we can discuss them [here](https://github.com/strblr/waymark/discussions).
|
|
1967
1968
|
|
|
1968
1969
|
---
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ type NormalizePath<P extends string> = RemoveTrailingSlash<DedupSlashes<`/${P}`>
|
|
|
10
10
|
type DedupSlashes<P extends string> = P extends `${infer Prefix}//${infer Rest}` ? `${Prefix}${DedupSlashes<`/${Rest}`>}` : P;
|
|
11
11
|
type RemoveTrailingSlash<P extends string> = P extends `${infer Prefix}/` ? Prefix extends "" ? "/" : Prefix : P;
|
|
12
12
|
type MaybeKey<K extends string, T> = T extends EmptyObject ? { [P in K]?: EmptyObject } : {} extends T ? { [P in K]?: T } : { [P in K]: T };
|
|
13
|
-
type OptionalOnUndefined<T extends object> = Simplify<{ [K in keyof T as undefined extends T[K] ? never : K]: T[K] } & { [K in keyof T as undefined extends T[K] ? K : never]?: T[K] }>;
|
|
14
13
|
//#endregion
|
|
15
14
|
//#region src/types.d.ts
|
|
16
15
|
interface Register {}
|
|
@@ -21,8 +20,8 @@ type Handle = Register extends {
|
|
|
21
20
|
handle: infer Handle;
|
|
22
21
|
} ? Handle : any;
|
|
23
22
|
interface Middleware<S extends {} = any> {
|
|
24
|
-
use: <S2 extends {}>(middleware: Middleware<S2>) => Middleware<Merge<S,
|
|
25
|
-
search: <S2 extends {}>(validate: Validator<S, S2>) => Middleware<Merge<S,
|
|
23
|
+
use: <S2 extends {}>(middleware: Middleware<S2>) => Middleware<Merge<S, S2>>;
|
|
24
|
+
search: <S2 extends {}>(validate: Validator<S, S2>) => Middleware<Merge<S, S2>>;
|
|
26
25
|
handle: (handle: Handle) => Middleware<S>;
|
|
27
26
|
preload: (preload: (context: PreloadContext<{}, S>) => Promise<any>) => Middleware<S>;
|
|
28
27
|
component: (component: ComponentType) => Middleware<S>;
|
|
@@ -65,7 +64,7 @@ type NavigateOptions<P extends Pattern> = {
|
|
|
65
64
|
to: P | GetRoute<P>;
|
|
66
65
|
replace?: boolean;
|
|
67
66
|
state?: any;
|
|
68
|
-
} & MaybeKey<"params", Params<P>> & MaybeKey<"search", Search<P
|
|
67
|
+
} & MaybeKey<"params", Params<P>> & MaybeKey<"search", Partial<Search<P>>>;
|
|
69
68
|
interface LinkOptions {
|
|
70
69
|
strict?: boolean;
|
|
71
70
|
preload?: "intent" | "render" | "viewport" | false;
|
|
@@ -108,12 +107,13 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
108
107
|
pattern: P;
|
|
109
108
|
keys: string[];
|
|
110
109
|
regex: RegExp;
|
|
110
|
+
loose: RegExp;
|
|
111
111
|
weights: number[];
|
|
112
112
|
validate: (search: Record<string, unknown>) => S;
|
|
113
113
|
handles: Handle[];
|
|
114
114
|
components: ComponentType[];
|
|
115
115
|
preloads: ((context: PreloadContext) => Promise<any>)[];
|
|
116
|
-
|
|
116
|
+
p?: Route;
|
|
117
117
|
};
|
|
118
118
|
readonly _types: {
|
|
119
119
|
params: Ps;
|
|
@@ -121,8 +121,8 @@ declare class Route<P extends string = string, Ps extends {} = any, S extends {}
|
|
|
121
121
|
};
|
|
122
122
|
constructor(_: typeof this._);
|
|
123
123
|
route: <P2 extends string>(pattern: P2) => Route<NormalizePath<`${P}/${P2}`>, ParsePattern<NormalizePath<`${P}/${P2}`>>, S>;
|
|
124
|
-
use: <S2 extends {}>(middleware: Middleware<S2>) => Route<P, Ps, Merge<S,
|
|
125
|
-
search: <S2 extends {}>(validate: Validator<S, S2>) => Route<P, Ps, Merge<S,
|
|
124
|
+
use: <S2 extends {}>(middleware: Middleware<S2>) => Route<P, Ps, Merge<S, S2>>;
|
|
125
|
+
search: <S2 extends {}>(validate: Validator<S, S2>) => Route<P, Ps, Merge<S, S2>>;
|
|
126
126
|
handle: (handle: Handle) => Route<P, Ps, S>;
|
|
127
127
|
preload: (preload: (context: PreloadContext<Ps, S>) => Promise<any>) => Route<P, Ps, S>;
|
|
128
128
|
component: (component: ComponentType) => Route<P, Ps, S>;
|
|
@@ -144,7 +144,7 @@ declare class Router {
|
|
|
144
144
|
private readonly _;
|
|
145
145
|
constructor(options: RouterOptions);
|
|
146
146
|
getRoute: <P extends Pattern>(pattern: P | GetRoute<P>) => GetRoute<P>;
|
|
147
|
-
match: <P extends Pattern>(path: string,
|
|
147
|
+
match: <P extends Pattern>(path: string, options: MatchOptions<P>) => Match<P> | null;
|
|
148
148
|
matchAll: (path: string) => Match | null;
|
|
149
149
|
createUrl: <P extends Pattern>(options: NavigateOptions<P>) => string;
|
|
150
150
|
preload: <P extends Pattern>(options: NavigateOptions<P>) => Promise<void>;
|
|
@@ -196,7 +196,7 @@ declare function Link<P extends Pattern>(props: LinkProps<P>): ReactNode;
|
|
|
196
196
|
//#region src/react/hooks.d.ts
|
|
197
197
|
declare function useRouter(): Router;
|
|
198
198
|
declare function useLocation(): HistoryLocation;
|
|
199
|
-
declare function useMatch<P extends Pattern>(options: MatchOptions<P>):
|
|
199
|
+
declare function useMatch<P extends Pattern>(options: MatchOptions<P>): Match<P> | null;
|
|
200
200
|
declare function useOutlet(): ReactNode;
|
|
201
201
|
declare function useNavigate(): <P extends Pattern>(options: number | HistoryPushOptions | NavigateOptions<P>) => void;
|
|
202
202
|
declare function useHandles(): Handle[];
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Component as e,Suspense as t,cloneElement as n,createContext as r,isValidElement as i,lazy as a,memo as o,useCallback as s,useContext as c,useEffect as l,useInsertionEffect as u,useLayoutEffect as d,useMemo as f,useRef as p,useState as m,useSyncExternalStore as h}from"react";import{inject as g,parse as _}from"regexparam";import{jsx as v}from"react/jsx-runtime";function y(e){return`/${e}`.replaceAll(/\/+/g,`/`).replace(/(.+)\/$/,`$1`)}function b(e){let{keys:t,pattern:n}=_(e);return{pattern:e,keys:t,regex:n,weights:e.split(`/`).slice(1).map(e=>e.includes(`*`)?0:e.includes(`:`)?1:2)}}function x(e){return typeof e==`function`?e:t=>{let n=e[`~standard`].validate(t);if(n instanceof Promise)throw Error(`[Waymark] Validation
|
|
1
|
+
import{Component as e,Suspense as t,cloneElement as n,createContext as r,isValidElement as i,lazy as a,memo as o,useCallback as s,useContext as c,useEffect as l,useInsertionEffect as u,useLayoutEffect as d,useMemo as f,useRef as p,useState as m,useSyncExternalStore as h}from"react";import{inject as g,parse as _}from"regexparam";import{jsx as v}from"react/jsx-runtime";function y(e){return`/${e}`.replaceAll(/\/+/g,`/`).replace(/(.+)\/$/,`$1`)}function b(e){let{keys:t,pattern:n}=_(e);return{pattern:e,keys:t,regex:n,loose:_(e,!0).pattern,weights:e.split(`/`).slice(1).map(e=>e.includes(`*`)?0:e.includes(`:`)?1:2)}}function x(e){return typeof e==`function`?e:t=>{let n=e[`~standard`].validate(t);if(n instanceof Promise)throw Error(`[Waymark] Validation can't be async`);if(n.issues)throw Error(`[Waymark] Validation failed`,{cause:n.issues});return n.value}}function S(e){return Object.entries(e).filter(([e,t])=>t!==void 0).map(([e,t])=>`${e}=${encodeURIComponent(w(t))}`).join(`&`)}function C(e){let t=new URLSearchParams(e);return Object.fromEntries([...t.entries()].map(([e,t])=>(t=decodeURIComponent(t),[e,T(t)?JSON.parse(t):t])))}function w(e){return typeof e==`string`&&!T(e)?e:JSON.stringify(e)}function T(e){try{return JSON.parse(e),!0}catch{return!1}}function E(e,t){return y(`${t}/${e}`)}function D(e,t){return(e===t||e.startsWith(`${t}/`))&&(e=e.slice(t.length)||`/`),e}function O(e,t){return[e,S(t)].filter(Boolean).join(`?`)}function k(e){let{pathname:t,search:n}=new URL(e,`http://w`);return{path:t,search:C(n)}}function A({keys:e,regex:t,loose:n},r,i,a){let o=(r?t:n).exec(D(i,a));if(!o)return null;let s={};return e.forEach((e,t)=>{let n=o[t+1];n&&(s[e]=n)}),s}function j(e){return[...e].sort((e,t)=>{let n=e.route._.weights,r=t.route._.weights,i=Math.max(n.length,r.length);for(let e=0;e<i;e++){let t=n[e]??-1,i=r[e]??-1;if(t!==i)return i-t}return 0})}const M=r(null),N=r(null),P=r(null),F=r(null);function I(){let e=c(M);if(e)return e;throw Error(`[Waymark] useRouter must be within a router context`)}function L(){let e=c(N);if(e)return e;throw Error(`[Waymark] useLocation must be within a router context`)}function R(e){let t=I(),{path:n}=L();return f(()=>t.match(n,e),[t,n,e.from,e.strict,e.params])}function z(){return c(F)}function B(){return I().navigate}function V(){let e=c(P);return f(()=>e?.route._.handles??[],[e])}function H(e){let t=R({from:e});if(t)return t.params;throw Error(`[Waymark] Can't read params for non-matching route ${e}`)}function U(e){let t=I(),{search:n,path:r}=L(),i=t.getRoute(e),a=f(()=>i._.validate(n),[i,n]);return[a,Z((e,n)=>{e=typeof e==`function`?e(a):e;let i=O(r,{...a,...e});t.navigate({url:i,replace:n})})]}var W=class{_;_loc=(e,t)=>{let{state:n}=history,[r,i]=this._??[];return i?.path===e&&r===t&&i.state===n?i:(this._=[t,{path:e,search:C(t),state:n}])[1]};constructor(){if(!window[G]){for(let e of[K,q]){let t=history[e];history[e]=function(...n){t.apply(this,n);let r=new Event(e);dispatchEvent(r)}}window[G]=1}}location=()=>this._loc(location.pathname,location.search);go=e=>history.go(e);push=e=>{let{url:t,replace:n,state:r}=e;history[n?q:K](r,``,t)};subscribe=e=>(J.forEach(t=>window.addEventListener(t,e)),()=>{J.forEach(t=>window.removeEventListener(t,e))})};const G=Symbol.for(`wmp01`),K=`pushState`,q=`replaceState`,J=[`popstate`,K,q,`hashchange`];var Y=class{routes;basePath;history;ssrContext;defaultLinkOptions;_;constructor(e){let{routes:t,basePath:n=`/`,history:r,ssrContext:i,defaultLinkOptions:a}=e;this.routes=t,this.basePath=y(n),this.history=r??new W,this.ssrContext=i,this.defaultLinkOptions=a,this._={routeMap:new Map(t.map(e=>[e._.pattern,e]))}}getRoute=e=>{if(typeof e!=`string`)return e;let t=this._.routeMap.get(e);if(!t)throw Error(`[Waymark] Route not found for ${e}`);return t};match=(e,t)=>{let{from:n,strict:r,params:i}=t,a=this.getRoute(n),o=A(a._,r,e,this.basePath);return o&&(!i||Object.keys(i).every(e=>i[e]===o[e]))?{route:a,params:o}:null};matchAll=e=>j(this.routes.map(t=>this.match(e,{from:t,strict:!0})).filter(e=>!!e))[0]??null;createUrl=e=>{let{to:t,params:n={},search:r={}}=e,{pattern:i}=this.getRoute(t)._;return O(E(g(i,n),this.basePath),r)};preload=async e=>{let{to:t,params:n={},search:r={}}=e,{preloads:i}=this.getRoute(t)._;await Promise.all(i.map(e=>e({params:n,search:r})))};navigate=e=>{if(typeof e==`number`)this.history.go(e);else if(`url`in e)this.history.push(e);else{let{replace:t,state:n}=e;this.history.push({url:this.createUrl(e),replace:t,state:n})}}},X=class{stack=[];index=0;listeners=new Set;constructor(e=`/`){this.stack.push({...k(e),state:void 0})}location=()=>this.stack[this.index];go=e=>{let t=this.index+e;this.stack[t]&&(this.index=t,this.listeners.forEach(e=>e()))};push=e=>{let{url:t,replace:n,state:r}=e,i={...k(t),state:r};this.stack=this.stack.slice(0,this.index+1),n?this.stack[this.index]=i:this.index=this.stack.push(i)-1,this.listeners.forEach(e=>e())};subscribe=e=>(this.listeners.add(e),()=>{this.listeners.delete(e)})},ee=class extends W{location=()=>{let{pathname:e,search:t}=new URL(location.hash.slice(1),`http://w`);return this._loc(e,t)};push=e=>{let{url:t,replace:n,state:r}=e;history[n?`replaceState`:`pushState`](r,``,`#${t}`)}};function te(e){let[t]=m(()=>`router`in e?e.router:new Y(e)),{subscribe:n,location:r}=t.history,i=h(n,r,r),a=f(()=>t.matchAll(i.path),[t,i.path]);return a||console.error(`[Waymark] No matching route for path`,i.path),f(()=>v(M.Provider,{value:t,children:v(N.Provider,{value:i,children:v(P.Provider,{value:a,children:a?.route._.components.reduceRight((e,t)=>v(F.Provider,{value:e,children:v(t,{})}),null)})})}),[t,i,a])}function ne(){return z()}function re(e){let t=I();return d(()=>t.navigate(e),[]),t.ssrContext&&(t.ssrContext.redirect=t.createUrl(e)),null}function ie(e){let t=I(),{to:r,replace:a,state:o,params:c,search:u,strict:d,preload:m,preloadDelay:h=50,style:g,className:_,activeStyle:y,activeClassName:b,asChild:x,children:S,...C}={...t.defaultLinkOptions,...e},w=p(null),T=p(null),E=t.createUrl(e),D=!!R({from:r,strict:d,params:c}),O=Z(()=>t.preload(e)),k=s(()=>{clearTimeout(T.current)},[]),A=s(()=>{k(),T.current=setTimeout(O,h)},[h,k]),j=f(()=>({"data-active":D,style:{...g,...D&&y},className:[_,D&&b].filter(Boolean).join(` `)||void 0}),[D,g,_,y,b]);l(()=>{if(m===`render`)A();else if(m===`viewport`&&w.current){let e=new IntersectionObserver(e=>e.forEach(e=>{e.isIntersecting?A():k()}));return e.observe(w.current),()=>{e.disconnect(),k()}}return k},[m,A,k]);let M=e=>{C.onClick?.(e),!(e.ctrlKey||e.metaKey||e.shiftKey||e.altKey||e.button!==0||e.defaultPrevented)&&(e.preventDefault(),t.navigate({url:E,replace:a,state:o}))},N=e=>{C.onFocus?.(e),m===`intent`&&!e.defaultPrevented&&A()},P=e=>{C.onBlur?.(e),m===`intent`&&k()},F=e=>{C.onPointerEnter?.(e),m===`intent`&&!e.defaultPrevented&&A()},L=e=>{C.onPointerLeave?.(e),m===`intent`&&k()},z={...C,...j,ref:ae(w,C.ref),href:E,onClick:M,onFocus:N,onBlur:P,onPointerEnter:F,onPointerLeave:L};return x&&i(S)?n(S,z):v(`a`,{...z,children:S})}function ae(e,t){return t?n=>{e.current=n;let r=typeof t==`function`?t(n):void(t.current=n);return r&&(()=>{e.current=null,r()})}:e}function Z(e){let t=p(e);return u(()=>{t.current=e},[e]),p(((...e)=>t.current(...e))).current}function oe(e){return()=>v(t,{fallback:v(e,{}),children:z()})}function se(t){class n extends e{constructor(e){super(e),this.state={children:e.children,error:null}}static getDerivedStateFromError(e){return{error:[e]}}static getDerivedStateFromProps(e,t){return e.children===t.children?t:{children:e.children,error:null}}render(){return this.state.error?v(t,{error:this.state.error[0]}):this.props.children}}return()=>v(n,{children:z()})}function Q(e){return new $({...b(y(e)),validate:e=>e,handles:[],components:[],preloads:[]})}function ce(){return Q(``)}var $=class e{_;_types;constructor(e){this._=e}route=t=>new e({...this._,...b(y(`${this._.pattern}/${t}`)),p:this});use=t=>{let{_:n}=t;return new e({...this._,handles:[...this._.handles,...n.handles],components:[...this._.components,...n.components],preloads:[...this._.preloads,...n.preloads]}).search(n.validate)};search=t=>(t=x(t),new e({...this._,validate:e=>{let n=this._.validate(e);return{...n,...t({...e,...n})}}}));handle=t=>new e({...this._,handles:[...this._.handles,t]});preload=t=>new e({...this._,preloads:[...this._.preloads,e=>t({params:e.params,search:this._.validate(e.search)})]});component=t=>new e({...this._,components:[...this._.components,o(t)]});lazy=e=>{let t=a(async()=>{let t=await e();return`default`in t?t:{default:t}});return this.preload(e).component(t)};suspense=e=>this.component(oe(e));error=e=>this.component(se(e));toString=()=>this._.pattern};export{W as BrowserHistory,ee as HashHistory,ie as Link,N as LocationContext,P as MatchContext,X as MemoryHistory,re as Navigate,ne as Outlet,F as OutletContext,$ as Route,Y as Router,M as RouterContext,te as RouterRoot,ce as middleware,Q as route,V as useHandles,L as useLocation,R as useMatch,B as useNavigate,z as useOutlet,H as useParams,I as useRouter,U as useSearch};
|