what-server 0.8.4 → 0.11.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.
@@ -1,2 +1,2 @@
1
- import{mount as E,signal as I,batch as g}from"what-core";var y=new Map,m=new Set,l=[],b=!1,h=new Map;function x(e,o){if(h.has(e))return h.get(e);let t={},n={};for(let[s,r]of Object.entries(o))n[s]=I(r),Object.defineProperty(t,s,{get:()=>n[s](),set:a=>n[s].set(a),enumerable:!0});return t._signals=n,t._subscribe=(s,r)=>{if(n[s])return n[s].subscribe(r)},t._batch=s=>g(s),t._getSnapshot=()=>{let s={};for(let[r,a]of Object.entries(n))s[r]=a.peek();return s},t._hydrate=s=>{g(()=>{for(let[r,a]of Object.entries(s))n[r]&&n[r].set(a)})},h.set(e,t),t}function S(e,o={}){return h.has(e)?h.get(e):x(e,o)}function N(){let e={};for(let[o,t]of h)e[o]=t._getSnapshot();return JSON.stringify(e)}function O(e){try{let o=typeof e=="string"?JSON.parse(e):e;for(let[t,n]of Object.entries(o))S(t,n)._hydrate(n)}catch(o){console.warn("[what] Failed to hydrate island stores:",o)}}function C(e,o,t={}){y.set(e,{loader:o,mode:t.mode||"idle",media:t.media||null,priority:t.priority||0,stores:t.stores||[]})}function _({name:e,props:o={},children:t,mode:n,priority:s,stores:r}){let a=y.get(e),i=n||a?.mode||"idle",d=s??a?.priority??0,c=r||a?.stores||[];return{tag:"div",props:{"data-island":e,"data-island-mode":i,"data-island-props":JSON.stringify(o),"data-island-priority":d,"data-island-stores":JSON.stringify(c)},children:t||[],key:null,_vnode:!0}}function f(e){let o=!1;for(let t=0;t<l.length;t++)if(e.priority>l[t].priority){l.splice(t,0,e),o=!0;break}o||l.push(e),w()}function w(){if(b||l.length===0)return;b=!0;let e=l.shift();Promise.resolve(e.hydrate()).catch(o=>console.error("[what] Island hydration failed:",e.name,o)).finally(()=>{b=!1,queueMicrotask(w)})}function F(e,o=100){for(let t of l)if(t.name===e){t.priority=o,l.sort((n,s)=>s.priority-n.priority);break}}function k(){let e=document.querySelector("script[data-island-stores]");e&&O(e.textContent);let o=document.querySelectorAll("[data-island]");for(let t of o){let n=t.dataset.island,s=t.dataset.islandMode||"idle",r=JSON.parse(t.dataset.islandProps||"{}"),a=parseInt(t.dataset.islandPriority||"0",10),i=JSON.parse(t.dataset.islandStores||"[]"),d=y.get(n);if(!d){console.warn(`[what] Island "${n}" not registered`);continue}m.has(t)||q(t,d,r,s,a,n,i)}}function q(e,o,t,n,s,r,a){let i=async()=>{if(m.has(e))return;m.add(e);let d=await o.loader(),c=d.default||d,u={};for(let v of a)u[v]=S(v);let p=c({...t,...u});e.childNodes.length>0?i(p,e):E(p,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:r,mode:n}}))};switch(n){case"load":f({name:r,priority:s+1e3,hydrate:i});break;case"idle":"requestIdleCallback"in window?requestIdleCallback(()=>{f({name:r,priority:s,hydrate:i})}):setTimeout(()=>{f({name:r,priority:s,hydrate:i})},200);break;case"visible":{new IntersectionObserver((c,u)=>{for(let p of c)if(p.isIntersecting){u.disconnect(),f({name:r,priority:s,hydrate:i});break}},{rootMargin:"200px"}).observe(e);break}case"media":{let d=window.matchMedia(o.media||"(max-width: 768px)");d.matches?f({name:r,priority:s,hydrate:i}):d.addEventListener("change",c=>{c.matches&&f({name:r,priority:s,hydrate:i})},{once:!0});break}case"action":{let d=["click","focus","mouseover","touchstart"],c=()=>{d.forEach(u=>e.removeEventListener(u,c)),f({name:r,priority:s+500,hydrate:i})};d.forEach(u=>e.addEventListener(u,c,{once:!0,passive:!0}));break}case"static":break;default:f({name:r,priority:s,hydrate:i})}}function L(e){for(let[o,t]of Object.entries(e))C(o,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",k):k())}function M(e,o){if(typeof document>"u")return;let t=document.querySelectorAll(e);for(let n of t)n.dataset.enhanced||(n.dataset.enhanced="true",o(n))}function j(e="form[data-enhance]"){M(e,o=>{o.addEventListener("submit",async t=>{t.preventDefault();let n=new FormData(o),s=o.method.toUpperCase()||"POST",r=o.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=o.getAttribute("data-no-csrf")==="true";if(!i&&!d){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.'),o.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(r,{method:s,body:s==="GET"?void 0:n,headers:c});o.dispatchEvent(new CustomEvent("form:response",{bubbles:!0,detail:{response:u,ok:u.ok}}))}catch(a){o.dispatchEvent(new CustomEvent("form:error",{bubbles:!0,detail:{error:a}}))}})})}function J(){return{registered:[...y.keys()],hydrated:m.size,pending:l.length,queue:l.map(o=>({name:o.name,priority:o.priority})),stores:[...h.keys()]}}export{_ as Island,L as autoIslands,F as boostIslandPriority,x as createIslandStore,M as enhance,j as enhanceForms,J as getIslandStatus,O as hydrateIslandStores,k as hydrateIslands,C as island,N as serializeIslandStores,S as useIslandStore};
1
+ import{mount as C,signal as O,batch as S}from"what-core";var E=new RegExp("[<>&\\u2028\\u2029]","g"),I={60:"\\u003c",62:"\\u003e",38:"\\u0026",8232:"\\u2028",8233:"\\u2029"};function v(e){return JSON.stringify(e).replace(E,o=>I[o.charCodeAt(0)])}var y=new Map,m=new Set,l=[],b=!1,p=new Map;function q(e,o){if(p.has(e))return p.get(e);let t={},s={};for(let[n,r]of Object.entries(o))s[n]=O(r),Object.defineProperty(t,n,{get:()=>s[n](),set:a=>s[n].set(a),enumerable:!0});return t._signals=s,t._subscribe=(n,r)=>{if(s[n])return s[n].subscribe(r)},t._batch=n=>S(n),t._getSnapshot=()=>{let n={};for(let[r,a]of Object.entries(s))n[r]=a.peek();return n},t._hydrate=n=>{S(()=>{for(let[r,a]of Object.entries(n))s[r]&&s[r].set(a)})},p.set(e,t),t}function w(e,o={}){return p.has(e)?p.get(e):q(e,o)}function J(){return v(A())}function A(){let e={};for(let[o,t]of p)e[o]=t._getSnapshot();return e}function M(e){try{let o=typeof e=="string"?JSON.parse(e):e;for(let[t,s]of Object.entries(o))w(t,s)._hydrate(s)}catch(o){console.warn("[what] Failed to hydrate island stores:",o)}}function P(e,o,t={}){y.set(e,{loader:o,mode:t.mode||"idle",media:t.media||null,priority:t.priority||0,stores:t.stores||[]})}function T({name:e,props:o={},children:t,mode:s,priority:n,stores:r}){let a=y.get(e),i=s||a?.mode||"idle",d=n??a?.priority??0,c=r||a?.stores||[];return{tag:"div",props:{"data-island":e,"data-island-mode":i,"data-island-props":JSON.stringify(o),"data-island-priority":d,"data-island-stores":JSON.stringify(c)},children:t||[],key:null,_vnode:!0}}function f(e){let o=!1;for(let t=0;t<l.length;t++)if(e.priority>l[t].priority){l.splice(t,0,e),o=!0;break}o||l.push(e),x()}function x(){if(b||l.length===0)return;b=!0;let e=l.shift();Promise.resolve(e.hydrate()).catch(o=>console.error("[what] Island hydration failed:",e.name,o)).finally(()=>{b=!1,queueMicrotask(x)})}function D(e,o=100){for(let t of l)if(t.name===e){t.priority=o,l.sort((s,n)=>n.priority-s.priority);break}}function k(){let e=document.querySelector("script[data-island-stores]");e&&M(e.textContent);let o=document.querySelectorAll("[data-island]");for(let t of o){let s=t.dataset.island,n=t.dataset.islandMode||"idle",r=JSON.parse(t.dataset.islandProps||"{}"),a=parseInt(t.dataset.islandPriority||"0",10),i=JSON.parse(t.dataset.islandStores||"[]"),d=y.get(s);if(!d){console.warn(`[what] Island "${s}" not registered`);continue}m.has(t)||N(t,d,r,n,a,s,i)}}function N(e,o,t,s,n,r,a){let i=async()=>{if(m.has(e))return;m.add(e);let d=await o.loader(),c=d.default||d,u={};for(let g of a)u[g]=w(g);let h=c({...t,...u});e.childNodes.length>0?i(h,e):C(h,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:r,mode:s}}))};switch(s){case"load":f({name:r,priority:n+1e3,hydrate:i});break;case"idle":"requestIdleCallback"in window?requestIdleCallback(()=>{f({name:r,priority:n,hydrate:i})}):setTimeout(()=>{f({name:r,priority:n,hydrate:i})},200);break;case"visible":{new IntersectionObserver((c,u)=>{for(let h of c)if(h.isIntersecting){u.disconnect(),f({name:r,priority:n,hydrate:i});break}},{rootMargin:"200px"}).observe(e);break}case"media":{let d=window.matchMedia(o.media||"(max-width: 768px)");d.matches?f({name:r,priority:n,hydrate:i}):d.addEventListener("change",c=>{c.matches&&f({name:r,priority:n,hydrate:i})},{once:!0});break}case"action":{let d=["click","focus","mouseover","touchstart"],c=()=>{d.forEach(u=>e.removeEventListener(u,c)),f({name:r,priority:n+500,hydrate:i})};d.forEach(u=>e.addEventListener(u,c,{once:!0,passive:!0}));break}case"static":break;default:f({name:r,priority:n,hydrate:i})}}function H(e){for(let[o,t]of Object.entries(e))P(o,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",k):k())}function _(e,o){if(typeof document>"u")return;let t=document.querySelectorAll(e);for(let s of t)s.dataset.enhanced||(s.dataset.enhanced="true",o(s))}function Q(e="form[data-enhance]"){_(e,o=>{o.addEventListener("submit",async t=>{t.preventDefault();let s=new FormData(o),n=o.method.toUpperCase()||"POST",r=o.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=o.getAttribute("data-no-csrf")==="true";if(!i&&!d){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.'),o.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(r,{method:n,body:n==="GET"?void 0:s,headers:c});o.dispatchEvent(new CustomEvent("form:response",{bubbles:!0,detail:{response:u,ok:u.ok}}))}catch(a){o.dispatchEvent(new CustomEvent("form:error",{bubbles:!0,detail:{error:a}}))}})})}function X(){return{registered:[...y.keys()],hydrated:m.size,pending:l.length,queue:l.map(o=>({name:o.name,priority:o.priority})),stores:[...p.keys()]}}export{T as Island,H as autoIslands,D as boostIslandPriority,q as createIslandStore,_ as enhance,Q as enhanceForms,X as getIslandStatus,A as getIslandStoresSnapshot,M as hydrateIslandStores,k as hydrateIslands,P as island,J as serializeIslandStores,w as useIslandStore};
2
2
  //# sourceMappingURL=islands.min.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/islands.js"],
4
- "sourcesContent": ["// What Framework - Islands Architecture\n// Each interactive piece of the page is an \"island\" \u2014 a self-contained\n// component that hydrates independently. The rest is static HTML.\n//\n// Features:\n// - Multiple hydration modes (load, idle, visible, action, media, static)\n// - Shared state across islands\n// - Priority-based hydration queue\n// - Progressive enhancement\n//\n// Modes:\n// 'static' - No JS shipped. Pure HTML. (nav, footer, etc.)\n// 'idle' - Hydrate when browser is idle (requestIdleCallback)\n// 'visible' - Hydrate when scrolled into view (IntersectionObserver)\n// 'load' - Hydrate immediately on page load\n// 'media' - Hydrate when media query matches (e.g., mobile-only)\n// 'action' - Hydrate on first user interaction (click, focus, hover)\n\nimport { mount, hydrate, signal, batch } from 'what-core';\n\nconst islandRegistry = new Map();\nconst hydratedIslands = new Set();\nconst hydrationQueue = [];\nlet isProcessingQueue = false;\n\n// --- Shared Island State ---\n// Global reactive store that persists across islands and page navigations\n\nconst sharedStores = new Map();\n\nexport function createIslandStore(name, initialState) {\n if (sharedStores.has(name)) {\n return sharedStores.get(name);\n }\n\n const store = {};\n const signals = {};\n\n // Create signals for each key in initial state\n for (const [key, value] of Object.entries(initialState)) {\n signals[key] = signal(value);\n Object.defineProperty(store, key, {\n get: () => signals[key](),\n set: (val) => signals[key].set(val),\n enumerable: true,\n });\n }\n\n // Methods to interact with store\n store._signals = signals;\n store._subscribe = (key, fn) => {\n if (signals[key]) {\n return signals[key].subscribe(fn);\n }\n };\n store._batch = (fn) => batch(fn);\n store._getSnapshot = () => {\n const snapshot = {};\n for (const [key, sig] of Object.entries(signals)) {\n snapshot[key] = sig.peek();\n }\n return snapshot;\n };\n store._hydrate = (data) => {\n batch(() => {\n for (const [key, value] of Object.entries(data)) {\n if (signals[key]) {\n signals[key].set(value);\n }\n }\n });\n };\n\n sharedStores.set(name, store);\n return store;\n}\n\n// Get or create a shared store\nexport function useIslandStore(name, fallbackInitial = {}) {\n if (sharedStores.has(name)) {\n return sharedStores.get(name);\n }\n return createIslandStore(name, fallbackInitial);\n}\n\n// Serialize all shared stores for SSR\nexport function serializeIslandStores() {\n const data = {};\n for (const [name, store] of sharedStores) {\n data[name] = store._getSnapshot();\n }\n return JSON.stringify(data);\n}\n\n// Hydrate shared stores from SSR data\nexport function hydrateIslandStores(serialized) {\n try {\n const data = typeof serialized === 'string' ? JSON.parse(serialized) : serialized;\n for (const [name, storeData] of Object.entries(data)) {\n const store = useIslandStore(name, storeData);\n store._hydrate(storeData);\n }\n } catch (e) {\n console.warn('[what] Failed to hydrate island stores:', e);\n }\n}\n\n// --- Register an island component ---\n\nexport function island(name, loader, opts = {}) {\n islandRegistry.set(name, {\n loader, // () => import('./MyComponent.js')\n mode: opts.mode || 'idle',\n media: opts.media || null,\n priority: opts.priority || 0, // Higher = hydrate first\n stores: opts.stores || [], // Shared stores this island uses\n });\n}\n\n// --- Island wrapper for SSR ---\n// Renders the static HTML with a marker the client can find.\n\nexport function Island({ name, props = {}, children, mode, priority, stores }) {\n const entry = islandRegistry.get(name);\n const resolvedMode = mode || entry?.mode || 'idle';\n const resolvedPriority = priority ?? entry?.priority ?? 0;\n const resolvedStores = stores || entry?.stores || [];\n\n // Server: render as a div with data attributes for hydration\n return {\n tag: 'div',\n props: {\n 'data-island': name,\n 'data-island-mode': resolvedMode,\n 'data-island-props': JSON.stringify(props),\n 'data-island-priority': resolvedPriority,\n 'data-island-stores': JSON.stringify(resolvedStores),\n },\n children: children || [],\n key: null,\n _vnode: true,\n };\n}\n\n// --- Priority Hydration Queue ---\n\nfunction enqueueHydration(task) {\n // Insert in priority order (higher priority first)\n let inserted = false;\n for (let i = 0; i < hydrationQueue.length; i++) {\n if (task.priority > hydrationQueue[i].priority) {\n hydrationQueue.splice(i, 0, task);\n inserted = true;\n break;\n }\n }\n if (!inserted) {\n hydrationQueue.push(task);\n }\n\n processQueue();\n}\n\nfunction processQueue() {\n if (isProcessingQueue || hydrationQueue.length === 0) return;\n isProcessingQueue = true;\n\n // Process one task at a time to avoid blocking\n const task = hydrationQueue.shift();\n\n Promise.resolve(task.hydrate())\n .catch(e => console.error('[what] Island hydration failed:', task.name, e))\n .finally(() => {\n isProcessingQueue = false;\n // Continue processing after a microtask\n queueMicrotask(processQueue);\n });\n}\n\n// Boost priority for an island (e.g., on user interaction)\nexport function boostIslandPriority(name, newPriority = 100) {\n for (const task of hydrationQueue) {\n if (task.name === name) {\n task.priority = newPriority;\n // Re-sort queue\n hydrationQueue.sort((a, b) => b.priority - a.priority);\n break;\n }\n }\n}\n\n// --- Client-side hydration ---\n\nexport function hydrateIslands() {\n // First, hydrate any shared stores from the page\n const storeScript = document.querySelector('script[data-island-stores]');\n if (storeScript) {\n hydrateIslandStores(storeScript.textContent);\n }\n\n const islands = document.querySelectorAll('[data-island]');\n\n for (const el of islands) {\n const name = el.dataset.island;\n const mode = el.dataset.islandMode || 'idle';\n const props = JSON.parse(el.dataset.islandProps || '{}');\n const priority = parseInt(el.dataset.islandPriority || '0', 10);\n const stores = JSON.parse(el.dataset.islandStores || '[]');\n const entry = islandRegistry.get(name);\n\n if (!entry) {\n console.warn(`[what] Island \"${name}\" not registered`);\n continue;\n }\n\n // Skip if already hydrated\n if (hydratedIslands.has(el)) continue;\n\n scheduleHydration(el, entry, props, mode, priority, name, stores);\n }\n}\n\nfunction scheduleHydration(el, entry, props, mode, priority, name, stores) {\n const hydrate = async () => {\n if (hydratedIslands.has(el)) return;\n hydratedIslands.add(el);\n\n const mod = await entry.loader();\n const Component = mod.default || mod;\n\n // Inject shared stores into props\n const storeProps = {};\n for (const storeName of stores) {\n storeProps[storeName] = useIslandStore(storeName);\n }\n\n // Use hydrate() to reuse server-rendered DOM instead of destroying/recreating\n const vnode = Component({ ...props, ...storeProps });\n if (el.childNodes.length > 0) {\n hydrate(vnode, el);\n } else {\n mount(vnode, el);\n }\n\n // Clean up data attributes\n el.removeAttribute('data-island');\n el.removeAttribute('data-island-mode');\n el.removeAttribute('data-island-props');\n el.removeAttribute('data-island-priority');\n el.removeAttribute('data-island-stores');\n\n // Dispatch event for analytics/debugging\n el.dispatchEvent(new CustomEvent('island:hydrated', {\n bubbles: true,\n detail: { name, mode },\n }));\n };\n\n switch (mode) {\n case 'load':\n // Immediate hydration via queue (respects priority)\n enqueueHydration({ name, priority: priority + 1000, hydrate });\n break;\n\n case 'idle':\n if ('requestIdleCallback' in window) {\n requestIdleCallback(() => {\n enqueueHydration({ name, priority, hydrate });\n });\n } else {\n setTimeout(() => {\n enqueueHydration({ name, priority, hydrate });\n }, 200);\n }\n break;\n\n case 'visible': {\n const observer = new IntersectionObserver((entries, obs) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n obs.disconnect();\n enqueueHydration({ name, priority, hydrate });\n break;\n }\n }\n }, { rootMargin: '200px' });\n observer.observe(el);\n break;\n }\n\n case 'media': {\n const mq = window.matchMedia(entry.media || '(max-width: 768px)');\n if (mq.matches) {\n enqueueHydration({ name, priority, hydrate });\n } else {\n mq.addEventListener('change', (e) => {\n if (e.matches) {\n enqueueHydration({ name, priority, hydrate });\n }\n }, { once: true });\n }\n break;\n }\n\n case 'action': {\n const events = ['click', 'focus', 'mouseover', 'touchstart'];\n const handler = () => {\n events.forEach(e => el.removeEventListener(e, handler));\n // Boost priority since user interacted\n enqueueHydration({ name, priority: priority + 500, hydrate });\n };\n events.forEach(e => el.addEventListener(e, handler, { once: true, passive: true }));\n break;\n }\n\n case 'static':\n // Never hydrate\n break;\n\n default:\n enqueueHydration({ name, priority, hydrate });\n }\n}\n\n// --- Auto-discover islands from data attributes ---\n// Call this once on the client to set up all islands.\n\nexport function autoIslands(registry) {\n for (const [name, config] of Object.entries(registry)) {\n island(name, config.loader || config, {\n mode: config.mode || 'idle',\n media: config.media,\n priority: config.priority || 0,\n stores: config.stores || [],\n });\n }\n\n if (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', hydrateIslands);\n } else {\n hydrateIslands();\n }\n }\n}\n\n// --- Progressive Enhancement Helpers ---\n\n// Mark an element as progressively enhanced\nexport function enhance(selector, handler) {\n if (typeof document === 'undefined') return;\n\n const elements = document.querySelectorAll(selector);\n for (const el of elements) {\n if (el.dataset.enhanced) continue;\n el.dataset.enhanced = 'true';\n handler(el);\n }\n}\n\n// Form enhancement: submit via fetch instead of page reload\nexport function enhanceForms(selector = 'form[data-enhance]') {\n enhance(selector, (form) => {\n form.addEventListener('submit', async (e) => {\n e.preventDefault();\n\n const formData = new FormData(form);\n const method = form.method.toUpperCase() || 'POST';\n const action = form.action || location.href;\n\n try {\n // Read CSRF token from meta tag\n const csrfMeta = document.querySelector('meta[name=\"csrf-token\"]') ||\n document.querySelector('meta[name=\"what-csrf-token\"]');\n const csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : null;\n\n // If no CSRF token and form hasn't opted out, block submission\n const noCsrf = form.getAttribute('data-no-csrf') === 'true';\n if (!csrfToken && !noCsrf) {\n console.warn(\n '[what] Form submission blocked: no CSRF token found. ' +\n 'Add a <meta name=\"csrf-token\"> tag or set data-no-csrf=\"true\" on the form to opt out.'\n );\n form.dispatchEvent(new CustomEvent('form:error', {\n bubbles: true,\n detail: { error: new Error('Missing CSRF token') },\n }));\n return;\n }\n\n const headers = {\n 'X-Requested-With': 'XMLHttpRequest',\n };\n if (csrfToken) {\n headers['X-CSRF-Token'] = csrfToken;\n }\n\n const response = await fetch(action, {\n method,\n body: method === 'GET' ? undefined : formData,\n headers,\n });\n\n form.dispatchEvent(new CustomEvent('form:response', {\n bubbles: true,\n detail: { response, ok: response.ok },\n }));\n } catch (error) {\n form.dispatchEvent(new CustomEvent('form:error', {\n bubbles: true,\n detail: { error },\n }));\n }\n });\n });\n}\n\n// --- Debugging ---\n\nexport function getIslandStatus() {\n const status = {\n registered: [...islandRegistry.keys()],\n hydrated: hydratedIslands.size,\n pending: hydrationQueue.length,\n queue: hydrationQueue.map(t => ({ name: t.name, priority: t.priority })),\n stores: [...sharedStores.keys()],\n };\n return status;\n}\n"],
5
- "mappings": "AAkBA,OAAS,SAAAA,EAAgB,UAAAC,EAAQ,SAAAC,MAAa,YAE9C,IAAMC,EAAiB,IAAI,IACrBC,EAAkB,IAAI,IACtBC,EAAiB,CAAC,EACpBC,EAAoB,GAKlBC,EAAe,IAAI,IAElB,SAASC,EAAkBC,EAAMC,EAAc,CACpD,GAAIH,EAAa,IAAIE,CAAI,EACvB,OAAOF,EAAa,IAAIE,CAAI,EAG9B,IAAME,EAAQ,CAAC,EACTC,EAAU,CAAC,EAGjB,OAAW,CAACC,EAAKC,CAAK,IAAK,OAAO,QAAQJ,CAAY,EACpDE,EAAQC,CAAG,EAAIZ,EAAOa,CAAK,EAC3B,OAAO,eAAeH,EAAOE,EAAK,CAChC,IAAK,IAAMD,EAAQC,CAAG,EAAE,EACxB,IAAME,GAAQH,EAAQC,CAAG,EAAE,IAAIE,CAAG,EAClC,WAAY,EACd,CAAC,EAIH,OAAAJ,EAAM,SAAWC,EACjBD,EAAM,WAAa,CAACE,EAAKG,IAAO,CAC9B,GAAIJ,EAAQC,CAAG,EACb,OAAOD,EAAQC,CAAG,EAAE,UAAUG,CAAE,CAEpC,EACAL,EAAM,OAAUK,GAAOd,EAAMc,CAAE,EAC/BL,EAAM,aAAe,IAAM,CACzB,IAAMM,EAAW,CAAC,EAClB,OAAW,CAACJ,EAAKK,CAAG,IAAK,OAAO,QAAQN,CAAO,EAC7CK,EAASJ,CAAG,EAAIK,EAAI,KAAK,EAE3B,OAAOD,CACT,EACAN,EAAM,SAAYQ,GAAS,CACzBjB,EAAM,IAAM,CACV,OAAW,CAACW,EAAKC,CAAK,IAAK,OAAO,QAAQK,CAAI,EACxCP,EAAQC,CAAG,GACbD,EAAQC,CAAG,EAAE,IAAIC,CAAK,CAG5B,CAAC,CACH,EAEAP,EAAa,IAAIE,EAAME,CAAK,EACrBA,CACT,CAGO,SAASS,EAAeX,EAAMY,EAAkB,CAAC,EAAG,CACzD,OAAId,EAAa,IAAIE,CAAI,EAChBF,EAAa,IAAIE,CAAI,EAEvBD,EAAkBC,EAAMY,CAAe,CAChD,CAGO,SAASC,GAAwB,CACtC,IAAMH,EAAO,CAAC,EACd,OAAW,CAACV,EAAME,CAAK,IAAKJ,EAC1BY,EAAKV,CAAI,EAAIE,EAAM,aAAa,EAElC,OAAO,KAAK,UAAUQ,CAAI,CAC5B,CAGO,SAASI,EAAoBC,EAAY,CAC9C,GAAI,CACF,IAAML,EAAO,OAAOK,GAAe,SAAW,KAAK,MAAMA,CAAU,EAAIA,EACvE,OAAW,CAACf,EAAMgB,CAAS,IAAK,OAAO,QAAQN,CAAI,EACnCC,EAAeX,EAAMgB,CAAS,EACtC,SAASA,CAAS,CAE5B,OAASC,EAAG,CACV,QAAQ,KAAK,0CAA2CA,CAAC,CAC3D,CACF,CAIO,SAASC,EAAOlB,EAAMmB,EAAQC,EAAO,CAAC,EAAG,CAC9C1B,EAAe,IAAIM,EAAM,CACvB,OAAAmB,EACA,KAAMC,EAAK,MAAQ,OACnB,MAAOA,EAAK,OAAS,KACrB,SAAUA,EAAK,UAAY,EAC3B,OAAQA,EAAK,QAAU,CAAC,CAC1B,CAAC,CACH,CAKO,SAASC,EAAO,CAAE,KAAArB,EAAM,MAAAsB,EAAQ,CAAC,EAAG,SAAAC,EAAU,KAAAC,EAAM,SAAAC,EAAU,OAAAC,CAAO,EAAG,CAC7E,IAAMC,EAAQjC,EAAe,IAAIM,CAAI,EAC/B4B,EAAeJ,GAAQG,GAAO,MAAQ,OACtCE,EAAmBJ,GAAYE,GAAO,UAAY,EAClDG,EAAiBJ,GAAUC,GAAO,QAAU,CAAC,EAGnD,MAAO,CACL,IAAK,MACL,MAAO,CACL,cAAe3B,EACf,mBAAoB4B,EACpB,oBAAqB,KAAK,UAAUN,CAAK,EACzC,uBAAwBO,EACxB,qBAAsB,KAAK,UAAUC,CAAc,CACrD,EACA,SAAUP,GAAY,CAAC,EACvB,IAAK,KACL,OAAQ,EACV,CACF,CAIA,SAASQ,EAAiBC,EAAM,CAE9B,IAAIC,EAAW,GACf,QAASC,EAAI,EAAGA,EAAItC,EAAe,OAAQsC,IACzC,GAAIF,EAAK,SAAWpC,EAAesC,CAAC,EAAE,SAAU,CAC9CtC,EAAe,OAAOsC,EAAG,EAAGF,CAAI,EAChCC,EAAW,GACX,KACF,CAEGA,GACHrC,EAAe,KAAKoC,CAAI,EAG1BG,EAAa,CACf,CAEA,SAASA,GAAe,CACtB,GAAItC,GAAqBD,EAAe,SAAW,EAAG,OACtDC,EAAoB,GAGpB,IAAMmC,EAAOpC,EAAe,MAAM,EAElC,QAAQ,QAAQoC,EAAK,QAAQ,CAAC,EAC3B,MAAMf,GAAK,QAAQ,MAAM,kCAAmCe,EAAK,KAAMf,CAAC,CAAC,EACzE,QAAQ,IAAM,CACbpB,EAAoB,GAEpB,eAAesC,CAAY,CAC7B,CAAC,CACL,CAGO,SAASC,EAAoBpC,EAAMqC,EAAc,IAAK,CAC3D,QAAWL,KAAQpC,EACjB,GAAIoC,EAAK,OAAShC,EAAM,CACtBgC,EAAK,SAAWK,EAEhBzC,EAAe,KAAK,CAAC0C,EAAGC,IAAMA,EAAE,SAAWD,EAAE,QAAQ,EACrD,KACF,CAEJ,CAIO,SAASE,GAAiB,CAE/B,IAAMC,EAAc,SAAS,cAAc,4BAA4B,EACnEA,GACF3B,EAAoB2B,EAAY,WAAW,EAG7C,IAAMC,EAAU,SAAS,iBAAiB,eAAe,EAEzD,QAAWC,KAAMD,EAAS,CACxB,IAAM1C,EAAO2C,EAAG,QAAQ,OAClBnB,EAAOmB,EAAG,QAAQ,YAAc,OAChCrB,EAAQ,KAAK,MAAMqB,EAAG,QAAQ,aAAe,IAAI,EACjDlB,EAAW,SAASkB,EAAG,QAAQ,gBAAkB,IAAK,EAAE,EACxDjB,EAAS,KAAK,MAAMiB,EAAG,QAAQ,cAAgB,IAAI,EACnDhB,EAAQjC,EAAe,IAAIM,CAAI,EAErC,GAAI,CAAC2B,EAAO,CACV,QAAQ,KAAK,kBAAkB3B,CAAI,kBAAkB,EACrD,QACF,CAGIL,EAAgB,IAAIgD,CAAE,GAE1BC,EAAkBD,EAAIhB,EAAOL,EAAOE,EAAMC,EAAUzB,EAAM0B,CAAM,CAClE,CACF,CAEA,SAASkB,EAAkBD,EAAIhB,EAAOL,EAAOE,EAAMC,EAAUzB,EAAM0B,EAAQ,CACzE,IAAMmB,EAAU,SAAY,CAC1B,GAAIlD,EAAgB,IAAIgD,CAAE,EAAG,OAC7BhD,EAAgB,IAAIgD,CAAE,EAEtB,IAAMG,EAAM,MAAMnB,EAAM,OAAO,EACzBoB,EAAYD,EAAI,SAAWA,EAG3BE,EAAa,CAAC,EACpB,QAAWC,KAAavB,EACtBsB,EAAWC,CAAS,EAAItC,EAAesC,CAAS,EAIlD,IAAMC,EAAQH,EAAU,CAAE,GAAGzB,EAAO,GAAG0B,CAAW,CAAC,EAC/CL,EAAG,WAAW,OAAS,EACzBE,EAAQK,EAAOP,CAAE,EAEjBpD,EAAM2D,EAAOP,CAAE,EAIjBA,EAAG,gBAAgB,aAAa,EAChCA,EAAG,gBAAgB,kBAAkB,EACrCA,EAAG,gBAAgB,mBAAmB,EACtCA,EAAG,gBAAgB,sBAAsB,EACzCA,EAAG,gBAAgB,oBAAoB,EAGvCA,EAAG,cAAc,IAAI,YAAY,kBAAmB,CAClD,QAAS,GACT,OAAQ,CAAE,KAAA3C,EAAM,KAAAwB,CAAK,CACvB,CAAC,CAAC,CACJ,EAEA,OAAQA,EAAM,CACZ,IAAK,OAEHO,EAAiB,CAAE,KAAA/B,EAAM,SAAUyB,EAAW,IAAM,QAAAoB,CAAQ,CAAC,EAC7D,MAEF,IAAK,OACC,wBAAyB,OAC3B,oBAAoB,IAAM,CACxBd,EAAiB,CAAE,KAAA/B,EAAM,SAAAyB,EAAU,QAAAoB,CAAQ,CAAC,CAC9C,CAAC,EAED,WAAW,IAAM,CACfd,EAAiB,CAAE,KAAA/B,EAAM,SAAAyB,EAAU,QAAAoB,CAAQ,CAAC,CAC9C,EAAG,GAAG,EAER,MAEF,IAAK,UAAW,CACG,IAAI,qBAAqB,CAACM,EAASC,IAAQ,CAC1D,QAAWzB,KAASwB,EAClB,GAAIxB,EAAM,eAAgB,CACxByB,EAAI,WAAW,EACfrB,EAAiB,CAAE,KAAA/B,EAAM,SAAAyB,EAAU,QAAAoB,CAAQ,CAAC,EAC5C,KACF,CAEJ,EAAG,CAAE,WAAY,OAAQ,CAAC,EACjB,QAAQF,CAAE,EACnB,KACF,CAEA,IAAK,QAAS,CACZ,IAAMU,EAAK,OAAO,WAAW1B,EAAM,OAAS,oBAAoB,EAC5D0B,EAAG,QACLtB,EAAiB,CAAE,KAAA/B,EAAM,SAAAyB,EAAU,QAAAoB,CAAQ,CAAC,EAE5CQ,EAAG,iBAAiB,SAAWpC,GAAM,CAC/BA,EAAE,SACJc,EAAiB,CAAE,KAAA/B,EAAM,SAAAyB,EAAU,QAAAoB,CAAQ,CAAC,CAEhD,EAAG,CAAE,KAAM,EAAK,CAAC,EAEnB,KACF,CAEA,IAAK,SAAU,CACb,IAAMS,EAAS,CAAC,QAAS,QAAS,YAAa,YAAY,EACrDC,EAAU,IAAM,CACpBD,EAAO,QAAQrC,GAAK0B,EAAG,oBAAoB1B,EAAGsC,CAAO,CAAC,EAEtDxB,EAAiB,CAAE,KAAA/B,EAAM,SAAUyB,EAAW,IAAK,QAAAoB,CAAQ,CAAC,CAC9D,EACAS,EAAO,QAAQrC,GAAK0B,EAAG,iBAAiB1B,EAAGsC,EAAS,CAAE,KAAM,GAAM,QAAS,EAAK,CAAC,CAAC,EAClF,KACF,CAEA,IAAK,SAEH,MAEF,QACExB,EAAiB,CAAE,KAAA/B,EAAM,SAAAyB,EAAU,QAAAoB,CAAQ,CAAC,CAChD,CACF,CAKO,SAASW,EAAYC,EAAU,CACpC,OAAW,CAACzD,EAAM0D,CAAM,IAAK,OAAO,QAAQD,CAAQ,EAClDvC,EAAOlB,EAAM0D,EAAO,QAAUA,EAAQ,CACpC,KAAMA,EAAO,MAAQ,OACrB,MAAOA,EAAO,MACd,SAAUA,EAAO,UAAY,EAC7B,OAAQA,EAAO,QAAU,CAAC,CAC5B,CAAC,EAGC,OAAO,SAAa,MAClB,SAAS,aAAe,UAC1B,SAAS,iBAAiB,mBAAoBlB,CAAc,EAE5DA,EAAe,EAGrB,CAKO,SAASmB,EAAQC,EAAUL,EAAS,CACzC,GAAI,OAAO,SAAa,IAAa,OAErC,IAAMM,EAAW,SAAS,iBAAiBD,CAAQ,EACnD,QAAWjB,KAAMkB,EACXlB,EAAG,QAAQ,WACfA,EAAG,QAAQ,SAAW,OACtBY,EAAQZ,CAAE,EAEd,CAGO,SAASmB,EAAaF,EAAW,qBAAsB,CAC5DD,EAAQC,EAAWG,GAAS,CAC1BA,EAAK,iBAAiB,SAAU,MAAO9C,GAAM,CAC3CA,EAAE,eAAe,EAEjB,IAAM+C,EAAW,IAAI,SAASD,CAAI,EAC5BE,EAASF,EAAK,OAAO,YAAY,GAAK,OACtCG,EAASH,EAAK,QAAU,SAAS,KAEvC,GAAI,CAEF,IAAMI,EAAW,SAAS,cAAc,yBAAyB,GAChD,SAAS,cAAc,8BAA8B,EAChEC,EAAYD,EAAWA,EAAS,aAAa,SAAS,EAAI,KAG1DE,EAASN,EAAK,aAAa,cAAc,IAAM,OACrD,GAAI,CAACK,GAAa,CAACC,EAAQ,CACzB,QAAQ,KACN,4IAEF,EACAN,EAAK,cAAc,IAAI,YAAY,aAAc,CAC/C,QAAS,GACT,OAAQ,CAAE,MAAO,IAAI,MAAM,oBAAoB,CAAE,CACnD,CAAC,CAAC,EACF,MACF,CAEA,IAAMO,EAAU,CACd,mBAAoB,gBACtB,EACIF,IACFE,EAAQ,cAAc,EAAIF,GAG5B,IAAMG,EAAW,MAAM,MAAML,EAAQ,CACnC,OAAAD,EACA,KAAMA,IAAW,MAAQ,OAAYD,EACrC,QAAAM,CACF,CAAC,EAEDP,EAAK,cAAc,IAAI,YAAY,gBAAiB,CAClD,QAAS,GACT,OAAQ,CAAE,SAAAQ,EAAU,GAAIA,EAAS,EAAG,CACtC,CAAC,CAAC,CACJ,OAASC,EAAO,CACdT,EAAK,cAAc,IAAI,YAAY,aAAc,CAC/C,QAAS,GACT,OAAQ,CAAE,MAAAS,CAAM,CAClB,CAAC,CAAC,CACJ,CACF,CAAC,CACH,CAAC,CACH,CAIO,SAASC,GAAkB,CAQhC,MAPe,CACb,WAAY,CAAC,GAAG/E,EAAe,KAAK,CAAC,EACrC,SAAUC,EAAgB,KAC1B,QAASC,EAAe,OACxB,MAAOA,EAAe,IAAI8E,IAAM,CAAE,KAAMA,EAAE,KAAM,SAAUA,EAAE,QAAS,EAAE,EACvE,OAAQ,CAAC,GAAG5E,EAAa,KAAK,CAAC,CACjC,CAEF",
6
- "names": ["mount", "signal", "batch", "islandRegistry", "hydratedIslands", "hydrationQueue", "isProcessingQueue", "sharedStores", "createIslandStore", "name", "initialState", "store", "signals", "key", "value", "val", "fn", "snapshot", "sig", "data", "useIslandStore", "fallbackInitial", "serializeIslandStores", "hydrateIslandStores", "serialized", "storeData", "e", "island", "loader", "opts", "Island", "props", "children", "mode", "priority", "stores", "entry", "resolvedMode", "resolvedPriority", "resolvedStores", "enqueueHydration", "task", "inserted", "i", "processQueue", "boostIslandPriority", "newPriority", "a", "b", "hydrateIslands", "storeScript", "islands", "el", "scheduleHydration", "hydrate", "mod", "Component", "storeProps", "storeName", "vnode", "entries", "obs", "mq", "events", "handler", "autoIslands", "registry", "config", "enhance", "selector", "elements", "enhanceForms", "form", "formData", "method", "action", "csrfMeta", "csrfToken", "noCsrf", "headers", "response", "error", "getIslandStatus", "t"]
3
+ "sources": ["../src/islands.js", "../src/serialize.js"],
4
+ "sourcesContent": ["// What Framework - Islands Architecture\n// Each interactive piece of the page is an \"island\" \u2014 a self-contained\n// component that hydrates independently. The rest is static HTML.\n//\n// Features:\n// - Multiple hydration modes (load, idle, visible, action, media, static)\n// - Shared state across islands\n// - Priority-based hydration queue\n// - Progressive enhancement\n//\n// Modes:\n// 'static' - No JS shipped. Pure HTML. (nav, footer, etc.)\n// 'idle' - Hydrate when browser is idle (requestIdleCallback)\n// 'visible' - Hydrate when scrolled into view (IntersectionObserver)\n// 'load' - Hydrate immediately on page load\n// 'media' - Hydrate when media query matches (e.g., mobile-only)\n// 'action' - Hydrate on first user interaction (click, focus, hover)\n\nimport { mount, hydrate, signal, batch } from 'what-core';\nimport { serializeState } from './serialize.js';\n\nconst islandRegistry = new Map();\nconst hydratedIslands = new Set();\nconst hydrationQueue = [];\nlet isProcessingQueue = false;\n\n// --- Shared Island State ---\n// Global reactive store that persists across islands and page navigations\n\nconst sharedStores = new Map();\n\nexport function createIslandStore(name, initialState) {\n if (sharedStores.has(name)) {\n return sharedStores.get(name);\n }\n\n const store = {};\n const signals = {};\n\n // Create signals for each key in initial state\n for (const [key, value] of Object.entries(initialState)) {\n signals[key] = signal(value);\n Object.defineProperty(store, key, {\n get: () => signals[key](),\n set: (val) => signals[key].set(val),\n enumerable: true,\n });\n }\n\n // Methods to interact with store\n store._signals = signals;\n store._subscribe = (key, fn) => {\n if (signals[key]) {\n return signals[key].subscribe(fn);\n }\n };\n store._batch = (fn) => batch(fn);\n store._getSnapshot = () => {\n const snapshot = {};\n for (const [key, sig] of Object.entries(signals)) {\n snapshot[key] = sig.peek();\n }\n return snapshot;\n };\n store._hydrate = (data) => {\n batch(() => {\n for (const [key, value] of Object.entries(data)) {\n if (signals[key]) {\n signals[key].set(value);\n }\n }\n });\n };\n\n sharedStores.set(name, store);\n return store;\n}\n\n// Get or create a shared store\nexport function useIslandStore(name, fallbackInitial = {}) {\n if (sharedStores.has(name)) {\n return sharedStores.get(name);\n }\n return createIslandStore(name, fallbackInitial);\n}\n\n// Serialize all shared stores for SSR.\n// Uses serializeState (not bare JSON.stringify) so user-controlled store values\n// containing \"</script>\" cannot break out of the <script> tag this is embedded\n// in. (AUDIT-2026-06-06 H3)\nexport function serializeIslandStores() {\n return serializeState(getIslandStoresSnapshot());\n}\n\n// Raw (unserialized) snapshot of all shared island stores, so renderDocument can\n// merge it into the single consolidated #__what_data payload (one serialize pass).\nexport function getIslandStoresSnapshot() {\n const data = {};\n for (const [name, store] of sharedStores) {\n data[name] = store._getSnapshot();\n }\n return data;\n}\n\n// Hydrate shared stores from SSR data\nexport function hydrateIslandStores(serialized) {\n try {\n const data = typeof serialized === 'string' ? JSON.parse(serialized) : serialized;\n for (const [name, storeData] of Object.entries(data)) {\n const store = useIslandStore(name, storeData);\n store._hydrate(storeData);\n }\n } catch (e) {\n console.warn('[what] Failed to hydrate island stores:', e);\n }\n}\n\n// --- Register an island component ---\n\nexport function island(name, loader, opts = {}) {\n islandRegistry.set(name, {\n loader, // () => import('./MyComponent.js')\n mode: opts.mode || 'idle',\n media: opts.media || null,\n priority: opts.priority || 0, // Higher = hydrate first\n stores: opts.stores || [], // Shared stores this island uses\n });\n}\n\n// --- Island wrapper for SSR ---\n// Renders the static HTML with a marker the client can find.\n\nexport function Island({ name, props = {}, children, mode, priority, stores }) {\n const entry = islandRegistry.get(name);\n const resolvedMode = mode || entry?.mode || 'idle';\n const resolvedPriority = priority ?? entry?.priority ?? 0;\n const resolvedStores = stores || entry?.stores || [];\n\n // Server: render as a div with data attributes for hydration\n return {\n tag: 'div',\n props: {\n 'data-island': name,\n 'data-island-mode': resolvedMode,\n 'data-island-props': JSON.stringify(props),\n 'data-island-priority': resolvedPriority,\n 'data-island-stores': JSON.stringify(resolvedStores),\n },\n children: children || [],\n key: null,\n _vnode: true,\n };\n}\n\n// --- Priority Hydration Queue ---\n\nfunction enqueueHydration(task) {\n // Insert in priority order (higher priority first)\n let inserted = false;\n for (let i = 0; i < hydrationQueue.length; i++) {\n if (task.priority > hydrationQueue[i].priority) {\n hydrationQueue.splice(i, 0, task);\n inserted = true;\n break;\n }\n }\n if (!inserted) {\n hydrationQueue.push(task);\n }\n\n processQueue();\n}\n\nfunction processQueue() {\n if (isProcessingQueue || hydrationQueue.length === 0) return;\n isProcessingQueue = true;\n\n // Process one task at a time to avoid blocking\n const task = hydrationQueue.shift();\n\n Promise.resolve(task.hydrate())\n .catch(e => console.error('[what] Island hydration failed:', task.name, e))\n .finally(() => {\n isProcessingQueue = false;\n // Continue processing after a microtask\n queueMicrotask(processQueue);\n });\n}\n\n// Boost priority for an island (e.g., on user interaction)\nexport function boostIslandPriority(name, newPriority = 100) {\n for (const task of hydrationQueue) {\n if (task.name === name) {\n task.priority = newPriority;\n // Re-sort queue\n hydrationQueue.sort((a, b) => b.priority - a.priority);\n break;\n }\n }\n}\n\n// --- Client-side hydration ---\n\nexport function hydrateIslands() {\n // First, hydrate any shared stores from the page\n const storeScript = document.querySelector('script[data-island-stores]');\n if (storeScript) {\n hydrateIslandStores(storeScript.textContent);\n }\n\n const islands = document.querySelectorAll('[data-island]');\n\n for (const el of islands) {\n const name = el.dataset.island;\n const mode = el.dataset.islandMode || 'idle';\n const props = JSON.parse(el.dataset.islandProps || '{}');\n const priority = parseInt(el.dataset.islandPriority || '0', 10);\n const stores = JSON.parse(el.dataset.islandStores || '[]');\n const entry = islandRegistry.get(name);\n\n if (!entry) {\n console.warn(`[what] Island \"${name}\" not registered`);\n continue;\n }\n\n // Skip if already hydrated\n if (hydratedIslands.has(el)) continue;\n\n scheduleHydration(el, entry, props, mode, priority, name, stores);\n }\n}\n\nfunction scheduleHydration(el, entry, props, mode, priority, name, stores) {\n const hydrate = async () => {\n if (hydratedIslands.has(el)) return;\n hydratedIslands.add(el);\n\n const mod = await entry.loader();\n const Component = mod.default || mod;\n\n // Inject shared stores into props\n const storeProps = {};\n for (const storeName of stores) {\n storeProps[storeName] = useIslandStore(storeName);\n }\n\n // Use hydrate() to reuse server-rendered DOM instead of destroying/recreating\n const vnode = Component({ ...props, ...storeProps });\n if (el.childNodes.length > 0) {\n hydrate(vnode, el);\n } else {\n mount(vnode, el);\n }\n\n // Clean up data attributes\n el.removeAttribute('data-island');\n el.removeAttribute('data-island-mode');\n el.removeAttribute('data-island-props');\n el.removeAttribute('data-island-priority');\n el.removeAttribute('data-island-stores');\n\n // Dispatch event for analytics/debugging\n el.dispatchEvent(new CustomEvent('island:hydrated', {\n bubbles: true,\n detail: { name, mode },\n }));\n };\n\n switch (mode) {\n case 'load':\n // Immediate hydration via queue (respects priority)\n enqueueHydration({ name, priority: priority + 1000, hydrate });\n break;\n\n case 'idle':\n if ('requestIdleCallback' in window) {\n requestIdleCallback(() => {\n enqueueHydration({ name, priority, hydrate });\n });\n } else {\n setTimeout(() => {\n enqueueHydration({ name, priority, hydrate });\n }, 200);\n }\n break;\n\n case 'visible': {\n const observer = new IntersectionObserver((entries, obs) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n obs.disconnect();\n enqueueHydration({ name, priority, hydrate });\n break;\n }\n }\n }, { rootMargin: '200px' });\n observer.observe(el);\n break;\n }\n\n case 'media': {\n const mq = window.matchMedia(entry.media || '(max-width: 768px)');\n if (mq.matches) {\n enqueueHydration({ name, priority, hydrate });\n } else {\n mq.addEventListener('change', (e) => {\n if (e.matches) {\n enqueueHydration({ name, priority, hydrate });\n }\n }, { once: true });\n }\n break;\n }\n\n case 'action': {\n const events = ['click', 'focus', 'mouseover', 'touchstart'];\n const handler = () => {\n events.forEach(e => el.removeEventListener(e, handler));\n // Boost priority since user interacted\n enqueueHydration({ name, priority: priority + 500, hydrate });\n };\n events.forEach(e => el.addEventListener(e, handler, { once: true, passive: true }));\n break;\n }\n\n case 'static':\n // Never hydrate\n break;\n\n default:\n enqueueHydration({ name, priority, hydrate });\n }\n}\n\n// --- Auto-discover islands from data attributes ---\n// Call this once on the client to set up all islands.\n\nexport function autoIslands(registry) {\n for (const [name, config] of Object.entries(registry)) {\n island(name, config.loader || config, {\n mode: config.mode || 'idle',\n media: config.media,\n priority: config.priority || 0,\n stores: config.stores || [],\n });\n }\n\n if (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', hydrateIslands);\n } else {\n hydrateIslands();\n }\n }\n}\n\n// --- Progressive Enhancement Helpers ---\n\n// Mark an element as progressively enhanced\nexport function enhance(selector, handler) {\n if (typeof document === 'undefined') return;\n\n const elements = document.querySelectorAll(selector);\n for (const el of elements) {\n if (el.dataset.enhanced) continue;\n el.dataset.enhanced = 'true';\n handler(el);\n }\n}\n\n// Form enhancement: submit via fetch instead of page reload\nexport function enhanceForms(selector = 'form[data-enhance]') {\n enhance(selector, (form) => {\n form.addEventListener('submit', async (e) => {\n e.preventDefault();\n\n const formData = new FormData(form);\n const method = form.method.toUpperCase() || 'POST';\n const action = form.action || location.href;\n\n try {\n // Read CSRF token from meta tag\n const csrfMeta = document.querySelector('meta[name=\"csrf-token\"]') ||\n document.querySelector('meta[name=\"what-csrf-token\"]');\n const csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : null;\n\n // If no CSRF token and form hasn't opted out, block submission\n const noCsrf = form.getAttribute('data-no-csrf') === 'true';\n if (!csrfToken && !noCsrf) {\n console.warn(\n '[what] Form submission blocked: no CSRF token found. ' +\n 'Add a <meta name=\"csrf-token\"> tag or set data-no-csrf=\"true\" on the form to opt out.'\n );\n form.dispatchEvent(new CustomEvent('form:error', {\n bubbles: true,\n detail: { error: new Error('Missing CSRF token') },\n }));\n return;\n }\n\n const headers = {\n 'X-Requested-With': 'XMLHttpRequest',\n };\n if (csrfToken) {\n headers['X-CSRF-Token'] = csrfToken;\n }\n\n const response = await fetch(action, {\n method,\n body: method === 'GET' ? undefined : formData,\n headers,\n });\n\n form.dispatchEvent(new CustomEvent('form:response', {\n bubbles: true,\n detail: { response, ok: response.ok },\n }));\n } catch (error) {\n form.dispatchEvent(new CustomEvent('form:error', {\n bubbles: true,\n detail: { error },\n }));\n }\n });\n });\n}\n\n// --- Debugging ---\n\nexport function getIslandStatus() {\n const status = {\n registered: [...islandRegistry.keys()],\n hydrated: hydratedIslands.size,\n pending: hydrationQueue.length,\n queue: hydrationQueue.map(t => ({ name: t.name, priority: t.priority })),\n stores: [...sharedStores.keys()],\n };\n return status;\n}\n", "// Safe serialization of state for inlining into an HTML <script> tag.\n//\n// Stateless on purpose: this module holds no shared state, so it is safe for\n// the bundler to inline into multiple server entry points without creating\n// divergent instances (unlike islands.js's sharedStores).\n//\n// JSON.stringify alone is NOT safe to drop inside <script>...</script>: a value\n// containing \"</script>\" (or \"<!--\", \"<script\") breaks out of the element and\n// injects markup -- stored XSS when any value is user-controlled\n// (AUDIT-2026-06-06 H3). Escaping \"<\", \">\", \"&\" as \\uXXXX keeps the output\n// valid JSON (so JSON.parse on hydrate still works) while making it inert in\n// HTML. U+2028/U+2029 are also escaped: they are valid in JSON strings but are\n// illegal in JS string literals and can break inline script parsing.\n\n// Built via new RegExp from escape sequences so this source file contains no\n// invisible separator characters. Matches: < > & U+2028 U+2029.\nconst SCRIPT_UNSAFE = new RegExp('[<>&\\\\u2028\\\\u2029]', 'g');\n\nconst ESCAPES = {\n 0x3c: '\\\\u003c', // <\n 0x3e: '\\\\u003e', // >\n 0x26: '\\\\u0026', // &\n 0x2028: '\\\\u2028',\n 0x2029: '\\\\u2029',\n};\n\n/**\n * Serialize a value to a JSON string that is safe to embed verbatim inside an\n * HTML <script> element. Always use this instead of bare JSON.stringify when\n * inlining hydration/state payloads into server-rendered HTML.\n */\nexport function serializeState(value) {\n return JSON.stringify(value).replace(SCRIPT_UNSAFE, (c) => ESCAPES[c.charCodeAt(0)]);\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["mount", "signal", "batch", "SCRIPT_UNSAFE", "ESCAPES", "serializeState", "value", "c", "islandRegistry", "hydratedIslands", "hydrationQueue", "isProcessingQueue", "sharedStores", "createIslandStore", "name", "initialState", "store", "signals", "key", "value", "signal", "val", "fn", "batch", "snapshot", "sig", "data", "useIslandStore", "fallbackInitial", "serializeIslandStores", "serializeState", "getIslandStoresSnapshot", "hydrateIslandStores", "serialized", "storeData", "e", "island", "loader", "opts", "Island", "props", "children", "mode", "priority", "stores", "entry", "resolvedMode", "resolvedPriority", "resolvedStores", "enqueueHydration", "task", "inserted", "i", "processQueue", "boostIslandPriority", "newPriority", "a", "b", "hydrateIslands", "storeScript", "islands", "el", "scheduleHydration", "hydrate", "mod", "Component", "storeProps", "storeName", "vnode", "mount", "entries", "obs", "mq", "events", "handler", "autoIslands", "registry", "config", "enhance", "selector", "elements", "enhanceForms", "form", "formData", "method", "action", "csrfMeta", "csrfToken", "noCsrf", "headers", "response", "error", "getIslandStatus", "t"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-server",
3
- "version": "0.8.4",
3
+ "version": "0.11.0",
4
4
  "description": "What Framework - SSR, islands architecture, static generation",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -36,7 +36,13 @@
36
36
  "author": "ZVN DEV (https://zvndev.com)",
37
37
  "license": "MIT",
38
38
  "peerDependencies": {
39
- "what-core": "^0.8.4"
39
+ "what-core": "^0.11.0",
40
+ "what-router": "^0.11.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "what-router": {
44
+ "optional": true
45
+ }
40
46
  },
41
47
  "repository": {
42
48
  "type": "git",
@@ -0,0 +1,331 @@
1
+ // What Framework - Served Server Actions
2
+ //
3
+ // actions.js defines actions and `handleActionRequest`, but nothing wires the
4
+ // `/__what_action` HTTP route the client posts to. This module provides that
5
+ // missing piece: a framework-agnostic core handler plus thin Node-middleware
6
+ // and Web-Fetch adapters. CSRF + dispatch + error masking are reused from
7
+ // actions.js (handleActionRequest) — no security logic is duplicated here.
8
+
9
+ import { handleActionRequest } from './actions.js';
10
+
11
+ const DEFAULT_BASE_PATH = '/__what_action';
12
+ const MAX_BODY_BYTES = 1024 * 1024; // 1 MB
13
+
14
+ function lowerHeaders(headers) {
15
+ if (!headers) return {};
16
+ // Headers (fetch) -> object
17
+ if (typeof headers.forEach === 'function' && typeof headers.get === 'function') {
18
+ const out = {};
19
+ headers.forEach((v, k) => { out[k.toLowerCase()] = v; });
20
+ return out;
21
+ }
22
+ const out = {};
23
+ for (const k in headers) out[k.toLowerCase()] = headers[k];
24
+ return out;
25
+ }
26
+
27
+ function jsonResponse(status, bodyObj) {
28
+ return {
29
+ status,
30
+ headers: { 'content-type': 'application/json' },
31
+ body: JSON.stringify(bodyObj),
32
+ };
33
+ }
34
+
35
+ function htmlResponse(status, message) {
36
+ return {
37
+ status,
38
+ headers: { 'content-type': 'text/html; charset=utf-8' },
39
+ body: `<!DOCTYPE html><html><body><h1>${status}</h1><p>${String(message)
40
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</p></body></html>`,
41
+ };
42
+ }
43
+
44
+ // Resolve a safe local redirect target for the form (POST/redirect/GET) path.
45
+ // `_redirect` must be a same-origin local path ("/x"); protocol-relative
46
+ // ("//evil"), backslash-smuggled ("/\evil", "/\\evil") and absolute
47
+ // ("https://evil") targets are rejected. The Referer header (an absolute URL)
48
+ // is reduced to its path + query. Falls back to '/'.
49
+ //
50
+ // Backslashes matter: browsers and `new URL()` treat "\" like "/", so
51
+ // "/\evil.com" canonicalizes to http://evil.com (an open redirect). We reject
52
+ // anything starting with two slash-or-backslash chars or containing a
53
+ // backslash, then canonicalize via URL and require the localhost origin.
54
+ function safeLocalPath(value) {
55
+ if (typeof value !== 'string' || !value.startsWith('/')) return null;
56
+ // Reject protocol-relative / backslash-smuggled targets up front.
57
+ if (/^[/\\]{2}/.test(value) || value.includes('\\')) return null;
58
+ try {
59
+ const u = new URL(value, 'http://localhost');
60
+ if (u.origin !== 'http://localhost') return null;
61
+ return u.pathname + u.search;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function safeRedirectTarget(form, headers) {
68
+ const explicit = safeLocalPath(form && form._redirect);
69
+ if (explicit) return explicit;
70
+ const referer = headers.referer || headers.referrer;
71
+ if (referer) {
72
+ try {
73
+ const u = new URL(referer, 'http://localhost');
74
+ const path = safeLocalPath(u.pathname + u.search);
75
+ if (path) return path;
76
+ } catch { /* fall through */ }
77
+ }
78
+ return '/';
79
+ }
80
+
81
+ // Reserved form fields consumed by the framework (not passed to the action).
82
+ const RESERVED_FORM_FIELDS = new Set(['_action', 'data-action', '_csrf', '_redirect']);
83
+
84
+ /**
85
+ * Framework-agnostic action dispatcher.
86
+ *
87
+ * options:
88
+ * - getCsrfToken(reqLike) -> sessionToken (sync or async). Omit + skipCsrf for none.
89
+ * - skipCsrf: bool — opt out of CSRF (e.g. token-authed APIs).
90
+ * - basePath: string — defaults to '/__what_action' (used by the adapters).
91
+ *
92
+ * Returns: async (reqLike) -> { status, headers, body:string }
93
+ * reqLike: { method, headers, body, query? }
94
+ *
95
+ * Two request shapes are accepted:
96
+ *
97
+ * 1. JSON + header (fetch clients — what the `action()` client wrapper sends):
98
+ * POST with `X-What-Action: <id>` header, JSON body `{ args: [...] }`,
99
+ * CSRF token in the `X-CSRF-Token` header. Responds with JSON.
100
+ *
101
+ * 2. Plain HTML form post (progressive enhancement — works without JS):
102
+ * POST with `Content-Type: application/x-www-form-urlencoded` and NO
103
+ * X-What-Action header. `body` is the parsed form fields object.
104
+ * - action id: `_action` (or `data-action`) hidden field, or `?action=`
105
+ * query param (reqLike.query.action)
106
+ * - CSRF token: `_csrf` hidden field
107
+ * - redirect: `_redirect` hidden field (local path), else Referer, else '/'
108
+ * The action receives ONE argument: the form fields object (reserved
109
+ * fields stripped). Success responds 303 See Other (POST/redirect/GET);
110
+ * failures respond with an HTML error page and the matching status.
111
+ */
112
+ export function createActionHandler(options = {}) {
113
+ const { getCsrfToken, skipCsrf = false } = options;
114
+
115
+ return async function handle(reqLike) {
116
+ const method = (reqLike.method || 'POST').toUpperCase();
117
+ if (method !== 'POST') {
118
+ return jsonResponse(405, { message: 'Method Not Allowed' });
119
+ }
120
+
121
+ const headers = lowerHeaders(reqLike.headers);
122
+ const headerActionId = headers['x-what-action'];
123
+ const contentType = headers['content-type'] || '';
124
+ const isFormPost = !headerActionId && contentType.includes('application/x-www-form-urlencoded');
125
+
126
+ const sessionCsrfToken = skipCsrf
127
+ ? undefined
128
+ : (getCsrfToken ? await getCsrfToken(reqLike) : undefined);
129
+
130
+ // --- Plain HTML form post (progressive enhancement) ---
131
+ if (isFormPost) {
132
+ const form = reqLike.body || {};
133
+ const actionId = form._action || form['data-action'] || (reqLike.query && reqLike.query.action);
134
+ if (!actionId) {
135
+ return htmlResponse(400, 'Missing action name (add a hidden "_action" field or ?action= query param)');
136
+ }
137
+
138
+ // CSRF token travels in the `_csrf` form field for plain forms; map it
139
+ // to the header slot handleActionRequest validates against.
140
+ const formHeaders = { ...headers };
141
+ if (form._csrf && !formHeaders['x-csrf-token']) formHeaders['x-csrf-token'] = String(form._csrf);
142
+
143
+ if (!skipCsrf && getCsrfToken && !sessionCsrfToken) {
144
+ // CSRF is configured but this client has no token (e.g. no cookie yet).
145
+ return htmlResponse(403, 'Missing CSRF token');
146
+ }
147
+
148
+ const data = {};
149
+ for (const [k, v] of Object.entries(form)) {
150
+ if (!RESERVED_FORM_FIELDS.has(k)) data[k] = v;
151
+ }
152
+
153
+ const result = await handleActionRequest(
154
+ { headers: formHeaders },
155
+ actionId,
156
+ [data],
157
+ { csrfToken: sessionCsrfToken, skipCsrf }
158
+ );
159
+
160
+ if (result.status === 200) {
161
+ return {
162
+ status: 303,
163
+ headers: { location: safeRedirectTarget(form, headers) },
164
+ body: '',
165
+ };
166
+ }
167
+ return htmlResponse(result.status, (result.body && result.body.message) || 'Action failed');
168
+ }
169
+
170
+ // --- JSON + X-What-Action header (fetch clients) ---
171
+ if (!headerActionId) {
172
+ return jsonResponse(400, { message: 'Missing X-What-Action header' });
173
+ }
174
+
175
+ if (!skipCsrf && getCsrfToken && !sessionCsrfToken) {
176
+ // CSRF configured, but the client presented no session token (no cookie).
177
+ return jsonResponse(403, { message: 'Missing CSRF token' });
178
+ }
179
+
180
+ const body = reqLike.body || {};
181
+ const args = body.args;
182
+
183
+ const result = await handleActionRequest(
184
+ { headers },
185
+ headerActionId,
186
+ args,
187
+ { csrfToken: sessionCsrfToken, skipCsrf }
188
+ );
189
+
190
+ return jsonResponse(result.status, result.body);
191
+ };
192
+ }
193
+
194
+ // --- Node connect/express middleware ---
195
+ // Mount before your routes: app.use(nodeActionMiddleware({ getCsrfToken }))
196
+
197
+ export function nodeActionMiddleware(options = {}) {
198
+ const basePath = options.basePath || DEFAULT_BASE_PATH;
199
+ const handle = createActionHandler(options);
200
+
201
+ return async function middleware(req, res, next) {
202
+ const [url, search] = (req.url || '').split('?');
203
+ if (url !== basePath || (req.method || '').toUpperCase() !== 'POST') {
204
+ return next ? next() : undefined;
205
+ }
206
+
207
+ let body;
208
+ try {
209
+ const raw = await readRawBody(req);
210
+ body = parseActionBody(raw, req.headers['content-type'] || '');
211
+ } catch (err) {
212
+ res.writeHead(err.code === 'BODY_TOO_LARGE' ? 413 : 400, { 'content-type': 'application/json' });
213
+ res.end(JSON.stringify({ message: err.code === 'BODY_TOO_LARGE' ? 'Payload too large' : 'Invalid request body' }));
214
+ return;
215
+ }
216
+
217
+ const query = Object.fromEntries(new URLSearchParams(search || ''));
218
+ const out = await handle({ method: req.method, headers: req.headers, body, query });
219
+ res.writeHead(out.status, out.headers);
220
+ res.end(out.body);
221
+ };
222
+ }
223
+
224
+ /** Parse an action request body by content type: form-urlencoded -> fields object, else JSON. */
225
+ export function parseActionBody(raw, contentType) {
226
+ if ((contentType || '').includes('application/x-www-form-urlencoded')) {
227
+ const fields = {};
228
+ for (const [k, v] of new URLSearchParams(String(raw))) {
229
+ if (fields[k] === undefined) fields[k] = v;
230
+ else if (Array.isArray(fields[k])) fields[k].push(v);
231
+ else fields[k] = [fields[k], v];
232
+ }
233
+ return fields;
234
+ }
235
+ if (raw == null || raw === '') return {};
236
+ return JSON.parse(String(raw));
237
+ }
238
+
239
+ /**
240
+ * Read a Web Fetch `Request` body as text with the same MAX_BODY_BYTES cap the
241
+ * Node middleware enforces. Used by the adapter/edge entry points (Vercel /
242
+ * Cloudflare / Node-adapter) so all three share one DoS guard.
243
+ *
244
+ * Returns { raw } on success or { tooLarge: true } when the cap is exceeded —
245
+ * checked first via Content-Length, then enforced while streaming (chunked /
246
+ * spoofed Content-Length can't bypass it).
247
+ *
248
+ * @param {Request} request
249
+ * @param {number} [limit=MAX_BODY_BYTES]
250
+ */
251
+ export async function readFetchBodyCapped(request, limit = MAX_BODY_BYTES) {
252
+ const declared = Number(request.headers.get('content-length'));
253
+ if (Number.isFinite(declared) && declared > limit) {
254
+ return { tooLarge: true };
255
+ }
256
+ const body = request.body;
257
+ // No stream available (or env without ReadableStream): fall back to text()
258
+ // but still re-check the resulting size against the cap.
259
+ if (!body || typeof body.getReader !== 'function') {
260
+ const raw = await request.text();
261
+ if (Buffer.byteLength(raw, 'utf8') > limit) return { tooLarge: true };
262
+ return { raw };
263
+ }
264
+ const reader = body.getReader();
265
+ const chunks = [];
266
+ let size = 0;
267
+ while (true) {
268
+ const { done, value } = await reader.read();
269
+ if (done) break;
270
+ if (value) {
271
+ size += value.byteLength;
272
+ if (size > limit) {
273
+ try { await reader.cancel(); } catch { /* ignore */ }
274
+ return { tooLarge: true };
275
+ }
276
+ chunks.push(value);
277
+ }
278
+ }
279
+ return { raw: Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8') };
280
+ }
281
+
282
+ function readRawBody(req) {
283
+ return new Promise((resolve, reject) => {
284
+ let size = 0;
285
+ const chunks = [];
286
+ req.on('data', (chunk) => {
287
+ size += chunk.length;
288
+ if (size > MAX_BODY_BYTES) {
289
+ const e = new Error('Body too large');
290
+ e.code = 'BODY_TOO_LARGE';
291
+ reject(e);
292
+ req.destroy?.();
293
+ return;
294
+ }
295
+ chunks.push(chunk);
296
+ });
297
+ req.on('end', () => {
298
+ if (chunks.length === 0) return resolve('');
299
+ resolve(Buffer.concat(chunks).toString('utf8'));
300
+ });
301
+ req.on('error', reject);
302
+ });
303
+ }
304
+
305
+ // --- Web Fetch handler (edge / Deno / Bun / Cloudflare) ---
306
+ // const handler = fetchActionHandler({ getCsrfToken }); addEventListener('fetch', e => e.respondWith(handler(e.request)))
307
+
308
+ export function fetchActionHandler(options = {}) {
309
+ const handle = createActionHandler(options);
310
+ return async function (request) {
311
+ let body = {};
312
+ try {
313
+ const read = await readFetchBodyCapped(request);
314
+ if (read.tooLarge) {
315
+ return new Response(JSON.stringify({ message: 'Payload too large' }), {
316
+ status: 413,
317
+ headers: { 'content-type': 'application/json' },
318
+ });
319
+ }
320
+ body = parseActionBody(read.raw, request.headers.get('content-type') || '');
321
+ } catch {
322
+ body = {};
323
+ }
324
+ let query = {};
325
+ try {
326
+ query = Object.fromEntries(new URL(request.url, 'http://localhost').searchParams);
327
+ } catch { /* no query */ }
328
+ const out = await handle({ method: request.method, headers: request.headers, body, query });
329
+ return new Response(out.body, { status: out.status, headers: out.headers });
330
+ };
331
+ }
package/src/actions.js CHANGED
@@ -14,6 +14,7 @@
14
14
  // const result = await saveUser({ name: 'John' });
15
15
 
16
16
  import { signal, batch } from 'what-core';
17
+ import { revalidatePath as serverRevalidatePath, revalidateTag as serverRevalidateTag } from './revalidation-registry.js';
17
18
 
18
19
  // Registry of server actions
19
20
  const actionRegistry = new Map();
@@ -402,7 +403,18 @@ export function handleActionRequest(req, actionId, args, options = {}) {
402
403
  }
403
404
 
404
405
  return action.fn(...args)
405
- .then(result => ({ status: 200, body: result }))
406
+ .then(async result => {
407
+ // Server-side cache revalidation: if the action declared revalidate paths
408
+ // or tags, purge them through the bound cache engine (no-op if unbound).
409
+ const opts = action.options || {};
410
+ if (Array.isArray(opts.revalidate)) {
411
+ for (const p of opts.revalidate) await serverRevalidatePath(p);
412
+ }
413
+ if (Array.isArray(opts.revalidateTags)) {
414
+ for (const t of opts.revalidateTags) await serverRevalidateTag(t);
415
+ }
416
+ return { status: 200, body: result };
417
+ })
406
418
  .catch(error => {
407
419
  // Log the full error server-side, return generic message to client
408
420
  console.error(`[what] Action "${actionId}" error:`, error);
@@ -0,0 +1,18 @@
1
+ // Cloudflare Workers adapter — exposes a `fetch(request, env, ctx)` worker
2
+ // entry over the same Web-Fetch core handler. ISR runs via the origin cache
3
+ // engine; pass a what-isr redis/KV-backed store for cross-isolate caching and
4
+ // use ctx.waitUntil for background regeneration.
5
+
6
+ import { createRequestHandler } from './core.js';
7
+
8
+ export function createCloudflareHandler(options = {}) {
9
+ const handle = createRequestHandler(options);
10
+ return {
11
+ async fetch(request, env, ctx) {
12
+ // Expose env/ctx to render via the request for adapters that need them.
13
+ if (env) request.__env = env;
14
+ if (ctx) request.__ctx = ctx;
15
+ return handle(request);
16
+ },
17
+ };
18
+ }