@xmachines/play-solid-router 1.0.0-beta.5 → 1.0.0-beta.50

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
@@ -1,637 +1,158 @@
1
- # @xmachines/play-solid-router
2
-
3
- **SolidJS Router adapter for XMachines Universal Player Architecture**
4
-
5
- SolidJS Router adapter using `RouterBridgeBase` for consistent actor↔router sync.
6
-
7
- ## Overview
8
-
9
- `@xmachines/play-solid-router` provides seamless integration between SolidJS Router and XMachines state machines. Built on Solid's reactive primitives, it enables zero-adaptation signals synchronization.
10
-
11
- Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
1
+ <!-- generated-by: gsd-doc-writer -->
12
2
 
13
- - **Actor Authority (INV-01):** State machine controls navigation, router reflects decisions
14
- - **Passive Infrastructure (INV-04):** Router observes `actor.currentRoute` signal
15
- - **Signal-Only Reactivity (INV-05):** TC39 watcher lifecycle + Solid reactive owner integration
16
-
17
- **Key Benefits:**
18
-
19
- - **Bridge-first:** Extends shared `RouterBridgeBase` policy used by other adapters
20
- - **Automatic tracking:** Uses Solid reactivity for router→actor while base class handles actor→router watcher lifecycle
21
- - **Fine-grained reactivity:** Updates only affected components
22
- - **Logic-driven navigation:** Business logic in state machines, not components
23
- - **Type-safe parameters:** Route params flow through state machine context
3
+ # @xmachines/play-solid-router
24
4
 
25
- **Framework Compatibility:**
5
+ SolidJS Router adapter for the XMachines Universal Player Architecture. Provides bidirectional synchronisation between a `PlayerActor`'s state machine routes and the browser URL via `@solidjs/router`.
26
6
 
27
- - SolidJS 1.8.4+ (signals-native architecture)
28
- - @solidjs/router 0.13.0+ (modern routing primitives)
29
- - TC39 Signals polyfill integration
7
+ Part of the [xmachines-js monorepo](../../README.md).
30
8
 
31
9
  ## Installation
32
10
 
33
11
  ```bash
34
- npm install @solidjs/router@^0.13.0 solid-js@^1.8.0 @xmachines/play-solid-router @xmachines/play-solid
12
+ npm install @xmachines/play-solid-router
35
13
  ```
36
14
 
37
- **Peer dependencies:**
15
+ **Peer dependencies** (must be installed separately):
38
16
 
39
- - `@solidjs/router` ^0.13.0 - SolidJS Router library
40
- - `solid-js` ^1.8.0 - SolidJS runtime
41
- - `@xmachines/play-solid` - Solid renderer (`PlayRenderer`)
42
- - `@xmachines/play-actor` - Actor base
43
- - `@xmachines/play-router` - Route extraction
44
- - `@xmachines/play-signals` - TC39 Signals polyfill
17
+ ```bash
18
+ npm install solid-js @solidjs/router xstate
19
+ ```
45
20
 
46
21
  ## Quick Start
47
22
 
48
- ```typescript
49
- import { Router, Route, useNavigate, useLocation, useParams } from '@solidjs/router';
50
- import { onCleanup } from 'solid-js';
51
- import { SolidRouterBridge, createRouteMap } from '@xmachines/play-solid-router';
52
- import { definePlayer } from '@xmachines/play-xstate';
53
-
54
- function App() {
55
- // 1. Get SolidJS Router hooks (MUST be inside component)
56
- const navigate = useNavigate();
57
- const location = useLocation();
58
- const params = useParams();
59
-
60
- // 2. Create route mapping from machine routes
61
- const routeMap = createRouteMap(authMachine);
62
-
63
- // 3. Create player with state machine
64
- const createPlayer = definePlayer({
65
- machine: authMachine,
66
- catalog: componentCatalog
67
- });
68
- const actor = createPlayer();
69
- actor.start();
70
-
71
- // 4. Create bridge to sync actor and router
72
- const bridge = new SolidRouterBridge(
73
- navigate,
74
- location,
75
- params,
76
- actor,
77
- routeMap
78
- );
79
-
80
- // 5. Start synchronization
81
- bridge.connect();
82
-
83
- // 6. Cleanup on component disposal
84
- onCleanup(() => {
85
- bridge.disconnect();
86
- });
87
-
88
- return (
89
- <Router>
90
- <Route path="/" component={HomeView} />
91
- <Route path="/profile/:userId" component={ProfileView} />
92
- <Route path="/settings/:section?" component={SettingsView} />
93
- </Router>
94
- );
95
- }
96
- ```
23
+ ```tsx
24
+ import { Router, Route, useNavigate, useLocation, useParams } from "@solidjs/router";
25
+ import { onCleanup, type ParentComponent } from "solid-js";
26
+ import { PlayRouterProvider, createRouteMap } from "@xmachines/play-solid-router";
27
+ import { definePlayer } from "@xmachines/play-xstate";
28
+ import { myMachine } from "./machine.js";
97
29
 
98
- ## API Reference
30
+ const actor = definePlayer({ machine: myMachine })();
31
+ actor.start();
99
32
 
100
- ### `SolidRouterBridge`
33
+ const routeMap = createRouteMap(myMachine);
101
34
 
102
- Router adapter implementing the `RouterBridge` protocol for SolidJS Router.
35
+ const Layout: ParentComponent = () => {
36
+ const navigate = useNavigate();
37
+ const location = useLocation();
38
+ const params = useParams();
103
39
 
104
- **Type Signature:**
40
+ onCleanup(() => actor.stop());
105
41
 
106
- ```typescript
107
- class SolidRouterBridge {
108
- constructor(
109
- navigate: ReturnType<typeof useNavigate>,
110
- location: ReturnType<typeof useLocation>,
111
- params: ReturnType<typeof useParams>,
112
- actor: AbstractActor<any>,
113
- routeMap: RouteMap,
42
+ return (
43
+ <PlayRouterProvider
44
+ actor={actor}
45
+ routeMap={routeMap}
46
+ router={{ navigate, location, params }}
47
+ renderer={(a) => <MyApp actor={a} />}
48
+ />
114
49
  );
115
- dispose(): void;
116
- }
117
- ```
118
-
119
- **Constructor Parameters:**
120
-
121
- - `navigate` - Function from `useNavigate()` hook (signals-aware navigation)
122
- - `location` - Object from `useLocation()` hook (reactive pathname, search, hash)
123
- - `params` - Object from `useParams()` hook (reactive route parameters)
124
- - `actor` - XMachines actor instance (from `definePlayer().actor`)
125
- - `routeMap` - Bidirectional state ID ↔ path mapping
126
-
127
- **Methods:**
128
-
129
- - `connect()` - Start bidirectional synchronization.
130
- - `disconnect()` - Stop synchronization and cleanup bridge resources.
131
- - `dispose()` - Alias of `disconnect()`.
132
-
133
- **Internal Behavior:**
134
-
135
- - Uses `RouterBridgeBase` TC39 watcher lifecycle for actor→router synchronization
136
- - Updates SolidJS Router via `navigate(path)` when actor state changes
137
- - Uses `createEffect(on(...))` to watch `location.pathname` signal
138
- - Sends `play.route` events to actor when user navigates
139
- - Prevents circular updates with path tracking and processing flags
140
-
141
- ### `RouteMap`
142
-
143
- Bidirectional mapping between XMachines state IDs and SolidJS Router paths with pattern matching support.
144
-
145
- **Type Signature:**
146
-
147
- ```typescript
148
- interface RouteMapping {
149
- stateId: string;
150
- path: string;
151
- }
152
-
153
- class RouteMap {
154
- constructor(mappings: RouteMapping[]);
155
- getPath(stateId: string, params?: Record<string, string>): string | undefined;
156
- getStateIdByPath(path: string): string | undefined;
157
- }
158
- ```
159
-
160
- **Constructor Parameters:**
161
-
162
- - `mappings` - Array of mapping objects with:
163
- - `stateId` - State machine state ID (e.g., `'#profile'`)
164
- - `path` - SolidJS Router path pattern (e.g., `'/profile/:userId'`)
165
-
166
- **Methods:**
167
-
168
- - `getPath(stateId, params?)` - Find path from state ID, optionally substitute params
169
- - `getStateIdByPath(path)` - Find state ID from path with pattern matching (supports `:param` and `:param?` syntax)
170
-
171
- **Pattern Matching:**
172
-
173
- Uses URLPattern API for robust dynamic route matching:
174
-
175
- ```typescript
176
- const routeMap = new RouteMap([{ stateId: "#settings", path: "/settings/:section?" }]);
177
-
178
- routeMap.getStateIdByPath("/settings"); // '#settings'
179
- routeMap.getStateIdByPath("/settings/account"); // '#settings'
180
- routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
181
- ```
182
-
183
- ## Examples
184
-
185
- ### Basic Usage: Simple 2-3 Route Setup
186
-
187
- ```typescript
188
- import { Router, Route } from '@solidjs/router';
189
- import { createSignal } from 'solid-js';
190
- import { defineCatalog } from '@xmachines/play-catalog';
191
-
192
- // State machine with 3 states
193
- const appMachine = setup({
194
- types: {
195
- events: {} as PlayRouteEvent
196
- }
197
- }).createMachine({
198
- id: 'app',
199
- initial: 'home',
200
- states: {
201
- home: {
202
- meta: { route: '/', view: { component: 'Home' } }
203
- },
204
- about: {
205
- meta: { route: '/about', view: { component: 'About' } }
206
- },
207
- contact: {
208
- meta: { route: '/contact', view: { component: 'Contact' } }
209
- }
210
- }
211
- });
212
-
213
- const catalog = defineCatalog({
214
- Home,
215
- About,
216
- Contact,
217
- });
218
-
219
- // Component setup
220
- function App() {
221
- const navigate = useNavigate();
222
- const location = useLocation();
223
- const params = useParams();
224
-
225
- const routeMap = createRouteMap(appMachine);
226
-
227
- const createPlayer = definePlayer({ machine: appMachine, catalog });
228
- const actor = createPlayer();
229
- actor.start();
230
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
231
-
232
- onCleanup(() => bridge.dispose());
233
-
234
- return (
235
- <Router>
236
- <Route path="/" component={Home} />
237
- <Route path="/about" component={About} />
238
- <Route path="/contact" component={Contact} />
239
- </Router>
240
- );
241
- }
242
- ```
243
-
244
- ### Parameter Handling: Dynamic Routes with `:param` Syntax
245
-
246
- ```typescript
247
- // State machine with parameter routes
248
- import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
249
- import { defineCatalog } from "@xmachines/play-catalog";
250
-
251
- const machineConfig = {
252
- id: 'app',
253
- context: {},
254
- states: {
255
- profile: {
256
- meta: {
257
- route: '/profile/:userId',
258
- view: { component: 'Profile' },
259
- },
260
- },
261
- settings: {
262
- meta: {
263
- route: '/settings/:section?',
264
- view: { component: 'Settings' },
265
- },
266
- }
267
- }
268
50
  };
269
51
 
270
- const appMachine = setup({
271
- types: {
272
- context: {} as { userId?: string; section?: string },
273
- events: {} as PlayRouteEvent
274
- }
275
- }).createMachine(formatPlayRouteTransitions(machineConfig));
276
-
277
- const catalog = defineCatalog({
278
- Profile,
279
- Settings,
280
- });
281
-
282
- // Router with dynamic routes
283
- function App() {
284
- const navigate = useNavigate();
285
- const location = useLocation();
286
- const params = useParams();
287
-
288
- const routeMap = createRouteMap(appMachine);
289
-
290
- const createPlayer = definePlayer({ machine: appMachine, catalog });
291
- const actor = createPlayer();
292
- actor.start();
293
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
294
-
295
- onCleanup(() => bridge.dispose());
296
-
297
- return (
298
- <Router>
299
- <Route path="/profile/:userId" component={Profile} />
300
- <Route path="/settings/:section?" component={Settings} />
301
- </Router>
302
- );
52
+ export default function App() {
53
+ return <Router root={Layout}>{/* one <Route> per routable state */}</Router>;
303
54
  }
304
55
  ```
305
56
 
306
- **Usage in component:**
57
+ ## API Summary
307
58
 
308
- ```tsx
309
- function ProfileButton(props: { userId: string }) {
310
- return (
311
- <button
312
- onClick={() =>
313
- props.actor.send({
314
- type: "play.route",
315
- to: "#profile",
316
- params: { userId: props.userId },
317
- })
318
- }
319
- >
320
- View Profile
321
- </button>
322
- );
323
- }
324
- ```
59
+ ### `PlayRouterProvider`
325
60
 
326
- ### Query Parameters: Search/Filters via Query Strings
327
-
328
- ```typescript
329
- // State machine with query param handling
330
- import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
331
- import { defineCatalog } from "@xmachines/play-catalog";
332
-
333
- const machineConfig = {
334
- context: { query: '', filters: {} },
335
- states: {
336
- search: {
337
- meta: {
338
- route: '/search',
339
- view: { component: 'Search' },
340
- },
341
- }
342
- }
343
- };
61
+ A SolidJS component that wires a `PlayerActor` to Solid Router. It creates and connects a `SolidRouterBridge` on mount and disconnects it via `onCleanup` on unmount.
344
62
 
345
- const searchMachine = setup({
346
- types: {
347
- context: {} as { query: string; filters: Record<string, string> },
348
- events: {} as PlayRouteEvent
349
- }
350
- }).createMachine(formatPlayRouteTransitions(machineConfig));
351
-
352
- const catalog = defineCatalog({
353
- Search,
354
- });
355
-
356
- const player = definePlayer({ machine: searchMachine, catalog });
357
-
358
- // Component sends query params
359
- function SearchBar(props) {
360
- const [searchTerm, setSearchTerm] = createSignal('');
361
-
362
- function handleSearch() {
363
- props.actor.send({
364
- type: 'play.route',
365
- to: '#search',
366
- query: { q: searchTerm(), tag: 'typescript' }
367
- });
368
- }
369
-
370
- return (
371
- <div>
372
- <input
373
- value={searchTerm()}
374
- onInput={(e) => setSearchTerm(e.target.value)}
375
- />
376
- <button onClick={handleSearch}>Search</button>
377
- </div>
378
- );
63
+ ```tsx
64
+ interface PlayRouterProviderProps<TActor extends RoutableActor> {
65
+ /** The actor to sync with Solid Router. */
66
+ actor: TActor;
67
+ /** Bidirectional route map for state ID ↔ URL path lookups. */
68
+ routeMap: RouteMap;
69
+ /**
70
+ * The three Solid Router hook results that drive bidirectional sync.
71
+ * Must be obtained via useNavigate(), useLocation(), and useParams()
72
+ * inside a router context.
73
+ */
74
+ router: SolidRouterHooks;
75
+ /** Render callback — receives the concrete actor type and router hooks. */
76
+ renderer: (actor: TActor, router: SolidRouterHooks) => JSX.Element;
379
77
  }
380
78
  ```
381
79
 
382
- **SolidJS Router automatically reflects query params in URL:**
383
-
384
- - `/search?q=xmachines&tag=typescript`
385
-
386
- ### Protected Routes: Authentication Guards
387
-
388
- ```typescript
389
- // State machine with auth guards
390
- import { defineCatalog } from "@xmachines/play-catalog";
391
-
392
- const authMachine = setup({
393
- types: {
394
- context: {} as { isAuthenticated: boolean },
395
- events: {} as PlayRouteEvent | { type: "login" } | { type: "logout" },
396
- },
397
- }).createMachine({
398
- context: { isAuthenticated: false },
399
- initial: "home",
400
- states: {
401
- home: {
402
- meta: { route: "/", view: { component: "Home" } },
403
- },
404
- login: {
405
- meta: { route: "/login", view: { component: "Login" } },
406
- on: {
407
- login: {
408
- target: "dashboard",
409
- actions: assign({ isAuthenticated: true }),
410
- },
411
- },
412
- },
413
- dashboard: {
414
- meta: { route: "/dashboard", view: { component: "Dashboard" } },
415
- always: {
416
- guard: ({ context }) => !context.isAuthenticated,
417
- target: "login",
418
- },
419
- },
420
- },
421
- });
422
-
423
- const catalog = defineCatalog({
424
- Home,
425
- Login,
426
- Dashboard,
427
- });
428
-
429
- const player = definePlayer({ machine: authMachine, catalog });
430
- ```
431
-
432
- **Guard behavior:**
80
+ ### `SolidRouterBridge`
433
81
 
434
- - User navigates to `/dashboard`
435
- - Bridge sends `play.route` event to actor
436
- - Actor's `always` guard checks `isAuthenticated`
437
- - If `false`, actor transitions to `login` state
438
- - Bridge detects state change via `createEffect`, redirects to `/login`
439
- - Actor Authority principle enforced
82
+ Low-level class for manual integration. Extends `RouterBridgeBase` from `@xmachines/play-router` and uses Solid's `createEffect` for reactive router→actor sync.
440
83
 
441
- ### Cleanup: Proper Disposal on Component Unmount
84
+ > **Important:** `connect()` must be called inside a Solid reactive owner (component or `createRoot`). Cleanup is not automatic call `disconnect()` (or `dispose()`) explicitly, typically in `onCleanup()`.
442
85
 
443
86
  ```tsx
444
- import { onCleanup } from "solid-js";
445
- import { SolidRouterBridge } from "@xmachines/play-solid-router";
87
+ import { useNavigate, useLocation, useParams, onCleanup } from "@solidjs/router";
88
+ import { SolidRouterBridge, RouteMap } from "@xmachines/play-solid-router";
446
89
 
447
90
  function App() {
448
91
  const navigate = useNavigate();
449
92
  const location = useLocation();
450
93
  const params = useParams();
451
- const actor = useContext(ActorContext);
452
- const routeMap = useContext(RouteMapContext);
453
94
 
454
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
95
+ const routeMap = new RouteMap([
96
+ { stateId: "#home", path: "/" },
97
+ { stateId: "#profile", path: "/profile/:userId" },
98
+ ]);
455
99
 
456
- // CRITICAL: Cleanup effects
457
- onCleanup(() => {
458
- bridge.dispose();
459
- });
100
+ const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
101
+ bridge.connect();
102
+ onCleanup(() => bridge.disconnect());
460
103
 
461
- return <Router>...</Router>;
104
+ return <div>...</div>;
462
105
  }
463
106
  ```
464
107
 
465
- **Why cleanup matters:**
466
-
467
- - `createEffect` subscriptions continue after disposal (memory leak)
468
- - Multiple bridge instances send duplicate events
469
- - Tests fail with "Cannot send to stopped actor" errors
470
- - Solid's fine-grained reactivity tracks disposed components
471
-
472
- ## Architecture
473
-
474
- ### Bidirectional Sync (Actor ↔ Router)
475
-
476
- **Actor → Router (Signal-driven via createEffect):**
477
-
478
- 1. Actor transitions to new state with `meta.route`
479
- 2. `actor.currentRoute` signal updates
480
- 3. `createEffect(on(...))` fires with new route value
481
- 4. Bridge extracts state ID from signal
482
- 5. Bridge looks up path via `routeMap.getPath(stateId, params)`
483
- 6. Bridge calls `navigate(path)`
484
- 7. SolidJS Router updates URL and renders component
485
-
486
- **Router → Actor (Location tracking via createEffect):**
487
-
488
- 1. User clicks link or browser back button
489
- 2. `location.pathname` signal updates
490
- 3. `createEffect(on(...))` fires with new pathname
491
- 4. Bridge looks up state ID via `routeMap.getStateIdByPath(pathname)`
492
- 5. Bridge extracts params from `useParams()` reactive object
493
- 6. Bridge sends `play.route` event to actor
494
- 7. Actor validates navigation (guards, transitions)
495
- 8. If accepted: Actor transitions, signal updates, URL stays
496
- 9. If rejected: Actor redirects, bridge corrects URL via `navigate()`
497
-
498
- ### Circular Update Prevention
499
-
500
- **Multi-layer guards prevent infinite loops:**
501
-
502
- 1. **`lastSyncedPath` tracking:** Stores last synchronized path, skips if unchanged
503
- 2. **`isProcessingNavigation` flag:** Set during navigation processing, prevents concurrent syncs
504
- 3. **Effect timing:** Solid's batched updates and `defer: true` option prevent rapid cycles
505
-
506
- **Signals-native pattern:**
507
-
508
- ```typescript
509
- // Actor → Router
510
- createEffect(
511
- on(
512
- () => this.actor.currentRoute.get(),
513
- (route) => {
514
- if (!route || route === this.lastSyncedPath || this.isProcessingNavigation) {
515
- return;
516
- }
517
- this.lastSyncedPath = route;
518
- this.navigate(route);
519
- },
520
- { defer: true },
521
- ),
522
- );
523
-
524
- // Router → Actor
525
- createEffect(
526
- on(
527
- () => this.location.pathname,
528
- (pathname) => {
529
- if (pathname === this.lastSyncedPath || this.isProcessingNavigation) {
530
- return;
531
- }
532
- this.isProcessingNavigation = true;
533
- this.actor.send({ type: "play.route", to: stateId, params });
534
- this.isProcessingNavigation = false;
535
- },
536
- { defer: true },
537
- ),
538
- );
539
- ```
540
-
541
- ### Relationship to Other Packages
108
+ ### `createRouteMap(machine)`
542
109
 
543
- **Package Dependencies:**
110
+ Factory that builds a `RouteMap` directly from an XState machine definition. Re-exported from `@xmachines/play-router`.
544
111
 
545
- - `@xmachines/play` - Protocol interfaces (`PlayRouteEvent`, `RouterBridge`)
546
- - `@xmachines/play-actor` - Actor base class with signal protocol
547
- - `@xmachines/play-router` - Route extraction and pattern matching
548
- - `@xmachines/play-signals` - TC39 Signals polyfill for reactivity
549
- - `@xmachines/play-xstate` - XState integration via `definePlayer()`
112
+ ```ts
113
+ import { createRouteMap } from "@xmachines/play-solid-router";
550
114
 
551
- **Architecture Layers:**
552
-
553
- ```
554
- ┌─────────────────────────────────────┐
555
- │ Solid Components (View Layer) │
556
- │ - Props include actor reference │
557
- │ - Sends play.route events │
558
- └─────────────────────────────────────┘
559
-
560
- ┌─────────────────────────────────────┐
561
- │ SolidRouterBridge (Adapter) │
562
- │ - createEffect(actor.currentRoute) │
563
- │ - createEffect(location.pathname) │
564
- └─────────────────────────────────────┘
565
- ↕ ↕
566
- ┌─────────────┐ ┌──────────────────┐
567
- │ SolidJS │ │ XMachines Actor │
568
- │ Router │ │ (Business Logic) │
569
- │ (Infra) │ │ │
570
- └─────────────┘ └──────────────────┘
115
+ const routeMap = createRouteMap(myMachine);
571
116
  ```
572
117
 
573
- ### Signals Integration (SolidJS-Specific)
574
-
575
- **Why signals-native matters:**
118
+ ### `RouteMap` / `RouteMapping`
576
119
 
577
- - **Zero adaptation:** Solid signals and TC39 Signals share reactive primitives
578
- - **Automatic tracking:** `createEffect(on(...))` tracks dependencies without manual Watcher setup
579
- - **Fine-grained updates:** Only affected components re-render (not full tree)
580
- - **Batched updates:** Solid batches multiple signal changes in single render cycle
120
+ Bidirectional state ID URL path mapping. Re-exported from `@xmachines/play-router`.
581
121
 
582
- **Hook context requirement (Pitfall 2):**
583
-
584
- SolidJS hooks (`useNavigate`, `useLocation`, `useParams`) **MUST** be called inside component tree:
585
-
586
- ```tsx
587
- // ❌ WRONG: Bridge created outside component
588
- const navigate = useNavigate(); // ERROR: No reactive context
589
- const bridge = new SolidRouterBridge(navigate, ...);
122
+ ```ts
123
+ import { RouteMap } from "@xmachines/play-solid-router";
590
124
 
591
- // CORRECT: Bridge created inside component
592
- function App() {
593
- const navigate = useNavigate();
594
- const location = useLocation();
595
- const params = useParams();
596
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
597
- onCleanup(() => bridge.dispose());
598
- return <Router>...</Router>;
599
- }
125
+ const routeMap = new RouteMap([
126
+ { stateId: "#home", path: "/" },
127
+ { stateId: "#profile", path: "/profile/:userId" },
128
+ { stateId: "#settings", path: "/settings/:section?" },
129
+ ]);
600
130
  ```
601
131
 
602
- **Why:** Solid's reactivity system requires reactive ownership context. Hooks create tracked scopes that exist only within component lifecycle.
132
+ ### Types
603
133
 
604
- ### Pattern Matching for Dynamic Routes
134
+ | Export | Description |
135
+ | ------------------------- | -------------------------------------------------------------------------------------------- |
136
+ | `RoutableActor` | Minimum actor shape accepted by `PlayRouterProvider` (`AbstractActor & Routable & Viewable`) |
137
+ | `SolidRouterHooks` | Shape of the `router` prop: `{ navigate, location, params }` |
138
+ | `PlayRouterProviderProps` | Full props interface for `PlayRouterProvider` |
139
+ | `PlayRouteEvent` | Event type sent to the actor on URL change (`play.route`) |
140
+ | `RouterBridge` | Interface implemented by `SolidRouterBridge` |
605
141
 
606
- **URLPattern API integration:**
142
+ ## Testing
607
143
 
608
- ```typescript
609
- private matchesPattern(path: string, pattern: string): boolean {
610
- // Use URLPattern for robust matching
611
- const urlPattern = new URLPattern({ pathname: pattern });
612
- return urlPattern.test({ pathname: path });
613
- }
614
- ```
615
-
616
- **Supported syntax:**
144
+ Run tests for this package in isolation:
617
145
 
618
- - `:param` - Required parameter (e.g., `/profile/:userId` matches `/profile/123`)
619
- - `:param?` - Optional parameter (e.g., `/settings/:section?` matches `/settings` and `/settings/account`)
620
- - Wildcards via `*` (future enhancement)
621
-
622
- **Example:**
623
-
624
- ```typescript
625
- const routeMap = new RouteMap([
626
- { stateId: "#profile", path: "/profile/:userId" },
627
- { stateId: "#settings", path: "/settings/:section?" },
628
- ]);
146
+ ```bash
147
+ # From the monorepo root
148
+ npm test -w packages/play-solid-router
629
149
 
630
- routeMap.getStateIdByPath("/profile/123"); // '#profile'
631
- routeMap.getStateIdByPath("/settings"); // '#settings'
632
- routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
150
+ # Or from this package directory
151
+ npm test
633
152
  ```
634
153
 
154
+ Coverage thresholds: **80%** lines, functions, branches, and statements.
155
+
635
156
  ## License
636
157
 
637
- MIT
158
+ MIT — see [LICENSE](LICENSE).