@xmachines/play-react 1.0.0-beta.21 → 1.0.0-beta.23

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/react @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/react`
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
@@ -187,6 +191,9 @@ Main component. Subscribes to `actor.currentView` and renders the spec.
187
191
 
188
192
  **`registry`** — Built with `defineRegistry(catalog, { components, actions })` from `@xmachines/play-react`.
189
193
 
194
+ `defineRegistry` also accepts `onRenderError(error, elementType)`, which receives errors
195
+ caught by `@json-render/react`'s inner element boundary before the default logger is used.
196
+
190
197
  **`actions`** — Maps json-render action names (from spec `on` bindings) to XState event type strings. Values are type-checked against `EventFromLogic<TLogic>["type"]` when `TLogic` is specified:
191
198
 
192
199
  ```tsx
@@ -213,6 +220,19 @@ const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "
213
220
  <PlayRenderer actor={actor} registry={registry} store={store} actions={{ login: "auth.login" }} />;
214
221
  ```
215
222
 
223
+ **Inner render errors** — You can intercept catalog component render failures without
224
+ overriding the outer `PlayErrorBoundary`:
225
+
226
+ ```tsx
227
+ const { registry } = defineRegistry(catalog, {
228
+ components: { Login, Dashboard },
229
+ actions: { login: async () => {}, logout: async () => {} },
230
+ onRenderError(error, elementType) {
231
+ reportExpectedRenderError(error, elementType);
232
+ },
233
+ });
234
+ ```
235
+
216
236
  **`fallback`** — Shown when `actor.currentView.get()` is `null` (machine in a no-view state, or during initialisation).
217
237
 
218
238
  ---
@@ -1 +1 @@
1
- {"version":3,"file":"PlayRenderer.d.ts","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;AAOhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAYpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAoEpD,CAAC"}
1
+ {"version":3,"file":"PlayRenderer.d.ts","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;AAOhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAYpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAqEpD,CAAC"}
@@ -57,7 +57,7 @@ function createViewStore(initialState) {
57
57
  * @param props - Component props
58
58
  * @returns React element rendering current view from actor
59
59
  */
60
- export const PlayRenderer = ({ actor, registry, store: externalStore, fallback = null, actions = {}, }) => {
60
+ export const PlayRenderer = ({ actor, registry, store: externalStore, fallback = null, onError, actions = {}, }) => {
61
61
  // React state for triggering re-renders (NOT business logic state)
62
62
  // Signal is source of truth, useState is just React's render trigger
63
63
  const [view, setView] = useState(() => actor.currentView.get());
@@ -98,6 +98,6 @@ export const PlayRenderer = ({ actor, registry, store: externalStore, fallback =
98
98
  actionName,
99
99
  async (params = {}) => actor.send({ type: eventType, ...params }),
100
100
  ]));
101
- return (_jsx(ActorContext.Provider, { value: actor, children: _jsx(PlayErrorBoundary, { fallback: fallback, children: _jsx(StateProvider, { store: store, children: _jsx(ActionProvider, { handlers: handlers, children: _jsx(VisibilityProvider, { children: _jsx(Renderer, { spec: view.spec ?? null, registry: registry }) }) }) }) }) }));
101
+ return (_jsx(ActorContext.Provider, { value: actor, children: _jsx(PlayErrorBoundary, { fallback: fallback, ...(onError && { onError }), children: _jsx(StateProvider, { store: store, children: _jsx(ActionProvider, { handlers: handlers, children: _jsx(VisibilityProvider, { children: _jsx(Renderer, { spec: view.spec ?? null, registry: registry }) }) }) }) }) }));
102
102
  };
103
103
  //# sourceMappingURL=PlayRenderer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -22,6 +22,8 @@ import type { AnyActorLogic, EventFromLogic } from "xstate";
22
22
  * store is the single source of truth for UI state. When omitted, a fresh
23
23
  * @xstate/store atom is created internally per view transition, seeded from spec.state.
24
24
  * @property fallback - Optional component shown when currentView is null
25
+ * @property onError - Optional callback invoked when a catalog component throws during render.
26
+ * Receives the error and React ErrorInfo — use to forward to Sentry, Datadog, etc.
25
27
  * @property actions - Maps json-render action names to XState event type strings.
26
28
  * Values are constrained to `EventFromLogic<TLogic>["type"]` — passing a non-existent
27
29
  * event type string is a compile error when TLogic is specified.
@@ -40,8 +42,19 @@ export interface PlayRendererProps<TLogic extends AnyActorLogic = AnyActorLogic>
40
42
  * new view, mirroring the actor's currentView lifecycle.
41
43
  */
42
44
  store?: StateStore;
43
- /** Optional component shown when currentView is null */
45
+ /** Optional component shown when currentView is null or a catalog component throws */
44
46
  fallback?: React.ReactNode;
47
+ /**
48
+ * Optional error handler callback invoked when a catalog component throws during render.
49
+ * Forwarded directly to the internal `PlayErrorBoundary` — use to integrate with
50
+ * production observability tools (Sentry, Datadog, etc.).
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * <PlayRenderer actor={actor} registry={registry} onError={Sentry.captureException} />
55
+ * ```
56
+ */
57
+ onError?: (error: Error, info: React.ErrorInfo) => void;
45
58
  /**
46
59
  * Maps json-render action names to XState event type strings.
47
60
  * Values are constrained to valid event types for TLogic — wrong event type strings
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE5D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,iBAAiB,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IAC9E,4EAA4E;IAC5E,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC;IAExC,oEAAoE;IACpE,QAAQ,EAAE,iBAAiB,CAAC;IAE5B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IAEnB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC;CACnE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,iBAAiB,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IAC9E,4EAA4E;IAC5E,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC;IAExC,oEAAoE;IACpE,QAAQ,EAAE,iBAAiB,CAAC;IAE5B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IAEnB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IAExD;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC;CACnE"}
@@ -51,6 +51,12 @@
51
51
  * **Implementation note:** We wrap the callback in Signal.Computed because
52
52
  * Signal.subtle.Watcher cannot automatically track arbitrary function calls.
53
53
  * The Computed handles dependency tracking, and the Watcher monitors it.
54
+ *
55
+ * **Memory safety (Phase 29):**
56
+ * - `disposed` flag prevents post-cleanup callback execution: if cleanup is
57
+ * called before a pending microtask fires, the microtask returns early.
58
+ * - `needsEnqueue` guard dedups rapid synchronous signal changes: only one
59
+ * microtask is ever queued per batch of synchronous mutations.
54
60
  */
55
61
  export declare const useSignalEffect: (callback: () => void | (() => void)) => void;
56
62
  //# sourceMappingURL=useSignalEffect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSignalEffect.d.ts","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,KAAG,IA6DrE,CAAC"}
1
+ {"version":3,"file":"useSignalEffect.d.ts","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,KAAG,IA+DrE,CAAC"}
@@ -58,6 +58,12 @@ const EFFECT_RUN_MARKER = Symbol("effect-run");
58
58
  * **Implementation note:** We wrap the callback in Signal.Computed because
59
59
  * Signal.subtle.Watcher cannot automatically track arbitrary function calls.
60
60
  * The Computed handles dependency tracking, and the Watcher monitors it.
61
+ *
62
+ * **Memory safety (Phase 29):**
63
+ * - `disposed` flag prevents post-cleanup callback execution: if cleanup is
64
+ * called before a pending microtask fires, the microtask returns early.
65
+ * - `needsEnqueue` guard dedups rapid synchronous signal changes: only one
66
+ * microtask is ever queued per batch of synchronous mutations.
61
67
  */
62
68
  export const useSignalEffect = (callback) => {
63
69
  // Store callback in a ref so the effect closure always calls the latest version
@@ -70,6 +76,7 @@ export const useSignalEffect = (callback) => {
70
76
  useEffect(() => {
71
77
  let cleanup;
72
78
  let needsEnqueue = true;
79
+ let disposed = false;
73
80
  // Wrap callback in a Computed to automatically track dependencies
74
81
  // The Computed will re-evaluate when any accessed signal changes
75
82
  const effect = new Signal.Computed(() => {
@@ -84,19 +91,21 @@ export const useSignalEffect = (callback) => {
84
91
  // Create watcher to detect when Computed needs re-evaluation
85
92
  const watcher = new Signal.subtle.Watcher(() => {
86
93
  // Batching flag prevents multiple microtasks for rapid changes
87
- if (needsEnqueue) {
88
- needsEnqueue = false;
89
- // Schedule microtask to batch updates (prevent React thrashing)
90
- queueMicrotask(() => {
91
- needsEnqueue = true;
92
- // Re-evaluate the Computed (runs callback with new signal values)
93
- effect.get();
94
- // Force React re-render
95
- forceUpdate();
96
- // Re-watch for next change
97
- watcher.watch();
98
- });
99
- }
94
+ if (disposed || !needsEnqueue)
95
+ return;
96
+ needsEnqueue = false;
97
+ // Schedule microtask to batch updates (prevent React thrashing)
98
+ queueMicrotask(() => {
99
+ if (disposed)
100
+ return;
101
+ needsEnqueue = true;
102
+ // Re-evaluate the Computed (runs callback with new signal values)
103
+ effect.get();
104
+ // Force React re-render
105
+ forceUpdate();
106
+ // Re-watch for next change
107
+ watcher.watch();
108
+ });
100
109
  });
101
110
  // Watch the Computed signal
102
111
  watcher.watch(effect);
@@ -104,6 +113,7 @@ export const useSignalEffect = (callback) => {
104
113
  effect.get();
105
114
  // Cleanup on unmount
106
115
  return () => {
116
+ disposed = true;
107
117
  if (typeof cleanup === "function")
108
118
  cleanup();
109
119
  // Unwatch to stop receiving signal updates
@@ -1 +1 @@
1
- {"version":3,"file":"useSignalEffect.js","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEjD;;;GAGG;AACH,MAAM,iBAAiB,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAAmC,EAAQ,EAAE;IAC5E,gFAAgF;IAChF,2FAA2F;IAC3F,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,8EAA8E;IAC9E,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IAE/B,8EAA8E;IAC9E,MAAM,CAAC,EAAE,WAAW,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAEpD,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,OAA4B,CAAC;QACjC,IAAI,YAAY,GAAG,IAAI,CAAC;QAExB,kEAAkE;QAClE,iEAAiE;QACjE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE;YACvC,mCAAmC;YACnC,IAAI,OAAO,OAAO,KAAK,UAAU;gBAAE,OAAO,EAAE,CAAC;YAE7C,kEAAkE;YAClE,OAAO,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;YAEhC,yDAAyD;YACzD,OAAO,iBAAiB,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,6DAA6D;QAC7D,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;YAC9C,+DAA+D;YAC/D,IAAI,YAAY,EAAE,CAAC;gBAClB,YAAY,GAAG,KAAK,CAAC;gBACrB,gEAAgE;gBAChE,cAAc,CAAC,GAAG,EAAE;oBACnB,YAAY,GAAG,IAAI,CAAC;oBAEpB,kEAAkE;oBAClE,MAAM,CAAC,GAAG,EAAE,CAAC;oBAEb,wBAAwB;oBACxB,WAAW,EAAE,CAAC;oBAEd,2BAA2B;oBAC3B,OAAO,CAAC,KAAK,EAAE,CAAC;gBACjB,CAAC,CAAC,CAAC;YACJ,CAAC;QACF,CAAC,CAAC,CAAC;QAEH,4BAA4B;QAC5B,OAAO,CAAC,KAAK,CAAC,MAAkC,CAAC,CAAC;QAElD,yCAAyC;QACzC,MAAM,CAAC,GAAG,EAAE,CAAC;QAEb,qBAAqB;QACrB,OAAO,GAAG,EAAE;YACX,IAAI,OAAO,OAAO,KAAK,UAAU;gBAAE,OAAO,EAAE,CAAC;YAC7C,2CAA2C;YAC3C,OAAO,CAAC,OAAO,CAAC,MAAkC,CAAC,CAAC;QACrD,CAAC,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,sEAAsE;AAC/E,CAAC,CAAC"}
1
+ {"version":3,"file":"useSignalEffect.js","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEjD;;;GAGG;AACH,MAAM,iBAAiB,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAAmC,EAAQ,EAAE;IAC5E,gFAAgF;IAChF,2FAA2F;IAC3F,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,8EAA8E;IAC9E,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IAE/B,8EAA8E;IAC9E,MAAM,CAAC,EAAE,WAAW,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAEpD,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,OAA4B,CAAC;QACjC,IAAI,YAAY,GAAG,IAAI,CAAC;QACxB,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,kEAAkE;QAClE,iEAAiE;QACjE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE;YACvC,mCAAmC;YACnC,IAAI,OAAO,OAAO,KAAK,UAAU;gBAAE,OAAO,EAAE,CAAC;YAE7C,kEAAkE;YAClE,OAAO,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;YAEhC,yDAAyD;YACzD,OAAO,iBAAiB,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,6DAA6D;QAC7D,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;YAC9C,+DAA+D;YAC/D,IAAI,QAAQ,IAAI,CAAC,YAAY;gBAAE,OAAO;YACtC,YAAY,GAAG,KAAK,CAAC;YACrB,gEAAgE;YAChE,cAAc,CAAC,GAAG,EAAE;gBACnB,IAAI,QAAQ;oBAAE,OAAO;gBACrB,YAAY,GAAG,IAAI,CAAC;gBAEpB,kEAAkE;gBAClE,MAAM,CAAC,GAAG,EAAE,CAAC;gBAEb,wBAAwB;gBACxB,WAAW,EAAE,CAAC;gBAEd,2BAA2B;gBAC3B,OAAO,CAAC,KAAK,EAAE,CAAC;YACjB,CAAC,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,4BAA4B;QAC5B,OAAO,CAAC,KAAK,CAAC,MAAkC,CAAC,CAAC;QAElD,yCAAyC;QACzC,MAAM,CAAC,GAAG,EAAE,CAAC;QAEb,qBAAqB;QACrB,OAAO,GAAG,EAAE;YACX,QAAQ,GAAG,IAAI,CAAC;YAChB,IAAI,OAAO,OAAO,KAAK,UAAU;gBAAE,OAAO,EAAE,CAAC;YAC7C,2CAA2C;YAC3C,OAAO,CAAC,OAAO,CAAC,MAAkC,CAAC,CAAC;QACrD,CAAC,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,sEAAsE;AAC/E,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-react",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.23",
4
4
  "description": "React renderer for XMachines Play architecture with signal-driven rendering",
5
5
  "keywords": [
6
6
  "actor",
@@ -36,15 +36,17 @@
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc --build",
39
- "clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__",
39
+ "clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__ node_modules/.vite*",
40
+ "lint": "oxlint .",
41
+ "format": "oxfmt .",
40
42
  "test": "vitest",
41
43
  "test:vitest": "vitest run",
42
44
  "test:browser": "vitest --browser.enabled --browser.name=chromium test/browser",
43
45
  "prepublishOnly": "npm run build"
44
46
  },
45
47
  "dependencies": {
46
- "@xmachines/play-actor": "1.0.0-beta.21",
47
- "@xmachines/play-signals": "1.0.0-beta.21"
48
+ "@xmachines/play-actor": "1.0.0-beta.23",
49
+ "@xmachines/play-signals": "1.0.0-beta.23"
48
50
  },
49
51
  "devDependencies": {
50
52
  "@json-render/core": "^0.16.0",
@@ -55,9 +57,11 @@
55
57
  "@types/node": "^25.5.0",
56
58
  "@types/react": "^19.2.14",
57
59
  "@types/react-dom": "^19.2.3",
58
- "@xmachines/shared": "1.0.0-beta.21",
60
+ "@xmachines/shared": "1.0.0-beta.23",
59
61
  "@xstate/store": ">=3.17.0",
60
62
  "jsdom": "^29.0.1",
63
+ "oxfmt": "^0.43.0",
64
+ "oxlint": "^1.57.0",
61
65
  "react": "^19.2.4",
62
66
  "react-dom": "^19.2.4",
63
67
  "typescript": "^5.9.3 || ^6.0.2",