what-router 0.11.8 → 0.12.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.min.js CHANGED
@@ -1,8 +1,8 @@
1
- import{signal as v,effect as I,computed as R,batch as H,h as c,ErrorBoundary as K}from"what-core";function U(e){let t=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(a,u)=>`*:${u}`).replace(/\[(\w+)\]/g,":$1"),r=[],n=null,o=t.split("/").map(a=>a.startsWith("*:")?(n=a.slice(2),r.push(n),"(.+)"):a==="*"?(n="rest",r.push("rest"),"(.+)"):a.startsWith(":")?(r.push(a.slice(1)),"([^/]+)"):a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("/");return{regex:new RegExp(`^${o}$`),paramNames:r,catchAll:n}}var S=new Map,z=1e3;function B(e){let t=S.get(e);return t||(t=U(e),S.size>=z&&S.clear(),S.set(e,t)),t}function F(e,t){let r;try{r=decodeURIComponent(e)}catch{return null}let n=r.split(/[/\\]/);return!t&&n.length>1||n.includes("..")?null:r}function k(e,t){let r=t.filter(n=>n.path);r.sort((n,o)=>{let i=(n.path.match(/:/g)||[]).length+(n.path.includes("*")?100:0),a=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0);return i-a});for(let n of r){let{regex:o,paramNames:i,catchAll:a}=B(n.path),u=e.match(o);if(u){let p={},l=!1;if(i.forEach((s,d)=>{let g=F(u[d+1],s===a);if(g===null){l=!0;return}p[s]=g}),l)continue;return{route:n,params:p}}}return null}function _(e){let t=Object.create(null);if(!e)return t;let r=e.startsWith("?")?e.slice(1):e;for(let n of r.split("&")){let[o,i]=n.split("=");if(!o)continue;let a=decodeURIComponent(o),u=i?decodeURIComponent(i):"";Object.prototype.hasOwnProperty.call(t,a)?Array.isArray(t[a])?t[a].push(u):t[a]=[t[a],u]:t[a]=u}return t}var M=new Set(["http:","https:","mailto:","tel:"]);function A(e){if(typeof e!="string")return!1;let r=e.trim().replace(/[\s\x00-\x1f]/g,"").toLowerCase();if(r.startsWith("javascript:")||r.startsWith("data:")||r.startsWith("vbscript:")||/^[/\\]{2}/.test(r))return!1;let n=r.match(/^([a-z][a-z0-9+.-]*:)/);return n?M.has(n[1]):!r.includes("\\")}var h=v(typeof location<"u"?location.pathname+location.search+location.hash:"/"),$=v({}),G=v({}),y=v(!1),W=v(null),m={get url(){return h()},get path(){return h().split("?")[0].split("#")[0]},get params(){return $()},get query(){return G()},get hash(){let e=h().split("#")[1];return e?"#"+e:""},get isNavigating(){return y()},get error(){return W()}},N=[],T=[];function D(e,t){return e.push(t),()=>{let r=e.indexOf(t);r!==-1&&e.splice(r,1)}}function ae(e){return D(N,e)}function ie(e){return D(T,e)}async function x(e,t={}){let{replace:r=!1,state:n=null,transition:o=!0,_fromPopstate:i=!1,_redirectChain:a=!1}=t;if(a||(w.length=0),!A(e)){typeof console<"u"&&console.warn("[what-router] Blocked navigation to unsafe URL:",e);return}if(typeof window<"u"&&e.startsWith("#")){let d=h().split("#")[0]+e;history.replaceState(n,"",d),h.set(d);let g=document.querySelector(e);g&&g.scrollIntoView({behavior:"smooth"});return}if(e===h()||y.peek())return;y.set(!0);let u=h();if(N.length){let l=!1;try{for(let s of N.slice())if(await s(e,u)===!1){l=!0;break}}catch(s){throw y.set(!1),s}if(l){y.set(!1),i&&typeof history<"u"&&history.pushState(null,"",u);return}}W.set(null);let p=()=>{i||(typeof window<"u"&&b.set(h(),{x:scrollX,y:scrollY}),r?history.replaceState(n,"",e):history.pushState(n,"",e)),h.set(e),y.set(!1)};if(o&&typeof document<"u"&&document.startViewTransition)try{await document.startViewTransition(p).finished}catch{}else p();if(T.length)for(let l of T.slice())l(e,u)}var V=Symbol.for("what.router.redirect"),X=Symbol.for("what.navigation.signal");function se(e,t={}){if(!A(e)){let n=typeof e=="string"?e:Object.prototype.toString.call(e),o=new Error(`[what-router] redirect() refused an unsafe target: ${n}`);throw o.code="ERR_UNSAFE_REDIRECT",o.suggestion='redirect() accepts same-origin paths and http:, https:, mailto: or tel: URLs only. Protocol-relative ("//host"), backslash-smuggled and javascript:/data: targets are open-redirect vectors. Check a user-supplied target against an allowlist first.',o.codeExample=`// Bad - a user-controlled target can leave your origin:
1
+ import{signal as x,effect as I,computed as b,batch as M,h as c,ErrorBoundary as X}from"what-core";function O(e){let t=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(i,u)=>`*:${u}`).replace(/\[(\w+)\]/g,":$1"),n=[],r=null,o=t.split("/").map(i=>i.startsWith("*:")?(r=i.slice(2),n.push(r),"(.+)"):i==="*"?(r="rest",n.push("rest"),"(.+)"):i.startsWith(":")?(n.push(i.slice(1)),"([^/]+)"):i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("/");return{regex:new RegExp(`^${o}$`),paramNames:n,catchAll:r}}var _=new Map,F=1e3;function H(e){let t=_.get(e);return t||(t=O(e),_.size>=F&&_.clear(),_.set(e,t)),t}function K(e,t){let n;try{n=decodeURIComponent(e)}catch{return null}let r=n.split(/[/\\]/);return!t&&r.length>1||r.includes("..")?null:n}function A(e,t){let n=t.filter(r=>r.path);n.sort((r,o)=>{let a=(r.path.match(/:/g)||[]).length+(r.path.includes("*")?100:0),i=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0);return a-i});for(let r of n){let{regex:o,paramNames:a,catchAll:i}=H(r.path),u=e.match(o);if(u){let p={},l=!1;if(a.forEach((s,d)=>{let g=K(u[d+1],s===i);if(g===null){l=!0;return}p[s]=g}),l)continue;return{route:r,params:p}}}return null}function C(e){let t=Object.create(null);if(!e)return t;let n=e.startsWith("?")?e.slice(1):e;for(let r of n.split("&")){let[o,a]=r.split("=");if(!o)continue;let i=decodeURIComponent(o),u=a?decodeURIComponent(a):"";Object.prototype.hasOwnProperty.call(t,i)?Array.isArray(t[i])?t[i].push(u):t[i]=[t[i],u]:t[i]=u}return t}var Q=new Set(["http:","https:","mailto:","tel:"]);function k(e){if(typeof e!="string")return!1;let n=e.trim().replace(/[\s\x00-\x1f]/g,"").toLowerCase();if(n.startsWith("javascript:")||n.startsWith("data:")||n.startsWith("vbscript:")||/^[/\\]{2}/.test(n))return!1;let r=n.match(/^([a-z][a-z0-9+.-]*:)/);return r?Q.has(r[1]):!n.includes("\\")}var h=x(typeof location<"u"?location.pathname+location.search+location.hash:"/"),G=x({}),D=x({}),y=x(!1),R=x(null),V=x(null),m={get url(){return h()},get path(){return h().split("?")[0].split("#")[0]},get params(){return G()},get query(){return D()},get hash(){let e=h().split("#")[1];return e?"#"+e:""},get isNavigating(){return y()},get error(){return V()}},T=[],q=[];function z(e,t){return e.push(t),()=>{let n=e.indexOf(t);n!==-1&&e.splice(n,1)}}function ue(e){return z(T,e)}function le(e){return z(q,e)}async function v(e,t={}){let{replace:n=!1,state:r=null,transition:o=!0,_fromPopstate:a=!1,_redirectChain:i=!1}=t;if(i||(w.length=0),!k(e)){typeof console<"u"&&console.warn("[what-router] Blocked navigation to unsafe URL:",e);return}if(typeof window<"u"&&e.startsWith("#")){let d=h().split("#")[0]+e;history.replaceState(r,"",d),h.set(d);let g=document.querySelector(e);g&&g.scrollIntoView({behavior:"smooth"});return}if(e===h()||y.peek())return;y.set(!0),R.set(e);let u=h();if(T.length){let l=!1;try{for(let s of T.slice())if(await s(e,u)===!1){l=!0;break}}catch(s){throw y.set(!1),R.set(null),s}if(l){y.set(!1),R.set(null),a&&typeof history<"u"&&history.pushState(null,"",u);return}}V.set(null);let p=()=>{a||(typeof window<"u"&&E.set(h(),{x:window.scrollX,y:window.scrollY}),n?history.replaceState(r,"",e):history.pushState(r,"",e)),h.set(e),y.set(!1),R.set(null)};if(o&&typeof document<"u"&&document.startViewTransition)try{await document.startViewTransition(p).finished}catch{}else p();if(q.length)for(let l of q.slice())l(e,u)}var B=Symbol.for("what.router.redirect"),Y=Symbol.for("what.navigation.signal");function fe(e,t={}){if(!k(e)){let r=typeof e=="string"?e:Object.prototype.toString.call(e),o=new Error(`[what-router] redirect() refused an unsafe target: ${r}`);throw o.code="ERR_UNSAFE_REDIRECT",o.suggestion='redirect() accepts same-origin paths and http:, https:, mailto: or tel: URLs only. Protocol-relative ("//host"), backslash-smuggled and javascript:/data: targets are open-redirect vectors. Check a user-supplied target against an allowlist first.',o.codeExample=`// Bad - a user-controlled target can leave your origin:
2
2
  redirect(query.next);
3
3
 
4
4
  // Good - allowlist the target first:
5
- redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let r=new Error(`[what-router] redirect to ${e}`);throw r.name="RouterRedirect",r.code="ERR_REDIRECT_NOT_CAUGHT",r.suggestion="Seeing this signal in your console means nothing caught it. redirect() works from route middleware and from a component body, where the Router catches it. From an event handler, a promise callback or a timer, call navigate(to) instead. A try/catch around the redirect() call also swallows it.",r.codeExample=`// Bad - an event handler runs long after the render the Router caught:
5
+ redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let n=new Error(`[what-router] redirect to ${e}`);throw n.name="RouterRedirect",n.code="ERR_REDIRECT_NOT_CAUGHT",n.suggestion="Seeing this signal in your console means nothing caught it. redirect() works from route middleware and from a component body, where the Router catches it. From an event handler, a promise callback or a timer, call navigate(to) instead. A try/catch around the redirect() call also swallows it.",n.codeExample=`// Bad - an event handler runs long after the render the Router caught:
6
6
  <button onclick={() => redirect('/login')}>Sign in</button>
7
7
 
8
8
  // Good - navigate() from a handler:
@@ -12,4 +12,4 @@ redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let r=new Error(`[what-
12
12
  function Private() {
13
13
  if (!user()) redirect('/login');
14
14
  return <Secret />;
15
- }`,r[V]=!0,r[X]=()=>{q(e,t)},r.to=e,r.options=t,r}typeof window<"u"&&window.addEventListener("popstate",()=>{b.set(h(),{x:scrollX,y:scrollY});let e=location.pathname+location.search+location.hash;x(e,{replace:!0,_fromPopstate:!0,transition:!1}).then(()=>{let t=b.get(e);t&&requestAnimationFrame(()=>window.scrollTo(t.x,t.y))})});function Q(e,t){let r=[];if(!e.path)return r;let n=e.path.split("/").filter(Boolean),o="";for(let i of n){o+="/"+i;let a=t.find(u=>u.layout&&u.path===o+"/_layout");a&&r.push(a.layout)}return e.layout&&r.push(e.layout),r}var w=[],Y=10;function O(e){return c("div",{class:"what-redirect-loop"},c("h1",null,"Redirect Loop"),c("p",null,e))}function q(e,t){if(w.push(e),w.length>Y){let o=w.slice(-5).join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect loop detected: ${o}`),y.set(!1),O("Too many redirects. Check your middleware configuration.")}let r=new Set,n=!1;for(let o of w){if(r.has(o)){n=!0;break}r.add(o)}if(n){let o=w.join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect cycle detected: ${o}`),y.set(!1),O("Circular redirect detected. Check your middleware configuration.")}return x(e,{replace:!0,...t,_redirectChain:!0}),null}function J({routes:e,fallback:t,globalLayout:r}){let n=()=>{let i=h(),a=i.split("?")[0].split("#")[0],u=i.split("?")[1]?.split("#")[0]||"",p=y(),l=k(a,e);if(l){H(()=>{$.set(l.params),G.set(_(u))});let{route:s,params:d}=l,g=_(u);if(s.middleware&&s.middleware.length>0)for(let E of s.middleware){let P=E({path:a,params:d,query:g,route:s});if(P===!1)return t?c(t,{}):c("div",{class:"what-403"},c("h1",null,"403"),c("p",null,"Access denied"));if(typeof P=="string")return q(P)}let f;s.loading&&p?f=c(s.loading,{}):f=c(s.component,{params:d,query:g,route:s}),s.error&&(f=c(K,{fallback:s.error},f));let C=Q(s,e);for(let E of C.reverse())f=c(E,{params:d,query:g},f);return f}return t?c(t,{}):c("div",{class:"what-404"},c("h1",null,"404"),c("p",null,"Page not found"))},o=()=>{let i=null;try{return n()}catch(a){if(!a||!a[V])throw a;i=a}return q(i.to,i.options)};return r?c(r,{},o):o}function Z({href:e,class:t,className:r,children:n,replace:o,prefetch:i=!0,activeClass:a="active",exactActiveClass:u="exact-active",transition:p=!0,...l}){let s=A(e)?e:"about:blank";!A(e)&&typeof console<"u"&&console.warn("[what-router] Link blocked unsafe href:",e);let d=s.split("?")[0].split("#")[0];return c("a",{href:s,class:()=>{let f=m.path,C=d==="/"?f==="/":f===d||f.startsWith(d+"/");return[t||r,C&&a,f===d&&u].filter(Boolean).join(" ")||void 0},onclick:f=>{f.ctrlKey||f.metaKey||f.shiftKey||f.altKey||f.button!==0||(f.preventDefault(),x(s,{replace:o,transition:p}))},onmouseenter:i?()=>L(s):void 0,...l},...Array.isArray(n)?n:[n])}function ce(e){return Z(e)}function ue(e){return Object.entries(e).map(([t,r])=>typeof r=="function"?{path:t,component:r}:{path:t,...r})}function le(e,t,r={}){let{layout:n,loading:o,error:i}=r;return t.map(a=>({...a,path:e+a.path,layout:a.layout||n,loading:a.loading||o,error:a.error||i}))}function fe(e,t,r={}){let{layout:n,middleware:o}=r;return t.map(i=>({...i,_group:e,layout:i.layout||n,middleware:[...i.middleware||[],...o||[]]}))}function de({to:e}){return x(e,{replace:!0}),null}function pe(e,t){return r=>function(o){let i=e(o);return i instanceof Promise?c("div",{class:"what-guard-loading"},"Loading..."):i?c(r,o):typeof t=="string"?(x(t,{replace:!0}),null):c(t,o)}}function he(e,t={}){let{fallback:r="/login",loading:n=null}=t;return o=>function(a){let u=v("pending"),p=v(null),l=!1;return I(()=>(l=!1,Promise.resolve(e(a)).then(s=>{l||(p.set(s),u.set(s?"allowed":"denied"))}).catch(()=>{l||u.set("denied")}),()=>{l=!0})),()=>{let s=u();return s==="pending"?n?c(n,{}):null:s==="allowed"?c(o,a):typeof r=="string"?(x(r,{replace:!0}),null):c(r,a)}}}var j=new Set;function L(e){if(typeof document>"u"||j.has(e))return;j.add(e);let t=document.createElement("link");t.rel="prefetch",t.href=e,document.head.appendChild(t)}var b=new Map;function me(){typeof window>"u"||(window.addEventListener("beforeunload",()=>{b.set(location.pathname,window.scrollY)}),I(()=>{let e=m.path,t=b.get(e);requestAnimationFrame(()=>{t!==void 0?window.scrollTo(0,t):m.hash?document.querySelector(m.hash)?.scrollIntoView():window.scrollTo(0,0)})}))}function ge(e){return{style:{viewTransitionName:e}}}function ye(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function we(){return{path:R(()=>m.path),params:R(()=>m.params),query:R(()=>m.query),hash:R(()=>m.hash),isNavigating:R(()=>m.isNavigating),navigate:x,prefetch:L}}function xe(){return m.params}function ve(){return m.query}function Re(){return x}function be(e){L(e)}function Ee({children:e}){return e||null}function Se({routes:e,layout:t,fallback:r,error:n}){let o=e.map(i=>({path:i.path,component:i.component,layout:i.layout||void 0,_mode:i.mode||"client"}));return J({routes:o,globalLayout:t,fallback:r||ee})}function ee(){return c("div",{style:"text-align:center;padding:60px 20px"},c("h1",{style:"font-size:48px;margin-bottom:8px"},"404"),c("p",{style:"color:#64748b"},"Page not found"))}export{Se as FileRouter,Z as Link,ce as NavLink,Ee as Outlet,de as Redirect,J as Router,ie as afterNavigate,he as asyncGuard,ae as beforeNavigate,U as compilePath,ue as defineRoutes,me as enableScrollRestoration,pe as guard,A as isSafeUrl,k as matchRoute,x as navigate,le as nestedRoutes,_ as parseQuery,L as prefetch,be as prefetchRoute,se as redirect,m as route,fe as routeGroup,ye as setViewTransition,Re as useNavigate,xe as useParams,we as useRoute,ve as useSearch,ge as viewTransitionName};
15
+ }`,n[B]=!0,n[Y]=()=>{L(e,t)},n.to=e,n.options=t,n}typeof window<"u"&&window.addEventListener("popstate",()=>{E.set(h(),{x:window.scrollX,y:window.scrollY});let e=location.pathname+location.search+location.hash;v(e,{replace:!0,_fromPopstate:!0,transition:!1}).then(()=>{let t=E.get(e);t&&requestAnimationFrame(()=>window.scrollTo(t.x,t.y))})});function J(e,t){let n=[];if(!e.path)return n;let r=e.path.split("/").filter(Boolean),o="";for(let a of r){o+="/"+a;let i=t.find(u=>u.layout&&u.path===o+"/_layout");i&&n.push(i.layout)}return e.layout&&n.push(e.layout),n}var w=[],Z=10;function W(e){return c("div",{class:"what-redirect-loop"},c("h1",null,"Redirect Loop"),c("p",null,e))}function L(e,t){if(w.push(e),w.length>Z){let o=w.slice(-5).join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect loop detected: ${o}`),y.set(!1),R.set(null),W("Too many redirects. Check your middleware configuration.")}let n=new Set,r=!1;for(let o of w){if(n.has(o)){r=!0;break}n.add(o)}if(r){let o=w.join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect cycle detected: ${o}`),y.set(!1),R.set(null),W("Circular redirect detected. Check your middleware configuration.")}return v(e,{replace:!0,...t,_redirectChain:!0}),null}function ee(e,t,n){if(!e)return null;let r=e.split("?")[0].split("#")[0],o=A(r,t);return!o||o.route===n?null:o.route.loading||null}function te({routes:e,fallback:t,globalLayout:n}){let r=()=>{let a=h(),i=a.split("?")[0].split("#")[0],u=a.split("?")[1]?.split("#")[0]||"",p=y(),l=A(i,e);if(l){M(()=>{G.set(l.params),D.set(C(u))});let{route:s,params:d}=l,g=C(u);if(s.middleware&&s.middleware.length>0)for(let P of s.middleware){let N=P({path:i,params:d,query:g,route:s});if(N===!1)return t?c(t,{}):c("div",{class:"what-403"},c("h1",null,"403"),c("p",null,"Access denied"));if(typeof N=="string")return L(N)}let f,S=p?ee(R(),e,s):null;S?f=c(S,{}):s.loading&&p?f=c(s.loading,{}):f=c(s.component,{params:d,query:g,route:s}),s.error&&(f=c(X,{fallback:s.error},f));let j=J(s,e);for(let P of j.reverse())f=c(P,{params:d,query:g},f);return f}return t?c(t,{}):c("div",{class:"what-404"},c("h1",null,"404"),c("p",null,"Page not found"))},o=()=>{let a=null;try{return r()}catch(i){if(!i||!i[B])throw i;a=i}return L(a.to,a.options)};return n?c(n,{},o):o}function ne({href:e,class:t,className:n,children:r,replace:o,prefetch:a=!0,activeClass:i="active",exactActiveClass:u="exact-active",transition:p=!0,...l}){let s=k(e)?e:"about:blank";!k(e)&&typeof console<"u"&&console.warn("[what-router] Link blocked unsafe href:",e);let d=s.split("?")[0].split("#")[0];return c("a",{href:s,class:()=>{let f=m.path,S=d==="/"?f==="/":f===d||f.startsWith(d+"/");return[t||n,S&&i,f===d&&u].filter(Boolean).join(" ")||void 0},onclick:f=>{f.ctrlKey||f.metaKey||f.shiftKey||f.altKey||f.button!==0||(f.preventDefault(),v(s,{replace:o,transition:p}))},onmouseenter:a?()=>U(s):void 0,...l},...Array.isArray(r)?r:[r])}function de(e){return ne(e)}function pe(e){return Object.entries(e).map(([t,n])=>typeof n=="function"?{path:t,component:n}:{path:t,...n})}function re(e,t){let n=e.endsWith("/")?e.slice(0,-1):e,r=!t||t==="/"?"/":t.startsWith("/")?t:`/${t}`;return r==="/"?n||"/":n+r}function he(e,t,n={}){let{layout:r,loading:o,error:a}=n;return t.map(i=>({...i,path:re(e,i.path),layout:i.layout||r,loading:i.loading||o,error:i.error||a}))}function me(e,t,n={}){let{layout:r,middleware:o}=n;return t.map(a=>({...a,_group:e,layout:a.layout||r,middleware:[...a.middleware||[],...o||[]]}))}function ge({to:e}){return v(e,{replace:!0}),null}function ye(e,t){return n=>function(o){let a=e(o);return a instanceof Promise?c("div",{class:"what-guard-loading"},"Loading..."):a?c(n,o):typeof t=="string"?(v(t,{replace:!0}),null):c(t,o)}}function we(e,t={}){let{fallback:n="/login",loading:r=null}=t;return o=>function(i){let u=x("pending"),p=x(null),l=!1;return I(()=>(l=!1,Promise.resolve(e(i)).then(s=>{l||(p.set(s),u.set(s?"allowed":"denied"))}).catch(()=>{l||u.set("denied")}),()=>{l=!0})),()=>{let s=u();return s==="pending"?r?c(r,{}):null:s==="allowed"?c(o,i):typeof n=="string"?(v(n,{replace:!0}),null):c(n,i)}}}var $=new Set;function U(e){if(typeof document>"u"||$.has(e))return;$.add(e);let t=document.createElement("link");t.rel="prefetch",t.href=e,document.head.appendChild(t)}var E=new Map;function xe(){typeof window>"u"||(window.addEventListener("beforeunload",()=>{E.set(location.pathname,window.scrollY)}),I(()=>{let e=m.path,t=E.get(e);requestAnimationFrame(()=>{t!==void 0?window.scrollTo(0,t):m.hash?document.querySelector(m.hash)?.scrollIntoView():window.scrollTo(0,0)})}))}function ve(e){return{style:{viewTransitionName:e}}}function Re(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function be(){return{path:b(()=>m.path),params:b(()=>m.params),query:b(()=>m.query),hash:b(()=>m.hash),isNavigating:b(()=>m.isNavigating),navigate:v,prefetch:U}}function Ee(){return m.params}function Se(){return m.query}function _e(){return v}function Ae(e){U(e)}function Ce({children:e}){return e||null}function ke({routes:e,layout:t,fallback:n,error:r}){let o=e.map(a=>({path:a.path,component:a.component,layout:a.layout||void 0,loading:a.loading||void 0,error:a.error||r||void 0,_mode:a.mode||"client"}));return te({routes:o,globalLayout:t,fallback:n||oe})}function oe(){return c("div",{style:"text-align:center;padding:60px 20px"},c("h1",{style:"font-size:48px;margin-bottom:8px"},"404"),c("p",{style:"color:#64748b"},"Page not found"))}export{ke as FileRouter,ne as Link,de as NavLink,Ce as Outlet,ge as Redirect,te as Router,le as afterNavigate,we as asyncGuard,ue as beforeNavigate,O as compilePath,pe as defineRoutes,xe as enableScrollRestoration,ye as guard,k as isSafeUrl,A as matchRoute,v as navigate,he as nestedRoutes,C as parseQuery,U as prefetch,Ae as prefetchRoute,fe as redirect,m as route,me as routeGroup,Re as setViewTransition,_e as useNavigate,Ee as useParams,be as useRoute,Se as useSearch,ve as viewTransitionName};
package/index.d.ts CHANGED
@@ -49,7 +49,7 @@ export interface RouteConfig {
49
49
  /** Loading component */
50
50
  loading?: Component<{}>;
51
51
  /** Error component */
52
- error?: Component<{ error: Error }>;
52
+ error?: Component<{ error: Error; reset: () => void }>;
53
53
  /** Route middleware */
54
54
  middleware?: RouteMiddleware[];
55
55
  }
@@ -91,7 +91,7 @@ export interface FileRouterProps {
91
91
  routes: FileRouteConfig[];
92
92
  layout?: Component<{ children?: VNodeChild }>;
93
93
  fallback?: Component<{}>;
94
- error?: Component<{ error: Error }>;
94
+ error?: Component<{ error: Error; reset: () => void }>;
95
95
  }
96
96
 
97
97
  /** Router driven by what-compiler's generated route manifest (virtual:what-routes). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-router",
3
- "version": "0.11.8",
3
+ "version": "0.12.0",
4
4
  "description": "What Framework - File-based & programmatic router with View Transitions",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -35,7 +35,7 @@
35
35
  "license": "MIT",
36
36
  "dependencies": {},
37
37
  "peerDependencies": {
38
- "what-core": "^0.11.8"
38
+ "what-core": "^0.12.0"
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",
package/src/index.js CHANGED
@@ -45,6 +45,12 @@ const _url = signal(typeof location !== 'undefined' ? location.pathname + locati
45
45
  const _params = signal({});
46
46
  const _query = signal({});
47
47
  const _isNavigating = signal(false);
48
+ // The URL a navigation is heading TO, live for the duration of the navigation.
49
+ // _url only commits at the very end, so during an awaited guard the router still
50
+ // matches the route being LEFT. Without this, a destination route's `loading:`
51
+ // component could never be shown: the only `loading:` in scope belonged to the
52
+ // page the user was leaving.
53
+ const _pendingUrl = signal(null);
48
54
  const _navigationError = signal(null);
49
55
 
50
56
  export const route = {
@@ -127,6 +133,7 @@ export async function navigate(to, opts = {}) {
127
133
  // beforeNavigate hook otherwise leaves a gap two navigations both get through.
128
134
  if (_isNavigating.peek()) return;
129
135
  _isNavigating.set(true);
136
+ _pendingUrl.set(to);
130
137
 
131
138
  const from = _url();
132
139
 
@@ -143,10 +150,12 @@ export async function navigate(to, opts = {}) {
143
150
  } catch (e) {
144
151
  // A throwing guard must not leave the router permanently wedged.
145
152
  _isNavigating.set(false);
153
+ _pendingUrl.set(null);
146
154
  throw e;
147
155
  }
148
156
  if (cancelled) {
149
157
  _isNavigating.set(false);
158
+ _pendingUrl.set(null);
150
159
  if (_fromPopstate && typeof history !== 'undefined') history.pushState(null, '', from);
151
160
  return;
152
161
  }
@@ -159,7 +168,7 @@ export async function navigate(to, opts = {}) {
159
168
  if (!_fromPopstate) {
160
169
  // Save scroll position for current URL before navigating away
161
170
  if (typeof window !== 'undefined') {
162
- scrollPositions.set(_url(), { x: scrollX, y: scrollY });
171
+ scrollPositions.set(_url(), { x: window.scrollX, y: window.scrollY });
163
172
  }
164
173
  if (replace) {
165
174
  history.replaceState(state, '', to);
@@ -169,6 +178,7 @@ export async function navigate(to, opts = {}) {
169
178
  }
170
179
  _url.set(to);
171
180
  _isNavigating.set(false);
181
+ _pendingUrl.set(null);
172
182
  };
173
183
 
174
184
  // Use View Transitions API if available and enabled
@@ -245,7 +255,7 @@ function Private() {
245
255
  if (typeof window !== 'undefined') {
246
256
  window.addEventListener('popstate', () => {
247
257
  // Save scroll position for the URL we're leaving
248
- scrollPositions.set(_url(), { x: scrollX, y: scrollY });
258
+ scrollPositions.set(_url(), { x: window.scrollX, y: window.scrollY });
249
259
 
250
260
  const newUrl = location.pathname + location.search + location.hash;
251
261
  // Use _fromPopstate flag so navigate() skips pushState (browser already updated URL)
@@ -325,6 +335,7 @@ function handleRedirect(target, options) {
325
335
  _redirectHistory.length = 0;
326
336
  console.error(`[what-router] Redirect loop detected: ${cycle}`);
327
337
  _isNavigating.set(false);
338
+ _pendingUrl.set(null);
328
339
  return loopScreen('Too many redirects. Check your middleware configuration.');
329
340
  }
330
341
 
@@ -339,6 +350,7 @@ function handleRedirect(target, options) {
339
350
  _redirectHistory.length = 0;
340
351
  console.error(`[what-router] Redirect cycle detected: ${cycle}`);
341
352
  _isNavigating.set(false);
353
+ _pendingUrl.set(null);
342
354
  return loopScreen('Circular redirect detected. Check your middleware configuration.');
343
355
  }
344
356
 
@@ -348,6 +360,17 @@ function handleRedirect(target, options) {
348
360
 
349
361
  // --- Router Component ---
350
362
 
363
+ // The `loading:` component of the route a navigation is heading to, or null when
364
+ // there is no pending destination, it does not match, it declares no loading
365
+ // component, or it is the route already being rendered.
366
+ function destinationLoading(pendingUrl, routes, currentRoute) {
367
+ if (!pendingUrl) return null;
368
+ const destPath = pendingUrl.split('?')[0].split('#')[0];
369
+ const dest = matchRoute(destPath, routes);
370
+ if (!dest || dest.route === currentRoute) return null;
371
+ return dest.route.loading || null;
372
+ }
373
+
351
374
  export function Router({ routes, fallback, globalLayout }) {
352
375
  // The Router component runs ONCE. `content` is a reactive function child that
353
376
  // re-evaluates whenever _url changes; the fine-grained runtime reconciles only
@@ -389,7 +412,17 @@ export function Router({ routes, fallback, globalLayout }) {
389
412
  // Build element with loading state support
390
413
  let element;
391
414
 
392
- if (r.loading && isNavigating) {
415
+ // While a navigation is in flight, a `loading:` declared by the
416
+ // DESTINATION wins. _url has not committed yet, so `r` is still the route
417
+ // being left: without this, declaring `loading:` on the page you are
418
+ // navigating TO could never show it. Falling back to the departing route's
419
+ // `loading:` preserves the existing behaviour for the case where only the
420
+ // page being left declares one.
421
+ const pendingLoading = isNavigating ? destinationLoading(_pendingUrl(), routes, r) : null;
422
+
423
+ if (pendingLoading) {
424
+ element = h(pendingLoading, {});
425
+ } else if (r.loading && isNavigating) {
393
426
  element = h(r.loading, {});
394
427
  } else {
395
428
  element = h(r.component, {
@@ -521,12 +554,23 @@ export function defineRoutes(config) {
521
554
 
522
555
  // --- Nested Route Helper ---
523
556
 
557
+ // Join a base path with a child path. Naive concatenation turned the index
558
+ // child of `nestedRoutes('/dashboard', [{ path: '/' }, ...])` — the form the
559
+ // README documents — into '/dashboard/', which '/dashboard' does not match, so
560
+ // the documented example 404s on its own index route.
561
+ function joinRoutePath(basePath, childPath) {
562
+ const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
563
+ const child = !childPath || childPath === '/' ? '/' : (childPath.startsWith('/') ? childPath : `/${childPath}`);
564
+ if (child === '/') return base || '/';
565
+ return base + child;
566
+ }
567
+
524
568
  export function nestedRoutes(basePath, children, options = {}) {
525
569
  const { layout, loading, error } = options;
526
570
 
527
571
  return children.map(child => ({
528
572
  ...child,
529
- path: basePath + child.path,
573
+ path: joinRoutePath(basePath, child.path),
530
574
  layout: child.layout || layout,
531
575
  loading: child.loading || loading,
532
576
  error: child.error || error,
@@ -743,11 +787,16 @@ export function FileRouter({
743
787
  fallback,
744
788
  error: globalError,
745
789
  }) {
746
- // Convert file-router route format to Router's expected format
790
+ // Convert file-router route format to Router's expected format.
791
+ // loading/error used to be dropped here, so a route could declare either and
792
+ // never see it rendered, and the `error` prop was destructured and then never
793
+ // read: a documented public prop that silently did nothing.
747
794
  const routerRoutes = routes.map(r => ({
748
795
  path: r.path,
749
796
  component: r.component,
750
797
  layout: r.layout || undefined,
798
+ loading: r.loading || undefined,
799
+ error: r.error || globalError || undefined,
751
800
  // Attach page mode as metadata for build system
752
801
  _mode: r.mode || 'client',
753
802
  }));