weifuwu 0.17.7 → 0.17.9
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 +58 -5
- 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 +119 -81
- package/dist/react.js +68 -43
- package/dist/tsx-context.d.ts +7 -7
- 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
|
|
@@ -1153,19 +1155,19 @@ In-memory page view tracking with a built-in dashboard. Zero extra dependencies.
|
|
|
1153
1155
|
```ts
|
|
1154
1156
|
import { analytics } from 'weifuwu'
|
|
1155
1157
|
|
|
1156
|
-
app.use(analytics()) // mounts middleware + /
|
|
1158
|
+
app.use(analytics()) // mounts middleware + /__analytics + /__analytics/data
|
|
1157
1159
|
```
|
|
1158
1160
|
|
|
1159
1161
|
| Endpoint | Description |
|
|
1160
1162
|
|----------|-------------|
|
|
1161
|
-
| `GET /
|
|
1163
|
+
| `GET /__analytics` | Dashboard — PV trend, top pages, referrers, device breakdown |
|
|
1162
1164
|
| `GET /__analytics/data?days=7` | Raw JSON data for custom dashboards |
|
|
1163
1165
|
|
|
1164
|
-
Excluded paths (not recorded): `/__analytics/*`,
|
|
1166
|
+
Excluded paths (not recorded): `/__analytics/*`, `/__wfw/*`, `/static/*`.
|
|
1165
1167
|
|
|
1166
1168
|
### Dashboard
|
|
1167
1169
|
|
|
1168
|
-
The built-in `/
|
|
1170
|
+
The built-in `/__analytics` page renders a server-generated HTML dashboard with:
|
|
1169
1171
|
|
|
1170
1172
|
- **Summary cards** — total PV, unique pages, mobile/desktop ratio
|
|
1171
1173
|
- **Bar chart** — daily page views for the selected period
|
|
@@ -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
|
@@ -579,7 +579,8 @@ import chokidar from "chokidar";
|
|
|
579
579
|
|
|
580
580
|
// tsx-context.ts
|
|
581
581
|
import { useSyncExternalStore, createContext } from "react";
|
|
582
|
-
var
|
|
582
|
+
var fallbackT = (key, _params, fallback) => fallback ?? key;
|
|
583
|
+
var _ctx = { params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} };
|
|
583
584
|
var _listeners = /* @__PURE__ */ new Set();
|
|
584
585
|
function setCtx(value) {
|
|
585
586
|
_ctx = { ..._ctx, ...value };
|
|
@@ -587,7 +588,7 @@ function setCtx(value) {
|
|
|
587
588
|
}
|
|
588
589
|
function _buildT() {
|
|
589
590
|
const messages2 = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
|
|
590
|
-
if (!messages2) return
|
|
591
|
+
if (!messages2) return fallbackT;
|
|
591
592
|
return (key, params, fallback) => {
|
|
592
593
|
const msg = key.split(".").reduce((o, k) => o?.[k], messages2);
|
|
593
594
|
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
@@ -610,11 +611,9 @@ function useCtx() {
|
|
|
610
611
|
);
|
|
611
612
|
const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
|
|
612
613
|
const t = data?.t ?? _ctx.t ?? _buildT();
|
|
613
|
-
|
|
614
|
-
if (t) result.t = t;
|
|
615
|
-
return result;
|
|
614
|
+
return { ..._ctx, ...data, t };
|
|
616
615
|
}
|
|
617
|
-
var TsxContext = createContext({ params: {}, query: {} });
|
|
616
|
+
var TsxContext = createContext({ params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} });
|
|
618
617
|
|
|
619
618
|
// tsx-instance.ts
|
|
620
619
|
var liveReloadClients = /* @__PURE__ */ new Set();
|
|
@@ -904,8 +903,6 @@ var TsxInstance = class {
|
|
|
904
903
|
user: ctx.user,
|
|
905
904
|
parsed: ctx.parsed,
|
|
906
905
|
prefs: ctx.prefs,
|
|
907
|
-
locale: ctx.locale,
|
|
908
|
-
theme: ctx.theme,
|
|
909
906
|
t: ctx.t,
|
|
910
907
|
env: ctx.env
|
|
911
908
|
});
|
|
@@ -1075,8 +1072,6 @@ ${src}`;
|
|
|
1075
1072
|
user: ctx.user,
|
|
1076
1073
|
parsed: ctx.parsed,
|
|
1077
1074
|
prefs: ctx.prefs,
|
|
1078
|
-
locale: ctx.locale,
|
|
1079
|
-
theme: ctx.theme,
|
|
1080
1075
|
t: ctx.t,
|
|
1081
1076
|
env: ctx.env
|
|
1082
1077
|
});
|
|
@@ -1345,7 +1340,7 @@ function streamResponse(reactStream, opts) {
|
|
|
1345
1340
|
function buildHeadPayload(opts) {
|
|
1346
1341
|
const { ctx, base, compiledTailwindCss } = opts;
|
|
1347
1342
|
let result = "";
|
|
1348
|
-
if (ctx.theme) {
|
|
1343
|
+
if (ctx.prefs?.theme) {
|
|
1349
1344
|
result += `<script>!function(){var t=(document.cookie.match(/(?:^|;\\s*)theme=([^;]+)/)||[])[1]||'system';if(t==='system'){t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'}document.documentElement.setAttribute('data-theme',t)}()</script>
|
|
1350
1345
|
`;
|
|
1351
1346
|
}
|
|
@@ -1362,11 +1357,9 @@ function buildHeadPayload(opts) {
|
|
|
1362
1357
|
params: ctx.params,
|
|
1363
1358
|
query: ctx.query,
|
|
1364
1359
|
user: ctx.user,
|
|
1365
|
-
parsed: ctx.parsed
|
|
1360
|
+
parsed: ctx.parsed,
|
|
1361
|
+
prefs: ctx.prefs
|
|
1366
1362
|
};
|
|
1367
|
-
if (ctx.prefs) ctxData.prefs = ctx.prefs;
|
|
1368
|
-
if (ctx.locale) ctxData.locale = ctx.locale;
|
|
1369
|
-
if (ctx.theme) ctxData.theme = ctx.theme;
|
|
1370
1363
|
const publicEnv = {};
|
|
1371
1364
|
for (const key of Object.keys(process.env)) {
|
|
1372
1365
|
if (key.startsWith("WEIFUWU_PUBLIC_")) {
|
|
@@ -6799,7 +6792,7 @@ function health(options) {
|
|
|
6799
6792
|
}
|
|
6800
6793
|
|
|
6801
6794
|
// analytics.ts
|
|
6802
|
-
var DEFAULT_EXCLUDED = ["/__analytics", "/__wfw", "/static"
|
|
6795
|
+
var DEFAULT_EXCLUDED = ["/__analytics", "/__wfw", "/static"];
|
|
6803
6796
|
var MemStore = class {
|
|
6804
6797
|
days = /* @__PURE__ */ new Map();
|
|
6805
6798
|
pages = /* @__PURE__ */ new Map();
|
|
@@ -6812,11 +6805,8 @@ var MemStore = class {
|
|
|
6812
6805
|
}
|
|
6813
6806
|
day.pv++;
|
|
6814
6807
|
day.uv.add(path2);
|
|
6815
|
-
if (mobile)
|
|
6816
|
-
|
|
6817
|
-
} else {
|
|
6818
|
-
day.desktop++;
|
|
6819
|
-
}
|
|
6808
|
+
if (mobile) day.mobile++;
|
|
6809
|
+
else day.desktop++;
|
|
6820
6810
|
let page = this.pages.get(path2);
|
|
6821
6811
|
if (!page) {
|
|
6822
6812
|
page = { count: 0 };
|
|
@@ -6837,11 +6827,8 @@ var MemStore = class {
|
|
|
6837
6827
|
since.setDate(since.getDate() - days);
|
|
6838
6828
|
const sinceStr = since.toISOString().slice(0, 10);
|
|
6839
6829
|
const daily = [];
|
|
6840
|
-
let totalPv = 0;
|
|
6841
|
-
|
|
6842
|
-
let totalDesktop = 0;
|
|
6843
|
-
const pageMap = /* @__PURE__ */ new Map();
|
|
6844
|
-
const allUv = /* @__PURE__ */ new Set();
|
|
6830
|
+
let totalPv = 0, totalMobile = 0, totalDesktop = 0;
|
|
6831
|
+
const allUv = /* @__PURE__ */ new Set(), pageMap = /* @__PURE__ */ new Map();
|
|
6845
6832
|
for (const [date, day] of this.days) {
|
|
6846
6833
|
if (date < sinceStr) continue;
|
|
6847
6834
|
daily.push({ date, pv: day.pv, uv: day.uv.size });
|
|
@@ -6850,64 +6837,80 @@ var MemStore = class {
|
|
|
6850
6837
|
totalDesktop += day.desktop;
|
|
6851
6838
|
for (const p of day.uv) allUv.add(p);
|
|
6852
6839
|
}
|
|
6853
|
-
for (const [path2, page] of this.pages)
|
|
6854
|
-
pageMap.set(path2, page.count);
|
|
6855
|
-
}
|
|
6840
|
+
for (const [path2, page] of this.pages) pageMap.set(path2, page.count);
|
|
6856
6841
|
const topPages = [...pageMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([path2, count]) => ({ path: path2, pv: count }));
|
|
6857
6842
|
const refMap = /* @__PURE__ */ new Map();
|
|
6858
6843
|
for (const [date, refs] of this.refs) {
|
|
6859
6844
|
if (date < sinceStr) continue;
|
|
6860
6845
|
for (const [domain, count] of refs) refMap.set(domain, (refMap.get(domain) || 0) + count);
|
|
6861
6846
|
}
|
|
6862
|
-
const referrers = [...refMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count }));
|
|
6863
6847
|
const total = totalMobile + totalDesktop || 1;
|
|
6864
6848
|
return {
|
|
6865
6849
|
total_pv: totalPv,
|
|
6866
6850
|
total_uv: allUv.size,
|
|
6867
6851
|
daily,
|
|
6868
6852
|
top_pages: topPages,
|
|
6869
|
-
referrers,
|
|
6853
|
+
referrers: [...refMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count })),
|
|
6870
6854
|
devices: { mobile: Math.round(totalMobile / total * 1e3) / 10, desktop: Math.round(totalDesktop / total * 1e3) / 10 }
|
|
6871
6855
|
};
|
|
6872
6856
|
}
|
|
6873
6857
|
};
|
|
6858
|
+
async function migratePg(sql2, table) {
|
|
6859
|
+
const analytics2 = table("__analytics", {
|
|
6860
|
+
date: text("date").notNull(),
|
|
6861
|
+
path: text("path").notNull(),
|
|
6862
|
+
count: integer("count").default(0),
|
|
6863
|
+
mobile: integer("mobile").default(0),
|
|
6864
|
+
desktop: integer("desktop").default(0)
|
|
6865
|
+
});
|
|
6866
|
+
await analytics2.create();
|
|
6867
|
+
await analytics2.createIndex(["date", "path"], { unique: true });
|
|
6868
|
+
return analytics2;
|
|
6869
|
+
}
|
|
6870
|
+
async function recordPg(sql2, path2, date, mobile) {
|
|
6871
|
+
await sql2`
|
|
6872
|
+
INSERT INTO __analytics (date, path, count, mobile, desktop)
|
|
6873
|
+
VALUES (${date}, ${path2}, 1, ${mobile ? 1 : 0}, ${mobile ? 0 : 1})
|
|
6874
|
+
ON CONFLICT (date, path) DO UPDATE SET
|
|
6875
|
+
count = __analytics.count + 1,
|
|
6876
|
+
mobile = __analytics.mobile + ${mobile ? 1 : 0},
|
|
6877
|
+
desktop = __analytics.desktop + ${mobile ? 0 : 1}
|
|
6878
|
+
`;
|
|
6879
|
+
}
|
|
6874
6880
|
async function queryPg(sql2, days) {
|
|
6875
6881
|
const since = /* @__PURE__ */ new Date();
|
|
6876
6882
|
since.setDate(since.getDate() - days);
|
|
6883
|
+
const sinceStr = since.toISOString().slice(0, 10);
|
|
6877
6884
|
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
|
|
6885
|
+
SELECT date, SUM(count)::int as pv, COUNT(DISTINCT path)::int as uv
|
|
6886
|
+
FROM __analytics WHERE date >= ${sinceStr} GROUP BY date ORDER BY date
|
|
6881
6887
|
`;
|
|
6882
6888
|
const pageRows = await sql2`
|
|
6883
|
-
SELECT path, SUM(count) as pv
|
|
6884
|
-
FROM __analytics WHERE date >= ${
|
|
6889
|
+
SELECT path, SUM(count)::int as pv
|
|
6890
|
+
FROM __analytics WHERE date >= ${sinceStr}
|
|
6885
6891
|
GROUP BY path ORDER BY pv DESC LIMIT 20
|
|
6886
6892
|
`;
|
|
6887
6893
|
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 >= ${
|
|
6894
|
+
SELECT COALESCE(SUM(count), 0)::int as total_pv,
|
|
6895
|
+
COALESCE(SUM(mobile), 0)::int as total_mobile,
|
|
6896
|
+
COALESCE(SUM(desktop), 0)::int as total_desktop
|
|
6897
|
+
FROM __analytics WHERE date >= ${sinceStr}
|
|
6892
6898
|
`;
|
|
6893
|
-
const
|
|
6894
|
-
const
|
|
6899
|
+
const t = totalRes[0];
|
|
6900
|
+
const denom = t.total_mobile + t.total_desktop || 1;
|
|
6895
6901
|
return {
|
|
6896
|
-
total_pv:
|
|
6902
|
+
total_pv: t.total_pv,
|
|
6897
6903
|
total_uv: pageRows.length,
|
|
6898
|
-
daily: daily.map((d) => ({ date: d.date, pv:
|
|
6899
|
-
top_pages: pageRows.map((p) => ({ path: p.path, pv:
|
|
6904
|
+
daily: daily.map((d) => ({ date: d.date, pv: d.pv, uv: d.uv })),
|
|
6905
|
+
top_pages: pageRows.map((p) => ({ path: p.path, pv: p.pv })),
|
|
6900
6906
|
referrers: [],
|
|
6901
|
-
devices: {
|
|
6902
|
-
mobile: Math.round(total.total_mobile / totalMobileDesktop * 1e3) / 10,
|
|
6903
|
-
desktop: Math.round(total.total_desktop / totalMobileDesktop * 1e3) / 10
|
|
6904
|
-
}
|
|
6907
|
+
devices: { mobile: Math.round(t.total_mobile / denom * 1e3) / 10, desktop: Math.round(t.total_desktop / denom * 1e3) / 10 }
|
|
6905
6908
|
};
|
|
6906
6909
|
}
|
|
6907
6910
|
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(
|
|
6911
|
+
const { total_pv, total_uv, top_pages, referrers } = data;
|
|
6912
|
+
const maxPv = Math.max(...data.daily.map((d) => d.pv), 1);
|
|
6913
|
+
const bars = data.daily.map(
|
|
6911
6914
|
(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
6915
|
).join("");
|
|
6913
6916
|
const rows = top_pages.map(
|
|
@@ -6916,11 +6919,9 @@ function renderDashboard(days, data) {
|
|
|
6916
6919
|
const refRows = referrers.map(
|
|
6917
6920
|
(r) => `<tr><td>${r.domain}</td><td class="num">${r.count}</td></tr>`
|
|
6918
6921
|
).join("");
|
|
6919
|
-
return `<!DOCTYPE html>
|
|
6920
|
-
<html lang="en">
|
|
6922
|
+
return `<!DOCTYPE html><html lang="en">
|
|
6921
6923
|
<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}
|
|
6924
|
+
<style>*,:before,:after{box-sizing:border-box;margin:0;padding:0}
|
|
6924
6925
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8f9fa;color:#333;padding:24px;max-width:960px;margin:0 auto}
|
|
6925
6926
|
h1{font-size:24px;font-weight:700;margin-bottom:24px}
|
|
6926
6927
|
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:24px}
|
|
@@ -6958,28 +6959,23 @@ function analytics(options) {
|
|
|
6958
6959
|
const excluded = options?.excluded ?? DEFAULT_EXCLUDED;
|
|
6959
6960
|
const pg = options?.pg;
|
|
6960
6961
|
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);
|
|
6962
|
+
const middleware = () => {
|
|
6963
|
+
const m = async (req, ctx, next) => {
|
|
6964
|
+
const path2 = new URL(req.url).pathname;
|
|
6965
|
+
if (excluded.some((e) => path2.startsWith(e))) return next(req, ctx);
|
|
6966
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6967
|
+
const ua = req.headers.get("user-agent") || "";
|
|
6968
|
+
const mobile = /mobile|android|iphone|ipad/i.test(ua);
|
|
6969
|
+
if (pg) {
|
|
6970
|
+
await recordPg(pg.sql, path2, date, mobile);
|
|
6971
|
+
} else {
|
|
6972
|
+
const ref = req.headers.get("referer") || "";
|
|
6973
|
+
const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
|
|
6974
|
+
store.record(path2, date, refDomain, mobile);
|
|
6975
|
+
}
|
|
6976
|
+
return next(req, ctx);
|
|
6977
|
+
};
|
|
6978
|
+
return m;
|
|
6983
6979
|
};
|
|
6984
6980
|
const handler = async (req) => {
|
|
6985
6981
|
const url = new URL(req.url);
|
|
@@ -6990,7 +6986,18 @@ function analytics(options) {
|
|
|
6990
6986
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
6991
6987
|
});
|
|
6992
6988
|
};
|
|
6993
|
-
|
|
6989
|
+
const router = () => {
|
|
6990
|
+
const r = new Router();
|
|
6991
|
+
r.get("/__analytics/data", handler);
|
|
6992
|
+
r.get("/__analytics", handler);
|
|
6993
|
+
return r;
|
|
6994
|
+
};
|
|
6995
|
+
const migrate = async () => {
|
|
6996
|
+
if (pg) await migratePg(pg.sql, pg.table);
|
|
6997
|
+
};
|
|
6998
|
+
const close = async () => {
|
|
6999
|
+
};
|
|
7000
|
+
return { middleware, router, migrate, close };
|
|
6994
7001
|
}
|
|
6995
7002
|
|
|
6996
7003
|
// preferences.ts
|
|
@@ -7020,6 +7027,30 @@ function extractCookie(req, name) {
|
|
|
7020
7027
|
}
|
|
7021
7028
|
return null;
|
|
7022
7029
|
}
|
|
7030
|
+
function prefCookie(name, value) {
|
|
7031
|
+
return `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
7032
|
+
}
|
|
7033
|
+
async function handlePrefSwitch(req, value, cookieName, load) {
|
|
7034
|
+
const isJson = req.headers.get("accept")?.includes("application/json");
|
|
7035
|
+
if (isJson) {
|
|
7036
|
+
const result = { ok: true };
|
|
7037
|
+
if (cookieName === "locale" || cookieName === "lang") {
|
|
7038
|
+
result.locale = value;
|
|
7039
|
+
const messages2 = await load(value);
|
|
7040
|
+
if (Object.keys(messages2).length > 0) result.messages = messages2;
|
|
7041
|
+
} else {
|
|
7042
|
+
result.theme = value;
|
|
7043
|
+
}
|
|
7044
|
+
return Response.json(result, {
|
|
7045
|
+
headers: { "Set-Cookie": prefCookie(cookieName, value) }
|
|
7046
|
+
});
|
|
7047
|
+
}
|
|
7048
|
+
const referer = req.headers.get("referer") || "/";
|
|
7049
|
+
return new Response(null, {
|
|
7050
|
+
status: 302,
|
|
7051
|
+
headers: { Location: referer, "Set-Cookie": prefCookie(cookieName, value) }
|
|
7052
|
+
});
|
|
7053
|
+
}
|
|
7023
7054
|
function preferences(options) {
|
|
7024
7055
|
const dir = options.dir ? resolve10(options.dir) : void 0;
|
|
7025
7056
|
const localeOpts = { ...defaults.locale, ...options.locale };
|
|
@@ -7041,11 +7072,18 @@ function preferences(options) {
|
|
|
7041
7072
|
}
|
|
7042
7073
|
}
|
|
7043
7074
|
return async (req, ctx, next) => {
|
|
7075
|
+
const url = new URL(req.url);
|
|
7076
|
+
const langMatch = url.pathname.match(/^\/__lang\/(\w+)$/);
|
|
7077
|
+
if (langMatch && req.method === "GET") {
|
|
7078
|
+
return handlePrefSwitch(req, langMatch[1], localeOpts.cookie, load);
|
|
7079
|
+
}
|
|
7080
|
+
const themeMatch = url.pathname.match(/^\/__theme\/(\w+)$/);
|
|
7081
|
+
if (themeMatch && req.method === "GET") {
|
|
7082
|
+
return handlePrefSwitch(req, themeMatch[1], themeOpts.cookie, load);
|
|
7083
|
+
}
|
|
7044
7084
|
const locale = detectLocale(req, localeOpts);
|
|
7045
7085
|
const theme = detectTheme(req, themeOpts);
|
|
7046
7086
|
ctx.prefs = { locale, theme };
|
|
7047
|
-
ctx.locale = locale;
|
|
7048
|
-
ctx.theme = theme;
|
|
7049
7087
|
if (dir) {
|
|
7050
7088
|
const msgs = await load(locale);
|
|
7051
7089
|
ctx.t = (key, params, fallback) => translate(msgs, key, params, fallback);
|
package/dist/react.js
CHANGED
|
@@ -140,17 +140,57 @@ 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 fallbackT = (key, _params, fallback) => fallback ?? key;
|
|
147
|
+
var _ctx = { params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} };
|
|
148
|
+
var _listeners = /* @__PURE__ */ new Set();
|
|
149
|
+
function setCtx(value) {
|
|
150
|
+
_ctx = { ..._ctx, ...value };
|
|
151
|
+
_listeners.forEach((fn) => fn());
|
|
152
|
+
}
|
|
153
|
+
function _buildT() {
|
|
154
|
+
const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
|
|
155
|
+
if (!messages) return fallbackT;
|
|
156
|
+
return (key, params, fallback) => {
|
|
157
|
+
const msg = key.split(".").reduce((o, k) => o?.[k], messages);
|
|
158
|
+
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
159
|
+
if (!params) return String(msg);
|
|
160
|
+
let result = String(msg);
|
|
161
|
+
for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
|
|
162
|
+
return result;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function useCtx() {
|
|
166
|
+
useSyncExternalStore(
|
|
167
|
+
(cb) => {
|
|
168
|
+
_listeners.add(cb);
|
|
169
|
+
return () => {
|
|
170
|
+
_listeners.delete(cb);
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
() => _ctx,
|
|
174
|
+
() => _ctx
|
|
175
|
+
);
|
|
176
|
+
const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
|
|
177
|
+
const t = data?.t ?? _ctx.t ?? _buildT();
|
|
178
|
+
return { ..._ctx, ...data, t };
|
|
179
|
+
}
|
|
180
|
+
var TsxContext = createContext({ params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} });
|
|
181
|
+
|
|
182
|
+
// client-router.ts
|
|
143
183
|
var _navigating = false;
|
|
144
|
-
var
|
|
184
|
+
var _listeners2 = [];
|
|
145
185
|
function onNavigate(fn) {
|
|
146
|
-
|
|
186
|
+
_listeners2.push(fn);
|
|
147
187
|
return () => {
|
|
148
|
-
|
|
188
|
+
_listeners2 = _listeners2.filter((l) => l !== fn);
|
|
149
189
|
};
|
|
150
190
|
}
|
|
151
191
|
function setNavigating(v) {
|
|
152
192
|
_navigating = v;
|
|
153
|
-
for (const fn of
|
|
193
|
+
for (const fn of _listeners2) fn(v);
|
|
154
194
|
}
|
|
155
195
|
async function navigate(href) {
|
|
156
196
|
if (typeof document === "undefined") return;
|
|
@@ -159,6 +199,30 @@ async function navigate(href) {
|
|
|
159
199
|
location.href = href;
|
|
160
200
|
return;
|
|
161
201
|
}
|
|
202
|
+
const langMatch = url.pathname.match(/^\/__lang\/(\w+)$/);
|
|
203
|
+
const themeMatch = url.pathname.match(/^\/__theme\/(\w+)$/);
|
|
204
|
+
if (langMatch || themeMatch) {
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch(url.pathname, {
|
|
207
|
+
headers: { accept: "application/json" }
|
|
208
|
+
});
|
|
209
|
+
const data = await res.json();
|
|
210
|
+
const ctx = { ...window.__WEIFUWU_CTX || {}, params: {}, query: {} };
|
|
211
|
+
if (data.locale) {
|
|
212
|
+
ctx.prefs = { ...ctx.prefs, locale: data.locale };
|
|
213
|
+
if (data.messages) window.__LOCALE_DATA__ = data.messages;
|
|
214
|
+
}
|
|
215
|
+
if (data.theme) {
|
|
216
|
+
ctx.prefs = { ...ctx.prefs, theme: data.theme };
|
|
217
|
+
}
|
|
218
|
+
;
|
|
219
|
+
window.__WEIFUWU_CTX = ctx;
|
|
220
|
+
setCtx(ctx);
|
|
221
|
+
} catch {
|
|
222
|
+
location.href = href;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
162
226
|
const scrollPos = [window.scrollX, window.scrollY];
|
|
163
227
|
setNavigating(true);
|
|
164
228
|
try {
|
|
@@ -315,45 +379,6 @@ async function prefetchPage(href) {
|
|
|
315
379
|
}
|
|
316
380
|
}
|
|
317
381
|
|
|
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
382
|
// head.tsx
|
|
358
383
|
import { createElement as createElement2 } from "react";
|
|
359
384
|
function Head({ children }) {
|
package/dist/tsx-context.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export interface CtxValue {
|
|
2
2
|
params: Record<string, string>;
|
|
3
3
|
query: Record<string, string>;
|
|
4
|
-
user
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
t
|
|
10
|
-
env
|
|
4
|
+
user: {
|
|
5
|
+
id?: string;
|
|
6
|
+
};
|
|
7
|
+
parsed: Record<string, unknown>;
|
|
8
|
+
prefs: Record<string, string>;
|
|
9
|
+
t: (key: string, params?: Record<string, string>, fallback?: string) => string;
|
|
10
|
+
env: Record<string, string>;
|
|
11
11
|
}
|
|
12
12
|
export declare function setCtx(value: Partial<CtxValue>): void;
|
|
13
13
|
export declare function useCtx(): CtxValue;
|