what-router 0.11.1 → 0.11.2

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 +1 @@
1
- import{signal as w,effect as N,computed as b,batch as j,h as s,ErrorBoundary as I}from"what-core";function _(e){let t=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(r,c)=>`*:${c}`).replace(/\[(\w+)\]/g,":$1"),n=[],a=null,o=t.split("/").map(r=>r.startsWith("*:")?(a=r.slice(2),n.push(a),"(.+)"):r==="*"?(a="rest",n.push("rest"),"(.+)"):r.startsWith(":")?(n.push(r.slice(1)),"([^/]+)"):r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("/");return{regex:new RegExp(`^${o}$`),paramNames:n,catchAll:a}}function q(e,t){let a=t.filter(o=>o.path).sort((o,i)=>{let r=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0),c=(i.path.match(/:/g)||[]).length+(i.path.includes("*")?100:0);return r-c});for(let o of a){let{regex:i,paramNames:r}=_(o.path),c=e.match(i);if(c){let u={};return r.forEach((f,l)=>{u[f]=decodeURIComponent(c[l+1])}),{route:o,params:u}}}return null}function S(e){let t={};if(!e)return t;let n=e.startsWith("?")?e.slice(1):e;for(let a of n.split("&")){let[o,i]=a.split("=");if(!o)continue;let r=decodeURIComponent(o),c=i?decodeURIComponent(i):"";r in t?Array.isArray(t[r])?t[r].push(c):t[r]=[t[r],c]:t[r]=c}return t}function A(e){if(typeof e!="string")return!1;let n=e.trim().replace(/[\s\x00-\x1f]/g,"").toLowerCase();return!(n.startsWith("javascript:")||n.startsWith("data:")||n.startsWith("vbscript:"))}var h=w(typeof location<"u"?location.pathname+location.search+location.hash:"/"),U=w({}),T=w({}),g=w(!1),$=w(null),m={get url(){return h()},get path(){return h().split("?")[0].split("#")[0]},get params(){return U()},get query(){return T()},get hash(){let e=h().split("#")[1];return e?"#"+e:""},get isNavigating(){return g()},get error(){return $()}};async function x(e,t={}){let{replace:n=!1,state:a=null,transition:o=!0,_fromPopstate:i=!1}=t;if(!A(e)){typeof console<"u"&&console.warn(`[what-router] Blocked navigation to unsafe URL: ${e}`);return}if(typeof window<"u"&&e.startsWith("#")){let f=h().split("#")[0]+e;history.replaceState(a,"",f),h.set(f);let l=document.querySelector(e);l&&l.scrollIntoView({behavior:"smooth"});return}if(e===h()||g.peek())return;g.set(!0),$.set(null);let r=()=>{i||(typeof window<"u"&&P.set(h(),{x:scrollX,y:scrollY}),n?history.replaceState(a,"",e):history.pushState(a,"",e)),h.set(e),g.set(!1)};if(o&&typeof document<"u"&&document.startViewTransition)try{await document.startViewTransition(r).finished}catch{}else r()}typeof window<"u"&&window.addEventListener("popstate",()=>{P.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=P.get(e);t&&requestAnimationFrame(()=>window.scrollTo(t.x,t.y))})});function V(e,t){let n=[];if(!e.path)return n;let a=e.path.split("/").filter(Boolean),o="";for(let i of a){o+="/"+i;let r=t.find(c=>c.layout&&c.path===o+"/_layout");r&&n.push(r.layout)}return e.layout&&n.push(e.layout),n}var y=[],K=10;function B({routes:e,fallback:t,globalLayout:n}){return()=>{let a=h(),o=a.split("?")[0].split("#")[0],i=a.split("?")[1]?.split("#")[0]||"",r=g(),c=q(o,e);if(c){j(()=>{U.set(c.params),T.set(S(i))});let{route:u,params:f}=c,l=S(i);if(u.middleware&&u.middleware.length>0)for(let d of u.middleware){let v=d({path:o,params:f,query:l,route:u});if(v===!1)return t?s(t,{}):s("div",{class:"what-403"},s("h1",null,"403"),s("p",null,"Access denied"));if(typeof v=="string"){if(y.push(v),y.length>K){let R=y.slice(-5).join(" \u2192 ");return y.length=0,console.error(`[what-router] Redirect loop detected: ${R}`),g.set(!1),s("div",{class:"what-redirect-loop"},s("h1",null,"Redirect Loop"),s("p",null,"Too many redirects. Check your middleware configuration."))}let k=new Set,E=!1;for(let R of y){if(k.has(R)){E=!0;break}k.add(R)}if(E){let R=y.join(" \u2192 ");return y.length=0,console.error(`[what-router] Redirect cycle detected: ${R}`),g.set(!1),s("div",{class:"what-redirect-loop"},s("h1",null,"Redirect Loop"),s("p",null,"Circular redirect detected. Check your middleware configuration."))}return x(v,{replace:!0}),null}}y.length=0;let p;u.loading&&r?p=s(u.loading,{}):p=s(u.component,{params:f,query:l,route:u}),u.error&&(p=s(I,{fallback:u.error},p));let C=V(u,e);for(let d of C.reverse())p=s(d,{params:f,query:l},p);return n&&(p=s(n,{},p)),p}return t?s(t,{}):s("div",{class:"what-404"},s("h1",null,"404"),s("p",null,"Page not found"))}}function G({href:e,class:t,className:n,children:a,replace:o,prefetch:i=!0,activeClass:r="active",exactActiveClass:c="exact-active",transition:u=!0,...f}){let l=A(e)?e:"about:blank";!A(e)&&typeof console<"u"&&console.warn(`[what-router] Link blocked unsafe href: ${e}`);let p=l.split("?")[0].split("#")[0];return s("a",{href:l,class:()=>{let d=m.path,v=p==="/"?d==="/":d===p||d.startsWith(p+"/");return[t||n,v&&r,d===p&&c].filter(Boolean).join(" ")||void 0},onclick:d=>{d.ctrlKey||d.metaKey||d.shiftKey||d.altKey||d.button!==0||(d.preventDefault(),x(l,{replace:o,transition:u}))},onmouseenter:i?()=>W(l):void 0,...f},...Array.isArray(a)?a:[a])}function X(e){return G(e)}function Y(e){return Object.entries(e).map(([t,n])=>typeof n=="function"?{path:t,component:n}:{path:t,...n})}function H(e,t,n={}){let{layout:a,loading:o,error:i}=n;return t.map(r=>({...r,path:e+r.path,layout:r.layout||a,loading:r.loading||o,error:r.error||i}))}function M(e,t,n={}){let{layout:a,middleware:o}=n;return t.map(i=>({...i,_group:e,layout:i.layout||a,middleware:[...i.middleware||[],...o||[]]}))}function J({to:e}){return x(e,{replace:!0}),null}function Z(e,t){return n=>function(o){let i=e(o);return i instanceof Promise?s("div",{class:"what-guard-loading"},"Loading..."):i?s(n,o):typeof t=="string"?(x(t,{replace:!0}),null):s(t,o)}}function ee(e,t={}){let{fallback:n="/login",loading:a=null}=t;return o=>function(r){let c=w("pending"),u=w(null),f=!1;return N(()=>(f=!1,Promise.resolve(e(r)).then(l=>{f||(u.set(l),c.set(l?"allowed":"denied"))}).catch(()=>{f||c.set("denied")}),()=>{f=!0})),()=>{let l=c();return l==="pending"?a?s(a,{}):null:l==="allowed"?s(o,r):typeof n=="string"?(x(n,{replace:!0}),null):s(n,r)}}}var L=new Set;function W(e){if(typeof document>"u"||L.has(e))return;L.add(e);let t=document.createElement("link");t.rel="prefetch",t.href=e,document.head.appendChild(t)}var P=new Map;function te(){typeof window>"u"||(window.addEventListener("beforeunload",()=>{P.set(location.pathname,window.scrollY)}),N(()=>{let e=m.path,t=P.get(e);requestAnimationFrame(()=>{t!==void 0?window.scrollTo(0,t):m.hash?document.querySelector(m.hash)?.scrollIntoView():window.scrollTo(0,0)})}))}function ne(e){return{style:{viewTransitionName:e}}}function re(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function oe(){return{path:b(()=>m.path),params:b(()=>m.params),query:b(()=>m.query),hash:b(()=>m.hash),isNavigating:b(()=>m.isNavigating),navigate:x,prefetch:W}}function ie({children:e}){return e||null}function ae({routes:e,layout:t,fallback:n,error:a}){let o=e.map(i=>({path:i.path,component:i.component,layout:i.layout||void 0,_mode:i.mode||"client"}));return B({routes:o,globalLayout:t,fallback:n||z})}function z(){return s("div",{style:"text-align:center;padding:60px 20px"},s("h1",{style:"font-size:48px;margin-bottom:8px"},"404"),s("p",{style:"color:#64748b"},"Page not found"))}export{ae as FileRouter,G as Link,X as NavLink,ie as Outlet,J as Redirect,B as Router,ee as asyncGuard,_ as compilePath,Y as defineRoutes,te as enableScrollRestoration,Z as guard,A as isSafeUrl,q as matchRoute,x as navigate,H as nestedRoutes,S as parseQuery,W as prefetch,m as route,M as routeGroup,re as setViewTransition,oe as useRoute,ne as viewTransitionName};
1
+ import{signal as x,effect as U,computed as S,batch as I,h as a,ErrorBoundary as V}from"what-core";function L(e){let t=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(r,c)=>`*:${c}`).replace(/\[(\w+)\]/g,":$1"),n=[],s=null,o=t.split("/").map(r=>r.startsWith("*:")?(s=r.slice(2),n.push(s),"(.+)"):r==="*"?(s="rest",n.push("rest"),"(.+)"):r.startsWith(":")?(n.push(r.slice(1)),"([^/]+)"):r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("/");return{regex:new RegExp(`^${o}$`),paramNames:n,catchAll:s}}function A(e,t){let s=t.filter(o=>o.path).sort((o,i)=>{let r=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0),c=(i.path.match(/:/g)||[]).length+(i.path.includes("*")?100:0);return r-c});for(let o of s){let{regex:i,paramNames:r}=L(o.path),c=e.match(i);if(c){let d={};return r.forEach((u,l)=>{d[u]=decodeURIComponent(c[l+1])}),{route:o,params:d}}}return null}function q(e){let t={};if(!e)return t;let n=e.startsWith("?")?e.slice(1):e;for(let s of n.split("&")){let[o,i]=s.split("=");if(!o)continue;let r=decodeURIComponent(o),c=i?decodeURIComponent(i):"";r in t?Array.isArray(t[r])?t[r].push(c):t[r]=[t[r],c]:t[r]=c}return t}function C(e){if(typeof e!="string")return!1;let n=e.trim().replace(/[\s\x00-\x1f]/g,"").toLowerCase();return!(n.startsWith("javascript:")||n.startsWith("data:")||n.startsWith("vbscript:"))}var p=x(typeof location<"u"?location.pathname+location.search+location.hash:"/"),T=x({}),$=x({}),w=x(!1),W=x(null),h={get url(){return p()},get path(){return p().split("?")[0].split("#")[0]},get params(){return T()},get query(){return $()},get hash(){let e=p().split("#")[1];return e?"#"+e:""},get isNavigating(){return w()},get error(){return W()}};async function v(e,t={}){let{replace:n=!1,state:s=null,transition:o=!0,_fromPopstate:i=!1}=t;if(!C(e)){typeof console<"u"&&console.warn(`[what-router] Blocked navigation to unsafe URL: ${e}`);return}if(typeof window<"u"&&e.startsWith("#")){let u=p().split("#")[0]+e;history.replaceState(s,"",u),p.set(u);let l=document.querySelector(e);l&&l.scrollIntoView({behavior:"smooth"});return}if(e===p()||w.peek())return;w.set(!0),W.set(null);let r=()=>{i||(typeof window<"u"&&k.set(p(),{x:scrollX,y:scrollY}),n?history.replaceState(s,"",e):history.pushState(s,"",e)),p.set(e),w.set(!1)};if(o&&typeof document<"u"&&document.startViewTransition)try{await document.startViewTransition(r).finished}catch{}else r()}typeof window<"u"&&window.addEventListener("popstate",()=>{k.set(p(),{x:scrollX,y:scrollY});let e=location.pathname+location.search+location.hash;v(e,{replace:!0,_fromPopstate:!0,transition:!1}).then(()=>{let t=k.get(e);t&&requestAnimationFrame(()=>window.scrollTo(t.x,t.y))})});function K(e,t){let n=[];if(!e.path)return n;let s=e.path.split("/").filter(Boolean),o="";for(let i of s){o+="/"+i;let r=t.find(c=>c.layout&&c.path===o+"/_layout");r&&n.push(r.layout)}return e.layout&&n.push(e.layout),n}var g=[],B=10;function G({routes:e,fallback:t,globalLayout:n}){let s=()=>{let o=p(),i=o.split("?")[0].split("#")[0],r=o.split("?")[1]?.split("#")[0]||"",c=w(),d=A(i,e);if(d){I(()=>{T.set(d.params),$.set(q(r))});let{route:u,params:l}=d,m=q(r);if(u.middleware&&u.middleware.length>0)for(let b of u.middleware){let P=b({path:i,params:l,query:m,route:u});if(P===!1)return t?a(t,{}):a("div",{class:"what-403"},a("h1",null,"403"),a("p",null,"Access denied"));if(typeof P=="string"){if(g.push(P),g.length>B){let R=g.slice(-5).join(" \u2192 ");return g.length=0,console.error(`[what-router] Redirect loop detected: ${R}`),w.set(!1),a("div",{class:"what-redirect-loop"},a("h1",null,"Redirect Loop"),a("p",null,"Too many redirects. Check your middleware configuration."))}let E=new Set,_=!1;for(let R of g){if(E.has(R)){_=!0;break}E.add(R)}if(_){let R=g.join(" \u2192 ");return g.length=0,console.error(`[what-router] Redirect cycle detected: ${R}`),w.set(!1),a("div",{class:"what-redirect-loop"},a("h1",null,"Redirect Loop"),a("p",null,"Circular redirect detected. Check your middleware configuration."))}return v(P,{replace:!0}),null}}g.length=0;let y;u.loading&&c?y=a(u.loading,{}):y=a(u.component,{params:l,query:m,route:u}),u.error&&(y=a(V,{fallback:u.error},y));let f=K(u,e);for(let b of f.reverse())y=a(b,{params:l,query:m},y);return y}return t?a(t,{}):a("div",{class:"what-404"},a("h1",null,"404"),a("p",null,"Page not found"))};return n?a(n,{},s):s}function z({href:e,class:t,className:n,children:s,replace:o,prefetch:i=!0,activeClass:r="active",exactActiveClass:c="exact-active",transition:d=!0,...u}){let l=C(e)?e:"about:blank";!C(e)&&typeof console<"u"&&console.warn(`[what-router] Link blocked unsafe href: ${e}`);let m=l.split("?")[0].split("#")[0];return a("a",{href:l,class:()=>{let f=h.path,b=m==="/"?f==="/":f===m||f.startsWith(m+"/");return[t||n,b&&r,f===m&&c].filter(Boolean).join(" ")||void 0},onclick:f=>{f.ctrlKey||f.metaKey||f.shiftKey||f.altKey||f.button!==0||(f.preventDefault(),v(l,{replace:o,transition:d}))},onmouseenter:i?()=>j(l):void 0,...u},...Array.isArray(s)?s:[s])}function Y(e){return z(e)}function H(e){return Object.entries(e).map(([t,n])=>typeof n=="function"?{path:t,component:n}:{path:t,...n})}function M(e,t,n={}){let{layout:s,loading:o,error:i}=n;return t.map(r=>({...r,path:e+r.path,layout:r.layout||s,loading:r.loading||o,error:r.error||i}))}function J(e,t,n={}){let{layout:s,middleware:o}=n;return t.map(i=>({...i,_group:e,layout:i.layout||s,middleware:[...i.middleware||[],...o||[]]}))}function Z({to:e}){return v(e,{replace:!0}),null}function ee(e,t){return n=>function(o){let i=e(o);return i instanceof Promise?a("div",{class:"what-guard-loading"},"Loading..."):i?a(n,o):typeof t=="string"?(v(t,{replace:!0}),null):a(t,o)}}function te(e,t={}){let{fallback:n="/login",loading:s=null}=t;return o=>function(r){let c=x("pending"),d=x(null),u=!1;return U(()=>(u=!1,Promise.resolve(e(r)).then(l=>{u||(d.set(l),c.set(l?"allowed":"denied"))}).catch(()=>{u||c.set("denied")}),()=>{u=!0})),()=>{let l=c();return l==="pending"?s?a(s,{}):null:l==="allowed"?a(o,r):typeof n=="string"?(v(n,{replace:!0}),null):a(n,r)}}}var N=new Set;function j(e){if(typeof document>"u"||N.has(e))return;N.add(e);let t=document.createElement("link");t.rel="prefetch",t.href=e,document.head.appendChild(t)}var k=new Map;function ne(){typeof window>"u"||(window.addEventListener("beforeunload",()=>{k.set(location.pathname,window.scrollY)}),U(()=>{let e=h.path,t=k.get(e);requestAnimationFrame(()=>{t!==void 0?window.scrollTo(0,t):h.hash?document.querySelector(h.hash)?.scrollIntoView():window.scrollTo(0,0)})}))}function re(e){return{style:{viewTransitionName:e}}}function oe(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function ie(){return{path:S(()=>h.path),params:S(()=>h.params),query:S(()=>h.query),hash:S(()=>h.hash),isNavigating:S(()=>h.isNavigating),navigate:v,prefetch:j}}function se({children:e}){return e||null}function ae({routes:e,layout:t,fallback:n,error:s}){let o=e.map(i=>({path:i.path,component:i.component,layout:i.layout||void 0,_mode:i.mode||"client"}));return G({routes:o,globalLayout:t,fallback:n||D})}function D(){return a("div",{style:"text-align:center;padding:60px 20px"},a("h1",{style:"font-size:48px;margin-bottom:8px"},"404"),a("p",{style:"color:#64748b"},"Page not found"))}export{ae as FileRouter,z as Link,Y as NavLink,se as Outlet,Z as Redirect,G as Router,te as asyncGuard,L as compilePath,H as defineRoutes,ne as enableScrollRestoration,ee as guard,C as isSafeUrl,A as matchRoute,v as navigate,M as nestedRoutes,q as parseQuery,j as prefetch,h as route,J as routeGroup,oe as setViewTransition,ie as useRoute,re as viewTransitionName};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-router",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "What Framework - File-based & programmatic router with View Transitions",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -32,7 +32,7 @@
32
32
  "author": "ZVN DEV (https://zvndev.com)",
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "what-core": "^0.11.1"
35
+ "what-core": "^0.11.2"
36
36
  },
37
37
  "repository": {
38
38
  "type": "git",
package/src/index.js CHANGED
@@ -167,10 +167,11 @@ const MAX_REDIRECTS = 10;
167
167
  // --- Router Component ---
168
168
 
169
169
  export function Router({ routes, fallback, globalLayout }) {
170
- // Return a reactive function child. The Router component runs ONCE,
171
- // but the returned function re-evaluates whenever _url changes,
172
- // and the fine-grained runtime updates the DOM accordingly.
173
- return () => {
170
+ // The Router component runs ONCE. `content` is a reactive function child that
171
+ // re-evaluates whenever _url changes; the fine-grained runtime reconciles only
172
+ // the matched page in place. The globalLayout is rendered ONCE around it (below)
173
+ // so the app shell persists across navigations instead of re-instantiating.
174
+ const content = () => {
174
175
  const currentUrl = _url();
175
176
  const path = currentUrl.split('?')[0].split('#')[0];
176
177
  const search = currentUrl.split('?')[1]?.split('#')[0] || '';
@@ -259,11 +260,6 @@ export function Router({ routes, fallback, globalLayout }) {
259
260
  element = h(Layout, { params, query: queryObj }, element);
260
261
  }
261
262
 
262
- // Global layout wrapper
263
- if (globalLayout) {
264
- element = h(globalLayout, {}, element);
265
- }
266
-
267
263
  return element;
268
264
  }
269
265
 
@@ -274,6 +270,16 @@ export function Router({ routes, fallback, globalLayout }) {
274
270
  h('p', null, 'Page not found')
275
271
  );
276
272
  };
273
+
274
+ // Render the global layout ONCE so it — and everything it mounts (sidebars,
275
+ // toasters, command palettes, global key listeners) — PERSISTS across
276
+ // navigations; the reactive `content` child swaps the matched page in place.
277
+ // A function child is a reactive region (see render.insert), so on navigation
278
+ // only the page subtree reconciles, never the shell. 404/403/redirect screens
279
+ // now render inside the shell too (the natural meaning of a "global" layout).
280
+ // Without a globalLayout, `content` itself is the reactive root — Router still
281
+ // returns a reactive function child (back-compat).
282
+ return globalLayout ? h(globalLayout, {}, content) : content;
277
283
  }
278
284
 
279
285
  // --- Link Component ---