@hyperspan/framework 0.0.3 → 0.1.1
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/README.md +3 -81
- package/build.ts +24 -26
- package/dist/assets.d.ts +34 -0
- package/dist/assets.js +394 -0
- package/dist/index.d.ts +101 -30
- package/dist/index.js +2421 -408
- package/dist/server.d.ts +85 -73
- package/dist/server.js +2142 -1603
- package/package.json +42 -29
- package/src/assets.ts +141 -0
- package/src/clientjs/hyperspan-client.ts +7 -175
- package/src/clientjs/idiomorph.esm.js +1278 -0
- package/src/clientjs/preact.ts +1 -0
- package/src/index.ts +1 -14
- package/src/server.ts +232 -151
- package/.prettierrc +0 -7
- package/bun.lockb +0 -0
- package/src/app.ts +0 -186
- package/src/clientjs/idomorph.esm.js +0 -854
- package/src/document.ts +0 -10
- package/src/forms.ts +0 -110
- package/src/html.test.ts +0 -69
- package/src/html.ts +0 -345
package/package.json
CHANGED
|
@@ -1,51 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
6
|
"public": true,
|
|
8
7
|
"publishConfig": {
|
|
9
8
|
"access": "public"
|
|
10
9
|
},
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./assets": {
|
|
16
|
+
"types": "./dist/assets.d.ts",
|
|
17
|
+
"default": "./dist/assets.js"
|
|
18
|
+
}
|
|
20
19
|
},
|
|
20
|
+
"author": "Vance Lucas <vance@vancelucas.com>",
|
|
21
|
+
"license": "BSD-3-Clause",
|
|
21
22
|
"keywords": [
|
|
22
23
|
"framework",
|
|
23
24
|
"node",
|
|
24
25
|
"bun",
|
|
25
|
-
"web",
|
|
26
|
-
"framework",
|
|
26
|
+
"web framework",
|
|
27
27
|
"javascript",
|
|
28
|
-
"typescript"
|
|
28
|
+
"typescript",
|
|
29
|
+
"streaming",
|
|
30
|
+
"hypermedia"
|
|
29
31
|
],
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
+
"homepage": "https://www.hyperspan.dev",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/vlucas/hyperspan.git"
|
|
36
|
+
},
|
|
32
37
|
"bugs": {
|
|
33
|
-
"url": "https://github.com/vlucas/hyperspan
|
|
38
|
+
"url": "https://github.com/vlucas/hyperspan/issues"
|
|
34
39
|
},
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"isbot": "^5.1.17",
|
|
41
|
-
"trek-middleware": "^1.2.0",
|
|
42
|
-
"trek-router": "^1.2.0"
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "bun ./build.ts",
|
|
42
|
+
"clean": "rm -rf dist",
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"prepack": "npm run clean && npm run build"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
|
-
"@types/bun": "^1.1.
|
|
46
|
-
"@types/
|
|
47
|
-
"@types/
|
|
47
|
+
"@types/bun": "^1.1.9",
|
|
48
|
+
"@types/node": "^22.5.5",
|
|
49
|
+
"@types/react": "^19.1.0",
|
|
48
50
|
"bun-plugin-dts": "^0.3.0",
|
|
49
|
-
"
|
|
51
|
+
"bun-types": "latest",
|
|
52
|
+
"prettier": "^3.2.5"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"typescript": "^5.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@hyperspan/html": "^0.1.1",
|
|
59
|
+
"@preact/compat": "^18.3.1",
|
|
60
|
+
"hono": "^4.7.4",
|
|
61
|
+
"isbot": "^5.1.25",
|
|
62
|
+
"zod": "^4.0.0-beta.20250415T232143"
|
|
50
63
|
}
|
|
51
64
|
}
|
package/src/assets.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { html } from '@hyperspan/html';
|
|
2
|
+
import { md5 } from './clientjs/md5';
|
|
3
|
+
import { readdir } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const IS_PROD = process.env.NODE_ENV === 'production';
|
|
7
|
+
const PWD = import.meta.dir;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build client JS for end users (minimal JS for Hyperspan to work)
|
|
11
|
+
*/
|
|
12
|
+
export const clientJSFiles = new Map<string, { src: string; type?: string }>();
|
|
13
|
+
export async function buildClientJS() {
|
|
14
|
+
const sourceFile = resolve(PWD, '../', './hyperspan/clientjs/hyperspan-client.ts');
|
|
15
|
+
const output = await Bun.build({
|
|
16
|
+
entrypoints: [sourceFile],
|
|
17
|
+
outdir: `./public/_hs/js`,
|
|
18
|
+
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
19
|
+
minify: IS_PROD,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const jsFile = output.outputs[0].path.split('/').reverse()[0];
|
|
23
|
+
clientJSFiles.set('_hs', { src: '/_hs/js/' + jsFile });
|
|
24
|
+
return jsFile;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find client CSS file built for end users
|
|
29
|
+
* @TODO: Build this in code here vs. relying on tailwindcss CLI tool from package scripts
|
|
30
|
+
*/
|
|
31
|
+
export const clientCSSFiles = new Map<string, string>();
|
|
32
|
+
export async function buildClientCSS() {
|
|
33
|
+
if (clientCSSFiles.has('_hs')) {
|
|
34
|
+
return clientCSSFiles.get('_hs');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Find file already built from tailwindcss CLI
|
|
38
|
+
const cssDir = './public/_hs/css/';
|
|
39
|
+
const cssFiles = await readdir(cssDir);
|
|
40
|
+
let foundCSSFile: string = '';
|
|
41
|
+
|
|
42
|
+
for (const file of cssFiles) {
|
|
43
|
+
// Only looking for CSS files
|
|
44
|
+
if (!file.endsWith('.css')) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
foundCSSFile = file.replace(cssDir, '');
|
|
49
|
+
clientCSSFiles.set('_hs', foundCSSFile);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!foundCSSFile) {
|
|
54
|
+
console.log(`Unable to build CSS files from ${cssDir}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Output HTML style tag for Hyperspan app
|
|
60
|
+
*/
|
|
61
|
+
export function hyperspanStyleTags() {
|
|
62
|
+
const cssFiles = Array.from(clientCSSFiles.entries());
|
|
63
|
+
return html`${cssFiles.map(
|
|
64
|
+
([key, file]) => html`<link rel="stylesheet" href="/_hs/css/${file}" />`
|
|
65
|
+
)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Output HTML script tag for Hyperspan app
|
|
70
|
+
* Required for functioning streaming so content can pop into place properly once ready
|
|
71
|
+
*/
|
|
72
|
+
export function hyperspanScriptTags() {
|
|
73
|
+
const jsFiles = Array.from(clientJSFiles.entries());
|
|
74
|
+
return html`
|
|
75
|
+
<script type="importmap">
|
|
76
|
+
{
|
|
77
|
+
"imports": {
|
|
78
|
+
"preact": "https://esm.sh/preact@10.26.4",
|
|
79
|
+
"preact/": "https://esm.sh/preact@10.26.4/",
|
|
80
|
+
"react": "https://esm.sh/preact@10.26.4/compat",
|
|
81
|
+
"react/": "https://esm.sh/preact@10.26.4/compat/",
|
|
82
|
+
"react-dom": "https://esm.sh/preact@10.26.4/compat"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
${jsFiles.map(
|
|
87
|
+
([key, file]) =>
|
|
88
|
+
html`<script
|
|
89
|
+
id="js-${key}"
|
|
90
|
+
type="${file.type || 'text/javascript'}"
|
|
91
|
+
src="${file.src}"
|
|
92
|
+
></script>`
|
|
93
|
+
)}
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Return a Preact component, mounted as an island in a <script> tag so it can be embedded into the page response.
|
|
99
|
+
*/
|
|
100
|
+
export async function createPreactIsland(file: string) {
|
|
101
|
+
let filePath = file.replace('file://', '');
|
|
102
|
+
|
|
103
|
+
let resultStr = 'import{h,render}from"preact";';
|
|
104
|
+
const build = await Bun.build({
|
|
105
|
+
entrypoints: [filePath],
|
|
106
|
+
minify: true,
|
|
107
|
+
external: ['react', 'preact'],
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
env: 'APP_PUBLIC_*', // Inlines any ENV that starts with 'APP_PUBLIC_'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
for (const output of build.outputs) {
|
|
113
|
+
resultStr += await output.text(); // string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Find default export - this is our component
|
|
117
|
+
const r = /export\{([a-zA-Z]+) as default\}/g;
|
|
118
|
+
const matchExport = r.exec(resultStr);
|
|
119
|
+
const jsId = md5(resultStr);
|
|
120
|
+
|
|
121
|
+
if (!matchExport) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'File does not have a default export! Ensure a function has export default to use this.'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Preact render/mount component
|
|
128
|
+
const fn = matchExport[1];
|
|
129
|
+
let _mounted = false;
|
|
130
|
+
|
|
131
|
+
// Return HTML that will embed this component
|
|
132
|
+
return (props: any) => {
|
|
133
|
+
if (!_mounted) {
|
|
134
|
+
_mounted = true;
|
|
135
|
+
resultStr += `render(h(${fn}, ${JSON.stringify(props)}), document.getElementById("${jsId}"));`;
|
|
136
|
+
}
|
|
137
|
+
return html.raw(
|
|
138
|
+
`<div id="${jsId}"></div><script type="module" data-source-id="${jsId}">${resultStr}</script>`
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { html
|
|
2
|
-
import { Idiomorph } from './
|
|
1
|
+
import { html } from '../html';
|
|
2
|
+
import { Idiomorph } from './idiomorph.esm';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Used for streaming content from the server to the client.
|
|
6
|
+
*/
|
|
7
|
+
function htmlAsyncContentObserver() {
|
|
5
8
|
if (typeof MutationObserver != 'undefined') {
|
|
6
9
|
// Hyperspan - Async content loader
|
|
7
10
|
// Puts streamed content in its place immediately after it is added to the DOM
|
|
@@ -41,178 +44,7 @@ function setupAsyncContentObserver() {
|
|
|
41
44
|
asyncContentObserver.observe(document.body, { childList: true, subtree: true });
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Event binding for added/updated content
|
|
48
|
-
*/
|
|
49
|
-
function setupEventBindingObserver() {
|
|
50
|
-
if (typeof MutationObserver != 'undefined') {
|
|
51
|
-
const eventBindingObserver = new MutationObserver((list) => {
|
|
52
|
-
bindHyperspanEvents(document.body);
|
|
53
|
-
});
|
|
54
|
-
eventBindingObserver.observe(document.body, { childList: true, subtree: true });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
setupEventBindingObserver();
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Global Window assignments...
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
// @ts-ignore
|
|
64
|
-
const hyperspan: any = {
|
|
65
|
-
_fn: new Map(),
|
|
66
|
-
wc: new Map(),
|
|
67
|
-
compIdOrLast(id?: string) {
|
|
68
|
-
let comp = hyperspan.wc.get(id);
|
|
69
|
-
|
|
70
|
-
// Get last component if id lookup failed
|
|
71
|
-
if (!comp) {
|
|
72
|
-
const lastComp = Array.from(hyperspan.wc).pop();
|
|
73
|
-
// @ts-ignore - The value returned from a Map is a tuple. The second value (lastComp[1]) is the actual value
|
|
74
|
-
comp = lastComp ? lastComp[1] : false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return comp || false;
|
|
78
|
-
},
|
|
79
|
-
fn(id: string, ufn: any) {
|
|
80
|
-
const comp = this.compIdOrLast(id);
|
|
81
|
-
|
|
82
|
-
const fnd = {
|
|
83
|
-
id,
|
|
84
|
-
cid: comp ? comp.id : null,
|
|
85
|
-
fn: comp ? ufn.bind(comp) : ufn,
|
|
86
|
-
comp,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
this._fn.set(id, fnd);
|
|
90
|
-
},
|
|
91
|
-
// Binds function execution to the component instance so 'this' keyword works as expected inside event handlers
|
|
92
|
-
fnc(id: string, ...args: any[]) {
|
|
93
|
-
const fnd = this._fn.get(id);
|
|
94
|
-
|
|
95
|
-
if (!fnd) {
|
|
96
|
-
console.log('[Hyperspan] Unable to find function with id ' + id);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (fnd.comp) {
|
|
101
|
-
fnd.fn.call(fnd.comp, ...args);
|
|
102
|
-
} else {
|
|
103
|
-
fnd.fn(...args);
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Web component (foundation of client components)
|
|
110
|
-
*/
|
|
111
|
-
class HyperspanComponent extends HTMLElement {
|
|
112
|
-
constructor() {
|
|
113
|
-
super();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
static get observedAttributes() {
|
|
117
|
-
return ['data-state'];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
randomId() {
|
|
121
|
-
return Math.random().toString(36).substring(2, 9);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async render() {
|
|
125
|
-
let content = '<div>Loading...</div>';
|
|
126
|
-
|
|
127
|
-
const comp = hyperspan.wc.get(this.id);
|
|
128
|
-
|
|
129
|
-
if (comp) {
|
|
130
|
-
content = await renderToString(comp.render());
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
Idiomorph.morph(this, content, { morphStyle: 'innerHTML' });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
connectedCallback() {
|
|
137
|
-
const comp = hyperspan.wc.get(this.id);
|
|
138
|
-
|
|
139
|
-
if (comp) {
|
|
140
|
-
comp.mount && comp.mount();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
attributeChangedCallback() {
|
|
145
|
-
this.render();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Bind events
|
|
150
|
-
function bindHyperspanEvents(webComponentEl: HTMLElement) {
|
|
151
|
-
const domEvents = [
|
|
152
|
-
'click',
|
|
153
|
-
'dblclick',
|
|
154
|
-
'contextmenu',
|
|
155
|
-
'hover',
|
|
156
|
-
'focus',
|
|
157
|
-
'blur',
|
|
158
|
-
'mouseup',
|
|
159
|
-
'mousedown',
|
|
160
|
-
'touchstart',
|
|
161
|
-
'touchend',
|
|
162
|
-
'touchcancel',
|
|
163
|
-
'touchmove',
|
|
164
|
-
'submit',
|
|
165
|
-
'change',
|
|
166
|
-
'scroll',
|
|
167
|
-
'keyup',
|
|
168
|
-
'keydown',
|
|
169
|
-
];
|
|
170
|
-
const eventEls = Array.from(
|
|
171
|
-
webComponentEl.querySelectorAll('[on' + domEvents.join('], [on') + ']')
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
for (let i = 0; i < eventEls.length; i++) {
|
|
175
|
-
const el = eventEls[i] as HTMLElement;
|
|
176
|
-
const elEvents = el.getAttributeNames();
|
|
177
|
-
|
|
178
|
-
elEvents
|
|
179
|
-
.filter((ev) => ev.startsWith('on'))
|
|
180
|
-
.map((event) => {
|
|
181
|
-
const fnId = el.getAttribute(event)?.replace('hyperspan:', '');
|
|
182
|
-
|
|
183
|
-
if (fnId && el.dataset[event] !== fnId) {
|
|
184
|
-
const eventName = event.replace('on', '');
|
|
185
|
-
el.addEventListener(eventName, globalEventDispatch);
|
|
186
|
-
el.dataset[event] = fnId;
|
|
187
|
-
el.removeAttribute(event);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Proxies all events to the function they go to by event type
|
|
194
|
-
function globalEventDispatch(e: Event) {
|
|
195
|
-
let el = e.target as HTMLElement;
|
|
196
|
-
|
|
197
|
-
if (el) {
|
|
198
|
-
const dataName = 'on' + e.type;
|
|
199
|
-
let fnId = el.dataset[dataName];
|
|
200
|
-
|
|
201
|
-
if (!fnId) {
|
|
202
|
-
el = el.closest('[data-' + dataName + ']') || el;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
fnId = el.dataset[dataName];
|
|
206
|
-
|
|
207
|
-
if (fnId) {
|
|
208
|
-
hyperspan.fnc(fnId, e, el);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
customElements.define('hs-wc', HyperspanComponent);
|
|
47
|
+
htmlAsyncContentObserver();
|
|
214
48
|
|
|
215
|
-
// @ts-ignore
|
|
216
|
-
window.hyperspan = hyperspan;
|
|
217
49
|
// @ts-ignore
|
|
218
50
|
window.html = html;
|