@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 +1 -1
- package/src/actions.test.ts +0 -1
- package/src/server.test.ts +34 -6
- package/src/server.ts +26 -4
- package/src/types.ts +10 -7
package/package.json
CHANGED
package/src/actions.test.ts
CHANGED
|
@@ -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', () => {
|
package/src/server.test.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
};
|