what-server 0.5.4 → 0.6.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.
@@ -0,0 +1,7 @@
1
+ {
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": "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",
6
+ "names": ["hydrate", "entry"]
7
+ }
@@ -0,0 +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};
2
+ //# sourceMappingURL=islands.min.js.map
@@ -0,0 +1,7 @@
1
+ {
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": "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",
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"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-server",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "What Framework - SSR, islands architecture, static generation",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -8,17 +8,21 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./index.d.ts",
11
+ "production": "./dist/index.min.js",
11
12
  "import": "./src/index.js"
12
13
  },
13
14
  "./islands": {
15
+ "production": "./dist/islands.min.js",
14
16
  "import": "./src/islands.js"
15
17
  },
16
18
  "./actions": {
19
+ "production": "./dist/actions.min.js",
17
20
  "import": "./src/actions.js"
18
21
  }
19
22
  },
20
23
  "files": [
21
24
  "src",
25
+ "dist",
22
26
  "index.d.ts"
23
27
  ],
24
28
  "sideEffects": false,
@@ -29,17 +33,17 @@
29
33
  "server-rendering",
30
34
  "what-framework"
31
35
  ],
32
- "author": "",
36
+ "author": "ZVN DEV (https://zvndev.com)",
33
37
  "license": "MIT",
34
38
  "peerDependencies": {
35
- "what-core": "^0.5.3"
39
+ "what-core": "^0.6.0"
36
40
  },
37
41
  "repository": {
38
42
  "type": "git",
39
- "url": "https://github.com/zvndev/what-fw"
43
+ "url": "https://github.com/CelsianJs/what-framework"
40
44
  },
41
45
  "bugs": {
42
- "url": "https://github.com/zvndev/what-fw/issues"
46
+ "url": "https://github.com/CelsianJs/what-framework/issues"
43
47
  },
44
48
  "homepage": "https://whatfw.com"
45
49
  }
package/src/actions.js CHANGED
@@ -17,7 +17,6 @@ import { signal, batch } from 'what-core';
17
17
 
18
18
  // Registry of server actions
19
19
  const actionRegistry = new Map();
20
- let actionIdCounter = 0;
21
20
 
22
21
  // --- CSRF Protection ---
23
22
  // Server generates a token per session; client sends it with every action request.
@@ -46,8 +45,14 @@ export function generateCsrfToken() {
46
45
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
47
46
  return crypto.randomUUID();
48
47
  }
49
- // Fallback for older Node
50
- return Math.random().toString(36).slice(2) + Date.now().toString(36);
48
+ // Fallback for environments without crypto.randomUUID — use crypto.getRandomValues
49
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
50
+ const arr = new Uint8Array(16);
51
+ crypto.getRandomValues(arr);
52
+ return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
53
+ }
54
+ // Last resort — should not be reached in modern environments
55
+ throw new Error('[what] No secure random source available for CSRF token generation');
51
56
  }
52
57
 
53
58
  // Server: validate CSRF token from request header against session token
@@ -170,11 +175,15 @@ export function formAction(actionFn, options = {}) {
170
175
  formData = formDataOrEvent;
171
176
  }
172
177
 
173
- // Convert FormData to plain object
178
+ // Convert FormData to plain object, preserving File instances
174
179
  const data = {};
180
+ let hasFiles = false;
175
181
  for (const [key, value] of formData.entries()) {
182
+ if (typeof File !== 'undefined' && value instanceof File) {
183
+ hasFiles = true;
184
+ }
176
185
  if (data[key]) {
177
- // Handle multiple values (e.g., checkboxes)
186
+ // Handle multiple values (e.g., checkboxes, multi-file inputs)
178
187
  if (Array.isArray(data[key])) {
179
188
  data[key].push(value);
180
189
  } else {
@@ -186,7 +195,11 @@ export function formAction(actionFn, options = {}) {
186
195
  }
187
196
 
188
197
  try {
189
- const result = await actionFn(data);
198
+ // If form contains files, pass the raw FormData as second arg
199
+ // so the action handler can access files directly
200
+ const result = hasFiles
201
+ ? await actionFn(data, formData)
202
+ : await actionFn(data);
190
203
  if (onSuccess) onSuccess(result, form);
191
204
  if (resetOnSuccess && form) form.reset();
192
205
  return result;
package/src/index.js CHANGED
@@ -4,6 +4,91 @@
4
4
 
5
5
  import { h } from 'what-core';
6
6
 
7
+ // --- Hydration ID Generator ---
8
+ let _hydrationIdCounter = 0;
9
+
10
+ function resetHydrationId() {
11
+ _hydrationIdCounter = 0;
12
+ }
13
+
14
+ function nextHydrationId() {
15
+ return 'h' + (_hydrationIdCounter++);
16
+ }
17
+
18
+ // --- Render to Hydratable String ---
19
+ // Renders with hydration markers (data-hk attributes, comment boundaries)
20
+ // so the client can reuse the server-rendered DOM.
21
+
22
+ export function renderToHydratableString(vnode) {
23
+ resetHydrationId();
24
+ return _renderHydratable(vnode);
25
+ }
26
+
27
+ function _renderHydratable(vnode) {
28
+ if (vnode == null || vnode === false || vnode === true) return '';
29
+
30
+ // Text
31
+ if (typeof vnode === 'string' || typeof vnode === 'number') {
32
+ return escapeHtml(String(vnode));
33
+ }
34
+
35
+ // Signal — unwrap
36
+ if (typeof vnode === 'function' && vnode._signal) {
37
+ return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
38
+ }
39
+
40
+ // Reactive function child — wrap in dynamic content markers
41
+ if (typeof vnode === 'function') {
42
+ try {
43
+ return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
44
+ } catch (e) {
45
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
46
+ console.warn('[what-server] Error rendering reactive function in SSR:', e.message);
47
+ }
48
+ return '<!--$--><!--/$-->';
49
+ }
50
+ }
51
+
52
+ // Array — wrap in list markers
53
+ if (Array.isArray(vnode)) {
54
+ return `<!--[]-->${vnode.map(_renderHydratable).join('')}<!--/[]-->`;
55
+ }
56
+
57
+ // Component — add hydration key to root element
58
+ if (typeof vnode.tag === 'function') {
59
+ const hkId = nextHydrationId();
60
+ const result = vnode.tag({ ...vnode.props, children: vnode.children });
61
+ const html = _renderHydratable(result);
62
+ // Inject data-hk into the first element tag if present
63
+ return injectHydrationKey(html, hkId);
64
+ }
65
+
66
+ // Element
67
+ const { tag, props, children } = vnode;
68
+ const attrs = renderAttrs(props || {});
69
+ const open = `<${tag}${attrs}>`;
70
+
71
+ // Void elements
72
+ if (VOID_ELEMENTS.has(tag)) return open;
73
+
74
+ const rawInner = _resolveInnerHTML(props);
75
+ const inner = rawInner != null ? String(rawInner) : children.map(_renderHydratable).join('');
76
+ return `${open}${inner}</${tag}>`;
77
+ }
78
+
79
+ // Inject data-hk="id" into the first HTML opening tag
80
+ function injectHydrationKey(html, hkId) {
81
+ // Skip comment markers to find the first real element
82
+ const match = html.match(/^((?:<!--.*?-->)*)<([a-zA-Z][a-zA-Z0-9-]*)/);
83
+ if (match) {
84
+ const prefix = match[1];
85
+ const tagName = match[2];
86
+ const insertAt = prefix.length + 1 + tagName.length; // after '<tagName'
87
+ return html.slice(0, insertAt) + ` data-hk="${hkId}"` + html.slice(insertAt);
88
+ }
89
+ return html;
90
+ }
91
+
7
92
  // --- Render to String ---
8
93
  // Renders a VNode tree to an HTML string. Used for SSR and static gen.
9
94
 
@@ -15,6 +100,23 @@ export function renderToString(vnode) {
15
100
  return escapeHtml(String(vnode));
16
101
  }
17
102
 
103
+ // Signal — unwrap by calling it
104
+ if (typeof vnode === 'function' && vnode._signal) {
105
+ return renderToString(vnode());
106
+ }
107
+
108
+ // Reactive function child — call to get value
109
+ if (typeof vnode === 'function') {
110
+ try {
111
+ return renderToString(vnode());
112
+ } catch (e) {
113
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
114
+ console.warn('[what-server] Error rendering reactive function in SSR:', e.message);
115
+ }
116
+ return '';
117
+ }
118
+ }
119
+
18
120
  // Array
19
121
  if (Array.isArray(vnode)) {
20
122
  return vnode.map(renderToString).join('');
@@ -34,9 +136,7 @@ export function renderToString(vnode) {
34
136
  // Void elements
35
137
  if (VOID_ELEMENTS.has(tag)) return open;
36
138
 
37
- const rawInner = props?.dangerouslySetInnerHTML?.__html
38
- ?? props?.innerHTML?.__html
39
- ?? props?.innerHTML;
139
+ const rawInner = _resolveInnerHTML(props);
40
140
  const inner = rawInner != null ? String(rawInner) : children.map(renderToString).join('');
41
141
  return `${open}${inner}</${tag}>`;
42
142
  }
@@ -52,6 +152,24 @@ export async function* renderToStream(vnode) {
52
152
  return;
53
153
  }
54
154
 
155
+ // Signal — unwrap by calling it
156
+ if (typeof vnode === 'function' && vnode._signal) {
157
+ yield* renderToStream(vnode());
158
+ return;
159
+ }
160
+
161
+ // Reactive function child — call to get value
162
+ if (typeof vnode === 'function') {
163
+ try {
164
+ yield* renderToStream(vnode());
165
+ } catch (e) {
166
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
167
+ console.warn('[what-server] Error rendering reactive function in stream SSR:', e.message);
168
+ }
169
+ }
170
+ return;
171
+ }
172
+
55
173
  if (Array.isArray(vnode)) {
56
174
  for (const child of vnode) {
57
175
  yield* renderToStream(child);
@@ -60,10 +178,19 @@ export async function* renderToStream(vnode) {
60
178
  }
61
179
 
62
180
  if (typeof vnode.tag === 'function') {
63
- const result = vnode.tag({ ...vnode.props, children: vnode.children });
64
- // Support async components
65
- const resolved = result instanceof Promise ? await result : result;
66
- yield* renderToStream(resolved);
181
+ try {
182
+ const result = vnode.tag({ ...vnode.props, children: vnode.children });
183
+ // Support async components
184
+ const resolved = result instanceof Promise ? await result : result;
185
+ yield* renderToStream(resolved);
186
+ } catch (e) {
187
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
188
+ console.warn('[what-server] Error rendering component in stream SSR:', e.message);
189
+ }
190
+ yield _isDevMode
191
+ ? `<!-- SSR Error: ${escapeHtml(e.message || 'Component error')} -->`
192
+ : `<!-- SSR Error -->`;
193
+ }
67
194
  return;
68
195
  }
69
196
 
@@ -72,9 +199,7 @@ export async function* renderToStream(vnode) {
72
199
  yield `<${tag}${attrs}>`;
73
200
 
74
201
  if (!VOID_ELEMENTS.has(tag)) {
75
- const rawInner = props?.dangerouslySetInnerHTML?.__html
76
- ?? props?.innerHTML?.__html
77
- ?? props?.innerHTML;
202
+ const rawInner = _resolveInnerHTML(props);
78
203
  if (rawInner != null) {
79
204
  yield String(rawInner);
80
205
  } else {
@@ -118,7 +243,7 @@ export function generateStaticPage(page, data = {}) {
118
243
 
119
244
  function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
120
245
  const metaTags = Object.entries(meta)
121
- .map(([name, content]) => `<meta name="${name}" content="${escapeHtml(content)}">`)
246
+ .map(([name, content]) => `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`)
122
247
  .join('\n ');
123
248
 
124
249
  const styleTags = styles
@@ -166,6 +291,42 @@ export function server(Component) {
166
291
 
167
292
  // --- Helpers ---
168
293
 
294
+ // Dev-mode flag for server
295
+ const _isDevMode = typeof process !== 'undefined'
296
+ ? process.env?.NODE_ENV !== 'production'
297
+ : true;
298
+
299
+ /**
300
+ * Resolve innerHTML / dangerouslySetInnerHTML from props.
301
+ * Requires { __html: ... } wrapper. Plain string innerHTML is rejected (XSS prevention).
302
+ */
303
+ function _resolveInnerHTML(props) {
304
+ if (!props) return null;
305
+
306
+ // dangerouslySetInnerHTML always requires { __html }
307
+ if (props.dangerouslySetInnerHTML) {
308
+ return props.dangerouslySetInnerHTML.__html ?? null;
309
+ }
310
+
311
+ // innerHTML with { __html } wrapper — allowed
312
+ if (props.innerHTML && typeof props.innerHTML === 'object' && '__html' in props.innerHTML) {
313
+ return props.innerHTML.__html ?? null;
314
+ }
315
+
316
+ // innerHTML as plain string — reject with warning
317
+ if (props.innerHTML != null && typeof props.innerHTML === 'string') {
318
+ if (_isDevMode) {
319
+ console.warn(
320
+ '[what-server] innerHTML received a raw string. This is a security risk (XSS). ' +
321
+ 'Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead.'
322
+ );
323
+ }
324
+ return null;
325
+ }
326
+
327
+ return null;
328
+ }
329
+
169
330
  function renderAttrs(props) {
170
331
  let out = '';
171
332
  for (const [key, val] of Object.entries(props)) {
@@ -181,7 +342,12 @@ function renderAttrs(props) {
181
342
  .join(';');
182
343
  out += ` style="${escapeHtml(css)}"`;
183
344
  } else if (val === true) {
184
- out += ` ${key}`;
345
+ // ARIA attributes require explicit ="true", HTML boolean attrs can be bare
346
+ if (key.startsWith('aria-') || key === 'role') {
347
+ out += ` ${key}="true"`;
348
+ } else {
349
+ out += ` ${key}`;
350
+ }
185
351
  } else {
186
352
  out += ` ${key}="${escapeHtml(String(val))}"`;
187
353
  }
@@ -195,10 +361,12 @@ function escapeHtml(str) {
195
361
  .replace(/&/g, '&amp;')
196
362
  .replace(/</g, '&lt;')
197
363
  .replace(/>/g, '&gt;')
198
- .replace(/"/g, '&quot;');
364
+ .replace(/"/g, '&quot;')
365
+ .replace(/'/g, '&#39;');
199
366
  }
200
367
 
201
368
  function camelToKebab(str) {
369
+ if (str.startsWith('--')) return str; // CSS custom properties (variables) — leave unchanged
202
370
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
203
371
  }
204
372
 
@@ -219,4 +387,7 @@ export {
219
387
  invalidatePath,
220
388
  handleActionRequest,
221
389
  getRegisteredActions,
390
+ generateCsrfToken,
391
+ validateCsrfToken,
392
+ csrfMetaTag,
222
393
  } from './actions.js';
package/src/islands.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // 'media' - Hydrate when media query matches (e.g., mobile-only)
17
17
  // 'action' - Hydrate on first user interaction (click, focus, hover)
18
18
 
19
- import { mount, signal, batch } from 'what-core';
19
+ import { mount, hydrate, signal, batch } from 'what-core';
20
20
 
21
21
  const islandRegistry = new Map();
22
22
  const hydratedIslands = new Set();
@@ -234,7 +234,13 @@ function scheduleHydration(el, entry, props, mode, priority, name, stores) {
234
234
  storeProps[storeName] = useIslandStore(storeName);
235
235
  }
236
236
 
237
- mount(Component({ ...props, ...storeProps }), el);
237
+ // Use hydrate() to reuse server-rendered DOM instead of destroying/recreating
238
+ const vnode = Component({ ...props, ...storeProps });
239
+ if (el.childNodes.length > 0) {
240
+ hydrate(vnode, el);
241
+ } else {
242
+ mount(vnode, el);
243
+ }
238
244
 
239
245
  // Clean up data attributes
240
246
  el.removeAttribute('data-island');
@@ -363,12 +369,36 @@ export function enhanceForms(selector = 'form[data-enhance]') {
363
369
  const action = form.action || location.href;
364
370
 
365
371
  try {
372
+ // Read CSRF token from meta tag
373
+ const csrfMeta = document.querySelector('meta[name="csrf-token"]') ||
374
+ document.querySelector('meta[name="what-csrf-token"]');
375
+ const csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : null;
376
+
377
+ // If no CSRF token and form hasn't opted out, block submission
378
+ const noCsrf = form.getAttribute('data-no-csrf') === 'true';
379
+ if (!csrfToken && !noCsrf) {
380
+ console.warn(
381
+ '[what] Form submission blocked: no CSRF token found. ' +
382
+ 'Add a <meta name="csrf-token"> tag or set data-no-csrf="true" on the form to opt out.'
383
+ );
384
+ form.dispatchEvent(new CustomEvent('form:error', {
385
+ bubbles: true,
386
+ detail: { error: new Error('Missing CSRF token') },
387
+ }));
388
+ return;
389
+ }
390
+
391
+ const headers = {
392
+ 'X-Requested-With': 'XMLHttpRequest',
393
+ };
394
+ if (csrfToken) {
395
+ headers['X-CSRF-Token'] = csrfToken;
396
+ }
397
+
366
398
  const response = await fetch(action, {
367
399
  method,
368
400
  body: method === 'GET' ? undefined : formData,
369
- headers: {
370
- 'X-Requested-With': 'XMLHttpRequest',
371
- },
401
+ headers,
372
402
  });
373
403
 
374
404
  form.dispatchEvent(new CustomEvent('form:response', {