@thi.ng/router 3.4.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/CHANGELOG.md +39 -1
- package/README.md +85 -40
- package/api.d.ts +86 -36
- package/basic.d.ts +20 -31
- package/basic.js +103 -80
- package/history.d.ts +10 -7
- package/history.js +18 -23
- package/html.d.ts +31 -0
- package/html.js +91 -0
- package/index.d.ts +3 -2
- package/index.js +3 -2
- package/package.json +22 -16
- package/router.d.ts +46 -0
- package/router.js +233 -0
- package/trie.d.ts +54 -0
- package/trie.js +45 -0
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
|
+
};
|