From 0c8d1865ffc4eed41bf728050c6a2b80f354479f Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 4 Jan 2025 18:04:40 -0700 Subject: [PATCH] prep for publish --- README.md | 59 +++++++-- deno.json | 8 +- file_router.ts | 329 +++---------------------------------------------- mod.ts | 3 + router.ts | 315 ++++++++++++++++++++++++++++++++++++++++++++-- types.ts | 1 + 6 files changed, 384 insertions(+), 331 deletions(-) diff --git a/README.md b/README.md index b87c7c8..ae62173 100755 --- a/README.md +++ b/README.md @@ -7,24 +7,26 @@ A simple router for Deno. ## Usage ### Basics + ```ts -import Router from '@bearmetal/router'; +import Router from "@bearmetal/router"; const router = new Router(); router - .route('/users') - .get((ctx) => { - return new Response('GET /users'); + .route("/users") + .get((req, ctx) => { + return new Response("GET /users"); }) - .post((ctx) => { - return new Response('POST /users'); + .post((req, ctx) => { + return new Response("POST /users"); }); -Deno.serve(router.handle) +Deno.serve(router.handle); ``` ### Middleware + ```ts ... @@ -37,6 +39,7 @@ router.use('/users', async (ctx, next) => { ``` ### Nested Routers + ```ts ... @@ -56,3 +59,45 @@ router.use('/users', nestedRouter); ... ``` +### Static Files + +```ts +... +router.serveDirectory('dirname', '/url-root') // files from 'dirname' directory will be available at '/url-root/filename' + +// To automatically locate index.html pages: +router.serveDirectory('dirWithIndexHtml', '/indexes', {showIndex: true}); +// Will also generate an index if there is no index.html present in the directory +... +``` + +### File-based Routing + +```ts +import { FileRouter } from "@bearmetal/router"; + +const router = new FileRouter("dirname"); +Deno.listen(router.handle); + +// dirname/index.ts - will be accessible at '/' +export default function (req, ctx) { + return new Response("Hello, world!"); +} + +// dirname/methods.ts - will be accessible at '/methods' +export const handlers = { + get() { + return new Response("Hello, world"); + }, + post(req, ctx) { + const data = doDataOp(req.body); + return new Response(data); + }, +}; + +// dirname/nestedRouter.ts - will be accessible at '/nestedRouter' +import { router } from "@bearmetal/router"; + +const router = new Router(); +export default router; +``` diff --git a/deno.json b/deno.json index e0b44b4..2c50863 100755 --- a/deno.json +++ b/deno.json @@ -1,9 +1,8 @@ { "name": "@bearmetal/router", "description": "A simple router for Deno", - "version": "0.1.3", + "version": "0.2.0", "stable": true, - "repository": "https://github.com/emmaos/bearmetal", "files": [ "mod.ts", "README.md", @@ -13,8 +12,11 @@ ".": "./mod.ts", "./types": "./types.ts" }, + "exclude": [ + ".vscode/" + ], "imports": { "@std/assert": "jsr:@std/assert@^1.0.7", "@std/testing": "jsr:@std/testing@^1.0.4" } -} +} \ No newline at end of file diff --git a/file_router.ts b/file_router.ts index 63d0258..b053cd7 100755 --- a/file_router.ts +++ b/file_router.ts @@ -1,6 +1,5 @@ import Router from "./router.ts"; import type { Handler, RouteConfigurator } from "./types.ts"; -import { NotFound } from "./util/response.ts"; function crawl(dir: string, callback: (path: string) => void) { for (const entry of Deno.readDirSync(dir)) { @@ -15,6 +14,15 @@ function crawl(dir: string, callback: (path: string) => void) { } } +/** + * @example + * ```ts + * import {FileRouter} from "@bearmetal/router" + * + * const router = new FileRouter("dirName"); + * Deno.listen(router.handle); + * ``` + */ export class FileRouter extends Router { constructor(root: string) { super(); @@ -22,16 +30,16 @@ export class FileRouter extends Router { let relativePath = path.replace(root, ""); if (path.endsWith(".ts") || path.endsWith(".js")) { relativePath = relativePath.replace(/\.[tj]s/, ""); - const asdf = await import(path); + const handlers = await import(path); - if (asdf.default) { - asdf.default instanceof Router - ? this.use(relativePath, asdf.default) - : this.route(relativePath).get(asdf.default); + if (handlers.default) { + handlers.default instanceof Router + ? this.use(relativePath, handlers.default) + : this.route(relativePath).get(handlers.default); } - if (asdf.handlers) { - for (const [method, handler] of Object.entries(asdf.handlers)) { + if (handlers.handlers) { + for (const [method, handler] of Object.entries(handlers.handlers)) { this.route(relativePath)[method as keyof RouteConfigurator]( handler as Handler, ); @@ -48,314 +56,11 @@ export class FileRouter extends Router { } }); } - - serveDirectory(dir: string, root: string, opts?: { showIndex: boolean }) { - this.route(root + "*").get(async (_req, ctx) => { - const { showIndex } = opts ?? { showIndex: false }; - - let normalizedPath = (dir + "/" + - ctx.url.pathname.replace(new RegExp("^" + root), "")).trim().replace( - "//", - "/", - ); - - normalizedPath = normalizedPath.replace( - /\/\s?$/, - "", - ); - - let fileInfo: Deno.FileInfo; - - try { - fileInfo = await Deno.stat(normalizedPath); - } catch (error) { - if (error instanceof Deno.errors.NotFound) { - return NotFound(); - } else { - throw error; - } - } - - if (fileInfo.isDirectory) { - if (!showIndex) return NotFound(); - normalizedPath += "/index.html"; - } - - try { - const file = await Deno.readFile(normalizedPath); - return new Response(file); - } catch (e) { - if (e instanceof Deno.errors.NotFound) { - return showIndex ? generateIndex(normalizedPath) : NotFound(); - } - throw e; - } - }); - } -} - -async function generateIndex(dir: string) { - dir = dir.replace(/index\.html$/, ""); - const items: Deno.DirEntry[] = []; - - for await (const entry of Deno.readDir(dir)) { - items.push(entry); - } - - const fileIcon = ` - - - - - - - - - - - - - - - - -`; - const folderIcon = ` - - - - - - - - - -`; - - const template = ` - - - - - - ${dir} - - - -

${dir}

- - - - `; - - return new Response(template, { - headers: { - "Content-Type": "text/html", - }, - }); } if (import.meta.main) { const router = new FileRouter("fileRouterTest"); - router.serveDirectory("things", "/things", { showIndex: true }); + router.serveDirectory("docs", "/", { showIndex: true }); Deno.serve({ port: 8000, handler: router.handle, diff --git a/mod.ts b/mod.ts index 7a61142..323bb02 100755 --- a/mod.ts +++ b/mod.ts @@ -4,3 +4,6 @@ */ export { Router } from "./router.ts"; +export { FileRouter } from "./file_router.ts"; + +export type { Handler, Middleware } from "./types.ts"; diff --git a/router.ts b/router.ts index 884ca88..64917d2 100755 --- a/router.ts +++ b/router.ts @@ -6,6 +6,7 @@ import type { RouteConfigurator, RouterContext, } from "./types.ts"; +import { NotFound } from "./util/response.ts"; /** * A simple router for Deno @@ -187,7 +188,6 @@ export class Router { const url = new URL(req.url); const method = req.method; - // Find the matching route const matchedRoute = this.findMatchingRoute(url); if (!matchedRoute) { return new Response("Not Found", { status: 404 }); @@ -198,10 +198,8 @@ export class Router { return new Response("Method Not Allowed", { status: 405 }); } - // Get matching middleware and sort by path specificity const matchingMiddleware = this.getMatchingMiddleware(url); - // Create the base context object const baseCtx: RouterContext = { url, params: {}, @@ -210,26 +208,22 @@ export class Router { request: req, }; - // Combine route parameters with the base context baseCtx.params = { ...baseCtx.params, ...matchedRoute.params, }; - // Execute middleware chain let index = 0; const executeMiddleware = async (): Promise => { if (index < matchingMiddleware.length) { const { middleware, params } = matchingMiddleware[index++]; - // Create a new context for each middleware with its specific parameters const middlewareCtx: RouterContext = { ...baseCtx, params: { ...baseCtx.params, ...params }, pattern: middleware.pattern, }; - return await middleware.handler(middlewareCtx, executeMiddleware); + return await middleware.handler(req, middlewareCtx, executeMiddleware); } - // Final handler gets the accumulated parameters return await handler(req, baseCtx); }; @@ -308,6 +302,309 @@ export class Router { ? `/${normalizedRoute}` : `${normalizedBase}/${normalizedRoute}`; } + + serveDirectory(dir: string, root: string, opts?: { showIndex: boolean }) { + this.route(root + "*").get(async (_req, ctx) => { + const { showIndex } = opts ?? { showIndex: false }; + + let normalizedPath = (dir + "/" + + ctx.url.pathname.replace(new RegExp("^" + root), "")).trim().replace( + "//", + "/", + ); + + normalizedPath = normalizedPath.replace( + /\/\s?$/, + "", + ); + + let fileInfo: Deno.FileInfo; + + try { + fileInfo = await Deno.stat(normalizedPath); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + return NotFound(); + } else { + throw error; + } + } + + if (fileInfo.isDirectory) { + if (!showIndex) return NotFound(); + normalizedPath += "/index.html"; + } + + try { + const file = await Deno.readFile(normalizedPath); + return new Response(file); + } catch (e) { + if (e instanceof Deno.errors.NotFound) { + return showIndex ? generateIndex(normalizedPath) : NotFound(); + } + throw e; + } + }); + } +} + +async function generateIndex(dir: string) { + dir = dir.replace(/index\.html$/, ""); + const items: Deno.DirEntry[] = []; + + for await (const entry of Deno.readDir(dir)) { + items.push(entry); + } + + const fileIcon = ` + + + + + + + + + + + + + + + + +`; + const folderIcon = ` + + + + + + + + + +`; + + const template = ` + + + + + + ${dir} + + + +

${dir}

+ + + + `; + + return new Response(template, { + headers: { + "Content-Type": "text/html", + }, + }); } export default Router; @@ -361,7 +658,7 @@ if (import.meta.main) { // return new Response('POST /*'); // }); - router.use("/users", async (_, next) => { + router.use("/users", async (_req, _ctx, next) => { console.log("Using middleware"); return await next(); }); diff --git a/types.ts b/types.ts index 42335f1..f2f04d6 100755 --- a/types.ts +++ b/types.ts @@ -16,6 +16,7 @@ export type Handler = ( ctx: RouterContext, ) => Promise | Response; export type Middleware = ( + req: Request, ctx: RouterContext, next: () => Promise, ) => Promise;