@real-router/navigation-plugin 0.5.1 → 0.6.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/README.md CHANGED
@@ -48,7 +48,7 @@ router.traverseToLast("users.list"); // jump back to the last users list
48
48
  router.usePlugin(
49
49
  navigationPluginFactory({
50
50
  base: "/app", // Base path prefix for all routes
51
- forceDeactivate: true, // Bypass canDeactivate guards on back/forward
51
+ forceDeactivate: false, // Respect canDeactivate guards on back/forward (default)
52
52
  }),
53
53
  );
54
54
  ```
@@ -56,7 +56,7 @@ router.usePlugin(
56
56
  | Option | Type | Default | Description |
57
57
  | ----------------- | --------- | ------- | ---------------------------------------------------------------------- |
58
58
  | `base` | `string` | `""` | Base path for all routes (e.g., `"/app"` → URLs start with `/app/...`) |
59
- | `forceDeactivate` | `boolean` | `true` | Bypass `canDeactivate` guards on browser back/forward |
59
+ | `forceDeactivate` | `boolean` | `false` | If `true`, browser back/forward skip `canDeactivate` guards. Default `false` respects guards — matches browser-plugin. |
60
60
 
61
61
  ## Router Extensions
62
62
 
@@ -202,6 +202,21 @@ type NavigationDirection = "forward" | "back" | "unknown";
202
202
 
203
203
  Exported from the package for use in type annotations.
204
204
 
205
+ ### Cross-document loads (F5, back/forward across page boundaries)
206
+
207
+ After a cross-document navigation — F5, browser back/forward across the JS context boundary, a fresh URL bar entry — the plugin reads [`navigation.activation.navigationType`](https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation/navigationType) (Baseline 2026: Chrome 123+, Firefox 147+, Safari 26.2+) and primes `state.context.navigation` for the first transition:
208
+
209
+ | `navigation.activation.navigationType` | `state.context.navigation.navigationType` | `direction` |
210
+ | -------------------------------------- | ----------------------------------------- | ----------- |
211
+ | `"reload"` | `"reload"` | `"unknown"` |
212
+ | `"traverse"` | `"traverse"` | `"unknown"` |
213
+ | `"push"` | `"push"` | `"forward"` |
214
+ | `"replace"` | `"replace"` | `"unknown"` |
215
+
216
+ `userInitiated` is always `false` for the primed first transition — the browser does not expose whether F5 came from a key press or `location.reload()`.
217
+
218
+ On browsers without `navigation.activation` (Chrome 102–122, custom mocks), the first transition falls back to `"replace"`.
219
+
205
220
  ### `buildUrl` vs `buildPath`
206
221
 
207
222
  ```typescript
@@ -234,10 +249,10 @@ router.usePlugin(plugin);
234
249
 
235
250
  ## Form Protection
236
251
 
237
- Set `forceDeactivate: false` to respect `canDeactivate` guards on back/forward:
252
+ `canDeactivate` guards run on browser back/forward by default — no extra configuration needed:
238
253
 
239
254
  ```typescript
240
- router.usePlugin(navigationPluginFactory({ forceDeactivate: false }));
255
+ router.usePlugin(navigationPluginFactory());
241
256
 
242
257
  import { getLifecycleApi } from "@real-router/core/api";
243
258
 
@@ -38,6 +38,12 @@ interface NavigationBrowser {
38
38
  addNavigateListener: (fn: (evt: NavigateEvent) => void) => () => void;
39
39
  entries: () => NavigationHistoryEntry[];
40
40
  currentEntry: NavigationHistoryEntry | null;
41
+ /**
42
+ * Type of the cross-document navigation that activated this document.
43
+ * Reads `navigation.activation.navigationType` (Baseline 2026 — Chrome 123+, Firefox 147+, Safari 26.2+).
44
+ * Returns `undefined` when activation is unavailable (older browsers, SSR).
45
+ */
46
+ getActivationType: () => NavigationMeta["navigationType"] | undefined;
41
47
  }
42
48
  type NavigationDirection = "forward" | "back" | "unknown";
43
49
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;AAAA;AAAA,KAWJ,mBAAA;AAMZ;;;;AAAA,UAAiB,cAAA;EAIf;EAFA,cAAA;EAMA;EAJA,aAAA;EAMA;EAJA,IAAA;EAIsB;EAFtB,SAAA,EAAW,mBAAA;;EAEX,aAAA,EAAe,OAAA;AAAA;;;iBC/CD,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCLS,YAAA;IACR,UAAA,GAJa,cAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IAClC,QAAA,GAAW,GAAA,aAAgB,KAAA;IAC3B,mBAAA,GAAsB,IAAA,UAAc,MAAA,GAAS,MAAA;IAC7C,QAAA,QAAgB,KAAA;IAChB,WAAA,QAAmB,KAAA;IACnB,UAAA,GAAa,SAAA;IACb,gBAAA;IACA,kBAAA,GAAqB,SAAA;IACrB,cAAA,GAAiB,SAAA,aAAsB,OAAA,CAAQ,KAAA;IAC/C,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;EADC;;;;;EAOf,iBAAA,QAAyB,cAAA;AAAA;AAAA,KAWf,mBAAA;;;;;UAMK,cAAA;EAQJ;EANX,cAAA;EAQe;EANf,aAAA;EAMsB;EAJtB,IAAA;;EAEA,SAAA,EAAW,mBAAA;ECnDG;EDqDd,aAAA,EAAe,OAAA;AAAA;;;iBCrDD,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCLS,YAAA;IACR,UAAA,GAJa,cAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IAClC,QAAA,GAAW,GAAA,aAAgB,KAAA;IAC3B,mBAAA,GAAsB,IAAA,UAAc,MAAA,GAAS,MAAA;IAC7C,QAAA,QAAgB,KAAA;IAChB,WAAA,QAAmB,KAAA;IACnB,UAAA,GAAa,SAAA;IACb,gBAAA;IACA,kBAAA,GAAqB,SAAA;IACrB,cAAA,GAAiB,SAAA,aAAsB,OAAA,CAAQ,KAAA;IAC/C,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
package/dist/cjs/index.js CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`),t=require(`@real-router/core`);const n=()=>globalThis.window!==void 0&&!!globalThis.history;function r(e){if(!e)return e;let t=e.replaceAll(/\/+/g,`/`);return t.startsWith(`/`)||(t=`/${t}`),t.length>1&&t.endsWith(`/`)&&(t=t.slice(0,-1)),t===`/`?``:t}const i=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},a=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function o(e,t,n){return r=>{if(r)for(let i of Object.keys(r)){if(!(i in e))continue;let a=r[i];if(a===void 0)continue;let o=typeof e[i],s=typeof a;if(s!==o)throw Error(`[${t}] Invalid type for '${i}': expected ${o}, got ${s}`);let c=n?.[i];if(c){let e=c.validate(a);if(e!==null)throw Error(`[${t}] Invalid '${i}': ${e}`)}}}}const s=/[\u0000-\u001F\u007F]/,c={validate:e=>s.test(e)?`must not contain control characters`:e.split(`/`).includes(`..`)?`must not contain '..' segments`:null};function l(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}function u(e){let t=e,n=t.indexOf(`://`);if(n!==-1){let e=n+3,r=t.length;for(let n=e;n<t.length;n++){let e=t[n];if(e===`/`||e===`?`||e===`#`){r=n;break}}t=r===t.length?`/`:t.slice(r),(t.startsWith(`?`)||t.startsWith(`#`))&&(t=`/${t}`)}let r=t.indexOf(`#`),i=r===-1?``:t.slice(r),a=r===-1?t:t.slice(0,r),o=a.indexOf(`?`),s=o===-1?``:a.slice(o);return{pathname:o===-1?a:a.slice(0,o),search:s,hash:i}}function d(e,t){if(!e)return`/`;if(t&&(e===t||e.startsWith(`${t}/`))){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e.startsWith(`/`)?e:`/${e}`}function f(e,t){return e?t?e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function p(e,t){let n=u(e);return d(n.pathname,t)+n.search}function m(e,t){return p(e,t)}const h={forceDeactivate:!0,base:``};function g(e){let t=globalThis.navigation;return{getLocation:()=>i(d(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,n)},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry}}}function _(e,t,n){if(e?.url)return t.matchPath(m(e.url,n))??void 0}function v(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return _(e.entries()[i+r],t,n)}function y(e,t,n){return v(e,t,n,-1)}function b(e,t,n){return v(e,t,n,1)}function x(e,t,n,r){return e.entries().some(e=>_(e,t,n)?.name===r)}function S(e,t,n){let r=new Set;for(let i of e.entries()){let e=_(i,t,n);e&&r.add(e.name)}return[...r]}function C(e,t,n,r){let i=0;for(let a of e.entries())_(a,t,n)?.name===r&&i++;return i}function w(e,t,n,r,i){for(let a=e.length-1;a>=0;a--){let o=e[a];if(o.key!==i&&_(o,n,r)?.name===t)return o}}function T(e){let t=e.currentEntry?.index;return t!=null&&t>0}function E(e){let t=e.currentEntry?.index;return t==null?!1:t<e.entries().length-1}function D(e,t,n,r){let i=e.currentEntry?.index;if(i==null)return!1;let a=e.entries();for(let e=i-1;e>=0;e--)if(_(a[e],t,n)?.name===r)return!0;return!1}function O(e,t,n){return e===`traverse`?t===n?`unknown`:t>n?`forward`:`back`:e===`push`?`forward`:`unknown`}function k(e){let{router:n,api:r,browser:i,isSyncingFromRouter:a,setSyncing:o,base:s,transitionOptions:c}=e,{allowNotFound:l}=r.getOptions();return function(f){if(!f.canIntercept||!n.isActive())return;if(a()){f.intercept({handler:async()=>{}});return}let p=u(f.destination.url),m=d(p.pathname,s)+p.search,h=r.matchPath(m),g=f.navigationType,_=i.currentEntry?.index??-1;e.setCapturedMeta({navigationType:g,userInitiated:f.userInitiated,info:f.info,direction:O(g,f.destination.index,_),sourceElement:f.sourceElement??null});let v=async e=>{try{await e()}catch(e){e instanceof t.RouterError||A(e,n,i,o)}};h?f.intercept({handler:()=>v(()=>n.navigate(h.name,h.params,{...c,signal:f.signal}))}):l?f.intercept({handler:()=>{n.navigateToNotFound(m)}}):f.intercept({handler:async()=>{let e=new t.RouterError(t.errorCodes.ROUTE_NOT_FOUND,{path:m});throw r.emitTransitionError(e),e}})}}function A(e,t,n,r){console.error(`[navigation-plugin] Critical error in navigate handler`,e);try{let e=t.getState();if(e){let i=t.buildUrl(e.name,e.params);r(!0);try{n.navigate(i,{state:{name:e.name,params:e.params,path:e.path},history:`replace`})}finally{r(!1)}}}catch(e){console.error(`[navigation-plugin] Failed to recover from critical error`,e)}}function j(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function M(e,t,n,r,i){return(a,o={})=>{let s=e.buildState(a,o);if(!s)throw Error(`[real-router] Cannot replace state: route "${a}" is not found`);let c=e.makeState(s.name,s.params,t.buildPath(s.name,s.params),{params:s.meta}),l=r(a,o)+n.getHash(),u={name:c.name,params:c.params,path:c.path};i(!0);try{n.replaceState(u,l)}finally{i(!1)}}}function N(e,t,n){return e.reload&&t.path===n?.path?`reload`:l(e,t,n)?`replace`:`push`}var P=class{#e;#t;#n;#r;#i;#a;#o;#s;#c=!1;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#o=t.claimContextNamespace(`navigation`),this.#i=j(t,r);let o=(t,r)=>f(e.buildPath(t,r),n.base);this.#a=t.extendRouter({buildUrl:o,matchUrl:e=>t.matchPath(p(e,n.base))??void 0,replaceHistoryState:M(t,e,r,o,e=>{this.#c=e}),peekBack:()=>y(r,t,n.base),peekForward:()=>b(r,t,n.base),hasVisited:e=>x(r,t,n.base,e),getVisitedRoutes:()=>S(r,t,n.base),getRouteVisitCount:e=>C(r,t,n.base,e),traverseToLast:e=>this.traverseToLast(e),canGoBack:()=>T(r),canGoForward:()=>E(r),canGoBackTo:e=>D(r,t,n.base,e)}),this.#s=F({browser:r,shared:a,handler:k({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#c,setSyncing:e=>{this.#c=e},setCapturedMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a,releaseClaim:()=>{this.#o.release()}})}async traverseToLast(e){let t=this.#r.entries(),n=this.#r.currentEntry?.key,r=w(t,e,this.#t,this.#n.base,n);if(!r)throw Error(`No history entry for route "${e}"`);if(!r.url)throw Error(`No matching route for entry URL "${r.url}"`);let i=m(r.url,this.#n.base),a=this.#t.matchPath(i);if(!a)throw Error(`No matching route for entry URL "${r.url}"`);let o=this.#r.currentEntry?.index??-1;return this.#l={navigationType:`traverse`,userInitiated:!1,direction:r.index>o?`forward`:`back`,sourceElement:null},this.#u=r.key,this.#e.navigate(a.name,a.params)}getPlugin(){return{...this.#s,onTransitionStart:e=>{this.#l&&this.#o.write(e,this.#l)},onTransitionSuccess:(e,n,r)=>{if(!this.#l){let t=N(r,e,n);this.#l={navigationType:t,userInitiated:!1,direction:t===`push`?`forward`:`unknown`,sourceElement:null}}let{navigationType:i}=this.#l;this.#o.write(e,Object.freeze(this.#l)),this.#l=void 0,this.#c=!0;try{if(this.#u)this.#r.traverseTo(this.#u),this.#u=void 0;else{let r=f(e.path,this.#n.base),a=!n||n.path===e.path?this.#r.getHash():``,o=a?r+a:r,s={name:e.name,params:e.params,path:e.path};if(e.name===t.UNKNOWN_ROUTE)this.#r.updateCurrentEntry({state:s});else{let e=i!==`push`;this.#r.navigate(o,{state:s,history:e?`replace`:`push`})}}}finally{this.#c=!1}},onTransitionCancel:()=>{this.#l=void 0,this.#u=void 0},onTransitionError:()=>{this.#l=void 0,this.#u=void 0}}}};function F(e){return{onStart(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=e.browser.addNavigateListener(e.handler)},onStop(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0},teardown(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0,e.removeStartInterceptor(),e.removeExtensions(),e.releaseClaim()}}}const I=()=>{},L=e=>{let t=a(e);return{getLocation:()=>(t(`getLocation`),`/`),getHash:()=>(t(`getHash`),``),navigate:()=>{t(`navigate`)},replaceState:()=>{t(`replaceState`)},updateCurrentEntry:()=>{t(`updateCurrentEntry`)},traverseTo:()=>{t(`traverseTo`)},addNavigateListener:()=>(t(`addNavigateListener`),I),entries:()=>(t(`entries`),[]),currentEntry:null}},R=o(h,`navigation-plugin`,{base:c});function z(t,i){if(!i&&n()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);R(t);let a={...h,...t};a.base=r(a.base);let o=i??B(a.base),s={forceDeactivate:a.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new P(t,(0,e.getPluginApi)(t),a,o,s,c).getPlugin()}function B(e){return`navigation`in globalThis?g(e):L(`navigation-plugin`)}exports.navigationPluginFactory=z;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`),t=require(`@real-router/core`);const n=()=>globalThis.window!==void 0&&!!globalThis.history;function r(e){if(!e)return e;let t=e.replaceAll(/\/+/g,`/`);return t.startsWith(`/`)||(t=`/${t}`),t.length>1&&t.endsWith(`/`)&&(t=t.slice(0,-1)),t===`/`?``:t}const i=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},a=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function o(e,t,n){return r=>{if(r)for(let i of Object.keys(r)){if(!(i in e))continue;let a=r[i];if(a===void 0)continue;let o=typeof e[i],s=typeof a;if(s!==o)throw Error(`[${t}] Invalid type for '${i}': expected ${o}, got ${s}`);let c=n?.[i];if(c){let e=c.validate(a);if(e!==null)throw Error(`[${t}] Invalid '${i}': ${e}`)}}}}const s=/[\u0000-\u001F\u007F]/,c={validate:e=>s.test(e)?`must not contain control characters`:e.split(`/`).includes(`..`)?`must not contain '..' segments`:null};function l(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}function u(e){let t=e,n=t.indexOf(`://`);if(n!==-1){let e=n+3,r=t.length;for(let n=e;n<t.length;n++){let e=t[n];if(e===`/`||e===`?`||e===`#`){r=n;break}}t=r===t.length?`/`:t.slice(r),(t.startsWith(`?`)||t.startsWith(`#`))&&(t=`/${t}`)}let r=t.indexOf(`#`),i=r===-1?``:t.slice(r),a=r===-1?t:t.slice(0,r),o=a.indexOf(`?`),s=o===-1?``:a.slice(o);return{pathname:o===-1?a:a.slice(0,o),search:s,hash:i}}function d(e,t){if(!e)return`/`;if(t&&(e===t||e.startsWith(`${t}/`))){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e.startsWith(`/`)?e:`/${e}`}function f(e,t){return e?t?e===`/`?t:e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function p(e,t){let n=u(e);return d(n.pathname,t)+n.search}function m(e,t){return p(e,t)}const h={forceDeactivate:!1,base:``};function g(e){let t=globalThis.navigation;return{getLocation:()=>i(d(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,n)},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry},getActivationType:()=>t.activation?.navigationType}}function _(e,t,n,r){if(!e)throw Error(`No history entry for route "${t}"`);if(!e.url)throw Error(`No matching route for entry URL "${e.url}"`);let i=m(e.url,r),a=n.matchPath(i);if(!a)throw Error(`No matching route for entry URL "${e.url}"`);return{entry:e,matchedState:a}}function v(e,t,n){if(e?.url)return t.matchPath(m(e.url,n))??void 0}function y(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return v(e.entries()[i+r],t,n)}function b(e,t,n){return y(e,t,n,-1)}function x(e,t,n){return y(e,t,n,1)}function S(e,t,n,r){return e.entries().some(e=>v(e,t,n)?.name===r)}function C(e,t,n){let r=new Set;for(let i of e.entries()){let e=v(i,t,n);e&&r.add(e.name)}return[...r]}function w(e,t,n,r){let i=0;for(let a of e.entries())v(a,t,n)?.name===r&&i++;return i}function T(e,t,n,r,i){for(let a=e.length-1;a>=0;a--){let o=e[a];if(o.key!==i&&v(o,n,r)?.name===t)return o}}function E(e){let t=e.currentEntry?.index;return t!=null&&t>0}function D(e){let t=e.currentEntry?.index;return t==null?!1:t<e.entries().length-1}function O(e,t,n,r){let i=e.currentEntry?.index;if(i==null)return!1;let a=e.entries();for(let e=i-1;e>=0;e--)if(v(a[e],t,n)?.name===r)return!0;return!1}function k(e,t,n){return e===`traverse`?t===n?`unknown`:t>n?`forward`:`back`:e===`push`?`forward`:`unknown`}function A(e){let{router:n,api:r,browser:i,isSyncingFromRouter:a,setSyncing:o,base:s,transitionOptions:c}=e,{allowNotFound:l}=r.getOptions();return function(u){if(!u.canIntercept||!n.isActive())return;if(a()){u.intercept({handler:async()=>{}});return}let d=p(u.destination.url,s),f=r.matchPath(d),m=u.navigationType,h=i.currentEntry?.index??-1;e.setCapturedMeta({navigationType:m,userInitiated:u.userInitiated,info:u.info,direction:k(m,u.destination.index,h),sourceElement:u.sourceElement??null});let g=async e=>{try{await e()}catch(e){if(!(e instanceof t.RouterError)){j(e,n,i,o);return}if(e.code===t.errorCodes.TRANSITION_CANCELLED||e.code===t.errorCodes.SAME_STATES)return;M(n,i,o)}};f?u.intercept({handler:()=>g(()=>n.navigate(f.name,f.params,{...c,signal:u.signal}))}):l?u.intercept({handler:()=>{n.navigateToNotFound(d)}}):u.intercept({handler:async()=>{let e=new t.RouterError(t.errorCodes.ROUTE_NOT_FOUND,{path:d});throw r.emitTransitionError(e),e}})}}function j(e,t,n,r){console.error(`[navigation-plugin] Critical error in navigate handler`,e),M(t,n,r)}function M(e,t,n){try{let r=e.getState();if(r){let i=e.buildUrl(r.name,r.params);n(!0);try{t.navigate(i,{state:{name:r.name,params:r.params,path:r.path},history:`replace`})}finally{n(!1)}}}catch(e){console.error(`[navigation-plugin] Failed to sync URL to router state`,e)}}function N(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function P(e,t,n,r,i){return(a,o={})=>{let s=e.buildState(a,o);if(!s)throw Error(`[real-router] Cannot replace state: route "${a}" is not found`);let c=e.makeState(s.name,s.params,t.buildPath(s.name,s.params),{params:s.meta}),l=r(a,o)+n.getHash(),u={name:c.name,params:c.params,path:c.path};i(!0);try{n.replaceState(u,l)}finally{i(!1)}}}function F(e,t,n){return e.reload&&t.path===n?.path?`reload`:l(e,t,n)?`replace`:`push`}var I=class{#e;#t;#n;#r;#i;#a;#o;#s;#c=!1;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#o=t.claimContextNamespace(`navigation`),this.#i=N(t,r);let o=r.getActivationType();o&&(this.#l={navigationType:o,userInitiated:!1,direction:o===`push`?`forward`:`unknown`,sourceElement:null});let s=(t,r)=>f(e.buildPath(t,r),n.base);this.#a=t.extendRouter({buildUrl:s,matchUrl:e=>t.matchPath(p(e,n.base))??void 0,replaceHistoryState:P(t,e,r,s,e=>{this.#c=e}),peekBack:()=>b(r,t,n.base),peekForward:()=>x(r,t,n.base),hasVisited:e=>S(r,t,n.base,e),getVisitedRoutes:()=>C(r,t,n.base),getRouteVisitCount:e=>w(r,t,n.base,e),traverseToLast:e=>this.traverseToLast(e),canGoBack:()=>E(r),canGoForward:()=>D(r),canGoBackTo:e=>O(r,t,n.base,e)}),this.#s=L({browser:r,shared:a,handler:A({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#c,setSyncing:e=>{this.#c=e},setCapturedMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a,releaseClaim:()=>{this.#o.release()}})}async traverseToLast(e){let t=this.#r.entries(),n=this.#r.currentEntry?.key,{entry:r,matchedState:i}=_(T(t,e,this.#t,this.#n.base,n),e,this.#t,this.#n.base),a=this.#r.currentEntry;if(!a)throw Error(`[navigation-plugin] Cannot determine direction for traverseToLast("${e}"): browser.currentEntry is null. The plugin must be started before calling traverseToLast.`);return this.#l={navigationType:`traverse`,userInitiated:!1,direction:r.index>a.index?`forward`:`back`,sourceElement:null},this.#u=r.key,this.#e.navigate(i.name,i.params)}getPlugin(){return{...this.#s,onTransitionStart:e=>{this.#l&&this.#o.write(e,this.#l)},onTransitionSuccess:(e,n,r)=>{if(!this.#l){let t=F(r,e,n);this.#l={navigationType:t,userInitiated:!1,direction:t===`push`?`forward`:`unknown`,sourceElement:null}}let i=Object.freeze(this.#l);this.#o.write(e,i),this.#l=void 0,this.#c=!0;let a=this.#u;this.#u=void 0;try{if(a)this.#r.traverseTo(a);else{let r=f(e.path,this.#n.base),a=!n||n.path===e.path?this.#r.getHash():``,o=a?r+a:r,s={name:e.name,params:e.params,path:e.path};if(e.name===t.UNKNOWN_ROUTE)this.#r.updateCurrentEntry({state:s});else{let e=i.navigationType!==`push`;this.#r.navigate(o,{state:s,history:e?`replace`:`push`})}}}finally{this.#c=!1}},onTransitionCancel:()=>{this.#l=void 0,this.#u=void 0},onTransitionError:()=>{this.#l=void 0,this.#u=void 0}}}};function L(e){return{onStart(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=e.browser.addNavigateListener(e.handler)},onStop(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0},teardown(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0,e.removeStartInterceptor(),e.removeExtensions(),e.releaseClaim()}}}const R=()=>{},z=e=>{let t=a(e);return{getLocation:()=>(t(`getLocation`),`/`),getHash:()=>(t(`getHash`),``),navigate:()=>{t(`navigate`)},replaceState:()=>{t(`replaceState`)},updateCurrentEntry:()=>{t(`updateCurrentEntry`)},traverseTo:()=>{t(`traverseTo`)},addNavigateListener:()=>(t(`addNavigateListener`),R),entries:()=>(t(`entries`),[]),currentEntry:null,getActivationType:()=>void 0}},B=o(h,`navigation-plugin`,{base:c});function V(t,i){if(!i&&n()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);B(t);let a={...h,...t};a.base=r(a.base);let o=i??H(a.base),s={forceDeactivate:a.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new I(t,(0,e.getPluginApi)(t),a,o,s,c).getPlugin()}function H(e){return`navigation`in globalThis?g(e):z(`navigation-plugin`)}exports.navigationPluginFactory=V;
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["RouterError","errorCodes","#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle","#isSyncingFromRouter","#capturedMeta","#pendingTraverseKey","UNKNOWN_ROUTE"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path to canonical form: leading slash, no trailing slash,\n * no repeated slashes. Isolated \"/\" collapses to \"\".\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"//app//\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n * normalizeBase(\"/\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base.replaceAll(/\\/+/g, \"/\");\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.length > 1 && result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result === \"/\" ? \"\" : result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export interface OptionRule<T> {\n validate: (value: T) => string | null;\n}\n\nexport type OptionRules<T extends object> = {\n [K in keyof T]?: OptionRule<NonNullable<T[K]>>;\n};\n\nexport function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\n rules?: OptionRules<T>,\n): (opts: Partial<T> | undefined) => void {\n return (opts) => {\n if (!opts) {\n return;\n }\n\n for (const key of Object.keys(opts)) {\n if (!(key in defaults)) {\n continue;\n }\n\n const value = opts[key as keyof typeof opts];\n\n if (value === undefined) {\n continue;\n }\n\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n\n const rule = rules?.[key as keyof T];\n\n if (rule) {\n const msg = (rule.validate as (input: unknown) => string | null)(value);\n\n if (msg !== null) {\n throw new Error(`[${loggerContext}] Invalid '${key}': ${msg}`);\n }\n }\n }\n };\n}\n\n// eslint-disable-next-line no-control-regex -- control characters are exactly what this rule rejects\nconst CONTROL_CHARS = /[\\u0000-\\u001F\\u007F]/;\n\nexport const safeBaseRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.split(\"/\").includes(\"..\")) {\n return \"must not contain '..' segments\";\n }\n\n return null;\n },\n};\n\nexport const safeHashPrefixRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.includes(\"/\")) {\n return \"must not contain '/' (slash is added before the path automatically)\";\n }\n\n if (value.includes(\"#\")) {\n return \"must not contain '#' (it is added as the hash delimiter)\";\n }\n\n if (value.includes(\"?\")) {\n return \"must not contain '?' (it conflicts with the query delimiter)\";\n }\n\n return null;\n },\n};\n\nexport const nonNegativeIntegerRule: OptionRule<number> = {\n validate: (value) => {\n if (!Number.isFinite(value)) {\n return `expected finite number, got ${String(value)}`;\n }\n\n if (!Number.isInteger(value)) {\n return `expected integer, got ${String(value)}`;\n }\n\n if (value < 0) {\n return `expected non-negative integer, got ${value}`;\n }\n\n return null;\n },\n};\n","import { createUpdateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n const updateState = createUpdateBrowserState();\n\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n if (navOptions.replace === true) {\n return true;\n }\n\n if (!fromState) {\n return navOptions.replace !== false;\n }\n\n return !!navOptions.reload && toState.path === fromState.path;\n}\n","export interface ParsedUrl {\n pathname: string;\n search: string;\n hash: string;\n}\n\n/**\n * Scheme-agnostic URL parser.\n *\n * Extracts `pathname`, `search`, and `hash` from any string — absolute\n * (`scheme://authority/path?q#h`), path-relative (`/path?q#h`), or opaque\n * (`data:...`, `javascript:...`). Never throws, never returns null.\n *\n * Routing does not care about scheme or authority, only about the path part.\n * This keeps `browser-plugin`, `navigation-plugin`, and `hash-plugin` working\n * in Electron (`file://`, `app://`), Tauri (`tauri://`, `https://`), and any\n * other webview that may ship with non-HTTP origins. See issue #496.\n */\nexport function safeParseUrl(url: string): ParsedUrl {\n let rest = url;\n\n const schemeIdx = rest.indexOf(\"://\");\n\n if (schemeIdx !== -1) {\n const authorityStart = schemeIdx + 3;\n let pathStart = rest.length;\n\n for (let i = authorityStart; i < rest.length; i++) {\n const ch = rest[i];\n\n if (ch === \"/\" || ch === \"?\" || ch === \"#\") {\n pathStart = i;\n\n break;\n }\n }\n\n rest = pathStart === rest.length ? \"/\" : rest.slice(pathStart);\n\n if (rest.startsWith(\"?\") || rest.startsWith(\"#\")) {\n rest = `/${rest}`;\n }\n }\n\n const hashIdx = rest.indexOf(\"#\");\n const hash = hashIdx === -1 ? \"\" : rest.slice(hashIdx);\n const beforeHash = hashIdx === -1 ? rest : rest.slice(0, hashIdx);\n\n const queryIdx = beforeHash.indexOf(\"?\");\n const search = queryIdx === -1 ? \"\" : beforeHash.slice(queryIdx);\n const pathname = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx);\n\n return { pathname, search, hash };\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(url: string, base: string): string {\n const parsedUrl = safeParseUrl(url);\n\n return extractPath(parsedUrl.pathname, base) + parsedUrl.search;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} kept for call-site readability — history-query\n * paths (Navigation API entries, etc.) are absolute URLs by contract.\n */\nexport function extractPathFromAbsoluteUrl(url: string, base: string): string {\n return urlToPath(url, base);\n}\n","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, options);\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n };\n}\n","import { extractPathFromAbsoluteUrl } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n return (\n api.matchPath(extractPathFromAbsoluteUrl(entry.url, base)) ?? undefined\n );\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n let count = 0;\n\n for (const entry of browser.entries()) {\n if (entryToState(entry, api, base)?.name === routeName) {\n count++;\n }\n }\n\n return count;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { extractPath, safeParseUrl } from \"./browser-env\";\n\nimport type {\n NavigationBrowser,\n NavigationDirection,\n NavigationMeta,\n} from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setCapturedMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function computeDirection(\n navigationType: NavigationMeta[\"navigationType\"],\n destinationIndex: number,\n currentIndex: number,\n): NavigationDirection {\n if (navigationType === \"traverse\") {\n if (destinationIndex === currentIndex) {\n return \"unknown\";\n }\n\n return destinationIndex > currentIndex ? \"forward\" : \"back\";\n }\n\n return navigationType === \"push\" ? \"forward\" : \"unknown\";\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept || !router.isActive()) {\n return;\n }\n\n if (isSyncingFromRouter()) {\n // Plugin-originated navigate event after its own successful transition\n // (onTransitionSuccess calls browser.navigate to sync URL). We must still\n // intercept — a bare `return` leaves the event un-intercepted, and\n // Chromium falls back to a cross-document navigation (full page reload).\n // The noop handler cancels the fallback without running router logic;\n // state is already committed.\n event.intercept({\n handler: async () => {},\n });\n\n return;\n }\n\n const destinationUrl = safeParseUrl(event.destination.url);\n const path =\n extractPath(destinationUrl.pathname, base) + destinationUrl.search;\n const matchedState = api.matchPath(path);\n\n const navType = event.navigationType as NavigationMeta[\"navigationType\"];\n const currentIndex = browser.currentEntry?.index ?? -1;\n\n deps.setCapturedMeta({\n navigationType: navType,\n userInitiated: event.userInitiated,\n info: event.info,\n direction: computeDirection(\n navType,\n event.destination.index,\n currentIndex,\n ),\n sourceElement: event.sourceElement ?? null,\n });\n\n const withRecovery = async (run: () => Promise<unknown>): Promise<void> => {\n try {\n await run();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n };\n\n if (matchedState) {\n event.intercept({\n handler: () =>\n withRecovery(() =>\n router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n }),\n ),\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and reject the\n // intercept so the Navigation API auto-rolls back the URL. No silent\n // fallback to defaultRoute.\n event.intercept({\n // eslint-disable-next-line @typescript-eslint/require-await -- Navigation API requires async handler; synchronous throw is the rollback signal\n handler: async () => {\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, { path });\n\n api.emitTransitionError(err);\n\n throw err;\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n\n try {\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n } finally {\n setSyncing(false);\n }\n }\n } catch (recoveryError) {\n console.error(\n \"[navigation-plugin] Failed to recover from critical error\",\n recoveryError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params) + browser.getHash();\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n\n try {\n browser.replaceState(historyState, url);\n } finally {\n setSyncing(false);\n }\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport {\n shouldReplaceHistory,\n buildUrl,\n extractPathFromAbsoluteUrl,\n urlToPath,\n} from \"./browser-env\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: NavigationMeta) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n #capturedMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#claim = api.claimContextNamespace(\"navigation\");\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) =>\n api.matchPath(urlToPath(url, options.base)) ?? undefined,\n replaceHistoryState: createReplaceHistoryState(\n api,\n router,\n browser,\n pluginBuildUrl,\n (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setCapturedMeta: (meta) => {\n this.#capturedMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n releaseClaim: () => {\n this.#claim.release();\n },\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const entry = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const path = extractPathFromAbsoluteUrl(entry.url, this.#options.base);\n const matchedState = this.#api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n /* v8 ignore next -- @preserve: currentEntry always exists when traverseToLast is callable (after start) */\n const currentIndex = this.#browser.currentEntry?.index ?? -1;\n\n this.#capturedMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n direction: entry.index > currentIndex ? \"forward\" : \"back\",\n sourceElement: null,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionStart: (toState: State) => {\n if (this.#capturedMeta) {\n this.#claim.write(toState, this.#capturedMeta);\n }\n },\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#capturedMeta) {\n const navigationType = deriveNavigationType(\n navOptions,\n toState,\n fromState,\n );\n\n this.#capturedMeta = {\n navigationType,\n userInitiated: false,\n direction: navigationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n const { navigationType } = this.#capturedMeta;\n\n this.#claim.write(toState, Object.freeze(this.#capturedMeta));\n this.#capturedMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n try {\n if (this.#pendingTraverseKey) {\n this.#browser.traverseTo(this.#pendingTraverseKey);\n this.#pendingTraverseKey = undefined;\n } else {\n const url = buildUrl(toState.path, this.#options.base);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const hash = shouldPreserveHash ? this.#browser.getHash() : \"\";\n const finalUrl = hash ? url + hash : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = navigationType !== \"push\";\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n } finally {\n this.#isSyncingFromRouter = false;\n }\n },\n\n onTransitionCancel: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n releaseClaim: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n deps.releaseClaim();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n };\n};\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["RouterError","errorCodes","#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle","#capturedMeta","#isSyncingFromRouter","#pendingTraverseKey","UNKNOWN_ROUTE"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path to canonical form: leading slash, no trailing slash,\n * no repeated slashes. Isolated \"/\" collapses to \"\".\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"//app//\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n * normalizeBase(\"/\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base.replaceAll(/\\/+/g, \"/\");\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.length > 1 && result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result === \"/\" ? \"\" : result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export interface OptionRule<T> {\n validate: (value: T) => string | null;\n}\n\nexport type OptionRules<T extends object> = {\n [K in keyof T]?: OptionRule<NonNullable<T[K]>>;\n};\n\nexport function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\n rules?: OptionRules<T>,\n): (opts: Partial<T> | undefined) => void {\n return (opts) => {\n if (!opts) {\n return;\n }\n\n for (const key of Object.keys(opts)) {\n if (!(key in defaults)) {\n continue;\n }\n\n const value = opts[key as keyof typeof opts];\n\n if (value === undefined) {\n continue;\n }\n\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n\n const rule = rules?.[key as keyof T];\n\n if (rule) {\n const msg = (rule.validate as (input: unknown) => string | null)(value);\n\n if (msg !== null) {\n throw new Error(`[${loggerContext}] Invalid '${key}': ${msg}`);\n }\n }\n }\n };\n}\n\n// eslint-disable-next-line no-control-regex -- control characters are exactly what this rule rejects\nconst CONTROL_CHARS = /[\\u0000-\\u001F\\u007F]/;\n\nexport const safeBaseRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.split(\"/\").includes(\"..\")) {\n return \"must not contain '..' segments\";\n }\n\n return null;\n },\n};\n\nexport const safeHashPrefixRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.includes(\"/\")) {\n return \"must not contain '/' (slash is added before the path automatically)\";\n }\n\n if (value.includes(\"#\")) {\n return \"must not contain '#' (it is added as the hash delimiter)\";\n }\n\n if (value.includes(\"?\")) {\n return \"must not contain '?' (it conflicts with the query delimiter)\";\n }\n\n return null;\n },\n};\n\nexport const nonNegativeIntegerRule: OptionRule<number> = {\n validate: (value) => {\n if (!Number.isFinite(value)) {\n return `expected finite number, got ${String(value)}`;\n }\n\n if (!Number.isInteger(value)) {\n return `expected integer, got ${String(value)}`;\n }\n\n if (value < 0) {\n return `expected non-negative integer, got ${value}`;\n }\n\n return null;\n },\n};\n","import { createUpdateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n const updateState = createUpdateBrowserState();\n\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n if (navOptions.replace === true) {\n return true;\n }\n\n if (!fromState) {\n return navOptions.replace !== false;\n }\n\n return !!navOptions.reload && toState.path === fromState.path;\n}\n","export interface ParsedUrl {\n pathname: string;\n search: string;\n hash: string;\n}\n\n/**\n * Scheme-agnostic URL parser.\n *\n * Extracts `pathname`, `search`, and `hash` from any string — absolute\n * (`scheme://authority/path?q#h`), path-relative (`/path?q#h`), or opaque\n * (`data:...`, `javascript:...`). Never throws, never returns null.\n *\n * Routing does not care about scheme or authority, only about the path part.\n * This keeps `browser-plugin`, `navigation-plugin`, and `hash-plugin` working\n * in Electron (`file://`, `app://`), Tauri (`tauri://`, `https://`), and any\n * other webview that may ship with non-HTTP origins. See issue #496.\n */\nexport function safeParseUrl(url: string): ParsedUrl {\n let rest = url;\n\n const schemeIdx = rest.indexOf(\"://\");\n\n if (schemeIdx !== -1) {\n const authorityStart = schemeIdx + 3;\n let pathStart = rest.length;\n\n for (let i = authorityStart; i < rest.length; i++) {\n const ch = rest[i];\n\n if (ch === \"/\" || ch === \"?\" || ch === \"#\") {\n pathStart = i;\n\n break;\n }\n }\n\n rest = pathStart === rest.length ? \"/\" : rest.slice(pathStart);\n\n if (rest.startsWith(\"?\") || rest.startsWith(\"#\")) {\n rest = `/${rest}`;\n }\n }\n\n const hashIdx = rest.indexOf(\"#\");\n const hash = hashIdx === -1 ? \"\" : rest.slice(hashIdx);\n const beforeHash = hashIdx === -1 ? rest : rest.slice(0, hashIdx);\n\n const queryIdx = beforeHash.indexOf(\"?\");\n const search = queryIdx === -1 ? \"\" : beforeHash.slice(queryIdx);\n const pathname = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx);\n\n return { pathname, search, hash };\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n // Path \"/\" with a non-empty base would otherwise produce `\"${base}/\"` —\n // a trailing-slash URL (e.g. `/app/`). The canonical form of the base\n // (normalizeBase strips trailing slash) is `/app`, and the router's\n // `extractPath(\"/app\", \"/app\")` round-trips to `\"/\"` regardless. Collapse\n // the index case to the canonical base to keep URLs symmetric.\n if (path === \"/\") {\n return base;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(url: string, base: string): string {\n const parsedUrl = safeParseUrl(url);\n\n return extractPath(parsedUrl.pathname, base) + parsedUrl.search;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} kept for call-site readability — history-query\n * paths (Navigation API entries, etc.) are absolute URLs by contract.\n */\nexport function extractPathFromAbsoluteUrl(url: string, base: string): string {\n return urlToPath(url, base);\n}\n","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n // Default `false` respects `canDeactivate` guards on browser back/forward,\n // matching the documented contract of `browser-plugin` and the core router.\n // Apps that want the browser's native history buttons to bypass guards\n // (e.g. to avoid dead-end UX) can opt in via `forceDeactivate: true`.\n forceDeactivate: false,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, options);\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n\n getActivationType: () => nav.activation?.navigationType,\n };\n}\n","import { extractPathFromAbsoluteUrl } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Validates a candidate history entry for `traverseToLast(routeName)` and\n * returns both the entry (now known non-null) and the matched router state.\n * Extracted from `NavigationPlugin` so the three error branches (missing\n * entry, null url, unmatched url) can be tested directly without vi.spyOn\n * on module namespaces — the star-import spy pattern is fragile under ESM\n * and was working by accident in history-extensions.test.ts.\n *\n * Throws a descriptive Error on any failure; the caller (NavigationPlugin)\n * propagates it as the rejection of `traverseToLast`.\n */\nexport function resolveEntryToMatchedState(\n entry: NavigationHistoryEntry | undefined,\n routeName: string,\n api: PluginApi,\n base: string,\n): { entry: NavigationHistoryEntry; matchedState: State } {\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const path = extractPathFromAbsoluteUrl(entry.url, base);\n const matchedState = api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n return { entry, matchedState };\n}\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n return (\n api.matchPath(extractPathFromAbsoluteUrl(entry.url, base)) ?? undefined\n );\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n let count = 0;\n\n for (const entry of browser.entries()) {\n if (entryToState(entry, api, base)?.name === routeName) {\n count++;\n }\n }\n\n return count;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { urlToPath } from \"./browser-env\";\n\nimport type {\n NavigationBrowser,\n NavigationDirection,\n NavigationMeta,\n} from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setCapturedMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function computeDirection(\n navigationType: NavigationMeta[\"navigationType\"],\n destinationIndex: number,\n currentIndex: number,\n): NavigationDirection {\n if (navigationType === \"traverse\") {\n if (destinationIndex === currentIndex) {\n return \"unknown\";\n }\n\n return destinationIndex > currentIndex ? \"forward\" : \"back\";\n }\n\n return navigationType === \"push\" ? \"forward\" : \"unknown\";\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept || !router.isActive()) {\n return;\n }\n\n if (isSyncingFromRouter()) {\n // Plugin-originated navigate event after its own successful transition\n // (onTransitionSuccess calls browser.navigate to sync URL). We must still\n // intercept — a bare `return` leaves the event un-intercepted, and\n // Chromium falls back to a cross-document navigation (full page reload).\n // The noop handler cancels the fallback without running router logic;\n // state is already committed.\n event.intercept({\n handler: async () => {},\n });\n\n return;\n }\n\n const path = urlToPath(event.destination.url, base);\n const matchedState = api.matchPath(path);\n\n const navType = event.navigationType as NavigationMeta[\"navigationType\"];\n const currentIndex = browser.currentEntry?.index ?? -1;\n\n deps.setCapturedMeta({\n navigationType: navType,\n userInitiated: event.userInitiated,\n info: event.info,\n direction: computeDirection(\n navType,\n event.destination.index,\n currentIndex,\n ),\n sourceElement: event.sourceElement ?? null,\n });\n\n const withRecovery = async (run: () => Promise<unknown>): Promise<void> => {\n try {\n await run();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n\n return;\n }\n\n // TRANSITION_CANCELLED: a newer navigation aborted this one — the\n // newer navigate event is (or will be) handled by this same plugin,\n // and THAT event is responsible for syncing URL/state. Firing our\n // own sync here races against it: browser.navigate(replace, same-url)\n // would cancel the in-flight newer transition, which is exactly the\n // rapid-fire-events storm failure mode.\n //\n // SAME_STATES: router refused because router.getState() already equals\n // the target. URL and router state are already consistent — no sync\n // needed.\n if (\n error.code === errorCodes.TRANSITION_CANCELLED ||\n error.code === errorCodes.SAME_STATES\n ) {\n return;\n }\n\n // Other RouterError codes (CANNOT_DEACTIVATE, CANNOT_ACTIVATE,\n // ROUTE_NOT_FOUND, …) — router rejected the transition, state is\n // unchanged, but URL may have already committed to a different\n // value by the Navigation API. Sync the URL back to the current\n // router state in a single visible transition (headless Chromium\n // and some cross-origin setups leave \"committed-then-reverted\"\n // windows if we relied on the native rollback via intercept reject).\n // Observers that care about the error see it through the router's\n // TRANSITION_ERROR event.\n syncUrlToRouterState(router, browser, setSyncing);\n }\n };\n\n if (matchedState) {\n event.intercept({\n handler: () =>\n withRecovery(() =>\n router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n }),\n ),\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and reject the\n // intercept so the Navigation API auto-rolls back the URL. No silent\n // fallback to defaultRoute.\n event.intercept({\n // eslint-disable-next-line @typescript-eslint/require-await -- Navigation API requires async handler; synchronous throw is the rollback signal\n handler: async () => {\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, { path });\n\n api.emitTransitionError(err);\n\n throw err;\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n syncUrlToRouterState(router, browser, setSyncing);\n}\n\nfunction syncUrlToRouterState(\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n\n try {\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n } finally {\n setSyncing(false);\n }\n }\n } catch (syncError) {\n console.error(\n \"[navigation-plugin] Failed to sync URL to router state\",\n syncError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params) + browser.getHash();\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n\n try {\n browser.replaceState(historyState, url);\n } finally {\n setSyncing(false);\n }\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport { shouldReplaceHistory, buildUrl, urlToPath } from \"./browser-env\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n resolveEntryToMatchedState,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: NavigationMeta) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n #capturedMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#claim = api.claimContextNamespace(\"navigation\");\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n // Cross-document load priming (#531). On F5, browser back/forward across\n // a page boundary, or a fresh URL bar entry, the prior JS context is\n // discarded — the navigate event handler never sees the activation.\n // Without this, deriveNavigationType in onTransitionSuccess falls through\n // to \"replace\" for every initial transition, breaking scroll restore on\n // reload (#497) and any consumer branching on navigationType.\n // navigation.activation reflects the cross-document navigation that\n // activated this document; it stays constant across same-document\n // navigations, so this only affects the FIRST transition.\n const activationType = browser.getActivationType();\n\n if (activationType) {\n this.#capturedMeta = {\n navigationType: activationType,\n userInitiated: false,\n direction: activationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) =>\n api.matchPath(urlToPath(url, options.base)) ?? undefined,\n replaceHistoryState: createReplaceHistoryState(\n api,\n router,\n browser,\n pluginBuildUrl,\n (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setCapturedMeta: (meta) => {\n this.#capturedMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n releaseClaim: () => {\n this.#claim.release();\n },\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const candidate = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n // resolveEntryToMatchedState throws for missing entry, null url, or\n // unmatched url — same three error branches the old inline checks\n // produced. Extracted so the error paths can be unit-tested directly\n // without namespace-level vi.spyOn gymnastics.\n const { entry, matchedState } = resolveEntryToMatchedState(\n candidate,\n routeName,\n this.#api,\n this.#options.base,\n );\n\n const currentEntry = this.#browser.currentEntry;\n\n if (!currentEntry) {\n // Invariant violation: traverseToLast is only callable after\n // router.start(), which guarantees a current entry. A null here means\n // the plugin was stopped mid-call or the browser abstraction is\n // broken — either way, silently picking direction \"forward\" from a\n // fallback `-1` would mask the bug. Fail loudly instead.\n throw new Error(\n `[navigation-plugin] Cannot determine direction for traverseToLast(\"${routeName}\"): browser.currentEntry is null. The plugin must be started before calling traverseToLast.`,\n );\n }\n\n this.#capturedMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n direction: entry.index > currentEntry.index ? \"forward\" : \"back\",\n sourceElement: null,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionStart: (toState: State) => {\n if (this.#capturedMeta) {\n this.#claim.write(toState, this.#capturedMeta);\n }\n },\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#capturedMeta) {\n const navigationType = deriveNavigationType(\n navOptions,\n toState,\n fromState,\n );\n\n this.#capturedMeta = {\n navigationType,\n userInitiated: false,\n direction: navigationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n const frozenMeta = Object.freeze(this.#capturedMeta);\n\n this.#claim.write(toState, frozenMeta);\n this.#capturedMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n // Consume pendingTraverseKey BEFORE calling browser.traverseTo.\n // If traverseTo throws (Navigation API can reject on evicted keys\n // under memory pressure), we must not leave the stale key behind —\n // otherwise the NEXT transition's onTransitionSuccess would see it\n // and replay the traverse against the same already-broken key.\n const traverseKey = this.#pendingTraverseKey;\n\n this.#pendingTraverseKey = undefined;\n\n try {\n if (traverseKey) {\n this.#browser.traverseTo(traverseKey);\n } else {\n const url = buildUrl(toState.path, this.#options.base);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const hash = shouldPreserveHash ? this.#browser.getHash() : \"\";\n const finalUrl = hash ? url + hash : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = frozenMeta.navigationType !== \"push\";\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n } finally {\n this.#isSyncingFromRouter = false;\n }\n },\n\n onTransitionCancel: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n releaseClaim: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n deps.releaseClaim();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n getActivationType: () => undefined,\n };\n};\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
@@ -38,6 +38,12 @@ interface NavigationBrowser {
38
38
  addNavigateListener: (fn: (evt: NavigateEvent) => void) => () => void;
39
39
  entries: () => NavigationHistoryEntry[];
40
40
  currentEntry: NavigationHistoryEntry | null;
41
+ /**
42
+ * Type of the cross-document navigation that activated this document.
43
+ * Reads `navigation.activation.navigationType` (Baseline 2026 — Chrome 123+, Firefox 147+, Safari 26.2+).
44
+ * Returns `undefined` when activation is unavailable (older browsers, SSR).
45
+ */
46
+ getActivationType: () => NavigationMeta["navigationType"] | undefined;
41
47
  }
42
48
  type NavigationDirection = "forward" | "back" | "unknown";
43
49
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;AAAA;AAAA,KAWJ,mBAAA;AAMZ;;;;AAAA,UAAiB,cAAA;EAIf;EAFA,cAAA;EAMA;EAJA,aAAA;EAMA;EAJA,IAAA;EAIsB;EAFtB,SAAA,EAAW,mBAAA;;EAEX,aAAA,EAAe,OAAA;AAAA;;;iBC/CD,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCLS,YAAA;IACR,UAAA,GAJa,cAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IAClC,QAAA,GAAW,GAAA,aAAgB,KAAA;IAC3B,mBAAA,GAAsB,IAAA,UAAc,MAAA,GAAS,MAAA;IAC7C,QAAA,QAAgB,KAAA;IAChB,WAAA,QAAmB,KAAA;IACnB,UAAA,GAAa,SAAA;IACb,gBAAA;IACA,kBAAA,GAAqB,SAAA;IACrB,cAAA,GAAiB,SAAA,aAAsB,OAAA,CAAQ,KAAA;IAC/C,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;EADC;;;;;EAOf,iBAAA,QAAyB,cAAA;AAAA;AAAA,KAWf,mBAAA;;;;;UAMK,cAAA;EAQJ;EANX,cAAA;EAQe;EANf,aAAA;EAMsB;EAJtB,IAAA;;EAEA,SAAA,EAAW,mBAAA;ECnDG;EDqDd,aAAA,EAAe,OAAA;AAAA;;;iBCrDD,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCLS,YAAA;IACR,UAAA,GAJa,cAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IAClC,QAAA,GAAW,GAAA,aAAgB,KAAA;IAC3B,mBAAA,GAAsB,IAAA,UAAc,MAAA,GAAS,MAAA;IAC7C,QAAA,QAAgB,KAAA;IAChB,WAAA,QAAmB,KAAA;IACnB,UAAA,GAAa,SAAA;IACb,gBAAA;IACA,kBAAA,GAAqB,SAAA;IACrB,cAAA,GAAiB,SAAA,aAAsB,OAAA,CAAQ,KAAA;IAC/C,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
@@ -1,2 +1,2 @@
1
- import{getPluginApi as e}from"@real-router/core/api";import{RouterError as t,UNKNOWN_ROUTE as n,errorCodes as r}from"@real-router/core";const i=()=>globalThis.window!==void 0&&!!globalThis.history;function a(e){if(!e)return e;let t=e.replaceAll(/\/+/g,`/`);return t.startsWith(`/`)||(t=`/${t}`),t.length>1&&t.endsWith(`/`)&&(t=t.slice(0,-1)),t===`/`?``:t}const o=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},s=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function c(e,t,n){return r=>{if(r)for(let i of Object.keys(r)){if(!(i in e))continue;let a=r[i];if(a===void 0)continue;let o=typeof e[i],s=typeof a;if(s!==o)throw Error(`[${t}] Invalid type for '${i}': expected ${o}, got ${s}`);let c=n?.[i];if(c){let e=c.validate(a);if(e!==null)throw Error(`[${t}] Invalid '${i}': ${e}`)}}}}const l=/[\u0000-\u001F\u007F]/,u={validate:e=>l.test(e)?`must not contain control characters`:e.split(`/`).includes(`..`)?`must not contain '..' segments`:null};function d(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}function f(e){let t=e,n=t.indexOf(`://`);if(n!==-1){let e=n+3,r=t.length;for(let n=e;n<t.length;n++){let e=t[n];if(e===`/`||e===`?`||e===`#`){r=n;break}}t=r===t.length?`/`:t.slice(r),(t.startsWith(`?`)||t.startsWith(`#`))&&(t=`/${t}`)}let r=t.indexOf(`#`),i=r===-1?``:t.slice(r),a=r===-1?t:t.slice(0,r),o=a.indexOf(`?`),s=o===-1?``:a.slice(o);return{pathname:o===-1?a:a.slice(0,o),search:s,hash:i}}function p(e,t){if(!e)return`/`;if(t&&(e===t||e.startsWith(`${t}/`))){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e.startsWith(`/`)?e:`/${e}`}function m(e,t){return e?t?e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function h(e,t){let n=f(e);return p(n.pathname,t)+n.search}function g(e,t){return h(e,t)}const _={forceDeactivate:!0,base:``};function v(e){let t=globalThis.navigation;return{getLocation:()=>o(p(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,n)},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry}}}function y(e,t,n){if(e?.url)return t.matchPath(g(e.url,n))??void 0}function b(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return y(e.entries()[i+r],t,n)}function x(e,t,n){return b(e,t,n,-1)}function S(e,t,n){return b(e,t,n,1)}function C(e,t,n,r){return e.entries().some(e=>y(e,t,n)?.name===r)}function w(e,t,n){let r=new Set;for(let i of e.entries()){let e=y(i,t,n);e&&r.add(e.name)}return[...r]}function T(e,t,n,r){let i=0;for(let a of e.entries())y(a,t,n)?.name===r&&i++;return i}function E(e,t,n,r,i){for(let a=e.length-1;a>=0;a--){let o=e[a];if(o.key!==i&&y(o,n,r)?.name===t)return o}}function D(e){let t=e.currentEntry?.index;return t!=null&&t>0}function O(e){let t=e.currentEntry?.index;return t==null?!1:t<e.entries().length-1}function k(e,t,n,r){let i=e.currentEntry?.index;if(i==null)return!1;let a=e.entries();for(let e=i-1;e>=0;e--)if(y(a[e],t,n)?.name===r)return!0;return!1}function A(e,t,n){return e===`traverse`?t===n?`unknown`:t>n?`forward`:`back`:e===`push`?`forward`:`unknown`}function j(e){let{router:n,api:i,browser:a,isSyncingFromRouter:o,setSyncing:s,base:c,transitionOptions:l}=e,{allowNotFound:u}=i.getOptions();return function(d){if(!d.canIntercept||!n.isActive())return;if(o()){d.intercept({handler:async()=>{}});return}let m=f(d.destination.url),h=p(m.pathname,c)+m.search,g=i.matchPath(h),_=d.navigationType,v=a.currentEntry?.index??-1;e.setCapturedMeta({navigationType:_,userInitiated:d.userInitiated,info:d.info,direction:A(_,d.destination.index,v),sourceElement:d.sourceElement??null});let y=async e=>{try{await e()}catch(e){e instanceof t||M(e,n,a,s)}};g?d.intercept({handler:()=>y(()=>n.navigate(g.name,g.params,{...l,signal:d.signal}))}):u?d.intercept({handler:()=>{n.navigateToNotFound(h)}}):d.intercept({handler:async()=>{let e=new t(r.ROUTE_NOT_FOUND,{path:h});throw i.emitTransitionError(e),e}})}}function M(e,t,n,r){console.error(`[navigation-plugin] Critical error in navigate handler`,e);try{let e=t.getState();if(e){let i=t.buildUrl(e.name,e.params);r(!0);try{n.navigate(i,{state:{name:e.name,params:e.params,path:e.path},history:`replace`})}finally{r(!1)}}}catch(e){console.error(`[navigation-plugin] Failed to recover from critical error`,e)}}function N(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function P(e,t,n,r,i){return(a,o={})=>{let s=e.buildState(a,o);if(!s)throw Error(`[real-router] Cannot replace state: route "${a}" is not found`);let c=e.makeState(s.name,s.params,t.buildPath(s.name,s.params),{params:s.meta}),l=r(a,o)+n.getHash(),u={name:c.name,params:c.params,path:c.path};i(!0);try{n.replaceState(u,l)}finally{i(!1)}}}function F(e,t,n){return e.reload&&t.path===n?.path?`reload`:d(e,t,n)?`replace`:`push`}var I=class{#e;#t;#n;#r;#i;#a;#o;#s;#c=!1;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#o=t.claimContextNamespace(`navigation`),this.#i=N(t,r);let o=(t,r)=>m(e.buildPath(t,r),n.base);this.#a=t.extendRouter({buildUrl:o,matchUrl:e=>t.matchPath(h(e,n.base))??void 0,replaceHistoryState:P(t,e,r,o,e=>{this.#c=e}),peekBack:()=>x(r,t,n.base),peekForward:()=>S(r,t,n.base),hasVisited:e=>C(r,t,n.base,e),getVisitedRoutes:()=>w(r,t,n.base),getRouteVisitCount:e=>T(r,t,n.base,e),traverseToLast:e=>this.traverseToLast(e),canGoBack:()=>D(r),canGoForward:()=>O(r),canGoBackTo:e=>k(r,t,n.base,e)}),this.#s=L({browser:r,shared:a,handler:j({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#c,setSyncing:e=>{this.#c=e},setCapturedMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a,releaseClaim:()=>{this.#o.release()}})}async traverseToLast(e){let t=this.#r.entries(),n=this.#r.currentEntry?.key,r=E(t,e,this.#t,this.#n.base,n);if(!r)throw Error(`No history entry for route "${e}"`);if(!r.url)throw Error(`No matching route for entry URL "${r.url}"`);let i=g(r.url,this.#n.base),a=this.#t.matchPath(i);if(!a)throw Error(`No matching route for entry URL "${r.url}"`);let o=this.#r.currentEntry?.index??-1;return this.#l={navigationType:`traverse`,userInitiated:!1,direction:r.index>o?`forward`:`back`,sourceElement:null},this.#u=r.key,this.#e.navigate(a.name,a.params)}getPlugin(){return{...this.#s,onTransitionStart:e=>{this.#l&&this.#o.write(e,this.#l)},onTransitionSuccess:(e,t,r)=>{if(!this.#l){let n=F(r,e,t);this.#l={navigationType:n,userInitiated:!1,direction:n===`push`?`forward`:`unknown`,sourceElement:null}}let{navigationType:i}=this.#l;this.#o.write(e,Object.freeze(this.#l)),this.#l=void 0,this.#c=!0;try{if(this.#u)this.#r.traverseTo(this.#u),this.#u=void 0;else{let r=m(e.path,this.#n.base),a=!t||t.path===e.path?this.#r.getHash():``,o=a?r+a:r,s={name:e.name,params:e.params,path:e.path};if(e.name===n)this.#r.updateCurrentEntry({state:s});else{let e=i!==`push`;this.#r.navigate(o,{state:s,history:e?`replace`:`push`})}}}finally{this.#c=!1}},onTransitionCancel:()=>{this.#l=void 0,this.#u=void 0},onTransitionError:()=>{this.#l=void 0,this.#u=void 0}}}};function L(e){return{onStart(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=e.browser.addNavigateListener(e.handler)},onStop(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0},teardown(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0,e.removeStartInterceptor(),e.removeExtensions(),e.releaseClaim()}}}const R=()=>{},z=e=>{let t=s(e);return{getLocation:()=>(t(`getLocation`),`/`),getHash:()=>(t(`getHash`),``),navigate:()=>{t(`navigate`)},replaceState:()=>{t(`replaceState`)},updateCurrentEntry:()=>{t(`updateCurrentEntry`)},traverseTo:()=>{t(`traverseTo`)},addNavigateListener:()=>(t(`addNavigateListener`),R),entries:()=>(t(`entries`),[]),currentEntry:null}},B=c(_,`navigation-plugin`,{base:u});function V(t,n){if(!n&&i()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);B(t);let r={..._,...t};r.base=a(r.base);let o=n??H(r.base),s={forceDeactivate:r.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new I(t,e(t),r,o,s,c).getPlugin()}function H(e){return`navigation`in globalThis?v(e):z(`navigation-plugin`)}export{V as navigationPluginFactory};
1
+ import{getPluginApi as e}from"@real-router/core/api";import{RouterError as t,UNKNOWN_ROUTE as n,errorCodes as r}from"@real-router/core";const i=()=>globalThis.window!==void 0&&!!globalThis.history;function a(e){if(!e)return e;let t=e.replaceAll(/\/+/g,`/`);return t.startsWith(`/`)||(t=`/${t}`),t.length>1&&t.endsWith(`/`)&&(t=t.slice(0,-1)),t===`/`?``:t}const o=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},s=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function c(e,t,n){return r=>{if(r)for(let i of Object.keys(r)){if(!(i in e))continue;let a=r[i];if(a===void 0)continue;let o=typeof e[i],s=typeof a;if(s!==o)throw Error(`[${t}] Invalid type for '${i}': expected ${o}, got ${s}`);let c=n?.[i];if(c){let e=c.validate(a);if(e!==null)throw Error(`[${t}] Invalid '${i}': ${e}`)}}}}const l=/[\u0000-\u001F\u007F]/,u={validate:e=>l.test(e)?`must not contain control characters`:e.split(`/`).includes(`..`)?`must not contain '..' segments`:null};function d(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}function f(e){let t=e,n=t.indexOf(`://`);if(n!==-1){let e=n+3,r=t.length;for(let n=e;n<t.length;n++){let e=t[n];if(e===`/`||e===`?`||e===`#`){r=n;break}}t=r===t.length?`/`:t.slice(r),(t.startsWith(`?`)||t.startsWith(`#`))&&(t=`/${t}`)}let r=t.indexOf(`#`),i=r===-1?``:t.slice(r),a=r===-1?t:t.slice(0,r),o=a.indexOf(`?`),s=o===-1?``:a.slice(o);return{pathname:o===-1?a:a.slice(0,o),search:s,hash:i}}function p(e,t){if(!e)return`/`;if(t&&(e===t||e.startsWith(`${t}/`))){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e.startsWith(`/`)?e:`/${e}`}function m(e,t){return e?t?e===`/`?t:e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function h(e,t){let n=f(e);return p(n.pathname,t)+n.search}function g(e,t){return h(e,t)}const _={forceDeactivate:!1,base:``};function v(e){let t=globalThis.navigation;return{getLocation:()=>o(p(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,n)},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry},getActivationType:()=>t.activation?.navigationType}}function y(e,t,n,r){if(!e)throw Error(`No history entry for route "${t}"`);if(!e.url)throw Error(`No matching route for entry URL "${e.url}"`);let i=g(e.url,r),a=n.matchPath(i);if(!a)throw Error(`No matching route for entry URL "${e.url}"`);return{entry:e,matchedState:a}}function b(e,t,n){if(e?.url)return t.matchPath(g(e.url,n))??void 0}function x(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return b(e.entries()[i+r],t,n)}function S(e,t,n){return x(e,t,n,-1)}function C(e,t,n){return x(e,t,n,1)}function w(e,t,n,r){return e.entries().some(e=>b(e,t,n)?.name===r)}function T(e,t,n){let r=new Set;for(let i of e.entries()){let e=b(i,t,n);e&&r.add(e.name)}return[...r]}function E(e,t,n,r){let i=0;for(let a of e.entries())b(a,t,n)?.name===r&&i++;return i}function D(e,t,n,r,i){for(let a=e.length-1;a>=0;a--){let o=e[a];if(o.key!==i&&b(o,n,r)?.name===t)return o}}function O(e){let t=e.currentEntry?.index;return t!=null&&t>0}function k(e){let t=e.currentEntry?.index;return t==null?!1:t<e.entries().length-1}function A(e,t,n,r){let i=e.currentEntry?.index;if(i==null)return!1;let a=e.entries();for(let e=i-1;e>=0;e--)if(b(a[e],t,n)?.name===r)return!0;return!1}function j(e,t,n){return e===`traverse`?t===n?`unknown`:t>n?`forward`:`back`:e===`push`?`forward`:`unknown`}function M(e){let{router:n,api:i,browser:a,isSyncingFromRouter:o,setSyncing:s,base:c,transitionOptions:l}=e,{allowNotFound:u}=i.getOptions();return function(d){if(!d.canIntercept||!n.isActive())return;if(o()){d.intercept({handler:async()=>{}});return}let f=h(d.destination.url,c),p=i.matchPath(f),m=d.navigationType,g=a.currentEntry?.index??-1;e.setCapturedMeta({navigationType:m,userInitiated:d.userInitiated,info:d.info,direction:j(m,d.destination.index,g),sourceElement:d.sourceElement??null});let _=async e=>{try{await e()}catch(e){if(!(e instanceof t)){N(e,n,a,s);return}if(e.code===r.TRANSITION_CANCELLED||e.code===r.SAME_STATES)return;P(n,a,s)}};p?d.intercept({handler:()=>_(()=>n.navigate(p.name,p.params,{...l,signal:d.signal}))}):u?d.intercept({handler:()=>{n.navigateToNotFound(f)}}):d.intercept({handler:async()=>{let e=new t(r.ROUTE_NOT_FOUND,{path:f});throw i.emitTransitionError(e),e}})}}function N(e,t,n,r){console.error(`[navigation-plugin] Critical error in navigate handler`,e),P(t,n,r)}function P(e,t,n){try{let r=e.getState();if(r){let i=e.buildUrl(r.name,r.params);n(!0);try{t.navigate(i,{state:{name:r.name,params:r.params,path:r.path},history:`replace`})}finally{n(!1)}}}catch(e){console.error(`[navigation-plugin] Failed to sync URL to router state`,e)}}function F(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function I(e,t,n,r,i){return(a,o={})=>{let s=e.buildState(a,o);if(!s)throw Error(`[real-router] Cannot replace state: route "${a}" is not found`);let c=e.makeState(s.name,s.params,t.buildPath(s.name,s.params),{params:s.meta}),l=r(a,o)+n.getHash(),u={name:c.name,params:c.params,path:c.path};i(!0);try{n.replaceState(u,l)}finally{i(!1)}}}function L(e,t,n){return e.reload&&t.path===n?.path?`reload`:d(e,t,n)?`replace`:`push`}var R=class{#e;#t;#n;#r;#i;#a;#o;#s;#c=!1;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#o=t.claimContextNamespace(`navigation`),this.#i=F(t,r);let o=r.getActivationType();o&&(this.#l={navigationType:o,userInitiated:!1,direction:o===`push`?`forward`:`unknown`,sourceElement:null});let s=(t,r)=>m(e.buildPath(t,r),n.base);this.#a=t.extendRouter({buildUrl:s,matchUrl:e=>t.matchPath(h(e,n.base))??void 0,replaceHistoryState:I(t,e,r,s,e=>{this.#c=e}),peekBack:()=>S(r,t,n.base),peekForward:()=>C(r,t,n.base),hasVisited:e=>w(r,t,n.base,e),getVisitedRoutes:()=>T(r,t,n.base),getRouteVisitCount:e=>E(r,t,n.base,e),traverseToLast:e=>this.traverseToLast(e),canGoBack:()=>O(r),canGoForward:()=>k(r),canGoBackTo:e=>A(r,t,n.base,e)}),this.#s=z({browser:r,shared:a,handler:M({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#c,setSyncing:e=>{this.#c=e},setCapturedMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a,releaseClaim:()=>{this.#o.release()}})}async traverseToLast(e){let t=this.#r.entries(),n=this.#r.currentEntry?.key,{entry:r,matchedState:i}=y(D(t,e,this.#t,this.#n.base,n),e,this.#t,this.#n.base),a=this.#r.currentEntry;if(!a)throw Error(`[navigation-plugin] Cannot determine direction for traverseToLast("${e}"): browser.currentEntry is null. The plugin must be started before calling traverseToLast.`);return this.#l={navigationType:`traverse`,userInitiated:!1,direction:r.index>a.index?`forward`:`back`,sourceElement:null},this.#u=r.key,this.#e.navigate(i.name,i.params)}getPlugin(){return{...this.#s,onTransitionStart:e=>{this.#l&&this.#o.write(e,this.#l)},onTransitionSuccess:(e,t,r)=>{if(!this.#l){let n=L(r,e,t);this.#l={navigationType:n,userInitiated:!1,direction:n===`push`?`forward`:`unknown`,sourceElement:null}}let i=Object.freeze(this.#l);this.#o.write(e,i),this.#l=void 0,this.#c=!0;let a=this.#u;this.#u=void 0;try{if(a)this.#r.traverseTo(a);else{let r=m(e.path,this.#n.base),a=!t||t.path===e.path?this.#r.getHash():``,o=a?r+a:r,s={name:e.name,params:e.params,path:e.path};if(e.name===n)this.#r.updateCurrentEntry({state:s});else{let e=i.navigationType!==`push`;this.#r.navigate(o,{state:s,history:e?`replace`:`push`})}}}finally{this.#c=!1}},onTransitionCancel:()=>{this.#l=void 0,this.#u=void 0},onTransitionError:()=>{this.#l=void 0,this.#u=void 0}}}};function z(e){return{onStart(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=e.browser.addNavigateListener(e.handler)},onStop(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0},teardown(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0,e.removeStartInterceptor(),e.removeExtensions(),e.releaseClaim()}}}const B=()=>{},V=e=>{let t=s(e);return{getLocation:()=>(t(`getLocation`),`/`),getHash:()=>(t(`getHash`),``),navigate:()=>{t(`navigate`)},replaceState:()=>{t(`replaceState`)},updateCurrentEntry:()=>{t(`updateCurrentEntry`)},traverseTo:()=>{t(`traverseTo`)},addNavigateListener:()=>(t(`addNavigateListener`),B),entries:()=>(t(`entries`),[]),currentEntry:null,getActivationType:()=>void 0}},H=c(_,`navigation-plugin`,{base:u});function U(t,n){if(!n&&i()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);H(t);let r={..._,...t};r.base=a(r.base);let o=n??W(r.base),s={forceDeactivate:r.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new R(t,e(t),r,o,s,c).getPlugin()}function W(e){return`navigation`in globalThis?v(e):V(`navigation-plugin`)}export{U as navigationPluginFactory};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle","#isSyncingFromRouter","#capturedMeta","#pendingTraverseKey"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path to canonical form: leading slash, no trailing slash,\n * no repeated slashes. Isolated \"/\" collapses to \"\".\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"//app//\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n * normalizeBase(\"/\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base.replaceAll(/\\/+/g, \"/\");\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.length > 1 && result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result === \"/\" ? \"\" : result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export interface OptionRule<T> {\n validate: (value: T) => string | null;\n}\n\nexport type OptionRules<T extends object> = {\n [K in keyof T]?: OptionRule<NonNullable<T[K]>>;\n};\n\nexport function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\n rules?: OptionRules<T>,\n): (opts: Partial<T> | undefined) => void {\n return (opts) => {\n if (!opts) {\n return;\n }\n\n for (const key of Object.keys(opts)) {\n if (!(key in defaults)) {\n continue;\n }\n\n const value = opts[key as keyof typeof opts];\n\n if (value === undefined) {\n continue;\n }\n\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n\n const rule = rules?.[key as keyof T];\n\n if (rule) {\n const msg = (rule.validate as (input: unknown) => string | null)(value);\n\n if (msg !== null) {\n throw new Error(`[${loggerContext}] Invalid '${key}': ${msg}`);\n }\n }\n }\n };\n}\n\n// eslint-disable-next-line no-control-regex -- control characters are exactly what this rule rejects\nconst CONTROL_CHARS = /[\\u0000-\\u001F\\u007F]/;\n\nexport const safeBaseRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.split(\"/\").includes(\"..\")) {\n return \"must not contain '..' segments\";\n }\n\n return null;\n },\n};\n\nexport const safeHashPrefixRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.includes(\"/\")) {\n return \"must not contain '/' (slash is added before the path automatically)\";\n }\n\n if (value.includes(\"#\")) {\n return \"must not contain '#' (it is added as the hash delimiter)\";\n }\n\n if (value.includes(\"?\")) {\n return \"must not contain '?' (it conflicts with the query delimiter)\";\n }\n\n return null;\n },\n};\n\nexport const nonNegativeIntegerRule: OptionRule<number> = {\n validate: (value) => {\n if (!Number.isFinite(value)) {\n return `expected finite number, got ${String(value)}`;\n }\n\n if (!Number.isInteger(value)) {\n return `expected integer, got ${String(value)}`;\n }\n\n if (value < 0) {\n return `expected non-negative integer, got ${value}`;\n }\n\n return null;\n },\n};\n","import { createUpdateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n const updateState = createUpdateBrowserState();\n\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n if (navOptions.replace === true) {\n return true;\n }\n\n if (!fromState) {\n return navOptions.replace !== false;\n }\n\n return !!navOptions.reload && toState.path === fromState.path;\n}\n","export interface ParsedUrl {\n pathname: string;\n search: string;\n hash: string;\n}\n\n/**\n * Scheme-agnostic URL parser.\n *\n * Extracts `pathname`, `search`, and `hash` from any string — absolute\n * (`scheme://authority/path?q#h`), path-relative (`/path?q#h`), or opaque\n * (`data:...`, `javascript:...`). Never throws, never returns null.\n *\n * Routing does not care about scheme or authority, only about the path part.\n * This keeps `browser-plugin`, `navigation-plugin`, and `hash-plugin` working\n * in Electron (`file://`, `app://`), Tauri (`tauri://`, `https://`), and any\n * other webview that may ship with non-HTTP origins. See issue #496.\n */\nexport function safeParseUrl(url: string): ParsedUrl {\n let rest = url;\n\n const schemeIdx = rest.indexOf(\"://\");\n\n if (schemeIdx !== -1) {\n const authorityStart = schemeIdx + 3;\n let pathStart = rest.length;\n\n for (let i = authorityStart; i < rest.length; i++) {\n const ch = rest[i];\n\n if (ch === \"/\" || ch === \"?\" || ch === \"#\") {\n pathStart = i;\n\n break;\n }\n }\n\n rest = pathStart === rest.length ? \"/\" : rest.slice(pathStart);\n\n if (rest.startsWith(\"?\") || rest.startsWith(\"#\")) {\n rest = `/${rest}`;\n }\n }\n\n const hashIdx = rest.indexOf(\"#\");\n const hash = hashIdx === -1 ? \"\" : rest.slice(hashIdx);\n const beforeHash = hashIdx === -1 ? rest : rest.slice(0, hashIdx);\n\n const queryIdx = beforeHash.indexOf(\"?\");\n const search = queryIdx === -1 ? \"\" : beforeHash.slice(queryIdx);\n const pathname = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx);\n\n return { pathname, search, hash };\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(url: string, base: string): string {\n const parsedUrl = safeParseUrl(url);\n\n return extractPath(parsedUrl.pathname, base) + parsedUrl.search;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} kept for call-site readability — history-query\n * paths (Navigation API entries, etc.) are absolute URLs by contract.\n */\nexport function extractPathFromAbsoluteUrl(url: string, base: string): string {\n return urlToPath(url, base);\n}\n","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, options);\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n };\n}\n","import { extractPathFromAbsoluteUrl } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n return (\n api.matchPath(extractPathFromAbsoluteUrl(entry.url, base)) ?? undefined\n );\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n let count = 0;\n\n for (const entry of browser.entries()) {\n if (entryToState(entry, api, base)?.name === routeName) {\n count++;\n }\n }\n\n return count;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { extractPath, safeParseUrl } from \"./browser-env\";\n\nimport type {\n NavigationBrowser,\n NavigationDirection,\n NavigationMeta,\n} from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setCapturedMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function computeDirection(\n navigationType: NavigationMeta[\"navigationType\"],\n destinationIndex: number,\n currentIndex: number,\n): NavigationDirection {\n if (navigationType === \"traverse\") {\n if (destinationIndex === currentIndex) {\n return \"unknown\";\n }\n\n return destinationIndex > currentIndex ? \"forward\" : \"back\";\n }\n\n return navigationType === \"push\" ? \"forward\" : \"unknown\";\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept || !router.isActive()) {\n return;\n }\n\n if (isSyncingFromRouter()) {\n // Plugin-originated navigate event after its own successful transition\n // (onTransitionSuccess calls browser.navigate to sync URL). We must still\n // intercept — a bare `return` leaves the event un-intercepted, and\n // Chromium falls back to a cross-document navigation (full page reload).\n // The noop handler cancels the fallback without running router logic;\n // state is already committed.\n event.intercept({\n handler: async () => {},\n });\n\n return;\n }\n\n const destinationUrl = safeParseUrl(event.destination.url);\n const path =\n extractPath(destinationUrl.pathname, base) + destinationUrl.search;\n const matchedState = api.matchPath(path);\n\n const navType = event.navigationType as NavigationMeta[\"navigationType\"];\n const currentIndex = browser.currentEntry?.index ?? -1;\n\n deps.setCapturedMeta({\n navigationType: navType,\n userInitiated: event.userInitiated,\n info: event.info,\n direction: computeDirection(\n navType,\n event.destination.index,\n currentIndex,\n ),\n sourceElement: event.sourceElement ?? null,\n });\n\n const withRecovery = async (run: () => Promise<unknown>): Promise<void> => {\n try {\n await run();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n };\n\n if (matchedState) {\n event.intercept({\n handler: () =>\n withRecovery(() =>\n router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n }),\n ),\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and reject the\n // intercept so the Navigation API auto-rolls back the URL. No silent\n // fallback to defaultRoute.\n event.intercept({\n // eslint-disable-next-line @typescript-eslint/require-await -- Navigation API requires async handler; synchronous throw is the rollback signal\n handler: async () => {\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, { path });\n\n api.emitTransitionError(err);\n\n throw err;\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n\n try {\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n } finally {\n setSyncing(false);\n }\n }\n } catch (recoveryError) {\n console.error(\n \"[navigation-plugin] Failed to recover from critical error\",\n recoveryError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params) + browser.getHash();\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n\n try {\n browser.replaceState(historyState, url);\n } finally {\n setSyncing(false);\n }\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport {\n shouldReplaceHistory,\n buildUrl,\n extractPathFromAbsoluteUrl,\n urlToPath,\n} from \"./browser-env\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: NavigationMeta) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n #capturedMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#claim = api.claimContextNamespace(\"navigation\");\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) =>\n api.matchPath(urlToPath(url, options.base)) ?? undefined,\n replaceHistoryState: createReplaceHistoryState(\n api,\n router,\n browser,\n pluginBuildUrl,\n (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setCapturedMeta: (meta) => {\n this.#capturedMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n releaseClaim: () => {\n this.#claim.release();\n },\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const entry = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const path = extractPathFromAbsoluteUrl(entry.url, this.#options.base);\n const matchedState = this.#api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n /* v8 ignore next -- @preserve: currentEntry always exists when traverseToLast is callable (after start) */\n const currentIndex = this.#browser.currentEntry?.index ?? -1;\n\n this.#capturedMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n direction: entry.index > currentIndex ? \"forward\" : \"back\",\n sourceElement: null,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionStart: (toState: State) => {\n if (this.#capturedMeta) {\n this.#claim.write(toState, this.#capturedMeta);\n }\n },\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#capturedMeta) {\n const navigationType = deriveNavigationType(\n navOptions,\n toState,\n fromState,\n );\n\n this.#capturedMeta = {\n navigationType,\n userInitiated: false,\n direction: navigationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n const { navigationType } = this.#capturedMeta;\n\n this.#claim.write(toState, Object.freeze(this.#capturedMeta));\n this.#capturedMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n try {\n if (this.#pendingTraverseKey) {\n this.#browser.traverseTo(this.#pendingTraverseKey);\n this.#pendingTraverseKey = undefined;\n } else {\n const url = buildUrl(toState.path, this.#options.base);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const hash = shouldPreserveHash ? this.#browser.getHash() : \"\";\n const finalUrl = hash ? url + hash : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = navigationType !== \"push\";\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n } finally {\n this.#isSyncingFromRouter = false;\n }\n },\n\n onTransitionCancel: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n releaseClaim: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n deps.releaseClaim();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n };\n};\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle","#capturedMeta","#isSyncingFromRouter","#pendingTraverseKey"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path to canonical form: leading slash, no trailing slash,\n * no repeated slashes. Isolated \"/\" collapses to \"\".\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"//app//\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n * normalizeBase(\"/\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base.replaceAll(/\\/+/g, \"/\");\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.length > 1 && result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result === \"/\" ? \"\" : result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export interface OptionRule<T> {\n validate: (value: T) => string | null;\n}\n\nexport type OptionRules<T extends object> = {\n [K in keyof T]?: OptionRule<NonNullable<T[K]>>;\n};\n\nexport function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\n rules?: OptionRules<T>,\n): (opts: Partial<T> | undefined) => void {\n return (opts) => {\n if (!opts) {\n return;\n }\n\n for (const key of Object.keys(opts)) {\n if (!(key in defaults)) {\n continue;\n }\n\n const value = opts[key as keyof typeof opts];\n\n if (value === undefined) {\n continue;\n }\n\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n\n const rule = rules?.[key as keyof T];\n\n if (rule) {\n const msg = (rule.validate as (input: unknown) => string | null)(value);\n\n if (msg !== null) {\n throw new Error(`[${loggerContext}] Invalid '${key}': ${msg}`);\n }\n }\n }\n };\n}\n\n// eslint-disable-next-line no-control-regex -- control characters are exactly what this rule rejects\nconst CONTROL_CHARS = /[\\u0000-\\u001F\\u007F]/;\n\nexport const safeBaseRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.split(\"/\").includes(\"..\")) {\n return \"must not contain '..' segments\";\n }\n\n return null;\n },\n};\n\nexport const safeHashPrefixRule: OptionRule<string> = {\n validate: (value) => {\n if (CONTROL_CHARS.test(value)) {\n return \"must not contain control characters\";\n }\n\n if (value.includes(\"/\")) {\n return \"must not contain '/' (slash is added before the path automatically)\";\n }\n\n if (value.includes(\"#\")) {\n return \"must not contain '#' (it is added as the hash delimiter)\";\n }\n\n if (value.includes(\"?\")) {\n return \"must not contain '?' (it conflicts with the query delimiter)\";\n }\n\n return null;\n },\n};\n\nexport const nonNegativeIntegerRule: OptionRule<number> = {\n validate: (value) => {\n if (!Number.isFinite(value)) {\n return `expected finite number, got ${String(value)}`;\n }\n\n if (!Number.isInteger(value)) {\n return `expected integer, got ${String(value)}`;\n }\n\n if (value < 0) {\n return `expected non-negative integer, got ${value}`;\n }\n\n return null;\n },\n};\n","import { createUpdateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n const updateState = createUpdateBrowserState();\n\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n if (navOptions.replace === true) {\n return true;\n }\n\n if (!fromState) {\n return navOptions.replace !== false;\n }\n\n return !!navOptions.reload && toState.path === fromState.path;\n}\n","export interface ParsedUrl {\n pathname: string;\n search: string;\n hash: string;\n}\n\n/**\n * Scheme-agnostic URL parser.\n *\n * Extracts `pathname`, `search`, and `hash` from any string — absolute\n * (`scheme://authority/path?q#h`), path-relative (`/path?q#h`), or opaque\n * (`data:...`, `javascript:...`). Never throws, never returns null.\n *\n * Routing does not care about scheme or authority, only about the path part.\n * This keeps `browser-plugin`, `navigation-plugin`, and `hash-plugin` working\n * in Electron (`file://`, `app://`), Tauri (`tauri://`, `https://`), and any\n * other webview that may ship with non-HTTP origins. See issue #496.\n */\nexport function safeParseUrl(url: string): ParsedUrl {\n let rest = url;\n\n const schemeIdx = rest.indexOf(\"://\");\n\n if (schemeIdx !== -1) {\n const authorityStart = schemeIdx + 3;\n let pathStart = rest.length;\n\n for (let i = authorityStart; i < rest.length; i++) {\n const ch = rest[i];\n\n if (ch === \"/\" || ch === \"?\" || ch === \"#\") {\n pathStart = i;\n\n break;\n }\n }\n\n rest = pathStart === rest.length ? \"/\" : rest.slice(pathStart);\n\n if (rest.startsWith(\"?\") || rest.startsWith(\"#\")) {\n rest = `/${rest}`;\n }\n }\n\n const hashIdx = rest.indexOf(\"#\");\n const hash = hashIdx === -1 ? \"\" : rest.slice(hashIdx);\n const beforeHash = hashIdx === -1 ? rest : rest.slice(0, hashIdx);\n\n const queryIdx = beforeHash.indexOf(\"?\");\n const search = queryIdx === -1 ? \"\" : beforeHash.slice(queryIdx);\n const pathname = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx);\n\n return { pathname, search, hash };\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n // Path \"/\" with a non-empty base would otherwise produce `\"${base}/\"` —\n // a trailing-slash URL (e.g. `/app/`). The canonical form of the base\n // (normalizeBase strips trailing slash) is `/app`, and the router's\n // `extractPath(\"/app\", \"/app\")` round-trips to `\"/\"` regardless. Collapse\n // the index case to the canonical base to keep URLs symmetric.\n if (path === \"/\") {\n return base;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(url: string, base: string): string {\n const parsedUrl = safeParseUrl(url);\n\n return extractPath(parsedUrl.pathname, base) + parsedUrl.search;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} kept for call-site readability — history-query\n * paths (Navigation API entries, etc.) are absolute URLs by contract.\n */\nexport function extractPathFromAbsoluteUrl(url: string, base: string): string {\n return urlToPath(url, base);\n}\n","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n // Default `false` respects `canDeactivate` guards on browser back/forward,\n // matching the documented contract of `browser-plugin` and the core router.\n // Apps that want the browser's native history buttons to bypass guards\n // (e.g. to avoid dead-end UX) can opt in via `forceDeactivate: true`.\n forceDeactivate: false,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, options);\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n\n getActivationType: () => nav.activation?.navigationType,\n };\n}\n","import { extractPathFromAbsoluteUrl } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Validates a candidate history entry for `traverseToLast(routeName)` and\n * returns both the entry (now known non-null) and the matched router state.\n * Extracted from `NavigationPlugin` so the three error branches (missing\n * entry, null url, unmatched url) can be tested directly without vi.spyOn\n * on module namespaces — the star-import spy pattern is fragile under ESM\n * and was working by accident in history-extensions.test.ts.\n *\n * Throws a descriptive Error on any failure; the caller (NavigationPlugin)\n * propagates it as the rejection of `traverseToLast`.\n */\nexport function resolveEntryToMatchedState(\n entry: NavigationHistoryEntry | undefined,\n routeName: string,\n api: PluginApi,\n base: string,\n): { entry: NavigationHistoryEntry; matchedState: State } {\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const path = extractPathFromAbsoluteUrl(entry.url, base);\n const matchedState = api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n return { entry, matchedState };\n}\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n return (\n api.matchPath(extractPathFromAbsoluteUrl(entry.url, base)) ?? undefined\n );\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n let count = 0;\n\n for (const entry of browser.entries()) {\n if (entryToState(entry, api, base)?.name === routeName) {\n count++;\n }\n }\n\n return count;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { urlToPath } from \"./browser-env\";\n\nimport type {\n NavigationBrowser,\n NavigationDirection,\n NavigationMeta,\n} from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setCapturedMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function computeDirection(\n navigationType: NavigationMeta[\"navigationType\"],\n destinationIndex: number,\n currentIndex: number,\n): NavigationDirection {\n if (navigationType === \"traverse\") {\n if (destinationIndex === currentIndex) {\n return \"unknown\";\n }\n\n return destinationIndex > currentIndex ? \"forward\" : \"back\";\n }\n\n return navigationType === \"push\" ? \"forward\" : \"unknown\";\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept || !router.isActive()) {\n return;\n }\n\n if (isSyncingFromRouter()) {\n // Plugin-originated navigate event after its own successful transition\n // (onTransitionSuccess calls browser.navigate to sync URL). We must still\n // intercept — a bare `return` leaves the event un-intercepted, and\n // Chromium falls back to a cross-document navigation (full page reload).\n // The noop handler cancels the fallback without running router logic;\n // state is already committed.\n event.intercept({\n handler: async () => {},\n });\n\n return;\n }\n\n const path = urlToPath(event.destination.url, base);\n const matchedState = api.matchPath(path);\n\n const navType = event.navigationType as NavigationMeta[\"navigationType\"];\n const currentIndex = browser.currentEntry?.index ?? -1;\n\n deps.setCapturedMeta({\n navigationType: navType,\n userInitiated: event.userInitiated,\n info: event.info,\n direction: computeDirection(\n navType,\n event.destination.index,\n currentIndex,\n ),\n sourceElement: event.sourceElement ?? null,\n });\n\n const withRecovery = async (run: () => Promise<unknown>): Promise<void> => {\n try {\n await run();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n\n return;\n }\n\n // TRANSITION_CANCELLED: a newer navigation aborted this one — the\n // newer navigate event is (or will be) handled by this same plugin,\n // and THAT event is responsible for syncing URL/state. Firing our\n // own sync here races against it: browser.navigate(replace, same-url)\n // would cancel the in-flight newer transition, which is exactly the\n // rapid-fire-events storm failure mode.\n //\n // SAME_STATES: router refused because router.getState() already equals\n // the target. URL and router state are already consistent — no sync\n // needed.\n if (\n error.code === errorCodes.TRANSITION_CANCELLED ||\n error.code === errorCodes.SAME_STATES\n ) {\n return;\n }\n\n // Other RouterError codes (CANNOT_DEACTIVATE, CANNOT_ACTIVATE,\n // ROUTE_NOT_FOUND, …) — router rejected the transition, state is\n // unchanged, but URL may have already committed to a different\n // value by the Navigation API. Sync the URL back to the current\n // router state in a single visible transition (headless Chromium\n // and some cross-origin setups leave \"committed-then-reverted\"\n // windows if we relied on the native rollback via intercept reject).\n // Observers that care about the error see it through the router's\n // TRANSITION_ERROR event.\n syncUrlToRouterState(router, browser, setSyncing);\n }\n };\n\n if (matchedState) {\n event.intercept({\n handler: () =>\n withRecovery(() =>\n router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n }),\n ),\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and reject the\n // intercept so the Navigation API auto-rolls back the URL. No silent\n // fallback to defaultRoute.\n event.intercept({\n // eslint-disable-next-line @typescript-eslint/require-await -- Navigation API requires async handler; synchronous throw is the rollback signal\n handler: async () => {\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, { path });\n\n api.emitTransitionError(err);\n\n throw err;\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n syncUrlToRouterState(router, browser, setSyncing);\n}\n\nfunction syncUrlToRouterState(\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n\n try {\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n } finally {\n setSyncing(false);\n }\n }\n } catch (syncError) {\n console.error(\n \"[navigation-plugin] Failed to sync URL to router state\",\n syncError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params) + browser.getHash();\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n\n try {\n browser.replaceState(historyState, url);\n } finally {\n setSyncing(false);\n }\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport { shouldReplaceHistory, buildUrl, urlToPath } from \"./browser-env\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n resolveEntryToMatchedState,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: NavigationMeta) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n #capturedMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#claim = api.claimContextNamespace(\"navigation\");\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n // Cross-document load priming (#531). On F5, browser back/forward across\n // a page boundary, or a fresh URL bar entry, the prior JS context is\n // discarded — the navigate event handler never sees the activation.\n // Without this, deriveNavigationType in onTransitionSuccess falls through\n // to \"replace\" for every initial transition, breaking scroll restore on\n // reload (#497) and any consumer branching on navigationType.\n // navigation.activation reflects the cross-document navigation that\n // activated this document; it stays constant across same-document\n // navigations, so this only affects the FIRST transition.\n const activationType = browser.getActivationType();\n\n if (activationType) {\n this.#capturedMeta = {\n navigationType: activationType,\n userInitiated: false,\n direction: activationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) =>\n api.matchPath(urlToPath(url, options.base)) ?? undefined,\n replaceHistoryState: createReplaceHistoryState(\n api,\n router,\n browser,\n pluginBuildUrl,\n (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setCapturedMeta: (meta) => {\n this.#capturedMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n releaseClaim: () => {\n this.#claim.release();\n },\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const candidate = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n // resolveEntryToMatchedState throws for missing entry, null url, or\n // unmatched url — same three error branches the old inline checks\n // produced. Extracted so the error paths can be unit-tested directly\n // without namespace-level vi.spyOn gymnastics.\n const { entry, matchedState } = resolveEntryToMatchedState(\n candidate,\n routeName,\n this.#api,\n this.#options.base,\n );\n\n const currentEntry = this.#browser.currentEntry;\n\n if (!currentEntry) {\n // Invariant violation: traverseToLast is only callable after\n // router.start(), which guarantees a current entry. A null here means\n // the plugin was stopped mid-call or the browser abstraction is\n // broken — either way, silently picking direction \"forward\" from a\n // fallback `-1` would mask the bug. Fail loudly instead.\n throw new Error(\n `[navigation-plugin] Cannot determine direction for traverseToLast(\"${routeName}\"): browser.currentEntry is null. The plugin must be started before calling traverseToLast.`,\n );\n }\n\n this.#capturedMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n direction: entry.index > currentEntry.index ? \"forward\" : \"back\",\n sourceElement: null,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionStart: (toState: State) => {\n if (this.#capturedMeta) {\n this.#claim.write(toState, this.#capturedMeta);\n }\n },\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#capturedMeta) {\n const navigationType = deriveNavigationType(\n navOptions,\n toState,\n fromState,\n );\n\n this.#capturedMeta = {\n navigationType,\n userInitiated: false,\n direction: navigationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n const frozenMeta = Object.freeze(this.#capturedMeta);\n\n this.#claim.write(toState, frozenMeta);\n this.#capturedMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n // Consume pendingTraverseKey BEFORE calling browser.traverseTo.\n // If traverseTo throws (Navigation API can reject on evicted keys\n // under memory pressure), we must not leave the stale key behind —\n // otherwise the NEXT transition's onTransitionSuccess would see it\n // and replay the traverse against the same already-broken key.\n const traverseKey = this.#pendingTraverseKey;\n\n this.#pendingTraverseKey = undefined;\n\n try {\n if (traverseKey) {\n this.#browser.traverseTo(traverseKey);\n } else {\n const url = buildUrl(toState.path, this.#options.base);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const hash = shouldPreserveHash ? this.#browser.getHash() : \"\";\n const finalUrl = hash ? url + hash : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = frozenMeta.navigationType !== \"push\";\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n } finally {\n this.#isSyncingFromRouter = false;\n }\n },\n\n onTransitionCancel: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n releaseClaim: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n deps.releaseClaim();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n getActivationType: () => undefined,\n };\n};\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/navigation-plugin",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "type": "commonjs",
5
5
  "description": "Navigation API integration plugin for browser URL synchronization",
6
6
  "main": "./dist/cjs/index.js",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "sideEffects": false,
46
46
  "dependencies": {
47
- "@real-router/core": "^0.50.0",
47
+ "@real-router/core": "^0.50.2",
48
48
  "@real-router/types": "^0.34.1"
49
49
  },
50
50
  "devDependencies": {
package/src/constants.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import type { NavigationPluginOptions } from "./types";
2
2
 
3
3
  export const defaultOptions: Required<NavigationPluginOptions> = {
4
- forceDeactivate: true,
4
+ // Default `false` respects `canDeactivate` guards on browser back/forward,
5
+ // matching the documented contract of `browser-plugin` and the core router.
6
+ // Apps that want the browser's native history buttons to bypass guards
7
+ // (e.g. to avoid dead-end UX) can opt in via `forceDeactivate: true`.
8
+ forceDeactivate: false,
5
9
  base: "",
6
10
  };
7
11
 
@@ -4,6 +4,41 @@ import type { NavigationBrowser } from "./types";
4
4
  import type { State } from "@real-router/core";
5
5
  import type { PluginApi } from "@real-router/core/api";
6
6
 
7
+ /**
8
+ * Validates a candidate history entry for `traverseToLast(routeName)` and
9
+ * returns both the entry (now known non-null) and the matched router state.
10
+ * Extracted from `NavigationPlugin` so the three error branches (missing
11
+ * entry, null url, unmatched url) can be tested directly without vi.spyOn
12
+ * on module namespaces — the star-import spy pattern is fragile under ESM
13
+ * and was working by accident in history-extensions.test.ts.
14
+ *
15
+ * Throws a descriptive Error on any failure; the caller (NavigationPlugin)
16
+ * propagates it as the rejection of `traverseToLast`.
17
+ */
18
+ export function resolveEntryToMatchedState(
19
+ entry: NavigationHistoryEntry | undefined,
20
+ routeName: string,
21
+ api: PluginApi,
22
+ base: string,
23
+ ): { entry: NavigationHistoryEntry; matchedState: State } {
24
+ if (!entry) {
25
+ throw new Error(`No history entry for route "${routeName}"`);
26
+ }
27
+
28
+ if (!entry.url) {
29
+ throw new Error(`No matching route for entry URL "${entry.url}"`);
30
+ }
31
+
32
+ const path = extractPathFromAbsoluteUrl(entry.url, base);
33
+ const matchedState = api.matchPath(path);
34
+
35
+ if (!matchedState) {
36
+ throw new Error(`No matching route for entry URL "${entry.url}"`);
37
+ }
38
+
39
+ return { entry, matchedState };
40
+ }
41
+
7
42
  /**
8
43
  * Converts a NavigationHistoryEntry to a State via URL matching.
9
44
  * Uses URL matching (not entry.getState()) because:
@@ -1,6 +1,6 @@
1
1
  import { errorCodes, RouterError } from "@real-router/core";
2
2
 
3
- import { extractPath, safeParseUrl } from "./browser-env";
3
+ import { urlToPath } from "./browser-env";
4
4
 
5
5
  import type {
6
6
  NavigationBrowser,
@@ -72,9 +72,7 @@ export function createNavigateHandler(deps: NavigateHandlerDeps) {
72
72
  return;
73
73
  }
74
74
 
75
- const destinationUrl = safeParseUrl(event.destination.url);
76
- const path =
77
- extractPath(destinationUrl.pathname, base) + destinationUrl.search;
75
+ const path = urlToPath(event.destination.url, base);
78
76
  const matchedState = api.matchPath(path);
79
77
 
80
78
  const navType = event.navigationType as NavigationMeta["navigationType"];
@@ -98,7 +96,37 @@ export function createNavigateHandler(deps: NavigateHandlerDeps) {
98
96
  } catch (error) {
99
97
  if (!(error instanceof RouterError)) {
100
98
  recoverFromNavigateError(error, router, browser, setSyncing);
99
+
100
+ return;
101
+ }
102
+
103
+ // TRANSITION_CANCELLED: a newer navigation aborted this one — the
104
+ // newer navigate event is (or will be) handled by this same plugin,
105
+ // and THAT event is responsible for syncing URL/state. Firing our
106
+ // own sync here races against it: browser.navigate(replace, same-url)
107
+ // would cancel the in-flight newer transition, which is exactly the
108
+ // rapid-fire-events storm failure mode.
109
+ //
110
+ // SAME_STATES: router refused because router.getState() already equals
111
+ // the target. URL and router state are already consistent — no sync
112
+ // needed.
113
+ if (
114
+ error.code === errorCodes.TRANSITION_CANCELLED ||
115
+ error.code === errorCodes.SAME_STATES
116
+ ) {
117
+ return;
101
118
  }
119
+
120
+ // Other RouterError codes (CANNOT_DEACTIVATE, CANNOT_ACTIVATE,
121
+ // ROUTE_NOT_FOUND, …) — router rejected the transition, state is
122
+ // unchanged, but URL may have already committed to a different
123
+ // value by the Navigation API. Sync the URL back to the current
124
+ // router state in a single visible transition (headless Chromium
125
+ // and some cross-origin setups leave "committed-then-reverted"
126
+ // windows if we relied on the native rollback via intercept reject).
127
+ // Observers that care about the error see it through the router's
128
+ // TRANSITION_ERROR event.
129
+ syncUrlToRouterState(router, browser, setSyncing);
102
130
  }
103
131
  };
104
132
 
@@ -147,6 +175,14 @@ function recoverFromNavigateError(
147
175
  error,
148
176
  );
149
177
 
178
+ syncUrlToRouterState(router, browser, setSyncing);
179
+ }
180
+
181
+ function syncUrlToRouterState(
182
+ router: Router,
183
+ browser: NavigationBrowser,
184
+ setSyncing: (value: boolean) => void,
185
+ ): void {
150
186
  try {
151
187
  const currentState = router.getState();
152
188
 
@@ -168,10 +204,10 @@ function recoverFromNavigateError(
168
204
  setSyncing(false);
169
205
  }
170
206
  }
171
- } catch (recoveryError) {
207
+ } catch (syncError) {
172
208
  console.error(
173
- "[navigation-plugin] Failed to recover from critical error",
174
- recoveryError,
209
+ "[navigation-plugin] Failed to sync URL to router state",
210
+ syncError,
175
211
  );
176
212
  }
177
213
  }
@@ -48,5 +48,7 @@ export function createNavigationBrowser(base: string): NavigationBrowser {
48
48
  get currentEntry() {
49
49
  return nav.currentEntry;
50
50
  },
51
+
52
+ getActivationType: () => nav.activation?.navigationType,
51
53
  };
52
54
  }
package/src/plugin.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  import { UNKNOWN_ROUTE } from "@real-router/core";
2
2
 
3
- import {
4
- shouldReplaceHistory,
5
- buildUrl,
6
- extractPathFromAbsoluteUrl,
7
- urlToPath,
8
- } from "./browser-env";
3
+ import { shouldReplaceHistory, buildUrl, urlToPath } from "./browser-env";
9
4
  import {
10
5
  peekBack,
11
6
  peekForward,
@@ -13,6 +8,7 @@ import {
13
8
  getVisitedRoutes,
14
9
  getRouteVisitCount,
15
10
  findLastEntryForRoute,
11
+ resolveEntryToMatchedState,
16
12
  canGoBack,
17
13
  canGoForward,
18
14
  canGoBackTo,
@@ -91,6 +87,26 @@ export class NavigationPlugin {
91
87
  this.#claim = api.claimContextNamespace("navigation");
92
88
  this.#removeStartInterceptor = createStartInterceptor(api, browser);
93
89
 
90
+ // Cross-document load priming (#531). On F5, browser back/forward across
91
+ // a page boundary, or a fresh URL bar entry, the prior JS context is
92
+ // discarded — the navigate event handler never sees the activation.
93
+ // Without this, deriveNavigationType in onTransitionSuccess falls through
94
+ // to "replace" for every initial transition, breaking scroll restore on
95
+ // reload (#497) and any consumer branching on navigationType.
96
+ // navigation.activation reflects the cross-document navigation that
97
+ // activated this document; it stays constant across same-document
98
+ // navigations, so this only affects the FIRST transition.
99
+ const activationType = browser.getActivationType();
100
+
101
+ if (activationType) {
102
+ this.#capturedMeta = {
103
+ navigationType: activationType,
104
+ userInitiated: false,
105
+ direction: activationType === "push" ? "forward" : "unknown",
106
+ sourceElement: null,
107
+ };
108
+ }
109
+
94
110
  const pluginBuildUrl = (route: string, params?: Params) => {
95
111
  const path = router.buildPath(route, params);
96
112
 
@@ -155,7 +171,7 @@ export class NavigationPlugin {
155
171
  async traverseToLast(routeName: string): Promise<State> {
156
172
  const entries = this.#browser.entries();
157
173
  const currentKey = this.#browser.currentEntry?.key;
158
- const entry = findLastEntryForRoute(
174
+ const candidate = findLastEntryForRoute(
159
175
  entries,
160
176
  routeName,
161
177
  this.#api,
@@ -163,28 +179,34 @@ export class NavigationPlugin {
163
179
  currentKey,
164
180
  );
165
181
 
166
- if (!entry) {
167
- throw new Error(`No history entry for route "${routeName}"`);
168
- }
169
-
170
- if (!entry.url) {
171
- throw new Error(`No matching route for entry URL "${entry.url}"`);
172
- }
182
+ // resolveEntryToMatchedState throws for missing entry, null url, or
183
+ // unmatched url same three error branches the old inline checks
184
+ // produced. Extracted so the error paths can be unit-tested directly
185
+ // without namespace-level vi.spyOn gymnastics.
186
+ const { entry, matchedState } = resolveEntryToMatchedState(
187
+ candidate,
188
+ routeName,
189
+ this.#api,
190
+ this.#options.base,
191
+ );
173
192
 
174
- const path = extractPathFromAbsoluteUrl(entry.url, this.#options.base);
175
- const matchedState = this.#api.matchPath(path);
193
+ const currentEntry = this.#browser.currentEntry;
176
194
 
177
- if (!matchedState) {
178
- throw new Error(`No matching route for entry URL "${entry.url}"`);
195
+ if (!currentEntry) {
196
+ // Invariant violation: traverseToLast is only callable after
197
+ // router.start(), which guarantees a current entry. A null here means
198
+ // the plugin was stopped mid-call or the browser abstraction is
199
+ // broken — either way, silently picking direction "forward" from a
200
+ // fallback `-1` would mask the bug. Fail loudly instead.
201
+ throw new Error(
202
+ `[navigation-plugin] Cannot determine direction for traverseToLast("${routeName}"): browser.currentEntry is null. The plugin must be started before calling traverseToLast.`,
203
+ );
179
204
  }
180
205
 
181
- /* v8 ignore next -- @preserve: currentEntry always exists when traverseToLast is callable (after start) */
182
- const currentIndex = this.#browser.currentEntry?.index ?? -1;
183
-
184
206
  this.#capturedMeta = {
185
207
  navigationType: "traverse",
186
208
  userInitiated: false,
187
- direction: entry.index > currentIndex ? "forward" : "back",
209
+ direction: entry.index > currentEntry.index ? "forward" : "back",
188
210
  sourceElement: null,
189
211
  };
190
212
  this.#pendingTraverseKey = entry.key;
@@ -222,17 +244,25 @@ export class NavigationPlugin {
222
244
  };
223
245
  }
224
246
 
225
- const { navigationType } = this.#capturedMeta;
247
+ const frozenMeta = Object.freeze(this.#capturedMeta);
226
248
 
227
- this.#claim.write(toState, Object.freeze(this.#capturedMeta));
249
+ this.#claim.write(toState, frozenMeta);
228
250
  this.#capturedMeta = undefined;
229
251
 
230
252
  this.#isSyncingFromRouter = true;
231
253
 
254
+ // Consume pendingTraverseKey BEFORE calling browser.traverseTo.
255
+ // If traverseTo throws (Navigation API can reject on evicted keys
256
+ // under memory pressure), we must not leave the stale key behind —
257
+ // otherwise the NEXT transition's onTransitionSuccess would see it
258
+ // and replay the traverse against the same already-broken key.
259
+ const traverseKey = this.#pendingTraverseKey;
260
+
261
+ this.#pendingTraverseKey = undefined;
262
+
232
263
  try {
233
- if (this.#pendingTraverseKey) {
234
- this.#browser.traverseTo(this.#pendingTraverseKey);
235
- this.#pendingTraverseKey = undefined;
264
+ if (traverseKey) {
265
+ this.#browser.traverseTo(traverseKey);
236
266
  } else {
237
267
  const url = buildUrl(toState.path, this.#options.base);
238
268
  const shouldPreserveHash =
@@ -248,7 +278,7 @@ export class NavigationPlugin {
248
278
  if (toState.name === UNKNOWN_ROUTE) {
249
279
  this.#browser.updateCurrentEntry({ state: historyState });
250
280
  } else {
251
- const replace = navigationType !== "push";
281
+ const replace = frozenMeta.navigationType !== "push";
252
282
 
253
283
  this.#browser.navigate(finalUrl, {
254
284
  state: historyState,
@@ -43,5 +43,6 @@ export const createNavigationFallbackBrowser = (
43
43
  return [];
44
44
  },
45
45
  currentEntry: null,
46
+ getActivationType: () => undefined,
46
47
  };
47
48
  };
package/src/types.ts CHANGED
@@ -35,6 +35,12 @@ export interface NavigationBrowser {
35
35
  addNavigateListener: (fn: (evt: NavigateEvent) => void) => () => void;
36
36
  entries: () => NavigationHistoryEntry[];
37
37
  currentEntry: NavigationHistoryEntry | null;
38
+ /**
39
+ * Type of the cross-document navigation that activated this document.
40
+ * Reads `navigation.activation.navigationType` (Baseline 2026 — Chrome 123+, Firefox 147+, Safari 26.2+).
41
+ * Returns `undefined` when activation is unavailable (older browsers, SSR).
42
+ */
43
+ getActivationType: () => NavigationMeta["navigationType"] | undefined;
38
44
  }
39
45
 
40
46
  /**