@hyperspan/framework 1.0.6 → 1.0.8

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.6",
3
+ "version": "1.0.8",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "src/server.ts",
6
6
  "types": "src/server.ts",
@@ -256,7 +256,7 @@ describe('createAction', () => {
256
256
 
257
257
  const responseText = await response.text();
258
258
  // Should render the custom error handler
259
- expect(responseText).toContain('Caught error in custom error handler: Input validation error(s)');
259
+ expect(responseText).toContain('Invalid email address');
260
260
  expect(responseText).toContain('Data: {"email":"not-an-email"}');
261
261
  // Should NOT contain the success message from post handler
262
262
  expect(responseText).not.toContain('Hello,');
package/src/actions.ts CHANGED
@@ -84,8 +84,8 @@ export function createAction<T extends z.ZodObject<any, any>>(params: { name: st
84
84
  * Form to render
85
85
  * This will be wrapped in a <hs-action> web component and submitted via fetch()
86
86
  */
87
- form(form: HS.ActionFormHandler<T>) {
88
- api._form = form;
87
+ form(_formFn) {
88
+ api._form = _formFn;
89
89
  return api;
90
90
  },
91
91
  /**
@@ -101,7 +101,7 @@ export function createAction<T extends z.ZodObject<any, any>>(params: { name: st
101
101
  /**
102
102
  * Get form renderer method
103
103
  */
104
- render(c: HS.Context, props?: HS.ActionProps<T>) {
104
+ render(c, props) {
105
105
  const formContent = api._form ? api._form(c, props || {}) : null;
106
106
  return formContent ? html`<hs-action url="${this._path()}">${formContent}</hs-action>${actionsClientJS.renderScriptTag()}` : null;
107
107
  },
@@ -109,6 +109,10 @@ export function createAction<T extends z.ZodObject<any, any>>(params: { name: st
109
109
  _errorHandler = handler;
110
110
  return api;
111
111
  },
112
+ use(middleware: HS.MiddlewareFunction) {
113
+ route.use(middleware);
114
+ return api;
115
+ },
112
116
  middleware(middleware: Array<HS.MiddlewareFunction>) {
113
117
  route.middleware(middleware);
114
118
  return api;
package/src/middleware.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { formDataToJSON } from './utils';
2
- import { z, flattenError } from 'zod/v4';
2
+ import { z, flattenError, prettifyError } from 'zod/v4';
3
3
 
4
4
  import type { ZodAny, ZodObject, ZodError } from 'zod/v4';
5
5
  import type { Hyperspan as HS } from './types';
@@ -26,12 +26,15 @@ function inferValidationType(headers: Headers): TValidationType {
26
26
  }
27
27
 
28
28
  export class ZodValidationError extends Error {
29
- constructor(flattened: ReturnType<typeof flattenError>) {
30
- super('Input validation error(s)');
29
+ public fieldErrors;
30
+ public formErrors: unknown[];
31
+ constructor(error: ZodError, schema: ZodObject | ZodAny) {
32
+ const message = prettifyError(error);
33
+ const flattened = flattenError<z.infer<typeof schema>>(error);
34
+ super(message);
31
35
  this.name = 'ZodValidationError';
32
-
33
- // Copy all properties from flattened error
34
- Object.assign(this, flattened);
36
+ this.fieldErrors = flattened.fieldErrors;
37
+ this.formErrors = flattened.formErrors;
35
38
  }
36
39
  }
37
40
 
@@ -44,8 +47,8 @@ export function validateQuery(schema: ZodObject | ZodAny): HS.MiddlewareFunction
44
47
  context.vars.query = validated.data as z.infer<typeof schema>;
45
48
 
46
49
  if (!validated.success) {
47
- const err = formatZodError(validated.error);
48
- return context.res.error(err, { status: 400 });
50
+ const err = new ZodValidationError(validated.error, schema);
51
+ throw new HTTPResponseException(err, { status: 400 });
49
52
  }
50
53
 
51
54
  return next();
@@ -72,20 +75,14 @@ export function validateBody(schema: ZodObject | ZodAny, type?: TValidationType)
72
75
  const validated = schema.safeParse(body);
73
76
 
74
77
  if (!validated.success) {
75
- const err = formatZodError(validated.error);
78
+ const err = new ZodValidationError(validated.error, schema);
76
79
  throw new HTTPResponseException(err, { status: 400 });
77
- //return context.res.error(err, { status: 400 });
78
80
  }
79
81
 
80
82
  return next();
81
83
  }
82
84
  }
83
85
 
84
- export function formatZodError(error: ZodError): ZodValidationError {
85
- const zodError = flattenError(error);
86
- return new ZodValidationError(zodError);
87
- }
88
-
89
86
  /**
90
87
  * Type guard to check if a handler is a middleware function
91
88
  * Middleware functions have 2 parameters (context, next)
package/src/server.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { HSHtml, html, isHSHtml, renderStream, renderAsync, render, _typeOf } from '@hyperspan/html';
2
2
  import { isbot } from 'isbot';
3
3
  import { executeMiddleware } from './middleware';
4
- import { parsePath } from './utils';
4
+ import { parsePath, removeUndefined } from './utils';
5
5
  import { Cookies } from './cookies';
6
6
 
7
7
  import type { Hyperspan as HS } from './types';
@@ -51,7 +51,7 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
51
51
  const headers = new Headers(req.headers);
52
52
  const path = route?._path() || '/';
53
53
  // @ts-ignore - Bun will put 'params' on the Request object even though it's not standardized
54
- const params: HS.RouteParamsParser<path> & Record<string, string | undefined> = Object.assign({}, req?.params || {}, route?._config.params || {});
54
+ const params: HS.RouteParamsParser<path> & Record<string, string | undefined> = Object.assign({}, req?.params || {}, removeUndefined(route?._config.params || {}));
55
55
 
56
56
  // Replace catch-all param with the value from the URL path
57
57
  const catchAllParam = Object.keys(params).find(key => key.startsWith('...'));
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { HSHtml } from '@hyperspan/html';
2
+ import { ZodValidationError } from './middleware';
2
3
  import * as z from 'zod/v4';
3
4
 
4
5
  /**
@@ -156,20 +157,29 @@ export namespace Hyperspan {
156
157
  /**
157
158
  * Action = Form + route handler
158
159
  */
159
- export type ActionResponse = HSHtml | void | null | Promise<HSHtml | void | null> | Response | Promise<Response>;
160
- export type ActionProps<T extends z.ZodTypeAny> = { data?: Partial<z.infer<T>>; error?: z.ZodError | Error };
160
+ // Form renderer
161
+ export type ActionFormResponse = HSHtml | void | null | Promise<HSHtml | void | null>;
162
+ export type ActionFormProps<T extends z.ZodTypeAny> = { data?: Partial<z.infer<T>>; error?: ZodValidationError };
163
+ export type ActionForm<T extends z.ZodTypeAny> = (
164
+ c: Context, props: ActionFormProps<T>
165
+ ) => ActionFormResponse;
166
+ // Form handler
167
+ export type ActionFormHandlerResponse = ActionFormResponse | Response | Promise<Response>;
168
+ export type ActionFormHandlerProps<T extends z.ZodTypeAny> = { data: z.infer<T>; error?: ZodValidationError | Error };
161
169
  export type ActionFormHandler<T extends z.ZodTypeAny> = (
162
- c: Context, props: ActionProps<T>
163
- ) => ActionResponse;
170
+ c: Context, props: ActionFormHandlerProps<T>
171
+ ) => ActionFormHandlerResponse;
172
+ // Action API
164
173
  export interface Action<T extends z.ZodTypeAny> {
165
174
  _kind: 'hsAction';
166
175
  _config: Partial<Hyperspan.RouteConfig>;
167
176
  _path(): string;
168
- _form: null | ActionFormHandler<T>;
169
- form(form: ActionFormHandler<T>): Action<T>;
170
- render: (c: Context, props?: ActionProps<T>) => ActionResponse;
177
+ _form: null | ActionForm<T>;
178
+ form(form: ActionForm<T>): Action<T>;
179
+ render: (c: Context, props?: ActionFormProps<T>) => ActionFormResponse;
171
180
  post: (handler: ActionFormHandler<T>) => Action<T>;
172
181
  errorHandler: (handler: ActionFormHandler<T>) => Action<T>;
182
+ use: (middleware: Hyperspan.MiddlewareFunction) => Action<T>;
173
183
  middleware: (middleware: Array<Hyperspan.MiddlewareFunction>) => Action<T>;
174
184
  fetch: (request: Request) => Promise<Response>;
175
185
  }
package/src/utils.ts CHANGED
@@ -136,4 +136,11 @@ export function formDataToJSON(formData: FormData | URLSearchParams): Record<str
136
136
  }
137
137
 
138
138
  return object;
139
+ }
140
+
141
+ /**
142
+ * Remove undefined values from an object
143
+ */
144
+ export function removeUndefined(obj: Record<string, any>): Record<string, any> {
145
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
139
146
  }