diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 8cbc1278..97be5561 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -26,8 +26,7 @@ jobs: uses: actions/checkout@v3 - name: Setup Deno - # uses: denoland/setup-deno@v1 - uses: denoland/setup-deno@004814556e37c54a2f6e31384c9e18e983317366 + uses: denoland/setup-deno@v1 with: deno-version: v1.x @@ -41,5 +40,5 @@ jobs: - name: Run tests run: deno task test - - name: Run profile - run: deno task profile + # - name: Run profile + # run: deno task profile diff --git a/README.md b/README.md index f5966c16..1c942389 100644 --- a/README.md +++ b/README.md @@ -9,28 +9,28 @@

  - - Server + + Types     - + Routing     - + Request handling     - + Response caching   @@ -64,21 +64,98 @@ - Community-driven - Popular tool integrations + contributions encouraged -

Getting started

+

Overview

+ +Routes and middleware are added to a `Router` instance with `.use`, `.addRoute` or `.get/post/put/delete`. + +The router is then used with your web server of choice, e.g. `Deno.serve`! ```js -import * as Peko from "https://deno.land/x/peko/mod.ts"; -// import from ".../peko/lib/Server.ts" for featherweight mode +import * as Peko from "https://deno.land/x/peko/mod.ts"; + +const router = new Peko.Router(); + +router.use(Peko.logger(console.log)); -const server = new Peko.Server(); +router.get("/shorthand-route", () => new Response("Hello world!")); -server.use(Peko.logger(console.log)); +router.post("/shorthand-route-ext", async (ctx, next) => { await next(); console.log(ctx.request.headers); }, (req) => new Response(req.body)); -server.get("/hello", () => new Response("Hello world!")); +router.addRoute({ + path: "/object-route", + middleware: async (ctx, next) => { await next(); console.log(ctx.request.headers); }, // can also be array of middleware + handler: () => new Response("Hello world!") +}) -server.listen(7777, () => console.log("Peko server started - let's go!")); +router.addRoutes([ /* array of route objects */ ]) + +Deno.serve((req) => router.requestHandler(req)) ``` +

Types

+ +### [**Router**](https://deno.land/x/peko/mod.ts?s=Router) +The main class of Peko, provides `requestHandler` method to generate `Response` from `Request` via configured routes and middleware. + +### [**Route**](https://deno.land/x/peko/mod.ts?s=Route) +Objects with `path`, `method`, `middleware`, and `handler` properties. Requests are matched to a regex generated from the given path. Dynamic parameters are supported in the `/users/:userid` syntax. + +### [**RequestContext**](https://deno.land/x/peko/mod.ts?s=RequestContext) +An object containing `url`, `params` and `state` properties that is provided to all middleware and handler functions associated to a router or matched route. + +### [**Middleware**](https://deno.land/x/peko/mod.ts?s=Middleware) +Functions that receives a RequestContext and a next fcn. Should update `ctx.state`, perform side-effects or return a response. + +### [**Handler**](https://deno.land/x/peko/mod.ts?s=Handler) +The final request handling function on a `Route`. Must generate and return a response using the provided request context. + +

Request handling

+ +Each route must have a handler function that generates a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). Upon receiving a request the `Server` will construct a [RequestContext](https://deno.land/x/peko/server.ts?s=RequestContext) and cascade it through any global middleware, then route middleware and finally the route handler. Global and route middleware are invoked in the order they are added. If a response is returned by any middleware along the chain no subsequent middleware/handler will run. + +Peko comes with a library of utilities, middleware and handlers for common route use-cases, such as: +- server-side-rendering +- opening WebSockets/server-sent events +- JWT signing/verifying & authentication +- logging +- caching + +See `handlers`, `mmiddleware` or `utils` for source, or dive into `examples` for demo implementations. + +The second argument to any middleware is the `next` fcn. This returns a promise that resolves to the first response returned by any subsequent middleware/handler. This is useful for error-handling as well as post-response operations such as editing headers or logging. See the below snippet or `middleware/logger.ts` for examples. + +If no matching route is found for a request an empty 404 response is sent. If an error occurs in handling a request an empty 500 response is sent. Both of these behaviours can be overwritten with the following middleware: + +```js +router.use(async (_, next) => { + const response = await next(); + if (!response) return new Response("Would you look at that? Nothing's here!", { status: 404 }); +}); +``` + +```js +router.use(async (_, next) => { + try { + await next(); + } catch(e) { + console.log(e); + return new Response("Oh no! An error occured :(", { status: 500 }); + } +}); +``` + +

Response caching

+ +In stateless computing, memory should only be used for source code and disposable cache data. Response caching ensures that we only store data that can be regenerated or refetched. Peko provides a `ResponseCache` utility for this with configurable item lifetime. The `cacher` middleware wraps it and provides drop in handler memoization and response caching for your routes. + +```js +const cache = new Peko.ResponseCache({ lifetime: 5000 }); + +router.addRoute("/do-stuff", Peko.cacher(cache), () => new Response(Date.now())); +``` + +And that's it! Check out the API docs for deeper info. Otherwise happy coding 🤓 +

App showcase

PR to add your project 🙌 @@ -122,5 +199,3 @@ The modern JavaScript edge rocks because the client-server gap practically disap This is made possible by engines such as Deno that are built to the [ECMAScript](https://tc39.es/) specification (support for URL module imports is the secret sauce). UI libraries like [Preact](https://github.com/preactjs/preact) combined with [htm](https://github.com/developit/htm) offer lightning fast client-side hydration with a browser-friendly markup syntax. Deno also has native TypeScript support, a rich runtime API and loads of community tools for your back-end needs. If you are interested in contributing please submit a PR or get in contact ^^ - -Read `overview.md` for a more detailed guide on using Peko. diff --git a/examples/auth/app.ts b/examples/auth/app.ts index 83a07df5..bef699b8 100644 --- a/examples/auth/app.ts +++ b/examples/auth/app.ts @@ -1,6 +1,7 @@ -import * as Peko from "https://deno.land/x/peko/mod.ts" +import * as Peko from "../../mod.ts" // "https://deno.land/x/peko/mod.ts" -const server = new Peko.Server() +const html = String +const router = new Peko.Router() const crypto = new Peko.Crypto("SUPER_SECRET_KEY_123") // <-- replace from env const user = { // <-- replace with db / auth provider query username: "test-user", @@ -13,8 +14,8 @@ const validateUser = async (username: string, password: string) => { && await crypto.hash(password) === user.password } -server.use(Peko.logger(console.log)) -server.post("/login", async (ctx) => { +router.use(Peko.logger(console.log)) +router.post("/login", async (ctx) => { const { username, password } = await ctx.request.json() if (!await validateUser(username, password)) { @@ -37,14 +38,13 @@ server.post("/login", async (ctx) => { }) }) -server.get( +router.get( "/verify", Peko.authenticator(crypto), () => new Response("You are authenticated!") ) -const html = String -server.get("/", Peko.ssrHandler(() => html` +router.get("/asdf", Peko.ssrHandler(() => html` Peko auth example @@ -132,4 +132,4 @@ server.get("/", Peko.ssrHandler(() => html` `)) -server.listen() \ No newline at end of file +Deno.serve((req) => router.requestHandler(req)) \ No newline at end of file diff --git a/examples/preact/index.ts b/examples/preact/index.ts index 155a852a..cae8eab9 100644 --- a/examples/preact/index.ts +++ b/examples/preact/index.ts @@ -1,20 +1,20 @@ -import { Server, logger } from "https://deno.land/x/peko/mod.ts" +import { Router, logger } from "../../mod.ts" //"https://deno.land/x/peko/mod.ts" import pages from "./routes/pages.ts" import assets from "./routes/assets.ts" import APIs from "./routes/APIs.ts" // initialize server -const server = new Server() -server.use(logger(console.log)) +const router = new Router() +router.use(logger(console.log)) // SSR'ed app page routes -server.addRoutes(pages) +router.addRoutes(pages) // Static assets -server.addRoutes(assets) +router.addRoutes(assets) // Custom API functions -server.addRoutes(APIs) +router.addRoutes(APIs) -// Start Peko server :^) -server.listen() \ No newline at end of file +// Start Deno server with Peko router :^) +Deno.serve((req) => router.requestHandler(req)) \ No newline at end of file diff --git a/examples/preact/routes/APIs.ts b/examples/preact/routes/APIs.ts index 2dc5c2de..cadbeef3 100644 --- a/examples/preact/routes/APIs.ts +++ b/examples/preact/routes/APIs.ts @@ -2,7 +2,7 @@ import { RequestContext, Route, sseHandler -} from "https://deno.land/x/peko/mod.ts" +} from "../../../mod.ts" const demoEventTarget = new EventTarget() setInterval(() => { diff --git a/examples/preact/routes/assets.ts b/examples/preact/routes/assets.ts index 5d3a18bd..1d61ed12 100644 --- a/examples/preact/routes/assets.ts +++ b/examples/preact/routes/assets.ts @@ -3,7 +3,7 @@ import { staticHandler, cacher, ResponseCache -} from "https://deno.land/x/peko/mod.ts" +} from "../../../mod.ts" import { recursiveReaddir } from "https://deno.land/x/recursive_readdir@v2.0.0/mod.ts" import { fromFileUrl } from "https://deno.land/std@0.174.0/path/mod.ts" diff --git a/examples/preact/routes/pages.ts b/examples/preact/routes/pages.ts index 3e08f1af..6eb17363 100644 --- a/examples/preact/routes/pages.ts +++ b/examples/preact/routes/pages.ts @@ -3,7 +3,7 @@ import { ssrHandler, cacher, ResponseCache -} from "https://deno.land/x/peko/mod.ts" +} from "../../../mod.ts" import { renderToString } from "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string" diff --git a/examples/preact/src/components/Layout.js b/examples/preact/src/components/Layout.js index 3ff0baad..426fcd6f 100644 --- a/examples/preact/src/components/Layout.js +++ b/examples/preact/src/components/Layout.js @@ -5,7 +5,8 @@ const Layout = ({ navColor, navLink, children }) => {
@@ -26,7 +27,7 @@ const Layout = ({ navColor, navLink, children }) => { Home About -

Made by Seb R

+

Made by Sejori

` } diff --git a/examples/preact/src/pages/Home.js b/examples/preact/src/pages/Home.js index 52c05fc0..1819c08b 100644 --- a/examples/preact/src/pages/Home.js +++ b/examples/preact/src/pages/Home.js @@ -7,12 +7,18 @@ const Home = () => { <${Layout} navLink="about" navColor="#101727">

Features

+

Guides

+
    +
  1. How to build a full-stack React application with Peko and Deno
  2. +
  3. Want to build a lightweight HTML or Preact app? Check out the examples!
  4. +
+

Handlers

@@ -36,8 +42,10 @@ const Home = () => {

Utils

diff --git a/lib/utils/Router.ts b/lib/Router.ts similarity index 58% rename from lib/utils/Router.ts rename to lib/Router.ts index 4a1ef292..ab2ee691 100644 --- a/lib/utils/Router.ts +++ b/lib/Router.ts @@ -1,12 +1,27 @@ -import { Middleware, Handler, Route } from "../types.ts" -import { Cascade } from "./Cascade.ts" +import { Cascade } from "./utils/Cascade.ts" +import { Middleware, Handler, Route } from "./types.ts" + +export class RequestContext { + url: URL + state: Record + params: Record = {} + + constructor( + public router: Router, + public request: Request, + state?: Record + ) { + this.url = new URL(request.url) + this.state = state ? state : {} + } +} export class _Route implements Route { path: `/${string}` params: Record = {} regexPath: RegExp method?: "GET" | "POST" | "PUT" | "DELETE" - middleware?: Middleware[] | Middleware + middleware: Middleware[] = [] handler: Handler constructor(routeObj: Route) { @@ -18,8 +33,8 @@ export class _Route implements Route { if (str[0] === ":") this.params[str.slice(1)] = i }); this.regexPath = this.params - ? new RegExp(this.path.replaceAll(/(?<=\/):(.)*?(?=\/|$)/g, "(.)*")) - : new RegExp(this.path) + ? new RegExp(`^${this.path.replaceAll(/(?<=\/):(.)*?(?=\/|$)/g, "(.)*")}\/?$`) + : new RegExp(`^${this.path}\/?$`) this.method = routeObj.method || "GET" this.handler = Cascade.promisify(routeObj.handler!) as Handler @@ -31,20 +46,35 @@ export class _Route implements Route { } export class Router { - constructor(public routes: _Route[] = []) {} - - static applyDefaults(routeObj: Partial): Route { - if (!routeObj.path) throw new Error("Route is missing path") - if (!routeObj.handler) throw new Error("Route is missing handler") + constructor( + public routes: _Route[] = [], + public middleware: Middleware[] = [] + ) {} - routeObj.method = routeObj.method || "GET" - routeObj.handler = Cascade.promisify(routeObj.handler!) as Handler - routeObj.middleware = [routeObj.middleware] - .flat() - .filter(Boolean) - .map((mware) => Cascade.promisify(mware!)) + /** + * Generate Response by running route middleware/handler with Cascade. + * @param request: Request + * @returns Promise + */ + async requestHandler(request: Request): Promise { + const ctx = new RequestContext(this, request) + const res = await new Cascade(ctx, this.middleware).run() + return res + ? res + : new Response("", { status: 404 }) + } - return routeObj as Route + /** + * Add global middleware or another router's middleware + * @param middleware: Middleware[] | Middleware | Router + * @returns number - server.middleware.length + */ + use(middleware: Middleware | Middleware[]) { + if (Array.isArray(middleware)) { + middleware.forEach(mware => this.use(mware)) + return middleware.length + } + return this.middleware.push(Cascade.promisify(middleware)) } /** @@ -69,13 +99,29 @@ export class Router { ? { path: arg1, handler: arg2 as Handler } : { path: arg1, ...arg2 as Partial } : { path: arg1, middleware: arg2 as Middleware | Middleware[], handler: arg3 } + + const fullRoute = new _Route(routeObj as Route) - if (this.routes.find(existing => existing.path === routeObj.path)) { + if (this.routes.find(existing => existing.regexPath.toString() === fullRoute.regexPath.toString())) { throw new Error(`Route with path ${routeObj.path} already exists!`) } - - const fullRoute = new _Route(routeObj as Route) + this.routes.push(fullRoute) + this.middleware.push(function RouteMiddleware(ctx) { + if ( + fullRoute.regexPath.test(ctx.url.pathname) && + fullRoute.method === ctx.request.method + ) { + if (fullRoute?.params) { + const pathBits = ctx.url.pathname.split("/") + for (const param in fullRoute.params) { + ctx.params[param] = pathBits[fullRoute.params[param]] + } + } + + return new Cascade(ctx, [...fullRoute.middleware, fullRoute.handler]).run() + } + }) return fullRoute } @@ -126,34 +172,34 @@ export class Router { /** * Add Routes * @param routes: Route[] - middleware can be Middlewares or Middleware - * @returns number - routes.length + * @returns Route[] - added routes */ - addRoutes(routes: Route[]): number { - routes.forEach(route => this.addRoute(route)) - return this.routes.length + addRoutes(routes: Route[]): Route[] { + return routes.map(route => this.addRoute(route)) } /** * Remove Route from Peko server * @param route: Route["path"] of route to remove - * @returns + * @returns Route - removed route */ - removeRoute(route: Route["path"]): number { + removeRoute(route: Route["path"]): Route | undefined { const routeToRemove = this.routes.find(r => r.path === route) if (routeToRemove) { this.routes.splice(this.routes.indexOf(routeToRemove), 1) } - return this.routes.length + return routeToRemove } /** * Remove Routes * @param routes: Route["path"] of routes to remove - * @returns + * @returns Array - removed routes */ - removeRoutes(routes: Route["path"][]): number { - routes.forEach(route => this.removeRoute(route)) - return this.routes.length + removeRoutes(routes: Route["path"][]): Array { + return routes.map(route => this.removeRoute(route)) } -} \ No newline at end of file +} + +export default Router \ No newline at end of file diff --git a/lib/Server.ts b/lib/Server.ts deleted file mode 100644 index 3cfb523a..00000000 --- a/lib/Server.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Server as stdServer } from "https://deno.land/std@0.174.0/http/server.ts" -import { Router, _Route } from "./utils/Router.ts" -import { Cascade, PromiseMiddleware } from "./utils/Cascade.ts" -import { Middleware } from "./types.ts" - -export class RequestContext { - url: URL - _route: _Route | undefined - state: Record - params: Record = {} - - constructor( - public server: Server, - public request: Request, - state?: Record - ) { - this.url = new URL(request.url) - this.state = state ? state : {} - } - - get route () { - return this._route - } - - set route (r: _Route | undefined) { - this._route = r; - if (r?.params) { - const pathBits = this.url.pathname.split("/") - for (const param in r.params) { - this.params[param] = pathBits[r.params[param]] - } - } - } -} - -export class Server extends Router { - stdServer: stdServer | undefined - port = 7777 - hostname = "127.0.0.1" - middleware: PromiseMiddleware[] = [] - routers: Router[] = [] - - public get allRoutes(): _Route[] { - return [ this, ...this.routers].map(router => router.routes).flat() - } - - constructor(config?: { - port?: number, - hostname?: string, - }) { - super() - if (!config) return - const { port, hostname } = config - if (port) this.port = port - if (hostname) this.hostname = hostname - } - - /** - * Add global middleware or another router - * @param middleware: Middleware[] | Middleware | Router - * @returns number - server.middleware.length - */ - use(middleware: Middleware | Middleware[] | Router) { - if (middleware instanceof Router) { - return this.routers.push(middleware) - } - - if (Array.isArray(middleware)) { - middleware.forEach(mware => this.use(mware)) - return middleware.length - } - return this.middleware.push(Cascade.promisify(middleware)) - } - - /** - * Start listening to HTTP requests. - * @param port: number - * @param onListen: onListen callback function - * @param onError: error handler - */ - async listen( - port?: number, - onListen?: (server: stdServer) => void, - onError?: (error: unknown) => Response | Promise - ): Promise { - if (port) this.port = port - - this.stdServer = new stdServer({ - port: this.port, - hostname: this.hostname, - handler: (request: Request) => this.requestHandler.call(this, request), - onError - }) - - if (onListen) { - onListen(this.stdServer) - } else { - console.log(`Peko server started on port ${this.port} with routes:`) - this.allRoutes.forEach((route, i) => console.log(`${route.method} ${route.path} ${i===this.routes.length-1 ? "\n" : ""}`)) - } - - return await this.stdServer.listenAndServe() - } - - /** - * Generate Response by running route middleware/handler with Cascade. - * @param request: Request - * @returns Promise - */ - async requestHandler(request: Request): Promise { - const ctx: RequestContext = new RequestContext(this, request) - - ctx.route = this.allRoutes.find(route => - route.regexPath.test(ctx.url.pathname) && - route.method === request.method - ) - - return await new Cascade(ctx).start() - } - - /** - * Stop listening to HTTP requests. - * @param port: number - * @param onListen: onListen callback function - * @param onError: onListen callback function - */ - close(): void { - if (this.stdServer) this.stdServer.close() - } -} diff --git a/lib/handlers/sse.ts b/lib/handlers/sse.ts index 88e9e3a4..8fb578d1 100644 --- a/lib/handlers/sse.ts +++ b/lib/handlers/sse.ts @@ -1,5 +1,5 @@ -import { Handler, HandlerOptions } from "../types.ts" import { mergeHeaders } from "../utils/helpers.ts" +import { Handler, HandlerOptions } from "../types.ts" const encoder = new TextEncoder() diff --git a/lib/handlers/ssr.ts b/lib/handlers/ssr.ts index 2ec7a40f..9664a75e 100644 --- a/lib/handlers/ssr.ts +++ b/lib/handlers/ssr.ts @@ -1,7 +1,7 @@ -import { RequestContext } from "../Server.ts" -import { Handler, HandlerOptions } from "../types.ts" +import { RequestContext } from "../Router.ts" import { Crypto } from "../utils/Crypto.ts" import { mergeHeaders } from "../utils/helpers.ts" +import { Handler, HandlerOptions } from "../types.ts" export type Render = (ctx: RequestContext) => BodyInit | Promise export interface ssrHandlerOptions extends HandlerOptions { diff --git a/lib/handlers/static.ts b/lib/handlers/static.ts index 297b99dd..a0215953 100644 --- a/lib/handlers/static.ts +++ b/lib/handlers/static.ts @@ -1,9 +1,9 @@ import { contentType } from "https://deno.land/std@0.174.0/media_types/mod.ts"; import { fromFileUrl } from "https://deno.land/std@0.174.0/path/mod.ts" -import { RequestContext } from "../Server.ts" -import { Handler, HandlerOptions } from "../types.ts" +import { RequestContext } from "../Router.ts" import { Crypto } from "../utils/Crypto.ts" import { mergeHeaders } from "../utils/helpers.ts" +import { Handler, HandlerOptions } from "../types.ts" const crypto = new Crypto(Array.from({length: 10}, () => Math.floor(Math.random() * 9)).toString()) export interface staticHandlerOptions extends HandlerOptions { diff --git a/lib/handlers/ws.ts b/lib/handlers/ws.ts deleted file mode 100644 index 505afaca..00000000 --- a/lib/handlers/ws.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Handler, HandlerOptions } from "../types.ts" - -/** - * Upgrades requests with "upgrade: websocket" header to WebSocket connection. - * Streams type "send" CustomEvents from provided EventTarget to client. - * Dispatches received MessageEvents to provided EventTarget. - * Routes using this handler should be requested via the WebSocket browser API. - * @param target: EventTarget - * @param opts: (optional) HandlerOptions - * @returns Handler: (ctx: RequestContext) => Promise - */ -export const wsHandler = (socketCallback: (s: WebSocket) => unknown, opts: HandlerOptions = {}): Handler => (ctx) => { - const { request } = ctx - const conn = request.headers.get("connection") || ""; - const upgrade = request.headers.get("upgrade") || ""; - if (!conn.toLowerCase().includes("upgrade") || upgrade.toLowerCase() != "websocket") { - return new Response("request isn't trying to upgrade to websocket.", { status: 400 }); - } - - const { socket, response } = Deno.upgradeWebSocket(request); - socketCallback(socket) - - if (opts.headers) for (const header of opts.headers) { - response.headers.append(header[0], header[1]) - } - - return response -} \ No newline at end of file diff --git a/lib/middleware/authenticator.ts b/lib/middleware/authenticator.ts index 45e043fb..772412c9 100644 --- a/lib/middleware/authenticator.ts +++ b/lib/middleware/authenticator.ts @@ -1,5 +1,5 @@ -import { Middleware } from "../types.ts" import { Crypto } from "../utils/Crypto.ts" +import { Middleware } from "../types.ts" /** * Auth middleware, uses Crypto utility class to verify JWTs diff --git a/lib/middleware/cacher.ts b/lib/middleware/cacher.ts index 035d0831..849868e6 100644 --- a/lib/middleware/cacher.ts +++ b/lib/middleware/cacher.ts @@ -1,6 +1,6 @@ -import type { RequestContext } from "../Server.ts"; -import { Middleware } from "../types.ts"; +import type { RequestContext } from "../Router.ts"; import { ResponseCache } from "../utils/ResponseCache.ts"; +import { Middleware } from "../types.ts"; // default key generator const defaultKeygen = (ctx: RequestContext) => { diff --git a/lib/types.ts b/lib/types.ts index ba0e80b8..36f1eb8e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,4 @@ -import { RequestContext } from "./Server.ts" +import { RequestContext } from "./Router.ts" export interface Route { path: `/${string}` @@ -7,7 +7,7 @@ export interface Route { handler: Handler } -export type Result = void | Response +export type Result = void | Response | undefined export type Next = () => Promise | Result export type Middleware = (ctx: RequestContext, next: Next) => Promise | Result diff --git a/lib/utils/Cascade.ts b/lib/utils/Cascade.ts index 6444fdb4..cf5d0d5e 100644 --- a/lib/utils/Cascade.ts +++ b/lib/utils/Cascade.ts @@ -1,21 +1,16 @@ -import { RequestContext } from "../Server.ts" +import { RequestContext } from "../Router.ts" import { Middleware, Result, Next } from "../types.ts" export type PromiseMiddleware = (ctx: RequestContext, next: Next) => Promise /** - * Utility class for running middleware functions as a cascade + * Utility class for running middleware functions in a cascade */ export class Cascade { - response: Response | undefined + result: Result called = 0 - toCall: PromiseMiddleware[] - constructor(public ctx: RequestContext) { - this.toCall = this.ctx.route - ? [...this.ctx.server.middleware, ...this.ctx.route.middleware as PromiseMiddleware[], this.ctx.route.handler as PromiseMiddleware] - : [...this.ctx.server.middleware] - } + constructor(public ctx: RequestContext, public middleware: Middleware[]) {} static promisify = (fcn: Middleware): PromiseMiddleware => { return fcn.constructor.name === "AsyncFunction" @@ -25,24 +20,17 @@ export class Cascade { }) } - async run(fcn: PromiseMiddleware): Promise { - if (!fcn) return this.response - - try { - const response = await fcn(this.ctx, async () => await this.run(this.toCall[++this.called])) - if (response) this.response = response - if (!this.response) await this.run(this.toCall[++this.called]) - } catch (error) { - throw error + async run(): Promise { + if (this.middleware[this.called]) { + try { + const res = await this.middleware[this.called++](this.ctx, () => this.run.call(this)) + if (res) this.result = res + else return await this.run() + } catch (error) { + throw error + } } - - return this.response - } - - async start() { - await this.run(this.toCall[this.called]) - return this.response - ? this.response - : new Response("", { status: 404 }) + + return this.result } } diff --git a/lib/utils/Profiler.ts b/lib/utils/Profiler.ts index c181f8d9..29524d86 100644 --- a/lib/utils/Profiler.ts +++ b/lib/utils/Profiler.ts @@ -1,4 +1,4 @@ -import { Server } from "../Server.ts" +import { Router } from "../Router.ts" import { Route } from "../types.ts" type ProfileConfig = { @@ -20,22 +20,22 @@ type ProfileResults = Record< } > -class Profiler { +export class Profiler { /** * Benchmark performance of all server routes one at a time - * @param server + * @param router * @param config * @returns results: ProfileResults */ - static async run(server: Server, config?: ProfileConfig) { - const url = (config && config.url) || `http://${server.hostname}:${server.port}` + static async run(router: Router, config?: ProfileConfig) { + const url = (config && config.url) || `http://localhost:7777` const count = (config && config.count) || 100 const excludedRoutes = (config && config.excludedRoutes) || [] const mode = (config && config.mode) || "serve" const results: ProfileResults = {} - for (const route of server.routes) { + for (const route of router.routes) { results[route.path] = { avgTime: 0, requests: [] } if (!excludedRoutes.includes(route)) { @@ -45,7 +45,7 @@ class Profiler { const start = Date.now() const response = mode === "serve" ? await fetch(routeUrl) - : await server.requestHandler(new Request(routeUrl)) + : await router.requestHandler(new Request(routeUrl)) const end = Date.now() return results[route.path].requests.push({ diff --git a/mod.ts b/mod.ts index 0aa295ad..42bb109a 100644 --- a/mod.ts +++ b/mod.ts @@ -3,7 +3,7 @@ */ // Core classes, functions & types -export * from "./lib/Server.ts" +export * from "./lib/Router.ts" export * from "./lib/types.ts" // Handlers @@ -17,11 +17,10 @@ export * from "./lib/middleware/cacher.ts" export * from "./lib/middleware/authenticator.ts" // Utils -export * from "./lib/utils/Router.ts" export * from "./lib/utils/Cascade.ts" export * from "./lib/utils/ResponseCache.ts" export * from "./lib/utils/Crypto.ts" export * from "./lib/utils/helpers.ts" -import { Server } from "./lib/Server.ts" -export default Server \ No newline at end of file +import { Router } from "./lib/Router.ts" +export default Router \ No newline at end of file diff --git a/overview.md b/overview.md deleted file mode 100644 index e7901e5b..00000000 --- a/overview.md +++ /dev/null @@ -1,115 +0,0 @@ -

Peko library overview

- - - -

Server

- -The TypeScript `server.ts` module describes a small framework for building HTTP servers on top of the Deno http/server module. - -Here are the main components: - -- **Server class**: which manages the HTTP server, the routes, and the middleware. -- **RequestContext class:** holds information about the server, the request, and state to be shared between middleware. - -Main types (`types.ts`): - -- **Route**: an object with path, method, middleware, and handler properties. -- **Middleware**: a function that receives a RequestContext and updates state or generates a response. -- **Handler**: a function that handles requests by receiving a RequestContext and generating a response. - -The Server class has several methods for adding and removing routes and middleware, as well as starting the server and handling requests: - -- **use(middleware: Middleware | Middleware[] | Router)**: add global middleware or a router. -- **addRoute(route: Route)**: adds a route to the server. -- **addRoutes(routes: Route[])**: adds multiple routes to the server. -- **removeRoute(route: string)**: removes a route from the server. -- **removeRoutes(routes: string[])**: removes multiple routes from the server. -- **listen(port?: number, onListen?: callback)**: starts listening to HTTP requests on the specified port. -- **close()**: stops to HTTP listener process. - -```js -import * as Peko from "https://deno.land/x/peko/mod.ts"; // or "../server.ts" for super featherweight - -const server = new Peko.Server(); - -server.use(Peko.logger(console.log)); - -server.addRoute("/hello", () => new Response("Hello world!")); - -server.listen(7777, () => console.log("Peko server started - let's go!")); -``` - -

Routing

- -Routes can be added to a Server instance directly or a Router instance. Below you can see the different ways routes can be added with `addRoute`. - -```js -import * as Peko from "https://deno.land/x/peko/mod.ts"; // or "https://deno.land/x/peko/server.ts" - -const server = new Peko.Server() -server.addRoute("/hello", () => new Response("Hello world!")) -server.removeRoute("/hello"); - -const router = new Peko.Router() - -router.addRoute("/shorthand-route", async (ctx, next) => { await next(); console.log(ctx.request.headers); }, () => new Response("Hello world!")); - -router.addRoute({ - path: "/object-route", - middleware: async (ctx, next) => { await next(); console.log(ctx.request.headers); }, // can also be array of middleware - handler: () => new Response("Hello world!") -}) - -router.addRoutes([ /* array of route objects */ ]) - -server.use(router) - -server.listen() -``` - -

Request handling

- -Each route must have a handler function that generates a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). Upon receiving a request the `Server` will construct a [RequestContext](https://deno.land/x/peko/server.ts?s=RequestContext) and cascade it through any global middleware, then route middleware and finally the route handler. Global and route middleware are invoked in the order they are added. If a response is returned by any middleware along the chain no subsequent middleware/handler will run. - -Peko comes with a library of utilities, middleware and handlers for common route use-cases, such as: -- server-side-rendering -- opening WebSockets -- JWT signing/verifying & authentication -- logging -- caching - -See `handlers`, `mmiddleware` or `utils` for source, or dive into `examples` for demo implementations. - -The second argument to any middleware is the `next` fcn. This returns a promise that resolves to the first response returned by any subsequent middleware/handler. This is useful for error-handling as well as post-response operations such as logging. See the below snippet or `middleware/logger.ts` for examples. - -If no matching route is found for a request an empty 404 response is sent. If an error occurs in handling a request an empty 500 response is sent. Both of these behaviours can be overwritten with the following middleware: - -```js -server.use(async (_, next) => { - const response = await next(); - if (!response) return new Response("Would you look at that? Nothing's here!", { status: 404 }); -}); -``` - -```js -server.use(async (_, next) => { - try { - await next(); - } catch(e) { - console.log(e); - return new Response("Oh no! An error occured :(", { status: 500 }); - } -}); -``` - -

Response caching

- -In stateless computing, memory should only be used for source code and disposable cache data. Response caching ensures that we only store data that can be regenerated or refetched. Peko provides a `ResponseCache` utility for this with configurable item lifetime. The `cacher` middleware wraps it and provides drop in handler memoization and response caching for your routes. - -```js -const cache = new Peko.ResponseCache({ lifetime: 5000 }); - -server.addRoute("/do-stuff", Peko.cacher(cache), () => new Response(Date.now())); -``` - -And that's it! Check out the API docs for deeper info. Otherwise happy coding 🤓 diff --git a/react.md b/react.md new file mode 100644 index 00000000..e6737c3b --- /dev/null +++ b/react.md @@ -0,0 +1 @@ +## THESE DOCS ARE A WORK IN PROGRESS \ No newline at end of file diff --git a/tests/Router_test.ts b/tests/Router_test.ts new file mode 100644 index 00000000..5339bf05 --- /dev/null +++ b/tests/Router_test.ts @@ -0,0 +1,164 @@ +import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" +import { Router } from "./../lib/Router.ts" +import { + testMiddleware2, + testMiddleware3, + testMiddleware1, + testHandler +} from "./mocks/middleware.ts" + +Deno.test("ROUTER: ADDING/REMOVING ROUTES", async (t) => { + const router = new Router() + + await t.step("routes added with full route and string arg options", async () => { + router.addRoute({ path: "/route1", handler: testHandler }) + router.addRoute("/route2", testMiddleware1, testHandler) + router.addRoute("/route3", { middleware: testMiddleware1, handler: testHandler }) + router.addRoute("/route4", [testMiddleware1, testMiddleware2], testHandler) + + assert(router.routes.length === 4) + + const request1 = new Request("http://localhost:7777/route1") + const request2 = new Request("http://localhost:7777/route2") + const request3 = new Request("http://localhost:7777/route3") + const request4 = new Request("http://localhost:7777/route4") + + const response1 = await router.requestHandler(request1) + const response2 = await router.requestHandler(request2) + const response3 = await router.requestHandler(request3) + const response4 = await router.requestHandler(request4) + + assert(response1.status === 200) + assert(response2.status === 200) + assert(response3.status === 200) + assert(response4.status === 200) + }) + + await t.step("routes removed", () => { + router.removeRoute("/route1") + router.removeRoute("/route2") + router.removeRoute("/route3") + router.removeRoute("/route4") + + assert(router.routes.length === 0) + }) + + await t.step ("routers on server can be subsequently editted", () => { + const aRouter = new Router() + aRouter.addRoutes([ + { path: "/route", handler: testHandler }, + { path: "/route2", handler: testHandler }, + { path: "/route3", handler: testHandler } + ]) + + aRouter.use(aRouter.middleware) + + aRouter.removeRoute("/route") + + assert(!aRouter.routes.find(route => route.path === "/route")) + assert(aRouter.routes.length === 2) + }) + + await t.step ("http shorthand methods work correctly", () => { + const router = new Router() + + const getRoute = router.get({ + path: "/get", + handler: () => new Response("GET") + }) + const postRoute = router.post({ + path: "/post", + handler: () => new Response("POST") + }) + const putRoute =router.put({ + path: "/put", + handler: () => new Response("PUT") + }) + const deleteRoute = router.delete({ + path: "/delete", + handler: () => new Response("DELETE") + }) + + assert(router.routes.length === 4) + assert(getRoute.method === "GET") + assert(postRoute.method === "POST") + assert(putRoute.method === "PUT") + assert(deleteRoute.method === "DELETE") + }) + + await t.step("Params correctly stored", () => { + const router = new Router() + router.addRoute("/hello/:id/world/:name", () => new Response("Hi!")) + + assert(router.routes[0].params["id"] === 2) + assert(router.routes[0].params["name"] === 4) + }) +}) + +Deno.test("ROUTER: HANDLING REQUESTS", async (t) => { + const router = new Router() + router.middleware = [] + + await t.step("params discovered in RequestContext creation", async () => { + const newRouter = new Router(); + + newRouter.addRoute("/hello/:id/world/:name", (ctx) => { + return new Response(JSON.stringify({ id: ctx.params["id"], name: ctx.params["name"] })) + }) + + const res = await newRouter.requestHandler(new Request("http://localhost:7777/hello/123/world/bruno")) + const json = await res.json() + assert(json.id === "123" && json.name === "bruno") + }) + + await t.step("no route found triggers basic 404", async () => { + const request = new Request("http://localhost:7777/404") + const response = await router.requestHandler(request) + assert(response.status === 404) + }) + + await t.step("custom 404", async () => { + router.use(async (_, next) => { + const response = await next() + if (!response) return new Response("Uh-oh!", { status: 404 }) + }) + + const request = new Request("http://localhost:7777/404") + const response = await router.requestHandler(request) + + assert(response.status === 404) + assert(await response.text() === "Uh-oh!") + }) + + await t.step("custom 500", async () => { + router.use(async (_, next) => { + try { + await next() + } catch(_) { + return new Response("Error! :(", { status: 500 }) + } + }) + router.get("/error-test", () => { throw new Error("Oopsie!") }) + + const request = new Request("http://localhost:7777/error-test") + const response = await router.requestHandler(request) + + assert(response.status === 500) + assert(await response.text() === "Error! :(") + }) + + await t.step("all middleware and handlers run", async () => { + router.addRoute({ + path: "/test", + middleware: [testMiddleware1, testMiddleware2, testMiddleware3], + handler: testHandler + }) + + const request = new Request("http://localhost:7777/test") + const response = await router.requestHandler(request) + + const body = await response.json() + + assert(body["middleware1"] && body["middleware2"] && body["middleware3"]) + }) +}) diff --git a/tests/handlers/sse_test.ts b/tests/handlers/sse_test.ts index e985aa0c..238a14a3 100644 --- a/tests/handlers/sse_test.ts +++ b/tests/handlers/sse_test.ts @@ -1,10 +1,10 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server, RequestContext } from "../../lib/Server.ts" +import { Router, RequestContext } from "../../lib/Router.ts" import { sseHandler } from "../../lib/handlers/sse.ts" Deno.test("HANDLER: Server-sent events", async (t) => { - const server = new Server() - const ctx = new RequestContext(server, new Request("http://localhost")) + const router = new Router() + const ctx = new RequestContext(router, new Request("http://localhost")) const eventTarget = new EventTarget() const decoder = new TextDecoder() const testData = { diff --git a/tests/handlers/ssr_test.ts b/tests/handlers/ssr_test.ts index ca355890..0e979d1f 100644 --- a/tests/handlers/ssr_test.ts +++ b/tests/handlers/ssr_test.ts @@ -1,9 +1,9 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server, RequestContext } from "../../lib/Server.ts" +import { Router, RequestContext } from "../../lib/Router.ts" import { ssrHandler } from "../../lib/handlers/ssr.ts" Deno.test("HANDLER: Server-side render", async (t) => { - const server = new Server() + const server = new Router() const ctx = new RequestContext(server, new Request("http://localhost")) const decoder = new TextDecoder() const cacheControl = "max-age=60, stale-while-revalidate=10" diff --git a/tests/handlers/static_test.ts b/tests/handlers/static_test.ts index c6106106..e0d3c5cc 100644 --- a/tests/handlers/static_test.ts +++ b/tests/handlers/static_test.ts @@ -1,9 +1,9 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server, RequestContext } from "../../lib/Server.ts" +import { Router, RequestContext } from "../../lib/Router.ts" import { staticHandler } from "../../lib/handlers/static.ts" Deno.test("HANDLER: Static", async (t) => { - const server = new Server() + const server = new Router() const ctx = new RequestContext(server, new Request("http://localhost")) const fileURL = new URL(import.meta.url) const decoder = new TextDecoder() diff --git a/tests/handlers/ws_test.ts b/tests/handlers/ws_test.ts deleted file mode 100644 index b5a63232..00000000 --- a/tests/handlers/ws_test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server } from "../../lib/Server.ts" -import { wsHandler } from "../../lib/handlers/ws.ts" - -Deno.test("HANDLER: WebSocket", async (t) => { - const server = new Server() - const sockets: Map = new Map() - const client_messages: string[] = [] - - server.addRoute("/ws", wsHandler((socket) => { - const socketID = crypto.randomUUID() - socket.addEventListener("open", () => sockets.set(socketID, socket)) - socket.addEventListener("message", (e) => client_messages.push(e.data)) - socket.addEventListener("close", () => sockets.delete(socketID)) - })) - - server.listen(3000, () => null) - - await t.step("Socket created and closed as expected", async () => { - const socket = new WebSocket("ws://localhost:3000/ws") - assert(await openPromise(socket)) - - socket.close() - - assert(await closePromise(socket)) - }) - - await t.step("messages received from client as expected", async () => { - const socket = new WebSocket("ws://localhost:3000/ws") - await openPromise(socket) - - const test_string = "why hello my friend!" - socket.send(test_string) - - while (!client_messages[0]) await new Promise(res => setTimeout(res, 100)) - assert(client_messages[0] === test_string) - - socket.close() - await closePromise(socket) - }) - - await t.step("messages sent to clients as expected", async () => { - const socket1 = new WebSocket("ws://localhost:3000/ws") - await openPromise(socket1) - const socket2 = new WebSocket("ws://localhost:3000/ws") - await openPromise(socket2) - const socket3 = new WebSocket("ws://localhost:3000/ws") - await openPromise(socket3) - - const messages: string[] = [] - socket1.addEventListener("message", (e) => messages[0] = e.data) - socket2.addEventListener("message", (e) => messages[1] = e.data) - socket3.addEventListener("message", (e) => messages[2] = e.data) - - const test_string = "I am the greatest." - sockets.forEach(socket => socket.send(test_string)) - - while (messages.length < 3) await new Promise(res => setTimeout(res, 100)) - assert(messages.length === 3 && messages.every(message => message === test_string)) - - socket1.close() - await closePromise(socket1) - socket2.close() - await closePromise(socket2) - socket3.close() - await closePromise(socket3) - }) - - server.close() -}); - -function openPromise(conn: WebSocket): Promise { - return new Promise(resolve => { - conn.addEventListener("open", () => { - resolve(true); - }); - }); -} - -function closePromise(conn: WebSocket): Promise { - return new Promise(resolve => { - conn.addEventListener("close", () => { - resolve(true); - }); - }); -} \ No newline at end of file diff --git a/tests/middleware/authenticator_test.ts b/tests/middleware/authenticator_test.ts index 4d6612a4..f344cca4 100644 --- a/tests/middleware/authenticator_test.ts +++ b/tests/middleware/authenticator_test.ts @@ -1,12 +1,12 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server, RequestContext } from "../../lib/Server.ts" +import { Router, RequestContext } from "../../lib/Router.ts" import { authenticator } from "../../lib/middleware/authenticator.ts" import { Crypto } from "../../lib/utils/Crypto.ts" Deno.test("MIDDLEWARE: Authenticator", async (t) => { const successString = "Authorized!" const crypto = new Crypto("test_key") - const server = new Server() + const server = new Router() const testPayload = { iat: Date.now(), exp: Date.now() + 1000, data: { foo: "bar" }} const token = await crypto.sign(testPayload) diff --git a/tests/middleware/cacher_test.ts b/tests/middleware/cacher_test.ts index 40cc6a82..c8047b1f 100644 --- a/tests/middleware/cacher_test.ts +++ b/tests/middleware/cacher_test.ts @@ -1,11 +1,11 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server, RequestContext } from "../../lib/Server.ts" +import { Router, RequestContext } from "../../lib/Router.ts" import { cacher } from "../../lib/middleware/cacher.ts" import { testHandler } from "../mocks/middleware.ts" import { ResponseCache } from "../../lib/utils/ResponseCache.ts" Deno.test("MIDDLEWARE: Cacher", async (t) => { - const server = new Server() + const server = new Router() const successString = "Success!" const CACHE_LIFETIME = 100 const cache = new ResponseCache({ diff --git a/tests/middleware/logger_test.ts b/tests/middleware/logger_test.ts index 57571009..e2fd5bfc 100644 --- a/tests/middleware/logger_test.ts +++ b/tests/middleware/logger_test.ts @@ -1,12 +1,12 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server, RequestContext } from "../../lib/Server.ts" +import { Router, RequestContext } from "../../lib/Router.ts" import { logger } from "../../lib/middleware/logger.ts" Deno.test("MIDDLEWARE: Logger", async (t) => { const successString = "Success!" let logOutput: unknown - const server = new Server() + const server = new Router() const testData = { foo: "bar" @@ -18,7 +18,6 @@ Deno.test("MIDDLEWARE: Logger", async (t) => { await logFcn(ctx, () => new Response(successString)) // TODO test this string - console.log(logOutput) assert(logOutput) }) }) \ No newline at end of file diff --git a/tests/scripts/profile.ts b/tests/scripts/profile.ts index c25b14f6..ecc2a8d5 100644 --- a/tests/scripts/profile.ts +++ b/tests/scripts/profile.ts @@ -1,30 +1,36 @@ -import { Server } from "../../lib/Server.ts" +import { Router } from "../../lib/Router.ts" +import Profiler from "../../lib/utils/Profiler.ts" import { testMiddleware2, testMiddleware3, testHandler, testMiddleware1 } from "../mocks/middleware.ts" -import Profiler from "../../lib/utils/Profiler.ts" -const server = new Server() +const router = new Router() -server.addRoute("/test", [ +router.addRoute("/test", [ testMiddleware1, testMiddleware2, testMiddleware3 ], testHandler) -server.addRoute("/bench", () => new Response("Hello, bench!")) +router.get("/bench", () => new Response("Hello, bench!")) + +const abortController = new AbortController() -server.listen(8000, () => {}) +Deno.serve({ + port: 7777, + signal: abortController.signal +}, (req) => router.requestHandler(req)) -const handleResults = await Profiler.run(server, { +const handleResults = await Profiler.run(router, { mode: "handle", count: 100 }) -const serveResults = await Profiler.run(server, { +const serveResults = await Profiler.run(router, { mode: "serve", + url: "http://localhost:7777", count: 100 }) @@ -35,4 +41,4 @@ console.log("serve results") console.log("/test: " + serveResults["/test"].avgTime) console.log("/bench: " + serveResults["/bench"].avgTime) -server.close() \ No newline at end of file +abortController.abort() \ No newline at end of file diff --git a/tests/server_test.ts b/tests/server_test.ts deleted file mode 100644 index 5b882e2c..00000000 --- a/tests/server_test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Server } from "../lib/Server.ts" -import { - testMiddleware2, - testMiddleware3, - testHandler, - testMiddleware1 -} from "./mocks/middleware.ts" -import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" - -Deno.test("SERVER", async (t) => { - const server = new Server() - server.middleware = [] - - await t.step("routes added with full route and string arg options", async () => { - server.addRoute({ path: "/route", handler: testHandler }) - server.addRoute("/anotherRoute", { handler: testHandler }) - server.addRoute("/anotherNotherRoute", testHandler) - server.addRoute("/anotherNotherNotherRoute", testMiddleware2, testHandler) - - assert(server.routes.length === 4) - - const request = new Request("http://localhost:7777/route") - const anotherRequest = new Request("http://localhost:7777/anotherRoute") - const anotherNotherRequest = new Request("http://localhost:7777/anotherNotherRoute") - const anotherNotherNotherRequest = new Request("http://localhost:7777/anotherNotherRoute") - - const response = await server.requestHandler(request) - const anotherResponse = await server.requestHandler(anotherRequest) - const anotherNotherResponse = await server.requestHandler(anotherNotherRequest) - const anotherNotherNotherResponse = await server.requestHandler(anotherNotherNotherRequest) - - assert(response.status === 200) - assert(anotherResponse.status === 200) - assert(anotherNotherResponse.status === 200) - assert(anotherNotherNotherResponse.status === 200) - }) - - await t.step("no route found triggers basic 404", async () => { - const request = new Request("http://localhost:7777/404") - const response = await server.requestHandler(request) - assert(response.status === 404) - }) - - await t.step("custom 404", async () => { - server.use(async (_, next) => { - const response = await next() - if (!response) return new Response("Uh-oh!", { status: 404 }) - }) - - const request = new Request("http://localhost:7777/404") - const response = await server.requestHandler(request) - - assert(response.status === 404) - assert(await response.text() === "Uh-oh!") - }) - - await t.step("custom 500", async () => { - server.use(async (_, next) => { - try { - await next() - } catch(_) { - return new Response("Error! :(", { status: 500 }) - } - }) - server.addRoute("/error-test", () => { throw new Error("Oopsie!") }) - - const request = new Request("http://localhost:7777/error-test") - const response = await server.requestHandler(request) - - assert(response.status === 500) - assert(await response.text() === "Error! :(") - }) - - await t.step("all middleware and handlers run", async () => { - server.addRoute({ - path: "/test", - middleware: [testMiddleware1, testMiddleware2, testMiddleware3], - handler: testHandler - }) - - const request = new Request("http://localhost:7777/test") - const response = await server.requestHandler(request) - - const body = await response.json() - - assert(body["middleware1"] && body["middleware2"] && body["middleware3"]) - }) - - await t.step("params discovered in RequestContext creation", async () => { - const newServer = new Server(); - - newServer.addRoute("/hello/:id/world/:name", (ctx) => { - return new Response(JSON.stringify({ id: ctx.params["id"], name: ctx.params["name"] })) - }) - - const res = await newServer.requestHandler(new Request("http://localhost:7777/hello/123/world/bruno")) - const json = await res.json() - assert(json.id === "123" && json.name === "bruno") - }) -}) diff --git a/tests/server_test_old.ts b/tests/server_test_old.ts new file mode 100644 index 00000000..96f29abc --- /dev/null +++ b/tests/server_test_old.ts @@ -0,0 +1,100 @@ +// import { Server } from "../lib/utils/Router.ts" +// import { +// testMiddleware2, +// testMiddleware3, +// testHandler, +// testMiddleware1 +// } from "./mocks/middleware.ts" +// import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" + +// Deno.test("SERVER", async (t) => { +// const server = new Server() +// server.middleware = [] + +// await t.step("routes added with full route and string arg options", async () => { +// server.addRoute({ path: "/route", handler: testHandler }) +// server.addRoute("/anotherRoute", { handler: testHandler }) +// server.addRoute("/anotherNotherRoute", testHandler) +// server.addRoute("/anotherNotherNotherRoute", testMiddleware2, testHandler) + +// assert(server.routes.length === 4) + +// const request = new Request("http://localhost:7777/route") +// const anotherRequest = new Request("http://localhost:7777/anotherRoute") +// const anotherNotherRequest = new Request("http://localhost:7777/anotherNotherRoute") +// const anotherNotherNotherRequest = new Request("http://localhost:7777/anotherNotherRoute") + +// const response = await server.requestHandler(request) +// const anotherResponse = await server.requestHandler(anotherRequest) +// const anotherNotherResponse = await server.requestHandler(anotherNotherRequest) +// const anotherNotherNotherResponse = await server.requestHandler(anotherNotherNotherRequest) + +// assert(response.status === 200) +// assert(anotherResponse.status === 200) +// assert(anotherNotherResponse.status === 200) +// assert(anotherNotherNotherResponse.status === 200) +// }) + +// await t.step("no route found triggers basic 404", async () => { +// const request = new Request("http://localhost:7777/404") +// const response = await server.requestHandler(request) +// assert(response.status === 404) +// }) + +// await t.step("custom 404", async () => { +// server.use(async (_, next) => { +// const response = await next() +// if (!response) return new Response("Uh-oh!", { status: 404 }) +// }) + +// const request = new Request("http://localhost:7777/404") +// const response = await server.requestHandler(request) + +// assert(response.status === 404) +// assert(await response.text() === "Uh-oh!") +// }) + +// await t.step("custom 500", async () => { +// server.use(async (_, next) => { +// try { +// await next() +// } catch(_) { +// return new Response("Error! :(", { status: 500 }) +// } +// }) +// server.get("/error-test", () => { throw new Error("Oopsie!") }) + +// const request = new Request("http://localhost:7777/error-test") +// const response = await server.requestHandler(request) + +// assert(response.status === 500) +// assert(await response.text() === "Error! :(") +// }) + +// await t.step("all middleware and handlers run", async () => { +// server.addRoute({ +// path: "/test", +// middleware: [testMiddleware1, testMiddleware2, testMiddleware3], +// handler: testHandler +// }) + +// const request = new Request("http://localhost:7777/test") +// const response = await server.requestHandler(request) + +// const body = await response.json() + +// assert(body["middleware1"] && body["middleware2"] && body["middleware3"]) +// }) + +// await t.step("params discovered in RequestContext creation", async () => { +// const newServer = new Server(); + +// newServer.addRoute("/hello/:id/world/:name", (ctx) => { +// return new Response(JSON.stringify({ id: ctx.params["id"], name: ctx.params["name"] })) +// }) + +// const res = await newServer.requestHandler(new Request("http://localhost:7777/hello/123/world/bruno")) +// const json = await res.json() +// assert(json.id === "123" && json.name === "bruno") +// }) +// }) diff --git a/tests/utils/Cascade_test.ts b/tests/utils/Cascade_test.ts index ee9585d1..0331f71b 100644 --- a/tests/utils/Cascade_test.ts +++ b/tests/utils/Cascade_test.ts @@ -1,7 +1,6 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server, RequestContext } from "../../lib/Server.ts" +import { Router, RequestContext } from "../../lib/Router.ts" import { Cascade } from "../../lib/utils/Cascade.ts" -import { _Route } from "../../lib/utils/Router.ts" import { testMiddleware1, testMiddleware2, @@ -10,26 +9,19 @@ import { } from "../../tests/mocks/middleware.ts" Deno.test("UTIL: Cascade", async (t) => { - const testServer = new Server() + const testServer = new Router() const testContext = new RequestContext(testServer, new Request("http://localhost")) - testContext._route = new _Route({ - path: "/", - middleware: [ - testMiddleware1, - testMiddleware2, - testMiddleware3, - testHandler - ], - handler: testHandler - }) + const testMiddleware = [ + testMiddleware1, + testMiddleware2, + testMiddleware3, + testHandler + ] - const cascade = new Cascade(testContext) - - const result = await cascade.start() + const cascade = new Cascade(testContext, testMiddleware) + const result = await cascade.run() await t.step("promisify works", () => { - const testServer = new Server() - const testContext = new RequestContext(testServer, new Request("http://localhost")) const testMW = () => new Response("hello") const testMWProm = Cascade.promisify(testMW) assert(testMWProm(testContext, () => {}) instanceof Promise) diff --git a/tests/utils/Profiler_test.ts b/tests/utils/Profiler_test.ts index 788817e8..9e6ed58e 100644 --- a/tests/utils/Profiler_test.ts +++ b/tests/utils/Profiler_test.ts @@ -1,20 +1,20 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server } from "../../lib/Server.ts" -import Profiler from "../../lib/utils/Profiler.ts" +import { Router } from "../../lib/Router.ts" +import { Profiler } from "../../lib/utils/Profiler.ts" Deno.test("UTIL: Profiler", async (t) => { - const server = new Server() + const router = new Router() - server.addRoute("/hello", () => { + router.addRoute("/hello", () => { return new Response("Hello, World!") }) - server.addRoute("/goodbye", () => { + router.addRoute("/goodbye", () => { return new Response("Goodbye, World!") }) await t.step("profiles handled requests", async () => { - const results = await Profiler.run(server, { + const results = await Profiler.run(router, { mode: "handle", count: 10, excludedRoutes: [], @@ -36,13 +36,12 @@ Deno.test("UTIL: Profiler", async (t) => { }) await t.step("profiles served requests", async () => { - server.listen(8000, () => {}) + const abortController = new AbortController() + Deno.serve({ signal: abortController.signal }, (req) => router.requestHandler(req)) - // can't await listen so timeout necessary - await new Promise(res => setTimeout(res, 500)) - - const results = await Profiler.run(server, { + const results = await Profiler.run(router, { mode: "serve", + url: "http://localhost:8000", count: 10, excludedRoutes: [], }) @@ -61,6 +60,6 @@ Deno.test("UTIL: Profiler", async (t) => { await Promise.all(results["/hello"].requests.map(request => request.response.body?.cancel())) await Promise.all(results["/goodbye"].requests.map(request => request.response.body?.cancel())) - server.close() + abortController.abort() }) }); \ No newline at end of file diff --git a/tests/utils/Router_test.ts b/tests/utils/Router_test.ts deleted file mode 100644 index 1ae2a3af..00000000 --- a/tests/utils/Router_test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server } from "../../lib/Server.ts" -import { Router } from "../../lib/utils/Router.ts" -import { - testMiddleware1, - testHandler, -} from "../mocks/middleware.ts" - -Deno.test("ROUTER", async (t) => { - const router = new Router() - - await t.step("routes added with full route and string arg options", () => { - router.addRoute({ path: "/route", handler: testHandler }) - router.addRoute("/anotherRoute", { handler: testHandler }) - router.addRoute("/anotherNotherRoute", testHandler) - const finalRoute = router.addRoute("/anotherNotherNotherRoute", testMiddleware1, testHandler) - - assert(finalRoute.path === "/anotherNotherNotherRoute" && router.routes.length === 4) - }) - - await t.step("routes removed", () => { - router.removeRoute("/route") - router.removeRoute("/anotherRoute") - router.removeRoute("/anotherNotherRoute") - const routesLength = router.removeRoute("/anotherNotherNotherRoute") - - assert(routesLength === 0 && router.routes.length === 0) - }) - - await t.step ("routers on server can be subsequently editted", () => { - const server = new Server() - - const aRouter = new Router() - aRouter.addRoutes([ - { path: "/route", handler: testHandler }, - { path: "/route2", handler: testHandler }, - { path: "/route3", handler: testHandler } - ]) - - server.use(aRouter) - - aRouter.removeRoute("/route") - - assert(!aRouter.routes.find(route => route.path === "/route")) - assert(aRouter.routes.length === 2) - }) - - await t.step ("http shorthand methods work correctly", () => { - const router = new Router() - - const getRoute = router.get({ - path: "/get", - handler: () => new Response("GET") - }) - const postRoute = router.post({ - path: "/post", - handler: () => new Response("POST") - }) - const putRoute =router.put({ - path: "/put", - handler: () => new Response("PUT") - }) - const deleteRoute = router.delete({ - path: "/delete", - handler: () => new Response("DELETE") - }) - - assert(router.routes.length === 4) - assert(getRoute.method === "GET") - assert(postRoute.method === "POST") - assert(putRoute.method === "PUT") - assert(deleteRoute.method === "DELETE") - }) - - await t.step("Params correctly stored", () => { - const router = new Router() - router.addRoute("/hello/:id/world/:name", () => new Response("Hi!")) - - assert(router.routes[0].params["id"] === 2) - assert(router.routes[0].params["name"] === 4) - }) -}) diff --git a/tests/utils/helpers_test.ts b/tests/utils/helpers_test.ts index 968f8409..ad722313 100644 --- a/tests/utils/helpers_test.ts +++ b/tests/utils/helpers_test.ts @@ -1,10 +1,10 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" -import { Server } from "../../lib/Server.ts" +import { Router } from "../../lib/Router.ts" import { mergeHeaders, routesFromDir } from "../../lib/utils/helpers.ts" -import { staticHandler } from "../../mod.ts" +import { staticHandler } from "../../lib/handlers/static.ts" Deno.test("UTIL: helpers", async (t) => { await t.step("mergeHeaders", () => { @@ -19,7 +19,7 @@ Deno.test("UTIL: helpers", async (t) => { }) await t.step("routesFromDir returns all file routes with supplied middleware and handler", async () => { - const server = new Server() + const server = new Router() const request = new Request('https://localhost:7777/tests/utils/helpers_test.ts') let text = '' @@ -36,14 +36,15 @@ Deno.test("UTIL: helpers", async (t) => { assert(routes.find(route => route.path.includes("middleware"))) assert(routes.find(route => route.path.includes("mocks"))) assert(routes.find(route => route.path.includes("utils"))) + assert(routes.find(route => route.path.includes("scripts"))) server.addRoutes(routes) const response = await server.requestHandler(request) const fileText = await response.text() - assert(fileText == await Deno.readTextFile(new URL("./helpers_test.ts", import.meta.url))) assert(text === "I was set") + assert(fileText == await Deno.readTextFile(new URL("./helpers_test.ts", import.meta.url))) }) // TODO: sitemap test