@ndiinginc/router 4.6.2 → 4.8.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/README.md CHANGED
@@ -5,8 +5,8 @@ Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsu
5
5
  ## Fitur
6
6
 
7
7
  - Zero dependency, murni `node:http`
8
- - Routing berbasis tree per segmen (static match diprioritaskan, lalu dynamic)
9
- - Parameter dinamis `:id` dan wildcard `*`
8
+ - Routing berbasis tree per segmen: static, lalu `:param`, lalu wildcard `*`, dengan backtracking
9
+ - Parameter dinamis `:id` dan wildcard `*` yang menangkap sisa path
10
10
  - LRU cache untuk hasil lookup route
11
11
  - Nested router dengan `use(path, router)`
12
12
  - Middleware global dan per-path, mendukung `async`
@@ -82,7 +82,7 @@ app.get("/profile", authenticate, loadUser, (req, res) => {
82
82
 
83
83
  ### Parameter dinamis
84
84
 
85
- Nilai parameter otomatis di-decode dengan `decodeURIComponent`. Jika formatnya tidak valid (misalnya `%`), nilai mentah dipakai apa adanya.
85
+ Segmen yang diawali `:` adalah parameter. Nilainya otomatis di-decode dengan `decodeURIComponent`; jika formatnya tidak valid (misalnya `%`), nilai mentah dipakai apa adanya.
86
86
 
87
87
  ```js
88
88
  app.get("/posts/:postId/comments/:commentId", (req, res) => {
@@ -91,20 +91,28 @@ app.get("/posts/:postId/comments/:commentId", (req, res) => {
91
91
  });
92
92
  ```
93
93
 
94
+ Satu segmen adalah satu parameter utuh; nama parameter tidak bisa digabung dengan teks lain dalam segmen yang sama (`:id.json` akan dianggap parameter bernama `id.json`).
95
+
96
+ > Di setiap level hanya ada satu parameter. Jika `/users/:id` dan `/users/:userId/posts` didaftarkan bersamaan, keduanya memakai nama yang pertama didaftarkan (`id`). Pakai nama yang sama untuk level yang sama.
97
+
94
98
  ### Wildcard
95
99
 
96
- `*` cocok dengan karakter apa pun **di dalam satu segmen**. Wildcard tidak melintasi `/`.
100
+ Segmen `*` menangkap **seluruh sisa path** (termasuk `/`) dan nilainya tersedia di `req.params[0]`.
97
101
 
98
102
  ```js
99
- app.get("/files/report-*", (req, res) => {
100
- res.json({ path: req.path });
103
+ app.get("/files/*", (req, res) => {
104
+ res.json({ file: req.params[0] });
101
105
  });
106
+
107
+ // GET /files/images/logo.png -> { file: "images/logo.png" }
102
108
  ```
103
109
 
110
+ Wildcard butuh minimal satu segmen tersisa, jadi `/files/*` tidak cocok dengan `/files`.
111
+
104
112
  ### Aturan matching
105
113
 
106
- - Untuk setiap segmen, router mencoba **static match dulu**, baru pattern dinamis. Jadi `/users/me` menang atas `/users/:id` bila keduanya terdaftar.
107
- - Jika ada beberapa pattern dinamis di level yang sama, yang **terdaftar lebih dulu** yang dipakai (tanpa backtracking).
114
+ - Di setiap level, router mencoba **static dulu, lalu parameter, lalu wildcard**. Jadi `/users/me` menang atas `/users/:id` bila keduanya terdaftar.
115
+ - Jika cabang static gagal di tengah jalan, router mundur (backtrack) dan mencoba parameter, lalu wildcard.
108
116
  - Trailing slash dan slash ganda diabaikan: `/users`, `/users/`, dan `//users` dianggap sama.
109
117
 
110
118
  ## Middleware
@@ -127,7 +135,7 @@ app.use("/admin", async (req, res) => {
127
135
 
128
136
  Chain berhenti otomatis begitu response selesai dikirim (`res.writableEnded`), jadi handler berikutnya tidak dipanggil.
129
137
 
130
- > Middleware tetap dijalankan walaupun tidak ada route yang cocok, sebelum respons 404 dikirim.
138
+ > Jika path tidak cocok dengan route mana pun, hanya middleware global (tanpa path) yang dijalankan sebelum respons 404 dikirim.
131
139
 
132
140
  ## Nested Router
133
141
 
@@ -226,7 +234,7 @@ app.get("/proxy", async (req, res) => {
226
234
  | --- | --- |
227
235
  | `req.path` | Pathname tanpa query string |
228
236
  | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array, dan `+` di-decode menjadi spasi |
229
- | `req.params` | Parameter dari route dinamis (sudah di-decode) |
237
+ | `req.params` | Parameter dari route dinamis (sudah di-decode). Wildcard `*` ada di `req.params[0]` |
230
238
  | `req.body` | Hasil parse body (butuh middleware `json()`) |
231
239
  | `req.signal` | `AbortSignal` yang di-abort saat timeout atau error handler (hanya jika `requestTimeout` aktif) |
232
240
 
@@ -237,7 +245,7 @@ app.get("/search", (req, res) => {
237
245
  });
238
246
  ```
239
247
 
240
- `req.query` dan `req.params` memakai object tanpa prototype, jadi gunakan `Object.hasOwn(req.query, "key")` atau `"key" in req.query`, bukan `req.query.hasOwnProperty(...)`.
248
+ `req.query` dan `req.params` yang sampai ke handler memakai object tanpa prototype, jadi gunakan `Object.hasOwn(req.query, "key")` atau `"key" in req.query`, bukan `req.query.hasOwnProperty(...)`.
241
249
 
242
250
  `Request` adalah turunan `http.IncomingMessage`, jadi `req.headers`, `req.method`, dan lainnya tetap tersedia.
243
251
 
@@ -280,7 +288,7 @@ Perilaku:
280
288
  ```js
281
289
  app.use(
282
290
  App.cors({
283
- origin: "https://example.com",
291
+ origin: ["https://example.com", "https://app.example.com"],
284
292
  credentials: true,
285
293
  methods: ["GET", "POST", "PUT", "DELETE"],
286
294
  headers: ["Content-Type", "Authorization"],
@@ -291,17 +299,17 @@ app.use(
291
299
 
292
300
  | Opsi | Default | Keterangan |
293
301
  | --- | --- | --- |
294
- | `origin` | `"*"` | `"*"`, satu origin, daftar dipisah koma, pola dengan `*` (misalnya `https://*.example.com`), atau `null` |
302
+ | `origin` | `"*"` | `"*"`, satu origin, array origin, atau `null` |
295
303
  | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
296
- | `headers` | - | `"*"` atau array nama header |
297
- | `methods` | - | `"*"` atau array nama method |
304
+ | `headers` | - | `"*"` atau array nama header (tidak membedakan huruf besar/kecil) |
305
+ | `methods` | - | `"*"` atau array nama method (tidak membedakan huruf besar/kecil) |
298
306
  | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
299
307
 
300
308
  Perilaku:
301
309
 
302
310
  - Request `OPTIONS` dijawab langsung dengan `204` dan chain berhenti
303
- - `Access-Control-Allow-Headers` dan `Access-Control-Allow-Methods` hanya dikirim pada preflight, jika nilai yang diminta ada di daftar
304
- - Jika `origin` berupa daftar atau pola (atau `"*"` dengan `credentials: true`), origin dari request dipantulkan bila cocok. Pencocokan dilakukan pada seluruh string (tanpa memperhatikan huruf besar/kecil), dan header `Vary: Origin` ditambahkan pada request non-preflight
311
+ - Origin dicocokkan **persis sama** (case-sensitive) dengan header `Origin` dari request. Jika cocok, origin tersebut dipantulkan, dan header `Vary: Origin` ditambahkan pada request non-preflight
312
+ - `Access-Control-Allow-Headers` dan `Access-Control-Allow-Methods` hanya dikirim pada preflight, dan hanya jika **semua** header atau method yang diminta ada di daftar
305
313
  - Nilai `"*"` untuk `headers` dan `methods` diabaikan saat `credentials: true`, sesuai spesifikasi CORS
306
314
  - Pada request non-preflight, `headers` (jika array) dikirim sebagai `Access-Control-Expose-Headers`
307
315
 
@@ -315,8 +323,8 @@ const app = new App({ cacheSize: 1000, requestTimeout: 5000 });
315
323
 
316
324
  | Opsi | Default | Keterangan |
317
325
  | --- | --- | --- |
318
- | `cacheSize` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path` |
319
- | `requestTimeout` | - | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#requestTimeout) |
326
+ | `cacheSize` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path`. Cache dikosongkan setiap ada route baru didaftarkan |
327
+ | `requestTimeout` | `0` | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
320
328
 
321
329
  `Router` hanya menerima `cacheSize`.
322
330
 
@@ -351,18 +359,6 @@ const server = app.listen(0); // port acak
351
359
  console.log(server.address().port);
352
360
  ```
353
361
 
354
- ## Benchmark
355
-
356
- Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
357
-
358
- | | Req/sec (avg) | Latency (avg) |
359
- | --- | --- | --- |
360
- | Fastify | 31.475 | 31,27 ms |
361
- | **@ndiinginc/router** | 29.735 | 33,12 ms |
362
- | Express | 20.211 | 48,93 ms |
363
-
364
- Hasil bervariasi tergantung mesin, versi Node.js, dan jumlah route. Benchmark ini hanya menggambarkan overhead dasar, bukan performa aplikasi nyata.
365
-
366
362
  ## Testing
367
363
 
368
364
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.6.2",
3
+ "version": "4.8.0",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -19,8 +19,8 @@
19
19
  "type": "commonjs",
20
20
  "devDependencies": {
21
21
  "@ndiinginc/commit": "^1.0.1",
22
- "@ndiinginc/test": "^1.5.1",
22
+ "@ndiinginc/test": "^1.17.0",
23
23
  "@types/jest": "^30.0.0",
24
- "@types/node": "^26.6.4"
24
+ "@types/node": "^26.6.5"
25
25
  }
26
26
  }
package/src/app.js CHANGED
@@ -67,6 +67,10 @@ class App extends Router {
67
67
  err = new Error(error.message, { cause: err });
68
68
  }
69
69
  }
70
+ err = this.fallback(res, err);
71
+ }
72
+
73
+ fallback(res, err) {
70
74
  if (!res.writableEnded) {
71
75
  if (!err) err = ErrorResponse.status(404);
72
76
  res.statusCode = err.statusCode || 500;
@@ -76,6 +80,7 @@ class App extends Router {
76
80
  }
77
81
  res.json({ error });
78
82
  }
83
+ return err;
79
84
  }
80
85
 
81
86
  listen(...params) {
package/src/cache.js CHANGED
@@ -1,5 +1,5 @@
1
1
  class Cache {
2
- constructor(size) {
2
+ constructor(size = 0) {
3
3
  this.size = size;
4
4
  this.map = new Map();
5
5
  }
@@ -19,6 +19,10 @@ class Cache {
19
19
  }
20
20
  this.map.set(key, value);
21
21
  }
22
+
23
+ clear() {
24
+ this.map.clear();
25
+ }
22
26
  }
23
27
 
24
28
  exports.Cache = Cache;
@@ -1,14 +1,3 @@
1
- function pattern(string) {
2
- return new RegExp(
3
- `^(${String(string)
4
- .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
5
- .replace(/ /g, "")
6
- .replace(/,/g, "|")
7
- .replace(/\\\*/g, ".*")})$`,
8
- "i",
9
- );
10
- }
11
-
12
1
  /**
13
2
  * @typedef CORSOptions
14
3
  * @property {String} [origin="*"]
@@ -22,9 +11,12 @@ function pattern(string) {
22
11
  * @param {CORSOptions} options
23
12
  */
24
13
  function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods } = {}) {
25
- const HEADERS_PATTERN = headers && pattern(headers);
26
- const METHODS_PATTERN = methods && pattern(methods);
27
- const ORIGIN_PATTERN = pattern(origin);
14
+ if (headers && Array.isArray(headers)) {
15
+ headers = headers.map((header) => header.toLowerCase());
16
+ }
17
+ if (methods && Array.isArray(methods)) {
18
+ methods = methods.map((method) => method.toUpperCase());
19
+ }
28
20
 
29
21
  /**
30
22
  * @param {import('http').ServerResponse} res
@@ -40,12 +32,13 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
40
32
  * @param {import('http').ServerResponse} res
41
33
  */
42
34
  function setAllowHeaders(req, res) {
43
- const reqHeaders = req.headers["access-control-request-headers"];
35
+ let reqHeaders = req.headers["access-control-request-headers"];
44
36
  if (!reqHeaders) return;
37
+ reqHeaders = reqHeaders.split(",").map((header) => header.trim().toLowerCase());
45
38
  let value;
46
39
  if (headers === "*" && !credentials) {
47
40
  value = "*";
48
- } else if (Array.isArray(headers) && HEADERS_PATTERN && HEADERS_PATTERN.test(reqHeaders)) {
41
+ } else if (Array.isArray(headers) && reqHeaders.every((reqHeader) => headers.some((header) => header === reqHeader))) {
49
42
  value = headers;
50
43
  }
51
44
  if (value) res.setHeader("Access-Control-Allow-Headers", value);
@@ -61,7 +54,7 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
61
54
  let value;
62
55
  if (methods === "*" && !credentials) {
63
56
  value = "*";
64
- } else if (Array.isArray(methods) && METHODS_PATTERN && METHODS_PATTERN.test(reqMethod)) {
57
+ } else if (Array.isArray(methods) && methods.some((method) => method === reqMethod.toUpperCase())) {
65
58
  value = methods;
66
59
  }
67
60
  if (value) res.setHeader("Access-Control-Allow-Methods", value);
@@ -80,7 +73,7 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
80
73
  value = "*";
81
74
  } else if (origin === null) {
82
75
  value = "null";
83
- } else if (ORIGIN_PATTERN.test(reqOrigin)) {
76
+ } else if (origin === "*" || origin === reqOrigin || (Array.isArray(origin) && origin.some((origin) => origin === reqOrigin))) {
84
77
  value = reqOrigin;
85
78
  explicit = true;
86
79
  }
package/src/node.js CHANGED
@@ -1,46 +1,80 @@
1
- const { Pattern } = require("./pattern.js");
2
-
3
1
  class Node {
4
2
  constructor(segment = "") {
5
- this.hasRegExpChildren = false;
6
- this.pattern = /[:*]/.test(segment) && new Pattern(segment);
7
- /**@type {Map<string,Node>}*/
8
- this.children = new Map();
3
+ this.segment = segment;
4
+ /**@type {Map<string, Node>}*/
5
+ this.staticChildren = new Map();
6
+ this.paramChild = null;
7
+ this.paramName = null;
8
+ this.wildcardChild = null;
9
+ /**@type {Map<string, Function[]>}*/
9
10
  this.handlers = new Map();
10
11
  }
11
-
12
+ /**
13
+ * @param {String} segment
14
+ * @returns {Node}
15
+ */
12
16
  add(segment) {
13
- return this.children.getOrInsertComputed(segment, () => {
14
- const node = new Node(segment);
15
- if (node.pattern) this.hasRegExpChildren = true;
16
- return node;
17
- });
17
+ if (segment.startsWith(":")) {
18
+ if (this.paramChild) return this.paramChild;
19
+ this.paramChild = new Node(segment);
20
+ this.paramChild.paramName = segment.slice(1);
21
+ this.paramChild.type = "param";
22
+ return this.paramChild;
23
+ } else if (segment.startsWith("*")) {
24
+ if (this.wildcardChild) return this.wildcardChild;
25
+ this.wildcardChild = new Node(segment);
26
+ this.wildcardChild.type = "wildcard";
27
+ return this.wildcardChild;
28
+ } else {
29
+ return this.staticChildren.getOrInsertComputed(segment, (segment) => {
30
+ const node = new Node(segment);
31
+ node.type = "wildcard";
32
+ return node;
33
+ });
34
+ }
18
35
  }
19
-
36
+ /**
37
+ * @param {Node} node
38
+ */
20
39
  merge(node, offset) {
21
40
  for (const [method, entries] of node.handlers) {
22
- const target = this.handlers.getOrInsert(method, []);
23
41
  for (const entry of entries) {
24
- target.push({ ...entry, seq: entry.seq + offset });
42
+ this.handlers.getOrInsert(method, []).push({
43
+ ...entry,
44
+ seq: entry.seq + offset,
45
+ });
25
46
  }
26
47
  }
27
- for (const [segment, child] of node.children) {
28
- const target = this.children.getOrInsertComputed(segment, () => new Node(segment));
29
- if (target.pattern) this.hasRegExpChildren = true;
30
- target.merge(child, offset);
48
+
49
+ if (node.paramChild) {
50
+ this.add(node.paramChild.segment).merge(node.paramChild, offset);
51
+ }
52
+ if (node.wildcardChild) {
53
+ this.add(node.wildcardChild.segment).merge(node.wildcardChild, offset);
54
+ }
55
+ for (const [segment, child] of node.staticChildren) {
56
+ this.add(segment).merge(child, offset);
31
57
  }
32
58
  }
33
59
 
34
60
  getMaxSeq() {
35
61
  let max = -1;
36
- for (const [, handlers] of this.handlers) {
37
- for (const entry of handlers) {
62
+ for (const [method, entries] of this.handlers) {
63
+ for (const entry of entries) {
38
64
  max = Math.max(max, entry.seq);
39
65
  }
40
66
  }
41
- for (const [, child] of this.children) {
67
+
68
+ if (this.paramChild) {
69
+ max = Math.max(max, this.paramChild.getMaxSeq());
70
+ }
71
+ if (this.wildcardChild) {
72
+ max = Math.max(max, this.wildcardChild.getMaxSeq());
73
+ }
74
+ for (const [path, child] of this.staticChildren) {
42
75
  max = Math.max(max, child.getMaxSeq());
43
76
  }
77
+
44
78
  return max;
45
79
  }
46
80
  }
package/src/response.js CHANGED
@@ -7,7 +7,7 @@ class Response extends http.ServerResponse {
7
7
  }
8
8
  send(chunk) {
9
9
  if (this.writableEnded) return;
10
- this.end(chunk);
10
+ this.end(chunk, null, null); //avoid ArgumentsAdaptorTrampoline from V8
11
11
  }
12
12
  json(value) {
13
13
  if (this.writableEnded) return;
@@ -16,7 +16,7 @@ class Response extends http.ServerResponse {
16
16
  return;
17
17
  }
18
18
  this.setHeader("Content-Type", "application/json");
19
- this.end(JSON.stringify(value));
19
+ this.end(JSON.stringify(value), null, null); //avoid ArgumentsAdaptorTrampoline from V8
20
20
  }
21
21
  }
22
22
  exports.Response = Response;
package/src/router.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const { Node } = require("./node.js");
2
2
  const { Cache } = require("./cache.js");
3
- const { decodePathParam } = require("./util.js");
3
+ const { decodePathParam } = require("./util.js");
4
4
 
5
5
  class Router {
6
6
  constructor(options = {}) {
@@ -16,14 +16,16 @@ class Router {
16
16
  }
17
17
 
18
18
  add(method, path, ...handlers) {
19
+ this.cache.clear();
20
+
19
21
  if (typeof path === "function") {
20
22
  handlers = [path, ...handlers];
21
23
  path = null;
22
24
  }
23
25
  let node = this.root;
24
- if (path !== null) {
25
- for (const segment of this.split(path)) {
26
- node = node.add(segment);
26
+ if (path) {
27
+ for (const part of this.split(path)) {
28
+ node = node.add(part);
27
29
  }
28
30
  }
29
31
 
@@ -32,10 +34,10 @@ class Router {
32
34
  node.merge(handler.root, this.seq);
33
35
  this.seq += handler.root.getMaxSeq() + 1;
34
36
  } else {
35
- node.handlers.getOrInsert(method, []).push({
37
+ const newMethod = handler.length === 3 ? "CATCH" : method;
38
+ node.handlers.getOrInsert(newMethod, []).push({
36
39
  handler,
37
40
  seq: this.seq++,
38
- isErrorHandler: handler.length === 3,
39
41
  });
40
42
  }
41
43
  }
@@ -96,71 +98,96 @@ class Router {
96
98
  return this;
97
99
  }
98
100
 
99
- matchRegExp(node, input, params) {
100
- let matched;
101
- for (const [, child] of node.children) {
102
- if (!child.pattern) continue;
103
- const match = child.pattern.exec(input);
104
- if (match) {
105
- matched = child;
106
- for (const name in match.groups) {
107
- params[name] = decodePathParam(match.groups[name]);
108
- }
109
- break;
101
+ collectMiddleware(node, handlers, errorHandlers) {
102
+ if (node.handlers.has("ALL")) {
103
+ for (const entry of node.handlers.get("ALL")) {
104
+ handlers[entry.seq] = entry.handler;
105
+ }
106
+ }
107
+ if (node.handlers.has("CATCH")) {
108
+ for (const entry of node.handlers.get("CATCH")) {
109
+ errorHandlers[entry.seq] = entry.handler;
110
+ }
111
+ }
112
+ }
113
+
114
+ collectHandler(node, method, handlers) {
115
+ if (node && node.handlers.has(method)) {
116
+ for (const entry of node.handlers.get(method)) {
117
+ handlers[entry.seq] = entry.handler;
110
118
  }
111
119
  }
112
- return matched;
113
120
  }
114
121
 
115
122
  /**
116
- * @param {'GET'|'QUERY'|'HEAD'|'OPTIONS'|'TRACE'|'PUT'|'DELETE'|'POST'|'PATCH'|'CONNECT'} method
117
- * @param {String} path
118
- * @returns {{params: {};handlers: any[];errorHandlers: any[];}}
123
+ * @param {Node} node
124
+ * @param {*} parts
125
+ * @param {*} i
126
+ * @param {*} params
127
+ * @param {*} path
128
+ * @returns
119
129
  */
130
+ search(node, parts, i, params, path) {
131
+ if (!node) return null;
132
+ path.push(node);
133
+ if (i === parts.length) return path;
134
+
135
+ const part = parts[i];
136
+
137
+ if (node.staticChildren.has(part)) {
138
+ const r = this.search(node.staticChildren.get(part), parts, i + 1, params, path);
139
+ if (r) return r;
140
+ }
141
+
142
+ if (node.paramChild) {
143
+ const saved = params[node.paramChild.paramName];
144
+ params[node.paramChild.paramName] = decodePathParam(part);
145
+ const r = this.search(node.paramChild, parts, i + 1, params, path);
146
+ if (r) return r;
147
+ if (saved === undefined) delete params[node.paramChild.paramName];
148
+ else params[node.paramChild.paramName] = saved;
149
+ }
150
+
151
+ if (node.wildcardChild) {
152
+ params[0] = decodePathParam(parts.slice(i).join("/"));
153
+ path.push(node.wildcardChild);
154
+ return path;
155
+ }
156
+
157
+ path.pop();
158
+ return null;
159
+ }
160
+
120
161
  find(method, path) {
121
162
  const key = `${method} ${path}`;
122
163
  const cached = this.cache.get(key);
123
164
  if (cached) return cached;
124
- let node = this.root;
125
- let params = Object.create(null);
165
+
166
+ const parts = this.split(path);
167
+ const params = {};
168
+ const nodePath = [];
169
+ const result = this.search(this.root, parts, 0, params, nodePath);
170
+
126
171
  const handlers = {};
127
172
  const errorHandlers = {};
128
- for (const input of this.split(path)) {
129
- let matched = node.children.get(input);
130
- if (!matched && node.hasRegExpChildren) {
131
- matched = this.matchRegExp(node, input, params);
132
- }
133
- if (!matched) {
134
- node = null;
135
- params = Object.create(null);
136
- break;
137
- }
138
- node = matched;
139
- if (node.handlers.has("ALL")) {
140
- for (const entry of node.handlers.get("ALL")) {
141
- if (entry.isErrorHandler) errorHandlers[entry.seq] = entry.handler;
142
- else handlers[entry.seq] = entry.handler;
143
- }
144
- }
145
- }
146
- if (this.root.handlers.has("ALL")) {
147
- for (const entry of this.root.handlers.get("ALL")) {
148
- if (entry.isErrorHandler) errorHandlers[entry.seq] = entry.handler;
149
- else handlers[entry.seq] = entry.handler;
150
- }
151
- }
152
- if (node && node.handlers.has(method)) {
153
- for (const entry of node.handlers.get(method)) {
154
- handlers[entry.seq] = entry.handler;
173
+
174
+ if (result) {
175
+ for (const n of result) {
176
+ this.collectMiddleware(n, handlers, errorHandlers);
155
177
  }
178
+ this.collectHandler(result[result.length - 1], method, handlers);
179
+ } else {
180
+ this.collectMiddleware(this.root, handlers, errorHandlers);
156
181
  }
157
- const result = {
182
+
183
+ const value = {
158
184
  params,
159
185
  handlers: Object.values(handlers),
160
186
  errorHandlers: Object.values(errorHandlers),
161
187
  };
162
- this.cache.set(key, result);
163
- return result;
188
+ this.cache.set(key, value);
189
+
190
+ return value;
164
191
  }
165
192
  }
166
193
  exports.Router = Router;
package/src/util.js CHANGED
@@ -1,11 +1,9 @@
1
- const URL_REGEX = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
2
- const QUERY_STRING_REGEX = /([^=&]*)?=([^&]*)?/g;
3
-
4
1
  function decodeQueryParam(encoded) {
2
+ const normalize = encoded.replace(/\+/g, " ");
5
3
  try {
6
- return decodeURIComponent(encoded.replace(/\+/g, " "));
4
+ return decodeURIComponent(normalize);
7
5
  } catch (error) {
8
- return encoded;
6
+ return normalize;
9
7
  }
10
8
  }
11
9
  exports.decodeQueryParam = decodeQueryParam;
@@ -17,26 +15,32 @@ function decodePathParam(encoded) {
17
15
  return encoded;
18
16
  }
19
17
  }
20
- exports.decodePathParam=decodePathParam
18
+
19
+ exports.decodePathParam = decodePathParam;
21
20
 
22
21
  function parseQueryString(queryString) {
23
- const result = Object.create(null);
22
+ const query = Object.create(null);
23
+ if (!queryString) return query;
24
+ const regex = /([^=&]*)?=([^&]*)?/g;
24
25
  let match;
25
- while ((match = QUERY_STRING_REGEX.exec(queryString)) !== null) {
26
+ while ((match = regex.exec(queryString))) {
26
27
  const [, rawName = "", rawValue = ""] = match;
27
28
  const name = decodeQueryParam(rawName);
28
29
  const value = decodeQueryParam(rawValue);
29
- if (Object.hasOwn(result, name)) {
30
- if (Array.isArray(result[name])) result[name].push(value);
31
- else result[name] = [result[name], value];
32
- } else result[name] = value;
30
+ if (Object.hasOwn(query, name)) {
31
+ if (Array.isArray(query[name])) query[name].push(value);
32
+ else query[name] = [query[name], value];
33
+ } else {
34
+ query[name] = value;
35
+ }
33
36
  }
34
- return result;
37
+ return query;
35
38
  }
36
39
  exports.parseQueryString = parseQueryString;
37
40
 
38
41
  function parseUrl(url) {
39
- const [, rawPath = "", , queryString = ""] = URL_REGEX.exec(url);
40
- return { path: rawPath || "/", queryString };
42
+ const regex = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
43
+ const [, path = "/", , queryString = ""] = regex.exec(url);
44
+ return { path: path || "/", queryString };
41
45
  }
42
46
  exports.parseUrl = parseUrl;
package/src/pattern.js DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * @typedef PatternExecResult
3
- * @property {number} index
4
- * @property {string} input
5
- * @property {Record<string, string>} [groups]
6
- * @property {number} length
7
- */
8
- class Pattern {
9
- constructor(input) {
10
- const pattern = `^${input
11
- .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
12
- .replace(/:(\w+)/g, "(?<$1>[^/]+)")
13
- .replace(/\\\*/g, ".*")}$`;
14
- Object.defineProperty(this, "regex", {
15
- value: new RegExp(pattern),
16
- configurable: true,
17
- enumerable: false,
18
- });
19
- }
20
-
21
- /**
22
- * @param {string} input
23
- * @returns {PatternExecResult | null}
24
- */
25
- exec(input) {
26
- return this.regex.exec(input);
27
- }
28
-
29
- test(input) {
30
- return this.regex.test(input);
31
- }
32
- }
33
-
34
- exports.Pattern = Pattern;