diff --git a/README.md b/README.md index 10c9d7f..36e6520 100644 --- a/README.md +++ b/README.md @@ -3,58 +3,13 @@ Mini-web server for Bun.js. This minimal server thing extends `Bun.serve()` and creates a simple wrapper with useful features such as a public directory, websocket support built-in, etc. -## Usage +## Specification -This isn't exactly a library (yet) so you'll have to copy the files from `handlers` into your project. - -Here are some server examples: - -**Basic HTTP server** -```ts -import { requestHandler } from "./handlers/requestHandler"; -import { ReqHandlerType, makeServer } from "./handlers/serverConstructor"; - -const server = makeServer({ - // Set up the server basics - port: 3000, - publicFolderPath: "./public", - - // Set up the request handler - requestHandler, - requestHandlerType: ReqHandlerType.UrlOnly -}); - -console.log(`Listening on localhost:${server.port}`); -``` - -**Basic websocket server** -```ts -import { requestHandler } from "./handlers/requestHandler"; -import { ReqHandlerType, makeServer } from "./handlers/serverConstructor"; - -const server = makeServer({ - // Set up the server basics - port: 3000, - publicFolderPath: "./public", - - // Set up the request handler - requestHandler, - requestHandlerType: ReqHandlerType.UrlAndWs, - - // Instance of WebSocketHandler - websocketConfig: { - message(ws, message) { - console.log(`${ws.data} said: ${message}`); - }, - // . . . (open, close, ping, pong, etc.) - }, - // Also see websocketHandler to set custom `ws.data`. -}); - -console.log(`Listening on localhost:${server.port}`); -``` - -## Setup +- Custom Handlers +- File-only mode +- WebSockets +- Very easy to understand configuration +- Ease of use handler builders To install dependencies: diff --git a/examples/helloWorld.ts b/examples/helloWorld.ts new file mode 100644 index 0000000..343dbff --- /dev/null +++ b/examples/helloWorld.ts @@ -0,0 +1,12 @@ +import miniDeluxe from "../index"; +const m = miniDeluxe.miniServer({ + port: 3000, + handlers: [ + { + urlPath: "/", + handler: async function (req: Request) { + return new Response("Hello, world!"); + }, + }, + ], +}); \ No newline at end of file diff --git a/examples/test_foh.ts b/examples/test_foh.ts new file mode 100644 index 0000000..57dd1dc --- /dev/null +++ b/examples/test_foh.ts @@ -0,0 +1,29 @@ +import { + createCookie, + editCookie, + pageBuilder, + rawFile, +} from "../handlers/handling"; +import miniDeluxe from "../index"; + +function fofHandler() { + return new Response( + `

404 Not Found

The file you were looking for does not exist.

`, + { + status: 404, + headers: { + "Content-Type": "text/html", + }, + statusText: "Not Found", + } + ); +} + +const m = miniDeluxe.miniServer({ + port: 3000, + fofHandler, + options: { + fileOnlyMode: true, + staticFilePath: "../public", + }, +}); diff --git a/examples/test_server.ts b/examples/test_server.ts new file mode 100644 index 0000000..21586ff --- /dev/null +++ b/examples/test_server.ts @@ -0,0 +1,48 @@ +import { + createCookie, + editCookie, + pageBuilder, + rawFile, +} from "../handlers/handling"; +import miniDeluxe from "../index"; + +const m = miniDeluxe.miniServer({ + port: 3000, + // options: { + // fileOnlyMode: true + // }, + handlers: [ + { + urlPath: "/", + handler: async function (req: Request) { + return pageBuilder( + "

New Webs

sponsored by cherries.to & tuck" + ); + }, + }, + { + urlPath: "/turtle", + handler: async function (req: Request) { + return rawFile("../public/sample.png"); + }, + }, + { + urlPath: "/cookie/make", + handler: async function (req: Request) { + return createCookie("cherries.to", "yes!"); + }, + }, + { + urlPath: "/cookie/edit", + handler: async function (req: Request) { + return editCookie("cherries.to", "edited"); + }, + }, + { + urlPath: "/error", + handler: async function (req: Request) { + throw new TypeError("Bad Error"); + }, + }, + ], +}); diff --git a/handlers/handling.ts b/handlers/handling.ts new file mode 100644 index 0000000..94c176f --- /dev/null +++ b/handlers/handling.ts @@ -0,0 +1,113 @@ +import { BunFile } from "bun"; +import path from "path"; + +export interface ReqHandler { + urlPath: string; + handler: CallableFunction; +} + +export function returnNotFound() { + return new Response("404 Not Found", { + status: 404, + headers: { "Content-Type": "text/html" }, + }); +} + +export interface Route { + /** + * The path of the route, e.g. `/` or `/chat`. + */ + urlPath: string; + handler: CallableFunction; +} + +export function fileOnlyHandlers(url: URL) { + return new Response(Bun.file(url.pathname)); +} + +// austin genius big brain iq momento here +export function generateRequestHandler(handlers: any[], fofHandler: Function = returnNotFound) { + async function requestHandler(req: Request): Promise { + // Generate URL from request data + const url = new URL(req.url); + + // Fall back to public directory + let urlPath = url.pathname; + + // default handlers built into the program + if (urlPath.endsWith("/")) { + urlPath = "/index.html"; + } + + let route: Route | null; + try { + route = handlers.find((r: Route) => { + if (r.urlPath.endsWith(url.pathname)) + // if (r.path.exec(url.pathname) !== null && r.method === req.method) + return r; + }); + } catch (e: any) { + return new Response(e, { status: 500 }); + } + + // If there's no such route, show a 404 + if (!route) return fofHandler(); + + // Run the route's handler + return await route?.handler({ req, url, route }); + } + + return requestHandler; +} + +import { join } from "path"; + +export function fileOnlyReqHandler(staticFilePath: string = "public") { + async function requestHandler(req: Request) { + // Generate URL from request data + const url = new URL(req.url); + + // Fall back to public directory + let urlPath = url.pathname; + + // default handlers built into the program + if (urlPath.endsWith("/")) { + urlPath = "/index.html"; + } + + return new Response(Bun.file(join(staticFilePath, urlPath))); + } + + return requestHandler; +} + +/** + * Build a page and return with content type. Meant for simple API serverside uses like sqlite3 just to return extra comments. + */ +export function pageBuilder(html: string, contentType: string = "text/html") { + return new Response(html, { + headers: { "content-type": contentType }, + }); +} + +/// Create a cookie by ${name} and ${value} +export function createCookie(name: string, value: string) { + return new Response("Cookie Created", { + headers: { "Set-Cookie": `${name}=${value}` }, + }); +} + +/// Edit an already existing cookie by ${name} and ${value} +export function editCookie(name: string, value: string) { + return new Response("Cookie Edited", { + headers: { Cookie: `${name}=${value}` }, + status: 301, + statusText: "Moved", + }); +} + +/// Return any raw file you would like, headers will be filled out by Bun.file() +export function rawFile(path: string) { + // return rawFile = Bun.file -> Response; + return new Response(Bun.file(path)); +} diff --git a/handlers/requestHandler.ts b/handlers/requestHandler.ts deleted file mode 100644 index ede4850..0000000 --- a/handlers/requestHandler.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BunFile } from "bun"; -import path from "path"; - -/** - * Typically used as a public folder handler - * or fallback handler - */ -export async function requestHandler(url: URL): Promise { - // Fall back to public directory - let urlPath = url.pathname; - - if (urlPath.endsWith("/")) { - urlPath = "/index.html"; - } - - try { - // Read the path and make a response - return new Response(Bun.file(path.join("./public/", urlPath))); - } catch (e) { - return new Response("Internal Server Error\n\n" + e, { - status: 500, - statusText: "Internal Server Error", - }); - } -} diff --git a/handlers/serverConstructor.ts b/handlers/serverConstructor.ts index 281e810..4184669 100644 --- a/handlers/serverConstructor.ts +++ b/handlers/serverConstructor.ts @@ -1,90 +1,81 @@ import { Server, WebSocketHandler } from "bun"; +import { + ReqHandler, + fileOnlyReqHandler, + generateRequestHandler, + returnNotFound, +} from "./handling"; -export enum ServerType { - HttpOnly, - SupportWs, -} -export enum ReqHandlerType { - Auto, - UrlOnly, - UrlAndWs, - Custom, +interface MiniDxServerOptions { + enableWs?: boolean; + fileOnlyMode?: boolean; + staticFilePath?: string; } -export interface IServerConfig { +interface MiniDxServerConfig { port?: number; - publicFolderPath: string; - requestHandler: Function; - requestHandlerType?: ReqHandlerType; - websocketConfig?: WebSocketHandler; - websocketHandler?: Function; + options?: MiniDxServerOptions; + handlers?: ReqHandler[]; + fofHandler?: Function; + errorHandler?: any; } -export function makeServer(config: IServerConfig, type?: ServerType): Server { - if (config.requestHandlerType === undefined) - config.requestHandlerType = ReqHandlerType.Auto; - - // fetchfunction can't inherit Function lol - let fetchFunction: any; +export function miniServer(config: MiniDxServerConfig): Server { + // Assign default port + if (!config.port) config.port = 3000; - console.log("[debug]", "fetchFunction"); + if (!config.fofHandler) config.fofHandler = returnNotFound; + // Assign default error handler + if (!config.errorHandler) + config.errorHandler = function (error: Error) { + console.log(error); + if (error.name === "ENOENT") { + //@ts-ignore ts stuff :) + return config.fofHandler(); + } - if ( - config.requestHandlerType === undefined || - config.requestHandlerType === ReqHandlerType.UrlOnly - ) { - fetchFunction = async function fetch(req: Request) { - return await config.requestHandler(new URL(req.url)); + return new Response( + `

${ + error.name + } ${error.cause !== undefined ? `(${error.cause})` : ""}

${ + error.message + }

${error.stack
+          ?.replaceAll("<", "<")
+          .replaceAll(">", ">")}
`, + { + status: 500, + headers: { + "Content-Type": "text/html", + }, + statusText: "Internal Server Error", + } + ); }; - console.log("[debug]", "fetchFunction for default handler"); - } else if (config.requestHandlerType === ReqHandlerType.Custom) { - fetchFunction = async function fetch(req: Request, server: Server) { - return await config.requestHandler(req, server); + if (!config.options) + config.options = { + enableWs: false, + fileOnlyMode: false, + staticFilePath: "public", }; - console.log("[debug]", "fetchFunction for custom handler"); - } else if ( - type === ServerType.SupportWs || - config.requestHandlerType === ReqHandlerType.UrlAndWs - ) { - fetchFunction = async function fetch(req: Request, server: Server) { - // determine if this is a websocket or http connection - if ( - req.headers.get("Connection") && - req.headers.get("Connection")?.includes("Upgrade") && - req.headers.get("Upgrade") !== null && - req.headers.get("Upgrade") === "websocket" - ) { - // Assume this is a WebSocket since we have verified the headers - if (config.websocketHandler === undefined) { - server.upgrade(req, { - // Example data passed to the websocket handler. - data: performance.now(), - }); - } else await config.websocketHandler(server, req); - } else { - // Fallback to request handler - return await config.requestHandler(new URL(req.url)); - } - }; - console.log("[debug]", "fetchFunction for ws handler"); + if (config.handlers === undefined && config.options.fileOnlyMode !== true) { + /** + * This text shows up somewhere + */ + throw new Error("No handlers assigned to your server"); } + if (!config.handlers) config.handlers = []; - if ( - type === ServerType.SupportWs || - config.requestHandlerType === ReqHandlerType.UrlAndWs - ) { - console.log("[debug]", "fetchFunction set on ws..."); - return Bun.serve({ - fetch: fetchFunction, - websocket: config.websocketConfig, - }); - } else { - // Assume httponly - console.log("[debug]", "fetchFunction set on httponly..."); - return Bun.serve({ - port: 3000, - fetch: fetchFunction, - }); - } + // Generate request handler + const fetchHandler = + config.options.fileOnlyMode == false + ? generateRequestHandler(config.handlers, config.fofHandler) + : fileOnlyReqHandler(config.options.staticFilePath); + + // Return final server + return Bun.serve({ + port: config.port, + fetch: fetchHandler, + error: config.errorHandler, + }); } diff --git a/index.ts b/index.ts index 1fff628..a45b72f 100644 --- a/index.ts +++ b/index.ts @@ -1,14 +1,5 @@ -import { requestHandler } from "./handlers/requestHandler"; -import { ReqHandlerType, makeServer } from "./handlers/serverConstructor"; +import { miniServer } from "./handlers/serverConstructor"; -const server = makeServer({ - // Set up the server basics - port: 3000, - publicFolderPath: "./public", - - // Set up the request handler - requestHandler, - requestHandlerType: ReqHandlerType.UrlOnly -}); - -console.log(`Listening on localhost:${server.port}`); +export default { + miniServer +}; diff --git a/package.json b/package.json index 85fe6bd..127d592 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,7 @@ { - "name": "silly-web-server", + "name": "mini-deluxe", + "description": "A minimal HTTP server library by cherries.to devs", + "version": "0.0.1", "module": "index.ts", "type": "module", "devDependencies": { diff --git a/public/index.html b/public/index.html index 60217a8..ea1865a 100644 --- a/public/index.html +++ b/public/index.html @@ -6,6 +6,6 @@ Document - Hey! This is my little server library made in Bun. + Hey! This is our little server library made in Bun. \ No newline at end of file diff --git a/public/sample.png b/public/sample.png new file mode 100644 index 0000000..8cd6a63 Binary files /dev/null and b/public/sample.png differ