weifuwu 0.8.1 → 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 +338 -91
- 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
|
@@ -10827,7 +10827,10 @@ function serve(handler, options) {
|
|
|
10827
10827
|
});
|
|
10828
10828
|
return {
|
|
10829
10829
|
stop: () => {
|
|
10830
|
-
|
|
10830
|
+
return new Promise((resolve3) => {
|
|
10831
|
+
server.closeAllConnections();
|
|
10832
|
+
server.close(() => resolve3());
|
|
10833
|
+
});
|
|
10831
10834
|
},
|
|
10832
10835
|
ready,
|
|
10833
10836
|
get port() {
|
|
@@ -11011,44 +11014,51 @@ var Router = class _Router {
|
|
|
11011
11014
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
11012
11015
|
const query = Object.fromEntries(url.searchParams);
|
|
11013
11016
|
const match = router.matchWsTrie(wsRoot, segments);
|
|
11014
|
-
if (
|
|
11015
|
-
|
|
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
|
+
});
|
|
11016
11052
|
return;
|
|
11017
11053
|
}
|
|
11018
|
-
const
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
)
|
|
11023
|
-
});
|
|
11024
|
-
const ctx = { params: match.params, query };
|
|
11025
|
-
if (match.middlewares.length === 0) {
|
|
11026
|
-
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);
|
|
11027
11059
|
return;
|
|
11028
11060
|
}
|
|
11029
|
-
|
|
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
|
-
});
|
|
11061
|
+
socket.destroy();
|
|
11052
11062
|
};
|
|
11053
11063
|
}
|
|
11054
11064
|
splitPath(path2) {
|
|
@@ -11210,14 +11220,64 @@ function sendHttpResponseOnSocket(socket, response) {
|
|
|
11210
11220
|
import { createElement, createContext, useContext } from "react";
|
|
11211
11221
|
import { renderToReadableStream } from "react-dom/server";
|
|
11212
11222
|
import * as esbuild from "esbuild";
|
|
11213
|
-
import { readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
|
|
11214
|
-
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";
|
|
11215
11226
|
import { pathToFileURL } from "node:url";
|
|
11216
11227
|
import { createHash } from "node:crypto";
|
|
11217
11228
|
var TsxContext = createContext({ params: {}, query: {} });
|
|
11218
11229
|
function useTsx() {
|
|
11219
11230
|
return useContext(TsxContext);
|
|
11220
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";
|
|
11221
11281
|
function id(s) {
|
|
11222
11282
|
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
11223
11283
|
}
|
|
@@ -11321,7 +11381,7 @@ function resolveLayouts(dir, pagesDir) {
|
|
|
11321
11381
|
}
|
|
11322
11382
|
return layouts.reverse();
|
|
11323
11383
|
}
|
|
11324
|
-
async function compileAll(files, outDir, platform) {
|
|
11384
|
+
async function compileAll(files, outDir, platform, alias) {
|
|
11325
11385
|
const entryPoints = {};
|
|
11326
11386
|
for (const f of files) {
|
|
11327
11387
|
entryPoints[id(f)] = f;
|
|
@@ -11345,23 +11405,21 @@ async function compileAll(files, outDir, platform) {
|
|
|
11345
11405
|
"@graphql-tools/schema",
|
|
11346
11406
|
"ai"
|
|
11347
11407
|
],
|
|
11408
|
+
alias,
|
|
11348
11409
|
write: true,
|
|
11349
11410
|
allowOverwrite: true
|
|
11350
11411
|
});
|
|
11351
11412
|
}
|
|
11352
11413
|
function compiledUrl(filePath, outDir) {
|
|
11353
|
-
const hash = id(join(outDir, id(filePath)));
|
|
11354
11414
|
const p = join(outDir, id(filePath) + ".js");
|
|
11355
11415
|
return pathToFileURL(p).href;
|
|
11356
11416
|
}
|
|
11357
|
-
var clientBundleCache = /* @__PURE__ */ new Map();
|
|
11358
11417
|
var clientRouteLog = /* @__PURE__ */ new WeakMap();
|
|
11359
11418
|
async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
|
|
11360
11419
|
const key = id(entryPath);
|
|
11361
11420
|
const url = `/__wfw/client/${key}.js`;
|
|
11362
11421
|
if (!clientRouteLog.get(router)?.has(url)) {
|
|
11363
|
-
|
|
11364
|
-
if (!buf) {
|
|
11422
|
+
if (!clientBundles.has(key)) {
|
|
11365
11423
|
try {
|
|
11366
11424
|
const nested = layoutPaths.slice(1);
|
|
11367
11425
|
const layoutsImport = nested.map(
|
|
@@ -11387,34 +11445,46 @@ async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router)
|
|
|
11387
11445
|
format: "esm",
|
|
11388
11446
|
jsx: "automatic",
|
|
11389
11447
|
jsxImportSource: "react",
|
|
11448
|
+
alias: resolveAliases(),
|
|
11390
11449
|
write: false,
|
|
11391
11450
|
minify: true
|
|
11392
11451
|
});
|
|
11393
|
-
|
|
11394
|
-
clientBundleCache.set(key, buf);
|
|
11452
|
+
clientBundles.set(key, result.outputFiles[0].contents);
|
|
11395
11453
|
} catch (err) {
|
|
11396
11454
|
console.error("hydration bundle failed:", err);
|
|
11397
11455
|
return null;
|
|
11398
11456
|
}
|
|
11399
11457
|
}
|
|
11400
|
-
router.get(url, () =>
|
|
11401
|
-
|
|
11402
|
-
|
|
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
|
+
});
|
|
11403
11464
|
const set = clientRouteLog.get(router) ?? /* @__PURE__ */ new Set();
|
|
11404
11465
|
set.add(url);
|
|
11405
11466
|
clientRouteLog.set(router, set);
|
|
11406
11467
|
}
|
|
11407
11468
|
return { url };
|
|
11408
11469
|
}
|
|
11409
|
-
function makeSsrHandler(
|
|
11470
|
+
function makeSsrHandler(entryPath, layoutPaths, loadPath, pagesDir, router) {
|
|
11410
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;
|
|
11411
11477
|
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
11412
11478
|
const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
|
|
11413
11479
|
let element = createElement(Component, allProps);
|
|
11414
|
-
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;
|
|
11415
11485
|
const isRoot = i === 0;
|
|
11416
11486
|
element = createElement(
|
|
11417
|
-
|
|
11487
|
+
Layout,
|
|
11418
11488
|
isRoot ? { children: element, req, ctx } : { children: element }
|
|
11419
11489
|
);
|
|
11420
11490
|
}
|
|
@@ -11429,9 +11499,20 @@ function makeSsrHandler(Component, loadFn, layouts, entryPath, layoutPaths, page
|
|
|
11429
11499
|
if (bundle) {
|
|
11430
11500
|
scripts.push(`<script type="module" src="${bundle.url}"></script>`);
|
|
11431
11501
|
}
|
|
11432
|
-
|
|
11502
|
+
let html = `<!DOCTYPE html>
|
|
11433
11503
|
${body}
|
|
11434
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
|
+
}
|
|
11435
11516
|
return new Response(html, {
|
|
11436
11517
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
11437
11518
|
});
|
|
@@ -11439,6 +11520,7 @@ ${scripts.join("\n")}`;
|
|
|
11439
11520
|
}
|
|
11440
11521
|
async function tsx(options) {
|
|
11441
11522
|
const pagesDir = resolve(options.dir);
|
|
11523
|
+
_projectDir = resolve(pagesDir, "..");
|
|
11442
11524
|
const outDir = join(pagesDir, "..", ".weifuwu", "ssr");
|
|
11443
11525
|
const pages = scanPages(pagesDir);
|
|
11444
11526
|
if (pages.length === 0) return new Router();
|
|
@@ -11457,79 +11539,102 @@ async function tsx(options) {
|
|
|
11457
11539
|
for (const lp of rootLayouts) allFiles.add(lp);
|
|
11458
11540
|
}
|
|
11459
11541
|
mkdirSync(outDir, { recursive: true });
|
|
11460
|
-
|
|
11542
|
+
const alias = resolveAliases();
|
|
11543
|
+
await compileAll([...allFiles], outDir, "node", alias);
|
|
11461
11544
|
const router = new Router();
|
|
11545
|
+
const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
11462
11546
|
for (const p of pages) {
|
|
11463
11547
|
if (p.routeOnly && p.routePath) {
|
|
11464
11548
|
const rUrl = compiledUrl(p.routePath, outDir);
|
|
11465
11549
|
const modR = await import(rUrl);
|
|
11466
|
-
const
|
|
11467
|
-
for (const
|
|
11468
|
-
if (modR[
|
|
11469
|
-
|
|
11470
|
-
|
|
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
|
+
);
|
|
11471
11566
|
}
|
|
11472
11567
|
continue;
|
|
11473
11568
|
}
|
|
11474
|
-
const
|
|
11475
|
-
|
|
11476
|
-
const Component = mod.default;
|
|
11477
|
-
let loadFn;
|
|
11569
|
+
const pageUrl = compiledUrl(p.entryPath, outDir);
|
|
11570
|
+
pageModules.set(p.entryPath, await import(pageUrl));
|
|
11478
11571
|
if (p.loadPath) {
|
|
11479
11572
|
const loadUrl = compiledUrl(p.loadPath, outDir);
|
|
11480
|
-
|
|
11481
|
-
loadFn = modLoad.default;
|
|
11573
|
+
loadModules.set(p.loadPath, await import(loadUrl));
|
|
11482
11574
|
}
|
|
11483
|
-
const layoutComponents = [];
|
|
11484
11575
|
for (const lp of p.layouts) {
|
|
11485
11576
|
const lUrl = compiledUrl(lp, outDir);
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
}
|
|
11489
|
-
const handler = makeSsrHandler(
|
|
11490
|
-
Component,
|
|
11491
|
-
loadFn,
|
|
11492
|
-
layoutComponents,
|
|
11493
|
-
p.entryPath,
|
|
11494
|
-
p.layouts,
|
|
11495
|
-
pagesDir,
|
|
11496
|
-
router
|
|
11497
|
-
);
|
|
11498
|
-
router.get(p.route, handler);
|
|
11577
|
+
layoutModules.set(lp, await import(lUrl));
|
|
11578
|
+
}
|
|
11499
11579
|
if (p.routePath) {
|
|
11500
11580
|
const rUrl = compiledUrl(p.routePath, outDir);
|
|
11501
11581
|
const modR = await import(rUrl);
|
|
11502
|
-
const
|
|
11503
|
-
for (const
|
|
11504
|
-
if (modR[
|
|
11505
|
-
|
|
11506
|
-
|
|
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
|
+
);
|
|
11507
11597
|
}
|
|
11508
11598
|
}
|
|
11509
11599
|
}
|
|
11510
11600
|
if (hasNotFound) {
|
|
11511
11601
|
const nfUrl = compiledUrl(nfPath, outDir);
|
|
11512
|
-
|
|
11513
|
-
const NfComponent = modNf.default;
|
|
11514
|
-
const nfLayouts = [];
|
|
11602
|
+
pageModules.set(nfPath, await import(nfUrl));
|
|
11515
11603
|
const rootLayouts = resolveLayouts(pagesDir, pagesDir);
|
|
11516
11604
|
for (const lp of rootLayouts) {
|
|
11517
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11605
|
+
if (!layoutModules.has(lp)) {
|
|
11606
|
+
const lUrl = compiledUrl(lp, outDir);
|
|
11607
|
+
layoutModules.set(lp, await import(lUrl));
|
|
11608
|
+
}
|
|
11520
11609
|
}
|
|
11521
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;
|
|
11522
11614
|
let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
|
|
11523
|
-
for (let i =
|
|
11524
|
-
|
|
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 });
|
|
11525
11619
|
}
|
|
11526
11620
|
element = createElement(TsxContext.Provider, {
|
|
11527
11621
|
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
11528
11622
|
}, element);
|
|
11529
11623
|
const stream = await renderToReadableStream(element);
|
|
11530
11624
|
const body = await readStream(stream);
|
|
11531
|
-
|
|
11625
|
+
let html = `<!DOCTYPE html>
|
|
11532
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
|
+
}
|
|
11533
11638
|
return new Response(html, {
|
|
11534
11639
|
status: 404,
|
|
11535
11640
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
@@ -11537,8 +11642,150 @@ ${body}`;
|
|
|
11537
11642
|
};
|
|
11538
11643
|
router.all("/*", handler);
|
|
11539
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
|
+
}
|
|
11540
11659
|
return router;
|
|
11541
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
|
+
}
|
|
11542
11789
|
|
|
11543
11790
|
// middleware.ts
|
|
11544
11791
|
function logger(options) {
|
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
|
},
|