what-server 0.6.0 → 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/dist/actions.js +2 -1
- package/dist/actions.js.map +2 -2
- package/dist/actions.min.js +1 -1
- package/dist/actions.min.js.map +3 -3
- package/dist/index.js +87 -14
- package/dist/index.js.map +2 -2
- package/dist/index.min.js +14 -11
- package/dist/index.min.js.map +3 -3
- package/index.d.ts +12 -0
- package/package.json +2 -2
- package/src/actions.js +6 -2
- package/src/index.js +113 -13
package/dist/actions.js
CHANGED
|
@@ -38,8 +38,9 @@ function csrfMetaTag(token) {
|
|
|
38
38
|
const escaped = String(token).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
39
39
|
return `<meta name="what-csrf-token" content="${escaped}">`;
|
|
40
40
|
}
|
|
41
|
+
var _actionCounter = 0;
|
|
41
42
|
function generateActionId() {
|
|
42
|
-
const rand = typeof crypto !== "undefined" && crypto.getRandomValues ? Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("") :
|
|
43
|
+
const rand = typeof crypto !== "undefined" && crypto.getRandomValues ? Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("") : `c${(++_actionCounter).toString(36)}_${Date.now().toString(36)}`;
|
|
43
44
|
return `a_${rand}`;
|
|
44
45
|
}
|
|
45
46
|
function action(fn, options = {}) {
|
package/dist/actions.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/actions.js"],
|
|
4
|
-
"sourcesContent": ["// What Framework - Server Actions\n// Call server-side functions from client code seamlessly.\n// Similar to Next.js Server Actions / SolidStart server functions.\n//\n// Usage:\n// // Define on server\n// const saveUser = action(async (formData) => {\n// 'use server';\n// const user = await db.users.create(formData);\n// return { success: true, id: user.id };\n// });\n//\n// // Call from client\n// const result = await saveUser({ name: 'John' });\n\nimport { signal, batch } from 'what-core';\n\n// Registry of server actions\nconst actionRegistry = new Map();\n\n// --- CSRF Protection ---\n// Server generates a token per session; client sends it with every action request.\n// The token is injected into the page via a meta tag or embedded in the server response.\n\n// Client: read the CSRF token from the page meta tag or cookie\n// Re-reads on every call to handle token rotation\nfunction getCsrfToken() {\n if (typeof document !== 'undefined') {\n // Try meta tag first\n const meta = document.querySelector('meta[name=\"what-csrf-token\"]');\n if (meta) {\n return meta.getAttribute('content');\n }\n // Try cookie\n const match = document.cookie.match(/(?:^|;\\s*)what-csrf=([^;]+)/);\n if (match) {\n return decodeURIComponent(match[1]);\n }\n }\n return null;\n}\n\n// Server: generate a CSRF token (call this per session/request)\nexport function generateCsrfToken() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID \u2014 use crypto.getRandomValues\n if (typeof crypto !== 'undefined' && crypto.getRandomValues) {\n const arr = new Uint8Array(16);\n crypto.getRandomValues(arr);\n return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');\n }\n // Last resort \u2014 should not be reached in modern environments\n throw new Error('[what] No secure random source available for CSRF token generation');\n}\n\n// Server: validate CSRF token from request header against session token\nexport function validateCsrfToken(requestToken, sessionToken) {\n if (!requestToken || !sessionToken) return false;\n // Constant-time comparison to prevent timing attacks\n if (requestToken.length !== sessionToken.length) return false;\n let result = 0;\n for (let i = 0; i < requestToken.length; i++) {\n result |= requestToken.charCodeAt(i) ^ sessionToken.charCodeAt(i);\n }\n return result === 0;\n}\n\n// Server: middleware helper to inject CSRF meta tag into HTML\nexport function csrfMetaTag(token) {\n // HTML-escape the token to prevent XSS if a non-standard value is used\n const escaped = String(token).replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n return `<meta name=\"what-csrf-token\" content=\"${escaped}\">`;\n}\n\n// --- Define a server action ---\n\nfunction generateActionId() {\n // Generate a random ID that's not easily enumerable\n const rand = typeof crypto !== 'undefined' && crypto.getRandomValues\n ? Array.from(crypto.getRandomValues(new Uint8Array(6)), b => b.toString(16).padStart(2, '0')).join('')\n : Math.random().toString(36).slice(2, 10) + Date.now().toString(36);\n return `a_${rand}`;\n}\n\nexport function action(fn, options = {}) {\n const id = options.id || generateActionId();\n const { onError, onSuccess, revalidate } = options;\n\n // Server-side: register the action\n if (typeof window === 'undefined') {\n actionRegistry.set(id, { fn, options });\n }\n\n // Create the callable wrapper\n async function callAction(...args) {\n // Server-side: call directly\n if (typeof window === 'undefined') {\n return fn(...args);\n }\n\n // Client-side: call via fetch with timeout support\n const timeout = options.timeout || 30000; // Default 30s timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const csrfToken = getCsrfToken();\n const headers = {\n 'Content-Type': 'application/json',\n 'X-What-Action': id,\n };\n if (csrfToken) headers['X-CSRF-Token'] = csrfToken;\n\n const response = await fetch('/__what_action', {\n method: 'POST',\n headers,\n credentials: 'same-origin',\n signal: controller.signal,\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ message: 'Action failed' }));\n throw new Error(error.message || 'Action failed');\n }\n\n const result = await response.json();\n\n if (onSuccess) onSuccess(result);\n if (revalidate) {\n // Trigger revalidation of specified paths\n for (const path of revalidate) {\n invalidatePath(path);\n }\n }\n\n return result;\n } catch (error) {\n if (error.name === 'AbortError') {\n const timeoutError = new Error(`Action \"${id}\" timed out after ${timeout}ms`);\n timeoutError.code = 'TIMEOUT';\n if (onError) onError(timeoutError);\n throw timeoutError;\n }\n if (onError) onError(error);\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n callAction._actionId = id;\n callAction._isAction = true;\n\n return callAction;\n}\n\n// --- Form action helper ---\n// For forms that submit to server actions.\n\nexport function formAction(actionFn, options = {}) {\n const { onSuccess, onError, resetOnSuccess = true } = options;\n\n return async (formDataOrEvent) => {\n let formData;\n let form;\n\n if (formDataOrEvent instanceof Event) {\n formDataOrEvent.preventDefault();\n form = formDataOrEvent.target;\n formData = new FormData(form);\n } else {\n formData = formDataOrEvent;\n }\n\n // Convert FormData to plain object, preserving File instances\n const data = {};\n let hasFiles = false;\n for (const [key, value] of formData.entries()) {\n if (typeof File !== 'undefined' && value instanceof File) {\n hasFiles = true;\n }\n if (data[key]) {\n // Handle multiple values (e.g., checkboxes, multi-file inputs)\n if (Array.isArray(data[key])) {\n data[key].push(value);\n } else {\n data[key] = [data[key], value];\n }\n } else {\n data[key] = value;\n }\n }\n\n try {\n // If form contains files, pass the raw FormData as second arg\n // so the action handler can access files directly\n const result = hasFiles\n ? await actionFn(data, formData)\n : await actionFn(data);\n if (onSuccess) onSuccess(result, form);\n if (resetOnSuccess && form) form.reset();\n return result;\n } catch (error) {\n if (onError) onError(error, form);\n throw error;\n }\n };\n}\n\n// --- useAction hook ---\n// Returns action state and trigger function.\n\nexport function useAction(actionFn) {\n const isPending = signal(false);\n const error = signal(null);\n const data = signal(null);\n\n async function trigger(...args) {\n isPending.set(true);\n error.set(null);\n\n try {\n const result = await actionFn(...args);\n data.set(result);\n return result;\n } catch (e) {\n error.set(e);\n throw e;\n } finally {\n isPending.set(false);\n }\n }\n\n return {\n trigger,\n isPending: () => isPending(),\n error: () => error(),\n data: () => data(),\n reset: () => {\n error.set(null);\n data.set(null);\n },\n };\n}\n\n// --- useFormAction hook ---\n// Combines useAction with form handling.\n\nexport function useFormAction(actionFn, options = {}) {\n const { resetOnSuccess = true } = options;\n const formRef = { current: null };\n const actionState = useAction(formAction(actionFn, { resetOnSuccess }));\n\n function handleSubmit(e) {\n e.preventDefault();\n const formData = new FormData(e.target);\n formRef.current = e.target;\n return actionState.trigger(formData);\n }\n\n return {\n ...actionState,\n handleSubmit,\n formRef,\n };\n}\n\n// --- Optimistic updates ---\n\nexport function useOptimistic(initialValue, reducer) {\n const value = signal(initialValue);\n const pending = signal([]);\n const baseValue = signal(initialValue); // Track the confirmed server value\n\n function addOptimistic(action) {\n const optimisticValue = reducer(value.peek(), action);\n batch(() => {\n pending.set([...pending.peek(), action]);\n value.set(optimisticValue);\n });\n }\n\n function resolve(action, serverValue) {\n batch(() => {\n pending.set(pending.peek().filter(a => a !== action));\n if (serverValue !== undefined) {\n baseValue.set(serverValue);\n // Recompute optimistic state from new base + remaining pending actions\n let current = serverValue;\n for (const a of pending.peek()) {\n current = reducer(current, a);\n }\n value.set(current);\n }\n });\n }\n\n function rollback(action, realValue) {\n batch(() => {\n const newPending = pending.peek().filter(a => a !== action);\n pending.set(newPending);\n const base = realValue !== undefined ? realValue : baseValue.peek();\n baseValue.set(base);\n // Recompute from base + remaining pending actions\n let current = base;\n for (const a of newPending) {\n current = reducer(current, a);\n }\n value.set(current);\n });\n }\n\n // Auto-rollback helper: wraps an async action with automatic rollback on error\n async function withOptimistic(action, asyncFn) {\n addOptimistic(action);\n try {\n const result = await asyncFn();\n resolve(action, result);\n return result;\n } catch (e) {\n rollback(action);\n throw e;\n }\n }\n\n return {\n value: () => value(),\n isPending: () => pending().length > 0,\n addOptimistic,\n resolve,\n rollback,\n withOptimistic,\n set: (v) => { value.set(v); baseValue.set(v); },\n };\n}\n\n// --- Path revalidation ---\n\nconst revalidationCallbacks = new Map();\n\nexport function onRevalidate(path, callback) {\n if (!revalidationCallbacks.has(path)) {\n revalidationCallbacks.set(path, new Set());\n }\n revalidationCallbacks.get(path).add(callback);\n\n return () => {\n revalidationCallbacks.get(path)?.delete(callback);\n };\n}\n\nexport function invalidatePath(path) {\n const callbacks = revalidationCallbacks.get(path);\n if (callbacks) {\n for (const cb of callbacks) {\n try { cb(); } catch (e) { console.error('[what] Revalidation error:', e); }\n }\n }\n}\n\n// --- Server-side action handler ---\n// Add this to your server middleware.\n\nexport function handleActionRequest(req, actionId, args, options = {}) {\n const { csrfToken: sessionCsrfToken, skipCsrf = false } = options;\n\n // Validate CSRF token unless explicitly skipped\n if (!skipCsrf) {\n if (!sessionCsrfToken) {\n // Fail closed: no CSRF token configured means the developer forgot to set it up.\n // This prevents silent security vulnerabilities in production.\n return Promise.resolve({\n status: 500,\n body: {\n message: '[what] CSRF token not configured. ' +\n 'Pass { csrfToken: sessionToken } to handleActionRequest, ' +\n 'or { skipCsrf: true } to explicitly opt out.'\n }\n });\n }\n const requestCsrfToken = req?.headers?.['x-csrf-token'] || req?.headers?.['X-CSRF-Token'];\n if (!validateCsrfToken(requestCsrfToken, sessionCsrfToken)) {\n return Promise.resolve({ status: 403, body: { message: 'Invalid CSRF token' } });\n }\n }\n\n const action = actionRegistry.get(actionId);\n if (!action) {\n return Promise.resolve({ status: 404, body: { message: 'Action not found' } });\n }\n\n // Validate args is an array to prevent prototype pollution\n if (!Array.isArray(args)) {\n return Promise.resolve({ status: 400, body: { message: 'Invalid action arguments' } });\n }\n\n return action.fn(...args)\n .then(result => ({ status: 200, body: result }))\n .catch(error => {\n // Log the full error server-side, return generic message to client\n console.error(`[what] Action \"${actionId}\" error:`, error);\n return {\n status: 500,\n body: { message: 'Action failed' },\n };\n });\n}\n\n// --- Get all registered actions (for SSR/build) ---\n\nexport function getRegisteredActions() {\n return [...actionRegistry.keys()];\n}\n\n// --- Mutation helper ---\n// Like useSWR mutation but simpler.\n\nexport function useMutation(mutationFn, options = {}) {\n const { onSuccess, onError, onSettled } = options;\n\n const state = {\n isPending: signal(false),\n error: signal(null),\n data: signal(null),\n };\n\n async function mutate(...args) {\n state.isPending.set(true);\n state.error.set(null);\n\n try {\n const result = await mutationFn(...args);\n state.data.set(result);\n if (onSuccess) onSuccess(result, ...args);\n return result;\n } catch (error) {\n state.error.set(error);\n if (onError) onError(error, ...args);\n throw error;\n } finally {\n state.isPending.set(false);\n if (onSettled) onSettled(state.data.peek(), state.error.peek(), ...args);\n }\n }\n\n return {\n mutate,\n isPending: () => state.isPending(),\n error: () => state.error(),\n data: () => state.data(),\n reset: () => {\n state.error.set(null);\n state.data.set(null);\n },\n };\n}\n"],
|
|
5
|
-
"mappings": ";AAeA,SAAS,QAAQ,aAAa;AAG9B,IAAM,iBAAiB,oBAAI,IAAI;AAQ/B,SAAS,eAAe;AACtB,MAAI,OAAO,aAAa,aAAa;AAEnC,UAAM,OAAO,SAAS,cAAc,8BAA8B;AAClE,QAAI,MAAM;AACR,aAAO,KAAK,aAAa,SAAS;AAAA,IACpC;AAEA,UAAM,QAAQ,SAAS,OAAO,MAAM,6BAA6B;AACjE,QAAI,OAAO;AACT,aAAO,mBAAmB,MAAM,CAAC,CAAC;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,oBAAoB;AAClC,MAAI,OAAO,WAAW,eAAe,OAAO,YAAY;AACtD,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,MAAI,OAAO,WAAW,eAAe,OAAO,iBAAiB;AAC3D,UAAM,MAAM,IAAI,WAAW,EAAE;AAC7B,WAAO,gBAAgB,GAAG;AAC1B,WAAO,MAAM,KAAK,KAAK,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAAA,EACtE;AAEA,QAAM,IAAI,MAAM,oEAAoE;AACtF;AAGO,SAAS,kBAAkB,cAAc,cAAc;AAC5D,MAAI,CAAC,gBAAgB,CAAC,aAAc,QAAO;AAE3C,MAAI,aAAa,WAAW,aAAa,OAAQ,QAAO;AACxD,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,cAAU,aAAa,WAAW,CAAC,IAAI,aAAa,WAAW,CAAC;AAAA,EAClE;AACA,SAAO,WAAW;AACpB;AAGO,SAAS,YAAY,OAAO;AAEjC,QAAM,UAAU,OAAO,KAAK,EAAE,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,QAAQ,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AACvH,SAAO,yCAAyC,OAAO;AACzD;AAIA,SAAS,mBAAmB;
|
|
4
|
+
"sourcesContent": ["// What Framework - Server Actions\n// Call server-side functions from client code seamlessly.\n// Similar to Next.js Server Actions / SolidStart server functions.\n//\n// Usage:\n// // Define on server\n// const saveUser = action(async (formData) => {\n// 'use server';\n// const user = await db.users.create(formData);\n// return { success: true, id: user.id };\n// });\n//\n// // Call from client\n// const result = await saveUser({ name: 'John' });\n\nimport { signal, batch } from 'what-core';\n\n// Registry of server actions\nconst actionRegistry = new Map();\n\n// --- CSRF Protection ---\n// Server generates a token per session; client sends it with every action request.\n// The token is injected into the page via a meta tag or embedded in the server response.\n\n// Client: read the CSRF token from the page meta tag or cookie\n// Re-reads on every call to handle token rotation\nfunction getCsrfToken() {\n if (typeof document !== 'undefined') {\n // Try meta tag first\n const meta = document.querySelector('meta[name=\"what-csrf-token\"]');\n if (meta) {\n return meta.getAttribute('content');\n }\n // Try cookie\n const match = document.cookie.match(/(?:^|;\\s*)what-csrf=([^;]+)/);\n if (match) {\n return decodeURIComponent(match[1]);\n }\n }\n return null;\n}\n\n// Server: generate a CSRF token (call this per session/request)\nexport function generateCsrfToken() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID \u2014 use crypto.getRandomValues\n if (typeof crypto !== 'undefined' && crypto.getRandomValues) {\n const arr = new Uint8Array(16);\n crypto.getRandomValues(arr);\n return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');\n }\n // Last resort \u2014 should not be reached in modern environments\n throw new Error('[what] No secure random source available for CSRF token generation');\n}\n\n// Server: validate CSRF token from request header against session token\nexport function validateCsrfToken(requestToken, sessionToken) {\n if (!requestToken || !sessionToken) return false;\n // Constant-time comparison to prevent timing attacks\n if (requestToken.length !== sessionToken.length) return false;\n let result = 0;\n for (let i = 0; i < requestToken.length; i++) {\n result |= requestToken.charCodeAt(i) ^ sessionToken.charCodeAt(i);\n }\n return result === 0;\n}\n\n// Server: middleware helper to inject CSRF meta tag into HTML\nexport function csrfMetaTag(token) {\n // HTML-escape the token to prevent XSS if a non-standard value is used\n const escaped = String(token).replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n return `<meta name=\"what-csrf-token\" content=\"${escaped}\">`;\n}\n\n// --- Define a server action ---\n\nlet _actionCounter = 0;\n\nfunction generateActionId() {\n // Generate a deterministic ID \u2014 prefer crypto.getRandomValues, fall back to a\n // monotonic counter (never Math.random, which is not cryptographically safe and\n // produces predictable IDs in some runtimes).\n const rand = typeof crypto !== 'undefined' && crypto.getRandomValues\n ? Array.from(crypto.getRandomValues(new Uint8Array(6)), b => b.toString(16).padStart(2, '0')).join('')\n : `c${(++_actionCounter).toString(36)}_${Date.now().toString(36)}`;\n return `a_${rand}`;\n}\n\nexport function action(fn, options = {}) {\n const id = options.id || generateActionId();\n const { onError, onSuccess, revalidate } = options;\n\n // Server-side: register the action\n if (typeof window === 'undefined') {\n actionRegistry.set(id, { fn, options });\n }\n\n // Create the callable wrapper\n async function callAction(...args) {\n // Server-side: call directly\n if (typeof window === 'undefined') {\n return fn(...args);\n }\n\n // Client-side: call via fetch with timeout support\n const timeout = options.timeout || 30000; // Default 30s timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const csrfToken = getCsrfToken();\n const headers = {\n 'Content-Type': 'application/json',\n 'X-What-Action': id,\n };\n if (csrfToken) headers['X-CSRF-Token'] = csrfToken;\n\n const response = await fetch('/__what_action', {\n method: 'POST',\n headers,\n credentials: 'same-origin',\n signal: controller.signal,\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ message: 'Action failed' }));\n throw new Error(error.message || 'Action failed');\n }\n\n const result = await response.json();\n\n if (onSuccess) onSuccess(result);\n if (revalidate) {\n // Trigger revalidation of specified paths\n for (const path of revalidate) {\n invalidatePath(path);\n }\n }\n\n return result;\n } catch (error) {\n if (error.name === 'AbortError') {\n const timeoutError = new Error(`Action \"${id}\" timed out after ${timeout}ms`);\n timeoutError.code = 'TIMEOUT';\n if (onError) onError(timeoutError);\n throw timeoutError;\n }\n if (onError) onError(error);\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n callAction._actionId = id;\n callAction._isAction = true;\n\n return callAction;\n}\n\n// --- Form action helper ---\n// For forms that submit to server actions.\n\nexport function formAction(actionFn, options = {}) {\n const { onSuccess, onError, resetOnSuccess = true } = options;\n\n return async (formDataOrEvent) => {\n let formData;\n let form;\n\n if (formDataOrEvent instanceof Event) {\n formDataOrEvent.preventDefault();\n form = formDataOrEvent.target;\n formData = new FormData(form);\n } else {\n formData = formDataOrEvent;\n }\n\n // Convert FormData to plain object, preserving File instances\n const data = {};\n let hasFiles = false;\n for (const [key, value] of formData.entries()) {\n if (typeof File !== 'undefined' && value instanceof File) {\n hasFiles = true;\n }\n if (data[key]) {\n // Handle multiple values (e.g., checkboxes, multi-file inputs)\n if (Array.isArray(data[key])) {\n data[key].push(value);\n } else {\n data[key] = [data[key], value];\n }\n } else {\n data[key] = value;\n }\n }\n\n try {\n // If form contains files, pass the raw FormData as second arg\n // so the action handler can access files directly\n const result = hasFiles\n ? await actionFn(data, formData)\n : await actionFn(data);\n if (onSuccess) onSuccess(result, form);\n if (resetOnSuccess && form) form.reset();\n return result;\n } catch (error) {\n if (onError) onError(error, form);\n throw error;\n }\n };\n}\n\n// --- useAction hook ---\n// Returns action state and trigger function.\n\nexport function useAction(actionFn) {\n const isPending = signal(false);\n const error = signal(null);\n const data = signal(null);\n\n async function trigger(...args) {\n isPending.set(true);\n error.set(null);\n\n try {\n const result = await actionFn(...args);\n data.set(result);\n return result;\n } catch (e) {\n error.set(e);\n throw e;\n } finally {\n isPending.set(false);\n }\n }\n\n return {\n trigger,\n isPending: () => isPending(),\n error: () => error(),\n data: () => data(),\n reset: () => {\n error.set(null);\n data.set(null);\n },\n };\n}\n\n// --- useFormAction hook ---\n// Combines useAction with form handling.\n\nexport function useFormAction(actionFn, options = {}) {\n const { resetOnSuccess = true } = options;\n const formRef = { current: null };\n const actionState = useAction(formAction(actionFn, { resetOnSuccess }));\n\n function handleSubmit(e) {\n e.preventDefault();\n const formData = new FormData(e.target);\n formRef.current = e.target;\n return actionState.trigger(formData);\n }\n\n return {\n ...actionState,\n handleSubmit,\n formRef,\n };\n}\n\n// --- Optimistic updates ---\n\nexport function useOptimistic(initialValue, reducer) {\n const value = signal(initialValue);\n const pending = signal([]);\n const baseValue = signal(initialValue); // Track the confirmed server value\n\n function addOptimistic(action) {\n const optimisticValue = reducer(value.peek(), action);\n batch(() => {\n pending.set([...pending.peek(), action]);\n value.set(optimisticValue);\n });\n }\n\n function resolve(action, serverValue) {\n batch(() => {\n pending.set(pending.peek().filter(a => a !== action));\n if (serverValue !== undefined) {\n baseValue.set(serverValue);\n // Recompute optimistic state from new base + remaining pending actions\n let current = serverValue;\n for (const a of pending.peek()) {\n current = reducer(current, a);\n }\n value.set(current);\n }\n });\n }\n\n function rollback(action, realValue) {\n batch(() => {\n const newPending = pending.peek().filter(a => a !== action);\n pending.set(newPending);\n const base = realValue !== undefined ? realValue : baseValue.peek();\n baseValue.set(base);\n // Recompute from base + remaining pending actions\n let current = base;\n for (const a of newPending) {\n current = reducer(current, a);\n }\n value.set(current);\n });\n }\n\n // Auto-rollback helper: wraps an async action with automatic rollback on error\n async function withOptimistic(action, asyncFn) {\n addOptimistic(action);\n try {\n const result = await asyncFn();\n resolve(action, result);\n return result;\n } catch (e) {\n rollback(action);\n throw e;\n }\n }\n\n return {\n value: () => value(),\n isPending: () => pending().length > 0,\n addOptimistic,\n resolve,\n rollback,\n withOptimistic,\n set: (v) => { value.set(v); baseValue.set(v); },\n };\n}\n\n// --- Path revalidation ---\n\nconst revalidationCallbacks = new Map();\n\nexport function onRevalidate(path, callback) {\n if (!revalidationCallbacks.has(path)) {\n revalidationCallbacks.set(path, new Set());\n }\n revalidationCallbacks.get(path).add(callback);\n\n return () => {\n revalidationCallbacks.get(path)?.delete(callback);\n };\n}\n\nexport function invalidatePath(path) {\n const callbacks = revalidationCallbacks.get(path);\n if (callbacks) {\n for (const cb of callbacks) {\n try { cb(); } catch (e) { console.error('[what] Revalidation error:', e); }\n }\n }\n}\n\n// --- Server-side action handler ---\n// Add this to your server middleware.\n\nexport function handleActionRequest(req, actionId, args, options = {}) {\n const { csrfToken: sessionCsrfToken, skipCsrf = false } = options;\n\n // Validate CSRF token unless explicitly skipped\n if (!skipCsrf) {\n if (!sessionCsrfToken) {\n // Fail closed: no CSRF token configured means the developer forgot to set it up.\n // This prevents silent security vulnerabilities in production.\n return Promise.resolve({\n status: 500,\n body: {\n message: '[what] CSRF token not configured. ' +\n 'Pass { csrfToken: sessionToken } to handleActionRequest, ' +\n 'or { skipCsrf: true } to explicitly opt out.'\n }\n });\n }\n const requestCsrfToken = req?.headers?.['x-csrf-token'] || req?.headers?.['X-CSRF-Token'];\n if (!validateCsrfToken(requestCsrfToken, sessionCsrfToken)) {\n return Promise.resolve({ status: 403, body: { message: 'Invalid CSRF token' } });\n }\n }\n\n const action = actionRegistry.get(actionId);\n if (!action) {\n return Promise.resolve({ status: 404, body: { message: 'Action not found' } });\n }\n\n // Validate args is an array to prevent prototype pollution\n if (!Array.isArray(args)) {\n return Promise.resolve({ status: 400, body: { message: 'Invalid action arguments' } });\n }\n\n return action.fn(...args)\n .then(result => ({ status: 200, body: result }))\n .catch(error => {\n // Log the full error server-side, return generic message to client\n console.error(`[what] Action \"${actionId}\" error:`, error);\n return {\n status: 500,\n body: { message: 'Action failed' },\n };\n });\n}\n\n// --- Get all registered actions (for SSR/build) ---\n\nexport function getRegisteredActions() {\n return [...actionRegistry.keys()];\n}\n\n// --- Mutation helper ---\n// Like useSWR mutation but simpler.\n\nexport function useMutation(mutationFn, options = {}) {\n const { onSuccess, onError, onSettled } = options;\n\n const state = {\n isPending: signal(false),\n error: signal(null),\n data: signal(null),\n };\n\n async function mutate(...args) {\n state.isPending.set(true);\n state.error.set(null);\n\n try {\n const result = await mutationFn(...args);\n state.data.set(result);\n if (onSuccess) onSuccess(result, ...args);\n return result;\n } catch (error) {\n state.error.set(error);\n if (onError) onError(error, ...args);\n throw error;\n } finally {\n state.isPending.set(false);\n if (onSettled) onSettled(state.data.peek(), state.error.peek(), ...args);\n }\n }\n\n return {\n mutate,\n isPending: () => state.isPending(),\n error: () => state.error(),\n data: () => state.data(),\n reset: () => {\n state.error.set(null);\n state.data.set(null);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["action"]
|
|
7
7
|
}
|
package/dist/actions.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{signal as p,batch as w}from"what-core";var A=new Map;function k(){if(typeof document<"u"){let e=document.querySelector('meta[name="what-csrf-token"]');if(e)return e.getAttribute("content");let t=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);if(t)return decodeURIComponent(t[1])}return null}function
|
|
1
|
+
import{signal as p,batch as w}from"what-core";var A=new Map;function k(){if(typeof document<"u"){let e=document.querySelector('meta[name="what-csrf-token"]');if(e)return e.getAttribute("content");let t=document.cookie.match(/(?:^|;\s*)what-csrf=([^;]+)/);if(t)return decodeURIComponent(t[1])}return null}function T(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();if(typeof crypto<"u"&&crypto.getRandomValues){let e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e,t=>t.toString(16).padStart(2,"0")).join("")}throw new Error("[what] No secure random source available for CSRF token generation")}function S(e,t){if(!e||!t||e.length!==t.length)return!1;let r=0;for(let n=0;n<e.length;n++)r|=e.charCodeAt(n)^t.charCodeAt(n);return r===0}function F(e){return`<meta name="what-csrf-token" content="${String(e).replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">")}">`}var b=0;function C(){return`a_${typeof crypto<"u"&&crypto.getRandomValues?Array.from(crypto.getRandomValues(new Uint8Array(6)),t=>t.toString(16).padStart(2,"0")).join(""):`c${(++b).toString(36)}_${Date.now().toString(36)}`}`}function I(e,t={}){let r=t.id||C(),{onError:n,onSuccess:f,revalidate:o}=t;typeof window>"u"&&A.set(r,{fn:e,options:t});async function s(...i){if(typeof window>"u")return e(...i);let u=t.timeout||3e4,l=new AbortController,a=setTimeout(()=>l.abort(),u);try{let c=k(),d={"Content-Type":"application/json","X-What-Action":r};c&&(d["X-CSRF-Token"]=c);let g=await fetch("/__what_action",{method:"POST",headers:d,credentials:"same-origin",signal:l.signal,body:JSON.stringify({args:i})});if(!g.ok){let h=await g.json().catch(()=>({message:"Action failed"}));throw new Error(h.message||"Action failed")}let m=await g.json();if(f&&f(m),o)for(let h of o)x(h);return m}catch(c){if(c.name==="AbortError"){let d=new Error(`Action "${r}" timed out after ${u}ms`);throw d.code="TIMEOUT",n&&n(d),d}throw n&&n(c),c}finally{clearTimeout(a)}}return s._actionId=r,s._isAction=!0,s}function R(e,t={}){let{onSuccess:r,onError:n,resetOnSuccess:f=!0}=t;return async o=>{let s,i;o instanceof Event?(o.preventDefault(),i=o.target,s=new FormData(i)):s=o;let u={},l=!1;for(let[a,c]of s.entries())typeof File<"u"&&c instanceof File&&(l=!0),u[a]?Array.isArray(u[a])?u[a].push(c):u[a]=[u[a],c]:u[a]=c;try{let a=l?await e(u,s):await e(u);return r&&r(a,i),f&&i&&i.reset(),a}catch(a){throw n&&n(a,i),a}}}function P(e){let t=p(!1),r=p(null),n=p(null);async function f(...o){t.set(!0),r.set(null);try{let s=await e(...o);return n.set(s),s}catch(s){throw r.set(s),s}finally{t.set(!1)}}return{trigger:f,isPending:()=>t(),error:()=>r(),data:()=>n(),reset:()=>{r.set(null),n.set(null)}}}function U(e,t={}){let{resetOnSuccess:r=!0}=t,n={current:null},f=P(R(e,{resetOnSuccess:r}));function o(s){s.preventDefault();let i=new FormData(s.target);return n.current=s.target,f.trigger(i)}return{...f,handleSubmit:o,formRef:n}}function _(e,t){let r=p(e),n=p([]),f=p(e);function o(l){let a=t(r.peek(),l);w(()=>{n.set([...n.peek(),l]),r.set(a)})}function s(l,a){w(()=>{if(n.set(n.peek().filter(c=>c!==l)),a!==void 0){f.set(a);let c=a;for(let d of n.peek())c=t(c,d);r.set(c)}})}function i(l,a){w(()=>{let c=n.peek().filter(m=>m!==l);n.set(c);let d=a!==void 0?a:f.peek();f.set(d);let g=d;for(let m of c)g=t(g,m);r.set(g)})}async function u(l,a){o(l);try{let c=await a();return s(l,c),c}catch(c){throw i(l),c}}return{value:()=>r(),isPending:()=>n().length>0,addOptimistic:o,resolve:s,rollback:i,withOptimistic:u,set:l=>{r.set(l),f.set(l)}}}var y=new Map;function E(e,t){return y.has(e)||y.set(e,new Set),y.get(e).add(t),()=>{y.get(e)?.delete(t)}}function x(e){let t=y.get(e);if(t)for(let r of t)try{r()}catch(n){console.error("[what] Revalidation error:",n)}}function $(e,t,r,n={}){let{csrfToken:f,skipCsrf:o=!1}=n;if(!o){if(!f)return Promise.resolve({status:500,body:{message:"[what] CSRF token not configured. Pass { csrfToken: sessionToken } to handleActionRequest, or { skipCsrf: true } to explicitly opt out."}});let i=e?.headers?.["x-csrf-token"]||e?.headers?.["X-CSRF-Token"];if(!S(i,f))return Promise.resolve({status:403,body:{message:"Invalid CSRF token"}})}let s=A.get(t);return s?Array.isArray(r)?s.fn(...r).then(i=>({status:200,body:i})).catch(i=>(console.error(`[what] Action "${t}" error:`,i),{status:500,body:{message:"Action failed"}})):Promise.resolve({status:400,body:{message:"Invalid action arguments"}}):Promise.resolve({status:404,body:{message:"Action not found"}})}function D(){return[...A.keys()]}function j(e,t={}){let{onSuccess:r,onError:n,onSettled:f}=t,o={isPending:p(!1),error:p(null),data:p(null)};async function s(...i){o.isPending.set(!0),o.error.set(null);try{let u=await e(...i);return o.data.set(u),r&&r(u,...i),u}catch(u){throw o.error.set(u),n&&n(u,...i),u}finally{o.isPending.set(!1),f&&f(o.data.peek(),o.error.peek(),...i)}}return{mutate:s,isPending:()=>o.isPending(),error:()=>o.error(),data:()=>o.data(),reset:()=>{o.error.set(null),o.data.set(null)}}}export{I as action,F as csrfMetaTag,R as formAction,T as generateCsrfToken,D as getRegisteredActions,$ as handleActionRequest,x as invalidatePath,E as onRevalidate,P as useAction,U as useFormAction,j as useMutation,_ as useOptimistic,S as validateCsrfToken};
|
|
2
2
|
//# sourceMappingURL=actions.min.js.map
|
package/dist/actions.min.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/actions.js"],
|
|
4
|
-
"sourcesContent": ["// What Framework - Server Actions\n// Call server-side functions from client code seamlessly.\n// Similar to Next.js Server Actions / SolidStart server functions.\n//\n// Usage:\n// // Define on server\n// const saveUser = action(async (formData) => {\n// 'use server';\n// const user = await db.users.create(formData);\n// return { success: true, id: user.id };\n// });\n//\n// // Call from client\n// const result = await saveUser({ name: 'John' });\n\nimport { signal, batch } from 'what-core';\n\n// Registry of server actions\nconst actionRegistry = new Map();\n\n// --- CSRF Protection ---\n// Server generates a token per session; client sends it with every action request.\n// The token is injected into the page via a meta tag or embedded in the server response.\n\n// Client: read the CSRF token from the page meta tag or cookie\n// Re-reads on every call to handle token rotation\nfunction getCsrfToken() {\n if (typeof document !== 'undefined') {\n // Try meta tag first\n const meta = document.querySelector('meta[name=\"what-csrf-token\"]');\n if (meta) {\n return meta.getAttribute('content');\n }\n // Try cookie\n const match = document.cookie.match(/(?:^|;\\s*)what-csrf=([^;]+)/);\n if (match) {\n return decodeURIComponent(match[1]);\n }\n }\n return null;\n}\n\n// Server: generate a CSRF token (call this per session/request)\nexport function generateCsrfToken() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID \u2014 use crypto.getRandomValues\n if (typeof crypto !== 'undefined' && crypto.getRandomValues) {\n const arr = new Uint8Array(16);\n crypto.getRandomValues(arr);\n return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');\n }\n // Last resort \u2014 should not be reached in modern environments\n throw new Error('[what] No secure random source available for CSRF token generation');\n}\n\n// Server: validate CSRF token from request header against session token\nexport function validateCsrfToken(requestToken, sessionToken) {\n if (!requestToken || !sessionToken) return false;\n // Constant-time comparison to prevent timing attacks\n if (requestToken.length !== sessionToken.length) return false;\n let result = 0;\n for (let i = 0; i < requestToken.length; i++) {\n result |= requestToken.charCodeAt(i) ^ sessionToken.charCodeAt(i);\n }\n return result === 0;\n}\n\n// Server: middleware helper to inject CSRF meta tag into HTML\nexport function csrfMetaTag(token) {\n // HTML-escape the token to prevent XSS if a non-standard value is used\n const escaped = String(token).replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n return `<meta name=\"what-csrf-token\" content=\"${escaped}\">`;\n}\n\n// --- Define a server action ---\n\nfunction generateActionId() {\n // Generate a random ID that's not easily enumerable\n const rand = typeof crypto !== 'undefined' && crypto.getRandomValues\n ? Array.from(crypto.getRandomValues(new Uint8Array(6)), b => b.toString(16).padStart(2, '0')).join('')\n : Math.random().toString(36).slice(2, 10) + Date.now().toString(36);\n return `a_${rand}`;\n}\n\nexport function action(fn, options = {}) {\n const id = options.id || generateActionId();\n const { onError, onSuccess, revalidate } = options;\n\n // Server-side: register the action\n if (typeof window === 'undefined') {\n actionRegistry.set(id, { fn, options });\n }\n\n // Create the callable wrapper\n async function callAction(...args) {\n // Server-side: call directly\n if (typeof window === 'undefined') {\n return fn(...args);\n }\n\n // Client-side: call via fetch with timeout support\n const timeout = options.timeout || 30000; // Default 30s timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const csrfToken = getCsrfToken();\n const headers = {\n 'Content-Type': 'application/json',\n 'X-What-Action': id,\n };\n if (csrfToken) headers['X-CSRF-Token'] = csrfToken;\n\n const response = await fetch('/__what_action', {\n method: 'POST',\n headers,\n credentials: 'same-origin',\n signal: controller.signal,\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ message: 'Action failed' }));\n throw new Error(error.message || 'Action failed');\n }\n\n const result = await response.json();\n\n if (onSuccess) onSuccess(result);\n if (revalidate) {\n // Trigger revalidation of specified paths\n for (const path of revalidate) {\n invalidatePath(path);\n }\n }\n\n return result;\n } catch (error) {\n if (error.name === 'AbortError') {\n const timeoutError = new Error(`Action \"${id}\" timed out after ${timeout}ms`);\n timeoutError.code = 'TIMEOUT';\n if (onError) onError(timeoutError);\n throw timeoutError;\n }\n if (onError) onError(error);\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n callAction._actionId = id;\n callAction._isAction = true;\n\n return callAction;\n}\n\n// --- Form action helper ---\n// For forms that submit to server actions.\n\nexport function formAction(actionFn, options = {}) {\n const { onSuccess, onError, resetOnSuccess = true } = options;\n\n return async (formDataOrEvent) => {\n let formData;\n let form;\n\n if (formDataOrEvent instanceof Event) {\n formDataOrEvent.preventDefault();\n form = formDataOrEvent.target;\n formData = new FormData(form);\n } else {\n formData = formDataOrEvent;\n }\n\n // Convert FormData to plain object, preserving File instances\n const data = {};\n let hasFiles = false;\n for (const [key, value] of formData.entries()) {\n if (typeof File !== 'undefined' && value instanceof File) {\n hasFiles = true;\n }\n if (data[key]) {\n // Handle multiple values (e.g., checkboxes, multi-file inputs)\n if (Array.isArray(data[key])) {\n data[key].push(value);\n } else {\n data[key] = [data[key], value];\n }\n } else {\n data[key] = value;\n }\n }\n\n try {\n // If form contains files, pass the raw FormData as second arg\n // so the action handler can access files directly\n const result = hasFiles\n ? await actionFn(data, formData)\n : await actionFn(data);\n if (onSuccess) onSuccess(result, form);\n if (resetOnSuccess && form) form.reset();\n return result;\n } catch (error) {\n if (onError) onError(error, form);\n throw error;\n }\n };\n}\n\n// --- useAction hook ---\n// Returns action state and trigger function.\n\nexport function useAction(actionFn) {\n const isPending = signal(false);\n const error = signal(null);\n const data = signal(null);\n\n async function trigger(...args) {\n isPending.set(true);\n error.set(null);\n\n try {\n const result = await actionFn(...args);\n data.set(result);\n return result;\n } catch (e) {\n error.set(e);\n throw e;\n } finally {\n isPending.set(false);\n }\n }\n\n return {\n trigger,\n isPending: () => isPending(),\n error: () => error(),\n data: () => data(),\n reset: () => {\n error.set(null);\n data.set(null);\n },\n };\n}\n\n// --- useFormAction hook ---\n// Combines useAction with form handling.\n\nexport function useFormAction(actionFn, options = {}) {\n const { resetOnSuccess = true } = options;\n const formRef = { current: null };\n const actionState = useAction(formAction(actionFn, { resetOnSuccess }));\n\n function handleSubmit(e) {\n e.preventDefault();\n const formData = new FormData(e.target);\n formRef.current = e.target;\n return actionState.trigger(formData);\n }\n\n return {\n ...actionState,\n handleSubmit,\n formRef,\n };\n}\n\n// --- Optimistic updates ---\n\nexport function useOptimistic(initialValue, reducer) {\n const value = signal(initialValue);\n const pending = signal([]);\n const baseValue = signal(initialValue); // Track the confirmed server value\n\n function addOptimistic(action) {\n const optimisticValue = reducer(value.peek(), action);\n batch(() => {\n pending.set([...pending.peek(), action]);\n value.set(optimisticValue);\n });\n }\n\n function resolve(action, serverValue) {\n batch(() => {\n pending.set(pending.peek().filter(a => a !== action));\n if (serverValue !== undefined) {\n baseValue.set(serverValue);\n // Recompute optimistic state from new base + remaining pending actions\n let current = serverValue;\n for (const a of pending.peek()) {\n current = reducer(current, a);\n }\n value.set(current);\n }\n });\n }\n\n function rollback(action, realValue) {\n batch(() => {\n const newPending = pending.peek().filter(a => a !== action);\n pending.set(newPending);\n const base = realValue !== undefined ? realValue : baseValue.peek();\n baseValue.set(base);\n // Recompute from base + remaining pending actions\n let current = base;\n for (const a of newPending) {\n current = reducer(current, a);\n }\n value.set(current);\n });\n }\n\n // Auto-rollback helper: wraps an async action with automatic rollback on error\n async function withOptimistic(action, asyncFn) {\n addOptimistic(action);\n try {\n const result = await asyncFn();\n resolve(action, result);\n return result;\n } catch (e) {\n rollback(action);\n throw e;\n }\n }\n\n return {\n value: () => value(),\n isPending: () => pending().length > 0,\n addOptimistic,\n resolve,\n rollback,\n withOptimistic,\n set: (v) => { value.set(v); baseValue.set(v); },\n };\n}\n\n// --- Path revalidation ---\n\nconst revalidationCallbacks = new Map();\n\nexport function onRevalidate(path, callback) {\n if (!revalidationCallbacks.has(path)) {\n revalidationCallbacks.set(path, new Set());\n }\n revalidationCallbacks.get(path).add(callback);\n\n return () => {\n revalidationCallbacks.get(path)?.delete(callback);\n };\n}\n\nexport function invalidatePath(path) {\n const callbacks = revalidationCallbacks.get(path);\n if (callbacks) {\n for (const cb of callbacks) {\n try { cb(); } catch (e) { console.error('[what] Revalidation error:', e); }\n }\n }\n}\n\n// --- Server-side action handler ---\n// Add this to your server middleware.\n\nexport function handleActionRequest(req, actionId, args, options = {}) {\n const { csrfToken: sessionCsrfToken, skipCsrf = false } = options;\n\n // Validate CSRF token unless explicitly skipped\n if (!skipCsrf) {\n if (!sessionCsrfToken) {\n // Fail closed: no CSRF token configured means the developer forgot to set it up.\n // This prevents silent security vulnerabilities in production.\n return Promise.resolve({\n status: 500,\n body: {\n message: '[what] CSRF token not configured. ' +\n 'Pass { csrfToken: sessionToken } to handleActionRequest, ' +\n 'or { skipCsrf: true } to explicitly opt out.'\n }\n });\n }\n const requestCsrfToken = req?.headers?.['x-csrf-token'] || req?.headers?.['X-CSRF-Token'];\n if (!validateCsrfToken(requestCsrfToken, sessionCsrfToken)) {\n return Promise.resolve({ status: 403, body: { message: 'Invalid CSRF token' } });\n }\n }\n\n const action = actionRegistry.get(actionId);\n if (!action) {\n return Promise.resolve({ status: 404, body: { message: 'Action not found' } });\n }\n\n // Validate args is an array to prevent prototype pollution\n if (!Array.isArray(args)) {\n return Promise.resolve({ status: 400, body: { message: 'Invalid action arguments' } });\n }\n\n return action.fn(...args)\n .then(result => ({ status: 200, body: result }))\n .catch(error => {\n // Log the full error server-side, return generic message to client\n console.error(`[what] Action \"${actionId}\" error:`, error);\n return {\n status: 500,\n body: { message: 'Action failed' },\n };\n });\n}\n\n// --- Get all registered actions (for SSR/build) ---\n\nexport function getRegisteredActions() {\n return [...actionRegistry.keys()];\n}\n\n// --- Mutation helper ---\n// Like useSWR mutation but simpler.\n\nexport function useMutation(mutationFn, options = {}) {\n const { onSuccess, onError, onSettled } = options;\n\n const state = {\n isPending: signal(false),\n error: signal(null),\n data: signal(null),\n };\n\n async function mutate(...args) {\n state.isPending.set(true);\n state.error.set(null);\n\n try {\n const result = await mutationFn(...args);\n state.data.set(result);\n if (onSuccess) onSuccess(result, ...args);\n return result;\n } catch (error) {\n state.error.set(error);\n if (onError) onError(error, ...args);\n throw error;\n } finally {\n state.isPending.set(false);\n if (onSettled) onSettled(state.data.peek(), state.error.peek(), ...args);\n }\n }\n\n return {\n mutate,\n isPending: () => state.isPending(),\n error: () => state.error(),\n data: () => state.data(),\n reset: () => {\n state.error.set(null);\n state.data.set(null);\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "AAeA,OAAS,UAAAA,EAAQ,SAAAC,MAAa,YAG9B,IAAMC,EAAiB,IAAI,IAQ3B,SAASC,GAAe,CACtB,GAAI,OAAO,SAAa,IAAa,CAEnC,IAAMC,EAAO,SAAS,cAAc,8BAA8B,EAClE,GAAIA,EACF,OAAOA,EAAK,aAAa,SAAS,EAGpC,IAAMC,EAAQ,SAAS,OAAO,MAAM,6BAA6B,EACjE,GAAIA,EACF,OAAO,mBAAmBA,EAAM,CAAC,CAAC,CAEtC,CACA,OAAO,IACT,CAGO,SAASC,GAAoB,CAClC,GAAI,OAAO,OAAW,KAAe,OAAO,WAC1C,OAAO,OAAO,WAAW,EAG3B,GAAI,OAAO,OAAW,KAAe,OAAO,gBAAiB,CAC3D,IAAMC,EAAM,IAAI,WAAW,EAAE,EAC7B,cAAO,gBAAgBA,CAAG,EACnB,MAAM,KAAKA,EAAKC,GAAKA,EAAE,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CACtE,CAEA,MAAM,IAAI,MAAM,oEAAoE,CACtF,CAGO,SAASC,EAAkBC,EAAcC,EAAc,CAG5D,GAFI,CAACD,GAAgB,CAACC,GAElBD,EAAa,SAAWC,EAAa,OAAQ,MAAO,GACxD,IAAIC,EAAS,EACb,QAASC,EAAI,EAAGA,EAAIH,EAAa,OAAQG,IACvCD,GAAUF,EAAa,WAAWG,CAAC,EAAIF,EAAa,WAAWE,CAAC,EAElE,OAAOD,IAAW,CACpB,CAGO,SAASE,EAAYC,EAAO,CAGjC,MAAO,yCADS,OAAOA,CAAK,EAAE,QAAQ,KAAM,OAAO,EAAE,QAAQ,KAAM,QAAQ,EAAE,QAAQ,KAAM,MAAM,EAAE,QAAQ,KAAM,MAAM,CAChE,IACzD,CAIA,SAASC,GAAmB,
|
|
6
|
-
"names": ["signal", "batch", "actionRegistry", "getCsrfToken", "meta", "match", "generateCsrfToken", "arr", "b", "validateCsrfToken", "requestToken", "sessionToken", "result", "i", "csrfMetaTag", "token", "generateActionId", "action", "fn", "options", "id", "onError", "onSuccess", "revalidate", "callAction", "args", "timeout", "controller", "timeoutId", "csrfToken", "headers", "response", "error", "path", "invalidatePath", "timeoutError", "formAction", "actionFn", "resetOnSuccess", "formDataOrEvent", "formData", "form", "data", "hasFiles", "key", "value", "useAction", "isPending", "trigger", "e", "useFormAction", "formRef", "actionState", "handleSubmit", "useOptimistic", "initialValue", "reducer", "pending", "baseValue", "addOptimistic", "optimisticValue", "resolve", "serverValue", "a", "current", "rollback", "realValue", "newPending", "base", "withOptimistic", "asyncFn", "v", "revalidationCallbacks", "onRevalidate", "callback", "callbacks", "cb", "handleActionRequest", "req", "actionId", "sessionCsrfToken", "skipCsrf", "requestCsrfToken", "getRegisteredActions", "useMutation", "mutationFn", "onSettled", "state", "mutate"]
|
|
4
|
+
"sourcesContent": ["// What Framework - Server Actions\n// Call server-side functions from client code seamlessly.\n// Similar to Next.js Server Actions / SolidStart server functions.\n//\n// Usage:\n// // Define on server\n// const saveUser = action(async (formData) => {\n// 'use server';\n// const user = await db.users.create(formData);\n// return { success: true, id: user.id };\n// });\n//\n// // Call from client\n// const result = await saveUser({ name: 'John' });\n\nimport { signal, batch } from 'what-core';\n\n// Registry of server actions\nconst actionRegistry = new Map();\n\n// --- CSRF Protection ---\n// Server generates a token per session; client sends it with every action request.\n// The token is injected into the page via a meta tag or embedded in the server response.\n\n// Client: read the CSRF token from the page meta tag or cookie\n// Re-reads on every call to handle token rotation\nfunction getCsrfToken() {\n if (typeof document !== 'undefined') {\n // Try meta tag first\n const meta = document.querySelector('meta[name=\"what-csrf-token\"]');\n if (meta) {\n return meta.getAttribute('content');\n }\n // Try cookie\n const match = document.cookie.match(/(?:^|;\\s*)what-csrf=([^;]+)/);\n if (match) {\n return decodeURIComponent(match[1]);\n }\n }\n return null;\n}\n\n// Server: generate a CSRF token (call this per session/request)\nexport function generateCsrfToken() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback for environments without crypto.randomUUID \u2014 use crypto.getRandomValues\n if (typeof crypto !== 'undefined' && crypto.getRandomValues) {\n const arr = new Uint8Array(16);\n crypto.getRandomValues(arr);\n return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');\n }\n // Last resort \u2014 should not be reached in modern environments\n throw new Error('[what] No secure random source available for CSRF token generation');\n}\n\n// Server: validate CSRF token from request header against session token\nexport function validateCsrfToken(requestToken, sessionToken) {\n if (!requestToken || !sessionToken) return false;\n // Constant-time comparison to prevent timing attacks\n if (requestToken.length !== sessionToken.length) return false;\n let result = 0;\n for (let i = 0; i < requestToken.length; i++) {\n result |= requestToken.charCodeAt(i) ^ sessionToken.charCodeAt(i);\n }\n return result === 0;\n}\n\n// Server: middleware helper to inject CSRF meta tag into HTML\nexport function csrfMetaTag(token) {\n // HTML-escape the token to prevent XSS if a non-standard value is used\n const escaped = String(token).replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n return `<meta name=\"what-csrf-token\" content=\"${escaped}\">`;\n}\n\n// --- Define a server action ---\n\nlet _actionCounter = 0;\n\nfunction generateActionId() {\n // Generate a deterministic ID \u2014 prefer crypto.getRandomValues, fall back to a\n // monotonic counter (never Math.random, which is not cryptographically safe and\n // produces predictable IDs in some runtimes).\n const rand = typeof crypto !== 'undefined' && crypto.getRandomValues\n ? Array.from(crypto.getRandomValues(new Uint8Array(6)), b => b.toString(16).padStart(2, '0')).join('')\n : `c${(++_actionCounter).toString(36)}_${Date.now().toString(36)}`;\n return `a_${rand}`;\n}\n\nexport function action(fn, options = {}) {\n const id = options.id || generateActionId();\n const { onError, onSuccess, revalidate } = options;\n\n // Server-side: register the action\n if (typeof window === 'undefined') {\n actionRegistry.set(id, { fn, options });\n }\n\n // Create the callable wrapper\n async function callAction(...args) {\n // Server-side: call directly\n if (typeof window === 'undefined') {\n return fn(...args);\n }\n\n // Client-side: call via fetch with timeout support\n const timeout = options.timeout || 30000; // Default 30s timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const csrfToken = getCsrfToken();\n const headers = {\n 'Content-Type': 'application/json',\n 'X-What-Action': id,\n };\n if (csrfToken) headers['X-CSRF-Token'] = csrfToken;\n\n const response = await fetch('/__what_action', {\n method: 'POST',\n headers,\n credentials: 'same-origin',\n signal: controller.signal,\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ message: 'Action failed' }));\n throw new Error(error.message || 'Action failed');\n }\n\n const result = await response.json();\n\n if (onSuccess) onSuccess(result);\n if (revalidate) {\n // Trigger revalidation of specified paths\n for (const path of revalidate) {\n invalidatePath(path);\n }\n }\n\n return result;\n } catch (error) {\n if (error.name === 'AbortError') {\n const timeoutError = new Error(`Action \"${id}\" timed out after ${timeout}ms`);\n timeoutError.code = 'TIMEOUT';\n if (onError) onError(timeoutError);\n throw timeoutError;\n }\n if (onError) onError(error);\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n callAction._actionId = id;\n callAction._isAction = true;\n\n return callAction;\n}\n\n// --- Form action helper ---\n// For forms that submit to server actions.\n\nexport function formAction(actionFn, options = {}) {\n const { onSuccess, onError, resetOnSuccess = true } = options;\n\n return async (formDataOrEvent) => {\n let formData;\n let form;\n\n if (formDataOrEvent instanceof Event) {\n formDataOrEvent.preventDefault();\n form = formDataOrEvent.target;\n formData = new FormData(form);\n } else {\n formData = formDataOrEvent;\n }\n\n // Convert FormData to plain object, preserving File instances\n const data = {};\n let hasFiles = false;\n for (const [key, value] of formData.entries()) {\n if (typeof File !== 'undefined' && value instanceof File) {\n hasFiles = true;\n }\n if (data[key]) {\n // Handle multiple values (e.g., checkboxes, multi-file inputs)\n if (Array.isArray(data[key])) {\n data[key].push(value);\n } else {\n data[key] = [data[key], value];\n }\n } else {\n data[key] = value;\n }\n }\n\n try {\n // If form contains files, pass the raw FormData as second arg\n // so the action handler can access files directly\n const result = hasFiles\n ? await actionFn(data, formData)\n : await actionFn(data);\n if (onSuccess) onSuccess(result, form);\n if (resetOnSuccess && form) form.reset();\n return result;\n } catch (error) {\n if (onError) onError(error, form);\n throw error;\n }\n };\n}\n\n// --- useAction hook ---\n// Returns action state and trigger function.\n\nexport function useAction(actionFn) {\n const isPending = signal(false);\n const error = signal(null);\n const data = signal(null);\n\n async function trigger(...args) {\n isPending.set(true);\n error.set(null);\n\n try {\n const result = await actionFn(...args);\n data.set(result);\n return result;\n } catch (e) {\n error.set(e);\n throw e;\n } finally {\n isPending.set(false);\n }\n }\n\n return {\n trigger,\n isPending: () => isPending(),\n error: () => error(),\n data: () => data(),\n reset: () => {\n error.set(null);\n data.set(null);\n },\n };\n}\n\n// --- useFormAction hook ---\n// Combines useAction with form handling.\n\nexport function useFormAction(actionFn, options = {}) {\n const { resetOnSuccess = true } = options;\n const formRef = { current: null };\n const actionState = useAction(formAction(actionFn, { resetOnSuccess }));\n\n function handleSubmit(e) {\n e.preventDefault();\n const formData = new FormData(e.target);\n formRef.current = e.target;\n return actionState.trigger(formData);\n }\n\n return {\n ...actionState,\n handleSubmit,\n formRef,\n };\n}\n\n// --- Optimistic updates ---\n\nexport function useOptimistic(initialValue, reducer) {\n const value = signal(initialValue);\n const pending = signal([]);\n const baseValue = signal(initialValue); // Track the confirmed server value\n\n function addOptimistic(action) {\n const optimisticValue = reducer(value.peek(), action);\n batch(() => {\n pending.set([...pending.peek(), action]);\n value.set(optimisticValue);\n });\n }\n\n function resolve(action, serverValue) {\n batch(() => {\n pending.set(pending.peek().filter(a => a !== action));\n if (serverValue !== undefined) {\n baseValue.set(serverValue);\n // Recompute optimistic state from new base + remaining pending actions\n let current = serverValue;\n for (const a of pending.peek()) {\n current = reducer(current, a);\n }\n value.set(current);\n }\n });\n }\n\n function rollback(action, realValue) {\n batch(() => {\n const newPending = pending.peek().filter(a => a !== action);\n pending.set(newPending);\n const base = realValue !== undefined ? realValue : baseValue.peek();\n baseValue.set(base);\n // Recompute from base + remaining pending actions\n let current = base;\n for (const a of newPending) {\n current = reducer(current, a);\n }\n value.set(current);\n });\n }\n\n // Auto-rollback helper: wraps an async action with automatic rollback on error\n async function withOptimistic(action, asyncFn) {\n addOptimistic(action);\n try {\n const result = await asyncFn();\n resolve(action, result);\n return result;\n } catch (e) {\n rollback(action);\n throw e;\n }\n }\n\n return {\n value: () => value(),\n isPending: () => pending().length > 0,\n addOptimistic,\n resolve,\n rollback,\n withOptimistic,\n set: (v) => { value.set(v); baseValue.set(v); },\n };\n}\n\n// --- Path revalidation ---\n\nconst revalidationCallbacks = new Map();\n\nexport function onRevalidate(path, callback) {\n if (!revalidationCallbacks.has(path)) {\n revalidationCallbacks.set(path, new Set());\n }\n revalidationCallbacks.get(path).add(callback);\n\n return () => {\n revalidationCallbacks.get(path)?.delete(callback);\n };\n}\n\nexport function invalidatePath(path) {\n const callbacks = revalidationCallbacks.get(path);\n if (callbacks) {\n for (const cb of callbacks) {\n try { cb(); } catch (e) { console.error('[what] Revalidation error:', e); }\n }\n }\n}\n\n// --- Server-side action handler ---\n// Add this to your server middleware.\n\nexport function handleActionRequest(req, actionId, args, options = {}) {\n const { csrfToken: sessionCsrfToken, skipCsrf = false } = options;\n\n // Validate CSRF token unless explicitly skipped\n if (!skipCsrf) {\n if (!sessionCsrfToken) {\n // Fail closed: no CSRF token configured means the developer forgot to set it up.\n // This prevents silent security vulnerabilities in production.\n return Promise.resolve({\n status: 500,\n body: {\n message: '[what] CSRF token not configured. ' +\n 'Pass { csrfToken: sessionToken } to handleActionRequest, ' +\n 'or { skipCsrf: true } to explicitly opt out.'\n }\n });\n }\n const requestCsrfToken = req?.headers?.['x-csrf-token'] || req?.headers?.['X-CSRF-Token'];\n if (!validateCsrfToken(requestCsrfToken, sessionCsrfToken)) {\n return Promise.resolve({ status: 403, body: { message: 'Invalid CSRF token' } });\n }\n }\n\n const action = actionRegistry.get(actionId);\n if (!action) {\n return Promise.resolve({ status: 404, body: { message: 'Action not found' } });\n }\n\n // Validate args is an array to prevent prototype pollution\n if (!Array.isArray(args)) {\n return Promise.resolve({ status: 400, body: { message: 'Invalid action arguments' } });\n }\n\n return action.fn(...args)\n .then(result => ({ status: 200, body: result }))\n .catch(error => {\n // Log the full error server-side, return generic message to client\n console.error(`[what] Action \"${actionId}\" error:`, error);\n return {\n status: 500,\n body: { message: 'Action failed' },\n };\n });\n}\n\n// --- Get all registered actions (for SSR/build) ---\n\nexport function getRegisteredActions() {\n return [...actionRegistry.keys()];\n}\n\n// --- Mutation helper ---\n// Like useSWR mutation but simpler.\n\nexport function useMutation(mutationFn, options = {}) {\n const { onSuccess, onError, onSettled } = options;\n\n const state = {\n isPending: signal(false),\n error: signal(null),\n data: signal(null),\n };\n\n async function mutate(...args) {\n state.isPending.set(true);\n state.error.set(null);\n\n try {\n const result = await mutationFn(...args);\n state.data.set(result);\n if (onSuccess) onSuccess(result, ...args);\n return result;\n } catch (error) {\n state.error.set(error);\n if (onError) onError(error, ...args);\n throw error;\n } finally {\n state.isPending.set(false);\n if (onSettled) onSettled(state.data.peek(), state.error.peek(), ...args);\n }\n }\n\n return {\n mutate,\n isPending: () => state.isPending(),\n error: () => state.error(),\n data: () => state.data(),\n reset: () => {\n state.error.set(null);\n state.data.set(null);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["signal", "batch", "actionRegistry", "getCsrfToken", "meta", "match", "generateCsrfToken", "arr", "b", "validateCsrfToken", "requestToken", "sessionToken", "result", "i", "csrfMetaTag", "token", "_actionCounter", "generateActionId", "action", "fn", "options", "id", "onError", "onSuccess", "revalidate", "callAction", "args", "timeout", "controller", "timeoutId", "csrfToken", "headers", "response", "error", "path", "invalidatePath", "timeoutError", "formAction", "actionFn", "resetOnSuccess", "formDataOrEvent", "formData", "form", "data", "hasFiles", "key", "value", "useAction", "isPending", "trigger", "e", "useFormAction", "formRef", "actionState", "handleSubmit", "useOptimistic", "initialValue", "reducer", "pending", "baseValue", "addOptimistic", "optimisticValue", "resolve", "serverValue", "a", "current", "rollback", "realValue", "newPending", "base", "withOptimistic", "asyncFn", "v", "revalidationCallbacks", "onRevalidate", "callback", "callbacks", "cb", "handleActionRequest", "req", "actionId", "sessionCsrfToken", "skipCsrf", "requestCsrfToken", "getRegisteredActions", "useMutation", "mutationFn", "onSettled", "state", "mutate"]
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -41,8 +41,9 @@ function csrfMetaTag(token) {
|
|
|
41
41
|
const escaped = String(token).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
42
42
|
return `<meta name="what-csrf-token" content="${escaped}">`;
|
|
43
43
|
}
|
|
44
|
+
var _actionCounter = 0;
|
|
44
45
|
function generateActionId() {
|
|
45
|
-
const rand = typeof crypto !== "undefined" && crypto.getRandomValues ? Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("") :
|
|
46
|
+
const rand = typeof crypto !== "undefined" && crypto.getRandomValues ? Array.from(crypto.getRandomValues(new Uint8Array(6)), (b) => b.toString(16).padStart(2, "0")).join("") : `c${(++_actionCounter).toString(36)}_${Date.now().toString(36)}`;
|
|
46
47
|
return `a_${rand}`;
|
|
47
48
|
}
|
|
48
49
|
function action(fn, options = {}) {
|
|
@@ -339,6 +340,46 @@ function useMutation(mutationFn, options = {}) {
|
|
|
339
340
|
}
|
|
340
341
|
|
|
341
342
|
// packages/server/src/index.js
|
|
343
|
+
var _ssrErrors = [];
|
|
344
|
+
var MAX_SSR_ERRORS = 50;
|
|
345
|
+
function _collectSSRError(error, context = {}) {
|
|
346
|
+
const entry = {
|
|
347
|
+
code: error.code || "ERR_SSR_RENDER",
|
|
348
|
+
message: error.message || String(error),
|
|
349
|
+
component: context.component || null,
|
|
350
|
+
timestamp: Date.now()
|
|
351
|
+
};
|
|
352
|
+
if (_isDevMode) {
|
|
353
|
+
entry.suggestion = error.suggestion || null;
|
|
354
|
+
entry.stack = error.stack?.split("\n").slice(0, 5).join("\n") || null;
|
|
355
|
+
}
|
|
356
|
+
_ssrErrors.push(entry);
|
|
357
|
+
if (_ssrErrors.length > MAX_SSR_ERRORS) _ssrErrors.shift();
|
|
358
|
+
}
|
|
359
|
+
function _resetSSRErrors() {
|
|
360
|
+
_ssrErrors = [];
|
|
361
|
+
}
|
|
362
|
+
function serializeSSRErrors() {
|
|
363
|
+
if (_ssrErrors.length === 0) return "";
|
|
364
|
+
const payload = _isDevMode ? _ssrErrors : _ssrErrors.map((e) => ({ code: e.code, component: e.component }));
|
|
365
|
+
const json = JSON.stringify(payload).replace(/<\//g, "<\\/");
|
|
366
|
+
return `<script type="application/json" data-what-ssr-errors>${json}<\/script>`;
|
|
367
|
+
}
|
|
368
|
+
function hydrateSSRErrors() {
|
|
369
|
+
if (typeof document === "undefined") return [];
|
|
370
|
+
const el = document.querySelector("script[data-what-ssr-errors]");
|
|
371
|
+
if (!el) return [];
|
|
372
|
+
try {
|
|
373
|
+
const errors = JSON.parse(el.textContent);
|
|
374
|
+
el.remove();
|
|
375
|
+
return errors;
|
|
376
|
+
} catch {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function getSSRErrors() {
|
|
381
|
+
return _ssrErrors.slice();
|
|
382
|
+
}
|
|
342
383
|
var _hydrationIdCounter = 0;
|
|
343
384
|
function resetHydrationId() {
|
|
344
385
|
_hydrationIdCounter = 0;
|
|
@@ -348,6 +389,7 @@ function nextHydrationId() {
|
|
|
348
389
|
}
|
|
349
390
|
function renderToHydratableString(vnode) {
|
|
350
391
|
resetHydrationId();
|
|
392
|
+
_resetSSRErrors();
|
|
351
393
|
return _renderHydratable(vnode);
|
|
352
394
|
}
|
|
353
395
|
function _renderHydratable(vnode) {
|
|
@@ -362,7 +404,8 @@ function _renderHydratable(vnode) {
|
|
|
362
404
|
try {
|
|
363
405
|
return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
|
|
364
406
|
} catch (e) {
|
|
365
|
-
|
|
407
|
+
_collectSSRError(e, { component: "reactive-function" });
|
|
408
|
+
if (_isDevMode) {
|
|
366
409
|
console.warn("[what-server] Error rendering reactive function in SSR:", e.message);
|
|
367
410
|
}
|
|
368
411
|
return "<!--$--><!--/$-->";
|
|
@@ -373,9 +416,19 @@ function _renderHydratable(vnode) {
|
|
|
373
416
|
}
|
|
374
417
|
if (typeof vnode.tag === "function") {
|
|
375
418
|
const hkId = nextHydrationId();
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
419
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || "Anonymous";
|
|
420
|
+
try {
|
|
421
|
+
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
422
|
+
const html = _renderHydratable(result);
|
|
423
|
+
return injectHydrationKey(html, hkId);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
_collectSSRError(e, { component: componentName });
|
|
426
|
+
if (_isDevMode) {
|
|
427
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in SSR:`, e.message);
|
|
428
|
+
return `<!--ssr-error:${escapeHtml(componentName)}-->`;
|
|
429
|
+
}
|
|
430
|
+
return `<!--ssr-error-->`;
|
|
431
|
+
}
|
|
379
432
|
}
|
|
380
433
|
const { tag, props, children } = vnode;
|
|
381
434
|
const attrs = renderAttrs(props || {});
|
|
@@ -407,7 +460,8 @@ function renderToString(vnode) {
|
|
|
407
460
|
try {
|
|
408
461
|
return renderToString(vnode());
|
|
409
462
|
} catch (e) {
|
|
410
|
-
|
|
463
|
+
_collectSSRError(e, { component: "reactive-function" });
|
|
464
|
+
if (_isDevMode) {
|
|
411
465
|
console.warn("[what-server] Error rendering reactive function in SSR:", e.message);
|
|
412
466
|
}
|
|
413
467
|
return "";
|
|
@@ -417,8 +471,18 @@ function renderToString(vnode) {
|
|
|
417
471
|
return vnode.map(renderToString).join("");
|
|
418
472
|
}
|
|
419
473
|
if (typeof vnode.tag === "function") {
|
|
420
|
-
const
|
|
421
|
-
|
|
474
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || "Anonymous";
|
|
475
|
+
try {
|
|
476
|
+
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
477
|
+
return renderToString(result);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
_collectSSRError(e, { component: componentName });
|
|
480
|
+
if (_isDevMode) {
|
|
481
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in SSR:`, e.message);
|
|
482
|
+
return `<!-- SSR Error in ${escapeHtml(componentName)}: ${escapeHtml(e.message)} -->`;
|
|
483
|
+
}
|
|
484
|
+
return `<!-- SSR Error -->`;
|
|
485
|
+
}
|
|
422
486
|
}
|
|
423
487
|
const { tag, props, children } = vnode;
|
|
424
488
|
const attrs = renderAttrs(props || {});
|
|
@@ -442,7 +506,8 @@ async function* renderToStream(vnode) {
|
|
|
442
506
|
try {
|
|
443
507
|
yield* renderToStream(vnode());
|
|
444
508
|
} catch (e) {
|
|
445
|
-
|
|
509
|
+
_collectSSRError(e, { component: "reactive-function" });
|
|
510
|
+
if (_isDevMode) {
|
|
446
511
|
console.warn("[what-server] Error rendering reactive function in stream SSR:", e.message);
|
|
447
512
|
}
|
|
448
513
|
}
|
|
@@ -455,15 +520,17 @@ async function* renderToStream(vnode) {
|
|
|
455
520
|
return;
|
|
456
521
|
}
|
|
457
522
|
if (typeof vnode.tag === "function") {
|
|
523
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || "Anonymous";
|
|
458
524
|
try {
|
|
459
525
|
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
460
526
|
const resolved = result instanceof Promise ? await result : result;
|
|
461
527
|
yield* renderToStream(resolved);
|
|
462
528
|
} catch (e) {
|
|
463
|
-
|
|
464
|
-
|
|
529
|
+
_collectSSRError(e, { component: componentName });
|
|
530
|
+
if (_isDevMode) {
|
|
531
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in stream SSR:`, e.message);
|
|
465
532
|
}
|
|
466
|
-
yield _isDevMode ? `<!-- SSR Error: ${escapeHtml(e.message || "Component error")} -->` : `<!-- SSR Error -->`;
|
|
533
|
+
yield _isDevMode ? `<!-- SSR Error in ${escapeHtml(componentName)}: ${escapeHtml(e.message || "Component error")} -->` : `<!-- SSR Error -->`;
|
|
467
534
|
}
|
|
468
535
|
return;
|
|
469
536
|
}
|
|
@@ -493,6 +560,7 @@ function definePage(config) {
|
|
|
493
560
|
};
|
|
494
561
|
}
|
|
495
562
|
function generateStaticPage(page, data = {}) {
|
|
563
|
+
_resetSSRErrors();
|
|
496
564
|
const vnode = page.component(data);
|
|
497
565
|
const html = renderToString(vnode);
|
|
498
566
|
const islands = page.islands || [];
|
|
@@ -503,10 +571,11 @@ function generateStaticPage(page, data = {}) {
|
|
|
503
571
|
islands,
|
|
504
572
|
scripts: page.mode === "static" ? [] : page.scripts || [],
|
|
505
573
|
styles: page.styles || [],
|
|
506
|
-
mode: page.mode
|
|
574
|
+
mode: page.mode,
|
|
575
|
+
ssrErrors: serializeSSRErrors()
|
|
507
576
|
});
|
|
508
577
|
}
|
|
509
|
-
function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
|
|
578
|
+
function wrapDocument({ title, meta, body, islands, scripts, styles, mode, ssrErrors = "" }) {
|
|
510
579
|
const metaTags = Object.entries(meta).map(([name, content]) => `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`).join("\n ");
|
|
511
580
|
const styleTags = styles.map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join("\n ");
|
|
512
581
|
const islandScript = islands.length > 0 ? `
|
|
@@ -528,6 +597,7 @@ function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
|
|
|
528
597
|
</head>
|
|
529
598
|
<body>
|
|
530
599
|
<div id="app">${body}</div>
|
|
600
|
+
${ssrErrors}
|
|
531
601
|
${islandScript}
|
|
532
602
|
${scriptTags}
|
|
533
603
|
${clientScript}
|
|
@@ -611,12 +681,15 @@ export {
|
|
|
611
681
|
generateCsrfToken,
|
|
612
682
|
generateStaticPage,
|
|
613
683
|
getRegisteredActions,
|
|
684
|
+
getSSRErrors,
|
|
614
685
|
handleActionRequest,
|
|
686
|
+
hydrateSSRErrors,
|
|
615
687
|
invalidatePath,
|
|
616
688
|
onRevalidate,
|
|
617
689
|
renderToHydratableString,
|
|
618
690
|
renderToStream,
|
|
619
691
|
renderToString,
|
|
692
|
+
serializeSSRErrors,
|
|
620
693
|
server,
|
|
621
694
|
useAction,
|
|
622
695
|
useFormAction,
|