From 26d7a22bc3381edf72a19ae26ae4d8c06d15d39b Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 17:54:56 +1000 Subject: [PATCH 01/18] adds route factory --- _app.ts | 19 ++++++++++++++-- _factories.ts | 33 ++++++++++++++++++++++++++++ _route.ts | 2 ++ example/routes/index.get.ts | 16 ++++++++------ example/routes/index.post.ts | 16 ++++++++------ example/routes/profile/[name].get.ts | 16 ++++++++------ mod.ts | 3 ++- 7 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 _factories.ts diff --git a/_app.ts b/_app.ts index 44057c5..a5b5bf9 100644 --- a/_app.ts +++ b/_app.ts @@ -1,3 +1,4 @@ +import { RouteFactory } from "./_factories.ts"; import createRequest, { ServaRequest } from "./_request.ts"; import createRoute, { Route } from "./_route.ts"; import { fs, http, path, flags } from "./deps.ts"; @@ -228,7 +229,7 @@ export default class App { ); // register route - let callback: RouteCallback; + let callback: unknown; try { let filePath = `file://${entry.path}`; if (remount) { @@ -246,10 +247,24 @@ export default class App { continue; } + // check if the export was a factory + // @ts-ignore https://github.com/Microsoft/TypeScript/issues/1863 + const factory = callback[Symbol.for("serva_factory")]; + if (factory) { + switch (factory) { + case "route": + callback = (callback as RouteFactory)(route); + break; + + default: + throw new Error(`Factory (${factory}) not implemented`); + } + } + routes.push( [ route.method, - [route, callback!], + [route, callback! as RouteCallback], ], ); } diff --git a/_factories.ts b/_factories.ts new file mode 100644 index 0000000..fed8c94 --- /dev/null +++ b/_factories.ts @@ -0,0 +1,33 @@ +import { Route } from "./_route.ts"; +import { RouteCallback } from "./_app.ts"; + +export interface RouteFactory { + (route: Route): RouteCallback; +} + +interface RouteApi {} + +interface RouteMeta { + method: string; + path: string; + paramNames: Array; +} + +export function route( + callback: (api: RouteApi, meta: RouteMeta) => RouteCallback, +): RouteFactory { + function routeFactory(route: Route) { + return callback({}, { + method: route.method, + path: route.path, + paramNames: route.paramNames, + }); + } + + // signal to the app this is a factory + Object.assign(routeFactory, { + [Symbol.for("serva_factory")]: "route", + }); + + return routeFactory; +} diff --git a/_route.ts b/_route.ts index 5ad210c..1aa5a4d 100644 --- a/_route.ts +++ b/_route.ts @@ -5,6 +5,7 @@ export interface Route { readonly method: string; readonly path: string; readonly regexp: RegExp; + readonly paramNames: Array; params: (path: string) => Map; } @@ -34,6 +35,7 @@ export default function create( path, method, regexp, + paramNames: keys.map((k) => k.name), params(path: string): Map { const matches = matcher(path); return new Map(matches ? Object.entries(matches.params) : []); diff --git a/example/routes/index.get.ts b/example/routes/index.get.ts index 44709f1..999f84b 100644 --- a/example/routes/index.get.ts +++ b/example/routes/index.get.ts @@ -1,10 +1,12 @@ // ./example/routes/index.get.ts -import { ServaRequest } from "../../mod.ts"; +import { route } from "../../mod.ts"; -export default ({ response }: ServaRequest) => { - response.headers = new Headers({ - "X-Powered-By": "Serva", - }); +export default route(() => { + return ({ response }) => { + response.headers = new Headers({ + "X-Powered-By": "Serva", + }); - return "Hello from Serva."; -}; + return "Hello from Serva."; + }; +}); diff --git a/example/routes/index.post.ts b/example/routes/index.post.ts index 92237dc..b4843b9 100644 --- a/example/routes/index.post.ts +++ b/example/routes/index.post.ts @@ -1,10 +1,12 @@ // ./example/routes/index.post.ts -import { ServaRequest } from "../../mod.ts"; +import { route } from "../../mod.ts"; -export default ({ response }: ServaRequest) => { - response.headers = new Headers({ - "X-Powered-By": "Serva", - }); +export default route(() => + ({ response }) => { + response.headers = new Headers({ + "X-Powered-By": "Serva", + }); - return "Wait a minute, please Mr. POST-man."; -}; + return "Wait a minute, please Mr. POST-man."; + } +); diff --git a/example/routes/profile/[name].get.ts b/example/routes/profile/[name].get.ts index c186b32..f4ec695 100644 --- a/example/routes/profile/[name].get.ts +++ b/example/routes/profile/[name].get.ts @@ -1,10 +1,12 @@ // ./example/routes/index.get.ts -import { ServaRequest } from "../../../mod.ts"; +import { route } from "../../../mod.ts"; -export default ({ response, params }: ServaRequest) => { - response.headers = new Headers({ - "X-Powered-By": "Serva", - }); +export default route(() => + ({ response, params }) => { + response.headers = new Headers({ + "X-Powered-By": "Serva", + }); - return `Welcome ${params.get("name")}.`; -}; + return `Welcome ${params.get("name")}.`; + } +); diff --git a/mod.ts b/mod.ts index 3abb7a3..8ee0098 100644 --- a/mod.ts +++ b/mod.ts @@ -1,2 +1,3 @@ -export { ServaRequest } from "./_request.ts"; export { RouteCallback, ServaConfig } from "./_app.ts"; +export { route } from "./_factories.ts"; +export { ServaRequest } from "./_request.ts"; From 2f4defcb780e349f15a72483989e2894938f13c9 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 18:07:59 +1000 Subject: [PATCH 02/18] adds on request callback --- _app.ts | 7 ++++++- _factories.ts | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/_app.ts b/_app.ts index a5b5bf9..466d536 100644 --- a/_app.ts +++ b/_app.ts @@ -3,6 +3,10 @@ import createRequest, { ServaRequest } from "./_request.ts"; import createRoute, { Route } from "./_route.ts"; import { fs, http, path, flags } from "./deps.ts"; +export interface OnRequestCallback { + (request: ServaRequest, next: () => Promise): Promise | any; +} + export interface RouteCallback { (request: ServaRequest): Promise | any; } @@ -253,7 +257,8 @@ export default class App { if (factory) { switch (factory) { case "route": - callback = (callback as RouteFactory)(route); + let hooks; + [hooks, callback] = (callback as RouteFactory)(route); break; default: diff --git a/_factories.ts b/_factories.ts index fed8c94..a8f9eec 100644 --- a/_factories.ts +++ b/_factories.ts @@ -1,11 +1,13 @@ +import { RouteCallback, OnRequestCallback } from "./_app.ts"; import { Route } from "./_route.ts"; -import { RouteCallback } from "./_app.ts"; export interface RouteFactory { - (route: Route): RouteCallback; + (route: Route): [OnRequestCallback[], RouteCallback]; } -interface RouteApi {} +interface RouteApi { + onRequest: (callback: OnRequestCallback) => void; +} interface RouteMeta { method: string; @@ -16,12 +18,17 @@ interface RouteMeta { export function route( callback: (api: RouteApi, meta: RouteMeta) => RouteCallback, ): RouteFactory { - function routeFactory(route: Route) { - return callback({}, { + function routeFactory(route: Route): [any[], RouteCallback] { + const hooks: any[] = []; + const cb = callback({ + onRequest: hooks.push.bind(hooks), + }, { method: route.method, path: route.path, paramNames: route.paramNames, }); + + return [hooks, cb]; } // signal to the app this is a factory From 521d0ee734a89e9979e30f202239667c9d6d73d8 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 18:37:21 +1000 Subject: [PATCH 03/18] updating tests to handle new route property paramNames --- _route_test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/_route_test.ts b/_route_test.ts index 10daea0..c03094b 100644 --- a/_route_test.ts +++ b/_route_test.ts @@ -9,6 +9,7 @@ Deno.test("basicRouteObject", () => { filePath: "/routes/index.get.ts", path: "/", method: "GET", + paramNames: [], regexp: pathToRegexp.pathToRegexp("/"), }); }); @@ -20,6 +21,7 @@ Deno.test("nonEndingRouteObject", () => { filePath: "/routes/_hook.ts", path: "/*", method: "GET", + paramNames: [], regexp: pathToRegexp.pathToRegexp("/", [], { end: false, }), @@ -27,12 +29,13 @@ Deno.test("nonEndingRouteObject", () => { }); Deno.test("routeParams", () => { - const { params } = createRoute( + const { params, paramNames } = createRoute( "GET", "/[first]-[last]/comments/[comment]/view", "./routes/[first]-[last]/comments/[comment]/view.get.ts", ); + assertEquals(paramNames, ["first", "last", "comment"]); assertEquals( params("/chris-turner/comments/123/view"), new Map([["first", "chris"], ["last", "turner"], ["comment", "123"]]), From 22e0d0357d0146f805c0b5c1bebbd45640f59c80 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 18:37:49 +1000 Subject: [PATCH 04/18] route onRequest hooks --- _app.ts | 56 +++++++++++++++++++++++++++---------- example/routes/index.get.ts | 10 ++++++- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/_app.ts b/_app.ts index 466d536..236300d 100644 --- a/_app.ts +++ b/_app.ts @@ -26,7 +26,7 @@ const DEFAULT_CONFIG = Object.freeze({ methods: "get|post|put|delete|patch".split("|"), }); -type RouteEntry = [Route, RouteCallback]; +type RouteEntry = [Route, OnRequestCallback[]]; type RoutesStruct = Map; export default class App { @@ -216,10 +216,10 @@ export default class App { const routes: [string, RouteEntry][] = []; - for await (const entry of fs.walk(routesPath)) { - // ignore directories and any non-typescript files + for await (const entry of fs.walk(routesPath, { includeDirs: false })) { + // ignore any non-typescript files // todo: support for javascript - if (entry.isDirectory || !entry.name.endsWith(this.config.extension)) { + if (!entry.name.endsWith(this.config.extension)) { continue; } @@ -254,10 +254,10 @@ export default class App { // check if the export was a factory // @ts-ignore https://github.com/Microsoft/TypeScript/issues/1863 const factory = callback[Symbol.for("serva_factory")]; + let hooks: OnRequestCallback[] = []; if (factory) { switch (factory) { case "route": - let hooks; [hooks, callback] = (callback as RouteFactory)(route); break; @@ -269,7 +269,10 @@ export default class App { routes.push( [ route.method, - [route, callback! as RouteCallback], + [ + route, + hooks.concat(routeToRequestCallback(callback as RouteCallback)), + ], ], ); } @@ -295,7 +298,7 @@ export default class App { private async handleRequest(req: http.ServerRequest): Promise { // find a matching route let route: Route; - let callback: RouteCallback; + let callbacks: OnRequestCallback[] = []; const { pathname } = new URL(req.url, "https://serva.land"); const possibleMethods = [req.method, "*"]; @@ -306,10 +309,10 @@ export default class App { possibleMethods.some((m) => { const entries = this.routes.get(m); if (entries) { - return entries.some(([r, cb]) => { + return entries.some(([r, cbs]) => { if (r.regexp.test(pathname)) { route = r; - callback = cb; + callbacks = cbs; // route found return true; } @@ -327,12 +330,7 @@ export default class App { } const request = createRequest(req, route); - const body = await callback!(request); - - // allow return values to set the body - if (body !== undefined) { - request.response.body = body; - } + await dispatch(callbacks, request); // if nobody has responded, send the current request's response if (request.httpRequest.w.usedBufferBytes === 0) { @@ -445,3 +443,31 @@ function sortRoutes(a: [string, RouteEntry], b: [string, RouteEntry]): number { return 0; } + +function dispatch(callbacks: OnRequestCallback[], request: ServaRequest) { + let i = -1; + const next = (current = 0): Promise => { + if (current <= i) { + throw new Error("next() already called"); + } + + const cb = callbacks[i = current]; + + return Promise.resolve( + cb ? cb(request, next.bind(undefined, i + 1)) : undefined, + ); + }; + + return next(); +} + +function routeToRequestCallback(callback: RouteCallback): OnRequestCallback { + return async function (request, next) { + const body = callback(request); + if (body !== undefined) { + request.response.body = body; + } + + return next(); + }; +} diff --git a/example/routes/index.get.ts b/example/routes/index.get.ts index 999f84b..a0ddd6f 100644 --- a/example/routes/index.get.ts +++ b/example/routes/index.get.ts @@ -1,12 +1,20 @@ // ./example/routes/index.get.ts import { route } from "../../mod.ts"; -export default route(() => { +export default route(({ onRequest }) => { + onRequest(async (_, next) => { + console.log("before"); + await next(); + console.log("after"); + }); + return ({ response }) => { response.headers = new Headers({ "X-Powered-By": "Serva", }); + console.log("route"); + return "Hello from Serva."; }; }); From 416b33492424ffa172b8ab9c0d27e87bac8eac55 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 18:40:41 +1000 Subject: [PATCH 05/18] internal function docblocks --- _app.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/_app.ts b/_app.ts index 236300d..cb62cc2 100644 --- a/_app.ts +++ b/_app.ts @@ -444,7 +444,17 @@ function sortRoutes(a: [string, RouteEntry], b: [string, RouteEntry]): number { return 0; } -function dispatch(callbacks: OnRequestCallback[], request: ServaRequest) { +/** + * The hooks dispatcher. + * + * @param {OnRequestCallback[]} callbacks + * @param {ServaRequest} request + * @returns {Promise} + */ +function dispatch( + callbacks: OnRequestCallback[], + request: ServaRequest, +): Promise { let i = -1; const next = (current = 0): Promise => { if (current <= i) { @@ -457,17 +467,21 @@ function dispatch(callbacks: OnRequestCallback[], request: ServaRequest) { cb ? cb(request, next.bind(undefined, i + 1)) : undefined, ); }; - return next(); } +/** + * Transforms a route callback to a request hook callback. + * + * @param {RouteCallback} callback + * @returns {OnRequestCallback} + */ function routeToRequestCallback(callback: RouteCallback): OnRequestCallback { return async function (request, next) { const body = callback(request); if (body !== undefined) { request.response.body = body; } - return next(); }; } From 37484736ffd2682f05ef7f1c6f92af80ecae28b3 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 19:03:38 +1000 Subject: [PATCH 06/18] removing meta from factories --- _factories.ts | 19 +++---------------- _factories_test.ts | 27 +++++++++++++++++++++++++++ _route.ts | 4 ++-- 3 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 _factories_test.ts diff --git a/_factories.ts b/_factories.ts index a8f9eec..9f6e73e 100644 --- a/_factories.ts +++ b/_factories.ts @@ -6,29 +6,16 @@ export interface RouteFactory { } interface RouteApi { + route: Route; onRequest: (callback: OnRequestCallback) => void; } -interface RouteMeta { - method: string; - path: string; - paramNames: Array; -} - export function route( - callback: (api: RouteApi, meta: RouteMeta) => RouteCallback, + callback: (api: RouteApi) => RouteCallback, ): RouteFactory { function routeFactory(route: Route): [any[], RouteCallback] { const hooks: any[] = []; - const cb = callback({ - onRequest: hooks.push.bind(hooks), - }, { - method: route.method, - path: route.path, - paramNames: route.paramNames, - }); - - return [hooks, cb]; + return [hooks, callback({ onRequest: hooks.push.bind(hooks), route })]; } // signal to the app this is a factory diff --git a/_factories_test.ts b/_factories_test.ts new file mode 100644 index 0000000..04890f3 --- /dev/null +++ b/_factories_test.ts @@ -0,0 +1,27 @@ +import { + assert, + assertEquals, +} from "https://deno.land/std@0.57.0/testing/asserts.ts"; +import { route } from "./_factories.ts"; +import createRoute from "./_route.ts"; + +Deno.test("routeFactory", () => { + let inner = false; + const r = createRoute("GET", "/", "./routes/index.get.ts"); + const hook = () => {}; + const callback = () => {}; + + const factory = route(({ route, onRequest }) => { + inner = true; // spy + assertEquals(route, r); + + onRequest(hook); + + return callback; + }); + + const res = factory(r); + + assert(inner); + assertEquals(res, [[hook], callback]); +}); diff --git a/_route.ts b/_route.ts index 1aa5a4d..89016ca 100644 --- a/_route.ts +++ b/_route.ts @@ -30,7 +30,7 @@ export default function create( }); const matcher = pathToRegexp.match(cleaned); - return { + return Object.freeze({ filePath, path, method, @@ -40,7 +40,7 @@ export default function create( const matches = matcher(path); return new Map(matches ? Object.entries(matches.params) : []); }, - }; + }); } /** From c94e9539c190d13c30bc55b29cbaf12c3eec8b48 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 20:49:10 +1000 Subject: [PATCH 07/18] allow json to be returned from the routes --- _app.ts | 27 +++++++++++++++++++++++++++ _request.ts | 10 +++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/_app.ts b/_app.ts index cb62cc2..4f939a2 100644 --- a/_app.ts +++ b/_app.ts @@ -334,6 +334,18 @@ export default class App { // if nobody has responded, send the current request's response if (request.httpRequest.w.usedBufferBytes === 0) { + const { response } = request; + + // detect json response + const tryJSON = !validHttpResponseBody(response.body); + if (tryJSON) { + response.body = JSON.stringify(response.body); + const headers = response.headers || (response.headers = new Headers()); + + // set the + headers.set("content-type", "application/json; charset=utf-8"); + } + req.respond(request.response); } } @@ -485,3 +497,18 @@ function routeToRequestCallback(callback: RouteCallback): OnRequestCallback { return next(); }; } + +function validHttpResponseBody(body: any): boolean { + switch (typeof body) { + case "undefined": + case "string": + return true; + + case "object": + return body && + (body instanceof Uint8Array || typeof body.read === "function"); + + default: + return false; + } +} diff --git a/_request.ts b/_request.ts index c3abdd1..000eaf2 100644 --- a/_request.ts +++ b/_request.ts @@ -1,6 +1,10 @@ import { Route } from "./_route.ts"; import { http } from "./deps.ts"; +interface ServaResponse extends http.Response { + body?: any; // allow routes to return anything +} + export interface ServaRequest { readonly httpRequest: http.ServerRequest; @@ -11,7 +15,7 @@ export interface ServaRequest { readonly headers: Headers; // response - readonly response: http.Response; + readonly response: ServaResponse; } /** @@ -25,7 +29,7 @@ export default function create( req: http.ServerRequest, route: Route, ): ServaRequest { - const response: http.Response = {}; + const response: ServaResponse = {}; const proto = req.proto.split("/")[0].toLowerCase(); const url = new URL(req.url, `${proto}://${req.headers.get("host")}`); @@ -35,7 +39,7 @@ export default function create( method: req.method, params: route.params(url.pathname), headers: req.headers, - get response(): http.Response { + get response(): ServaResponse { return response; }, }; From 16717c7bdf3004ab3fa4db102bdaf3860cb1df25 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 7 Jul 2020 20:51:55 +1000 Subject: [PATCH 08/18] adds docblock to internal function --- _app.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/_app.ts b/_app.ts index 4f939a2..5641a20 100644 --- a/_app.ts +++ b/_app.ts @@ -498,6 +498,12 @@ function routeToRequestCallback(callback: RouteCallback): OnRequestCallback { }; } +/** + * Chek if a given body is a valid Http response type. + * + * @param {any} body + * @returns {boolean} + */ function validHttpResponseBody(body: any): boolean { switch (typeof body) { case "undefined": From a5acad69224fa0e54ae78500c9109f1d8b20722e Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Thu, 16 Jul 2020 17:22:46 +1000 Subject: [PATCH 09/18] _factory < factory --- _app.ts | 2 +- _factories.ts => factories.ts | 0 _factories_test.ts => factories_test.ts | 2 +- mod.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename _factories.ts => factories.ts (100%) rename _factories_test.ts => factories_test.ts (93%) diff --git a/_app.ts b/_app.ts index 5641a20..aeaf211 100644 --- a/_app.ts +++ b/_app.ts @@ -1,4 +1,4 @@ -import { RouteFactory } from "./_factories.ts"; +import { RouteFactory } from "./factories.ts"; import createRequest, { ServaRequest } from "./_request.ts"; import createRoute, { Route } from "./_route.ts"; import { fs, http, path, flags } from "./deps.ts"; diff --git a/_factories.ts b/factories.ts similarity index 100% rename from _factories.ts rename to factories.ts diff --git a/_factories_test.ts b/factories_test.ts similarity index 93% rename from _factories_test.ts rename to factories_test.ts index 04890f3..215996e 100644 --- a/_factories_test.ts +++ b/factories_test.ts @@ -2,7 +2,7 @@ import { assert, assertEquals, } from "https://deno.land/std@0.57.0/testing/asserts.ts"; -import { route } from "./_factories.ts"; +import { route } from "./factories.ts"; import createRoute from "./_route.ts"; Deno.test("routeFactory", () => { diff --git a/mod.ts b/mod.ts index 8ee0098..ac20525 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,3 @@ export { RouteCallback, ServaConfig } from "./_app.ts"; -export { route } from "./_factories.ts"; +export { route } from "./factories.ts"; export { ServaRequest } from "./_request.ts"; From de12367189fc4765aec7fb630c3b486fbdeb54a4 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Thu, 16 Jul 2020 17:23:47 +1000 Subject: [PATCH 10/18] _request < request --- _app.ts | 2 +- mod.ts | 2 +- _request.ts => request.ts | 0 _request_test.ts => request_test.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename _request.ts => request.ts (100%) rename _request_test.ts => request_test.ts (95%) diff --git a/_app.ts b/_app.ts index aeaf211..be66019 100644 --- a/_app.ts +++ b/_app.ts @@ -1,5 +1,5 @@ import { RouteFactory } from "./factories.ts"; -import createRequest, { ServaRequest } from "./_request.ts"; +import createRequest, { ServaRequest } from "./request.ts"; import createRoute, { Route } from "./_route.ts"; import { fs, http, path, flags } from "./deps.ts"; diff --git a/mod.ts b/mod.ts index ac20525..3d592b3 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,3 @@ export { RouteCallback, ServaConfig } from "./_app.ts"; export { route } from "./factories.ts"; -export { ServaRequest } from "./_request.ts"; +export { ServaRequest } from "./request.ts"; diff --git a/_request.ts b/request.ts similarity index 100% rename from _request.ts rename to request.ts diff --git a/_request_test.ts b/request_test.ts similarity index 95% rename from _request_test.ts rename to request_test.ts index 9163dbe..cbe5b56 100644 --- a/_request_test.ts +++ b/request_test.ts @@ -1,6 +1,6 @@ import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; import { http } from "./deps.ts"; -import createRequest from "./_request.ts"; +import createRequest from "./request.ts"; import createRoute from "./_route.ts"; Deno.test("servaRequestObject", () => { From 0e1193afdc401d6a7141f1c9511f26bbdeeb77b4 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Thu, 16 Jul 2020 19:44:54 +1000 Subject: [PATCH 11/18] adds hooks files --- _app.ts | 124 +++++++++++++++++++-------- _route.ts | 2 + _route_test.ts | 12 ++- example/routes/_hooks.ts | 17 ++++ example/routes/index.get.ts | 10 +-- example/routes/index.post.ts | 12 +-- example/routes/profile/[name].get.ts | 12 +-- factories.ts | 28 +++++- factories_test.ts | 20 ++++- mod.ts | 2 +- request.ts | 5 +- request_test.ts | 4 +- 12 files changed, 172 insertions(+), 76 deletions(-) create mode 100644 example/routes/_hooks.ts diff --git a/_app.ts b/_app.ts index be66019..e097682 100644 --- a/_app.ts +++ b/_app.ts @@ -1,4 +1,4 @@ -import { RouteFactory } from "./factories.ts"; +import { RouteFactory, Hooksfactory } from "./factories.ts"; import createRequest, { ServaRequest } from "./request.ts"; import createRoute, { Route } from "./_route.ts"; import { fs, http, path, flags } from "./deps.ts"; @@ -164,7 +164,7 @@ export default class App { const configFilePath = this.path("serva.config.json"); const lstats = await Deno.lstat(configFilePath); if (!lstats.isFile) { - throw new Error(); + throw new Error("serva.config.json is not a file"); } // @ts-expect-error @@ -215,6 +215,7 @@ export default class App { } const routes: [string, RouteEntry][] = []; + const routeHooks: [string, RouteEntry][] = []; for await (const entry of fs.walk(routesPath, { includeDirs: false })) { // ignore any non-typescript files @@ -224,7 +225,16 @@ export default class App { } const relativeEntryPath = path.relative(routesPath, entry.path); - const method = routeMethod(relativeEntryPath, this.config); + let [filename, method] = nameAndMethodFromPath( + relativeEntryPath, + this.config, + ); + + // throw if dev is trying to: _hooks.get.ts + if (filename === "_hooks" && method !== "*") { + throw new Error("Hook files should not contain a methods"); + } + const urlPath = routePath(relativeEntryPath, this.config); // route information const route = createRoute( method, @@ -243,7 +253,7 @@ export default class App { ({ default: callback } = await import(filePath)); if (typeof callback !== "function") { throw new TypeError( - `${route.filePath} default export is not a callback.`, + `${route.filePath} default export is not a callback`, ); } } catch (err) { @@ -257,28 +267,53 @@ export default class App { let hooks: OnRequestCallback[] = []; if (factory) { switch (factory) { + case "hooks": + if (filename !== "_hooks") { + throw new Error("Invalid hooks filename"); + } + hooks = (callback as Hooksfactory)(route); + routeHooks.push([route.method, [route, hooks]]); + continue; + case "route": [hooks, callback] = (callback as RouteFactory)(route); + hooks.push(routeToRequestCallback(callback as RouteCallback)); break; default: throw new Error(`Factory (${factory}) not implemented`); } + } else { + hooks = [routeToRequestCallback(callback as RouteCallback)]; } - routes.push( - [ - route.method, - [ - route, - hooks.concat(routeToRequestCallback(callback as RouteCallback)), - ], - ], - ); + routes.push([route.method, [route, hooks]]); } // sort and set routes + routeHooks.sort(sortRoutes); routes.sort(sortRoutes).forEach(([method, entry]) => { + // merge routeHooks into route + const [route, hooks] = entry; + const fakeUrl = route.toPath( + route.paramNames.length + ? route.paramNames.reduce((previous, name) => ({ + ...previous, + [name]: "foo", + }), {}) + : undefined, + ); + + const hooksToMerge = routeHooks + .filter(([, [hookRoute]]) => hookRoute.regexp.test(fakeUrl)) + .map(([, entry]) => entry[1]) + .reduce((previous, hooks_) => [ + ...previous, + ...hooks_, + ], []); + + hooks.unshift(...hooksToMerge); + const entries = this.routes.get(method) || []; entries.push(entry); @@ -337,13 +372,9 @@ export default class App { const { response } = request; // detect json response - const tryJSON = !validHttpResponseBody(response.body); - if (tryJSON) { + if (!validHttpResponseBody(response.body)) { response.body = JSON.stringify(response.body); - const headers = response.headers || (response.headers = new Headers()); - - // set the - headers.set("content-type", "application/json; charset=utf-8"); + response.headers.set("content-type", "application/json; charset=utf-8"); } req.respond(request.response); @@ -351,13 +382,45 @@ export default class App { } } +/** + * Returns the filename and extracted method from a file path. + * + * @example + * nameAndMethodFromPath("index.ts") + * // => ["index", "*"] + * + * nameAndMethodFromPath("comments/[comment].get.ts"); + * // => ["[comment]", "GET"] + * + * @param {string} filePath + * @param {ServaConfig} config + * @returns [string, string] + */ +function nameAndMethodFromPath( + filePath: string, + config: ServaConfig, +): [string, string] { + let name = path.basename(filePath, config.extension); + let method = "*"; + + const matched = name.match( + new RegExp(`.*(?=\.(${config.methods.join("|")})$)`, "i"), + ); + + if (matched) { + [name, method] = matched; + } + + return [name, method.toUpperCase()]; +} + /** * Gets the route path from a file path. * * @example * routePath("index.ts"); * // => "/" - * + * * routePath("comments/[comment].get.ts"); * // => "/comments/[comment]" * @@ -366,17 +429,12 @@ export default class App { * @returns {string} */ function routePath(filePath: string, config: ServaConfig): string { - let name = path.basename(filePath, config.extension); - const matched = name.match( - new RegExp(`.*(?=\.(${config.methods.join("|")})$)`, "i"), - ); - - if (matched) { - [name] = matched; - } + let [name] = nameAndMethodFromPath(filePath, config); if (name === "index") { name = ""; + } else if (name === "_hooks") { + name = "*"; // single glob match for hook files } let base = path.dirname(filePath); @@ -402,15 +460,7 @@ function routePath(filePath: string, config: ServaConfig): string { * @returns {string} */ function routeMethod(filePath: string, config: ServaConfig): string { - const name = path.basename(filePath, config.extension); - const matched = name.match( - new RegExp(`.*(?=\.(${config.methods.join("|")})$)`, "i"), - ); - - let method = "*"; - if (matched) { - [, method] = matched; - } + const [, method] = nameAndMethodFromPath(filePath, config); return method.toUpperCase(); } diff --git a/_route.ts b/_route.ts index 89016ca..3e61c04 100644 --- a/_route.ts +++ b/_route.ts @@ -7,6 +7,7 @@ export interface Route { readonly regexp: RegExp; readonly paramNames: Array; params: (path: string) => Map; + toPath: (params?: object) => string; } /** @@ -36,6 +37,7 @@ export default function create( method, regexp, paramNames: keys.map((k) => k.name), + toPath: pathToRegexp.compile(cleaned, { encode: encodeURIComponent }), params(path: string): Map { const matches = matcher(path); return new Map(matches ? Object.entries(matches.params) : []); diff --git a/_route_test.ts b/_route_test.ts index c03094b..4ad79af 100644 --- a/_route_test.ts +++ b/_route_test.ts @@ -3,7 +3,11 @@ import createRoute from "./_route.ts"; import { pathToRegexp } from "./deps.ts"; Deno.test("basicRouteObject", () => { - const { params, ...route } = createRoute("GET", "/", "/routes/index.get.ts"); + const { params, toPath, ...route } = createRoute( + "GET", + "/", + "/routes/index.get.ts", + ); assertEquals(route, { filePath: "/routes/index.get.ts", @@ -15,7 +19,11 @@ Deno.test("basicRouteObject", () => { }); Deno.test("nonEndingRouteObject", () => { - const { params, ...route } = createRoute("GET", "/*", "/routes/_hook.ts"); + const { params, toPath, ...route } = createRoute( + "GET", + "/*", + "/routes/_hook.ts", + ); assertEquals(route, { filePath: "/routes/_hook.ts", diff --git a/example/routes/_hooks.ts b/example/routes/_hooks.ts new file mode 100644 index 0000000..d0f7697 --- /dev/null +++ b/example/routes/_hooks.ts @@ -0,0 +1,17 @@ +// ./example/routes/_hooks.ts +import { hooks } from "../../mod.ts"; + +export default hooks(({ onRequest }) => { + onRequest(async ({ url }, next) => { + const { pathname } = url; + + console.log(`--> ${pathname}`); + await next(); + console.log(`<-- ${pathname}`); + }); + + onRequest(async ({ response }, next) => { + await next(); + response.headers.set("X-Powered-By", "Serva"); + }); +}); diff --git a/example/routes/index.get.ts b/example/routes/index.get.ts index a0ddd6f..594ff01 100644 --- a/example/routes/index.get.ts +++ b/example/routes/index.get.ts @@ -8,13 +8,5 @@ export default route(({ onRequest }) => { console.log("after"); }); - return ({ response }) => { - response.headers = new Headers({ - "X-Powered-By": "Serva", - }); - - console.log("route"); - - return "Hello from Serva."; - }; + return () => "Hello from Serva."; }); diff --git a/example/routes/index.post.ts b/example/routes/index.post.ts index b4843b9..3e06111 100644 --- a/example/routes/index.post.ts +++ b/example/routes/index.post.ts @@ -1,12 +1,2 @@ // ./example/routes/index.post.ts -import { route } from "../../mod.ts"; - -export default route(() => - ({ response }) => { - response.headers = new Headers({ - "X-Powered-By": "Serva", - }); - - return "Wait a minute, please Mr. POST-man."; - } -); +export default () => "Wait a minute, please Mr. POST-man."; diff --git a/example/routes/profile/[name].get.ts b/example/routes/profile/[name].get.ts index f4ec695..a250304 100644 --- a/example/routes/profile/[name].get.ts +++ b/example/routes/profile/[name].get.ts @@ -1,12 +1,4 @@ // ./example/routes/index.get.ts -import { route } from "../../../mod.ts"; +import { ServaRequest } from "../../../mod.ts"; -export default route(() => - ({ response, params }) => { - response.headers = new Headers({ - "X-Powered-By": "Serva", - }); - - return `Welcome ${params.get("name")}.`; - } -); +export default ({ params }: ServaRequest) => `Welcome ${params.get("name")}.`; diff --git a/factories.ts b/factories.ts index 9f6e73e..6e070bb 100644 --- a/factories.ts +++ b/factories.ts @@ -5,16 +5,22 @@ export interface RouteFactory { (route: Route): [OnRequestCallback[], RouteCallback]; } -interface RouteApi { +export interface Hooksfactory { + (route: Route): OnRequestCallback[]; +} + +interface HooksApi { route: Route; onRequest: (callback: OnRequestCallback) => void; } +interface RouteApi extends HooksApi {} + export function route( callback: (api: RouteApi) => RouteCallback, ): RouteFactory { - function routeFactory(route: Route): [any[], RouteCallback] { - const hooks: any[] = []; + function routeFactory(route: Route): [OnRequestCallback[], RouteCallback] { + const hooks: OnRequestCallback[] = []; return [hooks, callback({ onRequest: hooks.push.bind(hooks), route })]; } @@ -25,3 +31,19 @@ export function route( return routeFactory; } + +export function hooks(callback: (api: HooksApi) => void): Hooksfactory { + function hooksFactory(route: Route): OnRequestCallback[] { + const hooks: OnRequestCallback[] = []; + callback({ onRequest: hooks.push.bind(hooks), route }); + + return hooks; + } + + // signal to the app this is a factory + Object.assign(hooksFactory, { + [Symbol.for("serva_factory")]: "hooks", + }); + + return hooksFactory; +} diff --git a/factories_test.ts b/factories_test.ts index 215996e..1210746 100644 --- a/factories_test.ts +++ b/factories_test.ts @@ -2,7 +2,7 @@ import { assert, assertEquals, } from "https://deno.land/std@0.57.0/testing/asserts.ts"; -import { route } from "./factories.ts"; +import { route, hooks } from "./factories.ts"; import createRoute from "./_route.ts"; Deno.test("routeFactory", () => { @@ -25,3 +25,21 @@ Deno.test("routeFactory", () => { assert(inner); assertEquals(res, [[hook], callback]); }); + +Deno.test("hooksFactory", () => { + let inner = false; + const r = createRoute("*", "/", "./routes/_hooks.ts"); + const hook = () => {}; + + const factory = hooks(({ route, onRequest }) => { + inner = true; // spy + assertEquals(route, r); + + onRequest(hook); + }); + + const res = factory(r); + + assert(inner); + assertEquals(res, [hook]); +}); diff --git a/mod.ts b/mod.ts index 3d592b3..7a2605e 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,3 @@ export { RouteCallback, ServaConfig } from "./_app.ts"; -export { route } from "./factories.ts"; +export { route, hooks } from "./factories.ts"; export { ServaRequest } from "./request.ts"; diff --git a/request.ts b/request.ts index 000eaf2..6ef0ba6 100644 --- a/request.ts +++ b/request.ts @@ -2,6 +2,7 @@ import { Route } from "./_route.ts"; import { http } from "./deps.ts"; interface ServaResponse extends http.Response { + headers: Headers; body?: any; // allow routes to return anything } @@ -29,7 +30,9 @@ export default function create( req: http.ServerRequest, route: Route, ): ServaRequest { - const response: ServaResponse = {}; + const response: ServaResponse = { + headers: new Headers(), + }; const proto = req.proto.split("/")[0].toLowerCase(); const url = new URL(req.url, `${proto}://${req.headers.get("host")}`); diff --git a/request_test.ts b/request_test.ts index cbe5b56..f2ae420 100644 --- a/request_test.ts +++ b/request_test.ts @@ -26,6 +26,8 @@ Deno.test("servaRequestObject", () => { method: mockRequest.method, params: new Map([["name", "chris"]]), headers: mockRequest.headers, - response: {}, + response: { + headers: new Headers(), + }, }); }); From 1eb61495bdf749e26ecccf6aa1f4d5da5395a81e Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 28 Jul 2020 18:01:33 +1000 Subject: [PATCH 12/18] updating std version --- deps.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deps.ts b/deps.ts index d523491..6d2f9b5 100644 --- a/deps.ts +++ b/deps.ts @@ -1,12 +1,12 @@ -export * as flags from "https://deno.land/std@0.57.0/flags/mod.ts"; +export * as flags from "https://deno.land/std@0.62.0/flags/mod.ts"; // fs contains unstable features just re-export under fs -import { readJson } from "https://deno.land/std@0.57.0/fs/read_json.ts"; -import { walk } from "https://deno.land/std@0.57.0/fs/walk.ts"; +import { readJson } from "https://deno.land/std@0.62.0/fs/read_json.ts"; +import { walk } from "https://deno.land/std@0.62.0/fs/walk.ts"; export const fs = { readJson, walk }; -export * as http from "https://deno.land/std@0.57.0/http/mod.ts"; +export * as http from "https://deno.land/std@0.62.0/http/mod.ts"; -export * as path from "https://deno.land/std@0.57.0/path/mod.ts"; +export * as path from "https://deno.land/std@0.62.0/path/mod.ts"; export * as pathToRegexp from "https://raw.githubusercontent.com/pillarjs/path-to-regexp/v6.1.0/src/index.ts"; From 1362a47171ba2a09ec7eac18d151b73dc17b75b6 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Tue, 28 Jul 2020 22:44:51 +1000 Subject: [PATCH 13/18] adds body reader to serva request --- _app.ts | 2 +- body_reader.ts | 64 ++++++++++++++++++++++++++++++++++++ example/routes/index.post.ts | 8 ++++- request.ts | 3 ++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 body_reader.ts diff --git a/_app.ts b/_app.ts index e097682..3d68b16 100644 --- a/_app.ts +++ b/_app.ts @@ -540,7 +540,7 @@ function dispatch( */ function routeToRequestCallback(callback: RouteCallback): OnRequestCallback { return async function (request, next) { - const body = callback(request); + const body = await callback(request); if (body !== undefined) { request.response.body = body; } diff --git a/body_reader.ts b/body_reader.ts new file mode 100644 index 0000000..c67ee40 --- /dev/null +++ b/body_reader.ts @@ -0,0 +1,64 @@ +import { http } from "./deps.ts"; + +interface JsonOptions { + reviver?: (this: any, key: string, value: any) => any; +} + +export default class BodyReader { + private request: http.ServerRequest; + + /** + * Creates a new instance of BodyParser. + * + * @param {http.ServerRequest} request + */ + constructor(request: http.ServerRequest) { + this.request = request; + } + + /** + * Reads the request body. + * + * @returns {Promise} + */ + async blob() { + return await Deno.readAll(this.request.body); + } + + /** + * Reads the request body as string. + * + * @returns {Promise} + */ + async text() { + const decoder = new TextDecoder(); + return decoder.decode(await this.blob()); + } + + /** + * Reads the request body as json. + * + * @param {JsonOptions} [options] + * @returns {Promise} + */ + async json(options?: JsonOptions): Promise { + return JSON.parse(await this.text(), options && options.reviver) as T; + } + + /** + * Reads the request body as form data. + * + * @returns {Promise} + */ + async form() { + const text = await this.text(); + const params = new URLSearchParams(text); + const form = new FormData(); + + for (const [key, value] of params.entries()) { + form.set(key, value); + } + + return form; + } +} diff --git a/example/routes/index.post.ts b/example/routes/index.post.ts index 3e06111..9149c7f 100644 --- a/example/routes/index.post.ts +++ b/example/routes/index.post.ts @@ -1,2 +1,8 @@ +import { ServaRequest } from "../../request.ts"; + // ./example/routes/index.post.ts -export default () => "Wait a minute, please Mr. POST-man."; +export default async ({ body }: ServaRequest) => { + const { name } = await body.json(); + + return `Hello, ${name}!`; +}; diff --git a/request.ts b/request.ts index 6ef0ba6..b2e1e40 100644 --- a/request.ts +++ b/request.ts @@ -1,5 +1,6 @@ import { Route } from "./_route.ts"; import { http } from "./deps.ts"; +import BodyReader from "./body_reader.ts"; interface ServaResponse extends http.Response { headers: Headers; @@ -14,6 +15,7 @@ export interface ServaRequest { readonly method: string; readonly params: ReadonlyMap; readonly headers: Headers; + readonly body: BodyReader; // response readonly response: ServaResponse; @@ -42,6 +44,7 @@ export default function create( method: req.method, params: route.params(url.pathname), headers: req.headers, + body: new BodyReader(req), get response(): ServaResponse { return response; }, From c630216605d76c1942c573c5ec5b56fdb200ec2b Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Sun, 2 Aug 2020 09:32:50 +1000 Subject: [PATCH 14/18] rename blob method to read --- body_reader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/body_reader.ts b/body_reader.ts index c67ee40..5ed5d48 100644 --- a/body_reader.ts +++ b/body_reader.ts @@ -21,7 +21,7 @@ export default class BodyReader { * * @returns {Promise} */ - async blob() { + async read() { return await Deno.readAll(this.request.body); } @@ -32,7 +32,7 @@ export default class BodyReader { */ async text() { const decoder = new TextDecoder(); - return decoder.decode(await this.blob()); + return decoder.decode(await this.read()); } /** From 9c5a51e2cbf9cc47f2467c9f6f24c885ed9cc6c5 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Sun, 2 Aug 2020 09:45:07 +1000 Subject: [PATCH 15/18] unused function --- _app.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/_app.ts b/_app.ts index 3d68b16..48f9acc 100644 --- a/_app.ts +++ b/_app.ts @@ -445,26 +445,6 @@ function routePath(filePath: string, config: ServaConfig): string { return "/" + (base ? path.join(base, name) : name); } -/** - * Returns the route method from a give route path. - * - * @example - * routeMethod("index.ts"); - * // => "*" - * - * routeMethod("/comments/[comment].get.ts") - * // => "GET" - * - * @param {string} filePath - * @param {ServaConfig} config - * @returns {string} - */ -function routeMethod(filePath: string, config: ServaConfig): string { - const [, method] = nameAndMethodFromPath(filePath, config); - - return method.toUpperCase(); -} - /** * Sort function for routes. * From 95879899fc6ba9e9ecfc6171560404803c19c7d9 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Sun, 2 Aug 2020 09:50:57 +1000 Subject: [PATCH 16/18] respecting routes with possible head method --- _app.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/_app.ts b/_app.ts index 48f9acc..b0013ed 100644 --- a/_app.ts +++ b/_app.ts @@ -338,7 +338,8 @@ export default class App { const possibleMethods = [req.method, "*"]; if (req.method === "HEAD") { - possibleMethods.splice(0, 1, "GET"); + // HEAD, GET, * + possibleMethods.splice(1, 0, "GET"); } possibleMethods.some((m) => { @@ -358,10 +359,7 @@ export default class App { // not found // @ts-ignore if (!route) { - req.respond({ - status: 404, - }); - return; + return req.respond({ status: 404 }); } const request = createRequest(req, route); From 239c0fffaa65ea48efbf564d5cc1fdf1994226a2 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Sun, 2 Aug 2020 09:52:10 +1000 Subject: [PATCH 17/18] interface typo --- factories.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factories.ts b/factories.ts index 6e070bb..c453ed0 100644 --- a/factories.ts +++ b/factories.ts @@ -5,7 +5,7 @@ export interface RouteFactory { (route: Route): [OnRequestCallback[], RouteCallback]; } -export interface Hooksfactory { +export interface HooksFactory { (route: Route): OnRequestCallback[]; } @@ -32,7 +32,7 @@ export function route( return routeFactory; } -export function hooks(callback: (api: HooksApi) => void): Hooksfactory { +export function hooks(callback: (api: HooksApi) => void): HooksFactory { function hooksFactory(route: Route): OnRequestCallback[] { const hooks: OnRequestCallback[] = []; callback({ onRequest: hooks.push.bind(hooks), route }); From 44da61b62ae662bb073f50bc0cd44402365ea179 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Sun, 2 Aug 2020 09:59:12 +1000 Subject: [PATCH 18/18] imports and exports --- _app.ts | 4 ++-- body_reader.ts | 8 ++++---- mod.ts | 6 +++--- request.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/_app.ts b/_app.ts index b0013ed..aa40aaa 100644 --- a/_app.ts +++ b/_app.ts @@ -1,4 +1,4 @@ -import { RouteFactory, Hooksfactory } from "./factories.ts"; +import { RouteFactory, HooksFactory } from "./factories.ts"; import createRequest, { ServaRequest } from "./request.ts"; import createRoute, { Route } from "./_route.ts"; import { fs, http, path, flags } from "./deps.ts"; @@ -271,7 +271,7 @@ export default class App { if (filename !== "_hooks") { throw new Error("Invalid hooks filename"); } - hooks = (callback as Hooksfactory)(route); + hooks = (callback as HooksFactory)(route); routeHooks.push([route.method, [route, hooks]]); continue; diff --git a/body_reader.ts b/body_reader.ts index 5ed5d48..74abdb5 100644 --- a/body_reader.ts +++ b/body_reader.ts @@ -1,6 +1,6 @@ import { http } from "./deps.ts"; -interface JsonOptions { +interface JSONOptions { reviver?: (this: any, key: string, value: any) => any; } @@ -38,10 +38,10 @@ export default class BodyReader { /** * Reads the request body as json. * - * @param {JsonOptions} [options] - * @returns {Promise} + * @param {JSONOptions} [options] + * @returns {Promise} */ - async json(options?: JsonOptions): Promise { + async json(options?: JSONOptions): Promise { return JSON.parse(await this.text(), options && options.reviver) as T; } diff --git a/mod.ts b/mod.ts index 7a2605e..3eaf814 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,3 @@ -export { RouteCallback, ServaConfig } from "./_app.ts"; -export { route, hooks } from "./factories.ts"; -export { ServaRequest } from "./request.ts"; +export { OnRequestCallback, RouteCallback, ServaConfig } from "./_app.ts"; +export { route, hooks, RouteFactory, HooksFactory } from "./factories.ts"; +export { ServaRequest, ServaResponse } from "./request.ts"; diff --git a/request.ts b/request.ts index b2e1e40..029cb69 100644 --- a/request.ts +++ b/request.ts @@ -2,7 +2,7 @@ import { Route } from "./_route.ts"; import { http } from "./deps.ts"; import BodyReader from "./body_reader.ts"; -interface ServaResponse extends http.Response { +export interface ServaResponse extends http.Response { headers: Headers; body?: any; // allow routes to return anything }