weifuwu 0.2.1 → 0.2.3
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 +168 -0
- package/dist/compress.d.ts +6 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1420 -0
- package/dist/middleware.d.ts +21 -0
- package/dist/rate-limit.d.ts +8 -0
- package/dist/router.d.ts +55 -0
- package/dist/serve.d.ts +19 -0
- package/dist/static.d.ts +7 -0
- package/dist/tsx.d.ts +17 -0
- package/dist/types.d.ts +9 -0
- package/dist/upload.d.ts +14 -0
- package/dist/validate.d.ts +9 -0
- package/package.json +14 -2
- package/AGENTS.md +0 -105
- package/compress.ts +0 -69
- package/cookie.ts +0 -58
- package/index.ts +0 -21
- package/middleware.ts +0 -178
- package/rate-limit.ts +0 -68
- package/router.ts +0 -702
- package/serve.ts +0 -126
- package/static.ts +0 -113
- package/test/compress.test.ts +0 -106
- package/test/cookie.test.ts +0 -79
- package/test/fixtures/pages/about/page.tsx +0 -3
- package/test/fixtures/pages/blog/[slug]/load.ts +0 -3
- package/test/fixtures/pages/blog/[slug]/page.tsx +0 -3
- package/test/fixtures/pages/blog/[slug]/route.ts +0 -7
- package/test/fixtures/pages/blog/layout.tsx +0 -3
- package/test/fixtures/pages/layout.tsx +0 -12
- package/test/fixtures/pages/page.tsx +0 -3
- package/test/middleware.test.ts +0 -407
- package/test/rate-limit.test.ts +0 -94
- package/test/static.test.ts +0 -93
- package/test/tsx.test.ts +0 -285
- package/test/unode.test.ts +0 -401
- package/test/upload.test.ts +0 -130
- package/test/validate.test.ts +0 -133
- package/tsconfig.json +0 -13
- package/tsx.ts +0 -354
- package/types.ts +0 -23
- package/upload.ts +0 -101
- package/validate.ts +0 -88
package/dist/index.js
ADDED
|
@@ -0,0 +1,1420 @@
|
|
|
1
|
+
// serve.ts
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
async function readBody(req) {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for await (const chunk of req) {
|
|
6
|
+
chunks.push(chunk);
|
|
7
|
+
}
|
|
8
|
+
return Buffer.concat(chunks);
|
|
9
|
+
}
|
|
10
|
+
function createRequest(req, body) {
|
|
11
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
12
|
+
const query = Object.fromEntries(url.searchParams);
|
|
13
|
+
const headers = {};
|
|
14
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
15
|
+
if (value !== void 0) {
|
|
16
|
+
headers[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const request = new Request(url.href, {
|
|
20
|
+
method: req.method?.toUpperCase() ?? "GET",
|
|
21
|
+
headers,
|
|
22
|
+
body: req.method !== "GET" && req.method !== "HEAD" && body.length > 0 ? body : null
|
|
23
|
+
});
|
|
24
|
+
return [request, query];
|
|
25
|
+
}
|
|
26
|
+
async function sendResponse(res, response) {
|
|
27
|
+
const headers = {};
|
|
28
|
+
response.headers.forEach((value, key) => {
|
|
29
|
+
headers[key] = value;
|
|
30
|
+
});
|
|
31
|
+
res.writeHead(response.status, response.statusText, headers);
|
|
32
|
+
if (response.body) {
|
|
33
|
+
const reader = response.body.getReader();
|
|
34
|
+
try {
|
|
35
|
+
while (true) {
|
|
36
|
+
const { done, value } = await reader.read();
|
|
37
|
+
if (done) break;
|
|
38
|
+
res.write(value);
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
reader.releaseLock();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
res.end();
|
|
45
|
+
}
|
|
46
|
+
function serve(handler, options) {
|
|
47
|
+
const port = options?.port ?? 0;
|
|
48
|
+
const hostname = options?.hostname ?? "0.0.0.0";
|
|
49
|
+
const server = http.createServer(async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const body = await readBody(req);
|
|
52
|
+
const [request, query] = createRequest(req, body);
|
|
53
|
+
const response = await handler(request, { params: {}, query });
|
|
54
|
+
await sendResponse(res, response);
|
|
55
|
+
} catch {
|
|
56
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
57
|
+
res.end("Internal Server Error");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (options?.websocket) {
|
|
61
|
+
server.on("upgrade", options.websocket);
|
|
62
|
+
}
|
|
63
|
+
let resolveReady;
|
|
64
|
+
const ready = new Promise((r) => {
|
|
65
|
+
resolveReady = r;
|
|
66
|
+
});
|
|
67
|
+
if (options?.signal) {
|
|
68
|
+
if (options.signal.aborted) {
|
|
69
|
+
server.close();
|
|
70
|
+
resolveReady();
|
|
71
|
+
return {
|
|
72
|
+
stop: () => {
|
|
73
|
+
},
|
|
74
|
+
ready,
|
|
75
|
+
get port() {
|
|
76
|
+
return 0;
|
|
77
|
+
},
|
|
78
|
+
get hostname() {
|
|
79
|
+
return hostname;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
options.signal.addEventListener("abort", () => {
|
|
84
|
+
server.close();
|
|
85
|
+
}, { once: true });
|
|
86
|
+
}
|
|
87
|
+
server.listen(port, hostname, () => {
|
|
88
|
+
resolveReady();
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
stop: () => {
|
|
92
|
+
server.close();
|
|
93
|
+
},
|
|
94
|
+
ready,
|
|
95
|
+
get port() {
|
|
96
|
+
const addr = server.address();
|
|
97
|
+
if (!addr || typeof addr === "string") return 0;
|
|
98
|
+
return addr.port;
|
|
99
|
+
},
|
|
100
|
+
get hostname() {
|
|
101
|
+
const addr = server.address();
|
|
102
|
+
if (!addr) return hostname;
|
|
103
|
+
return typeof addr === "string" ? addr : addr.address;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// router.ts
|
|
109
|
+
import { WebSocketServer } from "ws";
|
|
110
|
+
import { buildSchema, graphql } from "graphql";
|
|
111
|
+
import { makeExecutableSchema } from "@graphql-tools/schema";
|
|
112
|
+
import { streamText } from "ai";
|
|
113
|
+
var createTrieNode = () => ({
|
|
114
|
+
children: /* @__PURE__ */ new Map(),
|
|
115
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
116
|
+
middlewares: /* @__PURE__ */ new Map(),
|
|
117
|
+
pathMws: []
|
|
118
|
+
});
|
|
119
|
+
var createWsNode = () => ({
|
|
120
|
+
children: /* @__PURE__ */ new Map(),
|
|
121
|
+
middlewares: []
|
|
122
|
+
});
|
|
123
|
+
var getTrieNode = (node, segment) => {
|
|
124
|
+
if (segment.startsWith(":")) {
|
|
125
|
+
if (!node.children.has(":")) {
|
|
126
|
+
const child2 = createTrieNode();
|
|
127
|
+
child2.param = segment.slice(1);
|
|
128
|
+
node.children.set(":", child2);
|
|
129
|
+
}
|
|
130
|
+
const child = node.children.get(":");
|
|
131
|
+
if (child.param !== segment.slice(1)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Param name conflict: ":${child.param}" already registered at this path position, cannot register ":"${segment.slice(1)}"`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return child;
|
|
137
|
+
}
|
|
138
|
+
if (!node.children.has(segment)) {
|
|
139
|
+
node.children.set(segment, createTrieNode());
|
|
140
|
+
}
|
|
141
|
+
return node.children.get(segment);
|
|
142
|
+
};
|
|
143
|
+
var matchTrieNode = (node, segment, params) => {
|
|
144
|
+
if (node.children.has(segment)) return node.children.get(segment);
|
|
145
|
+
if (node.children.has(":")) {
|
|
146
|
+
const child = node.children.get(":");
|
|
147
|
+
if (child.param) params[child.param] = segment;
|
|
148
|
+
return child;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
var getWsNode = (node, segment) => {
|
|
153
|
+
if (segment.startsWith(":")) {
|
|
154
|
+
if (!node.children.has(":")) {
|
|
155
|
+
const child2 = createWsNode();
|
|
156
|
+
child2.param = segment.slice(1);
|
|
157
|
+
node.children.set(":", child2);
|
|
158
|
+
}
|
|
159
|
+
const child = node.children.get(":");
|
|
160
|
+
if (child.param !== segment.slice(1)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Param name conflict: ":${child.param}" already registered at this path position`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return child;
|
|
166
|
+
}
|
|
167
|
+
if (!node.children.has(segment)) {
|
|
168
|
+
node.children.set(segment, createWsNode());
|
|
169
|
+
}
|
|
170
|
+
return node.children.get(segment);
|
|
171
|
+
};
|
|
172
|
+
var matchWsNode = (node, segment, params) => {
|
|
173
|
+
if (node.children.has(segment)) return node.children.get(segment);
|
|
174
|
+
if (node.children.has(":")) {
|
|
175
|
+
const child = node.children.get(":");
|
|
176
|
+
if (child.param) params[child.param] = segment;
|
|
177
|
+
return child;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
var Router = class _Router {
|
|
182
|
+
root = createTrieNode();
|
|
183
|
+
wsRoot = createWsNode();
|
|
184
|
+
globalMws = [];
|
|
185
|
+
errorHandler;
|
|
186
|
+
use(arg1, arg2) {
|
|
187
|
+
if (typeof arg1 === "string") {
|
|
188
|
+
if (arg2 instanceof _Router) {
|
|
189
|
+
let node = this.root;
|
|
190
|
+
for (const segment of this.splitPath(arg1)) {
|
|
191
|
+
node = getTrieNode(node, segment);
|
|
192
|
+
}
|
|
193
|
+
node.subRouter = arg2;
|
|
194
|
+
} else if (typeof arg2 === "function") {
|
|
195
|
+
let node = this.root;
|
|
196
|
+
for (const segment of this.splitPath(arg1)) {
|
|
197
|
+
node = getTrieNode(node, segment);
|
|
198
|
+
}
|
|
199
|
+
node.pathMws.push(arg2);
|
|
200
|
+
}
|
|
201
|
+
} else if (typeof arg1 === "function") {
|
|
202
|
+
this.globalMws.push(arg1);
|
|
203
|
+
}
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
get(path, ...args) {
|
|
207
|
+
return this.route("GET", path, ...args);
|
|
208
|
+
}
|
|
209
|
+
post(path, ...args) {
|
|
210
|
+
return this.route("POST", path, ...args);
|
|
211
|
+
}
|
|
212
|
+
put(path, ...args) {
|
|
213
|
+
return this.route("PUT", path, ...args);
|
|
214
|
+
}
|
|
215
|
+
delete(path, ...args) {
|
|
216
|
+
return this.route("DELETE", path, ...args);
|
|
217
|
+
}
|
|
218
|
+
patch(path, ...args) {
|
|
219
|
+
return this.route("PATCH", path, ...args);
|
|
220
|
+
}
|
|
221
|
+
head(path, ...args) {
|
|
222
|
+
return this.route("HEAD", path, ...args);
|
|
223
|
+
}
|
|
224
|
+
options(path, ...args) {
|
|
225
|
+
return this.route("OPTIONS", path, ...args);
|
|
226
|
+
}
|
|
227
|
+
all(path, ...args) {
|
|
228
|
+
return this.route("*", path, ...args);
|
|
229
|
+
}
|
|
230
|
+
onError(handler) {
|
|
231
|
+
this.errorHandler = handler;
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
route(method, path, ...args) {
|
|
235
|
+
const handler = args.pop();
|
|
236
|
+
const middlewares = args;
|
|
237
|
+
const segments = this.splitPath(path);
|
|
238
|
+
let node = this.root;
|
|
239
|
+
for (const segment of segments) {
|
|
240
|
+
if (segment === "*") {
|
|
241
|
+
node.wildcard = true;
|
|
242
|
+
node.handlers.set(method, handler);
|
|
243
|
+
if (middlewares.length > 0) node.middlewares.set(method, middlewares);
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
node = getTrieNode(node, segment);
|
|
247
|
+
}
|
|
248
|
+
node.handlers.set(method, handler);
|
|
249
|
+
if (middlewares.length > 0) node.middlewares.set(method, middlewares);
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
ws(path, ...args) {
|
|
253
|
+
const handler = args.pop();
|
|
254
|
+
const middlewares = args;
|
|
255
|
+
const segments = this.splitPath(path);
|
|
256
|
+
let node = this.wsRoot;
|
|
257
|
+
for (const segment of segments) {
|
|
258
|
+
node = getWsNode(node, segment);
|
|
259
|
+
}
|
|
260
|
+
node.handler = handler;
|
|
261
|
+
if (middlewares.length > 0) node.middlewares = middlewares;
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
graphql(path, ...args) {
|
|
265
|
+
const options = args.pop();
|
|
266
|
+
const middlewares = args;
|
|
267
|
+
const schema = typeof options.schema === "string" ? options.resolvers ? makeExecutableSchema({
|
|
268
|
+
typeDefs: options.schema,
|
|
269
|
+
resolvers: options.resolvers
|
|
270
|
+
}) : buildSchema(options.schema) : options.schema;
|
|
271
|
+
const handler = (req, ctx) => {
|
|
272
|
+
const url = new URL(req.url);
|
|
273
|
+
if (options.graphiql && req.method === "GET" && !url.searchParams.has("query")) {
|
|
274
|
+
return new Response(getGraphiQLHtml(url.pathname), {
|
|
275
|
+
status: 200,
|
|
276
|
+
headers: { "Content-Type": "text/html" }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (req.method !== "GET" && req.method !== "POST") {
|
|
280
|
+
return new Response("Not Found", { status: 404 });
|
|
281
|
+
}
|
|
282
|
+
const paramsPromise = req.method === "GET" ? Promise.resolve(parseGraphQLParamsFromGet(url)) : parseGraphQLParamsFromPost(req);
|
|
283
|
+
return paramsPromise.then((params) => {
|
|
284
|
+
if (!params) {
|
|
285
|
+
return Response.json(
|
|
286
|
+
{ errors: [{ message: "Missing query" }] },
|
|
287
|
+
{ status: 400 }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return executeGraphQLQuery(schema, params, options, req, ctx);
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
return this.all(path, ...middlewares, handler);
|
|
294
|
+
}
|
|
295
|
+
ai(path, ...args) {
|
|
296
|
+
const handler = args.pop();
|
|
297
|
+
const middlewares = args;
|
|
298
|
+
const routeHandler = async (req, ctx) => {
|
|
299
|
+
const options = await handler(req, ctx);
|
|
300
|
+
const result = streamText(options);
|
|
301
|
+
return result.toTextStreamResponse();
|
|
302
|
+
};
|
|
303
|
+
return this.post(path, ...middlewares, routeHandler);
|
|
304
|
+
}
|
|
305
|
+
handler() {
|
|
306
|
+
return (req, ctx) => {
|
|
307
|
+
const url = new URL(req.url);
|
|
308
|
+
return this.handle(req, ctx, this.splitPath(url.pathname), Object.fromEntries(url.searchParams));
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
websocketHandler() {
|
|
312
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
313
|
+
const wsRoot = this.wsRoot;
|
|
314
|
+
const router = this;
|
|
315
|
+
return (req, socket, head) => {
|
|
316
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
317
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
318
|
+
const query = Object.fromEntries(url.searchParams);
|
|
319
|
+
const match = router.matchWsTrie(wsRoot, segments);
|
|
320
|
+
if (!match) {
|
|
321
|
+
socket.destroy();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const webReq = new Request(url.href, {
|
|
325
|
+
method: req.method ?? "GET",
|
|
326
|
+
headers: Object.fromEntries(
|
|
327
|
+
Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v ?? ""])
|
|
328
|
+
)
|
|
329
|
+
});
|
|
330
|
+
const ctx = { params: match.params, query };
|
|
331
|
+
if (match.middlewares.length === 0) {
|
|
332
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
let index = 0;
|
|
336
|
+
const dispatch = async (innerReq, ctx2) => {
|
|
337
|
+
if (index < match.middlewares.length) {
|
|
338
|
+
const mw = match.middlewares[index++];
|
|
339
|
+
return mw(innerReq, ctx2, dispatch);
|
|
340
|
+
}
|
|
341
|
+
return await new Promise((resolve3) => {
|
|
342
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx2);
|
|
343
|
+
resolve3(new Response(null, { status: 101 }));
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
347
|
+
if (result.status !== 101) {
|
|
348
|
+
sendHttpResponseOnSocket(socket, result);
|
|
349
|
+
}
|
|
350
|
+
}).catch(() => {
|
|
351
|
+
socket.destroy();
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
splitPath(path) {
|
|
356
|
+
return path.split("/").filter(Boolean);
|
|
357
|
+
}
|
|
358
|
+
matchTrie(method, segments) {
|
|
359
|
+
let node = this.root;
|
|
360
|
+
const params = {};
|
|
361
|
+
const pathMws = [...this.root.pathMws];
|
|
362
|
+
let wildcardHandler = null;
|
|
363
|
+
let wildcardMws = [];
|
|
364
|
+
let wildcardIdx = -1;
|
|
365
|
+
for (let i = 0; i < segments.length; i++) {
|
|
366
|
+
pathMws.push(...node.pathMws);
|
|
367
|
+
if (node.wildcard) {
|
|
368
|
+
const h = node.handlers.get(method) || node.handlers.get("*");
|
|
369
|
+
if (h) {
|
|
370
|
+
wildcardHandler = h;
|
|
371
|
+
wildcardMws = node.middlewares.get(method) || node.middlewares.get("*") || [];
|
|
372
|
+
wildcardIdx = i;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const segment = segments[i];
|
|
376
|
+
if (!segment) break;
|
|
377
|
+
const next = matchTrieNode(node, segment, params);
|
|
378
|
+
if (!next) {
|
|
379
|
+
if (node.subRouter) {
|
|
380
|
+
return {
|
|
381
|
+
pathMws,
|
|
382
|
+
params,
|
|
383
|
+
middlewares: [],
|
|
384
|
+
subRouter: { router: node.subRouter, remainingIdx: i }
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (wildcardHandler) {
|
|
388
|
+
params["*"] = segments.slice(wildcardIdx).join("/");
|
|
389
|
+
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params };
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
node = next;
|
|
394
|
+
}
|
|
395
|
+
if (node.subRouter) {
|
|
396
|
+
return {
|
|
397
|
+
pathMws,
|
|
398
|
+
params,
|
|
399
|
+
middlewares: [],
|
|
400
|
+
subRouter: { router: node.subRouter, remainingIdx: segments.length }
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
pathMws.push(...node.pathMws);
|
|
404
|
+
const handler = node.handlers.get(method) || node.handlers.get("*");
|
|
405
|
+
if (handler) {
|
|
406
|
+
if (node.wildcard) params["*"] = segments.slice(segments.length).join("/");
|
|
407
|
+
return {
|
|
408
|
+
handler,
|
|
409
|
+
middlewares: node.middlewares.get(method) || node.middlewares.get("*") || [],
|
|
410
|
+
pathMws,
|
|
411
|
+
params
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (wildcardHandler) {
|
|
415
|
+
params["*"] = segments.slice(wildcardIdx).join("/");
|
|
416
|
+
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params };
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
matchWsTrie(root, segments) {
|
|
421
|
+
let node = root;
|
|
422
|
+
const params = {};
|
|
423
|
+
for (const segment of segments) {
|
|
424
|
+
const next = matchWsNode(node, segment, params);
|
|
425
|
+
if (!next) return null;
|
|
426
|
+
node = next;
|
|
427
|
+
}
|
|
428
|
+
return node.handler ? { handler: node.handler, middlewares: node.middlewares, params } : null;
|
|
429
|
+
}
|
|
430
|
+
async handle(req, ctx, segments, query) {
|
|
431
|
+
const match = this.matchTrie(req.method, segments);
|
|
432
|
+
if (match?.subRouter) {
|
|
433
|
+
const { router: sub, remainingIdx } = match.subRouter;
|
|
434
|
+
const remainingSegments = segments.slice(remainingIdx);
|
|
435
|
+
const delegate = (req2, ctx2) => sub.handle(req2, ctx2, remainingSegments, query);
|
|
436
|
+
const allMws = this.globalMws.length + match.pathMws.length === 0 ? [] : [...this.globalMws, ...match.pathMws];
|
|
437
|
+
try {
|
|
438
|
+
return await this.runChain(allMws, delegate, req, { ...ctx, params: { ...ctx.params, ...match.params } });
|
|
439
|
+
} catch (e) {
|
|
440
|
+
return this.errorHandler ? this.errorHandler(e, req, ctx) : new Response("Internal Server Error", { status: 500 });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (match?.handler) {
|
|
444
|
+
const { handler, middlewares: routeMws, pathMws, params } = match;
|
|
445
|
+
const allMws = this.globalMws.length + pathMws.length + routeMws.length === 0 ? [] : [...this.globalMws, ...pathMws, ...routeMws];
|
|
446
|
+
const ctxWithMatch = { ...ctx, params: { ...ctx.params, ...params } };
|
|
447
|
+
try {
|
|
448
|
+
return await this.runChain(allMws, handler, req, ctxWithMatch);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return this.errorHandler ? this.errorHandler(e, req, ctxWithMatch) : new Response("Internal Server Error", { status: 500 });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (this.globalMws.length > 0) {
|
|
454
|
+
try {
|
|
455
|
+
const delegate = () => new Response("Not Found", { status: 404 });
|
|
456
|
+
return await this.runChain(this.globalMws, delegate, req, ctx);
|
|
457
|
+
} catch (e) {
|
|
458
|
+
return this.errorHandler ? this.errorHandler(e, req, ctx) : new Response("Internal Server Error", { status: 500 });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return new Response("Not Found", { status: 404 });
|
|
462
|
+
}
|
|
463
|
+
async runChain(middlewares, finalHandler, req, ctx) {
|
|
464
|
+
let index = 0;
|
|
465
|
+
const dispatch = async (req2, ctx2) => {
|
|
466
|
+
if (index < middlewares.length) {
|
|
467
|
+
const mw = middlewares[index++];
|
|
468
|
+
return mw ? await mw(req2, ctx2, dispatch) : new Response("Middleware error", { status: 500 });
|
|
469
|
+
}
|
|
470
|
+
return await finalHandler(req2, ctx2);
|
|
471
|
+
};
|
|
472
|
+
return dispatch(req, ctx);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
function upgradeSocket(wss, req, socket, head, handler, ctx) {
|
|
476
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
477
|
+
if (handler.open) {
|
|
478
|
+
handler.open(ws, ctx);
|
|
479
|
+
}
|
|
480
|
+
ws.on("message", (data) => {
|
|
481
|
+
handler.message?.(ws, ctx, data);
|
|
482
|
+
});
|
|
483
|
+
ws.on("close", () => {
|
|
484
|
+
handler.close?.(ws, ctx);
|
|
485
|
+
});
|
|
486
|
+
ws.on("error", (err) => {
|
|
487
|
+
handler.error?.(ws, ctx, err);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function sendHttpResponseOnSocket(socket, response) {
|
|
492
|
+
const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`;
|
|
493
|
+
const headerLines = [statusLine];
|
|
494
|
+
response.headers.forEach((value, key) => {
|
|
495
|
+
headerLines.push(`${key}: ${value}`);
|
|
496
|
+
});
|
|
497
|
+
headerLines.push("Connection: close");
|
|
498
|
+
headerLines.push("");
|
|
499
|
+
const headerStr = headerLines.join("\r\n");
|
|
500
|
+
response.arrayBuffer().then((buf) => {
|
|
501
|
+
const body = Buffer.from(buf);
|
|
502
|
+
socket.write(headerStr + "\r\n" + body.toString());
|
|
503
|
+
socket.end();
|
|
504
|
+
}).catch(() => {
|
|
505
|
+
socket.write(headerStr + "\r\n");
|
|
506
|
+
socket.end();
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function parseGraphQLParamsFromGet(url) {
|
|
510
|
+
const query = url.searchParams.get("query");
|
|
511
|
+
if (!query) return null;
|
|
512
|
+
const variablesStr = url.searchParams.get("variables");
|
|
513
|
+
let variables = {};
|
|
514
|
+
if (variablesStr) {
|
|
515
|
+
try {
|
|
516
|
+
variables = JSON.parse(variablesStr);
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
query,
|
|
523
|
+
variables,
|
|
524
|
+
operationName: url.searchParams.get("operationName") || void 0
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
async function parseGraphQLParamsFromPost(req) {
|
|
528
|
+
try {
|
|
529
|
+
const body = await req.json();
|
|
530
|
+
if (!body.query) return null;
|
|
531
|
+
return {
|
|
532
|
+
query: body.query,
|
|
533
|
+
variables: body.variables || {},
|
|
534
|
+
operationName: body.operationName
|
|
535
|
+
};
|
|
536
|
+
} catch {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function executeGraphQLQuery(schema, params, options, req, ctx) {
|
|
541
|
+
const contextValue = options.context ? await options.context(req, ctx) : ctx;
|
|
542
|
+
const result = await graphql({
|
|
543
|
+
schema,
|
|
544
|
+
source: params.query,
|
|
545
|
+
rootValue: options.rootValue,
|
|
546
|
+
contextValue,
|
|
547
|
+
variableValues: params.variables,
|
|
548
|
+
operationName: params.operationName
|
|
549
|
+
});
|
|
550
|
+
return Response.json(result, { status: result.errors ? 400 : 200 });
|
|
551
|
+
}
|
|
552
|
+
function getGraphiQLHtml(endpoint) {
|
|
553
|
+
return `<!doctype html>
|
|
554
|
+
<html lang="en">
|
|
555
|
+
<head>
|
|
556
|
+
<meta charset="UTF-8" />
|
|
557
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
558
|
+
<title>GraphiQL</title>
|
|
559
|
+
<style>
|
|
560
|
+
body { margin: 0; }
|
|
561
|
+
#graphiql { height: 100dvh; }
|
|
562
|
+
</style>
|
|
563
|
+
<link rel="stylesheet" href="https://esm.sh/graphiql@5.2.2/dist/style.css" />
|
|
564
|
+
<script type="importmap">
|
|
565
|
+
{
|
|
566
|
+
"imports": {
|
|
567
|
+
"react": "https://esm.sh/react@19.2.5",
|
|
568
|
+
"react/": "https://esm.sh/react@19.2.5/",
|
|
569
|
+
"react-dom": "https://esm.sh/react-dom@19.2.5",
|
|
570
|
+
"react-dom/": "https://esm.sh/react-dom@19.2.5/",
|
|
571
|
+
"graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
|
|
572
|
+
"graphiql/": "https://esm.sh/graphiql@5.2.2/",
|
|
573
|
+
"@graphiql/react": "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
|
|
574
|
+
"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql",
|
|
575
|
+
"graphql": "https://esm.sh/graphql@16.13.2",
|
|
576
|
+
"@emotion/is-prop-valid": "data:text/javascript,"
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
</script>
|
|
580
|
+
<script type="module">
|
|
581
|
+
import React from 'react';
|
|
582
|
+
import ReactDOM from 'react-dom/client';
|
|
583
|
+
import { GraphiQL } from 'graphiql';
|
|
584
|
+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
|
585
|
+
import 'graphiql/setup-workers/esm.sh';
|
|
586
|
+
|
|
587
|
+
const fetcher = createGraphiQLFetcher({ url: "${endpoint}" });
|
|
588
|
+
|
|
589
|
+
function App() {
|
|
590
|
+
return React.createElement(GraphiQL, { fetcher });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const container = document.getElementById('graphiql');
|
|
594
|
+
const root = ReactDOM.createRoot(container);
|
|
595
|
+
root.render(React.createElement(App));
|
|
596
|
+
</script>
|
|
597
|
+
</head>
|
|
598
|
+
<body>
|
|
599
|
+
<div id="graphiql">Loading\u2026</div>
|
|
600
|
+
</body>
|
|
601
|
+
</html>`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// tsx.ts
|
|
605
|
+
import { createElement, createContext, useContext } from "react";
|
|
606
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
607
|
+
import * as esbuild from "esbuild";
|
|
608
|
+
import { readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
|
|
609
|
+
import { join, relative, resolve, sep, dirname } from "node:path";
|
|
610
|
+
import { pathToFileURL } from "node:url";
|
|
611
|
+
import { createHash } from "node:crypto";
|
|
612
|
+
var TsxContext = createContext({ params: {}, query: {} });
|
|
613
|
+
function useTsx() {
|
|
614
|
+
return useContext(TsxContext);
|
|
615
|
+
}
|
|
616
|
+
function id(s) {
|
|
617
|
+
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
618
|
+
}
|
|
619
|
+
function concatUint8(chunks) {
|
|
620
|
+
const len = chunks.reduce((a, c) => a + c.length, 0);
|
|
621
|
+
const out = new Uint8Array(len);
|
|
622
|
+
let off = 0;
|
|
623
|
+
for (const c of chunks) {
|
|
624
|
+
out.set(c, off);
|
|
625
|
+
off += c.length;
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
async function readStream(stream) {
|
|
630
|
+
const chunks = [];
|
|
631
|
+
const reader = stream.getReader();
|
|
632
|
+
while (true) {
|
|
633
|
+
const { done, value } = await reader.read();
|
|
634
|
+
if (done) break;
|
|
635
|
+
chunks.push(value);
|
|
636
|
+
}
|
|
637
|
+
return new TextDecoder().decode(concatUint8(chunks));
|
|
638
|
+
}
|
|
639
|
+
function scanPages(dir) {
|
|
640
|
+
const pages = [];
|
|
641
|
+
function walk(current) {
|
|
642
|
+
let entries;
|
|
643
|
+
try {
|
|
644
|
+
entries = readdirSync(current);
|
|
645
|
+
} catch {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const dirs = [];
|
|
649
|
+
for (const name of entries) {
|
|
650
|
+
const full = join(current, name);
|
|
651
|
+
const st = statSync(full);
|
|
652
|
+
if (st.isDirectory()) {
|
|
653
|
+
if (!name.startsWith(".")) dirs.push(full);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const pagePath = join(current, "page.tsx");
|
|
657
|
+
const tsPagePath = join(current, "page.ts");
|
|
658
|
+
let entryPath = "";
|
|
659
|
+
if (existsSync(pagePath)) {
|
|
660
|
+
entryPath = pagePath;
|
|
661
|
+
} else if (existsSync(tsPagePath)) {
|
|
662
|
+
entryPath = tsPagePath;
|
|
663
|
+
}
|
|
664
|
+
if (entryPath) {
|
|
665
|
+
let relPath = relative(dir, entryPath).replace(sep, "/");
|
|
666
|
+
relPath = relPath.replace(/\/page\.tsx?$/, "");
|
|
667
|
+
relPath = relPath.replace(/^page\.tsx?$/, "");
|
|
668
|
+
const route = filePathToRoute(relPath);
|
|
669
|
+
const layouts = resolveLayouts(current, dir);
|
|
670
|
+
const loadPath = existsSync(join(current, "load.ts")) ? join(current, "load.ts") : void 0;
|
|
671
|
+
const rPath = existsSync(join(current, "route.ts")) ? join(current, "route.ts") : void 0;
|
|
672
|
+
pages.push({
|
|
673
|
+
route,
|
|
674
|
+
entryPath,
|
|
675
|
+
loadPath,
|
|
676
|
+
layouts,
|
|
677
|
+
routePath: rPath
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
for (const d of dirs) walk(d);
|
|
681
|
+
}
|
|
682
|
+
walk(dir);
|
|
683
|
+
return pages;
|
|
684
|
+
}
|
|
685
|
+
function filePathToRoute(relPath) {
|
|
686
|
+
let route = relPath.replace(/\\/g, "/");
|
|
687
|
+
route = route.replace(/\[\.\.\.(\w+)\]/g, "*");
|
|
688
|
+
route = route.replace(/\[(\w+)\]/g, ":$1");
|
|
689
|
+
return route.startsWith("/") ? route : "/" + route;
|
|
690
|
+
}
|
|
691
|
+
function resolveLayouts(dir, pagesDir) {
|
|
692
|
+
const layouts = [];
|
|
693
|
+
let current = dir;
|
|
694
|
+
while (current.startsWith(pagesDir)) {
|
|
695
|
+
const p = join(current, "layout.tsx");
|
|
696
|
+
if (existsSync(p)) {
|
|
697
|
+
layouts.push(p);
|
|
698
|
+
}
|
|
699
|
+
const parent = dirname(current);
|
|
700
|
+
if (parent === current) break;
|
|
701
|
+
current = parent;
|
|
702
|
+
}
|
|
703
|
+
return layouts.reverse();
|
|
704
|
+
}
|
|
705
|
+
async function compileAll(files, outDir, platform) {
|
|
706
|
+
const entryPoints = {};
|
|
707
|
+
for (const f of files) {
|
|
708
|
+
entryPoints[id(f)] = f;
|
|
709
|
+
}
|
|
710
|
+
const isBrowser = platform === "browser";
|
|
711
|
+
await esbuild.build({
|
|
712
|
+
entryPoints,
|
|
713
|
+
outdir: outDir,
|
|
714
|
+
format: "esm",
|
|
715
|
+
platform: "node",
|
|
716
|
+
jsx: "automatic",
|
|
717
|
+
jsxImportSource: "react",
|
|
718
|
+
bundle: true,
|
|
719
|
+
external: isBrowser ? void 0 : [
|
|
720
|
+
"react",
|
|
721
|
+
"react-dom",
|
|
722
|
+
"esbuild",
|
|
723
|
+
"graphql",
|
|
724
|
+
"ws",
|
|
725
|
+
"zod",
|
|
726
|
+
"@graphql-tools/schema",
|
|
727
|
+
"ai"
|
|
728
|
+
],
|
|
729
|
+
write: true,
|
|
730
|
+
allowOverwrite: true
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function compiledUrl(filePath, outDir) {
|
|
734
|
+
const hash = id(join(outDir, id(filePath)));
|
|
735
|
+
const p = join(outDir, id(filePath) + ".js");
|
|
736
|
+
return pathToFileURL(p).href;
|
|
737
|
+
}
|
|
738
|
+
var clientBundleCache = /* @__PURE__ */ new Map();
|
|
739
|
+
var clientRouteLog = /* @__PURE__ */ new WeakMap();
|
|
740
|
+
async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
|
|
741
|
+
const key = id(entryPath);
|
|
742
|
+
const url = `/__wfw/client/${key}.js`;
|
|
743
|
+
if (!clientRouteLog.get(router)?.has(url)) {
|
|
744
|
+
let buf = clientBundleCache.get(key);
|
|
745
|
+
if (!buf) {
|
|
746
|
+
try {
|
|
747
|
+
const nested = layoutPaths.slice(1);
|
|
748
|
+
const layoutsImport = nested.map(
|
|
749
|
+
(p, i) => `import L${i} from${JSON.stringify(p)};`
|
|
750
|
+
).join("");
|
|
751
|
+
const layoutsWrap = nested.map((_, i) => {
|
|
752
|
+
const idx = nested.length - 1 - i;
|
|
753
|
+
return `el=createElement(L${idx},null,el);`;
|
|
754
|
+
}).join("");
|
|
755
|
+
const code = [
|
|
756
|
+
`import{hydrateRoot}from'react-dom/client';`,
|
|
757
|
+
`import{createElement}from'react';`,
|
|
758
|
+
`import P from${JSON.stringify(entryPath)};`,
|
|
759
|
+
layoutsImport,
|
|
760
|
+
`const p=window.__WEIFUWU_PROPS;`,
|
|
761
|
+
`let el=createElement(P,p);`,
|
|
762
|
+
layoutsWrap,
|
|
763
|
+
`hydrateRoot(document.getElementById('__weifuwu_root'),el);`
|
|
764
|
+
].join("");
|
|
765
|
+
const result = await esbuild.build({
|
|
766
|
+
stdin: { contents: code, loader: "tsx", resolveDir: pagesDir },
|
|
767
|
+
bundle: true,
|
|
768
|
+
format: "esm",
|
|
769
|
+
jsx: "automatic",
|
|
770
|
+
jsxImportSource: "react",
|
|
771
|
+
write: false,
|
|
772
|
+
minify: true
|
|
773
|
+
});
|
|
774
|
+
buf = result.outputFiles[0].contents;
|
|
775
|
+
clientBundleCache.set(key, buf);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error("hydration bundle failed:", err);
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
router.get(url, () => new Response(buf, {
|
|
782
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
783
|
+
}));
|
|
784
|
+
const set = clientRouteLog.get(router) ?? /* @__PURE__ */ new Set();
|
|
785
|
+
set.add(url);
|
|
786
|
+
clientRouteLog.set(router, set);
|
|
787
|
+
}
|
|
788
|
+
return { url };
|
|
789
|
+
}
|
|
790
|
+
function makeSsrHandler(Component, loadFn, layouts, entryPath, layoutPaths, pagesDir, router) {
|
|
791
|
+
return async (req, ctx) => {
|
|
792
|
+
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
793
|
+
const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
|
|
794
|
+
let element = createElement(Component, allProps);
|
|
795
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
796
|
+
const isRoot = i === 0;
|
|
797
|
+
element = createElement(
|
|
798
|
+
layouts[i],
|
|
799
|
+
isRoot ? { children: element, req, ctx } : { children: element }
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
element = createElement(TsxContext.Provider, {
|
|
803
|
+
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
804
|
+
}, element);
|
|
805
|
+
const stream = await renderToReadableStream(element);
|
|
806
|
+
const body = await readStream(stream);
|
|
807
|
+
const scripts = [];
|
|
808
|
+
scripts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(allProps)}</script>`);
|
|
809
|
+
const bundle = await getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router);
|
|
810
|
+
if (bundle) {
|
|
811
|
+
scripts.push(`<script type="module" src="${bundle.url}"></script>`);
|
|
812
|
+
}
|
|
813
|
+
const html = `<!DOCTYPE html>
|
|
814
|
+
${body}
|
|
815
|
+
${scripts.join("\n")}`;
|
|
816
|
+
return new Response(html, {
|
|
817
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
async function tsx(options) {
|
|
822
|
+
const pagesDir = resolve(options.dir);
|
|
823
|
+
const outDir = join(pagesDir, "..", ".weifuwu", "ssr");
|
|
824
|
+
const clientDir = join(pagesDir, "..", ".weifuwu", "client");
|
|
825
|
+
const pages = scanPages(pagesDir);
|
|
826
|
+
if (pages.length === 0) return new Router();
|
|
827
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
828
|
+
const loadMap = /* @__PURE__ */ new Map();
|
|
829
|
+
const layoutMap = /* @__PURE__ */ new Map();
|
|
830
|
+
for (const p of pages) {
|
|
831
|
+
allFiles.add(p.entryPath);
|
|
832
|
+
if (p.loadPath) {
|
|
833
|
+
allFiles.add(p.loadPath);
|
|
834
|
+
loadMap.set(p.entryPath, p.loadPath);
|
|
835
|
+
}
|
|
836
|
+
for (const lp of p.layouts) allFiles.add(lp);
|
|
837
|
+
layoutMap.set(p.entryPath, [...p.layouts]);
|
|
838
|
+
if (p.routePath) allFiles.add(p.routePath);
|
|
839
|
+
}
|
|
840
|
+
mkdirSync(outDir, { recursive: true });
|
|
841
|
+
await compileAll([...allFiles], outDir, "node");
|
|
842
|
+
const router = new Router();
|
|
843
|
+
for (const p of pages) {
|
|
844
|
+
const url = compiledUrl(p.entryPath, outDir);
|
|
845
|
+
const mod = await import(url);
|
|
846
|
+
const Component = mod.default;
|
|
847
|
+
let loadFn;
|
|
848
|
+
if (p.loadPath) {
|
|
849
|
+
const loadUrl = compiledUrl(p.loadPath, outDir);
|
|
850
|
+
const modLoad = await import(loadUrl);
|
|
851
|
+
loadFn = modLoad.default;
|
|
852
|
+
}
|
|
853
|
+
const layoutComponents = [];
|
|
854
|
+
for (const lp of p.layouts) {
|
|
855
|
+
const lUrl = compiledUrl(lp, outDir);
|
|
856
|
+
const modL = await import(lUrl);
|
|
857
|
+
layoutComponents.push(modL.default);
|
|
858
|
+
}
|
|
859
|
+
const handler = makeSsrHandler(
|
|
860
|
+
Component,
|
|
861
|
+
loadFn,
|
|
862
|
+
layoutComponents,
|
|
863
|
+
p.entryPath,
|
|
864
|
+
p.layouts,
|
|
865
|
+
pagesDir,
|
|
866
|
+
router
|
|
867
|
+
);
|
|
868
|
+
router.get(p.route, handler);
|
|
869
|
+
if (p.routePath) {
|
|
870
|
+
const rUrl = compiledUrl(p.routePath, outDir);
|
|
871
|
+
const modR = await import(rUrl);
|
|
872
|
+
const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
873
|
+
for (const method of methods) {
|
|
874
|
+
if (modR[method]) {
|
|
875
|
+
router.route(method, p.route, modR[method]);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return router;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// middleware.ts
|
|
884
|
+
function logger(options) {
|
|
885
|
+
return async (req, ctx, next) => {
|
|
886
|
+
const start = Date.now();
|
|
887
|
+
const url = new URL(req.url);
|
|
888
|
+
const res = await next(req, ctx);
|
|
889
|
+
const ms = Date.now() - start;
|
|
890
|
+
if (options?.format === "combined") {
|
|
891
|
+
console.log(`${req.method} ${url.pathname}${url.search} ${res.status} ${ms}ms`);
|
|
892
|
+
} else {
|
|
893
|
+
console.log(`${req.method} ${url.pathname} ${res.status} ${ms}ms`);
|
|
894
|
+
}
|
|
895
|
+
return res;
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function cors(options) {
|
|
899
|
+
const opts = {
|
|
900
|
+
origin: "*",
|
|
901
|
+
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
|
|
902
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
903
|
+
...options
|
|
904
|
+
};
|
|
905
|
+
function resolveOrigin(requestOrigin) {
|
|
906
|
+
if (typeof opts.origin === "string") return opts.origin === "*" ? "*" : opts.origin;
|
|
907
|
+
if (Array.isArray(opts.origin)) {
|
|
908
|
+
return opts.origin.includes(requestOrigin) ? requestOrigin : "";
|
|
909
|
+
}
|
|
910
|
+
const result = opts.origin(requestOrigin);
|
|
911
|
+
if (typeof result === "boolean") return result ? requestOrigin : "";
|
|
912
|
+
if (typeof result === "string") return result;
|
|
913
|
+
return "";
|
|
914
|
+
}
|
|
915
|
+
function setCORSHeaders(res, acao) {
|
|
916
|
+
if (!acao) return res;
|
|
917
|
+
const headers = new Headers(res.headers);
|
|
918
|
+
headers.set("Access-Control-Allow-Origin", acao);
|
|
919
|
+
if (opts.credentials) headers.set("Access-Control-Allow-Credentials", "true");
|
|
920
|
+
if (opts.exposedHeaders?.length) headers.set("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
|
|
921
|
+
headers.set("Vary", "Origin");
|
|
922
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
923
|
+
}
|
|
924
|
+
return (req, ctx, next) => {
|
|
925
|
+
const requestOrigin = req.headers.get("origin") ?? "";
|
|
926
|
+
const acao = resolveOrigin(requestOrigin);
|
|
927
|
+
if (req.method === "OPTIONS" && acao) {
|
|
928
|
+
const headers = new Headers();
|
|
929
|
+
headers.set("Access-Control-Allow-Origin", acao);
|
|
930
|
+
headers.set("Access-Control-Allow-Methods", opts.methods.join(", "));
|
|
931
|
+
headers.set("Access-Control-Allow-Headers", opts.allowedHeaders.join(", "));
|
|
932
|
+
if (opts.credentials) headers.set("Access-Control-Allow-Credentials", "true");
|
|
933
|
+
if (opts.maxAge != null) headers.set("Access-Control-Max-Age", String(opts.maxAge));
|
|
934
|
+
headers.set("Vary", "Origin");
|
|
935
|
+
return new Response(null, { status: 204, headers });
|
|
936
|
+
}
|
|
937
|
+
if (!acao) return next(req, ctx);
|
|
938
|
+
return Promise.resolve(next(req, ctx)).then((res) => setCORSHeaders(res, acao));
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function auth(options) {
|
|
942
|
+
return async (req, ctx, next) => {
|
|
943
|
+
const headerName = options.header ?? "Authorization";
|
|
944
|
+
const header = req.headers.get(headerName);
|
|
945
|
+
if (!header) {
|
|
946
|
+
return new Response("Unauthorized", {
|
|
947
|
+
status: 401,
|
|
948
|
+
headers: headerName.toLowerCase() === "authorization" ? { "WWW-Authenticate": "Bearer" } : void 0
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
let token = header;
|
|
952
|
+
if (headerName.toLowerCase() === "authorization") {
|
|
953
|
+
const parts = header.split(" ");
|
|
954
|
+
if (parts[0]?.toLowerCase() === "bearer") {
|
|
955
|
+
token = parts.slice(1).join(" ");
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (options.proxy) {
|
|
959
|
+
const proxyUrl = typeof options.proxy === "string" ? new URL(options.proxy) : options.proxy;
|
|
960
|
+
const proxyHeaders = {};
|
|
961
|
+
if (headerName.toLowerCase() === "authorization") {
|
|
962
|
+
proxyHeaders["Authorization"] = header;
|
|
963
|
+
} else {
|
|
964
|
+
proxyUrl.searchParams.set("access_token", token);
|
|
965
|
+
}
|
|
966
|
+
for (const name of ["x-forwarded-for", "x-real-ip", "user-agent", "content-type"]) {
|
|
967
|
+
const v = req.headers.get(name);
|
|
968
|
+
if (v) proxyHeaders[name] = v;
|
|
969
|
+
}
|
|
970
|
+
const proxyRes = await fetch(proxyUrl.href, { headers: proxyHeaders });
|
|
971
|
+
if (proxyRes.status >= 400) {
|
|
972
|
+
return new Response(await proxyRes.text() || "Forbidden", { status: proxyRes.status });
|
|
973
|
+
}
|
|
974
|
+
let userData = void 0;
|
|
975
|
+
if (proxyRes.status === 200) {
|
|
976
|
+
const ct = proxyRes.headers.get("content-type");
|
|
977
|
+
if (ct?.includes("application/json")) {
|
|
978
|
+
try {
|
|
979
|
+
userData = await proxyRes.json();
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
ctx.user = userData;
|
|
985
|
+
return next(req, ctx);
|
|
986
|
+
}
|
|
987
|
+
if (options.token) {
|
|
988
|
+
if (token !== options.token) {
|
|
989
|
+
return new Response("Forbidden", { status: 403 });
|
|
990
|
+
}
|
|
991
|
+
return next(req, ctx);
|
|
992
|
+
}
|
|
993
|
+
if (options.verify) {
|
|
994
|
+
const result = await options.verify(token, req);
|
|
995
|
+
if (!result) {
|
|
996
|
+
return new Response("Forbidden", { status: 403 });
|
|
997
|
+
}
|
|
998
|
+
if (typeof result === "object" && result !== null) {
|
|
999
|
+
ctx.user = result;
|
|
1000
|
+
}
|
|
1001
|
+
return next(req, ctx);
|
|
1002
|
+
}
|
|
1003
|
+
return next(req, ctx);
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// static.ts
|
|
1008
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1009
|
+
import { open } from "node:fs/promises";
|
|
1010
|
+
import { extname, resolve as resolve2, normalize, sep as sep2 } from "node:path";
|
|
1011
|
+
function serveStatic(root, options) {
|
|
1012
|
+
const rootDir = resolve2(root);
|
|
1013
|
+
const opts = options ?? {};
|
|
1014
|
+
return async (req, ctx) => {
|
|
1015
|
+
const relativePath = ctx.params["*"] ?? new URL(req.url).pathname.slice(1);
|
|
1016
|
+
const decoded = decodeURIComponent(relativePath);
|
|
1017
|
+
if (decoded.includes("..") || decoded.includes("\0")) {
|
|
1018
|
+
return new Response("Forbidden", { status: 403 });
|
|
1019
|
+
}
|
|
1020
|
+
let filePath = normalize(resolve2(rootDir, decoded));
|
|
1021
|
+
if (!filePath.startsWith(rootDir + sep2) && filePath !== rootDir) {
|
|
1022
|
+
return new Response("Forbidden", { status: 403 });
|
|
1023
|
+
}
|
|
1024
|
+
let fileHandle;
|
|
1025
|
+
try {
|
|
1026
|
+
fileHandle = await open(filePath, "r");
|
|
1027
|
+
const stat = await fileHandle.stat();
|
|
1028
|
+
if (stat.isDirectory()) {
|
|
1029
|
+
await fileHandle.close();
|
|
1030
|
+
const indexFile = opts.index ?? "index.html";
|
|
1031
|
+
filePath = resolve2(filePath, indexFile);
|
|
1032
|
+
if (!filePath.startsWith(rootDir + sep2)) {
|
|
1033
|
+
return new Response("Forbidden", { status: 403 });
|
|
1034
|
+
}
|
|
1035
|
+
fileHandle = await open(filePath, "r");
|
|
1036
|
+
const dirStat = await fileHandle.stat();
|
|
1037
|
+
if (!dirStat.isFile()) {
|
|
1038
|
+
await fileHandle.close();
|
|
1039
|
+
return new Response("Not Found", { status: 404 });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const mimeType = MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
1043
|
+
const etag = `"${createHash2("md5").update(`${stat.size}-${stat.mtimeMs}`).digest("hex")}"`;
|
|
1044
|
+
const ifNoneMatch = req.headers.get("if-none-match");
|
|
1045
|
+
if (ifNoneMatch === etag) {
|
|
1046
|
+
await fileHandle.close();
|
|
1047
|
+
return new Response(null, { status: 304 });
|
|
1048
|
+
}
|
|
1049
|
+
const ifModifiedSince = req.headers.get("if-modified-since");
|
|
1050
|
+
if (ifModifiedSince && stat.mtimeMs <= new Date(ifModifiedSince).getTime()) {
|
|
1051
|
+
await fileHandle.close();
|
|
1052
|
+
return new Response(null, { status: 304 });
|
|
1053
|
+
}
|
|
1054
|
+
const headers = {
|
|
1055
|
+
"Content-Type": mimeType,
|
|
1056
|
+
"Content-Length": String(stat.size),
|
|
1057
|
+
"ETag": etag,
|
|
1058
|
+
"Last-Modified": stat.mtime.toUTCString(),
|
|
1059
|
+
"Cache-Control": opts.immutable ? `public, max-age=${opts.maxAge ?? 31536e3}, immutable` : `public, max-age=${opts.maxAge ?? 0}`
|
|
1060
|
+
};
|
|
1061
|
+
const stream = fileHandle.readableWebStream();
|
|
1062
|
+
return new Response(stream, { headers });
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
if (fileHandle) await fileHandle.close().catch(() => {
|
|
1065
|
+
});
|
|
1066
|
+
if (err?.code === "ENOENT") {
|
|
1067
|
+
return new Response("Not Found", { status: 404 });
|
|
1068
|
+
}
|
|
1069
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
var MIME_TYPES = {
|
|
1074
|
+
".html": "text/html; charset=utf-8",
|
|
1075
|
+
".htm": "text/html; charset=utf-8",
|
|
1076
|
+
".css": "text/css; charset=utf-8",
|
|
1077
|
+
".js": "application/javascript; charset=utf-8",
|
|
1078
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
1079
|
+
".json": "application/json",
|
|
1080
|
+
".png": "image/png",
|
|
1081
|
+
".jpg": "image/jpeg",
|
|
1082
|
+
".jpeg": "image/jpeg",
|
|
1083
|
+
".gif": "image/gif",
|
|
1084
|
+
".svg": "image/svg+xml",
|
|
1085
|
+
".ico": "image/x-icon",
|
|
1086
|
+
".webp": "image/webp",
|
|
1087
|
+
".avif": "image/avif",
|
|
1088
|
+
".woff": "font/woff",
|
|
1089
|
+
".woff2": "font/woff2",
|
|
1090
|
+
".ttf": "font/ttf",
|
|
1091
|
+
".otf": "font/otf",
|
|
1092
|
+
".eot": "application/vnd.ms-fontobject",
|
|
1093
|
+
".txt": "text/plain; charset=utf-8",
|
|
1094
|
+
".xml": "application/xml",
|
|
1095
|
+
".pdf": "application/pdf",
|
|
1096
|
+
".zip": "application/zip",
|
|
1097
|
+
".wasm": "application/wasm",
|
|
1098
|
+
".map": "application/json"
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
// validate.ts
|
|
1102
|
+
function validate(schemas) {
|
|
1103
|
+
return async (req, ctx, next) => {
|
|
1104
|
+
const parsed = {};
|
|
1105
|
+
const issues = [];
|
|
1106
|
+
if (schemas.params) {
|
|
1107
|
+
const result = schemas.params.safeParse(ctx.params);
|
|
1108
|
+
if (result.success) {
|
|
1109
|
+
parsed.params = result.data;
|
|
1110
|
+
} else {
|
|
1111
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1112
|
+
path: ["params", ...i.path.map(String)],
|
|
1113
|
+
message: i.message
|
|
1114
|
+
})));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (schemas.query) {
|
|
1118
|
+
const result = schemas.query.safeParse(ctx.query);
|
|
1119
|
+
if (result.success) {
|
|
1120
|
+
parsed.query = result.data;
|
|
1121
|
+
} else {
|
|
1122
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1123
|
+
path: ["query", ...i.path.map(String)],
|
|
1124
|
+
message: i.message
|
|
1125
|
+
})));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (schemas.headers) {
|
|
1129
|
+
const rawHeaders = {};
|
|
1130
|
+
req.headers.forEach((v, k) => {
|
|
1131
|
+
rawHeaders[k] = v;
|
|
1132
|
+
});
|
|
1133
|
+
const result = schemas.headers.safeParse(rawHeaders);
|
|
1134
|
+
if (result.success) {
|
|
1135
|
+
parsed.headers = result.data;
|
|
1136
|
+
} else {
|
|
1137
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1138
|
+
path: ["headers", ...i.path.map(String)],
|
|
1139
|
+
message: i.message
|
|
1140
|
+
})));
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (schemas.body) {
|
|
1144
|
+
if (req.body === null) {
|
|
1145
|
+
issues.push({ path: ["body"], message: "Request body is required" });
|
|
1146
|
+
} else {
|
|
1147
|
+
const bodyText = await req.text();
|
|
1148
|
+
if (!bodyText && req.method !== "GET" && req.method !== "HEAD") {
|
|
1149
|
+
issues.push({ path: ["body"], message: "Request body is required" });
|
|
1150
|
+
} else {
|
|
1151
|
+
let bodyValue;
|
|
1152
|
+
try {
|
|
1153
|
+
bodyValue = JSON.parse(bodyText);
|
|
1154
|
+
} catch {
|
|
1155
|
+
bodyValue = bodyText;
|
|
1156
|
+
}
|
|
1157
|
+
const result = schemas.body.safeParse(bodyValue);
|
|
1158
|
+
if (result.success) {
|
|
1159
|
+
parsed.body = result.data;
|
|
1160
|
+
} else {
|
|
1161
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1162
|
+
path: ["body", ...i.path.map(String)],
|
|
1163
|
+
message: i.message
|
|
1164
|
+
})));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (issues.length > 0) {
|
|
1170
|
+
return Response.json({ error: "Validation failed", issues }, { status: 400 });
|
|
1171
|
+
}
|
|
1172
|
+
ctx.parsed = parsed;
|
|
1173
|
+
return next(req, ctx);
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// cookie.ts
|
|
1178
|
+
function getCookies(req) {
|
|
1179
|
+
const header = req.headers.get("cookie");
|
|
1180
|
+
if (!header) return {};
|
|
1181
|
+
const cookies = {};
|
|
1182
|
+
for (const pair of header.split(";")) {
|
|
1183
|
+
const idx = pair.indexOf("=");
|
|
1184
|
+
if (idx === -1) continue;
|
|
1185
|
+
const name = pair.slice(0, idx).trim();
|
|
1186
|
+
const value = pair.slice(idx + 1).trim();
|
|
1187
|
+
if (name) {
|
|
1188
|
+
cookies[name] = decodeURIComponent(value);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return cookies;
|
|
1192
|
+
}
|
|
1193
|
+
function serializeCookie(name, value, options) {
|
|
1194
|
+
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
1195
|
+
if (options?.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
|
|
1196
|
+
if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
1197
|
+
if (options?.domain) parts.push(`Domain=${options.domain}`);
|
|
1198
|
+
if (options?.path) parts.push(`Path=${options.path}`);
|
|
1199
|
+
if (options?.httpOnly) parts.push("HttpOnly");
|
|
1200
|
+
if (options?.secure) parts.push("Secure");
|
|
1201
|
+
if (options?.sameSite) parts.push(`SameSite=${options.sameSite}`);
|
|
1202
|
+
return parts.join("; ");
|
|
1203
|
+
}
|
|
1204
|
+
function setCookie(res, name, value, options) {
|
|
1205
|
+
const headers = new Headers(res.headers);
|
|
1206
|
+
headers.append("Set-Cookie", serializeCookie(name, value, options));
|
|
1207
|
+
return new Response(res.body, {
|
|
1208
|
+
status: res.status,
|
|
1209
|
+
statusText: res.statusText,
|
|
1210
|
+
headers
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
function deleteCookie(res, name, options) {
|
|
1214
|
+
const headers = new Headers(res.headers);
|
|
1215
|
+
headers.append("Set-Cookie", serializeCookie(name, "", { ...options, maxAge: 0 }));
|
|
1216
|
+
return new Response(res.body, {
|
|
1217
|
+
status: res.status,
|
|
1218
|
+
statusText: res.statusText,
|
|
1219
|
+
headers
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// upload.ts
|
|
1224
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
1225
|
+
import { randomUUID } from "node:crypto";
|
|
1226
|
+
import { join as join2 } from "node:path";
|
|
1227
|
+
function upload(options) {
|
|
1228
|
+
const saveDir = options?.dir;
|
|
1229
|
+
return async (req, ctx, next) => {
|
|
1230
|
+
const ct = req.headers.get("content-type") ?? "";
|
|
1231
|
+
if (!ct.includes("multipart/form-data")) {
|
|
1232
|
+
return next(req, ctx);
|
|
1233
|
+
}
|
|
1234
|
+
const match = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
1235
|
+
if (!match) {
|
|
1236
|
+
return Response.json({ error: "Missing boundary" }, { status: 400 });
|
|
1237
|
+
}
|
|
1238
|
+
const boundary = match[1] ?? match[2];
|
|
1239
|
+
const body = await req.text();
|
|
1240
|
+
const rawParts = body.split(`--${boundary}`).filter((p) => p && !p.startsWith("--") && !p.startsWith("\r\n--"));
|
|
1241
|
+
const files = {};
|
|
1242
|
+
const fields = {};
|
|
1243
|
+
for (const raw of rawParts) {
|
|
1244
|
+
const trimmed = raw.replace(/^\r?\n/, "");
|
|
1245
|
+
const lines = trimmed.split(/\r?\n/);
|
|
1246
|
+
let i = 0;
|
|
1247
|
+
const headers = {};
|
|
1248
|
+
while (i < lines.length && lines[i].length > 0) {
|
|
1249
|
+
const sep3 = lines[i].indexOf(": ");
|
|
1250
|
+
if (sep3 !== -1) headers[lines[i].slice(0, sep3).toLowerCase()] = lines[i].slice(sep3 + 2);
|
|
1251
|
+
i++;
|
|
1252
|
+
}
|
|
1253
|
+
i++;
|
|
1254
|
+
const bodyValue = lines.slice(i).join("\r\n");
|
|
1255
|
+
const disposition = headers["content-disposition"] ?? "";
|
|
1256
|
+
const nameMatch = disposition.match(/name="([^"]*)"/);
|
|
1257
|
+
if (!nameMatch) continue;
|
|
1258
|
+
const name = nameMatch[1];
|
|
1259
|
+
const filenameMatch = disposition.match(/filename="([^"]*)"/);
|
|
1260
|
+
const filename = filenameMatch?.[1];
|
|
1261
|
+
if (filename) {
|
|
1262
|
+
const buf = Buffer.from(bodyValue.replace(/\r?\n$/, ""), "binary");
|
|
1263
|
+
if (options?.allowedTypes) {
|
|
1264
|
+
const mime = headers["content-type"] ?? "application/octet-stream";
|
|
1265
|
+
if (!options.allowedTypes.includes(mime)) {
|
|
1266
|
+
return Response.json({ error: `File type not allowed: ${mime}` }, { status: 415 });
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (options?.maxFileSize && buf.byteLength > options.maxFileSize) {
|
|
1270
|
+
return Response.json({ error: `File too large: ${filename}` }, { status: 413 });
|
|
1271
|
+
}
|
|
1272
|
+
const uf = {
|
|
1273
|
+
name: filename,
|
|
1274
|
+
type: headers["content-type"] ?? "application/octet-stream",
|
|
1275
|
+
size: buf.byteLength,
|
|
1276
|
+
buffer: saveDir ? void 0 : buf
|
|
1277
|
+
};
|
|
1278
|
+
if (saveDir) {
|
|
1279
|
+
const filePath = join2(saveDir, `${randomUUID()}-${filename}`);
|
|
1280
|
+
await mkdir(saveDir, { recursive: true });
|
|
1281
|
+
await writeFile(filePath, buf);
|
|
1282
|
+
uf.path = filePath;
|
|
1283
|
+
}
|
|
1284
|
+
if (files[name]) {
|
|
1285
|
+
const existing = files[name];
|
|
1286
|
+
files[name] = Array.isArray(existing) ? [...existing, uf] : [existing, uf];
|
|
1287
|
+
} else {
|
|
1288
|
+
files[name] = uf;
|
|
1289
|
+
}
|
|
1290
|
+
} else {
|
|
1291
|
+
fields[name] = bodyValue.replace(/\r?\n$/, "");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
ctx.parsed = { ...ctx.parsed, files, fields };
|
|
1295
|
+
return next(req, ctx);
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// rate-limit.ts
|
|
1300
|
+
function rateLimit(options) {
|
|
1301
|
+
const max = options?.max ?? 100;
|
|
1302
|
+
const window = options?.window ?? 6e4;
|
|
1303
|
+
const getKey = options?.key ?? ((req) => {
|
|
1304
|
+
const forwarded = req.headers.get("x-forwarded-for");
|
|
1305
|
+
if (forwarded) return forwarded.split(",")[0].trim();
|
|
1306
|
+
return new URL(req.url).hostname;
|
|
1307
|
+
});
|
|
1308
|
+
const message = options?.message ?? "Too Many Requests";
|
|
1309
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1310
|
+
const interval = setInterval(() => {
|
|
1311
|
+
const now = Date.now();
|
|
1312
|
+
for (const [key, entry] of hits) {
|
|
1313
|
+
if (entry.reset < now) hits.delete(key);
|
|
1314
|
+
}
|
|
1315
|
+
}, window);
|
|
1316
|
+
if (interval.unref) interval.unref();
|
|
1317
|
+
return async (req, ctx, next) => {
|
|
1318
|
+
const key = getKey(req);
|
|
1319
|
+
const now = Date.now();
|
|
1320
|
+
const entry = hits.get(key);
|
|
1321
|
+
if (!entry || entry.reset < now) {
|
|
1322
|
+
hits.set(key, { count: 1, reset: now + window });
|
|
1323
|
+
const res2 = await next(req, ctx);
|
|
1324
|
+
const headers2 = new Headers(res2.headers);
|
|
1325
|
+
headers2.set("X-RateLimit-Limit", String(max));
|
|
1326
|
+
headers2.set("X-RateLimit-Remaining", String(max - 1));
|
|
1327
|
+
headers2.set("X-RateLimit-Reset", String(Math.ceil((now + window) / 1e3)));
|
|
1328
|
+
return new Response(res2.body, { status: res2.status, statusText: res2.statusText, headers: headers2 });
|
|
1329
|
+
}
|
|
1330
|
+
entry.count++;
|
|
1331
|
+
const remaining = Math.max(0, max - entry.count);
|
|
1332
|
+
if (entry.count > max) {
|
|
1333
|
+
return new Response(message, {
|
|
1334
|
+
status: 429,
|
|
1335
|
+
headers: {
|
|
1336
|
+
"Retry-After": String(Math.ceil((entry.reset - now) / 1e3)),
|
|
1337
|
+
"X-RateLimit-Limit": String(max),
|
|
1338
|
+
"X-RateLimit-Remaining": "0",
|
|
1339
|
+
"X-RateLimit-Reset": String(Math.ceil(entry.reset / 1e3))
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
const res = await next(req, ctx);
|
|
1344
|
+
const headers = new Headers(res.headers);
|
|
1345
|
+
headers.set("X-RateLimit-Limit", String(max));
|
|
1346
|
+
headers.set("X-RateLimit-Remaining", String(remaining));
|
|
1347
|
+
headers.set("X-RateLimit-Reset", String(Math.ceil(entry.reset / 1e3)));
|
|
1348
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// compress.ts
|
|
1353
|
+
import { gzipSync, brotliCompressSync, constants } from "node:zlib";
|
|
1354
|
+
function compress(options) {
|
|
1355
|
+
const level = options?.level ?? 6;
|
|
1356
|
+
const threshold = options?.threshold ?? 1024;
|
|
1357
|
+
return async (req, ctx, next) => {
|
|
1358
|
+
const accept = req.headers.get("accept-encoding") ?? "";
|
|
1359
|
+
const useBrotli = accept.includes("br");
|
|
1360
|
+
const useGzip = !useBrotli && accept.includes("gzip");
|
|
1361
|
+
const useDeflate = !useBrotli && !useGzip && accept.includes("deflate");
|
|
1362
|
+
if (!useBrotli && !useGzip && !useDeflate) {
|
|
1363
|
+
return next(req, ctx);
|
|
1364
|
+
}
|
|
1365
|
+
const res = await next(req, ctx);
|
|
1366
|
+
if (res.status === 304 || res.status === 204 || res.status < 200 || res.status >= 300) {
|
|
1367
|
+
return res;
|
|
1368
|
+
}
|
|
1369
|
+
const ce = res.headers.get("content-encoding");
|
|
1370
|
+
if (ce) return res;
|
|
1371
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
1372
|
+
if (!ct || ct.startsWith("audio/") || ct.startsWith("video/") || ct.startsWith("image/") || ct === "application/zip") {
|
|
1373
|
+
return res;
|
|
1374
|
+
}
|
|
1375
|
+
const body = await res.bytes();
|
|
1376
|
+
if (body.byteLength < threshold) return res;
|
|
1377
|
+
let compressed;
|
|
1378
|
+
let encoding;
|
|
1379
|
+
if (useBrotli) {
|
|
1380
|
+
compressed = brotliCompressSync(body, {
|
|
1381
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) }
|
|
1382
|
+
});
|
|
1383
|
+
encoding = "br";
|
|
1384
|
+
} else if (useGzip) {
|
|
1385
|
+
compressed = gzipSync(body, { level: Math.min(level, 9) });
|
|
1386
|
+
encoding = "gzip";
|
|
1387
|
+
} else {
|
|
1388
|
+
compressed = gzipSync(body, { level: Math.min(level, 9) });
|
|
1389
|
+
encoding = "deflate";
|
|
1390
|
+
}
|
|
1391
|
+
const headers = new Headers(res.headers);
|
|
1392
|
+
headers.set("Content-Encoding", encoding);
|
|
1393
|
+
headers.set("Content-Length", String(compressed.byteLength));
|
|
1394
|
+
headers.delete("Content-Range");
|
|
1395
|
+
headers.set("Vary", "Accept-Encoding");
|
|
1396
|
+
return new Response(compressed, {
|
|
1397
|
+
status: res.status,
|
|
1398
|
+
statusText: res.statusText,
|
|
1399
|
+
headers
|
|
1400
|
+
});
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
export {
|
|
1404
|
+
Router,
|
|
1405
|
+
TsxContext,
|
|
1406
|
+
auth,
|
|
1407
|
+
compress,
|
|
1408
|
+
cors,
|
|
1409
|
+
deleteCookie,
|
|
1410
|
+
getCookies,
|
|
1411
|
+
logger,
|
|
1412
|
+
rateLimit,
|
|
1413
|
+
serve,
|
|
1414
|
+
serveStatic,
|
|
1415
|
+
setCookie,
|
|
1416
|
+
tsx,
|
|
1417
|
+
upload,
|
|
1418
|
+
useTsx,
|
|
1419
|
+
validate
|
|
1420
|
+
};
|