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 ### 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 ```ts
import { FileRouter } from "@bearmetal/router"; import { FileRouter } from "@bearmetal/router";

View File

@@ -1,7 +1,7 @@
{ {
"name": "@bearmetal/router", "name": "@bearmetal/router",
"description": "A simple router for Deno", "description": "A simple router for Deno",
"version": "0.2.1", "version": "0.2.4",
"stable": true, "stable": true,
"files": [ "files": [
"mod.ts", "mod.ts",
@@ -10,7 +10,8 @@
], ],
"exports": { "exports": {
".": "./mod.ts", ".": "./mod.ts",
"./types": "./types.ts" "./types": "./types.ts",
"./util/response": "./util/response.ts"
}, },
"exclude": [ "exclude": [
".vscode/" ".vscode/"
@@ -19,4 +20,4 @@
"@std/assert": "jsr:@std/assert@^1.0.7", "@std/assert": "jsr:@std/assert@^1.0.7",
"@std/testing": "jsr:@std/testing@^1.0.4" "@std/testing": "jsr:@std/testing@^1.0.4"
} }
} }

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 Router from "./router.ts";
import type { Handler, RouteConfigurator } from "./types.ts"; import type { Handler, RouteConfigurator } from "./types.ts";
import { isRelativePath } from "./util/isRelativePath.ts";
function crawl(dir: string, callback: (path: string) => void) { function crawl(dir: string, callback: (path: string) => void) {
for (const entry of Deno.readDirSync(dir)) { for (const entry of Deno.readDirSync(dir)) {
@@ -24,13 +25,20 @@ function crawl(dir: string, callback: (path: string) => void) {
* ``` * ```
*/ */
export class FileRouter extends Router { export class FileRouter extends Router {
constructor(root: string) { constructor(root: string, _import?: (path: string) => Promise<unknown>) {
super(); super();
crawl(root, async (path) => { crawl(root, async (path) => {
let relativePath = path.replace(root, ""); let relativePath = path.replace(root, "").replace(/index\/?/, "");
if (path.endsWith(".ts") || path.endsWith(".js")) { 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/, ""); relativePath = relativePath.replace(/\.[tj]s/, "");
const handlers = await import(path); const handlers = _import
? await _import(path)
: await import("file://" + path);
if (handlers.default) { if (handlers.default) {
handlers.default instanceof Router 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 { Router } from "./router.ts";
export { FileRouter } from "./file_router.ts"; export { FileRouter } from "./file_router.ts";
export type { Handler, Middleware } from "./types.ts";

View File

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

View File

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