routup 5.1.1 → 6.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -41
- package/dist/bun.d.mts +3 -3
- package/dist/bun.mjs +4 -4
- package/dist/bun.mjs.map +1 -1
- package/dist/cloudflare.d.mts +3 -3
- package/dist/cloudflare.mjs +4 -4
- package/dist/cloudflare.mjs.map +1 -1
- package/dist/deno.d.mts +3 -3
- package/dist/deno.mjs +4 -4
- package/dist/deno.mjs.map +1 -1
- package/dist/generic.d.mts +3 -3
- package/dist/generic.mjs +4 -4
- package/dist/generic.mjs.map +1 -1
- package/dist/index-kxLRw2Wc.d.mts +1950 -0
- package/dist/node.d.mts +4 -4
- package/dist/node.mjs +6 -6
- package/dist/node.mjs.map +1 -1
- package/dist/service-worker.d.mts +3 -3
- package/dist/service-worker.mjs +4 -4
- package/dist/service-worker.mjs.map +1 -1
- package/dist/src-gmPicCWT.mjs +3032 -0
- package/dist/src-gmPicCWT.mjs.map +1 -0
- package/package.json +8 -6
- package/dist/index-CvJhS_a6.d.mts +0 -1128
- package/dist/src-DFLGrih4.mjs +0 -1921
- package/dist/src-DFLGrih4.mjs.map +0 -1
|
@@ -0,0 +1,3032 @@
|
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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 nested apps
|
|
819
|
+
* temporarily override). Initialized to `{}` so consumers
|
|
820
|
+
* reading before any dispatch get a valid (empty) shape.
|
|
821
|
+
*/
|
|
822
|
+
appOptions;
|
|
823
|
+
/**
|
|
824
|
+
* `true` while at least one `App.dispatch` is on the call stack
|
|
825
|
+
* for this event. `App.dispatch` reads this on entry to derive
|
|
826
|
+
* `isRoot` and writes it on entry/exit so nested calls see it
|
|
827
|
+
* already set.
|
|
828
|
+
*/
|
|
829
|
+
isDispatching;
|
|
830
|
+
_dispatched;
|
|
831
|
+
_response;
|
|
832
|
+
_store;
|
|
833
|
+
/**
|
|
834
|
+
* Cached parsed URL (avoids double-parsing).
|
|
835
|
+
*/
|
|
836
|
+
_url;
|
|
837
|
+
/**
|
|
838
|
+
* Continuation function for middleware onion model.
|
|
839
|
+
*/
|
|
840
|
+
_next;
|
|
841
|
+
_signal;
|
|
842
|
+
_signalCleanup;
|
|
843
|
+
/**
|
|
844
|
+
* Whether _next has already been called (guard against double-invocation).
|
|
845
|
+
*/
|
|
846
|
+
_nextCalled;
|
|
847
|
+
/**
|
|
848
|
+
* The cached result of the next handler.
|
|
849
|
+
*/
|
|
850
|
+
_nextResult;
|
|
851
|
+
constructor(request) {
|
|
852
|
+
this.request = request;
|
|
853
|
+
this._url = new FastURL(request.url);
|
|
854
|
+
this.method = request.method;
|
|
855
|
+
this.path = this._url.pathname;
|
|
856
|
+
this.mountPath = "/";
|
|
857
|
+
this.params = {};
|
|
858
|
+
this.appOptions = {};
|
|
859
|
+
this.isDispatching = false;
|
|
860
|
+
this.methodsAllowed = /* @__PURE__ */ new Set();
|
|
861
|
+
this._dispatched = false;
|
|
862
|
+
this._nextCalled = false;
|
|
863
|
+
}
|
|
864
|
+
get response() {
|
|
865
|
+
if (!this._response) this._response = {
|
|
866
|
+
status: 200,
|
|
867
|
+
headers: new Headers()
|
|
868
|
+
};
|
|
869
|
+
return this._response;
|
|
870
|
+
}
|
|
871
|
+
get signal() {
|
|
872
|
+
if (!this._signal) this._signal = this.request.signal;
|
|
873
|
+
return this._signal;
|
|
874
|
+
}
|
|
875
|
+
set signal(value) {
|
|
876
|
+
if (this._signalCleanup) {
|
|
877
|
+
this._signalCleanup();
|
|
878
|
+
this._signalCleanup = void 0;
|
|
879
|
+
}
|
|
880
|
+
if (value === this.request.signal) {
|
|
881
|
+
this._signal = value;
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const controller = new AbortController();
|
|
885
|
+
const abort = (e) => {
|
|
886
|
+
const reason = e?.target instanceof AbortSignal ? e.target.reason : void 0;
|
|
887
|
+
this.request.signal.removeEventListener("abort", abort);
|
|
888
|
+
value.removeEventListener("abort", abort);
|
|
889
|
+
controller.abort(reason);
|
|
890
|
+
};
|
|
891
|
+
if (this.request.signal.aborted || value.aborted) {
|
|
892
|
+
const reason = this.request.signal.aborted ? this.request.signal.reason : value.reason;
|
|
893
|
+
controller.abort(reason);
|
|
894
|
+
} else {
|
|
895
|
+
this.request.signal.addEventListener("abort", abort, { once: true });
|
|
896
|
+
value.addEventListener("abort", abort, { once: true });
|
|
897
|
+
this._signalCleanup = () => {
|
|
898
|
+
this.request.signal.removeEventListener("abort", abort);
|
|
899
|
+
value.removeEventListener("abort", abort);
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
this._signal = controller.signal;
|
|
903
|
+
}
|
|
904
|
+
get dispatched() {
|
|
905
|
+
return this._dispatched;
|
|
906
|
+
}
|
|
907
|
+
set dispatched(value) {
|
|
908
|
+
this._dispatched = value;
|
|
909
|
+
}
|
|
910
|
+
async next(event, error) {
|
|
911
|
+
if (this._nextCalled) return this._nextResult;
|
|
912
|
+
this._nextCalled = true;
|
|
913
|
+
if (this._next) this._nextResult = this._next(event, error);
|
|
914
|
+
return this._nextResult;
|
|
915
|
+
}
|
|
916
|
+
setNext(fn) {
|
|
917
|
+
if (fn) this._next = async (event, error) => {
|
|
918
|
+
return toResponse(await fn(error), event);
|
|
919
|
+
};
|
|
920
|
+
else this._next = void 0;
|
|
921
|
+
this._nextCalled = false;
|
|
922
|
+
this._nextResult = void 0;
|
|
923
|
+
}
|
|
924
|
+
build(signal) {
|
|
925
|
+
return new AppEvent({
|
|
926
|
+
request: this.request,
|
|
927
|
+
params: this.params,
|
|
928
|
+
path: this.path,
|
|
929
|
+
method: this.method,
|
|
930
|
+
mountPath: this.mountPath,
|
|
931
|
+
headers: this.request.headers,
|
|
932
|
+
searchParams: new URLSearchParams(this._url.search),
|
|
933
|
+
response: this.response,
|
|
934
|
+
store: this.store,
|
|
935
|
+
signal: signal ?? this.signal,
|
|
936
|
+
appOptions: this.appOptions,
|
|
937
|
+
next: (event, error) => this.next(event, error)
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
get store() {
|
|
941
|
+
if (!this._store) this._store = Object.create(null);
|
|
942
|
+
return this._store;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/handler/constants.ts
|
|
947
|
+
const HandlerType = {
|
|
948
|
+
CORE: "core",
|
|
949
|
+
ERROR: "error"
|
|
950
|
+
};
|
|
951
|
+
const HandlerSymbol = Symbol.for("Handler");
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/hook/constants.ts
|
|
954
|
+
const HookName = {
|
|
955
|
+
/**
|
|
956
|
+
* Fired at the start of `App.dispatch`, before the pipeline walk.
|
|
957
|
+
* Once per router per request.
|
|
958
|
+
*/
|
|
959
|
+
START: "start",
|
|
960
|
+
/**
|
|
961
|
+
* Fired at the end of `App.dispatch`, after the pipeline walk
|
|
962
|
+
* (and OPTIONS auto-Allow synthesis) completes. Once per router per
|
|
963
|
+
* request.
|
|
964
|
+
*/
|
|
965
|
+
END: "end",
|
|
966
|
+
ERROR: "error",
|
|
967
|
+
CHILD_MATCH: "childMatch",
|
|
968
|
+
CHILD_DISPATCH_BEFORE: "childDispatchBefore",
|
|
969
|
+
CHILD_DISPATCH_AFTER: "childDispatchAfter"
|
|
970
|
+
};
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/hook/module.ts
|
|
973
|
+
var Hooks = class Hooks {
|
|
974
|
+
items;
|
|
975
|
+
/**
|
|
976
|
+
* Derived bit: `true` iff at least one entry exists across all
|
|
977
|
+
* hook names. Maintained on every `addListener` / `removeListener`
|
|
978
|
+
* so the dispatch hot path can short-circuit on a single boolean
|
|
979
|
+
* read rather than per-name lookup. Apps that never register a
|
|
980
|
+
* hook (the common case) pay one boolean check per request
|
|
981
|
+
* instead of a property access per pipeline step.
|
|
982
|
+
*/
|
|
983
|
+
_hasAny;
|
|
984
|
+
constructor() {
|
|
985
|
+
this.items = {};
|
|
986
|
+
this._hasAny = false;
|
|
987
|
+
}
|
|
988
|
+
hasAny() {
|
|
989
|
+
return this._hasAny;
|
|
990
|
+
}
|
|
991
|
+
hasListeners(name) {
|
|
992
|
+
if (!this._hasAny) return false;
|
|
993
|
+
return this.items[name] !== void 0;
|
|
994
|
+
}
|
|
995
|
+
addListener(name, fn, priority = 0) {
|
|
996
|
+
this.items[name] = this.items[name] || [];
|
|
997
|
+
const entry = {
|
|
998
|
+
fn,
|
|
999
|
+
priority
|
|
1000
|
+
};
|
|
1001
|
+
let i = 0;
|
|
1002
|
+
while (i < this.items[name].length && this.items[name][i].priority >= priority) i++;
|
|
1003
|
+
this.items[name].splice(i, 0, entry);
|
|
1004
|
+
this._hasAny = true;
|
|
1005
|
+
return () => {
|
|
1006
|
+
this.removeListener(name, fn);
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
removeListener(name, fn) {
|
|
1010
|
+
if (!this.items[name]) return;
|
|
1011
|
+
if (typeof fn === "undefined") {
|
|
1012
|
+
delete this.items[name];
|
|
1013
|
+
this.recomputeHasAny();
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof fn === "function") {
|
|
1017
|
+
const index = this.items[name].findIndex((entry) => entry.fn === fn);
|
|
1018
|
+
if (index !== -1) this.items[name].splice(index, 1);
|
|
1019
|
+
}
|
|
1020
|
+
if (this.items[name].length === 0) delete this.items[name];
|
|
1021
|
+
this.recomputeHasAny();
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Recompute `_hasAny` from the current `items` map. O(k) where k
|
|
1025
|
+
* is the number of distinct hook names ever registered (≤ ~6) —
|
|
1026
|
+
* effectively O(1). Called from `removeListener` so the fast-path
|
|
1027
|
+
* flag stays in sync with registration state.
|
|
1028
|
+
*/
|
|
1029
|
+
recomputeHasAny() {
|
|
1030
|
+
for (const name in this.items) if (this.items[name] && this.items[name].length > 0) {
|
|
1031
|
+
this._hasAny = true;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
this._hasAny = false;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Create a new `Hooks` instance seeded with the same listeners as this
|
|
1038
|
+
* one.
|
|
1039
|
+
*
|
|
1040
|
+
* Listener functions are shared by reference; priority and ordering are
|
|
1041
|
+
* preserved. Future mutations on the returned instance do not affect this
|
|
1042
|
+
* one (and vice versa).
|
|
1043
|
+
*/
|
|
1044
|
+
clone() {
|
|
1045
|
+
const next = new Hooks();
|
|
1046
|
+
const names = Object.keys(this.items);
|
|
1047
|
+
for (const name of names) {
|
|
1048
|
+
const entries = this.items[name];
|
|
1049
|
+
for (const entry of entries) next.addListener(name, entry.fn, entry.priority);
|
|
1050
|
+
}
|
|
1051
|
+
return next;
|
|
1052
|
+
}
|
|
1053
|
+
async trigger(name, event) {
|
|
1054
|
+
if (!this.items[name] || this.items[name].length === 0) return;
|
|
1055
|
+
try {
|
|
1056
|
+
for (let i = 0; i < this.items[name].length; i++) {
|
|
1057
|
+
const { fn } = this.items[name][i];
|
|
1058
|
+
await this.triggerListener(name, event, fn);
|
|
1059
|
+
if (event.dispatched) {
|
|
1060
|
+
if (event.error) event.error = void 0;
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
} catch (e) {
|
|
1065
|
+
if (!event.error) event.error = createError(e);
|
|
1066
|
+
if (!this.isErrorListenerHook(name)) {
|
|
1067
|
+
await this.trigger(HookName.ERROR, event);
|
|
1068
|
+
if (event.dispatched) {
|
|
1069
|
+
if (event.error) event.error = void 0;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
triggerListener(name, event, listener) {
|
|
1075
|
+
if (this.isErrorListenerHook(name)) {
|
|
1076
|
+
if (event.error) return listener(event);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
return listener(event);
|
|
1080
|
+
}
|
|
1081
|
+
isErrorListenerHook(input) {
|
|
1082
|
+
return input === HookName.ERROR;
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/handler/module.ts
|
|
1087
|
+
var Handler = class {
|
|
1088
|
+
config;
|
|
1089
|
+
hooks;
|
|
1090
|
+
method;
|
|
1091
|
+
constructor(handler) {
|
|
1092
|
+
this.config = handler;
|
|
1093
|
+
this.hooks = new Hooks();
|
|
1094
|
+
this.mountHooks();
|
|
1095
|
+
if (typeof handler.path === "string") this.config.path = withLeadingSlash(handler.path);
|
|
1096
|
+
this.method = this.config.method ? toMethodName(this.config.method) : void 0;
|
|
1097
|
+
markInstanceof(this, HandlerSymbol);
|
|
1098
|
+
}
|
|
1099
|
+
get type() {
|
|
1100
|
+
return this.config.type;
|
|
1101
|
+
}
|
|
1102
|
+
get path() {
|
|
1103
|
+
return this.config.path;
|
|
1104
|
+
}
|
|
1105
|
+
async dispatch(event) {
|
|
1106
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_BEFORE)) {
|
|
1107
|
+
await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, event);
|
|
1108
|
+
if (event.dispatched) return;
|
|
1109
|
+
}
|
|
1110
|
+
let response;
|
|
1111
|
+
try {
|
|
1112
|
+
const effectiveTimeout = this.resolveTimeout(event.appOptions);
|
|
1113
|
+
let childController;
|
|
1114
|
+
let cleanupParentListener;
|
|
1115
|
+
if (effectiveTimeout) {
|
|
1116
|
+
const parentSignal = event.signal;
|
|
1117
|
+
childController = new AbortController();
|
|
1118
|
+
if (parentSignal.aborted) childController.abort(parentSignal.reason);
|
|
1119
|
+
else {
|
|
1120
|
+
const onAbort = () => childController.abort(parentSignal.reason);
|
|
1121
|
+
parentSignal.addEventListener("abort", onAbort, { once: true });
|
|
1122
|
+
cleanupParentListener = () => parentSignal.removeEventListener("abort", onAbort);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const handlerEvent = childController ? event.build(childController.signal) : event.build();
|
|
1126
|
+
let result;
|
|
1127
|
+
try {
|
|
1128
|
+
let skipFn = false;
|
|
1129
|
+
let invocation;
|
|
1130
|
+
if (this.config.type === HandlerType.ERROR) if (event.error) {
|
|
1131
|
+
const { fn } = this.config;
|
|
1132
|
+
invocation = fn(event.error, handlerEvent);
|
|
1133
|
+
} else skipFn = true;
|
|
1134
|
+
else {
|
|
1135
|
+
const { fn } = this.config;
|
|
1136
|
+
invocation = fn(handlerEvent);
|
|
1137
|
+
}
|
|
1138
|
+
if (skipFn) {} else if (effectiveTimeout) result = await this.executeWithTimeout(() => this.resolveHandlerResult(invocation, handlerEvent), effectiveTimeout, childController);
|
|
1139
|
+
else if (isPromise(invocation)) {
|
|
1140
|
+
const awaited = await invocation;
|
|
1141
|
+
result = typeof awaited === "undefined" ? await this.resolveHandlerResult(void 0, handlerEvent) : awaited;
|
|
1142
|
+
} else if (typeof invocation === "undefined") result = await this.resolveHandlerResult(void 0, handlerEvent);
|
|
1143
|
+
else result = invocation;
|
|
1144
|
+
} finally {
|
|
1145
|
+
if (cleanupParentListener) cleanupParentListener();
|
|
1146
|
+
}
|
|
1147
|
+
const toResp = toResponse(result, handlerEvent);
|
|
1148
|
+
response = isPromise(toResp) ? await toResp : toResp;
|
|
1149
|
+
if (response) {
|
|
1150
|
+
event.dispatched = true;
|
|
1151
|
+
if (this.config.type === HandlerType.ERROR && event.error) event.error = void 0;
|
|
1152
|
+
}
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
event.error = isError(e) ? e : createError(e);
|
|
1155
|
+
if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
|
|
1156
|
+
if (event.dispatched) event.error = void 0;
|
|
1157
|
+
else throw event.error;
|
|
1158
|
+
}
|
|
1159
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_AFTER)) await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, event);
|
|
1160
|
+
return response;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Resolve a handler's return value into the final value handed to `toResponse`.
|
|
1164
|
+
*
|
|
1165
|
+
* Contract:
|
|
1166
|
+
* - non-undefined value → return as-is (becomes the response)
|
|
1167
|
+
* - `undefined` + `event.next()` was called → forward downstream result
|
|
1168
|
+
* - `undefined` + `event.next()` not yet called → wait until either `next()` is
|
|
1169
|
+
* invoked (e.g. from an async callback) or `signal` aborts. A global or
|
|
1170
|
+
* per-handler timeout aborts `signal` and surfaces as 408. With no timeout
|
|
1171
|
+
* configured and no eventual `next()` call, the request hangs by design.
|
|
1172
|
+
*/
|
|
1173
|
+
async resolveHandlerResult(invocation, handlerEvent) {
|
|
1174
|
+
const value = await invocation;
|
|
1175
|
+
if (typeof value !== "undefined") return value;
|
|
1176
|
+
if (handlerEvent.nextCalled) return handlerEvent.nextResult;
|
|
1177
|
+
const { signal } = handlerEvent;
|
|
1178
|
+
if (signal.aborted) throw createError({
|
|
1179
|
+
status: 408,
|
|
1180
|
+
message: "Request Timeout"
|
|
1181
|
+
});
|
|
1182
|
+
return new Promise((resolve, reject) => {
|
|
1183
|
+
const onAbort = () => {
|
|
1184
|
+
signal.removeEventListener("abort", onAbort);
|
|
1185
|
+
reject(createError({
|
|
1186
|
+
status: 408,
|
|
1187
|
+
message: "Request Timeout"
|
|
1188
|
+
}));
|
|
1189
|
+
};
|
|
1190
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1191
|
+
handlerEvent.whenNextCalled().then(() => {
|
|
1192
|
+
signal.removeEventListener("abort", onAbort);
|
|
1193
|
+
resolve(handlerEvent.nextResult);
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
async executeWithTimeout(fn, effectiveTimeout, controller) {
|
|
1198
|
+
if (!effectiveTimeout) return fn();
|
|
1199
|
+
let timerId;
|
|
1200
|
+
try {
|
|
1201
|
+
return await Promise.race([fn(), new Promise((_, reject) => {
|
|
1202
|
+
timerId = setTimeout(() => {
|
|
1203
|
+
if (controller) controller.abort();
|
|
1204
|
+
reject(createError({
|
|
1205
|
+
status: 408,
|
|
1206
|
+
message: "Request Timeout"
|
|
1207
|
+
}));
|
|
1208
|
+
}, effectiveTimeout);
|
|
1209
|
+
})]);
|
|
1210
|
+
} finally {
|
|
1211
|
+
clearTimeout(timerId);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
resolveTimeout(appOptions) {
|
|
1215
|
+
const routerDefault = appOptions.handlerTimeout;
|
|
1216
|
+
const handlerOverride = this.config.timeout;
|
|
1217
|
+
if (!routerDefault && !handlerOverride) return;
|
|
1218
|
+
if (!routerDefault) return handlerOverride;
|
|
1219
|
+
if (!handlerOverride) return routerDefault;
|
|
1220
|
+
if (appOptions.handlerTimeoutOverridable) return handlerOverride;
|
|
1221
|
+
return Math.min(routerDefault, handlerOverride);
|
|
1222
|
+
}
|
|
1223
|
+
mountHooks() {
|
|
1224
|
+
if (this.config.onBefore) this.hooks.addListener(HookName.CHILD_DISPATCH_BEFORE, this.config.onBefore);
|
|
1225
|
+
if (this.config.onAfter) this.hooks.addListener(HookName.CHILD_DISPATCH_AFTER, this.config.onAfter);
|
|
1226
|
+
if (this.config.onError) this.hooks.addListener(HookName.ERROR, this.config.onError);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
//#endregion
|
|
1230
|
+
//#region src/handler/core/define.ts
|
|
1231
|
+
function defineCoreHandler(input) {
|
|
1232
|
+
if (typeof input === "function") return new Handler({
|
|
1233
|
+
type: HandlerType.CORE,
|
|
1234
|
+
fn: input
|
|
1235
|
+
});
|
|
1236
|
+
return new Handler({
|
|
1237
|
+
type: HandlerType.CORE,
|
|
1238
|
+
...input
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/handler/error/define.ts
|
|
1243
|
+
function defineErrorHandler(input) {
|
|
1244
|
+
if (typeof input === "function") return new Handler({
|
|
1245
|
+
type: HandlerType.ERROR,
|
|
1246
|
+
fn: input
|
|
1247
|
+
});
|
|
1248
|
+
return new Handler({
|
|
1249
|
+
type: HandlerType.ERROR,
|
|
1250
|
+
...input
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/handler/adapters/node/define.ts
|
|
1255
|
+
const kHandled = /* @__PURE__ */ Symbol("handled");
|
|
1256
|
+
function callHandler(handler, req, res) {
|
|
1257
|
+
return new Promise((resolve, reject) => {
|
|
1258
|
+
let settled = false;
|
|
1259
|
+
const onClose = () => settle(kHandled);
|
|
1260
|
+
const onFinish = () => settle(kHandled);
|
|
1261
|
+
const onError = (error) => fail(error);
|
|
1262
|
+
function cleanup() {
|
|
1263
|
+
res.removeListener("close", onClose);
|
|
1264
|
+
res.removeListener("finish", onFinish);
|
|
1265
|
+
res.removeListener("error", onError);
|
|
1266
|
+
}
|
|
1267
|
+
function settle(value) {
|
|
1268
|
+
if (settled) return;
|
|
1269
|
+
settled = true;
|
|
1270
|
+
cleanup();
|
|
1271
|
+
resolve(value);
|
|
1272
|
+
}
|
|
1273
|
+
function fail(error) {
|
|
1274
|
+
if (settled) return;
|
|
1275
|
+
settled = true;
|
|
1276
|
+
cleanup();
|
|
1277
|
+
reject(error);
|
|
1278
|
+
}
|
|
1279
|
+
res.once("close", onClose);
|
|
1280
|
+
res.once("finish", onFinish);
|
|
1281
|
+
res.once("error", onError);
|
|
1282
|
+
try {
|
|
1283
|
+
Promise.resolve(handler(req, res)).then(() => settle(kHandled)).catch(fail);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
fail(error);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
function callMiddleware(handler, req, res) {
|
|
1290
|
+
return new Promise((resolve, reject) => {
|
|
1291
|
+
let settled = false;
|
|
1292
|
+
const onClose = () => settle(kHandled);
|
|
1293
|
+
const onFinish = () => settle(kHandled);
|
|
1294
|
+
const onError = (error) => fail(error);
|
|
1295
|
+
function cleanup() {
|
|
1296
|
+
res.removeListener("close", onClose);
|
|
1297
|
+
res.removeListener("finish", onFinish);
|
|
1298
|
+
res.removeListener("error", onError);
|
|
1299
|
+
}
|
|
1300
|
+
function settle(value) {
|
|
1301
|
+
if (settled) return;
|
|
1302
|
+
settled = true;
|
|
1303
|
+
cleanup();
|
|
1304
|
+
resolve(value);
|
|
1305
|
+
}
|
|
1306
|
+
function fail(error) {
|
|
1307
|
+
if (settled) return;
|
|
1308
|
+
settled = true;
|
|
1309
|
+
cleanup();
|
|
1310
|
+
reject(error);
|
|
1311
|
+
}
|
|
1312
|
+
res.once("close", onClose);
|
|
1313
|
+
res.once("finish", onFinish);
|
|
1314
|
+
res.once("error", onError);
|
|
1315
|
+
try {
|
|
1316
|
+
Promise.resolve(handler(req, res, (error) => {
|
|
1317
|
+
if (error) fail(error);
|
|
1318
|
+
else settle(res.writableEnded || res.destroyed ? kHandled : void 0);
|
|
1319
|
+
})).catch(fail);
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
fail(error);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
function createNodeBridge(handler, isMiddleware) {
|
|
1326
|
+
if (typeof handler !== "function") throw new AppError("fromNodeHandler/fromNodeMiddleware expects a function.");
|
|
1327
|
+
return defineCoreHandler({ fn: (async (event) => {
|
|
1328
|
+
const node = event.request.runtime?.node;
|
|
1329
|
+
if (!node?.req || !node?.res) throw new AppError("fromNodeHandler/fromNodeMiddleware requires a Node.js runtime.");
|
|
1330
|
+
const req = node.req;
|
|
1331
|
+
const res = node.res;
|
|
1332
|
+
if ((isMiddleware ? await callMiddleware(handler, req, res) : await callHandler(handler, req, res)) === kHandled) return null;
|
|
1333
|
+
return event.next();
|
|
1334
|
+
}) });
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Wraps a Node.js `(req, res)` handler for use in the routup pipeline.
|
|
1338
|
+
*
|
|
1339
|
+
* @example
|
|
1340
|
+
* ```typescript
|
|
1341
|
+
* import { fromNodeHandler } from 'routup/node';
|
|
1342
|
+
*
|
|
1343
|
+
* router.use(fromNodeHandler((req, res) => {
|
|
1344
|
+
* res.end('Hello');
|
|
1345
|
+
* }));
|
|
1346
|
+
* ```
|
|
1347
|
+
*/
|
|
1348
|
+
function fromNodeHandler(handler) {
|
|
1349
|
+
return createNodeBridge(handler, false);
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Wraps a Node.js `(req, res, next)` middleware for use in the routup pipeline.
|
|
1353
|
+
*
|
|
1354
|
+
* @example
|
|
1355
|
+
* ```typescript
|
|
1356
|
+
* import cors from 'cors';
|
|
1357
|
+
* import { fromNodeMiddleware } from 'routup/node';
|
|
1358
|
+
*
|
|
1359
|
+
* router.use(fromNodeMiddleware(cors()));
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
function fromNodeMiddleware(handler) {
|
|
1363
|
+
return createNodeBridge(handler, true);
|
|
1364
|
+
}
|
|
1365
|
+
//#endregion
|
|
1366
|
+
//#region src/handler/adapters/web/is.ts
|
|
1367
|
+
function isWebHandlerProvider(input) {
|
|
1368
|
+
return isObject(input) && typeof input.fetch === "function";
|
|
1369
|
+
}
|
|
1370
|
+
function isWebHandler(input) {
|
|
1371
|
+
return typeof input === "function";
|
|
1372
|
+
}
|
|
1373
|
+
//#endregion
|
|
1374
|
+
//#region src/handler/adapters/web/define.ts
|
|
1375
|
+
function fromWebHandler(input) {
|
|
1376
|
+
if (isWebHandlerProvider(input)) return fromWebHandler(input.fetch.bind(input));
|
|
1377
|
+
if (typeof input !== "function") throw new AppError("fromWebHandler expects a function or an object with a fetch method.");
|
|
1378
|
+
return defineCoreHandler({ fn: (event) => input(event.request) });
|
|
1379
|
+
}
|
|
1380
|
+
//#endregion
|
|
1381
|
+
//#region src/handler/is.ts
|
|
1382
|
+
function isHandlerOptions(input) {
|
|
1383
|
+
return isObject(input) && typeof input.fn === "function" && typeof input.type === "string";
|
|
1384
|
+
}
|
|
1385
|
+
function isHandler(input) {
|
|
1386
|
+
return hasInstanceof(input, HandlerSymbol);
|
|
1387
|
+
}
|
|
1388
|
+
//#endregion
|
|
1389
|
+
//#region src/handler/utils.ts
|
|
1390
|
+
/**
|
|
1391
|
+
* Match a request method against a handler's bound method.
|
|
1392
|
+
*
|
|
1393
|
+
* - When the handler has no method bound, matches every request method.
|
|
1394
|
+
* - Otherwise matches when the request method is the same.
|
|
1395
|
+
* - HEAD requests additionally match GET handlers.
|
|
1396
|
+
*/
|
|
1397
|
+
function matchHandlerMethod(handlerMethod, requestMethod) {
|
|
1398
|
+
return !handlerMethod || requestMethod === handlerMethod || requestMethod === MethodName.HEAD && handlerMethod === MethodName.GET;
|
|
1399
|
+
}
|
|
1400
|
+
//#endregion
|
|
1401
|
+
//#region src/request/helpers/cache.ts
|
|
1402
|
+
function isRequestCacheable(event, modifiedTime) {
|
|
1403
|
+
const modifiedSince = event.headers.get(HeaderName.IF_MODIFIED_SINCE);
|
|
1404
|
+
if (!modifiedSince) return false;
|
|
1405
|
+
modifiedTime = typeof modifiedTime === "string" ? new Date(modifiedTime) : modifiedTime;
|
|
1406
|
+
const sinceDate = new Date(modifiedSince);
|
|
1407
|
+
if (Number.isNaN(sinceDate.getTime()) || Number.isNaN(modifiedTime.getTime())) return false;
|
|
1408
|
+
return sinceDate >= modifiedTime;
|
|
1409
|
+
}
|
|
1410
|
+
//#endregion
|
|
1411
|
+
//#region src/request/helpers/header-accept-charset.ts
|
|
1412
|
+
function getRequestAcceptableCharsets(event) {
|
|
1413
|
+
return useRequestNegotiator(event).charsets();
|
|
1414
|
+
}
|
|
1415
|
+
function getRequestAcceptableCharset(event, input) {
|
|
1416
|
+
input = input || [];
|
|
1417
|
+
const items = Array.isArray(input) ? input : [input];
|
|
1418
|
+
if (items.length === 0) return getRequestAcceptableCharsets(event).shift();
|
|
1419
|
+
return useRequestNegotiator(event).charsets(items).shift() || void 0;
|
|
1420
|
+
}
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/request/helpers/header-accept-encoding.ts
|
|
1423
|
+
function getRequestAcceptableEncodings(event) {
|
|
1424
|
+
return useRequestNegotiator(event).encodings();
|
|
1425
|
+
}
|
|
1426
|
+
function getRequestAcceptableEncoding(event, input) {
|
|
1427
|
+
input = input || [];
|
|
1428
|
+
const items = Array.isArray(input) ? input : [input];
|
|
1429
|
+
if (items.length === 0) return getRequestAcceptableEncodings(event).shift();
|
|
1430
|
+
return useRequestNegotiator(event).encodings(items).shift() || void 0;
|
|
1431
|
+
}
|
|
1432
|
+
//#endregion
|
|
1433
|
+
//#region src/request/helpers/header-accept-language.ts
|
|
1434
|
+
function getRequestAcceptableLanguages(event) {
|
|
1435
|
+
return useRequestNegotiator(event).languages();
|
|
1436
|
+
}
|
|
1437
|
+
function getRequestAcceptableLanguage(event, input) {
|
|
1438
|
+
input = input || [];
|
|
1439
|
+
const items = Array.isArray(input) ? input : [input];
|
|
1440
|
+
if (items.length === 0) return getRequestAcceptableLanguages(event).shift();
|
|
1441
|
+
return useRequestNegotiator(event).languages(items).shift() || void 0;
|
|
1442
|
+
}
|
|
1443
|
+
//#endregion
|
|
1444
|
+
//#region src/request/helpers/header-content-type.ts
|
|
1445
|
+
function matchRequestContentType(event, contentType) {
|
|
1446
|
+
const header = getRequestHeader(event, HeaderName.CONTENT_TYPE);
|
|
1447
|
+
if (!header) return true;
|
|
1448
|
+
return header.split(";")[0].trim() === getMimeType(contentType);
|
|
1449
|
+
}
|
|
1450
|
+
//#endregion
|
|
1451
|
+
//#region src/request/helpers/hostname.ts
|
|
1452
|
+
function getRequestHostName(event, options = {}) {
|
|
1453
|
+
let trustProxy;
|
|
1454
|
+
if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
|
|
1455
|
+
else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
|
|
1456
|
+
let hostname = event.headers.get(HeaderName.X_FORWARDED_HOST);
|
|
1457
|
+
if (!hostname || !event.request.ip || !trustProxy(event.request.ip, 0)) hostname = event.headers.get(HeaderName.HOST);
|
|
1458
|
+
else if (hostname && hostname.includes(",")) hostname = hostname.substring(0, hostname.indexOf(",")).trimEnd();
|
|
1459
|
+
if (!hostname) return;
|
|
1460
|
+
const offset = hostname[0] === "[" ? hostname.indexOf("]") + 1 : 0;
|
|
1461
|
+
const index = hostname.indexOf(":", offset);
|
|
1462
|
+
const result = index !== -1 ? hostname.substring(0, index) : hostname;
|
|
1463
|
+
if (/[\x00-\x1F\x7F\s/@\\]/.test(result)) return;
|
|
1464
|
+
return result;
|
|
1465
|
+
}
|
|
1466
|
+
//#endregion
|
|
1467
|
+
//#region src/request/helpers/ip.ts
|
|
1468
|
+
/**
|
|
1469
|
+
* Get the client IP address from the request.
|
|
1470
|
+
*
|
|
1471
|
+
* When `trustProxy` is configured, walks the `X-Forwarded-For` chain
|
|
1472
|
+
* and returns the rightmost untrusted address (the actual client IP).
|
|
1473
|
+
* Falls back to `event.request.ip` (the direct connection IP).
|
|
1474
|
+
*/
|
|
1475
|
+
function getRequestIP(event, options = {}) {
|
|
1476
|
+
let trustProxy;
|
|
1477
|
+
if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
|
|
1478
|
+
else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
|
|
1479
|
+
const socketAddr = event.request.ip;
|
|
1480
|
+
if (!socketAddr) return;
|
|
1481
|
+
const forwarded = event.headers.get(HeaderName.X_FORWARDED_FOR);
|
|
1482
|
+
const addrs = [socketAddr];
|
|
1483
|
+
if (forwarded) {
|
|
1484
|
+
const parts = forwarded.split(",");
|
|
1485
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
1486
|
+
const addr = parts[i].trim();
|
|
1487
|
+
if (addr) addrs.push(addr);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
for (let i = 0; i < addrs.length - 1; i++) if (!trustProxy(addrs[i], i)) return addrs[i];
|
|
1491
|
+
return addrs[addrs.length - 1];
|
|
1492
|
+
}
|
|
1493
|
+
//#endregion
|
|
1494
|
+
//#region src/request/helpers/protocol.ts
|
|
1495
|
+
function getRequestProtocol(event, options = {}) {
|
|
1496
|
+
let trustProxy;
|
|
1497
|
+
if (typeof options.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(options.trustProxy);
|
|
1498
|
+
else trustProxy = event.appOptions.trustProxy ?? DEFAULT_TRUST_PROXY;
|
|
1499
|
+
let protocol;
|
|
1500
|
+
try {
|
|
1501
|
+
if (new URL(event.request.url).protocol === "https:") protocol = "https";
|
|
1502
|
+
else protocol = "http";
|
|
1503
|
+
} catch {
|
|
1504
|
+
protocol = options.default || "http";
|
|
1505
|
+
}
|
|
1506
|
+
if (!event.request.ip || !trustProxy(event.request.ip, 0)) return protocol;
|
|
1507
|
+
const header = event.headers.get(HeaderName.X_FORWARDED_PROTO);
|
|
1508
|
+
if (!header) return protocol;
|
|
1509
|
+
const index = header.indexOf(",");
|
|
1510
|
+
const forwarded = index !== -1 ? header.substring(0, index).trim().toLowerCase() : header.trim().toLowerCase();
|
|
1511
|
+
if (forwarded === "http" || forwarded === "https") return forwarded;
|
|
1512
|
+
return protocol;
|
|
1513
|
+
}
|
|
1514
|
+
//#endregion
|
|
1515
|
+
//#region src/path/matcher.ts
|
|
1516
|
+
function decodeParam(val) {
|
|
1517
|
+
/* istanbul ignore next */
|
|
1518
|
+
if (typeof val !== "string" || val.length === 0) return val;
|
|
1519
|
+
try {
|
|
1520
|
+
return decodeURIComponent(val);
|
|
1521
|
+
} catch {
|
|
1522
|
+
return val;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
var PathMatcher = class {
|
|
1526
|
+
path;
|
|
1527
|
+
regexp;
|
|
1528
|
+
regexpKeys = [];
|
|
1529
|
+
regexpOptions;
|
|
1530
|
+
constructor(path, options) {
|
|
1531
|
+
this.path = path;
|
|
1532
|
+
this.regexpOptions = options || {};
|
|
1533
|
+
const regexp = pathToRegexp(path, options);
|
|
1534
|
+
this.regexp = regexp.regexp;
|
|
1535
|
+
this.regexpKeys = regexp.keys;
|
|
1536
|
+
}
|
|
1537
|
+
test(path) {
|
|
1538
|
+
return this.regexp.test(path);
|
|
1539
|
+
}
|
|
1540
|
+
exec(path) {
|
|
1541
|
+
if (this.path === "/" && this.regexpOptions.end === false) return {
|
|
1542
|
+
path: "/",
|
|
1543
|
+
params: Object.create(null)
|
|
1544
|
+
};
|
|
1545
|
+
const match = this.regexp.exec(path);
|
|
1546
|
+
if (!match) return;
|
|
1547
|
+
const params = Object.create(null);
|
|
1548
|
+
for (let i = 1; i < match.length; i++) {
|
|
1549
|
+
const key = this.regexpKeys[i - 1];
|
|
1550
|
+
if (!key) continue;
|
|
1551
|
+
const prop = key.name;
|
|
1552
|
+
const val = decodeParam(match[i]);
|
|
1553
|
+
if (typeof val !== "undefined") params[prop] = val;
|
|
1554
|
+
}
|
|
1555
|
+
return {
|
|
1556
|
+
path: match[0],
|
|
1557
|
+
params
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/path/utils.ts
|
|
1563
|
+
function isPath(input) {
|
|
1564
|
+
return typeof input === "string";
|
|
1565
|
+
}
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region src/plugin/error/constants.ts
|
|
1568
|
+
const PluginErrorCode = {
|
|
1569
|
+
PLUGIN: "PLUGIN",
|
|
1570
|
+
NOT_INSTALLED: "PLUGIN_NOT_INSTALLED",
|
|
1571
|
+
ALREADY_INSTALLED: "PLUGIN_ALREADY_INSTALLED",
|
|
1572
|
+
INSTALL: "PLUGIN_INSTALL"
|
|
1573
|
+
};
|
|
1574
|
+
//#endregion
|
|
1575
|
+
//#region src/plugin/error/is.ts
|
|
1576
|
+
const PLUGIN_ERROR_CODES = new Set(Object.values(PluginErrorCode));
|
|
1577
|
+
function isPluginError(input) {
|
|
1578
|
+
if (!isError(input)) return false;
|
|
1579
|
+
return PLUGIN_ERROR_CODES.has(input.code);
|
|
1580
|
+
}
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region src/plugin/error/module.ts
|
|
1583
|
+
var PluginError = class extends AppError {
|
|
1584
|
+
constructor(input = {}) {
|
|
1585
|
+
const options = typeof input === "string" ? { message: input } : { ...input };
|
|
1586
|
+
if (!("code" in options) || !options.code) options.code = PluginErrorCode.PLUGIN;
|
|
1587
|
+
super(options);
|
|
1588
|
+
this.name = "PluginError";
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region src/plugin/error/sub/already-installed.ts
|
|
1593
|
+
var PluginAlreadyInstalledError = class extends PluginError {
|
|
1594
|
+
pluginName;
|
|
1595
|
+
constructor(pluginName) {
|
|
1596
|
+
super({
|
|
1597
|
+
message: `Plugin "${pluginName}" is already installed on this router.`,
|
|
1598
|
+
code: PluginErrorCode.ALREADY_INSTALLED
|
|
1599
|
+
});
|
|
1600
|
+
this.name = "PluginAlreadyInstalledError";
|
|
1601
|
+
this.pluginName = pluginName;
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
//#endregion
|
|
1605
|
+
//#region src/plugin/error/sub/install.ts
|
|
1606
|
+
var PluginInstallError = class extends PluginError {
|
|
1607
|
+
pluginName;
|
|
1608
|
+
constructor(pluginName, cause) {
|
|
1609
|
+
super({
|
|
1610
|
+
message: `Failed to install plugin "${pluginName}".`,
|
|
1611
|
+
code: PluginErrorCode.INSTALL,
|
|
1612
|
+
cause
|
|
1613
|
+
});
|
|
1614
|
+
this.name = "PluginInstallError";
|
|
1615
|
+
this.pluginName = pluginName;
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
//#endregion
|
|
1619
|
+
//#region src/plugin/error/sub/not-installed.ts
|
|
1620
|
+
var PluginNotInstalledError = class extends PluginError {
|
|
1621
|
+
pluginName;
|
|
1622
|
+
helperName;
|
|
1623
|
+
constructor(pluginName, helperName) {
|
|
1624
|
+
super({
|
|
1625
|
+
message: `${helperName}() requires the "${pluginName}" plugin. Register it with: router.use(${pluginName}())`,
|
|
1626
|
+
code: PluginErrorCode.NOT_INSTALLED
|
|
1627
|
+
});
|
|
1628
|
+
this.name = "PluginNotInstalledError";
|
|
1629
|
+
this.pluginName = pluginName;
|
|
1630
|
+
this.helperName = helperName;
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/plugin/is.ts
|
|
1635
|
+
function isPlugin(input) {
|
|
1636
|
+
if (!isObject(input)) return false;
|
|
1637
|
+
if (typeof input.name !== "undefined" && typeof input.name !== "string") return false;
|
|
1638
|
+
return typeof input.install === "function" && input.install.length === 1;
|
|
1639
|
+
}
|
|
1640
|
+
//#endregion
|
|
1641
|
+
//#region src/router/utils.ts
|
|
1642
|
+
/**
|
|
1643
|
+
* Build a path-to-regexp-backed `PathMatcher` for the route's mount
|
|
1644
|
+
* path, applying the exact-vs-prefix convention every router should
|
|
1645
|
+
* agree on:
|
|
1646
|
+
*
|
|
1647
|
+
* - `route.method !== undefined` → exact match (method-bound route)
|
|
1648
|
+
* - `route.method === undefined` → prefix match (middleware / nested
|
|
1649
|
+
* app)
|
|
1650
|
+
*
|
|
1651
|
+
* Returns `undefined` when the route has no mount path — middleware
|
|
1652
|
+
* registered without a path matches every request.
|
|
1653
|
+
*
|
|
1654
|
+
* Routers are free to ignore this helper and build their own match
|
|
1655
|
+
* mechanism (radix tree, single aggregated regex, etc.) — it's
|
|
1656
|
+
* provided as a convenience for routers that want path-to-regexp
|
|
1657
|
+
* semantics with minimal boilerplate.
|
|
1658
|
+
*/
|
|
1659
|
+
function buildRoutePathMatcher(route) {
|
|
1660
|
+
if (typeof route.path === "undefined") return;
|
|
1661
|
+
const end = typeof route.method !== "undefined";
|
|
1662
|
+
if (!end && route.path === "/") return;
|
|
1663
|
+
return new PathMatcher(route.path, { end });
|
|
1664
|
+
}
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region src/router/linear/module.ts
|
|
1667
|
+
/**
|
|
1668
|
+
* Default router — walks registered routes linearly per request and
|
|
1669
|
+
* runs each route's mount-level matcher (built via `buildRoutePathMatcher`,
|
|
1670
|
+
* path-to-regexp-backed). Routes without a mount path (mount-less
|
|
1671
|
+
* middleware / nested apps registered via `.use(handler)`) match every
|
|
1672
|
+
* request directly — there is no per-route `matchPath()` fallback.
|
|
1673
|
+
*
|
|
1674
|
+
* Behaviour-preserving wrapper around the previous in-line stack walk
|
|
1675
|
+
* in `executePipelineStepLookup`. The matcher allocations live here
|
|
1676
|
+
* (not on the registered route), so routers using a different matching
|
|
1677
|
+
* strategy (radix tree, aggregated regex, …) can ignore this file
|
|
1678
|
+
* entirely.
|
|
1679
|
+
*
|
|
1680
|
+
* Optional per-router lookup cache: pass an `ICache` via
|
|
1681
|
+
* `BaseRouterOptions.cache` to skip the linear walk on repeated
|
|
1682
|
+
* requests for the same path. Default is no caching.
|
|
1683
|
+
*/
|
|
1684
|
+
var LinearRouter = class LinearRouter {
|
|
1685
|
+
_routes;
|
|
1686
|
+
_matchers;
|
|
1687
|
+
cache;
|
|
1688
|
+
constructor(options = {}) {
|
|
1689
|
+
this._routes = [];
|
|
1690
|
+
this._matchers = [];
|
|
1691
|
+
this.cache = options.cache;
|
|
1692
|
+
}
|
|
1693
|
+
add(route) {
|
|
1694
|
+
this._routes.push(route);
|
|
1695
|
+
this._matchers.push(buildRoutePathMatcher(route));
|
|
1696
|
+
this.cache?.clear();
|
|
1697
|
+
}
|
|
1698
|
+
lookup(path, _method) {
|
|
1699
|
+
const cached = this.cache?.get(path);
|
|
1700
|
+
if (typeof cached !== "undefined") return cached;
|
|
1701
|
+
const matches = [];
|
|
1702
|
+
for (let i = 0; i < this._routes.length; i++) {
|
|
1703
|
+
const route = this._routes[i];
|
|
1704
|
+
const matcher = this._matchers[i];
|
|
1705
|
+
if (matcher) {
|
|
1706
|
+
const output = matcher.exec(path);
|
|
1707
|
+
if (typeof output === "undefined") continue;
|
|
1708
|
+
matches.push({
|
|
1709
|
+
route,
|
|
1710
|
+
index: i,
|
|
1711
|
+
params: output.params,
|
|
1712
|
+
path: output.path
|
|
1713
|
+
});
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
matches.push({
|
|
1717
|
+
route,
|
|
1718
|
+
index: i,
|
|
1719
|
+
params: Object.create(null)
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
this.cache?.set(path, matches);
|
|
1723
|
+
return matches;
|
|
1724
|
+
}
|
|
1725
|
+
clone() {
|
|
1726
|
+
return new LinearRouter({ cache: this.cache?.clone() });
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
//#endregion
|
|
1730
|
+
//#region src/router/trie/parser.ts
|
|
1731
|
+
/**
|
|
1732
|
+
* Trie-native path parser.
|
|
1733
|
+
*
|
|
1734
|
+
* Replaces the call-out to `path-to-regexp` for the syntax surface
|
|
1735
|
+
* the trie advertises:
|
|
1736
|
+
*
|
|
1737
|
+
* - Static segments: `users`, `v1`
|
|
1738
|
+
* - Named params: `:id`, `:slug`
|
|
1739
|
+
* - Optional params: `:id?` (T2 — expanded to two variants)
|
|
1740
|
+
* - Optional groups: `{...}` (T2 — expanded to two variants)
|
|
1741
|
+
* - Bare splat: `*` (matches the rest of the path)
|
|
1742
|
+
* - Named splat: `*rest`
|
|
1743
|
+
*
|
|
1744
|
+
* Returns a list of `Segment[]` *variants* — one path string can
|
|
1745
|
+
* expand into multiple route variants when it contains optional
|
|
1746
|
+
* markers. The trie inserts every variant with the same registration
|
|
1747
|
+
* `index` so they dedupe naturally on the candidate list.
|
|
1748
|
+
*
|
|
1749
|
+
* Returns `null` when the path uses syntax outside this surface
|
|
1750
|
+
* (compound segments `/files/:n.ext`, escape sequences `\:`, regex
|
|
1751
|
+
* constraints, splat-not-last, …). The caller falls back to the
|
|
1752
|
+
* universal bucket so correctness is preserved via path-to-regexp.
|
|
1753
|
+
*
|
|
1754
|
+
* Variant cap: nested optional groups expand combinatorially. The
|
|
1755
|
+
* parser caps the variant count at `MAX_VARIANTS` and falls back to
|
|
1756
|
+
* the universal bucket above that — registration-time explosion
|
|
1757
|
+
* isn't worth the lookup-time win on degenerate paths.
|
|
1758
|
+
*/
|
|
1759
|
+
const MAX_VARIANTS = 16;
|
|
1760
|
+
const PARAM_NAME = /^[a-zA-Z_]\w*$/;
|
|
1761
|
+
/**
|
|
1762
|
+
* Tokenize a single path segment (the substring between two `/`).
|
|
1763
|
+
* Returns `null` if the segment uses syntax we don't handle.
|
|
1764
|
+
*
|
|
1765
|
+
* Each segment must yield exactly one token — compound segments
|
|
1766
|
+
* (`/files/:n.ext`, `/users-:id`) produce multiple tokens and so
|
|
1767
|
+
* trip this check, falling back to the universal bucket.
|
|
1768
|
+
*/
|
|
1769
|
+
function tokenizeSegment(segment) {
|
|
1770
|
+
if (segment === "") return null;
|
|
1771
|
+
if (segment === "*") return {
|
|
1772
|
+
kind: "splat",
|
|
1773
|
+
name: "*"
|
|
1774
|
+
};
|
|
1775
|
+
if (segment.charAt(0) === "*") {
|
|
1776
|
+
const rest = segment.slice(1);
|
|
1777
|
+
if (PARAM_NAME.test(rest)) return {
|
|
1778
|
+
kind: "splat",
|
|
1779
|
+
name: rest
|
|
1780
|
+
};
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
if (segment.charAt(0) === ":") {
|
|
1784
|
+
const optional = segment.charAt(segment.length - 1) === "?";
|
|
1785
|
+
const nameRaw = optional ? segment.slice(1, -1) : segment.slice(1);
|
|
1786
|
+
if (PARAM_NAME.test(nameRaw)) return {
|
|
1787
|
+
kind: "param",
|
|
1788
|
+
name: nameRaw,
|
|
1789
|
+
optional
|
|
1790
|
+
};
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
if (/^[a-zA-Z0-9_\-.~%]+$/.test(segment)) return {
|
|
1794
|
+
kind: "literal",
|
|
1795
|
+
value: segment
|
|
1796
|
+
};
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Tokenize the full path into a token stream, recognizing slash-
|
|
1801
|
+
* spanning optional groups (`/users{/edit/:id}`).
|
|
1802
|
+
*
|
|
1803
|
+
* The group-open marker is emitted in place of the leading `/`
|
|
1804
|
+
* inside `{...}`; group-close before the trailing `}`. The walker
|
|
1805
|
+
* later expands by either keeping the run between markers or
|
|
1806
|
+
* dropping it.
|
|
1807
|
+
*/
|
|
1808
|
+
function tokenizePath(path) {
|
|
1809
|
+
const trimmed = path.charAt(0) === "/" ? path.slice(1) : path;
|
|
1810
|
+
if (trimmed === "") return [];
|
|
1811
|
+
const tokens = [];
|
|
1812
|
+
let i = 0;
|
|
1813
|
+
const n = trimmed.length;
|
|
1814
|
+
while (i < n) {
|
|
1815
|
+
if (trimmed.charAt(i) === "{") {
|
|
1816
|
+
let close = -1;
|
|
1817
|
+
for (let j = i + 1; j < n; j++) {
|
|
1818
|
+
const c = trimmed.charAt(j);
|
|
1819
|
+
if (c === "{") return null;
|
|
1820
|
+
if (c === "}") {
|
|
1821
|
+
close = j;
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (close === -1) return null;
|
|
1826
|
+
const inner = trimmed.slice(i + 1, close);
|
|
1827
|
+
if (inner.charAt(0) !== "/") return null;
|
|
1828
|
+
const after = close + 1 < n ? trimmed.charAt(close + 1) : "";
|
|
1829
|
+
if (after !== "" && after !== "/" && after !== "{") return null;
|
|
1830
|
+
tokens.push({ kind: "groupOpen" });
|
|
1831
|
+
const innerTokens = tokenizePath(inner);
|
|
1832
|
+
if (innerTokens === null) return null;
|
|
1833
|
+
for (const t of innerTokens) tokens.push(t);
|
|
1834
|
+
tokens.push({ kind: "groupClose" });
|
|
1835
|
+
i = close + 1;
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
let segEnd = i;
|
|
1839
|
+
while (segEnd < n) {
|
|
1840
|
+
const c = trimmed.charAt(segEnd);
|
|
1841
|
+
if (c === "/" || c === "{") break;
|
|
1842
|
+
segEnd++;
|
|
1843
|
+
}
|
|
1844
|
+
const segment = trimmed.slice(i, segEnd);
|
|
1845
|
+
if (segment !== "") {
|
|
1846
|
+
const token = tokenizeSegment(segment);
|
|
1847
|
+
if (token === null) return null;
|
|
1848
|
+
tokens.push(token);
|
|
1849
|
+
}
|
|
1850
|
+
i = segEnd;
|
|
1851
|
+
if (i < n && trimmed.charAt(i) === "/") i++;
|
|
1852
|
+
}
|
|
1853
|
+
return tokens;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Expand the token stream into one or more concrete `Token[]`
|
|
1857
|
+
* variants by:
|
|
1858
|
+
* 1. Splitting `groupOpen` … `groupClose` runs into a "with run"
|
|
1859
|
+
* and a "without run" choice (one optional group → ×2 variants).
|
|
1860
|
+
* 2. Splitting `param.optional = true` into a "with" and "without"
|
|
1861
|
+
* choice (one `:id?` → ×2 variants).
|
|
1862
|
+
*
|
|
1863
|
+
* Caps at `MAX_VARIANTS` — beyond that, returns `null` so the path
|
|
1864
|
+
* falls back to the universal bucket.
|
|
1865
|
+
*
|
|
1866
|
+
* Returns `Token[][]` (not `Segment[][]`) so the recursive
|
|
1867
|
+
* group-expansion can splice inner variants back into the outer
|
|
1868
|
+
* token stream without a lossy round-trip through `Segment`.
|
|
1869
|
+
* `parsePath` does the final `Token → Segment` projection.
|
|
1870
|
+
*/
|
|
1871
|
+
function expand(tokens) {
|
|
1872
|
+
let variants = [[]];
|
|
1873
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1874
|
+
const token = tokens[i];
|
|
1875
|
+
if (token.kind === "groupOpen") {
|
|
1876
|
+
let depth = 1;
|
|
1877
|
+
let close = -1;
|
|
1878
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
1879
|
+
const t = tokens[j];
|
|
1880
|
+
if (t.kind === "groupOpen") depth++;
|
|
1881
|
+
else if (t.kind === "groupClose") {
|
|
1882
|
+
depth--;
|
|
1883
|
+
if (depth === 0) {
|
|
1884
|
+
close = j;
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (close === -1) return null;
|
|
1890
|
+
const expandedInner = expand(tokens.slice(i + 1, close));
|
|
1891
|
+
if (expandedInner === null) return null;
|
|
1892
|
+
const next = [];
|
|
1893
|
+
for (const v of variants) {
|
|
1894
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1895
|
+
next.push(v.slice());
|
|
1896
|
+
for (const innerVariant of expandedInner) {
|
|
1897
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1898
|
+
next.push(v.concat(innerVariant));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
variants = next;
|
|
1902
|
+
i = close;
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
if (token.kind === "param" && token.optional) {
|
|
1906
|
+
const stripped = {
|
|
1907
|
+
kind: "param",
|
|
1908
|
+
name: token.name,
|
|
1909
|
+
optional: false
|
|
1910
|
+
};
|
|
1911
|
+
const next = [];
|
|
1912
|
+
for (const v of variants) {
|
|
1913
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1914
|
+
next.push(v.slice());
|
|
1915
|
+
if (next.length >= MAX_VARIANTS) return null;
|
|
1916
|
+
next.push(v.concat([stripped]));
|
|
1917
|
+
}
|
|
1918
|
+
variants = next;
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
for (const v of variants) v.push(token);
|
|
1922
|
+
}
|
|
1923
|
+
return variants;
|
|
1924
|
+
}
|
|
1925
|
+
function tokenToSegment(t) {
|
|
1926
|
+
if (t.kind === "literal") return {
|
|
1927
|
+
kind: "static",
|
|
1928
|
+
value: t.value
|
|
1929
|
+
};
|
|
1930
|
+
if (t.kind === "param") return {
|
|
1931
|
+
kind: "param",
|
|
1932
|
+
name: t.name
|
|
1933
|
+
};
|
|
1934
|
+
if (t.kind === "splat") return {
|
|
1935
|
+
kind: "splat",
|
|
1936
|
+
name: t.name
|
|
1937
|
+
};
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Stable, structural identity for a variant — used to drop duplicate
|
|
1942
|
+
* expansions like `/users{/:id?}` (which produces the bare-`/users`
|
|
1943
|
+
* variant twice: once from the "without group" branch and once from
|
|
1944
|
+
* the "with group, without optional param" branch).
|
|
1945
|
+
*/
|
|
1946
|
+
function variantKey(segs) {
|
|
1947
|
+
let out = "";
|
|
1948
|
+
for (const s of segs) if (s.kind === "static") out += `/s:${s.value}`;
|
|
1949
|
+
else if (s.kind === "param") out += `/p:${s.name}`;
|
|
1950
|
+
else out += `/*:${s.name}`;
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
function parsePath(path) {
|
|
1954
|
+
const tokens = tokenizePath(path);
|
|
1955
|
+
if (tokens === null) return null;
|
|
1956
|
+
const variants = expand(tokens);
|
|
1957
|
+
if (variants === null) return null;
|
|
1958
|
+
const result = [];
|
|
1959
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1960
|
+
for (const v of variants) {
|
|
1961
|
+
const segs = [];
|
|
1962
|
+
for (const t of v) {
|
|
1963
|
+
const s = tokenToSegment(t);
|
|
1964
|
+
if (s === null) return null;
|
|
1965
|
+
segs.push(s);
|
|
1966
|
+
}
|
|
1967
|
+
for (let i = 0; i < segs.length - 1; i++) if (segs[i].kind === "splat") return null;
|
|
1968
|
+
const key = variantKey(segs);
|
|
1969
|
+
if (seen.has(key)) continue;
|
|
1970
|
+
seen.add(key);
|
|
1971
|
+
result.push(segs);
|
|
1972
|
+
}
|
|
1973
|
+
return result;
|
|
1974
|
+
}
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region src/router/trie/node.ts
|
|
1977
|
+
function createMethodBuckets() {
|
|
1978
|
+
return Object.create(null);
|
|
1979
|
+
}
|
|
1980
|
+
function createTrieNode() {
|
|
1981
|
+
return {
|
|
1982
|
+
staticChildren: /* @__PURE__ */ new Map(),
|
|
1983
|
+
splatRoutes: createMethodBuckets(),
|
|
1984
|
+
exactRoutes: createMethodBuckets(),
|
|
1985
|
+
prefixRoutes: []
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
//#endregion
|
|
1989
|
+
//#region src/router/trie/module.ts
|
|
1990
|
+
function decodeOrRaw(s) {
|
|
1991
|
+
try {
|
|
1992
|
+
return decodeURIComponent(s);
|
|
1993
|
+
} catch {
|
|
1994
|
+
return s;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Build a `params` object from a request's pre-split segments using
|
|
1999
|
+
* the variant's pre-computed `ParamCapture[]`. No regex execution —
|
|
2000
|
+
* each capture is one indexed read from `segments` (and a join for
|
|
2001
|
+
* splats). Replaces the `matcher.exec` confirm pass for trie-walked
|
|
2002
|
+
* routes (T3).
|
|
2003
|
+
*/
|
|
2004
|
+
function extractTrieParams(segments, indexMap) {
|
|
2005
|
+
const out = Object.create(null);
|
|
2006
|
+
for (const cap of indexMap) if (cap.kind === "segment") out[cap.name] = decodeOrRaw(segments[cap.depth]);
|
|
2007
|
+
else {
|
|
2008
|
+
const slice = segments.slice(cap.depth).join("/");
|
|
2009
|
+
out[cap.name] = decodeOrRaw(slice);
|
|
2010
|
+
}
|
|
2011
|
+
return out;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Compute `match.path` (the matched-prefix string) from the request's
|
|
2015
|
+
* segments and the variant's recorded depth.
|
|
2016
|
+
*
|
|
2017
|
+
* - Non-splat variants: prefix = `segments[0..matchDepth].join('/')` —
|
|
2018
|
+
* exactly what the variant consumed (request length = variant
|
|
2019
|
+
* length for exact, request length ≥ variant length for prefix).
|
|
2020
|
+
* - Splat variants: the splat absorbed every remaining segment, so
|
|
2021
|
+
* the matched prefix is the entire request path. This mirrors what
|
|
2022
|
+
* `path-to-regexp`'s `output.path` would have returned pre-Phase-2
|
|
2023
|
+
* for `/files/*rest` matching `/files/a/b` (`/files/a/b`, not
|
|
2024
|
+
* `/files`).
|
|
2025
|
+
*/
|
|
2026
|
+
function trieMatchedPath(segments, matchDepth, splatTerminated) {
|
|
2027
|
+
const upTo = splatTerminated ? segments.length : matchDepth;
|
|
2028
|
+
if (upTo === 0) return "/";
|
|
2029
|
+
return `/${segments.slice(0, upTo).join("/")}`;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Pre-compute the `ParamCapture[]` for a variant's segments. Walk
|
|
2033
|
+
* the segments in order; emit one entry per `param` segment and a
|
|
2034
|
+
* terminal one for `splat` (always last). Static segments are
|
|
2035
|
+
* structurally consumed by the trie walk; they don't appear here.
|
|
2036
|
+
*/
|
|
2037
|
+
function buildParamsIndexMap(segments) {
|
|
2038
|
+
const out = [];
|
|
2039
|
+
for (const [i, seg] of segments.entries()) if (seg.kind === "param") out.push({
|
|
2040
|
+
kind: "segment",
|
|
2041
|
+
depth: i,
|
|
2042
|
+
name: seg.name
|
|
2043
|
+
});
|
|
2044
|
+
else if (seg.kind === "splat") {
|
|
2045
|
+
out.push({
|
|
2046
|
+
kind: "splat",
|
|
2047
|
+
depth: i,
|
|
2048
|
+
name: seg.name
|
|
2049
|
+
});
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
return out;
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Decide which method buckets a given request method should pull
|
|
2056
|
+
* from. Always includes `''` (method-agnostic). For HEAD also
|
|
2057
|
+
* includes GET (per `matchHandlerMethod`). For OPTIONS or no-method
|
|
2058
|
+
* lookups, returns `null` to signal "emit every bucket" — needed so
|
|
2059
|
+
* `event.methodsAllowed` is populated for OPTIONS auto-Allow and so
|
|
2060
|
+
* `IRouter.lookup(path)` (no method) keeps returning a complete
|
|
2061
|
+
* candidate set.
|
|
2062
|
+
*/
|
|
2063
|
+
function methodBucketKeys(method) {
|
|
2064
|
+
if (typeof method === "undefined" || method === MethodName.OPTIONS) return null;
|
|
2065
|
+
if (method === MethodName.HEAD) return [
|
|
2066
|
+
"",
|
|
2067
|
+
MethodName.HEAD,
|
|
2068
|
+
MethodName.GET
|
|
2069
|
+
];
|
|
2070
|
+
return ["", method];
|
|
2071
|
+
}
|
|
2072
|
+
function emitBucket(buckets, method, out) {
|
|
2073
|
+
const keys = methodBucketKeys(method);
|
|
2074
|
+
if (keys === null) {
|
|
2075
|
+
for (const k in buckets) {
|
|
2076
|
+
const list = buckets[k];
|
|
2077
|
+
for (const r of list) out.push(r);
|
|
2078
|
+
}
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
for (const k of keys) {
|
|
2082
|
+
const list = buckets[k];
|
|
2083
|
+
if (!list) continue;
|
|
2084
|
+
for (const r of list) out.push(r);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
function hasAnyBucket(buckets) {
|
|
2088
|
+
for (const _k in buckets) return true;
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
function pushIntoBucket(buckets, methodKey, route) {
|
|
2092
|
+
const bucket = buckets[methodKey];
|
|
2093
|
+
if (bucket) bucket.push(route);
|
|
2094
|
+
else buckets[methodKey] = [route];
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Radix-trie router — registers routes into a per-segment tree at
|
|
2098
|
+
* `add()` time and walks the tree at `lookup()` to collect
|
|
2099
|
+
* candidates by structure rather than by linear scan.
|
|
2100
|
+
*
|
|
2101
|
+
* Inspired by Hono's `TrieRouter` and rou3. The trie handles
|
|
2102
|
+
* routup's path vocabulary directly via its own parser
|
|
2103
|
+
* (`./parser.ts`):
|
|
2104
|
+
*
|
|
2105
|
+
* - Static segments (`/users`)
|
|
2106
|
+
* - Named params (`:id`)
|
|
2107
|
+
* - Optional params (`:id?`) — expanded to two route variants at
|
|
2108
|
+
* registration (T2)
|
|
2109
|
+
* - Optional groups (`/users{/edit}`) — same expansion strategy
|
|
2110
|
+
* - Bare and named splats (`/files/*`, `/files/*rest`)
|
|
2111
|
+
*
|
|
2112
|
+
* Per-leaf storage is bucketed by HTTP method (T4) so lookup
|
|
2113
|
+
* narrows to the request method's bucket(s) instead of emitting
|
|
2114
|
+
* every entry at the leaf and letting the dispatcher's filter
|
|
2115
|
+
* discard mismatches.
|
|
2116
|
+
*
|
|
2117
|
+
* Param extraction is `paramsIndexMap`-driven (T3): a pre-built
|
|
2118
|
+
* `Array<{ depth, name }>` per variant lets `extractTrieParams`
|
|
2119
|
+
* read params straight from the request's pre-split segments — no
|
|
2120
|
+
* regex execution per match.
|
|
2121
|
+
*
|
|
2122
|
+
* Paths the trie parser doesn't handle (compound segments like
|
|
2123
|
+
* `/files/:n.ext`, escape sequences `\:`, regex constraints) and
|
|
2124
|
+
* empty/root paths fall through to the `universal` bucket. That
|
|
2125
|
+
* bucket still uses `path-to-regexp` via `buildRoutePathMatcher`,
|
|
2126
|
+
* so correctness is preserved.
|
|
2127
|
+
*
|
|
2128
|
+
* Pure-static-spine fast path (`shortCircuit`): when the request
|
|
2129
|
+
* walks a static spine with no param/splat/prefix siblings on any
|
|
2130
|
+
* traversed node, the leaf's `exactRoutes` (filtered to the request
|
|
2131
|
+
* method's buckets) is the full answer — no need to walk the param
|
|
2132
|
+
* branch or collect prefix candidates at intermediate nodes.
|
|
2133
|
+
*/
|
|
2134
|
+
var TrieRouter = class TrieRouter {
|
|
2135
|
+
/**
|
|
2136
|
+
* Monotonic counter assigned as the registration `index` on each
|
|
2137
|
+
* route — the dispatch loop uses it to preserve registration
|
|
2138
|
+
* order across the candidate list. App owns the canonical
|
|
2139
|
+
* `Route<T>[]` list (Plan 019); the trie no longer keeps a
|
|
2140
|
+
* parallel copy.
|
|
2141
|
+
*/
|
|
2142
|
+
_routeCount;
|
|
2143
|
+
root;
|
|
2144
|
+
/**
|
|
2145
|
+
* Routes that bypass the trie — registered with no path, with
|
|
2146
|
+
* the root path `/`, or with a path containing syntax the
|
|
2147
|
+
* parser doesn't recognise. Walked linearly on every lookup,
|
|
2148
|
+
* merged into the result in registration order.
|
|
2149
|
+
*/
|
|
2150
|
+
universal;
|
|
2151
|
+
cache;
|
|
2152
|
+
constructor(options = {}) {
|
|
2153
|
+
this._routeCount = 0;
|
|
2154
|
+
this.root = createTrieNode();
|
|
2155
|
+
this.universal = [];
|
|
2156
|
+
this.cache = options.cache;
|
|
2157
|
+
}
|
|
2158
|
+
add(route) {
|
|
2159
|
+
const index = this._routeCount++;
|
|
2160
|
+
if (typeof route.path !== "string" || route.path === "" || route.path === "/") {
|
|
2161
|
+
this.universal.push({
|
|
2162
|
+
route,
|
|
2163
|
+
index,
|
|
2164
|
+
matcher: buildRoutePathMatcher(route)
|
|
2165
|
+
});
|
|
2166
|
+
this.cache?.clear();
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
const variants = parsePath(route.path);
|
|
2170
|
+
if (variants === null) {
|
|
2171
|
+
this.universal.push({
|
|
2172
|
+
route,
|
|
2173
|
+
index,
|
|
2174
|
+
matcher: buildRoutePathMatcher(route)
|
|
2175
|
+
});
|
|
2176
|
+
this.cache?.clear();
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
for (const segments of variants) this.insertIntoTrie(segments, route, index);
|
|
2180
|
+
this.cache?.clear();
|
|
2181
|
+
}
|
|
2182
|
+
lookup(path, method) {
|
|
2183
|
+
const cacheKey = `${method ?? ""}\t${path}`;
|
|
2184
|
+
const cached = this.cache?.get(cacheKey);
|
|
2185
|
+
if (typeof cached !== "undefined") return cached;
|
|
2186
|
+
const candidates = [];
|
|
2187
|
+
for (const u of this.universal) candidates.push(u);
|
|
2188
|
+
const segments = this.parseRequestPath(path);
|
|
2189
|
+
const shortCircuit = this.shortCircuit(segments, method);
|
|
2190
|
+
if (shortCircuit !== null) for (const c of shortCircuit) candidates.push(c);
|
|
2191
|
+
else this.walk(this.root, segments, 0, candidates, method);
|
|
2192
|
+
candidates.sort((a, b) => {
|
|
2193
|
+
if (a.index !== b.index) return a.index - b.index;
|
|
2194
|
+
const ad = a.matchDepth ?? -1;
|
|
2195
|
+
return (b.matchDepth ?? -1) - ad;
|
|
2196
|
+
});
|
|
2197
|
+
const matches = [];
|
|
2198
|
+
let lastIndex = -1;
|
|
2199
|
+
for (const candidate of candidates) {
|
|
2200
|
+
const { route, index, matcher, paramsIndexMap, matchDepth } = candidate;
|
|
2201
|
+
if (index === lastIndex) continue;
|
|
2202
|
+
if (matcher) {
|
|
2203
|
+
const output = matcher.exec(path);
|
|
2204
|
+
if (typeof output === "undefined") continue;
|
|
2205
|
+
matches.push({
|
|
2206
|
+
route,
|
|
2207
|
+
index,
|
|
2208
|
+
params: this.assignParams(output.params),
|
|
2209
|
+
path: output.path
|
|
2210
|
+
});
|
|
2211
|
+
lastIndex = index;
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
if (paramsIndexMap && typeof matchDepth === "number") {
|
|
2215
|
+
matches.push({
|
|
2216
|
+
route,
|
|
2217
|
+
index,
|
|
2218
|
+
params: extractTrieParams(segments, paramsIndexMap),
|
|
2219
|
+
path: trieMatchedPath(segments, matchDepth, candidate.splatTerminated === true)
|
|
2220
|
+
});
|
|
2221
|
+
lastIndex = index;
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
matches.push({
|
|
2225
|
+
route,
|
|
2226
|
+
index,
|
|
2227
|
+
params: Object.create(null)
|
|
2228
|
+
});
|
|
2229
|
+
lastIndex = index;
|
|
2230
|
+
}
|
|
2231
|
+
this.cache?.set(cacheKey, matches);
|
|
2232
|
+
return matches;
|
|
2233
|
+
}
|
|
2234
|
+
clone() {
|
|
2235
|
+
return new TrieRouter({ cache: this.cache?.clone() });
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* T1: returns the pre-computed candidate list when the request's
|
|
2239
|
+
* static spine has no param sibling, no prefix routes, and no
|
|
2240
|
+
* splats along the way. The leaf node's `exactRoutes` (filtered
|
|
2241
|
+
* to the request method's buckets) is then the complete answer —
|
|
2242
|
+
* no need to walk the param branch or collect prefix/splat
|
|
2243
|
+
* candidates from intermediate nodes. When any branch is
|
|
2244
|
+
* encountered, returns `null` and the caller falls through to
|
|
2245
|
+
* the regular `walk`.
|
|
2246
|
+
*/
|
|
2247
|
+
shortCircuit(segments, method) {
|
|
2248
|
+
let node = this.root;
|
|
2249
|
+
for (const segment of segments) {
|
|
2250
|
+
if (node.paramChild || hasAnyBucket(node.splatRoutes) || node.prefixRoutes.length > 0) return null;
|
|
2251
|
+
const child = node.staticChildren.get(segment);
|
|
2252
|
+
if (!child) return null;
|
|
2253
|
+
node = child;
|
|
2254
|
+
}
|
|
2255
|
+
if (node.paramChild || hasAnyBucket(node.splatRoutes) || node.prefixRoutes.length > 0) return null;
|
|
2256
|
+
const out = [];
|
|
2257
|
+
emitBucket(node.exactRoutes, method, out);
|
|
2258
|
+
return out;
|
|
2259
|
+
}
|
|
2260
|
+
parseRequestPath(path) {
|
|
2261
|
+
const trimmed = path.charAt(0) === "/" ? path.slice(1) : path;
|
|
2262
|
+
if (trimmed === "") return [];
|
|
2263
|
+
const parts = trimmed.split("/");
|
|
2264
|
+
const result = [];
|
|
2265
|
+
for (const part of parts) if (part !== "") result.push(part);
|
|
2266
|
+
return result;
|
|
2267
|
+
}
|
|
2268
|
+
insertIntoTrie(segments, route, index) {
|
|
2269
|
+
let node = this.root;
|
|
2270
|
+
const exact = this.isExactMatchRoute(route);
|
|
2271
|
+
const methodKey = route.method ?? "";
|
|
2272
|
+
const paramsIndexMap = buildParamsIndexMap(segments);
|
|
2273
|
+
for (const [i, segment] of segments.entries()) {
|
|
2274
|
+
const seg = segment;
|
|
2275
|
+
if (seg.kind === "splat") {
|
|
2276
|
+
pushIntoBucket(node.splatRoutes, methodKey, {
|
|
2277
|
+
route,
|
|
2278
|
+
index,
|
|
2279
|
+
paramsIndexMap,
|
|
2280
|
+
matchDepth: i,
|
|
2281
|
+
splatTerminated: true
|
|
2282
|
+
});
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (seg.kind === "param") {
|
|
2286
|
+
if (!node.paramChild) node.paramChild = createTrieNode();
|
|
2287
|
+
node = node.paramChild;
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
let child = node.staticChildren.get(seg.value);
|
|
2291
|
+
if (!child) {
|
|
2292
|
+
child = createTrieNode();
|
|
2293
|
+
node.staticChildren.set(seg.value, child);
|
|
2294
|
+
}
|
|
2295
|
+
node = child;
|
|
2296
|
+
}
|
|
2297
|
+
const indexed = {
|
|
2298
|
+
route,
|
|
2299
|
+
index,
|
|
2300
|
+
paramsIndexMap,
|
|
2301
|
+
matchDepth: segments.length
|
|
2302
|
+
};
|
|
2303
|
+
if (exact) pushIntoBucket(node.exactRoutes, methodKey, indexed);
|
|
2304
|
+
else node.prefixRoutes.push(indexed);
|
|
2305
|
+
}
|
|
2306
|
+
walk(node, segments, depth, collected, method) {
|
|
2307
|
+
emitBucket(node.splatRoutes, method, collected);
|
|
2308
|
+
if (depth === segments.length) {
|
|
2309
|
+
emitBucket(node.exactRoutes, method, collected);
|
|
2310
|
+
for (const p of node.prefixRoutes) collected.push(p);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
for (const p of node.prefixRoutes) collected.push(p);
|
|
2314
|
+
const seg = segments[depth];
|
|
2315
|
+
const staticChild = node.staticChildren.get(seg);
|
|
2316
|
+
if (staticChild) this.walk(staticChild, segments, depth + 1, collected, method);
|
|
2317
|
+
if (node.paramChild) this.walk(node.paramChild, segments, depth + 1, collected, method);
|
|
2318
|
+
}
|
|
2319
|
+
isExactMatchRoute(route) {
|
|
2320
|
+
return typeof route.method !== "undefined";
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* T5: copy params onto a prototype-less object so downstream
|
|
2324
|
+
* lookups skip prototype-chain traversal and avoid `__proto__` /
|
|
2325
|
+
* `hasOwnProperty` shadowing from user-controlled segment values.
|
|
2326
|
+
*/
|
|
2327
|
+
assignParams(source) {
|
|
2328
|
+
const out = Object.create(null);
|
|
2329
|
+
for (const k in source) if (Object.prototype.hasOwnProperty.call(source, k)) out[k] = source[k];
|
|
2330
|
+
return out;
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
//#endregion
|
|
2334
|
+
//#region src/router/smart/module.ts
|
|
2335
|
+
/**
|
|
2336
|
+
* Default crossover. Empirically `LinearRouter` wins for small route
|
|
2337
|
+
* counts (no per-request trie walk overhead, no static-spine setup);
|
|
2338
|
+
* `TrieRouter` wins past ~30 entries on typical workloads. Override
|
|
2339
|
+
* via `SmartRouterOptions.threshold` when a benchmark says otherwise
|
|
2340
|
+
* for your route shape.
|
|
2341
|
+
*/
|
|
2342
|
+
const DEFAULT_THRESHOLD = 30;
|
|
2343
|
+
/**
|
|
2344
|
+
* Auto-selecting router. Accumulates registered routes in a pending
|
|
2345
|
+
* buffer; on the first `lookup()` call, picks `LinearRouter` or
|
|
2346
|
+
* `TrieRouter` based on the registered route count and replays the
|
|
2347
|
+
* pending list onto the chosen inner router. Every subsequent call
|
|
2348
|
+
* — `add`, `lookup`, `clone` — forwards to the inner.
|
|
2349
|
+
*
|
|
2350
|
+
* Use this when you don't want to commit to a router family up-front
|
|
2351
|
+
* (e.g. a library that registers a variable number of routes
|
|
2352
|
+
* depending on configuration). For known workloads, prefer the
|
|
2353
|
+
* concrete router — `SmartRouter` adds one indirection per call.
|
|
2354
|
+
*
|
|
2355
|
+
* Inspired by Hono's `SmartRouter` (which auto-selects across more
|
|
2356
|
+
* candidates including `RegExpRouter`); ours covers the only choice
|
|
2357
|
+
* that matters in routup today: linear-vs-trie at the registration-
|
|
2358
|
+
* size crossover.
|
|
2359
|
+
*/
|
|
2360
|
+
var SmartRouter = class SmartRouter {
|
|
2361
|
+
inner;
|
|
2362
|
+
pending = [];
|
|
2363
|
+
threshold;
|
|
2364
|
+
/**
|
|
2365
|
+
* Cache handed off to whichever inner router gets chosen. Stays
|
|
2366
|
+
* `undefined` if the user didn't configure one.
|
|
2367
|
+
*/
|
|
2368
|
+
cache;
|
|
2369
|
+
constructor(options = {}) {
|
|
2370
|
+
this.threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
2371
|
+
this.cache = options.cache;
|
|
2372
|
+
}
|
|
2373
|
+
add(route) {
|
|
2374
|
+
if (this.inner) {
|
|
2375
|
+
this.inner.add(route);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
this.pending.push(route);
|
|
2379
|
+
}
|
|
2380
|
+
lookup(path, method) {
|
|
2381
|
+
if (!this.inner) {
|
|
2382
|
+
this.inner = this.choose();
|
|
2383
|
+
for (const r of this.pending) this.inner.add(r);
|
|
2384
|
+
this.pending = [];
|
|
2385
|
+
}
|
|
2386
|
+
return this.inner.lookup(path, method);
|
|
2387
|
+
}
|
|
2388
|
+
clone() {
|
|
2389
|
+
return new SmartRouter({
|
|
2390
|
+
threshold: this.threshold,
|
|
2391
|
+
cache: this.cache?.clone()
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Pick the inner router based on the registered route count.
|
|
2396
|
+
* `LinearRouter` for tiny tables, `TrieRouter` past the
|
|
2397
|
+
* configured threshold.
|
|
2398
|
+
*
|
|
2399
|
+
* @protected
|
|
2400
|
+
*/
|
|
2401
|
+
choose() {
|
|
2402
|
+
if (this.pending.length < this.threshold) return new LinearRouter({ cache: this.cache });
|
|
2403
|
+
return new TrieRouter({ cache: this.cache });
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/app/options.ts
|
|
2408
|
+
function normalizeAppOptions(input) {
|
|
2409
|
+
let etag;
|
|
2410
|
+
if (typeof input.etag !== "undefined") if (input.etag === null || input.etag === false) etag = null;
|
|
2411
|
+
else etag = buildEtagFn(input.etag);
|
|
2412
|
+
let trustProxy;
|
|
2413
|
+
if (typeof input.trustProxy !== "undefined") trustProxy = buildTrustProxyFn(input.trustProxy);
|
|
2414
|
+
if (typeof input.timeout !== "undefined") {
|
|
2415
|
+
if (!Number.isFinite(input.timeout) || input.timeout <= 0) delete input.timeout;
|
|
2416
|
+
}
|
|
2417
|
+
if (typeof input.handlerTimeout !== "undefined") {
|
|
2418
|
+
if (!Number.isFinite(input.handlerTimeout) || input.handlerTimeout <= 0) delete input.handlerTimeout;
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
...input,
|
|
2422
|
+
etag,
|
|
2423
|
+
trustProxy
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
//#endregion
|
|
2427
|
+
//#region src/app/constants.ts
|
|
2428
|
+
const AppSymbol = Symbol.for("App");
|
|
2429
|
+
const AppPipelineStep = {
|
|
2430
|
+
START: 0,
|
|
2431
|
+
LOOKUP: 1,
|
|
2432
|
+
CHILD_BEFORE: 2,
|
|
2433
|
+
CHILD_DISPATCH: 3,
|
|
2434
|
+
CHILD_AFTER: 4,
|
|
2435
|
+
FINISH: 5
|
|
2436
|
+
};
|
|
2437
|
+
const RouteEntryType = {
|
|
2438
|
+
APP: "app",
|
|
2439
|
+
HANDLER: "handler"
|
|
2440
|
+
};
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/app/check.ts
|
|
2443
|
+
function isAppInstance(input) {
|
|
2444
|
+
return hasInstanceof(input, AppSymbol);
|
|
2445
|
+
}
|
|
2446
|
+
//#endregion
|
|
2447
|
+
//#region src/app/module.ts
|
|
2448
|
+
/**
|
|
2449
|
+
* Merge resolver-supplied path params into `event.params` *only* when
|
|
2450
|
+
* `match.params` actually has keys. Skipping the object spread on the
|
|
2451
|
+
* empty-params path (every static route, every middleware match) saves
|
|
2452
|
+
* an allocation per match — the hottest path in static-route apps.
|
|
2453
|
+
*/
|
|
2454
|
+
function mergeMatchParams(event, matchParams) {
|
|
2455
|
+
let hasKeys = false;
|
|
2456
|
+
for (const _k in matchParams) {
|
|
2457
|
+
hasKeys = true;
|
|
2458
|
+
break;
|
|
2459
|
+
}
|
|
2460
|
+
if (!hasKeys) return;
|
|
2461
|
+
event.params = {
|
|
2462
|
+
...event.params,
|
|
2463
|
+
...matchParams
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Copy `source[key]` into `target[key]` when the target's value is
|
|
2468
|
+
* undefined; return whether a write happened.
|
|
2469
|
+
*
|
|
2470
|
+
* Bound to a single key `K` per call, so TypeScript can prove the
|
|
2471
|
+
* read and write hit the same property's value type — no `as` cast
|
|
2472
|
+
* needed. This is the standard escape hatch for the variance trap
|
|
2473
|
+
* you'd otherwise hit by writing `target[key] = source[key]` inside
|
|
2474
|
+
* a loop where `key: keyof AppOptions` is a *union* (read returns
|
|
2475
|
+
* the union of value types, write requires the intersection, which
|
|
2476
|
+
* collapses to `never`).
|
|
2477
|
+
*/
|
|
2478
|
+
function copyOptionIfUnset(target, source, key) {
|
|
2479
|
+
if (typeof target[key] !== "undefined") return false;
|
|
2480
|
+
target[key] = source[key];
|
|
2481
|
+
return true;
|
|
2482
|
+
}
|
|
2483
|
+
var App = class App {
|
|
2484
|
+
/**
|
|
2485
|
+
* A label for the router instance.
|
|
2486
|
+
*/
|
|
2487
|
+
name;
|
|
2488
|
+
/**
|
|
2489
|
+
* Registration-time path prefix for entries registered on this
|
|
2490
|
+
* App. Local to this instance — never inherited from a parent.
|
|
2491
|
+
*
|
|
2492
|
+
* @protected
|
|
2493
|
+
*/
|
|
2494
|
+
_path;
|
|
2495
|
+
/**
|
|
2496
|
+
* Pluggable router (route table) — owns the "which entries match
|
|
2497
|
+
* this path?" lookup. Defaults to `LinearRouter` (walks entries
|
|
2498
|
+
* linearly per request); swap in via `AppContext.router`
|
|
2499
|
+
* for a radix/trie implementation on apps with many routes.
|
|
2500
|
+
*
|
|
2501
|
+
* @protected
|
|
2502
|
+
*/
|
|
2503
|
+
router;
|
|
2504
|
+
/**
|
|
2505
|
+
* Lifecycle hook registry.
|
|
2506
|
+
*
|
|
2507
|
+
* @protected
|
|
2508
|
+
*/
|
|
2509
|
+
hooks;
|
|
2510
|
+
/**
|
|
2511
|
+
* Normalized options for this App instance.
|
|
2512
|
+
*
|
|
2513
|
+
* Frozen on construction and on every `extendOptions` update —
|
|
2514
|
+
* once published to `event.appOptions` it is shared across all
|
|
2515
|
+
* requests, and a handler must not be able to mutate
|
|
2516
|
+
* router-global state. `extendOptions` therefore uses a
|
|
2517
|
+
* functional update (build a new object, freeze it, replace
|
|
2518
|
+
* the slot) rather than mutating in place.
|
|
2519
|
+
*/
|
|
2520
|
+
_options;
|
|
2521
|
+
/**
|
|
2522
|
+
* Registry of installed plugins (name → version) on this router.
|
|
2523
|
+
*
|
|
2524
|
+
* @protected
|
|
2525
|
+
*/
|
|
2526
|
+
plugins = /* @__PURE__ */ new Map();
|
|
2527
|
+
/**
|
|
2528
|
+
* Every route registered on this App, in registration order.
|
|
2529
|
+
*
|
|
2530
|
+
* App owns the canonical list — the `IRouter` contract has no
|
|
2531
|
+
* `routes` field, so cascades / clones / `setRouter` replay
|
|
2532
|
+
* read from here instead of asking the router. Routes are
|
|
2533
|
+
* pushed alongside every `this.router.add()` via the `register`
|
|
2534
|
+
* helper.
|
|
2535
|
+
*
|
|
2536
|
+
* @protected
|
|
2537
|
+
*/
|
|
2538
|
+
_routes = [];
|
|
2539
|
+
constructor(input = {}) {
|
|
2540
|
+
this.name = input.name;
|
|
2541
|
+
this._path = input.path;
|
|
2542
|
+
this.hooks = input.hooks ?? new Hooks();
|
|
2543
|
+
this.plugins = new Map(input.plugins);
|
|
2544
|
+
this.router = input.router ?? new LinearRouter();
|
|
2545
|
+
this._options = Object.freeze(normalizeAppOptions(input.options ?? {}));
|
|
2546
|
+
markInstanceof(this, AppSymbol);
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Register a route with the active router and record it on the
|
|
2550
|
+
* App so we can replay it onto a different router later (see
|
|
2551
|
+
* `setRouter`) and so cascades / clones have a source of truth
|
|
2552
|
+
* independent of the router instance.
|
|
2553
|
+
*
|
|
2554
|
+
* @protected
|
|
2555
|
+
*/
|
|
2556
|
+
register(route) {
|
|
2557
|
+
this.router.add(route);
|
|
2558
|
+
this._routes.push(route);
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Swap the active router. Replays every previously-registered
|
|
2562
|
+
* route onto the new router so lookups stay correct.
|
|
2563
|
+
*
|
|
2564
|
+
* Useful for picking a router after route shape is known (e.g.
|
|
2565
|
+
* a SmartRouter-style decision), or for testing alternatives
|
|
2566
|
+
* mid-flight without rebuilding the App. Any cache the previous
|
|
2567
|
+
* router carried is dropped along with it.
|
|
2568
|
+
*/
|
|
2569
|
+
setRouter(router) {
|
|
2570
|
+
for (const route of this._routes) router.add(route);
|
|
2571
|
+
this.router = router;
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Public entry point — creates a DispatcherEvent from the request,
|
|
2575
|
+
* runs the pipeline, and returns a Response (with 404/500 fallbacks).
|
|
2576
|
+
*/
|
|
2577
|
+
async fetch(request) {
|
|
2578
|
+
const event = new DispatcherEvent(request);
|
|
2579
|
+
let response;
|
|
2580
|
+
try {
|
|
2581
|
+
const timeoutMs = this._options.timeout;
|
|
2582
|
+
if (timeoutMs) {
|
|
2583
|
+
const controller = new AbortController();
|
|
2584
|
+
event.signal = controller.signal;
|
|
2585
|
+
let timerId;
|
|
2586
|
+
try {
|
|
2587
|
+
response = await Promise.race([this.dispatch(event), new Promise((_, reject) => {
|
|
2588
|
+
timerId = setTimeout(() => {
|
|
2589
|
+
controller.abort();
|
|
2590
|
+
reject(createError({
|
|
2591
|
+
status: 408,
|
|
2592
|
+
message: "Request Timeout"
|
|
2593
|
+
}));
|
|
2594
|
+
}, timeoutMs);
|
|
2595
|
+
})]);
|
|
2596
|
+
} finally {
|
|
2597
|
+
clearTimeout(timerId);
|
|
2598
|
+
}
|
|
2599
|
+
} else response = await this.dispatch(event);
|
|
2600
|
+
} catch (e) {
|
|
2601
|
+
event.error = createError(e);
|
|
2602
|
+
}
|
|
2603
|
+
if (response) return response;
|
|
2604
|
+
if (event.error) return this.buildFallbackResponse(request, event, event.error.status || 500, event.error.message);
|
|
2605
|
+
return this.buildFallbackResponse(request, event, 404, "Not Found");
|
|
2606
|
+
}
|
|
2607
|
+
buildFallbackResponse(request, event, status, message) {
|
|
2608
|
+
const headers = new Headers(event.response.headers);
|
|
2609
|
+
if (acceptsJson(request)) {
|
|
2610
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
2611
|
+
return new Response(JSON.stringify({
|
|
2612
|
+
status,
|
|
2613
|
+
message
|
|
2614
|
+
}), {
|
|
2615
|
+
status,
|
|
2616
|
+
headers
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
headers.set("content-type", "text/plain; charset=utf-8");
|
|
2620
|
+
return new Response(message, {
|
|
2621
|
+
status,
|
|
2622
|
+
headers
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Mount-time option inheritance — fill in any of this App's
|
|
2627
|
+
* unset option keys from the supplied parent options. Called by
|
|
2628
|
+
* `App.use(child)` after narrowing to `App` via `isAppInstance`.
|
|
2629
|
+
*
|
|
2630
|
+
* Public so callers don't need to reach into another App's
|
|
2631
|
+
* protected fields: the parent passes its options by value and
|
|
2632
|
+
* the child decides what to do with them.
|
|
2633
|
+
*
|
|
2634
|
+
* Shallow per-key merge. App-local concerns — `name`, `path`,
|
|
2635
|
+
* `hooks`, `plugins`, `router`, and the router's cache — are
|
|
2636
|
+
* deliberately not propagated; they sit on `AppContext`, not
|
|
2637
|
+
* inside `AppOptions`.
|
|
2638
|
+
*
|
|
2639
|
+
* Cascades to any Apps already mounted on this one — a deeper
|
|
2640
|
+
* grandchild gets the new keys too. Without this, mounting
|
|
2641
|
+
* grandchild → child → parent in that order would leave the
|
|
2642
|
+
* grandchild without parent's options (it adopted from child
|
|
2643
|
+
* before child had them).
|
|
2644
|
+
*
|
|
2645
|
+
* Late mutation of the parent's options after this call does NOT
|
|
2646
|
+
* propagate. `AppOptions` is configured at construction-and-mount;
|
|
2647
|
+
* later changes are not a supported workflow.
|
|
2648
|
+
*/
|
|
2649
|
+
extendOptions(incoming) {
|
|
2650
|
+
let next;
|
|
2651
|
+
const keys = Object.keys(incoming);
|
|
2652
|
+
for (const key of keys) {
|
|
2653
|
+
if (typeof this._options[key] !== "undefined") continue;
|
|
2654
|
+
if (typeof incoming[key] === "undefined") continue;
|
|
2655
|
+
next ??= { ...this._options };
|
|
2656
|
+
copyOptionIfUnset(next, incoming, key);
|
|
2657
|
+
}
|
|
2658
|
+
if (!next) return;
|
|
2659
|
+
this._options = Object.freeze(next);
|
|
2660
|
+
for (const route of this._routes) if (route.data.type === RouteEntryType.APP && isAppInstance(route.data.data)) route.data.data.extendOptions(this._options);
|
|
2661
|
+
}
|
|
2662
|
+
async executePipelineStep(context) {
|
|
2663
|
+
while (context.step !== AppPipelineStep.FINISH) switch (context.step) {
|
|
2664
|
+
case AppPipelineStep.START:
|
|
2665
|
+
await this.executePipelineStepStart(context);
|
|
2666
|
+
break;
|
|
2667
|
+
case AppPipelineStep.LOOKUP:
|
|
2668
|
+
await this.executePipelineStepLookup(context);
|
|
2669
|
+
break;
|
|
2670
|
+
case AppPipelineStep.CHILD_BEFORE:
|
|
2671
|
+
await this.executePipelineStepChildBefore(context);
|
|
2672
|
+
break;
|
|
2673
|
+
case AppPipelineStep.CHILD_DISPATCH:
|
|
2674
|
+
await this.executePipelineStepChildDispatch(context);
|
|
2675
|
+
break;
|
|
2676
|
+
case AppPipelineStep.CHILD_AFTER:
|
|
2677
|
+
await this.executePipelineStepChildAfter(context);
|
|
2678
|
+
break;
|
|
2679
|
+
default:
|
|
2680
|
+
context.step = AppPipelineStep.FINISH;
|
|
2681
|
+
break;
|
|
2682
|
+
}
|
|
2683
|
+
await this.executePipelineStepFinish(context);
|
|
2684
|
+
}
|
|
2685
|
+
async executePipelineStepStart(context) {
|
|
2686
|
+
if (this.hooks.hasListeners(HookName.START)) await this.hooks.trigger(HookName.START, context.event);
|
|
2687
|
+
if (context.event.dispatched) context.step = AppPipelineStep.FINISH;
|
|
2688
|
+
else context.step = AppPipelineStep.LOOKUP;
|
|
2689
|
+
}
|
|
2690
|
+
async executePipelineStepLookup(context) {
|
|
2691
|
+
if (typeof context.matches === "undefined" || context.matchesPath !== context.event.path) {
|
|
2692
|
+
context.matches = this.router.lookup(context.event.path, context.event.method);
|
|
2693
|
+
context.matchesPath = context.event.path;
|
|
2694
|
+
}
|
|
2695
|
+
const { matches } = context;
|
|
2696
|
+
while (!context.event.dispatched && context.matchIndex < matches.length) {
|
|
2697
|
+
const { route } = matches[context.matchIndex];
|
|
2698
|
+
if (route.data.type === RouteEntryType.HANDLER) {
|
|
2699
|
+
const handler = route.data.data;
|
|
2700
|
+
if (context.event.error && handler.type === HandlerType.CORE || !context.event.error && handler.type === HandlerType.ERROR) {
|
|
2701
|
+
context.matchIndex++;
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
const { method } = route;
|
|
2705
|
+
if (method) context.event.methodsAllowed.add(method);
|
|
2706
|
+
if (!matchHandlerMethod(method, context.event.method)) {
|
|
2707
|
+
context.matchIndex++;
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
if (this.hooks.hasListeners(HookName.CHILD_MATCH)) await this.hooks.trigger(HookName.CHILD_MATCH, context.event);
|
|
2712
|
+
if (context.event.dispatched) {
|
|
2713
|
+
context.step = AppPipelineStep.FINISH;
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
if (context.event.path !== context.matchesPath) {
|
|
2717
|
+
context.matches = void 0;
|
|
2718
|
+
context.matchIndex = 0;
|
|
2719
|
+
context.step = AppPipelineStep.LOOKUP;
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
context.step = AppPipelineStep.CHILD_BEFORE;
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
context.step = AppPipelineStep.FINISH;
|
|
2726
|
+
}
|
|
2727
|
+
async executePipelineStepChildBefore(context) {
|
|
2728
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_BEFORE)) await this.hooks.trigger(HookName.CHILD_DISPATCH_BEFORE, context.event);
|
|
2729
|
+
if (context.event.dispatched) {
|
|
2730
|
+
context.step = AppPipelineStep.FINISH;
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (context.event.path !== context.matchesPath) {
|
|
2734
|
+
context.matches = void 0;
|
|
2735
|
+
context.matchIndex = 0;
|
|
2736
|
+
context.step = AppPipelineStep.LOOKUP;
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
context.step = AppPipelineStep.CHILD_DISPATCH;
|
|
2740
|
+
}
|
|
2741
|
+
async executePipelineStepChildAfter(context) {
|
|
2742
|
+
if (this.hooks.hasListeners(HookName.CHILD_DISPATCH_AFTER)) await this.hooks.trigger(HookName.CHILD_DISPATCH_AFTER, context.event);
|
|
2743
|
+
if (context.event.dispatched) context.step = AppPipelineStep.FINISH;
|
|
2744
|
+
else context.step = AppPipelineStep.LOOKUP;
|
|
2745
|
+
}
|
|
2746
|
+
async executePipelineStepChildDispatch(context) {
|
|
2747
|
+
const match = context.matches?.[context.matchIndex];
|
|
2748
|
+
if (context.event.dispatched || typeof match === "undefined") {
|
|
2749
|
+
context.step = AppPipelineStep.FINISH;
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
const { route } = match;
|
|
2753
|
+
const { event } = context;
|
|
2754
|
+
const savedPath = event.path;
|
|
2755
|
+
const savedMountPath = event.mountPath;
|
|
2756
|
+
const savedParams = event.params;
|
|
2757
|
+
if (route.data.type === RouteEntryType.APP && typeof match.path === "string") {
|
|
2758
|
+
event.mountPath = cleanDoubleSlashes(`${event.mountPath}/${match.path}`);
|
|
2759
|
+
if (event.path === match.path) event.path = "/";
|
|
2760
|
+
else event.path = withLeadingSlash(event.path.substring(match.path.length));
|
|
2761
|
+
mergeMatchParams(event, match.params);
|
|
2762
|
+
} else if (route.data.type === RouteEntryType.HANDLER && typeof match.path === "string") mergeMatchParams(event, match.params);
|
|
2763
|
+
try {
|
|
2764
|
+
const parentMatches = context.matches;
|
|
2765
|
+
const parentMatchesPath = context.matchesPath;
|
|
2766
|
+
const nextMatchIndex = context.matchIndex + 1;
|
|
2767
|
+
event.setNext(async (error) => {
|
|
2768
|
+
if (error) event.error = createError(error);
|
|
2769
|
+
const pathChanged = event.path !== parentMatchesPath;
|
|
2770
|
+
const savedStep = context.step;
|
|
2771
|
+
const savedMatchIndex = context.matchIndex;
|
|
2772
|
+
const savedMatches = context.matches;
|
|
2773
|
+
const savedMatchesPath = context.matchesPath;
|
|
2774
|
+
context.step = AppPipelineStep.LOOKUP;
|
|
2775
|
+
context.matchIndex = pathChanged ? 0 : nextMatchIndex;
|
|
2776
|
+
context.matches = pathChanged ? void 0 : parentMatches;
|
|
2777
|
+
context.matchesPath = pathChanged ? void 0 : parentMatchesPath;
|
|
2778
|
+
try {
|
|
2779
|
+
await this.executePipelineStep(context);
|
|
2780
|
+
} finally {
|
|
2781
|
+
context.step = savedStep;
|
|
2782
|
+
context.matchIndex = savedMatchIndex;
|
|
2783
|
+
context.matches = savedMatches;
|
|
2784
|
+
context.matchesPath = savedMatchesPath;
|
|
2785
|
+
}
|
|
2786
|
+
return context.response;
|
|
2787
|
+
});
|
|
2788
|
+
const response = await route.data.data.dispatch(event);
|
|
2789
|
+
if (response) {
|
|
2790
|
+
context.response = response;
|
|
2791
|
+
event.dispatched = true;
|
|
2792
|
+
}
|
|
2793
|
+
} catch (e) {
|
|
2794
|
+
event.error = createError(e);
|
|
2795
|
+
if (this.hooks.hasListeners(HookName.ERROR)) await this.hooks.trigger(HookName.ERROR, event);
|
|
2796
|
+
}
|
|
2797
|
+
if (!event.dispatched) {
|
|
2798
|
+
event.path = savedPath;
|
|
2799
|
+
event.mountPath = savedMountPath;
|
|
2800
|
+
event.params = savedParams;
|
|
2801
|
+
}
|
|
2802
|
+
context.matchIndex++;
|
|
2803
|
+
context.step = AppPipelineStep.CHILD_AFTER;
|
|
2804
|
+
}
|
|
2805
|
+
async executePipelineStepFinish(context) {
|
|
2806
|
+
if (!context.event.error && !context.event.dispatched && context.isRoot && context.event.method === MethodName.OPTIONS) {
|
|
2807
|
+
if (context.event.methodsAllowed.has(MethodName.GET)) context.event.methodsAllowed.add(MethodName.HEAD);
|
|
2808
|
+
const options = [...context.event.methodsAllowed].map((key) => key.toUpperCase()).join(",");
|
|
2809
|
+
const optionsHeaders = new Headers(context.event.response.headers);
|
|
2810
|
+
optionsHeaders.set(HeaderName.ALLOW, options);
|
|
2811
|
+
context.response = new Response(options, {
|
|
2812
|
+
status: context.event.response.status || 200,
|
|
2813
|
+
headers: optionsHeaders
|
|
2814
|
+
});
|
|
2815
|
+
context.event.dispatched = true;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
async dispatch(event) {
|
|
2819
|
+
const savedPath = event.path;
|
|
2820
|
+
const savedMountPath = event.mountPath;
|
|
2821
|
+
const savedParams = event.params;
|
|
2822
|
+
const savedAppOptions = event.appOptions;
|
|
2823
|
+
const wasDispatching = event.isDispatching;
|
|
2824
|
+
const isRoot = !wasDispatching;
|
|
2825
|
+
const context = {
|
|
2826
|
+
step: AppPipelineStep.START,
|
|
2827
|
+
event,
|
|
2828
|
+
isRoot,
|
|
2829
|
+
matchIndex: 0
|
|
2830
|
+
};
|
|
2831
|
+
event.appOptions = this._options;
|
|
2832
|
+
event.isDispatching = true;
|
|
2833
|
+
try {
|
|
2834
|
+
await this.executePipelineStep(context);
|
|
2835
|
+
if (this.hooks.hasListeners(HookName.END)) await this.hooks.trigger(HookName.END, event);
|
|
2836
|
+
} finally {
|
|
2837
|
+
event.appOptions = savedAppOptions;
|
|
2838
|
+
event.isDispatching = wasDispatching;
|
|
2839
|
+
if (!event.dispatched) {
|
|
2840
|
+
event.path = savedPath;
|
|
2841
|
+
event.mountPath = savedMountPath;
|
|
2842
|
+
event.params = savedParams;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
return context.response;
|
|
2846
|
+
}
|
|
2847
|
+
delete(...input) {
|
|
2848
|
+
this.useForMethod(MethodName.DELETE, ...input);
|
|
2849
|
+
return this;
|
|
2850
|
+
}
|
|
2851
|
+
get(...input) {
|
|
2852
|
+
this.useForMethod(MethodName.GET, ...input);
|
|
2853
|
+
return this;
|
|
2854
|
+
}
|
|
2855
|
+
post(...input) {
|
|
2856
|
+
this.useForMethod(MethodName.POST, ...input);
|
|
2857
|
+
return this;
|
|
2858
|
+
}
|
|
2859
|
+
put(...input) {
|
|
2860
|
+
this.useForMethod(MethodName.PUT, ...input);
|
|
2861
|
+
return this;
|
|
2862
|
+
}
|
|
2863
|
+
patch(...input) {
|
|
2864
|
+
this.useForMethod(MethodName.PATCH, ...input);
|
|
2865
|
+
return this;
|
|
2866
|
+
}
|
|
2867
|
+
head(...input) {
|
|
2868
|
+
this.useForMethod(MethodName.HEAD, ...input);
|
|
2869
|
+
return this;
|
|
2870
|
+
}
|
|
2871
|
+
options(...input) {
|
|
2872
|
+
this.useForMethod(MethodName.OPTIONS, ...input);
|
|
2873
|
+
return this;
|
|
2874
|
+
}
|
|
2875
|
+
useForMethod(method, ...input) {
|
|
2876
|
+
let path;
|
|
2877
|
+
for (const element of input) {
|
|
2878
|
+
if (isPath(element)) {
|
|
2879
|
+
path = element;
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
let handler;
|
|
2883
|
+
if (isHandler(element)) handler = element;
|
|
2884
|
+
else if (isHandlerOptions(element)) handler = new Handler({
|
|
2885
|
+
...element,
|
|
2886
|
+
method
|
|
2887
|
+
});
|
|
2888
|
+
else continue;
|
|
2889
|
+
this.register({
|
|
2890
|
+
path: joinPaths(this._path, path, handler.path),
|
|
2891
|
+
method,
|
|
2892
|
+
data: {
|
|
2893
|
+
type: RouteEntryType.HANDLER,
|
|
2894
|
+
data: handler
|
|
2895
|
+
}
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
use(...input) {
|
|
2900
|
+
let path;
|
|
2901
|
+
for (const item of input) {
|
|
2902
|
+
if (isPath(item)) {
|
|
2903
|
+
path = withLeadingSlash(item);
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
if (isAppInstance(item)) {
|
|
2907
|
+
item.extendOptions(this._options);
|
|
2908
|
+
this.register({
|
|
2909
|
+
path: joinPaths(this._path, path),
|
|
2910
|
+
data: {
|
|
2911
|
+
type: RouteEntryType.APP,
|
|
2912
|
+
data: item
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
2917
|
+
if (isHandler(item)) {
|
|
2918
|
+
this.register({
|
|
2919
|
+
path: joinPaths(this._path, path, item.path),
|
|
2920
|
+
method: item.method,
|
|
2921
|
+
data: {
|
|
2922
|
+
type: RouteEntryType.HANDLER,
|
|
2923
|
+
data: item
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2926
|
+
continue;
|
|
2927
|
+
}
|
|
2928
|
+
if (isHandlerOptions(item)) {
|
|
2929
|
+
const handler = new Handler({ ...item });
|
|
2930
|
+
this.register({
|
|
2931
|
+
path: joinPaths(this._path, path, handler.path),
|
|
2932
|
+
method: handler.method,
|
|
2933
|
+
data: {
|
|
2934
|
+
type: RouteEntryType.HANDLER,
|
|
2935
|
+
data: handler
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
continue;
|
|
2939
|
+
}
|
|
2940
|
+
if (isPlugin(item)) if (path) this.install(item, { path });
|
|
2941
|
+
else this.install(item);
|
|
2942
|
+
}
|
|
2943
|
+
return this;
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Check if a plugin with the given name is installed on this router.
|
|
2947
|
+
*/
|
|
2948
|
+
hasPlugin(name) {
|
|
2949
|
+
return this.plugins.has(name);
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Get the version of an installed plugin by name on this router,
|
|
2953
|
+
* or `undefined` if the plugin is not installed here.
|
|
2954
|
+
*/
|
|
2955
|
+
getPluginVersion(name) {
|
|
2956
|
+
return this.plugins.get(name);
|
|
2957
|
+
}
|
|
2958
|
+
install(plugin, context = {}) {
|
|
2959
|
+
if (this.plugins.has(plugin.name)) throw new PluginAlreadyInstalledError(plugin.name);
|
|
2960
|
+
const router = new App({
|
|
2961
|
+
name: plugin.name,
|
|
2962
|
+
router: this.router.clone()
|
|
2963
|
+
});
|
|
2964
|
+
plugin.install(router);
|
|
2965
|
+
if (context.path) this.use(context.path, router);
|
|
2966
|
+
else this.use(router);
|
|
2967
|
+
this.plugins.set(plugin.name, plugin.version);
|
|
2968
|
+
return this;
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Return a new `App` that mirrors this one but owns independent
|
|
2972
|
+
* mountable state.
|
|
2973
|
+
*
|
|
2974
|
+
* The new router has:
|
|
2975
|
+
* - a fresh `stack` array of shallow-copied entries (handlers and child
|
|
2976
|
+
* routers are shared by reference; only the wrapping entries are new)
|
|
2977
|
+
* - the same `pathMatcher` reference (it is stateless)
|
|
2978
|
+
* - a fresh `Hooks` instance seeded with the current listeners
|
|
2979
|
+
* - a shallow copy of `_options`
|
|
2980
|
+
* - a fresh `plugins` map with the same entries
|
|
2981
|
+
*
|
|
2982
|
+
* Use this when the same logical router needs to be mounted under
|
|
2983
|
+
* multiple paths — each mount can receive its own clone so subsequent
|
|
2984
|
+
* mutations on one mount do not bleed into the others.
|
|
2985
|
+
*/
|
|
2986
|
+
clone() {
|
|
2987
|
+
const next = new App({
|
|
2988
|
+
name: this.name,
|
|
2989
|
+
path: this._path,
|
|
2990
|
+
options: { ...this._options },
|
|
2991
|
+
hooks: this.hooks.clone(),
|
|
2992
|
+
plugins: this.plugins,
|
|
2993
|
+
router: this.router.clone()
|
|
2994
|
+
});
|
|
2995
|
+
for (const route of this._routes) {
|
|
2996
|
+
if (route.data.type === RouteEntryType.APP) {
|
|
2997
|
+
next.register({
|
|
2998
|
+
path: route.path,
|
|
2999
|
+
data: {
|
|
3000
|
+
type: RouteEntryType.APP,
|
|
3001
|
+
data: route.data.data.clone()
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
3004
|
+
continue;
|
|
3005
|
+
}
|
|
3006
|
+
next.register({
|
|
3007
|
+
path: route.path,
|
|
3008
|
+
method: route.method,
|
|
3009
|
+
data: {
|
|
3010
|
+
type: RouteEntryType.HANDLER,
|
|
3011
|
+
data: route.data.data
|
|
3012
|
+
}
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
return next;
|
|
3016
|
+
}
|
|
3017
|
+
on(name, fn, priority) {
|
|
3018
|
+
return this.hooks.addListener(name, fn, priority);
|
|
3019
|
+
}
|
|
3020
|
+
off(name, fn) {
|
|
3021
|
+
if (typeof fn === "undefined") {
|
|
3022
|
+
this.hooks.removeListener(name);
|
|
3023
|
+
return this;
|
|
3024
|
+
}
|
|
3025
|
+
this.hooks.removeListener(name, fn);
|
|
3026
|
+
return this;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
//#endregion
|
|
3030
|
+
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 };
|
|
3031
|
+
|
|
3032
|
+
//# sourceMappingURL=src-gmPicCWT.mjs.map
|