diff --git a/src/LayServer.js~ b/src/LayServer.js~ deleted file mode 100644 index a7c897e..0000000 --- a/src/LayServer.js~ +++ /dev/null @@ -1,308 +0,0 @@ -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~ deleted file mode 100644 index 39c0a09..0000000 --- a/src/Routing/BaseRouter.js~ +++ /dev/null @@ -1,50 +0,0 @@ -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~ deleted file mode 100644 index 84fcf1d..0000000 --- a/src/Routing/LayRouting.js~ +++ /dev/null @@ -1,100 +0,0 @@ -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/Utils/Placekeeper.js~ b/src/Utils/Placekeeper.js~ deleted file mode 100644 index a6da1c4..0000000 --- a/src/Utils/Placekeeper.js~ +++ /dev/null @@ -1,135 +0,0 @@ -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~ deleted file mode 100644 index e7c9ea2..0000000 --- a/src/Utils/RequestParser.js~ +++ /dev/null @@ -1,36 +0,0 @@ -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); - } -}