@hyperspan/framework 0.1.2 → 0.1.4

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/src/server.ts CHANGED
@@ -5,254 +5,203 @@ import { isbot } from 'isbot';
5
5
  import { buildClientJS, buildClientCSS } from './assets';
6
6
  import { Hono } from 'hono';
7
7
  import { serveStatic } from 'hono/bun';
8
- import * as z from 'zod';
9
8
  import type { Context, Handler } from 'hono';
9
+ import { HTTPException } from 'hono/http-exception';
10
+ import { create } from 'node:domain';
10
11
 
11
12
  export const IS_PROD = process.env.NODE_ENV === 'production';
12
13
  const CWD = process.cwd();
13
14
 
14
- /**
15
- * Route
16
- * Define a route that can handle a direct HTTP request
17
- * Route handlers should return a Response or TmplHtml object
18
- */
19
- export function createRoute(handler: Handler): HSRoute {
20
- return new HSRoute(handler);
21
- }
22
-
23
- /**
24
- * Component
25
- * Define a component or partial with an optional loading placeholder
26
- * These can be rendered anywhere inside other templates - even if async.
27
- */
28
- export function createComponent(render: () => THSComponentReturn | Promise<THSComponentReturn>) {
29
- return new HSComponent(render);
30
- }
31
-
32
- /**
33
- * Form + route handler
34
- * Automatically handles and parses form data
35
- *
36
- * INITIAL IDEA OF HOW THIS WILL WORK:
37
- * ---
38
- * 1. Renders component as initial form markup for GET request
39
- * 2. Bind form onSubmit function to custom client JS handling
40
- * 3. Submits form with JavaScript fetch()
41
- * 4. Replaces form content with content from server
42
- * 5. All validation and save logic is on the server
43
- * 6. Handles any Exception thrown on server as error displayed in client
44
- */
45
- export function createForm(
46
- renderForm: (data?: any) => THSResponseTypes,
47
- schema?: z.ZodSchema | null
48
- ): HSFormRoute {
49
- return new HSFormRoute(renderForm, schema);
50
- }
51
-
52
15
  /**
53
16
  * Types
54
17
  */
55
- export type THSComponentReturn = TmplHtml | string | number | null;
56
18
  export type THSResponseTypes = TmplHtml | Response | string | null;
57
- export const HS_DEFAULT_LOADING = () => html`<div>Loading...</div>`;
58
-
59
- /**
60
- * Route handler helper
61
- */
62
- export class HSComponent {
63
- _kind = 'hsComponent';
64
- _handlers: Record<string, Handler> = {};
65
- _loading?: () => TmplHtml;
66
- render: () => THSComponentReturn | Promise<THSComponentReturn>;
67
- constructor(render: () => THSComponentReturn | Promise<THSComponentReturn>) {
68
- this.render = render;
69
- }
70
-
71
- loading(fn: () => TmplHtml) {
72
- this._loading = fn;
73
- return this;
74
- }
75
- }
19
+ export type THSRouteHandler = (context: Context) => THSResponseTypes | Promise<THSResponseTypes>;
76
20
 
77
21
  /**
78
- * Route handler helper
79
- */
80
- export class HSRoute {
81
- _kind = 'hsRoute';
82
- _handlers: Record<string, Handler> = {};
83
- _methods: null | string[] = null;
84
- constructor(handler: Handler) {
85
- this._handlers.GET = handler;
86
- }
87
- }
88
-
89
- /**
90
- * Form route handler helper
22
+ * Route
23
+ * Define a route that can handle a direct HTTP request
24
+ * Route handlers should return a Response or TmplHtml object
91
25
  */
92
- export type THSFormRenderer = (data?: any) => THSResponseTypes;
93
- export class HSFormRoute {
94
- _kind = 'hsFormRoute';
95
- _handlers: Record<string, Handler> = {};
96
- _form: THSFormRenderer;
97
- _methods: null | string[] = null;
98
- _schema: null | z.ZodSchema = null;
99
-
100
- constructor(renderForm: THSFormRenderer, schema: z.ZodSchema | null = null) {
101
- // Haz schema?
102
- if (schema) {
103
- type TSchema = z.infer<typeof schema>;
104
- this._form = renderForm as (data: TSchema) => THSResponseTypes;
105
- this._schema = schema;
106
- } else {
107
- this._form = renderForm;
108
- }
109
-
110
- // GET request is render form by default
111
- this._handlers.GET = () => renderForm(this.getDefaultData());
112
- }
26
+ export function createRoute(handler?: THSRouteHandler) {
27
+ let _handlers: Record<string, THSRouteHandler> = {};
113
28
 
114
- // Form data
115
- getDefaultData() {
116
- if (!this._schema) {
117
- return {};
118
- }
119
-
120
- type TSchema = z.infer<typeof this._schema>;
121
- const data = z.parse(this._schema, {});
122
- return data as TSchema;
29
+ if (handler) {
30
+ _handlers['GET'] = handler;
123
31
  }
124
32
 
125
- /**
126
- * Get form renderer method
127
- */
128
- renderForm(data?: any) {
129
- return this._form(data || this.getDefaultData());
130
- }
33
+ const api = {
34
+ _kind: 'hsRoute',
35
+ get(handler: THSRouteHandler) {
36
+ _handlers['GET'] = handler;
37
+ return api;
38
+ },
39
+ post(handler: THSRouteHandler) {
40
+ _handlers['POST'] = handler;
41
+ return api;
42
+ },
43
+ put(handler: THSRouteHandler) {
44
+ _handlers['PUT'] = handler;
45
+ return api;
46
+ },
47
+ delete(handler: THSRouteHandler) {
48
+ _handlers['DELETE'] = handler;
49
+ return api;
50
+ },
51
+ patch(handler: THSRouteHandler) {
52
+ _handlers['PATCH'] = handler;
53
+ return api;
54
+ },
55
+ async run(method: string, context: Context): Promise<Response> {
56
+ const handler = _handlers[method];
57
+ if (!handler) {
58
+ throw new HTTPException(405, { message: 'Method not allowed' });
59
+ }
131
60
 
132
- // HTTP handlers
133
- get(handler: Handler) {
134
- this._handlers.GET = handler;
135
- return this;
136
- }
61
+ const routeContent = await handler(context);
137
62
 
138
- patch(handler: Handler) {
139
- this._handlers.PATCH = handler;
140
- return this;
141
- }
63
+ // Return Response if returned from route handler
64
+ if (routeContent instanceof Response) {
65
+ return routeContent;
66
+ }
142
67
 
143
- post(handler: Handler) {
144
- this._handlers.POST = handler;
145
- return this;
146
- }
68
+ // @TODO: Move this to config or something...
69
+ const userIsBot = isbot(context.req.header('User-Agent'));
70
+ const streamOpt = context.req.query('__nostream');
71
+ const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
72
+ const routeKind = typeof routeContent;
73
+
74
+ // Render TmplHtml if returned from route handler
75
+ if (
76
+ routeContent &&
77
+ routeKind === 'object' &&
78
+ (routeContent instanceof TmplHtml ||
79
+ routeContent.constructor.name === 'TmplHtml' ||
80
+ // @ts-ignore
81
+ routeContent?._kind === 'TmplHtml')
82
+ ) {
83
+ if (streamingEnabled) {
84
+ return new StreamResponse(renderStream(routeContent as TmplHtml)) as Response;
85
+ } else {
86
+ const output = await renderAsync(routeContent as TmplHtml);
87
+ return context.html(output);
88
+ }
89
+ }
147
90
 
148
- put(handler: Handler) {
149
- this._handlers.PUT = handler;
150
- return this;
151
- }
91
+ // Return unknown content - not specifically handled above
92
+ return context.text(String(routeContent));
93
+ },
94
+ };
152
95
 
153
- delete(handler: Handler) {
154
- this._handlers.DELETE = handler;
155
- return this;
156
- }
96
+ return api;
157
97
  }
98
+ export type THSRoute = ReturnType<typeof createRoute>;
158
99
 
159
100
  /**
160
- * Run route from file
101
+ * Create new API Route
102
+ * API Route handlers should return a JSON object or a Response
161
103
  */
162
- export async function runFileRoute(RouteModule: any, context: Context): Promise<Response | false> {
163
- const req = context.req;
164
- const url = new URL(req.url);
165
- const qs = url.searchParams;
166
-
167
- // @TODO: Move this to config or something...
168
- const userIsBot = isbot(context.req.header('User-Agent'));
169
- const streamOpt = qs.get('__nostream') ? !Boolean(qs.get('__nostream')) : undefined;
170
- const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
171
-
172
- // Route module
173
- const RouteComponent = RouteModule.default;
174
- const reqMethod = req.method.toUpperCase();
175
-
176
- try {
177
- // API Route?
178
- if (RouteModule[reqMethod] !== undefined) {
179
- return await runAPIRoute(RouteModule[reqMethod], context);
180
- }
181
-
182
- let routeContent;
183
-
184
- // No default export in this file...
185
- if (!RouteComponent) {
186
- throw new Error('No route was exported by default in matched route file.');
187
- }
104
+ export function createAPIRoute(handler?: THSRouteHandler) {
105
+ let _handlers: Record<string, THSRouteHandler> = {};
188
106
 
189
- // Route component
190
- if (typeof RouteComponent._handlers !== 'undefined') {
191
- const routeMethodHandler = RouteComponent._handlers[reqMethod];
107
+ if (handler) {
108
+ _handlers['GET'] = handler;
109
+ }
192
110
 
193
- if (!routeMethodHandler) {
194
- return new Response('Method Not Allowed', {
195
- status: 405,
196
- headers: { 'content-type': 'text/plain' },
197
- });
111
+ const api = {
112
+ _kind: 'hsRoute',
113
+ get(handler: THSRouteHandler) {
114
+ _handlers['GET'] = handler;
115
+ return api;
116
+ },
117
+ post(handler: THSRouteHandler) {
118
+ _handlers['POST'] = handler;
119
+ return api;
120
+ },
121
+ put(handler: THSRouteHandler) {
122
+ _handlers['PUT'] = handler;
123
+ return api;
124
+ },
125
+ delete(handler: THSRouteHandler) {
126
+ _handlers['DELETE'] = handler;
127
+ return api;
128
+ },
129
+ patch(handler: THSRouteHandler) {
130
+ _handlers['PATCH'] = handler;
131
+ return api;
132
+ },
133
+ async run(method: string, context: Context): Promise<Response> {
134
+ const handler = _handlers[method];
135
+ if (!handler) {
136
+ throw new Error('Method not allowed');
198
137
  }
199
138
 
200
- routeContent = await routeMethodHandler(context);
201
- } else {
202
- routeContent = await RouteComponent(context);
203
- }
139
+ try {
140
+ const response = await handler(context);
204
141
 
205
- if (routeContent instanceof Response) {
206
- return routeContent;
207
- }
142
+ if (response instanceof Response) {
143
+ return response;
144
+ }
208
145
 
209
- let routeKind = typeof routeContent;
210
-
211
- // Render TmplHtml if returned from route handler
212
- if (
213
- routeKind === 'object' &&
214
- (routeContent instanceof TmplHtml ||
215
- routeContent.constructor.name === 'TmplHtml' ||
216
- routeContent?._kind === 'TmplHtml')
217
- ) {
218
- if (streamingEnabled) {
219
- return new StreamResponse(renderStream(routeContent)) as Response;
220
- } else {
221
- const output = await renderAsync(routeContent);
222
- return context.html(output);
146
+ return context.json(
147
+ { meta: { success: true, dtResponse: new Date() }, data: response },
148
+ { status: 200 }
149
+ );
150
+ } catch (err) {
151
+ const e = err as Error;
152
+ console.error(e);
153
+
154
+ return context.json(
155
+ {
156
+ meta: { success: false, dtResponse: new Date() },
157
+ data: {},
158
+ error: {
159
+ message: e.message,
160
+ stack: IS_PROD ? undefined : e.stack?.split('\n'),
161
+ },
162
+ },
163
+ { status: 500 }
164
+ );
223
165
  }
224
- }
225
-
226
- console.log('Returning unknown type... ', routeContent);
166
+ },
167
+ };
227
168
 
228
- return routeContent;
229
- } catch (e) {
230
- console.error(e);
231
- return await showErrorReponse(context, e as Error);
232
- }
169
+ return api;
233
170
  }
171
+ export type THSAPIRoute = ReturnType<typeof createAPIRoute>;
234
172
 
235
173
  /**
236
- * Run route and handle response
174
+ * Get a Hyperspan runnable route from a module import
175
+ * @throws Error if no runnable route found
237
176
  */
238
- async function runAPIRoute(routeFn: any, context: Context, middlewareResult?: any) {
239
- try {
240
- return await routeFn(context, middlewareResult);
241
- } catch (err) {
242
- const e = err as Error;
243
- console.error(e);
244
-
245
- return context.json(
246
- {
247
- meta: { success: false },
248
- data: {
249
- message: e.message,
250
- stack: IS_PROD ? undefined : e.stack?.split('\n'),
251
- },
252
- },
253
- { status: 500 }
254
- );
177
+ export function getRunnableRoute(route: unknown): THSRoute {
178
+ // Runnable already? Just return it
179
+ if (isRouteRunnable(route)) {
180
+ return route as THSRoute;
181
+ }
182
+
183
+ const kind = typeof route;
184
+
185
+ // Plain function - wrap in createRoute()
186
+ if (kind === 'function') {
187
+ return createRoute(route as THSRouteHandler);
188
+ }
189
+
190
+ // Module - get default and use it
191
+ // @ts-ignore
192
+ if (kind === 'object' && 'default' in route) {
193
+ return getRunnableRoute(route.default);
255
194
  }
195
+
196
+ // No route -> error
197
+ throw new Error(
198
+ 'Route not runnable. Use "export default createRoute()" to create a Hyperspan route.'
199
+ );
200
+ }
201
+
202
+ export function isRouteRunnable(route: unknown): boolean {
203
+ // @ts-ignore
204
+ return typeof route === 'object' && 'run' in route;
256
205
  }
257
206
 
258
207
  /**
@@ -341,6 +290,29 @@ export async function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[
341
290
  return routes;
342
291
  }
343
292
 
293
+ /**
294
+ * Run route from file
295
+ */
296
+ export function createRouteFromModule(RouteModule: any): (context: Context) => Promise<Response> {
297
+ return async (context: Context) => {
298
+ const reqMethod = context.req.method.toUpperCase();
299
+
300
+ try {
301
+ const runnableRoute = getRunnableRoute(RouteModule);
302
+ const content = await runnableRoute.run(reqMethod, context);
303
+
304
+ if (content instanceof Response) {
305
+ return content;
306
+ }
307
+
308
+ return context.text(String(content));
309
+ } catch (e) {
310
+ console.error(e);
311
+ return await showErrorReponse(context, e as Error);
312
+ }
313
+ };
314
+ }
315
+
344
316
  /**
345
317
  * Create and start Bun HTTP server
346
318
  */
@@ -367,14 +339,7 @@ export async function createServer(config: THSServerConfig): Promise<Hono> {
367
339
  // Import route
368
340
  const routeModule = await import(fullRouteFile);
369
341
 
370
- app.all(routePattern, async (context) => {
371
- const matchedRoute = await runFileRoute(routeModule, context);
372
- if (matchedRoute) {
373
- return matchedRoute as Response;
374
- }
375
-
376
- return context.notFound();
377
- });
342
+ app.all(routePattern, createRouteFromModule(routeModule));
378
343
  }
379
344
 
380
345
  // Help route if no routes found
@@ -401,6 +366,12 @@ export async function createServer(config: THSServerConfig): Promise<Hono> {
401
366
  '*',
402
367
  serveStatic({
403
368
  root: config.staticFileRoot,
369
+ onFound: IS_PROD
370
+ ? (_, c) => {
371
+ // Cache static assets in prod (default 30 days)
372
+ c.header('Cache-Control', 'public, max-age=2592000');
373
+ }
374
+ : undefined,
404
375
  })
405
376
  );
406
377
 
@@ -422,8 +393,9 @@ export class StreamResponse extends Response {
422
393
  return new Response(stream, {
423
394
  status: 200,
424
395
  headers: {
425
- 'Content-Type': 'text/html',
426
396
  'Transfer-Encoding': 'chunked',
397
+ 'Content-Type': 'text/html; charset=UTF-8',
398
+ 'Content-Encoding': 'Identity',
427
399
  // @ts-ignore
428
400
  ...(options?.headers ?? {}),
429
401
  },
package/dist/index.d.ts DELETED
@@ -1,132 +0,0 @@
1
- // Generated by dts-bundle-generator v9.5.1
2
-
3
- import { Context, Handler, Hono } from 'hono';
4
-
5
- declare class TmplHtml {
6
- _kind: string;
7
- content: string;
8
- asyncContent: Array<{
9
- id: string;
10
- promise: Promise<{
11
- id: string;
12
- value: unknown;
13
- }>;
14
- }>;
15
- constructor(props: Pick<TmplHtml, "content" | "asyncContent">);
16
- }
17
- export declare const IS_PROD: boolean;
18
- /**
19
- * Route
20
- * Define a route that can handle a direct HTTP request
21
- * Route handlers should return a Response or TmplHtml object
22
- */
23
- export declare function createRoute(handler: Handler): HSRoute;
24
- /**
25
- * Component
26
- * Define a component or partial with an optional loading placeholder
27
- * These can be rendered anywhere inside other templates - even if async.
28
- */
29
- export declare function createComponent(render: () => THSComponentReturn | Promise<THSComponentReturn>): HSComponent;
30
- /**
31
- * Form + route handler
32
- * Automatically handles and parses form data
33
- *
34
- * INITIAL IDEA OF HOW THIS WILL WORK:
35
- * ---
36
- * 1. Renders component as initial form markup for GET request
37
- * 2. Bind form onSubmit function to custom client JS handling
38
- * 3. Submits form with JavaScript fetch()
39
- * 4. Replaces form content with content from server
40
- * 5. All validation and save logic is on the server
41
- * 6. Handles any Exception thrown on server as error displayed in client
42
- */
43
- export declare function createForm(renderForm: (data?: any) => THSResponseTypes, schema?: z.ZodSchema | null): HSFormRoute;
44
- /**
45
- * Types
46
- */
47
- export type THSComponentReturn = TmplHtml | string | number | null;
48
- export type THSResponseTypes = TmplHtml | Response | string | null;
49
- export declare const HS_DEFAULT_LOADING: () => TmplHtml;
50
- /**
51
- * Route handler helper
52
- */
53
- export declare class HSComponent {
54
- _kind: string;
55
- _handlers: Record<string, Handler>;
56
- _loading?: () => TmplHtml;
57
- render: () => THSComponentReturn | Promise<THSComponentReturn>;
58
- constructor(render: () => THSComponentReturn | Promise<THSComponentReturn>);
59
- loading(fn: () => TmplHtml): this;
60
- }
61
- /**
62
- * Route handler helper
63
- */
64
- export declare class HSRoute {
65
- _kind: string;
66
- _handlers: Record<string, Handler>;
67
- _methods: null | string[];
68
- constructor(handler: Handler);
69
- }
70
- /**
71
- * Form route handler helper
72
- */
73
- export type THSFormRenderer = (data?: any) => THSResponseTypes;
74
- export declare class HSFormRoute {
75
- _kind: string;
76
- _handlers: Record<string, Handler>;
77
- _form: THSFormRenderer;
78
- _methods: null | string[];
79
- _schema: null | z.ZodSchema;
80
- constructor(renderForm: THSFormRenderer, schema?: z.ZodSchema | null);
81
- getDefaultData(): unknown;
82
- /**
83
- * Get form renderer method
84
- */
85
- renderForm(data?: any): THSResponseTypes;
86
- get(handler: Handler): this;
87
- patch(handler: Handler): this;
88
- post(handler: Handler): this;
89
- put(handler: Handler): this;
90
- delete(handler: Handler): this;
91
- }
92
- /**
93
- * Run route from file
94
- */
95
- export declare function runFileRoute(RouteModule: any, context: Context): Promise<Response | false>;
96
- export type THSServerConfig = {
97
- appDir: string;
98
- staticFileRoot: string;
99
- rewrites?: Array<{
100
- source: string;
101
- destination: string;
102
- }>;
103
- beforeRoutesAdded?: (app: Hono) => void;
104
- afterRoutesAdded?: (app: Hono) => void;
105
- };
106
- export type THSRouteMap = {
107
- file: string;
108
- route: string;
109
- params: string[];
110
- };
111
- export declare function buildRoutes(config: THSServerConfig): Promise<THSRouteMap[]>;
112
- /**
113
- * Create and start Bun HTTP server
114
- */
115
- export declare function createServer(config: THSServerConfig): Promise<Hono>;
116
- /**
117
- * Streaming HTML Response
118
- */
119
- export declare class StreamResponse extends Response {
120
- constructor(iterator: AsyncIterator<unknown>, options?: {});
121
- }
122
- /**
123
- * Does what it says on the tin...
124
- */
125
- export declare function createReadableStreamFromAsyncGenerator(output: AsyncGenerator): ReadableStream<any>;
126
- /**
127
- * Normalize URL path
128
- * Removes trailing slash and lowercases path
129
- */
130
- export declare function normalizePath(urlPath: string): string;
131
-
132
- export {};