@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/LICENSE +21 -0
- package/README.md +140 -556
- 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 +72 -16
- 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 +27 -13
- 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 -78
- package/dist/route-map.d.ts.map +0 -1
- package/dist/route-map.js +0 -161
- package/dist/route-map.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,640 +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 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
|
-
|
|
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
|
-
```
|
|
107
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
37
|
+
const routeMap = createRouteMap(myMachine);
|
|
142
38
|
|
|
143
|
-
|
|
39
|
+
const Layout: ParentComponent = () => {
|
|
40
|
+
const navigate = useNavigate();
|
|
41
|
+
const location = useLocation();
|
|
42
|
+
const params = useParams();
|
|
144
43
|
|
|
145
|
-
|
|
44
|
+
onCleanup(() => actor.stop());
|
|
146
45
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
### `PlayRouterProvider`
|
|
174
64
|
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const routeMap = createRouteMap(appMachine);
|
|
95
|
+
const navigate = useNavigate();
|
|
96
|
+
const location = useLocation();
|
|
97
|
+
const params = useParams();
|
|
289
98
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
99
|
+
const routeMap = new RouteMap([
|
|
100
|
+
{ stateId: "#home", path: "/" },
|
|
101
|
+
{ stateId: "#profile", path: "/profile/:userId" },
|
|
102
|
+
]);
|
|
294
103
|
|
|
295
|
-
|
|
104
|
+
const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
|
|
105
|
+
bridge.connect();
|
|
106
|
+
onCleanup(() => bridge.disconnect());
|
|
296
107
|
|
|
297
|
-
|
|
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
|
-
|
|
112
|
+
### `createRouteMap(machine)`
|
|
307
113
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
###
|
|
122
|
+
### `RouteMap` / `RouteMapping`
|
|
327
123
|
|
|
328
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
136
|
+
### Types
|
|
383
137
|
|
|
384
|
-
|
|
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
|
-
|
|
149
|
+
## Usage Patterns
|
|
387
150
|
|
|
388
|
-
|
|
389
|
-
// State machine with auth guards
|
|
390
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
151
|
+
### Protected Routes and Guards
|
|
391
152
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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"
|
|
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
|
-
|
|
169
|
+
When a user navigates to `/dashboard` while unauthenticated:
|
|
542
170
|
|
|
543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
Path parameters are extracted from Solid's reactive `useParams()` proxy — no URLPattern polyfill is needed for parameterized routes.
|
|
574
190
|
|
|
575
|
-
|
|
191
|
+
## Testing
|
|
576
192
|
|
|
577
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
**
|
|
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
|
-
```
|
|
609
|
-
|
|
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
|
-
**
|
|
209
|
+
Coverage thresholds: **80%** lines, functions, branches, and statements.
|
|
617
210
|
|
|
618
|
-
|
|
619
|
-
- `:param?` - Optional parameter (e.g., `/settings/:section?` matches `/settings` and `/settings/account`)
|
|
620
|
-
- Wildcards via `*` (future enhancement)
|
|
211
|
+
## Related Packages
|
|
621
212
|
|
|
622
|
-
|
|
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
|
-
|
|
625
|
-
const routeMap = new RouteMap([
|
|
626
|
-
{ stateId: "#profile", path: "/profile/:userId" },
|
|
627
|
-
{ stateId: "#settings", path: "/settings/:section?" },
|
|
628
|
-
]);
|
|
218
|
+
## Learn More
|
|
629
219
|
|
|
630
|
-
|
|
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
|
-
|
|
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).
|