full site update

This commit is contained in:
2025-07-24 18:46:24 +02:00
parent bfe2b90d8d
commit 37a6e0ab31
6912 changed files with 540482 additions and 361712 deletions

View File

@@ -1,2 +1,3 @@
export * from './shared.js';
export declare function defineAction(): void;
export declare function getActionContext(): void;

View File

@@ -2,6 +2,10 @@ export * from "./shared.js";
function defineAction() {
throw new Error("[astro:action] `defineAction()` unexpectedly used on the client.");
}
function getActionContext() {
throw new Error("[astro:action] `getActionContext()` unexpectedly used on the client.");
}
export {
defineAction
defineAction,
getActionContext
};

View File

@@ -1,8 +0,0 @@
import type { ZodType } from 'zod';
import type { ActionAccept, ActionClient } from './server.js';
/**
* Get server-side action based on the route path.
* Imports from the virtual module `astro:internal-actions`, which maps to
* the user's `src/actions/index.ts` file at build-time.
*/
export declare function getAction(path: string): Promise<ActionClient<unknown, ActionAccept, ZodType>>;

View File

@@ -1,29 +0,0 @@
import { ActionNotFoundError } from "../../../core/errors/errors-data.js";
import { AstroError } from "../../../core/errors/errors.js";
async function getAction(path) {
const pathKeys = path.replace(/^.*\/_actions\//, "").split(".").map((key) => decodeURIComponent(key));
let { server: actionLookup } = await import("astro:internal-actions");
if (actionLookup == null || !(typeof actionLookup === "object")) {
throw new TypeError(
`Expected \`server\` export in actions file to be an object. Received ${typeof actionLookup}.`
);
}
for (const key of pathKeys) {
if (!(key in actionLookup)) {
throw new AstroError({
...ActionNotFoundError,
message: ActionNotFoundError.message(pathKeys.join("."))
});
}
actionLookup = actionLookup[key];
}
if (typeof actionLookup !== "function") {
throw new TypeError(
`Expected handler for action ${pathKeys.join(".")} to be a function. Received ${typeof actionLookup}.`
);
}
return actionLookup;
}
export {
getAction
};

View File

@@ -1,6 +1,7 @@
import { z } from 'zod';
import type { APIContext } from '../../../types/public/index.js';
import { type ActionAPIContext, type ErrorInferenceObject, type MaybePromise } from '../utils.js';
import { type SafeResult } from './shared.js';
import { deserializeActionResult, type SafeResult, type SerializedActionResult, serializeActionResult } from './shared.js';
export * from './shared.js';
export type ActionAccept = 'form' | 'json';
export type ActionHandler<TInputSchema, TOutput> = TInputSchema extends z.ZodType ? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput> : (input: any, context: ActionAPIContext) => MaybePromise<TOutput>;
@@ -18,3 +19,32 @@ export declare function defineAction<TOutput, TAccept extends ActionAccept | und
}): ActionClient<TOutput, TAccept, TInputSchema> & string;
/** Transform form data to an object based on a Zod schema. */
export declare function formDataToObject<T extends z.AnyZodObject>(formData: FormData, schema: T): Record<string, unknown>;
export type AstroActionContext = {
/** Information about an incoming action request. */
action?: {
/** Whether an action was called using an RPC function or by using an HTML form action. */
calledFrom: 'rpc' | 'form';
/** The name of the action. Useful to track the source of an action result during a redirect. */
name: string;
/** Programmatically call the action to get the result. */
handler: () => Promise<SafeResult<any, any>>;
};
/**
* Manually set the action result accessed via `getActionResult()`.
* Calling this function from middleware will disable Astro's own action result handling.
*/
setActionResult(actionName: string, actionResult: SerializedActionResult): void;
/**
* Serialize an action result to stored in a cookie or session.
* Also used to pass a result to Astro templates via `setActionResult()`.
*/
serializeActionResult: typeof serializeActionResult;
/**
* Deserialize an action result to access data and error objects.
*/
deserializeActionResult: typeof deserializeActionResult;
};
/**
* Access information about Action requests from middleware.
*/
export declare function getActionContext(context: APIContext): AstroActionContext;

View File

@@ -1,10 +1,24 @@
import { z } from "zod";
import { ActionCalledFromServerError } from "../../../core/errors/errors-data.js";
import { shouldAppendForwardSlash } from "../../../core/build/util.js";
import { AstroError } from "../../../core/errors/errors.js";
import { ActionCalledFromServerError } from "../../../core/errors/errors-data.js";
import { removeTrailingForwardSlash } from "../../../core/path.js";
import { apiContextRoutesSymbol } from "../../../core/render-context.js";
import { ACTION_RPC_ROUTE_PATTERN } from "../../consts.js";
import {
ACTION_API_CONTEXT_SYMBOL,
formContentTypes,
hasContentType,
isActionAPIContext
} from "../utils.js";
import { ActionError, ActionInputError, callSafely } from "./shared.js";
import {
ACTION_QUERY_PARAMS,
ActionError,
ActionInputError,
callSafely,
deserializeActionResult,
serializeActionResult
} from "./shared.js";
export * from "./shared.js";
function defineAction({
accept,
@@ -122,7 +136,82 @@ function unwrapBaseObjectSchema(schema, unparsedInput) {
}
return schema;
}
function getActionContext(context) {
const callerInfo = getCallerInfo(context);
const actionResultAlreadySet = Boolean(context.locals._actionPayload);
let action = void 0;
if (callerInfo && context.request.method === "POST" && !actionResultAlreadySet) {
action = {
calledFrom: callerInfo.from,
name: callerInfo.name,
handler: async () => {
const pipeline = Reflect.get(context, apiContextRoutesSymbol);
const callerInfoName = shouldAppendForwardSlash(
pipeline.manifest.trailingSlash,
pipeline.manifest.buildFormat
) ? removeTrailingForwardSlash(callerInfo.name) : callerInfo.name;
const baseAction = await pipeline.getAction(callerInfoName);
let input;
try {
input = await parseRequestBody(context.request);
} catch (e) {
if (e instanceof TypeError) {
return { data: void 0, error: new ActionError({ code: "UNSUPPORTED_MEDIA_TYPE" }) };
}
throw e;
}
const omitKeys = ["props", "getActionResult", "callAction", "redirect"];
const actionAPIContext = Object.create(
Object.getPrototypeOf(context),
Object.fromEntries(
Object.entries(Object.getOwnPropertyDescriptors(context)).filter(
([key]) => !omitKeys.includes(key)
)
)
);
Reflect.set(actionAPIContext, ACTION_API_CONTEXT_SYMBOL, true);
const handler = baseAction.bind(actionAPIContext);
return handler(input);
}
};
}
function setActionResult(actionName, actionResult) {
context.locals._actionPayload = {
actionResult,
actionName
};
}
return {
action,
setActionResult,
serializeActionResult,
deserializeActionResult
};
}
function getCallerInfo(ctx) {
if (ctx.routePattern === ACTION_RPC_ROUTE_PATTERN) {
return { from: "rpc", name: ctx.url.pathname.replace(/^.*\/_actions\//, "") };
}
const queryParam = ctx.url.searchParams.get(ACTION_QUERY_PARAMS.actionName);
if (queryParam) {
return { from: "form", name: queryParam };
}
return void 0;
}
async function parseRequestBody(request) {
const contentType = request.headers.get("content-type");
const contentLength = request.headers.get("Content-Length");
if (!contentType) return void 0;
if (hasContentType(contentType, formContentTypes)) {
return await request.clone().formData();
}
if (hasContentType(contentType, ["application/json"])) {
return contentLength === "0" ? void 0 : await request.clone().json();
}
throw new TypeError("Unsupported content type");
}
export {
defineAction,
formDataToObject
formDataToObject,
getActionContext
};

View File

@@ -1,12 +1,14 @@
import type { z } from 'zod';
import type { ErrorInferenceObject, MaybePromise, ActionAPIContext as _ActionAPIContext } from '../utils.js';
import { AstroError } from '../../../core/errors/errors.js';
import { appendForwardSlash as _appendForwardSlash } from '../../../core/path.js';
import type { ActionAPIContext as _ActionAPIContext, ErrorInferenceObject, MaybePromise } from '../utils.js';
export type ActionAPIContext = _ActionAPIContext;
export declare const ACTION_QUERY_PARAMS: {
actionName: string;
actionPayload: string;
actionRedirect: string;
};
export declare const ACTION_ERROR_CODES: readonly ["BAD_REQUEST", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "TIMEOUT", "CONFLICT", "PRECONDITION_FAILED", "PAYLOAD_TOO_LARGE", "UNSUPPORTED_MEDIA_TYPE", "UNPROCESSABLE_CONTENT", "TOO_MANY_REQUESTS", "CLIENT_CLOSED_REQUEST", "INTERNAL_SERVER_ERROR"];
export declare const appendForwardSlash: typeof _appendForwardSlash;
export declare const ACTION_ERROR_CODES: readonly ["BAD_REQUEST", "UNAUTHORIZED", "PAYMENT_REQUIRED", "FORBIDDEN", "NOT_FOUND", "METHOD_NOT_ALLOWED", "NOT_ACCEPTABLE", "PROXY_AUTHENTICATION_REQUIRED", "REQUEST_TIMEOUT", "CONFLICT", "GONE", "LENGTH_REQUIRED", "PRECONDITION_FAILED", "CONTENT_TOO_LARGE", "URI_TOO_LONG", "UNSUPPORTED_MEDIA_TYPE", "RANGE_NOT_SATISFIABLE", "EXPECTATION_FAILED", "MISDIRECTED_REQUEST", "UNPROCESSABLE_CONTENT", "LOCKED", "FAILED_DEPENDENCY", "TOO_EARLY", "UPGRADE_REQUIRED", "PRECONDITION_REQUIRED", "TOO_MANY_REQUESTS", "REQUEST_HEADER_FIELDS_TOO_LARGE", "UNAVAILABLE_FOR_LEGAL_REASONS", "INTERNAL_SERVER_ERROR", "NOT_IMPLEMENTED", "BAD_GATEWAY", "SERVICE_UNAVAILABLE", "GATEWAY_TIMEOUT", "HTTP_VERSION_NOT_SUPPORTED", "VARIANT_ALSO_NEGOTIATES", "INSUFFICIENT_STORAGE", "LOOP_DETECTED", "NETWORK_AUTHENTICATION_REQUIRED"];
export type ActionErrorCode = (typeof ACTION_ERROR_CODES)[number];
export declare class ActionError<_T extends ErrorInferenceObject = ErrorInferenceObject> extends Error {
type: string;
@@ -55,3 +57,4 @@ export type SerializedActionResult = {
};
export declare function serializeActionResult(res: SafeResult<any, any>): SerializedActionResult;
export declare function deserializeActionResult(res: SerializedActionResult): SafeResult<any, any>;
export declare function astroCalledServerError(): AstroError;

View File

@@ -1,40 +1,95 @@
import { parse as devalueParse, stringify as devalueStringify } from "devalue";
import { REDIRECT_STATUS_CODES } from "../../../core/constants.js";
import { ActionsReturnedInvalidDataError } from "../../../core/errors/errors-data.js";
import { AstroError } from "../../../core/errors/errors.js";
import {
ActionCalledFromServerError,
ActionsReturnedInvalidDataError
} from "../../../core/errors/errors-data.js";
import { appendForwardSlash as _appendForwardSlash } from "../../../core/path.js";
import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from "../../consts.js";
const ACTION_QUERY_PARAMS = _ACTION_QUERY_PARAMS;
const appendForwardSlash = _appendForwardSlash;
const ACTION_ERROR_CODES = [
"BAD_REQUEST",
"UNAUTHORIZED",
"PAYMENT_REQUIRED",
"FORBIDDEN",
"NOT_FOUND",
"TIMEOUT",
"METHOD_NOT_ALLOWED",
"NOT_ACCEPTABLE",
"PROXY_AUTHENTICATION_REQUIRED",
"REQUEST_TIMEOUT",
"CONFLICT",
"GONE",
"LENGTH_REQUIRED",
"PRECONDITION_FAILED",
"PAYLOAD_TOO_LARGE",
"CONTENT_TOO_LARGE",
"URI_TOO_LONG",
"UNSUPPORTED_MEDIA_TYPE",
"RANGE_NOT_SATISFIABLE",
"EXPECTATION_FAILED",
"MISDIRECTED_REQUEST",
"UNPROCESSABLE_CONTENT",
"LOCKED",
"FAILED_DEPENDENCY",
"TOO_EARLY",
"UPGRADE_REQUIRED",
"PRECONDITION_REQUIRED",
"TOO_MANY_REQUESTS",
"CLIENT_CLOSED_REQUEST",
"INTERNAL_SERVER_ERROR"
"REQUEST_HEADER_FIELDS_TOO_LARGE",
"UNAVAILABLE_FOR_LEGAL_REASONS",
"INTERNAL_SERVER_ERROR",
"NOT_IMPLEMENTED",
"BAD_GATEWAY",
"SERVICE_UNAVAILABLE",
"GATEWAY_TIMEOUT",
"HTTP_VERSION_NOT_SUPPORTED",
"VARIANT_ALSO_NEGOTIATES",
"INSUFFICIENT_STORAGE",
"LOOP_DETECTED",
"NETWORK_AUTHENTICATION_REQUIRED"
];
const codeToStatusMap = {
// Implemented from tRPC error code table
// https://trpc.io/docs/server/error-handling#error-codes
// Implemented from IANA HTTP Status Code Registry
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
TIMEOUT: 405,
METHOD_NOT_ALLOWED: 405,
NOT_ACCEPTABLE: 406,
PROXY_AUTHENTICATION_REQUIRED: 407,
REQUEST_TIMEOUT: 408,
CONFLICT: 409,
GONE: 410,
LENGTH_REQUIRED: 411,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
CONTENT_TOO_LARGE: 413,
URI_TOO_LONG: 414,
UNSUPPORTED_MEDIA_TYPE: 415,
RANGE_NOT_SATISFIABLE: 416,
EXPECTATION_FAILED: 417,
MISDIRECTED_REQUEST: 421,
UNPROCESSABLE_CONTENT: 422,
LOCKED: 423,
FAILED_DEPENDENCY: 424,
TOO_EARLY: 425,
UPGRADE_REQUIRED: 426,
PRECONDITION_REQUIRED: 428,
TOO_MANY_REQUESTS: 429,
CLIENT_CLOSED_REQUEST: 499,
INTERNAL_SERVER_ERROR: 500
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
HTTP_VERSION_NOT_SUPPORTED: 505,
VARIANT_ALSO_NEGOTIATES: 506,
INSUFFICIENT_STORAGE: 507,
LOOP_DETECTED: 508,
NETWORK_AUTHENTICATION_REQUIRED: 511
};
const statusToCodeMap = Object.entries(codeToStatusMap).reduce(
// reverse the key-value pairs
@@ -126,14 +181,24 @@ function serializeActionResult(res) {
if (import.meta.env?.DEV) {
actionResultErrorStack.set(res.error.stack);
}
let body2;
if (res.error instanceof ActionInputError) {
body2 = {
type: res.error.type,
issues: res.error.issues,
fields: res.error.fields
};
} else {
body2 = {
...res.error,
message: res.error.message
};
}
return {
type: "error",
status: res.error.status,
contentType: "application/json",
body: JSON.stringify({
...res.error,
message: res.error.message
})
body: JSON.stringify(body2)
};
}
if (res.data === void 0) {
@@ -212,11 +277,16 @@ const actionResultErrorStack = /* @__PURE__ */ function actionResultErrorStackFn
}
};
}();
function astroCalledServerError() {
return new AstroError(ActionCalledFromServerError);
}
export {
ACTION_ERROR_CODES,
ACTION_QUERY_PARAMS,
ActionError,
ActionInputError,
appendForwardSlash,
astroCalledServerError,
callSafely,
deserializeActionResult,
getActionQueryString,