@koordinates/xstate-tree 5.1.0-next.9 → 5.2.0

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
@@ -20,10 +20,9 @@ A minimal example of a single machine tree:
20
20
  ```tsx
21
21
  import React from "react";
22
22
  import { createRoot } from "react-dom/client";
23
- import { createMachine } from "xstate";
24
- import { assign } from "@xstate/immer";
23
+ import { setup, assign, assertEvent } from "xstate";
25
24
  import {
26
- createXStateTreeMachine
25
+ createXStateTreeMachine,
27
26
  buildRootComponent
28
27
  } from "@koordinates/xstate-tree";
29
28
 
@@ -32,40 +31,37 @@ type Events =
32
31
  | { type: "INCREMENT"; amount: number };
33
32
  type Context = { incremented: number };
34
33
 
35
- // A standard xstate machine, nothing extra is needed for xstate-tree
36
- const machine = createMachine<Context, Events>(
37
- {
38
- id: "root",
39
- initial: "inactive",
40
- context: {
41
- incremented: 0
42
- },
43
- states: {
44
- inactive: {
45
- on: {
46
- SWITCH_CLICKED: "active"
47
- }
48
- },
49
- active: {
50
- on: {
51
- SWITCH_CLICKED: "inactive",
52
- INCREMENT: { actions: "increment" }
53
- }
34
+ // A standard xstate v5 machine, nothing extra is needed for xstate-tree
35
+ const machine = setup({
36
+ types: { context: {} as Context, events: {} as Events },
37
+ actions: {
38
+ increment: assign({
39
+ incremented: ({ context, event }) => {
40
+ assertEvent(event, "INCREMENT");
41
+ return context.incremented + event.amount;
54
42
  }
55
- }
43
+ })
44
+ }
45
+ }).createMachine({
46
+ id: "root",
47
+ initial: "inactive",
48
+ context: {
49
+ incremented: 0
56
50
  },
57
- {
58
- actions: {
59
- increment: assign((context, event) => {
60
- if (event.type !== "INCREMENT") {
61
- return;
62
- }
63
-
64
- context.incremented += event.amount;
65
- })
51
+ states: {
52
+ inactive: {
53
+ on: {
54
+ SWITCH_CLICKED: "active"
55
+ }
56
+ },
57
+ active: {
58
+ on: {
59
+ SWITCH_CLICKED: "inactive",
60
+ INCREMENT: { actions: "increment" }
61
+ }
66
62
  }
67
63
  }
68
- );
64
+ });
69
65
 
70
66
  const RootMachine = createXStateTreeMachine(machine, {
71
67
  // Selectors to transform the machines state into a representation useful for the view
@@ -95,10 +91,10 @@ const RootMachine = createXStateTreeMachine(machine, {
95
91
  // If this tree had more than a single machine the slots to render child machines into would be defined here
96
92
  // see the codesandbox example for an expanded demonstration that uses slots
97
93
  slots: [],
98
- // A view to bring it all together
99
- // the return value is a plain React view that can be rendered anywhere by passing in the needed props
94
+ // A React component view to bring it all together
95
+ // the return value is a plain React component that can be rendered anywhere by passing in the needed props
100
96
  // the view has no knowledge of the machine it's bound to
101
- view({ actions, selectors }) {
97
+ View({ actions, selectors }) {
102
98
  return (
103
99
  <div>
104
100
  <button onClick={() => actions.switch()}>
@@ -118,6 +114,7 @@ const RootMachine = createXStateTreeMachine(machine, {
118
114
  });
119
115
 
120
116
  // Build the React host for the tree
117
+ // You can pass input to the machine via the input prop
121
118
  const XstateTreeRoot = buildRootComponent(RootMachine);
122
119
 
123
120
  // Rendering it with React
@@ -133,7 +130,7 @@ Each machine that forms the tree representing your UI has an associated set of s
133
130
  - Selector functions are provided with the current context of the machine, a function to determine if it can handle a given event and a function to determine if it is in a given state, and expose the returned result to the view.
134
131
  - Action functions are provided with the `send` method bound to the machines interpreter and the result of calling the selector function
135
132
  - Slots are how children of the machine are exposed to the view. They can be either single slot for a single actor, or multi slot for when you have a list of actors.
136
- - View functions are React views provided with the output of the selector and action functions, and the currently active slots
133
+ - View functions are React components provided with the output of the selector and action functions, and the currently active slots
137
134
 
138
135
  ## API
139
136
 
@@ -142,7 +139,7 @@ To assist in making xstate-tree easy to use with TypeScript there is the `create
142
139
  `createXStateTreeMachine` accepts the xstate machine as the first argument and takes an options argument with the following fields, it is important the fields are defined in this order or TypeScript will infer the wrong types:
143
140
  * `selectors`, receives an object with `ctx`, `inState`, `canHandleEvent`, and `meta` fields. `ctx` is the machines current context, `inState` is the xstate `state.matches` function to allow determining if the machine is in a given state, and `canHandleEvent` accepts an event object and returns whether the machine will do anything in response to that event in it's current state. `meta` is the xstate `state.meta` object with all the per state meta flattened into an object
144
141
  * `actions`, receives an object with `send` and `selectors` fields. `send` is the xstate `send` function bound to the machine, and `selectors` is the result of calling the selector function
145
- * `view`, is a React component that receives `actions`, `selectors`, and `slots` as props. `actions` and `selectors` being the result of the action/selector functions and `slots` being an object with keys as the slot names and the values the slots React component
142
+ * `View`, is a React component (note the capital V) that receives `actions`, `selectors`, and `slots` as props. `actions` and `selectors` being the result of the action/selector functions and `slots` being an object with keys as the slot names and the values the slots React component
146
143
 
147
144
  Full API docs coming soon, see [#20](https://github.com/koordinates/xstate-tree/issues/20)
148
145
 
@@ -189,11 +186,11 @@ These events can be added anywhere, either next to a component for component spe
189
186
 
190
187
  #### `viewToMachine`
191
188
 
192
- This utility accepts a React view that does not take any props and wraps it with an xstate-tree machine so you can easily invoke arbitrary React views in your xstate machines
189
+ This utility accepts a React component and wraps it with an xstate-tree machine so you can easily invoke arbitrary React components in your xstate machines. This utility also accepts Root components returned from `buildRootComponent`.
193
190
 
194
- ```
191
+ ```tsx
195
192
  function MyView() {
196
- return <div>My View</div>;
193
+ return <div>{"My View"}</div>;
197
194
  }
198
195
 
199
196
  const MyViewMachine = viewToMachine(MyView);
@@ -203,9 +200,9 @@ const MyViewMachine = viewToMachine(MyView);
203
200
 
204
201
  This utility aims to reduce boilerplate by generating a common type of state machine, a routing machine. This is a machine that solely consists of routing events that transition to states that invoke xstate-tree machines.
205
202
 
206
- The first argument is the array of routes you wish to handle, and the second is an object mapping from those event types to the xstate-tree machine that will be invoked for that routing event
203
+ The first argument is the array of routes you wish to handle, and the second is an object mapping from those event types to the xstate-tree machine that will be invoked for that routing event. The utility now supports routes with dots in their event names (e.g., "user.profile.view").
207
204
 
208
- ```
205
+ ```tsx
209
206
  const routeA = createRoute.simpleRoute()({
210
207
  url: "/a",
211
208
  event: "GO_TO_A",
@@ -227,25 +224,97 @@ There are some exported type helpers for use with xstate-tree
227
224
 
228
225
  * `SelectorsFrom<TMachine>`: Takes a machine and returns the type of the selectors object
229
226
  * `ActionsFrom<TMachine>`: Takes a machine and returns the type of the actions object
227
+ * `AnyXstateTreeMachine`: Type for any xstate-tree machine, useful for function parameters
228
+
229
+
230
+ ### New Features in v5
231
+
232
+ #### `buildRootComponent` Input Support
233
+
234
+ You can now pass input to your root machine when using `buildRootComponent`. The function signature has changed to accept a single object parameter:
235
+
236
+ ```tsx
237
+ // Without routing
238
+ const XstateTreeRoot = buildRootComponent(RootMachine);
239
+
240
+ // With routing
241
+ const XstateTreeRoot = buildRootComponent({
242
+ machine: RootMachine,
243
+ routing: {
244
+ routes,
245
+ history,
246
+ basePath: "/"
247
+ }
248
+ });
249
+
250
+ // Pass input to the machine
251
+ ReactRoot.render(<XstateTreeRoot input={{ initialData: data }} />);
252
+ ```
253
+
254
+ #### `useOnRoute` Hook
230
255
 
256
+ A new hook for executing side effects when specific routes are active. Unlike `useIsRouteActive`, this hook tells you when you're on the exact route (not just part of the active route chain):
257
+
258
+ ```tsx
259
+ import { useOnRoute } from "@koordinates/xstate-tree";
260
+
261
+ function MyComponent() {
262
+ useOnRoute(myRoute, ({ params, query }) => {
263
+ // Execute side effects when myRoute is the active end route
264
+ console.log("Route is active with params:", params);
265
+ });
266
+ }
267
+ ```
268
+
269
+ #### Children Support for Root Components
270
+
271
+ Root components and slots now support passing children, allowing you to wrap your application with providers:
272
+
273
+ ```tsx
274
+ <XstateTreeRoot>
275
+ <GlobalProvider>
276
+ {/* Your app renders here */}
277
+ </GlobalProvider>
278
+ </XstateTreeRoot>
279
+ ```
280
+
281
+ #### Improved `lazy` Loading
282
+
283
+ The `lazy` utility now supports passing input to the lazily loaded machine:
284
+
285
+ ```tsx
286
+ const LazyMachine = lazy(() => import("./MyMachine"), {
287
+ Loader: () => <div>Loading...</div>,
288
+ input: { initialData: "data" }
289
+ });
290
+ ```
291
+
292
+ #### Testing Improvements
293
+
294
+ - `TestRoutingContext` now supports nesting routing roots for better testing scenarios
295
+ - Removed deprecated testing utilities: `buildTestRootComponent`, `buildViewProps`, `slotTestingDummyFactory`
296
+
297
+ #### Logging Improvements
298
+
299
+ - Internal XState events are now filtered from logs for cleaner output
300
+ - `_subscription` property is stripped from logged data
301
+ - Route objects are no longer logged after matching to reduce console noise
302
+ - `loggingMetaOptions` export available for configuring logging behavior
303
+
304
+ ### Breaking Changes from v4
305
+
306
+ - **Removed v1 style builders**: `buildView`, `buildSelectors`, `buildActions` have been removed. Use `createXStateTreeMachine` instead.
307
+ - **Child actor behavior**: Children in final states are no longer automatically removed from views. You must manually call `stopChild` to remove them.
308
+ - **Testing utilities removed**: `buildTestRootComponent`, `buildViewProps`, and `slotTestingDummyFactory` have been removed.
231
309
 
232
310
  ### [Storybook](https://storybook.js.org)
233
311
 
234
- It is relatively simple to display xstate-tree views directly in Storybook. Since the views are plain React components that accept selectors/actions/slots/inState as props you can just import the view and render it in a Story
312
+ It is relatively simple to display xstate-tree views directly in Storybook. Since the views are plain React components that accept selectors/actions/slots as props you can just import the view and render it in a Story
235
313
 
236
- There are a few utilities in xstate-tree to make this easier
314
+ There is a utility in xstate-tree to make this easier:
237
315
 
238
316
  #### `genericSlotsTestingDummy`
239
317
 
240
318
  This is a simple Proxy object that renders a <div> containing the name of the slot whenever rendering
241
319
  a slot is attempted in the view. This will suffice as an argument for the slots prop in most views
242
320
  when rendering them in a Story
243
-
244
- #### `slotTestingDummyFactory`
245
-
246
- This is not relevant if using the render-view-component approach. But useful if you
247
- are planning on rendering the view using the xstate-tree machine itself, or testing the machine
248
- via the view.
249
-
250
- It's a simple function that takes a name argument and returns a basic xstate-tree machine that you
251
- can replace slot services with. It just renders a div containing the name supplied
package/lib/builders.js CHANGED
@@ -31,9 +31,19 @@ function createXStateTreeMachine(machine, options) {
31
31
  View: options.View,
32
32
  slots: (options.slots ?? []),
33
33
  };
34
- return machineWithMeta;
34
+ return fixProvideLosingXstateTreeMeta(machineWithMeta);
35
35
  }
36
36
  exports.createXStateTreeMachine = createXStateTreeMachine;
37
+ function fixProvideLosingXstateTreeMeta(machine) {
38
+ const originalProvide = machine.provide.bind(machine);
39
+ machine.provide = (impl) => {
40
+ const result = originalProvide(impl);
41
+ result._xstateTree = machine._xstateTree;
42
+ fixProvideLosingXstateTreeMeta(result);
43
+ return result;
44
+ };
45
+ return machine;
46
+ }
37
47
  /**
38
48
  * @public
39
49
  *
@@ -64,11 +74,19 @@ exports.viewToMachine = viewToMachine;
64
74
  * @returns an xstate-tree machine that will render the right machines based on the routing events
65
75
  */
66
76
  function buildRoutingMachine(_routes, mappings) {
77
+ /**
78
+ * States in xstate can't contain dots, since the states are named after the routing events
79
+ * if the routing event contains a dot that will make a state with a dot in it
80
+ * this function sanitizes the event name to remove dots and is used for the state names and targets
81
+ */
82
+ function sanitizeEventName(event) {
83
+ return event.replace(/\.([a-zA-Z])/g, (_, letter) => letter.toUpperCase());
84
+ }
67
85
  const contentSlot = (0, slots_1.singleSlot)("Content");
68
86
  const mappingsToStates = Object.entries(mappings).reduce((acc, [event, _machine]) => {
69
87
  return {
70
88
  ...acc,
71
- [event]: {
89
+ [sanitizeEventName(event)]: {
72
90
  invoke: {
73
91
  src: event,
74
92
  id: contentSlot.getId(),
@@ -79,7 +97,7 @@ function buildRoutingMachine(_routes, mappings) {
79
97
  const mappingsToEvents = Object.keys(mappings).reduce((acc, event) => ({
80
98
  ...acc,
81
99
  [event]: {
82
- target: `.${event}`,
100
+ target: `.${sanitizeEventName(event)}`,
83
101
  },
84
102
  }), {});
85
103
  const machine = (0, xstate_1.setup)({
package/lib/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.lazy = exports.loggingMetaOptions = exports.TestRoutingContext = exports.useActiveRouteEvents = exports.useRouteArgsIfActive = exports.useIsRouteActive = exports.matchRoute = exports.buildCreateRoute = exports.Link = exports.genericSlotsTestingDummy = exports.onBroadcast = exports.buildRootComponent = exports.broadcast = void 0;
17
+ exports.lazy = exports.loggingMetaOptions = exports.useOnRoute = exports.TestRoutingContext = exports.useActiveRouteEvents = exports.useRouteArgsIfActive = exports.useIsRouteActive = exports.matchRoute = exports.buildCreateRoute = exports.Link = exports.genericSlotsTestingDummy = exports.onBroadcast = exports.buildRootComponent = exports.broadcast = void 0;
18
18
  __exportStar(require("./builders"), exports);
19
19
  __exportStar(require("./slots"), exports);
20
20
  var xstateTree_1 = require("./xstateTree");
@@ -32,6 +32,7 @@ Object.defineProperty(exports, "useIsRouteActive", { enumerable: true, get: func
32
32
  Object.defineProperty(exports, "useRouteArgsIfActive", { enumerable: true, get: function () { return routing_1.useRouteArgsIfActive; } });
33
33
  Object.defineProperty(exports, "useActiveRouteEvents", { enumerable: true, get: function () { return routing_1.useActiveRouteEvents; } });
34
34
  Object.defineProperty(exports, "TestRoutingContext", { enumerable: true, get: function () { return routing_1.TestRoutingContext; } });
35
+ Object.defineProperty(exports, "useOnRoute", { enumerable: true, get: function () { return routing_1.useOnRoute; } });
35
36
  var useService_1 = require("./useService");
36
37
  Object.defineProperty(exports, "loggingMetaOptions", { enumerable: true, get: function () { return useService_1.loggingMetaOptions; } });
37
38
  var lazy_1 = require("./lazy");
@@ -69,7 +69,7 @@ function buildCreateRoute(history, basePath) {
69
69
  }
70
70
  return parentRoutes;
71
71
  }
72
- return ({ event, matcher, reverser, paramsSchema, querySchema, redirect, preload, }) => {
72
+ return ({ event, matcher, reverser, paramsSchema, querySchema, redirect, preload, canMatch, }) => {
73
73
  let fullParamsSchema = paramsSchema;
74
74
  let parentRoute = baseRoute;
75
75
  while (fullParamsSchema && parentRoute) {
@@ -86,6 +86,7 @@ function buildCreateRoute(history, basePath) {
86
86
  querySchema,
87
87
  parent: baseRoute,
88
88
  redirect,
89
+ canMatch,
89
90
  matcher: matcher,
90
91
  reverser: reverser,
91
92
  // @ts-ignore :cry:
@@ -132,6 +133,16 @@ function buildCreateRoute(history, basePath) {
132
133
  if (querySchema) {
133
134
  querySchema.parse(matches.query);
134
135
  }
136
+ // Check canMatch predicate if provided
137
+ if (canMatch) {
138
+ const canMatchResult = canMatch({
139
+ params: fullParams,
140
+ query: matches.query ?? {},
141
+ });
142
+ if (!canMatchResult) {
143
+ return false;
144
+ }
145
+ }
135
146
  return {
136
147
  originalUrl: `${fullUrl}${search}`,
137
148
  type: event,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useActiveRouteEvents = exports.useInRoutingContext = exports.TestRoutingContext = exports.RoutingContext = exports.useRouteArgsIfActive = exports.useIsRouteActive = exports.handleLocationChange = exports.matchRoute = exports.Link = exports.joinRoutes = exports.buildCreateRoute = void 0;
3
+ exports.useActiveRouteEvents = exports.useInTestRoutingContext = exports.useInRoutingContext = exports.TestRoutingContext = exports.RoutingContext = exports.useOnRoute = exports.useRouteArgsIfActive = exports.useIsRouteActive = exports.handleLocationChange = exports.matchRoute = exports.Link = exports.joinRoutes = exports.buildCreateRoute = void 0;
4
4
  var createRoute_1 = require("./createRoute");
5
5
  Object.defineProperty(exports, "buildCreateRoute", { enumerable: true, get: function () { return createRoute_1.buildCreateRoute; } });
6
6
  var joinRoutes_1 = require("./joinRoutes");
@@ -15,8 +15,11 @@ var useIsRouteActive_1 = require("./useIsRouteActive");
15
15
  Object.defineProperty(exports, "useIsRouteActive", { enumerable: true, get: function () { return useIsRouteActive_1.useIsRouteActive; } });
16
16
  var useRouteArgsIfActive_1 = require("./useRouteArgsIfActive");
17
17
  Object.defineProperty(exports, "useRouteArgsIfActive", { enumerable: true, get: function () { return useRouteArgsIfActive_1.useRouteArgsIfActive; } });
18
+ var useOnRoute_1 = require("./useOnRoute");
19
+ Object.defineProperty(exports, "useOnRoute", { enumerable: true, get: function () { return useOnRoute_1.useOnRoute; } });
18
20
  var providers_1 = require("./providers");
19
21
  Object.defineProperty(exports, "RoutingContext", { enumerable: true, get: function () { return providers_1.RoutingContext; } });
20
22
  Object.defineProperty(exports, "TestRoutingContext", { enumerable: true, get: function () { return providers_1.TestRoutingContext; } });
21
23
  Object.defineProperty(exports, "useInRoutingContext", { enumerable: true, get: function () { return providers_1.useInRoutingContext; } });
24
+ Object.defineProperty(exports, "useInTestRoutingContext", { enumerable: true, get: function () { return providers_1.useInTestRoutingContext; } });
22
25
  Object.defineProperty(exports, "useActiveRouteEvents", { enumerable: true, get: function () { return providers_1.useActiveRouteEvents; } });
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TestRoutingContext = exports.useActiveRouteEvents = exports.useInRoutingContext = exports.RoutingContext = void 0;
6
+ exports.TestRoutingContext = exports.useActiveRouteEvents = exports.useInTestRoutingContext = exports.useInRoutingContext = exports.RoutingContext = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const react_2 = require("react");
9
9
  exports.RoutingContext = (0, react_2.createContext)(undefined);
@@ -22,6 +22,14 @@ function useInRoutingContext() {
22
22
  return context !== undefined;
23
23
  }
24
24
  exports.useInRoutingContext = useInRoutingContext;
25
+ /**
26
+ * @private
27
+ */
28
+ function useInTestRoutingContext() {
29
+ const context = (0, react_2.useContext)(exports.RoutingContext);
30
+ return context?.isTestRoutingContext ?? false;
31
+ }
32
+ exports.useInTestRoutingContext = useInTestRoutingContext;
25
33
  /**
26
34
  * @public
27
35
  *
@@ -46,6 +54,9 @@ exports.useActiveRouteEvents = useActiveRouteEvents;
46
54
  * @param activeRouteEvents - The active route events to use in the context
47
55
  */
48
56
  function TestRoutingContext({ activeRouteEvents, children, }) {
49
- return (react_1.default.createElement(exports.RoutingContext.Provider, { value: { activeRouteEvents: { current: activeRouteEvents } } }, children));
57
+ return (react_1.default.createElement(exports.RoutingContext.Provider, { value: {
58
+ activeRouteEvents: { current: activeRouteEvents },
59
+ isTestRoutingContext: true,
60
+ } }, children));
50
61
  }
51
62
  exports.TestRoutingContext = TestRoutingContext;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useOnRoute = void 0;
4
+ const providers_1 = require("./providers");
5
+ /**
6
+ * @public
7
+ * Accepts a single Route and returns true if the route is currently active and marked as an index route.
8
+ * False if not.
9
+ *
10
+ * If used outside of a RoutingContext, an error will be thrown.
11
+ * @param route - the route to check
12
+ * @returns true if the route is active and an index route, false if not
13
+ * @throws if used outside of an xstate-tree root
14
+ */
15
+ function useOnRoute(route) {
16
+ const activeRouteEvents = (0, providers_1.useActiveRouteEvents)();
17
+ if (!activeRouteEvents) {
18
+ throw new Error("useOnRoute must be used within a RoutingContext. Are you using it outside of an xstate-tree Root?");
19
+ }
20
+ return activeRouteEvents.some((activeRouteEvent) => activeRouteEvent.type === route.event &&
21
+ activeRouteEvent.meta?.indexEvent === true);
22
+ }
23
+ exports.useOnRoute = useOnRoute;
package/lib/utils.js CHANGED
@@ -121,9 +121,12 @@ function mergeMeta(meta) {
121
121
  }, {});
122
122
  }
123
123
  exports.mergeMeta = mergeMeta;
124
- function getCircularReplacer() {
124
+ function getCircularReplacer(stripKeys) {
125
125
  const seen = new WeakSet();
126
126
  return (key, value) => {
127
+ if (stripKeys.includes(key)) {
128
+ return;
129
+ }
127
130
  if (typeof value === "object" && value !== null) {
128
131
  if (seen.has(value)) {
129
132
  // Circular reference found, discard key
@@ -135,7 +138,7 @@ function getCircularReplacer() {
135
138
  return value;
136
139
  };
137
140
  }
138
- function toJSON(value) {
139
- return JSON.parse(JSON.stringify(value, getCircularReplacer()));
141
+ function toJSON(value, stripKeys = []) {
142
+ return JSON.parse(JSON.stringify(value, getCircularReplacer(stripKeys)));
140
143
  }
141
144
  exports.toJSON = toJSON;
@@ -45,12 +45,13 @@ export declare type AnyRoute = {
45
45
  matcher: (url: string, query: ParsedQuery<string> | undefined) => any;
46
46
  reverser: any;
47
47
  redirect?: any;
48
+ canMatch?: any;
48
49
  };
49
50
 
50
51
  /**
51
52
  * @public
52
53
  */
53
- export declare type AnyXstateTreeMachine = XstateTreeMachine<AnyStateMachine>;
54
+ export declare type AnyXstateTreeMachine = XstateTreeMachine<AnyStateMachine, any, any, any[]>;
54
55
 
55
56
  /**
56
57
  * @public
@@ -91,6 +92,7 @@ export declare function buildCreateRoute(history: () => XstateTreeHistory, baseP
91
92
  meta?: TMeta | undefined;
92
93
  redirect?: RouteRedirect<MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta> & SharedMeta> | undefined;
93
94
  preload?: RouteArgumentFunctions<void, MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta>, RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta>>> | undefined;
95
+ canMatch?: RouteArgumentFunctions<boolean, MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta> & SharedMeta, RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta> & SharedMeta>> | undefined;
94
96
  }) => Route<MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, TEvent, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta> & SharedMeta>;
95
97
  route<TBaseRoute_1 extends AnyRoute>(baseRoute?: TBaseRoute_1 | undefined): <TEvent_1 extends string, TParamsSchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
96
98
  [x: string]: any;
@@ -100,7 +102,7 @@ export declare function buildCreateRoute(history: () => XstateTreeHistory, baseP
100
102
  [x: string]: any;
101
103
  }, {
102
104
  [x: string]: any;
103
- }> | undefined, TMeta_1 extends Record<string, unknown>>({ event, matcher, reverser, paramsSchema, querySchema, redirect, preload, }: {
105
+ }> | undefined, TMeta_1 extends Record<string, unknown>>({ event, matcher, reverser, paramsSchema, querySchema, redirect, preload, canMatch, }: {
104
106
  event: TEvent_1;
105
107
  paramsSchema?: TParamsSchema_1 | undefined;
106
108
  querySchema?: TQuerySchema_1 | undefined;
@@ -122,6 +124,7 @@ export declare function buildCreateRoute(history: () => XstateTreeHistory, baseP
122
124
  */
123
125
  reverser: RouteArgumentFunctions<string, MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>, RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>>>;
124
126
  preload?: RouteArgumentFunctions<void, MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>, RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>>> | undefined;
127
+ canMatch?: RouteArgumentFunctions<boolean, MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1> & SharedMeta, RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1> & SharedMeta>> | undefined;
125
128
  }) => Route<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, TEvent_1, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1> & SharedMeta>;
126
129
  };
127
130
 
@@ -136,7 +139,9 @@ export declare function buildCreateRoute(history: () => XstateTreeHistory, baseP
136
139
  export declare function buildRootComponent<TMachine extends AnyXstateTreeMachine>(options: {
137
140
  machine: TMachine;
138
141
  } & MarkOptionalLikePropertiesOptional<RootOptions<InputFrom<TMachine>>>): {
139
- (): JSX.Element;
142
+ ({ children, }: {
143
+ children?: React_2.ReactNode;
144
+ }): JSX.Element;
140
145
  rootMachine: TMachine;
141
146
  };
142
147
 
@@ -329,7 +334,7 @@ declare type OmitOptional<T> = {
329
334
  */
330
335
  export declare function onBroadcast(handler: (event: GlobalEvents) => void): () => void;
331
336
 
332
- declare type Options<TStateMachine extends AnyStateMachine> = {
337
+ declare type Options<TStateMachine extends AnyXstateTreeMachine> = {
333
338
  /**
334
339
  * Displayed while the promise is resolving, defaults to returning null
335
340
  */
@@ -370,6 +375,14 @@ export declare type Query<T> = T extends {
370
375
  query: infer TQuery;
371
376
  } ? TQuery : undefined;
372
377
 
378
+ /**
379
+ * Repairs the return type of the `provide` function on XstateTreeMachines to correctly return
380
+ * an XstateTreeMachine type instead of an xstate StateMachine
381
+ */
382
+ declare type RepairProvideReturnType<T extends AnyStateMachine, TSelectorsOutput, TActionsOutput, TSlots extends readonly Slot[]> = {
383
+ [K in keyof T]: K extends "provide" ? (...args: Parameters<T[K]>) => XstateTreeMachine<T, TSelectorsOutput, TActionsOutput, TSlots> : T[K];
384
+ };
385
+
373
386
  declare type ResolveZodType<T extends Z.ZodType<any> | undefined> = undefined extends T ? undefined : Z.TypeOf<Exclude<T, undefined>>;
374
387
 
375
388
  declare type Return<TRoutes extends Route<any, any, any, any>[]> = {
@@ -464,6 +477,12 @@ export declare type Route<TParams, TQuery, TEvent, TMeta> = {
464
477
  paramsSchema?: Z.ZodObject<any>;
465
478
  querySchema?: Z.ZodObject<any>;
466
479
  redirect?: RouteRedirect<TParams, TQuery, TMeta>;
480
+ /**
481
+ * Optional predicate to control whether this route can be matched.
482
+ * Called after URL matching but before the route is considered matched.
483
+ * Useful for access control or conditional routing.
484
+ */
485
+ canMatch?: RouteArgumentFunctions<boolean, TParams, TQuery, TMeta>;
467
486
  };
468
487
 
469
488
  /**
@@ -661,6 +680,18 @@ export declare function useActiveRouteEvents(): {
661
680
  */
662
681
  export declare function useIsRouteActive(...routes: AnyRoute[]): boolean;
663
682
 
683
+ /**
684
+ * @public
685
+ * Accepts a single Route and returns true if the route is currently active and marked as an index route.
686
+ * False if not.
687
+ *
688
+ * If used outside of a RoutingContext, an error will be thrown.
689
+ * @param route - the route to check
690
+ * @returns true if the route is active and an index route, false if not
691
+ * @throws if used outside of an xstate-tree root
692
+ */
693
+ export declare function useOnRoute(route: AnyRoute): boolean;
694
+
664
695
  /**
665
696
  * @public
666
697
  * Returns the arguments for the given route if the route is active.
@@ -691,6 +722,7 @@ export declare type View<TActionsOutput, TSelectorsOutput, TSlots extends readon
691
722
  slots: Record<GetSlotNames<TSlots>, React_2.ComponentType>;
692
723
  actions: TActionsOutput;
693
724
  selectors: TSelectorsOutput;
725
+ children?: React_2.ReactNode;
694
726
  }>;
695
727
 
696
728
  /**
@@ -701,7 +733,7 @@ export declare type View<TActionsOutput, TSelectorsOutput, TSlots extends readon
701
733
  * @param view - the React view you want to invoke in an xstate machine
702
734
  * @returns The view wrapped into an xstate-tree machine, ready to be invoked by other xstate machines or used with `buildRootComponent`
703
735
  */
704
- export declare function viewToMachine(view: () => JSX.Element): AnyXstateTreeMachine;
736
+ export declare function viewToMachine(view: (args?: any) => JSX.Element | null): AnyXstateTreeMachine;
705
737
 
706
738
  declare type WithParentPath<TCurrent extends string, TParentPath extends string> = `${TParentPath extends "" ? "" : `${TParentPath}.`}${TCurrent}`;
707
739
 
@@ -716,7 +748,7 @@ export declare type XstateTreeHistory<T = unknown> = History_2<{
716
748
  /**
717
749
  * @public
718
750
  */
719
- export declare type XstateTreeMachine<TMachine extends AnyStateMachine, TSelectorsOutput = ContextFrom<TMachine>, TActionsOutput = Record<never, string>, TSlots extends readonly Slot[] = Slot[]> = TMachine & XstateTreeMachineInjection<TMachine, TSelectorsOutput, TActionsOutput, TSlots>;
751
+ export declare type XstateTreeMachine<TMachine extends AnyStateMachine, TSelectorsOutput = ContextFrom<TMachine>, TActionsOutput = Record<never, string>, TSlots extends readonly Slot[] = Slot[]> = RepairProvideReturnType<TMachine, TSelectorsOutput, TActionsOutput, TSlots> & XstateTreeMachineInjection<TMachine, TSelectorsOutput, TActionsOutput, TSlots>;
720
752
 
721
753
  /**
722
754
  * @internal
package/lib/xstateTree.js CHANGED
@@ -32,7 +32,6 @@ const fast_memoize_1 = __importDefault(require("fast-memoize"));
32
32
  const react_2 = __importStar(require("react"));
33
33
  const tiny_emitter_1 = require("tiny-emitter");
34
34
  const routing_1 = require("./routing");
35
- const providers_1 = require("./routing/providers");
36
35
  const useConstant_1 = require("./useConstant");
37
36
  const useService_1 = require("./useService");
38
37
  const utils_1 = require("./utils");
@@ -69,8 +68,8 @@ interpreter) {
69
68
  return interpreter.sessionId;
70
69
  }
71
70
  const getViewForInterpreter = (0, fast_memoize_1.default)((interpreter) => {
72
- return react_2.default.memo(function InterpreterView() {
73
- const activeRouteEvents = (0, providers_1.useActiveRouteEvents)();
71
+ return react_2.default.memo(function InterpreterView({ children, }) {
72
+ const activeRouteEvents = (0, routing_1.useActiveRouteEvents)();
74
73
  (0, react_2.useEffect)(() => {
75
74
  if (activeRouteEvents) {
76
75
  activeRouteEvents.forEach((event) => {
@@ -80,7 +79,7 @@ const getViewForInterpreter = (0, fast_memoize_1.default)((interpreter) => {
80
79
  });
81
80
  }
82
81
  }, []);
83
- return react_2.default.createElement(XstateTreeView, { actor: interpreter });
82
+ return react_2.default.createElement(XstateTreeView, { actor: interpreter }, children);
84
83
  });
85
84
  },
86
85
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -105,7 +104,7 @@ function useSlots(interpreter, slots) {
105
104
  return slots.reduce((views, slot) => {
106
105
  return {
107
106
  ...views,
108
- [slot]: () => {
107
+ [slot]: (({ children: reactChildren }) => {
109
108
  // eslint-disable-next-line react-hooks/rules-of-hooks
110
109
  const [__, children] = (0, useService_1.useService)(interpreter);
111
110
  if (slot.toString().endsWith("Multi")) {
@@ -116,26 +115,25 @@ function useSlots(interpreter, slots) {
116
115
  const interpreterForSlot = children[`${slot.toLowerCase()}-slot`];
117
116
  if (interpreterForSlot) {
118
117
  const View = getViewForInterpreter(interpreterForSlot);
119
- return react_2.default.createElement(View, null);
118
+ return react_2.default.createElement(View, null, reactChildren);
120
119
  }
121
120
  else {
122
121
  // Waiting for the interpreter for this slot to be invoked
123
122
  return null;
124
123
  }
125
124
  }
126
- },
125
+ }),
127
126
  };
128
127
  }, {});
129
128
  });
130
129
  }
131
130
  function XstateTreeMultiSlotView({ childInterpreters, }) {
132
- console.log("XstateTreeMultiSlotView", childInterpreters);
133
131
  return (react_2.default.createElement(react_2.default.Fragment, null, childInterpreters.map((i) => (react_2.default.createElement(XstateTreeView, { key: i.id, actor: i })))));
134
132
  }
135
133
  /**
136
134
  * @internal
137
135
  */
138
- function XstateTreeView({ actor }) {
136
+ function XstateTreeView({ actor, children }) {
139
137
  const [current] = (0, useService_1.useService)(actor);
140
138
  const currentRef = (0, react_2.useRef)(current);
141
139
  currentRef.current = current;
@@ -175,7 +173,7 @@ function XstateTreeView({ actor }) {
175
173
  inState: inState,
176
174
  meta: (0, utils_1.mergeMeta)(current.getMeta()),
177
175
  });
178
- return (react_2.default.createElement(View, { selectors: selectorsRef.current, actions: actions, slots: slots }));
176
+ return (react_2.default.createElement(View, { selectors: selectorsRef.current, actions: actions, slots: slots }, children));
179
177
  }
180
178
  exports.XstateTreeView = XstateTreeView;
181
179
  /**
@@ -211,7 +209,7 @@ function buildRootComponent(options) {
211
209
  if (!machine._xstateTree.View) {
212
210
  throw new Error("Root machine has no associated view");
213
211
  }
214
- const RootComponent = function XstateTreeRootComponent() {
212
+ const RootComponent = function XstateTreeRootComponent({ children, }) {
215
213
  const lastSnapshotsRef = (0, react_2.useRef)({});
216
214
  const [_, __, interpreter] = (0, react_1.useActor)(machine, {
217
215
  input,
@@ -221,15 +219,20 @@ function buildRootComponent(options) {
221
219
  console.log(`[xstate-tree] actor spawned: ${event.actorRef.id}`);
222
220
  break;
223
221
  case "@xstate.event":
222
+ // Ignore internal events
223
+ if (event.event.type.includes("xstate.")) {
224
+ return;
225
+ }
224
226
  console.log(`[xstate-tree] event: ${event.sourceRef ? event.sourceRef.id : "UNKNOWN"} -> ${event.event.type} -> ${event.actorRef.id}`, event.event);
225
227
  break;
226
228
  case "@xstate.snapshot":
227
229
  const lastSnapshot = lastSnapshotsRef.current[event.actorRef.sessionId];
230
+ const strippedKeys = ["_subscription"];
228
231
  if (!lastSnapshot) {
229
- console.log(`[xstate-tree] initial snapshot: ${event.actorRef.id}`, (0, utils_1.toJSON)(event.snapshot));
232
+ console.log(`[xstate-tree] initial snapshot: ${event.actorRef.id}`, (0, utils_1.toJSON)(event.snapshot, strippedKeys));
230
233
  }
231
234
  else {
232
- console.log(`[xstate-tree] snapshot: ${event.actorRef.id} transitioning to`, (0, utils_1.toJSON)(event.snapshot), "from", (0, utils_1.toJSON)(lastSnapshot));
235
+ console.log(`[xstate-tree] snapshot: ${event.actorRef.id} transitioning to`, (0, utils_1.toJSON)(event.snapshot, strippedKeys), "from", (0, utils_1.toJSON)(lastSnapshot, strippedKeys));
233
236
  }
234
237
  lastSnapshotsRef.current[event.actorRef.sessionId] = event.snapshot;
235
238
  break;
@@ -243,7 +246,10 @@ function buildRootComponent(options) {
243
246
  activeRouteEventsRef.current = events;
244
247
  };
245
248
  const insideRoutingContext = (0, routing_1.useInRoutingContext)();
246
- if (insideRoutingContext && typeof routing !== "undefined") {
249
+ const inTestRoutingContext = (0, routing_1.useInTestRoutingContext)();
250
+ if (!inTestRoutingContext &&
251
+ insideRoutingContext &&
252
+ typeof routing !== "undefined") {
247
253
  const m = "Routing root rendered inside routing context, this implies a bug";
248
254
  if (process.env.NODE_ENV !== "production") {
249
255
  throw new Error(m);
@@ -363,10 +369,10 @@ function buildRootComponent(options) {
363
369
  }, [activeRoute]);
364
370
  if (routingProviderValue) {
365
371
  return (react_2.default.createElement(routing_1.RoutingContext.Provider, { value: routingProviderValue },
366
- react_2.default.createElement(XstateTreeView, { actor: interpreter })));
372
+ react_2.default.createElement(XstateTreeView, { actor: interpreter }, children)));
367
373
  }
368
374
  else {
369
- return react_2.default.createElement(XstateTreeView, { actor: interpreter });
375
+ return react_2.default.createElement(XstateTreeView, { actor: interpreter }, children);
370
376
  }
371
377
  };
372
378
  RootComponent.rootMachine = machine;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@koordinates/xstate-tree",
3
3
  "main": "lib/index.js",
4
4
  "types": "lib/xstate-tree.d.ts",
5
- "version": "5.1.0-next.9",
5
+ "version": "5.2.0",
6
6
  "license": "MIT",
7
7
  "description": "Build UIs with Actors using xstate and React",
8
8
  "keywords": [