@real-router/browser-plugin 0.1.0

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 ADDED
@@ -0,0 +1,965 @@
1
+ # @real-router/browser-plugin
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
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.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @real-router/browser-plugin
23
+ # or
24
+ pnpm add @real-router/browser-plugin
25
+ # or
26
+ yarn add @real-router/browser-plugin
27
+ # or
28
+ bun add @real-router/browser-plugin
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { createRouter } from "@real-router/core";
35
+ import { browserPluginFactory } from "@real-router/browser-plugin";
36
+
37
+ const router = createRouter([
38
+ { name: "home", path: "/" },
39
+ { name: "products", path: "/products/:id" },
40
+ { name: "cart", path: "/cart" },
41
+ ]);
42
+
43
+ // Basic usage
44
+ router.usePlugin(browserPluginFactory());
45
+
46
+ // With options
47
+ router.usePlugin(
48
+ browserPluginFactory({
49
+ useHash: false,
50
+ base: "/app",
51
+ }),
52
+ );
53
+
54
+ router.start();
55
+ ```
56
+
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`
156
+
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:**
163
+
164
+ ```typescript
165
+ router.usePlugin(
166
+ browserPluginFactory({
167
+ useHash: true, // Required for hashPrefix
168
+ hashPrefix: "!",
169
+ }),
170
+ );
171
+
172
+ router.navigate("products", { id: "123" });
173
+ // URL: http://example.com/#!/products/123
174
+ ```
175
+
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).
251
+
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
+ ```
279
+
280
+ **Use Cases:**
281
+
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.
287
+
288
+ ## Added Router Methods
289
+
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`
371
+
372
+ Read-only reference to last successful navigation state.
373
+
374
+ ```typescript
375
+ const state = router.lastKnownState;
376
+ // Returns frozen copy of state or undefined
377
+
378
+ if (state) {
379
+ console.log("Last route:", state.name);
380
+ console.log("Parameters:", state.params);
381
+ }
382
+ ```
383
+
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:**
407
+
408
+ ```typescript
409
+ router.navigate("page1");
410
+ router.navigate("page2");
411
+ router.navigate("page3");
412
+
413
+ // User clicks back twice rapidly
414
+ // Plugin ensures router ends at page1
415
+ // URL and router state remain synchronized
416
+ ```
417
+
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:
425
+
426
+ ```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
+ };
439
+ ```
440
+
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:**
459
+
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
+ ```
481
+
482
+ ## Usage Examples
483
+
484
+ ### Basic SPA Setup
485
+
486
+ ```typescript
487
+ const router = createRouter(routes);
488
+
489
+ router.usePlugin(
490
+ browserPluginFactory({
491
+ useHash: false,
492
+ preserveHash: true,
493
+ }),
494
+ );
495
+
496
+ router.start((err, state) => {
497
+ if (!err) {
498
+ renderApp(state);
499
+ }
500
+ });
501
+ ```
502
+
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
520
+
521
+ ```typescript
522
+ router.usePlugin(
523
+ browserPluginFactory({
524
+ mergeState: true,
525
+ }),
526
+ );
527
+
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
+ });
535
+ ```
536
+
537
+ ### Form Protection
538
+
539
+ ```typescript
540
+ router.usePlugin(
541
+ browserPluginFactory({
542
+ forceDeactivate: false,
543
+ }),
544
+ );
545
+
546
+ router.canDeactivate("checkout", () => {
547
+ if (hasUnsavedChanges()) {
548
+ return window.confirm("Leave without saving?");
549
+ }
550
+ return true;
551
+ });
552
+ ```
553
+
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)
749
+
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
+ ```
831
+
832
+ ### Back Button Not Working
833
+
834
+ ```typescript
835
+ // Ensure plugin is registered
836
+ 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);
848
+ ```
849
+
850
+ ### Hash Fragment Lost
851
+
852
+ ```typescript
853
+ // Enable preserveHash
854
+ router.usePlugin(
855
+ browserPluginFactory({
856
+ preserveHash: true,
857
+ useHash: false, // Only works in history mode
858
+ }),
859
+ );
860
+ ```
861
+
862
+ ### Conflicting Options Warning
863
+
864
+ If you see warnings about ignored options, check your configuration:
865
+
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
+ ```
899
+
900
+ ## Related Packages
901
+
902
+ - [@real-router/core](https://www.npmjs.com/package/@real-router/core) — Core router
903
+ - [@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
+ ```
962
+
963
+ ## License
964
+
965
+ MIT © [Oleg Ivanov](https://github.com/greydragon888)