@hyperspan/framework 0.4.0 → 0.4.2
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/dist/assets.js +37 -4
- package/dist/server.js +70 -3
- package/package.json +1 -1
- package/src/assets.ts +56 -3
- package/src/clientjs/hyperspan-client.ts +54 -26
- package/src/plugins.ts +90 -0
- package/src/server.ts +4 -3
package/dist/assets.js
CHANGED
|
@@ -7,18 +7,49 @@ import { readdir } from "node:fs/promises";
|
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
var IS_PROD = false;
|
|
9
9
|
var PWD = import.meta.dir;
|
|
10
|
+
var CLIENTJS_PUBLIC_PATH = "/_hs/js";
|
|
11
|
+
var ISLAND_PUBLIC_PATH = "/_hs/js/islands";
|
|
10
12
|
var clientImportMap = new Map;
|
|
11
13
|
var clientJSFiles = new Map;
|
|
12
14
|
async function buildClientJS() {
|
|
13
15
|
const sourceFile = resolve(PWD, "../", "./src/clientjs/hyperspan-client.ts");
|
|
14
16
|
const output = await Bun.build({
|
|
15
17
|
entrypoints: [sourceFile],
|
|
16
|
-
outdir: `./public
|
|
18
|
+
outdir: `./public/${CLIENTJS_PUBLIC_PATH}`,
|
|
17
19
|
naming: IS_PROD ? "[dir]/[name]-[hash].[ext]" : undefined,
|
|
18
20
|
minify: IS_PROD
|
|
19
21
|
});
|
|
20
22
|
const jsFile = output.outputs[0].path.split("/").reverse()[0];
|
|
21
|
-
clientJSFiles.set("_hs", { src:
|
|
23
|
+
clientJSFiles.set("_hs", { src: `${CLIENTJS_PUBLIC_PATH}/${jsFile}` });
|
|
24
|
+
}
|
|
25
|
+
function renderClientJS(module, onLoad) {
|
|
26
|
+
if (!module.__CLIENT_JS) {
|
|
27
|
+
throw new Error(`[Hyperspan] Client JS was not loaded by Hyperspan! Ensure the filename ends with .client.ts to use this render method.`);
|
|
28
|
+
}
|
|
29
|
+
return html.raw(module.__CLIENT_JS.renderScriptTag({
|
|
30
|
+
onLoad: onLoad ? functionToString(onLoad) : undefined
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
function functionToString(fn) {
|
|
34
|
+
let str = fn.toString().trim();
|
|
35
|
+
if (!str.includes("function ")) {
|
|
36
|
+
if (str.includes("async ")) {
|
|
37
|
+
str = "async function " + str.replace("async ", "");
|
|
38
|
+
} else {
|
|
39
|
+
str = "function " + str;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const lines = str.split(`
|
|
43
|
+
`);
|
|
44
|
+
const firstLine = lines[0];
|
|
45
|
+
const lastLine = lines[lines.length - 1];
|
|
46
|
+
if (!lastLine?.includes("}")) {
|
|
47
|
+
return str.replace("=> ", "{ return ") + "; }";
|
|
48
|
+
}
|
|
49
|
+
if (firstLine.includes("=>")) {
|
|
50
|
+
return str.replace("=> ", "");
|
|
51
|
+
}
|
|
52
|
+
return str;
|
|
22
53
|
}
|
|
23
54
|
var clientCSSFiles = new Map;
|
|
24
55
|
async function buildClientCSS() {
|
|
@@ -60,7 +91,6 @@ function hyperspanScriptTags() {
|
|
|
60
91
|
function assetHash(content) {
|
|
61
92
|
return createHash("md5").update(content).digest("hex");
|
|
62
93
|
}
|
|
63
|
-
var ISLAND_PUBLIC_PATH = "/_hs/js/islands";
|
|
64
94
|
var ISLAND_DEFAULTS = () => ({
|
|
65
95
|
ssr: true,
|
|
66
96
|
loading: undefined
|
|
@@ -73,8 +103,10 @@ function renderIsland(Component, props, options = ISLAND_DEFAULTS()) {
|
|
|
73
103
|
}
|
|
74
104
|
export {
|
|
75
105
|
renderIsland,
|
|
106
|
+
renderClientJS,
|
|
76
107
|
hyperspanStyleTags,
|
|
77
108
|
hyperspanScriptTags,
|
|
109
|
+
functionToString,
|
|
78
110
|
clientJSFiles,
|
|
79
111
|
clientImportMap,
|
|
80
112
|
clientCSSFiles,
|
|
@@ -82,6 +114,7 @@ export {
|
|
|
82
114
|
buildClientCSS,
|
|
83
115
|
assetHash,
|
|
84
116
|
ISLAND_PUBLIC_PATH,
|
|
85
|
-
ISLAND_DEFAULTS
|
|
117
|
+
ISLAND_DEFAULTS,
|
|
118
|
+
CLIENTJS_PUBLIC_PATH
|
|
86
119
|
};
|
|
87
120
|
|
package/dist/server.js
CHANGED
|
@@ -1,14 +1,81 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CLIENTJS_PUBLIC_PATH,
|
|
2
3
|
assetHash,
|
|
3
4
|
buildClientCSS,
|
|
4
|
-
buildClientJS
|
|
5
|
+
buildClientJS,
|
|
6
|
+
clientImportMap
|
|
5
7
|
} from "./assets.js";
|
|
6
8
|
import"./chunk-atw8cdg1.js";
|
|
7
9
|
|
|
10
|
+
// src/plugins.ts
|
|
11
|
+
var CLIENT_JS_CACHE = new Map;
|
|
12
|
+
var EXPORT_REGEX = /export\{(.*)\}/g;
|
|
13
|
+
async function clientJSPlugin(config) {
|
|
14
|
+
await Bun.plugin({
|
|
15
|
+
name: "Hyperspan Client JS Loader",
|
|
16
|
+
async setup(build) {
|
|
17
|
+
build.onLoad({ filter: /\.client\.ts$/ }, async (args) => {
|
|
18
|
+
const jsId = assetHash(args.path);
|
|
19
|
+
if (CLIENT_JS_CACHE.has(jsId)) {
|
|
20
|
+
return {
|
|
21
|
+
contents: CLIENT_JS_CACHE.get(jsId) || "",
|
|
22
|
+
loader: "js"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const result = await Bun.build({
|
|
26
|
+
entrypoints: [args.path],
|
|
27
|
+
outdir: `./public/${CLIENTJS_PUBLIC_PATH}`,
|
|
28
|
+
naming: IS_PROD ? "[dir]/[name]-[hash].[ext]" : undefined,
|
|
29
|
+
external: Array.from(clientImportMap.keys()),
|
|
30
|
+
minify: true,
|
|
31
|
+
format: "esm",
|
|
32
|
+
target: "browser",
|
|
33
|
+
env: "APP_PUBLIC_*"
|
|
34
|
+
});
|
|
35
|
+
const esmName = String(result.outputs[0].path.split("/").reverse()[0]).replace(".js", "");
|
|
36
|
+
clientImportMap.set(esmName, `${CLIENTJS_PUBLIC_PATH}/${esmName}.js`);
|
|
37
|
+
const contents = await result.outputs[0].text();
|
|
38
|
+
const exportLine = EXPORT_REGEX.exec(contents);
|
|
39
|
+
let exports = "{}";
|
|
40
|
+
if (exportLine) {
|
|
41
|
+
const exportName = exportLine[1];
|
|
42
|
+
exports = "{" + exportName.split(",").map((name) => name.trim().split(" as ")).map(([name, alias]) => `${alias === "default" ? "default as " + name : alias}`).join(", ") + "}";
|
|
43
|
+
}
|
|
44
|
+
const fnArgs = exports.replace(/(\w+)\s*as\s*(\w+)/g, "$1: $2");
|
|
45
|
+
const moduleCode = `// hyperspan:processed
|
|
46
|
+
import { functionToString } from '@hyperspan/framework/assets';
|
|
47
|
+
|
|
48
|
+
// Original file contents
|
|
49
|
+
${contents}
|
|
50
|
+
|
|
51
|
+
// hyperspan:client-js-plugin
|
|
52
|
+
export const __CLIENT_JS = {
|
|
53
|
+
id: "${jsId}",
|
|
54
|
+
esmName: "${esmName}",
|
|
55
|
+
sourceFile: "${args.path}",
|
|
56
|
+
outputFile: "${result.outputs[0].path}",
|
|
57
|
+
renderScriptTag: ({ onLoad }) => {
|
|
58
|
+
const fn = onLoad ? functionToString(onLoad) : undefined;
|
|
59
|
+
return \`<script type="module" data-source-id="${jsId}">import ${exports} from "${esmName}";
|
|
60
|
+
\${fn ? \`const fn = \${fn};
|
|
61
|
+
fn(${fnArgs});\` : ''}</script>\`;
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
CLIENT_JS_CACHE.set(jsId, moduleCode);
|
|
66
|
+
return {
|
|
67
|
+
contents: moduleCode,
|
|
68
|
+
loader: "js"
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
8
75
|
// src/server.ts
|
|
76
|
+
import { html, isHSHtml, renderStream, renderAsync, render } from "@hyperspan/html";
|
|
9
77
|
import { readdir } from "node:fs/promises";
|
|
10
78
|
import { basename, extname, join } from "node:path";
|
|
11
|
-
import { html, isHSHtml, renderStream, renderAsync, render } from "@hyperspan/html";
|
|
12
79
|
|
|
13
80
|
// ../../node_modules/isbot/index.mjs
|
|
14
81
|
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/";
|
|
@@ -2124,7 +2191,7 @@ function createRouteFromModule(RouteModule) {
|
|
|
2124
2191
|
return route._getRouteHandlers();
|
|
2125
2192
|
}
|
|
2126
2193
|
async function createServer(config) {
|
|
2127
|
-
await Promise.all([buildClientJS(), buildClientCSS()]);
|
|
2194
|
+
await Promise.all([buildClientJS(), buildClientCSS(), clientJSPlugin(config)]);
|
|
2128
2195
|
const app = new Hono2;
|
|
2129
2196
|
config.beforeRoutesAdded && config.beforeRoutesAdded(app);
|
|
2130
2197
|
const [routes, actions] = await Promise.all([buildRoutes(config), buildActions(config)]);
|
package/package.json
CHANGED
package/src/assets.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type THSIslandOptions = {
|
|
|
11
11
|
const IS_PROD = process.env.NODE_ENV === 'production';
|
|
12
12
|
const PWD = import.meta.dir;
|
|
13
13
|
|
|
14
|
+
export const CLIENTJS_PUBLIC_PATH = '/_hs/js';
|
|
15
|
+
export const ISLAND_PUBLIC_PATH = '/_hs/js/islands';
|
|
14
16
|
export const clientImportMap = new Map<string, string>();
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -21,14 +23,66 @@ export async function buildClientJS() {
|
|
|
21
23
|
const sourceFile = resolve(PWD, '../', './src/clientjs/hyperspan-client.ts');
|
|
22
24
|
const output = await Bun.build({
|
|
23
25
|
entrypoints: [sourceFile],
|
|
24
|
-
outdir: `./public
|
|
26
|
+
outdir: `./public/${CLIENTJS_PUBLIC_PATH}`,
|
|
25
27
|
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
26
28
|
minify: IS_PROD,
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
const jsFile = output.outputs[0].path.split('/').reverse()[0];
|
|
30
32
|
|
|
31
|
-
clientJSFiles.set('_hs', { src:
|
|
33
|
+
clientJSFiles.set('_hs', { src: `${CLIENTJS_PUBLIC_PATH}/${jsFile}` });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render a client JS module as a script tag
|
|
38
|
+
*/
|
|
39
|
+
export function renderClientJS<T>(module: T, onLoad?: (module: T) => void) {
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
if (!module.__CLIENT_JS) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[Hyperspan] Client JS was not loaded by Hyperspan! Ensure the filename ends with .client.ts to use this render method.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return html.raw(
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
module.__CLIENT_JS.renderScriptTag({
|
|
50
|
+
onLoad: onLoad ? functionToString(onLoad) : undefined,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert a function to a string (results in loss of context!)
|
|
57
|
+
* Handles named, async, and arrow functions
|
|
58
|
+
*/
|
|
59
|
+
export function functionToString(fn: any) {
|
|
60
|
+
let str = fn.toString().trim();
|
|
61
|
+
|
|
62
|
+
// Ensure consistent output & handle async
|
|
63
|
+
if (!str.includes('function ')) {
|
|
64
|
+
if (str.includes('async ')) {
|
|
65
|
+
str = 'async function ' + str.replace('async ', '');
|
|
66
|
+
} else {
|
|
67
|
+
str = 'function ' + str;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = str.split('\n');
|
|
72
|
+
const firstLine = lines[0];
|
|
73
|
+
const lastLine = lines[lines.length - 1];
|
|
74
|
+
|
|
75
|
+
// Arrow function conversion
|
|
76
|
+
if (!lastLine?.includes('}')) {
|
|
77
|
+
return str.replace('=> ', '{ return ') + '; }';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cleanup arrow function
|
|
81
|
+
if (firstLine.includes('=>')) {
|
|
82
|
+
return str.replace('=> ', '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return str;
|
|
32
86
|
}
|
|
33
87
|
|
|
34
88
|
/**
|
|
@@ -101,7 +155,6 @@ export function assetHash(content: string): string {
|
|
|
101
155
|
/**
|
|
102
156
|
* Island defaults
|
|
103
157
|
*/
|
|
104
|
-
export const ISLAND_PUBLIC_PATH = '/_hs/js/islands';
|
|
105
158
|
export const ISLAND_DEFAULTS: () => THSIslandOptions = () => ({
|
|
106
159
|
ssr: true,
|
|
107
160
|
loading: undefined,
|
|
@@ -12,28 +12,46 @@ function htmlAsyncContentObserver() {
|
|
|
12
12
|
const asyncContent = list
|
|
13
13
|
.map((mutation) =>
|
|
14
14
|
Array.from(mutation.addedNodes).find((node: any) => {
|
|
15
|
-
|
|
15
|
+
if (!node || !node?.id) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return node.id?.startsWith('async_loading_') && node.id?.endsWith('_content');
|
|
16
19
|
})
|
|
17
20
|
)
|
|
18
21
|
.filter((node: any) => node);
|
|
19
22
|
|
|
20
|
-
asyncContent.forEach((
|
|
23
|
+
asyncContent.forEach((templateEl: any) => {
|
|
21
24
|
try {
|
|
22
|
-
// Also observe
|
|
23
|
-
asyncContentObserver.observe(
|
|
25
|
+
// Also observe for content inside the template content (shadow DOM is separate)
|
|
26
|
+
asyncContentObserver.observe(templateEl.content, { childList: true, subtree: true });
|
|
24
27
|
|
|
25
|
-
const slotId =
|
|
28
|
+
const slotId = templateEl.id.replace('_content', '');
|
|
26
29
|
const slotEl = document.getElementById(slotId);
|
|
27
30
|
|
|
28
31
|
if (slotEl) {
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// Content AND slot are present - let's insert the content into the slot
|
|
33
|
+
// Ensure the content is fully done streaming in before inserting it into the slot
|
|
34
|
+
waitForContent(templateEl.content, (el2) => {
|
|
35
|
+
return Array.from(el2.childNodes).find(
|
|
36
|
+
(node) => node.nodeType === Node.COMMENT_NODE && node.nodeValue === 'end'
|
|
37
|
+
);
|
|
38
|
+
})
|
|
39
|
+
.then((endComment) => {
|
|
40
|
+
templateEl.content.removeChild(endComment);
|
|
41
|
+
const content = templateEl.content.cloneNode(true);
|
|
42
|
+
Idiomorph.morph(slotEl, content);
|
|
43
|
+
templateEl.parentNode.removeChild(templateEl);
|
|
44
|
+
lazyLoadScripts();
|
|
45
|
+
})
|
|
46
|
+
.catch(console.error);
|
|
47
|
+
} else {
|
|
48
|
+
// Slot is NOT present - wait for it to be added to the DOM so we can insert the content into it
|
|
49
|
+
waitForContent(document.body, () => {
|
|
50
|
+
return document.getElementById(slotId);
|
|
51
|
+
}).then((slotEl) => {
|
|
52
|
+
Idiomorph.morph(slotEl, templateEl.content.cloneNode(true));
|
|
53
|
+
lazyLoadScripts();
|
|
33
54
|
});
|
|
34
|
-
|
|
35
|
-
// Lazy load scripts (if any) after the content is inserted
|
|
36
|
-
lazyLoadScripts();
|
|
37
55
|
}
|
|
38
56
|
} catch (e) {
|
|
39
57
|
console.error(e);
|
|
@@ -49,18 +67,29 @@ htmlAsyncContentObserver();
|
|
|
49
67
|
* Wait until ALL of the content inside an element is present from streaming in.
|
|
50
68
|
* Large chunks of content can sometimes take more than a single tick to write to DOM.
|
|
51
69
|
*/
|
|
52
|
-
async function
|
|
53
|
-
|
|
70
|
+
async function waitForContent(
|
|
71
|
+
el: HTMLElement,
|
|
72
|
+
waitFn: (
|
|
73
|
+
node: HTMLElement
|
|
74
|
+
) => HTMLElement | HTMLTemplateElement | Node | ChildNode | null | undefined,
|
|
75
|
+
options: { timeoutMs?: number; intervalMs?: number } = { timeoutMs: 10000, intervalMs: 20 }
|
|
76
|
+
): Promise<HTMLElement | HTMLTemplateElement | Node | ChildNode | null | undefined> {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
let timeout: NodeJS.Timeout;
|
|
54
79
|
const interval = setInterval(() => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
const content = waitFn(el);
|
|
81
|
+
if (content) {
|
|
82
|
+
if (timeout) {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
}
|
|
60
85
|
clearInterval(interval);
|
|
61
|
-
resolve(
|
|
86
|
+
resolve(content);
|
|
62
87
|
}
|
|
63
|
-
},
|
|
88
|
+
}, options.intervalMs || 20);
|
|
89
|
+
timeout = setTimeout(() => {
|
|
90
|
+
clearInterval(interval);
|
|
91
|
+
reject(new Error(`[Hyperspan] Timeout waiting for end of streaming content ${el.id}`));
|
|
92
|
+
}, options.timeoutMs || 10000);
|
|
64
93
|
});
|
|
65
94
|
}
|
|
66
95
|
|
|
@@ -73,10 +102,8 @@ class HSAction extends HTMLElement {
|
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
connectedCallback() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
actionFormObserver.observe(this, { childList: true, subtree: true });
|
|
79
|
-
}, 10);
|
|
105
|
+
actionFormObserver.observe(this, { childList: true, subtree: true });
|
|
106
|
+
bindHSActionForm(this, this.querySelector('form') as HTMLFormElement);
|
|
80
107
|
}
|
|
81
108
|
}
|
|
82
109
|
window.customElements.define('hs-action', HSAction);
|
|
@@ -94,7 +121,7 @@ const actionFormObserver = new MutationObserver((list) => {
|
|
|
94
121
|
* Bind the form inside an hs-action element to the action URL and submit handler
|
|
95
122
|
*/
|
|
96
123
|
function bindHSActionForm(hsActionElement: HSAction, form: HTMLFormElement) {
|
|
97
|
-
if (!form) {
|
|
124
|
+
if (!hsActionElement || !form) {
|
|
98
125
|
return;
|
|
99
126
|
}
|
|
100
127
|
|
|
@@ -153,6 +180,7 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
153
180
|
|
|
154
181
|
Idiomorph.morph(target, content);
|
|
155
182
|
opts.afterResponse && opts.afterResponse();
|
|
183
|
+
lazyLoadScripts();
|
|
156
184
|
});
|
|
157
185
|
}
|
|
158
186
|
|
package/src/plugins.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { assetHash, CLIENTJS_PUBLIC_PATH, clientImportMap, clientJSFiles } from './assets';
|
|
2
|
+
import { IS_PROD, type THSServerConfig } from './server';
|
|
3
|
+
|
|
4
|
+
const CLIENT_JS_CACHE = new Map<string, string>();
|
|
5
|
+
const EXPORT_REGEX = /export\{(.*)\}/g;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hyperspan Client JS Plugin
|
|
9
|
+
*/
|
|
10
|
+
export async function clientJSPlugin(config: THSServerConfig) {
|
|
11
|
+
// Define a Bun plugin to handle .client.ts files
|
|
12
|
+
await Bun.plugin({
|
|
13
|
+
name: 'Hyperspan Client JS Loader',
|
|
14
|
+
async setup(build) {
|
|
15
|
+
// when a .client.ts file is imported...
|
|
16
|
+
build.onLoad({ filter: /\.client\.ts$/ }, async (args) => {
|
|
17
|
+
const jsId = assetHash(args.path);
|
|
18
|
+
|
|
19
|
+
// Cache: Avoid re-processing the same file
|
|
20
|
+
if (CLIENT_JS_CACHE.has(jsId)) {
|
|
21
|
+
return {
|
|
22
|
+
contents: CLIENT_JS_CACHE.get(jsId) || '',
|
|
23
|
+
loader: 'js',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// We need to build the file to ensure we can ship it to the client with dependencies
|
|
28
|
+
// Ironic, right? Calling Bun.build() inside of a plugin that runs on Bun.build()?
|
|
29
|
+
const result = await Bun.build({
|
|
30
|
+
entrypoints: [args.path],
|
|
31
|
+
outdir: `./public/${CLIENTJS_PUBLIC_PATH}`,
|
|
32
|
+
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
33
|
+
external: Array.from(clientImportMap.keys()),
|
|
34
|
+
minify: true,
|
|
35
|
+
format: 'esm',
|
|
36
|
+
target: 'browser',
|
|
37
|
+
env: 'APP_PUBLIC_*',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Add output file to import map
|
|
41
|
+
const esmName = String(result.outputs[0].path.split('/').reverse()[0]).replace('.js', '');
|
|
42
|
+
clientImportMap.set(esmName, `${CLIENTJS_PUBLIC_PATH}/${esmName}.js`);
|
|
43
|
+
|
|
44
|
+
const contents = await result.outputs[0].text();
|
|
45
|
+
const exportLine = EXPORT_REGEX.exec(contents);
|
|
46
|
+
|
|
47
|
+
let exports = '{}';
|
|
48
|
+
if (exportLine) {
|
|
49
|
+
const exportName = exportLine[1];
|
|
50
|
+
exports =
|
|
51
|
+
'{' +
|
|
52
|
+
exportName
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((name) => name.trim().split(' as '))
|
|
55
|
+
.map(([name, alias]) => `${alias === 'default' ? 'default as ' + name : alias}`)
|
|
56
|
+
.join(', ') +
|
|
57
|
+
'}';
|
|
58
|
+
}
|
|
59
|
+
const fnArgs = exports.replace(/(\w+)\s*as\s*(\w+)/g, '$1: $2');
|
|
60
|
+
|
|
61
|
+
// Export a special object that can be used to render the client JS as a script tag
|
|
62
|
+
const moduleCode = `// hyperspan:processed
|
|
63
|
+
import { functionToString } from '@hyperspan/framework/assets';
|
|
64
|
+
|
|
65
|
+
// Original file contents
|
|
66
|
+
${contents}
|
|
67
|
+
|
|
68
|
+
// hyperspan:client-js-plugin
|
|
69
|
+
export const __CLIENT_JS = {
|
|
70
|
+
id: "${jsId}",
|
|
71
|
+
esmName: "${esmName}",
|
|
72
|
+
sourceFile: "${args.path}",
|
|
73
|
+
outputFile: "${result.outputs[0].path}",
|
|
74
|
+
renderScriptTag: ({ onLoad }) => {
|
|
75
|
+
const fn = onLoad ? functionToString(onLoad) : undefined;
|
|
76
|
+
return \`<script type="module" data-source-id="${jsId}">import ${exports} from "${esmName}";\n\${fn ? \`const fn = \${fn};\nfn(${fnArgs});\` : ''}</script>\`;
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
CLIENT_JS_CACHE.set(jsId, moduleCode);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
contents: moduleCode,
|
|
85
|
+
loader: 'js',
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { buildClientJS, buildClientCSS, assetHash } from './assets';
|
|
2
|
+
import { clientJSPlugin } from './plugins';
|
|
3
|
+
import { HSHtml, html, isHSHtml, renderStream, renderAsync, render } from '@hyperspan/html';
|
|
1
4
|
import { readdir } from 'node:fs/promises';
|
|
2
5
|
import { basename, extname, join } from 'node:path';
|
|
3
|
-
import { HSHtml, html, isHSHtml, renderStream, renderAsync, render } from '@hyperspan/html';
|
|
4
6
|
import { isbot } from 'isbot';
|
|
5
|
-
import { buildClientJS, buildClientCSS, assetHash } from './assets';
|
|
6
7
|
import { Hono, type Context } from 'hono';
|
|
7
8
|
import { serveStatic } from 'hono/bun';
|
|
8
9
|
import { HTTPException } from 'hono/http-exception';
|
|
@@ -504,7 +505,7 @@ export function createRouteFromModule(
|
|
|
504
505
|
*/
|
|
505
506
|
export async function createServer(config: THSServerConfig): Promise<Hono> {
|
|
506
507
|
// Build client JS and CSS bundles so they are available for templates when streaming starts
|
|
507
|
-
await Promise.all([buildClientJS(), buildClientCSS()]);
|
|
508
|
+
await Promise.all([buildClientJS(), buildClientCSS(), clientJSPlugin(config)]);
|
|
508
509
|
|
|
509
510
|
const app = new Hono();
|
|
510
511
|
|