feat(project) cleanup, removed dependencies, updated structure

This commit is contained in:
Benjamin Wegener
2023-12-08 02:05:23 +00:00
parent 8aeea132a5
commit 07fa57f178
18 changed files with 853 additions and 173 deletions

182
.gitignore vendored
View File

@@ -1,169 +1,27 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
# dependencies
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
# bun
bun.lockb
web_modules/
# environments
*.env
*.env.*
!.env.example
# TypeScript cache
# logs
*.log
\*.tsbuildinfo
# system files
.DS_Store
Thumbs.db
# Optional npm cache directory
# builds
dist/
build/
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# ides
.idea/
.vscode/
*.sublime-workspace
*.sublime-project

24
.npmignore Normal file
View File

@@ -0,0 +1,24 @@
# docs
docs/
# web
website/
# examples
examples/
# test
tests/
# dev
node_modules/
*.log
*.env
*.bun
# system
.DS_Store
Thumbs.db
# bun
bun.lockb

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 Benjamin Wegener <contact@benni.fyi>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

BIN
bun.lockb

Binary file not shown.

78
examples/index.js Normal file
View File

@@ -0,0 +1,78 @@
import { LayServer } from '../src';
class InfoController {
static namespace = 'info';
constructor(server) {
server.get('/', this.index);
server.get('/:country', this.countryInfo);
server.get('/multiple/:a/:a/:a', this.multiple);
}
index(req, res) {
res.text('I think you are right here');
}
countryInfo(req, res) {
res.json({
name: req.params.get('country'),
description: `the country is ${req.params.get('country')}`,
params: [...req.params.entries()],
});
}
multiple(req, res) {
res.json({
params: [...req.params],
});
}
}
const server = new LayServer({
controllers: [
function Main(server) {
server.get('/', async (req, res) => {
return {
message: 'Hi mom, i am on the internet!',
batman: req.query.get('batman'),
body: req.query,
};
});
server.all('/data', (req, res) => {
let respBody = `your ${req.method} request was well recieved`;
switch (req.method) {
case 'POST':
case 'PUT':
return {
message: respBody,
data: {
body: req.body,
},
};
case 'PATCH':
respBody = `we can try but i think there is no way to ${req.method} this out.`;
break;
case 'DELETE':
respBody = 'What do you want to delete?';
break;
case 'OPTIONS':
respBody = `I guess, we are out of ${req.method}. 💩`;
break;
case 'HEAD':
respBody = `this response has not really a body but a big ${req.method}`;
break;
}
return new Response(respBody, {
status: 200,
});
});
server.get('/contact', (req, res) => {
return new Response('This is how you remind me!', {
status: 200,
});
});
},
InfoController,
],
});
server.listen(42069);

View File

@@ -1 +0,0 @@
console.log("Hello via Bun!");

View File

@@ -1,11 +1,11 @@
{
"name": "layc",
"module": "lib/index.js",
"type": "module",
"devDependencies": {
"bun-types": "latest"
"version": "1.0.0",
"main": "lib/layc.cjs",
"module": "lib/layc.js",
"scripts": {
"test": "bun test",
"examples": "bun run ./examples/index.js"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
"type": "module"
}

21
src/IO/LayRequest.js Normal file
View File

@@ -0,0 +1,21 @@
import RequestParser from '../Utils/RequestParser';
export default class LayRequest extends Request {
body = null;
constructor(req) {
super(req);
this.params = new Map();
this.query = new Map();
this.parseBody(req);
}
header(key) {
this.headers?.get(key);
}
async parseBody(req) {
this.body = await RequestParser(req);
}
}

102
src/IO/LayResponse.js Normal file
View File

@@ -0,0 +1,102 @@
import MimeTypeMap from './MimeTypeMap';
export default class LayResponse {
body;
statusCode;
headerList = new Map();
mimeType;
options = {
disableFileCheck: false,
};
constructor(router) {
this.router = router;
this.body = 'Not Implemented';
this.statusCode = 502;
this.headerList = new Map();
this.mimeType = 'text/plain';
}
async send(str) {
this.type('text');
this.body = str;
if (this.options.disableFileCheck) return;
try {
const temp = Bun.file(str);
if (await temp.exists()) {
this.file = str;
}
} catch (err) {}
}
json(data, options = {}) {
let { replacer, space } = options;
if (!Array.isArray(replacer) && typeof replacer != 'function') {
replacer = null;
}
if (isNaN(space) || Math.floor(space) < 0) {
space = 0;
}
this.type('json');
this.body = JSON.stringify(data, replacer, space);
}
type(key) {
if (!MimeTypeMap.has(key)) {
this.mimeType = MimeTypeMap.get('text');
return;
}
this.mimeType = MimeTypeMap.get(key);
}
status(value = null) {
if (isNaN(value)) throw new Error('status has to be a number');
this.statusCode = value || 502;
}
text(value = null) {
if (typeof value != 'string') {
value = JSON.stringify(value);
}
this.type('text');
this.body = value;
}
json(value = null) {
this.text(value);
this.type('json');
}
redirect(value = null, status = 302) {
if (value === null) {
return this.redirectUrl;
}
this.redirectUrl = value;
this.status(status);
}
headers() {
if (!this.headerList.get('Content-Type')) {
this.headerList.set('Content-Type', `${this.mimeType}`);
}
return this.headerList;
}
header(key, value) {
this.headerList.set(key, value);
}
build(options = {}) {
if (this.redirectUrl) {
return Response.redirect(this.redirectUrl, this.status());
}
return new Response(this.body, {
status: this.status(),
headers: this.headers(),
});
}
}

24
src/IO/MimeTypeMap.js Normal file
View File

@@ -0,0 +1,24 @@
export default new Map([
['text', 'text/plain'],
['html', 'text/html'],
['css', 'text/css'],
['csv', 'text/csv'],
['js', 'text/javascript'],
['gif', 'image/gif'],
['jpeg', 'image/jpeg'],
['png', 'image/png'],
['svg', 'image/svg+xml'],
['midi', 'audio/midi'],
['mp3', 'audio/mpeg'],
['oga', 'audio/ogg'],
['mp4', 'video/mp4'],
['mpeg', 'video/mpeg'],
['ogv', 'video/ogg'],
['zip', 'application/zip'],
['pdf', 'application/pdf'],
['epub', 'application/epub+zip'],
['gz', 'application/gzip'],
['bin', 'application/octet-stream'],
['json', 'application/json'],
['jsonld', 'application/ld+json'],
]);

168
src/LayServer.js Normal file
View File

@@ -0,0 +1,168 @@
import { resolve as resolvePath } from 'path';
import Router from './Routing/Router';
import LayRouting from './Routing/LayRouting';
import HtmlEngine from './Views/HtmlEngine';
export default class LayServer {
controllers = new Map();
proc = null;
router;
publicFiles;
options = new Map([
['publicDir', undefined],
['viewsPath', undefined],
['viewsEngine', undefined],
['onlySingleRoutes', true],
['preferIndexFiles', false],
['discoverPublicFiles', true],
['enforceTrailingSlash', true],
]);
constructor(options = {}) {
if (typeof options == 'object') {
const optionKeys = Object.getOwnPropertyNames(options);
for (const key of optionKeys) {
if (!this.options.has(key)) continue;
const valueType = typeof this.options.get(key);
if (typeof options[key] == valueType)
this.options.set(key, options[key]);
}
}
this.initRouting(options.router);
if (options.hasOwnProperty('controllers')) {
this.registerControllers(options.controllers);
}
if (options.publicFiles) {
this.addPublicFileRoutes(options.publicFiles);
}
if (
!options.publicFiles &&
this.options.discoverPublicFiles &&
this.options.publicDir
)
this.indexPublicFiles(this.options.publicDir);
if (!this.options.viewsEngine && this.options.viewsPath) {
this.options.viewsEngine = new HtmlEngine();
}
}
option(key, value = undefined) {
if (!this.options.has(key)) return undefined;
if (value !== undefined) this.option.set(key, value);
return this.options.get(key);
}
addPublicFileRoutes(filePaths) {
if (!filePaths) return;
if (!Array.isArray(filePaths))
throw new Error(
'publicFiles option has to be an array or undefined'
);
for (let path of filePaths) {
}
}
indexPublicFiles(publicDir) {}
initRouting(router = null) {
if (router) this.router = new router(this);
if (typeof this.router == 'undefined') this.router = new Router(this);
this.routeControl = new LayRouting(this.router);
}
registerControllers(controllers) {
if (!Array.isArray(controllers))
throw new Error('controllers is not an Array');
for (let controller of controllers) {
if (typeof controller != 'function') {
console.log(`ignored ${controller} for not being a function`);
continue;
}
this.controllers.set(
controller.name,
this.instantiateController(controller)
);
}
}
instantiateController(controller) {
this.routeControl.setNamespace(controller.namespace);
const _temp = new controller(this.routeControl);
if (controller.namespace) this.routeControl.setNamespace(null);
return _temp;
}
isRunning() {
proc != null;
}
async handleRequest(req) {
const url = new URL(req.url);
if (
!url.pathname.endsWith('/') &&
Array.isArray(this.publicFiles) &&
this.publicFiles.length > 0
) {
let path = this.publicFiles.find(
(v) => url.pathname.substring(1) === v
);
if (path) {
try {
return new Response(
Bun.file(resolvePath(import.meta.dir, 'public', path))
);
} catch (err) {
console.log(err);
return new Response('Internal Server Error', {
status: 500,
});
}
}
}
if (this.options.enforceTrailingSlash && !url.pathname.endsWith('/')) {
return Response.redirect(`${url.toString()}/`, 302);
}
return await this.router.handle(req);
}
listen(port = 3000) {
try {
this.proc = Bun.serve({
port,
development: Bun.env.BUN_ENV != 'production',
fetch: this.handleRequest.bind(this),
error(err) {
console.log(err.stack);
return new Response(`<pre>${err}\n${err.stack}</pre>`, {
headers: {
'Content-Type': 'text/html',
},
});
},
});
if (!this.proc) {
throw new Error('something went wrong');
}
console.log(
this.proc,
`listening on ${this.proc.hostname} with port ${this.proc.port}`
);
} catch (err) {
console.log('where', err.stack);
console.error(err);
}
}
}

View File

@@ -0,0 +1,3 @@
export default class BaseController {
constructor() {}
}

84
src/Routing/LayRouting.js Normal file
View File

@@ -0,0 +1,84 @@
export default class LayRouting {
router;
namespace;
constructor(router, namespace = null) {
this.router = router;
if (namespace !== null) this.namespace = namespace;
}
setNamespace(ns) {
if (!ns || typeof ns != 'string') this.namespace = null;
this.namespace = ns;
}
routes() {
return this.router.routes;
}
add(path, cb, method) {
if (this.namespace) {
if (!path.startsWith('/')) path = '/' + path;
path = this.namespace + path;
}
this.router.add(path, cb, method);
}
route(config = {}) {
if (typeof config != 'object')
throw new Error('route config is not an object');
const { path, handle, method = 'get' } = config;
if (typeof path != 'string')
throw new Error('route path is not a string');
if (typeof handle != 'function')
throw new Error('route handle is not a function');
this.add(path, handle, method);
return this;
}
any(path, cb) {
this.all(path, cb);
}
all(path, cb) {
this.add(path, cb, 'all');
return this;
}
get(path, cb) {
this.add(path, cb, 'get');
return this;
}
post(path, cb) {
this.add(path, cb, 'post');
return this;
}
put(path, cb) {
this.add(path, cb, 'put');
return this;
}
patch(path, cb) {
this.add(path, cb, 'patch');
return this;
}
delete(path, cb) {
this.add(path, cb, 'delete');
return this;
}
head(path, cb) {
this.add(path, cb, 'head');
return this;
}
options(path, cb) {
this.add(path, cb, 'options');
return this;
}
}

251
src/Routing/Router.js Normal file
View File

@@ -0,0 +1,251 @@
import LayRequest from '../IO/LayRequest';
import LayResponse from '../IO/LayResponse';
import RequestParser from '../Utils/RequestParser';
export default class Router {
server = undefined;
routes = new Map();
static allMethodKey = '$all';
static methods = ['all', 'get', 'post', 'put', 'patch', 'head', 'options'];
headerList = new Map();
constructor(server) {
this.server = server;
}
async handle(req) {
if (!(req instanceof Request)) {
throw new Error('`req` is not a proper Request');
}
let method = (req.method || '').toString().toLowerCase();
const methodKey = `$${method.toLowerCase()}`;
let path;
let url = req.url;
if (!(url instanceof URL) && typeof url == 'string') {
url = new URL(url);
path = url.pathname;
}
if (path.startsWith('/')) path = path.substring(1);
if (path.endsWith('/')) path = path.substring(0, path.length - 1);
let currentNode = this.routes;
let paramsList = [];
if (path != '') {
let segments = path.split('/');
for (let index = 0; index < segments.length; index++) {
const segment = segments[index];
if (currentNode.has(segment)) {
currentNode = currentNode.get(segment);
continue;
}
if (currentNode.has(':') || currentNode.has('?')) {
paramsList.push(segment);
currentNode = currentNode.get(':') ?? currentNode.get('?');
continue;
}
currentNode = null;
break;
}
}
if (
!(currentNode instanceof Map) ||
(['get', 'head'].indexOf(method) < 0 &&
!currentNode.has(methodKey) &&
!currentNode.has(Router.allMethodKey))
) {
return new Response(`[${method}] Method Not Allowed`, {
status: 405,
});
}
if (
!currentNode.has(methodKey) &&
!currentNode.get(Router.allMethodKey)
) {
return new Response(`Not Implemented: [${method}, ${req.url}]`, {
status: 501,
});
}
const reqContext = {
request: null,
response: null,
};
try {
reqContext.request = new LayRequest(req);
reqContext.response = new LayResponse(this);
} catch (err) {
console.log(err);
}
let handle =
currentNode.get(methodKey) || currentNode.get(Router.allMethodKey);
if (typeof handle == 'object' && typeof handle.callback == 'function') {
if (Array.isArray(handle.params)) {
for (const name of handle.params) {
let value = paramsList[reqContext.request.params.size];
if (reqContext.request.params.has(name)) {
let yet = reqContext.request.params.get(name);
if (!Array.isArray(yet)) {
yet = [yet];
}
yet.push(value);
value = yet;
}
reqContext.request.params.set(name, value);
}
}
}
try {
const retVal = await handle.callback(
reqContext.request,
reqContext.response
);
if (typeof retVal != 'undefined') {
if (retVal instanceof Response) {
return retVal;
} else if (typeof retVal == 'string') {
reqContext.response.send(retVal);
} else {
reqContext.response.json(retVal);
}
}
} catch (err) {
console.log(err);
return new Response('Error', { status: 500 });
}
if (reqContext.response.redirect()) {
return Response.redirect(
reqContext.response.redirect,
reqContext.response.status || 302
);
}
return new Response(reqContext.response.body, {
status: reqContext.response.status() || 200,
headers: reqContext.response.headers() || {},
});
}
add(route, callback, method = 'get', middleware = null) {
if (typeof callback != 'function')
throw new Error('`callback` is not a function');
if (typeof route != 'string')
throw new Error('`route` is not a string');
if (Router.methods.indexOf(method) < 0)
throw new Error(
`invalid \`method\` (${method}) given`,
Router.methods
);
if (!Array.isArray(middleware)) {
middleware = typeof middleware == 'function' ? [middleware] : null;
}
const methodKey = `$${method.toLowerCase()}`;
if (route.startsWith('/')) route = route.substring(1);
if (route.endsWith('/')) route = route.substring(0, route.length - 1);
let params = [];
let segments = route.split('/');
let currentNode = this.routes;
if (route == '') {
if (currentNode.get(methodKey)) {
if (!this.server.option('allowOverwrite')) {
console.log(
`overwriting of routes is prohibited: [${method}] \`${route}\``
);
return;
}
console.log(`overwriting route for \`${route}\``);
}
currentNode.set(methodKey, { callback, middleware, params: [] });
return;
}
// add method to the segments to store the callback with
// and to allow different methods for the same route
for (let index = 0; index < segments.length; index++) {
let segment = segments[index];
let varName = null;
if (segment.startsWith(':')) {
let newSegment = ':';
varName = segment.substring(1);
if (segment.endsWith('?')) {
// optional param
newSegment = '?';
varName = varName.substring(0, -1);
}
segment = newSegment;
params.push(varName);
}
if (!currentNode.has(segment)) currentNode.set(segment, new Map());
currentNode = currentNode.get(segment);
}
if (currentNode.has(methodKey)) {
if (!this.server.option('allowOverwrite')) {
console.log(
`overwriting of routes is prohibited: [${method}] \`${route}\``
);
return;
}
console.log(`overwriting route for \`${route}\``);
}
currentNode.set(methodKey, { callback, middleware, params });
}
remove(route, method, cb = null) {
const methodKey = `$${method.toLowerCase()}`;
if (route.startsWith('/')) route = route.substring(1);
if (route.endsWith('/')) route = route.substring(0, route.length - 1);
let currentNode = this.routes;
if (route != '') {
let segments = route.toString().split('/');
for (let index = 0; index < segments.length; index++) {
const segment = segments[index];
if (currentNode.has(segment)) {
currentNode = currentNode.get(segment);
continue;
}
if (currentNode.has(':') || currentNode.has('?')) {
currentNode = currentNode.get(':') ?? currentNode.get('?');
continue;
}
currentNode = null;
break;
}
}
if (
!(currentNode instanceof Map) ||
!currentNode.has(methodKey) ||
(typeof cb == 'function' && currentNode.get(methodKey) != cb)
) {
return false;
}
return currentNode.delete(methodKey);
}
}

5
src/Utils/Logger.js Normal file
View File

@@ -0,0 +1,5 @@
export class Logger {
static info() {
const [message, data] = arguments;
}
}

View File

@@ -0,0 +1,46 @@
export async function parseFormData(req, multipart = false) {
const data = await req.formData();
// console.log(tempFile, new Response(tempFile).headers.get('Content-Type'));
// console.log(upload.type, upload.type == tempFile.type);
for (const key of data.keys()) {
const value = data.get(key);
if (value.constructor.name == 'Blob' && multipart) {
// const ext = value.name.toString().split('.').pop().toLowerCase();
const tempFilePath = `/tmp/${Date.now().toString(36)}`;
await Bun.write(tempFilePath, value);
let tempFile = Bun.file(tempFilePath, { type: value.type });
console.log(
tempFile,
new Response(tempFile).headers.get('Content-Type')
);
console.log(value.type, value.type == tempFile.type);
// data.set(key, await Bun.write())
}
}
return data;
}
export default async function RequestParser(req) {
try {
const contentType = req.headers.get('Content-Type')?.toLowerCase();
let val = await req.text();
console.log(val);
return {};
if (!contentType) return await req.text();
if (contentType.startsWith('multipart/form-data'))
return await parseFormData(req, true);
if (contentType == 'application/x-www-form-urlencoded')
return await parseFormData(req);
if (contentType == 'application/json') return await req.json();
return await req.text();
} catch (err) {
console.log(err, typeof req, req.constructor.name);
}
}

7
src/Views/HtmlEngine.js Normal file
View File

@@ -0,0 +1,7 @@
export default class HtmlEngine {
constructor() {}
render(tpl, context = {}) {
return tpl;
}
}

1
src/index.js Normal file
View File

@@ -0,0 +1 @@
export { default as LayServer } from './LayServer';