From b5e8ce3144154d282ac67b72d10bc5ed1f28d48a Mon Sep 17 00:00:00 2001 From: gh0sTedBuddy Date: Wed, 20 Mar 2024 03:43:27 +0000 Subject: [PATCH] feat(domains) implement domain routing; Logger --- examples/index.js | 69 +++----- examples/views/index.html | 29 ++++ src/IO/LayResponse.js | 7 +- src/LayServer.js | 61 +++---- src/LayServer.js~ | 308 ++++++++++++++++++++++++++++++++++++ src/Routing/BaseRouter.js | 51 ++++++ src/Routing/BaseRouter.js~ | 50 ++++++ src/Routing/LayRouting.js | 114 +++++++------ src/Routing/LayRouting.js~ | 100 ++++++++++++ src/Routing/Router.js | 54 +++---- src/Utils/Logger.js | 16 +- src/Utils/Placekeeper.js | 40 ++++- src/Utils/Placekeeper.js~ | 135 ++++++++++++++++ src/Utils/RequestParser.js | 24 +-- src/Utils/RequestParser.js~ | 36 +++++ 15 files changed, 907 insertions(+), 187 deletions(-) create mode 100644 examples/views/index.html create mode 100644 src/LayServer.js~ create mode 100644 src/Routing/BaseRouter.js create mode 100644 src/Routing/BaseRouter.js~ create mode 100644 src/Routing/LayRouting.js~ create mode 100644 src/Utils/Placekeeper.js~ create mode 100644 src/Utils/RequestParser.js~ diff --git a/examples/index.js b/examples/index.js index 0f7413d..ade6c75 100644 --- a/examples/index.js +++ b/examples/index.js @@ -1,5 +1,8 @@ import { LayServer } from '../src'; -import { google } from 'googleapis'; +import {resolve} from 'path'; +import Html from '../src/Views/Html'; + +const APP_PORT = Bun.env.APP_PORT ?? Bun.env.PORT ?? 3000; class InfoController { static namespace = 'info'; @@ -10,7 +13,10 @@ class InfoController { } index(req, res) { - res.text('I think you are right here'); + return res.json({ + headers: req.headers + }) + res.text('I think you are right here now go here'); } countryInfo(req, res) { @@ -28,54 +34,16 @@ class InfoController { } } -class oAuthController { - static namespace = 'oauth'; - constructor(server) { - server.get('/:provider', this.auth.bind(this)); - server.get('/callback/:provider', this.callback.bind(this)); - } - - async auth(req, res) { - if (req.params.get('provider') != 'google') { - return { - messages: 'invalid provider', - data: [...req.params.entries()], - }; - } - - const scopes = 'https://www.googleapis.com/auth/youtube'; - const url = oauth2Client.generateAuthUrl({ - // 'online' (default) or 'offline' (gets refresh_token) - access_type: 'offline', - - // If you only need one scope you can pass it as a string - scope: scopes, - }); - - console.log(url); - - return Response.redirect(url, 301); - } - - callback(req, res) { - if (!req.query.get('code')) { - return new Response('/', { status: 302 }); - } - - return new Response(`/${req.query.get('code')}`); - } -} - const server = new LayServer({ + viewsPath: resolve(__dirname, 'views'), + loaders: new Map([[ + 'html', new Html() + ]]), 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, - }; - }); + return res.json({message: 'hello world'}) + }, { domain: 'api.tipedi.local' }); server.all('/data', (req, res) => { let respBody = `your ${req.method} request was well recieved`; switch (req.method) { @@ -110,9 +78,14 @@ const server = new LayServer({ }); }); }, - oAuthController, InfoController, ], }); -server.listen(Bun.env.APP_PORT ?? Bun.env.PORT ?? 42169); +server.get('/', async (req, res) => { + return res.render('index'); +}); + +server.listen(APP_PORT, () => { + console.log(`server listen on ${APP_PORT}`); +}); diff --git a/examples/views/index.html b/examples/views/index.html new file mode 100644 index 0000000..bbb0915 --- /dev/null +++ b/examples/views/index.html @@ -0,0 +1,29 @@ + + + + + + + {{ title }} + + +
+ + +
+
+ {{ content }} +
+ + + diff --git a/src/IO/LayResponse.js b/src/IO/LayResponse.js index 5e0e1ae..f08a4d1 100644 --- a/src/IO/LayResponse.js +++ b/src/IO/LayResponse.js @@ -1,6 +1,7 @@ import MimeTypeMap from './MimeTypeMap'; export default class LayResponse { + routing = undefined; body; statusCode; headerList = new Map(); @@ -9,8 +10,8 @@ export default class LayResponse { disableFileCheck: false, }; - constructor(router) { - this.router = router; + constructor(routing) { + this.routing = routing; this.body = 'Not Implemented'; this.statusCode = 502; this.headerList = new Map(); @@ -101,7 +102,7 @@ export default class LayResponse { } async render(path, data = {}) { - const loader = await this.router.getServer().load(path); + const loader = await this.routing.getServer().load(path); const content = await loader.parse(data, { status: this.status(), headers: this.headers(), diff --git a/src/LayServer.js b/src/LayServer.js index b778137..7739922 100644 --- a/src/LayServer.js +++ b/src/LayServer.js @@ -1,21 +1,24 @@ +import { readdir } from 'fs/promises'; import { resolve as resolvePath } from 'path'; -import Router from './Routing/Router'; + import LayRouting from './Routing/LayRouting'; import HtmlEngine from './Views/Html'; import DefaultLoader from './Views/DefaultLoader'; -import { readdir } from 'fs/promises'; +import BaseRouter from './Routing/BaseRouter'; + +import Logger from './Utils/Logger'; /** * Represents a LayServer instance that handles routing and server configuration. * @todo add multi domain support * @class */ -export default class LayServer { +export default class LayServer extends BaseRouter { controllers = new Map(); loaders = new Map(); proc = null; - router; publicFiles; + routing = null; options = new Map([ ['loaders', undefined], ['publicDir', undefined], @@ -24,6 +27,7 @@ export default class LayServer { ['discoverPublicFiles', false], ['enforceTrailingSlash', false], ['laycLoading', false], + ['allowRouteOverwrite', true], ]); /** @@ -31,6 +35,7 @@ export default class LayServer { * @param {Object} options - The options for configuring the LayServer. */ constructor(options = {}) { + super(); if (typeof options == 'object') { const optionKeys = Object.getOwnPropertyNames(options); @@ -53,7 +58,11 @@ export default class LayServer { } } - this.initRouting(options.router); + this.routing = new LayRouting(this); + + if(options.router) { + this.routing.addRouter(options.router); + } if (options.hasOwnProperty('controllers')) { this.registerControllers(options.controllers); @@ -110,19 +119,10 @@ export default class LayServer { * @param {string} publicDir - The directory where the public files are stored. */ indexPublicFiles(publicDir) { + Logger.error('@todo implement method `indexPublicFiles`') // this method is meant to try to iterate over all public files which are stored as static routes } - /** - * Initializes the routing for the LayServer. - * @param {Function} [router=null] - The router class to be used for routing. - */ - 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); - } - /** * Registers the given controllers. * @@ -133,13 +133,13 @@ export default class LayServer { for (let controller of controllers) { if (typeof controller != 'function') { - console.log(`ignored ${controller} for not being a function`); + Logger(`ignored ${controller} for not being a function`); continue; } // if the given controller is just an anonymous function defining the routes itself if (!controller?.prototype?.constructor?.name) { - controller(this.routeControl); + controller(this.routing); continue; } @@ -156,9 +156,9 @@ export default class LayServer { * @returns {Object} - The instantiated controller. */ instantiateController(controller) { - this.routeControl.setNamespace(controller.namespace); - const _temp = new controller(this.routeControl); - if (controller.namespace) this.routeControl.setNamespace(null); + this.routing.setNamespace(controller.namespace); + const _temp = new controller(this.routing); + if (controller.namespace) this.routing.setNamespace(null); return _temp; } @@ -199,7 +199,7 @@ export default class LayServer { ) ); } catch (err) { - console.log(err, import.meta.dir, 'public', path); + Logger(err, import.meta.dir, 'public', path); return new Response('Internal Server Error', { status: 500, }); @@ -211,11 +211,9 @@ export default class LayServer { return Response.redirect(`${url.toString()}/`, 302); } - let resp = await this.router.handle(req); - - if (!resp) return new Response('Not Found', { status: 404 }); - - return resp; + let resp = await this.routing.handle(req); + if (resp instanceof Response) return resp; + return new Response('Not Found', { status: 404 }); } /** @@ -229,7 +227,7 @@ export default class LayServer { development: Bun.env.BUN_ENV != 'production', fetch: this.handleRequest.bind(this), error(err) { - console.log('where', err.stack); + Logger('where', err.stack); return new Response(`
${err}\n${err.stack}
`, { headers: { 'Content-Type': 'text/html', @@ -246,8 +244,7 @@ export default class LayServer { cb(this.proc); } } catch (err) { - console.log('where', err.stack); - console.error(err); + Logger.error('where', err.stack); } } @@ -267,7 +264,7 @@ export default class LayServer { const files = await readdir(path); for (let file of files) { - console.log( + Logger( file, file.substring(0, file.indexOf('.')), filename.substring(0, filename.indexOf('.')) || filename @@ -304,4 +301,8 @@ export default class LayServer { return new DefaultLoader(filepath); } + + async add(path, cb) { + this.routing.add(...arguments) + } } diff --git a/src/LayServer.js~ b/src/LayServer.js~ new file mode 100644 index 0000000..a7c897e --- /dev/null +++ b/src/LayServer.js~ @@ -0,0 +1,308 @@ +import { readdir } from 'fs/promises'; +import { resolve as resolvePath } from 'path'; + +import LayRouting from './Routing/LayRouting'; +import HtmlEngine from './Views/Html'; +import DefaultLoader from './Views/DefaultLoader'; +import BaseRouter from './Routing/BaseRouter'; + +import Logger from './Utils/Logger'; + +/** + * Represents a LayServer instance that handles routing and server configuration. + * @todo add multi domain support + * @class + */ +export default class LayServer extends BaseRouter { + controllers = new Map(); + loaders = new Map(); + proc = null; + publicFiles; + routing = null; + options = new Map([ + ['loaders', undefined], + ['publicDir', undefined], + ['viewsPath', undefined], + ['preferIndexFiles', false], + ['discoverPublicFiles', false], + ['enforceTrailingSlash', false], + ['laycLoading', false], + ['allowRouteOverwrite', true], + ]); + + /** + * Creates a new instance of LayServer. + * @param {Object} options - The options for configuring the LayServer. + */ + constructor(options = {}) { + super(); + if (typeof options == 'object') { + const optionKeys = Object.getOwnPropertyNames(options); + + for (const key of optionKeys) { + if (!this.options.has(key)) continue; + if (key == 'loaders') continue; + const valueType = typeof this.options.get(key); + if ( + typeof options[key] !== 'undefined' && + (typeof options[key] == valueType || + valueType == 'undefined') + ) + this.options.set(key, options[key]); + } + + if (options.hasOwnProperty('loaders')) { + if (options.loaders instanceof Map) { + this.loaders = options.loaders; + } + } + } + + this.routing = new LayRouting(this); + + if(options.router) { + this.routing.addRouter(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(); + } + } + + /** + * Retrieves or sets the value of an option. + * @param {string} key - The key of the option. + * @param {*} [value] - The value to set for the option. If not provided, the current value of the option is returned. + * @returns {*} - The current value of the option, or undefined if the option does not exist. + */ + option(key, value = undefined) { + if (!this.options.has(key)) return undefined; + if (value !== undefined) this.option.set(key, value); + return this.options.get(key); + } + + /** + * Adds public file routes to the LayServer instance. + * @param {Array} filePaths - An array of file paths to be added as public file routes. + * @throws {Error} Throws an error if filePaths is not an array or undefined. + */ + addPublicFileRoutes(filePaths) { + if (!filePaths) return; + if (!Array.isArray(filePaths)) + throw new Error( + 'publicFiles option has to be an array or undefined' + ); + if (!this.publicFiles || !Array.isArray(this.publicFiles)) + this.publicFiles = []; + for (let path of filePaths) { + if (typeof path != 'string') continue; + this.publicFiles.push(path); + } + } + + /** + * Indexes all public files stored as static routes. + * @todo Implement this method. + * @param {string} publicDir - The directory where the public files are stored. + */ + indexPublicFiles(publicDir) { + Logger.error('@todo implement method `indexPublicFiles`') + // this method is meant to try to iterate over all public files which are stored as static routes + } + + /** + * Registers the given controllers. + * + * @param {Function|Function[]} controllers - The controllers to register. + */ + registerControllers(controllers) { + if (!Array.isArray(controllers)) controllers = [controllers]; + + for (let controller of controllers) { + if (typeof controller != 'function') { + Logger(`ignored ${controller} for not being a function`); + continue; + } + + // if the given controller is just an anonymous function defining the routes itself + if (!controller?.prototype?.constructor?.name) { + controller(this.routing); + continue; + } + + this.controllers.set( + controller?.name, + this.instantiateController(controller) + ); + } + } + + /** + * Instantiates a controller and sets its namespace in the route control. + * @param {Object} controller - The controller to be instantiated. + * @returns {Object} - The instantiated controller. + */ + instantiateController(controller) { + this.routing.setNamespace(controller.namespace); + const _temp = new controller(this.routing); + if (controller.namespace) this.routing.setNamespace(null); + return _temp; + } + + /** + * Checks if the server is running. + * @returns {boolean} True if the server is running, false otherwise. + */ + isRunning() { + proc != null; + } + + /** + * Handles an incoming request. + * + * @param {Request} req - The incoming request object. + * @returns {Promise} - A promise that resolves to a response object. + */ + 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( + this.options.get('publicDir') ?? + `${import.meta.dir}/public`, + path + ) + ) + ); + } catch (err) { + Logger(err, import.meta.dir, 'public', path); + return new Response('Internal Server Error', { + status: 500, + }); + } + } + } + + if (this.options.enforceTrailingSlash && !url.pathname.endsWith('/')) { + return Response.redirect(`${url.toString()}/`, 302); + } + + let resp = await this.routing.handle(req); + if (resp instanceof Response) return resp; + return new Response('Not Found', { status: 404 }); + } + + /** + * Starts the server and listens for incoming requests on the specified port. + * @param {number} [port=8080] - The port number to listen on. + */ + async listen(port = 8080, cb = () => {}) { + try { + this.proc = await Bun.serve({ + port, + development: Bun.env.BUN_ENV != 'production', + fetch: this.handleRequest.bind(this), + error(err) { + Logger('where', err.stack); + return new Response(`
${err}\n${err.stack}
`, { + headers: { + 'Content-Type': 'text/html', + }, + }); + }, + }); + + if (!this.proc) { + throw new Error('something went wrong'); + } + + if (!!cb && cb.constructor?.name.endsWith('Function')) { + cb(this.proc); + } + } catch (err) { + Logger.error('where', err.stack); + } + } + + /** + * Retrieves the path of a view file based on the given filepath. + * @param {string} filepath - The filepath of the view file. + * @returns {Promise} - The path of the view file, or null if not found. + */ + async getViewPath(filepath) { + let path = this.options.get('viewsPath'); + if (!path.endsWith('/')) path += '/'; + + const filename = filepath.split('/').pop().toString(); + const basedir = filepath.substring(0, filepath.indexOf(filename)); + path += basedir; + if (!path.endsWith('/')) path += '/'; + + const files = await readdir(path); + for (let file of files) { + console.log( + file, + file.substring(0, file.indexOf('.')), + filename.substring(0, filename.indexOf('.')) || filename + ); + if ( + (file.substring(0, file.indexOf('.')) || file) != + (filename.substring(0, filename.indexOf('.')) || filename) + ) + continue; + return `${path}${file}`; + } + + return null; + } + + /** + * Loads a file from the specified filepath. + * + * @param {string} filepath - The path of the file to load. + * @returns {DefaultLoader} - The loader for the file. + */ + async load(filepath) { + const fullFilePath = await this.getViewPath(filepath); + if (!fullFilePath) return new DefaultLoader(filepath); + const filename = fullFilePath.split('/').pop().toString(); + const ext = filename.substring(filename.indexOf('.') + 1); + + if (this.loaders.size > 0) { + for (const keys of this.loaders.keys()) { + if (!keys.includes(ext)) continue; + return this.loaders.get(keys).handle(fullFilePath); + } + } + + return new DefaultLoader(filepath); + } + + async add(path, cb) { + this.routing.add(...arguments) + } +} diff --git a/src/Routing/BaseRouter.js b/src/Routing/BaseRouter.js new file mode 100644 index 0000000..342ab8b --- /dev/null +++ b/src/Routing/BaseRouter.js @@ -0,0 +1,51 @@ +import Logger from '../Utils/Logger'; + +export default class BaseRouter { + add(path, cb, options = {}) { + Logger.error('add method not implemented'); + } + + any(path, cb) { + this.all(path, cb); + } + + all(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'all' }); + return this; + } + + get(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'get' }); + return this; + } + + post(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'post' }); + return this; + } + + put(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'put' }); + return this; + } + + patch(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'patch' }); + return this; + } + + delete(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'delete' }); + return this; + } + + head(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'head' }); + return this; + } + + options(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'options' }); + return this; + } +} diff --git a/src/Routing/BaseRouter.js~ b/src/Routing/BaseRouter.js~ new file mode 100644 index 0000000..39c0a09 --- /dev/null +++ b/src/Routing/BaseRouter.js~ @@ -0,0 +1,50 @@ +import Logger from '../Utils/Logger'; +export default class BaseRouter { + add(path, cb, options = {}) { + Logger.error('add method not implemented'); + } + + any(path, cb) { + this.all(path, cb); + } + + all(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'all' }); + return this; + } + + get(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'get' }); + return this; + } + + post(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'post' }); + return this; + } + + put(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'put' }); + return this; + } + + patch(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'patch' }); + return this; + } + + delete(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'delete' }); + return this; + } + + head(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'head' }); + return this; + } + + options(path, cb) { + this.add(path, cb, { ...(arguments[2] || {}), method: 'options' }); + return this; + } +} diff --git a/src/Routing/LayRouting.js b/src/Routing/LayRouting.js index 98a5a34..84fcf1d 100644 --- a/src/Routing/LayRouting.js +++ b/src/Routing/LayRouting.js @@ -1,10 +1,23 @@ -export default class LayRouting { - router; - namespace; - constructor(router, namespace = null) { - this.router = router; +import Router from './Router'; +import BaseRouter from './BaseRouter'; +import LayRequest from '../IO/LayRequest'; +import LayResponse from '../IO/LayResponse'; +import Logger from '../Utils/Logger'; - if (namespace !== null) this.namespace = namespace; +export default class LayRouting extends BaseRouter { + routers; + namespace = null; + server; + constructor(server, namespace = null) { + super(); + this.server = server; + this.routers = new Map([ + ['$default', new Router(this.server)] + ]); + } + + getServer() { + return this.server; } setNamespace(ns) { @@ -13,16 +26,18 @@ export default class LayRouting { } routes() { - return this.router.routes; + return this.routers.get('$default').routes; } - add(path, cb, method) { - if (this.namespace) { - if (!path.startsWith('/')) path = '/' + path; - path = this.namespace + path; + addRouter(router) { + let routerInstance; + if(typeof router == 'undefined' || router === null) { + routerInstance = new Router(this.server); + } else { + routerInstance = new router(this.server); } - this.router.add(path, cb, method); + this.routers.set(router.domain || '$default', routerInstance); } route(config = {}) { @@ -38,47 +53,48 @@ export default class LayRouting { return this; } - any(path, cb) { - this.all(path, cb); + async handle(req) { + const url = new URL(req.url); + const reqContext = { + request: null, + response: null, + }; + try { + reqContext.request = new LayRequest(req); + reqContext.response = new LayResponse(this); + } catch (err) { + Logger.error(err); + } + + let domain = url.hostname; + if([...this.routers.keys()].indexOf(domain) < 0) domain = '$default'; + return await this.routers.get(domain).handle(reqContext); } - all(path, cb) { - this.add(path, cb, 'all'); - return this; - } + add(path, cb) { + let options = arguments[2] ?? { method: 'get', domain: '$default' }; + if(typeof options.domain != 'string') options.domain = '$default'; + if (this.namespace) { + if (!path.startsWith('/')) path = '/' + path; + path = this.namespace + path; + } - get(path, cb) { - this.add(path, cb, 'get'); - return this; - } + Logger(path, options) - post(path, cb) { - this.add(path, cb, 'post'); - return this; - } + if(options.domain === '$default') { + let domains = this.routers.keys(); + for(let domain of [...domains]) { + this.routers.get(domain).add(path, cb, options); + } + } else { + if(!this.routers.has(options.domain)) { + const domainRouter = Router; + domainRouter.domain = options.domain; + this.addRouter(domainRouter); + Logger('added new router for domain', options.domain) + } - 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; + this.routers.get(options.domain).add (path, cb, options); + } } } diff --git a/src/Routing/LayRouting.js~ b/src/Routing/LayRouting.js~ new file mode 100644 index 0000000..84fcf1d --- /dev/null +++ b/src/Routing/LayRouting.js~ @@ -0,0 +1,100 @@ +import Router from './Router'; +import BaseRouter from './BaseRouter'; +import LayRequest from '../IO/LayRequest'; +import LayResponse from '../IO/LayResponse'; +import Logger from '../Utils/Logger'; + +export default class LayRouting extends BaseRouter { + routers; + namespace = null; + server; + constructor(server, namespace = null) { + super(); + this.server = server; + this.routers = new Map([ + ['$default', new Router(this.server)] + ]); + } + + getServer() { + return this.server; + } + + setNamespace(ns) { + if (!ns || typeof ns != 'string') this.namespace = null; + this.namespace = ns; + } + + routes() { + return this.routers.get('$default').routes; + } + + addRouter(router) { + let routerInstance; + if(typeof router == 'undefined' || router === null) { + routerInstance = new Router(this.server); + } else { + routerInstance = new router(this.server); + } + + this.routers.set(router.domain || '$default', routerInstance); + } + + 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; + } + + async handle(req) { + const url = new URL(req.url); + const reqContext = { + request: null, + response: null, + }; + try { + reqContext.request = new LayRequest(req); + reqContext.response = new LayResponse(this); + } catch (err) { + Logger.error(err); + } + + let domain = url.hostname; + if([...this.routers.keys()].indexOf(domain) < 0) domain = '$default'; + return await this.routers.get(domain).handle(reqContext); + } + + add(path, cb) { + let options = arguments[2] ?? { method: 'get', domain: '$default' }; + if(typeof options.domain != 'string') options.domain = '$default'; + if (this.namespace) { + if (!path.startsWith('/')) path = '/' + path; + path = this.namespace + path; + } + + Logger(path, options) + + if(options.domain === '$default') { + let domains = this.routers.keys(); + for(let domain of [...domains]) { + this.routers.get(domain).add(path, cb, options); + } + } else { + if(!this.routers.has(options.domain)) { + const domainRouter = Router; + domainRouter.domain = options.domain; + this.addRouter(domainRouter); + Logger('added new router for domain', options.domain) + } + + this.routers.get(options.domain).add (path, cb, options); + } + } +} diff --git a/src/Routing/Router.js b/src/Routing/Router.js index 8e621e8..6289219 100644 --- a/src/Routing/Router.js +++ b/src/Routing/Router.js @@ -1,8 +1,9 @@ -import LayRequest from '../IO/LayRequest'; -import LayResponse from '../IO/LayResponse'; import RequestParser from '../Utils/RequestParser'; +import BaseRouter from './BaseRouter'; +import Logger from '../Utils/Logger'; -export default class Router { +export default class Router extends BaseRouter { + domain = undefined; server = undefined; routes = new Map(); static allMethodKey = '$all'; @@ -10,6 +11,7 @@ export default class Router { headerList = new Map(); constructor(server) { + super(); this.server = server; } @@ -17,15 +19,11 @@ export default class Router { return this.server; } - async handle(req) { - if (!(req instanceof Request)) { - throw new Error('`req` is not a proper Request'); - } - - let method = (req.method || '').toString().toLowerCase(); + async handle(reqContext) { + let method = (reqContext.request.method || '').toString().toLowerCase(); const methodKey = `$${method.toLowerCase()}`; let path; - let url = req.url; + let url = reqContext.request.url; if (!(url instanceof URL) && typeof url == 'string') { url = new URL(url); @@ -78,16 +76,6 @@ export default class Router { }); } - 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); @@ -124,7 +112,7 @@ export default class Router { } } } catch (err) { - console.log(err); + Logger(err); return new Response('Error', { status: 500 }); } @@ -141,21 +129,23 @@ export default class Router { }); } - add(route, callback, method = 'get', middleware = null) { + add(route, callback) { + let options = arguments[2]; + let middleware = arguments[3]; 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) + if (Router.methods.indexOf(options.method) < 0) throw new Error( - `invalid \`method\` (${method}) given`, + `invalid \`method\` (${options.method}) given`, Router.methods ); if (!Array.isArray(middleware)) { middleware = typeof middleware == 'function' ? [middleware] : null; } - const methodKey = `$${method.toLowerCase()}`; + const methodKey = `$${options.method.toLowerCase()}`; if (route.startsWith('/')) route = route.substring(1); if (route.endsWith('/')) route = route.substring(0, route.length - 1); @@ -165,14 +155,14 @@ export default class Router { if (route == '') { if (currentNode.get(methodKey)) { - if (!this.server.option('allowOverwrite')) { - console.log( - `overwriting of routes is prohibited: [${method}] \`${route}\`` + if (!this.server.option('allowRouteOverwrite')) { + Logger( + `overwriting of routes is prohibited: [${options.method}] \`${route}\`` ); return; } - console.log(`overwriting route for \`${route}\``); + Logger(`overwriting route for \`${route}\``); } currentNode.set(methodKey, { callback, middleware, params: [] }); return; @@ -202,14 +192,14 @@ export default class Router { } if (currentNode.has(methodKey)) { - if (!this.server.option('allowOverwrite')) { - console.log( + if (!this.server.option('allowRouteOverwrite')) { + Logger( `overwriting of routes is prohibited: [${method}] \`${route}\`` ); return; } - console.log(`overwriting route for \`${route}\``); + Logger(`overwriting route for \`${route}\``); } currentNode.set(methodKey, { callback, middleware, params }); diff --git a/src/Utils/Logger.js b/src/Utils/Logger.js index ef83d79..7268021 100644 --- a/src/Utils/Logger.js +++ b/src/Utils/Logger.js @@ -1,5 +1,15 @@ -export class Logger { - static info() { - const [message, data] = arguments; +const LOGGING_TYPES = ['log', 'debug', 'error', 'warn', 'info']; +function Logger (data, type = 'debug') { + if(!process.env.DEBUG) return; + if(LOGGING_TYPES.indexOf(type) < 0) type = 'log'; + + console[type](...(Array.isArray(data) ? data : [data])); +} + +for(const k of LOGGING_TYPES) { + Logger.prototype[k] = function() { + Logger(arguments, k); } } + +export default Logger; diff --git a/src/Utils/Placekeeper.js b/src/Utils/Placekeeper.js index d7f2078..a6da1c4 100644 --- a/src/Utils/Placekeeper.js +++ b/src/Utils/Placekeeper.js @@ -1,5 +1,6 @@ +import Logger from './Logger'; + export default class Placekeeper { - static KEYWORDS = ['if', 'for', 'while', 'else', 'elseif', 'end']; static init(content) { const sections = Array(); const variables = new Map(); @@ -43,13 +44,46 @@ export default class Placekeeper { } } } - console.log(sections); + + Logger('content to array', Placekeeper.contentToArray(content)); + return { variables, sections, }; } + static contentToArray(content) { + const parts = content.split(/\{\{([^}]+)\}\}|\{(#[a-zA-Z]+)\s*([^}]+)\}/); + const result = []; + let stack = []; + function processStack() { + const block = stack.pop(); + result.push(new Function('data', 'with(data){return `'+ block.content.join('') + '`;}')); + } + + // Process each part of the split content + for (let i = 0; i < parts.length; i++) { + if (parts[i] === '#' && ['for', 'if', 'else', 'elseif', 'while'].includes(parts[i + 1])) { + // Start of control structure + stack.push({ type: parts[i + 1], content: [] }); + i += 2; // Skip the control structure identifier + } else if (parts[i] === '/#' && ['for', 'if', 'while'].includes(stack[stack.length - 1].type)) { + // End of control structure + i++; + processStack(); + } else if (stack.length > 0) { + // Inside a control structure + stack[stack.length - 1].content.push(parts[i]); + } else if (parts[i] !== undefined && parts[i] !== '#end') { + // Directly executable expression or static string + result.push(parts[i].startsWith('\n') ? '\n' + parts[i].trimStart() : parts[i].trimEnd()); + } + } + + return result; + } + static variableRegex = new RegExp(/[\w\d]/, 'i'); static parseVariables(content) { @@ -87,7 +121,7 @@ export default class Placekeeper { let parts = []; let offset = 0; for (const section of sections) { - if (!section instanceof Map) { + if (!(section instanceof Map)) { if (!Array.isArray(section)) continue; if (section.length < 3) continue; } diff --git a/src/Utils/Placekeeper.js~ b/src/Utils/Placekeeper.js~ new file mode 100644 index 0000000..a6da1c4 --- /dev/null +++ b/src/Utils/Placekeeper.js~ @@ -0,0 +1,135 @@ +import Logger from './Logger'; + +export default class Placekeeper { + static init(content) { + const sections = Array(); + const variables = new Map(); + const currentSection = new Map(); + + for (let idx = 0; idx < content.length; idx++) { + const char = content[idx]; + if (char == '{') { + if (content[idx + 1] == '{') { + if (currentSection.has('start')) currentSection.clear(); // everything before this was not a section then + // begins a new section meant to be for a variable but still can be an operational section + // @example: {{#if variableName}} {{#for variableName}} {{#while variableName}} + currentSection.set('start', idx); + } + } else if (currentSection.has('start')) { + if (char == '}') { + if (content[idx + 1] == '}') { + // ends a section + currentSection.set('end', idx + 2); + currentSection.set( + 'content', + content + .substring( + currentSection.get('start'), + currentSection.get('end') + ) + .toString() + .trim() + ); + + currentSection.set( + 'variables', + Placekeeper.parseVariables( + currentSection.get('content') + ) + ); + sections.push(new Map(currentSection)); + currentSection.clear(); + idx++; + } + } + } + } + + Logger('content to array', Placekeeper.contentToArray(content)); + + return { + variables, + sections, + }; + } + + static contentToArray(content) { + const parts = content.split(/\{\{([^}]+)\}\}|\{(#[a-zA-Z]+)\s*([^}]+)\}/); + const result = []; + let stack = []; + function processStack() { + const block = stack.pop(); + result.push(new Function('data', 'with(data){return `'+ block.content.join('') + '`;}')); + } + + // Process each part of the split content + for (let i = 0; i < parts.length; i++) { + if (parts[i] === '#' && ['for', 'if', 'else', 'elseif', 'while'].includes(parts[i + 1])) { + // Start of control structure + stack.push({ type: parts[i + 1], content: [] }); + i += 2; // Skip the control structure identifier + } else if (parts[i] === '/#' && ['for', 'if', 'while'].includes(stack[stack.length - 1].type)) { + // End of control structure + i++; + processStack(); + } else if (stack.length > 0) { + // Inside a control structure + stack[stack.length - 1].content.push(parts[i]); + } else if (parts[i] !== undefined && parts[i] !== '#end') { + // Directly executable expression or static string + result.push(parts[i].startsWith('\n') ? '\n' + parts[i].trimStart() : parts[i].trimEnd()); + } + } + + return result; + } + + static variableRegex = new RegExp(/[\w\d]/, 'i'); + + static parseVariables(content) { + // iterates over the content character by character to extract variables and returns a list of them + const variables = new Map(); + let currentVar; + + for (let idx = 0; idx < content.length; idx++) { + const char = content[idx]; + if (Placekeeper.variableRegex.test(char)) { + if (!(currentVar instanceof Map)) + currentVar = new Map([ + ['name', ''], + ['start', idx], + ['end', -1], + ]); + currentVar.set('name', currentVar.get('name') + char); + } else if (currentVar instanceof Map) { + currentVar.set('end', idx); + if (currentVar.get('name').length > 0) { + variables.set(currentVar.get('name'), currentVar); + } + currentVar = null; + } + } + } + + static parse(content, sections) { + // replace all sections with an empty string + if (!Array.isArray(sections)) { + if (!sections instanceof Map) return content; + sections = [...sections.values()]; + } + + let parts = []; + let offset = 0; + for (const section of sections) { + if (!(section instanceof Map)) { + if (!Array.isArray(section)) continue; + if (section.length < 3) continue; + } + parts.push(content.substring(offset, section.get('start'))); + offset = section.get('end'); + } + parts.push(content.substring(offset)); + + return parts.join(''); + } +} diff --git a/src/Utils/RequestParser.js b/src/Utils/RequestParser.js index cb7d1fa..800343d 100644 --- a/src/Utils/RequestParser.js +++ b/src/Utils/RequestParser.js @@ -1,22 +1,12 @@ +import Logger from './Logger'; 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()) + let tempFile = Bun.file(`/tmp/${Date.now().toString(36)}`, { type: value.type }); + data.set(key, await Bun.write(tempFile, value)); } } @@ -27,13 +17,9 @@ 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')) + if (contentType.indexOf('multipart') >= 0) return await parseFormData(req, true); if (contentType == 'application/x-www-form-urlencoded') return await parseFormData(req); @@ -41,6 +27,6 @@ export default async function RequestParser(req) { return await req.text(); } catch (err) { - console.log(err, typeof req, req.constructor.name); + Logger(err, typeof req, req.constructor.name); } } diff --git a/src/Utils/RequestParser.js~ b/src/Utils/RequestParser.js~ new file mode 100644 index 0000000..e7c9ea2 --- /dev/null +++ b/src/Utils/RequestParser.js~ @@ -0,0 +1,36 @@ +import Logger from './Logger'; +export async function parseFormData(req, multipart = false) { + const data = await req.formData(); + + for (const key of data.keys()) { + const value = data.get(key); + if (value.constructor.name == 'Blob' && multipart) { + let tempFile = Bun.file(`/tmp/${Date.now().toString(36)}`, { type: value.type }); + data.set(key, await Bun.write(tempFile, value)); + } + } + + 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.indexOf('multipart') >= 0) + 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); + } +}