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);
+ }
+}