Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "volten",
"packageManager": "pnpm@11.0.0",
"version": "0.0.2",
"version": "0.0.3",
"description": "A 0-dependency Node.js HTTP framework that is simple, modern, and fast.",
"author": "insanerx",
"license": "MIT",
Expand Down Expand Up @@ -37,7 +37,7 @@
"dev": "nodemon",
"test": "tsx tests/run.ts",
"lint": "eslint src/**/*.ts",
"format": "prettier --write \"src/**/*.ts\"",
"format": "prettier --write \"{src,tests}/**/*.ts\"",
"check": "pnpm run lint && pnpm run format",
"pack:npm": "node scripts/pack.js",
"release": "node scripts/release.js"
Expand Down
75 changes: 55 additions & 20 deletions src/core/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,24 @@ export class Router {
* return ctx.json({ users: [] });
* });
*/
get(path: string, ...handlers: VoltenHandler[]): void;
get<P extends string>(path: P, ...handlers: VoltenHandler<P>[]): void;
/**
* Registers a GET route with custom route options and handlers.
*
* @param {string} path - The route path pattern.
* @param {RouteOptions} options - Route options config (e.g. body limit).
* @param {...VoltenHandler[]} handlers - One or more handler functions.
*/
get(path: string, options: RouteOptions, ...handlers: VoltenHandler[]): void;
get(path: string, arg2: RouteOptions | VoltenHandler, ...handlers: VoltenHandler[]): void {
const { options, routeHandlers } = this.identifyParamType(arg2, ...handlers);
get<P extends string>(path: P, options: RouteOptions, ...handlers: VoltenHandler<P>[]): void;
get<P extends string>(
path: P,
arg2: RouteOptions | VoltenHandler<P>,
...handlers: VoltenHandler<P>[]
): void {
const { options, routeHandlers } = this.identifyParamType(
arg2 as RouteOptions | VoltenHandler,
...(handlers as unknown as VoltenHandler[]),
);
const handlersWithMiddleware = [...this.middleware, ...routeHandlers];
this.routes.push({ method: "GET", path, options, handlers: handlersWithMiddleware });
}
Expand All @@ -116,17 +123,24 @@ export class Router {
* return ctx.status(201).json({ created: true });
* });
*/
post(path: string, ...handlers: VoltenHandler[]): void;
post<P extends string>(path: P, ...handlers: VoltenHandler<P>[]): void;
/**
* Registers a POST route with custom route options and handlers.
*
* @param {string} path - The route path pattern.
* @param {RouteOptions} options - Route options config (e.g. body limit).
* @param {...VoltenHandler[]} handlers - One or more handler functions.
*/
post(path: string, options: RouteOptions, ...handlers: VoltenHandler[]): void;
post(path: string, arg2: RouteOptions | VoltenHandler, ...handlers: VoltenHandler[]): void {
const { options, routeHandlers } = this.identifyParamType(arg2, ...handlers);
post<P extends string>(path: P, options: RouteOptions, ...handlers: VoltenHandler<P>[]): void;
post<P extends string>(
path: P,
arg2: RouteOptions | VoltenHandler<P>,
...handlers: VoltenHandler<P>[]
): void {
const { options, routeHandlers } = this.identifyParamType(
arg2 as RouteOptions | VoltenHandler,
...(handlers as unknown as VoltenHandler[]),
);
const handlersWithMiddleware = [...this.middleware, ...routeHandlers];
this.routes.push({ method: "POST", path, options, handlers: handlersWithMiddleware });
}
Expand All @@ -142,17 +156,24 @@ export class Router {
* return ctx.json({ updated: true });
* });
*/
patch(path: string, ...handlers: VoltenHandler[]): void;
patch<P extends string>(path: P, ...handlers: VoltenHandler<P>[]): void;
/**
* Registers a PATCH route with custom route options and handlers.
*
* @param {string} path - The route path pattern.
* @param {RouteOptions} options - Route options config (e.g. body limit).
* @param {...VoltenHandler[]} handlers - One or more handler functions.
*/
patch(path: string, options: RouteOptions, ...handlers: VoltenHandler[]): void;
patch(path: string, arg2: RouteOptions | VoltenHandler, ...handlers: VoltenHandler[]): void {
const { options, routeHandlers } = this.identifyParamType(arg2, ...handlers);
patch<P extends string>(path: P, options: RouteOptions, ...handlers: VoltenHandler<P>[]): void;
patch<P extends string>(
path: P,
arg2: RouteOptions | VoltenHandler<P>,
...handlers: VoltenHandler<P>[]
): void {
const { options, routeHandlers } = this.identifyParamType(
arg2 as RouteOptions | VoltenHandler,
...(handlers as unknown as VoltenHandler[]),
);
const handlersWithMiddleware = [...this.middleware, ...routeHandlers];
this.routes.push({ method: "PATCH", path, options, handlers: handlersWithMiddleware });
}
Expand All @@ -168,17 +189,24 @@ export class Router {
* return ctx.json({ replaced: true });
* });
*/
put(path: string, ...handlers: VoltenHandler[]): void;
put<P extends string>(path: P, ...handlers: VoltenHandler<P>[]): void;
/**
* Registers a PUT route with custom route options and handlers.
*
* @param {string} path - The route path pattern.
* @param {RouteOptions} options - Route options config (e.g. body limit).
* @param {...VoltenHandler[]} handlers - One or more handler functions.
*/
put(path: string, options: RouteOptions, ...handlers: VoltenHandler[]): void;
put(path: string, arg2: RouteOptions | VoltenHandler, ...handlers: VoltenHandler[]): void {
const { options, routeHandlers } = this.identifyParamType(arg2, ...handlers);
put<P extends string>(path: P, options: RouteOptions, ...handlers: VoltenHandler<P>[]): void;
put<P extends string>(
path: P,
arg2: RouteOptions | VoltenHandler<P>,
...handlers: VoltenHandler<P>[]
): void {
const { options, routeHandlers } = this.identifyParamType(
arg2 as RouteOptions | VoltenHandler,
...(handlers as unknown as VoltenHandler[]),
);
const handlersWithMiddleware = [...this.middleware, ...routeHandlers];
this.routes.push({ method: "PUT", path, options, handlers: handlersWithMiddleware });
}
Expand All @@ -194,17 +222,24 @@ export class Router {
* return ctx.json({ deleted: true });
* });
*/
delete(path: string, ...handlers: VoltenHandler[]): void;
delete<P extends string>(path: P, ...handlers: VoltenHandler<P>[]): void;
/**
* Registers a DELETE route with custom route options and handlers.
*
* @param {string} path - The route path pattern.
* @param {RouteOptions} options - Route options config.
* @param {...VoltenHandler[]} handlers - One or more handler functions.
*/
delete(path: string, options: RouteOptions, ...handlers: VoltenHandler[]): void;
delete(path: string, arg2: RouteOptions | VoltenHandler, ...handlers: VoltenHandler[]): void {
const { options, routeHandlers } = this.identifyParamType(arg2, ...handlers);
delete<P extends string>(path: P, options: RouteOptions, ...handlers: VoltenHandler<P>[]): void;
delete<P extends string>(
path: P,
arg2: RouteOptions | VoltenHandler<P>,
...handlers: VoltenHandler<P>[]
): void {
const { options, routeHandlers } = this.identifyParamType(
arg2 as RouteOptions | VoltenHandler,
...(handlers as unknown as VoltenHandler[]),
);
const handlersWithMiddleware = [...this.middleware, ...routeHandlers];
this.routes.push({ method: "DELETE", path, options, handlers: handlersWithMiddleware });
}
Expand Down
39 changes: 33 additions & 6 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,50 @@ import { Readable } from "stream";

export type Next = () => Promise<void> | void;

export type VoltenHandler = (ctx: RequestContext, next: Next) => Promise<void> | void;
type ExtractParamKeys<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParamKeys<Rest>
: T extends `${string}:${infer Param}`
? Param
: T extends `${string}*${infer Rest}`
? "*" | ExtractParamKeys<Rest>
: never;

export type ExtractParams<T extends string> = string extends T
? Record<string, string>
: [ExtractParamKeys<T>] extends [never]
? Record<string, never>
: { [K in ExtractParamKeys<T>]: string };

export type VoltenHandler<P extends string = string> = (
ctx: RequestContext<P>,
next: Next,
) => Promise<void> | void;

export type VoltenChainHandler = (ctx: RequestContext) => Promise<void> | void;
export type VoltenChainHandler<P extends string = string> = (
ctx: RequestContext<P>,
) => Promise<void> | void;

export type PreflightHandler = (ctx: RequestContext) => Promise<void> | void;
export type PreflightHandler<P extends string = string> = (
ctx: RequestContext<P>,
) => Promise<void> | void;

export type ErrorHandler = (err: VoltenError, ctx: RequestContext) => Promise<void> | void;
export type ErrorHandler<P extends string = string> = (
err: VoltenError,
ctx: RequestContext<P>,
) => Promise<void> | void;

export type DefaultErrorHandler = (err: VoltenError, ctx: RequestContext) => void;
export type DefaultErrorHandler<P extends string = string> = (
err: VoltenError,
ctx: RequestContext<P>,
) => void;

export type NativeErrorHandler = (
err: VoltenError,
req: IncomingMessage,
res: ServerResponse,
) => Promise<void> | void;

export type Params = Record<string, unknown>;
export type Params = Record<string, string>;
export type Query = Record<string, string | string[]>;

export type SerializerFn = (data: unknown, ctx?: unknown) => string;
Expand Down
10 changes: 5 additions & 5 deletions src/utils/requestCtx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import type {
PathData,
JSONResponseOptions,
SendFileOptions,
Params,
ErrorHandler,
CookieOptions,
MultipartPart,
ExtractParams,
} from "../core/types.ts";
import { App } from "../core/server.ts";
import { parseUrl, parseQuery } from "./parseUrl.ts";
Expand All @@ -31,7 +31,7 @@ const timer = setInterval(() => {
}, 1000);
timer.unref();

export class RequestContext {
export class RequestContext<P extends string = string> {
public _app: App<string> | null = null;
private _req: http.IncomingMessage | null = null;
private _res: http.ServerResponse | null = null;
Expand All @@ -43,7 +43,7 @@ export class RequestContext {
public path!: string;
public _headers: http.IncomingHttpHeaders | null = null;
public state: Record<string, unknown> = {};
public params: Params = Object.create(null) as Params;
public params: ExtractParams<P> = Object.create(null) as ExtractParams<P>;
public inited: boolean = false;

private queryString!: string;
Expand Down Expand Up @@ -83,7 +83,7 @@ export class RequestContext {
this.queryString = queryStr;

this.queryValue = null;
this.params = Object.create(null) as Params;
this.params = Object.create(null) as ExtractParams<P>;
const headers = req.headers;
this._headers = headers;
this.method = req.method ?? "GET";
Expand Down Expand Up @@ -152,7 +152,7 @@ export class RequestContext {
this._res = null;
this._route = null;
this._headers = null;
this.params = Object.create(null) as Params;
this.params = Object.create(null) as ExtractParams<P>;
this.state = {};
this.queryValue = null;
this._bodyPromise = undefined;
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/core/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function captureLogs(fn: () => void): any[] {

test("App logger: default logger works and logs warn level by default", () => {
const app = new App();

const logs = captureLogs(() => {
app.logger.info("should not log");
app.logger.warn("this is a warning");
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/core/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { App } from "../../../src/core/server.ts";
import type { ExtractParams } from "../../../src/core/types.ts";

type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;

test("TS Type Param Extraction: resolves correct types", () => {
const t1: Equals<ExtractParams<"/users">, Record<string, never>> = true;
assert.ok(t1);

const t2: Equals<ExtractParams<"/users/:id">, { id: string }> = true;
assert.ok(t2);

const t3: Equals<
ExtractParams<"/users/:id/posts/:postId">,
{ id: string; postId: string }
> = true;
assert.ok(t3);

const t4: Equals<ExtractParams<"/files/*">, { "*": string }> = true;
assert.ok(t4);
});

test("Router path parameter TS type inference compiles", () => {
const app = new App();
app.get("/user/:id/posts/:postId", (ctx) => {
const id: string = ctx.params.id;
const postId: string = ctx.params.postId;

assert.equal(typeof id, "undefined"); // in this mock test context it's undefined
assert.equal(typeof postId, "undefined");
});
});
Loading