@xmachines/play-solid-router 1.0.0-beta.9 → 2.0.0-alpha.1
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/LICENSE +21 -0
- package/README.md +142 -559
- package/dist/create-play-router-provider.d.ts +62 -0
- package/dist/create-play-router-provider.d.ts.map +1 -0
- package/dist/create-play-router-provider.jsx +45 -0
- package/dist/create-play-router-provider.jsx.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/play-router-provider.d.ts +69 -17
- package/dist/play-router-provider.d.ts.map +1 -1
- package/dist/play-router-provider.jsx +38 -8
- package/dist/play-router-provider.jsx.map +1 -1
- package/dist/solid-router-bridge.d.ts +55 -13
- package/dist/solid-router-bridge.d.ts.map +1 -1
- package/dist/solid-router-bridge.js +71 -15
- package/dist/solid-router-bridge.js.map +1 -1
- package/dist/types.d.ts +0 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +29 -15
- package/dist/create-route-map.d.ts +0 -25
- package/dist/create-route-map.d.ts.map +0 -1
- package/dist/create-route-map.js +0 -36
- package/dist/create-route-map.js.map +0 -1
- package/dist/route-map.d.ts +0 -27
- package/dist/route-map.d.ts.map +0 -1
- package/dist/route-map.js +0 -27
- package/dist/route-map.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,641 +1,224 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 primitives
|
|
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
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## API Reference
|
|
99
|
-
|
|
100
|
-
### `SolidRouterBridge`
|
|
101
|
-
|
|
102
|
-
Router adapter implementing the `RouterBridge` protocol for SolidJS Router.
|
|
103
|
-
|
|
104
|
-
**Type Signature:**
|
|
105
|
-
|
|
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
|
-
}
|
|
12
|
+
pnpm add @xmachines/play-solid-router
|
|
117
13
|
```
|
|
118
14
|
|
|
119
|
-
**
|
|
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
|
|
15
|
+
**Peer dependencies** (must be installed separately):
|
|
140
16
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Bidirectional mapping between XMachines state IDs and SolidJS Router paths with pattern matching support.
|
|
144
|
-
|
|
145
|
-
`RouteMap` extends `BaseRouteMap` from `@xmachines/play-router`, inheriting bucket-indexed
|
|
146
|
-
bidirectional route matching. No routing logic lives in the adapter itself.
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
interface RouteMapping {
|
|
150
|
-
readonly stateId: string;
|
|
151
|
-
readonly path: string;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// RouteMap is a thin subclass of BaseRouteMap — no extra methods
|
|
155
|
-
class RouteMap extends BaseRouteMap {}
|
|
156
|
-
|
|
157
|
-
// Inherited API:
|
|
158
|
-
routeMap.getStateIdByPath(path: string): string | null
|
|
159
|
-
routeMap.getPathByStateId(stateId: string): string | null
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
`getStateIdByPath` and `getPathByStateId` both return `null` (not `undefined`) for misses.
|
|
163
|
-
|
|
164
|
-
**Constructor Parameters:**
|
|
165
|
-
|
|
166
|
-
- `mappings` - Array of `{ stateId, path }` entries:
|
|
167
|
-
- `stateId` — State machine state ID (e.g., `'#profile'`)
|
|
168
|
-
- `path` — SolidJS Router path pattern (e.g., `'/profile/:userId'`)
|
|
169
|
-
|
|
170
|
-
**Methods:**
|
|
171
|
-
|
|
172
|
-
- `getPathByStateId(stateId)` — Find path pattern from state ID
|
|
173
|
-
- `getStateIdByPath(path)` — Find state ID from path with pattern matching (supports `:param` and `:param?` syntax)
|
|
174
|
-
|
|
175
|
-
**Pattern Matching:**
|
|
176
|
-
|
|
177
|
-
Uses bucket-indexed RegExp matching for dynamic routes:
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
const routeMap = new RouteMap([{ stateId: "#settings", path: "/settings/:section?" }]);
|
|
181
|
-
|
|
182
|
-
routeMap.getStateIdByPath("/settings"); // '#settings'
|
|
183
|
-
routeMap.getStateIdByPath("/settings/account"); // '#settings'
|
|
184
|
-
routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
|
|
185
|
-
routeMap.getStateIdByPath("/other"); // null
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Examples
|
|
189
|
-
|
|
190
|
-
### Basic Usage: Simple 2-3 Route Setup
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
import { Router, Route } from '@solidjs/router';
|
|
194
|
-
import { createSignal } from 'solid-js';
|
|
195
|
-
import { defineCatalog } from '@xmachines/play-catalog';
|
|
196
|
-
|
|
197
|
-
// State machine with 3 states
|
|
198
|
-
const appMachine = setup({
|
|
199
|
-
types: {
|
|
200
|
-
events: {} as PlayRouteEvent
|
|
201
|
-
}
|
|
202
|
-
}).createMachine({
|
|
203
|
-
id: 'app',
|
|
204
|
-
initial: 'home',
|
|
205
|
-
states: {
|
|
206
|
-
home: {
|
|
207
|
-
meta: { route: '/', view: { component: 'Home' } }
|
|
208
|
-
},
|
|
209
|
-
about: {
|
|
210
|
-
meta: { route: '/about', view: { component: 'About' } }
|
|
211
|
-
},
|
|
212
|
-
contact: {
|
|
213
|
-
meta: { route: '/contact', view: { component: 'Contact' } }
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const catalog = defineCatalog({
|
|
219
|
-
Home,
|
|
220
|
-
About,
|
|
221
|
-
Contact,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// Component setup
|
|
225
|
-
function App() {
|
|
226
|
-
const navigate = useNavigate();
|
|
227
|
-
const location = useLocation();
|
|
228
|
-
const params = useParams();
|
|
229
|
-
|
|
230
|
-
const routeMap = createRouteMap(appMachine);
|
|
231
|
-
|
|
232
|
-
const createPlayer = definePlayer({ machine: appMachine, catalog });
|
|
233
|
-
const actor = createPlayer();
|
|
234
|
-
actor.start();
|
|
235
|
-
const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
|
|
236
|
-
|
|
237
|
-
onCleanup(() => bridge.dispose());
|
|
238
|
-
|
|
239
|
-
return (
|
|
240
|
-
<Router>
|
|
241
|
-
<Route path="/" component={Home} />
|
|
242
|
-
<Route path="/about" component={About} />
|
|
243
|
-
<Route path="/contact" component={Contact} />
|
|
244
|
-
</Router>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add solid-js @solidjs/router xstate
|
|
247
19
|
```
|
|
248
20
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// State machine with parameter routes
|
|
253
|
-
import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
254
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
21
|
+
- `solid-js` `^1.8.0`
|
|
22
|
+
- `@solidjs/router` `^0.16.1`
|
|
23
|
+
- `xstate` `^6.0.0-alpha.19`
|
|
255
24
|
|
|
256
|
-
|
|
257
|
-
id: 'app',
|
|
258
|
-
context: {},
|
|
259
|
-
states: {
|
|
260
|
-
profile: {
|
|
261
|
-
meta: {
|
|
262
|
-
route: '/profile/:userId',
|
|
263
|
-
view: { component: 'Profile' },
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
settings: {
|
|
267
|
-
meta: {
|
|
268
|
-
route: '/settings/:section?',
|
|
269
|
-
view: { component: 'Settings' },
|
|
270
|
-
},
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const appMachine = setup({
|
|
276
|
-
types: {
|
|
277
|
-
context: {} as { userId?: string; section?: string },
|
|
278
|
-
events: {} as PlayRouteEvent
|
|
279
|
-
}
|
|
280
|
-
}).createMachine(formatPlayRouteTransitions(machineConfig));
|
|
281
|
-
|
|
282
|
-
const catalog = defineCatalog({
|
|
283
|
-
Profile,
|
|
284
|
-
Settings,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// Router with dynamic routes
|
|
288
|
-
function App() {
|
|
289
|
-
const navigate = useNavigate();
|
|
290
|
-
const location = useLocation();
|
|
291
|
-
const params = useParams();
|
|
25
|
+
## Quick Start
|
|
292
26
|
|
|
293
|
-
|
|
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";
|
|
294
33
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
actor.start();
|
|
298
|
-
const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
|
|
34
|
+
const actor = definePlayer({ machine: myMachine })();
|
|
35
|
+
actor.start();
|
|
299
36
|
|
|
300
|
-
|
|
37
|
+
const routeMap = createRouteMap(myMachine);
|
|
301
38
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
</Router>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
```
|
|
39
|
+
const Layout: ParentComponent = () => {
|
|
40
|
+
const navigate = useNavigate();
|
|
41
|
+
const location = useLocation();
|
|
42
|
+
const params = useParams();
|
|
310
43
|
|
|
311
|
-
|
|
44
|
+
onCleanup(() => actor.stop());
|
|
312
45
|
|
|
313
|
-
```tsx
|
|
314
|
-
function ProfileButton(props: { userId: string }) {
|
|
315
46
|
return (
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
})
|
|
323
|
-
}
|
|
324
|
-
>
|
|
325
|
-
View Profile
|
|
326
|
-
</button>
|
|
47
|
+
<PlayRouterProvider
|
|
48
|
+
actor={actor}
|
|
49
|
+
routeMap={routeMap}
|
|
50
|
+
router={{ navigate, location, params }}
|
|
51
|
+
renderer={(a, router) => <MyApp actor={a} />}
|
|
52
|
+
/>
|
|
327
53
|
);
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### Query Parameters: Search/Filters via Query Strings
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
// State machine with query param handling
|
|
335
|
-
import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
336
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
337
|
-
|
|
338
|
-
const machineConfig = {
|
|
339
|
-
context: { query: '', filters: {} },
|
|
340
|
-
states: {
|
|
341
|
-
search: {
|
|
342
|
-
meta: {
|
|
343
|
-
route: '/search',
|
|
344
|
-
view: { component: 'Search' },
|
|
345
|
-
},
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
54
|
};
|
|
349
55
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
context: {} as { query: string; filters: Record<string, string> },
|
|
353
|
-
events: {} as PlayRouteEvent
|
|
354
|
-
}
|
|
355
|
-
}).createMachine(formatPlayRouteTransitions(machineConfig));
|
|
356
|
-
|
|
357
|
-
const catalog = defineCatalog({
|
|
358
|
-
Search,
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
const player = definePlayer({ machine: searchMachine, catalog });
|
|
362
|
-
|
|
363
|
-
// Component sends query params
|
|
364
|
-
function SearchBar(props) {
|
|
365
|
-
const [searchTerm, setSearchTerm] = createSignal('');
|
|
366
|
-
|
|
367
|
-
function handleSearch() {
|
|
368
|
-
props.actor.send({
|
|
369
|
-
type: 'play.route',
|
|
370
|
-
to: '#search',
|
|
371
|
-
query: { q: searchTerm(), tag: 'typescript' }
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return (
|
|
376
|
-
<div>
|
|
377
|
-
<input
|
|
378
|
-
value={searchTerm()}
|
|
379
|
-
onInput={(e) => setSearchTerm(e.target.value)}
|
|
380
|
-
/>
|
|
381
|
-
<button onClick={handleSearch}>Search</button>
|
|
382
|
-
</div>
|
|
383
|
-
);
|
|
56
|
+
export default function App() {
|
|
57
|
+
return <Router root={Layout}>{/* one <Route> per routable state */}</Router>;
|
|
384
58
|
}
|
|
385
59
|
```
|
|
386
60
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
- `/search?q=xmachines&tag=typescript`
|
|
61
|
+
## API Summary
|
|
390
62
|
|
|
391
|
-
###
|
|
63
|
+
### `PlayRouterProvider`
|
|
392
64
|
|
|
393
|
-
|
|
394
|
-
// State machine with auth guards
|
|
395
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
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.
|
|
396
66
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
login: {
|
|
413
|
-
target: "dashboard",
|
|
414
|
-
actions: assign({ isAuthenticated: true }),
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
},
|
|
418
|
-
dashboard: {
|
|
419
|
-
meta: { route: "/dashboard", view: { component: "Dashboard" } },
|
|
420
|
-
always: {
|
|
421
|
-
guard: ({ context }) => !context.isAuthenticated,
|
|
422
|
-
target: "login",
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
const catalog = defineCatalog({
|
|
429
|
-
Home,
|
|
430
|
-
Login,
|
|
431
|
-
Dashboard,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const player = definePlayer({ machine: authMachine, catalog });
|
|
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;
|
|
81
|
+
}
|
|
435
82
|
```
|
|
436
83
|
|
|
437
|
-
|
|
84
|
+
### `SolidRouterBridge`
|
|
438
85
|
|
|
439
|
-
-
|
|
440
|
-
- Bridge sends `play.route` event to actor
|
|
441
|
-
- Actor's `always` guard checks `isAuthenticated`
|
|
442
|
-
- If `false`, actor transitions to `login` state
|
|
443
|
-
- Bridge detects state change via `createEffect`, redirects to `/login`
|
|
444
|
-
- Actor Authority principle enforced
|
|
86
|
+
Low-level class for manual integration. Extends `RouterBridgeBase` from `@xmachines/play-router` and uses Solid's `createEffect` for reactive router→actor sync.
|
|
445
87
|
|
|
446
|
-
|
|
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()`.
|
|
447
89
|
|
|
448
90
|
```tsx
|
|
449
|
-
import { onCleanup } from "
|
|
450
|
-
import { SolidRouterBridge } from "@xmachines/play-solid-router";
|
|
91
|
+
import { useNavigate, useLocation, useParams, onCleanup } from "@solidjs/router";
|
|
92
|
+
import { SolidRouterBridge, RouteMap } from "@xmachines/play-solid-router";
|
|
451
93
|
|
|
452
94
|
function App() {
|
|
453
95
|
const navigate = useNavigate();
|
|
454
96
|
const location = useLocation();
|
|
455
97
|
const params = useParams();
|
|
456
|
-
const actor = useContext(ActorContext);
|
|
457
|
-
const routeMap = useContext(RouteMapContext);
|
|
458
98
|
|
|
459
|
-
const
|
|
99
|
+
const routeMap = new RouteMap([
|
|
100
|
+
{ stateId: "#home", path: "/" },
|
|
101
|
+
{ stateId: "#profile", path: "/profile/:userId" },
|
|
102
|
+
]);
|
|
460
103
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
});
|
|
104
|
+
const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
|
|
105
|
+
bridge.connect();
|
|
106
|
+
onCleanup(() => bridge.disconnect());
|
|
465
107
|
|
|
466
|
-
return <
|
|
108
|
+
return <div>...</div>;
|
|
467
109
|
}
|
|
468
110
|
```
|
|
469
111
|
|
|
470
|
-
|
|
112
|
+
### `createRouteMap(machine)`
|
|
471
113
|
|
|
472
|
-
|
|
473
|
-
- Multiple bridge instances send duplicate events
|
|
474
|
-
- Tests fail with "Cannot send to stopped actor" errors
|
|
475
|
-
- Solid's fine-grained reactivity tracks disposed components
|
|
114
|
+
Factory that builds a `RouteMap` directly from an XState machine definition. Re-exported from `@xmachines/play-router`.
|
|
476
115
|
|
|
477
|
-
|
|
116
|
+
```ts
|
|
117
|
+
import { createRouteMap } from "@xmachines/play-solid-router";
|
|
478
118
|
|
|
479
|
-
|
|
119
|
+
const routeMap = createRouteMap(myMachine);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `RouteMap` / `RouteMapping`
|
|
480
123
|
|
|
481
|
-
|
|
124
|
+
Bidirectional state ID ↔ URL path mapping. Re-exported from `@xmachines/play-router`.
|
|
482
125
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
3. `createEffect(on(...))` fires with new route value
|
|
486
|
-
4. Bridge extracts state ID from signal
|
|
487
|
-
5. Bridge looks up path via `routeMap.getPathByStateId(stateId)`
|
|
488
|
-
6. Bridge calls `navigate(path)`
|
|
489
|
-
7. SolidJS Router updates URL and renders component
|
|
126
|
+
```ts
|
|
127
|
+
import { RouteMap } from "@xmachines/play-solid-router";
|
|
490
128
|
|
|
491
|
-
|
|
129
|
+
const routeMap = new RouteMap([
|
|
130
|
+
{ stateId: "#home", path: "/" },
|
|
131
|
+
{ stateId: "#profile", path: "/profile/:userId" },
|
|
132
|
+
{ stateId: "#settings", path: "/settings/:section?" },
|
|
133
|
+
]);
|
|
134
|
+
```
|
|
492
135
|
|
|
493
|
-
|
|
494
|
-
2. `location.pathname` signal updates
|
|
495
|
-
3. `createEffect(on(...))` fires with new pathname
|
|
496
|
-
4. Bridge looks up state ID via `routeMap.getStateIdByPath(pathname)`
|
|
497
|
-
5. Bridge extracts params from `useParams()` reactive object
|
|
498
|
-
6. Bridge sends `play.route` event to actor
|
|
499
|
-
7. Actor validates navigation (guards, transitions)
|
|
500
|
-
8. If accepted: Actor transitions, signal updates, URL stays
|
|
501
|
-
9. If rejected: Actor redirects, bridge corrects URL via `navigate()`
|
|
136
|
+
### Types
|
|
502
137
|
|
|
503
|
-
|
|
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`. |
|
|
504
148
|
|
|
505
|
-
|
|
149
|
+
## Usage Patterns
|
|
506
150
|
|
|
507
|
-
|
|
508
|
-
2. **`isProcessingNavigation` flag:** Set during navigation processing, prevents concurrent syncs
|
|
509
|
-
3. **Effect timing:** Solid's batched updates and `defer: true` option prevent rapid cycles
|
|
151
|
+
### Protected Routes and Guards
|
|
510
152
|
|
|
511
|
-
|
|
153
|
+
Auth guards live entirely inside the state machine, preventing flashes of unauthorized content:
|
|
512
154
|
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
this.lastSyncedPath = route;
|
|
523
|
-
this.navigate(route);
|
|
524
|
-
},
|
|
525
|
-
{ defer: true },
|
|
526
|
-
),
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
// Router → Actor
|
|
530
|
-
createEffect(
|
|
531
|
-
on(
|
|
532
|
-
() => this.location.pathname,
|
|
533
|
-
(pathname) => {
|
|
534
|
-
if (pathname === this.lastSyncedPath || this.isProcessingNavigation) {
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
this.isProcessingNavigation = true;
|
|
538
|
-
this.actor.send({ type: "play.route", to: stateId, params });
|
|
539
|
-
this.isProcessingNavigation = false;
|
|
155
|
+
```ts
|
|
156
|
+
const machineConfig = {
|
|
157
|
+
states: {
|
|
158
|
+
dashboard: {
|
|
159
|
+
meta: { route: "/dashboard" },
|
|
160
|
+
always: ({ context }) => {
|
|
161
|
+
if (context.isAuthenticated) return;
|
|
162
|
+
return { target: "login" };
|
|
163
|
+
},
|
|
540
164
|
},
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
544
167
|
```
|
|
545
168
|
|
|
546
|
-
|
|
169
|
+
When a user navigates to `/dashboard` while unauthenticated:
|
|
547
170
|
|
|
548
|
-
|
|
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")`.
|
|
549
176
|
|
|
550
|
-
|
|
551
|
-
- `@xmachines/play-actor` - Actor base class with signal protocol
|
|
552
|
-
- `@xmachines/play-router` - Route extraction and pattern matching
|
|
553
|
-
- `@xmachines/play-signals` - TC39 Signals polyfill for reactivity
|
|
554
|
-
- `@xmachines/play-xstate` - XState integration via `definePlayer()`
|
|
177
|
+
### Dynamic Routes with Parameters
|
|
555
178
|
|
|
556
|
-
|
|
179
|
+
```ts
|
|
180
|
+
const routeMap = new RouteMap([
|
|
181
|
+
{ stateId: "#post", path: "/users/:userId/posts/:postId" },
|
|
182
|
+
{ stateId: "#settings", path: "/settings/:section?" },
|
|
183
|
+
]);
|
|
557
184
|
|
|
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: {} }
|
|
558
187
|
```
|
|
559
|
-
┌─────────────────────────────────────┐
|
|
560
|
-
│ Solid Components (View Layer) │
|
|
561
|
-
│ - Props include actor reference │
|
|
562
|
-
│ - Sends play.route events │
|
|
563
|
-
└─────────────────────────────────────┘
|
|
564
|
-
↕
|
|
565
|
-
┌─────────────────────────────────────┐
|
|
566
|
-
│ SolidRouterBridge (Adapter) │
|
|
567
|
-
│ - createEffect(actor.currentRoute) │
|
|
568
|
-
│ - createEffect(location.pathname) │
|
|
569
|
-
└─────────────────────────────────────┘
|
|
570
|
-
↕ ↕
|
|
571
|
-
┌─────────────┐ ┌──────────────────┐
|
|
572
|
-
│ SolidJS │ │ XMachines Actor │
|
|
573
|
-
│ Router │ │ (Business Logic) │
|
|
574
|
-
│ (Infra) │ │ │
|
|
575
|
-
└─────────────┘ └──────────────────┘
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
### Signals Integration (SolidJS-Specific)
|
|
579
188
|
|
|
580
|
-
|
|
189
|
+
Path parameters are extracted from Solid's reactive `useParams()` proxy — no URLPattern polyfill is needed for parameterized routes.
|
|
581
190
|
|
|
582
|
-
|
|
583
|
-
- **Automatic tracking:** `createEffect(on(...))` tracks dependencies without manual Watcher setup
|
|
584
|
-
- **Fine-grained updates:** Only affected components re-render (not full tree)
|
|
585
|
-
- **Batched updates:** Solid batches multiple signal changes in single render cycle
|
|
191
|
+
## Testing
|
|
586
192
|
|
|
587
|
-
|
|
193
|
+
Run tests for this package in isolation:
|
|
588
194
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// ❌ WRONG: Bridge created outside component
|
|
593
|
-
const navigate = useNavigate(); // ERROR: No reactive context
|
|
594
|
-
const bridge = new SolidRouterBridge(navigate, ...);
|
|
195
|
+
```bash
|
|
196
|
+
# From the monorepo root
|
|
197
|
+
pnpm --filter @xmachines/play-solid-router test
|
|
595
198
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const navigate = useNavigate();
|
|
599
|
-
const location = useLocation();
|
|
600
|
-
const params = useParams();
|
|
601
|
-
const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
|
|
602
|
-
onCleanup(() => bridge.dispose());
|
|
603
|
-
return <Router>...</Router>;
|
|
604
|
-
}
|
|
199
|
+
# Or from this package directory
|
|
200
|
+
pnpm test
|
|
605
201
|
```
|
|
606
202
|
|
|
607
|
-
**
|
|
608
|
-
|
|
609
|
-
### Pattern Matching for Dynamic Routes
|
|
203
|
+
**Browser tests** (`test/browser/**/*.browser.test.ts`) run against real Chromium via Playwright, covering async sequencing that jsdom cannot faithfully reproduce:
|
|
610
204
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
relevant bucket (plus the wildcard `*` bucket for `:param`-first routes) is scanned —
|
|
615
|
-
typically far fewer than all registered routes.
|
|
205
|
+
```bash
|
|
206
|
+
pnpm exec vitest --config vitest.browser.config.ts --project play-solid-router-browser
|
|
207
|
+
```
|
|
616
208
|
|
|
617
|
-
**
|
|
209
|
+
Coverage thresholds: **80%** lines, functions, branches, and statements.
|
|
618
210
|
|
|
619
|
-
|
|
620
|
-
- `:param?` - Optional parameter (e.g., `/settings/:section?` matches `/settings` and `/settings/account`)
|
|
621
|
-
- Wildcards via `*` (future enhancement)
|
|
211
|
+
## Related Packages
|
|
622
212
|
|
|
623
|
-
|
|
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 v6 player factory
|
|
624
217
|
|
|
625
|
-
|
|
626
|
-
const routeMap = new RouteMap([
|
|
627
|
-
{ stateId: "#profile", path: "/profile/:userId" },
|
|
628
|
-
{ stateId: "#settings", path: "/settings/:section?" },
|
|
629
|
-
]);
|
|
218
|
+
## Learn More
|
|
630
219
|
|
|
631
|
-
|
|
632
|
-
routeMap.getStateIdByPath("/settings"); // '#settings'
|
|
633
|
-
routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
|
|
634
|
-
```
|
|
220
|
+
- [Demo](examples/demo/README.md)
|
|
635
221
|
|
|
636
222
|
## License
|
|
637
223
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
This work is licensed under the terms of the MIT license.
|
|
641
|
-
For a copy, see <https://opensource.org/licenses/MIT>.
|
|
224
|
+
MIT — see [LICENSE](LICENSE).
|