@ndiinginc/router 0.0.0-stage → 4.0.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/.gitattributes +2 -0
- package/LICENSE +21 -0
- package/README.md +236 -2
- package/package.json +23 -3
- package/rest/router.http +18 -0
- package/src/middleware.js +52 -0
- package/src/node.js +70 -0
- package/src/pattern.js +24 -0
- package/src/polyfill.js +9 -0
- package/src/request.js +29 -0
- package/src/response.js +14 -0
- package/src/router.js +212 -0
package/.gitattributes
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ridho Prasetya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,237 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ndiinginc/router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsung di atas `node:http`, dengan routing berbasis tree, nested router, middleware async, error handler, dan JSON body parser bawaan.
|
|
4
|
+
|
|
5
|
+
## Fitur
|
|
6
|
+
|
|
7
|
+
- Zero dependency, murni `node:http`
|
|
8
|
+
- Routing berbasis tree per segment (static match diprioritaskan, lalu dynamic)
|
|
9
|
+
- Parameter dinamis `:id` dan wildcard `*`
|
|
10
|
+
- Nested router dengan `use(path, router)`
|
|
11
|
+
- Middleware global dan per-path, mendukung `async`
|
|
12
|
+
- Error handler dengan signature `(err, req, res)`
|
|
13
|
+
- `req.path`, `req.query`, `req.params`, `req.body`
|
|
14
|
+
- Helper `res.send()` dan `res.json()`
|
|
15
|
+
- JSON body parser dengan limit ukuran dan proteksi prototype pollution
|
|
16
|
+
- Semua HTTP method, termasuk `QUERY`
|
|
17
|
+
|
|
18
|
+
## Instalasi
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @ndiinginc/router
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Butuh **Node.js 22.1+** (memakai `URL.parse`).
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const Router = require("@ndiinginc/router");
|
|
30
|
+
|
|
31
|
+
const app = new Router();
|
|
32
|
+
|
|
33
|
+
app.get("/", (req, res) => {
|
|
34
|
+
res.json({ hello: "world" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.get("/users/:id", (req, res) => {
|
|
38
|
+
res.json({ id: req.params.id });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.listen(3000);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Routing
|
|
45
|
+
|
|
46
|
+
Semua method mengembalikan `this`, jadi bisa di-chain.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
app.get("/users", handler)
|
|
50
|
+
.post("/users", handler)
|
|
51
|
+
.put("/users/:id", handler)
|
|
52
|
+
.patch("/users/:id", handler)
|
|
53
|
+
.delete("/users/:id", handler);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Method yang tersedia: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `trace`, `connect`, `query`, dan `use` (semua method).
|
|
57
|
+
|
|
58
|
+
### Parameter dinamis
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
app.get("/posts/:postId/comments/:commentId", (req, res) => {
|
|
62
|
+
const { postId, commentId } = req.params;
|
|
63
|
+
res.json({ postId, commentId });
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Wildcard
|
|
68
|
+
|
|
69
|
+
`*` cocok dengan karakter apa pun di dalam satu segmen.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
app.get("/files/report-*", (req, res) => {
|
|
73
|
+
res.json({ path: req.path });
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Prioritas match
|
|
78
|
+
|
|
79
|
+
Untuk setiap segmen, router mencoba **static match dulu**, baru pattern dinamis. Jadi `/users/me` menang atas `/users/:id` bila keduanya terdaftar.
|
|
80
|
+
|
|
81
|
+
## Middleware
|
|
82
|
+
|
|
83
|
+
Middleware didaftarkan dengan `use()` dan dijalankan **berurutan sesuai urutan pendaftaran**.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
// Global
|
|
87
|
+
app.use((req, res) => {
|
|
88
|
+
console.log(req.method, req.path);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Scoped: jalan untuk /admin dan semua sub-path-nya
|
|
92
|
+
app.use("/admin", async (req, res) => {
|
|
93
|
+
if (req.headers.authorization !== "secret") {
|
|
94
|
+
res.statusCode = 401;
|
|
95
|
+
res.json({ error: "Unauthorized" });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Chain berhenti otomatis begitu response dikirim (`res.send()` / `res.json()` menandai `res.done = true`), jadi handler berikutnya tidak dipanggil.
|
|
101
|
+
|
|
102
|
+
> Middleware global tetap dijalankan walaupun tidak ada route yang cocok (sebelum 404 dikirim).
|
|
103
|
+
|
|
104
|
+
## Nested Router
|
|
105
|
+
|
|
106
|
+
Pecah aplikasi jadi beberapa router lalu pasang dengan `use(path, router)`. Nesting bisa berlapis.
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const users = new Router();
|
|
110
|
+
users.get("/", (req, res) => res.json({ list: [] }));
|
|
111
|
+
users.get("/:id", (req, res) => res.json({ id: req.params.id }));
|
|
112
|
+
|
|
113
|
+
const admin = new Router();
|
|
114
|
+
admin.use("/users", users);
|
|
115
|
+
|
|
116
|
+
const app = new Router();
|
|
117
|
+
app.use("/admin", admin);
|
|
118
|
+
|
|
119
|
+
// GET /admin/users -> list
|
|
120
|
+
// GET /admin/users/42 -> { id: "42" }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Error Handling
|
|
124
|
+
|
|
125
|
+
Handler dengan **tiga argumen** `(err, req, res)` dikenali sebagai error handler. Error handler dipanggil saat handler atau middleware melempar error (sync maupun async).
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
app.get("/boom", () => {
|
|
129
|
+
throw new Error("Something broke");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
app.use((err, req, res) => {
|
|
133
|
+
res.statusCode = err.statusCode || 500;
|
|
134
|
+
res.json({ error: err.message });
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Respons default
|
|
139
|
+
|
|
140
|
+
Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{ "error": { "code": undefined, "message": "Not Found" } }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- Tidak ada route cocok: status `404`
|
|
147
|
+
- Error dengan status 2xx: dinaikkan menjadi `500`
|
|
148
|
+
- Saat `NODE_ENV=development`, field `stack` ikut disertakan
|
|
149
|
+
|
|
150
|
+
## Request
|
|
151
|
+
|
|
152
|
+
| Properti | Keterangan |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `req.path` | Pathname tanpa query string |
|
|
155
|
+
| `req.query` | Object query string. Key yang berulang menjadi array |
|
|
156
|
+
| `req.params` | Parameter dari route dinamis |
|
|
157
|
+
| `req.body` | Hasil parse body (butuh middleware `json()`) |
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
// GET /search?tag=a&tag=b&page=2
|
|
161
|
+
app.get("/search", (req, res) => {
|
|
162
|
+
res.json(req.query); // { tag: ["a", "b"], page: "2" }
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Response
|
|
167
|
+
|
|
168
|
+
| Method | Keterangan |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `res.send(string)` | Kirim response dan akhiri chain |
|
|
171
|
+
| `res.json(object)` | Set `content-type: application/json`, serialize, lalu kirim |
|
|
172
|
+
|
|
173
|
+
Keduanya juga turunan dari `http.ServerResponse`, jadi `res.statusCode`, `res.setHeader()`, dan lainnya tetap tersedia.
|
|
174
|
+
|
|
175
|
+
## JSON Body Parser
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const Router = require("@ndiinginc/router");
|
|
179
|
+
const { json } = require("@ndiinginc/router/middleware");
|
|
180
|
+
|
|
181
|
+
const app = new Router();
|
|
182
|
+
|
|
183
|
+
app.use(json({ limit: 100 * 1024 })); // default 100 KB
|
|
184
|
+
|
|
185
|
+
app.post("/users", (req, res) => {
|
|
186
|
+
res.json({ received: req.body });
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Perilaku:
|
|
191
|
+
|
|
192
|
+
- Hanya memproses method `POST`, `PUT`, `PATCH`, dan `QUERY` dengan `content-type: application/json`
|
|
193
|
+
- Body melebihi `limit` memicu error `413 Payload Too Large` (dicek lewat `content-length` dan saat streaming)
|
|
194
|
+
- JSON tidak valid memicu error `400 Invalid JSON body`
|
|
195
|
+
- Body kosong menghasilkan `req.body = null`
|
|
196
|
+
- Key `__proto__`, `constructor`, dan `prototype` dibuang untuk mencegah prototype pollution
|
|
197
|
+
|
|
198
|
+
## API
|
|
199
|
+
|
|
200
|
+
### `new Router()`
|
|
201
|
+
|
|
202
|
+
Membuat instance router baru.
|
|
203
|
+
|
|
204
|
+
### `router.use([path], handler | router)`
|
|
205
|
+
|
|
206
|
+
Mendaftarkan middleware, error handler, atau nested router untuk semua method.
|
|
207
|
+
|
|
208
|
+
### `router.<method>([path], handler)`
|
|
209
|
+
|
|
210
|
+
Mendaftarkan handler untuk method tertentu. `path` opsional; jika dihilangkan, handler dipasang di root router.
|
|
211
|
+
|
|
212
|
+
### `router.find(method, path)`
|
|
213
|
+
|
|
214
|
+
Mencari route tanpa menjalankannya. Berguna untuk testing.
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
const { params, handlers, errorHandlers } = app.find("GET", "/users/1");
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `router.listen(...args)`
|
|
221
|
+
|
|
222
|
+
Membuat `http.Server` dengan `Request` dan `Response` milik router, lalu memanggil `server.listen(...args)`. Mengembalikan instance `http.Server`.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
const server = app.listen(0); // port acak
|
|
226
|
+
console.log(server.address().port);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Testing
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm test
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Lisensi
|
|
236
|
+
|
|
237
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ndiinginc/router",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "./src/router.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "nodemon --exec \"node -e \\\"require('@ndiinginc/test')(require('path').resolve('tests'),{verbose:true})\\\"\"",
|
|
8
|
+
"dev": "nodemon src/router.js",
|
|
9
|
+
"prettier": "npx prettier . --write",
|
|
10
|
+
"commit": "node -e \"require('@ndiinginc/commit')()\"",
|
|
11
|
+
"npm:dry-run": "npm publish --dry-run",
|
|
12
|
+
"npm:login": "npm login",
|
|
13
|
+
"npm:publish": "npm publish --access public",
|
|
14
|
+
"tsc": "npx tsc"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"type": "commonjs",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@ndiinginc/commit": "^1.0.1",
|
|
22
|
+
"@ndiinginc/test": "^1.5.1",
|
|
23
|
+
"@types/jest": "^30.0.0",
|
|
24
|
+
"@types/node": "^26.6.4"
|
|
25
|
+
}
|
|
6
26
|
}
|
package/rest/router.http
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
###
|
|
2
|
+
GET http://localhost:3000
|
|
3
|
+
###
|
|
4
|
+
POST http://localhost:3000
|
|
5
|
+
Content-Type: application/json; charset=utf-8; asdfadsf
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
"user":"name",
|
|
9
|
+
"pass":"word"
|
|
10
|
+
}
|
|
11
|
+
###
|
|
12
|
+
GET http://localhost:4000
|
|
13
|
+
###
|
|
14
|
+
GET http://localhost:5000
|
|
15
|
+
|
|
16
|
+
autocannon -c 100 -d 10 -p 10 localhost:3000
|
|
17
|
+
autocannon -c 100 -d 10 -p 10 localhost:4000
|
|
18
|
+
autocannon -c 100 -d 10 -p 10 localhost:5000
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const http = require("node:http");
|
|
2
|
+
|
|
3
|
+
function json({ limit = 100 * 1024 } = {}) {
|
|
4
|
+
const reviver = (key, value) => {
|
|
5
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
return async (req, res) => {
|
|
12
|
+
if (!["POST", "PUT", "PATCH", "QUERY"].includes(req.method)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const contentType = req.headers["content-type"];
|
|
17
|
+
if (!contentType || !contentType.includes("application/json")) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const contentLength = parseInt(req.headers["content-length"], 10);
|
|
22
|
+
if (contentLength && contentLength > limit) {
|
|
23
|
+
res.statusCode = 413;
|
|
24
|
+
throw new Error(http.STATUS_CODES[res.statusCode]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let bodySize = 0;
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for await (const chunk of req) {
|
|
30
|
+
bodySize += chunk.length;
|
|
31
|
+
if (bodySize > limit) {
|
|
32
|
+
res.statusCode = 413;
|
|
33
|
+
throw new Error(http.STATUS_CODES[res.statusCode]);
|
|
34
|
+
}
|
|
35
|
+
chunks.push(chunk);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (chunks.length === 0) {
|
|
39
|
+
req.body = null;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
44
|
+
try {
|
|
45
|
+
req.body = JSON.parse(rawBody, reviver);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
res.statusCode = 400;
|
|
48
|
+
throw new Error("Invalid JSON body");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
module.exports.json = json;
|
package/src/node.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { Pattern } = require("./pattern.js");
|
|
2
|
+
|
|
3
|
+
class Node {
|
|
4
|
+
constructor(segment) {
|
|
5
|
+
this.segment = segment;
|
|
6
|
+
this.pattern = new Pattern(segment);
|
|
7
|
+
/**@type {Map<string, Node>}*/
|
|
8
|
+
this.children = new Map();
|
|
9
|
+
/**@type {Map<string, Function[]>}*/
|
|
10
|
+
this.handlers = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
add(segment) {
|
|
14
|
+
if (!this.children.has(segment)) {
|
|
15
|
+
this.children.set(segment, new Node(segment));
|
|
16
|
+
}
|
|
17
|
+
return this.children.get(segment);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Node} node
|
|
22
|
+
* @param {Number} offset
|
|
23
|
+
*/
|
|
24
|
+
merge(node, offset) {
|
|
25
|
+
for (const [method, handlers] of node.handlers) {
|
|
26
|
+
for (const handler of handlers) {
|
|
27
|
+
handler.seq += offset;
|
|
28
|
+
this.handlers.getOrInsert(method, []).push(handler);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [segment, child] of node.children) {
|
|
33
|
+
if (!this.children.has(segment)) {
|
|
34
|
+
child.setSeqOffset(offset);
|
|
35
|
+
this.children.set(segment, child);
|
|
36
|
+
} else {
|
|
37
|
+
this.children.get(segment).merge(child, offset);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setSeqOffset(offset) {
|
|
43
|
+
for (const [, handlers] of this.handlers) {
|
|
44
|
+
for (const handler of handlers) {
|
|
45
|
+
handler.seq += offset;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [, child] of this.children) {
|
|
50
|
+
child.setSeqOffset(offset);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getMaxSeq() {
|
|
55
|
+
let max = -1;
|
|
56
|
+
|
|
57
|
+
for (const [, handlers] of this.handlers) {
|
|
58
|
+
for (const handler of handlers) {
|
|
59
|
+
max = Math.max(max, handler.seq);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [, child] of this.children) {
|
|
64
|
+
max = Math.max(max, child.getMaxSeq());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return max;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.Node = Node;
|
package/src/pattern.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Pattern {
|
|
2
|
+
static baseURL = "http://localhost";
|
|
3
|
+
|
|
4
|
+
constructor(input) {
|
|
5
|
+
const regexStr = input
|
|
6
|
+
.replace(/[.+*?^${}()|[\]\\]/g, "\\$&")
|
|
7
|
+
.replace(/:(\w+)/g, "(?<$1>[^/]+)")
|
|
8
|
+
.replace(/\\\*/g, ".*");
|
|
9
|
+
this.regex = new RegExp(`^${regexStr}$`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
exec(input) {
|
|
13
|
+
const match = this.regex.exec(input);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
return {
|
|
16
|
+
pathname: { groups: match.groups || {} },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test(input) {
|
|
21
|
+
return this.regex.test(input);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.Pattern = Pattern;
|
package/src/polyfill.js
ADDED
package/src/request.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { IncomingMessage } = require("node:http");
|
|
2
|
+
|
|
3
|
+
class Request extends IncomingMessage {
|
|
4
|
+
get path() {
|
|
5
|
+
if (this.url.indexOf("?") !== -1) {
|
|
6
|
+
this.parsed = URL.parse(this.url, "http://localhost");
|
|
7
|
+
return this.parsed.pathname;
|
|
8
|
+
}
|
|
9
|
+
return this.url;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get query() {
|
|
13
|
+
const query = {};
|
|
14
|
+
if (!this.parsed) return query;
|
|
15
|
+
for (const [name, value] of this.parsed.searchParams) {
|
|
16
|
+
if (query[name]) {
|
|
17
|
+
if (Array.isArray(query[name])) {
|
|
18
|
+
query[name].push(value);
|
|
19
|
+
} else {
|
|
20
|
+
query[name] = [query[name], value];
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
query[name] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return query;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.Request = Request;
|
package/src/response.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { ServerResponse } = require("node:http");
|
|
2
|
+
|
|
3
|
+
class Response extends ServerResponse {
|
|
4
|
+
send(string) {
|
|
5
|
+
this.done = true;
|
|
6
|
+
this.end(string);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
json(object) {
|
|
10
|
+
this.setHeader("content-type", "application/json");
|
|
11
|
+
this.send(JSON.stringify(object));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.Response = Response;
|
package/src/router.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
require("./polyfill.js");
|
|
2
|
+
const http = require("node:http");
|
|
3
|
+
const { Response } = require("./response.js");
|
|
4
|
+
const { Request } = require("./request.js");
|
|
5
|
+
const { Node } = require("./node.js");
|
|
6
|
+
|
|
7
|
+
class Router {
|
|
8
|
+
/**@type {Node}*/
|
|
9
|
+
root;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.root = new Node("");
|
|
13
|
+
this.seq = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
split(path) {
|
|
17
|
+
return path.split("/").filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
add(method, path, handler) {
|
|
21
|
+
if (typeof path === "function") {
|
|
22
|
+
handler = path;
|
|
23
|
+
path = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let node = this.root;
|
|
27
|
+
|
|
28
|
+
if (path !== null) {
|
|
29
|
+
for (const segment of this.split(path)) {
|
|
30
|
+
node = node.add(segment);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (handler.root) {
|
|
35
|
+
node.merge(handler.root, this.seq);
|
|
36
|
+
this.seq = node.getMaxSeq() + 1;
|
|
37
|
+
} else {
|
|
38
|
+
handler.seq = this.seq++;
|
|
39
|
+
node.handlers.getOrInsert(method, []).push(handler);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
use(path, handler) {
|
|
44
|
+
this.add("ALL", path, handler);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get(path, handler) {
|
|
49
|
+
this.add("GET", path, handler);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
query(path, handler) {
|
|
53
|
+
this.add("QUERY", path, handler);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
head(path, handler) {
|
|
57
|
+
this.add("HEAD", path, handler);
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
options(path, handler) {
|
|
61
|
+
this.add("OPTIONS", path, handler);
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
trace(path, handler) {
|
|
65
|
+
this.add("TRACE", path, handler);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
put(path, handler) {
|
|
69
|
+
this.add("PUT", path, handler);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
delete(path, handler) {
|
|
73
|
+
this.add("DELETE", path, handler);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
post(path, handler) {
|
|
77
|
+
this.add("POST", path, handler);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
patch(path, handler) {
|
|
81
|
+
this.add("PATCH", path, handler);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
connect(path, handler) {
|
|
85
|
+
this.add("CONNECT", path, handler);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
find(method, path) {
|
|
90
|
+
let params = {};
|
|
91
|
+
let handlers = {};
|
|
92
|
+
let errorHandlers = {};
|
|
93
|
+
|
|
94
|
+
let node = this.root;
|
|
95
|
+
|
|
96
|
+
const inputs = this.split(path);
|
|
97
|
+
if (inputs.length) {
|
|
98
|
+
for (const input of inputs) {
|
|
99
|
+
let matched = node.children.get(input);
|
|
100
|
+
|
|
101
|
+
if (!matched) {
|
|
102
|
+
for (const [, child] of node.children) {
|
|
103
|
+
const match = child.pattern.exec(input);
|
|
104
|
+
if (match) {
|
|
105
|
+
matched = child;
|
|
106
|
+
Object.assign(params, match.pathname.groups);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!matched) {
|
|
113
|
+
node = null;
|
|
114
|
+
params = {};
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
node = matched;
|
|
119
|
+
|
|
120
|
+
if (node.handlers.has("ALL")) {
|
|
121
|
+
for (const handler of node.handlers.get("ALL")) {
|
|
122
|
+
if (handler.length === 3) errorHandlers[handler.seq] = handler;
|
|
123
|
+
else handlers[handler.seq] = handler;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.root.handlers.has("ALL")) {
|
|
130
|
+
for (const handler of this.root.handlers.get("ALL")) {
|
|
131
|
+
if (handler.length === 3) errorHandlers[handler.seq] = handler;
|
|
132
|
+
else handlers[handler.seq] = handler;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (node && node.handlers.has(method)) {
|
|
137
|
+
for (const handler of node.handlers.get(method)) {
|
|
138
|
+
if (handler.length === 3) errorHandlers[handler.seq] = handler;
|
|
139
|
+
else handlers[handler.seq] = handler;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
params,
|
|
145
|
+
handlers: Object.values(handlers),
|
|
146
|
+
errorHandlers: Object.values(errorHandlers),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {import('http').RequestOptions} req
|
|
152
|
+
* @param {import('http').ServerResponse} res
|
|
153
|
+
*/
|
|
154
|
+
async handleRequest(req, res) {
|
|
155
|
+
const result = this.find(req.method, req.path);
|
|
156
|
+
req.params = result.params;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
for (const handler of result.handlers) {
|
|
160
|
+
if (res.done) break;
|
|
161
|
+
await handler(req, res);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
res.error = error;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
for (const handler of result.errorHandlers) {
|
|
168
|
+
if (res.done) break;
|
|
169
|
+
await handler(res.error, req, res);
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
res.error = error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!res.done) {
|
|
177
|
+
if (!res.error) {
|
|
178
|
+
res.statusCode = 404;
|
|
179
|
+
res.error = new Error(http.STATUS_CODES[res.statusCode]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
res.statusCode = res.statusCode >= 200 && res.statusCode < 300 ? 500 : res.statusCode;
|
|
183
|
+
|
|
184
|
+
const error = {
|
|
185
|
+
code: res.error.code,
|
|
186
|
+
message: res.error.message,
|
|
187
|
+
};
|
|
188
|
+
if (process.env.NODE_ENV === "development") error.stack = res.error.stack;
|
|
189
|
+
|
|
190
|
+
res.json({ error });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @returns {import('http').Server}
|
|
196
|
+
*/
|
|
197
|
+
listen(...params) {
|
|
198
|
+
const server = http.createServer(
|
|
199
|
+
{
|
|
200
|
+
IncomingMessage: Request,
|
|
201
|
+
ServerResponse: Response,
|
|
202
|
+
},
|
|
203
|
+
(req, res) => this.handleRequest(req, res),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
server.listen(...params);
|
|
207
|
+
|
|
208
|
+
return server;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = Router;
|