@real-router/browser-plugin 0.1.0 → 0.1.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 +68 -817
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metafile-cjs.json +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/metafile-esm.json +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -3,18 +3,7 @@
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://www.typescriptlang.org/)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## Problem
|
|
9
|
-
|
|
10
|
-
SPAs need bidirectional synchronization between router state and browser URL.
|
|
11
|
-
Without this plugin, navigating through your application would change the internal router state but leave the browser URL unchanged.
|
|
12
|
-
Additionally, browser navigation buttons (back/forward) wouldn't affect the router, breaking the expected browser behavior.
|
|
13
|
-
|
|
14
|
-
## Solution
|
|
15
|
-
|
|
16
|
-
The browser plugin automatically synchronizes router state with browser history. When you navigate programmatically, it updates the URL.
|
|
17
|
-
When users interact with browser controls or manually change the URL, it updates the router state accordingly.
|
|
6
|
+
Browser History API integration for Real-Router. Synchronizes router state with browser URL and handles back/forward navigation.
|
|
18
7
|
|
|
19
8
|
## Installation
|
|
20
9
|
|
|
@@ -54,112 +43,9 @@ router.usePlugin(
|
|
|
54
43
|
router.start();
|
|
55
44
|
```
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### `browserPluginFactory(options?, browser?)`
|
|
60
|
-
|
|
61
|
-
#### Parameters
|
|
62
|
-
|
|
63
|
-
**`options`**: `BrowserPluginOptions` (optional)
|
|
64
|
-
|
|
65
|
-
- Configuration object for plugin behavior
|
|
66
|
-
|
|
67
|
-
**`browser`**: `Browser` (optional)
|
|
68
|
-
|
|
69
|
-
- Browser API abstraction for testing/SSR
|
|
70
|
-
|
|
71
|
-
#### Returns
|
|
72
|
-
|
|
73
|
-
`PluginFactory` — plugin factory for `router.usePlugin()`
|
|
74
|
-
|
|
75
|
-
### Configuration Options
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
// Base options shared by both modes
|
|
79
|
-
interface BaseBrowserPluginOptions {
|
|
80
|
-
forceDeactivate?: boolean; // Force navigation even if canDeactivate returns false
|
|
81
|
-
base?: string; // Base path for all routes
|
|
82
|
-
mergeState?: boolean; // Merge with existing history.state
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Hash-based routing
|
|
86
|
-
interface HashModeOptions extends BaseBrowserPluginOptions {
|
|
87
|
-
useHash: true;
|
|
88
|
-
hashPrefix?: string; // Prefix for hash routes (e.g., "!" for #!/path)
|
|
89
|
-
preserveHash?: never; // Not available in hash mode
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// HTML5 History routing
|
|
93
|
-
interface HistoryModeOptions extends BaseBrowserPluginOptions {
|
|
94
|
-
useHash?: false;
|
|
95
|
-
preserveHash?: boolean; // Preserve hash fragment on navigation
|
|
96
|
-
hashPrefix?: never; // Not available in history mode
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Type-safe discriminated union prevents conflicting options
|
|
100
|
-
type BrowserPluginOptions = HashModeOptions | HistoryModeOptions;
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**Type Safety:** The configuration uses a discriminated union to prevent invalid option combinations at compile-time (TypeScript) and runtime (JavaScript).
|
|
104
|
-
|
|
105
|
-
#### `forceDeactivate`
|
|
106
|
-
|
|
107
|
-
Controls whether `canDeactivate` guards can block browser-initiated navigation (back/forward buttons).
|
|
108
|
-
|
|
109
|
-
- `true` **(default)** - browser navigation always succeeds, bypassing canDeactivate guards
|
|
110
|
-
- `false` - canDeactivate guards can block browser navigation
|
|
111
|
-
|
|
112
|
-
**Example:**
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
router.usePlugin(
|
|
116
|
-
browserPluginFactory({
|
|
117
|
-
forceDeactivate: false, // Allow guards to block back button
|
|
118
|
-
}),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// Route with canDeactivate guard
|
|
122
|
-
router.canDeactivate("form", () => {
|
|
123
|
-
return window.confirm("Unsaved changes. Leave anyway?");
|
|
124
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
When `forceDeactivate` is `false` and the user presses the browser back button, the confirmation dialog will appear. If the user cancels, the navigation is blocked and the URL is restored to match the current route. With the default `true` value, browser navigation would always proceed regardless of the guard's return value.
|
|
128
|
-
|
|
129
|
-
#### `useHash`
|
|
130
|
-
|
|
131
|
-
Enables hash-based routing for environments without HTML5 history support.
|
|
132
|
-
|
|
133
|
-
- `false` **(default)** - use HTML5 history (`/path`)
|
|
134
|
-
- `true` - use hash routing (`#/path`)
|
|
135
|
-
|
|
136
|
-
**Example:**
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
router.usePlugin(
|
|
140
|
-
browserPluginFactory({
|
|
141
|
-
useHash: true,
|
|
142
|
-
}),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
router.navigate("products", { id: "123" });
|
|
146
|
-
// URL: http://example.com/#/products/123
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
**Browser Compatibility:**
|
|
150
|
-
|
|
151
|
-
- History mode requires HTML5 History API support
|
|
152
|
-
- Hash mode works in all browsers
|
|
153
|
-
- Plugin automatically uses `hashchange` events for old IE
|
|
154
|
-
|
|
155
|
-
#### `hashPrefix`
|
|
46
|
+
---
|
|
156
47
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- `""` **(default)** - no prefix (`#/path`)
|
|
160
|
-
- Custom string - prefixed hash (`#!/path`)
|
|
161
|
-
|
|
162
|
-
**Example:**
|
|
48
|
+
## Configuration
|
|
163
49
|
|
|
164
50
|
```typescript
|
|
165
51
|
router.usePlugin(
|
|
@@ -173,203 +59,31 @@ router.navigate("products", { id: "123" });
|
|
|
173
59
|
// URL: http://example.com/#!/products/123
|
|
174
60
|
```
|
|
175
61
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
#### `base`
|
|
185
|
-
|
|
186
|
-
Sets base path when app is hosted in subdirectory.
|
|
187
|
-
|
|
188
|
-
- `""` **(default)** - root hosting
|
|
189
|
-
- Path string - subdirectory hosting
|
|
190
|
-
|
|
191
|
-
**Example:**
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
// App hosted at http://example.com/app/
|
|
195
|
-
router.usePlugin(
|
|
196
|
-
browserPluginFactory({
|
|
197
|
-
base: "/app",
|
|
198
|
-
}),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
router.navigate("products", { id: "123" });
|
|
202
|
-
// URL: http://example.com/app/products/123
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
**Automatic Normalization:**
|
|
206
|
-
|
|
207
|
-
The plugin automatically normalizes base paths to prevent common configuration errors:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
// Missing leading slash - automatically added
|
|
211
|
-
router.usePlugin(browserPluginFactory({ base: "app" }));
|
|
212
|
-
// Normalized to: '/app'
|
|
213
|
-
|
|
214
|
-
// Trailing slash - automatically removed
|
|
215
|
-
router.usePlugin(browserPluginFactory({ base: "/app/" }));
|
|
216
|
-
// Normalized to: '/app'
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
#### `mergeState`
|
|
220
|
-
|
|
221
|
-
Preserves external properties in `history.state`.
|
|
222
|
-
|
|
223
|
-
- `false` **(default)** - replace entire state
|
|
224
|
-
- `true` - merge router state with existing state
|
|
225
|
-
|
|
226
|
-
**Example:**
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
// External code sets history state
|
|
230
|
-
history.pushState({ externalData: "value" }, "", "/current");
|
|
231
|
-
|
|
232
|
-
router.usePlugin(
|
|
233
|
-
browserPluginFactory({
|
|
234
|
-
mergeState: true,
|
|
235
|
-
}),
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
router.navigate("products", { id: "123" });
|
|
239
|
-
// history.state contains both router state and externalData
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
**Use Cases:**
|
|
243
|
-
|
|
244
|
-
- Integration with analytics libraries
|
|
245
|
-
- Preserving scroll positions
|
|
246
|
-
- Third-party state management
|
|
247
|
-
|
|
248
|
-
#### `preserveHash`
|
|
249
|
-
|
|
250
|
-
Maintains URL hash fragment during navigation. **Only works with `useHash: false`** (history mode).
|
|
62
|
+
| Option | Type | Default | Description |
|
|
63
|
+
| ----------------- | --------- | ------- | -------------------------------------------------------------------- |
|
|
64
|
+
| `useHash` | `boolean` | `false` | Use hash routing (`#/path`) instead of History API |
|
|
65
|
+
| `hashPrefix` | `string` | `""` | Hash prefix (e.g., `"!"` → `#!/path`). Only with `useHash: true` |
|
|
66
|
+
| `preserveHash` | `boolean` | `true` | Keep URL hash fragment during navigation. Only with `useHash: false` |
|
|
67
|
+
| `base` | `string` | `""` | Base path for all routes (e.g., `"/app"`) |
|
|
68
|
+
| `forceDeactivate` | `boolean` | `true` | Bypass `canDeactivate` guards on browser back/forward |
|
|
69
|
+
| `mergeState` | `boolean` | `false` | Merge with existing `history.state` |
|
|
251
70
|
|
|
252
|
-
|
|
253
|
-
- `false` - remove hash fragment
|
|
254
|
-
|
|
255
|
-
**Example:**
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
// Current URL: /page#section
|
|
259
|
-
router.usePlugin(
|
|
260
|
-
browserPluginFactory({
|
|
261
|
-
useHash: false, // Required for preserveHash (default)
|
|
262
|
-
preserveHash: true,
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
router.navigate("other", {});
|
|
267
|
-
// URL: /other#section (hash preserved)
|
|
268
|
-
|
|
269
|
-
router.usePlugin(
|
|
270
|
-
browserPluginFactory({
|
|
271
|
-
useHash: false,
|
|
272
|
-
preserveHash: false,
|
|
273
|
-
}),
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
router.navigate("other", {});
|
|
277
|
-
// URL: /other (hash removed)
|
|
278
|
-
```
|
|
71
|
+
**Type Safety:** Options use discriminated union — `hashPrefix` and `preserveHash` are mutually exclusive at compile time.
|
|
279
72
|
|
|
280
|
-
|
|
73
|
+
See [Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin#3-configuration-options) for detailed option descriptions and examples.
|
|
281
74
|
|
|
282
|
-
|
|
283
|
-
- Deep linking to sections
|
|
284
|
-
- Progressive enhancement
|
|
285
|
-
|
|
286
|
-
**Note:** Using `preserveHash` with `useHash: true` will log a warning and the option will be ignored.
|
|
75
|
+
---
|
|
287
76
|
|
|
288
77
|
## Added Router Methods
|
|
289
78
|
|
|
290
|
-
The plugin extends the router
|
|
291
|
-
|
|
292
|
-
### `router.buildUrl(name, params?)`
|
|
293
|
-
|
|
294
|
-
Builds full URL including base path and hash prefix.
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
router.buildUrl("products", { id: "123" });
|
|
298
|
-
// Returns: "/products/123"
|
|
299
|
-
|
|
300
|
-
// With base="/app", useHash=true, hashPrefix="!"
|
|
301
|
-
// Returns: "/app#!/products/123"
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**Security Note:**
|
|
305
|
-
|
|
306
|
-
The plugin automatically URL-encodes parameters to prevent injection attacks. When using the output in templates:
|
|
307
|
-
|
|
308
|
-
```
|
|
309
|
-
// ✅ SAFE: Modern frameworks auto-escape
|
|
310
|
-
// React
|
|
311
|
-
<Link to={router.buildUrl('users', { id: userInput })} />
|
|
312
|
-
|
|
313
|
-
// Vue
|
|
314
|
-
<router-link :to="router.buildUrl('users', { id: userInput })" />
|
|
315
|
-
|
|
316
|
-
// Angular
|
|
317
|
-
<a [routerLink]="router.buildUrl('users', { id: userInput })">
|
|
318
|
-
|
|
319
|
-
// ❌ UNSAFE: Don't use innerHTML
|
|
320
|
-
element.innerHTML = `<a href="${router.buildUrl('users', params)}">Link</a>`; // DON'T
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Special characters are automatically encoded:
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
router.buildUrl("search", { q: '<script>alert("xss")</script>' });
|
|
327
|
-
// Returns: "/search?q=%3Cscript%3Ealert(%22xss%22)%3C%2Fscript%3E"
|
|
328
|
-
// Safe for browser APIs
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### `router.matchUrl(url)`
|
|
332
|
-
|
|
333
|
-
Parses URL and returns matching router state.
|
|
334
|
-
|
|
335
|
-
```typescript
|
|
336
|
-
const state = router.matchUrl("http://example.com/products/123?sort=name");
|
|
337
|
-
// Returns: {
|
|
338
|
-
// name: 'products',
|
|
339
|
-
// params: { id: '123', sort: 'name' },
|
|
340
|
-
// path: '/products/123?sort=name',
|
|
341
|
-
// meta: { ... }
|
|
342
|
-
// }
|
|
343
|
-
|
|
344
|
-
router.matchUrl("http://example.com/invalid");
|
|
345
|
-
// Returns: undefined
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
**Features:**
|
|
349
|
-
|
|
350
|
-
- Handles full URLs or paths
|
|
351
|
-
- Respects base path and hash settings
|
|
352
|
-
- Returns undefined for non-matching routes
|
|
353
|
-
|
|
354
|
-
### `router.replaceHistoryState(name, params?, title?)`
|
|
355
|
-
|
|
356
|
-
Updates browser URL without triggering navigation.
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
// Change URL without side effects
|
|
360
|
-
router.replaceHistoryState("products", { id: "456", filter: "new" });
|
|
361
|
-
// URL changes but no middleware/guards execute
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
**Use Cases:**
|
|
365
|
-
|
|
366
|
-
- Update URL after async data load
|
|
367
|
-
- Reflect UI state changes
|
|
368
|
-
- Correct invalid URLs
|
|
369
|
-
|
|
370
|
-
### `router.lastKnownState`
|
|
79
|
+
The plugin extends the router with browser-specific methods:
|
|
371
80
|
|
|
372
|
-
|
|
81
|
+
#### `router.buildUrl(name: string, params?: Params): string`
|
|
82
|
+
Build full URL with base path and hash prefix.\
|
|
83
|
+
`name: string` — route name\
|
|
84
|
+
`params?: Params` — route parameters\
|
|
85
|
+
Returns: `string` — full URL\
|
|
86
|
+
[Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin#5-router-interaction)
|
|
373
87
|
|
|
374
88
|
```typescript
|
|
375
89
|
const state = router.lastKnownState;
|
|
@@ -381,29 +95,11 @@ if (state) {
|
|
|
381
95
|
}
|
|
382
96
|
```
|
|
383
97
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
-
|
|
389
|
-
- Persists through errors/cancellations
|
|
390
|
-
|
|
391
|
-
## Browser Events
|
|
392
|
-
|
|
393
|
-
### Popstate Handling
|
|
394
|
-
|
|
395
|
-
The plugin synchronizes router state with browser navigation (back/forward buttons) through the `popstate` event.
|
|
396
|
-
|
|
397
|
-
**Behavior Guarantees:**
|
|
398
|
-
|
|
399
|
-
When users interact with browser controls, the plugin ensures:
|
|
400
|
-
|
|
401
|
-
- Router state stays synchronized with browser history
|
|
402
|
-
- URL bar always reflects the actual router state
|
|
403
|
-
- Rapid navigation (e.g., clicking back multiple times) is handled correctly
|
|
404
|
-
- No state corruption from concurrent transitions
|
|
405
|
-
|
|
406
|
-
**Example:**
|
|
98
|
+
#### `router.matchUrl(url: string): State | undefined`
|
|
99
|
+
Parse URL to router state.\
|
|
100
|
+
`url: string` — URL to parse\
|
|
101
|
+
Returns: `State | undefined`\
|
|
102
|
+
[Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin#5-router-interaction)
|
|
407
103
|
|
|
408
104
|
```typescript
|
|
409
105
|
router.navigate("page1");
|
|
@@ -415,123 +111,53 @@ router.navigate("page3");
|
|
|
415
111
|
// URL and router state remain synchronized
|
|
416
112
|
```
|
|
417
113
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
114
|
+
#### `router.replaceHistoryState(name: string, params?: Params, title?: string): void`
|
|
115
|
+
Update browser URL without triggering navigation.\
|
|
116
|
+
`name: string` — route name\
|
|
117
|
+
`params?: Params` — route parameters\
|
|
118
|
+
`title?: string` — page title\
|
|
119
|
+
Returns: `void`\
|
|
120
|
+
[Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin#5-router-interaction)
|
|
425
121
|
|
|
426
122
|
```typescript
|
|
427
|
-
|
|
428
|
-
name: "products",
|
|
429
|
-
params: { id: "123" },
|
|
430
|
-
path: "/products/123",
|
|
431
|
-
meta: {
|
|
432
|
-
id: 1,
|
|
433
|
-
params: {},
|
|
434
|
-
options: {},
|
|
435
|
-
redirected: false,
|
|
436
|
-
source: "popstate",
|
|
437
|
-
},
|
|
438
|
-
};
|
|
123
|
+
router.replaceHistoryState("users", { id: "456" });
|
|
439
124
|
```
|
|
440
125
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
```typescript
|
|
446
|
-
// Server-side rendering
|
|
447
|
-
import { browserPluginFactory } from "@real-router/browser-plugin";
|
|
448
|
-
|
|
449
|
-
const router = createRouter(routes);
|
|
450
|
-
router.usePlugin(browserPluginFactory());
|
|
451
|
-
|
|
452
|
-
// Methods return safe defaults
|
|
453
|
-
router.buildUrl("home"); // Works normally
|
|
454
|
-
router.matchUrl("/path"); // Returns undefined
|
|
455
|
-
router.start(); // No errors thrown
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
**Fallback Behavior:**
|
|
126
|
+
#### `router.lastKnownState: State | undefined`
|
|
127
|
+
Last successful navigation state (readonly).\
|
|
128
|
+
Returns: `State | undefined`\
|
|
129
|
+
[Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin#5-router-interaction)
|
|
459
130
|
|
|
460
|
-
|
|
461
|
-
- Single warning logged per session
|
|
462
|
-
- No errors thrown
|
|
463
|
-
- Router continues functioning
|
|
464
|
-
|
|
465
|
-
**Testing Support:**
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
// Custom browser implementation for tests
|
|
469
|
-
const mockBrowser: Browser = {
|
|
470
|
-
getBase: () => "/",
|
|
471
|
-
pushState: jest.fn(),
|
|
472
|
-
replaceState: jest.fn(),
|
|
473
|
-
addPopstateListener: () => () => {},
|
|
474
|
-
getLocation: (opts) => "/test",
|
|
475
|
-
getState: () => undefined,
|
|
476
|
-
getHash: () => "",
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
router.usePlugin(browserPluginFactory({}, mockBrowser));
|
|
480
|
-
```
|
|
131
|
+
---
|
|
481
132
|
|
|
482
133
|
## Usage Examples
|
|
483
134
|
|
|
484
|
-
###
|
|
135
|
+
### History Mode (default)
|
|
485
136
|
|
|
486
137
|
```typescript
|
|
487
|
-
const router = createRouter(routes);
|
|
488
|
-
|
|
489
138
|
router.usePlugin(
|
|
490
139
|
browserPluginFactory({
|
|
491
|
-
|
|
140
|
+
base: "/app",
|
|
492
141
|
preserveHash: true,
|
|
493
142
|
}),
|
|
494
143
|
);
|
|
495
144
|
|
|
496
|
-
router.
|
|
497
|
-
|
|
498
|
-
renderApp(state);
|
|
499
|
-
}
|
|
500
|
-
});
|
|
145
|
+
router.navigate("users", { id: "123" });
|
|
146
|
+
// URL: /app/users/123
|
|
501
147
|
```
|
|
502
148
|
|
|
503
|
-
###
|
|
504
|
-
|
|
505
|
-
```typescript
|
|
506
|
-
// Detect History API support
|
|
507
|
-
const supportsHistory = !!(window.history && window.history.pushState);
|
|
508
|
-
|
|
509
|
-
// Type-safe configuration based on browser support
|
|
510
|
-
router.usePlugin(
|
|
511
|
-
browserPluginFactory(
|
|
512
|
-
supportsHistory
|
|
513
|
-
? { useHash: false, preserveHash: true }
|
|
514
|
-
: { useHash: true, hashPrefix: "!" },
|
|
515
|
-
),
|
|
516
|
-
);
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### Analytics Integration
|
|
149
|
+
### Hash Mode
|
|
520
150
|
|
|
521
151
|
```typescript
|
|
522
152
|
router.usePlugin(
|
|
523
153
|
browserPluginFactory({
|
|
524
|
-
|
|
154
|
+
useHash: true,
|
|
155
|
+
hashPrefix: "!",
|
|
525
156
|
}),
|
|
526
157
|
);
|
|
527
158
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const analyticsData = history.state?.analytics;
|
|
531
|
-
if (analyticsData) {
|
|
532
|
-
trackPageView(analyticsData);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
159
|
+
router.navigate("users", { id: "123" });
|
|
160
|
+
// URL: #!/users/123
|
|
535
161
|
```
|
|
536
162
|
|
|
537
163
|
### Form Protection
|
|
@@ -543,422 +169,47 @@ router.usePlugin(
|
|
|
543
169
|
}),
|
|
544
170
|
);
|
|
545
171
|
|
|
546
|
-
router.canDeactivate("checkout", () => {
|
|
172
|
+
router.canDeactivate("checkout", () => (toState, fromState, done) => {
|
|
547
173
|
if (hasUnsavedChanges()) {
|
|
548
|
-
|
|
174
|
+
done({ error: new Error("Unsaved changes") });
|
|
175
|
+
} else {
|
|
176
|
+
done();
|
|
549
177
|
}
|
|
550
|
-
return true;
|
|
551
178
|
});
|
|
552
179
|
```
|
|
553
180
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
```typescript
|
|
557
|
-
// Preserve anchors during navigation
|
|
558
|
-
router.usePlugin(
|
|
559
|
-
browserPluginFactory({
|
|
560
|
-
preserveHash: true,
|
|
561
|
-
}),
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
// Navigate to section
|
|
565
|
-
window.location.hash = "#comments";
|
|
566
|
-
|
|
567
|
-
// Route change preserves hash
|
|
568
|
-
router.navigate("other-page");
|
|
569
|
-
// URL: /other-page#comments
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
## Advanced Features
|
|
573
|
-
|
|
574
|
-
### URL Encoding
|
|
575
|
-
|
|
576
|
-
The plugin handles special characters safely:
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
router.navigate("search", { q: "hello world" });
|
|
580
|
-
// URL: /search?q=hello%20world
|
|
581
|
-
|
|
582
|
-
router.navigate("category", { name: "日本語" });
|
|
583
|
-
// URL: /category?name=%E6%97%A5%E6%9C%AC%E8%AA%9E
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
**Features:**
|
|
587
|
-
|
|
588
|
-
- Automatic encoding/decoding
|
|
589
|
-
- Unicode support
|
|
590
|
-
- IPv6 URL compatibility
|
|
591
|
-
- Safe error handling
|
|
592
|
-
|
|
593
|
-
## Security
|
|
594
|
-
|
|
595
|
-
The plugin implements multiple layers of security to protect against common web vulnerabilities:
|
|
596
|
-
|
|
597
|
-
### URL Parameter Encoding
|
|
598
|
-
|
|
599
|
-
All route parameters are automatically URL-encoded by the underlying `route-tree` library, preventing XSS attacks through URL injection:
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
router.navigate("search", { q: '<script>alert("xss")</script>' });
|
|
603
|
-
// URL: /search?q=%3Cscript%3Ealert(%22xss%22)%3C%2Fscript%3E
|
|
604
|
-
// Script tags are encoded and harmless
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
### Protocol Whitelist
|
|
608
|
-
|
|
609
|
-
The `matchUrl` method only accepts `http:` and `https:` protocols, blocking dangerous protocols:
|
|
610
|
-
|
|
611
|
-
```typescript
|
|
612
|
-
// ✅ Allowed
|
|
613
|
-
router.matchUrl("https://example.com/path"); // Works
|
|
614
|
-
router.matchUrl("http://example.com/path"); // Works
|
|
615
|
-
|
|
616
|
-
// ❌ Blocked
|
|
617
|
-
router.matchUrl("javascript:alert(1)"); // Returns undefined
|
|
618
|
-
router.matchUrl("data:text/html,..."); // Returns undefined
|
|
619
|
-
router.matchUrl("vbscript:msgbox(1)"); // Returns undefined
|
|
620
|
-
router.matchUrl("file:///etc/passwd"); // Returns undefined
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
### State Validation
|
|
624
|
-
|
|
625
|
-
Popstate events are validated using type guards before processing. Invalid or malicious state structures are rejected:
|
|
626
|
-
|
|
627
|
-
```typescript
|
|
628
|
-
// Browser history manipulated by malicious code
|
|
629
|
-
history.pushState({ malicious: "data" }, "", "/");
|
|
630
|
-
|
|
631
|
-
// Plugin validates structure
|
|
632
|
-
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
633
|
-
// Invalid state is rejected, router remains in safe state
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
### Input Sanitization Limits
|
|
637
|
-
|
|
638
|
-
**What the plugin protects:**
|
|
639
|
-
|
|
640
|
-
- URL encoding for browser APIs (automatic)
|
|
641
|
-
- Protocol validation (whitelist)
|
|
642
|
-
- State structure validation (type guards)
|
|
643
|
-
- Prototype pollution prevention
|
|
644
|
-
|
|
645
|
-
**What the plugin does NOT protect:**
|
|
646
|
-
|
|
647
|
-
- XSS in template rendering (framework responsibility)
|
|
648
|
-
- SQL injection (backend responsibility)
|
|
649
|
-
- CSRF tokens (application responsibility)
|
|
650
|
-
|
|
651
|
-
**Best Practices:**
|
|
652
|
-
|
|
653
|
-
```typescript
|
|
654
|
-
// ✅ Safe: Let frameworks handle HTML escaping
|
|
655
|
-
<Link to={router.buildUrl('users', { id: userInput })} />
|
|
656
|
-
|
|
657
|
-
// ✅ Safe: DOM API automatically escapes
|
|
658
|
-
element.setAttribute('href', router.buildUrl('users', params));
|
|
659
|
-
|
|
660
|
-
// ❌ Unsafe: Manual HTML construction
|
|
661
|
-
element.innerHTML = `<a href="${router.buildUrl('users', params)}">...</a>`;
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
## Error Handling
|
|
665
|
-
|
|
666
|
-
### Navigation Errors
|
|
667
|
-
|
|
668
|
-
When navigation is blocked by a `canDeactivate` guard during browser-initiated navigation (and `forceDeactivate` is `false`), the plugin detects the `CANNOT_DEACTIVATE` error and automatically restores the browser URL to match the current router state. This ensures the URL bar stays synchronized even when navigation is prevented.
|
|
669
|
-
|
|
670
|
-
### Recovery Mechanism
|
|
671
|
-
|
|
672
|
-
The plugin includes a robust error recovery system for critical failures. If an unexpected error occurs during popstate event handling, the plugin first logs the error with full context for debugging. It then attempts to recover by synchronizing the browser's history state with the current router state. If this recovery also fails, a secondary error is logged, but the application continues running to prevent complete failure.
|
|
673
|
-
|
|
674
|
-
### Missing State
|
|
675
|
-
|
|
676
|
-
When the browser's history state is null, corrupted, or doesn't match the expected structure, the plugin handles it gracefully. It attempts to match the current URL against the router's routes to reconstruct the state. If a default route is configured and no match is found, it navigates to the default route. Invalid states are logged as warnings to aid debugging without interrupting the user experience.
|
|
677
|
-
|
|
678
|
-
## Browser Compatibility
|
|
679
|
-
|
|
680
|
-
### Modern Browsers (Full Support)
|
|
681
|
-
|
|
682
|
-
Chrome 5+, Firefox 4+, Safari 5+, Edge, Opera 11.5+
|
|
683
|
-
|
|
684
|
-
- HTML5 History API
|
|
685
|
-
- Popstate events
|
|
686
|
-
- Unicode URLs
|
|
687
|
-
- Performance optimizations
|
|
688
|
-
|
|
689
|
-
### Legacy Browsers (Hash Mode)
|
|
690
|
-
|
|
691
|
-
IE 9-11, Older mobile browsers
|
|
692
|
-
|
|
693
|
-
```typescript
|
|
694
|
-
// Automatic fallback for IE
|
|
695
|
-
router.usePlugin(
|
|
696
|
-
browserPluginFactory({
|
|
697
|
-
useHash: true, // Works everywhere
|
|
698
|
-
}),
|
|
699
|
-
);
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
**IE-Specific Handling:**
|
|
703
|
-
|
|
704
|
-
- Detects Trident engine
|
|
705
|
-
- Uses hashchange events
|
|
706
|
-
- No popstate on hash changes
|
|
707
|
-
|
|
708
|
-
### Server-Side Rendering
|
|
709
|
-
|
|
710
|
-
Node.js 14+
|
|
711
|
-
|
|
712
|
-
- Automatic fallback
|
|
713
|
-
- No errors thrown
|
|
714
|
-
- Warning logged once
|
|
715
|
-
- Methods return safe defaults
|
|
716
|
-
|
|
717
|
-
## TypeScript
|
|
718
|
-
|
|
719
|
-
The plugin is fully typed with TypeScript:
|
|
720
|
-
|
|
721
|
-
```typescript
|
|
722
|
-
import {
|
|
723
|
-
browserPluginFactory,
|
|
724
|
-
type BrowserPluginOptions,
|
|
725
|
-
type Browser,
|
|
726
|
-
type HistoryState,
|
|
727
|
-
type StartRouterArguments,
|
|
728
|
-
isState,
|
|
729
|
-
isHistoryState,
|
|
730
|
-
} from "@real-router/browser-plugin";
|
|
731
|
-
|
|
732
|
-
// Type-safe configuration
|
|
733
|
-
const options: BrowserPluginOptions = {
|
|
734
|
-
useHash: false,
|
|
735
|
-
base: "/app",
|
|
736
|
-
mergeState: true,
|
|
737
|
-
};
|
|
738
|
-
|
|
739
|
-
// Router interface is augmented automatically
|
|
740
|
-
router.usePlugin(browserPluginFactory(options));
|
|
741
|
-
router.buildUrl("home"); // TypeScript knows this method exists
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
### Type Safety & Runtime Protection
|
|
745
|
-
|
|
746
|
-
The plugin uses multiple layers of protection to prevent configuration conflicts:
|
|
747
|
-
|
|
748
|
-
#### 1. Compile-Time Safety (TypeScript)
|
|
181
|
+
---
|
|
749
182
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
```typescript
|
|
753
|
-
// ✅ Valid configurations
|
|
754
|
-
const config1: BrowserPluginOptions = {
|
|
755
|
-
useHash: true,
|
|
756
|
-
hashPrefix: "!",
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
const config2: BrowserPluginOptions = {
|
|
760
|
-
useHash: false,
|
|
761
|
-
preserveHash: true,
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
// ❌ TypeScript errors - conflicting options
|
|
765
|
-
const invalid1: BrowserPluginOptions = {
|
|
766
|
-
useHash: true,
|
|
767
|
-
preserveHash: true, // Error: Type 'true' is not assignable to type 'never'
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
const invalid2: BrowserPluginOptions = {
|
|
771
|
-
useHash: false,
|
|
772
|
-
hashPrefix: "!", // Error: Type 'string' is not assignable to type 'never'
|
|
773
|
-
};
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
#### 2. Runtime Validation (JavaScript)
|
|
777
|
-
|
|
778
|
-
For JavaScript users or dynamic configurations, the plugin validates options and warns about conflicts:
|
|
779
|
-
|
|
780
|
-
```javascript
|
|
781
|
-
// JavaScript - no compile-time checks
|
|
782
|
-
browserPluginFactory({
|
|
783
|
-
useHash: true,
|
|
784
|
-
preserveHash: true, // Conflict!
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
// Console warning:
|
|
788
|
-
// [browser-plugin] preserveHash ignored in hash mode
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
```javascript
|
|
792
|
-
browserPluginFactory({
|
|
793
|
-
useHash: false,
|
|
794
|
-
hashPrefix: "!", // Conflict!
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// Console warning:
|
|
798
|
-
// [browser-plugin] hashPrefix ignored in history mode
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
#### 3. Physical Property Removal
|
|
802
|
-
|
|
803
|
-
After validation, conflicting properties are physically deleted from the options object, ensuring clean configuration:
|
|
804
|
-
|
|
805
|
-
```javascript
|
|
806
|
-
// User passes conflicting options
|
|
807
|
-
const options = { useHash: true, preserveHash: true, hashPrefix: "!" };
|
|
808
|
-
browserPluginFactory(options);
|
|
809
|
-
|
|
810
|
-
// Inside plugin:
|
|
811
|
-
// - preserveHash is DELETED (delete options.preserveHash)
|
|
812
|
-
// - hashPrefix is KEPT (needed for hash mode)
|
|
813
|
-
// → Impossible to accidentally use preserveHash in hash mode
|
|
814
|
-
```
|
|
815
|
-
|
|
816
|
-
**Result:**
|
|
817
|
-
|
|
818
|
-
- ✅ TypeScript users: compile-time errors prevent invalid configs
|
|
819
|
-
- ✅ JavaScript users: runtime warnings + automatic cleanup
|
|
820
|
-
- ✅ Internal code: guaranteed clean options without conflicts
|
|
821
|
-
|
|
822
|
-
## Common Issues
|
|
823
|
-
|
|
824
|
-
### URL Not Updating
|
|
825
|
-
|
|
826
|
-
```typescript
|
|
827
|
-
// Ensure plugin is registered before start
|
|
828
|
-
router.usePlugin(browserPluginFactory()); // First
|
|
829
|
-
router.start(); // Second
|
|
830
|
-
```
|
|
183
|
+
## SSR Support
|
|
831
184
|
|
|
832
|
-
|
|
185
|
+
The plugin is SSR-safe with automatic fallback:
|
|
833
186
|
|
|
834
187
|
```typescript
|
|
835
|
-
//
|
|
188
|
+
// Server-side — no errors, methods return safe defaults
|
|
836
189
|
router.usePlugin(browserPluginFactory());
|
|
837
|
-
|
|
838
|
-
//
|
|
839
|
-
router.usePlugin(
|
|
840
|
-
browserPluginFactory({
|
|
841
|
-
forceDeactivate: false, // Allow guards to prevent back button
|
|
842
|
-
}),
|
|
843
|
-
);
|
|
844
|
-
|
|
845
|
-
// Check browser console for errors
|
|
846
|
-
// Verify history.state is valid
|
|
847
|
-
console.log(history.state);
|
|
190
|
+
router.buildUrl("home"); // Works
|
|
191
|
+
router.matchUrl("/path"); // Returns undefined
|
|
848
192
|
```
|
|
849
193
|
|
|
850
|
-
|
|
194
|
+
---
|
|
851
195
|
|
|
852
|
-
|
|
853
|
-
// Enable preserveHash
|
|
854
|
-
router.usePlugin(
|
|
855
|
-
browserPluginFactory({
|
|
856
|
-
preserveHash: true,
|
|
857
|
-
useHash: false, // Only works in history mode
|
|
858
|
-
}),
|
|
859
|
-
);
|
|
860
|
-
```
|
|
196
|
+
## Documentation
|
|
861
197
|
|
|
862
|
-
|
|
198
|
+
Full documentation available on the [Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin):
|
|
863
199
|
|
|
864
|
-
|
|
200
|
+
- [Configuration Options](https://github.com/greydragon888/real-router/wiki/browser-plugin#3-configuration-options)
|
|
201
|
+
- [Lifecycle Hooks](https://github.com/greydragon888/real-router/wiki/browser-plugin#4-lifecycle-hooks)
|
|
202
|
+
- [Router Methods](https://github.com/greydragon888/real-router/wiki/browser-plugin#5-router-interaction)
|
|
203
|
+
- [Behavior & Edge Cases](https://github.com/greydragon888/real-router/wiki/browser-plugin#8-behavior)
|
|
204
|
+
- [Migration from router5](https://github.com/greydragon888/real-router/wiki/browser-plugin#11-migration-from-router5)
|
|
865
205
|
|
|
866
|
-
|
|
867
|
-
// ❌ Wrong: preserveHash doesn't work with hash mode
|
|
868
|
-
router.usePlugin(
|
|
869
|
-
browserPluginFactory({
|
|
870
|
-
useHash: true,
|
|
871
|
-
preserveHash: true, // Warning: preserveHash ignored in hash mode
|
|
872
|
-
}),
|
|
873
|
-
);
|
|
874
|
-
|
|
875
|
-
// ✅ Correct: preserveHash only works in history mode
|
|
876
|
-
router.usePlugin(
|
|
877
|
-
browserPluginFactory({
|
|
878
|
-
useHash: false,
|
|
879
|
-
preserveHash: true,
|
|
880
|
-
}),
|
|
881
|
-
);
|
|
882
|
-
|
|
883
|
-
// ❌ Wrong: hashPrefix doesn't work with history mode
|
|
884
|
-
router.usePlugin(
|
|
885
|
-
browserPluginFactory({
|
|
886
|
-
useHash: false,
|
|
887
|
-
hashPrefix: "!", // Warning: hashPrefix ignored in history mode
|
|
888
|
-
}),
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
// ✅ Correct: hashPrefix only works in hash mode
|
|
892
|
-
router.usePlugin(
|
|
893
|
-
browserPluginFactory({
|
|
894
|
-
useHash: true,
|
|
895
|
-
hashPrefix: "!",
|
|
896
|
-
}),
|
|
897
|
-
);
|
|
898
|
-
```
|
|
206
|
+
---
|
|
899
207
|
|
|
900
208
|
## Related Packages
|
|
901
209
|
|
|
902
210
|
- [@real-router/core](https://www.npmjs.com/package/@real-router/core) — Core router
|
|
903
211
|
- [@real-router/react](https://www.npmjs.com/package/@real-router/react) — React integration
|
|
904
|
-
|
|
905
|
-
## Migration from router5-plugin-browser
|
|
906
|
-
|
|
907
|
-
### Import Changes
|
|
908
|
-
|
|
909
|
-
```diff
|
|
910
|
-
- import browserPlugin from 'router5-plugin-browser';
|
|
911
|
-
+ import { browserPluginFactory } from '@real-router/browser-plugin';
|
|
912
|
-
|
|
913
|
-
- router.usePlugin(browserPlugin({ useHash: true }));
|
|
914
|
-
+ router.usePlugin(browserPluginFactory({ useHash: true }));
|
|
915
|
-
```
|
|
916
|
-
|
|
917
|
-
### Type-Safe Options
|
|
918
|
-
|
|
919
|
-
Real-Router uses discriminated union types to prevent invalid option combinations:
|
|
920
|
-
|
|
921
|
-
```typescript
|
|
922
|
-
// ✅ Valid: hash mode with prefix
|
|
923
|
-
browserPluginFactory({ useHash: true, hashPrefix: "!" });
|
|
924
|
-
|
|
925
|
-
// ✅ Valid: history mode with hash preservation
|
|
926
|
-
browserPluginFactory({ useHash: false, preserveHash: true });
|
|
927
|
-
|
|
928
|
-
// ❌ TypeScript error: hashPrefix only works with useHash: true
|
|
929
|
-
browserPluginFactory({ useHash: false, hashPrefix: "!" });
|
|
930
|
-
|
|
931
|
-
// ❌ TypeScript error: preserveHash only works with useHash: false
|
|
932
|
-
browserPluginFactory({ useHash: true, preserveHash: true });
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
### API Changes
|
|
936
|
-
|
|
937
|
-
| Method | router5 | Real-Router |
|
|
938
|
-
|--------|---------|---------|
|
|
939
|
-
| `matchUrl()` | returns `State \| null` | returns `State \| undefined` |
|
|
940
|
-
|
|
941
|
-
```diff
|
|
942
|
-
const state = router.matchUrl('/users/123');
|
|
943
|
-
- if (state === null) {
|
|
944
|
-
+ if (state === undefined) {
|
|
945
|
-
// not found
|
|
946
|
-
}
|
|
947
|
-
```
|
|
948
|
-
|
|
949
|
-
### Full Migration Example
|
|
950
|
-
|
|
951
|
-
```diff
|
|
952
|
-
- import { createRouter } from 'router5';
|
|
953
|
-
- import browserPlugin from 'router5-plugin-browser';
|
|
954
|
-
+ import { createRouter } from '@real-router/core';
|
|
955
|
-
+ import { browserPluginFactory } from '@real-router/browser-plugin';
|
|
956
|
-
|
|
957
|
-
const router = createRouter(routes);
|
|
958
|
-
- router.usePlugin(browserPlugin({ useHash: false, preserveHash: true }));
|
|
959
|
-
+ router.usePlugin(browserPluginFactory({ useHash: false, preserveHash: true }));
|
|
960
|
-
router.start();
|
|
961
|
-
```
|
|
212
|
+
- [@real-router/logger-plugin](https://www.npmjs.com/package/@real-router/logger-plugin) — Debug logging
|
|
962
213
|
|
|
963
214
|
## License
|
|
964
215
|
|