diff --git a/lib/helpers/context.d.ts b/lib/helpers/context.d.ts new file mode 100644 index 0000000..c619d32 --- /dev/null +++ b/lib/helpers/context.d.ts @@ -0,0 +1,17 @@ +type ContextType = { + json: (json: object, status?: number) => Promise; + pretty: (json: object, status?: number) => Promise; + text: (text: string, status?: number) => Promise; + html: (html: string, status?: number) => Promise; + error: (message: string, status?: number) => Promise; + success: (message: string, status?: number) => Promise; + redirect: (url: string, status?: number) => Promise; + sendFile: (filePath: string, status?: number) => Promise; + readHtml: (filePath: string) => Promise; + query: { + get: (key: string) => string | null; + }; + req: Request; +}; +declare function Context(request: Request): ContextType; +export { Context, type ContextType }; diff --git a/lib/main/index.d.ts b/lib/main/index.d.ts index a8af36f..f9cf052 100644 --- a/lib/main/index.d.ts +++ b/lib/main/index.d.ts @@ -1,4 +1,5 @@ import { SendJSON, Success, Failure, ServerFailure, Redirect, Html, SendFile } from "../helpers/helper.ts"; +import { Context, type ContextType } from "../helpers/context.ts"; import { query } from "../helpers/query.ts"; import { param } from "../helpers/param.ts"; import MongoService from "../instances/mongodb.ts"; @@ -21,4 +22,4 @@ declare class ProBun { definePreMiddleware(middleware: any): void; definePostMiddleware(middleware: any): void; } -export { ProBun, SendJSON, Success, Failure, ServerFailure, Redirect, Html, query, param, MongoService, PgService, SendFile, json }; +export { ProBun, SendJSON, Success, Failure, ServerFailure, Redirect, Html, query, param, MongoService, PgService, SendFile, json, Context, type ContextType, }; diff --git a/lib/main/index.js b/lib/main/index.js index 9f0898e..914391e 100644 --- a/lib/main/index.js +++ b/lib/main/index.js @@ -1,2 +1,2 @@ // @bun -var{Glob}=globalThis.Bun;import*as path2 from"path/posix";import*as fs from"fs";import chalk3 from"chalk";import*as path from"path/posix";async function SendJSON(json,status=200){return new Response(JSON.stringify(json),{headers:{"Content-Type":"application/json"},status})}var Success=function(message,status=200){return SendJSON({message},status)};async function SendFile(filePath,status=200){const file=Bun.file(filePath);let rawFileName=path.basename(filePath);rawFileName=rawFileName.replace(/ /g,"_");rawFileName=rawFileName.replace(/\\/g,"_");rawFileName=rawFileName.split("_")[rawFileName.split("_").length-1];return new Response(file,{headers:{"Content-Type":"application/octet-stream","Content-Disposition":`attachment; filename="${rawFileName}"`},status})}var Failure=function(message,status=400){return SendJSON({message},status)};var ServerFailure=function(message,status=500){return SendJSON({message},status)};var Redirect=function(destination,status=302){return new Response(null,{status,headers:{Location:destination}})};var Html=function(html,status=200){return new Response(html,{headers:{"Content-Type":"text/html"},status})};async function query(query2,req){const url=new URL(req.url);const params=url.searchParams;const value=params.get(query2);if(!value){return}else{return value}}async function param(req){const url=new URL(req.url);let splitUrl=req.url.split("/");let id=splitUrl[splitUrl.length-1];if(id===""){id=splitUrl[splitUrl.length-2]}if(!id){return null}else{return id.replace(/\?.*/,"")}}var version="0.1.25";import{MongoClient} from"mongodb";import chalk from"chalk";class Mongo{client=null;isConnected=false;async connect(url){this.client=new MongoClient(url);const start=Date.now();console.log(chalk.bold.whiteBright(`Connecting to MongoDB...`));await this.client.connect().then(()=>{console.log(chalk.bold.whiteBright(`MongoDB connected in `)+chalk.bold.greenBright(`${Date.now()-start}ms`));this.isConnected=true})}async getCollection(db,col){if(!this.isConnected){throw new Error("Not connected to MongoDB")}return this.client.db(db).collection(col)}async getDatabase(db){if(!this.isConnected){throw new Error("Not connected to MongoDB")}return this.client.db(db)}async update(db,col,query2,update){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.updateOne(query2,update)}async insert(db,col,data){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.insertOne(data)}async find(db,col,query2){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.find(query2).toArray()}async findOne(db,col,query2){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.findOne(query2)}async delete(db,col,query2){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.deleteOne(query2)}async close(){await this.client.close()}}class MongoService{static instance=null;constructor(){throw new Error("Use MongoService.getInstance()")}static getInstance(){if(!MongoService.instance){MongoService.instance=new Mongo}return MongoService.instance}}var mongodb_default=MongoService;import{Pool} from"pg";import chalk2 from"chalk";class Pg{pool=null;isConnected=false;async connect(config){this.pool=new Pool({ssl:{rejectUnauthorized:false},...config});const start=Date.now();try{console.log(chalk2.bold.whiteBright(`Connecting to PostgreSQL...`));await this.pool.connect();console.log(chalk2.bold.whiteBright(`PostgreSQL connected in `)+chalk2.bold.greenBright(`${Date.now()-start}ms`));this.isConnected=true}catch(error){console.log("Error while trying to establish postgres connection",error)}}async endConnection(){await this.pool?.end()}async query(text,params){if(!this.isConnected){throw new Error("Not connected to PostgreSQL")}return this.pool?.query(text,params)}}class PgService{static instance=null;constructor(){throw new Error("Use PgService.getInstance()")}static getInstance(){if(!PgService.instance){PgService.instance=new Pg}return PgService.instance}}var postgres_default=PgService;async function json(req){const readableStream=await req.body.getReader().read();const uint8Array=readableStream.value;const bodyString=new TextDecoder().decode(uint8Array);if(bodyString==="[object Object]"){return{}}try{return JSON.parse(bodyString)}catch(e){}let body={};bodyString.split("&").forEach((pair)=>{const[key,value]=pair.split("=");body[decodeURIComponent(key)]=decodeURIComponent(value)});return body}async function loadFolder(folder){if(log){console.log(`${chalk3.bold.white(`Loading`)} ${chalk3.bold.green(`${folder}`)}...`)}const allRoutes=new Glob(`${folder}/*.ts`);for await(let file of allRoutes.scan(".")){file=file.replace(/\\/g,"/");let realfile=file.replace(globalFolder+"/","").replace(/.ts/g,"");file=file.split("/")[file.split("/").length-1];const splits=file.split("/");const filePath=path2.join(process.cwd(),folder,file);const routeModule=await import(filePath).then((m)=>m.default||m);const getModule=typeof routeModule==="object"?routeModule?.GET:routeModule;const postModule=typeof routeModule==="object"?routeModule?.POST:routeModule;const putModule=typeof routeModule==="object"?routeModule?.PUT:routeModule;const deleteModule=typeof routeModule==="object"?routeModule?.DELETE:routeModule;const patchModule=typeof routeModule==="object"?routeModule?.PATCH:routeModule;file=file.replace(/.ts/g,"");if(getModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.get[`${realfile}`]=getModule}if(postModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.post[`${realfile}`]=postModule}if(putModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.put[`${realfile}`]=putModule}if(deleteModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.delete[`${realfile}`]=deleteModule}if(patchModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.patch[`${realfile}`]=patchModule}}const folders=fs.readdirSync(folder);for(const subfolder of folders){if(subfolder.includes(".")){continue}await loadFolder(path2.join(folder,subfolder))}}async function loadRoutes(folder){const start=Date.now();await loadFolder(folder);if(log){console.log(`${chalk3.bold.white(`Loaded all routes in`)} ${chalk3.bold.green(`${Date.now()-start}ms`)}`)}}async function handleRequest(req){const start=Date.now();let customHeaders=new Headers;for(const middleware of premiddlewares){try{await middleware(req,{headers:customHeaders})}catch(error){console.error(`${chalk3.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`);return ServerFailure("Internal Server Error")}}const userMethod=req.method.toLowerCase();const url=req.url;let isIndex=false;let parsedUrl=new URL(url??"","http://localhost");let reqMessage=`${chalk3.bold.white(userMethod.toUpperCase())} ${parsedUrl.pathname}`;if(parsedUrl.pathname==="/favicon.ico"){return new Response("",{status:204})}if(parsedUrl.pathname==="/"){isIndex=true}if(parsedUrl.pathname.endsWith("/")){parsedUrl.pathname=parsedUrl.pathname.substring(0,parsedUrl.pathname.length-1)}if(userMethod==="options"){for(const middleware of premiddlewares){try{await middleware(req,{headers:customHeaders})}catch(error){console.error(`${chalk3.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`);return ServerFailure("Internal Server Error")}}customHeaders.set("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, PATCH, OPTIONS");customHeaders.set("Access-Control-Allow-Headers","Content-Type, Authorization");return new Response("",{status:200,headers:customHeaders})}let matchingRoute;if(isIndex){matchingRoute=methods[userMethod]["index"]}else{matchingRoute=methods[userMethod][parsedUrl.pathname.substring(1)];if(!matchingRoute){matchingRoute=methods[userMethod][parsedUrl.pathname.substring(1)+"/index"]}if(!matchingRoute){const parts=parsedUrl.pathname.split("/");parts.pop();let newPath=parts.join("/");newPath=newPath.substring(1);matchingRoute=methods[userMethod][newPath+"/params"]}}if(!matchingRoute){if(log){reqMessage+=` ${chalk3.bold.red("404")} ${chalk3.bold.gray(`${Date.now()-start}ms`)}`;console.log(reqMessage)}return new Response("Not found.",{status:404})}try{const response=await matchingRoute(req);for(const middleware of postmiddlewares){try{await middleware(req,{headers:customHeaders})}catch(error){console.error(`${chalk3.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`);return ServerFailure("Internal Server Error")}}const end=Date.now();response.headers.set("x-response-time",`${end-start}ms`);for(const[key,value]of customHeaders){response.headers.set(key,value)}if(log){let color="green";if(response.status>=100&&response.status<200){color="blue"}else if(response.status>=200&&response.status<300){color="green"}else if(response.status>=300&&response.status<400){color="yellow"}else if(response.status>=400&&response.status<500){color="magenta"}else if(response.status>=500){color="red"}reqMessage+=` ${chalk3.bold[color](response.status)}`;reqMessage+=` ${chalk3.bold.gray(`${end-start}ms`)}`;console.log(reqMessage)}return response}catch(error){console.error("Error while processing the requested route: ",error);if(log){reqMessage+=` ${chalk3.bold.red("500")} ${chalk3.bold.gray(`${Date.now()-start}ms`)}`;console.log(reqMessage)}return ServerFailure("Internal Server Error")}}async function startServer(port=3000,routes="routes",logger=true){await loadRoutes(routes);console.log(chalk3.bold.white(`Using ProBun ${chalk3.bold.green(`v${version}`)}`));console.log(chalk3.bold.white(`Starting server on port ${chalk3.bold.cyan(`${port}...`)}`));Bun.serve({port,fetch:handleRequest})}var globalFolder="";var log=false;var methods={get:{},post:{},put:{},delete:{},patch:{}};var premiddlewares=[];var postmiddlewares=[];class ProBun{port;routes;logger;mongoUri;pgConfig;constructor(props){const{port,routes,logger,mongoUri,pgConfig}=props;this.port=port;this.routes=routes;this.logger=logger;this.mongoUri=mongoUri;this.pgConfig=pgConfig}async start(){log=this.logger;if(this.mongoUri){await mongodb_default.getInstance().connect(this.mongoUri)}if(this.pgConfig){await postgres_default.getInstance().connect(this.pgConfig)}globalFolder=this.routes;startServer(this.port,this.routes,this.logger)}definePreMiddleware(middleware){premiddlewares.push(middleware);if(this.logger){console.log(`Added pre-middleware: ${chalk3.bold.green(middleware.name)}`)}}definePostMiddleware(middleware){postmiddlewares.push(middleware);if(this.logger){console.log(`Added post-middleware: ${chalk3.bold.green(middleware.name)}`)}}}export{query,param,json,Success,ServerFailure,SendJSON,SendFile,Redirect,ProBun,postgres_default as PgService,mongodb_default as MongoService,Html,Failure}; +var{Glob}=globalThis.Bun;import*as path3 from"path/posix";import*as fs from"fs";import chalk3 from"chalk";import*as path from"path/posix";async function SendJSON(json,status=200){return new Response(JSON.stringify(json),{headers:{"Content-Type":"application/json"},status})}var Success=function(message,status=200){return SendJSON({message},status)};async function SendFile(filePath,status=200){const file=Bun.file(filePath);let rawFileName=path.basename(filePath);rawFileName=rawFileName.replace(/ /g,"_");rawFileName=rawFileName.replace(/\\/g,"_");rawFileName=rawFileName.split("_")[rawFileName.split("_").length-1];return new Response(file,{headers:{"Content-Type":"application/octet-stream","Content-Disposition":`attachment; filename="${rawFileName}"`},status})}var Failure=function(message,status=400){return SendJSON({message},status)};var ServerFailure=function(message,status=500){return SendJSON({message},status)};var Redirect=function(destination,status=302){return new Response(null,{status,headers:{Location:destination}})};var Html=function(html,status=200){return new Response(html,{headers:{"Content-Type":"text/html"},status})};import*as path2 from"path/posix";var Context=function(request){const json=async(json2,status=200)=>{return new Response(JSON.stringify(json2),{status,headers:{"Content-Type":"application/json"}})};const pretty=async(json2,status=200)=>{return new Response(JSON.stringify(json2,null,2),{status,headers:{"Content-Type":"application/json"}})};const text=async(text2,status=200)=>{return new Response(text2,{status,headers:{"Content-Type":"text/plain"}})};const html=async(html2,status=200)=>{return new Response(html2,{status,headers:{"Content-Type":"text/html"}})};const error=async(message,status=500)=>{return json({error:message},status)};const success=async(message,status=200)=>{return json({message},status)};const redirect=async(url,status=302)=>{return new Response(null,{status,headers:{Location:url}})};const sendFile=async(filePath,status=200)=>{const file=Bun.file(filePath);let rawFileName=path2.basename(filePath);rawFileName=rawFileName.replace(/ /g,"_");rawFileName=rawFileName.replace(/\\/g,"_");rawFileName=rawFileName.split("_")[rawFileName.split("_").length-1];return new Response(file,{headers:{"Content-Type":"application/octet-stream","Content-Disposition":`attachment; filename="${rawFileName}"`},status})};const query={get:(key)=>{return new URL(request.url).searchParams.get(key)}};const readHtml=async(filePath)=>{if(!filePath.endsWith(".html"))throw new Error("File must be an HTML file");const file=await Bun.file(filePath).text();return file};return{json,pretty,text,html,error,success,redirect,sendFile,readHtml,query,req:request}};async function query(query2,req){const url=new URL(req.url);const params=url.searchParams;const value=params.get(query2);if(!value){return}else{return value}}async function param(req){const url=new URL(req.url);let splitUrl=req.url.split("/");let id=splitUrl[splitUrl.length-1];if(id===""){id=splitUrl[splitUrl.length-2]}if(!id){return null}else{return id.replace(/\?.*/,"")}}var version="0.1.26";import{MongoClient} from"mongodb";import chalk from"chalk";class Mongo{client=null;isConnected=false;async connect(url){this.client=new MongoClient(url);const start=Date.now();console.log(chalk.bold.whiteBright(`Connecting to MongoDB...`));await this.client.connect().then(()=>{console.log(chalk.bold.whiteBright(`MongoDB connected in `)+chalk.bold.greenBright(`${Date.now()-start}ms`));this.isConnected=true})}async getCollection(db,col){if(!this.isConnected){throw new Error("Not connected to MongoDB")}return this.client.db(db).collection(col)}async getDatabase(db){if(!this.isConnected){throw new Error("Not connected to MongoDB")}return this.client.db(db)}async update(db,col,query2,update){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.updateOne(query2,update)}async insert(db,col,data){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.insertOne(data)}async find(db,col,query2){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.find(query2).toArray()}async findOne(db,col,query2){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.findOne(query2)}async delete(db,col,query2){if(!this.isConnected){throw new Error("Not connected to MongoDB")}const collection=await this.getCollection(db,col);return collection.deleteOne(query2)}async close(){await this.client.close()}}class MongoService{static instance=null;constructor(){throw new Error("Use MongoService.getInstance()")}static getInstance(){if(!MongoService.instance){MongoService.instance=new Mongo}return MongoService.instance}}var mongodb_default=MongoService;import{Pool} from"pg";import chalk2 from"chalk";class Pg{pool=null;isConnected=false;async connect(config){this.pool=new Pool({ssl:{rejectUnauthorized:false},...config});const start=Date.now();try{console.log(chalk2.bold.whiteBright(`Connecting to PostgreSQL...`));await this.pool.connect();console.log(chalk2.bold.whiteBright(`PostgreSQL connected in `)+chalk2.bold.greenBright(`${Date.now()-start}ms`));this.isConnected=true}catch(error){console.log("Error while trying to establish postgres connection",error)}}async endConnection(){await this.pool?.end()}async query(text,params){if(!this.isConnected){throw new Error("Not connected to PostgreSQL")}return this.pool?.query(text,params)}}class PgService{static instance=null;constructor(){throw new Error("Use PgService.getInstance()")}static getInstance(){if(!PgService.instance){PgService.instance=new Pg}return PgService.instance}}var postgres_default=PgService;async function json(req){const readableStream=await req.body.getReader().read();const uint8Array=readableStream.value;const bodyString=new TextDecoder().decode(uint8Array);if(bodyString==="[object Object]"){return{}}try{return JSON.parse(bodyString)}catch(e){}let body={};bodyString.split("&").forEach((pair)=>{const[key,value]=pair.split("=");body[decodeURIComponent(key)]=decodeURIComponent(value)});return body}async function loadFolder(folder){if(log){console.log(`${chalk3.bold.white(`Loading`)} ${chalk3.bold.green(`${folder}`)}...`)}const allRoutes=new Glob(`${folder}/*.ts`);for await(let file of allRoutes.scan(".")){file=file.replace(/\\/g,"/");let realfile=file.replace(globalFolder+"/","").replace(/.ts/g,"");file=file.split("/")[file.split("/").length-1];const splits=file.split("/");const filePath=path3.join(process.cwd(),folder,file);const routeModule=await import(filePath).then((m)=>m.default||m);const getModule=typeof routeModule==="object"?routeModule?.GET:routeModule;const postModule=typeof routeModule==="object"?routeModule?.POST:routeModule;const putModule=typeof routeModule==="object"?routeModule?.PUT:routeModule;const deleteModule=typeof routeModule==="object"?routeModule?.DELETE:routeModule;const patchModule=typeof routeModule==="object"?routeModule?.PATCH:routeModule;file=file.replace(/.ts/g,"");if(getModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.get[`${realfile}`]=getModule}if(postModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.post[`${realfile}`]=postModule}if(putModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.put[`${realfile}`]=putModule}if(deleteModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.delete[`${realfile}`]=deleteModule}if(patchModule){if(file.includes("[")&&file.includes("]")){const parts=realfile.split("/");parts[parts.length-1]="params";realfile=parts.join("/")}methods.patch[`${realfile}`]=patchModule}}const folders=fs.readdirSync(folder);for(const subfolder of folders){if(subfolder.includes(".")){continue}await loadFolder(path3.join(folder,subfolder))}}async function loadRoutes(folder){const start=Date.now();await loadFolder(folder);if(log){console.log(`${chalk3.bold.white(`Loaded all routes in`)} ${chalk3.bold.green(`${Date.now()-start}ms`)}`)}}async function handleRequest(req){const start=Date.now();let customHeaders=new Headers;for(const middleware of premiddlewares){try{await middleware(req,{headers:customHeaders})}catch(error){console.error(`${chalk3.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`);return ServerFailure("Internal Server Error")}}const userMethod=req.method.toLowerCase();const url=req.url;let isIndex=false;let parsedUrl=new URL(url??"","http://localhost");let reqMessage=`${chalk3.bold.white(userMethod.toUpperCase())} ${parsedUrl.pathname}`;if(parsedUrl.pathname==="/favicon.ico"){return new Response("",{status:204})}if(parsedUrl.pathname==="/"){isIndex=true}if(parsedUrl.pathname.endsWith("/")){parsedUrl.pathname=parsedUrl.pathname.substring(0,parsedUrl.pathname.length-1)}if(userMethod==="options"){for(const middleware of premiddlewares){try{await middleware(req,{headers:customHeaders})}catch(error){console.error(`${chalk3.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`);return ServerFailure("Internal Server Error")}}customHeaders.set("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, PATCH, OPTIONS");customHeaders.set("Access-Control-Allow-Headers","Content-Type, Authorization");return new Response("",{status:200,headers:customHeaders})}let matchingRoute;if(isIndex){matchingRoute=methods[userMethod]["index"]}else{matchingRoute=methods[userMethod][parsedUrl.pathname.substring(1)];if(!matchingRoute){matchingRoute=methods[userMethod][parsedUrl.pathname.substring(1)+"/index"]}if(!matchingRoute){const parts=parsedUrl.pathname.split("/");parts.pop();let newPath=parts.join("/");newPath=newPath.substring(1);matchingRoute=methods[userMethod][newPath+"/params"]}}if(!matchingRoute){if(log){reqMessage+=` ${chalk3.bold.red("404")} ${chalk3.bold.gray(`${Date.now()-start}ms`)}`;console.log(reqMessage)}return new Response("Not found.",{status:404})}try{const response=await matchingRoute(req);for(const middleware of postmiddlewares){try{await middleware(req,{headers:customHeaders})}catch(error){console.error(`${chalk3.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`);return ServerFailure("Internal Server Error")}}const end=Date.now();response.headers.set("x-response-time",`${end-start}ms`);for(const[key,value]of customHeaders){response.headers.set(key,value)}if(log){let color="green";if(response.status>=100&&response.status<200){color="blue"}else if(response.status>=200&&response.status<300){color="green"}else if(response.status>=300&&response.status<400){color="yellow"}else if(response.status>=400&&response.status<500){color="magenta"}else if(response.status>=500){color="red"}reqMessage+=` ${chalk3.bold[color](response.status)}`;reqMessage+=` ${chalk3.bold.gray(`${end-start}ms`)}`;console.log(reqMessage)}return response}catch(error){console.error("Error while processing the requested route: ",error);if(log){reqMessage+=` ${chalk3.bold.red("500")} ${chalk3.bold.gray(`${Date.now()-start}ms`)}`;console.log(reqMessage)}return ServerFailure("Internal Server Error")}}async function startServer(port=3000,routes="routes",logger=true){await loadRoutes(routes);console.log(chalk3.bold.white(`Using ProBun ${chalk3.bold.green(`v${version}`)}`));console.log(chalk3.bold.white(`Starting server on port ${chalk3.bold.cyan(`${port}...`)}`));Bun.serve({port,fetch:handleRequest})}var globalFolder="";var log=false;var methods={get:{},post:{},put:{},delete:{},patch:{}};var premiddlewares=[];var postmiddlewares=[];class ProBun{port;routes;logger;mongoUri;pgConfig;constructor(props){const{port,routes,logger,mongoUri,pgConfig}=props;this.port=port;this.routes=routes;this.logger=logger;this.mongoUri=mongoUri;this.pgConfig=pgConfig}async start(){log=this.logger;if(this.mongoUri){await mongodb_default.getInstance().connect(this.mongoUri)}if(this.pgConfig){await postgres_default.getInstance().connect(this.pgConfig)}globalFolder=this.routes;startServer(this.port,this.routes,this.logger)}definePreMiddleware(middleware){premiddlewares.push(middleware);if(this.logger){console.log(`Added pre-middleware: ${chalk3.bold.green(middleware.name)}`)}}definePostMiddleware(middleware){postmiddlewares.push(middleware);if(this.logger){console.log(`Added post-middleware: ${chalk3.bold.green(middleware.name)}`)}}}export{query,param,json,Success,ServerFailure,SendJSON,SendFile,Redirect,ProBun,postgres_default as PgService,mongodb_default as MongoService,Html,Failure,Context}; diff --git a/package.json b/package.json index b346f4c..3592233 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "probun", "description": "Powerful file-based routing for Bun servers", "module": "src/main/index.ts", - "version": "0.1.25", + "version": "0.1.26", "main": "./lib/main/index.js", "type": "module", "homepage": "https://probun.dev", @@ -25,4 +25,4 @@ "mongodb": "^6.5.0", "pg": "^8.11.5" } -} \ No newline at end of file +} diff --git a/src/helpers/context.ts b/src/helpers/context.ts new file mode 100644 index 0000000..55f8f69 --- /dev/null +++ b/src/helpers/context.ts @@ -0,0 +1,115 @@ +import * as path from "path/posix"; + +type ContextType = { + json: (json: object, status?: number) => Promise; + pretty: (json: object, status?: number) => Promise; + text: (text: string, status?: number) => Promise; + html: (html: string, status?: number) => Promise; + error: (message: string, status?: number) => Promise; + success: (message: string, status?: number) => Promise; + redirect: (url: string, status?: number) => Promise; + sendFile: (filePath: string, status?: number) => Promise; + readHtml: (filePath: string) => Promise; + query: { + get: (key: string) => string | null; + }; + req: Request; +}; + +function Context(request: Request): ContextType { + const json = async (json: object, status: number = 200) => { + return new Response(JSON.stringify(json), { + status, + headers: { + "Content-Type": "application/json", + }, + }); + }; + + const pretty = async (json: object, status: number = 200) => { + return new Response(JSON.stringify(json, null, 2), { + status, + headers: { + "Content-Type": "application/json", + }, + }); + }; + + const text = async (text: string, status: number = 200) => { + return new Response(text, { + status, + headers: { + "Content-Type": "text/plain", + }, + }); + }; + + const html = async (html: string, status: number = 200) => { + return new Response(html, { + status, + headers: { + "Content-Type": "text/html", + }, + }); + }; + + const error = async (message: string, status: number = 500) => { + return json({ error: message }, status); + }; + + const success = async (message: string, status: number = 200) => { + return json({ message }, status); + }; + + const redirect = async (url: string, status: number = 302) => { + return new Response(null, { + status, + headers: { + Location: url, + }, + }); + }; + + const sendFile = async (filePath: string, status: number = 200) => { + const file = Bun.file(filePath); + let rawFileName = path.basename(filePath); + rawFileName = rawFileName.replace(/ /g, "_"); + rawFileName = rawFileName.replace(/\\/g, "_"); + rawFileName = rawFileName.split("_")[rawFileName.split("_").length - 1]; + return new Response(file, { + headers: { + "Content-Type": "application/octet-stream", + "Content-Disposition": `attachment; filename="${rawFileName}"`, + }, + status, + }); + }; + + const query = { + get: (key: string) => { + return new URL(request.url).searchParams.get(key); + }, + }; + + const readHtml = async (filePath: string) => { + if (!filePath.endsWith(".html")) throw new Error("File must be an HTML file"); + const file = await Bun.file(filePath).text(); + return file; + }; + + return { + json, + pretty, + text, + html, + error, + success, + redirect, + sendFile, + readHtml, + query, + req: request, + }; +} + +export { Context, type ContextType }; diff --git a/src/main/index.ts b/src/main/index.ts index b1552c4..96546c1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,21 +2,14 @@ import { Glob } from "bun"; import * as path from "path/posix"; import * as fs from "fs"; import chalk from "chalk"; -import { - SendJSON, - Success, - Failure, - ServerFailure, - Redirect, - Html, - SendFile, -} from "../helpers/helper.ts"; +import { SendJSON, Success, Failure, ServerFailure, Redirect, Html, SendFile } from "../helpers/helper.ts"; +import { Context, type ContextType } from "../helpers/context.ts"; import { query } from "../helpers/query.ts"; import { param } from "../helpers/param.ts"; import { version } from "../../package.json"; import MongoService from "../instances/mongodb.ts"; import PgService from "../instances/postgres.ts"; -import {json} from "../helpers/json.ts"; +import { json } from "../helpers/json.ts"; let globalFolder = ""; @@ -44,9 +37,7 @@ const postmiddlewares = [] as any; async function loadFolder(folder: string) { if (log) { - console.log( - `${chalk.bold.white(`Loading`)} ${chalk.bold.green(`${folder}`)}...` - ); + console.log(`${chalk.bold.white(`Loading`)} ${chalk.bold.green(`${folder}`)}...`); } const allRoutes = new Glob(`${folder}/*.ts`); for await (let file of allRoutes.scan(".")) { @@ -56,16 +47,11 @@ async function loadFolder(folder: string) { const splits = file.split("/"); const filePath = path.join(process.cwd(), folder, file); const routeModule = await import(filePath).then((m) => m.default || m); - const getModule = - typeof routeModule === "object" ? routeModule?.GET : routeModule; - const postModule = - typeof routeModule === "object" ? routeModule?.POST : routeModule; - const putModule = - typeof routeModule === "object" ? routeModule?.PUT : routeModule; - const deleteModule = - typeof routeModule === "object" ? routeModule?.DELETE : routeModule; - const patchModule = - typeof routeModule === "object" ? routeModule?.PATCH : routeModule; + const getModule = typeof routeModule === "object" ? routeModule?.GET : routeModule; + const postModule = typeof routeModule === "object" ? routeModule?.POST : routeModule; + const putModule = typeof routeModule === "object" ? routeModule?.PUT : routeModule; + const deleteModule = typeof routeModule === "object" ? routeModule?.DELETE : routeModule; + const patchModule = typeof routeModule === "object" ? routeModule?.PATCH : routeModule; file = file.replace(/.ts/g, ""); if (getModule) { if (file.includes("[") && file.includes("]")) { @@ -122,11 +108,7 @@ async function loadRoutes(folder: string) { const start = Date.now(); await loadFolder(folder); if (log) { - console.log( - `${chalk.bold.white(`Loaded all routes in`)} ${chalk.bold.green( - `${Date.now() - start}ms` - )}` - ); + console.log(`${chalk.bold.white(`Loaded all routes in`)} ${chalk.bold.green(`${Date.now() - start}ms`)}`); } } @@ -137,11 +119,7 @@ async function handleRequest(req: Request): Promise { try { await middleware(req, { headers: customHeaders }); } catch (error) { - console.error( - `${chalk.bold.red( - `Error while processing middleware ${middleware.name}:` - )} ${error}` - ); + console.error(`${chalk.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`); return ServerFailure("Internal Server Error"); } } @@ -149,9 +127,7 @@ async function handleRequest(req: Request): Promise { const url = req.url; let isIndex = false; let parsedUrl = new URL(url ?? "", "http://localhost"); - let reqMessage = `${chalk.bold.white(userMethod.toUpperCase())} ${ - parsedUrl.pathname - }`; + let reqMessage = `${chalk.bold.white(userMethod.toUpperCase())} ${parsedUrl.pathname}`; if (parsedUrl.pathname === "/favicon.ico") { return new Response("", { status: 204 }); } @@ -161,30 +137,23 @@ async function handleRequest(req: Request): Promise { } if (parsedUrl.pathname.endsWith("/")) { - parsedUrl.pathname = parsedUrl.pathname.substring( - 0, - parsedUrl.pathname.length - 1 - ); + parsedUrl.pathname = parsedUrl.pathname.substring(0, parsedUrl.pathname.length - 1); } // options request - if (userMethod === "options") { - for (const middleware of premiddlewares) { - try { - await middleware(req, { headers: customHeaders }); - } catch (error) { - console.error( - `${chalk.bold.red( - `Error while processing middleware ${middleware.name}:` - )} ${error}` - ); - return ServerFailure("Internal Server Error"); - } - } - customHeaders.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - customHeaders.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); - return new Response("", { status: 200, headers: customHeaders }); + if (userMethod === "options") { + for (const middleware of premiddlewares) { + try { + await middleware(req, { headers: customHeaders }); + } catch (error) { + console.error(`${chalk.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`); + return ServerFailure("Internal Server Error"); + } } + customHeaders.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + customHeaders.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); + return new Response("", { status: 200, headers: customHeaders }); + } let matchingRoute: Route | undefined; @@ -193,8 +162,7 @@ async function handleRequest(req: Request): Promise { } else { matchingRoute = methods[userMethod][parsedUrl.pathname.substring(1)]; if (!matchingRoute) { - matchingRoute = - methods[userMethod][parsedUrl.pathname.substring(1) + "/index"]; + matchingRoute = methods[userMethod][parsedUrl.pathname.substring(1) + "/index"]; } if (!matchingRoute) { const parts = parsedUrl.pathname.split("/"); @@ -207,9 +175,7 @@ async function handleRequest(req: Request): Promise { if (!matchingRoute) { if (log) { - reqMessage += ` ${chalk.bold.red("404")} ${chalk.bold.gray( - `${Date.now() - start}ms` - )}`; + reqMessage += ` ${chalk.bold.red("404")} ${chalk.bold.gray(`${Date.now() - start}ms`)}`; console.log(reqMessage); } return new Response("Not found.", { status: 404 }); @@ -222,11 +188,7 @@ async function handleRequest(req: Request): Promise { try { await middleware(req, { headers: customHeaders }); } catch (error) { - console.error( - `${chalk.bold.red( - `Error while processing middleware ${middleware.name}:` - )} ${error}` - ); + console.error(`${chalk.bold.red(`Error while processing middleware ${middleware.name}:`)} ${error}`); return ServerFailure("Internal Server Error"); } } @@ -257,27 +219,17 @@ async function handleRequest(req: Request): Promise { } catch (error) { console.error("Error while processing the requested route: ", error); if (log) { - reqMessage += ` ${chalk.bold.red("500")} ${chalk.bold.gray( - `${Date.now() - start}ms` - )}`; + reqMessage += ` ${chalk.bold.red("500")} ${chalk.bold.gray(`${Date.now() - start}ms`)}`; console.log(reqMessage); } return ServerFailure("Internal Server Error"); } } -async function startServer( - port: number = 3000, - routes: string = "routes", - logger: boolean = true -) { +async function startServer(port: number = 3000, routes: string = "routes", logger: boolean = true) { await loadRoutes(routes); - console.log( - chalk.bold.white(`Using ProBun ${chalk.bold.green(`v${version}`)}`) - ); - console.log( - chalk.bold.white(`Starting server on port ${chalk.bold.cyan(`${port}...`)}`) - ); + console.log(chalk.bold.white(`Using ProBun ${chalk.bold.green(`v${version}`)}`)); + console.log(chalk.bold.white(`Starting server on port ${chalk.bold.cyan(`${port}...`)}`)); Bun.serve({ port, fetch: handleRequest, @@ -291,13 +243,7 @@ class ProBun { private mongoUri: any; private pgConfig: any; - constructor(props: { - port: number; - routes: string; - logger: boolean; - mongoUri?: any; - pgConfig?: any; - }) { + constructor(props: { port: number; routes: string; logger: boolean; mongoUri?: any; pgConfig?: any }) { const { port, routes, logger, mongoUri, pgConfig } = props; this.port = port; this.routes = routes; @@ -328,9 +274,7 @@ class ProBun { definePostMiddleware(middleware: any) { postmiddlewares.push(middleware); if (this.logger) { - console.log( - `Added post-middleware: ${chalk.bold.green(middleware.name)}` - ); + console.log(`Added post-middleware: ${chalk.bold.green(middleware.name)}`); } } } @@ -348,5 +292,7 @@ export { MongoService, PgService, SendFile, - json + json, + Context, + type ContextType, };