@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.12",
4
4
  "description": "Hyperspan Web Framework",
5
5
  "main": "src/server.ts",
6
6
  "types": "src/server.ts",
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createConfig, createContext, createRoute, createServer, getRunnableRoute, StreamResponse, IS_PROD, HTTPException } from './server';
1
+ export { createConfig, createContext, createRoute, createServer, getRunnableRoute, StreamResponse, IS_PROD, HTTPResponseException } from './server';
2
2
  export type { Hyperspan } from './types';
@@ -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 will overwrite the previous Set-Cookie header)
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('HttpOnly');
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 HTTPException extends Error {
12
- constructor(public status: number, message?: string) {
13
- super(message);
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?: { status?: number; headers?: Headers | Record<string, string> }) => merge(new Response(html, { ...options, headers: { 'Content-Type': 'text/html; charset=UTF-8', ...options?.headers } })),
78
- json: (json: any, options?: { status?: number; headers?: Headers | Record<string, string> }) => merge(new Response(JSON.stringify(json), { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers } })),
79
- text: (text: string, options?: { status?: number; headers?: Headers | Record<string, string> }) => merge(new Response(text, { ...options, headers: { 'Content-Type': 'text/plain; charset=UTF-8', ...options?.headers } })),
80
- redirect: (url: string, options?: { status?: number; headers?: Headers | Record<string, string> }) => merge(new Response(null, { status: 302, headers: { Location: url, ...options?.headers } })),
81
- error: (error: Error, options?: { status?: number; headers?: Headers | Record<string, string> }) => merge(new Response(error.message, { status: 500, ...options })),
82
- notFound: (options?: { status?: number; headers?: Headers | Record<string, string> }) => merge(new Response('Not Found', { status: 404, ...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 HTTPException) {
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
- path: string;
56
- params: Record<string, string>;
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, '').replace('...', '');
36
+ const paramName = match.replace(/[^a-zA-Z_\.]+/g, '');
37
37
  params.push(paramName);
38
38
 
39
39
  if (match.includes('...')) {