Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hyperspan",
"version": "1.0.9",
"version": "1.0.10",
"description": "Hyperspan CLI - for @hyperspan/framework",
"type": "module",
"public": true,
Expand Down Expand Up @@ -39,8 +39,8 @@
"degit": "^2.8.4"
},
"devDependencies": {
"@types/bun": "^1.3.9",
"@types/bun": "^1.3.13",
"@types/degit": "^2.8.6",
"@types/node": "^24.10.13"
"@types/node": "^24.12.2"
}
}
1 change: 1 addition & 0 deletions packages/cli/src/runtimes/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function startBunServer(server: HS.Server) {

// Add main route
routes[path] = (request: Request) => {
log(`Matched route: ${path}`);
return route.fetch(request);
}

Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export async function addDirectoryAsRoutes(
const buildDir = join(CWD, '.build');
const cssPublicDir = join(CWD, server._config.publicDir, CSS_PUBLIC_PATH);

// Read local package.json to get the dependencies, so we can exclude them from the build for CSS below.
const packageJson = await Bun.file(join(CWD, 'package.json')).json();
const dependencies = packageJson.dependencies;

log(`Scanning directory for routes: ${directoryPath}`);

try {
Expand Down Expand Up @@ -137,7 +141,7 @@ export async function addDirectoryAsRoutes(
target: 'node',
// Only extract CSS — don't bundle framework packages or component files.
// Under bun --watch, bundling @hyperspan/* causes EISDIR path collisions.
external: ['@hyperspan/*', '*.vue', '*.svelte', '*.tsx', '*.jsx'],
external: [...Object.keys(dependencies), '@hyperspan/*', '*.vue', '*.svelte', '*.tsx', '*.jsx', '*.ts', '*.js'],
});

// Move CSS files to the public directory
Expand Down
12 changes: 12 additions & 0 deletions packages/framework/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ test('returns 405 when route path matches but HTTP method does not', async () =>
expect(text).toContain('Method not allowed');
});

test('createContext() req.ip reflects RouteFetchOptions', () => {
const request = new Request('http://localhost:3000/');
expect(createContext(request).req.ip).toBe('');
expect(createContext(request, undefined, { ip: '203.0.113.1' }).req.ip).toBe('203.0.113.1');
});

test('route.fetch forwards ip via RouteFetchOptions', async () => {
const route = createRoute().get((c: HS.Context) => c.res.text(c.req.ip));
const response = await route.fetch(new Request('http://localhost:3000/'), { ip: '198.51.100.2' });
expect(await response.text()).toBe('198.51.100.2');
});

test('createContext() can get and set cookies', () => {
// Create a request with cookies in the Cookie header
const request = new Request('http://localhost:3000/', {
Expand Down
8 changes: 5 additions & 3 deletions packages/framework/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export function createConfig(config: Partial<HS.Config> = {}): HS.Config {
/**
* Creates a context object for a request
*/
export function createContext(req: Request, route?: HS.Route): HS.Context {
export function createContext(req: Request, route?: HS.Route, options?: HS.RouteFetchOptions): HS.Context {
const clientIp = options?.ip ?? '';
const url = new URL(req.url);
const query = new URLSearchParams(url.search);
const method = req.method.toUpperCase();
Expand Down Expand Up @@ -92,6 +93,7 @@ export function createContext(req: Request, route?: HS.Route): HS.Context {
headers,
query,
cookies: new Cookies(req),
ip: clientIp,
async text() { return req.clone().text() },
async json<T = unknown>() { return await req.clone().json() as T },
async formData<T = unknown>() { return await req.clone().formData() as T },
Expand Down Expand Up @@ -240,8 +242,8 @@ export function createRoute(config: Partial<HS.RouteConfig> = {}): HS.Route {
/**
* Fetch - handle a direct HTTP request
*/
async fetch(request: Request) {
const context = createContext(request, api);
async fetch(request: Request, fetchOptions?: HS.RouteFetchOptions) {
const context = createContext(request, api, fetchOptions);
const method = context.req.method as HS.MiddlewareMethod;
const globalMiddleware = api._middleware['*'] || [];
const methodMiddleware = api._middleware[method] || [];
Expand Down
9 changes: 7 additions & 2 deletions packages/framework/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,18 @@ export namespace Hyperspan {
delete: (name: string) => void;
}

export type RouteFetchOptions = {
ip?: string;
};

export type HSRequest = {
url: URL;
raw: Request;
method: string; // Always uppercase
headers: Headers; // Case-insensitive
query: URLSearchParams;
cookies: Hyperspan.Cookies;
ip: string;
text: () => Promise<string>;
json<T = unknown>(): Promise<T>;
formData(): Promise<FormData>;
Expand Down Expand Up @@ -169,7 +174,7 @@ export namespace Hyperspan {
errorHandler: (handler: Hyperspan.ErrorHandler) => Hyperspan.Route;
use: (middleware: Hyperspan.MiddlewareFunction, opts?: Hyperspan.MiddlewareMethodOptions) => Hyperspan.Route;
middleware: (middleware: Array<Hyperspan.MiddlewareFunction>, opts?: Hyperspan.MiddlewareMethodOptions) => Hyperspan.Route;
fetch: (request: Request) => Promise<Response>;
fetch: (request: Request, options?: Hyperspan.RouteFetchOptions) => Promise<Response>;
};

/**
Expand Down Expand Up @@ -199,7 +204,7 @@ export namespace Hyperspan {
errorHandler: (handler: ActionFormHandler<T>) => Action<T>;
use: (middleware: Hyperspan.MiddlewareFunction, opts?: Hyperspan.MiddlewareMethodOptions) => Action<T>;
middleware: (middleware: Array<Hyperspan.MiddlewareFunction>, opts?: Hyperspan.MiddlewareMethodOptions) => Action<T>;
fetch: (request: Request) => Promise<Response>;
fetch: (request: Request, options?: Hyperspan.RouteFetchOptions) => Promise<Response>;
}

/**
Expand Down
Loading