@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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(bun test:*)"
5
+ ]
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "1.0.14",
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.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
  });
@@ -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
  }