diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/deno.json b/deno.json old mode 100644 new mode 100755 diff --git a/deno.lock b/deno.lock old mode 100644 new mode 100755 diff --git a/file_router.ts b/file_router.ts new file mode 100755 index 0000000..63d0258 --- /dev/null +++ b/file_router.ts @@ -0,0 +1,363 @@ +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)) { + const path = Deno.build.os === "windows" + ? `${dir}\\${entry.name}` + : `${dir}/${entry.name}`; + if (entry.isDirectory) { + crawl(path, callback); + } else { + callback(path); + } + } +} + +export class FileRouter extends Router { + constructor(root: string) { + super(); + crawl(root, async (path) => { + let relativePath = path.replace(root, ""); + if (path.endsWith(".ts") || path.endsWith(".js")) { + relativePath = relativePath.replace(/\.[tj]s/, ""); + const asdf = await import(path); + + if (asdf.default) { + asdf.default instanceof Router + ? this.use(relativePath, asdf.default) + : this.route(relativePath).get(asdf.default); + } + + if (asdf.handlers) { + for (const [method, handler] of Object.entries(asdf.handlers)) { + this.route(relativePath)[method as keyof RouteConfigurator]( + handler as Handler, + ); + } + } + } + + if (path.endsWith(".htm") || path.endsWith(".html")) { + const headers = new Headers(); + headers.set("Content-Type", "text/html"); + this.route(relativePath).get((_ctx) => { + return new Response(Deno.readTextFileSync(path), { headers }); + }); + } + }); + } + + 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 }); + Deno.serve({ + port: 8000, + handler: router.handle, + }); +} diff --git a/mod.ts b/mod.ts old mode 100644 new mode 100755 diff --git a/router.test.ts b/router.test.ts old mode 100644 new mode 100755 diff --git a/router.ts b/router.ts old mode 100644 new mode 100755 index 6b18c57..884ca88 --- a/router.ts +++ b/router.ts @@ -1,10 +1,17 @@ -import type { Handler, Middleware, MiddlewareConfig, RouteConfig, RouteConfigurator, RouterContext } from "./types.ts"; +import type { + Handler, + Middleware, + MiddlewareConfig, + RouteConfig, + RouteConfigurator, + RouterContext, +} from "./types.ts"; /** * A simple router for Deno - * + * * @author Emmaline Autumn - * + * * @example * ```ts * const router = new Router(); @@ -69,51 +76,54 @@ export class Router { private middleware: MiddlewareConfig[] = []; route(path: string): RouteConfigurator { - path = path.startsWith('/') ? path : `/${path}`; - + path = path.startsWith("/") ? path : `/${path}`; + const pattern = new URLPattern({ pathname: path }); const routeConfig: RouteConfig = { pattern, - handlers: {} + handlers: {}, }; this.routes.push(routeConfig); return { get(handler: Handler) { - routeConfig.handlers['GET'] = handler; + routeConfig.handlers["GET"] = handler; return this; }, post(handler: Handler) { - routeConfig.handlers['POST'] = handler; + routeConfig.handlers["POST"] = handler; return this; }, put(handler: Handler) { - routeConfig.handlers['PUT'] = handler; + routeConfig.handlers["PUT"] = handler; return this; }, delete(handler: Handler) { - routeConfig.handlers['DELETE'] = handler; + routeConfig.handlers["DELETE"] = handler; return this; }, patch(handler: Handler) { - routeConfig.handlers['PATCH'] = handler; + routeConfig.handlers["PATCH"] = handler; return this; }, options(handler: Handler) { - routeConfig.handlers['OPTIONS'] = handler; + routeConfig.handlers["OPTIONS"] = handler; return this; - } + }, }; } - use(pathOrMiddleware: string | Middleware, middlewareOrRouter?: Middleware | Router): Router { + use( + pathOrMiddleware: string | Middleware, + middlewareOrRouter?: Middleware | Router, + ): Router { // Handle the case where only middleware is provided - if (typeof pathOrMiddleware === 'function') { - const pattern = new URLPattern({ pathname: '/*' }); + if (typeof pathOrMiddleware === "function") { + const pattern = new URLPattern({ pathname: "/*" }); this.middleware.push({ pattern, handler: pathOrMiddleware, - path: '/*' + path: "/*", }); return this; } @@ -122,27 +132,30 @@ export class Router { const middleware = middlewareOrRouter; if (!middleware) { - throw new Error('Middleware or Router is required'); + throw new Error("Middleware or Router is required"); } // Normalize the path to handle both exact matches and nested paths - const normalizedPath = path.startsWith('/') ? path : `/${path}`; - const isParameterPath = normalizedPath.includes(':'); - const wildcardPath = isParameterPath ? - normalizedPath : - normalizedPath === '/' ? - '/*' : - `${normalizedPath.replace(/\/+$/, '')}/*?`; + const normalizedPath = path.startsWith("/") ? path : `/${path}`; + const isParameterPath = normalizedPath.includes(":"); + const wildcardPath = isParameterPath + ? normalizedPath + : normalizedPath === "/" + ? "/*" + : `${normalizedPath.replace(/\/+$/, "")}/*?`; if (middleware instanceof Router) { // Merge the nested router's routes for (const nestedRoute of middleware.routes) { - const combinedPath = this.combinePaths(path, nestedRoute.pattern.pathname); + const combinedPath = this.combinePaths( + path, + nestedRoute.pattern.pathname, + ); const newPattern = new URLPattern({ pathname: combinedPath }); - + this.routes.push({ pattern: newPattern, - handlers: { ...nestedRoute.handlers } + handlers: { ...nestedRoute.handlers }, }); } @@ -150,11 +163,11 @@ export class Router { for (const nestedMiddleware of middleware.middleware) { const combinedPath = this.combinePaths(path, nestedMiddleware.path); const newPattern = new URLPattern({ pathname: combinedPath }); - + this.middleware.push({ pattern: newPattern, handler: nestedMiddleware.handler, - path: combinedPath + path: combinedPath, }); } } else { @@ -163,7 +176,7 @@ export class Router { this.middleware.push({ pattern, handler: middleware, - path: wildcardPath + path: wildcardPath, }); } @@ -177,12 +190,12 @@ export class Router { // Find the matching route const matchedRoute = this.findMatchingRoute(url); if (!matchedRoute) { - return new Response('Not Found', { status: 404 }); + return new Response("Not Found", { status: 404 }); } const handler = matchedRoute.config.handlers[method]; if (!handler) { - return new Response('Method Not Allowed', { status: 405 }); + return new Response("Method Not Allowed", { status: 405 }); } // Get matching middleware and sort by path specificity @@ -194,13 +207,13 @@ export class Router { params: {}, state: {}, pattern: matchedRoute.config.pattern, - request: req + request: req, }; // Combine route parameters with the base context baseCtx.params = { ...baseCtx.params, - ...matchedRoute.params + ...matchedRoute.params, }; // Execute middleware chain @@ -212,7 +225,7 @@ export class Router { const middlewareCtx: RouterContext = { ...baseCtx, params: { ...baseCtx.params, ...params }, - pattern: middleware.pattern + pattern: middleware.pattern, }; return await middleware.handler(middlewareCtx, executeMiddleware); } @@ -223,43 +236,47 @@ export class Router { try { return await executeMiddleware(); } catch (error) { - console.error('Error handling request:', error); - return new Response('Internal Server Error', { status: 500 }); + console.error("Error handling request:", error); + return new Response("Internal Server Error", { status: 500 }); } - } + }; - private findMatchingRoute(url: URL): { config: RouteConfig; params: Record } | null { + private findMatchingRoute( + url: URL, + ): + | { config: RouteConfig; params: Record } + | null { for (const route of this.routes) { const result = route.pattern.exec(url); if (result) { return { config: route, - params: result.pathname.groups + params: result.pathname.groups, }; } } return null; } - private getMatchingMiddleware(url: URL): Array<{ - middleware: MiddlewareConfig; - params: Record + private getMatchingMiddleware(url: URL): Array<{ + middleware: MiddlewareConfig; + params: Record; }> { const matches = this.middleware - .map(mw => { - const result = mw.pattern.exec(url) ?? mw.pattern.exec(url.href + '/'); + .map((mw) => { + const result = mw.pattern.exec(url) ?? mw.pattern.exec(url.href + "/"); // console.log(url, mw.pattern, result); if (result) { return { middleware: mw, - params: result.pathname.groups + params: result.pathname.groups, }; } return null; }) - .filter((item): item is { - middleware: MiddlewareConfig; - params: Record + .filter((item): item is { + middleware: MiddlewareConfig; + params: Record; } => item !== null); // Sort middleware by path specificity @@ -268,13 +285,13 @@ export class Router { const pathB = b.middleware.path; // Global middleware comes first - if (pathA === '/*' && pathB !== '/*') return -1; - if (pathB === '/*' && pathA !== '/*') return 1; + if (pathA === "/*" && pathB !== "/*") return -1; + if (pathB === "/*" && pathA !== "/*") return 1; // More specific paths (with more segments) come later - const segmentsA = pathA.split('/').length; - const segmentsB = pathB.split('/').length; - + const segmentsA = pathA.split("/").length; + const segmentsB = pathB.split("/").length; + if (segmentsA !== segmentsB) { return segmentsA - segmentsB; } @@ -285,24 +302,26 @@ export class Router { } private combinePaths(basePath: string, routePath: string): string { - const normalizedBase = basePath.replace(/\/+$/, '').replace(/^\/*/, '/'); - const normalizedRoute = routePath.replace(/^\/*/, ''); - return normalizedBase === '/' ? `/${normalizedRoute}` : `${normalizedBase}/${normalizedRoute}`; + const normalizedBase = basePath.replace(/\/+$/, "").replace(/^\/*/, "/"); + const normalizedRoute = routePath.replace(/^\/*/, ""); + return normalizedBase === "/" + ? `/${normalizedRoute}` + : `${normalizedBase}/${normalizedRoute}`; } } export default Router; if (import.meta.main) { - console.log('Starting server...'); + console.log("Starting server..."); const router = new Router(); - router.route('/users') + router.route("/users") .get((_ctx) => { - return new Response('GET /users'); - }) - // .post((ctx) => { - // return new Response('POST /users'); - // }); + return new Response("GET /users"); + }); + // .post((ctx) => { + // return new Response('POST /users'); + // }); // router.route('/users/:id') // .get((ctx) => { @@ -342,14 +361,14 @@ if (import.meta.main) { // return new Response('POST /*'); // }); - router.use('/users', async (_, next) => { - console.log('Using middleware'); + router.use("/users", async (_, next) => { + console.log("Using middleware"); return await next(); }); Deno.serve({ port: 8000, - - handler: router.handle + + handler: router.handle, }); } diff --git a/types.ts b/types.ts old mode 100644 new mode 100755 index 502bc2a..42335f1 --- a/types.ts +++ b/types.ts @@ -11,8 +11,14 @@ export interface RouterContext { request: Request; } -export type Handler = (req: Request, ctx: RouterContext) => Promise | Response; -export type Middleware = (ctx: RouterContext, next: () => Promise) => Promise; +export type Handler = ( + req: Request, + ctx: RouterContext, +) => Promise | Response; +export type Middleware = ( + ctx: RouterContext, + next: () => Promise, +) => Promise; export interface RouteConfig { pattern: URLPattern; @@ -32,4 +38,4 @@ export interface RouteConfigurator { delete(handler: Handler): RouteConfigurator; patch(handler: Handler): RouteConfigurator; options(handler: Handler): RouteConfigurator; -} \ No newline at end of file +} diff --git a/util/response.ts b/util/response.ts new file mode 100755 index 0000000..fca469f --- /dev/null +++ b/util/response.ts @@ -0,0 +1,4 @@ +export const NotFound = (msg?: string) => + new Response(msg ?? "Not Found", { status: 404 }); +export const InternalError = (msg?: string) => + new Response(msg ?? "Internal Server Error", { status: 500 });