@real-router/navigation-plugin 0.0.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 ADDED
@@ -0,0 +1,255 @@
1
+ # @real-router/navigation-plugin
2
+
3
+ [![npm](https://img.shields.io/npm/v/@real-router/navigation-plugin.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/navigation-plugin)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@real-router/navigation-plugin.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/navigation-plugin)
5
+ [![bundle size](https://deno.bundlejs.com/?q=@real-router/navigation-plugin&treeshake=[*]&badge=detailed)](https://bundlejs.com/?q=@real-router/navigation-plugin&treeshake=[*])
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](../../LICENSE)
7
+
8
+ > Navigation API integration for [Real-Router](https://github.com/greydragon888/real-router). Drop-in replacement for browser-plugin with route-level history access.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @real-router/navigation-plugin
14
+ ```
15
+
16
+ **Peer dependency:** `@real-router/core`
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { createRouter } from "@real-router/core";
22
+ import { navigationPluginFactory } from "@real-router/navigation-plugin";
23
+
24
+ const router = createRouter([
25
+ { name: "home", path: "/" },
26
+ { name: "users", path: "/users/:id" },
27
+ ]);
28
+
29
+ router.usePlugin(navigationPluginFactory());
30
+ await router.start(); // path inferred from browser location
31
+ ```
32
+
33
+ ## Why Navigation API?
34
+
35
+ The [Navigation API](https://caniuse.com/mdn-api_navigation) (~89% browser support) gives you access to the full session history as structured data. Unlike the History API, you can inspect every entry, check what routes the user has visited, and traverse directly to a specific past entry.
36
+
37
+ ```typescript
38
+ // Not possible with browser-plugin:
39
+ router.peekBack(); // what's one step back?
40
+ router.hasVisited("checkout"); // did the user visit checkout?
41
+ router.getVisitedRoutes(); // all routes in this session
42
+ router.traverseToLast("users.list"); // jump back to the last users list
43
+ ```
44
+
45
+ ## Options
46
+
47
+ ```typescript
48
+ router.usePlugin(
49
+ navigationPluginFactory({
50
+ base: "/app", // Base path prefix for all routes
51
+ forceDeactivate: true, // Bypass canDeactivate guards on back/forward
52
+ }),
53
+ );
54
+ ```
55
+
56
+ | Option | Type | Default | Description |
57
+ | ----------------- | --------- | ------- | ---------------------------------------------------------------------- |
58
+ | `base` | `string` | `""` | Base path for all routes (e.g., `"/app"` → URLs start with `/app/...`) |
59
+ | `forceDeactivate` | `boolean` | `true` | Bypass `canDeactivate` guards on browser back/forward |
60
+
61
+ ## Router Extensions
62
+
63
+ ### Compatible extensions (same as browser-plugin)
64
+
65
+ | Method | Returns | Description |
66
+ | -------------------------------------------- | -------------------- | ------------------------------------------------ |
67
+ | `buildUrl(name, params?)` | `string` | Build full URL with base path |
68
+ | `matchUrl(url)` | `State \| undefined` | Parse URL to router state |
69
+ | `replaceHistoryState(name, params?, title?)` | `void` | Update browser URL without triggering navigation |
70
+
71
+ ```typescript
72
+ router.buildUrl("users", { id: "123" });
73
+ // => "/app/users/123" (with base "/app")
74
+
75
+ router.matchUrl("/app/users/123");
76
+ // => { name: "users", params: { id: "123" }, path: "/users/123" }
77
+
78
+ // Update URL silently (no transition, no guards)
79
+ router.replaceHistoryState("users", { id: "456" });
80
+ ```
81
+
82
+ ### Exclusive extensions (Navigation API only)
83
+
84
+ | Method | Returns | Description |
85
+ | ------------------------------- | ----------------------------- | ----------------------------------------------- |
86
+ | `peekBack()` | `State \| undefined` | State of the previous history entry |
87
+ | `peekForward()` | `State \| undefined` | State of the next history entry |
88
+ | `hasVisited(routeName)` | `boolean` | Whether any history entry matches the route |
89
+ | `getVisitedRoutes()` | `string[]` | Unique route names across all history entries |
90
+ | `getRouteVisitCount(routeName)` | `number` | How many history entries match the route |
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
+ | `canGoBack()` | `boolean` | Whether there's a previous history entry |
94
+ | `canGoForward()` | `boolean` | Whether there's a next history entry |
95
+ | `canGoBackTo(routeName)` | `boolean` | Whether any previous entry matches the route |
96
+
97
+ #### `peekBack` / `peekForward`
98
+
99
+ ```typescript
100
+ // Show a preview of where back/forward would take the user
101
+ const prev = router.peekBack();
102
+ if (prev) {
103
+ console.log(`Back goes to: ${prev.name}`);
104
+ }
105
+
106
+ const next = router.peekForward();
107
+ if (next) {
108
+ console.log(`Forward goes to: ${next.name}`);
109
+ }
110
+ ```
111
+
112
+ #### `hasVisited` / `getVisitedRoutes` / `getRouteVisitCount`
113
+
114
+ ```typescript
115
+ // Check if the user has been to a route in this session
116
+ if (router.hasVisited("checkout")) {
117
+ showResumeCheckoutBanner();
118
+ }
119
+
120
+ // Get all routes visited in this session
121
+ const visited = router.getVisitedRoutes();
122
+ // => ["home", "users.list", "users.view", "checkout"]
123
+
124
+ // How many times did the user visit the product page?
125
+ const count = router.getRouteVisitCount("products.view");
126
+ ```
127
+
128
+ #### `traverseToLast`
129
+
130
+ ```typescript
131
+ // Jump directly to the last time the user was on users.list
132
+ // (skips intermediate entries — no back/forward stepping)
133
+ await router.traverseToLast("users.list");
134
+ ```
135
+
136
+ #### `getNavigationMeta`
137
+
138
+ ```typescript
139
+ // In a guard — get metadata about the in-progress navigation
140
+ const lifecycle = getLifecycleApi(router);
141
+ lifecycle.addActivateGuard("checkout", () => () => {
142
+ const meta = router.getNavigationMeta(); // no arg = pending navigation
143
+ if (meta?.userInitiated) {
144
+ // user clicked back/forward or a link
145
+ }
146
+ return true;
147
+ });
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
+ ```
157
+
158
+ #### `canGoBack` / `canGoForward` / `canGoBackTo`
159
+
160
+ ```typescript
161
+ // Disable back button when there's nowhere to go
162
+ const backDisabled = !router.canGoBack();
163
+ const forwardDisabled = !router.canGoForward();
164
+
165
+ // Show "back to list" only if the user actually came from the list
166
+ if (router.canGoBackTo("users.list")) {
167
+ showBackToListButton();
168
+ }
169
+ ```
170
+
171
+ ### `buildUrl` vs `buildPath`
172
+
173
+ ```typescript
174
+ router.buildPath("users", { id: 1 }); // "/users/1" — core, no base
175
+ router.buildUrl("users", { id: 1 }); // "/app/users/1" — plugin, with base
176
+ ```
177
+
178
+ ### `replaceHistoryState` vs `navigate({ replace: true })`
179
+
180
+ ```typescript
181
+ router.replaceHistoryState(name, params); // URL only, no transition
182
+ router.navigate(name, params, { replace: true }); // Full transition + URL update
183
+ ```
184
+
185
+ ## Feature Detection
186
+
187
+ Use `navigationPluginFactory` when the Navigation API is available, fall back to `browserPluginFactory` otherwise:
188
+
189
+ ```typescript
190
+ import { browserPluginFactory } from "@real-router/browser-plugin";
191
+ import { navigationPluginFactory } from "@real-router/navigation-plugin";
192
+
193
+ const plugin =
194
+ "navigation" in globalThis
195
+ ? navigationPluginFactory({ base })
196
+ : browserPluginFactory({ base });
197
+
198
+ router.usePlugin(plugin);
199
+ ```
200
+
201
+ ## Form Protection
202
+
203
+ Set `forceDeactivate: false` to respect `canDeactivate` guards on back/forward:
204
+
205
+ ```typescript
206
+ router.usePlugin(navigationPluginFactory({ forceDeactivate: false }));
207
+
208
+ import { getLifecycleApi } from "@real-router/core/api";
209
+
210
+ const lifecycle = getLifecycleApi(router);
211
+ lifecycle.addDeactivateGuard(
212
+ "checkout",
213
+ (router, getDep) => (toState, fromState) => {
214
+ return !hasUnsavedChanges(); // false blocks back/forward
215
+ },
216
+ );
217
+ ```
218
+
219
+ ## SSR Support
220
+
221
+ The plugin is SSR-safe. In a non-browser environment it falls back to no-ops via `createNavigationFallbackBrowser`:
222
+
223
+ ```typescript
224
+ // Server-side — no errors, methods return safe defaults
225
+ router.usePlugin(navigationPluginFactory());
226
+ router.buildUrl("home"); // returns path without base
227
+ router.matchUrl("/path"); // returns undefined
228
+ ```
229
+
230
+ ## Documentation
231
+
232
+ Full documentation: [Wiki — navigation-plugin](https://github.com/greydragon888/real-router/wiki/navigation-plugin)
233
+
234
+ - [Configuration Options](https://github.com/greydragon888/real-router/wiki/navigation-plugin#3-configuration-options)
235
+ - [Lifecycle Hooks](https://github.com/greydragon888/real-router/wiki/navigation-plugin#4-lifecycle-hooks)
236
+ - [History Extensions](https://github.com/greydragon888/real-router/wiki/navigation-plugin#5-history-extensions)
237
+ - [Behavior & Edge Cases](https://github.com/greydragon888/real-router/wiki/navigation-plugin#8-behavior)
238
+
239
+ ## Related Packages
240
+
241
+ | Package | Description |
242
+ | ---------------------------------------------------------------------------------------- | ---------------------------------------------- |
243
+ | [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required peer dependency) |
244
+ | [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) | History API fallback (broader browser support) |
245
+ | [@real-router/hash-plugin](https://www.npmjs.com/package/@real-router/hash-plugin) | Hash-based routing (`#/path`) |
246
+ | [@real-router/react](https://www.npmjs.com/package/@real-router/react) | React integration |
247
+ | [@real-router/logger-plugin](https://www.npmjs.com/package/@real-router/logger-plugin) | Development logging |
248
+
249
+ ## Contributing
250
+
251
+ See [contributing guidelines](../../CONTRIBUTING.md) for development setup and PR process.
252
+
253
+ ## License
254
+
255
+ [MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
@@ -0,0 +1,79 @@
1
+ import { Params, PluginFactory, State } from "@real-router/core";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Navigation plugin configuration.
6
+ * Same options as browser-plugin — plugins are interchangeable.
7
+ */
8
+ interface NavigationPluginOptions {
9
+ /**
10
+ * Bypass canDeactivate guards on browser back/forward.
11
+ *
12
+ * @default true
13
+ */
14
+ forceDeactivate?: boolean;
15
+ /**
16
+ * Base path for all routes (e.g., "/app" for hosted at /app/).
17
+ *
18
+ * @default ""
19
+ */
20
+ base?: string;
21
+ }
22
+ /**
23
+ * Browser abstraction over Navigation API.
24
+ * Replaces History API's Browser interface with Navigation API equivalents.
25
+ */
26
+ interface NavigationBrowser {
27
+ getLocation: () => string;
28
+ getHash: () => string;
29
+ navigate: (url: string, options: {
30
+ state: unknown;
31
+ history: "push" | "replace";
32
+ }) => void;
33
+ replaceState: (state: unknown, url: string) => void;
34
+ updateCurrentEntry: (options: {
35
+ state: unknown;
36
+ }) => void;
37
+ traverseTo: (key: string) => void;
38
+ addNavigateListener: (fn: (evt: NavigateEvent) => void) => () => void;
39
+ entries: () => NavigationHistoryEntry[];
40
+ currentEntry: NavigationHistoryEntry | null;
41
+ }
42
+ /**
43
+ * Navigation metadata attached to State via WeakMap.
44
+ * Available in guards (via pendingMeta) and subscribe callbacks (via metaByState).
45
+ */
46
+ interface NavigationMeta {
47
+ /** Type of navigation: push, replace, traverse, or reload */
48
+ navigationType: "push" | "replace" | "traverse" | "reload";
49
+ /** Whether the navigation was initiated by the user (back/forward button, link click) */
50
+ userInitiated: boolean;
51
+ /** Ephemeral info passed via navigation.navigate({ info }) — lost on page reload */
52
+ info?: unknown;
53
+ }
54
+ //#endregion
55
+ //#region src/factory.d.ts
56
+ declare function navigationPluginFactory(opts?: Partial<NavigationPluginOptions>, browser?: NavigationBrowser): PluginFactory;
57
+ //#endregion
58
+ //#region src/index.d.ts
59
+ declare module "@real-router/core" {
60
+ interface Router {
61
+ buildUrl: (name: string, params?: Params) => string;
62
+ matchUrl: (url: string) => State | undefined;
63
+ replaceHistoryState: (name: string, params?: Params, title?: string) => void;
64
+ peekBack: () => State | undefined;
65
+ peekForward: () => State | undefined;
66
+ hasVisited: (routeName: string) => boolean;
67
+ getVisitedRoutes: () => string[];
68
+ getRouteVisitCount: (routeName: string) => number;
69
+ traverseToLast: (routeName: string) => Promise<State>;
70
+ getNavigationMeta: (state?: State) => NavigationMeta | undefined;
71
+ canGoBack: () => boolean;
72
+ canGoForward: () => boolean;
73
+ canGoBackTo: (routeName: string) => boolean;
74
+ start(path?: string): Promise<State>;
75
+ }
76
+ } //# sourceMappingURL=index.d.ts.map
77
+ //#endregion
78
+ export { type NavigationBrowser, type NavigationMeta, type NavigationPluginOptions, navigationPluginFactory };
79
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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;;;;;UAeC,cAAA;;EAEf,cAAA;;EAEA,aAAA;ECvCqC;EDyCrC,IAAA;AAAA;;;iBCzCc,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCNS,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,iBAAA,GACE,KAAA,GAAQ,KAAA,KAAK,cAAA;IAEf,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
@@ -0,0 +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){return e.entries().filter(e=>h(e,t,n)?.name===r).length}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){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);e.setPendingMeta({navigationType:d.navigationType,userInitiated:d.userInitiated,info:d.info}),m?d.intercept({handler:async()=>{try{await n.navigate(m.name,m.params,{...c,signal:d.signal})}catch(e){e instanceof t.RouterError||D(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||D(e,n,i,o)}}})}}function D(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 O(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function k(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 A(e,t,n){return e.reload&&t.path===n?.path?`reload`:s(e,t,n)?`replace`:`push`}var j=class{#e;#t;#n;#r;#i;#a;#o;#s=!1;#c=new WeakMap;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#i=O(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:k(t,e,r,o,e=>{this.#s=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),getNavigationMeta:e=>e?this.#c.get(e):this.#l,canGoBack:()=>C(r),canGoForward:()=>w(r),canGoBackTo:e=>T(r,t,n.base,e)}),this.#o=M({browser:r,shared:a,handler:E({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#s,setSyncing:e=>{this.#s=e},setPendingMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a})}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}"`);return this.#l={navigationType:`traverse`,userInitiated:!1},this.#u=r.key,this.#e.navigate(o.name,o.params)}getPlugin(){return{...this.#o,onTransitionSuccess:(e,n,r)=>{if(this.#l||={navigationType:A(r,e,n),userInitiated:!1},this.#c.set(e,this.#l),this.#l=void 0,this.#s=!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.#s=!1},onTransitionCancel:()=>{this.#l=void 0,this.#u=void 0},onTransitionError:()=>{this.#l=void 0,this.#u=void 0}}}};function M(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()}}}const N=()=>{},P=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`),N),entries:()=>(t(`entries`),[]),currentEntry:null}},F=o(f,p);function I(t,i){if(!i&&n()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);F(t);let a={...f,...t};a.base=r(a.base);let o=i??L(a.base),s={forceDeactivate:a.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new j(t,(0,e.getPluginApi)(t),a,o,s,c).getPlugin()}function L(e){return`navigation`in globalThis?m(e):P(`navigation-plugin`)}exports.navigationPluginFactory=I;
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["safelyEncodePath","extractPath","extractPath","extractPath","RouterError","shouldReplaceHistory","#router","#api","#options","#browser","#removeStartInterceptor","#removeExtensions","#lifecycle","#metaByState","buildUrl","urlToPath","#isSyncingFromRouter","#pendingMeta","extractPath","#pendingTraverseKey","UNKNOWN_ROUTE","createWarnOnce","createOptionsValidator","isBrowserEnvironment","normalizeBase"],"sources":["../../../browser-env/dist/esm/index.mjs","../../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":["import{isStateStrict as e}from\"type-guards\";import{RouterError as t}from\"@real-router/core\";const n=()=>globalThis.window!==void 0&&!!globalThis.history,r=(e,t)=>{globalThis.history.pushState(e,``,t)},i=(e,t)=>{globalThis.history.replaceState(e,``,t)},a=e=>(globalThis.addEventListener(`popstate`,e),()=>{globalThis.removeEventListener(`popstate`,e)}),o=()=>globalThis.location.hash;function s(e){if(!e)return e;let t=e;return t.startsWith(`/`)||(t=`/${t}`),t.endsWith(`/`)&&(t=t.slice(0,-1)),t}const c=e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path \"${e}\"`,t),e}},l=()=>{},u=e=>{let t=!1;return n=>{t||=(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: \"${e}\"). Method \"${n}\" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),!0)}},d=e=>{let t=u(e);return{pushState:()=>{t(`pushState`)},replaceState:()=>{t(`replaceState`)},addPopstateListener:()=>(t(`addPopstateListener`),l),getHash:()=>(t(`getHash`),``)}};function f(t,n,r){if(e(t.state))return{name:t.state.name,params:t.state.params};let i=n.matchPath(r.getLocation());return i?{name:i.name,params:i.params}:void 0}function p(e,t,n,r){let i={name:e.name,params:e.params,path:e.path};n?r.replaceState(i,t):r.pushState(i,t)}function m(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 h(e,t){if(n())return{pushState:r,replaceState:i,addPopstateListener:a,getLocation:e,getHash:o};let s=u(t);return{...d(t),getLocation:()=>(s(`getLocation`),``)}}function g(e){let n=!1,r=null;function i(){if(r){let t=r;r=null,console.warn(`[${e.loggerContext}] Processing deferred popstate event`),o(t)}}function a(t){console.error(`[${e.loggerContext}] Critical error in onPopState`,t);try{let t=e.router.getState();if(t){let n=e.buildUrl(t.name,t.params);e.browser.replaceState(t,n)}}catch(t){console.error(`[${e.loggerContext}] Failed to recover from critical error`,t)}}async function o(o){if(n){console.warn(`[${e.loggerContext}] Transition in progress, deferring popstate event`),r=o;return}n=!0;try{let t=f(o,e.api,e.browser);t?await e.router.navigate(t.name,t.params,e.transitionOptions):e.allowNotFound?e.router.navigateToNotFound(e.browser.getLocation()):await e.router.navigateToDefault({...e.transitionOptions,reload:!0,replace:!0})}catch(e){e instanceof t||a(e)}finally{n=!1,i()}}return e=>void o(e)}function _(e){return{onStart:()=>{e.shared.removePopStateListener&&e.shared.removePopStateListener(),e.shared.removePopStateListener=e.browser.addPopstateListener(e.handler)},onStop:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0)},teardown:()=>{e.shared.removePopStateListener&&(e.shared.removePopStateListener(),e.shared.removePopStateListener=void 0),e.cleanup()}}}function v(e,t){return e.addInterceptor(`start`,(e,n)=>e(n??t.getLocation()))}function y(e,t,n,r){return(i,a={})=>{let o=e.buildState(i,a);if(!o)throw Error(`[real-router] Cannot replace state: route \"${i}\" is not found`);p(e.makeState(o.name,o.params,t.buildPath(o.name,o.params),{params:o.meta}),r(i,a),!0,n)}}function b(e,t,n){return(e.replace??!n)||!!e.reload&&t.path===n.path}function x(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 S(e,t){if(t&&e.startsWith(t)){let n=e.slice(t.length);return n.startsWith(`/`)?n:`/${n}`}return e}function C(e,t){return t+e}function w(e,t,n){let r=x(e,n);return r?S(r.pathname,t)+r.search:null}export{a as addPopstateListener,C as buildUrl,d as createHistoryFallbackBrowser,m as createOptionsValidator,g as createPopstateHandler,_ as createPopstateLifecycle,y as createReplaceHistoryState,h as createSafeBrowser,v as createStartInterceptor,u as createWarnOnce,S as extractPath,o as getHash,f as getRouteFromEvent,n as isBrowserEnvironment,s as normalizeBase,r as pushState,i as replaceState,x as safeParseUrl,c as safelyEncodePath,b as shouldReplaceHistory,p as updateBrowserState,w as urlToPath};\n//# sourceMappingURL=index.mjs.map","import type { NavigationPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<NavigationPluginOptions> = {\n forceDeactivate: true,\n base: \"\",\n};\n\n/**\n * Source identifier for transitions triggered by navigate events.\n * Distinguishes browser-initiated navigation (back/forward, link clicks)\n * from programmatic navigation (router.navigate()).\n */\nexport const source = \"navigate\";\n\nexport const LOGGER_CONTEXT = \"navigation-plugin\";\n","import { safelyEncodePath, extractPath } from \"browser-env\";\n\nimport type { NavigationBrowser } from \"./types\";\n\n/**\n * Creates a NavigationBrowser wrapping the real Navigation API.\n * Only call this when `\"navigation\" in globalThis` is true.\n */\nexport function createNavigationBrowser(base: string): NavigationBrowser {\n const nav = globalThis.navigation;\n\n return {\n getLocation: () =>\n safelyEncodePath(extractPath(globalThis.location.pathname, base)) +\n globalThis.location.search,\n\n getHash: () => globalThis.location.hash,\n\n navigate: (url, options) => {\n nav.navigate(url, {\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\";\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\";\nimport { extractPath } from \"browser-env\";\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\";\nimport {\n shouldReplaceHistory,\n buildUrl,\n extractPath,\n urlToPath,\n} from \"browser-env\";\n\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\";\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\";\n\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\";\nimport { isBrowserEnvironment, normalizeBase } from \"browser-env\";\n\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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"}
@@ -0,0 +1,79 @@
1
+ import { Params, PluginFactory, State } from "@real-router/core";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Navigation plugin configuration.
6
+ * Same options as browser-plugin — plugins are interchangeable.
7
+ */
8
+ interface NavigationPluginOptions {
9
+ /**
10
+ * Bypass canDeactivate guards on browser back/forward.
11
+ *
12
+ * @default true
13
+ */
14
+ forceDeactivate?: boolean;
15
+ /**
16
+ * Base path for all routes (e.g., "/app" for hosted at /app/).
17
+ *
18
+ * @default ""
19
+ */
20
+ base?: string;
21
+ }
22
+ /**
23
+ * Browser abstraction over Navigation API.
24
+ * Replaces History API's Browser interface with Navigation API equivalents.
25
+ */
26
+ interface NavigationBrowser {
27
+ getLocation: () => string;
28
+ getHash: () => string;
29
+ navigate: (url: string, options: {
30
+ state: unknown;
31
+ history: "push" | "replace";
32
+ }) => void;
33
+ replaceState: (state: unknown, url: string) => void;
34
+ updateCurrentEntry: (options: {
35
+ state: unknown;
36
+ }) => void;
37
+ traverseTo: (key: string) => void;
38
+ addNavigateListener: (fn: (evt: NavigateEvent) => void) => () => void;
39
+ entries: () => NavigationHistoryEntry[];
40
+ currentEntry: NavigationHistoryEntry | null;
41
+ }
42
+ /**
43
+ * Navigation metadata attached to State via WeakMap.
44
+ * Available in guards (via pendingMeta) and subscribe callbacks (via metaByState).
45
+ */
46
+ interface NavigationMeta {
47
+ /** Type of navigation: push, replace, traverse, or reload */
48
+ navigationType: "push" | "replace" | "traverse" | "reload";
49
+ /** Whether the navigation was initiated by the user (back/forward button, link click) */
50
+ userInitiated: boolean;
51
+ /** Ephemeral info passed via navigation.navigate({ info }) — lost on page reload */
52
+ info?: unknown;
53
+ }
54
+ //#endregion
55
+ //#region src/factory.d.ts
56
+ declare function navigationPluginFactory(opts?: Partial<NavigationPluginOptions>, browser?: NavigationBrowser): PluginFactory;
57
+ //#endregion
58
+ //#region src/index.d.ts
59
+ declare module "@real-router/core" {
60
+ interface Router {
61
+ buildUrl: (name: string, params?: Params) => string;
62
+ matchUrl: (url: string) => State | undefined;
63
+ replaceHistoryState: (name: string, params?: Params, title?: string) => void;
64
+ peekBack: () => State | undefined;
65
+ peekForward: () => State | undefined;
66
+ hasVisited: (routeName: string) => boolean;
67
+ getVisitedRoutes: () => string[];
68
+ getRouteVisitCount: (routeName: string) => number;
69
+ traverseToLast: (routeName: string) => Promise<State>;
70
+ getNavigationMeta: (state?: State) => NavigationMeta | undefined;
71
+ canGoBack: () => boolean;
72
+ canGoForward: () => boolean;
73
+ canGoBackTo: (routeName: string) => boolean;
74
+ start(path?: string): Promise<State>;
75
+ }
76
+ } //# sourceMappingURL=index.d.ts.map
77
+ //#endregion
78
+ export { type NavigationBrowser, type NavigationMeta, type NavigationPluginOptions, navigationPluginFactory };
79
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +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;;;;;UAeC,cAAA;;EAEf,cAAA;;EAEA,aAAA;ECvCqC;EDyCrC,IAAA;AAAA;;;iBCzCc,uBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,uBAAA,GACf,OAAA,GAAU,iBAAA,GACT,aAAA;;;;YCNS,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,iBAAA,GACE,KAAA,GAAQ,KAAA,KAAK,cAAA;IAEf,SAAA;IACA,YAAA;IACA,WAAA,GAAc,SAAA;IACd,KAAA,CAAM,IAAA,YAAgB,OAAA,CAAQ,KAAA;EAAA;AAAA"}
@@ -0,0 +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){return e.entries().filter(e=>g(e,t,n)?.name===r).length}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){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);e.setPendingMeta({navigationType:d.navigationType,userInitiated:d.userInitiated,info:d.info}),m?d.intercept({handler:async()=>{try{await n.navigate(m.name,m.params,{...c,signal:d.signal})}catch(e){e instanceof t||O(e,n,i,o)}}}):l?d.intercept({handler:()=>{n.navigateToNotFound(p)}}):d.intercept({handler:async()=>{try{await n.navigateToDefault()}catch(e){e instanceof t||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`:c(e,t,n)?`replace`:`push`}var M=class{#e;#t;#n;#r;#i;#a;#o;#s=!1;#c=new WeakMap;#l;#u;constructor(e,t,n,r,i,a){this.#e=e,this.#t=t,this.#n=n,this.#r=r,this.#i=k(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:A(t,e,r,o,e=>{this.#s=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),getNavigationMeta:e=>e?this.#c.get(e):this.#l,canGoBack:()=>w(r),canGoForward:()=>T(r),canGoBackTo:e=>E(r,t,n.base,e)}),this.#o=N({browser:r,shared:a,handler:D({router:e,api:t,browser:r,isSyncingFromRouter:()=>this.#s,setSyncing:e=>{this.#s=e},setPendingMeta:e=>{this.#l=e},base:n.base,transitionOptions:i}),removeStartInterceptor:this.#i,removeExtensions:this.#a})}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}"`);return this.#l={navigationType:`traverse`,userInitiated:!1},this.#u=r.key,this.#e.navigate(o.name,o.params)}getPlugin(){return{...this.#o,onTransitionSuccess:(e,t,r)=>{if(this.#l||={navigationType:j(r,e,t),userInitiated:!1},this.#c.set(e,this.#l),this.#l=void 0,this.#s=!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.#s=!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()}}}const P=()=>{},F=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`),P),entries:()=>(t(`entries`),[]),currentEntry:null}},I=s(p,m);function L(t,n){if(!n&&r()&&!(`navigation`in globalThis))throw Error(`[navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.`);I(t);let a={...p,...t};a.base=i(a.base);let o=n??R(a.base),s={forceDeactivate:a.forceDeactivate,source:`navigate`,replace:!0},c={removeNavigateListener:void 0};return t=>new M(t,e(t),a,o,s,c).getPlugin()}function R(e){return`navigation`in globalThis?h(e):F(`navigation-plugin`)}export{L as navigationPluginFactory};
2
+ //# sourceMappingURL=index.mjs.map