@real-router/browser-plugin 0.17.5 → 0.17.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":["NavigationOptions","Params","State","isNavigationOptions","value","isRouteName","name","isState","P","isStateStrict","isString","isBoolean","isObjKey","T","Extract","key","obj","isPrimitiveValue","isParams","isParamsStrict","validateRouteName","methodName","validateState","state","method","getTypeDescription"],"sources":["../../src/types.ts","../../../../shared/browser-env/types.ts","../../../../shared/browser-env/url-context.ts","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;;AAAZ;;;;AAAyB;AAWzB;;;KAAY,gBAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;EACR,SAAA,EAAW,gBAAgB;AAAA;AAAA,UAGZ,oBAAA;EAJf;;;;;EAUA,eAAA;EANe;;;;AAaX;EAAJ,IAAI;AAAA;;;UClCW,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAa;EAC7C,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAc;EAC7C,WAAW;AAAA;;;;;;;;ADLb;;;;AAAyB;UEQR,UAAA;EFGW;EED1B,IAAA;EFC0B;EEC1B,WAAW;AAAA;;;iBCiCG,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,GACf,OAAA,GAAU,OAAA,GACT,aAAA;;;;;;;;;;;;;;;iBC8BcS,aAAAA,WAAwBR,QAAAA,GAASA,QAAAA,CAAAA,CAAQG,KAAAA,YAAiBA,KAAAA,IAASF,OAAAA,CAAMM,CAAAA;AAAAA;;;AAbL;;;;;;AJjErF;;;;AAAA;EAAA,UKsBY,YAAA;IACR,OAAA,GARmD,cAAA;ILJ3B;;AAAA;AAE5B;;IKgBI,GAAA,GAN0C,UAMF;EAAA;EAAA,UAGhC,iBAAA;ILlBF;IKoBN,MAAA;ILnBS;;AAAgB;AAG7B;IKqBI,IAAA;ILrBiC;;AAa/B;;IKaF,UAAA;EAAA;AAAA;AAAA;EAAA,UAKQ,MAAA;IJjDmC;;;;IIsD3C,QAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IJzDhB;;;;IIgEE,QAAA,CAAS,GAAA,WAAc,KAAA;IJ/DlB;AAAA;AAGT;;IIkEI,mBAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IAGd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":["value","NavigationOptions","name","P","Params","State","T","key","obj","Extract","methodName","state","method"],"sources":["../../src/types.ts","../../../../shared/browser-env/types.ts","../../../../shared/browser-env/url-context.ts","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;;AAAZ;;;;AAAyB;AAWzB;;;KAAY,gBAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;EACR,SAAA,EAAW,gBAAgB;AAAA;AAAA,UAGZ,oBAAA;EAJf;;;;;EAUA,eAAA;EANe;;;;AAaX;EAAJ,IAAI;AAAA;;;UClCW,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAa;EAC7C,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAc;EAC7C,WAAW;AAAA;;;;;;;;ADLb;;;;AAAyB;UEQR,UAAA;EFGW;EED1B,IAAA;EFC0B;EEC1B,WAAW;AAAA;;;iBCiCG,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,GACf,OAAA,GAAU,OAAA,GACT,aAAA;;;;;;;;;;;;;;;iBC8Bc,aAAA,WAAwB,QAAA,GAAS,QAAA,EAAQA,KAAAA,YAAiBA,KAAAA,IAAS,OAAA,CAAM,CAAA;AAAA;;;AAbL;;;;;;AJjErF;;;;AAAA;EAAA,UKsBY,YAAA;IACR,OAAA,GARmD,cAAA;ILJ3B;;AAAA;AAE5B;;IKgBI,GAAA,GAN0C,UAMF;EAAA;EAAA,UAGhC,iBAAA;ILlBF;IKoBN,MAAA;ILnBS;;AAAgB;AAG7B;IKqBI,IAAA;ILrBiC;;AAa/B;;IKaF,UAAA;EAAA;AAAA;AAAA;EAAA,UAKQ,MAAA;IJjDmC;;;;IIsD3C,QAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IJzDhB;;;;IIgEE,QAAA,CAAS,GAAA,WAAc,KAAA;IJ/DlB;AAAA;AAGT;;IIkEI,mBAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IAGd,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,r=(e,t)=>{globalThis.history.pushState(e,``,t)},i=(e,t)=>{globalThis.history.replaceState(e,``,t)},a=e=>(globalThis.addEventListener(`popstate`,e),()=>{globalThis.removeEventListener(`popstate`,e)}),o=()=>globalThis.location.hash;function s(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 c=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},l=()=>{},u=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)}},d=e=>{let t=u(e);return{pushState:()=>{t(`pushState`)},replaceState:()=>{t(`replaceState`)},addPopstateListener:()=>(t(`addPopstateListener`),l),getHash:()=>(t(`getHash`),``)}},f=/^[A-Z_a-z][\w-]*(?:\.[A-Z_a-z][\w-]*)*$/;function p(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:f.test(e):!1}function m(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>m(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>m(e,t))}return!1}function h(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function g(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!h(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?m(e):!0}function _(e){return p(e.name)&&typeof e.path==`string`&&g(e.params)}function v(e){return!(typeof e!=`object`||!e||!_(e))}function y(e,t,n){return v(e.state)?t.makeState(e.state.name,e.state.params,e.state.path):t.matchPath(n.getLocation())}function b(){let e={name:``,params:{},path:``};return(t,n,r,i)=>{e.name=t.name,e.params=t.params,e.path=t.path,r?i.replaceState(e,n):i.pushState(e,n)}}function x(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 w(e,t){if(n())return{pushState:r,replaceState:i,addPopstateListener:a,getLocation:e,getHash:o};let s=u(t);return{...d(t),getLocation:()=>(s(`getLocation`),``)}}function T(e,t){if(!e.getCurrentHash)return{};let n=e.getCurrentHash();return n!==(e.getCurrentContextHash?e.getCurrentContextHash():``)&&e.router.getState()?.path===t?{hash:n,force:!0,hashChange:!0}:{hash:n}}function E(e){let n=!1,r=null;function i(){if(r){let t=r;r=null,console.warn(`[${e.loggerContext}] Processing deferred popstate event`),s(t)}}function a(){let t=e.router.getState();if(!t)return;let n=t.context?.url?.hash,r=e.buildUrl(t.name,t.params,n?{hash:n}:void 0);e.browser.replaceState(t,r)}function o(t){console.error(`[${e.loggerContext}] Critical error in onPopState`,t);try{a()}catch(t){console.error(`[${e.loggerContext}] Failed to recover from critical error`,t)}}async function s(s){if(n){console.warn(`[${e.loggerContext}] Transition in progress, deferring popstate event`),r=s;return}n=!0;try{let n=y(s,e.api,e.browser);if(n)await e.api.navigateToState(n,{...e.transitionOptions,...T(e,n.path)});else if(e.allowNotFound)e.router.navigateToNotFound(e.browser.getLocation());else{let n=new t.RouterError(t.errorCodes.ROUTE_NOT_FOUND,{path:e.browser.getLocation()});e.api.emitTransitionError(n),a()}}catch(e){if(e instanceof t.RouterError)try{a()}catch{}else o(e)}finally{n=!1,i()}}return e=>void s(e)}function D(e){return{onStart:()=>{e.shared.removePopStateListener&&e.shared.removePopStateListener(),e.shared.removePopStateListener=e.browser.addPopstateListener(e.handler)},onStop:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0)},teardown:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0),e.cleanup()}}}function O(e){return encodeURI(e).replaceAll(`#`,`%23`)}function k(e){try{return decodeURIComponent(e)}catch{return e}}function A(e){let t=e;for(;t.startsWith(`#`);)t=t.slice(1);return k(t)}function j(e){let t=e.getHash();return t?k(t.startsWith(`#`)?t.slice(1):t):``}function M(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 N(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 P(e,t){return e?t?e===`/`?t:e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function F(e,t){let n=M(e);return N(n.pathname,t)+n.search}function I(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function L(e,t){return(n,r,i)=>{let a=P(e.buildPath(n,r),t);if(i?.hash===void 0)return a;let o=A(i.hash);return o?`${a}#${O(o)}`:a}}function R(e,t,n,r,i=!0){let a={name:``,params:{},path:``};return(o,s={},c)=>{let l=e.buildState(o,s);if(!l)throw Error(`[real-router] Cannot replace state: route "${o}" is not found`);let u=e.makeState(l.name,l.params,t.buildPath(l.name,l.params),{params:l.meta}),d;if(c?.hash!==void 0){let e=A(c.hash);d=e?`#${O(e)}`:``}else d=i?n.getHash():``;let f=r(o,s)+d;a.name=u.name,a.params=u.params,a.path=u.path,n.replaceState(a,f)}}function z(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}const B={forceDeactivate:!0,base:``},V=`popstate`,H=`browser-plugin`,U=x(B,H,{base:C}),W=Object.freeze({source:`popstate`,direction:`back`}),G=Object.freeze({source:`navigate`,direction:`forward`});function K(t,n){U(t);let r={...B,...t};r.base=s(r.base);let i=n??q(r.base),a={forceDeactivate:r.forceDeactivate,source:V,replace:!0},o={removePopStateListener:void 0};return function(t){return J(t,(0,e.getPluginApi)(t),r,i,a,o)}}function q(e){let t=`\0`,n=``,r=``;return w(()=>{let{pathname:i,search:a}=globalThis.location;return i===t&&a===n?r:(t=i,n=a,r=c(N(i,e))+a,r)},`browser-plugin`)}function J(e,t,n,r,i,a){let o=t.claimContextNamespace(`browser`),s=t.claimContextNamespace(`url`),c=b(),l=I(t,r),u=L(e,n.base),d=t.extendRouter({buildUrl:u,matchUrl:e=>t.matchPath(F(e,n.base))??void 0,replaceHistoryState:R(t,e,r,u)});return{...D({browser:r,shared:a,handler:E({router:e,api:t,browser:r,allowNotFound:t.getOptions().allowNotFound,transitionOptions:i,loggerContext:H,buildUrl:u,getCurrentHash:()=>j(r),getCurrentContextHash:()=>(e.getState()?.context)?.url?.hash??``}),cleanup:()=>{l(),d(),o.release(),s.release()}}),onTransitionSuccess:(e,t,i)=>{let a=z(i,e,t),l=j(r),u=(t?.context)?.url?.hash??``,d=i.hash===void 0?l:A(i.hash);s.write(e,Object.freeze({hash:d,hashChanged:i.hashChange??d!==u}));let f=P(e.path,n.base);c(e,d?`${f}#${O(d)}`:f,a,r);let p=i.source===V;o.write(e,p?W:G)}}}exports.browserPluginFactory=K,exports.isState=v;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@real-router/core/api"),t=require("@real-router/core");function n(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 r=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},i=/^[A-Z_a-z][\w-]*(?:\.[A-Z_a-z][\w-]*)*$/;function a(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:i.test(e):!1}function o(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>o(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>o(e,t))}return!1}function s(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function c(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!s(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?o(e):!0}function l(e){return a(e.name)&&typeof e.path==`string`&&c(e.params)}function u(e){return!(typeof e!=`object`||!e||!l(e))}function d(e,t,n){return u(e.state)?t.makeState(e.state.name,e.state.params,e.state.path):t.matchPath(n.getLocation())}function f(){let e={name:``,params:{},path:``};return(t,n,r,i)=>{e.name=t.name,e.params=t.params,e.path=t.path,r?i.replaceState(e,n):i.pushState(e,n)}}function p(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 m=/[\u0000-\u001F\u007F]/,h={validate:e=>m.test(e)?`must not contain control characters`:e.split(`/`).includes(`..`)?`must not contain '..' segments`:null},g=()=>globalThis.window!==void 0&&!!globalThis.history,_=(e,t)=>{globalThis.history.pushState(e,``,t)},v=(e,t)=>{globalThis.history.replaceState(e,``,t)},y=e=>(globalThis.addEventListener(`popstate`,e),()=>{globalThis.removeEventListener(`popstate`,e)}),b=()=>globalThis.location.hash,x=()=>{},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)}},C=e=>{let t=S(e);return{pushState:()=>{t(`pushState`)},replaceState:()=>{t(`replaceState`)},addPopstateListener:()=>(t(`addPopstateListener`),x),getHash:()=>(t(`getHash`),``)}};function w(e,t){if(g())return{pushState:_,replaceState:v,addPopstateListener:y,getLocation:e,getHash:b};let n=S(t);return{...C(t),getLocation:()=>(n(`getLocation`),``)}}function T(e,t){if(!e.getCurrentHash)return{};let n=e.getCurrentHash();return n!==(e.getCurrentContextHash?e.getCurrentContextHash():``)&&e.router.getState()?.path===t?{hash:n,force:!0,hashChange:!0}:{hash:n}}function E(e){let n=!1,r=null;function i(){if(r){let t=r;r=null,console.warn(`[${e.loggerContext}] Processing deferred popstate event`),s(t)}}function a(){let t=e.router.getState();if(!t)return;let n=t.context?.url?.hash,r=e.buildUrl(t.name,t.params,n?{hash:n}:void 0);e.browser.replaceState(t,r)}function o(t){console.error(`[${e.loggerContext}] Critical error in onPopState`,t);try{a()}catch(t){console.error(`[${e.loggerContext}] Failed to recover from critical error`,t)}}async function s(s){if(n){console.warn(`[${e.loggerContext}] Transition in progress, deferring popstate event`),r=s;return}n=!0;try{let n=d(s,e.api,e.browser);if(n)await e.api.navigateToState(n,{...e.transitionOptions,...T(e,n.path)});else if(e.allowNotFound)e.router.navigateToNotFound(e.browser.getLocation());else{let n=new t.RouterError(t.errorCodes.ROUTE_NOT_FOUND,{path:e.browser.getLocation()});e.api.emitTransitionError(n),a()}}catch(e){if(e instanceof t.RouterError)try{a()}catch{}else o(e)}finally{n=!1,i()}}return e=>void s(e)}function D(e){return{onStart:()=>{e.shared.removePopStateListener&&e.shared.removePopStateListener(),e.shared.removePopStateListener=e.browser.addPopstateListener(e.handler)},onStop:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0)},teardown:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0),e.cleanup()}}}function O(e){return encodeURI(e).replaceAll(`#`,`%23`)}function k(e){try{return decodeURIComponent(e)}catch{return e}}function A(e){let t=e;for(;t.startsWith(`#`);)t=t.slice(1);return k(t)}function j(e){let t=e.getHash();return t?k(t.startsWith(`#`)?t.slice(1):t):``}const M=new Set([`/`,`?`,`#`]);function N(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(M.has(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 F(e,t){return e?t?e===`/`?t:e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function I(e,t){let n=N(e);return P(n.pathname,t)+n.search}function L(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function R(e,t){return(n,r,i)=>{let a=F(e.buildPath(n,r),t);if(i?.hash===void 0)return a;let o=A(i.hash);return o?`${a}#${O(o)}`:a}}function z(e,t,n,r,i=!0){let a={name:``,params:{},path:``};return(o,s={},c)=>{let l=e.buildState(o,s);if(!l)throw Error(`[real-router] Cannot replace state: route "${o}" is not found`);let u=e.makeState(l.name,l.params,t.buildPath(l.name,l.params),{params:l.meta}),d;if(c?.hash!==void 0){let e=A(c.hash);d=e?`#${O(e)}`:``}else d=i?n.getHash():``;let f=r(o,s)+d;a.name=u.name,a.params=u.params,a.path=u.path,n.replaceState(a,f)}}function B(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}const V={forceDeactivate:!0,base:``},H=`popstate`,U=`browser-plugin`,W=p(V,U,{base:h}),G=Object.freeze({source:`popstate`,direction:`back`}),K=Object.freeze({source:`navigate`,direction:`forward`});function q(t,r){W(t);let i={...V,...t};i.base=n(i.base);let a=r??J(i.base),o={forceDeactivate:i.forceDeactivate,source:H,replace:!0},s={removePopStateListener:void 0};return function(t){return Y(t,(0,e.getPluginApi)(t),i,a,o,s)}}function J(e){let t=`\0`,n=``,i=``;return w(()=>{let{pathname:a,search:o}=globalThis.location;return a===t&&o===n?i:(t=a,n=o,i=r(P(a,e))+o,i)},`browser-plugin`)}function Y(e,t,n,r,i,a){let o=t.claimContextNamespace(`browser`),s=t.claimContextNamespace(`url`),c=f(),l=L(t,r),u=R(e,n.base),d=t.extendRouter({buildUrl:u,matchUrl:e=>t.matchPath(I(e,n.base))??void 0,replaceHistoryState:z(t,e,r,u)});return{...D({browser:r,shared:a,handler:E({router:e,api:t,browser:r,allowNotFound:t.getOptions().allowNotFound,transitionOptions:i,loggerContext:U,buildUrl:u,getCurrentHash:()=>j(r),getCurrentContextHash:()=>(e.getState()?.context)?.url?.hash??``}),cleanup:()=>{l(),d(),o.release(),s.release()}}),onTransitionSuccess:(e,t,i)=>{let a=B(i,e,t),l=j(r),u=(t?.context)?.url?.hash??``,d=i.hash===void 0?l:A(i.hash);s.write(e,Object.freeze({hash:d,hashChanged:i.hashChange??d!==u}));let f=F(e.path,n.base);c(e,d?`${f}#${O(d)}`:f,a,r);let p=i.source===H;o.write(e,p?G:K)}}}exports.browserPluginFactory=q,exports.isState=u;
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["isState","RouterError","errorCodes"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/history-api.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../type-guards/dist/esm/index.mjs","../../../../shared/browser-env/popstate-utils.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/safe-browser.ts","../../../../shared/browser-env/popstate-handler.ts","../../../../shared/browser-env/url-context.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../../../shared/browser-env/plugin-utils.ts","../../src/constants.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","import type { HistoryBrowser } from \"./types.js\";\n\nexport const pushState = (state: unknown, path: string): void => {\n globalThis.history.pushState(state, \"\", path);\n};\n\nexport const replaceState = (state: unknown, path: string): void => {\n globalThis.history.replaceState(state, \"\", path);\n};\n\nexport const addPopstateListener: HistoryBrowser[\"addPopstateListener\"] = (\n fn,\n) => {\n globalThis.addEventListener(\"popstate\", fn);\n\n return () => {\n globalThis.removeEventListener(\"popstate\", fn);\n };\n};\n\nexport const getHash = (): string => globalThis.location.hash;\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","const e=[`replace`,`reload`,`force`,`forceDeactivate`,`redirected`];function t(t){if(typeof t!=`object`||!t||Array.isArray(t))return!1;let n=t;for(let t of e){let e=n[t];if(e!==void 0&&typeof e!=`boolean`)return!1}let r=n.signal;return!(r!==void 0&&!(r instanceof AbortSignal))}const n=/\\S/,r=/^[A-Z_a-z][\\w-]*(?:\\.[A-Z_a-z][\\w-]*)*$/;function i(e,t){return TypeError(`[router.${e}] ${t}`)}function a(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:r.test(e):!1}function o(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>o(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>o(e,t))}return!1}function s(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function c(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!s(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?o(e):!0}function l(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):Array.isArray(e)?e.every(e=>{let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}):!1}function u(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let n=e[t];if(!l(n))return!1}return!0}function d(e){return a(e.name)&&typeof e.path==`string`&&c(e.params)}function f(e){return typeof e!=`object`||!e?!1:d(e)}function p(e){return!(typeof e!=`object`||!e||!d(e))}function m(e){return typeof e==`string`}function h(e){return typeof e==`boolean`}function g(e,t){return e in t}function _(e){return typeof e==`number`?Number.isFinite(e):typeof e==`string`||typeof e==`boolean`}function v(e,t){if(typeof e!=`string`)throw i(t,`Route name must be a string, got ${typeof e}`);if(e!==``){if(!n.test(e))throw i(t,`Route name cannot contain only whitespace`);if(e.length>1e4)throw i(t,`Route name exceeds maximum length of 10000 characters. This is a technical safety limit.`);if(!e.startsWith(`@@`)&&!r.test(e))throw i(t,`Invalid route name \"${e}\". Each segment must start with a letter or underscore, followed by letters, numbers, underscores, or hyphens. Segments are separated by dots (e.g., \"users.profile\").`)}}function y(e){return e===null?`null`:Array.isArray(e)?`array[${e.length}]`:typeof e==`object`?`constructor`in e&&e.constructor.name!==`Object`?e.constructor.name:`object`:typeof e}function b(e,t){if(!f(e))throw TypeError(`[${t}] Invalid state structure: ${y(e)}. Expected State object with name, params, and path properties.`)}export{y as getTypeDescription,h as isBoolean,t as isNavigationOptions,g as isObjKey,c as isParams,u as isParamsStrict,_ as isPrimitiveValue,a as isRouteName,f as isState,p as isStateStrict,m as isString,v as validateRouteName,b as validateState};\n//# sourceMappingURL=index.mjs.map","import { isStateStrict as isState } from \"type-guards\";\n\nimport type { Browser } from \"./types.js\";\nimport type { State, Params } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Resolves the popstate event into a navigation-ready `State`.\n *\n * - If `history.state` is a valid router state ({name, params, path} written\n * by browser-plugin/hash-plugin during their previous navigation), it is\n * the source of truth — synthesize a fully-typed `State` from it via\n * `api.makeState`. The synthesized `transition`/`context` fields are\n * placeholders; the navigation pipeline (`completeTransition` and plugin\n * claim writes) replaces them.\n * This branch is mandatory for hash-plugin: `browser.getLocation()`\n * returns the History pathname, not the hash, so the matchPath fallback\n * below cannot extract the hash route.\n * - Otherwise (e.g. manually entered URL with no recorded state), fall\n * back to `api.matchPath(browser.getLocation())`. browser-plugin's\n * `getLocation` returns the URL pathname — this works.\n * - `undefined` when neither path produces a match.\n *\n * Replaces the previous `{ name, params }` shape so the caller can hand\n * the State directly to `router.navigateToState(state, opts)` and skip\n * the redundant `forwardState`/`buildPath` round-trip in\n * `buildNavigateState` (issue #525).\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): State | undefined {\n if (isState(evt.state)) {\n return api.makeState(evt.state.name, evt.state.params, evt.state.path);\n }\n\n return api.matchPath(browser.getLocation());\n}\n\n/**\n * Updates browser state (pushState or replaceState)\n *\n * @param state - Router state\n * @param url - URL to set\n * @param replace - Whether to replace instead of push\n * @param browser - Browser API instance\n */\nexport function updateBrowserState(\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n): void {\n const historyState = {\n name: state.name,\n params: state.params,\n path: state.path,\n };\n\n if (replace) {\n browser.replaceState(historyState, url);\n } else {\n browser.pushState(historyState, url);\n }\n}\n\n/**\n * Creates a `updateBrowserState` closure that reuses a single mutable buffer\n * across calls instead of allocating a fresh `{ name, params, path }` object\n * per push/replace.\n *\n * Why: Browsers structured-clone `history.state` synchronously inside\n * `pushState`/`replaceState`, so the caller never sees the buffer escape —\n * it can be safely overwritten before the next call. Eliminates one\n * allocation per navigation on the hot path.\n *\n * Each plugin instance must own its own buffer (do not share across plugins).\n */\nexport function createUpdateBrowserState(): (\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n) => void {\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (state, url, replace, browser) => {\n buffer.name = state.name;\n buffer.params = state.params;\n buffer.path = state.path;\n\n if (replace) {\n browser.replaceState(buffer, url);\n } else {\n browser.pushState(buffer, url);\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 { isBrowserEnvironment } from \"./detect.js\";\nimport {\n pushState,\n replaceState,\n addPopstateListener,\n getHash,\n} from \"./history-api.js\";\nimport {\n createWarnOnce,\n createHistoryFallbackBrowser,\n} from \"./ssr-fallback.js\";\n\nimport type { Browser } from \"./types.js\";\n\nexport function createSafeBrowser(\n getLocation: () => string,\n context: string,\n): Browser {\n if (isBrowserEnvironment()) {\n return {\n pushState,\n replaceState,\n addPopstateListener,\n getLocation,\n getHash,\n };\n }\n\n const warnOnce = createWarnOnce(context);\n\n return {\n ...createHistoryFallbackBrowser(context),\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"\";\n },\n };\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { getRouteFromEvent } from \"./popstate-utils.js\";\n\nimport type { Browser, SharedFactoryState } from \"./types.js\";\nimport type { Params, Plugin, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Navigation options used by the popstate handler to trigger a\n * router.navigate() call from a back/forward event. `source` identifies\n * the origin of the transition to downstream context consumers;\n * `replace: true` keeps the history stack in sync with the browser.\n */\nexport interface PopstateTransitionOptions {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n}\n\nexport interface PopstateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: Browser;\n allowNotFound: boolean;\n transitionOptions: PopstateTransitionOptions;\n loggerContext: string;\n buildUrl: (\n name: string,\n params?: Params,\n options?: { hash?: string },\n ) => string;\n /**\n * Decoded hash of the current browser location (no leading \"#\"). Defaults\n * to a no-op (returns \"\") for plugins that do not participate in URL\n * fragment tracking — namely hash-plugin, where `#` is the route delimiter.\n * (#532)\n */\n getCurrentHash?: () => string;\n /**\n * Decoded hash from the previous transition's `state.context.url.hash`\n * (no leading \"#\"). Used by the popstate handler to detect hash-only\n * navigation and add `force: true, hashChange: true` to bypass SAME_STATES.\n * Defaults to no-op (returns \"\") for hash-plugin. (#532)\n */\n getCurrentContextHash?: () => string;\n}\n\n/**\n * Hash augmentation for popstate-driven navigateToState (#532).\n * Returns a partial options object that the caller spreads on top of\n * `deps.transitionOptions`. When the handler is wired without hash support\n * (hash-plugin), both deps default to undefined and an empty object is\n * returned — preserving the legacy behavior for that plugin.\n */\nfunction resolveHashOptions(\n deps: PopstateHandlerDeps,\n matchedPath: string,\n): { hash?: string; force?: true; hashChange?: true } {\n if (!deps.getCurrentHash) {\n return {};\n }\n\n const newHash = deps.getCurrentHash();\n const prevHash = deps.getCurrentContextHash\n ? deps.getCurrentContextHash()\n : \"\";\n const hashChange =\n newHash !== prevHash && deps.router.getState()?.path === matchedPath;\n\n return hashChange\n ? { hash: newHash, force: true, hashChange: true }\n : { hash: newHash };\n}\n\nexport function createPopstateHandler(\n deps: PopstateHandlerDeps,\n): (evt: PopStateEvent) => void {\n let isTransitioning = false;\n let deferredEvent: PopStateEvent | null = null;\n\n function processDeferredEvent(): void {\n if (deferredEvent) {\n const evt = deferredEvent;\n\n deferredEvent = null;\n console.warn(\n `[${deps.loggerContext}] Processing deferred popstate event`,\n );\n void onPopState(evt);\n }\n }\n\n function rollbackUrlToCurrentState(): void {\n const currentState = deps.router.getState();\n\n /* v8 ignore next -- @preserve: router always has state after start(); defensive guard for edge cases */\n if (!currentState) {\n return;\n }\n\n // Preserve hash on rollback so guard rejection / unmatched URL on\n // popstate doesn't strip the fragment from the visible URL (#532).\n const ctxHash = (\n currentState.context as { url?: { hash?: string } } | undefined\n )?.url?.hash;\n const url = deps.buildUrl(\n currentState.name,\n currentState.params,\n ctxHash ? { hash: ctxHash } : undefined,\n );\n\n deps.browser.replaceState(currentState, url);\n }\n\n function recoverFromCriticalError(error: unknown): void {\n console.error(\n `[${deps.loggerContext}] Critical error in onPopState`,\n error,\n );\n\n try {\n rollbackUrlToCurrentState();\n } catch (recoveryError) {\n console.error(\n `[${deps.loggerContext}] Failed to recover from critical error`,\n recoveryError,\n );\n }\n }\n\n async function onPopState(evt: PopStateEvent): Promise<void> {\n if (isTransitioning) {\n console.warn(\n `[${deps.loggerContext}] Transition in progress, deferring popstate event`,\n );\n deferredEvent = evt;\n\n return;\n }\n\n isTransitioning = true;\n\n try {\n const matched = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (matched) {\n // api.navigateToState — plugin-only entry point. Preserves\n // matchSourceTrailingSlash output and skips the redundant\n // forwardState/buildPath round-trip (#525). Hash augmentation (#532)\n // extracted into resolveHashOptions so this branch stays readable.\n await deps.api.navigateToState(matched, {\n ...deps.transitionOptions,\n ...resolveHashOptions(deps, matched.path),\n });\n } else if (deps.allowNotFound) {\n deps.router.navigateToNotFound(deps.browser.getLocation());\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and sync URL\n // back to the current router state (no silent fallback to defaultRoute).\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n path: deps.browser.getLocation(),\n });\n\n deps.api.emitTransitionError(err);\n rollbackUrlToCurrentState();\n }\n } catch (error) {\n if (error instanceof RouterError) {\n // navigate() already emitted $$error — just sync URL with router state.\n // Swallow rollback errors: teardown races may remove router.buildUrl\n // while a popstate event is still queued.\n try {\n rollbackUrlToCurrentState();\n } catch {\n // noop — nothing safe to do here\n }\n } else {\n recoverFromCriticalError(error);\n }\n } finally {\n isTransitioning = false;\n processDeferredEvent();\n }\n }\n\n return (evt: PopStateEvent) => void onPopState(evt);\n}\n\nexport interface PopstateLifecycleDeps {\n browser: Browser;\n shared: SharedFactoryState;\n handler: (evt: PopStateEvent) => void;\n cleanup: () => void;\n}\n\nexport function createPopstateLifecycle(\n deps: PopstateLifecycleDeps,\n): Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\"> {\n return {\n onStart: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n }\n\n deps.shared.removePopStateListener = deps.browser.addPopstateListener(\n deps.handler,\n );\n },\n\n onStop: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n },\n\n teardown: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n\n deps.cleanup();\n },\n };\n}\n","/**\n * URL fragment (\"hash\") shared layer (#532).\n *\n * Both URL plugins (navigation-plugin, browser-plugin) claim the `\"url\"`\n * `state.context` namespace and write `UrlContext` on every transition.\n * Mutually exclusive at runtime — only one URL plugin is installed per router.\n *\n * Hash form: decoded, no leading \"#\" — symmetric to `params` (no leading \"?\").\n * Encoding to/from URL form happens at the boundary (URL build / URL parse).\n */\n\nexport interface UrlContext {\n /** Decoded fragment, no leading \"#\". Empty string when URL has no fragment. */\n hash: string;\n /** Whether `hash` differs from the previous transition's `state.context.url.hash`. */\n hashChanged: boolean;\n}\n\n/**\n * Encode for URL fragment per RFC 3986: preserves sub-delims (`&`, `=`, `?`,\n * `:`, etc.) and the path/query characters that `encodeURI` already leaves\n * alone. Defensively percent-escapes `#` (a stray `#` in a decoded fragment\n * would otherwise terminate the fragment in the rendered URL).\n *\n * `encodeURIComponent` over-encodes RFC-3986 sub-delims (`&` → `%26`) and is\n * therefore wrong for fragments.\n */\nexport function encodeHashFragment(decoded: string): string {\n return encodeURI(decoded).replaceAll(\"#\", \"%23\");\n}\n\n/**\n * Decode a percent-encoded fragment. Falls back to the raw input on malformed\n * escapes — matches the resilience pattern in scroll-restore.\n */\nexport function decodeHashFragment(encoded: string): string {\n try {\n return decodeURIComponent(encoded);\n } catch {\n return encoded;\n }\n}\n\n/**\n * Normalize user-provided hash input: strip ALL leading \"#\" characters, then\n * decode. Defensive against `<Link hash=\"#section\">` — the prop is documented\n * to accept the fragment name without \"#\", but we accept both gracefully.\n *\n * Stripping a single \"#\" would leave the function non-idempotent on\n * pathological inputs like `\"##section\"` (caller's accidental double-hash,\n * concatenation bugs). Property test G9 in `hash-encoding.properties.ts`\n * locks in idempotence — `normalize(normalize(x)) === normalize(x)`.\n */\nexport function normalizeHashInput(input: string): string {\n let stripped = input;\n\n while (stripped.startsWith(\"#\")) {\n stripped = stripped.slice(1);\n }\n\n return decodeHashFragment(stripped);\n}\n\n/**\n * Read the current browser hash in decoded form, no leading \"#\".\n * Accepts any object with a `getHash()` method — works for both `Browser`\n * (History API) and `NavigationBrowser` (Navigation API). SSR-safe via the\n * abstractions, which return `\"\"` outside a real browser.\n */\nexport function getDecodedHash(browser: { getHash: () => string }): string {\n const raw = browser.getHash();\n\n if (!raw) {\n return \"\";\n }\n\n const stripped = raw.startsWith(\"#\") ? raw.slice(1) : raw;\n\n return decodeHashFragment(stripped);\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 { decodeHashFragment } from \"./url-context.js\";\nimport { 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 * Like `urlToPath` but also returns the decoded URL fragment (#532).\n *\n * Used by URL plugins to extract `event.destination.url`'s hash without\n * dropping it the way `urlToPath` does. The hash is returned in decoded form\n * with no leading \"#\" — same form as stored in `state.context.url.hash`.\n */\nexport function urlToPathAndHash(\n url: string,\n base: string,\n): { path: string; hash: string } {\n const parsed = safeParseUrl(url);\n const path = extractPath(parsed.pathname, base) + parsed.search;\n const hash = parsed.hash ? decodeHashFragment(parsed.hash.slice(1)) : \"\";\n\n return { path, hash };\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 { encodeHashFragment, normalizeHashInput } from \"./url-context.js\";\nimport { buildUrl } from \"./url-utils.js\";\n\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 interface LocationSource {\n getLocation: () => string;\n}\n\n/**\n * Minimal browser surface needed by `createReplaceHistoryState`.\n *\n * Both `Browser` (History API) and navigation-plugin's `NavigationBrowser`\n * (Navigation API) satisfy this structurally — the function never needs\n * `pushState`/`addPopstateListener`, only the replace path.\n */\nexport interface ReplaceStateBrowser {\n replaceState: (state: unknown, url: string) => void;\n getHash: () => string;\n}\n\n/**\n * Hash override option for `replaceHistoryState` (#532). Tri-state semantics:\n * `undefined` — preserve the current browser hash (legacy behavior, default)\n * `\"\"` — explicitly clear the fragment\n * non-empty — explicitly set the fragment (decoded form, no leading \"#\")\n */\nexport interface ReplaceHistoryStateOptions {\n hash?: string;\n}\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: LocationSource,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n// Shared `buildUrl` extension for browser-plugin and navigation-plugin.\n// Composes router.buildPath + base prefixing + tri-state hash (#532) into the\n// single function the plugins register via `api.extendRouter({ buildUrl })`.\nexport function createPluginBuildUrl(\n router: Router,\n base: string,\n): (route: string, params?: Params, opts?: { hash?: string }) => string {\n return (route, params, opts) => {\n const path = router.buildPath(route, params);\n const url = buildUrl(path, base);\n\n if (opts?.hash === undefined) {\n return url;\n }\n\n const norm = normalizeHashInput(opts.hash);\n\n return norm ? `${url}#${encodeHashFragment(norm)}` : url;\n };\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: ReplaceStateBrowser,\n buildUrlFn: (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n ) => string,\n preserveHash = true,\n): (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n) => void {\n // Reusable buffer — browsers structured-clone state synchronously inside\n // replaceState, so the buffer never escapes. Eliminates one allocation per\n // navigation on the hot path. (Mirrors createUpdateBrowserState.)\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (\n name: string,\n params: Params = {},\n options?: ReplaceHistoryStateOptions,\n ) => {\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 // Tri-state hash semantics (#532):\n // options.hash === undefined → preserve (legacy behavior, controlled by\n // preserveHash flag — true for browser/\n // navigation plugins, false for hash-plugin)\n // options.hash === \"\" → explicitly clear\n // options.hash === \"value\" → explicitly set\n let hashSegment: string;\n\n if (options?.hash !== undefined) {\n const norm = normalizeHashInput(options.hash);\n\n hashSegment = norm ? `#${encodeHashFragment(norm)}` : \"\";\n } else if (preserveHash) {\n hashSegment = browser.getHash();\n } else {\n hashSegment = \"\";\n }\n\n // Pass hash through buildUrl when the plugin understands it (avoids\n // double-append). Hash-plugin's buildUrl ignores the option and warns,\n // so call without options here for semantic clarity — but the result is\n // identical because hashSegment is \"\" in that branch (preserveHash=false).\n const url = buildUrlFn(name, params) + hashSegment;\n\n buffer.name = builtState.name;\n buffer.params = builtState.params;\n buffer.path = builtState.path;\n\n browser.replaceState(buffer, url);\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","import type { BrowserPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<BrowserPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by browser events.\n * Used to distinguish browser-initiated navigation (back/forward buttons)\n * from programmatic navigation (router.navigate()).\n */\nexport const POPSTATE_SOURCE = \"popstate\";\n\nexport const LOGGER_CONTEXT = \"browser-plugin\";\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { BrowserPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<BrowserPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport {\n buildUrl,\n createPluginBuildUrl,\n createPopstateHandler,\n createPopstateLifecycle,\n createReplaceHistoryState,\n createSafeBrowser,\n createStartInterceptor,\n createUpdateBrowserState,\n encodeHashFragment,\n extractPath,\n getDecodedHash,\n normalizeBase,\n normalizeHashInput,\n safelyEncodePath,\n shouldReplaceHistory,\n urlToPath,\n} from \"./browser-env\";\nimport { defaultOptions, LOGGER_CONTEXT, POPSTATE_SOURCE } from \"./constants\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n Browser,\n PopstateTransitionOptions,\n SharedFactoryState,\n UrlContext,\n} from \"./browser-env\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst FROZEN_POPSTATE: BrowserContext = Object.freeze({\n source: \"popstate\",\n direction: \"back\",\n});\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({\n source: \"navigate\",\n direction: \"forward\",\n});\n\nexport function browserPluginFactory(\n opts?: Partial<BrowserPluginOptions>,\n browser?: Browser,\n): PluginFactory {\n validateOptions(opts);\n\n const options: Required<BrowserPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createDefaultBrowser(options.base);\n\n const transitionOptions = {\n forceDeactivate: options.forceDeactivate,\n source: POPSTATE_SOURCE,\n replace: true as const,\n };\n\n const shared: SharedFactoryState = { removePopStateListener: undefined };\n\n return function browserPlugin(routerBase) {\n return createBrowserPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n };\n}\n\n/**\n * Creates the default `Browser` for the plugin, with a memoized `getLocation`\n * that skips re-running `extractPath`/`safelyEncodePath` when neither\n * `pathname` nor `search` has changed since the last call (#8.2 A7).\n *\n * Initial sentinel is `\"\\0\"` — a NUL byte cannot appear in a real\n * `location.pathname`, so the first call is always a miss without needing a\n * separate \"primed\" flag.\n */\nfunction createDefaultBrowser(base: string): Browser {\n let cachedPathname = \"\\0\";\n let cachedSearch = \"\";\n let cachedResult = \"\";\n\n return createSafeBrowser(() => {\n const { pathname, search } = globalThis.location;\n\n if (pathname === cachedPathname && search === cachedSearch) {\n return cachedResult;\n }\n\n cachedPathname = pathname;\n cachedSearch = search;\n cachedResult = safelyEncodePath(extractPath(pathname, base)) + search;\n\n return cachedResult;\n }, \"browser-plugin\");\n}\n\nfunction createBrowserPlugin(\n router: Router,\n api: PluginApi,\n options: Required<BrowserPluginOptions>,\n browser: Browser,\n transitionOptions: PopstateTransitionOptions,\n shared: SharedFactoryState,\n): Plugin {\n const claim = api.claimContextNamespace(\"browser\");\n // Shared URL namespace (#532) — both navigation-plugin and browser-plugin\n // claim \"url\"; mutually exclusive at runtime.\n const urlClaim = api.claimContextNamespace(\"url\") as {\n write: (state: State, value: UrlContext) => void;\n release: () => void;\n };\n const updateState = createUpdateBrowserState();\n const removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = createPluginBuildUrl(router, options.base);\n\n const 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 ),\n });\n\n const handler = createPopstateHandler({\n router,\n api,\n browser,\n allowNotFound: api.getOptions().allowNotFound,\n transitionOptions,\n loggerContext: LOGGER_CONTEXT,\n buildUrl: pluginBuildUrl,\n // Hash bridging (#532). popstate doesn't carry a URL — we sample\n // location.hash after the browser has updated to the destination.\n getCurrentHash: () => getDecodedHash(browser),\n getCurrentContextHash: () =>\n (router.getState()?.context as { url?: { hash?: string } } | undefined)\n ?.url?.hash ?? \"\",\n });\n\n const lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n removeStartInterceptor();\n removeExtensions();\n claim.release();\n urlClaim.release();\n },\n });\n\n return {\n ...lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n const replaceHistory = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n // Tri-state hash resolution (#532).\n // navOptions.hash === undefined → preserve current browser hash\n // navOptions.hash === \"\" → explicitly clear\n // navOptions.hash === \"value\" → explicitly set\n //\n // The \"preserve\" branch reads location.hash from the browser, not\n // fromState.context.url.hash — captures dynamic fragment changes\n // (anchor clicks, manual location.hash assignment) made outside the\n // plugin. hashChanged compares the chosen hash against the published\n // previous hash so subscribers see a true signal.\n const browserHash = getDecodedHash(browser);\n const publishedPrevHash =\n (fromState?.context as { url?: { hash?: string } } | undefined)?.url\n ?.hash ?? \"\";\n\n const hash =\n navOptions.hash === undefined\n ? browserHash\n : normalizeHashInput(navOptions.hash);\n\n urlClaim.write(\n toState,\n Object.freeze({\n hash,\n hashChanged: navOptions.hashChange ?? hash !== publishedPrevHash,\n }),\n );\n\n const url = buildUrl(toState.path, options.base);\n const finalUrl = hash ? `${url}#${encodeHashFragment(hash)}` : url;\n\n updateState(toState, finalUrl, replaceHistory, browser);\n\n const isPopstate = navOptions.source === POPSTATE_SOURCE;\n\n claim.write(toState, isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE);\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["isState","RouterError","errorCodes"],"sources":["../../../../shared/browser-env/utils.ts","../../../type-guards/dist/esm/index.mjs","../../../../shared/browser-env/popstate-utils.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/history-api.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/safe-browser.ts","../../../../shared/browser-env/popstate-handler.ts","../../../../shared/browser-env/url-context.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../../../shared/browser-env/plugin-utils.ts","../../src/constants.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["/**\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","const e=[`replace`,`reload`,`force`,`forceDeactivate`,`redirected`];function t(t){if(typeof t!=`object`||!t||Array.isArray(t))return!1;let n=t;for(let t of e){let e=n[t];if(e!==void 0&&typeof e!=`boolean`)return!1}let r=n.signal;return!(r!==void 0&&!(r instanceof AbortSignal))}const n=/\\S/,r=/^[A-Z_a-z][\\w-]*(?:\\.[A-Z_a-z][\\w-]*)*$/;function i(e,t){return TypeError(`[router.${e}] ${t}`)}function a(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:r.test(e):!1}function o(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>o(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>o(e,t))}return!1}function s(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function c(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!s(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?o(e):!0}function l(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):Array.isArray(e)?e.every(e=>{let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}):!1}function u(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let n=e[t];if(!l(n))return!1}return!0}function d(e){return a(e.name)&&typeof e.path==`string`&&c(e.params)}function f(e){return typeof e!=`object`||!e?!1:d(e)}function p(e){return!(typeof e!=`object`||!e||!d(e))}function m(e){return typeof e==`string`}function h(e){return typeof e==`boolean`}function g(e,t){return e in t}function _(e){return typeof e==`number`?Number.isFinite(e):typeof e==`string`||typeof e==`boolean`}function v(e,t){if(typeof e!=`string`)throw i(t,`Route name must be a string, got ${typeof e}`);if(e!==``){if(!n.test(e))throw i(t,`Route name cannot contain only whitespace`);if(e.length>1e4)throw i(t,`Route name exceeds maximum length of 10000 characters. This is a technical safety limit.`);if(!e.startsWith(`@@`)&&!r.test(e))throw i(t,`Invalid route name \"${e}\". Each segment must start with a letter or underscore, followed by letters, numbers, underscores, or hyphens. Segments are separated by dots (e.g., \"users.profile\").`)}}function y(e){return e===null?`null`:Array.isArray(e)?`array[${e.length}]`:typeof e==`object`?`constructor`in e&&e.constructor.name!==`Object`?e.constructor.name:`object`:typeof e}function b(e,t){if(!f(e))throw TypeError(`[${t}] Invalid state structure: ${y(e)}. Expected State object with name, params, and path properties.`)}export{y as getTypeDescription,h as isBoolean,t as isNavigationOptions,g as isObjKey,c as isParams,u as isParamsStrict,_ as isPrimitiveValue,a as isRouteName,f as isState,p as isStateStrict,m as isString,v as validateRouteName,b as validateState};\n//# sourceMappingURL=index.mjs.map","import { isStateStrict as isState } from \"type-guards\";\n\nimport type { Browser } from \"./types.js\";\nimport type { State, Params } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Resolves the popstate event into a navigation-ready `State`.\n *\n * - If `history.state` is a valid router state ({name, params, path} written\n * by browser-plugin/hash-plugin during their previous navigation), it is\n * the source of truth — synthesize a fully-typed `State` from it via\n * `api.makeState`. The synthesized `transition`/`context` fields are\n * placeholders; the navigation pipeline (`completeTransition` and plugin\n * claim writes) replaces them.\n * This branch is mandatory for hash-plugin: `browser.getLocation()`\n * returns the History pathname, not the hash, so the matchPath fallback\n * below cannot extract the hash route.\n * - Otherwise (e.g. manually entered URL with no recorded state), fall\n * back to `api.matchPath(browser.getLocation())`. browser-plugin's\n * `getLocation` returns the URL pathname — this works.\n * - `undefined` when neither path produces a match.\n *\n * Replaces the previous `{ name, params }` shape so the caller can hand\n * the State directly to `router.navigateToState(state, opts)` and skip\n * the redundant `forwardState`/`buildPath` round-trip in\n * `buildNavigateState` (issue #525).\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): State | undefined {\n if (isState(evt.state)) {\n return api.makeState(evt.state.name, evt.state.params, evt.state.path);\n }\n\n return api.matchPath(browser.getLocation());\n}\n\n/**\n * Updates browser state (pushState or replaceState)\n *\n * @param state - Router state\n * @param url - URL to set\n * @param replace - Whether to replace instead of push\n * @param browser - Browser API instance\n */\nexport function updateBrowserState(\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n): void {\n const historyState = {\n name: state.name,\n params: state.params,\n path: state.path,\n };\n\n if (replace) {\n browser.replaceState(historyState, url);\n } else {\n browser.pushState(historyState, url);\n }\n}\n\n/**\n * Creates a `updateBrowserState` closure that reuses a single mutable buffer\n * across calls instead of allocating a fresh `{ name, params, path }` object\n * per push/replace.\n *\n * Why: Browsers structured-clone `history.state` synchronously inside\n * `pushState`/`replaceState`, so the caller never sees the buffer escape —\n * it can be safely overwritten before the next call. Eliminates one\n * allocation per navigation on the hot path.\n *\n * Each plugin instance must own its own buffer (do not share across plugins).\n */\nexport function createUpdateBrowserState(): (\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n) => void {\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (state, url, replace, browser) => {\n buffer.name = state.name;\n buffer.params = state.params;\n buffer.path = state.path;\n\n if (replace) {\n browser.replaceState(buffer, url);\n } else {\n browser.pushState(buffer, url);\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","export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","import type { HistoryBrowser } from \"./types.js\";\n\nexport const pushState = (state: unknown, path: string): void => {\n globalThis.history.pushState(state, \"\", path);\n};\n\nexport const replaceState = (state: unknown, path: string): void => {\n globalThis.history.replaceState(state, \"\", path);\n};\n\nexport const addPopstateListener: HistoryBrowser[\"addPopstateListener\"] = (\n fn,\n) => {\n globalThis.addEventListener(\"popstate\", fn);\n\n return () => {\n globalThis.removeEventListener(\"popstate\", fn);\n };\n};\n\nexport const getHash = (): string => globalThis.location.hash;\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","import { isBrowserEnvironment } from \"./detect.js\";\nimport {\n pushState,\n replaceState,\n addPopstateListener,\n getHash,\n} from \"./history-api.js\";\nimport {\n createWarnOnce,\n createHistoryFallbackBrowser,\n} from \"./ssr-fallback.js\";\n\nimport type { Browser } from \"./types.js\";\n\nexport function createSafeBrowser(\n getLocation: () => string,\n context: string,\n): Browser {\n if (isBrowserEnvironment()) {\n return {\n pushState,\n replaceState,\n addPopstateListener,\n getLocation,\n getHash,\n };\n }\n\n const warnOnce = createWarnOnce(context);\n\n return {\n ...createHistoryFallbackBrowser(context),\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"\";\n },\n };\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { getRouteFromEvent } from \"./popstate-utils.js\";\n\nimport type { Browser, SharedFactoryState } from \"./types.js\";\nimport type { Params, Plugin, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Navigation options used by the popstate handler to trigger a\n * router.navigate() call from a back/forward event. `source` identifies\n * the origin of the transition to downstream context consumers;\n * `replace: true` keeps the history stack in sync with the browser.\n */\nexport interface PopstateTransitionOptions {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n}\n\nexport interface PopstateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: Browser;\n allowNotFound: boolean;\n transitionOptions: PopstateTransitionOptions;\n loggerContext: string;\n buildUrl: (\n name: string,\n params?: Params,\n options?: { hash?: string },\n ) => string;\n /**\n * Decoded hash of the current browser location (no leading \"#\"). Defaults\n * to a no-op (returns \"\") for plugins that do not participate in URL\n * fragment tracking — namely hash-plugin, where `#` is the route delimiter.\n * (#532)\n */\n getCurrentHash?: () => string;\n /**\n * Decoded hash from the previous transition's `state.context.url.hash`\n * (no leading \"#\"). Used by the popstate handler to detect hash-only\n * navigation and add `force: true, hashChange: true` to bypass SAME_STATES.\n * Defaults to no-op (returns \"\") for hash-plugin. (#532)\n */\n getCurrentContextHash?: () => string;\n}\n\n/**\n * Hash augmentation for popstate-driven navigateToState (#532).\n * Returns a partial options object that the caller spreads on top of\n * `deps.transitionOptions`. When the handler is wired without hash support\n * (hash-plugin), both deps default to undefined and an empty object is\n * returned — preserving the legacy behavior for that plugin.\n */\nfunction resolveHashOptions(\n deps: PopstateHandlerDeps,\n matchedPath: string,\n): { hash?: string; force?: true; hashChange?: true } {\n if (!deps.getCurrentHash) {\n return {};\n }\n\n const newHash = deps.getCurrentHash();\n const prevHash = deps.getCurrentContextHash\n ? deps.getCurrentContextHash()\n : \"\";\n const hashChange =\n newHash !== prevHash && deps.router.getState()?.path === matchedPath;\n\n return hashChange\n ? { hash: newHash, force: true, hashChange: true }\n : { hash: newHash };\n}\n\nexport function createPopstateHandler(\n deps: PopstateHandlerDeps,\n): (evt: PopStateEvent) => void {\n let isTransitioning = false;\n let deferredEvent: PopStateEvent | null = null;\n\n function processDeferredEvent(): void {\n if (deferredEvent) {\n const evt = deferredEvent;\n\n deferredEvent = null;\n console.warn(\n `[${deps.loggerContext}] Processing deferred popstate event`,\n );\n void onPopState(evt);\n }\n }\n\n function rollbackUrlToCurrentState(): void {\n const currentState = deps.router.getState();\n\n /* v8 ignore next -- @preserve: router always has state after start(); defensive guard for edge cases */\n if (!currentState) {\n return;\n }\n\n // Preserve hash on rollback so guard rejection / unmatched URL on\n // popstate doesn't strip the fragment from the visible URL (#532).\n const ctxHash = (\n currentState.context as { url?: { hash?: string } } | undefined\n )?.url?.hash;\n const url = deps.buildUrl(\n currentState.name,\n currentState.params,\n ctxHash ? { hash: ctxHash } : undefined,\n );\n\n deps.browser.replaceState(currentState, url);\n }\n\n function recoverFromCriticalError(error: unknown): void {\n console.error(\n `[${deps.loggerContext}] Critical error in onPopState`,\n error,\n );\n\n try {\n rollbackUrlToCurrentState();\n } catch (recoveryError) {\n console.error(\n `[${deps.loggerContext}] Failed to recover from critical error`,\n recoveryError,\n );\n }\n }\n\n async function onPopState(evt: PopStateEvent): Promise<void> {\n if (isTransitioning) {\n console.warn(\n `[${deps.loggerContext}] Transition in progress, deferring popstate event`,\n );\n deferredEvent = evt;\n\n return;\n }\n\n isTransitioning = true;\n\n try {\n const matched = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (matched) {\n // api.navigateToState — plugin-only entry point. Preserves\n // matchSourceTrailingSlash output and skips the redundant\n // forwardState/buildPath round-trip (#525). Hash augmentation (#532)\n // extracted into resolveHashOptions so this branch stays readable.\n await deps.api.navigateToState(matched, {\n ...deps.transitionOptions,\n ...resolveHashOptions(deps, matched.path),\n });\n } else if (deps.allowNotFound) {\n deps.router.navigateToNotFound(deps.browser.getLocation());\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and sync URL\n // back to the current router state (no silent fallback to defaultRoute).\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n path: deps.browser.getLocation(),\n });\n\n deps.api.emitTransitionError(err);\n rollbackUrlToCurrentState();\n }\n } catch (error) {\n if (error instanceof RouterError) {\n // navigate() already emitted $$error — just sync URL with router state.\n // Swallow rollback errors: teardown races may remove router.buildUrl\n // while a popstate event is still queued.\n try {\n rollbackUrlToCurrentState();\n } catch {\n // noop — nothing safe to do here\n }\n } else {\n recoverFromCriticalError(error);\n }\n } finally {\n isTransitioning = false;\n processDeferredEvent();\n }\n }\n\n return (evt: PopStateEvent) => void onPopState(evt);\n}\n\nexport interface PopstateLifecycleDeps {\n browser: Browser;\n shared: SharedFactoryState;\n handler: (evt: PopStateEvent) => void;\n cleanup: () => void;\n}\n\nexport function createPopstateLifecycle(\n deps: PopstateLifecycleDeps,\n): Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\"> {\n return {\n onStart: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n }\n\n deps.shared.removePopStateListener = deps.browser.addPopstateListener(\n deps.handler,\n );\n },\n\n onStop: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n },\n\n teardown: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n\n deps.cleanup();\n },\n };\n}\n","/**\n * URL fragment (\"hash\") shared layer (#532).\n *\n * Both URL plugins (navigation-plugin, browser-plugin) claim the `\"url\"`\n * `state.context` namespace and write `UrlContext` on every transition.\n * Mutually exclusive at runtime — only one URL plugin is installed per router.\n *\n * Hash form: decoded, no leading \"#\" — symmetric to `params` (no leading \"?\").\n * Encoding to/from URL form happens at the boundary (URL build / URL parse).\n */\n\nexport interface UrlContext {\n /** Decoded fragment, no leading \"#\". Empty string when URL has no fragment. */\n hash: string;\n /** Whether `hash` differs from the previous transition's `state.context.url.hash`. */\n hashChanged: boolean;\n}\n\n/**\n * Encode for URL fragment per RFC 3986: preserves sub-delims (`&`, `=`, `?`,\n * `:`, etc.) and the path/query characters that `encodeURI` already leaves\n * alone. Defensively percent-escapes `#` (a stray `#` in a decoded fragment\n * would otherwise terminate the fragment in the rendered URL).\n *\n * `encodeURIComponent` over-encodes RFC-3986 sub-delims (`&` → `%26`) and is\n * therefore wrong for fragments.\n */\nexport function encodeHashFragment(decoded: string): string {\n return encodeURI(decoded).replaceAll(\"#\", \"%23\");\n}\n\n/**\n * Decode a percent-encoded fragment. Falls back to the raw input on malformed\n * escapes — matches the resilience pattern in scroll-restore.\n */\nexport function decodeHashFragment(encoded: string): string {\n try {\n return decodeURIComponent(encoded);\n } catch {\n return encoded;\n }\n}\n\n/**\n * Normalize user-provided hash input: strip ALL leading \"#\" characters, then\n * decode. Defensive against `<Link hash=\"#section\">` — the prop is documented\n * to accept the fragment name without \"#\", but we accept both gracefully.\n *\n * Stripping a single \"#\" would leave the function non-idempotent on\n * pathological inputs like `\"##section\"` (caller's accidental double-hash,\n * concatenation bugs). Property test G9 in `hash-encoding.properties.ts`\n * locks in idempotence — `normalize(normalize(x)) === normalize(x)`.\n */\nexport function normalizeHashInput(input: string): string {\n let stripped = input;\n\n while (stripped.startsWith(\"#\")) {\n stripped = stripped.slice(1);\n }\n\n return decodeHashFragment(stripped);\n}\n\n/**\n * Read the current browser hash in decoded form, no leading \"#\".\n * Accepts any object with a `getHash()` method — works for both `Browser`\n * (History API) and `NavigationBrowser` (Navigation API). SSR-safe via the\n * abstractions, which return `\"\"` outside a real browser.\n */\nexport function getDecodedHash(browser: { getHash: () => string }): string {\n const raw = browser.getHash();\n\n if (!raw) {\n return \"\";\n }\n\n const stripped = raw.startsWith(\"#\") ? raw.slice(1) : raw;\n\n return decodeHashFragment(stripped);\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 */\nconst URL_DELIMITERS: ReadonlySet<string> = new Set([\"/\", \"?\", \"#\"]);\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 (URL_DELIMITERS.has(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 { decodeHashFragment } from \"./url-context.js\";\nimport { 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 * Like `urlToPath` but also returns the decoded URL fragment (#532).\n *\n * Used by URL plugins to extract `event.destination.url`'s hash without\n * dropping it the way `urlToPath` does. The hash is returned in decoded form\n * with no leading \"#\" — same form as stored in `state.context.url.hash`.\n */\nexport function urlToPathAndHash(\n url: string,\n base: string,\n): { path: string; hash: string } {\n const parsed = safeParseUrl(url);\n const path = extractPath(parsed.pathname, base) + parsed.search;\n const hash = parsed.hash ? decodeHashFragment(parsed.hash.slice(1)) : \"\";\n\n return { path, hash };\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 { encodeHashFragment, normalizeHashInput } from \"./url-context.js\";\nimport { buildUrl } from \"./url-utils.js\";\n\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 interface LocationSource {\n getLocation: () => string;\n}\n\n/**\n * Minimal browser surface needed by `createReplaceHistoryState`.\n *\n * Both `Browser` (History API) and navigation-plugin's `NavigationBrowser`\n * (Navigation API) satisfy this structurally — the function never needs\n * `pushState`/`addPopstateListener`, only the replace path.\n */\nexport interface ReplaceStateBrowser {\n replaceState: (state: unknown, url: string) => void;\n getHash: () => string;\n}\n\n/**\n * Hash override option for `replaceHistoryState` (#532). Tri-state semantics:\n * `undefined` — preserve the current browser hash (legacy behavior, default)\n * `\"\"` — explicitly clear the fragment\n * non-empty — explicitly set the fragment (decoded form, no leading \"#\")\n */\nexport interface ReplaceHistoryStateOptions {\n hash?: string;\n}\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: LocationSource,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n// Shared `buildUrl` extension for browser-plugin and navigation-plugin.\n// Composes router.buildPath + base prefixing + tri-state hash (#532) into the\n// single function the plugins register via `api.extendRouter({ buildUrl })`.\nexport function createPluginBuildUrl(\n router: Router,\n base: string,\n): (route: string, params?: Params, opts?: { hash?: string }) => string {\n return (route, params, opts) => {\n const path = router.buildPath(route, params);\n const url = buildUrl(path, base);\n\n if (opts?.hash === undefined) {\n return url;\n }\n\n const norm = normalizeHashInput(opts.hash);\n\n return norm ? `${url}#${encodeHashFragment(norm)}` : url;\n };\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: ReplaceStateBrowser,\n buildUrlFn: (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n ) => string,\n preserveHash = true,\n): (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n) => void {\n // Reusable buffer — browsers structured-clone state synchronously inside\n // replaceState, so the buffer never escapes. Eliminates one allocation per\n // navigation on the hot path. (Mirrors createUpdateBrowserState.)\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (\n name: string,\n params: Params = {},\n options?: ReplaceHistoryStateOptions,\n ) => {\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 // Tri-state hash semantics (#532):\n // options.hash === undefined → preserve (legacy behavior, controlled by\n // preserveHash flag — true for browser/\n // navigation plugins, false for hash-plugin)\n // options.hash === \"\" → explicitly clear\n // options.hash === \"value\" → explicitly set\n let hashSegment: string;\n\n if (options?.hash !== undefined) {\n const norm = normalizeHashInput(options.hash);\n\n hashSegment = norm ? `#${encodeHashFragment(norm)}` : \"\";\n } else if (preserveHash) {\n hashSegment = browser.getHash();\n } else {\n hashSegment = \"\";\n }\n\n // Pass hash through buildUrl when the plugin understands it (avoids\n // double-append). Hash-plugin's buildUrl ignores the option and warns,\n // so call without options here for semantic clarity — but the result is\n // identical because hashSegment is \"\" in that branch (preserveHash=false).\n const url = buildUrlFn(name, params) + hashSegment;\n\n buffer.name = builtState.name;\n buffer.params = builtState.params;\n buffer.path = builtState.path;\n\n browser.replaceState(buffer, url);\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","import type { BrowserPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<BrowserPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by browser events.\n * Used to distinguish browser-initiated navigation (back/forward buttons)\n * from programmatic navigation (router.navigate()).\n */\nexport const POPSTATE_SOURCE = \"popstate\";\n\nexport const LOGGER_CONTEXT = \"browser-plugin\";\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { BrowserPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<BrowserPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport {\n buildUrl,\n createPluginBuildUrl,\n createPopstateHandler,\n createPopstateLifecycle,\n createReplaceHistoryState,\n createSafeBrowser,\n createStartInterceptor,\n createUpdateBrowserState,\n encodeHashFragment,\n extractPath,\n getDecodedHash,\n normalizeBase,\n normalizeHashInput,\n safelyEncodePath,\n shouldReplaceHistory,\n urlToPath,\n} from \"./browser-env\";\nimport { defaultOptions, LOGGER_CONTEXT, POPSTATE_SOURCE } from \"./constants\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n Browser,\n PopstateTransitionOptions,\n SharedFactoryState,\n UrlContext,\n} from \"./browser-env\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst FROZEN_POPSTATE: BrowserContext = Object.freeze({\n source: \"popstate\",\n direction: \"back\",\n});\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({\n source: \"navigate\",\n direction: \"forward\",\n});\n\nexport function browserPluginFactory(\n opts?: Partial<BrowserPluginOptions>,\n browser?: Browser,\n): PluginFactory {\n validateOptions(opts);\n\n const options: Required<BrowserPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createDefaultBrowser(options.base);\n\n const transitionOptions = {\n forceDeactivate: options.forceDeactivate,\n source: POPSTATE_SOURCE,\n replace: true as const,\n };\n\n const shared: SharedFactoryState = { removePopStateListener: undefined };\n\n return function browserPlugin(routerBase) {\n return createBrowserPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n };\n}\n\n/**\n * Creates the default `Browser` for the plugin, with a memoized `getLocation`\n * that skips re-running `extractPath`/`safelyEncodePath` when neither\n * `pathname` nor `search` has changed since the last call (#8.2 A7).\n *\n * Initial sentinel is `\"\\0\"` — a NUL byte cannot appear in a real\n * `location.pathname`, so the first call is always a miss without needing a\n * separate \"primed\" flag.\n */\nfunction createDefaultBrowser(base: string): Browser {\n let cachedPathname = \"\\0\";\n let cachedSearch = \"\";\n let cachedResult = \"\";\n\n return createSafeBrowser(() => {\n const { pathname, search } = globalThis.location;\n\n if (pathname === cachedPathname && search === cachedSearch) {\n return cachedResult;\n }\n\n cachedPathname = pathname;\n cachedSearch = search;\n cachedResult = safelyEncodePath(extractPath(pathname, base)) + search;\n\n return cachedResult;\n }, \"browser-plugin\");\n}\n\nfunction createBrowserPlugin(\n router: Router,\n api: PluginApi,\n options: Required<BrowserPluginOptions>,\n browser: Browser,\n transitionOptions: PopstateTransitionOptions,\n shared: SharedFactoryState,\n): Plugin {\n const claim = api.claimContextNamespace(\"browser\");\n // Shared URL namespace (#532) — both navigation-plugin and browser-plugin\n // claim \"url\"; mutually exclusive at runtime.\n const urlClaim = api.claimContextNamespace(\"url\") as {\n write: (state: State, value: UrlContext) => void;\n release: () => void;\n };\n const updateState = createUpdateBrowserState();\n const removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = createPluginBuildUrl(router, options.base);\n\n const 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 ),\n });\n\n const handler = createPopstateHandler({\n router,\n api,\n browser,\n allowNotFound: api.getOptions().allowNotFound,\n transitionOptions,\n loggerContext: LOGGER_CONTEXT,\n buildUrl: pluginBuildUrl,\n // Hash bridging (#532). popstate doesn't carry a URL — we sample\n // location.hash after the browser has updated to the destination.\n getCurrentHash: () => getDecodedHash(browser),\n getCurrentContextHash: () =>\n (router.getState()?.context as { url?: { hash?: string } } | undefined)\n ?.url?.hash ?? \"\",\n });\n\n const lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n removeStartInterceptor();\n removeExtensions();\n claim.release();\n urlClaim.release();\n },\n });\n\n return {\n ...lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n const replaceHistory = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n // Tri-state hash resolution (#532).\n // navOptions.hash === undefined → preserve current browser hash\n // navOptions.hash === \"\" → explicitly clear\n // navOptions.hash === \"value\" → explicitly set\n //\n // The \"preserve\" branch reads location.hash from the browser, not\n // fromState.context.url.hash — captures dynamic fragment changes\n // (anchor clicks, manual location.hash assignment) made outside the\n // plugin. hashChanged compares the chosen hash against the published\n // previous hash so subscribers see a true signal.\n const browserHash = getDecodedHash(browser);\n const publishedPrevHash =\n (fromState?.context as { url?: { hash?: string } } | undefined)?.url\n ?.hash ?? \"\";\n\n const hash =\n navOptions.hash === undefined\n ? browserHash\n : normalizeHashInput(navOptions.hash);\n\n urlClaim.write(\n toState,\n Object.freeze({\n hash,\n hashChanged: navOptions.hashChange ?? hash !== publishedPrevHash,\n }),\n );\n\n const url = buildUrl(toState.path, options.base);\n const finalUrl = hash ? `${url}#${encodeHashFragment(hash)}` : url;\n\n updateState(toState, finalUrl, replaceHistory, browser);\n\n const isPopstate = navOptions.source === POPSTATE_SOURCE;\n\n claim.write(toState, isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE);\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":["NavigationOptions","Params","State","isNavigationOptions","value","isRouteName","name","isState","P","isStateStrict","isString","isBoolean","isObjKey","T","Extract","key","obj","isPrimitiveValue","isParams","isParamsStrict","validateRouteName","methodName","validateState","state","method","getTypeDescription"],"sources":["../../src/types.ts","../../../../shared/browser-env/types.ts","../../../../shared/browser-env/url-context.ts","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;;AAAZ;;;;AAAyB;AAWzB;;;KAAY,gBAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;EACR,SAAA,EAAW,gBAAgB;AAAA;AAAA,UAGZ,oBAAA;EAJf;;;;;EAUA,eAAA;EANe;;;;AAaX;EAAJ,IAAI;AAAA;;;UClCW,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAa;EAC7C,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAc;EAC7C,WAAW;AAAA;;;;;;;;ADLb;;;;AAAyB;UEQR,UAAA;EFGW;EED1B,IAAA;EFC0B;EEC1B,WAAW;AAAA;;;iBCiCG,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,GACf,OAAA,GAAU,OAAA,GACT,aAAA;;;;;;;;;;;;;;;iBC8BcS,aAAAA,WAAwBR,QAAAA,GAASA,QAAAA,CAAAA,CAAQG,KAAAA,YAAiBA,KAAAA,IAASF,OAAAA,CAAMM,CAAAA;AAAAA;;;AAbL;;;;;;AJjErF;;;;AAAA;EAAA,UKsBY,YAAA;IACR,OAAA,GARmD,cAAA;ILJ3B;;AAAA;AAE5B;;IKgBI,GAAA,GAN0C,UAMF;EAAA;EAAA,UAGhC,iBAAA;ILlBF;IKoBN,MAAA;ILnBS;;AAAgB;AAG7B;IKqBI,IAAA;ILrBiC;;AAa/B;;IKaF,UAAA;EAAA;AAAA;AAAA;EAAA,UAKQ,MAAA;IJjDmC;;;;IIsD3C,QAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IJzDhB;;;;IIgEE,QAAA,CAAS,GAAA,WAAc,KAAA;IJ/DlB;AAAA;AAGT;;IIkEI,mBAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IAGd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":["value","NavigationOptions","name","P","Params","State","T","key","obj","Extract","methodName","state","method"],"sources":["../../src/types.ts","../../../../shared/browser-env/types.ts","../../../../shared/browser-env/url-context.ts","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;;AAAZ;;;;AAAyB;AAWzB;;;KAAY,gBAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;EACR,SAAA,EAAW,gBAAgB;AAAA;AAAA,UAGZ,oBAAA;EAJf;;;;;EAUA,eAAA;EANe;;;;AAaX;EAAJ,IAAI;AAAA;;;UClCW,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAa;EAC7C,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAc;EAC7C,WAAW;AAAA;;;;;;;;ADLb;;;;AAAyB;UEQR,UAAA;EFGW;EED1B,IAAA;EFC0B;EEC1B,WAAW;AAAA;;;iBCiCG,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,GACf,OAAA,GAAU,OAAA,GACT,aAAA;;;;;;;;;;;;;;;iBC8Bc,aAAA,WAAwB,QAAA,GAAS,QAAA,EAAQA,KAAAA,YAAiBA,KAAAA,IAAS,OAAA,CAAM,CAAA;AAAA;;;AAbL;;;;;;AJjErF;;;;AAAA;EAAA,UKsBY,YAAA;IACR,OAAA,GARmD,cAAA;ILJ3B;;AAAA;AAE5B;;IKgBI,GAAA,GAN0C,UAMF;EAAA;EAAA,UAGhC,iBAAA;ILlBF;IKoBN,MAAA;ILnBS;;AAAgB;AAG7B;IKqBI,IAAA;ILrBiC;;AAa/B;;IKaF,UAAA;EAAA;AAAA;AAAA;EAAA,UAKQ,MAAA;IJjDmC;;;;IIsD3C,QAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IJzDhB;;;;IIgEE,QAAA,CAAS,GAAA,WAAc,KAAA;IJ/DlB;AAAA;AAGT;;IIkEI,mBAAA,CACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA;MAAY,IAAA;IAAA;IAGd,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,errorCodes as n}from"@real-router/core";const r=()=>globalThis.window!==void 0&&!!globalThis.history,i=(e,t)=>{globalThis.history.pushState(e,``,t)},a=(e,t)=>{globalThis.history.replaceState(e,``,t)},o=e=>(globalThis.addEventListener(`popstate`,e),()=>{globalThis.removeEventListener(`popstate`,e)}),s=()=>globalThis.location.hash;function c(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 l=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},u=()=>{},d=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)}},f=e=>{let t=d(e);return{pushState:()=>{t(`pushState`)},replaceState:()=>{t(`replaceState`)},addPopstateListener:()=>(t(`addPopstateListener`),u),getHash:()=>(t(`getHash`),``)}},p=/^[A-Z_a-z][\w-]*(?:\.[A-Z_a-z][\w-]*)*$/;function m(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:p.test(e):!1}function h(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>h(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>h(e,t))}return!1}function g(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function _(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!g(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?h(e):!0}function v(e){return m(e.name)&&typeof e.path==`string`&&_(e.params)}function y(e){return!(typeof e!=`object`||!e||!v(e))}function b(e,t,n){return y(e.state)?t.makeState(e.state.name,e.state.params,e.state.path):t.matchPath(n.getLocation())}function x(){let e={name:``,params:{},path:``};return(t,n,r,i)=>{e.name=t.name,e.params=t.params,e.path=t.path,r?i.replaceState(e,n):i.pushState(e,n)}}function S(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 C=/[\u0000-\u001F\u007F]/,w={validate:e=>C.test(e)?`must not contain control characters`:e.split(`/`).includes(`..`)?`must not contain '..' segments`:null};function T(e,t){if(r())return{pushState:i,replaceState:a,addPopstateListener:o,getLocation:e,getHash:s};let n=d(t);return{...f(t),getLocation:()=>(n(`getLocation`),``)}}function E(e,t){if(!e.getCurrentHash)return{};let n=e.getCurrentHash();return n!==(e.getCurrentContextHash?e.getCurrentContextHash():``)&&e.router.getState()?.path===t?{hash:n,force:!0,hashChange:!0}:{hash:n}}function D(e){let r=!1,i=null;function a(){if(i){let t=i;i=null,console.warn(`[${e.loggerContext}] Processing deferred popstate event`),c(t)}}function o(){let t=e.router.getState();if(!t)return;let n=t.context?.url?.hash,r=e.buildUrl(t.name,t.params,n?{hash:n}:void 0);e.browser.replaceState(t,r)}function s(t){console.error(`[${e.loggerContext}] Critical error in onPopState`,t);try{o()}catch(t){console.error(`[${e.loggerContext}] Failed to recover from critical error`,t)}}async function c(c){if(r){console.warn(`[${e.loggerContext}] Transition in progress, deferring popstate event`),i=c;return}r=!0;try{let r=b(c,e.api,e.browser);if(r)await e.api.navigateToState(r,{...e.transitionOptions,...E(e,r.path)});else if(e.allowNotFound)e.router.navigateToNotFound(e.browser.getLocation());else{let r=new t(n.ROUTE_NOT_FOUND,{path:e.browser.getLocation()});e.api.emitTransitionError(r),o()}}catch(e){if(e instanceof t)try{o()}catch{}else s(e)}finally{r=!1,a()}}return e=>void c(e)}function O(e){return{onStart:()=>{e.shared.removePopStateListener&&e.shared.removePopStateListener(),e.shared.removePopStateListener=e.browser.addPopstateListener(e.handler)},onStop:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0)},teardown:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0),e.cleanup()}}}function k(e){return encodeURI(e).replaceAll(`#`,`%23`)}function A(e){try{return decodeURIComponent(e)}catch{return e}}function j(e){let t=e;for(;t.startsWith(`#`);)t=t.slice(1);return A(t)}function M(e){let t=e.getHash();return t?A(t.startsWith(`#`)?t.slice(1):t):``}function N(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 F(e,t){return e?t?e===`/`?t:e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function I(e,t){let n=N(e);return P(n.pathname,t)+n.search}function L(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function R(e,t){return(n,r,i)=>{let a=F(e.buildPath(n,r),t);if(i?.hash===void 0)return a;let o=j(i.hash);return o?`${a}#${k(o)}`:a}}function z(e,t,n,r,i=!0){let a={name:``,params:{},path:``};return(o,s={},c)=>{let l=e.buildState(o,s);if(!l)throw Error(`[real-router] Cannot replace state: route "${o}" is not found`);let u=e.makeState(l.name,l.params,t.buildPath(l.name,l.params),{params:l.meta}),d;if(c?.hash!==void 0){let e=j(c.hash);d=e?`#${k(e)}`:``}else d=i?n.getHash():``;let f=r(o,s)+d;a.name=u.name,a.params=u.params,a.path=u.path,n.replaceState(a,f)}}function B(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}const V={forceDeactivate:!0,base:``},H=`popstate`,U=`browser-plugin`,W=S(V,U,{base:w}),G=Object.freeze({source:`popstate`,direction:`back`}),K=Object.freeze({source:`navigate`,direction:`forward`});function q(t,n){W(t);let r={...V,...t};r.base=c(r.base);let i=n??J(r.base),a={forceDeactivate:r.forceDeactivate,source:H,replace:!0},o={removePopStateListener:void 0};return function(t){return Y(t,e(t),r,i,a,o)}}function J(e){let t=`\0`,n=``,r=``;return T(()=>{let{pathname:i,search:a}=globalThis.location;return i===t&&a===n?r:(t=i,n=a,r=l(P(i,e))+a,r)},`browser-plugin`)}function Y(e,t,n,r,i,a){let o=t.claimContextNamespace(`browser`),s=t.claimContextNamespace(`url`),c=x(),l=L(t,r),u=R(e,n.base),d=t.extendRouter({buildUrl:u,matchUrl:e=>t.matchPath(I(e,n.base))??void 0,replaceHistoryState:z(t,e,r,u)});return{...O({browser:r,shared:a,handler:D({router:e,api:t,browser:r,allowNotFound:t.getOptions().allowNotFound,transitionOptions:i,loggerContext:U,buildUrl:u,getCurrentHash:()=>M(r),getCurrentContextHash:()=>(e.getState()?.context)?.url?.hash??``}),cleanup:()=>{l(),d(),o.release(),s.release()}}),onTransitionSuccess:(e,t,i)=>{let a=B(i,e,t),l=M(r),u=(t?.context)?.url?.hash??``,d=i.hash===void 0?l:j(i.hash);s.write(e,Object.freeze({hash:d,hashChanged:i.hashChange??d!==u}));let f=F(e.path,n.base);c(e,d?`${f}#${k(d)}`:f,a,r);let p=i.source===H;o.write(e,p?G:K)}}}export{q as browserPluginFactory,y as isState};
1
+ import{getPluginApi as e}from"@real-router/core/api";import{RouterError as t,errorCodes as n}from"@real-router/core";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=/^[A-Z_a-z][\w-]*(?:\.[A-Z_a-z][\w-]*)*$/;function o(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:a.test(e):!1}function s(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>s(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>s(e,t))}return!1}function c(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function l(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!c(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?s(e):!0}function u(e){return o(e.name)&&typeof e.path==`string`&&l(e.params)}function d(e){return!(typeof e!=`object`||!e||!u(e))}function f(e,t,n){return d(e.state)?t.makeState(e.state.name,e.state.params,e.state.path):t.matchPath(n.getLocation())}function p(){let e={name:``,params:{},path:``};return(t,n,r,i)=>{e.name=t.name,e.params=t.params,e.path=t.path,r?i.replaceState(e,n):i.pushState(e,n)}}function m(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 h=/[\u0000-\u001F\u007F]/,g={validate:e=>h.test(e)?`must not contain control characters`:e.split(`/`).includes(`..`)?`must not contain '..' segments`:null},_=()=>globalThis.window!==void 0&&!!globalThis.history,v=(e,t)=>{globalThis.history.pushState(e,``,t)},y=(e,t)=>{globalThis.history.replaceState(e,``,t)},b=e=>(globalThis.addEventListener(`popstate`,e),()=>{globalThis.removeEventListener(`popstate`,e)}),x=()=>globalThis.location.hash,S=()=>{},C=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)}},w=e=>{let t=C(e);return{pushState:()=>{t(`pushState`)},replaceState:()=>{t(`replaceState`)},addPopstateListener:()=>(t(`addPopstateListener`),S),getHash:()=>(t(`getHash`),``)}};function T(e,t){if(_())return{pushState:v,replaceState:y,addPopstateListener:b,getLocation:e,getHash:x};let n=C(t);return{...w(t),getLocation:()=>(n(`getLocation`),``)}}function E(e,t){if(!e.getCurrentHash)return{};let n=e.getCurrentHash();return n!==(e.getCurrentContextHash?e.getCurrentContextHash():``)&&e.router.getState()?.path===t?{hash:n,force:!0,hashChange:!0}:{hash:n}}function D(e){let r=!1,i=null;function a(){if(i){let t=i;i=null,console.warn(`[${e.loggerContext}] Processing deferred popstate event`),c(t)}}function o(){let t=e.router.getState();if(!t)return;let n=t.context?.url?.hash,r=e.buildUrl(t.name,t.params,n?{hash:n}:void 0);e.browser.replaceState(t,r)}function s(t){console.error(`[${e.loggerContext}] Critical error in onPopState`,t);try{o()}catch(t){console.error(`[${e.loggerContext}] Failed to recover from critical error`,t)}}async function c(c){if(r){console.warn(`[${e.loggerContext}] Transition in progress, deferring popstate event`),i=c;return}r=!0;try{let r=f(c,e.api,e.browser);if(r)await e.api.navigateToState(r,{...e.transitionOptions,...E(e,r.path)});else if(e.allowNotFound)e.router.navigateToNotFound(e.browser.getLocation());else{let r=new t(n.ROUTE_NOT_FOUND,{path:e.browser.getLocation()});e.api.emitTransitionError(r),o()}}catch(e){if(e instanceof t)try{o()}catch{}else s(e)}finally{r=!1,a()}}return e=>void c(e)}function O(e){return{onStart:()=>{e.shared.removePopStateListener&&e.shared.removePopStateListener(),e.shared.removePopStateListener=e.browser.addPopstateListener(e.handler)},onStop:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0)},teardown:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0),e.cleanup()}}}function k(e){return encodeURI(e).replaceAll(`#`,`%23`)}function A(e){try{return decodeURIComponent(e)}catch{return e}}function j(e){let t=e;for(;t.startsWith(`#`);)t=t.slice(1);return A(t)}function M(e){let t=e.getHash();return t?A(t.startsWith(`#`)?t.slice(1):t):``}const N=new Set([`/`,`?`,`#`]);function P(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(N.has(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 F(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 I(e,t){return e?t?e===`/`?t:e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function L(e,t){let n=P(e);return F(n.pathname,t)+n.search}function R(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function z(e,t){return(n,r,i)=>{let a=I(e.buildPath(n,r),t);if(i?.hash===void 0)return a;let o=j(i.hash);return o?`${a}#${k(o)}`:a}}function B(e,t,n,r,i=!0){let a={name:``,params:{},path:``};return(o,s={},c)=>{let l=e.buildState(o,s);if(!l)throw Error(`[real-router] Cannot replace state: route "${o}" is not found`);let u=e.makeState(l.name,l.params,t.buildPath(l.name,l.params),{params:l.meta}),d;if(c?.hash!==void 0){let e=j(c.hash);d=e?`#${k(e)}`:``}else d=i?n.getHash():``;let f=r(o,s)+d;a.name=u.name,a.params=u.params,a.path=u.path,n.replaceState(a,f)}}function V(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}const H={forceDeactivate:!0,base:``},U=`popstate`,W=`browser-plugin`,G=m(H,W,{base:g}),K=Object.freeze({source:`popstate`,direction:`back`}),q=Object.freeze({source:`navigate`,direction:`forward`});function J(t,n){G(t);let i={...H,...t};i.base=r(i.base);let a=n??Y(i.base),o={forceDeactivate:i.forceDeactivate,source:U,replace:!0},s={removePopStateListener:void 0};return function(t){return X(t,e(t),i,a,o,s)}}function Y(e){let t=`\0`,n=``,r=``;return T(()=>{let{pathname:a,search:o}=globalThis.location;return a===t&&o===n?r:(t=a,n=o,r=i(F(a,e))+o,r)},`browser-plugin`)}function X(e,t,n,r,i,a){let o=t.claimContextNamespace(`browser`),s=t.claimContextNamespace(`url`),c=p(),l=R(t,r),u=z(e,n.base),d=t.extendRouter({buildUrl:u,matchUrl:e=>t.matchPath(L(e,n.base))??void 0,replaceHistoryState:B(t,e,r,u)});return{...O({browser:r,shared:a,handler:D({router:e,api:t,browser:r,allowNotFound:t.getOptions().allowNotFound,transitionOptions:i,loggerContext:W,buildUrl:u,getCurrentHash:()=>M(r),getCurrentContextHash:()=>(e.getState()?.context)?.url?.hash??``}),cleanup:()=>{l(),d(),o.release(),s.release()}}),onTransitionSuccess:(e,t,i)=>{let a=V(i,e,t),l=M(r),u=(t?.context)?.url?.hash??``,d=i.hash===void 0?l:j(i.hash);s.write(e,Object.freeze({hash:d,hashChanged:i.hashChange??d!==u}));let f=I(e.path,n.base);c(e,d?`${f}#${k(d)}`:f,a,r);let p=i.source===U;o.write(e,p?K:q)}}}export{J as browserPluginFactory,d as isState};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["isState"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/history-api.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../type-guards/dist/esm/index.mjs","../../../../shared/browser-env/popstate-utils.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/safe-browser.ts","../../../../shared/browser-env/popstate-handler.ts","../../../../shared/browser-env/url-context.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../../../shared/browser-env/plugin-utils.ts","../../src/constants.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","import type { HistoryBrowser } from \"./types.js\";\n\nexport const pushState = (state: unknown, path: string): void => {\n globalThis.history.pushState(state, \"\", path);\n};\n\nexport const replaceState = (state: unknown, path: string): void => {\n globalThis.history.replaceState(state, \"\", path);\n};\n\nexport const addPopstateListener: HistoryBrowser[\"addPopstateListener\"] = (\n fn,\n) => {\n globalThis.addEventListener(\"popstate\", fn);\n\n return () => {\n globalThis.removeEventListener(\"popstate\", fn);\n };\n};\n\nexport const getHash = (): string => globalThis.location.hash;\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","const e=[`replace`,`reload`,`force`,`forceDeactivate`,`redirected`];function t(t){if(typeof t!=`object`||!t||Array.isArray(t))return!1;let n=t;for(let t of e){let e=n[t];if(e!==void 0&&typeof e!=`boolean`)return!1}let r=n.signal;return!(r!==void 0&&!(r instanceof AbortSignal))}const n=/\\S/,r=/^[A-Z_a-z][\\w-]*(?:\\.[A-Z_a-z][\\w-]*)*$/;function i(e,t){return TypeError(`[router.${e}] ${t}`)}function a(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:r.test(e):!1}function o(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>o(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>o(e,t))}return!1}function s(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function c(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!s(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?o(e):!0}function l(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):Array.isArray(e)?e.every(e=>{let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}):!1}function u(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let n=e[t];if(!l(n))return!1}return!0}function d(e){return a(e.name)&&typeof e.path==`string`&&c(e.params)}function f(e){return typeof e!=`object`||!e?!1:d(e)}function p(e){return!(typeof e!=`object`||!e||!d(e))}function m(e){return typeof e==`string`}function h(e){return typeof e==`boolean`}function g(e,t){return e in t}function _(e){return typeof e==`number`?Number.isFinite(e):typeof e==`string`||typeof e==`boolean`}function v(e,t){if(typeof e!=`string`)throw i(t,`Route name must be a string, got ${typeof e}`);if(e!==``){if(!n.test(e))throw i(t,`Route name cannot contain only whitespace`);if(e.length>1e4)throw i(t,`Route name exceeds maximum length of 10000 characters. This is a technical safety limit.`);if(!e.startsWith(`@@`)&&!r.test(e))throw i(t,`Invalid route name \"${e}\". Each segment must start with a letter or underscore, followed by letters, numbers, underscores, or hyphens. Segments are separated by dots (e.g., \"users.profile\").`)}}function y(e){return e===null?`null`:Array.isArray(e)?`array[${e.length}]`:typeof e==`object`?`constructor`in e&&e.constructor.name!==`Object`?e.constructor.name:`object`:typeof e}function b(e,t){if(!f(e))throw TypeError(`[${t}] Invalid state structure: ${y(e)}. Expected State object with name, params, and path properties.`)}export{y as getTypeDescription,h as isBoolean,t as isNavigationOptions,g as isObjKey,c as isParams,u as isParamsStrict,_ as isPrimitiveValue,a as isRouteName,f as isState,p as isStateStrict,m as isString,v as validateRouteName,b as validateState};\n//# sourceMappingURL=index.mjs.map","import { isStateStrict as isState } from \"type-guards\";\n\nimport type { Browser } from \"./types.js\";\nimport type { State, Params } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Resolves the popstate event into a navigation-ready `State`.\n *\n * - If `history.state` is a valid router state ({name, params, path} written\n * by browser-plugin/hash-plugin during their previous navigation), it is\n * the source of truth — synthesize a fully-typed `State` from it via\n * `api.makeState`. The synthesized `transition`/`context` fields are\n * placeholders; the navigation pipeline (`completeTransition` and plugin\n * claim writes) replaces them.\n * This branch is mandatory for hash-plugin: `browser.getLocation()`\n * returns the History pathname, not the hash, so the matchPath fallback\n * below cannot extract the hash route.\n * - Otherwise (e.g. manually entered URL with no recorded state), fall\n * back to `api.matchPath(browser.getLocation())`. browser-plugin's\n * `getLocation` returns the URL pathname — this works.\n * - `undefined` when neither path produces a match.\n *\n * Replaces the previous `{ name, params }` shape so the caller can hand\n * the State directly to `router.navigateToState(state, opts)` and skip\n * the redundant `forwardState`/`buildPath` round-trip in\n * `buildNavigateState` (issue #525).\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): State | undefined {\n if (isState(evt.state)) {\n return api.makeState(evt.state.name, evt.state.params, evt.state.path);\n }\n\n return api.matchPath(browser.getLocation());\n}\n\n/**\n * Updates browser state (pushState or replaceState)\n *\n * @param state - Router state\n * @param url - URL to set\n * @param replace - Whether to replace instead of push\n * @param browser - Browser API instance\n */\nexport function updateBrowserState(\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n): void {\n const historyState = {\n name: state.name,\n params: state.params,\n path: state.path,\n };\n\n if (replace) {\n browser.replaceState(historyState, url);\n } else {\n browser.pushState(historyState, url);\n }\n}\n\n/**\n * Creates a `updateBrowserState` closure that reuses a single mutable buffer\n * across calls instead of allocating a fresh `{ name, params, path }` object\n * per push/replace.\n *\n * Why: Browsers structured-clone `history.state` synchronously inside\n * `pushState`/`replaceState`, so the caller never sees the buffer escape —\n * it can be safely overwritten before the next call. Eliminates one\n * allocation per navigation on the hot path.\n *\n * Each plugin instance must own its own buffer (do not share across plugins).\n */\nexport function createUpdateBrowserState(): (\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n) => void {\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (state, url, replace, browser) => {\n buffer.name = state.name;\n buffer.params = state.params;\n buffer.path = state.path;\n\n if (replace) {\n browser.replaceState(buffer, url);\n } else {\n browser.pushState(buffer, url);\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 { isBrowserEnvironment } from \"./detect.js\";\nimport {\n pushState,\n replaceState,\n addPopstateListener,\n getHash,\n} from \"./history-api.js\";\nimport {\n createWarnOnce,\n createHistoryFallbackBrowser,\n} from \"./ssr-fallback.js\";\n\nimport type { Browser } from \"./types.js\";\n\nexport function createSafeBrowser(\n getLocation: () => string,\n context: string,\n): Browser {\n if (isBrowserEnvironment()) {\n return {\n pushState,\n replaceState,\n addPopstateListener,\n getLocation,\n getHash,\n };\n }\n\n const warnOnce = createWarnOnce(context);\n\n return {\n ...createHistoryFallbackBrowser(context),\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"\";\n },\n };\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { getRouteFromEvent } from \"./popstate-utils.js\";\n\nimport type { Browser, SharedFactoryState } from \"./types.js\";\nimport type { Params, Plugin, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Navigation options used by the popstate handler to trigger a\n * router.navigate() call from a back/forward event. `source` identifies\n * the origin of the transition to downstream context consumers;\n * `replace: true` keeps the history stack in sync with the browser.\n */\nexport interface PopstateTransitionOptions {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n}\n\nexport interface PopstateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: Browser;\n allowNotFound: boolean;\n transitionOptions: PopstateTransitionOptions;\n loggerContext: string;\n buildUrl: (\n name: string,\n params?: Params,\n options?: { hash?: string },\n ) => string;\n /**\n * Decoded hash of the current browser location (no leading \"#\"). Defaults\n * to a no-op (returns \"\") for plugins that do not participate in URL\n * fragment tracking — namely hash-plugin, where `#` is the route delimiter.\n * (#532)\n */\n getCurrentHash?: () => string;\n /**\n * Decoded hash from the previous transition's `state.context.url.hash`\n * (no leading \"#\"). Used by the popstate handler to detect hash-only\n * navigation and add `force: true, hashChange: true` to bypass SAME_STATES.\n * Defaults to no-op (returns \"\") for hash-plugin. (#532)\n */\n getCurrentContextHash?: () => string;\n}\n\n/**\n * Hash augmentation for popstate-driven navigateToState (#532).\n * Returns a partial options object that the caller spreads on top of\n * `deps.transitionOptions`. When the handler is wired without hash support\n * (hash-plugin), both deps default to undefined and an empty object is\n * returned — preserving the legacy behavior for that plugin.\n */\nfunction resolveHashOptions(\n deps: PopstateHandlerDeps,\n matchedPath: string,\n): { hash?: string; force?: true; hashChange?: true } {\n if (!deps.getCurrentHash) {\n return {};\n }\n\n const newHash = deps.getCurrentHash();\n const prevHash = deps.getCurrentContextHash\n ? deps.getCurrentContextHash()\n : \"\";\n const hashChange =\n newHash !== prevHash && deps.router.getState()?.path === matchedPath;\n\n return hashChange\n ? { hash: newHash, force: true, hashChange: true }\n : { hash: newHash };\n}\n\nexport function createPopstateHandler(\n deps: PopstateHandlerDeps,\n): (evt: PopStateEvent) => void {\n let isTransitioning = false;\n let deferredEvent: PopStateEvent | null = null;\n\n function processDeferredEvent(): void {\n if (deferredEvent) {\n const evt = deferredEvent;\n\n deferredEvent = null;\n console.warn(\n `[${deps.loggerContext}] Processing deferred popstate event`,\n );\n void onPopState(evt);\n }\n }\n\n function rollbackUrlToCurrentState(): void {\n const currentState = deps.router.getState();\n\n /* v8 ignore next -- @preserve: router always has state after start(); defensive guard for edge cases */\n if (!currentState) {\n return;\n }\n\n // Preserve hash on rollback so guard rejection / unmatched URL on\n // popstate doesn't strip the fragment from the visible URL (#532).\n const ctxHash = (\n currentState.context as { url?: { hash?: string } } | undefined\n )?.url?.hash;\n const url = deps.buildUrl(\n currentState.name,\n currentState.params,\n ctxHash ? { hash: ctxHash } : undefined,\n );\n\n deps.browser.replaceState(currentState, url);\n }\n\n function recoverFromCriticalError(error: unknown): void {\n console.error(\n `[${deps.loggerContext}] Critical error in onPopState`,\n error,\n );\n\n try {\n rollbackUrlToCurrentState();\n } catch (recoveryError) {\n console.error(\n `[${deps.loggerContext}] Failed to recover from critical error`,\n recoveryError,\n );\n }\n }\n\n async function onPopState(evt: PopStateEvent): Promise<void> {\n if (isTransitioning) {\n console.warn(\n `[${deps.loggerContext}] Transition in progress, deferring popstate event`,\n );\n deferredEvent = evt;\n\n return;\n }\n\n isTransitioning = true;\n\n try {\n const matched = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (matched) {\n // api.navigateToState — plugin-only entry point. Preserves\n // matchSourceTrailingSlash output and skips the redundant\n // forwardState/buildPath round-trip (#525). Hash augmentation (#532)\n // extracted into resolveHashOptions so this branch stays readable.\n await deps.api.navigateToState(matched, {\n ...deps.transitionOptions,\n ...resolveHashOptions(deps, matched.path),\n });\n } else if (deps.allowNotFound) {\n deps.router.navigateToNotFound(deps.browser.getLocation());\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and sync URL\n // back to the current router state (no silent fallback to defaultRoute).\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n path: deps.browser.getLocation(),\n });\n\n deps.api.emitTransitionError(err);\n rollbackUrlToCurrentState();\n }\n } catch (error) {\n if (error instanceof RouterError) {\n // navigate() already emitted $$error — just sync URL with router state.\n // Swallow rollback errors: teardown races may remove router.buildUrl\n // while a popstate event is still queued.\n try {\n rollbackUrlToCurrentState();\n } catch {\n // noop — nothing safe to do here\n }\n } else {\n recoverFromCriticalError(error);\n }\n } finally {\n isTransitioning = false;\n processDeferredEvent();\n }\n }\n\n return (evt: PopStateEvent) => void onPopState(evt);\n}\n\nexport interface PopstateLifecycleDeps {\n browser: Browser;\n shared: SharedFactoryState;\n handler: (evt: PopStateEvent) => void;\n cleanup: () => void;\n}\n\nexport function createPopstateLifecycle(\n deps: PopstateLifecycleDeps,\n): Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\"> {\n return {\n onStart: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n }\n\n deps.shared.removePopStateListener = deps.browser.addPopstateListener(\n deps.handler,\n );\n },\n\n onStop: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n },\n\n teardown: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n\n deps.cleanup();\n },\n };\n}\n","/**\n * URL fragment (\"hash\") shared layer (#532).\n *\n * Both URL plugins (navigation-plugin, browser-plugin) claim the `\"url\"`\n * `state.context` namespace and write `UrlContext` on every transition.\n * Mutually exclusive at runtime — only one URL plugin is installed per router.\n *\n * Hash form: decoded, no leading \"#\" — symmetric to `params` (no leading \"?\").\n * Encoding to/from URL form happens at the boundary (URL build / URL parse).\n */\n\nexport interface UrlContext {\n /** Decoded fragment, no leading \"#\". Empty string when URL has no fragment. */\n hash: string;\n /** Whether `hash` differs from the previous transition's `state.context.url.hash`. */\n hashChanged: boolean;\n}\n\n/**\n * Encode for URL fragment per RFC 3986: preserves sub-delims (`&`, `=`, `?`,\n * `:`, etc.) and the path/query characters that `encodeURI` already leaves\n * alone. Defensively percent-escapes `#` (a stray `#` in a decoded fragment\n * would otherwise terminate the fragment in the rendered URL).\n *\n * `encodeURIComponent` over-encodes RFC-3986 sub-delims (`&` → `%26`) and is\n * therefore wrong for fragments.\n */\nexport function encodeHashFragment(decoded: string): string {\n return encodeURI(decoded).replaceAll(\"#\", \"%23\");\n}\n\n/**\n * Decode a percent-encoded fragment. Falls back to the raw input on malformed\n * escapes — matches the resilience pattern in scroll-restore.\n */\nexport function decodeHashFragment(encoded: string): string {\n try {\n return decodeURIComponent(encoded);\n } catch {\n return encoded;\n }\n}\n\n/**\n * Normalize user-provided hash input: strip ALL leading \"#\" characters, then\n * decode. Defensive against `<Link hash=\"#section\">` — the prop is documented\n * to accept the fragment name without \"#\", but we accept both gracefully.\n *\n * Stripping a single \"#\" would leave the function non-idempotent on\n * pathological inputs like `\"##section\"` (caller's accidental double-hash,\n * concatenation bugs). Property test G9 in `hash-encoding.properties.ts`\n * locks in idempotence — `normalize(normalize(x)) === normalize(x)`.\n */\nexport function normalizeHashInput(input: string): string {\n let stripped = input;\n\n while (stripped.startsWith(\"#\")) {\n stripped = stripped.slice(1);\n }\n\n return decodeHashFragment(stripped);\n}\n\n/**\n * Read the current browser hash in decoded form, no leading \"#\".\n * Accepts any object with a `getHash()` method — works for both `Browser`\n * (History API) and `NavigationBrowser` (Navigation API). SSR-safe via the\n * abstractions, which return `\"\"` outside a real browser.\n */\nexport function getDecodedHash(browser: { getHash: () => string }): string {\n const raw = browser.getHash();\n\n if (!raw) {\n return \"\";\n }\n\n const stripped = raw.startsWith(\"#\") ? raw.slice(1) : raw;\n\n return decodeHashFragment(stripped);\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 { decodeHashFragment } from \"./url-context.js\";\nimport { 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 * Like `urlToPath` but also returns the decoded URL fragment (#532).\n *\n * Used by URL plugins to extract `event.destination.url`'s hash without\n * dropping it the way `urlToPath` does. The hash is returned in decoded form\n * with no leading \"#\" — same form as stored in `state.context.url.hash`.\n */\nexport function urlToPathAndHash(\n url: string,\n base: string,\n): { path: string; hash: string } {\n const parsed = safeParseUrl(url);\n const path = extractPath(parsed.pathname, base) + parsed.search;\n const hash = parsed.hash ? decodeHashFragment(parsed.hash.slice(1)) : \"\";\n\n return { path, hash };\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 { encodeHashFragment, normalizeHashInput } from \"./url-context.js\";\nimport { buildUrl } from \"./url-utils.js\";\n\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 interface LocationSource {\n getLocation: () => string;\n}\n\n/**\n * Minimal browser surface needed by `createReplaceHistoryState`.\n *\n * Both `Browser` (History API) and navigation-plugin's `NavigationBrowser`\n * (Navigation API) satisfy this structurally — the function never needs\n * `pushState`/`addPopstateListener`, only the replace path.\n */\nexport interface ReplaceStateBrowser {\n replaceState: (state: unknown, url: string) => void;\n getHash: () => string;\n}\n\n/**\n * Hash override option for `replaceHistoryState` (#532). Tri-state semantics:\n * `undefined` — preserve the current browser hash (legacy behavior, default)\n * `\"\"` — explicitly clear the fragment\n * non-empty — explicitly set the fragment (decoded form, no leading \"#\")\n */\nexport interface ReplaceHistoryStateOptions {\n hash?: string;\n}\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: LocationSource,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n// Shared `buildUrl` extension for browser-plugin and navigation-plugin.\n// Composes router.buildPath + base prefixing + tri-state hash (#532) into the\n// single function the plugins register via `api.extendRouter({ buildUrl })`.\nexport function createPluginBuildUrl(\n router: Router,\n base: string,\n): (route: string, params?: Params, opts?: { hash?: string }) => string {\n return (route, params, opts) => {\n const path = router.buildPath(route, params);\n const url = buildUrl(path, base);\n\n if (opts?.hash === undefined) {\n return url;\n }\n\n const norm = normalizeHashInput(opts.hash);\n\n return norm ? `${url}#${encodeHashFragment(norm)}` : url;\n };\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: ReplaceStateBrowser,\n buildUrlFn: (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n ) => string,\n preserveHash = true,\n): (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n) => void {\n // Reusable buffer — browsers structured-clone state synchronously inside\n // replaceState, so the buffer never escapes. Eliminates one allocation per\n // navigation on the hot path. (Mirrors createUpdateBrowserState.)\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (\n name: string,\n params: Params = {},\n options?: ReplaceHistoryStateOptions,\n ) => {\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 // Tri-state hash semantics (#532):\n // options.hash === undefined → preserve (legacy behavior, controlled by\n // preserveHash flag — true for browser/\n // navigation plugins, false for hash-plugin)\n // options.hash === \"\" → explicitly clear\n // options.hash === \"value\" → explicitly set\n let hashSegment: string;\n\n if (options?.hash !== undefined) {\n const norm = normalizeHashInput(options.hash);\n\n hashSegment = norm ? `#${encodeHashFragment(norm)}` : \"\";\n } else if (preserveHash) {\n hashSegment = browser.getHash();\n } else {\n hashSegment = \"\";\n }\n\n // Pass hash through buildUrl when the plugin understands it (avoids\n // double-append). Hash-plugin's buildUrl ignores the option and warns,\n // so call without options here for semantic clarity — but the result is\n // identical because hashSegment is \"\" in that branch (preserveHash=false).\n const url = buildUrlFn(name, params) + hashSegment;\n\n buffer.name = builtState.name;\n buffer.params = builtState.params;\n buffer.path = builtState.path;\n\n browser.replaceState(buffer, url);\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","import type { BrowserPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<BrowserPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by browser events.\n * Used to distinguish browser-initiated navigation (back/forward buttons)\n * from programmatic navigation (router.navigate()).\n */\nexport const POPSTATE_SOURCE = \"popstate\";\n\nexport const LOGGER_CONTEXT = \"browser-plugin\";\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { BrowserPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<BrowserPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport {\n buildUrl,\n createPluginBuildUrl,\n createPopstateHandler,\n createPopstateLifecycle,\n createReplaceHistoryState,\n createSafeBrowser,\n createStartInterceptor,\n createUpdateBrowserState,\n encodeHashFragment,\n extractPath,\n getDecodedHash,\n normalizeBase,\n normalizeHashInput,\n safelyEncodePath,\n shouldReplaceHistory,\n urlToPath,\n} from \"./browser-env\";\nimport { defaultOptions, LOGGER_CONTEXT, POPSTATE_SOURCE } from \"./constants\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n Browser,\n PopstateTransitionOptions,\n SharedFactoryState,\n UrlContext,\n} from \"./browser-env\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst FROZEN_POPSTATE: BrowserContext = Object.freeze({\n source: \"popstate\",\n direction: \"back\",\n});\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({\n source: \"navigate\",\n direction: \"forward\",\n});\n\nexport function browserPluginFactory(\n opts?: Partial<BrowserPluginOptions>,\n browser?: Browser,\n): PluginFactory {\n validateOptions(opts);\n\n const options: Required<BrowserPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createDefaultBrowser(options.base);\n\n const transitionOptions = {\n forceDeactivate: options.forceDeactivate,\n source: POPSTATE_SOURCE,\n replace: true as const,\n };\n\n const shared: SharedFactoryState = { removePopStateListener: undefined };\n\n return function browserPlugin(routerBase) {\n return createBrowserPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n };\n}\n\n/**\n * Creates the default `Browser` for the plugin, with a memoized `getLocation`\n * that skips re-running `extractPath`/`safelyEncodePath` when neither\n * `pathname` nor `search` has changed since the last call (#8.2 A7).\n *\n * Initial sentinel is `\"\\0\"` — a NUL byte cannot appear in a real\n * `location.pathname`, so the first call is always a miss without needing a\n * separate \"primed\" flag.\n */\nfunction createDefaultBrowser(base: string): Browser {\n let cachedPathname = \"\\0\";\n let cachedSearch = \"\";\n let cachedResult = \"\";\n\n return createSafeBrowser(() => {\n const { pathname, search } = globalThis.location;\n\n if (pathname === cachedPathname && search === cachedSearch) {\n return cachedResult;\n }\n\n cachedPathname = pathname;\n cachedSearch = search;\n cachedResult = safelyEncodePath(extractPath(pathname, base)) + search;\n\n return cachedResult;\n }, \"browser-plugin\");\n}\n\nfunction createBrowserPlugin(\n router: Router,\n api: PluginApi,\n options: Required<BrowserPluginOptions>,\n browser: Browser,\n transitionOptions: PopstateTransitionOptions,\n shared: SharedFactoryState,\n): Plugin {\n const claim = api.claimContextNamespace(\"browser\");\n // Shared URL namespace (#532) — both navigation-plugin and browser-plugin\n // claim \"url\"; mutually exclusive at runtime.\n const urlClaim = api.claimContextNamespace(\"url\") as {\n write: (state: State, value: UrlContext) => void;\n release: () => void;\n };\n const updateState = createUpdateBrowserState();\n const removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = createPluginBuildUrl(router, options.base);\n\n const 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 ),\n });\n\n const handler = createPopstateHandler({\n router,\n api,\n browser,\n allowNotFound: api.getOptions().allowNotFound,\n transitionOptions,\n loggerContext: LOGGER_CONTEXT,\n buildUrl: pluginBuildUrl,\n // Hash bridging (#532). popstate doesn't carry a URL — we sample\n // location.hash after the browser has updated to the destination.\n getCurrentHash: () => getDecodedHash(browser),\n getCurrentContextHash: () =>\n (router.getState()?.context as { url?: { hash?: string } } | undefined)\n ?.url?.hash ?? \"\",\n });\n\n const lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n removeStartInterceptor();\n removeExtensions();\n claim.release();\n urlClaim.release();\n },\n });\n\n return {\n ...lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n const replaceHistory = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n // Tri-state hash resolution (#532).\n // navOptions.hash === undefined → preserve current browser hash\n // navOptions.hash === \"\" → explicitly clear\n // navOptions.hash === \"value\" → explicitly set\n //\n // The \"preserve\" branch reads location.hash from the browser, not\n // fromState.context.url.hash — captures dynamic fragment changes\n // (anchor clicks, manual location.hash assignment) made outside the\n // plugin. hashChanged compares the chosen hash against the published\n // previous hash so subscribers see a true signal.\n const browserHash = getDecodedHash(browser);\n const publishedPrevHash =\n (fromState?.context as { url?: { hash?: string } } | undefined)?.url\n ?.hash ?? \"\";\n\n const hash =\n navOptions.hash === undefined\n ? browserHash\n : normalizeHashInput(navOptions.hash);\n\n urlClaim.write(\n toState,\n Object.freeze({\n hash,\n hashChanged: navOptions.hashChange ?? hash !== publishedPrevHash,\n }),\n );\n\n const url = buildUrl(toState.path, options.base);\n const finalUrl = hash ? `${url}#${encodeHashFragment(hash)}` : url;\n\n updateState(toState, finalUrl, replaceHistory, browser);\n\n const isPopstate = navOptions.source === POPSTATE_SOURCE;\n\n claim.write(toState, isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE);\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["isState"],"sources":["../../../../shared/browser-env/utils.ts","../../../type-guards/dist/esm/index.mjs","../../../../shared/browser-env/popstate-utils.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/history-api.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/safe-browser.ts","../../../../shared/browser-env/popstate-handler.ts","../../../../shared/browser-env/url-context.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../../../shared/browser-env/plugin-utils.ts","../../src/constants.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["/**\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","const e=[`replace`,`reload`,`force`,`forceDeactivate`,`redirected`];function t(t){if(typeof t!=`object`||!t||Array.isArray(t))return!1;let n=t;for(let t of e){let e=n[t];if(e!==void 0&&typeof e!=`boolean`)return!1}let r=n.signal;return!(r!==void 0&&!(r instanceof AbortSignal))}const n=/\\S/,r=/^[A-Z_a-z][\\w-]*(?:\\.[A-Z_a-z][\\w-]*)*$/;function i(e,t){return TypeError(`[router.${e}] ${t}`)}function a(e){return typeof e==`string`?e===``?!0:e.length>1e4?!1:e.startsWith(`@@`)?!0:r.test(e):!1}function o(e,t=new WeakSet){if(e==null)return!0;let n=typeof e;if(n===`string`||n===`boolean`)return!0;if(n===`number`)return Number.isFinite(e);if(n===`function`||n===`symbol`)return!1;if(Array.isArray(e))return t.has(e)?!1:(t.add(e),e.every(e=>o(e,t)));if(n===`object`){if(t.has(e))return!1;t.add(e);let n=Object.getPrototypeOf(e);return n!==null&&n!==Object.prototype?!1:Object.values(e).every(e=>o(e,t))}return!1}function s(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}function c(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype)return!1;let n=!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let r=e[t];if(!s(r)){let e=typeof r;if(e===`function`||e===`symbol`)return!1;n=!0;break}}return n?o(e):!0}function l(e){if(e==null)return!0;let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):Array.isArray(e)?e.every(e=>{let t=typeof e;return t===`string`||t===`boolean`?!0:t===`number`?Number.isFinite(e):!1}):!1}function u(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;for(let t in e){if(!Object.hasOwn(e,t))continue;let n=e[t];if(!l(n))return!1}return!0}function d(e){return a(e.name)&&typeof e.path==`string`&&c(e.params)}function f(e){return typeof e!=`object`||!e?!1:d(e)}function p(e){return!(typeof e!=`object`||!e||!d(e))}function m(e){return typeof e==`string`}function h(e){return typeof e==`boolean`}function g(e,t){return e in t}function _(e){return typeof e==`number`?Number.isFinite(e):typeof e==`string`||typeof e==`boolean`}function v(e,t){if(typeof e!=`string`)throw i(t,`Route name must be a string, got ${typeof e}`);if(e!==``){if(!n.test(e))throw i(t,`Route name cannot contain only whitespace`);if(e.length>1e4)throw i(t,`Route name exceeds maximum length of 10000 characters. This is a technical safety limit.`);if(!e.startsWith(`@@`)&&!r.test(e))throw i(t,`Invalid route name \"${e}\". Each segment must start with a letter or underscore, followed by letters, numbers, underscores, or hyphens. Segments are separated by dots (e.g., \"users.profile\").`)}}function y(e){return e===null?`null`:Array.isArray(e)?`array[${e.length}]`:typeof e==`object`?`constructor`in e&&e.constructor.name!==`Object`?e.constructor.name:`object`:typeof e}function b(e,t){if(!f(e))throw TypeError(`[${t}] Invalid state structure: ${y(e)}. Expected State object with name, params, and path properties.`)}export{y as getTypeDescription,h as isBoolean,t as isNavigationOptions,g as isObjKey,c as isParams,u as isParamsStrict,_ as isPrimitiveValue,a as isRouteName,f as isState,p as isStateStrict,m as isString,v as validateRouteName,b as validateState};\n//# sourceMappingURL=index.mjs.map","import { isStateStrict as isState } from \"type-guards\";\n\nimport type { Browser } from \"./types.js\";\nimport type { State, Params } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Resolves the popstate event into a navigation-ready `State`.\n *\n * - If `history.state` is a valid router state ({name, params, path} written\n * by browser-plugin/hash-plugin during their previous navigation), it is\n * the source of truth — synthesize a fully-typed `State` from it via\n * `api.makeState`. The synthesized `transition`/`context` fields are\n * placeholders; the navigation pipeline (`completeTransition` and plugin\n * claim writes) replaces them.\n * This branch is mandatory for hash-plugin: `browser.getLocation()`\n * returns the History pathname, not the hash, so the matchPath fallback\n * below cannot extract the hash route.\n * - Otherwise (e.g. manually entered URL with no recorded state), fall\n * back to `api.matchPath(browser.getLocation())`. browser-plugin's\n * `getLocation` returns the URL pathname — this works.\n * - `undefined` when neither path produces a match.\n *\n * Replaces the previous `{ name, params }` shape so the caller can hand\n * the State directly to `router.navigateToState(state, opts)` and skip\n * the redundant `forwardState`/`buildPath` round-trip in\n * `buildNavigateState` (issue #525).\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): State | undefined {\n if (isState(evt.state)) {\n return api.makeState(evt.state.name, evt.state.params, evt.state.path);\n }\n\n return api.matchPath(browser.getLocation());\n}\n\n/**\n * Updates browser state (pushState or replaceState)\n *\n * @param state - Router state\n * @param url - URL to set\n * @param replace - Whether to replace instead of push\n * @param browser - Browser API instance\n */\nexport function updateBrowserState(\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n): void {\n const historyState = {\n name: state.name,\n params: state.params,\n path: state.path,\n };\n\n if (replace) {\n browser.replaceState(historyState, url);\n } else {\n browser.pushState(historyState, url);\n }\n}\n\n/**\n * Creates a `updateBrowserState` closure that reuses a single mutable buffer\n * across calls instead of allocating a fresh `{ name, params, path }` object\n * per push/replace.\n *\n * Why: Browsers structured-clone `history.state` synchronously inside\n * `pushState`/`replaceState`, so the caller never sees the buffer escape —\n * it can be safely overwritten before the next call. Eliminates one\n * allocation per navigation on the hot path.\n *\n * Each plugin instance must own its own buffer (do not share across plugins).\n */\nexport function createUpdateBrowserState(): (\n state: State,\n url: string,\n replace: boolean,\n browser: Browser,\n) => void {\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (state, url, replace, browser) => {\n buffer.name = state.name;\n buffer.params = state.params;\n buffer.path = state.path;\n\n if (replace) {\n browser.replaceState(buffer, url);\n } else {\n browser.pushState(buffer, url);\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","export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","import type { HistoryBrowser } from \"./types.js\";\n\nexport const pushState = (state: unknown, path: string): void => {\n globalThis.history.pushState(state, \"\", path);\n};\n\nexport const replaceState = (state: unknown, path: string): void => {\n globalThis.history.replaceState(state, \"\", path);\n};\n\nexport const addPopstateListener: HistoryBrowser[\"addPopstateListener\"] = (\n fn,\n) => {\n globalThis.addEventListener(\"popstate\", fn);\n\n return () => {\n globalThis.removeEventListener(\"popstate\", fn);\n };\n};\n\nexport const getHash = (): string => globalThis.location.hash;\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","import { isBrowserEnvironment } from \"./detect.js\";\nimport {\n pushState,\n replaceState,\n addPopstateListener,\n getHash,\n} from \"./history-api.js\";\nimport {\n createWarnOnce,\n createHistoryFallbackBrowser,\n} from \"./ssr-fallback.js\";\n\nimport type { Browser } from \"./types.js\";\n\nexport function createSafeBrowser(\n getLocation: () => string,\n context: string,\n): Browser {\n if (isBrowserEnvironment()) {\n return {\n pushState,\n replaceState,\n addPopstateListener,\n getLocation,\n getHash,\n };\n }\n\n const warnOnce = createWarnOnce(context);\n\n return {\n ...createHistoryFallbackBrowser(context),\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"\";\n },\n };\n}\n","import { errorCodes, RouterError } from \"@real-router/core\";\n\nimport { getRouteFromEvent } from \"./popstate-utils.js\";\n\nimport type { Browser, SharedFactoryState } from \"./types.js\";\nimport type { Params, Plugin, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Navigation options used by the popstate handler to trigger a\n * router.navigate() call from a back/forward event. `source` identifies\n * the origin of the transition to downstream context consumers;\n * `replace: true` keeps the history stack in sync with the browser.\n */\nexport interface PopstateTransitionOptions {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n}\n\nexport interface PopstateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: Browser;\n allowNotFound: boolean;\n transitionOptions: PopstateTransitionOptions;\n loggerContext: string;\n buildUrl: (\n name: string,\n params?: Params,\n options?: { hash?: string },\n ) => string;\n /**\n * Decoded hash of the current browser location (no leading \"#\"). Defaults\n * to a no-op (returns \"\") for plugins that do not participate in URL\n * fragment tracking — namely hash-plugin, where `#` is the route delimiter.\n * (#532)\n */\n getCurrentHash?: () => string;\n /**\n * Decoded hash from the previous transition's `state.context.url.hash`\n * (no leading \"#\"). Used by the popstate handler to detect hash-only\n * navigation and add `force: true, hashChange: true` to bypass SAME_STATES.\n * Defaults to no-op (returns \"\") for hash-plugin. (#532)\n */\n getCurrentContextHash?: () => string;\n}\n\n/**\n * Hash augmentation for popstate-driven navigateToState (#532).\n * Returns a partial options object that the caller spreads on top of\n * `deps.transitionOptions`. When the handler is wired without hash support\n * (hash-plugin), both deps default to undefined and an empty object is\n * returned — preserving the legacy behavior for that plugin.\n */\nfunction resolveHashOptions(\n deps: PopstateHandlerDeps,\n matchedPath: string,\n): { hash?: string; force?: true; hashChange?: true } {\n if (!deps.getCurrentHash) {\n return {};\n }\n\n const newHash = deps.getCurrentHash();\n const prevHash = deps.getCurrentContextHash\n ? deps.getCurrentContextHash()\n : \"\";\n const hashChange =\n newHash !== prevHash && deps.router.getState()?.path === matchedPath;\n\n return hashChange\n ? { hash: newHash, force: true, hashChange: true }\n : { hash: newHash };\n}\n\nexport function createPopstateHandler(\n deps: PopstateHandlerDeps,\n): (evt: PopStateEvent) => void {\n let isTransitioning = false;\n let deferredEvent: PopStateEvent | null = null;\n\n function processDeferredEvent(): void {\n if (deferredEvent) {\n const evt = deferredEvent;\n\n deferredEvent = null;\n console.warn(\n `[${deps.loggerContext}] Processing deferred popstate event`,\n );\n void onPopState(evt);\n }\n }\n\n function rollbackUrlToCurrentState(): void {\n const currentState = deps.router.getState();\n\n /* v8 ignore next -- @preserve: router always has state after start(); defensive guard for edge cases */\n if (!currentState) {\n return;\n }\n\n // Preserve hash on rollback so guard rejection / unmatched URL on\n // popstate doesn't strip the fragment from the visible URL (#532).\n const ctxHash = (\n currentState.context as { url?: { hash?: string } } | undefined\n )?.url?.hash;\n const url = deps.buildUrl(\n currentState.name,\n currentState.params,\n ctxHash ? { hash: ctxHash } : undefined,\n );\n\n deps.browser.replaceState(currentState, url);\n }\n\n function recoverFromCriticalError(error: unknown): void {\n console.error(\n `[${deps.loggerContext}] Critical error in onPopState`,\n error,\n );\n\n try {\n rollbackUrlToCurrentState();\n } catch (recoveryError) {\n console.error(\n `[${deps.loggerContext}] Failed to recover from critical error`,\n recoveryError,\n );\n }\n }\n\n async function onPopState(evt: PopStateEvent): Promise<void> {\n if (isTransitioning) {\n console.warn(\n `[${deps.loggerContext}] Transition in progress, deferring popstate event`,\n );\n deferredEvent = evt;\n\n return;\n }\n\n isTransitioning = true;\n\n try {\n const matched = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (matched) {\n // api.navigateToState — plugin-only entry point. Preserves\n // matchSourceTrailingSlash output and skips the redundant\n // forwardState/buildPath round-trip (#525). Hash augmentation (#532)\n // extracted into resolveHashOptions so this branch stays readable.\n await deps.api.navigateToState(matched, {\n ...deps.transitionOptions,\n ...resolveHashOptions(deps, matched.path),\n });\n } else if (deps.allowNotFound) {\n deps.router.navigateToNotFound(deps.browser.getLocation());\n } else {\n // Strict mode — unmatched URL is an error. Emit $$error and sync URL\n // back to the current router state (no silent fallback to defaultRoute).\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n path: deps.browser.getLocation(),\n });\n\n deps.api.emitTransitionError(err);\n rollbackUrlToCurrentState();\n }\n } catch (error) {\n if (error instanceof RouterError) {\n // navigate() already emitted $$error — just sync URL with router state.\n // Swallow rollback errors: teardown races may remove router.buildUrl\n // while a popstate event is still queued.\n try {\n rollbackUrlToCurrentState();\n } catch {\n // noop — nothing safe to do here\n }\n } else {\n recoverFromCriticalError(error);\n }\n } finally {\n isTransitioning = false;\n processDeferredEvent();\n }\n }\n\n return (evt: PopStateEvent) => void onPopState(evt);\n}\n\nexport interface PopstateLifecycleDeps {\n browser: Browser;\n shared: SharedFactoryState;\n handler: (evt: PopStateEvent) => void;\n cleanup: () => void;\n}\n\nexport function createPopstateLifecycle(\n deps: PopstateLifecycleDeps,\n): Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\"> {\n return {\n onStart: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n }\n\n deps.shared.removePopStateListener = deps.browser.addPopstateListener(\n deps.handler,\n );\n },\n\n onStop: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n },\n\n teardown: () => {\n if (deps.shared.removePopStateListener) {\n deps.shared.removePopStateListener();\n deps.shared.removePopStateListener = undefined;\n }\n\n deps.cleanup();\n },\n };\n}\n","/**\n * URL fragment (\"hash\") shared layer (#532).\n *\n * Both URL plugins (navigation-plugin, browser-plugin) claim the `\"url\"`\n * `state.context` namespace and write `UrlContext` on every transition.\n * Mutually exclusive at runtime — only one URL plugin is installed per router.\n *\n * Hash form: decoded, no leading \"#\" — symmetric to `params` (no leading \"?\").\n * Encoding to/from URL form happens at the boundary (URL build / URL parse).\n */\n\nexport interface UrlContext {\n /** Decoded fragment, no leading \"#\". Empty string when URL has no fragment. */\n hash: string;\n /** Whether `hash` differs from the previous transition's `state.context.url.hash`. */\n hashChanged: boolean;\n}\n\n/**\n * Encode for URL fragment per RFC 3986: preserves sub-delims (`&`, `=`, `?`,\n * `:`, etc.) and the path/query characters that `encodeURI` already leaves\n * alone. Defensively percent-escapes `#` (a stray `#` in a decoded fragment\n * would otherwise terminate the fragment in the rendered URL).\n *\n * `encodeURIComponent` over-encodes RFC-3986 sub-delims (`&` → `%26`) and is\n * therefore wrong for fragments.\n */\nexport function encodeHashFragment(decoded: string): string {\n return encodeURI(decoded).replaceAll(\"#\", \"%23\");\n}\n\n/**\n * Decode a percent-encoded fragment. Falls back to the raw input on malformed\n * escapes — matches the resilience pattern in scroll-restore.\n */\nexport function decodeHashFragment(encoded: string): string {\n try {\n return decodeURIComponent(encoded);\n } catch {\n return encoded;\n }\n}\n\n/**\n * Normalize user-provided hash input: strip ALL leading \"#\" characters, then\n * decode. Defensive against `<Link hash=\"#section\">` — the prop is documented\n * to accept the fragment name without \"#\", but we accept both gracefully.\n *\n * Stripping a single \"#\" would leave the function non-idempotent on\n * pathological inputs like `\"##section\"` (caller's accidental double-hash,\n * concatenation bugs). Property test G9 in `hash-encoding.properties.ts`\n * locks in idempotence — `normalize(normalize(x)) === normalize(x)`.\n */\nexport function normalizeHashInput(input: string): string {\n let stripped = input;\n\n while (stripped.startsWith(\"#\")) {\n stripped = stripped.slice(1);\n }\n\n return decodeHashFragment(stripped);\n}\n\n/**\n * Read the current browser hash in decoded form, no leading \"#\".\n * Accepts any object with a `getHash()` method — works for both `Browser`\n * (History API) and `NavigationBrowser` (Navigation API). SSR-safe via the\n * abstractions, which return `\"\"` outside a real browser.\n */\nexport function getDecodedHash(browser: { getHash: () => string }): string {\n const raw = browser.getHash();\n\n if (!raw) {\n return \"\";\n }\n\n const stripped = raw.startsWith(\"#\") ? raw.slice(1) : raw;\n\n return decodeHashFragment(stripped);\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 */\nconst URL_DELIMITERS: ReadonlySet<string> = new Set([\"/\", \"?\", \"#\"]);\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 (URL_DELIMITERS.has(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 { decodeHashFragment } from \"./url-context.js\";\nimport { 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 * Like `urlToPath` but also returns the decoded URL fragment (#532).\n *\n * Used by URL plugins to extract `event.destination.url`'s hash without\n * dropping it the way `urlToPath` does. The hash is returned in decoded form\n * with no leading \"#\" — same form as stored in `state.context.url.hash`.\n */\nexport function urlToPathAndHash(\n url: string,\n base: string,\n): { path: string; hash: string } {\n const parsed = safeParseUrl(url);\n const path = extractPath(parsed.pathname, base) + parsed.search;\n const hash = parsed.hash ? decodeHashFragment(parsed.hash.slice(1)) : \"\";\n\n return { path, hash };\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 { encodeHashFragment, normalizeHashInput } from \"./url-context.js\";\nimport { buildUrl } from \"./url-utils.js\";\n\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 interface LocationSource {\n getLocation: () => string;\n}\n\n/**\n * Minimal browser surface needed by `createReplaceHistoryState`.\n *\n * Both `Browser` (History API) and navigation-plugin's `NavigationBrowser`\n * (Navigation API) satisfy this structurally — the function never needs\n * `pushState`/`addPopstateListener`, only the replace path.\n */\nexport interface ReplaceStateBrowser {\n replaceState: (state: unknown, url: string) => void;\n getHash: () => string;\n}\n\n/**\n * Hash override option for `replaceHistoryState` (#532). Tri-state semantics:\n * `undefined` — preserve the current browser hash (legacy behavior, default)\n * `\"\"` — explicitly clear the fragment\n * non-empty — explicitly set the fragment (decoded form, no leading \"#\")\n */\nexport interface ReplaceHistoryStateOptions {\n hash?: string;\n}\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: LocationSource,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n// Shared `buildUrl` extension for browser-plugin and navigation-plugin.\n// Composes router.buildPath + base prefixing + tri-state hash (#532) into the\n// single function the plugins register via `api.extendRouter({ buildUrl })`.\nexport function createPluginBuildUrl(\n router: Router,\n base: string,\n): (route: string, params?: Params, opts?: { hash?: string }) => string {\n return (route, params, opts) => {\n const path = router.buildPath(route, params);\n const url = buildUrl(path, base);\n\n if (opts?.hash === undefined) {\n return url;\n }\n\n const norm = normalizeHashInput(opts.hash);\n\n return norm ? `${url}#${encodeHashFragment(norm)}` : url;\n };\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: ReplaceStateBrowser,\n buildUrlFn: (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n ) => string,\n preserveHash = true,\n): (\n name: string,\n params?: Params,\n options?: ReplaceHistoryStateOptions,\n) => void {\n // Reusable buffer — browsers structured-clone state synchronously inside\n // replaceState, so the buffer never escapes. Eliminates one allocation per\n // navigation on the hot path. (Mirrors createUpdateBrowserState.)\n const buffer = {\n name: \"\",\n params: {} as Params,\n path: \"\",\n };\n\n return (\n name: string,\n params: Params = {},\n options?: ReplaceHistoryStateOptions,\n ) => {\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 // Tri-state hash semantics (#532):\n // options.hash === undefined → preserve (legacy behavior, controlled by\n // preserveHash flag — true for browser/\n // navigation plugins, false for hash-plugin)\n // options.hash === \"\" → explicitly clear\n // options.hash === \"value\" → explicitly set\n let hashSegment: string;\n\n if (options?.hash !== undefined) {\n const norm = normalizeHashInput(options.hash);\n\n hashSegment = norm ? `#${encodeHashFragment(norm)}` : \"\";\n } else if (preserveHash) {\n hashSegment = browser.getHash();\n } else {\n hashSegment = \"\";\n }\n\n // Pass hash through buildUrl when the plugin understands it (avoids\n // double-append). Hash-plugin's buildUrl ignores the option and warns,\n // so call without options here for semantic clarity — but the result is\n // identical because hashSegment is \"\" in that branch (preserveHash=false).\n const url = buildUrlFn(name, params) + hashSegment;\n\n buffer.name = builtState.name;\n buffer.params = builtState.params;\n buffer.path = builtState.path;\n\n browser.replaceState(buffer, url);\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","import type { BrowserPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<BrowserPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by browser events.\n * Used to distinguish browser-initiated navigation (back/forward buttons)\n * from programmatic navigation (router.navigate()).\n */\nexport const POPSTATE_SOURCE = \"popstate\";\n\nexport const LOGGER_CONTEXT = \"browser-plugin\";\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { BrowserPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<BrowserPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n { base: safeBaseRule },\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport {\n buildUrl,\n createPluginBuildUrl,\n createPopstateHandler,\n createPopstateLifecycle,\n createReplaceHistoryState,\n createSafeBrowser,\n createStartInterceptor,\n createUpdateBrowserState,\n encodeHashFragment,\n extractPath,\n getDecodedHash,\n normalizeBase,\n normalizeHashInput,\n safelyEncodePath,\n shouldReplaceHistory,\n urlToPath,\n} from \"./browser-env\";\nimport { defaultOptions, LOGGER_CONTEXT, POPSTATE_SOURCE } from \"./constants\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n Browser,\n PopstateTransitionOptions,\n SharedFactoryState,\n UrlContext,\n} from \"./browser-env\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst FROZEN_POPSTATE: BrowserContext = Object.freeze({\n source: \"popstate\",\n direction: \"back\",\n});\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({\n source: \"navigate\",\n direction: \"forward\",\n});\n\nexport function browserPluginFactory(\n opts?: Partial<BrowserPluginOptions>,\n browser?: Browser,\n): PluginFactory {\n validateOptions(opts);\n\n const options: Required<BrowserPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createDefaultBrowser(options.base);\n\n const transitionOptions = {\n forceDeactivate: options.forceDeactivate,\n source: POPSTATE_SOURCE,\n replace: true as const,\n };\n\n const shared: SharedFactoryState = { removePopStateListener: undefined };\n\n return function browserPlugin(routerBase) {\n return createBrowserPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n };\n}\n\n/**\n * Creates the default `Browser` for the plugin, with a memoized `getLocation`\n * that skips re-running `extractPath`/`safelyEncodePath` when neither\n * `pathname` nor `search` has changed since the last call (#8.2 A7).\n *\n * Initial sentinel is `\"\\0\"` — a NUL byte cannot appear in a real\n * `location.pathname`, so the first call is always a miss without needing a\n * separate \"primed\" flag.\n */\nfunction createDefaultBrowser(base: string): Browser {\n let cachedPathname = \"\\0\";\n let cachedSearch = \"\";\n let cachedResult = \"\";\n\n return createSafeBrowser(() => {\n const { pathname, search } = globalThis.location;\n\n if (pathname === cachedPathname && search === cachedSearch) {\n return cachedResult;\n }\n\n cachedPathname = pathname;\n cachedSearch = search;\n cachedResult = safelyEncodePath(extractPath(pathname, base)) + search;\n\n return cachedResult;\n }, \"browser-plugin\");\n}\n\nfunction createBrowserPlugin(\n router: Router,\n api: PluginApi,\n options: Required<BrowserPluginOptions>,\n browser: Browser,\n transitionOptions: PopstateTransitionOptions,\n shared: SharedFactoryState,\n): Plugin {\n const claim = api.claimContextNamespace(\"browser\");\n // Shared URL namespace (#532) — both navigation-plugin and browser-plugin\n // claim \"url\"; mutually exclusive at runtime.\n const urlClaim = api.claimContextNamespace(\"url\") as {\n write: (state: State, value: UrlContext) => void;\n release: () => void;\n };\n const updateState = createUpdateBrowserState();\n const removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = createPluginBuildUrl(router, options.base);\n\n const 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 ),\n });\n\n const handler = createPopstateHandler({\n router,\n api,\n browser,\n allowNotFound: api.getOptions().allowNotFound,\n transitionOptions,\n loggerContext: LOGGER_CONTEXT,\n buildUrl: pluginBuildUrl,\n // Hash bridging (#532). popstate doesn't carry a URL — we sample\n // location.hash after the browser has updated to the destination.\n getCurrentHash: () => getDecodedHash(browser),\n getCurrentContextHash: () =>\n (router.getState()?.context as { url?: { hash?: string } } | undefined)\n ?.url?.hash ?? \"\",\n });\n\n const lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n removeStartInterceptor();\n removeExtensions();\n claim.release();\n urlClaim.release();\n },\n });\n\n return {\n ...lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n const replaceHistory = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n // Tri-state hash resolution (#532).\n // navOptions.hash === undefined → preserve current browser hash\n // navOptions.hash === \"\" → explicitly clear\n // navOptions.hash === \"value\" → explicitly set\n //\n // The \"preserve\" branch reads location.hash from the browser, not\n // fromState.context.url.hash — captures dynamic fragment changes\n // (anchor clicks, manual location.hash assignment) made outside the\n // plugin. hashChanged compares the chosen hash against the published\n // previous hash so subscribers see a true signal.\n const browserHash = getDecodedHash(browser);\n const publishedPrevHash =\n (fromState?.context as { url?: { hash?: string } } | undefined)?.url\n ?.hash ?? \"\";\n\n const hash =\n navOptions.hash === undefined\n ? browserHash\n : normalizeHashInput(navOptions.hash);\n\n urlClaim.write(\n toState,\n Object.freeze({\n hash,\n hashChanged: navOptions.hashChange ?? hash !== publishedPrevHash,\n }),\n );\n\n const url = buildUrl(toState.path, options.base);\n const finalUrl = hash ? `${url}#${encodeHashFragment(hash)}` : url;\n\n updateState(toState, finalUrl, replaceHistory, browser);\n\n const isPopstate = navOptions.source === POPSTATE_SOURCE;\n\n claim.write(toState, isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE);\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/browser-plugin",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
4
4
  "type": "commonjs",
5
5
  "description": "Browser integration plugin with History API, hash routing, and popstate support",
6
6
  "main": "./dist/cjs/index.js",
@@ -45,13 +45,13 @@
45
45
  },
46
46
  "sideEffects": false,
47
47
  "dependencies": {
48
- "@real-router/core": "^0.55.0",
49
- "@real-router/types": "^0.35.0"
48
+ "@real-router/core": "^0.56.0",
49
+ "@real-router/types": "^0.36.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@testing-library/jest-dom": "6.9.1",
53
53
  "jsdom": "29.1.1",
54
- "type-guards": "^0.4.9"
54
+ "type-guards": "^0.4.10"
55
55
  },
56
56
  "scripts": {
57
57
  "test": "vitest",