summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/many-mayflies-enjoy.md60
-rw-r--r--packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro2
-rw-r--r--packages/astro/src/@types/astro.ts32
-rw-r--r--packages/astro/src/actions/runtime/middleware.ts58
-rw-r--r--packages/astro/src/actions/runtime/route.ts8
-rw-r--r--packages/astro/src/actions/runtime/store.ts18
-rw-r--r--packages/astro/src/actions/runtime/utils.ts2
-rw-r--r--packages/astro/src/actions/runtime/virtual/client.ts4
-rw-r--r--packages/astro/src/actions/runtime/virtual/server.ts36
-rw-r--r--packages/astro/src/actions/utils.ts13
-rw-r--r--packages/astro/src/core/errors/errors-data.ts30
-rw-r--r--packages/astro/src/core/middleware/index.ts5
-rw-r--r--packages/astro/src/core/render-context.ts6
-rw-r--r--packages/astro/templates/actions.mjs13
-rw-r--r--packages/astro/test/fixtures/actions/src/pages/subscribe.astro2
-rw-r--r--packages/astro/test/types/call-action.ts51
16 files changed, 256 insertions, 84 deletions
diff --git a/.changeset/many-mayflies-enjoy.md b/.changeset/many-mayflies-enjoy.md
new file mode 100644
index 000000000..a1da048d7
--- /dev/null
+++ b/.changeset/many-mayflies-enjoy.md
@@ -0,0 +1,60 @@
+---
+'astro': patch
+---
+
+Removes async local storage dependency from Astro Actions. This allows Actions to run in Cloudflare and Stackblitz without opt-in flags or other configuration.
+
+This also introduces a new convention for calling actions from server code. Instead of calling actions directly, you must wrap function calls with the new `Astro.callAction()` utility.
+
+> `callAction()` is meant to _trigger_ an action from server code. `getActionResult()` usage with form submissions remains unchanged.
+
+```astro
+---
+import { actions } from 'astro:actions';
+
+const result = await Astro.callAction(actions.searchPosts, {
+ searchTerm: Astro.url.searchParams.get('search'),
+});
+---
+
+{result.data && (
+ {/* render the results */}
+)}
+```
+
+## Migration
+
+If you call actions directly from server code, update function calls to use the `Astro.callAction()` wrapper for pages and `context.callAction()` for endpoints:
+
+```diff
+---
+import { actions } from 'astro:actions';
+
+- const result = await actions.searchPosts({ searchTerm: 'test' });
++ const result = await Astro.callAction(actions.searchPosts, { searchTerm: 'test' });
+---
+```
+
+If you deploy with Cloudflare and added [the `nodejs_compat` or `nodejs_als` flags](https://developers.cloudflare.com/workers/runtime-apis/nodejs) for Actions, we recommend removing these:
+
+```diff
+compatibility_flags = [
+- "nodejs_compat",
+- "nodejs_als"
+]
+```
+
+You can also remove `node:async_hooks` from the `vite.ssr.external` option in your `astro.config` file:
+
+```diff
+// astro.config.mjs
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+- vite: {
+- ssr: {
+- external: ["node:async_hooks"]
+- }
+- }
+})
+```
diff --git a/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro
index 64deb7a46..fe97a8de1 100644
--- a/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro
+++ b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro
@@ -24,7 +24,7 @@ const post = await getEntry('blog', Astro.params.slug)!;
const { Content } = await post.render();
if (Astro.url.searchParams.has('like')) {
- await actions.blog.like({postId: post.id });
+ await Astro.callAction(actions.blog.like.orThrow, {postId: post.id});
}
const comment = Astro.getActionResult(actions.blog.comment);
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index f83c44463..32d1cd30b 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -15,6 +15,7 @@ import type {
ActionAccept,
ActionClient,
ActionInputSchema,
+ ActionReturnType,
} from '../actions/runtime/virtual/server.js';
import type { RemotePattern } from '../assets/utils/remotePattern.js';
import type { AssetsPrefix, SSRManifest, SerializedSSRManifest } from '../core/app/types.js';
@@ -262,6 +263,21 @@ export interface AstroGlobal<
* ```
*/
getActionResult: AstroSharedContext['getActionResult'];
+ /**
+ * Call an Action directly from an Astro page or API endpoint.
+ * Expects the action function as the first parameter,
+ * and the type-safe action input as the second parameter.
+ * Returns a Promise with the action result.
+ *
+ * Example usage:
+ *
+ * ```typescript
+ * import { actions } from 'astro:actions';
+ *
+ * const result = await Astro.callAction(actions.getPost, { postId: 'test' });
+ * ```
+ */
+ callAction: AstroSharedContext['callAction'];
/** Redirect to another page
*
* Example usage:
@@ -2741,7 +2757,21 @@ interface AstroSharedContext<
TAction extends ActionClient<unknown, TAccept, TInputSchema>,
>(
action: TAction
- ) => Awaited<ReturnType<TAction>> | undefined;
+ ) => ActionReturnType<TAction> | undefined;
+ /**
+ * Call action handler from the server.
+ */
+ callAction: <
+ TAccept extends ActionAccept,
+ TInputSchema extends ActionInputSchema<TAccept>,
+ TOutput,
+ TAction extends
+ | ActionClient<TOutput, TAccept, TInputSchema>
+ | ActionClient<TOutput, TAccept, TInputSchema>['orThrow'],
+ >(
+ action: TAction,
+ input: Parameters<TAction>[0]
+ ) => Promise<ActionReturnType<TAction>>;
/**
* Route parameters for this request if this is a dynamic route.
*/
diff --git a/packages/astro/src/actions/runtime/middleware.ts b/packages/astro/src/actions/runtime/middleware.ts
index f916691b8..1e5231218 100644
--- a/packages/astro/src/actions/runtime/middleware.ts
+++ b/packages/astro/src/actions/runtime/middleware.ts
@@ -6,13 +6,13 @@ import {
} from '../../core/errors/errors-data.js';
import { AstroError } from '../../core/errors/errors.js';
import { defineMiddleware } from '../../core/middleware/index.js';
-import { ApiContextStorage } from './store.js';
import { formContentTypes, getAction, hasContentType } from './utils.js';
import { getActionQueryString } from './virtual/shared.js';
export type Locals = {
_actionsInternal: {
getActionResult: APIContext['getActionResult'];
+ callAction: APIContext['callAction'];
actionResult?: ReturnType<APIContext['getActionResult']>;
};
};
@@ -24,7 +24,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
// See https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md#ctxrewrite
// `_actionsInternal` is the same for every page,
// so short circuit if already defined.
- if (locals._actionsInternal) return ApiContextStorage.run(context, () => next());
+ if (locals._actionsInternal) {
+ // Re-bind `callAction` with the new API context
+ locals._actionsInternal.callAction = createCallAction(context);
+ return next();
+ }
// Heuristic: If body is null, Astro might've reset this for prerendering.
// Stub with warning when `getActionResult()` is used.
@@ -59,8 +63,8 @@ async function handlePost({
}: { context: APIContext; next: MiddlewareNext; actionName: string }) {
const { request } = context;
- const action = await getAction(actionName);
- if (!action) {
+ const baseAction = await getAction(actionName);
+ if (!baseAction) {
throw new AstroError({
...ActionQueryStringInvalidError,
message: ActionQueryStringInvalidError.message(actionName),
@@ -72,12 +76,13 @@ async function handlePost({
if (contentType && hasContentType(contentType, formContentTypes)) {
formData = await request.clone().formData();
}
- const actionResult = await ApiContextStorage.run(context, () => action(formData));
+ const action = baseAction.bind(context);
+ const actionResult = await action(formData);
return handleResult({ context, next, actionName, actionResult });
}
-function handleResult({
+async function handleResult({
context,
next,
actionName,
@@ -90,22 +95,21 @@ function handleResult({
}
return actionResult;
},
+ callAction: createCallAction(context),
actionResult,
};
const locals = context.locals as Locals;
Object.defineProperty(locals, '_actionsInternal', { writable: false, value: actionsInternal });
- return ApiContextStorage.run(context, async () => {
- const response = await next();
- if (actionResult.error) {
- return new Response(response.body, {
- status: actionResult.error.status,
- statusText: actionResult.error.type,
- headers: response.headers,
- });
- }
- return response;
- });
+ const response = await next();
+ if (actionResult.error) {
+ return new Response(response.body, {
+ status: actionResult.error.status,
+ statusText: actionResult.error.type,
+ headers: response.headers,
+ });
+ }
+ return response;
}
async function handlePostLegacy({ context, next }: { context: APIContext; next: MiddlewareNext }) {
@@ -127,15 +131,16 @@ async function handlePostLegacy({ context, next }: { context: APIContext; next:
const actionName = formData.get('_astroAction') as string;
if (!actionName) return nextWithLocalsStub(next, context);
- const action = await getAction(actionName);
- if (!action) {
+ const baseAction = await getAction(actionName);
+ if (!baseAction) {
throw new AstroError({
...ActionQueryStringInvalidError,
message: ActionQueryStringInvalidError.message(actionName),
});
}
- const actionResult = await ApiContextStorage.run(context, () => action(formData));
+ const action = baseAction.bind(context);
+ const actionResult = await action(formData);
return handleResult({ context, next, actionName, actionResult });
}
@@ -150,9 +155,10 @@ function nextWithStaticStub(next: MiddlewareNext, context: APIContext) {
);
return undefined;
},
+ callAction: createCallAction(context),
},
});
- return ApiContextStorage.run(context, () => next());
+ return next();
}
function nextWithLocalsStub(next: MiddlewareNext, context: APIContext) {
@@ -160,7 +166,15 @@ function nextWithLocalsStub(next: MiddlewareNext, context: APIContext) {
writable: false,
value: {
getActionResult: () => undefined,
+ callAction: createCallAction(context),
},
});
- return ApiContextStorage.run(context, () => next());
+ return next();
+}
+
+function createCallAction(context: APIContext): APIContext['callAction'] {
+ return (baseAction, input) => {
+ const action = baseAction.bind(context);
+ return action(input) as any;
+ };
}
diff --git a/packages/astro/src/actions/runtime/route.ts b/packages/astro/src/actions/runtime/route.ts
index a1e711b18..33467a4b7 100644
--- a/packages/astro/src/actions/runtime/route.ts
+++ b/packages/astro/src/actions/runtime/route.ts
@@ -1,11 +1,10 @@
import type { APIRoute } from '../../@types/astro.js';
-import { ApiContextStorage } from './store.js';
import { formContentTypes, getAction, hasContentType } from './utils.js';
export const POST: APIRoute = async (context) => {
const { request, url } = context;
- const action = await getAction(url.pathname);
- if (!action) {
+ const baseAction = await getAction(url.pathname);
+ if (!baseAction) {
return new Response(null, { status: 404 });
}
const contentType = request.headers.get('Content-Type');
@@ -22,7 +21,8 @@ export const POST: APIRoute = async (context) => {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415
return new Response(null, { status: 415 });
}
- const result = await ApiContextStorage.run(context, () => action(args));
+ const action = baseAction.bind(context);
+ const result = await action(args);
if (result.error) {
return new Response(
JSON.stringify({
diff --git a/packages/astro/src/actions/runtime/store.ts b/packages/astro/src/actions/runtime/store.ts
deleted file mode 100644
index a1110cd70..000000000
--- a/packages/astro/src/actions/runtime/store.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AsyncLocalStorage } from 'node:async_hooks';
-import type { APIContext } from '../../@types/astro.js';
-import { AstroError } from '../../core/errors/errors.js';
-
-export type ActionAPIContext = Omit<APIContext, 'getActionResult' | 'props'>;
-export const ApiContextStorage = new AsyncLocalStorage<ActionAPIContext>();
-
-export function getApiContext(): ActionAPIContext {
- const context = ApiContextStorage.getStore();
- if (!context) {
- throw new AstroError({
- name: 'AstroActionError',
- message: 'Unable to get API context.',
- hint: 'If you attempted to call this action from server code, trying using `Astro.getActionResult()` instead.',
- });
- }
- return context;
-}
diff --git a/packages/astro/src/actions/runtime/utils.ts b/packages/astro/src/actions/runtime/utils.ts
index 45f6cdc61..4c1555cbd 100644
--- a/packages/astro/src/actions/runtime/utils.ts
+++ b/packages/astro/src/actions/runtime/utils.ts
@@ -1,4 +1,5 @@
import type { ZodType } from 'zod';
+import type { APIContext } from '../../@types/astro.js';
import type { ActionAccept, ActionClient } from './virtual/server.js';
export const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
@@ -11,6 +12,7 @@ export function hasContentType(contentType: string, expected: string[]) {
return expected.some((t) => type === t);
}
+export type ActionAPIContext = Omit<APIContext, 'getActionResult' | 'callAction' | 'props'>;
export type MaybePromise<T> = T | Promise<T>;
/**
diff --git a/packages/astro/src/actions/runtime/virtual/client.ts b/packages/astro/src/actions/runtime/virtual/client.ts
index 3c81e19cb..ddeba9a75 100644
--- a/packages/astro/src/actions/runtime/virtual/client.ts
+++ b/packages/astro/src/actions/runtime/virtual/client.ts
@@ -4,10 +4,6 @@ export function defineAction() {
throw new Error('[astro:action] `defineAction()` unexpectedly used on the client.');
}
-export function getApiContext() {
- throw new Error('[astro:action] `getApiContext()` unexpectedly used on the client.');
-}
-
export const z = new Proxy(
{},
{
diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts
index fb6a36c92..d0f70c40f 100644
--- a/packages/astro/src/actions/runtime/virtual/server.ts
+++ b/packages/astro/src/actions/runtime/virtual/server.ts
@@ -1,15 +1,13 @@
import { z } from 'zod';
-import { type ActionAPIContext, getApiContext as _getApiContext } from '../store.js';
-import type { ErrorInferenceObject, MaybePromise } from '../utils.js';
+import type { ErrorInferenceObject, MaybePromise, ActionAPIContext } from '../utils.js';
import { ActionError, ActionInputError, type SafeResult, callSafely } from './shared.js';
+import { AstroError } from '../../../core/errors/errors.js';
+import { ActionCalledFromServerError } from '../../../core/errors/errors-data.js';
export * from './shared.js';
export { z } from 'zod';
-/** @deprecated Access context from the second `handler()` parameter. */
-export const getApiContext = _getApiContext;
-
export type ActionAccept = 'form' | 'json';
export type ActionInputSchema<T extends ActionAccept | undefined> = T extends 'form'
? z.AnyZodObject | z.ZodType<FormData>
@@ -66,12 +64,20 @@ export function defineAction<
? getFormServerHandler(handler, inputSchema)
: getJsonServerHandler(handler, inputSchema);
- const safeServerHandler = async (unparsedInput: unknown) => {
- return callSafely(() => serverHandler(unparsedInput));
- };
+ async function safeServerHandler(this: ActionAPIContext, unparsedInput: unknown) {
+ if (typeof this === 'function') {
+ throw new AstroError(ActionCalledFromServerError);
+ }
+ return callSafely(() => serverHandler(unparsedInput, this));
+ }
Object.assign(safeServerHandler, {
- orThrow: serverHandler,
+ orThrow(this: ActionAPIContext, unparsedInput: unknown) {
+ if (typeof this === 'function') {
+ throw new AstroError(ActionCalledFromServerError);
+ }
+ return serverHandler(unparsedInput, this);
+ },
});
return safeServerHandler as ActionClient<TOutput, TAccept, TInputSchema> & string;
@@ -81,7 +87,7 @@ function getFormServerHandler<TOutput, TInputSchema extends ActionInputSchema<'f
handler: ActionHandler<TInputSchema, TOutput>,
inputSchema?: TInputSchema
) {
- return async (unparsedInput: unknown): Promise<Awaited<TOutput>> => {
+ return async (unparsedInput: unknown, context: ActionAPIContext): Promise<Awaited<TOutput>> => {
if (!(unparsedInput instanceof FormData)) {
throw new ActionError({
code: 'UNSUPPORTED_MEDIA_TYPE',
@@ -89,13 +95,13 @@ function getFormServerHandler<TOutput, TInputSchema extends ActionInputSchema<'f
});
}
- if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput, getApiContext());
+ if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput, context);
const parsed = await inputSchema.safeParseAsync(formDataToObject(unparsedInput, inputSchema));
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
- return await handler(parsed.data, getApiContext());
+ return await handler(parsed.data, context);
};
}
@@ -103,7 +109,7 @@ function getJsonServerHandler<TOutput, TInputSchema extends ActionInputSchema<'j
handler: ActionHandler<TInputSchema, TOutput>,
inputSchema?: TInputSchema
) {
- return async (unparsedInput: unknown): Promise<Awaited<TOutput>> => {
+ return async (unparsedInput: unknown, context: ActionAPIContext): Promise<Awaited<TOutput>> => {
if (unparsedInput instanceof FormData) {
throw new ActionError({
code: 'UNSUPPORTED_MEDIA_TYPE',
@@ -111,12 +117,12 @@ function getJsonServerHandler<TOutput, TInputSchema extends ActionInputSchema<'j
});
}
- if (!inputSchema) return await handler(unparsedInput, getApiContext());
+ if (!inputSchema) return await handler(unparsedInput, context);
const parsed = await inputSchema.safeParseAsync(unparsedInput);
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
- return await handler(parsed.data, getApiContext());
+ return await handler(parsed.data, context);
};
}
diff --git a/packages/astro/src/actions/utils.ts b/packages/astro/src/actions/utils.ts
index 833cd6dac..cb00e4b79 100644
--- a/packages/astro/src/actions/utils.ts
+++ b/packages/astro/src/actions/utils.ts
@@ -18,3 +18,16 @@ export function createGetActionResult(locals: APIContext['locals']): APIContext[
return locals._actionsInternal.getActionResult(actionFn);
};
}
+
+export function createCallAction(locals: APIContext['locals']): APIContext['callAction'] {
+ return (actionFn, input) => {
+ if (!hasActionsInternal(locals))
+ throw new AstroError({
+ name: 'AstroActionError',
+ message: 'Experimental actions are not enabled in your project.',
+ hint: 'See https://docs.astro.build/en/reference/configuration-reference/#experimental-flags',
+ });
+
+ return locals._actionsInternal.callAction(actionFn, input);
+ };
+}
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index c94a710bb..398026bc7 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -1606,6 +1606,21 @@ export const DuplicateContentEntrySlugError = {
/**
* @docs
* @see
+ * - [devalue library](https://github.com/rich-harris/devalue)
+ * @description
+ * `transform()` functions in your content config must return valid JSON, or data types compatible with the devalue library (including Dates, Maps, and Sets).
+ */
+export const UnsupportedConfigTransformError = {
+ name: 'UnsupportedConfigTransformError',
+ title: 'Unsupported transform in content config.',
+ message: (parseError: string) =>
+ `\`transform()\` functions in your content config must return valid JSON, or data types compatible with the devalue library (including Dates, Maps, and Sets).\nFull error: ${parseError}`,
+ hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
+} satisfies ErrorData;
+
+/**
+ * @docs
+ * @see
* - [On-demand rendering](https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered)
* @description
* Your project must have a server output to create backend functions with Actions.
@@ -1650,17 +1665,14 @@ export const ActionQueryStringInvalidError = {
/**
* @docs
- * @see
- * - [devalue library](https://github.com/rich-harris/devalue)
* @description
- * `transform()` functions in your content config must return valid JSON, or data types compatible with the devalue library (including Dates, Maps, and Sets).
+ * Action called from a server page or endpoint without using `Astro.callAction()`.
*/
-export const UnsupportedConfigTransformError = {
- name: 'UnsupportedConfigTransformError',
- title: 'Unsupported transform in content config.',
- message: (parseError: string) =>
- `\`transform()\` functions in your content config must return valid JSON, or data types compatible with the devalue library (including Dates, Maps, and Sets).\nFull error: ${parseError}`,
- hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
+export const ActionCalledFromServerError = {
+ name: 'ActionCalledFromServerError',
+ title: 'Action unexpected called from the server.',
+ message: 'Action called from a server page or endpoint without using `Astro.callAction()`.',
+ hint: 'See the RFC section on server calls for usage instructions: https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md#call-actions-directly-from-server-code',
} satisfies ErrorData;
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip.
diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts
index 02fad1c61..5499232b1 100644
--- a/packages/astro/src/core/middleware/index.ts
+++ b/packages/astro/src/core/middleware/index.ts
@@ -1,5 +1,5 @@
import type { APIContext, MiddlewareHandler, Params, RewritePayload } from '../../@types/astro.js';
-import { createGetActionResult } from '../../actions/utils.js';
+import { createCallAction, createGetActionResult } from '../../actions/utils.js';
import {
computeCurrentLocale,
computePreferredLocale,
@@ -52,7 +52,7 @@ function createContext({
// return dummy response
return Promise.resolve(new Response(null));
};
- const context: Omit<APIContext, 'getActionResult'> = {
+ const context: Omit<APIContext, 'getActionResult' | 'callAction'> = {
cookies: new AstroCookies(request),
request,
params,
@@ -106,6 +106,7 @@ function createContext({
};
return Object.assign(context, {
getActionResult: createGetActionResult(context.locals),
+ callAction: createCallAction(context.locals),
});
}
diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts
index c55abc381..f8d805b09 100644
--- a/packages/astro/src/core/render-context.ts
+++ b/packages/astro/src/core/render-context.ts
@@ -9,8 +9,8 @@ import type {
RouteData,
SSRResult,
} from '../@types/astro.js';
-import type { ActionAPIContext } from '../actions/runtime/store.js';
-import { createGetActionResult, hasActionsInternal } from '../actions/utils.js';
+import type { ActionAPIContext } from '../actions/runtime/utils.js';
+import { createCallAction, createGetActionResult, hasActionsInternal } from '../actions/utils.js';
import {
computeCurrentLocale,
computePreferredLocale,
@@ -216,6 +216,7 @@ export class RenderContext {
return Object.assign(context, {
props,
getActionResult: createGetActionResult(context.locals),
+ callAction: createCallAction(context.locals),
});
}
@@ -458,6 +459,7 @@ export class RenderContext {
rewrite,
request: this.request,
getActionResult: createGetActionResult(locals),
+ callAction: createCallAction(locals),
response,
site: pipeline.site,
url,
diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs
index fbd89d75d..73ee8396d 100644
--- a/packages/astro/templates/actions.mjs
+++ b/packages/astro/templates/actions.mjs
@@ -7,7 +7,9 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') {
return target[objKey];
}
const path = aggregatedPath + objKey.toString();
- const action = (param) => callSafely(() => handleActionOrThrow(param, path));
+ function action(param) {
+ return callSafely(() => handleActionOrThrow(param, path, this));
+ }
Object.assign(action, {
queryString: getActionQueryString(path),
@@ -26,8 +28,8 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') {
// Note: `orThrow` does not have progressive enhancement info.
// If you want to throw exceptions,
// you must handle those exceptions with client JS.
- orThrow: (param) => {
- return handleActionOrThrow(param, path);
+ orThrow(param) {
+ return handleActionOrThrow(param, path, this);
},
});
@@ -41,16 +43,17 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') {
/**
* @param {*} param argument passed to the action when called server or client-side.
* @param {string} path Built path to call action by path name.
+ * @param {import('../src/@types/astro.d.ts').APIContext | undefined} context Injected API context when calling actions from the server.
* Usage: `actions.[name](param)`.
*/
-async function handleActionOrThrow(param, path) {
+async function handleActionOrThrow(param, path, context) {
// When running server-side, import the action and call it.
if (import.meta.env.SSR) {
const { getAction } = await import('astro/actions/runtime/utils.js');
const action = await getAction(path);
if (!action) throw new Error(`Action not found: ${path}`);
- return action.orThrow(param);
+ return action.orThrow.bind(context)(param);
}
// When running client-side, make a fetch request to the action path.
diff --git a/packages/astro/test/fixtures/actions/src/pages/subscribe.astro b/packages/astro/test/fixtures/actions/src/pages/subscribe.astro
index a5cadb179..be38adbce 100644
--- a/packages/astro/test/fixtures/actions/src/pages/subscribe.astro
+++ b/packages/astro/test/fixtures/actions/src/pages/subscribe.astro
@@ -1,7 +1,7 @@
---
import { actions } from 'astro:actions';
-const res = await actions.subscribeFromServer.orThrow({
+const res = await Astro.callAction(actions.subscribeFromServer.orThrow, {
channel: 'bholmesdev',
});
---
diff --git a/packages/astro/test/types/call-action.ts b/packages/astro/test/types/call-action.ts
new file mode 100644
index 000000000..9a74802fc
--- /dev/null
+++ b/packages/astro/test/types/call-action.ts
@@ -0,0 +1,51 @@
+import { describe, it } from 'node:test';
+import { expectTypeOf } from 'expect-type';
+import { type ActionReturnType, defineAction } from '../../dist/actions/runtime/virtual/server.js';
+import { z } from '../../zod.mjs';
+import type { APIContext } from '../../dist/@types/astro.js';
+
+describe('Astro.callAction', () => {
+ it('Infers JSON action result on callAction', async () => {
+ const context: APIContext = {} as any;
+ const action = defineAction({
+ input: z.object({
+ name: z.string(),
+ }),
+ handler: async ({ name }) => {
+ return { name };
+ },
+ });
+ const result = await context.callAction(action, { name: 'Ben' });
+ expectTypeOf<typeof result>().toEqualTypeOf<ActionReturnType<typeof action>>();
+ });
+
+ it('Infers form action result on callAction', async () => {
+ const context: APIContext = {} as any;
+ const action = defineAction({
+ accept: 'form',
+ input: z.object({
+ name: z.string(),
+ }),
+ handler: async ({ name }) => {
+ return { name };
+ },
+ });
+ const result = await context.callAction(action, new FormData());
+ expectTypeOf<typeof result>().toEqualTypeOf<ActionReturnType<typeof action>>();
+ });
+
+ it('Infers orThrow action result on callAction', async () => {
+ const context: APIContext = {} as any;
+ const action = defineAction({
+ accept: 'form',
+ input: z.object({
+ name: z.string(),
+ }),
+ handler: async ({ name }) => {
+ return { name };
+ },
+ });
+ const result = await context.callAction(action.orThrow, new FormData());
+ expectTypeOf<typeof result>().toEqualTypeOf<ActionReturnType<(typeof action)['orThrow']>>();
+ });
+});