what-server 0.11.7 → 0.12.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.min.js CHANGED
@@ -1,11 +1,11 @@
1
- import{h as yt,getServerContext as mt,runWithServerContext as S,beginHeadCollection as Zt,endHeadCollection as Q}from"what-core";var St=new RegExp("[<>&\\u2028\\u2029]","g"),bt={60:"\\u003c",62:"\\u003e",38:"\\u0026",8232:"\\u2028",8233:"\\u2029"};function F(t){return JSON.stringify(t).replace(St,e=>bt[e.charCodeAt(0)])}import{mount as de,signal as le,batch as pe,getServerContext as At}from"what-core";var Tt=new Map;function kt(t){let e=t||At();return e&&e.islandStores instanceof Map?e.islandStores:null}function st(t){let e=typeof document<"u"?Tt:kt(t),n={};for(let[r,o]of e||[])n[r]=o._getSnapshot();return n}import{signal as A,batch as J}from"what-core";var k=null,at=!(typeof process<"u");function U(t){k=t}function _t(){return k}async function B(t,e){if(k&&k.revalidatePath)return k.revalidatePath(t,e);at&&console.warn(`[what] revalidatePath('${t}') had no effect: no cache engine is bound. Create a what-isr engine and bind it in your adapter (setRevalidationHandler).`)}async function W(t,e){if(k&&k.revalidateTag)return k.revalidateTag(t,e);at&&console.warn(`[what] revalidateTag('${t}') had no effect: no cache engine is bound.`)}var V=new Map;function Rt(){if(typeof document<"u"){let t=document.querySelector('meta[name="what-csrf-token"]');if(t)return t.getAttribute("content");let e=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);if(e)return decodeURIComponent(e[1])}return null}function z(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();if(typeof crypto<"u"&&crypto.getRandomValues){let t=new Uint8Array(16);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,"0")).join("")}throw new Error("[what] No secure random source available for CSRF token generation")}function it(t,e){if(!t||!e||t.length!==e.length)return!1;let n=0;for(let r=0;r<t.length;r++)n|=t.charCodeAt(r)^e.charCodeAt(r);return n===0}function X(t){return`<meta name="what-csrf-token" content="${String(t).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}">`}var Ct=0,Et=!1;function vt(){return!Et&&typeof process<"u",`a_${typeof crypto<"u"&&crypto.getRandomValues?Array.from(crypto.getRandomValues(new Uint8Array(6)),e=>e.toString(16).padStart(2,"0")).join(""):`c${(++Ct).toString(36)}_${Date.now().toString(36)}`}`}function Pt(t,e={}){let n=e.id||vt(),{onError:r,onSuccess:o,revalidate:a}=e;typeof window>"u"&&V.set(n,{fn:t,options:e});async function s(...i){if(typeof window>"u")return t(...i);let c=e.timeout||3e4,u=new AbortController,d=setTimeout(()=>u.abort(),c);try{let f=Rt(),l={"Content-Type":"application/json","X-What-Action":n};f&&(l["X-CSRF-Token"]=f);let p=await fetch("/__what_action",{method:"POST",headers:l,credentials:"same-origin",signal:u.signal,body:JSON.stringify({args:i})});if(!p.ok){let g=await p.json().catch(()=>({message:"Action failed"}));throw new Error(g.message||"Action failed")}let h=await p.json();if(o&&o(h),a)for(let g of a)dt(g);return h}catch(f){if(f.name==="AbortError"){let l=new Error(`Action "${n}" timed out after ${c}ms`);throw l.code="TIMEOUT",r&&r(l),l}throw r&&r(f),f}finally{clearTimeout(d)}}return s._actionId=n,s._isAction=!0,s}function ct(t,e={}){let{onSuccess:n,onError:r,resetOnSuccess:o=!0}=e;return async a=>{let s,i;a instanceof Event?(a.preventDefault(),i=a.target,s=new FormData(i)):s=a;let c={},u=!1;for(let[d,f]of s.entries())typeof File<"u"&&f instanceof File&&(u=!0),c[d]?Array.isArray(c[d])?c[d].push(f):c[d]=[c[d],f]:c[d]=f;try{let d=u?await t(c,s):await t(c);return n&&n(d,i),o&&i&&i.reset(),d}catch(d){throw r&&r(d,i),d}}}function ut(t){let e=A(!1),n=A(null),r=A(null);async function o(...a){e.set(!0),n.set(null);try{let s=await t(...a);return r.set(s),s}catch(s){throw n.set(s),s}finally{e.set(!1)}}return{trigger:o,isPending:()=>e(),error:()=>n(),data:()=>r(),reset:()=>{n.set(null),r.set(null)}}}function xt(t,e={}){let{resetOnSuccess:n=!0}=e,r={current:null},o=ut(ct(t,{resetOnSuccess:n}));function a(s){s.preventDefault();let i=new FormData(s.target);return r.current=s.target,o.trigger(i)}return{...o,handleSubmit:a,formRef:r}}function Ot(t,e){let n=A(t),r=A([]),o=A(t);function a(u){let d=e(n.peek(),u);J(()=>{r.set([...r.peek(),u]),n.set(d)})}function s(u,d){J(()=>{if(r.set(r.peek().filter(f=>f!==u)),d!==void 0){o.set(d);let f=d;for(let l of r.peek())f=e(f,l);n.set(f)}})}function i(u,d){J(()=>{let f=r.peek().filter(h=>h!==u);r.set(f);let l=d!==void 0?d:o.peek();o.set(l);let p=l;for(let h of f)p=e(p,h);n.set(p)})}async function c(u,d){a(u);try{let f=await d();return s(u,f),f}catch(f){throw i(u),f}}return{value:()=>n(),isPending:()=>r().length>0,addOptimistic:a,resolve:s,rollback:i,withOptimistic:c,set:u=>{n.set(u),o.set(u)}}}var x=new Map;function $t(t,e){return x.has(t)||x.set(t,new Set),x.get(t).add(e),()=>{x.get(t)?.delete(e)}}function dt(t){let e=x.get(t);if(e)for(let n of e)try{n()}catch(r){console.error("[what] Revalidation error:",r)}}function I(t,e,n,r={}){let{csrfToken:o,skipCsrf:a=!1}=r;if(!a){if(!o)return Promise.resolve({status:500,body:{message:"[what] CSRF token not configured. Pass { csrfToken: sessionToken } to handleActionRequest, or { skipCsrf: true } to explicitly opt out."}});let i=t?.headers?.["x-csrf-token"]||t?.headers?.["X-CSRF-Token"];if(!it(i,o))return Promise.resolve({status:403,body:{message:"Invalid CSRF token"}})}let s=V.get(e);return s?Array.isArray(n)?s.fn(...n).then(async i=>{let c=s.options||{};if(Array.isArray(c.revalidate))for(let u of c.revalidate)await B(u);if(Array.isArray(c.revalidateTags))for(let u of c.revalidateTags)await W(u);return{status:200,body:i}}).catch(i=>(console.error(`[what] Action "${e}" error:`,i),{status:500,body:{message:"Action failed"}})):Promise.resolve({status:400,body:{message:"Invalid action arguments"}}):Promise.resolve({status:404,body:{message:"Action not found"}})}function Ht(){return[...V.keys()]}function It(t,e={}){let{onSuccess:n,onError:r,onSettled:o}=e,a={isPending:A(!1),error:A(null),data:A(null)};async function s(...i){a.isPending.set(!0),a.error.set(null);try{let c=await t(...i);return a.data.set(c),n&&n(c,...i),c}catch(c){throw a.error.set(c),r&&r(c,...i),c}finally{a.isPending.set(!1),o&&o(a.data.peek(),a.error.peek(),...i)}}return{mutate:s,isPending:()=>a.isPending(),error:()=>a.error(),data:()=>a.data(),reset:()=>{a.error.set(null),a.data.set(null)}}}var Dt="/__what_action",lt=1024*1024;function Lt(t){if(!t)return{};if(typeof t.forEach=="function"&&typeof t.get=="function"){let n={};return t.forEach((r,o)=>{n[o.toLowerCase()]=r}),n}let e={};for(let n in t)e[n.toLowerCase()]=t[n];return e}function D(t,e){return{status:t,headers:{"content-type":"application/json"},body:JSON.stringify(e)}}function Y(t,e){return{status:t,headers:{"content-type":"text/html; charset=utf-8"},body:`<!DOCTYPE html><html><body><h1>${t}</h1><p>${String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}</p></body></html>`}}function ft(t){if(typeof t!="string"||!t.startsWith("/")||/^[/\\]{2}/.test(t)||t.includes("\\"))return null;try{let e=new URL(t,"http://localhost");return e.origin!=="http://localhost"?null:e.pathname+e.search}catch{return null}}function jt(t,e){let n=ft(t&&t._redirect);if(n)return n;let r=e.referer||e.referrer;if(r)try{let o=new URL(r,"http://localhost"),a=ft(o.pathname+o.search);if(a)return a}catch{}return"/"}var Mt=new Set(["_action","data-action","_csrf","what-csrf-token","_redirect"]);function O(t={}){let{getCsrfToken:e,skipCsrf:n=!1}=t;return async function(o){if((o.method||"POST").toUpperCase()!=="POST")return D(405,{message:"Method Not Allowed"});let s=Lt(o.headers),i=s["x-what-action"],c=s["content-type"]||"",u=!i&&c.includes("application/x-www-form-urlencoded"),d=n?void 0:e?await e(o):void 0;if(u){let h=o.body||{},g=h._action||h["data-action"]||o.query&&o.query.action;if(!g)return Y(400,'Missing action name (add a hidden "_action" field or ?action= query param)');let b={...s},C=h._csrf??h["what-csrf-token"];if(C&&!b["x-csrf-token"]&&(b["x-csrf-token"]=String(C)),!n&&e&&!d)return Y(403,"Missing CSRF token");let E={};for(let[v,M]of Object.entries(h))Mt.has(v)||(E[v]=M);let R=await I({headers:b},g,[E],{csrfToken:d,skipCsrf:n});return R.status===200?{status:303,headers:{location:jt(h,s)},body:""}:Y(R.status,R.body&&R.body.message||"Action failed")}if(!i)return D(400,{message:"Missing X-What-Action header"});if(!n&&e&&!d)return D(403,{message:"Missing CSRF token"});let l=(o.body||{}).args,p=await I({headers:s},i,l,{csrfToken:d,skipCsrf:n});return D(p.status,p.body)}}function Nt(t={}){let e=t.basePath||Dt,n=O(t);return async function(o,a,s){let[i,c]=(o.url||"").split("?");if(i!==e||(o.method||"").toUpperCase()!=="POST")return s?s():void 0;let u;try{let l=await Ft(o);u=L(l,o.headers["content-type"]||"")}catch(l){a.writeHead(l.code==="BODY_TOO_LARGE"?413:400,{"content-type":"application/json"}),a.end(JSON.stringify({message:l.code==="BODY_TOO_LARGE"?"Payload too large":"Invalid request body"}));return}let d=Object.fromEntries(new URLSearchParams(c||"")),f=await n({method:o.method,headers:o.headers,body:u,query:d});a.writeHead(f.status,f.headers),a.end(f.body)}}function L(t,e){if((e||"").includes("application/x-www-form-urlencoded")){let n={};for(let[r,o]of new URLSearchParams(String(t)))n[r]===void 0?n[r]=o:Array.isArray(n[r])?n[r].push(o):n[r]=[n[r],o];return n}return t==null||t===""?{}:JSON.parse(String(t))}async function K(t,e=lt){let n=Number(t.headers.get("content-length"));if(Number.isFinite(n)&&n>e)return{tooLarge:!0};let r=t.body;if(!r||typeof r.getReader!="function"){let i=await t.text();return Buffer.byteLength(i,"utf8")>e?{tooLarge:!0}:{raw:i}}let o=r.getReader(),a=[],s=0;for(;;){let{done:i,value:c}=await o.read();if(i)break;if(c){if(s+=c.byteLength,s>e){try{await o.cancel()}catch{}return{tooLarge:!0}}a.push(c)}}return{raw:Buffer.concat(a.map(i=>Buffer.from(i))).toString("utf8")}}function Ft(t){return new Promise((e,n)=>{let r=0,o=[];t.on("data",a=>{if(r+=a.length,r>lt){let s=new Error("Body too large");s.code="BODY_TOO_LARGE",n(s),t.destroy?.();return}o.push(a)}),t.on("end",()=>{if(o.length===0)return e("");e(Buffer.concat(o).toString("utf8"))}),t.on("error",n)})}function Ut(t={}){let e=O(t);return async function(n){let r={};try{let s=await K(n);if(s.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});r=L(s.raw,n.headers.get("content-type")||"")}catch{r={}}let o={};try{o=Object.fromEntries(new URL(n.url,"http://localhost").searchParams)}catch{}let a=await e({method:n.method,headers:n.headers,body:r,query:o});return new Response(a.body,{status:a.status,headers:a.headers})}}import{matchRoute as Bt,parseQuery as Wt}from"what-router/match";var Jt="/__what_action",Vt="/__what_revalidate",Z="what-csrf";function j(t){let e={};return t&&typeof t.forEach=="function"&&t.forEach((n,r)=>{e[r.toLowerCase()]=n}),e}function pt(t,e){if(!t)return null;let n=String(t).match(new RegExp(`(?:^|;\\s*)${e}=([^;]+)`));return n?decodeURIComponent(n[1]):null}async function zt(t){try{let e=await K(t);return e.tooLarge?{tooLarge:!0}:L(e.raw,t.headers.get("content-type")||"")}catch{return{}}}async function Xt(t){try{return await t.json()}catch{return{}}}function Yt(t){return async function(n){let{route:r,params:o,query:a,request:s}=n,i={default:r.component,loader:r.loader},c=n.csrfToken?{...t,csrfToken:n.csrfToken}:t;return{html:await ht(i,{params:o,query:a,request:s},c),status:200,tags:n.config&&n.config.tags||[],path:n.path}}}function G(t={}){let{routes:e=[],cache:n,render:r,revalidateWebhook:o,document:a={},notFound:s,basePath:i="",csrf:c=!0}=t,u=c!==!1&&!t.actionHandler,d=t.actionHandler||O(u?{getCsrfToken:l=>pt(l.headers&&l.headers.cookie,Z)}:{skipCsrf:!0}),f=r||Yt(a);return n&&(n.revalidatePath||n.revalidateTag)&&U({revalidatePath:n.revalidatePath,revalidateTag:n.revalidateTag}),async function(p){let h=new URL(p.url,"http://localhost"),g=h.pathname;if(i&&g.startsWith(i)&&(g=g.slice(i.length)||"/"),p.method==="POST"&&g===Jt){let y=await zt(p);if(y&&y.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});let T=await d({method:"POST",headers:j(p.headers),body:y,query:Object.fromEntries(h.searchParams)});return new Response(T.body,{status:T.status,headers:T.headers})}let b=null,C=null;if(u&&(b=pt(j(p.headers).cookie,Z),!b)){b=z();let T=j(p.headers)["x-forwarded-proto"]==="https"||h.protocol==="https:"||!0;C=`${Z}=${encodeURIComponent(b)}; Path=/; SameSite=Lax`+(T?"; Secure":"")}let E=y=>(C&&(y["set-cookie"]=C),y);if(p.method==="POST"&&g===Vt&&o){let y=await Xt(p),T=await o({headers:j(p.headers),body:y});return new Response(JSON.stringify(T.body),{status:T.status,headers:{"content-type":"application/json"}})}let R=Bt(g,e);if(!R){let y=s?s():"<!DOCTYPE html><html><body><h1>404 \u2014 Not Found</h1></body></html>";return new Response(y,{status:404,headers:E({"content-type":"text/html; charset=utf-8"})})}let{route:v,params:M}=R,N=v.page||{mode:v.mode||"client"},H={path:g,query:Wt(h.search),config:N,route:v,params:M,request:p};if(n&&N.mode!=="server"){let y=await n.handle(H,()=>f(H));return new Response(y.html,{status:y.status||200,headers:E({"content-type":"text/html; charset=utf-8",...y.headers||{}})})}b&&(H.csrfToken=b);let rt=await f(H),ot=E({"content-type":"text/html; charset=utf-8"});return N.mode==="server"&&(ot["Cache-Control"]="private, no-store"),new Response(rt.html,{status:rt.status||200,headers:ot})}}function Kt(t={}){let e=G(t);return{async fetch(n,r,o){return r&&(n.__env=r),o&&(n.__ctx=o),e(n)}}}function _(t){return{head:Zt(),loaderData:t,resources:new Map,islandStores:new Map,resourceCounter:0,boundaryCounter:0,suspended:[]}}var gt=0;function Gt(){gt=0}function Qt(){return"h"+gt++}function qt(t){if(typeof document>"u"&&!mt()){let e=_(void 0);return S(e,()=>qt(t))}return Gt(),P(t)}function P(t){if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return m(String(t));if(typeof t=="function"&&t._signal)return`<!--$-->${P(t())}<!--/$-->`;if(typeof t=="function")try{return`<!--$-->${P(t())}<!--/$-->`}catch{return typeof process<"u","<!--$--><!--/$-->"}if(Array.isArray(t))return`<!--[]-->${t.map(P).join("")}<!--/[]-->`;if(typeof t.tag=="function"){let c=Qt(),u=t.tag({...t.props,children:t.children}),d=P(u);return te(d,c)}let{tag:e,props:n,children:r}=t,o=et(n||{}),a=`<${e}${o}>`;if(nt.has(e))return a;let s=tt(n),i=s!=null?String(s):r.map(P).join("");return`${a}${i}</${e}>`}function te(t,e){let n=t.match(/^((?:<!--.*?-->)*)<([a-zA-Z][a-zA-Z0-9-]*)/);if(n){let r=n[1],o=n[2],a=r.length+1+o.length;return t.slice(0,a)+` data-hk="${e}"`+t.slice(a)}return t}function w(t){if(typeof document>"u"&&!mt()){let c=_(void 0);return S(c,()=>w(t))}if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return m(String(t));if(typeof t=="function"&&t._signal)return w(t());if(typeof t=="function")try{return w(t())}catch{return typeof process<"u",""}if(Array.isArray(t))return t.map(w).join("");if(t.tag==="__suspense")try{return(t.children||[]).map(w).join("")}catch(c){if(c&&typeof c.then=="function")return w(t.props&&t.props.fallback);throw c}if(typeof t.tag=="function"){let c=t.tag({...t.props,children:t.children});return w(c)}let{tag:e,props:n,children:r}=t,o=et(n||{}),a=`<${e}${o}>`;if(nt.has(e))return a;let s=tt(n),i=s!=null?String(s):r.map(w).join("");return`${a}${i}</${e}>`}function He(t){let e=_(void 0);return{body:S(e,()=>w(t)),head:Q(e.head)}}async function Ie(t,e={}){let n=t.default||t,r=typeof t.loader=="function"?await t.loader(e):void 0,o=_(r),a=e.params||{};return{body:S(o,()=>w(yt(n,{...a,loaderData:r}))),head:Q(o.head),loaderData:r}}var wt=12;async function ee(t,e){e||(e=_(void 0));let n="";for(let o=0;o<wt;o++){n=S(e,()=>w(t));let a=[...e.resources.values()].filter(s=>s.status==="pending").map(s=>s.promise);if(a.length===0)break;await Promise.all(a)}let r={};for(let[o,a]of e.resources)a.status==="ready"&&(r[o]=a.value);return{body:n,head:Q(e.head),loaderData:e.loaderData,resources:r,ctx:e}}async function ht(t,e={},n={}){let r=t.default||t,o=typeof t.loader=="function"?await t.loader(e):void 0,a=_(o),s=e.params||{},{body:i,head:c,resources:u}=await ee(yt(r,{...s,loaderData:o}),a),d={loaderData:o??null,resources:u,islandStores:st(a)};return ne({body:i,head:c,payload:d,options:n})}function ne({body:t,head:e,payload:n,options:r={}}){let o=r.lang||"en",a=`<script id="__what_data" type="application/json">${F(n)}<\/script>`,s=r.clientEntry?`<script type="module" src="${m(r.clientEntry)}"><\/script>`:"",c=(r.csrfToken?X(r.csrfToken):"")+(r.head||""),u=r.bodyClass?` class="${m(r.bodyClass)}"`:"";return`<!DOCTYPE html><html lang="${m(o)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${e||""}${c}</head><body${u}>${t}${a}${s}</body></html>`}async function*$(t,e){if(e===void 0&&(e=_(void 0)),t==null||t===!1||t===!0)return;if(typeof t=="string"||typeof t=="number"){yield m(String(t));return}if(typeof t=="function"&&t._signal){let s=S(e,()=>t());yield*$(s,e);return}if(typeof t=="function"){try{let s=S(e,()=>t());yield*$(s,e)}catch{typeof process<"u"}return}if(Array.isArray(t)){for(let s of t)yield*$(s,e);return}if(t.tag==="__suspense"){let s=null;for(let i=0;i<wt&&s===null;i++){let c=null;try{s=S(e,()=>(t.children||[]).map(w).join(""))}catch(u){if(u&&typeof u.then=="function")c=u;else throw u}if(s===null){let u=[...e.resources.values()].filter(d=>d.status==="pending").map(d=>d.promise);await Promise.all([c,...u].filter(Boolean))}}s===null&&(s=S(e,()=>w(t.props&&t.props.fallback))),yield s;return}if(typeof t.tag=="function"){try{let s=S(e,()=>t.tag({...t.props,children:t.children})),i=s instanceof Promise?await s:s;yield*$(i,e)}catch(s){typeof process<"u",yield q?`<!-- SSR Error: ${m(s.message||"Component error")} -->`:"<!-- SSR Error -->"}return}let{tag:n,props:r,children:o}=t,a=et(r||{});if(yield`<${n}${a}>`,!nt.has(n)){let s=tt(r);if(s!=null)yield String(s);else for(let i of o)yield*$(i,e);yield`</${n}>`}}function De(t){return{mode:"static",...t}}function Le(t,e={}){let n=_(e),r=S(n,()=>{let a=t.component(e);return w(a)}),o=t.islands||[];return re({title:t.title||"",meta:t.meta||{},body:r,islands:o,scripts:t.mode==="static"?[]:t.scripts||[],styles:t.styles||[],mode:t.mode})}function re({title:t,meta:e,body:n,islands:r,scripts:o,styles:a,mode:s}){let i=Object.entries(e).map(([l,p])=>`<meta name="${m(l)}" content="${m(p)}">`).join(`
2
- `),c=a.map(l=>`<link rel="stylesheet" href="${m(l)}">`).join(`
3
- `),u=r.length>0?`
1
+ import{h as wt,_isAriaAttr as ne,getServerContext as St,runWithServerContext as S,beginHeadCollection as re,endHeadCollection as tt}from"what-core";var At=new RegExp("[<>&\\u2028\\u2029]","g"),Tt={60:"\\u003c",62:"\\u003e",38:"\\u0026",8232:"\\u2028",8233:"\\u2029"};function J(t){return JSON.stringify(t).replace(At,e=>Tt[e.charCodeAt(0)])}import{mount as Se,hydrate as be,signal as _e,batch as Ae,getServerContext as kt}from"what-core";var Rt=new Map;function Et(t){let e=t||kt();return e&&e.islandStores instanceof Map?e.islandStores:null}function it(t){let e=typeof document<"u"?Rt:Et(t),n={};for(let[r,o]of e||[])n[r]=o._getSnapshot();return n}import{signal as _,batch as X}from"what-core";var T=null,ct=!(typeof process<"u");function W(t){T=t}function Ct(){return T}async function V(t,e){if(T&&T.revalidatePath)return T.revalidatePath(t,e);ct&&console.warn(`[what] revalidatePath('${t}') had no effect: no cache engine is bound. Create a what-isr engine and bind it in your adapter (setRevalidationHandler).`)}async function z(t,e){if(T&&T.revalidateTag)return T.revalidateTag(t,e);ct&&console.warn(`[what] revalidateTag('${t}') had no effect: no cache engine is bound.`)}var Y=new Map;function vt(){if(typeof document<"u"){let t=document.querySelector('meta[name="what-csrf-token"]');if(t)return t.getAttribute("content");let e=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);if(e)return decodeURIComponent(e[1])}return null}function K(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();if(typeof crypto<"u"&&crypto.getRandomValues){let t=new Uint8Array(16);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,"0")).join("")}throw new Error("[what] No secure random source available for CSRF token generation")}function dt(t,e){if(!t||!e||t.length!==e.length)return!1;let n=0;for(let r=0;r<t.length;r++)n|=t.charCodeAt(r)^e.charCodeAt(r);return n===0}function Z(t){return`<meta name="what-csrf-token" content="${String(t).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}">`}var Ot=0,xt=!1;function Pt(){return!xt&&typeof process<"u",`a_${typeof crypto<"u"&&crypto.getRandomValues?Array.from(crypto.getRandomValues(new Uint8Array(6)),e=>e.toString(16).padStart(2,"0")).join(""):`c${(++Ot).toString(36)}_${Date.now().toString(36)}`}`}function $t(t,e={}){let n=e.id||Pt(),{onError:r,onSuccess:o,revalidate:s}=e;typeof window>"u"&&Y.set(n,{fn:t,options:e});async function a(...i){if(typeof window>"u")return t(...i);let c=e.timeout||3e4,d=new AbortController,u=setTimeout(()=>d.abort(),c);try{let f=vt(),l={"Content-Type":"application/json","X-What-Action":n};f&&(l["X-CSRF-Token"]=f);let p=await fetch("/__what_action",{method:"POST",headers:l,credentials:"same-origin",signal:d.signal,body:JSON.stringify({args:i})});if(!p.ok){let g=await p.json().catch(()=>({message:"Action failed"}));throw new Error(g.message||"Action failed")}let h=await p.json();if(o&&o(h),s)for(let g of s)lt(g);return h}catch(f){if(f.name==="AbortError"){let l=new Error(`Action "${n}" timed out after ${c}ms`);throw l.code="TIMEOUT",r&&r(l),l}throw r&&r(f),f}finally{clearTimeout(u)}}return a._actionId=n,a._isAction=!0,a}function ut(t,e={}){let{onSuccess:n,onError:r,resetOnSuccess:o=!0}=e;return async s=>{let a,i;s instanceof Event?(s.preventDefault(),i=s.target,a=new FormData(i)):a=s;let c={},d=!1;for(let[u,f]of a.entries())typeof File<"u"&&f instanceof File&&(d=!0),c[u]?Array.isArray(c[u])?c[u].push(f):c[u]=[c[u],f]:c[u]=f;try{let u=d?await t(c,a):await t(c);return n&&n(u,i),o&&i&&i.reset(),u}catch(u){throw r&&r(u,i),u}}}function ft(t){let e=_(!1),n=_(null),r=_(null);async function o(...s){e.set(!0),n.set(null);try{let a=await t(...s);return r.set(a),a}catch(a){throw n.set(a),a}finally{e.set(!1)}}return{trigger:o,isPending:()=>e(),error:()=>n(),data:()=>r(),reset:()=>{n.set(null),r.set(null)}}}function It(t,e={}){let{resetOnSuccess:n=!0}=e,r={current:null},o=ft(ut(t,{resetOnSuccess:n}));function s(a){a.preventDefault();let i=new FormData(a.target);return r.current=a.target,o.trigger(i)}return{...o,handleSubmit:s,formRef:r}}function Ht(t,e){let n=_(t),r=_([]),o=_(t);function s(d){let u=e(n.peek(),d);X(()=>{r.set([...r.peek(),d]),n.set(u)})}function a(d,u){X(()=>{if(r.set(r.peek().filter(f=>f!==d)),u!==void 0){o.set(u);let f=u;for(let l of r.peek())f=e(f,l);n.set(f)}})}function i(d,u){X(()=>{let f=r.peek().filter(h=>h!==d);r.set(f);let l=u!==void 0?u:o.peek();o.set(l);let p=l;for(let h of f)p=e(p,h);n.set(p)})}async function c(d,u){s(d);try{let f=await u();return a(d,f),f}catch(f){throw i(d),f}}return{value:()=>n(),isPending:()=>r().length>0,addOptimistic:s,resolve:a,rollback:i,withOptimistic:c,set:d=>{n.set(d),o.set(d)}}}var P=new Map;function Nt(t,e){return P.has(t)||P.set(t,new Set),P.get(t).add(e),()=>{P.get(t)?.delete(e)}}function lt(t){let e=P.get(t);if(e)for(let n of e)try{n()}catch(r){console.error("[what] Revalidation error:",r)}}function N(t,e,n,r={}){let{csrfToken:o,skipCsrf:s=!1}=r;if(!s){if(!o)return Promise.resolve({status:500,body:{message:"[what] CSRF token not configured. Pass { csrfToken: sessionToken } to handleActionRequest, or { skipCsrf: true } to explicitly opt out."}});let i=t?.headers?.["x-csrf-token"]||t?.headers?.["X-CSRF-Token"];if(!dt(i,o))return Promise.resolve({status:403,body:{message:"Invalid CSRF token"}})}let a=Y.get(e);return a?Array.isArray(n)?a.fn(...n).then(async i=>{let c=a.options||{};if(Array.isArray(c.revalidate))for(let d of c.revalidate)await V(d);if(Array.isArray(c.revalidateTags))for(let d of c.revalidateTags)await z(d);return{status:200,body:i}}).catch(i=>(console.error(`[what] Action "${e}" error:`,i),{status:500,body:{message:"Action failed"}})):Promise.resolve({status:400,body:{message:"Invalid action arguments"}}):Promise.resolve({status:404,body:{message:"Action not found"}})}function Dt(){return[...Y.keys()]}function jt(t,e={}){let{onSuccess:n,onError:r,onSettled:o}=e,s={isPending:_(!1),error:_(null),data:_(null)};async function a(...i){s.isPending.set(!0),s.error.set(null);try{let c=await t(...i);return s.data.set(c),n&&n(c,...i),c}catch(c){throw s.error.set(c),r&&r(c,...i),c}finally{s.isPending.set(!1),o&&o(s.data.peek(),s.error.peek(),...i)}}return{mutate:a,isPending:()=>s.isPending(),error:()=>s.error(),data:()=>s.data(),reset:()=>{s.error.set(null),s.data.set(null)}}}import{h as D}from"what-core";var pt="/__what_action";function Lt(t){if(typeof t=="string")return t;if(typeof t=="function"){let e=t._actionId;if(e)return e;throw new Error("[what] <Form action={fn}>: that function is not a server action. Wrap it with action() from what-server, or pass the action id as a string.")}throw new Error("[what] <Form> requires an `action` prop: a server action or its id.")}function Mt(){if(typeof document>"u")return null;let t=document.querySelector('meta[name="what-csrf-token"]');if(t)return t.getAttribute("content");let e=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);return e?decodeURIComponent(e[1]):null}function Ft({action:t,csrfToken:e,redirect:n,method:r="post",enhance:o=!0,children:s,...a}){let i=Lt(t),c=e!==void 0?e:Mt();c==null&&typeof process<"u";let d=[D("input",{type:"hidden",name:"_action",value:i}),D("input",{type:"hidden",name:"what-csrf-token",value:c||""})];n&&d.push(D("input",{type:"hidden",name:"_redirect",value:n}));let u={...a,method:r,action:a.action||pt,"data-action":i};o&&(u["data-enhance"]="");let f=s==null?[]:Array.isArray(s)?s:[s];return D("form",u,...d,...f)}var Ut="/__what_action",yt=1024*1024;function Bt(t){if(!t)return{};if(typeof t.forEach=="function"&&typeof t.get=="function"){let n={};return t.forEach((r,o)=>{n[o.toLowerCase()]=r}),n}let e={};for(let n in t)e[n.toLowerCase()]=t[n];return e}function j(t,e){return{status:t,headers:{"content-type":"application/json"},body:JSON.stringify(e)}}function G(t,e){return{status:t,headers:{"content-type":"text/html; charset=utf-8"},body:`<!DOCTYPE html><html><body><h1>${t}</h1><p>${String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}</p></body></html>`}}function ht(t){if(typeof t!="string"||!t.startsWith("/")||/^[/\\]{2}/.test(t)||t.includes("\\"))return null;try{let e=new URL(t,"http://localhost");return e.origin!=="http://localhost"?null:e.pathname+e.search}catch{return null}}function Jt(t,e){let n=ht(t&&t._redirect);if(n)return n;let r=e.referer||e.referrer;if(r)try{let o=new URL(r,"http://localhost"),s=ht(o.pathname+o.search);if(s)return s}catch{}return"/"}var Wt=new Set(["_action","data-action","_csrf","what-csrf-token","_redirect"]);function $(t={}){let{getCsrfToken:e,skipCsrf:n=!1}=t;return async function(o){if((o.method||"POST").toUpperCase()!=="POST")return j(405,{message:"Method Not Allowed"});let a=Bt(o.headers),i=a["x-what-action"],c=a["content-type"]||"",d=!i&&c.includes("application/x-www-form-urlencoded"),u=n?void 0:e?await e(o):void 0;if(d){let h=o.body||{},g=h._action||h["data-action"]||o.query&&o.query.action;if(!g)return G(400,'Missing action name (add a hidden "_action" field or ?action= query param)');let b={...a},E=h._csrf??h["what-csrf-token"];if(E&&!b["x-csrf-token"]&&(b["x-csrf-token"]=String(E)),!n&&e&&!u)return G(403,"Missing CSRF token");let C={};for(let[v,U]of Object.entries(h))Wt.has(v)||(C[v]=U);let R=await N({headers:b},g,[C],{csrfToken:u,skipCsrf:n});return R.status===200?{status:303,headers:{location:Jt(h,a)},body:""}:G(R.status,R.body&&R.body.message||"Action failed")}if(!i)return j(400,{message:"Missing X-What-Action header"});if(!n&&e&&!u)return j(403,{message:"Missing CSRF token"});let l=(o.body||{}).args,p=await N({headers:a},i,l,{csrfToken:u,skipCsrf:n});return j(p.status,p.body)}}function Vt(t={}){let e=t.basePath||Ut,n=$(t);return async function(o,s,a){let[i,c]=(o.url||"").split("?");if(i!==e||(o.method||"").toUpperCase()!=="POST")return a?a():void 0;let d;try{let l=await zt(o);d=L(l,o.headers["content-type"]||"")}catch(l){s.writeHead(l.code==="BODY_TOO_LARGE"?413:400,{"content-type":"application/json"}),s.end(JSON.stringify({message:l.code==="BODY_TOO_LARGE"?"Payload too large":"Invalid request body"}));return}let u=Object.fromEntries(new URLSearchParams(c||"")),f=await n({method:o.method,headers:o.headers,body:d,query:u});s.writeHead(f.status,f.headers),s.end(f.body)}}function L(t,e){if((e||"").includes("application/x-www-form-urlencoded")){let n={};for(let[r,o]of new URLSearchParams(String(t)))n[r]===void 0?n[r]=o:Array.isArray(n[r])?n[r].push(o):n[r]=[n[r],o];return n}return t==null||t===""?{}:JSON.parse(String(t))}async function M(t,e=yt){let n=Number(t.headers.get("content-length"));if(Number.isFinite(n)&&n>e)return{tooLarge:!0};let r=t.body;if(!r||typeof r.getReader!="function"){let i=await t.text();return Buffer.byteLength(i,"utf8")>e?{tooLarge:!0}:{raw:i}}let o=r.getReader(),s=[],a=0;for(;;){let{done:i,value:c}=await o.read();if(i)break;if(c){if(a+=c.byteLength,a>e){try{await o.cancel()}catch{}return{tooLarge:!0}}s.push(c)}}return{raw:Buffer.concat(s.map(i=>Buffer.from(i))).toString("utf8")}}function zt(t){return new Promise((e,n)=>{let r=0,o=[];t.on("data",s=>{if(r+=s.length,r>yt){let a=new Error("Body too large");a.code="BODY_TOO_LARGE",n(a),t.destroy?.();return}o.push(s)}),t.on("end",()=>{if(o.length===0)return e("");e(Buffer.concat(o).toString("utf8"))}),t.on("error",n)})}function Xt(t={}){let e=$(t);return async function(n){let r={};try{let a=await M(n);if(a.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});r=L(a.raw,n.headers.get("content-type")||"")}catch{r={}}let o={};try{o=Object.fromEntries(new URL(n.url,"http://localhost").searchParams)}catch{}let s=await e({method:n.method,headers:n.headers,body:r,query:o});return new Response(s.body,{status:s.status,headers:s.headers})}}import{matchRoute as Yt,parseQuery as Kt}from"what-router/match";var Zt="/__what_action",Gt="/__what_revalidate",Q="what-csrf";function I(t){let e={};return t&&typeof t.forEach=="function"&&t.forEach((n,r)=>{e[r.toLowerCase()]=n}),e}function mt(t,e){if(!t)return null;let n=String(t).match(new RegExp(`(?:^|;\\s*)${e}=([^;]+)`));return n?decodeURIComponent(n[1]):null}async function Qt(t){try{let e=await M(t);return e.tooLarge?{tooLarge:!0}:L(e.raw,t.headers.get("content-type")||"")}catch{return{}}}async function qt(t){let e;try{e=await M(t)}catch{return{json:{}}}if(e.tooLarge)return{tooLarge:!0};try{return{json:JSON.parse(e.raw)}}catch{return{json:{}}}}function te(t){return async function(n){let{route:r,params:o,query:s,request:a}=n,i={default:r.component,loader:r.loader},c=n.csrfToken?{...t,csrfToken:n.csrfToken}:t;return{html:await gt(i,{params:o,query:s,request:a},c),status:200,tags:n.config&&n.config.tags||[],path:n.path}}}function q(t={}){let{routes:e=[],cache:n,render:r,revalidateWebhook:o,document:s={},notFound:a,basePath:i="",csrf:c=!0}=t,d=c!==!1&&!t.actionHandler,u=t.actionHandler||$(d?{getCsrfToken:l=>mt(l.headers&&l.headers.cookie,Q)}:{skipCsrf:!0}),f=r||te(s);return n&&(n.revalidatePath||n.revalidateTag)&&W({revalidatePath:n.revalidatePath,revalidateTag:n.revalidateTag}),async function(p){let h=new URL(p.url,"http://localhost"),g=h.pathname;if(i&&g.startsWith(i)&&(g=g.slice(i.length)||"/"),p.method==="POST"&&g===Zt){let y=await Qt(p);if(y&&y.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});let A=await u({method:"POST",headers:I(p.headers),body:y,query:Object.fromEntries(h.searchParams)});return new Response(A.body,{status:A.status,headers:A.headers})}let b=null,E=null;if(d&&(b=mt(I(p.headers).cookie,Q),!b)){b=K();let A=I(p.headers)["x-forwarded-proto"]==="https"||h.protocol==="https:"||!0;E=`${Q}=${encodeURIComponent(b)}; Path=/; SameSite=Lax`+(A?"; Secure":"")}let C=y=>(E&&(y["set-cookie"]=E),y);if(p.method==="POST"&&g===Gt&&o){let y=await qt(p);if(y.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});let A=await o({headers:I(p.headers),body:y.json});return new Response(JSON.stringify(A.body),{status:A.status,headers:{"content-type":"application/json"}})}let R=Yt(g,e);if(!R){let y=a?a():"<!DOCTYPE html><html><body><h1>404 \u2014 Not Found</h1></body></html>";return new Response(y,{status:404,headers:C({"content-type":"text/html; charset=utf-8"})})}let{route:v,params:U}=R,B=v.page||{mode:v.mode||"client"},x={path:g,query:Kt(h.search),config:B,route:v,params:U,request:p};if(n&&B.mode!=="server"){x.varyHeaders=I(p.headers);let y=await n.handle(x,()=>f(x));return new Response(y.html,{status:y.status||200,headers:C({"content-type":"text/html; charset=utf-8",...y.headers||{}})})}b&&(x.csrfToken=b);let at=await f(x),st=C({"content-type":"text/html; charset=utf-8"});return B.mode==="server"&&(st["Cache-Control"]="private, no-store"),new Response(at.html,{status:at.status||200,headers:st})}}function ee(t={}){let e=q(t);return{async fetch(n,r,o){return r&&(n.__env=r),o&&(n.__ctx=o),e(n)}}}function k(t){return{head:re(),loaderData:t,resources:new Map,islandStores:new Map,resourceCounter:0,boundaryCounter:0,suspended:[]}}var bt=0;function oe(){bt=0}function ae(){return"h"+bt++}function se(t){if(!St()){let e=k(void 0);return S(e,()=>se(t))}return oe(),O(t)}function O(t){if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return m(String(t));if(typeof t=="function"&&t._signal)return`<!--$-->${O(t())}<!--/$-->`;if(typeof t=="function")try{return`<!--$-->${O(t())}<!--/$-->`}catch{return typeof process<"u","<!--$--><!--/$-->"}if(Array.isArray(t))return`<!--[]-->${t.map(O).join("")}<!--/[]-->`;if(typeof t.tag=="function"){let c=ae(),d=t.tag({...t.props,children:t.children}),u=O(d);return ie(u,c)}let{tag:e,props:n,children:r}=t;nt(e);let o=rt(n||{}),s=`<${e}${o}>`;if(ot.has(e))return s;let a=et(n),i=a!=null?String(a):r.map(O).join("");return`${s}${i}</${e}>`}function ie(t,e){let n=t.match(/^((?:<!--.*?-->)*)<([a-zA-Z][a-zA-Z0-9-]*)/);if(n){let r=n[1],o=n[2],s=r.length+1+o.length;return t.slice(0,s)+` data-hk="${e}"`+t.slice(s)}return t}function w(t){if(!St()){let c=k(void 0);return S(c,()=>w(t))}if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return m(String(t));if(typeof t=="function"&&t._signal)return w(t());if(typeof t=="function")try{return w(t())}catch{return typeof process<"u",""}if(Array.isArray(t))return t.map(w).join("");if(t.tag==="__suspense")try{return(t.children||[]).map(w).join("")}catch(c){if(c&&typeof c.then=="function")return w(t.props&&t.props.fallback);throw c}if(typeof t.tag=="function"){let c=t.tag({...t.props,children:t.children});return w(c)}let{tag:e,props:n,children:r}=t;nt(e);let o=rt(n||{}),s=`<${e}${o}>`;if(ot.has(e))return s;let a=et(n),i=a!=null?String(a):r.map(w).join("");return`${s}${i}</${e}>`}function We(t){let e=k(void 0);return{body:S(e,()=>w(t)),head:tt(e.head)}}async function Ve(t,e={}){let n=t.default||t,r=typeof t.loader=="function"?await t.loader(e):void 0,o=k(r),s=e.params||{};return{body:S(o,()=>w(wt(n,{...s,loaderData:r}))),head:tt(o.head),loaderData:r}}var _t=12;async function ce(t,e){e||(e=k(void 0));let n="";for(let o=0;o<_t;o++){n=S(e,()=>w(t));let s=[...e.resources.values()].filter(a=>a.status==="pending").map(a=>a.promise);if(s.length===0)break;await Promise.all(s)}let r={};for(let[o,s]of e.resources)s.status==="ready"&&(r[o]=s.value);return{body:n,head:tt(e.head),loaderData:e.loaderData,resources:r,ctx:e}}async function gt(t,e={},n={}){let r=t.default||t,o=typeof t.loader=="function"?await t.loader(e):void 0,s=k(o),a=e.params||{},{body:i,head:c,resources:d}=await ce(wt(r,{...a,loaderData:o}),s),u={loaderData:o??null,resources:d,islandStores:it(s)};return de({body:i,head:c,payload:u,options:n})}function de({body:t,head:e,payload:n,options:r={}}){let o=r.lang||"en",s=`<script id="__what_data" type="application/json">${J(n)}<\/script>`,a=r.clientEntry?`<script type="module" src="${m(r.clientEntry)}"><\/script>`:"",c=(r.csrfToken?Z(r.csrfToken):"")+(r.head||""),d=r.bodyClass?` class="${m(r.bodyClass)}"`:"";return`<!DOCTYPE html><html lang="${m(o)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${e||""}${c}</head><body${d}>${t}${s}${a}</body></html>`}async function*H(t,e){if(e===void 0&&(e=k(void 0)),t==null||t===!1||t===!0)return;if(typeof t=="string"||typeof t=="number"){yield m(String(t));return}if(typeof t=="function"&&t._signal){let a=S(e,()=>t());yield*H(a,e);return}if(typeof t=="function"){try{let a=S(e,()=>t());yield*H(a,e)}catch{typeof process<"u"}return}if(Array.isArray(t)){for(let a of t)yield*H(a,e);return}if(t.tag==="__suspense"){let a=null;for(let i=0;i<_t&&a===null;i++){let c=null;try{a=S(e,()=>(t.children||[]).map(w).join(""))}catch(d){if(d&&typeof d.then=="function")c=d;else throw d}if(a===null){let d=[...e.resources.values()].filter(u=>u.status==="pending").map(u=>u.promise);await Promise.all([c,...d].filter(Boolean))}}a===null&&(a=S(e,()=>w(t.props&&t.props.fallback))),yield a;return}if(typeof t.tag=="function"){try{let a=S(e,()=>t.tag({...t.props,children:t.children})),i=a instanceof Promise?await a:a;yield*H(i,e)}catch(a){typeof process<"u",yield F?`<!-- SSR Error: ${m(a.message||"Component error")} -->`:"<!-- SSR Error -->"}return}let{tag:n,props:r,children:o}=t;nt(n);let s=rt(r||{});if(yield`<${n}${s}>`,!ot.has(n)){let a=et(r);if(a!=null)yield String(a);else for(let i of o)yield*H(i,e);yield`</${n}>`}}function ze(t){return{mode:"static",...t}}function Xe(t,e={}){let n=k(e),r=S(n,()=>{let s=t.component(e);return w(s)}),o=t.islands||[];return ue({title:t.title||"",meta:t.meta||{},body:r,islands:o,scripts:t.mode==="static"?[]:t.scripts||[],styles:t.styles||[],mode:t.mode})}function ue({title:t,meta:e,body:n,islands:r,scripts:o,styles:s,mode:a}){let i=Object.entries(e).map(([l,p])=>`<meta name="${m(l)}" content="${m(p)}">`).join(`
2
+ `),c=s.map(l=>`<link rel="stylesheet" href="${m(l)}">`).join(`
3
+ `),d=r.length>0?`
4
4
  <script type="module">
5
5
  import { hydrateIslands } from '/@what/islands.js';
6
6
  hydrateIslands();
7
- <\/script>`:"",d=o.map(l=>`<script type="module" src="${m(l)}"><\/script>`).join(`
8
- `),f=s==="client"?`
7
+ <\/script>`:"",u=o.map(l=>`<script type="module" src="${m(l)}"><\/script>`).join(`
8
+ `),f=a==="client"?`
9
9
  <script type="module" src="/@what/client.js"><\/script>`:"";return`<!DOCTYPE html>
10
10
  <html lang="en">
11
11
  <head>
@@ -17,8 +17,8 @@ import{h as yt,getServerContext as mt,runWithServerContext as S,beginHeadCollect
17
17
  </head>
18
18
  <body>
19
19
  <div id="app">${n}</div>
20
- ${u}
21
20
  ${d}
21
+ ${u}
22
22
  ${f}
23
23
  </body>
24
- </html>`}function je(t){return t._server=!0,t}var q=!(typeof process<"u");function tt(t){return t?t.dangerouslySetInnerHTML?t.dangerouslySetInnerHTML.__html??null:t.innerHTML&&typeof t.innerHTML=="object"&&"__html"in t.innerHTML?t.innerHTML.__html??null:(t.innerHTML!=null&&typeof t.innerHTML=="string"&&q&&console.warn("[what-server] innerHTML received a raw string. This is a security risk (XSS). Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead."),null):null}var oe=/^[a-zA-Z_:][a-zA-Z0-9:._-]*$/;function et(t){let e="";for(let[n,r]of Object.entries(t))if(!(n==="key"||n==="ref"||n==="children"||n==="dangerouslySetInnerHTML"||n==="innerHTML")&&!(n.startsWith("on")&&n.length>2)&&!(r===!1||r==null)){if(!oe.test(n)){q&&console.warn(`[what-server] Skipping invalid attribute name in SSR: ${JSON.stringify(n)}`);continue}if(n==="className"||n==="class")e+=` class="${m(String(r))}"`;else if(n==="style"&&typeof r=="object"){let o=Object.entries(r).map(([a,s])=>`${ie(a)}:${s}`).join(";");e+=` style="${m(o)}"`}else if(r===!0)n.startsWith("aria-")||n==="role"?e+=` ${n}="true"`:e+=` ${n}`;else{if(se(n,r))continue;e+=` ${n}="${m(String(r))}"`}}return e}function se(t,e){let n=t.toLowerCase();if(!ae.has(n))return!1;let r=String(e).trim().replace(/[\u0000-\u001f\u007f\s]+/g,"").toLowerCase();return r.startsWith("javascript:")||r.startsWith("vbscript:")||r.startsWith("data:")}var ae=new Set(["href","src","action","formaction","xlink:href"]);function m(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function ie(t){return t.startsWith("--")?t:t.replace(/([A-Z])/g,"-$1").toLowerCase()}var nt=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);export{Pt as action,O as createActionHandler,Kt as createCloudflareHandler,G as createRequestHandler,X as csrfMetaTag,De as definePage,Ut as fetchActionHandler,ct as formAction,z as generateCsrfToken,Le as generateStaticPage,Ht as getRegisteredActions,_t as getRevalidationHandler,I as handleActionRequest,dt as invalidatePath,Nt as nodeActionMiddleware,$t as onRevalidate,ht as renderDocument,Ie as renderPage,qt as renderToHydratableString,$ as renderToStream,w as renderToString,ee as renderToStringAsync,He as renderToStringWithHead,B as revalidatePath,W as revalidateTag,F as serializeState,je as server,U as setRevalidationHandler,ut as useAction,xt as useFormAction,It as useMutation,Ot as useOptimistic,it as validateCsrfToken};
24
+ </html>`}function Ye(t){return t._server=!0,t}var F=!(typeof process<"u");function et(t){return t?t.dangerouslySetInnerHTML?t.dangerouslySetInnerHTML.__html??null:t.innerHTML&&typeof t.innerHTML=="object"&&"__html"in t.innerHTML?t.innerHTML.__html??null:(t.innerHTML!=null&&typeof t.innerHTML=="string"&&F&&console.warn("[what-server] innerHTML received a raw string. This is a security risk (XSS). Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead."),null):null}var fe=/^[a-zA-Z_:][a-zA-Z0-9:._-]*$/,le=/^[a-zA-Z_][a-zA-Z0-9._:-]*$/;function nt(t){if(typeof t!="string"||!le.test(t))throw new Error(`[what-server] Invalid tag name in SSR: ${JSON.stringify(t)}`)}function rt(t){let e="";for(let[n,r]of Object.entries(t)){if(n==="key"||n==="ref"||n==="children"||n==="dangerouslySetInnerHTML"||n==="innerHTML")continue;let o=n.toLowerCase();if(!(o.startsWith("on")&&n.length>2)){if(r===!1&&ne(n)){e+=` ${n}="false"`;continue}if(!(r===!1||r==null)){if(!fe.test(n)){F&&console.warn(`[what-server] Skipping invalid attribute name in SSR: ${JSON.stringify(n)}`);continue}if(ye.has(o)){F&&console.warn(`[what-server] Skipping unsafe attribute in SSR: ${JSON.stringify(n)}`);continue}if(n==="className"||n==="class")e+=` class="${m(String(r))}"`;else if(n==="style"&&typeof r=="object"){let s=Object.entries(r).map(([a,i])=>`${me(a)}:${i}`).join(";");e+=` style="${m(s)}"`}else if(r===!0)n.startsWith("aria-")||n==="role"?e+=` ${n}="true"`:e+=` ${n}`;else{if(pe(n,r))continue;e+=` ${n}="${m(String(r))}"`}}}}return e}function pe(t,e){let n=t.toLowerCase();if(!he.has(n))return!1;let r=String(e).trim().replace(/[\u0000-\u001f\u007f\s]+/g,"").toLowerCase();return r.startsWith("javascript:")||r.startsWith("vbscript:")||r.startsWith("data:")}var he=new Set(["href","src","action","formaction","data","ping","xlink:href"]),ye=new Set(["srcdoc"]);function m(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function me(t){return t.startsWith("--")?t:t.replace(/([A-Z])/g,"-$1").toLowerCase()}var ot=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);export{pt as ACTION_ENDPOINT,Ft as Form,$t as action,$ as createActionHandler,ee as createCloudflareHandler,q as createRequestHandler,Z as csrfMetaTag,ze as definePage,Xt as fetchActionHandler,ut as formAction,K as generateCsrfToken,Xe as generateStaticPage,Dt as getRegisteredActions,Ct as getRevalidationHandler,N as handleActionRequest,lt as invalidatePath,Vt as nodeActionMiddleware,Nt as onRevalidate,gt as renderDocument,Ve as renderPage,se as renderToHydratableString,H as renderToStream,w as renderToString,ce as renderToStringAsync,We as renderToStringWithHead,V as revalidatePath,z as revalidateTag,J as serializeState,Ye as server,W as setRevalidationHandler,ft as useAction,It as useFormAction,jt as useMutation,Ht as useOptimistic,dt as validateCsrfToken};
@@ -1 +1 @@
1
- import{mount as R,signal as q,batch as E,getServerContext as _}from"what-core";var A=new RegExp("[<>&\\u2028\\u2029]","g"),P={60:"\\u003c",62:"\\u003e",38:"\\u0026",8232:"\\u2028",8233:"\\u2029"};function x(e){return JSON.stringify(e).replace(A,r=>P[r.charCodeAt(0)])}var S=new Map,b=new Set,f=[],w=!1,v=new Map,p=new Map;function g(e){if(typeof structuredClone=="function")try{return structuredClone(e)}catch{}return Array.isArray(e)?e.map(g):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).map(([r,t])=>[r,g(t)])):e}function h(e,r,t){if(e.has(r))return e.get(r);let n={},s={};for(let[o,a]of Object.entries(g(t)))s[o]=q(a),Object.defineProperty(n,o,{get:()=>s[o](),set:i=>s[o].set(i),enumerable:!0});return n._signals=s,n._subscribe=(o,a)=>{if(s[o])return s[o].subscribe(a)},n._batch=o=>E(o),n._getSnapshot=()=>{let o={};for(let[a,i]of Object.entries(s))o[a]=i.peek();return o},n._hydrate=o=>{E(()=>{for(let[a,i]of Object.entries(o))s[a]&&s[a].set(i)})},e.set(r,n),n}function y(e){let r=e||_();return r&&r.islandStores instanceof Map?r.islandStores:null}function I(e,r){if(p.has(e))return p.get(e).handle;let t={initialState:g(r),handle:null},n=()=>{let s=y();if(!s)throw new Error(`[what-server] Island store "${e}" was accessed outside an active server render. Read or write module-scoped island stores from a component rendered by renderDocument/renderPage.`);return h(s,e,t.initialState)};return t.handle=new Proxy({},{get(s,o){return Reflect.get(n(),o)},set(s,o,a){return Reflect.set(n(),o,a)},has(s,o){return Reflect.has(n(),o)},ownKeys(){return Reflect.ownKeys(n())},getOwnPropertyDescriptor(s,o){let a=Reflect.getOwnPropertyDescriptor(n(),o);return a?{...a,configurable:!0}:void 0}}),p.set(e,t),t.handle}function X(e,r){if(typeof document<"u")return h(v,e,r);let t=y();if(t){let n=p.get(e);return h(t,e,n?.initialState??r)}return I(e,r)}function O(e,r={}){if(typeof document<"u")return h(v,e,r);let t=y();if(t){let n=p.get(e);return h(t,e,n?.initialState??r)}return I(e,r)}function z(){return x(j())}function j(e){let r=typeof document<"u"?v:y(e),t={};for(let[n,s]of r||[])t[n]=s._getSnapshot();return t}function D(e){try{let r=typeof e=="string"?JSON.parse(e):e;for(let[t,n]of Object.entries(r))O(t,n)._hydrate(n)}catch(r){console.warn("[what] Failed to hydrate island stores:",r)}}function N(e,r,t={}){S.set(e,{loader:r,mode:t.mode||"idle",media:t.media||null,priority:t.priority||0,stores:t.stores||[]})}function K({name:e,props:r={},children:t,mode:n,priority:s,stores:o}){let a=S.get(e),i=n||a?.mode||"idle",c=s??a?.priority??0,d=o||a?.stores||[];return{tag:"div",props:{"data-island":e,"data-island-mode":i,"data-island-props":JSON.stringify(r),"data-island-priority":c,"data-island-stores":JSON.stringify(d)},children:t||[],key:null,_vnode:!0}}function l(e){let r=!1;for(let t=0;t<f.length;t++)if(e.priority>f[t].priority){f.splice(t,0,e),r=!0;break}r||f.push(e),M()}function M(){if(w||f.length===0)return;w=!0;let e=f.shift();Promise.resolve(e.hydrate()).catch(r=>console.error("[what] Island hydration failed:",e.name,r)).finally(()=>{w=!1,queueMicrotask(M)})}function U(e,r=100){for(let t of f)if(t.name===e){t.priority=r,f.sort((n,s)=>s.priority-n.priority);break}}function C(){let e=document.querySelector("script[data-island-stores]");e&&D(e.textContent);let r=document.querySelectorAll("[data-island]");for(let t of r){let n=t.dataset.island,s=t.dataset.islandMode||"idle",o=JSON.parse(t.dataset.islandProps||"{}"),a=parseInt(t.dataset.islandPriority||"0",10),i=JSON.parse(t.dataset.islandStores||"[]"),c=S.get(n);if(!c){console.warn(`[what] Island "${n}" not registered`);continue}b.has(t)||F(t,c,o,s,a,n,i)}}function F(e,r,t,n,s,o,a){let i=async()=>{if(b.has(e))return;b.add(e);let c=await r.loader(),d=c.default||c,u={};for(let k of a)u[k]=O(k);let m=d({...t,...u});e.childNodes.length>0?i(m,e):R(m,e),e.removeAttribute("data-island"),e.removeAttribute("data-island-mode"),e.removeAttribute("data-island-props"),e.removeAttribute("data-island-priority"),e.removeAttribute("data-island-stores"),e.dispatchEvent(new CustomEvent("island:hydrated",{bubbles:!0,detail:{name:o,mode:n}}))};switch(n){case"load":l({name:o,priority:s+1e3,hydrate:i});break;case"idle":"requestIdleCallback"in window?requestIdleCallback(()=>{l({name:o,priority:s,hydrate:i})}):setTimeout(()=>{l({name:o,priority:s,hydrate:i})},200);break;case"visible":{new IntersectionObserver((d,u)=>{for(let m of d)if(m.isIntersecting){u.disconnect(),l({name:o,priority:s,hydrate:i});break}},{rootMargin:"200px"}).observe(e);break}case"media":{let c=window.matchMedia(r.media||"(max-width: 768px)");c.matches?l({name:o,priority:s,hydrate:i}):c.addEventListener("change",d=>{d.matches&&l({name:o,priority:s,hydrate:i})},{once:!0});break}case"action":{let c=["click","focus","mouseover","touchstart"],d=()=>{c.forEach(u=>e.removeEventListener(u,d)),l({name:o,priority:s+500,hydrate:i})};c.forEach(u=>e.addEventListener(u,d,{once:!0,passive:!0}));break}case"static":break;default:l({name:o,priority:s,hydrate:i})}}function $(e){for(let[r,t]of Object.entries(e))N(r,t.loader||t,{mode:t.mode||"idle",media:t.media,priority:t.priority||0,stores:t.stores||[]});typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",C):C())}function L(e,r){if(typeof document>"u")return;let t=document.querySelectorAll(e);for(let n of t)n.dataset.enhanced||(n.dataset.enhanced="true",r(n))}function G(e="form[data-enhance]"){L(e,r=>{r.addEventListener("submit",async t=>{t.preventDefault();let n=new FormData(r),s=r.method.toUpperCase()||"POST",o=r.action||location.href;try{let a=document.querySelector('meta[name="csrf-token"]')||document.querySelector('meta[name="what-csrf-token"]'),i=a?a.getAttribute("content"):null,c=r.getAttribute("data-no-csrf")==="true";if(!i&&!c){console.warn('[what] Form submission blocked: no CSRF token found. Add a <meta name="csrf-token"> tag or set data-no-csrf="true" on the form to opt out.'),r.dispatchEvent(new CustomEvent("form:error",{bubbles:!0,detail:{error:new Error("Missing CSRF token")}}));return}let d={"X-Requested-With":"XMLHttpRequest"};i&&(d["X-CSRF-Token"]=i);let u=await fetch(o,{method:s,body:s==="GET"?void 0:n,headers:d});r.dispatchEvent(new CustomEvent("form:response",{bubbles:!0,detail:{response:u,ok:u.ok}}))}catch(a){r.dispatchEvent(new CustomEvent("form:error",{bubbles:!0,detail:{error:a}}))}})})}function W(){let e=typeof document<"u"?[...v.keys()]:[...y()?.keys()||p.keys()];return{registered:[...S.keys()],hydrated:b.size,pending:f.length,queue:f.map(t=>({name:t.name,priority:t.priority})),stores:e}}export{K as Island,$ as autoIslands,U as boostIslandPriority,X as createIslandStore,L as enhance,G as enhanceForms,W as getIslandStatus,j as getIslandStoresSnapshot,D as hydrateIslandStores,C as hydrateIslands,N as island,z as serializeIslandStores,O as useIslandStore};
1
+ import{mount as P,hydrate as R,signal as q,batch as E,getServerContext as N}from"what-core";var _=new RegExp("[<>&\\u2028\\u2029]","g"),A={60:"\\u003c",62:"\\u003e",38:"\\u0026",8232:"\\u2028",8233:"\\u2029"};function x(e){return JSON.stringify(e).replace(_,r=>A[r.charCodeAt(0)])}var S=new Map,b=new Set,f=[],v=!1,w=new Map,p=new Map;function g(e){if(typeof structuredClone=="function")try{return structuredClone(e)}catch{}return Array.isArray(e)?e.map(g):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).map(([r,t])=>[r,g(t)])):e}function h(e,r,t){if(e.has(r))return e.get(r);let n={},s={};for(let[o,a]of Object.entries(g(t)))s[o]=q(a),Object.defineProperty(n,o,{get:()=>s[o](),set:i=>s[o].set(i),enumerable:!0});return n._signals=s,n._subscribe=(o,a)=>{if(s[o])return s[o].subscribe(a)},n._batch=o=>E(o),n._getSnapshot=()=>{let o={};for(let[a,i]of Object.entries(s))o[a]=i.peek();return o},n._hydrate=o=>{E(()=>{for(let[a,i]of Object.entries(o))s[a]&&s[a].set(i)})},e.set(r,n),n}function y(e){let r=e||N();return r&&r.islandStores instanceof Map?r.islandStores:null}function I(e,r){if(p.has(e))return p.get(e).handle;let t={initialState:g(r),handle:null},n=()=>{let s=y();if(!s)throw new Error(`[what-server] Island store "${e}" was accessed outside an active server render. Read or write module-scoped island stores from a component rendered by renderDocument/renderPage.`);return h(s,e,t.initialState)};return t.handle=new Proxy({},{get(s,o){return Reflect.get(n(),o)},set(s,o,a){return Reflect.set(n(),o,a)},has(s,o){return Reflect.has(n(),o)},ownKeys(){return Reflect.ownKeys(n())},getOwnPropertyDescriptor(s,o){let a=Reflect.getOwnPropertyDescriptor(n(),o);return a?{...a,configurable:!0}:void 0}}),p.set(e,t),t.handle}function X(e,r){if(typeof document<"u")return h(w,e,r);let t=y();if(t){let n=p.get(e);return h(t,e,n?.initialState??r)}return I(e,r)}function O(e,r={}){if(typeof document<"u")return h(w,e,r);let t=y();if(t){let n=p.get(e);return h(t,e,n?.initialState??r)}return I(e,r)}function z(){return x(j())}function j(e){let r=typeof document<"u"?w:y(e),t={};for(let[n,s]of r||[])t[n]=s._getSnapshot();return t}function D(e){try{let r=typeof e=="string"?JSON.parse(e):e;for(let[t,n]of Object.entries(r))O(t,n)._hydrate(n)}catch(r){console.warn("[what] Failed to hydrate island stores:",r)}}function F(e,r,t={}){S.set(e,{loader:r,mode:t.mode||"idle",media:t.media||null,priority:t.priority||0,stores:t.stores||[]})}function K({name:e,props:r={},children:t,mode:n,priority:s,stores:o}){let a=S.get(e),i=n||a?.mode||"idle",d=s??a?.priority??0,c=o||a?.stores||[];return{tag:"div",props:{"data-island":e,"data-island-mode":i,"data-island-props":JSON.stringify(r),"data-island-priority":d,"data-island-stores":JSON.stringify(c)},children:t||[],key:null,_vnode:!0}}function l(e){let r=!1;for(let t=0;t<f.length;t++)if(e.priority>f[t].priority){f.splice(t,0,e),r=!0;break}r||f.push(e),M()}function M(){if(v||f.length===0)return;v=!0;let e=f.shift();Promise.resolve(e.hydrate()).catch(r=>console.error("[what] Island hydration failed:",e.name,r)).finally(()=>{v=!1,queueMicrotask(M)})}function U(e,r=100){for(let t of f)if(t.name===e){t.priority=r,f.sort((n,s)=>s.priority-n.priority);break}}function C(){let e=document.getElementById("__what_data");if(e)try{let t=JSON.parse(e.textContent||"{}");t&&t.islandStores&&D(t.islandStores)}catch(t){console.warn("[what] Failed to parse hydration payload:",t)}let r=document.querySelectorAll("[data-island]");for(let t of r){if(t.dataset.islandSelf)continue;let n=t.dataset.island,s=t.dataset.islandMode||"idle",o=JSON.parse(t.dataset.islandProps||"{}"),a=parseInt(t.dataset.islandPriority||"0",10),i=JSON.parse(t.dataset.islandStores||"[]"),d=S.get(n);if(!d){console.warn(`[what] Island "${n}" not registered`);continue}b.has(t)||J(t,d,o,s,a,n,i)}}function J(e,r,t,n,s,o,a){let i=async()=>{if(b.has(e))return;b.add(e);let d=await r.loader(),c=d.default||d,u={};for(let k of a)u[k]=O(k);let m=c({...t,...u});e.childNodes.length>0?R(m,e):P(m,e),e.removeAttribute("data-island"),e.removeAttribute("data-island-mode"),e.removeAttribute("data-island-props"),e.removeAttribute("data-island-priority"),e.removeAttribute("data-island-stores"),e.dispatchEvent(new CustomEvent("island:hydrated",{bubbles:!0,detail:{name:o,mode:n}}))};switch(n){case"load":l({name:o,priority:s+1e3,hydrate:i});break;case"idle":"requestIdleCallback"in window?requestIdleCallback(()=>{l({name:o,priority:s,hydrate:i})}):setTimeout(()=>{l({name:o,priority:s,hydrate:i})},200);break;case"visible":{new IntersectionObserver((c,u)=>{for(let m of c)if(m.isIntersecting){u.disconnect(),l({name:o,priority:s,hydrate:i});break}},{rootMargin:"200px"}).observe(e);break}case"media":{let d=window.matchMedia(r.media||"(max-width: 768px)");d.matches?l({name:o,priority:s,hydrate:i}):d.addEventListener("change",c=>{c.matches&&l({name:o,priority:s,hydrate:i})},{once:!0});break}case"action":{let d=["click","focus","mouseover","touchstart"],c=()=>{d.forEach(u=>e.removeEventListener(u,c)),l({name:o,priority:s+500,hydrate:i})};d.forEach(u=>e.addEventListener(u,c,{once:!0,passive:!0}));break}case"static":break;default:l({name:o,priority:s,hydrate:i})}}function $(e){for(let[r,t]of Object.entries(e))F(r,t.loader||t,{mode:t.mode||"idle",media:t.media,priority:t.priority||0,stores:t.stores||[]});typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",C):C())}function L(e,r){if(typeof document>"u")return;let t=document.querySelectorAll(e);for(let n of t)n.dataset.enhanced||(n.dataset.enhanced="true",r(n))}function B(e="form[data-enhance]"){L(e,r=>{r.addEventListener("submit",async t=>{t.preventDefault();let n=new FormData(r),s=r.method.toUpperCase()||"POST",o=r.action||location.href;try{let a=document.querySelector('meta[name="csrf-token"]')||document.querySelector('meta[name="what-csrf-token"]'),i=a?a.getAttribute("content"):null,d=r.getAttribute("data-no-csrf")==="true";if(!i&&!d){console.warn('[what] Form submission blocked: no CSRF token found. Add a <meta name="what-csrf-token"> tag (csrfMetaTag() emits it) or set data-no-csrf="true" on the form to opt out.'),r.dispatchEvent(new CustomEvent("form:error",{bubbles:!0,detail:{error:new Error("Missing CSRF token")}}));return}let c={"X-Requested-With":"XMLHttpRequest"};i&&(c["X-CSRF-Token"]=i);let u=await fetch(o,{method:s,body:s==="GET"?void 0:n,headers:c});r.dispatchEvent(new CustomEvent("form:response",{bubbles:!0,detail:{response:u,ok:u.ok}}))}catch(a){r.dispatchEvent(new CustomEvent("form:error",{bubbles:!0,detail:{error:a}}))}})})}function G(){let e=typeof document<"u"?[...w.keys()]:[...y()?.keys()||p.keys()];return{registered:[...S.keys()],hydrated:b.size,pending:f.length,queue:f.map(t=>({name:t.name,priority:t.priority})),stores:e}}export{K as Island,$ as autoIslands,U as boostIslandPriority,X as createIslandStore,L as enhance,B as enhanceForms,G as getIslandStatus,j as getIslandStoresSnapshot,D as hydrateIslandStores,C as hydrateIslands,F as island,z as serializeIslandStores,O as useIslandStore};
package/dist/node.min.js CHANGED
@@ -1,11 +1,11 @@
1
- import{AsyncLocalStorage as _e}from"node:async_hooks";import{__installServerContextStorage as ke}from"what-core";import{h as yt,getServerContext as gt,runWithServerContext as b,beginHeadCollection as ee,endHeadCollection as q}from"what-core";var kt=new RegExp("[<>&\\u2028\\u2029]","g"),Rt={60:"\\u003c",62:"\\u003e",38:"\\u0026",8232:"\\u2028",8233:"\\u2029"};function P(t){return JSON.stringify(t).replace(kt,e=>Rt[e.charCodeAt(0)])}import{mount as Ce,signal as Ee,batch as Oe,getServerContext as vt}from"what-core";var Ct=new Map;function xt(t){let e=t||vt();return e&&e.islandStores instanceof Map?e.islandStores:null}function it(t){let e=typeof document<"u"?Ct:xt(t),n={};for(let[r,o]of e||[])n[r]=o._getSnapshot();return n}import{signal as A,batch as z}from"what-core";var _=null,ct=!(typeof process<"u");function V(t){_=t}function Et(){return _}async function W(t,e){if(_&&_.revalidatePath)return _.revalidatePath(t,e);ct&&console.warn(`[what] revalidatePath('${t}') had no effect: no cache engine is bound. Create a what-isr engine and bind it in your adapter (setRevalidationHandler).`)}async function J(t,e){if(_&&_.revalidateTag)return _.revalidateTag(t,e);ct&&console.warn(`[what] revalidateTag('${t}') had no effect: no cache engine is bound.`)}var X=new Map;function Ot(){if(typeof document<"u"){let t=document.querySelector('meta[name="what-csrf-token"]');if(t)return t.getAttribute("content");let e=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);if(e)return decodeURIComponent(e[1])}return null}function Y(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();if(typeof crypto<"u"&&crypto.getRandomValues){let t=new Uint8Array(16);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,"0")).join("")}throw new Error("[what] No secure random source available for CSRF token generation")}function ut(t,e){if(!t||!e||t.length!==e.length)return!1;let n=0;for(let r=0;r<t.length;r++)n|=t.charCodeAt(r)^e.charCodeAt(r);return n===0}function G(t){return`<meta name="what-csrf-token" content="${String(t).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}">`}var Pt=0,$t=!1;function Ht(){return!$t&&typeof process<"u",`a_${typeof crypto<"u"&&crypto.getRandomValues?Array.from(crypto.getRandomValues(new Uint8Array(6)),e=>e.toString(16).padStart(2,"0")).join(""):`c${(++Pt).toString(36)}_${Date.now().toString(36)}`}`}function jt(t,e={}){let n=e.id||Ht(),{onError:r,onSuccess:o,revalidate:s}=e;typeof window>"u"&&X.set(n,{fn:t,options:e});async function a(...i){if(typeof window>"u")return t(...i);let c=e.timeout||3e4,u=new AbortController,d=setTimeout(()=>u.abort(),c);try{let l=Ot(),f={"Content-Type":"application/json","X-What-Action":n};l&&(f["X-CSRF-Token"]=l);let p=await fetch("/__what_action",{method:"POST",headers:f,credentials:"same-origin",signal:u.signal,body:JSON.stringify({args:i})});if(!p.ok){let m=await p.json().catch(()=>({message:"Action failed"}));throw new Error(m.message||"Action failed")}let h=await p.json();if(o&&o(h),s)for(let m of s)ft(m);return h}catch(l){if(l.name==="AbortError"){let f=new Error(`Action "${n}" timed out after ${c}ms`);throw f.code="TIMEOUT",r&&r(f),f}throw r&&r(l),l}finally{clearTimeout(d)}}return a._actionId=n,a._isAction=!0,a}function dt(t,e={}){let{onSuccess:n,onError:r,resetOnSuccess:o=!0}=e;return async s=>{let a,i;s instanceof Event?(s.preventDefault(),i=s.target,a=new FormData(i)):a=s;let c={},u=!1;for(let[d,l]of a.entries())typeof File<"u"&&l instanceof File&&(u=!0),c[d]?Array.isArray(c[d])?c[d].push(l):c[d]=[c[d],l]:c[d]=l;try{let d=u?await t(c,a):await t(c);return n&&n(d,i),o&&i&&i.reset(),d}catch(d){throw r&&r(d,i),d}}}function lt(t){let e=A(!1),n=A(null),r=A(null);async function o(...s){e.set(!0),n.set(null);try{let a=await t(...s);return r.set(a),a}catch(a){throw n.set(a),a}finally{e.set(!1)}}return{trigger:o,isPending:()=>e(),error:()=>n(),data:()=>r(),reset:()=>{n.set(null),r.set(null)}}}function It(t,e={}){let{resetOnSuccess:n=!0}=e,r={current:null},o=lt(dt(t,{resetOnSuccess:n}));function s(a){a.preventDefault();let i=new FormData(a.target);return r.current=a.target,o.trigger(i)}return{...o,handleSubmit:s,formRef:r}}function Dt(t,e){let n=A(t),r=A([]),o=A(t);function s(u){let d=e(n.peek(),u);z(()=>{r.set([...r.peek(),u]),n.set(d)})}function a(u,d){z(()=>{if(r.set(r.peek().filter(l=>l!==u)),d!==void 0){o.set(d);let l=d;for(let f of r.peek())l=e(l,f);n.set(l)}})}function i(u,d){z(()=>{let l=r.peek().filter(h=>h!==u);r.set(l);let f=d!==void 0?d:o.peek();o.set(f);let p=f;for(let h of l)p=e(p,h);n.set(p)})}async function c(u,d){s(u);try{let l=await d();return a(u,l),l}catch(l){throw i(u),l}}return{value:()=>n(),isPending:()=>r().length>0,addOptimistic:s,resolve:a,rollback:i,withOptimistic:c,set:u=>{n.set(u),o.set(u)}}}var $=new Map;function Lt(t,e){return $.has(t)||$.set(t,new Set),$.get(t).add(e),()=>{$.get(t)?.delete(e)}}function ft(t){let e=$.get(t);if(e)for(let n of e)try{n()}catch(r){console.error("[what] Revalidation error:",r)}}function D(t,e,n,r={}){let{csrfToken:o,skipCsrf:s=!1}=r;if(!s){if(!o)return Promise.resolve({status:500,body:{message:"[what] CSRF token not configured. Pass { csrfToken: sessionToken } to handleActionRequest, or { skipCsrf: true } to explicitly opt out."}});let i=t?.headers?.["x-csrf-token"]||t?.headers?.["X-CSRF-Token"];if(!ut(i,o))return Promise.resolve({status:403,body:{message:"Invalid CSRF token"}})}let a=X.get(e);return a?Array.isArray(n)?a.fn(...n).then(async i=>{let c=a.options||{};if(Array.isArray(c.revalidate))for(let u of c.revalidate)await W(u);if(Array.isArray(c.revalidateTags))for(let u of c.revalidateTags)await J(u);return{status:200,body:i}}).catch(i=>(console.error(`[what] Action "${e}" error:`,i),{status:500,body:{message:"Action failed"}})):Promise.resolve({status:400,body:{message:"Invalid action arguments"}}):Promise.resolve({status:404,body:{message:"Action not found"}})}function Mt(){return[...X.keys()]}function Nt(t,e={}){let{onSuccess:n,onError:r,onSettled:o}=e,s={isPending:A(!1),error:A(null),data:A(null)};async function a(...i){s.isPending.set(!0),s.error.set(null);try{let c=await t(...i);return s.data.set(c),n&&n(c,...i),c}catch(c){throw s.error.set(c),r&&r(c,...i),c}finally{s.isPending.set(!1),o&&o(s.data.peek(),s.error.peek(),...i)}}return{mutate:a,isPending:()=>s.isPending(),error:()=>s.error(),data:()=>s.data(),reset:()=>{s.error.set(null),s.data.set(null)}}}var Ft="/__what_action",ht=1024*1024;function Ut(t){if(!t)return{};if(typeof t.forEach=="function"&&typeof t.get=="function"){let n={};return t.forEach((r,o)=>{n[o.toLowerCase()]=r}),n}let e={};for(let n in t)e[n.toLowerCase()]=t[n];return e}function L(t,e){return{status:t,headers:{"content-type":"application/json"},body:JSON.stringify(e)}}function K(t,e){return{status:t,headers:{"content-type":"text/html; charset=utf-8"},body:`<!DOCTYPE html><html><body><h1>${t}</h1><p>${String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}</p></body></html>`}}function pt(t){if(typeof t!="string"||!t.startsWith("/")||/^[/\\]{2}/.test(t)||t.includes("\\"))return null;try{let e=new URL(t,"http://localhost");return e.origin!=="http://localhost"?null:e.pathname+e.search}catch{return null}}function Bt(t,e){let n=pt(t&&t._redirect);if(n)return n;let r=e.referer||e.referrer;if(r)try{let o=new URL(r,"http://localhost"),s=pt(o.pathname+o.search);if(s)return s}catch{}return"/"}var Vt=new Set(["_action","data-action","_csrf","what-csrf-token","_redirect"]);function H(t={}){let{getCsrfToken:e,skipCsrf:n=!1}=t;return async function(o){if((o.method||"POST").toUpperCase()!=="POST")return L(405,{message:"Method Not Allowed"});let a=Ut(o.headers),i=a["x-what-action"],c=a["content-type"]||"",u=!i&&c.includes("application/x-www-form-urlencoded"),d=n?void 0:e?await e(o):void 0;if(u){let h=o.body||{},m=h._action||h["data-action"]||o.query&&o.query.action;if(!m)return K(400,'Missing action name (add a hidden "_action" field or ?action= query param)');let S={...a},C=h._csrf??h["what-csrf-token"];if(C&&!S["x-csrf-token"]&&(S["x-csrf-token"]=String(C)),!n&&e&&!d)return K(403,"Missing CSRF token");let x={};for(let[E,U]of Object.entries(h))Vt.has(E)||(x[E]=U);let v=await D({headers:S},m,[x],{csrfToken:d,skipCsrf:n});return v.status===200?{status:303,headers:{location:Bt(h,a)},body:""}:K(v.status,v.body&&v.body.message||"Action failed")}if(!i)return L(400,{message:"Missing X-What-Action header"});if(!n&&e&&!d)return L(403,{message:"Missing CSRF token"});let f=(o.body||{}).args,p=await D({headers:a},i,f,{csrfToken:d,skipCsrf:n});return L(p.status,p.body)}}function Wt(t={}){let e=t.basePath||Ft,n=H(t);return async function(o,s,a){let[i,c]=(o.url||"").split("?");if(i!==e||(o.method||"").toUpperCase()!=="POST")return a?a():void 0;let u;try{let f=await Jt(o);u=M(f,o.headers["content-type"]||"")}catch(f){s.writeHead(f.code==="BODY_TOO_LARGE"?413:400,{"content-type":"application/json"}),s.end(JSON.stringify({message:f.code==="BODY_TOO_LARGE"?"Payload too large":"Invalid request body"}));return}let d=Object.fromEntries(new URLSearchParams(c||"")),l=await n({method:o.method,headers:o.headers,body:u,query:d});s.writeHead(l.status,l.headers),s.end(l.body)}}function M(t,e){if((e||"").includes("application/x-www-form-urlencoded")){let n={};for(let[r,o]of new URLSearchParams(String(t)))n[r]===void 0?n[r]=o:Array.isArray(n[r])?n[r].push(o):n[r]=[n[r],o];return n}return t==null||t===""?{}:JSON.parse(String(t))}async function Z(t,e=ht){let n=Number(t.headers.get("content-length"));if(Number.isFinite(n)&&n>e)return{tooLarge:!0};let r=t.body;if(!r||typeof r.getReader!="function"){let i=await t.text();return Buffer.byteLength(i,"utf8")>e?{tooLarge:!0}:{raw:i}}let o=r.getReader(),s=[],a=0;for(;;){let{done:i,value:c}=await o.read();if(i)break;if(c){if(a+=c.byteLength,a>e){try{await o.cancel()}catch{}return{tooLarge:!0}}s.push(c)}}return{raw:Buffer.concat(s.map(i=>Buffer.from(i))).toString("utf8")}}function Jt(t){return new Promise((e,n)=>{let r=0,o=[];t.on("data",s=>{if(r+=s.length,r>ht){let a=new Error("Body too large");a.code="BODY_TOO_LARGE",n(a),t.destroy?.();return}o.push(s)}),t.on("end",()=>{if(o.length===0)return e("");e(Buffer.concat(o).toString("utf8"))}),t.on("error",n)})}function zt(t={}){let e=H(t);return async function(n){let r={};try{let a=await Z(n);if(a.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});r=M(a.raw,n.headers.get("content-type")||"")}catch{r={}}let o={};try{o=Object.fromEntries(new URL(n.url,"http://localhost").searchParams)}catch{}let s=await e({method:n.method,headers:n.headers,body:r,query:o});return new Response(s.body,{status:s.status,headers:s.headers})}}import{matchRoute as Xt,parseQuery as Yt}from"what-router/match";var Gt="/__what_action",Kt="/__what_revalidate",Q="what-csrf";function N(t){let e={};return t&&typeof t.forEach=="function"&&t.forEach((n,r)=>{e[r.toLowerCase()]=n}),e}function mt(t,e){if(!t)return null;let n=String(t).match(new RegExp(`(?:^|;\\s*)${e}=([^;]+)`));return n?decodeURIComponent(n[1]):null}async function Zt(t){try{let e=await Z(t);return e.tooLarge?{tooLarge:!0}:M(e.raw,t.headers.get("content-type")||"")}catch{return{}}}async function Qt(t){try{return await t.json()}catch{return{}}}function qt(t){return async function(n){let{route:r,params:o,query:s,request:a}=n,i={default:r.component,loader:r.loader},c=n.csrfToken?{...t,csrfToken:n.csrfToken}:t;return{html:await F(i,{params:o,query:s,request:a},c),status:200,tags:n.config&&n.config.tags||[],path:n.path}}}function k(t={}){let{routes:e=[],cache:n,render:r,revalidateWebhook:o,document:s={},notFound:a,basePath:i="",csrf:c=!0}=t,u=c!==!1&&!t.actionHandler,d=t.actionHandler||H(u?{getCsrfToken:f=>mt(f.headers&&f.headers.cookie,Q)}:{skipCsrf:!0}),l=r||qt(s);return n&&(n.revalidatePath||n.revalidateTag)&&V({revalidatePath:n.revalidatePath,revalidateTag:n.revalidateTag}),async function(p){let h=new URL(p.url,"http://localhost"),m=h.pathname;if(i&&m.startsWith(i)&&(m=m.slice(i.length)||"/"),p.method==="POST"&&m===Gt){let y=await Zt(p);if(y&&y.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});let T=await d({method:"POST",headers:N(p.headers),body:y,query:Object.fromEntries(h.searchParams)});return new Response(T.body,{status:T.status,headers:T.headers})}let S=null,C=null;if(u&&(S=mt(N(p.headers).cookie,Q),!S)){S=Y();let T=N(p.headers)["x-forwarded-proto"]==="https"||h.protocol==="https:"||!0;C=`${Q}=${encodeURIComponent(S)}; Path=/; SameSite=Lax`+(T?"; Secure":"")}let x=y=>(C&&(y["set-cookie"]=C),y);if(p.method==="POST"&&m===Kt&&o){let y=await Qt(p),T=await o({headers:N(p.headers),body:y});return new Response(JSON.stringify(T.body),{status:T.status,headers:{"content-type":"application/json"}})}let v=Xt(m,e);if(!v){let y=a?a():"<!DOCTYPE html><html><body><h1>404 \u2014 Not Found</h1></body></html>";return new Response(y,{status:404,headers:x({"content-type":"text/html; charset=utf-8"})})}let{route:E,params:U}=v,B=E.page||{mode:E.mode||"client"},I={path:m,query:Yt(h.search),config:B,route:E,params:U,request:p};if(n&&B.mode!=="server"){let y=await n.handle(I,()=>l(I));return new Response(y.html,{status:y.status||200,headers:x({"content-type":"text/html; charset=utf-8",...y.headers||{}})})}S&&(I.csrfToken=S);let st=await l(I),at=x({"content-type":"text/html; charset=utf-8"});return B.mode==="server"&&(at["Cache-Control"]="private, no-store"),new Response(st.html,{status:st.status||200,headers:at})}}function te(t={}){let e=k(t);return{async fetch(n,r,o){return r&&(n.__env=r),o&&(n.__ctx=o),e(n)}}}function R(t){return{head:ee(),loaderData:t,resources:new Map,islandStores:new Map,resourceCounter:0,boundaryCounter:0,suspended:[]}}var wt=0;function ne(){wt=0}function re(){return"h"+wt++}function oe(t){if(typeof document>"u"&&!gt()){let e=R(void 0);return b(e,()=>oe(t))}return ne(),O(t)}function O(t){if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return g(String(t));if(typeof t=="function"&&t._signal)return`<!--$-->${O(t())}<!--/$-->`;if(typeof t=="function")try{return`<!--$-->${O(t())}<!--/$-->`}catch{return typeof process<"u","<!--$--><!--/$-->"}if(Array.isArray(t))return`<!--[]-->${t.map(O).join("")}<!--/[]-->`;if(typeof t.tag=="function"){let c=re(),u=t.tag({...t.props,children:t.children}),d=O(u);return se(d,c)}let{tag:e,props:n,children:r}=t,o=nt(n||{}),s=`<${e}${o}>`;if(rt.has(e))return s;let a=et(n),i=a!=null?String(a):r.map(O).join("");return`${s}${i}</${e}>`}function se(t,e){let n=t.match(/^((?:<!--.*?-->)*)<([a-zA-Z][a-zA-Z0-9-]*)/);if(n){let r=n[1],o=n[2],s=r.length+1+o.length;return t.slice(0,s)+` data-hk="${e}"`+t.slice(s)}return t}function w(t){if(typeof document>"u"&&!gt()){let c=R(void 0);return b(c,()=>w(t))}if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return g(String(t));if(typeof t=="function"&&t._signal)return w(t());if(typeof t=="function")try{return w(t())}catch{return typeof process<"u",""}if(Array.isArray(t))return t.map(w).join("");if(t.tag==="__suspense")try{return(t.children||[]).map(w).join("")}catch(c){if(c&&typeof c.then=="function")return w(t.props&&t.props.fallback);throw c}if(typeof t.tag=="function"){let c=t.tag({...t.props,children:t.children});return w(c)}let{tag:e,props:n,children:r}=t,o=nt(n||{}),s=`<${e}${o}>`;if(rt.has(e))return s;let a=et(n),i=a!=null?String(a):r.map(w).join("");return`${s}${i}</${e}>`}function Ke(t){let e=R(void 0);return{body:b(e,()=>w(t)),head:q(e.head)}}async function Ze(t,e={}){let n=t.default||t,r=typeof t.loader=="function"?await t.loader(e):void 0,o=R(r),s=e.params||{};return{body:b(o,()=>w(yt(n,{...s,loaderData:r}))),head:q(o.head),loaderData:r}}var St=12;async function ae(t,e){e||(e=R(void 0));let n="";for(let o=0;o<St;o++){n=b(e,()=>w(t));let s=[...e.resources.values()].filter(a=>a.status==="pending").map(a=>a.promise);if(s.length===0)break;await Promise.all(s)}let r={};for(let[o,s]of e.resources)s.status==="ready"&&(r[o]=s.value);return{body:n,head:q(e.head),loaderData:e.loaderData,resources:r,ctx:e}}async function F(t,e={},n={}){let r=t.default||t,o=typeof t.loader=="function"?await t.loader(e):void 0,s=R(o),a=e.params||{},{body:i,head:c,resources:u}=await ae(yt(r,{...a,loaderData:o}),s),d={loaderData:o??null,resources:u,islandStores:it(s)};return ie({body:i,head:c,payload:d,options:n})}function ie({body:t,head:e,payload:n,options:r={}}){let o=r.lang||"en",s=`<script id="__what_data" type="application/json">${P(n)}<\/script>`,a=r.clientEntry?`<script type="module" src="${g(r.clientEntry)}"><\/script>`:"",c=(r.csrfToken?G(r.csrfToken):"")+(r.head||""),u=r.bodyClass?` class="${g(r.bodyClass)}"`:"";return`<!DOCTYPE html><html lang="${g(o)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${e||""}${c}</head><body${u}>${t}${s}${a}</body></html>`}async function*j(t,e){if(e===void 0&&(e=R(void 0)),t==null||t===!1||t===!0)return;if(typeof t=="string"||typeof t=="number"){yield g(String(t));return}if(typeof t=="function"&&t._signal){let a=b(e,()=>t());yield*j(a,e);return}if(typeof t=="function"){try{let a=b(e,()=>t());yield*j(a,e)}catch{typeof process<"u"}return}if(Array.isArray(t)){for(let a of t)yield*j(a,e);return}if(t.tag==="__suspense"){let a=null;for(let i=0;i<St&&a===null;i++){let c=null;try{a=b(e,()=>(t.children||[]).map(w).join(""))}catch(u){if(u&&typeof u.then=="function")c=u;else throw u}if(a===null){let u=[...e.resources.values()].filter(d=>d.status==="pending").map(d=>d.promise);await Promise.all([c,...u].filter(Boolean))}}a===null&&(a=b(e,()=>w(t.props&&t.props.fallback))),yield a;return}if(typeof t.tag=="function"){try{let a=b(e,()=>t.tag({...t.props,children:t.children})),i=a instanceof Promise?await a:a;yield*j(i,e)}catch(a){typeof process<"u",yield tt?`<!-- SSR Error: ${g(a.message||"Component error")} -->`:"<!-- SSR Error -->"}return}let{tag:n,props:r,children:o}=t,s=nt(r||{});if(yield`<${n}${s}>`,!rt.has(n)){let a=et(r);if(a!=null)yield String(a);else for(let i of o)yield*j(i,e);yield`</${n}>`}}function Qe(t){return{mode:"static",...t}}function qe(t,e={}){let n=R(e),r=b(n,()=>{let s=t.component(e);return w(s)}),o=t.islands||[];return ce({title:t.title||"",meta:t.meta||{},body:r,islands:o,scripts:t.mode==="static"?[]:t.scripts||[],styles:t.styles||[],mode:t.mode})}function ce({title:t,meta:e,body:n,islands:r,scripts:o,styles:s,mode:a}){let i=Object.entries(e).map(([f,p])=>`<meta name="${g(f)}" content="${g(p)}">`).join(`
2
- `),c=s.map(f=>`<link rel="stylesheet" href="${g(f)}">`).join(`
1
+ import{AsyncLocalStorage as Le}from"node:async_hooks";import{__installServerContextStorage as De}from"what-core";import{h as _t,_isAriaAttr as de,getServerContext as At,runWithServerContext as b,beginHeadCollection as fe,endHeadCollection as rt}from"what-core";var xt=new RegExp("[<>&\\u2028\\u2029]","g"),Pt={60:"\\u003c",62:"\\u003e",38:"\\u0026",8232:"\\u2028",8233:"\\u2029"};function $(t){return JSON.stringify(t).replace(xt,e=>Pt[e.charCodeAt(0)])}import{mount as Ue,hydrate as Be,signal as Je,batch as Ve,getServerContext as $t}from"what-core";var Ht=new Map;function It(t){let e=t||$t();return e&&e.islandStores instanceof Map?e.islandStores:null}function lt(t){let e=typeof document<"u"?Ht:It(t),n={};for(let[r,o]of e||[])n[r]=o._getSnapshot();return n}import{signal as _,batch as Z}from"what-core";var T=null,pt=!(typeof process<"u");function X(t){T=t}function Nt(){return T}async function G(t,e){if(T&&T.revalidatePath)return T.revalidatePath(t,e);pt&&console.warn(`[what] revalidatePath('${t}') had no effect: no cache engine is bound. Create a what-isr engine and bind it in your adapter (setRevalidationHandler).`)}async function K(t,e){if(T&&T.revalidateTag)return T.revalidateTag(t,e);pt&&console.warn(`[what] revalidateTag('${t}') had no effect: no cache engine is bound.`)}var Q=new Map;function jt(){if(typeof document<"u"){let t=document.querySelector('meta[name="what-csrf-token"]');if(t)return t.getAttribute("content");let e=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);if(e)return decodeURIComponent(e[1])}return null}function q(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();if(typeof crypto<"u"&&crypto.getRandomValues){let t=new Uint8Array(16);return crypto.getRandomValues(t),Array.from(t,e=>e.toString(16).padStart(2,"0")).join("")}throw new Error("[what] No secure random source available for CSRF token generation")}function ht(t,e){if(!t||!e||t.length!==e.length)return!1;let n=0;for(let r=0;r<t.length;r++)n|=t.charCodeAt(r)^e.charCodeAt(r);return n===0}function tt(t){return`<meta name="what-csrf-token" content="${String(t).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}">`}var Lt=0,Dt=!1;function Mt(){return!Dt&&typeof process<"u",`a_${typeof crypto<"u"&&crypto.getRandomValues?Array.from(crypto.getRandomValues(new Uint8Array(6)),e=>e.toString(16).padStart(2,"0")).join(""):`c${(++Lt).toString(36)}_${Date.now().toString(36)}`}`}function Ft(t,e={}){let n=e.id||Mt(),{onError:r,onSuccess:o,revalidate:a}=e;typeof window>"u"&&Q.set(n,{fn:t,options:e});async function s(...i){if(typeof window>"u")return t(...i);let c=e.timeout||3e4,u=new AbortController,d=setTimeout(()=>u.abort(),c);try{let f=jt(),l={"Content-Type":"application/json","X-What-Action":n};f&&(l["X-CSRF-Token"]=f);let p=await fetch("/__what_action",{method:"POST",headers:l,credentials:"same-origin",signal:u.signal,body:JSON.stringify({args:i})});if(!p.ok){let m=await p.json().catch(()=>({message:"Action failed"}));throw new Error(m.message||"Action failed")}let h=await p.json();if(o&&o(h),a)for(let m of a)gt(m);return h}catch(f){if(f.name==="AbortError"){let l=new Error(`Action "${n}" timed out after ${c}ms`);throw l.code="TIMEOUT",r&&r(l),l}throw r&&r(f),f}finally{clearTimeout(d)}}return s._actionId=n,s._isAction=!0,s}function mt(t,e={}){let{onSuccess:n,onError:r,resetOnSuccess:o=!0}=e;return async a=>{let s,i;a instanceof Event?(a.preventDefault(),i=a.target,s=new FormData(i)):s=a;let c={},u=!1;for(let[d,f]of s.entries())typeof File<"u"&&f instanceof File&&(u=!0),c[d]?Array.isArray(c[d])?c[d].push(f):c[d]=[c[d],f]:c[d]=f;try{let d=u?await t(c,s):await t(c);return n&&n(d,i),o&&i&&i.reset(),d}catch(d){throw r&&r(d,i),d}}}function yt(t){let e=_(!1),n=_(null),r=_(null);async function o(...a){e.set(!0),n.set(null);try{let s=await t(...a);return r.set(s),s}catch(s){throw n.set(s),s}finally{e.set(!1)}}return{trigger:o,isPending:()=>e(),error:()=>n(),data:()=>r(),reset:()=>{n.set(null),r.set(null)}}}function Ut(t,e={}){let{resetOnSuccess:n=!0}=e,r={current:null},o=yt(mt(t,{resetOnSuccess:n}));function a(s){s.preventDefault();let i=new FormData(s.target);return r.current=s.target,o.trigger(i)}return{...o,handleSubmit:a,formRef:r}}function Bt(t,e){let n=_(t),r=_([]),o=_(t);function a(u){let d=e(n.peek(),u);Z(()=>{r.set([...r.peek(),u]),n.set(d)})}function s(u,d){Z(()=>{if(r.set(r.peek().filter(f=>f!==u)),d!==void 0){o.set(d);let f=d;for(let l of r.peek())f=e(f,l);n.set(f)}})}function i(u,d){Z(()=>{let f=r.peek().filter(h=>h!==u);r.set(f);let l=d!==void 0?d:o.peek();o.set(l);let p=l;for(let h of f)p=e(p,h);n.set(p)})}async function c(u,d){a(u);try{let f=await d();return s(u,f),f}catch(f){throw i(u),f}}return{value:()=>n(),isPending:()=>r().length>0,addOptimistic:a,resolve:s,rollback:i,withOptimistic:c,set:u=>{n.set(u),o.set(u)}}}var H=new Map;function Jt(t,e){return H.has(t)||H.set(t,new Set),H.get(t).add(e),()=>{H.get(t)?.delete(e)}}function gt(t){let e=H.get(t);if(e)for(let n of e)try{n()}catch(r){console.error("[what] Revalidation error:",r)}}function D(t,e,n,r={}){let{csrfToken:o,skipCsrf:a=!1}=r;if(!a){if(!o)return Promise.resolve({status:500,body:{message:"[what] CSRF token not configured. Pass { csrfToken: sessionToken } to handleActionRequest, or { skipCsrf: true } to explicitly opt out."}});let i=t?.headers?.["x-csrf-token"]||t?.headers?.["X-CSRF-Token"];if(!ht(i,o))return Promise.resolve({status:403,body:{message:"Invalid CSRF token"}})}let s=Q.get(e);return s?Array.isArray(n)?s.fn(...n).then(async i=>{let c=s.options||{};if(Array.isArray(c.revalidate))for(let u of c.revalidate)await G(u);if(Array.isArray(c.revalidateTags))for(let u of c.revalidateTags)await K(u);return{status:200,body:i}}).catch(i=>(console.error(`[what] Action "${e}" error:`,i),{status:500,body:{message:"Action failed"}})):Promise.resolve({status:400,body:{message:"Invalid action arguments"}}):Promise.resolve({status:404,body:{message:"Action not found"}})}function Vt(){return[...Q.keys()]}function Wt(t,e={}){let{onSuccess:n,onError:r,onSettled:o}=e,a={isPending:_(!1),error:_(null),data:_(null)};async function s(...i){a.isPending.set(!0),a.error.set(null);try{let c=await t(...i);return a.data.set(c),n&&n(c,...i),c}catch(c){throw a.error.set(c),r&&r(c,...i),c}finally{a.isPending.set(!1),o&&o(a.data.peek(),a.error.peek(),...i)}}return{mutate:s,isPending:()=>a.isPending(),error:()=>a.error(),data:()=>a.data(),reset:()=>{a.error.set(null),a.data.set(null)}}}import{h as M}from"what-core";var wt="/__what_action";function zt(t){if(typeof t=="string")return t;if(typeof t=="function"){let e=t._actionId;if(e)return e;throw new Error("[what] <Form action={fn}>: that function is not a server action. Wrap it with action() from what-server, or pass the action id as a string.")}throw new Error("[what] <Form> requires an `action` prop: a server action or its id.")}function Yt(){if(typeof document>"u")return null;let t=document.querySelector('meta[name="what-csrf-token"]');if(t)return t.getAttribute("content");let e=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);return e?decodeURIComponent(e[1]):null}function Xt({action:t,csrfToken:e,redirect:n,method:r="post",enhance:o=!0,children:a,...s}){let i=zt(t),c=e!==void 0?e:Yt();c==null&&typeof process<"u";let u=[M("input",{type:"hidden",name:"_action",value:i}),M("input",{type:"hidden",name:"what-csrf-token",value:c||""})];n&&u.push(M("input",{type:"hidden",name:"_redirect",value:n}));let d={...s,method:r,action:s.action||wt,"data-action":i};o&&(d["data-enhance"]="");let f=a==null?[]:Array.isArray(a)?a:[a];return M("form",d,...u,...f)}var Gt="/__what_action",I=1024*1024;function Kt(t){if(!t)return{};if(typeof t.forEach=="function"&&typeof t.get=="function"){let n={};return t.forEach((r,o)=>{n[o.toLowerCase()]=r}),n}let e={};for(let n in t)e[n.toLowerCase()]=t[n];return e}function F(t,e){return{status:t,headers:{"content-type":"application/json"},body:JSON.stringify(e)}}function et(t,e){return{status:t,headers:{"content-type":"text/html; charset=utf-8"},body:`<!DOCTYPE html><html><body><h1>${t}</h1><p>${String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}</p></body></html>`}}function St(t){if(typeof t!="string"||!t.startsWith("/")||/^[/\\]{2}/.test(t)||t.includes("\\"))return null;try{let e=new URL(t,"http://localhost");return e.origin!=="http://localhost"?null:e.pathname+e.search}catch{return null}}function Zt(t,e){let n=St(t&&t._redirect);if(n)return n;let r=e.referer||e.referrer;if(r)try{let o=new URL(r,"http://localhost"),a=St(o.pathname+o.search);if(a)return a}catch{}return"/"}var Qt=new Set(["_action","data-action","_csrf","what-csrf-token","_redirect"]);function N(t={}){let{getCsrfToken:e,skipCsrf:n=!1}=t;return async function(o){if((o.method||"POST").toUpperCase()!=="POST")return F(405,{message:"Method Not Allowed"});let s=Kt(o.headers),i=s["x-what-action"],c=s["content-type"]||"",u=!i&&c.includes("application/x-www-form-urlencoded"),d=n?void 0:e?await e(o):void 0;if(u){let h=o.body||{},m=h._action||h["data-action"]||o.query&&o.query.action;if(!m)return et(400,'Missing action name (add a hidden "_action" field or ?action= query param)');let S={...s},E=h._csrf??h["what-csrf-token"];if(E&&!S["x-csrf-token"]&&(S["x-csrf-token"]=String(E)),!n&&e&&!d)return et(403,"Missing CSRF token");let C={};for(let[O,z]of Object.entries(h))Qt.has(O)||(C[O]=z);let v=await D({headers:S},m,[C],{csrfToken:d,skipCsrf:n});return v.status===200?{status:303,headers:{location:Zt(h,s)},body:""}:et(v.status,v.body&&v.body.message||"Action failed")}if(!i)return F(400,{message:"Missing X-What-Action header"});if(!n&&e&&!d)return F(403,{message:"Missing CSRF token"});let l=(o.body||{}).args,p=await D({headers:s},i,l,{csrfToken:d,skipCsrf:n});return F(p.status,p.body)}}function qt(t={}){let e=t.basePath||Gt,n=N(t);return async function(o,a,s){let[i,c]=(o.url||"").split("?");if(i!==e||(o.method||"").toUpperCase()!=="POST")return s?s():void 0;let u;try{let l=await te(o);u=U(l,o.headers["content-type"]||"")}catch(l){a.writeHead(l.code==="BODY_TOO_LARGE"?413:400,{"content-type":"application/json"}),a.end(JSON.stringify({message:l.code==="BODY_TOO_LARGE"?"Payload too large":"Invalid request body"}));return}let d=Object.fromEntries(new URLSearchParams(c||"")),f=await n({method:o.method,headers:o.headers,body:u,query:d});a.writeHead(f.status,f.headers),a.end(f.body)}}function U(t,e){if((e||"").includes("application/x-www-form-urlencoded")){let n={};for(let[r,o]of new URLSearchParams(String(t)))n[r]===void 0?n[r]=o:Array.isArray(n[r])?n[r].push(o):n[r]=[n[r],o];return n}return t==null||t===""?{}:JSON.parse(String(t))}async function B(t,e=I){let n=Number(t.headers.get("content-length"));if(Number.isFinite(n)&&n>e)return{tooLarge:!0};let r=t.body;if(!r||typeof r.getReader!="function"){let i=await t.text();return Buffer.byteLength(i,"utf8")>e?{tooLarge:!0}:{raw:i}}let o=r.getReader(),a=[],s=0;for(;;){let{done:i,value:c}=await o.read();if(i)break;if(c){if(s+=c.byteLength,s>e){try{await o.cancel()}catch{}return{tooLarge:!0}}a.push(c)}}return{raw:Buffer.concat(a.map(i=>Buffer.from(i))).toString("utf8")}}function te(t){return new Promise((e,n)=>{let r=0,o=[];t.on("data",a=>{if(r+=a.length,r>I){let s=new Error("Body too large");s.code="BODY_TOO_LARGE",n(s),t.destroy?.();return}o.push(a)}),t.on("end",()=>{if(o.length===0)return e("");e(Buffer.concat(o).toString("utf8"))}),t.on("error",n)})}function ee(t={}){let e=N(t);return async function(n){let r={};try{let s=await B(n);if(s.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});r=U(s.raw,n.headers.get("content-type")||"")}catch{r={}}let o={};try{o=Object.fromEntries(new URL(n.url,"http://localhost").searchParams)}catch{}let a=await e({method:n.method,headers:n.headers,body:r,query:o});return new Response(a.body,{status:a.status,headers:a.headers})}}import{matchRoute as ne,parseQuery as re}from"what-router/match";var oe="/__what_action",ae="/__what_revalidate",nt="what-csrf";function j(t){let e={};return t&&typeof t.forEach=="function"&&t.forEach((n,r)=>{e[r.toLowerCase()]=n}),e}function bt(t,e){if(!t)return null;let n=String(t).match(new RegExp(`(?:^|;\\s*)${e}=([^;]+)`));return n?decodeURIComponent(n[1]):null}async function se(t){try{let e=await B(t);return e.tooLarge?{tooLarge:!0}:U(e.raw,t.headers.get("content-type")||"")}catch{return{}}}async function ie(t){let e;try{e=await B(t)}catch{return{json:{}}}if(e.tooLarge)return{tooLarge:!0};try{return{json:JSON.parse(e.raw)}}catch{return{json:{}}}}function ce(t){return async function(n){let{route:r,params:o,query:a,request:s}=n,i={default:r.component,loader:r.loader},c=n.csrfToken?{...t,csrfToken:n.csrfToken}:t;return{html:await J(i,{params:o,query:a,request:s},c),status:200,tags:n.config&&n.config.tags||[],path:n.path}}}function R(t={}){let{routes:e=[],cache:n,render:r,revalidateWebhook:o,document:a={},notFound:s,basePath:i="",csrf:c=!0}=t,u=c!==!1&&!t.actionHandler,d=t.actionHandler||N(u?{getCsrfToken:l=>bt(l.headers&&l.headers.cookie,nt)}:{skipCsrf:!0}),f=r||ce(a);return n&&(n.revalidatePath||n.revalidateTag)&&X({revalidatePath:n.revalidatePath,revalidateTag:n.revalidateTag}),async function(p){let h=new URL(p.url,"http://localhost"),m=h.pathname;if(i&&m.startsWith(i)&&(m=m.slice(i.length)||"/"),p.method==="POST"&&m===oe){let y=await se(p);if(y&&y.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});let A=await d({method:"POST",headers:j(p.headers),body:y,query:Object.fromEntries(h.searchParams)});return new Response(A.body,{status:A.status,headers:A.headers})}let S=null,E=null;if(u&&(S=bt(j(p.headers).cookie,nt),!S)){S=q();let A=j(p.headers)["x-forwarded-proto"]==="https"||h.protocol==="https:"||!0;E=`${nt}=${encodeURIComponent(S)}; Path=/; SameSite=Lax`+(A?"; Secure":"")}let C=y=>(E&&(y["set-cookie"]=E),y);if(p.method==="POST"&&m===ae&&o){let y=await ie(p);if(y.tooLarge)return new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});let A=await o({headers:j(p.headers),body:y.json});return new Response(JSON.stringify(A.body),{status:A.status,headers:{"content-type":"application/json"}})}let v=ne(m,e);if(!v){let y=s?s():"<!DOCTYPE html><html><body><h1>404 \u2014 Not Found</h1></body></html>";return new Response(y,{status:404,headers:C({"content-type":"text/html; charset=utf-8"})})}let{route:O,params:z}=v,Y=O.page||{mode:O.mode||"client"},P={path:m,query:re(h.search),config:Y,route:O,params:z,request:p};if(n&&Y.mode!=="server"){P.varyHeaders=j(p.headers);let y=await n.handle(P,()=>f(P));return new Response(y.html,{status:y.status||200,headers:C({"content-type":"text/html; charset=utf-8",...y.headers||{}})})}S&&(P.csrfToken=S);let dt=await f(P),ft=C({"content-type":"text/html; charset=utf-8"});return Y.mode==="server"&&(ft["Cache-Control"]="private, no-store"),new Response(dt.html,{status:dt.status||200,headers:ft})}}function ue(t={}){let e=R(t);return{async fetch(n,r,o){return r&&(n.__env=r),o&&(n.__ctx=o),e(n)}}}function k(t){return{head:fe(),loaderData:t,resources:new Map,islandStores:new Map,resourceCounter:0,boundaryCounter:0,suspended:[]}}var Tt=0;function le(){Tt=0}function pe(){return"h"+Tt++}function he(t){if(!At()){let e=k(void 0);return b(e,()=>he(t))}return le(),x(t)}function x(t){if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return g(String(t));if(typeof t=="function"&&t._signal)return`<!--$-->${x(t())}<!--/$-->`;if(typeof t=="function")try{return`<!--$-->${x(t())}<!--/$-->`}catch{return typeof process<"u","<!--$--><!--/$-->"}if(Array.isArray(t))return`<!--[]-->${t.map(x).join("")}<!--/[]-->`;if(typeof t.tag=="function"){let c=pe(),u=t.tag({...t.props,children:t.children}),d=x(u);return me(d,c)}let{tag:e,props:n,children:r}=t;at(e);let o=st(n||{}),a=`<${e}${o}>`;if(it.has(e))return a;let s=ot(n),i=s!=null?String(s):r.map(x).join("");return`${a}${i}</${e}>`}function me(t,e){let n=t.match(/^((?:<!--.*?-->)*)<([a-zA-Z][a-zA-Z0-9-]*)/);if(n){let r=n[1],o=n[2],a=r.length+1+o.length;return t.slice(0,a)+` data-hk="${e}"`+t.slice(a)}return t}function w(t){if(!At()){let c=k(void 0);return b(c,()=>w(t))}if(t==null||t===!1||t===!0)return"";if(typeof t=="string"||typeof t=="number")return g(String(t));if(typeof t=="function"&&t._signal)return w(t());if(typeof t=="function")try{return w(t())}catch{return typeof process<"u",""}if(Array.isArray(t))return t.map(w).join("");if(t.tag==="__suspense")try{return(t.children||[]).map(w).join("")}catch(c){if(c&&typeof c.then=="function")return w(t.props&&t.props.fallback);throw c}if(typeof t.tag=="function"){let c=t.tag({...t.props,children:t.children});return w(c)}let{tag:e,props:n,children:r}=t;at(e);let o=st(n||{}),a=`<${e}${o}>`;if(it.has(e))return a;let s=ot(n),i=s!=null?String(s):r.map(w).join("");return`${a}${i}</${e}>`}function pn(t){let e=k(void 0);return{body:b(e,()=>w(t)),head:rt(e.head)}}async function hn(t,e={}){let n=t.default||t,r=typeof t.loader=="function"?await t.loader(e):void 0,o=k(r),a=e.params||{};return{body:b(o,()=>w(_t(n,{...a,loaderData:r}))),head:rt(o.head),loaderData:r}}var Rt=12;async function ye(t,e){e||(e=k(void 0));let n="";for(let o=0;o<Rt;o++){n=b(e,()=>w(t));let a=[...e.resources.values()].filter(s=>s.status==="pending").map(s=>s.promise);if(a.length===0)break;await Promise.all(a)}let r={};for(let[o,a]of e.resources)a.status==="ready"&&(r[o]=a.value);return{body:n,head:rt(e.head),loaderData:e.loaderData,resources:r,ctx:e}}async function J(t,e={},n={}){let r=t.default||t,o=typeof t.loader=="function"?await t.loader(e):void 0,a=k(o),s=e.params||{},{body:i,head:c,resources:u}=await ye(_t(r,{...s,loaderData:o}),a),d={loaderData:o??null,resources:u,islandStores:lt(a)};return ge({body:i,head:c,payload:d,options:n})}function ge({body:t,head:e,payload:n,options:r={}}){let o=r.lang||"en",a=`<script id="__what_data" type="application/json">${$(n)}<\/script>`,s=r.clientEntry?`<script type="module" src="${g(r.clientEntry)}"><\/script>`:"",c=(r.csrfToken?tt(r.csrfToken):"")+(r.head||""),u=r.bodyClass?` class="${g(r.bodyClass)}"`:"";return`<!DOCTYPE html><html lang="${g(o)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${e||""}${c}</head><body${u}>${t}${a}${s}</body></html>`}async function*L(t,e){if(e===void 0&&(e=k(void 0)),t==null||t===!1||t===!0)return;if(typeof t=="string"||typeof t=="number"){yield g(String(t));return}if(typeof t=="function"&&t._signal){let s=b(e,()=>t());yield*L(s,e);return}if(typeof t=="function"){try{let s=b(e,()=>t());yield*L(s,e)}catch{typeof process<"u"}return}if(Array.isArray(t)){for(let s of t)yield*L(s,e);return}if(t.tag==="__suspense"){let s=null;for(let i=0;i<Rt&&s===null;i++){let c=null;try{s=b(e,()=>(t.children||[]).map(w).join(""))}catch(u){if(u&&typeof u.then=="function")c=u;else throw u}if(s===null){let u=[...e.resources.values()].filter(d=>d.status==="pending").map(d=>d.promise);await Promise.all([c,...u].filter(Boolean))}}s===null&&(s=b(e,()=>w(t.props&&t.props.fallback))),yield s;return}if(typeof t.tag=="function"){try{let s=b(e,()=>t.tag({...t.props,children:t.children})),i=s instanceof Promise?await s:s;yield*L(i,e)}catch(s){typeof process<"u",yield V?`<!-- SSR Error: ${g(s.message||"Component error")} -->`:"<!-- SSR Error -->"}return}let{tag:n,props:r,children:o}=t;at(n);let a=st(r||{});if(yield`<${n}${a}>`,!it.has(n)){let s=ot(r);if(s!=null)yield String(s);else for(let i of o)yield*L(i,e);yield`</${n}>`}}function mn(t){return{mode:"static",...t}}function yn(t,e={}){let n=k(e),r=b(n,()=>{let a=t.component(e);return w(a)}),o=t.islands||[];return we({title:t.title||"",meta:t.meta||{},body:r,islands:o,scripts:t.mode==="static"?[]:t.scripts||[],styles:t.styles||[],mode:t.mode})}function we({title:t,meta:e,body:n,islands:r,scripts:o,styles:a,mode:s}){let i=Object.entries(e).map(([l,p])=>`<meta name="${g(l)}" content="${g(p)}">`).join(`
2
+ `),c=a.map(l=>`<link rel="stylesheet" href="${g(l)}">`).join(`
3
3
  `),u=r.length>0?`
4
4
  <script type="module">
5
5
  import { hydrateIslands } from '/@what/islands.js';
6
6
  hydrateIslands();
7
- <\/script>`:"",d=o.map(f=>`<script type="module" src="${g(f)}"><\/script>`).join(`
8
- `),l=a==="client"?`
7
+ <\/script>`:"",d=o.map(l=>`<script type="module" src="${g(l)}"><\/script>`).join(`
8
+ `),f=s==="client"?`
9
9
  <script type="module" src="/@what/client.js"><\/script>`:"";return`<!DOCTYPE html>
10
10
  <html lang="en">
11
11
  <head>
@@ -19,6 +19,6 @@ import{AsyncLocalStorage as _e}from"node:async_hooks";import{__installServerCont
19
19
  <div id="app">${n}</div>
20
20
  ${u}
21
21
  ${d}
22
- ${l}
22
+ ${f}
23
23
  </body>
24
- </html>`}function tn(t){return t._server=!0,t}var tt=!(typeof process<"u");function et(t){return t?t.dangerouslySetInnerHTML?t.dangerouslySetInnerHTML.__html??null:t.innerHTML&&typeof t.innerHTML=="object"&&"__html"in t.innerHTML?t.innerHTML.__html??null:(t.innerHTML!=null&&typeof t.innerHTML=="string"&&tt&&console.warn("[what-server] innerHTML received a raw string. This is a security risk (XSS). Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead."),null):null}var ue=/^[a-zA-Z_:][a-zA-Z0-9:._-]*$/;function nt(t){let e="";for(let[n,r]of Object.entries(t))if(!(n==="key"||n==="ref"||n==="children"||n==="dangerouslySetInnerHTML"||n==="innerHTML")&&!(n.startsWith("on")&&n.length>2)&&!(r===!1||r==null)){if(!ue.test(n)){tt&&console.warn(`[what-server] Skipping invalid attribute name in SSR: ${JSON.stringify(n)}`);continue}if(n==="className"||n==="class")e+=` class="${g(String(r))}"`;else if(n==="style"&&typeof r=="object"){let o=Object.entries(r).map(([s,a])=>`${fe(s)}:${a}`).join(";");e+=` style="${g(o)}"`}else if(r===!0)n.startsWith("aria-")||n==="role"?e+=` ${n}="true"`:e+=` ${n}`;else{if(de(n,r))continue;e+=` ${n}="${g(String(r))}"`}}return e}function de(t,e){let n=t.toLowerCase();if(!le.has(n))return!1;let r=String(e).trim().replace(/[\u0000-\u001f\u007f\s]+/g,"").toLowerCase();return r.startsWith("javascript:")||r.startsWith("vbscript:")||r.startsWith("data:")}var le=new Set(["href","src","action","formaction","xlink:href"]);function g(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function fe(t){return t.startsWith("--")?t:t.replace(/([A-Z])/g,"-$1").toLowerCase()}var rt=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);import pe from"node:http";async function bt(t){let n=`http://${t.headers.host||"localhost"}${t.url}`,r=new Headers;for(let[s,a]of Object.entries(t.headers))a!=null&&r.set(s,Array.isArray(a)?a.join(", "):String(a));let o;if(t.method!=="GET"&&t.method!=="HEAD"){let s=[];for await(let a of t)s.push(a);s.length&&(o=Buffer.concat(s))}return new Request(n,{method:t.method,headers:r,body:o})}async function At(t,e){t.statusCode=e.status,e.headers.forEach((r,o)=>t.setHeader(o,r));let n=await e.text();t.end(n)}function Tt(t){return async function(n,r){try{let o=await bt(n),s=await t(o);await At(r,s)}catch(o){r.headersSent||r.writeHead(500,{"content-type":"text/html; charset=utf-8"}),r.end("<!DOCTYPE html><html><body><h1>500 \u2014 Server Error</h1></body></html>"),console.error("[what-server] request error:",o)}}}function he(t={}){let e=k(t);return async function(r,o,s){let a=await bt(r),i=await e(a);if(i.status===404&&typeof s=="function")return s();await At(o,i)}}function me(t={}){let e=k(t),n=pe.createServer(Tt(e)),{scheduler:r}=t;if(r){r.start();let o=()=>{try{r.stop()}catch{}n.close()};process.once("SIGTERM",o),process.once("SIGINT",o)}return n}import{mkdir as ye,writeFile as _t}from"node:fs/promises";import{join as ot}from"node:path";import{matchRoute as ge}from"what-router/match";function we(t){return t.includes(":")||t.includes("*")||t.includes("[")}function Se(t,e){return t.replace(/\[\.\.\.(\w+)\]/g,(n,r)=>e[r]??"").replace(/\[(\w+)\]/g,(n,r)=>e[r]??"").replace(/[:*](\w+)/g,(n,r)=>e[r]??"")}async function be({routes:t=[],outDir:e,render:n,documentOptions:r={}}={}){let o=[];for(let s of t){let a=s.page&&s.page.mode||s.mode;if(a!=="static"&&a!=="hybrid")continue;let i={default:s.component,loader:s.loader},c=[s.path];if(we(s.path)){if(typeof s.getStaticPaths!="function")continue;c=((await s.getStaticPaths()).paths||[]).map(d=>Se(s.path,d.params||{}))}for(let u of c){let d=ge(u,[s]),f={params:d?d.params:{},query:{}},p=n?await n(i,f):await F(i,f,r),h=ot(e,u==="/"?"":u);if(await ye(h,{recursive:!0}),await _t(ot(h,"index.html"),p),typeof s.loader=="function"){let m=await s.loader(f);await _t(ot(h,"__what_data.json"),P({loaderData:m}))}o.push(u)}}return{pages:o}}function Ae(t={}){return k(t)}async function Te({outDir:t=".vercel/output",functionName:e="render",runtime:n="nodejs22.x",files:r=null,handler:o="index.mjs",staticDir:s=null}={}){let{mkdir:a,writeFile:i,cp:c}=await import("node:fs/promises"),{join:u,dirname:d}=await import("node:path");await a(t,{recursive:!0});let l={version:3,routes:[{handle:"filesystem"},{src:"/.*",dest:`/${e}`}]};await i(u(t,"config.json"),JSON.stringify(l,null,2)),s&&await c(s,u(t,"static"),{recursive:!0});let f=null;if(r&&typeof r=="object"){f=u(t,"functions",`${e}.func`),await a(f,{recursive:!0});let p={runtime:n,handler:o,launcherType:"Nodejs",shouldAddHelpers:!1,supportsResponseStreaming:!0};await i(u(f,".vc-config.json"),JSON.stringify(p,null,2));for(let[h,m]of Object.entries(r)){let S=u(f,h);await a(d(S),{recursive:!0}),await i(S,m)}o in r||console.warn(`[what-server] buildVercelOutput: files does not include the handler entry "${o}" \u2014 the deploy will 500 until your build emits it.`)}return{config:l,outDir:t,functionDir:f}}ke(new _e);export{jt as action,Te as buildVercelOutput,H as createActionHandler,te as createCloudflareHandler,k as createRequestHandler,me as createServer,Ae as createVercelHandler,G as csrfMetaTag,Qe as definePage,be as exportStatic,zt as fetchActionHandler,dt as formAction,Y as generateCsrfToken,qe as generateStaticPage,Mt as getRegisteredActions,Et as getRevalidationHandler,D as handleActionRequest,ft as invalidatePath,Wt as nodeActionMiddleware,Lt as onRevalidate,F as renderDocument,Ze as renderPage,oe as renderToHydratableString,j as renderToStream,w as renderToString,ae as renderToStringAsync,Ke as renderToStringWithHead,W as revalidatePath,J as revalidateTag,P as serializeState,tn as server,V as setRevalidationHandler,Tt as toNodeListener,lt as useAction,It as useFormAction,Nt as useMutation,Dt as useOptimistic,ut as validateCsrfToken,he as whatMiddleware};
24
+ </html>`}function gn(t){return t._server=!0,t}var V=!(typeof process<"u");function ot(t){return t?t.dangerouslySetInnerHTML?t.dangerouslySetInnerHTML.__html??null:t.innerHTML&&typeof t.innerHTML=="object"&&"__html"in t.innerHTML?t.innerHTML.__html??null:(t.innerHTML!=null&&typeof t.innerHTML=="string"&&V&&console.warn("[what-server] innerHTML received a raw string. This is a security risk (XSS). Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead."),null):null}var Se=/^[a-zA-Z_:][a-zA-Z0-9:._-]*$/,be=/^[a-zA-Z_][a-zA-Z0-9._:-]*$/;function at(t){if(typeof t!="string"||!be.test(t))throw new Error(`[what-server] Invalid tag name in SSR: ${JSON.stringify(t)}`)}function st(t){let e="";for(let[n,r]of Object.entries(t)){if(n==="key"||n==="ref"||n==="children"||n==="dangerouslySetInnerHTML"||n==="innerHTML")continue;let o=n.toLowerCase();if(!(o.startsWith("on")&&n.length>2)){if(r===!1&&de(n)){e+=` ${n}="false"`;continue}if(!(r===!1||r==null)){if(!Se.test(n)){V&&console.warn(`[what-server] Skipping invalid attribute name in SSR: ${JSON.stringify(n)}`);continue}if(Te.has(o)){V&&console.warn(`[what-server] Skipping unsafe attribute in SSR: ${JSON.stringify(n)}`);continue}if(n==="className"||n==="class")e+=` class="${g(String(r))}"`;else if(n==="style"&&typeof r=="object"){let a=Object.entries(r).map(([s,i])=>`${Re(s)}:${i}`).join(";");e+=` style="${g(a)}"`}else if(r===!0)n.startsWith("aria-")||n==="role"?e+=` ${n}="true"`:e+=` ${n}`;else{if(_e(n,r))continue;e+=` ${n}="${g(String(r))}"`}}}}return e}function _e(t,e){let n=t.toLowerCase();if(!Ae.has(n))return!1;let r=String(e).trim().replace(/[\u0000-\u001f\u007f\s]+/g,"").toLowerCase();return r.startsWith("javascript:")||r.startsWith("vbscript:")||r.startsWith("data:")}var Ae=new Set(["href","src","action","formaction","data","ping","xlink:href"]),Te=new Set(["srcdoc"]);function g(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function Re(t){return t.startsWith("--")?t:t.replace(/([A-Z])/g,"-$1").toLowerCase()}var it=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);import ke from"node:http";var ct=Symbol("what.bodyTooLarge"),kt=()=>new Response(JSON.stringify({message:"Payload too large"}),{status:413,headers:{"content-type":"application/json"}});async function vt(t){let n=`http://${t.headers.host||"localhost"}${t.url}`,r=new Headers;for(let[a,s]of Object.entries(t.headers))s!=null&&r.set(a,Array.isArray(s)?s.join(", "):String(s));let o;if(t.method!=="GET"&&t.method!=="HEAD"){let a=Number(t.headers["content-length"]),s=Number.isFinite(a)&&a>I,i=[],c=0;for await(let u of t)if(!s){if(c+=u.length,c>I){s=!0,i.length=0;continue}i.push(u)}if(s)return ct;i.length&&(o=Buffer.concat(i))}return new Request(n,{method:t.method,headers:r,body:o})}async function W(t,e){t.statusCode=e.status,e.headers.forEach((r,o)=>t.setHeader(o,r));let n=await e.text();t.end(n)}function Et(t){return async function(n,r){try{let o=await vt(n);if(o===ct)return await W(r,kt());let a=await t(o);await W(r,a)}catch(o){r.headersSent||r.writeHead(500,{"content-type":"text/html; charset=utf-8"}),r.end("<!DOCTYPE html><html><body><h1>500 \u2014 Server Error</h1></body></html>"),console.error("[what-server] request error:",o)}}}function ve(t={}){let e=R(t);return async function(r,o,a){let s=await vt(r);if(s===ct)return await W(o,kt());let i=await e(s);if(i.status===404&&typeof a=="function")return a();await W(o,i)}}function Ee(t={}){let e=R(t),n=ke.createServer(Et(e)),{scheduler:r}=t;if(r){r.start();let o=()=>{try{r.stop()}catch{}n.close()};process.once("SIGTERM",o),process.once("SIGINT",o)}return n}import{mkdir as Ce,writeFile as Ct}from"node:fs/promises";import{join as ut,resolve as Ot,sep as Oe}from"node:path";import{matchRoute as xe}from"what-router/match";function Pe(t){return t.includes(":")||t.includes("*")||t.includes("[")}function $e(t,e){return t.replace(/\[\.\.\.(\w+)\]/g,(n,r)=>e[r]??"").replace(/\[(\w+)\]/g,(n,r)=>e[r]??"").replace(/[:*](\w+)/g,(n,r)=>e[r]??"")}function He(t,e){let n=Ot(t),r=Ot(ut(t,e==="/"?"":e));if(r!==n&&!r.startsWith(n+Oe))throw new Error(`[what-server] Refusing to write outside outDir: ${e}`);return r}async function Ie({routes:t=[],outDir:e,render:n,documentOptions:r={}}={}){let o=[];for(let a of t){let s=a.page&&a.page.mode||a.mode;if(s!=="static"&&s!=="hybrid")continue;let i={default:a.component,loader:a.loader},c=[a.path];if(Pe(a.path)){if(typeof a.getStaticPaths!="function")continue;c=((await a.getStaticPaths()).paths||[]).map(d=>$e(a.path,d.params||{}))}for(let u of c){let d=He(e,u),f=xe(u,[a]),p={params:f?f.params:{},query:{}},h=n?await n(i,p):await J(i,p,r);if(await Ce(d,{recursive:!0}),await Ct(ut(d,"index.html"),h),typeof a.loader=="function"){let m=await a.loader(p);await Ct(ut(d,"__what_data.json"),$({loaderData:m}))}o.push(u)}}return{pages:o}}function Ne(t={}){return R(t)}async function je({outDir:t=".vercel/output",functionName:e="render",runtime:n="nodejs22.x",files:r=null,handler:o="index.mjs",staticDir:a=null}={}){let{mkdir:s,writeFile:i,cp:c}=await import("node:fs/promises"),{join:u,dirname:d}=await import("node:path");await s(t,{recursive:!0});let f={version:3,routes:[{handle:"filesystem"},{src:"/.*",dest:`/${e}`}]};await i(u(t,"config.json"),JSON.stringify(f,null,2)),a&&await c(a,u(t,"static"),{recursive:!0});let l=null;if(r&&typeof r=="object"){l=u(t,"functions",`${e}.func`),await s(l,{recursive:!0});let p={runtime:n,handler:o,launcherType:"Nodejs",shouldAddHelpers:!1,supportsResponseStreaming:!0};await i(u(l,".vc-config.json"),JSON.stringify(p,null,2));for(let[h,m]of Object.entries(r)){let S=u(l,h);await s(d(S),{recursive:!0}),await i(S,m)}o in r||console.warn(`[what-server] buildVercelOutput: files does not include the handler entry "${o}" \u2014 the deploy will 500 until your build emits it.`)}return{config:f,outDir:t,functionDir:l}}De(new Le);export{wt as ACTION_ENDPOINT,Xt as Form,Ft as action,je as buildVercelOutput,N as createActionHandler,ue as createCloudflareHandler,R as createRequestHandler,Ee as createServer,Ne as createVercelHandler,tt as csrfMetaTag,mn as definePage,Ie as exportStatic,ee as fetchActionHandler,mt as formAction,q as generateCsrfToken,yn as generateStaticPage,Vt as getRegisteredActions,Nt as getRevalidationHandler,D as handleActionRequest,gt as invalidatePath,qt as nodeActionMiddleware,Jt as onRevalidate,J as renderDocument,hn as renderPage,he as renderToHydratableString,L as renderToStream,w as renderToString,ye as renderToStringAsync,pn as renderToStringWithHead,G as revalidatePath,K as revalidateTag,$ as serializeState,gn as server,X as setRevalidationHandler,Et as toNodeListener,yt as useAction,Ut as useFormAction,Wt as useMutation,Bt as useOptimistic,ht as validateCsrfToken,ve as whatMiddleware};
package/index.d.ts CHANGED
@@ -10,6 +10,19 @@ export function renderToString(vnode: VNode): string;
10
10
  /** Render VNode tree as async iterator for streaming */
11
11
  export function renderToStream(vnode: VNode): AsyncGenerator<string>;
12
12
 
13
+ export interface RenderRequestContext {
14
+ params?: Record<string, string>;
15
+ query?: Record<string, string>;
16
+ request?: any;
17
+ [key: string]: any;
18
+ }
19
+
20
+ /** Run a page module's loader, then render it. Returns the body, head and loader data. */
21
+ export function renderPage(
22
+ pageModule: { default: (props: any) => VNode; loader?: (ctx: RenderRequestContext) => any } | ((props: any) => VNode),
23
+ reqCtx?: RenderRequestContext,
24
+ ): Promise<{ body: string; head: string; loaderData: any }>;
25
+
13
26
  // --- Page Configuration ---
14
27
 
15
28
  export interface PageConfig {
@@ -38,97 +51,17 @@ export function generateStaticPage(page: PageConfig, data?: any): string;
38
51
  export function server<P>(component: (props: P) => VNode): (props: P) => VNode;
39
52
 
40
53
  // --- Islands ---
41
-
42
- export interface IslandOptions {
43
- /** Hydration mode */
44
- mode?: 'static' | 'idle' | 'visible' | 'load' | 'media' | 'action';
45
- /** Media query for 'media' mode */
46
- media?: string;
47
- /** Priority (higher = hydrate first) */
48
- priority?: number;
49
- /** Shared stores this island uses */
50
- stores?: string[];
51
- }
52
-
53
- /** Register an island component */
54
- export function island(
55
- name: string,
56
- loader: () => Promise<any>,
57
- options?: IslandOptions
58
- ): void;
59
-
60
- /** Island component wrapper for SSR */
61
- export function Island(props: {
62
- name: string;
63
- props?: Record<string, any>;
64
- mode?: IslandOptions['mode'];
65
- priority?: number;
66
- stores?: string[];
67
- children?: VNodeChild;
68
- }): VNode;
69
-
70
- /** Hydrate all islands on the page */
71
- export function hydrateIslands(): void;
72
-
73
- /** Auto-discover and register islands */
74
- export function autoIslands(registry: Record<string, {
75
- loader: () => Promise<any>;
76
- mode?: IslandOptions['mode'];
77
- media?: string;
78
- priority?: number;
79
- stores?: string[];
80
- } | (() => Promise<any>)>): void;
81
-
82
- /** Boost hydration priority for an island */
83
- export function boostIslandPriority(name: string, newPriority?: number): void;
84
-
85
- // --- Shared Island State ---
86
-
87
- export interface IslandStore<T extends Record<string, any>> {
88
- _signals: Record<keyof T, Signal<any>>;
89
- _subscribe: (key: keyof T, fn: (value: any) => void) => () => void;
90
- _batch: (fn: () => void) => void;
91
- _getSnapshot: () => T;
92
- _hydrate: (data: Partial<T>) => void;
93
- }
94
-
95
- /** Create a shared store for islands */
96
- export function createIslandStore<T extends Record<string, any>>(
97
- name: string,
98
- initialState: T
99
- ): T & IslandStore<T>;
100
-
101
- /** Get or create a shared store */
102
- export function useIslandStore<T extends Record<string, any>>(
103
- name: string,
104
- fallbackInitial?: T
105
- ): T & IslandStore<T>;
106
-
107
- /** Serialize all shared stores for SSR */
108
- export function serializeIslandStores(): string;
109
-
110
- /** Hydrate shared stores from SSR data */
111
- export function hydrateIslandStores(serialized: string | Record<string, any>): void;
112
-
113
- // --- Progressive Enhancement ---
114
-
115
- /** Enhance elements matching selector */
116
- export function enhance(selector: string, handler: (el: Element) => void): void;
117
-
118
- /** Enhance forms for AJAX submission */
119
- export function enhanceForms(selector?: string): void;
120
-
121
- // --- Debugging ---
122
-
123
- export interface IslandStatus {
124
- registered: string[];
125
- hydrated: number;
126
- pending: number;
127
- queue: { name: string; priority: number }[];
128
- stores: string[];
129
- }
130
-
131
- export function getIslandStatus(): IslandStatus;
54
+ //
55
+ // The island runtime (island, Island, hydrateIslands, createIslandStore, …)
56
+ // is exported from the `what-server/islands` subpath, not from this entry.
57
+ // See ./islands.d.ts. The shapes are re-exported here because they describe
58
+ // values that cross the SSR boundary and are referenced by root-entry consumers.
59
+
60
+ // One definition each, owned by ./islands. Re-exporting rather than
61
+ // redeclaring is what keeps `export * from 'what-server'` and
62
+ // `export * from 'what-server/islands'` in the same file from colliding, which
63
+ // is exactly the ambiguity what-framework/server hits.
64
+ export type { IslandOptions, IslandStore, IslandStatus } from './islands.js';
132
65
 
133
66
  // --- Server Actions ---
134
67
 
@@ -235,3 +168,125 @@ export function handleActionRequest(
235
168
  ): Promise<ActionResponse>;
236
169
 
237
170
  export function getRegisteredActions(): string[];
171
+
172
+ // --- Additional SSR entry points ---
173
+
174
+ /**
175
+ * Render with hydration markers (data-hk attributes and comment boundaries) so
176
+ * the client can adopt the server DOM instead of recreating it.
177
+ */
178
+ export function renderToHydratableString(vnode: VNode): string;
179
+
180
+ /** Render, and return the <head> tags collected during that render alongside the body. */
181
+ export function renderToStringWithHead(vnode: VNode): { body: string; head: string };
182
+
183
+ /**
184
+ * Render, awaiting any suspended resources so their data is resolved in the
185
+ * output. `resources` is the payload the client reuses so it does not refetch.
186
+ */
187
+ export function renderToStringAsync(
188
+ vnode: VNode,
189
+ ctx?: unknown,
190
+ ): Promise<{ body: string; head: string; resources: Record<string, unknown> }>;
191
+
192
+ export interface DocumentOptions {
193
+ lang?: string;
194
+ head?: string;
195
+ bodyAttrs?: string;
196
+ scripts?: string[];
197
+ styles?: string[];
198
+ [key: string]: any;
199
+ }
200
+
201
+ /**
202
+ * Render a page module to a complete HTML document: runs its loader, renders the
203
+ * component, collects head tags, and inlines the hydration payload.
204
+ */
205
+ export function renderDocument(
206
+ pageModule: { default: (props: any) => VNode; loader?: (ctx: RenderRequestContext) => any } | ((props: any) => VNode),
207
+ reqCtx?: RenderRequestContext,
208
+ options?: DocumentOptions,
209
+ ): Promise<string>;
210
+
211
+ /** Render a PageConfig to a complete static HTML document. */
212
+ export function generateStaticPage(page: PageConfig, data?: any): string;
213
+
214
+ // --- <Form> ---
215
+ // A real <form method="post"> that posts to the action endpoint, so it submits
216
+ // with JavaScript disabled and is enhanced to a fetch when JS is present.
217
+
218
+ export interface FormProps {
219
+ /** A server action (from `action()`) or its id. */
220
+ action: ((...args: any[]) => any) | string;
221
+ /** Per-request CSRF token. Required for SSR; read from the page when omitted. */
222
+ csrfToken?: string | null;
223
+ /** Where to send the browser after a no-JS submit. */
224
+ redirect?: string;
225
+ method?: string;
226
+ /** Set false to leave a plain HTML form the client enhancer ignores. */
227
+ enhance?: boolean;
228
+ children?: VNodeChild;
229
+ [attr: string]: any;
230
+ }
231
+
232
+ export function Form(props: FormProps): VNode;
233
+
234
+ /** The endpoint server actions post to. */
235
+ export const ACTION_ENDPOINT: string;
236
+
237
+ // --- CSRF ---
238
+
239
+ /** Generate a CSRF token. Call once per session/request. */
240
+ export function generateCsrfToken(): string;
241
+
242
+ /** Constant-time comparison of a request token against the session token. */
243
+ export function validateCsrfToken(requestToken: string | null | undefined, sessionToken: string | null | undefined): boolean;
244
+
245
+ /** The `<meta name="what-csrf-token">` tag to inline into the document head. */
246
+ export function csrfMetaTag(token: string): string;
247
+
248
+ // --- Action handlers ---
249
+ // Runtime-neutral core, plus the two host bindings.
250
+
251
+ export interface ActionHandlerOptions {
252
+ csrfSecret?: string;
253
+ onError?: (error: unknown) => void;
254
+ [key: string]: any;
255
+ }
256
+
257
+ export function createActionHandler(options?: ActionHandlerOptions): (request: any) => Promise<any>;
258
+ export function nodeActionMiddleware(options?: ActionHandlerOptions): (req: any, res: any, next?: () => void) => void;
259
+ export function fetchActionHandler(options?: ActionHandlerOptions): (request: Request) => Promise<Response>;
260
+
261
+ // --- Deploy adapters ---
262
+
263
+ export interface RequestHandlerOptions {
264
+ routes?: any[];
265
+ documentOptions?: DocumentOptions;
266
+ [key: string]: any;
267
+ }
268
+
269
+ /** Runtime-neutral request handler: Request in, Response out. */
270
+ export function createRequestHandler(options?: RequestHandlerOptions): (request: Request, env?: any, ctx?: any) => Promise<Response>;
271
+
272
+ /** Cloudflare Workers entry wrapping createRequestHandler. */
273
+ export function createCloudflareHandler(options?: RequestHandlerOptions): { fetch: (request: Request, env?: any, ctx?: any) => Promise<Response> };
274
+
275
+ // --- Revalidation registry ---
276
+ // App code calls revalidatePath/revalidateTag; the deploy adapter binds a
277
+ // what-isr engine through setRevalidationHandler.
278
+
279
+ export interface RevalidationHandler {
280
+ revalidatePath?: (path: string, options?: any) => any;
281
+ revalidateTag?: (tag: string, options?: any) => any;
282
+ }
283
+
284
+ export function setRevalidationHandler(handler: RevalidationHandler | null): void;
285
+ export function getRevalidationHandler(): RevalidationHandler | null;
286
+ export function revalidatePath(path: string, options?: any): Promise<any>;
287
+ export function revalidateTag(tag: string, options?: any): Promise<any>;
288
+
289
+ // --- Serialization ---
290
+
291
+ /** Serialize a value for safe inlining into a <script> tag (escapes `</script`). */
292
+ export function serializeState(value: unknown): string;
package/islands.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ // What Framework Server Islands - TypeScript Definitions
2
+
3
+ import { VNode, VNodeChild, Signal } from 'what-core';
4
+
5
+ // --- Islands ---
6
+
7
+ export interface IslandOptions {
8
+ /** Hydration mode */
9
+ mode?: 'static' | 'idle' | 'visible' | 'load' | 'media' | 'action';
10
+ /** Media query for 'media' mode */
11
+ media?: string;
12
+ /** Priority (higher = hydrate first) */
13
+ priority?: number;
14
+ /** Shared stores this island uses */
15
+ stores?: string[];
16
+ }
17
+
18
+ /** Register an island component */
19
+ export function island(
20
+ name: string,
21
+ loader: () => Promise<any>,
22
+ options?: IslandOptions
23
+ ): void;
24
+
25
+ /** Island component wrapper for SSR */
26
+ export function Island(props: {
27
+ name: string;
28
+ props?: Record<string, any>;
29
+ mode?: IslandOptions['mode'];
30
+ priority?: number;
31
+ stores?: string[];
32
+ children?: VNodeChild;
33
+ }): VNode;
34
+
35
+ /** Hydrate all islands on the page */
36
+ export function hydrateIslands(): void;
37
+
38
+ /** Auto-discover and register islands */
39
+ export function autoIslands(registry: Record<string, {
40
+ loader: () => Promise<any>;
41
+ mode?: IslandOptions['mode'];
42
+ media?: string;
43
+ priority?: number;
44
+ stores?: string[];
45
+ } | (() => Promise<any>)>): void;
46
+
47
+ /** Boost hydration priority for an island */
48
+ export function boostIslandPriority(name: string, newPriority?: number): void;
49
+
50
+ // --- Shared Island State ---
51
+
52
+ export interface IslandStore<T extends Record<string, any>> {
53
+ _signals: Record<keyof T, Signal<any>>;
54
+ _subscribe: (key: keyof T, fn: (value: any) => void) => () => void;
55
+ _batch: (fn: () => void) => void;
56
+ _getSnapshot: () => T;
57
+ _hydrate: (data: Partial<T>) => void;
58
+ }
59
+
60
+ /** Create a shared store for islands */
61
+ export function createIslandStore<T extends Record<string, any>>(
62
+ name: string,
63
+ initialState: T
64
+ ): T & IslandStore<T>;
65
+
66
+ /** Get or create a shared store */
67
+ export function useIslandStore<T extends Record<string, any>>(
68
+ name: string,
69
+ fallbackInitial?: T
70
+ ): T & IslandStore<T>;
71
+
72
+ /** Serialize all shared stores for SSR */
73
+ export function serializeIslandStores(): string;
74
+
75
+ /** Snapshot every shared store for the current render context */
76
+ export function getIslandStoresSnapshot(context?: any): Record<string, any>;
77
+
78
+ /** Hydrate shared stores from SSR data */
79
+ export function hydrateIslandStores(serialized: string | Record<string, any>): void;
80
+
81
+ // --- Progressive Enhancement ---
82
+
83
+ /** Enhance elements matching selector */
84
+ export function enhance(selector: string, handler: (el: Element) => void): void;
85
+
86
+ /** Enhance forms for AJAX submission */
87
+ export function enhanceForms(selector?: string): void;
88
+
89
+ // --- Debugging ---
90
+
91
+ export interface IslandStatus {
92
+ registered: string[];
93
+ hydrated: number;
94
+ pending: number;
95
+ queue: { name: string; priority: number }[];
96
+ stores: string[];
97
+ }
98
+
99
+ export function getIslandStatus(): IslandStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-server",
3
- "version": "0.11.7",
3
+ "version": "0.12.0",
4
4
  "description": "What Framework - SSR, islands architecture, static generation",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -9,6 +9,7 @@
9
9
  ".": {
10
10
  "types": "./index.d.ts",
11
11
  "node": {
12
+ "types": "./node.d.ts",
12
13
  "production": "./dist/node.min.js",
13
14
  "import": "./src/node.js",
14
15
  "default": "./src/node.js"
@@ -18,6 +19,7 @@
18
19
  "default": "./src/index.js"
19
20
  },
20
21
  "./islands": {
22
+ "types": "./islands.d.ts",
21
23
  "production": "./dist/islands.min.js",
22
24
  "import": "./src/islands.js",
23
25
  "default": "./src/islands.js"
@@ -31,7 +33,8 @@
31
33
  "files": [
32
34
  "src",
33
35
  "dist/**/*.min.js",
34
- "index.d.ts"
36
+ "index.d.ts",
37
+ "islands.d.ts"
35
38
  ],
36
39
  "sideEffects": [
37
40
  "./src/node.js",
@@ -47,8 +50,8 @@
47
50
  "author": "ZVN DEV (https://zvndev.com)",
48
51
  "license": "MIT",
49
52
  "peerDependencies": {
50
- "what-core": "^0.11.7",
51
- "what-router": "^0.11.7"
53
+ "what-core": "^0.12.0",
54
+ "what-router": "^0.12.0"
52
55
  },
53
56
  "peerDependenciesMeta": {
54
57
  "what-router": {
@@ -9,7 +9,7 @@
9
9
  import { handleActionRequest } from './actions.js';
10
10
 
11
11
  const DEFAULT_BASE_PATH = '/__what_action';
12
- const MAX_BODY_BYTES = 1024 * 1024; // 1 MB
12
+ export const MAX_BODY_BYTES = 1024 * 1024; // 1 MB
13
13
 
14
14
  function lowerHeaders(headers) {
15
15
  if (!headers) return {};
@@ -51,7 +51,15 @@ function htmlResponse(status, message) {
51
51
  // "/\evil.com" canonicalizes to http://evil.com (an open redirect). We reject
52
52
  // anything starting with two slash-or-backslash chars or containing a
53
53
  // backslash, then canonicalize via URL and require the localhost origin.
54
- function safeLocalPath(value) {
54
+ //
55
+ // Sibling predicate: isSafeUrl in packages/router/src/index.js. That one gates
56
+ // a client navigation target and legitimately allows absolute http(s), mailto:
57
+ // and tel:. This one gates a server-issued `Location:` header, an
58
+ // attacker-controllable open-redirect primitive, so it must stay strictly
59
+ // narrower: same-origin local paths only. Harden one, re-read the other; do not
60
+ // unify them. packages/server/test/redirect-predicate-parity.test.js gates the
61
+ // ordering. Exported for that test only, not part of the package surface.
62
+ export function safeLocalPath(value) {
55
63
  if (typeof value !== 'string' || !value.startsWith('/')) return null;
56
64
  // Reject protocol-relative / backslash-smuggled targets up front.
57
65
  if (/^[/\\]{2}/.test(value) || value.includes('\\')) return null;
@@ -39,8 +39,14 @@ async function readActionBody(request) {
39
39
  } catch { return {}; }
40
40
  }
41
41
 
42
+ // Same cap for the revalidate webhook: the secret is only checked once the body
43
+ // is parsed, so an unauthenticated client must never be able to make the origin
44
+ // buffer an unbounded payload before its 401.
42
45
  async function readJsonBody(request) {
43
- try { return await request.json(); } catch { return {}; }
46
+ let read;
47
+ try { read = await readFetchBodyCapped(request); } catch { return { json: {} }; }
48
+ if (read.tooLarge) return { tooLarge: true };
49
+ try { return { json: JSON.parse(read.raw) }; } catch { return { json: {} }; }
44
50
  }
45
51
 
46
52
  function defaultRenderRoute(documentOptions) {
@@ -164,8 +170,14 @@ export function createRequestHandler(options = {}) {
164
170
 
165
171
  // On-demand revalidation webhook
166
172
  if (request.method === 'POST' && pathname === REVALIDATE_PATH && revalidateWebhook) {
167
- const body = await readJsonBody(request);
168
- const out = await revalidateWebhook({ headers: headersToObject(request.headers), body });
173
+ const read = await readJsonBody(request);
174
+ if (read.tooLarge) {
175
+ return new Response(JSON.stringify({ message: 'Payload too large' }), {
176
+ status: 413,
177
+ headers: { 'content-type': 'application/json' },
178
+ });
179
+ }
180
+ const out = await revalidateWebhook({ headers: headersToObject(request.headers), body: read.json });
169
181
  return new Response(JSON.stringify(out.body), {
170
182
  status: out.status,
171
183
  headers: { 'content-type': 'application/json' },
@@ -187,6 +199,10 @@ export function createRequestHandler(options = {}) {
187
199
  // NOTE: cached HTML is shared across users, so the per-user CSRF token is
188
200
  // NOT embedded in the page here — clients read it from the cookie instead.
189
201
  if (cache && config.mode !== 'server') {
202
+ // The cache engine resolves the route's declared `vary` names against
203
+ // these. Without them it fails closed: warns and bypasses on every
204
+ // request, which is how the vary control shipped never executing.
205
+ routeMatch.varyHeaders = headersToObject(request.headers);
190
206
  const result = await cache.handle(routeMatch, () => renderRoute(routeMatch));
191
207
  return new Response(result.html, {
192
208
  status: result.status || 200,
@@ -4,6 +4,17 @@
4
4
 
5
5
  import http from 'node:http';
6
6
  import { createRequestHandler } from './core.js';
7
+ import { MAX_BODY_BYTES } from '../action-handler.js';
8
+
9
+ // A Request object only exists after the body is read, so the cap the fetch
10
+ // path enforces would run too late here. Returned instead of a Request when the
11
+ // body is over the limit, so the caller answers 413 without buffering it all.
12
+ const TOO_LARGE = Symbol('what.bodyTooLarge');
13
+
14
+ const TOO_LARGE_RESPONSE = () => new Response(
15
+ JSON.stringify({ message: 'Payload too large' }),
16
+ { status: 413, headers: { 'content-type': 'application/json' } },
17
+ );
7
18
 
8
19
  async function nodeToWebRequest(req) {
9
20
  const host = req.headers.host || 'localhost';
@@ -14,8 +25,25 @@ async function nodeToWebRequest(req) {
14
25
  }
15
26
  let body;
16
27
  if (req.method !== 'GET' && req.method !== 'HEAD') {
28
+ const declared = Number(req.headers['content-length']);
29
+ let tooLarge = Number.isFinite(declared) && declared > MAX_BODY_BYTES;
17
30
  const chunks = [];
18
- for await (const chunk of req) chunks.push(chunk);
31
+ let size = 0;
32
+ // Once over the cap the remaining chunks are drained and discarded rather
33
+ // than buffered: memory stays bounded, and the socket survives long enough
34
+ // to carry the 413 back. Chunked transfer or a spoofed Content-Length
35
+ // cannot get past the running total.
36
+ for await (const chunk of req) {
37
+ if (tooLarge) continue;
38
+ size += chunk.length;
39
+ if (size > MAX_BODY_BYTES) {
40
+ tooLarge = true;
41
+ chunks.length = 0;
42
+ continue;
43
+ }
44
+ chunks.push(chunk);
45
+ }
46
+ if (tooLarge) return TOO_LARGE;
19
47
  if (chunks.length) body = Buffer.concat(chunks);
20
48
  }
21
49
  return new Request(url, { method: req.method, headers, body });
@@ -33,6 +61,7 @@ export function toNodeListener(handler) {
33
61
  return async function listener(req, res) {
34
62
  try {
35
63
  const webReq = await nodeToWebRequest(req);
64
+ if (webReq === TOO_LARGE) return await sendWebResponse(res, TOO_LARGE_RESPONSE());
36
65
  const webRes = await handler(webReq);
37
66
  await sendWebResponse(res, webRes);
38
67
  } catch (err) {
@@ -49,6 +78,7 @@ export function whatMiddleware(options = {}) {
49
78
  const handler = createRequestHandler(options);
50
79
  return async function middleware(req, res, next) {
51
80
  const webReq = await nodeToWebRequest(req);
81
+ if (webReq === TOO_LARGE) return await sendWebResponse(res, TOO_LARGE_RESPONSE());
52
82
  const webRes = await handler(webReq);
53
83
  if (webRes.status === 404 && typeof next === 'function') return next();
54
84
  await sendWebResponse(res, webRes);
@@ -3,7 +3,7 @@
3
3
  // navigation, mirroring Next's _next/data).
4
4
 
5
5
  import { mkdir, writeFile } from 'node:fs/promises';
6
- import { join } from 'node:path';
6
+ import { join, resolve, sep } from 'node:path';
7
7
  import { matchRoute } from 'what-router/match';
8
8
  import { renderDocument, serializeState } from '../index.js';
9
9
 
@@ -19,6 +19,19 @@ function buildConcretePath(pattern, params) {
19
19
  .replace(/[:*](\w+)/g, (_, n) => params[n] ?? '');
20
20
  }
21
21
 
22
+ // getStaticPaths params come from user data (a CMS slug, a filename), so a value
23
+ // like '../../etc/cron.d/x' would escape outDir once join() collapses it. Every
24
+ // output directory is checked against the resolved outDir before anything is
25
+ // written.
26
+ function resolveInsideOutDir(outDir, urlPath) {
27
+ const root = resolve(outDir);
28
+ const dirPath = resolve(join(outDir, urlPath === '/' ? '' : urlPath));
29
+ if (dirPath !== root && !dirPath.startsWith(root + sep)) {
30
+ throw new Error(`[what-server] Refusing to write outside outDir: ${urlPath}`);
31
+ }
32
+ return dirPath;
33
+ }
34
+
22
35
  export async function exportStatic({ routes = [], outDir, render, documentOptions = {} } = {}) {
23
36
  const written = [];
24
37
 
@@ -36,6 +49,7 @@ export async function exportStatic({ routes = [], outDir, render, documentOption
36
49
  }
37
50
 
38
51
  for (const urlPath of concrete) {
52
+ const dirPath = resolveInsideOutDir(outDir, urlPath);
39
53
  const matched = matchRoute(urlPath, [route]);
40
54
  const params = matched ? matched.params : {};
41
55
  const reqCtx = { params, query: {} };
@@ -44,7 +58,6 @@ export async function exportStatic({ routes = [], outDir, render, documentOption
44
58
  ? await render(pageModule, reqCtx)
45
59
  : await renderDocument(pageModule, reqCtx, documentOptions);
46
60
 
47
- const dirPath = join(outDir, urlPath === '/' ? '' : urlPath);
48
61
  await mkdir(dirPath, { recursive: true });
49
62
  await writeFile(join(dirPath, 'index.html'), html);
50
63
 
package/src/form.js ADDED
@@ -0,0 +1,92 @@
1
+ // What Framework - <Form>
2
+ //
3
+ // The no-JS server-action path works and is covered by the scaffold smoke test,
4
+ // but reaching it meant hand-writing four exact things per form: the endpoint,
5
+ // the `_action` id, the `what-csrf-token` field, and the `_redirect`. Getting the
6
+ // CSRF field name wrong is silent: the form posts, the double-submit check
7
+ // rejects it, and nothing on the page says why. This component is the one piece
8
+ // that was missing between the forms module and a path that already worked.
9
+ //
10
+ // <Form action={createPost} redirect="/">
11
+ // <input name="title" required />
12
+ // <button>Publish</button>
13
+ // </Form>
14
+ //
15
+ // Renders a real <form method="post">, so it submits with JavaScript disabled.
16
+ // With JS, the client enhancer intercepts it and posts without a page reload.
17
+
18
+ import { h } from 'what-core';
19
+
20
+ export const ACTION_ENDPOINT = '/__what_action';
21
+
22
+ // The action id lives on the callable wrapper that action() returns. Accepting
23
+ // the function itself is the point: passing the id as a string means keeping two
24
+ // spellings in sync by hand, which is the failure this component exists to stop.
25
+ function resolveActionId(action) {
26
+ if (typeof action === 'string') return action;
27
+ if (typeof action === 'function') {
28
+ const id = action._actionId;
29
+ if (id) return id;
30
+ throw new Error(
31
+ '[what] <Form action={fn}>: that function is not a server action. ' +
32
+ 'Wrap it with action() from what-server, or pass the action id as a string.'
33
+ );
34
+ }
35
+ throw new Error('[what] <Form> requires an `action` prop: a server action or its id.');
36
+ }
37
+
38
+ // On the server there is no document, so the per-request token must be handed in
39
+ // (typically from a loader). On the client it can be recovered from the page.
40
+ function readClientCsrfToken() {
41
+ if (typeof document === 'undefined') return null;
42
+ const meta = document.querySelector('meta[name="what-csrf-token"]');
43
+ if (meta) return meta.getAttribute('content');
44
+ const match = document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);
45
+ return match ? decodeURIComponent(match[1]) : null;
46
+ }
47
+
48
+ export function Form({
49
+ action,
50
+ csrfToken,
51
+ redirect,
52
+ method = 'post',
53
+ enhance = true,
54
+ children,
55
+ ...rest
56
+ }) {
57
+ const actionId = resolveActionId(action);
58
+ const token = csrfToken !== undefined ? csrfToken : readClientCsrfToken();
59
+
60
+ if (
61
+ token == null
62
+ && typeof process !== 'undefined'
63
+ && process.env?.NODE_ENV !== 'production'
64
+ ) {
65
+ console.warn(
66
+ `[what] <Form action="${actionId}"> has no CSRF token. Server-rendered forms must ` +
67
+ 'receive one (`csrfToken` is passed to loaders), otherwise the double-submit ' +
68
+ 'check rejects the POST and the failure is silent.'
69
+ );
70
+ }
71
+
72
+ const hidden = [
73
+ h('input', { type: 'hidden', name: '_action', value: actionId }),
74
+ h('input', { type: 'hidden', name: 'what-csrf-token', value: token || '' }),
75
+ ];
76
+ if (redirect) {
77
+ hidden.push(h('input', { type: 'hidden', name: '_redirect', value: redirect }));
78
+ }
79
+
80
+ const props = {
81
+ ...rest,
82
+ method,
83
+ action: rest.action || ACTION_ENDPOINT,
84
+ 'data-action': actionId,
85
+ };
86
+ // `data-enhance` is the selector enhanceForms() looks for. Opting out leaves a
87
+ // plain HTML form, which still works: that is the whole point of the design.
88
+ if (enhance) props['data-enhance'] = '';
89
+
90
+ const kids = children == null ? [] : (Array.isArray(children) ? children : [children]);
91
+ return h('form', props, ...hidden, ...kids);
92
+ }
package/src/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import {
6
6
  h,
7
+ _isAriaAttr,
7
8
  getServerContext,
8
9
  runWithServerContext,
9
10
  beginHeadCollection,
@@ -42,7 +43,14 @@ function nextHydrationId() {
42
43
  // so the client can reuse the server-rendered DOM.
43
44
 
44
45
  export function renderToHydratableString(vnode) {
45
- if (typeof document === 'undefined' && !getServerContext()) {
46
+ // Establish a render scope whenever one isn't already active. This used to be
47
+ // gated on `typeof document === 'undefined'` as a proxy for "are we on the
48
+ // server", but renderToString IS the server render, and the proxy is wrong
49
+ // under any DOM shim (jsdom, happy-dom, a Workers polyfill) where SSR ran with
50
+ // no scope at all and render-scoped state fell back to module globals. Every
51
+ // other consumer of the context checks `typeof document` itself before reading
52
+ // it, so a scope existing on the client changes nothing for them.
53
+ if (!getServerContext()) {
46
54
  const ctx = createRenderContext(undefined);
47
55
  return runWithServerContext(ctx, () => renderToHydratableString(vnode));
48
56
  }
@@ -91,6 +99,7 @@ function _renderHydratable(vnode) {
91
99
 
92
100
  // Element
93
101
  const { tag, props, children } = vnode;
102
+ assertSafeTag(tag);
94
103
  const attrs = renderAttrs(props || {});
95
104
  const open = `<${tag}${attrs}>`;
96
105
 
@@ -119,7 +128,9 @@ function injectHydrationKey(html, hkId) {
119
128
  // Renders a VNode tree to an HTML string. Used for SSR and static gen.
120
129
 
121
130
  export function renderToString(vnode) {
122
- if (typeof document === 'undefined' && !getServerContext()) {
131
+ // See renderToHydratableString: the scope is established by "no scope yet",
132
+ // not by "no document".
133
+ if (!getServerContext()) {
123
134
  const ctx = createRenderContext(undefined);
124
135
  return runWithServerContext(ctx, () => renderToString(vnode));
125
136
  }
@@ -174,6 +185,7 @@ export function renderToString(vnode) {
174
185
 
175
186
  // Element
176
187
  const { tag, props, children } = vnode;
188
+ assertSafeTag(tag);
177
189
  const attrs = renderAttrs(props || {});
178
190
  const open = `<${tag}${attrs}>`;
179
191
 
@@ -369,6 +381,7 @@ export async function* renderToStream(vnode, ctx) {
369
381
  }
370
382
 
371
383
  const { tag, props, children } = vnode;
384
+ assertSafeTag(tag);
372
385
  const attrs = renderAttrs(props || {});
373
386
  yield `<${tag}${attrs}>`;
374
387
 
@@ -512,11 +525,32 @@ function _resolveInnerHTML(props) {
512
525
  // xlink:href, SVG names like stroke-width, and namespaced attributes.
513
526
  const SAFE_ATTR_NAME = /^[a-zA-Z_:][a-zA-Z0-9:._-]*$/;
514
527
 
528
+ // Tag names are emitted verbatim too, so they get the same treatment:
529
+ // h('div onload=alert(1) x', {}) -> <div onload=alert(1) x>
530
+ // Allows custom elements (my-el), SVG camelCase (clipPath) and the internal
531
+ // '__suspense' marker tag.
532
+ const SAFE_TAG_NAME = /^[a-zA-Z_][a-zA-Z0-9._:-]*$/;
533
+
534
+ function assertSafeTag(tag) {
535
+ if (typeof tag !== 'string' || !SAFE_TAG_NAME.test(tag)) {
536
+ throw new Error(`[what-server] Invalid tag name in SSR: ${JSON.stringify(tag)}`);
537
+ }
538
+ }
539
+
515
540
  function renderAttrs(props) {
516
541
  let out = '';
517
542
  for (const [key, val] of Object.entries(props)) {
518
543
  if (key === 'key' || key === 'ref' || key === 'children' || key === 'dangerouslySetInnerHTML' || key === 'innerHTML') continue;
519
- if (key.startsWith('on') && key.length > 2) continue; // Skip event handlers in SSR
544
+ const lowerKey = key.toLowerCase();
545
+ if (lowerKey.startsWith('on') && key.length > 2) continue; // Skip event handlers in SSR
546
+ // aria-*/role are enumerated, so `false` is a real value and must survive:
547
+ // an absent `aria-expanded` means "unsupported", `aria-expanded="false"`
548
+ // means "collapsed". Every other attribute keeps HTML boolean semantics,
549
+ // where false means omit. Checked before the falsy skip for that reason.
550
+ if (val === false && _isAriaAttr(key)) {
551
+ out += ` ${key}="false"`;
552
+ continue;
553
+ }
520
554
  if (val === false || val == null) continue;
521
555
  if (!SAFE_ATTR_NAME.test(key)) {
522
556
  if (_isDevMode) {
@@ -524,6 +558,12 @@ function renderAttrs(props) {
524
558
  }
525
559
  continue;
526
560
  }
561
+ if (REFUSED_ATTRS.has(lowerKey)) {
562
+ if (_isDevMode) {
563
+ console.warn(`[what-server] Skipping unsafe attribute in SSR: ${JSON.stringify(key)}`);
564
+ }
565
+ continue;
566
+ }
527
567
 
528
568
  if (key === 'className' || key === 'class') {
529
569
  out += ` class="${escapeHtml(String(val))}"`;
@@ -556,10 +596,17 @@ function isUnsafeUrlAttribute(key, val) {
556
596
  return normalizedValue.startsWith('javascript:') || normalizedValue.startsWith('vbscript:') || normalizedValue.startsWith('data:');
557
597
  }
558
598
 
599
+ // Must stay in step with URL_ATTRS in what-core's dom.js. Both sets are gated by
600
+ // the parity test in test/ssr-security.test.js.
559
601
  const URL_ATTRS = new Set([
560
- 'href', 'src', 'action', 'formaction', 'xlink:href',
602
+ 'href', 'src', 'action', 'formaction', 'data', 'ping', 'xlink:href',
561
603
  ]);
562
604
 
605
+ // Attributes whose value the browser parses as markup or code, so escaping is
606
+ // not a defense: srcdoc is entity-decoded and parsed as a full document, which
607
+ // revives "&lt;script&gt;". These are refused outright.
608
+ const REFUSED_ATTRS = new Set(['srcdoc']);
609
+
563
610
  function escapeHtml(str) {
564
611
  return str
565
612
  .replace(/&/g, '&amp;')
@@ -596,6 +643,9 @@ export {
596
643
  csrfMetaTag,
597
644
  } from './actions.js';
598
645
 
646
+ // <Form>: the progressive-enhancement form that posts to the action endpoint.
647
+ export { Form, ACTION_ENDPOINT } from './form.js';
648
+
599
649
  // Served server actions: wire the /__what_action route (Node + fetch adapters)
600
650
  export {
601
651
  createActionHandler,
package/src/islands.js CHANGED
@@ -296,15 +296,26 @@ export function boostIslandPriority(name, newPriority = 100) {
296
296
  // --- Client-side hydration ---
297
297
 
298
298
  export function hydrateIslands() {
299
- // First, hydrate any shared stores from the page
300
- const storeScript = document.querySelector('script[data-island-stores]');
301
- if (storeScript) {
302
- hydrateIslandStores(storeScript.textContent);
299
+ // First, hydrate any shared stores from the page. renderDocument emits them
300
+ // inside the consolidated #__what_data payload.
301
+ const dataScript = document.getElementById('__what_data');
302
+ if (dataScript) {
303
+ try {
304
+ const payload = JSON.parse(dataScript.textContent || '{}');
305
+ if (payload && payload.islandStores) hydrateIslandStores(payload.islandStores);
306
+ } catch (e) {
307
+ console.warn('[what] Failed to parse hydration payload:', e);
308
+ }
303
309
  }
304
310
 
305
311
  const islands = document.querySelectorAll('[data-island]');
306
312
 
307
313
  for (const el of islands) {
314
+ // Compiler-emitted islands (`<Counter client:idle />`) carry a direct
315
+ // component reference and schedule their own hydration, so they are not in
316
+ // the registry and must not be claimed here.
317
+ if (el.dataset.islandSelf) continue;
318
+
308
319
  const name = el.dataset.island;
309
320
  const mode = el.dataset.islandMode || 'idle';
310
321
  const props = JSON.parse(el.dataset.islandProps || '{}');
@@ -325,7 +336,7 @@ export function hydrateIslands() {
325
336
  }
326
337
 
327
338
  function scheduleHydration(el, entry, props, mode, priority, name, stores) {
328
- const hydrate = async () => {
339
+ const hydrateIsland = async () => {
329
340
  if (hydratedIslands.has(el)) return;
330
341
  hydratedIslands.add(el);
331
342
 
@@ -363,17 +374,17 @@ function scheduleHydration(el, entry, props, mode, priority, name, stores) {
363
374
  switch (mode) {
364
375
  case 'load':
365
376
  // Immediate hydration via queue (respects priority)
366
- enqueueHydration({ name, priority: priority + 1000, hydrate });
377
+ enqueueHydration({ name, priority: priority + 1000, hydrate: hydrateIsland });
367
378
  break;
368
379
 
369
380
  case 'idle':
370
381
  if ('requestIdleCallback' in window) {
371
382
  requestIdleCallback(() => {
372
- enqueueHydration({ name, priority, hydrate });
383
+ enqueueHydration({ name, priority, hydrate: hydrateIsland });
373
384
  });
374
385
  } else {
375
386
  setTimeout(() => {
376
- enqueueHydration({ name, priority, hydrate });
387
+ enqueueHydration({ name, priority, hydrate: hydrateIsland });
377
388
  }, 200);
378
389
  }
379
390
  break;
@@ -383,7 +394,7 @@ function scheduleHydration(el, entry, props, mode, priority, name, stores) {
383
394
  for (const entry of entries) {
384
395
  if (entry.isIntersecting) {
385
396
  obs.disconnect();
386
- enqueueHydration({ name, priority, hydrate });
397
+ enqueueHydration({ name, priority, hydrate: hydrateIsland });
387
398
  break;
388
399
  }
389
400
  }
@@ -395,11 +406,11 @@ function scheduleHydration(el, entry, props, mode, priority, name, stores) {
395
406
  case 'media': {
396
407
  const mq = window.matchMedia(entry.media || '(max-width: 768px)');
397
408
  if (mq.matches) {
398
- enqueueHydration({ name, priority, hydrate });
409
+ enqueueHydration({ name, priority, hydrate: hydrateIsland });
399
410
  } else {
400
411
  mq.addEventListener('change', (e) => {
401
412
  if (e.matches) {
402
- enqueueHydration({ name, priority, hydrate });
413
+ enqueueHydration({ name, priority, hydrate: hydrateIsland });
403
414
  }
404
415
  }, { once: true });
405
416
  }
@@ -411,7 +422,7 @@ function scheduleHydration(el, entry, props, mode, priority, name, stores) {
411
422
  const handler = () => {
412
423
  events.forEach(e => el.removeEventListener(e, handler));
413
424
  // Boost priority since user interacted
414
- enqueueHydration({ name, priority: priority + 500, hydrate });
425
+ enqueueHydration({ name, priority: priority + 500, hydrate: hydrateIsland });
415
426
  };
416
427
  events.forEach(e => el.addEventListener(e, handler, { once: true, passive: true }));
417
428
  break;
@@ -422,7 +433,7 @@ function scheduleHydration(el, entry, props, mode, priority, name, stores) {
422
433
  break;
423
434
 
424
435
  default:
425
- enqueueHydration({ name, priority, hydrate });
436
+ enqueueHydration({ name, priority, hydrate: hydrateIsland });
426
437
  }
427
438
  }
428
439
 
@@ -483,7 +494,8 @@ export function enhanceForms(selector = 'form[data-enhance]') {
483
494
  if (!csrfToken && !noCsrf) {
484
495
  console.warn(
485
496
  '[what] Form submission blocked: no CSRF token found. ' +
486
- 'Add a <meta name="csrf-token"> tag or set data-no-csrf="true" on the form to opt out.'
497
+ 'Add a <meta name="what-csrf-token"> tag (csrfMetaTag() emits it) ' +
498
+ 'or set data-no-csrf="true" on the form to opt out.'
487
499
  );
488
500
  form.dispatchEvent(new CustomEvent('form:error', {
489
501
  bubbles: true,