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}
+
+ ${
+ items
+ .sort((a, b) => {
+ if (a.isDirectory && b.isDirectory) return a.name > b.name ? 1 : -1;
+
+ if (a.isDirectory) return -1;
+
+ if (b.isDirectory) return 1;
+
+ return a.name > b.name ? 1 : -1;
+ })
+ .map((e) =>
+ `- ${e.isFile ? fileIcon : folderIcon}${e.name}
`
+ )
+ .join("\n") || "- Directory is empty
"
+ }
+
+
+
+ `;
+
+ 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 });