UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

128 lines (127 loc) 4.11 kB
import { shouldAppendTrailingSlash } from "virtual:astro:actions/options"; import { internalFetchHeaders } from "virtual:astro:adapter-config/client"; import { ACTION_QUERY_PARAMS, ActionError, appendForwardSlash, astroCalledServerError, deserializeActionResult, getActionQueryString } from "./shared.js"; export * from "virtual:astro:actions/runtime"; const apiContextRoutesSymbol = Symbol.for("context.routes"); const ENCODED_DOT = "%2E"; function toActionProxy(actionCallback = {}, aggregatedPath = "") { return new Proxy(actionCallback, { get(target, objKey) { if (target.hasOwnProperty(objKey) || typeof objKey === "symbol") { return target[objKey]; } const path = aggregatedPath + encodeURIComponent(objKey.toString()).replaceAll(".", ENCODED_DOT); function action(param) { return handleAction(param, path, this); } Object.assign(action, { queryString: getActionQueryString(path), toString: () => action.queryString, // redefine prototype methods as the object's own property, not the prototype's bind: action.bind, valueOf: () => action.valueOf, // Progressive enhancement info for React. $$FORM_ACTION: function() { const searchParams = new URLSearchParams(action.toString()); return { method: "POST", // `name` creates a hidden input. // It's unused by Astro, but we can't turn this off. // At least use a name that won't conflict with a user's formData. name: "_astroAction", action: "?" + searchParams.toString() }; }, // Note: `orThrow` does not have progressive enhancement info. // If you want to throw exceptions, // you must handle those exceptions with client JS. async orThrow(param) { const { data, error } = await handleAction(param, path, this); if (error) throw error; return data; } }); return toActionProxy(action, path + "."); } }); } function _getActionPath(toString) { let path = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/_actions/${new URLSearchParams(toString()).get(ACTION_QUERY_PARAMS.actionName)}`; if (shouldAppendTrailingSlash) { path = appendForwardSlash(path); } return path; } function getActionPath(action) { return _getActionPath(action.toString); } async function handleAction(param, path, context) { if (import.meta.env.SSR && context) { const pipeline = Reflect.get(context, apiContextRoutesSymbol); if (!pipeline) { throw astroCalledServerError(); } const action = await pipeline.getAction(path); if (!action) throw new Error(`Action not found: ${path}`); return action.bind(context)(param); } const headers = new Headers(); headers.set("Accept", "application/json"); for (const [key, value] of Object.entries(internalFetchHeaders)) { headers.set(key, value); } let body = param; if (!(body instanceof FormData)) { try { body = JSON.stringify(param); } catch (e) { throw new ActionError({ code: "BAD_REQUEST", message: `Failed to serialize request body to JSON. Full error: ${e.message}` }); } if (body) { headers.set("Content-Type", "application/json"); } else { headers.set("Content-Length", "0"); } } const rawResult = await fetch( _getActionPath(() => getActionQueryString(path)), { method: "POST", body, headers } ); if (rawResult.status === 204) { return deserializeActionResult({ type: "empty", status: 204 }); } const bodyText = await rawResult.text(); if (rawResult.ok) { return deserializeActionResult({ type: "data", body: bodyText, status: 200, contentType: "application/json+devalue" }); } return deserializeActionResult({ type: "error", body: bodyText, status: rawResult.status, contentType: "application/json" }); } const actions = toActionProxy(); export { actions, getActionPath };