@@ -37,8 +37,9 @@ const actionRegistry = new Map<string, RegisteredAction>()
3737let actionCounter = 0
3838
3939/**
40- * Define a server action. Returns a client-callable function that
41- * sends a POST request to `/_zero/actions/<id>`.
40+ * Define a server action. Returns a callable function that:
41+ * - On the **client**: sends a POST request to `/_zero/actions/<id>`
42+ * - On the **server** (SSR): executes the handler directly (no fetch)
4243 *
4344 * @example
4445 * // In a route file or module:
@@ -59,13 +60,32 @@ export function defineAction<T = unknown>(
5960 actionRegistry . set ( id , { id, handler : handler as ActionHandler } )
6061
6162 const callable = async ( data ?: unknown ) : Promise < T > => {
63+ // Server-side: execute handler directly (no network round-trip)
64+ if ( typeof globalThis . window === 'undefined' ) {
65+ return handler ( {
66+ request : new Request ( `http://localhost/_zero/actions/${ id } ` , {
67+ method : 'POST' ,
68+ headers : { 'Content-Type' : 'application/json' } ,
69+ body : JSON . stringify ( data ?? null ) ,
70+ } ) ,
71+ formData : null ,
72+ json : data ?? null ,
73+ headers : new Headers ( { 'Content-Type' : 'application/json' } ) ,
74+ } )
75+ }
76+
77+ // Client-side: POST to the action endpoint
6278 const response = await fetch ( `/_zero/actions/${ id } ` , {
6379 method : 'POST' ,
6480 headers : { 'Content-Type' : 'application/json' } ,
6581 body : JSON . stringify ( data ?? null ) ,
6682 } )
6783 if ( ! response . ok ) {
68- throw new Error ( `Action failed: ${ response . statusText } ` )
84+ const body = await response . json ( ) . catch ( ( ) => ( { } ) )
85+ throw new Error (
86+ ( body as { error ?: string } ) . error ??
87+ `Action failed: ${ response . statusText } ` ,
88+ )
6989 }
7090 return response . json ( )
7191 }
@@ -104,51 +124,45 @@ export function createActionMiddleware(): (
104124 const action = actionRegistry . get ( actionId )
105125
106126 if ( ! action ) {
107- return new Response ( JSON . stringify ( { error : 'Action not found' } ) , {
108- status : 404 ,
109- headers : { 'Content-Type' : 'application/json' } ,
110- } )
127+ return Response . json ( { error : 'Action not found' } , { status : 404 } )
111128 }
112129
113130 if ( ctx . req . method !== 'POST' ) {
114- return new Response ( JSON . stringify ( { error : 'Method not allowed' } ) , {
115- status : 405 ,
116- headers : { 'Content-Type' : 'application/json' } ,
117- } )
131+ return Response . json ( { error : 'Method not allowed' } , { status : 405 } )
118132 }
119133
120- try {
121- const contentType = ctx . req . headers . get ( 'content-type' ) ?? ''
122- let formData : FormData | null = null
123- let json : unknown = null
124-
125- if ( contentType . includes ( 'application/json' ) ) {
126- json = await ctx . req . json ( )
127- } else if (
128- contentType . includes ( 'multipart/form-data' ) ||
129- contentType . includes ( 'application/x-www-form-urlencoded' )
130- ) {
131- formData = await ctx . req . formData ( )
132- }
133-
134- const result = await action . handler ( {
135- request : ctx . req ,
136- formData,
137- json,
138- headers : ctx . req . headers ,
139- } )
134+ return executeAction ( action , ctx . req )
135+ }
136+ }
140137
141- return new Response ( JSON . stringify ( result ?? null ) , {
142- status : 200 ,
143- headers : { 'Content-Type' : 'application/json' } ,
144- } )
145- } catch ( err ) {
146- const message =
147- err instanceof Error ? err . message : 'Internal server error'
148- return new Response ( JSON . stringify ( { error : message } ) , {
149- status : 500 ,
150- headers : { 'Content-Type' : 'application/json' } ,
151- } )
138+ async function executeAction (
139+ action : RegisteredAction ,
140+ req : Request ,
141+ ) : Promise < Response > {
142+ try {
143+ const contentType = req . headers . get ( 'content-type' ) ?? ''
144+ let formData : FormData | null = null
145+ let json : unknown = null
146+
147+ if ( contentType . includes ( 'application/json' ) ) {
148+ json = await req . json ( )
149+ } else if (
150+ contentType . includes ( 'multipart/form-data' ) ||
151+ contentType . includes ( 'application/x-www-form-urlencoded' )
152+ ) {
153+ formData = await req . formData ( )
152154 }
155+
156+ const result = await action . handler ( {
157+ request : req ,
158+ formData,
159+ json,
160+ headers : req . headers ,
161+ } )
162+
163+ return Response . json ( result ?? null )
164+ } catch ( err ) {
165+ const message = err instanceof Error ? err . message : 'Internal server error'
166+ return Response . json ( { error : message } , { status : 500 } )
153167 }
154168}
0 commit comments