what-server 0.5.5 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/actions.js +353 -0
- package/dist/actions.js.map +7 -0
- package/dist/actions.min.js +2 -0
- package/dist/actions.min.js.map +7 -0
- package/dist/index.js +700 -0
- package/dist/index.js.map +7 -0
- package/dist/index.min.js +28 -0
- package/dist/index.min.js.map +7 -0
- package/dist/islands.js +333 -0
- package/dist/islands.js.map +7 -0
- package/dist/islands.min.js +2 -0
- package/dist/islands.min.js.map +7 -0
- package/index.d.ts +12 -0
- package/package.json +8 -4
- package/src/actions.js +14 -4
- package/src/index.js +236 -16
- package/src/islands.js +35 -5
|
@@ -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/index.d.ts
CHANGED
|
@@ -7,9 +7,21 @@ import { VNode, VNodeChild, Signal } from '../core';
|
|
|
7
7
|
/** Render VNode tree to HTML string */
|
|
8
8
|
export function renderToString(vnode: VNode): string;
|
|
9
9
|
|
|
10
|
+
/** Render VNode tree with hydration markers (data-hk attributes, comment boundaries) */
|
|
11
|
+
export function renderToHydratableString(vnode: VNode): string;
|
|
12
|
+
|
|
10
13
|
/** Render VNode tree as async iterator for streaming */
|
|
11
14
|
export function renderToStream(vnode: VNode): AsyncGenerator<string>;
|
|
12
15
|
|
|
16
|
+
/** CSRF token generation for server actions */
|
|
17
|
+
export function generateCsrfToken(): string;
|
|
18
|
+
|
|
19
|
+
/** Validate a CSRF token from request against the session token */
|
|
20
|
+
export function validateCsrfToken(requestToken: string, sessionToken: string): boolean;
|
|
21
|
+
|
|
22
|
+
/** Generate a CSRF meta tag for inclusion in HTML head */
|
|
23
|
+
export function csrfMetaTag(token: string): string;
|
|
24
|
+
|
|
13
25
|
// --- Page Configuration ---
|
|
14
26
|
|
|
15
27
|
export interface PageConfig {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
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,
|
|
@@ -32,14 +36,14 @@
|
|
|
32
36
|
"author": "ZVN DEV (https://zvndev.com)",
|
|
33
37
|
"license": "MIT",
|
|
34
38
|
"peerDependencies": {
|
|
35
|
-
"what-core": "^0.
|
|
39
|
+
"what-core": "^0.6.2"
|
|
36
40
|
},
|
|
37
41
|
"repository": {
|
|
38
42
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/CelsianJs/
|
|
43
|
+
"url": "https://github.com/CelsianJs/what-framework"
|
|
40
44
|
},
|
|
41
45
|
"bugs": {
|
|
42
|
-
"url": "https://github.com/CelsianJs/
|
|
46
|
+
"url": "https://github.com/CelsianJs/what-framework/issues"
|
|
43
47
|
},
|
|
44
48
|
"homepage": "https://whatfw.com"
|
|
45
49
|
}
|
package/src/actions.js
CHANGED
|
@@ -45,8 +45,14 @@ export function generateCsrfToken() {
|
|
|
45
45
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
46
46
|
return crypto.randomUUID();
|
|
47
47
|
}
|
|
48
|
-
// Fallback for
|
|
49
|
-
|
|
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');
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
// Server: validate CSRF token from request header against session token
|
|
@@ -70,11 +76,15 @@ export function csrfMetaTag(token) {
|
|
|
70
76
|
|
|
71
77
|
// --- Define a server action ---
|
|
72
78
|
|
|
79
|
+
let _actionCounter = 0;
|
|
80
|
+
|
|
73
81
|
function generateActionId() {
|
|
74
|
-
// Generate a
|
|
82
|
+
// Generate a deterministic ID — prefer crypto.getRandomValues, fall back to a
|
|
83
|
+
// monotonic counter (never Math.random, which is not cryptographically safe and
|
|
84
|
+
// produces predictable IDs in some runtimes).
|
|
75
85
|
const rand = typeof crypto !== 'undefined' && crypto.getRandomValues
|
|
76
86
|
? Array.from(crypto.getRandomValues(new Uint8Array(6)), b => b.toString(16).padStart(2, '0')).join('')
|
|
77
|
-
:
|
|
87
|
+
: `c${(++_actionCounter).toString(36)}_${Date.now().toString(36)}`;
|
|
78
88
|
return `a_${rand}`;
|
|
79
89
|
}
|
|
80
90
|
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,169 @@
|
|
|
4
4
|
|
|
5
5
|
import { h } from 'what-core';
|
|
6
6
|
|
|
7
|
+
// --- SSR Error Collection ---
|
|
8
|
+
// Errors that occur during SSR are collected and serialized into the HTML output
|
|
9
|
+
// so the client can pick them up during hydration and display/report them.
|
|
10
|
+
|
|
11
|
+
let _ssrErrors = [];
|
|
12
|
+
const MAX_SSR_ERRORS = 50;
|
|
13
|
+
|
|
14
|
+
function _collectSSRError(error, context = {}) {
|
|
15
|
+
const entry = {
|
|
16
|
+
code: error.code || 'ERR_SSR_RENDER',
|
|
17
|
+
message: error.message || String(error),
|
|
18
|
+
component: context.component || null,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
};
|
|
21
|
+
// In dev mode, include extra detail for debugging
|
|
22
|
+
if (_isDevMode) {
|
|
23
|
+
entry.suggestion = error.suggestion || null;
|
|
24
|
+
entry.stack = error.stack?.split('\n').slice(0, 5).join('\n') || null;
|
|
25
|
+
}
|
|
26
|
+
_ssrErrors.push(entry);
|
|
27
|
+
if (_ssrErrors.length > MAX_SSR_ERRORS) _ssrErrors.shift();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _resetSSRErrors() {
|
|
31
|
+
_ssrErrors = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Serialize collected SSR errors into a script tag for client hydration.
|
|
36
|
+
* In dev mode: includes full error details (message, suggestion, stack).
|
|
37
|
+
* In production: includes only error code and component name.
|
|
38
|
+
*/
|
|
39
|
+
export function serializeSSRErrors() {
|
|
40
|
+
if (_ssrErrors.length === 0) return '';
|
|
41
|
+
const payload = _isDevMode
|
|
42
|
+
? _ssrErrors
|
|
43
|
+
: _ssrErrors.map(e => ({ code: e.code, component: e.component }));
|
|
44
|
+
const json = JSON.stringify(payload).replace(/<\//g, '<\\/'); // prevent XSS via </script>
|
|
45
|
+
return `<script type="application/json" data-what-ssr-errors>${json}</script>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read SSR errors from the DOM during client hydration.
|
|
50
|
+
* Call this on the client side during hydration to pick up errors from SSR.
|
|
51
|
+
* Returns an array of error objects, or empty array if none.
|
|
52
|
+
*/
|
|
53
|
+
export function hydrateSSRErrors() {
|
|
54
|
+
if (typeof document === 'undefined') return [];
|
|
55
|
+
const el = document.querySelector('script[data-what-ssr-errors]');
|
|
56
|
+
if (!el) return [];
|
|
57
|
+
try {
|
|
58
|
+
const errors = JSON.parse(el.textContent);
|
|
59
|
+
el.remove(); // clean up after reading
|
|
60
|
+
return errors;
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get collected SSR errors (for programmatic access before serialization).
|
|
68
|
+
*/
|
|
69
|
+
export function getSSRErrors() {
|
|
70
|
+
return _ssrErrors.slice();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- Hydration ID Generator ---
|
|
74
|
+
let _hydrationIdCounter = 0;
|
|
75
|
+
|
|
76
|
+
function resetHydrationId() {
|
|
77
|
+
_hydrationIdCounter = 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function nextHydrationId() {
|
|
81
|
+
return 'h' + (_hydrationIdCounter++);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Render to Hydratable String ---
|
|
85
|
+
// Renders with hydration markers (data-hk attributes, comment boundaries)
|
|
86
|
+
// so the client can reuse the server-rendered DOM.
|
|
87
|
+
|
|
88
|
+
export function renderToHydratableString(vnode) {
|
|
89
|
+
resetHydrationId();
|
|
90
|
+
_resetSSRErrors();
|
|
91
|
+
return _renderHydratable(vnode);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _renderHydratable(vnode) {
|
|
95
|
+
if (vnode == null || vnode === false || vnode === true) return '';
|
|
96
|
+
|
|
97
|
+
// Text
|
|
98
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
99
|
+
return escapeHtml(String(vnode));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Signal — unwrap
|
|
103
|
+
if (typeof vnode === 'function' && vnode._signal) {
|
|
104
|
+
return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Reactive function child — wrap in dynamic content markers
|
|
108
|
+
if (typeof vnode === 'function') {
|
|
109
|
+
try {
|
|
110
|
+
return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
_collectSSRError(e, { component: 'reactive-function' });
|
|
113
|
+
if (_isDevMode) {
|
|
114
|
+
console.warn('[what-server] Error rendering reactive function in SSR:', e.message);
|
|
115
|
+
}
|
|
116
|
+
return '<!--$--><!--/$-->';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Array — wrap in list markers
|
|
121
|
+
if (Array.isArray(vnode)) {
|
|
122
|
+
return `<!--[]-->${vnode.map(_renderHydratable).join('')}<!--/[]-->`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Component — add hydration key to root element
|
|
126
|
+
if (typeof vnode.tag === 'function') {
|
|
127
|
+
const hkId = nextHydrationId();
|
|
128
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || 'Anonymous';
|
|
129
|
+
try {
|
|
130
|
+
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
131
|
+
const html = _renderHydratable(result);
|
|
132
|
+
// Inject data-hk into the first element tag if present
|
|
133
|
+
return injectHydrationKey(html, hkId);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
_collectSSRError(e, { component: componentName });
|
|
136
|
+
if (_isDevMode) {
|
|
137
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in SSR:`, e.message);
|
|
138
|
+
return `<!--ssr-error:${escapeHtml(componentName)}-->`;
|
|
139
|
+
}
|
|
140
|
+
return `<!--ssr-error-->`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Element
|
|
145
|
+
const { tag, props, children } = vnode;
|
|
146
|
+
const attrs = renderAttrs(props || {});
|
|
147
|
+
const open = `<${tag}${attrs}>`;
|
|
148
|
+
|
|
149
|
+
// Void elements
|
|
150
|
+
if (VOID_ELEMENTS.has(tag)) return open;
|
|
151
|
+
|
|
152
|
+
const rawInner = _resolveInnerHTML(props);
|
|
153
|
+
const inner = rawInner != null ? String(rawInner) : children.map(_renderHydratable).join('');
|
|
154
|
+
return `${open}${inner}</${tag}>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Inject data-hk="id" into the first HTML opening tag
|
|
158
|
+
function injectHydrationKey(html, hkId) {
|
|
159
|
+
// Skip comment markers to find the first real element
|
|
160
|
+
const match = html.match(/^((?:<!--.*?-->)*)<([a-zA-Z][a-zA-Z0-9-]*)/);
|
|
161
|
+
if (match) {
|
|
162
|
+
const prefix = match[1];
|
|
163
|
+
const tagName = match[2];
|
|
164
|
+
const insertAt = prefix.length + 1 + tagName.length; // after '<tagName'
|
|
165
|
+
return html.slice(0, insertAt) + ` data-hk="${hkId}"` + html.slice(insertAt);
|
|
166
|
+
}
|
|
167
|
+
return html;
|
|
168
|
+
}
|
|
169
|
+
|
|
7
170
|
// --- Render to String ---
|
|
8
171
|
// Renders a VNode tree to an HTML string. Used for SSR and static gen.
|
|
9
172
|
|
|
@@ -25,7 +188,8 @@ export function renderToString(vnode) {
|
|
|
25
188
|
try {
|
|
26
189
|
return renderToString(vnode());
|
|
27
190
|
} catch (e) {
|
|
28
|
-
|
|
191
|
+
_collectSSRError(e, { component: 'reactive-function' });
|
|
192
|
+
if (_isDevMode) {
|
|
29
193
|
console.warn('[what-server] Error rendering reactive function in SSR:', e.message);
|
|
30
194
|
}
|
|
31
195
|
return '';
|
|
@@ -39,8 +203,18 @@ export function renderToString(vnode) {
|
|
|
39
203
|
|
|
40
204
|
// Component
|
|
41
205
|
if (typeof vnode.tag === 'function') {
|
|
42
|
-
const
|
|
43
|
-
|
|
206
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || 'Anonymous';
|
|
207
|
+
try {
|
|
208
|
+
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
209
|
+
return renderToString(result);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
_collectSSRError(e, { component: componentName });
|
|
212
|
+
if (_isDevMode) {
|
|
213
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in SSR:`, e.message);
|
|
214
|
+
return `<!-- SSR Error in ${escapeHtml(componentName)}: ${escapeHtml(e.message)} -->`;
|
|
215
|
+
}
|
|
216
|
+
return `<!-- SSR Error -->`;
|
|
217
|
+
}
|
|
44
218
|
}
|
|
45
219
|
|
|
46
220
|
// Element
|
|
@@ -51,9 +225,7 @@ export function renderToString(vnode) {
|
|
|
51
225
|
// Void elements
|
|
52
226
|
if (VOID_ELEMENTS.has(tag)) return open;
|
|
53
227
|
|
|
54
|
-
const rawInner = props
|
|
55
|
-
?? props?.innerHTML?.__html
|
|
56
|
-
?? props?.innerHTML;
|
|
228
|
+
const rawInner = _resolveInnerHTML(props);
|
|
57
229
|
const inner = rawInner != null ? String(rawInner) : children.map(renderToString).join('');
|
|
58
230
|
return `${open}${inner}</${tag}>`;
|
|
59
231
|
}
|
|
@@ -80,7 +252,8 @@ export async function* renderToStream(vnode) {
|
|
|
80
252
|
try {
|
|
81
253
|
yield* renderToStream(vnode());
|
|
82
254
|
} catch (e) {
|
|
83
|
-
|
|
255
|
+
_collectSSRError(e, { component: 'reactive-function' });
|
|
256
|
+
if (_isDevMode) {
|
|
84
257
|
console.warn('[what-server] Error rendering reactive function in stream SSR:', e.message);
|
|
85
258
|
}
|
|
86
259
|
}
|
|
@@ -95,16 +268,20 @@ export async function* renderToStream(vnode) {
|
|
|
95
268
|
}
|
|
96
269
|
|
|
97
270
|
if (typeof vnode.tag === 'function') {
|
|
271
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || 'Anonymous';
|
|
98
272
|
try {
|
|
99
273
|
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
100
274
|
// Support async components
|
|
101
275
|
const resolved = result instanceof Promise ? await result : result;
|
|
102
276
|
yield* renderToStream(resolved);
|
|
103
277
|
} catch (e) {
|
|
104
|
-
|
|
105
|
-
|
|
278
|
+
_collectSSRError(e, { component: componentName });
|
|
279
|
+
if (_isDevMode) {
|
|
280
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in stream SSR:`, e.message);
|
|
106
281
|
}
|
|
107
|
-
yield
|
|
282
|
+
yield _isDevMode
|
|
283
|
+
? `<!-- SSR Error in ${escapeHtml(componentName)}: ${escapeHtml(e.message || 'Component error')} -->`
|
|
284
|
+
: `<!-- SSR Error -->`;
|
|
108
285
|
}
|
|
109
286
|
return;
|
|
110
287
|
}
|
|
@@ -114,9 +291,7 @@ export async function* renderToStream(vnode) {
|
|
|
114
291
|
yield `<${tag}${attrs}>`;
|
|
115
292
|
|
|
116
293
|
if (!VOID_ELEMENTS.has(tag)) {
|
|
117
|
-
const rawInner = props
|
|
118
|
-
?? props?.innerHTML?.__html
|
|
119
|
-
?? props?.innerHTML;
|
|
294
|
+
const rawInner = _resolveInnerHTML(props);
|
|
120
295
|
if (rawInner != null) {
|
|
121
296
|
yield String(rawInner);
|
|
122
297
|
} else {
|
|
@@ -143,6 +318,7 @@ export function definePage(config) {
|
|
|
143
318
|
|
|
144
319
|
// Generate static HTML for a page
|
|
145
320
|
export function generateStaticPage(page, data = {}) {
|
|
321
|
+
_resetSSRErrors();
|
|
146
322
|
const vnode = page.component(data);
|
|
147
323
|
const html = renderToString(vnode);
|
|
148
324
|
const islands = page.islands || [];
|
|
@@ -155,12 +331,13 @@ export function generateStaticPage(page, data = {}) {
|
|
|
155
331
|
scripts: page.mode === 'static' ? [] : page.scripts || [],
|
|
156
332
|
styles: page.styles || [],
|
|
157
333
|
mode: page.mode,
|
|
334
|
+
ssrErrors: serializeSSRErrors(),
|
|
158
335
|
});
|
|
159
336
|
}
|
|
160
337
|
|
|
161
|
-
function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
|
|
338
|
+
function wrapDocument({ title, meta, body, islands, scripts, styles, mode, ssrErrors = '' }) {
|
|
162
339
|
const metaTags = Object.entries(meta)
|
|
163
|
-
.map(([name, content]) => `<meta name="${name}" content="${escapeHtml(content)}">`)
|
|
340
|
+
.map(([name, content]) => `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`)
|
|
164
341
|
.join('\n ');
|
|
165
342
|
|
|
166
343
|
const styleTags = styles
|
|
@@ -191,6 +368,7 @@ function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
|
|
|
191
368
|
</head>
|
|
192
369
|
<body>
|
|
193
370
|
<div id="app">${body}</div>
|
|
371
|
+
${ssrErrors}
|
|
194
372
|
${islandScript}
|
|
195
373
|
${scriptTags}
|
|
196
374
|
${clientScript}
|
|
@@ -208,6 +386,42 @@ export function server(Component) {
|
|
|
208
386
|
|
|
209
387
|
// --- Helpers ---
|
|
210
388
|
|
|
389
|
+
// Dev-mode flag for server
|
|
390
|
+
const _isDevMode = typeof process !== 'undefined'
|
|
391
|
+
? process.env?.NODE_ENV !== 'production'
|
|
392
|
+
: true;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Resolve innerHTML / dangerouslySetInnerHTML from props.
|
|
396
|
+
* Requires { __html: ... } wrapper. Plain string innerHTML is rejected (XSS prevention).
|
|
397
|
+
*/
|
|
398
|
+
function _resolveInnerHTML(props) {
|
|
399
|
+
if (!props) return null;
|
|
400
|
+
|
|
401
|
+
// dangerouslySetInnerHTML always requires { __html }
|
|
402
|
+
if (props.dangerouslySetInnerHTML) {
|
|
403
|
+
return props.dangerouslySetInnerHTML.__html ?? null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// innerHTML with { __html } wrapper — allowed
|
|
407
|
+
if (props.innerHTML && typeof props.innerHTML === 'object' && '__html' in props.innerHTML) {
|
|
408
|
+
return props.innerHTML.__html ?? null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// innerHTML as plain string — reject with warning
|
|
412
|
+
if (props.innerHTML != null && typeof props.innerHTML === 'string') {
|
|
413
|
+
if (_isDevMode) {
|
|
414
|
+
console.warn(
|
|
415
|
+
'[what-server] innerHTML received a raw string. This is a security risk (XSS). ' +
|
|
416
|
+
'Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead.'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
211
425
|
function renderAttrs(props) {
|
|
212
426
|
let out = '';
|
|
213
427
|
for (const [key, val] of Object.entries(props)) {
|
|
@@ -242,7 +456,8 @@ function escapeHtml(str) {
|
|
|
242
456
|
.replace(/&/g, '&')
|
|
243
457
|
.replace(/</g, '<')
|
|
244
458
|
.replace(/>/g, '>')
|
|
245
|
-
.replace(/"/g, '"')
|
|
459
|
+
.replace(/"/g, '"')
|
|
460
|
+
.replace(/'/g, ''');
|
|
246
461
|
}
|
|
247
462
|
|
|
248
463
|
function camelToKebab(str) {
|
|
@@ -255,6 +470,11 @@ const VOID_ELEMENTS = new Set([
|
|
|
255
470
|
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
256
471
|
]);
|
|
257
472
|
|
|
473
|
+
// SSR error serialization is exported above:
|
|
474
|
+
// serializeSSRErrors() — serialize collected errors to script tag
|
|
475
|
+
// hydrateSSRErrors() — read errors from DOM during client hydration
|
|
476
|
+
// getSSRErrors() — programmatic access to collected errors
|
|
477
|
+
|
|
258
478
|
// Re-export server actions
|
|
259
479
|
export {
|
|
260
480
|
action,
|
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
|
-
|
|
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', {
|