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.
@@ -0,0 +1,2694 @@
1
+ import QuickLRU from "quick-lru";
2
+ import { FastURL } from "srvx";
3
+ import { hasInstanceof, markInstanceof } from "@ebec/core";
4
+ import { HTTPError, isHTTPError } from "@ebec/http";
5
+ import { subtle } from "uncrypto";
6
+ import { merge } from "smob";
7
+ import { compile } from "proxy-addr";
8
+ import { get, getType } from "mime-explorer";
9
+ import Negotiator from "negotiator";
10
+ import { pathToRegexp } from "path-to-regexp";
11
+ //#region src/cache/lru.ts
12
+ const DEFAULT_MAX_SIZE = 1024;
13
+ /**
14
+ * Default `ICache` implementation — a bounded LRU backed by
15
+ * [`quick-lru`](https://github.com/sindresorhus/quick-lru). Picked for
16
+ * its small footprint (~1kB), ESM-only build (matches routup), and
17
+ * stable API.
18
+ *
19
+ * For TTL, size-based eviction, or dispose hooks, write your own
20
+ * `ICache` (e.g. wrapping `lru-cache`) and pass it via the router's
21
+ * `BaseRouterOptions.cache` slot.
22
+ */
23
+ var LruCache = class LruCache {
24
+ options;
25
+ inner;
26
+ constructor(options = {}) {
27
+ this.options = options;
28
+ this.inner = new QuickLRU({ maxSize: options.maxSize ?? DEFAULT_MAX_SIZE });
29
+ }
30
+ get(key) {
31
+ return this.inner.get(key);
32
+ }
33
+ set(key, value) {
34
+ this.inner.set(key, value);
35
+ }
36
+ delete(key) {
37
+ this.inner.delete(key);
38
+ }
39
+ clear() {
40
+ this.inner.clear();
41
+ }
42
+ clone() {
43
+ return new LruCache({ ...this.options });
44
+ }
45
+ };
46
+ //#endregion
47
+ //#region src/constants.ts
48
+ const MethodName = {
49
+ GET: "GET",
50
+ POST: "POST",
51
+ PUT: "PUT",
52
+ PATCH: "PATCH",
53
+ DELETE: "DELETE",
54
+ OPTIONS: "OPTIONS",
55
+ HEAD: "HEAD"
56
+ };
57
+ const HeaderName = {
58
+ ACCEPT: "accept",
59
+ ACCEPT_CHARSET: "accept-charset",
60
+ ACCEPT_ENCODING: "accept-encoding",
61
+ ACCEPT_LANGUAGE: "accept-language",
62
+ ACCEPT_RANGES: "accept-ranges",
63
+ ALLOW: "allow",
64
+ CACHE_CONTROL: "cache-control",
65
+ CONTENT_DISPOSITION: "content-disposition",
66
+ CONTENT_ENCODING: "content-encoding",
67
+ CONTENT_LENGTH: "content-length",
68
+ CONTENT_RANGE: "content-range",
69
+ CONTENT_TYPE: "content-type",
70
+ CONNECTION: "connection",
71
+ COOKIE: "cookie",
72
+ ETag: "etag",
73
+ HOST: "host",
74
+ IF_MODIFIED_SINCE: "if-modified-since",
75
+ IF_NONE_MATCH: "if-none-match",
76
+ LAST_MODIFIED: "last-modified",
77
+ LOCATION: "location",
78
+ RANGE: "range",
79
+ RATE_LIMIT_LIMIT: "ratelimit-limit",
80
+ RATE_LIMIT_REMAINING: "ratelimit-remaining",
81
+ RATE_LIMIT_RESET: "ratelimit-reset",
82
+ RETRY_AFTER: "retry-after",
83
+ SET_COOKIE: "set-cookie",
84
+ TRANSFER_ENCODING: "transfer-encoding",
85
+ X_ACCEL_BUFFERING: "x-accel-buffering",
86
+ X_FORWARDED_HOST: "x-forwarded-host",
87
+ X_FORWARDED_FOR: "x-forwarded-for",
88
+ X_FORWARDED_PROTO: "x-forwarded-proto"
89
+ };
90
+ //#endregion
91
+ //#region src/event/module.ts
92
+ var AppEvent = class {
93
+ request;
94
+ params;
95
+ path;
96
+ method;
97
+ mountPath;
98
+ headers;
99
+ searchParams;
100
+ response;
101
+ store;
102
+ signal;
103
+ appOptions;
104
+ _context;
105
+ _nextCalled = false;
106
+ _nextResult;
107
+ _nextCalledDeferred;
108
+ constructor(context) {
109
+ this._context = context;
110
+ this.request = context.request;
111
+ this.params = context.params;
112
+ this.path = context.path;
113
+ this.method = context.method;
114
+ this.mountPath = context.mountPath;
115
+ this.headers = context.headers;
116
+ this.searchParams = context.searchParams;
117
+ this.response = context.response;
118
+ this.store = context.store;
119
+ this.signal = context.signal;
120
+ this.appOptions = context.appOptions;
121
+ }
122
+ get nextCalled() {
123
+ return this._nextCalled;
124
+ }
125
+ get nextResult() {
126
+ return this._nextResult;
127
+ }
128
+ whenNextCalled() {
129
+ if (!this._nextCalledDeferred) {
130
+ let resolve;
131
+ const promise = new Promise((r) => {
132
+ resolve = r;
133
+ });
134
+ this._nextCalledDeferred = {
135
+ promise,
136
+ resolve
137
+ };
138
+ if (this._nextCalled) resolve();
139
+ }
140
+ return this._nextCalledDeferred.promise;
141
+ }
142
+ async next(error) {
143
+ if (this._nextCalled) return this._nextResult;
144
+ this._nextCalled = true;
145
+ this._nextResult = this._context.next(this, error);
146
+ if (this._nextCalledDeferred) this._nextCalledDeferred.resolve();
147
+ return this._nextResult;
148
+ }
149
+ };
150
+ //#endregion
151
+ //#region src/response/helpers/cache.ts
152
+ function setResponseCacheHeaders(event, options) {
153
+ options = options || {};
154
+ const cacheControls = ["public"].concat(options.cacheControls || []);
155
+ if (options.maxAge !== void 0) cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);
156
+ if (options.modifiedTime) {
157
+ const modifiedTime = typeof options.modifiedTime === "string" ? new Date(options.modifiedTime) : options.modifiedTime;
158
+ event.response.headers.set("last-modified", modifiedTime.toUTCString());
159
+ }
160
+ event.response.headers.set("cache-control", cacheControls.join(", "));
161
+ }
162
+ //#endregion
163
+ //#region src/error/module.ts
164
+ const ErrorSymbol = Symbol.for("AppError");
165
+ var AppError = class extends HTTPError {
166
+ constructor(input = {}) {
167
+ super(input);
168
+ this.name = "AppError";
169
+ markInstanceof(this, ErrorSymbol);
170
+ }
171
+ };
172
+ //#endregion
173
+ //#region src/response/helpers/event-stream/utils.ts
174
+ function stripNewlines(value) {
175
+ return value.replace(/[\r\n]/g, "");
176
+ }
177
+ function serializeEventStreamMessage(message) {
178
+ let result = "";
179
+ if (message.id) result += `id: ${stripNewlines(message.id)}\n`;
180
+ if (message.event) result += `event: ${stripNewlines(message.event)}\n`;
181
+ if (typeof message.retry === "number" && Number.isInteger(message.retry)) result += `retry: ${message.retry}\n`;
182
+ const lines = message.data.replace(/\r/g, "").split("\n");
183
+ for (const line of lines) result += `data: ${line}\n`;
184
+ result += "\n";
185
+ return result;
186
+ }
187
+ //#endregion
188
+ //#region src/response/helpers/event-stream/module.ts
189
+ function createEventStream(event, options) {
190
+ if (options?.maxMessageSize !== void 0) {
191
+ if (!Number.isInteger(options.maxMessageSize) || options.maxMessageSize < 0) throw new AppError("maxMessageSize must be a non-negative integer.");
192
+ }
193
+ let controller;
194
+ let closed = false;
195
+ const encoder = new TextEncoder();
196
+ const stream = new ReadableStream({
197
+ start(ctrl) {
198
+ controller = ctrl;
199
+ },
200
+ cancel() {
201
+ closed = true;
202
+ }
203
+ });
204
+ const headers = new Headers(event.response.headers);
205
+ headers.set(HeaderName.CONTENT_TYPE, "text/event-stream");
206
+ headers.set(HeaderName.CACHE_CONTROL, "private, no-cache, no-store, no-transform, must-revalidate, max-age=0");
207
+ headers.set(HeaderName.X_ACCEL_BUFFERING, "no");
208
+ headers.set(HeaderName.CONNECTION, "keep-alive");
209
+ const handle = {
210
+ write(message) {
211
+ if (closed) return false;
212
+ if (typeof message === "string") return handle.write({ data: message });
213
+ const serialized = serializeEventStreamMessage(message);
214
+ if (options?.maxMessageSize !== void 0) {
215
+ if (encoder.encode(serialized).byteLength > options.maxMessageSize) return false;
216
+ }
217
+ controller.enqueue(encoder.encode(serialized));
218
+ return true;
219
+ },
220
+ end() {
221
+ if (closed) return;
222
+ closed = true;
223
+ controller.close();
224
+ },
225
+ response: new Response(stream, {
226
+ status: event.response.status,
227
+ headers
228
+ })
229
+ };
230
+ return handle;
231
+ }
232
+ //#endregion
233
+ //#region src/utils/accepts-json.ts
234
+ /**
235
+ * Check if the request accepts JSON responses.
236
+ *
237
+ * Parses the `Accept` header per RFC 7231 (media ranges + quality
238
+ * params) rather than substring-matching, so:
239
+ * - `application/json-seq` does NOT count as accepting `application/json`
240
+ * - `application/json;q=0` is treated as an explicit rejection
241
+ *
242
+ * Returns true if no Accept header is present (API-first default).
243
+ */
244
+ function acceptsJson(request) {
245
+ const accept = request.headers.get("accept");
246
+ if (!accept) return true;
247
+ return accept.toLowerCase().split(",").some((entry) => {
248
+ const parts = entry.split(";").map((part) => part.trim());
249
+ const mediaRange = parts[0];
250
+ const qParam = parts.slice(1).map((param) => param.split("=").map((s) => s.trim())).find(([key]) => key === "q");
251
+ const q = qParam ? Number.parseFloat(qParam[1] ?? "") : 1;
252
+ if (!Number.isFinite(q) || q <= 0) return false;
253
+ return mediaRange === "*/*" || mediaRange === "application/*" || mediaRange === "application/json" || mediaRange.endsWith("+json");
254
+ });
255
+ }
256
+ //#endregion
257
+ //#region src/utils/header.ts
258
+ function sanitizeHeaderValue(value) {
259
+ return value.replace(/[\r\n]/g, "");
260
+ }
261
+ //#endregion
262
+ //#region src/utils/etag/module.ts
263
+ async function sha1(str) {
264
+ const enc = new TextEncoder();
265
+ const hash = await subtle.digest("SHA-1", enc.encode(str));
266
+ return btoa(String.fromCharCode(...new Uint8Array(hash)));
267
+ }
268
+ /**
269
+ * Generate an ETag.
270
+ */
271
+ async function generateETag(input) {
272
+ if (input.length === 0) return "\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"";
273
+ const hash = await sha1(input);
274
+ return `"${input.length.toString(16)}-${hash.substring(0, 27)}"`;
275
+ }
276
+ /**
277
+ * Create a simple ETag.
278
+ */
279
+ async function createEtag(input, options = {}) {
280
+ const tag = await generateETag(input);
281
+ return options.weak ? `W/${tag}` : tag;
282
+ }
283
+ //#endregion
284
+ //#region src/utils/object.ts
285
+ function isObject(item) {
286
+ return !!item && typeof item === "object" && !Array.isArray(item);
287
+ }
288
+ //#endregion
289
+ //#region src/utils/etag/utils.ts
290
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
291
+ function buildEtagFn(input) {
292
+ if (typeof input === "function") return input;
293
+ input = input ?? true;
294
+ if (input === false) return () => Promise.resolve(void 0);
295
+ let options = { weak: true };
296
+ if (isObject(input)) options = merge(input, options);
297
+ return async (body, size) => {
298
+ if (typeof options.threshold !== "undefined") {
299
+ if ((size ?? textEncoder.encode(body).byteLength) <= options.threshold) return;
300
+ }
301
+ return createEtag(body, options);
302
+ };
303
+ }
304
+ /**
305
+ * Default `EtagFn` used by `toResponse()` when `appOptions.etag` is
306
+ * undefined. Module-scoped so we don't allocate per-request and so
307
+ * all consumers share the same closure.
308
+ *
309
+ * `appOptions.etag === null` (explicitly disabled by the user)
310
+ * remains distinct: consumers must check `=== undefined`, not
311
+ * `== null`, before falling back to this default.
312
+ */
313
+ const DEFAULT_ETAG_FN = buildEtagFn();
314
+ //#endregion
315
+ //#region src/utils/trust-proxy/module.ts
316
+ function buildTrustProxyFn(input) {
317
+ if (typeof input === "function") return input;
318
+ if (input === true) return () => true;
319
+ if (typeof input === "number") return (_address, hop) => hop < input;
320
+ if (typeof input === "string") input = input.split(",").map((value) => value.trim());
321
+ return compile(input || []);
322
+ }
323
+ /**
324
+ * Default `TrustProxyFn` used by request helpers when neither the
325
+ * call's `options.trustProxy` nor `event.appOptions.trustProxy` is
326
+ * set. Trusts no addresses — the conservative default.
327
+ *
328
+ * Module-scoped so all helpers share the same reference and we don't
329
+ * allocate per-request.
330
+ */
331
+ const DEFAULT_TRUST_PROXY = buildTrustProxyFn();
332
+ //#endregion
333
+ //#region src/utils/mime.ts
334
+ function getMimeType(type) {
335
+ if (type.includes("/")) return type;
336
+ return getType(type);
337
+ }
338
+ function getCharsetForMimeType(type) {
339
+ if (/^text\/|^application\/(javascript|json)/.test(type)) return "utf-8";
340
+ const meta = get(type);
341
+ if (meta && meta.charset) return meta.charset.toLowerCase();
342
+ }
343
+ //#endregion
344
+ //#region src/utils/method.ts
345
+ function toMethodName(input, alt) {
346
+ if (input) return input.toUpperCase();
347
+ return alt;
348
+ }
349
+ //#endregion
350
+ //#region src/utils/path.ts
351
+ /**
352
+ * Based on https://github.com/unjs/pathe v1.1.1 (055f50a6f1131f4e5c56cf259dd8816168fba329)
353
+ */
354
+ function normalizeWindowsPath(input = "") {
355
+ if (!input || !input.includes("\\")) return input;
356
+ return input.replace(/\\/g, "/");
357
+ }
358
+ const EXTNAME_RE = /.(\.[^./]+)$/;
359
+ function extname(input) {
360
+ const match = EXTNAME_RE.exec(normalizeWindowsPath(input));
361
+ return match && match[1] || "";
362
+ }
363
+ function basename(input, extension) {
364
+ const lastSegment = normalizeWindowsPath(input).split("/").pop();
365
+ if (!lastSegment) return input;
366
+ return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
367
+ }
368
+ //#endregion
369
+ //#region src/utils/promise.ts
370
+ function isPromise(p) {
371
+ return isObject(p) && (p instanceof Promise || typeof p.then === "function");
372
+ }
373
+ //#endregion
374
+ //#region src/utils/url.ts
375
+ function hasLeadingSlash(input = "") {
376
+ return input.startsWith("/");
377
+ }
378
+ function withLeadingSlash(input = "") {
379
+ return hasLeadingSlash(input) ? input : `/${input}`;
380
+ }
381
+ function cleanDoubleSlashes(input = "") {
382
+ if (input.includes("://")) return input.split("://").map((str) => cleanDoubleSlashes(str)).join("://");
383
+ return input.replace(/\/+/g, "/");
384
+ }
385
+ /**
386
+ * Concatenate path parts into a single mount path.
387
+ *
388
+ * - Drops `undefined` and empty parts.
389
+ * - A lone `'/'` part still contributes (so `joinPaths('/')` returns
390
+ * `'/'`, distinguishing "match the root exactly" from "no path").
391
+ * - Returns `undefined` when every part is missing — callers
392
+ * interpret this as "no path" (always-match middleware).
393
+ * - Joins remaining parts with `/`, normalizes the leading slash,
394
+ * collapses any inner `//`, and trims a trailing slash on results
395
+ * longer than `/`.
396
+ *
397
+ * Used at registration time to fold a handler / router's intrinsic
398
+ * path into the mount path so the active `IRouter` is the
399
+ * only place that builds path matchers.
400
+ */
401
+ function joinPaths(...parts) {
402
+ const kept = [];
403
+ for (const part of parts) {
404
+ if (typeof part !== "string" || part === "") continue;
405
+ kept.push(part);
406
+ }
407
+ if (kept.length === 0) return;
408
+ const normalized = cleanDoubleSlashes(withLeadingSlash(kept.join("/")));
409
+ if (normalized.length > 1 && normalized.endsWith("/")) return normalized.slice(0, -1);
410
+ return normalized;
411
+ }
412
+ //#endregion
413
+ //#region src/response/helpers/header.ts
414
+ function appendResponseHeader(event, name, value) {
415
+ const { headers } = event.response;
416
+ if (Array.isArray(value)) for (const v of value) headers.append(name, sanitizeHeaderValue(v));
417
+ else headers.append(name, sanitizeHeaderValue(value));
418
+ }
419
+ function appendResponseHeaderDirective(event, name, value) {
420
+ const { headers } = event.response;
421
+ const existing = headers.get(name);
422
+ if (!existing) {
423
+ if (Array.isArray(value)) headers.set(name, sanitizeHeaderValue(value.join("; ")));
424
+ else headers.set(name, sanitizeHeaderValue(value));
425
+ return;
426
+ }
427
+ const directives = existing.split("; ");
428
+ if (Array.isArray(value)) directives.push(...value);
429
+ else directives.push(value);
430
+ const unique = [...new Set(directives)];
431
+ headers.set(name, sanitizeHeaderValue(unique.join("; ")));
432
+ }
433
+ //#endregion
434
+ //#region src/response/helpers/utils.ts
435
+ function setResponseContentTypeByFileName(event, fileName) {
436
+ const ext = extname(fileName);
437
+ if (ext) {
438
+ let type = getMimeType(ext.substring(1));
439
+ if (type) {
440
+ const charset = getCharsetForMimeType(type);
441
+ if (charset) type += `; charset=${charset}`;
442
+ event.response.headers.set(HeaderName.CONTENT_TYPE, type);
443
+ }
444
+ }
445
+ }
446
+ //#endregion
447
+ //#region src/response/helpers/header-disposition.ts
448
+ const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
449
+ const NON_ASCII_REGEXP = /[^\x20-\x7e]/g;
450
+ const QUOTE_REGEXP = /[\\"]/g;
451
+ const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
452
+ const ASCII_TEXT_REGEXP = /^[\x20-\x7e]+$/;
453
+ const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/;
454
+ function pencode(char) {
455
+ return `%${char.charCodeAt(0).toString(16).toUpperCase()}`;
456
+ }
457
+ function quoteString(value) {
458
+ return `"${value.replace(QUOTE_REGEXP, "\\$&")}"`;
459
+ }
460
+ function getAscii(value) {
461
+ return value.replace(NON_ASCII_REGEXP, "?");
462
+ }
463
+ function encodeExtended(value) {
464
+ return encodeURIComponent(value).replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode);
465
+ }
466
+ function formatFilename(value) {
467
+ if (TOKEN_REGEXP.test(value)) return `filename=${value}`;
468
+ return `filename=${quoteString(value)}`;
469
+ }
470
+ function setDisposition(event, type, filename) {
471
+ let disposition = type;
472
+ if (typeof filename === "string") {
473
+ setResponseContentTypeByFileName(event, filename);
474
+ if (ASCII_TEXT_REGEXP.test(filename) && !HEX_ESCAPE_REGEXP.test(filename)) disposition += `; ${formatFilename(filename)}`;
475
+ else {
476
+ disposition += `; ${formatFilename(getAscii(filename))}`;
477
+ disposition += `; filename*=UTF-8''${encodeExtended(filename)}`;
478
+ }
479
+ }
480
+ event.response.headers.set(HeaderName.CONTENT_DISPOSITION, disposition);
481
+ }
482
+ function setResponseHeaderAttachment(event, filename) {
483
+ setDisposition(event, "attachment", filename);
484
+ }
485
+ function setResponseHeaderInline(event, filename) {
486
+ setDisposition(event, "inline", filename);
487
+ }
488
+ //#endregion
489
+ //#region src/response/helpers/header-content-type.ts
490
+ function setResponseHeaderContentType(event, input, ifNotExists) {
491
+ if (ifNotExists) {
492
+ if (event.response.headers.get(HeaderName.CONTENT_TYPE)) return;
493
+ }
494
+ const contentType = getMimeType(input);
495
+ if (contentType) event.response.headers.set(HeaderName.CONTENT_TYPE, contentType);
496
+ }
497
+ //#endregion
498
+ //#region src/error/is.ts
499
+ function isError(input) {
500
+ return hasInstanceof(input, ErrorSymbol);
501
+ }
502
+ //#endregion
503
+ //#region src/error/create.ts
504
+ function isNativeError(input) {
505
+ return isObject(input) && typeof input.message === "string" && typeof input.name === "string";
506
+ }
507
+ /**
508
+ * Create an internal error object by
509
+ * - an existing AppError (returned as-is)
510
+ * - an HTTPError (wrapped into a AppError preserving status)
511
+ * - an Error (wrapped preserving message and cause)
512
+ * - an options object (status, message, etc.)
513
+ * - a message string
514
+ *
515
+ * @param input
516
+ */
517
+ function createError(input) {
518
+ if (isError(input)) return input;
519
+ if (typeof input === "string") return new AppError(input);
520
+ if (isHTTPError(input)) return new AppError({
521
+ message: input.message,
522
+ code: input.code,
523
+ status: input.status,
524
+ redirectURL: input.redirectURL,
525
+ cause: input
526
+ });
527
+ if (isNativeError(input)) return new AppError({
528
+ message: input.message,
529
+ cause: input
530
+ });
531
+ if (!isObject(input)) return new AppError();
532
+ const options = { ...input };
533
+ if (options.cause === void 0) options.cause = input;
534
+ return new AppError(options);
535
+ }
536
+ //#endregion
537
+ //#region src/response/to-response.ts
538
+ function stripWeakPrefix(etag) {
539
+ return etag.startsWith("W/") ? etag.slice(2) : etag;
540
+ }
541
+ /**
542
+ * Resolve the effective etag fn for this request. `null` means the
543
+ * user explicitly disabled ETag; `undefined` means the option was
544
+ * never set, and we apply the framework default. Anything else is the
545
+ * user's own fn.
546
+ */
547
+ function effectiveEtagFn(event) {
548
+ const opt = event.appOptions.etag;
549
+ if (opt === null) return null;
550
+ if (typeof opt === "undefined") return DEFAULT_ETAG_FN;
551
+ return opt;
552
+ }
553
+ /**
554
+ * Compute an ETag and conditionally return a 304, or set the header and
555
+ * return undefined to let the caller emit the full response. Always
556
+ * async because the ETag generator may be async (and typically is, via
557
+ * `uncrypto`).
558
+ */
559
+ async function applyEtag(body, event, headers) {
560
+ const etagFn = effectiveEtagFn(event);
561
+ if (!etagFn) return;
562
+ const etag = await etagFn(body);
563
+ if (!etag) return;
564
+ headers.set("etag", etag);
565
+ const ifNoneMatch = event.headers.get("if-none-match");
566
+ if (ifNoneMatch && (ifNoneMatch === "*" || ifNoneMatch.split(",").some((t) => stripWeakPrefix(t.trim()) === stripWeakPrefix(etag)))) return new Response(null, {
567
+ status: 304,
568
+ headers
569
+ });
570
+ }
571
+ /**
572
+ * Convert a handler's return value into a Web `Response`.
573
+ *
574
+ * Returns synchronously for the common cases (string, JSON object,
575
+ * binary, stream, blob) when ETag generation is disabled. Returns a
576
+ * `Promise` when an ETag must be computed (the generator is async).
577
+ *
578
+ * Callers that want the async return uniformly can `await` the result
579
+ * — `await` on a non-Promise still works but pays a microtask hop.
580
+ * The App fast path branches on `instanceof Promise` to keep the
581
+ * sync return truly sync.
582
+ */
583
+ function toResponse(value, event) {
584
+ if (value === void 0) return;
585
+ if (value === null) return new Response(null, {
586
+ status: event.response.status,
587
+ headers: event.response.headers
588
+ });
589
+ const t = typeof value;
590
+ if (t === "string") {
591
+ const { status, headers } = event.response;
592
+ if (!headers.has("content-type")) headers.set("content-type", "text/plain; charset=utf-8");
593
+ if (event.appOptions.etag !== null) return applyEtag(value, event, headers).then((cached) => cached ?? new Response(value, {
594
+ status,
595
+ headers
596
+ }));
597
+ return new Response(value, {
598
+ status,
599
+ headers
600
+ });
601
+ }
602
+ if (t === "object" && !Array.isArray(value)) {
603
+ if (value instanceof Response) return value;
604
+ const { status, headers } = event.response;
605
+ if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
606
+ if (!headers.has("content-type")) headers.set("content-type", "application/octet-stream");
607
+ return new Response(value, {
608
+ status,
609
+ headers
610
+ });
611
+ }
612
+ if (value instanceof ReadableStream) return new Response(value, {
613
+ status,
614
+ headers
615
+ });
616
+ if (value instanceof Blob) {
617
+ if (!headers.has("content-type")) headers.set("content-type", value.type || "application/octet-stream");
618
+ return new Response(value, {
619
+ status,
620
+ headers
621
+ });
622
+ }
623
+ }
624
+ const { status, headers } = event.response;
625
+ if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8");
626
+ let json;
627
+ try {
628
+ json = JSON.stringify(value);
629
+ } catch (e) {
630
+ throw createError({
631
+ message: "JSON serialization failed",
632
+ status: 500,
633
+ cause: e
634
+ });
635
+ }
636
+ if (event.appOptions.etag !== null) return applyEtag(json, event, headers).then((cached) => cached ?? new Response(json, {
637
+ status,
638
+ headers
639
+ }));
640
+ return new Response(json, {
641
+ status,
642
+ headers
643
+ });
644
+ }
645
+ //#endregion
646
+ //#region src/response/helpers/send-accepted.ts
647
+ async function sendAccepted(event, data) {
648
+ event.response.status = 202;
649
+ return await toResponse(data ?? "", event);
650
+ }
651
+ //#endregion
652
+ //#region src/response/helpers/send-created.ts
653
+ async function sendCreated(event, data) {
654
+ event.response.status = 201;
655
+ return await toResponse(data ?? "", event);
656
+ }
657
+ //#endregion
658
+ //#region src/response/helpers/send-file.ts
659
+ async function sendFile(event, options) {
660
+ let stats;
661
+ if (typeof options.stats === "function") stats = await options.stats();
662
+ else stats = options.stats;
663
+ const name = options.name || stats.name;
664
+ const { headers } = event.response;
665
+ const disposition = options.disposition ?? (options.attachment ? "attachment" : void 0);
666
+ if (name) {
667
+ const fileName = basename(name);
668
+ if (disposition) {
669
+ if (!headers.get(HeaderName.CONTENT_DISPOSITION)) if (disposition === "inline") setResponseHeaderInline(event, fileName);
670
+ else setResponseHeaderAttachment(event, fileName);
671
+ } else setResponseContentTypeByFileName(event, fileName);
672
+ }
673
+ const contentOptions = {};
674
+ let statusCode = event.response.status;
675
+ if (stats.size) {
676
+ const rangeHeader = event.headers.get(HeaderName.RANGE);
677
+ if (rangeHeader) {
678
+ const [x, y] = rangeHeader.replace("bytes=", "").split("-");
679
+ const parsedStart = Number.parseInt(x, 10);
680
+ const parsedEnd = Number.parseInt(y, 10);
681
+ contentOptions.start = Number.isFinite(parsedStart) && parsedStart >= 0 ? parsedStart : 0;
682
+ contentOptions.end = Number.isFinite(parsedEnd) && parsedEnd >= 0 ? Math.min(parsedEnd, stats.size - 1) : stats.size - 1;
683
+ if (contentOptions.start >= stats.size || contentOptions.start > contentOptions.end) {
684
+ const rangeHeaders = new Headers(headers);
685
+ rangeHeaders.set(HeaderName.CONTENT_RANGE, `bytes */${stats.size}`);
686
+ return new Response(null, {
687
+ status: 416,
688
+ headers: rangeHeaders
689
+ });
690
+ }
691
+ headers.set(HeaderName.CONTENT_RANGE, `bytes ${contentOptions.start}-${contentOptions.end}/${stats.size}`);
692
+ headers.set(HeaderName.CONTENT_LENGTH, `${contentOptions.end - contentOptions.start + 1}`);
693
+ statusCode = 206;
694
+ } else headers.set(HeaderName.CONTENT_LENGTH, `${stats.size}`);
695
+ headers.set(HeaderName.ACCEPT_RANGES, "bytes");
696
+ if (stats.mtime) {
697
+ const mtime = new Date(stats.mtime);
698
+ headers.set(HeaderName.LAST_MODIFIED, mtime.toUTCString());
699
+ headers.set(HeaderName.ETag, `W/"${stats.size}-${mtime.getTime()}"`);
700
+ }
701
+ }
702
+ const content = await options.content(contentOptions);
703
+ return new Response(content, {
704
+ status: statusCode,
705
+ headers
706
+ });
707
+ }
708
+ //#endregion
709
+ //#region src/request/helpers/header.ts
710
+ function getRequestHeader(event, name) {
711
+ return event.headers.get(name);
712
+ }
713
+ //#endregion
714
+ //#region src/request/helpers/negotiator.ts
715
+ const NEGOTIATOR_KEY = Symbol.for("routup:negotiator");
716
+ function headersToPlainObject(headers) {
717
+ const result = {};
718
+ headers.forEach((value, key) => {
719
+ result[key] = value;
720
+ });
721
+ return result;
722
+ }
723
+ function useRequestNegotiator(event) {
724
+ let value = event.store[NEGOTIATOR_KEY];
725
+ if (value) return value;
726
+ value = new Negotiator({ headers: headersToPlainObject(event.headers) });
727
+ event.store[NEGOTIATOR_KEY] = value;
728
+ return value;
729
+ }
730
+ //#endregion
731
+ //#region src/request/helpers/header-accept.ts
732
+ function getRequestAcceptableContentTypes(event) {
733
+ return useRequestNegotiator(event).mediaTypes();
734
+ }
735
+ function getRequestAcceptableContentType(event, input) {
736
+ input = input || [];
737
+ const items = Array.isArray(input) ? input : [input];
738
+ if (items.length === 0) return getRequestAcceptableContentTypes(event).shift();
739
+ if (!getRequestHeader(event, HeaderName.ACCEPT)) return items[0];
740
+ let polluted = false;
741
+ const mimeTypes = [];
742
+ for (const item of items) {
743
+ const mimeType = getMimeType(item);
744
+ if (mimeType) mimeTypes.push(mimeType);
745
+ else polluted = true;
746
+ }
747
+ const matches = useRequestNegotiator(event).mediaTypes(mimeTypes);
748
+ if (matches.length > 0) {
749
+ if (polluted) return items[0];
750
+ return items[mimeTypes.indexOf(matches[0])];
751
+ }
752
+ }
753
+ //#endregion
754
+ //#region src/response/helpers/send-format.ts
755
+ function sendFormat(event, input) {
756
+ const { default: formatDefault, ...formats } = input;
757
+ const contentTypes = Object.keys(formats);
758
+ if (contentTypes.length === 0) return formatDefault();
759
+ const contentType = getRequestAcceptableContentType(event, contentTypes);
760
+ if (contentType && formats[contentType]) return formats[contentType]();
761
+ return formatDefault();
762
+ }
763
+ //#endregion
764
+ //#region src/response/helpers/send-redirect.ts
765
+ function escapeHtml(str) {
766
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
767
+ }
768
+ function isAllowedRedirectUrl(location) {
769
+ if (location.startsWith("//")) return false;
770
+ if (location.startsWith("/") || location.startsWith(".")) return true;
771
+ try {
772
+ const url = new URL(location);
773
+ return url.protocol === "http:" || url.protocol === "https:";
774
+ } catch {
775
+ return true;
776
+ }
777
+ }
778
+ function sendRedirect(event, location, statusCode = 302) {
779
+ if (!isAllowedRedirectUrl(location)) throw new AppError({
780
+ status: 400,
781
+ message: "Invalid redirect URL scheme."
782
+ });
783
+ const sanitizedLocation = sanitizeHeaderValue(location);
784
+ const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${escapeHtml(location)}"></head></html>`;
785
+ const headers = new Headers(event.response.headers);
786
+ headers.set("location", sanitizedLocation);
787
+ headers.set("content-type", "text/html; charset=utf-8");
788
+ headers.delete("content-length");
789
+ return new Response(html, {
790
+ status: statusCode,
791
+ headers
792
+ });
793
+ }
794
+ //#endregion
795
+ //#region src/response/helpers/send-stream.ts
796
+ function sendStream(event, stream) {
797
+ const { status, headers } = event.response;
798
+ return new Response(stream, {
799
+ status,
800
+ headers
801
+ });
802
+ }
803
+ //#endregion
804
+ //#region src/dispatcher/module.ts
805
+ var DispatcherEvent = class {
806
+ request;
807
+ params;
808
+ path;
809
+ method;
810
+ /**
811
+ * Collected allowed methods (for OPTIONS).
812
+ */
813
+ methodsAllowed;
814
+ mountPath;
815
+ error;
816
+ /**
817
+ * Options of the App currently dispatching this event. Set on
818
+ * entry to `App.dispatch` and restored on exit so re-entrant
819
+ * dispatch calls leave the caller's view intact. Initialized to
820
+ * `{}` so consumers reading before any dispatch get a valid
821
+ * (empty) shape.
822
+ */
823
+ appOptions;
824
+ /**
825
+ * `true` while an `App.dispatch` call is on the stack for this
826
+ * event. `App.dispatch` reads this on entry to derive `isRoot`
827
+ * and writes it on entry/exit so re-entrant calls behave
828
+ * correctly.
829
+ */
830
+ isDispatching;
831
+ _dispatched;
832
+ _response;
833
+ _store;
834
+ /**
835
+ * Cached parsed URL (avoids double-parsing).
836
+ */
837
+ _url;
838
+ /**
839
+ * Continuation function for middleware onion model.
840
+ */
841
+ _next;
842
+ _signal;
843
+ _signalCleanup;
844
+ /**
845
+ * Whether _next has already been called (guard against double-invocation).
846
+ */
847
+ _nextCalled;
848
+ /**
849
+ * The cached result of the next handler.
850
+ */
851
+ _nextResult;
852
+ constructor(request) {
853
+ this.request = request;
854
+ this._url = new FastURL(request.url);
855
+ this.method = request.method;
856
+ this.path = this._url.pathname;
857
+ this.mountPath = "/";
858
+ this.params = {};
859
+ this.appOptions = {};
860
+ this.isDispatching = false;
861
+ this.methodsAllowed = /* @__PURE__ */ new Set();
862
+ this._dispatched = false;
863
+ this._nextCalled = false;
864
+ }
865
+ get response() {
866
+ if (!this._response) this._response = {
867
+ status: 200,
868
+ headers: new Headers()
869
+ };
870
+ return this._response;
871
+ }
872
+ get signal() {
873
+ if (!this._signal) this._signal = this.request.signal;
874
+ return this._signal;
875
+ }
876
+ set signal(value) {
877
+ if (this._signalCleanup) {
878
+ this._signalCleanup();
879
+ this._signalCleanup = void 0;
880
+ }
881
+ if (value === this.request.signal) {
882
+ this._signal = value;
883
+ return;
884
+ }
885
+ const controller = new AbortController();
886
+ const abort = (e) => {
887
+ const reason = e?.target instanceof AbortSignal ? e.target.reason : void 0;
888
+ this.request.signal.removeEventListener("abort", abort);
889
+ value.removeEventListener("abort", abort);
890
+ controller.abort(reason);
891
+ };
892
+ if (this.request.signal.aborted || value.aborted) {
893
+ const reason = this.request.signal.aborted ? this.request.signal.reason : value.reason;
894
+ controller.abort(reason);
895
+ } else {
896
+ this.request.signal.addEventListener("abort", abort, { once: true });
897
+ value.addEventListener("abort", abort, { once: true });
898
+ this._signalCleanup = () => {
899
+ this.request.signal.removeEventListener("abort", abort);
900
+ value.removeEventListener("abort", abort);
901
+ };
902
+ }
903
+ this._signal = controller.signal;
904
+ }
905
+ get dispatched() {
906
+ return this._dispatched;
907
+ }
908
+ set dispatched(value) {
909
+ this._dispatched = value;
910
+ }
911
+ async next(event, error) {
912
+ if (this._nextCalled) return this._nextResult;
913
+ this._nextCalled = true;
914
+ if (this._next) this._nextResult = this._next(event, error);
915
+ return this._nextResult;
916
+ }
917
+ setNext(fn) {
918
+ if (fn) this._next = async (event, error) => {
919
+ return toResponse(await fn(error), event);
920
+ };
921
+ else this._next = void 0;
922
+ this._nextCalled = false;
923
+ this._nextResult = void 0;
924
+ }
925
+ build(signal) {
926
+ return new AppEvent({
927
+ request: this.request,
928
+ params: this.params,
929
+ path: this.path,
930
+ method: this.method,
931
+ mountPath: this.mountPath,
932
+ headers: this.request.headers,
933
+ searchParams: new URLSearchParams(this._url.search),
934
+ response: this.response,
935
+ store: this.store,
936
+ signal: signal ?? this.signal,
937
+ appOptions: this.appOptions,
938
+ next: (event, error) => this.next(event, error)
939
+ });
940
+ }
941
+ get store() {
942
+ if (!this._store) this._store = Object.create(null);
943
+ return this._store;
944
+ }
945
+ };
946
+ //#endregion
947
+ //#region src/handler/constants.ts
948
+ const HandlerType = {
949
+ CORE: "core",
950
+ ERROR: "error"
951
+ };
952
+ const HandlerSymbol = Symbol.for("Handler");
953
+ //#endregion
954
+ //#region src/handler/module.ts
955
+ var Handler = class {
956
+ config;
957
+ method;
958
+ constructor(handler) {
959
+ this.config = handler;
960
+ if (typeof handler.path === "string") this.config.path = withLeadingSlash(handler.path);
961
+ this.method = this.config.method ? toMethodName(this.config.method) : void 0;
962
+ markInstanceof(this, HandlerSymbol);
963
+ }
964
+ get type() {
965
+ return this.config.type;
966
+ }
967
+ get path() {
968
+ return this.config.path;
969
+ }
970
+ async dispatch(event) {
971
+ let response;
972
+ let handlerEvent;
973
+ let cleanupParentListener;
974
+ try {
975
+ const effectiveTimeout = this.resolveTimeout(event.appOptions);
976
+ let childController;
977
+ if (effectiveTimeout) {
978
+ const parentSignal = event.signal;
979
+ childController = new AbortController();
980
+ if (parentSignal.aborted) childController.abort(parentSignal.reason);
981
+ else {
982
+ const onAbort = () => childController.abort(parentSignal.reason);
983
+ parentSignal.addEventListener("abort", onAbort, { once: true });
984
+ cleanupParentListener = () => parentSignal.removeEventListener("abort", onAbort);
985
+ }
986
+ }
987
+ handlerEvent = childController ? event.build(childController.signal) : event.build();
988
+ const skipFn = this.config.type === HandlerType.ERROR && !event.error;
989
+ if (!skipFn && this.config.onBefore) await this.config.onBefore(handlerEvent);
990
+ let invocation;
991
+ if (skipFn) {} else if (this.config.type === HandlerType.ERROR) {
992
+ const { fn } = this.config;
993
+ invocation = fn(event.error, handlerEvent);
994
+ } else {
995
+ const { fn } = this.config;
996
+ invocation = fn(handlerEvent);
997
+ }
998
+ let result;
999
+ if (skipFn) {} else if (effectiveTimeout) result = await this.executeWithTimeout(() => this.resolveHandlerResult(invocation, handlerEvent), effectiveTimeout, childController);
1000
+ else if (isPromise(invocation)) {
1001
+ const awaited = await invocation;
1002
+ result = typeof awaited === "undefined" ? await this.resolveHandlerResult(void 0, handlerEvent) : awaited;
1003
+ } else if (typeof invocation === "undefined") result = await this.resolveHandlerResult(void 0, handlerEvent);
1004
+ else result = invocation;
1005
+ const toResp = toResponse(result, handlerEvent);
1006
+ response = isPromise(toResp) ? await toResp : toResp;
1007
+ if (response) {
1008
+ event.dispatched = true;
1009
+ if (this.config.type === HandlerType.ERROR && event.error) event.error = void 0;
1010
+ }
1011
+ if (!skipFn && this.config.onAfter) await this.config.onAfter(handlerEvent, response);
1012
+ } catch (e) {
1013
+ event.error = isError(e) ? e : createError(e);
1014
+ if (this.config.onError) try {
1015
+ await this.config.onError(event.error, handlerEvent ?? event.build());
1016
+ } catch (innerErr) {
1017
+ event.error = isError(innerErr) ? innerErr : createError(innerErr);
1018
+ }
1019
+ throw event.error;
1020
+ } finally {
1021
+ if (cleanupParentListener) cleanupParentListener();
1022
+ }
1023
+ return response;
1024
+ }
1025
+ /**
1026
+ * Resolve a handler's return value into the final value handed to `toResponse`.
1027
+ *
1028
+ * Contract:
1029
+ * - non-undefined value → return as-is (becomes the response)
1030
+ * - `undefined` + `event.next()` was called → forward downstream result
1031
+ * - `undefined` + `event.next()` not yet called → wait until either `next()` is
1032
+ * invoked (e.g. from an async callback) or `signal` aborts. A global or
1033
+ * per-handler timeout aborts `signal` and surfaces as 408. With no timeout
1034
+ * configured and no eventual `next()` call, the request hangs by design.
1035
+ */
1036
+ async resolveHandlerResult(invocation, handlerEvent) {
1037
+ const value = await invocation;
1038
+ if (typeof value !== "undefined") return value;
1039
+ if (handlerEvent.nextCalled) return handlerEvent.nextResult;
1040
+ const { signal } = handlerEvent;
1041
+ if (signal.aborted) throw createError({
1042
+ status: 408,
1043
+ message: "Request Timeout"
1044
+ });
1045
+ return new Promise((resolve, reject) => {
1046
+ const onAbort = () => {
1047
+ signal.removeEventListener("abort", onAbort);
1048
+ reject(createError({
1049
+ status: 408,
1050
+ message: "Request Timeout"
1051
+ }));
1052
+ };
1053
+ signal.addEventListener("abort", onAbort, { once: true });
1054
+ handlerEvent.whenNextCalled().then(() => {
1055
+ signal.removeEventListener("abort", onAbort);
1056
+ resolve(handlerEvent.nextResult);
1057
+ });
1058
+ });
1059
+ }
1060
+ async executeWithTimeout(fn, effectiveTimeout, controller) {
1061
+ if (!effectiveTimeout) return fn();
1062
+ let timerId;
1063
+ try {
1064
+ return await Promise.race([fn(), new Promise((_, reject) => {
1065
+ timerId = setTimeout(() => {
1066
+ if (controller) controller.abort();
1067
+ reject(createError({
1068
+ status: 408,
1069
+ message: "Request Timeout"
1070
+ }));
1071
+ }, effectiveTimeout);
1072
+ })]);
1073
+ } finally {
1074
+ clearTimeout(timerId);
1075
+ }
1076
+ }
1077
+ resolveTimeout(appOptions) {
1078
+ const routerDefault = appOptions.handlerTimeout;
1079
+ const handlerOverride = this.config.timeout;
1080
+ if (!routerDefault && !handlerOverride) return;
1081
+ if (!routerDefault) return handlerOverride;
1082
+ if (!handlerOverride) return routerDefault;
1083
+ if (appOptions.handlerTimeoutOverridable) return handlerOverride;
1084
+ return Math.min(routerDefault, handlerOverride);
1085
+ }
1086
+ };
1087
+ //#endregion
1088
+ //#region src/handler/core/define.ts
1089
+ function defineCoreHandler(input) {
1090
+ if (typeof input === "function") return new Handler({
1091
+ type: HandlerType.CORE,
1092
+ fn: input
1093
+ });
1094
+ return new Handler({
1095
+ type: HandlerType.CORE,
1096
+ ...input
1097
+ });
1098
+ }
1099
+ //#endregion
1100
+ //#region src/handler/error/define.ts
1101
+ function defineErrorHandler(input) {
1102
+ if (typeof input === "function") return new Handler({
1103
+ type: HandlerType.ERROR,
1104
+ fn: input
1105
+ });
1106
+ return new Handler({
1107
+ type: HandlerType.ERROR,
1108
+ ...input
1109
+ });
1110
+ }
1111
+ //#endregion
1112
+ //#region src/handler/adapters/node/define.ts
1113
+ const kHandled = /* @__PURE__ */ Symbol("handled");
1114
+ function callHandler(handler, req, res) {
1115
+ return new Promise((resolve, reject) => {
1116
+ let settled = false;
1117
+ const onClose = () => settle(kHandled);
1118
+ const onFinish = () => settle(kHandled);
1119
+ const onError = (error) => fail(error);
1120
+ function cleanup() {
1121
+ res.removeListener("close", onClose);
1122
+ res.removeListener("finish", onFinish);
1123
+ res.removeListener("error", onError);
1124
+ }
1125
+ function settle(value) {
1126
+ if (settled) return;
1127
+ settled = true;
1128
+ cleanup();
1129
+ resolve(value);
1130
+ }
1131
+ function fail(error) {
1132
+ if (settled) return;
1133
+ settled = true;
1134
+ cleanup();
1135
+ reject(error);
1136
+ }
1137
+ res.once("close", onClose);
1138
+ res.once("finish", onFinish);
1139
+ res.once("error", onError);
1140
+ try {
1141
+ Promise.resolve(handler(req, res)).then(() => settle(kHandled)).catch(fail);
1142
+ } catch (error) {
1143
+ fail(error);
1144
+ }
1145
+ });
1146
+ }
1147
+ function callMiddleware(handler, req, res) {
1148
+ return new Promise((resolve, reject) => {
1149
+ let settled = false;
1150
+ const onClose = () => settle(kHandled);
1151
+ const onFinish = () => settle(kHandled);
1152
+ const onError = (error) => fail(error);
1153
+ function cleanup() {
1154
+ res.removeListener("close", onClose);
1155
+ res.removeListener("finish", onFinish);
1156
+ res.removeListener("error", onError);
1157
+ }
1158
+ function settle(value) {
1159
+ if (settled) return;
1160
+ settled = true;
1161
+ cleanup();
1162
+ resolve(value);
1163
+ }
1164
+ function fail(error) {
1165
+ if (settled) return;
1166
+ settled = true;
1167
+ cleanup();
1168
+ reject(error);
1169
+ }
1170
+ res.once("close", onClose);
1171
+ res.once("finish", onFinish);
1172
+ res.once("error", onError);
1173
+ try {
1174
+ Promise.resolve(handler(req, res, (error) => {
1175
+ if (error) fail(error);
1176
+ else settle(res.writableEnded || res.destroyed ? kHandled : void 0);
1177
+ })).catch(fail);
1178
+ } catch (error) {
1179
+ fail(error);
1180
+ }
1181
+ });
1182
+ }
1183
+ function createNodeBridge(handler, isMiddleware) {
1184
+ if (typeof handler !== "function") throw new AppError("fromNodeHandler/fromNodeMiddleware expects a function.");
1185
+ return defineCoreHandler({ fn: (async (event) => {
1186
+ const node = event.request.runtime?.node;
1187
+ if (!node?.req || !node?.res) throw new AppError("fromNodeHandler/fromNodeMiddleware requires a Node.js runtime.");
1188
+ const req = node.req;
1189
+ const res = node.res;
1190
+ if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) return null;
1191
+ return event.next();
1192
+ }) });
1193
+ }
1194
+ /**
1195
+ * Wraps a Node.js `(req, res)` handler for use in the routup pipeline.
1196
+ *
1197
+ * @example
1198
+ * ```typescript
1199
+ * import { fromNodeHandler } from 'routup/node';
1200
+ *
1201
+ * router.use(fromNodeHandler((req, res) => {
1202
+ * res.end('Hello');
1203
+ * }));
1204
+ * ```
1205
+ */
1206
+ function fromNodeHandler(handler) {
1207
+ return createNodeBridge(handler, false);
1208
+ }
1209
+ /**
1210
+ * Wraps a Node.js `(req, res, next)` middleware for use in the routup pipeline.
1211
+ *
1212
+ * @example
1213
+ * ```typescript
1214
+ * import cors from 'cors';
1215
+ * import { fromNodeMiddleware } from 'routup/node';
1216
+ *
1217
+ * router.use(fromNodeMiddleware(cors()));
1218
+ * ```
1219
+ */
1220
+ function fromNodeMiddleware(handler) {
1221
+ return createNodeBridge(handler, true);
1222
+ }
1223
+ //#endregion
1224
+ //#region src/handler/adapters/web/is.ts
1225
+ function isWebHandlerProvider(input) {
1226
+ return isObject(input) && typeof input.fetch === "function";
1227
+ }
1228
+ function isWebHandler(input) {
1229
+ return typeof input === "function";
1230
+ }
1231
+ //#endregion
1232
+ //#region src/handler/adapters/web/define.ts
1233
+ function fromWebHandler(input) {
1234
+ if (isWebHandlerProvider(input)) return fromWebHandler(input.fetch.bind(input));
1235
+ if (typeof input !== "function") throw new AppError("fromWebHandler expects a function or an object with a fetch method.");
1236
+ return defineCoreHandler({ fn: (event) => input(event.request) });
1237
+ }
1238
+ //#endregion
1239
+ //#region src/handler/is.ts
1240
+ function isHandlerOptions(input) {
1241
+ return isObject(input) && typeof input.fn === "function" && typeof input.type === "string";
1242
+ }
1243
+ function isHandler(input) {
1244
+ return hasInstanceof(input, HandlerSymbol);
1245
+ }
1246
+ //#endregion
1247
+ //#region src/handler/utils.ts
1248
+ /**
1249
+ * Match a request method against a handler's bound method.
1250
+ *
1251
+ * - When the handler has no method bound, matches every request method.
1252
+ * - Otherwise matches when the request method is the same.
1253
+ * - HEAD requests additionally match GET handlers.
1254
+ */
1255
+ function matchHandlerMethod(handlerMethod, requestMethod) {
1256
+ return !handlerMethod || requestMethod === handlerMethod || requestMethod === MethodName.HEAD && handlerMethod === MethodName.GET;
1257
+ }
1258
+ //#endregion
1259
+ //#region src/request/helpers/cache.ts
1260
+ function isRequestCacheable(event, modifiedTime) {
1261
+ const modifiedSince = event.headers.get(HeaderName.IF_MODIFIED_SINCE);
1262
+ if (!modifiedSince) return false;
1263
+ modifiedTime = typeof modifiedTime === "string" ? new Date(modifiedTime) : modifiedTime;
1264
+ const sinceDate = new Date(modifiedSince);
1265
+ if (Number.isNaN(sinceDate.getTime()) || Number.isNaN(modifiedTime.getTime())) return false;
1266
+ return sinceDate >= modifiedTime;
1267
+ }
1268
+ //#endregion
1269
+ //#region src/request/helpers/header-accept-charset.ts
1270
+ function getRequestAcceptableCharsets(event) {
1271
+ return useRequestNegotiator(event).charsets();
1272
+ }
1273
+ function getRequestAcceptableCharset(event, input) {
1274
+ input = input || [];
1275
+ const items = Array.isArray(input) ? input : [input];
1276
+ if (items.length === 0) return getRequestAcceptableCharsets(event).shift();
1277
+ return useRequestNegotiator(event).charsets(items).shift() || void 0;
1278
+ }
1279
+ //#endregion
1280
+ //#region src/request/helpers/header-accept-encoding.ts
1281
+ function getRequestAcceptableEncodings(event) {
1282
+ return useRequestNegotiator(event).encodings();
1283
+ }
1284
+ function getRequestAcceptableEncoding(event, input) {
1285
+ input = input || [];
1286
+ const items = Array.isArray(input) ? input : [input];
1287
+ if (items.length === 0) return getRequestAcceptableEncodings(event).shift();
1288
+ return useRequestNegotiator(event).encodings(items).shift() || void 0;
1289
+ }
1290
+ //#endregion
1291
+ //#region src/request/helpers/header-accept-language.ts
1292
+ function getRequestAcceptableLanguages(event) {
1293
+ return useRequestNegotiator(event).languages();
1294
+ }
1295
+ function getRequestAcceptableLanguage(event, input) {
1296
+ input = input || [];
1297
+ const items = Array.isArray(input) ? input : [input];
1298
+ if (items.length === 0) return getRequestAcceptableLanguages(event).shift();
1299
+ return useRequestNegotiator(event).languages(items).shift() || void 0;
1300
+ }
1301
+ //#endregion
1302
+ //#region src/request/helpers/header-content-type.ts
1303
+ function matchRequestContentType(event, contentType) {
1304
+ const header = getRequestHeader(event, HeaderName.CONTENT_TYPE);
1305
+ if (!header) return true;
1306
+ return header.split(";")[0].trim() === getMimeType(contentType);
1307
+ }
1308
+ //#endregion
1309
+ //#region src/request/helpers/hostname.ts
1310
+ function getRequestHostName(event, options = {}) {
1311
+ let trustProxy;
1312
+ if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1313
+ else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
1314
+ let hostname = event.headers.get(HeaderName.X_FORWARDED_HOST);
1315
+ if (!hostname || !event.request.ip || !trustProxy(event.request.ip, 0)) hostname = event.headers.get(HeaderName.HOST);
1316
+ else if (hostname && hostname.includes(",")) hostname = hostname.substring(0, hostname.indexOf(",")).trimEnd();
1317
+ if (!hostname) return;
1318
+ const offset = hostname[0] === "[" ? hostname.indexOf("]") + 1 : 0;
1319
+ const index = hostname.indexOf(":", offset);
1320
+ const result = index !== -1 ? hostname.substring(0, index) : hostname;
1321
+ if (/[\x00-\x1F\x7F\s/@\\]/.test(result)) return;
1322
+ return result;
1323
+ }
1324
+ //#endregion
1325
+ //#region src/request/helpers/ip.ts
1326
+ /**
1327
+ * Get the client IP address from the request.
1328
+ *
1329
+ * When `trustProxy` is configured, walks the `X-Forwarded-For` chain
1330
+ * and returns the rightmost untrusted address (the actual client IP).
1331
+ * Falls back to `event.request.ip` (the direct connection IP).
1332
+ */
1333
+ function getRequestIP(event, options = {}) {
1334
+ let trustProxy;
1335
+ if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1336
+ else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
1337
+ const socketAddr = event.request.ip;
1338
+ if (!socketAddr) return;
1339
+ const forwarded = event.headers.get(HeaderName.X_FORWARDED_FOR);
1340
+ const addrs = [socketAddr];
1341
+ if (forwarded) {
1342
+ const parts = forwarded.split(",");
1343
+ for (let i = parts.length - 1; i >= 0; i--) {
1344
+ const addr = parts[i].trim();
1345
+ if (addr) addrs.push(addr);
1346
+ }
1347
+ }
1348
+ for (let i = 0; i < addrs.length - 1; i++) if (!trustProxy(addrs[i], i)) return addrs[i];
1349
+ return addrs[addrs.length - 1];
1350
+ }
1351
+ //#endregion
1352
+ //#region src/request/helpers/protocol.ts
1353
+ function getRequestProtocol(event, options = {}) {
1354
+ let trustProxy;
1355
+ if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
1356
+ else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
1357
+ let protocol;
1358
+ try {
1359
+ if (new URL(event.request.url).protocol === "https:") protocol = "https";
1360
+ else protocol = "http";
1361
+ } catch {
1362
+ protocol = options.default || "http";
1363
+ }
1364
+ if (!event.request.ip || !trustProxy(event.request.ip, 0)) return protocol;
1365
+ const header = event.headers.get(HeaderName.X_FORWARDED_PROTO);
1366
+ if (!header) return protocol;
1367
+ const index = header.indexOf(",");
1368
+ const forwarded = index !== -1 ? header.substring(0, index).trim().toLowerCase() : header.trim().toLowerCase();
1369
+ if (forwarded === "http" || forwarded === "https") return forwarded;
1370
+ return protocol;
1371
+ }
1372
+ //#endregion
1373
+ //#region src/path/matcher.ts
1374
+ function decodeParam(val) {
1375
+ /* istanbul ignore next */
1376
+ if (typeof val !== "string" || val.length === 0) return val;
1377
+ try {
1378
+ return decodeURIComponent(val);
1379
+ } catch {
1380
+ return val;
1381
+ }
1382
+ }
1383
+ var PathMatcher = class {
1384
+ path;
1385
+ regexp;
1386
+ regexpKeys = [];
1387
+ regexpOptions;
1388
+ constructor(path, options) {
1389
+ this.path = path;
1390
+ this.regexpOptions = options || {};
1391
+ const regexp = pathToRegexp(path, options);
1392
+ this.regexp = regexp.regexp;
1393
+ this.regexpKeys = regexp.keys;
1394
+ }
1395
+ test(path) {
1396
+ return this.regexp.test(path);
1397
+ }
1398
+ exec(path) {
1399
+ if (this.path === "/" && this.regexpOptions.end === false) return {
1400
+ path: "/",
1401
+ params: Object.create(null)
1402
+ };
1403
+ const match = this.regexp.exec(path);
1404
+ if (!match) return;
1405
+ const params = Object.create(null);
1406
+ for (let i = 1; i < match.length; i++) {
1407
+ const key = this.regexpKeys[i - 1];
1408
+ if (!key) continue;
1409
+ const prop = key.name;
1410
+ const val = decodeParam(match[i]);
1411
+ if (typeof val !== "undefined") params[prop] = val;
1412
+ }
1413
+ return {
1414
+ path: match[0],
1415
+ params
1416
+ };
1417
+ }
1418
+ };
1419
+ //#endregion
1420
+ //#region src/path/utils.ts
1421
+ function isPath(input) {
1422
+ return typeof input === "string";
1423
+ }
1424
+ //#endregion
1425
+ //#region src/plugin/error/constants.ts
1426
+ const PluginErrorCode = {
1427
+ PLUGIN: "PLUGIN",
1428
+ NOT_INSTALLED: "PLUGIN_NOT_INSTALLED",
1429
+ ALREADY_INSTALLED: "PLUGIN_ALREADY_INSTALLED",
1430
+ INSTALL: "PLUGIN_INSTALL"
1431
+ };
1432
+ //#endregion
1433
+ //#region src/plugin/error/is.ts
1434
+ const PLUGIN_ERROR_CODES = new Set(Object.values(PluginErrorCode));
1435
+ function isPluginError(input) {
1436
+ if (!isError(input)) return false;
1437
+ return PLUGIN_ERROR_CODES.has(input.code);
1438
+ }
1439
+ //#endregion
1440
+ //#region src/plugin/error/module.ts
1441
+ var PluginError = class extends AppError {
1442
+ constructor(input = {}) {
1443
+ const options = typeof input === "string" ? { message: input } : { ...input };
1444
+ if (!("code" in options) || !options.code) options.code = PluginErrorCode.PLUGIN;
1445
+ super(options);
1446
+ this.name = "PluginError";
1447
+ }
1448
+ };
1449
+ //#endregion
1450
+ //#region src/plugin/error/sub/already-installed.ts
1451
+ var PluginAlreadyInstalledError = class extends PluginError {
1452
+ pluginName;
1453
+ constructor(pluginName) {
1454
+ super({
1455
+ message: `Plugin "${pluginName}" is already installed on this router.`,
1456
+ code: PluginErrorCode.ALREADY_INSTALLED
1457
+ });
1458
+ this.name = "PluginAlreadyInstalledError";
1459
+ this.pluginName = pluginName;
1460
+ }
1461
+ };
1462
+ //#endregion
1463
+ //#region src/plugin/error/sub/install.ts
1464
+ var PluginInstallError = class extends PluginError {
1465
+ pluginName;
1466
+ constructor(pluginName, cause) {
1467
+ super({
1468
+ message: `Failed to install plugin "${pluginName}".`,
1469
+ code: PluginErrorCode.INSTALL,
1470
+ cause
1471
+ });
1472
+ this.name = "PluginInstallError";
1473
+ this.pluginName = pluginName;
1474
+ }
1475
+ };
1476
+ //#endregion
1477
+ //#region src/plugin/error/sub/not-installed.ts
1478
+ var PluginNotInstalledError = class extends PluginError {
1479
+ pluginName;
1480
+ helperName;
1481
+ constructor(pluginName, helperName) {
1482
+ super({
1483
+ message: `${helperName}() requires the "${pluginName}" plugin. Register it with: router.use(${pluginName}())`,
1484
+ code: PluginErrorCode.NOT_INSTALLED
1485
+ });
1486
+ this.name = "PluginNotInstalledError";
1487
+ this.pluginName = pluginName;
1488
+ this.helperName = helperName;
1489
+ }
1490
+ };
1491
+ //#endregion
1492
+ //#region src/plugin/is.ts
1493
+ function isPlugin(input) {
1494
+ if (!isObject(input)) return false;
1495
+ if (typeof input.name !== "undefined" && typeof input.name !== "string") return false;
1496
+ return typeof input.install === "function" && input.install.length === 1;
1497
+ }
1498
+ //#endregion
1499
+ //#region src/router/utils.ts
1500
+ /**
1501
+ * Build a path-to-regexp-backed `PathMatcher` for the route's mount
1502
+ * path, applying the exact-vs-prefix convention every router should
1503
+ * agree on:
1504
+ *
1505
+ * - `route.method !== undefined` → exact match (method-bound route)
1506
+ * - `route.method === undefined` → prefix match (middleware / nested
1507
+ * app)
1508
+ *
1509
+ * Returns `undefined` when the route has no mount path — middleware
1510
+ * registered without a path matches every request.
1511
+ *
1512
+ * Routers are free to ignore this helper and build their own match
1513
+ * mechanism (radix tree, single aggregated regex, etc.) — it's
1514
+ * provided as a convenience for routers that want path-to-regexp
1515
+ * semantics with minimal boilerplate.
1516
+ */
1517
+ function buildRoutePathMatcher(route) {
1518
+ if (typeof route.path === "undefined") return;
1519
+ const end = typeof route.method !== "undefined";
1520
+ if (!end && route.path === "/") return;
1521
+ return new PathMatcher(route.path, { end });
1522
+ }
1523
+ //#endregion
1524
+ //#region src/router/linear/module.ts
1525
+ /**
1526
+ * Default router — walks registered routes linearly per request and
1527
+ * runs each route's mount-level matcher (built via `buildRoutePathMatcher`,
1528
+ * path-to-regexp-backed). Routes without a mount path (mount-less
1529
+ * middleware / nested apps registered via `.use(handler)`) match every
1530
+ * request directly — there is no per-route `matchPath()` fallback.
1531
+ *
1532
+ * Behaviour-preserving wrapper around the previous in-line stack walk
1533
+ * in `executePipelineStepLookup`. The matcher allocations live here
1534
+ * (not on the registered route), so routers using a different matching
1535
+ * strategy (radix tree, aggregated regex, …) can ignore this file
1536
+ * entirely.
1537
+ *
1538
+ * Optional per-router lookup cache: pass an `ICache` via
1539
+ * `BaseRouterOptions.cache` to skip the linear walk on repeated
1540
+ * requests for the same path. Default is no caching.
1541
+ */
1542
+ var LinearRouter = class LinearRouter {
1543
+ _routes;
1544
+ _matchers;
1545
+ cache;
1546
+ constructor(options = {}) {
1547
+ this._routes = [];
1548
+ this._matchers = [];
1549
+ this.cache = options.cache;
1550
+ }
1551
+ add(route) {
1552
+ this._routes.push(route);
1553
+ this._matchers.push(buildRoutePathMatcher(route));
1554
+ this.cache?.clear();
1555
+ }
1556
+ lookup(path, _method) {
1557
+ const cached = this.cache?.get(path);
1558
+ if (typeof cached !== "undefined") return cached;
1559
+ const matches = [];
1560
+ for (let i = 0; i < this._routes.length; i++) {
1561
+ const route = this._routes[i];
1562
+ const matcher = this._matchers[i];
1563
+ if (matcher) {
1564
+ const output = matcher.exec(path);
1565
+ if (typeof output === "undefined") continue;
1566
+ matches.push({
1567
+ route,
1568
+ index: i,
1569
+ params: output.params,
1570
+ path: output.path
1571
+ });
1572
+ continue;
1573
+ }
1574
+ matches.push({
1575
+ route,
1576
+ index: i,
1577
+ params: Object.create(null)
1578
+ });
1579
+ }
1580
+ this.cache?.set(path, matches);
1581
+ return matches;
1582
+ }
1583
+ clone() {
1584
+ return new LinearRouter({ cache: this.cache?.clone() });
1585
+ }
1586
+ };
1587
+ //#endregion
1588
+ //#region src/router/trie/parser.ts
1589
+ /**
1590
+ * Trie-native path parser.
1591
+ *
1592
+ * Replaces the call-out to `path-to-regexp` for the syntax surface
1593
+ * the trie advertises:
1594
+ *
1595
+ * - Static segments: `users`, `v1`
1596
+ * - Named params: `:id`, `:slug`
1597
+ * - Optional params: `:id?` (T2 — expanded to two variants)
1598
+ * - Optional groups: `{...}` (T2 — expanded to two variants)
1599
+ * - Bare splat: `*` (matches the rest of the path)
1600
+ * - Named splat: `*rest`
1601
+ *
1602
+ * Returns a list of `Segment[]` *variants* — one path string can
1603
+ * expand into multiple route variants when it contains optional
1604
+ * markers. The trie inserts every variant with the same registration
1605
+ * `index` so they dedupe naturally on the candidate list.
1606
+ *
1607
+ * Returns `null` when the path uses syntax outside this surface
1608
+ * (compound segments `/files/:n.ext`, escape sequences `\:`, regex
1609
+ * constraints, splat-not-last, …). The caller falls back to the
1610
+ * universal bucket so correctness is preserved via path-to-regexp.
1611
+ *
1612
+ * Variant cap: nested optional groups expand combinatorially. The
1613
+ * parser caps the variant count at `MAX_VARIANTS` and falls back to
1614
+ * the universal bucket above that — registration-time explosion
1615
+ * isn't worth the lookup-time win on degenerate paths.
1616
+ */
1617
+ const MAX_VARIANTS = 16;
1618
+ const PARAM_NAME = /^[a-zA-Z_]\w*$/;
1619
+ /**
1620
+ * Tokenize a single path segment (the substring between two `/`).
1621
+ * Returns `null` if the segment uses syntax we don't handle.
1622
+ *
1623
+ * Each segment must yield exactly one token — compound segments
1624
+ * (`/files/:n.ext`, `/users-:id`) produce multiple tokens and so
1625
+ * trip this check, falling back to the universal bucket.
1626
+ */
1627
+ function tokenizeSegment(segment) {
1628
+ if (segment === "") return null;
1629
+ if (segment === "*") return {
1630
+ kind: "splat",
1631
+ name: "*"
1632
+ };
1633
+ if (segment.charAt(0) === "*") {
1634
+ const rest = segment.slice(1);
1635
+ if (PARAM_NAME.test(rest)) return {
1636
+ kind: "splat",
1637
+ name: rest
1638
+ };
1639
+ return null;
1640
+ }
1641
+ if (segment.charAt(0) === ":") {
1642
+ const optional = segment.charAt(segment.length - 1) === "?";
1643
+ const nameRaw = optional ? segment.slice(1, -1) : segment.slice(1);
1644
+ if (PARAM_NAME.test(nameRaw)) return {
1645
+ kind: "param",
1646
+ name: nameRaw,
1647
+ optional
1648
+ };
1649
+ return null;
1650
+ }
1651
+ if (/^[a-zA-Z0-9_\-.~%]+$/.test(segment)) return {
1652
+ kind: "literal",
1653
+ value: segment
1654
+ };
1655
+ return null;
1656
+ }
1657
+ /**
1658
+ * Tokenize the full path into a token stream, recognizing slash-
1659
+ * spanning optional groups (`/users{/edit/:id}`).
1660
+ *
1661
+ * The group-open marker is emitted in place of the leading `/`
1662
+ * inside `{...}`; group-close before the trailing `}`. The walker
1663
+ * later expands by either keeping the run between markers or
1664
+ * dropping it.
1665
+ */
1666
+ function tokenizePath(path) {
1667
+ const trimmed = path.charAt(0) === "/" ? path.slice(1) : path;
1668
+ if (trimmed === "") return [];
1669
+ const tokens = [];
1670
+ let i = 0;
1671
+ const n = trimmed.length;
1672
+ while (i < n) {
1673
+ if (trimmed.charAt(i) === "{") {
1674
+ let close = -1;
1675
+ for (let j = i + 1; j < n; j++) {
1676
+ const c = trimmed.charAt(j);
1677
+ if (c === "{") return null;
1678
+ if (c === "}") {
1679
+ close = j;
1680
+ break;
1681
+ }
1682
+ }
1683
+ if (close === -1) return null;
1684
+ const inner = trimmed.slice(i + 1, close);
1685
+ if (inner.charAt(0) !== "/") return null;
1686
+ const after = close + 1 < n ? trimmed.charAt(close + 1) : "";
1687
+ if (after !== "" && after !== "/" && after !== "{") return null;
1688
+ tokens.push({ kind: "groupOpen" });
1689
+ const innerTokens = tokenizePath(inner);
1690
+ if (innerTokens === null) return null;
1691
+ for (const t of innerTokens) tokens.push(t);
1692
+ tokens.push({ kind: "groupClose" });
1693
+ i = close + 1;
1694
+ continue;
1695
+ }
1696
+ let segEnd = i;
1697
+ while (segEnd < n) {
1698
+ const c = trimmed.charAt(segEnd);
1699
+ if (c === "/" || c === "{") break;
1700
+ segEnd++;
1701
+ }
1702
+ const segment = trimmed.slice(i, segEnd);
1703
+ if (segment !== "") {
1704
+ const token = tokenizeSegment(segment);
1705
+ if (token === null) return null;
1706
+ tokens.push(token);
1707
+ }
1708
+ i = segEnd;
1709
+ if (i < n && trimmed.charAt(i) === "/") i++;
1710
+ }
1711
+ return tokens;
1712
+ }
1713
+ /**
1714
+ * Expand the token stream into one or more concrete `Token[]`
1715
+ * variants by:
1716
+ * 1. Splitting `groupOpen` … `groupClose` runs into a "with run"
1717
+ * and a "without run" choice (one optional group → ×2 variants).
1718
+ * 2. Splitting `param.optional = true` into a "with" and "without"
1719
+ * choice (one `:id?` → ×2 variants).
1720
+ *
1721
+ * Caps at `MAX_VARIANTS` — beyond that, returns `null` so the path
1722
+ * falls back to the universal bucket.
1723
+ *
1724
+ * Returns `Token[][]` (not `Segment[][]`) so the recursive
1725
+ * group-expansion can splice inner variants back into the outer
1726
+ * token stream without a lossy round-trip through `Segment`.
1727
+ * `parsePath` does the final `Token → Segment` projection.
1728
+ */
1729
+ function expand(tokens) {
1730
+ let variants = [[]];
1731
+ for (let i = 0; i < tokens.length; i++) {
1732
+ const token = tokens[i];
1733
+ if (token.kind === "groupOpen") {
1734
+ let depth = 1;
1735
+ let close = -1;
1736
+ for (let j = i + 1; j < tokens.length; j++) {
1737
+ const t = tokens[j];
1738
+ if (t.kind === "groupOpen") depth++;
1739
+ else if (t.kind === "groupClose") {
1740
+ depth--;
1741
+ if (depth === 0) {
1742
+ close = j;
1743
+ break;
1744
+ }
1745
+ }
1746
+ }
1747
+ if (close === -1) return null;
1748
+ const expandedInner = expand(tokens.slice(i + 1, close));
1749
+ if (expandedInner === null) return null;
1750
+ const next = [];
1751
+ for (const v of variants) {
1752
+ if (next.length >= MAX_VARIANTS) return null;
1753
+ next.push(v.slice());
1754
+ for (const innerVariant of expandedInner) {
1755
+ if (next.length >= MAX_VARIANTS) return null;
1756
+ next.push(v.concat(innerVariant));
1757
+ }
1758
+ }
1759
+ variants = next;
1760
+ i = close;
1761
+ continue;
1762
+ }
1763
+ if (token.kind === "param" && token.optional) {
1764
+ const stripped = {
1765
+ kind: "param",
1766
+ name: token.name,
1767
+ optional: false
1768
+ };
1769
+ const next = [];
1770
+ for (const v of variants) {
1771
+ if (next.length >= MAX_VARIANTS) return null;
1772
+ next.push(v.slice());
1773
+ if (next.length >= MAX_VARIANTS) return null;
1774
+ next.push(v.concat([stripped]));
1775
+ }
1776
+ variants = next;
1777
+ continue;
1778
+ }
1779
+ for (const v of variants) v.push(token);
1780
+ }
1781
+ return variants;
1782
+ }
1783
+ function tokenToSegment(t) {
1784
+ if (t.kind === "literal") return {
1785
+ kind: "static",
1786
+ value: t.value
1787
+ };
1788
+ if (t.kind === "param") return {
1789
+ kind: "param",
1790
+ name: t.name
1791
+ };
1792
+ if (t.kind === "splat") return {
1793
+ kind: "splat",
1794
+ name: t.name
1795
+ };
1796
+ return null;
1797
+ }
1798
+ /**
1799
+ * Stable, structural identity for a variant — used to drop duplicate
1800
+ * expansions like `/users{/:id?}` (which produces the bare-`/users`
1801
+ * variant twice: once from the "without group" branch and once from
1802
+ * the "with group, without optional param" branch).
1803
+ */
1804
+ function variantKey(segs) {
1805
+ let out = "";
1806
+ for (const s of segs) if (s.kind === "static") out += `/s:${s.value}`;
1807
+ else if (s.kind === "param") out += `/p:${s.name}`;
1808
+ else out += `/*:${s.name}`;
1809
+ return out;
1810
+ }
1811
+ function parsePath(path) {
1812
+ const tokens = tokenizePath(path);
1813
+ if (tokens === null) return null;
1814
+ const variants = expand(tokens);
1815
+ if (variants === null) return null;
1816
+ const result = [];
1817
+ const seen = /* @__PURE__ */ new Set();
1818
+ for (const v of variants) {
1819
+ const segs = [];
1820
+ for (const t of v) {
1821
+ const s = tokenToSegment(t);
1822
+ if (s === null) return null;
1823
+ segs.push(s);
1824
+ }
1825
+ for (let i = 0; i < segs.length - 1; i++) if (segs[i].kind === "splat") return null;
1826
+ const key = variantKey(segs);
1827
+ if (seen.has(key)) continue;
1828
+ seen.add(key);
1829
+ result.push(segs);
1830
+ }
1831
+ return result;
1832
+ }
1833
+ //#endregion
1834
+ //#region src/router/trie/node.ts
1835
+ function createMethodBuckets() {
1836
+ return Object.create(null);
1837
+ }
1838
+ function createTrieNode() {
1839
+ return {
1840
+ staticChildren: /* @__PURE__ */ new Map(),
1841
+ splatRoutes: createMethodBuckets(),
1842
+ exactRoutes: createMethodBuckets(),
1843
+ prefixRoutes: []
1844
+ };
1845
+ }
1846
+ //#endregion
1847
+ //#region src/router/trie/module.ts
1848
+ function decodeOrRaw(s) {
1849
+ try {
1850
+ return decodeURIComponent(s);
1851
+ } catch {
1852
+ return s;
1853
+ }
1854
+ }
1855
+ /**
1856
+ * Build a `params` object from a request's pre-split segments using
1857
+ * the variant's pre-computed `ParamCapture[]`. No regex execution —
1858
+ * each capture is one indexed read from `segments` (and a join for
1859
+ * splats). Replaces the `matcher.exec` confirm pass for trie-walked
1860
+ * routes (T3).
1861
+ */
1862
+ function extractTrieParams(segments, indexMap) {
1863
+ const out = Object.create(null);
1864
+ for (const cap of indexMap) if (cap.kind === "segment") out[cap.name] = decodeOrRaw(segments[cap.depth]);
1865
+ else {
1866
+ const slice = segments.slice(cap.depth).join("/");
1867
+ out[cap.name] = decodeOrRaw(slice);
1868
+ }
1869
+ return out;
1870
+ }
1871
+ /**
1872
+ * Compute `match.path` (the matched-prefix string) from the request's
1873
+ * segments and the variant's recorded depth.
1874
+ *
1875
+ * - Non-splat variants: prefix = `segments[0..matchDepth].join('/')` —
1876
+ * exactly what the variant consumed (request length = variant
1877
+ * length for exact, request length ≥ variant length for prefix).
1878
+ * - Splat variants: the splat absorbed every remaining segment, so
1879
+ * the matched prefix is the entire request path. This mirrors what
1880
+ * `path-to-regexp`'s `output.path` would have returned pre-Phase-2
1881
+ * for `/files/*rest` matching `/files/a/b` (`/files/a/b`, not
1882
+ * `/files`).
1883
+ */
1884
+ function trieMatchedPath(segments, matchDepth, splatTerminated) {
1885
+ const upTo = splatTerminated ? segments.length : matchDepth;
1886
+ if (upTo === 0) return "/";
1887
+ return `/${segments.slice(0, upTo).join("/")}`;
1888
+ }
1889
+ /**
1890
+ * Pre-compute the `ParamCapture[]` for a variant's segments. Walk
1891
+ * the segments in order; emit one entry per `param` segment and a
1892
+ * terminal one for `splat` (always last). Static segments are
1893
+ * structurally consumed by the trie walk; they don't appear here.
1894
+ */
1895
+ function buildParamsIndexMap(segments) {
1896
+ const out = [];
1897
+ for (const [i, seg] of segments.entries()) if (seg.kind === "param") out.push({
1898
+ kind: "segment",
1899
+ depth: i,
1900
+ name: seg.name
1901
+ });
1902
+ else if (seg.kind === "splat") {
1903
+ out.push({
1904
+ kind: "splat",
1905
+ depth: i,
1906
+ name: seg.name
1907
+ });
1908
+ break;
1909
+ }
1910
+ return out;
1911
+ }
1912
+ /**
1913
+ * Decide which method buckets a given request method should pull
1914
+ * from. Always includes `''` (method-agnostic). For HEAD also
1915
+ * includes GET (per `matchHandlerMethod`). For OPTIONS or no-method
1916
+ * lookups, returns `null` to signal "emit every bucket" — needed so
1917
+ * `event.methodsAllowed` is populated for OPTIONS auto-Allow and so
1918
+ * `IRouter.lookup(path)` (no method) keeps returning a complete
1919
+ * candidate set.
1920
+ */
1921
+ function methodBucketKeys(method) {
1922
+ if (typeof method === "undefined" || method === MethodName.OPTIONS) return null;
1923
+ if (method === MethodName.HEAD) return [
1924
+ "",
1925
+ MethodName.HEAD,
1926
+ MethodName.GET
1927
+ ];
1928
+ return ["", method];
1929
+ }
1930
+ function emitBucket(buckets, method, out) {
1931
+ const keys = methodBucketKeys(method);
1932
+ if (keys === null) {
1933
+ for (const k in buckets) {
1934
+ const list = buckets[k];
1935
+ for (const r of list) out.push(r);
1936
+ }
1937
+ return;
1938
+ }
1939
+ for (const k of keys) {
1940
+ const list = buckets[k];
1941
+ if (!list) continue;
1942
+ for (const r of list) out.push(r);
1943
+ }
1944
+ }
1945
+ function hasAnyBucket(buckets) {
1946
+ for (const _k in buckets) return true;
1947
+ return false;
1948
+ }
1949
+ function pushIntoBucket(buckets, methodKey, route) {
1950
+ const bucket = buckets[methodKey];
1951
+ if (bucket) bucket.push(route);
1952
+ else buckets[methodKey] = [route];
1953
+ }
1954
+ /**
1955
+ * Radix-trie router — registers routes into a per-segment tree at
1956
+ * `add()` time and walks the tree at `lookup()` to collect
1957
+ * candidates by structure rather than by linear scan.
1958
+ *
1959
+ * Inspired by Hono's `TrieRouter` and rou3. The trie handles
1960
+ * routup's path vocabulary directly via its own parser
1961
+ * (`./parser.ts`):
1962
+ *
1963
+ * - Static segments (`/users`)
1964
+ * - Named params (`:id`)
1965
+ * - Optional params (`:id?`) — expanded to two route variants at
1966
+ * registration (T2)
1967
+ * - Optional groups (`/users{/edit}`) — same expansion strategy
1968
+ * - Bare and named splats (`/files/*`, `/files/*rest`)
1969
+ *
1970
+ * Per-leaf storage is bucketed by HTTP method (T4) so lookup
1971
+ * narrows to the request method's bucket(s) instead of emitting
1972
+ * every entry at the leaf and letting the dispatcher's filter
1973
+ * discard mismatches.
1974
+ *
1975
+ * Param extraction is `paramsIndexMap`-driven (T3): a pre-built
1976
+ * `Array<{ depth, name }>` per variant lets `extractTrieParams`
1977
+ * read params straight from the request's pre-split segments — no
1978
+ * regex execution per match.
1979
+ *
1980
+ * Paths the trie parser doesn't handle (compound segments like
1981
+ * `/files/:n.ext`, escape sequences `\:`, regex constraints) and
1982
+ * empty/root paths fall through to the `universal` bucket. That
1983
+ * bucket still uses `path-to-regexp` via `buildRoutePathMatcher`,
1984
+ * so correctness is preserved.
1985
+ *
1986
+ * Pure-static-spine fast path (`shortCircuit`): when the request
1987
+ * walks a static spine with no param/splat/prefix siblings on any
1988
+ * traversed node, the leaf's `exactRoutes` (filtered to the request
1989
+ * method's buckets) is the full answer — no need to walk the param
1990
+ * branch or collect prefix candidates at intermediate nodes.
1991
+ */
1992
+ var TrieRouter = class TrieRouter {
1993
+ /**
1994
+ * Monotonic counter assigned as the registration `index` on each
1995
+ * route — the dispatch loop uses it to preserve registration
1996
+ * order across the candidate list. App owns the canonical
1997
+ * `Route<T>[]` list (Plan 019); the trie no longer keeps a
1998
+ * parallel copy.
1999
+ */
2000
+ _routeCount;
2001
+ root;
2002
+ /**
2003
+ * Routes that bypass the trie — registered with no path, with
2004
+ * the root path `/`, or with a path containing syntax the
2005
+ * parser doesn't recognise. Walked linearly on every lookup,
2006
+ * merged into the result in registration order.
2007
+ */
2008
+ universal;
2009
+ cache;
2010
+ constructor(options = {}) {
2011
+ this._routeCount = 0;
2012
+ this.root = createTrieNode();
2013
+ this.universal = [];
2014
+ this.cache = options.cache;
2015
+ }
2016
+ add(route) {
2017
+ const index = this._routeCount++;
2018
+ if (typeof route.path !== "string" || route.path === "" || route.path === "/") {
2019
+ this.universal.push({
2020
+ route,
2021
+ index,
2022
+ matcher: buildRoutePathMatcher(route)
2023
+ });
2024
+ this.cache?.clear();
2025
+ return;
2026
+ }
2027
+ const variants = parsePath(route.path);
2028
+ if (variants === null) {
2029
+ this.universal.push({
2030
+ route,
2031
+ index,
2032
+ matcher: buildRoutePathMatcher(route)
2033
+ });
2034
+ this.cache?.clear();
2035
+ return;
2036
+ }
2037
+ for (const segments of variants) this.insertIntoTrie(segments, route, index);
2038
+ this.cache?.clear();
2039
+ }
2040
+ lookup(path, method) {
2041
+ const cacheKey = `${method ?? ""}\t${path}`;
2042
+ const cached = this.cache?.get(cacheKey);
2043
+ if (typeof cached !== "undefined") return cached;
2044
+ const candidates = [];
2045
+ for (const u of this.universal) candidates.push(u);
2046
+ const segments = this.parseRequestPath(path);
2047
+ const shortCircuit = this.shortCircuit(segments, method);
2048
+ if (shortCircuit !== null) for (const c of shortCircuit) candidates.push(c);
2049
+ else this.walk(this.root, segments, 0, candidates, method);
2050
+ candidates.sort((a, b) => {
2051
+ if (a.index !== b.index) return a.index - b.index;
2052
+ const ad = a.matchDepth ?? -1;
2053
+ return (b.matchDepth ?? -1) - ad;
2054
+ });
2055
+ const matches = [];
2056
+ let lastIndex = -1;
2057
+ for (const candidate of candidates) {
2058
+ const { route, index, matcher, paramsIndexMap, matchDepth } = candidate;
2059
+ if (index === lastIndex) continue;
2060
+ if (matcher) {
2061
+ const output = matcher.exec(path);
2062
+ if (typeof output === "undefined") continue;
2063
+ matches.push({
2064
+ route,
2065
+ index,
2066
+ params: this.assignParams(output.params),
2067
+ path: output.path
2068
+ });
2069
+ lastIndex = index;
2070
+ continue;
2071
+ }
2072
+ if (paramsIndexMap && typeof matchDepth === "number") {
2073
+ matches.push({
2074
+ route,
2075
+ index,
2076
+ params: extractTrieParams(segments, paramsIndexMap),
2077
+ path: trieMatchedPath(segments, matchDepth, candidate.splatTerminated === true)
2078
+ });
2079
+ lastIndex = index;
2080
+ continue;
2081
+ }
2082
+ matches.push({
2083
+ route,
2084
+ index,
2085
+ params: Object.create(null)
2086
+ });
2087
+ lastIndex = index;
2088
+ }
2089
+ this.cache?.set(cacheKey, matches);
2090
+ return matches;
2091
+ }
2092
+ clone() {
2093
+ return new TrieRouter({ cache: this.cache?.clone() });
2094
+ }
2095
+ /**
2096
+ * T1: returns the pre-computed candidate list when the request's
2097
+ * static spine has no param sibling, no prefix routes, and no
2098
+ * splats along the way. The leaf node's `exactRoutes` (filtered
2099
+ * to the request method's buckets) is then the complete answer —
2100
+ * no need to walk the param branch or collect prefix/splat
2101
+ * candidates from intermediate nodes. When any branch is
2102
+ * encountered, returns `null` and the caller falls through to
2103
+ * the regular `walk`.
2104
+ */
2105
+ shortCircuit(segments, method) {
2106
+ let node = this.root;
2107
+ for (const segment of segments) {
2108
+ if (node.paramChild || hasAnyBucket(node.splatRoutes) || node.prefixRoutes.length > 0) return null;
2109
+ const child = node.staticChildren.get(segment);
2110
+ if (!child) return null;
2111
+ node = child;
2112
+ }
2113
+ if (node.paramChild || hasAnyBucket(node.splatRoutes) || node.prefixRoutes.length > 0) return null;
2114
+ const out = [];
2115
+ emitBucket(node.exactRoutes, method, out);
2116
+ return out;
2117
+ }
2118
+ parseRequestPath(path) {
2119
+ const trimmed = path.charAt(0) === "/" ? path.slice(1) : path;
2120
+ if (trimmed === "") return [];
2121
+ const parts = trimmed.split("/");
2122
+ const result = [];
2123
+ for (const part of parts) if (part !== "") result.push(part);
2124
+ return result;
2125
+ }
2126
+ insertIntoTrie(segments, route, index) {
2127
+ let node = this.root;
2128
+ const exact = this.isExactMatchRoute(route);
2129
+ const methodKey = route.method ?? "";
2130
+ const paramsIndexMap = buildParamsIndexMap(segments);
2131
+ for (const [i, segment] of segments.entries()) {
2132
+ const seg = segment;
2133
+ if (seg.kind === "splat") {
2134
+ pushIntoBucket(node.splatRoutes, methodKey, {
2135
+ route,
2136
+ index,
2137
+ paramsIndexMap,
2138
+ matchDepth: i,
2139
+ splatTerminated: true
2140
+ });
2141
+ return;
2142
+ }
2143
+ if (seg.kind === "param") {
2144
+ if (!node.paramChild) node.paramChild = createTrieNode();
2145
+ node = node.paramChild;
2146
+ continue;
2147
+ }
2148
+ let child = node.staticChildren.get(seg.value);
2149
+ if (!child) {
2150
+ child = createTrieNode();
2151
+ node.staticChildren.set(seg.value, child);
2152
+ }
2153
+ node = child;
2154
+ }
2155
+ const indexed = {
2156
+ route,
2157
+ index,
2158
+ paramsIndexMap,
2159
+ matchDepth: segments.length
2160
+ };
2161
+ if (exact) pushIntoBucket(node.exactRoutes, methodKey, indexed);
2162
+ else node.prefixRoutes.push(indexed);
2163
+ }
2164
+ walk(node, segments, depth, collected, method) {
2165
+ emitBucket(node.splatRoutes, method, collected);
2166
+ if (depth === segments.length) {
2167
+ emitBucket(node.exactRoutes, method, collected);
2168
+ for (const p of node.prefixRoutes) collected.push(p);
2169
+ return;
2170
+ }
2171
+ for (const p of node.prefixRoutes) collected.push(p);
2172
+ const seg = segments[depth];
2173
+ const staticChild = node.staticChildren.get(seg);
2174
+ if (staticChild) this.walk(staticChild, segments, depth + 1, collected, method);
2175
+ if (node.paramChild) this.walk(node.paramChild, segments, depth + 1, collected, method);
2176
+ }
2177
+ isExactMatchRoute(route) {
2178
+ return typeof route.method !== "undefined";
2179
+ }
2180
+ /**
2181
+ * T5: copy params onto a prototype-less object so downstream
2182
+ * lookups skip prototype-chain traversal and avoid `__proto__` /
2183
+ * `hasOwnProperty` shadowing from user-controlled segment values.
2184
+ */
2185
+ assignParams(source) {
2186
+ const out = Object.create(null);
2187
+ for (const k in source) if (Object.prototype.hasOwnProperty.call(source, k)) out[k] = source[k];
2188
+ return out;
2189
+ }
2190
+ };
2191
+ //#endregion
2192
+ //#region src/router/smart/module.ts
2193
+ /**
2194
+ * Default crossover. Empirically `LinearRouter` wins for small route
2195
+ * counts (no per-request trie walk overhead, no static-spine setup);
2196
+ * `TrieRouter` wins past ~30 entries on typical workloads. Override
2197
+ * via `SmartRouterOptions.threshold` when a benchmark says otherwise
2198
+ * for your route shape.
2199
+ */
2200
+ const DEFAULT_THRESHOLD = 30;
2201
+ /**
2202
+ * Auto-selecting router. Accumulates registered routes in a pending
2203
+ * buffer; on the first `lookup()` call, picks `LinearRouter` or
2204
+ * `TrieRouter` based on the registered route count and replays the
2205
+ * pending list onto the chosen inner router. Every subsequent call
2206
+ * — `add`, `lookup`, `clone` — forwards to the inner.
2207
+ *
2208
+ * Use this when you don't want to commit to a router family up-front
2209
+ * (e.g. a library that registers a variable number of routes
2210
+ * depending on configuration). For known workloads, prefer the
2211
+ * concrete router — `SmartRouter` adds one indirection per call.
2212
+ *
2213
+ * Inspired by Hono's `SmartRouter` (which auto-selects across more
2214
+ * candidates including `RegExpRouter`); ours covers the only choice
2215
+ * that matters in routup today: linear-vs-trie at the registration-
2216
+ * size crossover.
2217
+ */
2218
+ var SmartRouter = class SmartRouter {
2219
+ inner;
2220
+ pending = [];
2221
+ threshold;
2222
+ /**
2223
+ * Cache handed off to whichever inner router gets chosen. Stays
2224
+ * `undefined` if the user didn't configure one.
2225
+ */
2226
+ cache;
2227
+ constructor(options = {}) {
2228
+ this.threshold = options.threshold ?? DEFAULT_THRESHOLD;
2229
+ this.cache = options.cache;
2230
+ }
2231
+ add(route) {
2232
+ if (this.inner) {
2233
+ this.inner.add(route);
2234
+ return;
2235
+ }
2236
+ this.pending.push(route);
2237
+ }
2238
+ lookup(path, method) {
2239
+ if (!this.inner) {
2240
+ this.inner = this.choose();
2241
+ for (const r of this.pending) this.inner.add(r);
2242
+ this.pending = [];
2243
+ }
2244
+ return this.inner.lookup(path, method);
2245
+ }
2246
+ clone() {
2247
+ return new SmartRouter({
2248
+ threshold: this.threshold,
2249
+ cache: this.cache?.clone()
2250
+ });
2251
+ }
2252
+ /**
2253
+ * Pick the inner router based on the registered route count.
2254
+ * `LinearRouter` for tiny tables, `TrieRouter` past the
2255
+ * configured threshold.
2256
+ *
2257
+ * @protected
2258
+ */
2259
+ choose() {
2260
+ if (this.pending.length < this.threshold) return new LinearRouter({ cache: this.cache });
2261
+ return new TrieRouter({ cache: this.cache });
2262
+ }
2263
+ };
2264
+ //#endregion
2265
+ //#region src/app/options.ts
2266
+ function normalizeAppOptions(input) {
2267
+ let etag;
2268
+ if (typeof input.etag !== "undefined") if (input.etag === null || input.etag === false) etag = null;
2269
+ else etag = buildEtagFn(input.etag);
2270
+ let trustProxy;
2271
+ if (typeof input.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(input.trustProxy);
2272
+ if (typeof input.timeout !== "undefined") {
2273
+ if (!Number.isFinite(input.timeout) || input.timeout <= 0) delete input.timeout;
2274
+ }
2275
+ if (typeof input.handlerTimeout !== "undefined") {
2276
+ if (!Number.isFinite(input.handlerTimeout) || input.handlerTimeout <= 0) delete input.handlerTimeout;
2277
+ }
2278
+ return {
2279
+ ...input,
2280
+ etag,
2281
+ trustProxy
2282
+ };
2283
+ }
2284
+ //#endregion
2285
+ //#region src/app/constants.ts
2286
+ const AppSymbol = Symbol.for("App");
2287
+ //#endregion
2288
+ //#region src/app/check.ts
2289
+ function isAppInstance(input) {
2290
+ return hasInstanceof(input, AppSymbol);
2291
+ }
2292
+ //#endregion
2293
+ //#region src/app/module.ts
2294
+ /**
2295
+ * Merge resolver-supplied path params into `event.params` *only* when
2296
+ * `match.params` actually has keys. Skipping the object spread on the
2297
+ * empty-params path (every static route, every middleware match) saves
2298
+ * an allocation per match — the hottest path in static-route apps.
2299
+ */
2300
+ function mergeMatchParams(event, matchParams) {
2301
+ let hasKeys = false;
2302
+ for (const _k in matchParams) {
2303
+ hasKeys = true;
2304
+ break;
2305
+ }
2306
+ if (!hasKeys) return;
2307
+ event.params = {
2308
+ ...event.params,
2309
+ ...matchParams
2310
+ };
2311
+ }
2312
+ var App = class App {
2313
+ /**
2314
+ * A label for the App instance.
2315
+ */
2316
+ name;
2317
+ /**
2318
+ * Registration-time path prefix for entries registered on this
2319
+ * App. Local to this instance — never inherited from a parent.
2320
+ *
2321
+ * @protected
2322
+ */
2323
+ _path;
2324
+ /**
2325
+ * Pluggable router (route table) — owns the "which entries match
2326
+ * this path?" lookup. Defaults to `LinearRouter` (walks entries
2327
+ * linearly per request); swap in via `AppContext.router`
2328
+ * for a radix/trie implementation on apps with many routes.
2329
+ *
2330
+ * @protected
2331
+ */
2332
+ router;
2333
+ /**
2334
+ * Normalized options for this App instance.
2335
+ *
2336
+ * Frozen on construction — once published to `event.appOptions`
2337
+ * it is shared across all requests, and a handler must not be
2338
+ * able to mutate router-global state.
2339
+ */
2340
+ _options;
2341
+ /**
2342
+ * Registry of installed plugins (name → version) on this App.
2343
+ *
2344
+ * Read by `use(otherApp)` (via the public `plugins` getter) so
2345
+ * plugin registries merge into the parent at flatten time —
2346
+ * `parent.hasPlugin('foo')` then reflects plugins installed on
2347
+ * apps mounted into it.
2348
+ *
2349
+ * @protected
2350
+ */
2351
+ _plugins;
2352
+ /**
2353
+ * Every route registered on this App, in registration order.
2354
+ *
2355
+ * Read by `use(otherApp)` to snapshot routes at flatten time
2356
+ * and by `clone()` to seed the copy. Late mutations to `_routes`
2357
+ * after a flatten do not propagate.
2358
+ */
2359
+ _routes = [];
2360
+ constructor(input = {}) {
2361
+ this.name = input.name;
2362
+ this._path = input.path;
2363
+ this._plugins = new Map(input.plugins);
2364
+ this.router = input.router ?? new LinearRouter();
2365
+ this._options = Object.freeze(normalizeAppOptions(input.options ?? {}));
2366
+ markInstanceof(this, AppSymbol);
2367
+ }
2368
+ /**
2369
+ * Public read of the canonical route list. Used by `use(child)`
2370
+ * to snapshot the child's routes at flatten time. Returned
2371
+ * as `readonly` — callers must not mutate.
2372
+ */
2373
+ get routes() {
2374
+ return this._routes;
2375
+ }
2376
+ /**
2377
+ * Public read of the installed-plugin registry. Used by
2378
+ * `use(child)` to merge child plugins into the parent at
2379
+ * flatten time. Returned as `ReadonlyMap` — callers must not
2380
+ * mutate; go through `use(plugin)` to install.
2381
+ */
2382
+ get plugins() {
2383
+ return this._plugins;
2384
+ }
2385
+ /**
2386
+ * Register a route with the active router and record it on the
2387
+ * App so `clone` / `setRouter` / `use(child)` can read the
2388
+ * canonical list back.
2389
+ *
2390
+ * @protected
2391
+ */
2392
+ register(route) {
2393
+ this.router.add(route);
2394
+ this._routes.push(route);
2395
+ }
2396
+ /**
2397
+ * Swap the active router. Replays every previously-registered
2398
+ * route onto the new router so lookups stay correct.
2399
+ *
2400
+ * Useful for picking a router after route shape is known (e.g.
2401
+ * a SmartRouter-style decision), or for testing alternatives
2402
+ * mid-flight without rebuilding the App. Any cache the previous
2403
+ * router carried is dropped along with it.
2404
+ */
2405
+ setRouter(router) {
2406
+ for (const route of this._routes) router.add(route);
2407
+ this.router = router;
2408
+ }
2409
+ /**
2410
+ * Public entry point — creates a DispatcherEvent from the request,
2411
+ * runs the pipeline, and returns a Response (with 404/500 fallbacks).
2412
+ */
2413
+ async fetch(request) {
2414
+ const event = new DispatcherEvent(request);
2415
+ let response;
2416
+ try {
2417
+ const timeoutMs = this._options.timeout;
2418
+ if (timeoutMs) {
2419
+ const controller = new AbortController();
2420
+ event.signal = controller.signal;
2421
+ let timerId;
2422
+ try {
2423
+ response = await Promise.race([this.dispatch(event), new Promise((_, reject) => {
2424
+ timerId = setTimeout(() => {
2425
+ controller.abort();
2426
+ reject(createError({
2427
+ status: 408,
2428
+ message: "Request Timeout"
2429
+ }));
2430
+ }, timeoutMs);
2431
+ })]);
2432
+ } finally {
2433
+ clearTimeout(timerId);
2434
+ }
2435
+ } else response = await this.dispatch(event);
2436
+ } catch (e) {
2437
+ event.error = createError(e);
2438
+ }
2439
+ if (response) return response;
2440
+ if (event.error) return this.buildFallbackResponse(request, event, event.error.status || 500, event.error.message);
2441
+ return this.buildFallbackResponse(request, event, 404, "Not Found");
2442
+ }
2443
+ buildFallbackResponse(request, event, status, message) {
2444
+ const headers = new Headers(event.response.headers);
2445
+ if (acceptsJson(request)) {
2446
+ headers.set("content-type", "application/json; charset=utf-8");
2447
+ return new Response(JSON.stringify({
2448
+ status,
2449
+ message
2450
+ }), {
2451
+ status,
2452
+ headers
2453
+ });
2454
+ }
2455
+ headers.set("content-type", "text/plain; charset=utf-8");
2456
+ return new Response(message, {
2457
+ status,
2458
+ headers
2459
+ });
2460
+ }
2461
+ async dispatch(event) {
2462
+ const savedPath = event.path;
2463
+ const savedMountPath = event.mountPath;
2464
+ const savedParams = event.params;
2465
+ const savedAppOptions = event.appOptions;
2466
+ const wasDispatching = event.isDispatching;
2467
+ const isRoot = !wasDispatching;
2468
+ event.appOptions = this._options;
2469
+ event.isDispatching = true;
2470
+ let response;
2471
+ try {
2472
+ const matches = this.router.lookup(event.path, event.method);
2473
+ response = await this.runMatches(event, matches, event.path, 0);
2474
+ if (!event.error && !event.dispatched && isRoot && event.method === MethodName.OPTIONS) {
2475
+ if (event.methodsAllowed.has(MethodName.GET)) event.methodsAllowed.add(MethodName.HEAD);
2476
+ const options = [...event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
2477
+ const optionsHeaders = new Headers(event.response.headers);
2478
+ optionsHeaders.set(HeaderName.ALLOW, options);
2479
+ response = new Response(options, {
2480
+ status: event.response.status || 200,
2481
+ headers: optionsHeaders
2482
+ });
2483
+ event.dispatched = true;
2484
+ }
2485
+ } finally {
2486
+ event.appOptions = savedAppOptions;
2487
+ event.isDispatching = wasDispatching;
2488
+ if (!event.dispatched) {
2489
+ event.path = savedPath;
2490
+ event.mountPath = savedMountPath;
2491
+ event.params = savedParams;
2492
+ }
2493
+ }
2494
+ return response;
2495
+ }
2496
+ /**
2497
+ * Walk the matched routes for the current event, dispatching each
2498
+ * handler in order. Re-entered (recursively) from the `setNext`
2499
+ * continuation so `event.next()` resumes from the next match.
2500
+ */
2501
+ async runMatches(event, matches, matchesPath, startIndex) {
2502
+ let i = startIndex;
2503
+ let response;
2504
+ while (!event.dispatched && i < matches.length) {
2505
+ const match = matches[i];
2506
+ const handler = match.route.data;
2507
+ if (event.error && handler.type === HandlerType.CORE || !event.error && handler.type === HandlerType.ERROR) {
2508
+ i++;
2509
+ continue;
2510
+ }
2511
+ const { method } = match.route;
2512
+ if (method) event.methodsAllowed.add(method);
2513
+ if (!matchHandlerMethod(method, event.method)) {
2514
+ i++;
2515
+ continue;
2516
+ }
2517
+ mergeMatchParams(event, match.params);
2518
+ const savedMountPath = event.mountPath;
2519
+ if (typeof match.path === "string") event.mountPath = match.path;
2520
+ const capturedMatches = matches;
2521
+ const capturedMatchesPath = matchesPath;
2522
+ const nextIndex = i + 1;
2523
+ event.setNext(async (error) => {
2524
+ if (error) event.error = createError(error);
2525
+ const pathChanged = event.path !== capturedMatchesPath;
2526
+ const nextMatches = pathChanged ? this.router.lookup(event.path, event.method) : capturedMatches;
2527
+ const nextMatchesPath = pathChanged ? event.path : capturedMatchesPath;
2528
+ const nextStart = pathChanged ? 0 : nextIndex;
2529
+ return this.runMatches(event, nextMatches, nextMatchesPath, nextStart);
2530
+ });
2531
+ try {
2532
+ const dispatchResponse = await handler.dispatch(event);
2533
+ if (dispatchResponse) {
2534
+ response = dispatchResponse;
2535
+ event.dispatched = true;
2536
+ }
2537
+ } catch (e) {
2538
+ event.error = createError(e);
2539
+ } finally {
2540
+ event.mountPath = savedMountPath;
2541
+ }
2542
+ i++;
2543
+ }
2544
+ return response;
2545
+ }
2546
+ delete(...input) {
2547
+ this.useForMethod(MethodName.DELETE, ...input);
2548
+ return this;
2549
+ }
2550
+ get(...input) {
2551
+ this.useForMethod(MethodName.GET, ...input);
2552
+ return this;
2553
+ }
2554
+ post(...input) {
2555
+ this.useForMethod(MethodName.POST, ...input);
2556
+ return this;
2557
+ }
2558
+ put(...input) {
2559
+ this.useForMethod(MethodName.PUT, ...input);
2560
+ return this;
2561
+ }
2562
+ patch(...input) {
2563
+ this.useForMethod(MethodName.PATCH, ...input);
2564
+ return this;
2565
+ }
2566
+ head(...input) {
2567
+ this.useForMethod(MethodName.HEAD, ...input);
2568
+ return this;
2569
+ }
2570
+ options(...input) {
2571
+ this.useForMethod(MethodName.OPTIONS, ...input);
2572
+ return this;
2573
+ }
2574
+ useForMethod(method, ...input) {
2575
+ let path;
2576
+ for (const element of input) {
2577
+ if (isPath(element)) {
2578
+ path = element;
2579
+ continue;
2580
+ }
2581
+ let handler;
2582
+ if (isHandler(element)) handler = element;
2583
+ else if (isHandlerOptions(element)) handler = new Handler({
2584
+ ...element,
2585
+ method
2586
+ });
2587
+ else continue;
2588
+ this.register({
2589
+ path: joinPaths(this._path, path, handler.path),
2590
+ method,
2591
+ data: handler
2592
+ });
2593
+ }
2594
+ }
2595
+ use(...input) {
2596
+ let path;
2597
+ for (const item of input) {
2598
+ if (isPath(item)) {
2599
+ path = withLeadingSlash(item);
2600
+ continue;
2601
+ }
2602
+ if (isAppInstance(item)) {
2603
+ this.flatten(item, path);
2604
+ continue;
2605
+ }
2606
+ if (isHandler(item)) {
2607
+ this.register({
2608
+ path: joinPaths(this._path, path, item.path),
2609
+ method: item.method,
2610
+ data: item
2611
+ });
2612
+ continue;
2613
+ }
2614
+ if (isHandlerOptions(item)) {
2615
+ const handler = new Handler({ ...item });
2616
+ this.register({
2617
+ path: joinPaths(this._path, path, handler.path),
2618
+ method: handler.method,
2619
+ data: handler
2620
+ });
2621
+ continue;
2622
+ }
2623
+ if (isPlugin(item)) if (path) this.install(item, { path });
2624
+ else this.install(item);
2625
+ }
2626
+ return this;
2627
+ }
2628
+ /**
2629
+ * Snapshot a child App's routes and plugin registry into this
2630
+ * one. Each route's path is prefixed with `this._path`, the
2631
+ * supplied mount `path`, and the route's own path (in that
2632
+ * order); the resulting entry is registered on this App's
2633
+ * router. The child app is not retained — late mutations on it
2634
+ * after this call do not propagate.
2635
+ *
2636
+ * @protected
2637
+ */
2638
+ flatten(child, path) {
2639
+ for (const name of child.plugins.keys()) if (this._plugins.has(name)) throw new PluginAlreadyInstalledError(name);
2640
+ for (const [name, version] of child.plugins) this._plugins.set(name, version);
2641
+ for (const route of child.routes) this.register({
2642
+ path: joinPaths(this._path, path, route.path),
2643
+ method: route.method,
2644
+ data: route.data
2645
+ });
2646
+ }
2647
+ /**
2648
+ * Check if a plugin with the given name is installed on this App.
2649
+ */
2650
+ hasPlugin(name) {
2651
+ return this._plugins.has(name);
2652
+ }
2653
+ /**
2654
+ * Get the version of an installed plugin by name, or `undefined`
2655
+ * if the plugin is not installed.
2656
+ */
2657
+ getPluginVersion(name) {
2658
+ return this._plugins.get(name);
2659
+ }
2660
+ install(plugin, context = {}) {
2661
+ if (this._plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
2662
+ const scratch = new App({ name: plugin.name });
2663
+ plugin.install(scratch);
2664
+ if (context.path) this.use(context.path, scratch);
2665
+ else this.use(scratch);
2666
+ this._plugins.set(plugin.name, plugin.version);
2667
+ return this;
2668
+ }
2669
+ /**
2670
+ * Return a new `App` that mirrors this one but owns independent
2671
+ * mountable state — fresh router of the same family seeded with
2672
+ * the current routes, shallow copy of options, and a fresh
2673
+ * plugins map carrying the same entries.
2674
+ */
2675
+ clone() {
2676
+ const next = new App({
2677
+ name: this.name,
2678
+ path: this._path,
2679
+ options: { ...this._options },
2680
+ plugins: this._plugins,
2681
+ router: this.router.clone()
2682
+ });
2683
+ for (const route of this._routes) next.register({
2684
+ path: route.path,
2685
+ method: route.method,
2686
+ data: route.data
2687
+ });
2688
+ return next;
2689
+ }
2690
+ };
2691
+ //#endregion
2692
+ export { isError as $, fromWebHandler as A, DispatcherEvent as B, getRequestAcceptableEncodings as C, matchHandlerMethod as D, isRequestCacheable as E, defineErrorHandler as F, getRequestAcceptableContentTypes as G, sendRedirect as H, defineCoreHandler as I, sendFile as J, useRequestNegotiator as K, Handler as L, isWebHandlerProvider as M, fromNodeHandler as N, isHandler as O, fromNodeMiddleware as P, createError as Q, HandlerSymbol as R, getRequestAcceptableEncoding as S, getRequestAcceptableCharsets as T, sendFormat as U, sendStream as V, getRequestAcceptableContentType as W, sendAccepted as X, sendCreated as Y, toResponse as Z, getRequestIP as _, LinearRouter as a, appendResponseHeaderDirective as at, getRequestAcceptableLanguage as b, PluginNotInstalledError as c, AppError as ct, PluginError as d, AppEvent as dt, setResponseHeaderContentType as et, isPluginError as f, HeaderName as ft, getRequestProtocol as g, PathMatcher as h, TrieRouter as i, appendResponseHeader as it, isWebHandler as j, isHandlerOptions as k, PluginInstallError as l, ErrorSymbol as lt, isPath as m, LruCache as mt, normalizeAppOptions as n, setResponseHeaderInline as nt, buildRoutePathMatcher as o, createEventStream as ot, PluginErrorCode as p, MethodName as pt, getRequestHeader as q, SmartRouter as r, setResponseContentTypeByFileName as rt, isPlugin as s, serializeEventStreamMessage as st, App as t, setResponseHeaderAttachment as tt, PluginAlreadyInstalledError as u, setResponseCacheHeaders as ut, getRequestHostName as v, getRequestAcceptableCharset as w, getRequestAcceptableLanguages as x, matchRequestContentType as y, HandlerType as z };
2693
+
2694
+ //# sourceMappingURL=src-DaK6SZc0.mjs.map