@real-router/hash-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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/cjs/index.d.ts +109 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/metafile-cjs.json +1 -0
- package/dist/esm/index.d.mts +109 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/metafile-esm.json +1 -0
- package/package.json +62 -0
- package/src/constants.ts +16 -0
- package/src/factory.ts +63 -0
- package/src/hash-utils.ts +79 -0
- package/src/index.ts +48 -0
- package/src/plugin.ts +113 -0
- package/src/types.ts +28 -0
- package/src/validation.ts +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Oleg Ivanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# @real-router/hash-plugin
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
|
|
6
|
+
Hash-based routing plugin for Real-Router. Uses URL hash fragment for navigation — no server configuration needed.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
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
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createRouter } from "@real-router/core";
|
|
24
|
+
import { hashPluginFactory } from "@real-router/hash-plugin";
|
|
25
|
+
|
|
26
|
+
const router = createRouter([
|
|
27
|
+
{ name: "home", path: "/" },
|
|
28
|
+
{ name: "products", path: "/products/:id" },
|
|
29
|
+
{ name: "cart", path: "/cart" },
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// Basic usage
|
|
33
|
+
router.usePlugin(hashPluginFactory());
|
|
34
|
+
|
|
35
|
+
// With options
|
|
36
|
+
router.usePlugin(
|
|
37
|
+
hashPluginFactory({
|
|
38
|
+
hashPrefix: "!",
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
await router.start();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
```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
|
+
```
|
|
60
|
+
|
|
61
|
+
| Option | Type | Default | Description |
|
|
62
|
+
| ----------------- | --------- | ------- | ----------------------------------------------------- |
|
|
63
|
+
| `hashPrefix` | `string` | `""` | Prefix after `#` (e.g., `"!"` → `#!/path`) |
|
|
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
|
|
74
|
+
|
|
75
|
+
The plugin extends the router instance with browser-specific methods (via [`extendRouter()`](https://github.com/greydragon888/real-router/wiki/extendRouter)):
|
|
76
|
+
|
|
77
|
+
#### `router.buildUrl(name: string, params?: Params): string`
|
|
78
|
+
|
|
79
|
+
Build full URL with hash prefix.\
|
|
80
|
+
`name: string` — route name\
|
|
81
|
+
`params?: Params` — route parameters\
|
|
82
|
+
Returns: `string` — full URL\
|
|
83
|
+
[Wiki](https://github.com/greydragon888/real-router/wiki/hash-plugin#5-router-interaction)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
router.buildUrl("users", { id: "123" });
|
|
87
|
+
// => "#!/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
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const state = router.matchUrl("https://example.com/#!/users/123");
|
|
99
|
+
// => { name: "users", params: { id: "123" }, ... }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### `router.replaceHistoryState(name: string, params?: Params, title?: string): void`
|
|
103
|
+
|
|
104
|
+
Update browser URL without triggering navigation.\
|
|
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
|
|
112
|
+
router.replaceHistoryState("users", { id: "456" });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Usage Examples
|
|
118
|
+
|
|
119
|
+
### Hashbang Routing
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
router.usePlugin(
|
|
123
|
+
hashPluginFactory({
|
|
124
|
+
hashPrefix: "!",
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
router.navigate("users", { id: "123" });
|
|
129
|
+
// URL: #!/users/123
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### With Base Path
|
|
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
|
+
```
|
|
145
|
+
|
|
146
|
+
### Form Protection
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
router.usePlugin(
|
|
150
|
+
hashPluginFactory({
|
|
151
|
+
forceDeactivate: false,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
import { getLifecycleApi } from "@real-router/core";
|
|
156
|
+
|
|
157
|
+
const lifecycle = getLifecycleApi(router);
|
|
158
|
+
lifecycle.addDeactivateGuard("checkout", () => (toState, fromState) => {
|
|
159
|
+
return !hasUnsavedChanges(); // false blocks navigation
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## SSR Support
|
|
166
|
+
|
|
167
|
+
The plugin is SSR-safe with automatic fallback:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Server-side — no errors, methods return safe defaults
|
|
171
|
+
router.usePlugin(hashPluginFactory());
|
|
172
|
+
router.buildUrl("home"); // Works
|
|
173
|
+
router.matchUrl("/path"); // Returns undefined
|
|
174
|
+
```
|
|
175
|
+
|
|
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
|
+
## Documentation
|
|
191
|
+
|
|
192
|
+
Full documentation available on the [Wiki](https://github.com/greydragon888/real-router/wiki/hash-plugin):
|
|
193
|
+
|
|
194
|
+
- [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
|
+
- [Behavior & Edge Cases](https://github.com/greydragon888/real-router/wiki/hash-plugin#8-behavior)
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Related Packages
|
|
202
|
+
|
|
203
|
+
- [@real-router/core](https://www.npmjs.com/package/@real-router/core) — Core router
|
|
204
|
+
- [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) — History API routing
|
|
205
|
+
- [@real-router/react](https://www.npmjs.com/package/@real-router/react) — React integration
|
|
206
|
+
- [@real-router/logger-plugin](https://www.npmjs.com/package/@real-router/logger-plugin) — Debug logging
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT © [Oleg Ivanov](https://github.com/greydragon888)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { State as State$1, PluginFactory, Params as Params$1 } from '@real-router/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hash-based routing plugin configuration.
|
|
5
|
+
* Uses URL hash fragment for navigation (e.g., example.com/#/path).
|
|
6
|
+
*/
|
|
7
|
+
interface HashPluginOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Prefix for hash (e.g., "!" for "#!/path").
|
|
10
|
+
*
|
|
11
|
+
* @default ""
|
|
12
|
+
*/
|
|
13
|
+
hashPrefix?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Base path prepended before hash (e.g., "/app" → "/app#/path").
|
|
16
|
+
*
|
|
17
|
+
* @default ""
|
|
18
|
+
*/
|
|
19
|
+
base?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Force deactivation of current route even if canDeactivate returns false.
|
|
22
|
+
*
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
forceDeactivate?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface HistoryBrowser {
|
|
29
|
+
pushState: (state: State$1, path: string) => void;
|
|
30
|
+
replaceState: (state: State$1, path: string) => void;
|
|
31
|
+
addPopstateListener: (fn: (evt: PopStateEvent) => void) => () => void;
|
|
32
|
+
getHash: () => string;
|
|
33
|
+
}
|
|
34
|
+
interface Browser extends HistoryBrowser {
|
|
35
|
+
getLocation: () => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare function hashPluginFactory(opts?: Partial<HashPluginOptions>, browser?: Browser): PluginFactory;
|
|
39
|
+
|
|
40
|
+
type TransitionPhase = "deactivating" | "activating";
|
|
41
|
+
type TransitionReason = "success" | "blocked" | "cancelled" | "error";
|
|
42
|
+
interface TransitionMeta {
|
|
43
|
+
readonly reload?: boolean;
|
|
44
|
+
readonly redirected?: boolean;
|
|
45
|
+
phase: TransitionPhase;
|
|
46
|
+
from?: string;
|
|
47
|
+
reason: TransitionReason;
|
|
48
|
+
blocker?: string;
|
|
49
|
+
segments: {
|
|
50
|
+
deactivated: string[];
|
|
51
|
+
activated: string[];
|
|
52
|
+
intersection: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
interface State<P extends Params = Params, MP extends Params = Params> {
|
|
56
|
+
name: string;
|
|
57
|
+
params: P;
|
|
58
|
+
path: string;
|
|
59
|
+
meta?: StateMeta<MP> | undefined;
|
|
60
|
+
transition?: TransitionMeta | undefined;
|
|
61
|
+
}
|
|
62
|
+
interface StateMeta<P extends Params = Params> {
|
|
63
|
+
id: number;
|
|
64
|
+
params: P;
|
|
65
|
+
}
|
|
66
|
+
interface Params {
|
|
67
|
+
[key: string]: string | string[] | number | number[] | boolean | boolean[] | Params | Params[] | Record<string, string | number | boolean> | null | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Enhanced type guard for State with deep validation.
|
|
72
|
+
* Checks not only presence but also types of all required fields.
|
|
73
|
+
* Validates params using isParams and meta structure if present.
|
|
74
|
+
*
|
|
75
|
+
* @param value - Value to check
|
|
76
|
+
* @returns true if value is a valid State object with correct types
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* isStateStrict({ name: 'home', params: {}, path: '/', meta: { id: 1 } }); // true
|
|
80
|
+
* isStateStrict({ name: 'home', params: 'invalid', path: '/' }); // false
|
|
81
|
+
*/
|
|
82
|
+
declare function isStateStrict<P extends Params = Params, MP extends Params = Params>(value: unknown): value is State<P, MP>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Module augmentation for real-router.
|
|
86
|
+
* Extends Router interface with hash plugin methods.
|
|
87
|
+
*/
|
|
88
|
+
declare module "@real-router/core" {
|
|
89
|
+
interface Router {
|
|
90
|
+
/**
|
|
91
|
+
* Builds full URL for a route with base path and hash prefix.
|
|
92
|
+
* Added by hash plugin.
|
|
93
|
+
*/
|
|
94
|
+
buildUrl: (name: string, params?: Params$1) => string;
|
|
95
|
+
/**
|
|
96
|
+
* Matches URL and returns corresponding state.
|
|
97
|
+
* Added by hash plugin.
|
|
98
|
+
*/
|
|
99
|
+
matchUrl: (url: string) => State$1 | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Replaces current history state without triggering navigation.
|
|
102
|
+
* Added by hash plugin.
|
|
103
|
+
*/
|
|
104
|
+
replaceHistoryState: (name: string, params?: Params$1, title?: string) => void;
|
|
105
|
+
start(path?: string): Promise<State$1>;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { type Browser, type HashPluginOptions, hashPluginFactory, isStateStrict as isState };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=require("@real-router/core"),t=/^[A-Z_a-z][\w-]*(?:\.[A-Z_a-z][\w-]*)*$/;function r(e,t=new WeakSet){if(null==e)return!0;const n=typeof e;if("string"===n||"boolean"===n)return!0;if("number"===n)return Number.isFinite(e);if("function"===n||"symbol"===n)return!1;if(Array.isArray(e))return!t.has(e)&&(t.add(e),e.every(e=>r(e,t)));if("object"===n){if(t.has(e))return!1;t.add(e);const n=Object.getPrototypeOf(e);return(null===n||n===Object.prototype)&&Object.values(e).every(e=>r(e,t))}return!1}function n(e){if(null==e)return!0;const t=typeof e;return"string"===t||"boolean"===t||"number"===t&&Number.isFinite(e)}function o(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 o=!1;for(const t in e){if(!Object.hasOwn(e,t))continue;const r=e[t];if(!n(r)){const e=typeof r;if("function"===e||"symbol"===e)return!1;o=!0;break}}return!o||r(e)}function a(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 i(e){if("object"!=typeof e||null===e)return!1;const r=e;return!!function(e){return function(e){return"string"==typeof e&&(""===e||!(e.length>1e4)&&(!!e.startsWith("@@")||t.test(e)))}(e.name)&&"string"==typeof e.path&&o(e.params)}(r)&&(void 0===r.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)&&!a(e[t]))return!1;return!0}(t.params)||"id"in t&&"number"!=typeof t.id)}(r.meta))}var s=(e,t)=>{globalThis.history.pushState(e,"",t)},c=(e,t)=>{globalThis.history.replaceState(e,"",t)},u=e=>(globalThis.addEventListener("popstate",e),()=>{globalThis.removeEventListener("popstate",e)}),l=()=>globalThis.location.hash,p=()=>{},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)}},h=e=>{const t=f(e);return{pushState:()=>{t("pushState")},replaceState:()=>{t("replaceState")},addPopstateListener:()=>(t("addPopstateListener"),p),getHash:()=>(t("getHash"),"")}};function d(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 m(t){let r=!1,n=null;async function o(a){if(r)return console.warn(`[${t.loggerContext}] Transition in progress, deferring popstate event`),void(n=a);r=!0;try{const e=function(e,t,r){if(i(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,t.api,t.browser);e?await t.router.navigate(e.name,e.params,t.transitionOptions):await t.router.navigateToDefault({...t.transitionOptions,reload:!0,replace:!0})}catch(r){r instanceof e.RouterError||function(e){console.error(`[${t.loggerContext}] Critical error in onPopState`,e);try{const e=t.router.getState();if(e){const r=t.buildUrl(e.name,e.params);t.browser.replaceState(e,r)}}catch(e){console.error(`[${t.loggerContext}] Failed to recover from critical error`,e)}}(r)}finally{r=!1,function(){if(n){const e=n;n=null,console.warn(`[${t.loggerContext}] Processing deferred popstate event`),o(e)}}()}}return e=>{o(e)}}function b(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`);d(e.makeState(i.name,i.params,t.buildPath(i.name,i.params),{params:i.meta},1),n(o,a),!0,r)}}var g={hashPrefix:"",base:"",forceDeactivate:!0},v="hash-plugin",y=new Map;function w(e,t,r){const n=(e=>{const t=y.get(e);if(void 0!==t)return t;const r=e.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`);return y.set(e,r),r})(t);return(n?e.replace(r.get(`^#${n}`),""):e.slice(1))||"/"}var S,P,$=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=(t,n)=>{const o=e.buildPath(t,n);return`${r.base}#${r.hashPrefix}${o}`};this.#n=t.extendRouter({buildUrl:c,matchUrl:e=>{const n=function(e,t,r){const n=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,v);return n?w(n.hash,t,r)+n.search:null}(e,r.hashPrefix,o);return n?t.matchPath(n):void 0},replaceHistoryState:b(t,e,n,c)});const u=m({router:e,api:t,browser:n,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:u,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;d(e,this.#e.buildUrl(e.name,e.params),n,this.#t)}}}},L=(S=g,P=v,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){L(t);const n={...g,...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=function(){const e=new Map;return{get(t){const r=e.get(t);if(void 0!==r)return r;const n=new RegExp(t);return e.set(t,n),n}}}(),a=r??function(e,t){if(void 0!==globalThis.window&&globalThis.history)return{pushState:s,replaceState:c,addPopstateListener:u,getLocation:e,getHash:l};const r=f(t);return{...h(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,n.hashPrefix,o))+globalThis.location.search,"hash-plugin"),i={forceDeactivate:n.forceDeactivate,source:"popstate",replace:!0},p={removePopStateListener:void 0};return function(t){return new $(t,e.getPluginApi(t),n,a,o,i,p).getPlugin()}},exports.isState=i;//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/constants.ts","../../src/hash-utils.ts","../../src/plugin.ts","../../src/validation.ts","../../src/factory.ts"],"names":["i","f","c","getPluginApi"],"mappings":"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","file":"index.js","sourcesContent":["// packages/hash-plugin/src/constants.ts\n\nimport type { HashPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<HashPluginOptions> = {\n hashPrefix: \"\",\n base: \"\",\n forceDeactivate: true,\n};\n\n/**\n * Source identifier for transitions triggered by browser events.\n */\nexport const source = \"popstate\";\n\nexport const LOGGER_CONTEXT = \"hash-plugin\";\n","// packages/hash-plugin/src/hash-utils.ts\n\nimport { safeParseUrl } from \"browser-env\";\n\nimport { LOGGER_CONTEXT } from \"./constants\";\n\nexport interface RegExpCache {\n get: (pattern: string) => RegExp;\n}\n\nconst escapeRegExpCache = new Map<string, string>();\n\nexport const escapeRegExp = (str: string): string => {\n const cached = escapeRegExpCache.get(str);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const escaped = str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n escapeRegExpCache.set(str, escaped);\n\n return escaped;\n};\n\nexport function createRegExpCache(): RegExpCache {\n const cache = new Map<string, RegExp>();\n\n return {\n get(pattern: string): RegExp {\n const cached = cache.get(pattern);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const newRegExp = new RegExp(pattern);\n\n cache.set(pattern, newRegExp);\n\n return newRegExp;\n },\n };\n}\n\n/**\n * Extract path from URL hash, stripping hash prefix.\n *\n * @param hash - URL hash (e.g., \"#/path\" or \"#!/path\")\n * @param hashPrefix - Hash prefix to strip (e.g., \"!\")\n * @param regExpCache - RegExp cache for compiled patterns\n * @returns Extracted path (e.g., \"/path\")\n */\nexport function extractHashPath(\n hash: string,\n hashPrefix: string,\n regExpCache: RegExpCache,\n): string {\n const escapedHashPrefix = escapeRegExp(hashPrefix);\n const path = escapedHashPrefix\n ? hash.replace(regExpCache.get(`^#${escapedHashPrefix}`), \"\")\n : hash.slice(1);\n\n return path || \"/\";\n}\n\nexport function hashUrlToPath(\n url: string,\n hashPrefix: string,\n regExpCache: RegExpCache,\n): string | null {\n const parsedUrl = safeParseUrl(url, LOGGER_CONTEXT);\n\n return parsedUrl\n ? extractHashPath(parsedUrl.hash, hashPrefix, regExpCache) +\n parsedUrl.search\n : null;\n}\n","import {\n createPopstateHandler,\n createPopstateLifecycle,\n createStartInterceptor,\n createReplaceHistoryState,\n shouldReplaceHistory,\n updateBrowserState,\n} from \"browser-env\";\n\nimport { hashUrlToPath } from \"./hash-utils\";\n\nimport type { RegExpCache } from \"./hash-utils\";\nimport type { HashPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n PluginApi,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { Browser, SharedFactoryState } from \"browser-env\";\n\nexport class HashPlugin {\n readonly #router: Router;\n readonly #browser: Browser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<HashPluginOptions>,\n browser: Browser,\n regExpCache: RegExpCache,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: SharedFactoryState,\n ) {\n this.#router = router;\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 `${options.base}#${options.hashPrefix}${path}`;\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) => {\n const path = hashUrlToPath(url, options.hashPrefix, regExpCache);\n\n return path ? api.matchPath(path) : undefined;\n },\n replaceHistoryState: createReplaceHistoryState(\n api,\n router,\n browser,\n pluginBuildUrl,\n ),\n });\n\n const handler = createPopstateHandler({\n router,\n api,\n browser,\n transitionOptions,\n loggerContext: \"hash-plugin\",\n buildUrl: (name: string, params?: Params) =>\n router.buildUrl(name, params),\n });\n\n this.#lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n this.#removeStartInterceptor();\n this.#removeExtensions();\n },\n });\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n const replaceHistory = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n this.#router,\n );\n\n const url = this.#router.buildUrl(toState.name, toState.params);\n\n updateBrowserState(toState, url, replaceHistory, this.#browser);\n },\n };\n }\n}\n","import { createOptionsValidator } from \"browser-env\";\n\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { HashPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<HashPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n);\n","import { getPluginApi } from \"@real-router/core\";\nimport {\n createSafeBrowser,\n normalizeBase,\n safelyEncodePath,\n} from \"browser-env\";\n\nimport { defaultOptions, source } from \"./constants\";\nimport { createRegExpCache, extractHashPath } from \"./hash-utils\";\nimport { HashPlugin } from \"./plugin\";\nimport { validateOptions } from \"./validation\";\n\nimport type { HashPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\nimport type { Browser, SharedFactoryState } from \"browser-env\";\n\nexport function hashPluginFactory(\n opts?: Partial<HashPluginOptions>,\n browser?: Browser,\n): PluginFactory {\n validateOptions(opts);\n\n const options: Required<HashPluginOptions> = { ...defaultOptions, ...opts };\n\n options.base = normalizeBase(options.base);\n\n const regExpCache = createRegExpCache();\n const resolvedBrowser =\n browser ??\n createSafeBrowser(\n () =>\n safelyEncodePath(\n extractHashPath(\n globalThis.location.hash,\n options.hashPrefix,\n regExpCache,\n ),\n ) + globalThis.location.search,\n \"hash-plugin\",\n );\n\n const transitionOptions = {\n forceDeactivate: options.forceDeactivate,\n source,\n replace: true as const,\n };\n\n const shared: SharedFactoryState = { removePopStateListener: undefined };\n\n return function hashPlugin(routerBase) {\n const plugin = new HashPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n regExpCache,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"../type-guards/dist/esm/index.mjs":{"bytes":3451,"imports":[{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"../browser-env/dist/esm/index.mjs":{"bytes":4084,"imports":[{"path":"../type-guards/dist/esm/index.mjs","kind":"import-statement","original":"type-guards"},{"path":"@real-router/core","kind":"import-statement","external":true},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/constants.ts":{"bytes":367,"imports":[{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/hash-utils.ts":{"bytes":1808,"imports":[{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/plugin.ts":{"bytes":2749,"imports":[{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/hash-utils.ts","kind":"import-statement","original":"./hash-utils"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/validation.ts":{"bytes":282,"imports":[{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/factory.ts":{"bytes":1621,"imports":[{"path":"@real-router/core","kind":"import-statement","external":true},{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"src/hash-utils.ts","kind":"import-statement","original":"./hash-utils"},{"path":"src/plugin.ts","kind":"import-statement","original":"./plugin"},{"path":"src/validation.ts","kind":"import-statement","original":"./validation"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":1283,"imports":[{"path":"src/factory.ts","kind":"import-statement","original":"./factory"},{"path":"../type-guards/dist/esm/index.mjs","kind":"import-statement","original":"type-guards"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/cjs/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":10268},"dist/cjs/index.js":{"imports":[{"path":"@real-router/core","kind":"import-statement","external":true},{"path":"@real-router/core","kind":"import-statement","external":true}],"exports":["hashPluginFactory","isState"],"entryPoint":"src/index.ts","inputs":{"src/factory.ts":{"bytesInOutput":906},"../type-guards/dist/esm/index.mjs":{"bytesInOutput":2337},"../browser-env/dist/esm/index.mjs":{"bytesInOutput":4688},"src/constants.ts":{"bytesInOutput":141},"src/hash-utils.ts":{"bytesInOutput":1091},"src/plugin.ts":{"bytesInOutput":1615},"src/validation.ts":{"bytesInOutput":63},"src/index.ts":{"bytesInOutput":0}},"bytes":11085}}}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { State as State$1, PluginFactory, Params as Params$1 } from '@real-router/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hash-based routing plugin configuration.
|
|
5
|
+
* Uses URL hash fragment for navigation (e.g., example.com/#/path).
|
|
6
|
+
*/
|
|
7
|
+
interface HashPluginOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Prefix for hash (e.g., "!" for "#!/path").
|
|
10
|
+
*
|
|
11
|
+
* @default ""
|
|
12
|
+
*/
|
|
13
|
+
hashPrefix?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Base path prepended before hash (e.g., "/app" → "/app#/path").
|
|
16
|
+
*
|
|
17
|
+
* @default ""
|
|
18
|
+
*/
|
|
19
|
+
base?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Force deactivation of current route even if canDeactivate returns false.
|
|
22
|
+
*
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
forceDeactivate?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface HistoryBrowser {
|
|
29
|
+
pushState: (state: State$1, path: string) => void;
|
|
30
|
+
replaceState: (state: State$1, path: string) => void;
|
|
31
|
+
addPopstateListener: (fn: (evt: PopStateEvent) => void) => () => void;
|
|
32
|
+
getHash: () => string;
|
|
33
|
+
}
|
|
34
|
+
interface Browser extends HistoryBrowser {
|
|
35
|
+
getLocation: () => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare function hashPluginFactory(opts?: Partial<HashPluginOptions>, browser?: Browser): PluginFactory;
|
|
39
|
+
|
|
40
|
+
type TransitionPhase = "deactivating" | "activating";
|
|
41
|
+
type TransitionReason = "success" | "blocked" | "cancelled" | "error";
|
|
42
|
+
interface TransitionMeta {
|
|
43
|
+
readonly reload?: boolean;
|
|
44
|
+
readonly redirected?: boolean;
|
|
45
|
+
phase: TransitionPhase;
|
|
46
|
+
from?: string;
|
|
47
|
+
reason: TransitionReason;
|
|
48
|
+
blocker?: string;
|
|
49
|
+
segments: {
|
|
50
|
+
deactivated: string[];
|
|
51
|
+
activated: string[];
|
|
52
|
+
intersection: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
interface State<P extends Params = Params, MP extends Params = Params> {
|
|
56
|
+
name: string;
|
|
57
|
+
params: P;
|
|
58
|
+
path: string;
|
|
59
|
+
meta?: StateMeta<MP> | undefined;
|
|
60
|
+
transition?: TransitionMeta | undefined;
|
|
61
|
+
}
|
|
62
|
+
interface StateMeta<P extends Params = Params> {
|
|
63
|
+
id: number;
|
|
64
|
+
params: P;
|
|
65
|
+
}
|
|
66
|
+
interface Params {
|
|
67
|
+
[key: string]: string | string[] | number | number[] | boolean | boolean[] | Params | Params[] | Record<string, string | number | boolean> | null | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Enhanced type guard for State with deep validation.
|
|
72
|
+
* Checks not only presence but also types of all required fields.
|
|
73
|
+
* Validates params using isParams and meta structure if present.
|
|
74
|
+
*
|
|
75
|
+
* @param value - Value to check
|
|
76
|
+
* @returns true if value is a valid State object with correct types
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* isStateStrict({ name: 'home', params: {}, path: '/', meta: { id: 1 } }); // true
|
|
80
|
+
* isStateStrict({ name: 'home', params: 'invalid', path: '/' }); // false
|
|
81
|
+
*/
|
|
82
|
+
declare function isStateStrict<P extends Params = Params, MP extends Params = Params>(value: unknown): value is State<P, MP>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Module augmentation for real-router.
|
|
86
|
+
* Extends Router interface with hash plugin methods.
|
|
87
|
+
*/
|
|
88
|
+
declare module "@real-router/core" {
|
|
89
|
+
interface Router {
|
|
90
|
+
/**
|
|
91
|
+
* Builds full URL for a route with base path and hash prefix.
|
|
92
|
+
* Added by hash plugin.
|
|
93
|
+
*/
|
|
94
|
+
buildUrl: (name: string, params?: Params$1) => string;
|
|
95
|
+
/**
|
|
96
|
+
* Matches URL and returns corresponding state.
|
|
97
|
+
* Added by hash plugin.
|
|
98
|
+
*/
|
|
99
|
+
matchUrl: (url: string) => State$1 | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Replaces current history state without triggering navigation.
|
|
102
|
+
* Added by hash plugin.
|
|
103
|
+
*/
|
|
104
|
+
replaceHistoryState: (name: string, params?: Params$1, title?: string) => void;
|
|
105
|
+
start(path?: string): Promise<State$1>;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { type Browser, type HashPluginOptions, hashPluginFactory, isStateStrict as isState };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getPluginApi as e,RouterError as t}from"@real-router/core";var 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,f=()=>{},h=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=h(e);return{pushState:()=>{t("pushState")},replaceState:()=>{t("replaceState")},addPopstateListener:()=>(t("addPopstateListener"),f),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):await e.router.navigateToDefault({...e.transitionOptions,reload:!0,replace:!0})}catch(r){r instanceof t||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",w=new Map;function S(e,t,r){const n=(e=>{const t=w.get(e);if(void 0!==t)return t;const r=e.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`);return w.set(e,r),r})(t);return(n?e.replace(r.get(`^#${n}`),""):e.slice(1))||"/"}var 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=(t,n)=>{const o=e.buildPath(t,n);return`${r.base}#${r.hashPrefix}${o}`};this.#n=t.extendRouter({buildUrl:c,matchUrl:e=>{const n=function(e,t,r){const n=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 n?S(n.hash,t,r)+n.search:null}(e,r.hashPrefix,o);return n?t.matchPath(n):void 0},replaceHistoryState:g(t,e,n,c)});const u=b({router:e,api:t,browser:n,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:u,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)}}}},x=(P=v,$=y,e=>{if(e)for(const t of Object.keys(e))if(t in P){const r=e[t],n=typeof P[t],o=typeof r;if(void 0!==r&&o!==n)throw new Error(`[${$}] Invalid type for '${t}': expected ${n}, got ${o}`)}});function O(t,r){x(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=function(){const e=new Map;return{get(t){const r=e.get(t);if(void 0!==r)return r;const n=new RegExp(t);return e.set(t,n),n}}}(),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=h(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}})(S(globalThis.location.hash,n.hashPrefix,o))+globalThis.location.search,"hash-plugin"),i={forceDeactivate:n.forceDeactivate,source:"popstate",replace:!0},s={removePopStateListener:void 0};return function(t){return new L(t,e(t),n,a,o,i,s).getPlugin()}}export{O as hashPluginFactory,s as isState};//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/constants.ts","../../src/hash-utils.ts","../../src/plugin.ts","../../src/validation.ts","../../src/factory.ts"],"names":["i","f","c"],"mappings":"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","file":"index.mjs","sourcesContent":["// packages/hash-plugin/src/constants.ts\n\nimport type { HashPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<HashPluginOptions> = {\n hashPrefix: \"\",\n base: \"\",\n forceDeactivate: true,\n};\n\n/**\n * Source identifier for transitions triggered by browser events.\n */\nexport const source = \"popstate\";\n\nexport const LOGGER_CONTEXT = \"hash-plugin\";\n","// packages/hash-plugin/src/hash-utils.ts\n\nimport { safeParseUrl } from \"browser-env\";\n\nimport { LOGGER_CONTEXT } from \"./constants\";\n\nexport interface RegExpCache {\n get: (pattern: string) => RegExp;\n}\n\nconst escapeRegExpCache = new Map<string, string>();\n\nexport const escapeRegExp = (str: string): string => {\n const cached = escapeRegExpCache.get(str);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const escaped = str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n escapeRegExpCache.set(str, escaped);\n\n return escaped;\n};\n\nexport function createRegExpCache(): RegExpCache {\n const cache = new Map<string, RegExp>();\n\n return {\n get(pattern: string): RegExp {\n const cached = cache.get(pattern);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const newRegExp = new RegExp(pattern);\n\n cache.set(pattern, newRegExp);\n\n return newRegExp;\n },\n };\n}\n\n/**\n * Extract path from URL hash, stripping hash prefix.\n *\n * @param hash - URL hash (e.g., \"#/path\" or \"#!/path\")\n * @param hashPrefix - Hash prefix to strip (e.g., \"!\")\n * @param regExpCache - RegExp cache for compiled patterns\n * @returns Extracted path (e.g., \"/path\")\n */\nexport function extractHashPath(\n hash: string,\n hashPrefix: string,\n regExpCache: RegExpCache,\n): string {\n const escapedHashPrefix = escapeRegExp(hashPrefix);\n const path = escapedHashPrefix\n ? hash.replace(regExpCache.get(`^#${escapedHashPrefix}`), \"\")\n : hash.slice(1);\n\n return path || \"/\";\n}\n\nexport function hashUrlToPath(\n url: string,\n hashPrefix: string,\n regExpCache: RegExpCache,\n): string | null {\n const parsedUrl = safeParseUrl(url, LOGGER_CONTEXT);\n\n return parsedUrl\n ? extractHashPath(parsedUrl.hash, hashPrefix, regExpCache) +\n parsedUrl.search\n : null;\n}\n","import {\n createPopstateHandler,\n createPopstateLifecycle,\n createStartInterceptor,\n createReplaceHistoryState,\n shouldReplaceHistory,\n updateBrowserState,\n} from \"browser-env\";\n\nimport { hashUrlToPath } from \"./hash-utils\";\n\nimport type { RegExpCache } from \"./hash-utils\";\nimport type { HashPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n PluginApi,\n Router,\n State,\n Plugin,\n} from \"@real-router/core\";\nimport type { Browser, SharedFactoryState } from \"browser-env\";\n\nexport class HashPlugin {\n readonly #router: Router;\n readonly #browser: Browser;\n readonly #removeStartInterceptor: () => void;\n readonly #removeExtensions: () => void;\n readonly #lifecycle: Pick<Plugin, \"onStart\" | \"onStop\" | \"teardown\">;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<HashPluginOptions>,\n browser: Browser,\n regExpCache: RegExpCache,\n transitionOptions: {\n source: string;\n replace: true;\n forceDeactivate?: boolean;\n },\n shared: SharedFactoryState,\n ) {\n this.#router = router;\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 `${options.base}#${options.hashPrefix}${path}`;\n };\n\n this.#removeExtensions = api.extendRouter({\n buildUrl: pluginBuildUrl,\n matchUrl: (url: string) => {\n const path = hashUrlToPath(url, options.hashPrefix, regExpCache);\n\n return path ? api.matchPath(path) : undefined;\n },\n replaceHistoryState: createReplaceHistoryState(\n api,\n router,\n browser,\n pluginBuildUrl,\n ),\n });\n\n const handler = createPopstateHandler({\n router,\n api,\n browser,\n transitionOptions,\n loggerContext: \"hash-plugin\",\n buildUrl: (name: string, params?: Params) =>\n router.buildUrl(name, params),\n });\n\n this.#lifecycle = createPopstateLifecycle({\n browser,\n shared,\n handler,\n cleanup: () => {\n this.#removeStartInterceptor();\n this.#removeExtensions();\n },\n });\n }\n\n getPlugin(): Plugin {\n return {\n ...this.#lifecycle,\n\n onTransitionSuccess: (\n toState: State,\n fromState: State | undefined,\n navOptions: NavigationOptions,\n ) => {\n const replaceHistory = shouldReplaceHistory(\n navOptions,\n toState,\n fromState,\n this.#router,\n );\n\n const url = this.#router.buildUrl(toState.name, toState.params);\n\n updateBrowserState(toState, url, replaceHistory, this.#browser);\n },\n };\n }\n}\n","import { createOptionsValidator } from \"browser-env\";\n\nimport { LOGGER_CONTEXT, defaultOptions } from \"./constants\";\n\nimport type { HashPluginOptions } from \"./types\";\n\nexport const validateOptions = createOptionsValidator<HashPluginOptions>(\n defaultOptions,\n LOGGER_CONTEXT,\n);\n","import { getPluginApi } from \"@real-router/core\";\nimport {\n createSafeBrowser,\n normalizeBase,\n safelyEncodePath,\n} from \"browser-env\";\n\nimport { defaultOptions, source } from \"./constants\";\nimport { createRegExpCache, extractHashPath } from \"./hash-utils\";\nimport { HashPlugin } from \"./plugin\";\nimport { validateOptions } from \"./validation\";\n\nimport type { HashPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\nimport type { Browser, SharedFactoryState } from \"browser-env\";\n\nexport function hashPluginFactory(\n opts?: Partial<HashPluginOptions>,\n browser?: Browser,\n): PluginFactory {\n validateOptions(opts);\n\n const options: Required<HashPluginOptions> = { ...defaultOptions, ...opts };\n\n options.base = normalizeBase(options.base);\n\n const regExpCache = createRegExpCache();\n const resolvedBrowser =\n browser ??\n createSafeBrowser(\n () =>\n safelyEncodePath(\n extractHashPath(\n globalThis.location.hash,\n options.hashPrefix,\n regExpCache,\n ),\n ) + globalThis.location.search,\n \"hash-plugin\",\n );\n\n const transitionOptions = {\n forceDeactivate: options.forceDeactivate,\n source,\n replace: true as const,\n };\n\n const shared: SharedFactoryState = { removePopStateListener: undefined };\n\n return function hashPlugin(routerBase) {\n const plugin = new HashPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n resolvedBrowser,\n regExpCache,\n transitionOptions,\n shared,\n );\n\n return plugin.getPlugin();\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"inputs":{"../type-guards/dist/esm/index.mjs":{"bytes":3451,"imports":[],"format":"esm"},"../browser-env/dist/esm/index.mjs":{"bytes":4084,"imports":[{"path":"../type-guards/dist/esm/index.mjs","kind":"import-statement","original":"type-guards"},{"path":"@real-router/core","kind":"import-statement","external":true}],"format":"esm"},"src/constants.ts":{"bytes":367,"imports":[],"format":"esm"},"src/hash-utils.ts":{"bytes":1808,"imports":[{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/plugin.ts":{"bytes":2749,"imports":[{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/hash-utils.ts","kind":"import-statement","original":"./hash-utils"}],"format":"esm"},"src/validation.ts":{"bytes":282,"imports":[{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/factory.ts":{"bytes":1621,"imports":[{"path":"@real-router/core","kind":"import-statement","external":true},{"path":"../browser-env/dist/esm/index.mjs","kind":"import-statement","original":"browser-env"},{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"src/hash-utils.ts","kind":"import-statement","original":"./hash-utils"},{"path":"src/plugin.ts","kind":"import-statement","original":"./plugin"},{"path":"src/validation.ts","kind":"import-statement","original":"./validation"}],"format":"esm"},"src/index.ts":{"bytes":1283,"imports":[{"path":"src/factory.ts","kind":"import-statement","original":"./factory"},{"path":"../type-guards/dist/esm/index.mjs","kind":"import-statement","original":"type-guards"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":10268},"dist/esm/index.mjs":{"imports":[{"path":"@real-router/core","kind":"import-statement","external":true},{"path":"@real-router/core","kind":"import-statement","external":true}],"exports":["hashPluginFactory","isState"],"entryPoint":"src/index.ts","inputs":{"src/factory.ts":{"bytesInOutput":906},"../type-guards/dist/esm/index.mjs":{"bytesInOutput":2337},"../browser-env/dist/esm/index.mjs":{"bytesInOutput":4688},"src/constants.ts":{"bytesInOutput":141},"src/hash-utils.ts":{"bytesInOutput":1091},"src/plugin.ts":{"bytesInOutput":1615},"src/validation.ts":{"bytesInOutput":63},"src/index.ts":{"bytesInOutput":0}},"bytes":11085}}}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@real-router/hash-plugin",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"description": "Hash-based routing plugin for Real-Router",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"module": "./dist/esm/index.mjs",
|
|
8
|
+
"types": "./dist/esm/index.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"development": "./src/index.ts",
|
|
12
|
+
"types": {
|
|
13
|
+
"import": "./dist/esm/index.d.mts",
|
|
14
|
+
"require": "./dist/cjs/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"import": "./dist/esm/index.mjs",
|
|
17
|
+
"require": "./dist/cjs/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/greydragon888/real-router.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"real-router",
|
|
30
|
+
"hash",
|
|
31
|
+
"routing",
|
|
32
|
+
"navigation"
|
|
33
|
+
],
|
|
34
|
+
"author": {
|
|
35
|
+
"name": "Oleg Ivanov",
|
|
36
|
+
"email": "greydragon888@gmail.com",
|
|
37
|
+
"url": "https://github.com/greydragon888"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"homepage": "https://github.com/greydragon888/real-router",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/greydragon888/real-router/issues"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "vitest",
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"type-check": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
|
|
49
|
+
"lint:package": "publint",
|
|
50
|
+
"lint:types": "attw --pack ."
|
|
51
|
+
},
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@real-router/core": "workspace:^"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@testing-library/jest-dom": "6.9.1",
|
|
58
|
+
"browser-env": "workspace:^",
|
|
59
|
+
"jsdom": "27.4.0",
|
|
60
|
+
"type-guards": "workspace:^"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// packages/hash-plugin/src/constants.ts
|
|
2
|
+
|
|
3
|
+
import type { HashPluginOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
export const defaultOptions: Required<HashPluginOptions> = {
|
|
6
|
+
hashPrefix: "",
|
|
7
|
+
base: "",
|
|
8
|
+
forceDeactivate: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Source identifier for transitions triggered by browser events.
|
|
13
|
+
*/
|
|
14
|
+
export const source = "popstate";
|
|
15
|
+
|
|
16
|
+
export const LOGGER_CONTEXT = "hash-plugin";
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getPluginApi } from "@real-router/core";
|
|
2
|
+
import {
|
|
3
|
+
createSafeBrowser,
|
|
4
|
+
normalizeBase,
|
|
5
|
+
safelyEncodePath,
|
|
6
|
+
} from "browser-env";
|
|
7
|
+
|
|
8
|
+
import { defaultOptions, source } from "./constants";
|
|
9
|
+
import { createRegExpCache, extractHashPath } from "./hash-utils";
|
|
10
|
+
import { HashPlugin } from "./plugin";
|
|
11
|
+
import { validateOptions } from "./validation";
|
|
12
|
+
|
|
13
|
+
import type { HashPluginOptions } from "./types";
|
|
14
|
+
import type { PluginFactory, Router } from "@real-router/core";
|
|
15
|
+
import type { Browser, SharedFactoryState } from "browser-env";
|
|
16
|
+
|
|
17
|
+
export function hashPluginFactory(
|
|
18
|
+
opts?: Partial<HashPluginOptions>,
|
|
19
|
+
browser?: Browser,
|
|
20
|
+
): PluginFactory {
|
|
21
|
+
validateOptions(opts);
|
|
22
|
+
|
|
23
|
+
const options: Required<HashPluginOptions> = { ...defaultOptions, ...opts };
|
|
24
|
+
|
|
25
|
+
options.base = normalizeBase(options.base);
|
|
26
|
+
|
|
27
|
+
const regExpCache = createRegExpCache();
|
|
28
|
+
const resolvedBrowser =
|
|
29
|
+
browser ??
|
|
30
|
+
createSafeBrowser(
|
|
31
|
+
() =>
|
|
32
|
+
safelyEncodePath(
|
|
33
|
+
extractHashPath(
|
|
34
|
+
globalThis.location.hash,
|
|
35
|
+
options.hashPrefix,
|
|
36
|
+
regExpCache,
|
|
37
|
+
),
|
|
38
|
+
) + globalThis.location.search,
|
|
39
|
+
"hash-plugin",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const transitionOptions = {
|
|
43
|
+
forceDeactivate: options.forceDeactivate,
|
|
44
|
+
source,
|
|
45
|
+
replace: true as const,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const shared: SharedFactoryState = { removePopStateListener: undefined };
|
|
49
|
+
|
|
50
|
+
return function hashPlugin(routerBase) {
|
|
51
|
+
const plugin = new HashPlugin(
|
|
52
|
+
routerBase as Router,
|
|
53
|
+
getPluginApi(routerBase),
|
|
54
|
+
options,
|
|
55
|
+
resolvedBrowser,
|
|
56
|
+
regExpCache,
|
|
57
|
+
transitionOptions,
|
|
58
|
+
shared,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return plugin.getPlugin();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// packages/hash-plugin/src/hash-utils.ts
|
|
2
|
+
|
|
3
|
+
import { safeParseUrl } from "browser-env";
|
|
4
|
+
|
|
5
|
+
import { LOGGER_CONTEXT } from "./constants";
|
|
6
|
+
|
|
7
|
+
export interface RegExpCache {
|
|
8
|
+
get: (pattern: string) => RegExp;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const escapeRegExpCache = new Map<string, string>();
|
|
12
|
+
|
|
13
|
+
export const escapeRegExp = (str: string): string => {
|
|
14
|
+
const cached = escapeRegExpCache.get(str);
|
|
15
|
+
|
|
16
|
+
if (cached !== undefined) {
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const escaped = str.replaceAll(/[$()*+.?[\\\]^{|}-]/g, String.raw`\$&`);
|
|
21
|
+
|
|
22
|
+
escapeRegExpCache.set(str, escaped);
|
|
23
|
+
|
|
24
|
+
return escaped;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function createRegExpCache(): RegExpCache {
|
|
28
|
+
const cache = new Map<string, RegExp>();
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
get(pattern: string): RegExp {
|
|
32
|
+
const cached = cache.get(pattern);
|
|
33
|
+
|
|
34
|
+
if (cached !== undefined) {
|
|
35
|
+
return cached;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const newRegExp = new RegExp(pattern);
|
|
39
|
+
|
|
40
|
+
cache.set(pattern, newRegExp);
|
|
41
|
+
|
|
42
|
+
return newRegExp;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract path from URL hash, stripping hash prefix.
|
|
49
|
+
*
|
|
50
|
+
* @param hash - URL hash (e.g., "#/path" or "#!/path")
|
|
51
|
+
* @param hashPrefix - Hash prefix to strip (e.g., "!")
|
|
52
|
+
* @param regExpCache - RegExp cache for compiled patterns
|
|
53
|
+
* @returns Extracted path (e.g., "/path")
|
|
54
|
+
*/
|
|
55
|
+
export function extractHashPath(
|
|
56
|
+
hash: string,
|
|
57
|
+
hashPrefix: string,
|
|
58
|
+
regExpCache: RegExpCache,
|
|
59
|
+
): string {
|
|
60
|
+
const escapedHashPrefix = escapeRegExp(hashPrefix);
|
|
61
|
+
const path = escapedHashPrefix
|
|
62
|
+
? hash.replace(regExpCache.get(`^#${escapedHashPrefix}`), "")
|
|
63
|
+
: hash.slice(1);
|
|
64
|
+
|
|
65
|
+
return path || "/";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hashUrlToPath(
|
|
69
|
+
url: string,
|
|
70
|
+
hashPrefix: string,
|
|
71
|
+
regExpCache: RegExpCache,
|
|
72
|
+
): string | null {
|
|
73
|
+
const parsedUrl = safeParseUrl(url, LOGGER_CONTEXT);
|
|
74
|
+
|
|
75
|
+
return parsedUrl
|
|
76
|
+
? extractHashPath(parsedUrl.hash, hashPrefix, regExpCache) +
|
|
77
|
+
parsedUrl.search
|
|
78
|
+
: null;
|
|
79
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// packages/hash-plugin/src/index.ts
|
|
2
|
+
/* eslint-disable @typescript-eslint/method-signature-style -- method syntax required for declaration merging overload (property syntax causes TS2717) */
|
|
3
|
+
// Public API exports for hash-plugin
|
|
4
|
+
|
|
5
|
+
import type { Params, State } from "@real-router/core";
|
|
6
|
+
|
|
7
|
+
// Main plugin factory
|
|
8
|
+
export { hashPluginFactory } from "./factory";
|
|
9
|
+
|
|
10
|
+
// Types
|
|
11
|
+
export type { HashPluginOptions } from "./types";
|
|
12
|
+
|
|
13
|
+
export type { Browser } from "browser-env";
|
|
14
|
+
|
|
15
|
+
// Type guards
|
|
16
|
+
export { isStateStrict as isState } from "type-guards";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Module augmentation for real-router.
|
|
20
|
+
* Extends Router interface with hash plugin methods.
|
|
21
|
+
*/
|
|
22
|
+
declare module "@real-router/core" {
|
|
23
|
+
interface Router {
|
|
24
|
+
/**
|
|
25
|
+
* Builds full URL for a route with base path and hash prefix.
|
|
26
|
+
* Added by hash plugin.
|
|
27
|
+
*/
|
|
28
|
+
buildUrl: (name: string, params?: Params) => string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Matches URL and returns corresponding state.
|
|
32
|
+
* Added by hash plugin.
|
|
33
|
+
*/
|
|
34
|
+
matchUrl: (url: string) => State | undefined;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Replaces current history state without triggering navigation.
|
|
38
|
+
* Added by hash plugin.
|
|
39
|
+
*/
|
|
40
|
+
replaceHistoryState: (
|
|
41
|
+
name: string,
|
|
42
|
+
params?: Params,
|
|
43
|
+
title?: string,
|
|
44
|
+
) => void;
|
|
45
|
+
|
|
46
|
+
start(path?: string): Promise<State>;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPopstateHandler,
|
|
3
|
+
createPopstateLifecycle,
|
|
4
|
+
createStartInterceptor,
|
|
5
|
+
createReplaceHistoryState,
|
|
6
|
+
shouldReplaceHistory,
|
|
7
|
+
updateBrowserState,
|
|
8
|
+
} from "browser-env";
|
|
9
|
+
|
|
10
|
+
import { hashUrlToPath } from "./hash-utils";
|
|
11
|
+
|
|
12
|
+
import type { RegExpCache } from "./hash-utils";
|
|
13
|
+
import type { HashPluginOptions } from "./types";
|
|
14
|
+
import type {
|
|
15
|
+
NavigationOptions,
|
|
16
|
+
Params,
|
|
17
|
+
PluginApi,
|
|
18
|
+
Router,
|
|
19
|
+
State,
|
|
20
|
+
Plugin,
|
|
21
|
+
} from "@real-router/core";
|
|
22
|
+
import type { Browser, SharedFactoryState } from "browser-env";
|
|
23
|
+
|
|
24
|
+
export class HashPlugin {
|
|
25
|
+
readonly #router: Router;
|
|
26
|
+
readonly #browser: Browser;
|
|
27
|
+
readonly #removeStartInterceptor: () => void;
|
|
28
|
+
readonly #removeExtensions: () => void;
|
|
29
|
+
readonly #lifecycle: Pick<Plugin, "onStart" | "onStop" | "teardown">;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
router: Router,
|
|
33
|
+
api: PluginApi,
|
|
34
|
+
options: Required<HashPluginOptions>,
|
|
35
|
+
browser: Browser,
|
|
36
|
+
regExpCache: RegExpCache,
|
|
37
|
+
transitionOptions: {
|
|
38
|
+
source: string;
|
|
39
|
+
replace: true;
|
|
40
|
+
forceDeactivate?: boolean;
|
|
41
|
+
},
|
|
42
|
+
shared: SharedFactoryState,
|
|
43
|
+
) {
|
|
44
|
+
this.#router = router;
|
|
45
|
+
this.#browser = browser;
|
|
46
|
+
|
|
47
|
+
this.#removeStartInterceptor = createStartInterceptor(api, browser);
|
|
48
|
+
|
|
49
|
+
const pluginBuildUrl = (route: string, params?: Params) => {
|
|
50
|
+
const path = router.buildPath(route, params);
|
|
51
|
+
|
|
52
|
+
return `${options.base}#${options.hashPrefix}${path}`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.#removeExtensions = api.extendRouter({
|
|
56
|
+
buildUrl: pluginBuildUrl,
|
|
57
|
+
matchUrl: (url: string) => {
|
|
58
|
+
const path = hashUrlToPath(url, options.hashPrefix, regExpCache);
|
|
59
|
+
|
|
60
|
+
return path ? api.matchPath(path) : undefined;
|
|
61
|
+
},
|
|
62
|
+
replaceHistoryState: createReplaceHistoryState(
|
|
63
|
+
api,
|
|
64
|
+
router,
|
|
65
|
+
browser,
|
|
66
|
+
pluginBuildUrl,
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const handler = createPopstateHandler({
|
|
71
|
+
router,
|
|
72
|
+
api,
|
|
73
|
+
browser,
|
|
74
|
+
transitionOptions,
|
|
75
|
+
loggerContext: "hash-plugin",
|
|
76
|
+
buildUrl: (name: string, params?: Params) =>
|
|
77
|
+
router.buildUrl(name, params),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.#lifecycle = createPopstateLifecycle({
|
|
81
|
+
browser,
|
|
82
|
+
shared,
|
|
83
|
+
handler,
|
|
84
|
+
cleanup: () => {
|
|
85
|
+
this.#removeStartInterceptor();
|
|
86
|
+
this.#removeExtensions();
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getPlugin(): Plugin {
|
|
92
|
+
return {
|
|
93
|
+
...this.#lifecycle,
|
|
94
|
+
|
|
95
|
+
onTransitionSuccess: (
|
|
96
|
+
toState: State,
|
|
97
|
+
fromState: State | undefined,
|
|
98
|
+
navOptions: NavigationOptions,
|
|
99
|
+
) => {
|
|
100
|
+
const replaceHistory = shouldReplaceHistory(
|
|
101
|
+
navOptions,
|
|
102
|
+
toState,
|
|
103
|
+
fromState,
|
|
104
|
+
this.#router,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const url = this.#router.buildUrl(toState.name, toState.params);
|
|
108
|
+
|
|
109
|
+
updateBrowserState(toState, url, replaceHistory, this.#browser);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// packages/hash-plugin/src/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hash-based routing plugin configuration.
|
|
5
|
+
* Uses URL hash fragment for navigation (e.g., example.com/#/path).
|
|
6
|
+
*/
|
|
7
|
+
export interface HashPluginOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Prefix for hash (e.g., "!" for "#!/path").
|
|
10
|
+
*
|
|
11
|
+
* @default ""
|
|
12
|
+
*/
|
|
13
|
+
hashPrefix?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Base path prepended before hash (e.g., "/app" → "/app#/path").
|
|
17
|
+
*
|
|
18
|
+
* @default ""
|
|
19
|
+
*/
|
|
20
|
+
base?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Force deactivation of current route even if canDeactivate returns false.
|
|
24
|
+
*
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
forceDeactivate?: boolean;
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createOptionsValidator } from "browser-env";
|
|
2
|
+
|
|
3
|
+
import { LOGGER_CONTEXT, defaultOptions } from "./constants";
|
|
4
|
+
|
|
5
|
+
import type { HashPluginOptions } from "./types";
|
|
6
|
+
|
|
7
|
+
export const validateOptions = createOptionsValidator<HashPluginOptions>(
|
|
8
|
+
defaultOptions,
|
|
9
|
+
LOGGER_CONTEXT,
|
|
10
|
+
);
|