@hyperspan/framework 0.3.0 → 0.3.2

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/assets.js CHANGED
@@ -60,17 +60,12 @@ function assetHash(content) {
60
60
  }
61
61
  var ISLAND_PUBLIC_PATH = "/_hs/js/islands";
62
62
  var ISLAND_DEFAULTS = () => ({
63
- ssr: true
63
+ ssr: true,
64
+ loading: undefined
64
65
  });
65
66
  function renderIsland(Component, props, options = ISLAND_DEFAULTS()) {
66
67
  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);
68
+ return html.raw(Component.__HS_ISLAND.render(props, options));
74
69
  }
75
70
  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
71
  }
package/dist/server.js CHANGED
@@ -1847,18 +1847,6 @@ function createRoute(handler) {
1847
1847
  _handlers["POST"] = handler2;
1848
1848
  return api;
1849
1849
  },
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
1850
  middleware(middleware) {
1863
1851
  _middleware = middleware;
1864
1852
  return api;
@@ -1909,7 +1897,7 @@ function createAPIRoute(handler) {
1909
1897
  _handlers["GET"] = handler;
1910
1898
  }
1911
1899
  const api = {
1912
- _kind: "hsRoute",
1900
+ _kind: "hsAPIRoute",
1913
1901
  get(handler2) {
1914
1902
  _handlers["GET"] = handler2;
1915
1903
  return api;
@@ -1988,21 +1976,40 @@ function getRunnableRoute(route) {
1988
1976
  throw new Error(`Route not runnable. Use "export default createRoute()" to create a Hyperspan route. Exported methods found were: ${Object.keys(route).join(", ")}`);
1989
1977
  }
1990
1978
  function isRunnableRoute(route) {
1991
- return typeof route === "object" && "_getRouteHandlers" in route;
1979
+ if (typeof route !== "object") {
1980
+ return false;
1981
+ }
1982
+ const obj = route;
1983
+ const runnableKind = ["hsRoute", "hsAPIRoute", "hsAction"].includes(obj?._kind);
1984
+ return runnableKind && "_getRouteHandlers" in obj;
1992
1985
  }
1993
1986
  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(`
1987
+ let status = 500;
1988
+ const message = err.message || "Internal Server Error";
1989
+ if (err instanceof HTTPException) {
1990
+ status = err.status;
1991
+ }
1992
+ const stack = !IS_PROD && err.stack ? err.stack.split(`
1999
1993
  `).slice(1).join(`
2000
- `) : ""}</pre>
2001
- </main>
1994
+ `) : "";
1995
+ const output = render(html`
1996
+ <!DOCTYPE html>
1997
+ <html lang="en">
1998
+ <head>
1999
+ <meta charset="UTF-8" />
2000
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
2001
+ <title>Application Error</title>
2002
+ </head>
2003
+ <body>
2004
+ <main>
2005
+ <h1>Application Error</h1>
2006
+ <strong>${message}</strong>
2007
+ ${stack ? html`<pre>${stack}</pre>` : ""}
2008
+ </main>
2009
+ </body>
2010
+ </html>
2002
2011
  `);
2003
- return context.html(output, {
2004
- status: 500
2005
- });
2012
+ return context.html(output, { status });
2006
2013
  }
2007
2014
  var ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
2008
2015
  async function buildRoutes(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "dist/server.ts",
6
6
  "types": "src/server.ts",
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(
@@ -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;
package/src/server.ts CHANGED
@@ -7,6 +7,7 @@ import { Hono, type Context } from 'hono';
7
7
  import { serveStatic } from 'hono/bun';
8
8
  import { HTTPException } from 'hono/http-exception';
9
9
  import type { HandlerResponse, MiddlewareHandler } from 'hono/types';
10
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
10
11
 
11
12
  export const IS_PROD = process.env.NODE_ENV === 'production';
12
13
  const CWD = process.cwd();
@@ -22,12 +23,19 @@ export type THSRoute = {
22
23
  _kind: 'hsRoute';
23
24
  get: (handler: THSRouteHandler) => THSRoute;
24
25
  post: (handler: THSRouteHandler) => THSRoute;
25
- put: (handler: THSRouteHandler) => THSRoute;
26
- delete: (handler: THSRouteHandler) => THSRoute;
27
- patch: (handler: THSRouteHandler) => THSRoute;
28
26
  middleware: (middleware: Array<MiddlewareHandler>) => THSRoute;
29
27
  _getRouteHandlers: () => Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)>;
30
28
  };
29
+ export type THSAPIRoute = {
30
+ _kind: 'hsAPIRoute';
31
+ get: (handler: THSAPIRouteHandler) => THSAPIRoute;
32
+ post: (handler: THSAPIRouteHandler) => THSAPIRoute;
33
+ put: (handler: THSAPIRouteHandler) => THSAPIRoute;
34
+ delete: (handler: THSAPIRouteHandler) => THSAPIRoute;
35
+ patch: (handler: THSAPIRouteHandler) => THSAPIRoute;
36
+ middleware: (middleware: Array<MiddlewareHandler>) => THSAPIRoute;
37
+ _getRouteHandlers: () => Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)>;
38
+ };
31
39
 
32
40
  export function createConfig(config: THSServerConfig): THSServerConfig {
33
41
  return config;
@@ -55,18 +63,6 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
55
63
  _handlers['POST'] = handler;
56
64
  return api;
57
65
  },
58
- put(handler: THSRouteHandler) {
59
- _handlers['PUT'] = handler;
60
- return api;
61
- },
62
- delete(handler: THSRouteHandler) {
63
- _handlers['DELETE'] = handler;
64
- return api;
65
- },
66
- patch(handler: THSRouteHandler) {
67
- _handlers['PATCH'] = handler;
68
- return api;
69
- },
70
66
  middleware(middleware: Array<MiddlewareHandler>) {
71
67
  _middleware = middleware;
72
68
  return api;
@@ -128,7 +124,7 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
128
124
  * Create new API Route
129
125
  * API Route handlers should return a JSON object or a Response
130
126
  */
131
- export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
127
+ export function createAPIRoute(handler?: THSAPIRouteHandler): THSAPIRoute {
132
128
  let _handlers: Record<string, THSAPIRouteHandler> = {};
133
129
  let _middleware: Array<MiddlewareHandler> = [];
134
130
 
@@ -136,8 +132,8 @@ export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
136
132
  _handlers['GET'] = handler;
137
133
  }
138
134
 
139
- const api: THSRoute = {
140
- _kind: 'hsRoute',
135
+ const api: THSAPIRoute = {
136
+ _kind: 'hsAPIRoute',
141
137
  get(handler: THSAPIRouteHandler) {
142
138
  _handlers['GET'] = handler;
143
139
  return api;
@@ -246,9 +242,18 @@ export function getRunnableRoute(route: unknown): THSRoute {
246
242
  );
247
243
  }
248
244
 
245
+ /**
246
+ * Check if a route is runnable by Hyperspan
247
+ */
249
248
  export function isRunnableRoute(route: unknown): boolean {
250
- // @ts-ignore
251
- return typeof route === 'object' && '_getRouteHandlers' in route;
249
+ if (typeof route !== 'object') {
250
+ return false;
251
+ }
252
+
253
+ const obj = route as { _kind: string; _getRouteHandlers: any };
254
+ const runnableKind = ['hsRoute', 'hsAPIRoute', 'hsAction'].includes(obj?._kind);
255
+
256
+ return runnableKind && '_getRouteHandlers' in obj;
252
257
  }
253
258
 
254
259
  /**
@@ -256,17 +261,35 @@ export function isRunnableRoute(route: unknown): boolean {
256
261
  * @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
257
262
  */
258
263
  async function showErrorReponse(context: Context, err: Error) {
264
+ let status: ContentfulStatusCode = 500;
265
+ const message = err.message || 'Internal Server Error';
266
+
267
+ // Send correct status code if HTTPException
268
+ if (err instanceof HTTPException) {
269
+ status = err.status as ContentfulStatusCode;
270
+ }
271
+
272
+ const stack = !IS_PROD && err.stack ? err.stack.split('\n').slice(1).join('\n') : '';
273
+
259
274
  const output = render(html`
260
- <main>
261
- <h1>Error</h1>
262
- <pre>${err.message}</pre>
263
- <pre>${!IS_PROD && err.stack ? err.stack.split('\n').slice(1).join('\n') : ''}</pre>
264
- </main>
275
+ <!DOCTYPE html>
276
+ <html lang="en">
277
+ <head>
278
+ <meta charset="UTF-8" />
279
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
280
+ <title>Application Error</title>
281
+ </head>
282
+ <body>
283
+ <main>
284
+ <h1>Application Error</h1>
285
+ <strong>${message}</strong>
286
+ ${stack ? html`<pre>${stack}</pre>` : ''}
287
+ </main>
288
+ </body>
289
+ </html>
265
290
  `);
266
291
 
267
- return context.html(output, {
268
- status: 500,
269
- });
292
+ return context.html(output, { status });
270
293
  }
271
294
 
272
295
  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';