From 91435b66663fd485a0b7f974a0126c9e3412769b Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 15:21:02 -0400 Subject: [PATCH 01/19] feat: new orThrow types --- .../src/actions/runtime/virtual/server.ts | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index 93b3f54e1137..904be96af68e 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -26,23 +26,21 @@ export type ActionClient< TAccept extends ActionAccept | undefined, TInputSchema extends ActionInputSchema | undefined, > = TInputSchema extends z.ZodType - ? (( - input: TAccept extends 'form' ? FormData : z.input - ) => Promise>) & { + ? (input: TAccept extends 'form' ? FormData : z.input) => Promise< + SafeResult< + z.input extends ErrorInferenceObject + ? z.input + : ErrorInferenceObject, + Awaited + > + > & { queryString: string; - safe: ( + orThrow: ( input: TAccept extends 'form' ? FormData : z.input - ) => Promise< - SafeResult< - z.input extends ErrorInferenceObject - ? z.input - : ErrorInferenceObject, - Awaited - > - >; + ) => Promise>; } - : ((input?: any) => Promise>) & { - safe: (input?: any) => Promise>>; + : (input?: any) => Promise>> & { + orThrow: (input?: any) => Promise>; }; export function defineAction< @@ -66,12 +64,15 @@ export function defineAction< ? getFormServerHandler(handler, inputSchema) : getJsonServerHandler(handler, inputSchema); - Object.assign(serverHandler, { - safe: async (unparsedInput: unknown) => { - return callSafely(() => serverHandler(unparsedInput)); - }, + const safeServerHandler = async (unparsedInput: unknown) => { + return callSafely(() => serverHandler(unparsedInput)); + }; + + Object.assign(safeServerHandler, { + orThrow: serverHandler, }); - return serverHandler as ActionClient & string; + + return safeServerHandler as ActionClient & string; } function getFormServerHandler>( From 6440b5b8576aaa0367b10ae8f5ec374b9f3fc26e Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 15:43:28 -0400 Subject: [PATCH 02/19] fix: parens on return type --- packages/astro/src/actions/runtime/virtual/server.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index 904be96af68e..fb6a36c9256a 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -26,14 +26,16 @@ export type ActionClient< TAccept extends ActionAccept | undefined, TInputSchema extends ActionInputSchema | undefined, > = TInputSchema extends z.ZodType - ? (input: TAccept extends 'form' ? FormData : z.input) => Promise< + ? (( + input: TAccept extends 'form' ? FormData : z.input + ) => Promise< SafeResult< z.input extends ErrorInferenceObject ? z.input : ErrorInferenceObject, Awaited > - > & { + >) & { queryString: string; orThrow: ( input: TAccept extends 'form' ? FormData : z.input From 3922bbb8ae288a4059cb717374eef31f6abe07e4 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 15:44:27 -0400 Subject: [PATCH 03/19] feat: switch implementation to orThrow() --- packages/astro/src/@types/astro.ts | 2 +- packages/astro/src/actions/runtime/route.ts | 3 +- packages/astro/src/actions/runtime/utils.ts | 5 +- packages/astro/templates/actions.mjs | 62 +++++++++++---------- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 20eb91f63805..f0b286e8515d 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2845,7 +2845,7 @@ interface AstroSharedContext< TAction extends ActionClient, >( action: TAction - ) => Awaited> | undefined; + ) => Awaited> | undefined; /** * Route parameters for this request if this is a dynamic route. */ diff --git a/packages/astro/src/actions/runtime/route.ts b/packages/astro/src/actions/runtime/route.ts index 463a4ac6eee0..a1e711b186d6 100644 --- a/packages/astro/src/actions/runtime/route.ts +++ b/packages/astro/src/actions/runtime/route.ts @@ -1,7 +1,6 @@ import type { APIRoute } from '../../@types/astro.js'; import { ApiContextStorage } from './store.js'; import { formContentTypes, getAction, hasContentType } from './utils.js'; -import { callSafely } from './virtual/shared.js'; export const POST: APIRoute = async (context) => { const { request, url } = context; @@ -23,7 +22,7 @@ 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, () => callSafely(() => action(args))); + const result = await ApiContextStorage.run(context, () => action(args)); if (result.error) { return new Response( JSON.stringify({ diff --git a/packages/astro/src/actions/runtime/utils.ts b/packages/astro/src/actions/runtime/utils.ts index 91f2859d45e9..45f6cdc61f37 100644 --- a/packages/astro/src/actions/runtime/utils.ts +++ b/packages/astro/src/actions/runtime/utils.ts @@ -1,3 +1,6 @@ +import type { ZodType } from 'zod'; +import type { ActionAccept, ActionClient } from './virtual/server.js'; + export const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; export function hasContentType(contentType: string, expected: string[]) { @@ -17,7 +20,7 @@ export type MaybePromise = T | Promise; */ export async function getAction( path: string -): Promise<((param: unknown) => MaybePromise) | undefined> { +): Promise | undefined> { const pathKeys = path.replace('/_actions/', '').split('.'); // @ts-expect-error virtual module let { server: actionLookup } = await import('astro:internal-actions'); diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs index 1101c136181b..03faf82c5a3a 100644 --- a/packages/astro/templates/actions.mjs +++ b/packages/astro/templates/actions.mjs @@ -7,36 +7,41 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') { return target[objKey]; } const path = aggregatedPath + objKey.toString(); - const action = (param) => actionHandler(param, path); + const action = (param) => callSafely(() => actionHandler(param, path)); - action.toString = () => getActionQueryString(path); - action.queryString = action.toString(); - action.safe = (input) => { - return callSafely(() => action(input)); - }; - action.safe.toString = () => action.toString(); + Object.assign(action, { + toString: () => getActionQueryString(path), + queryString: action.toString(), + orThrow: (param) => { + return actionHandler(param, path); + }, + // Add progressive enhancement info for React. + $$FORM_ACTION: function () { + const data = new FormData(); + data.set('_astroActionSafe', 'true'); + 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: action.toString(), + data, + }; + }, + }); + + Object.assign(action.orThrow, { + toString: () => action.toString(), + $$FORM_ACTION: function () { + return { + method: 'POST', + name: '_astroAction', + action: action.toString(), + }; + }, + }); - // Add progressive enhancement info for React. - action.$$FORM_ACTION = function () { - 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: action.toString(), - }; - }; - action.safe.$$FORM_ACTION = function () { - const data = new FormData(); - data.set('_astroActionSafe', 'true'); - return { - method: 'POST', - name: '_astroAction', - action: action.toString(), - data, - }; - }; // recurse to construct queries for nested object paths // ex. actions.user.admins.auth() return toActionProxy(action, path + '.'); @@ -87,6 +92,7 @@ async function actionHandler(param, path) { if (res.status === 204) return; const json = await res.json(); + console.log('$$$json', json); return json; } From ba71bbb40c38e4f71016693796a71aa5d278bbd5 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 15:44:37 -0400 Subject: [PATCH 04/19] feat(e2e): update PostComment --- .../e2e/fixtures/actions-blog/src/components/PostComment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx b/packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx index f73d152e12bf..b6b6bcea1c29 100644 --- a/packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/PostComment.tsx @@ -21,7 +21,7 @@ export function PostComment({ e.preventDefault(); const form = e.target as HTMLFormElement; const formData = new FormData(form); - const { data, error } = await actions.blog.comment.safe(formData); + const { data, error } = await actions.blog.comment(formData); if (isInputError(error)) { return setBodyError(error.fields.body?.join(' ')); } else if (error) { From 7b14ab6818d9a7fe343aa27f6824a3b41749b2c7 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 15:47:09 -0400 Subject: [PATCH 05/19] fix: remove callSafely from middleware --- packages/astro/src/actions/runtime/middleware.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/astro/src/actions/runtime/middleware.ts b/packages/astro/src/actions/runtime/middleware.ts index 9662f5872b0c..252ee67aea3c 100644 --- a/packages/astro/src/actions/runtime/middleware.ts +++ b/packages/astro/src/actions/runtime/middleware.ts @@ -72,9 +72,7 @@ async function handlePost({ if (contentType && hasContentType(contentType, formContentTypes)) { formData = await request.clone().formData(); } - const actionResult = await ApiContextStorage.run(context, () => - callSafely(() => action(formData)) - ); + const actionResult = await ApiContextStorage.run(context, () => action(formData)); return handleResult({ context, next, actionName, actionResult }); } @@ -137,9 +135,7 @@ async function handlePostLegacy({ context, next }: { context: APIContext; next: }); } - const actionResult = await ApiContextStorage.run(context, () => - callSafely(() => action(formData)) - ); + const actionResult = await ApiContextStorage.run(context, () => action(formData)); return handleResult({ context, next, actionName, actionResult }); } From e9c3b91b4cd1dbcc3d0bea4ec1b77a35efef7b1e Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 15:47:16 -0400 Subject: [PATCH 06/19] fix: toString() for actions --- packages/astro/templates/actions.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs index 03faf82c5a3a..5ece772f6bd3 100644 --- a/packages/astro/templates/actions.mjs +++ b/packages/astro/templates/actions.mjs @@ -10,8 +10,8 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') { const action = (param) => callSafely(() => actionHandler(param, path)); Object.assign(action, { - toString: () => getActionQueryString(path), - queryString: action.toString(), + queryString: getActionQueryString(path), + toString: () => action.queryString, orThrow: (param) => { return actionHandler(param, path); }, From d0aeebae96b356aeeb85ca2ee47f40a1e425bd84 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 15:49:42 -0400 Subject: [PATCH 07/19] fix(e2e): more orThrow updates --- .../astro/e2e/fixtures/actions-blog/src/components/Like.tsx | 2 +- .../e2e/fixtures/actions-react-19/src/components/Like.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx b/packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx index 7d4e6a53d161..9e39d8f9c7cb 100644 --- a/packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/Like.tsx @@ -11,7 +11,7 @@ export function Like({ postId, initial }: { postId: string; initial: number }) { disabled={pending} onClick={async () => { setPending(true); - setLikes(await actions.blog.like({ postId })); + setLikes(await actions.blog.like.orThrow({ postId })); setPending(false); }} type="submit" diff --git a/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx b/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx index 0d2dde00916e..75b8ea9ca9c5 100644 --- a/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx +++ b/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx @@ -15,8 +15,8 @@ export function Like({ postId, label, likes }: { postId: string; label: string; export function LikeWithActionState({ postId, label, likes: initial }: { postId: string; label: string; likes: number }) { const [likes, action] = useActionState( - experimental_withState(actions.blog.likeWithActionState), - 10, + experimental_withState(actions.blog.likeWithActionState.orThrow), + initial, ); return ( From 4e3cb8c63eb203a740dabfb4802d72fe9eefeaff Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 16:22:30 -0400 Subject: [PATCH 08/19] feat: remove progressive enhancement from orThrow --- packages/astro/templates/actions.mjs | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs index 5ece772f6bd3..f536ec109010 100644 --- a/packages/astro/templates/actions.mjs +++ b/packages/astro/templates/actions.mjs @@ -12,13 +12,8 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') { Object.assign(action, { queryString: getActionQueryString(path), toString: () => action.queryString, - orThrow: (param) => { - return actionHandler(param, path); - }, - // Add progressive enhancement info for React. + // Progressive enhancement info for React. $$FORM_ACTION: function () { - const data = new FormData(); - data.set('_astroActionSafe', 'true'); return { method: 'POST', // `name` creates a hidden input. @@ -26,19 +21,13 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') { // At least use a name that won't conflict with a user's formData. name: '_astroAction', action: action.toString(), - data, }; }, - }); - - Object.assign(action.orThrow, { - toString: () => action.toString(), - $$FORM_ACTION: function () { - return { - method: 'POST', - name: '_astroAction', - action: action.toString(), - }; + // 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 actionHandler(param, path); }, }); From 93e39398d80778336e6416aa376d426d3c060881 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 16:32:09 -0400 Subject: [PATCH 09/19] fix: remove _astroActionSafe handler from react --- packages/integrations/react/server.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js index 6624a5610f00..16bf6785e28a 100644 --- a/packages/integrations/react/server.js +++ b/packages/integrations/react/server.js @@ -1,5 +1,4 @@ import opts from 'astro:react:opts'; -import { AstroError } from 'astro/errors'; import React from 'react'; import ReactDOM from 'react-dom/server'; import { incrementId } from './context.js'; @@ -151,17 +150,7 @@ async function getFormState({ result }) { if (!actionKey || !actionName) return undefined; - const isUsingSafe = formData.has('_astroActionSafe'); - if (!isUsingSafe && actionResult.error) { - throw new AstroError( - `Unhandled error calling action ${actionName.replace(/^\/_actions\//, '')}:\n[${ - actionResult.error.code - }] ${actionResult.error.message}`, - 'use `.safe()` to handle from your React component.' - ); - } - - return [isUsingSafe ? actionResult : actionResult.data, actionKey, actionName]; + return [actionResult, actionKey, actionName]; } async function renderToPipeableStreamAsync(vnode, options) { From d68dedb87b1287e6bf2e45c153b8dcba9b72581e Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 29 Jul 2024 16:32:17 -0400 Subject: [PATCH 10/19] feat(e2e): update test to use safe calling --- .../e2e/fixtures/actions-react-19/src/actions/index.ts | 7 ++++--- .../e2e/fixtures/actions-react-19/src/components/Like.tsx | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/astro/e2e/fixtures/actions-react-19/src/actions/index.ts b/packages/astro/e2e/fixtures/actions-react-19/src/actions/index.ts index 39f9dcf9aab8..cd42207729cf 100644 --- a/packages/astro/e2e/fixtures/actions-react-19/src/actions/index.ts +++ b/packages/astro/e2e/fixtures/actions-react-19/src/actions/index.ts @@ -1,5 +1,5 @@ import { db, Likes, eq, sql } from 'astro:db'; -import { defineAction, getApiContext, z } from 'astro:actions'; +import { defineAction, z, type SafeResult } from 'astro:actions'; import { experimental_getActionState } from '@astrojs/react/actions'; export const server = { @@ -28,12 +28,13 @@ export const server = { handler: async ({ postId }, ctx) => { await new Promise((r) => setTimeout(r, 200)); - const state = await experimental_getActionState(ctx); + const state = await experimental_getActionState>(ctx); + const previousLikes = state.data ?? 0; const { likes } = await db .update(Likes) .set({ - likes: state + 1, + likes: previousLikes + 1, }) .where(eq(Likes.postId, postId)) .returning() diff --git a/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx b/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx index 75b8ea9ca9c5..652ea935ab1c 100644 --- a/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx +++ b/packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx @@ -15,14 +15,14 @@ export function Like({ postId, label, likes }: { postId: string; label: string; export function LikeWithActionState({ postId, label, likes: initial }: { postId: string; label: string; likes: number }) { const [likes, action] = useActionState( - experimental_withState(actions.blog.likeWithActionState.orThrow), - initial, + experimental_withState(actions.blog.likeWithActionState), + { data: initial }, ); return (
-