@hyperspan/framework 0.2.0 → 0.3.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/build.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { build } from 'bun';
2
2
 
3
3
  const entrypoints = ['./src/server.ts', './src/assets.ts'];
4
- const external = ['@hyperspan/html', 'preact', 'preact-render-to-string'];
4
+ const external = ['@hyperspan/html'];
5
5
  const outdir = './dist';
6
6
  const target = 'node';
7
7
  const splitting = true;
package/dist/server.js CHANGED
@@ -1833,6 +1833,7 @@ function createConfig(config) {
1833
1833
  }
1834
1834
  function createRoute(handler) {
1835
1835
  let _handlers = {};
1836
+ let _middleware = [];
1836
1837
  if (handler) {
1837
1838
  _handlers["GET"] = handler;
1838
1839
  }
@@ -1858,34 +1859,52 @@ function createRoute(handler) {
1858
1859
  _handlers["PATCH"] = handler2;
1859
1860
  return api;
1860
1861
  },
1861
- async run(method, context) {
1862
- const handler2 = _handlers[method];
1863
- if (!handler2) {
1864
- throw new HTTPException(405, { message: "Method not allowed" });
1865
- }
1866
- const routeContent = await handler2(context);
1867
- if (routeContent instanceof Response) {
1868
- return routeContent;
1869
- }
1870
- const userIsBot = isbot(context.req.header("User-Agent"));
1871
- const streamOpt = context.req.query("__nostream");
1872
- const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1873
- const routeKind = typeof routeContent;
1874
- if (isHSHtml(routeContent)) {
1875
- if (streamingEnabled) {
1876
- return new StreamResponse(renderStream(routeContent));
1877
- } else {
1878
- const output = await renderAsync(routeContent);
1879
- return context.html(output);
1862
+ middleware(middleware) {
1863
+ _middleware = middleware;
1864
+ return api;
1865
+ },
1866
+ _getRouteHandlers() {
1867
+ return [
1868
+ ..._middleware,
1869
+ async (context) => {
1870
+ const method = context.req.method.toUpperCase();
1871
+ try {
1872
+ const handler2 = _handlers[method];
1873
+ if (!handler2) {
1874
+ throw new HTTPException(405, { message: "Method not allowed" });
1875
+ }
1876
+ const routeContent = await handler2(context);
1877
+ if (routeContent instanceof Response) {
1878
+ return routeContent;
1879
+ }
1880
+ const userIsBot = isbot(context.req.header("User-Agent"));
1881
+ const streamOpt = context.req.query("__nostream");
1882
+ const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
1883
+ if (isHSHtml(routeContent)) {
1884
+ if (streamingEnabled) {
1885
+ return new StreamResponse(renderStream(routeContent));
1886
+ } else {
1887
+ const output = await renderAsync(routeContent);
1888
+ return context.html(output);
1889
+ }
1890
+ }
1891
+ if (routeContent instanceof Response) {
1892
+ return routeContent;
1893
+ }
1894
+ return context.text(String(routeContent));
1895
+ } catch (e) {
1896
+ !IS_PROD && console.error(e);
1897
+ return await showErrorReponse(context, e);
1898
+ }
1880
1899
  }
1881
- }
1882
- return context.text(String(routeContent));
1900
+ ];
1883
1901
  }
1884
1902
  };
1885
1903
  return api;
1886
1904
  }
1887
1905
  function createAPIRoute(handler) {
1888
1906
  let _handlers = {};
1907
+ let _middleware = [];
1889
1908
  if (handler) {
1890
1909
  _handlers["GET"] = handler;
1891
1910
  }
@@ -1911,30 +1930,46 @@ function createAPIRoute(handler) {
1911
1930
  _handlers["PATCH"] = handler2;
1912
1931
  return api;
1913
1932
  },
1914
- async run(method, context) {
1915
- const handler2 = _handlers[method];
1916
- if (!handler2) {
1917
- throw new Error("Method not allowed");
1918
- }
1919
- try {
1920
- const response = await handler2(context);
1921
- if (response instanceof Response) {
1922
- return response;
1923
- }
1924
- return context.json({ meta: { success: true, dtResponse: new Date }, data: response }, { status: 200 });
1925
- } catch (err) {
1926
- const e = err;
1927
- console.error(e);
1928
- return context.json({
1929
- meta: { success: false, dtResponse: new Date },
1930
- data: {},
1931
- error: {
1932
- message: e.message,
1933
- stack: IS_PROD ? undefined : e.stack?.split(`
1933
+ middleware(middleware) {
1934
+ _middleware = middleware;
1935
+ return api;
1936
+ },
1937
+ _getRouteHandlers() {
1938
+ return [
1939
+ ..._middleware,
1940
+ async (context) => {
1941
+ const method = context.req.method.toUpperCase();
1942
+ const handler2 = _handlers[method];
1943
+ if (!handler2) {
1944
+ return context.json({
1945
+ meta: { success: false, dtResponse: new Date },
1946
+ data: {},
1947
+ error: {
1948
+ message: "Method not allowed"
1949
+ }
1950
+ }, { status: 405 });
1951
+ }
1952
+ try {
1953
+ const response = await handler2(context);
1954
+ if (response instanceof Response) {
1955
+ return response;
1956
+ }
1957
+ return context.json({ meta: { success: true, dtResponse: new Date }, data: response }, { status: 200 });
1958
+ } catch (err) {
1959
+ const e = err;
1960
+ !IS_PROD && console.error(e);
1961
+ return context.json({
1962
+ meta: { success: false, dtResponse: new Date },
1963
+ data: {},
1964
+ error: {
1965
+ message: e.message,
1966
+ stack: IS_PROD ? undefined : e.stack?.split(`
1934
1967
  `)
1968
+ }
1969
+ }, { status: 500 });
1935
1970
  }
1936
- }, { status: 500 });
1937
- }
1971
+ }
1972
+ ];
1938
1973
  }
1939
1974
  };
1940
1975
  return api;
@@ -1950,13 +1985,10 @@ function getRunnableRoute(route) {
1950
1985
  if (kind === "object" && "default" in route) {
1951
1986
  return getRunnableRoute(route.default);
1952
1987
  }
1953
- throw new Error('Route not runnable. Use "export default createRoute()" to create a Hyperspan route.');
1988
+ throw new Error(`Route not runnable. Use "export default createRoute()" to create a Hyperspan route. Exported methods found were: ${Object.keys(route).join(", ")}`);
1954
1989
  }
1955
1990
  function isRunnableRoute(route) {
1956
- return typeof route === "object" && "run" in route;
1957
- }
1958
- function createLayout(layout) {
1959
- return layout;
1991
+ return typeof route === "object" && "_getRouteHandlers" in route;
1960
1992
  }
1961
1993
  async function showErrorReponse(context, err) {
1962
1994
  const output = render(html`
@@ -2009,20 +2041,8 @@ async function buildRoutes(config) {
2009
2041
  return routes;
2010
2042
  }
2011
2043
  function createRouteFromModule(RouteModule) {
2012
- return async (context) => {
2013
- const reqMethod = context.req.method.toUpperCase();
2014
- try {
2015
- const runnableRoute = getRunnableRoute(RouteModule);
2016
- const content = await runnableRoute.run(reqMethod, context);
2017
- if (content instanceof Response) {
2018
- return content;
2019
- }
2020
- return context.text(String(content));
2021
- } catch (e) {
2022
- console.error(e);
2023
- return await showErrorReponse(context, e);
2024
- }
2025
- };
2044
+ const route = getRunnableRoute(RouteModule);
2045
+ return route._getRouteHandlers();
2026
2046
  }
2027
2047
  async function createServer(config) {
2028
2048
  await Promise.all([buildClientJS(), buildClientCSS()]);
@@ -2035,7 +2055,8 @@ async function createServer(config) {
2035
2055
  const fullRouteFile = join(CWD, route.file);
2036
2056
  const routePattern = normalizePath(route.route);
2037
2057
  routeMap.push({ route: routePattern, file: route.file });
2038
- app.all(routePattern, createRouteFromModule(await import(fullRouteFile)));
2058
+ const routeHandlers = createRouteFromModule(await import(fullRouteFile));
2059
+ app.all(routePattern, ...routeHandlers);
2039
2060
  }
2040
2061
  if (routeMap.length === 0) {
2041
2062
  app.get("/", (context) => {
@@ -2101,7 +2122,6 @@ export {
2101
2122
  createRouteFromModule,
2102
2123
  createRoute,
2103
2124
  createReadableStreamFromAsyncGenerator,
2104
- createLayout,
2105
2125
  createConfig,
2106
2126
  createAPIRoute,
2107
2127
  buildRoutes,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Hyperspan Web Framework",
5
- "main": "dist/server.js",
5
+ "main": "dist/server.ts",
6
6
  "types": "src/server.ts",
7
7
  "public": true,
8
8
  "publishConfig": {
@@ -63,6 +63,6 @@
63
63
  "@hyperspan/html": "^0.1.7",
64
64
  "hono": "^4.7.10",
65
65
  "isbot": "^5.1.28",
66
- "zod": "^3.25.28"
66
+ "zod": "^3.25.42"
67
67
  }
68
68
  }
package/src/server.ts CHANGED
@@ -6,6 +6,7 @@ import { buildClientJS, buildClientCSS } from './assets';
6
6
  import { Hono, type Context } from 'hono';
7
7
  import { serveStatic } from 'hono/bun';
8
8
  import { HTTPException } from 'hono/http-exception';
9
+ import type { HandlerResponse, MiddlewareHandler } from 'hono/types';
9
10
 
10
11
  export const IS_PROD = process.env.NODE_ENV === 'production';
11
12
  const CWD = process.cwd();
@@ -15,8 +16,7 @@ const CWD = process.cwd();
15
16
  */
16
17
  export type THSResponseTypes = HSHtml | Response | string | null;
17
18
  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>;
19
+ export type THSAPIRouteHandler = (context: Context) => Promise<any> | any;
20
20
 
21
21
  export type THSRoute = {
22
22
  _kind: 'hsRoute';
@@ -25,7 +25,18 @@ export type THSRoute = {
25
25
  put: (handler: THSRouteHandler) => THSRoute;
26
26
  delete: (handler: THSRouteHandler) => THSRoute;
27
27
  patch: (handler: THSRouteHandler) => THSRoute;
28
- run: (method: string, context: Context) => Promise<Response>;
28
+ middleware: (middleware: Array<MiddlewareHandler>) => THSRoute;
29
+ _getRouteHandlers: () => Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)>;
30
+ };
31
+ export type THSAPIRoute = {
32
+ _kind: 'hsAPIRoute';
33
+ get: (handler: THSAPIRouteHandler) => THSAPIRoute;
34
+ post: (handler: THSAPIRouteHandler) => THSAPIRoute;
35
+ put: (handler: THSAPIRouteHandler) => THSAPIRoute;
36
+ delete: (handler: THSAPIRouteHandler) => THSAPIRoute;
37
+ patch: (handler: THSAPIRouteHandler) => THSAPIRoute;
38
+ middleware: (middleware: Array<MiddlewareHandler>) => THSAPIRoute;
39
+ _getRouteHandlers: () => Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)>;
29
40
  };
30
41
 
31
42
  export function createConfig(config: THSServerConfig): THSServerConfig {
@@ -38,6 +49,7 @@ export function createConfig(config: THSServerConfig): THSServerConfig {
38
49
  */
39
50
  export function createRoute(handler?: THSRouteHandler): THSRoute {
40
51
  let _handlers: Record<string, THSRouteHandler> = {};
52
+ let _middleware: Array<MiddlewareHandler> = [];
41
53
 
42
54
  if (handler) {
43
55
  _handlers['GET'] = handler;
@@ -65,37 +77,57 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
65
77
  _handlers['PATCH'] = handler;
66
78
  return api;
67
79
  },
68
- async run(method: string, context: Context): Promise<Response> {
69
- const handler = _handlers[method];
70
- if (!handler) {
71
- throw new HTTPException(405, { message: 'Method not allowed' });
72
- }
73
-
74
- const routeContent = await handler(context);
75
-
76
- // Return Response if returned from route handler
77
- if (routeContent instanceof Response) {
78
- return routeContent;
79
- }
80
-
81
- // @TODO: Move this to config or something...
82
- const userIsBot = isbot(context.req.header('User-Agent'));
83
- const streamOpt = context.req.query('__nostream');
84
- const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
85
- const routeKind = typeof routeContent;
86
-
87
- // Render HSHtml if returned from route handler
88
- if (isHSHtml(routeContent)) {
89
- if (streamingEnabled) {
90
- return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
91
- } else {
92
- const output = await renderAsync(routeContent as HSHtml);
93
- return context.html(output);
94
- }
95
- }
96
-
97
- // Return unknown content - not specifically handled above
98
- return context.text(String(routeContent));
80
+ middleware(middleware: Array<MiddlewareHandler>) {
81
+ _middleware = middleware;
82
+ return api;
83
+ },
84
+ _getRouteHandlers() {
85
+ return [
86
+ ..._middleware,
87
+ async (context: Context) => {
88
+ const method = context.req.method.toUpperCase();
89
+
90
+ try {
91
+ const handler = _handlers[method];
92
+ if (!handler) {
93
+ throw new HTTPException(405, { message: 'Method not allowed' });
94
+ }
95
+
96
+ const routeContent = await handler(context);
97
+
98
+ // Return Response if returned from route handler
99
+ if (routeContent instanceof Response) {
100
+ return routeContent;
101
+ }
102
+
103
+ // @TODO: Move this to config or something...
104
+ const userIsBot = isbot(context.req.header('User-Agent'));
105
+ const streamOpt = context.req.query('__nostream');
106
+ const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
107
+
108
+ // Render HSHtml if returned from route handler
109
+ if (isHSHtml(routeContent)) {
110
+ if (streamingEnabled) {
111
+ return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
112
+ } else {
113
+ const output = await renderAsync(routeContent as HSHtml);
114
+ return context.html(output);
115
+ }
116
+ }
117
+
118
+ // Return custom Response if returned from route handler
119
+ if (routeContent instanceof Response) {
120
+ return routeContent;
121
+ }
122
+
123
+ // Return unknown content - not specifically handled above
124
+ return context.text(String(routeContent));
125
+ } catch (e) {
126
+ !IS_PROD && console.error(e);
127
+ return await showErrorReponse(context, e as Error);
128
+ }
129
+ },
130
+ ];
99
131
  },
100
132
  };
101
133
 
@@ -106,14 +138,15 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
106
138
  * Create new API Route
107
139
  * API Route handlers should return a JSON object or a Response
108
140
  */
109
- export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
141
+ export function createAPIRoute(handler?: THSAPIRouteHandler): THSAPIRoute {
110
142
  let _handlers: Record<string, THSAPIRouteHandler> = {};
143
+ let _middleware: Array<MiddlewareHandler> = [];
111
144
 
112
145
  if (handler) {
113
146
  _handlers['GET'] = handler;
114
147
  }
115
148
 
116
- const api: THSRoute = {
149
+ const api: THSAPIRoute = {
117
150
  _kind: 'hsRoute',
118
151
  get(handler: THSAPIRouteHandler) {
119
152
  _handlers['GET'] = handler;
@@ -135,39 +168,59 @@ export function createAPIRoute(handler?: THSAPIRouteHandler): THSRoute {
135
168
  _handlers['PATCH'] = handler;
136
169
  return api;
137
170
  },
138
- async run(method: string, context: Context): Promise<Response> {
139
- const handler = _handlers[method];
140
- if (!handler) {
141
- throw new Error('Method not allowed');
142
- }
143
-
144
- try {
145
- const response = await handler(context);
146
-
147
- if (response instanceof Response) {
148
- return response;
149
- }
171
+ middleware(middleware: Array<MiddlewareHandler>) {
172
+ _middleware = middleware;
173
+ return api;
174
+ },
175
+ _getRouteHandlers() {
176
+ return [
177
+ ..._middleware,
178
+ async (context: Context) => {
179
+ const method = context.req.method.toUpperCase();
180
+ const handler = _handlers[method];
181
+
182
+ if (!handler) {
183
+ return context.json(
184
+ {
185
+ meta: { success: false, dtResponse: new Date() },
186
+ data: {},
187
+ error: {
188
+ message: 'Method not allowed',
189
+ },
190
+ },
191
+ { status: 405 }
192
+ );
193
+ }
150
194
 
151
- return context.json(
152
- { meta: { success: true, dtResponse: new Date() }, data: response },
153
- { status: 200 }
154
- );
155
- } catch (err) {
156
- const e = err as Error;
157
- console.error(e);
158
-
159
- return context.json(
160
- {
161
- meta: { success: false, dtResponse: new Date() },
162
- data: {},
163
- error: {
164
- message: e.message,
165
- stack: IS_PROD ? undefined : e.stack?.split('\n'),
166
- },
167
- },
168
- { status: 500 }
169
- );
170
- }
195
+ try {
196
+ const response = await handler(context);
197
+
198
+ if (response instanceof Response) {
199
+ return response;
200
+ }
201
+
202
+ return context.json(
203
+ { meta: { success: true, dtResponse: new Date() }, data: response },
204
+ { status: 200 }
205
+ );
206
+ } catch (err) {
207
+ const e = err as Error;
208
+ !IS_PROD && console.error(e);
209
+
210
+ return context.json(
211
+ {
212
+ meta: { success: false, dtResponse: new Date() },
213
+ data: {},
214
+ error: {
215
+ message: e.message,
216
+ stack: IS_PROD ? undefined : e.stack?.split('\n'),
217
+ },
218
+ },
219
+ { status: 500 }
220
+ );
221
+ }
222
+ },
223
+ ];
171
224
  },
172
225
  };
173
226
 
@@ -199,21 +252,13 @@ export function getRunnableRoute(route: unknown): THSRoute {
199
252
 
200
253
  // No route -> error
201
254
  throw new Error(
202
- 'Route not runnable. Use "export default createRoute()" to create a Hyperspan route.'
255
+ `Route not runnable. Use "export default createRoute()" to create a Hyperspan route. Exported methods found were: ${Object.keys(route as {}).join(', ')}`
203
256
  );
204
257
  }
205
258
 
206
259
  export function isRunnableRoute(route: unknown): boolean {
207
260
  // @ts-ignore
208
- return typeof route === 'object' && 'run' in route;
209
- }
210
-
211
- /**
212
- * Create a layout for a Hyperspan app. Passthrough for now.
213
- * Future intent is to be able to conditionally render a layout for full page content vs. partial content.
214
- */
215
- export function createLayout<T>(layout: (props: T) => HSHtml | Promise<HSHtml>) {
216
- return layout;
261
+ return typeof route === 'object' && '_getRouteHandlers' in route;
217
262
  }
218
263
 
219
264
  /**
@@ -305,24 +350,11 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
305
350
  /**
306
351
  * Run route from file
307
352
  */
308
- export function createRouteFromModule(RouteModule: any): (context: Context) => Promise<Response> {
309
- return async (context: Context) => {
310
- const reqMethod = context.req.method.toUpperCase();
311
-
312
- try {
313
- const runnableRoute = getRunnableRoute(RouteModule);
314
- const content = await runnableRoute.run(reqMethod, context);
315
-
316
- if (content instanceof Response) {
317
- return content;
318
- }
319
-
320
- return context.text(String(content));
321
- } catch (e) {
322
- console.error(e);
323
- return await showErrorReponse(context, e as Error);
324
- }
325
- };
353
+ export function createRouteFromModule(
354
+ RouteModule: any
355
+ ): Array<MiddlewareHandler | ((context: Context) => HandlerResponse<any>)> {
356
+ const route = getRunnableRoute(RouteModule);
357
+ return route._getRouteHandlers();
326
358
  }
327
359
 
328
360
  /**
@@ -349,7 +381,8 @@ export async function createServer(config: THSServerConfig): Promise<Hono> {
349
381
  routeMap.push({ route: routePattern, file: route.file });
350
382
 
351
383
  // Import route
352
- app.all(routePattern, createRouteFromModule(await import(fullRouteFile)));
384
+ const routeHandlers = createRouteFromModule(await import(fullRouteFile));
385
+ app.all(routePattern, ...routeHandlers);
353
386
  }
354
387
 
355
388
  // Help route if no routes found
@@ -386,6 +419,7 @@ export async function createServer(config: THSServerConfig): Promise<Hono> {
386
419
  );
387
420
 
388
421
  app.notFound((context) => {
422
+ // @TODO: Add a custom 404 route
389
423
  return context.text('Not... found?', { status: 404 });
390
424
  });
391
425