@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/package.json CHANGED
@@ -1,51 +1,64 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.0.3",
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
- "scripts": {
12
- "build": "bun ./build.ts",
13
- "clean": "rm -rf dist",
14
- "test": "bun test",
15
- "prepack": "npm run clean && npm run build"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/vlucas/hyperspan-framework"
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
- "author": "Vance Lucas <vance@vancelucas.com>",
31
- "license": "BSD-3-Clause",
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-framework/issues"
38
+ "url": "https://github.com/vlucas/hyperspan/issues"
34
39
  },
35
- "homepage": "https://www.hyperspan.dev",
36
- "dependencies": {
37
- "@fastify/deepmerge": "^2.0.0",
38
- "@mjackson/headers": "^0.7.2",
39
- "escape-html": "^1.0.3",
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.11",
46
- "@types/escape-html": "^1.0.4",
47
- "@types/node": "^22.7.5",
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
- "typescript": "^5.6.3"
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, renderToString } from '../html';
2
- import { Idiomorph } from './idomorph.esm';
1
+ import { html } from '../html';
2
+ import { Idiomorph } from './idiomorph.esm';
3
3
 
4
- function setupAsyncContentObserver() {
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
- setupAsyncContentObserver();
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;