@xmachines/play-solid 1.0.0-beta.45 → 1.0.0-beta.47

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 (2) hide show
  1. package/README.md +117 -263
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -1,169 +1,56 @@
1
- # @xmachines/play-solid
2
-
3
- **Solid renderer for XMachines Play Architecture**
4
-
5
- Bridges TC39 Signal-driven actors to Solid's fine-grained reactivity. Business logic stays in the actor; Solid is purely a rendering target.
6
-
7
- ## Overview
1
+ <!-- generated-by: gsd-doc-writer -->
8
2
 
9
- `@xmachines/play-solid` provides `PlayRenderer`, a Solid component that:
3
+ # @xmachines/play-solid
10
4
 
11
- - Subscribes to `actor.currentView` (TC39 Signal) and re-renders on every state transition
12
- - Renders the current view's JSON spec via `@json-render/solid`
13
- - Routes action names from spec elements to `actor.send()` via the `actions` prop
14
- - Manages per-view UI state in an `@xstate/store` atom (automatic or caller-supplied)
5
+ > Solid renderer for XMachines Play architecture
15
6
 
16
- Per [Play RFC](../docs/rfc/play.md):
7
+ SolidJS rendering layer that passively observes actor signals and renders UI components via `@json-render/solid`. SolidJS reactivity is used solely to trigger re-renders — TC39 Signals are the source of truth.
17
8
 
18
- - **Actor Authority (INV-01):** Guards in the machine decide all state transitions
19
- - **Passive Infrastructure (INV-04):** Solid observes signals and dispatches events — never decides
20
- - **Signal-Only Reactivity (INV-05):** `actor.currentView` signal is the sole render trigger
9
+ Part of the [xmachines-js monorepo](../../README.md).
21
10
 
22
11
  ## Installation
23
12
 
24
13
  ```bash
25
14
  npm install @xmachines/play-solid
26
- npm install @json-render/solid @json-render/core # peer deps
27
- npm install @json-render/xstate @xstate/store # store integration
28
15
  ```
29
16
 
30
- In this monorepo, the root install applies a `patch-package` patch to `@json-render/solid`
31
- so `defineRegistry(..., { onRenderError })` can intercept inner element-boundary errors
32
- without muting console output.
33
-
34
- ## Current Exports
35
-
36
- - `PlayRenderer` — main renderer component
37
- - `useActor` — hook for accessing the actor inside a `PlayRenderer` tree
38
- - `defineRegistry` — re-exported from `@json-render/solid`
39
- - `useBoundProp` — re-exported from `@json-render/solid`
40
- - `ComponentFn` (type) — re-exported from `@json-render/solid`
41
- - `ComponentContext` (type) — re-exported from `@json-render/solid`
42
- - `ActorProvider` — escape hatch primitive (owns actor bridging, signal bridge, store lifecycle)
43
- - `PlayUIProvider` — batteries-included composite (wraps `ActorProvider` + `JSONUIProvider`)
44
- - `usePlayView` — hook for accessing the current view spec inside a provider tree
45
- - `RenderErrorHandler` (type) — inner per-element error callback signature
46
- - `ActorProviderProps` (type)
47
- - `ViewContextValue` (type)
48
- - `PlayActor` (type)
17
+ **Peer dependencies** install alongside the package:
18
+
19
+ ```bash
20
+ npm install solid-js xstate @xstate/store @json-render/solid @json-render/core @json-render/xstate
21
+ ```
49
22
 
50
23
  ## Quick Start
51
24
 
52
25
  ```tsx
53
- import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
54
- import { PlayRenderer } from "@xmachines/play-solid";
26
+ import { PlayUIProvider, PlayRenderer, defineRegistry } from "@xmachines/play-solid";
27
+ import { definePlayer } from "@xmachines/play-xstate";
55
28
  import { defineCatalog } from "@json-render/core";
56
- import { defineRegistry } from "@xmachines/play-solid";
57
- import type { ComponentFn } from "@xmachines/play-solid";
58
- import { setup, assign } from "xstate";
59
- import { z } from "zod";
60
-
61
- // 1. Define catalog the contract between machine spec and UI components
62
- const catalog = defineCatalog({
63
- elements: {
64
- Login: { props: z.object({ title: z.string() }), description: "Login form" },
65
- Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
29
+ import { schema } from "@json-render/solid/schema";
30
+
31
+ // 1. Define a catalog
32
+ const catalog = defineCatalog(schema, {
33
+ components: {
34
+ Home: { props: z.object({}), description: "Home screen" },
35
+ Login: { props: z.object({ error: z.string().optional() }), description: "Login screen" },
66
36
  },
37
+ actions: {},
67
38
  });
68
39
 
69
- // 2. Implement components using ComponentFn — typed against catalog entries
70
- const Login: ComponentFn<typeof catalog, "Login"> = ({ props, emit }) => (
71
- <div class="view">
72
- <h2>{props.title}</h2>
73
- <form
74
- onSubmit={(e) => {
75
- e.preventDefault();
76
- emit("submit");
77
- }}
78
- >
79
- <button type="submit">Log In</button>
80
- </form>
81
- </div>
82
- );
83
-
84
- const Dashboard: ComponentFn<typeof catalog, "Dashboard"> = ({ props }) => (
85
- <div class="view">Welcome, {props.username}!</div>
86
- );
87
-
88
- // 3. Build registry
40
+ // 2. Build a component registry
89
41
  const registryResult = defineRegistry(catalog, {
90
- components: { Login, Dashboard },
91
- actions: {
92
- login: async (params) => {
93
- if (!params) return;
94
- actor.send({ type: "auth.login", username: params.username });
95
- },
96
- logout: async (params) => {
97
- actor.send({ type: "auth.logout" });
98
- },
42
+ components: {
43
+ Home: () => <div>Welcome home!</div>,
44
+ Login: (ctx) => <div>Login {ctx.props.error && <span>{ctx.props.error}</span>}</div>,
99
45
  },
100
46
  });
101
47
 
102
- // 4. Define machine with view metadata
103
- const machine = setup({
104
- types: {
105
- context: {} as {
106
- isAuthenticated: boolean;
107
- username: string | null;
108
- params: Record<string, string>;
109
- query: Record<string, string>;
110
- },
111
- events: {} as
112
- | { type: "auth.login"; username: string }
113
- | { type: "auth.logout" }
114
- | { type: "play.route"; to: string; params?: Record<string, string> },
115
- },
116
- }).createMachine(
117
- formatPlayRouteTransitions({
118
- id: "app",
119
- initial: "login",
120
- context: { isAuthenticated: false, username: null, params: {}, query: {} },
121
- states: {
122
- login: {
123
- id: "login",
124
- meta: {
125
- route: "/login",
126
- view: {
127
- root: "root",
128
- elements: {
129
- root: { type: "Login", props: { title: "Sign In" }, children: [] },
130
- },
131
- },
132
- },
133
- },
134
- dashboard: {
135
- id: "dashboard",
136
- meta: {
137
- route: "/dashboard",
138
- view: {
139
- root: "root",
140
- elements: {
141
- root: { type: "Dashboard", props: { username: "" }, children: [] },
142
- },
143
- },
144
- },
145
- },
146
- },
147
- on: {
148
- "auth.login": {
149
- target: ".dashboard",
150
- guard: ({ context }) => !context.isAuthenticated,
151
- actions: assign({ isAuthenticated: true, username: ({ event }) => event.username }),
152
- },
153
- "auth.logout": {
154
- target: ".login",
155
- guard: ({ context }) => context.isAuthenticated,
156
- actions: assign({ isAuthenticated: false, username: null }),
157
- },
158
- },
159
- }),
160
- );
161
-
162
- // 5. Create actor and render
163
- const createPlayer = definePlayer({ machine });
48
+ // 3. Create and start an actor
49
+ const createPlayer = definePlayer({ machine: myMachine });
164
50
  const actor = createPlayer();
165
51
  actor.start();
166
52
 
53
+ // 4. Render
167
54
  function App() {
168
55
  return (
169
56
  <PlayUIProvider actor={actor} registryResult={registryResult}>
@@ -173,178 +60,145 @@ function App() {
173
60
  }
174
61
  ```
175
62
 
176
- ## API Reference
63
+ ## Usage
177
64
 
178
- ### `PlayUIProvider`
65
+ ### `PlayUIProvider` + `PlayRenderer` (recommended)
179
66
 
180
- Batteries-included composite provider. Wraps `ActorProvider` + `JSONUIProvider`. Pass `actor` and `registryResult` here, then place `<PlayRenderer />` inside as a zero-prop child.
67
+ `PlayUIProvider` is the batteries-included entry point. It wraps `ActorProvider` and `JSONUIProvider` into a single composite provider. `PlayRenderer` is a zero-prop leaf component that reads view context and renders the current spec.
181
68
 
182
69
  ```tsx
70
+ import { PlayUIProvider, PlayRenderer, defineRegistry } from "@xmachines/play-solid";
71
+
183
72
  <PlayUIProvider
184
73
  actor={actor}
185
74
  registryResult={registryResult}
186
- store={myStore}
187
- fallback={<p>Loading…</p>}
188
- onError={(err) => Sentry.captureException(err)}
189
- onRenderError={(error, elementType) => console.warn(`<${elementType}> crashed:`, error)}
75
+ fallback={<div>Loading…</div>}
76
+ onError={(err) => console.error(err)}
77
+ navigate={navigateFn} // optional: passed to JSONUIProvider
78
+ validationFunctions={valFns} // optional: form validation helpers
190
79
  >
191
80
  <PlayRenderer />
192
- </PlayUIProvider>
81
+ </PlayUIProvider>;
193
82
  ```
194
83
 
195
- **`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
84
+ ### `ActorProvider` (escape hatch)
196
85
 
197
- **`registryResult`** The full `DefineRegistryResult` returned by `defineRegistry(catalog, { components, actions })` from.
198
-
199
- **`store`** (optional) — Controls per-view UI state (`$state` bindings, form values):
200
-
201
- - **Omitted (uncontrolled, default):** A fresh `@xstate/store` atom is created per view transition, seeded from `view.spec.state`.
202
- - **Provided (controlled):** The caller owns the store; `spec.state` is ignored.
86
+ For library authors who need direct control over provider composition:
203
87
 
204
88
  ```tsx
205
- import { createAtom } from "@xstate/store";
206
- import { xstateStoreStateStore } from "@json-render/xstate";
207
- import type { StateStore } from "@json-render/core";
89
+ import { ActorProvider, PlayRenderer } from "@xmachines/play-solid";
208
90
 
209
- const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
210
-
211
- <PlayUIProvider actor={actor} registryResult={registryResult} store={store}>
91
+ <ActorProvider actor={actor} registryResult={registryResult}>
212
92
  <PlayRenderer />
213
- </PlayUIProvider>;
93
+ </ActorProvider>;
214
94
  ```
215
95
 
216
- **`fallback`** — Shown when `actor.currentView.get()` is `null`.
217
-
218
- **`onError`** — Called when the outer `ErrorBoundary` catches an error. Receives `(error: unknown)`. Use for observability tools.
219
-
220
- **`onRenderError`** — Called when an individual catalog component throws during render. Caught by `@json-render/solid`'s inner per-element `ErrorBoundary` — the failed component is silently removed while the rest of the spec continues rendering. `onError` / `fallback` are **not** triggered. When both `onRenderError` on `PlayUIProvider` and on `defineRegistry` are set, the prop wins.
221
-
222
- ---
96
+ ### `useActor` hook
223
97
 
224
- ### `ActorProvider`
225
-
226
- Escape hatch primitive. Owns actor bridging, signal bridge, and store lifecycle. Use this when you need direct control over the provider layer.
98
+ Access the raw actor instance anywhere inside an `ActorProvider` or `PlayUIProvider` tree:
227
99
 
228
100
  ```tsx
229
- import { ActorProvider } from "@xmachines/play-solid";
101
+ import { useActor } from "@xmachines/play-solid";
230
102
 
231
- <ActorProvider
232
- actor={actor}
233
- registryResult={registryResult}
234
- onRenderError={(err, type) => reportError(err, type)}
235
- >
236
- {/* your own JSONUIProvider + PlayRenderer tree */}
237
- </ActorProvider>;
103
+ function SubmitButton() {
104
+ const actor = useActor();
105
+ return <button onClick={() => actor.send({ type: "SUBMIT" })}>Submit</button>;
106
+ }
238
107
  ```
239
108
 
240
- ---
109
+ ### `usePlayView` hook
241
110
 
242
- ### `PlayRenderer`
243
-
244
- Zero-prop leaf component. Must be rendered inside a `PlayUIProvider` (or `ActorProvider`) tree. Subscribes to `actor.currentView` via context and renders the current spec.
111
+ Access the resolved view context (spec, handlers, registry) from within the provider tree:
245
112
 
246
113
  ```tsx
247
- <PlayUIProvider actor={actor} registryResult={registryResult}>
248
- <PlayRenderer />
249
- </PlayUIProvider>
250
- ```
114
+ import { usePlayView } from "@xmachines/play-solid";
115
+ import { Renderer } from "@json-render/solid";
251
116
 
252
- `PlayRenderer` accepts no props — all configuration (`actor`, `registryResult`, `store`, `fallback`, `onError`, `onRenderError`) is provided by the enclosing `PlayUIProvider` or `ActorProvider`.
253
-
254
- ## Error handling
255
-
256
- The provider tree has two layers of error boundaries:
257
-
258
- ### Outer boundary — `onError` and `fallback`
259
-
260
- Wraps the entire renderer via a SolidJS `ErrorBoundary`. Triggered when the spec or store setup throws, or when the inner boundary is not present.
261
-
262
- ```tsx
263
- <PlayUIProvider
264
- actor={actor}
265
- registryResult={registryResult}
266
- fallback={<p>Something went wrong.</p>}
267
- onError={(err) => Sentry.captureException(err)}
268
- >
269
- <PlayRenderer />
270
- </PlayUIProvider>
117
+ const MyRenderer = () => {
118
+ const view = usePlayView();
119
+ return <Renderer spec={view.spec} registry={view.registry} />;
120
+ };
271
121
  ```
272
122
 
273
- ### Inner boundary — `onRenderError`
123
+ ## API Summary
274
124
 
275
- Each catalog element is individually wrapped in a SolidJS `ErrorBoundary` by `@json-render/solid`. When a component throws, it is silently removed while the rest of the spec continues rendering. The outer boundary is **not** triggered.
125
+ ### Components
276
126
 
277
- Pass `onRenderError` to `PlayUIProvider` (or `ActorProvider`) — overrides any registry-level handler — or bake it into `defineRegistry`:
127
+ | Export | Description |
128
+ | ---------------- | ------------------------------------------------------------------------------ |
129
+ | `PlayUIProvider` | Batteries-included composite provider (recommended entry point) |
130
+ | `PlayRenderer` | Zero-prop leaf component; renders the current view spec inside a provider tree |
131
+ | `ActorProvider` | Lower-level smart provider for escape-hatch composition |
278
132
 
279
- ```tsx
280
- // via PlayUIProvider prop
281
- <PlayUIProvider
282
- actor={actor}
283
- registryResult={registryResult}
284
- onRenderError={(error, elementType) => {
285
- console.warn(`<${elementType}> crashed:`, error);
286
- }}
287
- >
288
- <PlayRenderer />
289
- </PlayUIProvider>
290
- ```
133
+ ### Hooks
291
134
 
292
- ```ts
293
- // via defineRegistry bakes the handler into the registry
294
- const registryResult = defineRegistry(catalog, {
295
- components: { Login, Dashboard },
296
- actions: { login: async (params) => { ... }, logout: async () => { ... } },
297
- onRenderError(error, elementType) {
298
- reportExpectedRenderError(error, elementType);
299
- },
300
- });
301
- ```
135
+ | Export | Description |
136
+ | --------------- | --------------------------------------------------------------------------------- |
137
+ | `useActor()` | Returns the raw `PlayActor` instance from context; throws outside a provider tree |
138
+ | `usePlayView()` | Returns the current `ViewContextValue` (spec, handlers, registry, store) |
302
139
 
303
- `onRenderError` is typed as `RenderErrorHandler` and exported from.
140
+ ### Context
304
141
 
305
- ---
142
+ | Export | Description |
143
+ | -------------- | -------------------------------------------------------------------------------------- |
144
+ | `ActorContext` | SolidJS context for the actor; use `ActorContext.Provider` directly as an escape hatch |
306
145
 
307
- ### `useActor`
146
+ ### Re-exports from `@json-render/solid`
308
147
 
309
- Solid hook for accessing the actor from inside any component rendered by `PlayRenderer`. No prop drilling needed.
148
+ This package re-exports the full `@json-render/solid` public API so consumers do not need a direct dependency:
310
149
 
311
150
  ```tsx
312
- import { useActor } from "@xmachines/play-solid";
313
-
314
- // Inside any component rendered inside PlayRenderer:
315
- function LogoutButton() {
316
- const actor = useActor();
317
- return <button onClick={() => actor.send({ type: "auth.logout" })}>Log Out</button>;
318
- }
151
+ import {
152
+ // Providers
153
+ JSONUIProvider,
154
+ StateProvider,
155
+ ActionProvider,
156
+ VisibilityProvider,
157
+ ValidationProvider,
158
+ // Renderer
159
+ Renderer,
160
+ // Registry factory + hooks
161
+ defineRegistry,
162
+ useBoundProp,
163
+ useStateBinding,
164
+ useStateValue,
165
+ useStateStore,
166
+ useActions,
167
+ useAction,
168
+ useIsVisible,
169
+ useFieldValidation,
170
+ useOptionalValidation,
171
+ useVisibility,
172
+ } from "@xmachines/play-solid";
319
173
  ```
320
174
 
321
- Throws `NonNullableError: "useActor() must be called inside <ActorProvider> (or <PlayUIProvider>)"` if called outside the tree.
175
+ ### Key Types
322
176
 
323
- ---
177
+ | Type | Description |
178
+ | --------------------- | --------------------------------------------------------------------- |
179
+ | `PlayUIProviderProps` | Props for `PlayUIProvider` |
180
+ | `ActorProviderProps` | Props for `ActorProvider` |
181
+ | `ViewContextValue` | Shape of the context value from `usePlayView()` |
182
+ | `PlayActor` | `AbstractActor<AnyActorLogic>` — the actor type accepted by providers |
324
183
 
325
- ## Route Parameters in Props
184
+ ## Testing
326
185
 
327
- When using `formatPlayRouteTransitions`, URL path parameters flow automatically into component props. Declare an `undefined` slot in the spec to opt in:
186
+ Run tests for this package in isolation:
328
187
 
329
- ```ts
330
- // spec: { section: undefined, user: "alice" }
331
- // After play.route to /settings/profile → context.params = { section: "profile" }
332
- // Component receives: { section: "profile", user: "alice" }
188
+ ```bash
189
+ npm test -w packages/play-solid
333
190
  ```
334
191
 
335
- Priority: **route param fills `undefined` slots; explicit non-`undefined` spec props always win.**
192
+ Or from within the package directory:
336
193
 
337
- ---
338
-
339
- ## Architecture Notes
194
+ ```bash
195
+ npm test # single run (jsdom environment)
196
+ npm run test:watch # watch mode
197
+ npm run test:ui # interactive Vitest UI
198
+ ```
340
199
 
341
- - SolidJS signals are only used to trigger re-renders not for business logic
342
- - `actor.currentView` (TC39 Signal) is bridged into a SolidJS `createSignal` inside `PlayRenderer`
343
- - Per-view UI state lives in an `@xstate/store` atom, not in SolidJS reactive state
344
- - `@json-render/solid` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useBoundProp` from
200
+ Coverage is collected with v8 (80% threshold for lines, functions, branches, and statements). Browser-specific tests live in `test/browser/` and are excluded from the default jsdom run.
345
201
 
346
- ## Learn More
202
+ ## License
347
203
 
348
- - [Demo](examples/demo/README.md)
349
- - [Solid Router adapter](../play-solid-router/README.md)
350
- - [TanStack Solid Router adapter](../play-tanstack-solid-router/README.md)
204
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-solid",
3
- "version": "1.0.0-beta.45",
3
+ "version": "1.0.0-beta.47",
4
4
  "description": "Solid renderer for XMachines Play architecture",
5
5
  "keywords": [
6
6
  "catalog",
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "npm run build"
40
40
  },
41
41
  "dependencies": {
42
- "@xmachines/play": "1.0.0-beta.45",
43
- "@xmachines/play-actor": "1.0.0-beta.45",
44
- "@xmachines/play-signals": "1.0.0-beta.45"
42
+ "@xmachines/play": "1.0.0-beta.47",
43
+ "@xmachines/play-actor": "1.0.0-beta.47",
44
+ "@xmachines/play-signals": "1.0.0-beta.47"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@json-render/core": "^0.18.0",
@@ -50,7 +50,7 @@
50
50
  "@solidjs/testing-library": "^0.8.10",
51
51
  "@testing-library/jest-dom": "^6.9.1",
52
52
  "@types/node": "^25.6.0",
53
- "@xmachines/shared": "1.0.0-beta.45",
53
+ "@xmachines/shared": "1.0.0-beta.47",
54
54
  "@xstate/store": "^3.17.0",
55
55
  "jsdom": "^29.0.2",
56
56
  "oxfmt": "^0.45.0",