@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 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] [![synk-image]][synk-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
- <sub>Built with ❤︎ by <a href="https://twitter.com/_prisis_">Daniel Bannert</a></sub>
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
- headers: {
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
- headers: {
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
- Please see my [contributing.md](https://github.com/visulima/visulima/blob/main/.github/CONTRIBUTING.md).
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/alpha.svg?style=for-the-badge&logo=npm
605
- [npm-url]: https://www.npmjs.com/package/@visulima/connect/v/alpha "npm"
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.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
- "dist/**",
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);
@@ -0,0 +1,10 @@
1
+ declare module "regexparam" {
2
+ // eslint-disable-next-line import/prefer-default-export
3
+ export function parse(
4
+ route: string | RegExp,
5
+ loose?: boolean,
6
+ ): {
7
+ keys: string[] | false;
8
+ pattern: RegExp;
9
+ };
10
+ }
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;