@hyperspan/framework 1.0.2 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "src/server.ts",
6
6
  "types": "src/server.ts",
@@ -2,7 +2,6 @@ import { test, expect, describe } from 'bun:test';
2
2
  import { createAction } from './actions';
3
3
  import { html, render, type HSHtml } from '@hyperspan/html';
4
4
  import { createContext } from './server';
5
- import type { Hyperspan as HS } from './types';
6
5
  import * as z from 'zod/v4';
7
6
 
8
7
  describe('createAction', () => {
@@ -67,6 +67,34 @@ test('server returns a route with a POST request', async () => {
67
67
  expect(await response.text()).toBe('<h1>POST /users</h1>');
68
68
  });
69
69
 
70
+ test('server returns a route with a ALL request', async () => {
71
+ const server = await createServer({
72
+ appDir: './app',
73
+ publicDir: './public',
74
+ plugins: [],
75
+ });
76
+
77
+ server.all('/users', (context: HS.Context) => {
78
+ return context.res.html('<h1>ALL /users</h1>');
79
+ });
80
+
81
+ const route = server._routes.find((route: HS.Route) => route._path() === '/users' && route._methods().includes('*')) as HS.Route;
82
+
83
+ // GET request
84
+ let request = new Request('http://localhost:3000/users', { method: 'GET' });
85
+ let response = await route.fetch(request);
86
+ expect(response).toBeInstanceOf(Response);
87
+ expect(response.status).toBe(200);
88
+ expect(await response.text()).toBe('<h1>ALL /users</h1>');
89
+
90
+ // POST request
91
+ request = new Request('http://localhost:3000/users', { method: 'POST' });
92
+ response = await route.fetch(request);
93
+ expect(response).toBeInstanceOf(Response);
94
+ expect(response.status).toBe(200);
95
+ expect(await response.text()).toBe('<h1>ALL /users</h1>');
96
+ });
97
+
70
98
  test('returns 405 when route path matches but HTTP method does not', async () => {
71
99
  const server = await createServer({
72
100
  appDir: './app',
@@ -144,7 +172,7 @@ test('createContext() can get and set cookies', () => {
144
172
  }
145
173
  });
146
174
 
147
- test('createContext() merge() function preserves custom headers when using response methods', () => {
175
+ test('createContext() merge() function preserves custom headers when using response methods', async () => {
148
176
  // Create a request
149
177
  const request = new Request('http://localhost:3000/');
150
178
 
@@ -157,7 +185,7 @@ test('createContext() merge() function preserves custom headers when using respo
157
185
  context.res.headers.set('Authorization', 'Bearer token123');
158
186
 
159
187
  // Use html() method which should merge headers
160
- const response = context.res.html('<h1>Test</h1>');
188
+ const response = await context.res.html('<h1>Test</h1>');
161
189
 
162
190
  // Verify the response has both the custom headers and the Content-Type header
163
191
  expect(response.headers.get('X-Custom-Header')).toBe('custom-value');
@@ -169,7 +197,7 @@ test('createContext() merge() function preserves custom headers when using respo
169
197
  expect(response.status).toBe(200);
170
198
  });
171
199
 
172
- test('createContext() merge() function preserves custom headers with json() method', () => {
200
+ test('createContext() merge() function preserves custom headers with json() method', async () => {
173
201
  const request = new Request('http://localhost:3000/');
174
202
  const context = createContext(request);
175
203
 
@@ -178,7 +206,7 @@ test('createContext() merge() function preserves custom headers with json() meth
178
206
  context.res.headers.set('X-Request-ID', 'req-123');
179
207
 
180
208
  // Use json() method
181
- const response = context.res.json({ message: 'Hello' });
209
+ const response = await context.res.json({ message: 'Hello' });
182
210
 
183
211
  // Verify headers are merged
184
212
  expect(response.headers.get('X-API-Version')).toBe('v1');
@@ -186,7 +214,7 @@ test('createContext() merge() function preserves custom headers with json() meth
186
214
  expect(response.headers.get('Content-Type')).toBe('application/json');
187
215
  });
188
216
 
189
- test('createContext() merge() function allows response headers to override context headers', () => {
217
+ test('createContext() merge() function allows response headers to override context headers', async () => {
190
218
  const request = new Request('http://localhost:3000/');
191
219
  const context = createContext(request);
192
220
 
@@ -194,7 +222,7 @@ test('createContext() merge() function allows response headers to override conte
194
222
  context.res.headers.set('X-Header', 'context-value');
195
223
 
196
224
  // Use html() with options that include the same header (should override)
197
- const response = context.res.html('<h1>Test</h1>', {
225
+ const response = await context.res.html('<h1>Test</h1>', {
198
226
  headers: {
199
227
  'X-Header': 'response-value',
200
228
  },
package/src/server.ts CHANGED
@@ -64,14 +64,14 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
64
64
  // Status override for the response. Will use if set. (e.g. c.res.status = 400)
65
65
  let status: number | undefined = undefined;
66
66
 
67
- const merge = (response: Response) => {
67
+ const merge = async (response: Response) => {
68
68
  // Convert headers to plain objects and merge (response headers override context headers)
69
69
  const mergedHeaders = {
70
70
  ...Object.fromEntries(headers.entries()),
71
71
  ...Object.fromEntries(response.headers.entries()),
72
72
  };
73
73
 
74
- return new Response(response.body, {
74
+ return new Response(await response.text(), {
75
75
  status: context.res.status ?? response.status,
76
76
  headers: mergedHeaders,
77
77
  });
@@ -184,6 +184,14 @@ export function createRoute(config: Partial<HS.RouteConfig> = {}): HS.Route {
184
184
  _middleware['OPTIONS'] = handlerOptions?.middleware || [];
185
185
  return api;
186
186
  },
187
+ /**
188
+ * Add a ALL route handler (typically to handle all HTTP methods)
189
+ */
190
+ all(handler: HS.RouteHandler, handlerOptions?: HS.RouteHandlerOptions) {
191
+ _handlers['*'] = handler;
192
+ _middleware['*'] = handlerOptions?.middleware || [];
193
+ return api;
194
+ },
187
195
  /**
188
196
  * Set a custom error handler for this route to fall back to if the route handler throws an error
189
197
  */
@@ -192,7 +200,15 @@ export function createRoute(config: Partial<HS.RouteConfig> = {}): HS.Route {
192
200
  return api;
193
201
  },
194
202
  /**
195
- * Add middleware specific to this route
203
+ * Add a middleware function to this route (for all HTTP methods) (non-destructive)
204
+ */
205
+ use(middleware: HS.MiddlewareFunction) {
206
+ _middleware['*'].push(middleware);
207
+ return api;
208
+ },
209
+ /**
210
+ * Set the complete middleware stack for this route (for all HTTP methods) (destructive)
211
+ * NOTE: This will override the middleware stack for this route
196
212
  */
197
213
  middleware(middleware: Array<HS.MiddlewareFunction>) {
198
214
  _middleware['*'] = middleware;
@@ -231,7 +247,7 @@ export function createRoute(config: Partial<HS.RouteConfig> = {}): HS.Route {
231
247
  );
232
248
  }
233
249
 
234
- const handler = method === 'HEAD' ? _handlers['GET'] : _handlers[method];
250
+ const handler = (method === 'HEAD' ? _handlers['GET'] : _handlers[method]) ?? _handlers['*'];
235
251
 
236
252
  if (!handler) {
237
253
  return context.res.error(new Error('Method not allowed'), { status: 405 });
@@ -332,6 +348,12 @@ export async function createServer(config: HS.Config = {} as HS.Config): Promise
332
348
  _routes.push(route);
333
349
  return route;
334
350
  },
351
+ all(path: string, handler: HS.RouteHandler, handlerOptions?: HS.RouteHandlerOptions) {
352
+ const route = createRoute().all(handler, handlerOptions);
353
+ route._config.path = path;
354
+ _routes.push(route);
355
+ return route;
356
+ },
335
357
  };
336
358
 
337
359
  return api;
package/src/types.ts CHANGED
@@ -16,6 +16,7 @@ export namespace Hyperspan {
16
16
  patch: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
17
17
  delete: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
18
18
  options: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
19
+ all: (path: string, handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
19
20
  };
20
21
 
21
22
  export type Plugin = (config: Hyperspan.Config) => Promise<void> | void;
@@ -69,13 +70,13 @@ export namespace Hyperspan {
69
70
  cookies: Hyperspan.Cookies;
70
71
  headers: Headers; // Headers to merge with final outgoing response
71
72
  status: number | undefined;
72
- html: (html: string, options?: ResponseInit) => Response
73
- json: (json: any, options?: ResponseInit) => Response;
74
- text: (text: string, options?: ResponseInit) => Response;
75
- redirect: (url: string, options?: ResponseInit) => Response;
76
- error: (error: Error, options?: ResponseInit) => Response;
77
- notFound: (options?: ResponseInit) => Response;
78
- merge: (response: Response) => Response;
73
+ html: (html: string, options?: ResponseInit) => Promise<Response>;
74
+ json: (json: any, options?: ResponseInit) => Promise<Response>;
75
+ text: (text: string, options?: ResponseInit) => Promise<Response>;
76
+ redirect: (url: string, options?: ResponseInit) => Promise<Response>;
77
+ error: (error: Error, options?: ResponseInit) => Promise<Response>;
78
+ notFound: (options?: ResponseInit) => Promise<Response>;
79
+ merge: (response: Response) => Promise<Response>;
79
80
  };
80
81
 
81
82
  export interface Context {
@@ -145,7 +146,9 @@ export namespace Hyperspan {
145
146
  patch: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
146
147
  delete: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
147
148
  options: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
149
+ all: (handler: Hyperspan.RouteHandler, handlerOptions?: Hyperspan.RouteHandlerOptions) => Hyperspan.Route;
148
150
  errorHandler: (handler: Hyperspan.ErrorHandler) => Hyperspan.Route;
151
+ use: (middleware: Hyperspan.MiddlewareFunction) => Hyperspan.Route;
149
152
  middleware: (middleware: Array<Hyperspan.MiddlewareFunction>) => Hyperspan.Route;
150
153
  fetch: (request: Request) => Promise<Response>;
151
154
  };