Skip to content

Commit

Permalink
flowrouter is the new router and ittyrouter is the old router
Browse files Browse the repository at this point in the history
  • Loading branch information
kwhitley committed Mar 14, 2024
1 parent f1dda38 commit 45fea10
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 109 deletions.
12 changes: 12 additions & 0 deletions example/bun-autorouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AutoRouter } from '../src/AutoRouter'

const router = AutoRouter({ port: 3001 })

router
.get('/basic', () => new Response('Success!'))
.get('/text', () => 'Success!')
.get('/params/:foo', ({ foo }) => foo)
.get('/json', () => ({ foo: 'bar' }))
.get('/throw', (a) => a.b.c)

export default router
22 changes: 22 additions & 0 deletions example/bun-flowrouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FlowRouter } from '../src/Router'
import { error } from '../src/error'
import { json } from '../src/json'
import { withParams } from '../src/withParams'

const router = FlowRouter({
port: 3001,
before: [withParams],
onError: [error],
after: [json],
missing: () => error(404, 'Are you sure about that?'),
})

router
.get('/basic', () => new Response('Success!'))
.get('/text', () => 'Success!')
.get('/params/:foo', ({ foo }) => foo)
.get('/json', () => ({ foo: 'bar' }))
.get('/throw', (a) => a.b.c)
// .all('*', () => error(404)) // still works

export default router
2 changes: 1 addition & 1 deletion example/request-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IRequest, IRequestStrict, Router } from '../src/Router'
import { IRequest, IRequestStrict, Router } from '../src/IttyRouter'

type FooRequest = {
foo: string
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"require": "./index.js",
"types": "./index.d.ts"
},
"./AutoRouter": {
"import": "./AutoRouter.mjs",
"require": "./AutoRouter.js",
"types": "./AutoRouter.d.ts"
},
"./createCors": {
"import": "./createCors.mjs",
"require": "./createCors.js",
Expand All @@ -26,6 +31,11 @@
"require": "./error.js",
"types": "./error.d.ts"
},
"./FlowRouter": {
"import": "./FlowRouter.mjs",
"require": "./FlowRouter.js",
"types": "./FlowRouter.d.ts"
},
"./html": {
"import": "./html.mjs",
"require": "./html.js",
Expand Down
12 changes: 12 additions & 0 deletions src/AutoRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { error } from 'error'
import { json } from 'json'
import { withParams } from 'withParams'
import { Router, RouterOptions} from './Router'

export const AutoRouter = (options?: RouterOptions) => Router({
before: [withParams],
onError: [error],
after: [json],
missing: () => error(404, 'Are you sure about that?'),
...options,
})
2 changes: 1 addition & 1 deletion src/Router.spec.ts → src/IttyRouter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { createTestRunner, extract, toReq } from '../test'
import { Router } from './Router'
import { IttyRouter as Router } from './IttyRouter'

const ERROR_MESSAGE = 'Error Message'

Expand Down
125 changes: 125 additions & 0 deletions src/IttyRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
export type GenericTraps = {
[key: string]: any
}

export type RequestLike = {
method: string,
url: string,
} & GenericTraps

export type IRequestStrict = {
method: string,
url: string,
route: string,
params: {
[key: string]: string,
},
query: {
[key: string]: string | string[] | undefined,
},
proxy?: any,
} & Request

export type IRequest = IRequestStrict & GenericTraps

export type IttyRouterOptions = {
base?: string
routes?: RouteEntry[]
} & Record<string, any>

export type RouteHandler<I = IRequest, A extends any[] = any[]> = {
(request: I, ...args: A): any
}

export type RouteEntry = [
httpMethod: string,
match: RegExp,
handlers: RouteHandler[],
path?: string,
]

// this is the generic "Route", which allows per-route overrides
export type Route = <RequestType = IRequest, Args extends any[] = any[], RT = RouterType>(
path: string,
...handlers: RouteHandler<RequestType, Args>[]
) => RT

// this is an alternative UniveralRoute, accepting generics (from upstream), but without
// per-route overrides
export type UniversalRoute<RequestType = IRequest, Args extends any[] = any[]> = (
path: string,
...handlers: RouteHandler<RequestType, Args>[]
) => RouterType<UniversalRoute<RequestType, Args>, Args>

// helper function to detect equality in types (used to detect custom Request on router)
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

export type CustomRoutes<R = Route> = {
[key: string]: R,
}

export type RouterType<R = Route, Args extends any[] = any[]> = {
__proto__: RouterType<R>,
routes: RouteEntry[],
fetch: <A extends any[] = Args>(request: RequestLike, ...extra: Equal<R, Args> extends true ? A : Args) => Promise<any>
handle: <A extends any[] = Args>(request: RequestLike, ...extra: Equal<R, Args> extends true ? A : Args) => Promise<any>
all: R,
delete: R,
get: R,
head: R,
options: R,
patch: R,
post: R,
put: R,
} & CustomRoutes<R> & Record<string, any>

export const IttyRouter = <
RequestType = IRequest,
Args extends any[] = any[],
RouteType = Equal<RequestType, IRequest> extends true ? Route : UniversalRoute<RequestType, Args>
>({ base = '', routes = [], ...other }: IttyRouterOptions = {}): RouterType<RouteType, Args> =>
// @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: RouterType, path: string) =>
prop == 'handle' ? receiver.fetch :
// @ts-expect-error - unresolved type
(route: string, ...handlers: RouteHandler<I>[]) =>
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
}),
routes,
...other,
async fetch (request: RequestLike, ...args) {
let response,
match,
url = new URL(request.url),
query: Record<string, any> = request.query = { __proto__: null }

// 1. parse query params
for (let [k, v] of url.searchParams)
query[k] = query[k] ? ([] as string[]).concat(query[k], v) : v

// 2. then test routes
for (let [method, regex, handlers, path] of routes)
if ((method == request.method || method == 'ALL') && (match = url.pathname.match(regex))) {
request.params = match.groups || {} // embed params in request
request.route = path // embed route path in request
for (let handler of handlers)
if ((response = await handler(request.proxy ?? request, ...args)) != null) return response
}
},
})
168 changes: 67 additions & 101 deletions src/Router.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,21 @@
export type GenericTraps = {
[key: string]: any
}
import {
Equal,
IRequest,
RequestLike,
Route,
RouteHandler,
IttyRouterOptions,
RouterType,
UniversalRoute,
} from './IttyRouter'

export type RequestLike = {
method: string,
url: string,
} & GenericTraps

export type IRequestStrict = {
method: string,
url: string,
route: string,
params: {
[key: string]: string,
},
query: {
[key: string]: string | string[] | undefined,
},
proxy?: any,
} & Request

export type IRequest = IRequestStrict & GenericTraps
export type ErrorHandler = <Input = Error>(input: Input) => void

export type RouterOptions = {
base?: string
routes?: RouteEntry[]
} & Record<string, any>

export type RouteHandler<I = IRequest, A extends any[] = any[]> = {
(request: I, ...args: A): any
}

export type RouteEntry = [
httpMethod: string,
match: RegExp,
handlers: RouteHandler[],
path?: string,
]

// this is the generic "Route", which allows per-route overrides
export type Route = <RequestType = IRequest, Args extends any[] = any[], RT = RouterType>(
path: string,
...handlers: RouteHandler<RequestType, Args>[]
) => RT

// this is an alternative UniveralRoute, accepting generics (from upstream), but without
// per-route overrides
export type UniversalRoute<RequestType = IRequest, Args extends any[] = any[]> = (
path: string,
...handlers: RouteHandler<RequestType, Args>[]
) => RouterType<UniversalRoute<RequestType, Args>, Args>

// helper function to detect equality in types (used to detect custom Request on router)
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

export type CustomRoutes<R = Route> = {
[key: string]: R,
}

export type RouterType<R = Route, Args extends any[] = any[]> = {
__proto__: RouterType<R>,
routes: RouteEntry[],
fetch: <A extends any[] = Args>(request: RequestLike, ...extra: Equal<R, Args> extends true ? A : Args) => Promise<any>
handle: <A extends any[] = Args>(request: RequestLike, ...extra: Equal<R, Args> extends true ? A : Args) => Promise<any>
all: R,
delete: R,
get: R,
head: R,
options: R,
patch: R,
post: R,
put: R,
} & CustomRoutes<R> & Record<string, any>
before?: Function[]
onError?: ErrorHandler[]
after?: Function[]
} & IttyRouterOptions

export const Router = <
RequestType = IRequest,
Expand All @@ -83,40 +27,62 @@ export const Router = <
__proto__: new Proxy({}, {
// @ts-expect-error (we're adding an expected prop "path" to the get)
get: (target: any, prop: string, receiver: RouterType, path: string) =>
prop == 'handle' ? receiver.fetch :
// @ts-expect-error - unresolved type
(route: string, ...handlers: RouteHandler<I>[]) =>
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
// prop == 'handle' ? receiver.fetch :
// @ts-expect-error - unresolved type
(route: string, ...handlers: RouteHandler<I>[]) =>
routes.push(
[
prop.toUpperCase?.(),
RegExp(`^${(path = (base + route)

Check failure on line 36 in src/Router.ts

View workflow job for this annotation

GitHub Actions / Build

src/createCors.spec.ts > createCors(options) > corsify(response: Response): Response > should throw if no Response passed as argument

SyntaxError: Invalid regular expression: /^function(\.\.\.s) { let r = A(t); r\.called = !0, r\.callCount++, r\.calls\.push(s); let m = r\.next\.shift(); if (m) { r\.results\.push(m); let [l, o] = m; if (l === "ok") return o; throw o; } let p, d = "ok"; if (r\.impl) try { new\.target ? p = Reflect\.construct(r\.impl, s, new\.target) : p = r\.impl\.apply(this, s), d = "ok"; } catch (l) { throw p = l, d = "error", r\.results\.push([d, l]), l; } let a = [d, p]; if (b(p)) { let l = p\.then((o) => a[1] = o)\.catch((o) => { throw a[0] = "error", a[1] = o, o; }); Object\.assign(l, p), p = l; } return r\.results\.push(a), p; }/*$/: Nothing to repeat ❯ Object.<anonymous> src/Router.ts:36:17 ❯ src/createCors.spec.ts:126:50

Check failure on line 36 in src/Router.ts

View workflow job for this annotation

GitHub Actions / Build

src/createCors.spec.ts > createCors(options) > corsify(response: Response): Response > should throw if no Response passed as argument

SyntaxError: Invalid regular expression: /^function(\.\.\.s) { let r = A(t); r\.called = !0, r\.callCount++, r\.calls\.push(s); let m = r\.next\.shift(); if (m) { r\.results\.push(m); let [l, o] = m; if (l === "ok") return o; throw o; } let p, d = "ok"; if (r\.impl) try { new\.target ? p = Reflect\.construct(r\.impl, s, new\.target) : p = r\.impl\.apply(this, s), d = "ok"; } catch (l) { throw p = l, d = "error", r\.results\.push([d, l]), l; } let a = [d, p]; if (b(p)) { let l = p\.then((o) => a[1] = o)\.catch((o) => { throw a[0] = "error", a[1] = o, o; }); Object\.assign(l, p), p = l; } return r\.results\.push(a), p; }/*$/: Nothing to repeat ❯ Object.<anonymous> src/Router.ts:36:17 ❯ src/createCors.spec.ts:126:50

Check failure on line 36 in src/Router.ts

View workflow job for this annotation

GitHub Actions / build

src/createCors.spec.ts > createCors(options) > corsify(response: Response): Response > should throw if no Response passed as argument

SyntaxError: Invalid regular expression: /^function(\.\.\.s) { let r = A(t); r\.called = !0, r\.callCount++, r\.calls\.push(s); let m = r\.next\.shift(); if (m) { r\.results\.push(m); let [l, o] = m; if (l === "ok") return o; throw o; } let p, d = "ok"; if (r\.impl) try { new\.target ? p = Reflect\.construct(r\.impl, s, new\.target) : p = r\.impl\.apply(this, s), d = "ok"; } catch (l) { throw p = l, d = "error", r\.results\.push([d, l]), l; } let a = [d, p]; if (b(p)) { let l = p\.then((o) => a[1] = o)\.catch((o) => { throw a[0] = "error", a[1] = o, o; }); Object\.assign(l, p), p = l; } return r\.results\.push(a), p; }/*$/: Nothing to repeat ❯ Object.<anonymous> src/Router.ts:36:17 ❯ src/createCors.spec.ts:126:50
.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
}),
routes,
...other,
async fetch (request: RequestLike, ...args) {
let response, match, url = new URL(request.url), query: Record<string, any> = request.query = { __proto__: null }
async fetch (request: RequestLike, ...args) {
let response,
match,
url = new URL(request.url),
query: Record<string, any> = request.query = { __proto__: null }

try {
// 1. parse query params
for (let [k, v] of url.searchParams)
query[k] = query[k] ? ([] as string[]).concat(query[k], v) : v

for (let handler of other.before || [])
if ((response = await handler(request.proxy ?? request, ...args)) != null) break

// 2. then test routes
outer: for (let [method, regex, handlers, path] of routes)
if ((method == request.method || method == 'ALL') && (match = url.pathname.match(regex))) {
request.params = match.groups || {} // embed params in request
request.route = path // embed route path in request

for (let handler of handlers)
if ((response = await handler(request.proxy ?? request, ...args)) != null) break outer
}

// 3. respond with missing hook if available + needed
response = response ?? other.missing?.(request.proxy ?? request, ...args)
} catch (err) {
if (!other.onError) throw err

for (let handler of other.onError || [])
response = await handler(response ?? err)
}

// 1. parse query params
for (let [k, v] of url.searchParams)
query[k] = query[k] ? ([] as string[]).concat(query[k], v) : v
for (let handler of other.after || [])
response = await handler(response)

// 2. then test routes
for (let [method, regex, handlers, path] of routes)
if ((method == request.method || method == 'ALL') && (match = url.pathname.match(regex))) {
request.params = match.groups || {} // embed params in request
request.route = path // embed route path in request
for (let handler of handlers)
if ((response = await handler(request.proxy ?? request, ...args)) != null) return response
}
return response
},
})
Loading

0 comments on commit 45fea10

Please sign in to comment.