@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 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/_hs/js`,
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: "/_hs/js/" + jsFile });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "dist/server.ts",
6
6
  "types": "src/server.ts",
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/_hs/js`,
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: '/_hs/js/' + jsFile });
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
- return node.id?.startsWith('async_') && node.id?.endsWith('_content');
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((el: any) => {
23
+ asyncContent.forEach((templateEl: any) => {
21
24
  try {
22
- // Also observe child nodes for nested async content
23
- asyncContentObserver.observe(el.content, { childList: true, subtree: true });
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 = el.id.replace('_content', '');
28
+ const slotId = templateEl.id.replace('_content', '');
26
29
  const slotEl = document.getElementById(slotId);
27
30
 
28
31
  if (slotEl) {
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);
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 waitForEndContent(el: HTMLElement) {
53
- return new Promise((resolve) => {
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 endComment = Array.from(el.childNodes).find((node) => {
56
- return node.nodeType === Node.COMMENT_NODE && node.nodeValue === 'end';
57
- });
58
- if (endComment) {
59
- el.removeChild(endComment);
80
+ const content = waitFn(el);
81
+ if (content) {
82
+ if (timeout) {
83
+ clearTimeout(timeout);
84
+ }
60
85
  clearInterval(interval);
61
- resolve(true);
86
+ resolve(content);
62
87
  }
63
- }, 10);
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
- setTimeout(() => {
77
- bindHSActionForm(this, this.querySelector('form') as HTMLFormElement);
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