@visulima/connect 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## @visulima/connect [1.1.0](https://github.com/visulima/visulima/compare/@visulima/connect@1.0.1...@visulima/connect@1.1.0) (2022-11-07)
2
+
3
+
4
+ ### Features
5
+
6
+ * **connect:** renamed createRouter to createNodeRouter ([92f6d3f](https://github.com/visulima/visulima/commit/92f6d3f4d3430c281ae7106f7d09bb5b744df341))
7
+
8
+ ## @visulima/connect [1.0.1](https://github.com/visulima/visulima/compare/@visulima/connect@1.0.0...@visulima/connect@1.0.1) (2022-10-27)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * fixed package.json files paths ([0d21e94](https://github.com/visulima/visulima/commit/0d21e94a75e9518f7b87293706615d8fb280095c))
14
+
1
15
  ## @visulima/connect 1.0.0 (2022-10-25)
2
16
 
3
17
 
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**
@@ -57,12 +66,12 @@ Below are some use cases.
57
66
  ```typescript
58
67
  // pages/api/hello.js
59
68
  import type { NextApiRequest, NextApiResponse } from "next";
60
- import { createRouter, expressWrapper } from "@visulima/connect";
69
+ import { createNodeRouter, expressWrapper } from "@visulima/connect";
61
70
  import cors from "cors";
62
71
 
63
72
  // Default Req and Res are IncomingMessage and ServerResponse
64
73
  // You may want to pass in NextApiRequest and NextApiResponse
65
- const router = createRouter<NextApiRequest, NextApiResponse>({
74
+ const router = createNodeRouter<NextApiRequest, NextApiResponse>({
66
75
  onError: (err, req, res) => {
67
76
  console.error(err.stack);
68
77
  res.status(500).end("Something broke!");
@@ -91,7 +100,7 @@ router
91
100
  .put(
92
101
  async (req, res, next) => {
93
102
  // You may want to pass in NextApiRequest & { isLoggedIn: true }
94
- // in createRouter generics to define this extra property
103
+ // in createNodeRouter generics to define this extra property
95
104
  if (!req.isLoggedIn) throw new Error("thrown stuff will be caught");
96
105
  // go to the next in chain
97
106
  return next();
@@ -102,8 +111,6 @@ router
102
111
  }
103
112
  );
104
113
 
105
- // create a handler from router with custom
106
- // onError and onNoMatch
107
114
  export default router.handler();
108
115
  ```
109
116
 
@@ -111,7 +118,7 @@ export default router.handler();
111
118
 
112
119
  ```jsx
113
120
  // page/users/[id].js
114
- import { createRouter } from "@visulima/connect";
121
+ import { createNodeRouter } from "@visulima/connect";
115
122
 
116
123
  export default function Page({ user, updated }) {
117
124
  return (
@@ -123,7 +130,7 @@ export default function Page({ user, updated }) {
123
130
  );
124
131
  }
125
132
 
126
- const router = createRouter()
133
+ const router = createNodeRouter()
127
134
  .use(async (req, res, next) => {
128
135
  // this serve as the error handling middleware
129
136
  try {
@@ -198,7 +205,7 @@ router
198
205
  res.json({ user });
199
206
  return new Response(JSON.stringify({ user }), {
200
207
  status: 200,
201
- headers: {
208
+ headerList: {
202
209
  "content-type": "application/json",
203
210
  },
204
211
  });
@@ -207,7 +214,7 @@ router
207
214
  const user = await updateUser(req.body.user);
208
215
  return new Response(JSON.stringify({ user }), {
209
216
  status: 200,
210
- headers: {
217
+ headerList: {
211
218
  "content-type": "application/json",
212
219
  },
213
220
  });
@@ -258,9 +265,9 @@ export function middleware(request: NextRequest) {
258
265
 
259
266
  ## API
260
267
 
261
- The following APIs are rewritten in terms of `NodeRouter` (`createRouter`), but they apply to `EdgeRouter` (`createEdgeRouter`) as well.
268
+ The following APIs are rewritten in terms of `NodeRouter` (`createNodeRouter`), but they apply to `EdgeRouter` (`createEdgeRouter`) as well.
262
269
 
263
- ### router = createRouter()
270
+ ### router = createNodeRouter()
264
271
 
265
272
  Create an instance Node.js router.
266
273
 
@@ -276,31 +283,31 @@ Create an instance Node.js router.
276
283
  ```javascript
277
284
  // Mount a middleware function
278
285
  router1.use(async (req, res, next) => {
279
- req.hello = "world";
280
- await next(); // call to proceed to the next in chain
281
- console.log("request is done"); // call after all downstream handler has run
286
+ req.hello = "world";
287
+ await next(); // call to proceed to the next in chain
288
+ console.log("request is done"); // call after all downstream handler has run
282
289
  });
283
290
 
284
291
  // Or include a base
285
292
  router2.use("/foo", fn); // Only run in /foo/**
286
293
 
287
294
  // mount an instance of router
288
- const sub1 = createRouter().use(fn1, fn2);
289
- const sub2 = createRouter().use("/dashboard", auth);
290
- const sub3 = createRouter()
291
- .use("/waldo", subby)
292
- .get(getty)
293
- .post("/baz", posty)
294
- .put("/", putty);
295
+ const sub1 = createNodeRouter().use(fn1, fn2);
296
+ const sub2 = createNodeRouter().use("/dashboard", auth);
297
+ const sub3 = createNodeRouter()
298
+ .use("/waldo", subby)
299
+ .get(getty)
300
+ .post("/baz", posty)
301
+ .put("/", putty);
295
302
  router3
296
- // - fn1 and fn2 always run
297
- // - auth runs only on /dashboard
298
- .use(sub1, sub2)
299
- // `subby` runs on ANY /foo/waldo?/*
300
- // `getty` runs on GET /foo/*
301
- // `posty` runs on POST /foo/baz
302
- // `putty` runs on PUT /foo
303
- .use("/foo", sub3);
303
+ // - fn1 and fn2 always run
304
+ // - auth runs only on /dashboard
305
+ .use(sub1, sub2)
306
+ // `subby` runs on ANY /foo/waldo?/*
307
+ // `getty` runs on GET /foo/*
308
+ // `posty` runs on POST /foo/baz
309
+ // `putty` runs on PUT /foo
310
+ .use("/foo", sub3);
304
311
  ```
305
312
 
306
313
  ### router.METHOD(pattern, ...fns)
@@ -348,13 +355,13 @@ By default, it responds with a generic `500 Internal Server Error` while logging
348
355
 
349
356
  ```javascript
350
357
  function onError(err, req, res) {
351
- logger.log(err);
352
- // OR: console.error(err);
358
+ logger.log(err);
359
+ // OR: console.error(err);
353
360
 
354
- res.status(500).end("Internal server error");
361
+ res.status(500).end("Internal server error");
355
362
  }
356
363
 
357
- const router = createRouter({ onError });
364
+ const router = createNodeRouter({onError});
358
365
 
359
366
  export default router.handler();
360
367
  ```
@@ -366,10 +373,10 @@ By default, it responds with a `404` status and a `Route [Method] [Url] not foun
366
373
 
367
374
  ```javascript
368
375
  function onNoMatch(req, res) {
369
- res.status(404).end("page is not found... or is it!?");
376
+ res.status(404).end("page is not found... or is it!?");
370
377
  }
371
378
 
372
- const router = createRouter({ onNoMatch });
379
+ const router = createNodeRouter({onNoMatch});
373
380
 
374
381
  export default router.handler();
375
382
  ```
@@ -480,14 +487,16 @@ console.log("finally"); // this will run before the get layer gets to finish
480
487
 
481
488
  ```javascript
482
489
  // api-libs/base.js
483
- export default createRouter().use(a).use(b);
490
+ export default createNodeRouter().use(a).use(b);
484
491
 
485
492
  // api/foo.js
486
493
  import router from "api-libs/base";
494
+
487
495
  export default router.get(x).handler();
488
496
 
489
497
  // api/bar.js
490
498
  import router from "api-libs/base";
499
+
491
500
  export default router.get(y).handler();
492
501
  ```
493
502
 
@@ -496,14 +505,16 @@ If you want to achieve something like that, you can use `router.clone` to return
496
505
 
497
506
  ```javascript
498
507
  // api-libs/base.js
499
- export default createRouter().use(a).use(b);
508
+ export default createNodeRouter().use(a).use(b);
500
509
 
501
510
  // api/foo.js
502
511
  import router from "api-libs/base";
512
+
503
513
  export default router.clone().get(x).handler();
504
514
 
505
515
  // api/bar.js
506
516
  import router from "api-libs/base";
517
+
507
518
  export default router.clone().get(y).handler();
508
519
  ```
509
520
 
@@ -511,22 +522,22 @@ export default router.clone().get(y).handler();
511
522
 
512
523
  ```javascript
513
524
  // page/index.js
514
- const handler = createRouter()
515
- .use((req, res) => {
516
- // BAD: res.redirect is not a function (not defined in `getServerSideProps`)
517
- // See https://github.com/hoangvvo/@visulima/connect/issues/194#issuecomment-1172961741 for a solution
518
- res.redirect("foo");
519
- })
520
- .use((req, res) => {
521
- // BAD: `getServerSideProps` gives undefined behavior if we try to send a response
522
- res.end("bar");
523
- });
525
+ const handler = createNodeRouter()
526
+ .use((req, res) => {
527
+ // BAD: res.redirect is not a function (not defined in `getServerSideProps`)
528
+ // See https://github.com/hoangvvo/@visulima/connect/issues/194#issuecomment-1172961741 for a solution
529
+ res.redirect("foo");
530
+ })
531
+ .use((req, res) => {
532
+ // BAD: `getServerSideProps` gives undefined behavior if we try to send a response
533
+ res.end("bar");
534
+ });
524
535
 
525
- export async function getServerSideProps({ req, res }) {
526
- await router.run(req, res);
527
- return {
528
- props: {},
529
- };
536
+ export async function getServerSideProps({req, res}) {
537
+ await router.run(req, res);
538
+ return {
539
+ props: {},
540
+ };
530
541
  }
531
542
  ```
532
543
 
@@ -534,14 +545,14 @@ export async function getServerSideProps({ req, res }) {
534
545
 
535
546
  ```javascript
536
547
  // page/index.js
537
- const router = createRouter().use(foo).use(bar);
548
+ const router = createNodeRouter().use(foo).use(bar);
538
549
  const handler = router.handler();
539
550
 
540
- export async function getServerSideProps({ req, res }) {
541
- await handler(req, res); // BAD: You should call router.run(req, res);
542
- return {
543
- props: {},
544
- };
551
+ export async function getServerSideProps({req, res}) {
552
+ await handler(req, res); // BAD: You should call router.run(req, res);
553
+ return {
554
+ props: {},
555
+ };
545
556
  }
546
557
  ```
547
558
 
@@ -558,9 +569,9 @@ If you need to create all handlers for all routes in one file (similar to `Expre
558
569
 
559
570
  ```javascript
560
571
  // pages/api/[[...slug]].js
561
- import { createRouter } from "@visulima/connect";
572
+ import { createNodeRouter } from "@visulima/connect";
562
573
 
563
- const router = createRouter()
574
+ const router = createNodeRouter()
564
575
  .use("/api/hello", someMiddleware())
565
576
  .get("/api/user/:userId", (req, res) => {
566
577
  res.send(`Hello ${req.params.userId}`);
@@ -589,19 +600,31 @@ router.use(expressWrapper(someExpressMiddleware));
589
600
 
590
601
  </details>
591
602
 
603
+ ## Supported Node.js Versions
604
+
605
+ Libraries in this ecosystem make the best effort to track
606
+ [Node.js' release schedule](https://nodejs.org/en/about/releases/). Here's [a
607
+ post on why we think this is important](https://medium.com/the-node-js-collection/maintainers-should-consider-following-node-js-release-schedule-ab08ed4de71a).
608
+
592
609
  ## Contributing
593
610
 
594
- Please see my [contributing.md](https://github.com/visulima/visulima/blob/main/.github/CONTRIBUTING.md).
611
+ 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.
612
+
613
+ > **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.
614
+
615
+ ## Credits
616
+
617
+ - [next-connect](https://github.com/hoangvvo/next-connect)
618
+ - [Daniel Bannert](https://github.com/prisis)
619
+ - [All Contributors](https://github.com/visulima/visulima/graphs/contributors)
595
620
 
596
621
  ## License
597
622
 
598
- [MIT][license-url]
623
+ The visulima connect is open-sourced software licensed under the [MIT][license-url]
599
624
 
600
625
  [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
601
626
  [typescript-url]: "typescript"
602
627
  [license-image]: https://img.shields.io/npm/l/@visulima/connect?color=blueviolet&style=for-the-badge
603
628
  [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"
629
+ [npm-image]: https://img.shields.io/npm/v/@visulima/connect/latest.svg?style=for-the-badge&logo=npm
630
+ [npm-url]: https://www.npmjs.com/package/@visulima/connect/v/latest "npm"
package/dist/index.d.ts CHANGED
@@ -138,4 +138,4 @@ declare const withZod: <Request_1 extends object, Response_1 extends unknown, Ha
138
138
 
139
139
  declare const sendJson: (response: ServerResponse, statusCode: number, jsonBody: any) => void;
140
140
 
141
- export { RequestHandler$1 as EdgeRequestHandler, EdgeRouter, ExpressRequestHandler, FindResult, FunctionLike, HandlerOptions, HttpMethod, NextHandler, Nextable, NodeRouter, RequestHandler, Route, RouteShortcutMethod, Router, ValueOrPromise, createEdgeRouter, createRouter, expressWrapper, sendJson, withZod };
141
+ export { RequestHandler$1 as EdgeRequestHandler, EdgeRouter, ExpressRequestHandler, FindResult, FunctionLike, HandlerOptions, HttpMethod, NextHandler, Nextable, RequestHandler as NodeRequestHandler, NodeRouter, Route, RouteShortcutMethod, Router, ValueOrPromise, createEdgeRouter, createRouter as createNodeRouter, createRouter, expressWrapper, sendJson, withZod };
package/dist/index.js CHANGED
@@ -331,5 +331,6 @@ var send_json_default = sendJson;
331
331
 
332
332
 
333
333
 
334
- exports.EdgeRouter = EdgeRouter; exports.NodeRouter = NodeRouter; exports.Router = Router; exports.createEdgeRouter = createEdgeRouter; exports.createRouter = createRouter; exports.expressWrapper = express_default; exports.sendJson = send_json_default; exports.withZod = with_zod_default;
334
+
335
+ exports.EdgeRouter = EdgeRouter; exports.NodeRouter = NodeRouter; exports.Router = Router; exports.createEdgeRouter = createEdgeRouter; exports.createNodeRouter = createRouter; exports.createRouter = createRouter; exports.expressWrapper = express_default; exports.sendJson = send_json_default; exports.withZod = with_zod_default;
335
336
  //# sourceMappingURL=index.js.map
package/dist/index.mjs CHANGED
@@ -327,6 +327,7 @@ export {
327
327
  NodeRouter,
328
328
  Router,
329
329
  createEdgeRouter,
330
+ createRouter as createNodeRouter,
330
331
  createRouter,
331
332
  express_default as expressWrapper,
332
333
  send_json_default as sendJson,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visulima/connect",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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,22 @@
1
+ export type { RequestHandler as EdgeRequestHandler } from "./edge";
2
+ export { createEdgeRouter, EdgeRouter } from "./edge";
3
+
4
+ export type { ExpressRequestHandler } from "./adapter/express";
5
+ export { default as expressWrapper } from "./adapter/express";
6
+
7
+ export type { RequestHandler as NodeRequestHandler } from "./node";
8
+ export { createRouter as createNodeRouter, NodeRouter } from "./node";
9
+
10
+ // @deprecated Use `createNodeRouter` instead
11
+ export { createRouter } from "./node";
12
+
13
+ export type { Route } from "./router";
14
+ export { Router } from "./router";
15
+
16
+ export { default as withZod } from "./adapter/with-zod";
17
+
18
+ export type {
19
+ HandlerOptions, NextHandler, FunctionLike, Nextable, ValueOrPromise, FindResult, RouteShortcutMethod, HttpMethod,
20
+ } from "./types";
21
+
22
+ 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;