@esportsplus/routing 0.6.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.
@@ -0,0 +1,172 @@
1
+ import { ON_DELETE, ON_GET, ON_POST, ON_PUT, PACKAGE_NAME } from '../constants.js';
2
+ import { Node } from './node.js';
3
+ import pipeline from '@esportsplus/pipeline';
4
+ function key(method, subdomain) {
5
+ return (method + (subdomain ? ' ' + subdomain : '')).toUpperCase();
6
+ }
7
+ function normalize(path) {
8
+ if (path) {
9
+ if (path[0] !== '/') {
10
+ path = '/' + path;
11
+ }
12
+ if (path.length > 1 && path[path.length - 1] === '/') {
13
+ path = path.slice(0, -1);
14
+ }
15
+ }
16
+ return path || '/';
17
+ }
18
+ function set(route, options) {
19
+ let pipeline = route.pipeline;
20
+ for (let key in options) {
21
+ let value = options[key];
22
+ if (key === 'middleware') {
23
+ for (let i = 0, n = value.length; i < n; i++) {
24
+ pipeline.add(value[i]);
25
+ }
26
+ }
27
+ else if (key === 'responder') {
28
+ pipeline.add(value);
29
+ }
30
+ else {
31
+ route[key] = (route[key] || '') + value;
32
+ }
33
+ }
34
+ }
35
+ class Router {
36
+ bucket = {};
37
+ groups = [];
38
+ routes = {};
39
+ subdomains = null;
40
+ add(method, path, route) {
41
+ let bucket = this.bucket[key(method, route.subdomain)] ??= {
42
+ root: new Node(),
43
+ static: {}
44
+ };
45
+ if (path.indexOf(':') === -1) {
46
+ if (path in bucket.static) {
47
+ throw new Error(`${PACKAGE_NAME}: static path '${path}' is already in use`);
48
+ }
49
+ bucket.static[path] = route;
50
+ }
51
+ else {
52
+ bucket.root.add(path, route);
53
+ }
54
+ return this;
55
+ }
56
+ create(options) {
57
+ let groups = this.groups, route = {
58
+ name: null,
59
+ path: null,
60
+ pipeline: pipeline(),
61
+ subdomain: null
62
+ };
63
+ for (let i = 0, n = groups.length; i < n; i++) {
64
+ set(route, groups[i]);
65
+ }
66
+ set(route, options);
67
+ if (route.path) {
68
+ route.path = normalize(route.path);
69
+ }
70
+ if (route.subdomain === 'www') {
71
+ route.subdomain = '';
72
+ }
73
+ return route;
74
+ }
75
+ delete(options) {
76
+ this.on(ON_DELETE, options);
77
+ return this;
78
+ }
79
+ get(options) {
80
+ this.on(ON_GET, options);
81
+ return this;
82
+ }
83
+ group(options) {
84
+ return {
85
+ routes: (fn) => {
86
+ this.groups.push(options);
87
+ fn(this);
88
+ this.groups.pop();
89
+ return this;
90
+ }
91
+ };
92
+ }
93
+ match(method, path, subdomain) {
94
+ let bucket = this.bucket[key(method, subdomain)];
95
+ if (!bucket) {
96
+ return {};
97
+ }
98
+ path = normalize(path);
99
+ if (path in bucket.static) {
100
+ return { route: bucket.static[path] };
101
+ }
102
+ return bucket.root.find(path);
103
+ }
104
+ on(methods, options) {
105
+ let route = this.create(options);
106
+ let name = route.name, path = route.path, subdomain = route.subdomain;
107
+ if (name) {
108
+ if (this.routes[name]) {
109
+ throw new Error(`${PACKAGE_NAME}: '${name}' is already in use`);
110
+ }
111
+ this.routes[name] = route;
112
+ }
113
+ if (path) {
114
+ for (let i = 0, n = methods.length; i < n; i++) {
115
+ let method = methods[i];
116
+ if (path.indexOf('?:') !== -1) {
117
+ let segments = path.split('?:'), url = segments[0];
118
+ this.add(method, url, route);
119
+ for (let i = 1; i < segments.length; i++) {
120
+ url += '/:' + segments[i];
121
+ this.add(method, url, route);
122
+ }
123
+ }
124
+ else {
125
+ this.add(method, path, route);
126
+ }
127
+ }
128
+ }
129
+ if (subdomain) {
130
+ (this.subdomains ??= []).push(subdomain.toLowerCase());
131
+ }
132
+ return this;
133
+ }
134
+ post(options) {
135
+ this.on(ON_POST, options);
136
+ return this;
137
+ }
138
+ put(options) {
139
+ this.on(ON_PUT, options);
140
+ return this;
141
+ }
142
+ uri(name, values = []) {
143
+ let path = this.routes[name]?.path;
144
+ if (!path) {
145
+ throw new Error(`${PACKAGE_NAME}: route name '${name}' does not exist or it does not provide a path`);
146
+ }
147
+ let resolved = [], segments = path.split('/'), v = 0;
148
+ for (let i = 0, n = segments.length; i < n; i++) {
149
+ let segment = segments[i], symbol = segment[0];
150
+ if (symbol === ':') {
151
+ resolved.push(values[v++]);
152
+ }
153
+ else if (symbol === '?') {
154
+ if (values[v] === undefined) {
155
+ break;
156
+ }
157
+ resolved.push(values[v++]);
158
+ }
159
+ else if (symbol === '*') {
160
+ for (let n = values.length; v < n; v++) {
161
+ resolved.push(values[v]);
162
+ }
163
+ break;
164
+ }
165
+ else {
166
+ resolved.push(segment);
167
+ }
168
+ }
169
+ return resolved.join('/');
170
+ }
171
+ }
172
+ export { Router };
@@ -0,0 +1,18 @@
1
+ import { Route } from './index.js';
2
+ declare class Node<T> {
3
+ parent: Node<T> | null;
4
+ path: string | null;
5
+ route: Route<T> | null;
6
+ static: Map<string | number, Node<T>> | null;
7
+ type: number | null;
8
+ name: string | null;
9
+ parameter: Node<T> | null;
10
+ wildcard: Node<T> | null;
11
+ constructor(parent?: Node<T>['parent']);
12
+ add(path: string, route: Route<T>): Node<T>;
13
+ find(path: string): {
14
+ parameters?: Readonly<Record<PropertyKey, unknown>>;
15
+ route?: Readonly<Route<T>>;
16
+ };
17
+ }
18
+ export { Node };
@@ -0,0 +1,80 @@
1
+ import { PARAMETER, STATIC, WILDCARD } from '../constants.js';
2
+ class Node {
3
+ parent = null;
4
+ path = null;
5
+ route = null;
6
+ static = null;
7
+ type = null;
8
+ name = null;
9
+ parameter = null;
10
+ wildcard = null;
11
+ constructor(parent = null) {
12
+ this.parent = parent;
13
+ }
14
+ add(path, route) {
15
+ let node = this, segments = path.split('/'), type = STATIC, unnamed = 0;
16
+ for (let i = 0, n = segments.length; i < n; i++) {
17
+ let segment = segments[i], symbol = segment[0];
18
+ if (symbol === ':') {
19
+ if (!node.parameter) {
20
+ node.parameter = new Node(node);
21
+ node.parameter.name = (segment.slice(1) || unnamed++).toString();
22
+ }
23
+ node = node.parameter;
24
+ type = PARAMETER;
25
+ }
26
+ else if (symbol === '*') {
27
+ if (!node.wildcard) {
28
+ node.wildcard = new Node(node);
29
+ node.wildcard.name = (segment.slice(2) || unnamed++).toString();
30
+ }
31
+ node = node.wildcard;
32
+ type = WILDCARD;
33
+ }
34
+ else {
35
+ let next = node.static?.get(segment);
36
+ if (!next) {
37
+ next = new Node(node);
38
+ (node.static ??= new Map()).set(segment, next);
39
+ }
40
+ node = next;
41
+ }
42
+ }
43
+ node.path = path;
44
+ node.route = route;
45
+ node.type = type;
46
+ return node;
47
+ }
48
+ find(path) {
49
+ let node = this, parameters, segments = path.split('/'), wildcard;
50
+ for (let i = 0, n = segments.length; i < n; i++) {
51
+ let segment = segments[i];
52
+ if (node.wildcard) {
53
+ wildcard = {
54
+ node: node.wildcard,
55
+ start: i
56
+ };
57
+ }
58
+ let next = node.static?.get(segment);
59
+ if (next) {
60
+ node = next;
61
+ continue;
62
+ }
63
+ if (!node.parameter) {
64
+ node = undefined;
65
+ break;
66
+ }
67
+ node = node.parameter;
68
+ (parameters ??= {})[node.name] = segment;
69
+ }
70
+ if ((node === undefined || node.route === null) && wildcard) {
71
+ node = wildcard.node;
72
+ (parameters ??= {})[node.name] = segments.slice(wildcard.start).join('/');
73
+ }
74
+ return {
75
+ parameters,
76
+ route: node?.route || undefined
77
+ };
78
+ }
79
+ }
80
+ export { Node };
@@ -0,0 +1,54 @@
1
+ import { NeverAsync } from '@esportsplus/utilities';
2
+ import { Router } from './router/index.js';
3
+ import pipeline from '@esportsplus/pipeline';
4
+ type AccumulateRoutes<T extends readonly RouteFactory<any>[]> = T extends readonly [infer F extends RouteFactory<any>, ...infer Rest extends readonly RouteFactory<any>[]] ? ExtractRoutes<F> & AccumulateRoutes<Rest> : {};
5
+ type ExtractOptionalParams<Path extends string> = Path extends `/${infer Segment}/${infer Rest}` ? (Segment extends `?:${infer Param}` ? Param : never) | ExtractOptionalParams<`/${Rest}`> : Path extends `/${infer Segment}` ? (Segment extends `?:${infer Param}` ? Param : never) : never;
6
+ type ExtractParamsTuple<Path extends string> = Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? [Param, ...ExtractParamsTuple<`/${Rest}`>] : Path extends `${infer _Start}:${infer Param}` ? [Param] : [];
7
+ type ExtractRequiredParams<Path extends string> = Path extends `/${infer Segment}/${infer Rest}` ? (Segment extends `:${infer Param}` ? Param : never) | ExtractRequiredParams<`/${Rest}`> : Path extends `/${infer Segment}` ? (Segment extends `:${infer Param}` ? Param : never) : never;
8
+ type ExtractRoutes<F> = F extends (r: Router<any, any>) => Router<any, infer Routes extends RouteRegistry> ? Routes : never;
9
+ type InferOutput<F> = F extends RouteFactory<infer T> ? T : never;
10
+ type LabeledParamsTuple<Params extends string[]> = Params extends [infer _First extends string, ...infer Rest extends string[]] ? [_First: string | number, ...LabeledParamsTuple<Rest>] : [];
11
+ type Middleware<T> = NeverAsync<(input: Request<T>, next: Next<T>) => T>;
12
+ type Name = string;
13
+ type Next<T> = NeverAsync<(input: Request<T>) => T>;
14
+ type Options<T> = {
15
+ middleware?: Middleware<T>[];
16
+ name?: string;
17
+ path?: string;
18
+ subdomain?: string;
19
+ };
20
+ type PathParamsObject<Path extends string> = {
21
+ [K in ExtractRequiredParams<Path>]: string | number;
22
+ } & {
23
+ [K in ExtractOptionalParams<Path>]?: string | number;
24
+ };
25
+ type PathParamsTuple<Path extends string> = LabeledParamsTuple<ExtractParamsTuple<Path>>;
26
+ type Request<T> = {
27
+ data: Record<PropertyKey, unknown> & ReturnType<Router<T>['match']>;
28
+ hostname: string;
29
+ href: string;
30
+ method: string;
31
+ origin: string;
32
+ path: string;
33
+ port: string;
34
+ protocol: string;
35
+ query: Record<string, unknown>;
36
+ subdomain?: string;
37
+ };
38
+ type Route<T> = {
39
+ name: Name | null;
40
+ path: string | null;
41
+ pipeline: ReturnType<typeof pipeline<Request<T>, T>>;
42
+ subdomain: string | null;
43
+ };
44
+ type RouteFactory<T> = (router: Router<T, any>) => Router<T, RouteRegistry>;
45
+ type RouteOptions<T> = Options<T> & {
46
+ responder: Next<T>;
47
+ };
48
+ type RoutePath<Routes, K extends keyof Routes> = Routes[K] extends {
49
+ path: infer P extends string;
50
+ } ? P : string;
51
+ type RouteRegistry = Record<string, {
52
+ path: string;
53
+ }>;
54
+ export type { AccumulateRoutes, ExtractOptionalParams, ExtractRequiredParams, InferOutput, Middleware, Name, Next, Options, PathParamsObject, PathParamsTuple, Request, Router, Route, RouteFactory, RouteOptions, RoutePath, RouteRegistry };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ declare const PACKAGE_NAME = "@esportsplus/routing";
2
+ export { PACKAGE_NAME };
@@ -0,0 +1,2 @@
1
+ const PACKAGE_NAME = '@esportsplus/routing';
2
+ export { PACKAGE_NAME };
@@ -0,0 +1 @@
1
+ export {};
package/build/index.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "author": "ICJR",
3
+ "dependencies": {
4
+ "@esportsplus/pipeline": "^1.2.2",
5
+ "@esportsplus/reactivity": "file:/../reactivity",
6
+ "@esportsplus/typescript": "file:/../typescript",
7
+ "@esportsplus/utilities": "^0.27.2"
8
+ },
9
+ "exports": {
10
+ "./client": {
11
+ "types": "./build/client/index.d.ts",
12
+ "default": "./build/client/index.js"
13
+ },
14
+ "./server": {
15
+ "types": "./build/server/index.d.ts",
16
+ "default": "./build/server/index.js"
17
+ },
18
+ ".": {
19
+ "types": "./build/index.d.ts",
20
+ "default": "./build/index.js"
21
+ }
22
+ },
23
+ "main": "./build/index.js",
24
+ "name": "@esportsplus/routing",
25
+ "private": false,
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/esportsplus/routing"
29
+ },
30
+ "type": "module",
31
+ "types": "./build/index.d.ts",
32
+ "version": "0.6.0",
33
+ "scripts": {
34
+ "build": "tsc",
35
+ "build:test": "vite build --config test/vite.config.ts",
36
+ "-": "-"
37
+ }
38
+ }
@@ -0,0 +1,22 @@
1
+ const ON_DELETE = ['DELETE'];
2
+
3
+ const ON_GET = ['GET'];
4
+
5
+ const ON_POST = ['POST'];
6
+
7
+ const ON_PUT = ['PUT'];
8
+
9
+ const PARAMETER = 0;
10
+
11
+ const STATIC = 1;
12
+
13
+ const WILDCARD = 2;
14
+
15
+
16
+ export {
17
+ ON_DELETE, ON_GET, ON_POST, ON_PUT,
18
+ PARAMETER,
19
+ STATIC,
20
+ WILDCARD
21
+ };
22
+ export { PACKAGE_NAME } from '~/constants';
@@ -0,0 +1,195 @@
1
+ import { effect, reactive, root } from '@esportsplus/reactivity';
2
+ import { AccumulateRoutes, ExtractOptionalParams, ExtractRequiredParams, InferOutput, Middleware, Next, PathParamsObject, Request, Route, RouteFactory, RoutePath } from './types';
3
+ import { Router } from './router';
4
+ import pipeline from '@esportsplus/pipeline';
5
+ import { PACKAGE_NAME } from './constants';
6
+
7
+
8
+ let cache: Request<any>[] = [],
9
+ location = window.location;
10
+
11
+
12
+ function back() {
13
+ window.history.back();
14
+ }
15
+
16
+ function forward() {
17
+ window.history.forward();
18
+ }
19
+
20
+ function href<T>() {
21
+ let hash = location.hash || '#/',
22
+ path = hash ? hash.slice(1).split('?') : ['/', ''],
23
+ request = {
24
+ hostname: location.hostname,
25
+ href: location.href,
26
+ method: 'GET',
27
+ origin: location.origin,
28
+ path: path[0],
29
+ port: location.port,
30
+ protocol: location.protocol,
31
+ query: {} as Record<PropertyKey, unknown>
32
+ };
33
+
34
+ if (path[1]) {
35
+ let params = new URLSearchParams(path[1]),
36
+ query = request.query;
37
+
38
+ for (let [key, value] of params.entries()) {
39
+ query[key] = value;
40
+ }
41
+ }
42
+
43
+ return request as Request<T>;
44
+ }
45
+
46
+ function match<T>(request: Request<T>, router: Router<T>, subdomain?: string) {
47
+ if (router.subdomains !== null) {
48
+ let hostname = request.hostname,
49
+ subdomains = router.subdomains;
50
+
51
+ for (let i = 0, n = subdomains.length; i < n; i++) {
52
+ if (!hostname.startsWith(subdomains[i])) {
53
+ continue;
54
+ }
55
+
56
+ subdomain = subdomains[i];
57
+ break;
58
+ }
59
+ }
60
+
61
+ return router.match(request.method, request.path, subdomain || '');
62
+ }
63
+
64
+ function middleware<T>(request: Request<T>, router: Router<T>) {
65
+ let middleware = pipeline<Request<T>, T>();
66
+
67
+ function host(...stages: Middleware<T>[]) {
68
+ for (let i = 0, n = stages.length; i < n; i++) {
69
+ middleware.add( stages[i] );
70
+ }
71
+
72
+ return middleware.dispatch(request) as T;
73
+ };
74
+
75
+ host.dispatch = (request: Request<T>) => {
76
+ let { route } = request.data;
77
+
78
+ if (route === undefined) {
79
+ throw new Error(`${PACKAGE_NAME}: route is undefined!`);
80
+ }
81
+
82
+ return route.pipeline.dispatch(request);
83
+ };
84
+
85
+ host.match = (fallback: Route<T>) => {
86
+ let state = reactive<ReturnType<typeof router.match>>({
87
+ parameters: undefined,
88
+ route: undefined
89
+ });
90
+
91
+ if (fallback === undefined) {
92
+ throw new Error(`${PACKAGE_NAME}: fallback route does not exist`);
93
+ }
94
+
95
+ effect(() => {
96
+ let { parameters, route } = match(request, router);
97
+
98
+ state.parameters = parameters;
99
+ state.route = route || fallback;
100
+ });
101
+
102
+ return (request: Request<T>, next: Next<T>) => {
103
+ if (state.route === undefined) {
104
+ throw new Error(`${PACKAGE_NAME}: route is undefined`);
105
+ }
106
+
107
+ return root(() => {
108
+ request.data = {
109
+ parameters: state.parameters,
110
+ route: state.route
111
+ };
112
+
113
+ return next(request);
114
+ });
115
+ };
116
+ };
117
+
118
+ return host;
119
+ }
120
+
121
+ function normalize(uri: string) {
122
+ if (uri[0] === '/') {
123
+ return '#' + uri;
124
+ }
125
+
126
+ return uri;
127
+ }
128
+
129
+ function onpopstate() {
130
+ let values = href();
131
+
132
+ for (let i = 0, n = cache.length; i < n; i++) {
133
+ let state = cache[i];
134
+
135
+ for (let key in values) {
136
+ // @ts-ignore
137
+ state[key] = values[key];
138
+ }
139
+ }
140
+ }
141
+
142
+
143
+ const router = <const Factories extends readonly RouteFactory<any>[]>(...factories: Factories) => {
144
+ type Routes = AccumulateRoutes<Factories>;
145
+ type T = InferOutput<Factories[number]>;
146
+
147
+ let instance = factories.reduce(
148
+ (r, factory) => factory(r),
149
+ new Router<T, {}>() as Router<T, any>
150
+ ) as Router<T, Routes>,
151
+ request = reactive<Request<T>>(Object.assign(href<T>(), { data: {} } as any));
152
+
153
+ if (cache.push(request) === 1) {
154
+ window.addEventListener('hashchange', onpopstate);
155
+ }
156
+
157
+ return {
158
+ back,
159
+ forward,
160
+ middleware: middleware(request, instance as Router<T>),
161
+ redirect: <RouteName extends keyof Routes>(
162
+ name: RouteName,
163
+ ...values: ExtractRequiredParams<RoutePath<Routes, RouteName>> extends never
164
+ ? ExtractOptionalParams<RoutePath<Routes, RouteName>> extends never
165
+ ? []
166
+ : [params?: PathParamsObject<RoutePath<Routes, RouteName>>]
167
+ : [params: PathParamsObject<RoutePath<Routes, RouteName>>]
168
+ ) => {
169
+ if ((name as string).indexOf('://') !== -1) {
170
+ return window.location.replace(name as any);
171
+ }
172
+
173
+ window.location.hash = normalize(instance.uri(name as any, values as any));
174
+ },
175
+ routes: instance.routes,
176
+ uri: <RouteName extends keyof Routes>(
177
+ name: RouteName,
178
+ ...values: ExtractRequiredParams<RoutePath<Routes, RouteName>> extends never
179
+ ? ExtractOptionalParams<RoutePath<Routes, RouteName>> extends never
180
+ ? []
181
+ : [params?: PathParamsObject<RoutePath<Routes, RouteName>>]
182
+ : [params: PathParamsObject<RoutePath<Routes, RouteName>>]
183
+ ) => {
184
+ return normalize(instance.uri(name as any, values as any));
185
+ }
186
+ };
187
+ };
188
+
189
+
190
+ export { router };
191
+ export type {
192
+ Middleware,
193
+ Next,
194
+ Request, Route, RouteFactory
195
+ } from './types';