From 2559423e5c45b3a3b6a5b75d2b083af11de5e2d8 Mon Sep 17 00:00:00 2001 From: Kevin Whitley Date: Thu, 14 Dec 2023 22:39:22 -0600 Subject: [PATCH] fixed request/args TS flow?! --- example/bun-flow-advanced.ts | 4 +- example/e2e middleware.ts | 21 ++++++-- example/universal-vs-overrides.ts | 52 +++++++++++++++++++ src/Router.ts | 85 +++++++++++++------------------ 4 files changed, 107 insertions(+), 55 deletions(-) create mode 100644 example/universal-vs-overrides.ts diff --git a/example/bun-flow-advanced.ts b/example/bun-flow-advanced.ts index b4fa86ba..048783fb 100644 --- a/example/bun-flow-advanced.ts +++ b/example/bun-flow-advanced.ts @@ -12,12 +12,12 @@ type CustomRequest = { type RouterArgs = [env: Env] -const router = Router() +const router = Router() router .get('/test', () => 'Success!') .get('/type-check', (request, env) => { - request + request. env. }) .get('/foo/:bar/:baz?', ({ bar, baz }) => ({ bar, baz })) diff --git a/example/e2e middleware.ts b/example/e2e middleware.ts index dbc5a34f..339a3f22 100644 --- a/example/e2e middleware.ts +++ b/example/e2e middleware.ts @@ -1,19 +1,32 @@ -import { IRequest, Router } from 'Router' +import { IRequest, IRequestStrict, Router } from 'Router' type FooRequest = { foo: string } & IRequest -const router = Router() +type HasFoo = { + foo?: string +} & IRequest + +type HasBar = { + bar?: string +} & IRequestStrict + +const router = Router() // MIDDLEWARE: adds foo to the request -const withFoo = (request: IRequest) => { +const withFoo = (request: HasFoo & IRequest) => { request.foo = 'bar' // no return = next handler gets fired (making this middleware) } +// MIDDLEWARE: adds foo to the request +const withBar = (request: HasBar & IRequest) => { + request.bar = 'baz' // no return = next handler gets fired (making this middleware) +} + // ADD ROUTES router - .all('*', withFoo) // we add Foo as global upstream middleware, but this could go anywhere + .all('*', withFoo, withBar) // we add Foo as global upstream middleware, but this could go anywhere .get('/foo-test', (request) => { return request.foo // 'bar' diff --git a/example/universal-vs-overrides.ts b/example/universal-vs-overrides.ts new file mode 100644 index 00000000..fe001ebd --- /dev/null +++ b/example/universal-vs-overrides.ts @@ -0,0 +1,52 @@ +// WITH FLOW (overriding options, and enabling CORS) +import { Router, error, flow } from '../src' +import { IRequest, IRequestStrict } from '../src' + +type Env = { + KV: object +} + +type CustomRequest = { + foo: string +} & IRequestStrict + +type RouterArgs = [env: Env] +type RouterArgs2 = [env: Env, ctx: ExecutionContext] + +const universalRouter = + Router() + .get('/type-check', (request, env) => { + request.foo // should be found + env.KV // should be found + env.KB // should NOT be found + }) + .get('/type-check', (request, env) => { + request.foo // should NOT be found + env.KB // should NOT be found ******* + env.KV // should be found ******* + }) + .puppy('/type-check', (request, env) => { + request.foo // should be found + env.KV // should be found + env.KB // should NOT be found + }) + .puppy('/type-check', (request, env) => { + request.foo // should be found + env.KV // should be found + env.KB // should NOT be found + }) + +const overrideRouter = + Router() + .get('/type-check', (request, env, ctx) => { + request.whatever // should ignore + env.whatever // should ignore + ctx.whatever // should ignore + }) + .get('/type-check', (request, env, ctx) => { + request.foo // should NOT be found + ctx.waitUntil // should infer + }) + .puppy() + + universalRouter.handle({ method: 'GET', url: '' }, {}) diff --git a/src/Router.ts b/src/Router.ts index 9e52aa7c..772c1070 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -1,6 +1,4 @@ -export type GenericTraps = { - [key: string]: any -} +export type GenericTraps = Record export type RequestLike = { method: string, @@ -27,74 +25,63 @@ export type RouterOptions = { routes?: RouteEntry[] } -export type RouteHandler = { +export type RouteHandler = { // @ts-expect-error - TS never likes this syntax - (request: I, ...args: Args): any + (request: R, ...args: Args): any } export type RouteEntry = [string, RegExp, RouteHandler[], string] // this is the generic "Route", which allows per-route overrides -export type Route = ( - path: string, - ...handlers: RouteHandler[] -) => RT - -// this is an alternative UniveralRoute, accepting generics (from upstream), but without -// per-route overrides -export type UniversalRoute = ( +export type Route = ( path: string, ...handlers: RouteHandler[] -) => RouterType, Args> - -// helper function to detect equality in types (used to detect custom Request on router) -type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false - -// used to determine if router generics have been modified -type IsUntouched = Args extends [] ? Equal extends true ? true : false : false + // @ts-ignore foo +) => RouterType export type CustomRoutes = { [key: string]: R, } -export type RouterType = { +export type RouterType = { __proto__: RouterType, routes: RouteEntry[], - handle: (request: RequestLike, ...extra: Equal extends true ? A : Args) => Promise - all: R, - delete: R, - get: R, - head: R, - options: R, - patch: R, - post: R, - put: R, -} & CustomRoutes + handle: (request: RequestLike, ...extra: Args) => Promise + all: Route, + delete: Route, + get: Route, + head: Route, + options: Route, + patch: Route, + post: Route, + put: Route, +} & CustomRoutes> export const Router = < RequestType = IRequest, - Args extends any[] = any[], - RouteType = IsUntouched extends true ? Route : UniversalRoute ->({ base = '', routes = [] }: RouterOptions = {}): RouterType => + Args extends any[] = any[] +>({ base = '', routes = [] }: RouterOptions = {}): RouterType => // @ts-expect-error TypeScript doesn't know that Proxy makes this work ({ __proto__: new Proxy({}, { // @ts-expect-error (we're adding an expected prop "path" to the get) - get: (target: any, prop: string, receiver: object, path: string) => (route: string, ...handlers: RouteHandler[]) => - routes.push( - [ - prop.toUpperCase(), - RegExp(`^${(path = (base + route) - .replace(/\/+(\/|$)/g, '$1')) // strip double & trailing splash - .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))') // greedy params - .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))') // named params and image format - .replace(/\./g, '\\.') // dot in path - .replace(/(\/?)\*/g, '($1.*)?') // wildcard - }/*$`), - handlers, // embed handlers - path, // embed clean route path - ] - ) && receiver + get: (target: any, prop: string, receiver: object, path: string) => + (route: string, ...handlers: RouteHandler[]) => + routes.push( + [ + prop.toUpperCase(), + RegExp(`^${(path = (base + route) + .replace(/\/+(\/|$)/g, '$1')) // strip double & trailing splash + .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))') // greedy params + .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))') // named params and image format + .replace(/\./g, '\\.') // dot in path + .replace(/(\/?)\*/g, '($1.*)?') // wildcard + }/*$`), + // @ts-ignore why not + handlers, // embed handlers + path, // embed clean route path + ] + ) && receiver }), routes, async handle (request: RequestLike, ...args) {