@hyperspan/framework 0.3.1 → 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";
@@ -60,17 +62,12 @@ function assetHash(content) {
60
62
  }
61
63
  var ISLAND_PUBLIC_PATH = "/_hs/js/islands";
62
64
  var ISLAND_DEFAULTS = () => ({
63
- ssr: true
65
+ ssr: true,
66
+ loading: undefined
64
67
  });
65
68
  function renderIsland(Component, props, options = ISLAND_DEFAULTS()) {
66
69
  if (Component.__HS_ISLAND?.render) {
67
- return Component.__HS_ISLAND.render(props, options);
68
- }
69
- if (Component.__HS_ISLAND?.ssr && options.ssr) {
70
- return Component.__HS_ISLAND.ssr(props);
71
- }
72
- if (Component.__HS_ISLAND?.clientOnly) {
73
- return Component.__HS_ISLAND.clientOnly(props);
70
+ return html.raw(Component.__HS_ISLAND.render(props, options));
74
71
  }
75
72
  throw new Error(`Module ${Component.name} was not loaded with an island plugin! Did you forget to install an island plugin and add it to the createServer() 'islandPlugins' config?`);
76
73
  }
@@ -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";
@@ -1847,18 +1848,6 @@ function createRoute(handler) {
1847
1848
  _handlers["POST"] = handler2;
1848
1849
  return api;
1849
1850
  },
1850
- put(handler2) {
1851
- _handlers["PUT"] = handler2;
1852
- return api;
1853
- },
1854
- delete(handler2) {
1855
- _handlers["DELETE"] = handler2;
1856
- return api;
1857
- },
1858
- patch(handler2) {
1859
- _handlers["PATCH"] = handler2;
1860
- return api;
1861
- },
1862
1851
  middleware(middleware) {
1863
1852
  _middleware = middleware;
1864
1853
  return api;
@@ -1881,7 +1870,7 @@ function createRoute(handler) {
1881
1870
  const streamOpt = context.req.query("__nostream");
1882
1871
  const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1883
1872
  if (isHSHtml(routeContent)) {
1884
- if (streamingEnabled) {
1873
+ if (streamingEnabled && routeContent.asyncContent?.length > 0) {
1885
1874
  return new StreamResponse(renderStream(routeContent));
1886
1875
  } else {
1887
1876
  const output = await renderAsync(routeContent);
@@ -1909,7 +1898,7 @@ function createAPIRoute(handler) {
1909
1898
  _handlers["GET"] = handler;
1910
1899
  }
1911
1900
  const api = {
1912
- _kind: "hsRoute",
1901
+ _kind: "hsAPIRoute",
1913
1902
  get(handler2) {
1914
1903
  _handlers["GET"] = handler2;
1915
1904
  return api;
@@ -1988,21 +1977,40 @@ function getRunnableRoute(route) {
1988
1977
  throw new Error(`Route not runnable. Use "export default createRoute()" to create a Hyperspan route. Exported methods found were: ${Object.keys(route).join(", ")}`);
1989
1978
  }
1990
1979
  function isRunnableRoute(route) {
1991
- return typeof route === "object" && "_getRouteHandlers" in route;
1980
+ if (typeof route !== "object") {
1981
+ return false;
1982
+ }
1983
+ const obj = route;
1984
+ const runnableKind = ["hsRoute", "hsAPIRoute", "hsAction"].includes(obj?._kind);
1985
+ return runnableKind && "_getRouteHandlers" in obj;
1992
1986
  }
1993
1987
  async function showErrorReponse(context, err) {
1994
- const output = render(html`
1995
- <main>
1996
- <h1>Error</h1>
1997
- <pre>${err.message}</pre>
1998
- <pre>${!IS_PROD && err.stack ? err.stack.split(`
1988
+ let status = 500;
1989
+ const message = err.message || "Internal Server Error";
1990
+ if (err instanceof HTTPException) {
1991
+ status = err.status;
1992
+ }
1993
+ const stack = !IS_PROD && err.stack ? err.stack.split(`
1999
1994
  `).slice(1).join(`
2000
- `) : ""}</pre>
2001
- </main>
1995
+ `) : "";
1996
+ const output = render(html`
1997
+ <!DOCTYPE html>
1998
+ <html lang="en">
1999
+ <head>
2000
+ <meta charset="UTF-8" />
2001
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
2002
+ <title>Application Error</title>
2003
+ </head>
2004
+ <body>
2005
+ <main>
2006
+ <h1>Application Error</h1>
2007
+ <strong>${message}</strong>
2008
+ ${stack ? html`<pre>${stack}</pre>` : ""}
2009
+ </main>
2010
+ </body>
2011
+ </html>
2002
2012
  `);
2003
- return context.html(output, {
2004
- status: 500
2005
- });
2013
+ return context.html(output, { status });
2006
2014
  }
2007
2015
  var ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
2008
2016
  async function buildRoutes(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.3.1",
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
  /**
package/src/assets.ts CHANGED
@@ -3,6 +3,11 @@ import { createHash } from 'node:crypto';
3
3
  import { readdir } from 'node:fs/promises';
4
4
  import { resolve } from 'node:path';
5
5
 
6
+ export type THSIslandOptions = {
7
+ ssr?: boolean;
8
+ loading?: 'lazy' | undefined;
9
+ };
10
+
6
11
  const IS_PROD = process.env.NODE_ENV === 'production';
7
12
  const PWD = import.meta.dir;
8
13
 
@@ -97,24 +102,15 @@ export function assetHash(content: string): string {
97
102
  * Island defaults
98
103
  */
99
104
  export const ISLAND_PUBLIC_PATH = '/_hs/js/islands';
100
- export const ISLAND_DEFAULTS = () => ({
105
+ export const ISLAND_DEFAULTS: () => THSIslandOptions = () => ({
101
106
  ssr: true,
107
+ loading: undefined,
102
108
  });
103
109
 
104
110
  export function renderIsland(Component: any, props: any, options = ISLAND_DEFAULTS()) {
105
- // Render is an OPTIONAL override that allows you to render the island with your own logic
111
+ // Render island with its own logic
106
112
  if (Component.__HS_ISLAND?.render) {
107
- return Component.__HS_ISLAND.render(props, options);
108
- }
109
-
110
- // If ssr is true, render the island with the ssr function
111
- if (Component.__HS_ISLAND?.ssr && options.ssr) {
112
- return Component.__HS_ISLAND.ssr(props);
113
- }
114
-
115
- // If ssr is false, render the island with the clientOnly function
116
- if (Component.__HS_ISLAND?.clientOnly) {
117
- return Component.__HS_ISLAND.clientOnly(props);
113
+ return html.raw(Component.__HS_ISLAND.render(props, options));
118
114
  }
119
115
 
120
116
  throw new Error(
@@ -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.
@@ -31,6 +31,9 @@ function htmlAsyncContentObserver() {
31
31
  Idiomorph.morph(slotEl, el.content.cloneNode(true));
32
32
  el.parentNode.removeChild(el);
33
33
  });
34
+
35
+ // Lazy load scripts (if any) after the content is inserted
36
+ lazyLoadScripts();
34
37
  }
35
38
  } catch (e) {
36
39
  console.error(e);
@@ -122,5 +125,37 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement) {
122
125
  });
123
126
  }
124
127
 
128
+ /**
129
+ * Intersection observer for lazy loading <script> tags
130
+ */
131
+ const lazyLoadScriptObserver = new IntersectionObserver(
132
+ (entries, observer) => {
133
+ entries
134
+ .filter((entry) => entry.isIntersecting)
135
+ .forEach((entry) => {
136
+ observer.unobserve(entry.target);
137
+ // @ts-ignore
138
+ if (entry.target.children[0]?.content) {
139
+ // @ts-ignore
140
+ entry.target.replaceWith(entry.target.children[0].content);
141
+ }
142
+ });
143
+ },
144
+ { rootMargin: '0px 0px -200px 0px' }
145
+ );
146
+
147
+ /**
148
+ * Lazy load <script> tags in the current document
149
+ */
150
+ function lazyLoadScripts() {
151
+ document
152
+ .querySelectorAll('div[data-loading=lazy]')
153
+ .forEach((el) => lazyLoadScriptObserver.observe(el));
154
+ }
155
+
156
+ window.addEventListener('load', () => {
157
+ lazyLoadScripts();
158
+ });
159
+
125
160
  // @ts-ignore
126
161
  window.html = html;
@@ -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,7 +6,9 @@ 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';
11
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
10
12
 
11
13
  export const IS_PROD = process.env.NODE_ENV === 'production';
12
14
  const CWD = process.cwd();
@@ -22,9 +24,6 @@ export type THSRoute = {
22
24
  _kind: 'hsRoute';
23
25
  get: (handler: THSRouteHandler) => THSRoute;
24
26
  post: (handler: THSRouteHandler) => THSRoute;
25
- put: (handler: THSRouteHandler) => THSRoute;
26
- delete: (handler: THSRouteHandler) => THSRoute;
27
- patch: (handler: THSRouteHandler) => THSRoute;
28
27
  middleware: (middleware: Array<MiddlewareHandler>) => THSRoute;
29
28
  _getRouteHandlers: () => Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)>;
30
29
  };
@@ -57,26 +56,23 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
57
56
 
58
57
  const api: THSRoute = {
59
58
  _kind: 'hsRoute',
59
+ /**
60
+ * Add a GET route handler (primary page display)
61
+ */
60
62
  get(handler: THSRouteHandler) {
61
63
  _handlers['GET'] = handler;
62
64
  return api;
63
65
  },
66
+ /**
67
+ * Add a POST route handler (typically to process form data)
68
+ */
64
69
  post(handler: THSRouteHandler) {
65
70
  _handlers['POST'] = handler;
66
71
  return api;
67
72
  },
68
- put(handler: THSRouteHandler) {
69
- _handlers['PUT'] = handler;
70
- return api;
71
- },
72
- delete(handler: THSRouteHandler) {
73
- _handlers['DELETE'] = handler;
74
- return api;
75
- },
76
- patch(handler: THSRouteHandler) {
77
- _handlers['PATCH'] = handler;
78
- return api;
79
- },
73
+ /**
74
+ * Add middleware specific to this route
75
+ */
80
76
  middleware(middleware: Array<MiddlewareHandler>) {
81
77
  _middleware = middleware;
82
78
  return api;
@@ -107,7 +103,8 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
107
103
 
108
104
  // Render HSHtml if returned from route handler
109
105
  if (isHSHtml(routeContent)) {
110
- if (streamingEnabled) {
106
+ // Stream only if enabled and there is async content to stream
107
+ if (streamingEnabled && (routeContent as HSHtml).asyncContent?.length > 0) {
111
108
  return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
112
109
  } else {
113
110
  const output = await renderAsync(routeContent as HSHtml);
@@ -147,7 +144,7 @@ export function createAPIRoute(handler?: THSAPIRouteHandler): THSAPIRoute {
147
144
  }
148
145
 
149
146
  const api: THSAPIRoute = {
150
- _kind: 'hsRoute',
147
+ _kind: 'hsAPIRoute',
151
148
  get(handler: THSAPIRouteHandler) {
152
149
  _handlers['GET'] = handler;
153
150
  return api;
@@ -256,9 +253,18 @@ export function getRunnableRoute(route: unknown): THSRoute {
256
253
  );
257
254
  }
258
255
 
256
+ /**
257
+ * Check if a route is runnable by Hyperspan
258
+ */
259
259
  export function isRunnableRoute(route: unknown): boolean {
260
- // @ts-ignore
261
- return typeof route === 'object' && '_getRouteHandlers' in route;
260
+ if (typeof route !== 'object') {
261
+ return false;
262
+ }
263
+
264
+ const obj = route as { _kind: string; _getRouteHandlers: any };
265
+ const runnableKind = ['hsRoute', 'hsAPIRoute', 'hsAction'].includes(obj?._kind);
266
+
267
+ return runnableKind && '_getRouteHandlers' in obj;
262
268
  }
263
269
 
264
270
  /**
@@ -266,17 +272,35 @@ export function isRunnableRoute(route: unknown): boolean {
266
272
  * @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
267
273
  */
268
274
  async function showErrorReponse(context: Context, err: Error) {
275
+ let status: ContentfulStatusCode = 500;
276
+ const message = err.message || 'Internal Server Error';
277
+
278
+ // Send correct status code if HTTPException
279
+ if (err instanceof HTTPException) {
280
+ status = err.status as ContentfulStatusCode;
281
+ }
282
+
283
+ const stack = !IS_PROD && err.stack ? err.stack.split('\n').slice(1).join('\n') : '';
284
+
269
285
  const output = render(html`
270
- <main>
271
- <h1>Error</h1>
272
- <pre>${err.message}</pre>
273
- <pre>${!IS_PROD && err.stack ? err.stack.split('\n').slice(1).join('\n') : ''}</pre>
274
- </main>
286
+ <!DOCTYPE html>
287
+ <html lang="en">
288
+ <head>
289
+ <meta charset="UTF-8" />
290
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
291
+ <title>Application Error</title>
292
+ </head>
293
+ <body>
294
+ <main>
295
+ <h1>Application Error</h1>
296
+ <strong>${message}</strong>
297
+ ${stack ? html`<pre>${stack}</pre>` : ''}
298
+ </main>
299
+ </body>
300
+ </html>
275
301
  `);
276
302
 
277
- return context.html(output, {
278
- status: 500,
279
- });
303
+ return context.html(output, { status });
280
304
  }
281
305
 
282
306
  export type THSServerConfig = {
@@ -1,176 +0,0 @@
1
- /**
2
- * Fast browser md5 function (used for static asset hashing)
3
- * @link https://www.myersdaily.org/joseph/javascript/md5.js
4
- */
5
- function md5cycle(x, k) {
6
- var a = x[0],
7
- b = x[1],
8
- c = x[2],
9
- d = x[3];
10
-
11
- a = ff(a, b, c, d, k[0], 7, -680876936);
12
- d = ff(d, a, b, c, k[1], 12, -389564586);
13
- c = ff(c, d, a, b, k[2], 17, 606105819);
14
- b = ff(b, c, d, a, k[3], 22, -1044525330);
15
- a = ff(a, b, c, d, k[4], 7, -176418897);
16
- d = ff(d, a, b, c, k[5], 12, 1200080426);
17
- c = ff(c, d, a, b, k[6], 17, -1473231341);
18
- b = ff(b, c, d, a, k[7], 22, -45705983);
19
- a = ff(a, b, c, d, k[8], 7, 1770035416);
20
- d = ff(d, a, b, c, k[9], 12, -1958414417);
21
- c = ff(c, d, a, b, k[10], 17, -42063);
22
- b = ff(b, c, d, a, k[11], 22, -1990404162);
23
- a = ff(a, b, c, d, k[12], 7, 1804603682);
24
- d = ff(d, a, b, c, k[13], 12, -40341101);
25
- c = ff(c, d, a, b, k[14], 17, -1502002290);
26
- b = ff(b, c, d, a, k[15], 22, 1236535329);
27
-
28
- a = gg(a, b, c, d, k[1], 5, -165796510);
29
- d = gg(d, a, b, c, k[6], 9, -1069501632);
30
- c = gg(c, d, a, b, k[11], 14, 643717713);
31
- b = gg(b, c, d, a, k[0], 20, -373897302);
32
- a = gg(a, b, c, d, k[5], 5, -701558691);
33
- d = gg(d, a, b, c, k[10], 9, 38016083);
34
- c = gg(c, d, a, b, k[15], 14, -660478335);
35
- b = gg(b, c, d, a, k[4], 20, -405537848);
36
- a = gg(a, b, c, d, k[9], 5, 568446438);
37
- d = gg(d, a, b, c, k[14], 9, -1019803690);
38
- c = gg(c, d, a, b, k[3], 14, -187363961);
39
- b = gg(b, c, d, a, k[8], 20, 1163531501);
40
- a = gg(a, b, c, d, k[13], 5, -1444681467);
41
- d = gg(d, a, b, c, k[2], 9, -51403784);
42
- c = gg(c, d, a, b, k[7], 14, 1735328473);
43
- b = gg(b, c, d, a, k[12], 20, -1926607734);
44
-
45
- a = hh(a, b, c, d, k[5], 4, -378558);
46
- d = hh(d, a, b, c, k[8], 11, -2022574463);
47
- c = hh(c, d, a, b, k[11], 16, 1839030562);
48
- b = hh(b, c, d, a, k[14], 23, -35309556);
49
- a = hh(a, b, c, d, k[1], 4, -1530992060);
50
- d = hh(d, a, b, c, k[4], 11, 1272893353);
51
- c = hh(c, d, a, b, k[7], 16, -155497632);
52
- b = hh(b, c, d, a, k[10], 23, -1094730640);
53
- a = hh(a, b, c, d, k[13], 4, 681279174);
54
- d = hh(d, a, b, c, k[0], 11, -358537222);
55
- c = hh(c, d, a, b, k[3], 16, -722521979);
56
- b = hh(b, c, d, a, k[6], 23, 76029189);
57
- a = hh(a, b, c, d, k[9], 4, -640364487);
58
- d = hh(d, a, b, c, k[12], 11, -421815835);
59
- c = hh(c, d, a, b, k[15], 16, 530742520);
60
- b = hh(b, c, d, a, k[2], 23, -995338651);
61
-
62
- a = ii(a, b, c, d, k[0], 6, -198630844);
63
- d = ii(d, a, b, c, k[7], 10, 1126891415);
64
- c = ii(c, d, a, b, k[14], 15, -1416354905);
65
- b = ii(b, c, d, a, k[5], 21, -57434055);
66
- a = ii(a, b, c, d, k[12], 6, 1700485571);
67
- d = ii(d, a, b, c, k[3], 10, -1894986606);
68
- c = ii(c, d, a, b, k[10], 15, -1051523);
69
- b = ii(b, c, d, a, k[1], 21, -2054922799);
70
- a = ii(a, b, c, d, k[8], 6, 1873313359);
71
- d = ii(d, a, b, c, k[15], 10, -30611744);
72
- c = ii(c, d, a, b, k[6], 15, -1560198380);
73
- b = ii(b, c, d, a, k[13], 21, 1309151649);
74
- a = ii(a, b, c, d, k[4], 6, -145523070);
75
- d = ii(d, a, b, c, k[11], 10, -1120210379);
76
- c = ii(c, d, a, b, k[2], 15, 718787259);
77
- b = ii(b, c, d, a, k[9], 21, -343485551);
78
-
79
- x[0] = add32(a, x[0]);
80
- x[1] = add32(b, x[1]);
81
- x[2] = add32(c, x[2]);
82
- x[3] = add32(d, x[3]);
83
- }
84
-
85
- function cmn(q, a, b, x, s, t) {
86
- a = add32(add32(a, q), add32(x, t));
87
- return add32((a << s) | (a >>> (32 - s)), b);
88
- }
89
-
90
- function ff(a, b, c, d, x, s, t) {
91
- return cmn((b & c) | (~b & d), a, b, x, s, t);
92
- }
93
-
94
- function gg(a, b, c, d, x, s, t) {
95
- return cmn((b & d) | (c & ~d), a, b, x, s, t);
96
- }
97
-
98
- function hh(a, b, c, d, x, s, t) {
99
- return cmn(b ^ c ^ d, a, b, x, s, t);
100
- }
101
-
102
- function ii(a, b, c, d, x, s, t) {
103
- return cmn(c ^ (b | ~d), a, b, x, s, t);
104
- }
105
-
106
- function md51(s) {
107
- var txt = '';
108
- var n = s.length,
109
- state = [1732584193, -271733879, -1732584194, 271733878],
110
- i;
111
- for (i = 64; i <= s.length; i += 64) {
112
- md5cycle(state, md5blk(s.substring(i - 64, i)));
113
- }
114
- s = s.substring(i - 64);
115
- var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
116
- for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
117
- tail[i >> 2] |= 0x80 << (i % 4 << 3);
118
- if (i > 55) {
119
- md5cycle(state, tail);
120
- for (i = 0; i < 16; i++) tail[i] = 0;
121
- }
122
- tail[14] = n * 8;
123
- md5cycle(state, tail);
124
- return state;
125
- }
126
-
127
- /* there needs to be support for Unicode here,
128
- * unless we pretend that we can redefine the MD-5
129
- * algorithm for multi-byte characters (perhaps
130
- * by adding every four 16-bit characters and
131
- * shortening the sum to 32 bits). Otherwise
132
- * I suggest performing MD-5 as if every character
133
- * was two bytes--e.g., 0040 0025 = @%--but then
134
- * how will an ordinary MD-5 sum be matched?
135
- * There is no way to standardize text to something
136
- * like UTF-8 before transformation; speed cost is
137
- * utterly prohibitive. The JavaScript standard
138
- * itself needs to look at this: it should start
139
- * providing access to strings as preformed UTF-8
140
- * 8-bit unsigned value arrays.
141
- */
142
- function md5blk(s) {
143
- /* I figured global was faster. */
144
- var md5blks = [],
145
- i; /* Andy King said do it this way. */
146
- for (i = 0; i < 64; i += 4) {
147
- md5blks[i >> 2] =
148
- s.charCodeAt(i) +
149
- (s.charCodeAt(i + 1) << 8) +
150
- (s.charCodeAt(i + 2) << 16) +
151
- (s.charCodeAt(i + 3) << 24);
152
- }
153
- return md5blks;
154
- }
155
-
156
- var hex_chr = '0123456789abcdef'.split('');
157
-
158
- function rhex(n) {
159
- var s = '',
160
- j = 0;
161
- for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f];
162
- return s;
163
- }
164
-
165
- function hex(x) {
166
- for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]);
167
- return x.join('');
168
- }
169
-
170
- function add32(a, b) {
171
- return (a + b) & 0xffffffff;
172
- }
173
-
174
- export function md5(s) {
175
- return hex(md51(s));
176
- }
@@ -1,3 +0,0 @@
1
- export * from 'preact/compat';
2
- export * from 'preact/hooks';
3
- export { h, render, hydrate } from 'preact';