Compare commits

...

4 Commits

10 changed files with 116 additions and 12 deletions

View File

@@ -73,6 +73,11 @@ router.serveDirectory('dirWithIndexHtml', '/indexes', {showIndex: true});
### File-based Routing
**Note:** _This is an experimental feature and may change in the future.
Currently, JSR does not support dynamic imports for external modules in Deno. In
order to use this feature, you will need to install as an HTTP module (available
soon)._
```ts
import { FileRouter } from "@bearmetal/router";

View File

@@ -1,7 +1,7 @@
{
"name": "@bearmetal/router",
"description": "A simple router for Deno",
"version": "0.2.1",
"version": "0.2.4",
"stable": true,
"files": [
"mod.ts",
@@ -10,7 +10,8 @@
],
"exports": {
".": "./mod.ts",
"./types": "./types.ts"
"./types": "./types.ts",
"./util/response": "./util/response.ts"
},
"exclude": [
".vscode/"

7
fileRouterTest/test.ts Executable file
View File

@@ -0,0 +1,7 @@
const handlers = {
get: () => {
return new Response("Hello World");
},
};
export default handlers;

View File

@@ -1,5 +1,6 @@
import Router from "./router.ts";
import type { Handler, RouteConfigurator } from "./types.ts";
import { isRelativePath } from "./util/isRelativePath.ts";
function crawl(dir: string, callback: (path: string) => void) {
for (const entry of Deno.readDirSync(dir)) {
@@ -24,13 +25,20 @@ function crawl(dir: string, callback: (path: string) => void) {
* ```
*/
export class FileRouter extends Router {
constructor(root: string) {
constructor(root: string, _import?: (path: string) => Promise<unknown>) {
super();
crawl(root, async (path) => {
let relativePath = path.replace(root, "");
let relativePath = path.replace(root, "").replace(/index\/?/, "");
if (path.endsWith(".ts") || path.endsWith(".js")) {
if (isRelativePath(path)) {
path = Deno.build.os === "windows"
? `${Deno.cwd()}\\${path.replace(/^.?.?\\/, "")}`
: `${Deno.cwd()}/${path.replace(/^.?.?\//, "")}`;
}
relativePath = relativePath.replace(/\.[tj]s/, "");
const handlers = await import(path);
const handlers = _import
? await _import(path)
: await import("file://" + path);
if (handlers.default) {
handlers.default instanceof Router
@@ -55,6 +63,15 @@ export class FileRouter extends Router {
});
}
});
if (Deno.env.get("BEARMETAL_ROUTER_DEBUG") === "true") {
this.route("/debug/dump").get((_ctx) => {
console.log("Dumping routes:");
return new Response(
this.routes.map((r) => r.pattern.pathname).join("\n"),
);
});
}
}
}

2
mod.ts
View File

@@ -5,5 +5,3 @@
export { Router } from "./router.ts";
export { FileRouter } from "./file_router.ts";
export type { Handler, Middleware } from "./types.ts";

View File

@@ -6,6 +6,7 @@ import type {
RouteConfigurator,
RouterContext,
} from "./types.ts";
import { getContentTypeByExtension } from "./util/contentType.ts";
import { NotFound } from "./util/response.ts";
/**
@@ -73,7 +74,7 @@ import { NotFound } from "./util/response.ts";
* });
*/
export class Router {
private routes: RouteConfig[] = [];
protected routes: RouteConfig[] = [];
private middleware: MiddlewareConfig[] = [];
/**
@@ -392,7 +393,9 @@ export class Router {
try {
const file = await Deno.readFile(normalizedPath);
return new Response(file);
const filetype = normalizedPath.split(".")[1];
const contentType = getContentTypeByExtension(filetype);
return new Response(file, { headers: { "Content-Type": contentType } });
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return showIndex ? generateIndex(normalizedPath) : NotFound();

View File

@@ -3,6 +3,9 @@
* BearMetal Router types
*/
/**
* @description a context object for a request
*/
export interface RouterContext {
url: URL;
params: Record<string, string | undefined>;
@@ -27,17 +30,26 @@ export type Middleware = (
next: () => Promise<Response>,
) => Promise<Response>;
/**
* @description a route configuration
*/
export interface RouteConfig {
pattern: URLPattern;
handlers: { [method: string]: Handler };
}
/**
* @description a middleware configuration
*/
export interface MiddlewareConfig {
pattern: URLPattern;
handler: Middleware;
path: string;
}
/**
* @description a route configurator
*/
export interface RouteConfigurator {
get(handler: Handler): RouteConfigurator;
post(handler: Handler): RouteConfigurator;

26
util/contentType.ts Executable file
View File

@@ -0,0 +1,26 @@
export function getContentTypeByExtension(extension: string) {
switch (extension) {
case "html":
case "htm":
return "text/html";
case "css":
return "text/css";
case "js":
return "text/javascript";
case "json":
return "application/json";
case "png":
return "image/png";
case "jpg":
case "jpeg":
return "image/jpeg";
case "gif":
return "image/gif";
case "svg":
return "image/svg+xml";
case "txt":
return "text/plain";
default:
return "application/octet-stream";
}
}

4
util/isRelativePath.ts Executable file
View File

@@ -0,0 +1,4 @@
export function isRelativePath(path: string) {
return !path.startsWith("/") &&
(path.startsWith("./") || path.startsWith("../"));
}

View File

@@ -1,4 +1,35 @@
export const NotFound = (msg?: string) =>
/**
* @module
* BearMetal Router response utilities
*/
/**
* @description a response with a status of 404
*/
export const NotFound = (msg?: string): Response =>
new Response(msg ?? "Not Found", { status: 404 });
export const InternalError = (msg?: string) =>
/**
* @description a response with a status of 500
*/
export const InternalError = (msg?: string): Response =>
new Response(msg ?? "Internal Server Error", { status: 500 });
/**
* @description a response with a status of 400
*/
export const BadRequest = (msg?: string): Response =>
new Response(msg ?? "Bad Request", { status: 400 });
/**
* @description a response with a status of 401
*/
export const Unauthorized = (msg?: string): Response =>
new Response(msg ?? "Unauthorized", { status: 401 });
/**
* @description a response with a status of 403
*/
export const Forbidden = (msg?: string): Response =>
new Response(msg ?? "Forbidden", { status: 403 });
/**
* @description a response with a status of 200
*/
export const Ok = (msg?: string): Response =>
new Response(msg ?? "OK", { status: 200 });