diff --git a/README.md b/README.md index 4630958..9c4bb13 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ **Joor** is a modern, high-performance backend framework built on **Node.js**, designed for **efficiency, scalability, and simplicity**. With **built-in tools** and a **lightweight core**, Joor minimizes dependencies while maximizing performance. +**Note**: Joor is in early development; documentation and features may be incomplete. + ## Why Choose Joor? Joor simplifies backend development while ensuring high performance and security. Whether you’re building small projects or enterprise-level applications, Joor provides a robust foundation with minimal complexity. diff --git a/dev/playgrounds/ground0/package-lock.json b/dev/playgrounds/ground0/package-lock.json index b89cf39..0f8f9e6 100644 --- a/dev/playgrounds/ground0/package-lock.json +++ b/dev/playgrounds/ground0/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^4.21.2" + "express": "^4.21.2", + "socket.io-client": "^4.8.1" } }, "../../../release": { @@ -20,6 +21,12 @@ "create-joor": "cli/creator/index.js" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -194,6 +201,51 @@ "node": ">= 0.8" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -779,6 +831,80 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -836,6 +962,35 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } } } } diff --git a/dev/playgrounds/ground0/package.json b/dev/playgrounds/ground0/package.json index 4ab139d..c7872b8 100644 --- a/dev/playgrounds/ground0/package.json +++ b/dev/playgrounds/ground0/package.json @@ -11,6 +11,7 @@ "license": "ISC", "description": "", "dependencies": { - "express": "^4.21.2" + "express": "^4.21.2", + "socket.io-client": "^4.8.1" } } diff --git a/dev/playgrounds/ground0/socket.js b/dev/playgrounds/ground0/socket.js new file mode 100644 index 0000000..849a54a --- /dev/null +++ b/dev/playgrounds/ground0/socket.js @@ -0,0 +1,27 @@ +const { io } = require('socket.io-client'); + +// Connect to the WebSocket server +const socket = io('http://localhost:3000'); + +// When connected +socket.on('connect', () => { + console.log(`Connected to server with ID: ${socket.id}`); + + // Send a message to the server + socket.emit('message', 'Hello from client!'); + + // Listen for responses + socket.on('response', (data) => { + console.log('Server says:', data); + }); + + // Send a new message every 3 seconds + setInterval(() => { + socket.emit('message', 'Ping from client!'); + }, 1000); +}); +socket.emit('message', 'Hello from client!'); +// Handle disconnection +socket.on('disconnect', () => { + console.log('Disconnected from server.'); +}); diff --git a/dev/playgrounds/ground1/index.ts b/dev/playgrounds/ground1/index.ts index 4c035e9..82aec61 100644 --- a/dev/playgrounds/ground1/index.ts +++ b/dev/playgrounds/ground1/index.ts @@ -1,121 +1,158 @@ import { - Joor, JoorRequest, httpLogger, serveFile, - serveStaticFiles, redirect, + serveStaticFiles, + Logger, } from 'joor'; import { Router, JoorResponse } from 'joor'; +import Joor from 'joor'; import path from 'node:path'; -process.env.JOOR_LOGGER_ENABLE_CONSOLE_LOGGING = 'true'; + +process.env.JOOR_LOGGER_ENABLE_CONSOLE_LOGGING = 'false'; process.env.JOOR_LOGGER_ENABLE_FILE_LOGGING = 'true'; process.env.JOOR_RESPONSE_STREAM_CHUNK_SIZE = '200000'; + const app = new Joor(); -app.use(httpLogger()); -const router = new Router(); - -// app.use(httpLogger()); -router.get('/', (req) => { - const response = new JoorResponse(); - response.setMessage('Hello Noobie').setStatus(200).sendAsStream(); - return response; -}); - -// app.use( -// cors({ -// origins: ['*'], -// methods: ['GET', 'POST', 'PUT', 'DELETE'], -// allowedHeaders: ['Content-Type '], -// exposedHeaders: ['Content-Type'], -// maxAge: 3600, -// allowsCookies: false, -// }) -// ); -router.get('/api/v1/hello', async (req) => { - const response = new JoorResponse(); - response.setDataAsJson({ data: { message: 'Hello from API v1' } }); - return response; -}); -const print = (req: JoorRequest) => { - console.log(req.params); -}; -router.get('/api/v1/hello/:id/:user', print, (req) => { - const response = new JoorResponse(); - const id = req.params?.id; - console.log(req.params); - if (id === '1') { - return redirect('/api/v1/hello/2', true); - } - response - .setMessage(`Hello from API v1 with id ${id}`) - .setStatus(200) - .setHeaders({ 'Content-Type': 'application/json' }); - return response; -}); - -router.get('/api/files/:type', (req) => { - if (req.params?.type === 'json') { - return serveFile({ - filePath: path.join(__dirname, 'public/benchmark.yml'), + +(async () => { + try { + await app.prepare(); + + const socketLogger = new Logger({ + name: 'Socket', + path: path.join(process.cwd(), 'logs', 'socket.log'), + }); + + app.use( + httpLogger({ + flushInterval: 120000, + }) + ); + + app.router.get('/hello', (req) => { + const response = new JoorResponse(); + response.setMessage('Hello Noobie').setStatus(200); + return response; + }); + + app.router.get('/', (req) => { + const response = new JoorResponse(); + response.setMessage('Hello Noobie').setStatus(200).sendAsStream(); + return response; + }); + + app.router.get('/api/v1/hello', async (req) => { + const response = new JoorResponse(); + response.setDataAsJson({ data: { message: 'Hello from API v1' } }); + return response; + }); + + const print = (req: JoorRequest) => { + console.log(req.params); + }; + // app.get('/a', (req) => {}); + + app.router.get('/api/v1/hello/:id/:user', print, (req) => { + const response = new JoorResponse(); + const id = req.params?.id; + console.log(req.params); + if (id === '1') { + return redirect({ + path: '/api/v1/hello/2', + permanent: true, + }); + } + response + .setMessage(`Hello from API v1 with id ${id}`) + .setStatus(200) + .setHeaders({ 'Content-Type': 'application/json' }); + return response; + }); + + app.router.get('/api/files/:type', (req) => { + if (req.params?.type === 'json') { + return serveFile({ + filePath: path.join(__dirname, 'public/benchmark.yml'), + stream: true, + download: false, + }); + } else if (req.params?.type === 'txt') { + return serveFile({ + filePath: path.join(__dirname, 'tes 1 .txt'), + stream: true, + download: false, + }); + } else { + const response = new JoorResponse(); + response.setMessage('File type not supported').setStatus(400); + return response; + } + }); + + app.router.get('/file', (req) => { + return serveFile({ + filePath: path.join(__dirname, 'tes 1 .txt'), + stream: true, + download: false, + }); + }); + + app.use('/file/:path', async (req) => { + console.log(req.params); + }); + + app.router.get( + '/file/:path', + serveStaticFiles({ + routePath: '/file', + folderPath: __dirname, + stream: true, + download: false, + }) + ); + + app.router.get('/public', (req) => { + return serveFile({ + filePath: path.join(__dirname, 'public/benchmark.yml'), + stream: true, + download: false, + }); + }); + + app.serveFiles({ + routePath: '/public1', + folderPath: path.join(__dirname, 'public'), stream: true, download: false, }); - } else if (req.params?.type === 'txt') { - return serveFile({ - filePath: path.join(__dirname, 'tes 1 .txt'), + + app.serveFiles({ + routePath: '/public', + folderPath: path.join(__dirname, 'public/f'), stream: true, download: false, }); - } else { - const response = new JoorResponse(); - response.setMessage('File type not supported').setStatus(400); - return response; + // app.sockets.on('connection', (socket) => { + // console.log(socket.id); + // socketLogger.info(`New socket connection: ${socket.id}`); + // socket.onAny((event, ...args) => { + // socketLogger.info(`Socket event: ${event}`, args); + // }); + // socket.on('message', (message) => { + // socketLogger.info(`Received message from ${socket.id}: ${message}`); + // socket.send(`Echo: ${message}`); + // }); + // socket.on('disconnect', () => { + // socketLogger.info(`Socket disconnected: ${socket.id}`); + // }); + // }); + + app.start(); + } catch (error) { + console.error('Error during app preparation:', error); + process.exit(1); } -}); - -router.get('/file', (req) => { - return serveFile({ - filePath: path.join(__dirname, 'tes 1 .txt'), - stream: true, - download: false, - }); -}); -app.use('/file/:path', async (req) => { - console.log(req.params); -}); -router.get( - '/file/:path', - serveStaticFiles({ - routePath: '/file', - folderPath: __dirname, - stream: true, - download: false, - }) -); - -router.get('/public', (req) => { - return serveFile({ - filePath: path.join(__dirname, 'public/benchmark.yml'), - stream: true, - download: false, - }); -}); -app.serveFiles({ - routePath: '/public1', - folderPath: path.join(__dirname, 'public'), - stream: true, - download: false, -}); - -app.serveFiles({ - routePath: '/public', - folderPath: path.join(__dirname, 'public/f'), - stream: true, - download: false, -}); - -// app.start(); - -app.start(); +})(); diff --git a/dev/playgrounds/ground1/joor.config.ts b/dev/playgrounds/ground1/joor.config.ts index 2adfa85..c9e2159 100644 --- a/dev/playgrounds/ground1/joor.config.ts +++ b/dev/playgrounds/ground1/joor.config.ts @@ -1,5 +1,5 @@ -import { JOOR_CONFIG } from 'joor'; -const config: JOOR_CONFIG = { +// import { JOOR_CONFIG } from 'joor'; +const config = { server: { port: 3000, host: 'localhost', diff --git a/dev/playgrounds/ground2/joor.config.js b/dev/playgrounds/ground2/joor.config.js index 8a7b431..9419767 100644 --- a/dev/playgrounds/ground2/joor.config.js +++ b/dev/playgrounds/ground2/joor.config.js @@ -1,6 +1,6 @@ const config = { server: { - port: 3000, + port: 2000, host: 'localhost', mode: 'http', }, diff --git a/dev/playgrounds/ground2/main.js b/dev/playgrounds/ground2/main.js index 4164aff..e191cca 100644 --- a/dev/playgrounds/ground2/main.js +++ b/dev/playgrounds/ground2/main.js @@ -187,4 +187,4 @@ router.get('/hello', (_req) => { response.message = 'OK'; return response; }); -app.start(); +app.prepare().then(() => app.start()); diff --git a/dev/playgrounds/ground2/package.json b/dev/playgrounds/ground2/package.json index e246697..eae3da4 100644 --- a/dev/playgrounds/ground2/package.json +++ b/dev/playgrounds/ground2/package.json @@ -9,5 +9,6 @@ "keywords": [], "author": "", "license": "ISC", - "description": "" + "description": "", + "dependencies": {} } diff --git a/dev/scripts/build.ts b/dev/scripts/build.ts index 8b0d8a1..dfb4218 100644 --- a/dev/scripts/build.ts +++ b/dev/scripts/build.ts @@ -15,9 +15,6 @@ const packageFileData = ` "main": "./src/index.js", "types": "./src/index.d.ts", "type":"commonjs", - "bin":{ - "create-joor": "cli/creator/index.js" - }, "dependencies": ##dependencies## } `; diff --git a/dev/scripts/setup.js b/dev/scripts/setup.js index 89f3d5b..a8c68cf 100644 --- a/dev/scripts/setup.js +++ b/dev/scripts/setup.js @@ -1,121 +1,128 @@ -// import { promisify } from 'util'; -// import { exec as execCallback } from 'child_process'; -// import fs from 'fs'; -// import path from 'path'; - -// // Promisify exec function to work with async/await -// const exec = promisify(execCallback); - -// // Function to check if a specific version of a package is already installed globally -// const isPackageVersionInstalledGlobally = async (packageName, version) => { -// try { -// const result = await exec(`npm list -g ${packageName}`); -// return result.stdout.includes(`${packageName}@${version}`); -// } catch { -// return false; -// } -// }; - -// // Function to check if a specific version of a local package is installed -// const isLocalPackageVersionInstalled = async (packageName, version) => { -// try { -// const packageJsonPath = path.join( -// process.cwd(), -// 'node_modules', -// packageName, -// 'package.json' -// ); -// if (fs.existsSync(packageJsonPath)) { -// const packageJson = require(packageJsonPath); -// return packageJson.version === version; -// } -// return false; -// } catch { -// return false; -// } -// }; - -// // Function to install packages if they aren't already installed -// const installPackage = async (name, version, isGlobal = false) => { -// const isInstalled = isGlobal -// ? await isPackageVersionInstalledGlobally(name, version) -// : await isLocalPackageVersionInstalled(name, version); - -// if (isInstalled) { -// console.info( -// `${name}@${version} is already installed ${ -// isGlobal ? 'globally' : 'locally' -// }.\n` -// ); -// return true; -// } else { -// const command = `npm i ${isGlobal ? '-g ' : ''}${name}@${version}`; -// console.info(`Installing ${name}@${version}...\n`); -// try { -// const result = await exec(command); -// if (result.stderr) { -// console.info( -// `Failed to install: ${name}@${version}. Try running: '${command}' manually.` -// ); -// return false; -// } -// return true; -// } catch (error) { -// console.error(`Error installing ${name}@${version}:`, error); -// return false; -// } -// } -// }; - -// // Main function to set up the development environment -// const main = async () => { -// const toInstallGlobally = [ -// { name: 'tsx', version: '3.0.0' }, -// { name: 'typescript', version: '5.6.3' }, -// ]; - -// let totalInstalled = 0; -// if (console.clear) { -// console.clear(); -// } else { -// process.stdout.write('\x1Bc'); -// } - -// console.info('\n\nSetting up the development environment for Joor.\n\n'); -// console.info('\nInstalling Global Packages\n'); - -// // Install global packages -// for (const { name, version } of toInstallGlobally) { -// const success = await installPackage(name, version, true); -// if (success) totalInstalled++; -// } - -// console.info( -// `\nInstalled ${totalInstalled}/${toInstallGlobally.length} global packages successfully.\n` -// ); - -// // Check if local dependencies are already installed -// const nodeModulesPath = path.join(process.cwd(), 'node_modules'); -// if (fs.existsSync(nodeModulesPath)) { -// console.info('\nLocal dependencies are already installed.\n'); -// } else { -// // Install local dependencies if not installed -// console.info("\nInstalling Joor's local dependencies...\n"); -// try { -// const dependenciesResult = await exec('npm i'); -// if (dependenciesResult.stderr) { -// console.info( -// "\nError while installing Joor's dependencies:\n", -// dependenciesResult.stderr -// ); -// } else { -// console.info('\nAll local dependencies installed successfully.'); -// } -// } catch (error) { -// console.error('\nError installing dependencies:', error); -// } -// } -// }; - -// // Call the main function to start the setup process -// main(); +import { exec as execCallback } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; + +// Promisify exec function to work with async/await +const exec = promisify(execCallback); + +// Function to check if a specific version of a package is already installed globally +const isPackageVersionInstalledGlobally = async (packageName, version) => { + try { + const result = await exec(`npm list -g ${packageName}`); + + return result.stdout.includes(`${packageName}@${version}`); + } catch { + return false; + } +}; + +// Function to check if a specific version of a local package is installed +const isLocalPackageVersionInstalled = async (packageName, version) => { + try { + const packageJsonPath = path.join( + process.cwd(), + 'node_modules', + packageName, + 'package.json' + ); + + if (fs.existsSync(packageJsonPath)) { + const packageJson = require(packageJsonPath); + + return packageJson.version === version; + } + + return false; + } catch { + return false; + } +}; + +// Function to install packages if they aren't already installed +const installPackage = async (name, version, isGlobal = false) => { + const isInstalled = isGlobal + ? await isPackageVersionInstalledGlobally(name, version) + : await isLocalPackageVersionInstalled(name, version); + + if (isInstalled) { + console.info( + `${name}@${version} is already installed ${ + isGlobal ? 'globally' : 'locally' + }.\n` + ); + return true; + } else { + const command = `npm i ${isGlobal ? '-g ' : ''}${name}@${version}`; + console.info(`Installing ${name}@${version}...\n`); + try { + const result = await exec(command); + + if (result.stderr) { + console.info( + `Failed to install: ${name}@${version}. Try running: '${command}' manually.` + ); + return false; + } + + return true; + } catch (error) { + console.error(`Error installing ${name}@${version}:`, error); + return false; + } + } +}; + +// Main function to set up the development environment +const main = async () => { + const toInstallGlobally = [ + { name: 'tsx', version: '4.19.2' }, + { name: 'typescript', version: '5.7.3' }, + { name: 'mintlify', version: '4.0.365' }, + ]; + + let totalInstalled = 0; + + /* eslint-disable no-console */ + if (console.clear) { + console.clear(); + } else { + process.stdout.write('\x1Bc'); + } + /* eslint-enable no-console */ + console.info('\n\nSetting up the development environment for Joor.\n\n'); + console.info('\nInstalling Global Packages\n'); + // Install global packages + for (const { name, version } of toInstallGlobally) { + const success = await installPackage(name, version, true); + if (success) totalInstalled++; + } + console.info( + `\nInstalled ${totalInstalled}/${toInstallGlobally.length} global packages successfully.\n` + ); + // Check if local dependencies are already installed + const nodeModulesPath = path.join(process.cwd(), 'node_modules'); + + if (fs.existsSync(nodeModulesPath)) { + console.info('\nLocal dependencies are already installed.\n'); + } else { + // Install local dependencies if not installed + console.info("\nInstalling Joor's local dependencies...\n"); + try { + const dependenciesResult = await exec('npm i'); + + if (dependenciesResult.stderr) { + console.info( + "\nError while installing Joor's dependencies:\n", + dependenciesResult.stderr + ); + } else { + console.info('\nAll local dependencies installed successfully.'); + } + } catch (error) { + console.error('\nError installing dependencies:', error); + } + } +}; +// Call the main function to start the setup process +main(); diff --git a/docs/mint.json b/docs/mint.json index d5b97a5..5f13b70 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -19,7 +19,7 @@ }, "topbarCtaButton": { "type": "github", - "url": "https://github.com/arpan404/joor" + "url": "https://github.com/socioy/joor" }, "tabs": [ { diff --git a/docs/v1/reference/env.mdx b/docs/v1/reference/env.mdx new file mode 100644 index 0000000..c0a8ff8 --- /dev/null +++ b/docs/v1/reference/env.mdx @@ -0,0 +1,3 @@ +JOOR_LOGGER_ENABLE_CONSOLE_LOGGING +JOOR_LOGGER_ENABLE_FILE_LOGGING +JOOR_RESPONSE_STREAM_CHUNK_SIZE diff --git a/package.json b/package.json index febf639..a40f6d4 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "access": "public" }, "dependencies": { - "mime-types": "^2.1.35" + "mime-types": "^2.1.35", + "socket.io": "^4.8.1" }, "exports": { "./middlewares": "./src/middlewares/index.js" diff --git a/src/core/config/configuration.ts b/src/core/config/configuration.ts new file mode 100644 index 0000000..af54205 --- /dev/null +++ b/src/core/config/configuration.ts @@ -0,0 +1,113 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import Jrror from '@/core/error/index'; +// import validateConfig from '@/helpers/validateConfig'; +import JOOR_CONFIG from '@/types/config'; + +/** + * A class responsible for loading and managing the configuration data. + */ +class Configuration { + /** + * Static variable to hold the configuration data. + * @private + */ + private static configData: JOOR_CONFIG | null = null; + + /** + * Loads the configuration data from the configuration file (either `joor.config.js` or `joor.config.ts`). + * Throws an error if the configuration has already been loaded or if loading fails. + * + * @throws {Jrror} Throws an error if configuration data is already loaded or if loading fails. + * @returns {Promise} A promise that resolves when the configuration is successfully loaded. + * @private + */ + private async loadConfig(): Promise { + // Check if the configuration data is already loaded + if (Configuration.configData !== null) { + throw new Jrror({ + code: 'config-loaded-already', + docsPath: '/configuration', + message: + 'The configuration data is already loaded. Attempting to load it again is not recommended', + type: 'warn', + }); + } + + try { + // Default config file name is joor.config.js or else fallback to joor.config.ts + let configFile = 'joor.config.js'; + + if (!fs.existsSync(path.resolve(process.cwd(), configFile))) { + configFile = 'joor.config.ts'; + } + + if (!fs.existsSync(path.resolve(process.cwd(), configFile))) { + throw new Jrror({ + code: 'config-file-missing', + docsPath: '/configuration', + message: + 'The configuration file (joor.config.js or joor.config.ts) is missing in the root directory.', + type: 'error', + }); + } + + const configPath = path.resolve(process.cwd(), configFile); + // Dynamically import the configuration file + const configData = (await import(configPath)).config as JOOR_CONFIG; + // Configuration.configData = validateConfig(configData); + Configuration.configData = configData; + this.setConfigToEnv(); + } catch (error) { + throw new Jrror({ + code: 'config-load-failed', + message: `Error occured while loading the configuration file. ${error}`, + type: 'panic', + docsPath: '/configuration', + }); + } + } + + /** + * Sets the needed configuration data to environment variables. + * This env values are used in the application by other modules and packages. + */ + private setConfigToEnv(): void { + // File Size for Logger + process.env.JOOR_LOGGER_MAX_FILE_SIZE = + Configuration.configData?.logger?.maxFileSize?.toString() ?? '10485760'; + // Logger File Logging + process.env.JOOR_LOGGER_ENABLE_FILE_LOGGING = + Configuration.configData?.logger?.enable?.file?.toString() ?? 'true'; + // Logger Console Logging; only enabled in development mode for performance reasons + process.env.JOOR_LOGGER_ENABLE_CONSOLE_LOGGING = + (Configuration.configData?.logger?.enable?.console?.toString() ?? + Configuration.configData?.mode === 'development') + ? 'true' + : 'false'; + + if (Configuration.configData?.env?.values) { + const keys = Object.keys(Configuration.configData.env.values); + keys.forEach((key) => { + process.env[key] = Configuration.configData?.env?.values?.[key]; + }); + } + } + /** + * Retrieves the configuration data. If the configuration data is not already loaded, it will load it. + * + * @returns {Promise} A promise that resolves with the configuration data. + * @throws {Jrror} Throws an error if the configuration cannot be loaded. + */ + public async getConfig(): Promise { + // Load the configuration data if not already loaded + if (Configuration.configData === null) { + await this.loadConfig(); + } + + return Configuration.configData as JOOR_CONFIG; + } +} + +export default Configuration; diff --git a/src/core/config/index.ts b/src/core/config/index.ts index 294f07e..4c96eac 100644 --- a/src/core/config/index.ts +++ b/src/core/config/index.ts @@ -1,85 +1,2 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import Jrror from '@/core/error/index'; -import JOOR_CONFIG from '@/types/config'; - -/** - * A class responsible for loading and managing the configuration data. - */ -class Configuration { - /** - * Static variable to hold the configuration data. - * @private - */ - private static configData: JOOR_CONFIG | null = null; - - /** - * Loads the configuration data from the configuration file (either `joor.config.js` or `joor.config.ts`). - * Throws an error if the configuration has already been loaded or if loading fails. - * - * @throws {Jrror} Throws an error if configuration data is already loaded or if loading fails. - * @returns {Promise} A promise that resolves when the configuration is successfully loaded. - * @private - */ - private async loadConfig(): Promise { - // Check if the configuration data is already loaded - if (Configuration.configData !== null) { - throw new Jrror({ - code: 'config-loaded-already', - docsPath: '/configuration', - message: - 'The configuration data is already loaded. Attempting to load it again is not recommended', - type: 'warn', - }); - } - - try { - // Default config file name is joor.config.js or else fallback to joor.config.ts - let configFile = 'joor.config.js'; - - if (!fs.existsSync(path.resolve(process.cwd(), configFile))) { - configFile = 'joor.config.ts'; - } - - if (!fs.existsSync(path.resolve(process.cwd(), configFile))) { - throw new Jrror({ - code: 'config-file-missing', - docsPath: '/configuration', - message: - 'The configuration file (joor.config.js or joor.config.ts) is missing in the root directory.', - type: 'error', - }); - } - - const configPath = path.resolve(process.cwd(), configFile); - // Dynamically import the configuration file - Configuration.configData = (await import(configPath)) - .config as JOOR_CONFIG; - } catch (error) { - throw new Jrror({ - code: 'config-load-failed', - message: `Error occured while loading the configuration file. ${error}`, - type: 'panic', - docsPath: '/configuration', - }); - } - } - - /** - * Retrieves the configuration data. If the configuration data is not already loaded, it will load it. - * - * @returns {Promise} A promise that resolves with the configuration data. - * @throws {Jrror} Throws an error if the configuration cannot be loaded. - */ - public async getConfig(): Promise { - // Load the configuration data if not already loaded - if (Configuration.configData === null) { - await this.loadConfig(); - } - - return Configuration.configData as JOOR_CONFIG; - } -} - +import Configuration from '@/core/config/configuration'; export default Configuration; diff --git a/src/core/joor/index.ts b/src/core/joor/index.ts index 00d5d59..32301ea 100644 --- a/src/core/joor/index.ts +++ b/src/core/joor/index.ts @@ -1,8 +1,10 @@ -import addMiddlewares from '../router/addMiddlewares'; +import { Server as SocketServer } from 'socket.io'; import Configuration from '@/core/config'; import Jrror from '@/core/error'; import Server from '@/core/joor/server'; +import Router from '@/core/router'; +import addMiddlewares from '@/core/router/addMiddlewares'; import loadEnv from '@/enhanchers/loadEnv'; import logger from '@/helpers/joorLogger'; import JOOR_CONFIG from '@/types/config'; @@ -10,98 +12,153 @@ import { SERVE_FILES, SERVE_FILES_CONFIG } from '@/types/joor'; import { ROUTE_HANDLER, ROUTE_PATH } from '@/types/route'; /** - * Represents the Joor framework server. - * This class is used to initiate and start a new Joor server with the provided configuration. - * - * @example - * ```typescript - * import Joor from "joor"; - * const app = new Joor(); - * await app.start(); - * ``` - * This example starts a new Joor server using the default configuration data from the `joor.config.ts` or `joor.config.js` file. - * + * The core class for creating a Joor server application. */ class Joor { - // Private variable to hold configuration data used in the server, initialized as null + /** + * The loaded configuration data for the server. + */ private configData: JOOR_CONFIG | undefined; + + /** + * Static file serving configuration. Stores paths and details for serving static files. + */ public static staticFileDirectories = { paths: [] as Array, details: {} as SERVE_FILES, }; - // public static + + /** + * The underlying HTTP/HTTPS server instance. + */ + private server: Server = null as unknown as Server; + + /** + * The Socket.IO server instance for real-time communication. + */ + public sockets: SocketServer | null = null; + + /** + * The router instance for defining application routes. + * Use this to define your API endpoints. + * @example + * ```typescript + * app.router.get('/hello', (req) => { + * return new JoorResponse().setMessage('Hello Noobie').setStatus(200); + * }); + * ``` + */ + public router = new Router(); + + /** + * Prepares the Joor application by initializing the server and sockets. + * This method must be called immediately after creating an instance of Joor. + * @returns A Promise that resolves to the Joor instance. + * @throws {Jrror} If server preparation fails. + */ + public async prepare(): Promise { + try { + this.server = new Server(); + await this.server.initialize(); + await this.initializeSockets(); + return this; + } catch (error) { + logger.error('Failed to prepare server:', error); + throw new Jrror({ + code: 'server-preparation-failed', + message: `Failed to prepare server: ${error}`, + type: 'panic', + docsPath: '/joor-server', + }); + } + } + /** - * Starts a new Joor server. - * This method must always be awaited as it is asynchronous. - * + * Starts the Joor server. This method is asynchronous and must be awaited. + * @returns A Promise that resolves when the server starts successfully. + * @throws {Jrror} If configuration loading or server start fails. * @example * ```typescript * const app = new Joor(); - * await app.start(); + * await app.prepare().start(); * ``` - * This example starts the server with the loaded configuration. - * - * @throws {Jrror} Throws a custom error if configuration is not found. But also handles it. */ public async start(): Promise { try { await this.initialize(); loadEnv(); - if (this.configData) { - const server = new Server(); - await server.listen(); - } else { + if (!this.configData) { throw new Jrror({ code: 'config-load-failed', - message: `Error occured while loading the configuration file.`, + message: 'Configuration not loaded', type: 'panic', docsPath: '/configuration', }); } + await this.server.listen(); } catch (error: unknown) { if (error instanceof Jrror) { error.handle(); } else { - logger.info(`Unknown Error Occurred.\n${error}`); + logger.error(`Server start failed:`, error); + throw new Jrror({ + code: 'server-start-failed', + message: `Failed to start server: ${error}`, + type: 'panic', + docsPath: '/joor-server', + }); } } } /** - * Adds global middlewares to specified routes. - * + * Initializes the Socket.IO server. + * @returns A Promise that resolves when the Socket.IO server is initialized. + * @throws {Jrror} If Socket.IO initialization fails. + */ + private async initializeSockets(): Promise { + try { + this.sockets = new SocketServer( + this.server.server, + this.configData?.socket?.options + ); + } catch (error) { + logger.error('Socket initialization failed:', error); + throw new Jrror({ + code: 'socket-initialization-failed', + message: `Failed to initialize Socket.IO: ${error}`, + type: 'error', + docsPath: '/websockets', + }); + } + } + + /** + * Adds global middleware to specified routes. + * @param {...(ROUTE_PATH | ROUTE_HANDLER)[]} data - An array of route paths and middleware functions. + * Route paths can be strings (e.g., "/", "/api/*"). + * Middleware functions are functions that handle requests. * @example * ```typescript - * const app = new Joor(); * app.use("/", middleware1, middleware2); // Adds middleware1 and middleware2 to the route "/" and all its methods. * app.use("/api/*", middleware3); // Adds middleware3 to all routes starting with "/api/". * ``` - * - * @param {...(ROUTE_PATH | ROUTE_HANDLER)[]} data - An array of route paths and middleware functions. - * - * @remarks - * Middleware must accept a request object of type `JoorRequest` and must return `JoorResponse` to interrupt the request-response cycle. - * If the request needs to be processed further, the middleware must return `void`. + * @remarks Middleware functions should accept a request object (JoorRequest) and can optionally return a response object (JoorResponse) to interrupt the request-response cycle. If the request should continue to the next handler, the middleware should not return anything (void). */ public use(...data: Array): void { let paths: Array = []; - let middlewares: Array = []; + const middlewares: Array = []; - // Separate paths and middleware functions from the provided data for (const d of data) { if (typeof d === 'string') { - paths = [...paths, d]; + paths.push(d); } else if (typeof d === 'function') { - middlewares = [...middlewares, d]; + middlewares.push(d); } else { - logger.warn( - `Invalid data type "${typeof d}" passed to use method. Ignoring...` - ); + logger.warn(`Invalid data type "${typeof d}" passed to use method`); } } - // Add middlewares to the specified paths - // console.log(paths, middlewares); if (paths.length === 0) { paths = ['/']; } @@ -111,25 +168,21 @@ class Joor { addMiddlewares(p, middlewares); } } else { - logger.warn( - `Invalid data passed to use method. Ensure both paths and middlewares are provided. Ignoring...` - ); + logger.warn('Invalid data passed to use method'); } } /** - * Registers a new route to serve static files. + * Configures static file serving for a specific route path. + * @param {SERVE_FILES_CONFIG} config - Configuration object for serving static files. * @example * ```typescript - * const app = new Joor(); * app.serveFiles({ - * routePath: "/files", - * folderPath: path.join(__dirname, "public"), - * stream: true, - * download: false, + * routePath: '/static', + * folderPath: path.join(__dirname, 'public'), * }); + * ``` */ - public serveFiles({ routePath, folderPath, @@ -145,14 +198,25 @@ class Joor { } /** - * Initializes the configuration for the server. - * Loads configuration data asynchronously and assigns it to `configData`. - * - * @throws {Jrror} Throws a custom error if initialization fails. + * Initializes the server configuration. + * @returns A Promise that resolves when the configuration is loaded. + * @throws {Jrror} If configuration loading fails. */ private async initialize(): Promise { - const config = new Configuration(); - this.configData = await config.getConfig(); + try { + const config = new Configuration(); + this.configData = await config.getConfig(); + if (!this.configData) { + throw new Error('Configuration not loaded'); + } + } catch (error) { + throw new Jrror({ + code: 'initialization-failed', + message: `Failed to initialize: ${error}`, + type: 'panic', + docsPath: '/configuration', + }); + } } } diff --git a/src/core/joor/server.ts b/src/core/joor/server.ts index 36dc278..581ec98 100644 --- a/src/core/joor/server.ts +++ b/src/core/joor/server.ts @@ -5,84 +5,99 @@ import path from 'node:path'; import mime from 'mime-types'; +import JoorError from '../error/JoorError'; +import prepareResponse from '../response/prepare'; +import handleRoute from '../router/handle'; + import Configuration from '@/core/config'; import Jrror from '@/core/error'; -import JoorError from '@/core/error/JoorError'; -import prepareResponse from '@/core/response/prepare'; -import handleRoute from '@/core/router/handle'; import logger from '@/helpers/joorLogger'; +import JOOR_CONFIG from '@/types/config'; import { JoorRequest } from '@/types/request'; import { PREPARED_RESPONSE } from '@/types/response'; -/** - * Represents the server class responsible for starting the HTTP(S) server - * and processing incoming requests. - */ class Server { + public server: http.Server | https.Server = null as unknown as http.Server; + private configData: JOOR_CONFIG = null as unknown as JOOR_CONFIG; + private isInitialized = false; + /** - * Starts the server and listens on the configured port. - * Handles both HTTP and HTTPS based on the configuration. - * @returns {Promise} A promise that resolves when the server starts listening. - * @throws {Jrror} Throws an error if the configuration is not loaded or SSL files cannot be read. + * Initializes the server with SSL if configured, and sets up error handling. + * @returns {Promise} A promise that resolves when the server is initialized. + * @throws {Jrror} Throws an error if server initialization fails. */ - public async listen(): Promise { - const config = new Configuration(); - const configData = await config.getConfig(); - - if (!configData) { - throw new Jrror({ - code: 'config-not-loaded', - message: 'Configuration not loaded properly', - type: 'error', - docsPath: '/configuration', - }); - } + public async initialize(): Promise { + try { + if (this.isInitialized) { + return; + } - let server: http.Server | https.Server; + const config = new Configuration(); + this.configData = await config.getConfig(); + if (!this.configData) { + throw new Error('Configuration not loaded'); + } - if (configData.server?.ssl?.cert && configData.server?.ssl?.key) { - try { + if ( + this.configData.server?.ssl?.cert && + this.configData.server?.ssl?.key + ) { const credentials = { - key: fs.readFileSync(configData.server.ssl.key), - cert: fs.readFileSync(configData.server.ssl.cert), + key: await fs.promises.readFile(this.configData.server.ssl.key), + cert: await fs.promises.readFile(this.configData.server.ssl.cert), }; - server = https.createServer( + this.server = https.createServer( credentials, // eslint-disable-next-line @typescript-eslint/no-misused-promises async (req: JoorRequest, res: http.ServerResponse) => { await this.process(req, res); } ); - } catch (error: unknown) { + } else { + this.server = http.createServer( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (req: JoorRequest, res: http.ServerResponse) => { + await this.process(req, res); + } + ); + } + // Add server-level error handling + this.server.on('error', (error) => { + logger.error('Server error:', error); throw new Jrror({ - code: 'ssl-error', - message: `Failed to read SSL files.\n${error}`, + code: 'server-error', + message: `Server error: ${error}`, type: 'error', docsPath: '/joor-server', }); - } - } else { - server = http.createServer( - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (req: JoorRequest, res: http.ServerResponse) => { - await this.process(req, res); - } - ); + }); + this.isInitialized = true; + } catch (error) { + logger.error('Server initialization failed:', error); + throw new Jrror({ + code: 'server-initialization-failed', + message: `Failed to initialize server: ${error}`, + type: 'panic', + docsPath: '/joor-server', + }); } + } + public async listen(): Promise { try { - server.listen(configData.server.port, () => { + await this.initialize(); // Ensure initialization is complete before listening + this.server.listen(this.configData.server.port, () => { logger.info( - `Server listening on ${configData.server.ssl ? 'https' : 'http'}://${ - configData.server.host ?? 'localhost' - }:${configData.server.port}` + `Server listening on ${this.configData.server.ssl ? 'https' : 'http'}://${ + this.configData.server.host ?? 'localhost' + }:${this.configData.server.port}` ); }); } catch (error: unknown) { if ((error as Error).message.includes('EADDRINUSE')) { throw new Jrror({ code: 'server-port-in-use', - message: `Port ${configData.server.port} is already in use.`, + message: `Port ${this.configData.server.port} is already in use.`, type: 'error', docsPath: '/joor-server', }); @@ -152,8 +167,11 @@ class Server { await this.handleFileRequest(req, res, parsedResponse); } } catch (error: unknown) { - res.statusCode = 500; - res.end('Internal Server Error'); + if (!res.headersSent) { + res.statusCode = 500; + res.end('Internal Server Error'); + } + if (error instanceof Jrror || error instanceof JoorError) { error.handle(); } else { diff --git a/src/data/defaultConfig.ts b/src/data/defaultConfig.ts new file mode 100644 index 0000000..9b4dcc5 --- /dev/null +++ b/src/data/defaultConfig.ts @@ -0,0 +1,26 @@ +import JOOR_CONFIG from '@/types/config'; +const defaultConfig: JOOR_CONFIG = { + server: { + port: 8080, + host: 'localhost', + mode: 'http', + ssl: undefined, + }, + socket: undefined, + logger: { + enable: { + file: true, + console: true, + }, + maxFileSize: 10485760, // 10MB + }, + mode: 'development', + env: { + values: undefined, + defaults: { + enable: true, + }, + }, +}; + +export default defaultConfig; diff --git a/src/helpers/validateConfig.ts b/src/helpers/validateConfig.ts new file mode 100644 index 0000000..8b25769 --- /dev/null +++ b/src/helpers/validateConfig.ts @@ -0,0 +1,202 @@ +import defaultConfig from '@/data/defaultConfig'; +import logger from '@/helpers/joorLogger'; +import JOOR_CONFIG from '@/types/config'; + +interface ValidationRule { + isValid: (value: unknown) => value is T; + errorMessage: (_received: unknown) => string; +} + +const createValidator = + (rule: ValidationRule) => + (value: unknown, _path: string): value is T => { + if (!rule.isValid(value)) { + logger.warn(rule.errorMessage(value)); + return false; + } + + return true; + }; + +const validators = { + port: createValidator({ + isValid: (value): value is JOOR_CONFIG['server']['port'] => + typeof value === 'number' && value > 0 && value < 65536, + errorMessage: (received) => + `Invalid 'server.port': expected a number between 1-65535, received ${typeof received}. Using default: 8080`, + }), + + host: createValidator({ + isValid: (value): value is JOOR_CONFIG['server']['host'] => + typeof value === 'string' && value.length > 0, + errorMessage: (received) => + `Invalid 'server.host': expected non-empty string, received ${typeof received}. Using default: localhost`, + }), + + serverMode: createValidator({ + isValid: (value): value is 'tls' | 'http' => + typeof value === 'string' && ['tls', 'http'].includes(value), + errorMessage: (received) => + `Invalid 'server.mode': expected 'tls' or 'http', received ${received}. Using default: http`, + }), + + ssl: createValidator({ + isValid: (value): value is JOOR_CONFIG['server']['ssl'] => + typeof value === 'object' && + value !== null && + typeof (value as JOOR_CONFIG['server']['ssl'])?.key === 'string' && + typeof (value as JOOR_CONFIG['server']['ssl'])?.cert === 'string' && + (value as JOOR_CONFIG['server']['ssl'])?.key !== '' && + (value as JOOR_CONFIG['server']['ssl'])?.cert !== '', + errorMessage: () => + `Invalid 'server.ssl': expected object with 'key' and 'cert' strings. SSL disabled`, + }), + + socketOptions: createValidator({ + isValid: (value): value is object => + typeof value === 'object' && value !== null, + errorMessage: (received) => + `Invalid 'socket.options': expected object, received ${typeof received}`, + }), + + loggerEnable: createValidator<{ file: boolean; console: boolean }>({ + isValid: (value): value is { file: boolean; console: boolean } => + value !== undefined && + typeof value === 'object' && + value !== null && + typeof (value as { file: boolean; console: boolean }).file === + 'boolean' && + typeof (value as { file: boolean; console: boolean }).console === + 'boolean', + errorMessage: () => + `Invalid 'logger.enable': expected object with 'file' and 'console' booleans`, + }), + + maxFileSize: createValidator({ + isValid: (value): value is number => typeof value === 'number' && value > 0, + errorMessage: (received) => + `Invalid 'logger.maxFileSize': expected positive number, received ${typeof received}. Using default: 10MB`, + }), + + mode: createValidator({ + isValid: (value): value is JOOR_CONFIG['mode'] => + typeof value === 'string' && + ['development', 'production', 'testing', 'staging'].includes(value), + errorMessage: (received) => + `Invalid 'mode': expected 'development'/'production'/'testing'/'staging', received ${received}. Using default: development`, + }), + + envValues: createValidator>({ + isValid: (value): value is Record => { + if (typeof value !== 'object' || value === null) return false; + const entries = Object.entries(value); + if (entries.length === 0) return false; + + return entries.every(([, val]) => typeof val === 'string'); + }, + errorMessage: (received) => + `Invalid 'env.values': expected non-empty object with string values, received ${typeof received}`, + }), + + envDefaults: createValidator<{ enable?: boolean; file?: string }>({ + isValid: (value): value is { enable?: boolean; file?: string } => + typeof value === 'object' && + value !== null && + (!('enable' in value) || + typeof (value as { enable?: boolean }).enable === 'boolean') && + (!('file' in value) || + typeof (value as { file?: string }).file === 'string'), + errorMessage: () => + `Invalid 'env.defaults': expected object with optional 'enable' (boolean) and 'file' (string)`, + }), +}; + +const validateConfig = (config: Partial): JOOR_CONFIG => { + const validatedConfig = JSON.parse(JSON.stringify(defaultConfig)); + + // Server validation + if (config.server) { + if ( + config.server.port && + validators.port(config.server.port, 'server.port') + ) { + validatedConfig.server.port = config.server.port; + } + + if ( + config.server.host && + validators.host(config.server.host, 'server.host') + ) { + validatedConfig.server.host = config.server.host; + } + + if ( + config.server.mode && + validators.serverMode(config.server.mode, 'server.mode') + ) { + validatedConfig.server.mode = config.server.mode; + } + + if (config.server.ssl && validators.ssl(config.server.ssl, 'server.ssl')) { + validatedConfig.server.ssl = config.server.ssl; + } + } + + // Socket validation + if ( + config.socket?.options && + validators.socketOptions(config.socket.options, 'socket.options') + ) { + validatedConfig.socket = config.socket; + } + + // Logger validation + if (config.logger) { + let isLoggerValid = true; + + if ('enable' in config.logger) { + isLoggerValid = validators.loggerEnable( + config.logger.enable, + 'logger.enable' + ); + } + + if ('maxFileSize' in config.logger) { + isLoggerValid = validators.maxFileSize( + config.logger.maxFileSize, + 'logger.maxFileSize' + ); + } + + if (isLoggerValid) { + validatedConfig.logger = config.logger; + } + } + + // Mode validation + if (config.mode && validators.mode(config.mode, 'mode')) { + validatedConfig.mode = config.mode; + } + + // Environment validation + if (config.env) { + validatedConfig.env = { ...defaultConfig.env }; + if ( + config.env.values && + validators.envValues(config.env.values, 'env.values') + ) { + validatedConfig.env.values = config.env.values; + } + + if ( + config.env.defaults && + validators.envDefaults(config.env.defaults, 'env.defaults') + ) { + validatedConfig.env.defaults = config.env.defaults; + } + } + + return validatedConfig; +}; + +export default validateConfig; diff --git a/src/index.ts b/src/index.ts index 76bf51b..27cdc59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import Joor from '@/core/joor'; import JoorResponse from '@/core/response'; import Router from '@/core/router'; import { loadEnv, redirect, serveFile } from '@/enhanchers'; +import { httpLogger, cors, serveStaticFiles } from '@/middlewares'; import env from '@/packages/env'; import Logger from '@/packages/logger'; import marker from '@/packages/marker'; @@ -13,6 +14,7 @@ export default Joor; // export all other methods and functions except TYPES export { + Joor, Router, JoorResponse, loadEnv, @@ -21,6 +23,9 @@ export { marker, Logger, env, + httpLogger, + cors, + serveStaticFiles, }; // export types diff --git a/src/types/config.ts b/src/types/config.ts index 1193464..57c2da8 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,3 +1,4 @@ +import { ServerOptions } from 'socket.io'; interface JOOR_CONFIG { server: { port?: number; @@ -8,6 +9,24 @@ interface JOOR_CONFIG { cert: string; }; }; + socket?: { + options: ServerOptions; + }; + logger?: { + enable?: { + file: boolean; + console: boolean; + }; + maxFileSize: number; + }; + mode: 'development' | 'production' | 'testing' | 'staging'; + env?: { + values?: Record; + defaults?: { + enable?: boolean; + file?: string; + }; + }; } export default JOOR_CONFIG; diff --git a/src/types/joor.ts b/src/types/joor.ts index 363567f..25a8989 100644 --- a/src/types/joor.ts +++ b/src/types/joor.ts @@ -1,6 +1,11 @@ +import { Socket } from 'socket.io'; + import { ROUTE_HANDLER } from '@/types/route'; type GLOBAL_MIDDLEWARES = Array; + +type SOCKET_HANDLER = (_socket: Socket) => void; + interface SERVE_FILES { [key: string]: { folderPath: string; @@ -15,14 +20,19 @@ interface SERVE_FILES_CONFIG { download?: boolean; } -interface WEBSOCKET_CONFIG { - route: string; - handler: ROUTE_HANDLER; +interface SOCKET_CONFIG { + event: string; + handler: SOCKET_HANDLER; } +interface SOCKET { + [key: string]: SOCKET_HANDLER; +} export { GLOBAL_MIDDLEWARES, SERVE_FILES_CONFIG, SERVE_FILES, - WEBSOCKET_CONFIG, + SOCKET_CONFIG, + SOCKET_HANDLER, + SOCKET, }; diff --git a/tests/unit/core/joor/use.test.ts b/tests/unit/core/joor/use.test.ts index 374d4a3..e8d8a01 100644 --- a/tests/unit/core/joor/use.test.ts +++ b/tests/unit/core/joor/use.test.ts @@ -1,4 +1,4 @@ -import { jest, describe, it, expect } from '@jest/globals'; +// import { jest, describe, it, expect } from '@jest/globals'; import Joor from '@/core/joor'; import Router from '@/core/router'; diff --git a/tests/unit/helpers/validateConfig.test.ts b/tests/unit/helpers/validateConfig.test.ts new file mode 100644 index 0000000..81e559b --- /dev/null +++ b/tests/unit/helpers/validateConfig.test.ts @@ -0,0 +1,301 @@ +import logger from '@/helpers/joorLogger'; +import validateConfig from '@/helpers/validateConfig'; +// Mock the logger +jest.mock('@/helpers/joorLogger', () => ({ + warn: jest.fn(), +})); +describe('validateConfig', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('Server Configuration', () => { + it('accepts valid server configuration', () => { + const config = { + server: { + port: 3000, + host: 'example.com', + mode: 'http' as const, + ssl: { + key: 'key-content', + cert: 'cert-content', + }, + }, + }; + + const validated = validateConfig(config); + expect(validated.server).toEqual(config.server); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('validates port number range', () => { + const invalidPorts = [-1, 0, 65536, 70000]; + invalidPorts.forEach((port) => { + const config = { server: { port } }; + const validated = validateConfig(config); + expect(validated.server.port).toBe(8080); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'server.port'") + ); + }); + }); + it('validates host string', () => { + const invalidHosts = ['', null, undefined, 123]; + invalidHosts.forEach((host) => { + const config = { server: { host } as any }; + const validated = validateConfig(config); + expect(validated.server.host).toBe('localhost'); + if (host === 123) { + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'server.host'") + ); + } + }); + }); + it('validates server mode', () => { + const invalidModes = ['https', 'tcp', '', null, 123]; + invalidModes.forEach((mode) => { + const config = { server: { mode } as any }; + const validated = validateConfig(config); + expect(validated.server.mode).toBe('http'); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'server.mode'") + ); + }); + }); + it('validates SSL configuration', () => { + const invalidSSLConfigs = [ + { key: 123, cert: 'cert' }, + { key: 'key', cert: null }, + { key: '', cert: '' }, + { cert: 'cert' }, + { key: 'key' }, + ]; + invalidSSLConfigs.forEach((ssl) => { + const config = { server: { ssl } as any }; + const validated = validateConfig(config); + expect(validated.server.ssl).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'server.ssl'") + ); + }); + }); + }); + describe('Socket Configuration', () => { + it('accepts valid socket configuration', () => { + const config = { + socket: { + options: { + path: '/socket.io', + cors: { origin: '*' }, + }, + }, + }; + + const validated = validateConfig(config as any); + expect(validated.socket).toEqual(config.socket); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('validates socket options', () => { + const invalidOptions = [123, true]; + invalidOptions.forEach((options) => { + const config = { socket: { options } as any }; + const validated = validateConfig(config); + expect(validated.socket).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'socket.options'") + ); + }); + }); + }); + describe('Logger Configuration', () => { + it('accepts valid logger configuration', () => { + const config = { + logger: { + enable: { + file: true, + console: false, + }, + maxFileSize: 5242880, + }, + }; + const validated = validateConfig(config); + expect(validated.logger).toEqual(config.logger); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('validates logger enable flags', () => { + const invalidEnableConfigs = [ + { file: 'true', console: true }, + { file: true, console: 'false' }, + { file: true }, + { console: true }, + null, + undefined, + 123, + 'invalid', + ]; + invalidEnableConfigs.forEach((enable) => { + const config = { logger: { enable } as any }; + const validated = validateConfig(config); + expect(validated.logger?.enable).toEqual({ + file: true, + console: true, + }); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'logger.enable'") + ); + }); + }); + it('validates maxFileSize', () => { + const invalidSizes = [-1, 0, null, undefined, 'invalid', true]; + invalidSizes.forEach((maxFileSize) => { + const config = { logger: { maxFileSize } as any }; + const validated = validateConfig(config); + expect(validated.logger?.maxFileSize).toBe(10485760); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'logger.maxFileSize'") + ); + }); + }); + }); + describe('Mode Configuration', () => { + it('accepts valid modes', () => { + const validModes = [ + 'development', + 'production', + 'testing', + 'staging', + ] as const; + validModes.forEach((mode) => { + const config = { mode }; + const validated = validateConfig(config); + expect(validated.mode).toBe(mode); + expect(logger.warn).not.toHaveBeenCalled(); + }); + }); + it('validates mode value', () => { + const invalidModes = ['dev', 'prod', '', null, undefined, 123, true]; + invalidModes.forEach((mode) => { + const config = { mode } as any; + const validated = validateConfig(config); + expect(validated.mode).toBe('development'); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'mode'") + ); + }); + }); + }); + describe('Environment Configuration', () => { + it('accepts valid environment configuration', () => { + const config = { + env: { + values: { + NODE_ENV: 'development', + API_KEY: 'secret', + }, + defaults: { + enable: true, + file: '.env', + }, + }, + }; + + const validated = validateConfig(config); + expect(validated.env).toEqual(config.env); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('validates env values', () => { + const invalidValues = [{}, null, undefined, 123, true, '']; + invalidValues.forEach((values) => { + const config = { env: { values } as any }; + const validated = validateConfig(config); + expect(validated.env?.values).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'env.values'") + ); + }); + }); + it('validates env defaults', () => { + const invalidDefaults = [ + { enable: 'true' }, + { enable: 1 }, + { file: 123 }, + null, + undefined, + 123, + '', + ]; + invalidDefaults.forEach((defaults) => { + const config = { env: { defaults } as any }; + const validated = validateConfig(config); + expect(validated.env?.defaults).toEqual({ + enable: true, + }); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'env.defaults'") + ); + }); + }); + }); + describe('Default Values and Edge Cases', () => { + it('returns complete default configuration for empty input', () => { + const validated = validateConfig({}); + expect(validated).toEqual({ + server: { + port: 8080, + host: 'localhost', + mode: 'http', + ssl: undefined, + }, + socket: undefined, + logger: { + enable: { + file: true, + console: true, + }, + maxFileSize: 10485760, + }, + mode: 'development', + env: { + values: undefined, + defaults: { + enable: true, + }, + }, + }); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('handles null values', () => { + const config = { + server: null, + socket: null, + logger: null, + env: null, + } as any; + + const validated = validateConfig(config); + expect(validated).toEqual( + expect.objectContaining({ + server: { + port: 8080, + host: 'localhost', + mode: 'http', + ssl: undefined, + }, + }) + ); + }); + it('handles partial configuration', () => { + const config = { + server: { + port: 3000, + }, + mode: 'production' as const, + }; + + const validated = validateConfig(config); + expect(validated.server.port).toBe(3000); + expect(validated.server.host).toBe('localhost'); + expect(validated.mode).toBe('production'); + expect(logger.warn).not.toHaveBeenCalled(); + }); + }); +});