@real-router/browser-plugin 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.mts +4 -0
- package/dist/esm/index.d.mts.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/factory.ts +140 -18
- package/src/index.ts +6 -1
- package/src/validation.ts +1 -1
- package/src/plugin.ts +0 -135
package/README.md
CHANGED
|
@@ -83,6 +83,27 @@ router.replaceHistoryState(name, params); // URL only, no transition
|
|
|
83
83
|
router.navigate(name, params, { replace: true }); // Full transition + URL update
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
## Navigation Source (`state.context.browser.source`)
|
|
87
|
+
|
|
88
|
+
On every successful transition the plugin tags `state.context.browser` with the trigger origin — use this in `subscribe` handlers to distinguish back/forward from programmatic navigation:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
router.subscribe(({ route }) => {
|
|
92
|
+
if (route.context.browser?.source === "popstate") {
|
|
93
|
+
// back/forward button — restore scroll, skip analytics "view" event, ...
|
|
94
|
+
} else {
|
|
95
|
+
// router.navigate()/router.start() — programmatic
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Both values are frozen singletons, so object-identity comparison is safe and zero-allocation.
|
|
101
|
+
|
|
102
|
+
| Value | Meaning |
|
|
103
|
+
| ----------- | ------------------------------------------------------------- |
|
|
104
|
+
| `"navigate"` | Triggered by `router.navigate()`, `router.start()`, or `replaceHistoryState()` |
|
|
105
|
+
| `"popstate"` | Triggered by browser back/forward buttons (popstate event) |
|
|
106
|
+
|
|
86
107
|
## Form Protection
|
|
87
108
|
|
|
88
109
|
Set `forceDeactivate: false` to respect `canDeactivate` guards on back/forward:
|
|
@@ -103,15 +124,19 @@ lifecycle.addDeactivateGuard(
|
|
|
103
124
|
|
|
104
125
|
## SSR Support
|
|
105
126
|
|
|
106
|
-
The plugin is SSR-safe —
|
|
127
|
+
The plugin is SSR-safe — `createSafeBrowser` detects the absence of `window`/`history` and swaps the History API calls (`pushState`, `replaceState`, `addPopstateListener`, `getLocation`, `getHash`) for warn-once no-ops. Pure URL helpers are environment-agnostic and behave identically on the server:
|
|
107
128
|
|
|
108
129
|
```typescript
|
|
109
|
-
// Server-side — no
|
|
110
|
-
router.usePlugin(browserPluginFactory());
|
|
111
|
-
|
|
112
|
-
router.
|
|
130
|
+
// Server-side — no crashes, History API calls become no-ops
|
|
131
|
+
router.usePlugin(browserPluginFactory({ base: "/app" }));
|
|
132
|
+
|
|
133
|
+
router.buildUrl("home"); // "/app/home" — base is still applied
|
|
134
|
+
router.matchUrl("/app/users/123"); // { name: "users", params: { id: "123" }, path: "/users/123" }
|
|
135
|
+
await router.start("/app/home"); // transitions normally; no popstate subscription
|
|
113
136
|
```
|
|
114
137
|
|
|
138
|
+
The first call to any History API method emits a one-time console warning so accidental SSR usage is visible without spamming logs.
|
|
139
|
+
|
|
115
140
|
## Documentation
|
|
116
141
|
|
|
117
142
|
Full documentation: [Wiki — browser-plugin](https://github.com/greydragon888/real-router/wiki/browser-plugin)
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -69,6 +69,10 @@ declare module "@real-router/types" {
|
|
|
69
69
|
interface StateContext {
|
|
70
70
|
browser?: BrowserContext;
|
|
71
71
|
}
|
|
72
|
+
interface NavigationOptions {
|
|
73
|
+
/** @internal — set by browser/hash/navigation plugins to mark transition origin. */
|
|
74
|
+
source?: string;
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
declare module "@real-router/core" {
|
|
74
78
|
interface Router {
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -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","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;AAAA;AAAA,UAGO,oBAAA;EANQ;;AAEzB;;;EAUE,eAAA;EATqB;AAGvB;;;;EAaE,IAAA;AAAA;;;UCtBe,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAA;EAC/B,WAAA;AAAA;;;
|
|
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","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;AAAA;AAAA,UAGO,oBAAA;EANQ;;AAEzB;;;EAUE,eAAA;EATqB;AAGvB;;;;EAaE,IAAA;AAAA;;;UCtBe,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAA;EAC/B,WAAA;AAAA;;;iBC8Bc,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,GACf,OAAA,GAAU,OAAA,GACT,aAAA;;;;;;;;;;;;;;;iBCwCcS,aAAAA,WAAwBR,QAAAA,GAASA,QAAAA,CAAAA,CAAQG,KAAAA,YAAiBA,KAAAA,IAASF,OAAAA,CAAMM,CAAAA;AAAAA;;;;;;AChEnC;;;AJdvD;;;;AAAA;EAAA,UIqBY,YAAA;IACR,OAAA,GARmD,cAAA;EAAA;EAAA,UAW3C,iBAAA;IJtBW;IIwBnB,MAAA;EAAA;AAAA;AAAA;EAAA,UAKQ,MAAA;;;;AHnCZ;IGwCI,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IHrCJ;;;;IG2C9B,QAAA,GAAW,GAAA,aAAgB,KAAA;IH5C7B;;;;IGkDE,mBAAA,GAAsB,IAAA,UAAc,MAAA,GAAS,MAAA;IAE7C,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){if(v(e.state))return{name:e.state.name,params:e.state.params};let r=t.matchPath(n.getLocation());return r?{name:r.name,params:r.params}:void 0}function b(e,t,n,r){
|
|
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){if(v(e.state))return{name:e.state.name,params:e.state.params};let r=t.matchPath(n.getLocation());return r?{name:r.name,params:r.params}:void 0}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){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=e.buildUrl(t.name,t.params);e.browser.replaceState(t,n)}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.router.navigate(n.name,n.params,e.transitionOptions);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 E(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 D(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function O(e,t,n,r,i=!0){let a=b();return(o,s={})=>{let c=e.buildState(o,s);if(!c)throw Error(`[real-router] Cannot replace state: route "${o}" is not found`);let l=e.makeState(c.name,c.params,t.buildPath(c.name,c.params),{params:c.meta}),u=i?n.getHash():``;a(l,r(o,s)+u,!0,n)}}function k(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}function A(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 j(e,t){if(!e)return`/`;if(t&&(e===t||e.startsWith(`${t}/`))){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e.startsWith(`/`)?e:`/${e}`}function M(e,t){return e?t?e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function N(e,t){let n=A(e);return j(n.pathname,t)+n.search}const P={forceDeactivate:!0,base:``},F=`popstate`,I=`browser-plugin`,L=x(P,I,{base:C}),R=Object.freeze({source:`popstate`}),z=Object.freeze({source:`navigate`});function B(t,n){L(t);let r={...P,...t};r.base=s(r.base);let i=n??V(r.base),a={forceDeactivate:r.forceDeactivate,source:F,replace:!0},o={removePopStateListener:void 0};return function(t){return H(t,(0,e.getPluginApi)(t),r,i,a,o)}}function V(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(j(i,e))+a,r)},`browser-plugin`)}function H(e,t,n,r,i,a){let o=t.claimContextNamespace(`browser`),s=b(),c=D(t,r),l=(t,r)=>M(e.buildPath(t,r),n.base),u=t.extendRouter({buildUrl:l,matchUrl:e=>t.matchPath(N(e,n.base))??void 0,replaceHistoryState:O(t,e,r,l)});return{...E({browser:r,shared:a,handler:T({router:e,api:t,browser:r,allowNotFound:t.getOptions().allowNotFound,transitionOptions:i,loggerContext:I,buildUrl:l}),cleanup:()=>{c(),u(),o.release()}}),onTransitionSuccess:(e,t,i)=>{let a=k(i,e,t),c=M(e.path,n.base),l=!t||t.path===e.path?r.getHash():``;s(e,l?c+l:c,a,r);let u=i.source===F;o.write(e,u?R:z)}}}exports.browserPluginFactory=B,exports.isState=v;
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["isState","RouterError","errorCodes","#browser","#base","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle"],"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/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/plugin.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 * Extracts route name and params from a popstate event.\n *\n * - If history.state is a valid router state → returns name/params from it\n * - If not (e.g. manually entered URL) → matches current URL against route tree\n * - Returns undefined if no route matches\n *\n * @param evt - PopStateEvent from browser\n * @param api - PluginApi instance\n * @param browser - Browser API instance\n * @returns Route identifier or undefined\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): { name: string; params: Params } | undefined {\n if (isState(evt.state)) {\n return { name: evt.state.name, params: evt.state.params };\n }\n\n const state = api.matchPath(browser.getLocation());\n\n return state ? { name: state.name, params: state.params } : undefined;\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","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\nexport interface PopstateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: Browser;\n allowNotFound: boolean;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n loggerContext: string;\n buildUrl: (name: string, params?: Params) => string;\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 const url = deps.buildUrl(currentState.name, currentState.params);\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 route = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (route) {\n await deps.router.navigate(\n route.name,\n route.params,\n deps.transitionOptions,\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","import { updateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateBrowserState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n return (\n (navOptions.replace ?? !fromState) ||\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- fromState is NOT narrowed when replace is false (#447)\n (!!navOptions.reload && toState.path === fromState?.path)\n );\n}\n","export function safeParseUrl(url: string, loggerContext: string): URL | null {\n try {\n const parsedUrl = new URL(url, globalThis.location.origin);\n\n if (![\"http:\", \"https:\"].includes(parsedUrl.protocol)) {\n console.warn(`[${loggerContext}] Invalid URL protocol in ${url}`);\n\n return null;\n }\n\n return parsedUrl;\n } catch (error) {\n console.warn(`[${loggerContext}] Could not parse url ${url}`, error);\n\n return null;\n }\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(\n url: string,\n base: string,\n context: string,\n): string | null {\n const parsedUrl = safeParseUrl(url, context);\n\n return parsedUrl\n ? extractPath(parsedUrl.pathname, base) + parsedUrl.search\n : null;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} with an explicit non-null contract when the caller\n * already knows the URL is valid (e.g., sourced from the Navigation API or a\n * plugin-owned history store). Safe against malformed input — returns `null`.\n */\nexport function extractPathFromAbsoluteUrl(\n url: string,\n base: string,\n context: string,\n): string | null {\n return urlToPath(url, base, context);\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 {\n createPopstateHandler,\n createPopstateLifecycle,\n createStartInterceptor,\n createReplaceHistoryState,\n shouldReplaceHistory,\n updateBrowserState,\n buildUrl,\n urlToPath,\n} from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT, POPSTATE_SOURCE } from \"./constants\";\n\nimport type { Browser, SharedFactoryState } from \"./browser-env/index.js\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst FROZEN_POPSTATE: BrowserContext = Object.freeze({ source: \"popstate\" });\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({ source: \"navigate\" });\n\n/** @internal — instantiated by `browserPluginFactory`; not part of the public API. */\nexport class BrowserPlugin {\n readonly #browser: Browser;\n readonly #base: string;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: BrowserContext) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<BrowserPluginOptions>,\n browser: Browser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: SharedFactoryState,\n ) {\n this.#browser = browser;\n this.#base = options.base;\n this.#claim = api.claimContextNamespace(\"browser\");\n\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) => {\n const path = urlToPath(url, options.base, LOGGER_CONTEXT);\n\n return path ? api.matchPath(path) : undefined;\n },\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 });\n\n this.#lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n this.#removeStartInterceptor();\n this.#removeExtensions();\n this.#claim.release();\n },\n });\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#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 const url = buildUrl(toState.path, this.#base);\n\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n\n const hash = shouldPreserveHash ? this.#browser.getHash() : \"\";\n const finalUrl = hash ? url + hash : url;\n\n updateBrowserState(toState, finalUrl, replaceHistory, this.#browser);\n\n const isPopstate =\n (navOptions as Record<string, unknown>).source === POPSTATE_SOURCE;\n\n this.#claim.write(\n toState,\n isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE,\n );\n },\n };\n }\n}\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env/index.js\";\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 createSafeBrowser,\n normalizeBase,\n safelyEncodePath,\n extractPath,\n} from \"./browser-env/index.js\";\nimport { defaultOptions, POPSTATE_SOURCE } from \"./constants\";\nimport { BrowserPlugin } from \"./plugin\";\nimport { validateOptions } from \"./validation\";\n\nimport type { Browser, SharedFactoryState } from \"./browser-env/index.js\";\nimport type { BrowserPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\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 =\n browser ??\n createSafeBrowser(\n () =>\n safelyEncodePath(\n extractPath(globalThis.location.pathname, options.base),\n ) + globalThis.location.search,\n \"browser-plugin\",\n );\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 const plugin = new BrowserPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"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"}
|
|
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/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-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 * Extracts route name and params from a popstate event.\n *\n * - If history.state is a valid router state → returns name/params from it\n * - If not (e.g. manually entered URL) → matches current URL against route tree\n * - Returns undefined if no route matches\n *\n * @param evt - PopStateEvent from browser\n * @param api - PluginApi instance\n * @param browser - Browser API instance\n * @returns Route identifier or undefined\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): { name: string; params: Params } | undefined {\n if (isState(evt.state)) {\n return { name: evt.state.name, params: evt.state.params };\n }\n\n const state = api.matchPath(browser.getLocation());\n\n return state ? { name: state.name, params: state.params } : undefined;\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: (name: string, params?: Params) => string;\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 const url = deps.buildUrl(currentState.name, currentState.params);\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 route = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (route) {\n await deps.router.navigate(\n route.name,\n route.params,\n deps.transitionOptions,\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","import { createUpdateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n const updateState = createUpdateBrowserState();\n\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n if (navOptions.replace === true) {\n return true;\n }\n\n if (!fromState) {\n return navOptions.replace !== false;\n }\n\n return !!navOptions.reload && toState.path === fromState.path;\n}\n","export interface ParsedUrl {\n pathname: string;\n search: string;\n hash: string;\n}\n\n/**\n * Scheme-agnostic URL parser.\n *\n * Extracts `pathname`, `search`, and `hash` from any string — absolute\n * (`scheme://authority/path?q#h`), path-relative (`/path?q#h`), or opaque\n * (`data:...`, `javascript:...`). Never throws, never returns null.\n *\n * Routing does not care about scheme or authority, only about the path part.\n * This keeps `browser-plugin`, `navigation-plugin`, and `hash-plugin` working\n * in Electron (`file://`, `app://`), Tauri (`tauri://`, `https://`), and any\n * other webview that may ship with non-HTTP origins. See issue #496.\n */\nexport function safeParseUrl(url: string): ParsedUrl {\n let rest = url;\n\n const schemeIdx = rest.indexOf(\"://\");\n\n if (schemeIdx !== -1) {\n const authorityStart = schemeIdx + 3;\n let pathStart = rest.length;\n\n for (let i = authorityStart; i < rest.length; i++) {\n const ch = rest[i];\n\n if (ch === \"/\" || ch === \"?\" || ch === \"#\") {\n pathStart = i;\n\n break;\n }\n }\n\n rest = pathStart === rest.length ? \"/\" : rest.slice(pathStart);\n\n if (rest.startsWith(\"?\") || rest.startsWith(\"#\")) {\n rest = `/${rest}`;\n }\n }\n\n const hashIdx = rest.indexOf(\"#\");\n const hash = hashIdx === -1 ? \"\" : rest.slice(hashIdx);\n const beforeHash = hashIdx === -1 ? rest : rest.slice(0, hashIdx);\n\n const queryIdx = beforeHash.indexOf(\"?\");\n const search = queryIdx === -1 ? \"\" : beforeHash.slice(queryIdx);\n const pathname = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx);\n\n return { pathname, search, hash };\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(url: string, base: string): string {\n const parsedUrl = safeParseUrl(url);\n\n return extractPath(parsedUrl.pathname, base) + parsedUrl.search;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} kept for call-site readability — history-query\n * paths (Navigation API entries, etc.) are absolute URLs by contract.\n */\nexport function extractPathFromAbsoluteUrl(url: string, base: string): string {\n return urlToPath(url, base);\n}\n","import type { 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 createPopstateHandler,\n createPopstateLifecycle,\n createReplaceHistoryState,\n createSafeBrowser,\n createStartInterceptor,\n createUpdateBrowserState,\n extractPath,\n normalizeBase,\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} from \"./browser-env\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Params,\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({ source: \"popstate\" });\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({ source: \"navigate\" });\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 const updateState = createUpdateBrowserState();\n const removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n 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 });\n\n const lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n removeStartInterceptor();\n removeExtensions();\n claim.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 const url = buildUrl(toState.path, options.base);\n\n const shouldPreserveHash = !fromState || fromState.path === toState.path;\n\n const hash = shouldPreserveHash ? browser.getHash() : \"\";\n const finalUrl = hash ? url + 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/dist/esm/index.d.mts
CHANGED
|
@@ -69,6 +69,10 @@ declare module "@real-router/types" {
|
|
|
69
69
|
interface StateContext {
|
|
70
70
|
browser?: BrowserContext;
|
|
71
71
|
}
|
|
72
|
+
interface NavigationOptions {
|
|
73
|
+
/** @internal — set by browser/hash/navigation plugins to mark transition origin. */
|
|
74
|
+
source?: string;
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
declare module "@real-router/core" {
|
|
74
78
|
interface Router {
|
package/dist/esm/index.d.mts.map
CHANGED
|
@@ -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","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;AAAA;AAAA,UAGO,oBAAA;EANQ;;AAEzB;;;EAUE,eAAA;EATqB;AAGvB;;;;EAaE,IAAA;AAAA;;;UCtBe,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAA;EAC/B,WAAA;AAAA;;;
|
|
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","../../src/factory.ts","../../../type-guards/dist/esm/index.d.mts","../../src/index.ts"],"mappings":";;;;;;;KAGY,aAAA;AAAA,UAEK,cAAA;EACf,MAAA,EAAQ,aAAA;AAAA;AAAA,UAGO,oBAAA;EANQ;;AAEzB;;;EAUE,eAAA;EATqB;AAGvB;;;;EAaE,IAAA;AAAA;;;UCtBe,cAAA;EACf,SAAA,GAAY,KAAA,WAAgB,IAAA;EAC5B,YAAA,GAAe,KAAA,WAAgB,IAAA;EAC/B,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA;AAAA;AAAA,UAGe,OAAA,SAAgB,cAAA;EAC/B,WAAA;AAAA;;;iBC8Bc,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,GACf,OAAA,GAAU,OAAA,GACT,aAAA;;;;;;;;;;;;;;;iBCwCcS,aAAAA,WAAwBR,QAAAA,GAASA,QAAAA,CAAAA,CAAQG,KAAAA,YAAiBA,KAAAA,IAASF,OAAAA,CAAMM,CAAAA;AAAAA;;;;;;AChEnC;;;AJdvD;;;;AAAA;EAAA,UIqBY,YAAA;IACR,OAAA,GARmD,cAAA;EAAA;EAAA,UAW3C,iBAAA;IJtBW;IIwBnB,MAAA;EAAA;AAAA;AAAA;EAAA,UAKQ,MAAA;;;;AHnCZ;IGwCI,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IHrCJ;;;;IG2C9B,QAAA,GAAW,GAAA,aAAgB,KAAA;IH5C7B;;;;IGkDE,mBAAA,GAAsB,IAAA,UAAc,MAAA,GAAS,MAAA;IAE7C,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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){if(y(e.state))return{name:e.state.name,params:e.state.params};let r=t.matchPath(n.getLocation());return r?{name:r.name,params:r.params}:void 0}function x(e,t,n,r){
|
|
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){if(y(e.state))return{name:e.state.name,params:e.state.params};let r=t.matchPath(n.getLocation());return r?{name:r.name,params:r.params}:void 0}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){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=e.buildUrl(t.name,t.params);e.browser.replaceState(t,n)}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.router.navigate(r.name,r.params,e.transitionOptions);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 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,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function k(e,t,n,r,i=!0){let a=x();return(o,s={})=>{let c=e.buildState(o,s);if(!c)throw Error(`[real-router] Cannot replace state: route "${o}" is not found`);let l=e.makeState(c.name,c.params,t.buildPath(c.name,c.params),{params:c.meta}),u=i?n.getHash():``;a(l,r(o,s)+u,!0,n)}}function A(e,t,n){return e.replace===!0?!0:n?!!e.reload&&t.path===n.path:e.replace!==!1}function j(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 M(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 N(e,t){return e?t?e.startsWith(`/`)?`${t}${e}`:`${t}/${e}`:e.startsWith(`/`)?e:`/${e}`:t}function P(e,t){let n=j(e);return M(n.pathname,t)+n.search}const F={forceDeactivate:!0,base:``},I=`popstate`,L=`browser-plugin`,R=S(F,L,{base:w}),z=Object.freeze({source:`popstate`}),B=Object.freeze({source:`navigate`});function V(t,n){R(t);let r={...F,...t};r.base=c(r.base);let i=n??H(r.base),a={forceDeactivate:r.forceDeactivate,source:I,replace:!0},o={removePopStateListener:void 0};return function(t){return U(t,e(t),r,i,a,o)}}function H(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(M(i,e))+a,r)},`browser-plugin`)}function U(e,t,n,r,i,a){let o=t.claimContextNamespace(`browser`),s=x(),c=O(t,r),l=(t,r)=>N(e.buildPath(t,r),n.base),u=t.extendRouter({buildUrl:l,matchUrl:e=>t.matchPath(P(e,n.base))??void 0,replaceHistoryState:k(t,e,r,l)});return{...D({browser:r,shared:a,handler:E({router:e,api:t,browser:r,allowNotFound:t.getOptions().allowNotFound,transitionOptions:i,loggerContext:L,buildUrl:l}),cleanup:()=>{c(),u(),o.release()}}),onTransitionSuccess:(e,t,i)=>{let a=A(i,e,t),c=N(e.path,n.base),l=!t||t.path===e.path?r.getHash():``;s(e,l?c+l:c,a,r);let u=i.source===I;o.write(e,u?z:B)}}}export{V as browserPluginFactory,y as isState};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["isState","#browser","#base","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle"],"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/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/plugin.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 * Extracts route name and params from a popstate event.\n *\n * - If history.state is a valid router state → returns name/params from it\n * - If not (e.g. manually entered URL) → matches current URL against route tree\n * - Returns undefined if no route matches\n *\n * @param evt - PopStateEvent from browser\n * @param api - PluginApi instance\n * @param browser - Browser API instance\n * @returns Route identifier or undefined\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): { name: string; params: Params } | undefined {\n if (isState(evt.state)) {\n return { name: evt.state.name, params: evt.state.params };\n }\n\n const state = api.matchPath(browser.getLocation());\n\n return state ? { name: state.name, params: state.params } : undefined;\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","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\nexport interface PopstateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: Browser;\n allowNotFound: boolean;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n loggerContext: string;\n buildUrl: (name: string, params?: Params) => string;\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 const url = deps.buildUrl(currentState.name, currentState.params);\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 route = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (route) {\n await deps.router.navigate(\n route.name,\n route.params,\n deps.transitionOptions,\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","import { updateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateBrowserState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n return (\n (navOptions.replace ?? !fromState) ||\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- fromState is NOT narrowed when replace is false (#447)\n (!!navOptions.reload && toState.path === fromState?.path)\n );\n}\n","export function safeParseUrl(url: string, loggerContext: string): URL | null {\n try {\n const parsedUrl = new URL(url, globalThis.location.origin);\n\n if (![\"http:\", \"https:\"].includes(parsedUrl.protocol)) {\n console.warn(`[${loggerContext}] Invalid URL protocol in ${url}`);\n\n return null;\n }\n\n return parsedUrl;\n } catch (error) {\n console.warn(`[${loggerContext}] Could not parse url ${url}`, error);\n\n return null;\n }\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(\n url: string,\n base: string,\n context: string,\n): string | null {\n const parsedUrl = safeParseUrl(url, context);\n\n return parsedUrl\n ? extractPath(parsedUrl.pathname, base) + parsedUrl.search\n : null;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} with an explicit non-null contract when the caller\n * already knows the URL is valid (e.g., sourced from the Navigation API or a\n * plugin-owned history store). Safe against malformed input — returns `null`.\n */\nexport function extractPathFromAbsoluteUrl(\n url: string,\n base: string,\n context: string,\n): string | null {\n return urlToPath(url, base, context);\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 {\n createPopstateHandler,\n createPopstateLifecycle,\n createStartInterceptor,\n createReplaceHistoryState,\n shouldReplaceHistory,\n updateBrowserState,\n buildUrl,\n urlToPath,\n} from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT, POPSTATE_SOURCE } from \"./constants\";\n\nimport type { Browser, SharedFactoryState } from \"./browser-env/index.js\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst FROZEN_POPSTATE: BrowserContext = Object.freeze({ source: \"popstate\" });\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({ source: \"navigate\" });\n\n/** @internal — instantiated by `browserPluginFactory`; not part of the public API. */\nexport class BrowserPlugin {\n readonly #browser: Browser;\n readonly #base: string;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: BrowserContext) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<BrowserPluginOptions>,\n browser: Browser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: SharedFactoryState,\n ) {\n this.#browser = browser;\n this.#base = options.base;\n this.#claim = api.claimContextNamespace(\"browser\");\n\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) => {\n const path = urlToPath(url, options.base, LOGGER_CONTEXT);\n\n return path ? api.matchPath(path) : undefined;\n },\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 });\n\n this.#lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n this.#removeStartInterceptor();\n this.#removeExtensions();\n this.#claim.release();\n },\n });\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#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 const url = buildUrl(toState.path, this.#base);\n\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n\n const hash = shouldPreserveHash ? this.#browser.getHash() : \"\";\n const finalUrl = hash ? url + hash : url;\n\n updateBrowserState(toState, finalUrl, replaceHistory, this.#browser);\n\n const isPopstate =\n (navOptions as Record<string, unknown>).source === POPSTATE_SOURCE;\n\n this.#claim.write(\n toState,\n isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE,\n );\n },\n };\n }\n}\n","import { createOptionsValidator, safeBaseRule } from \"./browser-env/index.js\";\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 createSafeBrowser,\n normalizeBase,\n safelyEncodePath,\n extractPath,\n} from \"./browser-env/index.js\";\nimport { defaultOptions, POPSTATE_SOURCE } from \"./constants\";\nimport { BrowserPlugin } from \"./plugin\";\nimport { validateOptions } from \"./validation\";\n\nimport type { Browser, SharedFactoryState } from \"./browser-env/index.js\";\nimport type { BrowserPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\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 =\n browser ??\n createSafeBrowser(\n () =>\n safelyEncodePath(\n extractPath(globalThis.location.pathname, options.base),\n ) + globalThis.location.search,\n \"browser-plugin\",\n );\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 const plugin = new BrowserPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"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"}
|
|
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/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-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 * Extracts route name and params from a popstate event.\n *\n * - If history.state is a valid router state → returns name/params from it\n * - If not (e.g. manually entered URL) → matches current URL against route tree\n * - Returns undefined if no route matches\n *\n * @param evt - PopStateEvent from browser\n * @param api - PluginApi instance\n * @param browser - Browser API instance\n * @returns Route identifier or undefined\n */\nexport function getRouteFromEvent(\n evt: PopStateEvent,\n api: PluginApi,\n browser: Browser,\n): { name: string; params: Params } | undefined {\n if (isState(evt.state)) {\n return { name: evt.state.name, params: evt.state.params };\n }\n\n const state = api.matchPath(browser.getLocation());\n\n return state ? { name: state.name, params: state.params } : undefined;\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: (name: string, params?: Params) => string;\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 const url = deps.buildUrl(currentState.name, currentState.params);\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 route = getRouteFromEvent(evt, deps.api, deps.browser);\n\n if (route) {\n await deps.router.navigate(\n route.name,\n route.params,\n deps.transitionOptions,\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","import { createUpdateBrowserState } from \"./popstate-utils.js\";\n\nimport type { Browser } from \"./types.js\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function createStartInterceptor(\n api: PluginApi,\n browser: Browser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: Browser,\n buildUrl: (name: string, params?: Params) => string,\n preserveHash = true,\n): (name: string, params?: Params) => void {\n const updateState = createUpdateBrowserState();\n\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const hash = preserveHash ? browser.getHash() : \"\";\n const url = buildUrl(name, params) + hash;\n\n updateState(builtState, url, true, browser);\n };\n}\n\nexport function shouldReplaceHistory(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): boolean {\n if (navOptions.replace === true) {\n return true;\n }\n\n if (!fromState) {\n return navOptions.replace !== false;\n }\n\n return !!navOptions.reload && toState.path === fromState.path;\n}\n","export interface ParsedUrl {\n pathname: string;\n search: string;\n hash: string;\n}\n\n/**\n * Scheme-agnostic URL parser.\n *\n * Extracts `pathname`, `search`, and `hash` from any string — absolute\n * (`scheme://authority/path?q#h`), path-relative (`/path?q#h`), or opaque\n * (`data:...`, `javascript:...`). Never throws, never returns null.\n *\n * Routing does not care about scheme or authority, only about the path part.\n * This keeps `browser-plugin`, `navigation-plugin`, and `hash-plugin` working\n * in Electron (`file://`, `app://`), Tauri (`tauri://`, `https://`), and any\n * other webview that may ship with non-HTTP origins. See issue #496.\n */\nexport function safeParseUrl(url: string): ParsedUrl {\n let rest = url;\n\n const schemeIdx = rest.indexOf(\"://\");\n\n if (schemeIdx !== -1) {\n const authorityStart = schemeIdx + 3;\n let pathStart = rest.length;\n\n for (let i = authorityStart; i < rest.length; i++) {\n const ch = rest[i];\n\n if (ch === \"/\" || ch === \"?\" || ch === \"#\") {\n pathStart = i;\n\n break;\n }\n }\n\n rest = pathStart === rest.length ? \"/\" : rest.slice(pathStart);\n\n if (rest.startsWith(\"?\") || rest.startsWith(\"#\")) {\n rest = `/${rest}`;\n }\n }\n\n const hashIdx = rest.indexOf(\"#\");\n const hash = hashIdx === -1 ? \"\" : rest.slice(hashIdx);\n const beforeHash = hashIdx === -1 ? rest : rest.slice(0, hashIdx);\n\n const queryIdx = beforeHash.indexOf(\"?\");\n const search = queryIdx === -1 ? \"\" : beforeHash.slice(queryIdx);\n const pathname = queryIdx === -1 ? beforeHash : beforeHash.slice(0, queryIdx);\n\n return { pathname, search, hash };\n}\n","import { safeParseUrl } from \"./url-parsing.js\";\n\nexport function extractPath(pathname: string, base: string): string {\n if (!pathname) {\n return \"/\";\n }\n\n if (base && (pathname === base || pathname.startsWith(`${base}/`))) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n}\n\nexport function buildUrl(path: string, base: string): string {\n if (!path) {\n return base;\n }\n\n if (!base) {\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n return path.startsWith(\"/\") ? `${base}${path}` : `${base}/${path}`;\n}\n\nexport function urlToPath(url: string, base: string): string {\n const parsedUrl = safeParseUrl(url);\n\n return extractPath(parsedUrl.pathname, base) + parsedUrl.search;\n}\n\n/**\n * Parses an absolute URL and returns its path + search, stripped of `base`.\n * Alias of {@link urlToPath} kept for call-site readability — history-query\n * paths (Navigation API entries, etc.) are absolute URLs by contract.\n */\nexport function extractPathFromAbsoluteUrl(url: string, base: string): string {\n return urlToPath(url, base);\n}\n","import type { 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 createPopstateHandler,\n createPopstateLifecycle,\n createReplaceHistoryState,\n createSafeBrowser,\n createStartInterceptor,\n createUpdateBrowserState,\n extractPath,\n normalizeBase,\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} from \"./browser-env\";\nimport type { BrowserContext, BrowserPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Params,\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({ source: \"popstate\" });\nconst FROZEN_NAVIGATE: BrowserContext = Object.freeze({ source: \"navigate\" });\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 const updateState = createUpdateBrowserState();\n const removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n 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 });\n\n const lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n removeStartInterceptor();\n removeExtensions();\n claim.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 const url = buildUrl(toState.path, options.base);\n\n const shouldPreserveHash = !fromState || fromState.path === toState.path;\n\n const hash = shouldPreserveHash ? browser.getHash() : \"\";\n const finalUrl = hash ? url + 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.
|
|
3
|
+
"version": "0.15.0",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"sideEffects": false,
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@real-router/core": "^0.50.0",
|
|
49
|
-
"@real-router/types": "^0.34.
|
|
49
|
+
"@real-router/types": "^0.34.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@testing-library/jest-dom": "6.9.1",
|
package/src/factory.ts
CHANGED
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
import { getPluginApi } from "@real-router/core/api";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
buildUrl,
|
|
5
|
+
createPopstateHandler,
|
|
6
|
+
createPopstateLifecycle,
|
|
7
|
+
createReplaceHistoryState,
|
|
4
8
|
createSafeBrowser,
|
|
9
|
+
createStartInterceptor,
|
|
10
|
+
createUpdateBrowserState,
|
|
11
|
+
extractPath,
|
|
5
12
|
normalizeBase,
|
|
6
13
|
safelyEncodePath,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
14
|
+
shouldReplaceHistory,
|
|
15
|
+
urlToPath,
|
|
16
|
+
} from "./browser-env";
|
|
17
|
+
import { defaultOptions, LOGGER_CONTEXT, POPSTATE_SOURCE } from "./constants";
|
|
11
18
|
import { validateOptions } from "./validation";
|
|
12
19
|
|
|
13
|
-
import type {
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
import type {
|
|
21
|
+
Browser,
|
|
22
|
+
PopstateTransitionOptions,
|
|
23
|
+
SharedFactoryState,
|
|
24
|
+
} from "./browser-env";
|
|
25
|
+
import type { BrowserContext, BrowserPluginOptions } from "./types";
|
|
26
|
+
import type {
|
|
27
|
+
NavigationOptions,
|
|
28
|
+
Params,
|
|
29
|
+
Plugin,
|
|
30
|
+
PluginFactory,
|
|
31
|
+
Router,
|
|
32
|
+
State,
|
|
33
|
+
} from "@real-router/core";
|
|
34
|
+
import type { PluginApi } from "@real-router/core/api";
|
|
35
|
+
|
|
36
|
+
const FROZEN_POPSTATE: BrowserContext = Object.freeze({ source: "popstate" });
|
|
37
|
+
const FROZEN_NAVIGATE: BrowserContext = Object.freeze({ source: "navigate" });
|
|
16
38
|
|
|
17
39
|
export function browserPluginFactory(
|
|
18
40
|
opts?: Partial<BrowserPluginOptions>,
|
|
@@ -27,15 +49,7 @@ export function browserPluginFactory(
|
|
|
27
49
|
|
|
28
50
|
options.base = normalizeBase(options.base);
|
|
29
51
|
|
|
30
|
-
const resolvedBrowser =
|
|
31
|
-
browser ??
|
|
32
|
-
createSafeBrowser(
|
|
33
|
-
() =>
|
|
34
|
-
safelyEncodePath(
|
|
35
|
-
extractPath(globalThis.location.pathname, options.base),
|
|
36
|
-
) + globalThis.location.search,
|
|
37
|
-
"browser-plugin",
|
|
38
|
-
);
|
|
52
|
+
const resolvedBrowser = browser ?? createDefaultBrowser(options.base);
|
|
39
53
|
|
|
40
54
|
const transitionOptions = {
|
|
41
55
|
forceDeactivate: options.forceDeactivate,
|
|
@@ -46,7 +60,7 @@ export function browserPluginFactory(
|
|
|
46
60
|
const shared: SharedFactoryState = { removePopStateListener: undefined };
|
|
47
61
|
|
|
48
62
|
return function browserPlugin(routerBase) {
|
|
49
|
-
|
|
63
|
+
return createBrowserPlugin(
|
|
50
64
|
routerBase as Router,
|
|
51
65
|
getPluginApi(routerBase),
|
|
52
66
|
options,
|
|
@@ -54,7 +68,115 @@ export function browserPluginFactory(
|
|
|
54
68
|
transitionOptions,
|
|
55
69
|
shared,
|
|
56
70
|
);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates the default `Browser` for the plugin, with a memoized `getLocation`
|
|
76
|
+
* that skips re-running `extractPath`/`safelyEncodePath` when neither
|
|
77
|
+
* `pathname` nor `search` has changed since the last call (#8.2 A7).
|
|
78
|
+
*
|
|
79
|
+
* Initial sentinel is `"\0"` — a NUL byte cannot appear in a real
|
|
80
|
+
* `location.pathname`, so the first call is always a miss without needing a
|
|
81
|
+
* separate "primed" flag.
|
|
82
|
+
*/
|
|
83
|
+
function createDefaultBrowser(base: string): Browser {
|
|
84
|
+
let cachedPathname = "\0";
|
|
85
|
+
let cachedSearch = "";
|
|
86
|
+
let cachedResult = "";
|
|
87
|
+
|
|
88
|
+
return createSafeBrowser(() => {
|
|
89
|
+
const { pathname, search } = globalThis.location;
|
|
90
|
+
|
|
91
|
+
if (pathname === cachedPathname && search === cachedSearch) {
|
|
92
|
+
return cachedResult;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
cachedPathname = pathname;
|
|
96
|
+
cachedSearch = search;
|
|
97
|
+
cachedResult = safelyEncodePath(extractPath(pathname, base)) + search;
|
|
98
|
+
|
|
99
|
+
return cachedResult;
|
|
100
|
+
}, "browser-plugin");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createBrowserPlugin(
|
|
104
|
+
router: Router,
|
|
105
|
+
api: PluginApi,
|
|
106
|
+
options: Required<BrowserPluginOptions>,
|
|
107
|
+
browser: Browser,
|
|
108
|
+
transitionOptions: PopstateTransitionOptions,
|
|
109
|
+
shared: SharedFactoryState,
|
|
110
|
+
): Plugin {
|
|
111
|
+
const claim = api.claimContextNamespace("browser");
|
|
112
|
+
const updateState = createUpdateBrowserState();
|
|
113
|
+
const removeStartInterceptor = createStartInterceptor(api, browser);
|
|
114
|
+
|
|
115
|
+
const pluginBuildUrl = (route: string, params?: Params) => {
|
|
116
|
+
const path = router.buildPath(route, params);
|
|
117
|
+
|
|
118
|
+
return buildUrl(path, options.base);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const removeExtensions = api.extendRouter({
|
|
122
|
+
buildUrl: pluginBuildUrl,
|
|
123
|
+
matchUrl: (url: string) =>
|
|
124
|
+
api.matchPath(urlToPath(url, options.base)) ?? undefined,
|
|
125
|
+
replaceHistoryState: createReplaceHistoryState(
|
|
126
|
+
api,
|
|
127
|
+
router,
|
|
128
|
+
browser,
|
|
129
|
+
pluginBuildUrl,
|
|
130
|
+
),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const handler = createPopstateHandler({
|
|
134
|
+
router,
|
|
135
|
+
api,
|
|
136
|
+
browser,
|
|
137
|
+
allowNotFound: api.getOptions().allowNotFound,
|
|
138
|
+
transitionOptions,
|
|
139
|
+
loggerContext: LOGGER_CONTEXT,
|
|
140
|
+
buildUrl: pluginBuildUrl,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const lifecycle = createPopstateLifecycle({
|
|
144
|
+
browser,
|
|
145
|
+
shared,
|
|
146
|
+
handler,
|
|
147
|
+
cleanup: () => {
|
|
148
|
+
removeStartInterceptor();
|
|
149
|
+
removeExtensions();
|
|
150
|
+
claim.release();
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
...lifecycle,
|
|
156
|
+
|
|
157
|
+
onTransitionSuccess: (
|
|
158
|
+
toState: State,
|
|
159
|
+
fromState: State | undefined,
|
|
160
|
+
navOptions: NavigationOptions,
|
|
161
|
+
) => {
|
|
162
|
+
const replaceHistory = shouldReplaceHistory(
|
|
163
|
+
navOptions,
|
|
164
|
+
toState,
|
|
165
|
+
fromState,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const url = buildUrl(toState.path, options.base);
|
|
169
|
+
|
|
170
|
+
const shouldPreserveHash = !fromState || fromState.path === toState.path;
|
|
171
|
+
|
|
172
|
+
const hash = shouldPreserveHash ? browser.getHash() : "";
|
|
173
|
+
const finalUrl = hash ? url + hash : url;
|
|
174
|
+
|
|
175
|
+
updateState(toState, finalUrl, replaceHistory, browser);
|
|
176
|
+
|
|
177
|
+
const isPopstate = navOptions.source === POPSTATE_SOURCE;
|
|
57
178
|
|
|
58
|
-
|
|
179
|
+
claim.write(toState, isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE);
|
|
180
|
+
},
|
|
59
181
|
};
|
|
60
182
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type {
|
|
|
12
12
|
BrowserSource,
|
|
13
13
|
} from "./types";
|
|
14
14
|
|
|
15
|
-
export type { Browser } from "./browser-env
|
|
15
|
+
export type { Browser } from "./browser-env";
|
|
16
16
|
|
|
17
17
|
// Type guards
|
|
18
18
|
export { isStateStrict as isState } from "type-guards";
|
|
@@ -25,6 +25,11 @@ declare module "@real-router/types" {
|
|
|
25
25
|
interface StateContext {
|
|
26
26
|
browser?: import("./types").BrowserContext;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
interface NavigationOptions {
|
|
30
|
+
/** @internal — set by browser/hash/navigation plugins to mark transition origin. */
|
|
31
|
+
source?: string;
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
declare module "@real-router/core" {
|
package/src/validation.ts
CHANGED
package/src/plugin.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createPopstateHandler,
|
|
3
|
-
createPopstateLifecycle,
|
|
4
|
-
createStartInterceptor,
|
|
5
|
-
createReplaceHistoryState,
|
|
6
|
-
shouldReplaceHistory,
|
|
7
|
-
updateBrowserState,
|
|
8
|
-
buildUrl,
|
|
9
|
-
urlToPath,
|
|
10
|
-
} from "./browser-env/index.js";
|
|
11
|
-
import { LOGGER_CONTEXT, POPSTATE_SOURCE } from "./constants";
|
|
12
|
-
|
|
13
|
-
import type { Browser, SharedFactoryState } from "./browser-env/index.js";
|
|
14
|
-
import type { BrowserContext, BrowserPluginOptions } from "./types";
|
|
15
|
-
import type {
|
|
16
|
-
NavigationOptions,
|
|
17
|
-
Params,
|
|
18
|
-
Router,
|
|
19
|
-
State,
|
|
20
|
-
Plugin,
|
|
21
|
-
} from "@real-router/core";
|
|
22
|
-
import type { PluginApi } from "@real-router/core/api";
|
|
23
|
-
|
|
24
|
-
const FROZEN_POPSTATE: BrowserContext = Object.freeze({ source: "popstate" });
|
|
25
|
-
const FROZEN_NAVIGATE: BrowserContext = Object.freeze({ source: "navigate" });
|
|
26
|
-
|
|
27
|
-
/** @internal — instantiated by `browserPluginFactory`; not part of the public API. */
|
|
28
|
-
export class BrowserPlugin {
|
|
29
|
-
readonly #browser: Browser;
|
|
30
|
-
readonly #base: string;
|
|
31
|
-
readonly #removeStartInterceptor: () => void;
|
|
32
|
-
readonly #removeExtensions: () => void;
|
|
33
|
-
readonly #claim: {
|
|
34
|
-
write: (state: State, value: BrowserContext) => void;
|
|
35
|
-
release: () => void;
|
|
36
|
-
};
|
|
37
|
-
readonly #lifecycle: Pick<Plugin, "onStart" | "onStop" | "teardown">;
|
|
38
|
-
|
|
39
|
-
constructor(
|
|
40
|
-
router: Router,
|
|
41
|
-
api: PluginApi,
|
|
42
|
-
options: Required<BrowserPluginOptions>,
|
|
43
|
-
browser: Browser,
|
|
44
|
-
transitionOptions: {
|
|
45
|
-
source: string;
|
|
46
|
-
replace: true;
|
|
47
|
-
forceDeactivate?: boolean;
|
|
48
|
-
},
|
|
49
|
-
shared: SharedFactoryState,
|
|
50
|
-
) {
|
|
51
|
-
this.#browser = browser;
|
|
52
|
-
this.#base = options.base;
|
|
53
|
-
this.#claim = api.claimContextNamespace("browser");
|
|
54
|
-
|
|
55
|
-
this.#removeStartInterceptor = createStartInterceptor(api, browser);
|
|
56
|
-
|
|
57
|
-
const pluginBuildUrl = (route: string, params?: Params) => {
|
|
58
|
-
const path = router.buildPath(route, params);
|
|
59
|
-
|
|
60
|
-
return buildUrl(path, options.base);
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
this.#removeExtensions = api.extendRouter({
|
|
64
|
-
buildUrl: pluginBuildUrl,
|
|
65
|
-
matchUrl: (url: string) => {
|
|
66
|
-
const path = urlToPath(url, options.base, LOGGER_CONTEXT);
|
|
67
|
-
|
|
68
|
-
return path ? api.matchPath(path) : undefined;
|
|
69
|
-
},
|
|
70
|
-
replaceHistoryState: createReplaceHistoryState(
|
|
71
|
-
api,
|
|
72
|
-
router,
|
|
73
|
-
browser,
|
|
74
|
-
pluginBuildUrl,
|
|
75
|
-
),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const handler = createPopstateHandler({
|
|
79
|
-
router,
|
|
80
|
-
api,
|
|
81
|
-
browser,
|
|
82
|
-
allowNotFound: api.getOptions().allowNotFound,
|
|
83
|
-
transitionOptions,
|
|
84
|
-
loggerContext: LOGGER_CONTEXT,
|
|
85
|
-
buildUrl: pluginBuildUrl,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
this.#lifecycle = createPopstateLifecycle({
|
|
89
|
-
browser,
|
|
90
|
-
shared,
|
|
91
|
-
handler,
|
|
92
|
-
cleanup: () => {
|
|
93
|
-
this.#removeStartInterceptor();
|
|
94
|
-
this.#removeExtensions();
|
|
95
|
-
this.#claim.release();
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
getPlugin(): Plugin {
|
|
101
|
-
return {
|
|
102
|
-
...this.#lifecycle,
|
|
103
|
-
|
|
104
|
-
onTransitionSuccess: (
|
|
105
|
-
toState: State,
|
|
106
|
-
fromState: State | undefined,
|
|
107
|
-
navOptions: NavigationOptions,
|
|
108
|
-
) => {
|
|
109
|
-
const replaceHistory = shouldReplaceHistory(
|
|
110
|
-
navOptions,
|
|
111
|
-
toState,
|
|
112
|
-
fromState,
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
const url = buildUrl(toState.path, this.#base);
|
|
116
|
-
|
|
117
|
-
const shouldPreserveHash =
|
|
118
|
-
!fromState || fromState.path === toState.path;
|
|
119
|
-
|
|
120
|
-
const hash = shouldPreserveHash ? this.#browser.getHash() : "";
|
|
121
|
-
const finalUrl = hash ? url + hash : url;
|
|
122
|
-
|
|
123
|
-
updateBrowserState(toState, finalUrl, replaceHistory, this.#browser);
|
|
124
|
-
|
|
125
|
-
const isPopstate =
|
|
126
|
-
(navOptions as Record<string, unknown>).source === POPSTATE_SOURCE;
|
|
127
|
-
|
|
128
|
-
this.#claim.write(
|
|
129
|
-
toState,
|
|
130
|
-
isPopstate ? FROZEN_POPSTATE : FROZEN_NAVIGATE,
|
|
131
|
-
);
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
}
|