@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 +255 -0
- package/dist/cjs/index.d.ts +79 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.d.mts +79 -0
- package/dist/esm/index.d.mts.map +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -0
- package/package.json +63 -0
- package/src/constants.ts +15 -0
- package/src/factory.ts +64 -0
- package/src/history-extensions.ts +171 -0
- package/src/index.ts +36 -0
- package/src/navigate-handler.ts +129 -0
- package/src/navigation-browser.ts +55 -0
- package/src/plugin-utils.ts +61 -0
- package/src/plugin.ts +298 -0
- package/src/ssr-fallback.ts +47 -0
- package/src/types.ts +59 -0
- package/src/validation.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# @real-router/navigation-plugin
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@real-router/navigation-plugin)
|
|
4
|
+
[](https://www.npmjs.com/package/@real-router/navigation-plugin)
|
|
5
|
+
[](https://bundlejs.com/?q=@real-router/navigation-plugin&treeshake=[*])
|
|
6
|
+
[](../../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
|