router-http 1.0.12 → 2.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/LICENSE.md +0 -0
- package/README.md +201 -89
- package/package.json +17 -16
- package/src/index.js +214 -100
package/LICENSE.md
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -4,177 +4,289 @@
|
|
|
4
4
|
[](https://coveralls.io/github/Kikobeats/router-http)
|
|
5
5
|
[](https://www.npmjs.org/package/router-http)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [router-http](#router-http)
|
|
8
|
+
- [Why not Express router?](#why-not-express-router)
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Getting Started](#getting-started)
|
|
11
|
+
- [Declaring routes](#declaring-routes)
|
|
12
|
+
- [Declaring middlewares](#declaring-middlewares)
|
|
13
|
+
- [Starting the server](#starting-the-server)
|
|
14
|
+
- [Advanced](#advanced)
|
|
15
|
+
- [Request object](#request-object)
|
|
16
|
+
- [Print routes](#print-routes)
|
|
17
|
+
- [Nested routers](#nested-routers)
|
|
18
|
+
- [Skipping to parent router](#skipping-to-parent-router)
|
|
19
|
+
- [Benchmark](#benchmark)
|
|
20
|
+
- [Related](#related)
|
|
21
|
+
- [License](#license)
|
|
8
22
|
|
|
9
|
-
- Faster (x3 compared with Express).
|
|
10
|
-
- Maintained and well tested.
|
|
11
|
-
- Smaller (1.4 kB).
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
A middleware-style router similar to [express router](https://github.com/pillarjs/router), with key advantages:
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
- **Predictable performance** – Backed by [find-my-way](https://github.com/delvedor/find-my-way), a trie-based router with constant O(1) lookup time.
|
|
27
|
+
- **Battle-tested** – Well maintained with comprehensive test coverage.
|
|
28
|
+
- **Lightweight** – Only 1.3 kB (minifized + gzipped)
|
|
16
29
|
|
|
17
|
-
##
|
|
30
|
+
## Why not Express router?
|
|
31
|
+
|
|
32
|
+
Express uses regex-based route matching that degrades linearly as routes increase:
|
|
33
|
+
|
|
34
|
+
| Routes | `express@router` | `router-http` |
|
|
35
|
+
|--------|-----------|---------------|
|
|
36
|
+
| 5 | ~10.7M ops/sec | **~13.7M ops/sec** |
|
|
37
|
+
| 10 | ~6.5M ops/sec | **~13.7M ops/sec** |
|
|
38
|
+
| 50 | ~1.5M ops/sec | **~11.5M ops/sec** |
|
|
39
|
+
| 1000 | ~41K ops/sec | **~10.6M ops/sec** |
|
|
40
|
+
|
|
41
|
+
In contrast, **router-http** is backed by a trie-based implementation that maintains nearly constant performance regardless of the number of routes.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
18
44
|
|
|
19
45
|
```bash
|
|
20
|
-
|
|
46
|
+
npm install router-http
|
|
21
47
|
```
|
|
22
48
|
|
|
23
|
-
##
|
|
49
|
+
## Getting Started
|
|
24
50
|
|
|
25
|
-
First,
|
|
51
|
+
First, define a handler for errors and unmatched routes:
|
|
26
52
|
|
|
27
53
|
```js
|
|
28
54
|
const createRouter = require('router-http')
|
|
29
55
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
56
|
+
const finalHandler = (error, req, res) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
res.statusCode = 500
|
|
59
|
+
res.end(error.message)
|
|
60
|
+
} else {
|
|
61
|
+
res.statusCode = 404
|
|
62
|
+
res.end('Not Found')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const router = createRouter(finalHandler)
|
|
35
67
|
```
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
You can also pass [find-my-way options](https://github.com/delvedor/find-my-way#options) as a second argument:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const router = createRouter(finalHandler, {
|
|
73
|
+
caseSensitive: false,
|
|
74
|
+
ignoreTrailingSlash: true
|
|
75
|
+
})
|
|
76
|
+
```
|
|
38
77
|
|
|
39
78
|
### Declaring routes
|
|
40
79
|
|
|
41
|
-
|
|
80
|
+
Use HTTP verb methods to define your routes:
|
|
42
81
|
|
|
43
82
|
```js
|
|
44
|
-
/**
|
|
45
|
-
* Declaring multiple routes based on the HTTP verb.
|
|
46
|
-
*/
|
|
47
83
|
router
|
|
48
|
-
.get('/', (req, res) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.post('/ping', (req, res) => res.end('pong'))
|
|
53
|
-
.get('/greetings/:name', (req, res) => {
|
|
54
|
-
const { name } = req.params
|
|
55
|
-
res.end(`Hello, ${name}!`)
|
|
56
|
-
})
|
|
84
|
+
.get('/', (req, res) => res.end('Hello World'))
|
|
85
|
+
.post('/users', (req, res) => res.end('User created'))
|
|
86
|
+
.put('/users/:id', (req, res) => res.end('User updated'))
|
|
87
|
+
.delete('/users/:id', (req, res) => res.end('User deleted'))
|
|
57
88
|
```
|
|
58
89
|
|
|
59
|
-
|
|
90
|
+
Use `.all()` to match any HTTP method:
|
|
60
91
|
|
|
61
92
|
```js
|
|
62
|
-
/**
|
|
63
|
-
* Declaring a route to match all the HTTP verbs.
|
|
64
|
-
*/
|
|
65
93
|
router.all('/ping', (req, res) => res.end('pong'))
|
|
66
94
|
```
|
|
67
95
|
|
|
96
|
+
The dynamic segments will be captured using the `:param` syntax, with parameters accessible via `req.params`:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
router.get('/users/:id', (req, res) => {
|
|
100
|
+
res.end(`User ID: ${req.params.id}`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
router.get('/posts/:year/:month', (req, res) => {
|
|
104
|
+
const { year, month } = req.params
|
|
105
|
+
res.end(`Posts from ${month}/${year}`)
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
See [Request object](#request-object) for details on how to access route parameters and other useful properties added to `req`.
|
|
110
|
+
|
|
68
111
|
### Declaring middlewares
|
|
69
112
|
|
|
70
|
-
A middleware
|
|
113
|
+
A middleware is declared using `.use()`. it will be added globally before running any route:
|
|
71
114
|
|
|
72
115
|
```js
|
|
73
|
-
|
|
74
|
-
* Declaring a middleware that will be always executed.
|
|
75
|
-
*/
|
|
116
|
+
// Global middleware (runs on every request)
|
|
76
117
|
router
|
|
77
|
-
.use(
|
|
118
|
+
.use((req, res, next) => {
|
|
78
119
|
req.timestamp = Date.now()
|
|
79
120
|
next()
|
|
80
|
-
|
|
121
|
+
})
|
|
81
122
|
```
|
|
82
123
|
|
|
83
|
-
|
|
124
|
+
You can also declare middleware specific to routes:
|
|
84
125
|
|
|
85
126
|
```js
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
const auth = (req, res, next) => { /* verify token */ next() }
|
|
128
|
+
const log = (req, res, next) => { /* log request */ next() }
|
|
129
|
+
|
|
130
|
+
const protected = [auth, log]
|
|
131
|
+
|
|
89
132
|
router
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
next()
|
|
93
|
-
})
|
|
94
|
-
.get('/greetings/:username', (req, res) => {
|
|
95
|
-
res.end(`${req.greetings}, ${req.params.username}`)
|
|
96
|
-
})
|
|
133
|
+
.get('/admin', protected, (req, res) => res.end('Admin panel'))
|
|
134
|
+
.get('/settings', protected, (req, res) => res.end('Settings'))
|
|
97
135
|
```
|
|
98
136
|
|
|
99
|
-
|
|
137
|
+
If you want to add a middleware conditionally, just return a falsy value:
|
|
100
138
|
|
|
101
139
|
```js
|
|
102
|
-
/**
|
|
103
|
-
* Just add the middleware if it's production environment
|
|
104
|
-
*/
|
|
105
140
|
router
|
|
106
|
-
.use(process.env.NODE_ENV === 'production' &&
|
|
141
|
+
.use(process.env.NODE_ENV === 'production' && rateLimiter())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Starting the server
|
|
145
|
+
|
|
146
|
+
The router is a standard request handler. Pass it to `http.createServer`:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
const http = require('http')
|
|
150
|
+
|
|
151
|
+
console.log(router.prettyPrint())
|
|
152
|
+
// └── / (GET)
|
|
153
|
+
// ├── favicon.ico (GET)
|
|
154
|
+
// └── user/
|
|
155
|
+
// └── :id (GET)
|
|
156
|
+
|
|
157
|
+
http.createServer(router).listen(3000)
|
|
107
158
|
```
|
|
108
159
|
|
|
109
|
-
|
|
160
|
+
## Advanced
|
|
110
161
|
|
|
111
|
-
|
|
162
|
+
### Request object
|
|
112
163
|
|
|
113
|
-
|
|
164
|
+
The router adds these properties to `req`:
|
|
114
165
|
|
|
115
|
-
|
|
166
|
+
| Property | Description |
|
|
167
|
+
|----------|-------------|
|
|
168
|
+
| `req.path` | URL pathname |
|
|
169
|
+
| `req.params` | Route parameters object |
|
|
170
|
+
| `req.query` | Raw query string (after `?`) |
|
|
171
|
+
| `req.search` | Raw search string (including `?`) |
|
|
172
|
+
|
|
173
|
+
> `req.query` and `req.search` are only set if not already present.
|
|
174
|
+
|
|
175
|
+
### Print routes
|
|
176
|
+
|
|
177
|
+
You can visualize your router's routes in a readable tree format using the `router.prettyPrint()` method. This is especially helpful for debugging or understanding your route structure at a glance.
|
|
178
|
+
|
|
179
|
+
For example:
|
|
116
180
|
|
|
117
181
|
```js
|
|
118
|
-
|
|
182
|
+
const http = require('http')
|
|
183
|
+
|
|
184
|
+
console.log(router.prettyPrint())
|
|
185
|
+
// └── / (GET)
|
|
186
|
+
// ├── favicon.ico (GET)
|
|
187
|
+
// └── user/
|
|
188
|
+
// └── :id (GET)
|
|
189
|
+
|
|
190
|
+
http.createServer(router).listen(3000)
|
|
191
|
+
```
|
|
119
192
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
193
|
+
The printed output shows the nested structure of your routes along with their registered HTTP methods. This works for both flat and deeply nested routers, including those mounted via `.use()`.
|
|
194
|
+
|
|
195
|
+
See more in [find-my-way prettyPrint documentation](https://github.com/delvedor/find-my-way#routerprettyprint).
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
### Nested routers
|
|
199
|
+
|
|
200
|
+
You can use a router as a middleware for another router. This is useful for prefixing routes or for modularizing your application:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const createRouter = require('router-http')
|
|
204
|
+
const http = require('http')
|
|
205
|
+
|
|
206
|
+
const final = (err, req, res) => {
|
|
207
|
+
res.statusCode = err ? 500 : 404
|
|
208
|
+
res.end(err ? err.message : 'Not Found')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 1. Create a sub-router for API v1
|
|
212
|
+
const v1 = createRouter(final)
|
|
213
|
+
v1.get('/info', (req, res) => res.end('v1 info'))
|
|
214
|
+
|
|
215
|
+
// 2. Create another sub-router for API v2
|
|
216
|
+
const v2 = createRouter(final)
|
|
217
|
+
v2.get('/info', (req, res) => res.end('v2 info'))
|
|
218
|
+
|
|
219
|
+
// 3. Create the main router and mount sub-routers
|
|
123
220
|
const router = createRouter(final)
|
|
221
|
+
|
|
124
222
|
router
|
|
125
|
-
.use('/
|
|
126
|
-
.use('/
|
|
223
|
+
.use('/v1', v1)
|
|
224
|
+
.use('/v2', v2)
|
|
225
|
+
.get('/', (req, res) => res.end('Welcome to the main router'))
|
|
226
|
+
|
|
227
|
+
http.createServer(router).listen(3000)
|
|
127
228
|
```
|
|
128
229
|
|
|
129
|
-
|
|
230
|
+
When a sub-router is used as a middleware, it will only handle requests that match its prefix. If no route matches inside the sub-router, it will automatically call `next()` to pass control back to the parent router.
|
|
231
|
+
|
|
232
|
+
### Skipping to parent router
|
|
130
233
|
|
|
131
|
-
|
|
234
|
+
Use `next('router')` to exit the current router and pass control back to the parent:
|
|
132
235
|
|
|
133
236
|
```js
|
|
134
|
-
const
|
|
237
|
+
const beta = createRouter(finalHandler)
|
|
238
|
+
|
|
239
|
+
beta.use((req, res, next) => {
|
|
240
|
+
if (!req.isBetaTester) return next('router')
|
|
241
|
+
next()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
beta.get('/feature', (req, res) => res.end('Beta feature'))
|
|
245
|
+
|
|
246
|
+
router.use('/v1', beta)
|
|
247
|
+
router.get('/v1/feature', (req, res) => res.end('Stable feature'))
|
|
135
248
|
```
|
|
136
249
|
|
|
137
250
|
## Benchmark
|
|
138
251
|
|
|
139
|
-
**express
|
|
252
|
+
With all the improvements, **router-http** is approximately 30% faster than the express router:
|
|
253
|
+
|
|
254
|
+
**express@5.2.1**
|
|
140
255
|
|
|
141
256
|
```
|
|
142
257
|
Running 30s test @ http://localhost:3000/user/123
|
|
143
258
|
8 threads and 100 connections
|
|
144
259
|
Thread Stats Avg Stdev Max +/- Stdev
|
|
145
|
-
Latency
|
|
146
|
-
Req/Sec
|
|
147
|
-
|
|
148
|
-
Requests/sec:
|
|
149
|
-
Transfer/sec:
|
|
260
|
+
Latency 1.23ms 1.40ms 96.27ms 99.61%
|
|
261
|
+
Req/Sec 10.15k 615.89 11.07k 86.24%
|
|
262
|
+
2430687 requests in 30.10s, 356.98MB read
|
|
263
|
+
Requests/sec: 80752.48
|
|
264
|
+
Transfer/sec: 11.86MB
|
|
150
265
|
```
|
|
151
266
|
|
|
152
|
-
**router-http
|
|
267
|
+
**router-http**
|
|
153
268
|
|
|
154
269
|
```
|
|
155
270
|
Running 30s test @ http://localhost:3000/user/123
|
|
156
271
|
8 threads and 100 connections
|
|
157
272
|
Thread Stats Avg Stdev Max +/- Stdev
|
|
158
|
-
Latency 1.
|
|
159
|
-
Req/Sec
|
|
160
|
-
|
|
161
|
-
Requests/sec:
|
|
162
|
-
Transfer/sec:
|
|
273
|
+
Latency 0.97ms 1.27ms 84.82ms 99.77%
|
|
274
|
+
Req/Sec 12.91k 1.07k 14.67k 71.51%
|
|
275
|
+
3092927 requests in 30.10s, 386.40MB read
|
|
276
|
+
Requests/sec: 102751.65
|
|
277
|
+
Transfer/sec: 12.84MB
|
|
163
278
|
```
|
|
164
279
|
|
|
165
|
-
See
|
|
280
|
+
See [benchmark](/benchmark) for details.
|
|
166
281
|
|
|
167
282
|
## Related
|
|
168
283
|
|
|
169
|
-
- [send-http](https://github.com/Kikobeats/send-http) –
|
|
170
|
-
- [http-body](https://github.com/Kikobeats/http-body) – Parse
|
|
171
|
-
- [http-compression](https://github.com/Kikobeats/http-compression) –
|
|
284
|
+
- [send-http](https://github.com/Kikobeats/send-http) – `res.end` with data type detection
|
|
285
|
+
- [http-body](https://github.com/Kikobeats/http-body) – Parse request body to text/json/buffer
|
|
286
|
+
- [http-compression](https://github.com/Kikobeats/http-compression) – Gzip/Brotli compression middleware
|
|
172
287
|
|
|
173
288
|
## License
|
|
174
289
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
**router-http** © [Kiko Beats](https://kikobeats.com), released under the [MIT](https://github.com/Kikobeats/router-http/blob/master/LICENSE.md) License.<br>
|
|
178
|
-
Authored and maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/Kikobeats/router-http/contributors).
|
|
290
|
+
**router-http** © [Kiko Beats](https://kikobeats.com), released under the [MIT](https://github.com/Kikobeats/router-http/blob/master/LICENSE.md) License.
|
|
179
291
|
|
|
180
|
-
|
|
292
|
+
Credits to [Luke Edwards](https://github.com/lukeed) for [Polka](https://github.com/lukeed/polka) which inspired this project.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "router-http",
|
|
3
3
|
"description": "Simple HTTP router compatible with Express",
|
|
4
4
|
"homepage": "https://github.com/Kikobeats/router-http",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "2.0.0",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"author": {
|
|
8
8
|
"email": "josefrancisco.verdu@gmail.com",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"routes"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"
|
|
32
|
+
"find-my-way": "~9.3.0",
|
|
33
|
+
"null-prototype-object": "~1.2.5"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@commitlint/cli": "latest",
|
|
@@ -54,19 +55,6 @@
|
|
|
54
55
|
"files": [
|
|
55
56
|
"src"
|
|
56
57
|
],
|
|
57
|
-
"scripts": {
|
|
58
|
-
"clean": "rm -rf node_modules",
|
|
59
|
-
"contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
60
|
-
"coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
|
|
61
|
-
"lint": "standard",
|
|
62
|
-
"postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
|
|
63
|
-
"prerelease": "npm run contributors",
|
|
64
|
-
"pretest": "npm run lint",
|
|
65
|
-
"release": "standard-version -a",
|
|
66
|
-
"release:github": "github-generate-release",
|
|
67
|
-
"release:tags": "git push --follow-tags origin HEAD:master",
|
|
68
|
-
"test": "c8 ava"
|
|
69
|
-
},
|
|
70
58
|
"license": "MIT",
|
|
71
59
|
"commitlint": {
|
|
72
60
|
"extends": [
|
|
@@ -90,5 +78,18 @@
|
|
|
90
78
|
"simple-git-hooks": {
|
|
91
79
|
"commit-msg": "npx commitlint --edit",
|
|
92
80
|
"pre-commit": "npx nano-staged"
|
|
81
|
+
},
|
|
82
|
+
"scripts": {
|
|
83
|
+
"clean": "rm -rf node_modules",
|
|
84
|
+
"contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
85
|
+
"coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
|
|
86
|
+
"lint": "standard",
|
|
87
|
+
"postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
|
|
88
|
+
"prerelease": "npm run contributors",
|
|
89
|
+
"pretest": "npm run lint",
|
|
90
|
+
"release": "standard-version -a",
|
|
91
|
+
"release:github": "github-generate-release",
|
|
92
|
+
"release:tags": "git push --follow-tags origin HEAD:master",
|
|
93
|
+
"test": "c8 ava"
|
|
93
94
|
}
|
|
94
|
-
}
|
|
95
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,128 +1,242 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const NullProtoObj = require('null-prototype-object')
|
|
4
|
+
const FindMyWay = require('find-my-way')
|
|
5
|
+
|
|
6
|
+
const HTTP_METHODS = [
|
|
7
|
+
'get',
|
|
8
|
+
'post',
|
|
9
|
+
'put',
|
|
10
|
+
'patch',
|
|
11
|
+
'delete',
|
|
12
|
+
'head',
|
|
13
|
+
'options',
|
|
14
|
+
'trace',
|
|
15
|
+
'connect'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const SLASH_CHAR_CODE = 47
|
|
19
|
+
const SYNC_ITERATION_LIMIT = 100
|
|
4
20
|
|
|
5
21
|
const requiredFinalHandler = () => {
|
|
6
22
|
throw new TypeError('You should to provide a final handler')
|
|
7
23
|
}
|
|
8
24
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
const lead = route => (route.charCodeAt(0) === 47 ? route : `/${route}`)
|
|
25
|
+
const ensureLeadingSlash = route =>
|
|
26
|
+
route.charCodeAt(0) === SLASH_CHAR_CODE ? route : `/${route}`
|
|
13
27
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
return
|
|
28
|
+
const getFirstPathSegment = pathname => {
|
|
29
|
+
const secondSlashIndex = pathname.indexOf('/', 1)
|
|
30
|
+
return secondSlashIndex > 1
|
|
31
|
+
? pathname.substring(0, secondSlashIndex)
|
|
32
|
+
: pathname
|
|
17
33
|
}
|
|
18
34
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
const parseUrl = ({ url }) => {
|
|
36
|
+
const queryIndex = url.indexOf('?', 1)
|
|
37
|
+
if (queryIndex === -1) {
|
|
38
|
+
return { pathname: url, query: null, search: null }
|
|
39
|
+
}
|
|
40
|
+
const search = url.substring(queryIndex)
|
|
41
|
+
return {
|
|
42
|
+
pathname: url.substring(0, queryIndex),
|
|
43
|
+
query: search.substring(1),
|
|
44
|
+
search
|
|
26
45
|
}
|
|
27
|
-
return obj
|
|
28
46
|
}
|
|
29
47
|
|
|
30
|
-
const
|
|
31
|
-
req.url = req.url.substring(
|
|
32
|
-
req.path = req.path.substring(
|
|
48
|
+
const mutateRequestUrl = (prefix, req) => {
|
|
49
|
+
req.url = req.url.substring(prefix.length) || '/'
|
|
50
|
+
req.path = req.path.substring(prefix.length) || '/'
|
|
33
51
|
}
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
module.exports = (finalhandler = requiredFinalHandler(), options = {}) => {
|
|
54
|
+
const router = FindMyWay({
|
|
55
|
+
...options,
|
|
56
|
+
defaultRoute: (req, res) => finalhandler(undefined, req, res)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const globalMiddlewares = []
|
|
60
|
+
const middlewaresByPath = new NullProtoObj()
|
|
61
|
+
|
|
62
|
+
const findRoute = (method, path, constraints) => {
|
|
63
|
+
const result = router.find(method, path, constraints)
|
|
64
|
+
if (result === null) {
|
|
65
|
+
return { params: {}, handlers: [] }
|
|
66
|
+
}
|
|
67
|
+
return { params: result.params, handlers: result.handler.handlers }
|
|
39
68
|
}
|
|
40
69
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* .use(one, two)
|
|
56
|
-
* .use('/v2', two)
|
|
57
|
-
*/
|
|
58
|
-
use = (page = '/', ...fns) => {
|
|
59
|
-
if (typeof page === 'function' || typeof page === 'boolean') {
|
|
60
|
-
this.#middlewares = this.#middlewares.concat(page, fns).filter(Boolean)
|
|
61
|
-
} else if (page === '/') {
|
|
62
|
-
this.#middlewares = this.#middlewares.concat(fns).filter(Boolean)
|
|
63
|
-
} else {
|
|
64
|
-
page = lead(page)
|
|
65
|
-
fns.filter(Boolean).forEach(fn => {
|
|
66
|
-
const array = this.#middlewaresBy[page] ?? []
|
|
67
|
-
// eslint-disable-next-line no-sequences
|
|
68
|
-
array.length > 0 || array.push((r, _, nxt) => (mutate(page, r), nxt()))
|
|
69
|
-
this.#middlewaresBy[page] = array.concat(fn)
|
|
70
|
-
})
|
|
70
|
+
const registerRoute = (method, path, handlers) => {
|
|
71
|
+
const routeHandler = () => {}
|
|
72
|
+
routeHandler.handlers = handlers
|
|
73
|
+
router.on(method, path, routeHandler)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const addRoute = (method, path, ...handlers) => {
|
|
77
|
+
const fns = handlers.flat().filter(Boolean)
|
|
78
|
+
if (fns.length === 0) return handler
|
|
79
|
+
|
|
80
|
+
const methods = method === '' ? HTTP_METHODS : [method]
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < methods.length; i++) {
|
|
83
|
+
registerRoute(methods[i].toUpperCase(), path, fns)
|
|
71
84
|
}
|
|
72
|
-
|
|
85
|
+
|
|
86
|
+
return handler
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const selectMiddleware = (
|
|
90
|
+
index,
|
|
91
|
+
globalLen,
|
|
92
|
+
pathLen,
|
|
93
|
+
globalMw,
|
|
94
|
+
pathMw,
|
|
95
|
+
routeHandlers
|
|
96
|
+
) => {
|
|
97
|
+
if (index < globalLen) return globalMw[index]
|
|
98
|
+
if (index < globalLen + pathLen) return pathMw[index - globalLen]
|
|
99
|
+
return routeHandlers[index - globalLen - pathLen]
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
handler = (req, res,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
const handler = (req, res, next) => {
|
|
103
|
+
const urlInfo = parseUrl(req)
|
|
104
|
+
const pathname = urlInfo.pathname
|
|
105
|
+
req.path = pathname
|
|
106
|
+
|
|
107
|
+
const pathSegment = getFirstPathSegment(pathname)
|
|
108
|
+
|
|
109
|
+
let route = findRoute(req.method, pathname)
|
|
110
|
+
|
|
111
|
+
if (route.handlers.length === 0 && req.method === 'HEAD') {
|
|
112
|
+
route = findRoute('GET', pathname)
|
|
83
113
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
|
|
115
|
+
const globalMw = globalMiddlewares
|
|
116
|
+
const pathMw = middlewaresByPath[pathSegment]
|
|
117
|
+
const routeHandlers = route.handlers.length > 0 ? route.handlers : null
|
|
118
|
+
|
|
119
|
+
if (routeHandlers !== null) {
|
|
120
|
+
req.params =
|
|
121
|
+
req.params !== undefined
|
|
122
|
+
? { ...req.params, ...route.params }
|
|
123
|
+
: route.params
|
|
124
|
+
} else {
|
|
125
|
+
req.params = req.params || {}
|
|
87
126
|
}
|
|
88
|
-
|
|
89
|
-
req.search = req.query
|
|
90
|
-
req.query = req.query
|
|
91
|
-
|
|
127
|
+
|
|
128
|
+
req.search = req.query || urlInfo.search
|
|
129
|
+
req.query = req.query || urlInfo.query
|
|
130
|
+
|
|
92
131
|
let index = 0
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
let syncCount = 0
|
|
133
|
+
|
|
134
|
+
const globalLen = globalMw.length
|
|
135
|
+
const pathLen = pathMw !== undefined ? pathMw.length : 0
|
|
136
|
+
const routeLen = routeHandlers !== null ? routeHandlers.length : 0
|
|
137
|
+
const totalMiddlewares = globalLen + pathLen + routeLen
|
|
138
|
+
|
|
139
|
+
const handleNext = err => {
|
|
140
|
+
if (err === 'router') {
|
|
141
|
+
if (next !== undefined) return next()
|
|
142
|
+
index = totalMiddlewares
|
|
143
|
+
err = undefined
|
|
144
|
+
}
|
|
145
|
+
if (err !== undefined) return finalhandler(err, req, res, next)
|
|
146
|
+
if (++syncCount > SYNC_ITERATION_LIMIT) {
|
|
147
|
+
syncCount = 0
|
|
148
|
+
return setImmediate(executeLoop)
|
|
149
|
+
}
|
|
150
|
+
executeLoop()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const executeLoop = () => {
|
|
154
|
+
if (index < totalMiddlewares) {
|
|
155
|
+
if (res.writableEnded) return
|
|
156
|
+
|
|
157
|
+
const currentIndex = index++
|
|
158
|
+
const middleware = selectMiddleware(
|
|
159
|
+
currentIndex,
|
|
160
|
+
globalLen,
|
|
161
|
+
pathLen,
|
|
162
|
+
globalMw,
|
|
163
|
+
pathMw,
|
|
164
|
+
routeHandlers
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = middleware(req, res, handleNext)
|
|
169
|
+
if (
|
|
170
|
+
result !== null &&
|
|
171
|
+
result !== undefined &&
|
|
172
|
+
typeof result.then === 'function'
|
|
173
|
+
) {
|
|
174
|
+
result.then(undefined, handleNext)
|
|
115
175
|
}
|
|
116
|
-
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
handleNext(err)
|
|
178
|
+
}
|
|
179
|
+
return
|
|
180
|
+
}
|
|
117
181
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
182
|
+
if (res.writableEnded) return
|
|
183
|
+
if (next !== undefined) return next()
|
|
184
|
+
finalhandler(undefined, req, res, handleNext)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
executeLoop()
|
|
121
188
|
}
|
|
122
|
-
}
|
|
123
189
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
190
|
+
handler.use = (path = '/', ...fns) => {
|
|
191
|
+
if (typeof path === 'function' || typeof path === 'boolean') {
|
|
192
|
+
const middlewares = [path, ...fns].filter(Boolean)
|
|
193
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
194
|
+
globalMiddlewares.push(middlewares[i])
|
|
195
|
+
}
|
|
196
|
+
} else if (path === '/') {
|
|
197
|
+
const middlewares = fns.filter(Boolean)
|
|
198
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
199
|
+
globalMiddlewares.push(middlewares[i])
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
const normalizedPath = ensureLeadingSlash(path)
|
|
203
|
+
const middlewares = fns.filter(Boolean)
|
|
204
|
+
|
|
205
|
+
if (middlewares.length > 0) {
|
|
206
|
+
let pathMiddlewares = middlewaresByPath[normalizedPath]
|
|
207
|
+
|
|
208
|
+
if (pathMiddlewares === undefined) {
|
|
209
|
+
pathMiddlewares = []
|
|
210
|
+
pathMiddlewares.push((req, _, next) => {
|
|
211
|
+
mutateRequestUrl(normalizedPath, req)
|
|
212
|
+
next()
|
|
213
|
+
})
|
|
214
|
+
middlewaresByPath[normalizedPath] = pathMiddlewares
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
218
|
+
pathMiddlewares.push(middlewares[i])
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return handler
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
handler.all = addRoute.bind(null, '')
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < HTTP_METHODS.length; i++) {
|
|
228
|
+
const method = HTTP_METHODS[i]
|
|
229
|
+
handler[method] = addRoute.bind(null, method)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
handler.del = handler.delete
|
|
233
|
+
|
|
234
|
+
handler.prettyPrint = (...args) => router.prettyPrint(...args)
|
|
235
|
+
|
|
236
|
+
Object.defineProperty(handler, 'routes', {
|
|
237
|
+
get: () => router.routes,
|
|
238
|
+
enumerable: true
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return handler
|
|
128
242
|
}
|