weifuwu 0.18.18 → 0.19.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 CHANGED
@@ -15,8 +15,7 @@ serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
15
15
  ```
16
16
 
17
17
  ```ts
18
- import { serve, Router, preferences } from 'weifuwu'
19
- import { ssr, layout, liveReload } from 'weifuwu/ssr'
18
+ import { serve, Router, preferences, ssr, layout, liveReload } from 'weifuwu'
20
19
  const app = new Router()
21
20
  app.use(preferences({ dir: './locales' }))
22
21
  app.use(layout('./layouts/root.tsx'))
@@ -568,12 +567,12 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
568
567
 
569
568
  ---
570
569
 
571
- ## React SSR (weifuwu/ssr)
570
+ ## React SSR (weifuwu)
572
571
 
573
- Import from `'weifuwu/ssr'`:
572
+ Import from `'weifuwu'` (no separate ssr entry):
574
573
 
575
574
  ```ts
576
- import { ssr, layout, liveReload, errorBoundary, notFound, tailwind } from 'weifuwu/ssr'
575
+ import { ssr, layout, liveReload, errorBoundary, notFound, tailwind } from 'weifuwu'
577
576
  ```
578
577
 
579
578
  ### ssr(path) [β]
@@ -588,7 +587,8 @@ app.get('/about', ssr('./pages/about.tsx'))
588
587
  - Reads `ctx.layoutStack` (set by `layout()` middleware) and wraps the component from outer to inner
589
588
  - Injects hydration script pointing to the auto-generated client bundle at `/__ssr/[hash].js`
590
589
  - Serializes middleware-injected `ctx` data to `window.__WEIFUWU_CTX` for client-side hydration
591
- - Dev mode: injects live reload WebSocket script
590
+ - **Dev mode:** uses `createRoot` instead of `hydrateRoot` — all hooks (`useState`, `useEffect`) work correctly; SSR content is still streamed for fast first paint
591
+ - **Prod mode:** uses `hydrateRoot` for full SSR hydration
592
592
 
593
593
  ### layout(path) [β]
594
594
 
@@ -610,7 +610,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
610
610
 
611
611
  ### liveReload(opts) [β]
612
612
 
613
- Returns a `Router` that registers a WebSocket endpoint at `/__weifuwu/livereload` and starts a file watcher on the given directories. When a `.tsx` file changes, it clears the compile cache and broadcasts a reload to all connected browsers.
613
+ Returns a `Router` that registers a WebSocket endpoint at `/__weifuwu/livereload` and starts a file watcher on the given directories. When a `.tsx` file changes, it compiles a hot-update bundle and broadcasts it to all connected browsers.
614
614
 
615
615
  ```ts
616
616
  if (process.env.NODE_ENV !== 'production') {
@@ -618,6 +618,21 @@ if (process.env.NODE_ENV !== 'production') {
618
618
  }
619
619
  ```
620
620
 
621
+ **How HMR works:**
622
+
623
+ 1. File change detected → compiles the entry `page.tsx` (bundles all dependencies) with esbuild
624
+ 2. Browser receives WS message → `import()` the hot bundle
625
+ 3. Hot bundle calls `window.__WFW_REFRESH(newComponent)` → updates a stable proxy component
626
+ 4. Proxy reference unchanged → React reuses fiber tree → `useState` values preserved
627
+ 5. Sets `ctx.tick` to trigger re-render without page navigation
628
+
629
+ **Key properties:**
630
+
631
+ - **State preservation** — input fields, scroll position, and other `useState`-driven state survive edits
632
+ - **Entry hash isolation** — each `ssr()` route has its own entry hash; WS messages carry the hash so only matching tabs update
633
+ - **Lazy hydration cache** — hydration bundle is only rebuilt on actual page load, not on every file change
634
+ - **Compilation fallback** — if esbuild encounters a syntax error, `location.reload()` is called to show the error
635
+
621
636
  Mount without a path — the internal `/__weifuwu/livereload` route is invisible to the user. The `ssr()` function automatically injects the client-side WS script in dev mode.
622
637
 
623
638
  | Option | Type | Default | Description |
@@ -652,15 +667,15 @@ Returns a catch-all handler for 404 pages. Typically registered last:
652
667
  app.all('/*', notFound('./not-found.tsx'))
653
668
  ```
654
669
 
655
- ### tailwind(path) [α]
670
+ ### tailwind(cssPath) [α]
656
671
 
657
- Compiles Tailwind CSS v4 via `@tailwindcss/postcss` and serves it at `/__wfw/style.css`. In dev mode, watches the CSS file for changes.
672
+ Compiles Tailwind CSS v4 via `@tailwindcss/postcss` and serves it at `/__wfw/style.css`. The file is compiled once and cached; on `app.css` changes in dev mode, the style is pushed to connected browsers via liveReload's WebSocket.
658
673
 
659
674
  ```ts
660
- app.use(tailwind('./app.css'))
675
+ app.use(tailwind('./ui'))
661
676
  ```
662
677
 
663
- When `tailwind()` middleware is detected, `ssr()` automatically injects `<link rel="stylesheet" href="/__wfw/style.css" />` into the HTML `<head>`.
678
+ The `dir` argument is the directory containing `app.css`. When `tailwind()` middleware is detected, `ssr()` automatically injects `<link rel="stylesheet" href="/__wfw/style.css" />` into the HTML `<head>`.
664
679
 
665
680
  ### seo [β] + seoMiddleware [α]
666
681
 
@@ -893,14 +908,14 @@ function Toast() {
893
908
  Auto-detected when `NODE_ENV !== 'production'`. File watching + live reload via `liveReload()`:
894
909
 
895
910
  ```ts
896
- import { liveReload } from 'weifuwu/ssr'
911
+ import { liveReload } from 'weifuwu'
897
912
 
898
913
  if (process.env.NODE_ENV !== 'production') {
899
914
  app.use(liveReload({ dirs: ['./pages', './layouts'] }))
900
915
  }
901
916
  ```
902
917
 
903
- When a `.tsx` file changes, `ssr()` clears its compile cache and the browser auto-refreshes. No process restart needed.
918
+ When a `.tsx` file changes, the browser hot-updates without refreshing `useState` values are preserved. See `liveReload` section for details.
904
919
 
905
920
  Tailwind v4 auto-compile via `tailwind()` middleware:
906
921
 
package/cli.ts CHANGED
@@ -35,6 +35,24 @@ async function cmdInit(name: string) {
35
35
  const templateDir = join(pkgRoot, 'cli', 'template')
36
36
  await cp(templateDir, targetDir, { recursive: true })
37
37
 
38
+ // Rewrite local imports → package imports for the copied project
39
+ for (const file of ['app.ts', 'index.ts', 'ui/page.tsx']) {
40
+ const fp = join(targetDir, file)
41
+ let content = await readFile(fp, 'utf-8')
42
+ content = content
43
+ .replace(/from '\.\.\/\.\.\/index\.ts'/g, "from 'weifuwu'")
44
+ .replace(/from '\.\.\/\.\.\/\.\.\/react\.ts'/g, "from 'weifuwu/react'")
45
+ .replace(/import \{ join \} from 'node:path'\n/gm, '')
46
+ .replace(/const _ui = join\(import\.meta\.dirname, 'ui'\)\n/gm, '')
47
+ .replace(/const _loc = join\(import\.meta\.dirname, 'locales'\)\n\n/, '')
48
+ .replace(/preferences\(\{ dir: _loc, locale: \{ default: 'en' \}, theme: \{ default: 'system' \} \}\)/g, "preferences({ dir: './locales' })")
49
+ .replace(/tailwind\(_ui\)/g, "tailwind('./ui')")
50
+ .replace(/layout\(join\(_ui, 'layout\.tsx'\)\)/g, "layout('./ui/layout.tsx')")
51
+ .replace(/ssr\(join\(_ui, 'page\.tsx'\)\)/g, "ssr('./ui/page.tsx')")
52
+ .replace(/join\(import\.meta\.dirname, 'ui'\)/g, "'./ui'")
53
+ await writeFile(fp, content)
54
+ }
55
+
38
56
  // Write config files
39
57
  await writeFile(join(targetDir, 'package.json'), JSON.stringify({
40
58
  name,
package/dist/cli.js CHANGED
@@ -28,6 +28,12 @@ async function cmdInit(name) {
28
28
  await mkdir(targetDir, { recursive: true });
29
29
  const templateDir = join(pkgRoot, "cli", "template");
30
30
  await cp(templateDir, targetDir, { recursive: true });
31
+ for (const file of ["app.ts", "index.ts", "ui/page.tsx"]) {
32
+ const fp = join(targetDir, file);
33
+ let content = await readFile(fp, "utf-8");
34
+ content = content.replace(/from '\.\.\/\.\.\/index\.ts'/g, "from 'weifuwu'").replace(/from '\.\.\/\.\.\/\.\.\/react\.ts'/g, "from 'weifuwu/react'").replace(/import \{ join \} from 'node:path'\n/gm, "").replace(/const _ui = join\(import\.meta\.dirname, 'ui'\)\n/gm, "").replace(/const _loc = join\(import\.meta\.dirname, 'locales'\)\n\n/, "").replace(/preferences\(\{ dir: _loc, locale: \{ default: 'en' \}, theme: \{ default: 'system' \} \}\)/g, "preferences({ dir: './locales' })").replace(/tailwind\(_ui\)/g, "tailwind('./ui')").replace(/layout\(join\(_ui, 'layout\.tsx'\)\)/g, "layout('./ui/layout.tsx')").replace(/ssr\(join\(_ui, 'page\.tsx'\)\)/g, "ssr('./ui/page.tsx')").replace(/join\(import\.meta\.dirname, 'ui'\)/g, "'./ui'");
35
+ await writeFile(fp, content);
36
+ }
31
37
  await writeFile(join(targetDir, "package.json"), JSON.stringify({
32
38
  name,
33
39
  type: "module",
package/dist/compile.d.ts CHANGED
@@ -1,4 +1,12 @@
1
+ export declare function id(s: string): string;
1
2
  export declare function clearCompileCache(): void;
2
3
  export declare function compileTsx(path: string): Promise<any>;
3
4
  /** Dev hot-reload: CJS + in-memory + vm (faster than ESM + disk + import) */
4
5
  export declare function compileTsxDev(path: string): Promise<any>;
6
+ /** Build a single vendor bundle containing all needed vendor modules */
7
+ export declare function compileVendorBundle(): Promise<string>;
8
+ /** Hot-reload: ESM bundle, calls __WFW_REFRESH on import */
9
+ export declare function compileHotComponent(path: string): Promise<{
10
+ hash: string;
11
+ code: string;
12
+ }>;
package/dist/index.js CHANGED
@@ -376,13 +376,13 @@ var Router = class _Router {
376
376
  const mw = match.middlewares[index++];
377
377
  return mw(innerReq, ctx2, dispatch);
378
378
  }
379
- return await new Promise((resolve13) => {
379
+ return await new Promise((resolve14) => {
380
380
  try {
381
381
  upgradeSocket(router.wss, req, socket, head, match.handler, ctx2);
382
- resolve13(new Response(null, { status: 101 }));
382
+ resolve14(new Response(null, { status: 101 }));
383
383
  } catch {
384
384
  socket.destroy();
385
- resolve13(new Response("WebSocket upgrade failed", { status: 500 }));
385
+ resolve14(new Response("WebSocket upgrade failed", { status: 500 }));
386
386
  }
387
387
  });
388
388
  };
@@ -572,7 +572,7 @@ function sendHttpResponseOnSocket(socket, response) {
572
572
  }
573
573
 
574
574
  // tsx-context.ts
575
- import { createContext } from "react";
575
+ import { useSyncExternalStore, createContext } from "react";
576
576
  var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
577
577
  var KEY = "__WEIFUWU_CTX_STORE";
578
578
  function getStore() {
@@ -2845,7 +2845,7 @@ function createHub(opts) {
2845
2845
  }
2846
2846
  });
2847
2847
  }
2848
- function join7(key, ws) {
2848
+ function join8(key, ws) {
2849
2849
  if (!channels.has(key)) {
2850
2850
  channels.set(key, /* @__PURE__ */ new Set());
2851
2851
  redisSub?.subscribe(`${prefix}${key}`);
@@ -2886,7 +2886,7 @@ function createHub(opts) {
2886
2886
  await redisSub.quit();
2887
2887
  }
2888
2888
  }
2889
- return { join: join7, leave, broadcast, close };
2889
+ return { join: join8, leave, broadcast, close };
2890
2890
  }
2891
2891
 
2892
2892
  // queue/index.ts
@@ -4696,14 +4696,14 @@ function forkApp(opts) {
4696
4696
  return { child, port: opts.port };
4697
4697
  }
4698
4698
  function stopProcess(mp, timeout = 1e4) {
4699
- return new Promise((resolve13) => {
4699
+ return new Promise((resolve14) => {
4700
4700
  const timer = setTimeout(() => {
4701
4701
  mp.child.kill("SIGKILL");
4702
- resolve13();
4702
+ resolve14();
4703
4703
  }, timeout);
4704
4704
  mp.child.on("exit", () => {
4705
4705
  clearTimeout(timer);
4706
- resolve13();
4706
+ resolve14();
4707
4707
  });
4708
4708
  mp.child.kill("SIGTERM");
4709
4709
  });
@@ -5150,6 +5150,7 @@ import { createHash as createHash2 } from "node:crypto";
5150
5150
  import vm from "node:vm";
5151
5151
  import { createRequire } from "node:module";
5152
5152
  var _cjsRequire = createRequire(import.meta.url);
5153
+ var _userRequire = null;
5153
5154
  var OUT_DIR = ".weifuwu/ssr";
5154
5155
  var cache = /* @__PURE__ */ new Map();
5155
5156
  var externals = [
@@ -5247,6 +5248,62 @@ async function compileTsxDev(path2) {
5247
5248
  cache.set(absPath, mod);
5248
5249
  return mod;
5249
5250
  }
5251
+ var vendorBundle = null;
5252
+ async function compileVendorBundle() {
5253
+ if (vendorBundle) return vendorBundle;
5254
+ if (!_userRequire) _userRequire = createRequire(join2(process.cwd(), "package.json"));
5255
+ const modules = {
5256
+ "react": [],
5257
+ "react-dom": ["react"],
5258
+ "react-dom/client": ["react"],
5259
+ "react/jsx-runtime": ["react"]
5260
+ };
5261
+ for (const request of Object.keys(modules)) {
5262
+ const mod = _userRequire(request);
5263
+ const keys = Object.keys(mod).filter((k) => !k.startsWith("_") && k !== "default");
5264
+ modules[request] = keys;
5265
+ }
5266
+ const wfwMod = _userRequire("weifuwu/react");
5267
+ const wfwKeys = Object.keys(wfwMod).filter((k) => !k.startsWith("_") && k !== "default");
5268
+ const used = /* @__PURE__ */ new Set();
5269
+ const stmts = [""];
5270
+ for (const [request, keys] of Object.entries(modules)) {
5271
+ const unique = keys.filter((k) => !used.has(k) && used.add(k));
5272
+ if (unique.length > 0) stmts.push(`export { ${unique.join(", ")} } from ${JSON.stringify(request)};`);
5273
+ }
5274
+ const uidWfw = wfwKeys.filter((k) => !used.has(k) && used.add(k));
5275
+ if (uidWfw.length > 0) stmts.push(`export { ${uidWfw.join(", ")} } from 'weifuwu/react';`);
5276
+ const result = await esbuild.build({
5277
+ stdin: { contents: stmts.join("\n"), resolveDir: process.cwd() },
5278
+ format: "esm",
5279
+ bundle: true,
5280
+ write: false
5281
+ });
5282
+ vendorBundle = new TextDecoder().decode(result.outputFiles[0].contents);
5283
+ return vendorBundle;
5284
+ }
5285
+ async function compileHotComponent(path2) {
5286
+ const absPath = resolve3(path2);
5287
+ const h = id(absPath);
5288
+ const stdin = `import C from ${JSON.stringify(absPath)};
5289
+ (window.__WFW_REFRESH||function(){})(C)`;
5290
+ const result = await esbuild.build({
5291
+ stdin: { contents: stdin, loader: "tsx", resolveDir: dirname(absPath) },
5292
+ format: "esm",
5293
+ platform: "browser",
5294
+ jsx: "automatic",
5295
+ jsxImportSource: "react",
5296
+ bundle: true,
5297
+ external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu/react"],
5298
+ write: false
5299
+ });
5300
+ let code = new TextDecoder().decode(result.outputFiles[0].contents);
5301
+ if (code.includes("__require") && (code.includes('"react"') || code.includes("'react'"))) {
5302
+ code = `import __r from '/__wfw/v/bundle';
5303
+ ` + code.replace(/__require\(["']react["']\)/g, "__r");
5304
+ }
5305
+ return { hash: h, code };
5306
+ }
5250
5307
 
5251
5308
  // stream.ts
5252
5309
  import { TextDecoder as TextDecoder2, TextEncoder as TextEncoder2 } from "node:util";
@@ -5262,8 +5319,20 @@ function getPublicEnv() {
5262
5319
  return _publicEnv;
5263
5320
  }
5264
5321
  function buildHeadPayload(opts) {
5265
- const { ctx, base, compiledTailwindCss } = opts;
5322
+ const { ctx, base, compiledTailwindCss, isDev: isDev2 } = opts;
5266
5323
  let result = "";
5324
+ if (isDev2) {
5325
+ result += `<script type="importmap">{
5326
+ "imports": {
5327
+ "react": "/__wfw/v/bundle",
5328
+ "react-dom": "/__wfw/v/bundle",
5329
+ "react-dom/client": "/__wfw/v/bundle",
5330
+ "react/jsx-runtime": "/__wfw/v/bundle",
5331
+ "weifuwu/react": "/__wfw/v/bundle"
5332
+ }
5333
+ }</script>
5334
+ `;
5335
+ }
5267
5336
  if (ctx.prefs?.theme) {
5268
5337
  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>
5269
5338
  `;
@@ -5352,7 +5421,7 @@ function streamResponse(reactStream, opts) {
5352
5421
  if (opts.isDev) {
5353
5422
  controller.enqueue(encoder2.encode(
5354
5423
  `
5355
- <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`
5424
+ <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');var t=0;ws.onmessage=function(e){try{var m=JSON.parse(e.data);if(m.type==='component'){if(m.entry&&m.entry!==window.__WFW_ENTRY)return;import('/__wfw/h/'+m.hash+'?'+Date.now()).catch(function(){location.reload()});if(m.css){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css}return}if(m.type==='css'){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css;return}}catch(_){}if(e.data==='reload'&&Date.now()-t>1e3){t=Date.now();location.reload()}};ws.onclose=function(){if(Date.now()-t>1e3){t=Date.now();setTimeout(function(){location.reload()},500)}}})()</script>`
5356
5425
  ));
5357
5426
  }
5358
5427
  } catch {
@@ -5374,17 +5443,30 @@ var als = new AsyncLocalStorage();
5374
5443
  __registerAls(() => als.getStore());
5375
5444
  var isDev = process.env.NODE_ENV !== "production";
5376
5445
  var bundleCache = /* @__PURE__ */ new Map();
5446
+ var _bundleDirty = false;
5447
+ function markClientBundleDirty() {
5448
+ _bundleDirty = true;
5449
+ }
5450
+ function getBundle(key) {
5451
+ if (_bundleDirty) {
5452
+ bundleCache.clear();
5453
+ _bundleDirty = false;
5454
+ }
5455
+ return bundleCache.get(key);
5456
+ }
5457
+ function setBundle(key, buf) {
5458
+ if (_bundleDirty) {
5459
+ bundleCache.clear();
5460
+ _bundleDirty = false;
5461
+ }
5462
+ bundleCache.set(key, buf);
5463
+ }
5377
5464
  function id2(s) {
5378
5465
  return createHash3("md5").update(s).digest("hex").slice(0, 8);
5379
5466
  }
5380
5467
  function serializeLoaderData(ctx) {
5381
- const data = {};
5382
- for (const key of Object.keys(ctx)) {
5383
- if (!["params", "query", "mountPath", "layoutStack"].includes(key)) {
5384
- data[key] = ctx[key];
5385
- }
5386
- }
5387
- return data;
5468
+ const ld = ctx.loaderData;
5469
+ return ld && typeof ld === "object" ? ld : {};
5388
5470
  }
5389
5471
  async function buildClientBundle(entryPath, layoutPaths) {
5390
5472
  try {
@@ -5394,26 +5476,24 @@ async function buildClientBundle(entryPath, layoutPaths) {
5394
5476
  const _sc = `(function(){var k='__WEIFUWU_CTX_STORE';var s=typeof globalThis!='undefined'&&globalThis[k];if(!s)return function(){};return function(v){s._ctx={...s._ctx,...v};s._snapshot={params:s._ctx.params,query:s._ctx.query,user:s._ctx.user,parsed:s._ctx.parsed,prefs:s._ctx.prefs,env:s._ctx.env};s._listeners.forEach(function(fn){fn()})}})()`;
5395
5477
  const code = [
5396
5478
  layoutImports,
5397
- `import{hydrateRoot}from'react-dom/client';`,
5398
- `import{createElement,useState,useEffect}from'react';`,
5479
+ `${isDev ? "import{createRoot}from'react-dom/client';" : "import{hydrateRoot}from'react-dom/client';"}`,
5480
+ `import{createElement}from'react';`,
5399
5481
  `import{TsxContext}from'weifuwu/react';`,
5400
5482
  `import P from${JSON.stringify(absEntry)};`,
5401
5483
  `var setCtx=${_sc};`,
5402
5484
  `const c=document.getElementById('__weifuwu_root');`,
5403
5485
  `if(window.__WEIFUWU_PROPS)setCtx({loaderData:window.__WEIFUWU_PROPS});`,
5404
- `if(!window.__WFW_ROOT){`,
5486
+ // Dev: stable proxy chain — _P → _W (stable) → actual component
5487
+ isDev ? `const _W=function(props){return(_W._fn||P)(props)};_W._fn=P;const _P=function(props){return createElement(_W,props)};` : "",
5488
+ // Dev: HMR handler — updates proxy + re-renders root
5489
+ isDev ? `window.__WFW_ENTRY=${JSON.stringify(id2(absEntry))};window.__WFW_REFRESH=function(n){_W._fn=n;window.__WFW_ROOT.render(createElement(App))};` : "",
5405
5490
  `function App(){`,
5406
- `const[p,setP]=useState({C:P});`,
5407
- `useEffect(()=>{window.__WFW_SET_PAGE=(C)=>{setCtx({loaderData:window.__WEIFUWU_PROPS});setP({C})}},[]);`,
5408
5491
  `const ctx=window.__WEIFUWU_CTX||{};`,
5409
5492
  `return createElement(TsxContext.Provider,{value:ctx},`,
5410
- `createElement(p.C,null))`,
5493
+ isDev ? `createElement(_P,null))` : `createElement(P,null))`,
5411
5494
  `}`,
5412
- `window.__WFW_ROOT=hydrateRoot(c,createElement(App));`,
5413
- `}else{`,
5414
- `window.__WFW_SET_PAGE?.(P);`,
5415
- `}`
5416
- ].join("");
5495
+ isDev ? `window.__WFW_ROOT=createRoot(c);window.__WFW_ROOT.render(createElement(App));` : `hydrateRoot(c,createElement(App));`
5496
+ ].filter(Boolean).join("");
5417
5497
  const { default: esbuild2 } = await import("esbuild");
5418
5498
  const result = await esbuild2.build({
5419
5499
  stdin: { contents: code, loader: "tsx", resolveDir: dirname2(absEntry) },
@@ -5423,8 +5503,9 @@ async function buildClientBundle(entryPath, layoutPaths) {
5423
5503
  jsxImportSource: "react",
5424
5504
  banner: { js: "self.process={env:{}};" },
5425
5505
  loader: { ".node": "empty" },
5506
+ external: isDev ? ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu/react"] : void 0,
5426
5507
  write: false,
5427
- minify: true
5508
+ minify: !isDev
5428
5509
  });
5429
5510
  return result.outputFiles[0].contents;
5430
5511
  } catch (err) {
@@ -5437,7 +5518,7 @@ function ssr(path2) {
5437
5518
  const bundleKey = `/__ssr/${entryId}.js`;
5438
5519
  const r = new Router();
5439
5520
  r.get("/__ssr/:path", (req, ctx) => {
5440
- const buf = bundleCache.get("/__ssr/" + ctx.params.path);
5521
+ const buf = getBundle("/__ssr/" + ctx.params.path);
5441
5522
  return buf ? new Response(buf, {
5442
5523
  headers: { "content-type": "application/javascript; charset=utf-8" }
5443
5524
  }) : new Response("", { status: 404 });
@@ -5462,12 +5543,16 @@ function ssr(path2) {
5462
5543
  };
5463
5544
  return als.run(ctxValue, async () => {
5464
5545
  setCtx(ctxValue);
5546
+ if (ctxValue.parsed?.__localeData) {
5547
+ ;
5548
+ globalThis.__LOCALE_DATA__ = ctxValue.parsed.__localeData;
5549
+ }
5465
5550
  let element = createElement(
5466
- TsxContext.Provider,
5467
- { value: ctxValue },
5551
+ "div",
5552
+ { id: "__weifuwu_root" },
5468
5553
  createElement(
5469
- "div",
5470
- { id: "__weifuwu_root" },
5554
+ TsxContext.Provider,
5555
+ { value: ctxValue },
5471
5556
  createElement(Component, null)
5472
5557
  )
5473
5558
  );
@@ -5490,11 +5575,11 @@ function ssr(path2) {
5490
5575
  }
5491
5576
  }
5492
5577
  let bundle = null;
5493
- if (!bundleCache.has(bundleKey)) {
5578
+ if (!getBundle(bundleKey)) {
5494
5579
  const buf = await buildClientBundle(path2, layoutPaths);
5495
- if (buf) bundleCache.set(bundleKey, buf);
5580
+ if (buf) setBundle(bundleKey, buf);
5496
5581
  }
5497
- if (bundleCache.has(bundleKey)) {
5582
+ if (getBundle(bundleKey)) {
5498
5583
  bundle = { url: bundleKey };
5499
5584
  }
5500
5585
  const { renderToReadableStream } = await import("react-dom/server");
@@ -5515,78 +5600,30 @@ function ssr(path2) {
5515
5600
  // tailwind.ts
5516
5601
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
5517
5602
  import { join as join3, relative, resolve as resolve5 } from "node:path";
5518
-
5519
- // live.ts
5520
- import chokidar from "chokidar";
5521
- var clients = /* @__PURE__ */ new Set();
5522
- function broadcastReload() {
5523
- for (const ws of clients) {
5524
- try {
5525
- ws.send("reload");
5526
- } catch {
5527
- clients.delete(ws);
5528
- }
5529
- }
5530
- }
5531
- function liveReload(opts) {
5532
- const r = new Router();
5533
- r.ws("/__weifuwu/livereload", {
5534
- open(ws) {
5535
- clients.add(ws);
5536
- ws.on("close", () => clients.delete(ws));
5537
- ws.on("error", () => clients.delete(ws));
5538
- }
5539
- });
5540
- const watcher = chokidar.watch(opts.dirs, {
5541
- ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
5542
- ignoreInitial: true
5543
- });
5544
- watcher.on("change", async (filePath) => {
5545
- if (!/\.tsx?$/.test(filePath)) return;
5546
- clearCompileCache();
5547
- try {
5548
- await compileTsxDev(filePath);
5549
- } catch (e) {
5550
- console.error("live reload compile failed:", e);
5551
- }
5552
- broadcastReload();
5553
- });
5554
- r.close = () => {
5555
- watcher.close();
5556
- clients.clear();
5557
- };
5558
- return r;
5559
- }
5560
-
5561
- // tailwind.ts
5562
- var isDev2 = process.env.NODE_ENV !== "production";
5563
5603
  var extraSources = /* @__PURE__ */ new Set();
5604
+ var cssCache = /* @__PURE__ */ new Map();
5564
5605
  function tailwind(dir) {
5565
5606
  const cssDir = resolve5(dir);
5566
5607
  const cssPath = join3(cssDir, "app.css");
5567
- let compiledCss = "";
5568
- let twWatcher = null;
5569
5608
  const r = new Router();
5570
5609
  r.use(async (req, ctx, next) => {
5571
- if (!compiledCss) compiledCss = await compile(cssPath, cssDir);
5572
- ctx.compiledTailwindCss = compiledCss;
5573
- if (isDev2 && !twWatcher) {
5574
- twWatcher = watchFile(cssPath, () => {
5575
- compiledCss = "";
5576
- broadcastReload();
5577
- });
5610
+ if (!cssCache.has(cssPath)) {
5611
+ cssCache.set(cssPath, await compileTailwindCss(cssPath, cssDir));
5578
5612
  }
5613
+ ctx.compiledTailwindCss = cssCache.get(cssPath);
5579
5614
  return next(req, ctx);
5580
5615
  });
5581
5616
  r.get("/__wfw/style.css", async (req, ctx) => {
5582
- if (!compiledCss) compiledCss = await compile(cssPath, cssDir);
5583
- return new Response(compiledCss || "", {
5617
+ if (!cssCache.has(cssPath)) {
5618
+ cssCache.set(cssPath, await compileTailwindCss(cssPath, cssDir));
5619
+ }
5620
+ return new Response(cssCache.get(cssPath) || "", {
5584
5621
  headers: { "content-type": "text/css; charset=utf-8" }
5585
5622
  });
5586
5623
  });
5587
5624
  return r;
5588
5625
  }
5589
- async function compile(cssPath, cssDir) {
5626
+ async function compileTailwindCss(cssPath, cssDir) {
5590
5627
  try {
5591
5628
  if (!existsSync3(cssPath)) {
5592
5629
  mkdirSync2(cssDir, { recursive: true });
@@ -5603,20 +5640,13 @@ ${src}`;
5603
5640
  ${src}`;
5604
5641
  }
5605
5642
  const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
5643
+ cssCache.set(cssPath, result.css);
5606
5644
  return result.css;
5607
5645
  } catch (err) {
5608
5646
  console.warn("Tailwind CSS processing failed:", err.message);
5609
5647
  return "";
5610
5648
  }
5611
5649
  }
5612
- function watchFile(path2, onChange) {
5613
- let watcher = null;
5614
- import("chokidar").then((chokidar2) => {
5615
- watcher = chokidar2.default.watch(resolve5(path2), { persistent: false });
5616
- watcher.on("change", onChange);
5617
- });
5618
- return watcher;
5619
- }
5620
5650
 
5621
5651
  // layout.ts
5622
5652
  function layout(path2) {
@@ -5865,10 +5895,10 @@ function createBashTool(ctx) {
5865
5895
  return { stdout: "", stderr: "Command denied: potentially dangerous command", exitCode: 1 };
5866
5896
  }
5867
5897
  const cwd = workdir ? `${ctx.workspace}/${workdir}` : ctx.workspace;
5868
- return new Promise((resolve13) => {
5898
+ return new Promise((resolve14) => {
5869
5899
  const child = exec(command, { cwd, timeout: timeout * 1e3, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
5870
5900
  const truncated = stdout.length > 1e6 || stderr.length > 1e6;
5871
- resolve13({
5901
+ resolve14({
5872
5902
  stdout: stdout.slice(0, 1e6),
5873
5903
  stderr: stderr.slice(0, 1e6),
5874
5904
  exitCode: error?.code ?? 0,
@@ -6109,7 +6139,7 @@ function createQuestionTool(ctx) {
6109
6139
  options: z12.array(z12.string()).optional().describe("Optional multiple choice options")
6110
6140
  }),
6111
6141
  execute: async ({ question, options }, { toolCallId }) => {
6112
- return new Promise((resolve13, reject) => {
6142
+ return new Promise((resolve14, reject) => {
6113
6143
  const timeout = setTimeout(() => {
6114
6144
  ctx.pendingQuestions.delete(toolCallId);
6115
6145
  reject(new Error("Question timed out"));
@@ -6117,7 +6147,7 @@ function createQuestionTool(ctx) {
6117
6147
  ctx.pendingQuestions.set(toolCallId, {
6118
6148
  resolve: (answer) => {
6119
6149
  clearTimeout(timeout);
6120
- resolve13(answer);
6150
+ resolve14(answer);
6121
6151
  },
6122
6152
  reject: (err) => {
6123
6153
  clearTimeout(timeout);
@@ -6274,17 +6304,17 @@ async function buildRouter4(deps) {
6274
6304
  }
6275
6305
 
6276
6306
  // opencode/ws.ts
6277
- var clients2 = /* @__PURE__ */ new WeakMap();
6307
+ var clients = /* @__PURE__ */ new WeakMap();
6278
6308
  function createWSHandler2(deps) {
6279
6309
  const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
6280
6310
  return {
6281
6311
  open(ws, ctx) {
6282
6312
  const userId = ctx.user?.id ?? 0;
6283
6313
  const mountPath = ctx.mountPath ?? "";
6284
- clients2.set(ws, { userId, mountPath });
6314
+ clients.set(ws, { userId, mountPath });
6285
6315
  },
6286
6316
  async message(ws, ctx, data) {
6287
- const client = clients2.get(ws);
6317
+ const client = clients.get(ws);
6288
6318
  if (!client) return;
6289
6319
  let msg;
6290
6320
  try {
@@ -6381,17 +6411,17 @@ function createWSHandler2(deps) {
6381
6411
  }
6382
6412
  },
6383
6413
  close(ws) {
6384
- const client = clients2.get(ws);
6414
+ const client = clients.get(ws);
6385
6415
  if (client) {
6386
6416
  client.abortController?.abort();
6387
- clients2.delete(ws);
6417
+ clients.delete(ws);
6388
6418
  }
6389
6419
  },
6390
6420
  error(ws, _ctx, _err) {
6391
- const client = clients2.get(ws);
6421
+ const client = clients.get(ws);
6392
6422
  if (client) {
6393
6423
  client.abortController?.abort();
6394
- clients2.delete(ws);
6424
+ clients.delete(ws);
6395
6425
  }
6396
6426
  }
6397
6427
  };
@@ -6837,23 +6867,33 @@ function preferences(options) {
6837
6867
  const cached = cache2.get(locale);
6838
6868
  if (cached) return cached;
6839
6869
  const filePath = join6(dir, `${locale}.json`);
6840
- if (!existsSync5(filePath)) return {};
6841
- try {
6842
- const content = await readFile2(filePath, "utf-8");
6843
- const data = JSON.parse(content);
6844
- cache2.set(locale, data);
6845
- return data;
6846
- } catch {
6847
- return {};
6870
+ if (existsSync5(filePath)) {
6871
+ try {
6872
+ const content = await readFile2(filePath, "utf-8");
6873
+ const data = JSON.parse(content);
6874
+ cache2.set(locale, data);
6875
+ return data;
6876
+ } catch {
6877
+ return {};
6878
+ }
6879
+ }
6880
+ const short = locale.split("-")[0];
6881
+ if (short !== locale) {
6882
+ const fallback = cache2.get(short) || await load(short);
6883
+ if (fallback && Object.keys(fallback).length > 0) {
6884
+ cache2.set(locale, fallback);
6885
+ return fallback;
6886
+ }
6848
6887
  }
6888
+ return {};
6849
6889
  }
6850
6890
  return async (req, ctx, next) => {
6851
6891
  const url = new URL(req.url);
6852
- const langMatch = url.pathname.match(/^\/__lang\/(\w+)$/);
6892
+ const langMatch = url.pathname.match(/^\/__lang\/([\w-]+)$/);
6853
6893
  if (langMatch && req.method === "GET") {
6854
6894
  return handlePrefSwitch(req, langMatch[1], localeOpts.cookie, load);
6855
6895
  }
6856
- const themeMatch = url.pathname.match(/^\/__theme\/(\w+)$/);
6896
+ const themeMatch = url.pathname.match(/^\/__theme\/([\w-]+)$/);
6857
6897
  if (themeMatch && req.method === "GET") {
6858
6898
  return handlePrefSwitch(req, themeMatch[1], themeOpts.cookie, load);
6859
6899
  }
@@ -6900,7 +6940,7 @@ function detectLocale(req, opts) {
6900
6940
  if (fromCookie) return fromCookie;
6901
6941
  }
6902
6942
  if (opts.fromAcceptLanguage) {
6903
- const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.split("-")[0];
6943
+ const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
6904
6944
  if (fromHeader) return fromHeader;
6905
6945
  }
6906
6946
  return opts.default;
@@ -7863,12 +7903,12 @@ function iii(opts = {}) {
7863
7903
  const handler = async (payload) => {
7864
7904
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
7865
7905
  const invocationId = crypto6.randomUUID();
7866
- return new Promise((resolve13, reject) => {
7906
+ return new Promise((resolve14, reject) => {
7867
7907
  const timer = setTimeout(() => {
7868
7908
  pending.delete(invocationId);
7869
7909
  reject(new Error(`Invocation timed out for "${id3}"`));
7870
7910
  }, 3e4);
7871
- pending.set(invocationId, { resolve: resolve13, reject, timer });
7911
+ pending.set(invocationId, { resolve: resolve14, reject, timer });
7872
7912
  worker.ws.send(JSON.stringify({
7873
7913
  type: "invoke",
7874
7914
  invocation_id: invocationId,
@@ -8109,8 +8149,8 @@ function registerWorker(url) {
8109
8149
  function connect() {
8110
8150
  if (intentionalClose) return;
8111
8151
  ws = new WebSocket(url);
8112
- ready = new Promise((resolve13) => {
8113
- resolveReady = resolve13;
8152
+ ready = new Promise((resolve14) => {
8153
+ resolveReady = resolve14;
8114
8154
  });
8115
8155
  ws.onopen = () => {
8116
8156
  reconnectAttempt = 0;
@@ -8230,13 +8270,13 @@ function registerWorker(url) {
8230
8270
  }
8231
8271
  return Promise.resolve(fn(request.payload, ctx));
8232
8272
  }
8233
- return new Promise((resolve13, reject) => {
8273
+ return new Promise((resolve14, reject) => {
8234
8274
  const invocationId = genId();
8235
8275
  const timer = setTimeout(() => {
8236
8276
  pendingInvocations.delete(invocationId);
8237
8277
  reject(new Error(`Invocation timed out for "${request.function_id}"`));
8238
8278
  }, request.timeout_ms || 3e4);
8239
- pendingInvocations.set(invocationId, { resolve: resolve13, reject, timer });
8279
+ pendingInvocations.set(invocationId, { resolve: resolve14, reject, timer });
8240
8280
  send({
8241
8281
  type: "invoke",
8242
8282
  invocation_id: invocationId,
@@ -8257,6 +8297,125 @@ function registerWorker(url) {
8257
8297
  };
8258
8298
  }
8259
8299
 
8300
+ // live.ts
8301
+ import chokidar from "chokidar";
8302
+ import { existsSync as existsSync6 } from "node:fs";
8303
+ import { join as join7, resolve as resolve13 } from "node:path";
8304
+ var clients2 = /* @__PURE__ */ new Set();
8305
+ var hotBundleCache = /* @__PURE__ */ new Map();
8306
+ var hotKeys = [];
8307
+ var MAX_HOT = 10;
8308
+ function setHot(hash, code) {
8309
+ if (!hotBundleCache.has(hash)) {
8310
+ hotKeys.push(hash);
8311
+ if (hotKeys.length > MAX_HOT) {
8312
+ const old = hotKeys.shift();
8313
+ hotBundleCache.delete(old);
8314
+ }
8315
+ }
8316
+ hotBundleCache.set(hash, code);
8317
+ }
8318
+ function broadcastReload() {
8319
+ for (const ws of clients2) {
8320
+ try {
8321
+ ws.send("reload");
8322
+ } catch {
8323
+ clients2.delete(ws);
8324
+ }
8325
+ }
8326
+ }
8327
+ function broadcastCss(css) {
8328
+ const msg = JSON.stringify({ type: "css", css });
8329
+ for (const ws of clients2) {
8330
+ try {
8331
+ ws.send(msg);
8332
+ } catch {
8333
+ clients2.delete(ws);
8334
+ }
8335
+ }
8336
+ }
8337
+ function liveReload(opts) {
8338
+ const r = new Router();
8339
+ const entryPath = (() => {
8340
+ for (const dir of opts.dirs) {
8341
+ const p = join7(resolve13(dir), "page.tsx");
8342
+ if (existsSync6(p)) return p;
8343
+ }
8344
+ return "";
8345
+ })();
8346
+ r.get("/__wfw/v/bundle", async (req, ctx) => {
8347
+ const code = await compileVendorBundle();
8348
+ return new Response(code, {
8349
+ headers: { "content-type": "application/javascript; charset=utf-8" }
8350
+ });
8351
+ });
8352
+ r.get("/__wfw/h/:hash", async (req, ctx) => {
8353
+ const hash = ctx.params.hash.replace(/\.js$/i, "");
8354
+ const code = hotBundleCache.get(hash);
8355
+ if (!code) return new Response("", { status: 404 });
8356
+ return new Response(code, {
8357
+ headers: { "content-type": "application/javascript; charset=utf-8" }
8358
+ });
8359
+ });
8360
+ r.ws("/__weifuwu/livereload", {
8361
+ open(ws) {
8362
+ clients2.add(ws);
8363
+ ws.on("close", () => clients2.delete(ws));
8364
+ ws.on("error", () => clients2.delete(ws));
8365
+ }
8366
+ });
8367
+ const watcher = chokidar.watch(opts.dirs, {
8368
+ ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
8369
+ ignoreInitial: true
8370
+ });
8371
+ watcher.on("change", async (filePath) => {
8372
+ if (/\.tsx?$/i.test(filePath)) {
8373
+ clearCompileCache();
8374
+ markClientBundleDirty();
8375
+ try {
8376
+ const target = entryPath || filePath;
8377
+ await compileTsxDev(target);
8378
+ const { hash, code } = await compileHotComponent(target);
8379
+ setHot(hash, code);
8380
+ let css;
8381
+ for (const dir of opts.dirs) {
8382
+ const cssPath = join7(resolve13(dir), "app.css");
8383
+ if (existsSync6(cssPath)) {
8384
+ css = await compileTailwindCss(cssPath, resolve13(dir));
8385
+ }
8386
+ }
8387
+ const entry = entryPath ? id(entryPath) : "";
8388
+ const msg = { type: "component", hash, entry };
8389
+ if (css) msg.css = css;
8390
+ const str = JSON.stringify(msg);
8391
+ for (const ws of clients2) {
8392
+ try {
8393
+ ws.send(str);
8394
+ } catch {
8395
+ clients2.delete(ws);
8396
+ }
8397
+ }
8398
+ } catch (e) {
8399
+ console.error("live reload failed, fallback to full reload:", e);
8400
+ broadcastReload();
8401
+ }
8402
+ } else if (/\.css$/i.test(filePath)) {
8403
+ for (const dir of opts.dirs) {
8404
+ const cssPath = join7(resolve13(dir), "app.css");
8405
+ if (existsSync6(cssPath)) {
8406
+ const css = await compileTailwindCss(cssPath, resolve13(dir));
8407
+ if (css) broadcastCss(css);
8408
+ }
8409
+ }
8410
+ }
8411
+ });
8412
+ r.close = () => {
8413
+ watcher.close();
8414
+ clients2.clear();
8415
+ };
8416
+ return r;
8417
+ }
8418
+
8260
8419
  // not-found.ts
8261
8420
  function notFound(path2) {
8262
8421
  return async (req, ctx) => {
package/dist/react.js CHANGED
@@ -331,7 +331,7 @@ async function prefetchPage(href) {
331
331
  }
332
332
 
333
333
  // tsx-context.ts
334
- import { createContext } from "react";
334
+ import { useSyncExternalStore, createContext } from "react";
335
335
  var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
336
336
  var KEY = "__WEIFUWU_CTX_STORE";
337
337
  function getStore() {
@@ -350,22 +350,29 @@ function getStore() {
350
350
  return s;
351
351
  }
352
352
  var store = getStore();
353
+ var subscribe = (cb) => {
354
+ store._listeners.add(cb);
355
+ return () => {
356
+ store._listeners.delete(cb);
357
+ };
358
+ };
359
+ var getSnapshot = () => store._snapshot;
353
360
  function setCtx(value) {
354
361
  store._ctx = { ...store._ctx, ...value };
355
362
  store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, prefs: store._ctx.prefs, env: store._ctx.env };
356
363
  store._listeners.forEach((fn) => fn());
357
364
  }
358
365
  function useCtx() {
366
+ if (typeof window !== "undefined") {
367
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot);
368
+ return { ...snapshot, ...window.__WEIFUWU_CTX };
369
+ }
359
370
  const alsStore = store._alsGetStore?.();
360
- const base = alsStore ?? store._ctx;
361
- const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
362
- return { ...base, ...data };
371
+ return alsStore ?? store._ctx;
363
372
  }
364
373
  function useLoaderData() {
365
- const alsStore = store._alsGetStore?.();
366
- const base = alsStore ?? store._ctx;
367
- const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
368
- return { ...base, ...data }.loaderData;
374
+ const ctx = useCtx();
375
+ return ctx.loaderData;
369
376
  }
370
377
  var TsxContext = createContext(DEFAULT_CTX);
371
378
 
@@ -386,19 +393,19 @@ function createStore(initial) {
386
393
  state = { ...state, ...next };
387
394
  listeners.forEach((fn) => fn());
388
395
  };
389
- const subscribe = (listener) => {
396
+ const subscribe2 = (listener) => {
390
397
  listeners.add(listener);
391
398
  return () => {
392
399
  listeners.delete(listener);
393
400
  };
394
401
  };
395
402
  const useStore = ((selector) => useSyncExternalStore2(
396
- subscribe,
403
+ subscribe2,
397
404
  () => selector ? selector(state) : state
398
405
  ));
399
406
  useStore.getState = getState;
400
407
  useStore.setState = setState;
401
- useStore.subscribe = subscribe;
408
+ useStore.subscribe = subscribe2;
402
409
  return useStore;
403
410
  }
404
411
  var dataCache = /* @__PURE__ */ new Map();
@@ -478,7 +485,7 @@ function notifyQueryListeners() {
478
485
  window.dispatchEvent(new PopStateEvent("popstate"));
479
486
  }
480
487
  function useQueryState(key, defaultValue = "") {
481
- function getSnapshot() {
488
+ function getSnapshot2() {
482
489
  if (typeof window === "undefined") return defaultValue;
483
490
  const params = new URLSearchParams(window.location.search);
484
491
  return params.get(key) ?? defaultValue;
@@ -490,12 +497,12 @@ function useQueryState(key, defaultValue = "") {
490
497
  window.addEventListener("popstate", cb);
491
498
  return () => window.removeEventListener("popstate", cb);
492
499
  },
493
- getSnapshot,
500
+ getSnapshot2,
494
501
  () => defaultValue
495
502
  );
496
503
  const setValue = useCallback4((val) => {
497
504
  if (typeof window === "undefined") return;
498
- const resolved = typeof val === "function" ? val(getSnapshot()) : val;
505
+ const resolved = typeof val === "function" ? val(getSnapshot2()) : val;
499
506
  const url = new URL(window.location.href);
500
507
  if (resolved === defaultValue || resolved === "") {
501
508
  url.searchParams.delete(key);
@@ -510,7 +517,7 @@ function useQueryState(key, defaultValue = "") {
510
517
 
511
518
  // client-locale.ts
512
519
  function buildT() {
513
- const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
520
+ const messages = globalThis.__LOCALE_DATA__ || (typeof window !== "undefined" ? window.__LOCALE_DATA__ : null);
514
521
  if (!messages) return (key, _p, fb) => fb ?? key;
515
522
  return (key, params, fallback) => {
516
523
  const msg = key.split(".").reduce((o, k) => o?.[k], messages);
@@ -522,7 +529,7 @@ function buildT() {
522
529
  };
523
530
  }
524
531
  addInterceptor(async (url) => {
525
- const m = url.pathname.match(/^\/__lang\/(\w+)$/);
532
+ const m = url.pathname.match(/^\/__lang\/([\w-]+)$/);
526
533
  if (!m) return false;
527
534
  try {
528
535
  const res = await fetch(url.pathname, {
@@ -575,7 +582,7 @@ function applyTheme(theme) {
575
582
  }
576
583
  }
577
584
  addInterceptor(async (url) => {
578
- const m = url.pathname.match(/^\/__theme\/(\w+)$/);
585
+ const m = url.pathname.match(/^\/__theme\/([\w-]+)$/);
579
586
  if (!m) return false;
580
587
  try {
581
588
  const res = await fetch(url.pathname, {
package/dist/ssr.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import { Router } from './router.ts';
2
+ export declare function markClientBundleDirty(): void;
2
3
  export declare function ssr(path: string): Router;
@@ -1,3 +1,4 @@
1
1
  import { Router } from './router.ts';
2
2
  export declare function addTailwindSource(dir: string): void;
3
3
  export declare function tailwind(dir: string): Router;
4
+ export declare function compileTailwindCss(cssPath: string, cssDir: string): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.18.18",
3
+ "version": "0.19.0",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",