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/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 useTsx() {
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: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
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
- const body = await readStream(stream);
899
- let html = body.startsWith("<!DOCTYPE html>") ? body : `<!DOCTYPE html>
900
- ${body}`;
901
- if (this.compiledTailwindCss && html.includes("</head>")) {
902
- html = html.replace(
903
- "</head>",
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, _layoutPaths, pagesDir) {
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
- `const r=hydrateRoot(c,createElement(P,p));`,
984
- `window.__WEIFUWU_ROOT=r;`
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: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
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
- if (bundle) {
1096
- scripts.push(`<script type="module" src="${base}${bundle.url}"></script>`);
1097
- }
1098
- let html = body.startsWith("<!DOCTYPE html>") ? body : `<!DOCTYPE html>
1099
- ${body}`;
1100
- html += "\n" + scripts.join("\n");
1101
- if (this.compiledTailwindCss && html.includes("</head>")) {
1102
- html = html.replace(
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 window = options?.window ?? 6e4;
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
- }, window);
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 + window });
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 + window) / 1e3)));
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
- // i18n.ts
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
- function i18n(options) {
6693
- const dir = resolve10(options.dir);
6694
- const defaultLocale = options.defaultLocale ?? "en";
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
- function detect(req) {
6711
- const url = new URL(req.url);
6712
- const fromCookie = extractCookie(req, "locale");
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 async (req, ctx, next) => {
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 extractCookie(req, name) {
6737
- const cookie = req.headers.get("cookie");
6738
- if (!cookie) return null;
6739
- for (const part of cookie.split(";")) {
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 null;
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
- useTsx,
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, useTsx } from './tsx-context.ts';
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';