@hyperspan/framework 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.ts +1 -1
- package/dist/assets.js +56 -9
- package/dist/server.js +12 -6
- 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 +7 -14
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
|
+
const preactClient = 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,10 +6,10 @@ import {
|
|
|
6
6
|
// src/server.ts
|
|
7
7
|
import { readdir } from "node:fs/promises";
|
|
8
8
|
import { basename, extname, join } from "node:path";
|
|
9
|
-
import {
|
|
9
|
+
import { html, isHSHtml, renderStream, renderAsync, render } from "@hyperspan/html";
|
|
10
10
|
|
|
11
11
|
// node_modules/isbot/index.mjs
|
|
12
|
-
var fullPattern = " daum[ /]| deusu/| yadirectfetcher|(?:^|[^g])news(?!sapphire)|(?<! (?:channel/|google/))google(?!(app|/google| pixel))|(?<! cu)bots?(?:\\b|_)|(?<!(?:lib))http|(?<![hg]m)score|@[a-z][\\w-]+\\.|\\(\\)|\\.com\\b|\\btime/|\\||^<|^[\\w \\.\\-\\(?:\\):%]+(?:/v?\\d+(?:\\.\\d+)?(?:\\.\\d{1,10})*?)?(?:,|$)|^[^ ]{50,}$|^\\d+\\b|^\\w*search\\b|^\\w+/[\\w\\(\\)]*$|^active|^ad muncher|^amaya|^avsdevicesdk/|^biglotron|^bot|^bw/|^clamav[ /]|^client/|^cobweb/|^custom|^ddg[_-]android|^discourse|^dispatch/\\d|^downcast/|^duckduckgo|^email|^facebook|^getright/|^gozilla/|^hobbit|^hotzonu|^hwcdn/|^igetter/|^jeode/|^jetty/|^jigsaw|^microsoft bits|^movabletype|^mozilla/\\d\\.\\d\\s[\\w\\.-]+$|^mozilla/\\d\\.\\d\\s\\(compatible;?(?:\\s\\w+\\/\\d+\\.\\d+)?\\)$|^navermailapp|^netsurf|^offline|^openai/|^owler|^php|^postman|^python|^rank|^read|^reed|^rest|^rss|^snapchat|^space bison|^svn|^swcd |^taringa|^thumbor/|^track|^w3c|^webbandit/|^webcopier|^wget|^whatsapp|^wordpress|^xenu link sleuth|^yahoo|^yandex|^zdm/\\d|^zoom marketplace/|^{{.*}}$|
|
|
12
|
+
var fullPattern = " daum[ /]| deusu/| yadirectfetcher|(?:^|[^g])news(?!sapphire)|(?<! (?:channel/|google/))google(?!(app|/google| pixel))|(?<! cu)bots?(?:\\b|_)|(?<!(?:lib))http|(?<![hg]m)score|(?<!cam)scan|@[a-z][\\w-]+\\.|\\(\\)|\\.com\\b|\\btime/|\\||^<|^[\\w \\.\\-\\(?:\\):%]+(?:/v?\\d+(?:\\.\\d+)?(?:\\.\\d{1,10})*?)?(?:,|$)|^[^ ]{50,}$|^\\d+\\b|^\\w*search\\b|^\\w+/[\\w\\(\\)]*$|^active|^ad muncher|^amaya|^avsdevicesdk/|^biglotron|^bot|^bw/|^clamav[ /]|^client/|^cobweb/|^custom|^ddg[_-]android|^discourse|^dispatch/\\d|^downcast/|^duckduckgo|^email|^facebook|^getright/|^gozilla/|^hobbit|^hotzonu|^hwcdn/|^igetter/|^jeode/|^jetty/|^jigsaw|^microsoft bits|^movabletype|^mozilla/\\d\\.\\d\\s[\\w\\.-]+$|^mozilla/\\d\\.\\d\\s\\(compatible;?(?:\\s\\w+\\/\\d+\\.\\d+)?\\)$|^navermailapp|^netsurf|^offline|^openai/|^owler|^php|^postman|^python|^rank|^read|^reed|^rest|^rss|^snapchat|^space bison|^svn|^swcd |^taringa|^thumbor/|^track|^w3c|^webbandit/|^webcopier|^wget|^whatsapp|^wordpress|^xenu link sleuth|^yahoo|^yandex|^zdm/\\d|^zoom marketplace/|^{{.*}}$|analyzer|archive|ask jeeves/teoma|audit|bit\\.ly/|bluecoat drtr|browsex|burpcollaborator|capture|catch|check\\b|checker|chrome-lighthouse|chromeframe|classifier|cloudflare|convertify|crawl|cypress/|dareboost|datanyze|dejaclick|detect|dmbrowser|download|evc-batch/|exaleadcloudview|feed|firephp|functionize|gomezagent|grab|headless|httrack|hubspot marketing grader|hydra|ibisbrowser|infrawatch|insight|inspect|iplabel|ips-agent|java(?!;)|library|linkcheck|mail\\.ru/|manager|measure|neustar wpm|node|nutch|offbyone|onetrust|optimize|pageburst|pagespeed|parser|perl|phantomjs|pingdom|powermarks|preview|proxy|ptst[ /]\\d|retriever|rexx;|rigor|rss\\b|scrape|server|sogou|sparkler/|speedcurve|spider|splash|statuscake|supercleaner|synapse|synthetic|tools|torrent|transcoder|url|validator|virtuoso|wappalyzer|webglance|webkit2png|whatcms/";
|
|
13
13
|
var naivePattern = /bot|crawl|http|lighthouse|scan|search|spider/i;
|
|
14
14
|
var pattern;
|
|
15
15
|
function getPattern() {
|
|
@@ -841,7 +841,11 @@ var Hono = class {
|
|
|
841
841
|
optionHandler = options;
|
|
842
842
|
} else {
|
|
843
843
|
optionHandler = options.optionHandler;
|
|
844
|
-
|
|
844
|
+
if (options.replaceRequest === false) {
|
|
845
|
+
replaceRequest = (request) => request;
|
|
846
|
+
} else {
|
|
847
|
+
replaceRequest = options.replaceRequest;
|
|
848
|
+
}
|
|
845
849
|
}
|
|
846
850
|
}
|
|
847
851
|
const getOptions = optionHandler ? (c) => {
|
|
@@ -851,7 +855,8 @@ var Hono = class {
|
|
|
851
855
|
let executionContext = undefined;
|
|
852
856
|
try {
|
|
853
857
|
executionContext = c.executionCtx;
|
|
854
|
-
} catch {
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
855
860
|
return [c.env, executionContext];
|
|
856
861
|
};
|
|
857
862
|
replaceRequest ||= (() => {
|
|
@@ -1745,7 +1750,8 @@ var serveStatic2 = (options) => {
|
|
|
1745
1750
|
try {
|
|
1746
1751
|
const stats = await stat(path);
|
|
1747
1752
|
isDir2 = stats.isDirectory();
|
|
1748
|
-
} catch {
|
|
1753
|
+
} catch {
|
|
1754
|
+
}
|
|
1749
1755
|
return isDir2;
|
|
1750
1756
|
};
|
|
1751
1757
|
return serveStatic({
|
|
@@ -1862,7 +1868,7 @@ function createRoute(handler) {
|
|
|
1862
1868
|
const streamOpt = context.req.query("__nostream");
|
|
1863
1869
|
const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
|
|
1864
1870
|
const routeKind = typeof routeContent;
|
|
1865
|
-
if (
|
|
1871
|
+
if (isHSHtml(routeContent)) {
|
|
1866
1872
|
if (streamingEnabled) {
|
|
1867
1873
|
return new StreamResponse(renderStream(routeContent));
|
|
1868
1874
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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
|
+
const preactClient = 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,7 +13,7 @@ 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
18
|
|
|
19
19
|
export type THSRoute = {
|
|
@@ -28,7 +28,7 @@ export type THSRoute = {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Define a route that can handle a direct HTTP request.
|
|
31
|
-
* Route handlers should return a
|
|
31
|
+
* Route handlers should return a HSHtml or Response object
|
|
32
32
|
*/
|
|
33
33
|
export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
34
34
|
let _handlers: Record<string, THSRouteHandler> = {};
|
|
@@ -78,19 +78,12 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
|
78
78
|
const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
|
|
79
79
|
const routeKind = typeof routeContent;
|
|
80
80
|
|
|
81
|
-
// Render
|
|
82
|
-
if (
|
|
83
|
-
routeContent &&
|
|
84
|
-
routeKind === 'object' &&
|
|
85
|
-
(routeContent instanceof TmplHtml ||
|
|
86
|
-
routeContent.constructor.name === 'TmplHtml' ||
|
|
87
|
-
// @ts-ignore
|
|
88
|
-
routeContent?._kind === 'TmplHtml')
|
|
89
|
-
) {
|
|
81
|
+
// Render HSHtml if returned from route handler
|
|
82
|
+
if (isHSHtml(routeContent)) {
|
|
90
83
|
if (streamingEnabled) {
|
|
91
|
-
return new StreamResponse(renderStream(routeContent as
|
|
84
|
+
return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
|
|
92
85
|
} else {
|
|
93
|
-
const output = await renderAsync(routeContent as
|
|
86
|
+
const output = await renderAsync(routeContent as HSHtml);
|
|
94
87
|
return context.html(output);
|
|
95
88
|
}
|
|
96
89
|
}
|