@hyperspan/framework 0.3.4 → 0.4.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/dist/server.js CHANGED
@@ -1858,8 +1858,25 @@ function createRoute(handler) {
1858
1858
  ..._middleware,
1859
1859
  async (context) => {
1860
1860
  const method = context.req.method.toUpperCase();
1861
+ if (method === "OPTIONS") {
1862
+ return context.html(render(html`
1863
+ <!DOCTYPE html>
1864
+ <html lang="en"></html>
1865
+ `), {
1866
+ status: 200,
1867
+ headers: {
1868
+ "Access-Control-Allow-Origin": "*",
1869
+ "Access-Control-Allow-Methods": [
1870
+ "HEAD",
1871
+ "OPTIONS",
1872
+ ...Object.keys(_handlers)
1873
+ ].join(", "),
1874
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
1875
+ }
1876
+ });
1877
+ }
1861
1878
  return returnHTMLResponse(context, () => {
1862
- const handler2 = _handlers[method];
1879
+ const handler2 = method === "HEAD" ? _handlers["GET"] : _handlers[method];
1863
1880
  if (!handler2) {
1864
1881
  throw new HTTPException(405, { message: "Method not allowed" });
1865
1882
  }
@@ -1908,7 +1925,24 @@ function createAPIRoute(handler) {
1908
1925
  ..._middleware,
1909
1926
  async (context) => {
1910
1927
  const method = context.req.method.toUpperCase();
1911
- const handler2 = _handlers[method];
1928
+ if (method === "OPTIONS") {
1929
+ return context.json({
1930
+ meta: { success: true, dtResponse: new Date },
1931
+ data: {}
1932
+ }, {
1933
+ status: 200,
1934
+ headers: {
1935
+ "Access-Control-Allow-Origin": "*",
1936
+ "Access-Control-Allow-Methods": [
1937
+ "HEAD",
1938
+ "OPTIONS",
1939
+ ...Object.keys(_handlers)
1940
+ ].join(", "),
1941
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
1942
+ }
1943
+ });
1944
+ }
1945
+ const handler2 = method === "HEAD" ? _handlers["GET"] : _handlers[method];
1912
1946
  if (!handler2) {
1913
1947
  return context.json({
1914
1948
  meta: { success: false, dtResponse: new Date },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "dist/server.ts",
6
6
  "types": "src/server.ts",
package/src/actions.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { html, HSHtml } from '@hyperspan/html';
2
2
  import * as z from 'zod/v4';
3
3
  import { HTTPException } from 'hono/http-exception';
4
-
4
+ import { assetHash } from './assets';
5
5
  import { IS_PROD, returnHTMLResponse, type THSResponseTypes } from './server';
6
6
  import type { Context, MiddlewareHandler } from 'hono';
7
7
  import type { HandlerResponse, Next, TypedResponse } from 'hono/types';
8
- import { assetHash } from './assets';
9
8
 
10
9
  /**
11
10
  * Actions = Form + route handler
@@ -20,13 +19,19 @@ import { assetHash } from './assets';
20
19
  * 5. Replaces form content in place with HTML response content from server via the Idiomorph library
21
20
  * 6. Handles any Exception thrown on server as error displayed back to user on the page
22
21
  */
23
- type TActionResponse = THSResponseTypes | HandlerResponse<any> | TypedResponse<any, any, any>;
22
+ export type TActionResponse =
23
+ | THSResponseTypes
24
+ | HandlerResponse<any>
25
+ | TypedResponse<any, any, any>;
24
26
  export interface HSAction<T extends z.ZodTypeAny> {
25
27
  _kind: string;
26
28
  _route: string;
27
29
  _form: Parameters<HSAction<T>['form']>[0];
28
30
  form(
29
- renderForm: ({ data, error }: { data?: z.infer<T>; error?: z.ZodError | Error }) => HSHtml
31
+ renderForm: (
32
+ c: Context<any, any, {}>,
33
+ { data, error }: { data?: z.infer<T>; error?: z.ZodError | Error }
34
+ ) => HSHtml | void | null | Promise<HSHtml | void | null>
30
35
  ): HSAction<T>;
31
36
  post(
32
37
  handler: (
@@ -40,12 +45,17 @@ export interface HSAction<T extends z.ZodTypeAny> {
40
45
  { data, error }: { data?: z.infer<T>; error?: z.ZodError | Error }
41
46
  ) => TActionResponse
42
47
  ): HSAction<T>;
43
- render(props?: { data?: z.infer<T>; error?: z.ZodError | Error }): TActionResponse;
48
+ render(
49
+ c: Context<any, any, {}>,
50
+ props?: { data?: z.infer<T>; error?: z.ZodError | Error }
51
+ ): TActionResponse;
44
52
  run(c: Context<any, any, {}>): TActionResponse | Promise<TActionResponse>;
45
53
  middleware: (
46
54
  middleware: Array<
47
55
  | MiddlewareHandler
48
- | ((context: Context<any, string, {}>) => TActionResponse | Promise<TActionResponse>)
56
+ | ((
57
+ context: Context<any, string, {}>
58
+ ) => TActionResponse | Promise<TActionResponse> | void | Promise<void>)
49
59
  >
50
60
  ) => HSAction<T>;
51
61
  _getRouteHandlers: () => Array<
@@ -103,8 +113,11 @@ export function unstable__createAction<T extends z.ZodTypeAny>(
103
113
  /**
104
114
  * Get form renderer method
105
115
  */
106
- render(formState?: { data?: z.infer<T>; error?: z.ZodError | Error }) {
107
- const form = _form ? _form(formState || {}) : null;
116
+ render(
117
+ c: Context<any, any, {}>,
118
+ formState?: { data?: z.infer<T>; error?: z.ZodError | Error }
119
+ ) {
120
+ const form = _form ? _form(c, formState || {}) : null;
108
121
  return form ? html`<hs-action url="${this._route}">${form}</hs-action>` : null;
109
122
  },
110
123
 
@@ -136,7 +149,7 @@ export function unstable__createAction<T extends z.ZodTypeAny>(
136
149
  const method = c.req.method;
137
150
 
138
151
  if (method === 'GET') {
139
- return await api.render();
152
+ return await api.render(c);
140
153
  }
141
154
 
142
155
  if (method !== 'POST') {
@@ -171,18 +184,13 @@ export function unstable__createAction<T extends z.ZodTypeAny>(
171
184
  });
172
185
  }
173
186
 
174
- return await returnHTMLResponse(c, () => api.render({ data, error }), { status: 400 });
187
+ return await returnHTMLResponse(c, () => api.render(c, { data, error }), { status: 400 });
175
188
  },
176
189
  };
177
190
 
178
191
  return api;
179
192
  }
180
193
 
181
- /**
182
- * Form route handler helper
183
- */
184
- export type THSHandlerResponse = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
185
-
186
194
  /**
187
195
  * Return JSON data structure for a given FormData object
188
196
  * Accounts for array fields (e.g. name="options[]" or <select multiple>)
@@ -73,24 +73,40 @@ class HSAction extends HTMLElement {
73
73
  }
74
74
 
75
75
  connectedCallback() {
76
- // Have to run this code AFTER it is added to the DOM...
77
76
  setTimeout(() => {
78
- const form = this.querySelector('form');
79
-
80
- if (form) {
81
- form.setAttribute('action', this.getAttribute('url') || '');
82
- const submitHandler = (e: Event) => {
83
- formSubmitToRoute(e, form as HTMLFormElement, {
84
- afterResponse: () => this.connectedCallback(),
85
- });
86
- form.removeEventListener('submit', submitHandler);
87
- };
88
- form.addEventListener('submit', submitHandler);
77
+ bindHSActionForm(this, this.querySelector('form') as HTMLFormElement);
78
+ actionFormObserver.observe(this, { childList: true, subtree: true });
79
+ }, 10);
80
+ }
81
+ }
82
+ window.customElements.define('hs-action', HSAction);
83
+ const actionFormObserver = new MutationObserver((list) => {
84
+ list.forEach((mutation) => {
85
+ mutation.addedNodes.forEach((node) => {
86
+ if (node instanceof HTMLFormElement) {
87
+ bindHSActionForm(node.closest('hs-action') as HSAction, node);
89
88
  }
90
89
  });
90
+ });
91
+ });
92
+
93
+ /**
94
+ * Bind the form inside an hs-action element to the action URL and submit handler
95
+ */
96
+ function bindHSActionForm(hsActionElement: HSAction, form: HTMLFormElement) {
97
+ if (!form) {
98
+ return;
91
99
  }
100
+
101
+ form.setAttribute('action', hsActionElement.getAttribute('url') || '');
102
+ const submitHandler = (e: Event) => {
103
+ formSubmitToRoute(e, form as HTMLFormElement, {
104
+ afterResponse: () => bindHSActionForm(hsActionElement, form),
105
+ });
106
+ form.removeEventListener('submit', submitHandler);
107
+ };
108
+ form.addEventListener('submit', submitHandler);
92
109
  }
93
- window.customElements.define('hs-action', HSAction);
94
110
 
95
111
  /**
96
112
  * Submit form data to route and replace contents with response
@@ -107,8 +123,6 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
107
123
  'X-Request-Type': 'partial',
108
124
  };
109
125
 
110
- let response: Response;
111
-
112
126
  const hsActionTag = form.closest('hs-action');
113
127
  const submitBtn = form.querySelector('button[type=submit],input[type=submit]');
114
128
  if (submitBtn) {
@@ -127,7 +141,6 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
127
141
  return '';
128
142
  }
129
143
 
130
- response = res;
131
144
  return res.text();
132
145
  })
133
146
  .then((content: string) => {
package/src/server.ts CHANGED
@@ -83,8 +83,32 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
83
83
  async (context: Context) => {
84
84
  const method = context.req.method.toUpperCase();
85
85
 
86
+ // Handle CORS preflight requests
87
+ if (method === 'OPTIONS') {
88
+ return context.html(
89
+ render(html`
90
+ <!DOCTYPE html>
91
+ <html lang="en"></html>
92
+ `),
93
+ {
94
+ status: 200,
95
+ headers: {
96
+ 'Access-Control-Allow-Origin': '*',
97
+ 'Access-Control-Allow-Methods': [
98
+ 'HEAD',
99
+ 'OPTIONS',
100
+ ...Object.keys(_handlers),
101
+ ].join(', '),
102
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
103
+ },
104
+ }
105
+ );
106
+ }
107
+
108
+ // Handle other requests, HEAD is GET with no body
86
109
  return returnHTMLResponse(context, () => {
87
- const handler = _handlers[method];
110
+ const handler = method === 'HEAD' ? _handlers['GET'] : _handlers[method];
111
+
88
112
  if (!handler) {
89
113
  throw new HTTPException(405, { message: 'Method not allowed' });
90
114
  }
@@ -142,7 +166,31 @@ export function createAPIRoute(handler?: THSAPIRouteHandler): THSAPIRoute {
142
166
  ..._middleware,
143
167
  async (context: Context) => {
144
168
  const method = context.req.method.toUpperCase();
145
- const handler = _handlers[method];
169
+
170
+ // Handle CORS preflight requests
171
+ if (method === 'OPTIONS') {
172
+ return context.json(
173
+ {
174
+ meta: { success: true, dtResponse: new Date() },
175
+ data: {},
176
+ },
177
+ {
178
+ status: 200,
179
+ headers: {
180
+ 'Access-Control-Allow-Origin': '*',
181
+ 'Access-Control-Allow-Methods': [
182
+ 'HEAD',
183
+ 'OPTIONS',
184
+ ...Object.keys(_handlers),
185
+ ].join(', '),
186
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
187
+ },
188
+ }
189
+ );
190
+ }
191
+
192
+ // Handle other requests, HEAD is GET with no body
193
+ const handler = method === 'HEAD' ? _handlers['GET'] : _handlers[method];
146
194
 
147
195
  if (!handler) {
148
196
  return context.json(