@thi.ng/router 3.3.0 → 4.0.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/router.js ADDED
@@ -0,0 +1,233 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result)
9
+ __defProp(target, key, result);
10
+ return result;
11
+ };
12
+ import { INotifyMixin } from "@thi.ng/api/mixins/inotify";
13
+ import { isString } from "@thi.ng/checks/is-string";
14
+ import { equiv } from "@thi.ng/equiv";
15
+ import { assert } from "@thi.ng/errors/assert";
16
+ import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
17
+ import { illegalArity } from "@thi.ng/errors/illegal-arity";
18
+ import { illegalState } from "@thi.ng/errors/illegal-state";
19
+ import {
20
+ EVENT_ROUTE_CHANGED,
21
+ EVENT_ROUTE_FAILED
22
+ } from "./api.js";
23
+ import { Trie } from "./trie.js";
24
+ let Router = class {
25
+ opts;
26
+ current;
27
+ index = {};
28
+ routes = new Trie();
29
+ constructor(config) {
30
+ this.opts = {
31
+ authenticator: (match) => match,
32
+ prefix: "/",
33
+ separator: "/",
34
+ trim: true,
35
+ ...config
36
+ };
37
+ this.addRoutes(this.opts.routes);
38
+ assert(
39
+ this.routeForID(this.opts.default) !== void 0,
40
+ `missing config for default route: '${this.opts.default}'`
41
+ );
42
+ if (config.initial) {
43
+ const route = this.routeForID(config.initial);
44
+ assert(
45
+ route !== void 0,
46
+ `missing config for initial route: ${this.opts.initial}`
47
+ );
48
+ assert(!route.params, "initial route MUST not be parametric");
49
+ }
50
+ }
51
+ // @ts-ignore: arguments
52
+ // prettier-ignore
53
+ addListener(id, fn, scope) {
54
+ }
55
+ // @ts-ignore: arguments
56
+ // prettier-ignore
57
+ removeListener(id, fn, scope) {
58
+ }
59
+ // @ts-ignore: arguments
60
+ notify(event) {
61
+ }
62
+ start() {
63
+ if (this.opts.initial) {
64
+ const route = this.routeForID(this.opts.initial);
65
+ this.current = { id: route.id, params: {} };
66
+ this.notify({ id: EVENT_ROUTE_CHANGED, value: this.current });
67
+ }
68
+ }
69
+ addRoutes(routes) {
70
+ for (let r of routes) {
71
+ try {
72
+ const route = this.augmentRoute(r);
73
+ this.routes.set(route.match, route);
74
+ this.index[route.id] = route;
75
+ } catch (e) {
76
+ illegalArgs(
77
+ `error in route "${r.id}": ${e.origMessage}`
78
+ );
79
+ }
80
+ }
81
+ }
82
+ /**
83
+ * Main router function. Attempts to match given input string against all
84
+ * configured routes. Before returning, triggers {@link EVENT_ROUTE_CHANGED}
85
+ * with return value as well. If none of the routes matches, emits
86
+ * {@link EVENT_ROUTE_FAILED} and then falls back to configured default
87
+ * route.
88
+ *
89
+ * @remarks
90
+ * See {@link RouteAuthenticator} for details about `ctx` handling.
91
+ *
92
+ * @param src - route path to match
93
+ * @param ctx - arbitrary user context
94
+ */
95
+ route(src, ctx) {
96
+ if (this.opts.trim && src.charAt(src.length - 1) === this.opts.separator) {
97
+ src = src.substring(0, src.length - 1);
98
+ }
99
+ src = src.substring(this.opts.prefix.length);
100
+ let match = this.matchRoutes(src, ctx);
101
+ if (!match) {
102
+ this.notify({ id: EVENT_ROUTE_FAILED, value: src });
103
+ if (!this.handleRouteFailure()) {
104
+ return;
105
+ }
106
+ const route = this.routeForID(this.opts.default);
107
+ match = { id: route.id, redirect: true };
108
+ }
109
+ if (!equiv(match, this.current)) {
110
+ this.current = match;
111
+ this.notify({ id: EVENT_ROUTE_CHANGED, value: match });
112
+ }
113
+ return match;
114
+ }
115
+ format(...args) {
116
+ let [id, params, rest] = args;
117
+ let match;
118
+ switch (args.length) {
119
+ case 3:
120
+ match = { id, params, rest };
121
+ break;
122
+ case 2:
123
+ match = { id, params };
124
+ break;
125
+ case 1:
126
+ match = isString(id) ? { id } : id;
127
+ break;
128
+ default:
129
+ illegalArity(args.length);
130
+ }
131
+ const route = this.routeForID(match.id);
132
+ if (route) {
133
+ const params2 = match.params;
134
+ let parts = route.match.map((x) => {
135
+ if (isRouteParam(x)) {
136
+ const id2 = x.substring(1);
137
+ const p = params2?.[id2];
138
+ if (p == null) {
139
+ illegalArgs(`missing value for param '${id2}'`);
140
+ }
141
+ return p;
142
+ }
143
+ return x;
144
+ });
145
+ if (route.rest >= 0)
146
+ parts = parts.slice(0, route.rest).concat(match.rest || []);
147
+ return this.opts.prefix + parts.join(this.opts.separator);
148
+ } else {
149
+ illegalArgs(`invalid route ID: ${match.id}`);
150
+ }
151
+ }
152
+ routeForID(id) {
153
+ return this.index[id];
154
+ }
155
+ augmentRoute(route) {
156
+ const match = isString(route.match) ? route.match.split(this.opts.separator).filter((x) => !!x) : route.match;
157
+ const existing = this.routes.get(match);
158
+ if (existing) {
159
+ illegalArgs(
160
+ `duplicate route: ${match} (id: ${route.id}, conflicts with: ${existing.id})`
161
+ );
162
+ }
163
+ let hasParams = false;
164
+ const params = match.reduce((acc, x, i) => {
165
+ if (isRouteParam(x)) {
166
+ hasParams = true;
167
+ acc[i] = x.substring(1);
168
+ }
169
+ return acc;
170
+ }, {});
171
+ return {
172
+ ...route,
173
+ match,
174
+ params: hasParams ? params : void 0,
175
+ rest: match.indexOf("+")
176
+ };
177
+ }
178
+ matchRoutes(src, ctx) {
179
+ const curr = src.split(this.opts.separator);
180
+ const route = this.routes.get(curr);
181
+ if (!route)
182
+ return;
183
+ let params;
184
+ if (route.params) {
185
+ params = Object.entries(route.params).reduce(
186
+ (acc, [i, k]) => (acc[k] = curr[+i], acc),
187
+ {}
188
+ );
189
+ }
190
+ if (route.validate && !this.validateRouteParams(params, route.validate)) {
191
+ return;
192
+ }
193
+ const rest = route.rest >= 0 ? curr.slice(route.rest) : void 0;
194
+ let match = {
195
+ id: route.id,
196
+ params,
197
+ rest
198
+ };
199
+ if (route.auth) {
200
+ match = this.opts.authenticator(match, route, ctx);
201
+ if (match && !this.index[match.id]) {
202
+ illegalState(
203
+ "auth handler returned invalid route ID: " + match.id
204
+ );
205
+ }
206
+ }
207
+ return match;
208
+ }
209
+ validateRouteParams(params, validators) {
210
+ for (let id in validators) {
211
+ if (params[id] !== void 0) {
212
+ const val = validators[id];
213
+ if (val.coerce) {
214
+ params[id] = val.coerce(params[id]);
215
+ }
216
+ if (val.check && !val.check(params[id])) {
217
+ return false;
218
+ }
219
+ }
220
+ }
221
+ return true;
222
+ }
223
+ handleRouteFailure() {
224
+ return true;
225
+ }
226
+ };
227
+ Router = __decorateClass([
228
+ INotifyMixin
229
+ ], Router);
230
+ const isRouteParam = (x) => x[0] === "?";
231
+ export {
232
+ Router
233
+ };
package/trie.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Trie data structure for storing & matching route patterns with wildcards.
3
+ *
4
+ * @remarks
5
+ * Wildcard handling:
6
+ *
7
+ * - `?` - matches any single item
8
+ * - `+` - matches 1 or more items following (only to be used in final position)
9
+ *
10
+ * Match priorities (highest to lowest):
11
+ *
12
+ * 1. Non-parametric values
13
+ * 2. `?` wildcard (params)
14
+ * 3. `+` wildcard
15
+ *
16
+ * If any higher priority route fails and a compatible lower priority route
17
+ * exists, it will still be attempted to be matched.
18
+ *
19
+ * @example
20
+ * ```ts tangle:../export/wildcards.ts
21
+ * import { Trie } from "@thi.ng/router";
22
+ *
23
+ * const trie = new Trie();
24
+ * trie.set(["a", "?", "c"], "A");
25
+ * trie.set(["a", "b"], "B");
26
+ * trie.set(["a", "+"], "C");
27
+ * trie.set(["+"], "D");
28
+ *
29
+ * // matches A because B doesn't match
30
+ * // and A has higher priority than C
31
+ * console.log(trie.get(["a", "b", "c"]));
32
+ * // A
33
+ *
34
+ * // perfect match B
35
+ * console.log(trie.get(["a", "b"]));
36
+ * // B
37
+ *
38
+ * // matches C because neither A or B matches
39
+ * console.log(trie.get(["a", "b", "d"]));
40
+ * // C
41
+ *
42
+ * // matches D because all others fail
43
+ * console.log(trie.get(["a"]));
44
+ * // D
45
+ * ```
46
+ */
47
+ export declare class Trie<T> {
48
+ n: Record<string, Trie<T>>;
49
+ v?: T;
50
+ constructor(key?: string[], v?: T | undefined, i?: number);
51
+ set(key: string[], v: T, i?: number): void;
52
+ get(key: string[], i?: number): T | undefined;
53
+ }
54
+ //# sourceMappingURL=trie.d.ts.map
package/trie.js ADDED
@@ -0,0 +1,45 @@
1
+ import { illegalArgs } from "@thi.ng/errors";
2
+ class Trie {
3
+ n = {};
4
+ v;
5
+ constructor(key, v, i = 0) {
6
+ if (key && v !== void 0) {
7
+ if (i < key.length)
8
+ this.set(key, v, i);
9
+ else
10
+ this.v = v;
11
+ }
12
+ }
13
+ set(key, v, i = 0) {
14
+ if (i >= key.length) {
15
+ this.v = v;
16
+ return;
17
+ }
18
+ let k = key[i];
19
+ if (k === "+" && i < key.length - 1)
20
+ illegalArgs("`+` only allowed in tail position");
21
+ if (k[0] === "?")
22
+ k = "?";
23
+ const next = this.n[k];
24
+ if (next)
25
+ next.set(key, v, i + 1);
26
+ else
27
+ this.n[k] = new Trie(key, v, i + 1);
28
+ }
29
+ get(key, i = 0) {
30
+ if (i >= key.length)
31
+ return this.v;
32
+ let value;
33
+ let next;
34
+ if (next = this.n[key[i]])
35
+ value = next.get(key, i + 1);
36
+ if (value !== void 0)
37
+ return value;
38
+ if (next = this.n["?"])
39
+ value = next.get(key, i + 1);
40
+ return value !== void 0 ? value : this.n["+"]?.v;
41
+ }
42
+ }
43
+ export {
44
+ Trie
45
+ };