diff --git a/package.json b/package.json index 8b61c2b..3b6f2de 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" diff --git a/src/core/router.ts b/src/core/router.ts index 0b28bb3..fa51de9 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -89,7 +89,7 @@ export class Router { * return ctx.json({ users: [] }); * }); */ - get(path: string, ...handlers: VoltenHandler[]): void; + get

(path: P, ...handlers: VoltenHandler

[]): void; /** * Registers a GET route with custom route options and handlers. * @@ -97,9 +97,16 @@ export class Router { * @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

(path: P, options: RouteOptions, ...handlers: VoltenHandler

[]): void; + get

( + path: P, + arg2: RouteOptions | VoltenHandler

, + ...handlers: VoltenHandler

[] + ): 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 }); } @@ -116,7 +123,7 @@ export class Router { * return ctx.status(201).json({ created: true }); * }); */ - post(path: string, ...handlers: VoltenHandler[]): void; + post

(path: P, ...handlers: VoltenHandler

[]): void; /** * Registers a POST route with custom route options and handlers. * @@ -124,9 +131,16 @@ export class Router { * @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

(path: P, options: RouteOptions, ...handlers: VoltenHandler

[]): void; + post

( + path: P, + arg2: RouteOptions | VoltenHandler

, + ...handlers: VoltenHandler

[] + ): 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 }); } @@ -142,7 +156,7 @@ export class Router { * return ctx.json({ updated: true }); * }); */ - patch(path: string, ...handlers: VoltenHandler[]): void; + patch

(path: P, ...handlers: VoltenHandler

[]): void; /** * Registers a PATCH route with custom route options and handlers. * @@ -150,9 +164,16 @@ export class Router { * @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

(path: P, options: RouteOptions, ...handlers: VoltenHandler

[]): void; + patch

( + path: P, + arg2: RouteOptions | VoltenHandler

, + ...handlers: VoltenHandler

[] + ): 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 }); } @@ -168,7 +189,7 @@ export class Router { * return ctx.json({ replaced: true }); * }); */ - put(path: string, ...handlers: VoltenHandler[]): void; + put

(path: P, ...handlers: VoltenHandler

[]): void; /** * Registers a PUT route with custom route options and handlers. * @@ -176,9 +197,16 @@ export class Router { * @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

(path: P, options: RouteOptions, ...handlers: VoltenHandler

[]): void; + put

( + path: P, + arg2: RouteOptions | VoltenHandler

, + ...handlers: VoltenHandler

[] + ): 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 }); } @@ -194,7 +222,7 @@ export class Router { * return ctx.json({ deleted: true }); * }); */ - delete(path: string, ...handlers: VoltenHandler[]): void; + delete

(path: P, ...handlers: VoltenHandler

[]): void; /** * Registers a DELETE route with custom route options and handlers. * @@ -202,9 +230,16 @@ export class Router { * @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

(path: P, options: RouteOptions, ...handlers: VoltenHandler

[]): void; + delete

( + path: P, + arg2: RouteOptions | VoltenHandler

, + ...handlers: VoltenHandler

[] + ): 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 }); } diff --git a/src/core/types.ts b/src/core/types.ts index 8e527e7..cf6f6c8 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -6,15 +6,42 @@ import { Readable } from "stream"; export type Next = () => Promise | void; -export type VoltenHandler = (ctx: RequestContext, next: Next) => Promise | void; +type ExtractParamKeys = T extends `${string}:${infer Param}/${infer Rest}` + ? Param | ExtractParamKeys + : T extends `${string}:${infer Param}` + ? Param + : T extends `${string}*${infer Rest}` + ? "*" | ExtractParamKeys + : never; + +export type ExtractParams = string extends T + ? Record + : [ExtractParamKeys] extends [never] + ? Record + : { [K in ExtractParamKeys]: string }; + +export type VoltenHandler

= ( + ctx: RequestContext

, + next: Next, +) => Promise | void; -export type VoltenChainHandler = (ctx: RequestContext) => Promise | void; +export type VoltenChainHandler

= ( + ctx: RequestContext

, +) => Promise | void; -export type PreflightHandler = (ctx: RequestContext) => Promise | void; +export type PreflightHandler

= ( + ctx: RequestContext

, +) => Promise | void; -export type ErrorHandler = (err: VoltenError, ctx: RequestContext) => Promise | void; +export type ErrorHandler

= ( + err: VoltenError, + ctx: RequestContext

, +) => Promise | void; -export type DefaultErrorHandler = (err: VoltenError, ctx: RequestContext) => void; +export type DefaultErrorHandler

= ( + err: VoltenError, + ctx: RequestContext

, +) => void; export type NativeErrorHandler = ( err: VoltenError, @@ -22,7 +49,7 @@ export type NativeErrorHandler = ( res: ServerResponse, ) => Promise | void; -export type Params = Record; +export type Params = Record; export type Query = Record; export type SerializerFn = (data: unknown, ctx?: unknown) => string; diff --git a/src/utils/requestCtx.ts b/src/utils/requestCtx.ts index f208eec..c49e2a3 100644 --- a/src/utils/requestCtx.ts +++ b/src/utils/requestCtx.ts @@ -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"; @@ -31,7 +31,7 @@ const timer = setInterval(() => { }, 1000); timer.unref(); -export class RequestContext { +export class RequestContext

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

= Object.create(null) as ExtractParams

; public inited: boolean = false; private queryString!: string; @@ -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

; const headers = req.headers; this._headers = headers; this.method = req.method ?? "GET"; @@ -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

; this.state = {}; this.queryValue = null; this._bodyPromise = undefined; diff --git a/tests/unit/core/server.test.ts b/tests/unit/core/server.test.ts index 369326d..87940d6 100644 --- a/tests/unit/core/server.test.ts +++ b/tests/unit/core/server.test.ts @@ -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"); diff --git a/tests/unit/core/types.test.ts b/tests/unit/core/types.test.ts new file mode 100644 index 0000000..496dec3 --- /dev/null +++ b/tests/unit/core/types.test.ts @@ -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 = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; + +test("TS Type Param Extraction: resolves correct types", () => { + const t1: Equals, Record> = true; + assert.ok(t1); + + const t2: Equals, { 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, { "*": 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"); + }); +});