@xmachines/play-xstate 1.0.0-beta.2 → 1.0.0-beta.21

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.
Files changed (80) hide show
  1. package/README.md +263 -107
  2. package/dist/define-player.d.ts +6 -56
  3. package/dist/define-player.d.ts.map +1 -1
  4. package/dist/define-player.js +8 -60
  5. package/dist/define-player.js.map +1 -1
  6. package/dist/define-player.typecheck.d.ts +2 -0
  7. package/dist/define-player.typecheck.d.ts.map +1 -0
  8. package/dist/define-player.typecheck.js +48 -0
  9. package/dist/define-player.typecheck.js.map +1 -0
  10. package/dist/errors.d.ts +66 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +76 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/guards/compose.d.ts +14 -3
  15. package/dist/guards/compose.d.ts.map +1 -1
  16. package/dist/guards/compose.js +26 -0
  17. package/dist/guards/compose.js.map +1 -1
  18. package/dist/guards/helpers.d.ts +13 -17
  19. package/dist/guards/helpers.d.ts.map +1 -1
  20. package/dist/guards/helpers.js +20 -25
  21. package/dist/guards/helpers.js.map +1 -1
  22. package/dist/guards/index.d.ts +2 -1
  23. package/dist/guards/index.d.ts.map +1 -1
  24. package/dist/guards/index.js +1 -1
  25. package/dist/guards/index.js.map +1 -1
  26. package/dist/guards/types.d.ts +3 -2
  27. package/dist/guards/types.d.ts.map +1 -1
  28. package/dist/index.d.ts +7 -8
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -5
  31. package/dist/index.js.map +1 -1
  32. package/dist/player-actor.d.ts +70 -22
  33. package/dist/player-actor.d.ts.map +1 -1
  34. package/dist/player-actor.js +290 -88
  35. package/dist/player-actor.js.map +1 -1
  36. package/dist/player-actor.typecheck.d.ts +2 -0
  37. package/dist/player-actor.typecheck.d.ts.map +1 -0
  38. package/dist/player-actor.typecheck.js +27 -0
  39. package/dist/player-actor.typecheck.js.map +1 -0
  40. package/dist/routing/build-url.d.ts +22 -16
  41. package/dist/routing/build-url.d.ts.map +1 -1
  42. package/dist/routing/build-url.js +27 -20
  43. package/dist/routing/build-url.js.map +1 -1
  44. package/dist/routing/derive-route.d.ts +2 -2
  45. package/dist/routing/derive-route.d.ts.map +1 -1
  46. package/dist/routing/derive-route.js +3 -3
  47. package/dist/routing/derive-route.js.map +1 -1
  48. package/dist/routing/format-play-route-transitions.d.ts +41 -4
  49. package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
  50. package/dist/routing/format-play-route-transitions.js +22 -19
  51. package/dist/routing/format-play-route-transitions.js.map +1 -1
  52. package/dist/routing/index.d.ts +2 -1
  53. package/dist/routing/index.d.ts.map +1 -1
  54. package/dist/routing/types.d.ts +8 -13
  55. package/dist/routing/types.d.ts.map +1 -1
  56. package/dist/signals/index.d.ts +0 -1
  57. package/dist/signals/index.d.ts.map +1 -1
  58. package/dist/signals/index.js +0 -1
  59. package/dist/signals/index.js.map +1 -1
  60. package/dist/signals/state-signal.d.ts +1 -1
  61. package/dist/signals/state-signal.d.ts.map +1 -1
  62. package/dist/types.d.ts +20 -14
  63. package/dist/types.d.ts.map +1 -1
  64. package/package.json +26 -19
  65. package/dist/catalog/index.d.ts +0 -12
  66. package/dist/catalog/index.d.ts.map +0 -1
  67. package/dist/catalog/index.js +0 -11
  68. package/dist/catalog/index.js.map +0 -1
  69. package/dist/catalog/types.d.ts +0 -36
  70. package/dist/catalog/types.d.ts.map +0 -1
  71. package/dist/catalog/types.js +0 -2
  72. package/dist/catalog/types.js.map +0 -1
  73. package/dist/catalog/validate-binding.d.ts +0 -21
  74. package/dist/catalog/validate-binding.d.ts.map +0 -1
  75. package/dist/catalog/validate-binding.js +0 -30
  76. package/dist/catalog/validate-binding.js.map +0 -1
  77. package/dist/catalog/validate-props.d.ts +0 -41
  78. package/dist/catalog/validate-props.d.ts.map +0 -1
  79. package/dist/catalog/validate-props.js +0 -95
  80. package/dist/catalog/validate-props.js.map +0 -1
package/README.md CHANGED
@@ -6,9 +6,9 @@ Transform declarative state machines into live actors with TC39 Signals and para
6
6
 
7
7
  ## Overview
8
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.
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, signal lifecycle management, and XState DevTools compatibility.
10
10
 
11
- Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
11
+ Per [Play RFC](../docs/rfc/play.md), this package implements:
12
12
 
13
13
  - **Actor Authority (INV-01):** State machine guards decide navigation validity
14
14
  - **Strict Separation (INV-02):** Zero React/framework imports in business logic
@@ -19,89 +19,97 @@ Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md),
19
19
  ## Installation
20
20
 
21
21
  ```bash
22
- npm install xstate@^5.0.0 zod@^3.23.0
22
+ npm install xstate@^5.0.0
23
23
  npm install @xmachines/play-xstate
24
24
  ```
25
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
26
  **Peer dependencies:**
35
27
 
36
- - `xstate` ^5.0.0 - State machine runtime
37
- - `zod` ^3.23.0 - Schema validation for component props
28
+ - `xstate` ^5.0.0 State machine runtime
38
29
 
39
30
  ## Quick Start
40
31
 
41
32
  ```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";
33
+ import { setup, assign } from "xstate";
34
+ import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
46
35
 
47
- // 1. Define XState machine with meta.route
36
+ // 1. Define XState machine with meta.route and view spec
48
37
  const machine = setup({
49
38
  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,
39
+ context: {} as {
40
+ isAuthenticated: boolean;
41
+ routeParams: Record<string, string>;
42
+ queryParams: Record<string, string>;
43
+ },
44
+ events: {} as
45
+ | { type: "play.route"; to: string; params?: Record<string, string> }
46
+ | { type: "auth.login" }
47
+ | { type: "auth.logout" },
55
48
  },
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" },
49
+ }).createMachine(
50
+ formatPlayRouteTransitions({
51
+ id: "app",
52
+ initial: "login",
53
+ context: { isAuthenticated: false, routeParams: {}, queryParams: {} },
54
+ states: {
55
+ login: {
56
+ id: "login",
57
+ meta: {
58
+ route: "/login",
59
+ view: {
60
+ component: "Login",
61
+ spec: {
62
+ root: "root",
63
+ elements: {
64
+ root: { type: "Login", props: { title: "Sign In" }, children: [] },
65
+ },
66
+ },
67
+ },
68
+ },
66
69
  },
67
- on: {
68
- "auth.login": {
69
- guard: "isLoggedIn",
70
- target: "dashboard",
70
+ dashboard: {
71
+ id: "dashboard",
72
+ meta: {
73
+ route: "/dashboard",
74
+ view: {
75
+ component: "Dashboard",
76
+ spec: {
77
+ root: "root",
78
+ elements: {
79
+ root: { type: "Dashboard", props: {}, children: [] },
80
+ },
81
+ },
82
+ },
71
83
  },
84
+ always: [{ target: "login", guard: ({ context }) => !context.isAuthenticated }],
72
85
  },
73
86
  },
74
- dashboard: {
75
- id: "dashboard",
76
- meta: {
77
- route: "/dashboard",
78
- view: { component: "Dashboard", props: { userId: "" } },
87
+ on: {
88
+ "auth.login": {
89
+ target: ".dashboard",
90
+ guard: ({ context }) => !context.isAuthenticated,
91
+ actions: assign({ isAuthenticated: true }),
92
+ },
93
+ "auth.logout": {
94
+ target: ".login",
95
+ actions: assign({ isAuthenticated: false }),
79
96
  },
80
97
  },
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
- });
98
+ }),
99
+ );
89
100
 
90
- // 3. Create player factory
91
- const createPlayer = definePlayer({ machine, catalog });
101
+ // 2. Create player factory
102
+ const createPlayer = definePlayer({ machine });
92
103
 
93
- // 4. Create and start actor
94
- const actor = createPlayer({ userId: "" });
104
+ // 3. Create and start actor
105
+ const actor = createPlayer();
95
106
  actor.start();
96
107
 
97
- // 5. Send events (play.route with parameters)
98
- actor.send({ type: "play.route", to: "/login" });
99
-
100
- // 6. Observe signals
108
+ // 4. Observe signals
101
109
  console.log(actor.currentRoute.get()); // "/login"
102
- console.log(actor.currentView.get()); // { component: "LoginForm", props: {...} }
110
+ console.log(actor.currentView.get()); // { component: "Login", spec: {...} }
103
111
 
104
- // 7. Cleanup
112
+ // 5. Cleanup
105
113
  actor.dispose();
106
114
  ```
107
115
 
@@ -109,12 +117,11 @@ actor.dispose();
109
117
 
110
118
  ### definePlayer()
111
119
 
112
- Create a player factory from XState machine and catalog:
120
+ Create a player factory from an XState machine:
113
121
 
114
122
  ```typescript
115
- const createPlayer = definePlayer<TMachine, TCatalog>({
123
+ const createPlayer = definePlayer<TMachine>({
116
124
  machine: AnyStateMachine,
117
- catalog?: Catalog,
118
125
  options?: PlayerOptions,
119
126
  }): PlayerFactory;
120
127
  ```
@@ -122,17 +129,15 @@ const createPlayer = definePlayer<TMachine, TCatalog>({
122
129
  **Config:**
123
130
 
124
131
  - `machine` (required) - XState v5 state machine
125
- - `catalog` (optional) - UI component catalog with Zod schemas
126
132
  - `options` (optional) - Lifecycle hooks
127
133
 
128
- **Returns:** Factory function `(input?) => PlayerActor`
134
+ **Returns:** Factory function `(input?, { snapshot }?) => PlayerActor`
129
135
 
130
136
  **Example:**
131
137
 
132
138
  ```typescript
133
139
  const createPlayer = definePlayer({
134
140
  machine: authMachine,
135
- catalog: authCatalog,
136
141
  options: {
137
142
  onStart: (actor) => console.log("Started:", actor.id),
138
143
  onTransition: (actor, prev, next) => {
@@ -146,26 +151,49 @@ const actor2 = createPlayer({ userId: "user2" });
146
151
  // Multiple independent actor instances
147
152
  ```
148
153
 
154
+ **Restoring from a snapshot:**
155
+
156
+ ```typescript
157
+ const createPlayer = definePlayer({ machine });
158
+
159
+ const actor = createPlayer();
160
+ actor.start();
161
+
162
+ // ...persist the actor snapshot however your app stores it
163
+ const snapshot = actor.getSnapshot();
164
+ actor.stop();
165
+
166
+ const restoredActor = createPlayer(undefined, { snapshot });
167
+ restoredActor.start();
168
+ ```
169
+
149
170
  ### PlayerActor
150
171
 
151
172
  Concrete actor implementing Play signal protocol:
152
173
 
153
174
  **Signal Properties:**
154
175
 
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
176
+ - `state: Signal.State<AnyMachineSnapshot>` Reactive snapshot of current state
177
+ - `currentRoute: Signal.Computed<string | null>` Derived URL from the current state's `meta.route` and `context.routeParams`. Returns `null` when no route metadata is present or a required route parameter is missing from context.
178
+ - `currentView: Signal.State<ViewMetadata | null>` Current view spec (updated on every state transition). The spec is automatically enriched with `context.routeParams` (URL params) and any `contextProps`-allowlisted context fields before being emitted (see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context)).
158
179
 
159
- **Actor Properties:**
180
+ **Methods:**
160
181
 
161
- - `catalog: Catalog` - Component catalog
182
+ - `start()` Start the actor (must call after creation)
183
+ - `stop()` — Stop the actor
184
+ - `send(event)` — Send event to actor. Typed as `EventFromLogic<TMachine>` when `TMachine` is specified.
185
+ - `dispose()` — Convenience cleanup (calls `stop()`)
186
+ - `getSnapshot()` — Get current XState snapshot, typed as `SnapshotFrom<TMachine>`
162
187
 
163
- **Methods:**
188
+ **Routing failure behavior:**
189
+
190
+ - `currentRoute` returns `null` instead of throwing when a required route parameter (e.g. `:username`) is absent from context. This handles transient states during navigation without crashing signal watchers.
191
+ - Errors from `buildRouteUrl()` are typed as `MissingRouteParamError` (importable from `@xmachines/play-xstate/errors`).
164
192
 
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())
193
+ **View derivation and route-param enrichment:**
194
+
195
+ - `currentView` emits a fresh `ViewMetadata` object on every transition so TC39 Signal equality checks always detect changes (including re-entries to the same state with different params).
196
+ - `context.routeParams` and `contextProps`-allowlisted context fields are merged into spec element props — see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context).
169
197
 
170
198
  **Example:**
171
199
 
@@ -193,7 +221,7 @@ import {
193
221
  negateGuard,
194
222
  hasContext,
195
223
  eventMatches,
196
- stateMatches,
224
+ contextFieldMatches,
197
225
  } from "@xmachines/play-xstate";
198
226
 
199
227
  const machine = setup({
@@ -226,13 +254,11 @@ const machine = setup({
226
254
 
227
255
  - `hasContext(path: string)` - Check if context property is truthy
228
256
  - `eventMatches(type: string)` - Check event type
229
- - `stateMatches(value: string)` - Check state value
257
+ - `contextFieldMatches(fieldPath: string, expectedValue: unknown)` - Check context field with proper equality (no substring false-matches; use this instead of the removed `stateMatches`)
230
258
  - `composeGuards(guards: Array)` - AND composition
231
259
  - `composeGuardsOr(guards: Array)` - OR composition
232
260
  - `negateGuard(guard)` - NOT composition
233
261
 
234
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-xstate)
235
-
236
262
  ## Examples
237
263
 
238
264
  ### Guard Placement Philosophy
@@ -242,7 +268,6 @@ const machine = setup({
242
268
  ```typescript
243
269
  import { setup } from "xstate";
244
270
  import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
245
- import { defineCatalog } from "@xmachines/play-catalog";
246
271
 
247
272
  // Pattern 1: RECOMMENDED - Use formatPlayRouteTransitions utility
248
273
  const machineConfig = {
@@ -275,17 +300,18 @@ const machineConfig = {
275
300
  // formatPlayRouteTransitions handles routing infrastructure
276
301
  const machine = setup({
277
302
  types: {
278
- events: {} as { type: "play.route"; to: string } | { type: "auth.login" },
303
+ context: {} as {
304
+ isAuthenticated: boolean;
305
+ routeParams: Record<string, string>;
306
+ queryParams: Record<string, string>;
307
+ },
308
+ events: {} as
309
+ | { type: "play.route"; to: string; params?: Record<string, string> }
310
+ | { type: "auth.login" },
279
311
  },
280
312
  }).createMachine(formatPlayRouteTransitions(machineConfig));
281
313
 
282
- const catalog = defineCatalog({
283
- Home,
284
- Dashboard,
285
- Login,
286
- });
287
-
288
- const createPlayer = definePlayer({ machine, catalog });
314
+ const createPlayer = definePlayer({ machine });
289
315
  const actor = createPlayer();
290
316
  actor.start();
291
317
 
@@ -320,7 +346,6 @@ on: {
320
346
  ```typescript
321
347
  const createPlayer = definePlayer({
322
348
  machine,
323
- catalog,
324
349
  options: {
325
350
  onStart: (actor) => {
326
351
  console.log("Actor started:", actor.id);
@@ -355,7 +380,7 @@ import { definePlayer } from "@xmachines/play-xstate";
355
380
 
356
381
  const { inspect } = createBrowserInspector();
357
382
 
358
- const createPlayer = definePlayer({ machine, catalog });
383
+ const createPlayer = definePlayer({ machine });
359
384
  const actor = createPlayer();
360
385
  actor.start();
361
386
 
@@ -400,30 +425,155 @@ meta: {
400
425
 
401
426
  ### View Metadata
402
427
 
428
+ View metadata drives the `currentView` signal. The `spec` field follows the `@json-render/core` `Spec` shape:
429
+
403
430
  ```typescript
404
431
  meta: {
405
432
  view: {
406
- component: "Dashboard", // Must exist in catalog
407
- props: { userId: "user123" }, // Validated against Zod schema
408
- title: "Dashboard", // Additional metadata
433
+ component: "Dashboard", // Component name — must exist in the registry
434
+ spec: {
435
+ root: "root", // Root element key
436
+ state: { tab: "general" }, // Optional: initial UI state (form values etc.)
437
+ elements: {
438
+ root: {
439
+ type: "Dashboard",
440
+ props: { title: "Dashboard" }, // Static props (non-undefined = cannot be overridden by route params)
441
+ children: [],
442
+ },
443
+ },
444
+ },
409
445
  },
410
446
  }
447
+ ```
411
448
 
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
- },
449
+ **Prop slots:** Declare a prop as `undefined` to allow URL params or context fields to fill
450
+ it in automatically (see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context)):
451
+
452
+ ```typescript
453
+ // Route: /settings/:section? Context: { username: "alice" }
454
+ spec: {
455
+ root: "root",
456
+ contextProps: ["username"], // expose context.username to components
457
+ elements: {
458
+ root: { type: "Settings", props: { section: undefined, username: undefined, theme: "dark" }, children: [] },
459
+ },
460
+ }
461
+ // After play.route to /settings/profile:
462
+ // Component receives: { section: "profile", username: "alice", theme: "dark" }
463
+ // section → from routeParams (URL)
464
+ // username → from contextProps (context)
465
+ // theme → from spec (explicit value, untouched)
466
+ ```
467
+
468
+ ## Error Handling
469
+
470
+ All runtime errors thrown by this package extend `PlayError` from `@xmachines/play` and
471
+ are exported from the `./errors` subpath:
472
+
473
+ ```typescript
474
+ import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
475
+ ```
476
+
477
+ | Class | Code | When thrown |
478
+ | ------------------------ | -------------------------- | -------------------------------------------------------- |
479
+ | `MissingRouteParamError` | `PLAY_ROUTE_PARAM_MISSING` | Required `:param` has no matching value in actor context |
480
+ | `InvalidEventError` | `PLAY_INVALID_EVENT` | `actor.send()` called with a non-object value |
481
+
482
+ `MissingRouteParamError` carries `param` and `template` fields for programmatic access.
483
+ `InvalidEventError` carries a `detail` property with the offending value.
484
+
485
+ > **Note:** `currentRoute` does NOT throw `MissingRouteParamError` — it catches the error internally and returns `null` instead, keeping signal watchers stable during transient states.
486
+
487
+ ```typescript
488
+ import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
489
+
490
+ try {
491
+ actor.send(null as any);
492
+ } catch (err) {
493
+ if (err instanceof InvalidEventError) {
494
+ console.error("Non-object event passed to actor.send():", err.detail);
495
+ }
496
+ }
497
+
498
+ try {
499
+ const route = actor.currentRoute.get();
500
+ } catch (err) {
501
+ if (err instanceof MissingRouteParamError) {
502
+ console.error(`Missing "${err.param}" for template "${err.template}"`);
503
+ }
504
+ }
505
+ ```
506
+
507
+ ## Prop Enrichment from Routing and Context
508
+
509
+ When `currentView` is derived, each spec element's `props` are enriched from two sources.
510
+ Both use the **opt-in slot** pattern: declare a prop as `undefined` in the spec to allow
511
+ it to be filled in automatically.
512
+
513
+ ### Merge priority (highest → lowest)
514
+
515
+ | Source | When it applies | Wins over |
516
+ | --------------------------------------- | -------------------------------- | ------------------------- |
517
+ | Explicit non-`undefined` spec prop | Always | Everything |
518
+ | URL route param (`context.routeParams`) | State has a `:param` URL segment | `contextProps` values |
519
+ | `contextProps` field | Listed in `spec.contextProps` | Nothing (lowest priority) |
520
+
521
+ ### URL route parameters
522
+
523
+ URL path parameters (`:section?`, `:username`) are filled automatically — declare the prop
524
+ as `undefined` to opt in:
525
+
526
+ ```typescript
527
+ // Route: /settings/:section?
528
+ elements: {
529
+ root: { type: "Settings", props: { section: undefined, user: "alice" }, children: [] }
421
530
  }
531
+
532
+ // play.route event: { to: "#settings", params: { section: "profile" } }
533
+ // Component receives: { section: "profile", user: "alice" }
534
+ ```
535
+
536
+ Extra params not declared in the spec are also passed through:
537
+
538
+ ```typescript
539
+ // params: { section: "profile", tab: "security" }
540
+ // Component receives: { section: "profile", tab: "security", user: "alice" }
541
+ ```
542
+
543
+ ### Context fields via `contextProps`
544
+
545
+ For states where context data (e.g. `context.username`) should reach a component but there
546
+ is no corresponding URL param, declare `contextProps` in the spec as an explicit allowlist:
547
+
548
+ ```typescript
549
+ // Dashboard at /dashboard — no :username URL param, but we want to show context.username
550
+ spec: {
551
+ root: "root",
552
+ contextProps: ["username"], // ← explicit allowlist
553
+ elements: {
554
+ root: { type: "Dashboard", props: { username: undefined }, children: [] },
555
+ },
556
+ }
557
+
558
+ // After auth.login sets context.username = "alice":
559
+ // Component receives: { username: "alice" }
560
+ ```
561
+
562
+ **Nothing leaks from context without an explicit `contextProps` declaration.**
563
+ Only the named fields are ever exposed. `null` and `undefined` context values are excluded.
564
+
565
+ If both a route param and a `contextProps` field share the same key, the route param wins:
566
+
567
+ ```typescript
568
+ // context.username = "alice" (logged-in user)
569
+ // play.route to /profile/demo → routeParams.username = "demo"
570
+ // contextProps: ["username"], props: { username: undefined }
571
+ // Component receives: { username: "demo" } ← route param wins
422
572
  ```
423
573
 
424
574
  ## Architecture
425
575
 
426
- This package implements RFC Play v1 requirements:
576
+ This package implements Play RFC requirements:
427
577
 
428
578
  **Architectural Invariants:**
429
579
 
@@ -445,10 +595,16 @@ This package implements RFC Play v1 requirements:
445
595
 
446
596
  - **[@xmachines/play-actor](../play-actor)** - AbstractActor base class
447
597
  - **[@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
598
+ - **[@xmachines/play-router](../play-router)** - Route extraction utilities
599
+ - **[@xmachines/play-react](../play-react)** - React renderer
600
+ - **[@xmachines/play-vue](../play-vue)** - Vue renderer
601
+ - **[@xmachines/play-solid](../play-solid)** - SolidJS renderer
602
+ - **[@xmachines/play-dom](../play-dom)** - Vanilla DOM renderer
450
603
  - **[@xmachines/play](../play)** - Protocol types (PlayEvent)
451
604
 
452
605
  ## License
453
606
 
454
- MIT
607
+ Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
608
+
609
+ This work is licensed under the terms of the MIT license.
610
+ For a copy, see <https://opensource.org/licenses/MIT>.
@@ -1,29 +1,23 @@
1
1
  import type { AnyStateMachine } from "xstate";
2
2
  import type { PlayerConfig, PlayerFactory } from "./types.js";
3
3
  /**
4
- * Create a player factory from XState machine and catalog
4
+ * Create a player factory from an XState machine
5
5
  *
6
- * Factory pattern that accepts an XState v5 machine and optional UI catalog,
6
+ * Factory pattern that accepts an XState v5 machine,
7
7
  * returning a function that creates {@link PlayerActor} instances. This enables
8
8
  * creating multiple actor instances from a single configuration, useful for
9
9
  * testing, multi-instance scenarios, or server-side rendering.
10
10
  *
11
- * **Architectural Context:** Implements **Strict Separation (INV-02)** by accepting
12
- * the machine definition (pure business logic) separately from the catalog (UI vocabulary).
13
- * The machine references component names in `meta.view` without importing React/framework code.
14
- *
15
11
  * @typeParam TMachine - XState v5 state machine type
16
- * @typeParam TCatalog - Optional UI component catalog type
17
12
  *
18
13
  * @param config - Player configuration object
19
14
  * @param config.machine - XState v5 state machine
20
- * @param config.catalog - Optional UI component catalog (allows machines without UI)
21
15
  * @param config.options - Optional lifecycle hooks (onStart, onTransition, etc.)
22
16
  *
23
17
  * @returns Factory function that creates actor instances with optional input context
24
18
  *
25
19
  * @example
26
- * Basic player factory without catalog
20
+ * Basic player factory
27
21
  * ```typescript
28
22
  * import { setup } from "xstate";
29
23
  * import { definePlayer } from "@xmachines/play-xstate";
@@ -42,53 +36,9 @@ import type { PlayerConfig, PlayerFactory } from "./types.js";
42
36
  * ```
43
37
  *
44
38
  * @example
45
- * Player factory with catalog and parameter-aware routing
46
- * ```typescript
47
- * import { setup } from "xstate";
48
- * import { defineCatalog } from "@xmachines/play-catalog";
49
- * import { definePlayer } from "@xmachines/play-xstate";
50
- * import { z } from "zod";
51
- *
52
- * const catalog = defineCatalog({
53
- * HomePage: z.object({}),
54
- * ProfilePage: z.object({ userId: z.string() })
55
- * });
56
- *
57
- * const machine = setup({
58
- * types: {
59
- * context: {} as { userId: string },
60
- * events: {} as { type: 'play.route'; to: string; params?: Record<string, string> }
61
- * }
62
- * }).createMachine({
63
- * initial: 'home',
64
- * context: { userId: '' },
65
- * states: {
66
- * home: {
67
- * route: {},
68
- * meta: {
69
- * route: '/',
70
- * view: { component: 'HomePage' }
71
- * }
72
- * },
73
- * profile: {
74
- * route: {},
75
- * meta: {
76
- * route: '/profile/:userId',
77
- * view: { component: 'ProfilePage', userId: (ctx) => ctx.userId }
78
- * }
79
- * }
80
- * }
81
- * });
82
- *
83
- * const createPlayer = definePlayer({ machine, catalog });
84
- * const actor = createPlayer({ userId: 'user123' });
85
- * actor.start();
86
- * ```
87
- *
88
- * @example
89
39
  * Multiple actor instances from single factory
90
40
  * ```typescript
91
- * const createPlayer = definePlayer({ machine, catalog });
41
+ * const createPlayer = definePlayer({ machine });
92
42
  *
93
43
  * // Create actors for different users
94
44
  * const alice = createPlayer({ userId: 'alice' });
@@ -101,10 +51,10 @@ import type { PlayerConfig, PlayerFactory } from "./types.js";
101
51
  * console.log(alice.state.get() !== bob.state.get());
102
52
  * ```
103
53
  *
104
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1}
54
+ * @see [Play RFC](../../docs/rfc/play.md)
105
55
  * @see {@link PlayerActor} for the concrete actor implementation
106
56
  * @see {@link PlayerConfig} for configuration options
107
57
  * @see {@link PlayerFactory} for factory function signature
108
58
  */
109
- export declare const definePlayer: <TMachine extends AnyStateMachine, TCatalog = any>(config: PlayerConfig<TMachine, TCatalog>) => PlayerFactory;
59
+ export declare const definePlayer: <TMachine extends AnyStateMachine>(config: PlayerConfig<TMachine>) => PlayerFactory<TMachine>;
110
60
  //# sourceMappingURL=define-player.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-player.d.ts","sourceRoot":"","sources":["../src/define-player.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,eAAe,EAAE,QAAQ,GAAG,GAAG,EAC5E,QAAQ,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,KACtC,aAQF,CAAC"}
1
+ {"version":3,"file":"define-player.d.ts","sourceRoot":"","sources":["../src/define-player.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,QAAQ,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAA8B,MAAM,YAAY,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,eAAe,EAC5D,QAAQ,YAAY,CAAC,QAAQ,CAAC,KAC5B,aAAa,CAAC,QAAQ,CAMxB,CAAC"}