From f0b82d535e4718d7e1f3e2464fe918f693d0a26c Mon Sep 17 00:00:00 2001 From: Kat <49850194+datkat21@users.noreply.github.com> Date: Tue, 7 Nov 2023 00:54:28 -0600 Subject: [PATCH] Publish --- README.md | 57 ++------------ examples/helloWorld.ts | 12 +++ examples/test_foh.ts | 29 +++++++ examples/test_server.ts | 48 ++++++++++++ handlers/handling.ts | 113 +++++++++++++++++++++++++++ handlers/requestHandler.ts | 25 ------ handlers/serverConstructor.ts | 141 ++++++++++++++++------------------ index.ts | 17 +--- package.json | 4 +- public/index.html | 2 +- public/sample.png | Bin 0 -> 8428 bytes 11 files changed, 282 insertions(+), 166 deletions(-) create mode 100644 examples/helloWorld.ts create mode 100644 examples/test_foh.ts create mode 100644 examples/test_server.ts create mode 100644 handlers/handling.ts delete mode 100644 handlers/requestHandler.ts create mode 100644 public/sample.png 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 0000000000000000000000000000000000000000..8cd6a63648c414f30c1eb7de73000bcfa82ad344 GIT binary patch literal 8428 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