@hyperspan/framework 0.1.5 → 0.1.7

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,5 +1,4 @@
1
- import {build} from 'bun';
2
- import dts from 'bun-plugin-dts';
1
+ import { build } from 'bun';
3
2
 
4
3
  const entrypoints = ['./src/server.ts', './src/assets.ts'];
5
4
  const external = ['@hyperspan/html'];
@@ -7,23 +6,11 @@ const outdir = './dist';
7
6
  const target = 'node';
8
7
  const splitting = true;
9
8
 
10
- await Promise.all([
11
- // Build JS
12
- build({
13
- entrypoints,
14
- external,
15
- outdir,
16
- target,
17
- splitting,
18
- }),
19
-
20
- // Build type files for TypeScript
21
- build({
22
- entrypoints,
23
- external,
24
- outdir,
25
- target,
26
- splitting,
27
- plugins: [dts()],
28
- }),
29
- ]);
9
+ // Build JS
10
+ await build({
11
+ entrypoints,
12
+ external,
13
+ outdir,
14
+ target,
15
+ splitting,
16
+ });
package/dist/assets.js CHANGED
@@ -1,145 +1,11 @@
1
1
  // src/assets.ts
2
2
  import { html } from "@hyperspan/html";
3
-
4
- // src/clientjs/md5.js
5
- function md5cycle(x, k) {
6
- var a = x[0], b = x[1], c = x[2], d = x[3];
7
- a = ff(a, b, c, d, k[0], 7, -680876936);
8
- d = ff(d, a, b, c, k[1], 12, -389564586);
9
- c = ff(c, d, a, b, k[2], 17, 606105819);
10
- b = ff(b, c, d, a, k[3], 22, -1044525330);
11
- a = ff(a, b, c, d, k[4], 7, -176418897);
12
- d = ff(d, a, b, c, k[5], 12, 1200080426);
13
- c = ff(c, d, a, b, k[6], 17, -1473231341);
14
- b = ff(b, c, d, a, k[7], 22, -45705983);
15
- a = ff(a, b, c, d, k[8], 7, 1770035416);
16
- d = ff(d, a, b, c, k[9], 12, -1958414417);
17
- c = ff(c, d, a, b, k[10], 17, -42063);
18
- b = ff(b, c, d, a, k[11], 22, -1990404162);
19
- a = ff(a, b, c, d, k[12], 7, 1804603682);
20
- d = ff(d, a, b, c, k[13], 12, -40341101);
21
- c = ff(c, d, a, b, k[14], 17, -1502002290);
22
- b = ff(b, c, d, a, k[15], 22, 1236535329);
23
- a = gg(a, b, c, d, k[1], 5, -165796510);
24
- d = gg(d, a, b, c, k[6], 9, -1069501632);
25
- c = gg(c, d, a, b, k[11], 14, 643717713);
26
- b = gg(b, c, d, a, k[0], 20, -373897302);
27
- a = gg(a, b, c, d, k[5], 5, -701558691);
28
- d = gg(d, a, b, c, k[10], 9, 38016083);
29
- c = gg(c, d, a, b, k[15], 14, -660478335);
30
- b = gg(b, c, d, a, k[4], 20, -405537848);
31
- a = gg(a, b, c, d, k[9], 5, 568446438);
32
- d = gg(d, a, b, c, k[14], 9, -1019803690);
33
- c = gg(c, d, a, b, k[3], 14, -187363961);
34
- b = gg(b, c, d, a, k[8], 20, 1163531501);
35
- a = gg(a, b, c, d, k[13], 5, -1444681467);
36
- d = gg(d, a, b, c, k[2], 9, -51403784);
37
- c = gg(c, d, a, b, k[7], 14, 1735328473);
38
- b = gg(b, c, d, a, k[12], 20, -1926607734);
39
- a = hh(a, b, c, d, k[5], 4, -378558);
40
- d = hh(d, a, b, c, k[8], 11, -2022574463);
41
- c = hh(c, d, a, b, k[11], 16, 1839030562);
42
- b = hh(b, c, d, a, k[14], 23, -35309556);
43
- a = hh(a, b, c, d, k[1], 4, -1530992060);
44
- d = hh(d, a, b, c, k[4], 11, 1272893353);
45
- c = hh(c, d, a, b, k[7], 16, -155497632);
46
- b = hh(b, c, d, a, k[10], 23, -1094730640);
47
- a = hh(a, b, c, d, k[13], 4, 681279174);
48
- d = hh(d, a, b, c, k[0], 11, -358537222);
49
- c = hh(c, d, a, b, k[3], 16, -722521979);
50
- b = hh(b, c, d, a, k[6], 23, 76029189);
51
- a = hh(a, b, c, d, k[9], 4, -640364487);
52
- d = hh(d, a, b, c, k[12], 11, -421815835);
53
- c = hh(c, d, a, b, k[15], 16, 530742520);
54
- b = hh(b, c, d, a, k[2], 23, -995338651);
55
- a = ii(a, b, c, d, k[0], 6, -198630844);
56
- d = ii(d, a, b, c, k[7], 10, 1126891415);
57
- c = ii(c, d, a, b, k[14], 15, -1416354905);
58
- b = ii(b, c, d, a, k[5], 21, -57434055);
59
- a = ii(a, b, c, d, k[12], 6, 1700485571);
60
- d = ii(d, a, b, c, k[3], 10, -1894986606);
61
- c = ii(c, d, a, b, k[10], 15, -1051523);
62
- b = ii(b, c, d, a, k[1], 21, -2054922799);
63
- a = ii(a, b, c, d, k[8], 6, 1873313359);
64
- d = ii(d, a, b, c, k[15], 10, -30611744);
65
- c = ii(c, d, a, b, k[6], 15, -1560198380);
66
- b = ii(b, c, d, a, k[13], 21, 1309151649);
67
- a = ii(a, b, c, d, k[4], 6, -145523070);
68
- d = ii(d, a, b, c, k[11], 10, -1120210379);
69
- c = ii(c, d, a, b, k[2], 15, 718787259);
70
- b = ii(b, c, d, a, k[9], 21, -343485551);
71
- x[0] = add32(a, x[0]);
72
- x[1] = add32(b, x[1]);
73
- x[2] = add32(c, x[2]);
74
- x[3] = add32(d, x[3]);
75
- }
76
- function cmn(q, a, b, x, s, t) {
77
- a = add32(add32(a, q), add32(x, t));
78
- return add32(a << s | a >>> 32 - s, b);
79
- }
80
- function ff(a, b, c, d, x, s, t) {
81
- return cmn(b & c | ~b & d, a, b, x, s, t);
82
- }
83
- function gg(a, b, c, d, x, s, t) {
84
- return cmn(b & d | c & ~d, a, b, x, s, t);
85
- }
86
- function hh(a, b, c, d, x, s, t) {
87
- return cmn(b ^ c ^ d, a, b, x, s, t);
88
- }
89
- function ii(a, b, c, d, x, s, t) {
90
- return cmn(c ^ (b | ~d), a, b, x, s, t);
91
- }
92
- function md51(s) {
93
- var txt = "";
94
- var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i;
95
- for (i = 64;i <= s.length; i += 64) {
96
- md5cycle(state, md5blk(s.substring(i - 64, i)));
97
- }
98
- s = s.substring(i - 64);
99
- var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
100
- for (i = 0;i < s.length; i++)
101
- tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
102
- tail[i >> 2] |= 128 << (i % 4 << 3);
103
- if (i > 55) {
104
- md5cycle(state, tail);
105
- for (i = 0;i < 16; i++)
106
- tail[i] = 0;
107
- }
108
- tail[14] = n * 8;
109
- md5cycle(state, tail);
110
- return state;
111
- }
112
- function md5blk(s) {
113
- var md5blks = [], i;
114
- for (i = 0;i < 64; i += 4) {
115
- md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24);
116
- }
117
- return md5blks;
118
- }
119
- var hex_chr = "0123456789abcdef".split("");
120
- function rhex(n) {
121
- var s = "", j = 0;
122
- for (;j < 4; j++)
123
- s += hex_chr[n >> j * 8 + 4 & 15] + hex_chr[n >> j * 8 & 15];
124
- return s;
125
- }
126
- function hex(x) {
127
- for (var i = 0;i < x.length; i++)
128
- x[i] = rhex(x[i]);
129
- return x.join("");
130
- }
131
- function add32(a, b) {
132
- return a + b & 4294967295;
133
- }
134
- function md5(s) {
135
- return hex(md51(s));
136
- }
137
-
138
- // src/assets.ts
3
+ import { createHash } from "node:crypto";
139
4
  import { readdir } from "node:fs/promises";
140
5
  import { resolve } from "node:path";
141
6
  var IS_PROD = false;
142
7
  var PWD = import.meta.dir;
8
+ var clientImportMap = new Map;
143
9
  var clientJSFiles = new Map;
144
10
  async function buildClientJS() {
145
11
  const sourceFile = resolve(PWD, "../", "./src/clientjs/hyperspan-client.ts");
@@ -180,15 +46,7 @@ function hyperspanScriptTags() {
180
46
  const jsFiles = Array.from(clientJSFiles.entries());
181
47
  return html`
182
48
  <script type="importmap">
183
- {
184
- "imports": {
185
- "preact": "https://esm.sh/preact@10.26.4",
186
- "preact/": "https://esm.sh/preact@10.26.4/",
187
- "react": "https://esm.sh/preact@10.26.4/compat",
188
- "react/": "https://esm.sh/preact@10.26.4/compat/",
189
- "react-dom": "https://esm.sh/preact@10.26.4/compat"
190
- }
191
- }
49
+ {"imports": ${Object.fromEntries(clientImportMap)}}
192
50
  </script>
193
51
  ${jsFiles.map(([key, file]) => html`<script
194
52
  id="js-${key}"
@@ -197,21 +55,46 @@ function hyperspanScriptTags() {
197
55
  ></script>`)}
198
56
  `;
199
57
  }
58
+ var PREACT_PUBLIC_FILE_PATH = "/_hs/js/preact.js";
59
+ function md5(content) {
60
+ return createHash("md5").update(content).digest("hex");
61
+ }
62
+ async function copyPreactToPublicFolder() {
63
+ const sourceFile = resolve(PWD, "../", "./src/clientjs/preact.ts");
64
+ const preactClient = Bun.build({
65
+ entrypoints: [sourceFile],
66
+ outdir: "./public/_hs/js",
67
+ minify: true,
68
+ format: "esm",
69
+ target: "browser"
70
+ });
71
+ }
200
72
  async function createPreactIsland(file) {
201
73
  let filePath = file.replace("file://", "");
74
+ const jsId = md5(filePath);
75
+ if (!clientImportMap.has("preact")) {
76
+ await copyPreactToPublicFolder();
77
+ clientImportMap.set("preact", "" + PREACT_PUBLIC_FILE_PATH);
78
+ clientImportMap.set("preact/compat", "" + PREACT_PUBLIC_FILE_PATH);
79
+ clientImportMap.set("preact/hooks", "" + PREACT_PUBLIC_FILE_PATH);
80
+ clientImportMap.set("preact/jsx-runtime", "" + PREACT_PUBLIC_FILE_PATH);
81
+ }
82
+ if (!clientImportMap.has("react")) {
83
+ clientImportMap.set("react", "." + PREACT_PUBLIC_FILE_PATH);
84
+ clientImportMap.set("react-dom", "." + PREACT_PUBLIC_FILE_PATH);
85
+ }
202
86
  let resultStr = 'import{h,render}from"preact";';
203
- const build = await Bun.build({
87
+ const buildResult = await Bun.build({
204
88
  entrypoints: [filePath],
205
89
  minify: true,
206
90
  external: ["react", "preact"],
207
91
  env: "APP_PUBLIC_*"
208
92
  });
209
- for (const output of build.outputs) {
93
+ for (const output of buildResult.outputs) {
210
94
  resultStr += await output.text();
211
95
  }
212
96
  const r = /export\{([a-zA-Z]+) as default\}/g;
213
97
  const matchExport = r.exec(resultStr);
214
- const jsId = md5(resultStr);
215
98
  if (!matchExport) {
216
99
  throw new Error("File does not have a default export! Ensure a function has export default to use this.");
217
100
  }
@@ -230,6 +113,7 @@ export {
230
113
  hyperspanScriptTags,
231
114
  createPreactIsland,
232
115
  clientJSFiles,
116
+ clientImportMap,
233
117
  clientCSSFiles,
234
118
  buildClientJS,
235
119
  buildClientCSS
package/dist/server.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  // src/server.ts
7
7
  import { readdir } from "node:fs/promises";
8
8
  import { basename, extname, join } from "node:path";
9
- import { TmplHtml, html, renderStream, renderAsync, render } from "@hyperspan/html";
9
+ import { html, isHSHtml, renderStream, renderAsync, render } from "@hyperspan/html";
10
10
 
11
11
  // node_modules/isbot/index.mjs
12
- var fullPattern = " daum[ /]| deusu/| yadirectfetcher|(?:^|[^g])news(?!sapphire)|(?<! (?:channel/|google/))google(?!(app|/google| pixel))|(?<! cu)bots?(?:\\b|_)|(?<!(?:lib))http|(?<![hg]m)score|@[a-z][\\w-]+\\.|\\(\\)|\\.com\\b|\\btime/|\\||^<|^[\\w \\.\\-\\(?:\\):%]+(?:/v?\\d+(?:\\.\\d+)?(?:\\.\\d{1,10})*?)?(?:,|$)|^[^ ]{50,}$|^\\d+\\b|^\\w*search\\b|^\\w+/[\\w\\(\\)]*$|^active|^ad muncher|^amaya|^avsdevicesdk/|^biglotron|^bot|^bw/|^clamav[ /]|^client/|^cobweb/|^custom|^ddg[_-]android|^discourse|^dispatch/\\d|^downcast/|^duckduckgo|^email|^facebook|^getright/|^gozilla/|^hobbit|^hotzonu|^hwcdn/|^igetter/|^jeode/|^jetty/|^jigsaw|^microsoft bits|^movabletype|^mozilla/\\d\\.\\d\\s[\\w\\.-]+$|^mozilla/\\d\\.\\d\\s\\(compatible;?(?:\\s\\w+\\/\\d+\\.\\d+)?\\)$|^navermailapp|^netsurf|^offline|^openai/|^owler|^php|^postman|^python|^rank|^read|^reed|^rest|^rss|^snapchat|^space bison|^svn|^swcd |^taringa|^thumbor/|^track|^w3c|^webbandit/|^webcopier|^wget|^whatsapp|^wordpress|^xenu link sleuth|^yahoo|^yandex|^zdm/\\d|^zoom marketplace/|^{{.*}}$|adscanner/|analyzer|archive|ask jeeves/teoma|audit|bit\\.ly/|bluecoat drtr|browsex|burpcollaborator|capture|catch|check\\b|checker|chrome-lighthouse|chromeframe|classifier|cloudflare|convertify|cookiehubscan|crawl|cypress/|dareboost|datanyze|dejaclick|detect|dmbrowser|download|evc-batch/|exaleadcloudview|feed|firephp|functionize|gomezagent|headless|httrack|hubspot marketing grader|hydra|ibisbrowser|infrawatch|insight|inspect|iplabel|ips-agent|java(?!;)|jsjcw_scanner|library|linkcheck|mail\\.ru/|manager|measure|neustar wpm|node|nutch|offbyone|onetrust|optimize|pageburst|pagespeed|parser|perl|phantomjs|pingdom|powermarks|preview|proxy|ptst[ /]\\d|retriever|rexx;|rigor|rss\\b|scanner\\.|scrape|server|sogou|sparkler/|speedcurve|spider|splash|statuscake|supercleaner|synapse|synthetic|tools|torrent|transcoder|url|validator|virtuoso|wappalyzer|webglance|webkit2png|whatcms/|zgrab";
12
+ var fullPattern = " daum[ /]| deusu/| yadirectfetcher|(?:^|[^g])news(?!sapphire)|(?<! (?:channel/|google/))google(?!(app|/google| pixel))|(?<! cu)bots?(?:\\b|_)|(?<!(?:lib))http|(?<![hg]m)score|(?<!cam)scan|@[a-z][\\w-]+\\.|\\(\\)|\\.com\\b|\\btime/|\\||^<|^[\\w \\.\\-\\(?:\\):%]+(?:/v?\\d+(?:\\.\\d+)?(?:\\.\\d{1,10})*?)?(?:,|$)|^[^ ]{50,}$|^\\d+\\b|^\\w*search\\b|^\\w+/[\\w\\(\\)]*$|^active|^ad muncher|^amaya|^avsdevicesdk/|^biglotron|^bot|^bw/|^clamav[ /]|^client/|^cobweb/|^custom|^ddg[_-]android|^discourse|^dispatch/\\d|^downcast/|^duckduckgo|^email|^facebook|^getright/|^gozilla/|^hobbit|^hotzonu|^hwcdn/|^igetter/|^jeode/|^jetty/|^jigsaw|^microsoft bits|^movabletype|^mozilla/\\d\\.\\d\\s[\\w\\.-]+$|^mozilla/\\d\\.\\d\\s\\(compatible;?(?:\\s\\w+\\/\\d+\\.\\d+)?\\)$|^navermailapp|^netsurf|^offline|^openai/|^owler|^php|^postman|^python|^rank|^read|^reed|^rest|^rss|^snapchat|^space bison|^svn|^swcd |^taringa|^thumbor/|^track|^w3c|^webbandit/|^webcopier|^wget|^whatsapp|^wordpress|^xenu link sleuth|^yahoo|^yandex|^zdm/\\d|^zoom marketplace/|^{{.*}}$|analyzer|archive|ask jeeves/teoma|audit|bit\\.ly/|bluecoat drtr|browsex|burpcollaborator|capture|catch|check\\b|checker|chrome-lighthouse|chromeframe|classifier|cloudflare|convertify|crawl|cypress/|dareboost|datanyze|dejaclick|detect|dmbrowser|download|evc-batch/|exaleadcloudview|feed|firephp|functionize|gomezagent|grab|headless|httrack|hubspot marketing grader|hydra|ibisbrowser|infrawatch|insight|inspect|iplabel|ips-agent|java(?!;)|library|linkcheck|mail\\.ru/|manager|measure|neustar wpm|node|nutch|offbyone|onetrust|optimize|pageburst|pagespeed|parser|perl|phantomjs|pingdom|powermarks|preview|proxy|ptst[ /]\\d|retriever|rexx;|rigor|rss\\b|scrape|server|sogou|sparkler/|speedcurve|spider|splash|statuscake|supercleaner|synapse|synthetic|tools|torrent|transcoder|url|validator|virtuoso|wappalyzer|webglance|webkit2png|whatcms/";
13
13
  var naivePattern = /bot|crawl|http|lighthouse|scan|search|spider/i;
14
14
  var pattern;
15
15
  function getPattern() {
@@ -841,7 +841,11 @@ var Hono = class {
841
841
  optionHandler = options;
842
842
  } else {
843
843
  optionHandler = options.optionHandler;
844
- replaceRequest = options.replaceRequest;
844
+ if (options.replaceRequest === false) {
845
+ replaceRequest = (request) => request;
846
+ } else {
847
+ replaceRequest = options.replaceRequest;
848
+ }
845
849
  }
846
850
  }
847
851
  const getOptions = optionHandler ? (c) => {
@@ -851,7 +855,8 @@ var Hono = class {
851
855
  let executionContext = undefined;
852
856
  try {
853
857
  executionContext = c.executionCtx;
854
- } catch {}
858
+ } catch {
859
+ }
855
860
  return [c.env, executionContext];
856
861
  };
857
862
  replaceRequest ||= (() => {
@@ -1745,7 +1750,8 @@ var serveStatic2 = (options) => {
1745
1750
  try {
1746
1751
  const stats = await stat(path);
1747
1752
  isDir2 = stats.isDirectory();
1748
- } catch {}
1753
+ } catch {
1754
+ }
1749
1755
  return isDir2;
1750
1756
  };
1751
1757
  return serveStatic({
@@ -1862,7 +1868,7 @@ function createRoute(handler) {
1862
1868
  const streamOpt = context.req.query("__nostream");
1863
1869
  const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1864
1870
  const routeKind = typeof routeContent;
1865
- if (routeContent && routeKind === "object" && (routeContent instanceof TmplHtml || routeContent.constructor.name === "TmplHtml" || routeContent?._kind === "TmplHtml")) {
1871
+ if (isHSHtml(routeContent)) {
1866
1872
  if (streamingEnabled) {
1867
1873
  return new StreamResponse(renderStream(routeContent));
1868
1874
  } else {
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "dist/server.js",
6
+ "types": "src/server.ts",
6
7
  "public": true,
7
8
  "publishConfig": {
8
9
  "access": "public"
@@ -63,10 +64,10 @@
63
64
  "typescript": "^5.0.0"
64
65
  },
65
66
  "dependencies": {
66
- "@hyperspan/html": "^0.1.4",
67
- "@preact/compat": "^18.3.1",
67
+ "@hyperspan/html": "^0.1.6",
68
68
  "hono": "^4.7.4",
69
69
  "isbot": "^5.1.25",
70
+ "preact": "^10.26.5",
70
71
  "zod": "^4.0.0-beta.20250415T232143"
71
72
  }
72
73
  }
@@ -1,16 +1,16 @@
1
1
  import z from 'zod';
2
- import { createAction, THSFormData } from './actions';
2
+ import { createAction } from './actions';
3
3
  import { describe, it, expect } from 'bun:test';
4
- import { html, render, type TmplHtml } from '@hyperspan/html';
4
+ import { html, render, type HSHtml } from '@hyperspan/html';
5
5
  import type { Context } from 'hono';
6
6
 
7
7
  describe('createAction', () => {
8
- const formWithNameOnly = ({ data }: THSFormData) => {
8
+ const formWithNameOnly = ({ data }: { data?: { name: string } }) => {
9
9
  return html`
10
10
  <form>
11
11
  <p>
12
12
  Name:
13
- <input type="text" name="name" value="${data.name || ''}" />
13
+ <input type="text" name="name" value="${data?.name || ''}" />
14
14
  </p>
15
15
  <button type="submit">Submit</button>
16
16
  </form>
@@ -22,9 +22,9 @@ describe('createAction', () => {
22
22
  const schema = z.object({
23
23
  name: z.string(),
24
24
  });
25
- const action = createAction(schema).form(formWithNameOnly);
25
+ const action = createAction(schema, formWithNameOnly);
26
26
 
27
- const formResponse = render(action.render({ data: { name: 'John' } }) as TmplHtml);
27
+ const formResponse = render(action.render({ data: { name: 'John' } }) as HSHtml);
28
28
  expect(formResponse).toContain('value="John"');
29
29
  });
30
30
  });
@@ -34,10 +34,9 @@ describe('createAction', () => {
34
34
  const schema = z.object({
35
35
  name: z.string().nonempty(),
36
36
  });
37
- const action = createAction(schema)
38
- .form(formWithNameOnly)
39
- .handler((c, { data }) => {
40
- return html`<div>Thanks for submitting the form, ${data.name}!</div>`;
37
+ const action = createAction(schema, formWithNameOnly)
38
+ .post((c, { data }) => {
39
+ return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
41
40
  })
42
41
  .error((c, { error }) => {
43
42
  return html`<div>There was an error! ${error?.message}</div>`;
@@ -56,7 +55,7 @@ describe('createAction', () => {
56
55
 
57
56
  const response = await action.run('POST', mockContext);
58
57
 
59
- const formResponse = render(response as TmplHtml);
58
+ const formResponse = render(response as HSHtml);
60
59
  expect(formResponse).toContain('Thanks for submitting the form, John!');
61
60
  });
62
61
  });
@@ -68,8 +67,8 @@ describe('createAction', () => {
68
67
  });
69
68
  const action = createAction(schema)
70
69
  .form(formWithNameOnly)
71
- .handler((c, { data }) => {
72
- return html`<div>Thanks for submitting the form, ${data.name}!</div>`;
70
+ .post((c, { data }) => {
71
+ return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
73
72
  })
74
73
  .error((c, { error }) => {
75
74
  return html`<div>There was an error! ${error?.message}</div>`;
@@ -88,7 +87,7 @@ describe('createAction', () => {
88
87
 
89
88
  const response = await action.run('POST', mockContext);
90
89
 
91
- const formResponse = render(response as TmplHtml);
90
+ const formResponse = render(response as HSHtml);
92
91
  expect(formResponse).toContain('There was an error!');
93
92
  });
94
93
  });
package/src/actions.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { html } from '@hyperspan/html';
1
+ import { html, HSHtml } from '@hyperspan/html';
2
2
  import * as z from 'zod';
3
3
  import { HTTPException } from 'hono/http-exception';
4
4
 
@@ -20,21 +20,24 @@ import type { Context } from 'hono';
20
20
  */
21
21
  export interface HSAction<T extends z.ZodTypeAny> {
22
22
  _kind: string;
23
- form(renderForm: (data: z.infer<T>) => THSResponseTypes): HSAction<T>;
24
- handler(handler: (c: Context, { data }: { data: z.infer<T> }) => THSResponseTypes): HSAction<T>;
23
+ form(renderForm: ({ data }: { data?: z.infer<T> }) => HSHtml): HSAction<T>;
24
+ post(handler: (c: Context, { data }: { data?: z.infer<T> }) => THSResponseTypes): HSAction<T>;
25
25
  error(
26
26
  handler: (
27
27
  c: Context,
28
- { data, error }: { data: z.infer<T>; error?: z.ZodError | Error }
28
+ { data, error }: { data?: z.infer<T>; error?: z.ZodError | Error }
29
29
  ) => THSResponseTypes
30
30
  ): HSAction<T>;
31
- render(props?: { data: z.infer<T>; error?: z.ZodError | Error }): THSResponseTypes;
31
+ render(props?: { data?: z.infer<T>; error?: z.ZodError | Error }): THSResponseTypes;
32
32
  run(method: 'GET' | 'POST', c: Context): Promise<THSResponseTypes>;
33
33
  }
34
34
 
35
- export function createAction<T extends z.ZodTypeAny>(schema: T | null = null) {
36
- let _handler: Parameters<HSAction<T>['handler']>[0] | null = null,
37
- _form: Parameters<HSAction<T>['form']>[0] | null = null,
35
+ export function createAction<T extends z.ZodTypeAny>(
36
+ schema: T | null = null,
37
+ form: Parameters<HSAction<T>['form']>[0] | null = null
38
+ ) {
39
+ let _handler: Parameters<HSAction<T>['post']>[0] | null = null,
40
+ _form: Parameters<HSAction<T>['form']>[0] | null = form,
38
41
  _errorHandler: Parameters<HSAction<T>['error']>[0] | null = null;
39
42
 
40
43
  const api: HSAction<T> = {
@@ -50,7 +53,7 @@ export function createAction<T extends z.ZodTypeAny>(schema: T | null = null) {
50
53
  * Returns result from form processing if successful
51
54
  * Re-renders form with data and error information otherwise
52
55
  */
53
- handler(handler) {
56
+ post(handler) {
54
57
  _handler = handler;
55
58
  return api;
56
59
  },
@@ -63,8 +66,8 @@ export function createAction<T extends z.ZodTypeAny>(schema: T | null = null) {
63
66
  /**
64
67
  * Get form renderer method
65
68
  */
66
- render(data) {
67
- const form = _form ? _form(data || { data: {} }) : null;
69
+ render(formState?: { data?: z.infer<T>; error?: z.ZodError | Error }) {
70
+ const form = _form ? _form(formState || {}) : null;
68
71
  return form ? html`<hs-action>${form}</hs-action>` : null;
69
72
  },
70
73
 
@@ -86,7 +89,7 @@ export function createAction<T extends z.ZodTypeAny>(schema: T | null = null) {
86
89
  const formData = await c.req.formData();
87
90
  const jsonData = formDataToJSON(formData);
88
91
  const schemaData = schema ? schema.safeParse(jsonData) : null;
89
- const data = schemaData?.success ? (schemaData.data as z.infer<T>) : {};
92
+ const data = schemaData?.success ? (schemaData.data as z.infer<T>) : undefined;
90
93
  let error: z.ZodError | Error | null = null;
91
94
 
92
95
  try {
@@ -95,7 +98,7 @@ export function createAction<T extends z.ZodTypeAny>(schema: T | null = null) {
95
98
  }
96
99
 
97
100
  if (!_handler) {
98
- throw new Error('Action handler not set! Every action must have a handler.');
101
+ throw new Error('Action POST handler not set! Every action must have a POST handler.');
99
102
  }
100
103
 
101
104
  return _handler(c, { data });
package/src/assets.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { html } from '@hyperspan/html';
2
- import { md5 } from './clientjs/md5';
2
+ import { createHash } from 'node:crypto';
3
3
  import { readdir } from 'node:fs/promises';
4
4
  import { resolve } from 'node:path';
5
5
 
6
6
  const IS_PROD = process.env.NODE_ENV === 'production';
7
7
  const PWD = import.meta.dir;
8
8
 
9
+ export const clientImportMap = new Map<string, string>();
10
+
9
11
  /**
10
12
  * Build client JS for end users (minimal JS for Hyperspan to work)
11
13
  */
@@ -74,15 +76,7 @@ export function hyperspanScriptTags() {
74
76
 
75
77
  return html`
76
78
  <script type="importmap">
77
- {
78
- "imports": {
79
- "preact": "https://esm.sh/preact@10.26.4",
80
- "preact/": "https://esm.sh/preact@10.26.4/",
81
- "react": "https://esm.sh/preact@10.26.4/compat",
82
- "react/": "https://esm.sh/preact@10.26.4/compat/",
83
- "react-dom": "https://esm.sh/preact@10.26.4/compat"
84
- }
85
- }
79
+ {"imports": ${Object.fromEntries(clientImportMap)}}
86
80
  </script>
87
81
  ${jsFiles.map(
88
82
  ([key, file]) =>
@@ -95,14 +89,49 @@ export function hyperspanScriptTags() {
95
89
  `;
96
90
  }
97
91
 
92
+ // External ESM = https://esm.sh/preact@10.26.4/compat
93
+ const PREACT_PUBLIC_FILE_PATH = '/_hs/js/preact.js';
94
+
95
+ function md5(content: string): string {
96
+ return createHash('md5').update(content).digest('hex');
97
+ }
98
+
99
+ /**
100
+ * Build Preact client JS and copy to public folder
101
+ */
102
+ async function copyPreactToPublicFolder() {
103
+ const sourceFile = resolve(PWD, '../', './src/clientjs/preact.ts');
104
+ const preactClient = Bun.build({
105
+ entrypoints: [sourceFile],
106
+ outdir: './public/_hs/js',
107
+ minify: true,
108
+ format: 'esm',
109
+ target: 'browser',
110
+ });
111
+ }
112
+
98
113
  /**
99
114
  * Return a Preact component, mounted as an island in a <script> tag so it can be embedded into the page response.
100
115
  */
101
116
  export async function createPreactIsland(file: string) {
102
117
  let filePath = file.replace('file://', '');
118
+ const jsId = md5(filePath);
119
+
120
+ // Add Preact to client import map if not already present
121
+ if (!clientImportMap.has('preact')) {
122
+ await copyPreactToPublicFolder();
123
+ clientImportMap.set('preact', '' + PREACT_PUBLIC_FILE_PATH);
124
+ clientImportMap.set('preact/compat', '' + PREACT_PUBLIC_FILE_PATH);
125
+ clientImportMap.set('preact/hooks', '' + PREACT_PUBLIC_FILE_PATH);
126
+ clientImportMap.set('preact/jsx-runtime', '' + PREACT_PUBLIC_FILE_PATH);
127
+ }
128
+ if (!clientImportMap.has('react')) {
129
+ clientImportMap.set('react', '.' + PREACT_PUBLIC_FILE_PATH);
130
+ clientImportMap.set('react-dom', '.' + PREACT_PUBLIC_FILE_PATH);
131
+ }
103
132
 
104
133
  let resultStr = 'import{h,render}from"preact";';
105
- const build = await Bun.build({
134
+ const buildResult = await Bun.build({
106
135
  entrypoints: [filePath],
107
136
  minify: true,
108
137
  external: ['react', 'preact'],
@@ -110,14 +139,13 @@ export async function createPreactIsland(file: string) {
110
139
  env: 'APP_PUBLIC_*', // Inlines any ENV that starts with 'APP_PUBLIC_'
111
140
  });
112
141
 
113
- for (const output of build.outputs) {
142
+ for (const output of buildResult.outputs) {
114
143
  resultStr += await output.text(); // string
115
144
  }
116
145
 
117
146
  // Find default export - this is our component
118
147
  const r = /export\{([a-zA-Z]+) as default\}/g;
119
148
  const matchExport = r.exec(resultStr);
120
- const jsId = md5(resultStr);
121
149
 
122
150
  if (!matchExport) {
123
151
  throw new Error(
@@ -26,14 +26,10 @@ function htmlAsyncContentObserver() {
26
26
  const slotEl = document.getElementById(slotId);
27
27
 
28
28
  if (slotEl) {
29
- // Wait until next paint for streaming content to finish writing to DOM
30
- // @TODO: Need a more guaranteed way to know HTML element is done streaming in...
31
- // Maybe some ".end" element that is hidden and then removed before insertion?
32
- requestAnimationFrame(() => {
33
- setTimeout(() => {
34
- Idiomorph.morph(slotEl, el.content.cloneNode(true));
35
- el.parentNode.removeChild(el);
36
- }, 100);
29
+ // Only insert the content if it is done streaming in
30
+ waitForEndContent(el.content).then(() => {
31
+ Idiomorph.morph(slotEl, el.content.cloneNode(true));
32
+ el.parentNode.removeChild(el);
37
33
  });
38
34
  }
39
35
  } catch (e) {
@@ -46,6 +42,25 @@ function htmlAsyncContentObserver() {
46
42
  }
47
43
  htmlAsyncContentObserver();
48
44
 
45
+ /**
46
+ * Wait until ALL of the content inside an element is present from streaming in.
47
+ * Large chunks of content can sometimes take more than a single tick to write to DOM.
48
+ */
49
+ async function waitForEndContent(el: HTMLElement) {
50
+ return new Promise((resolve) => {
51
+ const interval = setInterval(() => {
52
+ const endComment = Array.from(el.childNodes).find((node) => {
53
+ return node.nodeType === Node.COMMENT_NODE && node.nodeValue === 'end';
54
+ });
55
+ if (endComment) {
56
+ el.removeChild(endComment);
57
+ clearInterval(interval);
58
+ resolve(true);
59
+ }
60
+ }, 10);
61
+ });
62
+ }
63
+
49
64
  /**
50
65
  * Server action component to handle the client-side form submission and HTML replacement
51
66
  */
@@ -1 +1,2 @@
1
- export * from '@preact/compat';
1
+ export * from 'preact/compat';
2
+ export { h, render } from 'preact';
package/src/server.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readdir } from 'node:fs/promises';
2
2
  import { basename, extname, join } from 'node:path';
3
- import { TmplHtml, html, renderStream, renderAsync, render } from '@hyperspan/html';
3
+ import { HSHtml, html, isHSHtml, renderStream, renderAsync, render } from '@hyperspan/html';
4
4
  import { isbot } from 'isbot';
5
5
  import { buildClientJS, buildClientCSS } from './assets';
6
6
  import { Hono, type Context } from 'hono';
@@ -13,7 +13,7 @@ const CWD = process.cwd();
13
13
  /**
14
14
  * Types
15
15
  */
16
- export type THSResponseTypes = TmplHtml | Response | string | null;
16
+ export type THSResponseTypes = HSHtml | Response | string | null;
17
17
  export type THSRouteHandler = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
18
18
 
19
19
  export type THSRoute = {
@@ -28,7 +28,7 @@ export type THSRoute = {
28
28
 
29
29
  /**
30
30
  * Define a route that can handle a direct HTTP request.
31
- * Route handlers should return a TmplHtml or Response object
31
+ * Route handlers should return a HSHtml or Response object
32
32
  */
33
33
  export function createRoute(handler?: THSRouteHandler): THSRoute {
34
34
  let _handlers: Record<string, THSRouteHandler> = {};
@@ -78,19 +78,12 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
78
78
  const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
79
79
  const routeKind = typeof routeContent;
80
80
 
81
- // Render TmplHtml if returned from route handler
82
- if (
83
- routeContent &&
84
- routeKind === 'object' &&
85
- (routeContent instanceof TmplHtml ||
86
- routeContent.constructor.name === 'TmplHtml' ||
87
- // @ts-ignore
88
- routeContent?._kind === 'TmplHtml')
89
- ) {
81
+ // Render HSHtml if returned from route handler
82
+ if (isHSHtml(routeContent)) {
90
83
  if (streamingEnabled) {
91
- return new StreamResponse(renderStream(routeContent as TmplHtml)) as Response;
84
+ return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
92
85
  } else {
93
- const output = await renderAsync(routeContent as TmplHtml);
86
+ const output = await renderAsync(routeContent as HSHtml);
94
87
  return context.html(output);
95
88
  }
96
89
  }
package/dist/assets.d.ts DELETED
@@ -1,46 +0,0 @@
1
- // Generated by dts-bundle-generator v9.5.1
2
-
3
- declare class TmplHtml {
4
- _kind: string;
5
- content: string;
6
- asyncContent: Array<{
7
- id: string;
8
- promise: Promise<{
9
- id: string;
10
- value: unknown;
11
- }>;
12
- }>;
13
- constructor(props: Pick<TmplHtml, "content" | "asyncContent">);
14
- }
15
- /**
16
- * Build client JS for end users (minimal JS for Hyperspan to work)
17
- */
18
- export declare const clientJSFiles: Map<string, {
19
- src: string;
20
- type?: string;
21
- }>;
22
- export declare function buildClientJS(): Promise<void>;
23
- /**
24
- * Find client CSS file built for end users
25
- * @TODO: Build this in code here vs. relying on tailwindcss CLI tool from package scripts
26
- */
27
- export declare const clientCSSFiles: Map<string, string>;
28
- export declare function buildClientCSS(): Promise<string | undefined>;
29
- /**
30
- * Output HTML style tag for Hyperspan app
31
- */
32
- export declare function hyperspanStyleTags(): TmplHtml;
33
- /**
34
- * Output HTML script tag for Hyperspan app
35
- * Required for functioning streaming so content can pop into place properly once ready
36
- */
37
- export declare function hyperspanScriptTags(): TmplHtml;
38
- /**
39
- * Return a Preact component, mounted as an island in a <script> tag so it can be embedded into the page response.
40
- */
41
- export declare function createPreactIsland(file: string): Promise<(props: any) => {
42
- _kind: string;
43
- content: string;
44
- }>;
45
-
46
- export {};
package/dist/server.d.ts DELETED
@@ -1,88 +0,0 @@
1
- // Generated by dts-bundle-generator v9.5.1
2
-
3
- import { Context, Hono } from 'hono';
4
-
5
- declare class TmplHtml {
6
- _kind: string;
7
- content: string;
8
- asyncContent: Array<{
9
- id: string;
10
- promise: Promise<{
11
- id: string;
12
- value: unknown;
13
- }>;
14
- }>;
15
- constructor(props: Pick<TmplHtml, "content" | "asyncContent">);
16
- }
17
- export declare const IS_PROD: boolean;
18
- /**
19
- * Types
20
- */
21
- export type THSResponseTypes = TmplHtml | Response | string | null;
22
- export type THSRouteHandler = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
23
- export type THSRoute = {
24
- _kind: "hsRoute";
25
- get: (handler: THSRouteHandler) => THSRoute;
26
- post: (handler: THSRouteHandler) => THSRoute;
27
- put: (handler: THSRouteHandler) => THSRoute;
28
- delete: (handler: THSRouteHandler) => THSRoute;
29
- patch: (handler: THSRouteHandler) => THSRoute;
30
- run: (method: string, context: Context) => Promise<Response>;
31
- };
32
- /**
33
- * Define a route that can handle a direct HTTP request.
34
- * Route handlers should return a TmplHtml or Response object
35
- */
36
- export declare function createRoute(handler?: THSRouteHandler): THSRoute;
37
- /**
38
- * Create new API Route
39
- * API Route handlers should return a JSON object or a Response
40
- */
41
- export declare function createAPIRoute(handler?: THSRouteHandler): THSRoute;
42
- /**
43
- * Get a Hyperspan runnable route from a module import
44
- * @throws Error if no runnable route found
45
- */
46
- export declare function getRunnableRoute(route: unknown): THSRoute;
47
- export declare function isRunnableRoute(route: unknown): boolean;
48
- export type THSServerConfig = {
49
- appDir: string;
50
- staticFileRoot: string;
51
- rewrites?: Array<{
52
- source: string;
53
- destination: string;
54
- }>;
55
- beforeRoutesAdded?: (app: Hono) => void;
56
- afterRoutesAdded?: (app: Hono) => void;
57
- };
58
- export type THSRouteMap = {
59
- file: string;
60
- route: string;
61
- params: string[];
62
- };
63
- export declare function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[]>;
64
- /**
65
- * Run route from file
66
- */
67
- export declare function createRouteFromModule(RouteModule: any): (context: Context) => Promise<Response>;
68
- /**
69
- * Create and start Bun HTTP server
70
- */
71
- export declare function createServer(config: THSServerConfig): Promise<Hono>;
72
- /**
73
- * Streaming HTML Response
74
- */
75
- export declare class StreamResponse extends Response {
76
- constructor(iterator: AsyncIterator<unknown>, options?: {});
77
- }
78
- /**
79
- * Does what it says on the tin...
80
- */
81
- export declare function createReadableStreamFromAsyncGenerator(output: AsyncGenerator): ReadableStream<any>;
82
- /**
83
- * Normalize URL path
84
- * Removes trailing slash and lowercases path
85
- */
86
- export declare function normalizePath(urlPath: string): string;
87
-
88
- export {};