@hyperspan/framework 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "src/server.ts",
6
6
  "types": "src/server.ts",
@@ -67,12 +67,14 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/bun": "^1.3.1",
70
+ "@types/debug": "^4.1.12",
70
71
  "@types/node": "^24.10.0",
71
72
  "prettier": "^3.5.2",
72
73
  "typescript": "^5.9.3"
73
74
  },
74
75
  "dependencies": {
75
76
  "@hyperspan/html": "^1.0.0",
77
+ "debug": "^4.4.3",
76
78
  "isbot": "^5.1.32",
77
79
  "zod": "^4.1.12"
78
80
  }
@@ -40,6 +40,74 @@ describe('createAction', () => {
40
40
  expect(htmlString).toContain('name="name"');
41
41
  });
42
42
 
43
+ test('creates an action with a simple form and no schema that returns HTML on POST', async () => {
44
+ const action = createAction({
45
+ name: 'test',
46
+ }).form((c) => {
47
+ return html`
48
+ <form>
49
+ <input type="text" name="name" />
50
+ <button type="submit">Submit</button>
51
+ </form>
52
+ `;
53
+ }).post(async (c, { data }) => {
54
+ return c.res.html(`
55
+ <p>Hello, ${data?.name}!</p>
56
+ `);
57
+ });
58
+
59
+ // Build form data
60
+ const formData = new FormData();
61
+ formData.append('name', 'John Doe');
62
+
63
+ // Test render method
64
+ const request = new Request(`http://localhost:3000${action._path()}`, {
65
+ method: 'POST',
66
+ body: formData,
67
+ });
68
+ const response = await action.fetch(request);
69
+ expect(response).toBeInstanceOf(Response);
70
+ expect(response.status).toBe(200);
71
+ const responseText = await response.text();
72
+ expect(responseText).toContain('<p>Hello, John Doe!</p>');
73
+ });
74
+
75
+ test('errors thrown on POST handler provided by user are caught and rendered', async () => {
76
+ const action = createAction({
77
+ name: 'test',
78
+ }).form((c, { error }) => {
79
+ if (error) {
80
+ return html`
81
+ <p>Error: ${error.message}</p>
82
+ `;
83
+ }
84
+
85
+ return html`
86
+ <form>
87
+ <input type="text" name="name" />
88
+ <button type="submit">Submit</button>
89
+ </form>
90
+ `;
91
+ }).post(async (c, { data }) => {
92
+ throw new Error('Test error');
93
+ });
94
+
95
+ // Build form data
96
+ const formData = new FormData();
97
+ formData.append('name', 'John Doe');
98
+
99
+ // Test render method
100
+ const request = new Request(`http://localhost:3000${action._path()}`, {
101
+ method: 'POST',
102
+ body: formData,
103
+ });
104
+ const response = await action.fetch(request);
105
+ expect(response).toBeInstanceOf(Response);
106
+ expect(response.status).toBe(500);
107
+ const responseText = await response.text();
108
+ expect(responseText).toContain('<p>Error: Test error</p>');
109
+ });
110
+
43
111
  test('creates an action with a Zod schema matching form inputs', async () => {
44
112
  const schema = z.object({
45
113
  name: z.string().min(1, 'Name is required'),
package/src/actions.ts CHANGED
@@ -5,7 +5,9 @@ import type { Hyperspan as HS } from './types';
5
5
  import { assetHash, formDataToJSON } from './utils';
6
6
  import { buildClientJS } from './client/js';
7
7
  import { validateBody, ZodValidationError } from './middleware';
8
+ import { debug } from 'debug';
8
9
 
10
+ const log = debug('hyperspan:actions');
9
11
  const actionsClientJS = await buildClientJS(import.meta.resolve('./client/_hs/hyperspan-actions.client'));
10
12
 
11
13
  /**
@@ -35,7 +37,10 @@ export function createAction<T extends z.ZodObject<any, any>>(params: { name: st
35
37
  throw new Error('Action POST handler not set! Every action must have a POST handler.');
36
38
  }
37
39
 
38
- const response = await _handler(c, { data: c.vars.body });
40
+ const data = c.vars.body as z.infer<T> || formDataToJSON(await c.req.formData()) || {};
41
+ log('POST handler', { data });
42
+ const response = await _handler(c, { data });
43
+ log('POST handler response', { response });
39
44
 
40
45
  if (response instanceof Response) {
41
46
  // Replace redirects with special header because fetch() automatically follows redirects
@@ -52,11 +57,13 @@ export function createAction<T extends z.ZodObject<any, any>>(params: { name: st
52
57
  * Custom error handler for the action since validateBody() throws a HTTPResponseException
53
58
  */
54
59
  .errorHandler(async (c: HS.Context, err: HTTPResponseException) => {
55
- const data = c.vars.body as Partial<z.infer<T>>;
56
- const error = err._error as ZodValidationError;
60
+ const data = c.vars.body as z.infer<T> || formDataToJSON(await c.req.formData()) || {};
61
+ const error = err._error as ZodValidationError || err;
57
62
 
58
- // Set the status to 400 by default
59
- c.res.status = 400;
63
+ // Set the status to 400 if it's a ZodValidationError, otherwise 500 (Error thrown by user POST handler)
64
+ c.res.status = err._error ? 400 : 500;
65
+
66
+ log('errorHandler', { data, error });
60
67
 
61
68
  return await returnHTMLResponse(c, () => {
62
69
  return _errorHandler ? _errorHandler(c, { data, error }) : api.render(c, { data, error });