weifuwu 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -10827,7 +10827,10 @@ function serve(handler, options) {
10827
10827
  });
10828
10828
  return {
10829
10829
  stop: () => {
10830
- server.close();
10830
+ return new Promise((resolve3) => {
10831
+ server.closeAllConnections();
10832
+ server.close(() => resolve3());
10833
+ });
10831
10834
  },
10832
10835
  ready,
10833
10836
  get port() {
@@ -11011,44 +11014,51 @@ var Router = class _Router {
11011
11014
  const segments = url.pathname.split("/").filter(Boolean);
11012
11015
  const query = Object.fromEntries(url.searchParams);
11013
11016
  const match = router.matchWsTrie(wsRoot, segments);
11014
- if (!match) {
11015
- socket.destroy();
11017
+ if (match) {
11018
+ const webReq = new Request(url.href, {
11019
+ method: req.method ?? "GET",
11020
+ headers: Object.fromEntries(
11021
+ Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v ?? ""])
11022
+ )
11023
+ });
11024
+ const ctx = { params: match.params, query };
11025
+ if (match.middlewares.length === 0) {
11026
+ upgradeSocket(wss, req, socket, head, match.handler, ctx);
11027
+ return;
11028
+ }
11029
+ let index = 0;
11030
+ const dispatch = async (innerReq, ctx2) => {
11031
+ if (index < match.middlewares.length) {
11032
+ const mw = match.middlewares[index++];
11033
+ return mw(innerReq, ctx2, dispatch);
11034
+ }
11035
+ return await new Promise((resolve3) => {
11036
+ try {
11037
+ upgradeSocket(wss, req, socket, head, match.handler, ctx2);
11038
+ resolve3(new Response(null, { status: 101 }));
11039
+ } catch {
11040
+ socket.destroy();
11041
+ resolve3(new Response("WebSocket upgrade failed", { status: 500 }));
11042
+ }
11043
+ });
11044
+ };
11045
+ Promise.resolve(dispatch(webReq, ctx)).then((result) => {
11046
+ if (result.status !== 101) {
11047
+ sendHttpResponseOnSocket(socket, result);
11048
+ }
11049
+ }).catch(() => {
11050
+ socket.destroy();
11051
+ });
11016
11052
  return;
11017
11053
  }
11018
- const webReq = new Request(url.href, {
11019
- method: req.method ?? "GET",
11020
- headers: Object.fromEntries(
11021
- Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v ?? ""])
11022
- )
11023
- });
11024
- const ctx = { params: match.params, query };
11025
- if (match.middlewares.length === 0) {
11026
- upgradeSocket(wss, req, socket, head, match.handler, ctx);
11054
+ const httpMatch = router.matchTrie("GET", segments);
11055
+ if (httpMatch?.subRouter) {
11056
+ const remaining = "/" + segments.slice(httpMatch.subRouter.remainingIdx).join("/");
11057
+ req.url = remaining;
11058
+ httpMatch.subRouter.router.websocketHandler()(req, socket, head);
11027
11059
  return;
11028
11060
  }
11029
- let index = 0;
11030
- const dispatch = async (innerReq, ctx2) => {
11031
- if (index < match.middlewares.length) {
11032
- const mw = match.middlewares[index++];
11033
- return mw(innerReq, ctx2, dispatch);
11034
- }
11035
- return await new Promise((resolve3) => {
11036
- try {
11037
- upgradeSocket(wss, req, socket, head, match.handler, ctx2);
11038
- resolve3(new Response(null, { status: 101 }));
11039
- } catch {
11040
- socket.destroy();
11041
- resolve3(new Response("WebSocket upgrade failed", { status: 500 }));
11042
- }
11043
- });
11044
- };
11045
- Promise.resolve(dispatch(webReq, ctx)).then((result) => {
11046
- if (result.status !== 101) {
11047
- sendHttpResponseOnSocket(socket, result);
11048
- }
11049
- }).catch(() => {
11050
- socket.destroy();
11051
- });
11061
+ socket.destroy();
11052
11062
  };
11053
11063
  }
11054
11064
  splitPath(path2) {
@@ -11210,14 +11220,64 @@ function sendHttpResponseOnSocket(socket, response) {
11210
11220
  import { createElement, createContext, useContext } from "react";
11211
11221
  import { renderToReadableStream } from "react-dom/server";
11212
11222
  import * as esbuild from "esbuild";
11213
- import { readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
11214
- import { join, relative, resolve, sep, dirname } from "node:path";
11223
+ import { readdirSync, statSync, existsSync, mkdirSync, readFileSync } from "node:fs";
11224
+ import chokidar from "chokidar";
11225
+ import { join, relative, resolve, sep, dirname, basename } from "node:path";
11215
11226
  import { pathToFileURL } from "node:url";
11216
11227
  import { createHash } from "node:crypto";
11217
11228
  var TsxContext = createContext({ params: {}, query: {} });
11218
11229
  function useTsx() {
11219
11230
  return useContext(TsxContext);
11220
11231
  }
11232
+ var pageModules = /* @__PURE__ */ new Map();
11233
+ var layoutModules = /* @__PURE__ */ new Map();
11234
+ var loadModules = /* @__PURE__ */ new Map();
11235
+ var routeModules = /* @__PURE__ */ new Map();
11236
+ var clientBundles = /* @__PURE__ */ new Map();
11237
+ var liveReloadClients = /* @__PURE__ */ new Set();
11238
+ var _watcher = null;
11239
+ var _cssWatcher = null;
11240
+ function broadcastReload() {
11241
+ for (const ws of liveReloadClients) {
11242
+ try {
11243
+ ws.send("reload");
11244
+ } catch {
11245
+ liveReloadClients.delete(ws);
11246
+ }
11247
+ }
11248
+ }
11249
+ var tailwindCssUrl = null;
11250
+ var tailwindCssCode = "";
11251
+ var _projectDir = "";
11252
+ var _watcherStarted = false;
11253
+ var _alias = null;
11254
+ function resolveAliases() {
11255
+ if (_alias) return _alias;
11256
+ const configFiles = ["tsconfig.json", "jsconfig.json"];
11257
+ for (const file of configFiles) {
11258
+ const p = resolve(file);
11259
+ if (existsSync(p)) {
11260
+ try {
11261
+ const config = JSON.parse(readFileSync(p, "utf-8"));
11262
+ const paths = config.compilerOptions?.paths;
11263
+ if (paths) {
11264
+ const alias = {};
11265
+ for (const [key, values] of Object.entries(paths)) {
11266
+ const cleanKey = key.replace("/*", "");
11267
+ const val = values[0]?.replace("/*", "");
11268
+ if (val) alias[cleanKey] = resolve(dirname(p), val);
11269
+ }
11270
+ _alias = alias;
11271
+ return alias;
11272
+ }
11273
+ } catch {
11274
+ }
11275
+ }
11276
+ }
11277
+ _alias = {};
11278
+ return {};
11279
+ }
11280
+ var isDev = process.env.NODE_ENV !== "production";
11221
11281
  function id(s) {
11222
11282
  return createHash("md5").update(s).digest("hex").slice(0, 8);
11223
11283
  }
@@ -11321,7 +11381,7 @@ function resolveLayouts(dir, pagesDir) {
11321
11381
  }
11322
11382
  return layouts.reverse();
11323
11383
  }
11324
- async function compileAll(files, outDir, platform) {
11384
+ async function compileAll(files, outDir, platform, alias) {
11325
11385
  const entryPoints = {};
11326
11386
  for (const f of files) {
11327
11387
  entryPoints[id(f)] = f;
@@ -11345,23 +11405,21 @@ async function compileAll(files, outDir, platform) {
11345
11405
  "@graphql-tools/schema",
11346
11406
  "ai"
11347
11407
  ],
11408
+ alias,
11348
11409
  write: true,
11349
11410
  allowOverwrite: true
11350
11411
  });
11351
11412
  }
11352
11413
  function compiledUrl(filePath, outDir) {
11353
- const hash = id(join(outDir, id(filePath)));
11354
11414
  const p = join(outDir, id(filePath) + ".js");
11355
11415
  return pathToFileURL(p).href;
11356
11416
  }
11357
- var clientBundleCache = /* @__PURE__ */ new Map();
11358
11417
  var clientRouteLog = /* @__PURE__ */ new WeakMap();
11359
11418
  async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
11360
11419
  const key = id(entryPath);
11361
11420
  const url = `/__wfw/client/${key}.js`;
11362
11421
  if (!clientRouteLog.get(router)?.has(url)) {
11363
- let buf = clientBundleCache.get(key);
11364
- if (!buf) {
11422
+ if (!clientBundles.has(key)) {
11365
11423
  try {
11366
11424
  const nested = layoutPaths.slice(1);
11367
11425
  const layoutsImport = nested.map(
@@ -11387,34 +11445,46 @@ async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router)
11387
11445
  format: "esm",
11388
11446
  jsx: "automatic",
11389
11447
  jsxImportSource: "react",
11448
+ alias: resolveAliases(),
11390
11449
  write: false,
11391
11450
  minify: true
11392
11451
  });
11393
- buf = result.outputFiles[0].contents;
11394
- clientBundleCache.set(key, buf);
11452
+ clientBundles.set(key, result.outputFiles[0].contents);
11395
11453
  } catch (err) {
11396
11454
  console.error("hydration bundle failed:", err);
11397
11455
  return null;
11398
11456
  }
11399
11457
  }
11400
- router.get(url, () => new Response(buf, {
11401
- headers: { "content-type": "application/javascript; charset=utf-8" }
11402
- }));
11458
+ router.get(url, () => {
11459
+ const buf = clientBundles.get(key);
11460
+ return buf ? new Response(buf, {
11461
+ headers: { "content-type": "application/javascript; charset=utf-8" }
11462
+ }) : new Response("", { status: 500 });
11463
+ });
11403
11464
  const set = clientRouteLog.get(router) ?? /* @__PURE__ */ new Set();
11404
11465
  set.add(url);
11405
11466
  clientRouteLog.set(router, set);
11406
11467
  }
11407
11468
  return { url };
11408
11469
  }
11409
- function makeSsrHandler(Component, loadFn, layouts, entryPath, layoutPaths, pagesDir, router) {
11470
+ function makeSsrHandler(entryPath, layoutPaths, loadPath, pagesDir, router) {
11410
11471
  return async (req, ctx) => {
11472
+ const pageMod = pageModules.get(entryPath);
11473
+ if (!pageMod) return new Response("", { status: 500 });
11474
+ const Component = pageMod.default;
11475
+ const loadMod = loadPath ? loadModules.get(loadPath) : void 0;
11476
+ const loadFn = loadMod?.default;
11411
11477
  const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
11412
11478
  const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
11413
11479
  let element = createElement(Component, allProps);
11414
- for (let i = layouts.length - 1; i >= 0; i--) {
11480
+ for (let i = layoutPaths.length - 1; i >= 0; i--) {
11481
+ const lp = layoutPaths[i];
11482
+ const LMod = layoutModules.get(lp);
11483
+ if (!LMod) continue;
11484
+ const Layout = LMod.default;
11415
11485
  const isRoot = i === 0;
11416
11486
  element = createElement(
11417
- layouts[i],
11487
+ Layout,
11418
11488
  isRoot ? { children: element, req, ctx } : { children: element }
11419
11489
  );
11420
11490
  }
@@ -11429,9 +11499,20 @@ function makeSsrHandler(Component, loadFn, layouts, entryPath, layoutPaths, page
11429
11499
  if (bundle) {
11430
11500
  scripts.push(`<script type="module" src="${bundle.url}"></script>`);
11431
11501
  }
11432
- const html = `<!DOCTYPE html>
11502
+ let html = `<!DOCTYPE html>
11433
11503
  ${body}
11434
11504
  ${scripts.join("\n")}`;
11505
+ if (tailwindCssUrl && html.includes("</head>")) {
11506
+ html = html.replace(
11507
+ "</head>",
11508
+ `<link rel="stylesheet" href="${tailwindCssUrl}" />
11509
+ </head>`
11510
+ );
11511
+ }
11512
+ if (isDev) {
11513
+ html += `
11514
+ <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
11515
+ }
11435
11516
  return new Response(html, {
11436
11517
  headers: { "content-type": "text/html; charset=utf-8" }
11437
11518
  });
@@ -11439,6 +11520,7 @@ ${scripts.join("\n")}`;
11439
11520
  }
11440
11521
  async function tsx(options) {
11441
11522
  const pagesDir = resolve(options.dir);
11523
+ _projectDir = resolve(pagesDir, "..");
11442
11524
  const outDir = join(pagesDir, "..", ".weifuwu", "ssr");
11443
11525
  const pages = scanPages(pagesDir);
11444
11526
  if (pages.length === 0) return new Router();
@@ -11457,79 +11539,102 @@ async function tsx(options) {
11457
11539
  for (const lp of rootLayouts) allFiles.add(lp);
11458
11540
  }
11459
11541
  mkdirSync(outDir, { recursive: true });
11460
- await compileAll([...allFiles], outDir, "node");
11542
+ const alias = resolveAliases();
11543
+ await compileAll([...allFiles], outDir, "node", alias);
11461
11544
  const router = new Router();
11545
+ const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
11462
11546
  for (const p of pages) {
11463
11547
  if (p.routeOnly && p.routePath) {
11464
11548
  const rUrl = compiledUrl(p.routePath, outDir);
11465
11549
  const modR = await import(rUrl);
11466
- const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
11467
- for (const method of methods) {
11468
- if (modR[method]) {
11469
- router.route(method, p.route, modR[method]);
11470
- }
11550
+ const handlers = /* @__PURE__ */ new Map();
11551
+ for (const m of ["GET", ...methods]) {
11552
+ if (modR[m]) handlers.set(m, modR[m]);
11553
+ }
11554
+ routeModules.set(p.routePath, handlers);
11555
+ router.route(
11556
+ "GET",
11557
+ p.route,
11558
+ (req, ctx) => routeModules.get(p.routePath)?.get("GET")?.(req, ctx) ?? new Response("", { status: 501 })
11559
+ );
11560
+ for (const m of methods) {
11561
+ router.route(
11562
+ m,
11563
+ p.route,
11564
+ (req, ctx) => routeModules.get(p.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
11565
+ );
11471
11566
  }
11472
11567
  continue;
11473
11568
  }
11474
- const url = compiledUrl(p.entryPath, outDir);
11475
- const mod = await import(url);
11476
- const Component = mod.default;
11477
- let loadFn;
11569
+ const pageUrl = compiledUrl(p.entryPath, outDir);
11570
+ pageModules.set(p.entryPath, await import(pageUrl));
11478
11571
  if (p.loadPath) {
11479
11572
  const loadUrl = compiledUrl(p.loadPath, outDir);
11480
- const modLoad = await import(loadUrl);
11481
- loadFn = modLoad.default;
11573
+ loadModules.set(p.loadPath, await import(loadUrl));
11482
11574
  }
11483
- const layoutComponents = [];
11484
11575
  for (const lp of p.layouts) {
11485
11576
  const lUrl = compiledUrl(lp, outDir);
11486
- const modL = await import(lUrl);
11487
- layoutComponents.push(modL.default);
11488
- }
11489
- const handler = makeSsrHandler(
11490
- Component,
11491
- loadFn,
11492
- layoutComponents,
11493
- p.entryPath,
11494
- p.layouts,
11495
- pagesDir,
11496
- router
11497
- );
11498
- router.get(p.route, handler);
11577
+ layoutModules.set(lp, await import(lUrl));
11578
+ }
11499
11579
  if (p.routePath) {
11500
11580
  const rUrl = compiledUrl(p.routePath, outDir);
11501
11581
  const modR = await import(rUrl);
11502
- const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
11503
- for (const method of methods) {
11504
- if (modR[method]) {
11505
- router.route(method, p.route, modR[method]);
11506
- }
11582
+ const handlers = /* @__PURE__ */ new Map();
11583
+ for (const m of methods) {
11584
+ if (modR[m]) handlers.set(m, modR[m]);
11585
+ }
11586
+ routeModules.set(p.routePath, handlers);
11587
+ }
11588
+ const handler = makeSsrHandler(p.entryPath, p.layouts, p.loadPath, pagesDir, router);
11589
+ router.get(p.route, handler);
11590
+ if (p.routePath) {
11591
+ for (const m of methods) {
11592
+ router.route(
11593
+ m,
11594
+ p.route,
11595
+ (req, ctx) => routeModules.get(p.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
11596
+ );
11507
11597
  }
11508
11598
  }
11509
11599
  }
11510
11600
  if (hasNotFound) {
11511
11601
  const nfUrl = compiledUrl(nfPath, outDir);
11512
- const modNf = await import(nfUrl);
11513
- const NfComponent = modNf.default;
11514
- const nfLayouts = [];
11602
+ pageModules.set(nfPath, await import(nfUrl));
11515
11603
  const rootLayouts = resolveLayouts(pagesDir, pagesDir);
11516
11604
  for (const lp of rootLayouts) {
11517
- const lUrl = compiledUrl(lp, outDir);
11518
- const modL = await import(lUrl);
11519
- nfLayouts.push(modL.default);
11605
+ if (!layoutModules.has(lp)) {
11606
+ const lUrl = compiledUrl(lp, outDir);
11607
+ layoutModules.set(lp, await import(lUrl));
11608
+ }
11520
11609
  }
11521
11610
  const handler = async (req, ctx) => {
11611
+ const nfMod = pageModules.get(nfPath);
11612
+ if (!nfMod) return new Response("Not Found", { status: 404 });
11613
+ const NfComponent = nfMod.default;
11522
11614
  let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
11523
- for (let i = nfLayouts.length - 1; i >= 0; i--) {
11524
- element = createElement(nfLayouts[i], { children: element });
11615
+ for (let i = rootLayouts.length - 1; i >= 0; i--) {
11616
+ const LMod = layoutModules.get(rootLayouts[i]);
11617
+ if (!LMod) continue;
11618
+ element = createElement(LMod.default, { children: element });
11525
11619
  }
11526
11620
  element = createElement(TsxContext.Provider, {
11527
11621
  value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
11528
11622
  }, element);
11529
11623
  const stream = await renderToReadableStream(element);
11530
11624
  const body = await readStream(stream);
11531
- const html = `<!DOCTYPE html>
11625
+ let html = `<!DOCTYPE html>
11532
11626
  ${body}`;
11627
+ if (tailwindCssUrl && html.includes("</head>")) {
11628
+ html = html.replace(
11629
+ "</head>",
11630
+ `<link rel="stylesheet" href="${tailwindCssUrl}" />
11631
+ </head>`
11632
+ );
11633
+ }
11634
+ if (isDev) {
11635
+ html += `
11636
+ <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
11637
+ }
11533
11638
  return new Response(html, {
11534
11639
  status: 404,
11535
11640
  headers: { "content-type": "text/html; charset=utf-8" }
@@ -11537,8 +11642,150 @@ ${body}`;
11537
11642
  };
11538
11643
  router.all("/*", handler);
11539
11644
  }
11645
+ tailwindCssUrl = await setupTailwind(pagesDir, router, alias);
11646
+ if (isDev) {
11647
+ router.ws("/__weifuwu/livereload", {
11648
+ open(ws) {
11649
+ liveReloadClients.add(ws);
11650
+ ws.on("close", () => liveReloadClients.delete(ws));
11651
+ ws.on("error", () => liveReloadClients.delete(ws));
11652
+ }
11653
+ });
11654
+ if (!_watcherStarted) {
11655
+ startFileWatcher(pagesDir, outDir);
11656
+ _watcherStarted = true;
11657
+ }
11658
+ }
11540
11659
  return router;
11541
11660
  }
11661
+ async function setupTailwind(pagesDir, router, alias) {
11662
+ let tailwindPlugin, postcss, autoprefixer;
11663
+ try {
11664
+ tailwindPlugin = (await import("@tailwindcss/postcss")).default;
11665
+ postcss = (await import("postcss")).default;
11666
+ autoprefixer = (await import("autoprefixer")).default;
11667
+ } catch {
11668
+ return null;
11669
+ }
11670
+ const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
11671
+ let inputFile = "";
11672
+ for (const c of candidates) {
11673
+ const p = resolve(pagesDir, "..", c);
11674
+ if (existsSync(p)) {
11675
+ inputFile = p;
11676
+ break;
11677
+ }
11678
+ }
11679
+ if (!inputFile) return null;
11680
+ try {
11681
+ const src = readFileSync(inputFile, "utf-8");
11682
+ const result = await postcss([tailwindPlugin(), autoprefixer]).process(src, { from: inputFile });
11683
+ tailwindCssCode = result.css;
11684
+ } catch (err) {
11685
+ console.warn("Tailwind CSS processing failed:", err.message);
11686
+ return null;
11687
+ }
11688
+ const url = "/__wfw/style.css";
11689
+ router.get(url, () => new Response(tailwindCssCode, {
11690
+ headers: { "content-type": "text/css; charset=utf-8" }
11691
+ }));
11692
+ if (isDev) {
11693
+ _cssWatcher = chokidar.watch(inputFile, { persistent: false });
11694
+ _cssWatcher.on("change", async () => {
11695
+ try {
11696
+ const newSrc = readFileSync(inputFile, "utf-8");
11697
+ const newResult = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
11698
+ tailwindCssCode = newResult.css;
11699
+ broadcastReload();
11700
+ } catch (err) {
11701
+ console.warn("Tailwind CSS reprocessing failed:", err.message);
11702
+ }
11703
+ });
11704
+ }
11705
+ return url;
11706
+ }
11707
+ function startFileWatcher(pagesDir, outDir) {
11708
+ let timeout = null;
11709
+ const pending = /* @__PURE__ */ new Set();
11710
+ _watcher = chokidar.watch(pagesDir, {
11711
+ ignored: /(^|[/\\])\.(?!\.)|\.weifuwu/,
11712
+ persistent: false,
11713
+ ignoreInitial: true
11714
+ });
11715
+ _watcher.on("all", async (event, filePath) => {
11716
+ if (event !== "change" && event !== "add") return;
11717
+ if (!/\.tsx?$/.test(filePath)) return;
11718
+ pending.add(filePath);
11719
+ if (timeout) clearTimeout(timeout);
11720
+ timeout = setTimeout(async () => {
11721
+ timeout = null;
11722
+ const files = [...pending];
11723
+ pending.clear();
11724
+ for (const f of files) {
11725
+ if (existsSync(f)) await recompileAndSwap(f, outDir);
11726
+ }
11727
+ }, 50);
11728
+ });
11729
+ }
11730
+ async function recompileAndSwap(filePath, outDir) {
11731
+ try {
11732
+ await esbuild.build({
11733
+ entryPoints: { [id(filePath)]: filePath },
11734
+ outdir: outDir,
11735
+ alias: resolveAliases(),
11736
+ format: "esm",
11737
+ platform: "node",
11738
+ jsx: "automatic",
11739
+ jsxImportSource: "react",
11740
+ bundle: true,
11741
+ external: ["react", "react-dom", "esbuild", "graphql", "ws", "zod", "@graphql-tools/schema", "ai"],
11742
+ write: true,
11743
+ allowOverwrite: true
11744
+ });
11745
+ const bustUrl = compiledUrl(filePath, outDir) + "?t=" + Date.now();
11746
+ const freshMod = await import(bustUrl);
11747
+ const name15 = basename(filePath);
11748
+ if (name15 === "layout.tsx") {
11749
+ layoutModules.set(filePath, freshMod);
11750
+ } else if (name15 === "route.ts") {
11751
+ const handlers = /* @__PURE__ */ new Map();
11752
+ for (const m of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) {
11753
+ if (freshMod[m]) handlers.set(m, freshMod[m]);
11754
+ }
11755
+ routeModules.set(filePath, handlers);
11756
+ } else if (name15 === "load.ts") {
11757
+ loadModules.set(filePath, freshMod);
11758
+ } else {
11759
+ pageModules.set(filePath, freshMod);
11760
+ clientBundles.delete(id(filePath));
11761
+ }
11762
+ if (tailwindCssUrl) {
11763
+ try {
11764
+ const tailwindPlugin = (await import("@tailwindcss/postcss")).default;
11765
+ const postcss = (await import("postcss")).default;
11766
+ const autoprefixer = (await import("autoprefixer")).default;
11767
+ const candidates = ["app.css", "globals.css", "src/app.css", "src/globals.css", "style.css"];
11768
+ let inputFile = "";
11769
+ for (const c of candidates) {
11770
+ const p = resolve(_projectDir, c);
11771
+ if (existsSync(p)) {
11772
+ inputFile = p;
11773
+ break;
11774
+ }
11775
+ }
11776
+ if (inputFile) {
11777
+ const newSrc = readFileSync(inputFile, "utf-8");
11778
+ const result = await postcss([tailwindPlugin(), autoprefixer]).process(newSrc, { from: inputFile });
11779
+ tailwindCssCode = result.css;
11780
+ }
11781
+ } catch {
11782
+ }
11783
+ }
11784
+ broadcastReload();
11785
+ } catch (err) {
11786
+ console.error("recompile failed:", err.message);
11787
+ }
11788
+ }
11542
11789
 
11543
11790
  // middleware.ts
11544
11791
  function logger(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,21 +14,26 @@
14
14
  "LICENSE"
15
15
  ],
16
16
  "scripts": {
17
- "build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:react --external:react-dom --external:esbuild --external:graphql --external:ws --external:zod --external:@graphql-tools/schema --external:ai --external:postgres --external:jsonwebtoken",
17
+ "build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:react --external:react-dom --external:esbuild --external:graphql --external:ws --external:zod --external:@graphql-tools/schema --external:ai --external:postgres --external:jsonwebtoken --external:chokidar --external:tailwindcss --external:@tailwindcss/* --external:postcss --external:autoprefixer --external:lightningcss",
18
18
  "prepublishOnly": "npm run build && tsc --emitDeclarationOnly --outdir dist",
19
19
  "test": "node --test 'test/**/*.test.ts'"
20
20
  },
21
21
  "dependencies": {
22
22
  "@ai-sdk/openai": "^3.0.66",
23
23
  "@graphql-tools/schema": "^10",
24
+ "@tailwindcss/postcss": "^4",
24
25
  "ai": "^6",
26
+ "autoprefixer": "^10",
27
+ "chokidar": "^5.0.0",
25
28
  "esbuild": "^0.28.0",
26
29
  "graphql": "^16",
27
30
  "ioredis": "^5.11.0",
28
31
  "jsonwebtoken": "^9.0.3",
32
+ "postcss": "^8",
29
33
  "postgres": "^3.4.9",
30
34
  "react": "^19",
31
35
  "react-dom": "^19",
36
+ "tailwindcss": "^4",
32
37
  "ws": "^8",
33
38
  "zod": "^4.4.3"
34
39
  },