@hyperspan/framework 1.0.0-alpha.10 → 1.0.0-alpha.12
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/index.ts +1 -1
- package/src/server.test.ts +2 -8
- package/src/server.ts +23 -12
- package/src/types.ts +30 -29
- package/src/utils.test.ts +2 -2
- package/src/utils.ts +1 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createConfig, createContext, createRoute, createServer, getRunnableRoute, StreamResponse, IS_PROD,
|
|
1
|
+
export { createConfig, createContext, createRoute, createServer, getRunnableRoute, StreamResponse, IS_PROD, HTTPResponseException } from './server';
|
|
2
2
|
export type { Hyperspan } from './types';
|
package/src/server.test.ts
CHANGED
|
@@ -113,7 +113,7 @@ test('createContext() can get and set cookies', () => {
|
|
|
113
113
|
expect(setCookieHeader).toBeTruthy();
|
|
114
114
|
expect(setCookieHeader).toContain('newCookie=newValue');
|
|
115
115
|
|
|
116
|
-
// Test setting a cookie with options (this
|
|
116
|
+
// Test setting a cookie with options (this should NOT overwrite the previous Set-Cookie header)
|
|
117
117
|
context.res.cookies.set('secureCookie', 'secureValue', {
|
|
118
118
|
httpOnly: true,
|
|
119
119
|
secure: true,
|
|
@@ -125,13 +125,7 @@ test('createContext() can get and set cookies', () => {
|
|
|
125
125
|
setCookieHeader = context.res.headers.get('Set-Cookie');
|
|
126
126
|
expect(setCookieHeader).toBeTruthy();
|
|
127
127
|
expect(setCookieHeader).toContain('secureCookie=secureValue');
|
|
128
|
-
expect(setCookieHeader).toContain('
|
|
129
|
-
expect(setCookieHeader).toContain('Secure');
|
|
130
|
-
expect(setCookieHeader).toContain('SameSite=Strict');
|
|
131
|
-
expect(setCookieHeader).toContain('Max-Age=3600');
|
|
132
|
-
|
|
133
|
-
// Verify the previous cookie was overwritten
|
|
134
|
-
expect(setCookieHeader).not.toContain('newCookie=newValue');
|
|
128
|
+
expect(setCookieHeader).toContain('newCookie=newValue');
|
|
135
129
|
|
|
136
130
|
// Test deleting a cookie
|
|
137
131
|
context.res.cookies.delete('sessionId');
|
package/src/server.ts
CHANGED
|
@@ -5,12 +5,15 @@ import { parsePath } from './utils';
|
|
|
5
5
|
import { Cookies } from './cookies';
|
|
6
6
|
|
|
7
7
|
import type { Hyperspan as HS } from './types';
|
|
8
|
+
import { RequestOptions } from 'node:http';
|
|
8
9
|
|
|
9
10
|
export const IS_PROD = process.env.NODE_ENV === 'production';
|
|
10
11
|
|
|
11
|
-
export class
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export class HTTPResponseException extends Error {
|
|
13
|
+
public _response?: Response;
|
|
14
|
+
constructor(body: string | undefined, options?: ResponseInit) {
|
|
15
|
+
super(body);
|
|
16
|
+
this._response = new Response(body, options);
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -36,7 +39,15 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
|
|
|
36
39
|
const headers = new Headers(req.headers);
|
|
37
40
|
const path = route?._path() || '/';
|
|
38
41
|
// @ts-ignore - Bun will put 'params' on the Request object even though it's not standardized
|
|
39
|
-
const params: HS.RouteParamsParser<path> = req?.params || {};
|
|
42
|
+
const params: HS.RouteParamsParser<path> & Record<string, string | undefined> = Object.assign({}, req?.params || {}, route?._config.params || {});
|
|
43
|
+
|
|
44
|
+
// Replace catch-all param with the value from the URL path
|
|
45
|
+
const catchAllParam = Object.keys(params).find(key => key.startsWith('...'));
|
|
46
|
+
if (catchAllParam && path.includes('/*')) {
|
|
47
|
+
const catchAllValue = url.pathname.split(path.replace('/*', '/')).pop();
|
|
48
|
+
params[catchAllParam.replace('...', '')] = catchAllValue;
|
|
49
|
+
delete params[catchAllParam];
|
|
50
|
+
}
|
|
40
51
|
|
|
41
52
|
const merge = (response: Response) => {
|
|
42
53
|
// Convert headers to plain objects and merge (response headers override context headers)
|
|
@@ -74,12 +85,12 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
|
|
|
74
85
|
cookies: new Cookies(req, headers),
|
|
75
86
|
headers,
|
|
76
87
|
raw: new Response(),
|
|
77
|
-
html: (html: string, options?:
|
|
78
|
-
json: (json: any, options?:
|
|
79
|
-
text: (text: string, options?:
|
|
80
|
-
redirect: (url: string, options?:
|
|
81
|
-
error: (error: Error, options?:
|
|
82
|
-
notFound: (options?:
|
|
88
|
+
html: (html: string, options?: ResponseInit) => merge(new Response(html, { ...options, headers: { 'Content-Type': 'text/html; charset=UTF-8', ...options?.headers } })),
|
|
89
|
+
json: (json: any, options?: ResponseInit) => merge(new Response(JSON.stringify(json), { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers } })),
|
|
90
|
+
text: (text: string, options?: ResponseInit) => merge(new Response(text, { ...options, headers: { 'Content-Type': 'text/plain; charset=UTF-8', ...options?.headers } })),
|
|
91
|
+
redirect: (url: string, options?: ResponseInit) => merge(new Response(null, { status: 302, headers: { Location: url, ...options?.headers } })),
|
|
92
|
+
error: (error: Error, options?: ResponseInit) => merge(new Response(error.message, { status: 500, ...options })),
|
|
93
|
+
notFound: (options?: ResponseInit) => merge(new Response('Not Found', { status: 404, ...options })),
|
|
83
94
|
merge,
|
|
84
95
|
},
|
|
85
96
|
};
|
|
@@ -398,8 +409,8 @@ async function showErrorReponse(
|
|
|
398
409
|
const message = err.message || 'Internal Server Error';
|
|
399
410
|
|
|
400
411
|
// Send correct status code if HTTPException
|
|
401
|
-
if (err instanceof
|
|
402
|
-
status = err.status;
|
|
412
|
+
if (err instanceof HTTPResponseException) {
|
|
413
|
+
status = err._response?.status ?? 500;
|
|
403
414
|
}
|
|
404
415
|
|
|
405
416
|
const stack = !IS_PROD && err.stack ? err.stack.split('\n').slice(1).join('\n') : '';
|
package/src/types.ts
CHANGED
|
@@ -49,37 +49,37 @@ export namespace Hyperspan {
|
|
|
49
49
|
delete: (name: string) => void;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export type HSRequest = {
|
|
53
|
+
url: URL;
|
|
54
|
+
raw: Request;
|
|
55
|
+
method: string; // Always uppercase
|
|
56
|
+
headers: Headers; // Case-insensitive
|
|
57
|
+
query: URLSearchParams;
|
|
58
|
+
cookies: Hyperspan.Cookies;
|
|
59
|
+
text: () => Promise<string>;
|
|
60
|
+
json<T = unknown>(): Promise<T>;
|
|
61
|
+
formData(): Promise<FormData>;
|
|
62
|
+
urlencoded(): Promise<URLSearchParams>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type HSResponse = {
|
|
66
|
+
cookies: Hyperspan.Cookies;
|
|
67
|
+
headers: Headers; // Headers to merge with final outgoing response
|
|
68
|
+
html: (html: string, options?: ResponseInit) => Response
|
|
69
|
+
json: (json: any, options?: ResponseInit) => Response;
|
|
70
|
+
text: (text: string, options?: ResponseInit) => Response;
|
|
71
|
+
redirect: (url: string, options?: ResponseInit) => Response;
|
|
72
|
+
error: (error: Error, options?: ResponseInit) => Response;
|
|
73
|
+
notFound: (options?: ResponseInit) => Response;
|
|
74
|
+
merge: (response: Response) => Response;
|
|
75
|
+
raw: Response;
|
|
76
|
+
};
|
|
77
|
+
|
|
52
78
|
export interface Context {
|
|
53
79
|
vars: Record<string, any>;
|
|
54
|
-
route:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
cssImports?: string[];
|
|
58
|
-
}
|
|
59
|
-
req: {
|
|
60
|
-
url: URL;
|
|
61
|
-
raw: Request;
|
|
62
|
-
method: string; // Always uppercase
|
|
63
|
-
headers: Headers; // Case-insensitive
|
|
64
|
-
query: URLSearchParams;
|
|
65
|
-
cookies: Hyperspan.Cookies;
|
|
66
|
-
text: () => Promise<string>;
|
|
67
|
-
json<T = unknown>(): Promise<T>;
|
|
68
|
-
formData(): Promise<FormData>;
|
|
69
|
-
urlencoded(): Promise<URLSearchParams>;
|
|
70
|
-
};
|
|
71
|
-
res: {
|
|
72
|
-
cookies: Hyperspan.Cookies;
|
|
73
|
-
headers: Headers; // Headers to merge with final outgoing response
|
|
74
|
-
html: (html: string, options?: { status?: number; headers?: Record<string, string> }) => Response
|
|
75
|
-
json: (json: any, options?: { status?: number; headers?: Record<string, string> }) => Response;
|
|
76
|
-
text: (text: string, options?: { status?: number; headers?: Record<string, string> }) => Response;
|
|
77
|
-
redirect: (url: string, options?: { status?: number; headers?: Record<string, string> }) => Response;
|
|
78
|
-
error: (error: Error, options?: { status?: number; headers?: Record<string, string> }) => Response;
|
|
79
|
-
notFound: (options?: { status?: number; headers?: Record<string, string> }) => Response;
|
|
80
|
-
merge: (response: Response) => Response;
|
|
81
|
-
raw: Response;
|
|
82
|
-
};
|
|
80
|
+
route: RouteConfig;
|
|
81
|
+
req: HSRequest;
|
|
82
|
+
res: HSResponse;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export type ClientIslandOptions = {
|
|
@@ -90,6 +90,7 @@ export namespace Hyperspan {
|
|
|
90
90
|
export type RouteConfig = {
|
|
91
91
|
name?: string;
|
|
92
92
|
path?: string;
|
|
93
|
+
params?: Record<string, string | undefined>;
|
|
93
94
|
cssImports?: string[];
|
|
94
95
|
};
|
|
95
96
|
export type RouteHandler = (context: Hyperspan.Context) => unknown;
|
package/src/utils.test.ts
CHANGED
|
@@ -149,13 +149,13 @@ describe('parsePath', () => {
|
|
|
149
149
|
test('parsePath handles catch-all param with spread', () => {
|
|
150
150
|
const result = parsePath('users/[...slug]');
|
|
151
151
|
expect(result.path).toBe('/users/*');
|
|
152
|
-
expect(result.params).toEqual(['slug']);
|
|
152
|
+
expect(result.params).toEqual(['...slug']);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
test('parsePath handles catch-all param at root', () => {
|
|
156
156
|
const result = parsePath('[...slug]');
|
|
157
157
|
expect(result.path).toBe('/*');
|
|
158
|
-
expect(result.params).toEqual(['slug']);
|
|
158
|
+
expect(result.params).toEqual(['...slug']);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
test('parsePath preserves param names in path but converts format', () => {
|
package/src/utils.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function parsePath(urlPath: string): { path: string, params: string[] } {
|
|
|
33
33
|
// Dynamic params
|
|
34
34
|
if (ROUTE_SEGMENT_REGEX.test(urlPath)) {
|
|
35
35
|
urlPath = urlPath.replace(ROUTE_SEGMENT_REGEX, (match: string) => {
|
|
36
|
-
const paramName = match.replace(/[^a-zA-Z_\.]+/g, '')
|
|
36
|
+
const paramName = match.replace(/[^a-zA-Z_\.]+/g, '');
|
|
37
37
|
params.push(paramName);
|
|
38
38
|
|
|
39
39
|
if (match.includes('...')) {
|