weifuwu 0.8.2 → 0.9.1
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
|
@@ -109,7 +109,10 @@ function serve(handler, options) {
|
|
|
109
109
|
});
|
|
110
110
|
return {
|
|
111
111
|
stop: () => {
|
|
112
|
-
|
|
112
|
+
return new Promise((resolve3) => {
|
|
113
|
+
server.closeAllConnections();
|
|
114
|
+
server.close(() => resolve3());
|
|
115
|
+
});
|
|
113
116
|
},
|
|
114
117
|
ready,
|
|
115
118
|
get port() {
|
|
@@ -293,44 +296,51 @@ var Router = class _Router {
|
|
|
293
296
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
294
297
|
const query = Object.fromEntries(url.searchParams);
|
|
295
298
|
const match = router.matchWsTrie(wsRoot, segments);
|
|
296
|
-
if (
|
|
297
|
-
|
|
299
|
+
if (match) {
|
|
300
|
+
const webReq = new Request(url.href, {
|
|
301
|
+
method: req.method ?? "GET",
|
|
302
|
+
headers: Object.fromEntries(
|
|
303
|
+
Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v ?? ""])
|
|
304
|
+
)
|
|
305
|
+
});
|
|
306
|
+
const ctx = { params: match.params, query };
|
|
307
|
+
if (match.middlewares.length === 0) {
|
|
308
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
let index = 0;
|
|
312
|
+
const dispatch = async (innerReq, ctx2) => {
|
|
313
|
+
if (index < match.middlewares.length) {
|
|
314
|
+
const mw = match.middlewares[index++];
|
|
315
|
+
return mw(innerReq, ctx2, dispatch);
|
|
316
|
+
}
|
|
317
|
+
return await new Promise((resolve3) => {
|
|
318
|
+
try {
|
|
319
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx2);
|
|
320
|
+
resolve3(new Response(null, { status: 101 }));
|
|
321
|
+
} catch {
|
|
322
|
+
socket.destroy();
|
|
323
|
+
resolve3(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
328
|
+
if (result.status !== 101) {
|
|
329
|
+
sendHttpResponseOnSocket(socket, result);
|
|
330
|
+
}
|
|
331
|
+
}).catch(() => {
|
|
332
|
+
socket.destroy();
|
|
333
|
+
});
|
|
298
334
|
return;
|
|
299
335
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
)
|
|
305
|
-
});
|
|
306
|
-
const ctx = { params: match.params, query };
|
|
307
|
-
if (match.middlewares.length === 0) {
|
|
308
|
-
upgradeSocket(wss, req, socket, head, match.handler, ctx);
|
|
336
|
+
const httpMatch = router.matchTrie("GET", segments);
|
|
337
|
+
if (httpMatch?.subRouter) {
|
|
338
|
+
const remaining = "/" + segments.slice(httpMatch.subRouter.remainingIdx).join("/");
|
|
339
|
+
req.url = remaining;
|
|
340
|
+
httpMatch.subRouter.router.websocketHandler()(req, socket, head);
|
|
309
341
|
return;
|
|
310
342
|
}
|
|
311
|
-
|
|
312
|
-
const dispatch = async (innerReq, ctx2) => {
|
|
313
|
-
if (index < match.middlewares.length) {
|
|
314
|
-
const mw = match.middlewares[index++];
|
|
315
|
-
return mw(innerReq, ctx2, dispatch);
|
|
316
|
-
}
|
|
317
|
-
return await new Promise((resolve3) => {
|
|
318
|
-
try {
|
|
319
|
-
upgradeSocket(wss, req, socket, head, match.handler, ctx2);
|
|
320
|
-
resolve3(new Response(null, { status: 101 }));
|
|
321
|
-
} catch {
|
|
322
|
-
socket.destroy();
|
|
323
|
-
resolve3(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
};
|
|
327
|
-
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
328
|
-
if (result.status !== 101) {
|
|
329
|
-
sendHttpResponseOnSocket(socket, result);
|
|
330
|
-
}
|
|
331
|
-
}).catch(() => {
|
|
332
|
-
socket.destroy();
|
|
333
|
-
});
|
|
343
|
+
socket.destroy();
|
|
334
344
|
};
|
|
335
345
|
}
|
|
336
346
|
splitPath(path2) {
|
|
@@ -492,14 +502,64 @@ function sendHttpResponseOnSocket(socket, response) {
|
|
|
492
502
|
import { createElement, createContext, useContext } from "react";
|
|
493
503
|
import { renderToReadableStream } from "react-dom/server";
|
|
494
504
|
import * as esbuild from "esbuild";
|
|
495
|
-
import { readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
|
|
496
|
-
import
|
|
505
|
+
import { readdirSync, statSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
506
|
+
import chokidar from "chokidar";
|
|
507
|
+
import { join, relative, resolve, sep, dirname, basename } from "node:path";
|
|
497
508
|
import { pathToFileURL } from "node:url";
|
|
498
509
|
import { createHash } from "node:crypto";
|
|
499
510
|
var TsxContext = createContext({ params: {}, query: {} });
|
|
500
511
|
function useTsx() {
|
|
501
512
|
return useContext(TsxContext);
|
|
502
513
|
}
|
|
514
|
+
var pageModules = /* @__PURE__ */ new Map();
|
|
515
|
+
var layoutModules = /* @__PURE__ */ new Map();
|
|
516
|
+
var loadModules = /* @__PURE__ */ new Map();
|
|
517
|
+
var routeModules = /* @__PURE__ */ new Map();
|
|
518
|
+
var clientBundles = /* @__PURE__ */ new Map();
|
|
519
|
+
var liveReloadClients = /* @__PURE__ */ new Set();
|
|
520
|
+
var _watcher = null;
|
|
521
|
+
var _cssWatcher = null;
|
|
522
|
+
function broadcastReload() {
|
|
523
|
+
for (const ws of liveReloadClients) {
|
|
524
|
+
try {
|
|
525
|
+
ws.send("reload");
|
|
526
|
+
} catch {
|
|
527
|
+
liveReloadClients.delete(ws);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
var tailwindCssUrl = null;
|
|
532
|
+
var tailwindCssCode = "";
|
|
533
|
+
var _projectDir = "";
|
|
534
|
+
var _watcherStarted = false;
|
|
535
|
+
var _alias = null;
|
|
536
|
+
function resolveAliases() {
|
|
537
|
+
if (_alias) return _alias;
|
|
538
|
+
const configFiles = ["tsconfig.json", "jsconfig.json"];
|
|
539
|
+
for (const file of configFiles) {
|
|
540
|
+
const p = resolve(file);
|
|
541
|
+
if (existsSync(p)) {
|
|
542
|
+
try {
|
|
543
|
+
const config = JSON.parse(readFileSync(p, "utf-8"));
|
|
544
|
+
const paths = config.compilerOptions?.paths;
|
|
545
|
+
if (paths) {
|
|
546
|
+
const alias = {};
|
|
547
|
+
for (const [key, values] of Object.entries(paths)) {
|
|
548
|
+
const cleanKey = key.replace("/*", "");
|
|
549
|
+
const val = values[0]?.replace("/*", "");
|
|
550
|
+
if (val) alias[cleanKey] = resolve(dirname(p), val);
|
|
551
|
+
}
|
|
552
|
+
_alias = alias;
|
|
553
|
+
return alias;
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
_alias = {};
|
|
560
|
+
return {};
|
|
561
|
+
}
|
|
562
|
+
var isDev = process.env.NODE_ENV !== "production";
|
|
503
563
|
function id(s) {
|
|
504
564
|
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
505
565
|
}
|
|
@@ -603,7 +663,7 @@ function resolveLayouts(dir, pagesDir) {
|
|
|
603
663
|
}
|
|
604
664
|
return layouts.reverse();
|
|
605
665
|
}
|
|
606
|
-
async function compileAll(files, outDir, platform) {
|
|
666
|
+
async function compileAll(files, outDir, platform, alias) {
|
|
607
667
|
const entryPoints = {};
|
|
608
668
|
for (const f of files) {
|
|
609
669
|
entryPoints[id(f)] = f;
|
|
@@ -627,23 +687,21 @@ async function compileAll(files, outDir, platform) {
|
|
|
627
687
|
"@graphql-tools/schema",
|
|
628
688
|
"ai"
|
|
629
689
|
],
|
|
690
|
+
alias,
|
|
630
691
|
write: true,
|
|
631
692
|
allowOverwrite: true
|
|
632
693
|
});
|
|
633
694
|
}
|
|
634
695
|
function compiledUrl(filePath, outDir) {
|
|
635
|
-
const hash = id(join(outDir, id(filePath)));
|
|
636
696
|
const p = join(outDir, id(filePath) + ".js");
|
|
637
697
|
return pathToFileURL(p).href;
|
|
638
698
|
}
|
|
639
|
-
var clientBundleCache = /* @__PURE__ */ new Map();
|
|
640
699
|
var clientRouteLog = /* @__PURE__ */ new WeakMap();
|
|
641
700
|
async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
|
|
642
701
|
const key = id(entryPath);
|
|
643
702
|
const url = `/__wfw/client/${key}.js`;
|
|
644
703
|
if (!clientRouteLog.get(router)?.has(url)) {
|
|
645
|
-
|
|
646
|
-
if (!buf) {
|
|
704
|
+
if (!clientBundles.has(key)) {
|
|
647
705
|
try {
|
|
648
706
|
const nested = layoutPaths.slice(1);
|
|
649
707
|
const layoutsImport = nested.map(
|
|
@@ -669,34 +727,46 @@ async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router)
|
|
|
669
727
|
format: "esm",
|
|
670
728
|
jsx: "automatic",
|
|
671
729
|
jsxImportSource: "react",
|
|
730
|
+
alias: resolveAliases(),
|
|
672
731
|
write: false,
|
|
673
732
|
minify: true
|
|
674
733
|
});
|
|
675
|
-
|
|
676
|
-
clientBundleCache.set(key, buf);
|
|
734
|
+
clientBundles.set(key, result.outputFiles[0].contents);
|
|
677
735
|
} catch (err) {
|
|
678
736
|
console.error("hydration bundle failed:", err);
|
|
679
737
|
return null;
|
|
680
738
|
}
|
|
681
739
|
}
|
|
682
|
-
router.get(url, () =>
|
|
683
|
-
|
|
684
|
-
|
|
740
|
+
router.get(url, () => {
|
|
741
|
+
const buf = clientBundles.get(key);
|
|
742
|
+
return buf ? new Response(buf, {
|
|
743
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
744
|
+
}) : new Response("", { status: 500 });
|
|
745
|
+
});
|
|
685
746
|
const set = clientRouteLog.get(router) ?? /* @__PURE__ */ new Set();
|
|
686
747
|
set.add(url);
|
|
687
748
|
clientRouteLog.set(router, set);
|
|
688
749
|
}
|
|
689
750
|
return { url };
|
|
690
751
|
}
|
|
691
|
-
function makeSsrHandler(
|
|
752
|
+
function makeSsrHandler(entryPath, layoutPaths, loadPath, pagesDir, router) {
|
|
692
753
|
return async (req, ctx) => {
|
|
754
|
+
const pageMod = pageModules.get(entryPath);
|
|
755
|
+
if (!pageMod) return new Response("", { status: 500 });
|
|
756
|
+
const Component = pageMod.default;
|
|
757
|
+
const loadMod = loadPath ? loadModules.get(loadPath) : void 0;
|
|
758
|
+
const loadFn = loadMod?.default;
|
|
693
759
|
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
694
760
|
const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
|
|
695
761
|
let element = createElement(Component, allProps);
|
|
696
|
-
for (let i =
|
|
762
|
+
for (let i = layoutPaths.length - 1; i >= 0; i--) {
|
|
763
|
+
const lp = layoutPaths[i];
|
|
764
|
+
const LMod = layoutModules.get(lp);
|
|
765
|
+
if (!LMod) continue;
|
|
766
|
+
const Layout = LMod.default;
|
|
697
767
|
const isRoot = i === 0;
|
|
698
768
|
element = createElement(
|
|
699
|
-
|
|
769
|
+
Layout,
|
|
700
770
|
isRoot ? { children: element, req, ctx } : { children: element }
|
|
701
771
|
);
|
|
702
772
|
}
|
|
@@ -711,9 +781,20 @@ function makeSsrHandler(Component, loadFn, layouts, entryPath, layoutPaths, page
|
|
|
711
781
|
if (bundle) {
|
|
712
782
|
scripts.push(`<script type="module" src="${bundle.url}"></script>`);
|
|
713
783
|
}
|
|
714
|
-
|
|
784
|
+
let html = `<!DOCTYPE html>
|
|
715
785
|
${body}
|
|
716
786
|
${scripts.join("\n")}`;
|
|
787
|
+
if (tailwindCssUrl && html.includes("</head>")) {
|
|
788
|
+
html = html.replace(
|
|
789
|
+
"</head>",
|
|
790
|
+
`<link rel="stylesheet" href="${tailwindCssUrl}" />
|
|
791
|
+
</head>`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
if (isDev) {
|
|
795
|
+
html += `
|
|
796
|
+
<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>`;
|
|
797
|
+
}
|
|
717
798
|
return new Response(html, {
|
|
718
799
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
719
800
|
});
|
|
@@ -721,6 +802,7 @@ ${scripts.join("\n")}`;
|
|
|
721
802
|
}
|
|
722
803
|
async function tsx(options) {
|
|
723
804
|
const pagesDir = resolve(options.dir);
|
|
805
|
+
_projectDir = resolve(pagesDir, "..");
|
|
724
806
|
const outDir = join(pagesDir, "..", ".weifuwu", "ssr");
|
|
725
807
|
const pages = scanPages(pagesDir);
|
|
726
808
|
if (pages.length === 0) return new Router();
|
|
@@ -739,79 +821,102 @@ async function tsx(options) {
|
|
|
739
821
|
for (const lp of rootLayouts) allFiles.add(lp);
|
|
740
822
|
}
|
|
741
823
|
mkdirSync(outDir, { recursive: true });
|
|
742
|
-
|
|
824
|
+
const alias = resolveAliases();
|
|
825
|
+
await compileAll([...allFiles], outDir, "node", alias);
|
|
743
826
|
const router = new Router();
|
|
827
|
+
const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
744
828
|
for (const p of pages) {
|
|
745
829
|
if (p.routeOnly && p.routePath) {
|
|
746
830
|
const rUrl = compiledUrl(p.routePath, outDir);
|
|
747
831
|
const modR = await import(rUrl);
|
|
748
|
-
const
|
|
749
|
-
for (const
|
|
750
|
-
if (modR[
|
|
751
|
-
|
|
752
|
-
|
|
832
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
833
|
+
for (const m of ["GET", ...methods]) {
|
|
834
|
+
if (modR[m]) handlers.set(m, modR[m]);
|
|
835
|
+
}
|
|
836
|
+
routeModules.set(p.routePath, handlers);
|
|
837
|
+
router.route(
|
|
838
|
+
"GET",
|
|
839
|
+
p.route,
|
|
840
|
+
(req, ctx) => routeModules.get(p.routePath)?.get("GET")?.(req, ctx) ?? new Response("", { status: 501 })
|
|
841
|
+
);
|
|
842
|
+
for (const m of methods) {
|
|
843
|
+
router.route(
|
|
844
|
+
m,
|
|
845
|
+
p.route,
|
|
846
|
+
(req, ctx) => routeModules.get(p.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
|
|
847
|
+
);
|
|
753
848
|
}
|
|
754
849
|
continue;
|
|
755
850
|
}
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
const Component = mod.default;
|
|
759
|
-
let loadFn;
|
|
851
|
+
const pageUrl = compiledUrl(p.entryPath, outDir);
|
|
852
|
+
pageModules.set(p.entryPath, await import(pageUrl));
|
|
760
853
|
if (p.loadPath) {
|
|
761
854
|
const loadUrl = compiledUrl(p.loadPath, outDir);
|
|
762
|
-
|
|
763
|
-
loadFn = modLoad.default;
|
|
855
|
+
loadModules.set(p.loadPath, await import(loadUrl));
|
|
764
856
|
}
|
|
765
|
-
const layoutComponents = [];
|
|
766
857
|
for (const lp of p.layouts) {
|
|
767
858
|
const lUrl = compiledUrl(lp, outDir);
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
const handler = makeSsrHandler(
|
|
772
|
-
Component,
|
|
773
|
-
loadFn,
|
|
774
|
-
layoutComponents,
|
|
775
|
-
p.entryPath,
|
|
776
|
-
p.layouts,
|
|
777
|
-
pagesDir,
|
|
778
|
-
router
|
|
779
|
-
);
|
|
780
|
-
router.get(p.route, handler);
|
|
859
|
+
layoutModules.set(lp, await import(lUrl));
|
|
860
|
+
}
|
|
781
861
|
if (p.routePath) {
|
|
782
862
|
const rUrl = compiledUrl(p.routePath, outDir);
|
|
783
863
|
const modR = await import(rUrl);
|
|
784
|
-
const
|
|
785
|
-
for (const
|
|
786
|
-
if (modR[
|
|
787
|
-
|
|
788
|
-
|
|
864
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
865
|
+
for (const m of methods) {
|
|
866
|
+
if (modR[m]) handlers.set(m, modR[m]);
|
|
867
|
+
}
|
|
868
|
+
routeModules.set(p.routePath, handlers);
|
|
869
|
+
}
|
|
870
|
+
const handler = makeSsrHandler(p.entryPath, p.layouts, p.loadPath, pagesDir, router);
|
|
871
|
+
router.get(p.route, handler);
|
|
872
|
+
if (p.routePath) {
|
|
873
|
+
for (const m of methods) {
|
|
874
|
+
router.route(
|
|
875
|
+
m,
|
|
876
|
+
p.route,
|
|
877
|
+
(req, ctx) => routeModules.get(p.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
|
|
878
|
+
);
|
|
789
879
|
}
|
|
790
880
|
}
|
|
791
881
|
}
|
|
792
882
|
if (hasNotFound) {
|
|
793
883
|
const nfUrl = compiledUrl(nfPath, outDir);
|
|
794
|
-
|
|
795
|
-
const NfComponent = modNf.default;
|
|
796
|
-
const nfLayouts = [];
|
|
884
|
+
pageModules.set(nfPath, await import(nfUrl));
|
|
797
885
|
const rootLayouts = resolveLayouts(pagesDir, pagesDir);
|
|
798
886
|
for (const lp of rootLayouts) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
887
|
+
if (!layoutModules.has(lp)) {
|
|
888
|
+
const lUrl = compiledUrl(lp, outDir);
|
|
889
|
+
layoutModules.set(lp, await import(lUrl));
|
|
890
|
+
}
|
|
802
891
|
}
|
|
803
892
|
const handler = async (req, ctx) => {
|
|
893
|
+
const nfMod = pageModules.get(nfPath);
|
|
894
|
+
if (!nfMod) return new Response("Not Found", { status: 404 });
|
|
895
|
+
const NfComponent = nfMod.default;
|
|
804
896
|
let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
|
|
805
|
-
for (let i =
|
|
806
|
-
|
|
897
|
+
for (let i = rootLayouts.length - 1; i >= 0; i--) {
|
|
898
|
+
const LMod = layoutModules.get(rootLayouts[i]);
|
|
899
|
+
if (!LMod) continue;
|
|
900
|
+
element = createElement(LMod.default, { children: element });
|
|
807
901
|
}
|
|
808
902
|
element = createElement(TsxContext.Provider, {
|
|
809
903
|
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
810
904
|
}, element);
|
|
811
905
|
const stream = await renderToReadableStream(element);
|
|
812
906
|
const body = await readStream(stream);
|
|
813
|
-
|
|
907
|
+
let html = `<!DOCTYPE html>
|
|
814
908
|
${body}`;
|
|
909
|
+
if (tailwindCssUrl && html.includes("</head>")) {
|
|
910
|
+
html = html.replace(
|
|
911
|
+
"</head>",
|
|
912
|
+
`<link rel="stylesheet" href="${tailwindCssUrl}" />
|
|
913
|
+
</head>`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (isDev) {
|
|
917
|
+
html += `
|
|
918
|
+
<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>`;
|
|
919
|
+
}
|
|
815
920
|
return new Response(html, {
|
|
816
921
|
status: 404,
|
|
817
922
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
@@ -819,8 +924,150 @@ ${body}`;
|
|
|
819
924
|
};
|
|
820
925
|
router.all("/*", handler);
|
|
821
926
|
}
|
|
927
|
+
tailwindCssUrl = await setupTailwind(pagesDir, router, alias);
|
|
928
|
+
if (isDev) {
|
|
929
|
+
router.ws("/__weifuwu/livereload", {
|
|
930
|
+
open(ws) {
|
|
931
|
+
liveReloadClients.add(ws);
|
|
932
|
+
ws.on("close", () => liveReloadClients.delete(ws));
|
|
933
|
+
ws.on("error", () => liveReloadClients.delete(ws));
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
if (!_watcherStarted) {
|
|
937
|
+
startFileWatcher(pagesDir, outDir);
|
|
938
|
+
_watcherStarted = true;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
822
941
|
return router;
|
|
823
942
|
}
|
|
943
|
+
async function setupTailwind(pagesDir, router, alias) {
|
|
944
|
+
let tailwindPlugin, postcss, autoprefixer;
|
|
945
|
+
try {
|
|
946
|
+
tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
947
|
+
postcss = (await import("postcss")).default;
|
|
948
|
+
autoprefixer = (await import("autoprefixer")).default;
|
|
949
|
+
} catch {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
|
|
953
|
+
let inputFile = "";
|
|
954
|
+
for (const c of candidates) {
|
|
955
|
+
const p = resolve(pagesDir, "..", c);
|
|
956
|
+
if (existsSync(p)) {
|
|
957
|
+
inputFile = p;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (!inputFile) return null;
|
|
962
|
+
try {
|
|
963
|
+
const src = readFileSync(inputFile, "utf-8");
|
|
964
|
+
const result = await postcss([tailwindPlugin(), autoprefixer]).process(src, { from: inputFile });
|
|
965
|
+
tailwindCssCode = result.css;
|
|
966
|
+
} catch (err) {
|
|
967
|
+
console.warn("Tailwind CSS processing failed:", err.message);
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const url = "/__wfw/style.css";
|
|
971
|
+
router.get(url, () => new Response(tailwindCssCode, {
|
|
972
|
+
headers: { "content-type": "text/css; charset=utf-8" }
|
|
973
|
+
}));
|
|
974
|
+
if (isDev) {
|
|
975
|
+
_cssWatcher = chokidar.watch(inputFile, { persistent: false });
|
|
976
|
+
_cssWatcher.on("change", async () => {
|
|
977
|
+
try {
|
|
978
|
+
const newSrc = readFileSync(inputFile, "utf-8");
|
|
979
|
+
const newResult = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
|
|
980
|
+
tailwindCssCode = newResult.css;
|
|
981
|
+
broadcastReload();
|
|
982
|
+
} catch (err) {
|
|
983
|
+
console.warn("Tailwind CSS reprocessing failed:", err.message);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
return url;
|
|
988
|
+
}
|
|
989
|
+
function startFileWatcher(pagesDir, outDir) {
|
|
990
|
+
let timeout = null;
|
|
991
|
+
const pending = /* @__PURE__ */ new Set();
|
|
992
|
+
_watcher = chokidar.watch(pagesDir, {
|
|
993
|
+
ignored: /(^|[/\\])\.(?!\.)|\.weifuwu/,
|
|
994
|
+
persistent: false,
|
|
995
|
+
ignoreInitial: true
|
|
996
|
+
});
|
|
997
|
+
_watcher.on("all", async (event, filePath) => {
|
|
998
|
+
if (event !== "change" && event !== "add") return;
|
|
999
|
+
if (!/\.tsx?$/.test(filePath)) return;
|
|
1000
|
+
pending.add(filePath);
|
|
1001
|
+
if (timeout) clearTimeout(timeout);
|
|
1002
|
+
timeout = setTimeout(async () => {
|
|
1003
|
+
timeout = null;
|
|
1004
|
+
const files = [...pending];
|
|
1005
|
+
pending.clear();
|
|
1006
|
+
for (const f of files) {
|
|
1007
|
+
if (existsSync(f)) await recompileAndSwap(f, outDir);
|
|
1008
|
+
}
|
|
1009
|
+
}, 50);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
async function recompileAndSwap(filePath, outDir) {
|
|
1013
|
+
try {
|
|
1014
|
+
await esbuild.build({
|
|
1015
|
+
entryPoints: { [id(filePath)]: filePath },
|
|
1016
|
+
outdir: outDir,
|
|
1017
|
+
alias: resolveAliases(),
|
|
1018
|
+
format: "esm",
|
|
1019
|
+
platform: "node",
|
|
1020
|
+
jsx: "automatic",
|
|
1021
|
+
jsxImportSource: "react",
|
|
1022
|
+
bundle: true,
|
|
1023
|
+
external: ["react", "react-dom", "esbuild", "graphql", "ws", "zod", "@graphql-tools/schema", "ai"],
|
|
1024
|
+
write: true,
|
|
1025
|
+
allowOverwrite: true
|
|
1026
|
+
});
|
|
1027
|
+
const bustUrl = compiledUrl(filePath, outDir) + "?t=" + Date.now();
|
|
1028
|
+
const freshMod = await import(bustUrl);
|
|
1029
|
+
const name15 = basename(filePath);
|
|
1030
|
+
if (name15 === "layout.tsx") {
|
|
1031
|
+
layoutModules.set(filePath, freshMod);
|
|
1032
|
+
} else if (name15 === "route.ts") {
|
|
1033
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1034
|
+
for (const m of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) {
|
|
1035
|
+
if (freshMod[m]) handlers.set(m, freshMod[m]);
|
|
1036
|
+
}
|
|
1037
|
+
routeModules.set(filePath, handlers);
|
|
1038
|
+
} else if (name15 === "load.ts") {
|
|
1039
|
+
loadModules.set(filePath, freshMod);
|
|
1040
|
+
} else {
|
|
1041
|
+
pageModules.set(filePath, freshMod);
|
|
1042
|
+
clientBundles.delete(id(filePath));
|
|
1043
|
+
}
|
|
1044
|
+
if (tailwindCssUrl) {
|
|
1045
|
+
try {
|
|
1046
|
+
const tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
1047
|
+
const postcss = (await import("postcss")).default;
|
|
1048
|
+
const autoprefixer = (await import("autoprefixer")).default;
|
|
1049
|
+
const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
|
|
1050
|
+
let inputFile = "";
|
|
1051
|
+
for (const c of candidates) {
|
|
1052
|
+
const p = resolve(_projectDir, c);
|
|
1053
|
+
if (existsSync(p)) {
|
|
1054
|
+
inputFile = p;
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (inputFile) {
|
|
1059
|
+
const newSrc = readFileSync(inputFile, "utf-8");
|
|
1060
|
+
const result = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
|
|
1061
|
+
tailwindCssCode = result.css;
|
|
1062
|
+
}
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
broadcastReload();
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
console.error("recompile failed:", err.message);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
824
1071
|
|
|
825
1072
|
// middleware.ts
|
|
826
1073
|
function logger(options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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 --external:ioredis",
|
|
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:ioredis --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
|
},
|