@gnosticdev/hono-actions 1.2.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Astro Actions with Hono and Valibot
1
+ # Astro Actions with Hono
2
2
 
3
3
  Define server actions with built-in validation, error handling, and a pre-built hono client for calling the routes.
4
4
 
@@ -16,20 +16,96 @@ bun add @gnosticdev/hono-actions
16
16
 
17
17
  This package requires:
18
18
 
19
- - `astro`: ^5.13.3
19
+ - `astro`: ^5.13.0
20
20
 
21
- All other dependencies (`hono`, `valibot`, `@hono/valibot-validator`, etc.) are bundled with the integration.
21
+ ## Supported Adapters
22
+
23
+ This integration works with all supported Astro adapters:
24
+
25
+ - `@astrojs/cloudflare`
26
+ - `@astrojs/node`
27
+ - `@astrojs/vercel`
28
+ - `@astrojs/netlify`
22
29
 
23
30
  ## Setup
24
31
 
25
32
  ### 1. Add the integration to your Astro config
26
33
 
34
+ The integration works with all Astro adapters. Here are examples for each:
35
+
36
+ #### Cloudflare
37
+
38
+ ```typescript
39
+ // astro.config.ts
40
+ import { defineConfig } from 'astro/config'
41
+ import cloudflare from '@astrojs/cloudflare'
42
+ import honoActions from '@gnosticdev/hono-actions/integration'
43
+
44
+ export default defineConfig({
45
+ output: 'server',
46
+ adapter: cloudflare(),
47
+ integrations: [
48
+ honoActions({
49
+ basePath: '/api', // Optional: default is '/api'
50
+ actionsPath: 'src/server/actions.ts' // Optional: custom path to your actions file
51
+ })
52
+ ]
53
+ })
54
+ ```
55
+
56
+ #### Node.js
57
+
58
+ ```typescript
59
+ // astro.config.ts
60
+ import { defineConfig } from 'astro/config'
61
+ import node from '@astrojs/node'
62
+ import honoActions from '@gnosticdev/hono-actions/integration'
63
+
64
+ export default defineConfig({
65
+ output: 'server',
66
+ adapter: node({
67
+ mode: 'standalone' // or 'middleware'
68
+ }),
69
+ integrations: [
70
+ honoActions({
71
+ basePath: '/api', // Optional: default is '/api'
72
+ actionsPath: 'src/server/actions.ts' // Optional: custom path to your actions file
73
+ })
74
+ ]
75
+ })
76
+ ```
77
+
78
+ #### Vercel
79
+
27
80
  ```typescript
28
81
  // astro.config.ts
29
82
  import { defineConfig } from 'astro/config'
83
+ import vercel from '@astrojs/vercel/serverless'
30
84
  import honoActions from '@gnosticdev/hono-actions/integration'
31
85
 
32
86
  export default defineConfig({
87
+ output: 'server',
88
+ adapter: vercel(),
89
+ integrations: [
90
+ honoActions({
91
+ basePath: '/api', // Optional: default is '/api'
92
+ actionsPath: 'src/server/actions.ts' // Optional: custom path to your actions file
93
+ })
94
+ ]
95
+ })
96
+ ```
97
+
98
+ #### Netlify
99
+
100
+ ```typescript
101
+ // astro.config.ts
102
+ import { defineConfig } from 'astro/config'
103
+ import netlify from '@astrojs/netlify'
104
+ import honoActions from '@gnosticdev/hono-actions/integration'
105
+
106
+ export default defineConfig({
107
+ output: 'server',
108
+ adapter: netlify(),
33
109
  integrations: [
34
110
  honoActions({
35
111
  basePath: '/api', // Optional: default is '/api'
@@ -41,7 +117,7 @@ export default defineConfig({
41
117
 
42
118
  ### 2. Create your actions file
43
119
 
44
- Create a file at one of these locations (the integration will auto-discover):
120
+ If not using a custom actions path, create a file at one of these locations:
45
121
 
46
122
  - `src/server/actions.ts`
47
123
  - `src/hono/actions.ts`
@@ -51,57 +127,61 @@ Create a file at one of these locations (the integration will auto-discover):
51
127
  ## Usage
52
128
 
53
129
  ```typescript
54
- // src/server/actions.ts
55
- import { defineHonoAction, HonoActionError } from '@gnosticdev/hono-actions'
56
- import * as v from 'valibot'
57
-
58
- // Define a simple action
59
- export const simpleAction = defineHonoAction({
60
- path: '/simple',
61
- schema: v.object({
62
- name: v.string()
130
+ // src/hono.ts (or any of the supported locations above)
131
+ import { defineHonoAction type HonoEnv } from '@gnosticdev/hono-actions/actions'
132
+ import { z } from 'astro/zod'
133
+ import { Hono } from 'hono'
134
+
135
+ // Define a POST action with Zod validation (no `path` option is used anymore)
136
+ export const myAction = defineHonoAction({
137
+ schema: z.object({
138
+ name: z.string()
63
139
  }),
64
140
  handler: async (input, ctx) => {
65
- // input is automatically typed based on schema
141
+ // `input` is automatically typed from the schema
142
+ // `ctx` is a strongly-typed Hono Context with your `HonoEnv`
66
143
  return { message: `Hello ${input.name}!` }
67
144
  }
68
145
  })
69
146
 
70
- // Define an action with validation
71
- export const validatedAction = defineHonoAction({
72
- path: '/validated',
73
- schema: v.object({
74
- name: v.string(),
75
- email: v.pipe(v.string(), v.email())
76
- }),
147
+ // Define another POST action
148
+ export const anotherAction = defineHonoAction({
149
+ schema: z.object({ name2: z.string() }),
77
150
  handler: async (input, ctx) => {
78
- // input is automatically typed based on schema
79
151
  return {
80
- message: `Hello ${input.name}!`,
81
- email: input.email
152
+ message2: `Hello ${input.name2}!`
82
153
  }
83
154
  }
84
155
  })
85
156
 
86
- // Use custom error handling
87
- export const errorAction = defineHonoAction({
88
- path: '/error',
157
+ // Optional: Define an action without a schema (accepts any JSON)
158
+ export const noSchemaAction = defineHonoAction({
89
159
  handler: async (input, ctx) => {
90
- if (someCondition) {
160
+ if (!('name' in input)) {
91
161
  throw new HonoActionError({
92
- message: 'Custom error message',
93
- code: 'EXTERNAL_API_ERROR'
162
+ message: 'Name is required',
163
+ code: 'INPUT_VALIDATION_ERROR'
94
164
  })
95
165
  }
96
- return { success: true }
166
+ return { message: `Hello ${String((input as any).name)}!` }
97
167
  }
98
168
  })
99
169
 
100
- // Export all actions in a honoActions object
170
+ // You can also define standard Hono routes (GET/PATCH/etc.), not just POST actions.
171
+ // This is useful where standard Astro actions are POST-only.
172
+ const app = new Hono<HonoEnv>()
173
+ const getRoute = app.get('/', (c) => c.json({ message: 'Hi from a get route' }))
174
+
175
+ // Export all actions and routes in a single `honoActions` object.
176
+ // Each key becomes the route name under your basePath, e.g.:
177
+ // - POST /api/myAction
178
+ // - POST /api/anotherAction
179
+ // - GET /api/getRoute
101
180
  export const honoActions = {
102
- simpleAction,
103
- validatedAction,
104
- errorAction
181
+ myAction,
182
+ anotherAction,
183
+ noSchemaAction,
184
+ getRoute
105
185
  }
106
186
  ```
107
187
 
@@ -110,22 +190,22 @@ export const honoActions = {
110
190
  ```typescript
111
191
  // src/pages/example.astro or any .astro file
112
192
  ---
113
- import { honoClient } from '@gnosticdev/hono-actions/client'
193
+ import { honoClient, parseResponse } from '@gnosticdev/hono-actions/client'
114
194
 
115
- const response = await honoClient.simpleAction.$post({
116
- json: { name: 'John' }
117
- })
195
+ // Call a POST action
196
+ const { data: actionRes } = await parseResponse(
197
+ await honoClient.api.myAction.$post({ json: { name: 'John' } })
198
+ )
118
199
 
119
- let result = null
120
- if (response.ok) {
121
- result = await response.json() // { message: 'Hello John!' }
122
- } else {
123
- console.error(await response.text()) // Error message
124
- }
200
+ // Call a GET route
201
+ const { message } = await parseResponse(
202
+ await honoClient.api.getRoute.$get()
203
+ )
125
204
  ---
126
205
 
127
206
  <div>
128
- {result && <p>{result.message}</p>}
207
+ {actionRes && <p>{actionRes.message}</p>}
208
+ <p>{message}</p>
129
209
  </div>
130
210
  ```
131
211
 
@@ -137,10 +217,9 @@ import { honoClient } from '@gnosticdev/hono-actions/client'
137
217
 
138
218
  // Make requests from the browser
139
219
  const handleSubmit = async (formData: FormData) => {
140
- const response = await honoClient.validatedAction.$post({
220
+ const response = await honoClient.api.anotherAction.$post({
141
221
  json: {
142
- name: formData.get('name') as string,
143
- email: formData.get('email') as string
222
+ name2: formData.get('name') as string
144
223
  }
145
224
  })
146
225
 
@@ -156,11 +235,12 @@ const handleSubmit = async (formData: FormData) => {
156
235
 
157
236
  ## Package Structure
158
237
 
159
- This package provides two main entry points:
238
+ This package provides these entry points:
160
239
 
161
- - **`@gnosticdev/hono-actions`** (default): Action definition utilities (`defineHonoAction`, `HonoActionError`, types)
162
- - Safe for browser environments
163
- - Used in your action files and client-side code
240
+ - **`@gnosticdev/hono-actions/actions`**: Action definition utilities (`defineHonoAction`, `HonoActionError`, `HonoEnv`)
241
+ - Used in your actions file(s)
242
+ - **`@gnosticdev/hono-actions/client`**: Pre-built Hono client and helpers (`honoClient`, `parseResponse`)
243
+ - Safe for browser and server environments
164
244
  - **`@gnosticdev/hono-actions/integration`**: Astro integration
165
245
  - Uses Node.js built-ins (fs, path)
166
246
  - Only used in `astro.config.ts`
@@ -175,11 +255,12 @@ The integration accepts the following options:
175
255
  ## Features
176
256
 
177
257
  - ✅ **Type-safe**: Full TypeScript support with automatic type inference
178
- - ✅ **Validation**: Built-in request validation using Valibot schemas
258
+ - ✅ **Validation**: Built-in request validation using Zod schemas
179
259
  - ✅ **Error handling**: Custom error types and automatic error responses
180
260
  - ✅ **Auto-discovery**: Automatically finds your actions file
181
261
  - ✅ **Client generation**: Pre-built client with full type safety
182
262
  - ✅ **Development**: Hot reload support during development
263
+ - ✅ **Flexible routing**: Define standard Hono routes (GET/PATCH/etc.) alongside POST actions
183
264
 
184
265
  ## Troubleshooting
185
266
 
@@ -188,7 +269,7 @@ The integration accepts the following options:
188
269
  If you get an error that no actions were found, make sure:
189
270
 
190
271
  1. Your actions file is in one of the supported locations
191
- 2. You export a `honoActions` object containing your actions
272
+ 2. You export a `honoActions` object containing your actions and any Hono routes
192
273
  3. The file path matches the `actionsPath` option if you specified one
193
274
 
194
275
  ### Type errors
package/dist/actions.d.ts CHANGED
@@ -24,6 +24,15 @@ interface Bindings {
24
24
  * HonoEnv is passed to the Hono context to provide types on `ctx.env`.
25
25
  *
26
26
  * We are using `HonoEnv` to avoid confusion with the Cloudflare types on `Env` -> which cooresponds to `Bindings`
27
+ *
28
+ * * **NOTE** For Cloudflare users, you can declare this in your src/env.d.ts file to get strong
29
+ * typing for `ctx.env`.
30
+ *
31
+ * ```ts
32
+ * declare namespace App {
33
+ * interface Locals extends Runtime {}
34
+ * }
35
+ * ```
27
36
  */
28
37
  interface HonoEnv {
29
38
  Bindings: Bindings;
@@ -58,7 +67,7 @@ type HonoActionParams<TSchema extends HonoActionSchema, TReturn, TEnv extends Ho
58
67
  handler: (params: z.output<TSchema>, context: TContext extends infer Ctx ? Ctx : never) => Promise<TReturn>;
59
68
  };
60
69
  /**
61
- * Defines a type-safe Hono action using Zod for input validation.
70
+ * Defines a POST route with Zod validation for the request body.
62
71
  *
63
72
  * @param schema - The Zod schema for validation (optional).
64
73
  * @param handler - The handler function for the action.
package/dist/index.js CHANGED
@@ -11,7 +11,11 @@ import { glob } from "tinyglobby";
11
11
 
12
12
  // src/integration-files.ts
13
13
  function generateRouter(opts) {
14
- const { basePath, relativeActionsPath } = opts;
14
+ const { basePath, relativeActionsPath, adapter } = opts;
15
+ let exportedApp = `export default app`;
16
+ if (adapter === "@astrojs/netlify") {
17
+ exportedApp = `export default handle(app)`;
18
+ }
15
19
  return `import type { HonoEnv, MergeActionKeyIntoPath } from '@gnosticdev/hono-actions/actions'
16
20
  import { Hono } from 'hono'
17
21
  import { cors } from 'hono/cors'
@@ -19,12 +23,13 @@ import { showRoutes } from 'hono/dev'
19
23
  import { logger } from 'hono/logger'
20
24
  import { prettyJSON } from 'hono/pretty-json'
21
25
  import type { ExtractSchema, MergeSchemaPath } from 'hono/types'
26
+ ${adapter === "@astrojs/netlify" ? "import { handle } from 'hono/netlify'" : ""}
22
27
 
23
28
  async function buildRouter(){
24
29
  type ActionsWithKeyedPaths = MergeActionKeyIntoPath<typeof honoActions>
25
30
  type ActionSchema = ExtractSchema<ActionsWithKeyedPaths[keyof ActionsWithKeyedPaths]>
26
31
  const { honoActions} = await import('${relativeActionsPath}')
27
- const app = new Hono<HonoEnv, MergeSchemaPath<ActionSchema, \`${basePath}\`>>().basePath('${basePath}')
32
+ const app = new Hono<HonoEnv, MergeSchemaPath<ActionSchema, '${basePath}'>>().basePath('${basePath}')
28
33
 
29
34
  app.use('*', cors(), logger(), prettyJSON())
30
35
 
@@ -41,7 +46,7 @@ const app = await buildRouter()
41
46
  console.log('------- Hono Routes -------')
42
47
  showRoutes(app)
43
48
  console.log('---------------------------')
44
- export default app`;
49
+ ${exportedApp}`;
45
50
  }
46
51
  var generateAstroHandler = (adapter) => {
47
52
  switch (adapter) {
@@ -49,17 +54,49 @@ var generateAstroHandler = (adapter) => {
49
54
  return `
50
55
  /// <reference types="./types.d.ts" />
51
56
  // Generated by Hono Actions Integration
57
+ // adapter: ${adapter}
58
+ import type { APIContext, APIRoute } from 'astro'
59
+ import router from './router.js'
60
+
61
+ const handler: APIRoute<APIContext> = async (ctx) => {
62
+ return router.fetch(
63
+ ctx.request,
64
+ ctx.locals.runtime.env, // required for cloudflare adapter
65
+ ctx.locals.runtime.ctx, // required for cloudflare adapter
66
+ )
67
+ }
68
+
69
+ export { handler as ALL }
70
+ `;
71
+ case "@astrojs/node":
72
+ case "@astrojs/vercel":
73
+ return `
74
+ /// <reference types="./types.d.ts" />
75
+ // Generated by Hono Actions Integration
76
+ // adapter: ${adapter}
52
77
  import type { APIContext, APIRoute } from 'astro'
53
78
  import router from './router.js'
54
79
 
55
80
  const handler: APIRoute<APIContext> = async (ctx) => {
56
81
  return router.fetch(
57
82
  ctx.request,
58
- ctx.locals.runtime.env,
59
- ctx.locals.runtime.ctx,
60
83
  )
61
84
  }
62
85
 
86
+ export { handler as ALL }
87
+ `;
88
+ case "@astrojs/netlify":
89
+ return `
90
+ /// <reference types="./types.d.ts" />
91
+ // Generated by Hono Actions Integration
92
+ // adapter: ${adapter}
93
+ import type { APIContext, APIRoute } from 'astro'
94
+ import netlifyHandler from './router.js'
95
+
96
+ const handler: APIRoute<APIContext> = async (ctx) => {
97
+ return netlifyHandler(ctx.request, ctx)
98
+ }
99
+
63
100
  export { handler as ALL }
64
101
  `;
65
102
  default:
@@ -70,6 +107,7 @@ var generateHonoClient = (port) => `
70
107
  // Generated by Hono Actions Integration
71
108
  import type { HonoRouter } from './router.js'
72
109
  import { hc, parseResponse } from 'hono/client'
110
+ import type { DetailedError } from 'hono/client'
73
111
 
74
112
  function getBaseUrl() {
75
113
  // client side can just use the base path
@@ -86,11 +124,16 @@ function getBaseUrl() {
86
124
  return import.meta.env.SITE ?? ''
87
125
  }
88
126
  export { parseResponse, hc }
127
+ export type { DetailedError }
89
128
  export const honoClient = hc<HonoRouter>(getBaseUrl())
90
129
  `;
91
130
 
92
131
  // src/lib/utils.ts
93
132
  var reservedRoutes = ["_astro", "_actions", "_server_islands"];
133
+ var SUPPORTED_ADAPTERS = ["@astrojs/cloudflare", "@astrojs/node", "@astrojs/netlify", "@astrojs/vercel"];
134
+ function isSupportedAdapter(adapter) {
135
+ return SUPPORTED_ADAPTERS.includes(adapter);
136
+ }
94
137
 
95
138
  // src/integration.ts
96
139
  var optionsSchema = z.object({
@@ -119,10 +162,6 @@ var ACTION_PATTERNS = [
119
162
  "src/hono/index.ts",
120
163
  "src/hono.ts"
121
164
  ];
122
- var SUPPORTED_ADAPTERS = ["@astrojs/cloudflare"];
123
- function isSupportedAdapter(adapter) {
124
- return SUPPORTED_ADAPTERS.includes(adapter);
125
- }
126
165
  var integration_default = defineIntegration({
127
166
  name: "@gnosticdev/hono-actions",
128
167
  optionsSchema,
@@ -164,33 +203,31 @@ ${ACTION_PATTERNS.map((p) => ` - ${p}`).join("\n")}`
164
203
  "router.ts"
165
204
  );
166
205
  const relFromGenToActions = path.relative(codeGenDir.pathname, resolvedActionsPath).split(path.sep).join("/");
167
- const routerContent = generateRouter({
168
- basePath,
169
- relativeActionsPath: relFromGenToActions
170
- });
171
- await fs.writeFile(routerPathAbs, routerContent, "utf-8");
172
- const astroHandlerPathAbs = path.join(
173
- codeGenDir.pathname,
174
- "api.ts"
175
- );
176
206
  const adapter = params.config.adapter?.name;
177
207
  if (!adapter) {
178
208
  logger.error(
179
209
  `No Astro adapter found. Add one of:
180
- - ${SUPPORTED_ADAPTERS.join("\n - ")} to your astro.config.mjs`
210
+ - ${SUPPORTED_ADAPTERS.join("\n - ")} to your astro.config.mjs`
181
211
  );
182
212
  return;
183
213
  }
184
- let astroHandlerContent;
185
- if (isSupportedAdapter(adapter)) {
186
- astroHandlerContent = generateAstroHandler(adapter);
187
- } else {
188
- throw new Error(`Unsupported adapter: ${adapter}`, {
189
- cause: `Only ${SUPPORTED_ADAPTERS.join(
190
- ", "
191
- )} are supported for now`
192
- });
214
+ if (!isSupportedAdapter(adapter)) {
215
+ logger.error(
216
+ `Unsupported adapter: ${adapter}. Only ${SUPPORTED_ADAPTERS.join("\n - ")} are supported`
217
+ );
218
+ return;
193
219
  }
220
+ const routerContent = generateRouter({
221
+ basePath,
222
+ relativeActionsPath: relFromGenToActions,
223
+ adapter
224
+ });
225
+ await fs.writeFile(routerPathAbs, routerContent, "utf-8");
226
+ const astroHandlerPathAbs = path.join(
227
+ codeGenDir.pathname,
228
+ "api.ts"
229
+ );
230
+ const astroHandlerContent = generateAstroHandler(adapter);
194
231
  await fs.writeFile(
195
232
  astroHandlerPathAbs,
196
233
  astroHandlerContent,
@@ -241,22 +278,28 @@ export {}
241
278
  declare module '@gnosticdev/hono-actions/client' {
242
279
  export const honoClient: typeof import('./client').honoClient
243
280
  export const parseResponse: typeof import('./client').parseResponse
281
+ exoprt type DetailedError = import('./client').DetailedError
244
282
  }
245
283
  `;
246
- if (!config.adapter?.name) {
284
+ const adapter = config.adapter?.name;
285
+ if (!adapter) {
247
286
  logger.warn("No adapter found...");
248
287
  return;
249
288
  }
250
- if (config.adapter.name !== "@astrojs/cloudflare") {
251
- logger.warn("Unsupported adapter...");
289
+ if (!isSupportedAdapter(adapter)) {
290
+ logger.warn(
291
+ `Unsupported adapter: ${adapter}. Only ${SUPPORTED_ADAPTERS.join("\n - ")} are supported`
292
+ );
252
293
  return;
253
294
  }
254
- clientTypes += `
295
+ if (adapter === "@astrojs/cloudflare") {
296
+ clientTypes += `
255
297
  type Runtime = import('@astrojs/cloudflare').Runtime<Env>
256
- declare namespace App {
257
- interface Locals extends Runtime {}
258
- }
259
- `;
298
+ declare namespace App {
299
+ interface Locals extends Runtime {}
300
+ }
301
+ `;
302
+ }
260
303
  injectTypes({
261
304
  filename: "types.d.ts",
262
305
  content: clientTypes
package/package.json CHANGED
@@ -14,7 +14,12 @@
14
14
  "devDependencies": {
15
15
  "tsup": "^8.5.0",
16
16
  "typescript": "catalog:",
17
- "vitest": "catalog:"
17
+ "vitest": "catalog:",
18
+ "@astrojs/cloudflare": "catalog:",
19
+ "@astrojs/netlify": "catalog:",
20
+ "@astrojs/node": "catalog:",
21
+ "@astrojs/vercel": "catalog:",
22
+ "astro": "catalog:"
18
23
  },
19
24
  "exports": {
20
25
  ".": {
@@ -56,5 +61,5 @@
56
61
  },
57
62
  "type": "module",
58
63
  "types": "./dist/index.d.ts",
59
- "version": "1.2.4"
64
+ "version": "2.0.0"
60
65
  }