@hyperspan/framework 0.1.8 → 0.3.0

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/LICENSE.txt ADDED
@@ -0,0 +1,30 @@
1
+ BSD 3-Clause License
2
+
3
+ Hyperspan copyright (c) 2025, Vance Lucas.
4
+ LINK: https://www.hyperspan.dev
5
+ All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ 3. Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @hyperspan/framework
2
2
 
3
- > [!NOTE] > **The Hyperspan framework package is still in the heavy development. APIs may change without notice.**
3
+ ![License](https://img.shields.io/npm/l/%40hyperspan%2Fframework?style=for-the-badge&labelColor=2e3440&color=6f4fbe)
4
+ ![Version](https://img.shields.io/npm/v/%40hyperspan%2Fframework.svg?label=Version&style=for-the-badge&labelColor=2e3440&color=eea837)
5
+ ![Downloads](https://img.shields.io/npm/dw/%40hyperspan%2Fframework?style=for-the-badge&labelColor=2e3440&color=50b6a9)
6
+ ![Bun Badge](https://img.shields.io/badge/Bun-000?logo=bun&logoColor=fff&style=for-the-badge&color=2e3440)
7
+ ![TypeScript Badge](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=fff&style=for-the-badge)
4
8
 
5
- Full Hyperspan framework docs coming soon...
9
+ Hyperspan web framework!
10
+
11
+ Information and docs are at [Hyperspan.dev](https://www.hyperspan.dev)
package/dist/assets.js CHANGED
@@ -55,67 +55,36 @@ function hyperspanScriptTags() {
55
55
  ></script>`)}
56
56
  `;
57
57
  }
58
- var PREACT_PUBLIC_FILE_PATH = "/_hs/js/preact.js";
59
- function md5(content) {
58
+ function assetHash(content) {
60
59
  return createHash("md5").update(content).digest("hex");
61
60
  }
62
- async function copyPreactToPublicFolder() {
63
- const sourceFile = resolve(PWD, "../", "./src/clientjs/preact.ts");
64
- await Bun.build({
65
- entrypoints: [sourceFile],
66
- outdir: "./public/_hs/js",
67
- minify: true,
68
- format: "esm",
69
- target: "browser"
70
- });
71
- }
72
- async function createPreactIsland(file) {
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);
61
+ var ISLAND_PUBLIC_PATH = "/_hs/js/islands";
62
+ var ISLAND_DEFAULTS = () => ({
63
+ ssr: true
64
+ });
65
+ function renderIsland(Component, props, options = ISLAND_DEFAULTS()) {
66
+ if (Component.__HS_ISLAND?.render) {
67
+ return Component.__HS_ISLAND.render(props, options);
81
68
  }
82
- if (!clientImportMap.has("react")) {
83
- clientImportMap.set("react", "." + PREACT_PUBLIC_FILE_PATH);
84
- clientImportMap.set("react-dom", "." + PREACT_PUBLIC_FILE_PATH);
69
+ if (Component.__HS_ISLAND?.ssr && options.ssr) {
70
+ return Component.__HS_ISLAND.ssr(props);
85
71
  }
86
- let resultStr = 'import{h,render}from"preact";';
87
- const buildResult = await Bun.build({
88
- entrypoints: [filePath],
89
- minify: true,
90
- external: ["react", "preact"],
91
- env: "APP_PUBLIC_*"
92
- });
93
- for (const output of buildResult.outputs) {
94
- resultStr += await output.text();
95
- }
96
- const r = /export\{([a-zA-Z]+) as default\}/g;
97
- const matchExport = r.exec(resultStr);
98
- if (!matchExport) {
99
- throw new Error("File does not have a default export! Ensure a function has export default to use this.");
72
+ if (Component.__HS_ISLAND?.clientOnly) {
73
+ return Component.__HS_ISLAND.clientOnly(props);
100
74
  }
101
- const fn = matchExport[1];
102
- let _mounted = false;
103
- return (props) => {
104
- if (!_mounted) {
105
- _mounted = true;
106
- resultStr += `render(h(${fn}, ${JSON.stringify(props)}), document.getElementById("${jsId}"));`;
107
- }
108
- return html.raw(`<div id="${jsId}"></div><script type="module" data-source-id="${jsId}">${resultStr}</script>`);
109
- };
75
+ 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?`);
110
76
  }
111
77
  export {
78
+ renderIsland,
112
79
  hyperspanStyleTags,
113
80
  hyperspanScriptTags,
114
- createPreactIsland,
115
81
  clientJSFiles,
116
82
  clientImportMap,
117
83
  clientCSSFiles,
118
84
  buildClientJS,
119
- buildClientCSS
85
+ buildClientCSS,
86
+ assetHash,
87
+ ISLAND_PUBLIC_PATH,
88
+ ISLAND_DEFAULTS
120
89
  };
121
90
 
package/dist/server.js CHANGED
@@ -8,8 +8,8 @@ import { readdir } from "node:fs/promises";
8
8
  import { basename, extname, join } from "node:path";
9
9
  import { html, isHSHtml, renderStream, renderAsync, render } from "@hyperspan/html";
10
10
 
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";
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|(?<!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/|xtate/";
13
13
  var naivePattern = /bot|crawl|http|lighthouse|scan|search|spider/i;
14
14
  var pattern;
15
15
  function getPattern() {
@@ -27,7 +27,7 @@ function isbot(userAgent) {
27
27
  return Boolean(userAgent) && getPattern().test(userAgent);
28
28
  }
29
29
 
30
- // node_modules/hono/dist/compose.js
30
+ // ../../node_modules/hono/dist/compose.js
31
31
  var compose = (middleware, onError, onNotFound) => {
32
32
  return (context, next) => {
33
33
  let index = -1;
@@ -71,7 +71,7 @@ var compose = (middleware, onError, onNotFound) => {
71
71
  };
72
72
  };
73
73
 
74
- // node_modules/hono/dist/utils/body.js
74
+ // ../../node_modules/hono/dist/utils/body.js
75
75
  var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
76
76
  const { all = false, dot = false } = options;
77
77
  const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
@@ -135,7 +135,7 @@ var handleParsingNestedValues = (form, key, value) => {
135
135
  });
136
136
  };
137
137
 
138
- // node_modules/hono/dist/utils/url.js
138
+ // ../../node_modules/hono/dist/utils/url.js
139
139
  var splitPath = (path) => {
140
140
  const paths = path.split("/");
141
141
  if (paths[0] === "") {
@@ -330,7 +330,7 @@ var getQueryParams = (url, key) => {
330
330
  };
331
331
  var decodeURIComponent_ = decodeURIComponent;
332
332
 
333
- // node_modules/hono/dist/request.js
333
+ // ../../node_modules/hono/dist/request.js
334
334
  var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
335
335
  var HonoRequest = class {
336
336
  raw;
@@ -438,7 +438,7 @@ var HonoRequest = class {
438
438
  }
439
439
  };
440
440
 
441
- // node_modules/hono/dist/utils/html.js
441
+ // ../../node_modules/hono/dist/utils/html.js
442
442
  var HtmlEscapedCallbackPhase = {
443
443
  Stringify: 1,
444
444
  BeforeStream: 2,
@@ -476,7 +476,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
476
476
  }
477
477
  };
478
478
 
479
- // node_modules/hono/dist/context.js
479
+ // ../../node_modules/hono/dist/context.js
480
480
  var TEXT_PLAIN = "text/plain; charset=UTF-8";
481
481
  var setHeaders = (headers, map = {}) => {
482
482
  for (const key of Object.keys(map)) {
@@ -717,7 +717,7 @@ var Context = class {
717
717
  };
718
718
  };
719
719
 
720
- // node_modules/hono/dist/router.js
720
+ // ../../node_modules/hono/dist/router.js
721
721
  var METHOD_NAME_ALL = "ALL";
722
722
  var METHOD_NAME_ALL_LOWERCASE = "all";
723
723
  var METHODS = ["get", "post", "put", "delete", "options", "patch"];
@@ -725,10 +725,10 @@ var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is
725
725
  var UnsupportedPathError = class extends Error {
726
726
  };
727
727
 
728
- // node_modules/hono/dist/utils/constants.js
728
+ // ../../node_modules/hono/dist/utils/constants.js
729
729
  var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
730
730
 
731
- // node_modules/hono/dist/hono-base.js
731
+ // ../../node_modules/hono/dist/hono-base.js
732
732
  var notFoundHandler = (c) => {
733
733
  return c.text("404 Not Found", 404);
734
734
  };
@@ -801,6 +801,8 @@ var Hono = class {
801
801
  router: this.router,
802
802
  getPath: this.getPath
803
803
  });
804
+ clone.errorHandler = this.errorHandler;
805
+ clone.#notFoundHandler = this.#notFoundHandler;
804
806
  clone.routes = this.routes;
805
807
  return clone;
806
808
  }
@@ -841,7 +843,11 @@ var Hono = class {
841
843
  optionHandler = options;
842
844
  } else {
843
845
  optionHandler = options.optionHandler;
844
- replaceRequest = options.replaceRequest;
846
+ if (options.replaceRequest === false) {
847
+ replaceRequest = (request) => request;
848
+ } else {
849
+ replaceRequest = options.replaceRequest;
850
+ }
845
851
  }
846
852
  }
847
853
  const getOptions = optionHandler ? (c) => {
@@ -940,7 +946,7 @@ var Hono = class {
940
946
  };
941
947
  };
942
948
 
943
- // node_modules/hono/dist/router/reg-exp-router/node.js
949
+ // ../../node_modules/hono/dist/router/reg-exp-router/node.js
944
950
  var LABEL_REG_EXP_STR = "[^/]+";
945
951
  var ONLY_WILDCARD_REG_EXP_STR = ".*";
946
952
  var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
@@ -1041,7 +1047,7 @@ var Node = class {
1041
1047
  }
1042
1048
  };
1043
1049
 
1044
- // node_modules/hono/dist/router/reg-exp-router/trie.js
1050
+ // ../../node_modules/hono/dist/router/reg-exp-router/trie.js
1045
1051
  var Trie = class {
1046
1052
  #context = { varIndex: 0 };
1047
1053
  #root = new Node;
@@ -1097,7 +1103,7 @@ var Trie = class {
1097
1103
  }
1098
1104
  };
1099
1105
 
1100
- // node_modules/hono/dist/router/reg-exp-router/router.js
1106
+ // ../../node_modules/hono/dist/router/reg-exp-router/router.js
1101
1107
  var emptyParam = [];
1102
1108
  var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
1103
1109
  var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
@@ -1279,7 +1285,7 @@ var RegExpRouter = class {
1279
1285
  }
1280
1286
  };
1281
1287
 
1282
- // node_modules/hono/dist/router/smart-router/router.js
1288
+ // ../../node_modules/hono/dist/router/smart-router/router.js
1283
1289
  var SmartRouter = class {
1284
1290
  name = "SmartRouter";
1285
1291
  #routers = [];
@@ -1334,7 +1340,7 @@ var SmartRouter = class {
1334
1340
  }
1335
1341
  };
1336
1342
 
1337
- // node_modules/hono/dist/router/trie-router/node.js
1343
+ // ../../node_modules/hono/dist/router/trie-router/node.js
1338
1344
  var emptyParams = /* @__PURE__ */ Object.create(null);
1339
1345
  var Node2 = class {
1340
1346
  #methods;
@@ -1490,7 +1496,7 @@ var Node2 = class {
1490
1496
  }
1491
1497
  };
1492
1498
 
1493
- // node_modules/hono/dist/router/trie-router/router.js
1499
+ // ../../node_modules/hono/dist/router/trie-router/router.js
1494
1500
  var TrieRouter = class {
1495
1501
  name = "TrieRouter";
1496
1502
  #node;
@@ -1512,7 +1518,7 @@ var TrieRouter = class {
1512
1518
  }
1513
1519
  };
1514
1520
 
1515
- // node_modules/hono/dist/hono.js
1521
+ // ../../node_modules/hono/dist/hono.js
1516
1522
  var Hono2 = class extends Hono {
1517
1523
  constructor(options = {}) {
1518
1524
  super(options);
@@ -1522,13 +1528,13 @@ var Hono2 = class extends Hono {
1522
1528
  }
1523
1529
  };
1524
1530
 
1525
- // node_modules/hono/dist/adapter/bun/serve-static.js
1531
+ // ../../node_modules/hono/dist/adapter/bun/serve-static.js
1526
1532
  import { stat } from "node:fs/promises";
1527
1533
 
1528
- // node_modules/hono/dist/utils/compress.js
1534
+ // ../../node_modules/hono/dist/utils/compress.js
1529
1535
  var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
1530
1536
 
1531
- // node_modules/hono/dist/utils/filepath.js
1537
+ // ../../node_modules/hono/dist/utils/filepath.js
1532
1538
  var getFilePath = (options) => {
1533
1539
  let filename = options.filename;
1534
1540
  const defaultDocument = options.defaultDocument || "index.html";
@@ -1560,7 +1566,7 @@ var getFilePathWithoutDefaultDocument = (options) => {
1560
1566
  return path;
1561
1567
  };
1562
1568
 
1563
- // node_modules/hono/dist/utils/mime.js
1569
+ // ../../node_modules/hono/dist/utils/mime.js
1564
1570
  var getMimeType = (filename, mimes = baseMimes) => {
1565
1571
  const regexp = /\.([a-zA-Z0-9]+?)$/;
1566
1572
  const match = filename.match(regexp);
@@ -1632,7 +1638,7 @@ var _baseMimes = {
1632
1638
  };
1633
1639
  var baseMimes = _baseMimes;
1634
1640
 
1635
- // node_modules/hono/dist/middleware/serve-static/index.js
1641
+ // ../../node_modules/hono/dist/middleware/serve-static/index.js
1636
1642
  var ENCODINGS = {
1637
1643
  br: ".br",
1638
1644
  zstd: ".zst",
@@ -1729,7 +1735,7 @@ var serveStatic = (options) => {
1729
1735
  };
1730
1736
  };
1731
1737
 
1732
- // node_modules/hono/dist/adapter/bun/serve-static.js
1738
+ // ../../node_modules/hono/dist/adapter/bun/serve-static.js
1733
1739
  var serveStatic2 = (options) => {
1734
1740
  return async function serveStatic2(c, next) {
1735
1741
  const getContent = async (path) => {
@@ -1757,7 +1763,7 @@ var serveStatic2 = (options) => {
1757
1763
  };
1758
1764
  };
1759
1765
 
1760
- // node_modules/hono/dist/helper/ssg/middleware.js
1766
+ // ../../node_modules/hono/dist/helper/ssg/middleware.js
1761
1767
  var X_HONO_DISABLE_SSG_HEADER_KEY = "x-hono-disable-ssg";
1762
1768
  var SSG_DISABLED_RESPONSE = (() => {
1763
1769
  try {
@@ -1769,10 +1775,10 @@ var SSG_DISABLED_RESPONSE = (() => {
1769
1775
  return null;
1770
1776
  }
1771
1777
  })();
1772
- // node_modules/hono/dist/adapter/bun/ssg.js
1778
+ // ../../node_modules/hono/dist/adapter/bun/ssg.js
1773
1779
  var { write } = Bun;
1774
1780
 
1775
- // node_modules/hono/dist/helper/websocket/index.js
1781
+ // ../../node_modules/hono/dist/helper/websocket/index.js
1776
1782
  var WSContext = class {
1777
1783
  #init;
1778
1784
  constructor(init) {
@@ -1796,7 +1802,7 @@ var WSContext = class {
1796
1802
  }
1797
1803
  };
1798
1804
 
1799
- // node_modules/hono/dist/http-exception.js
1805
+ // ../../node_modules/hono/dist/http-exception.js
1800
1806
  var HTTPException = class extends Error {
1801
1807
  res;
1802
1808
  status;
@@ -1822,8 +1828,12 @@ var HTTPException = class extends Error {
1822
1828
  // src/server.ts
1823
1829
  var IS_PROD = false;
1824
1830
  var CWD = process.cwd();
1831
+ function createConfig(config) {
1832
+ return config;
1833
+ }
1825
1834
  function createRoute(handler) {
1826
1835
  let _handlers = {};
1836
+ let _middleware = [];
1827
1837
  if (handler) {
1828
1838
  _handlers["GET"] = handler;
1829
1839
  }
@@ -1849,34 +1859,52 @@ function createRoute(handler) {
1849
1859
  _handlers["PATCH"] = handler2;
1850
1860
  return api;
1851
1861
  },
1852
- async run(method, context) {
1853
- const handler2 = _handlers[method];
1854
- if (!handler2) {
1855
- throw new HTTPException(405, { message: "Method not allowed" });
1856
- }
1857
- const routeContent = await handler2(context);
1858
- if (routeContent instanceof Response) {
1859
- return routeContent;
1860
- }
1861
- const userIsBot = isbot(context.req.header("User-Agent"));
1862
- const streamOpt = context.req.query("__nostream");
1863
- const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1864
- const routeKind = typeof routeContent;
1865
- if (isHSHtml(routeContent)) {
1866
- if (streamingEnabled) {
1867
- return new StreamResponse(renderStream(routeContent));
1868
- } else {
1869
- const output = await renderAsync(routeContent);
1870
- return context.html(output);
1862
+ middleware(middleware) {
1863
+ _middleware = middleware;
1864
+ return api;
1865
+ },
1866
+ _getRouteHandlers() {
1867
+ return [
1868
+ ..._middleware,
1869
+ async (context) => {
1870
+ const method = context.req.method.toUpperCase();
1871
+ try {
1872
+ const handler2 = _handlers[method];
1873
+ if (!handler2) {
1874
+ throw new HTTPException(405, { message: "Method not allowed" });
1875
+ }
1876
+ const routeContent = await handler2(context);
1877
+ if (routeContent instanceof Response) {
1878
+ return routeContent;
1879
+ }
1880
+ const userIsBot = isbot(context.req.header("User-Agent"));
1881
+ const streamOpt = context.req.query("__nostream");
1882
+ const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1883
+ if (isHSHtml(routeContent)) {
1884
+ if (streamingEnabled) {
1885
+ return new StreamResponse(renderStream(routeContent));
1886
+ } else {
1887
+ const output = await renderAsync(routeContent);
1888
+ return context.html(output);
1889
+ }
1890
+ }
1891
+ if (routeContent instanceof Response) {
1892
+ return routeContent;
1893
+ }
1894
+ return context.text(String(routeContent));
1895
+ } catch (e) {
1896
+ !IS_PROD && console.error(e);
1897
+ return await showErrorReponse(context, e);
1898
+ }
1871
1899
  }
1872
- }
1873
- return context.text(String(routeContent));
1900
+ ];
1874
1901
  }
1875
1902
  };
1876
1903
  return api;
1877
1904
  }
1878
1905
  function createAPIRoute(handler) {
1879
1906
  let _handlers = {};
1907
+ let _middleware = [];
1880
1908
  if (handler) {
1881
1909
  _handlers["GET"] = handler;
1882
1910
  }
@@ -1902,30 +1930,46 @@ function createAPIRoute(handler) {
1902
1930
  _handlers["PATCH"] = handler2;
1903
1931
  return api;
1904
1932
  },
1905
- async run(method, context) {
1906
- const handler2 = _handlers[method];
1907
- if (!handler2) {
1908
- throw new Error("Method not allowed");
1909
- }
1910
- try {
1911
- const response = await handler2(context);
1912
- if (response instanceof Response) {
1913
- return response;
1914
- }
1915
- return context.json({ meta: { success: true, dtResponse: new Date }, data: response }, { status: 200 });
1916
- } catch (err) {
1917
- const e = err;
1918
- console.error(e);
1919
- return context.json({
1920
- meta: { success: false, dtResponse: new Date },
1921
- data: {},
1922
- error: {
1923
- message: e.message,
1924
- stack: IS_PROD ? undefined : e.stack?.split(`
1933
+ middleware(middleware) {
1934
+ _middleware = middleware;
1935
+ return api;
1936
+ },
1937
+ _getRouteHandlers() {
1938
+ return [
1939
+ ..._middleware,
1940
+ async (context) => {
1941
+ const method = context.req.method.toUpperCase();
1942
+ const handler2 = _handlers[method];
1943
+ if (!handler2) {
1944
+ return context.json({
1945
+ meta: { success: false, dtResponse: new Date },
1946
+ data: {},
1947
+ error: {
1948
+ message: "Method not allowed"
1949
+ }
1950
+ }, { status: 405 });
1951
+ }
1952
+ try {
1953
+ const response = await handler2(context);
1954
+ if (response instanceof Response) {
1955
+ return response;
1956
+ }
1957
+ return context.json({ meta: { success: true, dtResponse: new Date }, data: response }, { status: 200 });
1958
+ } catch (err) {
1959
+ const e = err;
1960
+ !IS_PROD && console.error(e);
1961
+ return context.json({
1962
+ meta: { success: false, dtResponse: new Date },
1963
+ data: {},
1964
+ error: {
1965
+ message: e.message,
1966
+ stack: IS_PROD ? undefined : e.stack?.split(`
1925
1967
  `)
1968
+ }
1969
+ }, { status: 500 });
1926
1970
  }
1927
- }, { status: 500 });
1928
- }
1971
+ }
1972
+ ];
1929
1973
  }
1930
1974
  };
1931
1975
  return api;
@@ -1941,13 +1985,10 @@ function getRunnableRoute(route) {
1941
1985
  if (kind === "object" && "default" in route) {
1942
1986
  return getRunnableRoute(route.default);
1943
1987
  }
1944
- throw new Error('Route not runnable. Use "export default createRoute()" to create a Hyperspan route.');
1988
+ throw new Error(`Route not runnable. Use "export default createRoute()" to create a Hyperspan route. Exported methods found were: ${Object.keys(route).join(", ")}`);
1945
1989
  }
1946
1990
  function isRunnableRoute(route) {
1947
- return typeof route === "object" && "run" in route;
1948
- }
1949
- function createLayout(layout) {
1950
- return layout;
1991
+ return typeof route === "object" && "_getRouteHandlers" in route;
1951
1992
  }
1952
1993
  async function showErrorReponse(context, err) {
1953
1994
  const output = render(html`
@@ -2000,20 +2041,8 @@ async function buildRoutes(config) {
2000
2041
  return routes;
2001
2042
  }
2002
2043
  function createRouteFromModule(RouteModule) {
2003
- return async (context) => {
2004
- const reqMethod = context.req.method.toUpperCase();
2005
- try {
2006
- const runnableRoute = getRunnableRoute(RouteModule);
2007
- const content = await runnableRoute.run(reqMethod, context);
2008
- if (content instanceof Response) {
2009
- return content;
2010
- }
2011
- return context.text(String(content));
2012
- } catch (e) {
2013
- console.error(e);
2014
- return await showErrorReponse(context, e);
2015
- }
2016
- };
2044
+ const route = getRunnableRoute(RouteModule);
2045
+ return route._getRouteHandlers();
2017
2046
  }
2018
2047
  async function createServer(config) {
2019
2048
  await Promise.all([buildClientJS(), buildClientCSS()]);
@@ -2026,7 +2055,8 @@ async function createServer(config) {
2026
2055
  const fullRouteFile = join(CWD, route.file);
2027
2056
  const routePattern = normalizePath(route.route);
2028
2057
  routeMap.push({ route: routePattern, file: route.file });
2029
- app.all(routePattern, createRouteFromModule(await import(fullRouteFile)));
2058
+ const routeHandlers = createRouteFromModule(await import(fullRouteFile));
2059
+ app.all(routePattern, ...routeHandlers);
2030
2060
  }
2031
2061
  if (routeMap.length === 0) {
2032
2062
  app.get("/", (context) => {
@@ -2092,7 +2122,7 @@ export {
2092
2122
  createRouteFromModule,
2093
2123
  createRoute,
2094
2124
  createReadableStreamFromAsyncGenerator,
2095
- createLayout,
2125
+ createConfig,
2096
2126
  createAPIRoute,
2097
2127
  buildRoutes,
2098
2128
  StreamResponse,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.1.8",
3
+ "version": "0.3.0",
4
4
  "description": "Hyperspan Web Framework",
5
- "main": "dist/server.js",
5
+ "main": "dist/server.ts",
6
6
  "types": "src/server.ts",
7
7
  "public": true,
8
8
  "publishConfig": {
@@ -53,21 +53,16 @@
53
53
  "prepack": "bun run clean && bun run build"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/bun": "^1.1.9",
57
- "@types/node": "^22.5.5",
58
- "@types/react": "^19.1.0",
59
- "bun-plugin-dts": "^0.3.0",
60
- "bun-types": "latest",
61
- "prettier": "^3.2.5"
62
- },
63
- "peerDependencies": {
64
- "typescript": "^5.0.0"
56
+ "@types/bun": "^1.2.14",
57
+ "@types/node": "^22.15.20",
58
+ "@types/react": "^19.1.5",
59
+ "prettier": "^3.5.3",
60
+ "typescript": "^5.8.3"
65
61
  },
66
62
  "dependencies": {
67
- "@hyperspan/html": "^0.1.6",
68
- "hono": "^4.7.4",
69
- "isbot": "^5.1.25",
70
- "preact": "^10.26.5",
71
- "zod": "^4.0.0-beta.20250415T232143"
63
+ "@hyperspan/html": "^0.1.7",
64
+ "hono": "^4.7.10",
65
+ "isbot": "^5.1.28",
66
+ "zod": "^3.25.42"
72
67
  }
73
68
  }
@@ -1,4 +1,4 @@
1
- import z from 'zod';
1
+ import z from 'zod/v4';
2
2
  import { createAction } from './actions';
3
3
  import { describe, it, expect } from 'bun:test';
4
4
  import { html, render, type HSHtml } from '@hyperspan/html';
package/src/actions.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { html, HSHtml } from '@hyperspan/html';
2
- import * as z from 'zod';
2
+ import * as z from 'zod/v4';
3
3
  import { HTTPException } from 'hono/http-exception';
4
4
 
5
5
  import type { THSResponseTypes } from './server';
package/src/assets.ts CHANGED
@@ -89,82 +89,35 @@ export function hyperspanScriptTags() {
89
89
  `;
90
90
  }
91
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 {
92
+ export function assetHash(content: string): string {
96
93
  return createHash('md5').update(content).digest('hex');
97
94
  }
98
95
 
99
96
  /**
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
- await Bun.build({
105
- entrypoints: [sourceFile],
106
- outdir: './public/_hs/js',
107
- minify: true,
108
- format: 'esm',
109
- target: 'browser',
110
- });
111
- }
112
-
113
- /**
114
- * Return a Preact component, mounted as an island in a <script> tag so it can be embedded into the page response.
97
+ * Island defaults
115
98
  */
116
- export async function createPreactIsland(file: string) {
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);
99
+ export const ISLAND_PUBLIC_PATH = '/_hs/js/islands';
100
+ export const ISLAND_DEFAULTS = () => ({
101
+ ssr: true,
102
+ });
103
+
104
+ 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
106
+ if (Component.__HS_ISLAND?.render) {
107
+ return Component.__HS_ISLAND.render(props, options);
127
108
  }
128
- if (!clientImportMap.has('react')) {
129
- clientImportMap.set('react', '.' + PREACT_PUBLIC_FILE_PATH);
130
- clientImportMap.set('react-dom', '.' + PREACT_PUBLIC_FILE_PATH);
131
- }
132
-
133
- let resultStr = 'import{h,render}from"preact";';
134
- const buildResult = await Bun.build({
135
- entrypoints: [filePath],
136
- minify: true,
137
- external: ['react', 'preact'],
138
- // @ts-ignore
139
- env: 'APP_PUBLIC_*', // Inlines any ENV that starts with 'APP_PUBLIC_'
140
- });
141
109
 
142
- for (const output of buildResult.outputs) {
143
- resultStr += await output.text(); // string
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);
144
113
  }
145
114
 
146
- // Find default export - this is our component
147
- const r = /export\{([a-zA-Z]+) as default\}/g;
148
- const matchExport = r.exec(resultStr);
149
-
150
- if (!matchExport) {
151
- throw new Error(
152
- 'File does not have a default export! Ensure a function has export default to use this.'
153
- );
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);
154
118
  }
155
119
 
156
- // Preact render/mount component
157
- const fn = matchExport[1];
158
- let _mounted = false;
159
-
160
- // Return HTML that will embed this component
161
- return (props: any) => {
162
- if (!_mounted) {
163
- _mounted = true;
164
- resultStr += `render(h(${fn}, ${JSON.stringify(props)}), document.getElementById("${jsId}"));`;
165
- }
166
- return html.raw(
167
- `<div id="${jsId}"></div><script type="module" data-source-id="${jsId}">${resultStr}</script>`
168
- );
169
- };
120
+ throw new Error(
121
+ `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?`
122
+ );
170
123
  }
@@ -1,2 +1,3 @@
1
1
  export * from 'preact/compat';
2
- export { h, render } from 'preact';
2
+ export * from 'preact/hooks';
3
+ export { h, render, hydrate } from 'preact';
package/src/server.ts CHANGED
@@ -6,6 +6,7 @@ import { buildClientJS, buildClientCSS } from './assets';
6
6
  import { Hono, type Context } from 'hono';
7
7
  import { serveStatic } from 'hono/bun';
8
8
  import { HTTPException } from 'hono/http-exception';
9
+ import type { HandlerResponse, MiddlewareHandler } from 'hono/types';
9
10
 
10
11
  export const IS_PROD = process.env.NODE_ENV === 'production';
11
12
  const CWD = process.cwd();
@@ -15,8 +16,7 @@ const CWD = process.cwd();
15
16
  */
16
17
  export type THSResponseTypes = HSHtml | Response | string | null;
17
18
  export type THSRouteHandler = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
18
- export type THSAPIResponseTypes = Response | Record<any, any> | void;
19
- export type THSAPIRouteHandler = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
19
+ export type THSAPIRouteHandler = (context: Context) => Promise<any> | any;
20
20
 
21
21
  export type THSRoute = {
22
22
  _kind: 'hsRoute';
@@ -25,15 +25,21 @@ export type THSRoute = {
25
25
  put: (handler: THSRouteHandler) => THSRoute;
26
26
  delete: (handler: THSRouteHandler) => THSRoute;
27
27
  patch: (handler: THSRouteHandler) => THSRoute;
28
- run: (method: string, context: Context) => Promise<Response>;
28
+ middleware: (middleware: Array<MiddlewareHandler>) => THSRoute;
29
+ _getRouteHandlers: () => Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)>;
29
30
  };
30
31
 
32
+ export function createConfig(config: THSServerConfig): THSServerConfig {
33
+ return config;
34
+ }
35
+
31
36
  /**
32
37
  * Define a route that can handle a direct HTTP request.
33
38
  * Route handlers should return a HSHtml or Response object
34
39
  */
35
40
  export function createRoute(handler?: THSRouteHandler): THSRoute {
36
41
  let _handlers: Record<string, THSRouteHandler> = {};
42
+ let _middleware: Array<MiddlewareHandler> = [];
37
43
 
38
44
  if (handler) {
39
45
  _handlers['GET'] = handler;
@@ -61,37 +67,57 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
61
67
  _handlers['PATCH'] = handler;
62
68
  return api;
63
69
  },
64
- async run(method: string, context: Context): Promise<Response> {
65
- const handler = _handlers[method];
66
- if (!handler) {
67
- throw new HTTPException(405, { message: 'Method not allowed' });
68
- }
69
-
70
- const routeContent = await handler(context);
71
-
72
- // Return Response if returned from route handler
73
- if (routeContent instanceof Response) {
74
- return routeContent;
75
- }
76
-
77
- // @TODO: Move this to config or something...
78
- const userIsBot = isbot(context.req.header('User-Agent'));
79
- const streamOpt = context.req.query('__nostream');
80
- const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
81
- const routeKind = typeof routeContent;
82
-
83
- // Render HSHtml if returned from route handler
84
- if (isHSHtml(routeContent)) {
85
- if (streamingEnabled) {
86
- return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
87
- } else {
88
- const output = await renderAsync(routeContent as HSHtml);
89
- return context.html(output);
90
- }
91
- }
92
-
93
- // Return unknown content - not specifically handled above
94
- return context.text(String(routeContent));
70
+ middleware(middleware: Array<MiddlewareHandler>) {
71
+ _middleware = middleware;
72
+ return api;
73
+ },
74
+ _getRouteHandlers() {
75
+ return [
76
+ ..._middleware,
77
+ async (context: Context) => {
78
+ const method = context.req.method.toUpperCase();
79
+
80
+ try {
81
+ const handler = _handlers[method];
82
+ if (!handler) {
83
+ throw new HTTPException(405, { message: 'Method not allowed' });
84
+ }
85
+
86
+ const routeContent = await handler(context);
87
+
88
+ // Return Response if returned from route handler
89
+ if (routeContent instanceof Response) {
90
+ return routeContent;
91
+ }
92
+
93
+ // @TODO: Move this to config or something...
94
+ const userIsBot = isbot(context.req.header('User-Agent'));
95
+ const streamOpt = context.req.query('__nostream');
96
+ const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
97
+
98
+ // Render HSHtml if returned from route handler
99
+ if (isHSHtml(routeContent)) {
100
+ if (streamingEnabled) {
101
+ return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
102
+ } else {
103
+ const output = await renderAsync(routeContent as HSHtml);
104
+ return context.html(output);
105
+ }
106
+ }
107
+
108
+ // Return custom Response if returned from route handler
109
+ if (routeContent instanceof Response) {
110
+ return routeContent;
111
+ }
112
+
113
+ // Return unknown content - not specifically handled above
114
+ return context.text(String(routeContent));
115
+ } catch (e) {
116
+ !IS_PROD && console.error(e);
117
+ return await showErrorReponse(context, e as Error);
118
+ }
119
+ },
120
+ ];
95
121
  },
96
122
  };
97
123
 
@@ -104,6 +130,7 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
104
130
  */
105
131
  export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
106
132
  let _handlers: Record<string, THSAPIRouteHandler> = {};
133
+ let _middleware: Array<MiddlewareHandler> = [];
107
134
 
108
135
  if (handler) {
109
136
  _handlers['GET'] = handler;
@@ -131,39 +158,59 @@ export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
131
158
  _handlers['PATCH'] = handler;
132
159
  return api;
133
160
  },
134
- async run(method: string, context: Context): Promise<Response> {
135
- const handler = _handlers[method];
136
- if (!handler) {
137
- throw new Error('Method not allowed');
138
- }
139
-
140
- try {
141
- const response = await handler(context);
142
-
143
- if (response instanceof Response) {
144
- return response;
145
- }
161
+ middleware(middleware: Array<MiddlewareHandler>) {
162
+ _middleware = middleware;
163
+ return api;
164
+ },
165
+ _getRouteHandlers() {
166
+ return [
167
+ ..._middleware,
168
+ async (context: Context) => {
169
+ const method = context.req.method.toUpperCase();
170
+ const handler = _handlers[method];
171
+
172
+ if (!handler) {
173
+ return context.json(
174
+ {
175
+ meta: { success: false, dtResponse: new Date() },
176
+ data: {},
177
+ error: {
178
+ message: 'Method not allowed',
179
+ },
180
+ },
181
+ { status: 405 }
182
+ );
183
+ }
146
184
 
147
- return context.json(
148
- { meta: { success: true, dtResponse: new Date() }, data: response },
149
- { status: 200 }
150
- );
151
- } catch (err) {
152
- const e = err as Error;
153
- console.error(e);
154
-
155
- return context.json(
156
- {
157
- meta: { success: false, dtResponse: new Date() },
158
- data: {},
159
- error: {
160
- message: e.message,
161
- stack: IS_PROD ? undefined : e.stack?.split('\n'),
162
- },
163
- },
164
- { status: 500 }
165
- );
166
- }
185
+ try {
186
+ const response = await handler(context);
187
+
188
+ if (response instanceof Response) {
189
+ return response;
190
+ }
191
+
192
+ return context.json(
193
+ { meta: { success: true, dtResponse: new Date() }, data: response },
194
+ { status: 200 }
195
+ );
196
+ } catch (err) {
197
+ const e = err as Error;
198
+ !IS_PROD && console.error(e);
199
+
200
+ return context.json(
201
+ {
202
+ meta: { success: false, dtResponse: new Date() },
203
+ data: {},
204
+ error: {
205
+ message: e.message,
206
+ stack: IS_PROD ? undefined : e.stack?.split('\n'),
207
+ },
208
+ },
209
+ { status: 500 }
210
+ );
211
+ }
212
+ },
213
+ ];
167
214
  },
168
215
  };
169
216
 
@@ -195,21 +242,13 @@ export function getRunnableRoute(route: unknown): THSRoute {
195
242
 
196
243
  // No route -> error
197
244
  throw new Error(
198
- 'Route not runnable. Use "export default createRoute()" to create a Hyperspan route.'
245
+ `Route not runnable. Use "export default createRoute()" to create a Hyperspan route. Exported methods found were: ${Object.keys(route as {}).join(', ')}`
199
246
  );
200
247
  }
201
248
 
202
249
  export function isRunnableRoute(route: unknown): boolean {
203
250
  // @ts-ignore
204
- return typeof route === 'object' && 'run' in route;
205
- }
206
-
207
- /**
208
- * Create a layout for a Hyperspan app. Passthrough for now.
209
- * Future intent is to be able to conditionally render a layout for full page content vs. partial content.
210
- */
211
- export function createLayout<T>(layout: (props: T) => HSHtml | Promise<HSHtml>) {
212
- return layout;
251
+ return typeof route === 'object' && '_getRouteHandlers' in route;
213
252
  }
214
253
 
215
254
  /**
@@ -234,6 +273,7 @@ export type THSServerConfig = {
234
273
  appDir: string;
235
274
  staticFileRoot: string;
236
275
  rewrites?: Array<{ source: string; destination: string }>;
276
+ islandPlugins?: Array<any>; // Loaders for client islands
237
277
  // For customizing the routes and adding your own...
238
278
  beforeRoutesAdded?: (app: Hono) => void;
239
279
  afterRoutesAdded?: (app: Hono) => void;
@@ -300,24 +340,11 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
300
340
  /**
301
341
  * Run route from file
302
342
  */
303
- export function createRouteFromModule(RouteModule: any): (context: Context) => Promise<Response> {
304
- return async (context: Context) => {
305
- const reqMethod = context.req.method.toUpperCase();
306
-
307
- try {
308
- const runnableRoute = getRunnableRoute(RouteModule);
309
- const content = await runnableRoute.run(reqMethod, context);
310
-
311
- if (content instanceof Response) {
312
- return content;
313
- }
314
-
315
- return context.text(String(content));
316
- } catch (e) {
317
- console.error(e);
318
- return await showErrorReponse(context, e as Error);
319
- }
320
- };
343
+ export function createRouteFromModule(
344
+ RouteModule: any
345
+ ): Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)> {
346
+ const route = getRunnableRoute(RouteModule);
347
+ return route._getRouteHandlers();
321
348
  }
322
349
 
323
350
  /**
@@ -344,7 +371,8 @@ export async function createServer(config: THSServerConfig): Promise<Hono> {
344
371
  routeMap.push({ route: routePattern, file: route.file });
345
372
 
346
373
  // Import route
347
- app.all(routePattern, createRouteFromModule(await import(fullRouteFile)));
374
+ const routeHandlers = createRouteFromModule(await import(fullRouteFile));
375
+ app.all(routePattern, ...routeHandlers);
348
376
  }
349
377
 
350
378
  // Help route if no routes found
@@ -381,6 +409,7 @@ export async function createServer(config: THSServerConfig): Promise<Hono> {
381
409
  );
382
410
 
383
411
  app.notFound((context) => {
412
+ // @TODO: Add a custom 404 route
384
413
  return context.text('Not... found?', { status: 404 });
385
414
  });
386
415