@xmachines/play-vue 1.0.0-beta.46 → 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 CHANGED
@@ -1,354 +1,209 @@
1
- # @xmachines/play-vue
1
+ <!-- generated-by: gsd-doc-writer -->
2
2
 
3
- **Vue 3 renderer for XMachines Play Architecture**
3
+ # `@xmachines/play-vue`
4
4
 
5
- Bridges TC39 Signal-driven actors to Vue's reactivity. Business logic stays in the actor; Vue is purely a rendering target.
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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
+ [![Version](https://img.shields.io/badge/version-1.0.0--beta.46-blue.svg)](package.json)
11
+
12
+ ---
6
13
 
7
14
  ## Overview
8
15
 
9
- `@xmachines/play-vue` provides `PlayRenderer`, a Vue component that:
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
- - Subscribes to `actor.currentView` (TC39 Signal) and re-renders on every state transition
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
- Per [Play RFC](../docs/rfc/play.md):
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
- - **Actor Authority (INV-01):** Guards in the machine decide all state transitions
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
- In this monorepo, the root install applies a `patch-package` patch to `@json-render/vue`
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
- export type Catalog = typeof catalog;
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
- ```vue
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
- const { props, emit, bindings } = defineProps<ComponentContext<Catalog, "Login">>();
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
- <div class="view">
80
- <h2>{{ props.title }}</h2>
81
- <form @submit.prevent="emit('submit')">
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 { PlayUIProvider, PlayRenderer } from "@xmachines/play-vue";
180
- import { machine } from "./machine.js";
181
- import { registryResult } from "./registry.js";
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
- <template>
189
- <PlayUIProvider :actor="actor" :registryResult="registryResult">
190
- <PlayRenderer />
191
- </PlayUIProvider>
192
- </template>
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
- ## API Reference
196
-
197
- ### `PlayUIProvider`
75
+ ---
198
76
 
199
- Batteries-included composite provider. Wraps `ActorProvider` + `JSONUIProvider`. Pass `actor` and `registryResult` here, then place `<PlayRenderer />` inside as a zero-prop child.
77
+ ## API Summary
200
78
 
201
- ```vue
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
- **`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
81
+ #### `<PlayUIProvider>`
214
82
 
215
- **`registryResult`** The full `DefineRegistryResult` returned by `defineRegistry(catalog, { components, actions })` from.
83
+ Batteries-included composite provider. Wraps `<ActorProvider>` and `JSONUIProvider` in one component. **Recommended for most apps.**
216
84
 
217
- **`store`** (optional) Controls per-view UI state (`$state` bindings, form values):
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
- - **Omitted (uncontrolled, default):** A fresh `@xstate/store` atom is created per view transition, seeded from `view.spec.state`.
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
- ```ts
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
- const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
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" :store="store">
232
- <PlayRenderer />
102
+ <PlayUIProvider :actor="actor" :registryResult="registryResult">
103
+ <PlayRenderer />
233
104
  </PlayUIProvider>
234
105
  ```
235
106
 
236
- **`onRenderError`** — Called when an individual catalog component throws during render. Caught by `@json-render/vue`'s inner per-element error boundary — the failed component is silently removed while the rest of the spec continues rendering. When both `onRenderError` on `PlayUIProvider` and on `defineRegistry` are set, the prop wins.
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
- ### `ActorProvider`
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
- Escape hatch primitive. Owns actor bridging, signal bridge, and store lifecycle. Use this when you need direct control over the provider layer.
118
+ ---
243
119
 
244
- ```vue
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
- ### `PlayRenderer`
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
- Zero-prop leaf component. Must be rendered inside a `PlayUIProvider` (or `ActorProvider`) tree. Subscribes to `actor.currentView` via context and renders the current spec.
126
+ ```typescript
127
+ import { defineRegistry } from "@xmachines/play-vue";
128
+ // NOT: import { defineRegistry } from "@json-render/vue"
255
129
 
256
- ```vue
257
- <PlayUIProvider :actor="actor" :registryResult="registryResult">
258
- <PlayRenderer />
259
- </PlayUIProvider>
260
- ```
130
+ import LoginSFC from "./views/Login.vue";
131
+ import DashboardSFC from "./views/Dashboard.vue";
261
132
 
262
- `PlayRenderer` accepts no props — all configuration (`actor`, `registryResult`, `store`, `fallback`, `onRenderError`) is provided by the enclosing `PlayUIProvider` or `ActorProvider`.
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
- ## Error handling
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
- The provider tree has two layers of error boundaries:
148
+ #### `useActor()`
267
149
 
268
- ### Outer boundary `fallback` slot
150
+ Vue composable for accessing the raw actor inside a `PlayRenderer` tree. Avoids prop drilling for deeply nested components.
269
151
 
270
- Vue's `onErrorCaptured` wraps the entire renderer. Triggered when the spec or store setup throws, or when the inner boundary is not present. Use Vue's built-in `onErrorCaptured` in a parent component for observability.
152
+ ```typescript
153
+ import { useActor } from "@xmachines/play-vue";
271
154
 
272
- ```vue
273
- <PlayUIProvider :actor="actor" :registryResult="registryResult">
274
- <template #fallback>
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
- ### Inner boundary `onRenderError`
160
+ Throws if called outside an `<ActorProvider>` or `<PlayUIProvider>` tree.
282
161
 
283
- Each catalog element is individually wrapped in an error boundary by `@json-render/vue`. When a component throws, it is silently removed while the rest of the spec continues rendering. The outer boundary is **not** triggered.
162
+ #### `getPlayViewContext()`
284
163
 
285
- Pass `onRenderError` to `PlayUIProvider` (or `ActorProvider`) overrides any registry-level handleror bake it into `defineRegistry`:
164
+ Access the current `ViewContextValue` `{ spec, handlers, registry, store }` from inside an `<ActorProvider>` tree.
286
165
 
287
- ```vue
288
- <!-- via PlayUIProvider prop -->
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
- ```ts
299
- // via defineRegistry — bakes the handler into the registry
300
- const registryResult = defineRegistry(catalog, {
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
- ### `useActor`
176
+ ### Re-exported from `@json-render/vue`
314
177
 
315
- Vue composable for accessing the actor from inside any component rendered by `PlayRenderer`.
178
+ The following are re-exported so consumers import everything from `@xmachines/play-vue`:
316
179
 
317
- ```ts
318
- import { useActor } from "@xmachines/play-vue";
180
+ **Components:** `JSONUIProvider`, `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`, `Renderer`
319
181
 
320
- // Inside any component rendered inside PlayRenderer:
321
- const actor = useActor();
322
- actor.send({ type: "auth.logout" });
323
- ```
182
+ **Composables:** `useBoundProp`
324
183
 
325
- Throws `NonNullableError: "useActor() must be called inside <ActorProvider> (or <PlayUIProvider>)"` if called outside the tree.
184
+ **Types:** `JSONUIProviderProps`, `StateProviderProps`, `ActionProviderProps`, `ValidationProviderProps`, `RendererProps`, `ComponentFn`, `ComponentContext`, `DefineRegistryResult`
326
185
 
327
186
  ---
328
187
 
329
- ## Route Parameters in Props
188
+ ## Testing
330
189
 
331
- When using `formatPlayRouteTransitions`, URL path parameters flow automatically into component props. Declare an `undefined` slot in the spec to opt in:
190
+ Run tests for this package in isolation:
332
191
 
333
- ```ts
334
- // spec: { section: undefined, user: "alice" }
335
- // After play.route to /settings/profile → context.params = { section: "profile" }
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
- Priority: **route param fills `undefined` slots; explicit non-`undefined` spec props always win.**
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
- ## Architecture Notes
203
+ Tests use [Vitest](https://vitest.dev/) with `jsdom` environment and `@vue/test-utils` for component mounting.
344
204
 
345
- - Vue reactivity is only used to trigger re-renders — not for business logic
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
- ## Learn More
207
+ ## License
352
208
 
353
- - [Demo](examples/demo/README.md)
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,GADjB;EACiC,YAAY;EAAmB,CAAC"}
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.46",
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",
46
- "@xmachines/play-actor": "1.0.0-beta.46",
47
- "@xmachines/play-signals": "1.0.0-beta.46"
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.6",
56
- "@xmachines/shared": "1.0.0-beta.46",
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.45.0",
59
- "oxlint": "^1.60.0",
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.4",
63
- "vue": "^3.5.31",
64
- "xstate": "^5.30.0"
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.30.0"
72
+ "xstate": "^5.31.0"
73
73
  }
74
74
  }