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