@hyperspan/framework 0.1.6 → 0.1.8

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 CHANGED
@@ -1,4 +1,4 @@
1
- import {build} from 'bun';
1
+ import { build } from 'bun';
2
2
 
3
3
  const entrypoints = ['./src/server.ts', './src/assets.ts'];
4
4
  const external = ['@hyperspan/html'];
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
+ await 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,7 +6,7 @@ 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 { TmplHtml, html, renderStream, renderAsync, render } from "@hyperspan/html";
9
+ import { html, isHSHtml, renderStream, renderAsync, render } from "@hyperspan/html";
10
10
 
11
11
  // node_modules/isbot/index.mjs
12
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/|^{{.*}}$|adscanner/|analyzer|archive|ask jeeves/teoma|audit|bit\\.ly/|bluecoat drtr|browsex|burpcollaborator|capture|catch|check\\b|checker|chrome-lighthouse|chromeframe|classifier|cloudflare|convertify|cookiehubscan|crawl|cypress/|dareboost|datanyze|dejaclick|detect|dmbrowser|download|evc-batch/|exaleadcloudview|feed|firephp|functionize|gomezagent|headless|httrack|hubspot marketing grader|hydra|ibisbrowser|infrawatch|insight|inspect|iplabel|ips-agent|java(?!;)|jsjcw_scanner|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|scanner\\.|scrape|server|sogou|sparkler/|speedcurve|spider|splash|statuscake|supercleaner|synapse|synthetic|tools|torrent|transcoder|url|validator|virtuoso|wappalyzer|webglance|webkit2png|whatcms/|zgrab";
@@ -1862,7 +1862,7 @@ function createRoute(handler) {
1862
1862
  const streamOpt = context.req.query("__nostream");
1863
1863
  const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1864
1864
  const routeKind = typeof routeContent;
1865
- if (routeContent && routeKind === "object" && (routeContent instanceof TmplHtml || routeContent.constructor.name === "TmplHtml" || routeContent?._kind === "TmplHtml")) {
1865
+ if (isHSHtml(routeContent)) {
1866
1866
  if (streamingEnabled) {
1867
1867
  return new StreamResponse(renderStream(routeContent));
1868
1868
  } else {
@@ -1946,6 +1946,9 @@ function getRunnableRoute(route) {
1946
1946
  function isRunnableRoute(route) {
1947
1947
  return typeof route === "object" && "run" in route;
1948
1948
  }
1949
+ function createLayout(layout) {
1950
+ return layout;
1951
+ }
1949
1952
  async function showErrorReponse(context, err) {
1950
1953
  const output = render(html`
1951
1954
  <main>
@@ -1963,7 +1966,6 @@ async function showErrorReponse(context, err) {
1963
1966
  var ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
1964
1967
  async function buildRoutes(config) {
1965
1968
  const routesDir = join(config.appDir, "routes");
1966
- console.log(routesDir);
1967
1969
  const files = await readdir(routesDir, { recursive: true });
1968
1970
  const routes = [];
1969
1971
  for (const file of files) {
@@ -2090,6 +2092,7 @@ export {
2090
2092
  createRouteFromModule,
2091
2093
  createRoute,
2092
2094
  createReadableStreamFromAsyncGenerator,
2095
+ createLayout,
2093
2096
  createAPIRoute,
2094
2097
  buildRoutes,
2095
2098
  StreamResponse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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.5",
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
  }
@@ -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 TmplHtml } from '@hyperspan/html';
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 TmplHtml);
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 TmplHtml);
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 TmplHtml);
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, TmplHtml } from '@hyperspan/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> }) => TmplHtml): HSAction<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
+ await 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
- // Wait until next paint for streaming content to finish writing to DOM
30
- // @TODO: Need a more guaranteed way to know HTML element is done streaming in...
31
- // Maybe some ".end" element that is hidden and then removed before insertion?
32
- requestAnimationFrame(() => {
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
  */
@@ -1 +1,2 @@
1
- export * from '@preact/compat';
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 { TmplHtml, html, renderStream, renderAsync, render } from '@hyperspan/html';
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,8 +13,10 @@ const CWD = process.cwd();
13
13
  /**
14
14
  * Types
15
15
  */
16
- export type THSResponseTypes = TmplHtml | Response | string | null;
16
+ export type THSResponseTypes = HSHtml | Response | string | null;
17
17
  export type THSRouteHandler = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
18
+ export type THSAPIResponseTypes = Response | Record<any, any> | void;
19
+ export type THSAPIRouteHandler = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
18
20
 
19
21
  export type THSRoute = {
20
22
  _kind: 'hsRoute';
@@ -28,7 +30,7 @@ export type THSRoute = {
28
30
 
29
31
  /**
30
32
  * Define a route that can handle a direct HTTP request.
31
- * Route handlers should return a TmplHtml or Response object
33
+ * Route handlers should return a HSHtml or Response object
32
34
  */
33
35
  export function createRoute(handler?: THSRouteHandler): THSRoute {
34
36
  let _handlers: Record<string, THSRouteHandler> = {};
@@ -78,19 +80,12 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
78
80
  const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
79
81
  const routeKind = typeof routeContent;
80
82
 
81
- // Render TmplHtml if returned from route handler
82
- if (
83
- routeContent &&
84
- routeKind === 'object' &&
85
- (routeContent instanceof TmplHtml ||
86
- routeContent.constructor.name === 'TmplHtml' ||
87
- // @ts-ignore
88
- routeContent?._kind === 'TmplHtml')
89
- ) {
83
+ // Render HSHtml if returned from route handler
84
+ if (isHSHtml(routeContent)) {
90
85
  if (streamingEnabled) {
91
- return new StreamResponse(renderStream(routeContent as TmplHtml)) as Response;
86
+ return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
92
87
  } else {
93
- const output = await renderAsync(routeContent as TmplHtml);
88
+ const output = await renderAsync(routeContent as HSHtml);
94
89
  return context.html(output);
95
90
  }
96
91
  }
@@ -107,8 +102,8 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
107
102
  * Create new API Route
108
103
  * API Route handlers should return a JSON object or a Response
109
104
  */
110
- export function createAPIRoute(handler?: THSRouteHandler): THSRoute {
111
- let _handlers: Record<string, THSRouteHandler> = {};
105
+ export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
106
+ let _handlers: Record<string, THSAPIRouteHandler> = {};
112
107
 
113
108
  if (handler) {
114
109
  _handlers['GET'] = handler;
@@ -116,23 +111,23 @@ export function createAPIRoute(handler?: THSRouteHandler): THSRoute {
116
111
 
117
112
  const api: THSRoute = {
118
113
  _kind: 'hsRoute',
119
- get(handler: THSRouteHandler) {
114
+ get(handler: THSAPIRouteHandler) {
120
115
  _handlers['GET'] = handler;
121
116
  return api;
122
117
  },
123
- post(handler: THSRouteHandler) {
118
+ post(handler: THSAPIRouteHandler) {
124
119
  _handlers['POST'] = handler;
125
120
  return api;
126
121
  },
127
- put(handler: THSRouteHandler) {
122
+ put(handler: THSAPIRouteHandler) {
128
123
  _handlers['PUT'] = handler;
129
124
  return api;
130
125
  },
131
- delete(handler: THSRouteHandler) {
126
+ delete(handler: THSAPIRouteHandler) {
132
127
  _handlers['DELETE'] = handler;
133
128
  return api;
134
129
  },
135
- patch(handler: THSRouteHandler) {
130
+ patch(handler: THSAPIRouteHandler) {
136
131
  _handlers['PATCH'] = handler;
137
132
  return api;
138
133
  },
@@ -209,6 +204,14 @@ export function isRunnableRoute(route: unknown): boolean {
209
204
  return typeof route === 'object' && 'run' in route;
210
205
  }
211
206
 
207
+ /**
208
+ * Create a layout for a Hyperspan app. Passthrough for now.
209
+ * Future intent is to be able to conditionally render a layout for full page content vs. partial content.
210
+ */
211
+ export function createLayout<T>(layout: (props: T) => HSHtml | Promise<HSHtml>) {
212
+ return layout;
213
+ }
214
+
212
215
  /**
213
216
  * Basic error handling
214
217
  * @TODO: Should check for and load user-customizeable template with special name (app/__error.ts ?)
@@ -249,7 +252,6 @@ const ROUTE_SEGMENT = /(\[[a-zA-Z_\.]+\])/g;
249
252
  export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[]> {
250
253
  // Walk all pages and add them as routes
251
254
  const routesDir = join(config.appDir, 'routes');
252
- console.log(routesDir);
253
255
  const files = await readdir(routesDir, { recursive: true });
254
256
  const routes: THSRouteMap[] = [];
255
257