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 +54 -1
- package/dist/analytics.d.ts +10 -5
- package/dist/client-router.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +110 -63
- 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,11 +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[];
|
|
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
|
|
9
|
-
middleware: Middleware;
|
|
10
|
-
|
|
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;
|
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,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 >= ${
|
|
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 >= ${
|
|
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 >= ${
|
|
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
|
|
6894
|
-
const
|
|
6906
|
+
const t = totalRes[0];
|
|
6907
|
+
const denom = t.total_mobile + t.total_desktop || 1;
|
|
6895
6908
|
return {
|
|
6896
|
-
total_pv:
|
|
6909
|
+
total_pv: t.total_pv,
|
|
6897
6910
|
total_uv: pageRows.length,
|
|
6898
|
-
daily: daily.map((d) => ({ date: d.date, pv:
|
|
6899
|
-
top_pages: pageRows.map((p) => ({ path: p.path, 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,
|
|
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 =
|
|
6962
|
-
const
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
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
|
-
|
|
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
|
|
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 }) {
|