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
JavaScript
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
};