what-router 0.12.4 → 0.13.1
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 +2 -2
- package/package.json +2 -2
- package/src/index.js +4 -2
package/dist/index.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{signal as R,effect as $,computed as b,batch as M,h as c,ErrorBoundary as X}from"what-core";function O(e){let n=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(i,
|
|
1
|
+
import{signal as R,effect as $,computed as b,batch as M,h as c,ErrorBoundary as X}from"what-core";function O(e){let n=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(i,u)=>`*:${u}`).replace(/\[(\w+)\]/g,":$1"),t=[],r=null,o=n.split("/").map(i=>i.startsWith("*:")?(r=i.slice(2),t.push(r),"(.+)"):i==="*"?(r="rest",t.push("rest"),"(.+)"):i.startsWith(":")?(t.push(i.slice(1)),"([^/]+)"):i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("/");return{regex:new RegExp(`^${o}$`),paramNames:t,catchAll:r}}var S=new Map,z=1e3;function H(e){let n=S.get(e);return n||(n=O(e),S.size>=z&&S.clear(),S.set(e,n)),n}function K(e,n){let t;try{t=decodeURIComponent(e)}catch{return null}let r=t.split(/[/\\]/);return!n&&r.length>1||r.includes("..")?null:t}function _(e,n){let t=n.filter(r=>r.path);t.sort((r,o)=>{let s=(r.path.match(/:/g)||[]).length+(r.path.includes("*")?100:0),i=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0);return s-i});for(let r of t){let{regex:o,paramNames:s,catchAll:i}=H(r.path),u=e.match(o);if(u){let p={},l=!1;if(s.forEach((a,d)=>{let m=K(u[d+1],a===i);if(m===null){l=!0;return}p[a]=m}),l)continue;return{route:r,params:p}}}return null}function A(e){let n=Object.create(null);if(!e)return n;let t=e.startsWith("?")?e.slice(1):e;for(let r of t.split("&")){let[o,s]=r.split("=");if(!o)continue;let i=decodeURIComponent(o),u=s?decodeURIComponent(s):"";Object.prototype.hasOwnProperty.call(n,i)?Array.isArray(n[i])?n[i].push(u):n[i]=[n[i],u]:n[i]=u}return n}var Q=new Set(["http:","https:","mailto:","tel:"]);function C(e){if(typeof e!="string")return!1;let t=e.trim().replace(/[\s\x00-\x1f]/g,"").toLowerCase();if(t.startsWith("javascript:")||t.startsWith("data:")||t.startsWith("vbscript:")||/^[/\\]{2}/.test(t))return!1;let r=t.match(/^([a-z][a-z0-9+.-]*:)/);return r?Q.has(r[1]):!t.includes("\\")}var h=R(typeof location<"u"?location.pathname+location.search+location.hash:"/"),G=R({}),F=R({}),y=R(!1),x=R(null),B=R(null),g={get url(){return h()},get path(){return h().split("?")[0].split("#")[0]},get params(){return G()},get query(){return F()},get hash(){let e=h().split("#")[1];return e?"#"+e:""},get isNavigating(){return y()},get error(){return B()}},q=[],P=[];function D(e,n){return e.push(n),()=>{let t=e.indexOf(n);t!==-1&&e.splice(t,1)}}function ue(e){return D(q,e)}function le(e){return D(P,e)}async function v(e,n={}){let{replace:t=!1,state:r=null,transition:o=!0,_fromPopstate:s=!1,_redirectChain:i=!1}=n;if(i||(w.length=0),!C(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 m=document.querySelector(e);m&&m.scrollIntoView({behavior:"smooth"});return}if(e===h()||y.peek())return;y.set(!0),x.set(e);let u=h();if(q.length){let l=!1;try{for(let a of q.slice())if(await a(e,u)===!1){l=!0;break}}catch(a){throw y.set(!1),x.set(null),a}if(l){y.set(!1),x.set(null),s&&typeof history<"u"&&history.pushState(null,"",u);return}}B.set(null);let p=()=>{s||(typeof window<"u"&&k.set(h(),{x:window.scrollX,y:window.scrollY}),t?history.replaceState(r,"",e):history.pushState(r,"",e)),h.set(e),y.set(!1),x.set(null)};if(o&&typeof document<"u"&&document.startViewTransition)try{let l=document.startViewTransition(p);l.ready&&typeof l.ready.catch=="function"&&l.ready.catch(()=>{}),await l.finished}catch{}else p();if(P.length)for(let l of P.slice())l(e,u)}var V=Symbol.for("what.router.redirect"),Y=Symbol.for("what.navigation.signal");function fe(e,n={}){if(!C(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:
|
|
@@ -12,4 +12,4 @@ redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let t=new Error(`[what-
|
|
|
12
12
|
function Private() {
|
|
13
13
|
if (!user()) redirect('/login');
|
|
14
14
|
return <Secret />;
|
|
15
|
-
}`,t[V]=!0,t[Y]=()=>{L(e,n)},t.to=e,t.options=n,t}typeof window<"u"&&window.addEventListener("popstate",()=>{k.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 n=k.get(e);n&&requestAnimationFrame(()=>window.scrollTo(n.x,n.y))})});function J(e,n){let t=[];if(!e.path)return t;let r=e.path.split("/").filter(Boolean),o="";for(let
|
|
15
|
+
}`,t[V]=!0,t[Y]=()=>{L(e,n)},t.to=e,t.options=n,t}typeof window<"u"&&window.addEventListener("popstate",()=>{k.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 n=k.get(e);n&&requestAnimationFrame(()=>window.scrollTo(n.x,n.y))})});function J(e,n){let t=[];if(!e.path)return t;let r=e.path.split("/").filter(Boolean),o="";for(let s of r){o+="/"+s;let i=n.find(u=>u.layout&&u.path===o+"/_layout");i&&t.push(i.layout)}return e.layout&&t.push(e.layout),t}var w=[],Z=10;function I(e){return c("div",{class:"what-redirect-loop"},c("h1",null,"Redirect Loop"),c("p",null,e))}function L(e,n){if(x.peek()===e)return null;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),x.set(null),I("Too many redirects. Check your middleware configuration.")}let t=new Set,r=!1;for(let o of w){if(t.has(o)){r=!0;break}t.add(o)}if(r){let o=w.join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect cycle detected: ${o}`),y.set(!1),x.set(null),I("Circular redirect detected. Check your middleware configuration.")}return v(e,{replace:!0,...n,_redirectChain:!0}),null}function ee(e,n,t){if(!e)return null;let r=e.split("?")[0].split("#")[0],o=_(r,n);return!o||o.route===t?null:o.route.loading||null}function te({routes:e,fallback:n,globalLayout:t}){let r=()=>{let s=h(),i=s.split("?")[0].split("#")[0],u=s.split("?")[1]?.split("#")[0]||"",p=y(),l=_(i,e);if(l){M(()=>{G.set(l.params),F.set(A(u))});let{route:a,params:d}=l,m=A(u);if(a.middleware&&a.middleware.length>0)for(let N of a.middleware){let T=N({path:i,params:d,query:m,route:a});if(T===!1)return n?c(n,{}):c("div",{class:"what-403"},c("h1",null,"403"),c("p",null,"Access denied"));if(typeof T=="string")return L(T)}let f,E=p?ee(x(),e,a):null;E?f=c(E,{}):a.loading&&p?f=c(a.loading,{}):f=c(a.component,{params:d,query:m,route:a}),a.error&&(f=c(X,{fallback:a.error},f));let j=J(a,e);for(let N of j.reverse())f=c(N,{params:d,query:m},f);return f}return n?c(n,{}):c("div",{class:"what-404"},c("h1",null,"404"),c("p",null,"Page not found"))},o=()=>{let s=null;try{return r()}catch(i){if(!i||!i[V])throw i;s=i}return L(s.to,s.options)};return t?c(t,{},o):o}function ne({href:e,class:n,className:t,children:r,replace:o,prefetch:s=!0,activeClass:i="active",exactActiveClass:u="exact-active",transition:p=!0,...l}){let a=C(e)?e:"about:blank";!C(e)&&typeof console<"u"&&console.warn("[what-router] Link blocked unsafe href:",e);let d=a.split("?")[0].split("#")[0];return c("a",{href:a,class:()=>{let f=g.path,E=d==="/"?f==="/":f===d||f.startsWith(d+"/");return[n||t,E&&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(a,{replace:o,transition:p}))},onmouseenter:s?()=>U(a):void 0,...l},...Array.isArray(r)?r:[r])}function de(e){return ne(e)}function pe(e){return Object.entries(e).map(([n,t])=>typeof t=="function"?{path:n,component:t}:{path:n,...t})}function re(e,n){let t=e.endsWith("/")?e.slice(0,-1):e,r=!n||n==="/"?"/":n.startsWith("/")?n:`/${n}`;return r==="/"?t||"/":t+r}function he(e,n,t={}){let{layout:r,loading:o,error:s}=t;return n.map(i=>({...i,path:re(e,i.path),layout:i.layout||r,loading:i.loading||o,error:i.error||s}))}function me(e,n,t={}){let{layout:r,middleware:o}=t;return n.map(s=>({...s,_group:e,layout:s.layout||r,middleware:[...s.middleware||[],...o||[]]}))}function ge({to:e}){return v(e,{replace:!0}),null}function ye(e,n){return t=>function(o){let s=e(o);return s instanceof Promise?c("div",{class:"what-guard-loading"},"Loading..."):s?c(t,o):typeof n=="string"?(v(n,{replace:!0}),null):c(n,o)}}function we(e,n={}){let{fallback:t="/login",loading:r=null}=n;return o=>function(i){let u=R("pending"),p=R(null),l=!1;return $(()=>(l=!1,Promise.resolve(e(i)).then(a=>{l||(p.set(a),u.set(a?"allowed":"denied"))}).catch(()=>{l||u.set("denied")}),()=>{l=!0})),()=>{let a=u();return a==="pending"?r?c(r,{}):null:a==="allowed"?c(o,i):typeof t=="string"?(v(t,{replace:!0}),null):c(t,i)}}}var W=new Set;function U(e){if(typeof document>"u"||W.has(e))return;W.add(e);let n=document.createElement("link");n.rel="prefetch",n.href=e,document.head.appendChild(n)}var k=new Map;function xe(){if(typeof window>"u")return;let e=!0;$(()=>{let n=g.url,t=k.get(n),r=g.hash,o=e;e=!1,requestAnimationFrame(()=>{r?oe(r)?.scrollIntoView():t?window.scrollTo(t.x,t.y):o||window.scrollTo(0,0)})})}function oe(e){try{return document.querySelector(e)}catch{return document.getElementById(e.slice(1))}}function Re(e){return{style:{viewTransitionName:e}}}function ve(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function be(){return{path:b(()=>g.path),params:b(()=>g.params),query:b(()=>g.query),hash:b(()=>g.hash),isNavigating:b(()=>g.isNavigating),navigate:v,prefetch:U}}function Ee(){return g.params}function Se(){return g.query}function _e(){return v}function Ae(e){U(e)}function Ce({children:e}){return e||null}function ke({routes:e,layout:n,fallback:t,error:r}){let o=e.map(s=>({path:s.path,component:s.component,layout:s.layout||void 0,loading:s.loading||void 0,error:s.error||r||void 0,_mode:s.mode||"client"}));return te({routes:o,globalLayout:n,fallback:t||ie})}function ie(){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,C as isSafeUrl,_ as matchRoute,v as navigate,he as nestedRoutes,A as parseQuery,U as prefetch,Ae as prefetchRoute,fe as redirect,g as route,me as routeGroup,ve as setViewTransition,_e as useNavigate,Ee as useParams,be as useRoute,Se as useSearch,Re as viewTransitionName};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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.
|
|
38
|
+
"what-core": "^0.13.1"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// route groups, view transitions, and middleware.
|
|
4
4
|
|
|
5
5
|
import { signal, effect, computed, batch, h, ErrorBoundary } from 'what-core';
|
|
6
|
-
import {
|
|
6
|
+
import { matchRoute, parseQuery } from './match.js';
|
|
7
7
|
|
|
8
8
|
// --- URL Sanitization ---
|
|
9
9
|
// Rejects javascript:, data:, vbscript: protocols (case-insensitive, trimmed),
|
|
@@ -196,7 +196,7 @@ export async function navigate(to, opts = {}) {
|
|
|
196
196
|
viewTransition.ready.catch(() => {});
|
|
197
197
|
}
|
|
198
198
|
await viewTransition.finished;
|
|
199
|
-
} catch
|
|
199
|
+
} catch {
|
|
200
200
|
// Transition failed, navigation still happened
|
|
201
201
|
}
|
|
202
202
|
} else {
|
|
@@ -229,6 +229,7 @@ const NAV_SIGNAL = Symbol.for('what.navigation.signal');
|
|
|
229
229
|
export function redirect(to, options = {}) {
|
|
230
230
|
if (!isSafeUrl(to)) {
|
|
231
231
|
const target = typeof to === 'string' ? to : Object.prototype.toString.call(to);
|
|
232
|
+
/** @type {Error & { code?: string, suggestion?: string, codeExample?: string }} */
|
|
232
233
|
const err = new Error(`[what-router] redirect() refused an unsafe target: ${target}`);
|
|
233
234
|
err.code = 'ERR_UNSAFE_REDIRECT';
|
|
234
235
|
err.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.';
|
|
@@ -240,6 +241,7 @@ redirect(ALLOWED.has(query.next) ? query.next : '/');`;
|
|
|
240
241
|
throw err;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/** @type {Error & { code?: string, suggestion?: string, codeExample?: string, to?: any, options?: any }} */
|
|
243
245
|
const sig = new Error(`[what-router] redirect to ${to}`);
|
|
244
246
|
sig.name = 'RouterRedirect';
|
|
245
247
|
sig.code = 'ERR_REDIRECT_NOT_CAUGHT';
|