weifuwu 0.16.6 → 0.17.0
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 +223 -30
- package/dist/client-router.d.ts +11 -7
- package/dist/client-state.d.ts +22 -0
- package/dist/head.d.ts +6 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +249 -113
- package/dist/preferences.d.ts +14 -0
- package/dist/react.d.ts +5 -2
- package/dist/react.js +315 -41
- package/dist/tsx-context.d.ts +5 -6
- package/dist/tsx-instance.d.ts +2 -2
- package/dist/tsx.d.ts +2 -2
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -571,7 +571,7 @@ import { renderToReadableStream } from "react-dom/server";
|
|
|
571
571
|
import * as esbuild from "esbuild";
|
|
572
572
|
import { readdirSync, statSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
573
573
|
import { join, relative, resolve as resolve2, sep, dirname, basename } from "node:path";
|
|
574
|
-
import { pathToFileURL } from "node:url";
|
|
574
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
575
575
|
import { createHash } from "node:crypto";
|
|
576
576
|
import vm from "node:vm";
|
|
577
577
|
import { createRequire } from "node:module";
|
|
@@ -580,11 +580,26 @@ import chokidar from "chokidar";
|
|
|
580
580
|
// tsx-context.ts
|
|
581
581
|
import { createContext, useContext } from "react";
|
|
582
582
|
var TsxContext = createContext({ params: {}, query: {} });
|
|
583
|
-
function
|
|
583
|
+
function useCtx() {
|
|
584
|
+
if (typeof window !== "undefined") {
|
|
585
|
+
const wc = window.__WEIFUWU_CTX;
|
|
586
|
+
if (wc) {
|
|
587
|
+
const messages2 = window.__LOCALE_DATA__;
|
|
588
|
+
if (messages2 && !wc.t) {
|
|
589
|
+
wc.t = (key, params) => {
|
|
590
|
+
let msg = messages2[key] ?? key;
|
|
591
|
+
if (params) for (const [k, v] of Object.entries(params)) msg = msg.replace(`{${k}}`, v);
|
|
592
|
+
return msg;
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
return wc;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
584
598
|
return useContext(TsxContext);
|
|
585
599
|
}
|
|
586
600
|
|
|
587
601
|
// tsx-instance.ts
|
|
602
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
588
603
|
var liveReloadClients = /* @__PURE__ */ new Set();
|
|
589
604
|
function broadcastReload() {
|
|
590
605
|
for (const ws of liveReloadClients) {
|
|
@@ -638,26 +653,6 @@ function resolveAliases() {
|
|
|
638
653
|
_alias = {};
|
|
639
654
|
return {};
|
|
640
655
|
}
|
|
641
|
-
function concatUint8(chunks) {
|
|
642
|
-
const len = chunks.reduce((a, c) => a + c.length, 0);
|
|
643
|
-
const out = new Uint8Array(len);
|
|
644
|
-
let off = 0;
|
|
645
|
-
for (const c of chunks) {
|
|
646
|
-
out.set(c, off);
|
|
647
|
-
off += c.length;
|
|
648
|
-
}
|
|
649
|
-
return out;
|
|
650
|
-
}
|
|
651
|
-
async function readStream(stream) {
|
|
652
|
-
const chunks = [];
|
|
653
|
-
const reader = stream.getReader();
|
|
654
|
-
while (true) {
|
|
655
|
-
const { done, value } = await reader.read();
|
|
656
|
-
if (done) break;
|
|
657
|
-
chunks.push(value);
|
|
658
|
-
}
|
|
659
|
-
return new TextDecoder().decode(concatUint8(chunks));
|
|
660
|
-
}
|
|
661
656
|
function scanPages(dir) {
|
|
662
657
|
const pages = [];
|
|
663
658
|
function walk(current) {
|
|
@@ -887,7 +882,16 @@ var TsxInstance = class {
|
|
|
887
882
|
if (!nfMod) return new Response("Not Found", { status: 404 });
|
|
888
883
|
const NfComponent = nfMod.default;
|
|
889
884
|
let element = createElement(TsxContext.Provider, {
|
|
890
|
-
value: {
|
|
885
|
+
value: {
|
|
886
|
+
params: ctx.params,
|
|
887
|
+
query: ctx.query,
|
|
888
|
+
user: ctx.user,
|
|
889
|
+
parsed: ctx.parsed,
|
|
890
|
+
prefs: ctx.prefs,
|
|
891
|
+
locale: ctx.locale,
|
|
892
|
+
theme: ctx.theme,
|
|
893
|
+
t: ctx.t
|
|
894
|
+
}
|
|
891
895
|
}, createElement(NfComponent, { params: ctx.params, query: ctx.query }));
|
|
892
896
|
for (let i = rootLayouts.length - 1; i >= 0; i--) {
|
|
893
897
|
const LMod = this.layoutModules.get(rootLayouts[i]);
|
|
@@ -895,23 +899,12 @@ var TsxInstance = class {
|
|
|
895
899
|
element = createElement(LMod.default, { children: element });
|
|
896
900
|
}
|
|
897
901
|
const stream = await renderToReadableStream(element);
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
`<link rel="stylesheet" href="${base}/__wfw/style.css" />
|
|
905
|
-
</head>`
|
|
906
|
-
);
|
|
907
|
-
}
|
|
908
|
-
if (isDev) {
|
|
909
|
-
html += `
|
|
910
|
-
<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${base}/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
|
|
911
|
-
}
|
|
912
|
-
return new Response(html, {
|
|
913
|
-
status: 404,
|
|
914
|
-
headers: { "content-type": "text/html; charset=utf-8" }
|
|
902
|
+
return streamResponse(stream, {
|
|
903
|
+
ctx,
|
|
904
|
+
base,
|
|
905
|
+
compiledTailwindCss: this.compiledTailwindCss,
|
|
906
|
+
isDev,
|
|
907
|
+
status: 404
|
|
915
908
|
});
|
|
916
909
|
};
|
|
917
910
|
this.router.all("/*", handler);
|
|
@@ -972,16 +965,26 @@ ${src}`;
|
|
|
972
965
|
}
|
|
973
966
|
}
|
|
974
967
|
// ── client bundle ─────────────────────────────────────────────────────────
|
|
975
|
-
async buildClientBundle(entryPath,
|
|
968
|
+
async buildClientBundle(entryPath, layoutPaths, pagesDir) {
|
|
976
969
|
try {
|
|
977
970
|
const code = [
|
|
978
971
|
`import{hydrateRoot}from'react-dom/client';`,
|
|
979
|
-
`import{createElement}from'react';`,
|
|
972
|
+
`import{createElement,useState,useEffect}from'react';`,
|
|
973
|
+
`import{TsxContext}from'weifuwu/react';`,
|
|
980
974
|
`import P from${JSON.stringify(entryPath)};`,
|
|
981
|
-
`const p=window.__WEIFUWU_PROPS;`,
|
|
982
975
|
`const c=document.getElementById('__weifuwu_root');`,
|
|
983
|
-
`
|
|
984
|
-
`
|
|
976
|
+
`if(!window.__WFW_ROOT){`,
|
|
977
|
+
`function App(){`,
|
|
978
|
+
`const[p,setP]=useState({C:P,props:window.__WEIFUWU_PROPS});`,
|
|
979
|
+
`useEffect(()=>{window.__WFW_SET_PAGE=(C,props)=>setP({C,props})},[]);`,
|
|
980
|
+
`const ctx=window.__WEIFUWU_CTX||{params:{},query:{}};`,
|
|
981
|
+
`return createElement(TsxContext.Provider,{value:ctx},`,
|
|
982
|
+
`createElement(p.C,p.props))`,
|
|
983
|
+
`}`,
|
|
984
|
+
`window.__WFW_ROOT=hydrateRoot(c,createElement(App));`,
|
|
985
|
+
`}else{`,
|
|
986
|
+
`window.__WFW_SET_PAGE?.(P,window.__WEIFUWU_PROPS);`,
|
|
987
|
+
`}`
|
|
985
988
|
].join("");
|
|
986
989
|
const publicEnv = {};
|
|
987
990
|
for (const key of Object.keys(process.env)) {
|
|
@@ -989,13 +992,16 @@ ${src}`;
|
|
|
989
992
|
publicEnv[`process.env.${key}`] = JSON.stringify(process.env[key]);
|
|
990
993
|
}
|
|
991
994
|
}
|
|
995
|
+
const weifuwuAlias = {
|
|
996
|
+
"weifuwu/react": resolve2(__dirname, "react.ts")
|
|
997
|
+
};
|
|
992
998
|
const result = await esbuild.build({
|
|
993
999
|
stdin: { contents: code, loader: "tsx", resolveDir: pagesDir },
|
|
994
1000
|
bundle: true,
|
|
995
1001
|
format: "esm",
|
|
996
1002
|
jsx: "automatic",
|
|
997
1003
|
jsxImportSource: "react",
|
|
998
|
-
alias: resolveAliases(),
|
|
1004
|
+
alias: { ...resolveAliases(), ...weifuwuAlias },
|
|
999
1005
|
banner: { js: "self.process={env:{}};" },
|
|
1000
1006
|
define: Object.keys(publicEnv).length > 0 ? publicEnv : void 0,
|
|
1001
1007
|
loader: { ".node": "empty" },
|
|
@@ -1053,7 +1059,16 @@ ${src}`;
|
|
|
1053
1059
|
"div",
|
|
1054
1060
|
{ id: "__weifuwu_root" },
|
|
1055
1061
|
createElement(TsxContext.Provider, {
|
|
1056
|
-
value: {
|
|
1062
|
+
value: {
|
|
1063
|
+
params: ctx.params,
|
|
1064
|
+
query: ctx.query,
|
|
1065
|
+
user: ctx.user,
|
|
1066
|
+
parsed: ctx.parsed,
|
|
1067
|
+
prefs: ctx.prefs,
|
|
1068
|
+
locale: ctx.locale,
|
|
1069
|
+
theme: ctx.theme,
|
|
1070
|
+
t: ctx.t
|
|
1071
|
+
}
|
|
1057
1072
|
}, createElement(Component, allProps))
|
|
1058
1073
|
);
|
|
1059
1074
|
if (layoutPaths.length === 0) {
|
|
@@ -1082,35 +1097,15 @@ ${src}`;
|
|
|
1082
1097
|
);
|
|
1083
1098
|
}
|
|
1084
1099
|
}
|
|
1085
|
-
const stream = await renderToReadableStream(element);
|
|
1086
|
-
const body = await readStream(stream);
|
|
1087
|
-
if (layoutPaths.length > 0 && (body.match(/__weifuwu_root/g) || []).length > 1) {
|
|
1088
|
-
console.warn(
|
|
1089
|
-
'[weifuwu/tsx] <div id="__weifuwu_root"> is auto-injected by the framework. Remove the duplicate from your root layout to avoid hydration conflicts.'
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
const scripts = [];
|
|
1093
|
-
scripts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(allProps)}</script>`);
|
|
1094
1100
|
const bundle = await this.getOrBuildClientBundle(entryPath, layoutPaths, this.pagesDir);
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
"</head>",
|
|
1104
|
-
`<link rel="stylesheet" href="${base}/__wfw/style.css" />
|
|
1105
|
-
</head>`
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1108
|
-
if (isDev) {
|
|
1109
|
-
html += `
|
|
1110
|
-
<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${base}/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
|
|
1111
|
-
}
|
|
1112
|
-
return new Response(html, {
|
|
1113
|
-
headers: { "content-type": "text/html; charset=utf-8" }
|
|
1101
|
+
const stream = await renderToReadableStream(element);
|
|
1102
|
+
return streamResponse(stream, {
|
|
1103
|
+
ctx,
|
|
1104
|
+
base,
|
|
1105
|
+
compiledTailwindCss: this.compiledTailwindCss,
|
|
1106
|
+
isDev,
|
|
1107
|
+
bundle,
|
|
1108
|
+
allProps
|
|
1114
1109
|
});
|
|
1115
1110
|
};
|
|
1116
1111
|
}
|
|
@@ -1276,6 +1271,101 @@ ${body}`;
|
|
|
1276
1271
|
}
|
|
1277
1272
|
}
|
|
1278
1273
|
};
|
|
1274
|
+
function streamResponse(reactStream, opts) {
|
|
1275
|
+
const decoder = new TextDecoder();
|
|
1276
|
+
const encoder2 = new TextEncoder();
|
|
1277
|
+
const headPayload = buildHeadPayload(opts);
|
|
1278
|
+
let buffer = "";
|
|
1279
|
+
let headFlushed = false;
|
|
1280
|
+
let extractedHead = "";
|
|
1281
|
+
const output = new ReadableStream({
|
|
1282
|
+
async start(controller) {
|
|
1283
|
+
const reader = reactStream.getReader();
|
|
1284
|
+
async function push(chunk) {
|
|
1285
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1286
|
+
if (!extractedHead) {
|
|
1287
|
+
const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
|
|
1288
|
+
if (m) {
|
|
1289
|
+
extractedHead = m[1];
|
|
1290
|
+
buffer = buffer.replace(m[0], "");
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (!headFlushed) {
|
|
1294
|
+
const idx = buffer.indexOf("</head>");
|
|
1295
|
+
if (idx !== -1) {
|
|
1296
|
+
const before = buffer.slice(0, idx);
|
|
1297
|
+
let injection = "";
|
|
1298
|
+
if (extractedHead) injection += "\n" + extractedHead;
|
|
1299
|
+
injection += headPayload;
|
|
1300
|
+
controller.enqueue(encoder2.encode(before + injection));
|
|
1301
|
+
buffer = buffer.slice(idx);
|
|
1302
|
+
headFlushed = true;
|
|
1303
|
+
}
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
controller.enqueue(encoder2.encode(buffer));
|
|
1307
|
+
buffer = "";
|
|
1308
|
+
}
|
|
1309
|
+
while (true) {
|
|
1310
|
+
const { done, value } = await reader.read();
|
|
1311
|
+
if (done) break;
|
|
1312
|
+
await push(value);
|
|
1313
|
+
}
|
|
1314
|
+
if (buffer) controller.enqueue(encoder2.encode(buffer));
|
|
1315
|
+
const body = buildBodyScripts(opts);
|
|
1316
|
+
if (body) controller.enqueue(encoder2.encode("\n" + body));
|
|
1317
|
+
if (opts.isDev) {
|
|
1318
|
+
controller.enqueue(encoder2.encode(
|
|
1319
|
+
`
|
|
1320
|
+
<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${opts.base}/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`
|
|
1321
|
+
));
|
|
1322
|
+
}
|
|
1323
|
+
controller.close();
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
return new Response(output, {
|
|
1327
|
+
status: opts.status ?? 200,
|
|
1328
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
function buildHeadPayload(opts) {
|
|
1332
|
+
const { ctx, base, compiledTailwindCss } = opts;
|
|
1333
|
+
let result = "";
|
|
1334
|
+
if (ctx.theme) {
|
|
1335
|
+
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>
|
|
1336
|
+
`;
|
|
1337
|
+
}
|
|
1338
|
+
if (compiledTailwindCss) {
|
|
1339
|
+
result += `<link rel="stylesheet" href="${base}/__wfw/style.css" />
|
|
1340
|
+
`;
|
|
1341
|
+
}
|
|
1342
|
+
const localeData = globalThis.__LOCALE_DATA__;
|
|
1343
|
+
if (localeData && Object.keys(localeData).length > 0) {
|
|
1344
|
+
result += `<script>window.__LOCALE_DATA__=${JSON.stringify(localeData)}</script>
|
|
1345
|
+
`;
|
|
1346
|
+
}
|
|
1347
|
+
const ctxData = {
|
|
1348
|
+
params: ctx.params,
|
|
1349
|
+
query: ctx.query,
|
|
1350
|
+
user: ctx.user,
|
|
1351
|
+
parsed: ctx.parsed
|
|
1352
|
+
};
|
|
1353
|
+
if (ctx.prefs) ctxData.prefs = ctx.prefs;
|
|
1354
|
+
if (ctx.locale) ctxData.locale = ctx.locale;
|
|
1355
|
+
if (ctx.theme) ctxData.theme = ctx.theme;
|
|
1356
|
+
result += `<script>window.__WEIFUWU_CTX=${JSON.stringify(ctxData)}</script>
|
|
1357
|
+
`;
|
|
1358
|
+
return result;
|
|
1359
|
+
}
|
|
1360
|
+
function buildBodyScripts(opts) {
|
|
1361
|
+
if (!opts.bundle) return "";
|
|
1362
|
+
const parts = [];
|
|
1363
|
+
if (opts.allProps) {
|
|
1364
|
+
parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.allProps)}</script>`);
|
|
1365
|
+
}
|
|
1366
|
+
parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
|
|
1367
|
+
return parts.join("\n");
|
|
1368
|
+
}
|
|
1279
1369
|
|
|
1280
1370
|
// tsx.ts
|
|
1281
1371
|
async function tsx(options) {
|
|
@@ -1777,7 +1867,7 @@ function upload(options) {
|
|
|
1777
1867
|
// rate-limit.ts
|
|
1778
1868
|
function rateLimit(options) {
|
|
1779
1869
|
const max = options?.max ?? 100;
|
|
1780
|
-
const
|
|
1870
|
+
const window2 = options?.window ?? 6e4;
|
|
1781
1871
|
const getKey = options?.key ?? ((req) => {
|
|
1782
1872
|
const forwarded = req.headers.get("x-forwarded-for");
|
|
1783
1873
|
if (forwarded) return forwarded.split(",")[0].trim();
|
|
@@ -1794,19 +1884,19 @@ function rateLimit(options) {
|
|
|
1794
1884
|
for (const [key, entry] of hits) {
|
|
1795
1885
|
if (entry.reset < now) hits.delete(key);
|
|
1796
1886
|
}
|
|
1797
|
-
},
|
|
1887
|
+
}, window2);
|
|
1798
1888
|
if (interval.unref) interval.unref();
|
|
1799
1889
|
const mw = async (req, ctx, next) => {
|
|
1800
1890
|
const key = getKey(req);
|
|
1801
1891
|
const now = Date.now();
|
|
1802
1892
|
const entry = hits.get(key);
|
|
1803
1893
|
if (!entry || entry.reset < now) {
|
|
1804
|
-
hits.set(key, { count: 1, reset: now +
|
|
1894
|
+
hits.set(key, { count: 1, reset: now + window2 });
|
|
1805
1895
|
const res2 = await next(req, ctx);
|
|
1806
1896
|
const headers2 = new Headers(res2.headers);
|
|
1807
1897
|
headers2.set("X-RateLimit-Limit", String(max));
|
|
1808
1898
|
headers2.set("X-RateLimit-Remaining", String(max - 1));
|
|
1809
|
-
headers2.set("X-RateLimit-Reset", String(Math.ceil((now +
|
|
1899
|
+
headers2.set("X-RateLimit-Reset", String(Math.ceil((now + window2) / 1e3)));
|
|
1810
1900
|
return new Response(res2.body, { status: res2.status, statusText: res2.statusText, headers: headers2 });
|
|
1811
1901
|
}
|
|
1812
1902
|
entry.count++;
|
|
@@ -6685,15 +6775,39 @@ function health(options) {
|
|
|
6685
6775
|
return r;
|
|
6686
6776
|
}
|
|
6687
6777
|
|
|
6688
|
-
//
|
|
6778
|
+
// preferences.ts
|
|
6689
6779
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
6690
6780
|
import { existsSync as existsSync4 } from "node:fs";
|
|
6691
6781
|
import { join as join4, resolve as resolve10 } from "node:path";
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6782
|
+
var defaults = {
|
|
6783
|
+
locale: { default: "en", cookie: "locale", fromAcceptLanguage: true },
|
|
6784
|
+
theme: { default: "system", cookie: "theme" }
|
|
6785
|
+
};
|
|
6786
|
+
function translate(msgs, key, params) {
|
|
6787
|
+
const msg = msgs[key] ?? key;
|
|
6788
|
+
if (!params) return msg;
|
|
6789
|
+
let result = msg;
|
|
6790
|
+
for (const [k, v] of Object.entries(params)) {
|
|
6791
|
+
result = result.replace(`{${k}}`, v);
|
|
6792
|
+
}
|
|
6793
|
+
return result;
|
|
6794
|
+
}
|
|
6795
|
+
function extractCookie(req, name) {
|
|
6796
|
+
const cookie = req.headers.get("cookie");
|
|
6797
|
+
if (!cookie) return null;
|
|
6798
|
+
for (const part of cookie.split(";")) {
|
|
6799
|
+
const [k, v] = part.trim().split("=");
|
|
6800
|
+
if (k === name && v) return decodeURIComponent(v);
|
|
6801
|
+
}
|
|
6802
|
+
return null;
|
|
6803
|
+
}
|
|
6804
|
+
function preferences(options) {
|
|
6805
|
+
const dir = options.dir ? resolve10(options.dir) : void 0;
|
|
6806
|
+
const localeOpts = { ...defaults.locale, ...options.locale };
|
|
6807
|
+
const themeOpts = { ...defaults.theme, ...options.theme };
|
|
6695
6808
|
const cache = /* @__PURE__ */ new Map();
|
|
6696
6809
|
async function load(locale) {
|
|
6810
|
+
if (!dir) return {};
|
|
6697
6811
|
const cached = cache.get(locale);
|
|
6698
6812
|
if (cached) return cached;
|
|
6699
6813
|
const filePath = join4(dir, `${locale}.json`);
|
|
@@ -6707,40 +6821,62 @@ function i18n(options) {
|
|
|
6707
6821
|
return {};
|
|
6708
6822
|
}
|
|
6709
6823
|
}
|
|
6710
|
-
|
|
6711
|
-
const
|
|
6712
|
-
const
|
|
6824
|
+
return async (req, ctx, next) => {
|
|
6825
|
+
const locale = detectLocale(req, localeOpts);
|
|
6826
|
+
const theme = detectTheme(req, themeOpts);
|
|
6827
|
+
ctx.prefs = { locale, theme };
|
|
6828
|
+
ctx.locale = locale;
|
|
6829
|
+
ctx.theme = theme;
|
|
6830
|
+
if (dir) {
|
|
6831
|
+
const msgs = await load(locale);
|
|
6832
|
+
ctx.t = (key, params) => translate(msgs, key, params);
|
|
6833
|
+
globalThis.__LOCALE_DATA__ = msgs;
|
|
6834
|
+
}
|
|
6835
|
+
ctx.setPref = (name, value) => {
|
|
6836
|
+
const cookieOpts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "SameSite=Lax"];
|
|
6837
|
+
const referer = req.headers.get("referer") || "/";
|
|
6838
|
+
return new Response(null, {
|
|
6839
|
+
status: 302,
|
|
6840
|
+
headers: {
|
|
6841
|
+
Location: referer,
|
|
6842
|
+
"Set-Cookie": cookieOpts.join("; ")
|
|
6843
|
+
}
|
|
6844
|
+
});
|
|
6845
|
+
};
|
|
6846
|
+
const flashVal = extractCookie(req, "flash");
|
|
6847
|
+
if (flashVal) {
|
|
6848
|
+
try {
|
|
6849
|
+
ctx.prefs.flash = JSON.parse(flashVal);
|
|
6850
|
+
} catch {
|
|
6851
|
+
ctx.prefs.flash = flashVal;
|
|
6852
|
+
}
|
|
6853
|
+
}
|
|
6854
|
+
const res = await next(req, ctx);
|
|
6855
|
+
if (flashVal) {
|
|
6856
|
+
const headers = new Headers(res.headers);
|
|
6857
|
+
headers.append("Set-Cookie", "flash=; Path=/; Max-Age=0");
|
|
6858
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
6859
|
+
}
|
|
6860
|
+
return res;
|
|
6861
|
+
};
|
|
6862
|
+
}
|
|
6863
|
+
function detectLocale(req, opts) {
|
|
6864
|
+
if (opts.cookie) {
|
|
6865
|
+
const fromCookie = extractCookie(req, opts.cookie);
|
|
6713
6866
|
if (fromCookie) return fromCookie;
|
|
6867
|
+
}
|
|
6868
|
+
if (opts.fromAcceptLanguage) {
|
|
6714
6869
|
const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.split("-")[0];
|
|
6715
6870
|
if (fromHeader) return fromHeader;
|
|
6716
|
-
return defaultLocale;
|
|
6717
|
-
}
|
|
6718
|
-
function t(localeMsgs, key, params) {
|
|
6719
|
-
let msg = localeMsgs[key];
|
|
6720
|
-
if (msg === void 0) msg = key;
|
|
6721
|
-
if (params) {
|
|
6722
|
-
for (const [k, v] of Object.entries(params)) {
|
|
6723
|
-
msg = msg.replace(`{${k}}`, v);
|
|
6724
|
-
}
|
|
6725
|
-
}
|
|
6726
|
-
return msg;
|
|
6727
6871
|
}
|
|
6728
|
-
return
|
|
6729
|
-
const locale = detect(req);
|
|
6730
|
-
const msgs = await load(locale);
|
|
6731
|
-
ctx.locale = locale;
|
|
6732
|
-
ctx.t = (key, params) => t(msgs, key, params);
|
|
6733
|
-
return next(req, ctx);
|
|
6734
|
-
};
|
|
6872
|
+
return opts.default;
|
|
6735
6873
|
}
|
|
6736
|
-
function
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
const [k, v] = part.trim().split("=");
|
|
6741
|
-
if (k === name && v) return decodeURIComponent(v);
|
|
6874
|
+
function detectTheme(req, opts) {
|
|
6875
|
+
if (opts.cookie) {
|
|
6876
|
+
const fromCookie = extractCookie(req, opts.cookie);
|
|
6877
|
+
if (fromCookie) return fromCookie;
|
|
6742
6878
|
}
|
|
6743
|
-
return
|
|
6879
|
+
return opts.default;
|
|
6744
6880
|
}
|
|
6745
6881
|
|
|
6746
6882
|
// seo.ts
|
|
@@ -8134,7 +8270,6 @@ export {
|
|
|
8134
8270
|
graphql,
|
|
8135
8271
|
health,
|
|
8136
8272
|
helmet,
|
|
8137
|
-
i18n,
|
|
8138
8273
|
iii,
|
|
8139
8274
|
loadEnv,
|
|
8140
8275
|
logdb,
|
|
@@ -8144,6 +8279,7 @@ export {
|
|
|
8144
8279
|
openai,
|
|
8145
8280
|
opencode,
|
|
8146
8281
|
postgres,
|
|
8282
|
+
preferences,
|
|
8147
8283
|
queue,
|
|
8148
8284
|
rateLimit,
|
|
8149
8285
|
redis,
|
|
@@ -8163,7 +8299,7 @@ export {
|
|
|
8163
8299
|
tool2 as tool,
|
|
8164
8300
|
tsx,
|
|
8165
8301
|
upload,
|
|
8166
|
-
|
|
8302
|
+
useCtx,
|
|
8167
8303
|
user,
|
|
8168
8304
|
validate
|
|
8169
8305
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Middleware } from './types.ts';
|
|
2
|
+
export interface PrefOptions {
|
|
3
|
+
dir?: string;
|
|
4
|
+
locale?: {
|
|
5
|
+
default?: string;
|
|
6
|
+
cookie?: string;
|
|
7
|
+
fromAcceptLanguage?: boolean;
|
|
8
|
+
};
|
|
9
|
+
theme?: {
|
|
10
|
+
default?: string;
|
|
11
|
+
cookie?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function preferences(options: PrefOptions): Middleware;
|
package/dist/react.d.ts
CHANGED
|
@@ -2,5 +2,8 @@ export { useWebsocket } from './use-websocket.ts';
|
|
|
2
2
|
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
|
-
export { Link, useNavigate, navigate } from './client-router.ts';
|
|
6
|
-
export { TsxContext,
|
|
5
|
+
export { Link, useNavigate, navigate, useNavigating } from './client-router.ts';
|
|
6
|
+
export { TsxContext, useCtx } from './tsx-context.ts';
|
|
7
|
+
export { Head } from './head.tsx';
|
|
8
|
+
export { createStore, useData, useQueryState } from './client-state.ts';
|
|
9
|
+
export type { StoreApi } from './client-state.ts';
|