@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 +14 -0
- package/README.md +91 -68
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/index.mjs +1 -0
- 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 +22 -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,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]
|
|
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**
|
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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` (`
|
|
268
|
+
The following APIs are rewritten in terms of `NodeRouter` (`createNodeRouter`), but they apply to `EdgeRouter` (`createEdgeRouter`) as well.
|
|
262
269
|
|
|
263
|
-
### router =
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 =
|
|
289
|
-
const sub2 =
|
|
290
|
-
const sub3 =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
352
|
-
|
|
358
|
+
logger.log(err);
|
|
359
|
+
// OR: console.error(err);
|
|
353
360
|
|
|
354
|
-
|
|
361
|
+
res.status(500).end("Internal server error");
|
|
355
362
|
}
|
|
356
363
|
|
|
357
|
-
const router =
|
|
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
|
-
|
|
376
|
+
res.status(404).end("page is not found... or is it!?");
|
|
370
377
|
}
|
|
371
378
|
|
|
372
|
-
const router =
|
|
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
|
|
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
|
|
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 =
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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({
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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 =
|
|
548
|
+
const router = createNodeRouter().use(foo).use(bar);
|
|
538
549
|
const handler = router.handler();
|
|
539
550
|
|
|
540
|
-
export async function getServerSideProps({
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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 {
|
|
572
|
+
import { createNodeRouter } from "@visulima/connect";
|
|
562
573
|
|
|
563
|
-
const router =
|
|
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
|
-
|
|
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/
|
|
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"
|
|
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,
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visulima/connect",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"
|
|
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);
|
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;
|