weifuwu 0.8.0 → 0.9.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 +36 -5
- package/dist/index.js +432 -145
- package/dist/serve.d.ts +2 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -853,7 +853,23 @@ import { tsx } from 'weifuwu/tsx'
|
|
|
853
853
|
const app = new Router()
|
|
854
854
|
app.use('/', await tsx({ dir: './pages/' }))
|
|
855
855
|
|
|
856
|
-
serve(app.handler(), { port: 3000 })
|
|
856
|
+
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### Development mode
|
|
860
|
+
|
|
861
|
+
`tsx()` automatically runs in development mode (`NODE_ENV !== 'production'`):
|
|
862
|
+
|
|
863
|
+
- **File watching** — editing a `.tsx`/`.ts` file triggers recompilation and the browser auto-refreshes via WebSocket
|
|
864
|
+
- **Tailwind CSS** — if an `app.css` or `globals.css` file is found, Tailwind CSS is processed automatically. Write `className` directly.
|
|
865
|
+
- **`@` aliases** — if `tsconfig.json` or `jsconfig.json` has `compilerOptions.paths`, the `@` alias is passed to esbuild automatically (works with shadcn/ui)
|
|
866
|
+
- **Process state preserved** — DB connections, WebSockets, in-memory caches are not lost
|
|
867
|
+
|
|
868
|
+
Production mode (`NODE_ENV=production`) disables file watching and live reload. All other features work the same.
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
node app.ts # development
|
|
872
|
+
NODE_ENV=production node app.ts # production
|
|
857
873
|
```
|
|
858
874
|
|
|
859
875
|
### File conventions
|
|
@@ -947,12 +963,12 @@ app.use('/graphql', graphql(() => ({ schema: `type Query { hello: String }`, res
|
|
|
947
963
|
app.use('/agent', workflow(() => ({ tools: myTools, stream: true })))
|
|
948
964
|
app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
|
|
949
965
|
|
|
950
|
-
serve(app.handler())
|
|
966
|
+
serve(app.handler(), { websocket: app.websocketHandler() })
|
|
951
967
|
```
|
|
952
968
|
|
|
953
969
|
```bash
|
|
954
|
-
node
|
|
955
|
-
node app.ts
|
|
970
|
+
node app.ts # development (auto-reload on changes)
|
|
971
|
+
NODE_ENV=production node app.ts # production
|
|
956
972
|
```
|
|
957
973
|
|
|
958
974
|
No build step, no configuration file — just Node.js.
|
|
@@ -1094,10 +1110,25 @@ Returns `MessagerModule` — `{ migrate, router, wsHandler, send, close }`.
|
|
|
1094
1110
|
|
|
1095
1111
|
| Option | Default | Description |
|
|
1096
1112
|
|--------|---------|-------------|
|
|
1097
|
-
| `dir`
|
|
1113
|
+
| `dir` | — | Pages directory path |
|
|
1098
1114
|
|
|
1099
1115
|
Returns `Promise<Router>`.
|
|
1100
1116
|
|
|
1117
|
+
Development features (auto-detected, no configuration needed):
|
|
1118
|
+
|
|
1119
|
+
| Feature | Behavior |
|
|
1120
|
+
|---------|----------|
|
|
1121
|
+
| **File watching** | Enabled when `NODE_ENV !== 'production'`. Watches pages directory, recompiles on change, sends live-reload signal via WebSocket |
|
|
1122
|
+
| **Tailwind CSS** | Auto-detected when `app.css` / `globals.css` exists. Processed through PostCSS + Tailwind plugin. Served at `/__wfw/style.css` and auto-injected into HTML `<head>` |
|
|
1123
|
+
| **`@` alias** | Read from `tsconfig.json` / `jsconfig.json` `compilerOptions.paths` and passed to esbuild |
|
|
1124
|
+
| **WebSocket live reload** | Endpoint at `/__weifuwu/livereload`. Browser auto-refreshes on file changes or server restart |
|
|
1125
|
+
|
|
1126
|
+
To use WebSocket features, pass `router.websocketHandler()` to `serve()`:
|
|
1127
|
+
|
|
1128
|
+
```ts
|
|
1129
|
+
serve(app.handler(), { websocket: app.websocketHandler() })
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1101
1132
|
### `Router`
|
|
1102
1133
|
|
|
1103
1134
|
| Method | Description |
|
package/dist/index.js
CHANGED
|
@@ -10718,9 +10718,24 @@ var require_built3 = __commonJS({
|
|
|
10718
10718
|
|
|
10719
10719
|
// serve.ts
|
|
10720
10720
|
import http from "node:http";
|
|
10721
|
-
async function readBody(req) {
|
|
10721
|
+
async function readBody(req, maxSize) {
|
|
10722
|
+
if (maxSize) {
|
|
10723
|
+
const cl = parseInt(req.headers["content-length"] ?? "0", 10);
|
|
10724
|
+
if (cl > maxSize) {
|
|
10725
|
+
const err = new Error("Request body too large");
|
|
10726
|
+
err.status = 413;
|
|
10727
|
+
throw err;
|
|
10728
|
+
}
|
|
10729
|
+
}
|
|
10722
10730
|
const chunks = [];
|
|
10731
|
+
let total = 0;
|
|
10723
10732
|
for await (const chunk of req) {
|
|
10733
|
+
total += chunk.byteLength;
|
|
10734
|
+
if (maxSize && total > maxSize) {
|
|
10735
|
+
const err = new Error("Request body too large");
|
|
10736
|
+
err.status = 413;
|
|
10737
|
+
throw err;
|
|
10738
|
+
}
|
|
10724
10739
|
chunks.push(chunk);
|
|
10725
10740
|
}
|
|
10726
10741
|
return Buffer.concat(chunks);
|
|
@@ -10766,11 +10781,16 @@ function serve(handler, options) {
|
|
|
10766
10781
|
const hostname = options?.hostname ?? "0.0.0.0";
|
|
10767
10782
|
const server = http.createServer(async (req, res) => {
|
|
10768
10783
|
try {
|
|
10769
|
-
const body = await readBody(req);
|
|
10784
|
+
const body = await readBody(req, options?.maxBodySize);
|
|
10770
10785
|
const [request, query] = createRequest(req, body);
|
|
10771
10786
|
const response = await handler(request, { params: {}, query });
|
|
10772
10787
|
await sendResponse(res, response);
|
|
10773
|
-
} catch {
|
|
10788
|
+
} catch (err) {
|
|
10789
|
+
if (err?.status === 413) {
|
|
10790
|
+
res.writeHead(413, { "Content-Type": "text/plain" });
|
|
10791
|
+
res.end("Request Body Too Large");
|
|
10792
|
+
return;
|
|
10793
|
+
}
|
|
10774
10794
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
10775
10795
|
res.end("Internal Server Error");
|
|
10776
10796
|
}
|
|
@@ -10807,7 +10827,10 @@ function serve(handler, options) {
|
|
|
10807
10827
|
});
|
|
10808
10828
|
return {
|
|
10809
10829
|
stop: () => {
|
|
10810
|
-
|
|
10830
|
+
return new Promise((resolve3) => {
|
|
10831
|
+
server.closeAllConnections();
|
|
10832
|
+
server.close(() => resolve3());
|
|
10833
|
+
});
|
|
10811
10834
|
},
|
|
10812
10835
|
ready,
|
|
10813
10836
|
get port() {
|
|
@@ -10991,39 +11014,51 @@ var Router = class _Router {
|
|
|
10991
11014
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
10992
11015
|
const query = Object.fromEntries(url.searchParams);
|
|
10993
11016
|
const match = router.matchWsTrie(wsRoot, segments);
|
|
10994
|
-
if (
|
|
10995
|
-
|
|
11017
|
+
if (match) {
|
|
11018
|
+
const webReq = new Request(url.href, {
|
|
11019
|
+
method: req.method ?? "GET",
|
|
11020
|
+
headers: Object.fromEntries(
|
|
11021
|
+
Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v ?? ""])
|
|
11022
|
+
)
|
|
11023
|
+
});
|
|
11024
|
+
const ctx = { params: match.params, query };
|
|
11025
|
+
if (match.middlewares.length === 0) {
|
|
11026
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx);
|
|
11027
|
+
return;
|
|
11028
|
+
}
|
|
11029
|
+
let index = 0;
|
|
11030
|
+
const dispatch = async (innerReq, ctx2) => {
|
|
11031
|
+
if (index < match.middlewares.length) {
|
|
11032
|
+
const mw = match.middlewares[index++];
|
|
11033
|
+
return mw(innerReq, ctx2, dispatch);
|
|
11034
|
+
}
|
|
11035
|
+
return await new Promise((resolve3) => {
|
|
11036
|
+
try {
|
|
11037
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx2);
|
|
11038
|
+
resolve3(new Response(null, { status: 101 }));
|
|
11039
|
+
} catch {
|
|
11040
|
+
socket.destroy();
|
|
11041
|
+
resolve3(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
11042
|
+
}
|
|
11043
|
+
});
|
|
11044
|
+
};
|
|
11045
|
+
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
11046
|
+
if (result.status !== 101) {
|
|
11047
|
+
sendHttpResponseOnSocket(socket, result);
|
|
11048
|
+
}
|
|
11049
|
+
}).catch(() => {
|
|
11050
|
+
socket.destroy();
|
|
11051
|
+
});
|
|
10996
11052
|
return;
|
|
10997
11053
|
}
|
|
10998
|
-
const
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
)
|
|
11003
|
-
});
|
|
11004
|
-
const ctx = { params: match.params, query };
|
|
11005
|
-
if (match.middlewares.length === 0) {
|
|
11006
|
-
upgradeSocket(wss, req, socket, head, match.handler, ctx);
|
|
11054
|
+
const httpMatch = router.matchTrie("GET", segments);
|
|
11055
|
+
if (httpMatch?.subRouter) {
|
|
11056
|
+
const remaining = "/" + segments.slice(httpMatch.subRouter.remainingIdx).join("/");
|
|
11057
|
+
req.url = remaining;
|
|
11058
|
+
httpMatch.subRouter.router.websocketHandler()(req, socket, head);
|
|
11007
11059
|
return;
|
|
11008
11060
|
}
|
|
11009
|
-
|
|
11010
|
-
const dispatch = async (innerReq, ctx2) => {
|
|
11011
|
-
if (index < match.middlewares.length) {
|
|
11012
|
-
const mw = match.middlewares[index++];
|
|
11013
|
-
return mw(innerReq, ctx2, dispatch);
|
|
11014
|
-
}
|
|
11015
|
-
return await new Promise((resolve3) => {
|
|
11016
|
-
upgradeSocket(wss, req, socket, head, match.handler, ctx2);
|
|
11017
|
-
resolve3(new Response(null, { status: 101 }));
|
|
11018
|
-
});
|
|
11019
|
-
};
|
|
11020
|
-
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
11021
|
-
if (result.status !== 101) {
|
|
11022
|
-
sendHttpResponseOnSocket(socket, result);
|
|
11023
|
-
}
|
|
11024
|
-
}).catch(() => {
|
|
11025
|
-
socket.destroy();
|
|
11026
|
-
});
|
|
11061
|
+
socket.destroy();
|
|
11027
11062
|
};
|
|
11028
11063
|
}
|
|
11029
11064
|
splitPath(path2) {
|
|
@@ -11185,14 +11220,64 @@ function sendHttpResponseOnSocket(socket, response) {
|
|
|
11185
11220
|
import { createElement, createContext, useContext } from "react";
|
|
11186
11221
|
import { renderToReadableStream } from "react-dom/server";
|
|
11187
11222
|
import * as esbuild from "esbuild";
|
|
11188
|
-
import { readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
|
|
11189
|
-
import
|
|
11223
|
+
import { readdirSync, statSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
11224
|
+
import chokidar from "chokidar";
|
|
11225
|
+
import { join, relative, resolve, sep, dirname, basename } from "node:path";
|
|
11190
11226
|
import { pathToFileURL } from "node:url";
|
|
11191
11227
|
import { createHash } from "node:crypto";
|
|
11192
11228
|
var TsxContext = createContext({ params: {}, query: {} });
|
|
11193
11229
|
function useTsx() {
|
|
11194
11230
|
return useContext(TsxContext);
|
|
11195
11231
|
}
|
|
11232
|
+
var pageModules = /* @__PURE__ */ new Map();
|
|
11233
|
+
var layoutModules = /* @__PURE__ */ new Map();
|
|
11234
|
+
var loadModules = /* @__PURE__ */ new Map();
|
|
11235
|
+
var routeModules = /* @__PURE__ */ new Map();
|
|
11236
|
+
var clientBundles = /* @__PURE__ */ new Map();
|
|
11237
|
+
var liveReloadClients = /* @__PURE__ */ new Set();
|
|
11238
|
+
var _watcher = null;
|
|
11239
|
+
var _cssWatcher = null;
|
|
11240
|
+
function broadcastReload() {
|
|
11241
|
+
for (const ws of liveReloadClients) {
|
|
11242
|
+
try {
|
|
11243
|
+
ws.send("reload");
|
|
11244
|
+
} catch {
|
|
11245
|
+
liveReloadClients.delete(ws);
|
|
11246
|
+
}
|
|
11247
|
+
}
|
|
11248
|
+
}
|
|
11249
|
+
var tailwindCssUrl = null;
|
|
11250
|
+
var tailwindCssCode = "";
|
|
11251
|
+
var _projectDir = "";
|
|
11252
|
+
var _watcherStarted = false;
|
|
11253
|
+
var _alias = null;
|
|
11254
|
+
function resolveAliases() {
|
|
11255
|
+
if (_alias) return _alias;
|
|
11256
|
+
const configFiles = ["tsconfig.json", "jsconfig.json"];
|
|
11257
|
+
for (const file of configFiles) {
|
|
11258
|
+
const p = resolve(file);
|
|
11259
|
+
if (existsSync(p)) {
|
|
11260
|
+
try {
|
|
11261
|
+
const config = JSON.parse(readFileSync(p, "utf-8"));
|
|
11262
|
+
const paths = config.compilerOptions?.paths;
|
|
11263
|
+
if (paths) {
|
|
11264
|
+
const alias = {};
|
|
11265
|
+
for (const [key, values] of Object.entries(paths)) {
|
|
11266
|
+
const cleanKey = key.replace("/*", "");
|
|
11267
|
+
const val = values[0]?.replace("/*", "");
|
|
11268
|
+
if (val) alias[cleanKey] = resolve(dirname(p), val);
|
|
11269
|
+
}
|
|
11270
|
+
_alias = alias;
|
|
11271
|
+
return alias;
|
|
11272
|
+
}
|
|
11273
|
+
} catch {
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
}
|
|
11277
|
+
_alias = {};
|
|
11278
|
+
return {};
|
|
11279
|
+
}
|
|
11280
|
+
var isDev = process.env.NODE_ENV !== "production";
|
|
11196
11281
|
function id(s) {
|
|
11197
11282
|
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
11198
11283
|
}
|
|
@@ -11296,7 +11381,7 @@ function resolveLayouts(dir, pagesDir) {
|
|
|
11296
11381
|
}
|
|
11297
11382
|
return layouts.reverse();
|
|
11298
11383
|
}
|
|
11299
|
-
async function compileAll(files, outDir, platform) {
|
|
11384
|
+
async function compileAll(files, outDir, platform, alias) {
|
|
11300
11385
|
const entryPoints = {};
|
|
11301
11386
|
for (const f of files) {
|
|
11302
11387
|
entryPoints[id(f)] = f;
|
|
@@ -11320,23 +11405,21 @@ async function compileAll(files, outDir, platform) {
|
|
|
11320
11405
|
"@graphql-tools/schema",
|
|
11321
11406
|
"ai"
|
|
11322
11407
|
],
|
|
11408
|
+
alias,
|
|
11323
11409
|
write: true,
|
|
11324
11410
|
allowOverwrite: true
|
|
11325
11411
|
});
|
|
11326
11412
|
}
|
|
11327
11413
|
function compiledUrl(filePath, outDir) {
|
|
11328
|
-
const hash = id(join(outDir, id(filePath)));
|
|
11329
11414
|
const p = join(outDir, id(filePath) + ".js");
|
|
11330
11415
|
return pathToFileURL(p).href;
|
|
11331
11416
|
}
|
|
11332
|
-
var clientBundleCache = /* @__PURE__ */ new Map();
|
|
11333
11417
|
var clientRouteLog = /* @__PURE__ */ new WeakMap();
|
|
11334
11418
|
async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
|
|
11335
11419
|
const key = id(entryPath);
|
|
11336
11420
|
const url = `/__wfw/client/${key}.js`;
|
|
11337
11421
|
if (!clientRouteLog.get(router)?.has(url)) {
|
|
11338
|
-
|
|
11339
|
-
if (!buf) {
|
|
11422
|
+
if (!clientBundles.has(key)) {
|
|
11340
11423
|
try {
|
|
11341
11424
|
const nested = layoutPaths.slice(1);
|
|
11342
11425
|
const layoutsImport = nested.map(
|
|
@@ -11362,34 +11445,46 @@ async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router)
|
|
|
11362
11445
|
format: "esm",
|
|
11363
11446
|
jsx: "automatic",
|
|
11364
11447
|
jsxImportSource: "react",
|
|
11448
|
+
alias: resolveAliases(),
|
|
11365
11449
|
write: false,
|
|
11366
11450
|
minify: true
|
|
11367
11451
|
});
|
|
11368
|
-
|
|
11369
|
-
clientBundleCache.set(key, buf);
|
|
11452
|
+
clientBundles.set(key, result.outputFiles[0].contents);
|
|
11370
11453
|
} catch (err) {
|
|
11371
11454
|
console.error("hydration bundle failed:", err);
|
|
11372
11455
|
return null;
|
|
11373
11456
|
}
|
|
11374
11457
|
}
|
|
11375
|
-
router.get(url, () =>
|
|
11376
|
-
|
|
11377
|
-
|
|
11458
|
+
router.get(url, () => {
|
|
11459
|
+
const buf = clientBundles.get(key);
|
|
11460
|
+
return buf ? new Response(buf, {
|
|
11461
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
11462
|
+
}) : new Response("", { status: 500 });
|
|
11463
|
+
});
|
|
11378
11464
|
const set = clientRouteLog.get(router) ?? /* @__PURE__ */ new Set();
|
|
11379
11465
|
set.add(url);
|
|
11380
11466
|
clientRouteLog.set(router, set);
|
|
11381
11467
|
}
|
|
11382
11468
|
return { url };
|
|
11383
11469
|
}
|
|
11384
|
-
function makeSsrHandler(
|
|
11470
|
+
function makeSsrHandler(entryPath, layoutPaths, loadPath, pagesDir, router) {
|
|
11385
11471
|
return async (req, ctx) => {
|
|
11472
|
+
const pageMod = pageModules.get(entryPath);
|
|
11473
|
+
if (!pageMod) return new Response("", { status: 500 });
|
|
11474
|
+
const Component = pageMod.default;
|
|
11475
|
+
const loadMod = loadPath ? loadModules.get(loadPath) : void 0;
|
|
11476
|
+
const loadFn = loadMod?.default;
|
|
11386
11477
|
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
11387
11478
|
const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
|
|
11388
11479
|
let element = createElement(Component, allProps);
|
|
11389
|
-
for (let i =
|
|
11480
|
+
for (let i = layoutPaths.length - 1; i >= 0; i--) {
|
|
11481
|
+
const lp = layoutPaths[i];
|
|
11482
|
+
const LMod = layoutModules.get(lp);
|
|
11483
|
+
if (!LMod) continue;
|
|
11484
|
+
const Layout = LMod.default;
|
|
11390
11485
|
const isRoot = i === 0;
|
|
11391
11486
|
element = createElement(
|
|
11392
|
-
|
|
11487
|
+
Layout,
|
|
11393
11488
|
isRoot ? { children: element, req, ctx } : { children: element }
|
|
11394
11489
|
);
|
|
11395
11490
|
}
|
|
@@ -11404,9 +11499,20 @@ function makeSsrHandler(Component, loadFn, layouts, entryPath, layoutPaths, page
|
|
|
11404
11499
|
if (bundle) {
|
|
11405
11500
|
scripts.push(`<script type="module" src="${bundle.url}"></script>`);
|
|
11406
11501
|
}
|
|
11407
|
-
|
|
11502
|
+
let html = `<!DOCTYPE html>
|
|
11408
11503
|
${body}
|
|
11409
11504
|
${scripts.join("\n")}`;
|
|
11505
|
+
if (tailwindCssUrl && html.includes("</head>")) {
|
|
11506
|
+
html = html.replace(
|
|
11507
|
+
"</head>",
|
|
11508
|
+
`<link rel="stylesheet" href="${tailwindCssUrl}" />
|
|
11509
|
+
</head>`
|
|
11510
|
+
);
|
|
11511
|
+
}
|
|
11512
|
+
if (isDev) {
|
|
11513
|
+
html += `
|
|
11514
|
+
<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
|
|
11515
|
+
}
|
|
11410
11516
|
return new Response(html, {
|
|
11411
11517
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
11412
11518
|
});
|
|
@@ -11414,6 +11520,7 @@ ${scripts.join("\n")}`;
|
|
|
11414
11520
|
}
|
|
11415
11521
|
async function tsx(options) {
|
|
11416
11522
|
const pagesDir = resolve(options.dir);
|
|
11523
|
+
_projectDir = resolve(pagesDir, "..");
|
|
11417
11524
|
const outDir = join(pagesDir, "..", ".weifuwu", "ssr");
|
|
11418
11525
|
const pages = scanPages(pagesDir);
|
|
11419
11526
|
if (pages.length === 0) return new Router();
|
|
@@ -11432,79 +11539,102 @@ async function tsx(options) {
|
|
|
11432
11539
|
for (const lp of rootLayouts) allFiles.add(lp);
|
|
11433
11540
|
}
|
|
11434
11541
|
mkdirSync(outDir, { recursive: true });
|
|
11435
|
-
|
|
11542
|
+
const alias = resolveAliases();
|
|
11543
|
+
await compileAll([...allFiles], outDir, "node", alias);
|
|
11436
11544
|
const router = new Router();
|
|
11545
|
+
const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
11437
11546
|
for (const p of pages) {
|
|
11438
11547
|
if (p.routeOnly && p.routePath) {
|
|
11439
11548
|
const rUrl = compiledUrl(p.routePath, outDir);
|
|
11440
11549
|
const modR = await import(rUrl);
|
|
11441
|
-
const
|
|
11442
|
-
for (const
|
|
11443
|
-
if (modR[
|
|
11444
|
-
|
|
11445
|
-
|
|
11550
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
11551
|
+
for (const m of ["GET", ...methods]) {
|
|
11552
|
+
if (modR[m]) handlers.set(m, modR[m]);
|
|
11553
|
+
}
|
|
11554
|
+
routeModules.set(p.routePath, handlers);
|
|
11555
|
+
router.route(
|
|
11556
|
+
"GET",
|
|
11557
|
+
p.route,
|
|
11558
|
+
(req, ctx) => routeModules.get(p.routePath)?.get("GET")?.(req, ctx) ?? new Response("", { status: 501 })
|
|
11559
|
+
);
|
|
11560
|
+
for (const m of methods) {
|
|
11561
|
+
router.route(
|
|
11562
|
+
m,
|
|
11563
|
+
p.route,
|
|
11564
|
+
(req, ctx) => routeModules.get(p.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
|
|
11565
|
+
);
|
|
11446
11566
|
}
|
|
11447
11567
|
continue;
|
|
11448
11568
|
}
|
|
11449
|
-
const
|
|
11450
|
-
|
|
11451
|
-
const Component = mod.default;
|
|
11452
|
-
let loadFn;
|
|
11569
|
+
const pageUrl = compiledUrl(p.entryPath, outDir);
|
|
11570
|
+
pageModules.set(p.entryPath, await import(pageUrl));
|
|
11453
11571
|
if (p.loadPath) {
|
|
11454
11572
|
const loadUrl = compiledUrl(p.loadPath, outDir);
|
|
11455
|
-
|
|
11456
|
-
loadFn = modLoad.default;
|
|
11573
|
+
loadModules.set(p.loadPath, await import(loadUrl));
|
|
11457
11574
|
}
|
|
11458
|
-
const layoutComponents = [];
|
|
11459
11575
|
for (const lp of p.layouts) {
|
|
11460
11576
|
const lUrl = compiledUrl(lp, outDir);
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
}
|
|
11464
|
-
const handler = makeSsrHandler(
|
|
11465
|
-
Component,
|
|
11466
|
-
loadFn,
|
|
11467
|
-
layoutComponents,
|
|
11468
|
-
p.entryPath,
|
|
11469
|
-
p.layouts,
|
|
11470
|
-
pagesDir,
|
|
11471
|
-
router
|
|
11472
|
-
);
|
|
11473
|
-
router.get(p.route, handler);
|
|
11577
|
+
layoutModules.set(lp, await import(lUrl));
|
|
11578
|
+
}
|
|
11474
11579
|
if (p.routePath) {
|
|
11475
11580
|
const rUrl = compiledUrl(p.routePath, outDir);
|
|
11476
11581
|
const modR = await import(rUrl);
|
|
11477
|
-
const
|
|
11478
|
-
for (const
|
|
11479
|
-
if (modR[
|
|
11480
|
-
|
|
11481
|
-
|
|
11582
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
11583
|
+
for (const m of methods) {
|
|
11584
|
+
if (modR[m]) handlers.set(m, modR[m]);
|
|
11585
|
+
}
|
|
11586
|
+
routeModules.set(p.routePath, handlers);
|
|
11587
|
+
}
|
|
11588
|
+
const handler = makeSsrHandler(p.entryPath, p.layouts, p.loadPath, pagesDir, router);
|
|
11589
|
+
router.get(p.route, handler);
|
|
11590
|
+
if (p.routePath) {
|
|
11591
|
+
for (const m of methods) {
|
|
11592
|
+
router.route(
|
|
11593
|
+
m,
|
|
11594
|
+
p.route,
|
|
11595
|
+
(req, ctx) => routeModules.get(p.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
|
|
11596
|
+
);
|
|
11482
11597
|
}
|
|
11483
11598
|
}
|
|
11484
11599
|
}
|
|
11485
11600
|
if (hasNotFound) {
|
|
11486
11601
|
const nfUrl = compiledUrl(nfPath, outDir);
|
|
11487
|
-
|
|
11488
|
-
const NfComponent = modNf.default;
|
|
11489
|
-
const nfLayouts = [];
|
|
11602
|
+
pageModules.set(nfPath, await import(nfUrl));
|
|
11490
11603
|
const rootLayouts = resolveLayouts(pagesDir, pagesDir);
|
|
11491
11604
|
for (const lp of rootLayouts) {
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11605
|
+
if (!layoutModules.has(lp)) {
|
|
11606
|
+
const lUrl = compiledUrl(lp, outDir);
|
|
11607
|
+
layoutModules.set(lp, await import(lUrl));
|
|
11608
|
+
}
|
|
11495
11609
|
}
|
|
11496
11610
|
const handler = async (req, ctx) => {
|
|
11611
|
+
const nfMod = pageModules.get(nfPath);
|
|
11612
|
+
if (!nfMod) return new Response("Not Found", { status: 404 });
|
|
11613
|
+
const NfComponent = nfMod.default;
|
|
11497
11614
|
let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
|
|
11498
|
-
for (let i =
|
|
11499
|
-
|
|
11615
|
+
for (let i = rootLayouts.length - 1; i >= 0; i--) {
|
|
11616
|
+
const LMod = layoutModules.get(rootLayouts[i]);
|
|
11617
|
+
if (!LMod) continue;
|
|
11618
|
+
element = createElement(LMod.default, { children: element });
|
|
11500
11619
|
}
|
|
11501
11620
|
element = createElement(TsxContext.Provider, {
|
|
11502
11621
|
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
11503
11622
|
}, element);
|
|
11504
11623
|
const stream = await renderToReadableStream(element);
|
|
11505
11624
|
const body = await readStream(stream);
|
|
11506
|
-
|
|
11625
|
+
let html = `<!DOCTYPE html>
|
|
11507
11626
|
${body}`;
|
|
11627
|
+
if (tailwindCssUrl && html.includes("</head>")) {
|
|
11628
|
+
html = html.replace(
|
|
11629
|
+
"</head>",
|
|
11630
|
+
`<link rel="stylesheet" href="${tailwindCssUrl}" />
|
|
11631
|
+
</head>`
|
|
11632
|
+
);
|
|
11633
|
+
}
|
|
11634
|
+
if (isDev) {
|
|
11635
|
+
html += `
|
|
11636
|
+
<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
|
|
11637
|
+
}
|
|
11508
11638
|
return new Response(html, {
|
|
11509
11639
|
status: 404,
|
|
11510
11640
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
@@ -11512,8 +11642,150 @@ ${body}`;
|
|
|
11512
11642
|
};
|
|
11513
11643
|
router.all("/*", handler);
|
|
11514
11644
|
}
|
|
11645
|
+
tailwindCssUrl = await setupTailwind(pagesDir, router, alias);
|
|
11646
|
+
if (isDev) {
|
|
11647
|
+
router.ws("/__weifuwu/livereload", {
|
|
11648
|
+
open(ws) {
|
|
11649
|
+
liveReloadClients.add(ws);
|
|
11650
|
+
ws.on("close", () => liveReloadClients.delete(ws));
|
|
11651
|
+
ws.on("error", () => liveReloadClients.delete(ws));
|
|
11652
|
+
}
|
|
11653
|
+
});
|
|
11654
|
+
if (!_watcherStarted) {
|
|
11655
|
+
startFileWatcher(pagesDir, outDir);
|
|
11656
|
+
_watcherStarted = true;
|
|
11657
|
+
}
|
|
11658
|
+
}
|
|
11515
11659
|
return router;
|
|
11516
11660
|
}
|
|
11661
|
+
async function setupTailwind(pagesDir, router, alias) {
|
|
11662
|
+
let tailwindPlugin, postcss, autoprefixer;
|
|
11663
|
+
try {
|
|
11664
|
+
tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
11665
|
+
postcss = (await import("postcss")).default;
|
|
11666
|
+
autoprefixer = (await import("autoprefixer")).default;
|
|
11667
|
+
} catch {
|
|
11668
|
+
return null;
|
|
11669
|
+
}
|
|
11670
|
+
const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
|
|
11671
|
+
let inputFile = "";
|
|
11672
|
+
for (const c of candidates) {
|
|
11673
|
+
const p = resolve(pagesDir, "..", c);
|
|
11674
|
+
if (existsSync(p)) {
|
|
11675
|
+
inputFile = p;
|
|
11676
|
+
break;
|
|
11677
|
+
}
|
|
11678
|
+
}
|
|
11679
|
+
if (!inputFile) return null;
|
|
11680
|
+
try {
|
|
11681
|
+
const src = readFileSync(inputFile, "utf-8");
|
|
11682
|
+
const result = await postcss([tailwindPlugin(), autoprefixer]).process(src, { from: inputFile });
|
|
11683
|
+
tailwindCssCode = result.css;
|
|
11684
|
+
} catch (err) {
|
|
11685
|
+
console.warn("Tailwind CSS processing failed:", err.message);
|
|
11686
|
+
return null;
|
|
11687
|
+
}
|
|
11688
|
+
const url = "/__wfw/style.css";
|
|
11689
|
+
router.get(url, () => new Response(tailwindCssCode, {
|
|
11690
|
+
headers: { "content-type": "text/css; charset=utf-8" }
|
|
11691
|
+
}));
|
|
11692
|
+
if (isDev) {
|
|
11693
|
+
_cssWatcher = chokidar.watch(inputFile, { persistent: false });
|
|
11694
|
+
_cssWatcher.on("change", async () => {
|
|
11695
|
+
try {
|
|
11696
|
+
const newSrc = readFileSync(inputFile, "utf-8");
|
|
11697
|
+
const newResult = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
|
|
11698
|
+
tailwindCssCode = newResult.css;
|
|
11699
|
+
broadcastReload();
|
|
11700
|
+
} catch (err) {
|
|
11701
|
+
console.warn("Tailwind CSS reprocessing failed:", err.message);
|
|
11702
|
+
}
|
|
11703
|
+
});
|
|
11704
|
+
}
|
|
11705
|
+
return url;
|
|
11706
|
+
}
|
|
11707
|
+
function startFileWatcher(pagesDir, outDir) {
|
|
11708
|
+
let timeout = null;
|
|
11709
|
+
const pending = /* @__PURE__ */ new Set();
|
|
11710
|
+
_watcher = chokidar.watch(pagesDir, {
|
|
11711
|
+
ignored: /(^|[/\\])\.(?!\.)|\.weifuwu/,
|
|
11712
|
+
persistent: false,
|
|
11713
|
+
ignoreInitial: true
|
|
11714
|
+
});
|
|
11715
|
+
_watcher.on("all", async (event, filePath) => {
|
|
11716
|
+
if (event !== "change" && event !== "add") return;
|
|
11717
|
+
if (!/\.tsx?$/.test(filePath)) return;
|
|
11718
|
+
pending.add(filePath);
|
|
11719
|
+
if (timeout) clearTimeout(timeout);
|
|
11720
|
+
timeout = setTimeout(async () => {
|
|
11721
|
+
timeout = null;
|
|
11722
|
+
const files = [...pending];
|
|
11723
|
+
pending.clear();
|
|
11724
|
+
for (const f of files) {
|
|
11725
|
+
if (existsSync(f)) await recompileAndSwap(f, outDir);
|
|
11726
|
+
}
|
|
11727
|
+
}, 50);
|
|
11728
|
+
});
|
|
11729
|
+
}
|
|
11730
|
+
async function recompileAndSwap(filePath, outDir) {
|
|
11731
|
+
try {
|
|
11732
|
+
await esbuild.build({
|
|
11733
|
+
entryPoints: { [id(filePath)]: filePath },
|
|
11734
|
+
outdir: outDir,
|
|
11735
|
+
alias: resolveAliases(),
|
|
11736
|
+
format: "esm",
|
|
11737
|
+
platform: "node",
|
|
11738
|
+
jsx: "automatic",
|
|
11739
|
+
jsxImportSource: "react",
|
|
11740
|
+
bundle: true,
|
|
11741
|
+
external: ["react", "react-dom", "esbuild", "graphql", "ws", "zod", "@graphql-tools/schema", "ai"],
|
|
11742
|
+
write: true,
|
|
11743
|
+
allowOverwrite: true
|
|
11744
|
+
});
|
|
11745
|
+
const bustUrl = compiledUrl(filePath, outDir) + "?t=" + Date.now();
|
|
11746
|
+
const freshMod = await import(bustUrl);
|
|
11747
|
+
const name15 = basename(filePath);
|
|
11748
|
+
if (name15 === "layout.tsx") {
|
|
11749
|
+
layoutModules.set(filePath, freshMod);
|
|
11750
|
+
} else if (name15 === "route.ts") {
|
|
11751
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
11752
|
+
for (const m of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) {
|
|
11753
|
+
if (freshMod[m]) handlers.set(m, freshMod[m]);
|
|
11754
|
+
}
|
|
11755
|
+
routeModules.set(filePath, handlers);
|
|
11756
|
+
} else if (name15 === "load.ts") {
|
|
11757
|
+
loadModules.set(filePath, freshMod);
|
|
11758
|
+
} else {
|
|
11759
|
+
pageModules.set(filePath, freshMod);
|
|
11760
|
+
clientBundles.delete(id(filePath));
|
|
11761
|
+
}
|
|
11762
|
+
if (tailwindCssUrl) {
|
|
11763
|
+
try {
|
|
11764
|
+
const tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
11765
|
+
const postcss = (await import("postcss")).default;
|
|
11766
|
+
const autoprefixer = (await import("autoprefixer")).default;
|
|
11767
|
+
const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
|
|
11768
|
+
let inputFile = "";
|
|
11769
|
+
for (const c of candidates) {
|
|
11770
|
+
const p = resolve(_projectDir, c);
|
|
11771
|
+
if (existsSync(p)) {
|
|
11772
|
+
inputFile = p;
|
|
11773
|
+
break;
|
|
11774
|
+
}
|
|
11775
|
+
}
|
|
11776
|
+
if (inputFile) {
|
|
11777
|
+
const newSrc = readFileSync(inputFile, "utf-8");
|
|
11778
|
+
const result = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
|
|
11779
|
+
tailwindCssCode = result.css;
|
|
11780
|
+
}
|
|
11781
|
+
} catch {
|
|
11782
|
+
}
|
|
11783
|
+
}
|
|
11784
|
+
broadcastReload();
|
|
11785
|
+
} catch (err) {
|
|
11786
|
+
console.error("recompile failed:", err.message);
|
|
11787
|
+
}
|
|
11788
|
+
}
|
|
11517
11789
|
|
|
11518
11790
|
// middleware.ts
|
|
11519
11791
|
function logger(options) {
|
|
@@ -11815,7 +12087,7 @@ function validate(schemas) {
|
|
|
11815
12087
|
if (issues.length > 0) {
|
|
11816
12088
|
return Response.json({ error: "Validation failed", issues }, { status: 400 });
|
|
11817
12089
|
}
|
|
11818
|
-
ctx.parsed = parsed;
|
|
12090
|
+
ctx.parsed = { ...ctx.parsed, ...parsed };
|
|
11819
12091
|
return next(req, ctx);
|
|
11820
12092
|
};
|
|
11821
12093
|
}
|
|
@@ -11831,7 +12103,11 @@ function getCookies(req) {
|
|
|
11831
12103
|
const name15 = pair.slice(0, idx).trim();
|
|
11832
12104
|
const value = pair.slice(idx + 1).trim();
|
|
11833
12105
|
if (name15) {
|
|
11834
|
-
|
|
12106
|
+
try {
|
|
12107
|
+
cookies[name15] = decodeURIComponent(value);
|
|
12108
|
+
} catch {
|
|
12109
|
+
cookies[name15] = value;
|
|
12110
|
+
}
|
|
11835
12111
|
}
|
|
11836
12112
|
}
|
|
11837
12113
|
return cookies;
|
|
@@ -11874,67 +12150,45 @@ function upload(options) {
|
|
|
11874
12150
|
const saveDir = options?.dir;
|
|
11875
12151
|
return async (req, ctx, next) => {
|
|
11876
12152
|
const ct = req.headers.get("content-type") ?? "";
|
|
11877
|
-
if (!ct.includes("multipart/form-data"))
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
return Response.json({ error: "
|
|
12153
|
+
if (!ct.includes("multipart/form-data")) return next(req, ctx);
|
|
12154
|
+
let formData;
|
|
12155
|
+
try {
|
|
12156
|
+
formData = await req.formData();
|
|
12157
|
+
} catch {
|
|
12158
|
+
return Response.json({ error: "Invalid multipart data" }, { status: 400 });
|
|
11883
12159
|
}
|
|
11884
|
-
const boundary = match[1] ?? match[2];
|
|
11885
|
-
const body = await req.text();
|
|
11886
|
-
const rawParts = body.split(`--${boundary}`).filter((p) => p && !p.startsWith("--") && !p.startsWith("\r\n--"));
|
|
11887
12160
|
const files = {};
|
|
11888
12161
|
const fields = {};
|
|
11889
|
-
for (const
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
const headers = {};
|
|
11894
|
-
while (i < lines.length && lines[i].length > 0) {
|
|
11895
|
-
const sep3 = lines[i].indexOf(": ");
|
|
11896
|
-
if (sep3 !== -1) headers[lines[i].slice(0, sep3).toLowerCase()] = lines[i].slice(sep3 + 2);
|
|
11897
|
-
i++;
|
|
11898
|
-
}
|
|
11899
|
-
i++;
|
|
11900
|
-
const bodyValue = lines.slice(i).join("\r\n");
|
|
11901
|
-
const disposition = headers["content-disposition"] ?? "";
|
|
11902
|
-
const nameMatch = disposition.match(/name="([^"]*)"/);
|
|
11903
|
-
if (!nameMatch) continue;
|
|
11904
|
-
const name15 = nameMatch[1];
|
|
11905
|
-
const filenameMatch = disposition.match(/filename="([^"]*)"/);
|
|
11906
|
-
const filename = filenameMatch?.[1];
|
|
11907
|
-
if (filename) {
|
|
11908
|
-
const buf = Buffer.from(bodyValue.replace(/\r?\n$/, ""), "binary");
|
|
11909
|
-
if (options?.allowedTypes) {
|
|
11910
|
-
const mime = headers["content-type"] ?? "application/octet-stream";
|
|
11911
|
-
if (!options.allowedTypes.includes(mime)) {
|
|
11912
|
-
return Response.json({ error: `File type not allowed: ${mime}` }, { status: 415 });
|
|
11913
|
-
}
|
|
12162
|
+
for (const [key, value] of formData) {
|
|
12163
|
+
if (value instanceof File) {
|
|
12164
|
+
if (options?.allowedTypes && !options.allowedTypes.includes(value.type)) {
|
|
12165
|
+
return Response.json({ error: `File type not allowed: ${value.type}` }, { status: 415 });
|
|
11914
12166
|
}
|
|
11915
|
-
if (options?.maxFileSize &&
|
|
11916
|
-
return Response.json({ error: `File too large: ${
|
|
12167
|
+
if (options?.maxFileSize && value.size > options.maxFileSize) {
|
|
12168
|
+
return Response.json({ error: `File too large: ${value.name}` }, { status: 413 });
|
|
11917
12169
|
}
|
|
12170
|
+
const buf = Buffer.from(await value.arrayBuffer());
|
|
11918
12171
|
const uf = {
|
|
11919
|
-
name:
|
|
11920
|
-
type:
|
|
12172
|
+
name: value.name,
|
|
12173
|
+
type: value.type,
|
|
11921
12174
|
size: buf.byteLength,
|
|
11922
12175
|
buffer: saveDir ? void 0 : buf
|
|
11923
12176
|
};
|
|
11924
12177
|
if (saveDir) {
|
|
11925
|
-
const
|
|
12178
|
+
const safeName = value.name.replace(/[/\\]/g, "");
|
|
12179
|
+
const filePath = join2(saveDir, `${randomUUID()}-${safeName}`);
|
|
11926
12180
|
await mkdir(saveDir, { recursive: true });
|
|
11927
12181
|
await writeFile(filePath, buf);
|
|
11928
12182
|
uf.path = filePath;
|
|
11929
12183
|
}
|
|
11930
|
-
if (files[
|
|
11931
|
-
const existing = files[
|
|
11932
|
-
files[
|
|
12184
|
+
if (files[key]) {
|
|
12185
|
+
const existing = files[key];
|
|
12186
|
+
files[key] = Array.isArray(existing) ? [...existing, uf] : [existing, uf];
|
|
11933
12187
|
} else {
|
|
11934
|
-
files[
|
|
12188
|
+
files[key] = uf;
|
|
11935
12189
|
}
|
|
11936
12190
|
} else {
|
|
11937
|
-
fields[
|
|
12191
|
+
fields[key] = value;
|
|
11938
12192
|
}
|
|
11939
12193
|
}
|
|
11940
12194
|
ctx.parsed = { ...ctx.parsed, files, fields };
|
|
@@ -13542,12 +13796,21 @@ function queue(opts) {
|
|
|
13542
13796
|
if (!running) return;
|
|
13543
13797
|
try {
|
|
13544
13798
|
const now = Date.now();
|
|
13545
|
-
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13799
|
+
while (true) {
|
|
13800
|
+
const result = await redis2.zpopmin(jobKey);
|
|
13801
|
+
if (result.length < 2) break;
|
|
13802
|
+
const raw = result[0];
|
|
13803
|
+
const score = parseInt(result[1], 10);
|
|
13804
|
+
if (score > now) {
|
|
13805
|
+
await redis2.zadd(jobKey, score, raw);
|
|
13806
|
+
break;
|
|
13807
|
+
}
|
|
13808
|
+
let job;
|
|
13809
|
+
try {
|
|
13810
|
+
job = JSON.parse(raw);
|
|
13811
|
+
} catch {
|
|
13812
|
+
continue;
|
|
13813
|
+
}
|
|
13551
13814
|
const handler = handlers.get(job.type);
|
|
13552
13815
|
if (handler) {
|
|
13553
13816
|
handler(job).then(() => {
|
|
@@ -13555,11 +13818,13 @@ function queue(opts) {
|
|
|
13555
13818
|
try {
|
|
13556
13819
|
const nextRun = cronNext(job.schedule);
|
|
13557
13820
|
const nextJob = { ...job, id: crypto3.randomUUID(), runAt: nextRun, createdAt: Date.now() };
|
|
13558
|
-
redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob))
|
|
13821
|
+
redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob)).catch(() => {
|
|
13822
|
+
});
|
|
13559
13823
|
} catch {
|
|
13560
13824
|
}
|
|
13561
13825
|
}
|
|
13562
|
-
}).catch(() => {
|
|
13826
|
+
}).catch((e) => {
|
|
13827
|
+
console.error("[queue] handler error:", e);
|
|
13563
13828
|
});
|
|
13564
13829
|
}
|
|
13565
13830
|
}
|
|
@@ -13844,6 +14109,10 @@ async function getUserTable(sql, tenantId, slug) {
|
|
|
13844
14109
|
`;
|
|
13845
14110
|
return row ?? null;
|
|
13846
14111
|
}
|
|
14112
|
+
function requireAdmin(ctx) {
|
|
14113
|
+
if (ctx.tenant?.role !== "admin") return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
14114
|
+
return null;
|
|
14115
|
+
}
|
|
13847
14116
|
function buildRouter(sql, usersTable) {
|
|
13848
14117
|
const r = new Router();
|
|
13849
14118
|
r.post("/sys/tenants", async (req, ctx) => {
|
|
@@ -13869,6 +14138,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13869
14138
|
return Response.json(rows);
|
|
13870
14139
|
});
|
|
13871
14140
|
r.post("/sys/tenants/invite", async (req, ctx) => {
|
|
14141
|
+
const err = requireAdmin(ctx);
|
|
14142
|
+
if (err) return err;
|
|
13872
14143
|
const { email, role = "member" } = await req.json();
|
|
13873
14144
|
const [user2] = await sql`
|
|
13874
14145
|
SELECT id FROM ${sql(usersTable)} WHERE "email" = ${email} LIMIT 1
|
|
@@ -13886,6 +14157,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13886
14157
|
return Response.json({ ok: true }, { status: 201 });
|
|
13887
14158
|
});
|
|
13888
14159
|
r.delete("/sys/tenants/members/:userId", async (req, ctx) => {
|
|
14160
|
+
const err = requireAdmin(ctx);
|
|
14161
|
+
if (err) return err;
|
|
13889
14162
|
const userId = parseInt(ctx.params.userId, 10);
|
|
13890
14163
|
await sql`
|
|
13891
14164
|
DELETE FROM "_tenant_members"
|
|
@@ -13894,6 +14167,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13894
14167
|
return Response.json({ ok: true });
|
|
13895
14168
|
});
|
|
13896
14169
|
r.post("/sys/tables", async (req, ctx) => {
|
|
14170
|
+
const err = requireAdmin(ctx);
|
|
14171
|
+
if (err) return err;
|
|
13897
14172
|
const body = await req.json();
|
|
13898
14173
|
const slugErr = validateSlug(body.slug);
|
|
13899
14174
|
if (slugErr) return Response.json({ error: slugErr }, { status: 400 });
|
|
@@ -13932,6 +14207,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13932
14207
|
return Response.json(table);
|
|
13933
14208
|
});
|
|
13934
14209
|
r.patch("/sys/tables/:slug", async (req, ctx) => {
|
|
14210
|
+
const err = requireAdmin(ctx);
|
|
14211
|
+
if (err) return err;
|
|
13935
14212
|
const body = await req.json();
|
|
13936
14213
|
if (!body.fields || !Array.isArray(body.fields)) {
|
|
13937
14214
|
return Response.json({ error: "fields array required" }, { status: 400 });
|
|
@@ -13952,6 +14229,8 @@ function buildRouter(sql, usersTable) {
|
|
|
13952
14229
|
return Response.json({ ...table, fields: merged });
|
|
13953
14230
|
});
|
|
13954
14231
|
r.delete("/sys/tables/:slug", async (_req, ctx) => {
|
|
14232
|
+
const err = requireAdmin(ctx);
|
|
14233
|
+
if (err) return err;
|
|
13955
14234
|
await sql.unsafe(dropTableSQL(ctx.tenant.id, ctx.params.slug));
|
|
13956
14235
|
await sql`
|
|
13957
14236
|
DELETE FROM "_user_tables"
|
|
@@ -24407,15 +24686,19 @@ function createWSHandler(deps) {
|
|
|
24407
24686
|
VALUES (${channel_id}, ${am.member_id}, 'agent', ${result.output})
|
|
24408
24687
|
`.then(([r]) => {
|
|
24409
24688
|
broadcastToChannel(channel_id, { type: "message", data: r });
|
|
24689
|
+
}).catch((e) => {
|
|
24690
|
+
console.error("[messager] agent reply insert failed:", e);
|
|
24410
24691
|
});
|
|
24411
24692
|
}
|
|
24412
|
-
}).catch(() => {
|
|
24693
|
+
}).catch((e) => {
|
|
24694
|
+
console.error("[messager] agent run failed:", e);
|
|
24413
24695
|
});
|
|
24414
24696
|
}
|
|
24415
24697
|
}
|
|
24416
24698
|
break;
|
|
24417
24699
|
}
|
|
24418
24700
|
case "typing": {
|
|
24701
|
+
if (channel_id) subscribe(ws, userId, channel_id);
|
|
24419
24702
|
broadcastToChannel(channel_id, {
|
|
24420
24703
|
type: "typing",
|
|
24421
24704
|
channel_id,
|
|
@@ -24426,6 +24709,7 @@ function createWSHandler(deps) {
|
|
|
24426
24709
|
}
|
|
24427
24710
|
case "read": {
|
|
24428
24711
|
if (!channel_id || !last_message_id) return;
|
|
24712
|
+
subscribe(ws, userId, channel_id);
|
|
24429
24713
|
await sql`
|
|
24430
24714
|
UPDATE "_channel_members"
|
|
24431
24715
|
SET last_read_id = ${last_message_id}, last_read_at = NOW()
|
|
@@ -24570,9 +24854,12 @@ function buildRouter3(deps) {
|
|
|
24570
24854
|
VALUES (${channelId}, ${am.member_id}, 'agent', ${result.output})
|
|
24571
24855
|
`.then(([r2]) => {
|
|
24572
24856
|
broadcastToChannel(channelId, { type: "message", data: r2 });
|
|
24857
|
+
}).catch((e) => {
|
|
24858
|
+
console.error("[messager] agent reply insert failed:", e);
|
|
24573
24859
|
});
|
|
24574
24860
|
}
|
|
24575
|
-
}).catch(() => {
|
|
24861
|
+
}).catch((e) => {
|
|
24862
|
+
console.error("[messager] agent run failed:", e);
|
|
24576
24863
|
});
|
|
24577
24864
|
}
|
|
24578
24865
|
}
|
package/dist/serve.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface ServeOptions {
|
|
|
6
6
|
hostname?: string;
|
|
7
7
|
signal?: AbortSignal;
|
|
8
8
|
websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
9
|
+
maxBodySize?: number;
|
|
9
10
|
}
|
|
10
11
|
export interface Server {
|
|
11
12
|
stop: () => void;
|
|
@@ -13,7 +14,7 @@ export interface Server {
|
|
|
13
14
|
readonly hostname: string;
|
|
14
15
|
ready: Promise<void>;
|
|
15
16
|
}
|
|
16
|
-
export declare function readBody(req: IncomingMessage): Promise<Buffer>;
|
|
17
|
+
export declare function readBody(req: IncomingMessage, maxSize?: number): Promise<Buffer>;
|
|
17
18
|
export declare function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>];
|
|
18
19
|
export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
19
20
|
export declare function serve(handler: Handler, options?: ServeOptions): Server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,21 +14,26 @@
|
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:react --external:react-dom --external:esbuild --external:graphql --external:ws --external:zod --external:@graphql-tools/schema --external:ai --external:postgres --external:jsonwebtoken",
|
|
17
|
+
"build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:react --external:react-dom --external:esbuild --external:graphql --external:ws --external:zod --external:@graphql-tools/schema --external:ai --external:postgres --external:jsonwebtoken --external:chokidar --external:tailwindcss --external:@tailwindcss/* --external:postcss --external:autoprefixer --external:lightningcss",
|
|
18
18
|
"prepublishOnly": "npm run build && tsc --emitDeclarationOnly --outdir dist",
|
|
19
19
|
"test": "node --test 'test/**/*.test.ts'"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@ai-sdk/openai": "^3.0.66",
|
|
23
23
|
"@graphql-tools/schema": "^10",
|
|
24
|
+
"@tailwindcss/postcss": "^4",
|
|
24
25
|
"ai": "^6",
|
|
26
|
+
"autoprefixer": "^10",
|
|
27
|
+
"chokidar": "^5.0.0",
|
|
25
28
|
"esbuild": "^0.28.0",
|
|
26
29
|
"graphql": "^16",
|
|
27
30
|
"ioredis": "^5.11.0",
|
|
28
31
|
"jsonwebtoken": "^9.0.3",
|
|
32
|
+
"postcss": "^8",
|
|
29
33
|
"postgres": "^3.4.9",
|
|
30
34
|
"react": "^19",
|
|
31
35
|
"react-dom": "^19",
|
|
36
|
+
"tailwindcss": "^4",
|
|
32
37
|
"ws": "^8",
|
|
33
38
|
"zod": "^4.4.3"
|
|
34
39
|
},
|