UNPKG

litenode

Version:

Lightweight and modular web framework

279 lines (231 loc) 9.63 kB
// Built-in modules import http from "node:http" import { writeFile } from "node:fs/promises" // Internal classes import { RouteNode } from "./RouteNode.js" import { StaticAssetLoader } from "./StaticAssetLoader.js" import { STE } from "../STE/ste.js" // Internal Router functions import { printNode } from "./methods/printNode.js" import { addRoute } from "./methods/addRoute.js" import { handleRequest } from "./methods/handleRequest.js" import { mergeNodes } from "./methods/mergeNodes.js" // Internal Markdown handler import { MarkdownHandler } from "./methods/markdownHandler.js" // Internal Utility functions import { generateTOC } from "../utils/generateTOC.js" import { bodyParser } from "../utils/bodyParser.js" import { checkForUpdate } from "../utils/updateChecker.js" import { loadEnv, getEnv } from "../utils/envLoader.js" import { cookieParser, createSignedCookies } from "../utils/cookieManager.js" export class LiteNode { #rootNode #addRoute #notFoundHandler #errorHandler #middlewareStack #handleRequest #staticDir #viewsDir #staticAssetLoader #markdownHandler constructor(staticDir = "static", viewsDir = "views") { // If staticDir is provided but viewsDir isn't, use default views this.#staticDir = staticDir this.#viewsDir = viewsDir // Initialize the root node for routing this.#rootNode = new RouteNode() // Bind addRoute method to LiteNode instance this.#addRoute = (httpMethod, routePath, ...handlers) => { return addRoute(this.#rootNode, httpMethod, routePath, ...handlers) } this.#notFoundHandler = null // Handler for 404 errors this.#errorHandler = null // Handler for general errors this.#middlewareStack = [] // Stack of middleware functions // Bind handleRequest method to LiteNode instance this.#handleRequest = (nativeReq, nativeRes) => { try { return handleRequest( this.#middlewareStack, this.#rootNode, this.#notFoundHandler, this.#errorHandler, nativeReq, nativeRes, this.#viewsDir ) } catch (error) { console.error(`Error handling request: ${error.message}`) nativeRes.statusCode = 500 nativeRes.end("Internal Server Error") } } this.#staticDir = staticDir if (staticDir !== "__NO_STATIC_DIR__") { this.#staticAssetLoader = new StaticAssetLoader(staticDir) } // Initialize MarkdownHandler with the views directory this.#markdownHandler = new MarkdownHandler(this.#viewsDir) } printTree() { // Print the route tree for debugging purposes printNode(this.#rootNode, "Root") } notFound(handler) { // Set custom handler for 404 Not Found errors this.#notFoundHandler = handler } onError(handler) { // Set custom handler for general errors this.#errorHandler = handler } get(routePath, ...handlers) { // Define GET route this.#addRoute("GET", routePath, ...handlers) return this } // Private helper method for adding routes with body parsing #addMethodWithBodyParsing(method, routePath, handlers) { const customMaxRequestSize = typeof handlers[handlers.length - 1] === "number" ? handlers.pop() : null const allHandlers = handlers.slice(0, -1).concat(bodyParser(handlers, customMaxRequestSize)) this.#addRoute(method, routePath, ...allHandlers) return this } post(routePath, ...handlers) { // Define POST route with optional max request size return this.#addMethodWithBodyParsing("POST", routePath, handlers) } put(routePath, ...handlers) { // Define PUT route with optional max request size return this.#addMethodWithBodyParsing("PUT", routePath, handlers) } delete(routePath, ...handlers) { // Define DELETE route with optional max request size return this.#addMethodWithBodyParsing("DELETE", routePath, handlers) } patch(routePath, ...handlers) { // Define PATCH route with optional max request size return this.#addMethodWithBodyParsing("PATCH", routePath, handlers) } // Method to support wildcard routes any(routePath, ...handlers) { // Define route for ALL HTTP methods const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] methods.forEach((method) => { this.#addRoute(method, routePath, ...handlers) }) return this } // Helper method for wildcard routes wildcard(routePath, ...handlers) { // Ensure path ends with a wildcard and has a slash const basePath = routePath.endsWith("/") ? routePath.slice(0, -1) : routePath return this.get(`${basePath}/*`, ...handlers) } // Helper method for catch-all routes catchAll(routePath, ...handlers) { // Ensure path ends with a double-wildcard and has a slash const basePath = routePath.endsWith("/") ? routePath.slice(0, -1) : routePath return this.get(`${basePath}/**`, ...handlers) } merge(routerToMerge, ...middlewares) { // Merge another router into this one mergeNodes(this.#rootNode, routerToMerge.#rootNode, middlewares) } #generateNestedRoutes(node, prefix, newRouter) { // Recursively generate nested routes for (const [method, handlers] of Object.entries(node.handler)) { newRouter.#addRoute(method, prefix, ...handlers) } for (const [pathSegment, subNode] of Object.entries(node.children)) { this.#generateNestedRoutes(subNode, `${prefix}/${pathSegment}`, newRouter) } if (node.param) { this.#generateNestedRoutes(node.param, `${prefix}/:${node.param.paramName}`, newRouter) } } #nestNodes(currentNode, nodeToNest, prefix, middlewares = []) { const newRouter = new LiteNode("__NO_STATIC_DIR__") this.#generateNestedRoutes(nodeToNest, prefix, newRouter) mergeNodes(currentNode, newRouter.#rootNode, middlewares) } nest(prefix, routerToNest, ...middlewares) { // Nest another router under a specific prefix this.#nestNodes(this.#rootNode, routerToNest.#rootNode, prefix, middlewares) return this } use(middleware) { // Add middleware to the stack this.#middlewareStack.push(middleware) return this } async renderToFile(template, data, outputPath) { try { // Render a template to a file const templateEngine = new STE(this.#viewsDir) const html = await templateEngine.render(template, data) await writeFile(outputPath, html, "utf-8") } catch (error) { console.error(`Error rendering template or saving file: ${error.message}`) } } parseMarkdownFile(filePath) { return this.#markdownHandler.parseMarkdownFile(filePath) } async parseMarkdownFileS(dir) { return this.#markdownHandler.parseMarkdownFileS(dir) } async extractMarkdownProperties(input, properties) { return this.#markdownHandler.extractMarkdownProperties(input, properties) } async groupByMarkdownProperty(dir, properties, groupByField) { return this.#markdownHandler.groupByMarkdownProperty(dir, properties, groupByField) } async paginateMarkdownFiles(input, page = 1, perPage = 10) { return this.#markdownHandler.paginateMarkdownFiles(input, page, perPage) } generateTOC(input) { return generateTOC(input) } loadEnv(path = ".env", options = {}) { return loadEnv(path, options) } getEnv(key, defaultValue = undefined) { return getEnv(key, defaultValue) } /** * Creates a cookie parser middleware and adds it to the middleware stack * @returns {LiteNode} The LiteNode instance for chaining */ enableCookieParser() { this.use(cookieParser()) return this } /** * Creates signed cookie utilities with the provided secret * @param {string} secret - The secret to use for signing cookies * @returns {Object} An object with methods for working with signed cookies */ createSignedCookies(secret) { return createSignedCookies(secret) } async startServer(port = 5000) { try { // Check for updates before starting the server await checkForUpdate() } catch (err) { console.error("Error checking for updates:", err) } if (this.#staticAssetLoader) { // Serve static assets if the static asset loader is initialized this.#staticAssetLoader.serveStaticAssets(this) } // Read environment PORT variable if available const envPort = getEnv("PORT", port) http.createServer((req, res) => { this.#handleRequest(req, res) }).listen(envPort, () => { console.log(`App @ http://localhost:${envPort}`) }) } }