@hyperspan/framework 1.0.14 → 1.0.16
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.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"main": "src/server.ts",
|
|
6
6
|
"types": "src/server.ts",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"typescript": "^5.9.3"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@hyperspan/html": "^1.0.
|
|
76
|
+
"@hyperspan/html": "^1.0.1",
|
|
77
77
|
"debug": "^4.4.3",
|
|
78
78
|
"isbot": "^5.1.32",
|
|
79
79
|
"zod": "^4.1.12"
|
|
@@ -58,11 +58,19 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
58
58
|
const formData = new FormData(form);
|
|
59
59
|
const formUrl = form.getAttribute('action') || '';
|
|
60
60
|
const method = form.getAttribute('method')?.toUpperCase() || 'POST';
|
|
61
|
+
const confirmMessage = form.getAttribute('data-confirm') || '';
|
|
61
62
|
const headers = {
|
|
62
63
|
Accept: 'text/html',
|
|
63
64
|
'X-Request-Type': 'partial',
|
|
64
65
|
};
|
|
65
66
|
|
|
67
|
+
if (confirmMessage) {
|
|
68
|
+
const confirmed = window.confirm(confirmMessage);
|
|
69
|
+
if (!confirmed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
const hsActionTag = form.closest('hs-action');
|
|
67
75
|
const submitBtn = form.querySelector('button[type=submit],input[type=submit]');
|
|
68
76
|
if (submitBtn) {
|
|
@@ -92,6 +100,13 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
92
100
|
const target = content.includes('<html') ? window.document.body : hsActionTag || form;
|
|
93
101
|
|
|
94
102
|
Idiomorph.morph(target, content, { morphStyle: 'innerHTML' });
|
|
103
|
+
|
|
104
|
+
// Check for nested hs-action elements and remove them if present
|
|
105
|
+
const outerElement = target.querySelector('hs-action');
|
|
106
|
+
if (outerElement) {
|
|
107
|
+
outerElement.replaceWith(...outerElement.childNodes);
|
|
108
|
+
}
|
|
109
|
+
|
|
95
110
|
opts.afterResponse && opts.afterResponse();
|
|
96
111
|
lazyLoadScripts();
|
|
97
112
|
});
|
package/src/server.test.ts
CHANGED
|
@@ -214,6 +214,75 @@ test('createContext() merge() function preserves custom headers with json() meth
|
|
|
214
214
|
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
test('route returning AsyncGenerator produces a streaming response', async () => {
|
|
218
|
+
async function* streamingHandler() {
|
|
219
|
+
yield '<h1>Hello</h1>';
|
|
220
|
+
yield '<p>World</p>';
|
|
221
|
+
yield '<p>Streaming</p>';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const route = createRoute().get(async (_context: HS.Context) => {
|
|
225
|
+
return streamingHandler();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const request = new Request('http://localhost:3000/');
|
|
229
|
+
const response = await route.fetch(request);
|
|
230
|
+
|
|
231
|
+
expect(response).toBeInstanceOf(Response);
|
|
232
|
+
expect(response.status).toBe(200);
|
|
233
|
+
expect(response.headers.get('Transfer-Encoding')).toBe('chunked');
|
|
234
|
+
expect(response.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
|
|
235
|
+
|
|
236
|
+
const text = await response.text();
|
|
237
|
+
expect(text).toBe('<h1>Hello</h1><p>World</p><p>Streaming</p>');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('route returning a sync Generator produces a streaming response', async () => {
|
|
241
|
+
function* streamingHandler() {
|
|
242
|
+
yield '<h1>Hello</h1>';
|
|
243
|
+
yield '<p>World</p>';
|
|
244
|
+
yield '<p>Streaming</p>';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const route = createRoute().get((_context: HS.Context) => {
|
|
248
|
+
return streamingHandler();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const request = new Request('http://localhost:3000/');
|
|
252
|
+
const response = await route.fetch(request);
|
|
253
|
+
|
|
254
|
+
expect(response).toBeInstanceOf(Response);
|
|
255
|
+
expect(response.status).toBe(200);
|
|
256
|
+
expect(response.headers.get('Transfer-Encoding')).toBe('chunked');
|
|
257
|
+
expect(response.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
|
|
258
|
+
|
|
259
|
+
const text = await response.text();
|
|
260
|
+
expect(text).toBe('<h1>Hello</h1><p>World</p><p>Streaming</p>');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('route returning a Generator respects Content-Type set on context.res.headers', async () => {
|
|
264
|
+
async function* streamingHandler() {
|
|
265
|
+
yield 'Hello';
|
|
266
|
+
yield ' World';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const route = createRoute().get((context: HS.Context) => {
|
|
270
|
+
context.res.headers.set('Content-Type', 'text/plain');
|
|
271
|
+
return streamingHandler();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const request = new Request('http://localhost:3000/');
|
|
275
|
+
const response = await route.fetch(request);
|
|
276
|
+
|
|
277
|
+
expect(response).toBeInstanceOf(Response);
|
|
278
|
+
expect(response.status).toBe(200);
|
|
279
|
+
expect(response.headers.get('Transfer-Encoding')).toBe('chunked');
|
|
280
|
+
expect(response.headers.get('Content-Type')).toBe('text/plain');
|
|
281
|
+
|
|
282
|
+
const text = await response.text();
|
|
283
|
+
expect(text).toBe('Hello World');
|
|
284
|
+
});
|
|
285
|
+
|
|
217
286
|
test('createContext() merge() function allows response headers to override context headers', async () => {
|
|
218
287
|
const request = new Request('http://localhost:3000/');
|
|
219
288
|
const context = createContext(request);
|
package/src/server.ts
CHANGED
|
@@ -291,7 +291,7 @@ export function createRoute(config: Partial<HS.RouteConfig> = {}): HS.Route {
|
|
|
291
291
|
|
|
292
292
|
const contentType = _typeOf(routeContent);
|
|
293
293
|
if (contentType === 'generator') {
|
|
294
|
-
return new StreamResponse(routeContent as AsyncGenerator);
|
|
294
|
+
return new StreamResponse(routeContent as AsyncGenerator, { headers: Object.fromEntries(context.res.headers.entries()) });
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
return routeContent;
|
|
@@ -545,14 +545,20 @@ export class StreamResponse extends Response {
|
|
|
545
545
|
const { status, headers, ...restOptions } = options;
|
|
546
546
|
const stream = createReadableStreamFromAsyncGenerator(iterator as AsyncGenerator);
|
|
547
547
|
|
|
548
|
+
const mergedHeaders = new Headers({
|
|
549
|
+
'Transfer-Encoding': 'chunked',
|
|
550
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
551
|
+
'Content-Encoding': 'Identity',
|
|
552
|
+
});
|
|
553
|
+
if (headers) {
|
|
554
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
555
|
+
mergedHeaders.set(key, value);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
548
559
|
return new Response(stream, {
|
|
549
560
|
status: status ?? 200,
|
|
550
|
-
headers:
|
|
551
|
-
'Transfer-Encoding': 'chunked',
|
|
552
|
-
'Content-Type': 'text/html; charset=UTF-8',
|
|
553
|
-
'Content-Encoding': 'Identity',
|
|
554
|
-
...(headers ?? {}),
|
|
555
|
-
},
|
|
561
|
+
headers: mergedHeaders,
|
|
556
562
|
...restOptions,
|
|
557
563
|
});
|
|
558
564
|
}
|