@hyperspan/framework 0.3.2 → 0.3.3

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/build.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { build } from 'bun';
2
2
 
3
- const entrypoints = ['./src/server.ts', './src/assets.ts'];
3
+ const entrypoints = ['./src/server.ts', './src/assets.ts', './src/middleware.ts'];
4
4
  const external = ['@hyperspan/html'];
5
5
  const outdir = './dist';
6
6
  const target = 'node';
package/dist/assets.js CHANGED
@@ -1,3 +1,5 @@
1
+ import"./chunk-atw8cdg1.js";
2
+
1
3
  // src/assets.ts
2
4
  import { html } from "@hyperspan/html";
3
5
  import { createHash } from "node:crypto";
@@ -0,0 +1,19 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __toESM = (mod, isNodeMode, target) => {
7
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
8
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
9
+ for (let key of __getOwnPropNames(mod))
10
+ if (!__hasOwnProp.call(to, key))
11
+ __defProp(to, key, {
12
+ get: () => mod[key],
13
+ enumerable: true
14
+ });
15
+ return to;
16
+ };
17
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
18
+
19
+ export { __toESM, __commonJS };
@@ -0,0 +1,178 @@
1
+ import {
2
+ __commonJS,
3
+ __toESM
4
+ } from "./chunk-atw8cdg1.js";
5
+
6
+ // ../../node_modules/timestring/index.js
7
+ var require_timestring = __commonJS((exports, module) => {
8
+ module.exports = parseTimestring;
9
+ var DEFAULT_OPTS = {
10
+ hoursPerDay: 24,
11
+ daysPerWeek: 7,
12
+ weeksPerMonth: 4,
13
+ monthsPerYear: 12,
14
+ daysPerYear: 365.25
15
+ };
16
+ var UNIT_MAP = {
17
+ ms: ["ms", "milli", "millisecond", "milliseconds"],
18
+ s: ["s", "sec", "secs", "second", "seconds"],
19
+ m: ["m", "min", "mins", "minute", "minutes"],
20
+ h: ["h", "hr", "hrs", "hour", "hours"],
21
+ d: ["d", "day", "days"],
22
+ w: ["w", "week", "weeks"],
23
+ mth: ["mon", "mth", "mths", "month", "months"],
24
+ y: ["y", "yr", "yrs", "year", "years"]
25
+ };
26
+ function parseTimestring(value, returnUnit, opts) {
27
+ opts = Object.assign({}, DEFAULT_OPTS, opts || {});
28
+ if (typeof value === "number" || value.match(/^[-+]?[0-9.]+$/g)) {
29
+ value = parseInt(value) + "ms";
30
+ }
31
+ let totalSeconds = 0;
32
+ const unitValues = getUnitValues(opts);
33
+ const groups = value.toLowerCase().replace(/[^.\w+-]+/g, "").match(/[-+]?[0-9.]+[a-z]+/g);
34
+ if (groups === null) {
35
+ throw new Error(`The value [${value}] could not be parsed by timestring`);
36
+ }
37
+ groups.forEach((group) => {
38
+ const value2 = group.match(/[0-9.]+/g)[0];
39
+ const unit = group.match(/[a-z]+/g)[0];
40
+ totalSeconds += getSeconds(value2, unit, unitValues);
41
+ });
42
+ if (returnUnit) {
43
+ return convert(totalSeconds, returnUnit, unitValues);
44
+ }
45
+ return totalSeconds;
46
+ }
47
+ function getUnitValues(opts) {
48
+ const unitValues = {
49
+ ms: 0.001,
50
+ s: 1,
51
+ m: 60,
52
+ h: 3600
53
+ };
54
+ unitValues.d = opts.hoursPerDay * unitValues.h;
55
+ unitValues.w = opts.daysPerWeek * unitValues.d;
56
+ unitValues.mth = opts.daysPerYear / opts.monthsPerYear * unitValues.d;
57
+ unitValues.y = opts.daysPerYear * unitValues.d;
58
+ return unitValues;
59
+ }
60
+ function getUnitKey(unit) {
61
+ for (const key of Object.keys(UNIT_MAP)) {
62
+ if (UNIT_MAP[key].indexOf(unit) > -1) {
63
+ return key;
64
+ }
65
+ }
66
+ throw new Error(`The unit [${unit}] is not supported by timestring`);
67
+ }
68
+ function getSeconds(value, unit, unitValues) {
69
+ return value * unitValues[getUnitKey(unit)];
70
+ }
71
+ function convert(value, unit, unitValues) {
72
+ return value / unitValues[getUnitKey(unit)];
73
+ }
74
+ });
75
+
76
+ // ../../node_modules/hono/dist/middleware/etag/digest.js
77
+ var mergeBuffers = (buffer1, buffer2) => {
78
+ if (!buffer1) {
79
+ return buffer2;
80
+ }
81
+ const merged = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
82
+ merged.set(new Uint8Array(buffer1), 0);
83
+ merged.set(buffer2, buffer1.byteLength);
84
+ return merged;
85
+ };
86
+ var generateDigest = async (stream, generator) => {
87
+ if (!stream) {
88
+ return null;
89
+ }
90
+ let result = undefined;
91
+ const reader = stream.getReader();
92
+ for (;; ) {
93
+ const { value, done } = await reader.read();
94
+ if (done) {
95
+ break;
96
+ }
97
+ result = await generator(mergeBuffers(result, value));
98
+ }
99
+ if (!result) {
100
+ return null;
101
+ }
102
+ return Array.prototype.map.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, "0")).join("");
103
+ };
104
+
105
+ // ../../node_modules/hono/dist/middleware/etag/index.js
106
+ var RETAINED_304_HEADERS = [
107
+ "cache-control",
108
+ "content-location",
109
+ "date",
110
+ "etag",
111
+ "expires",
112
+ "vary"
113
+ ];
114
+ function etagMatches(etag2, ifNoneMatch) {
115
+ return ifNoneMatch != null && ifNoneMatch.split(/,\s*/).indexOf(etag2) > -1;
116
+ }
117
+ function initializeGenerator(generator) {
118
+ if (!generator) {
119
+ if (crypto && crypto.subtle) {
120
+ generator = (body) => crypto.subtle.digest({
121
+ name: "SHA-1"
122
+ }, body);
123
+ }
124
+ }
125
+ return generator;
126
+ }
127
+ var etag = (options) => {
128
+ const retainedHeaders = options?.retainedHeaders ?? RETAINED_304_HEADERS;
129
+ const weak = options?.weak ?? false;
130
+ const generator = initializeGenerator(options?.generateDigest);
131
+ return async function etag2(c, next) {
132
+ const ifNoneMatch = c.req.header("If-None-Match") ?? null;
133
+ await next();
134
+ const res = c.res;
135
+ let etag3 = res.headers.get("ETag");
136
+ if (!etag3) {
137
+ if (!generator) {
138
+ return;
139
+ }
140
+ const hash = await generateDigest(res.clone().body, generator);
141
+ if (hash === null) {
142
+ return;
143
+ }
144
+ etag3 = weak ? `W/"${hash}"` : `"${hash}"`;
145
+ }
146
+ if (etagMatches(etag3, ifNoneMatch)) {
147
+ c.res = new Response(null, {
148
+ status: 304,
149
+ statusText: "Not Modified",
150
+ headers: {
151
+ ETag: etag3
152
+ }
153
+ });
154
+ c.res.headers.forEach((_, key) => {
155
+ if (retainedHeaders.indexOf(key.toLowerCase()) === -1) {
156
+ c.res.headers.delete(key);
157
+ }
158
+ });
159
+ } else {
160
+ c.res.headers.set("ETag", etag3);
161
+ }
162
+ };
163
+ };
164
+
165
+ // src/middleware.ts
166
+ var import_timestring = __toESM(require_timestring(), 1);
167
+ function cacheTime(timeStrOrSeconds) {
168
+ return (c, next) => etag()(c, () => {
169
+ if (c.req.method.toUpperCase() === "GET") {
170
+ const timeInSeconds = typeof timeStrOrSeconds === "number" ? timeStrOrSeconds : import_timestring.default(timeStrOrSeconds);
171
+ c.header("Cache-Control", `public, max-age=${timeInSeconds}`);
172
+ }
173
+ return next();
174
+ });
175
+ }
176
+ export {
177
+ cacheTime
178
+ };
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  buildClientCSS,
3
3
  buildClientJS
4
4
  } from "./assets.js";
5
+ import"./chunk-atw8cdg1.js";
5
6
 
6
7
  // src/server.ts
7
8
  import { readdir } from "node:fs/promises";
@@ -1869,7 +1870,7 @@ function createRoute(handler) {
1869
1870
  const streamOpt = context.req.query("__nostream");
1870
1871
  const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1871
1872
  if (isHSHtml(routeContent)) {
1872
- if (streamingEnabled) {
1873
+ if (streamingEnabled && routeContent.asyncContent?.length > 0) {
1873
1874
  return new StreamResponse(renderStream(routeContent));
1874
1875
  } else {
1875
1876
  const output = await renderAsync(routeContent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "dist/server.ts",
6
6
  "types": "src/server.ts",
@@ -17,13 +17,17 @@
17
17
  "types": "./src/server.ts",
18
18
  "default": "./dist/server.js"
19
19
  },
20
- "./actions": {
21
- "types": "./src/actions.ts",
22
- "default": "./src/actions.ts"
23
- },
24
20
  "./assets": {
25
21
  "types": "./src/assets.ts",
26
22
  "default": "./dist/assets.js"
23
+ },
24
+ "./middleware": {
25
+ "types": "./src/middleware.ts",
26
+ "default": "./dist/middleware.js"
27
+ },
28
+ "./unstable/actions": {
29
+ "types": "./src/actions.ts",
30
+ "default": "./src/actions.ts"
27
31
  }
28
32
  },
29
33
  "author": "Vance Lucas <vance@vancelucas.com>",
@@ -56,6 +60,7 @@
56
60
  "@types/bun": "^1.2.14",
57
61
  "@types/node": "^22.15.20",
58
62
  "@types/react": "^19.1.5",
63
+ "@types/timestring": "^7.0.0",
59
64
  "prettier": "^3.5.3",
60
65
  "typescript": "^5.8.3"
61
66
  },
@@ -63,6 +68,7 @@
63
68
  "@hyperspan/html": "^0.1.7",
64
69
  "hono": "^4.7.10",
65
70
  "isbot": "^5.1.28",
66
- "zod": "^3.25.42"
71
+ "timestring": "^7.0.0",
72
+ "zod": "^3.25.67"
67
73
  }
68
74
  }
@@ -1,5 +1,5 @@
1
- import z from 'zod/v4';
2
- import { createAction } from './actions';
1
+ import { z } from 'zod';
2
+ import { unstable__createAction } from './actions';
3
3
  import { describe, it, expect } from 'bun:test';
4
4
  import { html, render, type HSHtml } from '@hyperspan/html';
5
5
  import type { Context } from 'hono';
@@ -22,7 +22,7 @@ describe('createAction', () => {
22
22
  const schema = z.object({
23
23
  name: z.string(),
24
24
  });
25
- const action = createAction(schema, formWithNameOnly);
25
+ const action = unstable__createAction(schema, formWithNameOnly);
26
26
 
27
27
  const formResponse = render(action.render({ data: { name: 'John' } }) as HSHtml);
28
28
  expect(formResponse).toContain('value="John"');
@@ -34,7 +34,7 @@ describe('createAction', () => {
34
34
  const schema = z.object({
35
35
  name: z.string().nonempty(),
36
36
  });
37
- const action = createAction(schema, formWithNameOnly)
37
+ const action = unstable__createAction(schema, formWithNameOnly)
38
38
  .post((c, { data }) => {
39
39
  return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
40
40
  })
@@ -65,7 +65,7 @@ describe('createAction', () => {
65
65
  const schema = z.object({
66
66
  name: z.string().nonempty(),
67
67
  });
68
- const action = createAction(schema)
68
+ const action = unstable__createAction(schema)
69
69
  .form(formWithNameOnly)
70
70
  .post((c, { data }) => {
71
71
  return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
package/src/actions.ts CHANGED
@@ -32,7 +32,7 @@ export interface HSAction<T extends z.ZodTypeAny> {
32
32
  run(method: 'GET' | 'POST', c: Context): Promise<THSResponseTypes>;
33
33
  }
34
34
 
35
- export function createAction<T extends z.ZodTypeAny>(
35
+ export function unstable__createAction<T extends z.ZodTypeAny>(
36
36
  schema: T | null = null,
37
37
  form: Parameters<HSAction<T>['form']>[0] | null = null
38
38
  ) {
@@ -87,7 +87,7 @@ export function createAction<T extends z.ZodTypeAny>(
87
87
  }
88
88
 
89
89
  const formData = await c.req.formData();
90
- const jsonData = formDataToJSON(formData);
90
+ const jsonData = unstable__formDataToJSON(formData);
91
91
  const schemaData = schema ? schema.safeParse(jsonData) : null;
92
92
  const data = schemaData?.success ? (schemaData.data as z.infer<T>) : undefined;
93
93
  let error: z.ZodError | Error | null = null;
@@ -128,7 +128,7 @@ export type THSHandlerResponse = (context: Context) => THSResponseTypes | Promis
128
128
  *
129
129
  * @link https://stackoverflow.com/a/75406413
130
130
  */
131
- export function formDataToJSON(formData: FormData): Record<string, string | string[]> {
131
+ export function unstable__formDataToJSON(formData: FormData): Record<string, string | string[]> {
132
132
  let object = {};
133
133
 
134
134
  /**
@@ -1,5 +1,5 @@
1
1
  import { html } from '@hyperspan/html';
2
- import { Idiomorph } from './idiomorph.esm';
2
+ import { Idiomorph } from './idiomorph';
3
3
 
4
4
  /**
5
5
  * Used for streaming content from the server to the client.
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /**
2
3
  * @typedef {object} ConfigHead
3
4
  *
@@ -206,9 +207,8 @@ var Idiomorph = (function () {
206
207
  */
207
208
  function saveAndRestoreFocus(ctx, fn) {
208
209
  if (!ctx.config.restoreFocus) return fn();
209
- let activeElement = /** @type {HTMLInputElement|HTMLTextAreaElement|null} */ (
210
- document.activeElement
211
- );
210
+ let activeElement =
211
+ /** @type {HTMLInputElement|HTMLTextAreaElement|null} */ document.activeElement;
212
212
 
213
213
  // don't bother if the active element is not an input or textarea
214
214
  if (
@@ -324,7 +324,7 @@ var Idiomorph = (function () {
324
324
  if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
325
325
  if (ctx.idMap.has(newChild)) {
326
326
  // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm
327
- const newEmptyChild = document.createElement(/** @type {Element} */ (newChild).tagName);
327
+ const newEmptyChild = document.createElement(/** @type {Element} */ newChild.tagName);
328
328
  oldParent.insertBefore(newEmptyChild, insertionPoint);
329
329
  morphNode(newEmptyChild, newChild, ctx);
330
330
  ctx.callbacks.afterNodeAdded(newEmptyChild);
@@ -431,8 +431,8 @@ var Idiomorph = (function () {
431
431
  */
432
432
  function isSoftMatch(oldNode, newNode) {
433
433
  // ok to cast: if one is not element, `id` and `tagName` will be undefined and we'll just compare that.
434
- const oldElt = /** @type {Element} */ (oldNode);
435
- const newElt = /** @type {Element} */ (newNode);
434
+ const oldElt = /** @type {Element} */ oldNode;
435
+ const newElt = /** @type {Element} */ newNode;
436
436
 
437
437
  return (
438
438
  oldElt.nodeType === newElt.nodeType &&
@@ -483,7 +483,7 @@ var Idiomorph = (function () {
483
483
  let cursor = startInclusive;
484
484
  // remove nodes until the endExclusive node
485
485
  while (cursor && cursor !== endExclusive) {
486
- let tempNode = /** @type {Node} */ (cursor);
486
+ let tempNode = /** @type {Node} */ cursor;
487
487
  cursor = cursor.nextSibling;
488
488
  removeNode(ctx, tempNode);
489
489
  }
@@ -503,11 +503,9 @@ var Idiomorph = (function () {
503
503
  function moveBeforeById(parentNode, id, after, ctx) {
504
504
  const target =
505
505
  /** @type {Element} - will always be found */
506
- (
507
- (ctx.target.id === id && ctx.target) ||
508
- ctx.target.querySelector(`[id="${id}"]`) ||
509
- ctx.pantry.querySelector(`[id="${id}"]`)
510
- );
506
+ (ctx.target.id === id && ctx.target) ||
507
+ ctx.target.querySelector(`[id="${id}"]`) ||
508
+ ctx.pantry.querySelector(`[id="${id}"]`);
511
509
  removeElementFromAncestorsIdMaps(target, ctx);
512
510
  moveBefore(parentNode, target, after);
513
511
  return target;
@@ -587,7 +585,7 @@ var Idiomorph = (function () {
587
585
  // ignore the head element
588
586
  } else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== 'morph') {
589
587
  // ok to cast: if newContent wasn't also a <head>, it would've got caught in the `!isSoftMatch` branch above
590
- handleHeadElement(oldNode, /** @type {HTMLHeadElement} */ (newContent), ctx);
588
+ handleHeadElement(oldNode, /** @type {HTMLHeadElement} */ newContent, ctx);
591
589
  } else {
592
590
  morphAttributes(oldNode, newContent, ctx);
593
591
  if (!ignoreValueOfActiveElement(oldNode, ctx)) {
@@ -613,8 +611,8 @@ var Idiomorph = (function () {
613
611
  // if is an element type, sync the attributes from the
614
612
  // new node into the new node
615
613
  if (type === 1 /* element type */) {
616
- const oldElt = /** @type {Element} */ (oldNode);
617
- const newElt = /** @type {Element} */ (newNode);
614
+ const oldElt = /** @type {Element} */ oldNode;
615
+ const newElt = /** @type {Element} */ newNode;
618
616
 
619
617
  const oldAttributes = oldElt.attributes;
620
618
  const newAttributes = newElt.attributes;
@@ -868,9 +866,9 @@ var Idiomorph = (function () {
868
866
  let promises = [];
869
867
  for (const newNode of nodesToAppend) {
870
868
  // TODO: This could theoretically be null, based on type
871
- let newElt = /** @type {ChildNode} */ (
872
- document.createRange().createContextualFragment(newNode.outerHTML).firstChild
873
- );
869
+ let newElt = /** @type {ChildNode} */ document
870
+ .createRange()
871
+ .createContextualFragment(newNode.outerHTML).firstChild;
874
872
  if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
875
873
  if (('href' in newElt && newElt.href) || ('src' in newElt && newElt.src)) {
876
874
  /** @type {(result?: any) => void} */ let resolve;
@@ -1116,16 +1114,16 @@ var Idiomorph = (function () {
1116
1114
  return document.createElement('div'); // dummy parent element
1117
1115
  } else if (typeof newContent === 'string') {
1118
1116
  return normalizeParent(parseContent(newContent));
1119
- } else if (generatedByIdiomorph.has(/** @type {Element} */ (newContent))) {
1117
+ } else if (generatedByIdiomorph.has(/** @type {Element} */ newContent)) {
1120
1118
  // the template tag created by idiomorph parsing can serve as a dummy parent
1121
- return /** @type {Element} */ (newContent);
1119
+ return /** @type {Element} */ newContent;
1122
1120
  } else if (newContent instanceof Node) {
1123
1121
  if (newContent.parentNode) {
1124
1122
  // we can't use the parent directly because newContent may have siblings
1125
1123
  // that we don't want in the morph, and reparenting might be expensive (TODO is it?),
1126
1124
  // so instead we create a fake parent node that only sees a slice of its children.
1127
1125
  /** @type {Element} */
1128
- return /** @type {any} */ (new SlicedParentNode(newContent));
1126
+ return /** @type {any} */ new SlicedParentNode(newContent);
1129
1127
  } else {
1130
1128
  // a single node is added as a child to a dummy parent
1131
1129
  const dummyParent = document.createElement('div');
@@ -1154,7 +1152,7 @@ var Idiomorph = (function () {
1154
1152
  /** @param {Node} node */
1155
1153
  constructor(node) {
1156
1154
  this.originalNode = node;
1157
- this.realParentNode = /** @type {Element} */ (node.parentNode);
1155
+ this.realParentNode = /** @type {Element} */ node.parentNode;
1158
1156
  this.previousSibling = node.previousSibling;
1159
1157
  this.nextSibling = node.nextSibling;
1160
1158
  }
@@ -1178,16 +1176,19 @@ var Idiomorph = (function () {
1178
1176
  * @returns {Element[]}
1179
1177
  */
1180
1178
  querySelectorAll(selector) {
1181
- return this.childNodes.reduce((results, node) => {
1182
- if (node instanceof Element) {
1183
- if (node.matches(selector)) results.push(node);
1184
- const nodeList = node.querySelectorAll(selector);
1185
- for (let i = 0; i < nodeList.length; i++) {
1186
- results.push(nodeList[i]);
1179
+ return this.childNodes.reduce(
1180
+ (results, node) => {
1181
+ if (node instanceof Element) {
1182
+ if (node.matches(selector)) results.push(node);
1183
+ const nodeList = node.querySelectorAll(selector);
1184
+ for (let i = 0; i < nodeList.length; i++) {
1185
+ results.push(nodeList[i]);
1186
+ }
1187
1187
  }
1188
- }
1189
- return results;
1190
- }, /** @type {Element[]} */ ([]));
1188
+ return results;
1189
+ },
1190
+ /** @type {Element[]} */ []
1191
+ );
1191
1192
  }
1192
1193
 
1193
1194
  /**
@@ -1255,9 +1256,8 @@ var Idiomorph = (function () {
1255
1256
  '<body><template>' + newContent + '</template></body>',
1256
1257
  'text/html'
1257
1258
  );
1258
- let content = /** @type {HTMLTemplateElement} */ (
1259
- responseDoc.body.querySelector('template')
1260
- ).content;
1259
+ let content =
1260
+ /** @type {HTMLTemplateElement} */ responseDoc.body.querySelector('template').content;
1261
1261
  generatedByIdiomorph.add(content);
1262
1262
  return content;
1263
1263
  }
@@ -0,0 +1,19 @@
1
+ import { Context, Next } from 'hono';
2
+ import { etag } from 'hono/etag';
3
+ import timestring from 'timestring';
4
+
5
+ /**
6
+ * Cache the response for a given time length ('30s', '1d', '1w', '1m', etc) or given number of seconds
7
+ */
8
+ export function cacheTime(timeStrOrSeconds: string | number) {
9
+ return (c: Context, next: Next) =>
10
+ etag()(c, () => {
11
+ // Only cache GET requests
12
+ if (c.req.method.toUpperCase() === 'GET') {
13
+ const timeInSeconds =
14
+ typeof timeStrOrSeconds === 'number' ? timeStrOrSeconds : timestring(timeStrOrSeconds);
15
+ c.header('Cache-Control', `public, max-age=${timeInSeconds}`);
16
+ }
17
+ return next();
18
+ });
19
+ }
package/src/server.ts CHANGED
@@ -6,6 +6,7 @@ import { buildClientJS, buildClientCSS } from './assets';
6
6
  import { Hono, type Context } from 'hono';
7
7
  import { serveStatic } from 'hono/bun';
8
8
  import { HTTPException } from 'hono/http-exception';
9
+
9
10
  import type { HandlerResponse, MiddlewareHandler } from 'hono/types';
10
11
  import type { ContentfulStatusCode } from 'hono/utils/http-status';
11
12
 
@@ -55,14 +56,23 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
55
56
 
56
57
  const api: THSRoute = {
57
58
  _kind: 'hsRoute',
59
+ /**
60
+ * Add a GET route handler (primary page display)
61
+ */
58
62
  get(handler: THSRouteHandler) {
59
63
  _handlers['GET'] = handler;
60
64
  return api;
61
65
  },
66
+ /**
67
+ * Add a POST route handler (typically to process form data)
68
+ */
62
69
  post(handler: THSRouteHandler) {
63
70
  _handlers['POST'] = handler;
64
71
  return api;
65
72
  },
73
+ /**
74
+ * Add middleware specific to this route
75
+ */
66
76
  middleware(middleware: Array<MiddlewareHandler>) {
67
77
  _middleware = middleware;
68
78
  return api;
@@ -93,7 +103,8 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
93
103
 
94
104
  // Render HSHtml if returned from route handler
95
105
  if (isHSHtml(routeContent)) {
96
- if (streamingEnabled) {
106
+ // Stream only if enabled and there is async content to stream
107
+ if (streamingEnabled && (routeContent as HSHtml).asyncContent?.length > 0) {
97
108
  return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
98
109
  } else {
99
110
  const output = await renderAsync(routeContent as HSHtml);