@real-router/hash-plugin 0.2.1 → 0.2.2
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 +54 -138
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +1 -1
- package/dist/esm/index.d.mts +3 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# @real-router/hash-plugin
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@real-router/hash-plugin)
|
|
4
|
+
[](https://www.npmjs.com/package/@real-router/hash-plugin)
|
|
5
|
+
[](https://bundlejs.com/?q=@real-router/hash-plugin&treeshake=[*])
|
|
6
|
+
[](../../LICENSE)
|
|
5
7
|
|
|
6
|
-
Hash-based routing plugin for Real-Router. Uses URL hash fragment for navigation — no server configuration needed.
|
|
8
|
+
> Hash-based routing plugin for [Real-Router](https://github.com/greydragon888/real-router). Uses URL hash fragment (`#/path`) for navigation — no server configuration needed.
|
|
9
|
+
|
|
10
|
+
Works on static hosting (GitHub Pages, S3, Netlify) without redirect rules. Tradeoff: URLs include `#` (`example.com/#!/users` vs `example.com/users`).
|
|
11
|
+
|
|
12
|
+
> **Looking for clean URLs?** Use [`@real-router/browser-plugin`](https://www.npmjs.com/package/@real-router/browser-plugin) (History API).
|
|
7
13
|
|
|
8
14
|
## Installation
|
|
9
15
|
|
|
10
16
|
```bash
|
|
11
17
|
npm install @real-router/hash-plugin
|
|
12
|
-
# or
|
|
13
|
-
pnpm add @real-router/hash-plugin
|
|
14
|
-
# or
|
|
15
|
-
yarn add @real-router/hash-plugin
|
|
16
|
-
# or
|
|
17
|
-
bun add @real-router/hash-plugin
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
**Peer dependency:** `@real-router/core`
|
|
21
|
+
|
|
20
22
|
## Quick Start
|
|
21
23
|
|
|
22
24
|
```typescript
|
|
@@ -25,186 +27,100 @@ import { hashPluginFactory } from "@real-router/hash-plugin";
|
|
|
25
27
|
|
|
26
28
|
const router = createRouter([
|
|
27
29
|
{ name: "home", path: "/" },
|
|
28
|
-
{ name: "
|
|
29
|
-
{ name: "cart", path: "/cart" },
|
|
30
|
+
{ name: "users", path: "/users/:id" },
|
|
30
31
|
]);
|
|
31
32
|
|
|
32
|
-
// Basic usage
|
|
33
33
|
router.usePlugin(hashPluginFactory());
|
|
34
|
-
|
|
35
|
-
// With options
|
|
36
|
-
router.usePlugin(
|
|
37
|
-
hashPluginFactory({
|
|
38
|
-
hashPrefix: "!",
|
|
39
|
-
}),
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
await router.start();
|
|
34
|
+
await router.start(); // reads hash from browser location
|
|
43
35
|
```
|
|
44
36
|
|
|
45
|
-
|
|
37
|
+
## Options
|
|
46
38
|
|
|
47
|
-
|
|
39
|
+
| Option | Type | Default | Description |
|
|
40
|
+
|--------|------|---------|-------------|
|
|
41
|
+
| `hashPrefix` | `string` | `""` | Prefix after `#` (e.g., `"!"` → `#!/path`) |
|
|
42
|
+
| `base` | `string` | `""` | Base path before hash (e.g., `"/app"` → `/app#/path`) |
|
|
43
|
+
| `forceDeactivate` | `boolean` | `true` | Bypass `canDeactivate` guards on back/forward |
|
|
48
44
|
|
|
49
45
|
```typescript
|
|
50
|
-
router.usePlugin(
|
|
51
|
-
hashPluginFactory({
|
|
52
|
-
hashPrefix: "!",
|
|
53
|
-
forceDeactivate: true,
|
|
54
|
-
}),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
router.navigate("products", { id: "123" });
|
|
58
|
-
// URL: http://example.com/#!/products/123
|
|
59
|
-
```
|
|
46
|
+
router.usePlugin(hashPluginFactory({ hashPrefix: "!", base: "/app" }));
|
|
60
47
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| `base` | `string` | `""` | Base path before hash (e.g., `"/app"` → `/app#/path`) |
|
|
65
|
-
| `forceDeactivate` | `boolean` | `true` | Bypass `canDeactivate` guards on browser back/forward |
|
|
66
|
-
|
|
67
|
-
> **Looking for History API routing?** Use [`@real-router/browser-plugin`](https://www.npmjs.com/package/@real-router/browser-plugin) instead.
|
|
68
|
-
|
|
69
|
-
See [Wiki](https://github.com/greydragon888/real-router/wiki/hash-plugin) for detailed option descriptions and examples.
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## Added Router Methods
|
|
48
|
+
router.navigate("users", { id: "123" });
|
|
49
|
+
// URL: /app#!/users/123
|
|
50
|
+
```
|
|
74
51
|
|
|
75
|
-
|
|
52
|
+
## Router Extensions
|
|
76
53
|
|
|
77
|
-
|
|
54
|
+
The plugin extends the router instance with three methods via [`extendRouter()`](https://github.com/greydragon888/real-router/wiki/plugin-architecture):
|
|
78
55
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`params
|
|
82
|
-
|
|
83
|
-
|
|
56
|
+
| Method | Returns | Description |
|
|
57
|
+
|--------|---------|-------------|
|
|
58
|
+
| `buildUrl(name, params?)` | `string` | Build full URL with hash and prefix |
|
|
59
|
+
| `matchUrl(url)` | `State \| undefined` | Parse hash URL to router state |
|
|
60
|
+
| `replaceHistoryState(name, params?, title?)` | `void` | Update browser URL without navigation |
|
|
84
61
|
|
|
85
62
|
```typescript
|
|
86
63
|
router.buildUrl("users", { id: "123" });
|
|
87
64
|
// => "#!/users/123" (with hashPrefix "!")
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
#### `router.matchUrl(url: string): State | undefined`
|
|
91
|
-
|
|
92
|
-
Parse URL to router state.\
|
|
93
|
-
`url: string` — URL to parse\
|
|
94
|
-
Returns: `State | undefined`\
|
|
95
|
-
[Wiki](https://github.com/greydragon888/real-router/wiki/hash-plugin#5-router-interaction)
|
|
96
65
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// => { name: "users", params: { id: "123" }, ... }
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
#### `router.replaceHistoryState(name: string, params?: Params, title?: string): void`
|
|
66
|
+
router.matchUrl("https://example.com/#!/users/123");
|
|
67
|
+
// => { name: "users", params: { id: "123" }, path: "/users/123" }
|
|
103
68
|
|
|
104
|
-
Update
|
|
105
|
-
`name: string` — route name\
|
|
106
|
-
`params?: Params` — route parameters\
|
|
107
|
-
`title?: string` — page title\
|
|
108
|
-
Returns: `void`\
|
|
109
|
-
[Wiki](https://github.com/greydragon888/real-router/wiki/hash-plugin#5-router-interaction)
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
69
|
+
// Update URL silently (no transition, no guards)
|
|
112
70
|
router.replaceHistoryState("users", { id: "456" });
|
|
113
71
|
```
|
|
114
72
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
## Usage Examples
|
|
118
|
-
|
|
119
|
-
### Hashbang Routing
|
|
73
|
+
### `buildUrl` vs `buildPath`
|
|
120
74
|
|
|
121
75
|
```typescript
|
|
122
|
-
router.
|
|
123
|
-
|
|
124
|
-
hashPrefix: "!",
|
|
125
|
-
}),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
router.navigate("users", { id: "123" });
|
|
129
|
-
// URL: #!/users/123
|
|
76
|
+
router.buildPath("users", { id: 1 }); // "/users/1" — core, no hash
|
|
77
|
+
router.buildUrl("users", { id: 1 }); // "#!/users/1" — plugin, with hash prefix
|
|
130
78
|
```
|
|
131
79
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
router.usePlugin(
|
|
136
|
-
hashPluginFactory({
|
|
137
|
-
hashPrefix: "!",
|
|
138
|
-
base: "/app",
|
|
139
|
-
}),
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
router.navigate("users", { id: "123" });
|
|
143
|
-
// URL: /app#!/users/123
|
|
144
|
-
```
|
|
80
|
+
## Form Protection
|
|
145
81
|
|
|
146
|
-
|
|
82
|
+
Set `forceDeactivate: false` to respect `canDeactivate` guards on back/forward:
|
|
147
83
|
|
|
148
84
|
```typescript
|
|
149
|
-
router.usePlugin(
|
|
150
|
-
hashPluginFactory({
|
|
151
|
-
forceDeactivate: false,
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
85
|
+
router.usePlugin(hashPluginFactory({ forceDeactivate: false }));
|
|
154
86
|
|
|
155
87
|
import { getLifecycleApi } from "@real-router/core/api";
|
|
156
88
|
|
|
157
89
|
const lifecycle = getLifecycleApi(router);
|
|
158
90
|
lifecycle.addDeactivateGuard("checkout", () => (toState, fromState) => {
|
|
159
|
-
return !hasUnsavedChanges(); // false blocks
|
|
91
|
+
return !hasUnsavedChanges(); // false blocks back/forward
|
|
160
92
|
});
|
|
161
93
|
```
|
|
162
94
|
|
|
163
|
-
---
|
|
164
|
-
|
|
165
95
|
## SSR Support
|
|
166
96
|
|
|
167
|
-
|
|
97
|
+
SSR-safe — automatically detects the environment and falls back to no-ops:
|
|
168
98
|
|
|
169
99
|
```typescript
|
|
170
|
-
// Server-side — no errors, methods return safe defaults
|
|
171
100
|
router.usePlugin(hashPluginFactory());
|
|
172
|
-
router.buildUrl("home");
|
|
173
|
-
router.matchUrl("/path");
|
|
101
|
+
router.buildUrl("home"); // returns hash path
|
|
102
|
+
router.matchUrl("/path"); // returns undefined
|
|
174
103
|
```
|
|
175
104
|
|
|
176
|
-
---
|
|
177
|
-
|
|
178
|
-
## Why Hash Routing?
|
|
179
|
-
|
|
180
|
-
Hash-based routing stores the entire route in the URL hash fragment (`#/path`). This means:
|
|
181
|
-
|
|
182
|
-
- **No server configuration** — the server always serves the same `index.html` regardless of the URL
|
|
183
|
-
- **Works on static hosting** — GitHub Pages, S3, Netlify (without redirect rules)
|
|
184
|
-
- **Legacy browser support** — works everywhere that supports `hashchange` events
|
|
185
|
-
|
|
186
|
-
The tradeoff is less clean URLs (`example.com/#!/users` vs `example.com/users`).
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
105
|
## Documentation
|
|
191
106
|
|
|
192
|
-
Full documentation
|
|
107
|
+
Full documentation: [Wiki — hash-plugin](https://github.com/greydragon888/real-router/wiki/hash-plugin)
|
|
193
108
|
|
|
194
109
|
- [Configuration Options](https://github.com/greydragon888/real-router/wiki/hash-plugin#3-configuration-options)
|
|
195
|
-
- [Lifecycle Hooks](https://github.com/greydragon888/real-router/wiki/hash-plugin#4-lifecycle-hooks)
|
|
196
|
-
- [Router Methods](https://github.com/greydragon888/real-router/wiki/hash-plugin#5-router-interaction)
|
|
197
110
|
- [Behavior & Edge Cases](https://github.com/greydragon888/real-router/wiki/hash-plugin#8-behavior)
|
|
198
111
|
|
|
199
|
-
---
|
|
200
|
-
|
|
201
112
|
## Related Packages
|
|
202
113
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
114
|
+
| Package | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required peer dependency) |
|
|
117
|
+
| [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) | History API routing (clean URLs) |
|
|
118
|
+
| [@real-router/react](https://www.npmjs.com/package/@real-router/react) | React integration |
|
|
119
|
+
|
|
120
|
+
## Contributing
|
|
121
|
+
|
|
122
|
+
See [contributing guidelines](../../CONTRIBUTING.md) for development setup and PR process.
|
|
207
123
|
|
|
208
124
|
## License
|
|
209
125
|
|
|
210
|
-
MIT © [Oleg Ivanov](https://github.com/greydragon888)
|
|
126
|
+
[MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -40,11 +40,11 @@ declare function hashPluginFactory(opts?: Partial<HashPluginOptions>, browser?:
|
|
|
40
40
|
type TransitionPhase = "deactivating" | "activating";
|
|
41
41
|
type TransitionReason = "success" | "blocked" | "cancelled" | "error";
|
|
42
42
|
interface TransitionMeta {
|
|
43
|
-
readonly reload?: boolean;
|
|
44
|
-
readonly redirected?: boolean;
|
|
45
43
|
phase: TransitionPhase;
|
|
46
|
-
from?: string;
|
|
47
44
|
reason: TransitionReason;
|
|
45
|
+
reload?: boolean;
|
|
46
|
+
redirected?: boolean;
|
|
47
|
+
from?: string;
|
|
48
48
|
blocker?: string;
|
|
49
49
|
segments: {
|
|
50
50
|
deactivated: string[];
|
package/dist/cjs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=require("@real-router/core/api"),t=require("@real-router/core"),r=/^[A-Z_a-z][\w-]*(?:\.[A-Z_a-z][\w-]*)*$/;function n(e,t=new WeakSet){if(null==e)return!0;const r=typeof e;if("string"===r||"boolean"===r)return!0;if("number"===r)return Number.isFinite(e);if("function"===r||"symbol"===r)return!1;if(Array.isArray(e))return!t.has(e)&&(t.add(e),e.every(e=>n(e,t)));if("object"===r){if(t.has(e))return!1;t.add(e);const r=Object.getPrototypeOf(e);return(null===r||r===Object.prototype)&&Object.values(e).every(e=>n(e,t))}return!1}function o(e){if(null==e)return!0;const t=typeof e;return"string"===t||"boolean"===t||"number"===t&&Number.isFinite(e)}function a(e){if("object"!=typeof e||null===e||Array.isArray(e))return!1;const t=Object.getPrototypeOf(e);if(null!==t&&t!==Object.prototype)return!1;let r=!1;for(const t in e){if(!Object.hasOwn(e,t))continue;const n=e[t];if(!o(n)){const e=typeof n;if("function"===e||"symbol"===e)return!1;r=!0;break}}return!r||n(e)}function i(e){if(null==e)return!0;const t=typeof e;return"string"===t||"boolean"===t||("number"===t?Number.isFinite(e):!!Array.isArray(e)&&e.every(e=>{const t=typeof e;return"string"===t||"boolean"===t||"number"===t&&Number.isFinite(e)}))}function s(e){if("object"!=typeof e||null===e)return!1;const t=e;return!!function(e){return function(e){return"string"==typeof e&&(""===e||!(e.length>1e4)&&(!!e.startsWith("@@")||r.test(e)))}(e.name)&&"string"==typeof e.path&&a(e.params)}(t)&&(void 0===t.meta||function(e){if("object"!=typeof e||null===e)return!1;const t=e;return!("params"in t&&!function(e){if("object"!=typeof e||null===e||Array.isArray(e))return!1;for(const t in e)if(Object.hasOwn(e,t)&&!i(e[t]))return!1;return!0}(t.params)||"id"in t&&"number"!=typeof t.id)}(t.meta))}var c=(e,t)=>{globalThis.history.pushState(e,"",t)},u=(e,t)=>{globalThis.history.replaceState(e,"",t)},l=e=>(globalThis.addEventListener("popstate",e),()=>{globalThis.removeEventListener("popstate",e)}),p=()=>globalThis.location.hash,h=()=>{},f=e=>{let t=!1;return r=>{t||(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${r}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),t=!0)}},d=e=>{const t=f(e);return{pushState:()=>{t("pushState")},replaceState:()=>{t("replaceState")},addPopstateListener:()=>(t("addPopstateListener"),h),getHash:()=>(t("getHash"),"")}};function m(e,t,r,n){const o={meta:e.meta,name:e.name,params:e.params,path:e.path};r?n.replaceState(o,t):n.pushState(o,t)}function b(e){let r=!1,n=null;async function o(a){if(r)return console.warn(`[${e.loggerContext}] Transition in progress, deferring popstate event`),void(n=a);r=!0;try{const t=function(e,t,r){if(s(e.state))return{name:e.state.name,params:e.state.params};const n=t.matchPath(r.getLocation());return n?{name:n.name,params:n.params}:void 0}(a,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(r){r instanceof t.RouterError||function(t){console.error(`[${e.loggerContext}] Critical error in onPopState`,t);try{const t=e.router.getState();if(t){const r=e.buildUrl(t.name,t.params);e.browser.replaceState(t,r)}}catch(t){console.error(`[${e.loggerContext}] Failed to recover from critical error`,t)}}(r)}finally{r=!1,function(){if(n){const t=n;n=null,console.warn(`[${e.loggerContext}] Processing deferred popstate event`),o(t)}}()}}return e=>{o(e)}}function g(e,t,r,n){return(o,a={})=>{const i=e.buildState(o,a);if(!i)throw new Error(`[real-router] Cannot replace state: route "${o}" is not found`);m(e.makeState(i.name,i.params,t.buildPath(i.name,i.params),{params:i.meta},1),n(o,a),!0,r)}}var v={hashPrefix:"",base:"",forceDeactivate:!0},y="hash-plugin";function w(e,t){return(t?e.replace(t,""):e.slice(1))||"/"}var S,P,L=class{#e;#t;#r;#n;#o;constructor(e,t,r,n,o,a,i){var s;this.#e=e,this.#t=n,this.#r=(s=n,t.addInterceptor("start",(e,t)=>e(t??s.getLocation())));const c=`${r.base}#${r.hashPrefix}`,u=(t,r)=>c+e.buildPath(t,r);this.#n=t.extendRouter({buildUrl:u,matchUrl:e=>{const r=function(e,t){const r=function(e,t){try{const r=new URL(e,globalThis.location.origin);return["http:","https:"].includes(r.protocol)?r:(console.warn(`[${t}] Invalid URL protocol in ${e}`),null)}catch(r){return console.warn(`[${t}] Could not parse url ${e}`,r),null}}(e,y);return r?w(r.hash,t)+r.search:null}(e,o);return r?t.matchPath(r):void 0},replaceHistoryState:g(t,e,n,u)});const l=b({router:e,api:t,browser:n,allowNotFound:t.getOptions().allowNotFound,transitionOptions:a,loggerContext:"hash-plugin",buildUrl:(t,r)=>e.buildUrl(t,r)});this.#o=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()}}}({browser:n,shared:i,handler:l,cleanup:()=>{this.#r(),this.#n()}})}getPlugin(){return{...this.#o,onTransitionSuccess:(e,t,r)=>{const n=(a=t,i=this.#e,((o=r).replace??!a)||!!o.reload&&i.areStatesEqual(e,a,!1));var o,a,i;m(e,this.#e.buildUrl(e.name,e.params),n,this.#t)}}}},$=(S=v,P=y,e=>{if(e)for(const t of Object.keys(e))if(t in S){const r=e[t],n=typeof S[t],o=typeof r;if(void 0!==r&&o!==n)throw new Error(`[${P}] Invalid type for '${t}': expected ${n}, got ${o}`)}});exports.hashPluginFactory=function(t,r){$(t);const n={...v,...t};n.base=function(e){if(!e)return e;let t=e;return t.startsWith("/")||(t=`/${t}`),t.endsWith("/")&&(t=t.slice(0,-1)),t}(n.base);const o=(h=n.hashPrefix)?new RegExp(`^#${m=h,m.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`)}`):null,a=r??function(e,t){if(void 0!==globalThis.window&&globalThis.history)return{pushState:c,replaceState:u,addPopstateListener:l,getLocation:e,getHash:p};const r=f(t);return{...d(t),getLocation:()=>(r("getLocation"),"")}}(()=>(e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}})(w(globalThis.location.hash,o))+globalThis.location.search,"hash-plugin"),i={forceDeactivate:n.forceDeactivate,source:"popstate",replace:!0},s={removePopStateListener:void 0};var h,m;return function(t){return new L(t,e.getPluginApi(t),n,a,o,i,s).getPlugin()}},exports.isState=s;//# sourceMappingURL=index.js.map
|
|
1
|
+
"use strict";var e=require("@real-router/core/api"),t=require("@real-router/core"),r=/^[A-Z_a-z][\w-]*(?:\.[A-Z_a-z][\w-]*)*$/;function n(e,t=new WeakSet){if(null==e)return!0;const r=typeof e;if("string"===r||"boolean"===r)return!0;if("number"===r)return Number.isFinite(e);if("function"===r||"symbol"===r)return!1;if(Array.isArray(e))return!t.has(e)&&(t.add(e),e.every(e=>n(e,t)));if("object"===r){if(t.has(e))return!1;t.add(e);const r=Object.getPrototypeOf(e);return(null===r||r===Object.prototype)&&Object.values(e).every(e=>n(e,t))}return!1}function o(e){if(null==e)return!0;const t=typeof e;return"string"===t||"boolean"===t||"number"===t&&Number.isFinite(e)}function a(e){if("object"!=typeof e||null===e||Array.isArray(e))return!1;const t=Object.getPrototypeOf(e);if(null!==t&&t!==Object.prototype)return!1;let r=!1;for(const t in e){if(!Object.hasOwn(e,t))continue;const n=e[t];if(!o(n)){const e=typeof n;if("function"===e||"symbol"===e)return!1;r=!0;break}}return!r||n(e)}function i(e){if(null==e)return!0;const t=typeof e;return"string"===t||"boolean"===t||("number"===t?Number.isFinite(e):!!Array.isArray(e)&&e.every(e=>{const t=typeof e;return"string"===t||"boolean"===t||"number"===t&&Number.isFinite(e)}))}function s(e){if("object"!=typeof e||null===e)return!1;const t=e;return!!function(e){return function(e){return"string"==typeof e&&(""===e||!(e.length>1e4)&&(!!e.startsWith("@@")||r.test(e)))}(e.name)&&"string"==typeof e.path&&a(e.params)}(t)&&(void 0===t.meta||function(e){if("object"!=typeof e||null===e)return!1;const t=e;return!("params"in t&&!function(e){if("object"!=typeof e||null===e||Array.isArray(e))return!1;for(const t in e)if(Object.hasOwn(e,t)&&!i(e[t]))return!1;return!0}(t.params)||"id"in t&&"number"!=typeof t.id)}(t.meta))}var c=(e,t)=>{globalThis.history.pushState(e,"",t)},u=(e,t)=>{globalThis.history.replaceState(e,"",t)},l=e=>(globalThis.addEventListener("popstate",e),()=>{globalThis.removeEventListener("popstate",e)}),p=()=>globalThis.location.hash,h=()=>{},f=e=>{let t=!1;return r=>{t||(console.warn(`[browser-env] Browser API is running in a non-browser environment (context: "${e}"). Method "${r}" is a no-op. This is expected for SSR, but may indicate misconfiguration if you expected browser behavior.`),t=!0)}},d=e=>{const t=f(e);return{pushState:()=>{t("pushState")},replaceState:()=>{t("replaceState")},addPopstateListener:()=>(t("addPopstateListener"),h),getHash:()=>(t("getHash"),"")}};function m(e,t,r,n){const o={meta:e.meta,name:e.name,params:e.params,path:e.path};r?n.replaceState(o,t):n.pushState(o,t)}function b(e){let r=!1,n=null;async function o(a){if(r)return console.warn(`[${e.loggerContext}] Transition in progress, deferring popstate event`),void(n=a);r=!0;try{const t=function(e,t,r){if(s(e.state))return{name:e.state.name,params:e.state.params};const n=t.matchPath(r.getLocation());return n?{name:n.name,params:n.params}:void 0}(a,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(r){r instanceof t.RouterError||function(t){console.error(`[${e.loggerContext}] Critical error in onPopState`,t);try{const t=e.router.getState();if(t){const r=e.buildUrl(t.name,t.params);e.browser.replaceState(t,r)}}catch(t){console.error(`[${e.loggerContext}] Failed to recover from critical error`,t)}}(r)}finally{r=!1,function(){if(n){const t=n;n=null,console.warn(`[${e.loggerContext}] Processing deferred popstate event`),o(t)}}()}}return e=>{o(e)}}function g(e,t,r,n){return(o,a={})=>{const i=e.buildState(o,a);if(!i)throw new Error(`[real-router] Cannot replace state: route "${o}" is not found`);m(e.makeState(i.name,i.params,t.buildPath(i.name,i.params),{params:i.meta},1),n(o,a),!0,r)}}var v={hashPrefix:"",base:"",forceDeactivate:!0},y="hash-plugin";function w(e,t){return(t?e.replace(t,""):e.slice(1))||"/"}var S,P,L=class{#e;#t;#r;#n;#o;constructor(e,t,r,n,o,a,i){var s;this.#e=e,this.#t=n,this.#r=(s=n,t.addInterceptor("start",(e,t)=>e(t??s.getLocation())));const c=`${r.base}#${r.hashPrefix}`,u=(t,r)=>c+e.buildPath(t,r);this.#n=t.extendRouter({buildUrl:u,matchUrl:e=>{const r=function(e,t){const r=function(e,t){try{const r=new URL(e,globalThis.location.origin);return["http:","https:"].includes(r.protocol)?r:(console.warn(`[${t}] Invalid URL protocol in ${e}`),null)}catch(r){return console.warn(`[${t}] Could not parse url ${e}`,r),null}}(e,y);return r?w(r.hash,t)+r.search:null}(e,o);return r?t.matchPath(r):void 0},replaceHistoryState:g(t,e,n,u)});const l=b({router:e,api:t,browser:n,allowNotFound:t.getOptions().allowNotFound,transitionOptions:a,loggerContext:"hash-plugin",buildUrl:(t,r)=>e.buildUrl(t,r)});this.#o=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()}}}({browser:n,shared:i,handler:l,cleanup:()=>{this.#r(),this.#n()}})}getPlugin(){return{...this.#o,onTransitionSuccess:(e,t,r)=>{const n=(a=t,i=this.#e,((o=r).replace??!a)||!!o.reload&&i.areStatesEqual(e,a,!1));var o,a,i;m(e,this.#e.buildUrl(e.name,e.params),n,this.#t)}}}},$=(S=v,P=y,e=>{if(e)for(const t of Object.keys(e))if(t in S){const r=e[t],n=typeof S[t],o=typeof r;if(void 0!==r&&o!==n)throw new Error(`[${P}] Invalid type for '${t}': expected ${n}, got ${o}`)}});exports.hashPluginFactory=function(t,r){$(t);const n={...v,...t};n.base=function(e){if(!e)return e;let t=e;return t.startsWith("/")||(t=`/${t}`),t.endsWith("/")&&(t=t.slice(0,-1)),t}(n.base);const o=(h=n.hashPrefix)?new RegExp(`^#${m=h,m.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`)}`):null,a=r??function(e,t){if(void 0!==globalThis.window&&globalThis.history)return{pushState:c,replaceState:u,addPopstateListener:l,getLocation:e,getHash:p};const r=f(t);return{...d(t),getLocation:()=>(r("getLocation"),"")}}(()=>(e=>{try{return encodeURI(decodeURI(e))}catch(t){return console.warn(`[browser-env] Could not encode path "${e}"`,t),e}})(w(globalThis.location.hash,o))+globalThis.location.search,"hash-plugin"),i={forceDeactivate:n.forceDeactivate,source:"popstate",replace:!0},s={removePopStateListener:void 0};var h,m;return function(t){return new L(t,e.getPluginApi(t),n,a,o,i,s).getPlugin()}},exports.isState=s;//# sourceMappingURL=index.js.map
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -40,11 +40,11 @@ declare function hashPluginFactory(opts?: Partial<HashPluginOptions>, browser?:
|
|
|
40
40
|
type TransitionPhase = "deactivating" | "activating";
|
|
41
41
|
type TransitionReason = "success" | "blocked" | "cancelled" | "error";
|
|
42
42
|
interface TransitionMeta {
|
|
43
|
-
readonly reload?: boolean;
|
|
44
|
-
readonly redirected?: boolean;
|
|
45
43
|
phase: TransitionPhase;
|
|
46
|
-
from?: string;
|
|
47
44
|
reason: TransitionReason;
|
|
45
|
+
reload?: boolean;
|
|
46
|
+
redirected?: boolean;
|
|
47
|
+
from?: string;
|
|
48
48
|
blocker?: string;
|
|
49
49
|
segments: {
|
|
50
50
|
deactivated: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/hash-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Hash-based routing plugin for Real-Router",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
},
|
|
44
44
|
"sideEffects": false,
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@real-router/core": "^0.
|
|
46
|
+
"@real-router/core": "^0.37.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@testing-library/jest-dom": "6.9.1",
|
|
50
50
|
"jsdom": "28.1.0",
|
|
51
|
-
"browser-env": "^0.1.
|
|
52
|
-
"type-guards": "^0.3.
|
|
51
|
+
"browser-env": "^0.1.4",
|
|
52
|
+
"type-guards": "^0.3.7"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"test": "vitest",
|