weifuwu 0.31.1 → 0.31.2

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/dist/index.js CHANGED
@@ -225,14 +225,14 @@ function serve(router, options) {
225
225
  if (!server.listening) return;
226
226
  server.close();
227
227
  server.closeIdleConnections();
228
- return new Promise((resolve2) => {
228
+ return new Promise((resolve5) => {
229
229
  const timer = setTimeout(() => {
230
230
  server.closeAllConnections();
231
- resolve2();
231
+ resolve5();
232
232
  }, timeoutMs);
233
233
  server.on("close", () => {
234
234
  clearTimeout(timer);
235
- resolve2();
235
+ resolve5();
236
236
  });
237
237
  });
238
238
  }
@@ -277,7 +277,7 @@ function createHub(opts) {
277
277
  }
278
278
  });
279
279
  }
280
- function join2(key, ws) {
280
+ function join3(key, ws) {
281
281
  if (!channels.has(key)) {
282
282
  channels.set(key, /* @__PURE__ */ new Set());
283
283
  redisSub?.subscribe(`${prefix}${key}`);
@@ -339,7 +339,7 @@ function createHub(opts) {
339
339
  redisPub = void 0;
340
340
  redisSub = null;
341
341
  }
342
- return { join: join2, leave, broadcast, close };
342
+ return { join: join3, leave, broadcast, close };
343
343
  }
344
344
 
345
345
  // src/core/ws.ts
@@ -503,6 +503,20 @@ var Router = class _Router {
503
503
  this._checkMiddlewareMeta(mw, "global");
504
504
  return this;
505
505
  }
506
+ /**
507
+ * Install a plugin — a function that configures the Router with
508
+ * routes, middleware, and error handlers. Use when `.use()` isn't
509
+ * enough because you need to call `app.get()`, `app.onError()`, etc.
510
+ *
511
+ * @example
512
+ * ```ts
513
+ * app.plugin(app => createReactApp(app, { pages: {...}, layout: '...', tailwind: {...} }))
514
+ * ```
515
+ */
516
+ plugin(fn) {
517
+ fn(this);
518
+ return this;
519
+ }
506
520
  mount(path, router) {
507
521
  this._mountRouter(path, router);
508
522
  return this;
@@ -912,13 +926,13 @@ function serveStatic(root, options) {
912
926
  let fileHandle;
913
927
  try {
914
928
  fileHandle = await open(filePath, "r");
915
- let stat = await fileHandle.stat();
929
+ let stat3 = await fileHandle.stat();
916
930
  const realPath = await realpath(filePath);
917
931
  if (!realPath.startsWith(rootDir + sep) && realPath !== rootDir) {
918
932
  await fileHandle.close();
919
933
  return new Response("Forbidden", { status: 403 });
920
934
  }
921
- if (stat.isDirectory()) {
935
+ if (stat3.isDirectory()) {
922
936
  await fileHandle.close();
923
937
  const indexFile = opts.index ?? "index.html";
924
938
  filePath = resolve(filePath, indexFile);
@@ -926,29 +940,29 @@ function serveStatic(root, options) {
926
940
  return new Response("Forbidden", { status: 403 });
927
941
  }
928
942
  fileHandle = await open(filePath, "r");
929
- stat = await fileHandle.stat();
930
- if (!stat.isFile()) {
943
+ stat3 = await fileHandle.stat();
944
+ if (!stat3.isFile()) {
931
945
  await fileHandle.close();
932
946
  return new Response("Not Found", { status: 404 });
933
947
  }
934
948
  }
935
949
  const mimeType = MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
936
- const etag = `"${stat.ino}-${stat.size}-${stat.mtimeMs}"`;
950
+ const etag = `"${stat3.ino}-${stat3.size}-${stat3.mtimeMs}"`;
937
951
  const ifNoneMatch = req.headers.get("if-none-match");
938
952
  if (ifNoneMatch === etag) {
939
953
  await fileHandle.close();
940
954
  return new Response(null, { status: 304 });
941
955
  }
942
956
  const ifModifiedSince = req.headers.get("if-modified-since");
943
- if (ifModifiedSince && stat.mtimeMs <= new Date(ifModifiedSince).getTime()) {
957
+ if (ifModifiedSince && stat3.mtimeMs <= new Date(ifModifiedSince).getTime()) {
944
958
  await fileHandle.close();
945
959
  return new Response(null, { status: 304 });
946
960
  }
947
961
  const headers = {
948
962
  "Content-Type": mimeType,
949
- "Content-Length": String(stat.size),
963
+ "Content-Length": String(stat3.size),
950
964
  ETag: etag,
951
- "Last-Modified": stat.mtime.toUTCString(),
965
+ "Last-Modified": stat3.mtime.toUTCString(),
952
966
  "Cache-Control": opts.immutable ? `public, max-age=${opts.maxAge ?? 31536e3}, immutable` : `public, max-age=${opts.maxAge ?? 0}`
953
967
  };
954
968
  const readStream = fileHandle.createReadStream();
@@ -1099,6 +1113,536 @@ function upload(options) {
1099
1113
  return mw;
1100
1114
  }
1101
1115
 
1116
+ // src/middleware/esbuild-dev.ts
1117
+ import { resolve as resolve2, relative, dirname } from "node:path";
1118
+ import { stat, readFile, realpath as realpath2 } from "node:fs/promises";
1119
+ function escapeHtml(s) {
1120
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1121
+ }
1122
+ function defaultErrorTemplate(errors) {
1123
+ return `<!DOCTYPE html>
1124
+ <html>
1125
+ <head>
1126
+ <meta charset="utf-8">
1127
+ <title>Build Error</title>
1128
+ <style>
1129
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1130
+ body { font-family: system-ui; background: #1a1a2e; color: #e0e0e0; padding: 2rem; }
1131
+ .overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
1132
+ .card { background: #16213e; border: 1px solid #e74c3c; border-radius: 12px; padding: 2rem; max-width: 800px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
1133
+ h1 { color: #e74c3c; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
1134
+ pre { background: #0f0f23; padding: 1.5rem; border-radius: 8px; overflow: auto; font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
1135
+ </style>
1136
+ </head>
1137
+ <body>
1138
+ <div class="overlay">
1139
+ <div class="card">
1140
+ <h1>\u26A0 esbuild Build Error</h1>
1141
+ <pre>${escapeHtml(errors)}</pre>
1142
+ </div>
1143
+ </div>
1144
+ </body>
1145
+ </html>`;
1146
+ }
1147
+ async function collectDeps(entryPath) {
1148
+ const files = /* @__PURE__ */ new Map();
1149
+ const visited = /* @__PURE__ */ new Set();
1150
+ const queue2 = [entryPath];
1151
+ while (queue2.length > 0) {
1152
+ const p = queue2.shift();
1153
+ try {
1154
+ const real = await realpath2(p);
1155
+ if (visited.has(real)) continue;
1156
+ visited.add(real);
1157
+ const s = await stat(real);
1158
+ files.set(real, s.mtimeMs);
1159
+ } catch {
1160
+ continue;
1161
+ }
1162
+ try {
1163
+ const content = await readFile(p, "utf-8");
1164
+ const importRe = /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g;
1165
+ let m;
1166
+ while ((m = importRe.exec(content)) !== null) {
1167
+ const spec = m[1] ?? m[2];
1168
+ if (spec.startsWith(".") || spec.startsWith("/")) {
1169
+ const dir = dirname(p);
1170
+ const resolved = resolve2(dir, spec);
1171
+ for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"]) {
1172
+ const candidate = resolved + ext;
1173
+ if (!visited.has(candidate)) {
1174
+ queue2.push(candidate);
1175
+ break;
1176
+ }
1177
+ break;
1178
+ }
1179
+ queue2.push(resolved);
1180
+ }
1181
+ }
1182
+ } catch {
1183
+ }
1184
+ }
1185
+ return files;
1186
+ }
1187
+ function esbuildDev(opts) {
1188
+ const cache = opts.cache ?? "memory";
1189
+ const importmap = opts.importmap ?? false;
1190
+ const importmapOverrides = opts.importmapOverrides ?? {};
1191
+ const errorTemplate = opts.errorTemplate ?? defaultErrorTemplate;
1192
+ const entries = Object.entries(opts.entries).map(([p, e]) => {
1193
+ const path = p.startsWith("/") ? p : "/" + p;
1194
+ const config = typeof e === "string" ? { entry: e } : e;
1195
+ return { path, config };
1196
+ });
1197
+ const cacheStore = /* @__PURE__ */ new Map();
1198
+ const chunkMap = /* @__PURE__ */ new Map();
1199
+ let esbuild_ = null;
1200
+ let esbuildLoadError = null;
1201
+ async function getEsbuild() {
1202
+ if (esbuild_) return esbuild_;
1203
+ try {
1204
+ esbuild_ = await import("esbuild");
1205
+ return esbuild_;
1206
+ } catch (err) {
1207
+ esbuildLoadError = `esbuild is not installed. Run: npm install -D esbuild
1208
+
1209
+ ${String(err)}`;
1210
+ throw new Error(esbuildLoadError, { cause: err });
1211
+ }
1212
+ }
1213
+ let importmapScript = null;
1214
+ function buildImportmap() {
1215
+ const mappings = {};
1216
+ Object.assign(mappings, importmapOverrides);
1217
+ for (const { path, config } of entries) {
1218
+ const entryName = path.split("/").pop() ?? "";
1219
+ if (entryName.includes("vendor") || config.external && config.external.length === 0 || !config.external && config.bundle !== false) {
1220
+ if (!mappings["react"]) mappings["react"] = path;
1221
+ if (!mappings["react/jsx-runtime"]) mappings["react/jsx-runtime"] = path;
1222
+ if (!mappings["react-dom/client"]) mappings["react-dom/client"] = path;
1223
+ if (!mappings["react-dom"]) mappings["react-dom"] = path;
1224
+ }
1225
+ }
1226
+ const json = JSON.stringify({ imports: mappings });
1227
+ return `<script type="importmap">${json}</script>`;
1228
+ }
1229
+ async function compile(entry) {
1230
+ const esbuild = await getEsbuild();
1231
+ const isClientRouter = !!entry.clientRouter;
1232
+ let entryAbs;
1233
+ let buildOpts;
1234
+ if (isClientRouter) {
1235
+ const cr = entry.clientRouter;
1236
+ const virtualModule = "weifuwu:client-entry";
1237
+ entryAbs = virtualModule;
1238
+ const layoutImport = `import * as _layout from '${cr.layout}'
1239
+ const _lx = Object.entries(_layout).filter(([,v]) => typeof v === 'function')
1240
+ const Layout = _layout.default || (_lx[0]?.[1])`;
1241
+ let routesCode;
1242
+ if (cr.pages) {
1243
+ const entries2 = Object.entries(cr.pages).map(
1244
+ ([path, component]) => ` '${path}': () => import('${component}'),`
1245
+ );
1246
+ routesCode = `const routes = {
1247
+ ${entries2.join("\n")}
1248
+ }`;
1249
+ } else if (cr.routes) {
1250
+ routesCode = `import { routes } from '${cr.routes}'`;
1251
+ } else {
1252
+ routesCode = "const routes = {}";
1253
+ }
1254
+ const fallbackLine = cr.fallback ? `const fallback = () => import('${cr.fallback}')` : "";
1255
+ const fallbackOpt = cr.fallback ? " fallback," : "";
1256
+ const generatedCode = [
1257
+ `import { createBrowserRouter } from 'weifuwu/react/client'`,
1258
+ layoutImport,
1259
+ routesCode,
1260
+ fallbackLine,
1261
+ "",
1262
+ "createBrowserRouter({",
1263
+ " layout: Layout,",
1264
+ " routes,",
1265
+ fallbackOpt,
1266
+ "})"
1267
+ ].filter(Boolean).join("\n");
1268
+ buildOpts = {
1269
+ entryPoints: [virtualModule],
1270
+ bundle: entry.bundle ?? true,
1271
+ external: entry.external ?? [],
1272
+ minify: entry.minify ?? true,
1273
+ platform: entry.platform ?? "browser",
1274
+ format: entry.format ?? "esm",
1275
+ sourcemap: entry.sourcemap ?? false,
1276
+ splitting: entry.splitting ?? false,
1277
+ ...entry.splitting ? { outdir: resolve2(".weifuwu-esbuild-out") } : {},
1278
+ define: entry.define,
1279
+ loader: entry.loader,
1280
+ write: false,
1281
+ logLevel: "silent",
1282
+ plugins: [{
1283
+ name: "weifuwu-client-router",
1284
+ setup(build) {
1285
+ build.onResolve({ filter: new RegExp(`^${virtualModule.replace(/:/g, "\\:")}$`) }, () => ({
1286
+ path: virtualModule,
1287
+ namespace: "weifuwu-client"
1288
+ }));
1289
+ build.onLoad({ filter: /.*/, namespace: "weifuwu-client" }, () => ({
1290
+ contents: generatedCode,
1291
+ loader: "ts",
1292
+ resolveDir: process.cwd()
1293
+ }));
1294
+ }
1295
+ }]
1296
+ };
1297
+ } else {
1298
+ entryAbs = resolve2(entry.entry);
1299
+ buildOpts = {
1300
+ entryPoints: [entryAbs],
1301
+ bundle: entry.bundle ?? true,
1302
+ external: entry.external ?? [],
1303
+ minify: entry.minify ?? true,
1304
+ platform: entry.platform ?? "browser",
1305
+ format: entry.format ?? "esm",
1306
+ sourcemap: entry.sourcemap ?? false,
1307
+ splitting: entry.splitting ?? false,
1308
+ ...entry.splitting ? { outdir: dirname(entryAbs) } : {},
1309
+ define: entry.define,
1310
+ loader: entry.loader,
1311
+ write: false,
1312
+ logLevel: "silent"
1313
+ };
1314
+ }
1315
+ const result = await esbuild.build(buildOpts);
1316
+ const msgs = [];
1317
+ for (const w of result.warnings) {
1318
+ const loc = w.location ? ` at ${relative(".", w.location.file ?? "")}:${w.location.line}:${w.location.column}` : "";
1319
+ msgs.push(`[warn] ${w.text}${loc}`);
1320
+ }
1321
+ for (const e of result.errors) {
1322
+ const loc = e.location ? ` at ${relative(".", e.location.file ?? "")}:${e.location.line}:${e.location.column}` : "";
1323
+ msgs.push(`[error] ${e.text}${loc}`);
1324
+ }
1325
+ if (result.errors.length > 0) {
1326
+ throw new Error(msgs.join("\n"));
1327
+ }
1328
+ const outputFiles = result.outputFiles;
1329
+ const entryFile = outputFiles.find((f) => f.path === entryAbs) ?? outputFiles[0];
1330
+ const code = entryFile?.text ?? "";
1331
+ let chunks;
1332
+ if (outputFiles.length > 1) {
1333
+ chunks = /* @__PURE__ */ new Map();
1334
+ for (const f of outputFiles) {
1335
+ if (f === entryFile) continue;
1336
+ const chunkName = f.path.split("/").pop() ?? f.path;
1337
+ chunks.set(chunkName, { code: f.text, etag: `"esbuild-${hashCode(f.text)}"` });
1338
+ }
1339
+ }
1340
+ const etag = `"esbuild-${hashCode(code)}"`;
1341
+ if (msgs.length > 0) {
1342
+ console.error("[esbuildDev]", msgs.join("\n"));
1343
+ }
1344
+ return { code, etag, chunks };
1345
+ }
1346
+ function isCacheValid2(cached) {
1347
+ return Promise.all(
1348
+ [...cached.files].map(async ([file, mtime]) => {
1349
+ try {
1350
+ const s = await stat(file);
1351
+ return s.mtimeMs === mtime;
1352
+ } catch {
1353
+ return false;
1354
+ }
1355
+ })
1356
+ ).then((results) => results.every(Boolean));
1357
+ }
1358
+ return async (req, ctx, next) => {
1359
+ const url = new URL(req.url);
1360
+ const pathname = url.pathname;
1361
+ if (importmap && pathname === "/assets/importmap") {
1362
+ if (!importmapScript) importmapScript = buildImportmap();
1363
+ return new Response(importmapScript, {
1364
+ headers: {
1365
+ "Content-Type": "application/javascript; charset=utf-8",
1366
+ "Cache-Control": "no-cache"
1367
+ }
1368
+ });
1369
+ }
1370
+ const matched = entries.find((e) => e.path === pathname);
1371
+ const chunkName = pathname.split("/").pop() ?? "";
1372
+ const chunkOwner = chunkMap.get(chunkName);
1373
+ if (!matched && !chunkOwner) return next(req, ctx);
1374
+ if (!matched && chunkOwner) {
1375
+ const ownerCache = cacheStore.get(chunkOwner);
1376
+ const chunk = ownerCache?.chunks?.get(chunkName);
1377
+ if (chunk) {
1378
+ if (req.headers.get("if-none-match") === chunk.etag) {
1379
+ return new Response(null, { status: 304 });
1380
+ }
1381
+ return new Response(chunk.code, {
1382
+ headers: {
1383
+ "Content-Type": "text/javascript; charset=utf-8",
1384
+ ETag: chunk.etag,
1385
+ "Cache-Control": "no-cache"
1386
+ }
1387
+ });
1388
+ }
1389
+ return next(req, ctx);
1390
+ }
1391
+ const { config } = matched;
1392
+ try {
1393
+ if (cache === "memory") {
1394
+ const cached = cacheStore.get(pathname);
1395
+ if (cached) {
1396
+ const valid = await isCacheValid2(cached);
1397
+ if (valid) {
1398
+ if (req.headers.get("if-none-match") === cached.etag) {
1399
+ return new Response(null, { status: 304 });
1400
+ }
1401
+ return new Response(cached.code, {
1402
+ headers: {
1403
+ "Content-Type": "text/javascript; charset=utf-8",
1404
+ ETag: cached.etag,
1405
+ "Cache-Control": "no-cache"
1406
+ }
1407
+ });
1408
+ }
1409
+ for (const [cn] of cached.chunks ?? []) chunkMap.delete(cn);
1410
+ cacheStore.delete(pathname);
1411
+ }
1412
+ }
1413
+ const { code, etag, chunks } = await compile(config);
1414
+ const depEntry = config.clientRouter?.routes ?? (config.clientRouter?.pages ? Object.values(config.clientRouter.pages)[0] : void 0) ?? config.entry;
1415
+ const deps = await collectDeps(resolve2(depEntry));
1416
+ if (cache === "memory") {
1417
+ cacheStore.set(pathname, { code, etag, files: deps, chunks });
1418
+ if (chunks) {
1419
+ for (const [cn] of chunks) chunkMap.set(cn, pathname);
1420
+ }
1421
+ }
1422
+ return new Response(code, {
1423
+ headers: {
1424
+ "Content-Type": "text/javascript; charset=utf-8",
1425
+ ETag: etag,
1426
+ "Cache-Control": "no-cache"
1427
+ }
1428
+ });
1429
+ } catch (err) {
1430
+ const message = err instanceof Error ? err.message : String(err);
1431
+ const html = errorTemplate(message);
1432
+ return new Response(html, {
1433
+ status: 500,
1434
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1435
+ });
1436
+ }
1437
+ };
1438
+ }
1439
+ function hashCode(s) {
1440
+ let hash = 0;
1441
+ for (let i = 0; i < s.length; i++) {
1442
+ const ch = s.charCodeAt(i);
1443
+ hash = (hash << 5) - hash + ch;
1444
+ hash |= 0;
1445
+ }
1446
+ return Math.abs(hash).toString(36);
1447
+ }
1448
+
1449
+ // src/middleware/tailwind-dev.ts
1450
+ import { resolve as resolve3 } from "node:path";
1451
+ import { stat as stat2, readFile as readFile2, glob } from "node:fs/promises";
1452
+ function escapeHtml2(s) {
1453
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1454
+ }
1455
+ function defaultErrorTemplate2(errors) {
1456
+ return `<!DOCTYPE html>
1457
+ <html>
1458
+ <head>
1459
+ <meta charset="utf-8">
1460
+ <title>Tailwind Build Error</title>
1461
+ <style>
1462
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1463
+ body { font-family: system-ui; background: #1a1a2e; color: #e0e0e0; padding: 2rem; }
1464
+ .overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
1465
+ .card { background: #16213e; border: 1px solid #06b6d4; border-radius: 12px; padding: 2rem; max-width: 800px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
1466
+ h1 { color: #06b6d4; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
1467
+ pre { background: #0f0f23; padding: 1.5rem; border-radius: 8px; overflow: auto; font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
1468
+ </style>
1469
+ </head>
1470
+ <body>
1471
+ <div class="overlay">
1472
+ <div class="card">
1473
+ <h1>\u{1F3A8} Tailwind CSS Build Error</h1>
1474
+ <pre>${escapeHtml2(errors)}</pre>
1475
+ </div>
1476
+ </div>
1477
+ </body>
1478
+ </html>`;
1479
+ }
1480
+ var TW_CANDIDATE_RE = /[a-z][\w-]*(?::[a-z][\w-]*)*(?:-\[[^\]]*\])*(?:\/[0-9]+)?/gi;
1481
+ var CLASS_CONTEXT_RE = /(?:class(?:Name)?\s*[=:]\s*["'`]([^"'`]+)["'`]|["'`]([^"'`\n]{2,})["'`])/g;
1482
+ async function extractCandidates(patterns, root) {
1483
+ const seen = /* @__PURE__ */ new Set();
1484
+ for (const pattern of patterns) {
1485
+ const resolved = resolve3(root, pattern);
1486
+ try {
1487
+ for await (const file of glob(resolved)) {
1488
+ try {
1489
+ const content = await readFile2(file, "utf-8");
1490
+ for (const m of content.matchAll(CLASS_CONTEXT_RE)) {
1491
+ const str = m[1] ?? m[2];
1492
+ if (!str) continue;
1493
+ for (const c of str.matchAll(TW_CANDIDATE_RE)) {
1494
+ seen.add(c[0]);
1495
+ }
1496
+ }
1497
+ } catch {
1498
+ }
1499
+ }
1500
+ } catch {
1501
+ }
1502
+ }
1503
+ return [...seen];
1504
+ }
1505
+ async function collectFileMtimes(files) {
1506
+ const map = /* @__PURE__ */ new Map();
1507
+ await Promise.all(
1508
+ files.map(async (f) => {
1509
+ try {
1510
+ const s = await stat2(f);
1511
+ map.set(f, s.mtimeMs);
1512
+ } catch {
1513
+ }
1514
+ })
1515
+ );
1516
+ return map;
1517
+ }
1518
+ function isCacheValid(cached) {
1519
+ return Promise.all(
1520
+ [...cached.files].map(async ([file, mtime]) => {
1521
+ try {
1522
+ const s = await stat2(file);
1523
+ return s.mtimeMs === mtime;
1524
+ } catch {
1525
+ return false;
1526
+ }
1527
+ })
1528
+ ).then((results) => results.every(Boolean));
1529
+ }
1530
+ function tailwindDev(opts) {
1531
+ const cacheMode = opts.cache ?? "memory";
1532
+ const errorTemplate = opts.errorTemplate ?? defaultErrorTemplate2;
1533
+ const entries = Object.entries(opts.entries).map(([p, e]) => {
1534
+ const path = p.startsWith("/") ? p : "/" + p;
1535
+ const config = typeof e === "string" ? { entry: e } : e;
1536
+ return { path, config };
1537
+ });
1538
+ const cacheStore = /* @__PURE__ */ new Map();
1539
+ let compileFn = null;
1540
+ let loadError = null;
1541
+ async function getCompile() {
1542
+ if (compileFn) return compileFn;
1543
+ try {
1544
+ const mod = await import("@tailwindcss/node");
1545
+ compileFn = mod.compile;
1546
+ return compileFn;
1547
+ } catch (err) {
1548
+ loadError = `@tailwindcss/node is not installed. Run: npm install -D tailwindcss @tailwindcss/node
1549
+
1550
+ ${String(err)}`;
1551
+ throw new Error(loadError, { cause: err });
1552
+ }
1553
+ }
1554
+ async function compileCss(entry) {
1555
+ const compile = await getCompile();
1556
+ const entryAbs = resolve3(entry.entry);
1557
+ const cssSource = await readFile2(entryAbs, "utf-8");
1558
+ const baseDir = resolve3(entry.entry, "..");
1559
+ const deps = [entryAbs];
1560
+ const result = await compile(cssSource, {
1561
+ base: baseDir,
1562
+ onDependency: (p) => {
1563
+ if (!deps.includes(p)) deps.push(p);
1564
+ }
1565
+ });
1566
+ const contentPatterns = entry.content ?? [];
1567
+ for (const src of result.sources) {
1568
+ if (!src.negated) {
1569
+ contentPatterns.push(resolve3(src.base, src.pattern));
1570
+ }
1571
+ }
1572
+ let candidates = [];
1573
+ if (contentPatterns.length > 0) {
1574
+ candidates = await extractCandidates(contentPatterns, baseDir);
1575
+ }
1576
+ const css = result.build(candidates);
1577
+ for (const pattern of contentPatterns) {
1578
+ try {
1579
+ for await (const file of glob(resolve3(baseDir, pattern))) {
1580
+ if (!deps.includes(file)) deps.push(file);
1581
+ }
1582
+ } catch {
1583
+ }
1584
+ }
1585
+ const etag = `"tw-${hashCode2(css)}"`;
1586
+ return { css, etag, files: deps };
1587
+ }
1588
+ return async (req, ctx, next) => {
1589
+ const url = new URL(req.url);
1590
+ const pathname = url.pathname;
1591
+ const matched = entries.find((e) => e.path === pathname);
1592
+ if (!matched) return next(req, ctx);
1593
+ const { config } = matched;
1594
+ try {
1595
+ if (cacheMode === "memory") {
1596
+ const cached = cacheStore.get(pathname);
1597
+ if (cached) {
1598
+ const valid = await isCacheValid(cached);
1599
+ if (valid) {
1600
+ if (req.headers.get("if-none-match") === cached.etag) {
1601
+ return new Response(null, { status: 304 });
1602
+ }
1603
+ return new Response(cached.css, {
1604
+ headers: {
1605
+ "Content-Type": "text/css; charset=utf-8",
1606
+ ETag: cached.etag,
1607
+ "Cache-Control": "no-cache"
1608
+ }
1609
+ });
1610
+ }
1611
+ cacheStore.delete(pathname);
1612
+ }
1613
+ }
1614
+ const { css, etag, files } = await compileCss(config);
1615
+ if (cacheMode === "memory") {
1616
+ const mtimes = await collectFileMtimes(files);
1617
+ cacheStore.set(pathname, { css, etag, files: mtimes });
1618
+ }
1619
+ return new Response(css, {
1620
+ headers: {
1621
+ "Content-Type": "text/css; charset=utf-8",
1622
+ ETag: etag,
1623
+ "Cache-Control": "no-cache"
1624
+ }
1625
+ });
1626
+ } catch (err) {
1627
+ const message = err instanceof Error ? err.message : String(err);
1628
+ const html = errorTemplate(message);
1629
+ return new Response(html, {
1630
+ status: 500,
1631
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1632
+ });
1633
+ }
1634
+ };
1635
+ }
1636
+ function hashCode2(s) {
1637
+ let hash = 0;
1638
+ for (let i = 0; i < s.length; i++) {
1639
+ const ch = s.charCodeAt(i);
1640
+ hash = (hash << 5) - hash + ch;
1641
+ hash |= 0;
1642
+ }
1643
+ return Math.abs(hash).toString(36);
1644
+ }
1645
+
1102
1646
  // src/middleware/rate-limit.ts
1103
1647
  function defaultKey(_req, _ctx) {
1104
1648
  const forwarded = _req.headers.get("x-forwarded-for");
@@ -1111,7 +1655,7 @@ function defaultKey(_req, _ctx) {
1111
1655
  }
1112
1656
  function rateLimit(options) {
1113
1657
  const max = options?.max ?? 100;
1114
- const window2 = options?.window ?? 6e4;
1658
+ const window = options?.window ?? 6e4;
1115
1659
  const getKey = options?.key ?? defaultKey;
1116
1660
  const message = options?.message ?? "Too Many Requests";
1117
1661
  const storeType = options?.store ?? "memory";
@@ -1138,7 +1682,7 @@ function rateLimit(options) {
1138
1682
  }
1139
1683
  }
1140
1684
  },
1141
- Math.min(window2, 3e4)
1685
+ Math.min(window, 3e4)
1142
1686
  ) : null;
1143
1687
  if (interval?.unref) interval.unref();
1144
1688
  async function checkAndIncrement(key) {
@@ -1147,16 +1691,16 @@ function rateLimit(options) {
1147
1691
  const redisKey = `${keyPrefix}${key}`;
1148
1692
  const count = await redis2.incr(redisKey);
1149
1693
  if (count === 1) {
1150
- await redis2.pexpire(redisKey, window2);
1694
+ await redis2.pexpire(redisKey, window);
1151
1695
  }
1152
1696
  const pttl = await redis2.pttl(redisKey);
1153
- const reset = pttl > 0 ? now + pttl : now + window2;
1697
+ const reset = pttl > 0 ? now + pttl : now + window;
1154
1698
  return { count, reset };
1155
1699
  }
1156
1700
  const entry = hits.get(key);
1157
1701
  if (!entry || entry.reset < now) {
1158
- hits.set(key, { count: 1, reset: now + window2 });
1159
- return { count: 1, reset: now + window2 };
1702
+ hits.set(key, { count: 1, reset: now + window });
1703
+ return { count: 1, reset: now + window };
1160
1704
  }
1161
1705
  entry.count++;
1162
1706
  return { count: entry.count, reset: entry.reset };
@@ -1879,339 +2423,380 @@ function queue(opts) {
1879
2423
  }
1880
2424
 
1881
2425
  // src/react/index.ts
1882
- import { Fragment as Fragment2 } from "react";
1883
-
1884
- // src/react/render.ts
1885
- import {
1886
- createElement,
1887
- Fragment
1888
- } from "react";
1889
- import { renderToString, renderToReadableStream } from "react-dom/server";
1890
-
1891
- // src/react/context.ts
1892
- import { createContext } from "react";
1893
- var CTX_KEY = /* @__PURE__ */ Symbol.for("weifuwu.react.ServerDataContext");
1894
- function getServerDataContext() {
1895
- const globalStore = globalThis;
1896
- if (globalStore[CTX_KEY]) return globalStore[CTX_KEY];
1897
- const ctx = createContext({});
1898
- ctx.displayName = "ServerData";
1899
- globalStore[CTX_KEY] = ctx;
1900
- return ctx;
1901
- }
1902
- var ServerDataContext = getServerDataContext();
2426
+ import { createElement as createElement2 } from "react";
2427
+ import { renderToReadableStream } from "react-dom/server";
1903
2428
 
1904
- // src/react/render.ts
1905
- var encoder = new TextEncoder();
1906
- var decoder = new TextDecoder();
1907
- function escapeAttr(s) {
1908
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
1909
- }
1910
- function buildHeadTags(head) {
1911
- if (!head) return "";
1912
- const tags = [];
1913
- if (head.title) tags.push(`<title>${escapeAttr(head.title)}</title>`);
1914
- if (head.meta) {
1915
- for (const [name, content] of Object.entries(head.meta)) {
1916
- tags.push(`<meta name="${escapeAttr(name)}" content="${escapeAttr(content)}">`);
1917
- }
1918
- }
1919
- if (head.links) {
1920
- for (const link of head.links) {
1921
- const attrs = Object.entries(link).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ");
1922
- tags.push(`<link ${attrs}>`);
1923
- }
1924
- }
1925
- if (head.scripts) {
1926
- for (const script of head.scripts) {
1927
- const entries = Object.entries(script).filter(([, v]) => v !== void 0);
1928
- const attrs = entries.map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ");
1929
- tags.push(`<script ${attrs}></script>`);
2429
+ // src/react/compile.ts
2430
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3 } from "node:fs/promises";
2431
+ import { resolve as resolve4, join as join2, dirname as dirname2 } from "node:path";
2432
+ import { createHash } from "node:crypto";
2433
+ import { existsSync } from "node:fs";
2434
+ var EXTERNAL_PKGS = [
2435
+ "react",
2436
+ "react/jsx-runtime",
2437
+ "react-dom",
2438
+ "react-dom/server",
2439
+ "react-dom/client",
2440
+ "weifuwu",
2441
+ "weifuwu/react"
2442
+ ];
2443
+ var FRAMEWORK_IMPORTS = [
2444
+ "react",
2445
+ "react/jsx-runtime",
2446
+ "react-dom",
2447
+ "react-dom/server",
2448
+ "react-dom/client",
2449
+ "weifuwu",
2450
+ "weifuwu/react"
2451
+ ];
2452
+ var memCache = /* @__PURE__ */ new Map();
2453
+ function buildResolveMap() {
2454
+ const map = {};
2455
+ for (const spec of FRAMEWORK_IMPORTS) {
2456
+ try {
2457
+ map[spec] = import.meta.resolve(spec);
2458
+ } catch {
1930
2459
  }
1931
2460
  }
1932
- return tags.join("");
2461
+ return map;
1933
2462
  }
1934
- function injectHeadIntoHtml(html, headTags) {
1935
- if (!headTags) return html;
1936
- let result = html;
1937
- if (headTags.includes("<title>")) {
1938
- const titleMatch = headTags.match(/<title>[^<]*<\/title>/);
1939
- if (titleMatch) {
1940
- result = result.replace(/<title>[^<]*<\/title>/, titleMatch[0]);
1941
- headTags = headTags.replace(titleMatch[0], "");
1942
- }
1943
- }
1944
- if (headTags) {
1945
- result = result.replace("</head>", headTags + "</head>");
2463
+ var _resolveMap = null;
2464
+ function resolveMap() {
2465
+ if (!_resolveMap) _resolveMap = buildResolveMap();
2466
+ return _resolveMap;
2467
+ }
2468
+ function rewriteImports(code) {
2469
+ const map = resolveMap();
2470
+ let result = code;
2471
+ for (const [spec, url] of Object.entries(map)) {
2472
+ const quoted = JSON.stringify(spec);
2473
+ const escaped = quoted.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2474
+ result = result.replace(new RegExp(`(from\\s+)${escaped}`, "g"), `$1${JSON.stringify(url)}`).replace(new RegExp(`(import\\s+)${escaped}(\\s*;)`, "g"), `$1${JSON.stringify(url)}$2`).replace(new RegExp(`(export\\s+.*?from\\s+)${escaped}`, "g"), `$1${JSON.stringify(url)}`);
1946
2475
  }
1947
2476
  return result;
1948
2477
  }
1949
- function injectHeadIntoStream(sourceStream, headTags) {
1950
- if (!headTags) return sourceStream;
1951
- let titleTag = "";
1952
- let rest = headTags;
1953
- const titleMatch = headTags.match(/<title>[^<]*<\/title>/);
1954
- if (titleMatch) {
1955
- titleTag = titleMatch[0];
1956
- rest = headTags.replace(titleMatch[0], "");
1957
- }
1958
- let titleInjected = !titleTag;
1959
- let headInjected = !rest;
1960
- return new ReadableStream({
1961
- async start(controller) {
1962
- const reader = sourceStream.getReader();
1963
- let buffer = "";
1964
- try {
1965
- while (true) {
1966
- const { done, value } = await reader.read();
1967
- if (done) break;
1968
- buffer += decoder.decode(value, { stream: true });
1969
- if (!titleInjected) {
1970
- const idx = buffer.indexOf("<title>");
1971
- const endIdx = buffer.indexOf("</title>", idx);
1972
- if (idx !== -1 && endIdx !== -1) {
1973
- controller.enqueue(
1974
- encoder.encode(buffer.slice(0, idx) + titleTag + buffer.slice(endIdx + 8))
1975
- );
1976
- buffer = "";
1977
- titleInjected = true;
1978
- continue;
1979
- }
1980
- }
1981
- if (!headInjected) {
1982
- const idx = buffer.indexOf("</head>");
1983
- if (idx !== -1) {
1984
- controller.enqueue(
1985
- encoder.encode(buffer.slice(0, idx) + rest + buffer.slice(idx))
1986
- );
1987
- buffer = "";
1988
- headInjected = true;
1989
- continue;
1990
- }
1991
- }
1992
- if (titleInjected && headInjected && buffer) {
1993
- controller.enqueue(encoder.encode(buffer));
1994
- buffer = "";
1995
- }
1996
- }
1997
- if (buffer) controller.enqueue(encoder.encode(buffer));
1998
- controller.close();
1999
- } catch (err) {
2000
- controller.error(err);
2001
- }
2002
- }
2003
- });
2478
+ var _cacheDir = null;
2479
+ function setReactCacheDir(dir) {
2480
+ _cacheDir = dir;
2004
2481
  }
2005
- function wrapElement(element, layouts, data) {
2006
- let wrapped = element;
2007
- for (let i = layouts.length - 1; i >= 0; i--) {
2008
- wrapped = createElement(layouts[i], null, wrapped);
2009
- }
2010
- const json = JSON.stringify(data).replace(/</g, "\\u003c");
2011
- const dataScript = createElement("script", {
2012
- id: "__WEIFUWU_DATA__",
2013
- type: "application/json",
2014
- dangerouslySetInnerHTML: { __html: json }
2015
- });
2016
- return createElement(
2017
- ServerDataContext.Provider,
2018
- { value: data },
2019
- createElement(Fragment, null, wrapped, dataScript)
2020
- );
2482
+ function getCacheDir() {
2483
+ if (_cacheDir) return _cacheDir;
2484
+ _cacheDir = join2(process.cwd(), "node_modules", ".weifuwu", "react");
2485
+ return _cacheDir;
2021
2486
  }
2022
- function render(element, layouts, options = {}) {
2023
- const data = options.data ?? {};
2024
- const wrapped = wrapElement(element, layouts, data);
2025
- let html = renderToString(wrapped);
2026
- const headTags = buildHeadTags(options.head);
2027
- html = injectHeadIntoHtml(html, headTags);
2028
- return new Response("<!DOCTYPE html>\n" + html, {
2029
- status: options.status ?? 200,
2030
- headers: {
2031
- "content-type": "text/html; charset=utf-8",
2032
- ...options.headers
2487
+ async function loadTsxModule(entryPath) {
2488
+ const abs = resolve4(entryPath);
2489
+ let source;
2490
+ try {
2491
+ source = await readFile3(abs, "utf-8");
2492
+ } catch {
2493
+ throw new Error(`Cannot read file: ${entryPath}`);
2494
+ }
2495
+ const sourceHash = createHash("sha256").update(source).digest("hex").slice(0, 16);
2496
+ const memCached = memCache.get(abs);
2497
+ if (memCached && memCached.sourceHash === sourceHash) {
2498
+ return memCached.mod;
2499
+ }
2500
+ const tmpDir = getCacheDir();
2501
+ const tmpFile = join2(tmpDir, `${sourceHash}.mjs`);
2502
+ if (existsSync(tmpFile)) {
2503
+ try {
2504
+ const mod2 = await import(tmpFile + "?" + sourceHash);
2505
+ memCache.set(abs, { sourceHash, mod: mod2 });
2506
+ return mod2;
2507
+ } catch {
2033
2508
  }
2509
+ }
2510
+ const esbuild = await import("esbuild");
2511
+ const result = await esbuild.build({
2512
+ entryPoints: [abs],
2513
+ bundle: true,
2514
+ write: false,
2515
+ format: "esm",
2516
+ platform: "node",
2517
+ jsx: "automatic",
2518
+ external: EXTERNAL_PKGS,
2519
+ logLevel: "silent"
2034
2520
  });
2521
+ let code = result.outputFiles[0]?.text ?? "";
2522
+ if (!code) throw new Error(`esbuild produced empty output for ${entryPath}`);
2523
+ code = rewriteImports(code);
2524
+ await mkdir2(dirname2(tmpFile), { recursive: true });
2525
+ await writeFile2(tmpFile, code);
2526
+ const mod = await import(tmpFile + "?" + sourceHash);
2527
+ memCache.set(abs, { sourceHash, mod });
2528
+ return mod;
2035
2529
  }
2036
- async function renderStream(element, layouts, options = {}) {
2037
- const data = options.data ?? {};
2038
- const wrapped = wrapElement(element, layouts, data);
2039
- const headTags = buildHeadTags(options.head);
2040
- let stream = await renderToReadableStream(wrapped);
2041
- stream = injectHeadIntoStream(stream, headTags);
2042
- const doctype = encoder.encode("<!DOCTYPE html>\n");
2043
- const combined = new ReadableStream({
2044
- async start(controller) {
2045
- controller.enqueue(doctype);
2046
- try {
2047
- const reader = stream.getReader();
2048
- while (true) {
2049
- const { done, value } = await reader.read();
2050
- if (done) {
2051
- controller.close();
2052
- break;
2053
- }
2054
- controller.enqueue(value);
2055
- }
2056
- } catch (err) {
2057
- controller.error(err);
2058
- }
2059
- }
2060
- });
2061
- return new Response(combined, {
2062
- status: options.status ?? 200,
2063
- headers: {
2064
- "content-type": "text/html; charset=utf-8",
2065
- "transfer-encoding": "chunked",
2066
- ...options.headers
2067
- }
2068
- });
2530
+ async function loadTsxComponent(entryPath) {
2531
+ const mod = await loadTsxModule(entryPath);
2532
+ if (mod.default && typeof mod.default === "function") return mod.default;
2533
+ for (const [, val] of Object.entries(mod)) {
2534
+ if (typeof val === "function") return val;
2535
+ }
2536
+ throw new Error(`No component export found in ${entryPath}. Export a default or named component function.`);
2069
2537
  }
2070
2538
 
2071
- // src/react/hooks.ts
2072
- import { useContext } from "react";
2073
- function useServerData() {
2074
- return useContext(ServerDataContext);
2539
+ // src/react/context.ts
2540
+ import { createContext } from "react";
2541
+ var CTX_KEY = /* @__PURE__ */ Symbol.for("weifuwu.react.ServerDataContext");
2542
+ function getServerDataContext() {
2543
+ const globalStore = globalThis;
2544
+ if (globalStore[CTX_KEY]) return globalStore[CTX_KEY];
2545
+ const ctx = createContext({});
2546
+ ctx.displayName = "ServerData";
2547
+ globalStore[CTX_KEY] = ctx;
2548
+ return ctx;
2075
2549
  }
2076
-
2077
- // src/react/navigation.ts
2078
- import {
2079
- createElement as createElement2,
2080
- createContext as createContext2,
2081
- useContext as useContext2
2082
- } from "react";
2550
+ var ServerDataContext = getServerDataContext();
2083
2551
 
2084
2552
  // src/react/error-boundary.ts
2085
- import { Component } from "react";
2553
+ import { createElement, Component } from "react";
2086
2554
  var ErrorBoundary = class extends Component {
2087
- state = { error: null };
2555
+ state = { hasError: false, error: null };
2088
2556
  static getDerivedStateFromError(error) {
2089
- return { error };
2557
+ return { hasError: true, error };
2090
2558
  }
2091
- componentDidCatch(error, info) {
2092
- this.props.onError?.(error, info);
2559
+ componentDidCatch(error) {
2560
+ this.props.onError?.(error);
2093
2561
  }
2094
2562
  render() {
2095
- if (this.state.error) {
2096
- return this.props.fallback;
2563
+ if (this.state.hasError) {
2564
+ if (this.props.fallback) return this.props.fallback;
2565
+ return createElement(
2566
+ "div",
2567
+ { style: { padding: "2rem", textAlign: "center" } },
2568
+ createElement("h1", null, "Something went wrong"),
2569
+ createElement("p", null, this.state.error?.message ?? "Unknown error")
2570
+ );
2097
2571
  }
2098
2572
  return this.props.children;
2099
2573
  }
2100
2574
  };
2101
2575
 
2102
- // src/react/navigation.ts
2103
- var RouterContext = createContext2(null);
2104
- RouterContext.displayName = "ClientRouter";
2105
- function useParams() {
2106
- return useContext2(RouterContext)?.params ?? {};
2576
+ // src/react/hooks.ts
2577
+ import { useContext } from "react";
2578
+ function useServerData() {
2579
+ return useContext(ServerDataContext);
2107
2580
  }
2108
- function useNavigate() {
2109
- const ctx = useContext2(RouterContext);
2110
- if (!ctx) throw new Error("useNavigate() must be used within a client router");
2111
- return ctx.navigate;
2581
+
2582
+ // src/react/index.ts
2583
+ function HtmlShell({ children, importMap, stylesheets, data }) {
2584
+ const headChildren = [
2585
+ createElement2("meta", { charSet: "utf-8", key: "charset" }),
2586
+ createElement2("meta", { name: "viewport", content: "width=device-width, initial-scale=1", key: "viewport" })
2587
+ ];
2588
+ if (stylesheets) {
2589
+ for (const href of stylesheets) {
2590
+ headChildren.push(
2591
+ createElement2("link", { rel: "stylesheet", href, key: `css-${href}` })
2592
+ );
2593
+ }
2594
+ }
2595
+ if (importMap) {
2596
+ headChildren.push(
2597
+ createElement2("script", {
2598
+ type: "importmap",
2599
+ key: "importmap",
2600
+ dangerouslySetInnerHTML: { __html: JSON.stringify(importMap) }
2601
+ })
2602
+ );
2603
+ }
2604
+ const bodyChildren = [
2605
+ createElement2("div", { id: "root", key: "root" }, children)
2606
+ ];
2607
+ if (data && Object.keys(data).length > 0) {
2608
+ bodyChildren.push(
2609
+ createElement2("script", {
2610
+ id: "__WEIFUWU_DATA__",
2611
+ type: "application/json",
2612
+ key: "weifuwu-data",
2613
+ dangerouslySetInnerHTML: { __html: JSON.stringify(data).replace(/</g, "\\u003c") }
2614
+ })
2615
+ );
2616
+ }
2617
+ return createElement2(
2618
+ "html",
2619
+ { lang: "en" },
2620
+ createElement2("head", null, ...headChildren),
2621
+ createElement2("body", null, ...bodyChildren)
2622
+ );
2112
2623
  }
2113
- function useNavigation() {
2114
- const ctx = useContext2(RouterContext);
2115
- return { state: ctx?.state ?? "idle" };
2624
+ async function renderComponent(Component2, data, layout, renderOpts) {
2625
+ let element = createElement2(Component2, renderOpts.props ?? {});
2626
+ if (layout) {
2627
+ element = createElement2(layout, { children: element });
2628
+ }
2629
+ element = createElement2(ServerDataContext.Provider, { value: data }, element);
2630
+ const page = createElement2(HtmlShell, {
2631
+ children: element,
2632
+ importMap: renderOpts.importMap,
2633
+ stylesheets: renderOpts.stylesheets,
2634
+ data: Object.keys(data).length > 0 ? data : void 0
2635
+ });
2636
+ const rstream = await renderToReadableStream(page, {
2637
+ bootstrapScripts: renderOpts.bootstrapScripts,
2638
+ bootstrapModules: renderOpts.bootstrapModules
2639
+ });
2640
+ if (renderOpts.stream === false) {
2641
+ await rstream.allReady;
2642
+ }
2643
+ return new Response(rstream, {
2644
+ status: renderOpts.status ?? 200,
2645
+ headers: { "content-type": "text/html; charset=utf-8", ...renderOpts.headers }
2646
+ });
2116
2647
  }
2117
- function Link({ href, children, ...props }) {
2118
- const router = useContext2(RouterContext);
2119
- const isExternal = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:");
2120
- if (!router || isExternal || props.target !== void 0) {
2121
- return createElement2("a", { href, ...props }, children);
2122
- }
2123
- const handleClick = (e) => {
2124
- if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
2125
- if (e.button !== 0) return;
2126
- e.preventDefault();
2127
- router.navigate(href);
2128
- };
2129
- return createElement2("a", { href, onClick: handleClick, ...props }, children);
2130
- }
2131
- function useRevalidate() {
2132
- const ctx = useContext2(RouterContext);
2133
- if (!ctx) throw new Error("useRevalidate() must be used within a client router");
2134
- return ctx.revalidate;
2135
- }
2136
- function Form({ method = "post", action, children, ...props }) {
2137
- const router = useContext2(RouterContext);
2138
- if (!router) {
2139
- return createElement2("form", { method, action, ...props }, children);
2140
- }
2141
- const handleSubmit = async (e) => {
2142
- e.preventDefault();
2143
- const form = e.target;
2144
- const formData = new FormData(form);
2145
- const url = action || window.location.pathname;
2146
- const fetchMethod = method === "get" ? "GET" : method.toUpperCase();
2648
+ function react(opts) {
2649
+ if (opts && "pages" in opts) {
2650
+ return (app) => createFullReactApp(app, opts);
2651
+ }
2652
+ if (opts?.cacheDir) setReactCacheDir(opts.cacheDir);
2653
+ let LayoutComponent = null;
2654
+ let layoutLoaded = false;
2655
+ let layoutLoadError = null;
2656
+ async function getLayout() {
2657
+ if (!opts?.layout) return null;
2658
+ if (layoutLoaded) {
2659
+ if (layoutLoadError) throw layoutLoadError;
2660
+ return LayoutComponent;
2661
+ }
2147
2662
  try {
2148
- const res = await fetch(url, {
2149
- method: fetchMethod,
2150
- body: fetchMethod === "GET" ? void 0 : formData,
2151
- redirect: "manual"
2152
- });
2153
- if (res.status >= 300 && res.status < 400) {
2154
- const loc = res.headers.get("Location");
2155
- if (loc) {
2156
- router.navigate(loc);
2157
- return;
2158
- }
2159
- }
2160
- await router.revalidate();
2663
+ LayoutComponent = await loadTsxComponent(opts.layout);
2664
+ layoutLoaded = true;
2665
+ return LayoutComponent;
2161
2666
  } catch (err) {
2162
- console.error("[weifuwu/react] Form submit failed:", err);
2667
+ layoutLoadError = err instanceof Error ? err : new Error(String(err));
2668
+ layoutLoaded = true;
2669
+ throw layoutLoadError;
2163
2670
  }
2671
+ }
2672
+ return async (_req, ctx, next) => {
2673
+ ctx.render = async (path, renderOpts) => {
2674
+ let data = renderOpts?.data ?? {};
2675
+ if (renderOpts?.loader) {
2676
+ const loaderData = await renderOpts.loader(ctx);
2677
+ data = { ...data, ...loaderData };
2678
+ }
2679
+ const Component2 = await loadTsxComponent(path);
2680
+ const layout = await getLayout();
2681
+ return renderComponent(Component2, data, layout, renderOpts ?? {});
2682
+ };
2683
+ return next(_req, ctx);
2164
2684
  };
2165
- return createElement2(
2166
- "form",
2167
- { method, action, ...props, onSubmit: handleSubmit },
2168
- children
2169
- );
2170
2685
  }
2171
-
2172
- // src/react/index.ts
2173
- var LAYOUTS_KEY = /* @__PURE__ */ Symbol.for("weifuwu:react:layouts");
2174
- var SETUP_KEY = /* @__PURE__ */ Symbol.for("weifuwu:react:setup");
2175
- function bag(ctx) {
2176
- return ctx;
2686
+ async function resolveLoader(modulePath) {
2687
+ const mod = await loadTsxModule(modulePath);
2688
+ return typeof mod.loader === "function" ? mod.loader : null;
2177
2689
  }
2178
- function isDataRequest(req) {
2690
+ async function callLoader(loader, ctx, data, rethrow = true) {
2691
+ if (!loader) return data;
2179
2692
  try {
2180
- return new URL(req.url).searchParams.has("_data");
2181
- } catch {
2182
- return false;
2693
+ return { ...data, ...await loader(ctx) };
2694
+ } catch (err) {
2695
+ if (rethrow) throw err;
2696
+ return data;
2183
2697
  }
2184
2698
  }
2185
- function react(opts = {}) {
2186
- const layout = opts.layout ?? Fragment2;
2187
- const mw = (req, ctx, next) => {
2188
- const b = bag(ctx);
2189
- const layouts = b[LAYOUTS_KEY] ?? [];
2190
- b[LAYOUTS_KEY] = layouts;
2191
- layouts.push(layout);
2192
- if (!b[SETUP_KEY]) {
2193
- b[SETUP_KEY] = true;
2194
- ctx.render = (element, renderOpts) => {
2195
- if (renderOpts?.data && isDataRequest(req)) {
2196
- return Promise.resolve(Response.json(renderOpts.data));
2197
- }
2198
- return Promise.resolve(render(element, layouts, renderOpts));
2199
- };
2200
- ctx.renderStream = (element, renderOpts) => {
2201
- if (renderOpts?.data && isDataRequest(req)) {
2202
- return Promise.resolve(Response.json(renderOpts.data));
2699
+ function createFullReactApp(app, opts) {
2700
+ const stylesheets = [...opts.stylesheets ?? []];
2701
+ if (opts.tailwind) {
2702
+ const twPath = opts.tailwind.path ?? "/assets/tailwind.css";
2703
+ const twEntry = opts.tailwind.entry ?? "./styles/input.css";
2704
+ app.use(tailwindDev({ entries: { [twPath]: { entry: twEntry } } }));
2705
+ if (!stylesheets.includes(twPath)) stylesheets.push(twPath);
2706
+ }
2707
+ app.use(react({ layout: opts.layout, cacheDir: opts.cacheDir }));
2708
+ const layoutLoaderP = resolveLoader(opts.layout);
2709
+ const notFoundLoaderP = opts.notFound ? resolveLoader(opts.notFound) : Promise.resolve(null);
2710
+ const clientCfg = typeof opts.client === "object" ? opts.client : {};
2711
+ const clientPath = clientCfg.path ?? "/assets/client.js";
2712
+ const bootstrapModules = [...opts.bootstrapModules ?? []];
2713
+ if (opts.client !== false && !bootstrapModules.includes(clientPath)) {
2714
+ bootstrapModules.push(clientPath);
2715
+ }
2716
+ const renderOpts = {
2717
+ stylesheets: stylesheets.length > 0 ? stylesheets : void 0,
2718
+ bootstrapModules: bootstrapModules.length > 0 ? bootstrapModules : void 0,
2719
+ stream: opts.stream
2720
+ };
2721
+ for (const [path, component] of Object.entries(opts.pages)) {
2722
+ app.get(path, async (_req, ctx) => {
2723
+ let data = {};
2724
+ data = await callLoader(await layoutLoaderP, ctx, data);
2725
+ const pageLoader = opts.loaders?.[path] ?? await resolveLoader(component);
2726
+ data = await callLoader(pageLoader, ctx, data);
2727
+ return ctx.render(component, { ...renderOpts, data });
2728
+ });
2729
+ }
2730
+ if (opts.notFound) {
2731
+ const notFoundPath = opts.notFound;
2732
+ app.onError(async (err, _req, ctx) => {
2733
+ const status = typeof err === "object" && err !== null && "status" in err ? err.status : 500;
2734
+ if (ctx.render) {
2735
+ let data = { error: String(err) };
2736
+ data = await callLoader(await layoutLoaderP, ctx, data, false);
2737
+ data = await callLoader(await notFoundLoaderP, ctx, data, false);
2738
+ return ctx.render(notFoundPath, { ...renderOpts, status, data });
2739
+ }
2740
+ return new Response("Internal Server Error", { status });
2741
+ });
2742
+ }
2743
+ if (opts.client !== false) {
2744
+ app.use(esbuildDev({
2745
+ entries: {
2746
+ [clientPath]: {
2747
+ clientRouter: {
2748
+ pages: opts.pages,
2749
+ layout: opts.layout,
2750
+ fallback: opts.notFound
2751
+ },
2752
+ bundle: true,
2753
+ splitting: clientCfg.splitting ?? true,
2754
+ minify: clientCfg.minify ?? false
2203
2755
  }
2204
- return renderStream(element, layouts, renderOpts);
2205
- };
2756
+ }
2757
+ }));
2758
+ }
2759
+ }
2760
+ function extractImportPath(fn) {
2761
+ const src = fn.toString();
2762
+ const m = src.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
2763
+ if (!m) throw new Error(`Cannot extract import path from: ${src}`);
2764
+ return m[1];
2765
+ }
2766
+ function reactRouter(app, routes, opts = {}) {
2767
+ let LayoutComponent = null;
2768
+ let layoutPromise = null;
2769
+ async function getLayout() {
2770
+ if (!opts.layout) return null;
2771
+ if (LayoutComponent) return LayoutComponent;
2772
+ if (!layoutPromise) {
2773
+ layoutPromise = loadTsxComponent(opts.layout).then((c) => {
2774
+ LayoutComponent = c;
2775
+ return c;
2776
+ });
2206
2777
  }
2207
- return next(req, ctx);
2208
- };
2209
- return mw;
2778
+ return layoutPromise;
2779
+ }
2780
+ for (const [path, importer] of Object.entries(routes)) {
2781
+ app.get(path, async (_req, ctx) => {
2782
+ const cmpPath = extractImportPath(importer);
2783
+ const Component2 = await loadTsxComponent(cmpPath);
2784
+ const layout = await getLayout();
2785
+ let data = {};
2786
+ const loader = opts.loaders?.[path];
2787
+ if (loader) {
2788
+ data = await loader(ctx);
2789
+ }
2790
+ return renderComponent(Component2, data, layout, opts);
2791
+ });
2792
+ }
2793
+ }
2794
+ function Link({ href, children, ...props }) {
2795
+ return createElement2("a", { href, ...props }, children);
2210
2796
  }
2211
2797
  export {
2212
2798
  DEFAULT_MAX_BODY,
2213
2799
  ErrorBoundary,
2214
- Form,
2215
2800
  HttpError,
2216
2801
  Link,
2217
2802
  MIGRATIONS_TABLE,
@@ -2222,6 +2807,7 @@ export {
2222
2807
  createHub,
2223
2808
  currentTrace,
2224
2809
  currentTraceId,
2810
+ esbuildDev,
2225
2811
  graphql,
2226
2812
  helmet,
2227
2813
  logger,
@@ -2229,16 +2815,14 @@ export {
2229
2815
  queue,
2230
2816
  rateLimit,
2231
2817
  react,
2818
+ reactRouter,
2232
2819
  redis,
2233
2820
  runWithTrace,
2234
2821
  serve,
2235
2822
  serveStatic,
2823
+ tailwindDev,
2236
2824
  trace,
2237
2825
  traceElapsed,
2238
2826
  upload,
2239
- useNavigate,
2240
- useNavigation,
2241
- useParams,
2242
- useRevalidate,
2243
2827
  useServerData
2244
2828
  };