@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 +30 -0
- package/README.md +8 -2
- package/dist/assets.js +18 -49
- package/dist/server.js +123 -93
- package/package.json +11 -16
- package/src/actions.test.ts +1 -1
- package/src/actions.ts +1 -1
- package/src/assets.ts +20 -67
- package/src/clientjs/preact.ts +2 -1
- package/src/server.ts +124 -95
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
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
4
8
|
|
|
5
|
-
|
|
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
|
-
|
|
59
|
-
function md5(content) {
|
|
58
|
+
function assetHash(content) {
|
|
60
59
|
return createHash("md5").update(content).digest("hex");
|
|
61
60
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 (
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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/|^{{.*}}$|
|
|
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
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
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
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
-
}
|
|
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(
|
|
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" && "
|
|
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
|
-
|
|
2004
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
|
-
"main": "dist/server.
|
|
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.
|
|
57
|
-
"@types/node": "^22.
|
|
58
|
-
"@types/react": "^19.1.
|
|
59
|
-
"
|
|
60
|
-
"
|
|
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.
|
|
68
|
-
"hono": "^4.7.
|
|
69
|
-
"isbot": "^5.1.
|
|
70
|
-
"
|
|
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
|
}
|
package/src/actions.test.ts
CHANGED
package/src/actions.ts
CHANGED
package/src/assets.ts
CHANGED
|
@@ -89,82 +89,35 @@ export function hyperspanScriptTags() {
|
|
|
89
89
|
`;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
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
|
-
*
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
}
|
package/src/clientjs/preact.ts
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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' && '
|
|
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(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
|