expediate 1.0.4 → 1.0.6
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 +138 -0
- package/CONTRIBUTING.md +150 -0
- package/LICENSE +16 -16
- package/README.md +330 -444
- package/dist/apis.d.ts +504 -27
- package/dist/apis.d.ts.map +1 -1
- package/dist/apis.js +618 -107
- package/dist/apis.js.map +1 -1
- package/dist/cjs/index.js +4066 -0
- package/dist/cjs/package.json +1 -0
- package/dist/git.d.ts +72 -9
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +129 -74
- package/dist/git.js.map +1 -1
- package/dist/http-objects.d.ts +26 -0
- package/dist/http-objects.d.ts.map +1 -0
- package/dist/http-objects.js +588 -0
- package/dist/http-objects.js.map +1 -0
- package/dist/index.d.ts +18 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -24
- package/dist/index.js.map +1 -1
- package/dist/jwt-auth.d.ts +158 -57
- package/dist/jwt-auth.d.ts.map +1 -1
- package/dist/jwt-auth.js +447 -207
- package/dist/jwt-auth.js.map +1 -1
- package/dist/middleware.d.ts +476 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +647 -0
- package/dist/middleware.js.map +1 -0
- package/dist/mimetypes.json +882 -1
- package/dist/misc.d.ts +268 -25
- package/dist/misc.d.ts.map +1 -1
- package/dist/misc.js +449 -168
- package/dist/misc.js.map +1 -1
- package/dist/openapi.d.ts +433 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +624 -0
- package/dist/openapi.js.map +1 -0
- package/dist/router-types.d.ts +760 -0
- package/dist/router-types.d.ts.map +1 -0
- package/dist/router-types.js +23 -0
- package/dist/router-types.js.map +1 -0
- package/dist/router.d.ts +37 -201
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +502 -244
- package/dist/router.js.map +1 -1
- package/dist/static.d.ts +3 -3
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +164 -105
- package/dist/static.js.map +1 -1
- package/docs/THREAT_MODEL.md +52 -0
- package/docs/api-builder-v2-design.md +644 -0
- package/docs/api-builder-v3-design.md +397 -0
- package/docs/api-builder.md +454 -0
- package/docs/benchmark.md +27 -0
- package/docs/body-parsing.md +223 -0
- package/docs/errors.md +359 -0
- package/docs/expediate.png +0 -0
- package/docs/git.md +139 -0
- package/docs/jwt-auth.md +251 -0
- package/docs/logo.svg +12 -0
- package/docs/middleware.md +264 -0
- package/docs/openapi.md +180 -0
- package/docs/router.md +356 -0
- package/docs/static.md +128 -0
- package/docs/wiki.json +123 -0
- package/package.json +47 -8
- package/.npmignore +0 -16
package/dist/router.js
CHANGED
|
@@ -19,43 +19,10 @@
|
|
|
19
19
|
* DEALINGS IN THE SOFTWARE.
|
|
20
20
|
*/
|
|
21
21
|
'use strict';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
-
}
|
|
28
|
-
Object.defineProperty(o, k2, desc);
|
|
29
|
-
}) : (function(o, m, k, k2) {
|
|
30
|
-
if (k2 === undefined) k2 = k;
|
|
31
|
-
o[k2] = m[k];
|
|
32
|
-
}));
|
|
33
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
-
}) : function(o, v) {
|
|
36
|
-
o["default"] = v;
|
|
37
|
-
});
|
|
38
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
-
var ownKeys = function(o) {
|
|
40
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
-
var ar = [];
|
|
42
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
-
return ar;
|
|
44
|
-
};
|
|
45
|
-
return ownKeys(o);
|
|
46
|
-
};
|
|
47
|
-
return function (mod) {
|
|
48
|
-
if (mod && mod.__esModule) return mod;
|
|
49
|
-
var result = {};
|
|
50
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
-
__setModuleDefault(result, mod);
|
|
52
|
-
return result;
|
|
53
|
-
};
|
|
54
|
-
})();
|
|
55
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
-
const http = __importStar(require("http"));
|
|
57
|
-
const https = __importStar(require("https"));
|
|
58
|
-
const misc_1 = require("./misc");
|
|
22
|
+
import * as http from 'http';
|
|
23
|
+
import * as https from 'https';
|
|
24
|
+
import * as http2 from 'http2';
|
|
25
|
+
import { updateHttpObjects } from './http-objects.js';
|
|
59
26
|
// ---------------------------------------------------------------------------
|
|
60
27
|
// Pattern compilation
|
|
61
28
|
// ---------------------------------------------------------------------------
|
|
@@ -69,7 +36,43 @@ const misc_1 = require("./misc");
|
|
|
69
36
|
* @returns `true` if the pattern should be treated as a glob.
|
|
70
37
|
*/
|
|
71
38
|
function isGlobPattern(pattern) {
|
|
72
|
-
|
|
39
|
+
// Walk character-by-character, skipping over :name(constraint) segments so
|
|
40
|
+
// that regex metacharacters (e.g. '?') inside inline constraints are not
|
|
41
|
+
// mistaken for glob wildcards.
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < pattern.length) {
|
|
44
|
+
const ch = pattern[i];
|
|
45
|
+
if (ch === '\\') {
|
|
46
|
+
i += 2;
|
|
47
|
+
continue;
|
|
48
|
+
} // escaped — skip next char
|
|
49
|
+
if (ch === ':') {
|
|
50
|
+
i++;
|
|
51
|
+
while (i < pattern.length && /\w/.test(pattern[i]))
|
|
52
|
+
i++; // skip param name
|
|
53
|
+
if (i < pattern.length && pattern[i] === '(') {
|
|
54
|
+
// Skip balanced constraint parens so their '?' / '*' are not counted.
|
|
55
|
+
let depth = 1;
|
|
56
|
+
i++;
|
|
57
|
+
while (i < pattern.length && depth > 0) {
|
|
58
|
+
if (pattern[i] === '\\') {
|
|
59
|
+
i += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (pattern[i] === '(')
|
|
63
|
+
depth++;
|
|
64
|
+
else if (pattern[i] === ')')
|
|
65
|
+
depth--;
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === '*' || ch === '?')
|
|
72
|
+
return true;
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
73
76
|
}
|
|
74
77
|
/**
|
|
75
78
|
* Compile a `.gitignore`-style glob string into a prefix-anchored `RegExp`.
|
|
@@ -93,7 +96,7 @@ function isGlobPattern(pattern) {
|
|
|
93
96
|
* compileGlob('/api/*') .test('/api/users/123'); // false
|
|
94
97
|
* ```
|
|
95
98
|
*/
|
|
96
|
-
function compileGlob(glob) {
|
|
99
|
+
function compileGlob(glob, exact = false) {
|
|
97
100
|
// Escape all regex special characters, leaving our wildcard characters intact.
|
|
98
101
|
let src = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
99
102
|
// Replace in order: `**` must be handled before `*`.
|
|
@@ -102,37 +105,118 @@ function compileGlob(glob) {
|
|
|
102
105
|
.replace(/\*/g, '[^/]*') // single-segment wildcard
|
|
103
106
|
.replace(/\?/g, '[^/]') // single-character wildcard
|
|
104
107
|
.replace(/\x00GLOBSTAR\x00/g, '.*'); // cross-segment wildcard
|
|
105
|
-
return new RegExp('^' + src);
|
|
108
|
+
return new RegExp('^' + src + (exact ? '$' : ''));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
/**
|
|
112
|
+
* Extract the content between a balanced pair of parentheses starting at
|
|
113
|
+
* `openIdx` in `str`, skipping backslash-escaped characters.
|
|
114
|
+
*
|
|
115
|
+
* Returns the inner pattern and the index of the closing `)` so callers can
|
|
116
|
+
* inspect any literal suffix that follows the constraint (e.g. `\.txt` in
|
|
117
|
+
* `:name([\w-]+)\.txt`).
|
|
118
|
+
*
|
|
119
|
+
* @param str - The full segment string, e.g. `':id(\\d+)'`.
|
|
120
|
+
* @param openIdx - Index of the opening `(`.
|
|
121
|
+
* @returns `{ pattern, closeIdx }` — the inner pattern string and the index
|
|
122
|
+
* of the matching `)`.
|
|
123
|
+
* @throws {SyntaxError} When parentheses are unbalanced.
|
|
124
|
+
*/
|
|
125
|
+
function extractInlinePattern(str, openIdx) {
|
|
126
|
+
let depth = 0;
|
|
127
|
+
let i = openIdx;
|
|
128
|
+
for (; i < str.length; i++) {
|
|
129
|
+
if (str[i] === '\\') {
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
} // skip escape sequences
|
|
133
|
+
if (str[i] === '(') {
|
|
134
|
+
depth++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (str[i] === ')') {
|
|
138
|
+
depth--;
|
|
139
|
+
if (depth === 0)
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (depth !== 0)
|
|
144
|
+
throw new SyntaxError(`Unbalanced parentheses in route segment '${str}'`);
|
|
145
|
+
return { pattern: str.slice(openIdx + 1, i), closeIdx: i };
|
|
106
146
|
}
|
|
107
147
|
/**
|
|
108
148
|
* Compile a plain path string with optional `:name` parameter segments into a
|
|
109
149
|
* prefix-anchored `RegExp` that uses named capture groups.
|
|
110
150
|
*
|
|
111
|
-
* Each `:name` segment is converted to
|
|
112
|
-
*
|
|
113
|
-
* Literal segments are escaped and matched exactly.
|
|
151
|
+
* **Basic parameters** — Each `:name` segment is converted to
|
|
152
|
+
* `(?<name>[^/]+)`, matching any non-slash sequence.
|
|
114
153
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
154
|
+
* **Inline constraints** — A parameter may optionally be followed by a
|
|
155
|
+
* parenthesised regex pattern: `:name(pattern)`. The pattern replaces the
|
|
156
|
+
* default `[^/]+` body, so only paths where that segment matches the
|
|
157
|
+
* constraint will be routed to the handler.
|
|
117
158
|
*
|
|
118
|
-
*
|
|
159
|
+
* ```
|
|
160
|
+
* :id → (?<id>[^/]+) (any non-slash value)
|
|
161
|
+
* :id(\d+) → (?<id>\d+) (digits only)
|
|
162
|
+
* :slug([\w-]+) → (?<slug>[\w-]+) (word chars and hyphens)
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* Literal segments are regex-escaped and matched exactly. The expression
|
|
166
|
+
* matches up to a segment boundary (`/` or end-of-string) so that `/users`
|
|
167
|
+
* never inadvertently matches `/users-admin`.
|
|
168
|
+
*
|
|
169
|
+
* @param path - A plain path string such as `'/users/:id(\d+)/posts'`.
|
|
119
170
|
* @returns A prefix-anchored `RegExp` with named groups for each parameter.
|
|
171
|
+
* @throws {SyntaxError} When an inline constraint is malformed, contains
|
|
172
|
+
* named capture groups (which conflict with the outer wrapper), or produces
|
|
173
|
+
* an invalid `RegExp`.
|
|
120
174
|
*
|
|
121
175
|
* @example
|
|
122
176
|
* ```ts
|
|
123
|
-
* const re = compilePlainPath('/users/:id');
|
|
124
|
-
* re.
|
|
177
|
+
* const re = compilePlainPath('/users/:id(\\d+)');
|
|
178
|
+
* re.test('/users/42'); // true
|
|
179
|
+
* re.test('/users/abc'); // false
|
|
180
|
+
* re.exec('/users/7')?.groups; // { id: '7' }
|
|
125
181
|
* ```
|
|
126
182
|
*/
|
|
127
|
-
function compilePlainPath(path) {
|
|
183
|
+
function compilePlainPath(path, exact = false) {
|
|
128
184
|
const segments = path.split('/').filter((s) => s.length > 0);
|
|
129
185
|
const src = segments
|
|
130
|
-
.map((seg) =>
|
|
131
|
-
|
|
132
|
-
|
|
186
|
+
.map((seg) => {
|
|
187
|
+
if (!seg.startsWith(':'))
|
|
188
|
+
return seg.replace(/[.+^${}()|[\]\\]/g, '\\$&'); // escaped literal
|
|
189
|
+
// Parameter segment: extract name and optional inline constraint.
|
|
190
|
+
const parenIdx = seg.indexOf('(', 1);
|
|
191
|
+
if (parenIdx === -1) {
|
|
192
|
+
// Plain :name — match any non-slash sequence.
|
|
193
|
+
return `(?<${seg.slice(1)}>[^/]+)`;
|
|
194
|
+
}
|
|
195
|
+
// :name(pattern) — optionally followed by a literal suffix, e.g. \.txt
|
|
196
|
+
const name = seg.slice(1, parenIdx);
|
|
197
|
+
if (!name)
|
|
198
|
+
throw new SyntaxError(`Route parameter missing name before '(' in segment '${seg}'`);
|
|
199
|
+
const { pattern, closeIdx } = extractInlinePattern(seg, parenIdx);
|
|
200
|
+
// Named capture groups inside the constraint conflict with the outer
|
|
201
|
+
// (?<name>…) wrapper and would cause duplicate-group errors.
|
|
202
|
+
if (/\(\?<[^>]+>/.test(pattern))
|
|
203
|
+
throw new SyntaxError(`Inline constraint for ':${name}' must not contain named capture groups.`);
|
|
204
|
+
// Any literal characters after the closing ')' are regex-escaped and
|
|
205
|
+
// appended — e.g. ':name([\\w-]+)\\.txt' → '(?<name>[\\w-]+)\\.txt'.
|
|
206
|
+
const suffix = seg.slice(closeIdx + 1);
|
|
207
|
+
const escapedSuffix = suffix.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
208
|
+
return `(?<${name}>${pattern})${escapedSuffix}`;
|
|
209
|
+
})
|
|
133
210
|
.join('/');
|
|
134
|
-
//
|
|
135
|
-
|
|
211
|
+
// Validate and return — surface any regex syntax errors as SyntaxError.
|
|
212
|
+
try {
|
|
213
|
+
return new RegExp(exact
|
|
214
|
+
? '^/?' + src + (src ? '/?' : '') + '$'
|
|
215
|
+
: '^/?' + src + '(?=/|$)');
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
throw new SyntaxError(`Invalid inline regex constraint in path '${path}': ${e.message}`, { cause: e });
|
|
219
|
+
}
|
|
136
220
|
}
|
|
137
221
|
/**
|
|
138
222
|
* Convert any supported path pattern into the single canonical `RegExp`
|
|
@@ -141,18 +225,18 @@ function compilePlainPath(path) {
|
|
|
141
225
|
* | Input type | Strategy |
|
|
142
226
|
* |--------------|-------------------------------------------------------|
|
|
143
227
|
* | Glob string | {@link compileGlob} — `.gitignore`-style wildcards |
|
|
144
|
-
* | Plain string | {@link compilePlainPath} — `:name` → named groups
|
|
228
|
+
* | Plain string | {@link compilePlainPath} — `:name` or `:name(re)` → named groups |
|
|
145
229
|
* | `RegExp` | Used as-is; named groups are surfaced as params |
|
|
146
230
|
*
|
|
147
231
|
* @param path - The raw path pattern supplied by the caller.
|
|
148
232
|
* @returns A `RegExp` suitable for use in `matchRouteLayer`.
|
|
149
233
|
*/
|
|
150
|
-
function compilePattern(path) {
|
|
234
|
+
function compilePattern(path, exact = false) {
|
|
151
235
|
if (path instanceof RegExp)
|
|
152
236
|
return path;
|
|
153
237
|
if (isGlobPattern(path))
|
|
154
|
-
return compileGlob(path);
|
|
155
|
-
return compilePlainPath(path);
|
|
238
|
+
return compileGlob(path, exact);
|
|
239
|
+
return compilePlainPath(path, exact);
|
|
156
240
|
}
|
|
157
241
|
// ---------------------------------------------------------------------------
|
|
158
242
|
// Layer construction
|
|
@@ -179,7 +263,15 @@ function compilePattern(path) {
|
|
|
179
263
|
function buildRouteLayer(method, path, middleware, stripPath) {
|
|
180
264
|
if (typeof middleware !== 'function')
|
|
181
265
|
throw new TypeError('Incorrect middleware type: expected a function');
|
|
182
|
-
|
|
266
|
+
// Reject RegExp patterns with the global (g) or sticky (y) flag.
|
|
267
|
+
// Both flags make RegExp.exec()/test() stateful: lastIndex advances after
|
|
268
|
+
// each match, so the same regex object alternates match/no-match across
|
|
269
|
+
// requests, causing intermittent 404s that are nearly impossible to debug.
|
|
270
|
+
// Express 4 silently allowed this (a known footgun); we reject it early.
|
|
271
|
+
if (path instanceof RegExp && (path.global || path.sticky))
|
|
272
|
+
throw new TypeError(`Route RegExp /${path.source}/${path.flags} must not use the g (global) or y (sticky) flag — ` +
|
|
273
|
+
'these flags make exec() stateful and cause intermittent routing failures.');
|
|
274
|
+
return { method, path, regex: compilePattern(path, !stripPath), stripPath, middleware };
|
|
183
275
|
}
|
|
184
276
|
// ---------------------------------------------------------------------------
|
|
185
277
|
// Layer matching
|
|
@@ -205,12 +297,17 @@ function buildRouteLayer(method, path, middleware, stripPath) {
|
|
|
205
297
|
* `false` otherwise.
|
|
206
298
|
*/
|
|
207
299
|
function matchRouteLayer(layer, req, path) {
|
|
208
|
-
|
|
300
|
+
// A HEAD request is served by a matching GET layer (RFC 7231 §4.3.2); Node
|
|
301
|
+
// suppresses the response body for HEAD automatically, so the GET handler can
|
|
302
|
+
// run unchanged.
|
|
303
|
+
if (layer.method &&
|
|
304
|
+
layer.method !== req.method &&
|
|
305
|
+
!(req.method === 'HEAD' && layer.method === 'GET'))
|
|
209
306
|
return false;
|
|
210
307
|
const m = layer.regex.exec(path);
|
|
211
308
|
if (m === null)
|
|
212
309
|
return false;
|
|
213
|
-
const captured = m.groups ?? {};
|
|
310
|
+
const captured = (m.groups) ?? {};
|
|
214
311
|
// Only rewrite req.path for prefix-style (use) registrations.
|
|
215
312
|
// Exact-method routes leave req.path intact so chained middlewares
|
|
216
313
|
// sharing the same path each see the full, unmodified path.
|
|
@@ -221,123 +318,25 @@ function matchRouteLayer(layer, req, path) {
|
|
|
221
318
|
Object.assign(req.params, captured);
|
|
222
319
|
return true;
|
|
223
320
|
}
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
// HTTP object augmentation
|
|
226
|
-
// ---------------------------------------------------------------------------
|
|
227
321
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* -
|
|
238
|
-
* -
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* **Helpers added to `res`:**
|
|
242
|
-
* - `send(data?)` — write `data` and end the response.
|
|
243
|
-
* - `status(code, headers?)` — set the status code and optional headers.
|
|
244
|
-
* - `redirect(url)` — issue a 302 redirect.
|
|
245
|
-
* - `cookie(name, val, opts)` — append a `Set-Cookie` header.
|
|
246
|
-
*
|
|
247
|
-
* @param req - The raw incoming message to augment.
|
|
248
|
-
* @param res - The raw server response to augment.
|
|
322
|
+
* Test whether a layer's path pattern matches the given path string,
|
|
323
|
+
* **ignoring the HTTP method**. Does NOT mutate `req`.
|
|
324
|
+
*
|
|
325
|
+
* Used exclusively for **405 Method Not Allowed** detection: when
|
|
326
|
+
* `matchRouteLayer` returns `false` due to a method mismatch, calling this
|
|
327
|
+
* function lets the dispatcher confirm that the path itself is registered
|
|
328
|
+
* (just under a different method) so it can respond with 405 and an
|
|
329
|
+
* `Allow` header instead of the generic 404.
|
|
330
|
+
*
|
|
331
|
+
* @param layer - The layer whose path pattern to test.
|
|
332
|
+
* @param path - The current value of `req.path`.
|
|
333
|
+
* @returns `true` when the path pattern matches, regardless of method.
|
|
249
334
|
*/
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
const rRes = res;
|
|
253
|
-
if (rReq.queries)
|
|
254
|
-
return; // Already augmented.
|
|
255
|
-
rReq.queries = {};
|
|
256
|
-
const qry = new URL(`http://${req.headers.host}${req.url}`);
|
|
257
|
-
rReq.originalUrl = req.url;
|
|
258
|
-
rReq.path = qry.pathname;
|
|
259
|
-
// Parse URL query parameters.
|
|
260
|
-
const urlParams = {};
|
|
261
|
-
for (const [key, value] of qry.searchParams.entries())
|
|
262
|
-
urlParams[key] = value;
|
|
263
|
-
rReq.queries.url = urlParams;
|
|
264
|
-
rReq.params = { ...urlParams };
|
|
265
|
-
// Parse cookies.
|
|
266
|
-
if (rReq.cookies == null) {
|
|
267
|
-
rReq.cookies = {};
|
|
268
|
-
if (req.headers.cookie) {
|
|
269
|
-
for (const raw of req.headers.cookie.split(';')) {
|
|
270
|
-
const eqIdx = raw.indexOf('=');
|
|
271
|
-
if (eqIdx === -1)
|
|
272
|
-
continue;
|
|
273
|
-
rReq.cookies[raw.slice(0, eqIdx).trim()] = raw.slice(eqIdx + 1).trim();
|
|
274
|
-
// TODO: 's:' prefix → signed cookie, 'j:' prefix → JSON cookie
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
rReq.json = (opts) => {
|
|
279
|
-
return new Promise((resolve, reject) => {
|
|
280
|
-
(0, misc_1.readReqBody)(rReq, { limit: opts?.limit ?? '100kb', inflate: opts?.inflate ?? true, reviver: null, strict: false }, 'application/json')
|
|
281
|
-
.then(ret => {
|
|
282
|
-
if (ret == null)
|
|
283
|
-
return resolve(null);
|
|
284
|
-
const charset = (0, misc_1.extractCharset)(ret.mimetype);
|
|
285
|
-
try {
|
|
286
|
-
rReq.body = JSON.parse(ret.content.toString(charset), opts?.reviver ?? undefined);
|
|
287
|
-
return resolve(rReq.body);
|
|
288
|
-
}
|
|
289
|
-
catch (ex) {
|
|
290
|
-
reject({ status: 500, message: ex.message });
|
|
291
|
-
}
|
|
292
|
-
})
|
|
293
|
-
.catch(err => reject(err));
|
|
294
|
-
});
|
|
295
|
-
};
|
|
296
|
-
rRes.setHeader('X-Powered-By', 'Expediate');
|
|
297
|
-
rRes.send = (data) => {
|
|
298
|
-
if (data)
|
|
299
|
-
res.write(data);
|
|
300
|
-
res.end();
|
|
301
|
-
};
|
|
302
|
-
rRes.json = (data) => {
|
|
303
|
-
res.write(JSON.stringify(data));
|
|
304
|
-
res.end();
|
|
305
|
-
};
|
|
306
|
-
rRes.status = (code, headers) => {
|
|
307
|
-
res.statusCode = code;
|
|
308
|
-
if (headers)
|
|
309
|
-
for (const [k, v] of Object.entries(headers))
|
|
310
|
-
res.setHeader(k, v);
|
|
311
|
-
return rRes;
|
|
312
|
-
};
|
|
313
|
-
rRes.redirect = (url) => {
|
|
314
|
-
res.setHeader('location', url);
|
|
315
|
-
res.writeHead(302);
|
|
316
|
-
res.write(`Found. Redirecting to ${url}`);
|
|
317
|
-
res.end();
|
|
318
|
-
};
|
|
319
|
-
rRes.cookie = (name, value, options) => {
|
|
320
|
-
const opts = options ?? {};
|
|
321
|
-
if (opts.signed && !req.secret)
|
|
322
|
-
throw new Error('cookieParser("secret") required for signed cookies');
|
|
323
|
-
let val = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value);
|
|
324
|
-
if (opts.signed)
|
|
325
|
-
val = 's:' + val; // sign() integration point
|
|
326
|
-
let txt = `${name}=${val}`;
|
|
327
|
-
if (opts.maxAge != null) {
|
|
328
|
-
opts.expires = new Date(Date.now() + opts.maxAge);
|
|
329
|
-
opts.maxAge = Math.floor(opts.maxAge / 1000);
|
|
330
|
-
txt += `; Max-Age=${opts.maxAge}`;
|
|
331
|
-
}
|
|
332
|
-
if (opts.expires)
|
|
333
|
-
txt += `; Expires=${opts.expires.toUTCString()}`;
|
|
334
|
-
txt += `; Path=${opts.path ?? '/'}`;
|
|
335
|
-
res.setHeader('Set-Cookie', txt);
|
|
336
|
-
return rRes;
|
|
337
|
-
};
|
|
335
|
+
function pathMatchesLayer(layer, path) {
|
|
336
|
+
return layer.regex.test(path);
|
|
338
337
|
}
|
|
339
338
|
// ---------------------------------------------------------------------------
|
|
340
|
-
// Route registration
|
|
339
|
+
// Route registration helpers
|
|
341
340
|
// ---------------------------------------------------------------------------
|
|
342
341
|
/**
|
|
343
342
|
* Recursively resolve a `MiddlewareArg` value down to individual `Middleware`
|
|
@@ -352,11 +351,7 @@ function updateHttpObjects(req, res) {
|
|
|
352
351
|
* @param method - HTTP method string or `null` for method-agnostic layers.
|
|
353
352
|
* @param path - URL pattern (plain string, glob, or `RegExp`).
|
|
354
353
|
* @param arg - The middleware value(s) to register.
|
|
355
|
-
* @param stripPath - Forwarded to `buildRouteLayer`.
|
|
356
|
-
* style registrations (`use`) so the matched prefix
|
|
357
|
-
* is stripped from `req.path`, and `false` for exact-method
|
|
358
|
-
* routes so that chained middlewares sharing the same path
|
|
359
|
-
* each see the unmodified path.
|
|
354
|
+
* @param stripPath - Forwarded to `buildRouteLayer`.
|
|
360
355
|
* @throws {TypeError} When `arg` contains a value that cannot be resolved to
|
|
361
356
|
* a `Middleware` function.
|
|
362
357
|
*/
|
|
@@ -368,15 +363,34 @@ function registerRoute(routes, method, path, arg, stripPath) {
|
|
|
368
363
|
else if (typeof arg === 'function') {
|
|
369
364
|
routes.push(buildRouteLayer(method, path, arg, stripPath));
|
|
370
365
|
}
|
|
371
|
-
else if (arg && typeof arg.listener === 'function') {
|
|
366
|
+
else if (arg && typeof (arg).listener === 'function') {
|
|
372
367
|
// Router instance — unwrap its listener.
|
|
373
|
-
routes.push(buildRouteLayer(method, path, arg.listener, stripPath));
|
|
368
|
+
routes.push(buildRouteLayer(method, path, (arg).listener, stripPath));
|
|
374
369
|
}
|
|
375
370
|
else {
|
|
376
371
|
throw new TypeError('Unexpected value registered as middleware: expected a Middleware ' +
|
|
377
372
|
'function, a Router instance, or an array of either');
|
|
378
373
|
}
|
|
379
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* If `arg` is a `Router` instance, return its configured {@link Router.prefix};
|
|
377
|
+
* otherwise return `undefined`.
|
|
378
|
+
*
|
|
379
|
+
* Used by `router.use()` to infer the mount path when no explicit path is
|
|
380
|
+
* provided:
|
|
381
|
+
* ```ts
|
|
382
|
+
* const v1 = createRouter('/api/v1');
|
|
383
|
+
* app.use(v1); // prefix '/api/v1' is inferred automatically
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* @param arg - The first argument passed to `router.use()`.
|
|
387
|
+
* @returns The router's prefix string, or `undefined`.
|
|
388
|
+
*/
|
|
389
|
+
function extractRouterPrefix(arg) {
|
|
390
|
+
if (Array.isArray(arg) || typeof arg === 'function')
|
|
391
|
+
return undefined;
|
|
392
|
+
return (arg).prefix;
|
|
393
|
+
}
|
|
380
394
|
// ---------------------------------------------------------------------------
|
|
381
395
|
// Router factory
|
|
382
396
|
// ---------------------------------------------------------------------------
|
|
@@ -388,79 +402,231 @@ function registerRoute(routes, method, path, arg, stripPath) {
|
|
|
388
402
|
* mounted inside another router:
|
|
389
403
|
* ```ts
|
|
390
404
|
* parent.use('/api', child);
|
|
405
|
+
* // or, if child has a prefix:
|
|
406
|
+
* parent.use(child);
|
|
407
|
+
* ```
|
|
408
|
+
*
|
|
409
|
+
* **Prefix shorthand:**
|
|
410
|
+
* Pass a path string as the first argument to associate a prefix with this
|
|
411
|
+
* router. The prefix is used automatically when the router is mounted via
|
|
412
|
+
* `parent.use(child)`:
|
|
413
|
+
* ```ts
|
|
414
|
+
* const v1 = createRouter('/api/v1');
|
|
415
|
+
* v1.get('/users', handler); // handler is reached at /api/v1/users
|
|
416
|
+
* app.use(v1); // same as app.use('/api/v1', v1)
|
|
391
417
|
* ```
|
|
392
418
|
*
|
|
393
419
|
* **Path patterns** accepted by all route-registration methods:
|
|
394
420
|
* - Plain strings with optional `:name` segments — e.g. `'/users/:id'`.
|
|
395
|
-
* Each `:name` is compiled to a named capture group and exposed in
|
|
396
|
-
* `req.params` on a match.
|
|
397
421
|
* - Glob strings following `.gitignore` rules — e.g. `'/**\/*.php'`.
|
|
398
|
-
* Supported wildcards: `?` (one non-slash char), `*` (any non-slash chars),
|
|
399
|
-
* `**` (any chars, including slashes).
|
|
400
422
|
* - `RegExp` objects — used directly; named groups become route parameters.
|
|
401
423
|
*
|
|
402
|
-
* **
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* - A `Router` instance (its `listener` is registered automatically).
|
|
406
|
-
* - An array of either of the above.
|
|
424
|
+
* **Error handling:**
|
|
425
|
+
* Register a global error handler with `router.onError()`. It is called
|
|
426
|
+
* when any middleware throws, rejects, or calls `next(err)`.
|
|
407
427
|
*
|
|
408
|
-
* **
|
|
409
|
-
*
|
|
410
|
-
* invoking middleware. Nested routers therefore only see the remaining suffix.
|
|
411
|
-
* - `all` / `get` / `post` / `put` / `delete` / `patch` — leave `req.path` intact
|
|
412
|
-
* so that multiple middlewares registered for the same exact path can each
|
|
413
|
-
* match and be invoked in sequence via `next()`.
|
|
428
|
+
* **Graceful shutdown:**
|
|
429
|
+
* Call `router.shutdown()` to stop the server created by `router.listen()`.
|
|
414
430
|
*
|
|
415
|
-
* @
|
|
416
|
-
*
|
|
431
|
+
* @param prefixOrOpts - Optional path prefix string (e.g. `'/api/v1'`) **or**
|
|
432
|
+
* an {@link RouterOptions} object.
|
|
433
|
+
* @param opts - Options when `prefixOrOpts` is a string.
|
|
434
|
+
* @returns A fully initialised `Router`.
|
|
417
435
|
*
|
|
418
436
|
* @example
|
|
419
437
|
* ```ts
|
|
420
|
-
* const
|
|
421
|
-
*
|
|
422
|
-
*
|
|
438
|
+
* const app = createRouter({ secret: process.env.COOKIE_SECRET, timeout: 30_000 });
|
|
439
|
+
*
|
|
440
|
+
* const v1 = createRouter('/api/v1');
|
|
441
|
+
* v1.get('/users', handler);
|
|
423
442
|
*
|
|
424
|
-
*
|
|
425
|
-
* app.
|
|
426
|
-
* app.get('/users/:id', requireAuth, getUser); // multiple middleware
|
|
427
|
-
* app.get('/**\/*.php', (req, res) => // glob pattern
|
|
428
|
-
* res.status(403).send('Forbidden'));
|
|
443
|
+
* app.use(v1);
|
|
444
|
+
* app.onError((err, _req, res) => res.status(500).json({ error: String(err) }));
|
|
429
445
|
*
|
|
430
|
-
*
|
|
446
|
+
* // Custom 404: register a catch-all as the LAST layer. Because layers match
|
|
447
|
+
* // in registration order, it only runs when nothing earlier claimed the
|
|
448
|
+
* // request. Use `all('/**', …)` (glob) to match any method and path.
|
|
449
|
+
* app.all('/**', (_req, res) => res.status(404).json({ error: 'Not Found' }));
|
|
450
|
+
*
|
|
451
|
+
* app.listen(3000, () => console.log('Listening'));
|
|
452
|
+
* process.on('SIGTERM', () => app.shutdown(10_000));
|
|
431
453
|
* ```
|
|
432
454
|
*/
|
|
433
|
-
function createRouter() {
|
|
455
|
+
function createRouter(prefixOrOpts, opts) {
|
|
456
|
+
// Resolve overloaded first argument.
|
|
457
|
+
const routerPrefix = typeof prefixOrOpts === 'string' ? prefixOrOpts : undefined;
|
|
458
|
+
const options = typeof prefixOrOpts === 'object' ? prefixOrOpts : (opts ?? {});
|
|
459
|
+
const secret = options.secret;
|
|
460
|
+
const timeoutMs = options.timeout;
|
|
461
|
+
const trustProxy = options.trustProxy ?? false;
|
|
434
462
|
const routes = [];
|
|
463
|
+
/** Ordered error-handling middlewares registered via `router.error()`. */
|
|
464
|
+
const errorHandlers = [];
|
|
465
|
+
/** Terminal fallback registered via `router.onError()`, or `undefined`. */
|
|
466
|
+
let errorHandler;
|
|
467
|
+
/** Server created by `router.listen()`, used by `router.shutdown()`. */
|
|
468
|
+
let activeServer = null;
|
|
469
|
+
/** All open sockets tracked for forced teardown on shutdown. */
|
|
470
|
+
const activeSockets = new Set();
|
|
471
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
472
|
+
// Core dispatch listener
|
|
473
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
435
474
|
/**
|
|
436
475
|
* Core dispatch function. Walks the route table in registration order and
|
|
437
|
-
* invokes the first layer that matches the current request.
|
|
438
|
-
*
|
|
476
|
+
* invokes the first layer that matches the current request.
|
|
477
|
+
*
|
|
478
|
+
* - **404** — no layer's path matched (register a catch-all last to customise).
|
|
479
|
+
* - **405 Method Not Allowed** — a layer's path matched but no layer
|
|
480
|
+
* accepted the HTTP method. The `Allow` header lists all registered methods.
|
|
481
|
+
* - **500** (or custom `onError` handler) — a middleware threw or rejected,
|
|
482
|
+
* or `next(err)` was called with a non-null error.
|
|
439
483
|
*/
|
|
440
484
|
const listener = (req, res, done) => {
|
|
441
485
|
const method = req.method;
|
|
442
486
|
const url = req.url;
|
|
443
487
|
let idx = 0;
|
|
444
|
-
updateHttpObjects(req, res);
|
|
445
|
-
|
|
488
|
+
updateHttpObjects(req, res, secret, trustProxy);
|
|
489
|
+
// ── Optional per-request timeout ──────────────────────────────────────
|
|
490
|
+
// Uses both a socket-level idle timeout (as the transport boundary) and a
|
|
491
|
+
// wall-clock setTimeout guard. The wall-clock timer is the primary
|
|
492
|
+
// mechanism because it is unaffected by the OS socket buffer state;
|
|
493
|
+
// socket.setTimeout is set in addition so the idle signal propagates down
|
|
494
|
+
// to keep-alive connections that would otherwise hold the socket open.
|
|
495
|
+
if (timeoutMs) {
|
|
496
|
+
if (req.socket)
|
|
497
|
+
req.socket.setTimeout(timeoutMs);
|
|
498
|
+
const timer = setTimeout(() => {
|
|
499
|
+
if (!res.writableEnded)
|
|
500
|
+
res.status(408).end('Request Timeout');
|
|
501
|
+
}, timeoutMs);
|
|
502
|
+
res.once('finish', () => {
|
|
503
|
+
clearTimeout(timer);
|
|
504
|
+
if (req.socket)
|
|
505
|
+
req.socket.setTimeout(0);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// ── Centralised error dispatch ─────────────────────────────────────────
|
|
509
|
+
// Invoked for sync throws, async rejections, and next(err) calls.
|
|
510
|
+
//
|
|
511
|
+
// Resolution order:
|
|
512
|
+
// 1. Each error() middleware in turn (it may end the response or forward).
|
|
513
|
+
// 2. The onError() terminal fallback, if registered.
|
|
514
|
+
// 3. Bubble to the parent router's error channel via done(err).
|
|
515
|
+
// 4. Top-level router with no handler → default 500.
|
|
516
|
+
const invokeErrorHandler = (e) => {
|
|
517
|
+
if (res.writableEnded)
|
|
518
|
+
return;
|
|
519
|
+
let i = 0;
|
|
520
|
+
const runNext = (err) => {
|
|
521
|
+
if (res.writableEnded)
|
|
522
|
+
return;
|
|
523
|
+
// 1. Ordered error() middleware chain.
|
|
524
|
+
if (i < errorHandlers.length) {
|
|
525
|
+
const handler = errorHandlers[i++];
|
|
526
|
+
try {
|
|
527
|
+
// `next()` forwards the same error; `next(newErr)` replaces it.
|
|
528
|
+
handler(err, req, res, (nextErr) => runNext(nextErr ?? err));
|
|
529
|
+
}
|
|
530
|
+
catch (e2) {
|
|
531
|
+
runNext(e2);
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// 2. onError() terminal fallback for this router.
|
|
536
|
+
if (errorHandler) {
|
|
537
|
+
try {
|
|
538
|
+
errorHandler(err, req, res);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
if (!res.writableEnded)
|
|
542
|
+
res.status(500).end(`Error ${method} ${url}`);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
// 3. Bubble to the parent router's error channel (when mounted via use()).
|
|
547
|
+
if (done) {
|
|
548
|
+
done(err);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// 4. Top-level router with no handler: default 500.
|
|
552
|
+
res.status(500).end(`Error ${method} ${url}`);
|
|
553
|
+
};
|
|
554
|
+
runNext(e);
|
|
555
|
+
};
|
|
556
|
+
// ── Safe middleware invocation ─────────────────────────────────────────
|
|
557
|
+
// Catches sync throws AND async rejections, routing both to invokeErrorHandler.
|
|
558
|
+
const invoke = (mw, nextFn) => {
|
|
559
|
+
try {
|
|
560
|
+
const ret = mw(req, res, nextFn);
|
|
561
|
+
if (ret instanceof Promise)
|
|
562
|
+
ret.catch(invokeErrorHandler);
|
|
563
|
+
}
|
|
564
|
+
catch (e) {
|
|
565
|
+
invokeErrorHandler(e);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
// Accumulate methods from layers whose path matched but whose method
|
|
569
|
+
// did not, for a 405 response with an accurate Allow header.
|
|
570
|
+
const allowedMethods = new Set();
|
|
571
|
+
// ── Main dispatch loop ─────────────────────────────────────────────────
|
|
572
|
+
const next = (err) => {
|
|
573
|
+
// If an error is passed, skip remaining routes and call error handler.
|
|
574
|
+
if (err != null) {
|
|
575
|
+
invokeErrorHandler(err);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
446
578
|
while (idx < routes.length) {
|
|
447
579
|
const layer = routes[idx++];
|
|
448
580
|
const pathBefore = req.path;
|
|
449
581
|
if (matchRouteLayer(layer, req, req.path)) {
|
|
450
582
|
if (layer.stripPath) {
|
|
451
|
-
// For prefix layers (use),
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
583
|
+
// For prefix layers (use), restore req.path and req.baseUrl after
|
|
584
|
+
// the sub-router calls done() so subsequent sibling layers see the
|
|
585
|
+
// original values.
|
|
586
|
+
const baseUrlBefore = req.baseUrl;
|
|
587
|
+
const strippedPrefix = pathBefore.slice(0, pathBefore.length - req.path.length);
|
|
588
|
+
req.baseUrl = baseUrlBefore + strippedPrefix;
|
|
589
|
+
// The continuation doubles as the sub-router's `done`: it runs both
|
|
590
|
+
// when the sub-router falls through (no error) and when it bubbles an
|
|
591
|
+
// error up. Forward `err` so a bubbled error reaches this router's
|
|
592
|
+
// error channel instead of being silently dropped.
|
|
593
|
+
invoke(layer.middleware, (err) => {
|
|
594
|
+
req.path = pathBefore;
|
|
595
|
+
req.baseUrl = baseUrlBefore;
|
|
596
|
+
next(err);
|
|
459
597
|
});
|
|
598
|
+
return;
|
|
460
599
|
}
|
|
461
|
-
|
|
600
|
+
invoke(layer.middleware, next);
|
|
601
|
+
return;
|
|
462
602
|
}
|
|
603
|
+
// Path matched but method did not → remember for 405 detection.
|
|
604
|
+
if (layer.method !== null && pathMatchesLayer(layer, pathBefore)) {
|
|
605
|
+
allowedMethods.add(layer.method);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// All layers exhausted without a full match.
|
|
609
|
+
if (allowedMethods.size > 0) {
|
|
610
|
+
// The path is registered, just not for this method. Build the Allow
|
|
611
|
+
// header, advertising HEAD (served by GET) and OPTIONS (handled here)
|
|
612
|
+
// alongside the explicitly registered methods.
|
|
613
|
+
if (allowedMethods.has('GET'))
|
|
614
|
+
allowedMethods.add('HEAD');
|
|
615
|
+
allowedMethods.add('OPTIONS');
|
|
616
|
+
const allow = [...allowedMethods].sort().join(', ');
|
|
617
|
+
// Automatic OPTIONS: when nothing claimed the request (no explicit
|
|
618
|
+
// OPTIONS route, no cors() middleware), reply 204 with the Allow header.
|
|
619
|
+
if (method === 'OPTIONS') {
|
|
620
|
+
res.status(204, { Allow: allow }).end();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
// Otherwise the method is genuinely not allowed for this path.
|
|
624
|
+
res.status(405, { Allow: allow }).end(`Cannot ${method} ${url}`);
|
|
625
|
+
return;
|
|
463
626
|
}
|
|
627
|
+
// Genuine 404 — delegate to the parent router, or send the default 404.
|
|
628
|
+
// To customise, register a catch-all layer last (e.g. `app.all('/**', …)`);
|
|
629
|
+
// it matches in registration order after every real route.
|
|
464
630
|
if (done)
|
|
465
631
|
return done();
|
|
466
632
|
res.status(404).end(`Cannot ${method} ${url}`);
|
|
@@ -469,25 +635,17 @@ function createRouter() {
|
|
|
469
635
|
next();
|
|
470
636
|
}
|
|
471
637
|
catch (e) {
|
|
472
|
-
|
|
473
|
-
res.status(500).end(`Error ${method} ${url}`);
|
|
638
|
+
invokeErrorHandler(e);
|
|
474
639
|
}
|
|
475
640
|
};
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
641
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
642
|
+
// Registration helper for method-specific routes
|
|
643
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
479
644
|
/**
|
|
480
645
|
* Return the route-registration function used by all HTTP-method helpers.
|
|
481
646
|
*
|
|
482
|
-
* The produced function accepts a mandatory `path` followed by any number
|
|
483
|
-
* of `MiddlewareArg` values (functions, `Router` instances, or arrays
|
|
484
|
-
* thereof), and delegates each to `registerRoute`.
|
|
485
|
-
*
|
|
486
647
|
* @param method - HTTP method to restrict layers to, or `null` for any.
|
|
487
|
-
* @param stripPath - Whether the matched path prefix should be stripped
|
|
488
|
-
* `req.path` before middleware is invoked. `true` for
|
|
489
|
-
* prefix-style registrations, `false` for exact-method ones.
|
|
490
|
-
* @returns A variadic route-registration function.
|
|
648
|
+
* @param stripPath - Whether the matched path prefix should be stripped.
|
|
491
649
|
*/
|
|
492
650
|
function makeRegister(method, stripPath) {
|
|
493
651
|
return (path, ...args) => {
|
|
@@ -495,31 +653,131 @@ function createRouter() {
|
|
|
495
653
|
registerRoute(routes, method, path, arg, stripPath);
|
|
496
654
|
};
|
|
497
655
|
}
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
656
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
657
|
+
// `use()` — supports both path-first and no-path (Router-first) forms
|
|
658
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
659
|
+
/**
|
|
660
|
+
* Register prefix-style middleware. Accepts:
|
|
661
|
+
* 1. `use(path, ...middlewares)` — explicit path.
|
|
662
|
+
* 2. `use(routerOrMiddleware, ...more)` — infers path from Router.prefix or '/'.
|
|
663
|
+
*/
|
|
664
|
+
const use = (pathOrFirst, ...args) => {
|
|
665
|
+
if (typeof pathOrFirst === 'string' || pathOrFirst instanceof RegExp) {
|
|
666
|
+
// Normal form: explicit path string or RegExp.
|
|
667
|
+
for (const arg of args)
|
|
668
|
+
registerRoute(routes, null, pathOrFirst, arg, true);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
// No explicit path: the first argument is itself a middleware / Router.
|
|
672
|
+
// Infer mount path from the Router's prefix, or default to '/'.
|
|
673
|
+
const inferredPath = extractRouterPrefix(pathOrFirst) ?? '/';
|
|
674
|
+
registerRoute(routes, null, inferredPath, pathOrFirst, true);
|
|
675
|
+
for (const arg of args)
|
|
676
|
+
registerRoute(routes, null, '/', arg, true);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
680
|
+
// Public router object
|
|
681
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
501
682
|
const router = {
|
|
683
|
+
prefix: routerPrefix,
|
|
502
684
|
listener,
|
|
503
|
-
use
|
|
504
|
-
all: makeRegister(null, false),
|
|
685
|
+
use,
|
|
686
|
+
all: makeRegister(null, false),
|
|
505
687
|
get: makeRegister('GET', false),
|
|
506
688
|
put: makeRegister('PUT', false),
|
|
507
689
|
post: makeRegister('POST', false),
|
|
508
690
|
delete: makeRegister('DELETE', false),
|
|
509
691
|
patch: makeRegister('PATCH', false),
|
|
692
|
+
head: makeRegister('HEAD', false),
|
|
693
|
+
options: makeRegister('OPTIONS', false),
|
|
694
|
+
// ── route ────────────────────────────────────────────────────────────────
|
|
695
|
+
route(path) {
|
|
696
|
+
// Each method forwards to the router's own registration helper with the
|
|
697
|
+
// cached path and returns the builder so calls can be chained.
|
|
698
|
+
const builder = {
|
|
699
|
+
all(...args) { router.all(path, ...args); return builder; },
|
|
700
|
+
get(...args) { router.get(path, ...args); return builder; },
|
|
701
|
+
put(...args) { router.put(path, ...args); return builder; },
|
|
702
|
+
post(...args) { router.post(path, ...args); return builder; },
|
|
703
|
+
delete(...args) { router.delete(path, ...args); return builder; },
|
|
704
|
+
patch(...args) { router.patch(path, ...args); return builder; },
|
|
705
|
+
head(...args) { router.head(path, ...args); return builder; },
|
|
706
|
+
options(...args) { router.options(path, ...args); return builder; },
|
|
707
|
+
};
|
|
708
|
+
return builder;
|
|
709
|
+
},
|
|
710
|
+
// ── onError ─────────────────────────────────────────────────────────────
|
|
711
|
+
onError(handler) {
|
|
712
|
+
errorHandler = handler;
|
|
713
|
+
},
|
|
714
|
+
// ── error ────────────────────────────────────────────────────────────────
|
|
715
|
+
error(handler) {
|
|
716
|
+
errorHandlers.push(handler);
|
|
717
|
+
},
|
|
718
|
+
// ── routes ───────────────────────────────────────────────────────────────
|
|
719
|
+
routes() {
|
|
720
|
+
return routes.map((l) => ({
|
|
721
|
+
method: l.method,
|
|
722
|
+
path: l.path,
|
|
723
|
+
stripPath: l.stripPath,
|
|
724
|
+
}));
|
|
725
|
+
},
|
|
726
|
+
// ── shutdown ─────────────────────────────────────────────────────────────
|
|
727
|
+
shutdown(timeout = 5000) {
|
|
728
|
+
if (!activeServer)
|
|
729
|
+
return Promise.resolve();
|
|
730
|
+
return new Promise((resolve, reject) => {
|
|
731
|
+
// Stop accepting new connections. Resolves when all existing
|
|
732
|
+
// connections have been closed (or when forcibly destroyed below).
|
|
733
|
+
activeServer.close((err) => {
|
|
734
|
+
if (err)
|
|
735
|
+
reject(err);
|
|
736
|
+
else
|
|
737
|
+
resolve();
|
|
738
|
+
});
|
|
739
|
+
// Forcibly destroy any remaining idle sockets after the grace period.
|
|
740
|
+
if (timeout > 0) {
|
|
741
|
+
setTimeout(() => {
|
|
742
|
+
for (const socket of activeSockets)
|
|
743
|
+
socket.destroy();
|
|
744
|
+
activeSockets.clear();
|
|
745
|
+
}, timeout);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
},
|
|
749
|
+
// ── listen ───────────────────────────────────────────────────────────────
|
|
510
750
|
listen(port, opts, cb) {
|
|
511
751
|
if (typeof opts === 'function') {
|
|
512
752
|
cb = opts;
|
|
513
753
|
opts = undefined;
|
|
514
754
|
}
|
|
515
755
|
const rawListener = listener;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
756
|
+
let server;
|
|
757
|
+
const tlsOpts = opts;
|
|
758
|
+
if (tlsOpts?.key && tlsOpts?.cert) {
|
|
759
|
+
if (tlsOpts.http2) {
|
|
760
|
+
// HTTP/2 secure server — same TLS options, different factory.
|
|
761
|
+
server = http2.createSecureServer(tlsOpts, rawListener);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
server = https.createServer(tlsOpts, rawListener);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
server = http.createServer(rawListener);
|
|
769
|
+
}
|
|
770
|
+
// Track open sockets so shutdown() can forcibly destroy them.
|
|
771
|
+
server.on('connection', (socket) => {
|
|
772
|
+
activeSockets.add(socket);
|
|
773
|
+
socket.once('close', () => activeSockets.delete(socket));
|
|
774
|
+
});
|
|
775
|
+
activeServer = server;
|
|
776
|
+
server.listen(port, cb);
|
|
777
|
+
return server;
|
|
520
778
|
},
|
|
521
779
|
};
|
|
522
780
|
return router;
|
|
523
781
|
}
|
|
524
|
-
|
|
782
|
+
export default createRouter;
|
|
525
783
|
//# sourceMappingURL=router.js.map
|