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.
Files changed (3) hide show
  1. package/README.md +36 -5
  2. package/dist/index.js +338 -91
  3. 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 --watch app.ts # development
955
- node app.ts # production
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` | — | Pages directory path |
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
- server.close();
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 (!match) {
297
- socket.destroy();
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 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);
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
- 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
- });
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 { join, relative, resolve, sep, dirname } from "node:path";
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
- let buf = clientBundleCache.get(key);
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
- buf = result.outputFiles[0].contents;
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, () => new Response(buf, {
683
- headers: { "content-type": "application/javascript; charset=utf-8" }
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(Component, loadFn, layouts, entryPath, layoutPaths, pagesDir, router) {
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 = layouts.length - 1; i >= 0; 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
- layouts[i],
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
- const html = `<!DOCTYPE html>
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
- await compileAll([...allFiles], outDir, "node");
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 methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
749
- for (const method of methods) {
750
- if (modR[method]) {
751
- router.route(method, p.route, modR[method]);
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 url = compiledUrl(p.entryPath, outDir);
757
- const mod = await import(url);
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
- const modLoad = await import(loadUrl);
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
- const modL = await import(lUrl);
769
- layoutComponents.push(modL.default);
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 methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
785
- for (const method of methods) {
786
- if (modR[method]) {
787
- router.route(method, p.route, modR[method]);
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
- const modNf = await import(nfUrl);
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
- const lUrl = compiledUrl(lp, outDir);
800
- const modL = await import(lUrl);
801
- nfLayouts.push(modL.default);
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 = nfLayouts.length - 1; i >= 0; i--) {
806
- element = createElement(nfLayouts[i], { children: element });
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
- const html = `<!DOCTYPE html>
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.8.2",
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
  },