@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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
|
+
return false;
|
|
56
34
|
}
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
const params = match.params;
|
|
36
|
+
if (params && paramsSchema) {
|
|
37
|
+
paramsSchema.parse(params);
|
|
59
38
|
}
|
|
60
|
-
if (
|
|
61
|
-
|
|
39
|
+
if (query && querySchema) {
|
|
40
|
+
querySchema.parse(query);
|
|
62
41
|
}
|
|
63
|
-
return {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return reverse({ params, query });
|
|
42
|
+
return {
|
|
43
|
+
matchLength: match.path.length,
|
|
44
|
+
params,
|
|
45
|
+
query,
|
|
46
|
+
};
|
|
69
47
|
},
|
|
70
|
-
// @ts-ignore
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
124
|
-
querySchema: opts === null || opts === void 0 ? void 0 : opts.query,
|
|
79
|
+
basePath,
|
|
125
80
|
event,
|
|
126
81
|
history,
|
|
127
|
-
|
|
128
|
-
|
|
82
|
+
paramsSchema,
|
|
83
|
+
querySchema,
|
|
129
84
|
parent: baseRoute,
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
135
|
-
matches(
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
142
|
-
if
|
|
143
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
|
|
135
|
+
type: event,
|
|
136
|
+
params: fullParams,
|
|
137
|
+
query: (_c = matches.query) !== null && _c !== void 0 ? _c : {},
|
|
154
138
|
};
|
|
155
139
|
},
|
|
156
|
-
// @ts-ignore
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/xstate-tree.d.ts
CHANGED
|
@@ -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
|
-
}
|
|
99
|
+
}> | undefined, TQuerySchema extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
|
|
113
100
|
[x: string]: any;
|
|
114
101
|
}, {
|
|
115
102
|
[x: string]: any;
|
|
116
|
-
}
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
}
|
|
115
|
+
}> | undefined, TQuerySchema_1 extends Z.ZodObject<any, "strip", Z.ZodTypeAny, {
|
|
154
116
|
[x: string]: any;
|
|
155
117
|
}, {
|
|
156
118
|
[x: string]: any;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
316
|
-
]
|
|
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, }?:
|
|
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>) |
|
|
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(),
|
|
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,
|
|
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": "
|
|
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.
|
|
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"
|