weifuwu 0.17.3 → 0.17.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/README.md CHANGED
@@ -1146,6 +1146,56 @@ await deploy(config)
1146
1146
 
1147
1147
  ---
1148
1148
 
1149
+ ## Analytics
1150
+
1151
+ In-memory page view tracking with a built-in dashboard. Zero extra dependencies.
1152
+
1153
+ ```ts
1154
+ import { analytics } from 'weifuwu'
1155
+
1156
+ app.use(analytics()) // mounts middleware + /analytics + /__analytics/data
1157
+ ```
1158
+
1159
+ | Endpoint | Description |
1160
+ |----------|-------------|
1161
+ | `GET /analytics` | Dashboard — PV trend, top pages, referrers, device breakdown |
1162
+ | `GET /__analytics/data?days=7` | Raw JSON data for custom dashboards |
1163
+
1164
+ Excluded paths (not recorded): `/__analytics/*`, `__wfw/*`, `/static/*`, `/analytics`.
1165
+
1166
+ ### Dashboard
1167
+
1168
+ The built-in `/analytics` page renders a server-generated HTML dashboard with:
1169
+
1170
+ - **Summary cards** — total PV, unique pages, mobile/desktop ratio
1171
+ - **Bar chart** — daily page views for the selected period
1172
+ - **Top pages table** — most visited paths ranked by views
1173
+ - **Referrers table** — top referring domains
1174
+
1175
+ ### JSON API
1176
+
1177
+ ```ts
1178
+ // GET /__analytics/data?days=30
1179
+ {
1180
+ "total_pv": 2847,
1181
+ "total_uv": 1231,
1182
+ "daily": [{ "date": "2026-06-05", "pv": 520, "uv": 310 }],
1183
+ "top_pages": [{ "path": "/tools/uppercase", "pv": 1284 }],
1184
+ "referrers": [{ "domain": "google.com", "count": 380 }],
1185
+ "devices": { "mobile": 45.2, "desktop": 54.8 }
1186
+ }
1187
+ ```
1188
+
1189
+ ### Options
1190
+
1191
+ ```ts
1192
+ app.use(analytics({
1193
+ excluded: ['/admin', '/api'], // custom exclude patterns (defaults listed above)
1194
+ }))
1195
+ ```
1196
+
1197
+ ---
1198
+
1149
1199
  ## Health check
1150
1200
 
1151
1201
  ```ts
@@ -0,0 +1,8 @@
1
+ import { Router } from './router.ts';
2
+ export interface AnalyticsOptions {
3
+ pg?: {
4
+ sql: any;
5
+ };
6
+ excluded?: string[];
7
+ }
8
+ export declare function analytics(options?: AnalyticsOptions): Router;
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { serve, createTestServer } from './serve.ts';
4
4
  export type { ServeOptions, Server } from './serve.ts';
5
5
  export { Router } from './router.ts';
6
6
  export type { WebSocketHandler } from './router.ts';
7
- export { tsx, TsxContext, useCtx } from './tsx.ts';
7
+ export { tsx, TsxContext, useCtx, setCtx } from './tsx.ts';
8
8
  export type { TsxOptions } from './tsx.ts';
9
9
  export { auth, cors, logger } from './middleware.ts';
10
10
  export type { AuthOptions, CORSOptions, LoggerOptions } from './middleware.ts';
@@ -54,6 +54,8 @@ export { opencode } from './opencode/index.ts';
54
54
  export type { OpencodeOptions, OpencodeModule, SkillDef, OpencodePermissions, Session as OpencodeSession, } from './opencode/types.ts';
55
55
  export { health } from './health.ts';
56
56
  export type { HealthOptions } from './health.ts';
57
+ export { analytics } from './analytics.ts';
58
+ export type { AnalyticsOptions } from './analytics.ts';
57
59
  export { preferences } from './preferences.ts';
58
60
  export type { PrefOptions } from './preferences.ts';
59
61
  export { seo, seoMiddleware, seoTags } from './seo.ts';
package/dist/index.js CHANGED
@@ -578,26 +578,43 @@ import { createRequire } from "node:module";
578
578
  import chokidar from "chokidar";
579
579
 
580
580
  // tsx-context.ts
581
- import { createContext, useContext } from "react";
582
- var TsxContext = createContext({ params: {}, query: {} });
581
+ import { useSyncExternalStore, createContext } from "react";
582
+ var _ctx = { params: {}, query: {} };
583
+ var _listeners = /* @__PURE__ */ new Set();
584
+ function setCtx(value) {
585
+ _ctx = { ..._ctx, ...value };
586
+ _listeners.forEach((fn) => fn());
587
+ }
588
+ function _buildT() {
589
+ const messages2 = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
590
+ if (!messages2) return void 0;
591
+ return (key, params, fallback) => {
592
+ const msg = key.split(".").reduce((o, k) => o?.[k], messages2);
593
+ if (msg === void 0 || msg === null) return fallback ?? key;
594
+ if (!params) return String(msg);
595
+ let result = String(msg);
596
+ for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
597
+ return result;
598
+ };
599
+ }
583
600
  function useCtx() {
584
- const wc = typeof window !== "undefined" ? window.__WEIFUWU_CTX : globalThis.__WEIFUWU_CTX;
585
- if (wc) {
586
- const messages2 = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
587
- if (messages2 && typeof wc.t !== "function") {
588
- wc.t = (key, params) => {
589
- const msg = key.split(".").reduce((o, k) => o?.[k], messages2);
590
- if (msg === void 0 || msg === null) return key;
591
- if (!params) return String(msg);
592
- let result = String(msg);
593
- for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
594
- return result;
601
+ useSyncExternalStore(
602
+ (cb) => {
603
+ _listeners.add(cb);
604
+ return () => {
605
+ _listeners.delete(cb);
595
606
  };
596
- }
597
- return wc;
598
- }
599
- return useContext(TsxContext);
607
+ },
608
+ () => _ctx,
609
+ () => _ctx
610
+ );
611
+ const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
612
+ const t = data?.t ?? _ctx.t ?? _buildT();
613
+ const result = { ..._ctx, ...data };
614
+ if (t) result.t = t;
615
+ return result;
600
616
  }
617
+ var TsxContext = createContext({ params: {}, query: {} });
601
618
 
602
619
  // tsx-instance.ts
603
620
  var liveReloadClients = /* @__PURE__ */ new Set();
@@ -881,25 +898,23 @@ var TsxInstance = class {
881
898
  const nfMod = this.pageModules.get(nfPath);
882
899
  if (!nfMod) return new Response("Not Found", { status: 404 });
883
900
  const NfComponent = nfMod.default;
884
- let element = createElement(TsxContext.Provider, {
885
- value: {
886
- params: ctx.params,
887
- query: ctx.query,
888
- user: ctx.user,
889
- parsed: ctx.parsed,
890
- prefs: ctx.prefs,
891
- locale: ctx.locale,
892
- theme: ctx.theme,
893
- t: ctx.t,
894
- env: ctx.env
895
- }
896
- }, createElement(NfComponent, { params: ctx.params, query: ctx.query }));
901
+ setCtx({
902
+ params: ctx.params,
903
+ query: ctx.query,
904
+ user: ctx.user,
905
+ parsed: ctx.parsed,
906
+ prefs: ctx.prefs,
907
+ locale: ctx.locale,
908
+ theme: ctx.theme,
909
+ t: ctx.t,
910
+ env: ctx.env
911
+ });
912
+ let element = createElement(NfComponent, { params: ctx.params, query: ctx.query });
897
913
  for (let i = rootLayouts.length - 1; i >= 0; i--) {
898
914
  const LMod = this.layoutModules.get(rootLayouts[i]);
899
915
  if (!LMod) continue;
900
916
  element = createElement(LMod.default, { children: element });
901
917
  }
902
- setGlobalCtx(ctx);
903
918
  const stream = await renderToReadableStream(element);
904
919
  return streamResponse(stream, {
905
920
  ctx,
@@ -1054,22 +1069,21 @@ ${src}`;
1054
1069
  const loadFn = loadMod?.default;
1055
1070
  const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
1056
1071
  const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
1072
+ setCtx({
1073
+ params: ctx.params,
1074
+ query: ctx.query,
1075
+ user: ctx.user,
1076
+ parsed: ctx.parsed,
1077
+ prefs: ctx.prefs,
1078
+ locale: ctx.locale,
1079
+ theme: ctx.theme,
1080
+ t: ctx.t,
1081
+ env: ctx.env
1082
+ });
1057
1083
  let element = createElement(
1058
1084
  "div",
1059
1085
  { id: "__weifuwu_root" },
1060
- createElement(TsxContext.Provider, {
1061
- value: {
1062
- params: ctx.params,
1063
- query: ctx.query,
1064
- user: ctx.user,
1065
- parsed: ctx.parsed,
1066
- prefs: ctx.prefs,
1067
- locale: ctx.locale,
1068
- theme: ctx.theme,
1069
- t: ctx.t,
1070
- env: ctx.env
1071
- }
1072
- }, createElement(Component, allProps))
1086
+ createElement(Component, allProps)
1073
1087
  );
1074
1088
  if (layoutPaths.length === 0) {
1075
1089
  element = createElement(
@@ -1093,12 +1107,11 @@ ${src}`;
1093
1107
  const isRoot = i === 0;
1094
1108
  element = createElement(
1095
1109
  Layout,
1096
- isRoot ? { children: element, req, ctx } : { children: element }
1110
+ isRoot ? { children: element, req } : { children: element }
1097
1111
  );
1098
1112
  }
1099
1113
  }
1100
1114
  const bundle = await this.getOrBuildClientBundle(entryPath, layoutPaths, this.pagesDir);
1101
- setGlobalCtx(ctx);
1102
1115
  const stream = await renderToReadableStream(element);
1103
1116
  return streamResponse(stream, {
1104
1117
  ctx,
@@ -1272,18 +1285,6 @@ ${src}`;
1272
1285
  }
1273
1286
  }
1274
1287
  };
1275
- function setGlobalCtx(ctx) {
1276
- ;
1277
- globalThis.__WEIFUWU_CTX = {
1278
- params: ctx.params,
1279
- query: ctx.query,
1280
- user: ctx.user,
1281
- parsed: ctx.parsed,
1282
- prefs: ctx.prefs,
1283
- locale: ctx.locale,
1284
- theme: ctx.theme
1285
- };
1286
- }
1287
1288
  function streamResponse(reactStream, opts) {
1288
1289
  const decoder = new TextDecoder();
1289
1290
  const encoder2 = new TextEncoder();
@@ -3208,7 +3209,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3208
3209
  <body><h2>${error}</h2>${description ? `<p class="desc">${description}</p>` : ""}</body>
3209
3210
  </html>`, { status: 400, headers: { "Content-Type": "text/html; charset=utf-8" } });
3210
3211
  }
3211
- async function authorizeHandler(req, _ctx) {
3212
+ async function authorizeHandler(req, _ctx2) {
3212
3213
  const url = new URL(req.url);
3213
3214
  const clientId = url.searchParams.get("client_id") || "";
3214
3215
  const redirectUri = url.searchParams.get("redirect_uri") || "";
@@ -4593,23 +4594,23 @@ function buildGraphQLHandler(sql2) {
4593
4594
  });
4594
4595
  return Response.json(result, { status: result.errors ? 400 : 200 });
4595
4596
  });
4596
- r.get("/", async (req, _ctx) => {
4597
+ r.get("/", async (req, _ctx2) => {
4597
4598
  const url = new URL(req.url);
4598
4599
  if (url.searchParams.has("query")) {
4599
- return handleGET(req, _ctx);
4600
+ return handleGET(req, _ctx2);
4600
4601
  }
4601
4602
  return new Response("GraphQL endpoint. Send POST /graphql with { query, variables }", {
4602
4603
  status: 200,
4603
4604
  headers: { "Content-Type": "text/plain" }
4604
4605
  });
4605
4606
  });
4606
- async function handleGET(req, _ctx) {
4607
+ async function handleGET(req, _ctx2) {
4607
4608
  const tables = await sql2`
4608
4609
  SELECT * FROM "_user_tables"
4609
- WHERE tenant_id = ${_ctx.tenant.id}
4610
+ WHERE tenant_id = ${_ctx2.tenant.id}
4610
4611
  ORDER BY created_at ASC
4611
4612
  `;
4612
- const buildCtx = { sql: sql2, tenantId: _ctx.tenant.id, tables };
4613
+ const buildCtx = { sql: sql2, tenantId: _ctx2.tenant.id, tables };
4613
4614
  const schema = new GraphQLSchema({
4614
4615
  query: new GraphQLObjectType({
4615
4616
  name: "Query",
@@ -4629,7 +4630,7 @@ function buildGraphQLHandler(sql2) {
4629
4630
  source: query,
4630
4631
  variableValues: variables,
4631
4632
  operationName: url.searchParams.get("operationName") || void 0,
4632
- contextValue: _ctx
4633
+ contextValue: _ctx2
4633
4634
  });
4634
4635
  return Response.json(result, { status: result.errors ? 400 : 200 });
4635
4636
  }
@@ -6633,7 +6634,7 @@ function createWSHandler2(deps) {
6633
6634
  clients.delete(ws);
6634
6635
  }
6635
6636
  },
6636
- error(ws, _ctx, _err) {
6637
+ error(ws, _ctx2, _err) {
6637
6638
  const client = clients.get(ws);
6638
6639
  if (client) {
6639
6640
  client.abortController?.abort();
@@ -6797,6 +6798,179 @@ function health(options) {
6797
6798
  return r;
6798
6799
  }
6799
6800
 
6801
+ // analytics.ts
6802
+ var DEFAULT_EXCLUDED = ["/__analytics", "/__wfw", "/static", "/analytics"];
6803
+ var MemStore = class {
6804
+ days = /* @__PURE__ */ new Map();
6805
+ pages = /* @__PURE__ */ new Map();
6806
+ refs = /* @__PURE__ */ new Map();
6807
+ record(path2, date, refDomain, mobile) {
6808
+ let day = this.days.get(date);
6809
+ if (!day) {
6810
+ day = { pv: 0, uv: /* @__PURE__ */ new Set(), mobile: 0, desktop: 0 };
6811
+ this.days.set(date, day);
6812
+ }
6813
+ day.pv++;
6814
+ day.uv.add(path2);
6815
+ if (mobile) {
6816
+ day.mobile++;
6817
+ } else {
6818
+ day.desktop++;
6819
+ }
6820
+ let page = this.pages.get(path2);
6821
+ if (!page) {
6822
+ page = { count: 0, dates: /* @__PURE__ */ new Set() };
6823
+ this.pages.set(path2, page);
6824
+ }
6825
+ page.count++;
6826
+ page.dates.add(date);
6827
+ if (refDomain) {
6828
+ let refs = this.refs.get(date);
6829
+ if (!refs) {
6830
+ refs = /* @__PURE__ */ new Map();
6831
+ this.refs.set(date, refs);
6832
+ }
6833
+ refs.set(refDomain, (refs.get(refDomain) || 0) + 1);
6834
+ }
6835
+ }
6836
+ query(days) {
6837
+ const since = /* @__PURE__ */ new Date();
6838
+ since.setDate(since.getDate() - days);
6839
+ const sinceStr = since.toISOString().slice(0, 10);
6840
+ const daily = [];
6841
+ let totalPv = 0;
6842
+ let totalMobile = 0;
6843
+ let totalDesktop = 0;
6844
+ const pageMap = /* @__PURE__ */ new Map();
6845
+ const allUv = /* @__PURE__ */ new Set();
6846
+ for (const [date, day] of this.days) {
6847
+ if (date < sinceStr) continue;
6848
+ daily.push({ date, pv: day.pv, uv: day.uv.size });
6849
+ totalPv += day.pv;
6850
+ totalMobile += day.mobile;
6851
+ totalDesktop += day.desktop;
6852
+ for (const p of day.uv) allUv.add(p);
6853
+ }
6854
+ for (const [path2, page] of this.pages) {
6855
+ let count = 0;
6856
+ for (const d of page.dates) if (d >= sinceStr) count += page.count / page.dates.size;
6857
+ pageMap.set(path2, page.count);
6858
+ }
6859
+ const topPages = [...pageMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([path2, count]) => ({ path: path2, pv: count }));
6860
+ const refMap = /* @__PURE__ */ new Map();
6861
+ for (const [date, refs] of this.refs) {
6862
+ if (date < sinceStr) continue;
6863
+ for (const [domain, count] of refs) refMap.set(domain, (refMap.get(domain) || 0) + count);
6864
+ }
6865
+ const referrers = [...refMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count }));
6866
+ const total = totalMobile + totalDesktop || 1;
6867
+ return {
6868
+ total_pv: totalPv,
6869
+ total_uv: allUv.size,
6870
+ daily,
6871
+ top_pages: topPages,
6872
+ referrers,
6873
+ devices: { mobile: Math.round(totalMobile / total * 1e3) / 10, desktop: Math.round(totalDesktop / total * 1e3) / 10 }
6874
+ };
6875
+ }
6876
+ };
6877
+ function analytics(options) {
6878
+ const excluded = options?.excluded ?? DEFAULT_EXCLUDED;
6879
+ const store = new MemStore();
6880
+ function shouldExclude(path2) {
6881
+ return excluded.some((e) => path2.startsWith(e));
6882
+ }
6883
+ const middleware = async (req, ctx, next) => {
6884
+ const url = new URL(req.url);
6885
+ const path2 = url.pathname;
6886
+ if (!shouldExclude(path2)) {
6887
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6888
+ const ref = req.headers.get("referer") || "";
6889
+ const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
6890
+ const ua = req.headers.get("user-agent") || "";
6891
+ const mobile = /mobile|android|iphone|ipad/i.test(ua);
6892
+ store.record(path2, date, refDomain, mobile);
6893
+ }
6894
+ return next(req, ctx);
6895
+ };
6896
+ const router = new Router();
6897
+ router.use(middleware);
6898
+ router.get("/__analytics/data", async (req) => {
6899
+ const url = new URL(req.url);
6900
+ const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 7, 1), 365);
6901
+ return Response.json(store.query(days));
6902
+ });
6903
+ router.get("/analytics", async (req) => {
6904
+ const url = new URL(req.url);
6905
+ const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 7, 1), 365);
6906
+ const data = store.query(days);
6907
+ return new Response(renderDashboard(days, data), {
6908
+ headers: { "content-type": "text/html; charset=utf-8" }
6909
+ });
6910
+ });
6911
+ return router;
6912
+ }
6913
+ function renderDashboard(days, data) {
6914
+ const { total_pv, total_uv, daily, top_pages, referrers, devices } = data;
6915
+ const maxPv = Math.max(...daily.map((d) => d.pv), 1);
6916
+ const bars = daily.map(
6917
+ (d) => `<div class="bar-wrap"><div class="bar" style="height:${d.pv / maxPv * 100}%"></div><span class="bar-label">${d.date.slice(5)}</span></div>`
6918
+ ).join("");
6919
+ const rows = top_pages.map(
6920
+ (p, i) => `<tr><td class="num">${i + 1}</td><td class="path">${p.path}</td><td class="num">${p.pv}</td></tr>`
6921
+ ).join("");
6922
+ const refRows = referrers.map(
6923
+ (r) => `<tr><td>${r.domain}</td><td class="num">${r.count}</td></tr>`
6924
+ ).join("");
6925
+ return `<!DOCTYPE html>
6926
+ <html lang="en">
6927
+ <head>
6928
+ <meta charset="utf-8"/>
6929
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
6930
+ <title>Analytics - weifuwu</title>
6931
+ <style>
6932
+ *,:before,:after{box-sizing:border-box;margin:0;padding:0}
6933
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8f9fa;color:#333;padding:24px;max-width:960px;margin:0 auto}
6934
+ h1{font-size:24px;font-weight:700;margin-bottom:24px}
6935
+ .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:24px}
6936
+ .card{background:#fff;border-radius:10px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08)}
6937
+ .card .val{font-size:28px;font-weight:700;color:#2563eb}
6938
+ .card .lbl{font-size:12px;color:#888;margin-top:4px}
6939
+ .section{background:#fff;border-radius:10px;padding:20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.08)}
6940
+ .section h2{font-size:14px;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:.05em;margin-bottom:16px}
6941
+ .chart{display:flex;align-items:flex-end;gap:4px;height:160px;padding-top:8px}
6942
+ .bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end}
6943
+ .bar{width:100%;background:#2563eb;border-radius:4px 4px 0 0;min-height:2px;transition:height .3s}
6944
+ .bar-label{font-size:10px;color:#888;margin-top:6px;white-space:nowrap}
6945
+ table{width:100%;border-collapse:collapse;font-size:13px}
6946
+ th{text-align:left;padding:6px 8px;color:#888;font-weight:500;border-bottom:1px solid #eee}
6947
+ td{padding:6px 8px;border-bottom:1px solid #f0f0f0}
6948
+ .num{text-align:right;font-variant-numeric:tabular-nums}
6949
+ .path{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px}
6950
+ tr:hover td{background:#f8faff}
6951
+ </style>
6952
+ </head>
6953
+ <body>
6954
+ <h1>Analytics</h1>
6955
+ <div class="cards">
6956
+ <div class="card"><div class="val">${total_pv}</div><div class="lbl">Page Views (${days}d)</div></div>
6957
+ <div class="card"><div class="val">${total_uv}</div><div class="lbl">Unique Pages</div></div>
6958
+ <div class="card"><div class="val">${devices.mobile}%</div><div class="lbl">Mobile</div></div>
6959
+ <div class="card"><div class="val">${devices.desktop}%</div><div class="lbl">Desktop</div></div>
6960
+ </div>
6961
+ <div class="section">
6962
+ <h2>Daily Page Views</h2>
6963
+ <div class="chart">${bars}</div>
6964
+ </div>
6965
+ <div class="section">
6966
+ <h2>Top Pages</h2>
6967
+ <table><thead><tr><th style="width:32px">#</th><th>Path</th><th style="width:64px">Views</th></tr></thead><tbody>${rows}</tbody></table>
6968
+ </div>
6969
+ ${referrers.length ? `<div class="section"><h2>Referrers</h2><table><thead><tr><th>Domain</th><th style="width:64px">Views</th></tr></thead><tbody>${refRows}</tbody></table></div>` : ""}
6970
+ </body>
6971
+ </html>`;
6972
+ }
6973
+
6800
6974
  // preferences.ts
6801
6975
  import { readFile as readFile2 } from "node:fs/promises";
6802
6976
  import { existsSync as existsSync4 } from "node:fs";
@@ -6805,9 +6979,9 @@ var defaults = {
6805
6979
  locale: { default: "en", cookie: "locale", fromAcceptLanguage: true },
6806
6980
  theme: { default: "system", cookie: "theme" }
6807
6981
  };
6808
- function translate(msgs, key, params) {
6982
+ function translate(msgs, key, params, fallback) {
6809
6983
  const msg = key.split(".").reduce((o, k) => o?.[k], msgs);
6810
- if (msg === void 0 || msg === null) return key;
6984
+ if (msg === void 0 || msg === null) return fallback ?? key;
6811
6985
  if (!params) return String(msg);
6812
6986
  let result = String(msg);
6813
6987
  for (const [k, v] of Object.entries(params)) {
@@ -6852,7 +7026,7 @@ function preferences(options) {
6852
7026
  ctx.theme = theme;
6853
7027
  if (dir) {
6854
7028
  const msgs = await load(locale);
6855
- ctx.t = (key, params) => translate(msgs, key, params);
7029
+ ctx.t = (key, params, fallback) => translate(msgs, key, params, fallback);
6856
7030
  globalThis.__LOCALE_DATA__ = msgs;
6857
7031
  }
6858
7032
  ctx.setPref = (name, value) => {
@@ -7659,7 +7833,7 @@ function createWsHandler(deps) {
7659
7833
  return wsToWorkerId.get(ws) || "";
7660
7834
  }
7661
7835
  return {
7662
- open(_ws, _ctx) {
7836
+ open(_ws, _ctx2) {
7663
7837
  },
7664
7838
  async message(ws, ctx, data) {
7665
7839
  let msg;
@@ -8271,6 +8445,7 @@ export {
8271
8445
  TsxContext,
8272
8446
  agent,
8273
8447
  aiStream,
8448
+ analytics,
8274
8449
  auth,
8275
8450
  compress,
8276
8451
  cors,
@@ -8315,6 +8490,7 @@ export {
8315
8490
  serve,
8316
8491
  serveStatic,
8317
8492
  setCookie,
8493
+ setCtx,
8318
8494
  smoothStream,
8319
8495
  streamObject,
8320
8496
  streamText,
package/dist/react.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { UseWebsocketOptions, UseWebsocketReturn } from './use-websocket.ts
3
3
  export { useAction } from './use-action.ts';
4
4
  export type { UseActionOptions, UseActionReturn } from './use-action.ts';
5
5
  export { Link, useNavigate, navigate, useNavigating } from './client-router.ts';
6
- export { TsxContext, useCtx } from './tsx-context.ts';
6
+ export { TsxContext, useCtx, setCtx } from './tsx-context.ts';
7
7
  export { Head } from './head.tsx';
8
8
  export { createStore, useData, useQueryState } from './client-state.ts';
9
9
  export type { StoreApi } from './client-state.ts';
package/dist/react.js CHANGED
@@ -316,26 +316,43 @@ async function prefetchPage(href) {
316
316
  }
317
317
 
318
318
  // tsx-context.ts
319
- import { createContext, useContext } from "react";
320
- var TsxContext = createContext({ params: {}, query: {} });
319
+ import { useSyncExternalStore, createContext } from "react";
320
+ var _ctx = { params: {}, query: {} };
321
+ var _listeners2 = /* @__PURE__ */ new Set();
322
+ function setCtx(value) {
323
+ _ctx = { ..._ctx, ...value };
324
+ _listeners2.forEach((fn) => fn());
325
+ }
326
+ function _buildT() {
327
+ const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
328
+ if (!messages) return void 0;
329
+ return (key, params, fallback) => {
330
+ const msg = key.split(".").reduce((o, k) => o?.[k], messages);
331
+ if (msg === void 0 || msg === null) return fallback ?? key;
332
+ if (!params) return String(msg);
333
+ let result = String(msg);
334
+ for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
335
+ return result;
336
+ };
337
+ }
321
338
  function useCtx() {
322
- const wc = typeof window !== "undefined" ? window.__WEIFUWU_CTX : globalThis.__WEIFUWU_CTX;
323
- if (wc) {
324
- const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
325
- if (messages && typeof wc.t !== "function") {
326
- wc.t = (key, params) => {
327
- const msg = key.split(".").reduce((o, k) => o?.[k], messages);
328
- if (msg === void 0 || msg === null) return key;
329
- if (!params) return String(msg);
330
- let result = String(msg);
331
- for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
332
- return result;
339
+ useSyncExternalStore(
340
+ (cb) => {
341
+ _listeners2.add(cb);
342
+ return () => {
343
+ _listeners2.delete(cb);
333
344
  };
334
- }
335
- return wc;
336
- }
337
- return useContext(TsxContext);
345
+ },
346
+ () => _ctx,
347
+ () => _ctx
348
+ );
349
+ const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
350
+ const t = data?.t ?? _ctx.t ?? _buildT();
351
+ const result = { ..._ctx, ...data };
352
+ if (t) result.t = t;
353
+ return result;
338
354
  }
355
+ var TsxContext = createContext({ params: {}, query: {} });
339
356
 
340
357
  // head.tsx
341
358
  import { createElement as createElement2 } from "react";
@@ -344,7 +361,7 @@ function Head({ children }) {
344
361
  }
345
362
 
346
363
  // client-state.ts
347
- import { useSyncExternalStore, useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
364
+ import { useSyncExternalStore as useSyncExternalStore2, useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
348
365
  function createStore(initial) {
349
366
  let state = { ...initial };
350
367
  const listeners = /* @__PURE__ */ new Set();
@@ -360,7 +377,7 @@ function createStore(initial) {
360
377
  listeners.delete(listener);
361
378
  };
362
379
  };
363
- const useStore = ((selector) => useSyncExternalStore(
380
+ const useStore = ((selector) => useSyncExternalStore2(
364
381
  subscribe,
365
382
  () => selector ? selector(state) : state
366
383
  ));
@@ -451,7 +468,7 @@ function useQueryState(key, defaultValue = "") {
451
468
  const params = new URLSearchParams(window.location.search);
452
469
  return params.get(key) ?? defaultValue;
453
470
  }
454
- const value = useSyncExternalStore(
471
+ const value = useSyncExternalStore2(
455
472
  (cb) => {
456
473
  if (typeof window === "undefined") return () => {
457
474
  };
@@ -481,6 +498,7 @@ export {
481
498
  TsxContext,
482
499
  createStore,
483
500
  navigate,
501
+ setCtx,
484
502
  useAction,
485
503
  useCtx,
486
504
  useData,
@@ -1,4 +1,4 @@
1
- export declare const TsxContext: import("react").Context<{
1
+ export interface CtxValue {
2
2
  params: Record<string, string>;
3
3
  query: Record<string, string>;
4
4
  user?: unknown;
@@ -6,7 +6,9 @@ export declare const TsxContext: import("react").Context<{
6
6
  prefs?: Record<string, string>;
7
7
  locale?: string;
8
8
  theme?: string;
9
- t?: (key: string, params?: Record<string, string>) => string;
9
+ t?: (key: string, params?: Record<string, string>, fallback?: string) => string;
10
10
  env?: Record<string, string>;
11
- }>;
12
- export declare function useCtx(): any;
11
+ }
12
+ export declare function setCtx(value: Partial<CtxValue>): void;
13
+ export declare function useCtx(): CtxValue;
14
+ export declare const TsxContext: import("react").Context<CtxValue>;
@@ -1,6 +1,6 @@
1
1
  import { Router } from './router.ts';
2
- import { TsxContext, useCtx } from './tsx-context.ts';
3
- export { TsxContext, useCtx };
2
+ import { TsxContext, useCtx, setCtx } from './tsx-context.ts';
3
+ export { TsxContext, useCtx, setCtx };
4
4
  export interface TsxOptions {
5
5
  dir: string;
6
6
  }
package/dist/tsx.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { TsxContext, useCtx } from './tsx-instance.ts';
1
+ import { TsxContext, useCtx, setCtx } from './tsx-instance.ts';
2
2
  import type { TsxOptions } from './tsx-instance.ts';
3
3
  import type { Router } from './router.ts';
4
- export { TsxContext, useCtx };
4
+ export { TsxContext, useCtx, setCtx };
5
5
  export type { TsxOptions };
6
6
  export declare function tsx(options: TsxOptions): Promise<Router>;
package/dist/types.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface Context {
5
5
  parsed?: Record<string, unknown>;
6
6
  mountPath?: string;
7
7
  locale?: string;
8
- t?: (key: string, params?: Record<string, string>) => string;
8
+ t?: (key: string, params?: Record<string, string>, fallback?: string) => string;
9
9
  requestId?: string;
10
10
  prefs?: Record<string, string>;
11
11
  theme?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.17.3",
3
+ "version": "0.17.5",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",