@xmachines/play-solid-router 1.0.0-beta.8 → 1.0.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 CHANGED
@@ -1,640 +1,224 @@
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:
12
-
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:**
1
+ <!-- generated-by: gsd-doc-writer -->
18
2
 
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
35
- ```
36
-
37
- **Peer dependencies:**
38
-
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
45
-
46
- ## Quick Start
47
-
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
- }
12
+ pnpm add @xmachines/play-solid-router
96
13
  ```
97
14
 
98
- ## API Reference
99
-
100
- ### `SolidRouterBridge`
101
-
102
- Router adapter implementing the `RouterBridge` protocol for SolidJS Router.
103
-
104
- **Type Signature:**
15
+ **Peer dependencies** (must be installed separately):
105
16
 
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,
114
- );
115
- dispose(): void;
116
- }
17
+ ```bash
18
+ pnpm add solid-js @solidjs/router xstate
117
19
  ```
118
20
 
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
21
+ - `solid-js` `^1.8.0`
22
+ - `@solidjs/router` `^0.16.1`
23
+ - `xstate` `^5.31.0`
126
24
 
127
- **Methods:**
128
-
129
- - `connect()` - Start bidirectional synchronization.
130
- - `disconnect()` - Stop synchronization and cleanup bridge resources.
131
- - `dispose()` - Alias of `disconnect()`.
25
+ ## Quick Start
132
26
 
133
- **Internal Behavior:**
27
+ ```tsx
28
+ import { Router, Route, useNavigate, useLocation, useParams } from "@solidjs/router";
29
+ import { onCleanup, type ParentComponent } from "solid-js";
30
+ import { PlayRouterProvider, createRouteMap } from "@xmachines/play-solid-router";
31
+ import { definePlayer } from "@xmachines/play-xstate";
32
+ import { myMachine } from "./machine.js";
134
33
 
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
34
+ const actor = definePlayer({ machine: myMachine })();
35
+ actor.start();
140
36
 
141
- ### `RouteMap`
37
+ const routeMap = createRouteMap(myMachine);
142
38
 
143
- Bidirectional mapping between XMachines state IDs and SolidJS Router paths with pattern matching support.
39
+ const Layout: ParentComponent = () => {
40
+ const navigate = useNavigate();
41
+ const location = useLocation();
42
+ const params = useParams();
144
43
 
145
- **Type Signature:**
44
+ onCleanup(() => actor.stop());
146
45
 
147
- ```typescript
148
- interface RouteMapping {
149
- stateId: string;
150
- path: string;
151
- }
46
+ return (
47
+ <PlayRouterProvider
48
+ actor={actor}
49
+ routeMap={routeMap}
50
+ router={{ navigate, location, params }}
51
+ renderer={(a, router) => <MyApp actor={a} />}
52
+ />
53
+ );
54
+ };
152
55
 
153
- class RouteMap {
154
- constructor(mappings: RouteMapping[]);
155
- getPath(stateId: string, params?: Record<string, string>): string | undefined;
156
- getStateIdByPath(path: string): string | undefined;
56
+ export default function App() {
57
+ return <Router root={Layout}>{/* one <Route> per routable state */}</Router>;
157
58
  }
158
59
  ```
159
60
 
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:**
61
+ ## API Summary
172
62
 
173
- Uses URLPattern API for robust dynamic route matching:
63
+ ### `PlayRouterProvider`
174
64
 
175
- ```typescript
176
- const routeMap = new RouteMap([{ stateId: "#settings", path: "/settings/:section?" }]);
65
+ 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.
177
66
 
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
- );
67
+ ```tsx
68
+ interface PlayRouterProviderProps<TActor extends PlayActor> {
69
+ /** The actor to sync with Solid Router. */
70
+ actor: TActor;
71
+ /** Bidirectional route map for state ID ↔ URL path lookups. */
72
+ routeMap: RouteMap;
73
+ /**
74
+ * The three Solid Router hook results that drive bidirectional sync.
75
+ * Must be obtained via useNavigate(), useLocation(), and useParams()
76
+ * inside a router context.
77
+ */
78
+ router: SolidRouterHooks;
79
+ /** Render callback receives the concrete actor type and router hooks. */
80
+ renderer: (actor: TActor, router: SolidRouterHooks) => JSX.Element;
241
81
  }
242
82
  ```
243
83
 
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";
84
+ ### `SolidRouterBridge`
250
85
 
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
- };
86
+ Low-level class for manual integration. Extends `RouterBridgeBase` from `@xmachines/play-router` and uses Solid's `createEffect` for reactive router→actor sync.
269
87
 
270
- const appMachine = setup({
271
- types: {
272
- context: {} as { userId?: string; section?: string },
273
- events: {} as PlayRouteEvent
274
- }
275
- }).createMachine(formatPlayRouteTransitions(machineConfig));
88
+ > **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()`.
276
89
 
277
- const catalog = defineCatalog({
278
- Profile,
279
- Settings,
280
- });
90
+ ```tsx
91
+ import { useNavigate, useLocation, useParams, onCleanup } from "@solidjs/router";
92
+ import { SolidRouterBridge, RouteMap } from "@xmachines/play-solid-router";
281
93
 
282
- // Router with dynamic routes
283
94
  function App() {
284
- const navigate = useNavigate();
285
- const location = useLocation();
286
- const params = useParams();
287
-
288
- const routeMap = createRouteMap(appMachine);
95
+ const navigate = useNavigate();
96
+ const location = useLocation();
97
+ const params = useParams();
289
98
 
290
- const createPlayer = definePlayer({ machine: appMachine, catalog });
291
- const actor = createPlayer();
292
- actor.start();
293
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
99
+ const routeMap = new RouteMap([
100
+ { stateId: "#home", path: "/" },
101
+ { stateId: "#profile", path: "/profile/:userId" },
102
+ ]);
294
103
 
295
- onCleanup(() => bridge.dispose());
104
+ const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
105
+ bridge.connect();
106
+ onCleanup(() => bridge.disconnect());
296
107
 
297
- return (
298
- <Router>
299
- <Route path="/profile/:userId" component={Profile} />
300
- <Route path="/settings/:section?" component={Settings} />
301
- </Router>
302
- );
108
+ return <div>...</div>;
303
109
  }
304
110
  ```
305
111
 
306
- **Usage in component:**
112
+ ### `createRouteMap(machine)`
307
113
 
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
- }
114
+ Factory that builds a `RouteMap` directly from an XState machine definition. Re-exported from `@xmachines/play-router`.
115
+
116
+ ```ts
117
+ import { createRouteMap } from "@xmachines/play-solid-router";
118
+
119
+ const routeMap = createRouteMap(myMachine);
324
120
  ```
325
121
 
326
- ### Query Parameters: Search/Filters via Query Strings
122
+ ### `RouteMap` / `RouteMapping`
327
123
 
328
- ```typescript
329
- // State machine with query param handling
330
- import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
331
- import { defineCatalog } from "@xmachines/play-catalog";
124
+ Bidirectional state ID ↔ URL path mapping. Re-exported from `@xmachines/play-router`.
332
125
 
333
- const machineConfig = {
334
- context: { query: '', filters: {} },
335
- states: {
336
- search: {
337
- meta: {
338
- route: '/search',
339
- view: { component: 'Search' },
340
- },
341
- }
342
- }
343
- };
126
+ ```ts
127
+ import { RouteMap } from "@xmachines/play-solid-router";
344
128
 
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
- );
379
- }
129
+ const routeMap = new RouteMap([
130
+ { stateId: "#home", path: "/" },
131
+ { stateId: "#profile", path: "/profile/:userId" },
132
+ { stateId: "#settings", path: "/settings/:section?" },
133
+ ]);
380
134
  ```
381
135
 
382
- **SolidJS Router automatically reflects query params in URL:**
136
+ ### Types
383
137
 
384
- - `/search?q=xmachines&tag=typescript`
138
+ | Export | Description |
139
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `PlayActor` | `AbstractActor & Routable & Viewable` — canonical actor shape from `@xmachines/play-router`. Required by `PlayRouterProvider`, which renders the current view spec in addition to synchronizing routes. |
141
+ | `RoutableActor` | Deprecated alias for `PlayActor`. Use `PlayActor` from `@xmachines/play-router` in new code. |
142
+ | `AbstractActor` | Re-exported from `@xmachines/play-actor` for convenience when typing renderer callbacks. |
143
+ | `SolidRouterHooks` | Shape of the `router` prop: `{ navigate, location, params }` |
144
+ | `PlayRouterProviderProps` | Full props interface for `PlayRouterProvider` |
145
+ | `PlayRouteEvent` | Event type sent to the actor on URL change (`play.route`) |
146
+ | `RouterBridge` | Interface implemented by `SolidRouterBridge` |
147
+ | `RouteMapOptions` | Options bag for `RouteMap` construction. Re-exported from `@xmachines/play-router`. |
385
148
 
386
- ### Protected Routes: Authentication Guards
149
+ ## Usage Patterns
387
150
 
388
- ```typescript
389
- // State machine with auth guards
390
- import { defineCatalog } from "@xmachines/play-catalog";
151
+ ### Protected Routes and Guards
391
152
 
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",
153
+ Auth guards live entirely inside the state machine, preventing flashes of unauthorized content:
154
+
155
+ ```ts
156
+ const machineConfig = {
400
157
  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
158
  dashboard: {
414
- meta: { route: "/dashboard", view: { component: "Dashboard" } },
159
+ meta: { route: "/dashboard" },
415
160
  always: {
416
161
  guard: ({ context }) => !context.isAuthenticated,
417
162
  target: "login",
418
163
  },
419
164
  },
420
165
  },
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:**
433
-
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
440
-
441
- ### Cleanup: Proper Disposal on Component Unmount
442
-
443
- ```tsx
444
- import { onCleanup } from "solid-js";
445
- import { SolidRouterBridge } from "@xmachines/play-solid-router";
446
-
447
- function App() {
448
- const navigate = useNavigate();
449
- const location = useLocation();
450
- const params = useParams();
451
- const actor = useContext(ActorContext);
452
- const routeMap = useContext(RouteMapContext);
453
-
454
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
455
-
456
- // CRITICAL: Cleanup effects
457
- onCleanup(() => {
458
- bridge.dispose();
459
- });
460
-
461
- return <Router>...</Router>;
462
- }
463
- ```
464
-
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
- );
166
+ };
539
167
  ```
540
168
 
541
- ### Relationship to Other Packages
169
+ When a user navigates to `/dashboard` while unauthenticated:
542
170
 
543
- **Package Dependencies:**
171
+ 1. Solid Router updates the URL.
172
+ 2. Bridge intercepts and sends `play.route` to the actor.
173
+ 3. Actor evaluates the guard — denies transition, moves to `login` instead.
174
+ 4. Bridge observes new actor route (`/login`) via TC39 Signal.
175
+ 5. Bridge calls `navigate("/login")`.
544
176
 
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()`
177
+ ### Dynamic Routes with Parameters
550
178
 
551
- **Architecture Layers:**
179
+ ```ts
180
+ const routeMap = new RouteMap([
181
+ { stateId: "#post", path: "/users/:userId/posts/:postId" },
182
+ { stateId: "#settings", path: "/settings/:section?" },
183
+ ]);
552
184
 
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
- └─────────────┘ └──────────────────┘
185
+ // Params are extracted from Solid's useParams() and forwarded in the play.route event:
186
+ // { type: "play.route", to: "#post", params: { userId: "123", postId: "456" }, query: {} }
571
187
  ```
572
188
 
573
- ### Signals Integration (SolidJS-Specific)
189
+ Path parameters are extracted from Solid's reactive `useParams()` proxy — no URLPattern polyfill is needed for parameterized routes.
574
190
 
575
- **Why signals-native matters:**
191
+ ## Testing
576
192
 
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
193
+ Run tests for this package in isolation:
581
194
 
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, ...);
195
+ ```bash
196
+ # From the monorepo root
197
+ pnpm --filter @xmachines/play-solid-router test
590
198
 
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
- }
199
+ # Or from this package directory
200
+ pnpm test
600
201
  ```
601
202
 
602
- **Why:** Solid's reactivity system requires reactive ownership context. Hooks create tracked scopes that exist only within component lifecycle.
603
-
604
- ### Pattern Matching for Dynamic Routes
605
-
606
- **URLPattern API integration:**
203
+ **Browser tests** (`test/browser/**/*.browser.test.ts`) run against real Chromium via Playwright, covering async sequencing that jsdom cannot faithfully reproduce:
607
204
 
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
- }
205
+ ```bash
206
+ pnpm exec vitest --config vitest.browser.config.ts --project play-solid-router-browser
614
207
  ```
615
208
 
616
- **Supported syntax:**
209
+ Coverage thresholds: **80%** lines, functions, branches, and statements.
617
210
 
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)
211
+ ## Related Packages
621
212
 
622
- **Example:**
213
+ - [@xmachines/play-router](../play-router/README.md) — core router primitives and `RouterBridgeBase`
214
+ - [@xmachines/play-tanstack-solid-router](../play-tanstack-solid-router/README.md) — TanStack Solid Router adapter
215
+ - [@xmachines/play-solid](../play-solid/README.md) — SolidJS view renderer
216
+ - [@xmachines/play-xstate](../play-xstate/README.md) — XState v5 player factory
623
217
 
624
- ```typescript
625
- const routeMap = new RouteMap([
626
- { stateId: "#profile", path: "/profile/:userId" },
627
- { stateId: "#settings", path: "/settings/:section?" },
628
- ]);
218
+ ## Learn More
629
219
 
630
- routeMap.getStateIdByPath("/profile/123"); // '#profile'
631
- routeMap.getStateIdByPath("/settings"); // '#settings'
632
- routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
633
- ```
220
+ - [Demo](examples/demo/README.md)
634
221
 
635
222
  ## License
636
223
 
637
- Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
638
-
639
- This work is licensed under the terms of the MIT license.
640
- For a copy, see <https://opensource.org/licenses/MIT>.
224
+ MIT see [LICENSE](LICENSE).