weifuwu 0.17.6 → 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 +54 -1
- package/dist/analytics.d.ts +13 -5
- package/dist/client-router.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +145 -61
- package/dist/react.js +71 -43
- package/package.json +1 -1
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` |
|
package/dist/analytics.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Middleware } from './types.ts';
|
|
2
|
+
import { Router } from './router.ts';
|
|
2
3
|
export interface AnalyticsOptions {
|
|
3
4
|
excluded?: string[];
|
|
5
|
+
pg?: {
|
|
6
|
+
sql: (strings: TemplateStringsArray, ...values: any[]) => Promise<any[]>;
|
|
7
|
+
table: (name: string, cols: any) => any;
|
|
8
|
+
};
|
|
4
9
|
}
|
|
5
|
-
export
|
|
6
|
-
middleware: Middleware;
|
|
7
|
-
|
|
8
|
-
|
|
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;
|
package/dist/client-router.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,56 +6844,91 @@ 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
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
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
|
+
}
|
|
6887
|
+
async function queryPg(sql2, days) {
|
|
6888
|
+
const since = /* @__PURE__ */ new Date();
|
|
6889
|
+
since.setDate(since.getDate() - days);
|
|
6890
|
+
const sinceStr = since.toISOString().slice(0, 10);
|
|
6891
|
+
const daily = await sql2`
|
|
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
|
|
6894
|
+
`;
|
|
6895
|
+
const pageRows = await sql2`
|
|
6896
|
+
SELECT path, SUM(count)::int as pv
|
|
6897
|
+
FROM __analytics WHERE date >= ${sinceStr}
|
|
6898
|
+
GROUP BY path ORDER BY pv DESC LIMIT 20
|
|
6899
|
+
`;
|
|
6900
|
+
const totalRes = await sql2`
|
|
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}
|
|
6905
|
+
`;
|
|
6906
|
+
const t = totalRes[0];
|
|
6907
|
+
const denom = t.total_mobile + t.total_desktop || 1;
|
|
6908
|
+
return {
|
|
6909
|
+
total_pv: t.total_pv,
|
|
6910
|
+
total_uv: pageRows.length,
|
|
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 })),
|
|
6913
|
+
referrers: [],
|
|
6914
|
+
devices: { mobile: Math.round(t.total_mobile / denom * 1e3) / 10, desktop: Math.round(t.total_desktop / denom * 1e3) / 10 }
|
|
6915
|
+
};
|
|
6916
|
+
}
|
|
6917
|
+
function renderDashboard(days, data) {
|
|
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(
|
|
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>`
|
|
6922
|
+
).join("");
|
|
6923
|
+
const rows = top_pages.map(
|
|
6924
|
+
(p, i) => `<tr><td class="num">${i + 1}</td><td class="path">${p.path}</td><td class="num">${p.pv}</td></tr>`
|
|
6925
|
+
).join("");
|
|
6926
|
+
const refRows = referrers.map(
|
|
6927
|
+
(r) => `<tr><td>${r.domain}</td><td class="num">${r.count}</td></tr>`
|
|
6928
|
+
).join("");
|
|
6929
|
+
return `<!DOCTYPE html><html lang="en">
|
|
6900
6930
|
<head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Analytics - weifuwu</title>
|
|
6901
|
-
<style
|
|
6902
|
-
*,:before,:after{box-sizing:border-box;margin:0;padding:0}
|
|
6931
|
+
<style>*,:before,:after{box-sizing:border-box;margin:0;padding:0}
|
|
6903
6932
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8f9fa;color:#333;padding:24px;max-width:960px;margin:0 auto}
|
|
6904
6933
|
h1{font-size:24px;font-weight:700;margin-bottom:24px}
|
|
6905
6934
|
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:24px}
|
|
@@ -6924,36 +6953,58 @@ tr:hover td{background:#f8faff}
|
|
|
6924
6953
|
<div class="cards">
|
|
6925
6954
|
<div class="card"><div class="val">${total_pv}</div><div class="lbl">Page Views (${days}d)</div></div>
|
|
6926
6955
|
<div class="card"><div class="val">${total_uv}</div><div class="lbl">Unique Pages</div></div>
|
|
6927
|
-
<div class="card"><div class="val">${devices.mobile}%</div><div class="lbl">Mobile</div></div>
|
|
6928
|
-
<div class="card"><div class="val">${devices.desktop}%</div><div class="lbl">Desktop</div></div>
|
|
6956
|
+
<div class="card"><div class="val">${data.devices.mobile}%</div><div class="lbl">Mobile</div></div>
|
|
6957
|
+
<div class="card"><div class="val">${data.devices.desktop}%</div><div class="lbl">Desktop</div></div>
|
|
6929
6958
|
</div>
|
|
6930
6959
|
<div class="section"><h2>Daily Page Views</h2><div class="chart">${bars}</div></div>
|
|
6931
6960
|
<div class="section"><h2>Top Pages</h2>
|
|
6932
6961
|
<table><thead><tr><th style="width:32px">#</th><th>Path</th><th style="width:64px">Views</th></tr></thead><tbody>${rows}</tbody></table></div>
|
|
6933
6962
|
${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>` : ""}
|
|
6934
6963
|
</body></html>`;
|
|
6935
|
-
|
|
6936
|
-
};
|
|
6964
|
+
}
|
|
6937
6965
|
function analytics(options) {
|
|
6938
6966
|
const excluded = options?.excluded ?? DEFAULT_EXCLUDED;
|
|
6939
|
-
const
|
|
6940
|
-
const
|
|
6941
|
-
|
|
6942
|
-
const
|
|
6943
|
-
|
|
6967
|
+
const pg = options?.pg;
|
|
6968
|
+
const store = pg ? null : new MemStore();
|
|
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);
|
|
6944
6973
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6945
|
-
const ref = req.headers.get("referer") || "";
|
|
6946
|
-
const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
|
|
6947
6974
|
const ua = req.headers.get("user-agent") || "";
|
|
6948
6975
|
const mobile = /mobile|android|iphone|ipad/i.test(ua);
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
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;
|
|
6952
6986
|
};
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6987
|
+
const handler = async (req) => {
|
|
6988
|
+
const url = new URL(req.url);
|
|
6989
|
+
const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 7, 1), 365);
|
|
6990
|
+
const data = pg ? await queryPg(pg.sql, days) : store.query(days);
|
|
6991
|
+
if (url.pathname === "/__analytics/data") return Response.json(data);
|
|
6992
|
+
return new Response(renderDashboard(days, data), {
|
|
6993
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
6994
|
+
});
|
|
6956
6995
|
};
|
|
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 };
|
|
6957
7008
|
}
|
|
6958
7009
|
|
|
6959
7010
|
// preferences.ts
|
|
@@ -6983,6 +7034,30 @@ function extractCookie(req, name) {
|
|
|
6983
7034
|
}
|
|
6984
7035
|
return null;
|
|
6985
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
|
+
}
|
|
6986
7061
|
function preferences(options) {
|
|
6987
7062
|
const dir = options.dir ? resolve10(options.dir) : void 0;
|
|
6988
7063
|
const localeOpts = { ...defaults.locale, ...options.locale };
|
|
@@ -7004,6 +7079,15 @@ function preferences(options) {
|
|
|
7004
7079
|
}
|
|
7005
7080
|
}
|
|
7006
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
|
+
}
|
|
7007
7091
|
const locale = detectLocale(req, localeOpts);
|
|
7008
7092
|
const theme = detectTheme(req, themeOpts);
|
|
7009
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
|
|
185
|
+
var _listeners2 = [];
|
|
145
186
|
function onNavigate(fn) {
|
|
146
|
-
|
|
187
|
+
_listeners2.push(fn);
|
|
147
188
|
return () => {
|
|
148
|
-
|
|
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
|
|
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 }) {
|