@real-router/browser-plugin 0.1.1 → 0.1.3

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 CHANGED
@@ -3,18 +3,7 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
5
5
 
6
- A Real-Router plugin that synchronizes router state with browser history, enabling browser navigation buttons (back/forward) and URL management for single-page applications.
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
- ## API
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
- Adds prefix to hash routes. **Only works with `useHash: true`** (hash mode).
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
- **Common Prefixes:**
177
-
178
- - `"!"` - Hashbang URLs for SEO (Google's deprecated AJAX crawling)
179
- - `"/"` - Clear visual separation
180
- - Custom app identifiers
181
-
182
- **Note:** Using `hashPrefix` with `useHash: false` will log a warning and the option will be ignored.
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
- - `true` **(default)** - keep hash fragment
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
- **Use Cases:**
73
+ See [Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin#3-configuration-options) for detailed option descriptions and examples.
281
74
 
282
- - Anchor navigation within pages
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 instance with browser-specific methods:
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
- Read-only reference to last successful navigation state.
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
- **Characteristics:**
385
-
386
- - Immutable (frozen object)
387
- - Updated on successful navigation only
388
- - Undefined before first navigation
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
- **Error Recovery:**
419
-
420
- If an error occurs during navigation, the plugin automatically synchronizes the URL with the current router state, ensuring the URL bar never displays incorrect information.
421
-
422
- ### History State Structure
423
-
424
- The plugin stores router state in browser history:
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
- history.state = {
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
- ## SSR Support
442
-
443
- The plugin works in server environments with automatic fallback:
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
- - Browser methods become no-ops
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
- ### Basic SPA Setup
135
+ ### History Mode (default)
485
136
 
486
137
  ```typescript
487
- const router = createRouter(routes);
488
-
489
138
  router.usePlugin(
490
139
  browserPluginFactory({
491
- useHash: false,
140
+ base: "/app",
492
141
  preserveHash: true,
493
142
  }),
494
143
  );
495
144
 
496
- router.start((err, state) => {
497
- if (!err) {
498
- renderApp(state);
499
- }
500
- });
145
+ router.navigate("users", { id: "123" });
146
+ // URL: /app/users/123
501
147
  ```
502
148
 
503
- ### Legacy Browser Support
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
- mergeState: true,
154
+ useHash: true,
155
+ hashPrefix: "!",
525
156
  }),
526
157
  );
527
158
 
528
- // Analytics library can access its data
529
- router.subscribe(({ route }) => {
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
- return window.confirm("Leave without saving?");
174
+ done({ error: new Error("Unsaved changes") });
175
+ } else {
176
+ done();
549
177
  }
550
- return true;
551
178
  });
552
179
  ```
553
180
 
554
- ### Hash Fragment Navigation
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
- Discriminated union types make invalid configurations impossible:
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
- ### Back Button Not Working
185
+ The plugin is SSR-safe with automatic fallback:
833
186
 
834
187
  ```typescript
835
- // Ensure plugin is registered
188
+ // Server-side no errors, methods return safe defaults
836
189
  router.usePlugin(browserPluginFactory());
837
-
838
- // If you need guards to block navigation:
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
- ### Hash Fragment Lost
194
+ ---
851
195
 
852
- ```typescript
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
- ### Conflicting Options Warning
198
+ Full documentation available on the [Wiki](https://github.com/greydragon888/real-router/wiki/browser-plugin):
863
199
 
864
- If you see warnings about ignored options, check your configuration:
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
- ```typescript
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