weifuwu 0.9.2 → 0.9.4
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 +150 -28
- package/dist/index.js +186 -167
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -34,11 +34,42 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
|
|
|
34
34
|
|
|
35
35
|
## Quick start
|
|
36
36
|
|
|
37
|
+
### Hello World
|
|
38
|
+
|
|
37
39
|
```ts
|
|
38
40
|
import { serve } from 'weifuwu'
|
|
39
41
|
serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
|
|
40
42
|
```
|
|
41
43
|
|
|
44
|
+
### React + Tailwind
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install weifuwu
|
|
48
|
+
mkdir -p ui/pages
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// app.ts
|
|
53
|
+
import { serve, Router } from 'weifuwu'
|
|
54
|
+
|
|
55
|
+
const app = new Router()
|
|
56
|
+
app.use('/', await tsx({ dir: './ui/' }))
|
|
57
|
+
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// ui/pages/page.tsx
|
|
62
|
+
export default function Home() {
|
|
63
|
+
return <h1 className="text-3xl font-bold text-blue-600">Hello</h1>
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node app.ts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Open http://localhost:3000 — Tailwind CSS is compiled automatically, pages hot-reload on save.
|
|
72
|
+
|
|
42
73
|
## Router
|
|
43
74
|
|
|
44
75
|
```ts
|
|
@@ -851,44 +882,134 @@ import { serve, Router } from 'weifuwu'
|
|
|
851
882
|
import { tsx } from 'weifuwu/tsx'
|
|
852
883
|
|
|
853
884
|
const app = new Router()
|
|
854
|
-
app.use('/', await tsx({ dir: './
|
|
885
|
+
app.use('/', await tsx({ dir: './ui/' }))
|
|
855
886
|
|
|
856
887
|
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
857
888
|
```
|
|
858
889
|
|
|
859
|
-
###
|
|
890
|
+
### Directory structure
|
|
860
891
|
|
|
861
|
-
|
|
892
|
+
```
|
|
893
|
+
ui/
|
|
894
|
+
├── pages/ ← 页面文件
|
|
895
|
+
│ ├── page.tsx → GET / (React component, default export)
|
|
896
|
+
│ ├── layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
|
|
897
|
+
│ ├── not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
|
|
898
|
+
│ ├── about/page.tsx → GET /about
|
|
899
|
+
│ ├── blog/[slug]/
|
|
900
|
+
│ │ ├── page.tsx → GET /blog/:slug
|
|
901
|
+
│ │ ├── load.ts → data fetching (server-only, default export)
|
|
902
|
+
│ │ └── route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
|
|
903
|
+
│ ├── blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
|
|
904
|
+
│ └── api/search/
|
|
905
|
+
│ └── route.ts → GET /api/search (standalone API, no page.tsx needed)
|
|
906
|
+
└── components/ ← 组件文件(会被热更自动感知)
|
|
907
|
+
└── button.tsx
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
### Development mode
|
|
862
911
|
|
|
863
|
-
|
|
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
|
|
912
|
+
tsx() runs in development mode automatically when `NODE_ENV !== 'production'`:
|
|
867
913
|
|
|
868
|
-
|
|
914
|
+
- **File watching** — chokidar watches the `dir` directory for `.tsx`/`.ts` changes
|
|
915
|
+
- Page files in `pages/` → single-file recompilation + registry update
|
|
916
|
+
- Component files in `components/` → full rebuild of all pages
|
|
917
|
+
- New files are detected automatically
|
|
918
|
+
- **Live reload** — Compiled via esbuild `write: false` + `vm.Script.runInContext` (no disk writes, no `node --watch` conflict)
|
|
919
|
+
- **WebSocket auto-refresh** — `/__weifuwu/livereload` endpoint pushes reload signals; browser refreshes automatically
|
|
920
|
+
- **`node --watch` compatible** — External files (`app.ts`, `middleware/`) handled by `--watch` restart; `ui/` changes handled by tsx() without conflict
|
|
869
921
|
|
|
870
922
|
```bash
|
|
871
|
-
node app.ts # development
|
|
923
|
+
node app.ts # development (auto-reload + live refresh)
|
|
872
924
|
NODE_ENV=production node app.ts # production
|
|
873
925
|
```
|
|
874
926
|
|
|
875
|
-
###
|
|
927
|
+
### Tailwind CSS
|
|
876
928
|
|
|
929
|
+
tsx() includes built-in Tailwind CSS v4 support. If an `app.css` file exists in the `dir` directory, it is compiled automatically through PostCSS + `@tailwindcss/postcss`. If no `app.css` is found, one is created automatically:
|
|
930
|
+
|
|
931
|
+
```css
|
|
932
|
+
@import "tailwindcss";
|
|
877
933
|
```
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
load.ts → data fetching (server-only, default export)
|
|
886
|
-
route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
|
|
887
|
-
blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
|
|
888
|
-
api/search/
|
|
889
|
-
route.ts → GET /api/search (standalone API, no page.tsx needed)
|
|
934
|
+
|
|
935
|
+
Write `className` directly in your components — no CLI, no configuration:
|
|
936
|
+
|
|
937
|
+
```tsx
|
|
938
|
+
export default function Home() {
|
|
939
|
+
return <h1 className="text-3xl font-bold text-blue-600">Hello</h1>
|
|
940
|
+
}
|
|
890
941
|
```
|
|
891
942
|
|
|
943
|
+
In development mode, Tailwind is reprocessed whenever a `.tsx` file changes (new class names are picked up automatically).
|
|
944
|
+
|
|
945
|
+
### `@` alias
|
|
946
|
+
|
|
947
|
+
If your project has a `tsconfig.json` or `jsconfig.json` with `compilerOptions.paths`, tsx() reads it automatically and passes aliases to all esbuild builds (SSR compilation, hydration bundles, and hot reload):
|
|
948
|
+
|
|
949
|
+
```json
|
|
950
|
+
{
|
|
951
|
+
"compilerOptions": {
|
|
952
|
+
"paths": {
|
|
953
|
+
"@/*": ["./ui/*"]
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
This enables imports like `@/components/button` or `@/lib/utils` in both server-rendered and client-hydrated code.
|
|
960
|
+
|
|
961
|
+
### shadcn/ui
|
|
962
|
+
|
|
963
|
+
tsx() works with [shadcn/ui](https://ui.shadcn.com) out of the box. The `@` alias and Tailwind CSS are handled automatically.
|
|
964
|
+
|
|
965
|
+
```bash
|
|
966
|
+
# 1. Install shadcn CLI and init (select "other" framework)
|
|
967
|
+
npx shadcn@latest init
|
|
968
|
+
|
|
969
|
+
# 2. When prompted, configure:
|
|
970
|
+
# - Style: your preference
|
|
971
|
+
# - Base color: your preference
|
|
972
|
+
# - CSS file path: ui/app.css
|
|
973
|
+
# - Import alias: @/ → ./ui/
|
|
974
|
+
# - React hooks: yes
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
```json
|
|
978
|
+
// tsconfig.json (generated by shadcn init)
|
|
979
|
+
{
|
|
980
|
+
"compilerOptions": {
|
|
981
|
+
"paths": {
|
|
982
|
+
"@/*": ["./ui/*"]
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
Add components:
|
|
989
|
+
|
|
990
|
+
```bash
|
|
991
|
+
npx shadcn@latest add button card dialog
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
Use them in your pages:
|
|
995
|
+
|
|
996
|
+
```tsx
|
|
997
|
+
// ui/pages/page.tsx
|
|
998
|
+
import { Button } from '@/components/ui/button'
|
|
999
|
+
|
|
1000
|
+
export default function Home() {
|
|
1001
|
+
return <Button variant="outline">Click me</Button>
|
|
1002
|
+
}
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
```bash
|
|
1006
|
+
node app.ts
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
### Backward compatibility
|
|
1010
|
+
|
|
1011
|
+
`tsx({ dir: './pages/' })` still works. When there is no `pages/` subdirectory under `dir`, the `dir` itself is used as the pages directory.
|
|
1012
|
+
|
|
892
1013
|
### page.tsx — page component
|
|
893
1014
|
|
|
894
1015
|
```tsx
|
|
@@ -967,7 +1088,7 @@ serve(app.handler(), { websocket: app.websocketHandler() })
|
|
|
967
1088
|
```
|
|
968
1089
|
|
|
969
1090
|
```bash
|
|
970
|
-
node app.ts # development (auto-reload
|
|
1091
|
+
node app.ts # development (auto-reload + live refresh)
|
|
971
1092
|
NODE_ENV=production node app.ts # production
|
|
972
1093
|
```
|
|
973
1094
|
|
|
@@ -1110,18 +1231,19 @@ Returns `MessagerModule` — `{ migrate, router, wsHandler, send, close }`.
|
|
|
1110
1231
|
|
|
1111
1232
|
| Option | Default | Description |
|
|
1112
1233
|
|--------|---------|-------------|
|
|
1113
|
-
| `dir`
|
|
1234
|
+
| `dir` | — | UI directory path (containing `pages/` and optionally `components/`) |
|
|
1114
1235
|
|
|
1115
1236
|
Returns `Promise<Router>`.
|
|
1116
1237
|
|
|
1117
|
-
|
|
1238
|
+
Auto-detected features (no configuration needed):
|
|
1118
1239
|
|
|
1119
1240
|
| Feature | Behavior |
|
|
1120
1241
|
|---------|----------|
|
|
1121
|
-
| **File watching** | Enabled
|
|
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 |
|
|
1242
|
+
| **File watching** | Enabled in dev mode. Watches `dir` for changes, recompiles on the fly, sends reload via WebSocket |
|
|
1124
1243
|
| **WebSocket live reload** | Endpoint at `/__weifuwu/livereload`. Browser auto-refreshes on file changes or server restart |
|
|
1244
|
+
| **Tailwind CSS** | Auto-detected when `app.css` exists. Compiled through PostCSS + `@tailwindcss/postcss`. Served at `/__wfw/style.css`, auto-injected into HTML `<head>` |
|
|
1245
|
+
| **`@` alias** | Read from `tsconfig.json` / `jsconfig.json` `compilerOptions.paths`. Passed to all esbuild builds |
|
|
1246
|
+
| **Process state** | Dev mode keeps the process alive on file changes. DB connections, WebSockets, in-memory caches persist |
|
|
1125
1247
|
|
|
1126
1248
|
To use WebSocket features, pass `router.websocketHandler()` to `serve()`:
|
|
1127
1249
|
|
package/dist/index.js
CHANGED
|
@@ -109,10 +109,7 @@ function serve(handler, options) {
|
|
|
109
109
|
});
|
|
110
110
|
return {
|
|
111
111
|
stop: () => {
|
|
112
|
-
|
|
113
|
-
server.closeAllConnections();
|
|
114
|
-
server.close(() => resolve3());
|
|
115
|
-
});
|
|
112
|
+
server.close();
|
|
116
113
|
},
|
|
117
114
|
ready,
|
|
118
115
|
get port() {
|
|
@@ -294,9 +291,9 @@ var Router = class _Router {
|
|
|
294
291
|
return (req, socket, head) => {
|
|
295
292
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
296
293
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
297
|
-
const query = Object.fromEntries(url.searchParams);
|
|
298
294
|
const match = router.matchWsTrie(wsRoot, segments);
|
|
299
295
|
if (match) {
|
|
296
|
+
const query = Object.fromEntries(url.searchParams);
|
|
300
297
|
const webReq = new Request(url.href, {
|
|
301
298
|
method: req.method ?? "GET",
|
|
302
299
|
headers: Object.fromEntries(
|
|
@@ -502,11 +499,13 @@ function sendHttpResponseOnSocket(socket, response) {
|
|
|
502
499
|
import { createElement, createContext, useContext } from "react";
|
|
503
500
|
import { renderToReadableStream } from "react-dom/server";
|
|
504
501
|
import * as esbuild from "esbuild";
|
|
505
|
-
import { readdirSync, statSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
506
|
-
import chokidar from "chokidar";
|
|
502
|
+
import { readdirSync, statSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
507
503
|
import { join, relative, resolve, sep, dirname, basename } from "node:path";
|
|
508
504
|
import { pathToFileURL } from "node:url";
|
|
509
505
|
import { createHash } from "node:crypto";
|
|
506
|
+
import vm from "node:vm";
|
|
507
|
+
import { createRequire } from "node:module";
|
|
508
|
+
import chokidar from "chokidar";
|
|
510
509
|
var TsxContext = createContext({ params: {}, query: {} });
|
|
511
510
|
function useTsx() {
|
|
512
511
|
return useContext(TsxContext);
|
|
@@ -515,10 +514,7 @@ var pageModules = /* @__PURE__ */ new Map();
|
|
|
515
514
|
var layoutModules = /* @__PURE__ */ new Map();
|
|
516
515
|
var loadModules = /* @__PURE__ */ new Map();
|
|
517
516
|
var routeModules = /* @__PURE__ */ new Map();
|
|
518
|
-
var clientBundles = /* @__PURE__ */ new Map();
|
|
519
517
|
var liveReloadClients = /* @__PURE__ */ new Set();
|
|
520
|
-
var _watcher = null;
|
|
521
|
-
var _cssWatcher = null;
|
|
522
518
|
function broadcastReload() {
|
|
523
519
|
for (const ws of liveReloadClients) {
|
|
524
520
|
try {
|
|
@@ -528,10 +524,25 @@ function broadcastReload() {
|
|
|
528
524
|
}
|
|
529
525
|
}
|
|
530
526
|
}
|
|
527
|
+
var isDev = process.env.NODE_ENV !== "production";
|
|
528
|
+
var _uiDir = "";
|
|
529
|
+
var _allFiles = [];
|
|
530
|
+
var _outDir = "";
|
|
531
531
|
var tailwindCssUrl = null;
|
|
532
532
|
var tailwindCssCode = "";
|
|
533
|
-
var
|
|
534
|
-
var
|
|
533
|
+
var _cjsRequire = createRequire(import.meta.url);
|
|
534
|
+
var _vmCtx = vm.createContext(Object.create(globalThis));
|
|
535
|
+
function loadSSRModule(code) {
|
|
536
|
+
const mod = { exports: {} };
|
|
537
|
+
_vmCtx.require = (name15) => _cjsRequire(name15);
|
|
538
|
+
_vmCtx.module = mod;
|
|
539
|
+
_vmCtx.exports = mod.exports;
|
|
540
|
+
new vm.Script(code).runInContext(_vmCtx);
|
|
541
|
+
return mod.exports;
|
|
542
|
+
}
|
|
543
|
+
function id(s) {
|
|
544
|
+
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
545
|
+
}
|
|
535
546
|
var _alias = null;
|
|
536
547
|
function resolveAliases() {
|
|
537
548
|
if (_alias) return _alias;
|
|
@@ -559,10 +570,6 @@ function resolveAliases() {
|
|
|
559
570
|
_alias = {};
|
|
560
571
|
return {};
|
|
561
572
|
}
|
|
562
|
-
var isDev = process.env.NODE_ENV !== "production";
|
|
563
|
-
function id(s) {
|
|
564
|
-
return createHash("md5").update(s).digest("hex").slice(0, 8);
|
|
565
|
-
}
|
|
566
573
|
function concatUint8(chunks) {
|
|
567
574
|
const len = chunks.reduce((a, c) => a + c.length, 0);
|
|
568
575
|
const out = new Uint8Array(len);
|
|
@@ -687,21 +694,171 @@ async function compileAll(files, outDir, platform, alias) {
|
|
|
687
694
|
"@graphql-tools/schema",
|
|
688
695
|
"ai"
|
|
689
696
|
],
|
|
690
|
-
alias,
|
|
691
697
|
write: true,
|
|
698
|
+
alias,
|
|
692
699
|
allowOverwrite: true
|
|
693
700
|
});
|
|
694
701
|
}
|
|
695
702
|
function compiledUrl(filePath, outDir) {
|
|
703
|
+
const hash = id(join(outDir, id(filePath)));
|
|
696
704
|
const p = join(outDir, id(filePath) + ".js");
|
|
697
705
|
return pathToFileURL(p).href;
|
|
698
706
|
}
|
|
707
|
+
function startFileWatcher() {
|
|
708
|
+
let timeout = null;
|
|
709
|
+
const pending = /* @__PURE__ */ new Set();
|
|
710
|
+
chokidar.watch(_uiDir, {
|
|
711
|
+
ignored: /(^|[/\\])\.(?!\.)|node_modules|\.weifuwu|dist/,
|
|
712
|
+
persistent: false,
|
|
713
|
+
ignoreInitial: true
|
|
714
|
+
}).on("all", async (event, filePath) => {
|
|
715
|
+
if (event !== "change" && event !== "add") return;
|
|
716
|
+
if (!/\.tsx?$/.test(filePath)) return;
|
|
717
|
+
pending.add(filePath);
|
|
718
|
+
if (timeout) clearTimeout(timeout);
|
|
719
|
+
timeout = setTimeout(async () => {
|
|
720
|
+
timeout = null;
|
|
721
|
+
const files = [...pending];
|
|
722
|
+
pending.clear();
|
|
723
|
+
const exists = files.filter((f) => existsSync(f));
|
|
724
|
+
const allKnown = exists.every(
|
|
725
|
+
(f) => pageModules.has(f) || layoutModules.has(f) || loadModules.has(f) || routeModules.has(f)
|
|
726
|
+
);
|
|
727
|
+
if (allKnown) {
|
|
728
|
+
for (const f of exists) await recompileAndSwap(f, _outDir);
|
|
729
|
+
} else {
|
|
730
|
+
await recompileAll();
|
|
731
|
+
}
|
|
732
|
+
}, 50);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
async function recompileAndSwap(filePath, outDir) {
|
|
736
|
+
try {
|
|
737
|
+
const result = await esbuild.build({
|
|
738
|
+
entryPoints: { [id(filePath)]: filePath },
|
|
739
|
+
outdir: outDir,
|
|
740
|
+
format: "cjs",
|
|
741
|
+
platform: "node",
|
|
742
|
+
jsx: "automatic",
|
|
743
|
+
jsxImportSource: "react",
|
|
744
|
+
bundle: true,
|
|
745
|
+
external: ["react", "react-dom", "esbuild", "graphql", "ws", "zod", "@graphql-tools/schema", "ai"],
|
|
746
|
+
alias: resolveAliases(),
|
|
747
|
+
write: false
|
|
748
|
+
});
|
|
749
|
+
const code = new TextDecoder().decode(result.outputFiles[0].contents);
|
|
750
|
+
const mod = loadSSRModule(code);
|
|
751
|
+
const name15 = basename(filePath);
|
|
752
|
+
if (name15 === "layout.tsx") {
|
|
753
|
+
layoutModules.set(filePath, mod);
|
|
754
|
+
} else if (name15 === "route.ts") {
|
|
755
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
756
|
+
for (const m of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) {
|
|
757
|
+
if (mod[m]) handlers.set(m, mod[m]);
|
|
758
|
+
}
|
|
759
|
+
routeModules.set(filePath, handlers);
|
|
760
|
+
} else if (name15 === "load.ts") {
|
|
761
|
+
loadModules.set(filePath, mod);
|
|
762
|
+
} else {
|
|
763
|
+
pageModules.set(filePath, mod);
|
|
764
|
+
clientBundleCache.delete(id(filePath));
|
|
765
|
+
}
|
|
766
|
+
await reprocessTailwind();
|
|
767
|
+
broadcastReload();
|
|
768
|
+
} catch (err) {
|
|
769
|
+
console.error("recompile failed:", err.message);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function recompileAll() {
|
|
773
|
+
try {
|
|
774
|
+
const result = await esbuild.build({
|
|
775
|
+
entryPoints: Object.fromEntries(_allFiles.map((f) => [id(f), f])),
|
|
776
|
+
outdir: _outDir,
|
|
777
|
+
format: "cjs",
|
|
778
|
+
platform: "node",
|
|
779
|
+
jsx: "automatic",
|
|
780
|
+
jsxImportSource: "react",
|
|
781
|
+
bundle: true,
|
|
782
|
+
external: ["react", "react-dom", "esbuild", "graphql", "ws", "zod", "@graphql-tools/schema", "ai"],
|
|
783
|
+
alias: resolveAliases(),
|
|
784
|
+
write: false
|
|
785
|
+
});
|
|
786
|
+
for (const file of result.outputFiles) {
|
|
787
|
+
const code = new TextDecoder().decode(file.contents);
|
|
788
|
+
const mod = loadSSRModule(code);
|
|
789
|
+
const srcPath = _allFiles.find((f) => file.path.endsWith(id(f) + ".js"));
|
|
790
|
+
if (!srcPath) continue;
|
|
791
|
+
const name15 = basename(srcPath);
|
|
792
|
+
if (name15 === "layout.tsx") layoutModules.set(srcPath, mod);
|
|
793
|
+
else if (name15 === "load.ts") loadModules.set(srcPath, mod);
|
|
794
|
+
else pageModules.set(srcPath, mod);
|
|
795
|
+
}
|
|
796
|
+
clientBundleCache.clear();
|
|
797
|
+
await reprocessTailwind();
|
|
798
|
+
broadcastReload();
|
|
799
|
+
} catch (err) {
|
|
800
|
+
console.error("recompile all failed:", err.message);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async function setupTailwind(uiDir, router) {
|
|
804
|
+
let tailwindPlugin, postcss;
|
|
805
|
+
try {
|
|
806
|
+
tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
807
|
+
postcss = (await import("postcss")).default;
|
|
808
|
+
} catch {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const inputFile = resolve(uiDir, "app.css");
|
|
812
|
+
if (!existsSync(inputFile)) {
|
|
813
|
+
mkdirSync(uiDir, { recursive: true });
|
|
814
|
+
writeFileSync(inputFile, '@import "tailwindcss"\n', "utf-8");
|
|
815
|
+
console.log("\u2139 weifuwu/tsx: created " + relative(process.cwd(), inputFile));
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const src = readFileSync(inputFile, "utf-8");
|
|
819
|
+
const result = await postcss([tailwindPlugin()]).process(src, { from: inputFile });
|
|
820
|
+
tailwindCssCode = result.css;
|
|
821
|
+
} catch (err) {
|
|
822
|
+
console.warn("Tailwind CSS processing failed:", err.message);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
router.get("/__wfw/style.css", () => new Response(tailwindCssCode, {
|
|
826
|
+
headers: { "content-type": "text/css; charset=utf-8" }
|
|
827
|
+
}));
|
|
828
|
+
tailwindCssUrl = "/__wfw/style.css";
|
|
829
|
+
if (isDev) {
|
|
830
|
+
chokidar.watch(inputFile, { persistent: false }).on("change", async () => {
|
|
831
|
+
try {
|
|
832
|
+
const newSrc = readFileSync(inputFile, "utf-8");
|
|
833
|
+
const newResult = await postcss([tailwindPlugin()]).process(newSrc, { from: inputFile });
|
|
834
|
+
tailwindCssCode = newResult.css;
|
|
835
|
+
broadcastReload();
|
|
836
|
+
} catch (err) {
|
|
837
|
+
console.warn("Tailwind CSS reprocess failed:", err.message);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function reprocessTailwind() {
|
|
843
|
+
if (!tailwindCssUrl) return;
|
|
844
|
+
try {
|
|
845
|
+
const inputFile = resolve(_uiDir, "app.css");
|
|
846
|
+
if (!existsSync(inputFile)) return;
|
|
847
|
+
const tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
848
|
+
const postcss = (await import("postcss")).default;
|
|
849
|
+
const src = readFileSync(inputFile, "utf-8");
|
|
850
|
+
const result = await postcss([tailwindPlugin()]).process(src, { from: inputFile });
|
|
851
|
+
tailwindCssCode = result.css;
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
var clientBundleCache = /* @__PURE__ */ new Map();
|
|
699
856
|
var clientRouteLog = /* @__PURE__ */ new WeakMap();
|
|
700
857
|
async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
|
|
701
858
|
const key = id(entryPath);
|
|
702
859
|
const url = `/__wfw/client/${key}.js`;
|
|
703
860
|
if (!clientRouteLog.get(router)?.has(url)) {
|
|
704
|
-
if (!
|
|
861
|
+
if (!clientBundleCache.has(key)) {
|
|
705
862
|
try {
|
|
706
863
|
const nested = layoutPaths.slice(1);
|
|
707
864
|
const layoutsImport = nested.map(
|
|
@@ -731,14 +888,14 @@ async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router)
|
|
|
731
888
|
write: false,
|
|
732
889
|
minify: true
|
|
733
890
|
});
|
|
734
|
-
|
|
891
|
+
clientBundleCache.set(key, result.outputFiles[0].contents);
|
|
735
892
|
} catch (err) {
|
|
736
893
|
console.error("hydration bundle failed:", err);
|
|
737
894
|
return null;
|
|
738
895
|
}
|
|
739
896
|
}
|
|
740
897
|
router.get(url, () => {
|
|
741
|
-
const buf =
|
|
898
|
+
const buf = clientBundleCache.get(key);
|
|
742
899
|
return buf ? new Response(buf, {
|
|
743
900
|
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
744
901
|
}) : new Response("", { status: 500 });
|
|
@@ -801,9 +958,11 @@ ${scripts.join("\n")}`;
|
|
|
801
958
|
};
|
|
802
959
|
}
|
|
803
960
|
async function tsx(options) {
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
961
|
+
const uiDir = resolve(options.dir);
|
|
962
|
+
const pagesDir = existsSync(join(uiDir, "pages")) ? join(uiDir, "pages") : uiDir;
|
|
963
|
+
_uiDir = uiDir;
|
|
964
|
+
const outDir = join(uiDir, ".weifuwu", "ssr");
|
|
965
|
+
_outDir = outDir;
|
|
807
966
|
const pages = scanPages(pagesDir);
|
|
808
967
|
if (pages.length === 0) return new Router();
|
|
809
968
|
const allFiles = /* @__PURE__ */ new Set();
|
|
@@ -821,8 +980,8 @@ async function tsx(options) {
|
|
|
821
980
|
for (const lp of rootLayouts) allFiles.add(lp);
|
|
822
981
|
}
|
|
823
982
|
mkdirSync(outDir, { recursive: true });
|
|
824
|
-
|
|
825
|
-
await compileAll(
|
|
983
|
+
_allFiles = [...allFiles];
|
|
984
|
+
await compileAll(_allFiles, outDir, "node", resolveAliases());
|
|
826
985
|
const router = new Router();
|
|
827
986
|
const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
828
987
|
for (const p of pages) {
|
|
@@ -924,7 +1083,7 @@ ${body}`;
|
|
|
924
1083
|
};
|
|
925
1084
|
router.all("/*", handler);
|
|
926
1085
|
}
|
|
927
|
-
|
|
1086
|
+
await setupTailwind(uiDir, router);
|
|
928
1087
|
if (isDev) {
|
|
929
1088
|
router.ws("/__weifuwu/livereload", {
|
|
930
1089
|
open(ws) {
|
|
@@ -933,150 +1092,10 @@ ${body}`;
|
|
|
933
1092
|
ws.on("error", () => liveReloadClients.delete(ws));
|
|
934
1093
|
}
|
|
935
1094
|
});
|
|
936
|
-
|
|
937
|
-
startFileWatcher(pagesDir, outDir);
|
|
938
|
-
_watcherStarted = true;
|
|
939
|
-
}
|
|
1095
|
+
startFileWatcher();
|
|
940
1096
|
}
|
|
941
1097
|
return router;
|
|
942
1098
|
}
|
|
943
|
-
async function setupTailwind(pagesDir, router, alias) {
|
|
944
|
-
let tailwindPlugin, postcss, autoprefixer;
|
|
945
|
-
const _onWarning = (w) => {
|
|
946
|
-
if (w.code === "DEP0205") return;
|
|
947
|
-
process.removeListener("warning", _onWarning);
|
|
948
|
-
process.emitWarning(w);
|
|
949
|
-
process.on("warning", _onWarning);
|
|
950
|
-
};
|
|
951
|
-
process.on("warning", _onWarning);
|
|
952
|
-
try {
|
|
953
|
-
tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
954
|
-
postcss = (await import("postcss")).default;
|
|
955
|
-
autoprefixer = (await import("autoprefixer")).default;
|
|
956
|
-
} catch {
|
|
957
|
-
process.removeListener("warning", _onWarning);
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
process.removeListener("warning", _onWarning);
|
|
961
|
-
const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
|
|
962
|
-
let inputFile = "";
|
|
963
|
-
for (const c of candidates) {
|
|
964
|
-
const p = resolve(pagesDir, "..", c);
|
|
965
|
-
if (existsSync(p)) {
|
|
966
|
-
inputFile = p;
|
|
967
|
-
break;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
if (!inputFile) return null;
|
|
971
|
-
try {
|
|
972
|
-
const src = readFileSync(inputFile, "utf-8");
|
|
973
|
-
const result = await postcss([tailwindPlugin(), autoprefixer]).process(src, { from: inputFile });
|
|
974
|
-
tailwindCssCode = result.css;
|
|
975
|
-
} catch (err) {
|
|
976
|
-
console.warn("Tailwind CSS processing failed:", err.message);
|
|
977
|
-
return null;
|
|
978
|
-
}
|
|
979
|
-
const url = "/__wfw/style.css";
|
|
980
|
-
router.get(url, () => new Response(tailwindCssCode, {
|
|
981
|
-
headers: { "content-type": "text/css; charset=utf-8" }
|
|
982
|
-
}));
|
|
983
|
-
if (isDev) {
|
|
984
|
-
_cssWatcher = chokidar.watch(inputFile, { persistent: false });
|
|
985
|
-
_cssWatcher.on("change", async () => {
|
|
986
|
-
try {
|
|
987
|
-
const newSrc = readFileSync(inputFile, "utf-8");
|
|
988
|
-
const newResult = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
|
|
989
|
-
tailwindCssCode = newResult.css;
|
|
990
|
-
broadcastReload();
|
|
991
|
-
} catch (err) {
|
|
992
|
-
console.warn("Tailwind CSS reprocessing failed:", err.message);
|
|
993
|
-
}
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
return url;
|
|
997
|
-
}
|
|
998
|
-
function startFileWatcher(pagesDir, outDir) {
|
|
999
|
-
let timeout = null;
|
|
1000
|
-
const pending = /* @__PURE__ */ new Set();
|
|
1001
|
-
_watcher = chokidar.watch(pagesDir, {
|
|
1002
|
-
ignored: /(^|[/\\])\.(?!\.)|\.weifuwu/,
|
|
1003
|
-
persistent: false,
|
|
1004
|
-
ignoreInitial: true
|
|
1005
|
-
});
|
|
1006
|
-
_watcher.on("all", async (event, filePath) => {
|
|
1007
|
-
if (event !== "change" && event !== "add") return;
|
|
1008
|
-
if (!/\.tsx?$/.test(filePath)) return;
|
|
1009
|
-
pending.add(filePath);
|
|
1010
|
-
if (timeout) clearTimeout(timeout);
|
|
1011
|
-
timeout = setTimeout(async () => {
|
|
1012
|
-
timeout = null;
|
|
1013
|
-
const files = [...pending];
|
|
1014
|
-
pending.clear();
|
|
1015
|
-
for (const f of files) {
|
|
1016
|
-
if (existsSync(f)) await recompileAndSwap(f, outDir);
|
|
1017
|
-
}
|
|
1018
|
-
}, 50);
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
async function recompileAndSwap(filePath, outDir) {
|
|
1022
|
-
try {
|
|
1023
|
-
await esbuild.build({
|
|
1024
|
-
entryPoints: { [id(filePath)]: filePath },
|
|
1025
|
-
outdir: outDir,
|
|
1026
|
-
alias: resolveAliases(),
|
|
1027
|
-
format: "esm",
|
|
1028
|
-
platform: "node",
|
|
1029
|
-
jsx: "automatic",
|
|
1030
|
-
jsxImportSource: "react",
|
|
1031
|
-
bundle: true,
|
|
1032
|
-
external: ["react", "react-dom", "esbuild", "graphql", "ws", "zod", "@graphql-tools/schema", "ai"],
|
|
1033
|
-
write: true,
|
|
1034
|
-
allowOverwrite: true
|
|
1035
|
-
});
|
|
1036
|
-
const bustUrl = compiledUrl(filePath, outDir) + "?t=" + Date.now();
|
|
1037
|
-
const freshMod = await import(bustUrl);
|
|
1038
|
-
const name15 = basename(filePath);
|
|
1039
|
-
if (name15 === "layout.tsx") {
|
|
1040
|
-
layoutModules.set(filePath, freshMod);
|
|
1041
|
-
} else if (name15 === "route.ts") {
|
|
1042
|
-
const handlers = /* @__PURE__ */ new Map();
|
|
1043
|
-
for (const m of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) {
|
|
1044
|
-
if (freshMod[m]) handlers.set(m, freshMod[m]);
|
|
1045
|
-
}
|
|
1046
|
-
routeModules.set(filePath, handlers);
|
|
1047
|
-
} else if (name15 === "load.ts") {
|
|
1048
|
-
loadModules.set(filePath, freshMod);
|
|
1049
|
-
} else {
|
|
1050
|
-
pageModules.set(filePath, freshMod);
|
|
1051
|
-
clientBundles.delete(id(filePath));
|
|
1052
|
-
}
|
|
1053
|
-
if (tailwindCssUrl) {
|
|
1054
|
-
try {
|
|
1055
|
-
const tailwindPlugin = (await import("@tailwindcss/postcss")).default;
|
|
1056
|
-
const postcss = (await import("postcss")).default;
|
|
1057
|
-
const autoprefixer = (await import("autoprefixer")).default;
|
|
1058
|
-
const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
|
|
1059
|
-
let inputFile = "";
|
|
1060
|
-
for (const c of candidates) {
|
|
1061
|
-
const p = resolve(_projectDir, c);
|
|
1062
|
-
if (existsSync(p)) {
|
|
1063
|
-
inputFile = p;
|
|
1064
|
-
break;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
if (inputFile) {
|
|
1068
|
-
const newSrc = readFileSync(inputFile, "utf-8");
|
|
1069
|
-
const result = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
|
|
1070
|
-
tailwindCssCode = result.css;
|
|
1071
|
-
}
|
|
1072
|
-
} catch {
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
broadcastReload();
|
|
1076
|
-
} catch (err) {
|
|
1077
|
-
console.error("recompile failed:", err.message);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
1099
|
|
|
1081
1100
|
// middleware.ts
|
|
1082
1101
|
function logger(options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
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,28 +14,25 @@
|
|
|
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 --external:chokidar --external
|
|
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:postcss",
|
|
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",
|
|
25
24
|
"ai": "^6",
|
|
26
|
-
"autoprefixer": "^10",
|
|
27
25
|
"chokidar": "^5.0.0",
|
|
28
26
|
"esbuild": "^0.28.0",
|
|
29
27
|
"graphql": "^16",
|
|
30
28
|
"ioredis": "^5.11.0",
|
|
31
29
|
"jsonwebtoken": "^9.0.3",
|
|
32
|
-
"postcss": "^8",
|
|
33
30
|
"postgres": "^3.4.9",
|
|
34
31
|
"react": "^19",
|
|
35
32
|
"react-dom": "^19",
|
|
36
|
-
"tailwindcss": "^4",
|
|
37
33
|
"ws": "^8",
|
|
38
|
-
"zod": "^4.4.3"
|
|
34
|
+
"zod": "^4.4.3",
|
|
35
|
+
"@tailwindcss/postcss": "^4"
|
|
39
36
|
},
|
|
40
37
|
"type": "module",
|
|
41
38
|
"license": "MIT",
|