@koordinates/xstate-tree 2.0.11 → 3.0.0-beta.2

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.
@@ -1,6 +1,5 @@
1
1
  import { match, compile } from "path-to-regexp";
2
2
  import { parse, stringify } from "query-string";
3
- import { isNil } from "../../utils";
4
3
  import { joinRoutes } from "../joinRoutes";
5
4
  /**
6
5
  * @public
@@ -19,163 +18,137 @@ export function buildCreateRoute(history, basePath) {
19
18
  });
20
19
  }
21
20
  return {
22
- /**
23
- * Creates a dynamic Route using the supplied options
24
- *
25
- * The return value of dynamicRoute is a function that accepts the routes "dynamic" options
26
- * The argument to dynamicRoute itself is the params/query/meta schemas defining the route
27
- *
28
- * The returned function accepts a singular option object with the following fields
29
- *
30
- * `event`, the string constant for the routes event
31
- * `matches`, a function that is passed a url/query string and determines if the route matches
32
- * if the route is matched it returns the extracted params/query objects
33
- * `reverse`, a function that is passed params/query objects and turns them into a URL
34
- *
35
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
36
- */
37
- dynamicRoute: function createDynamicRoute(opts) {
38
- return ({ event, matches, reverse, }) => {
39
- return {
40
- paramsSchema: opts === null || opts === void 0 ? void 0 : opts.params,
41
- querySchema: opts === null || opts === void 0 ? void 0 : opts.query,
42
- event,
43
- history,
44
- basePath,
45
- parent: undefined,
46
- // @ts-ignore the usual
47
- getEvent({ params, query, meta } = {}) {
48
- return { type: event, params, query, meta };
49
- },
50
- // @ts-ignore not sure how to type this
51
- matches(url, search) {
52
- const query = parse(search);
53
- const match = matches(url, query);
21
+ simpleRoute(baseRoute) {
22
+ return ({ url, paramsSchema, querySchema, ...args }) => {
23
+ const matcher = match(url, { end: false });
24
+ const reverser = compile(url);
25
+ return this.route(baseRoute)({
26
+ ...args,
27
+ paramsSchema,
28
+ querySchema,
29
+ // @ts-ignore :cry:
30
+ matcher: (url, query) => {
31
+ const match = matcher(url);
54
32
  if (match === false) {
55
- return undefined;
33
+ return false;
56
34
  }
57
- if ((opts === null || opts === void 0 ? void 0 : opts.params) && "params" in match) {
58
- opts.params.parse(match.params);
35
+ const params = match.params;
36
+ if (params && paramsSchema) {
37
+ paramsSchema.parse(params);
59
38
  }
60
- if ((opts === null || opts === void 0 ? void 0 : opts.query) && "query" in match) {
61
- opts.query.parse(match.query);
39
+ if (query && querySchema) {
40
+ querySchema.parse(query);
62
41
  }
63
- return { type: event, originalUrl: `${url}${search}`, ...match };
64
- },
65
- // @ts-ignore not sure how to type this correctly
66
- // The types from external to this function are correct however
67
- reverse({ params, query } = {}) {
68
- return reverse({ params, query });
42
+ return {
43
+ matchLength: match.path.length,
44
+ params,
45
+ query,
46
+ };
69
47
  },
70
- // @ts-ignore not sure how to type this correctly
71
- // The types from external to this function are correct however
72
- navigate({ params, query, meta } = {}) {
73
- // @ts-ignore same problem
74
- const url = this.reverse({ params, query });
75
- navigate({
76
- url: joinRoutes(this.basePath, url),
77
- meta,
78
- history: this.history,
79
- });
48
+ // @ts-ignore :cry:
49
+ reverser: (args) => {
50
+ const url = reverser(args.params);
51
+ if (args.query) {
52
+ return `${url}?${stringify(args.query)}`;
53
+ }
54
+ return url;
80
55
  },
81
- };
56
+ });
82
57
  };
83
58
  },
84
- /**
85
- * Creates a static Route using the supplied options
86
- *
87
- * The return value of staticRoute is a function that accepts the routes options
88
- * The only argument to staticRoute itself is an optional parent route
89
- *
90
- * The returned function accepts 3 arguments
91
- *
92
- * 1. URL of the route
93
- * 2. The event type of the route
94
- * 3. The routes options, params schema, query schema and meta type
95
- *
96
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
97
- *
98
- * When creating a route that has a parent route, the following happens
99
- *
100
- * 1. The parent routes url is prepended to the routes URL
101
- * 2. The parents params schema is merged with the routes schema
102
- * 3. The parents meta type is merged with the routes meta type
103
- */
104
- staticRoute: function createStaticRoute(baseRoute) {
105
- return (url, event, opts) => {
106
- if (baseRoute && isNil(baseRoute.url)) {
107
- throw new Error("Somehow constructing a route with a base route missing a URL, did you pass a dynamic route?");
59
+ route(baseRoute) {
60
+ function getParentArray() {
61
+ const parentRoutes = [];
62
+ let currentParent = baseRoute;
63
+ while (currentParent) {
64
+ parentRoutes.unshift(currentParent);
65
+ currentParent = currentParent.parent;
66
+ }
67
+ return parentRoutes;
68
+ }
69
+ return ({ event, matcher, reverser, paramsSchema, querySchema, redirect, }) => {
70
+ let fullParamsSchema = paramsSchema;
71
+ let parentRoute = baseRoute;
72
+ while (fullParamsSchema && parentRoute) {
73
+ if (parentRoute.paramsSchema) {
74
+ fullParamsSchema = fullParamsSchema.merge(parentRoute.paramsSchema);
75
+ }
76
+ parentRoute = parentRoute.parent;
108
77
  }
109
- const urlWithTrailingSlash = url.endsWith("/") ? url : `${url}/`;
110
- const fullUrl = baseRoute
111
- ? joinRoutes(baseRoute.url, urlWithTrailingSlash)
112
- : urlWithTrailingSlash;
113
- const matcher = match(fullUrl, {});
114
- const reverser = compile(fullUrl);
115
- const paramsSchema = (baseRoute === null || baseRoute === void 0 ? void 0 : baseRoute.paramsSchema)
116
- ? (opts === null || opts === void 0 ? void 0 : opts.params)
117
- ? baseRoute.paramsSchema.merge(opts.params)
118
- : baseRoute.paramsSchema
119
- : (opts === null || opts === void 0 ? void 0 : opts.params)
120
- ? opts.params
121
- : undefined;
122
78
  return {
123
- paramsSchema,
124
- querySchema: opts === null || opts === void 0 ? void 0 : opts.query,
79
+ basePath,
125
80
  event,
126
81
  history,
127
- basePath,
128
- url: fullUrl,
82
+ paramsSchema,
83
+ querySchema,
129
84
  parent: baseRoute,
130
- // @ts-ignore the usual
131
- getEvent({ params, query, meta } = {}) {
85
+ redirect,
86
+ matcher: matcher,
87
+ reverser: reverser,
88
+ // @ts-ignore :cry:
89
+ getEvent(args) {
90
+ const { params, query, meta } = args !== null && args !== void 0 ? args : {};
132
91
  return { type: event, params, query, meta };
133
92
  },
134
- // @ts-ignore not sure how to type this
135
- matches(url, search) {
136
- const fullUrl = url.endsWith("/") ? url : `${url}/`;
137
- const matches = matcher(fullUrl);
138
- if (matches === false) {
139
- return undefined;
93
+ // @ts-ignore :cry:
94
+ matches(suppliedUrl, search) {
95
+ var _a, _b, _c;
96
+ const fullUrl = suppliedUrl.endsWith("/")
97
+ ? suppliedUrl
98
+ : suppliedUrl + "/";
99
+ let url = fullUrl;
100
+ const parentRoutes = getParentArray();
101
+ let params = {};
102
+ while (parentRoutes.length) {
103
+ const parentRoute = parentRoutes.shift();
104
+ const parentMatch = parentRoute.matcher(url, undefined);
105
+ if (parentMatch === false) {
106
+ return false;
107
+ }
108
+ url = url.slice(parentMatch.matchLength);
109
+ // All routes assume the url starts with a /
110
+ // so if the parent route matches the / in the url, which consumes it
111
+ // need to re-add it for the next route to match against
112
+ if (!url.startsWith("/")) {
113
+ url = "/" + url;
114
+ }
115
+ params = { ...params, ...((_a = parentMatch.params) !== null && _a !== void 0 ? _a : {}) };
140
116
  }
141
- const params = matches.params;
142
- if (params && paramsSchema) {
143
- paramsSchema.parse(params);
117
+ const matches = matcher(url, parse(search));
118
+ // if there is any URL left after matching this route, the last to match
119
+ // that means the match isn't actually a match
120
+ if (matches === false || matches.matchLength !== url.length) {
121
+ return false;
144
122
  }
145
- const query = parse(search);
146
- if (opts === null || opts === void 0 ? void 0 : opts.query) {
147
- opts.query.parse(query);
123
+ const fullParams = {
124
+ ...params,
125
+ ...((_b = matches.params) !== null && _b !== void 0 ? _b : {}),
126
+ };
127
+ if (fullParamsSchema) {
128
+ fullParamsSchema.parse(fullParams);
129
+ }
130
+ if (querySchema) {
131
+ querySchema.parse(matches.query);
148
132
  }
149
133
  return {
150
- type: event,
151
134
  originalUrl: `${fullUrl}${search}`,
152
- params,
153
- query,
135
+ type: event,
136
+ params: fullParams,
137
+ query: (_c = matches.query) !== null && _c !== void 0 ? _c : {},
154
138
  };
155
139
  },
156
- // @ts-ignore not sure how to type this correctly
157
- // The types from external to this function are correct however
158
- reverse({ params, query } = {}) {
159
- const url = (() => {
160
- if (params) {
161
- // @ts-ignore same problem
162
- return reverser(params);
163
- }
164
- else {
165
- return reverser();
166
- }
167
- })();
168
- if (!isNil(query)) {
169
- return `${url}?${stringify(query)}`;
170
- }
171
- else {
172
- return url;
173
- }
140
+ // @ts-ignore :cry:
141
+ reverse(args) {
142
+ const { params, query } = args !== null && args !== void 0 ? args : {};
143
+ const parentRoutes = getParentArray();
144
+ const baseUrl = parentRoutes
145
+ .map((route) => route.reverser({ params }))
146
+ .reduce((fullUrl, urlPartial) => joinRoutes(fullUrl, urlPartial), "");
147
+ return `${joinRoutes(baseUrl, reverser({ params, query }))}`;
174
148
  },
175
- // @ts-ignore not sure how to type this correctly
176
- // The types from external to this function are correct however
177
- navigate({ params, query, meta } = {}) {
178
- // @ts-ignore same problem
149
+ // @ts-ignore :cry:
150
+ navigate(args) {
151
+ const { params, query, meta } = args !== null && args !== void 0 ? args : {};
179
152
  const url = this.reverse({ params, query });
180
153
  navigate({
181
154
  url: joinRoutes(this.basePath, url),
@@ -3,7 +3,7 @@ import { matchRoute } from "../matchRoute";
3
3
  /**
4
4
  * @internal
5
5
  */
6
- export function handleLocationChange(routes, basePath, path, search, setActiveRouteEvents, meta) {
6
+ export function handleLocationChange(routes, basePath, path, search, meta) {
7
7
  console.debug("[xstate-tree] Matching routes", basePath, path, search, meta);
8
8
  const match = matchRoute(routes, basePath, path, search);
9
9
  console.debug("[xstate-tree] Match result", match);
@@ -14,9 +14,11 @@ export function handleLocationChange(routes, basePath, path, search, setActiveRo
14
14
  };
15
15
  // @ts-ignore the event won't match GlobalEvents
16
16
  broadcast(fourOhFour);
17
+ return;
17
18
  }
18
19
  else if (match.type === "match-error") {
19
20
  console.error("Error matching route for", location.pathname);
21
+ return;
20
22
  }
21
23
  else {
22
24
  const matchedEvent = match.event;
@@ -29,7 +31,7 @@ export function handleLocationChange(routes, basePath, path, search, setActiveRo
29
31
  routingEvents.push(route.parent.getEvent({ params, query: {}, meta: { ...(meta !== null && meta !== void 0 ? meta : {}) } }));
30
32
  route = route.parent;
31
33
  }
32
- setActiveRouteEvents([...routingEvents, match.event]);
34
+ const clonedRoutingEvents = [...routingEvents];
33
35
  while (routingEvents.length > 0) {
34
36
  const event = routingEvents.pop();
35
37
  // copy the originalUrl to all parent events
@@ -39,5 +41,9 @@ export function handleLocationChange(routes, basePath, path, search, setActiveRo
39
41
  }
40
42
  // @ts-ignore the event won't match GlobalEvents
41
43
  broadcast(matchedEvent);
44
+ return {
45
+ events: [...clonedRoutingEvents, match.event],
46
+ matchedRoute: match.route,
47
+ };
42
48
  }
43
49
  }
@@ -1,5 +1,15 @@
1
1
  export function joinRoutes(base, route) {
2
2
  const realBase = base.endsWith("/") ? base.slice(0, -1) : base;
3
3
  const realRoute = route.startsWith("/") ? route : `/${route}`;
4
- return realBase + realRoute;
4
+ const joinedUrl = realBase + realRoute;
5
+ if (!joinedUrl.endsWith("/")) {
6
+ if (!joinedUrl.includes("?")) {
7
+ return `${joinedUrl}/`;
8
+ }
9
+ if (!joinedUrl.includes("/?")) {
10
+ return joinedUrl.replace("?", "/?");
11
+ }
12
+ return joinedUrl;
13
+ }
14
+ return joinedUrl;
5
15
  }
@@ -32,12 +32,14 @@ export declare type AnyRoute = {
32
32
  navigate: any;
33
33
  getEvent: any;
34
34
  event: string;
35
- url?: string;
36
35
  basePath: string;
37
36
  history: XstateTreeHistory;
38
37
  parent?: AnyRoute;
39
38
  paramsSchema?: Z.ZodObject<any>;
40
39
  querySchema?: Z.ZodObject<any>;
40
+ matcher: (url: string, query: ParsedQuery<string> | undefined) => any;
41
+ reverser: any;
42
+ redirect?: any;
41
43
  };
42
44
 
43
45
  /**
@@ -90,79 +92,52 @@ export declare function buildActions<TMachine extends AnyStateMachine, TActions,
90
92
  * @param basePath - the base path for this route factory
91
93
  */
92
94
  export declare function buildCreateRoute(history: XstateTreeHistory, basePath: string): {
93
- /**
94
- * Creates a dynamic Route using the supplied options
95
- *
96
- * The return value of dynamicRoute is a function that accepts the routes "dynamic" options
97
- * The argument to dynamicRoute itself is the params/query/meta schemas defining the route
98
- *
99
- * The returned function accepts a singular option object with the following fields
100
- *
101
- * `event`, the string constant for the routes event
102
- * `matches`, a function that is passed a url/query string and determines if the route matches
103
- * if the route is matched it returns the extracted params/query objects
104
- * `reverse`, a function that is passed params/query objects and turns them into a URL
105
- *
106
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
107
- */
108
- dynamicRoute: <TOpts extends Options<Z.ZodObject<any, "strip", Z.ZodTypeAny, {
95
+ simpleRoute<TBaseRoute extends AnyRoute>(baseRoute?: TBaseRoute | undefined): <TEvent extends string, TParamsSchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
109
96
  [x: string]: any;
110
97
  }, {
111
98
  [x: string]: any;
112
- }>, Z.ZodObject<any, "strip", Z.ZodTypeAny, {
99
+ }> | undefined, TQuerySchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
113
100
  [x: string]: any;
114
101
  }, {
115
102
  [x: string]: any;
116
- }>, any>>(opts?: TOpts | undefined) => <TEvent extends string, TParamsSchema = Params<TOpts>, TQuerySchema = Query<TOpts>, TMeta = Meta<TOpts>, TParams = TParamsSchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
117
- [x: string]: any;
118
- }, {
119
- [x: string]: any;
120
- }> ? Z.TypeOf<TParamsSchema> : undefined, TQuery = TQuerySchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
121
- [x: string]: any;
122
- }, {
123
- [x: string]: any;
124
- }> ? Z.TypeOf<TQuerySchema> : undefined, TFullMeta = TMeta extends undefined ? SharedMeta : TMeta & SharedMeta>({ event, matches, reverse, }: {
103
+ }> | undefined, TMeta extends Record<string, unknown>>({ url, paramsSchema, querySchema, ...args }: {
125
104
  event: TEvent;
126
- matches: (url: string, query: ParsedQuery<string>) => false | RouteArguments<TParams, TQuery, TFullMeta>;
127
- reverse: RouteArgumentFunctions<string, TParams, TQuery, TFullMeta, RouteArguments<TParams, TQuery, TFullMeta>>;
128
- }) => Route<TParams, TQuery, TEvent, TFullMeta>;
129
- /**
130
- * Creates a static Route using the supplied options
131
- *
132
- * The return value of staticRoute is a function that accepts the routes options
133
- * The only argument to staticRoute itself is an optional parent route
134
- *
135
- * The returned function accepts 3 arguments
136
- *
137
- * 1. URL of the route
138
- * 2. The event type of the route
139
- * 3. The routes options, params schema, query schema and meta type
140
- *
141
- * The params and query schemas are ZodSchemas, they both need to be an object (ie Z.object())
142
- *
143
- * When creating a route that has a parent route, the following happens
144
- *
145
- * 1. The parent routes url is prepended to the routes URL
146
- * 2. The parents params schema is merged with the routes schema
147
- * 3. The parents meta type is merged with the routes meta type
148
- */
149
- staticRoute: <TBaseRoute extends AnyRoute | undefined = undefined, TBaseParams = RouteParams<TBaseRoute>, TBaseMeta = RouteMeta<TBaseRoute>>(baseRoute?: TBaseRoute | undefined) => <TOpts_1 extends Options<Z.ZodObject<any, "strip", Z.ZodTypeAny, {
105
+ url: string;
106
+ paramsSchema?: TParamsSchema | undefined;
107
+ querySchema?: TQuerySchema | undefined;
108
+ meta?: TMeta | undefined;
109
+ redirect?: RouteRedirect<MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta> & SharedMeta> | undefined;
110
+ }) => Route<MergeRouteTypes<RouteParams<TBaseRoute>, ResolveZodType<TParamsSchema>>, ResolveZodType<TQuerySchema>, TEvent, MergeRouteTypes<RouteMeta<TBaseRoute>, TMeta> & SharedMeta>;
111
+ route<TBaseRoute_1 extends AnyRoute>(baseRoute?: TBaseRoute_1 | undefined): <TEvent_1 extends string, TParamsSchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
150
112
  [x: string]: any;
151
113
  }, {
152
114
  [x: string]: any;
153
- }>, Z.ZodObject<any, "strip", Z.ZodTypeAny, {
115
+ }> | undefined, TQuerySchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
154
116
  [x: string]: any;
155
117
  }, {
156
118
  [x: string]: any;
157
- }>, any>, TEvent_1 extends string, TParamsSchema_1 = Params<TOpts_1>, TQuerySchema_1 = Query<TOpts_1>, TMeta_1 = Meta<TOpts_1>, TParams_1 = TParamsSchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
158
- [x: string]: any;
159
- }, {
160
- [x: string]: any;
161
- }> ? Z.TypeOf<TParamsSchema_1> : undefined, TQuery_1 = TQuerySchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
162
- [x: string]: any;
163
- }, {
164
- [x: string]: any;
165
- }> ? Z.TypeOf<TQuerySchema_1> : undefined, TFullParams = TParams_1 extends undefined ? TBaseParams extends undefined ? undefined : TBaseParams : TParams_1 & (TBaseParams extends undefined ? {} : TBaseParams), TFullMeta_1 = TMeta_1 extends undefined ? TBaseMeta extends undefined ? SharedMeta : TBaseMeta & SharedMeta : TMeta_1 & (TBaseMeta extends undefined ? {} : TBaseMeta) & SharedMeta>(url: string, event: TEvent_1, opts?: TOpts_1 | undefined) => Route<TFullParams, TQuery_1, TEvent_1, TFullMeta_1>;
119
+ }> | undefined, TMeta_1 extends Record<string, unknown>>({ event, matcher, reverser, paramsSchema, querySchema, redirect, }: {
120
+ event: TEvent_1;
121
+ paramsSchema?: TParamsSchema_1 | undefined;
122
+ querySchema?: TQuerySchema_1 | undefined;
123
+ meta?: TMeta_1 | undefined;
124
+ redirect?: RouteRedirect<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1> & SharedMeta> | undefined;
125
+ /**
126
+ * Determines if the route matches the given url and query
127
+ *
128
+ * If there is no match, return false
129
+ * If there is a match, return the parsed params and query as well as the length of the matched path in the URL
130
+ */
131
+ matcher: (url: string, query: ParsedQuery<string> | undefined) => false | (RouteArguments<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1>> & {
132
+ matchLength: number;
133
+ });
134
+ /**
135
+ * Reverses the route to a URL
136
+ *
137
+ * Supplied with params/query objects and constructs the correct URL based on them
138
+ */
139
+ 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>>>;
140
+ }) => Route<MergeRouteTypes<RouteParams<TBaseRoute_1>, ResolveZodType<TParamsSchema_1>>, ResolveZodType<TQuerySchema_1>, TEvent_1, MergeRouteTypes<RouteMeta<TBaseRoute_1>, TMeta_1> & SharedMeta>;
166
141
  };
167
142
 
168
143
  /**
@@ -311,9 +286,9 @@ declare type InferViewProps<T> = T extends ViewProps<infer TSelectors, infer TAc
311
286
  inState: (state: Parameters<TMatches>[0]) => TMatches;
312
287
  } : never;
313
288
 
314
- declare type IsEmptyObject<Obj, ExcludeOptional extends boolean = false> = [
315
- keyof (ExcludeOptional extends true ? OmitOptional<Obj> : Obj)
316
- ] extends [never] ? true : false;
289
+ declare type IsEmptyObject<Obj, ExcludeOptional extends boolean = false> = undefined extends Obj ? true : [keyof (ExcludeOptional extends true ? OmitOptional<Obj> : Obj)] extends [
290
+ never
291
+ ] ? true : false;
317
292
 
318
293
  /**
319
294
  * @public
@@ -325,7 +300,7 @@ keyof (ExcludeOptional extends true ? OmitOptional<Obj> : Obj)
325
300
  * @param options - configure loading component and context to invoke machine with
326
301
  * @returns an xstate-tree machine that wraps the promise, invoking the resulting machine when it resolves
327
302
  */
328
- export declare function lazy<TMachine extends AnyStateMachine>(factory: () => Promise<TMachine>, { Loader, withContext, }?: Options_2<TMachine["context"]>): StateMachine<Context, any, Events, States, any, any, any>;
303
+ export declare function lazy<TMachine extends AnyStateMachine>(factory: () => Promise<TMachine>, { Loader, withContext, }?: Options<TMachine["context"]>): StateMachine<Context, any, Events, States, any, any, any>;
329
304
 
330
305
  /**
331
306
  * @public
@@ -372,6 +347,8 @@ export declare type MatchesFrom<T extends AnyStateMachine> = StateFrom<T>["match
372
347
  */
373
348
  export declare function matchRoute<TRoutes extends Route<any, any, any, any>[]>(routes: TRoutes, basePath: string, path: string, search: string): Return<TRoutes>;
374
349
 
350
+ declare type MergeRouteTypes<TBase, TSupplied> = undefined extends TBase ? TSupplied : undefined extends TSupplied ? TBase : TBase & TSupplied;
351
+
375
352
  /**
376
353
  * @public
377
354
  *
@@ -408,16 +385,7 @@ declare type OmitOptional<T> = {
408
385
  */
409
386
  export declare function onBroadcast(handler: (event: GlobalEvents) => void): () => void;
410
387
 
411
- /**
412
- * @public
413
- */
414
- export declare type Options<TParamsSchema extends Z.ZodObject<any>, TQuerySchema extends Z.ZodObject<any>, TMetaSchema> = {
415
- params?: TParamsSchema;
416
- query?: TQuerySchema;
417
- meta?: TMetaSchema;
418
- };
419
-
420
- declare type Options_2<TContext> = {
388
+ declare type Options<TContext> = {
421
389
  /**
422
390
  * Displayed while the promise is resolving, defaults to returning null
423
391
  */
@@ -465,6 +433,8 @@ export declare type Query<T> = T extends {
465
433
  query: infer TQuery;
466
434
  } ? TQuery : undefined;
467
435
 
436
+ declare type ResolveZodType<T extends Z.ZodType<any> | undefined> = undefined extends T ? undefined : Z.TypeOf<Exclude<T, undefined>>;
437
+
468
438
  declare type Return<TRoutes extends Route<any, any, any, any>[]> = {
469
439
  type: "matched";
470
440
  route: TRoutes[number];
@@ -498,7 +468,7 @@ export declare type Route<TParams, TQuery, TEvent, TMeta> = {
498
468
  matches: (url: string, search: string) => ({
499
469
  type: TEvent;
500
470
  originalUrl: string;
501
- } & RouteArguments<TParams, TQuery, TMeta>) | undefined;
471
+ } & RouteArguments<TParams, TQuery, TMeta>) | false;
502
472
  /**
503
473
  * Takes in query/params objects as required by the route and returns a URL for that route
504
474
  *
@@ -520,16 +490,20 @@ export declare type Route<TParams, TQuery, TEvent, TMeta> = {
520
490
  getEvent: RouteArgumentFunctions<{
521
491
  type: TEvent;
522
492
  } & RouteArguments<TParams, TQuery, TMeta>, TParams, TQuery, TMeta>;
493
+ matcher: (url: string, query: ParsedQuery<string> | undefined) => (RouteArguments<TParams, TQuery, TMeta> & {
494
+ matchLength: number;
495
+ }) | false;
496
+ reverser: RouteArgumentFunctions<string, TParams, TQuery, TMeta>;
523
497
  /**
524
498
  * Event type for this route
525
499
  */
526
500
  event: TEvent;
527
- url?: string;
528
501
  history: XstateTreeHistory;
529
502
  basePath: string;
530
503
  parent?: AnyRoute;
531
504
  paramsSchema?: Z.ZodObject<any>;
532
505
  querySchema?: Z.ZodObject<any>;
506
+ redirect?: RouteRedirect<TParams, TQuery, TMeta>;
533
507
  };
534
508
 
535
509
  /**
@@ -575,6 +549,13 @@ export declare type RouteMeta<T> = T extends Route<any, any, any, infer TMeta> ?
575
549
  */
576
550
  export declare type RouteParams<T> = T extends Route<infer TParams, any, any, any> ? TParams : undefined;
577
551
 
552
+ declare type RouteRedirect<TParams, TQuery, TMeta> = (args: MakeEmptyObjectPropertiesOptional<{
553
+ params: TParams;
554
+ query: TQuery;
555
+ meta?: TMeta;
556
+ abortSignal: AbortSignal;
557
+ }>) => Promise<undefined | RouteArguments<Partial<TParams>, Partial<TQuery>, TMeta>>;
558
+
578
559
  /**
579
560
  * @public
580
561
  */
package/lib/xstateTree.js CHANGED
@@ -6,7 +6,7 @@ import { handleLocationChange, RoutingContext, } from "./routing";
6
6
  import { useActiveRouteEvents } from "./routing/providers";
7
7
  import { useConstant } from "./useConstant";
8
8
  import { useService } from "./useService";
9
- import { isLikelyPageLoad } from "./utils";
9
+ import { assertIsDefined, isLikelyPageLoad } from "./utils";
10
10
  export const emitter = new TinyEmitter();
11
11
  /**
12
12
  * @public
@@ -173,6 +173,7 @@ export function buildRootComponent(machine, routing) {
173
173
  }
174
174
  const RootComponent = function XstateTreeRootComponent() {
175
175
  const [_, __, interpreter] = useMachine(machine, { devTools: true });
176
+ const [activeRoute, setActiveRoute] = useState(undefined);
176
177
  const activeRouteEventsRef = useRef([]);
177
178
  const [forceRenderValue, forceRender] = useState(false);
178
179
  const setActiveRouteEvents = (events) => {
@@ -187,11 +188,72 @@ export function buildRootComponent(machine, routing) {
187
188
  emitter.off("event", handler);
188
189
  };
189
190
  }, [interpreter]);
191
+ useEffect(() => {
192
+ if (activeRoute === undefined) {
193
+ return;
194
+ }
195
+ const controller = new AbortController();
196
+ const routes = [activeRoute];
197
+ let route = activeRoute;
198
+ while (route.parent) {
199
+ routes.unshift(route.parent);
200
+ route = route.parent;
201
+ }
202
+ const routeEventPairs = [];
203
+ const activeRoutesEvent = activeRouteEventsRef.current.find((e) => e.type === activeRoute.event);
204
+ assertIsDefined(activeRoutesEvent);
205
+ for (let i = 0; i < routes.length; i++) {
206
+ const route = routes[i];
207
+ const routeEvent = activeRouteEventsRef.current[i];
208
+ routeEventPairs.push([route, routeEvent]);
209
+ }
210
+ const routePairsWithRedirects = routeEventPairs.filter(([route]) => {
211
+ return route.redirect !== undefined;
212
+ });
213
+ const redirectPromises = routePairsWithRedirects.map(([route, event]) => {
214
+ assertIsDefined(route.redirect);
215
+ return route.redirect({
216
+ signal: controller.signal,
217
+ query: event.query,
218
+ params: event.params,
219
+ meta: event.meta,
220
+ });
221
+ });
222
+ void Promise.all(redirectPromises).then((redirects) => {
223
+ var _a, _b, _c;
224
+ const didAnyRedirect = redirects.some((x) => x !== undefined);
225
+ if (!didAnyRedirect || controller.signal.aborted) {
226
+ return;
227
+ }
228
+ const routeArguments = redirects.reduce((args, redirect) => {
229
+ if (redirect) {
230
+ args.query = { ...args.query, ...redirect.query };
231
+ args.params = { ...args.params, ...redirect.params };
232
+ args.meta = { ...args.meta, ...redirect.meta };
233
+ }
234
+ return args;
235
+ }, {
236
+ // since the redirect results are partials, need to merge them with the original event
237
+ // params/query to ensure that all params/query are present
238
+ query: { ...((_a = activeRoutesEvent.query) !== null && _a !== void 0 ? _a : {}) },
239
+ params: { ...((_b = activeRoutesEvent.params) !== null && _b !== void 0 ? _b : {}) },
240
+ meta: { ...((_c = activeRoutesEvent.meta) !== null && _c !== void 0 ? _c : {}) },
241
+ });
242
+ activeRoute.navigate(routeArguments);
243
+ });
244
+ return () => {
245
+ controller.abort();
246
+ };
247
+ }, [activeRoute]);
190
248
  useEffect(() => {
191
249
  if (routing) {
192
250
  const { getPathName = () => window.location.pathname, getQueryString = () => window.location.search, } = routing;
193
251
  const queryString = getQueryString();
194
- handleLocationChange(routing.routes, routing.basePath, getPathName(), getQueryString(), setActiveRouteEvents, { onloadEvent: isLikelyPageLoad() });
252
+ const result = handleLocationChange(routing.routes, routing.basePath, getPathName(), getQueryString(), { onloadEvent: isLikelyPageLoad() });
253
+ if (result) {
254
+ setActiveRouteEvents(result.events);
255
+ setActiveRoute({ ...result.matchedRoute });
256
+ }
195
257
  // Hack to ensure the initial location doesn't have undefined state
196
258
  // It's not supposed to, but it does for some reason
197
259
  // And the history library ignores popstate events with undefined state
@@ -202,7 +264,11 @@ export function buildRootComponent(machine, routing) {
202
264
  if (routing) {
203
265
  const unsub = routing.history.listen((location) => {
204
266
  var _a;
205
- handleLocationChange(routing.routes, routing.basePath, location.pathname, location.search, setActiveRouteEvents, (_a = location.state) === null || _a === void 0 ? void 0 : _a.meta);
267
+ const result = handleLocationChange(routing.routes, routing.basePath, location.pathname, location.search, (_a = location.state) === null || _a === void 0 ? void 0 : _a.meta);
268
+ if (result) {
269
+ setActiveRouteEvents(result.events);
270
+ setActiveRoute({ ...result.matchedRoute });
271
+ }
206
272
  });
207
273
  return () => {
208
274
  unsub();
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": "2.0.11",
5
+ "version": "3.0.0-beta.2",
6
6
  "license": "MIT",
7
7
  "description": "Build UIs with Actors using xstate and React",
8
8
  "keywords": [
@@ -81,7 +81,7 @@
81
81
  "test-examples": "tsc --noEmit",
82
82
  "todomvc": "vite dev",
83
83
  "build": "rimraf lib && rimraf out && tsc -p tsconfig.build.json",
84
- "build:watch": "tsc -p tsconfig.build.json -w",
84
+ "build:watch": "tsc -p tsconfig.json -w",
85
85
  "api-extractor": "api-extractor run",
86
86
  "release": "semantic-release",
87
87
  "commitlint": "commitlint --edit"