@esportsplus/routing 0.0.47 → 0.0.49

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.
@@ -8,7 +8,7 @@ declare const _default: <T>(instance?: Router<T>) => {
8
8
  middleware: {
9
9
  (...middleware: Middleware<T>[]): () => T;
10
10
  dispatch(request: Request<T>): T;
11
- match(fallback: Route<T>, scheduler: Scheduler, subdomain?: string | undefined): (request: Request<T>, next: Next<T>) => import("@esportsplus/template").Renderable;
11
+ match(fallback: Route<T>, scheduler: Scheduler, subdomain?: string | undefined): (request: Request<T>, next: Next<T>) => T;
12
12
  };
13
13
  redirect: (path: string, values?: unknown[]) => void;
14
14
  router: Router<T>;
package/build/browser.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { effect, reactive, root } from '@esportsplus/reactivity';
2
- import { html } from '@esportsplus/template';
3
2
  import pipeline from '@esportsplus/pipeline';
4
3
  import factory from './router';
5
4
  let cache = [];
@@ -67,22 +66,20 @@ function middleware(request, router) {
67
66
  state.route = route || fallback;
68
67
  });
69
68
  return (request, next) => {
70
- return html `${() => {
71
- if (state.route === undefined) {
72
- throw new Error('Routing: route is undefined');
73
- }
74
- if (state.root !== undefined) {
75
- state.root.dispose();
76
- }
77
- return root((root) => {
78
- request.data = {
79
- parameters: state.parameters,
80
- route: state.route
81
- };
82
- state.root = root;
83
- return next(request);
84
- }, scheduler);
85
- }}`;
69
+ if (state.route === undefined) {
70
+ throw new Error('Routing: route is undefined');
71
+ }
72
+ if (state.root !== undefined) {
73
+ state.root.dispose();
74
+ }
75
+ return root((root) => {
76
+ request.data = {
77
+ parameters: state.parameters,
78
+ route: state.route
79
+ };
80
+ state.root = root;
81
+ return next(request);
82
+ }, scheduler);
86
83
  };
87
84
  };
88
85
  return host;
package/build/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import browser from './browser';
2
2
  import router from './router';
3
+ import { Router as R2 } from './router2';
3
4
  import slugify from './slugify';
4
- export { browser, router, slugify };
5
+ export { browser, router, slugify, R2 };
5
6
  export * from './types';
package/build/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import browser from './browser';
2
2
  import router from './router';
3
+ import { Router as R2 } from './router2';
3
4
  import slugify from './slugify';
4
- export { browser, router, slugify };
5
+ export { browser, router, slugify, R2 };
5
6
  export * from './types';
@@ -0,0 +1,3 @@
1
+ declare const METHOD_NAME_ALL = "ALL";
2
+ declare const PATH_ERROR: unique symbol;
3
+ export { METHOD_NAME_ALL, PATH_ERROR };
@@ -0,0 +1,3 @@
1
+ const METHOD_NAME_ALL = 'ALL';
2
+ const PATH_ERROR = Symbol();
3
+ export { METHOD_NAME_ALL, PATH_ERROR };
@@ -0,0 +1,11 @@
1
+ import { HandlerMap, Method, Path, Result } from './types';
2
+ declare class Router<T> {
3
+ middleware?: Record<Method, Record<Path, HandlerMap<T>[]>>;
4
+ routes?: Record<Method, Record<Path, HandlerMap<T>[]>>;
5
+ constructor();
6
+ private buildAllMatchers;
7
+ private buildMatcher;
8
+ add(method: Method, path: Path, handler: T): void;
9
+ match(method: Method, path: Path): Result<T>;
10
+ }
11
+ export { Router };
@@ -0,0 +1,226 @@
1
+ import { METHOD_NAME_ALL, PATH_ERROR } from './constants';
2
+ import { Trie } from './trie';
3
+ const NULL_MATCHER = [/^$/, [], Object.create(null)];
4
+ let empty = [], wildcardCache = Object.create(null);
5
+ function buildMatcherFromPreprocessedRoutes(routes) {
6
+ if (routes.length === 0) {
7
+ return NULL_MATCHER;
8
+ }
9
+ let handlerMetadata = [], routesWithStaticPathFlag = routes
10
+ .map((route) => [!/\*|\/:/.test(route[0]), ...route])
11
+ .sort(([isStaticA, pathA], [isStaticB, pathB]) => isStaticA ? 1 : isStaticB ? -1 : pathA.length - pathB.length), staticMap = Object.create(null), trie = new Trie();
12
+ for (let i = 0, j = -1, n = routesWithStaticPathFlag.length; i < n; i++) {
13
+ let [validatePathOnly, path, handlers] = routesWithStaticPathFlag[i];
14
+ if (validatePathOnly) {
15
+ staticMap[path] = [handlers.map(([h]) => [h, Object.create(null)]), empty];
16
+ }
17
+ else {
18
+ j++;
19
+ }
20
+ let parameterMetadata;
21
+ try {
22
+ parameterMetadata = trie.insert(path, j, validatePathOnly);
23
+ }
24
+ catch (e) {
25
+ throw e === PATH_ERROR ? new Error(`Routing: '${path}' is not supported`) : e;
26
+ }
27
+ if (validatePathOnly) {
28
+ continue;
29
+ }
30
+ handlerMetadata[j] = handlers.map(([h, i]) => {
31
+ let paramIndexMap = Object.create(null);
32
+ i -= 1;
33
+ for (; i >= 0; i--) {
34
+ let [key, value] = parameterMetadata[i];
35
+ paramIndexMap[key] = value;
36
+ }
37
+ return [h, paramIndexMap];
38
+ });
39
+ }
40
+ let [regexp, handlerReplacementMap, parameterReplacementMap] = trie.build();
41
+ for (let i = 0, n = handlerMetadata.length; i < n; i++) {
42
+ let metadata = handlerMetadata[i];
43
+ for (let j = 0, n = metadata.length; j < n; j++) {
44
+ let map = metadata[j]?.[1];
45
+ if (!map) {
46
+ continue;
47
+ }
48
+ for (let key in map) {
49
+ map[key] = parameterReplacementMap[map[key]];
50
+ }
51
+ }
52
+ }
53
+ let handlerMap = [];
54
+ for (let i = 0, n = handlerReplacementMap.length; i < n; i++) {
55
+ handlerMap[i] = handlerMetadata[handlerReplacementMap[i]];
56
+ }
57
+ return [regexp, handlerMap, staticMap];
58
+ }
59
+ function buildWildcardRegExp(path) {
60
+ return (wildcardCache[path] ??= new RegExp(path === '*'
61
+ ? ''
62
+ : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, char) => char ? `\\${char}` : '(?:|/.*)')}$`));
63
+ }
64
+ function clearWildcardCache() {
65
+ wildcardCache = Object.create(null);
66
+ }
67
+ function findMiddleware(middleware, path) {
68
+ if (!middleware) {
69
+ return undefined;
70
+ }
71
+ let keys = Object.keys(middleware).sort((a, b) => b.length - a.length);
72
+ for (let i = 0, n = keys.length; i < n; i++) {
73
+ if (buildWildcardRegExp(keys[i]).test(path)) {
74
+ return [...middleware[keys[i]]];
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+ function findOptionalParameters(path) {
80
+ if (!path.match(/\:.+\?$/)) {
81
+ return null;
82
+ }
83
+ let base = '', results = [], segments = path.split('/');
84
+ for (let i = 0, n = segments.length; i < n; i++) {
85
+ let segment = segments[i];
86
+ if (segment !== '' && !/\:/.test(segment)) {
87
+ base += '/' + segment;
88
+ }
89
+ else if (/\:/.test(segment)) {
90
+ if (/\?/.test(segment)) {
91
+ if (results.length === 0 && base === '') {
92
+ results.push('/');
93
+ }
94
+ else {
95
+ results.push(base);
96
+ }
97
+ base += '/' + segment.replace('?', '');
98
+ results.push(base);
99
+ }
100
+ else {
101
+ base += '/' + segment;
102
+ }
103
+ }
104
+ }
105
+ return results.filter((v, i, a) => a.indexOf(v) === i);
106
+ }
107
+ class Router {
108
+ middleware;
109
+ routes;
110
+ constructor() {
111
+ this.middleware = {
112
+ [METHOD_NAME_ALL]: Object.create(null)
113
+ };
114
+ this.routes = {
115
+ [METHOD_NAME_ALL]: Object.create(null)
116
+ };
117
+ }
118
+ buildAllMatchers() {
119
+ let matchers = Object.create(null);
120
+ for (let method in this.middleware) {
121
+ matchers[method] ||= this.buildMatcher(method);
122
+ }
123
+ for (let method in this.routes) {
124
+ matchers[method] ||= this.buildMatcher(method);
125
+ }
126
+ this.middleware = this.routes = undefined;
127
+ return matchers;
128
+ }
129
+ buildMatcher(method) {
130
+ let isAll = method === METHOD_NAME_ALL, properties = [this.middleware, this.routes], property, routes = [];
131
+ while (property = properties.pop()) {
132
+ let values = property[method]
133
+ ? Object.keys(property[method]).map((path) => [path, property[method][path]])
134
+ : [];
135
+ if (values.length !== 0) {
136
+ isAll ||= true;
137
+ routes.push(...values);
138
+ }
139
+ else if (method !== METHOD_NAME_ALL) {
140
+ routes.push(...Object.keys(property[METHOD_NAME_ALL]).map((path) => [path, property[METHOD_NAME_ALL][path]]));
141
+ }
142
+ }
143
+ return isAll === true ? buildMatcherFromPreprocessedRoutes(routes) : null;
144
+ }
145
+ add(method, path, handler) {
146
+ let { middleware, routes } = this;
147
+ if (!middleware || !routes) {
148
+ throw new Error('Routing: Cannot add route after matcher has been built.');
149
+ }
150
+ if (!middleware[method]) {
151
+ let properties = [middleware, routes], property;
152
+ while (property = properties.pop()) {
153
+ let copy = property[METHOD_NAME_ALL], into = property[method];
154
+ property[method] = Object.create(null);
155
+ for (let path in copy) {
156
+ into[path] = [...copy[path]];
157
+ }
158
+ }
159
+ }
160
+ if (path === '/*') {
161
+ path = '*';
162
+ }
163
+ let parameters = (path.match(/\/:/g) || []).length;
164
+ if (/\*$/.test(path)) {
165
+ let regex = buildWildcardRegExp(path);
166
+ if (method === METHOD_NAME_ALL) {
167
+ for (let m in middleware) {
168
+ middleware[m][path] ||=
169
+ findMiddleware(middleware[m], path) ||
170
+ findMiddleware(middleware[METHOD_NAME_ALL], path) ||
171
+ [];
172
+ }
173
+ }
174
+ else {
175
+ middleware[method][path] ||=
176
+ findMiddleware(middleware[method], path) ||
177
+ findMiddleware(middleware[METHOD_NAME_ALL], path) ||
178
+ [];
179
+ }
180
+ let properties = [middleware, routes], property;
181
+ while (property = properties.pop()) {
182
+ for (let m in property) {
183
+ if (method === METHOD_NAME_ALL || method === m) {
184
+ let routes = property[m];
185
+ for (let path in routes) {
186
+ regex.test(path) && routes[path].push([handler, parameters]);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ return;
192
+ }
193
+ let paths = findOptionalParameters(path) || [path];
194
+ for (let i = 0, n = paths.length; i < n; i++) {
195
+ let path = paths[i];
196
+ for (let m in routes) {
197
+ if (method === METHOD_NAME_ALL || method === m) {
198
+ let r = routes[m];
199
+ r[path] ||= [
200
+ ...(findMiddleware(middleware[m], path) ||
201
+ findMiddleware(middleware[METHOD_NAME_ALL], path) ||
202
+ []),
203
+ ];
204
+ r[path].push([handler, parameters - n + i + 1]);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ match(method, path) {
210
+ clearWildcardCache();
211
+ let matchers = this.buildAllMatchers();
212
+ this.match = (method, path) => {
213
+ let matcher = (matchers[method] || matchers[METHOD_NAME_ALL]), staticMatch = matcher[2][path];
214
+ if (staticMatch) {
215
+ return staticMatch;
216
+ }
217
+ let match = path.match(matcher[0]);
218
+ if (!match) {
219
+ return [[], empty];
220
+ }
221
+ return [matcher[1][match.indexOf('', 1)], match];
222
+ };
223
+ return this.match(method, path);
224
+ }
225
+ }
226
+ export { Router };
@@ -0,0 +1,10 @@
1
+ import type { Trie } from './trie';
2
+ import { ParameterMetadata, Path } from './types';
3
+ declare class Node {
4
+ children: Record<Path, Node>;
5
+ index?: number;
6
+ slot?: number;
7
+ buildRegexString(): Path;
8
+ insert(tokens: readonly Path[], index: number, parameters: ParameterMetadata, context: Trie['context'], validatePathOnly: boolean): void;
9
+ }
10
+ export { Node };
@@ -0,0 +1,117 @@
1
+ import { PATH_ERROR } from './constants';
2
+ const LABEL_REGEXP = '[^/]+';
3
+ const REGEXP_CHARACTERS = new Set('.\\+*[^]$()');
4
+ const WILDCARD_ONLY_REGEXP = '.*';
5
+ const WILDCARD_TAIL_REGEXP = '(?:|/.*)';
6
+ const MATCH_WILDCARD_ONLY = ['', '', WILDCARD_ONLY_REGEXP];
7
+ const MATCH_LABEL = ['', '', LABEL_REGEXP];
8
+ const MATCH_WILDCARD_TAIL = ['', '', WILDCARD_TAIL_REGEXP];
9
+ function sort(a, b) {
10
+ if (a.length === 1) {
11
+ return b.length === 1 ? (a < b ? -1 : 1) : -1;
12
+ }
13
+ else if (b.length === 1) {
14
+ return 1;
15
+ }
16
+ if (a === WILDCARD_ONLY_REGEXP || a === WILDCARD_TAIL_REGEXP) {
17
+ return 1;
18
+ }
19
+ else if (b === WILDCARD_ONLY_REGEXP || b === WILDCARD_TAIL_REGEXP) {
20
+ return -1;
21
+ }
22
+ if (a === LABEL_REGEXP) {
23
+ return 1;
24
+ }
25
+ else if (b === LABEL_REGEXP) {
26
+ return -1;
27
+ }
28
+ return a.length === b.length ? (a < b ? -1 : 1) : b.length - a.length;
29
+ }
30
+ class Node {
31
+ children = Object.create(null);
32
+ index;
33
+ slot;
34
+ buildRegexString() {
35
+ let children = this.children, path, paths = Object.keys(children).sort(sort);
36
+ for (let i = 0, n = paths.length; i < n; i++) {
37
+ let child = children[path = paths[i]];
38
+ paths[i] = (typeof child.slot === 'number'
39
+ ? `(${path})@${child.slot}`
40
+ : REGEXP_CHARACTERS.has(path)
41
+ ? `\\${path}`
42
+ : path) + child.buildRegexString();
43
+ }
44
+ if (typeof this.index === 'number') {
45
+ paths.unshift(`#${this.index}`);
46
+ }
47
+ if (paths.length === 0) {
48
+ return '';
49
+ }
50
+ else if (paths.length === 1) {
51
+ return paths[0];
52
+ }
53
+ return '(?:' + paths.join('|') + ')';
54
+ }
55
+ insert(tokens, index, parameters, context, validatePathOnly) {
56
+ if (tokens.length === 0) {
57
+ if (this.index !== undefined) {
58
+ throw PATH_ERROR;
59
+ }
60
+ else if (validatePathOnly) {
61
+ return;
62
+ }
63
+ this.index = index;
64
+ return;
65
+ }
66
+ let [token, ...remainingTokens] = tokens, node, pattern = token === '*'
67
+ ? remainingTokens.length === 0
68
+ ? MATCH_WILDCARD_ONLY
69
+ : MATCH_LABEL
70
+ : token === '/*'
71
+ ? MATCH_WILDCARD_TAIL
72
+ : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
73
+ if (pattern) {
74
+ let name = pattern[1], path = pattern[2] || LABEL_REGEXP;
75
+ if (name && pattern[2]) {
76
+ path = path.replace(/^\((?!\?:)(?=[^)]+\)$)/, '(?:');
77
+ if (/\((?!\?:)/.test(path)) {
78
+ throw PATH_ERROR;
79
+ }
80
+ }
81
+ node = this.children[path];
82
+ if (!node) {
83
+ for (let key in this.children) {
84
+ if (key !== WILDCARD_ONLY_REGEXP && key !== WILDCARD_TAIL_REGEXP) {
85
+ throw PATH_ERROR;
86
+ }
87
+ }
88
+ if (validatePathOnly) {
89
+ return;
90
+ }
91
+ node = this.children[path] = new Node();
92
+ if (name !== '') {
93
+ node.slot = context.slot++;
94
+ }
95
+ }
96
+ if (name !== '' && validatePathOnly === false) {
97
+ parameters.push([name, node.slot]);
98
+ }
99
+ }
100
+ else {
101
+ node = this.children[token];
102
+ if (!node) {
103
+ for (let key in this.children) {
104
+ if (key.length > 1 && key !== WILDCARD_ONLY_REGEXP && key !== WILDCARD_TAIL_REGEXP) {
105
+ throw PATH_ERROR;
106
+ }
107
+ }
108
+ if (validatePathOnly) {
109
+ return;
110
+ }
111
+ node = this.children[token] = new Node();
112
+ }
113
+ }
114
+ node.insert(remainingTokens, index, parameters, context, validatePathOnly);
115
+ }
116
+ }
117
+ export { Node };
@@ -0,0 +1,11 @@
1
+ import { Node } from './node';
2
+ import { Indexes, ParameterMetadata, Path } from './types';
3
+ declare class Trie {
4
+ context: {
5
+ slot: number;
6
+ };
7
+ root: Node;
8
+ build(): [RegExp, Indexes, Indexes];
9
+ insert(path: Path, index: number, pathErrorCheckOnly: boolean): ParameterMetadata;
10
+ }
11
+ export { Trie };
@@ -0,0 +1,53 @@
1
+ import { Node } from './node';
2
+ const NEVER_MATCH = [/^$/, [], []];
3
+ class Trie {
4
+ context = { slot: 0 };
5
+ root = new Node();
6
+ build() {
7
+ let regex = this.root.buildRegexString();
8
+ if (regex === '') {
9
+ return NEVER_MATCH;
10
+ }
11
+ let handlers = [], i = 0, parameters = [];
12
+ regex = regex.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handler, parameter) => {
13
+ if (typeof handler !== 'undefined') {
14
+ handlers[++i] = Number(handler);
15
+ return '$()';
16
+ }
17
+ else if (typeof parameter !== 'undefined') {
18
+ parameters[Number(parameter)] = ++i;
19
+ }
20
+ return '';
21
+ });
22
+ return [new RegExp(`^${regex}`), handlers, parameters];
23
+ }
24
+ insert(path, index, pathErrorCheckOnly) {
25
+ let groups = [], parameters = [];
26
+ for (let i = 0;;) {
27
+ let replaced = false;
28
+ path = path.replace(/\{[^}]+\}/g, (m) => {
29
+ let mark = `@\\${i}`;
30
+ groups[i++] = [mark, m];
31
+ replaced = true;
32
+ return mark;
33
+ });
34
+ if (!replaced) {
35
+ break;
36
+ }
37
+ }
38
+ let tokens = path.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
39
+ for (let i = groups.length - 1; i >= 0; i--) {
40
+ let [mark, replacement] = groups[i], token;
41
+ for (let j = tokens.length - 1; j >= 0; j--) {
42
+ token = tokens[j];
43
+ if (token.indexOf(mark) !== -1) {
44
+ token = token.replace(mark, replacement);
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ this.root.insert(tokens, index, parameters, this.context, pathErrorCheckOnly);
50
+ return parameters;
51
+ }
52
+ }
53
+ export { Trie };
@@ -0,0 +1,11 @@
1
+ type HandlerMap<T> = [T, number];
2
+ type HandlerMetadata<T> = [T, ParameterMap][];
3
+ type Indexes = number[];
4
+ type Matcher<T> = [RegExp, HandlerMetadata<T>[], StaticMap<T>];
5
+ type Method = string;
6
+ type ParameterMap = Record<string, number>;
7
+ type ParameterMetadata = [string, number][];
8
+ type Path = string;
9
+ type Result<T> = [[T, ParameterMap][], string[]] | [[T, Record<string, string>][]];
10
+ type StaticMap<T> = Record<Path, Result<T>>;
11
+ export { HandlerMap, HandlerMetadata, Indexes, Matcher, Method, ParameterMap, ParameterMetadata, Path, Result, StaticMap };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -2,8 +2,7 @@
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
4
  "@esportsplus/pipeline": "^0.0.6",
5
- "@esportsplus/reactivity": "^0.1.21",
6
- "@esportsplus/template": "^0.2.4"
5
+ "@esportsplus/reactivity": "^0.1.21"
7
6
  },
8
7
  "devDependencies": {
9
8
  "@esportsplus/typescript": "^0.1.2"
@@ -18,5 +17,5 @@
18
17
  "prepublishOnly": "npm run build"
19
18
  },
20
19
  "types": "./build/index.d.ts",
21
- "version": "0.0.47"
20
+ "version": "0.0.49"
22
21
  }
package/src/browser.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { effect, reactive, root, Root, Scheduler } from '@esportsplus/reactivity';
2
- import { html } from '@esportsplus/template';
3
2
  import { Middleware, Next, Request, Route, Router } from './types';
4
3
  import pipeline from '@esportsplus/pipeline';
5
4
  import factory from './router';
@@ -90,25 +89,23 @@ function middleware<T>(request: Request<T>, router: Router<T>) {
90
89
  });
91
90
 
92
91
  return (request: Request<T>, next: Next<T>) => {
93
- return html`${() => {
94
- if (state.route === undefined) {
95
- throw new Error('Routing: route is undefined');
96
- }
97
-
98
- if (state.root !== undefined) {
99
- state.root.dispose();
100
- }
101
-
102
- return root((root) => {
103
- request.data = {
104
- parameters: state.parameters,
105
- route: state.route
106
- };
107
- state.root = root;
108
-
109
- return next(request);
110
- }, scheduler);
111
- }}`;
92
+ if (state.route === undefined) {
93
+ throw new Error('Routing: route is undefined');
94
+ }
95
+
96
+ if (state.root !== undefined) {
97
+ state.root.dispose();
98
+ }
99
+
100
+ return root((root) => {
101
+ request.data = {
102
+ parameters: state.parameters,
103
+ route: state.route
104
+ };
105
+ state.root = root;
106
+
107
+ return next(request);
108
+ }, scheduler);
112
109
  };
113
110
  };
114
111
 
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import browser from './browser';
2
2
  import router from './router';
3
+ import { Router as R2 } from './router2';
3
4
  import slugify from './slugify';
4
5
 
5
6
 
6
- export { browser, router, slugify };
7
+ export { browser, router, slugify, R2 };
7
8
  export * from './types';
@@ -0,0 +1,6 @@
1
+ const METHOD_NAME_ALL = 'ALL';
2
+
3
+ const PATH_ERROR = Symbol();
4
+
5
+
6
+ export { METHOD_NAME_ALL, PATH_ERROR };
@@ -0,0 +1,330 @@
1
+ import { METHOD_NAME_ALL, PATH_ERROR } from './constants';
2
+ import { Trie } from './trie';
3
+ import { HandlerMap, HandlerMetadata, Matcher, Method, ParameterMap, ParameterMetadata, Path, Result, StaticMap } from './types';
4
+
5
+
6
+ const NULL_MATCHER = [/^$/, [], Object.create(null)] as Matcher<any>;
7
+
8
+
9
+ let empty: any[] = [],
10
+ wildcardCache: Record<Path, RegExp> = Object.create(null);
11
+
12
+
13
+ function buildMatcherFromPreprocessedRoutes<T>(routes: [Path, HandlerMap<T>[]][]): Matcher<T> {
14
+ if (routes.length === 0) {
15
+ return NULL_MATCHER;
16
+ }
17
+
18
+ let handlerMetadata: HandlerMetadata<T>[] = [],
19
+ routesWithStaticPathFlag = routes
20
+ .map(
21
+ (route) => [!/\*|\/:/.test(route[0]), ...route] as [boolean, Path, HandlerMap<T>[]]
22
+ )
23
+ .sort(([isStaticA, pathA], [isStaticB, pathB]) =>
24
+ isStaticA ? 1 : isStaticB ? -1 : pathA.length - pathB.length
25
+ ),
26
+ staticMap: StaticMap<T> = Object.create(null),
27
+ trie = new Trie();
28
+
29
+ for (let i = 0, j = -1, n = routesWithStaticPathFlag.length; i < n; i++) {
30
+ let [ validatePathOnly, path, handlers ] = routesWithStaticPathFlag[i];
31
+
32
+ if (validatePathOnly) {
33
+ staticMap[path] = [ handlers.map(([h]) => [h, Object.create(null)]), empty ]
34
+ }
35
+ else {
36
+ j++
37
+ }
38
+
39
+ let parameterMetadata: ParameterMetadata;
40
+
41
+ try {
42
+ parameterMetadata = trie.insert(path, j, validatePathOnly)
43
+ }
44
+ catch (e) {
45
+ throw e === PATH_ERROR ? new Error(`Routing: '${path}' is not supported`) : e
46
+ }
47
+
48
+ if (validatePathOnly) {
49
+ continue
50
+ }
51
+
52
+ handlerMetadata[j] = handlers.map(([h, i]) => {
53
+ let paramIndexMap: ParameterMap = Object.create(null);
54
+
55
+ i -= 1;
56
+
57
+ for (; i >= 0; i--) {
58
+ let [key, value] = parameterMetadata[i];
59
+
60
+ paramIndexMap[key] = value;
61
+ }
62
+
63
+ return [h, paramIndexMap];
64
+ })
65
+ }
66
+
67
+ let [regexp, handlerReplacementMap, parameterReplacementMap] = trie.build();
68
+
69
+ for (let i = 0, n = handlerMetadata.length; i < n; i++) {
70
+ let metadata = handlerMetadata[i];
71
+
72
+ for (let j = 0, n = metadata.length; j < n; j++) {
73
+ let map = metadata[j]?.[1];
74
+
75
+ if (!map) {
76
+ continue;
77
+ }
78
+
79
+ for (let key in map) {
80
+ map[key] = parameterReplacementMap[ map[key] ];
81
+ }
82
+ }
83
+ }
84
+
85
+ let handlerMap: HandlerMetadata<T>[] = [];
86
+
87
+ for (let i = 0, n = handlerReplacementMap.length; i < n; i++) {
88
+ handlerMap[i] = handlerMetadata[handlerReplacementMap[i]];
89
+ }
90
+
91
+ return [regexp, handlerMap, staticMap] as Matcher<T>;
92
+ }
93
+
94
+ function buildWildcardRegExp(path: Path): RegExp {
95
+ return (wildcardCache[path] ??= new RegExp(
96
+ path === '*'
97
+ ? ''
98
+ : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, char) =>
99
+ char ? `\\${char}` : '(?:|/.*)'
100
+ )}$`
101
+ ))
102
+ }
103
+
104
+ function clearWildcardCache() {
105
+ wildcardCache = Object.create(null);
106
+ }
107
+
108
+ function findMiddleware<T>(middleware: Record<string, T[]> | undefined, path: Path): T[] | undefined {
109
+ if (!middleware) {
110
+ return undefined;
111
+ }
112
+
113
+ let keys = Object.keys(middleware).sort((a, b) => b.length - a.length);
114
+
115
+ for (let i = 0, n = keys.length; i < n; i++) {
116
+ if (buildWildcardRegExp(keys[i]).test(path)) {
117
+ return [...middleware[keys[i]]];
118
+ }
119
+ }
120
+
121
+ return undefined;
122
+ }
123
+
124
+ // If path is `/api/animals/:type?` [`/api/animals`, `/api/animals/:type`] else null
125
+ function findOptionalParameters(path: Path): string[] | null {
126
+ if (!path.match(/\:.+\?$/)) {
127
+ return null
128
+ }
129
+
130
+ let base = '',
131
+ results: string[] = [],
132
+ segments = path.split('/');
133
+
134
+ for (let i = 0, n = segments.length; i < n; i++) {
135
+ let segment = segments[i];
136
+
137
+ if (segment !== '' && !/\:/.test(segment)) {
138
+ base += '/' + segment;
139
+ }
140
+ else if (/\:/.test(segment)) {
141
+ if (/\?/.test(segment)) {
142
+ if (results.length === 0 && base === '') {
143
+ results.push('/');
144
+ }
145
+ else {
146
+ results.push(base);
147
+ }
148
+
149
+ base += '/' + segment.replace('?', '');
150
+ results.push(base);
151
+ }
152
+ else {
153
+ base += '/' + segment;
154
+ }
155
+ }
156
+ }
157
+
158
+ return results.filter((v, i, a) => a.indexOf(v) === i);
159
+ }
160
+
161
+
162
+ class Router<T> {
163
+ middleware?: Record<Method, Record<Path, HandlerMap<T>[]>>
164
+ routes?: Record<Method, Record<Path, HandlerMap<T>[]>>
165
+
166
+
167
+ constructor() {
168
+ this.middleware = {
169
+ [METHOD_NAME_ALL]: Object.create(null)
170
+ };
171
+ this.routes = {
172
+ [METHOD_NAME_ALL]: Object.create(null)
173
+ };
174
+ }
175
+
176
+
177
+ private buildAllMatchers(): Record<Method, Matcher<T> | null> {
178
+ let matchers: Record<Method, Matcher<T> | null> = Object.create(null);
179
+
180
+ for (let method in this.middleware) {
181
+ matchers[method] ||= this.buildMatcher(method);
182
+ }
183
+
184
+ for (let method in this.routes) {
185
+ matchers[method] ||= this.buildMatcher(method);
186
+ }
187
+
188
+ this.middleware = this.routes = undefined;
189
+
190
+ return matchers;
191
+ }
192
+
193
+ private buildMatcher(method: Method): Matcher<T> | null {
194
+ let isAll = method === METHOD_NAME_ALL,
195
+ properties = [this.middleware, this.routes],
196
+ property,
197
+ routes: [Path, HandlerMap<T>[]][] = [];
198
+
199
+ while (property = properties.pop()) {
200
+ let values = property[method]
201
+ ? Object.keys(property[method]).map((path) => [path, property![method][path]])
202
+ : [];
203
+
204
+ if (values.length !== 0) {
205
+ isAll ||= true;
206
+ routes.push(...(values as typeof routes));
207
+ }
208
+ else if (method !== METHOD_NAME_ALL) {
209
+ routes.push(
210
+ ...(Object.keys(property[METHOD_NAME_ALL]).map((path) => [path, property![METHOD_NAME_ALL][path]]) as typeof routes)
211
+ );
212
+ }
213
+ }
214
+
215
+ return isAll === true ? buildMatcherFromPreprocessedRoutes(routes) : null;
216
+ }
217
+
218
+ add(method: Method, path: Path, handler: T) {
219
+ let { middleware, routes } = this;
220
+
221
+ if (!middleware || !routes) {
222
+ throw new Error('Routing: Cannot add route after matcher has been built.');
223
+ }
224
+
225
+ if (!middleware[method]) {
226
+ let properties = [middleware, routes],
227
+ property;
228
+
229
+ while (property = properties.pop()) {
230
+ let copy = property[METHOD_NAME_ALL],
231
+ into = property[method];
232
+
233
+ property[method] = Object.create(null);
234
+
235
+ for (let path in copy) {
236
+ into[path] = [ ...copy[path] ];
237
+ }
238
+ }
239
+ }
240
+
241
+ if (path === '/*') {
242
+ path = '*';
243
+ }
244
+
245
+ let parameters = (path.match(/\/:/g) || []).length;
246
+
247
+ if (/\*$/.test(path)) {
248
+ let regex = buildWildcardRegExp(path);
249
+
250
+ if (method === METHOD_NAME_ALL) {
251
+ for (let m in middleware) {
252
+ middleware[m][path] ||=
253
+ findMiddleware(middleware[m], path) ||
254
+ findMiddleware(middleware[METHOD_NAME_ALL], path) ||
255
+ [];
256
+ }
257
+ }
258
+ else {
259
+ middleware[method][path] ||=
260
+ findMiddleware(middleware[method], path) ||
261
+ findMiddleware(middleware[METHOD_NAME_ALL], path) ||
262
+ [];
263
+ }
264
+
265
+ let properties = [middleware, routes],
266
+ property;
267
+
268
+ while (property = properties.pop()) {
269
+ for (let m in property) {
270
+ if (method === METHOD_NAME_ALL || method === m) {
271
+ let routes = property[m];
272
+
273
+ for (let path in routes) {
274
+ regex.test(path) && routes[path].push([handler, parameters]);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ return;
281
+ }
282
+
283
+ let paths = findOptionalParameters(path) || [path];
284
+
285
+ for (let i = 0, n = paths.length; i < n; i++) {
286
+ let path = paths[i];
287
+
288
+ for (let m in routes) {
289
+ if (method === METHOD_NAME_ALL || method === m) {
290
+ let r = routes[m];
291
+
292
+ r[path] ||= [
293
+ ...(findMiddleware(middleware[m], path) ||
294
+ findMiddleware(middleware[METHOD_NAME_ALL], path) ||
295
+ []),
296
+ ];
297
+ r[path].push([handler, parameters - n + i + 1]);
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ match(method: Method, path: Path): Result<T> {
304
+ clearWildcardCache();
305
+
306
+ let matchers = this.buildAllMatchers();
307
+
308
+ this.match = (method, path) => {
309
+ let matcher = (matchers[method] || matchers[METHOD_NAME_ALL]) as Matcher<T>,
310
+ staticMatch = matcher[2][path];
311
+
312
+ if (staticMatch) {
313
+ return staticMatch;
314
+ }
315
+
316
+ let match = path.match(matcher[0]);
317
+
318
+ if (!match) {
319
+ return [[], empty];
320
+ }
321
+
322
+ return [matcher[1][match.indexOf('', 1)], match];
323
+ }
324
+
325
+ return this.match(method, path);
326
+ }
327
+ }
328
+
329
+
330
+ export { Router };
@@ -0,0 +1,184 @@
1
+ import { PATH_ERROR } from './constants';
2
+ import type { Trie } from './trie';
3
+ import { ParameterMetadata, Path } from './types';
4
+
5
+
6
+ const LABEL_REGEXP = '[^/]+';
7
+
8
+ const REGEXP_CHARACTERS = new Set('.\\+*[^]$()');
9
+
10
+ const WILDCARD_ONLY_REGEXP = '.*';
11
+
12
+ const WILDCARD_TAIL_REGEXP = '(?:|/.*)';
13
+
14
+
15
+ // '*' matches to all the trailing paths
16
+ const MATCH_WILDCARD_ONLY = ['', '', WILDCARD_ONLY_REGEXP];
17
+
18
+ const MATCH_LABEL = ['', '', LABEL_REGEXP];
19
+
20
+ // '/path/to/*' is /\/path\/to(?:|/.*)$
21
+ const MATCH_WILDCARD_TAIL = ['', '', WILDCARD_TAIL_REGEXP];
22
+
23
+
24
+ /**
25
+ * Sort order:
26
+ * 1. literal
27
+ * 2. special pattern (e.g. :label{[0-9]+})
28
+ * 3. common label pattern (e.g. :label)
29
+ * 4. wildcard
30
+ */
31
+ function sort(a: Path, b: Path): number {
32
+ if (a.length === 1) {
33
+ return b.length === 1 ? (a < b ? -1 : 1) : -1;
34
+ }
35
+ else if (b.length === 1) {
36
+ return 1;
37
+ }
38
+
39
+ if (a === WILDCARD_ONLY_REGEXP || a === WILDCARD_TAIL_REGEXP) {
40
+ return 1;
41
+ }
42
+ else if (b === WILDCARD_ONLY_REGEXP || b === WILDCARD_TAIL_REGEXP) {
43
+ return -1;
44
+ }
45
+
46
+ if (a === LABEL_REGEXP) {
47
+ return 1;
48
+ }
49
+ else if (b === LABEL_REGEXP) {
50
+ return -1;
51
+ }
52
+
53
+ return a.length === b.length ? (a < b ? -1 : 1) : b.length - a.length;
54
+ }
55
+
56
+
57
+ class Node {
58
+ children: Record<Path, Node> = Object.create(null);
59
+ index?: number;
60
+ slot?: number;
61
+
62
+
63
+ buildRegexString(): Path {
64
+ let children = this.children,
65
+ path,
66
+ paths: Path[] = Object.keys(children).sort(sort);
67
+
68
+ for (let i = 0, n = paths.length; i < n; i++) {
69
+ let child = children[ path = paths[i] ];
70
+
71
+ paths[i] = (
72
+ typeof child.slot === 'number'
73
+ ? `(${path})@${child.slot}`
74
+ : REGEXP_CHARACTERS.has(path)
75
+ ? `\\${path}`
76
+ : path
77
+ ) + child.buildRegexString();
78
+ }
79
+
80
+ if (typeof this.index === 'number') {
81
+ paths.unshift(`#${this.index}`);
82
+ }
83
+
84
+ if (paths.length === 0) {
85
+ return '';
86
+ }
87
+ else if (paths.length === 1) {
88
+ return paths[0];
89
+ }
90
+
91
+ return '(?:' + paths.join('|') + ')';
92
+ }
93
+
94
+ insert(
95
+ tokens: readonly Path[],
96
+ index: number,
97
+ parameters: ParameterMetadata,
98
+ context: Trie['context'],
99
+ validatePathOnly: boolean
100
+ ): void {
101
+ if (tokens.length === 0) {
102
+ if (this.index !== undefined) {
103
+ throw PATH_ERROR;
104
+ }
105
+ else if (validatePathOnly) {
106
+ return;
107
+ }
108
+
109
+ this.index = index;
110
+ return;
111
+ }
112
+
113
+ let [token, ...remainingTokens] = tokens,
114
+ node,
115
+ pattern = token === '*'
116
+ ? remainingTokens.length === 0
117
+ ? MATCH_WILDCARD_ONLY
118
+ : MATCH_LABEL
119
+ : token === '/*'
120
+ ? MATCH_WILDCARD_TAIL
121
+ : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
122
+
123
+ if (pattern) {
124
+ let name = pattern[1],
125
+ path = pattern[2] || LABEL_REGEXP;
126
+
127
+ if (name && pattern[2]) {
128
+ // (a|b) => (?:a|b)
129
+ path = path.replace(/^\((?!\?:)(?=[^)]+\)$)/, '(?:');
130
+
131
+ // prefix(?:a|b) is allowed, but prefix(a|b) is not
132
+ if (/\((?!\?:)/.test(path)) {
133
+ throw PATH_ERROR;
134
+ }
135
+ }
136
+
137
+ node = this.children[path];
138
+
139
+ if (!node) {
140
+ for (let key in this.children) {
141
+ if (key !== WILDCARD_ONLY_REGEXP && key !== WILDCARD_TAIL_REGEXP) {
142
+ throw PATH_ERROR;
143
+ }
144
+ }
145
+
146
+ if (validatePathOnly) {
147
+ return;
148
+ }
149
+
150
+ node = this.children[path] = new Node();
151
+
152
+ if (name !== '') {
153
+ node.slot = context.slot++;
154
+ }
155
+ }
156
+
157
+ if (name !== '' && validatePathOnly === false) {
158
+ parameters.push([name, node.slot as number]);
159
+ }
160
+ }
161
+ else {
162
+ node = this.children[token];
163
+
164
+ if (!node) {
165
+ for (let key in this.children) {
166
+ if (key.length > 1 && key !== WILDCARD_ONLY_REGEXP && key !== WILDCARD_TAIL_REGEXP) {
167
+ throw PATH_ERROR;
168
+ }
169
+ }
170
+
171
+ if (validatePathOnly) {
172
+ return;
173
+ }
174
+
175
+ node = this.children[token] = new Node();
176
+ }
177
+ }
178
+
179
+ node.insert(remainingTokens, index, parameters, context, validatePathOnly);
180
+ }
181
+ }
182
+
183
+
184
+ export { Node };
@@ -0,0 +1,88 @@
1
+ import { Node } from './node'
2
+ import { Indexes, ParameterMetadata, Path } from './types';
3
+
4
+
5
+ const NEVER_MATCH = [/^$/, [], []] as [RegExp, Indexes, Indexes];
6
+
7
+
8
+ class Trie {
9
+ context = { slot: 0 };
10
+ root = new Node();
11
+
12
+
13
+ build(): [RegExp, Indexes, Indexes] {
14
+ let regex = this.root.buildRegexString();
15
+
16
+ if (regex === '') {
17
+ return NEVER_MATCH;
18
+ }
19
+
20
+ let handlers: Indexes = [],
21
+ i = 0,
22
+ parameters: Indexes = [];
23
+
24
+ regex = regex.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handler, parameter) => {
25
+ if (typeof handler !== 'undefined') {
26
+ handlers[++i] = Number(handler);
27
+ return '$()';
28
+ }
29
+ else if (typeof parameter !== 'undefined') {
30
+ parameters[Number(parameter)] = ++i;
31
+ }
32
+
33
+ return '';
34
+ })
35
+
36
+ return [new RegExp(`^${regex}`), handlers, parameters];
37
+ }
38
+
39
+ insert(path: Path, index: number, pathErrorCheckOnly: boolean): ParameterMetadata {
40
+ let groups: [string, string][] = [],
41
+ parameters: ParameterMetadata = [];
42
+
43
+ for (let i = 0; ;) {
44
+ let replaced = false;
45
+
46
+ path = path.replace(/\{[^}]+\}/g, (m) => {
47
+ let mark = `@\\${i}`;
48
+
49
+ groups[i++] = [mark, m];
50
+ replaced = true;
51
+
52
+ return mark;
53
+ });
54
+
55
+ if (!replaced) {
56
+ break;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * - pattern (:label, :label{0-9]+}, ...)
62
+ * - /* wildcard
63
+ * - character
64
+ */
65
+ let tokens = path.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
66
+
67
+ for (let i = groups.length - 1; i >= 0; i--) {
68
+ let [ mark, replacement ] = groups[i],
69
+ token;
70
+
71
+ for (let j = tokens.length - 1; j >= 0; j--) {
72
+ token = tokens[j];
73
+
74
+ if (token.indexOf(mark) !== -1) {
75
+ token = token.replace(mark, replacement);
76
+ break
77
+ }
78
+ }
79
+ }
80
+
81
+ this.root.insert(tokens, index, parameters, this.context, pathErrorCheckOnly);
82
+
83
+ return parameters;
84
+ }
85
+ }
86
+
87
+
88
+ export { Trie };
@@ -0,0 +1,59 @@
1
+ type HandlerMap<T> = [T, number];
2
+
3
+ type HandlerMetadata<T> = [T, ParameterMap][];
4
+
5
+ type Indexes = number[];
6
+
7
+ type Matcher<T> = [RegExp, HandlerMetadata<T>[], StaticMap<T>];
8
+
9
+ type Method = string;
10
+
11
+ type ParameterMap = Record<string, number>;
12
+
13
+ type ParameterMetadata = [string, number][];
14
+
15
+ type Path = string;
16
+
17
+ /**
18
+ * The result can be in one of two formats:
19
+ * 1. An array of handlers with their corresponding parameter index maps, followed by a parameter stash.
20
+ * 2. An array of handlers with their corresponding parameter maps.
21
+ *
22
+ * Example:
23
+ *
24
+ * [[handler, paramIndexMap][], paramArray]
25
+ * ```typescript
26
+ * [
27
+ * [
28
+ * [middlewareA, {}], // '*'
29
+ * [funcA, {'id': 0}], // '/user/:id/*'
30
+ * [funcB, {'id': 0, 'action': 1}], // '/user/:id/:action'
31
+ * ],
32
+ * ['123', 'abc']
33
+ * ]
34
+ * ```
35
+ *
36
+ * [[handler, params][]]
37
+ * ```typescript
38
+ * [
39
+ * [
40
+ * [middlewareA, {}], // '*'
41
+ * [funcA, {'id': '123'}], // '/user/:id/*'
42
+ * [funcB, {'id': '123', 'action': 'abc'}], // '/user/:id/:action'
43
+ * ]
44
+ * ]
45
+ * ```
46
+ */
47
+ type Result<T> = [ [T, ParameterMap][], string[] ] | [ [T, Record<string, string>][] ];
48
+
49
+ type StaticMap<T> = Record<Path, Result<T>>;
50
+
51
+
52
+ export {
53
+ HandlerMap, HandlerMetadata,
54
+ Indexes,
55
+ Matcher, Method,
56
+ ParameterMap, ParameterMetadata, Path,
57
+ Result,
58
+ StaticMap
59
+ };