@hyperspan/framework 0.1.6 → 0.1.8
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 +1 -1
- package/dist/assets.js +56 -9
- package/dist/server.js +6 -3
- package/package.json +3 -3
- package/src/actions.test.ts +4 -4
- package/src/actions.ts +2 -2
- package/src/assets.ts +82 -9
- package/src/clientjs/hyperspan-client.ts +23 -8
- package/src/clientjs/preact.ts +2 -1
- package/src/server.ts +24 -22
package/build.ts
CHANGED
package/dist/assets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/assets.ts
|
|
2
2
|
import { html } from "@hyperspan/html";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { readdir } from "node:fs/promises";
|
|
4
5
|
import { resolve } from "node:path";
|
|
5
6
|
var IS_PROD = false;
|
|
@@ -45,15 +46,7 @@ function hyperspanScriptTags() {
|
|
|
45
46
|
const jsFiles = Array.from(clientJSFiles.entries());
|
|
46
47
|
return html`
|
|
47
48
|
<script type="importmap">
|
|
48
|
-
{
|
|
49
|
-
"imports": {
|
|
50
|
-
"preact": "https://esm.sh/preact@10.26.4",
|
|
51
|
-
"preact/": "https://esm.sh/preact@10.26.4/",
|
|
52
|
-
"react": "https://esm.sh/preact@10.26.4/compat",
|
|
53
|
-
"react/": "https://esm.sh/preact@10.26.4/compat/",
|
|
54
|
-
"react-dom": "https://esm.sh/preact@10.26.4/compat"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
49
|
+
{"imports": ${Object.fromEntries(clientImportMap)}}
|
|
57
50
|
</script>
|
|
58
51
|
${jsFiles.map(([key, file]) => html`<script
|
|
59
52
|
id="js-${key}"
|
|
@@ -62,9 +55,63 @@ function hyperspanScriptTags() {
|
|
|
62
55
|
></script>`)}
|
|
63
56
|
`;
|
|
64
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
|
+
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);
|
|
81
|
+
}
|
|
82
|
+
if (!clientImportMap.has("react")) {
|
|
83
|
+
clientImportMap.set("react", "." + PREACT_PUBLIC_FILE_PATH);
|
|
84
|
+
clientImportMap.set("react-dom", "." + PREACT_PUBLIC_FILE_PATH);
|
|
85
|
+
}
|
|
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.");
|
|
100
|
+
}
|
|
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
|
+
};
|
|
110
|
+
}
|
|
65
111
|
export {
|
|
66
112
|
hyperspanStyleTags,
|
|
67
113
|
hyperspanScriptTags,
|
|
114
|
+
createPreactIsland,
|
|
68
115
|
clientJSFiles,
|
|
69
116
|
clientImportMap,
|
|
70
117
|
clientCSSFiles,
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ 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 {
|
|
9
|
+
import { html, isHSHtml, renderStream, renderAsync, render } from "@hyperspan/html";
|
|
10
10
|
|
|
11
11
|
// node_modules/isbot/index.mjs
|
|
12
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";
|
|
@@ -1862,7 +1862,7 @@ function createRoute(handler) {
|
|
|
1862
1862
|
const streamOpt = context.req.query("__nostream");
|
|
1863
1863
|
const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
|
|
1864
1864
|
const routeKind = typeof routeContent;
|
|
1865
|
-
if (
|
|
1865
|
+
if (isHSHtml(routeContent)) {
|
|
1866
1866
|
if (streamingEnabled) {
|
|
1867
1867
|
return new StreamResponse(renderStream(routeContent));
|
|
1868
1868
|
} else {
|
|
@@ -1946,6 +1946,9 @@ function getRunnableRoute(route) {
|
|
|
1946
1946
|
function isRunnableRoute(route) {
|
|
1947
1947
|
return typeof route === "object" && "run" in route;
|
|
1948
1948
|
}
|
|
1949
|
+
function createLayout(layout) {
|
|
1950
|
+
return layout;
|
|
1951
|
+
}
|
|
1949
1952
|
async function showErrorReponse(context, err) {
|
|
1950
1953
|
const output = render(html`
|
|
1951
1954
|
<main>
|
|
@@ -1963,7 +1966,6 @@ async function showErrorReponse(context, err) {
|
|
|
1963
1966
|
var ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
|
|
1964
1967
|
async function buildRoutes(config) {
|
|
1965
1968
|
const routesDir = join(config.appDir, "routes");
|
|
1966
|
-
console.log(routesDir);
|
|
1967
1969
|
const files = await readdir(routesDir, { recursive: true });
|
|
1968
1970
|
const routes = [];
|
|
1969
1971
|
for (const file of files) {
|
|
@@ -2090,6 +2092,7 @@ export {
|
|
|
2090
2092
|
createRouteFromModule,
|
|
2091
2093
|
createRoute,
|
|
2092
2094
|
createReadableStreamFromAsyncGenerator,
|
|
2095
|
+
createLayout,
|
|
2093
2096
|
createAPIRoute,
|
|
2094
2097
|
buildRoutes,
|
|
2095
2098
|
StreamResponse,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"types": "src/server.ts",
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
"typescript": "^5.0.0"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@hyperspan/html": "^0.1.
|
|
68
|
-
"@preact/compat": "^18.3.1",
|
|
67
|
+
"@hyperspan/html": "^0.1.6",
|
|
69
68
|
"hono": "^4.7.4",
|
|
70
69
|
"isbot": "^5.1.25",
|
|
70
|
+
"preact": "^10.26.5",
|
|
71
71
|
"zod": "^4.0.0-beta.20250415T232143"
|
|
72
72
|
}
|
|
73
73
|
}
|
package/src/actions.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
import { createAction } from './actions';
|
|
3
3
|
import { describe, it, expect } from 'bun:test';
|
|
4
|
-
import { html, render, type
|
|
4
|
+
import { html, render, type HSHtml } from '@hyperspan/html';
|
|
5
5
|
import type { Context } from 'hono';
|
|
6
6
|
|
|
7
7
|
describe('createAction', () => {
|
|
@@ -24,7 +24,7 @@ describe('createAction', () => {
|
|
|
24
24
|
});
|
|
25
25
|
const action = createAction(schema, formWithNameOnly);
|
|
26
26
|
|
|
27
|
-
const formResponse = render(action.render({ data: { name: 'John' } }) as
|
|
27
|
+
const formResponse = render(action.render({ data: { name: 'John' } }) as HSHtml);
|
|
28
28
|
expect(formResponse).toContain('value="John"');
|
|
29
29
|
});
|
|
30
30
|
});
|
|
@@ -55,7 +55,7 @@ describe('createAction', () => {
|
|
|
55
55
|
|
|
56
56
|
const response = await action.run('POST', mockContext);
|
|
57
57
|
|
|
58
|
-
const formResponse = render(response as
|
|
58
|
+
const formResponse = render(response as HSHtml);
|
|
59
59
|
expect(formResponse).toContain('Thanks for submitting the form, John!');
|
|
60
60
|
});
|
|
61
61
|
});
|
|
@@ -87,7 +87,7 @@ describe('createAction', () => {
|
|
|
87
87
|
|
|
88
88
|
const response = await action.run('POST', mockContext);
|
|
89
89
|
|
|
90
|
-
const formResponse = render(response as
|
|
90
|
+
const formResponse = render(response as HSHtml);
|
|
91
91
|
expect(formResponse).toContain('There was an error!');
|
|
92
92
|
});
|
|
93
93
|
});
|
package/src/actions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { 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,7 +20,7 @@ import type { Context } from 'hono';
|
|
|
20
20
|
*/
|
|
21
21
|
export interface HSAction<T extends z.ZodTypeAny> {
|
|
22
22
|
_kind: string;
|
|
23
|
-
form(renderForm: ({ data }: { data?: z.infer<T> }) =>
|
|
23
|
+
form(renderForm: ({ data }: { data?: z.infer<T> }) => HSHtml): HSAction<T>;
|
|
24
24
|
post(handler: (c: Context, { data }: { data?: z.infer<T> }) => THSResponseTypes): HSAction<T>;
|
|
25
25
|
error(
|
|
26
26
|
handler: (
|
package/src/assets.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { html } from '@hyperspan/html';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { readdir } from 'node:fs/promises';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
5
|
|
|
@@ -75,15 +76,7 @@ export function hyperspanScriptTags() {
|
|
|
75
76
|
|
|
76
77
|
return html`
|
|
77
78
|
<script type="importmap">
|
|
78
|
-
{
|
|
79
|
-
"imports": {
|
|
80
|
-
"preact": "https://esm.sh/preact@10.26.4",
|
|
81
|
-
"preact/": "https://esm.sh/preact@10.26.4/",
|
|
82
|
-
"react": "https://esm.sh/preact@10.26.4/compat",
|
|
83
|
-
"react/": "https://esm.sh/preact@10.26.4/compat/",
|
|
84
|
-
"react-dom": "https://esm.sh/preact@10.26.4/compat"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
79
|
+
{"imports": ${Object.fromEntries(clientImportMap)}}
|
|
87
80
|
</script>
|
|
88
81
|
${jsFiles.map(
|
|
89
82
|
([key, file]) =>
|
|
@@ -95,3 +88,83 @@ export function hyperspanScriptTags() {
|
|
|
95
88
|
)}
|
|
96
89
|
`;
|
|
97
90
|
}
|
|
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
|
+
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.
|
|
115
|
+
*/
|
|
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);
|
|
127
|
+
}
|
|
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
|
+
|
|
142
|
+
for (const output of buildResult.outputs) {
|
|
143
|
+
resultStr += await output.text(); // string
|
|
144
|
+
}
|
|
145
|
+
|
|
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
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
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
|
+
};
|
|
170
|
+
}
|
|
@@ -26,14 +26,10 @@ function htmlAsyncContentObserver() {
|
|
|
26
26
|
const slotEl = document.getElementById(slotId);
|
|
27
27
|
|
|
28
28
|
if (slotEl) {
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
*/
|
package/src/clientjs/preact.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from '
|
|
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 {
|
|
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,8 +13,10 @@ const CWD = process.cwd();
|
|
|
13
13
|
/**
|
|
14
14
|
* Types
|
|
15
15
|
*/
|
|
16
|
-
export type THSResponseTypes =
|
|
16
|
+
export type THSResponseTypes = HSHtml | Response | string | null;
|
|
17
17
|
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>;
|
|
18
20
|
|
|
19
21
|
export type THSRoute = {
|
|
20
22
|
_kind: 'hsRoute';
|
|
@@ -28,7 +30,7 @@ export type THSRoute = {
|
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Define a route that can handle a direct HTTP request.
|
|
31
|
-
* Route handlers should return a
|
|
33
|
+
* Route handlers should return a HSHtml or Response object
|
|
32
34
|
*/
|
|
33
35
|
export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
34
36
|
let _handlers: Record<string, THSRouteHandler> = {};
|
|
@@ -78,19 +80,12 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
|
78
80
|
const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
|
|
79
81
|
const routeKind = typeof routeContent;
|
|
80
82
|
|
|
81
|
-
// Render
|
|
82
|
-
if (
|
|
83
|
-
routeContent &&
|
|
84
|
-
routeKind === 'object' &&
|
|
85
|
-
(routeContent instanceof TmplHtml ||
|
|
86
|
-
routeContent.constructor.name === 'TmplHtml' ||
|
|
87
|
-
// @ts-ignore
|
|
88
|
-
routeContent?._kind === 'TmplHtml')
|
|
89
|
-
) {
|
|
83
|
+
// Render HSHtml if returned from route handler
|
|
84
|
+
if (isHSHtml(routeContent)) {
|
|
90
85
|
if (streamingEnabled) {
|
|
91
|
-
return new StreamResponse(renderStream(routeContent as
|
|
86
|
+
return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
|
|
92
87
|
} else {
|
|
93
|
-
const output = await renderAsync(routeContent as
|
|
88
|
+
const output = await renderAsync(routeContent as HSHtml);
|
|
94
89
|
return context.html(output);
|
|
95
90
|
}
|
|
96
91
|
}
|
|
@@ -107,8 +102,8 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
|
107
102
|
* Create new API Route
|
|
108
103
|
* API Route handlers should return a JSON object or a Response
|
|
109
104
|
*/
|
|
110
|
-
export function createAPIRoute(handler?:
|
|
111
|
-
let _handlers: Record<string,
|
|
105
|
+
export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
|
|
106
|
+
let _handlers: Record<string, THSAPIRouteHandler> = {};
|
|
112
107
|
|
|
113
108
|
if (handler) {
|
|
114
109
|
_handlers['GET'] = handler;
|
|
@@ -116,23 +111,23 @@ export function createAPIRoute(handler?: THSRouteHandler): THSRoute {
|
|
|
116
111
|
|
|
117
112
|
const api: THSRoute = {
|
|
118
113
|
_kind: 'hsRoute',
|
|
119
|
-
get(handler:
|
|
114
|
+
get(handler: THSAPIRouteHandler) {
|
|
120
115
|
_handlers['GET'] = handler;
|
|
121
116
|
return api;
|
|
122
117
|
},
|
|
123
|
-
post(handler:
|
|
118
|
+
post(handler: THSAPIRouteHandler) {
|
|
124
119
|
_handlers['POST'] = handler;
|
|
125
120
|
return api;
|
|
126
121
|
},
|
|
127
|
-
put(handler:
|
|
122
|
+
put(handler: THSAPIRouteHandler) {
|
|
128
123
|
_handlers['PUT'] = handler;
|
|
129
124
|
return api;
|
|
130
125
|
},
|
|
131
|
-
delete(handler:
|
|
126
|
+
delete(handler: THSAPIRouteHandler) {
|
|
132
127
|
_handlers['DELETE'] = handler;
|
|
133
128
|
return api;
|
|
134
129
|
},
|
|
135
|
-
patch(handler:
|
|
130
|
+
patch(handler: THSAPIRouteHandler) {
|
|
136
131
|
_handlers['PATCH'] = handler;
|
|
137
132
|
return api;
|
|
138
133
|
},
|
|
@@ -209,6 +204,14 @@ export function isRunnableRoute(route: unknown): boolean {
|
|
|
209
204
|
return typeof route === 'object' && 'run' in route;
|
|
210
205
|
}
|
|
211
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;
|
|
213
|
+
}
|
|
214
|
+
|
|
212
215
|
/**
|
|
213
216
|
* Basic error handling
|
|
214
217
|
* @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
|
|
@@ -249,7 +252,6 @@ const ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
|
|
|
249
252
|
export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[]> {
|
|
250
253
|
// Walk all pages and add them as routes
|
|
251
254
|
const routesDir = join(config.appDir, 'routes');
|
|
252
|
-
console.log(routesDir);
|
|
253
255
|
const files = await readdir(routesDir, { recursive: true });
|
|
254
256
|
const routes: THSRouteMap[] = [];
|
|
255
257
|
|