weifuwu 0.19.2 → 0.19.5

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
@@ -376,13 +376,13 @@ var Router = class _Router {
376
376
  const mw = match.middlewares[index++];
377
377
  return mw(innerReq, ctx2, dispatch);
378
378
  }
379
- return await new Promise((resolve14) => {
379
+ return await new Promise((resolve15) => {
380
380
  try {
381
381
  upgradeSocket(router.wss, req, socket, head, match.handler, ctx2);
382
- resolve14(new Response(null, { status: 101 }));
382
+ resolve15(new Response(null, { status: 101 }));
383
383
  } catch {
384
384
  socket.destroy();
385
- resolve14(new Response("WebSocket upgrade failed", { status: 500 }));
385
+ resolve15(new Response("WebSocket upgrade failed", { status: 500 }));
386
386
  }
387
387
  });
388
388
  };
@@ -2845,7 +2845,7 @@ function createHub(opts) {
2845
2845
  }
2846
2846
  });
2847
2847
  }
2848
- function join8(key, ws) {
2848
+ function join9(key, ws) {
2849
2849
  if (!channels.has(key)) {
2850
2850
  channels.set(key, /* @__PURE__ */ new Set());
2851
2851
  redisSub?.subscribe(`${prefix}${key}`);
@@ -2886,7 +2886,7 @@ function createHub(opts) {
2886
2886
  await redisSub.quit();
2887
2887
  }
2888
2888
  }
2889
- return { join: join8, leave, broadcast, close };
2889
+ return { join: join9, leave, broadcast, close };
2890
2890
  }
2891
2891
 
2892
2892
  // queue/index.ts
@@ -4584,16 +4584,8 @@ import path from "node:path";
4584
4584
 
4585
4585
  // deploy/gateway.ts
4586
4586
  import WebSocket2, { WebSocketServer as WebSocketServer2 } from "ws";
4587
- function isBareDomain(host, domain) {
4588
- return host === domain || host === `www.${domain}`;
4589
- }
4590
4587
  function matchApp(config, getPort, host, pathname) {
4591
- for (const [name, ac] of Object.entries(config.apps)) {
4592
- if (ac.subdomain && host === `${ac.subdomain}.${config.domain}`) {
4593
- const port = getPort(name);
4594
- if (port) return { name, port };
4595
- }
4596
- }
4588
+ const domain = config.domain || "localhost";
4597
4589
  const pathApps = Object.entries(config.apps).filter(([, ac]) => ac.path).sort(([, a], [, b]) => (b.path?.length ?? 0) - (a.path?.length ?? 0));
4598
4590
  for (const [name, ac] of pathApps) {
4599
4591
  if (ac.path && pathname.startsWith(ac.path)) {
@@ -4601,7 +4593,16 @@ function matchApp(config, getPort, host, pathname) {
4601
4593
  if (port) return { name, port, stripPath: ac.path };
4602
4594
  }
4603
4595
  }
4604
- if (config.defaultApp && isBareDomain(host, config.domain)) {
4596
+ for (const [name, ac] of Object.entries(config.apps)) {
4597
+ if (ac.path) continue;
4598
+ const hostMatch = host === `${name}.${domain}` || host === name;
4599
+ const pathMatch = pathname === `/${name}` || pathname.startsWith(`/${name}/`);
4600
+ if (hostMatch || pathMatch) {
4601
+ const port = getPort(name);
4602
+ if (port) return { name, port };
4603
+ }
4604
+ }
4605
+ if (config.defaultApp) {
4605
4606
  const port = getPort(config.defaultApp);
4606
4607
  if (port) return { name: config.defaultApp, port };
4607
4608
  }
@@ -4696,14 +4697,14 @@ function forkApp(opts) {
4696
4697
  return { child, port: opts.port };
4697
4698
  }
4698
4699
  function stopProcess(mp, timeout = 1e4) {
4699
- return new Promise((resolve14) => {
4700
+ return new Promise((resolve15) => {
4700
4701
  const timer = setTimeout(() => {
4701
4702
  mp.child.kill("SIGKILL");
4702
- resolve14();
4703
+ resolve15();
4703
4704
  }, timeout);
4704
4705
  mp.child.on("exit", () => {
4705
4706
  clearTimeout(timer);
4706
- resolve14();
4707
+ resolve15();
4707
4708
  });
4708
4709
  mp.child.kill("SIGTERM");
4709
4710
  });
@@ -4814,83 +4815,36 @@ function createManager(config, apps, manager) {
4814
4815
  }
4815
4816
  });
4816
4817
  });
4817
- router.post("/reload", auth2, async () => {
4818
- try {
4819
- await manager.reloadConfig();
4820
- return Response.json({ success: true });
4821
- } catch (err) {
4822
- const msg = err instanceof Error ? err.message : String(err);
4823
- return Response.json({ error: msg }, { status: 400 });
4824
- }
4825
- });
4826
- router.post("/webhook", async (req) => {
4827
- const rawBody = await req.text();
4828
- if (config.webhookSecret) {
4829
- const sig = req.headers.get("x-hub-signature-256") ?? "";
4830
- const expected = `sha256=${crypto5.createHmac("sha256", config.webhookSecret).update(rawBody).digest("hex")}`;
4831
- const sigBuf = Buffer.from(sig);
4832
- const expectedBuf = Buffer.from(expected);
4833
- if (sigBuf.length !== expectedBuf.length || !crypto5.timingSafeEqual(sigBuf, expectedBuf)) {
4834
- return Response.json({ error: "invalid signature" }, { status: 401 });
4835
- }
4836
- }
4837
- let payload;
4838
- try {
4839
- payload = JSON.parse(rawBody);
4840
- } catch {
4841
- payload = {};
4842
- }
4843
- const repoUrl = payload?.repository?.clone_url ?? payload?.repository?.html_url ?? "";
4844
- if (!repoUrl) return Response.json({ deployed: [] });
4845
- const deployed = [];
4846
- for (const [name] of apps) {
4847
- const ac = apps.get(name)?.config;
4848
- if (!ac) continue;
4849
- const repoNorm = ac.repo.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
4850
- const payloadNorm = repoUrl.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
4851
- if (payloadNorm.includes(repoNorm)) {
4852
- await manager.deployApp(name);
4853
- deployed.push(name);
4854
- }
4855
- }
4856
- return Response.json({ deployed });
4857
- });
4858
4818
  return router;
4859
4819
  }
4860
4820
 
4861
4821
  // deploy/config.ts
4862
4822
  function defineConfig(config) {
4863
- if (!config.domain) throw new Error("deploy: domain is required");
4864
- if (!config.apps || Object.keys(config.apps).length === 0) {
4865
- throw new Error("deploy: at least one app is required");
4866
- }
4867
- for (const [name, app] of Object.entries(config.apps)) {
4868
- if (!app.repo) throw new Error(`deploy: app "${name}" has no repo`);
4869
- if (!app.entry) throw new Error(`deploy: app "${name}" has no entry`);
4870
- if (!app.port) throw new Error(`deploy: app "${name}" has no port`);
4823
+ const domain = config.domain || "localhost";
4824
+ const port = config.port ?? 3e3;
4825
+ let nextPort = 3001;
4826
+ for (const [name, ac] of Object.entries(config.apps)) {
4827
+ ac.dir ??= name;
4828
+ ac.entry ??= "index.ts";
4829
+ ac.port ??= nextPort++;
4830
+ if (!ac.path && domain === "localhost") {
4831
+ ac.path = `/${name}`;
4832
+ }
4871
4833
  }
4872
- return {
4873
- port: config.port ?? 80,
4874
- appsDir: config.appsDir ?? "/opt/weifuwu/apps",
4875
- ...config
4876
- };
4834
+ return { ...config, domain, port };
4877
4835
  }
4878
4836
 
4879
4837
  // deploy/index.ts
4880
4838
  async function deploy(config) {
4881
- const appsDir = config.appsDir ?? "/opt/weifuwu/apps";
4882
4839
  const apps = /* @__PURE__ */ new Map();
4883
4840
  let httpServer;
4884
- if (!fs.existsSync(appsDir)) {
4885
- fs.mkdirSync(appsDir, { recursive: true });
4886
- }
4887
- async function forkAndCheck(cwd, entry, port, env, onLog, healthEndpoint) {
4841
+ async function forkAndCheck(name, cwd, entry, port, env, onLog, healthEndpoint) {
4888
4842
  try {
4889
4843
  const mp = forkApp({ cwd, entry, port, env, onLog });
4890
- onLog(`[deploy] forked pid ${mp.child.pid} on port ${mp.port}`);
4844
+ onLog(`[deploy] forked ${name} (pid ${mp.child.pid}) on port ${mp.port}`);
4891
4845
  const healthy = await healthCheck(port, healthEndpoint ?? "/");
4892
- if (healthy) onLog("[deploy] health check passed");
4893
- else onLog("[deploy] health check failed");
4846
+ if (healthy) onLog(`[deploy] health check passed`);
4847
+ else onLog(`[deploy] health check failed`);
4894
4848
  return mp;
4895
4849
  } catch (err) {
4896
4850
  const msg = err instanceof Error ? err.message : String(err);
@@ -4912,49 +4866,15 @@ async function deploy(config) {
4912
4866
  clearTimeout(old.restartTimer);
4913
4867
  old.restartTimer = void 0;
4914
4868
  }
4915
- const appDir = path.join(appsDir, name);
4869
+ const appDir = path.resolve(ac.dir);
4916
4870
  const logs = [];
4917
4871
  const log = (line) => {
4918
4872
  logs.push(line);
4919
4873
  if (logs.length > 1e3) logs.splice(0, logs.length - 1e3);
4920
4874
  };
4921
- try {
4922
- if (typeof ac.repo !== "string" || !/^https?:\/\/[^\s"']+\/[^\s"']+/.test(ac.repo) && !/^git@[^\s"']+:[^\s"']+/.test(ac.repo)) {
4923
- throw new Error(`Invalid repo URL: ${ac.repo}`);
4924
- }
4925
- if (ac.branch && typeof ac.branch === "string" && !/^[\w.\-/]+$/.test(ac.branch)) {
4926
- throw new Error(`Invalid branch name: ${ac.branch}`);
4927
- }
4928
- if (fs.existsSync(path.join(appDir, ".git"))) {
4929
- execSync("git pull", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
4930
- log("[deploy] git pull done");
4931
- } else {
4932
- if (fs.existsSync(appDir)) {
4933
- fs.rmSync(appDir, { recursive: true });
4934
- }
4935
- execSync(`git clone --depth=1 ${ac.repo} ${appDir}`, { stdio: "pipe", timeout: 12e4 });
4936
- log("[deploy] git clone done");
4937
- if (ac.branch) {
4938
- execSync(`git checkout ${ac.branch}`, { cwd: appDir, stdio: "pipe", timeout: 3e4 });
4939
- log(`[deploy] switched to branch ${ac.branch}`);
4940
- }
4941
- }
4942
- } catch (err) {
4943
- const msg = err instanceof Error ? err.message : String(err);
4944
- setAppRuntime(name, ac, logs, { status: "error", port: ac.port, error: msg });
4945
- log(`[deploy] git error: ${msg}`);
4946
- if (old?.process) {
4947
- apps.set(name, old);
4948
- }
4949
- return;
4950
- }
4951
- try {
4952
- execSync("npm install", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
4953
- log("[deploy] npm install done");
4954
- } catch (err) {
4955
- const msg = err instanceof Error ? err.message : String(err);
4956
- setAppRuntime(name, ac, logs, { status: "error", port: ac.port, error: msg });
4957
- log(`[deploy] npm install error: ${msg}`);
4875
+ if (!fs.existsSync(appDir)) {
4876
+ setAppRuntime(name, ac, logs, { status: "error", error: `dir not found: ${appDir}` });
4877
+ log(`[deploy] dir not found: ${appDir}`);
4958
4878
  if (old?.process) apps.set(name, old);
4959
4879
  return;
4960
4880
  }
@@ -4978,7 +4898,7 @@ async function deploy(config) {
4978
4898
  if (ac.ports && old?.process) {
4979
4899
  targetPort = old.currentPort === ac.ports[0] ? ac.ports[1] : ac.ports[0];
4980
4900
  }
4981
- const mp = await forkAndCheck(appDir, ac.entry, targetPort, ac.env, log, ac.healthEndpoint);
4901
+ const mp = await forkAndCheck(name, appDir, ac.entry, targetPort, ac.env, log, ac.healthEndpoint);
4982
4902
  if (!mp) {
4983
4903
  log("[deploy] new process failed to start, keeping old running");
4984
4904
  if (old?.process) apps.set(name, old);
@@ -4989,7 +4909,13 @@ async function deploy(config) {
4989
4909
  }
4990
4910
  const runtime = {
4991
4911
  config: ac,
4992
- status: { name, status: "running", port: targetPort, subdomain: ac.subdomain, path: ac.path, pid: mp.child.pid ?? void 0 },
4912
+ status: {
4913
+ name,
4914
+ status: "running",
4915
+ port: targetPort,
4916
+ path: ac.path,
4917
+ pid: mp.child.pid ?? void 0
4918
+ },
4993
4919
  logs,
4994
4920
  process: mp.child,
4995
4921
  currentPort: targetPort,
@@ -5023,7 +4949,7 @@ async function deploy(config) {
5023
4949
  status: { name, ...overrides },
5024
4950
  logs,
5025
4951
  process: null,
5026
- currentPort: overrides.port ?? ac.port,
4952
+ currentPort: overrides.port ?? ac.port ?? 0,
5027
4953
  startedAt: null,
5028
4954
  restartCount: 0,
5029
4955
  restartTimer: void 0
@@ -5052,14 +4978,10 @@ async function deploy(config) {
5052
4978
  }
5053
4979
  return gw.handler(req, ctx);
5054
4980
  };
5055
- if (config.ssl) {
5056
- ensureCertificates(config);
5057
- }
5058
4981
  httpServer = serve(fullHandler, {
5059
4982
  port: config.port,
5060
4983
  websocket: gw.wsHandler
5061
4984
  });
5062
- const portSuffix = config.port !== 80 ? `:${config.port}` : "";
5063
4985
  return {
5064
4986
  close: async () => {
5065
4987
  for (const [, app] of apps) {
@@ -5071,7 +4993,7 @@ async function deploy(config) {
5071
4993
  httpServer?.stop();
5072
4994
  },
5073
4995
  ready: httpServer.ready,
5074
- url: `http://${config.domain}${portSuffix}`,
4996
+ url: `http://localhost:${config.port}/`,
5075
4997
  apps: {
5076
4998
  list: () => Array.from(apps.values()).map((a) => a.status),
5077
4999
  status: (name) => apps.get(name)?.status,
@@ -5096,44 +5018,12 @@ async function deploy(config) {
5096
5018
  }
5097
5019
  };
5098
5020
  }
5099
- function ensureCertificates(config) {
5100
- const { domain, ssl } = config;
5101
- if (!ssl) return;
5102
- const certDir = "/etc/weifuwu/ssl";
5103
- const certPath = path.join(certDir, `${domain}.pem`);
5104
- const keyPath = path.join(certDir, `${domain}-key.pem`);
5105
- if (fs.existsSync(certPath) && fs.existsSync(keyPath)) return;
5106
- if (!fs.existsSync(certDir)) {
5107
- fs.mkdirSync(certDir, { recursive: true });
5108
- }
5109
- const acmeHome = path.join(certDir, ".acme.sh");
5110
- try {
5111
- execSync("which acme.sh", { stdio: "pipe" });
5112
- } catch {
5113
- execSync(
5114
- `curl -s https://get.acme.sh | sh -s email=${ssl.email}`,
5115
- { stdio: "pipe", timeout: 6e4 }
5116
- );
5117
- }
5118
- const subdomains = Object.values(config.apps).filter((a) => a.subdomain).map((a) => `${a.subdomain}.${domain}`).join(",");
5119
- const allDomains = subdomains ? `${domain},${subdomains}` : domain;
5120
- const acmeSh = path.join(acmeHome, "acme.sh");
5121
- const staging = ssl.staging ? " --staging" : "";
5122
- execSync(
5123
- `${acmeSh} --issue -d ${allDomains} --standalone${staging} --cert-file ${certPath} --key-file ${keyPath}`,
5124
- { stdio: "pipe", timeout: 12e4 }
5125
- );
5126
- execSync(
5127
- `${acmeSh} --install-cronjob`,
5128
- { stdio: "pipe", timeout: 3e4 }
5129
- );
5130
- }
5131
5021
 
5132
5022
  // opencode/client.ts
5133
5023
  import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
5134
5024
 
5135
5025
  // opencode/rest.ts
5136
- import { join as join5 } from "node:path";
5026
+ import { join as join7 } from "node:path";
5137
5027
 
5138
5028
  // ssr.ts
5139
5029
  import { createElement } from "react";
@@ -5248,6 +5138,9 @@ async function compileTsxDev(path2) {
5248
5138
  cache.set(absPath, mod);
5249
5139
  return mod;
5250
5140
  }
5141
+ function compile(path2) {
5142
+ return process.env.NODE_ENV !== "production" ? compileTsxDev(path2) : compileTsx(path2);
5143
+ }
5251
5144
  var vendorBundle = null;
5252
5145
  async function compileVendorBundle() {
5253
5146
  if (vendorBundle) return vendorBundle;
@@ -5322,13 +5215,14 @@ function buildHeadPayload(opts) {
5322
5215
  const { ctx, base, compiledTailwindCss, isDev: isDev2 } = opts;
5323
5216
  let result = "";
5324
5217
  if (isDev2) {
5218
+ const vUrl = `${base}/__wfw/v/bundle`;
5325
5219
  result += `<script type="importmap">{
5326
5220
  "imports": {
5327
- "react": "/__wfw/v/bundle",
5328
- "react-dom": "/__wfw/v/bundle",
5329
- "react-dom/client": "/__wfw/v/bundle",
5330
- "react/jsx-runtime": "/__wfw/v/bundle",
5331
- "weifuwu/react": "/__wfw/v/bundle"
5221
+ "react": "${vUrl}",
5222
+ "react-dom": "${vUrl}",
5223
+ "react-dom/client": "${vUrl}",
5224
+ "react/jsx-runtime": "${vUrl}",
5225
+ "weifuwu/react": "${vUrl}"
5332
5226
  }
5333
5227
  }</script>
5334
5228
  `;
@@ -5338,7 +5232,8 @@ function buildHeadPayload(opts) {
5338
5232
  `;
5339
5233
  }
5340
5234
  if (compiledTailwindCss) {
5341
- result += `<link rel="stylesheet" href="${base}/__wfw/style.css" />
5235
+ const cssUrl = ctx.tailwindCssUrl || "/__wfw/style.css";
5236
+ result += `<link rel="stylesheet" href="${cssUrl}" />
5342
5237
  `;
5343
5238
  }
5344
5239
  const localeData = ctx.parsed?.__localeData ?? globalThis.__LOCALE_DATA__;
@@ -5419,9 +5314,11 @@ function streamResponse(reactStream, opts) {
5419
5314
  const body = buildBodyScripts(opts);
5420
5315
  if (body) controller.enqueue(encoder2.encode("\n" + body));
5421
5316
  if (opts.isDev) {
5317
+ const wsUrl = `${opts.base}/__weifuwu/livereload`;
5318
+ const hbUrl = `${opts.base}/__wfw/h/`;
5422
5319
  controller.enqueue(encoder2.encode(
5423
5320
  `
5424
- <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');var t=0;ws.onmessage=function(e){try{var m=JSON.parse(e.data);if(m.type==='component'){if(m.entry&&m.entry!==window.__WFW_ENTRY)return;import('/__wfw/h/'+m.hash+'?'+Date.now()).catch(function(){location.reload()});if(m.css){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css}return}if(m.type==='css'){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css;return}}catch(_){}if(e.data==='reload'&&Date.now()-t>1e3){t=Date.now();location.reload()}};ws.onclose=function(){if(Date.now()-t>1e3){t=Date.now();setTimeout(function(){location.reload()},500)}}})()</script>`
5321
+ <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${wsUrl}');var t=0;ws.onmessage=function(e){try{var m=JSON.parse(e.data);if(m.type==='component'){if(m.entry&&m.entry!==window.__WFW_ENTRY)return;import('${hbUrl}'+m.hash+'?'+Date.now()).catch(function(){location.reload()});if(m.css){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css}return}if(m.type==='css'){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css;return}}catch(_){}if(e.data==='reload'&&Date.now()-t>1e3){t=Date.now();location.reload()}};ws.onclose=function(){if(Date.now()-t>1e3){t=Date.now();setTimeout(function(){location.reload()},500)}}})()</script>`
5425
5322
  ));
5426
5323
  }
5427
5324
  } catch {
@@ -5503,7 +5400,7 @@ async function buildClientBundle(entryPath, layoutPaths) {
5503
5400
  jsxImportSource: "react",
5504
5401
  banner: { js: "self.process={env:{}};" },
5505
5402
  loader: { ".node": "empty" },
5506
- external: isDev ? ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu/react"] : void 0,
5403
+ external: isDev ? ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"] : void 0,
5507
5404
  write: false,
5508
5405
  minify: !isDev
5509
5406
  });
@@ -5524,7 +5421,7 @@ function ssr(path2) {
5524
5421
  }) : new Response("", { status: 404 });
5525
5422
  });
5526
5423
  r.get("/", async (req, ctx) => {
5527
- const pageMod = await compileTsx(path2);
5424
+ const pageMod = await compile(path2);
5528
5425
  const Component = pageMod.default;
5529
5426
  if (!Component) return new Response("", { status: 500 });
5530
5427
  const layouts = ctx.layoutStack || [];
@@ -5597,7 +5494,12 @@ function ssr(path2) {
5597
5494
  return r;
5598
5495
  }
5599
5496
 
5497
+ // root-layout.ts
5498
+ import { existsSync as existsSync5 } from "node:fs";
5499
+ import { join as join5, resolve as resolve7 } from "node:path";
5500
+
5600
5501
  // tailwind.ts
5502
+ import { createHash as createHash4 } from "node:crypto";
5601
5503
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
5602
5504
  import { join as join3, relative, resolve as resolve5 } from "node:path";
5603
5505
  var extraSources = /* @__PURE__ */ new Set();
@@ -5608,16 +5510,21 @@ function tailwind(dir) {
5608
5510
  const r = new Router();
5609
5511
  r.use(async (req, ctx, next) => {
5610
5512
  if (!cssCache.has(cssPath)) {
5611
- cssCache.set(cssPath, await compileTailwindCss(cssPath, cssDir));
5513
+ await compileTailwindCss(cssPath, cssDir);
5612
5514
  }
5613
- ctx.compiledTailwindCss = cssCache.get(cssPath);
5515
+ const entry = cssCache.get(cssPath);
5516
+ ctx.compiledTailwindCss = entry.css;
5517
+ const base = (ctx.mountPath || "").replace(/\/$/, "");
5518
+ ctx.tailwindCssUrl = base ? `${base}/__wfw/style/${entry.hash}.css` : `/__wfw/style/${entry.hash}.css`;
5614
5519
  return next(req, ctx);
5615
5520
  });
5616
- r.get("/__wfw/style.css", async (req, ctx) => {
5521
+ r.get("/__wfw/style/:hash.css", async (req, ctx) => {
5617
5522
  if (!cssCache.has(cssPath)) {
5618
- cssCache.set(cssPath, await compileTailwindCss(cssPath, cssDir));
5523
+ await compileTailwindCss(cssPath, cssDir);
5619
5524
  }
5620
- return new Response(cssCache.get(cssPath) || "", {
5525
+ const entry = cssCache.get(cssPath);
5526
+ if (!entry) return new Response("", { status: 404 });
5527
+ return new Response(entry.css, {
5621
5528
  headers: { "content-type": "text/css; charset=utf-8" }
5622
5529
  });
5623
5530
  });
@@ -5640,7 +5547,8 @@ ${src}`;
5640
5547
  ${src}`;
5641
5548
  }
5642
5549
  const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
5643
- cssCache.set(cssPath, result.css);
5550
+ const hash = createHash4("md5").update(result.css).digest("hex").slice(0, 8);
5551
+ cssCache.set(cssPath, { css: result.css, hash });
5644
5552
  return result.css;
5645
5553
  } catch (err) {
5646
5554
  console.warn("Tailwind CSS processing failed:", err.message);
@@ -5648,20 +5556,144 @@ ${src}`;
5648
5556
  }
5649
5557
  }
5650
5558
 
5651
- // layout.ts
5652
- function layout(path2) {
5653
- return async (req, ctx, next) => {
5654
- const mod = await compileTsx(path2);
5655
- const Component = mod.default;
5656
- if (!Component) throw new Error(`Layout ${path2} has no default export`);
5657
- ctx.layoutStack = [...ctx.layoutStack || [], { path: path2, component: Component }];
5658
- return next(req, ctx);
5559
+ // live.ts
5560
+ import chokidar from "chokidar";
5561
+ import { existsSync as existsSync4 } from "node:fs";
5562
+ import { join as join4, resolve as resolve6 } from "node:path";
5563
+ var clients = /* @__PURE__ */ new Set();
5564
+ var hotBundleCache = /* @__PURE__ */ new Map();
5565
+ var hotKeys = [];
5566
+ var MAX_HOT = 10;
5567
+ function setHot(hash, code) {
5568
+ if (!hotBundleCache.has(hash)) {
5569
+ hotKeys.push(hash);
5570
+ if (hotKeys.length > MAX_HOT) {
5571
+ const old = hotKeys.shift();
5572
+ hotBundleCache.delete(old);
5573
+ }
5574
+ }
5575
+ hotBundleCache.set(hash, code);
5576
+ }
5577
+ function broadcastReload() {
5578
+ for (const ws of clients) {
5579
+ try {
5580
+ ws.send("reload");
5581
+ } catch {
5582
+ clients.delete(ws);
5583
+ }
5584
+ }
5585
+ }
5586
+ function broadcastCss(css) {
5587
+ const msg = JSON.stringify({ type: "css", css });
5588
+ for (const ws of clients) {
5589
+ try {
5590
+ ws.send(msg);
5591
+ } catch {
5592
+ clients.delete(ws);
5593
+ }
5594
+ }
5595
+ }
5596
+ function liveReload(dir) {
5597
+ const r = new Router();
5598
+ const resolved = resolve6(dir);
5599
+ const entryPath = join4(resolved, "page.tsx");
5600
+ r.get("/__wfw/v/bundle", async (req, ctx) => {
5601
+ const code = await compileVendorBundle();
5602
+ return new Response(code, {
5603
+ headers: { "content-type": "application/javascript; charset=utf-8" }
5604
+ });
5605
+ });
5606
+ r.get("/__wfw/h/:hash", async (req, ctx) => {
5607
+ const hash = ctx.params.hash.replace(/\.js$/i, "");
5608
+ const code = hotBundleCache.get(hash);
5609
+ if (!code) return new Response("", { status: 404 });
5610
+ return new Response(code, {
5611
+ headers: { "content-type": "application/javascript; charset=utf-8" }
5612
+ });
5613
+ });
5614
+ r.ws("/__weifuwu/livereload", {
5615
+ open(ws) {
5616
+ clients.add(ws);
5617
+ ws.on("close", () => clients.delete(ws));
5618
+ ws.on("error", () => clients.delete(ws));
5619
+ }
5620
+ });
5621
+ const watcher = chokidar.watch(dir, {
5622
+ ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
5623
+ ignoreInitial: true
5624
+ });
5625
+ watcher.on("change", async (filePath) => {
5626
+ if (/\.tsx?$/i.test(filePath)) {
5627
+ if (filePath.endsWith("layout.tsx")) {
5628
+ return broadcastReload();
5629
+ }
5630
+ clearCompileCache();
5631
+ markClientBundleDirty();
5632
+ try {
5633
+ const target = existsSync4(entryPath) ? entryPath : filePath;
5634
+ await compileTsxDev(target);
5635
+ const { hash, code } = await compileHotComponent(target);
5636
+ setHot(hash, code);
5637
+ let css;
5638
+ const cssPath = join4(resolved, "app.css");
5639
+ if (existsSync4(cssPath)) {
5640
+ css = await compileTailwindCss(cssPath, resolved);
5641
+ }
5642
+ const entry = id(entryPath);
5643
+ const msg = { type: "component", hash, entry };
5644
+ if (css) msg.css = css;
5645
+ const str = JSON.stringify(msg);
5646
+ for (const ws of clients) {
5647
+ try {
5648
+ ws.send(str);
5649
+ } catch {
5650
+ clients.delete(ws);
5651
+ }
5652
+ }
5653
+ } catch (e) {
5654
+ console.error("live reload failed, fallback to full reload:", e);
5655
+ broadcastReload();
5656
+ }
5657
+ } else if (/\.css$/i.test(filePath)) {
5658
+ const cssPath = join4(resolved, "app.css");
5659
+ if (existsSync4(cssPath)) {
5660
+ const css = await compileTailwindCss(cssPath, resolved);
5661
+ if (css) broadcastCss(css);
5662
+ }
5663
+ }
5664
+ });
5665
+ r.close = () => {
5666
+ watcher.close();
5667
+ clients.clear();
5659
5668
  };
5669
+ return r;
5670
+ }
5671
+
5672
+ // root-layout.ts
5673
+ function rootLayout(dir) {
5674
+ const r = new Router();
5675
+ const resolved = resolve7(dir);
5676
+ const isDev2 = process.env.NODE_ENV !== "production";
5677
+ const layoutPath = join5(resolved, "layout.tsx");
5678
+ r.use(async (req, ctx, next) => {
5679
+ const mod = await compile(layoutPath);
5680
+ if (mod?.default) ctx.layoutStack = [{ path: layoutPath, component: mod.default }];
5681
+ return next(req, ctx);
5682
+ });
5683
+ if (existsSync5(join5(resolved, "app.css"))) {
5684
+ r.use(tailwind(resolved));
5685
+ }
5686
+ if (isDev2) {
5687
+ const lr = liveReload(resolved);
5688
+ r.use(lr);
5689
+ r.close = lr.close;
5690
+ }
5691
+ return r;
5660
5692
  }
5661
5693
 
5662
5694
  // opencode/session.ts
5663
5695
  import { randomUUID as randomUUID2 } from "node:crypto";
5664
- import { join as join4 } from "node:path";
5696
+ import { join as join6 } from "node:path";
5665
5697
  import { mkdir as mkdir2 } from "node:fs/promises";
5666
5698
  var sessions = pgTable("_opencode_sessions", {
5667
5699
  id: uuid("id"),
@@ -5698,7 +5730,7 @@ async function createSession(sql2, opts, cwd, mountPath) {
5698
5730
  }
5699
5731
  function computeSessionWorkspace(cwd, mountPath, sessionId) {
5700
5732
  const name = !mountPath || mountPath === "/" ? "default" : mountPath.replace(/^\//, "");
5701
- return join4(cwd, ".sessions", name, sessionId);
5733
+ return join6(cwd, ".sessions", name, sessionId);
5702
5734
  }
5703
5735
  async function getSession(sql2, id3) {
5704
5736
  const { data: rows } = await sessions.readMany(sql2, { id: id3, active: true });
@@ -5895,10 +5927,10 @@ function createBashTool(ctx) {
5895
5927
  return { stdout: "", stderr: "Command denied: potentially dangerous command", exitCode: 1 };
5896
5928
  }
5897
5929
  const cwd = workdir ? `${ctx.workspace}/${workdir}` : ctx.workspace;
5898
- return new Promise((resolve14) => {
5930
+ return new Promise((resolve15) => {
5899
5931
  const child = exec(command, { cwd, timeout: timeout * 1e3, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
5900
5932
  const truncated = stdout.length > 1e6 || stderr.length > 1e6;
5901
- resolve14({
5933
+ resolve15({
5902
5934
  stdout: stdout.slice(0, 1e6),
5903
5935
  stderr: stderr.slice(0, 1e6),
5904
5936
  exitCode: error?.code ?? 0,
@@ -5915,7 +5947,7 @@ function createBashTool(ctx) {
5915
5947
  import { tool as tool4 } from "ai";
5916
5948
  import { z as z6 } from "zod";
5917
5949
  import { readFileSync as readFileSync4 } from "node:fs";
5918
- import { resolve as resolve6 } from "node:path";
5950
+ import { resolve as resolve8 } from "node:path";
5919
5951
  function createReadTool(ctx) {
5920
5952
  return tool4({
5921
5953
  description: "Read file contents. Supports offset and limit for reading specific line ranges.",
@@ -5925,7 +5957,7 @@ function createReadTool(ctx) {
5925
5957
  limit: z6.number().optional().describe("Number of lines to read")
5926
5958
  }),
5927
5959
  execute: async ({ path: path2, offset, limit }) => {
5928
- const resolved = resolve6(ctx.workspace, path2);
5960
+ const resolved = resolve8(ctx.workspace, path2);
5929
5961
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
5930
5962
  return { error: "Path not allowed", content: null, totalLines: 0 };
5931
5963
  }
@@ -5957,7 +5989,7 @@ function createReadTool(ctx) {
5957
5989
  import { tool as tool5 } from "ai";
5958
5990
  import { z as z7 } from "zod";
5959
5991
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "node:fs";
5960
- import { resolve as resolve7, dirname as dirname3 } from "node:path";
5992
+ import { resolve as resolve9, dirname as dirname3 } from "node:path";
5961
5993
  function createWriteTool(ctx) {
5962
5994
  return tool5({
5963
5995
  description: "Create or overwrite a file. Parent directories are created automatically.",
@@ -5966,7 +5998,7 @@ function createWriteTool(ctx) {
5966
5998
  content: z7.string().describe("File content")
5967
5999
  }),
5968
6000
  execute: async ({ path: path2, content }) => {
5969
- const resolved = resolve7(ctx.workspace, path2);
6001
+ const resolved = resolve9(ctx.workspace, path2);
5970
6002
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
5971
6003
  return { error: "Path not allowed" };
5972
6004
  }
@@ -5981,7 +6013,7 @@ function createWriteTool(ctx) {
5981
6013
  import { tool as tool6 } from "ai";
5982
6014
  import { z as z8 } from "zod";
5983
6015
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
5984
- import { resolve as resolve8 } from "node:path";
6016
+ import { resolve as resolve10 } from "node:path";
5985
6017
  function createEditTool(ctx) {
5986
6018
  return tool6({
5987
6019
  description: "Perform exact string replacements in a file. If oldString appears multiple times, provide more surrounding context.",
@@ -5992,7 +6024,7 @@ function createEditTool(ctx) {
5992
6024
  replaceAll: z8.boolean().default(false).describe("Replace all occurrences")
5993
6025
  }),
5994
6026
  execute: async ({ path: path2, oldString, newString, replaceAll }) => {
5995
- const resolved = resolve8(ctx.workspace, path2);
6027
+ const resolved = resolve10(ctx.workspace, path2);
5996
6028
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
5997
6029
  return { error: "Path not allowed" };
5998
6030
  }
@@ -6025,8 +6057,8 @@ function createEditTool(ctx) {
6025
6057
  import { tool as tool7 } from "ai";
6026
6058
  import { z as z9 } from "zod";
6027
6059
  import { execFileSync } from "node:child_process";
6028
- import { resolve as resolve9 } from "node:path";
6029
- import { existsSync as existsSync4 } from "node:fs";
6060
+ import { resolve as resolve11 } from "node:path";
6061
+ import { existsSync as existsSync6 } from "node:fs";
6030
6062
  function createGrepTool(ctx) {
6031
6063
  return tool7({
6032
6064
  description: "Search file contents using regex. Supports file type filtering and context lines.",
@@ -6037,10 +6069,10 @@ function createGrepTool(ctx) {
6037
6069
  context: z9.number().default(0).describe("Number of context lines before and after each match")
6038
6070
  }),
6039
6071
  execute: async ({ pattern, include, path: path2, context }) => {
6040
- const searchDir = path2 ? resolve9(ctx.workspace, path2) : ctx.workspace;
6072
+ const searchDir = path2 ? resolve11(ctx.workspace, path2) : ctx.workspace;
6041
6073
  try {
6042
6074
  let stdout;
6043
- if (existsSync4("/usr/bin/rg") || existsSync4("/usr/local/bin/rg")) {
6075
+ if (existsSync6("/usr/bin/rg") || existsSync6("/usr/local/bin/rg")) {
6044
6076
  const args = ["-n"];
6045
6077
  if (context > 0) args.push("-C", String(context));
6046
6078
  if (include) args.push("-g", include);
@@ -6069,7 +6101,7 @@ function createGrepTool(ctx) {
6069
6101
  import { tool as tool8 } from "ai";
6070
6102
  import { z as z10 } from "zod";
6071
6103
  import { execFileSync as execFileSync2 } from "node:child_process";
6072
- import { resolve as resolve10 } from "node:path";
6104
+ import { resolve as resolve12 } from "node:path";
6073
6105
  function createGlobTool(ctx) {
6074
6106
  return tool8({
6075
6107
  description: "Find files matching a glob pattern.",
@@ -6078,7 +6110,7 @@ function createGlobTool(ctx) {
6078
6110
  path: z10.string().optional().describe("Subdirectory relative to workspace")
6079
6111
  }),
6080
6112
  execute: async ({ pattern, path: path2 }) => {
6081
- const searchDir = path2 ? resolve10(ctx.workspace, path2) : ctx.workspace;
6113
+ const searchDir = path2 ? resolve12(ctx.workspace, path2) : ctx.workspace;
6082
6114
  try {
6083
6115
  const stdout = execFileSync2("find", [
6084
6116
  searchDir,
@@ -6139,7 +6171,7 @@ function createQuestionTool(ctx) {
6139
6171
  options: z12.array(z12.string()).optional().describe("Optional multiple choice options")
6140
6172
  }),
6141
6173
  execute: async ({ question, options }, { toolCallId }) => {
6142
- return new Promise((resolve14, reject) => {
6174
+ return new Promise((resolve15, reject) => {
6143
6175
  const timeout = setTimeout(() => {
6144
6176
  ctx.pendingQuestions.delete(toolCallId);
6145
6177
  reject(new Error("Question timed out"));
@@ -6147,7 +6179,7 @@ function createQuestionTool(ctx) {
6147
6179
  ctx.pendingQuestions.set(toolCallId, {
6148
6180
  resolve: (answer) => {
6149
6181
  clearTimeout(timeout);
6150
- resolve14(answer);
6182
+ resolve15(answer);
6151
6183
  },
6152
6184
  reject: (err) => {
6153
6185
  clearTimeout(timeout);
@@ -6294,9 +6326,8 @@ async function buildRouter4(deps) {
6294
6326
  });
6295
6327
  try {
6296
6328
  const uiDir = new URL("../opencode/ui/", import.meta.url).pathname;
6297
- router.use(tailwind(uiDir));
6298
- router.use(layout(join5(uiDir, "layout.tsx")));
6299
- router.get("/", ssr(join5(uiDir, "page.tsx")));
6329
+ router.use(rootLayout(uiDir));
6330
+ router.get("/", ssr(join7(uiDir, "page.tsx")));
6300
6331
  } catch (e) {
6301
6332
  console.warn("[opencode] UI not available:", e);
6302
6333
  }
@@ -6304,17 +6335,17 @@ async function buildRouter4(deps) {
6304
6335
  }
6305
6336
 
6306
6337
  // opencode/ws.ts
6307
- var clients = /* @__PURE__ */ new WeakMap();
6338
+ var clients2 = /* @__PURE__ */ new WeakMap();
6308
6339
  function createWSHandler2(deps) {
6309
6340
  const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
6310
6341
  return {
6311
6342
  open(ws, ctx) {
6312
6343
  const userId = ctx.user?.id ?? 0;
6313
6344
  const mountPath = ctx.mountPath ?? "";
6314
- clients.set(ws, { userId, mountPath });
6345
+ clients2.set(ws, { userId, mountPath });
6315
6346
  },
6316
6347
  async message(ws, ctx, data) {
6317
- const client = clients.get(ws);
6348
+ const client = clients2.get(ws);
6318
6349
  if (!client) return;
6319
6350
  let msg;
6320
6351
  try {
@@ -6411,17 +6442,17 @@ function createWSHandler2(deps) {
6411
6442
  }
6412
6443
  },
6413
6444
  close(ws) {
6414
- const client = clients.get(ws);
6445
+ const client = clients2.get(ws);
6415
6446
  if (client) {
6416
6447
  client.abortController?.abort();
6417
- clients.delete(ws);
6448
+ clients2.delete(ws);
6418
6449
  }
6419
6450
  },
6420
6451
  error(ws, _ctx, _err) {
6421
- const client = clients.get(ws);
6452
+ const client = clients2.get(ws);
6422
6453
  if (client) {
6423
6454
  client.abortController?.abort();
6424
- clients.delete(ws);
6455
+ clients2.delete(ws);
6425
6456
  }
6426
6457
  }
6427
6458
  };
@@ -6431,7 +6462,7 @@ function createWSHandler2(deps) {
6431
6462
  import { readFile } from "node:fs/promises";
6432
6463
  import { glob } from "node:fs/promises";
6433
6464
  import { homedir } from "node:os";
6434
- import { resolve as resolve11 } from "node:path";
6465
+ import { resolve as resolve13 } from "node:path";
6435
6466
  import { parse as parseYaml } from "yaml";
6436
6467
  var SEARCH_DIRS = [
6437
6468
  (ws) => `${ws}/.opencode/skills`,
@@ -6469,7 +6500,7 @@ async function scanDir(dir) {
6469
6500
  try {
6470
6501
  const files = [];
6471
6502
  for await (const entry of glob("*/SKILL.md", { cwd: dir })) {
6472
- const skill = await parseSkillFile(resolve11(dir, entry));
6503
+ const skill = await parseSkillFile(resolve13(dir, entry));
6473
6504
  if (skill) files.push(skill);
6474
6505
  }
6475
6506
  return files;
@@ -6804,8 +6835,8 @@ function analytics(options) {
6804
6835
 
6805
6836
  // preferences.ts
6806
6837
  import { readFile as readFile2 } from "node:fs/promises";
6807
- import { existsSync as existsSync5 } from "node:fs";
6808
- import { join as join6, resolve as resolve12 } from "node:path";
6838
+ import { existsSync as existsSync7 } from "node:fs";
6839
+ import { join as join8, resolve as resolve14 } from "node:path";
6809
6840
  var defaults = {
6810
6841
  locale: { default: "en", cookie: "locale", fromAcceptLanguage: true },
6811
6842
  theme: { default: "system", cookie: "theme" }
@@ -6854,7 +6885,7 @@ async function handlePrefSwitch(req, value, cookieName, load) {
6854
6885
  });
6855
6886
  }
6856
6887
  function preferences(options) {
6857
- const dir = options.dir ? resolve12(options.dir) : void 0;
6888
+ const dir = options.dir ? resolve14(options.dir) : void 0;
6858
6889
  const localeOpts = { ...defaults.locale, ...options.locale };
6859
6890
  const themeOpts = { ...defaults.theme, ...options.theme };
6860
6891
  const cache2 = /* @__PURE__ */ new Map();
@@ -6866,8 +6897,8 @@ function preferences(options) {
6866
6897
  if (!validLocale(locale)) return {};
6867
6898
  const cached = cache2.get(locale);
6868
6899
  if (cached) return cached;
6869
- const filePath = join6(dir, `${locale}.json`);
6870
- if (existsSync5(filePath)) {
6900
+ const filePath = join8(dir, `${locale}.json`);
6901
+ if (existsSync7(filePath)) {
6871
6902
  try {
6872
6903
  const content = await readFile2(filePath, "utf-8");
6873
6904
  const data = JSON.parse(content);
@@ -7903,12 +7934,12 @@ function iii(opts = {}) {
7903
7934
  const handler = async (payload) => {
7904
7935
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
7905
7936
  const invocationId = crypto6.randomUUID();
7906
- return new Promise((resolve14, reject) => {
7937
+ return new Promise((resolve15, reject) => {
7907
7938
  const timer = setTimeout(() => {
7908
7939
  pending.delete(invocationId);
7909
7940
  reject(new Error(`Invocation timed out for "${id3}"`));
7910
7941
  }, 3e4);
7911
- pending.set(invocationId, { resolve: resolve14, reject, timer });
7942
+ pending.set(invocationId, { resolve: resolve15, reject, timer });
7912
7943
  worker.ws.send(JSON.stringify({
7913
7944
  type: "invoke",
7914
7945
  invocation_id: invocationId,
@@ -8149,8 +8180,8 @@ function registerWorker(url) {
8149
8180
  function connect() {
8150
8181
  if (intentionalClose) return;
8151
8182
  ws = new WebSocket(url);
8152
- ready = new Promise((resolve14) => {
8153
- resolveReady = resolve14;
8183
+ ready = new Promise((resolve15) => {
8184
+ resolveReady = resolve15;
8154
8185
  });
8155
8186
  ws.onopen = () => {
8156
8187
  reconnectAttempt = 0;
@@ -8270,13 +8301,13 @@ function registerWorker(url) {
8270
8301
  }
8271
8302
  return Promise.resolve(fn(request.payload, ctx));
8272
8303
  }
8273
- return new Promise((resolve14, reject) => {
8304
+ return new Promise((resolve15, reject) => {
8274
8305
  const invocationId = genId();
8275
8306
  const timer = setTimeout(() => {
8276
8307
  pendingInvocations.delete(invocationId);
8277
8308
  reject(new Error(`Invocation timed out for "${request.function_id}"`));
8278
8309
  }, request.timeout_ms || 3e4);
8279
- pendingInvocations.set(invocationId, { resolve: resolve14, reject, timer });
8310
+ pendingInvocations.set(invocationId, { resolve: resolve15, reject, timer });
8280
8311
  send({
8281
8312
  type: "invoke",
8282
8313
  invocation_id: invocationId,
@@ -8297,175 +8328,111 @@ function registerWorker(url) {
8297
8328
  };
8298
8329
  }
8299
8330
 
8300
- // live.ts
8301
- import chokidar from "chokidar";
8302
- import { existsSync as existsSync6 } from "node:fs";
8303
- import { join as join7, resolve as resolve13 } from "node:path";
8304
- var clients2 = /* @__PURE__ */ new Set();
8305
- var hotBundleCache = /* @__PURE__ */ new Map();
8306
- var hotKeys = [];
8307
- var MAX_HOT = 10;
8308
- function setHot(hash, code) {
8309
- if (!hotBundleCache.has(hash)) {
8310
- hotKeys.push(hash);
8311
- if (hotKeys.length > MAX_HOT) {
8312
- const old = hotKeys.shift();
8313
- hotBundleCache.delete(old);
8314
- }
8315
- }
8316
- hotBundleCache.set(hash, code);
8317
- }
8318
- function broadcastReload() {
8319
- for (const ws of clients2) {
8320
- try {
8321
- ws.send("reload");
8322
- } catch {
8323
- clients2.delete(ws);
8324
- }
8325
- }
8326
- }
8327
- function broadcastCss(css) {
8328
- const msg = JSON.stringify({ type: "css", css });
8329
- for (const ws of clients2) {
8330
- try {
8331
- ws.send(msg);
8332
- } catch {
8333
- clients2.delete(ws);
8334
- }
8335
- }
8336
- }
8337
- function liveReload(opts) {
8338
- const r = new Router();
8339
- const entryPath = (() => {
8340
- for (const dir of opts.dirs) {
8341
- const p = join7(resolve13(dir), "page.tsx");
8342
- if (existsSync6(p)) return p;
8343
- }
8344
- return "";
8345
- })();
8346
- r.get("/__wfw/v/bundle", async (req, ctx) => {
8347
- const code = await compileVendorBundle();
8348
- return new Response(code, {
8349
- headers: { "content-type": "application/javascript; charset=utf-8" }
8350
- });
8351
- });
8352
- r.get("/__wfw/h/:hash", async (req, ctx) => {
8353
- const hash = ctx.params.hash.replace(/\.js$/i, "");
8354
- const code = hotBundleCache.get(hash);
8355
- if (!code) return new Response("", { status: 404 });
8356
- return new Response(code, {
8357
- headers: { "content-type": "application/javascript; charset=utf-8" }
8358
- });
8359
- });
8360
- r.ws("/__weifuwu/livereload", {
8361
- open(ws) {
8362
- clients2.add(ws);
8363
- ws.on("close", () => clients2.delete(ws));
8364
- ws.on("error", () => clients2.delete(ws));
8365
- }
8366
- });
8367
- const watcher = chokidar.watch(opts.dirs, {
8368
- ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
8369
- ignoreInitial: true
8370
- });
8371
- watcher.on("change", async (filePath) => {
8372
- if (/\.tsx?$/i.test(filePath)) {
8373
- clearCompileCache();
8374
- markClientBundleDirty();
8375
- try {
8376
- const target = entryPath || filePath;
8377
- await compileTsxDev(target);
8378
- const { hash, code } = await compileHotComponent(target);
8379
- setHot(hash, code);
8380
- let css;
8381
- for (const dir of opts.dirs) {
8382
- const cssPath = join7(resolve13(dir), "app.css");
8383
- if (existsSync6(cssPath)) {
8384
- css = await compileTailwindCss(cssPath, resolve13(dir));
8385
- }
8386
- }
8387
- const entry = entryPath ? id(entryPath) : "";
8388
- const msg = { type: "component", hash, entry };
8389
- if (css) msg.css = css;
8390
- const str = JSON.stringify(msg);
8391
- for (const ws of clients2) {
8392
- try {
8393
- ws.send(str);
8394
- } catch {
8395
- clients2.delete(ws);
8396
- }
8397
- }
8398
- } catch (e) {
8399
- console.error("live reload failed, fallback to full reload:", e);
8400
- broadcastReload();
8401
- }
8402
- } else if (/\.css$/i.test(filePath)) {
8403
- for (const dir of opts.dirs) {
8404
- const cssPath = join7(resolve13(dir), "app.css");
8405
- if (existsSync6(cssPath)) {
8406
- const css = await compileTailwindCss(cssPath, resolve13(dir));
8407
- if (css) broadcastCss(css);
8408
- }
8409
- }
8410
- }
8411
- });
8412
- r.close = () => {
8413
- watcher.close();
8414
- clients2.clear();
8331
+ // layout.ts
8332
+ function layout(path2) {
8333
+ return async (req, ctx, next) => {
8334
+ const mod = await compile(path2);
8335
+ const Component = mod.default;
8336
+ if (!Component) throw new Error(`Layout ${path2} has no default export`);
8337
+ ctx.layoutStack = [...ctx.layoutStack || [], { path: path2, component: Component }];
8338
+ return next(req, ctx);
8415
8339
  };
8416
- return r;
8417
8340
  }
8418
8341
 
8419
8342
  // not-found.ts
8343
+ import { createElement as createElement2 } from "react";
8420
8344
  function notFound(path2) {
8421
8345
  return async (req, ctx) => {
8422
8346
  if (!path2) return new Response("Not Found", { status: 404 });
8423
- const mod = await compileTsx(path2);
8424
- const Component = mod?.default;
8425
- const body = Component ? "404 - Not Found" : "404 - Not Found";
8426
- return new Response(body, {
8427
- status: 404,
8428
- headers: { "content-type": "text/html; charset=utf-8" }
8347
+ let Component;
8348
+ try {
8349
+ const mod = await compile(path2);
8350
+ Component = mod?.default;
8351
+ } catch {
8352
+ return new Response("Not Found", { status: 404 });
8353
+ }
8354
+ if (!Component) return new Response("Not Found", { status: 404 });
8355
+ const layouts = ctx.layoutStack || [];
8356
+ const layoutComponents = layouts.map((l) => l.component);
8357
+ const base = (ctx.mountPath || "").replace(/\/$/, "");
8358
+ let element = createElement2(
8359
+ "div",
8360
+ { id: "__weifuwu_root" },
8361
+ createElement2(Component, null)
8362
+ );
8363
+ if (layoutComponents.length === 0) {
8364
+ element = createElement2(
8365
+ "html",
8366
+ { lang: "en" },
8367
+ createElement2(
8368
+ "head",
8369
+ null,
8370
+ createElement2("meta", { charSet: "utf-8" }),
8371
+ createElement2("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
8372
+ createElement2("title", null, "404")
8373
+ ),
8374
+ createElement2("body", null, element)
8375
+ );
8376
+ } else {
8377
+ for (const L of layoutComponents.toReversed()) {
8378
+ element = createElement2(L, { children: element });
8379
+ }
8380
+ }
8381
+ const { renderToReadableStream } = await import("react-dom/server");
8382
+ const stream = await renderToReadableStream(element);
8383
+ return streamResponse(stream, {
8384
+ ctx,
8385
+ base,
8386
+ isDev: process.env.NODE_ENV !== "production",
8387
+ compiledTailwindCss: ctx.compiledTailwindCss,
8388
+ status: 404
8429
8389
  });
8430
8390
  };
8431
8391
  }
8432
8392
 
8433
8393
  // error-boundary.ts
8434
- import { createElement as createElement2 } from "react";
8435
- import { TextEncoder as TextEncoder3 } from "node:util";
8394
+ import { createElement as createElement3 } from "react";
8436
8395
  function errorBoundary(errorPath) {
8437
8396
  return async (req, ctx, next) => {
8438
8397
  try {
8439
8398
  return await next(req, ctx);
8440
8399
  } catch (err) {
8441
- const mod = await compileTsx(errorPath);
8400
+ const mod = await compile(errorPath);
8442
8401
  const ErrorComponent = mod.default;
8443
8402
  if (!ErrorComponent) throw err;
8444
8403
  const layouts = (ctx.layoutStack || []).map((l) => l.component);
8445
- const stream = await import("react-dom/server").then((m) => m.renderToReadableStream(
8446
- createElement2(ErrorComponent, {
8447
- error: err instanceof Error ? err : new Error(String(err)),
8448
- reset: () => {
8449
- }
8450
- })
8451
- ));
8452
- const reader = stream.getReader();
8453
- const chunks = [];
8454
- while (true) {
8455
- const { done, value } = await reader.read();
8456
- if (done) break;
8457
- chunks.push(value);
8458
- }
8459
- const encoder2 = new TextEncoder3();
8460
- const body = chunks.reduce((acc, c) => {
8461
- const merged = new Uint8Array(acc.length + c.length);
8462
- merged.set(acc);
8463
- merged.set(c, acc.length);
8464
- return merged;
8465
- }, new Uint8Array(0));
8466
- return new Response(body, {
8467
- status: 500,
8468
- headers: { "content-type": "text/html; charset=utf-8" }
8404
+ const base = (ctx.mountPath || "").replace(/\/$/, "");
8405
+ let element = createElement3(ErrorComponent, {
8406
+ error: err instanceof Error ? err : new Error(String(err)),
8407
+ reset: () => {
8408
+ }
8409
+ });
8410
+ if (layouts.length === 0) {
8411
+ element = createElement3(
8412
+ "html",
8413
+ { lang: "en" },
8414
+ createElement3(
8415
+ "head",
8416
+ null,
8417
+ createElement3("meta", { charSet: "utf-8" }),
8418
+ createElement3("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
8419
+ createElement3("title", null, "500")
8420
+ ),
8421
+ createElement3("body", null, element)
8422
+ );
8423
+ } else {
8424
+ for (const L of layouts.toReversed()) {
8425
+ element = createElement3(L, { children: element });
8426
+ }
8427
+ }
8428
+ const { renderToReadableStream } = await import("react-dom/server");
8429
+ const stream = await renderToReadableStream(element);
8430
+ return streamResponse(stream, {
8431
+ ctx,
8432
+ base,
8433
+ isDev: process.env.NODE_ENV !== "production",
8434
+ compiledTailwindCss: ctx.compiledTailwindCss,
8435
+ status: 500
8469
8436
  });
8470
8437
  }
8471
8438
  };
@@ -8502,7 +8469,6 @@ export {
8502
8469
  helmet,
8503
8470
  iii,
8504
8471
  layout,
8505
- liveReload,
8506
8472
  loadEnv,
8507
8473
  logdb,
8508
8474
  logger,
@@ -8518,6 +8484,7 @@ export {
8518
8484
  redis,
8519
8485
  registerWorker,
8520
8486
  requestId,
8487
+ rootLayout,
8521
8488
  runWorkflow,
8522
8489
  seo,
8523
8490
  seoMiddleware,
@@ -8529,7 +8496,6 @@ export {
8529
8496
  ssr,
8530
8497
  streamObject,
8531
8498
  streamText,
8532
- tailwind,
8533
8499
  tenant,
8534
8500
  tool2 as tool,
8535
8501
  upload,