@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 +36 -2
- package/package.json +1 -1
- package/src/actions.ts +23 -15
- package/src/clientjs/hyperspan-client.ts +29 -16
- package/src/server.ts +50 -2
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
|
-
|
|
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
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 =
|
|
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: (
|
|
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(
|
|
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
|
-
| ((
|
|
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(
|
|
107
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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(
|