Implements basic file based router
This commit is contained in:
157
router.ts
Normal file → Executable file
157
router.ts
Normal file → Executable file
@@ -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<string, string | undefined> } | null {
|
||||
private findMatchingRoute(
|
||||
url: URL,
|
||||
):
|
||||
| { config: RouteConfig; params: Record<string, string | undefined> }
|
||||
| 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<string, string>
|
||||
private getMatchingMiddleware(url: URL): Array<{
|
||||
middleware: MiddlewareConfig;
|
||||
params: Record<string, string>;
|
||||
}> {
|
||||
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<string, string>
|
||||
.filter((item): item is {
|
||||
middleware: MiddlewareConfig;
|
||||
params: Record<string, string>;
|
||||
} => 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,
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user