@visulima/connect 1.0.0 → 1.0.1
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 +7 -0
- package/README.md +31 -10
- package/package.json +4 -2
- package/src/adapter/express.ts +18 -0
- package/src/adapter/with-zod.ts +35 -0
- package/src/edge.ts +165 -0
- package/src/index.ts +18 -0
- package/src/node.ts +173 -0
- package/src/regexparam.d.ts +10 -0
- package/src/router.ts +195 -0
- package/src/types.d.ts +36 -0
- package/src/utils/send-json.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## @visulima/connect [1.0.1](https://github.com/visulima/visulima/compare/@visulima/connect@1.0.0...@visulima/connect@1.0.1) (2022-10-27)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* fixed package.json files paths ([0d21e94](https://github.com/visulima/visulima/commit/0d21e94a75e9518f7b87293706615d8fb280095c))
|
|
7
|
+
|
|
1
8
|
## @visulima/connect 1.0.0 (2022-10-25)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -15,14 +15,22 @@
|
|
|
15
15
|
|
|
16
16
|
<div align="center">
|
|
17
17
|
|
|
18
|
-
[![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url]
|
|
18
|
+
[![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url]
|
|
19
19
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
+
---
|
|
23
|
+
|
|
22
24
|
<div align="center">
|
|
23
|
-
|
|
25
|
+
<p>
|
|
26
|
+
<sup>
|
|
27
|
+
Daniel Bannert's open source work is supported by the community on <a href="https://github.com/sponsors/prisis">GitHub Sponsors</a>
|
|
28
|
+
</sup>
|
|
29
|
+
</p>
|
|
24
30
|
</div>
|
|
25
31
|
|
|
32
|
+
---
|
|
33
|
+
|
|
26
34
|
## Features
|
|
27
35
|
|
|
28
36
|
- Async middleware
|
|
@@ -44,6 +52,7 @@ yarn add @visulima/connect
|
|
|
44
52
|
```sh
|
|
45
53
|
pnpm add @visulima/connect
|
|
46
54
|
```
|
|
55
|
+
|
|
47
56
|
## Usage
|
|
48
57
|
|
|
49
58
|
> **Note**
|
|
@@ -198,7 +207,7 @@ router
|
|
|
198
207
|
res.json({ user });
|
|
199
208
|
return new Response(JSON.stringify({ user }), {
|
|
200
209
|
status: 200,
|
|
201
|
-
|
|
210
|
+
headerList: {
|
|
202
211
|
"content-type": "application/json",
|
|
203
212
|
},
|
|
204
213
|
});
|
|
@@ -207,7 +216,7 @@ router
|
|
|
207
216
|
const user = await updateUser(req.body.user);
|
|
208
217
|
return new Response(JSON.stringify({ user }), {
|
|
209
218
|
status: 200,
|
|
210
|
-
|
|
219
|
+
headerList: {
|
|
211
220
|
"content-type": "application/json",
|
|
212
221
|
},
|
|
213
222
|
});
|
|
@@ -589,19 +598,31 @@ router.use(expressWrapper(someExpressMiddleware));
|
|
|
589
598
|
|
|
590
599
|
</details>
|
|
591
600
|
|
|
601
|
+
## Supported Node.js Versions
|
|
602
|
+
|
|
603
|
+
Libraries in this ecosystem make the best effort to track
|
|
604
|
+
[Node.js' release schedule](https://nodejs.org/en/about/releases/). Here's [a
|
|
605
|
+
post on why we think this is important](https://medium.com/the-node-js-collection/maintainers-should-consider-following-node-js-release-schedule-ab08ed4de71a).
|
|
606
|
+
|
|
592
607
|
## Contributing
|
|
593
608
|
|
|
594
|
-
|
|
609
|
+
If you would like to help take a look at the [list of issues](https://github.com/visulima/visulima/issues) and check our [Contributing](.github/CONTRIBUTING.md) guild.
|
|
610
|
+
|
|
611
|
+
> **Note:** please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
|
|
612
|
+
|
|
613
|
+
## Credits
|
|
614
|
+
|
|
615
|
+
- [next-connect](https://github.com/hoangvvo/next-connect)
|
|
616
|
+
- [Daniel Bannert](https://github.com/prisis)
|
|
617
|
+
- [All Contributors](https://github.com/visulima/visulima/graphs/contributors)
|
|
595
618
|
|
|
596
619
|
## License
|
|
597
620
|
|
|
598
|
-
[MIT][license-url]
|
|
621
|
+
The visulima connect is open-sourced software licensed under the [MIT][license-url]
|
|
599
622
|
|
|
600
623
|
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
|
|
601
624
|
[typescript-url]: "typescript"
|
|
602
625
|
[license-image]: https://img.shields.io/npm/l/@visulima/connect?color=blueviolet&style=for-the-badge
|
|
603
626
|
[license-url]: LICENSE.md "license"
|
|
604
|
-
[npm-image]: https://img.shields.io/npm/v/@visulima/connect/
|
|
605
|
-
[npm-url]: https://www.npmjs.com/package/@visulima/connect/v/
|
|
606
|
-
[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/visulima/connect?label=Synk%20Vulnerabilities&style=for-the-badge
|
|
607
|
-
[synk-url]: https://snyk.io/test/github/visulima/connect?targetFile=package.json "synk"
|
|
627
|
+
[npm-image]: https://img.shields.io/npm/v/@visulima/connect/latest.svg?style=for-the-badge&logo=npm
|
|
628
|
+
[npm-url]: https://www.npmjs.com/package/@visulima/connect/v/latest "npm"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visulima/connect",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "The minimal router and middleware layer for Next.js, Micro, Vercel, or Node.js http/http2 with support for zod validation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"javascript",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"source": "src/index.ts",
|
|
53
53
|
"types": "dist/index.d.ts",
|
|
54
54
|
"files": [
|
|
55
|
-
"
|
|
55
|
+
"src",
|
|
56
|
+
"dist",
|
|
56
57
|
"README.md",
|
|
57
58
|
"CHANGELOG.md",
|
|
58
59
|
"LICENSE.md"
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
88
89
|
"eslint-plugin-import": "^2.26.0",
|
|
89
90
|
"eslint-plugin-json": "^3.1.0",
|
|
91
|
+
"eslint-plugin-jsonc": "^2.5.0",
|
|
90
92
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
|
91
93
|
"eslint-plugin-markdown": "^3.0.0",
|
|
92
94
|
"eslint-plugin-material-ui": "^1.0.1",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
import type { RequestHandler } from "../node";
|
|
4
|
+
import type { Nextable } from "../types";
|
|
5
|
+
|
|
6
|
+
type NextFunction = (error?: any) => void;
|
|
7
|
+
|
|
8
|
+
const expressWrapper = <Request extends IncomingMessage, Response extends ServerResponse>(
|
|
9
|
+
function_: ExpressRequestHandler<Request, Response>,
|
|
10
|
+
// eslint-disable-next-line compat/compat
|
|
11
|
+
): Nextable<RequestHandler<Request, Response>> => (request, response, next) => new Promise<void>((resolve, reject) => {
|
|
12
|
+
function_(request, response, (error) => (error ? reject(error) : resolve()));
|
|
13
|
+
// eslint-disable-next-line promise/no-callback-in-promise
|
|
14
|
+
}).then(next);
|
|
15
|
+
|
|
16
|
+
export type ExpressRequestHandler<Request, Response> = (request: Request, response: Response, next: NextFunction) => void;
|
|
17
|
+
|
|
18
|
+
export default expressWrapper;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import createHttpError from "http-errors";
|
|
2
|
+
import type { AnyZodObject } from "zod";
|
|
3
|
+
import { ZodError, ZodObject } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { Nextable, NextHandler } from "../types";
|
|
6
|
+
|
|
7
|
+
const withZod = <
|
|
8
|
+
Request extends object,
|
|
9
|
+
Response extends unknown,
|
|
10
|
+
Handler extends Nextable<any>,
|
|
11
|
+
Schema extends ZodObject<{ body?: AnyZodObject; headers?: AnyZodObject; query?: AnyZodObject }>,
|
|
12
|
+
>(
|
|
13
|
+
schema: Schema,
|
|
14
|
+
handler: Handler,
|
|
15
|
+
): ((request: Request, response: Response, next: NextHandler) => Promise<Response>) => async (request: Request, response: Response, next) => {
|
|
16
|
+
let transformedRequest: Request = request;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
transformedRequest = (await schema.parseAsync(request)) as Request;
|
|
20
|
+
} catch (error: any) {
|
|
21
|
+
let { message } = error;
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
24
|
+
if (error instanceof ZodError && typeof error.format === "function") {
|
|
25
|
+
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
26
|
+
message = error.issues.map((issue) => `${issue.path.join("/")} - ${issue.message}`).join("/n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw createHttpError(422, message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return handler(transformedRequest, response, next);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default withZod;
|
package/src/edge.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { AnyZodObject } from "zod";
|
|
2
|
+
import { ZodObject } from "zod";
|
|
3
|
+
|
|
4
|
+
import withZod from "./adapter/with-zod";
|
|
5
|
+
import { Route, Router } from "./router";
|
|
6
|
+
import type {
|
|
7
|
+
FindResult,
|
|
8
|
+
FunctionLike,
|
|
9
|
+
HandlerOptions,
|
|
10
|
+
HttpMethod,
|
|
11
|
+
Nextable,
|
|
12
|
+
RouteMatch,
|
|
13
|
+
RoutesExtendedRequestHandler,
|
|
14
|
+
RouteShortcutMethod,
|
|
15
|
+
ValueOrPromise,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line max-len
|
|
19
|
+
const onNoMatch = async (request: Request) => new Response(request.method !== "HEAD" ? `Route ${request.method} ${request.url} not found` : null, { status: 404 });
|
|
20
|
+
|
|
21
|
+
const onError = async (error: unknown) => {
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.error(error);
|
|
24
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function getPathname(request: Request & { nextUrl?: URL }) {
|
|
28
|
+
// eslint-disable-next-line compat/compat
|
|
29
|
+
return (request.nextUrl || new URL(request.url)).pathname;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line max-len
|
|
33
|
+
export type RequestHandler<R extends Request, Context> = (request: R, context_: Context) => ValueOrPromise<Response | void>;
|
|
34
|
+
|
|
35
|
+
export class EdgeRouter<R extends Request = Request, Context = unknown, RResponse extends Response = Response, Schema extends AnyZodObject = ZodObject<any>> {
|
|
36
|
+
private router = new Router<RequestHandler<R, Context>>();
|
|
37
|
+
|
|
38
|
+
private readonly onNoMatch: RoutesExtendedRequestHandler<R, Context, RResponse, Route<Nextable<FunctionLike>>[]>;
|
|
39
|
+
|
|
40
|
+
private readonly onError: (
|
|
41
|
+
error: unknown,
|
|
42
|
+
...arguments_: Parameters<RoutesExtendedRequestHandler<R, Context, RResponse, Route<Nextable<FunctionLike>>[]>>
|
|
43
|
+
) => ReturnType<RoutesExtendedRequestHandler<R, Context, RResponse, Route<Nextable<FunctionLike>>[]>>;
|
|
44
|
+
|
|
45
|
+
constructor(options: HandlerOptions<RoutesExtendedRequestHandler<R, Context, RResponse, Route<Nextable<FunctionLike>>[]>> = {}) {
|
|
46
|
+
this.onNoMatch = options.onNoMatch || onNoMatch as unknown as RoutesExtendedRequestHandler<R, Context, RResponse, Route<Nextable<FunctionLike>>[]>;
|
|
47
|
+
this.onError = options.onError
|
|
48
|
+
|| (onError as unknown as (
|
|
49
|
+
error: unknown,
|
|
50
|
+
...arguments_: Parameters<RoutesExtendedRequestHandler<R, Context, RResponse, Route<Nextable<FunctionLike>>[]>>
|
|
51
|
+
) => ReturnType<RoutesExtendedRequestHandler<R, Context, RResponse, Route<Nextable<FunctionLike>>[]>>);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private add(
|
|
55
|
+
method: HttpMethod | "",
|
|
56
|
+
routeOrFunction: RouteMatch | Nextable<RequestHandler<R, Context>>,
|
|
57
|
+
zodOrRouteOrFunction?: RouteMatch | Schema | Nextable<RequestHandler<R, Context>>,
|
|
58
|
+
...fns: Nextable<RequestHandler<R, Context>>[]
|
|
59
|
+
) {
|
|
60
|
+
if (typeof routeOrFunction === "string" && typeof zodOrRouteOrFunction === "function") {
|
|
61
|
+
// eslint-disable-next-line no-param-reassign
|
|
62
|
+
fns = [zodOrRouteOrFunction];
|
|
63
|
+
} else if (typeof zodOrRouteOrFunction === "object") {
|
|
64
|
+
// eslint-disable-next-line unicorn/prefer-ternary
|
|
65
|
+
if (typeof routeOrFunction === "function") {
|
|
66
|
+
// eslint-disable-next-line no-param-reassign
|
|
67
|
+
fns = [withZod<R, Context, Nextable<RequestHandler<R, Context>>, Schema>(zodOrRouteOrFunction as Schema, routeOrFunction)];
|
|
68
|
+
} else {
|
|
69
|
+
// eslint-disable-next-line no-param-reassign,max-len
|
|
70
|
+
fns = fns.map((function_) => withZod<R, Context, Nextable<RequestHandler<R, Context>>, Schema>(zodOrRouteOrFunction as Schema, function_));
|
|
71
|
+
}
|
|
72
|
+
} else if (typeof zodOrRouteOrFunction === "function") {
|
|
73
|
+
// eslint-disable-next-line no-param-reassign
|
|
74
|
+
fns = [zodOrRouteOrFunction];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.router.add(method, routeOrFunction, ...fns);
|
|
78
|
+
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public all: RouteShortcutMethod<this, Schema, RequestHandler<R, Context>> = this.add.bind(this, "");
|
|
83
|
+
|
|
84
|
+
public get: RouteShortcutMethod<this, Schema, RequestHandler<R, Context>> = this.add.bind(this, "GET");
|
|
85
|
+
|
|
86
|
+
public head: RouteShortcutMethod<this, Schema, RequestHandler<R, Context>> = this.add.bind(this, "HEAD");
|
|
87
|
+
|
|
88
|
+
public post: RouteShortcutMethod<this, Schema, RequestHandler<R, Context>> = this.add.bind(this, "POST");
|
|
89
|
+
|
|
90
|
+
public put: RouteShortcutMethod<this, Schema, RequestHandler<R, Context>> = this.add.bind(this, "PUT");
|
|
91
|
+
|
|
92
|
+
public patch: RouteShortcutMethod<this, Schema, RequestHandler<R, Context>> = this.add.bind(this, "PATCH");
|
|
93
|
+
|
|
94
|
+
public delete: RouteShortcutMethod<this, Schema, RequestHandler<R, Context>> = this.add.bind(this, "DELETE");
|
|
95
|
+
|
|
96
|
+
public use(
|
|
97
|
+
base: RouteMatch | Nextable<RequestHandler<R, Context>> | EdgeRouter<R, Context>,
|
|
98
|
+
...fns: (Nextable<RequestHandler<R, Context>> | EdgeRouter<R, Context>)[]
|
|
99
|
+
) {
|
|
100
|
+
if (typeof base === "function" || base instanceof EdgeRouter) {
|
|
101
|
+
fns.unshift(base);
|
|
102
|
+
// eslint-disable-next-line no-param-reassign
|
|
103
|
+
base = "/";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.router.use(base, ...fns.map((function_) => (function_ instanceof EdgeRouter ? function_.router : function_)));
|
|
107
|
+
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// eslint-disable-next-line class-methods-use-this
|
|
112
|
+
private prepareRequest(request: R & { params?: Record<string, unknown> }, findResult: FindResult<RequestHandler<R, Context>>) {
|
|
113
|
+
request.params = {
|
|
114
|
+
...findResult.params,
|
|
115
|
+
...request.params, // original params will take precedence
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public clone() {
|
|
120
|
+
const r = new EdgeRouter<R, Context, RResponse, Schema>({ onNoMatch: this.onNoMatch, onError: this.onError });
|
|
121
|
+
|
|
122
|
+
r.router = this.router.clone();
|
|
123
|
+
|
|
124
|
+
return r;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async run(request: R, context_: Context) {
|
|
128
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference,unicorn/no-array-method-this-argument
|
|
129
|
+
const result = this.router.find(request.method as HttpMethod, getPathname(request));
|
|
130
|
+
|
|
131
|
+
if (result.fns.length === 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.prepareRequest(request, result);
|
|
136
|
+
|
|
137
|
+
// eslint-disable-next-line consistent-return
|
|
138
|
+
return Router.exec(result.fns, request, context_);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
handler() {
|
|
142
|
+
const { routes } = this.router as Router<FunctionLike>;
|
|
143
|
+
|
|
144
|
+
return async (request: R, context_: Context): Promise<any> => {
|
|
145
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference,unicorn/no-array-method-this-argument
|
|
146
|
+
const result = this.router.find(request.method as HttpMethod, getPathname(request));
|
|
147
|
+
|
|
148
|
+
this.prepareRequest(request, result);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
return await (result.fns.length === 0 || result.middleOnly
|
|
152
|
+
? this.onNoMatch(request, context_, routes)
|
|
153
|
+
: Router.exec(result.fns, request, context_));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return this.onError(error, request, context_, routes);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createEdgeRouter<R extends Request, Context>(
|
|
162
|
+
options: HandlerOptions<RoutesExtendedRequestHandler<R, Context, Response, Route<Nextable<FunctionLike>>[]>> = {},
|
|
163
|
+
) {
|
|
164
|
+
return new EdgeRouter<R, Context, Response>(options);
|
|
165
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type { RequestHandler as EdgeRequestHandler } from "./edge";
|
|
2
|
+
export { createEdgeRouter, EdgeRouter } from "./edge";
|
|
3
|
+
export type { ExpressRequestHandler } from "./adapter/express";
|
|
4
|
+
export { default as expressWrapper } from "./adapter/express";
|
|
5
|
+
|
|
6
|
+
export type { RequestHandler } from "./node";
|
|
7
|
+
export { createRouter, NodeRouter } from "./node";
|
|
8
|
+
|
|
9
|
+
export type { Route } from "./router";
|
|
10
|
+
export { Router } from "./router";
|
|
11
|
+
|
|
12
|
+
export { default as withZod } from "./adapter/with-zod";
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
HandlerOptions, NextHandler, FunctionLike, Nextable, ValueOrPromise, FindResult, RouteShortcutMethod, HttpMethod,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
export { default as sendJson } from "./utils/send-json";
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { AnyZodObject } from "zod";
|
|
3
|
+
import { ZodObject } from "zod";
|
|
4
|
+
|
|
5
|
+
import withZod from "./adapter/with-zod";
|
|
6
|
+
import type { Route } from "./router";
|
|
7
|
+
import { Router } from "./router";
|
|
8
|
+
import type {
|
|
9
|
+
FindResult,
|
|
10
|
+
FunctionLike,
|
|
11
|
+
HandlerOptions,
|
|
12
|
+
HttpMethod,
|
|
13
|
+
Nextable,
|
|
14
|
+
RouteMatch,
|
|
15
|
+
RoutesExtendedRequestHandler,
|
|
16
|
+
RouteShortcutMethod,
|
|
17
|
+
ValueOrPromise,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
const onNoMatch = async (request: IncomingMessage, response: ServerResponse) => {
|
|
21
|
+
response.statusCode = 404;
|
|
22
|
+
response.end(request.method !== "HEAD" ? `Route ${request.method} ${request.url} not found` : undefined);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const onError = async (error: unknown, _request: IncomingMessage, response: ServerResponse) => {
|
|
26
|
+
response.statusCode = 500;
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.error(error);
|
|
29
|
+
|
|
30
|
+
response.end("Internal Server Error");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function getPathname(url: string) {
|
|
34
|
+
const queryIndex = url.indexOf("?");
|
|
35
|
+
|
|
36
|
+
return queryIndex !== -1 ? url.slice(0, Math.max(0, queryIndex)) : url;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type RequestHandler<Request extends IncomingMessage, Response extends ServerResponse> = (request: Request, response: Response) => ValueOrPromise<void>;
|
|
40
|
+
|
|
41
|
+
export class NodeRouter<
|
|
42
|
+
Request extends IncomingMessage = IncomingMessage,
|
|
43
|
+
Response extends ServerResponse = ServerResponse,
|
|
44
|
+
Schema extends AnyZodObject = ZodObject<any>,
|
|
45
|
+
> {
|
|
46
|
+
private router = new Router<RequestHandler<Request, Response>>();
|
|
47
|
+
|
|
48
|
+
private readonly onNoMatch: RoutesExtendedRequestHandler<Request, Response, Response, Route<Nextable<FunctionLike>>[]>;
|
|
49
|
+
|
|
50
|
+
private readonly onError: (
|
|
51
|
+
error: unknown,
|
|
52
|
+
...arguments_: Parameters<RoutesExtendedRequestHandler<Request, Response, Response, Route<Nextable<FunctionLike>>[]>>
|
|
53
|
+
) => ReturnType<RoutesExtendedRequestHandler<Request, Response, Response, Route<Nextable<FunctionLike>>[]>>;
|
|
54
|
+
|
|
55
|
+
constructor(options: HandlerOptions<RoutesExtendedRequestHandler<Request, Response, Response, Route<Nextable<FunctionLike>>[]>> = {}) {
|
|
56
|
+
this.onNoMatch = options.onNoMatch || onNoMatch;
|
|
57
|
+
this.onError = options.onError || onError;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private add(
|
|
61
|
+
method: HttpMethod | "",
|
|
62
|
+
routeOrFunction: RouteMatch | Nextable<RequestHandler<Request, Response>>,
|
|
63
|
+
zodOrRouteOrFunction?: RouteMatch | Schema | Nextable<RequestHandler<Request, Response>>,
|
|
64
|
+
...fns: Nextable<RequestHandler<Request, Response>>[]
|
|
65
|
+
) {
|
|
66
|
+
if (typeof routeOrFunction === "string" && typeof zodOrRouteOrFunction === "function") {
|
|
67
|
+
// eslint-disable-next-line no-param-reassign
|
|
68
|
+
fns = [zodOrRouteOrFunction];
|
|
69
|
+
} else if (typeof zodOrRouteOrFunction === "object") {
|
|
70
|
+
// eslint-disable-next-line unicorn/prefer-ternary
|
|
71
|
+
if (typeof routeOrFunction === "function") {
|
|
72
|
+
// eslint-disable-next-line no-param-reassign
|
|
73
|
+
fns = [withZod<Request, Response, Nextable<RequestHandler<Request, Response>>, Schema>(
|
|
74
|
+
zodOrRouteOrFunction as Schema,
|
|
75
|
+
routeOrFunction,
|
|
76
|
+
)];
|
|
77
|
+
} else {
|
|
78
|
+
// eslint-disable-next-line no-param-reassign,max-len
|
|
79
|
+
fns = fns.map((function_) => withZod<Request, Response, Nextable<RequestHandler<Request, Response>>, Schema>(zodOrRouteOrFunction as Schema, function_));
|
|
80
|
+
}
|
|
81
|
+
} else if (typeof zodOrRouteOrFunction === "function") {
|
|
82
|
+
// eslint-disable-next-line no-param-reassign
|
|
83
|
+
fns = [zodOrRouteOrFunction];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.router.add(method, routeOrFunction, ...fns);
|
|
87
|
+
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public all: RouteShortcutMethod<this, Schema, RequestHandler<Request, Response>> = this.add.bind(this, "");
|
|
92
|
+
|
|
93
|
+
public get: RouteShortcutMethod<this, Schema, RequestHandler<Request, Response>> = this.add.bind(this, "GET");
|
|
94
|
+
|
|
95
|
+
public head: RouteShortcutMethod<this, Schema, RequestHandler<Request, Response>> = this.add.bind(this, "HEAD");
|
|
96
|
+
|
|
97
|
+
public post: RouteShortcutMethod<this, Schema, RequestHandler<Request, Response>> = this.add.bind(this, "POST");
|
|
98
|
+
|
|
99
|
+
public put: RouteShortcutMethod<this, Schema, RequestHandler<Request, Response>> = this.add.bind(this, "PUT");
|
|
100
|
+
|
|
101
|
+
public patch: RouteShortcutMethod<this, Schema, RequestHandler<Request, Response>> = this.add.bind(this, "PATCH");
|
|
102
|
+
|
|
103
|
+
public delete: RouteShortcutMethod<this, Schema, RequestHandler<Request, Response>> = this.add.bind(this, "DELETE");
|
|
104
|
+
|
|
105
|
+
public use(
|
|
106
|
+
base: RouteMatch | Nextable<RequestHandler<Request, Response>> | NodeRouter<Request, Response, Schema>,
|
|
107
|
+
...fns: (Nextable<RequestHandler<Request, Response>> | NodeRouter<Request, Response, Schema>)[]
|
|
108
|
+
) {
|
|
109
|
+
if (typeof base === "function" || base instanceof NodeRouter) {
|
|
110
|
+
fns.unshift(base);
|
|
111
|
+
// eslint-disable-next-line no-param-reassign
|
|
112
|
+
base = "/";
|
|
113
|
+
}
|
|
114
|
+
this.router.use(base, ...fns.map((function_) => (function_ instanceof NodeRouter ? function_.router : function_)));
|
|
115
|
+
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// eslint-disable-next-line class-methods-use-this
|
|
120
|
+
private prepareRequest(request: Request & { params?: Record<string, unknown> }, findResult: FindResult<RequestHandler<Request, Response>>) {
|
|
121
|
+
request.params = {
|
|
122
|
+
...findResult.params,
|
|
123
|
+
...request.params, // original params will take precedence
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public clone() {
|
|
128
|
+
const r = new NodeRouter<Request, Response, Schema>({ onNoMatch: this.onNoMatch, onError: this.onError });
|
|
129
|
+
|
|
130
|
+
r.router = this.router.clone();
|
|
131
|
+
|
|
132
|
+
return r;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async run(request: Request, response: Response): Promise<unknown> {
|
|
136
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference,unicorn/no-array-method-this-argument
|
|
137
|
+
const result = this.router.find(request.method as HttpMethod, getPathname(request.url as string));
|
|
138
|
+
|
|
139
|
+
if (result.fns.length === 0) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.prepareRequest(request, result);
|
|
144
|
+
|
|
145
|
+
// eslint-disable-next-line consistent-return
|
|
146
|
+
return Router.exec(result.fns, request, response);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
handler() {
|
|
150
|
+
const { routes } = this.router as Router<FunctionLike>;
|
|
151
|
+
|
|
152
|
+
return async (request: Request, response: Response) => {
|
|
153
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference,unicorn/no-array-method-this-argument
|
|
154
|
+
const result = this.router.find(request.method as HttpMethod, getPathname(request.url as string));
|
|
155
|
+
|
|
156
|
+
this.prepareRequest(request, result);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await (result.fns.length === 0 || result.middleOnly ? this.onNoMatch(request, response, routes) : Router.exec(result.fns, request, response));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
await this.onError(error, request, response, routes);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const createRouter = <
|
|
168
|
+
Request extends IncomingMessage,
|
|
169
|
+
Response extends ServerResponse,
|
|
170
|
+
Schema extends AnyZodObject = ZodObject<{ body?: AnyZodObject; headers?: AnyZodObject; query?: AnyZodObject }>,
|
|
171
|
+
>(
|
|
172
|
+
options: HandlerOptions<RoutesExtendedRequestHandler<Request, Response, Response, Route<Nextable<FunctionLike>>[]>> = {},
|
|
173
|
+
) => new NodeRouter<Request, Response, Schema>(options);
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agnostic router class
|
|
3
|
+
* Adapted from lukeed/trouter library:
|
|
4
|
+
* https://github.com/lukeed/trouter/blob/master/index.mjs
|
|
5
|
+
*/
|
|
6
|
+
import { parse } from "regexparam";
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
FindResult, FunctionLike, HttpMethod, Nextable, RouteMatch,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export type Route<H> = {
|
|
13
|
+
method: HttpMethod | "";
|
|
14
|
+
fns: (H | Router<H extends FunctionLike ? H : never>)[];
|
|
15
|
+
isMiddleware: boolean;
|
|
16
|
+
} & (
|
|
17
|
+
| {
|
|
18
|
+
keys: string[] | false;
|
|
19
|
+
pattern: RegExp;
|
|
20
|
+
}
|
|
21
|
+
| { matchAll: true }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export class Router<H extends FunctionLike> {
|
|
25
|
+
constructor(public base: string = "/", public routes: Route<Nextable<H>>[] = []) {}
|
|
26
|
+
|
|
27
|
+
public add(method: HttpMethod | "", route: RouteMatch | Nextable<H>, ...fns: Nextable<H>[]): this {
|
|
28
|
+
if (typeof route === "function") {
|
|
29
|
+
fns.unshift(route);
|
|
30
|
+
// eslint-disable-next-line no-param-reassign
|
|
31
|
+
route = "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (route === "") {
|
|
35
|
+
this.routes.push({
|
|
36
|
+
matchAll: true,
|
|
37
|
+
method,
|
|
38
|
+
fns,
|
|
39
|
+
isMiddleware: false,
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
const { keys, pattern } = parse(route);
|
|
43
|
+
|
|
44
|
+
this.routes.push({
|
|
45
|
+
keys,
|
|
46
|
+
pattern,
|
|
47
|
+
method,
|
|
48
|
+
fns,
|
|
49
|
+
isMiddleware: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public use(base: RouteMatch | Nextable<H> | Router<H>, ...fns: (Nextable<H> | Router<H>)[]) {
|
|
57
|
+
if (typeof base === "function" || base instanceof Router) {
|
|
58
|
+
fns.unshift(base);
|
|
59
|
+
// eslint-disable-next-line no-param-reassign
|
|
60
|
+
base = "/";
|
|
61
|
+
}
|
|
62
|
+
// mount subrouter
|
|
63
|
+
// eslint-disable-next-line no-param-reassign
|
|
64
|
+
fns = fns.map((function_) => {
|
|
65
|
+
if (function_ instanceof Router) {
|
|
66
|
+
if (typeof base === "string") return function_.clone(base);
|
|
67
|
+
throw new Error("Mounting a router to RegExp base is not supported");
|
|
68
|
+
}
|
|
69
|
+
return function_;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { keys, pattern } = parse(base, true);
|
|
73
|
+
|
|
74
|
+
this.routes.push({
|
|
75
|
+
keys,
|
|
76
|
+
pattern,
|
|
77
|
+
method: "",
|
|
78
|
+
fns,
|
|
79
|
+
isMiddleware: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public clone(base?: string) {
|
|
86
|
+
return new Router<H>(base, [...this.routes]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static async exec<H extends FunctionLike>(fns: Nextable<H>[], ...arguments_: Parameters<H>): Promise<unknown> {
|
|
90
|
+
let index = 0;
|
|
91
|
+
|
|
92
|
+
// eslint-disable-next-line no-plusplus
|
|
93
|
+
const next = () => (fns[++index] as FunctionLike)(...arguments_, next);
|
|
94
|
+
|
|
95
|
+
return (fns[index] as FunctionLike)(...arguments_, next);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// eslint-disable-next-line radar/cognitive-complexity
|
|
99
|
+
find(method: HttpMethod, pathname: string): FindResult<H> {
|
|
100
|
+
let middleOnly = true;
|
|
101
|
+
|
|
102
|
+
const fns: Nextable<H>[] = [];
|
|
103
|
+
const parameters: Record<string, string> = {};
|
|
104
|
+
const isHead = method === "HEAD";
|
|
105
|
+
|
|
106
|
+
// eslint-disable-next-line radar/cognitive-complexity
|
|
107
|
+
Object.values(this.routes).forEach((route) => {
|
|
108
|
+
if (
|
|
109
|
+
route.method !== method
|
|
110
|
+
// matches any method
|
|
111
|
+
&& route.method !== ""
|
|
112
|
+
// The HEAD method requests that the target resource transfer a representation of its state, as for a GET request...
|
|
113
|
+
&& !(isHead && route.method === "GET")
|
|
114
|
+
) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let matched = false;
|
|
119
|
+
|
|
120
|
+
if ("matchAll" in route) {
|
|
121
|
+
matched = true;
|
|
122
|
+
} else if (route.keys === false) {
|
|
123
|
+
// routes.key is RegExp: https://github.com/lukeed/regexparam/blob/master/src/index.js#L2
|
|
124
|
+
const matches = route.pattern.exec(pathname);
|
|
125
|
+
|
|
126
|
+
if (matches === null) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// eslint-disable-next-line no-void
|
|
131
|
+
if (matches.groups !== void 0) {
|
|
132
|
+
Object.keys(matches.groups).forEach((key) => {
|
|
133
|
+
// @ts-ignore @TODO: fix this
|
|
134
|
+
parameters[key] = matches.groups[key] as string;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
matched = true;
|
|
139
|
+
} else if (route.keys.length > 0) {
|
|
140
|
+
const matches = route.pattern.exec(pathname);
|
|
141
|
+
|
|
142
|
+
if (matches === null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (let index = 0; index < route.keys.length;) {
|
|
147
|
+
const parameterKey = route.keys[index];
|
|
148
|
+
|
|
149
|
+
// @ts-ignore @TODO: fix this
|
|
150
|
+
// eslint-disable-next-line no-plusplus
|
|
151
|
+
parameters[parameterKey] = matches[++index];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
matched = true;
|
|
155
|
+
} else if (route.pattern.test(pathname)) {
|
|
156
|
+
matched = true;
|
|
157
|
+
} // else not a match
|
|
158
|
+
|
|
159
|
+
if (matched) {
|
|
160
|
+
fns.push(
|
|
161
|
+
...route.fns.flatMap((function_) => {
|
|
162
|
+
if (function_ instanceof Router) {
|
|
163
|
+
const base = function_.base as string;
|
|
164
|
+
|
|
165
|
+
let stripPathname = pathname.slice(base.length);
|
|
166
|
+
|
|
167
|
+
// fix stripped pathname, not sure why this happens
|
|
168
|
+
// eslint-disable-next-line eqeqeq
|
|
169
|
+
if (stripPathname[0] != "/") {
|
|
170
|
+
stripPathname = `/${stripPathname}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument
|
|
174
|
+
const result = function_.find(method, stripPathname);
|
|
175
|
+
|
|
176
|
+
if (!result.middleOnly) {
|
|
177
|
+
middleOnly = false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// merge params
|
|
181
|
+
Object.assign(parameters, result.params);
|
|
182
|
+
|
|
183
|
+
return result.fns;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return function_;
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
if (!route.isMiddleware) middleOnly = false;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return { fns, params: parameters, middleOnly };
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AnyZodObject } from "zod";
|
|
2
|
+
|
|
3
|
+
export type HttpMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
|
|
5
|
+
export type FunctionLike = (...arguments_: any[]) => unknown;
|
|
6
|
+
|
|
7
|
+
export type RouteMatch = string | RegExp;
|
|
8
|
+
|
|
9
|
+
export type NextHandler = () => ValueOrPromise<any>;
|
|
10
|
+
|
|
11
|
+
export type Nextable<H extends FunctionLike> = (...arguments_: [...Parameters<H>, NextHandler]) => ValueOrPromise<any>;
|
|
12
|
+
|
|
13
|
+
export type FindResult<H extends FunctionLike> = {
|
|
14
|
+
fns: Nextable<H>[];
|
|
15
|
+
params: Record<string, string>;
|
|
16
|
+
middleOnly: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RoutesExtendedRequestHandler<Request extends object, Context extends unknown, RResponse extends unknown, Routes> = (
|
|
20
|
+
request: Request,
|
|
21
|
+
response: Context,
|
|
22
|
+
routes: Routes,
|
|
23
|
+
) => ValueOrPromise<RResponse | void>;
|
|
24
|
+
|
|
25
|
+
export interface HandlerOptions<Handler extends FunctionLike> {
|
|
26
|
+
onNoMatch?: Handler;
|
|
27
|
+
onError?: (error: unknown, ...arguments_: Parameters<Handler>) => ReturnType<Handler>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ValueOrPromise<T> = T | Promise<T>;
|
|
31
|
+
|
|
32
|
+
export type RouteShortcutMethod<This, Schema extends AnyZodObject, H extends FunctionLike> = (
|
|
33
|
+
route: RouteMatch | Nextable<H>,
|
|
34
|
+
zodSchemaOrRouteOrFns?: Schema | RouteMatch | Nextable<H> | string,
|
|
35
|
+
...fns: Nextable<H>[]
|
|
36
|
+
) => This;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Send `JSON` object
|
|
5
|
+
* @param {ServerResponse} response response object
|
|
6
|
+
* @param {number} statusCode
|
|
7
|
+
* @param {any} jsonBody of data
|
|
8
|
+
*/
|
|
9
|
+
const sendJson = (response: ServerResponse, statusCode: number, jsonBody: any): void => {
|
|
10
|
+
// Set header to application/json
|
|
11
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
12
|
+
|
|
13
|
+
response.statusCode = statusCode;
|
|
14
|
+
response.end(JSON.stringify(jsonBody, null, 2));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default sendJson;
|