From 07fa57f17839e1b053a335fc7ae5d45d2200ce10 Mon Sep 17 00:00:00 2001 From: Benjamin Wegener Date: Fri, 8 Dec 2023 02:05:23 +0000 Subject: [PATCH] feat(project) cleanup, removed dependencies, updated structure --- .gitignore | 182 +++--------------------- .npmignore | 24 ++++ LICENSE | 9 ++ bun.lockb | Bin 1314 -> 6694 bytes examples/index.js | 78 +++++++++++ lib/index.js | 1 - package.json | 20 +-- src/IO/LayRequest.js | 21 +++ src/IO/LayResponse.js | 102 ++++++++++++++ src/IO/MimeTypeMap.js | 24 ++++ src/LayServer.js | 168 +++++++++++++++++++++++ src/Routing/BaseController.js | 3 + src/Routing/LayRouting.js | 84 ++++++++++++ src/Routing/Router.js | 251 ++++++++++++++++++++++++++++++++++ src/Utils/Logger.js | 5 + src/Utils/RequestParser.js | 46 +++++++ src/Views/HtmlEngine.js | 7 + src/index.js | 1 + 18 files changed, 853 insertions(+), 173 deletions(-) create mode 100644 .npmignore create mode 100644 LICENSE create mode 100644 examples/index.js delete mode 100644 lib/index.js create mode 100644 src/IO/LayRequest.js create mode 100644 src/IO/LayResponse.js create mode 100644 src/IO/MimeTypeMap.js create mode 100644 src/LayServer.js create mode 100644 src/Routing/BaseController.js create mode 100644 src/Routing/LayRouting.js create mode 100644 src/Routing/Router.js create mode 100644 src/Utils/Logger.js create mode 100644 src/Utils/RequestParser.js create mode 100644 src/Views/HtmlEngine.js create mode 100644 src/index.js diff --git a/.gitignore b/.gitignore index f81d56e..27022db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,169 +1,27 @@ -# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore - -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -\*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -\*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - +# dependencies node_modules/ -jspm_packages/ -# Snowpack dependency directory (https://snowpack.dev/) +# bun +bun.lockb -web_modules/ +# environments +*.env +*.env.* +!.env.example -# TypeScript cache +# logs +*.log -\*.tsbuildinfo +# system files +.DS_Store +Thumbs.db -# Optional npm cache directory +# builds +dist/ +build/ -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -\*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.cache -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -.cache/ - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp -.cache - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VSCode versions used for testing VSCode extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.\* +# ides +.idea/ +.vscode/ +*.sublime-workspace +*.sublime-project diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..004e083 --- /dev/null +++ b/.npmignore @@ -0,0 +1,24 @@ +# docs +docs/ + +# web +website/ + +# examples +examples/ + +# test +tests/ + +# dev +node_modules/ +*.log +*.env +*.bun + +# system +.DS_Store +Thumbs.db + +# bun +bun.lockb diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62e0c41 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 Benjamin Wegener + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/bun.lockb b/bun.lockb index b609b6103481d4fcb7e70106c13bf9d96376a618..36292b70e742f3a201fd383d9348f161b9136617 100755 GIT binary patch literal 6694 zcmeHLd0dV88h^`C-Avh{2@!5&IcukoI+hw_mm))rqux$OXUSPomTtP)D!XJ2A;}m* zWf-HZcU+mqy-e9lm{huqQ)r4v3 zC|ae{YG`R_s9Y0S410f}sJJhow}oD=<;oQ58B~fHMOnjqG`rpf)C1DEAuw+N{S1vX zgw|426|CbqQ8Obx0ea+n){M6D;qz`m{W-TeqwzF*G@M zlKgtw!?Z=myfbTUGxF_75BL6~*tTVm&BVHM%+C4Vte@h6c zT%9wwN>;d}O=UMQ>wjwal(2z~RDZA&%%z&c5BsJxf=9<79_NVtQ2aI)k2nkXHMJ4> zr2z0Sk>6Cm5PU}f_^|djRW_a@_^$x(4tNd={!sh30^XC=A9crHQyP(f4FIS=&YOxw zeu5XkU?httYab$i4uB+p$JjynP30ixj=(?+c;Yv*-k1*L+%p)U|14QRQ++`2gF!I* z5B10Pq4;Hh$M_@ijVz7-6Zz)=Pu@S2P5jUp1Hre0z?%T_iQXg*-vCj2SohWDZp+Jl zFz9`J$bmo1zL5KvhCOmYZe*HikL#bY>)7MmhMi-NbI83+-;(WHu{}xT*rUw0@FVTx z(BpN>*J!~1xtUQ$UQ=q0?eVI4JI6gpxW24*=?340&d*asPYd+F2rWmXc&BbE3DO<$ z^S9!B*T2&S{diHp3E{Ns&pY)0SNxz*ZiK_w?H+4DN94t4jlLs|bH78g`G4EW&%0_i z;A%X*Vpq2EWS5_6axw(xvpR(yE$7yJ=lFT&Bh3wtfg83M?o1uMV#AA@W88nH3%6}{ zU1xvG8;FR$rg6wgC_nt`)t-;+ReP?K9=Dxx>wvhmOVwUU))xCNv8U{At%zw~eq&Fs z@2ijaUc01rsx1mf1nH8LeRzEHkd;o8_So6^oA8qN zm*K6rrzk(x@5`d}1NuErxr-+99kWIbONt8^<#l#uWh(ntNupFQhv;E-g4etB5y&;_ZF5)SX)&xt6l@Pj;C=U>x&!SOw~i7K(F z9CJBo$?34yUfjFBqF7PK799&WyL>%T)TYnA12^tFFP=Iu_E2o=r4?q)cHSG&esFc# zH@?Fi_NPWyZ8za1Us8-8LJL|;D?N64ZaH6?9(C@RN5P|$@CNyp;t%e|Vdiw^eAy{nXL>2z8k019PO$n@BF)hJM`Mk@j*K6&1g`Rr4=7-ptf~!R)yd?iI zyqOQY?C+2Eo1V)(YiORM-kd&bwBh8Y@(H^hI@y-)y*Tvc`Y-6(;^L9j%k?%ZBIdjI zOYFVM#Vhgq9pO1kOMFsxnedW4%J5dMd6^_hz2CLZcewS`$!BwKak1tZBC34-#$I+j?UV1z{sZs``y!)(P91; zlg8`M{ft=X?|;mn(KV_*I53(b*sOcsd-22ru3jq;b|1UHbfuWc}J799O)m zwpo_dH#n%1xGdbNlXyvz+Pn71*kjob=tDWF4?Sx$S3eG4n<1GWW*!i~Hr?=Q_O0&b z&V=Yw|EGGu^zQpp75OCmlLtNq53FRrhIg{PwWn4S!jr4CI;ldzBQ+JCzlNqIP9lMm zRz8PThxK(5J4qFCnMxgIyft6HF*Y;_^JWoC*Q$-tNNZ2Mt4IQnS^-yeMCUT3SYzOV2caK@4PiIurRs-v~Kw-qH) z(XlcMcICP8Tq%mwen{=b+$`3WC*+AJid2nA)rQ#-;LZ?`1NOy{x(@8P88M?_2ds!A z6(HCV!H$5UNUe(0ieN{~6Yzu-MXFh(n#9Th45@pOx)ZbGj%=hdMk-Srl;euJks2AP zQJI?si+G>|RaYe=RVrpjh&!ZSM(S0}j^O=T2eVp}S{kWk0j8hP9#UN+)h*a@GwzUj z8>x4(7_jGEAHefrG{|q2cErVS zWQ>LdI$DRGZ9x0Chl~^~0`!L$p>f$JPs)flSztIA{nGDM2LaHzk;igs$_HnZhdaIHuV4i delta 369 zcmZ2xvWRPfo~B(|$ga|@Pv<$^eu$#ACL9o8qEvVQ3jp&ytZ%nNwu`qxE)5LIj zr79)}hk+p{u`(IN<^p0)28Mj2c& zD@>Ea1#M-4hWz^v0U*P{mN0`@K#Q0suMgtWwg z&Tp8n+MuJ|nwliQ7-y(D**LH7nJ6*DYdXuFjOec ntV+#K(*d#)b28KO^3%9VDhpDJlZ!G7N|H+RbiusI>XQEfw%lFR diff --git a/examples/index.js b/examples/index.js new file mode 100644 index 0000000..e9683a8 --- /dev/null +++ b/examples/index.js @@ -0,0 +1,78 @@ +import { LayServer } from '../src'; + +class InfoController { + static namespace = 'info'; + constructor(server) { + server.get('/', this.index); + server.get('/:country', this.countryInfo); + server.get('/multiple/:a/:a/:a', this.multiple); + } + + index(req, res) { + res.text('I think you are right here'); + } + + countryInfo(req, res) { + res.json({ + name: req.params.get('country'), + description: `the country is ${req.params.get('country')}`, + params: [...req.params.entries()], + }); + } + + multiple(req, res) { + res.json({ + params: [...req.params], + }); + } +} + +const server = new LayServer({ + controllers: [ + function Main(server) { + server.get('/', async (req, res) => { + return { + message: 'Hi mom, i am on the internet!', + batman: req.query.get('batman'), + body: req.query, + }; + }); + server.all('/data', (req, res) => { + let respBody = `your ${req.method} request was well recieved`; + switch (req.method) { + case 'POST': + case 'PUT': + return { + message: respBody, + data: { + body: req.body, + }, + }; + case 'PATCH': + respBody = `we can try but i think there is no way to ${req.method} this out.`; + break; + case 'DELETE': + respBody = 'What do you want to delete?'; + break; + case 'OPTIONS': + respBody = `I guess, we are out of ${req.method}. 💩`; + break; + case 'HEAD': + respBody = `this response has not really a body but a big ${req.method}`; + break; + } + return new Response(respBody, { + status: 200, + }); + }); + server.get('/contact', (req, res) => { + return new Response('This is how you remind me!', { + status: 200, + }); + }); + }, + InfoController, + ], +}); + +server.listen(42069); diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index f67b2c6..0000000 --- a/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json index 7ad76de..5dba6b5 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "layc", - "module": "lib/index.js", - "type": "module", - "devDependencies": { - "bun-types": "latest" - }, - "peerDependencies": { - "typescript": "^5.0.0" - } -} \ No newline at end of file + "name": "layc", + "version": "1.0.0", + "main": "lib/layc.cjs", + "module": "lib/layc.js", + "scripts": { + "test": "bun test", + "examples": "bun run ./examples/index.js" + }, + "type": "module" +} diff --git a/src/IO/LayRequest.js b/src/IO/LayRequest.js new file mode 100644 index 0000000..4221120 --- /dev/null +++ b/src/IO/LayRequest.js @@ -0,0 +1,21 @@ +import RequestParser from '../Utils/RequestParser'; + +export default class LayRequest extends Request { + body = null; + constructor(req) { + super(req); + + this.params = new Map(); + this.query = new Map(); + + this.parseBody(req); + } + + header(key) { + this.headers?.get(key); + } + + async parseBody(req) { + this.body = await RequestParser(req); + } +} diff --git a/src/IO/LayResponse.js b/src/IO/LayResponse.js new file mode 100644 index 0000000..37cf653 --- /dev/null +++ b/src/IO/LayResponse.js @@ -0,0 +1,102 @@ +import MimeTypeMap from './MimeTypeMap'; + +export default class LayResponse { + body; + statusCode; + headerList = new Map(); + mimeType; + options = { + disableFileCheck: false, + }; + + constructor(router) { + this.router = router; + this.body = 'Not Implemented'; + this.statusCode = 502; + this.headerList = new Map(); + this.mimeType = 'text/plain'; + } + + async send(str) { + this.type('text'); + this.body = str; + + if (this.options.disableFileCheck) return; + try { + const temp = Bun.file(str); + + if (await temp.exists()) { + this.file = str; + } + } catch (err) {} + } + + json(data, options = {}) { + let { replacer, space } = options; + if (!Array.isArray(replacer) && typeof replacer != 'function') { + replacer = null; + } + if (isNaN(space) || Math.floor(space) < 0) { + space = 0; + } + this.type('json'); + this.body = JSON.stringify(data, replacer, space); + } + + type(key) { + if (!MimeTypeMap.has(key)) { + this.mimeType = MimeTypeMap.get('text'); + return; + } + this.mimeType = MimeTypeMap.get(key); + } + + status(value = null) { + if (isNaN(value)) throw new Error('status has to be a number'); + this.statusCode = value || 502; + } + + text(value = null) { + if (typeof value != 'string') { + value = JSON.stringify(value); + } + + this.type('text'); + this.body = value; + } + + json(value = null) { + this.text(value); + this.type('json'); + } + + redirect(value = null, status = 302) { + if (value === null) { + return this.redirectUrl; + } + + this.redirectUrl = value; + this.status(status); + } + + headers() { + if (!this.headerList.get('Content-Type')) { + this.headerList.set('Content-Type', `${this.mimeType}`); + } + return this.headerList; + } + + header(key, value) { + this.headerList.set(key, value); + } + + build(options = {}) { + if (this.redirectUrl) { + return Response.redirect(this.redirectUrl, this.status()); + } + return new Response(this.body, { + status: this.status(), + headers: this.headers(), + }); + } +} diff --git a/src/IO/MimeTypeMap.js b/src/IO/MimeTypeMap.js new file mode 100644 index 0000000..4dff0aa --- /dev/null +++ b/src/IO/MimeTypeMap.js @@ -0,0 +1,24 @@ +export default new Map([ + ['text', 'text/plain'], + ['html', 'text/html'], + ['css', 'text/css'], + ['csv', 'text/csv'], + ['js', 'text/javascript'], + ['gif', 'image/gif'], + ['jpeg', 'image/jpeg'], + ['png', 'image/png'], + ['svg', 'image/svg+xml'], + ['midi', 'audio/midi'], + ['mp3', 'audio/mpeg'], + ['oga', 'audio/ogg'], + ['mp4', 'video/mp4'], + ['mpeg', 'video/mpeg'], + ['ogv', 'video/ogg'], + ['zip', 'application/zip'], + ['pdf', 'application/pdf'], + ['epub', 'application/epub+zip'], + ['gz', 'application/gzip'], + ['bin', 'application/octet-stream'], + ['json', 'application/json'], + ['jsonld', 'application/ld+json'], +]); diff --git a/src/LayServer.js b/src/LayServer.js new file mode 100644 index 0000000..9180a20 --- /dev/null +++ b/src/LayServer.js @@ -0,0 +1,168 @@ +import { resolve as resolvePath } from 'path'; +import Router from './Routing/Router'; +import LayRouting from './Routing/LayRouting'; +import HtmlEngine from './Views/HtmlEngine'; + +export default class LayServer { + controllers = new Map(); + proc = null; + router; + publicFiles; + options = new Map([ + ['publicDir', undefined], + ['viewsPath', undefined], + ['viewsEngine', undefined], + ['onlySingleRoutes', true], + ['preferIndexFiles', false], + ['discoverPublicFiles', true], + ['enforceTrailingSlash', true], + ]); + + constructor(options = {}) { + if (typeof options == 'object') { + const optionKeys = Object.getOwnPropertyNames(options); + + for (const key of optionKeys) { + if (!this.options.has(key)) continue; + const valueType = typeof this.options.get(key); + if (typeof options[key] == valueType) + this.options.set(key, options[key]); + } + } + + this.initRouting(options.router); + + if (options.hasOwnProperty('controllers')) { + this.registerControllers(options.controllers); + } + + if (options.publicFiles) { + this.addPublicFileRoutes(options.publicFiles); + } + + if ( + !options.publicFiles && + this.options.discoverPublicFiles && + this.options.publicDir + ) + this.indexPublicFiles(this.options.publicDir); + + if (!this.options.viewsEngine && this.options.viewsPath) { + this.options.viewsEngine = new HtmlEngine(); + } + } + + option(key, value = undefined) { + if (!this.options.has(key)) return undefined; + if (value !== undefined) this.option.set(key, value); + return this.options.get(key); + } + + addPublicFileRoutes(filePaths) { + if (!filePaths) return; + if (!Array.isArray(filePaths)) + throw new Error( + 'publicFiles option has to be an array or undefined' + ); + for (let path of filePaths) { + } + } + + indexPublicFiles(publicDir) {} + + initRouting(router = null) { + if (router) this.router = new router(this); + if (typeof this.router == 'undefined') this.router = new Router(this); + this.routeControl = new LayRouting(this.router); + } + + registerControllers(controllers) { + if (!Array.isArray(controllers)) + throw new Error('controllers is not an Array'); + + for (let controller of controllers) { + if (typeof controller != 'function') { + console.log(`ignored ${controller} for not being a function`); + continue; + } + + this.controllers.set( + controller.name, + this.instantiateController(controller) + ); + } + } + + instantiateController(controller) { + this.routeControl.setNamespace(controller.namespace); + const _temp = new controller(this.routeControl); + if (controller.namespace) this.routeControl.setNamespace(null); + return _temp; + } + + isRunning() { + proc != null; + } + + async handleRequest(req) { + const url = new URL(req.url); + + if ( + !url.pathname.endsWith('/') && + Array.isArray(this.publicFiles) && + this.publicFiles.length > 0 + ) { + let path = this.publicFiles.find( + (v) => url.pathname.substring(1) === v + ); + if (path) { + try { + return new Response( + Bun.file(resolvePath(import.meta.dir, 'public', path)) + ); + } catch (err) { + console.log(err); + return new Response('Internal Server Error', { + status: 500, + }); + } + } + } + + if (this.options.enforceTrailingSlash && !url.pathname.endsWith('/')) { + return Response.redirect(`${url.toString()}/`, 302); + } + + return await this.router.handle(req); + } + + listen(port = 3000) { + try { + this.proc = Bun.serve({ + port, + development: Bun.env.BUN_ENV != 'production', + fetch: this.handleRequest.bind(this), + error(err) { + console.log(err.stack); + return new Response(`
${err}\n${err.stack}
`, { + headers: { + 'Content-Type': 'text/html', + }, + }); + }, + }); + + if (!this.proc) { + throw new Error('something went wrong'); + } + + console.log( + this.proc, + `listening on ${this.proc.hostname} with port ${this.proc.port}` + ); + } catch (err) { + console.log('where', err.stack); + console.error(err); + } + } +} diff --git a/src/Routing/BaseController.js b/src/Routing/BaseController.js new file mode 100644 index 0000000..3a38666 --- /dev/null +++ b/src/Routing/BaseController.js @@ -0,0 +1,3 @@ +export default class BaseController { + constructor() {} +} diff --git a/src/Routing/LayRouting.js b/src/Routing/LayRouting.js new file mode 100644 index 0000000..98a5a34 --- /dev/null +++ b/src/Routing/LayRouting.js @@ -0,0 +1,84 @@ +export default class LayRouting { + router; + namespace; + constructor(router, namespace = null) { + this.router = router; + + if (namespace !== null) this.namespace = namespace; + } + + setNamespace(ns) { + if (!ns || typeof ns != 'string') this.namespace = null; + this.namespace = ns; + } + + routes() { + return this.router.routes; + } + + add(path, cb, method) { + if (this.namespace) { + if (!path.startsWith('/')) path = '/' + path; + path = this.namespace + path; + } + + this.router.add(path, cb, method); + } + + route(config = {}) { + if (typeof config != 'object') + throw new Error('route config is not an object'); + + const { path, handle, method = 'get' } = config; + if (typeof path != 'string') + throw new Error('route path is not a string'); + if (typeof handle != 'function') + throw new Error('route handle is not a function'); + this.add(path, handle, method); + return this; + } + + any(path, cb) { + this.all(path, cb); + } + + all(path, cb) { + this.add(path, cb, 'all'); + return this; + } + + get(path, cb) { + this.add(path, cb, 'get'); + return this; + } + + post(path, cb) { + this.add(path, cb, 'post'); + return this; + } + + put(path, cb) { + this.add(path, cb, 'put'); + return this; + } + + patch(path, cb) { + this.add(path, cb, 'patch'); + return this; + } + + delete(path, cb) { + this.add(path, cb, 'delete'); + return this; + } + + head(path, cb) { + this.add(path, cb, 'head'); + return this; + } + + options(path, cb) { + this.add(path, cb, 'options'); + return this; + } +} diff --git a/src/Routing/Router.js b/src/Routing/Router.js new file mode 100644 index 0000000..3fe2b0b --- /dev/null +++ b/src/Routing/Router.js @@ -0,0 +1,251 @@ +import LayRequest from '../IO/LayRequest'; +import LayResponse from '../IO/LayResponse'; +import RequestParser from '../Utils/RequestParser'; + +export default class Router { + server = undefined; + routes = new Map(); + static allMethodKey = '$all'; + static methods = ['all', 'get', 'post', 'put', 'patch', 'head', 'options']; + headerList = new Map(); + + constructor(server) { + this.server = server; + } + + async handle(req) { + if (!(req instanceof Request)) { + throw new Error('`req` is not a proper Request'); + } + + let method = (req.method || '').toString().toLowerCase(); + const methodKey = `$${method.toLowerCase()}`; + let path; + let url = req.url; + + if (!(url instanceof URL) && typeof url == 'string') { + url = new URL(url); + path = url.pathname; + } + if (path.startsWith('/')) path = path.substring(1); + if (path.endsWith('/')) path = path.substring(0, path.length - 1); + + let currentNode = this.routes; + let paramsList = []; + + if (path != '') { + let segments = path.split('/'); + for (let index = 0; index < segments.length; index++) { + const segment = segments[index]; + + if (currentNode.has(segment)) { + currentNode = currentNode.get(segment); + continue; + } + + if (currentNode.has(':') || currentNode.has('?')) { + paramsList.push(segment); + currentNode = currentNode.get(':') ?? currentNode.get('?'); + continue; + } + + currentNode = null; + break; + } + } + + if ( + !(currentNode instanceof Map) || + (['get', 'head'].indexOf(method) < 0 && + !currentNode.has(methodKey) && + !currentNode.has(Router.allMethodKey)) + ) { + return new Response(`[${method}] Method Not Allowed`, { + status: 405, + }); + } + + if ( + !currentNode.has(methodKey) && + !currentNode.get(Router.allMethodKey) + ) { + return new Response(`Not Implemented: [${method}, ${req.url}]`, { + status: 501, + }); + } + + const reqContext = { + request: null, + response: null, + }; + try { + reqContext.request = new LayRequest(req); + reqContext.response = new LayResponse(this); + } catch (err) { + console.log(err); + } + let handle = + currentNode.get(methodKey) || currentNode.get(Router.allMethodKey); + + if (typeof handle == 'object' && typeof handle.callback == 'function') { + if (Array.isArray(handle.params)) { + for (const name of handle.params) { + let value = paramsList[reqContext.request.params.size]; + if (reqContext.request.params.has(name)) { + let yet = reqContext.request.params.get(name); + if (!Array.isArray(yet)) { + yet = [yet]; + } + yet.push(value); + value = yet; + } + reqContext.request.params.set(name, value); + } + } + } + + try { + const retVal = await handle.callback( + reqContext.request, + reqContext.response + ); + if (typeof retVal != 'undefined') { + if (retVal instanceof Response) { + return retVal; + } else if (typeof retVal == 'string') { + reqContext.response.send(retVal); + } else { + reqContext.response.json(retVal); + } + } + } catch (err) { + console.log(err); + return new Response('Error', { status: 500 }); + } + + if (reqContext.response.redirect()) { + return Response.redirect( + reqContext.response.redirect, + reqContext.response.status || 302 + ); + } + + return new Response(reqContext.response.body, { + status: reqContext.response.status() || 200, + headers: reqContext.response.headers() || {}, + }); + } + + add(route, callback, method = 'get', middleware = null) { + if (typeof callback != 'function') + throw new Error('`callback` is not a function'); + if (typeof route != 'string') + throw new Error('`route` is not a string'); + if (Router.methods.indexOf(method) < 0) + throw new Error( + `invalid \`method\` (${method}) given`, + Router.methods + ); + if (!Array.isArray(middleware)) { + middleware = typeof middleware == 'function' ? [middleware] : null; + } + + const methodKey = `$${method.toLowerCase()}`; + if (route.startsWith('/')) route = route.substring(1); + if (route.endsWith('/')) route = route.substring(0, route.length - 1); + + let params = []; + let segments = route.split('/'); + let currentNode = this.routes; + + if (route == '') { + if (currentNode.get(methodKey)) { + if (!this.server.option('allowOverwrite')) { + console.log( + `overwriting of routes is prohibited: [${method}] \`${route}\`` + ); + return; + } + + console.log(`overwriting route for \`${route}\``); + } + currentNode.set(methodKey, { callback, middleware, params: [] }); + return; + } + + // add method to the segments to store the callback with + // and to allow different methods for the same route + for (let index = 0; index < segments.length; index++) { + let segment = segments[index]; + let varName = null; + + if (segment.startsWith(':')) { + let newSegment = ':'; + varName = segment.substring(1); + if (segment.endsWith('?')) { + // optional param + newSegment = '?'; + varName = varName.substring(0, -1); + } + segment = newSegment; + params.push(varName); + } + + if (!currentNode.has(segment)) currentNode.set(segment, new Map()); + + currentNode = currentNode.get(segment); + } + + if (currentNode.has(methodKey)) { + if (!this.server.option('allowOverwrite')) { + console.log( + `overwriting of routes is prohibited: [${method}] \`${route}\`` + ); + return; + } + + console.log(`overwriting route for \`${route}\``); + } + + currentNode.set(methodKey, { callback, middleware, params }); + } + + remove(route, method, cb = null) { + const methodKey = `$${method.toLowerCase()}`; + if (route.startsWith('/')) route = route.substring(1); + if (route.endsWith('/')) route = route.substring(0, route.length - 1); + + let currentNode = this.routes; + + if (route != '') { + let segments = route.toString().split('/'); + + for (let index = 0; index < segments.length; index++) { + const segment = segments[index]; + + if (currentNode.has(segment)) { + currentNode = currentNode.get(segment); + continue; + } + + if (currentNode.has(':') || currentNode.has('?')) { + currentNode = currentNode.get(':') ?? currentNode.get('?'); + continue; + } + + currentNode = null; + break; + } + } + + if ( + !(currentNode instanceof Map) || + !currentNode.has(methodKey) || + (typeof cb == 'function' && currentNode.get(methodKey) != cb) + ) { + return false; + } + + return currentNode.delete(methodKey); + } +} diff --git a/src/Utils/Logger.js b/src/Utils/Logger.js new file mode 100644 index 0000000..ef83d79 --- /dev/null +++ b/src/Utils/Logger.js @@ -0,0 +1,5 @@ +export class Logger { + static info() { + const [message, data] = arguments; + } +} diff --git a/src/Utils/RequestParser.js b/src/Utils/RequestParser.js new file mode 100644 index 0000000..cb7d1fa --- /dev/null +++ b/src/Utils/RequestParser.js @@ -0,0 +1,46 @@ +export async function parseFormData(req, multipart = false) { + const data = await req.formData(); + + // console.log(tempFile, new Response(tempFile).headers.get('Content-Type')); + // console.log(upload.type, upload.type == tempFile.type); + + for (const key of data.keys()) { + const value = data.get(key); + if (value.constructor.name == 'Blob' && multipart) { + // const ext = value.name.toString().split('.').pop().toLowerCase(); + const tempFilePath = `/tmp/${Date.now().toString(36)}`; + await Bun.write(tempFilePath, value); + let tempFile = Bun.file(tempFilePath, { type: value.type }); + console.log( + tempFile, + new Response(tempFile).headers.get('Content-Type') + ); + console.log(value.type, value.type == tempFile.type); + // data.set(key, await Bun.write()) + } + } + + return data; +} + +export default async function RequestParser(req) { + try { + const contentType = req.headers.get('Content-Type')?.toLowerCase(); + + let val = await req.text(); + console.log(val); + return {}; + + if (!contentType) return await req.text(); + + if (contentType.startsWith('multipart/form-data')) + return await parseFormData(req, true); + if (contentType == 'application/x-www-form-urlencoded') + return await parseFormData(req); + if (contentType == 'application/json') return await req.json(); + + return await req.text(); + } catch (err) { + console.log(err, typeof req, req.constructor.name); + } +} diff --git a/src/Views/HtmlEngine.js b/src/Views/HtmlEngine.js new file mode 100644 index 0000000..b0756ed --- /dev/null +++ b/src/Views/HtmlEngine.js @@ -0,0 +1,7 @@ +export default class HtmlEngine { + constructor() {} + + render(tpl, context = {}) { + return tpl; + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..2481cdd --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +export { default as LayServer } from './LayServer';