Compare commits
No commits in common. "5dad5bc0b1671f528f80ea93658f766d019933cd" and "05831b364be1a5d965c1459319b3361847c7131f" have entirely different histories.
5dad5bc0b1
...
05831b364b
0
.vscode/settings.json
vendored
Executable file → Normal file
0
.vscode/settings.json
vendored
Executable file → Normal file
63
README.md
Executable file → Normal file
63
README.md
Executable file → Normal file
@ -7,26 +7,24 @@ 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((req, ctx) => {
|
||||
return new Response("GET /users");
|
||||
.route('/users')
|
||||
.get((ctx) => {
|
||||
return new Response('GET /users');
|
||||
})
|
||||
.post((req, ctx) => {
|
||||
return new Response("POST /users");
|
||||
.post((ctx) => {
|
||||
return new Response('POST /users');
|
||||
});
|
||||
|
||||
Deno.serve(router.handle);
|
||||
Deno.serve(router.handle)
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
```ts
|
||||
...
|
||||
|
||||
@ -39,7 +37,6 @@ router.use('/users', async (ctx, next) => {
|
||||
```
|
||||
|
||||
### Nested Routers
|
||||
|
||||
```ts
|
||||
...
|
||||
|
||||
@ -47,10 +44,10 @@ const nestedRouter = new Router();
|
||||
|
||||
nestedRouter
|
||||
.route('/users')
|
||||
.get((req, ctx) => {
|
||||
.get((ctx) => {
|
||||
return new Response('GET /users');
|
||||
})
|
||||
.post((req, ctx) => {
|
||||
.post((ctx) => {
|
||||
return new Response('POST /users');
|
||||
});
|
||||
|
||||
@ -59,45 +56,3 @@ 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;
|
||||
```
|
||||
|
13
deno.json
Executable file → Normal file
13
deno.json
Executable file → Normal file
@ -1,22 +1,17 @@
|
||||
{
|
||||
"name": "@bearmetal/router",
|
||||
"description": "A simple router for Deno",
|
||||
"version": "0.2.1",
|
||||
"version": "0.1.0",
|
||||
"stable": true,
|
||||
"repository": "https://github.com/emmaos/bearmetal",
|
||||
"files": [
|
||||
"mod.ts",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"exports": {
|
||||
".": "./mod.ts",
|
||||
"./types": "./types.ts"
|
||||
},
|
||||
"exclude": [
|
||||
".vscode/"
|
||||
],
|
||||
"exports": "./mod.ts",
|
||||
"imports": {
|
||||
"@std/assert": "jsr:@std/assert@^1.0.7",
|
||||
"@std/testing": "jsr:@std/testing@^1.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,68 +0,0 @@
|
||||
import Router from "./router.ts";
|
||||
import type { Handler, RouteConfigurator } from "./types.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
crawl(root, async (path) => {
|
||||
let relativePath = path.replace(root, "");
|
||||
if (path.endsWith(".ts") || path.endsWith(".js")) {
|
||||
relativePath = relativePath.replace(/\.[tj]s/, "");
|
||||
const handlers = await import(path);
|
||||
|
||||
if (handlers.default) {
|
||||
handlers.default instanceof Router
|
||||
? this.use(relativePath, handlers.default)
|
||||
: this.route(relativePath).get(handlers.default);
|
||||
}
|
||||
|
||||
if (handlers.handlers) {
|
||||
for (const [method, handler] of Object.entries(handlers.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 });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const router = new FileRouter("fileRouterTest");
|
||||
router.serveDirectory("docs", "/", { showIndex: true });
|
||||
Deno.serve({
|
||||
port: 8000,
|
||||
handler: router.handle,
|
||||
});
|
||||
}
|
10
mod.ts
Executable file → Normal file
10
mod.ts
Executable file → Normal file
@ -1,9 +1 @@
|
||||
/**
|
||||
* @module
|
||||
* BearMetal Router, for routing HTTP requests with Deno.serve
|
||||
*/
|
||||
|
||||
export { Router } from "./router.ts";
|
||||
export { FileRouter } from "./file_router.ts";
|
||||
|
||||
export type { Handler, Middleware } from "./types.ts";
|
||||
export { Router } from "./router.ts";
|
8
router.test.ts
Executable file → Normal file
8
router.test.ts
Executable file → Normal file
@ -49,7 +49,7 @@ describe("Router", () => {
|
||||
describe("Route Parameters", () => {
|
||||
it("should handle route parameters", async () => {
|
||||
router.route("/users/:id")
|
||||
.get(async (_, ctx) => new Response(ctx.params.id));
|
||||
.get(async (ctx) => new Response(ctx.params.id));
|
||||
|
||||
const req = new Request("http://localhost/users/123", {
|
||||
method: "GET",
|
||||
@ -61,7 +61,7 @@ describe("Router", () => {
|
||||
|
||||
it("should handle multiple route parameters", async () => {
|
||||
router.route("/users/:userId/posts/:postId")
|
||||
.get(async (_, ctx) => {
|
||||
.get(async (ctx) => {
|
||||
return new Response(JSON.stringify(ctx.params));
|
||||
});
|
||||
|
||||
@ -182,7 +182,7 @@ describe("Router", () => {
|
||||
it("should handle nested routes with parameters", async () => {
|
||||
const apiRouter = new Router();
|
||||
apiRouter.route("/users/:id")
|
||||
.get(async (_, ctx) => new Response(ctx.params.id));
|
||||
.get(async (ctx) => new Response(ctx.params.id));
|
||||
|
||||
router.use("/api/:version", apiRouter);
|
||||
|
||||
@ -225,7 +225,7 @@ describe("Router", () => {
|
||||
});
|
||||
|
||||
router.route("/test")
|
||||
.get(async (_, ctx) => {
|
||||
.get(async (ctx) => {
|
||||
return new Response(ctx.state.test as string);
|
||||
});
|
||||
|
||||
|
559
router.ts
Executable file → Normal file
559
router.ts
Executable file → Normal file
@ -1,18 +1,40 @@
|
||||
import type {
|
||||
Handler,
|
||||
Middleware,
|
||||
MiddlewareConfig,
|
||||
RouteConfig,
|
||||
RouteConfigurator,
|
||||
RouterContext,
|
||||
} from "./types.ts";
|
||||
import { NotFound } from "./util/response.ts";
|
||||
interface RouterContext {
|
||||
url: URL;
|
||||
params: Record<string, string | undefined>;
|
||||
state: Record<string, unknown>;
|
||||
pattern: URLPattern;
|
||||
request: Request;
|
||||
}
|
||||
|
||||
type Handler = (ctx: RouterContext) => Promise<Response> | Response;
|
||||
type Middleware = (ctx: RouterContext, next: () => Promise<Response>) => Promise<Response>;
|
||||
|
||||
interface RouteConfig {
|
||||
pattern: URLPattern;
|
||||
handlers: { [method: string]: Handler };
|
||||
}
|
||||
|
||||
interface MiddlewareConfig {
|
||||
pattern: URLPattern;
|
||||
handler: Middleware;
|
||||
// Add path for sorting
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface RouteConfigurator {
|
||||
get(handler: Handler): RouteConfigurator;
|
||||
post(handler: Handler): RouteConfigurator;
|
||||
put(handler: Handler): RouteConfigurator;
|
||||
delete(handler: Handler): RouteConfigurator;
|
||||
patch(handler: Handler): RouteConfigurator;
|
||||
options(handler: Handler): RouteConfigurator;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple router for Deno
|
||||
*
|
||||
*
|
||||
* @author Emmaline Autumn
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const router = new Router();
|
||||
@ -76,85 +98,52 @@ export class Router {
|
||||
private routes: RouteConfig[] = [];
|
||||
private middleware: MiddlewareConfig[] = [];
|
||||
|
||||
/**
|
||||
* @description defines a new route
|
||||
* @param path the path to match, uses the same syntax as the URLPattern constructor
|
||||
* @returns a RouteConfigurator object
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* router.route('/users')
|
||||
* .get((req, ctx) => {
|
||||
* return new Response('GET /users');
|
||||
* })
|
||||
* .post((req, ctx) => {
|
||||
* return new Response('POST /users');
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
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;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description adds a middleware to the router
|
||||
* @param pathOrMiddleware the path to match, uses the same syntax as the URLPattern constructor
|
||||
* @param middlewareOrRouter the middleware to add, or a Router object to nest routes
|
||||
* @returns the Router object
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* router.use('/users', async (req, ctx, next) => {
|
||||
* console.log('Using middleware');
|
||||
* return await next();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@ -163,30 +152,27 @@ 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 }
|
||||
});
|
||||
}
|
||||
|
||||
@ -194,11 +180,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 {
|
||||
@ -207,113 +193,103 @@ export class Router {
|
||||
this.middleware.push({
|
||||
pattern,
|
||||
handler: middleware,
|
||||
path: wildcardPath,
|
||||
path: wildcardPath
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description handles incoming requests
|
||||
* @param req the incoming request
|
||||
* @returns a Response object
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Deno.serve({
|
||||
* port: 8000,
|
||||
* handler: router.handle
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
handle = async (req: Request): Promise<Response> => {
|
||||
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 });
|
||||
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
|
||||
const matchingMiddleware = this.getMatchingMiddleware(url);
|
||||
|
||||
// Create the base context object
|
||||
const baseCtx: RouterContext = {
|
||||
url,
|
||||
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
|
||||
let index = 0;
|
||||
const executeMiddleware = async (): Promise<Response> => {
|
||||
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,
|
||||
pattern: middleware.pattern
|
||||
};
|
||||
return await middleware.handler(req, middlewareCtx, executeMiddleware);
|
||||
return await middleware.handler(middlewareCtx, executeMiddleware);
|
||||
}
|
||||
return await handler(req, baseCtx);
|
||||
// Final handler gets the accumulated parameters
|
||||
return await handler(baseCtx);
|
||||
};
|
||||
|
||||
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
|
||||
@ -322,13 +298,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;
|
||||
}
|
||||
@ -339,341 +315,24 @@ 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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description serves a directory as a static website
|
||||
* @param dir the directory to serve
|
||||
* @param root the root path to serve the directory from
|
||||
* @param opts optional options
|
||||
* @returns the Router object
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* router.serveDirectory('/public', './public');
|
||||
* ```
|
||||
*/
|
||||
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 = `
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
sodipodi:docname="file icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="17.318182"
|
||||
inkscape:cx="10.018373"
|
||||
inkscape:cy="28.207349"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1417"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="1432"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
d="m 3.5196818,1.7373617 c -0.3351528,0 -0.605131,0.2694614 -0.605131,0.6046142 v 8.0160481 c 0,0.335153 0.2699782,0.604614 0.605131,0.604614 h 5.6606364 c 0.3351528,0 0.605131,-0.269461 0.605131,-0.604614 V 3.7372396 L 7.7850545,1.7373617 Z"
|
||||
style="fill:#4d1f6d;stroke-width:0.264583"
|
||||
id="path4" />
|
||||
<path
|
||||
d="M 9.6529679,3.8697209 7.6525732,1.869843 v 1.3952637 c 0,0.3351528 0.2699782,0.6046142 0.605131,0.6046142 z"
|
||||
style="fill:#918b93;fill-opacity:1;stroke:#4d1f6d;stroke-width:0.265;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="path5" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.285269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect5"
|
||||
width="5.5833006"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="4.0022206" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.272493;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect6"
|
||||
width="5.0944114"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="4.8455539" />
|
||||
<rect
|
||||
style="fill:#463e4b;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect7"
|
||||
width="5.3694115"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="5.682776" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect8"
|
||||
width="5.3694115"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="5.6888871" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.285269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect9"
|
||||
width="5.5833006"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="6.5322208" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.272493;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect10"
|
||||
width="5.0944114"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="7.3755541" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect11"
|
||||
width="5.3694115"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="8.2188873" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.217416;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect12"
|
||||
width="3.2431545"
|
||||
height="0.5505569"
|
||||
x="5.8984962"
|
||||
y="9.7313871" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
const folderIcon = `
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
sodipodi:docname="folder icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="12.245804"
|
||||
inkscape:cx="17.924507"
|
||||
inkscape:cy="30.990207"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1417"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="1432"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#463e4b;fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect3"
|
||||
width="10.867838"
|
||||
height="7.4972959"
|
||||
x="0.91608107"
|
||||
y="2.1476252"
|
||||
ry="1.1883322" />
|
||||
<rect
|
||||
style="fill:#4d1f6d;fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect1"
|
||||
width="10.867838"
|
||||
height="7.4972959"
|
||||
x="0.91608107"
|
||||
y="2.601352"
|
||||
ry="1.1883322" />
|
||||
<rect
|
||||
style="fill:#4d1f6d;fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect2"
|
||||
width="4.2779961"
|
||||
height="4.0619354"
|
||||
x="0.91608107"
|
||||
y="1.5426561"
|
||||
ry="1.1883322" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const template = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${dir}</title>
|
||||
<style>
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 4px 2px;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background-color: rgba(0,0,0, .2)
|
||||
}
|
||||
|
||||
& svg {
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
background-color: #1b1220;
|
||||
color: #dcdcdc
|
||||
}
|
||||
ul {
|
||||
background-color: rgba(255,255,255,.1)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${dir}</h1>
|
||||
<ul>
|
||||
${
|
||||
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) =>
|
||||
`<li>${e.isFile ? fileIcon : folderIcon}<a href="${
|
||||
"/" + dir.replace(/^\//, "") + e.name
|
||||
}">${e.name}</a></li>`
|
||||
)
|
||||
.join("\n") || "<li>Directory is empty</li>"
|
||||
}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return new Response(template, {
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default Router;
|
||||
|
||||
if (import.meta.main) {
|
||||
console.log("Starting server...");
|
||||
console.log('Starting server...');
|
||||
const router = new Router();
|
||||
router.route("/users")
|
||||
.get((_ctx) => {
|
||||
return new Response("GET /users");
|
||||
});
|
||||
// .post((ctx) => {
|
||||
// return new Response('POST /users');
|
||||
// });
|
||||
router.route('/users')
|
||||
.get((ctx) => {
|
||||
return new Response('GET /users');
|
||||
})
|
||||
// .post((ctx) => {
|
||||
// return new Response('POST /users');
|
||||
// });
|
||||
|
||||
// router.route('/users/:id')
|
||||
// .get((ctx) => {
|
||||
@ -713,14 +372,14 @@ if (import.meta.main) {
|
||||
// return new Response('POST /*');
|
||||
// });
|
||||
|
||||
router.use("/users", async (_req, _ctx, 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
|
||||
});
|
||||
}
|
||||
|
48
types.ts
48
types.ts
@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @module
|
||||
* BearMetal Router types
|
||||
*/
|
||||
|
||||
export interface RouterContext {
|
||||
url: URL;
|
||||
params: Record<string, string | undefined>;
|
||||
state: Record<string, unknown>;
|
||||
pattern: URLPattern;
|
||||
request: Request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description a function that handles incoming requests
|
||||
*/
|
||||
export type Handler = (
|
||||
req: Request,
|
||||
ctx: RouterContext,
|
||||
) => Promise<Response> | Response;
|
||||
/**
|
||||
* @description a middleware function
|
||||
*/
|
||||
export type Middleware = (
|
||||
req: Request,
|
||||
ctx: RouterContext,
|
||||
next: () => Promise<Response>,
|
||||
) => Promise<Response>;
|
||||
|
||||
export interface RouteConfig {
|
||||
pattern: URLPattern;
|
||||
handlers: { [method: string]: Handler };
|
||||
}
|
||||
|
||||
export interface MiddlewareConfig {
|
||||
pattern: URLPattern;
|
||||
handler: Middleware;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RouteConfigurator {
|
||||
get(handler: Handler): RouteConfigurator;
|
||||
post(handler: Handler): RouteConfigurator;
|
||||
put(handler: Handler): RouteConfigurator;
|
||||
delete(handler: Handler): RouteConfigurator;
|
||||
patch(handler: Handler): RouteConfigurator;
|
||||
options(handler: Handler): RouteConfigurator;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
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 });
|
Loading…
x
Reference in New Issue
Block a user