@symbo.ls/router 3.14.599 → 3.14.601
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/cjs/index.js +1 -1
- package/dist/esm/index.js +1 -1
- package/index.js +48 -0
- package/package.json +2 -2
package/dist/cjs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var O=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var D=Object.prototype.hasOwnProperty;var H=(e,o)=>{for(var r in o)O(e,r,{get:o[r],enumerable:!0})},J=(e,o,r,s)=>{if(o&&typeof o=="object"||typeof o=="function")for(let t of Q(o))!D.call(e,t)&&t!==r&&O(e,t,{get:()=>o[t],enumerable:!(s=G(o,t))||s.enumerable});return e};var V=e=>J(O({},"__esModule",{value:!0}),e);var ot={};H(ot,{default:()=>et,getActiveRoute:()=>I,lastLevel:()=>_,lastPathname:()=>C,matchRoute:()=>B,parseQuery:()=>F,parseRoutePattern:()=>k,router:()=>K,runGuards:()=>q});module.exports=V(ot);var i=require("@symbo.ls/utils");const j=/^:(.+)/,M=/^\*$/,A=new Map,k=e=>{const o=A.get(e);if(o)return o;const r=e.replace(/^\//,"").split("/"),s=[];let t=!1;for(let l=0;l<r.length;l++){const n=r[l].match(j);n?s.push({index:l,name:n[1]}):M.test(r[l])&&(t=!0)}const a={segments:r,params:s,hasWildcard:t,pattern:e};return A.set(e,a),a},B=(e,o,r=0)=>{const t=e.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");if(!o)return{key:null,content:null,params:{},routePath:a};let l=null,n=-1,f={};for(const d in o){if(d==="/*")continue;const x=k(d),R=X(t,x);R>n&&(n=R,l=d,f=Y(t,x))}return!l&&o["/*"]&&(l="/*"),{key:l,content:l?o[l]:null,params:f,routePath:a}},X=(e,o)=>{const{segments:r,hasWildcard:s}=o;if(!s&&r.length!==e.length&&r.length!==1&&r.length>e.length)return-1;let t=0;const a=Math.min(r.length,e.length);for(let l=0;l<a;l++)if(r[l]===e[l])t+=3;else if(j.test(r[l]))t+=1;else if(M.test(r[l]))t+=.5;else return-1;return t},Y=(e,o)=>{const r={};for(const{index:s,name:t}of o.params)e[s]&&(r[t]=decodeURIComponent(e[s]));return r},F=e=>{if(!e||e==="?")return{};const o={};return new URLSearchParams(e).forEach((s,t)=>{o[t]!==void 0?(Array.isArray(o[t])||(o[t]=[o[t]]),o[t].push(s)):o[t]=s}),o},q=async(e,o)=>{if(!e||!e.length)return!0;for(const r of e){const s=await r(o);if(s===!1)return!1;if(typeof s=="string")return s}return!0},I=(e=0,o)=>{o||(o=typeof i.window<"u"?i.window.location.pathname:"/");const s=o.split("/")[e+1];if(s)return`/${s}`};let C,_=0;const Z=e=>!e||e==="srcdoc"||e==="about:srcdoc"?"/":e,tt={level:_,pushState:!0,initialRender:!1,scrollToTop:!0,scrollToNode:!1,get scrollNode(){return typeof i.document<"u"&&(i.document.scrollingElement||i.document.documentElement)||null},scrollBody:!1,useFragment:!1,updateState:!0,scrollToOffset:0,contentElementKey:"content",scrollToOptions:{behavior:"smooth"},useParamsMatching:!0},K=async(e,o,r={},s={})=>{const t=o||void 0,a=t?.context?.window||i.window,l=t?.context?.document||i.document,n={...tt,...t.context.routerOptions,...s};_=n.lastLevel;const f=t.__ref;(n.contentElementKey!=="content"&&n.contentElementKey!==f.contentElementKey||!f.contentElementKey)&&(f.contentElementKey=n.contentElementKey||"content");const d=f.contentElementKey||n.contentElementKey||"content",x=a.location.origin!=="null"?a.location.origin:"http://localhost";e=Z(e);const R=typeof a.URL=="function"?a.URL:typeof globalThis<"u"&&globalThis.URL||URL,W=new R(x+e),{pathname:p,search:N,hash:T}=W,v=F(N),S=t.node,$=T&&T!==a.location.hash.slice(1),U=p!==C;C=p;const P=f.__routerRendered;let h,w,u,b;if(n.useParamsMatching){const c=B(p,t.routes,n.level);h=c.key,w=c.routePath,u=c.content,b=c.params}else{h=I(n.level,p),w=h;const c=t.routes;u=c?c[h||"/"]||c["/*"]:null,b={}}const E=n.scrollToNode?S:n.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!u){n.onNotFound?n.onNotFound({pathname:p,route:h,element:t}):n.silent||console.warn("[smbls/router] no content matched for path",p,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(n.guards&&n.guards.length){const c={pathname:p,route:h,params:b,query:v,hash:T,element:t,state:r},g=await q(n.guards,c);if(g===!1)return;if(typeof g=="string")return K(g,o,r,{...s,guards:[]})}if(n.pushState)try{a.history.pushState(r,null,p+(N||"")+(T||""))}catch{}if(U||!$){const c={route:h,routePath:w,hash:T,debugging:!1};Object.keys(b).length&&(c.params=b),Object.keys(v).length&&(c.query=v),n.updateState&&t.state.update(c,{preventContentUpdate:!0})}const z=!P||P.content!==u||P.route!==h;if(U||z||n.force===!0){d&&n.removeOldElement&&t[d].remove();const c=t.__ref?.origin?.content,g={};if(c)for(const m in c){const y=c[m];m==="__ref"||m==="props"||m==="node"||m==="parent"||m==="key"||(typeof y=="string"||typeof y=="number"||typeof y=="boolean"||typeof y=="object"&&y!==null&&!y.node&&!y.__ref)&&(g[m]=y)}const L={...g,...typeof u=="object"?u:{extends:u}};n.useFragment&&(L.tag="fragment");try{if(typeof t?.set!="function")return;t.set(L,{contentElementKey:d}),f.__routerRendered={content:u,route:h}}catch(m){console.error("[smbls/router] failed to render route content",p,m)}}if(n.scrollToTop&&E?.scrollTo&&E.scrollTo({...n.scrollToOptions||{},top:0,left:0}),n.scrollToNode&&u[d]?.node?.scrollTo&&u[d].node.scrollTo({...n.scrollToOptions||{},top:0,left:0}),T){const c=l.getElementById(T);if(c&&E?.scrollTo){const g=c.getBoundingClientRect().top+S.scrollTop-(n.scrollToOffset||0);E.scrollTo({...n.scrollToOptions||{},top:g,left:0})}}(0,i.triggerEventOn)("routeChanged",t,n)};var et=K;
|
package/dist/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{document as
|
|
1
|
+
import{document as E,window as O,triggerEventOn as q}from"@symbo.ls/utils";const S=/^:(.+)/,U=/^\*$/,L=new Map,I=o=>{const n=L.get(o);if(n)return n;const r=o.replace(/^\//,"").split("/"),c=[];let t=!1;for(let l=0;l<r.length;l++){const e=r[l].match(S);e?c.push({index:l,name:e[1]}):U.test(r[l])&&(t=!0)}const a={segments:r,params:c,hasWildcard:t,pattern:o};return L.set(o,a),a},W=(o,n,r=0)=>{const t=o.replace(/^\//,"").split("/").filter(Boolean).slice(r),a="/"+(t[0]||"");if(!n)return{key:null,content:null,params:{},routePath:a};let l=null,e=-1,u={};for(const f in n){if(f==="/*")continue;const b=I(f),x=$(t,b);x>e&&(e=x,l=f,u=z(t,b))}return!l&&n["/*"]&&(l="/*"),{key:l,content:l?n[l]:null,params:u,routePath:a}},$=(o,n)=>{const{segments:r,hasWildcard:c}=n;if(!c&&r.length!==o.length&&r.length!==1&&r.length>o.length)return-1;let t=0;const a=Math.min(r.length,o.length);for(let l=0;l<a;l++)if(r[l]===o[l])t+=3;else if(S.test(r[l]))t+=1;else if(U.test(r[l]))t+=.5;else return-1;return t},z=(o,n)=>{const r={};for(const{index:c,name:t}of n.params)o[c]&&(r[t]=decodeURIComponent(o[c]));return r},G=o=>{if(!o||o==="?")return{};const n={};return new URLSearchParams(o).forEach((c,t)=>{n[t]!==void 0?(Array.isArray(n[t])||(n[t]=[n[t]]),n[t].push(c)):n[t]=c}),n},Q=async(o,n)=>{if(!o||!o.length)return!0;for(const r of o){const c=await r(n);if(c===!1)return!1;if(typeof c=="string")return c}return!0},D=(o=0,n)=>{n||(n=typeof O<"u"?O.location.pathname:"/");const c=n.split("/")[o+1];if(c)return`/${c}`};let j,M=0;const H=o=>!o||o==="srcdoc"||o==="about:srcdoc"?"/":o,J={level:M,pushState:!0,initialRender:!1,scrollToTop:!0,scrollToNode:!1,get scrollNode(){return typeof E<"u"&&(E.scrollingElement||E.documentElement)||null},scrollBody:!1,useFragment:!1,updateState:!0,scrollToOffset:0,contentElementKey:"content",scrollToOptions:{behavior:"smooth"},useParamsMatching:!0},A=async(o,n,r={},c={})=>{const t=n||void 0,a=t?.context?.window||O,l=t?.context?.document||E,e={...J,...t.context.routerOptions,...c};M=e.lastLevel;const u=t.__ref;(e.contentElementKey!=="content"&&e.contentElementKey!==u.contentElementKey||!u.contentElementKey)&&(u.contentElementKey=e.contentElementKey||"content");const f=u.contentElementKey||e.contentElementKey||"content",b=a.location.origin!=="null"?a.location.origin:"http://localhost";o=H(o);const x=typeof a.URL=="function"?a.URL:typeof globalThis<"u"&&globalThis.URL||URL,k=new x(b+o),{pathname:d,search:C,hash:y}=k,v=G(C),_=t.node,B=y&&y!==a.location.hash.slice(1),K=d!==j;j=d;const P=u.__routerRendered;let p,w,i,T;if(e.useParamsMatching){const s=W(d,t.routes,e.level);p=s.key,w=s.routePath,i=s.content,T=s.params}else{p=D(e.level,d),w=p;const s=t.routes;i=s?s[p||"/"]||s["/*"]:null,T={}}const R=e.scrollToNode?_:e.scrollNode;if(t.state?.root?.debugging){t.state.root.debugging=!1;return}if(!i){e.onNotFound?e.onNotFound({pathname:d,route:p,element:t}):e.silent||console.warn("[smbls/router] no content matched for path",d,"\u2014 available routes:",Object.keys(t.routes||{}));return}if(e.guards&&e.guards.length){const s={pathname:d,route:p,params:T,query:v,hash:y,element:t,state:r},m=await Q(e.guards,s);if(m===!1)return;if(typeof m=="string")return A(m,n,r,{...c,guards:[]})}if(e.pushState)try{a.history.pushState(r,null,d+(C||"")+(y||""))}catch{}if(K||!B){const s={route:p,routePath:w,hash:y,debugging:!1};Object.keys(T).length&&(s.params=T),Object.keys(v).length&&(s.query=v),e.updateState&&t.state.update(s,{preventContentUpdate:!0})}const F=!P||P.content!==i||P.route!==p;if(K||F||e.force===!0){f&&e.removeOldElement&&t[f].remove();const s=t.__ref?.origin?.content,m={};if(s)for(const h in s){const g=s[h];h==="__ref"||h==="props"||h==="node"||h==="parent"||h==="key"||(typeof g=="string"||typeof g=="number"||typeof g=="boolean"||typeof g=="object"&&g!==null&&!g.node&&!g.__ref)&&(m[h]=g)}const N={...m,...typeof i=="object"?i:{extends:i}};e.useFragment&&(N.tag="fragment");try{if(typeof t?.set!="function")return;t.set(N,{contentElementKey:f}),u.__routerRendered={content:i,route:p}}catch(h){console.error("[smbls/router] failed to render route content",d,h)}}if(e.scrollToTop&&R?.scrollTo&&R.scrollTo({...e.scrollToOptions||{},top:0,left:0}),e.scrollToNode&&i[f]?.node?.scrollTo&&i[f].node.scrollTo({...e.scrollToOptions||{},top:0,left:0}),y){const s=l.getElementById(y);if(s&&R?.scrollTo){const m=s.getBoundingClientRect().top+_.scrollTop-(e.scrollToOffset||0);R.scrollTo({...e.scrollToOptions||{},top:m,left:0})}}q("routeChanged",t,e)};var Y=A;export{Y as default,D as getActiveRoute,M as lastLevel,j as lastPathname,W as matchRoute,G as parseQuery,I as parseRoutePattern,A as router,Q as runGuards};
|
package/index.js
CHANGED
|
@@ -219,9 +219,17 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
219
219
|
const query = parseQuery(search)
|
|
220
220
|
|
|
221
221
|
const rootNode = element.node
|
|
222
|
+
// Captured against `win.location.hash` BEFORE `history.pushState` below
|
|
223
|
+
// mutates it — `hashChanged` only drives the state.update timing gate
|
|
224
|
+
// now (see ROUTER-QUERY-SWAP-1), not the content-swap decision.
|
|
222
225
|
const hashChanged = hash && hash !== win.location.hash.slice(1)
|
|
223
226
|
const pathChanged = pathname !== lastPathname
|
|
224
227
|
lastPathname = pathname
|
|
228
|
+
// APP-PAGE-COLD-LOAD-NOT-FOUND-1: `lastPathname` is MODULE-level, so it
|
|
229
|
+
// answers "was this pathname the last one ANY routed element resolved",
|
|
230
|
+
// never "is the right content on screen for THIS element". Keep the real
|
|
231
|
+
// rendered identity on the element itself — see the swap gate below.
|
|
232
|
+
const lastRendered = ref.__routerRendered
|
|
225
233
|
|
|
226
234
|
// Route matching - support both simple and param-based
|
|
227
235
|
let route, routePath, content, params
|
|
@@ -293,7 +301,42 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
293
301
|
{ preventContentUpdate: true }
|
|
294
302
|
)
|
|
295
303
|
}
|
|
304
|
+
}
|
|
296
305
|
|
|
306
|
+
// ROUTER-QUERY-SWAP-1 (fable.md): `content` is matched purely from
|
|
307
|
+
// `pathname` — matchRoute/getActiveRoute never consult `search` or
|
|
308
|
+
// `hash` — so only an actual pathname change can produce different
|
|
309
|
+
// content. This used to be folded into the `pathChanged || !hashChanged`
|
|
310
|
+
// block above, and `!hashChanged` is true for the vast majority of
|
|
311
|
+
// calls (any nav with no hash involved at all, i.e. every plain
|
|
312
|
+
// query-only `router('/x?a=2', root)` call), so the routed subtree was
|
|
313
|
+
// torn down and rebuilt — losing local state and re-running onCreate —
|
|
314
|
+
// on every query-only navigation even though the matched content never
|
|
315
|
+
// changed. Gate the actual element.set() swap on pathChanged alone so a
|
|
316
|
+
// same-path call (query-only OR a genuine same-path hash jump) updates
|
|
317
|
+
// state/query above but leaves the mounted content element identity
|
|
318
|
+
// untouched.
|
|
319
|
+
//
|
|
320
|
+
// APP-PAGE-COLD-LOAD-NOT-FOUND-1 — "did the pathname change" is the wrong
|
|
321
|
+
// question when the ROUTE TABLE can change under a live page. A cold load
|
|
322
|
+
// of a late-registered route (a workspace app injecting its pages after
|
|
323
|
+
// boot) resolves once against an incomplete table, legitimately matches
|
|
324
|
+
// the `/*` catch-all, and burns that pathname: every later re-resolve of
|
|
325
|
+
// the SAME path — including the app loader's own recovery re-dispatch,
|
|
326
|
+
// measured live at +4.0s with the route present — saw `pathChanged`
|
|
327
|
+
// false and skipped the swap, so "Not found" stayed on screen until a
|
|
328
|
+
// navigation away and back moved `lastPathname` twice.
|
|
329
|
+
//
|
|
330
|
+
// Ask the honest question instead: does the resolved content differ from
|
|
331
|
+
// what this element currently renders? That subsumes the pathname test
|
|
332
|
+
// (different path => different match) and keeps ROUTER-QUERY-SWAP-1
|
|
333
|
+
// intact, because a query-only nav re-matches the SAME route entry and so
|
|
334
|
+
// yields the identical `content` reference. `opts.force` stays available
|
|
335
|
+
// for a caller that knows the content object was mutated in place.
|
|
336
|
+
const contentChanged =
|
|
337
|
+
!lastRendered || lastRendered.content !== content || lastRendered.route !== route
|
|
338
|
+
const shouldSwap = pathChanged || contentChanged || opts.force === true
|
|
339
|
+
if (shouldSwap) {
|
|
297
340
|
if (contentElementKey && opts.removeOldElement) {
|
|
298
341
|
element[contentElementKey].remove()
|
|
299
342
|
}
|
|
@@ -328,6 +371,11 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
328
371
|
// "bail without exploding" contract for dead elements.
|
|
329
372
|
if (typeof element?.set !== 'function') return
|
|
330
373
|
element.set(nextContent, { contentElementKey })
|
|
374
|
+
// Recorded only after the swap actually happened, so a dead element
|
|
375
|
+
// (the early return above) or a throw below never marks content as
|
|
376
|
+
// rendered that never reached the DOM — that would re-arm exactly the
|
|
377
|
+
// stuck state this gate exists to clear.
|
|
378
|
+
ref.__routerRendered = { content, route }
|
|
331
379
|
} catch (err) {
|
|
332
380
|
console.error('[smbls/router] failed to render route content', pathname, err)
|
|
333
381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/router",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.601",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"browser": "./index.js",
|
|
39
39
|
"sideEffects": false,
|
|
40
40
|
"publishConfig": {
|
|
41
|
-
"access": "
|
|
41
|
+
"access": "public"
|
|
42
42
|
}
|
|
43
43
|
}
|