@xmachines/play-xstate 1.0.0-beta.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/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +3 -0
- package/README.md +454 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.d.ts.map +1 -0
- package/dist/catalog/index.js +11 -0
- package/dist/catalog/index.js.map +1 -0
- package/dist/catalog/types.d.ts +36 -0
- package/dist/catalog/types.d.ts.map +1 -0
- package/dist/catalog/types.js +2 -0
- package/dist/catalog/types.js.map +1 -0
- package/dist/catalog/validate-binding.d.ts +21 -0
- package/dist/catalog/validate-binding.d.ts.map +1 -0
- package/dist/catalog/validate-binding.js +30 -0
- package/dist/catalog/validate-binding.js.map +1 -0
- package/dist/catalog/validate-props.d.ts +41 -0
- package/dist/catalog/validate-props.d.ts.map +1 -0
- package/dist/catalog/validate-props.js +95 -0
- package/dist/catalog/validate-props.js.map +1 -0
- package/dist/define-player.d.ts +110 -0
- package/dist/define-player.d.ts.map +1 -0
- package/dist/define-player.js +116 -0
- package/dist/define-player.js.map +1 -0
- package/dist/guards/compose.d.ts +136 -0
- package/dist/guards/compose.d.ts.map +1 -0
- package/dist/guards/compose.js +156 -0
- package/dist/guards/compose.js.map +1 -0
- package/dist/guards/helpers.d.ts +60 -0
- package/dist/guards/helpers.d.ts.map +1 -0
- package/dist/guards/helpers.js +91 -0
- package/dist/guards/helpers.js.map +1 -0
- package/dist/guards/index.d.ts +12 -0
- package/dist/guards/index.d.ts.map +1 -0
- package/dist/guards/index.js +11 -0
- package/dist/guards/index.js.map +1 -0
- package/dist/guards/types.d.ts +21 -0
- package/dist/guards/types.d.ts.map +1 -0
- package/dist/guards/types.js +2 -0
- package/dist/guards/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/player-actor.d.ts +143 -0
- package/dist/player-actor.d.ts.map +1 -0
- package/dist/player-actor.js +294 -0
- package/dist/player-actor.js.map +1 -0
- package/dist/routing/build-url.d.ts +27 -0
- package/dist/routing/build-url.d.ts.map +1 -0
- package/dist/routing/build-url.js +111 -0
- package/dist/routing/build-url.js.map +1 -0
- package/dist/routing/derive-route.d.ts +111 -0
- package/dist/routing/derive-route.d.ts.map +1 -0
- package/dist/routing/derive-route.js +144 -0
- package/dist/routing/derive-route.js.map +1 -0
- package/dist/routing/format-play-route-transitions.d.ts +31 -0
- package/dist/routing/format-play-route-transitions.d.ts.map +1 -0
- package/dist/routing/format-play-route-transitions.js +70 -0
- package/dist/routing/format-play-route-transitions.js.map +1 -0
- package/dist/routing/index.d.ts +13 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +12 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/routing/types.d.ts +25 -0
- package/dist/routing/types.d.ts.map +1 -0
- package/dist/routing/types.js +2 -0
- package/dist/routing/types.js.map +1 -0
- package/dist/signals/debounce.d.ts +18 -0
- package/dist/signals/debounce.d.ts.map +1 -0
- package/dist/signals/debounce.js +35 -0
- package/dist/signals/debounce.js.map +1 -0
- package/dist/signals/index.d.ts +3 -0
- package/dist/signals/index.d.ts.map +1 -0
- package/dist/signals/index.js +3 -0
- package/dist/signals/index.js.map +1 -0
- package/dist/signals/state-signal.d.ts +33 -0
- package/dist/signals/state-signal.d.ts.map +1 -0
- package/dist/signals/state-signal.js +41 -0
- package/dist/signals/state-signal.js.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/examples/simple-machine.ts +187 -0
- package/package.json +46 -0
- package/src/catalog/index.ts +12 -0
- package/src/catalog/types.ts +38 -0
- package/src/catalog/validate-binding.ts +35 -0
- package/src/catalog/validate-props.ts +109 -0
- package/src/define-player.ts +121 -0
- package/src/guards/compose.ts +169 -0
- package/src/guards/helpers.ts +104 -0
- package/src/guards/index.ts +12 -0
- package/src/guards/types.ts +23 -0
- package/src/index.ts +40 -0
- package/src/player-actor.ts +346 -0
- package/src/routing/build-url.ts +127 -0
- package/src/routing/derive-route.ts +152 -0
- package/src/routing/format-play-route-transitions.ts +77 -0
- package/src/routing/index.ts +13 -0
- package/src/routing/types.ts +26 -0
- package/src/signals/debounce.ts +38 -0
- package/src/signals/index.ts +2 -0
- package/src/signals/state-signal.ts +45 -0
- package/src/types.ts +47 -0
- package/test/derive-route.test.ts +166 -0
- package/test/devtools-integration.spec.ts +97 -0
- package/test/format-play-route-transitions-query.test.ts +187 -0
- package/test/guards-edge-cases.spec.ts +630 -0
- package/test/player-actor-basic.spec.ts +189 -0
- package/test/player-actor-edge-cases.spec.ts +769 -0
- package/test/routing-edge-cases.spec.ts +340 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +27 -0
package/.oxfmtrc.json
ADDED
package/.oxlintrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# @xmachines/play-xstate
|
|
2
|
+
|
|
3
|
+
**XState v5 adapter for Play Architecture with signal-driven reactivity and routing**
|
|
4
|
+
|
|
5
|
+
Transform declarative state machines into live actors with TC39 Signals and parameter-aware navigation.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`@xmachines/play-xstate` provides `definePlayer()`, the primary API for binding XState v5 state machines to the Play Architecture actor base. It enables business logic to control routing and state through guard-enforced transitions with catalog binding, signal lifecycle management, and XState DevTools compatibility.
|
|
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 guards decide navigation validity
|
|
14
|
+
- **Strict Separation (INV-02):** Zero React/framework imports in business logic
|
|
15
|
+
- **Signal-Only Reactivity (INV-05):** TC39 Signals expose all state changes
|
|
16
|
+
|
|
17
|
+
**Routing:** Supports `meta.route` patterns, `play.route` events with parameters, and route extraction.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install xstate@^5.0.0 zod@^3.23.0
|
|
23
|
+
npm install @xmachines/play-xstate
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Current Exports
|
|
27
|
+
|
|
28
|
+
- `definePlayer`, `PlayerActor`
|
|
29
|
+
- player types: `PlayerConfig`, `PlayerOptions`, `PlayerFactory`
|
|
30
|
+
- guard utilities: `composeGuards`, `composeGuardsOr`, `negateGuard`, `hasContext`, `eventMatches`, `stateMatches`
|
|
31
|
+
- routing utilities: `deriveRoute`, `isAbsoluteRoute`, `buildRouteUrl`, `formatPlayRouteTransitions`
|
|
32
|
+
- catalog utilities: `validateComponentBinding`, `validateViewProps`, `mergeViewProps`
|
|
33
|
+
|
|
34
|
+
**Peer dependencies:**
|
|
35
|
+
|
|
36
|
+
- `xstate` ^5.0.0 - State machine runtime
|
|
37
|
+
- `zod` ^3.23.0 - Schema validation for component props
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { setup } from "xstate";
|
|
43
|
+
import { z } from "zod";
|
|
44
|
+
import { definePlayer } from "@xmachines/play-xstate";
|
|
45
|
+
import { defineCatalog } from "@xmachines/play-catalog";
|
|
46
|
+
|
|
47
|
+
// 1. Define XState machine with meta.route
|
|
48
|
+
const machine = setup({
|
|
49
|
+
types: {
|
|
50
|
+
context: {} as { userId: string },
|
|
51
|
+
events: {} as { type: "play.route"; to: string } | { type: "auth.login"; userId: string },
|
|
52
|
+
},
|
|
53
|
+
guards: {
|
|
54
|
+
isLoggedIn: ({ context }) => !!context.userId,
|
|
55
|
+
},
|
|
56
|
+
}).createMachine({
|
|
57
|
+
id: "app",
|
|
58
|
+
initial: "login",
|
|
59
|
+
context: { userId: "" },
|
|
60
|
+
states: {
|
|
61
|
+
login: {
|
|
62
|
+
id: "login",
|
|
63
|
+
meta: {
|
|
64
|
+
route: "/login",
|
|
65
|
+
view: { component: "LoginForm" },
|
|
66
|
+
},
|
|
67
|
+
on: {
|
|
68
|
+
"auth.login": {
|
|
69
|
+
guard: "isLoggedIn",
|
|
70
|
+
target: "dashboard",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
dashboard: {
|
|
75
|
+
id: "dashboard",
|
|
76
|
+
meta: {
|
|
77
|
+
route: "/dashboard",
|
|
78
|
+
view: { component: "Dashboard", props: { userId: "" } },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 2. Define catalog with Zod schemas
|
|
85
|
+
const catalog = defineCatalog({
|
|
86
|
+
LoginForm: z.object({ error: z.string().optional() }),
|
|
87
|
+
Dashboard: z.object({ userId: z.string() }),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 3. Create player factory
|
|
91
|
+
const createPlayer = definePlayer({ machine, catalog });
|
|
92
|
+
|
|
93
|
+
// 4. Create and start actor
|
|
94
|
+
const actor = createPlayer({ userId: "" });
|
|
95
|
+
actor.start();
|
|
96
|
+
|
|
97
|
+
// 5. Send events (play.route with parameters)
|
|
98
|
+
actor.send({ type: "play.route", to: "/login" });
|
|
99
|
+
|
|
100
|
+
// 6. Observe signals
|
|
101
|
+
console.log(actor.currentRoute.get()); // "/login"
|
|
102
|
+
console.log(actor.currentView.get()); // { component: "LoginForm", props: {...} }
|
|
103
|
+
|
|
104
|
+
// 7. Cleanup
|
|
105
|
+
actor.dispose();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API Reference
|
|
109
|
+
|
|
110
|
+
### definePlayer()
|
|
111
|
+
|
|
112
|
+
Create a player factory from XState machine and catalog:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const createPlayer = definePlayer<TMachine, TCatalog>({
|
|
116
|
+
machine: AnyStateMachine,
|
|
117
|
+
catalog?: Catalog,
|
|
118
|
+
options?: PlayerOptions,
|
|
119
|
+
}): PlayerFactory;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Config:**
|
|
123
|
+
|
|
124
|
+
- `machine` (required) - XState v5 state machine
|
|
125
|
+
- `catalog` (optional) - UI component catalog with Zod schemas
|
|
126
|
+
- `options` (optional) - Lifecycle hooks
|
|
127
|
+
|
|
128
|
+
**Returns:** Factory function `(input?) => PlayerActor`
|
|
129
|
+
|
|
130
|
+
**Example:**
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const createPlayer = definePlayer({
|
|
134
|
+
machine: authMachine,
|
|
135
|
+
catalog: authCatalog,
|
|
136
|
+
options: {
|
|
137
|
+
onStart: (actor) => console.log("Started:", actor.id),
|
|
138
|
+
onTransition: (actor, prev, next) => {
|
|
139
|
+
console.log("Transition:", prev.value, "→", next.value);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const actor1 = createPlayer({ userId: "user1" });
|
|
145
|
+
const actor2 = createPlayer({ userId: "user2" });
|
|
146
|
+
// Multiple independent actor instances
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### PlayerActor
|
|
150
|
+
|
|
151
|
+
Concrete actor implementing Play signal protocol:
|
|
152
|
+
|
|
153
|
+
**Signal Properties:**
|
|
154
|
+
|
|
155
|
+
- `state: Signal.State<Snapshot>` - Reactive snapshot of current state
|
|
156
|
+
- `currentRoute: Signal.Computed<string | null>` - Derived navigation path
|
|
157
|
+
- `currentView: Signal.Computed<ViewStructure | null>` - Derived UI structure
|
|
158
|
+
|
|
159
|
+
**Actor Properties:**
|
|
160
|
+
|
|
161
|
+
- `catalog: Catalog` - Component catalog
|
|
162
|
+
|
|
163
|
+
**Methods:**
|
|
164
|
+
|
|
165
|
+
- `start()` - Start the actor (must call after creation)
|
|
166
|
+
- `stop()` - Stop the actor
|
|
167
|
+
- `send(event: PlayEvent)` - Send event to actor
|
|
168
|
+
- `dispose()` - Convenience cleanup (calls stop())
|
|
169
|
+
|
|
170
|
+
**Example:**
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const actor = createPlayer();
|
|
174
|
+
actor.start();
|
|
175
|
+
|
|
176
|
+
// Observe signals with watcher
|
|
177
|
+
const watcher = new Signal.subtle.Watcher(() => {
|
|
178
|
+
queueMicrotask(() => {
|
|
179
|
+
const route = actor.currentRoute.get();
|
|
180
|
+
console.log("Route changed:", route);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
watcher.watch(actor.currentRoute);
|
|
184
|
+
actor.currentRoute.get(); // Initial read
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Guard Composition
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import {
|
|
191
|
+
composeGuards,
|
|
192
|
+
composeGuardsOr,
|
|
193
|
+
negateGuard,
|
|
194
|
+
hasContext,
|
|
195
|
+
eventMatches,
|
|
196
|
+
stateMatches,
|
|
197
|
+
} from "@xmachines/play-xstate";
|
|
198
|
+
|
|
199
|
+
const machine = setup({
|
|
200
|
+
guards: {
|
|
201
|
+
isLoggedIn: hasContext("userId"),
|
|
202
|
+
isAdmin: ({ context }) => context.role === "admin",
|
|
203
|
+
},
|
|
204
|
+
}).createMachine({
|
|
205
|
+
on: {
|
|
206
|
+
accessAdmin: {
|
|
207
|
+
// Array means AND - all guards must pass
|
|
208
|
+
guard: composeGuards(["isLoggedIn", "isAdmin"]),
|
|
209
|
+
target: "adminPanel",
|
|
210
|
+
},
|
|
211
|
+
accessPublic: {
|
|
212
|
+
// OR composition - any guard passes
|
|
213
|
+
guard: composeGuardsOr(["isLoggedIn", ({ event }) => event.type === "guest.access"]),
|
|
214
|
+
target: "publicArea",
|
|
215
|
+
},
|
|
216
|
+
logout: {
|
|
217
|
+
// NOT composition
|
|
218
|
+
guard: negateGuard("isLoggedIn"),
|
|
219
|
+
target: "login",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Helpers:**
|
|
226
|
+
|
|
227
|
+
- `hasContext(path: string)` - Check if context property is truthy
|
|
228
|
+
- `eventMatches(type: string)` - Check event type
|
|
229
|
+
- `stateMatches(value: string)` - Check state value
|
|
230
|
+
- `composeGuards(guards: Array)` - AND composition
|
|
231
|
+
- `composeGuardsOr(guards: Array)` - OR composition
|
|
232
|
+
- `negateGuard(guard)` - NOT composition
|
|
233
|
+
|
|
234
|
+
**Complete API:** See [API Documentation](../../docs/api/@xmachines/play-xstate)
|
|
235
|
+
|
|
236
|
+
## Examples
|
|
237
|
+
|
|
238
|
+
### Guard Placement Philosophy
|
|
239
|
+
|
|
240
|
+
**Guards check if you can BE in a state (state entry), not if you can TAKE an event (event handlers).**
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { setup } from "xstate";
|
|
244
|
+
import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
245
|
+
import { defineCatalog } from "@xmachines/play-catalog";
|
|
246
|
+
|
|
247
|
+
// Pattern 1: RECOMMENDED - Use formatPlayRouteTransitions utility
|
|
248
|
+
const machineConfig = {
|
|
249
|
+
id: "app",
|
|
250
|
+
initial: "home",
|
|
251
|
+
context: { isAuthenticated: false },
|
|
252
|
+
states: {
|
|
253
|
+
home: {
|
|
254
|
+
id: "home",
|
|
255
|
+
meta: { route: "/", view: { component: "Home" } },
|
|
256
|
+
},
|
|
257
|
+
dashboard: {
|
|
258
|
+
id: "dashboard",
|
|
259
|
+
meta: { route: "/dashboard", view: { component: "Dashboard" } },
|
|
260
|
+
// Always-guard validates state entry
|
|
261
|
+
always: [
|
|
262
|
+
{
|
|
263
|
+
target: "login",
|
|
264
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
login: {
|
|
269
|
+
id: "login",
|
|
270
|
+
meta: { route: "/login", view: { component: "Login" } },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// formatPlayRouteTransitions handles routing infrastructure
|
|
276
|
+
const machine = setup({
|
|
277
|
+
types: {
|
|
278
|
+
events: {} as { type: "play.route"; to: string } | { type: "auth.login" },
|
|
279
|
+
},
|
|
280
|
+
}).createMachine(formatPlayRouteTransitions(machineConfig));
|
|
281
|
+
|
|
282
|
+
const catalog = defineCatalog({
|
|
283
|
+
Home,
|
|
284
|
+
Dashboard,
|
|
285
|
+
Login,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const createPlayer = definePlayer({ machine, catalog });
|
|
289
|
+
const actor = createPlayer();
|
|
290
|
+
actor.start();
|
|
291
|
+
|
|
292
|
+
// Navigation via play.route event
|
|
293
|
+
actor.send({ type: "play.route", to: "/dashboard" });
|
|
294
|
+
// Guard validates: Can I BE in dashboard state?
|
|
295
|
+
// If !isAuthenticated → redirects to login
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Why this works:**
|
|
299
|
+
|
|
300
|
+
- `formatPlayRouteTransitions` adds routing infrastructure (event.to → state mapping)
|
|
301
|
+
- Always-guards handle business logic (authentication checks)
|
|
302
|
+
- Clear separation: routing is infrastructure, guards are business logic
|
|
303
|
+
|
|
304
|
+
**Anti-pattern (DON'T DO THIS):**
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// ❌ WRONG - Guard on event checking event properties
|
|
308
|
+
on: {
|
|
309
|
+
"play.route": {
|
|
310
|
+
guard: ({ event }) => event.to === "/dashboard",
|
|
311
|
+
target: "dashboard"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Reference:** See `docs/examples/routing-patterns.md` for canonical `formatPlayRouteTransitions` usage with always-guards for authentication.
|
|
317
|
+
|
|
318
|
+
### Lifecycle Hooks
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const createPlayer = definePlayer({
|
|
322
|
+
machine,
|
|
323
|
+
catalog,
|
|
324
|
+
options: {
|
|
325
|
+
onStart: (actor) => {
|
|
326
|
+
console.log("Actor started:", actor.id);
|
|
327
|
+
},
|
|
328
|
+
onStop: (actor) => {
|
|
329
|
+
console.log("Actor stopped:", actor.id);
|
|
330
|
+
},
|
|
331
|
+
onTransition: (actor, prev, next) => {
|
|
332
|
+
console.log("State change:", {
|
|
333
|
+
from: prev.value,
|
|
334
|
+
to: next.value,
|
|
335
|
+
timestamp: Date.now(),
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
onStateChange: (actor, state) => {
|
|
339
|
+
// Called on every state update
|
|
340
|
+
console.log("Snapshot updated:", state.value);
|
|
341
|
+
},
|
|
342
|
+
onError: (actor, error) => {
|
|
343
|
+
console.error("Actor error:", error);
|
|
344
|
+
// Log to monitoring service, show error UI, etc.
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### XState DevTools Integration
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { createBrowserInspector } from "@statelyai/inspect";
|
|
354
|
+
import { definePlayer } from "@xmachines/play-xstate";
|
|
355
|
+
|
|
356
|
+
const { inspect } = createBrowserInspector();
|
|
357
|
+
|
|
358
|
+
const createPlayer = definePlayer({ machine, catalog });
|
|
359
|
+
const actor = createPlayer();
|
|
360
|
+
actor.start();
|
|
361
|
+
|
|
362
|
+
// PlayerActor maintains XState Inspector compatibility
|
|
363
|
+
// Inspector displays:
|
|
364
|
+
// - State transitions and values
|
|
365
|
+
// - Context data
|
|
366
|
+
// - Events sent to actor
|
|
367
|
+
// - Guard evaluation results
|
|
368
|
+
|
|
369
|
+
// Signals accessible via actor properties, not snapshots
|
|
370
|
+
console.log(actor.currentRoute.get()); // "/dashboard"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Metadata Conventions
|
|
374
|
+
|
|
375
|
+
### Route Metadata
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// meta.route marks states as routable
|
|
379
|
+
states: {
|
|
380
|
+
dashboard: {
|
|
381
|
+
id: "dashboard",
|
|
382
|
+
meta: {
|
|
383
|
+
route: "/dashboard", // URL path - marks state as routable
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Parameters
|
|
389
|
+
meta: {
|
|
390
|
+
route: "/profile/:userId", // Required parameter
|
|
391
|
+
route: "/settings/:section?", // Optional parameter
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Inheritance
|
|
395
|
+
meta: {
|
|
396
|
+
route: "/absolute", // Starts with / → doesn't inherit parent route
|
|
397
|
+
route: "relative", // Doesn't start with / → inherits parent route
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### View Metadata
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
meta: {
|
|
405
|
+
view: {
|
|
406
|
+
component: "Dashboard", // Must exist in catalog
|
|
407
|
+
props: { userId: "user123" }, // Validated against Zod schema
|
|
408
|
+
title: "Dashboard", // Additional metadata
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Dynamic props from context
|
|
413
|
+
meta: {
|
|
414
|
+
view: {
|
|
415
|
+
component: "Dashboard",
|
|
416
|
+
props: (context) => ({
|
|
417
|
+
userId: context.userId,
|
|
418
|
+
notifications: context.unreadCount,
|
|
419
|
+
}),
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Architecture
|
|
425
|
+
|
|
426
|
+
This package implements RFC Play v1 requirements:
|
|
427
|
+
|
|
428
|
+
**Architectural Invariants:**
|
|
429
|
+
|
|
430
|
+
- **Actor Authority (INV-01):** Guards decide navigation validity
|
|
431
|
+
- **Strict Separation (INV-02):** Zero framework imports
|
|
432
|
+
- **Signal-Only Reactivity (INV-05):** All state via TC39 Signals
|
|
433
|
+
|
|
434
|
+
**XState DevTools:** Maintains Inspector compatibility — snapshots remain pure XState format, signals accessible via actor properties.
|
|
435
|
+
|
|
436
|
+
**Routing:**
|
|
437
|
+
|
|
438
|
+
- `meta.route` property marks states as routable
|
|
439
|
+
- `play.route` events support parameters (enhancement)
|
|
440
|
+
- Route extraction for URL patterns
|
|
441
|
+
|
|
442
|
+
**Note:** Route parameter extraction uses URLPattern API. See [@xmachines/play-tanstack-react-router browser support](../play-tanstack-react-router/README.md#browser-support) for polyfill requirements.
|
|
443
|
+
|
|
444
|
+
## Related Packages
|
|
445
|
+
|
|
446
|
+
- **[@xmachines/play-actor](../play-actor)** - AbstractActor base class
|
|
447
|
+
- **[@xmachines/play-signals](../play-signals)** - TC39 Signals polyfill
|
|
448
|
+
- **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
|
|
449
|
+
- **[@xmachines/play-router](../play-router)** - Route extraction
|
|
450
|
+
- **[@xmachines/play](../play)** - Protocol types (PlayEvent)
|
|
451
|
+
|
|
452
|
+
## License
|
|
453
|
+
|
|
454
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog binding and validation utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for validating component references and props
|
|
5
|
+
* using Zod schemas from the catalog.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export { validateComponentBinding } from "./validate-binding.js";
|
|
10
|
+
export { validateViewProps, mergeViewProps } from "./validate-props.js";
|
|
11
|
+
export type { Catalog, CatalogEntry, ViewMetadata } from "./types.js";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/catalog/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACxE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog binding and validation utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for validating component references and props
|
|
5
|
+
* using Zod schemas from the catalog.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export { validateComponentBinding } from "./validate-binding.js";
|
|
10
|
+
export { validateViewProps, mergeViewProps } from "./validate-props.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/catalog/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Catalog as BaseCatalog } from "@xmachines/play-catalog";
|
|
3
|
+
/**
|
|
4
|
+
* UI component catalog entry with schema and component reference
|
|
5
|
+
*
|
|
6
|
+
* Per CONTEXT.md: String keys reference components, Zod schemas validate props
|
|
7
|
+
*/
|
|
8
|
+
export interface CatalogEntry {
|
|
9
|
+
/** Zod schema for component props */
|
|
10
|
+
schema: z.ZodType<any>;
|
|
11
|
+
/** React component (or other framework component) */
|
|
12
|
+
component: any;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Component catalog mapping
|
|
16
|
+
*
|
|
17
|
+
* Re-export from @xmachines/play-catalog for consistent API.
|
|
18
|
+
*
|
|
19
|
+
* Supports two formats:
|
|
20
|
+
* - Direct Zod schemas: `Record<string, z.ZodType>` via defineCatalog()
|
|
21
|
+
* - CatalogEntry objects: `Record<string, CatalogEntry>` with { schema, component }
|
|
22
|
+
*
|
|
23
|
+
* PlayerActor accepts either format - if entry has 'schema' property, uses CatalogEntry pattern,
|
|
24
|
+
* otherwise treats as direct Zod schema.
|
|
25
|
+
*/
|
|
26
|
+
export type Catalog = BaseCatalog | Record<string, CatalogEntry>;
|
|
27
|
+
/**
|
|
28
|
+
* View metadata from state machine
|
|
29
|
+
*/
|
|
30
|
+
export interface ViewMetadata {
|
|
31
|
+
/** Component name from catalog */
|
|
32
|
+
component: string;
|
|
33
|
+
/** Additional view props/data */
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/catalog/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B,qCAAqC;IACrC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvB,qDAAqD;IACrD,SAAS,EAAE,GAAG,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/catalog/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Catalog, ViewMetadata } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validate component binding against catalog
|
|
4
|
+
*
|
|
5
|
+
* Per CONTEXT.md:
|
|
6
|
+
* - "Component references: String keys"
|
|
7
|
+
* - "Mismatch detection: Both compile-time (TypeScript) and runtime validation"
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const view = { component: 'Dashboard', userId: '123' };
|
|
12
|
+
* validateComponentBinding(view, catalog);
|
|
13
|
+
* // Throws if 'Dashboard' not in catalog
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param view - View metadata from meta.view
|
|
17
|
+
* @param catalog - Component catalog
|
|
18
|
+
* @throws Error if component not found in catalog
|
|
19
|
+
*/
|
|
20
|
+
export declare const validateComponentBinding: (view: ViewMetadata, catalog: Catalog) => void;
|
|
21
|
+
//# sourceMappingURL=validate-binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-binding.d.ts","sourceRoot":"","sources":["../../src/catalog/validate-binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,YAAY,EAAE,SAAS,OAAO,KAAG,IAc/E,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate component binding against catalog
|
|
3
|
+
*
|
|
4
|
+
* Per CONTEXT.md:
|
|
5
|
+
* - "Component references: String keys"
|
|
6
|
+
* - "Mismatch detection: Both compile-time (TypeScript) and runtime validation"
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const view = { component: 'Dashboard', userId: '123' };
|
|
11
|
+
* validateComponentBinding(view, catalog);
|
|
12
|
+
* // Throws if 'Dashboard' not in catalog
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @param view - View metadata from meta.view
|
|
16
|
+
* @param catalog - Component catalog
|
|
17
|
+
* @throws Error if component not found in catalog
|
|
18
|
+
*/
|
|
19
|
+
export const validateComponentBinding = (view, catalog) => {
|
|
20
|
+
if (!view || !view.component) {
|
|
21
|
+
throw new Error("Invalid view metadata: missing 'component' property");
|
|
22
|
+
}
|
|
23
|
+
const { component } = view;
|
|
24
|
+
// Runtime validation
|
|
25
|
+
if (!(component in catalog)) {
|
|
26
|
+
const available = Object.keys(catalog).join(", ");
|
|
27
|
+
throw new Error(`Component "${component}" not found in catalog. Available components: ${available || "(none)"}`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=validate-binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-binding.js","sourceRoot":"","sources":["../../src/catalog/validate-binding.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,IAAkB,EAAE,OAAgB,EAAQ,EAAE;IACtF,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAE3B,qBAAqB;IACrB,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,IAAI,KAAK,CACd,cAAc,SAAS,iDAAiD,SAAS,IAAI,QAAQ,EAAE,CAC/F,CAAC;IACH,CAAC;AACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Catalog, ViewMetadata } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Validate view props against Zod schema
|
|
5
|
+
*
|
|
6
|
+
* Per CONTEXT.md:
|
|
7
|
+
* - "Prop validation: At state entry"
|
|
8
|
+
* - "currentView derivation: Merge meta.view with relevant context data"
|
|
9
|
+
*
|
|
10
|
+
* Per RESEARCH.md Pattern 4: Use safeParse() for validation
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const props = { userId: '123', stats: { logins: 5 } };
|
|
15
|
+
* const result = validateViewProps('Dashboard', props, catalog);
|
|
16
|
+
* if (!result.success) {
|
|
17
|
+
* console.error(result.error);
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param componentName - Component name from catalog
|
|
22
|
+
* @param props - Props to validate
|
|
23
|
+
* @param catalog - Component catalog with schemas
|
|
24
|
+
* @returns Zod parse result
|
|
25
|
+
*/
|
|
26
|
+
export declare const validateViewProps: (componentName: string, props: any, catalog: Catalog) => {
|
|
27
|
+
success: boolean;
|
|
28
|
+
data?: any;
|
|
29
|
+
error?: z.ZodError | Error;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Merge view metadata with context for props
|
|
33
|
+
*
|
|
34
|
+
* Per CONTEXT.md: "Merge meta.view with relevant context data for component props"
|
|
35
|
+
*
|
|
36
|
+
* @param view - View metadata from meta.view
|
|
37
|
+
* @param context - Machine context
|
|
38
|
+
* @returns Merged props object
|
|
39
|
+
*/
|
|
40
|
+
export declare const mergeViewProps: (view: ViewMetadata, context: any) => Record<string, any>;
|
|
41
|
+
//# sourceMappingURL=validate-props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-props.d.ts","sourceRoot":"","sources":["../../src/catalog/validate-props.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,eAAe,MAAM,EACrB,OAAO,GAAG,EACV,SAAS,OAAO,KACd;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,CAAC,QAAQ,GAAG,KAAK,CAAA;CA2B5D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,YAAY,EAAE,SAAS,GAAG,KAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAwCnF,CAAC"}
|