@real-router/navigation-plugin 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -22
- package/dist/cjs/index.d.ts +13 -4
- 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 +13 -4
- 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 +6 -5
- package/src/history-extensions.ts +8 -4
- package/src/index.ts +7 -3
- package/src/navigate-handler.ts +29 -5
- package/src/plugin.ts +41 -24
- package/src/types.ts +8 -2
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ router.usePlugin(
|
|
|
66
66
|
| -------------------------------------------- | -------------------- | ------------------------------------------------ |
|
|
67
67
|
| `buildUrl(name, params?)` | `string` | Build full URL with base path |
|
|
68
68
|
| `matchUrl(url)` | `State \| undefined` | Parse URL to router state |
|
|
69
|
-
| `replaceHistoryState(name, params
|
|
69
|
+
| `replaceHistoryState(name, params?)` | `void` | Update browser URL without triggering navigation |
|
|
70
70
|
|
|
71
71
|
```typescript
|
|
72
72
|
router.buildUrl("users", { id: "123" });
|
|
@@ -89,7 +89,6 @@ router.replaceHistoryState("users", { id: "456" });
|
|
|
89
89
|
| `getVisitedRoutes()` | `string[]` | Unique route names across all history entries |
|
|
90
90
|
| `getRouteVisitCount(routeName)` | `number` | How many history entries match the route |
|
|
91
91
|
| `traverseToLast(routeName)` | `Promise<State>` | Navigate to the last history entry for a route |
|
|
92
|
-
| `getNavigationMeta(state?)` | `NavigationMeta \| undefined` | Navigation metadata (type, userInitiated, info) |
|
|
93
92
|
| `canGoBack()` | `boolean` | Whether there's a previous history entry |
|
|
94
93
|
| `canGoForward()` | `boolean` | Whether there's a next history entry |
|
|
95
94
|
| `canGoBackTo(routeName)` | `boolean` | Whether any previous entry matches the route |
|
|
@@ -133,41 +132,76 @@ const count = router.getRouteVisitCount("products.view");
|
|
|
133
132
|
await router.traverseToLast("users.list");
|
|
134
133
|
```
|
|
135
134
|
|
|
136
|
-
#### `
|
|
135
|
+
#### `canGoBack` / `canGoForward` / `canGoBackTo`
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Disable back button when there's nowhere to go
|
|
139
|
+
const backDisabled = !router.canGoBack();
|
|
140
|
+
const forwardDisabled = !router.canGoForward();
|
|
141
|
+
|
|
142
|
+
// Show "back to list" only if the user actually came from the list
|
|
143
|
+
if (router.canGoBackTo("users.list")) {
|
|
144
|
+
showBackToListButton();
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Navigation Metadata
|
|
149
|
+
|
|
150
|
+
Navigation metadata is available on `state.context.navigation` after each transition. The plugin writes it via the claim-based State Context API, and it is frozen (`Object.freeze`) for mutation protection.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// In subscribe callbacks
|
|
154
|
+
router.subscribe((state) => {
|
|
155
|
+
const meta = state.context.navigation;
|
|
156
|
+
console.log(meta?.navigationType); // "push" | "replace" | "traverse" | "reload"
|
|
157
|
+
console.log(meta?.userInitiated); // true if user clicked back/forward/link
|
|
158
|
+
console.log(meta?.direction); // "forward" | "back" | "unknown"
|
|
159
|
+
console.log(meta?.sourceElement); // the DOM element that initiated the nav, or null
|
|
160
|
+
console.log(meta?.info); // data passed via navigation.navigate({ info })
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
In guards during browser-initiated navigation, meta is available on `toState.context.navigation` (written in `onTransitionStart`):
|
|
137
165
|
|
|
138
166
|
```typescript
|
|
139
|
-
|
|
167
|
+
import { getLifecycleApi } from "@real-router/core/api";
|
|
168
|
+
|
|
140
169
|
const lifecycle = getLifecycleApi(router);
|
|
141
|
-
lifecycle.addActivateGuard("checkout", () => () => {
|
|
142
|
-
const meta =
|
|
170
|
+
lifecycle.addActivateGuard("checkout", () => (toState) => {
|
|
171
|
+
const meta = toState.context.navigation;
|
|
143
172
|
if (meta?.userInitiated) {
|
|
144
173
|
// user clicked back/forward or a link
|
|
145
174
|
}
|
|
146
175
|
return true;
|
|
147
176
|
});
|
|
148
|
-
|
|
149
|
-
// After navigation — get metadata for a completed state
|
|
150
|
-
router.subscribe((state) => {
|
|
151
|
-
const meta = router.getNavigationMeta(state);
|
|
152
|
-
console.log(meta?.navigationType); // "push" | "replace" | "traverse" | "reload"
|
|
153
|
-
console.log(meta?.userInitiated); // true if user clicked back/forward/link
|
|
154
|
-
console.log(meta?.info); // data passed via navigation.navigate({ info })
|
|
155
|
-
});
|
|
156
177
|
```
|
|
157
178
|
|
|
158
|
-
|
|
179
|
+
In framework components, access via the route's context:
|
|
159
180
|
|
|
160
181
|
```typescript
|
|
161
|
-
//
|
|
162
|
-
const
|
|
163
|
-
const
|
|
182
|
+
// React example
|
|
183
|
+
const { route } = useRoute();
|
|
184
|
+
const meta = route.context.navigation;
|
|
185
|
+
```
|
|
164
186
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
### NavigationMeta
|
|
188
|
+
|
|
189
|
+
| Field | Type | Description |
|
|
190
|
+
| ----------------- | -------------------------------------------------- | ------------------------------------------------------ |
|
|
191
|
+
| `navigationType` | `"push" \| "replace" \| "traverse" \| "reload"` | Type of navigation |
|
|
192
|
+
| `userInitiated` | `boolean` | Whether the user clicked back/forward/link |
|
|
193
|
+
| `direction` | `"forward" \| "back" \| "unknown"` | Direction in the history stack |
|
|
194
|
+
| `sourceElement` | `Element \| null` | DOM element that initiated the navigation, or null |
|
|
195
|
+
| `info` | `unknown` | Ephemeral data from `navigation.navigate({ info })` |
|
|
196
|
+
|
|
197
|
+
### NavigationDirection
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
type NavigationDirection = "forward" | "back" | "unknown";
|
|
169
201
|
```
|
|
170
202
|
|
|
203
|
+
Exported from the package for use in type annotations.
|
|
204
|
+
|
|
171
205
|
### `buildUrl` vs `buildPath`
|
|
172
206
|
|
|
173
207
|
```typescript
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -39,9 +39,10 @@ interface NavigationBrowser {
|
|
|
39
39
|
entries: () => NavigationHistoryEntry[];
|
|
40
40
|
currentEntry: NavigationHistoryEntry | null;
|
|
41
41
|
}
|
|
42
|
+
type NavigationDirection = "forward" | "back" | "unknown";
|
|
42
43
|
/**
|
|
43
|
-
* Navigation metadata attached to State via
|
|
44
|
-
* Available in
|
|
44
|
+
* Navigation metadata attached to State via state.context.navigation.
|
|
45
|
+
* Available in subscribe callbacks and components after transition completes.
|
|
45
46
|
*/
|
|
46
47
|
interface NavigationMeta {
|
|
47
48
|
/** Type of navigation: push, replace, traverse, or reload */
|
|
@@ -50,12 +51,21 @@ interface NavigationMeta {
|
|
|
50
51
|
userInitiated: boolean;
|
|
51
52
|
/** Ephemeral info passed via navigation.navigate({ info }) — lost on page reload */
|
|
52
53
|
info?: unknown;
|
|
54
|
+
/** Direction of navigation in the history stack */
|
|
55
|
+
direction: NavigationDirection;
|
|
56
|
+
/** The DOM element that initiated the navigation (e.g., anchor tag), or null for programmatic */
|
|
57
|
+
sourceElement: Element | null;
|
|
53
58
|
}
|
|
54
59
|
//#endregion
|
|
55
60
|
//#region src/factory.d.ts
|
|
56
61
|
declare function navigationPluginFactory(opts?: Partial<NavigationPluginOptions>, browser?: NavigationBrowser): PluginFactory;
|
|
57
62
|
//#endregion
|
|
58
63
|
//#region src/index.d.ts
|
|
64
|
+
declare module "@real-router/types" {
|
|
65
|
+
interface StateContext {
|
|
66
|
+
navigation?: NavigationMeta;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
59
69
|
declare module "@real-router/core" {
|
|
60
70
|
interface Router {
|
|
61
71
|
buildUrl: (name: string, params?: Params) => string;
|
|
@@ -67,7 +77,6 @@ declare module "@real-router/core" {
|
|
|
67
77
|
getVisitedRoutes: () => string[];
|
|
68
78
|
getRouteVisitCount: (routeName: string) => number;
|
|
69
79
|
traverseToLast: (routeName: string) => Promise<State>;
|
|
70
|
-
getNavigationMeta: (state?: State) => NavigationMeta | undefined;
|
|
71
80
|
canGoBack: () => boolean;
|
|
72
81
|
canGoForward: () => boolean;
|
|
73
82
|
canGoBackTo: (routeName: string) => boolean;
|
|
@@ -75,5 +84,5 @@ declare module "@real-router/core" {
|
|
|
75
84
|
}
|
|
76
85
|
} //# sourceMappingURL=index.d.ts.map
|
|
77
86
|
//#endregion
|
|
78
|
-
export { type NavigationBrowser, type NavigationMeta, type NavigationPluginOptions, navigationPluginFactory };
|
|
87
|
+
export { type NavigationBrowser, type NavigationDirection, type NavigationMeta, type NavigationPluginOptions, navigationPluginFactory };
|
|
79
88
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;AAAA;AAAA,KAWJ,mBAAA;AAMZ;;;;AAAA,UAAiB,cAAA;EAIf;EAFA,cAAA;EAMA;EAJA,aAAA;EAMA;EAJA,IAAA;EAIsB;EAFtB,SAAA,EAAW,mBAAA;;EAEX,aAAA,EAAe,OAAA;AAAA;;;iBC/CD,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCLS,YAAA;IACR,UAAA,GAJa,cAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IAClC,QAAA,GAAW,GAAA,aAAgB,KAAA;IAC3B,mBAAA,GACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,KAAA;IAEF,QAAA,QAAgB,KAAA;IAChB,WAAA,QAAmB,KAAA;IACnB,UAAA,GAAa,SAAA;IACb,gBAAA;IACA,kBAAA,GAAqB,SAAA;IACrB,cAAA,GAAiB,SAAA,aAAsB,OAAA,CAAQ,KAAA;IAC/C,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`),t=require(`@real-router/core`);const n=()=>globalThis.window!==void 0&&!!globalThis.history;function r(e){if(!e)return e;let t=e;return t.startsWith(`/`)||(t=`/${t}`),t.endsWith(`/`)&&(t=t.slice(0,-1)),t}const i=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},a=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function o(e,t){return n=>{if(n){for(let r of Object.keys(n))if(r in e){let i=n[r],a=typeof e[r],o=typeof i;if(i!==void 0&&o!==a)throw Error(`[${t}] Invalid type for '${r}': expected ${a}, got ${o}`)}}}}function s(e,t,n){return(e.replace??!n)||!!e.reload&&t.path===n.path}function c(e,t){try{let n=new URL(e,globalThis.location.origin);return[`http:`,`https:`].includes(n.protocol)?n:(console.warn(`[${t}] Invalid URL protocol in ${e}`),null)}catch(n){return console.warn(`[${t}] Could not parse url ${e}`,n),null}}function l(e,t){if(t&&e.startsWith(t)){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e}function u(e,t){return t+e}function d(e,t,n){let r=c(e,n);return r?l(r.pathname,t)+r.search:null}const f={forceDeactivate:!0,base:``},p=`navigation-plugin`;function m(e){let t=globalThis.navigation;return{getLocation:()=>i(l(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,{state:n.state,history:n.history})},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry}}}function h(e,t,n){if(!e?.url)return;let r=new URL(e.url).pathname,i=l(r,n);return t.matchPath(i)??void 0}function g(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return h(e.entries()[i+r],t,n)}function _(e,t,n){return g(e,t,n,-1)}function v(e,t,n){return g(e,t,n,1)}function y(e,t,n,r){return e.entries().some(e=>h(e,t,n)?.name===r)}function b(e,t,n){let r=new Set;for(let i of e.entries()){let e=h(i,t,n);e&&r.add(e.name)}return[...r]}function x(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;function r(e){if(!e)return e;let t=e;return t.startsWith(`/`)||(t=`/${t}`),t.endsWith(`/`)&&(t=t.slice(0,-1)),t}const i=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},a=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function o(e,t){return n=>{if(n){for(let r of Object.keys(n))if(r in e){let i=n[r],a=typeof e[r],o=typeof i;if(i!==void 0&&o!==a)throw Error(`[${t}] Invalid type for '${r}': expected ${a}, got ${o}`)}}}}function s(e,t,n){return(e.replace??!n)||!!e.reload&&t.path===n.path}function c(e,t){try{let n=new URL(e,globalThis.location.origin);return[`http:`,`https:`].includes(n.protocol)?n:(console.warn(`[${t}] Invalid URL protocol in ${e}`),null)}catch(n){return console.warn(`[${t}] Could not parse url ${e}`,n),null}}function l(e,t){if(t&&e.startsWith(t)){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e}function u(e,t){return t+e}function d(e,t,n){let r=c(e,n);return r?l(r.pathname,t)+r.search:null}const f={forceDeactivate:!0,base:``},p=`navigation-plugin`;function m(e){let t=globalThis.navigation;return{getLocation:()=>i(l(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,{state:n.state,history:n.history})},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry}}}function h(e,t,n){if(!e?.url)return;let r=new URL(e.url).pathname,i=l(r,n);return t.matchPath(i)??void 0}function g(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return h(e.entries()[i+r],t,n)}function _(e,t,n){return g(e,t,n,-1)}function v(e,t,n){return g(e,t,n,1)}function y(e,t,n,r){return e.entries().some(e=>h(e,t,n)?.name===r)}function b(e,t,n){let r=new Set;for(let i of e.entries()){let e=h(i,t,n);e&&r.add(e.name)}return[...r]}function x(e,t,n,r){let i=0;for(let a of e.entries())h(a,t,n)?.name===r&&i++;return i}function S(e,t,n,r,i){for(let a=e.length-1;a>=0;a--){let o=e[a];if(o.key!==i&&h(o,n,r)?.name===t)return o}}function C(e){let t=e.currentEntry?.index;return t!=null&&t>0}function w(e){let t=e.currentEntry?.index;return t==null?!1:t<e.entries().length-1}function T(e,t,n,r){let i=e.currentEntry?.index;if(i==null)return!1;let a=e.entries();for(let e=i-1;e>=0;e--)if(h(a[e],t,n)?.name===r)return!0;return!1}function E(e,t,n){return e===`traverse`?t>n?`forward`:`back`:e===`push`?`forward`:`unknown`}function D(e){let{router:n,api:r,browser:i,isSyncingFromRouter:a,setSyncing:o,base:s,transitionOptions:c}=e,{allowNotFound:u}=r.getOptions();return function(d){if(!d.canIntercept||a()||!n.isActive())return;let f=new URL(d.destination.url),p=l(f.pathname,s)+f.search,m=r.matchPath(p),h=d.navigationType,g=i.currentEntry?.index??-1;e.setCapturedMeta({navigationType:h,userInitiated:d.userInitiated,info:d.info,direction:E(h,d.destination.index,g),sourceElement:d.sourceElement??null}),m?d.intercept({handler:async()=>{try{await n.navigate(m.name,m.params,{...c,signal:d.signal})}catch(e){e instanceof t.RouterError||O(e,n,i,o)}}}):u?d.intercept({handler:()=>{n.navigateToNotFound(p)}}):d.intercept({handler:async()=>{try{await n.navigateToDefault()}catch(e){e instanceof t.RouterError||O(e,n,i,o)}}})}}function O(e,t,n,r){console.error(`[navigation-plugin] Critical error in navigate handler`,e);try{let e=t.getState();if(e){let i=t.buildUrl(e.name,e.params);r(!0),n.navigate(i,{state:{name:e.name,params:e.params,path:e.path},history:`replace`}),r(!1)}}catch(e){console.error(`[navigation-plugin] Failed to recover from critical error`,e)}}function k(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function A(e,t,n,r,i){return(a,o={})=>{let s=e.buildState(a,o);if(!s)throw Error(`[real-router] Cannot replace state: route "${a}" is not found`);let c=e.makeState(s.name,s.params,t.buildPath(s.name,s.params),{params:s.meta}),l=r(a,o),u={name:c.name,params:c.params,path:c.path};i(!0),n.replaceState(u,l),i(!1)}}function j(e,t,n){return e.reload&&t.path===n?.path?`reload`:s(e,t,n)?`replace`:`push`}var M=class{#e;#t;#n;#r;#i;#a;#o;#s;#c=!1;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#o=t.claimContextNamespace(`navigation`),this.#i=k(t,r);let o=(t,r)=>u(e.buildPath(t,r),n.base);this.#a=t.extendRouter({buildUrl:o,matchUrl:e=>{let r=d(e,n.base,p);return r?t.matchPath(r):void 0},replaceHistoryState:A(t,e,r,o,e=>{this.#c=e}),peekBack:()=>_(r,t,n.base),peekForward:()=>v(r,t,n.base),hasVisited:e=>y(r,t,n.base,e),getVisitedRoutes:()=>b(r,t,n.base),getRouteVisitCount:e=>x(r,t,n.base,e),traverseToLast:e=>this.traverseToLast(e),canGoBack:()=>C(r),canGoForward:()=>w(r),canGoBackTo:e=>T(r,t,n.base,e)}),this.#s=N({browser:r,shared:a,handler:D({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#c,setSyncing:e=>{this.#c=e},setCapturedMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a,releaseClaim:()=>{this.#o.release()}})}async traverseToLast(e){let t=this.#r.entries(),n=this.#r.currentEntry?.key,r=S(t,e,this.#t,this.#n.base,n);if(!r)throw Error(`No history entry for route "${e}"`);if(!r.url)throw Error(`No matching route for entry URL "${r.url}"`);let i=new URL(r.url),a=l(i.pathname,this.#n.base)+i.search,o=this.#t.matchPath(a);if(!o)throw Error(`No matching route for entry URL "${r.url}"`);let s=this.#r.currentEntry?.index??-1;return this.#l={navigationType:`traverse`,userInitiated:!1,direction:r.index>s?`forward`:`back`,sourceElement:null},this.#u=r.key,this.#e.navigate(o.name,o.params)}getPlugin(){return{...this.#s,onTransitionStart:e=>{this.#l&&this.#o.write(e,this.#l)},onTransitionSuccess:(e,n,r)=>{if(!this.#l){let t=j(r,e,n);this.#l={navigationType:t,userInitiated:!1,direction:t===`push`?`forward`:`unknown`,sourceElement:null}}if(this.#o.write(e,Object.freeze(this.#l)),this.#l=void 0,this.#c=!0,this.#u)this.#r.traverseTo(this.#u),this.#u=void 0;else{let i=this.#e.buildUrl(e.name,e.params),a=!n||n.path===e.path?i+this.#r.getHash():i,o={name:e.name,params:e.params,path:e.path};if(e.name===t.UNKNOWN_ROUTE)this.#r.updateCurrentEntry({state:o});else{let t=s(r,e,n);this.#r.navigate(a,{state:o,history:t?`replace`:`push`})}}this.#c=!1},onTransitionCancel:()=>{this.#l=void 0,this.#u=void 0},onTransitionError:()=>{this.#l=void 0,this.#u=void 0}}}};function N(e){return{onStart(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=e.browser.addNavigateListener(e.handler)},onStop(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0},teardown(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0,e.removeStartInterceptor(),e.removeExtensions(),e.releaseClaim()}}}const P=()=>{},F=e=>{let t=a(e);return{getLocation:()=>(t(`getLocation`),`/`),getHash:()=>(t(`getHash`),``),navigate:()=>{t(`navigate`)},replaceState:()=>{t(`replaceState`)},updateCurrentEntry:()=>{t(`updateCurrentEntry`)},traverseTo:()=>{t(`traverseTo`)},addNavigateListener:()=>(t(`addNavigateListener`),P),entries:()=>(t(`entries`),[]),currentEntry:null}},I=o(f,p);function L(t,i){if(!i&&n()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);I(t);let a={...f,...t};a.base=r(a.base);let o=i??R(a.base),s={forceDeactivate:a.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new M(t,(0,e.getPluginApi)(t),a,o,s,c).getPlugin()}function R(e){return`navigation`in globalThis?m(e):F(`navigation-plugin`)}exports.navigationPluginFactory=L;
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["RouterError","#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#lifecycle","#metaByState","#isSyncingFromRouter","#pendingMeta","#pendingTraverseKey","UNKNOWN_ROUTE"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path: ensures leading slash, removes trailing slash.\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base;\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\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 const value = opts[key as keyof typeof opts];\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (value !== undefined && actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n }\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): (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 updateBrowserState(builtState, buildUrl(name, params), 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 (!!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 (base && pathname.startsWith(base)) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname;\n}\n\nexport function buildUrl(path: string, base: string): string {\n return 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","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, {\n state: options.state,\n history: options.history,\n });\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n };\n}\n","import { extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n const pathname = new URL(entry.url).pathname;\n const path = extractPath(pathname, base);\n\n return api.matchPath(path) ?? undefined;\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n return browser.entries().filter((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n }).length;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { RouterError } from \"@real-router/core\";\n\nimport { extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser, NavigationMeta } from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setPendingMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept) {\n return;\n }\n if (isSyncingFromRouter()) {\n return;\n }\n if (!router.isActive()) {\n return;\n }\n\n const destinationUrl = new URL(event.destination.url);\n const path =\n extractPath(destinationUrl.pathname, base) + destinationUrl.search;\n const matchedState = api.matchPath(path);\n\n // Set pendingMeta BEFORE event.intercept() — available in guards via getNavigationMeta()\n deps.setPendingMeta({\n navigationType: event.navigationType as NavigationMeta[\"navigationType\"],\n userInitiated: event.userInitiated,\n info: event.info,\n });\n\n if (matchedState) {\n event.intercept({\n handler: async () => {\n try {\n await router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n });\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n event.intercept({\n handler: async () => {\n try {\n await router.navigateToDefault();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n setSyncing(false);\n }\n } catch (recoveryError) {\n console.error(\n \"[navigation-plugin] Failed to recover from critical error\",\n recoveryError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params);\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n browser.replaceState(historyState, url);\n setSyncing(false);\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport {\n shouldReplaceHistory,\n buildUrl,\n extractPath,\n urlToPath,\n} from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT } from \"./constants\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nfunction deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n readonly #metaByState = new WeakMap<State, NavigationMeta>();\n #pendingMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#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 (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n getNavigationMeta: (state?: State): NavigationMeta | undefined => {\n if (!state) {\n return this.#pendingMeta;\n }\n\n return this.#metaByState.get(state);\n },\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setPendingMeta: (meta) => {\n this.#pendingMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const entry = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const parsedUrl = new URL(entry.url);\n const path =\n extractPath(parsedUrl.pathname, this.#options.base) + parsedUrl.search;\n const matchedState = this.#api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n this.#pendingMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#pendingMeta) {\n this.#pendingMeta = {\n navigationType: deriveNavigationType(\n navOptions,\n toState,\n fromState,\n ),\n userInitiated: false,\n };\n }\n\n this.#metaByState.set(toState, this.#pendingMeta);\n this.#pendingMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n if (this.#pendingTraverseKey) {\n this.#browser.traverseTo(this.#pendingTraverseKey);\n this.#pendingTraverseKey = undefined;\n } else {\n const url = this.#router.buildUrl(toState.name, toState.params);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const finalUrl = shouldPreserveHash\n ? url + this.#browser.getHash()\n : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n\n this.#isSyncingFromRouter = false;\n },\n\n onTransitionCancel: () => {\n this.#pendingMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#pendingMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n };\n};\n","import { createOptionsValidator } from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env/index.js\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["RouterError","#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle","#isSyncingFromRouter","#capturedMeta","#pendingTraverseKey","UNKNOWN_ROUTE"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path: ensures leading slash, removes trailing slash.\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base;\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\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 const value = opts[key as keyof typeof opts];\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (value !== undefined && actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n }\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): (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 updateBrowserState(builtState, buildUrl(name, params), 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 (!!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 (base && pathname.startsWith(base)) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname;\n}\n\nexport function buildUrl(path: string, base: string): string {\n return 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","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, {\n state: options.state,\n history: options.history,\n });\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n };\n}\n","import { extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n const pathname = new URL(entry.url).pathname;\n const path = extractPath(pathname, base);\n\n return api.matchPath(path) ?? undefined;\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n let count = 0;\n\n for (const entry of browser.entries()) {\n if (entryToState(entry, api, base)?.name === routeName) {\n count++;\n }\n }\n\n return count;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { RouterError } from \"@real-router/core\";\n\nimport { extractPath } from \"./browser-env/index.js\";\n\nimport type {\n NavigationBrowser,\n NavigationDirection,\n NavigationMeta,\n} from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setCapturedMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function computeDirection(\n navigationType: NavigationMeta[\"navigationType\"],\n destinationIndex: number,\n currentIndex: number,\n): NavigationDirection {\n if (navigationType === \"traverse\") {\n return destinationIndex > currentIndex ? \"forward\" : \"back\";\n }\n\n return navigationType === \"push\" ? \"forward\" : \"unknown\";\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept) {\n return;\n }\n if (isSyncingFromRouter()) {\n return;\n }\n if (!router.isActive()) {\n return;\n }\n\n const destinationUrl = new URL(event.destination.url);\n const path =\n extractPath(destinationUrl.pathname, base) + destinationUrl.search;\n const matchedState = api.matchPath(path);\n\n const navType = event.navigationType as NavigationMeta[\"navigationType\"];\n const currentIndex = browser.currentEntry?.index ?? -1;\n\n deps.setCapturedMeta({\n navigationType: navType,\n userInitiated: event.userInitiated,\n info: event.info,\n direction: computeDirection(\n navType,\n event.destination.index,\n currentIndex,\n ),\n sourceElement: event.sourceElement ?? null,\n });\n\n if (matchedState) {\n event.intercept({\n handler: async () => {\n try {\n await router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n });\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n event.intercept({\n handler: async () => {\n try {\n await router.navigateToDefault();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n setSyncing(false);\n }\n } catch (recoveryError) {\n console.error(\n \"[navigation-plugin] Failed to recover from critical error\",\n recoveryError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params);\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n browser.replaceState(historyState, url);\n setSyncing(false);\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport {\n shouldReplaceHistory,\n buildUrl,\n extractPath,\n urlToPath,\n} from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT } from \"./constants\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: NavigationMeta) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n #capturedMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#claim = api.claimContextNamespace(\"navigation\");\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) => {\n 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 (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setCapturedMeta: (meta) => {\n this.#capturedMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n releaseClaim: () => {\n this.#claim.release();\n },\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const entry = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const parsedUrl = new URL(entry.url);\n const path =\n extractPath(parsedUrl.pathname, this.#options.base) + parsedUrl.search;\n const matchedState = this.#api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n /* v8 ignore next -- @preserve: currentEntry always exists when traverseToLast is callable (after start) */\n const currentIndex = this.#browser.currentEntry?.index ?? -1;\n\n this.#capturedMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n direction: entry.index > currentIndex ? \"forward\" : \"back\",\n sourceElement: null,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionStart: (toState: State) => {\n if (this.#capturedMeta) {\n this.#claim.write(toState, this.#capturedMeta);\n }\n },\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#capturedMeta) {\n const navigationType = deriveNavigationType(\n navOptions,\n toState,\n fromState,\n );\n\n this.#capturedMeta = {\n navigationType,\n userInitiated: false,\n direction: navigationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n this.#claim.write(toState, Object.freeze(this.#capturedMeta));\n this.#capturedMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n if (this.#pendingTraverseKey) {\n this.#browser.traverseTo(this.#pendingTraverseKey);\n this.#pendingTraverseKey = undefined;\n } else {\n const url = this.#router.buildUrl(toState.name, toState.params);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const finalUrl = shouldPreserveHash\n ? url + this.#browser.getHash()\n : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n\n this.#isSyncingFromRouter = false;\n },\n\n onTransitionCancel: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n releaseClaim: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n deps.releaseClaim();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n };\n};\n","import { createOptionsValidator } from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env/index.js\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -39,9 +39,10 @@ interface NavigationBrowser {
|
|
|
39
39
|
entries: () => NavigationHistoryEntry[];
|
|
40
40
|
currentEntry: NavigationHistoryEntry | null;
|
|
41
41
|
}
|
|
42
|
+
type NavigationDirection = "forward" | "back" | "unknown";
|
|
42
43
|
/**
|
|
43
|
-
* Navigation metadata attached to State via
|
|
44
|
-
* Available in
|
|
44
|
+
* Navigation metadata attached to State via state.context.navigation.
|
|
45
|
+
* Available in subscribe callbacks and components after transition completes.
|
|
45
46
|
*/
|
|
46
47
|
interface NavigationMeta {
|
|
47
48
|
/** Type of navigation: push, replace, traverse, or reload */
|
|
@@ -50,12 +51,21 @@ interface NavigationMeta {
|
|
|
50
51
|
userInitiated: boolean;
|
|
51
52
|
/** Ephemeral info passed via navigation.navigate({ info }) — lost on page reload */
|
|
52
53
|
info?: unknown;
|
|
54
|
+
/** Direction of navigation in the history stack */
|
|
55
|
+
direction: NavigationDirection;
|
|
56
|
+
/** The DOM element that initiated the navigation (e.g., anchor tag), or null for programmatic */
|
|
57
|
+
sourceElement: Element | null;
|
|
53
58
|
}
|
|
54
59
|
//#endregion
|
|
55
60
|
//#region src/factory.d.ts
|
|
56
61
|
declare function navigationPluginFactory(opts?: Partial<NavigationPluginOptions>, browser?: NavigationBrowser): PluginFactory;
|
|
57
62
|
//#endregion
|
|
58
63
|
//#region src/index.d.ts
|
|
64
|
+
declare module "@real-router/types" {
|
|
65
|
+
interface StateContext {
|
|
66
|
+
navigation?: NavigationMeta;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
59
69
|
declare module "@real-router/core" {
|
|
60
70
|
interface Router {
|
|
61
71
|
buildUrl: (name: string, params?: Params) => string;
|
|
@@ -67,7 +77,6 @@ declare module "@real-router/core" {
|
|
|
67
77
|
getVisitedRoutes: () => string[];
|
|
68
78
|
getRouteVisitCount: (routeName: string) => number;
|
|
69
79
|
traverseToLast: (routeName: string) => Promise<State>;
|
|
70
|
-
getNavigationMeta: (state?: State) => NavigationMeta | undefined;
|
|
71
80
|
canGoBack: () => boolean;
|
|
72
81
|
canGoForward: () => boolean;
|
|
73
82
|
canGoBackTo: (routeName: string) => boolean;
|
|
@@ -75,5 +84,5 @@ declare module "@real-router/core" {
|
|
|
75
84
|
}
|
|
76
85
|
} //# sourceMappingURL=index.d.ts.map
|
|
77
86
|
//#endregion
|
|
78
|
-
export { type NavigationBrowser, type NavigationMeta, type NavigationPluginOptions, navigationPluginFactory };
|
|
87
|
+
export { type NavigationBrowser, type NavigationDirection, type NavigationMeta, type NavigationPluginOptions, navigationPluginFactory };
|
|
79
88
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/esm/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;AAAA
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAIA;UAAiB,uBAAA;;;;AAoBjB;;EAdE,eAAA;EAwBgC;;;;;EAjBhC,IAAA;AAAA;;;;;UAOe,iBAAA;EACf,WAAA;EACA,OAAA;EACA,QAAA,GACE,GAAA,UACA,OAAA;IAAW,KAAA;IAAgB,OAAA;EAAA;EAE7B,YAAA,GAAe,KAAA,WAAgB,GAAA;EAC/B,kBAAA,GAAqB,OAAA;IAAW,KAAA;EAAA;EAChC,UAAA,GAAa,GAAA;EACb,mBAAA,GAAsB,EAAA,GAAK,GAAA,EAAK,aAAA;EAChC,OAAA,QAAe,sBAAA;EACf,YAAA,EAAc,sBAAA;AAAA;AAAA,KAWJ,mBAAA;AAMZ;;;;AAAA,UAAiB,cAAA;EAIf;EAFA,cAAA;EAMA;EAJA,aAAA;EAMA;EAJA,IAAA;EAIsB;EAFtB,SAAA,EAAW,mBAAA;;EAEX,aAAA,EAAe,OAAA;AAAA;;;iBC/CD,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCLS,YAAA;IACR,UAAA,GAJa,cAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,QAAA,GAAW,IAAA,UAAc,MAAA,GAAS,MAAA;IAClC,QAAA,GAAW,GAAA,aAAgB,KAAA;IAC3B,mBAAA,GACE,IAAA,UACA,MAAA,GAAS,MAAA,EACT,KAAA;IAEF,QAAA,QAAgB,KAAA;IAChB,WAAA,QAAmB,KAAA;IACnB,UAAA,GAAa,SAAA;IACb,gBAAA;IACA,kBAAA,GAAqB,SAAA;IACrB,cAAA,GAAiB,SAAA,aAAsB,OAAA,CAAQ,KAAA;IAC/C,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getPluginApi as e}from"@real-router/core/api";import{RouterError as t,UNKNOWN_ROUTE as n}from"@real-router/core";const r=()=>globalThis.window!==void 0&&!!globalThis.history;function i(e){if(!e)return e;let t=e;return t.startsWith(`/`)||(t=`/${t}`),t.endsWith(`/`)&&(t=t.slice(0,-1)),t}const a=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},o=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function s(e,t){return n=>{if(n){for(let r of Object.keys(n))if(r in e){let i=n[r],a=typeof e[r],o=typeof i;if(i!==void 0&&o!==a)throw Error(`[${t}] Invalid type for '${r}': expected ${a}, got ${o}`)}}}}function c(e,t,n){return(e.replace??!n)||!!e.reload&&t.path===n.path}function l(e,t){try{let n=new URL(e,globalThis.location.origin);return[`http:`,`https:`].includes(n.protocol)?n:(console.warn(`[${t}] Invalid URL protocol in ${e}`),null)}catch(n){return console.warn(`[${t}] Could not parse url ${e}`,n),null}}function u(e,t){if(t&&e.startsWith(t)){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e}function d(e,t){return t+e}function f(e,t,n){let r=l(e,n);return r?u(r.pathname,t)+r.search:null}const p={forceDeactivate:!0,base:``},m=`navigation-plugin`;function h(e){let t=globalThis.navigation;return{getLocation:()=>a(u(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,{state:n.state,history:n.history})},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry}}}function g(e,t,n){if(!e?.url)return;let r=new URL(e.url).pathname,i=u(r,n);return t.matchPath(i)??void 0}function _(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return g(e.entries()[i+r],t,n)}function v(e,t,n){return _(e,t,n,-1)}function y(e,t,n){return _(e,t,n,1)}function b(e,t,n,r){return e.entries().some(e=>g(e,t,n)?.name===r)}function x(e,t,n){let r=new Set;for(let i of e.entries()){let e=g(i,t,n);e&&r.add(e.name)}return[...r]}function S(e,t,n,r){
|
|
1
|
+
import{getPluginApi as e}from"@real-router/core/api";import{RouterError as t,UNKNOWN_ROUTE as n}from"@real-router/core";const r=()=>globalThis.window!==void 0&&!!globalThis.history;function i(e){if(!e)return e;let t=e;return t.startsWith(`/`)||(t=`/${t}`),t.endsWith(`/`)&&(t=t.slice(0,-1)),t}const a=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}},o=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${n}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}};function s(e,t){return n=>{if(n){for(let r of Object.keys(n))if(r in e){let i=n[r],a=typeof e[r],o=typeof i;if(i!==void 0&&o!==a)throw Error(`[${t}] Invalid type for '${r}': expected ${a}, got ${o}`)}}}}function c(e,t,n){return(e.replace??!n)||!!e.reload&&t.path===n.path}function l(e,t){try{let n=new URL(e,globalThis.location.origin);return[`http:`,`https:`].includes(n.protocol)?n:(console.warn(`[${t}] Invalid URL protocol in ${e}`),null)}catch(n){return console.warn(`[${t}] Could not parse url ${e}`,n),null}}function u(e,t){if(t&&e.startsWith(t)){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e}function d(e,t){return t+e}function f(e,t,n){let r=l(e,n);return r?u(r.pathname,t)+r.search:null}const p={forceDeactivate:!0,base:``},m=`navigation-plugin`;function h(e){let t=globalThis.navigation;return{getLocation:()=>a(u(globalThis.location.pathname,e))+globalThis.location.search,getHash:()=>globalThis.location.hash,navigate:(e,n)=>{t.navigate(e,{state:n.state,history:n.history})},replaceState:(e,n)=>{t.navigate(n,{state:e,history:`replace`})},updateCurrentEntry:e=>{t.updateCurrentEntry(e)},traverseTo:e=>{t.traverseTo(e)},addNavigateListener:e=>(t.addEventListener(`navigate`,e),()=>{t.removeEventListener(`navigate`,e)}),entries:()=>t.entries(),get currentEntry(){return t.currentEntry}}}function g(e,t,n){if(!e?.url)return;let r=new URL(e.url).pathname,i=u(r,n);return t.matchPath(i)??void 0}function _(e,t,n,r){let i=e.currentEntry?.index;if(i!=null)return g(e.entries()[i+r],t,n)}function v(e,t,n){return _(e,t,n,-1)}function y(e,t,n){return _(e,t,n,1)}function b(e,t,n,r){return e.entries().some(e=>g(e,t,n)?.name===r)}function x(e,t,n){let r=new Set;for(let i of e.entries()){let e=g(i,t,n);e&&r.add(e.name)}return[...r]}function S(e,t,n,r){let i=0;for(let a of e.entries())g(a,t,n)?.name===r&&i++;return i}function C(e,t,n,r,i){for(let a=e.length-1;a>=0;a--){let o=e[a];if(o.key!==i&&g(o,n,r)?.name===t)return o}}function w(e){let t=e.currentEntry?.index;return t!=null&&t>0}function T(e){let t=e.currentEntry?.index;return t==null?!1:t<e.entries().length-1}function E(e,t,n,r){let i=e.currentEntry?.index;if(i==null)return!1;let a=e.entries();for(let e=i-1;e>=0;e--)if(g(a[e],t,n)?.name===r)return!0;return!1}function D(e,t,n){return e===`traverse`?t>n?`forward`:`back`:e===`push`?`forward`:`unknown`}function O(e){let{router:n,api:r,browser:i,isSyncingFromRouter:a,setSyncing:o,base:s,transitionOptions:c}=e,{allowNotFound:l}=r.getOptions();return function(d){if(!d.canIntercept||a()||!n.isActive())return;let f=new URL(d.destination.url),p=u(f.pathname,s)+f.search,m=r.matchPath(p),h=d.navigationType,g=i.currentEntry?.index??-1;e.setCapturedMeta({navigationType:h,userInitiated:d.userInitiated,info:d.info,direction:D(h,d.destination.index,g),sourceElement:d.sourceElement??null}),m?d.intercept({handler:async()=>{try{await n.navigate(m.name,m.params,{...c,signal:d.signal})}catch(e){e instanceof t||k(e,n,i,o)}}}):l?d.intercept({handler:()=>{n.navigateToNotFound(p)}}):d.intercept({handler:async()=>{try{await n.navigateToDefault()}catch(e){e instanceof t||k(e,n,i,o)}}})}}function k(e,t,n,r){console.error(`[navigation-plugin] Critical error in navigate handler`,e);try{let e=t.getState();if(e){let i=t.buildUrl(e.name,e.params);r(!0),n.navigate(i,{state:{name:e.name,params:e.params,path:e.path},history:`replace`}),r(!1)}}catch(e){console.error(`[navigation-plugin] Failed to recover from critical error`,e)}}function A(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function j(e,t,n,r,i){return(a,o={})=>{let s=e.buildState(a,o);if(!s)throw Error(`[real-router] Cannot replace state: route "${a}" is not found`);let c=e.makeState(s.name,s.params,t.buildPath(s.name,s.params),{params:s.meta}),l=r(a,o),u={name:c.name,params:c.params,path:c.path};i(!0),n.replaceState(u,l),i(!1)}}function M(e,t,n){return e.reload&&t.path===n?.path?`reload`:c(e,t,n)?`replace`:`push`}var N=class{#e;#t;#n;#r;#i;#a;#o;#s;#c=!1;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#o=t.claimContextNamespace(`navigation`),this.#i=A(t,r);let o=(t,r)=>d(e.buildPath(t,r),n.base);this.#a=t.extendRouter({buildUrl:o,matchUrl:e=>{let r=f(e,n.base,m);return r?t.matchPath(r):void 0},replaceHistoryState:j(t,e,r,o,e=>{this.#c=e}),peekBack:()=>v(r,t,n.base),peekForward:()=>y(r,t,n.base),hasVisited:e=>b(r,t,n.base,e),getVisitedRoutes:()=>x(r,t,n.base),getRouteVisitCount:e=>S(r,t,n.base,e),traverseToLast:e=>this.traverseToLast(e),canGoBack:()=>w(r),canGoForward:()=>T(r),canGoBackTo:e=>E(r,t,n.base,e)}),this.#s=P({browser:r,shared:a,handler:O({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#c,setSyncing:e=>{this.#c=e},setCapturedMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a,releaseClaim:()=>{this.#o.release()}})}async traverseToLast(e){let t=this.#r.entries(),n=this.#r.currentEntry?.key,r=C(t,e,this.#t,this.#n.base,n);if(!r)throw Error(`No history entry for route "${e}"`);if(!r.url)throw Error(`No matching route for entry URL "${r.url}"`);let i=new URL(r.url),a=u(i.pathname,this.#n.base)+i.search,o=this.#t.matchPath(a);if(!o)throw Error(`No matching route for entry URL "${r.url}"`);let s=this.#r.currentEntry?.index??-1;return this.#l={navigationType:`traverse`,userInitiated:!1,direction:r.index>s?`forward`:`back`,sourceElement:null},this.#u=r.key,this.#e.navigate(o.name,o.params)}getPlugin(){return{...this.#s,onTransitionStart:e=>{this.#l&&this.#o.write(e,this.#l)},onTransitionSuccess:(e,t,r)=>{if(!this.#l){let n=M(r,e,t);this.#l={navigationType:n,userInitiated:!1,direction:n===`push`?`forward`:`unknown`,sourceElement:null}}if(this.#o.write(e,Object.freeze(this.#l)),this.#l=void 0,this.#c=!0,this.#u)this.#r.traverseTo(this.#u),this.#u=void 0;else{let i=this.#e.buildUrl(e.name,e.params),a=!t||t.path===e.path?i+this.#r.getHash():i,o={name:e.name,params:e.params,path:e.path};if(e.name===n)this.#r.updateCurrentEntry({state:o});else{let n=c(r,e,t);this.#r.navigate(a,{state:o,history:n?`replace`:`push`})}}this.#c=!1},onTransitionCancel:()=>{this.#l=void 0,this.#u=void 0},onTransitionError:()=>{this.#l=void 0,this.#u=void 0}}}};function P(e){return{onStart(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=e.browser.addNavigateListener(e.handler)},onStop(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0},teardown(){e.shared.removeNavigateListener?.(),e.shared.removeNavigateListener=void 0,e.removeStartInterceptor(),e.removeExtensions(),e.releaseClaim()}}}const F=()=>{},I=e=>{let t=o(e);return{getLocation:()=>(t(`getLocation`),`/`),getHash:()=>(t(`getHash`),``),navigate:()=>{t(`navigate`)},replaceState:()=>{t(`replaceState`)},updateCurrentEntry:()=>{t(`updateCurrentEntry`)},traverseTo:()=>{t(`traverseTo`)},addNavigateListener:()=>(t(`addNavigateListener`),F),entries:()=>(t(`entries`),[]),currentEntry:null}},L=s(p,m);function R(t,n){if(!n&&r()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);L(t);let a={...p,...t};a.base=i(a.base);let o=n??z(a.base),s={forceDeactivate:a.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new N(t,e(t),a,o,s,c).getPlugin()}function z(e){return`navigation`in globalThis?h(e):I(`navigation-plugin`)}export{R as navigationPluginFactory};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#lifecycle","#metaByState","#isSyncingFromRouter","#pendingMeta","#pendingTraverseKey"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path: ensures leading slash, removes trailing slash.\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base;\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\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 const value = opts[key as keyof typeof opts];\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (value !== undefined && actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n }\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): (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 updateBrowserState(builtState, buildUrl(name, params), 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 (!!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 (base && pathname.startsWith(base)) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname;\n}\n\nexport function buildUrl(path: string, base: string): string {\n return 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","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, {\n state: options.state,\n history: options.history,\n });\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n };\n}\n","import { extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n const pathname = new URL(entry.url).pathname;\n const path = extractPath(pathname, base);\n\n return api.matchPath(path) ?? undefined;\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n return browser.entries().filter((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n }).length;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { RouterError } from \"@real-router/core\";\n\nimport { extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser, NavigationMeta } from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setPendingMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept) {\n return;\n }\n if (isSyncingFromRouter()) {\n return;\n }\n if (!router.isActive()) {\n return;\n }\n\n const destinationUrl = new URL(event.destination.url);\n const path =\n extractPath(destinationUrl.pathname, base) + destinationUrl.search;\n const matchedState = api.matchPath(path);\n\n // Set pendingMeta BEFORE event.intercept() — available in guards via getNavigationMeta()\n deps.setPendingMeta({\n navigationType: event.navigationType as NavigationMeta[\"navigationType\"],\n userInitiated: event.userInitiated,\n info: event.info,\n });\n\n if (matchedState) {\n event.intercept({\n handler: async () => {\n try {\n await router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n });\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n event.intercept({\n handler: async () => {\n try {\n await router.navigateToDefault();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n setSyncing(false);\n }\n } catch (recoveryError) {\n console.error(\n \"[navigation-plugin] Failed to recover from critical error\",\n recoveryError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params);\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n browser.replaceState(historyState, url);\n setSyncing(false);\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport {\n shouldReplaceHistory,\n buildUrl,\n extractPath,\n urlToPath,\n} from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT } from \"./constants\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nfunction deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n readonly #metaByState = new WeakMap<State, NavigationMeta>();\n #pendingMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#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 (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n getNavigationMeta: (state?: State): NavigationMeta | undefined => {\n if (!state) {\n return this.#pendingMeta;\n }\n\n return this.#metaByState.get(state);\n },\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setPendingMeta: (meta) => {\n this.#pendingMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const entry = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const parsedUrl = new URL(entry.url);\n const path =\n extractPath(parsedUrl.pathname, this.#options.base) + parsedUrl.search;\n const matchedState = this.#api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n this.#pendingMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#pendingMeta) {\n this.#pendingMeta = {\n navigationType: deriveNavigationType(\n navOptions,\n toState,\n fromState,\n ),\n userInitiated: false,\n };\n }\n\n this.#metaByState.set(toState, this.#pendingMeta);\n this.#pendingMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n if (this.#pendingTraverseKey) {\n this.#browser.traverseTo(this.#pendingTraverseKey);\n this.#pendingTraverseKey = undefined;\n } else {\n const url = this.#router.buildUrl(toState.name, toState.params);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const finalUrl = shouldPreserveHash\n ? url + this.#browser.getHash()\n : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n\n this.#isSyncingFromRouter = false;\n },\n\n onTransitionCancel: () => {\n this.#pendingMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#pendingMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n };\n};\n","import { createOptionsValidator } from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env/index.js\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#claim","#lifecycle","#isSyncingFromRouter","#capturedMeta","#pendingTraverseKey"],"sources":["../../../../shared/browser-env/detect.ts","../../../../shared/browser-env/utils.ts","../../../../shared/browser-env/ssr-fallback.ts","../../../../shared/browser-env/validation.ts","../../../../shared/browser-env/plugin-utils.ts","../../../../shared/browser-env/url-parsing.ts","../../../../shared/browser-env/url-utils.ts","../../src/constants.ts","../../src/navigation-browser.ts","../../src/history-extensions.ts","../../src/navigate-handler.ts","../../src/plugin-utils.ts","../../src/plugin.ts","../../src/ssr-fallback.ts","../../src/validation.ts","../../src/factory.ts"],"sourcesContent":["export const isBrowserEnvironment = (): boolean =>\n typeof globalThis.window !== \"undefined\" && !!globalThis.history;\n","/**\n * Normalizes base path: ensures leading slash, removes trailing slash.\n *\n * @example\n * normalizeBase(\"app\") // \"/app\"\n * normalizeBase(\"/app/\") // \"/app\"\n * normalizeBase(\"\") // \"\"\n */\nexport function normalizeBase(base: string): string {\n if (!base) {\n return base;\n }\n\n let result = base;\n\n if (!result.startsWith(\"/\")) {\n result = `/${result}`;\n }\n\n if (result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n\n return result;\n}\n\nexport const safelyEncodePath = (path: string): string => {\n try {\n return encodeURI(decodeURI(path));\n } catch (error) {\n console.warn(`[browser-env] Could not encode path \"${path}\"`, error);\n\n return path;\n }\n};\n","import type { HistoryBrowser } from \"./types.js\";\n\nconst NOOP = (): void => {};\n\nexport const createWarnOnce = (context: string) => {\n let hasWarned = false;\n\n return (method: string): void => {\n if (!hasWarned) {\n console.warn(\n `[browser-env] Browser API is running in a non-browser environment (context: \"${context}\"). ` +\n `Method \"${method}\" is a no-op. ` +\n `This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`,\n );\n hasWarned = true;\n }\n };\n};\n\nexport const createHistoryFallbackBrowser = (\n context: string,\n): HistoryBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n pushState: () => {\n warnOnce(\"pushState\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n addPopstateListener: () => {\n warnOnce(\"addPopstateListener\");\n\n return NOOP;\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n };\n};\n","export function createOptionsValidator<T extends object>(\n defaults: Required<T>,\n loggerContext: string,\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 const value = opts[key as keyof typeof opts];\n const expected = typeof defaults[key as keyof typeof defaults];\n const actual = typeof value;\n\n if (value !== undefined && actual !== expected) {\n throw new Error(\n `[${loggerContext}] Invalid type for '${key}': expected ${expected}, got ${actual}`,\n );\n }\n }\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): (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 updateBrowserState(builtState, buildUrl(name, params), 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 (!!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 (base && pathname.startsWith(base)) {\n const stripped = pathname.slice(base.length);\n\n return stripped.startsWith(\"/\") ? stripped : `/${stripped}`;\n }\n\n return pathname;\n}\n\nexport function buildUrl(path: string, base: string): string {\n return 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","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, {\n state: options.state,\n history: options.history,\n });\n },\n\n replaceState: (state, url) => {\n nav.navigate(url, {\n state,\n history: \"replace\",\n });\n },\n\n updateCurrentEntry: (options) => {\n nav.updateCurrentEntry(options);\n },\n\n traverseTo: (key) => {\n nav.traverseTo(key);\n },\n\n addNavigateListener: (fn) => {\n nav.addEventListener(\"navigate\", fn);\n\n return () => {\n nav.removeEventListener(\"navigate\", fn);\n };\n },\n\n entries: () => nav.entries(),\n\n get currentEntry() {\n return nav.currentEntry;\n },\n };\n}\n","import { extractPath } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\nimport type { State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Converts a NavigationHistoryEntry to a State via URL matching.\n * Uses URL matching (not entry.getState()) because:\n * - Entries before plugin init have no state\n * - Entries after router.replace(routes) may have stale state\n * - Entries from other SPAs on the same origin have foreign state\n */\nexport function entryToState(\n entry: NavigationHistoryEntry | undefined,\n api: PluginApi,\n base: string,\n): State | undefined {\n if (!entry?.url) {\n return undefined;\n }\n\n const pathname = new URL(entry.url).pathname;\n const path = extractPath(pathname, base);\n\n return api.matchPath(path) ?? undefined;\n}\n\nfunction peekAt(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n offset: number,\n): State | undefined {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return undefined;\n }\n\n return entryToState(browser.entries()[idx + offset], api, base);\n}\n\nexport function peekBack(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, -1);\n}\n\nexport function peekForward(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): State | undefined {\n return peekAt(browser, api, base, 1);\n}\n\nexport function hasVisited(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n return browser.entries().some((entry) => {\n const state = entryToState(entry, api, base);\n\n return state?.name === routeName;\n });\n}\n\nexport function getVisitedRoutes(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n): string[] {\n const names = new Set<string>();\n\n for (const entry of browser.entries()) {\n const state = entryToState(entry, api, base);\n\n if (state) {\n names.add(state.name);\n }\n }\n\n return [...names];\n}\n\nexport function getRouteVisitCount(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): number {\n let count = 0;\n\n for (const entry of browser.entries()) {\n if (entryToState(entry, api, base)?.name === routeName) {\n count++;\n }\n }\n\n return count;\n}\n\n/**\n * Finds the last NavigationHistoryEntry matching the given route name,\n * excluding the current entry (to avoid SAME_STATES on traverseToLast(\"current-route\")).\n */\nexport function findLastEntryForRoute(\n entries: NavigationHistoryEntry[],\n routeName: string,\n api: PluginApi,\n base: string,\n currentKey: string | undefined,\n): NavigationHistoryEntry | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n\n if (entry.key === currentKey) {\n continue;\n }\n\n const state = entryToState(entry, api, base);\n\n if (state?.name === routeName) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function canGoBack(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n return idx != null && idx > 0;\n}\n\nexport function canGoForward(browser: NavigationBrowser): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n return idx < browser.entries().length - 1;\n}\n\nexport function canGoBackTo(\n browser: NavigationBrowser,\n api: PluginApi,\n base: string,\n routeName: string,\n): boolean {\n const idx = browser.currentEntry?.index;\n\n if (idx == null) {\n return false;\n }\n\n const entries = browser.entries();\n\n for (let i = idx - 1; i >= 0; i--) {\n const state = entryToState(entries[i], api, base);\n\n if (state?.name === routeName) {\n return true;\n }\n }\n\n return false;\n}\n","import { RouterError } from \"@real-router/core\";\n\nimport { extractPath } from \"./browser-env/index.js\";\n\nimport type {\n NavigationBrowser,\n NavigationDirection,\n NavigationMeta,\n} from \"./types\";\nimport type { Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ninterface NavigateHandlerDeps {\n router: Router;\n api: PluginApi;\n browser: NavigationBrowser;\n isSyncingFromRouter: () => boolean;\n setSyncing: (value: boolean) => void;\n setCapturedMeta: (meta: NavigationMeta) => void;\n base: string;\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n };\n}\n\nexport function computeDirection(\n navigationType: NavigationMeta[\"navigationType\"],\n destinationIndex: number,\n currentIndex: number,\n): NavigationDirection {\n if (navigationType === \"traverse\") {\n return destinationIndex > currentIndex ? \"forward\" : \"back\";\n }\n\n return navigationType === \"push\" ? \"forward\" : \"unknown\";\n}\n\nexport function createNavigateHandler(deps: NavigateHandlerDeps) {\n const {\n router,\n api,\n browser,\n isSyncingFromRouter,\n setSyncing,\n base,\n transitionOptions,\n } = deps;\n const { allowNotFound } = api.getOptions();\n\n return function handleNavigateEvent(event: NavigateEvent): void {\n if (!event.canIntercept) {\n return;\n }\n if (isSyncingFromRouter()) {\n return;\n }\n if (!router.isActive()) {\n return;\n }\n\n const destinationUrl = new URL(event.destination.url);\n const path =\n extractPath(destinationUrl.pathname, base) + destinationUrl.search;\n const matchedState = api.matchPath(path);\n\n const navType = event.navigationType as NavigationMeta[\"navigationType\"];\n const currentIndex = browser.currentEntry?.index ?? -1;\n\n deps.setCapturedMeta({\n navigationType: navType,\n userInitiated: event.userInitiated,\n info: event.info,\n direction: computeDirection(\n navType,\n event.destination.index,\n currentIndex,\n ),\n sourceElement: event.sourceElement ?? null,\n });\n\n if (matchedState) {\n event.intercept({\n handler: async () => {\n try {\n await router.navigate(matchedState.name, matchedState.params, {\n ...transitionOptions,\n signal: event.signal,\n });\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n } else if (allowNotFound) {\n event.intercept({\n handler: () => {\n router.navigateToNotFound(path);\n },\n });\n } else {\n event.intercept({\n handler: async () => {\n try {\n await router.navigateToDefault();\n } catch (error) {\n if (!(error instanceof RouterError)) {\n recoverFromNavigateError(error, router, browser, setSyncing);\n }\n }\n },\n });\n }\n };\n}\n\nfunction recoverFromNavigateError(\n error: unknown,\n router: Router,\n browser: NavigationBrowser,\n setSyncing: (value: boolean) => void,\n): void {\n console.error(\n \"[navigation-plugin] Critical error in navigate handler\",\n error,\n );\n\n try {\n const currentState = router.getState();\n\n if (currentState) {\n const url = router.buildUrl(currentState.name, currentState.params);\n\n setSyncing(true);\n browser.navigate(url, {\n state: {\n name: currentState.name,\n params: currentState.params,\n path: currentState.path,\n },\n history: \"replace\",\n });\n setSyncing(false);\n }\n } catch (recoveryError) {\n console.error(\n \"[navigation-plugin] Failed to recover from critical error\",\n recoveryError,\n );\n }\n}\n","import type { NavigationBrowser } from \"./types\";\nimport type { Params, Router } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\n/**\n * Makes `router.start()` path optional by injecting browser location.\n * Identical to browser-env's createStartInterceptor, adapted for NavigationBrowser.\n */\nexport function createStartInterceptor(\n api: PluginApi,\n browser: NavigationBrowser,\n): () => void {\n return api.addInterceptor(\"start\", (next, path) =>\n next(path ?? browser.getLocation()),\n );\n}\n\n/**\n * Creates replaceHistoryState extension for NavigationBrowser.\n *\n * IMPORTANT: Must set isSyncingFromRouter=true before calling browser.replaceState\n * because navigation.navigate({history:\"replace\"}) fires a navigate event.\n * Without this flag, the navigate handler would trigger a full navigation.\n */\nexport function createReplaceHistoryState(\n api: PluginApi,\n router: Router,\n browser: NavigationBrowser,\n buildUrl: (name: string, params?: Params) => string,\n setSyncing: (value: boolean) => void,\n): (name: string, params?: Params) => void {\n return (name: string, params: Params = {}) => {\n const state = api.buildState(name, params);\n\n if (!state) {\n throw new Error(\n `[real-router] Cannot replace state: route \"${name}\" is not found`,\n );\n }\n\n const builtState = api.makeState(\n state.name,\n state.params,\n router.buildPath(state.name, state.params),\n {\n params: state.meta,\n },\n );\n\n const url = buildUrl(name, params);\n const historyState = {\n name: builtState.name,\n params: builtState.params,\n path: builtState.path,\n };\n\n setSyncing(true);\n browser.replaceState(historyState, url);\n setSyncing(false);\n };\n}\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\n\nimport {\n shouldReplaceHistory,\n buildUrl,\n extractPath,\n urlToPath,\n} from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT } from \"./constants\";\nimport {\n peekBack,\n peekForward,\n hasVisited,\n getVisitedRoutes,\n getRouteVisitCount,\n findLastEntryForRoute,\n canGoBack,\n canGoForward,\n canGoBackTo,\n} from \"./history-extensions\";\nimport { createNavigateHandler } from \"./navigate-handler\";\nimport {\n createStartInterceptor,\n createReplaceHistoryState,\n} from \"./plugin-utils\";\n\nimport type {\n NavigationBrowser,\n NavigationMeta,\n NavigationPluginOptions,\n NavigationSharedState,\n} from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nexport function deriveNavigationType(\n navOptions: NavigationOptions,\n toState: State,\n fromState: State | undefined,\n): NavigationMeta[\"navigationType\"] {\n if (navOptions.reload && toState.path === fromState?.path) {\n return \"reload\";\n }\n\n if (shouldReplaceHistory(navOptions, toState, fromState)) {\n return \"replace\";\n }\n\n return \"push\";\n}\n\nexport class NavigationPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<NavigationPluginOptions>;\n readonly #browser: NavigationBrowser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: NavigationMeta) => void;\n release: () => void;\n };\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n #isSyncingFromRouter = false;\n #capturedMeta: NavigationMeta | undefined;\n #pendingTraverseKey: string | undefined;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<NavigationPluginOptions>,\n browser: NavigationBrowser,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: NavigationSharedState,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#browser = browser;\n\n this.#claim = api.claimContextNamespace(\"navigation\");\n this.#removeStartInterceptor = createStartInterceptor(api, browser);\n\n const pluginBuildUrl = (route: string, params?: Params) => {\n const path = router.buildPath(route, params);\n\n return buildUrl(path, options.base);\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) => {\n 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 (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n ),\n\n peekBack: () => peekBack(browser, api, options.base),\n peekForward: () => peekForward(browser, api, options.base),\n hasVisited: (routeName: string) =>\n hasVisited(browser, api, options.base, routeName),\n getVisitedRoutes: () => getVisitedRoutes(browser, api, options.base),\n getRouteVisitCount: (routeName: string) =>\n getRouteVisitCount(browser, api, options.base, routeName),\n traverseToLast: (routeName: string) => this.traverseToLast(routeName),\n canGoBack: () => canGoBack(browser),\n canGoForward: () => canGoForward(browser),\n canGoBackTo: (routeName: string) =>\n canGoBackTo(browser, api, options.base, routeName),\n });\n\n const handler = createNavigateHandler({\n router,\n api,\n browser,\n isSyncingFromRouter: () => this.#isSyncingFromRouter,\n setSyncing: (syncing) => {\n this.#isSyncingFromRouter = syncing;\n },\n setCapturedMeta: (meta) => {\n this.#capturedMeta = meta;\n },\n base: options.base,\n transitionOptions,\n });\n\n this.#lifecycle = createNavigateLifecycle({\n browser,\n shared,\n handler,\n removeStartInterceptor: this.#removeStartInterceptor,\n removeExtensions: this.#removeExtensions,\n releaseClaim: () => {\n this.#claim.release();\n },\n });\n }\n\n async traverseToLast(routeName: string): Promise<State> {\n const entries = this.#browser.entries();\n const currentKey = this.#browser.currentEntry?.key;\n const entry = findLastEntryForRoute(\n entries,\n routeName,\n this.#api,\n this.#options.base,\n currentKey,\n );\n\n if (!entry) {\n throw new Error(`No history entry for route \"${routeName}\"`);\n }\n\n if (!entry.url) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n const parsedUrl = new URL(entry.url);\n const path =\n extractPath(parsedUrl.pathname, this.#options.base) + parsedUrl.search;\n const matchedState = this.#api.matchPath(path);\n\n if (!matchedState) {\n throw new Error(`No matching route for entry URL \"${entry.url}\"`);\n }\n\n /* v8 ignore next -- @preserve: currentEntry always exists when traverseToLast is callable (after start) */\n const currentIndex = this.#browser.currentEntry?.index ?? -1;\n\n this.#capturedMeta = {\n navigationType: \"traverse\",\n userInitiated: false,\n direction: entry.index > currentIndex ? \"forward\" : \"back\",\n sourceElement: null,\n };\n this.#pendingTraverseKey = entry.key;\n\n return this.#router.navigate(matchedState.name, matchedState.params);\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionStart: (toState: State) => {\n if (this.#capturedMeta) {\n this.#claim.write(toState, this.#capturedMeta);\n }\n },\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n if (!this.#capturedMeta) {\n const navigationType = deriveNavigationType(\n navOptions,\n toState,\n fromState,\n );\n\n this.#capturedMeta = {\n navigationType,\n userInitiated: false,\n direction: navigationType === \"push\" ? \"forward\" : \"unknown\",\n sourceElement: null,\n };\n }\n\n this.#claim.write(toState, Object.freeze(this.#capturedMeta));\n this.#capturedMeta = undefined;\n\n this.#isSyncingFromRouter = true;\n\n if (this.#pendingTraverseKey) {\n this.#browser.traverseTo(this.#pendingTraverseKey);\n this.#pendingTraverseKey = undefined;\n } else {\n const url = this.#router.buildUrl(toState.name, toState.params);\n const shouldPreserveHash =\n !fromState || fromState.path === toState.path;\n const finalUrl = shouldPreserveHash\n ? url + this.#browser.getHash()\n : url;\n const historyState = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (toState.name === UNKNOWN_ROUTE) {\n this.#browser.updateCurrentEntry({ state: historyState });\n } else {\n const replace = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n );\n\n this.#browser.navigate(finalUrl, {\n state: historyState,\n history: replace ? \"replace\" : \"push\",\n });\n }\n }\n\n this.#isSyncingFromRouter = false;\n },\n\n onTransitionCancel: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n\n onTransitionError: () => {\n this.#capturedMeta = undefined;\n this.#pendingTraverseKey = undefined;\n },\n };\n }\n}\n\ninterface NavigateLifecycleDeps {\n browser: NavigationBrowser;\n handler: (event: NavigateEvent) => void;\n removeStartInterceptor: () => void;\n removeExtensions: () => void;\n releaseClaim: () => void;\n shared: NavigationSharedState;\n}\n\nfunction createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {\n return {\n onStart() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = deps.browser.addNavigateListener(\n deps.handler,\n );\n },\n\n onStop() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n },\n\n teardown() {\n deps.shared.removeNavigateListener?.();\n deps.shared.removeNavigateListener = undefined;\n deps.removeStartInterceptor();\n deps.removeExtensions();\n deps.releaseClaim();\n },\n };\n}\n","import { createWarnOnce } from \"./browser-env/index.js\";\n\nimport type { NavigationBrowser } from \"./types\";\n\nconst NOOP = (): void => {};\n\nexport const createNavigationFallbackBrowser = (\n context: string,\n): NavigationBrowser => {\n const warnOnce = createWarnOnce(context);\n\n return {\n getLocation: () => {\n warnOnce(\"getLocation\");\n\n return \"/\";\n },\n getHash: () => {\n warnOnce(\"getHash\");\n\n return \"\";\n },\n navigate: () => {\n warnOnce(\"navigate\");\n },\n replaceState: () => {\n warnOnce(\"replaceState\");\n },\n updateCurrentEntry: () => {\n warnOnce(\"updateCurrentEntry\");\n },\n traverseTo: () => {\n warnOnce(\"traverseTo\");\n },\n addNavigateListener: () => {\n warnOnce(\"addNavigateListener\");\n\n return NOOP;\n },\n entries: () => {\n warnOnce(\"entries\");\n\n return [];\n },\n currentEntry: null,\n };\n};\n","import { createOptionsValidator } from \"./browser-env/index.js\";\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { NavigationPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<NavigationPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n);\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { isBrowserEnvironment, normalizeBase } from \"./browser-env/index.js\";\nimport { defaultOptions, source } from \"./constants\";\nimport { createNavigationBrowser } from \"./navigation-browser\";\nimport { NavigationPlugin } from \"./plugin\";\nimport { createNavigationFallbackBrowser } from \"./ssr-fallback\";\nimport { validateOptions } from \"./validation\";\n\nimport type {\n NavigationPluginOptions,\n NavigationBrowser,\n NavigationSharedState,\n} from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function navigationPluginFactory(\n opts?: Partial<NavigationPluginOptions>,\n browser?: NavigationBrowser,\n): PluginFactory {\n if (!browser && isBrowserEnvironment() && !(\"navigation\" in globalThis)) {\n throw new Error(\n \"[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.\",\n );\n }\n\n validateOptions(opts);\n\n const options: Required<NavigationPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n options.base = normalizeBase(options.base);\n\n const resolvedBrowser = browser ?? createBrowser(options.base);\n\n const forceDeactivate = options.forceDeactivate;\n const transitionOptions = { forceDeactivate, source, replace: true as const };\n const shared: NavigationSharedState = { removeNavigateListener: undefined };\n\n return (routerBase) => {\n const api = getPluginApi(routerBase);\n\n const plugin = new NavigationPlugin(\n routerBase as Router,\n api,\n options,\n resolvedBrowser,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n\nfunction createBrowser(base: string): NavigationBrowser {\n if (\"navigation\" in globalThis) {\n return createNavigationBrowser(base);\n }\n\n return createNavigationFallbackBrowser(\"navigation-plugin\");\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/navigation-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Navigation API integration plugin for browser URL synchronization",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"types": "./dist/esm/index.d.mts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"@real-router/internal-source": "./src/index.ts",
|
|
11
12
|
"types": {
|
|
12
13
|
"import": "./dist/esm/index.d.mts",
|
|
13
14
|
"require": "./dist/cjs/index.d.ts"
|
|
@@ -43,11 +44,12 @@
|
|
|
43
44
|
},
|
|
44
45
|
"sideEffects": false,
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@real-router/core": "^0.
|
|
47
|
+
"@real-router/core": "^0.48.0",
|
|
48
|
+
"@real-router/types": "^0.34.0"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"jsdom": "28.1.0",
|
|
50
|
-
"type-guards": "^0.4.
|
|
52
|
+
"type-guards": "^0.4.8"
|
|
51
53
|
},
|
|
52
54
|
"scripts": {
|
|
53
55
|
"test": "vitest",
|
|
@@ -57,7 +59,6 @@
|
|
|
57
59
|
"type-check": "tsc --noEmit",
|
|
58
60
|
"lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
|
|
59
61
|
"lint:package": "publint",
|
|
60
|
-
"lint:types": "attw --pack ."
|
|
61
|
-
"build:dist-only": "tsdown --config-loader unrun"
|
|
62
|
+
"lint:types": "attw --pack ."
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -94,11 +94,15 @@ export function getRouteVisitCount(
|
|
|
94
94
|
base: string,
|
|
95
95
|
routeName: string,
|
|
96
96
|
): number {
|
|
97
|
-
|
|
98
|
-
const state = entryToState(entry, api, base);
|
|
97
|
+
let count = 0;
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
for (const entry of browser.entries()) {
|
|
100
|
+
if (entryToState(entry, api, base)?.name === routeName) {
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return count;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
/**
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,15 @@ export type {
|
|
|
8
8
|
NavigationPluginOptions,
|
|
9
9
|
NavigationBrowser,
|
|
10
10
|
NavigationMeta,
|
|
11
|
+
NavigationDirection,
|
|
11
12
|
} from "./types";
|
|
12
13
|
|
|
14
|
+
declare module "@real-router/types" {
|
|
15
|
+
interface StateContext {
|
|
16
|
+
navigation?: import("./types").NavigationMeta;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
declare module "@real-router/core" {
|
|
14
21
|
interface Router {
|
|
15
22
|
buildUrl: (name: string, params?: Params) => string;
|
|
@@ -25,9 +32,6 @@ declare module "@real-router/core" {
|
|
|
25
32
|
getVisitedRoutes: () => string[];
|
|
26
33
|
getRouteVisitCount: (routeName: string) => number;
|
|
27
34
|
traverseToLast: (routeName: string) => Promise<State>;
|
|
28
|
-
getNavigationMeta: (
|
|
29
|
-
state?: State,
|
|
30
|
-
) => import("./types").NavigationMeta | undefined;
|
|
31
35
|
canGoBack: () => boolean;
|
|
32
36
|
canGoForward: () => boolean;
|
|
33
37
|
canGoBackTo: (routeName: string) => boolean;
|
package/src/navigate-handler.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { RouterError } from "@real-router/core";
|
|
|
2
2
|
|
|
3
3
|
import { extractPath } from "./browser-env/index.js";
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
NavigationBrowser,
|
|
7
|
+
NavigationDirection,
|
|
8
|
+
NavigationMeta,
|
|
9
|
+
} from "./types";
|
|
6
10
|
import type { Router } from "@real-router/core";
|
|
7
11
|
import type { PluginApi } from "@real-router/core/api";
|
|
8
12
|
|
|
@@ -12,7 +16,7 @@ interface NavigateHandlerDeps {
|
|
|
12
16
|
browser: NavigationBrowser;
|
|
13
17
|
isSyncingFromRouter: () => boolean;
|
|
14
18
|
setSyncing: (value: boolean) => void;
|
|
15
|
-
|
|
19
|
+
setCapturedMeta: (meta: NavigationMeta) => void;
|
|
16
20
|
base: string;
|
|
17
21
|
transitionOptions: {
|
|
18
22
|
source: string;
|
|
@@ -21,6 +25,18 @@ interface NavigateHandlerDeps {
|
|
|
21
25
|
};
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
export function computeDirection(
|
|
29
|
+
navigationType: NavigationMeta["navigationType"],
|
|
30
|
+
destinationIndex: number,
|
|
31
|
+
currentIndex: number,
|
|
32
|
+
): NavigationDirection {
|
|
33
|
+
if (navigationType === "traverse") {
|
|
34
|
+
return destinationIndex > currentIndex ? "forward" : "back";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return navigationType === "push" ? "forward" : "unknown";
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
export function createNavigateHandler(deps: NavigateHandlerDeps) {
|
|
25
41
|
const {
|
|
26
42
|
router,
|
|
@@ -49,11 +65,19 @@ export function createNavigateHandler(deps: NavigateHandlerDeps) {
|
|
|
49
65
|
extractPath(destinationUrl.pathname, base) + destinationUrl.search;
|
|
50
66
|
const matchedState = api.matchPath(path);
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
const navType = event.navigationType as NavigationMeta["navigationType"];
|
|
69
|
+
const currentIndex = browser.currentEntry?.index ?? -1;
|
|
70
|
+
|
|
71
|
+
deps.setCapturedMeta({
|
|
72
|
+
navigationType: navType,
|
|
55
73
|
userInitiated: event.userInitiated,
|
|
56
74
|
info: event.info,
|
|
75
|
+
direction: computeDirection(
|
|
76
|
+
navType,
|
|
77
|
+
event.destination.index,
|
|
78
|
+
currentIndex,
|
|
79
|
+
),
|
|
80
|
+
sourceElement: event.sourceElement ?? null,
|
|
57
81
|
});
|
|
58
82
|
|
|
59
83
|
if (matchedState) {
|
package/src/plugin.ts
CHANGED
|
@@ -39,7 +39,7 @@ import type {
|
|
|
39
39
|
} from "@real-router/core";
|
|
40
40
|
import type { PluginApi } from "@real-router/core/api";
|
|
41
41
|
|
|
42
|
-
function deriveNavigationType(
|
|
42
|
+
export function deriveNavigationType(
|
|
43
43
|
navOptions: NavigationOptions,
|
|
44
44
|
toState: State,
|
|
45
45
|
fromState: State | undefined,
|
|
@@ -62,11 +62,14 @@ export class NavigationPlugin {
|
|
|
62
62
|
readonly #browser: NavigationBrowser;
|
|
63
63
|
readonly #removeStartInterceptor: () => void;
|
|
64
64
|
readonly #removeExtensions: () => void;
|
|
65
|
+
readonly #claim: {
|
|
66
|
+
write: (state: State, value: NavigationMeta) => void;
|
|
67
|
+
release: () => void;
|
|
68
|
+
};
|
|
65
69
|
readonly #lifecycle: Pick<Plugin, "onStart" | "onStop" | "teardown">;
|
|
66
70
|
|
|
67
71
|
#isSyncingFromRouter = false;
|
|
68
|
-
|
|
69
|
-
#pendingMeta: NavigationMeta | undefined;
|
|
72
|
+
#capturedMeta: NavigationMeta | undefined;
|
|
70
73
|
#pendingTraverseKey: string | undefined;
|
|
71
74
|
|
|
72
75
|
constructor(
|
|
@@ -86,6 +89,7 @@ export class NavigationPlugin {
|
|
|
86
89
|
this.#options = options;
|
|
87
90
|
this.#browser = browser;
|
|
88
91
|
|
|
92
|
+
this.#claim = api.claimContextNamespace("navigation");
|
|
89
93
|
this.#removeStartInterceptor = createStartInterceptor(api, browser);
|
|
90
94
|
|
|
91
95
|
const pluginBuildUrl = (route: string, params?: Params) => {
|
|
@@ -119,13 +123,6 @@ export class NavigationPlugin {
|
|
|
119
123
|
getRouteVisitCount: (routeName: string) =>
|
|
120
124
|
getRouteVisitCount(browser, api, options.base, routeName),
|
|
121
125
|
traverseToLast: (routeName: string) => this.traverseToLast(routeName),
|
|
122
|
-
getNavigationMeta: (state?: State): NavigationMeta | undefined => {
|
|
123
|
-
if (!state) {
|
|
124
|
-
return this.#pendingMeta;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return this.#metaByState.get(state);
|
|
128
|
-
},
|
|
129
126
|
canGoBack: () => canGoBack(browser),
|
|
130
127
|
canGoForward: () => canGoForward(browser),
|
|
131
128
|
canGoBackTo: (routeName: string) =>
|
|
@@ -140,8 +137,8 @@ export class NavigationPlugin {
|
|
|
140
137
|
setSyncing: (syncing) => {
|
|
141
138
|
this.#isSyncingFromRouter = syncing;
|
|
142
139
|
},
|
|
143
|
-
|
|
144
|
-
this.#
|
|
140
|
+
setCapturedMeta: (meta) => {
|
|
141
|
+
this.#capturedMeta = meta;
|
|
145
142
|
},
|
|
146
143
|
base: options.base,
|
|
147
144
|
transitionOptions,
|
|
@@ -153,6 +150,9 @@ export class NavigationPlugin {
|
|
|
153
150
|
handler,
|
|
154
151
|
removeStartInterceptor: this.#removeStartInterceptor,
|
|
155
152
|
removeExtensions: this.#removeExtensions,
|
|
153
|
+
releaseClaim: () => {
|
|
154
|
+
this.#claim.release();
|
|
155
|
+
},
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -184,9 +184,14 @@ export class NavigationPlugin {
|
|
|
184
184
|
throw new Error(`No matching route for entry URL "${entry.url}"`);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
/* v8 ignore next -- @preserve: currentEntry always exists when traverseToLast is callable (after start) */
|
|
188
|
+
const currentIndex = this.#browser.currentEntry?.index ?? -1;
|
|
189
|
+
|
|
190
|
+
this.#capturedMeta = {
|
|
188
191
|
navigationType: "traverse",
|
|
189
192
|
userInitiated: false,
|
|
193
|
+
direction: entry.index > currentIndex ? "forward" : "back",
|
|
194
|
+
sourceElement: null,
|
|
190
195
|
};
|
|
191
196
|
this.#pendingTraverseKey = entry.key;
|
|
192
197
|
|
|
@@ -197,24 +202,34 @@ export class NavigationPlugin {
|
|
|
197
202
|
return {
|
|
198
203
|
...this.#lifecycle,
|
|
199
204
|
|
|
205
|
+
onTransitionStart: (toState: State) => {
|
|
206
|
+
if (this.#capturedMeta) {
|
|
207
|
+
this.#claim.write(toState, this.#capturedMeta);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
200
211
|
onTransitionSuccess: (
|
|
201
212
|
toState: State,
|
|
202
213
|
fromState: State | undefined,
|
|
203
214
|
navOptions: NavigationOptions,
|
|
204
215
|
) => {
|
|
205
|
-
if (!this.#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
if (!this.#capturedMeta) {
|
|
217
|
+
const navigationType = deriveNavigationType(
|
|
218
|
+
navOptions,
|
|
219
|
+
toState,
|
|
220
|
+
fromState,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
this.#capturedMeta = {
|
|
224
|
+
navigationType,
|
|
212
225
|
userInitiated: false,
|
|
226
|
+
direction: navigationType === "push" ? "forward" : "unknown",
|
|
227
|
+
sourceElement: null,
|
|
213
228
|
};
|
|
214
229
|
}
|
|
215
230
|
|
|
216
|
-
this.#
|
|
217
|
-
this.#
|
|
231
|
+
this.#claim.write(toState, Object.freeze(this.#capturedMeta));
|
|
232
|
+
this.#capturedMeta = undefined;
|
|
218
233
|
|
|
219
234
|
this.#isSyncingFromRouter = true;
|
|
220
235
|
|
|
@@ -254,12 +269,12 @@ export class NavigationPlugin {
|
|
|
254
269
|
},
|
|
255
270
|
|
|
256
271
|
onTransitionCancel: () => {
|
|
257
|
-
this.#
|
|
272
|
+
this.#capturedMeta = undefined;
|
|
258
273
|
this.#pendingTraverseKey = undefined;
|
|
259
274
|
},
|
|
260
275
|
|
|
261
276
|
onTransitionError: () => {
|
|
262
|
-
this.#
|
|
277
|
+
this.#capturedMeta = undefined;
|
|
263
278
|
this.#pendingTraverseKey = undefined;
|
|
264
279
|
},
|
|
265
280
|
};
|
|
@@ -271,6 +286,7 @@ interface NavigateLifecycleDeps {
|
|
|
271
286
|
handler: (event: NavigateEvent) => void;
|
|
272
287
|
removeStartInterceptor: () => void;
|
|
273
288
|
removeExtensions: () => void;
|
|
289
|
+
releaseClaim: () => void;
|
|
274
290
|
shared: NavigationSharedState;
|
|
275
291
|
}
|
|
276
292
|
|
|
@@ -293,6 +309,7 @@ function createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {
|
|
|
293
309
|
deps.shared.removeNavigateListener = undefined;
|
|
294
310
|
deps.removeStartInterceptor();
|
|
295
311
|
deps.removeExtensions();
|
|
312
|
+
deps.releaseClaim();
|
|
296
313
|
},
|
|
297
314
|
};
|
|
298
315
|
}
|
package/src/types.ts
CHANGED
|
@@ -45,9 +45,11 @@ export interface NavigationSharedState {
|
|
|
45
45
|
removeNavigateListener: (() => void) | undefined;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export type NavigationDirection = "forward" | "back" | "unknown";
|
|
49
|
+
|
|
48
50
|
/**
|
|
49
|
-
* Navigation metadata attached to State via
|
|
50
|
-
* Available in
|
|
51
|
+
* Navigation metadata attached to State via state.context.navigation.
|
|
52
|
+
* Available in subscribe callbacks and components after transition completes.
|
|
51
53
|
*/
|
|
52
54
|
export interface NavigationMeta {
|
|
53
55
|
/** Type of navigation: push, replace, traverse, or reload */
|
|
@@ -56,4 +58,8 @@ export interface NavigationMeta {
|
|
|
56
58
|
userInitiated: boolean;
|
|
57
59
|
/** Ephemeral info passed via navigation.navigate({ info }) — lost on page reload */
|
|
58
60
|
info?: unknown;
|
|
61
|
+
/** Direction of navigation in the history stack */
|
|
62
|
+
direction: NavigationDirection;
|
|
63
|
+
/** The DOM element that initiated the navigation (e.g., anchor tag), or null for programmatic */
|
|
64
|
+
sourceElement: Element | null;
|
|
59
65
|
}
|