weifuwu 0.17.7 → 0.17.8

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
@@ -58,7 +58,7 @@ app.use('/', await tsx({ dir: './ui' }))
58
58
  serve(app.handler(), { port: 3000 })
59
59
  ```
60
60
 
61
- Your tsx pages can use it directly:
61
+ Your tsx pages can use it directly, and locale/theme switching routes (`/__lang/zh`, `/__theme/dark`) work automatically without extra routes:
62
62
 
63
63
  ```tsx
64
64
  import { Head, useCtx, useData, createStore } from 'weifuwu/react'
@@ -556,6 +556,8 @@ function Nav() {
556
556
 
557
557
  `navigate(href)` fetches the target via SSR, extracts `__weifuwu_root` content and `__WEIFUWU_PROPS`, replaces in-place, then imports the new hydration bundle. `load.ts` runs on the server for every navigation. Initial load is full SSR; subsequent navigations are client-side.
558
558
 
559
+ Language and theme switching URLs (`/__lang/:locale`, `/__theme/:theme`) are handled by `navigate()` without page reload — the preference cookie is set via a JSON request and the context is updated via `setCtx()` (see [Preferences](#preferences)).
560
+
559
561
  - `<Link prefetch>` — pre-fetches page data on hover / when entering viewport (200px margin)
560
562
  - `useNavigating()` — reactive boolean, `true` while navigation is in-flight
561
563
  - `isNavigating()` / `onNavigate(fn)` — non-hook alternatives
@@ -1242,6 +1244,56 @@ Flash messages: set via `ctx.setPref('flash', ...)` → auto-read from cookie
1242
1244
  `ctx.t()` supports dot-path nested keys: `t('tools.uppercase.title')` traverses the JSON structure.
1243
1245
  `ctx.env` exposes `WEIFUWU_PUBLIC_*` environment variables on both server and client (via `useCtx().env`).
1244
1246
 
1247
+ ### Language / Theme switching
1248
+
1249
+ The middleware automatically handles `/__lang/:locale` and `/__theme/:theme` without needing extra routes:
1250
+
1251
+ ```ts
1252
+ app.use(preferences({ dir: './locales' }))
1253
+ // ✓ /__lang/zh, /__lang/en, /__theme/dark, /__theme/light automatically work
1254
+ ```
1255
+
1256
+ **Server behavior:**
1257
+
1258
+ | Mode | Request | Response |
1259
+ |------|---------|----------|
1260
+ | Redirect | `GET /__lang/zh` (browser) | 302 with `Set-Cookie: locale=zh` back to referer |
1261
+ | JSON | `GET /__lang/zh` + `Accept: application/json` | `{ ok: true, locale: 'zh', messages: { ... } }` + `Set-Cookie` |
1262
+
1263
+ **Client-side no-refresh switching:**
1264
+
1265
+ ```tsx
1266
+ import { Link } from 'weifuwu/react'
1267
+
1268
+ <Link href="/__lang/zh">中文</Link> // Instant switch — no page reload
1269
+ <Link href="/__lang/en">English</Link> // Instant switch — no page reload
1270
+ <Link href="/__theme/dark">🌙</Link> // Instant switch — no page reload
1271
+ ```
1272
+
1273
+ When using `<Link>` (or `navigate()`), the router intercepts `__lang`/`__theme` URLs:
1274
+ 1. Fetches the endpoint with `Accept: application/json`
1275
+ 2. Updates `window.__WEIFUWU_CTX` (locale, theme, messages)
1276
+ 3. Calls `setCtx()` — React re-renders via `useSyncExternalStore`
1277
+ 4. Page content stays intact, only translations/theme update
1278
+
1279
+ Plain `<a href="/__lang/zh">` still works via traditional 302 redirect for full backward compatibility.
1280
+
1281
+ ### Options
1282
+
1283
+ ```ts
1284
+ app.use(preferences({
1285
+ dir: './locales', // translation JSON directory
1286
+ locale: {
1287
+ default: 'en', // fallback when no cookie or Accept-Language
1288
+ cookie: 'locale', // cookie name (default: 'locale')
1289
+ fromAcceptLanguage: true, // detect from Accept-Language header
1290
+ },
1291
+ theme: {
1292
+ default: 'system', // 'light' | 'dark' | 'system'
1293
+ cookie: 'theme', // cookie name (default: 'theme')
1294
+ },
1295
+ }))
1296
+
1245
1297
  ---
1246
1298
 
1247
1299
  ## Email
@@ -1290,6 +1342,7 @@ app.get('/stream', (req, ctx) => createSSEStream(events()))
1290
1342
  | Hook / Component | Description |
1291
1343
  |-----------------|-------------|
1292
1344
  | `useCtx()` | Unified context — `{ prefs, locale, theme, t, params, query, env }` (requires `preferences` middleware) |
1345
+ | `setCtx(value)` | Update context — triggers re-render in all `useCtx()` consumers |
1293
1346
  | `createStore(initial)` | Zustand-compatible shared state — `getState`, `setState`, `subscribe` |
1294
1347
  | `useData(url, opts?)` | SWR-style data fetching — cache, dedup, mutate, fallback |
1295
1348
  | `useQueryState(key, default)` | URL query param sync — `?page=1` via `useSyncExternalStore` |
@@ -1,11 +1,16 @@
1
- import type { Handler, Middleware } from './types.ts';
1
+ import type { Middleware } from './types.ts';
2
+ import { Router } from './router.ts';
2
3
  export interface AnalyticsOptions {
3
4
  excluded?: string[];
4
5
  pg?: {
5
6
  sql: (strings: TemplateStringsArray, ...values: any[]) => Promise<any[]>;
7
+ table: (name: string, cols: any) => any;
6
8
  };
7
9
  }
8
- export declare function analytics(options?: AnalyticsOptions): {
9
- middleware: Middleware;
10
- handler: Handler;
11
- };
10
+ export interface AnalyticsModule {
11
+ middleware: () => Middleware;
12
+ router: () => Router;
13
+ migrate: () => Promise<void>;
14
+ close: () => Promise<void>;
15
+ }
16
+ export declare function analytics(options?: AnalyticsOptions): AnalyticsModule;
@@ -9,6 +9,7 @@ interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>,
9
9
  prefetch?: boolean;
10
10
  }
11
11
  export declare function Link({ href, children, onClick, prefetch, ...props }: LinkProps): import("react").DetailedReactHTMLElement<{
12
+ id?: string | undefined | undefined;
12
13
  slot?: string | undefined | undefined;
13
14
  style?: import("react").CSSProperties | undefined;
14
15
  title?: string | undefined | undefined;
@@ -34,7 +35,6 @@ export declare function Link({ href, children, onClick, prefetch, ...props }: Li
34
35
  draggable?: (boolean | "true" | "false") | undefined;
35
36
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
36
37
  hidden?: boolean | undefined | undefined;
37
- id?: string | undefined | undefined;
38
38
  lang?: string | undefined | undefined;
39
39
  nonce?: string | undefined | undefined;
40
40
  spellCheck?: (boolean | "true" | "false") | undefined;
package/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ export type { OpencodeOptions, OpencodeModule, SkillDef, OpencodePermissions, Se
55
55
  export { health } from './health.ts';
56
56
  export type { HealthOptions } from './health.ts';
57
57
  export { analytics } from './analytics.ts';
58
- export type { AnalyticsOptions } from './analytics.ts';
58
+ export type { AnalyticsOptions, AnalyticsModule } from './analytics.ts';
59
59
  export { preferences } from './preferences.ts';
60
60
  export type { PrefOptions } from './preferences.ts';
61
61
  export { seo, seoMiddleware, seoTags } from './seo.ts';
package/dist/index.js CHANGED
@@ -6812,11 +6812,8 @@ var MemStore = class {
6812
6812
  }
6813
6813
  day.pv++;
6814
6814
  day.uv.add(path2);
6815
- if (mobile) {
6816
- day.mobile++;
6817
- } else {
6818
- day.desktop++;
6819
- }
6815
+ if (mobile) day.mobile++;
6816
+ else day.desktop++;
6820
6817
  let page = this.pages.get(path2);
6821
6818
  if (!page) {
6822
6819
  page = { count: 0 };
@@ -6837,11 +6834,8 @@ var MemStore = class {
6837
6834
  since.setDate(since.getDate() - days);
6838
6835
  const sinceStr = since.toISOString().slice(0, 10);
6839
6836
  const daily = [];
6840
- let totalPv = 0;
6841
- let totalMobile = 0;
6842
- let totalDesktop = 0;
6843
- const pageMap = /* @__PURE__ */ new Map();
6844
- const allUv = /* @__PURE__ */ new Set();
6837
+ let totalPv = 0, totalMobile = 0, totalDesktop = 0;
6838
+ const allUv = /* @__PURE__ */ new Set(), pageMap = /* @__PURE__ */ new Map();
6845
6839
  for (const [date, day] of this.days) {
6846
6840
  if (date < sinceStr) continue;
6847
6841
  daily.push({ date, pv: day.pv, uv: day.uv.size });
@@ -6850,64 +6844,80 @@ var MemStore = class {
6850
6844
  totalDesktop += day.desktop;
6851
6845
  for (const p of day.uv) allUv.add(p);
6852
6846
  }
6853
- for (const [path2, page] of this.pages) {
6854
- pageMap.set(path2, page.count);
6855
- }
6847
+ for (const [path2, page] of this.pages) pageMap.set(path2, page.count);
6856
6848
  const topPages = [...pageMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([path2, count]) => ({ path: path2, pv: count }));
6857
6849
  const refMap = /* @__PURE__ */ new Map();
6858
6850
  for (const [date, refs] of this.refs) {
6859
6851
  if (date < sinceStr) continue;
6860
6852
  for (const [domain, count] of refs) refMap.set(domain, (refMap.get(domain) || 0) + count);
6861
6853
  }
6862
- const referrers = [...refMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count }));
6863
6854
  const total = totalMobile + totalDesktop || 1;
6864
6855
  return {
6865
6856
  total_pv: totalPv,
6866
6857
  total_uv: allUv.size,
6867
6858
  daily,
6868
6859
  top_pages: topPages,
6869
- referrers,
6860
+ referrers: [...refMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count })),
6870
6861
  devices: { mobile: Math.round(totalMobile / total * 1e3) / 10, desktop: Math.round(totalDesktop / total * 1e3) / 10 }
6871
6862
  };
6872
6863
  }
6873
6864
  };
6865
+ async function migratePg(sql2, table) {
6866
+ const analytics2 = table("__analytics", {
6867
+ date: text("date").notNull(),
6868
+ path: text("path").notNull(),
6869
+ count: integer("count").default(0),
6870
+ mobile: integer("mobile").default(0),
6871
+ desktop: integer("desktop").default(0)
6872
+ });
6873
+ await analytics2.create();
6874
+ await analytics2.createIndex(["date", "path"], { unique: true });
6875
+ return analytics2;
6876
+ }
6877
+ async function recordPg(sql2, path2, date, mobile) {
6878
+ await sql2`
6879
+ INSERT INTO __analytics (date, path, count, mobile, desktop)
6880
+ VALUES (${date}, ${path2}, 1, ${mobile ? 1 : 0}, ${mobile ? 0 : 1})
6881
+ ON CONFLICT (date, path) DO UPDATE SET
6882
+ count = __analytics.count + 1,
6883
+ mobile = __analytics.mobile + ${mobile ? 1 : 0},
6884
+ desktop = __analytics.desktop + ${mobile ? 0 : 1}
6885
+ `;
6886
+ }
6874
6887
  async function queryPg(sql2, days) {
6875
6888
  const since = /* @__PURE__ */ new Date();
6876
6889
  since.setDate(since.getDate() - days);
6890
+ const sinceStr = since.toISOString().slice(0, 10);
6877
6891
  const daily = await sql2`
6878
- SELECT date, SUM(count) as pv, COUNT(DISTINCT path) as uv
6879
- FROM __analytics WHERE date >= ${since.toISOString().slice(0, 10)}
6880
- GROUP BY date ORDER BY date
6892
+ SELECT date, SUM(count)::int as pv, COUNT(DISTINCT path)::int as uv
6893
+ FROM __analytics WHERE date >= ${sinceStr} GROUP BY date ORDER BY date
6881
6894
  `;
6882
6895
  const pageRows = await sql2`
6883
- SELECT path, SUM(count) as pv
6884
- FROM __analytics WHERE date >= ${since.toISOString().slice(0, 10)}
6896
+ SELECT path, SUM(count)::int as pv
6897
+ FROM __analytics WHERE date >= ${sinceStr}
6885
6898
  GROUP BY path ORDER BY pv DESC LIMIT 20
6886
6899
  `;
6887
6900
  const totalRes = await sql2`
6888
- SELECT COALESCE(SUM(count), 0) as total_pv,
6889
- COALESCE(SUM(mobile), 0) as total_mobile,
6890
- COALESCE(SUM(desktop), 0) as total_desktop
6891
- FROM __analytics WHERE date >= ${since.toISOString().slice(0, 10)}
6901
+ SELECT COALESCE(SUM(count), 0)::int as total_pv,
6902
+ COALESCE(SUM(mobile), 0)::int as total_mobile,
6903
+ COALESCE(SUM(desktop), 0)::int as total_desktop
6904
+ FROM __analytics WHERE date >= ${sinceStr}
6892
6905
  `;
6893
- const total = totalRes[0];
6894
- const totalMobileDesktop = total.total_mobile + total.total_desktop || 1;
6906
+ const t = totalRes[0];
6907
+ const denom = t.total_mobile + t.total_desktop || 1;
6895
6908
  return {
6896
- total_pv: total.total_pv,
6909
+ total_pv: t.total_pv,
6897
6910
  total_uv: pageRows.length,
6898
- daily: daily.map((d) => ({ date: d.date, pv: Number(d.pv), uv: Number(d.uv) })),
6899
- top_pages: pageRows.map((p) => ({ path: p.path, pv: Number(p.pv) })),
6911
+ daily: daily.map((d) => ({ date: d.date, pv: d.pv, uv: d.uv })),
6912
+ top_pages: pageRows.map((p) => ({ path: p.path, pv: p.pv })),
6900
6913
  referrers: [],
6901
- devices: {
6902
- mobile: Math.round(total.total_mobile / totalMobileDesktop * 1e3) / 10,
6903
- desktop: Math.round(total.total_desktop / totalMobileDesktop * 1e3) / 10
6904
- }
6914
+ devices: { mobile: Math.round(t.total_mobile / denom * 1e3) / 10, desktop: Math.round(t.total_desktop / denom * 1e3) / 10 }
6905
6915
  };
6906
6916
  }
6907
6917
  function renderDashboard(days, data) {
6908
- const { total_pv, total_uv, daily, top_pages, referrers } = data;
6909
- const maxPv = Math.max(...daily.map((d) => d.pv), 1);
6910
- const bars = daily.map(
6918
+ const { total_pv, total_uv, top_pages, referrers } = data;
6919
+ const maxPv = Math.max(...data.daily.map((d) => d.pv), 1);
6920
+ const bars = data.daily.map(
6911
6921
  (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>`
6912
6922
  ).join("");
6913
6923
  const rows = top_pages.map(
@@ -6916,11 +6926,9 @@ function renderDashboard(days, data) {
6916
6926
  const refRows = referrers.map(
6917
6927
  (r) => `<tr><td>${r.domain}</td><td class="num">${r.count}</td></tr>`
6918
6928
  ).join("");
6919
- return `<!DOCTYPE html>
6920
- <html lang="en">
6929
+ return `<!DOCTYPE html><html lang="en">
6921
6930
  <head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Analytics - weifuwu</title>
6922
- <style>
6923
- *,:before,:after{box-sizing:border-box;margin:0;padding:0}
6931
+ <style>*,:before,:after{box-sizing:border-box;margin:0;padding:0}
6924
6932
  body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8f9fa;color:#333;padding:24px;max-width:960px;margin:0 auto}
6925
6933
  h1{font-size:24px;font-weight:700;margin-bottom:24px}
6926
6934
  .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:24px}
@@ -6958,28 +6966,23 @@ function analytics(options) {
6958
6966
  const excluded = options?.excluded ?? DEFAULT_EXCLUDED;
6959
6967
  const pg = options?.pg;
6960
6968
  const store = pg ? null : new MemStore();
6961
- const middleware = async (req, ctx, next) => {
6962
- const url = new URL(req.url);
6963
- const path2 = url.pathname;
6964
- if (excluded.some((e) => path2.startsWith(e))) return next(req, ctx);
6965
- const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6966
- const ref = req.headers.get("referer") || "";
6967
- const ua = req.headers.get("user-agent") || "";
6968
- const mobile = /mobile|android|iphone|ipad/i.test(ua);
6969
- if (pg) {
6970
- await pg.sql`
6971
- INSERT INTO __analytics (date, path, count, mobile, desktop)
6972
- VALUES (${date}, ${path2}, 1, ${mobile ? 1 : 0}, ${mobile ? 0 : 1})
6973
- ON CONFLICT (date, path) DO UPDATE SET
6974
- count = __analytics.count + 1,
6975
- mobile = __analytics.mobile + ${mobile ? 1 : 0},
6976
- desktop = __analytics.desktop + ${mobile ? 0 : 1}
6977
- `;
6978
- } else {
6979
- const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
6980
- store.record(path2, date, refDomain, mobile);
6981
- }
6982
- return next(req, ctx);
6969
+ const middleware = () => {
6970
+ const m = async (req, ctx, next) => {
6971
+ const path2 = new URL(req.url).pathname;
6972
+ if (excluded.some((e) => path2.startsWith(e))) return next(req, ctx);
6973
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6974
+ const ua = req.headers.get("user-agent") || "";
6975
+ const mobile = /mobile|android|iphone|ipad/i.test(ua);
6976
+ if (pg) {
6977
+ await recordPg(pg.sql, path2, date, mobile);
6978
+ } else {
6979
+ const ref = req.headers.get("referer") || "";
6980
+ const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
6981
+ store.record(path2, date, refDomain, mobile);
6982
+ }
6983
+ return next(req, ctx);
6984
+ };
6985
+ return m;
6983
6986
  };
6984
6987
  const handler = async (req) => {
6985
6988
  const url = new URL(req.url);
@@ -6990,7 +6993,18 @@ function analytics(options) {
6990
6993
  headers: { "content-type": "text/html; charset=utf-8" }
6991
6994
  });
6992
6995
  };
6993
- return { middleware, handler };
6996
+ const router = () => {
6997
+ const r = new Router();
6998
+ r.get("/__analytics/data", handler);
6999
+ r.get("/analytics", handler);
7000
+ return r;
7001
+ };
7002
+ const migrate = async () => {
7003
+ if (pg) await migratePg(pg.sql, pg.table);
7004
+ };
7005
+ const close = async () => {
7006
+ };
7007
+ return { middleware, router, migrate, close };
6994
7008
  }
6995
7009
 
6996
7010
  // preferences.ts
@@ -7020,6 +7034,30 @@ function extractCookie(req, name) {
7020
7034
  }
7021
7035
  return null;
7022
7036
  }
7037
+ function prefCookie(name, value) {
7038
+ return `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
7039
+ }
7040
+ async function handlePrefSwitch(req, value, cookieName, load) {
7041
+ const isJson = req.headers.get("accept")?.includes("application/json");
7042
+ if (isJson) {
7043
+ const result = { ok: true };
7044
+ if (cookieName === "locale" || cookieName === "lang") {
7045
+ result.locale = value;
7046
+ const messages2 = await load(value);
7047
+ if (Object.keys(messages2).length > 0) result.messages = messages2;
7048
+ } else {
7049
+ result.theme = value;
7050
+ }
7051
+ return Response.json(result, {
7052
+ headers: { "Set-Cookie": prefCookie(cookieName, value) }
7053
+ });
7054
+ }
7055
+ const referer = req.headers.get("referer") || "/";
7056
+ return new Response(null, {
7057
+ status: 302,
7058
+ headers: { Location: referer, "Set-Cookie": prefCookie(cookieName, value) }
7059
+ });
7060
+ }
7023
7061
  function preferences(options) {
7024
7062
  const dir = options.dir ? resolve10(options.dir) : void 0;
7025
7063
  const localeOpts = { ...defaults.locale, ...options.locale };
@@ -7041,6 +7079,15 @@ function preferences(options) {
7041
7079
  }
7042
7080
  }
7043
7081
  return async (req, ctx, next) => {
7082
+ const url = new URL(req.url);
7083
+ const langMatch = url.pathname.match(/^\/__lang\/(\w+)$/);
7084
+ if (langMatch && req.method === "GET") {
7085
+ return handlePrefSwitch(req, langMatch[1], localeOpts.cookie, load);
7086
+ }
7087
+ const themeMatch = url.pathname.match(/^\/__theme\/(\w+)$/);
7088
+ if (themeMatch && req.method === "GET") {
7089
+ return handlePrefSwitch(req, themeMatch[1], themeOpts.cookie, load);
7090
+ }
7044
7091
  const locale = detectLocale(req, localeOpts);
7045
7092
  const theme = detectTheme(req, themeOpts);
7046
7093
  ctx.prefs = { locale, theme };
package/dist/react.js CHANGED
@@ -140,17 +140,58 @@ function useAction(url, options) {
140
140
 
141
141
  // client-router.ts
142
142
  import { createElement, useCallback as useCallback3, useState as useState3, useEffect as useEffect2 } from "react";
143
+
144
+ // tsx-context.ts
145
+ import { useSyncExternalStore, createContext } from "react";
146
+ var _ctx = { params: {}, query: {} };
147
+ var _listeners = /* @__PURE__ */ new Set();
148
+ function setCtx(value) {
149
+ _ctx = { ..._ctx, ...value };
150
+ _listeners.forEach((fn) => fn());
151
+ }
152
+ function _buildT() {
153
+ const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
154
+ if (!messages) return void 0;
155
+ return (key, params, fallback) => {
156
+ const msg = key.split(".").reduce((o, k) => o?.[k], messages);
157
+ if (msg === void 0 || msg === null) return fallback ?? key;
158
+ if (!params) return String(msg);
159
+ let result = String(msg);
160
+ for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
161
+ return result;
162
+ };
163
+ }
164
+ function useCtx() {
165
+ useSyncExternalStore(
166
+ (cb) => {
167
+ _listeners.add(cb);
168
+ return () => {
169
+ _listeners.delete(cb);
170
+ };
171
+ },
172
+ () => _ctx,
173
+ () => _ctx
174
+ );
175
+ const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
176
+ const t = data?.t ?? _ctx.t ?? _buildT();
177
+ const result = { ..._ctx, ...data };
178
+ if (t) result.t = t;
179
+ return result;
180
+ }
181
+ var TsxContext = createContext({ params: {}, query: {} });
182
+
183
+ // client-router.ts
143
184
  var _navigating = false;
144
- var _listeners = [];
185
+ var _listeners2 = [];
145
186
  function onNavigate(fn) {
146
- _listeners.push(fn);
187
+ _listeners2.push(fn);
147
188
  return () => {
148
- _listeners = _listeners.filter((l) => l !== fn);
189
+ _listeners2 = _listeners2.filter((l) => l !== fn);
149
190
  };
150
191
  }
151
192
  function setNavigating(v) {
152
193
  _navigating = v;
153
- for (const fn of _listeners) fn(v);
194
+ for (const fn of _listeners2) fn(v);
154
195
  }
155
196
  async function navigate(href) {
156
197
  if (typeof document === "undefined") return;
@@ -159,6 +200,32 @@ async function navigate(href) {
159
200
  location.href = href;
160
201
  return;
161
202
  }
203
+ const langMatch = url.pathname.match(/^\/__lang\/(\w+)$/);
204
+ const themeMatch = url.pathname.match(/^\/__theme\/(\w+)$/);
205
+ if (langMatch || themeMatch) {
206
+ try {
207
+ const res = await fetch(url.pathname, {
208
+ headers: { accept: "application/json" }
209
+ });
210
+ const data = await res.json();
211
+ const ctx = { ...window.__WEIFUWU_CTX || {}, params: {}, query: {} };
212
+ if (data.locale) {
213
+ ctx.locale = data.locale;
214
+ ctx.prefs = { ...ctx.prefs, locale: data.locale };
215
+ if (data.messages) window.__LOCALE_DATA__ = data.messages;
216
+ }
217
+ if (data.theme) {
218
+ ctx.theme = data.theme;
219
+ ctx.prefs = { ...ctx.prefs, theme: data.theme };
220
+ }
221
+ ;
222
+ window.__WEIFUWU_CTX = ctx;
223
+ setCtx(ctx);
224
+ } catch {
225
+ location.href = href;
226
+ }
227
+ return;
228
+ }
162
229
  const scrollPos = [window.scrollX, window.scrollY];
163
230
  setNavigating(true);
164
231
  try {
@@ -315,45 +382,6 @@ async function prefetchPage(href) {
315
382
  }
316
383
  }
317
384
 
318
- // tsx-context.ts
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
- }
338
- function useCtx() {
339
- useSyncExternalStore(
340
- (cb) => {
341
- _listeners2.add(cb);
342
- return () => {
343
- _listeners2.delete(cb);
344
- };
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;
354
- }
355
- var TsxContext = createContext({ params: {}, query: {} });
356
-
357
385
  // head.tsx
358
386
  import { createElement as createElement2 } from "react";
359
387
  function Head({ children }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.17.7",
3
+ "version": "0.17.8",
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",