weifuwu 0.19.4 → 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";
@@ -5325,13 +5215,14 @@ function buildHeadPayload(opts) {
5325
5215
  const { ctx, base, compiledTailwindCss, isDev: isDev2 } = opts;
5326
5216
  let result = "";
5327
5217
  if (isDev2) {
5218
+ const vUrl = `${base}/__wfw/v/bundle`;
5328
5219
  result += `<script type="importmap">{
5329
5220
  "imports": {
5330
- "react": "/__wfw/v/bundle",
5331
- "react-dom": "/__wfw/v/bundle",
5332
- "react-dom/client": "/__wfw/v/bundle",
5333
- "react/jsx-runtime": "/__wfw/v/bundle",
5334
- "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}"
5335
5226
  }
5336
5227
  }</script>
5337
5228
  `;
@@ -5341,7 +5232,8 @@ function buildHeadPayload(opts) {
5341
5232
  `;
5342
5233
  }
5343
5234
  if (compiledTailwindCss) {
5344
- result += `<link rel="stylesheet" href="${base}/__wfw/style.css" />
5235
+ const cssUrl = ctx.tailwindCssUrl || "/__wfw/style.css";
5236
+ result += `<link rel="stylesheet" href="${cssUrl}" />
5345
5237
  `;
5346
5238
  }
5347
5239
  const localeData = ctx.parsed?.__localeData ?? globalThis.__LOCALE_DATA__;
@@ -5422,9 +5314,11 @@ function streamResponse(reactStream, opts) {
5422
5314
  const body = buildBodyScripts(opts);
5423
5315
  if (body) controller.enqueue(encoder2.encode("\n" + body));
5424
5316
  if (opts.isDev) {
5317
+ const wsUrl = `${opts.base}/__weifuwu/livereload`;
5318
+ const hbUrl = `${opts.base}/__wfw/h/`;
5425
5319
  controller.enqueue(encoder2.encode(
5426
5320
  `
5427
- <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>`
5428
5322
  ));
5429
5323
  }
5430
5324
  } catch {
@@ -5506,7 +5400,7 @@ async function buildClientBundle(entryPath, layoutPaths) {
5506
5400
  jsxImportSource: "react",
5507
5401
  banner: { js: "self.process={env:{}};" },
5508
5402
  loader: { ".node": "empty" },
5509
- 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,
5510
5404
  write: false,
5511
5405
  minify: !isDev
5512
5406
  });
@@ -5600,7 +5494,12 @@ function ssr(path2) {
5600
5494
  return r;
5601
5495
  }
5602
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
+
5603
5501
  // tailwind.ts
5502
+ import { createHash as createHash4 } from "node:crypto";
5604
5503
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
5605
5504
  import { join as join3, relative, resolve as resolve5 } from "node:path";
5606
5505
  var extraSources = /* @__PURE__ */ new Set();
@@ -5611,16 +5510,21 @@ function tailwind(dir) {
5611
5510
  const r = new Router();
5612
5511
  r.use(async (req, ctx, next) => {
5613
5512
  if (!cssCache.has(cssPath)) {
5614
- cssCache.set(cssPath, await compileTailwindCss(cssPath, cssDir));
5513
+ await compileTailwindCss(cssPath, cssDir);
5615
5514
  }
5616
- 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`;
5617
5519
  return next(req, ctx);
5618
5520
  });
5619
- r.get("/__wfw/style.css", async (req, ctx) => {
5521
+ r.get("/__wfw/style/:hash.css", async (req, ctx) => {
5620
5522
  if (!cssCache.has(cssPath)) {
5621
- cssCache.set(cssPath, await compileTailwindCss(cssPath, cssDir));
5523
+ await compileTailwindCss(cssPath, cssDir);
5622
5524
  }
5623
- 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, {
5624
5528
  headers: { "content-type": "text/css; charset=utf-8" }
5625
5529
  });
5626
5530
  });
@@ -5643,7 +5547,8 @@ ${src}`;
5643
5547
  ${src}`;
5644
5548
  }
5645
5549
  const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
5646
- 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 });
5647
5552
  return result.css;
5648
5553
  } catch (err) {
5649
5554
  console.warn("Tailwind CSS processing failed:", err.message);
@@ -5651,20 +5556,144 @@ ${src}`;
5651
5556
  }
5652
5557
  }
5653
5558
 
5654
- // layout.ts
5655
- function layout(path2) {
5656
- return async (req, ctx, next) => {
5657
- const mod = await compile(path2);
5658
- const Component = mod.default;
5659
- if (!Component) throw new Error(`Layout ${path2} has no default export`);
5660
- ctx.layoutStack = [...ctx.layoutStack || [], { path: path2, component: Component }];
5661
- 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();
5662
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;
5663
5692
  }
5664
5693
 
5665
5694
  // opencode/session.ts
5666
5695
  import { randomUUID as randomUUID2 } from "node:crypto";
5667
- import { join as join4 } from "node:path";
5696
+ import { join as join6 } from "node:path";
5668
5697
  import { mkdir as mkdir2 } from "node:fs/promises";
5669
5698
  var sessions = pgTable("_opencode_sessions", {
5670
5699
  id: uuid("id"),
@@ -5701,7 +5730,7 @@ async function createSession(sql2, opts, cwd, mountPath) {
5701
5730
  }
5702
5731
  function computeSessionWorkspace(cwd, mountPath, sessionId) {
5703
5732
  const name = !mountPath || mountPath === "/" ? "default" : mountPath.replace(/^\//, "");
5704
- return join4(cwd, ".sessions", name, sessionId);
5733
+ return join6(cwd, ".sessions", name, sessionId);
5705
5734
  }
5706
5735
  async function getSession(sql2, id3) {
5707
5736
  const { data: rows } = await sessions.readMany(sql2, { id: id3, active: true });
@@ -5898,10 +5927,10 @@ function createBashTool(ctx) {
5898
5927
  return { stdout: "", stderr: "Command denied: potentially dangerous command", exitCode: 1 };
5899
5928
  }
5900
5929
  const cwd = workdir ? `${ctx.workspace}/${workdir}` : ctx.workspace;
5901
- return new Promise((resolve14) => {
5930
+ return new Promise((resolve15) => {
5902
5931
  const child = exec(command, { cwd, timeout: timeout * 1e3, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
5903
5932
  const truncated = stdout.length > 1e6 || stderr.length > 1e6;
5904
- resolve14({
5933
+ resolve15({
5905
5934
  stdout: stdout.slice(0, 1e6),
5906
5935
  stderr: stderr.slice(0, 1e6),
5907
5936
  exitCode: error?.code ?? 0,
@@ -5918,7 +5947,7 @@ function createBashTool(ctx) {
5918
5947
  import { tool as tool4 } from "ai";
5919
5948
  import { z as z6 } from "zod";
5920
5949
  import { readFileSync as readFileSync4 } from "node:fs";
5921
- import { resolve as resolve6 } from "node:path";
5950
+ import { resolve as resolve8 } from "node:path";
5922
5951
  function createReadTool(ctx) {
5923
5952
  return tool4({
5924
5953
  description: "Read file contents. Supports offset and limit for reading specific line ranges.",
@@ -5928,7 +5957,7 @@ function createReadTool(ctx) {
5928
5957
  limit: z6.number().optional().describe("Number of lines to read")
5929
5958
  }),
5930
5959
  execute: async ({ path: path2, offset, limit }) => {
5931
- const resolved = resolve6(ctx.workspace, path2);
5960
+ const resolved = resolve8(ctx.workspace, path2);
5932
5961
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
5933
5962
  return { error: "Path not allowed", content: null, totalLines: 0 };
5934
5963
  }
@@ -5960,7 +5989,7 @@ function createReadTool(ctx) {
5960
5989
  import { tool as tool5 } from "ai";
5961
5990
  import { z as z7 } from "zod";
5962
5991
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "node:fs";
5963
- import { resolve as resolve7, dirname as dirname3 } from "node:path";
5992
+ import { resolve as resolve9, dirname as dirname3 } from "node:path";
5964
5993
  function createWriteTool(ctx) {
5965
5994
  return tool5({
5966
5995
  description: "Create or overwrite a file. Parent directories are created automatically.",
@@ -5969,7 +5998,7 @@ function createWriteTool(ctx) {
5969
5998
  content: z7.string().describe("File content")
5970
5999
  }),
5971
6000
  execute: async ({ path: path2, content }) => {
5972
- const resolved = resolve7(ctx.workspace, path2);
6001
+ const resolved = resolve9(ctx.workspace, path2);
5973
6002
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
5974
6003
  return { error: "Path not allowed" };
5975
6004
  }
@@ -5984,7 +6013,7 @@ function createWriteTool(ctx) {
5984
6013
  import { tool as tool6 } from "ai";
5985
6014
  import { z as z8 } from "zod";
5986
6015
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
5987
- import { resolve as resolve8 } from "node:path";
6016
+ import { resolve as resolve10 } from "node:path";
5988
6017
  function createEditTool(ctx) {
5989
6018
  return tool6({
5990
6019
  description: "Perform exact string replacements in a file. If oldString appears multiple times, provide more surrounding context.",
@@ -5995,7 +6024,7 @@ function createEditTool(ctx) {
5995
6024
  replaceAll: z8.boolean().default(false).describe("Replace all occurrences")
5996
6025
  }),
5997
6026
  execute: async ({ path: path2, oldString, newString, replaceAll }) => {
5998
- const resolved = resolve8(ctx.workspace, path2);
6027
+ const resolved = resolve10(ctx.workspace, path2);
5999
6028
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
6000
6029
  return { error: "Path not allowed" };
6001
6030
  }
@@ -6028,8 +6057,8 @@ function createEditTool(ctx) {
6028
6057
  import { tool as tool7 } from "ai";
6029
6058
  import { z as z9 } from "zod";
6030
6059
  import { execFileSync } from "node:child_process";
6031
- import { resolve as resolve9 } from "node:path";
6032
- import { existsSync as existsSync4 } from "node:fs";
6060
+ import { resolve as resolve11 } from "node:path";
6061
+ import { existsSync as existsSync6 } from "node:fs";
6033
6062
  function createGrepTool(ctx) {
6034
6063
  return tool7({
6035
6064
  description: "Search file contents using regex. Supports file type filtering and context lines.",
@@ -6040,10 +6069,10 @@ function createGrepTool(ctx) {
6040
6069
  context: z9.number().default(0).describe("Number of context lines before and after each match")
6041
6070
  }),
6042
6071
  execute: async ({ pattern, include, path: path2, context }) => {
6043
- const searchDir = path2 ? resolve9(ctx.workspace, path2) : ctx.workspace;
6072
+ const searchDir = path2 ? resolve11(ctx.workspace, path2) : ctx.workspace;
6044
6073
  try {
6045
6074
  let stdout;
6046
- if (existsSync4("/usr/bin/rg") || existsSync4("/usr/local/bin/rg")) {
6075
+ if (existsSync6("/usr/bin/rg") || existsSync6("/usr/local/bin/rg")) {
6047
6076
  const args = ["-n"];
6048
6077
  if (context > 0) args.push("-C", String(context));
6049
6078
  if (include) args.push("-g", include);
@@ -6072,7 +6101,7 @@ function createGrepTool(ctx) {
6072
6101
  import { tool as tool8 } from "ai";
6073
6102
  import { z as z10 } from "zod";
6074
6103
  import { execFileSync as execFileSync2 } from "node:child_process";
6075
- import { resolve as resolve10 } from "node:path";
6104
+ import { resolve as resolve12 } from "node:path";
6076
6105
  function createGlobTool(ctx) {
6077
6106
  return tool8({
6078
6107
  description: "Find files matching a glob pattern.",
@@ -6081,7 +6110,7 @@ function createGlobTool(ctx) {
6081
6110
  path: z10.string().optional().describe("Subdirectory relative to workspace")
6082
6111
  }),
6083
6112
  execute: async ({ pattern, path: path2 }) => {
6084
- const searchDir = path2 ? resolve10(ctx.workspace, path2) : ctx.workspace;
6113
+ const searchDir = path2 ? resolve12(ctx.workspace, path2) : ctx.workspace;
6085
6114
  try {
6086
6115
  const stdout = execFileSync2("find", [
6087
6116
  searchDir,
@@ -6142,7 +6171,7 @@ function createQuestionTool(ctx) {
6142
6171
  options: z12.array(z12.string()).optional().describe("Optional multiple choice options")
6143
6172
  }),
6144
6173
  execute: async ({ question, options }, { toolCallId }) => {
6145
- return new Promise((resolve14, reject) => {
6174
+ return new Promise((resolve15, reject) => {
6146
6175
  const timeout = setTimeout(() => {
6147
6176
  ctx.pendingQuestions.delete(toolCallId);
6148
6177
  reject(new Error("Question timed out"));
@@ -6150,7 +6179,7 @@ function createQuestionTool(ctx) {
6150
6179
  ctx.pendingQuestions.set(toolCallId, {
6151
6180
  resolve: (answer) => {
6152
6181
  clearTimeout(timeout);
6153
- resolve14(answer);
6182
+ resolve15(answer);
6154
6183
  },
6155
6184
  reject: (err) => {
6156
6185
  clearTimeout(timeout);
@@ -6297,9 +6326,8 @@ async function buildRouter4(deps) {
6297
6326
  });
6298
6327
  try {
6299
6328
  const uiDir = new URL("../opencode/ui/", import.meta.url).pathname;
6300
- router.use(tailwind(uiDir));
6301
- router.use(layout(join5(uiDir, "layout.tsx")));
6302
- router.get("/", ssr(join5(uiDir, "page.tsx")));
6329
+ router.use(rootLayout(uiDir));
6330
+ router.get("/", ssr(join7(uiDir, "page.tsx")));
6303
6331
  } catch (e) {
6304
6332
  console.warn("[opencode] UI not available:", e);
6305
6333
  }
@@ -6307,17 +6335,17 @@ async function buildRouter4(deps) {
6307
6335
  }
6308
6336
 
6309
6337
  // opencode/ws.ts
6310
- var clients = /* @__PURE__ */ new WeakMap();
6338
+ var clients2 = /* @__PURE__ */ new WeakMap();
6311
6339
  function createWSHandler2(deps) {
6312
6340
  const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
6313
6341
  return {
6314
6342
  open(ws, ctx) {
6315
6343
  const userId = ctx.user?.id ?? 0;
6316
6344
  const mountPath = ctx.mountPath ?? "";
6317
- clients.set(ws, { userId, mountPath });
6345
+ clients2.set(ws, { userId, mountPath });
6318
6346
  },
6319
6347
  async message(ws, ctx, data) {
6320
- const client = clients.get(ws);
6348
+ const client = clients2.get(ws);
6321
6349
  if (!client) return;
6322
6350
  let msg;
6323
6351
  try {
@@ -6414,17 +6442,17 @@ function createWSHandler2(deps) {
6414
6442
  }
6415
6443
  },
6416
6444
  close(ws) {
6417
- const client = clients.get(ws);
6445
+ const client = clients2.get(ws);
6418
6446
  if (client) {
6419
6447
  client.abortController?.abort();
6420
- clients.delete(ws);
6448
+ clients2.delete(ws);
6421
6449
  }
6422
6450
  },
6423
6451
  error(ws, _ctx, _err) {
6424
- const client = clients.get(ws);
6452
+ const client = clients2.get(ws);
6425
6453
  if (client) {
6426
6454
  client.abortController?.abort();
6427
- clients.delete(ws);
6455
+ clients2.delete(ws);
6428
6456
  }
6429
6457
  }
6430
6458
  };
@@ -6434,7 +6462,7 @@ function createWSHandler2(deps) {
6434
6462
  import { readFile } from "node:fs/promises";
6435
6463
  import { glob } from "node:fs/promises";
6436
6464
  import { homedir } from "node:os";
6437
- import { resolve as resolve11 } from "node:path";
6465
+ import { resolve as resolve13 } from "node:path";
6438
6466
  import { parse as parseYaml } from "yaml";
6439
6467
  var SEARCH_DIRS = [
6440
6468
  (ws) => `${ws}/.opencode/skills`,
@@ -6472,7 +6500,7 @@ async function scanDir(dir) {
6472
6500
  try {
6473
6501
  const files = [];
6474
6502
  for await (const entry of glob("*/SKILL.md", { cwd: dir })) {
6475
- const skill = await parseSkillFile(resolve11(dir, entry));
6503
+ const skill = await parseSkillFile(resolve13(dir, entry));
6476
6504
  if (skill) files.push(skill);
6477
6505
  }
6478
6506
  return files;
@@ -6807,8 +6835,8 @@ function analytics(options) {
6807
6835
 
6808
6836
  // preferences.ts
6809
6837
  import { readFile as readFile2 } from "node:fs/promises";
6810
- import { existsSync as existsSync5 } from "node:fs";
6811
- 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";
6812
6840
  var defaults = {
6813
6841
  locale: { default: "en", cookie: "locale", fromAcceptLanguage: true },
6814
6842
  theme: { default: "system", cookie: "theme" }
@@ -6857,7 +6885,7 @@ async function handlePrefSwitch(req, value, cookieName, load) {
6857
6885
  });
6858
6886
  }
6859
6887
  function preferences(options) {
6860
- const dir = options.dir ? resolve12(options.dir) : void 0;
6888
+ const dir = options.dir ? resolve14(options.dir) : void 0;
6861
6889
  const localeOpts = { ...defaults.locale, ...options.locale };
6862
6890
  const themeOpts = { ...defaults.theme, ...options.theme };
6863
6891
  const cache2 = /* @__PURE__ */ new Map();
@@ -6869,8 +6897,8 @@ function preferences(options) {
6869
6897
  if (!validLocale(locale)) return {};
6870
6898
  const cached = cache2.get(locale);
6871
6899
  if (cached) return cached;
6872
- const filePath = join6(dir, `${locale}.json`);
6873
- if (existsSync5(filePath)) {
6900
+ const filePath = join8(dir, `${locale}.json`);
6901
+ if (existsSync7(filePath)) {
6874
6902
  try {
6875
6903
  const content = await readFile2(filePath, "utf-8");
6876
6904
  const data = JSON.parse(content);
@@ -7906,12 +7934,12 @@ function iii(opts = {}) {
7906
7934
  const handler = async (payload) => {
7907
7935
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
7908
7936
  const invocationId = crypto6.randomUUID();
7909
- return new Promise((resolve14, reject) => {
7937
+ return new Promise((resolve15, reject) => {
7910
7938
  const timer = setTimeout(() => {
7911
7939
  pending.delete(invocationId);
7912
7940
  reject(new Error(`Invocation timed out for "${id3}"`));
7913
7941
  }, 3e4);
7914
- pending.set(invocationId, { resolve: resolve14, reject, timer });
7942
+ pending.set(invocationId, { resolve: resolve15, reject, timer });
7915
7943
  worker.ws.send(JSON.stringify({
7916
7944
  type: "invoke",
7917
7945
  invocation_id: invocationId,
@@ -8152,8 +8180,8 @@ function registerWorker(url) {
8152
8180
  function connect() {
8153
8181
  if (intentionalClose) return;
8154
8182
  ws = new WebSocket(url);
8155
- ready = new Promise((resolve14) => {
8156
- resolveReady = resolve14;
8183
+ ready = new Promise((resolve15) => {
8184
+ resolveReady = resolve15;
8157
8185
  });
8158
8186
  ws.onopen = () => {
8159
8187
  reconnectAttempt = 0;
@@ -8273,13 +8301,13 @@ function registerWorker(url) {
8273
8301
  }
8274
8302
  return Promise.resolve(fn(request.payload, ctx));
8275
8303
  }
8276
- return new Promise((resolve14, reject) => {
8304
+ return new Promise((resolve15, reject) => {
8277
8305
  const invocationId = genId();
8278
8306
  const timer = setTimeout(() => {
8279
8307
  pendingInvocations.delete(invocationId);
8280
8308
  reject(new Error(`Invocation timed out for "${request.function_id}"`));
8281
8309
  }, request.timeout_ms || 3e4);
8282
- pendingInvocations.set(invocationId, { resolve: resolve14, reject, timer });
8310
+ pendingInvocations.set(invocationId, { resolve: resolve15, reject, timer });
8283
8311
  send({
8284
8312
  type: "invoke",
8285
8313
  invocation_id: invocationId,
@@ -8300,142 +8328,70 @@ function registerWorker(url) {
8300
8328
  };
8301
8329
  }
8302
8330
 
8303
- // live.ts
8304
- import chokidar from "chokidar";
8305
- import { existsSync as existsSync6 } from "node:fs";
8306
- import { join as join7, resolve as resolve13 } from "node:path";
8307
- var clients2 = /* @__PURE__ */ new Set();
8308
- var hotBundleCache = /* @__PURE__ */ new Map();
8309
- var hotKeys = [];
8310
- var MAX_HOT = 10;
8311
- function setHot(hash, code) {
8312
- if (!hotBundleCache.has(hash)) {
8313
- hotKeys.push(hash);
8314
- if (hotKeys.length > MAX_HOT) {
8315
- const old = hotKeys.shift();
8316
- hotBundleCache.delete(old);
8317
- }
8318
- }
8319
- hotBundleCache.set(hash, code);
8320
- }
8321
- function broadcastReload() {
8322
- for (const ws of clients2) {
8323
- try {
8324
- ws.send("reload");
8325
- } catch {
8326
- clients2.delete(ws);
8327
- }
8328
- }
8329
- }
8330
- function broadcastCss(css) {
8331
- const msg = JSON.stringify({ type: "css", css });
8332
- for (const ws of clients2) {
8333
- try {
8334
- ws.send(msg);
8335
- } catch {
8336
- clients2.delete(ws);
8337
- }
8338
- }
8339
- }
8340
- function liveReload(opts) {
8341
- const r = new Router();
8342
- const entryPath = (() => {
8343
- for (const dir of opts.dirs) {
8344
- const p = join7(resolve13(dir), "page.tsx");
8345
- if (existsSync6(p)) return p;
8346
- }
8347
- return "";
8348
- })();
8349
- r.get("/__wfw/v/bundle", async (req, ctx) => {
8350
- const code = await compileVendorBundle();
8351
- return new Response(code, {
8352
- headers: { "content-type": "application/javascript; charset=utf-8" }
8353
- });
8354
- });
8355
- r.get("/__wfw/h/:hash", async (req, ctx) => {
8356
- const hash = ctx.params.hash.replace(/\.js$/i, "");
8357
- const code = hotBundleCache.get(hash);
8358
- if (!code) return new Response("", { status: 404 });
8359
- return new Response(code, {
8360
- headers: { "content-type": "application/javascript; charset=utf-8" }
8361
- });
8362
- });
8363
- r.ws("/__weifuwu/livereload", {
8364
- open(ws) {
8365
- clients2.add(ws);
8366
- ws.on("close", () => clients2.delete(ws));
8367
- ws.on("error", () => clients2.delete(ws));
8368
- }
8369
- });
8370
- const watcher = chokidar.watch(opts.dirs, {
8371
- ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
8372
- ignoreInitial: true
8373
- });
8374
- watcher.on("change", async (filePath) => {
8375
- if (/\.tsx?$/i.test(filePath)) {
8376
- clearCompileCache();
8377
- markClientBundleDirty();
8378
- try {
8379
- const target = entryPath || filePath;
8380
- await compileTsxDev(target);
8381
- const { hash, code } = await compileHotComponent(target);
8382
- setHot(hash, code);
8383
- let css;
8384
- for (const dir of opts.dirs) {
8385
- const cssPath = join7(resolve13(dir), "app.css");
8386
- if (existsSync6(cssPath)) {
8387
- css = await compileTailwindCss(cssPath, resolve13(dir));
8388
- }
8389
- }
8390
- const entry = entryPath ? id(entryPath) : "";
8391
- const msg = { type: "component", hash, entry };
8392
- if (css) msg.css = css;
8393
- const str = JSON.stringify(msg);
8394
- for (const ws of clients2) {
8395
- try {
8396
- ws.send(str);
8397
- } catch {
8398
- clients2.delete(ws);
8399
- }
8400
- }
8401
- } catch (e) {
8402
- console.error("live reload failed, fallback to full reload:", e);
8403
- broadcastReload();
8404
- }
8405
- } else if (/\.css$/i.test(filePath)) {
8406
- for (const dir of opts.dirs) {
8407
- const cssPath = join7(resolve13(dir), "app.css");
8408
- if (existsSync6(cssPath)) {
8409
- const css = await compileTailwindCss(cssPath, resolve13(dir));
8410
- if (css) broadcastCss(css);
8411
- }
8412
- }
8413
- }
8414
- });
8415
- r.close = () => {
8416
- watcher.close();
8417
- 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);
8418
8339
  };
8419
- return r;
8420
8340
  }
8421
8341
 
8422
8342
  // not-found.ts
8343
+ import { createElement as createElement2 } from "react";
8423
8344
  function notFound(path2) {
8424
8345
  return async (req, ctx) => {
8425
8346
  if (!path2) return new Response("Not Found", { status: 404 });
8426
- const mod = await compile(path2);
8427
- const Component = mod?.default;
8428
- const body = Component ? "404 - Not Found" : "404 - Not Found";
8429
- return new Response(body, {
8430
- status: 404,
8431
- 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
8432
8389
  });
8433
8390
  };
8434
8391
  }
8435
8392
 
8436
8393
  // error-boundary.ts
8437
- import { createElement as createElement2 } from "react";
8438
- import { TextEncoder as TextEncoder3 } from "node:util";
8394
+ import { createElement as createElement3 } from "react";
8439
8395
  function errorBoundary(errorPath) {
8440
8396
  return async (req, ctx, next) => {
8441
8397
  try {
@@ -8445,30 +8401,38 @@ function errorBoundary(errorPath) {
8445
8401
  const ErrorComponent = mod.default;
8446
8402
  if (!ErrorComponent) throw err;
8447
8403
  const layouts = (ctx.layoutStack || []).map((l) => l.component);
8448
- const stream = await import("react-dom/server").then((m) => m.renderToReadableStream(
8449
- createElement2(ErrorComponent, {
8450
- error: err instanceof Error ? err : new Error(String(err)),
8451
- reset: () => {
8452
- }
8453
- })
8454
- ));
8455
- const reader = stream.getReader();
8456
- const chunks = [];
8457
- while (true) {
8458
- const { done, value } = await reader.read();
8459
- if (done) break;
8460
- chunks.push(value);
8461
- }
8462
- const encoder2 = new TextEncoder3();
8463
- const body = chunks.reduce((acc, c) => {
8464
- const merged = new Uint8Array(acc.length + c.length);
8465
- merged.set(acc);
8466
- merged.set(c, acc.length);
8467
- return merged;
8468
- }, new Uint8Array(0));
8469
- return new Response(body, {
8470
- status: 500,
8471
- 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
8472
8436
  });
8473
8437
  }
8474
8438
  };
@@ -8505,7 +8469,6 @@ export {
8505
8469
  helmet,
8506
8470
  iii,
8507
8471
  layout,
8508
- liveReload,
8509
8472
  loadEnv,
8510
8473
  logdb,
8511
8474
  logger,
@@ -8521,6 +8484,7 @@ export {
8521
8484
  redis,
8522
8485
  registerWorker,
8523
8486
  requestId,
8487
+ rootLayout,
8524
8488
  runWorkflow,
8525
8489
  seo,
8526
8490
  seoMiddleware,
@@ -8532,7 +8496,6 @@ export {
8532
8496
  ssr,
8533
8497
  streamObject,
8534
8498
  streamText,
8535
- tailwind,
8536
8499
  tenant,
8537
8500
  tool2 as tool,
8538
8501
  upload,