@xmachines/play-vue 1.0.0-beta.21 → 1.0.0-beta.22

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
@@ -27,6 +27,10 @@ npm install @json-render/vue @json-render/core # peer deps
27
27
  npm install @json-render/xstate @xstate/store # store integration
28
28
  ```
29
29
 
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
+
30
34
  ## Current Exports
31
35
 
32
36
  - `PlayRenderer` — main renderer component (Vue SFC)
@@ -202,6 +206,9 @@ Main Vue component. Subscribes to `actor.currentView` and renders the spec.
202
206
 
203
207
  **`registry`** — Built with `defineRegistry(catalog, { components, actions })` from `@xmachines/play-vue`. Pass `.vue` SFCs directly — they are auto-wrapped via `h(SFC, ctx)` so `useStateBinding` and other composables work inside `<script setup>`.
204
208
 
209
+ `defineRegistry` also accepts `onRenderError(error, elementType)`, which receives errors
210
+ caught by `@json-render/vue`'s inner element boundary before the default logger is used.
211
+
205
212
  **`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.
206
213
 
207
214
  **`store`** (optional) — Controls per-view UI state (`$state` bindings, form values):
@@ -217,6 +224,19 @@ import type { StateStore } from "@json-render/core";
217
224
  const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
218
225
  ```
219
226
 
227
+ **Inner render errors** — You can intercept catalog component render failures without
228
+ overriding the outer Vue error boundary:
229
+
230
+ ```ts
231
+ export const { registry } = defineRegistry(catalog, {
232
+ components: { Login: LoginSFC, Dashboard: DashboardSFC },
233
+ actions: { login: async () => {}, logout: async () => {} },
234
+ onRenderError(error, elementType) {
235
+ reportExpectedRenderError(error, elementType);
236
+ },
237
+ });
238
+ ```
239
+
220
240
  ```vue
221
241
  <PlayRenderer
222
242
  :actor="actor"
@@ -1 +1 @@
1
- {"version":3,"file":"PlayRenderer.js","names":[],"sources":["../src/PlayRenderer.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * PlayRenderer - Main Vue renderer component for XMachines Play architecture\n *\n * Architecture (per RESEARCH.md Pattern 1):\n * - Subscribes to actor.currentView signal via Signal.subtle.Watcher\n * - Renders view.spec via @json-render/vue Renderer + StateProvider + ActionProvider\n * - Routes actor.send() calls via ActionProvider handlers\n * - Vue ref only for triggering renders, NOT business logic\n *\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 * The atom resets automatically when the actor transitions to a new view.\n *\n * Signal bridge uses one-shot re-watch pattern:\n * TC39 Signal watchers stop watching after notification, so watcher.watch()\n * must be called inside a microtask after getPending() to re-arm for the\n * next notification.\n *\n * CRITICAL: Never call signal.get() or signal.set() inside the Watcher's\n * notify callback. The callback runs synchronously during the signal graph's\n * dirty-propagation phase. All reads must be deferred to a queueMicrotask.\n *\n * @invariant Actor Authority - Actor decides all state transitions via guards\n * @invariant Passive Infrastructure - Component observes signals, sends events\n * @invariant Signal-Only Reactivity - Business logic state lives in actor signals\n */\n\nimport { defineComponent, ref, toRaw, onUnmounted, h } from \"vue\";\nimport type { PropType } from \"vue\";\nimport { watchSignal } from \"@xmachines/play-signals\";\nimport type { PlayRendererProps } from \"./types.js\";\nimport type { AbstractActor, Viewable, ViewMetadata } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type { ComponentRegistry } from \"@json-render/vue\";\nimport type { StateStore } from \"@json-render/core\";\nimport { Renderer, StateProvider, ActionProvider, VisibilityProvider } from \"@json-render/vue\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { provideActor, type PlayActor } from \"./useActor.js\";\n\nexport default defineComponent({\n\tname: \"PlayRenderer\",\n\tprops: {\n\t\tactor: {\n\t\t\ttype: Object as PropType<AbstractActor<AnyActorLogic> & Viewable>,\n\t\t\trequired: true,\n\t\t},\n\t\tregistry: {\n\t\t\ttype: Object as PropType<ComponentRegistry>,\n\t\t\trequired: true,\n\t\t},\n\t\tstore: {\n\t\t\ttype: Object as PropType<StateStore>,\n\t\t\tdefault: undefined,\n\t\t},\n\t\tactions: {\n\t\t\ttype: Object as PropType<Record<string, string>>,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Unwrap actor from Vue's reactive proxy to access raw Signal objects\n\t\t// CRITICAL: toRaw() preserves Signal's 'this' binding for .get() and watcher operations\n\t\tconst actor = toRaw(props.actor);\n\n\t\t// Provide the raw actor to all descendants via Vue's provide/inject mechanism\n\t\tprovideActor(actor as PlayActor);\n\n\t\t// Get initial value from unwrapped signal\n\t\tconst initialView = actor.currentView.get();\n\n\t\t// Vue ref for triggering re-renders (NOT business logic state)\n\t\t// Signal is source of truth, ref is just Vue's render trigger\n\t\tconst view = ref<ViewMetadata | null>(initialView);\n\n\t\t// Internal per-view store — recreated on each view transition when no external store.\n\t\tlet internalStore: StateStore | null = null;\n\t\tlet lastView: ViewMetadata | null = null;\n\t\t// Key counter: forces Vue to remount StateProvider (and all descendants) when the\n\t\t// store changes. Without this, StateProvider's setup() only runs once — its provide()\n\t\t// call captures the FIRST store and never updates, leaving ActionProvider and child\n\t\t// components referencing a stale store.\n\t\tlet storeKey = 0;\n\n\t\t// Signal watcher for bridging TC39 Signals to Vue reactivity\n\t\tconst unwatch = watchSignal(actor.currentView, (nextView) => {\n\t\t\tview.value = nextView;\n\t\t});\n\n\t\tonUnmounted(() => {\n\t\t\tunwatch();\n\t\t});\n\n\t\t// Use render function to avoid Vue 3.5 SFC template <slot> + jsdom renderSlot crash\n\t\treturn () => {\n\t\t\t// No view — show fallback slot\n\t\t\tif (!view.value) {\n\t\t\t\treturn slots.fallback ? slots.fallback() : null;\n\t\t\t}\n\n\t\t\tconst spec = view.value.spec;\n\n\t\t\t// Resolve the store: external (controlled) or internal per-view atom\n\t\t\tlet store: StateStore;\n\t\t\tif (props.store) {\n\t\t\t\tstore = props.store;\n\t\t\t} else {\n\t\t\t\tif (internalStore === null || lastView !== view.value) {\n\t\t\t\t\tconst initialState = (spec.state as Record<string, unknown>) ?? {};\n\t\t\t\t\tinternalStore = xstateStoreStateStore({ atom: createAtom(initialState) });\n\t\t\t\t\tlastView = view.value;\n\t\t\t\t\tstoreKey++;\n\t\t\t\t}\n\t\t\t\tstore = internalStore;\n\t\t\t}\n\n\t\t\t// Map json-render action names to actor.send() calls\n\t\t\tconst handlers = Object.fromEntries(\n\t\t\t\tObject.entries(props.actions ?? {}).map(([actionName, eventType]) => [\n\t\t\t\t\tactionName,\n\t\t\t\t\tasync (params: Record<string, unknown> = {}) =>\n\t\t\t\t\t\tactor.send({ type: eventType, ...params }),\n\t\t\t\t]),\n\t\t\t);\n\n\t\t\treturn h(StateProvider, { store, key: storeKey }, () =>\n\t\t\t\th(ActionProvider, { handlers }, () =>\n\t\t\t\t\th(VisibilityProvider, {}, () =>\n\t\t\t\t\t\th(Renderer, { spec, registry: props.registry }),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t};\n\t},\n});\n</script>\n"],"mappings":""}
1
+ {"version":3,"file":"PlayRenderer.js","names":[],"sources":["../src/PlayRenderer.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * PlayRenderer - Main Vue renderer component for XMachines Play architecture\n *\n * Architecture (per RESEARCH.md Pattern 1):\n * - Subscribes to actor.currentView signal via Signal.subtle.Watcher\n * - Renders view.spec via @json-render/vue Renderer + StateProvider + ActionProvider\n * - Routes actor.send() calls via ActionProvider handlers\n * - Vue ref only for triggering renders, NOT business logic\n *\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 * The atom resets automatically when the actor transitions to a new view.\n *\n * Signal bridge uses one-shot re-watch pattern:\n * TC39 Signal watchers stop watching after notification, so watcher.watch()\n * must be called inside a microtask after getPending() to re-arm for the\n * next notification.\n *\n * CRITICAL: Never call signal.get() or signal.set() inside the Watcher's\n * notify callback. The callback runs synchronously during the signal graph's\n * dirty-propagation phase. All reads must be deferred to a queueMicrotask.\n *\n * @invariant Actor Authority - Actor decides all state transitions via guards\n * @invariant Passive Infrastructure - Component observes signals, sends events\n * @invariant Signal-Only Reactivity - Business logic state lives in actor signals\n */\n\nimport { defineComponent, ref, toRaw, markRaw, onUnmounted, h } from \"vue\";\nimport type { PropType } from \"vue\";\nimport { watchSignal } from \"@xmachines/play-signals\";\nimport type { PlayRendererProps } from \"./types.js\";\nimport type { AbstractActor, Viewable, ViewMetadata } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type { ComponentRegistry } from \"@json-render/vue\";\nimport type { StateStore } from \"@json-render/core\";\nimport { Renderer, StateProvider, ActionProvider, VisibilityProvider } from \"@json-render/vue\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { provideActor, type PlayActor } from \"./useActor.js\";\n\nexport default defineComponent({\n\tname: \"PlayRenderer\",\n\tprops: {\n\t\tactor: {\n\t\t\ttype: Object as PropType<AbstractActor<AnyActorLogic> & Viewable>,\n\t\t\trequired: true,\n\t\t},\n\t\tregistry: {\n\t\t\ttype: Object as PropType<ComponentRegistry>,\n\t\t\trequired: true,\n\t\t},\n\t\tstore: {\n\t\t\ttype: Object as PropType<StateStore>,\n\t\t\tdefault: undefined,\n\t\t},\n\t\tactions: {\n\t\t\ttype: Object as PropType<Record<string, string>>,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Unwrap actor from Vue's reactive proxy to access raw Signal objects\n\t\t// CRITICAL: toRaw() preserves Signal's 'this' binding for .get() and watcher operations\n\t\tconst actor = toRaw(props.actor);\n\n\t\t// Unwrap the registry prop from Vue's reactive proxy first, then mark the\n\t\t// container and each component value as raw.\n\t\t//\n\t\t// props.registry is already a reactive proxy when setup() runs — reading values\n\t\t// from it yields proxied component objects. toRaw() strips the proxy layer so\n\t\t// markRaw() stamps __v_skip on the real objects, not the proxy wrappers.\n\t\t// Without toRaw(), markRaw() marks throw-away proxy objects that Vue re-creates\n\t\t// on the next prop pass, leaving the underlying component objects unmarked.\n\t\tconst rawRegistry: typeof props.registry = markRaw(\n\t\t\tObject.fromEntries(\n\t\t\t\tObject.entries(toRaw(props.registry)).map(([k, v]) => [k, markRaw(v as object)]),\n\t\t\t) as typeof props.registry,\n\t\t);\n\n\t\t// Provide the raw actor to all descendants via Vue's provide/inject mechanism\n\t\tprovideActor(actor as PlayActor);\n\n\t\t// Get initial value from unwrapped signal\n\t\tconst initialView = actor.currentView.get();\n\n\t\t// Vue ref for triggering re-renders (NOT business logic state)\n\t\t// Signal is source of truth, ref is just Vue's render trigger\n\t\tconst view = ref<ViewMetadata | null>(initialView);\n\n\t\t// Internal per-view store — recreated on each view transition when no external store.\n\t\tlet internalStore: StateStore | null = null;\n\t\tlet lastView: ViewMetadata | null = null;\n\t\t// Key counter: forces Vue to remount StateProvider (and all descendants) when the\n\t\t// store changes. Without this, StateProvider's setup() only runs once — its provide()\n\t\t// call captures the FIRST store and never updates, leaving ActionProvider and child\n\t\t// components referencing a stale store.\n\t\tlet storeKey = 0;\n\n\t\t// Signal watcher for bridging TC39 Signals to Vue reactivity\n\t\tconst unwatch = watchSignal(actor.currentView, (nextView) => {\n\t\t\tview.value = nextView;\n\t\t});\n\n\t\tonUnmounted(() => {\n\t\t\tunwatch();\n\t\t});\n\n\t\t// Use render function to avoid Vue 3.5 SFC template <slot> + jsdom renderSlot crash\n\t\treturn () => {\n\t\t\t// No view — show fallback slot\n\t\t\tif (!view.value) {\n\t\t\t\treturn slots.fallback ? slots.fallback() : null;\n\t\t\t}\n\n\t\t\tconst spec = view.value.spec;\n\n\t\t\t// Resolve the store: external (controlled) or internal per-view atom\n\t\t\tlet store: StateStore;\n\t\t\tif (props.store) {\n\t\t\t\tstore = props.store;\n\t\t\t} else {\n\t\t\t\tif (internalStore === null || lastView !== view.value) {\n\t\t\t\t\tconst initialState = (spec.state as Record<string, unknown>) ?? {};\n\t\t\t\t\tinternalStore = xstateStoreStateStore({ atom: createAtom(initialState) });\n\t\t\t\t\tlastView = view.value;\n\t\t\t\t\tstoreKey++;\n\t\t\t\t}\n\t\t\t\tstore = internalStore;\n\t\t\t}\n\n\t\t\t// Map json-render action names to actor.send() calls\n\t\t\tconst handlers = Object.fromEntries(\n\t\t\t\tObject.entries(props.actions ?? {}).map(([actionName, eventType]) => [\n\t\t\t\t\tactionName,\n\t\t\t\t\tasync (params: Record<string, unknown> = {}) =>\n\t\t\t\t\t\tactor.send({ type: eventType, ...params }),\n\t\t\t\t]),\n\t\t\t);\n\n\t\t\treturn h(StateProvider, { store, key: storeKey }, () =>\n\t\t\t\th(ActionProvider, { handlers }, () =>\n\t\t\t\t\th(VisibilityProvider, {}, () => h(Renderer, { spec, registry: rawRegistry })),\n\t\t\t\t),\n\t\t\t);\n\t\t};\n\t},\n});\n</script>\n"],"mappings":""}
@@ -2,10 +2,10 @@ import { ActionProvider as e, Renderer as t, StateProvider as n, VisibilityProvi
2
2
  import { createAtom as i } from "./node_modules/@xstate/store/dist/store-69e7e2d5.esm.js";
3
3
  import { xstateStoreStateStore as a } from "./node_modules/@json-render/xstate/dist/index.js";
4
4
  import { provideActor as o } from "./useActor.js";
5
- import { defineComponent as s, h as c, onUnmounted as l, ref as u, toRaw as d } from "vue";
6
- import { watchSignal as f } from "@xmachines/play-signals";
5
+ import { defineComponent as s, h as c, markRaw as l, onUnmounted as u, ref as d, toRaw as f } from "vue";
6
+ import { watchSignal as p } from "@xmachines/play-signals";
7
7
  //#region src/PlayRenderer.vue?vue&type=script&lang.ts
8
- var p = s({
8
+ var m = s({
9
9
  name: "PlayRenderer",
10
10
  props: {
11
11
  actor: {
@@ -25,33 +25,33 @@ var p = s({
25
25
  default: () => ({})
26
26
  }
27
27
  },
28
- setup(s, { slots: p }) {
29
- let m = d(s.actor);
30
- o(m);
31
- let h = u(m.currentView.get()), g = null, _ = null, v = 0, y = f(m.currentView, (e) => {
32
- h.value = e;
28
+ setup(s, { slots: m }) {
29
+ let h = f(s.actor), g = l(Object.fromEntries(Object.entries(f(s.registry)).map(([e, t]) => [e, l(t)])));
30
+ o(h);
31
+ let _ = d(h.currentView.get()), v = null, y = null, b = 0, x = p(h.currentView, (e) => {
32
+ _.value = e;
33
33
  });
34
- return l(() => {
35
- y();
34
+ return u(() => {
35
+ x();
36
36
  }), () => {
37
- if (!h.value) return p.fallback ? p.fallback() : null;
38
- let o = h.value.spec, l;
39
- s.store ? l = s.store : ((g === null || _ !== h.value) && (g = a({ atom: i(o.state ?? {}) }), _ = h.value, v++), l = g);
40
- let u = Object.fromEntries(Object.entries(s.actions ?? {}).map(([e, t]) => [e, async (e = {}) => m.send({
37
+ if (!_.value) return m.fallback ? m.fallback() : null;
38
+ let o = _.value.spec, l;
39
+ s.store ? l = s.store : ((v === null || y !== _.value) && (v = a({ atom: i(o.state ?? {}) }), y = _.value, b++), l = v);
40
+ let u = Object.fromEntries(Object.entries(s.actions ?? {}).map(([e, t]) => [e, async (e = {}) => h.send({
41
41
  type: t,
42
42
  ...e
43
43
  })]));
44
44
  return c(n, {
45
45
  store: l,
46
- key: v
46
+ key: b
47
47
  }, () => c(e, { handlers: u }, () => c(r, {}, () => c(t, {
48
48
  spec: o,
49
- registry: s.registry
49
+ registry: g
50
50
  }))));
51
51
  };
52
52
  }
53
53
  });
54
54
  //#endregion
55
- export { p as default };
55
+ export { m as default };
56
56
 
57
57
  //# sourceMappingURL=PlayRenderer.vue_vue_type_script_lang.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PlayRenderer.vue_vue_type_script_lang.js","names":[],"sources":["../src/PlayRenderer.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * PlayRenderer - Main Vue renderer component for XMachines Play architecture\n *\n * Architecture (per RESEARCH.md Pattern 1):\n * - Subscribes to actor.currentView signal via Signal.subtle.Watcher\n * - Renders view.spec via @json-render/vue Renderer + StateProvider + ActionProvider\n * - Routes actor.send() calls via ActionProvider handlers\n * - Vue ref only for triggering renders, NOT business logic\n *\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 * The atom resets automatically when the actor transitions to a new view.\n *\n * Signal bridge uses one-shot re-watch pattern:\n * TC39 Signal watchers stop watching after notification, so watcher.watch()\n * must be called inside a microtask after getPending() to re-arm for the\n * next notification.\n *\n * CRITICAL: Never call signal.get() or signal.set() inside the Watcher's\n * notify callback. The callback runs synchronously during the signal graph's\n * dirty-propagation phase. All reads must be deferred to a queueMicrotask.\n *\n * @invariant Actor Authority - Actor decides all state transitions via guards\n * @invariant Passive Infrastructure - Component observes signals, sends events\n * @invariant Signal-Only Reactivity - Business logic state lives in actor signals\n */\n\nimport { defineComponent, ref, toRaw, onUnmounted, h } from \"vue\";\nimport type { PropType } from \"vue\";\nimport { watchSignal } from \"@xmachines/play-signals\";\nimport type { PlayRendererProps } from \"./types.js\";\nimport type { AbstractActor, Viewable, ViewMetadata } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type { ComponentRegistry } from \"@json-render/vue\";\nimport type { StateStore } from \"@json-render/core\";\nimport { Renderer, StateProvider, ActionProvider, VisibilityProvider } from \"@json-render/vue\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { provideActor, type PlayActor } from \"./useActor.js\";\n\nexport default defineComponent({\n\tname: \"PlayRenderer\",\n\tprops: {\n\t\tactor: {\n\t\t\ttype: Object as PropType<AbstractActor<AnyActorLogic> & Viewable>,\n\t\t\trequired: true,\n\t\t},\n\t\tregistry: {\n\t\t\ttype: Object as PropType<ComponentRegistry>,\n\t\t\trequired: true,\n\t\t},\n\t\tstore: {\n\t\t\ttype: Object as PropType<StateStore>,\n\t\t\tdefault: undefined,\n\t\t},\n\t\tactions: {\n\t\t\ttype: Object as PropType<Record<string, string>>,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Unwrap actor from Vue's reactive proxy to access raw Signal objects\n\t\t// CRITICAL: toRaw() preserves Signal's 'this' binding for .get() and watcher operations\n\t\tconst actor = toRaw(props.actor);\n\n\t\t// Provide the raw actor to all descendants via Vue's provide/inject mechanism\n\t\tprovideActor(actor as PlayActor);\n\n\t\t// Get initial value from unwrapped signal\n\t\tconst initialView = actor.currentView.get();\n\n\t\t// Vue ref for triggering re-renders (NOT business logic state)\n\t\t// Signal is source of truth, ref is just Vue's render trigger\n\t\tconst view = ref<ViewMetadata | null>(initialView);\n\n\t\t// Internal per-view store — recreated on each view transition when no external store.\n\t\tlet internalStore: StateStore | null = null;\n\t\tlet lastView: ViewMetadata | null = null;\n\t\t// Key counter: forces Vue to remount StateProvider (and all descendants) when the\n\t\t// store changes. Without this, StateProvider's setup() only runs once — its provide()\n\t\t// call captures the FIRST store and never updates, leaving ActionProvider and child\n\t\t// components referencing a stale store.\n\t\tlet storeKey = 0;\n\n\t\t// Signal watcher for bridging TC39 Signals to Vue reactivity\n\t\tconst unwatch = watchSignal(actor.currentView, (nextView) => {\n\t\t\tview.value = nextView;\n\t\t});\n\n\t\tonUnmounted(() => {\n\t\t\tunwatch();\n\t\t});\n\n\t\t// Use render function to avoid Vue 3.5 SFC template <slot> + jsdom renderSlot crash\n\t\treturn () => {\n\t\t\t// No view — show fallback slot\n\t\t\tif (!view.value) {\n\t\t\t\treturn slots.fallback ? slots.fallback() : null;\n\t\t\t}\n\n\t\t\tconst spec = view.value.spec;\n\n\t\t\t// Resolve the store: external (controlled) or internal per-view atom\n\t\t\tlet store: StateStore;\n\t\t\tif (props.store) {\n\t\t\t\tstore = props.store;\n\t\t\t} else {\n\t\t\t\tif (internalStore === null || lastView !== view.value) {\n\t\t\t\t\tconst initialState = (spec.state as Record<string, unknown>) ?? {};\n\t\t\t\t\tinternalStore = xstateStoreStateStore({ atom: createAtom(initialState) });\n\t\t\t\t\tlastView = view.value;\n\t\t\t\t\tstoreKey++;\n\t\t\t\t}\n\t\t\t\tstore = internalStore;\n\t\t\t}\n\n\t\t\t// Map json-render action names to actor.send() calls\n\t\t\tconst handlers = Object.fromEntries(\n\t\t\t\tObject.entries(props.actions ?? {}).map(([actionName, eventType]) => [\n\t\t\t\t\tactionName,\n\t\t\t\t\tasync (params: Record<string, unknown> = {}) =>\n\t\t\t\t\t\tactor.send({ type: eventType, ...params }),\n\t\t\t\t]),\n\t\t\t);\n\n\t\t\treturn h(StateProvider, { store, key: storeKey }, () =>\n\t\t\t\th(ActionProvider, { handlers }, () =>\n\t\t\t\t\th(VisibilityProvider, {}, () =>\n\t\t\t\t\t\th(Renderer, { spec, registry: props.registry }),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t};\n\t},\n});\n</script>\n"],"mappings":";;;;;;;AAyCA,IAAA,IAAe,EAAgB;CAC9B,MAAM;CACN,OAAO;EACN,OAAO;GACN,MAAM;GACN,UAAU;GACV;EACD,UAAU;GACT,MAAM;GACN,UAAU;GACV;EACD,OAAO;GACN,MAAM;GACN,SAAS,KAAA;GACT;EACD,SAAS;GACR,MAAM;GACN,gBAAgB,EAAE;GAClB;EACD;CACD,MAAM,GAAO,EAAE,YAAS;EAGvB,IAAM,IAAQ,EAAM,EAAM,MAAM;AAGhC,IAAa,EAAmB;EAOhC,IAAM,IAAO,EAJO,EAAM,YAAY,KAAK,CAIO,EAG9C,IAAmC,MACnC,IAAgC,MAKhC,IAAW,GAGT,IAAU,EAAY,EAAM,cAAc,MAAa;AAC5D,KAAK,QAAQ;IACZ;AAOF,SALA,QAAkB;AACjB,MAAS;IACR,QAGW;AAEZ,OAAI,CAAC,EAAK,MACT,QAAO,EAAM,WAAW,EAAM,UAAS,GAAI;GAG5C,IAAM,IAAO,EAAK,MAAM,MAGpB;AACJ,GAAI,EAAM,QACT,IAAQ,EAAM,UAEV,MAAkB,QAAQ,MAAa,EAAK,WAE/C,IAAgB,EAAsB,EAAE,MAAM,EADxB,EAAK,SAAqC,EAAE,CACG,EAAG,CAAC,EACzE,IAAW,EAAK,OAChB,MAED,IAAQ;GAIT,IAAM,IAAW,OAAO,YACvB,OAAO,QAAQ,EAAM,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,GAAY,OAAe,CACpE,GACA,OAAO,IAAkC,EAAE,KAC1C,EAAM,KAAK;IAAE,MAAM;IAAW,GAAG;IAAQ,CAAC,CAC3C,CAAC,CACF;AAED,UAAO,EAAE,GAAe;IAAE;IAAO,KAAK;IAAU,QAC/C,EAAE,GAAgB,EAAE,aAAU,QAC7B,EAAE,GAAoB,EAAE,QACvB,EAAE,GAAU;IAAE;IAAM,UAAU,EAAM;IAAU,CAAC,CAC/C,CACD,CACD;;;CAGH,CAAC"}
1
+ {"version":3,"file":"PlayRenderer.vue_vue_type_script_lang.js","names":[],"sources":["../src/PlayRenderer.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * PlayRenderer - Main Vue renderer component for XMachines Play architecture\n *\n * Architecture (per RESEARCH.md Pattern 1):\n * - Subscribes to actor.currentView signal via Signal.subtle.Watcher\n * - Renders view.spec via @json-render/vue Renderer + StateProvider + ActionProvider\n * - Routes actor.send() calls via ActionProvider handlers\n * - Vue ref only for triggering renders, NOT business logic\n *\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 * The atom resets automatically when the actor transitions to a new view.\n *\n * Signal bridge uses one-shot re-watch pattern:\n * TC39 Signal watchers stop watching after notification, so watcher.watch()\n * must be called inside a microtask after getPending() to re-arm for the\n * next notification.\n *\n * CRITICAL: Never call signal.get() or signal.set() inside the Watcher's\n * notify callback. The callback runs synchronously during the signal graph's\n * dirty-propagation phase. All reads must be deferred to a queueMicrotask.\n *\n * @invariant Actor Authority - Actor decides all state transitions via guards\n * @invariant Passive Infrastructure - Component observes signals, sends events\n * @invariant Signal-Only Reactivity - Business logic state lives in actor signals\n */\n\nimport { defineComponent, ref, toRaw, markRaw, onUnmounted, h } from \"vue\";\nimport type { PropType } from \"vue\";\nimport { watchSignal } from \"@xmachines/play-signals\";\nimport type { PlayRendererProps } from \"./types.js\";\nimport type { AbstractActor, Viewable, ViewMetadata } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type { ComponentRegistry } from \"@json-render/vue\";\nimport type { StateStore } from \"@json-render/core\";\nimport { Renderer, StateProvider, ActionProvider, VisibilityProvider } from \"@json-render/vue\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { provideActor, type PlayActor } from \"./useActor.js\";\n\nexport default defineComponent({\n\tname: \"PlayRenderer\",\n\tprops: {\n\t\tactor: {\n\t\t\ttype: Object as PropType<AbstractActor<AnyActorLogic> & Viewable>,\n\t\t\trequired: true,\n\t\t},\n\t\tregistry: {\n\t\t\ttype: Object as PropType<ComponentRegistry>,\n\t\t\trequired: true,\n\t\t},\n\t\tstore: {\n\t\t\ttype: Object as PropType<StateStore>,\n\t\t\tdefault: undefined,\n\t\t},\n\t\tactions: {\n\t\t\ttype: Object as PropType<Record<string, string>>,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Unwrap actor from Vue's reactive proxy to access raw Signal objects\n\t\t// CRITICAL: toRaw() preserves Signal's 'this' binding for .get() and watcher operations\n\t\tconst actor = toRaw(props.actor);\n\n\t\t// Unwrap the registry prop from Vue's reactive proxy first, then mark the\n\t\t// container and each component value as raw.\n\t\t//\n\t\t// props.registry is already a reactive proxy when setup() runs — reading values\n\t\t// from it yields proxied component objects. toRaw() strips the proxy layer so\n\t\t// markRaw() stamps __v_skip on the real objects, not the proxy wrappers.\n\t\t// Without toRaw(), markRaw() marks throw-away proxy objects that Vue re-creates\n\t\t// on the next prop pass, leaving the underlying component objects unmarked.\n\t\tconst rawRegistry: typeof props.registry = markRaw(\n\t\t\tObject.fromEntries(\n\t\t\t\tObject.entries(toRaw(props.registry)).map(([k, v]) => [k, markRaw(v as object)]),\n\t\t\t) as typeof props.registry,\n\t\t);\n\n\t\t// Provide the raw actor to all descendants via Vue's provide/inject mechanism\n\t\tprovideActor(actor as PlayActor);\n\n\t\t// Get initial value from unwrapped signal\n\t\tconst initialView = actor.currentView.get();\n\n\t\t// Vue ref for triggering re-renders (NOT business logic state)\n\t\t// Signal is source of truth, ref is just Vue's render trigger\n\t\tconst view = ref<ViewMetadata | null>(initialView);\n\n\t\t// Internal per-view store — recreated on each view transition when no external store.\n\t\tlet internalStore: StateStore | null = null;\n\t\tlet lastView: ViewMetadata | null = null;\n\t\t// Key counter: forces Vue to remount StateProvider (and all descendants) when the\n\t\t// store changes. Without this, StateProvider's setup() only runs once — its provide()\n\t\t// call captures the FIRST store and never updates, leaving ActionProvider and child\n\t\t// components referencing a stale store.\n\t\tlet storeKey = 0;\n\n\t\t// Signal watcher for bridging TC39 Signals to Vue reactivity\n\t\tconst unwatch = watchSignal(actor.currentView, (nextView) => {\n\t\t\tview.value = nextView;\n\t\t});\n\n\t\tonUnmounted(() => {\n\t\t\tunwatch();\n\t\t});\n\n\t\t// Use render function to avoid Vue 3.5 SFC template <slot> + jsdom renderSlot crash\n\t\treturn () => {\n\t\t\t// No view — show fallback slot\n\t\t\tif (!view.value) {\n\t\t\t\treturn slots.fallback ? slots.fallback() : null;\n\t\t\t}\n\n\t\t\tconst spec = view.value.spec;\n\n\t\t\t// Resolve the store: external (controlled) or internal per-view atom\n\t\t\tlet store: StateStore;\n\t\t\tif (props.store) {\n\t\t\t\tstore = props.store;\n\t\t\t} else {\n\t\t\t\tif (internalStore === null || lastView !== view.value) {\n\t\t\t\t\tconst initialState = (spec.state as Record<string, unknown>) ?? {};\n\t\t\t\t\tinternalStore = xstateStoreStateStore({ atom: createAtom(initialState) });\n\t\t\t\t\tlastView = view.value;\n\t\t\t\t\tstoreKey++;\n\t\t\t\t}\n\t\t\t\tstore = internalStore;\n\t\t\t}\n\n\t\t\t// Map json-render action names to actor.send() calls\n\t\t\tconst handlers = Object.fromEntries(\n\t\t\t\tObject.entries(props.actions ?? {}).map(([actionName, eventType]) => [\n\t\t\t\t\tactionName,\n\t\t\t\t\tasync (params: Record<string, unknown> = {}) =>\n\t\t\t\t\t\tactor.send({ type: eventType, ...params }),\n\t\t\t\t]),\n\t\t\t);\n\n\t\t\treturn h(StateProvider, { store, key: storeKey }, () =>\n\t\t\t\th(ActionProvider, { handlers }, () =>\n\t\t\t\t\th(VisibilityProvider, {}, () => h(Renderer, { spec, registry: rawRegistry })),\n\t\t\t\t),\n\t\t\t);\n\t\t};\n\t},\n});\n</script>\n"],"mappings":";;;;;;;AAyCA,IAAA,IAAe,EAAgB;CAC9B,MAAM;CACN,OAAO;EACN,OAAO;GACN,MAAM;GACN,UAAU;GACV;EACD,UAAU;GACT,MAAM;GACN,UAAU;GACV;EACD,OAAO;GACN,MAAM;GACN,SAAS,KAAA;GACT;EACD,SAAS;GACR,MAAM;GACN,gBAAgB,EAAE;GAClB;EACD;CACD,MAAM,GAAO,EAAE,YAAS;EAGvB,IAAM,IAAQ,EAAM,EAAM,MAAM,EAU1B,IAAqC,EAC1C,OAAO,YACN,OAAO,QAAQ,EAAM,EAAM,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,EAAQ,EAAY,CAAC,CAAC,CACjF,CACA;AAGD,IAAa,EAAmB;EAOhC,IAAM,IAAO,EAJO,EAAM,YAAY,KAAK,CAIO,EAG9C,IAAmC,MACnC,IAAgC,MAKhC,IAAW,GAGT,IAAU,EAAY,EAAM,cAAc,MAAa;AAC5D,KAAK,QAAQ;IACZ;AAOF,SALA,QAAkB;AACjB,MAAS;IACR,QAGW;AAEZ,OAAI,CAAC,EAAK,MACT,QAAO,EAAM,WAAW,EAAM,UAAS,GAAI;GAG5C,IAAM,IAAO,EAAK,MAAM,MAGpB;AACJ,GAAI,EAAM,QACT,IAAQ,EAAM,UAEV,MAAkB,QAAQ,MAAa,EAAK,WAE/C,IAAgB,EAAsB,EAAE,MAAM,EADxB,EAAK,SAAqC,EAAE,CACG,EAAG,CAAC,EACzE,IAAW,EAAK,OAChB,MAED,IAAQ;GAIT,IAAM,IAAW,OAAO,YACvB,OAAO,QAAQ,EAAM,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,GAAY,OAAe,CACpE,GACA,OAAO,IAAkC,EAAE,KAC1C,EAAM,KAAK;IAAE,MAAM;IAAW,GAAG;IAAQ,CAAC,CAC3C,CAAC,CACF;AAED,UAAO,EAAE,GAAe;IAAE;IAAO,KAAK;IAAU,QAC/C,EAAE,GAAgB,EAAE,aAAU,QAC7B,EAAE,GAAoB,EAAE,QAAQ,EAAE,GAAU;IAAE;IAAM,UAAU;IAAa,CAAC,CAAC,CAC7E,CACD;;;CAGH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-vue",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.22",
4
4
  "description": "Vue renderer for XMachines Play architecture",
5
5
  "keywords": [
6
6
  "reactive",
@@ -34,15 +34,17 @@
34
34
  },
35
35
  "scripts": {
36
36
  "build": "vite build && tsc --build --force",
37
- "clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__",
37
+ "clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__ node_modules/.vite-temp",
38
+ "lint": "oxlint .",
39
+ "format": "oxfmt .",
38
40
  "test": "vitest",
39
41
  "test:watch": "vitest",
40
42
  "prepublishOnly": "npm run build"
41
43
  },
42
44
  "dependencies": {
43
- "@xmachines/play": "1.0.0-beta.21",
44
- "@xmachines/play-actor": "1.0.0-beta.21",
45
- "@xmachines/play-signals": "1.0.0-beta.21"
45
+ "@xmachines/play": "1.0.0-beta.22",
46
+ "@xmachines/play-actor": "1.0.0-beta.22",
47
+ "@xmachines/play-signals": "1.0.0-beta.22"
46
48
  },
47
49
  "devDependencies": {
48
50
  "@json-render/core": "^0.16.0",
@@ -51,7 +53,7 @@
51
53
  "@types/node": "^25.5.0",
52
54
  "@vitejs/plugin-vue": "^6.0.5",
53
55
  "@vue/test-utils": "^2.4.6",
54
- "@xmachines/shared": "1.0.0-beta.21",
56
+ "@xmachines/shared": "1.0.0-beta.22",
55
57
  "@xstate/store": ">=3.17.0",
56
58
  "typescript": "^5.9.3 || ^6.0.2",
57
59
  "vitest": "^4.1.2",