@xmachines/play-solid 1.0.0-beta.6 → 1.0.0-beta.8
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 +251 -62
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
# @xmachines/play-solid
|
|
2
2
|
|
|
3
|
-
SolidJS renderer
|
|
3
|
+
**SolidJS renderer consuming signals and UI schema with provider pattern**
|
|
4
|
+
|
|
5
|
+
Signal-driven SolidJS rendering layer observing actor state with zero SolidJS state for business logic.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`@xmachines/play-solid` provides `PlayRenderer` for building SolidJS UIs that passively observe actor signals. This package enables framework-swappable architecture where SolidJS is just a rendering target subscribing to signal changes — business logic lives entirely in the actor.
|
|
10
|
+
|
|
11
|
+
Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
|
|
12
|
+
|
|
13
|
+
- **Signal-Only Reactivity (INV-05):** No createSignal/createStore for business logic, TC39 signals only
|
|
14
|
+
- **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
|
|
15
|
+
|
|
16
|
+
**Key Principle:** SolidJS state is never used for business logic. Signals are the source of truth.
|
|
17
|
+
|
|
18
|
+
Renderer receives actor via props (provider pattern), not children.
|
|
4
19
|
|
|
5
20
|
## Installation
|
|
6
21
|
|
|
7
22
|
```bash
|
|
8
|
-
npm install
|
|
23
|
+
npm install solid-js@^1.8.0
|
|
24
|
+
npm install @xmachines/play-solid
|
|
9
25
|
```
|
|
10
26
|
|
|
11
27
|
## Current Exports
|
|
@@ -13,98 +29,271 @@ npm install @xmachines/play-solid solid-js
|
|
|
13
29
|
- `PlayRenderer`
|
|
14
30
|
- `PlayRendererProps` (type)
|
|
15
31
|
|
|
16
|
-
|
|
32
|
+
**Peer dependencies:**
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import { defineCatalog } from '@xmachines/play-catalog';
|
|
34
|
+
- `solid-js` ^1.8.0 - SolidJS runtime
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
```tsx
|
|
39
|
+
import { render } from "solid-js/web";
|
|
40
|
+
import { definePlayer } from "@xmachines/play-xstate";
|
|
41
|
+
import { defineCatalog } from "@xmachines/play-catalog";
|
|
42
|
+
import { PlayRenderer } from "@xmachines/play-solid";
|
|
43
|
+
import { z } from "zod";
|
|
44
|
+
|
|
45
|
+
// 1. Define catalog (business logic layer)
|
|
24
46
|
const catalog = defineCatalog({
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
LoginForm: z.object({ error: z.string().optional() }),
|
|
48
|
+
Dashboard: z.object({
|
|
49
|
+
userId: z.string(),
|
|
50
|
+
username: z.string(),
|
|
51
|
+
}),
|
|
27
52
|
});
|
|
28
53
|
|
|
29
|
-
// Create
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
// 2. Create SolidJS components (view layer)
|
|
55
|
+
const components = {
|
|
56
|
+
LoginForm: (props) => (
|
|
57
|
+
<form
|
|
58
|
+
onSubmit={(e) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
const data = new FormData(e.currentTarget);
|
|
61
|
+
props.send({
|
|
62
|
+
type: "auth.login",
|
|
63
|
+
username: data.get("username"),
|
|
64
|
+
});
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{props.error && <p style={{ color: "red" }}>{props.error}</p>}
|
|
68
|
+
<input name="username" required placeholder="Username" />
|
|
69
|
+
<button type="submit">Log In</button>
|
|
70
|
+
</form>
|
|
71
|
+
),
|
|
72
|
+
Dashboard: (props) => (
|
|
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
|
+
};
|
|
34
80
|
|
|
81
|
+
// 3. Create player actor (business logic runtime)
|
|
82
|
+
const createPlayer = definePlayer({ machine: authMachine, catalog });
|
|
35
83
|
const actor = createPlayer();
|
|
36
84
|
actor.start();
|
|
37
85
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
props.send({ type: 'auth.login', payload: {...} });
|
|
45
|
-
}}>
|
|
46
|
-
{props.error && <p>{props.error}</p>}
|
|
47
|
-
<input type="text" name="username" />
|
|
48
|
-
<button type="submit">Login</button>
|
|
49
|
-
</form>
|
|
50
|
-
)
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Render
|
|
54
|
-
<PlayRenderer actor={actor} components={components} />
|
|
86
|
+
// 4. Render UI (actor via props)
|
|
87
|
+
render(
|
|
88
|
+
() => <PlayRenderer actor={actor} components={components} />,
|
|
89
|
+
document.getElementById("app")!,
|
|
90
|
+
);
|
|
55
91
|
```
|
|
56
92
|
|
|
57
|
-
## API
|
|
93
|
+
## API Reference
|
|
58
94
|
|
|
59
95
|
### PlayRenderer
|
|
60
96
|
|
|
61
|
-
|
|
97
|
+
Main renderer component subscribing to actor signals and dynamically rendering catalog components:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
interface PlayRendererProps {
|
|
101
|
+
actor: AbstractActor<any>;
|
|
102
|
+
components: Record<string, Component<any>>;
|
|
103
|
+
fallback?: JSX.Element;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
62
106
|
|
|
63
107
|
**Props:**
|
|
64
108
|
|
|
65
|
-
- `actor
|
|
66
|
-
- `components
|
|
67
|
-
- `fallback
|
|
109
|
+
- `actor` - Actor instance with `currentView` signal
|
|
110
|
+
- `components` - Map of component names to SolidJS components
|
|
111
|
+
- `fallback` - Component shown when `currentView` is null
|
|
68
112
|
|
|
69
|
-
**
|
|
113
|
+
**Behavior:**
|
|
70
114
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- Uses one-shot watcher re-watch pattern for proper signal observation
|
|
115
|
+
1. Subscribes to `actor.currentView` signal using a `Signal.subtle.Watcher` inside the component
|
|
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 />`
|
|
75
118
|
|
|
76
|
-
|
|
119
|
+
**Example:**
|
|
77
120
|
|
|
78
|
-
|
|
121
|
+
```tsx
|
|
122
|
+
<PlayRenderer
|
|
123
|
+
actor={actor}
|
|
124
|
+
components={{
|
|
125
|
+
HomePage: (props) => <div>Home</div>,
|
|
126
|
+
AboutPage: (props) => <div>About</div>,
|
|
127
|
+
}}
|
|
128
|
+
fallback={<div>Loading...</div>}
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
79
131
|
|
|
80
|
-
|
|
81
|
-
2. `queueMicrotask`
|
|
82
|
-
3. `getPending()`
|
|
83
|
-
4. read actor signals and update framework-local render trigger
|
|
84
|
-
5. re-arm with `watch(...)` or `watch()`
|
|
132
|
+
## Examples
|
|
85
133
|
|
|
86
|
-
|
|
134
|
+
### Component Receiving Props from Catalog
|
|
87
135
|
|
|
88
|
-
|
|
136
|
+
```tsx
|
|
137
|
+
import { PlayRenderer } from "@xmachines/play-solid";
|
|
138
|
+
import { defineCatalog } from "@xmachines/play-catalog";
|
|
139
|
+
import { z } from "zod";
|
|
89
140
|
|
|
90
|
-
|
|
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
|
+
});
|
|
91
153
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
154
|
+
// Component receives type-safe props + send
|
|
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
|
+
```
|
|
181
|
+
|
|
182
|
+
### Provider Pattern
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { PlayTanStackRouterProvider } from "@xmachines/play-tanstack-solid-router";
|
|
186
|
+
import { PlayRenderer } from "@xmachines/play-solid";
|
|
187
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
188
|
+
|
|
189
|
+
// Renderer receives actor via props (not children)
|
|
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
|
+
}
|
|
224
|
+
```
|
|
95
225
|
|
|
96
226
|
## Architecture
|
|
97
227
|
|
|
98
|
-
|
|
228
|
+
This package implements **Signal-Only Reactivity (INV-05)** and **Passive Infrastructure (INV-04)**:
|
|
229
|
+
|
|
230
|
+
1. **No Business Logic in SolidJS:**
|
|
231
|
+
- No createSignal/createStore for business state
|
|
232
|
+
- No createEffect for business side effects
|
|
233
|
+
- SolidJS only triggers renders, doesn't control state
|
|
234
|
+
|
|
235
|
+
2. **Signals as Source of Truth:**
|
|
236
|
+
- `actor.currentView.get()` provides UI structure
|
|
237
|
+
- `actor.currentRoute.get()` provides navigation state
|
|
238
|
+
- Components observe signals via explicit watcher patterns
|
|
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
|
|
244
|
+
|
|
245
|
+
4. **Microtask Batching:**
|
|
246
|
+
- `Signal.subtle.Watcher` coalesces rapid signal changes
|
|
247
|
+
- Prevents SolidJS thrashing from multiple signal updates
|
|
248
|
+
- Single SolidJS render per microtask batch
|
|
249
|
+
|
|
250
|
+
5. **Explicit Disposal Contract:**
|
|
251
|
+
- Component teardown calls watcher `unwatch` in `onCleanup`
|
|
252
|
+
- Do not rely on GC-only cleanup
|
|
99
253
|
|
|
100
|
-
|
|
101
|
-
- **Passive Infrastructure**: Renderer observes signals, sends events
|
|
102
|
-
- **Signal-Only Reactivity**: Business logic state lives in actor signals
|
|
254
|
+
**Pattern:**
|
|
103
255
|
|
|
104
|
-
|
|
256
|
+
- Renderer receives actor via props (provider pattern)
|
|
257
|
+
- Enables composition with navigation, headers, footers
|
|
258
|
+
- Supports multiple renderers in same app
|
|
105
259
|
|
|
106
|
-
|
|
260
|
+
**Architectural Invariants:**
|
|
261
|
+
|
|
262
|
+
- **Signal-Only Reactivity (INV-05):** No SolidJS state for business logic
|
|
263
|
+
- **Passive Infrastructure (INV-04):** Components reflect, never decide
|
|
264
|
+
|
|
265
|
+
## Canonical Watcher Lifecycle
|
|
266
|
+
|
|
267
|
+
If you write your own custom integration, use the same watcher flow as `PlayRenderer`:
|
|
268
|
+
|
|
269
|
+
1. `notify` callback runs
|
|
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()`
|
|
274
|
+
|
|
275
|
+
Watcher notify is one-shot. Re-arm is required for continuous observation.
|
|
276
|
+
|
|
277
|
+
## Benefits
|
|
278
|
+
|
|
279
|
+
- **Framework Swappable:** Business logic has zero SolidJS imports
|
|
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
|
|
284
|
+
|
|
285
|
+
## Related Packages
|
|
286
|
+
|
|
287
|
+
- **[@xmachines/play-xstate](../play-xstate)** - XState adapter providing actors
|
|
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
|
|
107
293
|
|
|
108
294
|
## License
|
|
109
295
|
|
|
110
|
-
|
|
296
|
+
Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
|
|
297
|
+
|
|
298
|
+
This work is licensed under the terms of the MIT license.
|
|
299
|
+
For a copy, see <https://opensource.org/licenses/MIT>.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmachines/play-solid",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.8",
|
|
4
4
|
"description": "SolidJS renderer for XMachines Play architecture",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"catalog",
|
|
@@ -29,22 +29,22 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "vite build && tsc --build",
|
|
32
|
-
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
32
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo node_modules/.vite node_modules/.vite-temp",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
|
-
"test": "vitest
|
|
34
|
+
"test": "vitest",
|
|
35
35
|
"test:watch": "vitest",
|
|
36
36
|
"test:ui": "vitest --ui",
|
|
37
37
|
"prepublishOnly": "npm run build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@xmachines/play-actor": "1.0.0-beta.
|
|
41
|
-
"@xmachines/play-catalog": "1.0.0-beta.
|
|
42
|
-
"@xmachines/play-signals": "1.0.0-beta.
|
|
40
|
+
"@xmachines/play-actor": "1.0.0-beta.8",
|
|
41
|
+
"@xmachines/play-catalog": "1.0.0-beta.8",
|
|
42
|
+
"@xmachines/play-signals": "1.0.0-beta.8"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@solidjs/testing-library": "^0.8.10",
|
|
46
46
|
"@types/node": "^25.5.0",
|
|
47
|
-
"@xmachines/shared": "1.0.0-beta.
|
|
47
|
+
"@xmachines/shared": "1.0.0-beta.8",
|
|
48
48
|
"solid-js": "^1.9.11",
|
|
49
49
|
"typescript": "^5.9.3",
|
|
50
50
|
"vite": "^8.0.0",
|