diff --git a/packages/framework/src/define-handler.test.ts b/packages/framework/src/define-handler.test.ts new file mode 100644 index 0000000..b065548 --- /dev/null +++ b/packages/framework/src/define-handler.test.ts @@ -0,0 +1,109 @@ +import { expect, expectTypeOf, test } from "vitest"; + +import { defineHandler } from "./define-handler"; +import type { HandlerModule } from "./index"; + +declare module "./index" { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface RouteMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + "/api/health": Record; + // eslint-disable-next-line @typescript-eslint/naming-convention + "/api/users/[id]": { id: string }; + } +} + +// --- TDD Slice 1: identity at runtime --- + +test("defineHandler returns config unchanged (identity)", () => { + const config = { + // eslint-disable-next-line @typescript-eslint/naming-convention + GET: () => Response.json({ ok: true }), + }; + + const result = defineHandler("/api/health")(config); + + expect(result).toBe(config); +}); + +// --- TDD Slice 2: supports all HTTP methods --- + +test("defineHandler accepts all HTTP method handlers", () => { + const config = { + // eslint-disable-next-line @typescript-eslint/naming-convention + GET: () => new Response("get"), + // eslint-disable-next-line @typescript-eslint/naming-convention + POST: () => new Response("post"), + // eslint-disable-next-line @typescript-eslint/naming-convention + PUT: () => new Response("put"), + // eslint-disable-next-line @typescript-eslint/naming-convention + PATCH: () => new Response("patch"), + // eslint-disable-next-line @typescript-eslint/naming-convention + DELETE: () => new Response("delete"), + }; + + const result = defineHandler("/api/health")(config); + + expect(result).toBe(config); +}); + +// --- TDD Slice 3: all methods are optional --- + +test("defineHandler allows omitting all methods", () => { + const result = defineHandler("/api/health")({}); + + expect(result).toEqual({}); +}); + +// --- TDD Slice 4: type — params are typed from RouteMap --- + +test("handler receives typed params from RouteMap", () => { + defineHandler("/api/users/[id]")({ + // eslint-disable-next-line @typescript-eslint/naming-convention + GET: (ctx) => { + expectTypeOf(ctx.params).toEqualTypeOf<{ id: string }>(); + expectTypeOf(ctx.request).toEqualTypeOf(); + return Response.json({ id: ctx.params.id }); + }, + }); +}); + +// --- TDD Slice 5: type — return type is HandlerModule --- + +test("defineHandler returns a HandlerModule", () => { + const result = defineHandler("/api/users/[id]")({ + // eslint-disable-next-line @typescript-eslint/naming-convention + GET: () => Response.json({ ok: true }), + }); + + expectTypeOf(result).toExtend>(); +}); + +// --- TDD Slice 6: type — handlers accept async functions --- + +test("handlers can return Promise", () => { + const result = defineHandler("/api/health")({ + // eslint-disable-next-line @typescript-eslint/naming-convention + POST: async (ctx) => { + const body = (await ctx.request.json()) as Record; + return Response.json({ received: body }); + }, + }); + + expectTypeOf(result.POST).toExtend< + | ((ctx: { request: Request; params: Record }) => Response | Promise) + | undefined + >(); +}); + +// --- TDD Slice 7: type — paramless route has empty params --- + +test("paramless route has Record params", () => { + defineHandler("/api/health")({ + // eslint-disable-next-line @typescript-eslint/naming-convention + GET: (ctx) => { + expectTypeOf(ctx.params).toEqualTypeOf>(); + return Response.json({ ok: true }); + }, + }); +}); diff --git a/packages/framework/src/define-handler.ts b/packages/framework/src/define-handler.ts new file mode 100644 index 0000000..e91f740 --- /dev/null +++ b/packages/framework/src/define-handler.ts @@ -0,0 +1,5 @@ +import type { HandlerModule, RouteMap } from "./core/index"; + +export function defineHandler(_path: TPath) { + return (config: HandlerModule) => config; +} diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index 61ece85..008d033 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -2,6 +2,7 @@ export const VERSION = "0.0.0"; export { createApp } from "./create-app"; export type { AppConfig } from "./create-app"; +export { defineHandler } from "./define-handler"; export { definePage } from "./define-page"; export type {