@xmachines/play-vue 1.0.0-beta.45 → 1.0.0-beta.48
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 +126 -271
- package/dist/define-registry.js.map +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -1,354 +1,209 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- generated-by: gsd-doc-writer -->
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# `@xmachines/play-vue`
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Vue 3 renderer for the XMachines Play Architecture — passively observes actor signals and renders UI via `@json-render/vue`.
|
|
6
|
+
|
|
7
|
+
Part of the [XMachines Play monorepo](../../README.md).
|
|
8
|
+
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](package.json)
|
|
11
|
+
|
|
12
|
+
---
|
|
6
13
|
|
|
7
14
|
## Overview
|
|
8
15
|
|
|
9
|
-
`@xmachines/play-vue`
|
|
16
|
+
`@xmachines/play-vue` is the Vue 3 rendering layer for XMachines Play. It bridges TC39 Signals (actor state) to Vue reactivity and drives component rendering through `@json-render/vue`.
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
- Renders the current view's JSON spec via `@json-render/vue`
|
|
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)
|
|
18
|
+
**Architecture invariants this package upholds:**
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
- **Passive Infrastructure** — Components observe actor signals; they never decide state transitions.
|
|
21
|
+
- **Signal-Only Reactivity** — TC39 Signals are the source of truth; Vue reactivity is used only to trigger re-renders.
|
|
22
|
+
- **Actor Authority** — The actor controls view selection; the renderer reflects it.
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
- **Passive Infrastructure (INV-04):** Vue observes signals and dispatches events — never decides
|
|
20
|
-
- **Signal-Only Reactivity (INV-05):** `actor.currentView` signal is the sole render trigger
|
|
24
|
+
---
|
|
21
25
|
|
|
22
26
|
## Installation
|
|
23
27
|
|
|
24
28
|
```bash
|
|
25
29
|
npm install @xmachines/play-vue
|
|
26
|
-
npm install @json-render/vue @json-render/core # peer deps
|
|
27
|
-
npm install @json-render/xstate @xstate/store # store integration
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
|
|
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 (Vue SFC)
|
|
37
|
-
- `useActor` — composable for accessing the actor inside a `PlayRenderer` tree
|
|
38
|
-
- `defineRegistry` — SFC-aware wrapper; auto-wraps `.vue` SFCs via `h(SFC, ctx)`
|
|
39
|
-
- `useBoundProp` — re-exported from `@json-render/vue`
|
|
40
|
-
- `ComponentFn` (type) — re-exported from `@json-render/vue`
|
|
41
|
-
- `ComponentContext` (type) — re-exported from `@json-render/vue`
|
|
42
|
-
- `ActorProvider` — escape hatch primitive (owns actor bridging, signal bridge, store lifecycle)
|
|
43
|
-
- `PlayUIProvider` — batteries-included composite (wraps `ActorProvider` + `JSONUIProvider`)
|
|
44
|
-
- `getPlayViewContext` — composable 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)
|
|
49
|
-
|
|
50
|
-
## Quick Start
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
// catalog.ts — shared contract
|
|
54
|
-
import { defineCatalog } from "@json-render/core";
|
|
55
|
-
import { z } from "zod";
|
|
56
|
-
|
|
57
|
-
export const catalog = defineCatalog({
|
|
58
|
-
elements: {
|
|
59
|
-
Login: { props: z.object({ title: z.string() }), description: "Login form" },
|
|
60
|
-
Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
|
|
61
|
-
},
|
|
62
|
-
});
|
|
32
|
+
**Peer dependencies** (install alongside):
|
|
63
33
|
|
|
64
|
-
|
|
34
|
+
```bash
|
|
35
|
+
npm install vue@^3.5.0 xstate@^5.30.0 @xstate/store@^3.17.0 @json-render/vue@^0.18.0 @json-render/core@^0.18.0 @json-render/xstate@^0.18.0
|
|
65
36
|
```
|
|
66
37
|
|
|
67
|
-
|
|
68
|
-
<!-- Login.vue — Vue SFC; useBoundProp works in <script setup> -->
|
|
69
|
-
<script setup lang="ts">
|
|
70
|
-
import { useBoundProp } from "@xmachines/play-vue";
|
|
71
|
-
import type { ComponentContext } from "@xmachines/play-vue";
|
|
72
|
-
import type { Catalog } from "./catalog.js";
|
|
38
|
+
---
|
|
73
39
|
|
|
74
|
-
|
|
75
|
-
const [username, setUsername] = useBoundProp<string>(bindings?.username ?? "/username");
|
|
76
|
-
</script>
|
|
40
|
+
## Quick Start
|
|
77
41
|
|
|
42
|
+
```vue
|
|
43
|
+
<!-- App.vue -->
|
|
78
44
|
<template>
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
<input id="username" v-model="username" @input="setUsername(username)" />
|
|
83
|
-
<button type="submit">Log In</button>
|
|
84
|
-
</form>
|
|
85
|
-
</div>
|
|
45
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult">
|
|
46
|
+
<PlayRenderer />
|
|
47
|
+
</PlayUIProvider>
|
|
86
48
|
</template>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
// registry.ts — pass SFCs directly; defineRegistry auto-wraps them
|
|
91
|
-
import { defineRegistry } from "@xmachines/play-vue";
|
|
92
|
-
import { catalog } from "./catalog.js";
|
|
93
|
-
import LoginSFC from "./Login.vue";
|
|
94
|
-
import DashboardSFC from "./Dashboard.vue";
|
|
95
|
-
|
|
96
|
-
export const registryResult = defineRegistry(catalog, {
|
|
97
|
-
components: { Login: LoginSFC, Dashboard: DashboardSFC },
|
|
98
|
-
actions: {
|
|
99
|
-
login: async (params) => {
|
|
100
|
-
if (!params) return;
|
|
101
|
-
actor.send({ type: "auth.login", username: params.username });
|
|
102
|
-
},
|
|
103
|
-
logout: async (params) => {
|
|
104
|
-
actor.send({ type: "auth.logout" });
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
// machine.ts
|
|
112
|
-
import { setup, assign } from "xstate";
|
|
113
|
-
import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
114
|
-
|
|
115
|
-
export const machine = setup({
|
|
116
|
-
types: {
|
|
117
|
-
context: {} as {
|
|
118
|
-
isAuthenticated: boolean;
|
|
119
|
-
username: string | null;
|
|
120
|
-
params: Record<string, string>;
|
|
121
|
-
query: Record<string, string>;
|
|
122
|
-
},
|
|
123
|
-
events: {} as
|
|
124
|
-
| { type: "auth.login"; username: string }
|
|
125
|
-
| { type: "auth.logout" }
|
|
126
|
-
| { type: "play.route"; to: string; params?: Record<string, string> },
|
|
127
|
-
},
|
|
128
|
-
}).createMachine(
|
|
129
|
-
formatPlayRouteTransitions({
|
|
130
|
-
id: "app",
|
|
131
|
-
initial: "login",
|
|
132
|
-
context: { isAuthenticated: false, username: null, params: {}, query: {} },
|
|
133
|
-
states: {
|
|
134
|
-
login: {
|
|
135
|
-
id: "login",
|
|
136
|
-
meta: {
|
|
137
|
-
route: "/login",
|
|
138
|
-
view: {
|
|
139
|
-
root: "root",
|
|
140
|
-
elements: {
|
|
141
|
-
root: { type: "Login", props: { title: "Sign In" }, children: [] },
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
dashboard: {
|
|
147
|
-
id: "dashboard",
|
|
148
|
-
meta: {
|
|
149
|
-
route: "/dashboard",
|
|
150
|
-
view: {
|
|
151
|
-
root: "root",
|
|
152
|
-
elements: {
|
|
153
|
-
root: { type: "Dashboard", props: { username: "" }, children: [] },
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
on: {
|
|
160
|
-
"auth.login": {
|
|
161
|
-
target: ".dashboard",
|
|
162
|
-
guard: ({ context }) => !context.isAuthenticated,
|
|
163
|
-
actions: assign({ isAuthenticated: true, username: ({ event }) => event.username }),
|
|
164
|
-
},
|
|
165
|
-
"auth.logout": {
|
|
166
|
-
target: ".login",
|
|
167
|
-
guard: ({ context }) => context.isAuthenticated,
|
|
168
|
-
actions: assign({ isAuthenticated: false, username: null }),
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
}),
|
|
172
|
-
);
|
|
173
|
-
```
|
|
174
49
|
|
|
175
|
-
```vue
|
|
176
|
-
<!-- App.vue -->
|
|
177
50
|
<script setup lang="ts">
|
|
51
|
+
import { defineRegistry, PlayUIProvider, PlayRenderer } from "@xmachines/play-vue";
|
|
178
52
|
import { definePlayer } from "@xmachines/play-xstate";
|
|
179
|
-
import {
|
|
180
|
-
import {
|
|
181
|
-
import
|
|
53
|
+
import { myMachine } from "./machine.js";
|
|
54
|
+
import { myCatalog } from "./catalog.js";
|
|
55
|
+
import HomeSFC from "./views/Home.vue";
|
|
56
|
+
import LoginSFC from "./views/Login.vue";
|
|
182
57
|
|
|
183
|
-
const createPlayer = definePlayer({ machine });
|
|
58
|
+
const createPlayer = definePlayer({ machine: myMachine });
|
|
184
59
|
const actor = createPlayer();
|
|
185
60
|
actor.start();
|
|
186
|
-
</script>
|
|
187
61
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
62
|
+
const registryResult = defineRegistry(myCatalog, {
|
|
63
|
+
components: {
|
|
64
|
+
Home: HomeSFC, // .vue SFCs are auto-wrapped
|
|
65
|
+
Login: LoginSFC,
|
|
66
|
+
},
|
|
67
|
+
actions: {
|
|
68
|
+
login: async (args) => actor.send({ type: "auth.login", ...args }),
|
|
69
|
+
logout: async () => actor.send({ type: "auth.logout" }),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
193
73
|
```
|
|
194
74
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
### `PlayUIProvider`
|
|
75
|
+
---
|
|
198
76
|
|
|
199
|
-
|
|
77
|
+
## API Summary
|
|
200
78
|
|
|
201
|
-
|
|
202
|
-
<PlayUIProvider
|
|
203
|
-
:actor="actor"
|
|
204
|
-
:registryResult="registryResult"
|
|
205
|
-
:store="myStore"
|
|
206
|
-
:onRenderError="(error, elementType) => console.warn(`<${elementType}> crashed:`, error)"
|
|
207
|
-
>
|
|
208
|
-
<template #fallback><p>Something went wrong.</p></template>
|
|
209
|
-
<PlayRenderer />
|
|
210
|
-
</PlayUIProvider>
|
|
211
|
-
```
|
|
79
|
+
### Components
|
|
212
80
|
|
|
213
|
-
|
|
81
|
+
#### `<PlayUIProvider>`
|
|
214
82
|
|
|
215
|
-
|
|
83
|
+
Batteries-included composite provider. Wraps `<ActorProvider>` and `JSONUIProvider` in one component. **Recommended for most apps.**
|
|
216
84
|
|
|
217
|
-
|
|
85
|
+
| Prop | Type | Required | Description |
|
|
86
|
+
| --------------------- | -------------------------- | -------- | ------------------------------------------- |
|
|
87
|
+
| `actor` | `AbstractActor & Viewable` | ✅ | The XMachines actor instance |
|
|
88
|
+
| `registryResult` | `DefineRegistryResult` | ✅ | Result of `defineRegistry()` |
|
|
89
|
+
| `store` | `StateStore` | — | External controlled state store (optional) |
|
|
90
|
+
| `onRenderError` | `RenderErrorHandler` | — | Error handler for render failures |
|
|
91
|
+
| `navigate` | `(path: string) => void` | — | Link navigation function |
|
|
92
|
+
| `validationFunctions` | `Record<string, Function>` | — | Custom validation functions |
|
|
93
|
+
| `functions` | `Record<string, Function>` | — | Named functions for `$computed` expressions |
|
|
218
94
|
|
|
219
|
-
|
|
220
|
-
- **Provided (controlled):** The caller owns the store; `spec.state` is ignored.
|
|
95
|
+
**Slots:** `default` (rendered content), `fallback` (shown while actor view is `null`)
|
|
221
96
|
|
|
222
|
-
|
|
223
|
-
import { createAtom } from "@xstate/store";
|
|
224
|
-
import { xstateStoreStateStore } from "@json-render/xstate";
|
|
225
|
-
import type { StateStore } from "@json-render/core";
|
|
97
|
+
#### `<PlayRenderer>`
|
|
226
98
|
|
|
227
|
-
|
|
228
|
-
```
|
|
99
|
+
Zero-prop leaf component. Reads the current `spec` and `registry` from the nearest `<ActorProvider>` or `<PlayUIProvider>` context and renders via `<Renderer>`. Must be placed inside one of those providers.
|
|
229
100
|
|
|
230
101
|
```vue
|
|
231
|
-
<PlayUIProvider :actor="actor" :registryResult="registryResult"
|
|
232
|
-
|
|
102
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult">
|
|
103
|
+
<PlayRenderer />
|
|
233
104
|
</PlayUIProvider>
|
|
234
105
|
```
|
|
235
106
|
|
|
236
|
-
|
|
107
|
+
#### `<ActorProvider>`
|
|
237
108
|
|
|
238
|
-
|
|
109
|
+
Low-level escape hatch for custom provider composition. Owns the full actor lifecycle — signal subscription, per-view state store, handler resolution, and Vue context provision. Use `<PlayUIProvider>` unless you need fine-grained control.
|
|
239
110
|
|
|
240
|
-
|
|
111
|
+
| Prop | Type | Required | Description |
|
|
112
|
+
| ---------------- | -------------------------- | -------- | ------------------------------- |
|
|
113
|
+
| `actor` | `AbstractActor & Viewable` | ✅ | The XMachines actor instance |
|
|
114
|
+
| `registryResult` | `DefineRegistryResult` | ✅ | Result of `defineRegistry()` |
|
|
115
|
+
| `store` | `StateStore` | — | External controlled state store |
|
|
116
|
+
| `onRenderError` | `RenderErrorHandler` | — | Override render error handler |
|
|
241
117
|
|
|
242
|
-
|
|
118
|
+
---
|
|
243
119
|
|
|
244
|
-
|
|
245
|
-
<ActorProvider :actor="actor" :registryResult="registryResult" :onRenderError="handleError">
|
|
246
|
-
<!-- your own JSONUIProvider + PlayRenderer tree -->
|
|
247
|
-
</ActorProvider>
|
|
248
|
-
```
|
|
120
|
+
### Functions
|
|
249
121
|
|
|
250
|
-
|
|
122
|
+
#### `defineRegistry(catalog, options)`
|
|
251
123
|
|
|
252
|
-
|
|
124
|
+
Drop-in replacement for `defineRegistry` from `@json-render/vue`. **Always import from `@xmachines/play-vue`** rather than `@json-render/vue` when working with Vue SFCs — this wrapper automatically detects `.vue` SFCs in the `components` map and wraps them via `h()` so Vue composables (including `inject`-based ones) work correctly inside `<script setup>`.
|
|
253
125
|
|
|
254
|
-
|
|
126
|
+
```typescript
|
|
127
|
+
import { defineRegistry } from "@xmachines/play-vue";
|
|
128
|
+
// NOT: import { defineRegistry } from "@json-render/vue"
|
|
255
129
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<PlayRenderer />
|
|
259
|
-
</PlayUIProvider>
|
|
260
|
-
```
|
|
130
|
+
import LoginSFC from "./views/Login.vue";
|
|
131
|
+
import DashboardSFC from "./views/Dashboard.vue";
|
|
261
132
|
|
|
262
|
-
|
|
133
|
+
const registryResult = defineRegistry(catalog, {
|
|
134
|
+
components: {
|
|
135
|
+
Login: LoginSFC, // .vue SFC — auto-wrapped via h()
|
|
136
|
+
Dashboard: DashboardSFC,
|
|
137
|
+
},
|
|
138
|
+
actions: {
|
|
139
|
+
login: async (args, setState, getState) => {
|
|
140
|
+
/* ... */
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
```
|
|
263
145
|
|
|
264
|
-
|
|
146
|
+
Plain `ComponentFn` functions (non-SFC) also work and are passed through unchanged. Mixing SFCs and plain functions in the same registry is supported.
|
|
265
147
|
|
|
266
|
-
|
|
148
|
+
#### `useActor()`
|
|
267
149
|
|
|
268
|
-
|
|
150
|
+
Vue composable for accessing the raw actor inside a `PlayRenderer` tree. Avoids prop drilling for deeply nested components.
|
|
269
151
|
|
|
270
|
-
|
|
152
|
+
```typescript
|
|
153
|
+
import { useActor } from "@xmachines/play-vue";
|
|
271
154
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<p>Something went wrong.</p>
|
|
276
|
-
</template>
|
|
277
|
-
<PlayRenderer />
|
|
278
|
-
</PlayUIProvider>
|
|
155
|
+
// Inside a component rendered by PlayRenderer:
|
|
156
|
+
const actor = useActor();
|
|
157
|
+
actor.send({ type: "SUBMIT" });
|
|
279
158
|
```
|
|
280
159
|
|
|
281
|
-
|
|
160
|
+
Throws if called outside an `<ActorProvider>` or `<PlayUIProvider>` tree.
|
|
282
161
|
|
|
283
|
-
|
|
162
|
+
#### `getPlayViewContext()`
|
|
284
163
|
|
|
285
|
-
|
|
164
|
+
Access the current `ViewContextValue` — `{ spec, handlers, registry, store }` — from inside an `<ActorProvider>` tree.
|
|
286
165
|
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
<PlayUIProvider
|
|
290
|
-
:actor="actor"
|
|
291
|
-
:registryResult="registryResult"
|
|
292
|
-
:onRenderError="(error, elementType) => console.warn(`<${elementType}> crashed:`, error)"
|
|
293
|
-
>
|
|
294
|
-
<PlayRenderer />
|
|
295
|
-
</PlayUIProvider>
|
|
296
|
-
```
|
|
166
|
+
```typescript
|
|
167
|
+
import { getPlayViewContext } from "@xmachines/play-vue";
|
|
297
168
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
components: { Login: LoginSFC, Dashboard: DashboardSFC },
|
|
302
|
-
actions: { login: async (params) => { ... }, logout: async () => { ... } },
|
|
303
|
-
onRenderError(error, elementType) {
|
|
304
|
-
reportExpectedRenderError(error, elementType);
|
|
305
|
-
},
|
|
306
|
-
});
|
|
169
|
+
// Inside setup() of a component within an ActorProvider tree:
|
|
170
|
+
const view = getPlayViewContext();
|
|
171
|
+
// view.spec, view.handlers, view.registry, view.store
|
|
307
172
|
```
|
|
308
173
|
|
|
309
|
-
`onRenderError` is typed as `RenderErrorHandler` and exported from.
|
|
310
|
-
|
|
311
174
|
---
|
|
312
175
|
|
|
313
|
-
### `
|
|
176
|
+
### Re-exported from `@json-render/vue`
|
|
314
177
|
|
|
315
|
-
|
|
178
|
+
The following are re-exported so consumers import everything from `@xmachines/play-vue`:
|
|
316
179
|
|
|
317
|
-
|
|
318
|
-
import { useActor } from "@xmachines/play-vue";
|
|
180
|
+
**Components:** `JSONUIProvider`, `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`, `Renderer`
|
|
319
181
|
|
|
320
|
-
|
|
321
|
-
const actor = useActor();
|
|
322
|
-
actor.send({ type: "auth.logout" });
|
|
323
|
-
```
|
|
182
|
+
**Composables:** `useBoundProp`
|
|
324
183
|
|
|
325
|
-
|
|
184
|
+
**Types:** `JSONUIProviderProps`, `StateProviderProps`, `ActionProviderProps`, `ValidationProviderProps`, `RendererProps`, `ComponentFn`, `ComponentContext`, `DefineRegistryResult`
|
|
326
185
|
|
|
327
186
|
---
|
|
328
187
|
|
|
329
|
-
##
|
|
188
|
+
## Testing
|
|
330
189
|
|
|
331
|
-
|
|
190
|
+
Run tests for this package in isolation:
|
|
332
191
|
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// Component receives: { section: "profile", user: "alice" }
|
|
337
|
-
```
|
|
192
|
+
```bash
|
|
193
|
+
# From the monorepo root
|
|
194
|
+
npm test -w packages/play-vue
|
|
338
195
|
|
|
339
|
-
|
|
196
|
+
# Watch mode
|
|
197
|
+
npm run test:watch -w packages/play-vue
|
|
340
198
|
|
|
341
|
-
|
|
199
|
+
# With coverage (80% threshold enforced on lines, functions, branches, statements)
|
|
200
|
+
npx vitest run --coverage --config packages/play-vue/vitest.config.ts
|
|
201
|
+
```
|
|
342
202
|
|
|
343
|
-
|
|
203
|
+
Tests use [Vitest](https://vitest.dev/) with `jsdom` environment and `@vue/test-utils` for component mounting.
|
|
344
204
|
|
|
345
|
-
|
|
346
|
-
- `actor.currentView` (TC39 Signal) is bridged to Vue's reactive system inside `PlayRenderer`
|
|
347
|
-
- Per-view UI state lives in an `@xstate/store` atom, not in Vue reactive state
|
|
348
|
-
- `@json-render/vue` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useBoundProp` from
|
|
349
|
-
- Vue views should be `.vue` SFCs using `ComponentContext<MyCatalog, "X">` — `defineRegistry` from and Vue composables work correctly
|
|
205
|
+
---
|
|
350
206
|
|
|
351
|
-
##
|
|
207
|
+
## License
|
|
352
208
|
|
|
353
|
-
|
|
354
|
-
- [Vue Router adapter](../play-vue-router/README.md)
|
|
209
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-registry.js","names":[],"sources":["../src/define-registry.ts"],"sourcesContent":["/**\n * `defineRegistry` wrapper for @xmachines/play-vue.\n *\n * Wraps `defineRegistry` from `@json-render/vue` with automatic SFC support.\n *\n * ## Vue-specific — React and Solid do not need this\n *\n * In React, `useContext()` works anywhere inside the component call tree.\n * In Solid, `useContext()` works inside reactive computations and component renders.\n * Neither has the strict \"synchronous setup() only\" constraint that Vue's `inject()`\n * imposes — so their `defineRegistry` implementations call `componentFn(ctx)` directly\n * with no wrapping needed.\n *\n * For the DOM renderer, there is no component system at all — just render functions\n * returning `HTMLElement` — so injection context is not applicable.\n *\n * Only Vue requires this adapter.\n *\n * ## Why this wrapper exists\n *\n * `@json-render/vue`'s `defineRegistry` calls each registered component as a plain\n * function: `componentFn(ctx)`. A `.vue` SFC (output of `defineComponent` or\n * `<script setup>`) is an **object**, not a function — calling it throws.\n *\n * More fundamentally, `defineRegistry` calls components inside its own render\n * function (the return value of `setup()`). Vue's `inject()` — and composables built\n * on it: `useStateBinding`, `useStateStore` — only work during synchronous `setup()`\n * execution, not inside render functions. Plain `.ts` `ComponentFn` files cannot\n * call any Vue composable for this reason.\n *\n * This wrapper auto-detects Vue SFCs in the `components` map and wraps them via\n * `h(SFC, ctx)`. The SFC renders as a child component with its own `setup()`,\n * giving full access to composables inside `<script setup>`.\n *\n * ## Usage\n *\n * Import `defineRegistry` from `@xmachines/play-vue` instead of `@json-render/vue`:\n *\n * ```ts\n * import { defineRegistry } from \"@xmachines/play-vue\";\n * // not: import { defineRegistry } from \"@json-render/vue\";\n *\n * import LoginSFC from \"./views/Login.vue\";\n * import DashboardSFC from \"./views/Dashboard.vue\";\n *\n * const { registry } = defineRegistry(catalog, {\n * components: {\n * Login: LoginSFC, // .vue SFC — auto-wrapped\n * Dashboard: DashboardSFC, // .vue SFC — auto-wrapped\n * },\n * });\n * ```\n *\n * Plain `ComponentFn` functions still work and are passed through unchanged.\n * Mixing SFCs and plain functions in the same registry is supported.\n */\n\nimport { h, type Component } from \"vue\";\nimport {\n\tdefineRegistry as defineRegistryBase,\n\ttype ComponentContext,\n\ttype ComponentFn,\n\ttype Components,\n} from \"@json-render/vue\";\nimport type { Catalog, InferCatalogComponents } from \"@json-render/core\";\n\n/**\n * Detect whether a value is a Vue component object (SFC) rather than a plain\n * `ComponentFn` function.\n *\n * Vue SFCs produced by `defineComponent` or `<script setup>` compilation are\n * plain objects (not callable). A `ComponentFn` is always a plain function.\n *\n * Detection strategy (checked in order):\n * 1. `__vccOpts` — set by vite-plugin-vue / vue-loader on every `<script setup>` SFC.\n * 2. `__name` — set by Vite on named SFCs (present alongside `__vccOpts` but also\n * on `defineComponent({ name: \"...\" })` output without `<script setup>`).\n * 3. `setup` is a function — the `defineComponent({ setup() {} })` signature.\n * 4. `render` is a function — the `defineComponent({ render() {} })` signature.\n *\n * Requiring `setup` / `render` to be **functions** prevents plain POJOs that\n * happen to have a `setup` or `render` key from being misidentified as components.\n */\nfunction isVueSFC(value: unknown): value is Component {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst v = value as Record<string, unknown>;\n\t// Compiler-injected markers — unambiguous Vue component identifiers.\n\tif (\"__vccOpts\" in v || \"__name\" in v) return true;\n\t// defineComponent() output: setup or render must be callable, not just present.\n\treturn typeof v[\"setup\"] === \"function\" || typeof v[\"render\"] === \"function\";\n}\n\n/**\n * Wrap a Vue SFC as a `ComponentFn` by rendering it via `h()`.\n *\n * The SFC receives the full `ComponentContext` as its props and renders in its\n * own child component `setup()`, where Vue composables work correctly.\n */\nfunction wrapSFC<C extends Catalog, K extends keyof InferCatalogComponents<C>>(\n\tcomponent: Component,\n): ComponentFn<C, K> {\n\treturn (ctx: ComponentContext<C, K>) => h(component, ctx);\n}\n\nexport type ComponentEntry<C extends Catalog, K extends keyof InferCatalogComponents<C>> =\n\t| ComponentFn<C, K>\n\t| Component;\n\nexport type ComponentsMap<C extends Catalog> = {\n\t[K in keyof InferCatalogComponents<C>]?: ComponentEntry<C, K>;\n};\n\nexport type DefineRegistryOptions<C extends Catalog> = Omit<\n\tParameters<typeof defineRegistryBase<C>>[1],\n\t\"components\"\n> & {\n\tcomponents?: ComponentsMap<C>;\n};\n\n/**\n * Create a component registry, automatically wrapping `.vue` SFCs so they work\n * correctly with `@json-render/vue`'s rendering pipeline.\n *\n * Drop-in replacement for `defineRegistry` from `@json-render/vue`. Import from\n * `@xmachines/play-vue` to get SFC support for free.\n *\n * @param catalog - The json-render catalog defining component prop shapes.\n * @param options - Registry options. `components` entries may be `.vue` SFCs\n * (objects) or plain `ComponentFn` functions — both are handled automatically.\n */\nexport function defineRegistry<C extends Catalog>(\n\tcatalog: C,\n\toptions: DefineRegistryOptions<C>,\n): ReturnType<typeof defineRegistryBase<C>> {\n\tconst wrappedComponents = {} as Components<C>;\n\n\tfor (const [key, component] of Object.entries(options.components ?? {})) {\n\t\tif (component === undefined) continue;\n\t\tconst k = key as keyof InferCatalogComponents<C>;\n\t\twrappedComponents[k] = isVueSFC(component)\n\t\t\t? wrapSFC(component as Component)\n\t\t\t: (component as ComponentFn<C, typeof k>);\n\t}\n\n\tconst baseOptions = options as Parameters<typeof defineRegistryBase<C>>[1];\n\treturn defineRegistryBase(catalog, { ...baseOptions, components: wrappedComponents });\n}\n"],"mappings":";;;AAmFA,SAAS,EAAS,GAAoC;AACrD,KAAI,OAAO,KAAU,aAAY,EAAgB,QAAO;CACxD,IAAM,IAAI;AAIV,QAFI,eAAe,KAAK,YAAY,IAAU,KAEvC,OAAO,EAAE,SAAa,cAAc,OAAO,EAAE,UAAc;;AASnE,SAAS,EACR,GACoB;AACpB,SAAQ,MAAgC,EAAE,GAAW,EAAI;;AA6B1D,SAAgB,EACf,GACA,GAC2C;CAC3C,IAAM,IAAoB,EAAE;AAE5B,MAAK,IAAM,CAAC,GAAK,MAAc,OAAO,QAAQ,EAAQ,cAAc,EAAE,CAAC,EAAE;AACxE,MAAI,MAAc,KAAA,EAAW;EAC7B,IAAM,IAAI;AACV,IAAkB,KAAK,EAAS,EAAU,GACvC,EAAQ,EAAuB,GAC9B;;AAIL,QAAO,EAAmB,GAAS;EAAE,
|
|
1
|
+
{"version":3,"file":"define-registry.js","names":[],"sources":["../src/define-registry.ts"],"sourcesContent":["/**\n * `defineRegistry` wrapper for @xmachines/play-vue.\n *\n * Wraps `defineRegistry` from `@json-render/vue` with automatic SFC support.\n *\n * ## Vue-specific — React and Solid do not need this\n *\n * In React, `useContext()` works anywhere inside the component call tree.\n * In Solid, `useContext()` works inside reactive computations and component renders.\n * Neither has the strict \"synchronous setup() only\" constraint that Vue's `inject()`\n * imposes — so their `defineRegistry` implementations call `componentFn(ctx)` directly\n * with no wrapping needed.\n *\n * For the DOM renderer, there is no component system at all — just render functions\n * returning `HTMLElement` — so injection context is not applicable.\n *\n * Only Vue requires this adapter.\n *\n * ## Why this wrapper exists\n *\n * `@json-render/vue`'s `defineRegistry` calls each registered component as a plain\n * function: `componentFn(ctx)`. A `.vue` SFC (output of `defineComponent` or\n * `<script setup>`) is an **object**, not a function — calling it throws.\n *\n * More fundamentally, `defineRegistry` calls components inside its own render\n * function (the return value of `setup()`). Vue's `inject()` — and composables built\n * on it: `useStateBinding`, `useStateStore` — only work during synchronous `setup()`\n * execution, not inside render functions. Plain `.ts` `ComponentFn` files cannot\n * call any Vue composable for this reason.\n *\n * This wrapper auto-detects Vue SFCs in the `components` map and wraps them via\n * `h(SFC, ctx)`. The SFC renders as a child component with its own `setup()`,\n * giving full access to composables inside `<script setup>`.\n *\n * ## Usage\n *\n * Import `defineRegistry` from `@xmachines/play-vue` instead of `@json-render/vue`:\n *\n * ```ts\n * import { defineRegistry } from \"@xmachines/play-vue\";\n * // not: import { defineRegistry } from \"@json-render/vue\";\n *\n * import LoginSFC from \"./views/Login.vue\";\n * import DashboardSFC from \"./views/Dashboard.vue\";\n *\n * const { registry } = defineRegistry(catalog, {\n * components: {\n * Login: LoginSFC, // .vue SFC — auto-wrapped\n * Dashboard: DashboardSFC, // .vue SFC — auto-wrapped\n * },\n * });\n * ```\n *\n * Plain `ComponentFn` functions still work and are passed through unchanged.\n * Mixing SFCs and plain functions in the same registry is supported.\n */\n\nimport { h, type Component } from \"vue\";\nimport {\n\tdefineRegistry as defineRegistryBase,\n\ttype ComponentContext,\n\ttype ComponentFn,\n\ttype Components,\n} from \"@json-render/vue\";\nimport type { Catalog, InferCatalogComponents } from \"@json-render/core\";\n\n/**\n * Detect whether a value is a Vue component object (SFC) rather than a plain\n * `ComponentFn` function.\n *\n * Vue SFCs produced by `defineComponent` or `<script setup>` compilation are\n * plain objects (not callable). A `ComponentFn` is always a plain function.\n *\n * Detection strategy (checked in order):\n * 1. `__vccOpts` — set by vite-plugin-vue / vue-loader on every `<script setup>` SFC.\n * 2. `__name` — set by Vite on named SFCs (present alongside `__vccOpts` but also\n * on `defineComponent({ name: \"...\" })` output without `<script setup>`).\n * 3. `setup` is a function — the `defineComponent({ setup() {} })` signature.\n * 4. `render` is a function — the `defineComponent({ render() {} })` signature.\n *\n * Requiring `setup` / `render` to be **functions** prevents plain POJOs that\n * happen to have a `setup` or `render` key from being misidentified as components.\n */\nfunction isVueSFC(value: unknown): value is Component {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst v = value as Record<string, unknown>;\n\t// Compiler-injected markers — unambiguous Vue component identifiers.\n\tif (\"__vccOpts\" in v || \"__name\" in v) return true;\n\t// defineComponent() output: setup or render must be callable, not just present.\n\treturn typeof v[\"setup\"] === \"function\" || typeof v[\"render\"] === \"function\";\n}\n\n/**\n * Wrap a Vue SFC as a `ComponentFn` by rendering it via `h()`.\n *\n * The SFC receives the full `ComponentContext` as its props and renders in its\n * own child component `setup()`, where Vue composables work correctly.\n */\nfunction wrapSFC<C extends Catalog, K extends keyof InferCatalogComponents<C>>(\n\tcomponent: Component,\n): ComponentFn<C, K> {\n\treturn (ctx: ComponentContext<C, K>) => h(component, ctx);\n}\n\nexport type ComponentEntry<C extends Catalog, K extends keyof InferCatalogComponents<C>> =\n\t| ComponentFn<C, K>\n\t| Component;\n\nexport type ComponentsMap<C extends Catalog> = {\n\t[K in keyof InferCatalogComponents<C>]?: ComponentEntry<C, K>;\n};\n\nexport type DefineRegistryOptions<C extends Catalog> = Omit<\n\tParameters<typeof defineRegistryBase<C>>[1],\n\t\"components\"\n> & {\n\tcomponents?: ComponentsMap<C>;\n};\n\n/**\n * Create a component registry, automatically wrapping `.vue` SFCs so they work\n * correctly with `@json-render/vue`'s rendering pipeline.\n *\n * Drop-in replacement for `defineRegistry` from `@json-render/vue`. Import from\n * `@xmachines/play-vue` to get SFC support for free.\n *\n * @param catalog - The json-render catalog defining component prop shapes.\n * @param options - Registry options. `components` entries may be `.vue` SFCs\n * (objects) or plain `ComponentFn` functions — both are handled automatically.\n */\nexport function defineRegistry<C extends Catalog>(\n\tcatalog: C,\n\toptions: DefineRegistryOptions<C>,\n): ReturnType<typeof defineRegistryBase<C>> {\n\tconst wrappedComponents = {} as Components<C>;\n\n\tfor (const [key, component] of Object.entries(options.components ?? {})) {\n\t\tif (component === undefined) continue;\n\t\tconst k = key as keyof InferCatalogComponents<C>;\n\t\twrappedComponents[k] = isVueSFC(component)\n\t\t\t? wrapSFC(component as Component)\n\t\t\t: (component as ComponentFn<C, typeof k>);\n\t}\n\n\tconst baseOptions = options as Parameters<typeof defineRegistryBase<C>>[1];\n\treturn defineRegistryBase(catalog, { ...baseOptions, components: wrappedComponents });\n}\n"],"mappings":";;;AAmFA,SAAS,EAAS,GAAoC;AACrD,KAAI,OAAO,KAAU,aAAY,EAAgB,QAAO;CACxD,IAAM,IAAI;AAIV,QAFI,eAAe,KAAK,YAAY,IAAU,KAEvC,OAAO,EAAE,SAAa,cAAc,OAAO,EAAE,UAAc;;AASnE,SAAS,EACR,GACoB;AACpB,SAAQ,MAAgC,EAAE,GAAW,EAAI;;AA6B1D,SAAgB,EACf,GACA,GAC2C;CAC3C,IAAM,IAAoB,EAAE;AAE5B,MAAK,IAAM,CAAC,GAAK,MAAc,OAAO,QAAQ,EAAQ,cAAc,EAAE,CAAC,EAAE;AACxE,MAAI,MAAc,KAAA,EAAW;EAC7B,IAAM,IAAI;AACV,IAAkB,KAAK,EAAS,EAAU,GACvC,EAAQ,EAAuB,GAC9B;;AAIL,QAAO,EAAmB,GAAS;EAAE,GAAG;EAAa,YAAY;EAAmB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmachines/play-vue",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.48",
|
|
4
4
|
"description": "Vue renderer for XMachines Play architecture",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reactive",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"prepublishOnly": "npm run build"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@xmachines/play": "1.0.0-beta.
|
|
46
|
-
"@xmachines/play-actor": "1.0.0-beta.
|
|
47
|
-
"@xmachines/play-signals": "1.0.0-beta.
|
|
45
|
+
"@xmachines/play": "1.0.0-beta.48",
|
|
46
|
+
"@xmachines/play-actor": "1.0.0-beta.48",
|
|
47
|
+
"@xmachines/play-signals": "1.0.0-beta.48"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@json-render/core": "^0.18.0",
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
"@json-render/xstate": "^0.18.0",
|
|
53
53
|
"@types/node": "^25.6.0",
|
|
54
54
|
"@vitejs/plugin-vue": "^6.0.5",
|
|
55
|
-
"@vue/test-utils": "^2.4.
|
|
56
|
-
"@xmachines/shared": "1.0.0-beta.
|
|
55
|
+
"@vue/test-utils": "^2.4.9",
|
|
56
|
+
"@xmachines/shared": "1.0.0-beta.48",
|
|
57
57
|
"@xstate/store": "^3.17.0",
|
|
58
|
-
"oxfmt": "^0.
|
|
59
|
-
"oxlint": "^1.
|
|
58
|
+
"oxfmt": "^0.47.0",
|
|
59
|
+
"oxlint": "^1.62.0",
|
|
60
60
|
"typescript": "^5.9.3 || ^6.0.3",
|
|
61
61
|
"vite-plugin-dts": "^4.5.4",
|
|
62
|
-
"vitest": "^4.1.
|
|
63
|
-
"vue": "^3.5.
|
|
64
|
-
"xstate": "^5.
|
|
62
|
+
"vitest": "^4.1.5",
|
|
63
|
+
"vue": "^3.5.33",
|
|
64
|
+
"xstate": "^5.31.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@json-render/core": "^0.18.0",
|
|
@@ -69,6 +69,6 @@
|
|
|
69
69
|
"@json-render/xstate": "^0.18.0",
|
|
70
70
|
"@xstate/store": "^3.17.0",
|
|
71
71
|
"vue": "^3.5.0",
|
|
72
|
-
"xstate": "^5.
|
|
72
|
+
"xstate": "^5.31.0"
|
|
73
73
|
}
|
|
74
74
|
}
|