2024-03-20 03:43:27 +00:00
|
|
|
import { readdir } from 'fs/promises';
|
2023-12-08 02:05:23 +00:00
|
|
|
import { resolve as resolvePath } from 'path';
|
2024-03-20 03:43:27 +00:00
|
|
|
|
2023-12-08 02:05:23 +00:00
|
|
|
import LayRouting from './Routing/LayRouting';
|
2024-02-03 01:12:05 +00:00
|
|
|
import HtmlEngine from './Views/Html';
|
|
|
|
|
import DefaultLoader from './Views/DefaultLoader';
|
2024-03-20 03:43:27 +00:00
|
|
|
import BaseRouter from './Routing/BaseRouter';
|
|
|
|
|
|
|
|
|
|
import Logger from './Utils/Logger';
|
2023-12-08 02:05:23 +00:00
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Represents a LayServer instance that handles routing and server configuration.
|
|
|
|
|
* @todo add multi domain support
|
|
|
|
|
* @class
|
|
|
|
|
*/
|
2024-03-20 03:43:27 +00:00
|
|
|
export default class LayServer extends BaseRouter {
|
2024-03-28 07:07:35 +00:00
|
|
|
#controllers = new Map();
|
|
|
|
|
#loaders = new Map();
|
|
|
|
|
#proc = null;
|
|
|
|
|
#publicFiles;
|
|
|
|
|
#routing = null;
|
|
|
|
|
#options = new Map([
|
|
|
|
|
['layout', undefined],
|
2024-01-20 04:59:20 +00:00
|
|
|
['loaders', undefined],
|
2023-12-08 02:05:23 +00:00
|
|
|
['publicDir', undefined],
|
|
|
|
|
['viewsPath', undefined],
|
|
|
|
|
['preferIndexFiles', false],
|
2024-01-20 04:59:20 +00:00
|
|
|
['discoverPublicFiles', false],
|
|
|
|
|
['enforceTrailingSlash', false],
|
|
|
|
|
['laycLoading', false],
|
2024-03-20 03:43:27 +00:00
|
|
|
['allowRouteOverwrite', true],
|
2023-12-08 02:05:23 +00:00
|
|
|
]);
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Creates a new instance of LayServer.
|
|
|
|
|
* @param {Object} options - The options for configuring the LayServer.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
constructor(options = {}) {
|
2024-03-20 03:43:27 +00:00
|
|
|
super();
|
2023-12-08 02:05:23 +00:00
|
|
|
if (typeof options == 'object') {
|
|
|
|
|
const optionKeys = Object.getOwnPropertyNames(options);
|
|
|
|
|
|
|
|
|
|
for (const key of optionKeys) {
|
2024-03-28 07:07:35 +00:00
|
|
|
if (!this.#options.has(key)) continue;
|
2024-01-20 06:03:07 +00:00
|
|
|
if (key == 'loaders') continue;
|
2024-03-28 07:07:35 +00:00
|
|
|
const valueType = typeof this.#options.get(key);
|
2024-01-20 04:59:20 +00:00
|
|
|
if (
|
|
|
|
|
typeof options[key] !== 'undefined' &&
|
|
|
|
|
(typeof options[key] == valueType ||
|
|
|
|
|
valueType == 'undefined')
|
|
|
|
|
)
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#options.set(key, options[key]);
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
2024-01-20 06:03:07 +00:00
|
|
|
|
|
|
|
|
if (options.hasOwnProperty('loaders')) {
|
|
|
|
|
if (options.loaders instanceof Map) {
|
|
|
|
|
this.loaders = options.loaders;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#routing = new LayRouting(this);
|
2024-03-20 03:43:27 +00:00
|
|
|
|
|
|
|
|
if(options.router) {
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#routing.addRouter(options.router);
|
2024-03-20 03:43:27 +00:00
|
|
|
}
|
2023-12-08 02:05:23 +00:00
|
|
|
|
|
|
|
|
if (options.hasOwnProperty('controllers')) {
|
|
|
|
|
this.registerControllers(options.controllers);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
if (options.publicFiles) this.addPublicFileRoutes(options.publicFiles);
|
2023-12-08 02:05:23 +00:00
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!options.publicFiles &&
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#options.discoverPublicFiles &&
|
|
|
|
|
this.#options.publicDir
|
2023-12-08 02:05:23 +00:00
|
|
|
)
|
2024-03-28 07:07:35 +00:00
|
|
|
this.indexPublicFiles(this.#options.publicDir);
|
2023-12-08 02:05:23 +00:00
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
if (!this.#options.viewsEngine && this.#options.viewsPath) {
|
|
|
|
|
this.#options.viewsEngine = new HtmlEngine();
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
option(key, value = undefined) {
|
2024-03-28 07:07:35 +00:00
|
|
|
if (!this.#options.has(key)) return undefined;
|
2023-12-08 02:05:23 +00:00
|
|
|
if (value !== undefined) this.option.set(key, value);
|
2024-03-28 07:07:35 +00:00
|
|
|
return this.#options.get(key);
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Adds public file routes to the LayServer instance.
|
|
|
|
|
* @param {Array<string>} 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.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
addPublicFileRoutes(filePaths) {
|
|
|
|
|
if (!filePaths) return;
|
|
|
|
|
if (!Array.isArray(filePaths))
|
|
|
|
|
throw new Error(
|
|
|
|
|
'publicFiles option has to be an array or undefined'
|
|
|
|
|
);
|
2024-03-28 07:07:35 +00:00
|
|
|
if (!this.#publicFiles || !Array.isArray(this.#publicFiles))
|
|
|
|
|
this.#publicFiles = [];
|
2023-12-08 02:05:23 +00:00
|
|
|
for (let path of filePaths) {
|
2024-01-20 04:59:20 +00:00
|
|
|
if (typeof path != 'string') continue;
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#publicFiles.push(path);
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* 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) {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger.error('@todo implement method `indexPublicFiles`')
|
2024-02-03 01:12:05 +00:00
|
|
|
// this method is meant to try to iterate over all public files which are stored as static routes
|
|
|
|
|
}
|
2023-12-08 02:05:23 +00:00
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Registers the given controllers.
|
|
|
|
|
*
|
|
|
|
|
* @param {Function|Function[]} controllers - The controllers to register.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
registerControllers(controllers) {
|
2024-01-20 04:59:20 +00:00
|
|
|
if (!Array.isArray(controllers)) controllers = [controllers];
|
2023-12-08 02:05:23 +00:00
|
|
|
|
|
|
|
|
for (let controller of controllers) {
|
|
|
|
|
if (typeof controller != 'function') {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger(`ignored ${controller} for not being a function`);
|
2023-12-08 02:05:23 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-20 04:59:20 +00:00
|
|
|
// if the given controller is just an anonymous function defining the routes itself
|
|
|
|
|
if (!controller?.prototype?.constructor?.name) {
|
2024-03-28 07:07:35 +00:00
|
|
|
controller(this.#routing);
|
2024-01-20 04:59:20 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#controllers.set(
|
2024-01-20 04:59:20 +00:00
|
|
|
controller?.name,
|
2023-12-08 02:05:23 +00:00
|
|
|
this.instantiateController(controller)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Instantiates a controller and sets its namespace in the route control.
|
|
|
|
|
* @param {Object} controller - The controller to be instantiated.
|
|
|
|
|
* @returns {Object} - The instantiated controller.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
instantiateController(controller) {
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#routing.setNamespace(controller.namespace);
|
|
|
|
|
const _temp = new controller(this.#routing);
|
|
|
|
|
if (controller.namespace) this.#routing.setNamespace(null);
|
2023-12-08 02:05:23 +00:00
|
|
|
return _temp;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Checks if the server is running.
|
|
|
|
|
* @returns {boolean} True if the server is running, false otherwise.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
isRunning() {
|
|
|
|
|
proc != null;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Handles an incoming request.
|
|
|
|
|
*
|
|
|
|
|
* @param {Request} req - The incoming request object.
|
|
|
|
|
* @returns {Promise<Response>} - A promise that resolves to a response object.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
async handleRequest(req) {
|
|
|
|
|
const url = new URL(req.url);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!url.pathname.endsWith('/') &&
|
2024-03-28 07:07:35 +00:00
|
|
|
Array.isArray(this.#publicFiles) &&
|
|
|
|
|
this.#publicFiles.length > 0
|
2023-12-08 02:05:23 +00:00
|
|
|
) {
|
2024-03-28 07:07:35 +00:00
|
|
|
let path = this.#publicFiles.find(
|
2023-12-08 02:05:23 +00:00
|
|
|
(v) => url.pathname.substring(1) === v
|
|
|
|
|
);
|
|
|
|
|
if (path) {
|
|
|
|
|
try {
|
|
|
|
|
return new Response(
|
2024-01-20 04:59:20 +00:00
|
|
|
Bun.file(
|
|
|
|
|
resolvePath(
|
2024-03-28 07:07:35 +00:00
|
|
|
this.#options.get('publicDir') ??
|
2024-01-20 04:59:20 +00:00
|
|
|
`${import.meta.dir}/public`,
|
|
|
|
|
path
|
|
|
|
|
)
|
|
|
|
|
)
|
2023-12-08 02:05:23 +00:00
|
|
|
);
|
|
|
|
|
} catch (err) {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger(err, import.meta.dir, 'public', path);
|
2023-12-08 02:05:23 +00:00
|
|
|
return new Response('Internal Server Error', {
|
|
|
|
|
status: 500,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
if (this.#options.enforceTrailingSlash && !url.pathname.endsWith('/')) {
|
2023-12-08 02:05:23 +00:00
|
|
|
return Response.redirect(`${url.toString()}/`, 302);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
let resp = await this.#routing.handle(req);
|
2024-03-20 03:43:27 +00:00
|
|
|
if (resp instanceof Response) return resp;
|
|
|
|
|
return new Response('Not Found', { status: 404 });
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* 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 = () => {}) {
|
2023-12-08 02:05:23 +00:00
|
|
|
try {
|
2024-02-03 01:12:05 +00:00
|
|
|
this.proc = await Bun.serve({
|
2023-12-08 02:05:23 +00:00
|
|
|
port,
|
|
|
|
|
development: Bun.env.BUN_ENV != 'production',
|
|
|
|
|
fetch: this.handleRequest.bind(this),
|
|
|
|
|
error(err) {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger('where', err.stack);
|
2023-12-08 02:05:23 +00:00
|
|
|
return new Response(`<pre>${err}\n${err.stack}</pre>`, {
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'text/html',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!this.proc) {
|
|
|
|
|
throw new Error('something went wrong');
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
if (!!cb && cb.constructor?.name.endsWith('Function')) {
|
|
|
|
|
cb(this.proc);
|
|
|
|
|
}
|
2023-12-08 02:05:23 +00:00
|
|
|
} catch (err) {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger.error('where', err.stack);
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-20 04:59:20 +00:00
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
/**
|
|
|
|
|
* Retrieves the path of a view file based on the given filepath.
|
|
|
|
|
* @param {string} filepath - The filepath of the view file.
|
|
|
|
|
* @returns {Promise<string|null>} - The path of the view file, or null if not found.
|
|
|
|
|
*/
|
|
|
|
|
async getViewPath(filepath) {
|
2024-03-28 07:07:35 +00:00
|
|
|
let path = this.#options.get('viewsPath');
|
2024-02-03 01:12:05 +00:00
|
|
|
if (!path.endsWith('/')) path += '/';
|
|
|
|
|
|
2024-01-20 04:59:20 +00:00
|
|
|
const filename = filepath.split('/').pop().toString();
|
2024-02-03 01:12:05 +00:00
|
|
|
const basedir = filepath.substring(0, filepath.indexOf(filename));
|
|
|
|
|
path += basedir;
|
|
|
|
|
if (!path.endsWith('/')) path += '/';
|
2024-01-20 04:59:20 +00:00
|
|
|
|
2024-02-03 01:12:05 +00:00
|
|
|
const files = await readdir(path);
|
|
|
|
|
for (let file of files) {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger(
|
2024-02-03 01:12:05 +00:00
|
|
|
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();
|
2024-03-28 07:07:35 +00:00
|
|
|
const ext = filename.substring(filename.lastIndexOf('.') + 1);
|
2024-02-03 01:12:05 +00:00
|
|
|
|
|
|
|
|
if (this.loaders.size > 0) {
|
|
|
|
|
for (const keys of this.loaders.keys()) {
|
|
|
|
|
if (!keys.includes(ext)) continue;
|
|
|
|
|
return this.loaders.get(keys).handle(fullFilePath);
|
2024-01-20 04:59:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-03 01:12:05 +00:00
|
|
|
|
|
|
|
|
return new DefaultLoader(filepath);
|
2024-01-20 04:59:20 +00:00
|
|
|
}
|
2024-03-20 03:43:27 +00:00
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
add(path, cb) {
|
|
|
|
|
this.#routing.add(...arguments)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
use (cb) {
|
|
|
|
|
this.add('*', cb, arguments[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasLayout() {
|
|
|
|
|
return typeof(this.#options.get('layout')) == 'string' && this.#options.get('layout').length > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getLayout() {
|
|
|
|
|
if(!this.#options.has('layout')) return undefined;
|
|
|
|
|
|
|
|
|
|
return this.#options.get('layout');
|
2024-03-20 03:43:27 +00:00
|
|
|
}
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|