@xmachines/play-solid 1.0.0-beta.16 → 1.0.0-beta.17
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/README.md +174 -220
- package/dist/PlayRenderer.js +52 -24
- package/dist/PlayRenderer.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/node_modules/@json-render/core/dist/chunk-AFLK3Q4T.js +111 -0
- package/dist/node_modules/@json-render/core/dist/chunk-AFLK3Q4T.js.map +1 -0
- package/dist/node_modules/@json-render/core/dist/index.js +863 -0
- package/dist/node_modules/@json-render/core/dist/index.js.map +1 -0
- package/dist/node_modules/@json-render/core/dist/store-utils.js +1 -0
- package/dist/node_modules/@json-render/solid/dist/chunk-XTSB6FIM.js +57 -0
- package/dist/node_modules/@json-render/solid/dist/chunk-XTSB6FIM.js.map +1 -0
- package/dist/node_modules/@json-render/solid/dist/index.js +617 -0
- package/dist/node_modules/@json-render/solid/dist/index.js.map +1 -0
- package/dist/node_modules/@json-render/xstate/dist/index.js +20 -0
- package/dist/node_modules/@json-render/xstate/dist/index.js.map +1 -0
- package/dist/node_modules/@xstate/store/dist/store-69e7e2d5.esm.js +227 -0
- package/dist/node_modules/@xstate/store/dist/store-69e7e2d5.esm.js.map +1 -0
- package/dist/node_modules/zod/v4/classic/errors.js +25 -0
- package/dist/node_modules/zod/v4/classic/errors.js.map +1 -0
- package/dist/node_modules/zod/v4/classic/iso.js +33 -0
- package/dist/node_modules/zod/v4/classic/iso.js.map +1 -0
- package/dist/node_modules/zod/v4/classic/parse.js +8 -0
- package/dist/node_modules/zod/v4/classic/parse.js.map +1 -0
- package/dist/node_modules/zod/v4/classic/schemas.js +362 -0
- package/dist/node_modules/zod/v4/classic/schemas.js.map +1 -0
- package/dist/node_modules/zod/v4/core/api.js +530 -0
- package/dist/node_modules/zod/v4/core/api.js.map +1 -0
- package/dist/node_modules/zod/v4/core/checks.js +285 -0
- package/dist/node_modules/zod/v4/core/checks.js.map +1 -0
- package/dist/node_modules/zod/v4/core/core.js +46 -0
- package/dist/node_modules/zod/v4/core/core.js.map +1 -0
- package/dist/node_modules/zod/v4/core/doc.js +25 -0
- package/dist/node_modules/zod/v4/core/doc.js.map +1 -0
- package/dist/node_modules/zod/v4/core/errors.js +43 -0
- package/dist/node_modules/zod/v4/core/errors.js.map +1 -0
- package/dist/node_modules/zod/v4/core/json-schema-processors.js +183 -0
- package/dist/node_modules/zod/v4/core/json-schema-processors.js.map +1 -0
- package/dist/node_modules/zod/v4/core/parse.js +70 -0
- package/dist/node_modules/zod/v4/core/parse.js.map +1 -0
- package/dist/node_modules/zod/v4/core/regexes.js +27 -0
- package/dist/node_modules/zod/v4/core/regexes.js.map +1 -0
- package/dist/node_modules/zod/v4/core/registries.js +42 -0
- package/dist/node_modules/zod/v4/core/registries.js.map +1 -0
- package/dist/node_modules/zod/v4/core/schemas.js +823 -0
- package/dist/node_modules/zod/v4/core/schemas.js.map +1 -0
- package/dist/node_modules/zod/v4/core/to-json-schema.js +224 -0
- package/dist/node_modules/zod/v4/core/to-json-schema.js.map +1 -0
- package/dist/node_modules/zod/v4/core/util.js +268 -0
- package/dist/node_modules/zod/v4/core/util.js.map +1 -0
- package/dist/node_modules/zod/v4/core/versions.js +10 -0
- package/dist/node_modules/zod/v4/core/versions.js.map +1 -0
- package/dist/useActor.js +12 -0
- package/dist/useActor.js.map +1 -0
- package/package.json +14 -5
package/README.md
CHANGED
|
@@ -1,299 +1,253 @@
|
|
|
1
1
|
# @xmachines/play-solid
|
|
2
2
|
|
|
3
|
-
**SolidJS renderer
|
|
3
|
+
**SolidJS renderer for XMachines Play Architecture**
|
|
4
4
|
|
|
5
|
-
Signal-driven SolidJS
|
|
5
|
+
Bridges TC39 Signal-driven actors to SolidJS's fine-grained reactivity. Business logic stays in the actor; Solid is purely a rendering target.
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
`@xmachines/play-solid` provides `PlayRenderer
|
|
9
|
+
`@xmachines/play-solid` provides `PlayRenderer`, a Solid component that:
|
|
10
10
|
|
|
11
|
-
|
|
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)
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
- **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
|
|
16
|
+
Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md):
|
|
15
17
|
|
|
16
|
-
**
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
19
21
|
|
|
20
22
|
## Installation
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
npm install solid-js@^1.8.0
|
|
24
25
|
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
|
|
25
28
|
```
|
|
26
29
|
|
|
27
30
|
## Current Exports
|
|
28
31
|
|
|
29
|
-
- `PlayRenderer`
|
|
32
|
+
- `PlayRenderer` — main renderer component
|
|
33
|
+
- `useActor` — hook for accessing the actor inside a `PlayRenderer` tree
|
|
34
|
+
- `defineRegistry` — re-exported from `@json-render/solid`
|
|
35
|
+
- `useStateBinding` — re-exported from `@json-render/solid`
|
|
36
|
+
- `ComponentFn` (type) — re-exported from `@json-render/solid`
|
|
37
|
+
- `ComponentContext` (type) — re-exported from `@json-render/solid`
|
|
30
38
|
- `PlayRendererProps` (type)
|
|
31
|
-
|
|
32
|
-
**Peer dependencies:**
|
|
33
|
-
|
|
34
|
-
- `solid-js` ^1.8.0 - SolidJS runtime
|
|
39
|
+
- `PlayActor` (type)
|
|
35
40
|
|
|
36
41
|
## Quick Start
|
|
37
42
|
|
|
38
43
|
```tsx
|
|
39
|
-
import {
|
|
40
|
-
import { definePlayer } from "@xmachines/play-xstate";
|
|
41
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
44
|
+
import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
42
45
|
import { PlayRenderer } from "@xmachines/play-solid";
|
|
46
|
+
import { defineCatalog } from "@json-render/core";
|
|
47
|
+
import { defineRegistry } from "@xmachines/play-solid";
|
|
48
|
+
import type { ComponentFn } from "@xmachines/play-solid";
|
|
49
|
+
import { setup, assign } from "xstate";
|
|
43
50
|
import { z } from "zod";
|
|
44
51
|
|
|
45
|
-
// 1. Define catalog
|
|
52
|
+
// 1. Define catalog — the contract between machine spec and UI components
|
|
46
53
|
const catalog = defineCatalog({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}),
|
|
54
|
+
elements: {
|
|
55
|
+
Login: { props: z.object({ title: z.string() }), description: "Login form" },
|
|
56
|
+
Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
|
|
57
|
+
},
|
|
52
58
|
});
|
|
53
59
|
|
|
54
|
-
// 2.
|
|
55
|
-
const
|
|
56
|
-
|
|
60
|
+
// 2. Implement components using ComponentFn — typed against catalog entries
|
|
61
|
+
const Login: ComponentFn<typeof catalog, "Login"> = ({ props, emit }) => (
|
|
62
|
+
<div class="view">
|
|
63
|
+
<h2>{props.title}</h2>
|
|
57
64
|
<form
|
|
58
65
|
onSubmit={(e) => {
|
|
59
66
|
e.preventDefault();
|
|
60
|
-
|
|
61
|
-
props.send({
|
|
62
|
-
type: "auth.login",
|
|
63
|
-
username: data.get("username"),
|
|
64
|
-
});
|
|
67
|
+
emit("submit");
|
|
65
68
|
}}
|
|
66
69
|
>
|
|
67
|
-
{props.error && <p style={{ color: "red" }}>{props.error}</p>}
|
|
68
|
-
<input name="username" required placeholder="Username" />
|
|
69
70
|
<button type="submit">Log In</button>
|
|
70
71
|
</form>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<div>
|
|
74
|
-
<h1>Welcome, {props.username}!</h1>
|
|
75
|
-
<p>User ID: {props.userId}</p>
|
|
76
|
-
<button onClick={() => props.send({ type: "auth.logout" })}>Log Out</button>
|
|
77
|
-
</div>
|
|
78
|
-
),
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// 3. Create player actor (business logic runtime)
|
|
82
|
-
const createPlayer = definePlayer({ machine: authMachine, catalog });
|
|
83
|
-
const actor = createPlayer();
|
|
84
|
-
actor.start();
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
() => <PlayRenderer actor={actor} components={components} />,
|
|
89
|
-
document.getElementById("app")!,
|
|
75
|
+
const Dashboard: ComponentFn<typeof catalog, "Dashboard"> = ({ props }) => (
|
|
76
|
+
<div class="view">Welcome, {props.username}!</div>
|
|
90
77
|
);
|
|
91
|
-
```
|
|
92
78
|
|
|
93
|
-
|
|
79
|
+
// 3. Build registry
|
|
80
|
+
const { registry } = defineRegistry(catalog, {
|
|
81
|
+
components: { Login, Dashboard },
|
|
82
|
+
actions: { login: async () => {}, logout: async () => {} },
|
|
83
|
+
});
|
|
94
84
|
|
|
95
|
-
|
|
85
|
+
// 4. Define machine with view metadata
|
|
86
|
+
const machine = setup({
|
|
87
|
+
types: {
|
|
88
|
+
context: {} as {
|
|
89
|
+
isAuthenticated: boolean;
|
|
90
|
+
username: string | null;
|
|
91
|
+
routeParams: Record<string, string>;
|
|
92
|
+
queryParams: Record<string, string>;
|
|
93
|
+
},
|
|
94
|
+
events: {} as
|
|
95
|
+
| { type: "auth.login"; username: string }
|
|
96
|
+
| { type: "auth.logout" }
|
|
97
|
+
| { type: "play.route"; to: string; params?: Record<string, string> },
|
|
98
|
+
},
|
|
99
|
+
}).createMachine(
|
|
100
|
+
formatPlayRouteTransitions({
|
|
101
|
+
id: "app",
|
|
102
|
+
initial: "login",
|
|
103
|
+
context: { isAuthenticated: false, username: null, routeParams: {}, queryParams: {} },
|
|
104
|
+
states: {
|
|
105
|
+
login: {
|
|
106
|
+
id: "login",
|
|
107
|
+
meta: {
|
|
108
|
+
route: "/login",
|
|
109
|
+
view: {
|
|
110
|
+
component: "Login",
|
|
111
|
+
spec: {
|
|
112
|
+
root: "root",
|
|
113
|
+
elements: {
|
|
114
|
+
root: { type: "Login", props: { title: "Sign In" }, children: [] },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
dashboard: {
|
|
121
|
+
id: "dashboard",
|
|
122
|
+
meta: {
|
|
123
|
+
route: "/dashboard",
|
|
124
|
+
view: {
|
|
125
|
+
component: "Dashboard",
|
|
126
|
+
spec: {
|
|
127
|
+
root: "root",
|
|
128
|
+
elements: {
|
|
129
|
+
root: { type: "Dashboard", props: { username: "" }, children: [] },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
on: {
|
|
137
|
+
"auth.login": {
|
|
138
|
+
target: ".dashboard",
|
|
139
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
140
|
+
actions: assign({ isAuthenticated: true, username: ({ event }) => event.username }),
|
|
141
|
+
},
|
|
142
|
+
"auth.logout": {
|
|
143
|
+
target: ".login",
|
|
144
|
+
guard: ({ context }) => context.isAuthenticated,
|
|
145
|
+
actions: assign({ isAuthenticated: false, username: null }),
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
96
150
|
|
|
97
|
-
|
|
151
|
+
// 5. Create actor and render
|
|
152
|
+
const createPlayer = definePlayer({ machine });
|
|
153
|
+
const actor = createPlayer();
|
|
154
|
+
actor.start();
|
|
98
155
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
156
|
+
function App() {
|
|
157
|
+
return (
|
|
158
|
+
<PlayRenderer
|
|
159
|
+
actor={actor}
|
|
160
|
+
registry={registry}
|
|
161
|
+
actions={{ login: "auth.login", logout: "auth.logout" }}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
104
164
|
}
|
|
105
165
|
```
|
|
106
166
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- `actor` - Actor instance with `currentView` signal
|
|
110
|
-
- `components` - Map of component names to SolidJS components
|
|
111
|
-
- `fallback` - Component shown when `currentView` is null
|
|
112
|
-
|
|
113
|
-
**Behavior:**
|
|
167
|
+
## API Reference
|
|
114
168
|
|
|
115
|
-
|
|
116
|
-
2. Looks up component from `components` map using `view.component` string
|
|
117
|
-
3. Renders component with props from `view.props` + `send` function using Solid's `<Dynamic />`
|
|
169
|
+
### `PlayRenderer`
|
|
118
170
|
|
|
119
|
-
|
|
171
|
+
Main component. Subscribes to `actor.currentView` and renders the spec.
|
|
120
172
|
|
|
121
173
|
```tsx
|
|
122
174
|
<PlayRenderer
|
|
123
175
|
actor={actor}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
fallback={<div>Loading...</div>}
|
|
176
|
+
registry={registry}
|
|
177
|
+
actions={{ login: "auth.login" }}
|
|
178
|
+
store={myStore}
|
|
179
|
+
fallback={<p>Loading…</p>}
|
|
129
180
|
/>
|
|
130
181
|
```
|
|
131
182
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
### Component Receiving Props from Catalog
|
|
135
|
-
|
|
136
|
-
```tsx
|
|
137
|
-
import { PlayRenderer } from "@xmachines/play-solid";
|
|
138
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
139
|
-
import { z } from "zod";
|
|
140
|
-
|
|
141
|
-
// Define schema in catalog
|
|
142
|
-
const catalog = defineCatalog({
|
|
143
|
-
UserProfile: z.object({
|
|
144
|
-
userId: z.string(),
|
|
145
|
-
name: z.string(),
|
|
146
|
-
avatar: z.string().url().optional(),
|
|
147
|
-
stats: z.object({
|
|
148
|
-
posts: z.number(),
|
|
149
|
-
followers: z.number(),
|
|
150
|
-
}),
|
|
151
|
-
}),
|
|
152
|
-
});
|
|
183
|
+
**`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
|
|
153
184
|
|
|
154
|
-
|
|
155
|
-
const components = {
|
|
156
|
-
UserProfile: (props) => (
|
|
157
|
-
<div>
|
|
158
|
-
{props.avatar && <img src={props.avatar} alt={props.name} />}
|
|
159
|
-
<h1>{props.name}</h1>
|
|
160
|
-
<p>ID: {props.userId}</p>
|
|
161
|
-
<div>
|
|
162
|
-
<span>{props.stats.posts} posts</span>
|
|
163
|
-
<span>{props.stats.followers} followers</span>
|
|
164
|
-
</div>
|
|
165
|
-
<button
|
|
166
|
-
onClick={() =>
|
|
167
|
-
props.send({
|
|
168
|
-
type: "profile.edit",
|
|
169
|
-
userId: props.userId,
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
>
|
|
173
|
-
Edit Profile
|
|
174
|
-
</button>
|
|
175
|
-
</div>
|
|
176
|
-
),
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
<PlayRenderer actor={actor} components={components} />;
|
|
180
|
-
```
|
|
185
|
+
**`registry`** — Built with `defineRegistry(catalog, { components, actions })` from `@xmachines/play-solid`.
|
|
181
186
|
|
|
182
|
-
|
|
187
|
+
**`actions`** — Maps json-render action names (from spec `on` bindings) to XState event type strings. Type-checked against `EventFromLogic<TLogic>["type"]` when `TLogic` is specified:
|
|
183
188
|
|
|
184
189
|
```tsx
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
function App() {
|
|
191
|
-
return (
|
|
192
|
-
<PlayTanStackRouterProvider
|
|
193
|
-
actor={actor}
|
|
194
|
-
router={router}
|
|
195
|
-
routeMap={routeMap}
|
|
196
|
-
renderer={(currentActor, currentRouter) => {
|
|
197
|
-
return (
|
|
198
|
-
<div>
|
|
199
|
-
<Header actor={currentActor} />
|
|
200
|
-
<PlayRenderer actor={currentActor} components={components} />
|
|
201
|
-
<Footer />
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}}
|
|
205
|
-
/>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Header component also receives actor
|
|
210
|
-
function Header(props) {
|
|
211
|
-
const [route, setRoute] = createSignal<string | null>(null);
|
|
212
|
-
|
|
213
|
-
// Manual watcher setup for custom component reading signals
|
|
214
|
-
let watcher: Signal.subtle.Watcher;
|
|
215
|
-
|
|
216
|
-
// ... setup watcher to update route signal ...
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<header>
|
|
220
|
-
<nav>Current: {route()}</nav>
|
|
221
|
-
</header>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
190
|
+
<PlayRenderer<typeof machine>
|
|
191
|
+
actor={actor}
|
|
192
|
+
registry={registry}
|
|
193
|
+
actions={{ login: "auth.login", logout: "auth.logout" }}
|
|
194
|
+
/>
|
|
224
195
|
```
|
|
225
196
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
This package implements **Signal-Only Reactivity (INV-05)** and **Passive Infrastructure (INV-04)**:
|
|
197
|
+
**`store`** (optional) — Controls per-view UI state (`$state` bindings, form values):
|
|
229
198
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- No createEffect for business side effects
|
|
233
|
-
- SolidJS only triggers renders, doesn't control state
|
|
199
|
+
- **Omitted (uncontrolled, default):** A fresh `@xstate/store` atom is created per view transition, seeded from `view.spec.state`.
|
|
200
|
+
- **Provided (controlled):** The caller owns the store; `spec.state` is ignored.
|
|
234
201
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
3. **Event Forwarding:**
|
|
241
|
-
- Components receive `send` function via props
|
|
242
|
-
- User actions send events to actor (e.g., `{ type: "auth.login" }`)
|
|
243
|
-
- Actor guards validate and process events
|
|
202
|
+
```tsx
|
|
203
|
+
import { createAtom } from "@xstate/store";
|
|
204
|
+
import { xstateStoreStateStore } from "@json-render/xstate";
|
|
205
|
+
import type { StateStore } from "@json-render/core";
|
|
244
206
|
|
|
245
|
-
|
|
246
|
-
- `Signal.subtle.Watcher` coalesces rapid signal changes
|
|
247
|
-
- Prevents SolidJS thrashing from multiple signal updates
|
|
248
|
-
- Single SolidJS render per microtask batch
|
|
207
|
+
const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
|
|
249
208
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
- Do not rely on GC-only cleanup
|
|
209
|
+
<PlayRenderer actor={actor} registry={registry} store={store} actions={{ login: "auth.login" }} />;
|
|
210
|
+
```
|
|
253
211
|
|
|
254
|
-
|
|
212
|
+
**`fallback`** — Shown when `actor.currentView.get()` is `null`.
|
|
255
213
|
|
|
256
|
-
|
|
257
|
-
- Enables composition with navigation, headers, footers
|
|
258
|
-
- Supports multiple renderers in same app
|
|
214
|
+
---
|
|
259
215
|
|
|
260
|
-
|
|
216
|
+
### `useActor`
|
|
261
217
|
|
|
262
|
-
|
|
263
|
-
- **Passive Infrastructure (INV-04):** Components reflect, never decide
|
|
218
|
+
Solid hook for accessing the actor from inside any component rendered by `PlayRenderer`. No prop drilling needed.
|
|
264
219
|
|
|
265
|
-
|
|
220
|
+
```tsx
|
|
221
|
+
import { useActor } from "@xmachines/play-solid";
|
|
266
222
|
|
|
267
|
-
|
|
223
|
+
// Inside any component rendered inside PlayRenderer:
|
|
224
|
+
function LogoutButton() {
|
|
225
|
+
const actor = useActor();
|
|
226
|
+
return <button onClick={() => actor.send({ type: "auth.logout" })}>Log Out</button>;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
268
229
|
|
|
269
|
-
|
|
270
|
-
2. Schedule work with `queueMicrotask`
|
|
271
|
-
3. Drain `watcher.getPending()`
|
|
272
|
-
4. Read actor signals and update SolidJS-local signal state (`setSignal()`)
|
|
273
|
-
5. Re-arm with `watch(...)` or `watch()`
|
|
230
|
+
Throws `"useActor() must be called inside <PlayRenderer>"` if called outside the tree.
|
|
274
231
|
|
|
275
|
-
|
|
232
|
+
---
|
|
276
233
|
|
|
277
|
-
##
|
|
234
|
+
## Route Parameters in Props
|
|
278
235
|
|
|
279
|
-
|
|
280
|
-
- **Type Safety:** Props validated against catalog schemas
|
|
281
|
-
- **Simple Testing:** Test actors without SolidJS renderer
|
|
282
|
-
- **Performance:** Microtask batching reduces unnecessary renders
|
|
283
|
-
- **Composability:** Renderer prop enables complex layouts
|
|
236
|
+
When using `formatPlayRouteTransitions`, URL path parameters flow automatically into component props. Declare an `undefined` slot in the spec to opt in:
|
|
284
237
|
|
|
285
|
-
|
|
238
|
+
```ts
|
|
239
|
+
// spec: { section: undefined, user: "alice" }
|
|
240
|
+
// After play.route to /settings/profile → context.routeParams = { section: "profile" }
|
|
241
|
+
// Component receives: { section: "profile", user: "alice" }
|
|
242
|
+
```
|
|
286
243
|
|
|
287
|
-
|
|
288
|
-
- **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
|
|
289
|
-
- **[@xmachines/play-solid-router](../play-solid-router)** - Solid Router integration
|
|
290
|
-
- **[@xmachines/play-tanstack-solid-router](../play-tanstack-solid-router)** - TanStack Solid Router integration
|
|
291
|
-
- **[@xmachines/play-actor](../play-actor)** - Actor base
|
|
292
|
-
- **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
|
|
244
|
+
Priority: **route param fills `undefined` slots; explicit non-`undefined` spec props always win.**
|
|
293
245
|
|
|
294
|
-
|
|
246
|
+
---
|
|
295
247
|
|
|
296
|
-
|
|
248
|
+
## Architecture Notes
|
|
297
249
|
|
|
298
|
-
|
|
299
|
-
|
|
250
|
+
- SolidJS signals are only used to trigger re-renders — not for business logic
|
|
251
|
+
- `actor.currentView` (TC39 Signal) is bridged into a SolidJS `createSignal` inside `PlayRenderer`
|
|
252
|
+
- Per-view UI state lives in an `@xstate/store` atom, not in SolidJS reactive state
|
|
253
|
+
- `@json-render/solid` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useStateBinding` from `@xmachines/play-solid`
|
package/dist/PlayRenderer.js
CHANGED
|
@@ -1,31 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { ActionProvider as e, Renderer as t, StateProvider as n, VisibilityProvider as r } from "./node_modules/@json-render/solid/dist/index.js";
|
|
2
|
+
import { createAtom as i } from "./node_modules/@xstate/store/dist/store-69e7e2d5.esm.js";
|
|
3
|
+
import { xstateStoreStateStore as a } from "./node_modules/@json-render/xstate/dist/index.js";
|
|
4
|
+
import { ActorProvider as o } from "./useActor.js";
|
|
5
|
+
import { createComponent as s } from "solid-js/web";
|
|
6
|
+
import { createSignal as c, onCleanup as l, onMount as u } from "solid-js";
|
|
7
|
+
import { watchSignal as d } from "@xmachines/play-signals";
|
|
4
8
|
//#region src/PlayRenderer.tsx
|
|
5
|
-
var
|
|
6
|
-
let [
|
|
7
|
-
|
|
8
|
-
let e =
|
|
9
|
-
|
|
10
|
-
e.getPending(), d(a.actor.currentView.get()), e.watch(a.actor.currentView);
|
|
11
|
-
});
|
|
9
|
+
var f = (f) => {
|
|
10
|
+
let [p, m] = c(f.actor.currentView.get()), h = null, g = null;
|
|
11
|
+
return u(() => {
|
|
12
|
+
let e = d(f.actor.currentView, (e) => {
|
|
13
|
+
m(e);
|
|
12
14
|
});
|
|
13
|
-
|
|
15
|
+
l(() => {
|
|
16
|
+
e();
|
|
17
|
+
});
|
|
18
|
+
}), s(o, {
|
|
19
|
+
get value() {
|
|
20
|
+
return f.actor;
|
|
21
|
+
},
|
|
22
|
+
get children() {
|
|
23
|
+
return (() => {
|
|
24
|
+
let o = p();
|
|
25
|
+
if (!o) return f.fallback ?? null;
|
|
26
|
+
let c;
|
|
27
|
+
f.store ? c = f.store : ((h === null || g !== o) && (h = a({ atom: i(o.spec.state ?? {}) }), g = o), c = h);
|
|
28
|
+
let l = Object.fromEntries(Object.entries(f.actions ?? {}).map(([e, t]) => [e, async (e = {}) => f.actor.send({
|
|
29
|
+
type: t,
|
|
30
|
+
...e
|
|
31
|
+
})]));
|
|
32
|
+
return s(n, {
|
|
33
|
+
store: c,
|
|
34
|
+
get children() {
|
|
35
|
+
return s(e, {
|
|
36
|
+
handlers: l,
|
|
37
|
+
get children() {
|
|
38
|
+
return s(r, { get children() {
|
|
39
|
+
return s(t, {
|
|
40
|
+
get spec() {
|
|
41
|
+
return o.spec;
|
|
42
|
+
},
|
|
43
|
+
get registry() {
|
|
44
|
+
return f.registry;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
})();
|
|
53
|
+
}
|
|
14
54
|
});
|
|
15
|
-
let f = a.actor.send.bind(a.actor);
|
|
16
|
-
return [
|
|
17
|
-
r(() => r(() => !u())() && (a.fallback || null)),
|
|
18
|
-
r(() => r(() => !!(u() && !a.components))() && (console.error(`Components catalog is ${a.components === null ? "null" : "undefined"}. Cannot render component "${u().component}".`), a.fallback || null)),
|
|
19
|
-
r(() => r(() => !!(u() && a.components && !a.components[u().component]))() && (console.error(`Component "${u().component}" not found in catalog. Available components: ${Object.keys(a.components).join(", ")}`), (() => {
|
|
20
|
-
var e = l(), t = e.firstChild.nextSibling;
|
|
21
|
-
return t.nextSibling, n(e, () => u().component, t), n(e, () => Object.keys(a.components).join(", "), null), e;
|
|
22
|
-
})())),
|
|
23
|
-
r(() => r(() => !!(u() && a.components && a.components[u().component]))() && t(e, i({ get component() {
|
|
24
|
-
return a.components[u().component];
|
|
25
|
-
} }, () => u().props, { send: f })))
|
|
26
|
-
];
|
|
27
55
|
};
|
|
28
56
|
//#endregion
|
|
29
|
-
export {
|
|
57
|
+
export { f as PlayRenderer };
|
|
30
58
|
|
|
31
59
|
//# sourceMappingURL=PlayRenderer.js.map
|
package/dist/PlayRenderer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayRenderer.js","names":["createSignal","onMount","Component","
|
|
1
|
+
{"version":3,"file":"PlayRenderer.js","names":["createSignal","onCleanup","onMount","Component","Renderer","StateProvider","ActionProvider","VisibilityProvider","ActionHandler","StateStore","createAtom","xstateStoreStateStore","watchSignal","PlayRendererProps","ViewMetadata","ActorProvider","PlayActor","PlayRenderer","props","view","setView","actor","currentView","get","internalStore","lastView","unwatch","nextView","_$createComponent","value","children","fallback","store","initialState","spec","state","Record","atom","handlers","Object","fromEntries","entries","actions","map","actionName","eventType","params","send","type","registry"],"sources":["../src/PlayRenderer.tsx"],"sourcesContent":["/**\n * PlayRenderer - Main SolidJS renderer component for XMachines Play architecture\n *\n * @packageDocumentation\n */\n\nimport { createSignal, onCleanup, onMount, type Component } from \"solid-js\";\nimport { Renderer, StateProvider, ActionProvider, VisibilityProvider } from \"@json-render/solid\";\nimport type { ActionHandler, StateStore } from \"@json-render/core\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { watchSignal } from \"@xmachines/play-signals\";\nimport type { PlayRendererProps } from \"./types.js\";\nimport type { ViewMetadata } from \"@xmachines/play-actor\";\nimport { ActorProvider, type PlayActor } from \"./useActor.js\";\n\n/**\n * Main renderer component that subscribes to actor signals and renders UI\n *\n * Architecture (per XMachines Play patterns):\n * - Subscribes to actor.currentView signal via TC39 Signal.subtle.Watcher\n * - Renders view.spec via @json-render/solid Renderer backed by the registry\n * - Routes actions to actor.send() via ActionProvider handlers\n * - SolidJS signal only for triggering renders, NOT business logic\n * - State store: uses external `store` prop if provided (controlled mode); otherwise\n * creates a fresh @xstate/store atom per view transition seeded from spec.state.\n *\n * Invariant: Actor Authority - Actor decides all state transitions via guards.\n * Invariant: Passive Infrastructure - Component observes signals and sends events.\n * Invariant: Signal-Only Reactivity - Business logic state lives in actor signals.\n *\n * @example\n * ```typescript\n * import { PlayRenderer } from \"@xmachines/play-solid\";\n * import { defineRegistry } from \"@json-render/solid\";\n *\n * const { registry } = defineRegistry(catalog, { components: { ... } });\n *\n * // Uncontrolled — fresh atom per view, seeded from spec.state:\n * <PlayRenderer actor={actor} registry={registry} actions={{ login: \"auth.login\" }} />\n *\n * // Controlled — caller provides and owns the store:\n * import { createAtom } from \"@xstate/store\";\n * import { xstateStoreStateStore } from \"@json-render/xstate\";\n * const store = xstateStoreStateStore({ atom: createAtom({ username: \"\" }) });\n * <PlayRenderer actor={actor} registry={registry} store={store} actions={{ login: \"auth.login\" }} />\n * ```\n */\nexport const PlayRenderer: Component<PlayRendererProps> = (props) => {\n\t// Create SolidJS signal for view\n\t// Signal is NOT business logic state - it's just SolidJS's render trigger\n\tconst [view, setView] = createSignal<ViewMetadata | null>(\n\t\tprops.actor.currentView.get() as ViewMetadata | null,\n\t);\n\n\t// Internal per-view store — recreated on each view transition when no external store.\n\tlet internalStore: StateStore | null = null;\n\tlet lastView: ViewMetadata | null = null;\n\n\t// Bridge TC39 Signal to SolidJS signal\n\tonMount(() => {\n\t\tconst unwatch = watchSignal(props.actor.currentView, (nextView: ViewMetadata | null) => {\n\t\t\tsetView(nextView);\n\t\t});\n\t\tonCleanup(() => {\n\t\t\tunwatch();\n\t\t});\n\t});\n\n\treturn (\n\t\t<ActorProvider value={props.actor as PlayActor}>\n\t\t\t{(() => {\n\t\t\t\tconst currentView = view();\n\t\t\t\tif (!currentView) return props.fallback ?? null;\n\n\t\t\t\t// Resolve the store: external (controlled) or internal per-view atom\n\t\t\t\tlet store: StateStore;\n\t\t\t\tif (props.store) {\n\t\t\t\t\tstore = props.store;\n\t\t\t\t} else {\n\t\t\t\t\tif (internalStore === null || lastView !== currentView) {\n\t\t\t\t\t\tconst initialState =\n\t\t\t\t\t\t\t(currentView.spec.state as Record<string, unknown>) ?? {};\n\t\t\t\t\t\tinternalStore = xstateStoreStateStore({\n\t\t\t\t\t\t\tatom: createAtom(initialState),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tlastView = currentView;\n\t\t\t\t\t}\n\t\t\t\t\tstore = internalStore;\n\t\t\t\t}\n\n\t\t\t\tconst handlers: Record<string, ActionHandler> = Object.fromEntries(\n\t\t\t\t\tObject.entries(props.actions ?? {}).map(([actionName, eventType]) => [\n\t\t\t\t\t\tactionName,\n\t\t\t\t\t\tasync (params: Record<string, unknown> = {}) =>\n\t\t\t\t\t\t\tprops.actor.send({ type: eventType, ...params }),\n\t\t\t\t\t]),\n\t\t\t\t);\n\n\t\t\t\treturn (\n\t\t\t\t\t<StateProvider store={store}>\n\t\t\t\t\t\t<ActionProvider handlers={handlers}>\n\t\t\t\t\t\t\t<VisibilityProvider>\n\t\t\t\t\t\t\t\t<Renderer spec={currentView.spec} registry={props.registry} />\n\t\t\t\t\t\t\t</VisibilityProvider>\n\t\t\t\t\t\t</ActionProvider>\n\t\t\t\t\t</StateProvider>\n\t\t\t\t);\n\t\t\t})()}\n\t\t</ActorProvider>\n\t);\n};\n"],"mappings":";;;;;;;;AAgDA,IAAaiB,KAA8CC,MAAU;CAGpE,IAAM,CAACC,GAAMC,KAAWpB,EACvBkB,EAAMG,MAAMC,YAAYC,KAAK,CAC7B,EAGGC,IAAmC,MACnCC,IAAgC;AAYpC,QATAvB,QAAc;EACb,IAAMwB,IAAUd,EAAYM,EAAMG,MAAMC,cAAcK,MAAkC;AACvFP,KAAQO,EAAS;IAChB;AACF1B,UAAgB;AACfyB,MAAS;IACR;GACD,EAEFE,EACEb,GAAa;EAAA,IAACc,QAAK;AAAA,UAAEX,EAAMG;;EAAkB,IAAAS,WAAA;AAAA,iBACrC;IACP,IAAMR,IAAcH,GAAM;AAC1B,QAAI,CAACG,EAAa,QAAOJ,EAAMa,YAAY;IAG3C,IAAIC;AACJ,IAAId,EAAMc,QACTA,IAAQd,EAAMc,UAEVR,MAAkB,QAAQC,MAAaH,OAG1CE,IAAgBb,EAAsB,EACrC0B,MAAM3B,EAFLY,EAAYY,KAAKC,SAAqC,EAAE,CAE5B,EAC7B,CAAC,EACFV,IAAWH,IAEZU,IAAQR;IAGT,IAAMc,IAA0CC,OAAOC,YACtDD,OAAOE,QAAQvB,EAAMwB,WAAW,EAAE,CAAC,CAACC,KAAK,CAACC,GAAYC,OAAe,CACpED,GACA,OAAOE,IAAkC,EAAE,KAC1C5B,EAAMG,MAAM0B,KAAK;KAAEC,MAAMH;KAAW,GAAGC;KAAQ,CAAC,CACjD,CACF,CAAC;AAED,WAAAlB,EACEvB,GAAa;KAAQ2B;KAAK,IAAAF,WAAA;AAAA,aAAAF,EACzBtB,GAAc;OAAWgC;OAAQ,IAAAR,WAAA;AAAA,eAAAF,EAChCrB,GAAkB,EAAA,IAAAuB,WAAA;AAAA,gBAAAF,EACjBxB,GAAQ;UAAA,IAAC8B,OAAI;AAAA,kBAAEZ,EAAYY;;UAAI,IAAEe,WAAQ;AAAA,kBAAE/B,EAAM+B;;UAAQ,CAAA;WAAA,CAAA;;OAAA,CAAA;;KAAA,CAAA;OAK3D;;EAAA,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { defineRegistry as e, useStateBinding as t } from "./node_modules/@json-render/solid/dist/index.js";
|
|
2
|
+
import { useActor as n } from "./useActor.js";
|
|
3
|
+
import { PlayRenderer as r } from "./PlayRenderer.js";
|
|
4
|
+
export { r as PlayRenderer, e as defineRegistry, n as useActor, t as useStateBinding };
|