routup 5.2.0 → 6.0.0-beta.1

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.
@@ -1,1993 +0,0 @@
1
- import { FastURL } from "srvx";
2
- import { hasInstanceof, markInstanceof } from "@ebec/core";
3
- import { HTTPError, isHTTPError } from "@ebec/http";
4
- import { subtle } from "uncrypto";
5
- import { merge } from "smob";
6
- import { compile } from "proxy-addr";
7
- import { get, getType } from "mime-explorer";
8
- import Negotiator from "negotiator";
9
- import { pathToRegexp } from "path-to-regexp";
10
- //#region src/constants.ts
11
- const MethodName = {
12
- GET: "GET",
13
- POST: "POST",
14
- PUT: "PUT",
15
- PATCH: "PATCH",
16
- DELETE: "DELETE",
17
- OPTIONS: "OPTIONS",
18
- HEAD: "HEAD"
19
- };
20
- const HeaderName = {
21
- ACCEPT: "accept",
22
- ACCEPT_CHARSET: "accept-charset",
23
- ACCEPT_ENCODING: "accept-encoding",
24
- ACCEPT_LANGUAGE: "accept-language",
25
- ACCEPT_RANGES: "accept-ranges",
26
- ALLOW: "allow",
27
- CACHE_CONTROL: "cache-control",
28
- CONTENT_DISPOSITION: "content-disposition",
29
- CONTENT_ENCODING: "content-encoding",
30
- CONTENT_LENGTH: "content-length",
31
- CONTENT_RANGE: "content-range",
32
- CONTENT_TYPE: "content-type",
33
- CONNECTION: "connection",
34
- COOKIE: "cookie",
35
- ETag: "etag",
36
- HOST: "host",
37
- IF_MODIFIED_SINCE: "if-modified-since",
38
- IF_NONE_MATCH: "if-none-match",
39
- LAST_MODIFIED: "last-modified",
40
- LOCATION: "location",
41
- RANGE: "range",
42
- RATE_LIMIT_LIMIT: "ratelimit-limit",
43
- RATE_LIMIT_REMAINING: "ratelimit-remaining",
44
- RATE_LIMIT_RESET: "ratelimit-reset",
45
- RETRY_AFTER: "retry-after",
46
- SET_COOKIE: "set-cookie",
47
- TRANSFER_ENCODING: "transfer-encoding",
48
- X_ACCEL_BUFFERING: "x-accel-buffering",
49
- X_FORWARDED_HOST: "x-forwarded-host",
50
- X_FORWARDED_FOR: "x-forwarded-for",
51
- X_FORWARDED_PROTO: "x-forwarded-proto"
52
- };
53
- //#endregion
54
- //#region src/response/helpers/cache.ts
55
- function setResponseCacheHeaders(event, options) {
56
- options = options || {};
57
- const cacheControls = ["public"].concat(options.cacheControls || []);
58
- if (options.maxAge !== void 0) cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);
59
- if (options.modifiedTime) {
60
- const modifiedTime = typeof options.modifiedTime === "string" ? new Date(options.modifiedTime) : options.modifiedTime;
61
- event.response.headers.set("last-modified", modifiedTime.toUTCString());
62
- }
63
- event.response.headers.set("cache-control", cacheControls.join(", "));
64
- }
65
- //#endregion
66
- //#region src/error/module.ts
67
- const ErrorSymbol = Symbol.for("RoutupError");
68
- var RoutupError = class extends HTTPError {
69
- constructor(input = {}) {
70
- super(input);
71
- this.name = "RoutupError";
72
- markInstanceof(this, ErrorSymbol);
73
- }
74
- };
75
- //#endregion
76
- //#region src/response/helpers/event-stream/utils.ts
77
- function stripNewlines(value) {
78
- return value.replace(/[\r\n]/g, "");
79
- }
80
- function serializeEventStreamMessage(message) {
81
- let result = "";
82
- if (message.id) result += `id: ${stripNewlines(message.id)}\n`;
83
- if (message.event) result += `event: ${stripNewlines(message.event)}\n`;
84
- if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
85
- const lines = message.data.replace(/\r/g, "").split("\n");
86
- for (const line of lines) result += `data: ${line}\n`;
87
- result += "\n";
88
- return result;
89
- }
90
- //#endregion
91
- //#region src/response/helpers/event-stream/module.ts
92
- function createEventStream(event, options) {
93
- if (options?.maxMessageSize !== void 0) {
94
- if (!Number.isInteger(options.maxMessageSize) || options.maxMessageSize < 0) throw new RoutupError("maxMessageSize must be a non-negative integer.");
95
- }
96
- let controller;
97
- let closed = false;
98
- const encoder = new TextEncoder();
99
- const stream = new ReadableStream({
100
- start(ctrl) {
101
- controller = ctrl;
102
- },
103
- cancel() {
104
- closed = true;
105
- }
106
- });
107
- const headers = new Headers(event.response.headers);
108
- headers.set(HeaderName.CONTENT_TYPE, "text/event-stream");
109
- headers.set(HeaderName.CACHE_CONTROL, "private, no-cache, no-store, no-transform, must-revalidate, max-age=0");
110
- headers.set(HeaderName.X_ACCEL_BUFFERING, "no");
111
- headers.set(HeaderName.CONNECTION, "keep-alive");
112
- const handle = {
113
- write(message) {
114
- if (closed) return false;
115
- if (typeof message === "string") return handle.write({ data: message });
116
- const serialized = serializeEventStreamMessage(message);
117
- if (options?.maxMessageSize !== void 0) {
118
- if (encoder.encode(serialized).byteLength > options.maxMessageSize) return false;
119
- }
120
- controller.enqueue(encoder.encode(serialized));
121
- return true;
122
- },
123
- end() {
124
- if (closed) return;
125
- closed = true;
126
- controller.close();
127
- },
128
- response: new Response(stream, {
129
- status: event.response.status,
130
- headers
131
- })
132
- };
133
- return handle;
134
- }
135
- //#endregion
136
- //#region src/utils/header.ts
137
- function sanitizeHeaderValue(value) {
138
- return value.replace(/[\r\n]/g, "");
139
- }
140
- //#endregion
141
- //#region src/utils/etag/module.ts
142
- async function sha1(str) {
143
- const enc = new TextEncoder();
144
- const hash = await subtle.digest("SHA-1", enc.encode(str));
145
- return btoa(String.fromCharCode(...new Uint8Array(hash)));
146
- }
147
- /**
148
- * Generate an ETag.
149
- */
150
- async function generateETag(input) {
151
- if (input.length === 0) return "\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"";
152
- const hash = await sha1(input);
153
- return `"${input.length.toString(16)}-${hash.substring(0, 27)}"`;
154
- }
155
- /**
156
- * Create a simple ETag.
157
- */
158
- async function createEtag(input, options = {}) {
159
- const tag = await generateETag(input);
160
- return options.weak ? `W/${tag}` : tag;
161
- }
162
- //#endregion
163
- //#region src/utils/object.ts
164
- function isObject(item) {
165
- return !!item && typeof item === "object" && !Array.isArray(item);
166
- }
167
- //#endregion
168
- //#region src/utils/etag/utils.ts
169
- const textEncoder = /* @__PURE__ */ new TextEncoder();
170
- function buildEtagFn(input) {
171
- if (typeof input === "function") return input;
172
- input = input ?? true;
173
- if (input === false) return () => Promise.resolve(void 0);
174
- let options = { weak: true };
175
- if (isObject(input)) options = merge(input, options);
176
- return async (body, size) => {
177
- if (typeof options.threshold !== "undefined") {
178
- if ((size ?? textEncoder.encode(body).byteLength) <= options.threshold) return;
179
- }
180
- return createEtag(body, options);
181
- };
182
- }
183
- //#endregion
184
- //#region src/utils/trust-proxy/module.ts
185
- function buildTrustProxyFn(input) {
186
- if (typeof input === "function") return input;
187
- if (input === true) return () => true;
188
- if (typeof input === "number") return (_address, hop) => hop < input;
189
- if (typeof input === "string") input = input.split(",").map((value) => value.trim());
190
- return compile(input || []);
191
- }
192
- //#endregion
193
- //#region src/utils/mime.ts
194
- function getMimeType(type) {
195
- if (type.includes("/")) return type;
196
- return getType(type);
197
- }
198
- function getCharsetForMimeType(type) {
199
- if (/^text\/|^application\/(javascript|json)/.test(type)) return "utf-8";
200
- const meta = get(type);
201
- if (meta && meta.charset) return meta.charset.toLowerCase();
202
- }
203
- //#endregion
204
- //#region src/utils/method.ts
205
- function toMethodName(input, alt) {
206
- if (input) return input.toUpperCase();
207
- return alt;
208
- }
209
- //#endregion
210
- //#region src/utils/path.ts
211
- /**
212
- * Based on https://github.com/unjs/pathe v1.1.1 (055f50a6f1131f4e5c56cf259dd8816168fba329)
213
- */
214
- function normalizeWindowsPath(input = "") {
215
- if (!input || !input.includes("\\")) return input;
216
- return input.replace(/\\/g, "/");
217
- }
218
- const EXTNAME_RE = /.(\.[^./]+)$/;
219
- function extname(input) {
220
- const match = EXTNAME_RE.exec(normalizeWindowsPath(input));
221
- return match && match[1] || "";
222
- }
223
- function basename(input, extension) {
224
- const lastSegment = normalizeWindowsPath(input).split("/").pop();
225
- if (!lastSegment) return input;
226
- return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
227
- }
228
- //#endregion
229
- //#region src/utils/url.ts
230
- const TRAILING_SLASH_RE = /\/$|\/\?/;
231
- function hasTrailingSlash(input = "", queryParams = false) {
232
- if (!queryParams) return input.endsWith("/");
233
- return TRAILING_SLASH_RE.test(input);
234
- }
235
- function withoutTrailingSlash(input = "", queryParams = false) {
236
- if (!queryParams) return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/";
237
- if (!hasTrailingSlash(input, true)) return input || "/";
238
- const [s0, ...s] = input.split("?");
239
- return (s0.slice(0, -1) || "/") + (s.length ? `?${s.join("?")}` : "");
240
- }
241
- function hasLeadingSlash(input = "") {
242
- return input.startsWith("/");
243
- }
244
- function withLeadingSlash(input = "") {
245
- return hasLeadingSlash(input) ? input : `/${input}`;
246
- }
247
- function cleanDoubleSlashes(input = "") {
248
- if (input.includes("://")) return input.split("://").map((str) => cleanDoubleSlashes(str)).join("://");
249
- return input.replace(/\/+/g, "/");
250
- }
251
- //#endregion
252
- //#region src/response/helpers/header.ts
253
- function appendResponseHeader(event, name, value) {
254
- const { headers } = event.response;
255
- if (Array.isArray(value)) for (const v of value) headers.append(name, sanitizeHeaderValue(v));
256
- else headers.append(name, sanitizeHeaderValue(value));
257
- }
258
- function appendResponseHeaderDirective(event, name, value) {
259
- const { headers } = event.response;
260
- const existing = headers.get(name);
261
- if (!existing) {
262
- if (Array.isArray(value)) headers.set(name, sanitizeHeaderValue(value.join("; ")));
263
- else headers.set(name, sanitizeHeaderValue(value));
264
- return;
265
- }
266
- const directives = existing.split("; ");
267
- if (Array.isArray(value)) directives.push(...value);
268
- else directives.push(value);
269
- const unique = [...new Set(directives)];
270
- headers.set(name, sanitizeHeaderValue(unique.join("; ")));
271
- }
272
- //#endregion
273
- //#region src/response/helpers/utils.ts
274
- function setResponseContentTypeByFileName(event, fileName) {
275
- const ext = extname(fileName);
276
- if (ext) {
277
- let type = getMimeType(ext.substring(1));
278
- if (type) {
279
- const charset = getCharsetForMimeType(type);
280
- if (charset) type += `; charset=${charset}`;
281
- event.response.headers.set(HeaderName.CONTENT_TYPE, type);
282
- }
283
- }
284
- }
285
- //#endregion
286
- //#region src/response/helpers/header-disposition.ts
287
- const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
288
- const NON_ASCII_REGEXP = /[^\x20-\x7e]/g;
289
- const QUOTE_REGEXP = /[\\"]/g;
290
- const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
291
- const ASCII_TEXT_REGEXP = /^[\x20-\x7e]+$/;
292
- const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/;
293
- function pencode(char) {
294
- return `%${char.charCodeAt(0).toString(16).toUpperCase()}`;
295
- }
296
- function quoteString(value) {
297
- return `"${value.replace(QUOTE_REGEXP, "\\$&")}"`;
298
- }
299
- function getAscii(value) {
300
- return value.replace(NON_ASCII_REGEXP, "?");
301
- }
302
- function encodeExtended(value) {
303
- return encodeURIComponent(value).replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode);
304
- }
305
- function formatFilename(value) {
306
- if (TOKEN_REGEXP.test(value)) return `filename=${value}`;
307
- return `filename=${quoteString(value)}`;
308
- }
309
- function setDisposition(event, type, filename) {
310
- let disposition = type;
311
- if (typeof filename === "string") {
312
- setResponseContentTypeByFileName(event, filename);
313
- if (ASCII_TEXT_REGEXP.test(filename) && !HEX_ESCAPE_REGEXP.test(filename)) disposition += `; ${formatFilename(filename)}`;
314
- else {
315
- disposition += `; ${formatFilename(getAscii(filename))}`;
316
- disposition += `; filename*=UTF-8''${encodeExtended(filename)}`;
317
- }
318
- }
319
- event.response.headers.set(HeaderName.CONTENT_DISPOSITION, disposition);
320
- }
321
- function setResponseHeaderAttachment(event, filename) {
322
- setDisposition(event, "attachment", filename);
323
- }
324
- function setResponseHeaderInline(event, filename) {
325
- setDisposition(event, "inline", filename);
326
- }
327
- //#endregion
328
- //#region src/response/helpers/header-content-type.ts
329
- function setResponseHeaderContentType(event, input, ifNotExists) {
330
- if (ifNotExists) {
331
- if (event.response.headers.get(HeaderName.CONTENT_TYPE)) return;
332
- }
333
- const contentType = getMimeType(input);
334
- if (contentType) event.response.headers.set(HeaderName.CONTENT_TYPE, contentType);
335
- }
336
- //#endregion
337
- //#region src/error/is.ts
338
- function isError(input) {
339
- return hasInstanceof(input, ErrorSymbol);
340
- }
341
- //#endregion
342
- //#region src/error/create.ts
343
- function isNativeError(input) {
344
- return isObject(input) && typeof input.message === "string" && typeof input.name === "string";
345
- }
346
- /**
347
- * Create an internal error object by
348
- * - an existing RoutupError (returned as-is)
349
- * - an HTTPError (wrapped into a RoutupError preserving status)
350
- * - an Error (wrapped preserving message and cause)
351
- * - an options object (status, message, etc.)
352
- * - a message string
353
- *
354
- * @param input
355
- */
356
- function createError(input) {
357
- if (isError(input)) return input;
358
- if (typeof input === "string") return new RoutupError(input);
359
- if (isHTTPError(input)) return new RoutupError({
360
- message: input.message,
361
- code: input.code,
362
- status: input.status,
363
- redirectURL: input.redirectURL,
364
- cause: input
365
- });
366
- if (isNativeError(input)) return new RoutupError({
367
- message: input.message,
368
- cause: input
369
- });
370
- if (!isObject(input)) return new RoutupError();
371
- const options = { ...input };
372
- if (options.cause === void 0) options.cause = input;
373
- return new RoutupError(options);
374
- }
375
- //#endregion
376
- //#region src/response/to-response.ts
377
- function stripWeakPrefix(etag) {
378
- return etag.startsWith("W/") ? etag.slice(2) : etag;
379
- }
380
- async function applyEtag(body, event, headers) {
381
- const etagFn = event.routerOptions.etag;
382
- if (!etagFn) return void 0;
383
- const etag = await etagFn(body);
384
- if (!etag) return void 0;
385
- headers.set("etag", etag);
386
- const ifNoneMatch = event.headers.get("if-none-match");
387
- if (ifNoneMatch && (ifNoneMatch === "*" || ifNoneMatch.split(",").some((t) => stripWeakPrefix(t.trim()) === stripWeakPrefix(etag)))) return new Response(null, {
388
- status: 304,
389
- headers
390
- });
391
- }
392
- async function toResponse(value, event) {
393
- if (value === void 0) return;
394
- if (value === null) return new Response(null, {
395
- status: event.response.status,
396
- headers: event.response.headers
397
- });
398
- if (value instanceof Response) return value;
399
- const { status, headers } = event.response;
400
- if (typeof value === "string") {
401
- if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
402
- const cached = await applyEtag(value, event, headers);
403
- if (cached) return cached;
404
- return new Response(value, {
405
- status,
406
- headers
407
- });
408
- }
409
- if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
410
- if (!headers.has("content-type")) headers.set("content-type", "application/octet-stream");
411
- return new Response(value, {
412
- status,
413
- headers
414
- });
415
- }
416
- if (value instanceof ReadableStream) return new Response(value, {
417
- status,
418
- headers
419
- });
420
- if (value instanceof Blob) {
421
- if (!headers.has("content-type")) headers.set("content-type", value.type || "application/octet-stream");
422
- return new Response(value, {
423
- status,
424
- headers
425
- });
426
- }
427
- if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
428
- let json;
429
- try {
430
- json = JSON.stringify(value);
431
- } catch (e) {
432
- throw createError({
433
- message: "JSON serialization failed",
434
- status: 500,
435
- cause: e
436
- });
437
- }
438
- const cached = await applyEtag(json, event, headers);
439
- if (cached) return cached;
440
- return new Response(json, {
441
- status,
442
- headers
443
- });
444
- }
445
- //#endregion
446
- //#region src/response/helpers/send-accepted.ts
447
- async function sendAccepted(event, data) {
448
- event.response.status = 202;
449
- return await toResponse(data ?? "", event);
450
- }
451
- //#endregion
452
- //#region src/response/helpers/send-created.ts
453
- async function sendCreated(event, data) {
454
- event.response.status = 201;
455
- return await toResponse(data ?? "", event);
456
- }
457
- //#endregion
458
- //#region src/response/helpers/send-file.ts
459
- async function sendFile(event, options) {
460
- let stats;
461
- if (typeof options.stats === "function") stats = await options.stats();
462
- else stats = options.stats;
463
- const name = options.name || stats.name;
464
- const { headers } = event.response;
465
- const disposition = options.disposition ?? (options.attachment ? "attachment" : void 0);
466
- if (name) {
467
- const fileName = basename(name);
468
- if (disposition) {
469
- if (!headers.get(HeaderName.CONTENT_DISPOSITION)) if (disposition === "inline") setResponseHeaderInline(event, fileName);
470
- else setResponseHeaderAttachment(event, fileName);
471
- } else setResponseContentTypeByFileName(event, fileName);
472
- }
473
- const contentOptions = {};
474
- let statusCode = event.response.status;
475
- if (stats.size) {
476
- const rangeHeader = event.headers.get(HeaderName.RANGE);
477
- if (rangeHeader) {
478
- const [x, y] = rangeHeader.replace("bytes=", "").split("-");
479
- const parsedStart = Number.parseInt(x, 10);
480
- const parsedEnd = Number.parseInt(y, 10);
481
- contentOptions.start = Number.isFinite(parsedStart) && parsedStart >= 0 ? parsedStart : 0;
482
- contentOptions.end = Number.isFinite(parsedEnd) && parsedEnd >= 0 ? Math.min(parsedEnd, stats.size - 1) : stats.size - 1;
483
- if (contentOptions.start >= stats.size || contentOptions.start > contentOptions.end) {
484
- const rangeHeaders = new Headers(headers);
485
- rangeHeaders.set(HeaderName.CONTENT_RANGE, `bytes */${stats.size}`);
486
- return new Response(null, {
487
- status: 416,
488
- headers: rangeHeaders
489
- });
490
- }
491
- headers.set(HeaderName.CONTENT_RANGE, `bytes ${contentOptions.start}-${contentOptions.end}/${stats.size}`);
492
- headers.set(HeaderName.CONTENT_LENGTH, `${contentOptions.end - contentOptions.start + 1}`);
493
- statusCode = 206;
494
- } else headers.set(HeaderName.CONTENT_LENGTH, `${stats.size}`);
495
- headers.set(HeaderName.ACCEPT_RANGES, "bytes");
496
- if (stats.mtime) {
497
- const mtime = new Date(stats.mtime);
498
- headers.set(HeaderName.LAST_MODIFIED, mtime.toUTCString());
499
- headers.set(HeaderName.ETag, `W/"${stats.size}-${mtime.getTime()}"`);
500
- }
501
- }
502
- const content = await options.content(contentOptions);
503
- return new Response(content, {
504
- status: statusCode,
505
- headers
506
- });
507
- }
508
- //#endregion
509
- //#region src/request/helpers/header.ts
510
- function getRequestHeader(event, name) {
511
- return event.headers.get(name);
512
- }
513
- //#endregion
514
- //#region src/request/helpers/negotiator.ts
515
- const NEGOTIATOR_KEY = Symbol.for("routup:negotiator");
516
- function headersToPlainObject(headers) {
517
- const result = {};
518
- headers.forEach((value, key) => {
519
- result[key] = value;
520
- });
521
- return result;
522
- }
523
- function useRequestNegotiator(event) {
524
- let value = event.store[NEGOTIATOR_KEY];
525
- if (value) return value;
526
- value = new Negotiator({ headers: headersToPlainObject(event.headers) });
527
- event.store[NEGOTIATOR_KEY] = value;
528
- return value;
529
- }
530
- //#endregion
531
- //#region src/request/helpers/header-accept.ts
532
- function getRequestAcceptableContentTypes(event) {
533
- return useRequestNegotiator(event).mediaTypes();
534
- }
535
- function getRequestAcceptableContentType(event, input) {
536
- input = input || [];
537
- const items = Array.isArray(input) ? input : [input];
538
- if (items.length === 0) return getRequestAcceptableContentTypes(event).shift();
539
- if (!getRequestHeader(event, HeaderName.ACCEPT)) return items[0];
540
- let polluted = false;
541
- const mimeTypes = [];
542
- for (const item of items) {
543
- const mimeType = getMimeType(item);
544
- if (mimeType) mimeTypes.push(mimeType);
545
- else polluted = true;
546
- }
547
- const matches = useRequestNegotiator(event).mediaTypes(mimeTypes);
548
- if (matches.length > 0) {
549
- if (polluted) return items[0];
550
- return items[mimeTypes.indexOf(matches[0])];
551
- }
552
- }
553
- //#endregion
554
- //#region src/response/helpers/send-format.ts
555
- function sendFormat(event, input) {
556
- const { default: formatDefault, ...formats } = input;
557
- const contentTypes = Object.keys(formats);
558
- if (contentTypes.length === 0) return formatDefault();
559
- const contentType = getRequestAcceptableContentType(event, contentTypes);
560
- if (contentType && formats[contentType]) return formats[contentType]();
561
- return formatDefault();
562
- }
563
- //#endregion
564
- //#region src/response/helpers/send-redirect.ts
565
- function escapeHtml(str) {
566
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
567
- }
568
- function isAllowedRedirectUrl(location) {
569
- if (location.startsWith("//")) return false;
570
- if (location.startsWith("/") || location.startsWith(".")) return true;
571
- try {
572
- const url = new URL(location);
573
- return url.protocol === "http:" || url.protocol === "https:";
574
- } catch {
575
- return true;
576
- }
577
- }
578
- function sendRedirect(event, location, statusCode = 302) {
579
- if (!isAllowedRedirectUrl(location)) throw new RoutupError({
580
- status: 400,
581
- message: "Invalid redirect URL scheme."
582
- });
583
- const sanitizedLocation = sanitizeHeaderValue(location);
584
- const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}"></head></html>`;
585
- const headers = new Headers(event.response.headers);
586
- headers.set("location", sanitizedLocation);
587
- headers.set("content-type", "text/html; charset=utf-8");
588
- headers.delete("content-length");
589
- return new Response(html, {
590
- status: statusCode,
591
- headers
592
- });
593
- }
594
- //#endregion
595
- //#region src/response/helpers/send-stream.ts
596
- function sendStream(event, stream) {
597
- const { status, headers } = event.response;
598
- return new Response(stream, {
599
- status,
600
- headers
601
- });
602
- }
603
- //#endregion
604
- //#region src/event/module.ts
605
- var RoutupEvent = class {
606
- request;
607
- params;
608
- path;
609
- method;
610
- mountPath;
611
- headers;
612
- searchParams;
613
- response;
614
- store;
615
- signal;
616
- _context;
617
- _routerOptions;
618
- _nextCalled = false;
619
- _nextResult;
620
- _nextCalledDeferred;
621
- constructor(context) {
622
- this._context = context;
623
- this.request = context.request;
624
- this.params = context.params;
625
- this.path = context.path;
626
- this.method = context.method;
627
- this.mountPath = context.mountPath;
628
- this.headers = context.headers;
629
- this.searchParams = context.searchParams;
630
- this.response = context.response;
631
- this.store = context.store;
632
- this.signal = context.signal;
633
- }
634
- get routerOptions() {
635
- if (!this._routerOptions) this._routerOptions = this._context.routerOptions();
636
- return this._routerOptions;
637
- }
638
- get nextCalled() {
639
- return this._nextCalled;
640
- }
641
- get nextResult() {
642
- return this._nextResult;
643
- }
644
- whenNextCalled() {
645
- if (!this._nextCalledDeferred) {
646
- let resolve;
647
- const promise = new Promise((r) => {
648
- resolve = r;
649
- });
650
- this._nextCalledDeferred = {
651
- promise,
652
- resolve
653
- };
654
- if (this._nextCalled) resolve();
655
- }
656
- return this._nextCalledDeferred.promise;
657
- }
658
- async next(error) {
659
- if (this._nextCalled) return this._nextResult;
660
- this._nextCalled = true;
661
- this._nextResult = this._context.next(this, error);
662
- if (this._nextCalledDeferred) this._nextCalledDeferred.resolve();
663
- return this._nextResult;
664
- }
665
- };
666
- //#endregion
667
- //#region src/dispatcher/module.ts
668
- var DispatcherEvent = class {
669
- request;
670
- params;
671
- path;
672
- method;
673
- /**
674
- * Collected allowed methods (for OPTIONS).
675
- */
676
- methodsAllowed;
677
- mountPath;
678
- error;
679
- routerPath;
680
- _dispatched;
681
- _response;
682
- _store;
683
- /**
684
- * Cached parsed URL (avoids double-parsing).
685
- */
686
- _url;
687
- /**
688
- * Continuation function for middleware onion model.
689
- */
690
- _next;
691
- _signal;
692
- _signalCleanup;
693
- /**
694
- * Whether _next has already been called (guard against double-invocation).
695
- */
696
- _nextCalled;
697
- /**
698
- * The cached result of the next handler.
699
- */
700
- _nextResult;
701
- constructor(request) {
702
- this.request = request;
703
- this._url = new FastURL(request.url);
704
- this.method = request.method;
705
- this.path = this._url.pathname;
706
- this.mountPath = "/";
707
- this.params = {};
708
- this.routerPath = [];
709
- this.methodsAllowed = /* @__PURE__ */ new Set();
710
- this._dispatched = false;
711
- this._nextCalled = false;
712
- }
713
- get response() {
714
- if (!this._response) this._response = {
715
- status: 200,
716
- headers: new Headers()
717
- };
718
- return this._response;
719
- }
720
- get signal() {
721
- if (!this._signal) this._signal = this.request.signal;
722
- return this._signal;
723
- }
724
- set signal(value) {
725
- if (this._signalCleanup) {
726
- this._signalCleanup();
727
- this._signalCleanup = void 0;
728
- }
729
- if (value === this.request.signal) {
730
- this._signal = value;
731
- return;
732
- }
733
- const controller = new AbortController();
734
- const abort = (e) => {
735
- const reason = e?.target instanceof AbortSignal ? e.target.reason : void 0;
736
- this.request.signal.removeEventListener("abort", abort);
737
- value.removeEventListener("abort", abort);
738
- controller.abort(reason);
739
- };
740
- if (this.request.signal.aborted || value.aborted) {
741
- const reason = this.request.signal.aborted ? this.request.signal.reason : value.reason;
742
- controller.abort(reason);
743
- } else {
744
- this.request.signal.addEventListener("abort", abort, { once: true });
745
- value.addEventListener("abort", abort, { once: true });
746
- this._signalCleanup = () => {
747
- this.request.signal.removeEventListener("abort", abort);
748
- value.removeEventListener("abort", abort);
749
- };
750
- }
751
- this._signal = controller.signal;
752
- }
753
- get dispatched() {
754
- return this._dispatched;
755
- }
756
- set dispatched(value) {
757
- this._dispatched = value;
758
- }
759
- async next(event, error) {
760
- if (this._nextCalled) return this._nextResult;
761
- this._nextCalled = true;
762
- if (this._next) this._nextResult = this._next(event, error);
763
- return this._nextResult;
764
- }
765
- setNext(fn) {
766
- if (fn) this._next = async (event, error) => {
767
- return toResponse(await fn(error), event);
768
- };
769
- else this._next = void 0;
770
- this._nextCalled = false;
771
- this._nextResult = void 0;
772
- }
773
- build(signal) {
774
- return new RoutupEvent({
775
- request: this.request,
776
- params: this.params,
777
- path: this.path,
778
- method: this.method,
779
- mountPath: this.mountPath,
780
- headers: this.request.headers,
781
- searchParams: new URLSearchParams(this._url.search),
782
- response: this.response,
783
- store: this.store,
784
- signal: signal ?? this.signal,
785
- routerOptions: () => this.resolveOptions(),
786
- next: (event, error) => this.next(event, error)
787
- });
788
- }
789
- get store() {
790
- if (!this._store) this._store = Object.create(null);
791
- return this._store;
792
- }
793
- resolveOptions() {
794
- const resolved = {
795
- trustProxy: () => false,
796
- subdomainOffset: 2,
797
- etag: buildEtagFn(),
798
- proxyIpMax: 0
799
- };
800
- for (let i = 0; i < this.routerPath.length; i++) {
801
- const node = this.routerPath[i];
802
- const entries = Object.entries(node.options);
803
- for (const entry of entries) {
804
- const [key, value] = entry;
805
- if (typeof value !== "undefined") resolved[key] = value;
806
- }
807
- }
808
- return resolved;
809
- }
810
- };
811
- //#endregion
812
- //#region src/handler/constants.ts
813
- const HandlerType = {
814
- CORE: "core",
815
- ERROR: "error"
816
- };
817
- const HandlerSymbol = Symbol.for("Handler");
818
- //#endregion
819
- //#region src/hook/constants.ts
820
- const HookName = {
821
- REQUEST: "request",
822
- RESPONSE: "response",
823
- ERROR: "error",
824
- CHILD_MATCH: "childMatch",
825
- CHILD_DISPATCH_BEFORE: "childDispatchBefore",
826
- CHILD_DISPATCH_AFTER: "childDispatchAfter"
827
- };
828
- //#endregion
829
- //#region src/hook/module.ts
830
- var Hooks = class Hooks {
831
- items;
832
- constructor() {
833
- this.items = {};
834
- }
835
- addListener(name, fn, priority = 0) {
836
- this.items[name] = this.items[name] || [];
837
- const entry = {
838
- fn,
839
- priority
840
- };
841
- let i = 0;
842
- while (i < this.items[name].length && this.items[name][i].priority >= priority) i++;
843
- this.items[name].splice(i, 0, entry);
844
- return () => {
845
- this.removeListener(name, fn);
846
- };
847
- }
848
- removeListener(name, fn) {
849
- if (!this.items[name]) return;
850
- if (typeof fn === "undefined") {
851
- delete this.items[name];
852
- return;
853
- }
854
- if (typeof fn === "function") {
855
- const index = this.items[name].findIndex((entry) => entry.fn === fn);
856
- if (index !== -1) this.items[name].splice(index, 1);
857
- }
858
- if (this.items[name].length === 0) delete this.items[name];
859
- }
860
- /**
861
- * Create a new `Hooks` instance seeded with the same listeners as this
862
- * one.
863
- *
864
- * Listener functions are shared by reference; priority and ordering are
865
- * preserved. Future mutations on the returned instance do not affect this
866
- * one (and vice versa).
867
- */
868
- clone() {
869
- const next = new Hooks();
870
- const names = Object.keys(this.items);
871
- for (const name of names) {
872
- const entries = this.items[name];
873
- for (const entry of entries) next.addListener(name, entry.fn, entry.priority);
874
- }
875
- return next;
876
- }
877
- async trigger(name, event) {
878
- if (!this.items[name] || this.items[name].length === 0) return;
879
- try {
880
- for (let i = 0; i < this.items[name].length; i++) {
881
- const { fn } = this.items[name][i];
882
- await this.triggerListener(name, event, fn);
883
- if (event.dispatched) {
884
- if (event.error) event.error = void 0;
885
- return;
886
- }
887
- }
888
- } catch (e) {
889
- if (!event.error) event.error = createError(e);
890
- if (!this.isErrorListenerHook(name)) {
891
- await this.trigger(HookName.ERROR, event);
892
- if (event.dispatched) {
893
- if (event.error) event.error = void 0;
894
- }
895
- }
896
- }
897
- }
898
- triggerListener(name, event, listener) {
899
- if (this.isErrorListenerHook(name)) {
900
- if (event.error) return listener(event);
901
- return;
902
- }
903
- return listener(event);
904
- }
905
- isErrorListenerHook(input) {
906
- return input === HookName.ERROR;
907
- }
908
- };
909
- //#endregion
910
- //#region src/path/matcher.ts
911
- function decodeParam(val) {
912
- /* istanbul ignore next */
913
- if (typeof val !== "string" || val.length === 0) return val;
914
- try {
915
- return decodeURIComponent(val);
916
- } catch {
917
- return val;
918
- }
919
- }
920
- var PathMatcher = class {
921
- path;
922
- regexp;
923
- regexpKeys = [];
924
- regexpOptions;
925
- constructor(path, options) {
926
- this.path = path;
927
- this.regexpOptions = options || {};
928
- const regexp = pathToRegexp(path, options);
929
- this.regexp = regexp.regexp;
930
- this.regexpKeys = regexp.keys;
931
- }
932
- test(path) {
933
- return this.regexp.test(path);
934
- }
935
- exec(path) {
936
- if (this.path === "/" && this.regexpOptions.end === false) return {
937
- path: "/",
938
- params: Object.create(null)
939
- };
940
- const match = this.regexp.exec(path);
941
- if (!match) return;
942
- const params = Object.create(null);
943
- for (let i = 1; i < match.length; i++) {
944
- const key = this.regexpKeys[i - 1];
945
- if (!key) continue;
946
- const prop = key.name;
947
- const val = decodeParam(match[i]);
948
- if (typeof val !== "undefined") params[prop] = val;
949
- }
950
- return {
951
- path: match[0],
952
- params
953
- };
954
- }
955
- };
956
- //#endregion
957
- //#region src/path/utils.ts
958
- function isPath(input) {
959
- return typeof input === "string";
960
- }
961
- //#endregion
962
- //#region src/handler/utils.ts
963
- /**
964
- * Build a `PathMatcher` for a handler-side path.
965
- *
966
- * Returns `undefined` when no path is supplied. The `end` flag controls
967
- * whether the matcher requires a full match (`true` for method handlers
968
- * matching exact routes) or accepts a prefix (`false` for middleware).
969
- */
970
- function buildHandlerPathMatcher(path, end) {
971
- if (typeof path === "undefined") return;
972
- return new PathMatcher(typeof path === "string" ? withLeadingSlash(path) : path, { end });
973
- }
974
- /**
975
- * Match a request method against a handler's bound method.
976
- *
977
- * - When the handler has no method bound, matches every request method.
978
- * - Otherwise matches when the request method is the same.
979
- * - HEAD requests additionally match GET handlers.
980
- */
981
- function matchHandlerMethod(handlerMethod, requestMethod) {
982
- return !handlerMethod || requestMethod === handlerMethod || requestMethod === MethodName.HEAD && handlerMethod === MethodName.GET;
983
- }
984
- //#endregion
985
- //#region src/handler/module.ts
986
- var Handler = class {
987
- config;
988
- hooks;
989
- pathMatcher;
990
- method;
991
- constructor(handler) {
992
- this.config = handler;
993
- this.hooks = new Hooks();
994
- this.mountHooks();
995
- if (typeof handler.path === "string") this.config.path = withLeadingSlash(handler.path);
996
- this.pathMatcher = buildHandlerPathMatcher(this.config.path, !!this.config.method);
997
- this.method = this.config.method ? toMethodName(this.config.method) : void 0;
998
- markInstanceof(this, HandlerSymbol);
999
- }
1000
- get type() {
1001
- return this.config.type;
1002
- }
1003
- get path() {
1004
- return this.config.path;
1005
- }
1006
- async dispatch(event) {
1007
- if (this.pathMatcher) {
1008
- const pathMatch = this.pathMatcher.exec(event.path);
1009
- if (pathMatch) event.params = {
1010
- ...event.params,
1011
- ...pathMatch.params
1012
- };
1013
- }
1014
- await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, event);
1015
- if (event.dispatched) return;
1016
- let response;
1017
- try {
1018
- let result;
1019
- const previewEvent = event.build();
1020
- const effectiveTimeout = this.resolveTimeout(previewEvent.routerOptions);
1021
- let childController;
1022
- let cleanupParentListener;
1023
- let handlerEvent = previewEvent;
1024
- if (effectiveTimeout) {
1025
- const parentSignal = event.signal;
1026
- childController = new AbortController();
1027
- if (parentSignal.aborted) childController.abort(parentSignal.reason);
1028
- else {
1029
- const onAbort = () => childController.abort(parentSignal.reason);
1030
- parentSignal.addEventListener("abort", onAbort, { once: true });
1031
- cleanupParentListener = () => parentSignal.removeEventListener("abort", onAbort);
1032
- }
1033
- handlerEvent = event.build(childController.signal);
1034
- }
1035
- try {
1036
- if (this.config.type === HandlerType.ERROR) {
1037
- if (event.error) {
1038
- const { fn } = this.config;
1039
- const { error } = event;
1040
- result = await this.executeWithTimeout(() => this.resolveHandlerResult(fn(error, handlerEvent), handlerEvent), handlerEvent.routerOptions, childController);
1041
- }
1042
- } else {
1043
- const { fn } = this.config;
1044
- result = await this.executeWithTimeout(() => this.resolveHandlerResult(fn(handlerEvent), handlerEvent), handlerEvent.routerOptions, childController);
1045
- }
1046
- } finally {
1047
- if (cleanupParentListener) cleanupParentListener();
1048
- }
1049
- response = await toResponse(result, handlerEvent);
1050
- if (response) event.dispatched = true;
1051
- } catch (e) {
1052
- event.error = isError(e) ? e : createError(e);
1053
- await this.hooks.trigger(HookName.ERROR, event);
1054
- if (event.dispatched) event.error = void 0;
1055
- else throw event.error;
1056
- }
1057
- await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, event);
1058
- return response;
1059
- }
1060
- matchPath(path) {
1061
- if (!this.pathMatcher) return true;
1062
- return this.pathMatcher.test(path);
1063
- }
1064
- /**
1065
- * Resolve a handler's return value into the final value handed to `toResponse`.
1066
- *
1067
- * Contract:
1068
- * - non-undefined value → return as-is (becomes the response)
1069
- * - `undefined` + `event.next()` was called → forward downstream result
1070
- * - `undefined` + `event.next()` not yet called → wait until either `next()` is
1071
- * invoked (e.g. from an async callback) or `signal` aborts. A global or
1072
- * per-handler timeout aborts `signal` and surfaces as 408. With no timeout
1073
- * configured and no eventual `next()` call, the request hangs by design.
1074
- */
1075
- async resolveHandlerResult(invocation, handlerEvent) {
1076
- const value = await invocation;
1077
- if (typeof value !== "undefined") return value;
1078
- if (handlerEvent.nextCalled) return handlerEvent.nextResult;
1079
- const { signal } = handlerEvent;
1080
- if (signal.aborted) throw createError({
1081
- status: 408,
1082
- message: "Request Timeout"
1083
- });
1084
- return new Promise((resolve, reject) => {
1085
- const onAbort = () => {
1086
- signal.removeEventListener("abort", onAbort);
1087
- reject(createError({
1088
- status: 408,
1089
- message: "Request Timeout"
1090
- }));
1091
- };
1092
- signal.addEventListener("abort", onAbort, { once: true });
1093
- handlerEvent.whenNextCalled().then(() => {
1094
- signal.removeEventListener("abort", onAbort);
1095
- resolve(handlerEvent.nextResult);
1096
- });
1097
- });
1098
- }
1099
- async executeWithTimeout(fn, routerOptions, controller) {
1100
- const effectiveTimeout = this.resolveTimeout(routerOptions);
1101
- if (!effectiveTimeout) return fn();
1102
- let timerId;
1103
- try {
1104
- return await Promise.race([fn(), new Promise((_, reject) => {
1105
- timerId = setTimeout(() => {
1106
- if (controller) controller.abort();
1107
- reject(createError({
1108
- status: 408,
1109
- message: "Request Timeout"
1110
- }));
1111
- }, effectiveTimeout);
1112
- })]);
1113
- } finally {
1114
- clearTimeout(timerId);
1115
- }
1116
- }
1117
- resolveTimeout(routerOptions) {
1118
- const routerDefault = routerOptions.handlerTimeout;
1119
- const handlerOverride = this.config.timeout;
1120
- if (!routerDefault && !handlerOverride) return;
1121
- if (!routerDefault) return handlerOverride;
1122
- if (!handlerOverride) return routerDefault;
1123
- if (routerOptions.handlerTimeoutOverridable) return handlerOverride;
1124
- return Math.min(routerDefault, handlerOverride);
1125
- }
1126
- mountHooks() {
1127
- if (this.config.onBefore) this.hooks.addListener(HookName.CHILD_DISPATCH_BEFORE, this.config.onBefore);
1128
- if (this.config.onAfter) this.hooks.addListener(HookName.CHILD_DISPATCH_AFTER, this.config.onAfter);
1129
- if (this.config.onError) this.hooks.addListener(HookName.ERROR, this.config.onError);
1130
- }
1131
- };
1132
- //#endregion
1133
- //#region src/handler/core/define.ts
1134
- function defineCoreHandler(input) {
1135
- if (typeof input === "function") return new Handler({
1136
- type: HandlerType.CORE,
1137
- fn: input
1138
- });
1139
- return new Handler({
1140
- type: HandlerType.CORE,
1141
- ...input
1142
- });
1143
- }
1144
- //#endregion
1145
- //#region src/handler/error/define.ts
1146
- function defineErrorHandler(input) {
1147
- if (typeof input === "function") return new Handler({
1148
- type: HandlerType.ERROR,
1149
- fn: input
1150
- });
1151
- return new Handler({
1152
- type: HandlerType.ERROR,
1153
- ...input
1154
- });
1155
- }
1156
- //#endregion
1157
- //#region src/handler/adapters/node/define.ts
1158
- const kHandled = /* @__PURE__ */ Symbol("handled");
1159
- function callHandler(handler, req, res) {
1160
- return new Promise((resolve, reject) => {
1161
- let settled = false;
1162
- const onClose = () => settle(kHandled);
1163
- const onFinish = () => settle(kHandled);
1164
- const onError = (error) => fail(error);
1165
- function cleanup() {
1166
- res.removeListener("close", onClose);
1167
- res.removeListener("finish", onFinish);
1168
- res.removeListener("error", onError);
1169
- }
1170
- function settle(value) {
1171
- if (settled) return;
1172
- settled = true;
1173
- cleanup();
1174
- resolve(value);
1175
- }
1176
- function fail(error) {
1177
- if (settled) return;
1178
- settled = true;
1179
- cleanup();
1180
- reject(error);
1181
- }
1182
- res.once("close", onClose);
1183
- res.once("finish", onFinish);
1184
- res.once("error", onError);
1185
- try {
1186
- Promise.resolve(handler(req, res)).then(() => settle(kHandled)).catch(fail);
1187
- } catch (error) {
1188
- fail(error);
1189
- }
1190
- });
1191
- }
1192
- function callMiddleware(handler, req, res) {
1193
- return new Promise((resolve, reject) => {
1194
- let settled = false;
1195
- const onClose = () => settle(kHandled);
1196
- const onFinish = () => settle(kHandled);
1197
- const onError = (error) => fail(error);
1198
- function cleanup() {
1199
- res.removeListener("close", onClose);
1200
- res.removeListener("finish", onFinish);
1201
- res.removeListener("error", onError);
1202
- }
1203
- function settle(value) {
1204
- if (settled) return;
1205
- settled = true;
1206
- cleanup();
1207
- resolve(value);
1208
- }
1209
- function fail(error) {
1210
- if (settled) return;
1211
- settled = true;
1212
- cleanup();
1213
- reject(error);
1214
- }
1215
- res.once("close", onClose);
1216
- res.once("finish", onFinish);
1217
- res.once("error", onError);
1218
- try {
1219
- Promise.resolve(handler(req, res, (error) => {
1220
- if (error) fail(error);
1221
- else settle(res.writableEnded || res.destroyed ? kHandled : void 0);
1222
- })).catch(fail);
1223
- } catch (error) {
1224
- fail(error);
1225
- }
1226
- });
1227
- }
1228
- function createNodeBridge(handler, isMiddleware) {
1229
- if (typeof handler !== "function") throw new RoutupError("fromNodeHandler/fromNodeMiddleware expects a function.");
1230
- return defineCoreHandler({ fn: (async (event) => {
1231
- const node = event.request.runtime?.node;
1232
- if (!node?.req || !node?.res) throw new RoutupError("fromNodeHandler/fromNodeMiddleware requires a Node.js runtime.");
1233
- const req = node.req;
1234
- const res = node.res;
1235
- if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) return null;
1236
- return event.next();
1237
- }) });
1238
- }
1239
- /**
1240
- * Wraps a Node.js `(req, res)` handler for use in the routup pipeline.
1241
- *
1242
- * @example
1243
- * ```typescript
1244
- * import { fromNodeHandler } from 'routup/node';
1245
- *
1246
- * router.use(fromNodeHandler((req, res) => {
1247
- * res.end('Hello');
1248
- * }));
1249
- * ```
1250
- */
1251
- function fromNodeHandler(handler) {
1252
- return createNodeBridge(handler, false);
1253
- }
1254
- /**
1255
- * Wraps a Node.js `(req, res, next)` middleware for use in the routup pipeline.
1256
- *
1257
- * @example
1258
- * ```typescript
1259
- * import cors from 'cors';
1260
- * import { fromNodeMiddleware } from 'routup/node';
1261
- *
1262
- * router.use(fromNodeMiddleware(cors()));
1263
- * ```
1264
- */
1265
- function fromNodeMiddleware(handler) {
1266
- return createNodeBridge(handler, true);
1267
- }
1268
- //#endregion
1269
- //#region src/handler/adapters/web/is.ts
1270
- function isWebHandlerProvider(input) {
1271
- return isObject(input) && typeof input.fetch === "function";
1272
- }
1273
- function isWebHandler(input) {
1274
- return typeof input === "function";
1275
- }
1276
- //#endregion
1277
- //#region src/handler/adapters/web/define.ts
1278
- function fromWebHandler(input) {
1279
- if (isWebHandlerProvider(input)) return fromWebHandler(input.fetch.bind(input));
1280
- if (typeof input !== "function") throw new RoutupError("fromWebHandler expects a function or an object with a fetch method.");
1281
- return defineCoreHandler({ fn: (event) => input(event.request) });
1282
- }
1283
- //#endregion
1284
- //#region src/handler/is.ts
1285
- function isHandlerOptions(input) {
1286
- return isObject(input) && typeof input.fn === "function" && typeof input.type === "string";
1287
- }
1288
- function isHandler(input) {
1289
- return hasInstanceof(input, HandlerSymbol);
1290
- }
1291
- //#endregion
1292
- //#region src/request/helpers/cache.ts
1293
- function isRequestCacheable(event, modifiedTime) {
1294
- const modifiedSince = event.headers.get(HeaderName.IF_MODIFIED_SINCE);
1295
- if (!modifiedSince) return false;
1296
- modifiedTime = typeof modifiedTime === "string" ? new Date(modifiedTime) : modifiedTime;
1297
- const sinceDate = new Date(modifiedSince);
1298
- if (Number.isNaN(sinceDate.getTime()) || Number.isNaN(modifiedTime.getTime())) return false;
1299
- return sinceDate >= modifiedTime;
1300
- }
1301
- //#endregion
1302
- //#region src/request/helpers/header-accept-charset.ts
1303
- function getRequestAcceptableCharsets(event) {
1304
- return useRequestNegotiator(event).charsets();
1305
- }
1306
- function getRequestAcceptableCharset(event, input) {
1307
- input = input || [];
1308
- const items = Array.isArray(input) ? input : [input];
1309
- if (items.length === 0) return getRequestAcceptableCharsets(event).shift();
1310
- return useRequestNegotiator(event).charsets(items).shift() || void 0;
1311
- }
1312
- //#endregion
1313
- //#region src/request/helpers/header-accept-encoding.ts
1314
- function getRequestAcceptableEncodings(event) {
1315
- return useRequestNegotiator(event).encodings();
1316
- }
1317
- function getRequestAcceptableEncoding(event, input) {
1318
- input = input || [];
1319
- const items = Array.isArray(input) ? input : [input];
1320
- if (items.length === 0) return getRequestAcceptableEncodings(event).shift();
1321
- return useRequestNegotiator(event).encodings(items).shift() || void 0;
1322
- }
1323
- //#endregion
1324
- //#region src/request/helpers/header-accept-language.ts
1325
- function getRequestAcceptableLanguages(event) {
1326
- return useRequestNegotiator(event).languages();
1327
- }
1328
- function getRequestAcceptableLanguage(event, input) {
1329
- input = input || [];
1330
- const items = Array.isArray(input) ? input : [input];
1331
- if (items.length === 0) return getRequestAcceptableLanguages(event).shift();
1332
- return useRequestNegotiator(event).languages(items).shift() || void 0;
1333
- }
1334
- //#endregion
1335
- //#region src/request/helpers/header-content-type.ts
1336
- function matchRequestContentType(event, contentType) {
1337
- const header = getRequestHeader(event, HeaderName.CONTENT_TYPE);
1338
- if (!header) return true;
1339
- return header.split(";")[0].trim() === getMimeType(contentType);
1340
- }
1341
- //#endregion
1342
- //#region src/request/helpers/hostname.ts
1343
- function getRequestHostName(event, options = {}) {
1344
- let trustProxy;
1345
- if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1346
- else trustProxy = event.routerOptions.trustProxy;
1347
- let hostname = event.headers.get(HeaderName.X_FORWARDED_HOST);
1348
- if (!hostname || !event.request.ip || !trustProxy(event.request.ip, 0)) hostname = event.headers.get(HeaderName.HOST);
1349
- else if (hostname && hostname.includes(",")) hostname = hostname.substring(0, hostname.indexOf(",")).trimEnd();
1350
- if (!hostname) return;
1351
- const offset = hostname[0] === "[" ? hostname.indexOf("]") + 1 : 0;
1352
- const index = hostname.indexOf(":", offset);
1353
- const result = index !== -1 ? hostname.substring(0, index) : hostname;
1354
- if (/[\x00-\x1F\x7F\s/@\\]/.test(result)) return;
1355
- return result;
1356
- }
1357
- //#endregion
1358
- //#region src/request/helpers/ip.ts
1359
- /**
1360
- * Get the client IP address from the request.
1361
- *
1362
- * When `trustProxy` is configured, walks the `X-Forwarded-For` chain
1363
- * and returns the rightmost untrusted address (the actual client IP).
1364
- * Falls back to `event.request.ip` (the direct connection IP).
1365
- */
1366
- function getRequestIP(event, options = {}) {
1367
- let trustProxy;
1368
- if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1369
- else trustProxy = event.routerOptions.trustProxy;
1370
- const socketAddr = event.request.ip;
1371
- if (!socketAddr) return;
1372
- const forwarded = event.headers.get(HeaderName.X_FORWARDED_FOR);
1373
- const addrs = [socketAddr];
1374
- if (forwarded) {
1375
- const parts = forwarded.split(",");
1376
- for (let i = parts.length - 1; i >= 0; i--) {
1377
- const addr = parts[i].trim();
1378
- if (addr) addrs.push(addr);
1379
- }
1380
- }
1381
- for (let i = 0; i < addrs.length - 1; i++) if (!trustProxy(addrs[i], i)) return addrs[i];
1382
- return addrs[addrs.length - 1];
1383
- }
1384
- //#endregion
1385
- //#region src/request/helpers/protocol.ts
1386
- function getRequestProtocol(event, options = {}) {
1387
- let trustProxy;
1388
- if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1389
- else trustProxy = event.routerOptions.trustProxy;
1390
- let protocol;
1391
- try {
1392
- if (new URL(event.request.url).protocol === "https:") protocol = "https";
1393
- else protocol = "http";
1394
- } catch {
1395
- protocol = options.default || "http";
1396
- }
1397
- if (!event.request.ip || !trustProxy(event.request.ip, 0)) return protocol;
1398
- const header = event.headers.get(HeaderName.X_FORWARDED_PROTO);
1399
- if (!header) return protocol;
1400
- const index = header.indexOf(",");
1401
- const forwarded = index !== -1 ? header.substring(0, index).trim().toLowerCase() : header.trim().toLowerCase();
1402
- if (forwarded === "http" || forwarded === "https") return forwarded;
1403
- return protocol;
1404
- }
1405
- //#endregion
1406
- //#region src/plugin/error/constants.ts
1407
- const PluginErrorCode = {
1408
- PLUGIN: "PLUGIN",
1409
- NOT_INSTALLED: "PLUGIN_NOT_INSTALLED",
1410
- ALREADY_INSTALLED: "PLUGIN_ALREADY_INSTALLED",
1411
- INSTALL: "PLUGIN_INSTALL"
1412
- };
1413
- //#endregion
1414
- //#region src/plugin/error/is.ts
1415
- const PLUGIN_ERROR_CODES = new Set(Object.values(PluginErrorCode));
1416
- function isPluginError(input) {
1417
- if (!isError(input)) return false;
1418
- return PLUGIN_ERROR_CODES.has(input.code);
1419
- }
1420
- //#endregion
1421
- //#region src/plugin/error/module.ts
1422
- var PluginError = class extends RoutupError {
1423
- constructor(input = {}) {
1424
- const options = typeof input === "string" ? { message: input } : { ...input };
1425
- if (!("code" in options) || !options.code) options.code = PluginErrorCode.PLUGIN;
1426
- super(options);
1427
- this.name = "PluginError";
1428
- }
1429
- };
1430
- //#endregion
1431
- //#region src/plugin/error/sub/already-installed.ts
1432
- var PluginAlreadyInstalledError = class extends PluginError {
1433
- pluginName;
1434
- constructor(pluginName) {
1435
- super({
1436
- message: `Plugin "${pluginName}" is already installed on this router.`,
1437
- code: PluginErrorCode.ALREADY_INSTALLED
1438
- });
1439
- this.name = "PluginAlreadyInstalledError";
1440
- this.pluginName = pluginName;
1441
- }
1442
- };
1443
- //#endregion
1444
- //#region src/plugin/error/sub/install.ts
1445
- var PluginInstallError = class extends PluginError {
1446
- pluginName;
1447
- constructor(pluginName, cause) {
1448
- super({
1449
- message: `Failed to install plugin "${pluginName}".`,
1450
- code: PluginErrorCode.INSTALL,
1451
- cause
1452
- });
1453
- this.name = "PluginInstallError";
1454
- this.pluginName = pluginName;
1455
- }
1456
- };
1457
- //#endregion
1458
- //#region src/plugin/error/sub/not-installed.ts
1459
- var PluginNotInstalledError = class extends PluginError {
1460
- pluginName;
1461
- helperName;
1462
- constructor(pluginName, helperName) {
1463
- super({
1464
- message: `${helperName}() requires the "${pluginName}" plugin. Register it with: router.use(${pluginName}())`,
1465
- code: PluginErrorCode.NOT_INSTALLED
1466
- });
1467
- this.name = "PluginNotInstalledError";
1468
- this.pluginName = pluginName;
1469
- this.helperName = helperName;
1470
- }
1471
- };
1472
- //#endregion
1473
- //#region src/plugin/is.ts
1474
- function isPlugin(input) {
1475
- if (!isObject(input)) return false;
1476
- if (typeof input.name !== "undefined" && typeof input.name !== "string") return false;
1477
- return typeof input.install === "function" && input.install.length === 1;
1478
- }
1479
- //#endregion
1480
- //#region src/router/options.ts
1481
- function normalizeRouterOptions(input) {
1482
- if (typeof input.etag !== "undefined") input.etag = buildEtagFn(input.etag);
1483
- if (typeof input.trustProxy !== "undefined") input.trustProxy = buildTrustProxyFn(input.trustProxy);
1484
- if (typeof input.timeout !== "undefined") {
1485
- if (typeof input.timeout !== "number" || !Number.isFinite(input.timeout) || input.timeout <= 0) delete input.timeout;
1486
- }
1487
- if (typeof input.handlerTimeout !== "undefined") {
1488
- if (typeof input.handlerTimeout !== "number" || !Number.isFinite(input.handlerTimeout) || input.handlerTimeout <= 0) delete input.handlerTimeout;
1489
- }
1490
- return input;
1491
- }
1492
- //#endregion
1493
- //#region src/router/constants.ts
1494
- const RouterSymbol = Symbol.for("Router");
1495
- const RouterPipelineStep = {
1496
- START: 0,
1497
- LOOKUP: 1,
1498
- CHILD_BEFORE: 2,
1499
- CHILD_DISPATCH: 3,
1500
- CHILD_AFTER: 4,
1501
- FINISH: 5
1502
- };
1503
- const RouterStackEntryType = {
1504
- ROUTER: "router",
1505
- HANDLER: "handler"
1506
- };
1507
- //#endregion
1508
- //#region src/router/utils.ts
1509
- function isRouterInstance(input) {
1510
- return hasInstanceof(input, RouterSymbol);
1511
- }
1512
- /**
1513
- * Build a non-terminal `PathMatcher` for a router mount path.
1514
- *
1515
- * Returns `undefined` when the path is the root (`/`) or omitted entirely —
1516
- * a router mounted at the root has no intrinsic path filter.
1517
- */
1518
- function buildRouterPathMatcher(value) {
1519
- if (value === "/" || typeof value === "undefined") return;
1520
- return new PathMatcher(withLeadingSlash(withoutTrailingSlash(`${value}`)), { end: false });
1521
- }
1522
- /**
1523
- * Check if the request accepts JSON responses.
1524
- * Matches application/json and +json suffixes (e.g. application/vnd.api+json).
1525
- * Returns true if no Accept header is present (API-first default).
1526
- */
1527
- function acceptsJson(request) {
1528
- const accept = request.headers.get("accept");
1529
- if (!accept) return true;
1530
- return accept.includes("application/json") || accept.includes("+json") || accept.includes("*/*");
1531
- }
1532
- //#endregion
1533
- //#region src/router/module.ts
1534
- const METHOD_TO_REGISTER = {
1535
- [MethodName.GET]: "get",
1536
- [MethodName.POST]: "post",
1537
- [MethodName.PUT]: "put",
1538
- [MethodName.PATCH]: "patch",
1539
- [MethodName.DELETE]: "delete",
1540
- [MethodName.HEAD]: "head",
1541
- [MethodName.OPTIONS]: "options"
1542
- };
1543
- var Router = class Router {
1544
- /**
1545
- * A label for the router instance.
1546
- */
1547
- name;
1548
- /**
1549
- * Array of mounted layers, routes & routers, each tagged by kind so the
1550
- * dispatch loop can discriminate without `isRouterInstance`/`isHandler`
1551
- * runtime checks.
1552
- *
1553
- * @protected
1554
- */
1555
- stack = [];
1556
- /**
1557
- * Path matcher for the current mount path.
1558
- *
1559
- * @protected
1560
- */
1561
- pathMatcher;
1562
- /**
1563
- * Lifecycle hook registry.
1564
- *
1565
- * @protected
1566
- */
1567
- hooks;
1568
- /**
1569
- * Normalized options for this router instance.
1570
- */
1571
- _options;
1572
- /**
1573
- * Registry of installed plugins (name → version) on this router.
1574
- *
1575
- * @protected
1576
- */
1577
- plugins = /* @__PURE__ */ new Map();
1578
- constructor(input = {}) {
1579
- this.name = input.name;
1580
- const { hooks = new Hooks(), plugins = /* @__PURE__ */ new Map(), ...options } = input;
1581
- this.hooks = hooks;
1582
- this.plugins = new Map(plugins);
1583
- this._options = normalizeRouterOptions(options);
1584
- this.pathMatcher = buildRouterPathMatcher(options.path);
1585
- markInstanceof(this, RouterSymbol);
1586
- }
1587
- matchPath(path) {
1588
- if (this.pathMatcher) return this.pathMatcher.test(path);
1589
- return true;
1590
- }
1591
- /**
1592
- * Public entry point — creates a DispatcherEvent from the request,
1593
- * runs the pipeline, and returns a Response (with 404/500 fallbacks).
1594
- */
1595
- async fetch(request) {
1596
- const event = new DispatcherEvent(request);
1597
- let response;
1598
- try {
1599
- const timeoutMs = this._options.timeout;
1600
- if (timeoutMs) {
1601
- const controller = new AbortController();
1602
- event.signal = controller.signal;
1603
- let timerId;
1604
- try {
1605
- response = await Promise.race([this.dispatch(event), new Promise((_, reject) => {
1606
- timerId = setTimeout(() => {
1607
- controller.abort();
1608
- reject(createError({
1609
- status: 408,
1610
- message: "Request Timeout"
1611
- }));
1612
- }, timeoutMs);
1613
- })]);
1614
- } finally {
1615
- clearTimeout(timerId);
1616
- }
1617
- } else response = await this.dispatch(event);
1618
- } catch (e) {
1619
- event.error = createError(e);
1620
- }
1621
- if (response) return response;
1622
- if (event.error) return this.buildFallbackResponse(request, event, event.error.status || 500, event.error.message);
1623
- return this.buildFallbackResponse(request, event, 404, "Not Found");
1624
- }
1625
- buildFallbackResponse(request, event, status, message) {
1626
- const headers = new Headers(event.response.headers);
1627
- if (acceptsJson(request)) {
1628
- headers.set("content-type", "application/json; charset=utf-8");
1629
- return new Response(JSON.stringify({
1630
- status,
1631
- message
1632
- }), {
1633
- status,
1634
- headers
1635
- });
1636
- }
1637
- headers.set("content-type", "text/plain; charset=utf-8");
1638
- return new Response(message, {
1639
- status,
1640
- headers
1641
- });
1642
- }
1643
- async executePipelineStep(context) {
1644
- while (context.step !== RouterPipelineStep.FINISH) switch (context.step) {
1645
- case RouterPipelineStep.START:
1646
- await this.executePipelineStepStart(context);
1647
- break;
1648
- case RouterPipelineStep.LOOKUP:
1649
- await this.executePipelineStepLookup(context);
1650
- break;
1651
- case RouterPipelineStep.CHILD_BEFORE:
1652
- await this.executePipelineStepChildBefore(context);
1653
- break;
1654
- case RouterPipelineStep.CHILD_DISPATCH:
1655
- await this.executePipelineStepChildDispatch(context);
1656
- break;
1657
- case RouterPipelineStep.CHILD_AFTER:
1658
- await this.executePipelineStepChildAfter(context);
1659
- break;
1660
- default:
1661
- context.step = RouterPipelineStep.FINISH;
1662
- break;
1663
- }
1664
- await this.executePipelineStepFinish(context);
1665
- }
1666
- async executePipelineStepStart(context) {
1667
- await this.hooks.trigger(HookName.REQUEST, context.event);
1668
- if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1669
- else context.step = RouterPipelineStep.LOOKUP;
1670
- }
1671
- async executePipelineStepLookup(context) {
1672
- while (!context.event.dispatched && context.stackIndex < this.stack.length) {
1673
- const entry = this.stack[context.stackIndex];
1674
- if (entry.type === RouterStackEntryType.HANDLER) {
1675
- const handler = entry.data;
1676
- if (context.event.error && handler.type === HandlerType.CORE || !context.event.error && handler.type === HandlerType.ERROR) {
1677
- context.stackIndex++;
1678
- continue;
1679
- }
1680
- if (entry.pathMatcher ? entry.pathMatcher.test(context.event.path) : handler.matchPath(context.event.path)) {
1681
- const method = entry.method ?? handler.method;
1682
- if (method) context.event.methodsAllowed.add(method);
1683
- if (matchHandlerMethod(method, context.event.method)) {
1684
- await this.hooks.trigger(HookName.CHILD_MATCH, context.event);
1685
- if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1686
- else context.step = RouterPipelineStep.CHILD_BEFORE;
1687
- return;
1688
- }
1689
- }
1690
- context.stackIndex++;
1691
- continue;
1692
- }
1693
- if (entry.pathMatcher ? entry.pathMatcher.test(context.event.path) : entry.data.matchPath(context.event.path)) {
1694
- await this.hooks.trigger(HookName.CHILD_MATCH, context.event);
1695
- if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1696
- else context.step = RouterPipelineStep.CHILD_BEFORE;
1697
- return;
1698
- }
1699
- context.stackIndex++;
1700
- }
1701
- context.step = RouterPipelineStep.FINISH;
1702
- }
1703
- async executePipelineStepChildBefore(context) {
1704
- await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, context.event);
1705
- if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1706
- else context.step = RouterPipelineStep.CHILD_DISPATCH;
1707
- }
1708
- async executePipelineStepChildAfter(context) {
1709
- await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, context.event);
1710
- if (context.event.dispatched) context.step = RouterPipelineStep.FINISH;
1711
- else context.step = RouterPipelineStep.LOOKUP;
1712
- }
1713
- async executePipelineStepChildDispatch(context) {
1714
- const entry = this.stack[context.stackIndex];
1715
- if (context.event.dispatched || typeof entry === "undefined") {
1716
- context.step = RouterPipelineStep.FINISH;
1717
- return;
1718
- }
1719
- const { event } = context;
1720
- const savedPath = event.path;
1721
- const savedMountPath = event.mountPath;
1722
- const savedParams = event.params;
1723
- if (entry.type === RouterStackEntryType.ROUTER && entry.pathMatcher) {
1724
- const output = entry.pathMatcher.exec(event.path);
1725
- if (typeof output !== "undefined") {
1726
- event.mountPath = cleanDoubleSlashes(`${event.mountPath}/${output.path}`);
1727
- if (event.path === output.path) event.path = "/";
1728
- else event.path = withLeadingSlash(event.path.substring(output.path.length));
1729
- event.params = {
1730
- ...event.params,
1731
- ...output.params
1732
- };
1733
- }
1734
- } else if (entry.type === RouterStackEntryType.HANDLER && entry.pathMatcher) {
1735
- const output = entry.pathMatcher.exec(event.path);
1736
- if (typeof output !== "undefined") event.params = {
1737
- ...event.params,
1738
- ...output.params
1739
- };
1740
- }
1741
- try {
1742
- event.setNext(async (error) => {
1743
- if (error) event.error = createError(error);
1744
- const nextContext = {
1745
- step: RouterPipelineStep.LOOKUP,
1746
- event,
1747
- stackIndex: context.stackIndex + 1,
1748
- response: void 0
1749
- };
1750
- await this.executePipelineStep(nextContext);
1751
- return nextContext.response;
1752
- });
1753
- const response = await entry.data.dispatch(event);
1754
- if (response) {
1755
- context.response = response;
1756
- event.dispatched = true;
1757
- }
1758
- } catch (e) {
1759
- event.error = createError(e);
1760
- await this.hooks.trigger(HookName.ERROR, event);
1761
- }
1762
- if (!event.dispatched) {
1763
- event.path = savedPath;
1764
- event.mountPath = savedMountPath;
1765
- event.params = savedParams;
1766
- }
1767
- context.stackIndex++;
1768
- context.step = RouterPipelineStep.CHILD_AFTER;
1769
- }
1770
- async executePipelineStepFinish(context) {
1771
- if (context.event.error || context.event.dispatched) return this.hooks.trigger(HookName.RESPONSE, context.event);
1772
- if (!context.event.dispatched && context.event.routerPath.length === 1 && context.event.method && context.event.method === MethodName.OPTIONS) {
1773
- if (context.event.methodsAllowed.has(MethodName.GET)) context.event.methodsAllowed.add(MethodName.HEAD);
1774
- const options = [...context.event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
1775
- const optionsHeaders = new Headers(context.event.response.headers);
1776
- optionsHeaders.set(HeaderName.ALLOW, options);
1777
- context.response = new Response(options, {
1778
- status: context.event.response.status || 200,
1779
- headers: optionsHeaders
1780
- });
1781
- context.event.dispatched = true;
1782
- }
1783
- return this.hooks.trigger(HookName.RESPONSE, context.event);
1784
- }
1785
- async dispatch(event) {
1786
- const savedPath = event.path;
1787
- const savedMountPath = event.mountPath;
1788
- const savedParams = event.params;
1789
- if (this.pathMatcher) {
1790
- const output = this.pathMatcher.exec(event.path);
1791
- if (typeof output !== "undefined") {
1792
- event.mountPath = cleanDoubleSlashes(`${event.mountPath}/${output.path}`);
1793
- if (event.path === output.path) event.path = "/";
1794
- else event.path = withLeadingSlash(event.path.substring(output.path.length));
1795
- event.params = {
1796
- ...event.params,
1797
- ...output.params
1798
- };
1799
- }
1800
- }
1801
- const context = {
1802
- step: RouterPipelineStep.START,
1803
- event,
1804
- stackIndex: 0
1805
- };
1806
- event.routerPath.push({
1807
- name: this.name,
1808
- options: this._options
1809
- });
1810
- try {
1811
- await this.executePipelineStep(context);
1812
- } finally {
1813
- event.routerPath.pop();
1814
- if (!event.dispatched) {
1815
- event.path = savedPath;
1816
- event.mountPath = savedMountPath;
1817
- event.params = savedParams;
1818
- }
1819
- }
1820
- return context.response;
1821
- }
1822
- delete(...input) {
1823
- this.useForMethod(MethodName.DELETE, ...input);
1824
- return this;
1825
- }
1826
- get(...input) {
1827
- this.useForMethod(MethodName.GET, ...input);
1828
- return this;
1829
- }
1830
- post(...input) {
1831
- this.useForMethod(MethodName.POST, ...input);
1832
- return this;
1833
- }
1834
- put(...input) {
1835
- this.useForMethod(MethodName.PUT, ...input);
1836
- return this;
1837
- }
1838
- patch(...input) {
1839
- this.useForMethod(MethodName.PATCH, ...input);
1840
- return this;
1841
- }
1842
- head(...input) {
1843
- this.useForMethod(MethodName.HEAD, ...input);
1844
- return this;
1845
- }
1846
- options(...input) {
1847
- this.useForMethod(MethodName.OPTIONS, ...input);
1848
- return this;
1849
- }
1850
- useForMethod(method, ...input) {
1851
- let path;
1852
- for (const element of input) {
1853
- if (isPath(element)) {
1854
- path = element;
1855
- continue;
1856
- }
1857
- let handler;
1858
- if (isHandlerOptions(element)) handler = new Handler({
1859
- ...element,
1860
- method,
1861
- path: path ?? element.path
1862
- });
1863
- else if (isHandler(element)) handler = element;
1864
- else continue;
1865
- this.stack.push({
1866
- type: RouterStackEntryType.HANDLER,
1867
- data: handler,
1868
- method,
1869
- path,
1870
- pathMatcher: buildHandlerPathMatcher(path ?? handler.path, true)
1871
- });
1872
- }
1873
- }
1874
- use(...input) {
1875
- let path;
1876
- for (const item of input) {
1877
- if (isPath(item)) {
1878
- path = withLeadingSlash(item);
1879
- continue;
1880
- }
1881
- if (isRouterInstance(item)) {
1882
- this.stack.push({
1883
- type: RouterStackEntryType.ROUTER,
1884
- data: item,
1885
- path,
1886
- pathMatcher: buildRouterPathMatcher(path)
1887
- });
1888
- continue;
1889
- }
1890
- if (isHandlerOptions(item)) {
1891
- const handler = new Handler({
1892
- ...item,
1893
- path: path ?? item.path
1894
- });
1895
- this.stack.push({
1896
- type: RouterStackEntryType.HANDLER,
1897
- data: handler,
1898
- path
1899
- });
1900
- continue;
1901
- }
1902
- if (isHandler(item)) {
1903
- this.stack.push({
1904
- type: RouterStackEntryType.HANDLER,
1905
- data: item,
1906
- path,
1907
- pathMatcher: buildHandlerPathMatcher(path, !!item.method)
1908
- });
1909
- continue;
1910
- }
1911
- if (isPlugin(item)) if (path) this.install(item, { path });
1912
- else this.install(item);
1913
- }
1914
- return this;
1915
- }
1916
- /**
1917
- * Check if a plugin with the given name is installed on this router.
1918
- */
1919
- hasPlugin(name) {
1920
- return this.plugins.has(name);
1921
- }
1922
- /**
1923
- * Get the version of an installed plugin by name on this router,
1924
- * or `undefined` if the plugin is not installed here.
1925
- */
1926
- getPluginVersion(name) {
1927
- return this.plugins.get(name);
1928
- }
1929
- install(plugin, context = {}) {
1930
- if (this.plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
1931
- const router = new Router({ name: plugin.name });
1932
- plugin.install(router);
1933
- if (context.path) this.use(context.path, router);
1934
- else this.use(router);
1935
- this.plugins.set(plugin.name, plugin.version);
1936
- return this;
1937
- }
1938
- /**
1939
- * Return a new `Router` that mirrors this one but owns independent
1940
- * mountable state.
1941
- *
1942
- * The new router has:
1943
- * - a fresh `stack` array of shallow-copied entries (handlers and child
1944
- * routers are shared by reference; only the wrapping entries are new)
1945
- * - the same `pathMatcher` reference (it is stateless)
1946
- * - a fresh `Hooks` instance seeded with the current listeners
1947
- * - a shallow copy of `_options`
1948
- * - a fresh `plugins` map with the same entries
1949
- *
1950
- * Use this when the same logical router needs to be mounted under
1951
- * multiple paths — each mount can receive its own clone so subsequent
1952
- * mutations on one mount do not bleed into the others.
1953
- */
1954
- clone() {
1955
- const next = new Router({
1956
- ...this._options,
1957
- hooks: this.hooks.clone(),
1958
- plugins: this.plugins
1959
- });
1960
- for (const entry of this.stack) {
1961
- if (entry.type === RouterStackEntryType.ROUTER) {
1962
- const data = entry.data.clone();
1963
- if (entry.path) next.use(entry.path, data);
1964
- else next.use(data);
1965
- continue;
1966
- }
1967
- if (entry.method) {
1968
- const register = METHOD_TO_REGISTER[entry.method];
1969
- if (entry.path) next[register](entry.path, entry.data);
1970
- else next[register](entry.data);
1971
- continue;
1972
- }
1973
- if (entry.path) next.use(entry.path, entry.data);
1974
- else next.use(entry.data);
1975
- }
1976
- return next;
1977
- }
1978
- on(name, fn, priority) {
1979
- return this.hooks.addListener(name, fn, priority);
1980
- }
1981
- off(name, fn) {
1982
- if (typeof fn === "undefined") {
1983
- this.hooks.removeListener(name);
1984
- return this;
1985
- }
1986
- this.hooks.removeListener(name, fn);
1987
- return this;
1988
- }
1989
- };
1990
- //#endregion
1991
- export { setResponseHeaderAttachment as $, Handler as A, sendRedirect as B, fromWebHandler as C, fromNodeMiddleware as D, fromNodeHandler as E, HandlerSymbol as F, getRequestHeader as G, getRequestAcceptableContentType as H, HandlerType as I, sendAccepted as J, sendFile as K, DispatcherEvent as L, matchHandlerMethod as M, isPath as N, defineErrorHandler as O, PathMatcher as P, setResponseHeaderContentType as Q, RoutupEvent as R, isHandlerOptions as S, isWebHandlerProvider as T, getRequestAcceptableContentTypes as U, sendFormat as V, useRequestNegotiator as W, createError as X, toResponse as Y, isError as Z, getRequestAcceptableEncodings as _, PluginInstallError as a, serializeEventStreamMessage as at, isRequestCacheable as b, isPluginError as c, setResponseCacheHeaders as ct, getRequestIP as d, setResponseHeaderInline as et, getRequestHostName as f, getRequestAcceptableEncoding as g, getRequestAcceptableLanguages as h, PluginNotInstalledError as i, createEventStream as it, buildHandlerPathMatcher as j, defineCoreHandler as k, PluginErrorCode as l, HeaderName as lt, getRequestAcceptableLanguage as m, normalizeRouterOptions as n, appendResponseHeader as nt, PluginAlreadyInstalledError as o, ErrorSymbol as ot, matchRequestContentType as p, sendCreated as q, isPlugin as r, appendResponseHeaderDirective as rt, PluginError as s, RoutupError as st, Router as t, setResponseContentTypeByFileName as tt, getRequestProtocol as u, MethodName as ut, getRequestAcceptableCharset as v, isWebHandler as w, isHandler as x, getRequestAcceptableCharsets as y, sendStream as z };
1992
-
1993
- //# sourceMappingURL=src-DX0rndew.mjs.map