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.
Files changed (3) hide show
  1. package/README.md +150 -28
  2. package/dist/index.js +186 -167
  3. 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: './pages/' }))
885
+ app.use('/', await tsx({ dir: './ui/' }))
855
886
 
856
887
  serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
857
888
  ```
858
889
 
859
- ### Development mode
890
+ ### Directory structure
860
891
 
861
- `tsx()` automatically runs in development mode (`NODE_ENV !== 'production'`):
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
- - **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
912
+ tsx() runs in development mode automatically when `NODE_ENV !== 'production'`:
867
913
 
868
- Production mode (`NODE_ENV=production`) disables file watching and live reload. All other features work the same.
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
- ### File conventions
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
- pages/
879
- page.tsx → GET / (React component, default export)
880
- layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
881
- not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
882
- about/page.tsx → GET /about
883
- blog/[slug]/
884
- page.tsx → GET /blog/:slug
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 on changes)
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` | — | Pages directory path |
1234
+ | `dir` | — | UI directory path (containing `pages/` and optionally `components/`) |
1114
1235
 
1115
1236
  Returns `Promise<Router>`.
1116
1237
 
1117
- Development features (auto-detected, no configuration needed):
1238
+ Auto-detected features (no configuration needed):
1118
1239
 
1119
1240
  | Feature | Behavior |
1120
1241
  |---------|----------|
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 |
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
- return new Promise((resolve3) => {
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 _projectDir = "";
534
- var _watcherStarted = false;
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 (!clientBundles.has(key)) {
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
- clientBundles.set(key, result.outputFiles[0].contents);
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 = clientBundles.get(key);
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 pagesDir = resolve(options.dir);
805
- _projectDir = resolve(pagesDir, "..");
806
- const outDir = join(pagesDir, "..", ".weifuwu", "ssr");
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
- const alias = resolveAliases();
825
- await compileAll([...allFiles], outDir, "node", alias);
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
- tailwindCssUrl = await setupTailwind(pagesDir, router, alias);
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
- if (!_watcherStarted) {
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.2",
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:tailwindcss --external:@tailwindcss/* --external:postcss --external:autoprefixer --external:lightningcss",
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",