weifuwu 0.31.1 → 0.32.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 +238 -120
- package/dist/ai/agent.d.ts +127 -0
- package/dist/ai/index.d.ts +26 -0
- package/dist/ai/types.d.ts +59 -0
- package/dist/core/router.d.ts +11 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +1186 -315
- package/dist/middleware/auth.d.ts +68 -0
- package/dist/middleware/esbuild-dev.d.ts +69 -0
- package/dist/middleware/esbuild-dev.js +335 -0
- package/dist/middleware/sandbox.d.ts +52 -0
- package/dist/middleware/tailwind-dev.d.ts +43 -0
- package/dist/middleware/tailwind-dev.js +199 -0
- package/dist/react/client.d.ts +64 -27
- package/dist/react/client.js +130 -248
- package/dist/react/compile.d.ts +17 -0
- package/dist/react/error-boundary.d.ts +18 -19
- package/dist/react/index.d.ts +80 -16
- package/dist/react/index.js +835 -267
- package/dist/react/types.d.ts +134 -34
- package/dist/types.d.ts +7 -0
- package/package.json +17 -6
- package/dist/react/navigation.d.ts +0 -63
- package/dist/react/navigation.js +0 -154
- package/dist/react/render.d.ts +0 -8
- package/dist/react/route-utils.d.ts +0 -31
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ var HttpError = class extends Error {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/core/trace.ts
|
|
12
|
-
import
|
|
12
|
+
import crypto2 from "node:crypto";
|
|
13
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
14
|
var als = new AsyncLocalStorage();
|
|
15
15
|
function currentTraceId() {
|
|
@@ -19,7 +19,7 @@ function currentTrace() {
|
|
|
19
19
|
return als.getStore();
|
|
20
20
|
}
|
|
21
21
|
function runWithTrace(incomingTraceId, fn) {
|
|
22
|
-
const traceId = incomingTraceId ||
|
|
22
|
+
const traceId = incomingTraceId || crypto2.randomUUID();
|
|
23
23
|
const startTime = Date.now();
|
|
24
24
|
return als.run({ traceId, startTime }, fn);
|
|
25
25
|
}
|
|
@@ -30,7 +30,7 @@ function traceElapsed() {
|
|
|
30
30
|
}
|
|
31
31
|
function trace(options) {
|
|
32
32
|
const header = options?.header ?? "X-Request-ID";
|
|
33
|
-
const gen = options?.generator ?? (() =>
|
|
33
|
+
const gen = options?.generator ?? (() => crypto2.randomUUID());
|
|
34
34
|
return async (req, ctx, next) => {
|
|
35
35
|
const existing = req.headers.get(header);
|
|
36
36
|
const requestId = existing ?? gen();
|
|
@@ -225,14 +225,14 @@ function serve(router, options) {
|
|
|
225
225
|
if (!server.listening) return;
|
|
226
226
|
server.close();
|
|
227
227
|
server.closeIdleConnections();
|
|
228
|
-
return new Promise((
|
|
228
|
+
return new Promise((resolve6) => {
|
|
229
229
|
const timer = setTimeout(() => {
|
|
230
230
|
server.closeAllConnections();
|
|
231
|
-
|
|
231
|
+
resolve6();
|
|
232
232
|
}, timeoutMs);
|
|
233
233
|
server.on("close", () => {
|
|
234
234
|
clearTimeout(timer);
|
|
235
|
-
|
|
235
|
+
resolve6();
|
|
236
236
|
});
|
|
237
237
|
});
|
|
238
238
|
}
|
|
@@ -277,7 +277,7 @@ function createHub(opts) {
|
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
279
|
}
|
|
280
|
-
function
|
|
280
|
+
function join4(key, ws) {
|
|
281
281
|
if (!channels.has(key)) {
|
|
282
282
|
channels.set(key, /* @__PURE__ */ new Set());
|
|
283
283
|
redisSub?.subscribe(`${prefix}${key}`);
|
|
@@ -339,7 +339,7 @@ function createHub(opts) {
|
|
|
339
339
|
redisPub = void 0;
|
|
340
340
|
redisSub = null;
|
|
341
341
|
}
|
|
342
|
-
return { join:
|
|
342
|
+
return { join: join4, leave, broadcast, close };
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
// src/core/ws.ts
|
|
@@ -503,6 +503,20 @@ var Router = class _Router {
|
|
|
503
503
|
this._checkMiddlewareMeta(mw, "global");
|
|
504
504
|
return this;
|
|
505
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Install a plugin — a function that configures the Router with
|
|
508
|
+
* routes, middleware, and error handlers. Use when `.use()` isn't
|
|
509
|
+
* enough because you need to call `app.get()`, `app.onError()`, etc.
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* ```ts
|
|
513
|
+
* app.plugin(app => createReactApp(app, { pages: {...}, layout: '...', tailwind: {...} }))
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
plugin(fn) {
|
|
517
|
+
fn(this);
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
506
520
|
mount(path, router) {
|
|
507
521
|
this._mountRouter(path, router);
|
|
508
522
|
return this;
|
|
@@ -892,6 +906,78 @@ function cors(options) {
|
|
|
892
906
|
};
|
|
893
907
|
}
|
|
894
908
|
|
|
909
|
+
// src/middleware/auth.ts
|
|
910
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
911
|
+
function base64urlDecode(str) {
|
|
912
|
+
return Buffer.from(str.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
|
|
913
|
+
}
|
|
914
|
+
function sign(payload, secret) {
|
|
915
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
916
|
+
}
|
|
917
|
+
function parseCookie(cookieHeader, name) {
|
|
918
|
+
if (!cookieHeader) return null;
|
|
919
|
+
for (const c of cookieHeader.split(";")) {
|
|
920
|
+
const [key, ...rest] = c.trim().split("=");
|
|
921
|
+
if (key === name) return rest.join("=");
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
function auth(opts) {
|
|
926
|
+
return async (req, ctx, next) => {
|
|
927
|
+
if (opts.jwt) {
|
|
928
|
+
const cookie = opts.jwt.cookie ?? "token";
|
|
929
|
+
const token = parseCookie(req.headers.get("cookie"), cookie) ?? req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
930
|
+
if (token) {
|
|
931
|
+
try {
|
|
932
|
+
const [headerB64, payloadB64, sigB64] = token.split(".");
|
|
933
|
+
const expectedSig = sign(`${headerB64}.${payloadB64}`, opts.jwt.secret);
|
|
934
|
+
const sigBuf = Buffer.from(sigB64, "base64url");
|
|
935
|
+
const expectedBuf = Buffer.from(expectedSig, "base64url");
|
|
936
|
+
if (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf)) {
|
|
937
|
+
const payload = JSON.parse(base64urlDecode(payloadB64));
|
|
938
|
+
ctx.user = {
|
|
939
|
+
id: payload.sub ?? payload.id ?? "unknown",
|
|
940
|
+
role: payload.role,
|
|
941
|
+
tenant: payload.tenant,
|
|
942
|
+
...payload
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (opts.session && !ctx.user) {
|
|
950
|
+
const cookie = opts.session.cookie ?? "session";
|
|
951
|
+
const raw = parseCookie(req.headers.get("cookie"), cookie);
|
|
952
|
+
if (raw) {
|
|
953
|
+
try {
|
|
954
|
+
const [dataB64, sigB64] = raw.split(".");
|
|
955
|
+
const expectedSig = sign(dataB64, opts.session.secret);
|
|
956
|
+
const sigBuf = Buffer.from(sigB64, "base64url");
|
|
957
|
+
const expectedBuf = Buffer.from(expectedSig, "base64url");
|
|
958
|
+
if (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf)) {
|
|
959
|
+
const data = JSON.parse(base64urlDecode(dataB64));
|
|
960
|
+
const user = await opts.session.loadUser(data);
|
|
961
|
+
if (user) ctx.user = user;
|
|
962
|
+
}
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (opts.apiKey && !ctx.user) {
|
|
968
|
+
const header = opts.apiKey.header ?? "authorization";
|
|
969
|
+
const prefix = opts.apiKey.prefix ?? "Bearer ";
|
|
970
|
+
const raw = req.headers.get(header);
|
|
971
|
+
if (raw?.startsWith(prefix)) {
|
|
972
|
+
const key = raw.slice(prefix.length);
|
|
973
|
+
const user = await opts.apiKey.validate(key);
|
|
974
|
+
if (user) ctx.user = user;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return next(req, ctx);
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
895
981
|
// src/middleware/static.ts
|
|
896
982
|
import { open, realpath } from "node:fs/promises";
|
|
897
983
|
import { extname, resolve, normalize, sep } from "node:path";
|
|
@@ -912,13 +998,13 @@ function serveStatic(root, options) {
|
|
|
912
998
|
let fileHandle;
|
|
913
999
|
try {
|
|
914
1000
|
fileHandle = await open(filePath, "r");
|
|
915
|
-
let
|
|
1001
|
+
let stat3 = await fileHandle.stat();
|
|
916
1002
|
const realPath = await realpath(filePath);
|
|
917
1003
|
if (!realPath.startsWith(rootDir + sep) && realPath !== rootDir) {
|
|
918
1004
|
await fileHandle.close();
|
|
919
1005
|
return new Response("Forbidden", { status: 403 });
|
|
920
1006
|
}
|
|
921
|
-
if (
|
|
1007
|
+
if (stat3.isDirectory()) {
|
|
922
1008
|
await fileHandle.close();
|
|
923
1009
|
const indexFile = opts.index ?? "index.html";
|
|
924
1010
|
filePath = resolve(filePath, indexFile);
|
|
@@ -926,29 +1012,29 @@ function serveStatic(root, options) {
|
|
|
926
1012
|
return new Response("Forbidden", { status: 403 });
|
|
927
1013
|
}
|
|
928
1014
|
fileHandle = await open(filePath, "r");
|
|
929
|
-
|
|
930
|
-
if (!
|
|
1015
|
+
stat3 = await fileHandle.stat();
|
|
1016
|
+
if (!stat3.isFile()) {
|
|
931
1017
|
await fileHandle.close();
|
|
932
1018
|
return new Response("Not Found", { status: 404 });
|
|
933
1019
|
}
|
|
934
1020
|
}
|
|
935
1021
|
const mimeType = MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
936
|
-
const etag = `"${
|
|
1022
|
+
const etag = `"${stat3.ino}-${stat3.size}-${stat3.mtimeMs}"`;
|
|
937
1023
|
const ifNoneMatch = req.headers.get("if-none-match");
|
|
938
1024
|
if (ifNoneMatch === etag) {
|
|
939
1025
|
await fileHandle.close();
|
|
940
1026
|
return new Response(null, { status: 304 });
|
|
941
1027
|
}
|
|
942
1028
|
const ifModifiedSince = req.headers.get("if-modified-since");
|
|
943
|
-
if (ifModifiedSince &&
|
|
1029
|
+
if (ifModifiedSince && stat3.mtimeMs <= new Date(ifModifiedSince).getTime()) {
|
|
944
1030
|
await fileHandle.close();
|
|
945
1031
|
return new Response(null, { status: 304 });
|
|
946
1032
|
}
|
|
947
1033
|
const headers = {
|
|
948
1034
|
"Content-Type": mimeType,
|
|
949
|
-
"Content-Length": String(
|
|
1035
|
+
"Content-Length": String(stat3.size),
|
|
950
1036
|
ETag: etag,
|
|
951
|
-
"Last-Modified":
|
|
1037
|
+
"Last-Modified": stat3.mtime.toUTCString(),
|
|
952
1038
|
"Cache-Control": opts.immutable ? `public, max-age=${opts.maxAge ?? 31536e3}, immutable` : `public, max-age=${opts.maxAge ?? 0}`
|
|
953
1039
|
};
|
|
954
1040
|
const readStream = fileHandle.createReadStream();
|
|
@@ -1099,6 +1185,604 @@ function upload(options) {
|
|
|
1099
1185
|
return mw;
|
|
1100
1186
|
}
|
|
1101
1187
|
|
|
1188
|
+
// src/middleware/sandbox.ts
|
|
1189
|
+
import { join as join2, resolve as resolve2, normalize as normalize2 } from "node:path";
|
|
1190
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2, rm } from "node:fs/promises";
|
|
1191
|
+
import { exec as cpExec } from "node:child_process";
|
|
1192
|
+
import { promisify } from "node:util";
|
|
1193
|
+
var runExec = promisify(cpExec);
|
|
1194
|
+
function sandbox(opts = {}) {
|
|
1195
|
+
const base = resolve2(opts.baseDir ?? "/tmp/weifuwu-sandboxes");
|
|
1196
|
+
const defaultTimeout = opts.timeout ?? 3e4;
|
|
1197
|
+
return async (req, ctx, next) => {
|
|
1198
|
+
let sessionId;
|
|
1199
|
+
if (opts.isolateBy === "user" && ctx.user) {
|
|
1200
|
+
sessionId = ctx.user.id ?? "default";
|
|
1201
|
+
} else {
|
|
1202
|
+
sessionId = crypto.randomUUID();
|
|
1203
|
+
}
|
|
1204
|
+
const workDir = join2(base, encodeURIComponent(sessionId));
|
|
1205
|
+
await mkdir2(workDir, { recursive: true });
|
|
1206
|
+
function safePath(p) {
|
|
1207
|
+
const resolved = resolve2(workDir, normalize2(p).replace(/^[/\\]+/, ""));
|
|
1208
|
+
if (!resolved.startsWith(workDir + "/") && resolved !== workDir) {
|
|
1209
|
+
throw new Error(`Path escape rejected: ${p}`);
|
|
1210
|
+
}
|
|
1211
|
+
return resolved;
|
|
1212
|
+
}
|
|
1213
|
+
ctx.sandbox = {
|
|
1214
|
+
workDir,
|
|
1215
|
+
async readFile(path) {
|
|
1216
|
+
return readFile(safePath(path), "utf-8");
|
|
1217
|
+
},
|
|
1218
|
+
async writeFile(path, content) {
|
|
1219
|
+
const resolved = safePath(path);
|
|
1220
|
+
await mkdir2(join2(resolved, ".."), { recursive: true });
|
|
1221
|
+
return writeFile2(resolved, content, "utf-8");
|
|
1222
|
+
},
|
|
1223
|
+
async exec(cmd, execOpts) {
|
|
1224
|
+
const cwd = execOpts?.cwd ? safePath(execOpts.cwd) : workDir;
|
|
1225
|
+
const { stdout, stderr } = await runExec(cmd, {
|
|
1226
|
+
cwd,
|
|
1227
|
+
timeout: execOpts?.timeout ?? defaultTimeout,
|
|
1228
|
+
env: {
|
|
1229
|
+
...process.env,
|
|
1230
|
+
HOME: workDir,
|
|
1231
|
+
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"
|
|
1232
|
+
},
|
|
1233
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1234
|
+
// 10MB
|
|
1235
|
+
});
|
|
1236
|
+
return { stdout, stderr };
|
|
1237
|
+
},
|
|
1238
|
+
async grep(pattern) {
|
|
1239
|
+
const { stdout } = await runExec(
|
|
1240
|
+
`grep -rn --include='*' "${pattern.replace(/"/g, '\\"')}" . 2>/dev/null || true`,
|
|
1241
|
+
{ cwd: workDir, timeout: 1e4 }
|
|
1242
|
+
);
|
|
1243
|
+
return stdout;
|
|
1244
|
+
},
|
|
1245
|
+
async destroy() {
|
|
1246
|
+
await rm(workDir, { recursive: true, force: true });
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
try {
|
|
1250
|
+
return await next(req, ctx);
|
|
1251
|
+
} finally {
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/middleware/esbuild-dev.ts
|
|
1257
|
+
import { resolve as resolve3, relative, dirname } from "node:path";
|
|
1258
|
+
import { stat, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
|
|
1259
|
+
function escapeHtml(s) {
|
|
1260
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1261
|
+
}
|
|
1262
|
+
function defaultErrorTemplate(errors) {
|
|
1263
|
+
return `<!DOCTYPE html>
|
|
1264
|
+
<html>
|
|
1265
|
+
<head>
|
|
1266
|
+
<meta charset="utf-8">
|
|
1267
|
+
<title>Build Error</title>
|
|
1268
|
+
<style>
|
|
1269
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1270
|
+
body { font-family: system-ui; background: #1a1a2e; color: #e0e0e0; padding: 2rem; }
|
|
1271
|
+
.overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
1272
|
+
.card { background: #16213e; border: 1px solid #e74c3c; border-radius: 12px; padding: 2rem; max-width: 800px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
|
1273
|
+
h1 { color: #e74c3c; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
1274
|
+
pre { background: #0f0f23; padding: 1.5rem; border-radius: 8px; overflow: auto; font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
1275
|
+
</style>
|
|
1276
|
+
</head>
|
|
1277
|
+
<body>
|
|
1278
|
+
<div class="overlay">
|
|
1279
|
+
<div class="card">
|
|
1280
|
+
<h1>\u26A0 esbuild Build Error</h1>
|
|
1281
|
+
<pre>${escapeHtml(errors)}</pre>
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
</body>
|
|
1285
|
+
</html>`;
|
|
1286
|
+
}
|
|
1287
|
+
async function collectDeps(entryPath) {
|
|
1288
|
+
const files = /* @__PURE__ */ new Map();
|
|
1289
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1290
|
+
const queue2 = [entryPath];
|
|
1291
|
+
while (queue2.length > 0) {
|
|
1292
|
+
const p = queue2.shift();
|
|
1293
|
+
try {
|
|
1294
|
+
const real = await realpath2(p);
|
|
1295
|
+
if (visited.has(real)) continue;
|
|
1296
|
+
visited.add(real);
|
|
1297
|
+
const s = await stat(real);
|
|
1298
|
+
files.set(real, s.mtimeMs);
|
|
1299
|
+
} catch {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
const content = await readFile2(p, "utf-8");
|
|
1304
|
+
const importRe = /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g;
|
|
1305
|
+
let m;
|
|
1306
|
+
while ((m = importRe.exec(content)) !== null) {
|
|
1307
|
+
const spec = m[1] ?? m[2];
|
|
1308
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
1309
|
+
const dir = dirname(p);
|
|
1310
|
+
const resolved = resolve3(dir, spec);
|
|
1311
|
+
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"]) {
|
|
1312
|
+
const candidate = resolved + ext;
|
|
1313
|
+
if (!visited.has(candidate)) {
|
|
1314
|
+
queue2.push(candidate);
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
queue2.push(resolved);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
} catch {
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return files;
|
|
1326
|
+
}
|
|
1327
|
+
function esbuildDev(opts) {
|
|
1328
|
+
const cache = opts.cache ?? "memory";
|
|
1329
|
+
const importmap = opts.importmap ?? false;
|
|
1330
|
+
const importmapOverrides = opts.importmapOverrides ?? {};
|
|
1331
|
+
const errorTemplate = opts.errorTemplate ?? defaultErrorTemplate;
|
|
1332
|
+
const entries = Object.entries(opts.entries).map(([p, e]) => {
|
|
1333
|
+
const path = p.startsWith("/") ? p : "/" + p;
|
|
1334
|
+
const config = typeof e === "string" ? { entry: e } : e;
|
|
1335
|
+
return { path, config };
|
|
1336
|
+
});
|
|
1337
|
+
const cacheStore = /* @__PURE__ */ new Map();
|
|
1338
|
+
const chunkMap = /* @__PURE__ */ new Map();
|
|
1339
|
+
let esbuild_ = null;
|
|
1340
|
+
let esbuildLoadError = null;
|
|
1341
|
+
async function getEsbuild() {
|
|
1342
|
+
if (esbuild_) return esbuild_;
|
|
1343
|
+
try {
|
|
1344
|
+
esbuild_ = await import("esbuild");
|
|
1345
|
+
return esbuild_;
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
esbuildLoadError = `esbuild is not installed. Run: npm install -D esbuild
|
|
1348
|
+
|
|
1349
|
+
${String(err)}`;
|
|
1350
|
+
throw new Error(esbuildLoadError, { cause: err });
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
let importmapScript = null;
|
|
1354
|
+
function buildImportmap() {
|
|
1355
|
+
const mappings = {};
|
|
1356
|
+
Object.assign(mappings, importmapOverrides);
|
|
1357
|
+
for (const { path, config } of entries) {
|
|
1358
|
+
const entryName = path.split("/").pop() ?? "";
|
|
1359
|
+
if (entryName.includes("vendor") || config.external && config.external.length === 0 || !config.external && config.bundle !== false) {
|
|
1360
|
+
if (!mappings["react"]) mappings["react"] = path;
|
|
1361
|
+
if (!mappings["react/jsx-runtime"]) mappings["react/jsx-runtime"] = path;
|
|
1362
|
+
if (!mappings["react-dom/client"]) mappings["react-dom/client"] = path;
|
|
1363
|
+
if (!mappings["react-dom"]) mappings["react-dom"] = path;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const json = JSON.stringify({ imports: mappings });
|
|
1367
|
+
return `<script type="importmap">${json}</script>`;
|
|
1368
|
+
}
|
|
1369
|
+
async function compile(entry) {
|
|
1370
|
+
const esbuild = await getEsbuild();
|
|
1371
|
+
const isClientRouter = !!entry.clientRouter;
|
|
1372
|
+
let entryAbs;
|
|
1373
|
+
let buildOpts;
|
|
1374
|
+
if (isClientRouter) {
|
|
1375
|
+
const cr = entry.clientRouter;
|
|
1376
|
+
const virtualModule = "weifuwu:client-entry";
|
|
1377
|
+
entryAbs = virtualModule;
|
|
1378
|
+
const layoutImport = `import * as _layout from '${cr.layout}'
|
|
1379
|
+
const _lx = Object.entries(_layout).filter(([,v]) => typeof v === 'function')
|
|
1380
|
+
const Layout = _layout.default || (_lx[0]?.[1])`;
|
|
1381
|
+
let routesCode;
|
|
1382
|
+
if (cr.pages) {
|
|
1383
|
+
const entries2 = Object.entries(cr.pages).map(
|
|
1384
|
+
([path, component]) => ` '${path}': () => import('${component}'),`
|
|
1385
|
+
);
|
|
1386
|
+
routesCode = `const routes = {
|
|
1387
|
+
${entries2.join("\n")}
|
|
1388
|
+
}`;
|
|
1389
|
+
} else if (cr.routes) {
|
|
1390
|
+
routesCode = `import { routes } from '${cr.routes}'`;
|
|
1391
|
+
} else {
|
|
1392
|
+
routesCode = "const routes = {}";
|
|
1393
|
+
}
|
|
1394
|
+
const fallbackLine = cr.fallback ? `const fallback = () => import('${cr.fallback}')` : "";
|
|
1395
|
+
const fallbackOpt = cr.fallback ? " fallback," : "";
|
|
1396
|
+
const generatedCode = [
|
|
1397
|
+
`import { createBrowserRouter } from 'weifuwu/react/client'`,
|
|
1398
|
+
layoutImport,
|
|
1399
|
+
routesCode,
|
|
1400
|
+
fallbackLine,
|
|
1401
|
+
"",
|
|
1402
|
+
"createBrowserRouter({",
|
|
1403
|
+
" layout: Layout,",
|
|
1404
|
+
" routes,",
|
|
1405
|
+
fallbackOpt,
|
|
1406
|
+
"})"
|
|
1407
|
+
].filter(Boolean).join("\n");
|
|
1408
|
+
buildOpts = {
|
|
1409
|
+
entryPoints: [virtualModule],
|
|
1410
|
+
bundle: entry.bundle ?? true,
|
|
1411
|
+
external: entry.external ?? [],
|
|
1412
|
+
minify: entry.minify ?? true,
|
|
1413
|
+
platform: entry.platform ?? "browser",
|
|
1414
|
+
format: entry.format ?? "esm",
|
|
1415
|
+
sourcemap: entry.sourcemap ?? false,
|
|
1416
|
+
splitting: entry.splitting ?? false,
|
|
1417
|
+
...entry.splitting ? { outdir: resolve3(".weifuwu-esbuild-out") } : {},
|
|
1418
|
+
define: entry.define,
|
|
1419
|
+
loader: entry.loader,
|
|
1420
|
+
write: false,
|
|
1421
|
+
logLevel: "silent",
|
|
1422
|
+
plugins: [{
|
|
1423
|
+
name: "weifuwu-client-router",
|
|
1424
|
+
setup(build) {
|
|
1425
|
+
build.onResolve({ filter: new RegExp(`^${virtualModule.replace(/:/g, "\\:")}$`) }, () => ({
|
|
1426
|
+
path: virtualModule,
|
|
1427
|
+
namespace: "weifuwu-client"
|
|
1428
|
+
}));
|
|
1429
|
+
build.onLoad({ filter: /.*/, namespace: "weifuwu-client" }, () => ({
|
|
1430
|
+
contents: generatedCode,
|
|
1431
|
+
loader: "ts",
|
|
1432
|
+
resolveDir: process.cwd()
|
|
1433
|
+
}));
|
|
1434
|
+
}
|
|
1435
|
+
}]
|
|
1436
|
+
};
|
|
1437
|
+
} else {
|
|
1438
|
+
entryAbs = resolve3(entry.entry);
|
|
1439
|
+
buildOpts = {
|
|
1440
|
+
entryPoints: [entryAbs],
|
|
1441
|
+
bundle: entry.bundle ?? true,
|
|
1442
|
+
external: entry.external ?? [],
|
|
1443
|
+
minify: entry.minify ?? true,
|
|
1444
|
+
platform: entry.platform ?? "browser",
|
|
1445
|
+
format: entry.format ?? "esm",
|
|
1446
|
+
sourcemap: entry.sourcemap ?? false,
|
|
1447
|
+
splitting: entry.splitting ?? false,
|
|
1448
|
+
...entry.splitting ? { outdir: dirname(entryAbs) } : {},
|
|
1449
|
+
define: entry.define,
|
|
1450
|
+
loader: entry.loader,
|
|
1451
|
+
write: false,
|
|
1452
|
+
logLevel: "silent"
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const result = await esbuild.build(buildOpts);
|
|
1456
|
+
const msgs = [];
|
|
1457
|
+
for (const w of result.warnings) {
|
|
1458
|
+
const loc = w.location ? ` at ${relative(".", w.location.file ?? "")}:${w.location.line}:${w.location.column}` : "";
|
|
1459
|
+
msgs.push(`[warn] ${w.text}${loc}`);
|
|
1460
|
+
}
|
|
1461
|
+
for (const e of result.errors) {
|
|
1462
|
+
const loc = e.location ? ` at ${relative(".", e.location.file ?? "")}:${e.location.line}:${e.location.column}` : "";
|
|
1463
|
+
msgs.push(`[error] ${e.text}${loc}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (result.errors.length > 0) {
|
|
1466
|
+
throw new Error(msgs.join("\n"));
|
|
1467
|
+
}
|
|
1468
|
+
const outputFiles = result.outputFiles;
|
|
1469
|
+
const entryFile = outputFiles.find((f) => f.path === entryAbs) ?? outputFiles[0];
|
|
1470
|
+
const code = entryFile?.text ?? "";
|
|
1471
|
+
let chunks;
|
|
1472
|
+
if (outputFiles.length > 1) {
|
|
1473
|
+
chunks = /* @__PURE__ */ new Map();
|
|
1474
|
+
for (const f of outputFiles) {
|
|
1475
|
+
if (f === entryFile) continue;
|
|
1476
|
+
const chunkName = f.path.split("/").pop() ?? f.path;
|
|
1477
|
+
chunks.set(chunkName, { code: f.text, etag: `"esbuild-${hashCode(f.text)}"` });
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
const etag = `"esbuild-${hashCode(code)}"`;
|
|
1481
|
+
if (msgs.length > 0) {
|
|
1482
|
+
console.error("[esbuildDev]", msgs.join("\n"));
|
|
1483
|
+
}
|
|
1484
|
+
return { code, etag, chunks };
|
|
1485
|
+
}
|
|
1486
|
+
function isCacheValid2(cached) {
|
|
1487
|
+
return Promise.all(
|
|
1488
|
+
[...cached.files].map(async ([file, mtime]) => {
|
|
1489
|
+
try {
|
|
1490
|
+
const s = await stat(file);
|
|
1491
|
+
return s.mtimeMs === mtime;
|
|
1492
|
+
} catch {
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
})
|
|
1496
|
+
).then((results) => results.every(Boolean));
|
|
1497
|
+
}
|
|
1498
|
+
return async (req, ctx, next) => {
|
|
1499
|
+
const url = new URL(req.url);
|
|
1500
|
+
const pathname = url.pathname;
|
|
1501
|
+
if (importmap && pathname === "/assets/importmap") {
|
|
1502
|
+
if (!importmapScript) importmapScript = buildImportmap();
|
|
1503
|
+
return new Response(importmapScript, {
|
|
1504
|
+
headers: {
|
|
1505
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
1506
|
+
"Cache-Control": "no-cache"
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
const matched = entries.find((e) => e.path === pathname);
|
|
1511
|
+
const chunkName = pathname.split("/").pop() ?? "";
|
|
1512
|
+
const chunkOwner = chunkMap.get(chunkName);
|
|
1513
|
+
if (!matched && !chunkOwner) return next(req, ctx);
|
|
1514
|
+
if (!matched && chunkOwner) {
|
|
1515
|
+
const ownerCache = cacheStore.get(chunkOwner);
|
|
1516
|
+
const chunk = ownerCache?.chunks?.get(chunkName);
|
|
1517
|
+
if (chunk) {
|
|
1518
|
+
if (req.headers.get("if-none-match") === chunk.etag) {
|
|
1519
|
+
return new Response(null, { status: 304 });
|
|
1520
|
+
}
|
|
1521
|
+
return new Response(chunk.code, {
|
|
1522
|
+
headers: {
|
|
1523
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
1524
|
+
ETag: chunk.etag,
|
|
1525
|
+
"Cache-Control": "no-cache"
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
return next(req, ctx);
|
|
1530
|
+
}
|
|
1531
|
+
const { config } = matched;
|
|
1532
|
+
try {
|
|
1533
|
+
if (cache === "memory") {
|
|
1534
|
+
const cached = cacheStore.get(pathname);
|
|
1535
|
+
if (cached) {
|
|
1536
|
+
const valid = await isCacheValid2(cached);
|
|
1537
|
+
if (valid) {
|
|
1538
|
+
if (req.headers.get("if-none-match") === cached.etag) {
|
|
1539
|
+
return new Response(null, { status: 304 });
|
|
1540
|
+
}
|
|
1541
|
+
return new Response(cached.code, {
|
|
1542
|
+
headers: {
|
|
1543
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
1544
|
+
ETag: cached.etag,
|
|
1545
|
+
"Cache-Control": "no-cache"
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
for (const [cn] of cached.chunks ?? []) chunkMap.delete(cn);
|
|
1550
|
+
cacheStore.delete(pathname);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const { code, etag, chunks } = await compile(config);
|
|
1554
|
+
const depEntry = config.clientRouter?.routes ?? (config.clientRouter?.pages ? Object.values(config.clientRouter.pages)[0] : void 0) ?? config.entry;
|
|
1555
|
+
const deps = await collectDeps(resolve3(depEntry));
|
|
1556
|
+
if (cache === "memory") {
|
|
1557
|
+
cacheStore.set(pathname, { code, etag, files: deps, chunks });
|
|
1558
|
+
if (chunks) {
|
|
1559
|
+
for (const [cn] of chunks) chunkMap.set(cn, pathname);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return new Response(code, {
|
|
1563
|
+
headers: {
|
|
1564
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
1565
|
+
ETag: etag,
|
|
1566
|
+
"Cache-Control": "no-cache"
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1571
|
+
const html = errorTemplate(message);
|
|
1572
|
+
return new Response(html, {
|
|
1573
|
+
status: 500,
|
|
1574
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
function hashCode(s) {
|
|
1580
|
+
let hash = 0;
|
|
1581
|
+
for (let i = 0; i < s.length; i++) {
|
|
1582
|
+
const ch = s.charCodeAt(i);
|
|
1583
|
+
hash = (hash << 5) - hash + ch;
|
|
1584
|
+
hash |= 0;
|
|
1585
|
+
}
|
|
1586
|
+
return Math.abs(hash).toString(36);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/middleware/tailwind-dev.ts
|
|
1590
|
+
import { resolve as resolve4 } from "node:path";
|
|
1591
|
+
import { stat as stat2, readFile as readFile3, glob } from "node:fs/promises";
|
|
1592
|
+
function escapeHtml2(s) {
|
|
1593
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1594
|
+
}
|
|
1595
|
+
function defaultErrorTemplate2(errors) {
|
|
1596
|
+
return `<!DOCTYPE html>
|
|
1597
|
+
<html>
|
|
1598
|
+
<head>
|
|
1599
|
+
<meta charset="utf-8">
|
|
1600
|
+
<title>Tailwind Build Error</title>
|
|
1601
|
+
<style>
|
|
1602
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1603
|
+
body { font-family: system-ui; background: #1a1a2e; color: #e0e0e0; padding: 2rem; }
|
|
1604
|
+
.overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
1605
|
+
.card { background: #16213e; border: 1px solid #06b6d4; border-radius: 12px; padding: 2rem; max-width: 800px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
|
1606
|
+
h1 { color: #06b6d4; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
1607
|
+
pre { background: #0f0f23; padding: 1.5rem; border-radius: 8px; overflow: auto; font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
1608
|
+
</style>
|
|
1609
|
+
</head>
|
|
1610
|
+
<body>
|
|
1611
|
+
<div class="overlay">
|
|
1612
|
+
<div class="card">
|
|
1613
|
+
<h1>\u{1F3A8} Tailwind CSS Build Error</h1>
|
|
1614
|
+
<pre>${escapeHtml2(errors)}</pre>
|
|
1615
|
+
</div>
|
|
1616
|
+
</div>
|
|
1617
|
+
</body>
|
|
1618
|
+
</html>`;
|
|
1619
|
+
}
|
|
1620
|
+
var TW_CANDIDATE_RE = /[a-z][\w-]*(?::[a-z][\w-]*)*(?:-\[[^\]]*\])*(?:\/[0-9]+)?/gi;
|
|
1621
|
+
var CLASS_CONTEXT_RE = /(?:class(?:Name)?\s*[=:]\s*["'`]([^"'`]+)["'`]|["'`]([^"'`\n]{2,})["'`])/g;
|
|
1622
|
+
async function extractCandidates(patterns, root) {
|
|
1623
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1624
|
+
for (const pattern of patterns) {
|
|
1625
|
+
const resolved = resolve4(root, pattern);
|
|
1626
|
+
try {
|
|
1627
|
+
for await (const file of glob(resolved)) {
|
|
1628
|
+
try {
|
|
1629
|
+
const content = await readFile3(file, "utf-8");
|
|
1630
|
+
for (const m of content.matchAll(CLASS_CONTEXT_RE)) {
|
|
1631
|
+
const str = m[1] ?? m[2];
|
|
1632
|
+
if (!str) continue;
|
|
1633
|
+
for (const c of str.matchAll(TW_CANDIDATE_RE)) {
|
|
1634
|
+
seen.add(c[0]);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return [...seen];
|
|
1644
|
+
}
|
|
1645
|
+
async function collectFileMtimes(files) {
|
|
1646
|
+
const map = /* @__PURE__ */ new Map();
|
|
1647
|
+
await Promise.all(
|
|
1648
|
+
files.map(async (f) => {
|
|
1649
|
+
try {
|
|
1650
|
+
const s = await stat2(f);
|
|
1651
|
+
map.set(f, s.mtimeMs);
|
|
1652
|
+
} catch {
|
|
1653
|
+
}
|
|
1654
|
+
})
|
|
1655
|
+
);
|
|
1656
|
+
return map;
|
|
1657
|
+
}
|
|
1658
|
+
function isCacheValid(cached) {
|
|
1659
|
+
return Promise.all(
|
|
1660
|
+
[...cached.files].map(async ([file, mtime]) => {
|
|
1661
|
+
try {
|
|
1662
|
+
const s = await stat2(file);
|
|
1663
|
+
return s.mtimeMs === mtime;
|
|
1664
|
+
} catch {
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
})
|
|
1668
|
+
).then((results) => results.every(Boolean));
|
|
1669
|
+
}
|
|
1670
|
+
function tailwindDev(opts) {
|
|
1671
|
+
const cacheMode = opts.cache ?? "memory";
|
|
1672
|
+
const errorTemplate = opts.errorTemplate ?? defaultErrorTemplate2;
|
|
1673
|
+
const entries = Object.entries(opts.entries).map(([p, e]) => {
|
|
1674
|
+
const path = p.startsWith("/") ? p : "/" + p;
|
|
1675
|
+
const config = typeof e === "string" ? { entry: e } : e;
|
|
1676
|
+
return { path, config };
|
|
1677
|
+
});
|
|
1678
|
+
const cacheStore = /* @__PURE__ */ new Map();
|
|
1679
|
+
let compileFn = null;
|
|
1680
|
+
let loadError = null;
|
|
1681
|
+
async function getCompile() {
|
|
1682
|
+
if (compileFn) return compileFn;
|
|
1683
|
+
try {
|
|
1684
|
+
const mod = await import("@tailwindcss/node");
|
|
1685
|
+
compileFn = mod.compile;
|
|
1686
|
+
return compileFn;
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
loadError = `@tailwindcss/node is not installed. Run: npm install -D tailwindcss @tailwindcss/node
|
|
1689
|
+
|
|
1690
|
+
${String(err)}`;
|
|
1691
|
+
throw new Error(loadError, { cause: err });
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
async function compileCss(entry) {
|
|
1695
|
+
const compile = await getCompile();
|
|
1696
|
+
const entryAbs = resolve4(entry.entry);
|
|
1697
|
+
const cssSource = await readFile3(entryAbs, "utf-8");
|
|
1698
|
+
const baseDir = resolve4(entry.entry, "..");
|
|
1699
|
+
const deps = [entryAbs];
|
|
1700
|
+
const result = await compile(cssSource, {
|
|
1701
|
+
base: baseDir,
|
|
1702
|
+
onDependency: (p) => {
|
|
1703
|
+
if (!deps.includes(p)) deps.push(p);
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
const contentPatterns = entry.content ?? [];
|
|
1707
|
+
for (const src of result.sources) {
|
|
1708
|
+
if (!src.negated) {
|
|
1709
|
+
contentPatterns.push(resolve4(src.base, src.pattern));
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
let candidates = [];
|
|
1713
|
+
if (contentPatterns.length > 0) {
|
|
1714
|
+
candidates = await extractCandidates(contentPatterns, baseDir);
|
|
1715
|
+
}
|
|
1716
|
+
const css = result.build(candidates);
|
|
1717
|
+
for (const pattern of contentPatterns) {
|
|
1718
|
+
try {
|
|
1719
|
+
for await (const file of glob(resolve4(baseDir, pattern))) {
|
|
1720
|
+
if (!deps.includes(file)) deps.push(file);
|
|
1721
|
+
}
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const etag = `"tw-${hashCode2(css)}"`;
|
|
1726
|
+
return { css, etag, files: deps };
|
|
1727
|
+
}
|
|
1728
|
+
return async (req, ctx, next) => {
|
|
1729
|
+
const url = new URL(req.url);
|
|
1730
|
+
const pathname = url.pathname;
|
|
1731
|
+
const matched = entries.find((e) => e.path === pathname);
|
|
1732
|
+
if (!matched) return next(req, ctx);
|
|
1733
|
+
const { config } = matched;
|
|
1734
|
+
try {
|
|
1735
|
+
if (cacheMode === "memory") {
|
|
1736
|
+
const cached = cacheStore.get(pathname);
|
|
1737
|
+
if (cached) {
|
|
1738
|
+
const valid = await isCacheValid(cached);
|
|
1739
|
+
if (valid) {
|
|
1740
|
+
if (req.headers.get("if-none-match") === cached.etag) {
|
|
1741
|
+
return new Response(null, { status: 304 });
|
|
1742
|
+
}
|
|
1743
|
+
return new Response(cached.css, {
|
|
1744
|
+
headers: {
|
|
1745
|
+
"Content-Type": "text/css; charset=utf-8",
|
|
1746
|
+
ETag: cached.etag,
|
|
1747
|
+
"Cache-Control": "no-cache"
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
cacheStore.delete(pathname);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
const { css, etag, files } = await compileCss(config);
|
|
1755
|
+
if (cacheMode === "memory") {
|
|
1756
|
+
const mtimes = await collectFileMtimes(files);
|
|
1757
|
+
cacheStore.set(pathname, { css, etag, files: mtimes });
|
|
1758
|
+
}
|
|
1759
|
+
return new Response(css, {
|
|
1760
|
+
headers: {
|
|
1761
|
+
"Content-Type": "text/css; charset=utf-8",
|
|
1762
|
+
ETag: etag,
|
|
1763
|
+
"Cache-Control": "no-cache"
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1768
|
+
const html = errorTemplate(message);
|
|
1769
|
+
return new Response(html, {
|
|
1770
|
+
status: 500,
|
|
1771
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function hashCode2(s) {
|
|
1777
|
+
let hash = 0;
|
|
1778
|
+
for (let i = 0; i < s.length; i++) {
|
|
1779
|
+
const ch = s.charCodeAt(i);
|
|
1780
|
+
hash = (hash << 5) - hash + ch;
|
|
1781
|
+
hash |= 0;
|
|
1782
|
+
}
|
|
1783
|
+
return Math.abs(hash).toString(36);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1102
1786
|
// src/middleware/rate-limit.ts
|
|
1103
1787
|
function defaultKey(_req, _ctx) {
|
|
1104
1788
|
const forwarded = _req.headers.get("x-forwarded-for");
|
|
@@ -1111,7 +1795,7 @@ function defaultKey(_req, _ctx) {
|
|
|
1111
1795
|
}
|
|
1112
1796
|
function rateLimit(options) {
|
|
1113
1797
|
const max = options?.max ?? 100;
|
|
1114
|
-
const
|
|
1798
|
+
const window = options?.window ?? 6e4;
|
|
1115
1799
|
const getKey = options?.key ?? defaultKey;
|
|
1116
1800
|
const message = options?.message ?? "Too Many Requests";
|
|
1117
1801
|
const storeType = options?.store ?? "memory";
|
|
@@ -1138,7 +1822,7 @@ function rateLimit(options) {
|
|
|
1138
1822
|
}
|
|
1139
1823
|
}
|
|
1140
1824
|
},
|
|
1141
|
-
Math.min(
|
|
1825
|
+
Math.min(window, 3e4)
|
|
1142
1826
|
) : null;
|
|
1143
1827
|
if (interval?.unref) interval.unref();
|
|
1144
1828
|
async function checkAndIncrement(key) {
|
|
@@ -1147,16 +1831,16 @@ function rateLimit(options) {
|
|
|
1147
1831
|
const redisKey = `${keyPrefix}${key}`;
|
|
1148
1832
|
const count = await redis2.incr(redisKey);
|
|
1149
1833
|
if (count === 1) {
|
|
1150
|
-
await redis2.pexpire(redisKey,
|
|
1834
|
+
await redis2.pexpire(redisKey, window);
|
|
1151
1835
|
}
|
|
1152
1836
|
const pttl = await redis2.pttl(redisKey);
|
|
1153
|
-
const reset = pttl > 0 ? now + pttl : now +
|
|
1837
|
+
const reset = pttl > 0 ? now + pttl : now + window;
|
|
1154
1838
|
return { count, reset };
|
|
1155
1839
|
}
|
|
1156
1840
|
const entry = hits.get(key);
|
|
1157
1841
|
if (!entry || entry.reset < now) {
|
|
1158
|
-
hits.set(key, { count: 1, reset: now +
|
|
1159
|
-
return { count: 1, reset: now +
|
|
1842
|
+
hits.set(key, { count: 1, reset: now + window });
|
|
1843
|
+
return { count: 1, reset: now + window };
|
|
1160
1844
|
}
|
|
1161
1845
|
entry.count++;
|
|
1162
1846
|
return { count: entry.count, reset: entry.reset };
|
|
@@ -1202,10 +1886,10 @@ function addRateLimitHeaders(res, limit, remaining, reset) {
|
|
|
1202
1886
|
|
|
1203
1887
|
// src/middleware/compress.ts
|
|
1204
1888
|
import { constants, brotliCompress, gzip, deflate } from "node:zlib";
|
|
1205
|
-
import { promisify } from "node:util";
|
|
1206
|
-
var brotliCompressAsync =
|
|
1207
|
-
var gzipAsync =
|
|
1208
|
-
var deflateAsync =
|
|
1889
|
+
import { promisify as promisify2 } from "node:util";
|
|
1890
|
+
var brotliCompressAsync = promisify2(brotliCompress);
|
|
1891
|
+
var gzipAsync = promisify2(gzip);
|
|
1892
|
+
var deflateAsync = promisify2(deflate);
|
|
1209
1893
|
function compress(options) {
|
|
1210
1894
|
const level = options?.level ?? 6;
|
|
1211
1895
|
const threshold = options?.threshold ?? 1024;
|
|
@@ -1610,7 +2294,7 @@ function redis(opts) {
|
|
|
1610
2294
|
}
|
|
1611
2295
|
|
|
1612
2296
|
// src/queue/index.ts
|
|
1613
|
-
import
|
|
2297
|
+
import crypto3 from "node:crypto";
|
|
1614
2298
|
import { Redis as IORedis2 } from "ioredis";
|
|
1615
2299
|
|
|
1616
2300
|
// src/queue/cron.ts
|
|
@@ -1724,7 +2408,7 @@ function queue(opts) {
|
|
|
1724
2408
|
}
|
|
1725
2409
|
if (job.schedule) {
|
|
1726
2410
|
try {
|
|
1727
|
-
await insert({ ...job, id:
|
|
2411
|
+
await insert({ ...job, id: crypto3.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
|
|
1728
2412
|
} catch {
|
|
1729
2413
|
}
|
|
1730
2414
|
}
|
|
@@ -1750,7 +2434,7 @@ function queue(opts) {
|
|
|
1750
2434
|
});
|
|
1751
2435
|
const q = mw;
|
|
1752
2436
|
mw.add = function add(type, payload, opts2) {
|
|
1753
|
-
const id =
|
|
2437
|
+
const id = crypto3.randomUUID();
|
|
1754
2438
|
let runAt;
|
|
1755
2439
|
if (opts2?.schedule) {
|
|
1756
2440
|
try {
|
|
@@ -1868,7 +2552,7 @@ function queue(opts) {
|
|
|
1868
2552
|
return r;
|
|
1869
2553
|
};
|
|
1870
2554
|
q.cron = function(pattern, handler) {
|
|
1871
|
-
const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" +
|
|
2555
|
+
const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto3.randomUUID().slice(0, 8);
|
|
1872
2556
|
q.process(id, async () => {
|
|
1873
2557
|
await handler();
|
|
1874
2558
|
});
|
|
@@ -1879,14 +2563,118 @@ function queue(opts) {
|
|
|
1879
2563
|
}
|
|
1880
2564
|
|
|
1881
2565
|
// src/react/index.ts
|
|
1882
|
-
import {
|
|
2566
|
+
import { createElement as createElement2 } from "react";
|
|
2567
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
1883
2568
|
|
|
1884
|
-
// src/react/
|
|
1885
|
-
import {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
} from "
|
|
1889
|
-
|
|
2569
|
+
// src/react/compile.ts
|
|
2570
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4 } from "node:fs/promises";
|
|
2571
|
+
import { resolve as resolve5, join as join3, dirname as dirname2 } from "node:path";
|
|
2572
|
+
import { createHash } from "node:crypto";
|
|
2573
|
+
import { existsSync } from "node:fs";
|
|
2574
|
+
var EXTERNAL_PKGS = [
|
|
2575
|
+
"react",
|
|
2576
|
+
"react/jsx-runtime",
|
|
2577
|
+
"react-dom",
|
|
2578
|
+
"react-dom/server",
|
|
2579
|
+
"react-dom/client",
|
|
2580
|
+
"weifuwu",
|
|
2581
|
+
"weifuwu/react"
|
|
2582
|
+
];
|
|
2583
|
+
var FRAMEWORK_IMPORTS = [
|
|
2584
|
+
"react",
|
|
2585
|
+
"react/jsx-runtime",
|
|
2586
|
+
"react-dom",
|
|
2587
|
+
"react-dom/server",
|
|
2588
|
+
"react-dom/client",
|
|
2589
|
+
"weifuwu",
|
|
2590
|
+
"weifuwu/react"
|
|
2591
|
+
];
|
|
2592
|
+
var memCache = /* @__PURE__ */ new Map();
|
|
2593
|
+
function buildResolveMap() {
|
|
2594
|
+
const map = {};
|
|
2595
|
+
for (const spec of FRAMEWORK_IMPORTS) {
|
|
2596
|
+
try {
|
|
2597
|
+
map[spec] = import.meta.resolve(spec);
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return map;
|
|
2602
|
+
}
|
|
2603
|
+
var _resolveMap = null;
|
|
2604
|
+
function resolveMap() {
|
|
2605
|
+
if (!_resolveMap) _resolveMap = buildResolveMap();
|
|
2606
|
+
return _resolveMap;
|
|
2607
|
+
}
|
|
2608
|
+
function rewriteImports(code) {
|
|
2609
|
+
const map = resolveMap();
|
|
2610
|
+
let result = code;
|
|
2611
|
+
for (const [spec, url] of Object.entries(map)) {
|
|
2612
|
+
const quoted = JSON.stringify(spec);
|
|
2613
|
+
const escaped = quoted.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2614
|
+
result = result.replace(new RegExp(`(from\\s+)${escaped}`, "g"), `$1${JSON.stringify(url)}`).replace(new RegExp(`(import\\s+)${escaped}(\\s*;)`, "g"), `$1${JSON.stringify(url)}$2`).replace(new RegExp(`(export\\s+.*?from\\s+)${escaped}`, "g"), `$1${JSON.stringify(url)}`);
|
|
2615
|
+
}
|
|
2616
|
+
return result;
|
|
2617
|
+
}
|
|
2618
|
+
var _cacheDir = null;
|
|
2619
|
+
function setReactCacheDir(dir) {
|
|
2620
|
+
_cacheDir = dir;
|
|
2621
|
+
}
|
|
2622
|
+
function getCacheDir() {
|
|
2623
|
+
if (_cacheDir) return _cacheDir;
|
|
2624
|
+
_cacheDir = join3(process.cwd(), "node_modules", ".weifuwu", "react");
|
|
2625
|
+
return _cacheDir;
|
|
2626
|
+
}
|
|
2627
|
+
async function loadTsxModule(entryPath) {
|
|
2628
|
+
const abs = resolve5(entryPath);
|
|
2629
|
+
let source;
|
|
2630
|
+
try {
|
|
2631
|
+
source = await readFile4(abs, "utf-8");
|
|
2632
|
+
} catch {
|
|
2633
|
+
throw new Error(`Cannot read file: ${entryPath}`);
|
|
2634
|
+
}
|
|
2635
|
+
const sourceHash = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
2636
|
+
const memCached = memCache.get(abs);
|
|
2637
|
+
if (memCached && memCached.sourceHash === sourceHash) {
|
|
2638
|
+
return memCached.mod;
|
|
2639
|
+
}
|
|
2640
|
+
const tmpDir = getCacheDir();
|
|
2641
|
+
const tmpFile = join3(tmpDir, `${sourceHash}.mjs`);
|
|
2642
|
+
if (existsSync(tmpFile)) {
|
|
2643
|
+
try {
|
|
2644
|
+
const mod2 = await import(tmpFile + "?" + sourceHash);
|
|
2645
|
+
memCache.set(abs, { sourceHash, mod: mod2 });
|
|
2646
|
+
return mod2;
|
|
2647
|
+
} catch {
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
const esbuild = await import("esbuild");
|
|
2651
|
+
const result = await esbuild.build({
|
|
2652
|
+
entryPoints: [abs],
|
|
2653
|
+
bundle: true,
|
|
2654
|
+
write: false,
|
|
2655
|
+
format: "esm",
|
|
2656
|
+
platform: "node",
|
|
2657
|
+
jsx: "automatic",
|
|
2658
|
+
external: EXTERNAL_PKGS,
|
|
2659
|
+
logLevel: "silent"
|
|
2660
|
+
});
|
|
2661
|
+
let code = result.outputFiles[0]?.text ?? "";
|
|
2662
|
+
if (!code) throw new Error(`esbuild produced empty output for ${entryPath}`);
|
|
2663
|
+
code = rewriteImports(code);
|
|
2664
|
+
await mkdir3(dirname2(tmpFile), { recursive: true });
|
|
2665
|
+
await writeFile3(tmpFile, code);
|
|
2666
|
+
const mod = await import(tmpFile + "?" + sourceHash);
|
|
2667
|
+
memCache.set(abs, { sourceHash, mod });
|
|
2668
|
+
return mod;
|
|
2669
|
+
}
|
|
2670
|
+
async function loadTsxComponent(entryPath) {
|
|
2671
|
+
const mod = await loadTsxModule(entryPath);
|
|
2672
|
+
if (mod.default && typeof mod.default === "function") return mod.default;
|
|
2673
|
+
for (const [, val] of Object.entries(mod)) {
|
|
2674
|
+
if (typeof val === "function") return val;
|
|
2675
|
+
}
|
|
2676
|
+
throw new Error(`No component export found in ${entryPath}. Export a default or named component function.`);
|
|
2677
|
+
}
|
|
1890
2678
|
|
|
1891
2679
|
// src/react/context.ts
|
|
1892
2680
|
import { createContext } from "react";
|
|
@@ -1901,327 +2689,411 @@ function getServerDataContext() {
|
|
|
1901
2689
|
}
|
|
1902
2690
|
var ServerDataContext = getServerDataContext();
|
|
1903
2691
|
|
|
1904
|
-
// src/react/
|
|
1905
|
-
|
|
1906
|
-
var
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
}
|
|
1910
|
-
function buildHeadTags(head) {
|
|
1911
|
-
if (!head) return "";
|
|
1912
|
-
const tags = [];
|
|
1913
|
-
if (head.title) tags.push(`<title>${escapeAttr(head.title)}</title>`);
|
|
1914
|
-
if (head.meta) {
|
|
1915
|
-
for (const [name, content] of Object.entries(head.meta)) {
|
|
1916
|
-
tags.push(`<meta name="${escapeAttr(name)}" content="${escapeAttr(content)}">`);
|
|
1917
|
-
}
|
|
2692
|
+
// src/react/error-boundary.ts
|
|
2693
|
+
import { createElement, Component } from "react";
|
|
2694
|
+
var ErrorBoundary = class extends Component {
|
|
2695
|
+
state = { hasError: false, error: null };
|
|
2696
|
+
static getDerivedStateFromError(error) {
|
|
2697
|
+
return { hasError: true, error };
|
|
1918
2698
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
const attrs = Object.entries(link).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ");
|
|
1922
|
-
tags.push(`<link ${attrs}>`);
|
|
1923
|
-
}
|
|
2699
|
+
componentDidCatch(error) {
|
|
2700
|
+
this.props.onError?.(error);
|
|
1924
2701
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2702
|
+
render() {
|
|
2703
|
+
if (this.state.hasError) {
|
|
2704
|
+
if (this.props.fallback) return this.props.fallback;
|
|
2705
|
+
return createElement(
|
|
2706
|
+
"div",
|
|
2707
|
+
{ style: { padding: "2rem", textAlign: "center" } },
|
|
2708
|
+
createElement("h1", null, "Something went wrong"),
|
|
2709
|
+
createElement("p", null, this.state.error?.message ?? "Unknown error")
|
|
2710
|
+
);
|
|
1930
2711
|
}
|
|
2712
|
+
return this.props.children;
|
|
1931
2713
|
}
|
|
1932
|
-
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
// src/react/hooks.ts
|
|
2717
|
+
import { useContext } from "react";
|
|
2718
|
+
function useServerData() {
|
|
2719
|
+
return useContext(ServerDataContext);
|
|
1933
2720
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2721
|
+
|
|
2722
|
+
// src/react/index.ts
|
|
2723
|
+
function HtmlShell({ children, importMap, stylesheets, data }) {
|
|
2724
|
+
const headChildren = [
|
|
2725
|
+
createElement2("meta", { charSet: "utf-8", key: "charset" }),
|
|
2726
|
+
createElement2("meta", { name: "viewport", content: "width=device-width, initial-scale=1", key: "viewport" })
|
|
2727
|
+
];
|
|
2728
|
+
if (stylesheets) {
|
|
2729
|
+
for (const href of stylesheets) {
|
|
2730
|
+
headChildren.push(
|
|
2731
|
+
createElement2("link", { rel: "stylesheet", href, key: `css-${href}` })
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if (importMap) {
|
|
2736
|
+
headChildren.push(
|
|
2737
|
+
createElement2("script", {
|
|
2738
|
+
type: "importmap",
|
|
2739
|
+
key: "importmap",
|
|
2740
|
+
dangerouslySetInnerHTML: { __html: JSON.stringify(importMap) }
|
|
2741
|
+
})
|
|
2742
|
+
);
|
|
1943
2743
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
2744
|
+
const bodyChildren = [
|
|
2745
|
+
createElement2("div", { id: "root", key: "root" }, children)
|
|
2746
|
+
];
|
|
2747
|
+
if (data && Object.keys(data).length > 0) {
|
|
2748
|
+
bodyChildren.push(
|
|
2749
|
+
createElement2("script", {
|
|
2750
|
+
id: "__WEIFUWU_DATA__",
|
|
2751
|
+
type: "application/json",
|
|
2752
|
+
key: "weifuwu-data",
|
|
2753
|
+
dangerouslySetInnerHTML: { __html: JSON.stringify(data).replace(/</g, "\\u003c") }
|
|
2754
|
+
})
|
|
2755
|
+
);
|
|
1946
2756
|
}
|
|
1947
|
-
return
|
|
2757
|
+
return createElement2(
|
|
2758
|
+
"html",
|
|
2759
|
+
{ lang: "en" },
|
|
2760
|
+
createElement2("head", null, ...headChildren),
|
|
2761
|
+
createElement2("body", null, ...bodyChildren)
|
|
2762
|
+
);
|
|
1948
2763
|
}
|
|
1949
|
-
function
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
return new ReadableStream({
|
|
1961
|
-
async start(controller) {
|
|
1962
|
-
const reader = sourceStream.getReader();
|
|
1963
|
-
let buffer = "";
|
|
1964
|
-
try {
|
|
1965
|
-
while (true) {
|
|
1966
|
-
const { done, value } = await reader.read();
|
|
1967
|
-
if (done) break;
|
|
1968
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1969
|
-
if (!titleInjected) {
|
|
1970
|
-
const idx = buffer.indexOf("<title>");
|
|
1971
|
-
const endIdx = buffer.indexOf("</title>", idx);
|
|
1972
|
-
if (idx !== -1 && endIdx !== -1) {
|
|
1973
|
-
controller.enqueue(
|
|
1974
|
-
encoder.encode(buffer.slice(0, idx) + titleTag + buffer.slice(endIdx + 8))
|
|
1975
|
-
);
|
|
1976
|
-
buffer = "";
|
|
1977
|
-
titleInjected = true;
|
|
1978
|
-
continue;
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
if (!headInjected) {
|
|
1982
|
-
const idx = buffer.indexOf("</head>");
|
|
1983
|
-
if (idx !== -1) {
|
|
1984
|
-
controller.enqueue(
|
|
1985
|
-
encoder.encode(buffer.slice(0, idx) + rest + buffer.slice(idx))
|
|
1986
|
-
);
|
|
1987
|
-
buffer = "";
|
|
1988
|
-
headInjected = true;
|
|
1989
|
-
continue;
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
if (titleInjected && headInjected && buffer) {
|
|
1993
|
-
controller.enqueue(encoder.encode(buffer));
|
|
1994
|
-
buffer = "";
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
if (buffer) controller.enqueue(encoder.encode(buffer));
|
|
1998
|
-
controller.close();
|
|
1999
|
-
} catch (err) {
|
|
2000
|
-
controller.error(err);
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2764
|
+
async function renderComponent(Component2, data, layout, renderOpts) {
|
|
2765
|
+
let element = createElement2(Component2, renderOpts.props ?? {});
|
|
2766
|
+
if (layout) {
|
|
2767
|
+
element = createElement2(layout, { children: element });
|
|
2768
|
+
}
|
|
2769
|
+
element = createElement2(ServerDataContext.Provider, { value: data }, element);
|
|
2770
|
+
const page = createElement2(HtmlShell, {
|
|
2771
|
+
children: element,
|
|
2772
|
+
importMap: renderOpts.importMap,
|
|
2773
|
+
stylesheets: renderOpts.stylesheets,
|
|
2774
|
+
data: Object.keys(data).length > 0 ? data : void 0
|
|
2003
2775
|
});
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
2008
|
-
wrapped = createElement(layouts[i], null, wrapped);
|
|
2009
|
-
}
|
|
2010
|
-
const json = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
2011
|
-
const dataScript = createElement("script", {
|
|
2012
|
-
id: "__WEIFUWU_DATA__",
|
|
2013
|
-
type: "application/json",
|
|
2014
|
-
dangerouslySetInnerHTML: { __html: json }
|
|
2776
|
+
const rstream = await renderToReadableStream(page, {
|
|
2777
|
+
bootstrapScripts: renderOpts.bootstrapScripts,
|
|
2778
|
+
bootstrapModules: renderOpts.bootstrapModules
|
|
2015
2779
|
});
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
function render(element, layouts, options = {}) {
|
|
2023
|
-
const data = options.data ?? {};
|
|
2024
|
-
const wrapped = wrapElement(element, layouts, data);
|
|
2025
|
-
let html = renderToString(wrapped);
|
|
2026
|
-
const headTags = buildHeadTags(options.head);
|
|
2027
|
-
html = injectHeadIntoHtml(html, headTags);
|
|
2028
|
-
return new Response("<!DOCTYPE html>\n" + html, {
|
|
2029
|
-
status: options.status ?? 200,
|
|
2030
|
-
headers: {
|
|
2031
|
-
"content-type": "text/html; charset=utf-8",
|
|
2032
|
-
...options.headers
|
|
2033
|
-
}
|
|
2780
|
+
if (renderOpts.stream === false) {
|
|
2781
|
+
await rstream.allReady;
|
|
2782
|
+
}
|
|
2783
|
+
return new Response(rstream, {
|
|
2784
|
+
status: renderOpts.status ?? 200,
|
|
2785
|
+
headers: { "content-type": "text/html; charset=utf-8", ...renderOpts.headers }
|
|
2034
2786
|
});
|
|
2035
2787
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
const { done, value } = await reader.read();
|
|
2050
|
-
if (done) {
|
|
2051
|
-
controller.close();
|
|
2052
|
-
break;
|
|
2053
|
-
}
|
|
2054
|
-
controller.enqueue(value);
|
|
2055
|
-
}
|
|
2056
|
-
} catch (err) {
|
|
2057
|
-
controller.error(err);
|
|
2058
|
-
}
|
|
2788
|
+
function react(opts) {
|
|
2789
|
+
if (opts && "pages" in opts) {
|
|
2790
|
+
return (app) => createFullReactApp(app, opts);
|
|
2791
|
+
}
|
|
2792
|
+
if (opts?.cacheDir) setReactCacheDir(opts.cacheDir);
|
|
2793
|
+
let LayoutComponent = null;
|
|
2794
|
+
let layoutLoaded = false;
|
|
2795
|
+
let layoutLoadError = null;
|
|
2796
|
+
async function getLayout() {
|
|
2797
|
+
if (!opts?.layout) return null;
|
|
2798
|
+
if (layoutLoaded) {
|
|
2799
|
+
if (layoutLoadError) throw layoutLoadError;
|
|
2800
|
+
return LayoutComponent;
|
|
2059
2801
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2802
|
+
try {
|
|
2803
|
+
LayoutComponent = await loadTsxComponent(opts.layout);
|
|
2804
|
+
layoutLoaded = true;
|
|
2805
|
+
return LayoutComponent;
|
|
2806
|
+
} catch (err) {
|
|
2807
|
+
layoutLoadError = err instanceof Error ? err : new Error(String(err));
|
|
2808
|
+
layoutLoaded = true;
|
|
2809
|
+
throw layoutLoadError;
|
|
2067
2810
|
}
|
|
2068
|
-
}
|
|
2811
|
+
}
|
|
2812
|
+
return async (_req, ctx, next) => {
|
|
2813
|
+
ctx.render = async (path, renderOpts) => {
|
|
2814
|
+
let data = renderOpts?.data ?? {};
|
|
2815
|
+
if (renderOpts?.loader) {
|
|
2816
|
+
const loaderData = await renderOpts.loader(ctx);
|
|
2817
|
+
data = { ...data, ...loaderData };
|
|
2818
|
+
}
|
|
2819
|
+
const Component2 = await loadTsxComponent(path);
|
|
2820
|
+
const layout = await getLayout();
|
|
2821
|
+
return renderComponent(Component2, data, layout, renderOpts ?? {});
|
|
2822
|
+
};
|
|
2823
|
+
return next(_req, ctx);
|
|
2824
|
+
};
|
|
2069
2825
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
function useServerData() {
|
|
2074
|
-
return useContext(ServerDataContext);
|
|
2826
|
+
async function resolveLoader(modulePath) {
|
|
2827
|
+
const mod = await loadTsxModule(modulePath);
|
|
2828
|
+
return typeof mod.loader === "function" ? mod.loader : null;
|
|
2075
2829
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
// src/react/error-boundary.ts
|
|
2085
|
-
import { Component } from "react";
|
|
2086
|
-
var ErrorBoundary = class extends Component {
|
|
2087
|
-
state = { error: null };
|
|
2088
|
-
static getDerivedStateFromError(error) {
|
|
2089
|
-
return { error };
|
|
2830
|
+
async function callLoader(loader, ctx, data, rethrow = true) {
|
|
2831
|
+
if (!loader) return data;
|
|
2832
|
+
try {
|
|
2833
|
+
return { ...data, ...await loader(ctx) };
|
|
2834
|
+
} catch (err) {
|
|
2835
|
+
if (rethrow) throw err;
|
|
2836
|
+
return data;
|
|
2090
2837
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2838
|
+
}
|
|
2839
|
+
function createFullReactApp(app, opts) {
|
|
2840
|
+
const stylesheets = [...opts.stylesheets ?? []];
|
|
2841
|
+
if (opts.tailwind) {
|
|
2842
|
+
const twPath = opts.tailwind.path ?? "/assets/tailwind.css";
|
|
2843
|
+
const twEntry = opts.tailwind.entry ?? "./styles/input.css";
|
|
2844
|
+
app.use(tailwindDev({ entries: { [twPath]: { entry: twEntry } } }));
|
|
2845
|
+
if (!stylesheets.includes(twPath)) stylesheets.push(twPath);
|
|
2846
|
+
}
|
|
2847
|
+
app.use(react({ layout: opts.layout, cacheDir: opts.cacheDir }));
|
|
2848
|
+
const layoutLoaderP = resolveLoader(opts.layout);
|
|
2849
|
+
const notFoundLoaderP = opts.notFound ? resolveLoader(opts.notFound) : Promise.resolve(null);
|
|
2850
|
+
const clientCfg = typeof opts.client === "object" ? opts.client : {};
|
|
2851
|
+
const clientPath = clientCfg.path ?? "/assets/client.js";
|
|
2852
|
+
const bootstrapModules = [...opts.bootstrapModules ?? []];
|
|
2853
|
+
if (opts.client !== false && !bootstrapModules.includes(clientPath)) {
|
|
2854
|
+
bootstrapModules.push(clientPath);
|
|
2855
|
+
}
|
|
2856
|
+
const renderOpts = {
|
|
2857
|
+
stylesheets: stylesheets.length > 0 ? stylesheets : void 0,
|
|
2858
|
+
bootstrapModules: bootstrapModules.length > 0 ? bootstrapModules : void 0,
|
|
2859
|
+
stream: opts.stream
|
|
2860
|
+
};
|
|
2861
|
+
for (const [path, component] of Object.entries(opts.pages)) {
|
|
2862
|
+
app.get(path, async (_req, ctx) => {
|
|
2863
|
+
let data = {};
|
|
2864
|
+
data = await callLoader(await layoutLoaderP, ctx, data);
|
|
2865
|
+
const pageLoader = opts.loaders?.[path] ?? await resolveLoader(component);
|
|
2866
|
+
data = await callLoader(pageLoader, ctx, data);
|
|
2867
|
+
return ctx.render(component, { ...renderOpts, data });
|
|
2868
|
+
});
|
|
2093
2869
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2870
|
+
if (opts.notFound) {
|
|
2871
|
+
const notFoundPath = opts.notFound;
|
|
2872
|
+
app.onError(async (err, _req, ctx) => {
|
|
2873
|
+
const status = typeof err === "object" && err !== null && "status" in err ? err.status : 500;
|
|
2874
|
+
if (ctx.render) {
|
|
2875
|
+
let data = { error: String(err) };
|
|
2876
|
+
data = await callLoader(await layoutLoaderP, ctx, data, false);
|
|
2877
|
+
data = await callLoader(await notFoundLoaderP, ctx, data, false);
|
|
2878
|
+
return ctx.render(notFoundPath, { ...renderOpts, status, data });
|
|
2879
|
+
}
|
|
2880
|
+
return new Response("Internal Server Error", { status });
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
if (opts.client !== false) {
|
|
2884
|
+
app.use(esbuildDev({
|
|
2885
|
+
entries: {
|
|
2886
|
+
[clientPath]: {
|
|
2887
|
+
clientRouter: {
|
|
2888
|
+
pages: opts.pages,
|
|
2889
|
+
layout: opts.layout,
|
|
2890
|
+
fallback: opts.notFound
|
|
2891
|
+
},
|
|
2892
|
+
bundle: true,
|
|
2893
|
+
splitting: clientCfg.splitting ?? true,
|
|
2894
|
+
minify: clientCfg.minify ?? false
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}));
|
|
2099
2898
|
}
|
|
2100
|
-
};
|
|
2101
|
-
|
|
2102
|
-
// src/react/navigation.ts
|
|
2103
|
-
var RouterContext = createContext2(null);
|
|
2104
|
-
RouterContext.displayName = "ClientRouter";
|
|
2105
|
-
function useParams() {
|
|
2106
|
-
return useContext2(RouterContext)?.params ?? {};
|
|
2107
2899
|
}
|
|
2108
|
-
function
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
|
|
2900
|
+
function extractImportPath(fn) {
|
|
2901
|
+
const src = fn.toString();
|
|
2902
|
+
const m = src.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
2903
|
+
if (!m) throw new Error(`Cannot extract import path from: ${src}`);
|
|
2904
|
+
return m[1];
|
|
2112
2905
|
}
|
|
2113
|
-
function
|
|
2114
|
-
|
|
2115
|
-
|
|
2906
|
+
function reactRouter(app, routes, opts = {}) {
|
|
2907
|
+
let LayoutComponent = null;
|
|
2908
|
+
let layoutPromise = null;
|
|
2909
|
+
async function getLayout() {
|
|
2910
|
+
if (!opts.layout) return null;
|
|
2911
|
+
if (LayoutComponent) return LayoutComponent;
|
|
2912
|
+
if (!layoutPromise) {
|
|
2913
|
+
layoutPromise = loadTsxComponent(opts.layout).then((c) => {
|
|
2914
|
+
LayoutComponent = c;
|
|
2915
|
+
return c;
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
return layoutPromise;
|
|
2919
|
+
}
|
|
2920
|
+
for (const [path, importer] of Object.entries(routes)) {
|
|
2921
|
+
app.get(path, async (_req, ctx) => {
|
|
2922
|
+
const cmpPath = extractImportPath(importer);
|
|
2923
|
+
const Component2 = await loadTsxComponent(cmpPath);
|
|
2924
|
+
const layout = await getLayout();
|
|
2925
|
+
let data = {};
|
|
2926
|
+
const loader = opts.loaders?.[path];
|
|
2927
|
+
if (loader) {
|
|
2928
|
+
data = await loader(ctx);
|
|
2929
|
+
}
|
|
2930
|
+
return renderComponent(Component2, data, layout, opts);
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2116
2933
|
}
|
|
2117
2934
|
function Link({ href, children, ...props }) {
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
}
|
|
2129
|
-
return
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
if (loc) {
|
|
2156
|
-
router.navigate(loc);
|
|
2157
|
-
return;
|
|
2935
|
+
return createElement2("a", { href, ...props }, children);
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
// src/ai/index.ts
|
|
2939
|
+
function ai(opts) {
|
|
2940
|
+
let aiModule = null;
|
|
2941
|
+
const model = opts.model;
|
|
2942
|
+
async function getAi() {
|
|
2943
|
+
if (!aiModule) aiModule = await import("ai");
|
|
2944
|
+
return aiModule;
|
|
2945
|
+
}
|
|
2946
|
+
return async (_req, ctx, next) => {
|
|
2947
|
+
ctx.ai = {
|
|
2948
|
+
model,
|
|
2949
|
+
system: opts.system,
|
|
2950
|
+
async generateText(params) {
|
|
2951
|
+
const { generateText } = await getAi();
|
|
2952
|
+
const messages = [...params.messages ?? []];
|
|
2953
|
+
const system = params.system ?? opts.system;
|
|
2954
|
+
if (system) {
|
|
2955
|
+
const hasSystem = messages.some((m) => m.role === "system");
|
|
2956
|
+
if (!hasSystem) {
|
|
2957
|
+
messages.unshift({ role: "system", content: system });
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
const baseOpts = {
|
|
2961
|
+
model,
|
|
2962
|
+
tools: opts.tools,
|
|
2963
|
+
maxSteps: params.maxSteps ?? opts.maxSteps ?? 1,
|
|
2964
|
+
temperature: params.temperature ?? opts.temperature,
|
|
2965
|
+
maxTokens: params.maxTokens ?? opts.maxTokens,
|
|
2966
|
+
abortSignal: _req.signal
|
|
2967
|
+
};
|
|
2968
|
+
if (messages.length > 0) {
|
|
2969
|
+
baseOpts.messages = messages;
|
|
2970
|
+
} else {
|
|
2971
|
+
baseOpts.prompt = params.prompt;
|
|
2158
2972
|
}
|
|
2973
|
+
return generateText(baseOpts);
|
|
2159
2974
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
console.error("[weifuwu/react] Form submit failed:", err);
|
|
2163
|
-
}
|
|
2975
|
+
};
|
|
2976
|
+
return next(_req, ctx);
|
|
2164
2977
|
};
|
|
2165
|
-
return createElement2(
|
|
2166
|
-
"form",
|
|
2167
|
-
{ method, action, ...props, onSubmit: handleSubmit },
|
|
2168
|
-
children
|
|
2169
|
-
);
|
|
2170
2978
|
}
|
|
2171
2979
|
|
|
2172
|
-
// src/
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
function
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
function isDataRequest(req) {
|
|
2179
|
-
try {
|
|
2180
|
-
return new URL(req.url).searchParams.has("_data");
|
|
2181
|
-
} catch {
|
|
2182
|
-
return false;
|
|
2980
|
+
// src/ai/agent.ts
|
|
2981
|
+
function agent(opts) {
|
|
2982
|
+
let aiModule = null;
|
|
2983
|
+
async function getAi() {
|
|
2984
|
+
if (!aiModule) aiModule = await import("ai");
|
|
2985
|
+
return aiModule;
|
|
2183
2986
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2987
|
+
return async (req, ctx, next) => {
|
|
2988
|
+
ctx.agent = {
|
|
2989
|
+
model: opts.model,
|
|
2990
|
+
system: opts.system,
|
|
2991
|
+
async chat(prompt, chatOpts = {}) {
|
|
2992
|
+
const { generateText } = await getAi();
|
|
2993
|
+
let knowledgeContext = "";
|
|
2994
|
+
if (opts.knowledge) {
|
|
2995
|
+
try {
|
|
2996
|
+
const results = await opts.knowledge.search(prompt, ctx);
|
|
2997
|
+
const topK = opts.knowledge.topK ?? 3;
|
|
2998
|
+
const minScore = opts.knowledge.minScore ?? 0;
|
|
2999
|
+
const filtered = results.filter((r) => (r.score ?? 1) >= minScore).slice(0, topK);
|
|
3000
|
+
if (filtered.length > 0) {
|
|
3001
|
+
knowledgeContext = "\n\n\u53C2\u8003\u4EE5\u4E0B\u77E5\u8BC6\uFF1A\n" + filtered.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
|
|
3002
|
+
}
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
2197
3005
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
3006
|
+
const system = chatOpts.system ?? opts.system ?? "";
|
|
3007
|
+
const messages = [...chatOpts.messages ?? []];
|
|
3008
|
+
if (system) {
|
|
3009
|
+
const hasSystem = messages.some((m) => m.role === "system");
|
|
3010
|
+
if (!hasSystem) {
|
|
3011
|
+
messages.unshift({
|
|
3012
|
+
role: "system",
|
|
3013
|
+
content: knowledgeContext ? system + knowledgeContext : system
|
|
3014
|
+
});
|
|
3015
|
+
} else if (knowledgeContext) {
|
|
3016
|
+
const sysMsg = messages.find((m) => m.role === "system");
|
|
3017
|
+
if (sysMsg) sysMsg.content += knowledgeContext;
|
|
3018
|
+
}
|
|
2203
3019
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
3020
|
+
if (prompt && !messages.some((m) => m.role === "user")) {
|
|
3021
|
+
messages.push({ role: "user", content: prompt });
|
|
3022
|
+
}
|
|
3023
|
+
const result = await generateText({
|
|
3024
|
+
model: opts.model,
|
|
3025
|
+
messages,
|
|
3026
|
+
tools: opts.tools,
|
|
3027
|
+
maxSteps: chatOpts.maxSteps ?? opts.maxSteps ?? 1,
|
|
3028
|
+
temperature: chatOpts.temperature ?? opts.temperature,
|
|
3029
|
+
maxTokens: chatOpts.maxTokens ?? opts.maxTokens,
|
|
3030
|
+
abortSignal: req.signal
|
|
3031
|
+
});
|
|
3032
|
+
return {
|
|
3033
|
+
text: result.text,
|
|
3034
|
+
toolCalls: result.toolCalls,
|
|
3035
|
+
toolResults: result.toolResults,
|
|
3036
|
+
steps: result.steps,
|
|
3037
|
+
finishReason: result.finishReason,
|
|
3038
|
+
usage: result.usage
|
|
3039
|
+
};
|
|
3040
|
+
},
|
|
3041
|
+
chatStreamResponse(streamOpts) {
|
|
3042
|
+
const encoder = new TextEncoder();
|
|
3043
|
+
const stream = new ReadableStream({
|
|
3044
|
+
async start(controller) {
|
|
3045
|
+
try {
|
|
3046
|
+
const result = await ctx.agent.chat(
|
|
3047
|
+
streamOpts.messages.filter((m) => m.role === "user").pop()?.content ?? "",
|
|
3048
|
+
{
|
|
3049
|
+
messages: streamOpts.messages,
|
|
3050
|
+
system: streamOpts.system
|
|
3051
|
+
}
|
|
3052
|
+
);
|
|
3053
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "text", content: result.text })}
|
|
3054
|
+
|
|
3055
|
+
`));
|
|
3056
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
3057
|
+
controller.close();
|
|
3058
|
+
} catch (err) {
|
|
3059
|
+
controller.enqueue(encoder.encode(
|
|
3060
|
+
`data: ${JSON.stringify({ type: "error", error: String(err) })}
|
|
3061
|
+
|
|
3062
|
+
`
|
|
3063
|
+
));
|
|
3064
|
+
controller.close();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
return new Response(stream, {
|
|
3069
|
+
headers: {
|
|
3070
|
+
"Content-Type": "text/event-stream",
|
|
3071
|
+
"Cache-Control": "no-cache",
|
|
3072
|
+
Connection: "keep-alive"
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
2207
3077
|
return next(req, ctx);
|
|
2208
3078
|
};
|
|
2209
|
-
return mw;
|
|
2210
3079
|
}
|
|
2211
3080
|
export {
|
|
2212
3081
|
DEFAULT_MAX_BODY,
|
|
2213
3082
|
ErrorBoundary,
|
|
2214
|
-
Form,
|
|
2215
3083
|
HttpError,
|
|
2216
3084
|
Link,
|
|
2217
3085
|
MIGRATIONS_TABLE,
|
|
2218
3086
|
Router,
|
|
2219
3087
|
ServerDataContext,
|
|
3088
|
+
agent,
|
|
3089
|
+
ai,
|
|
3090
|
+
auth,
|
|
2220
3091
|
compress,
|
|
2221
3092
|
cors,
|
|
2222
3093
|
createHub,
|
|
2223
3094
|
currentTrace,
|
|
2224
3095
|
currentTraceId,
|
|
3096
|
+
esbuildDev,
|
|
2225
3097
|
graphql,
|
|
2226
3098
|
helmet,
|
|
2227
3099
|
logger,
|
|
@@ -2229,16 +3101,15 @@ export {
|
|
|
2229
3101
|
queue,
|
|
2230
3102
|
rateLimit,
|
|
2231
3103
|
react,
|
|
3104
|
+
reactRouter,
|
|
2232
3105
|
redis,
|
|
2233
3106
|
runWithTrace,
|
|
3107
|
+
sandbox,
|
|
2234
3108
|
serve,
|
|
2235
3109
|
serveStatic,
|
|
3110
|
+
tailwindDev,
|
|
2236
3111
|
trace,
|
|
2237
3112
|
traceElapsed,
|
|
2238
3113
|
upload,
|
|
2239
|
-
useNavigate,
|
|
2240
|
-
useNavigation,
|
|
2241
|
-
useParams,
|
|
2242
|
-
useRevalidate,
|
|
2243
3114
|
useServerData
|
|
2244
3115
|
};
|