@xmachines/play-react 1.0.0-beta.46 → 1.0.0-beta.47

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.
Files changed (2) hide show
  1. package/README.md +109 -320
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -1,172 +1,54 @@
1
+ <!-- generated-by: gsd-doc-writer -->
2
+
1
3
  # @xmachines/play-react
2
4
 
3
- **React renderer for XMachines Play Architecture**
5
+ React renderer for XMachines Play architecture with signal-driven rendering.
4
6
 
5
- Bridges TC39 Signal-driven actors to React's render cycle. Business logic stays in the actor; React is purely a rendering target.
7
+ Part of the [xmachines-js monorepo](../../README.md).
6
8
 
7
- ## Overview
9
+ ## Installation
8
10
 
9
- `@xmachines/play-react` provides `PlayRenderer`, a React component that:
11
+ ```bash
12
+ npm install @xmachines/play-react
13
+ ```
10
14
 
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/react`
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)
15
+ **Peer dependencies** (must be installed separately):
15
16
 
16
- Per [Play RFC](../docs/rfc/play.md):
17
+ ```bash
18
+ npm install react react-dom xstate @xstate/store @json-render/react @json-render/core @json-render/xstate
19
+ ```
17
20
 
18
- - **Actor Authority (INV-01):** Guards in the machine decide all state transitions
19
- - **Passive Infrastructure (INV-04):** React observes signals and dispatches events — never decides
20
- - **Signal-Only Reactivity (INV-05):** `actor.currentView` signal is the sole render trigger
21
+ Supported versions:
21
22
 
22
- ## Installation
23
+ - `react` / `react-dom`: `^18.0.0 || ^19.0.0`
24
+ - `xstate`: `^5.30.0`
25
+ - `@xstate/store`: `^3.17.0`
26
+ - `@json-render/*`: `^0.18.0`
23
27
 
24
- ```bash
25
- npm install @xmachines/play-react
26
- npm install @json-render/react @json-render/core # peer deps
27
- npm install @json-render/xstate @xstate/store # store integration
28
- ```
28
+ ## Usage
29
+
30
+ ### Standard usage `PlayUIProvider` + `PlayRenderer`
29
31
 
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
-
34
- ## Current Exports
35
-
36
- - `PlayRenderer` — main renderer component
37
- - `useActor` — hook for accessing the actor inside a `PlayRenderer` tree
38
- - `useSignalEffect` — React hook for subscribing to TC39 Signals
39
- - `PlayErrorBoundary` — error boundary wrapping renderer output
40
- - `defineRegistry` — re-exported from `@json-render/react`
41
- - `useBoundProp` — re-exported from `@json-render/react`
42
- - `ComponentFn` (type) — re-exported from `@json-render/react`
43
- - `ComponentContext` (type) — re-exported from `@json-render/react`
44
- - `ActorProvider` — escape hatch primitive (owns actor bridging, signal bridge, store lifecycle)
45
- - `PlayUIProvider` — batteries-included composite (wraps `ActorProvider` + `JSONUIProvider`)
46
- - `usePlayView` — hook for accessing the current view spec inside a provider tree
47
- - `RenderErrorHandler` (type) — inner per-element error callback signature
48
- - `ActorProviderProps` (type)
49
- - `ViewContextValue` (type)
50
- - `PlayActor` (type)
51
-
52
- ## Quick Start
32
+ The recommended pattern for actor-driven React rendering:
53
33
 
54
34
  ```tsx
55
- import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
56
- import { PlayUIProvider, PlayRenderer } from "@xmachines/play-react";
57
- import { defineCatalog } from "@json-render/core";
58
- import { defineRegistry } from "@xmachines/play-react";
59
- import type { ComponentFn } from "@xmachines/play-react";
60
- import { setup, assign } from "xstate";
61
- import { z } from "zod";
62
-
63
- // 1. Define catalog — the contract between machine spec and UI components
64
- const catalog = defineCatalog({
65
- elements: {
66
- Login: { props: z.object({ title: z.string() }), description: "Login form" },
67
- Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
68
- },
69
- });
35
+ import { PlayUIProvider, PlayRenderer, defineRegistry } from "@xmachines/play-react";
36
+ import { definePlayer } from "@xmachines/play-xstate";
37
+
38
+ // 1. Create and start the actor
39
+ const actor = definePlayer({ machine: myMachine })();
40
+ actor.start();
70
41
 
71
- // 2. Implement components using ComponentFn typed against catalog entries
72
- const Login: ComponentFn<typeof catalog, "Login"> = ({ props, emit }) => (
73
- <form
74
- onSubmit={(e) => {
75
- e.preventDefault();
76
- emit("submit");
77
- }}
78
- >
79
- <h2>{props.title}</h2>
80
- <button type="submit">Log In</button>
81
- </form>
82
- );
83
-
84
- const Dashboard: ComponentFn<typeof catalog, "Dashboard"> = ({ props }) => (
85
- <div>Welcome, {props.username}!</div>
86
- );
87
-
88
- // 3. Build registry — wires components to catalog and declares action handlers
89
- const registryResult = defineRegistry(catalog, {
42
+ // 2. Define the component registry with action handlers
43
+ const registryResult = defineRegistry(myCatalog, {
90
44
  components: { Login, Dashboard },
91
45
  actions: {
92
- login: async (params) => {
93
- if (!params) return;
94
- actor.send({ type: "auth.login", username: params.username });
95
- },
96
- logout: async () => {
97
- actor.send({ type: "auth.logout" });
98
- },
46
+ login: async ({ username }) => actor.send({ type: "auth.login", username }),
47
+ logout: async () => actor.send({ type: "auth.logout" }),
99
48
  },
100
49
  });
101
50
 
102
- // 4. Define machine with view metadata
103
- const machine = setup({
104
- types: {
105
- context: {} as {
106
- isAuthenticated: boolean;
107
- username: string | null;
108
- params: Record<string, string>;
109
- query: Record<string, string>;
110
- },
111
- events: {} as
112
- | { type: "auth.login"; username: string }
113
- | { type: "auth.logout" }
114
- | { type: "play.route"; to: string; params?: Record<string, string> },
115
- },
116
- }).createMachine(
117
- formatPlayRouteTransitions({
118
- id: "app",
119
- initial: "login",
120
- context: { isAuthenticated: false, username: null, params: {}, query: {} },
121
- states: {
122
- login: {
123
- id: "login",
124
- meta: {
125
- route: "/login",
126
- view: {
127
- root: "root",
128
- elements: {
129
- root: { type: "Login", props: { title: "Sign In" }, children: [] },
130
- },
131
- },
132
- },
133
- },
134
- dashboard: {
135
- id: "dashboard",
136
- meta: {
137
- route: "/dashboard",
138
- view: {
139
- root: "root",
140
- elements: {
141
- root: { type: "Dashboard", props: { username: "" }, children: [] },
142
- },
143
- },
144
- },
145
- },
146
- },
147
- on: {
148
- "auth.login": {
149
- target: ".dashboard",
150
- guard: ({ context }) => !context.isAuthenticated,
151
- actions: assign({
152
- isAuthenticated: true,
153
- username: ({ event }) => event.username,
154
- }),
155
- },
156
- "auth.logout": {
157
- target: ".login",
158
- guard: ({ context }) => context.isAuthenticated,
159
- actions: assign({ isAuthenticated: false, username: null }),
160
- },
161
- },
162
- }),
163
- );
164
-
165
- // 5. Create actor and render
166
- const createPlayer = definePlayer({ machine });
167
- const actor = createPlayer();
168
- actor.start();
169
-
51
+ // 3. Render signals drive view transitions automatically
170
52
  function App() {
171
53
  return (
172
54
  <PlayUIProvider actor={actor} registryResult={registryResult}>
@@ -176,229 +58,136 @@ function App() {
176
58
  }
177
59
  ```
178
60
 
179
- ## API Reference
61
+ ### With optional `JSONUIProvider` props
180
62
 
181
- ### `PlayUIProvider`
182
-
183
- Batteries-included composite provider. Wraps `ActorProvider` + `JSONUIProvider`. Pass `actor` and `registryResult` here, then place `<PlayRenderer />` inside as a zero-prop child.
63
+ Pass navigation and validation helpers through `PlayUIProvider`:
184
64
 
185
65
  ```tsx
186
66
  <PlayUIProvider
187
- actor={actor} // required
188
- registryResult={registryResult} // required
189
- store={myStore} // optional — controlled mode
190
- fallback={<p>Loading…</p>} // optional
191
- onError={(err, info) => Sentry.captureException(err, { extra: info })} // optional
192
- onRenderError={(error, elementType) => console.warn(`<${elementType}> crashed:`, error)} // optional
193
- >
194
- <PlayRenderer />
195
- </PlayUIProvider>
196
- ```
197
-
198
- **`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
199
-
200
- **`registryResult`** — Full result from `defineRegistry(catalog, { components, actions })`. Contains both the component registry and action handlers.
201
-
202
- **`store`** (optional) — Controls per-view UI state (form values, `$state` bindings):
203
-
204
- - **Omitted (uncontrolled, default):** A fresh `@xstate/store` atom is created per view transition, seeded from `view.spec.state`. The atom resets automatically on each state transition.
205
- - **Provided (controlled):** The caller owns the store lifecycle. `spec.state` is ignored.
206
-
207
- ```tsx
208
- import { createAtom } from "@xstate/store";
209
- import { xstateStoreStateStore } from "@json-render/xstate";
210
- import type { StateStore } from "@json-render/core";
211
-
212
- const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "alice" }) });
213
-
214
- <PlayUIProvider actor={actor} registryResult={registryResult} store={store}>
215
- <PlayRenderer />
216
- </PlayUIProvider>;
217
- ```
218
-
219
- **`fallback`** — Shown when `actor.currentView.get()` is `null` (machine in a no-view state, or during initialisation).
220
-
221
- **`onError`** — Called when the outer `PlayErrorBoundary` catches an error. Receives `(error: Error, info: React.ErrorInfo)`. Use for observability tools (Sentry, Datadog, etc.).
222
-
223
- **`onRenderError`** — Called when an individual catalog component throws during render. Caught by `@json-render/react`'s inner per-element boundary — the failed component is silently removed while the rest of the spec continues rendering. `onError` / `fallback` are **not** triggered. When both `onRenderError` on `PlayUIProvider` and on `defineRegistry` are set, the prop wins.
224
-
225
- ---
226
-
227
- ### `ActorProvider`
228
-
229
- Escape hatch primitive. Owns actor bridging, signal bridge, and store lifecycle. Use this when you need direct control over the provider layer (e.g. custom `JSONUIProvider` configuration).
230
-
231
- ```tsx
232
- import { ActorProvider } from "@xmachines/play-react";
233
-
234
- <ActorProvider
235
67
  actor={actor}
236
68
  registryResult={registryResult}
237
- onRenderError={(err, type) => reportError(err, type)}
238
- >
239
- {/* your own JSONUIProvider + PlayRenderer tree */}
240
- </ActorProvider>;
241
- ```
242
-
243
- ---
244
-
245
- ### `PlayRenderer`
246
-
247
- Zero-prop leaf component. Must be rendered inside a `PlayUIProvider` (or `ActorProvider`) tree. Subscribes to `actor.currentView` via context and renders the current spec.
248
-
249
- ```tsx
250
- <PlayUIProvider actor={actor} registryResult={registryResult}>
251
- <PlayRenderer />
252
- </PlayUIProvider>
253
- ```
254
-
255
- `PlayRenderer` accepts no props — all configuration (`actor`, `registryResult`, `store`, `fallback`, `onError`, `onRenderError`) is provided by the enclosing `PlayUIProvider` or `ActorProvider`.
256
-
257
- ## Error handling
258
-
259
- The provider tree has two layers of error boundaries:
260
-
261
- ### Outer boundary — `onError` and `fallback`
262
-
263
- Wraps the entire renderer output via `PlayErrorBoundary`. Triggered when the spec or store setup throws, or when the inner boundary is not present.
264
-
265
- ```tsx
266
- <PlayUIProvider
267
- actor={actor}
268
- registryResult={registryResult}
269
- fallback={<p>Something went wrong.</p>}
270
- onError={(err, info) => Sentry.captureException(err, { extra: info })}
69
+ navigate={(path) => router.push(path)}
70
+ validationFunctions={{ isEmail: (v) => /^.+@.+$/.test(String(v)) }}
271
71
  >
272
72
  <PlayRenderer />
273
73
  </PlayUIProvider>
274
74
  ```
275
75
 
276
- ### Inner boundary`onRenderError`
76
+ ### Escape hatchcustom provider composition
277
77
 
278
- Each catalog element is individually wrapped in an error boundary by `@json-render/react`. When a component throws, it is silently removed while the rest of the spec continues rendering. The outer boundary is **not** triggered.
279
-
280
- Pass `onRenderError` to `PlayUIProvider` (or `ActorProvider`) — overrides any registry-level handler — or bake it into `defineRegistry`:
78
+ Use `ActorProvider` directly when you need to compose providers manually:
281
79
 
282
80
  ```tsx
283
- // via PlayUIProvider prop
284
- <PlayUIProvider
285
- actor={actor}
286
- registryResult={registryResult}
287
- onRenderError={(error, elementType) => {
288
- console.warn(`<${elementType}> crashed:`, error);
289
- }}
290
- >
291
- <PlayRenderer />
292
- </PlayUIProvider>
293
- ```
81
+ import { ActorProvider, JSONUIProvider, PlayRenderer } from "@xmachines/play-react";
294
82
 
295
- ```ts
296
- // via defineRegistry — bakes the handler into the registry
297
- const registryResult = defineRegistry(catalog, {
298
- components: { Login, Dashboard },
299
- actions: { login: async (params) => { ... }, logout: async () => { ... } },
300
- onRenderError(error, elementType) {
301
- reportExpectedRenderError(error, elementType);
302
- },
303
- });
83
+ <ActorProvider actor={actor} registryResult={registryResult}>
84
+ <JSONUIProvider registry={registryResult.registry}>
85
+ <PlayRenderer />
86
+ </JSONUIProvider>
87
+ </ActorProvider>;
304
88
  ```
305
89
 
306
- `onRenderError` is typed as `RenderErrorHandler` and exported from.
307
-
308
- ---
309
-
310
- ### `useActor`
311
-
312
- Hook for accessing the actor from inside any component rendered by `PlayRenderer`. No prop drilling needed.
90
+ ### Accessing the actor from inside the tree
313
91
 
314
92
  ```tsx
315
93
  import { useActor } from "@xmachines/play-react";
316
94
 
317
- // Works in any component rendered inside PlayRenderer:
318
- function LogoutButton() {
95
+ function SubmitButton() {
319
96
  const actor = useActor();
320
- return <button onClick={() => actor.send({ type: "auth.logout" })}>Log Out</button>;
97
+ return <button onClick={() => actor.send({ type: "SUBMIT" })}>Submit</button>;
321
98
  }
322
99
  ```
323
100
 
324
- Throws `NonNullableError: "useActor() must be called inside <ActorProvider> (or <PlayUIProvider>)"` if called outside the tree.
325
-
326
- ---
327
-
328
- ### `useSignalEffect`
329
-
330
- Hook for subscribing to TC39 Signals in React components that live outside a `PlayRenderer` tree (e.g. nav bars, status indicators driven by actor state).
101
+ ### Subscribing to signals directly
331
102
 
332
103
  ```tsx
333
104
  import { useSignalEffect } from "@xmachines/play-react";
334
105
 
335
- function NavBar({ actor }: { actor: ReturnType<typeof createPlayer> }) {
336
- const [isAuth, setIsAuth] = useState(false);
106
+ function MyComponent({ actor }) {
107
+ const [view, setView] = useState(null);
337
108
 
338
109
  useSignalEffect(() => {
339
- const snap = actor.state.get();
340
- setIsAuth((snap.context as { isAuthenticated: boolean }).isAuthenticated);
110
+ setView(actor.currentView.get());
341
111
  });
342
112
 
343
- return <nav>{isAuth ? <LogoutBtn /> : <LoginBtn />}</nav>;
113
+ return <div>{view?.component}</div>;
344
114
  }
345
115
  ```
346
116
 
347
- ---
117
+ ## API Summary
348
118
 
349
- ### `PlayErrorBoundary`
119
+ ### Components
350
120
 
351
- Class error boundary that wraps the rendered output. `PlayRenderer` wraps its own output in this boundary automatically — use this directly only if you need to wrap other content or nest boundaries manually.
121
+ | Export | Description |
122
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
123
+ | `<PlayUIProvider>` | Batteries-included provider: wraps `ActorProvider` + `JSONUIProvider`. Standard entry point. |
124
+ | `<PlayRenderer>` | Zero-prop leaf component. Reads the current actor view from context and renders it. Must be inside `PlayUIProvider` or `ActorProvider`. |
125
+ | `<ActorProvider>` | Escape-hatch primitive. Owns actor bridging, signal subscription, and per-view `StateStore` lifecycle. |
126
+ | `<PlayErrorBoundary>` | React class error boundary for catching catalog component render errors. |
352
127
 
353
- `componentDidCatch` invokes the `onError` prop (for observability tools) but does **not** re-throw — re-throwing from `componentDidCatch` can unmount the entire React 19 root. `getDerivedStateFromError` handles fallback state transition instead.
128
+ ### Hooks
354
129
 
355
- ```tsx
356
- import { PlayErrorBoundary } from "@xmachines/play-react";
130
+ | Export | Description |
131
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
132
+ | `useSignalEffect(callback)` | Subscribes to TC39 signal changes; re-runs the callback and forces a React re-render when any accessed signal changes. Cleanup is automatic on unmount. |
133
+ | `useActor()` | Returns the raw actor instance. Must be called inside an `ActorProvider`/`PlayUIProvider` tree. |
134
+ | `usePlayView()` | Returns `{ spec, handlers, registry, store }` for the current view. Must be called inside an `ActorProvider`/`PlayUIProvider` tree. |
357
135
 
358
- <PlayErrorBoundary
359
- fallback={<p>Something went wrong.</p>}
360
- onError={(err, info) => Sentry.captureException(err, { extra: info })}
361
- >
362
- <PlayUIProvider actor={actor} registryResult={registryResult}>
363
- <PlayRenderer />
364
- </PlayUIProvider>
365
- </PlayErrorBoundary>;
366
- ```
136
+ ### Types
367
137
 
368
- ---
138
+ | Export | Description |
139
+ | ------------------------ | ---------------------------------------------------------------------------------------------- |
140
+ | `PlayUIProviderProps` | Props for `<PlayUIProvider>` |
141
+ | `ActorProviderProps` | Props for `<ActorProvider>` (also exported as `PlayRendererProps` for migration compatibility) |
142
+ | `PlayErrorBoundaryProps` | Props for `<PlayErrorBoundary>` |
143
+ | `PlayErrorBoundaryState` | State shape for `<PlayErrorBoundary>` |
144
+ | `PlayActor` | Type alias for `AbstractActor<AnyActorLogic>` |
145
+ | `ViewContextValue` | Value shape returned by `usePlayView()` |
146
+ | `RenderErrorHandler` | Error handler callback type for render errors |
369
147
 
370
- ## Route Parameters in Props
148
+ ### Re-exports from `@json-render/react`
371
149
 
372
- When using `formatPlayRouteTransitions`, URL path parameters flow automatically into component props. Declare an `undefined` slot in the spec to opt in:
150
+ `@xmachines/play-react` re-exports the full `@json-render/react` surface so consumers only need one import:
373
151
 
374
152
  ```ts
375
- // spec: { section: undefined, user: "alice" }
376
- // After play.route to /settings/profile → context.params = { section: "profile" }
377
- // Component receives: { section: "profile", user: "alice" }
153
+ import {
154
+ defineRegistry,
155
+ useBoundProp,
156
+ JSONUIProvider,
157
+ StateProvider,
158
+ ActionProvider,
159
+ VisibilityProvider,
160
+ ValidationProvider,
161
+ Renderer,
162
+ } from "@xmachines/play-react";
378
163
  ```
379
164
 
380
- Priority: **route param fills `undefined` slots; explicit non-`undefined` spec props always win.**
165
+ ## Key Principle
166
+
167
+ React state is **never** used for business logic — only for triggering React's render cycle. Signals (`@xmachines/play-signals`) are the source of truth. `PlayUIProvider` passively observes actor signals via `useSignalEffect` and re-renders when the current view changes. Rapid signal updates are batched via microtasks to prevent unnecessary React renders.
168
+
169
+ ## Testing
170
+
171
+ Run unit tests (jsdom environment):
381
172
 
382
- ---
173
+ ```bash
174
+ npm test -w @xmachines/play-react
175
+ ```
383
176
 
384
- ## Error Handling
177
+ Run tests with coverage:
385
178
 
386
- | Error | Cause | Fix |
387
- | ------------------------------------------------------------------------ | ---------------------------- | ---------------------------------------------------------------- |
388
- | `useActor() must be called inside <ActorProvider> (or <PlayUIProvider>)` | Hook called outside the tree | Wrap with `<PlayUIProvider>` or `<ActorProvider>` |
389
- | Component render error | Component throws | `PlayErrorBoundary` catches it; inspect component implementation |
179
+ ```bash
180
+ npm run test:coverage -w @xmachines/play-react
181
+ ```
390
182
 
391
- ---
183
+ Run browser integration tests (requires Chromium):
392
184
 
393
- ## Architecture Notes
185
+ ```bash
186
+ npm run test:browser -w @xmachines/play-react
187
+ ```
394
188
 
395
- - React `useState` is **only** used to trigger re-renders — never for business logic
396
- - `actor.currentView` (TC39 Signal) is the sole render trigger; `PlayRenderer` is a passive observer
397
- - Per-view UI state lives in an `@xstate/store` atom, not in React state
398
- - `@json-render/react` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useBoundProp` from
189
+ Coverage thresholds: 80% lines, functions, branches, and statements.
399
190
 
400
- ## Learn More
191
+ ## License
401
192
 
402
- - [Demo](examples/demo/README.md)
403
- - [React Router adapter](../play-react-router/README.md)
404
- - [TanStack React Router adapter](../play-tanstack-react-router/README.md)
193
+ MIT — see [LICENSE](./LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-react",
3
- "version": "1.0.0-beta.46",
3
+ "version": "1.0.0-beta.47",
4
4
  "description": "React renderer for XMachines Play architecture with signal-driven rendering",
5
5
  "keywords": [
6
6
  "actor",
@@ -45,9 +45,9 @@
45
45
  "prepublishOnly": "npm run build"
46
46
  },
47
47
  "dependencies": {
48
- "@xmachines/play": "1.0.0-beta.46",
49
- "@xmachines/play-actor": "1.0.0-beta.46",
50
- "@xmachines/play-signals": "1.0.0-beta.46"
48
+ "@xmachines/play": "1.0.0-beta.47",
49
+ "@xmachines/play-actor": "1.0.0-beta.47",
50
+ "@xmachines/play-signals": "1.0.0-beta.47"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@json-render/core": "^0.18.0",
@@ -58,7 +58,7 @@
58
58
  "@types/node": "^25.6.0",
59
59
  "@types/react": "^19.2.14",
60
60
  "@types/react-dom": "^19.2.3",
61
- "@xmachines/shared": "1.0.0-beta.46",
61
+ "@xmachines/shared": "1.0.0-beta.47",
62
62
  "@xstate/store": "^3.17.0",
63
63
  "jsdom": "^29.0.2",
64
64
  "oxfmt": "^0.45.0",