├── .gitignore ├── LICENSE ├── config.ts ├── global.d.ts ├── index.ts ├── package.json ├── readme.md ├── src ├── .engine ├── Classes │ └── ControllerClass.ts ├── Console │ ├── Commands.ts │ ├── JobHelper.ts │ └── ProcessManager.js ├── ControllerEngine.ts ├── Controllers │ ├── ControllerService.ts │ ├── ControllerServiceError.ts │ └── ProcessServices.ts ├── ErrorEngine.ts ├── Errors │ ├── Deprecated.ts │ └── InXpresserError.ts ├── Events │ ├── Emitter.ts │ ├── Loader.ts │ └── OnEventsLoader.ts ├── Extensions │ ├── Helpers.ts │ ├── If.ts │ ├── Loggers.ts │ └── Path.ts ├── Factory │ ├── controller.hbs │ ├── controller.ts.hbs │ ├── controller_object.hbs │ ├── controller_object.ts.hbs │ ├── controller_service.hbs │ ├── controller_with_services.hbs │ ├── controller_with_services.ts.hbs │ ├── event.hbs │ ├── event.ts.hbs │ ├── job.hbs │ ├── job.ts.hbs │ ├── middleware.hbs │ ├── middleware.ts.hbs │ ├── model.hbs │ └── types │ │ ├── index.d.ts │ │ ├── modules.d.ts │ │ └── xpresser.d.ts ├── FileEngine.ts ├── Functions │ ├── artisan.fn.ts │ ├── inbuilt.fn.ts │ ├── internals.fn.ts │ ├── modules.fn.ts │ ├── plugins.fn.ts │ ├── request.fn.ts │ ├── router.fn.ts │ └── util.fn.ts ├── Helpers │ ├── Base64.ts │ ├── Conditions.ts │ ├── Path.ts │ └── String.ts ├── MiddlewareEngine.ts ├── Objects │ └── consoleColors.obj.ts ├── On.ts ├── PluginEngine.ts ├── Plugins │ ├── ExtendedRequestEngine.ts │ └── Installer.ts ├── RequestEngine.ts ├── RouterEngine.ts ├── Routes │ └── Loader.ts ├── StartConsole.ts ├── StartHttp.ts ├── UseEngine.ts ├── XpresserRepl.ts ├── backend │ └── views │ │ └── __errors │ │ ├── bootstrap.ejs │ │ └── index.ejs ├── global.ts └── types.ts ├── tests ├── backend │ ├── MyRequestEngine.ts │ ├── controllers │ │ └── AppController.ts │ ├── middlewares │ │ └── TestMiddleware.ts │ ├── plugins.json │ └── use.json ├── server.ts ├── specs │ └── index.spec.ts └── test-plugin │ ├── plugin-index.ts │ └── use.json ├── truth.ts ├── tsconfig.json ├── types ├── helpers.d.ts ├── http.d.ts ├── index.d.ts ├── modules.d.ts └── node.d.ts ├── xpresser-logo-black.png └── xpresser-logo-white.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .env 4 | dist 5 | storage 6 | .sqlite 7 | yarn.lock 8 | /test.js 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 xpresserjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | const Config = { 2 | // App Name 3 | name: "Xpresser", 4 | 5 | // App ENV, should be equivalent to NODE_ENV 6 | env: "development", 7 | 8 | // Enable Debugging 9 | debug: { 10 | // If set to false all debugging && debug logs are disabled 11 | // While if set to true all debug settings are set to their configuration values. 12 | enabled: true, 13 | 14 | // Enable showing controller action on every request. 15 | requests: { 16 | // Enable Request Debugging 17 | enabled: true, 18 | 19 | // Enable color in logs 20 | colored: true, 21 | 22 | // Show all request log data 23 | showAll: true, 24 | 25 | // Items to show in the request debug log 26 | show: { 27 | time: false, 28 | statusCode: true, 29 | statusMessage: false 30 | }, 31 | 32 | // Ignore specific urls 33 | ignore: [] 34 | }, 35 | 36 | // Deprecated warnings 37 | deprecationWarnings: { 38 | enabled: true, 39 | showStack: false 40 | }, 41 | }, 42 | 43 | // Log Configurations 44 | log: { 45 | // Log enabled Plugins on boot 46 | plugins: true, 47 | serverDomainAndPort: false, 48 | }, 49 | 50 | /** 51 | * Project configurations 52 | * Its safe to store project related settings here. 53 | */ 54 | project: { 55 | fileExtension: ".js", 56 | }, 57 | 58 | // Server Configuration 59 | server: { 60 | 61 | /** 62 | * Middleware to handle server under maintenance mood 63 | * 64 | * if not found default is used. 65 | */ 66 | maintenanceMiddleware: "MaintenanceMiddleware.js", 67 | /** 68 | * Server Port for http connections 69 | */ 70 | port: 2000, 71 | 72 | /** 73 | * Url protocol (http|https) 74 | * Use https if ssl is enabled. 75 | */ 76 | protocol: "http", 77 | 78 | /** 79 | * Server domain 80 | */ 81 | domain: "localhost", 82 | 83 | /** 84 | * Root Folder 85 | * if calling xpresser from another folder not route 86 | * specify e.g root: '/folder/' 87 | * 88 | * must end with trailing slash 89 | */ 90 | root: "/", 91 | 92 | /** 93 | * In most development environment this is required to be true. 94 | * When true url helpers will append server port after server url 95 | * 96 | * @example 97 | * http://localhost:2000/some/path 98 | */ 99 | includePortInUrl: true, 100 | 101 | /** 102 | * Specify Application BaseUrl directly 103 | */ 104 | baseUrl: "", 105 | 106 | /** 107 | * SSL Configurations. 108 | */ 109 | ssl: { 110 | // Enable ssl 111 | enabled: false, 112 | 113 | // Ssl Port 114 | port: 443, 115 | }, 116 | 117 | /** 118 | * Enable or disable PoweredBy 119 | * For security purposes this is advised to be false. 120 | */ 121 | poweredBy: true, 122 | 123 | /** 124 | * Enable if you want public folder to be served 125 | */ 126 | servePublicFolder: true, 127 | 128 | /** 129 | * Xpresser comes with a few packages for security, 130 | * You can enable or disable them here. 131 | * ['bodyParser', 'flash' 'helmet'] 132 | */ 133 | use: { 134 | // Use BodyParser, 135 | bodyParser: true, 136 | // Enable Flash 137 | flash: false, 138 | }, 139 | 140 | requestEngine: { 141 | dataKey: 'data', 142 | proceedKey: 'proceed', 143 | messageKey: '_say' 144 | }, 145 | 146 | /** 147 | * Xpresser Router Config 148 | */ 149 | router: { 150 | pathCase: "snake" // snake or kebab 151 | }, 152 | }, 153 | 154 | // Date Configurations 155 | date: { 156 | timezone: null, 157 | format: "YYYY-MM-DD H:mm:ss", 158 | }, 159 | 160 | // Paths Configurations 161 | paths: { 162 | base: __dirname, 163 | // Should be relative to the base set above. 164 | // e.g base+'/'+backend should resolve to /full/path/base/backend 165 | backend: "base://backend", 166 | 167 | // Must be relative to base 168 | frontend: "frontend", 169 | public: "public", 170 | storage: "storage", 171 | xjs: "xjs", 172 | // Npm Dir 173 | npm: "base://node_modules", 174 | 175 | // Other Paths 176 | routesFile: "backend://routes.js", 177 | events: "backend://events", 178 | controllers: "backend://controllers", 179 | models: "backend://models", 180 | middlewares: "backend://middlewares", 181 | views: "backend://views", 182 | jsonConfigs: "backend://", 183 | configs: "backend://configs" 184 | }, 185 | 186 | // Template Configurations 187 | template: { 188 | use: false, 189 | engine: "ejs", 190 | extension: "ejs", 191 | 192 | locals: { 193 | all: true, 194 | query: false, 195 | body: false, 196 | stackedScripts: false, 197 | }, 198 | }, 199 | 200 | // Response Configurations 201 | response: { 202 | cacheFiles: false, 203 | cacheFileExtensions: ["js", "css"], 204 | cacheIfMatch: [], 205 | cacheMaxAge: 31536000, 206 | overrideServerName: true, 207 | }, 208 | 209 | // Artisan/Console Configurations 210 | artisan: { 211 | loadEvents: false, 212 | singleModelName: true, 213 | pluralizeModelTable: true, 214 | 215 | // Replace factory files 216 | factory: { 217 | // model: null, 218 | // controller: null, 219 | // view: null 220 | } 221 | }, 222 | 223 | // Modules Configurations 224 | packages: {}, 225 | 226 | // Plugins Configurations 227 | plugins: {}, 228 | }; 229 | 230 | const Options = { 231 | requireOnly: false, 232 | autoBoot: false, 233 | isConsole: false, 234 | isTinker: false, 235 | exposeDollarSign: true, 236 | instanceId: undefined, 237 | isFromXjsCli: false, 238 | }; 239 | 240 | export = {Config, Options}; 241 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | 7 | import ControllerClass from "./src/Classes/ControllerClass"; 8 | import XpresserRepl from "./src/XpresserRepl"; 9 | import {DollarSign, Options} from "./types"; 10 | import XpresserRouter from "@xpresser/router"; 11 | import InXpresserError from "./src/Errors/InXpresserError"; 12 | 13 | /** 14 | * Initialize Xpresser 15 | * @param config - Config object or path to config file. 16 | * @param options - Options 17 | * @constructor 18 | */ 19 | declare function init(config: object | string, options?: Options): DollarSign; 20 | 21 | /** 22 | * Get Current Xpresser Instance. 23 | * Use instead of global $ 24 | * @example 25 | * const $ = global['$']; 26 | * const $ = getInstance(); 27 | * @param [instanceId] 28 | */ 29 | declare function getInstance(instanceId?: string): DollarSign; 30 | 31 | /** 32 | * Get Xpresser Instance Router. 33 | * @param instanceId 34 | * @returns {XpresserRouter} 35 | */ 36 | declare function getInstanceRouter(instanceId?: string): XpresserRouter; 37 | 38 | export {init, getInstance, getInstanceRouter, ControllerClass, XpresserRepl, InXpresserError} 39 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import {DollarSign, Options} from "./types"; 2 | import ControllerClass from "./src/Classes/ControllerClass"; 3 | import XpresserRepl from "./src/XpresserRepl"; 4 | import XpresserRouter from "@xpresser/router"; 5 | import InXpresserError from "./src/Errors/InXpresserError"; 6 | 7 | // Xpresser Instance Holder 8 | const instanceHolder: Record = {}; 9 | 10 | 11 | /** 12 | * Get Xpresser Instance. 13 | * 14 | * if instanceId is not defined the first getInstance will be returned; 15 | * @param instanceId 16 | */ 17 | function getInstance(instanceId?: string): DollarSign { 18 | if (instanceId) { 19 | if (instanceId === ':keys') return Object.keys(instanceHolder) as any; 20 | if (instanceId === ':id') return require('./truth')?.instanceId; 21 | 22 | if (!instanceHolder.hasOwnProperty(instanceId)) 23 | throw new InXpresserError(`Xpresser instanceId: ${instanceId} not found!`); 24 | 25 | return instanceHolder[instanceId]; 26 | } else { 27 | 28 | // If $ is defined then return. 29 | if (global.hasOwnProperty('$')) { 30 | // @ts-ignore 31 | return global.$; 32 | } 33 | 34 | const truth = require("./truth"); 35 | 36 | if (truth && truth.instanceId) { 37 | return instanceHolder[truth.instanceId]; 38 | } 39 | 40 | const instances = Object.keys(instanceHolder); 41 | if (!instances.length) 42 | throw new InXpresserError(`No Xpresser instance defined found!`); 43 | 44 | return instanceHolder[instances[0]]; 45 | } 46 | } 47 | 48 | 49 | /** 50 | * Get Xpresser Instance Router. 51 | * @param instanceId 52 | * @returns {XpresserRouter} 53 | */ 54 | function getInstanceRouter(instanceId?: string): XpresserRouter { 55 | return getInstance(instanceId).router; 56 | } 57 | 58 | 59 | /** 60 | * Initialize Xpresser; 61 | * @param AppConfig 62 | * @param {Options} AppOptions 63 | * @constructor 64 | */ 65 | function init(AppConfig: Record | string, AppOptions: Options = {}): DollarSign { 66 | // Expose xpresserInstance as global function 67 | // @ts-ignore 68 | if (!global.xpresserInstance) global.xpresserInstance = getInstance; 69 | // @ts-ignore 70 | if (!global.InXpresserError) global.InXpresserError = InXpresserError; 71 | 72 | /** 73 | * Require Modules only when this function is called. 74 | * This is to avoid requiring un-needed packages when ever we run 75 | * const {getInstance} = require('xpresser') 76 | */ 77 | const fs = require("fs"); 78 | const chalk = require("chalk"); 79 | const XpresserRouter = require("@xpresser/router"); 80 | // Import default config. 81 | const {Config, Options} = require("./config"); 82 | const ObjectCollection = require("object-collection"); 83 | const {randomStr} = require('./src/Functions/inbuilt.fn') 84 | const lodash = require("lodash"); 85 | const truth = require("./truth"); 86 | 87 | 88 | /** 89 | * Importing Package.json 90 | * 91 | * Since typescript is bundled in `dist` folder 92 | * package.json will be in the parent directory 93 | */ 94 | let PackageDotJson: any = {}; 95 | 96 | try { 97 | PackageDotJson = require("./package.json"); 98 | } catch (e) { 99 | PackageDotJson = require("../package.json"); 100 | } 101 | 102 | if (AppConfig === undefined) { 103 | AppConfig = {}; 104 | } 105 | 106 | if (AppOptions === undefined) { 107 | AppOptions = {}; 108 | } 109 | 110 | // Set Instance id to random string if not defined 111 | if (!AppOptions['instanceId']) truth.instanceId = AppOptions['instanceId'] = randomStr(10); 112 | 113 | // Set DollarSign Global Var: $ 114 | const $ = instanceHolder[AppOptions['instanceId'] as string] = {} as DollarSign; 115 | 116 | $.exit = (...args) => { 117 | return process.exit(...args); 118 | }; 119 | 120 | if (typeof AppConfig === "string") { 121 | const configFile = AppConfig; 122 | AppConfig = {} as Record; 123 | 124 | if (fs.existsSync(configFile)) { 125 | 126 | try { 127 | AppConfig = require(configFile); 128 | // tslint:disable-next-line:max-line-length 129 | if (typeof AppConfig !== "object" || (typeof AppConfig === "object" && !Object.keys(AppConfig).length)) { 130 | // noinspection ExceptionCaughtLocallyJS 131 | throw new InXpresserError(`CONFIG: No exported object found in config file: (${configFile})`); 132 | } 133 | } catch (e) { 134 | console.error((e as InXpresserError).stack); 135 | $.exit(); 136 | } 137 | 138 | } else { 139 | 140 | console.error("Config file not found!"); 141 | $.exit(); 142 | 143 | } 144 | } 145 | 146 | AppConfig = AppConfig as Record; 147 | /** 148 | * Check if config {paths.base} exists in user defined config. 149 | */ 150 | const noBaseFolderDefinedError = `No base folder defined in config {paths.base}`; 151 | if (!AppConfig.hasOwnProperty('paths')) { 152 | console.log(noBaseFolderDefinedError); 153 | $.exit() 154 | } else { 155 | // @ts-ignore 156 | if (!AppConfig['paths'].hasOwnProperty('base')) { 157 | console.log(noBaseFolderDefinedError); 158 | $.exit() 159 | } 160 | } 161 | 162 | /** 163 | * Check if env exist in config 164 | */ 165 | if (!AppConfig.hasOwnProperty('env') || !AppConfig.env) { 166 | console.log(`Config {env} is missing, options: (development | production | others)`) 167 | $.exit(); 168 | } 169 | 170 | if (typeof AppConfig.env !== "string") { 171 | console.log(`Config {env} must be of type string!`) 172 | $.exit(); 173 | } 174 | 175 | // Merge Config with DefaultConfig to replace missing values. 176 | AppConfig = lodash.merge(lodash.clone(Config), AppConfig) as Record; 177 | AppOptions = lodash.merge(lodash.clone(Options), AppOptions) as Options; 178 | 179 | 180 | // Initialize {$.on} for the first time. 181 | // @ts-ignore 182 | $.on = {}; 183 | 184 | // Set {$.objectCollection} 185 | if (typeof AppConfig['ObjectCollection'] === "function") { 186 | const OwnObjectCollection: any = AppConfig.ObjectCollection() 187 | $.objectCollection = (obj?) => new OwnObjectCollection(obj) as typeof ObjectCollection; 188 | } else { 189 | $.objectCollection = (obj?) => new ObjectCollection(obj); 190 | } 191 | 192 | 193 | // Expose {$}(DollarSign) to globals. 194 | // @ts-ignore 195 | if (AppOptions.exposeDollarSign) global.$ = $; 196 | 197 | /** 198 | * Get Xjs Cli Config 199 | */ 200 | const CliConfig: any = (global as any)["XjsCliConfig"]; 201 | 202 | /** 203 | * Set Config to object-collection of AppConfig 204 | */ 205 | $.config = $.objectCollection(AppConfig as object); 206 | 207 | /** 208 | * Set $.options 209 | * @type Options 210 | */ 211 | $.options = AppOptions; 212 | 213 | /** 214 | * Engine Data serves as the store 215 | * for all data stored by Xpresser files/components 216 | */ 217 | $.engineData = $.objectCollection({}); 218 | 219 | /** 220 | * Store serves as the store 221 | * for the application 222 | */ 223 | $.store = $.objectCollection({}); 224 | 225 | const LaunchType = process.argv[2]; 226 | $.engineData.set("LaunchType", LaunchType); 227 | 228 | if (typeof CliConfig !== "undefined" || LaunchType === "cli") { 229 | $.options.isConsole = true; 230 | } 231 | 232 | // Set $.isTypeScript 233 | $.isTypescript = () => { 234 | return $.config.get('project.fileExtension') === ".ts"; 235 | } 236 | 237 | 238 | $.isNativeCliCommand = () => { 239 | return LaunchType === "cli" && 240 | (process.argv[3] && 241 | process.argv[3].slice(0, 5) === "make:") as boolean 242 | } 243 | 244 | 245 | // Set Engine Path 246 | const enginePath = $.config.get('paths.engine'); 247 | if (!enginePath) { 248 | let dirName = __dirname; 249 | 250 | /** 251 | * Check if xpresser dist folder if being used 252 | * "\\ is for windows" 253 | */ 254 | if (["/dist", "\\dist"].includes(dirName.slice(-5))) { 255 | dirName = dirName.slice(0, dirName.length - 5) 256 | } 257 | 258 | $.config.set('paths.engine', `${dirName}/src/`); 259 | } 260 | 261 | /* ------------- $.on Events Loader ------------- */ 262 | require("./src/On"); 263 | // Require $.file 264 | require("./src/FileEngine"); 265 | // Include Loggers 266 | require("./src/Extensions/Loggers"); 267 | // Include If extensions 268 | require("./src/Extensions/If"); 269 | 270 | // Log if not console 271 | $.ifNotConsole(() => { 272 | $.logCalmly(`${PackageDotJson.name} version ${PackageDotJson.version}`); 273 | let {name, env}: { name: string, env: string } = $.config.all(); 274 | if (env) { 275 | env = lodash.startCase(env); 276 | env = env.toLowerCase() === "development" ? chalk.yellow(`(${env})`) : chalk.greenBright(`(${env})`); 277 | env = chalk.yellow(env); 278 | } 279 | $.log(`${name} ${env}`.trim()); 280 | }); 281 | 282 | /** 283 | * Change timezone if timezone is defined. 284 | */ 285 | const timezone: string | undefined = $.config.get('date.timezone'); 286 | if (timezone) { 287 | process.env.TZ = timezone; 288 | } 289 | 290 | // Include PathHelper Extensions 291 | require("./src/Extensions/Path"); 292 | 293 | // Require Global 294 | require("./src/global"); 295 | 296 | // Get OnEvents Loader. 297 | const {runBootEvent} = require("./src/Events/OnEventsLoader"); 298 | 299 | async function afterStartEvents() { 300 | // Require Plugin Engine and load plugins 301 | const PluginEngine = require("./src/PluginEngine"); 302 | const PluginData = await PluginEngine.loadPlugins(PackageDotJson); 303 | 304 | $.engineData.set("PluginEngineData", PluginData); 305 | 306 | /** 307 | * Load `use.json` 308 | * This is after plugins have loaded 309 | */ 310 | const useDotJson = $.objectCollection(); 311 | const useDotJsonPath = $.path.jsonConfigs("use.json"); 312 | 313 | if ($.file.exists(useDotJsonPath)) { 314 | // Import Use.json 315 | useDotJson.merge(require(useDotJsonPath)); 316 | // Save to EngineData 317 | $.engineData.set("UseDotJson", useDotJson); 318 | } 319 | 320 | /** 321 | * @type {UseEngine} 322 | */ 323 | $.use = require("./src/UseEngine"); 324 | 325 | /** 326 | * Add Router 327 | * @type {XpresserRouter} 328 | */ 329 | $.router = new XpresserRouter(undefined, () => $); 330 | 331 | $.ifNotConsole(() => { 332 | /** 333 | * Load Registered Events 334 | */ 335 | require("./src/Events/Loader"); 336 | }); 337 | } 338 | 339 | $.boot = () => { 340 | // Prevents `$.boot()` booting twice 341 | if ($.engineData.has('hasBooted')) 342 | return false; 343 | 344 | // Set HasBooted in engine data 345 | $.engineData.set('hasBooted', true); 346 | 347 | /** 348 | * Load on.start Events 349 | */ 350 | return runBootEvent("start", () => { 351 | afterStartEvents().then(() => { 352 | /** 353 | * Load on.boot Events 354 | */ 355 | runBootEvent("boot", () => { 356 | /** 357 | * AppOptions.require_only 358 | * This config is only used by xpresser cron. 359 | * Used to load your main file without booting it 360 | */ 361 | if (!$.options.requireOnly) { 362 | $.ifConsole(() => { 363 | require("./src/StartConsole"); 364 | }, () => { 365 | require("./src/StartHttp"); 366 | }); 367 | } 368 | }); 369 | }).catch(e => { 370 | $.logError(e); 371 | $.logErrorAndExit('Error in $.on.boot events'); 372 | }) 373 | }); 374 | }; 375 | 376 | /** 377 | * Boot if $.options.autoBoot is true. 378 | */ 379 | if ($.options.autoBoot === true) { 380 | $.boot(); 381 | } 382 | 383 | return $; 384 | } 385 | 386 | export {init, getInstance, getInstanceRouter, ControllerClass, XpresserRepl, InXpresserError} 387 | 388 | 389 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xpresser", 3 | "version": "0.32.7", 4 | "description": "Nodejs Framework for building scalable apps.", 5 | "main": "dist/index.js", 6 | "types": "global.d.ts", 7 | "scripts": { 8 | "prepublishOnly": "npx tsc", 9 | "watch": "npx tsc --watch", 10 | "build": "npx tsc", 11 | "test": "npx tsx watch tests/server.ts", 12 | "test:dev": "npx nodemon dist/tests/server.js", 13 | "test:specs": "node --import tsx --test tests/specs/*.spec.ts" 14 | }, 15 | "repository": "https://github.com/xpresserjs/framework.git", 16 | "homepage": "https://xpresserjs.com", 17 | "author": "xpresserjs", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@types/express": "^4.17.21", 21 | "@types/fs-extra": "^11.0.4", 22 | "@xpresser/router": "^2.0.1", 23 | "body-parser": "^1.20.3", 24 | "build-url-ts": "^6.1.8", 25 | "chalk": "^4.1.2", 26 | "cors": "^2.8.5", 27 | "ejs": "^3.1.10", 28 | "express": "^4.21.0", 29 | "express-flash": "^0.0.2", 30 | "fs-extra": "^11.2.0", 31 | "handlebars": "^4.7.8", 32 | "has-pkg": "^0.0.1", 33 | "helmet": "^7.1.0", 34 | "lodash": "^4.17.21", 35 | "moment": "^2.30.1", 36 | "moment-timezone": "^0.5.45", 37 | "object-collection": "^3.0.1", 38 | "path-to-regexp": "^6.2.2", 39 | "pluralize": "^8.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/lodash": "^4.17.7", 43 | "@types/node": "^22.5.5", 44 | "@types/pluralize": "^0.0.33", 45 | "tsx": "^4.19.1", 46 | "typescript": "^5.6.2" 47 | }, 48 | "engines": { 49 | "node": ">=12" 50 | }, 51 | "keywords": [ 52 | "xpresser", 53 | "xpresserjs", 54 | "xjs", 55 | "xjs-cli", 56 | "framework", 57 | "expressjs" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Xpresser NodeJs Framework 2 | 3 | ![Alt text](https://cdn.jsdelivr.net/npm/xpresser/xpresser-logo-black.png "Xpresser Logo") 4 | 5 | A powerful framework for nodejs, Using `expressjs` as a server and also other community proven worthy libraries. 6 | 7 | Full Documentation [xpresserjs.com](https://xpresserjs.com) 8 | -------------------------------------------------------------------------------- /src/.engine: -------------------------------------------------------------------------------- 1 | ------ This file is used to check if xpresser engine is detected. ----- -------------------------------------------------------------------------------- /src/Classes/ControllerClass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Controller 3 | * @class 4 | */ 5 | class ControllerClass { 6 | /** 7 | * Helper to detect if a variable inherits this controller. 8 | * @return {boolean} 9 | */ 10 | static get extendsMainController(): boolean { 11 | return true; 12 | } 13 | } 14 | 15 | export = ControllerClass; 16 | -------------------------------------------------------------------------------- /src/Console/Commands.ts: -------------------------------------------------------------------------------- 1 | import {getInstance} from "../../index"; 2 | import {parseControllerString} from "../Functions/internals.fn"; 3 | import lodash from "lodash"; 4 | import os from "os"; 5 | import fs from "fs"; 6 | import fse from "fs-extra"; 7 | import Artisan from "../Functions/artisan.fn"; 8 | import colors from "../Objects/consoleColors.obj"; 9 | import PathHelper from "../Helpers/Path"; 10 | 11 | const $ = getInstance(); 12 | const artisanConfig: any = $.config.get('artisan', {}); 13 | const {logThis, logThisAndExit} = Artisan; 14 | 15 | /** 16 | * Remove slash at the end of str passed 17 | * @param str 18 | */ 19 | const removeSlashAtEnd = (str: string) => { 20 | if (str.slice(-1) === "/") { 21 | return str.slice(0, str.length - 1); 22 | } 23 | 24 | return str; 25 | }; 26 | 27 | /** 28 | * PathHelper to maintenance file 29 | */ 30 | const maintenanceFile = $.path.base('.maintenance'); 31 | 32 | 33 | const Commands = { 34 | /** 35 | * Enable Maintenance Mood 36 | */ 37 | up() { 38 | if (!fs.existsSync(maintenanceFile)) { 39 | return $.logAndExit('App is already up') 40 | } 41 | 42 | fs.unlinkSync(maintenanceFile); 43 | 44 | $.log('App is now up!'); 45 | $.logAndExit('Reload your server if you have an active getInstance already running.') 46 | }, 47 | 48 | /** 49 | * Disabled Maintenance Mood 50 | */ 51 | down() { 52 | if (fs.existsSync(maintenanceFile)) { 53 | return $.logAndExit('App is already down.') 54 | } 55 | 56 | fs.writeFileSync(maintenanceFile, JSON.stringify({ 57 | date: (new Date()).toUTCString() 58 | })); 59 | 60 | $.log('App is now in maintenance mood!'); 61 | $.logAndExit('Reload your server if you have an active getInstance already running.') 62 | 63 | }, 64 | 65 | /** 66 | * @deprecated 67 | * @param $plugin 68 | */ 69 | install([$plugin]: string[]) { 70 | if ($plugin === "undefined") { 71 | return logThis("Plugin not specified!"); 72 | } 73 | 74 | const PluginInstaller = require("../Plugins/Installer"); 75 | PluginInstaller($plugin); 76 | }, 77 | 78 | /** 79 | * List routes in this project 80 | * @param search 81 | * @param query 82 | */ 83 | routes([search, query]: string[]) { 84 | if (search && !query) { 85 | query = search; 86 | search = 'path'; 87 | } 88 | 89 | if (search === 'ctrl') search = 'controller'; 90 | if (search === 'method' && query) query = query.toUpperCase(); 91 | 92 | let data: any[] = []; 93 | 94 | data = $.routerEngine.allProcessedRoutes(); 95 | data.map((e) => { 96 | e.method = e.method.toUpperCase(); 97 | 98 | if (typeof e.controller === "string") { 99 | const {controller, method} = parseControllerString(e.controller); 100 | e.controller = `${controller}.${method}`; 101 | } 102 | 103 | if (typeof e.controller === "function") { 104 | if (e.controller.name) { 105 | e.controller = e.controller.name; 106 | if (e.controller.indexOf("getFile(") !== 0) { 107 | e.controller += '()' 108 | } else { 109 | e.controller = e.controller.replace($.path.base(), '') 110 | } 111 | } else { 112 | e.controller = "anonymous()" 113 | } 114 | } 115 | 116 | if (!e.controller) e.controller = ''; 117 | 118 | if (!e.name) e.name = null; 119 | 120 | // Hide Url since path is showing already 121 | delete e.url; 122 | 123 | }) 124 | 125 | const searchResults: any = []; 126 | if (search) { 127 | for (const route of data) { 128 | if (!route.hasOwnProperty(search)) { 129 | console.log(colors.fgRed, `Routes table has no column named: (${search}), ACCEPTED: (method | path | controller | name)`) 130 | return $.exit() 131 | } 132 | 133 | if (route[search] && route[search].includes(query)) { 134 | searchResults.push(route) 135 | } 136 | } 137 | } 138 | 139 | data = search ? searchResults : data 140 | let message = `Total Routes: ${data.length}`; 141 | if (search) message = `Found Routes: ${data.length} WHERE {column: ${search}, query: ${query}}`; 142 | 143 | 144 | if (data.length) { 145 | console.log(colors.fgCyan); 146 | console.table(data); 147 | } 148 | console.log(colors.fgYellow, message) 149 | console.log() 150 | 151 | }, 152 | 153 | /** 154 | * Make Job 155 | * @param args 156 | */ 157 | "make:job"(args: string[]) { 158 | const job = args[0]; 159 | let command = args[1]; 160 | 161 | if (typeof job === "undefined") { 162 | return logThis("Job name not defined!"); 163 | } 164 | 165 | if (typeof command === "undefined") { 166 | command = job.trim(); 167 | } 168 | 169 | const jobsPath = $.path.backend("jobs"); 170 | Artisan.copyFromFactoryToApp("job", job, jobsPath, {name: job, command}, false); 171 | 172 | return $.exit(); 173 | }, 174 | 175 | /** 176 | * Generate Events File. 177 | * @param args 178 | */ 179 | "make:event"(args: string[]) { 180 | const name = args[0]; 181 | const namespace = args[1]; 182 | 183 | const eventsPath = $.path.events(); 184 | Artisan.copyFromFactoryToApp("event", name, eventsPath, {name, namespace}, false); 185 | 186 | return $.exit(); 187 | }, 188 | 189 | /** 190 | * Generate Controller file. 191 | * @param args 192 | */ 193 | "make:controller"(args: string[]) { 194 | const controller = args[0]; 195 | 196 | if (typeof controller === "undefined") { 197 | return logThis("Controller name not defined!"); 198 | } 199 | 200 | const controllersPath = removeSlashAtEnd($.path.controllers()); 201 | Artisan.copyFromFactoryToApp("controller", controller, controllersPath); 202 | 203 | return $.exit(); 204 | }, 205 | 206 | /** 207 | * Generate Controller Object 208 | * @param args 209 | */ 210 | "make:controller_object"(args: string[]) { 211 | const controller = args[0]; 212 | if (typeof controller === "undefined") { 213 | return logThis("Controller name not defined!"); 214 | } 215 | 216 | const controllersPath = removeSlashAtEnd($.path.controllers()); 217 | Artisan.copyFromFactoryToApp(["controller", "controller_object"], controller, controllersPath); 218 | 219 | return $.exit(); 220 | }, 221 | 222 | /** 223 | * Generate Controller with Services 224 | * @param args 225 | */ 226 | "make:controller_services"(args: string[]) { 227 | const controller = args[0]; 228 | if (typeof controller === "undefined") { 229 | return logThis("Controller name not defined!"); 230 | } 231 | 232 | const controllersPath = removeSlashAtEnd($.path.controllers()); 233 | Artisan.copyFromFactoryToApp(["controller", "controller_with_services"], controller, controllersPath); 234 | 235 | return $.exit(); 236 | }, 237 | 238 | /** 239 | * Generate Controller Service. 240 | * @param args 241 | */ 242 | "make:controllerService"(args: string[]) { 243 | const service = args[0]; 244 | 245 | if (typeof service === "undefined") { 246 | return logThis("Service name not defined!"); 247 | } 248 | 249 | const controllersPath = removeSlashAtEnd($.path.controllers("services")); 250 | Artisan.copyFromFactoryToApp(["CService", "controller_service"], service, controllersPath); 251 | 252 | return $.exit(); 253 | }, 254 | 255 | /** 256 | * Generate Middleware file. 257 | * @param args 258 | */ 259 | "make:middleware"(args: string[]) { 260 | const middleware = args[0]; 261 | if (typeof middleware === "undefined") { 262 | return logThis("Middleware name not defined!"); 263 | } 264 | 265 | const middlewaresPath = PathHelper.resolve($.config.get('paths.middlewares')); 266 | Artisan.copyFromFactoryToApp("middleware", middleware, middlewaresPath); 267 | 268 | return $.exit(); 269 | }, 270 | 271 | 272 | /** 273 | * Make Model File. 274 | * @param args 275 | */ 276 | "make:model"(args: string[]) { 277 | let name: string = args[0]; 278 | let table: string = args[1]; 279 | 280 | if (typeof name === "undefined") { 281 | return logThis("Model name not defined!"); 282 | } 283 | 284 | if (typeof table === "undefined") { 285 | table = lodash.snakeCase(PathHelper.path().basename(name)); 286 | } 287 | 288 | if (artisanConfig.singleModelName) { 289 | name = Artisan.singular(name); 290 | } 291 | 292 | if (artisanConfig.pluralizeModelTable) { 293 | table = Artisan.pluralize(table); 294 | } 295 | 296 | const modelPath = $.path.models(); 297 | Artisan.copyFromFactoryToApp("model", name, modelPath, {name, table}); 298 | 299 | $.exit(); 300 | }, 301 | 302 | /** 303 | * Make View File. 304 | * @param args 305 | */ 306 | "make:view"(args: string[]) { 307 | const config = $.config.get('template'); 308 | let name = args[0]; 309 | let defaultContent = ""; 310 | 311 | if (typeof name === "undefined") { 312 | return logThis("View name not defined!"); 313 | } 314 | 315 | if (name === "__routes") { 316 | defaultContent = $.base64.encode($.routerEngine.nameToUrl()); 317 | defaultContent = ""; 320 | } 321 | 322 | name += "." + config.extension; 323 | 324 | const fullPath = $.path.views(name); 325 | PathHelper.makeDirIfNotExist(fullPath, true); 326 | 327 | if (name.substring(0, 2) !== "__" && fs.existsSync(fullPath)) { 328 | return logThisAndExit("view {" + colors.fgYellow + name + colors.fgCyan + "} already exits!"); 329 | } 330 | 331 | if (!defaultContent.length) { 332 | const defaultContentFile = $.path.views("_." + config.extension); 333 | if (fs.existsSync(defaultContentFile)) { 334 | defaultContent = fs.readFileSync(defaultContentFile).toString(); 335 | } 336 | } 337 | 338 | fs.writeFileSync(fullPath, defaultContent); 339 | logThis("View created successfully!"); 340 | logThis("Located @ " + fullPath); 341 | 342 | return $.exit(); 343 | }, 344 | 345 | /** 346 | * Import importable files 347 | * @param plugin 348 | * @param folder 349 | * @param overwrite 350 | */ 351 | import([plugin, folder, shouldOverwrite]: string[]) { 352 | const overwrite = shouldOverwrite === 'overwrite'; 353 | 354 | if (plugin.toLowerCase() === 'xpresser') { 355 | return this.importFiles(folder, overwrite); 356 | } 357 | 358 | const allowedImportables = [ 359 | "configs", 360 | "models", 361 | "views", 362 | "events", 363 | "controllers", 364 | "middlewares", 365 | ] 366 | 367 | folder = folder.toLowerCase(); 368 | // @ts-ignore 369 | if (!allowedImportables.includes(folder)) 370 | return $.logErrorAndExit(`Import does not support any importable folder named (${folder})`); 371 | 372 | // Get config 373 | const config = $.engineData.get(`PluginEngine:namespaces[${plugin}]`); 374 | 375 | if (!config) 376 | return $.logErrorAndExit(`No plugin namespaced {${plugin}} registered in your project`); 377 | 378 | const importable = config.importable || config.publishable; 379 | if (!importable) 380 | return $.logErrorAndExit(`Plugin: {${plugin}} does not have any importables`); 381 | 382 | 383 | let importableFactory = importable[folder]; 384 | if (!importableFactory) 385 | return $.logErrorAndExit(`Plugin: {${plugin}} does not have any importable item named (${folder})`); 386 | 387 | if (importable.hasOwnProperty(folder + '.ts') && $.isTypescript()) { 388 | importableFactory = importable[folder + '.ts']; 389 | } 390 | 391 | const from = config.path + '/' + importableFactory; 392 | if (!fs.existsSync(from)) 393 | return $.logErrorAndExit(`File/Folder {${importableFactory}} does not exists in plugin (${plugin}) directory.`) 394 | 395 | 396 | // @ts-ignore 397 | let to: string = $.path[folder](lodash.kebabCase(config.namespace)) 398 | 399 | if ($.file.isFile(from)) { 400 | const ext = PathHelper.getExtension(from); 401 | if (ext) to += ext; 402 | 403 | PathHelper.makeDirIfNotExist(to, true); 404 | } else { 405 | PathHelper.makeDirIfNotExist(to); 406 | } 407 | 408 | // Copy Folders 409 | fse.copy(from, to, {overwrite}) 410 | .then(() => { 411 | const base = $.path.base(); 412 | 413 | $.logInfo(`From: (${from.replace(base, '')})`) 414 | $.logInfo(`To: (${to.replace(base, '')})`) 415 | 416 | $.logAndExit('Publish completed!') 417 | }) 418 | .catch(err => { 419 | $.logError('An error occurred while publishing the folder.') 420 | return $.logAndExit(err) 421 | }) 422 | }, 423 | 424 | 425 | /** 426 | * Import Factory files like e.g typescript types. 427 | */ 428 | async importFiles(folder: string, overwrite: boolean) { 429 | if (folder !== 'types') { 430 | return $.logErrorAndExit(`Xpresser does not have any importable item named (${folder})`); 431 | } 432 | 433 | const base = $.path.base(); 434 | const typesDestination = $.path.backend('types'); 435 | const factoryTypesFolder = $.path.engine('Factory/types'); 436 | 437 | /** 438 | * If typesDestination exits, make 439 | */ 440 | if (fs.existsSync(typesDestination)) { 441 | try { 442 | for (const type of ['index', 'modules', 'xpresser']) { 443 | const from = `${factoryTypesFolder}/${type}.d.ts`; 444 | const to = `${typesDestination}/${type}.d.ts`; 445 | 446 | await fse.copy(from, to, {overwrite}); 447 | 448 | $.logInfo(`From: (${from.replace(base, '')})`); 449 | $.logInfo(`To: (${to.replace(base, '')})`); 450 | } 451 | 452 | $.logAndExit('Import completed!') 453 | } catch (e) { 454 | return $.logAndExit(e); 455 | } 456 | } else { 457 | $.file.makeDirIfNotExist(typesDestination); 458 | 459 | fse.copy(factoryTypesFolder, typesDestination, {overwrite}) 460 | .then(() => { 461 | 462 | $.logInfo(`From: (${factoryTypesFolder.replace(base, '')})`) 463 | $.logInfo(`To: (${typesDestination.replace(base, '')})`) 464 | 465 | }) 466 | .catch(err => { 467 | $.logError('An error occurred while publishing the folder.') 468 | return $.logAndExit(err) 469 | }) 470 | } 471 | } 472 | }; 473 | 474 | export = {Commands, Artisan}; 475 | -------------------------------------------------------------------------------- /src/Console/JobHelper.ts: -------------------------------------------------------------------------------- 1 | import {getInstance} from "../../index"; 2 | import {exec} from "child_process"; 3 | 4 | const $ = getInstance(); 5 | 6 | class JobHelper { 7 | public name: string | null = null; 8 | public $: typeof $; 9 | 10 | constructor(name: string) { 11 | if (name) this.name = name; 12 | this.$ = $; 13 | } 14 | 15 | /** 16 | * Check if job was called from xjs-cli 17 | */ 18 | public isFromXjsCli() { 19 | return $.options.isFromXjsCli 20 | } 21 | 22 | public end(silent: boolean = false) { 23 | if (!silent) { 24 | $.log(`Job: (${this.name}) ran at: ${$.helpers.now()}`); 25 | } 26 | 27 | return $.exit(); 28 | } 29 | 30 | 31 | /** 32 | * Dispatch a job from anywhere in your application. 33 | * @param job 34 | * @param args 35 | */ 36 | static dispatch(job: string, args: any[] = []) { 37 | /** 38 | * Set Time to 1secs 39 | */ 40 | setTimeout(() => { 41 | /** 42 | * Add to next tick. 43 | */ 44 | process.nextTick(() => { 45 | // Show logs if in development 46 | const showLog = $.config.get("env") === "development"; 47 | 48 | // Get Project base path using `tsBaseFolder | default base` 49 | const base = $.engineData.get("tsBaseFolder", $.path.base()); 50 | 51 | // Get main module and remove base path out of it. 52 | const mainModule = require.main!.filename.replace(base, ""); 53 | 54 | // Use `ts-node` if is typescript. 55 | const app = $.isTypescript() ? "npx ts-node" : "node"; 56 | 57 | // Build Command 58 | const command = `cd ${base} && ${app} .${mainModule} cli @${job} ${args.join(" ")}`.trim(); 59 | 60 | // Execute command 61 | exec(command, (err, stdout) => { 62 | if (showLog) { 63 | if (err) console.error(err); 64 | else console.log((stdout || "").trim()); 65 | } 66 | }); 67 | }); 68 | }, 1000); 69 | } 70 | } 71 | 72 | export = JobHelper; -------------------------------------------------------------------------------- /src/Console/ProcessManager.js: -------------------------------------------------------------------------------- 1 | const {spawn} = require('child_process'); 2 | 3 | /** 4 | * Lets assume you are calling process manager from your project root. 5 | * @type {string} 6 | */ 7 | const RootDir = __dirname; 8 | 9 | class ProcessManager { 10 | constructor(rootDir = undefined) { 11 | if (rootDir !== undefined) { 12 | this.rootDir = rootDir; 13 | } else { 14 | this.rootDir = RootDir; 15 | } 16 | 17 | if (this.rootDir.slice(-1) === '/') { 18 | this.rootDir = this.rootDir.substring(0, this.rootDir.length - 1) 19 | } 20 | } 21 | 22 | addCommandProcess(file, $command) { 23 | // const processes = this.currentData(); 24 | const $commands = $command.trim().split(' '); 25 | const [, ...$afterFirstCommand] = $commands; 26 | const $process = spawn($commands[0], $afterFirstCommand); 27 | 28 | $process.stdout.on('data', (msg) => { 29 | console.log(msg.toString().trim()) 30 | }); 31 | } 32 | } 33 | 34 | ProcessManager.prototype.rootDir = './'; 35 | 36 | module.exports = ProcessManager; 37 | -------------------------------------------------------------------------------- /src/Controllers/ControllerService.ts: -------------------------------------------------------------------------------- 1 | import type {Controller} from "../../types/http"; 2 | import lodash from "lodash"; 3 | 4 | const $defaultController = { 5 | services: {}, 6 | }; 7 | 8 | class ControllerService { 9 | public controller: { 10 | [name: string]: any, 11 | __extend__?: { 12 | services?: object, 13 | }, 14 | }; 15 | 16 | constructor($controller: Controller.Object | any) { 17 | if ($controller) { 18 | this.controller = $controller; 19 | this.controller.__extend__ = $defaultController; 20 | } else { 21 | throw Error("Service not defined!"); 22 | } 23 | } 24 | 25 | /** 26 | * Register controller services. 27 | * @param $services - object of services. 28 | */ 29 | public services($services: Controller.Services): this { 30 | // @ts-ignore 31 | this.controller.__extend__.services = Object.assign(this.controller.__extend__.services as object || {}, $services); 32 | return this; 33 | } 34 | 35 | /** 36 | * Clone current controller. 37 | */ 38 | protected getClone(): any { 39 | return lodash.cloneDeep(this.controller); 40 | } 41 | 42 | } 43 | 44 | export = ControllerService; 45 | -------------------------------------------------------------------------------- /src/Controllers/ControllerServiceError.ts: -------------------------------------------------------------------------------- 1 | class ControllerServiceError { 2 | public args: any[]; 3 | 4 | constructor(args: any[]) { 5 | this.args = args; 6 | } 7 | } 8 | 9 | export = ControllerServiceError; 10 | -------------------------------------------------------------------------------- /src/Controllers/ProcessServices.ts: -------------------------------------------------------------------------------- 1 | import type {Http} from "../../types/http"; 2 | import {ServerResponse} from "http"; 3 | import ControllerServiceError from "./ControllerServiceError"; 4 | 5 | 6 | 7 | /** 8 | * This functions loops through all defined services, 9 | * Runs them and add any data returned to the services object. 10 | * 11 | * Each service gets the data returned by the previous service object. 12 | */ 13 | export = async ( 14 | http: Http.Request, 15 | boot: any, 16 | requestServices: any, 17 | config: { 18 | services?: object, 19 | }, 20 | error: (...args: any[]) => any, 21 | ) => { 22 | 23 | // List of services defined in this controller 24 | const DefinedServices: Record = config.services || {}; 25 | // Holds data of each completed service 26 | const completedServices: Record = {}; 27 | const serviceKeys = Object.keys(requestServices); 28 | 29 | // Each service error handler. 30 | const serviceErrorHandler = (...args: any[]) => { 31 | return new ControllerServiceError(args); 32 | }; 33 | 34 | // loop through services. 35 | for (const serviceKey of serviceKeys) { 36 | const options = { 37 | boot, 38 | http, 39 | services: completedServices, 40 | error: serviceErrorHandler 41 | }; 42 | 43 | // Service action [function | string] 44 | const action = DefinedServices[serviceKey]; 45 | let serviceResult: any | ControllerServiceError; 46 | 47 | if (Array.isArray(action)) { 48 | serviceResult = await action[0](options); 49 | } else { 50 | serviceResult = await DefinedServices[serviceKey]( 51 | requestServices[serviceKey], 52 | options, 53 | ); 54 | } 55 | 56 | // Run user defined error 57 | if (serviceResult instanceof ControllerServiceError && error) { 58 | serviceResult = error(http, ...serviceResult.args); 59 | } 60 | 61 | // if a service returns ServerResponse then stop loop and return that response. 62 | if (serviceResult instanceof ServerResponse) { 63 | return serviceResult; 64 | } 65 | 66 | // Add ServiceKey to completed service keys. 67 | completedServices[serviceKey] = serviceResult; 68 | } 69 | 70 | const completedServiceKeys = Object.keys(completedServices); 71 | const lastCompletedService = completedServiceKeys[completedServiceKeys.length - 1]; 72 | 73 | if (lastCompletedService) { 74 | return completedServices[lastCompletedService]; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/ErrorEngine.ts: -------------------------------------------------------------------------------- 1 | import type {HttpError} from "../types/http"; 2 | import type RequestEngine from "./RequestEngine"; 3 | 4 | const statusCodes = { 5 | 400: "Bad Request", 6 | 401: "Unauthorized", 7 | 404: "Not Found", 8 | 500: "Server Error", 9 | } 10 | 11 | 12 | class ErrorEngine { 13 | public http: RequestEngine; 14 | public error?: any; 15 | 16 | constructor(http: RequestEngine, e?: any) { 17 | this.http = http; 18 | this.error = e; 19 | } 20 | 21 | hasCustomErrorHandler() { 22 | return typeof (this.http as any)["onError"] === "function"; 23 | }; 24 | 25 | public view(data: {error: HttpError.Data}, status = 500) { 26 | if (this.hasCustomErrorHandler()) { 27 | return (this.http as unknown as HttpError.onError)["onError"](this.error, data.error); 28 | } 29 | 30 | return this.http.status(status).renderViewFromEngine("__errors/index", { 31 | ...data, 32 | statusCode: status, 33 | statusCodes 34 | }); 35 | } 36 | 37 | 38 | public pageNotFound() { 39 | const req = this.http.req; 40 | 41 | const error = { 42 | title: `404 Error!`, 43 | message: `${req.method}: ${req.url}

Route not found!`, 44 | }; 45 | 46 | return this.view({error}, 404); 47 | } 48 | } 49 | 50 | export = ErrorEngine; 51 | -------------------------------------------------------------------------------- /src/Errors/Deprecated.ts: -------------------------------------------------------------------------------- 1 | class Deprecated extends Error { 2 | constructor(message?: string) { 3 | super(message); 4 | this.name = "Deprecated" 5 | } 6 | } 7 | 8 | export = Deprecated; -------------------------------------------------------------------------------- /src/Errors/InXpresserError.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | 3 | /** 4 | * InXpresserError 5 | * 6 | * This is xpresser's default Error handler. 7 | * It defers from the default node Error handler because it has the time of error in its data. 8 | */ 9 | class InXpresserError extends Error { 10 | // Holds Date 11 | public date: Date; 12 | // Holds human-readable DateString 13 | public dateString: string; 14 | 15 | constructor(message?: string | undefined) { 16 | super(message); 17 | 18 | this.date = new Date(); 19 | this.dateString = new Date().toLocaleDateString('en-US', { 20 | day: 'numeric', 21 | weekday: 'short', 22 | year: 'numeric', 23 | month: 'short', 24 | hour: 'numeric', 25 | minute: 'numeric', 26 | second: 'numeric', 27 | }); 28 | 29 | // Ensure the name of this error is the same as the class name 30 | this.name = 'Error'; 31 | // This clips the constructor invocation from the stack trace. 32 | // It's not absolutely essential, but it does make the stack trace a little nicer. 33 | // @see Node.js reference (bottom) 34 | Error.captureStackTrace(this, this.constructor); 35 | } 36 | 37 | /** 38 | * TryOrCatch 39 | * 40 | * this method runs any function passed to it in a try catch. 41 | * It also returns the value of the function called or logs error. 42 | * 43 | * **Note:** It does not throw errors, only console.logs them. 44 | * @param fn 45 | */ 46 | static try(fn: () => T): T { 47 | return this.tryOrCatch(fn, (e) => { 48 | console.log(e); 49 | }) 50 | } 51 | 52 | /** 53 | * TryOrCatch 54 | * 55 | * this method runs any function passed to it in a try catch. 56 | * It also returns the value of the function called or logs error. 57 | * 58 | * **Note:** It throw errors 59 | * @param fn 60 | * @param handleError 61 | */ 62 | static tryOrCatch(fn: () => T, handleError?: (error: InXpresserError) => any): T { 63 | try { 64 | return fn(); 65 | } catch (e) { 66 | if(handleError) return handleError(this.use(e as Error)); 67 | throw this.use(e as Error); 68 | } 69 | } 70 | 71 | static use(e: Error) { 72 | const error = new this(e.message) 73 | const stack: string[] = e.stack!.split(os.EOL); 74 | stack.splice(0, 1); 75 | stack.unshift(error.stack!.split(os.EOL)[0]) 76 | error.stack = stack.join(os.EOL); 77 | return error; 78 | } 79 | } 80 | 81 | export = InXpresserError; -------------------------------------------------------------------------------- /src/Events/Emitter.ts: -------------------------------------------------------------------------------- 1 | import events from "events"; 2 | import {getInstance} from "../../index"; 3 | 4 | const $ = getInstance(); 5 | 6 | export = (DefinedEvents: Record) => { 7 | 8 | const EventEmitter = new events.EventEmitter(); 9 | 10 | EventEmitter.on("runEvent", async ($payload: { 11 | event: string, 12 | payload: any[], 13 | callback?: (eventResult: any) => any | void, 14 | }) => { 15 | if (DefinedEvents.hasOwnProperty($payload.event)) { 16 | 17 | try { 18 | if ($payload.callback) { 19 | let eventResult: any; 20 | 21 | if ($payload.payload) { 22 | eventResult = await DefinedEvents[$payload.event](...$payload.payload); 23 | } else { 24 | eventResult = await DefinedEvents[$payload.event](); 25 | } 26 | 27 | $payload.callback(eventResult); 28 | } else { 29 | 30 | if ($payload.payload) { 31 | DefinedEvents[$payload.event](...$payload.payload); 32 | } else { 33 | DefinedEvents[$payload.event](); 34 | } 35 | 36 | } 37 | } catch (e) { 38 | $.logError(e); 39 | } 40 | } 41 | }); 42 | 43 | return class XpresserEventsEmitter { 44 | public static emit(event: string, ...args: any[]): void { 45 | EventEmitter.emit("runEvent", { 46 | event, 47 | payload: args, 48 | }); 49 | } 50 | 51 | public static emitAfter(time: number = 3000, event: string, ...args: any[]): void { 52 | setTimeout(() => { 53 | XpresserEventsEmitter.emit(event, ...args); 54 | }, time); 55 | } 56 | 57 | public static emitWithCallback(event: string, args: any[], callback: (eventResult: any) => any) { 58 | EventEmitter.emit("runEvent", { 59 | event, 60 | payload: args, 61 | callback, 62 | }); 63 | } 64 | 65 | public static define(event: string, run: (...args: any[]) => void | any): void { 66 | DefinedEvents[event] = run; 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/Events/Loader.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import {getInstance} from "../../index"; 3 | import Path from "../Helpers/Path"; 4 | import ObjectCollection from "object-collection/index"; 5 | import EventsEmitter from "./Emitter"; 6 | 7 | const $ = getInstance(); 8 | const DefinedEvents: Record = {}; 9 | const EventsPath = $.path.events(); 10 | 11 | // Load all event files 12 | const $useDotJson: ObjectCollection = $.engineData.get("UseDotJson"); 13 | const excludedFiles: string[] = $useDotJson.get("exclude.events.files", []); 14 | const excludedFolders: string[] = $useDotJson.get("exclude.events.folders", []); 15 | Path.addProjectFileExtension(excludedFiles); 16 | 17 | function readEventsDirectory (parent: string) { 18 | const Events = fs.readdirSync(parent); 19 | 20 | for (let i = 0; i < Events.length; i++) { 21 | const event: string[] | string = Events[i]; 22 | const fullPath = parent + "/" + event; 23 | const shortPath = fullPath.replace(EventsPath + "/", ""); 24 | 25 | if (fs.lstatSync(fullPath).isDirectory()) { 26 | // Get events not excluded in use.json 27 | if (!excludedFolders.includes(event)) { 28 | readEventsDirectory(fullPath); 29 | } 30 | } else { 31 | // Get events not excluded in use.json 32 | if (!excludedFiles.includes(event)) { 33 | let event: any; 34 | 35 | try { 36 | event = require(fullPath); 37 | } catch (e) { 38 | $.logPerLine([ 39 | {error: `Error in ${shortPath}`}, 40 | {error: e}, 41 | ]); 42 | } 43 | 44 | if (typeof event === "object") { 45 | const eventKeys = Object.keys(event); 46 | const namespace = event.namespace || Path.removeProjectFileExtension(shortPath); 47 | 48 | for (let j = 0; j < eventKeys.length; j++) { 49 | const eventKey = eventKeys[j]; 50 | if (typeof event[eventKey] === "function") { 51 | let name = namespace; 52 | 53 | if (eventKey !== "index") { 54 | name = `${namespace}.${eventKey}`; 55 | } 56 | 57 | DefinedEvents[name] = event[eventKey]; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | if (fs.existsSync(EventsPath)) { 67 | $.ifConsole(() => { 68 | if ($.config.get('artisan.loadEvents')) { 69 | readEventsDirectory(EventsPath); 70 | } 71 | }, () => { 72 | readEventsDirectory(EventsPath); 73 | }); 74 | } 75 | 76 | // Set global $.events to Emitter 77 | $.events = EventsEmitter(DefinedEvents); 78 | -------------------------------------------------------------------------------- /src/Events/OnEventsLoader.ts: -------------------------------------------------------------------------------- 1 | import {getInstance, InXpresserError} from "../../index"; 2 | 3 | /** 4 | * Get on events loader 5 | * @param name 6 | * @param done 7 | */ 8 | export const runBootEvent = (name: string, done?: () => void) => { 9 | const $ = getInstance(); 10 | 11 | const key = `on.${name}`; 12 | const onEvents: any[] = $.on.events()[name]; 13 | 14 | if (onEvents.length) { 15 | onEvents.push(done); 16 | $.engineData.set(key, 0); 17 | 18 | const next = () => { 19 | const currentIndex = $.engineData.get(key, 0); 20 | const nextIndex = currentIndex + 1; 21 | $.engineData.set(key, nextIndex); 22 | 23 | if (typeof onEvents[nextIndex] === "function") { 24 | return InXpresserError.tryOrCatch(() => onEvents[nextIndex](next, $)); 25 | } 26 | }; 27 | 28 | // Pass next and current xpresser instance 29 | return InXpresserError.tryOrCatch(() => onEvents[0](next, $)); 30 | } else { 31 | return done ? done() : false; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/Extensions/Helpers.ts: -------------------------------------------------------------------------------- 1 | import BuildUrl from "build-url-ts"; 2 | import {getInstance} from "../../index"; 3 | import moment from "moment"; 4 | 5 | const $ = getInstance(); 6 | 7 | /* HELPER FUNCTIONS */ 8 | const helpers = { 9 | urlBuilder: BuildUrl, 10 | 11 | /** 12 | * Get full url of path 13 | * @param {string} $path 14 | * @param {object} $query 15 | */ 16 | url($path: string = "", $query: any = {}) { 17 | let url = ""; 18 | const server: Record = $.config.get('server'); 19 | 20 | if ($path.substring(0, 1) === "/") 21 | $path = $path.substring(1); 22 | 23 | if (server.baseUrl.length) { 24 | url = server.baseUrl + $path; 25 | } else { 26 | let d = server.domain; 27 | let p = server.protocol; 28 | 29 | if (server.includePortInUrl && (server.port !== 80 && server.port !== 443)) { 30 | d = d + ":" + server.port; 31 | } 32 | 33 | if ($.config.get("server.ssl.enabled", false)) { 34 | p = "https"; 35 | } 36 | 37 | url = p + "://" + d + server.root + $path; 38 | } 39 | 40 | if (Object.keys($query).length) { 41 | url = BuildUrl(url, { 42 | queryParams: $query, 43 | })!; 44 | } 45 | 46 | return url; 47 | }, 48 | 49 | /** 50 | * Get url of route. 51 | * @param {string} $route 52 | * @param {array|string} $keys 53 | * @param {Object|boolean} $query 54 | * @param {boolean} $includeUrl 55 | */ 56 | route($route: string, $keys: string | string[] = [], $query: object | boolean = {}, $includeUrl = true) { 57 | 58 | if (typeof $query === "boolean") { 59 | $includeUrl = $query; 60 | $query = {}; 61 | } 62 | 63 | if (!Array.isArray($keys)) { 64 | $keys = [$keys]; 65 | } 66 | 67 | const routes = $.routerEngine.nameToPath(); 68 | 69 | if (typeof routes[$route] !== "undefined") { 70 | let path = routes[$route]; 71 | if (path.slice(-1) === "*" && !$keys.length) { 72 | return path.slice(0, path.length - 1); 73 | } 74 | const hasRegex = path.match(new RegExp('[|&;$%@"<>()+:,*]')); 75 | if (Array.isArray(hasRegex) && hasRegex.length) { 76 | // find * and :keys 77 | const findKeys = new RegExp("[*]|(:[a-z_A-Z]+)", "g"); 78 | const HasKeys = path.match(findKeys); 79 | 80 | if (Array.isArray(HasKeys) && HasKeys.length) { 81 | let counter = 0; 82 | const replacer = (...args: any[]) => { 83 | if (args[0] === "*" && !$keys.length) { 84 | counter++; 85 | return "*"; 86 | } 87 | 88 | const key = $keys[counter] || (args[0] === "*" ? "*" : "_??_"); 89 | counter++; 90 | return key; 91 | }; 92 | 93 | path = path.replace(findKeys, replacer); 94 | } 95 | } 96 | return $includeUrl ? helpers.url(path, $query) : path; 97 | } 98 | return $includeUrl ? helpers.url($route, $query) : $route; 99 | }, 100 | 101 | /** 102 | * Get Config 103 | * @param {string} $config - Config key 104 | * @param {*} $default - Default return value if config is not found. 105 | */ 106 | config($config: string, $default?: any) { 107 | return $.config.get($config, $default); 108 | }, 109 | 110 | /** 111 | * Laravel Mix Helper 112 | * @param {string} file - Public path to file. 113 | */ 114 | mix(file: string) { 115 | let mix; 116 | const localVariableName = "laravelMixManifest"; 117 | if (file.substring(0, 1) !== "/") { 118 | file = "/" + file; 119 | } 120 | 121 | if ($.engineData.has(localVariableName)) { 122 | mix = $.engineData.get(localVariableName); 123 | } else { 124 | const mixFile = $.path.base("public/mix-manifest.json"); 125 | if ($.file.exists(mixFile)) { 126 | mix = require(mixFile); 127 | $.engineData.set(localVariableName, mix); 128 | } 129 | } 130 | 131 | if (typeof mix[file] !== "undefined") { 132 | file = mix[file]; 133 | } 134 | 135 | return helpers.url(file); 136 | }, 137 | 138 | env(key: string, $default?: any): any { 139 | return $.env(key, $default); 140 | }, 141 | 142 | /** 143 | * Random string generator 144 | * @param {number} length - length of string. 145 | */ 146 | randomStr(length = 10): string { 147 | let i: number; 148 | let possible: string; 149 | let text: string; 150 | 151 | text = ""; 152 | possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 153 | i = 0; 154 | 155 | while (i < length) { 156 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 157 | i++; 158 | } 159 | return text; 160 | }, 161 | 162 | /** 163 | * Random Array Generator 164 | * @param {number} length 165 | * @return {Array} 166 | */ 167 | randomArray(length = 5): number[] { 168 | let i = 0; 169 | const newArray: number[] = []; 170 | while (i < length) { 171 | newArray.push(i); 172 | i++; 173 | } 174 | 175 | return newArray; 176 | }, 177 | 178 | randomInteger(min: number, max: number): number { 179 | return Math.floor(Math.random() * (max - min + 1)) + min; 180 | }, 181 | 182 | // --------------------------- 183 | // --------------------------- 184 | /* ------ DATE ------ */ 185 | // --------------------------- 186 | // --------------------------- 187 | 188 | now(): string { 189 | return moment().format($.config.get('date.format')); 190 | }, 191 | 192 | today(): string { 193 | return moment().format($.config.get('date.format')); 194 | }, 195 | 196 | /** 197 | * Parse Date 198 | */ 199 | toDate(date?: any, format?: string): moment.Moment { 200 | if (!format) { 201 | format = $.config.get('date.format'); 202 | } 203 | 204 | if (!date) { 205 | date = helpers.now(); 206 | } 207 | return moment(date, format); 208 | }, 209 | 210 | /** 211 | * Time Ago 212 | */ 213 | timeAgo(date: any, format?: string): string { 214 | return helpers.toDate(date, format).fromNow(); 215 | }, 216 | }; 217 | 218 | export = helpers; 219 | -------------------------------------------------------------------------------- /src/Extensions/If.ts: -------------------------------------------------------------------------------- 1 | import {getInstance} from "../../index"; 2 | const $ = getInstance(); 3 | 4 | // ---------- IF Helpers ---------- 5 | $.ifConsole = (isConsole: () => void, notConsole: () => void): void => { 6 | if ($.options.isConsole) { 7 | isConsole(); 8 | } else { 9 | notConsole(); 10 | } 11 | }; 12 | 13 | $.ifIsConsole = (isConsole: () => void): void => { 14 | if ($.options.isConsole) { 15 | isConsole(); 16 | } 17 | }; 18 | 19 | $.ifNotConsole = (notConsole: () => void): void => { 20 | if (!$.options.isConsole) { 21 | notConsole(); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/Extensions/Loggers.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import lodash from "lodash"; 3 | import {getInstance, InXpresserError} from "../../index"; 4 | import {touchMyMustache} from "../Functions/inbuilt.fn"; 5 | import os from "os"; 6 | 7 | const $ = getInstance(); 8 | 9 | const isDebugEnabled = $.config.sync('debug.enabled', true); 10 | const depWarnings = $.config.sync<{ enabled: boolean, showStack: boolean }>('debug.deprecationWarnings'); 11 | 12 | $.log = (...args) => { 13 | if (!args.length) { 14 | return console.log(""); 15 | } 16 | 17 | args.unshift(chalk.white("=>")); 18 | 19 | if (args.length === 2 && typeof args[1] === "string") { 20 | return console.log(chalk.cyanBright(...args)); 21 | } 22 | 23 | return console.log(...args); 24 | }; 25 | 26 | $.logCalmly = (...args) => { 27 | if (!args.length) { 28 | return console.log(""); 29 | } 30 | 31 | args.unshift(chalk.white("=>")); 32 | 33 | if (args.length === 2 && typeof args[1] === "string") { 34 | return console.log(chalk.white(...args)); 35 | } 36 | 37 | return console.log(...args); 38 | }; 39 | 40 | $.logDeprecated = (since: string, removedAt: string, message: string | string[], hasStack = true) => { 41 | 42 | // Check if messages. 43 | if (Array.isArray(message)) { 44 | const m: (string | null)[] = message; 45 | 46 | for (const i in m) { 47 | if (m[i] === null) { 48 | m[i] = os.EOL; 49 | } else { 50 | m[i] += ' '; 51 | } 52 | } 53 | 54 | message = message.join('').trim(); 55 | } 56 | 57 | const mustaches = touchMyMustache(message); 58 | if (mustaches.length) { 59 | mustaches.forEach(m => { 60 | // remove mustache 61 | const withoutMustache = m.replace('{{', '').replace('}}', ''); 62 | message = (message as string).replace(m, chalk.cyan(withoutMustache)) 63 | }); 64 | } 65 | 66 | const config = depWarnings.sync; 67 | if (isDebugEnabled.sync && config.enabled) { 68 | 69 | console.log(chalk.gray('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')) 70 | console.log(chalk.whiteBright(`!! DEPRECATED ALERT !!`)); 71 | 72 | if (hasStack && config.showStack) { 73 | console.log(chalk.white(os.EOL + message)); 74 | console.trace() 75 | console.log(); 76 | } else { 77 | console.log(chalk.white(os.EOL + message + os.EOL)); 78 | } 79 | 80 | console.log( 81 | chalk.whiteBright(`Since: `) + chalk.white(since) + `, ` + 82 | chalk.whiteBright(`To be removed: `) + chalk.white(removedAt) 83 | ); 84 | console.log(chalk.gray('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<')) 85 | } 86 | }; 87 | 88 | $.logInfo = (...args) => { 89 | if (!args.length) { 90 | return console.log(""); 91 | } 92 | 93 | args.unshift("=>"); 94 | 95 | if (args.length === 2 && typeof args[1] === "string") { 96 | return console.log(chalk.magentaBright(...args)); 97 | } 98 | 99 | return console.log(...args); 100 | }; 101 | 102 | $.logSuccess = (...args) => { 103 | if (!args.length) { 104 | return console.log(""); 105 | } 106 | 107 | args.unshift("✔✔"); 108 | 109 | if (args.length === 2 && typeof args[1] === "string") { 110 | return console.log(chalk.greenBright(...args)); 111 | } 112 | 113 | return console.log(...args); 114 | }; 115 | 116 | $.logWarning = (...args) => { 117 | if (!args.length) { 118 | return console.log(""); 119 | } 120 | 121 | args.unshift("!!"); 122 | 123 | if (args.length === 2 && typeof args[1] === "string") { 124 | return console.log(chalk.yellow(...args)); 125 | } 126 | 127 | return console.log(...args); 128 | }; 129 | 130 | $.logIfNotConsole = (...args) => { 131 | if (!$.options.isConsole) { 132 | $.log(...args); 133 | } 134 | }; 135 | 136 | $.logAndExit = (...args) => { 137 | if (args.length) { 138 | $.log(...args); 139 | } 140 | return $.exit(); 141 | }; 142 | 143 | $.logError = (error: any, exit: boolean = false) => { 144 | 145 | if (error instanceof InXpresserError) { 146 | console.log(chalk.redBright(error.stack)); 147 | if(error.dateString){ 148 | $.logWarning('Occurred: ' + error.dateString); 149 | } 150 | } else if (error instanceof Error) { 151 | console.log(chalk.redBright(error.stack ? error.stack : error)); 152 | } else if (typeof error === "string") { 153 | console.log(chalk.redBright(error)); 154 | } else { 155 | console.error(error); 156 | } 157 | 158 | if (exit) { 159 | return $.exit(); 160 | } 161 | }; 162 | 163 | $.logErrorAndExit = (error: any) => { 164 | return $.logError(error, true); 165 | }; 166 | 167 | $.logPerLine = ($logs = [], $spacePerLine = false) => { 168 | 169 | console.log(); 170 | for (let i = 0; i < $logs.length; i++) { 171 | const $log = $logs[i]; 172 | 173 | if (typeof $log === "function") { 174 | 175 | $log(); 176 | 177 | } else if (typeof $log === "object") { 178 | const key = Object.keys($log)[0]; 179 | 180 | // @ts-ignore 181 | $["log" + lodash.upperFirst(key)]($log[key]); 182 | 183 | } else { 184 | if (typeof $log === "string" && !$log.length) { 185 | $.log(); 186 | } else { 187 | $.log($log); 188 | } 189 | } 190 | 191 | if ($spacePerLine) { 192 | $.log(); 193 | } 194 | } 195 | console.log(); 196 | }; 197 | 198 | $.env = (key, $default) => { 199 | if (typeof process.env[key] === "undefined") { 200 | return $default; 201 | } 202 | 203 | return process.env[key]; 204 | }; 205 | -------------------------------------------------------------------------------- /src/Extensions/Path.ts: -------------------------------------------------------------------------------- 1 | import PathHelper from "../Helpers/Path"; 2 | import {getInstance} from "../../index"; 3 | import Path from "path"; 4 | 5 | const packageName: string = "xpresser"; 6 | const $ = getInstance(); 7 | 8 | const paths: Record = $.config.get('paths'); 9 | const baseFiles = paths.base + "/"; 10 | const backendFiles = PathHelper.resolve(paths.backend); 11 | 12 | // DollarSign `path` property 13 | $.path = { 14 | resolve: (path: string | string[], resolve: boolean = true): string => { 15 | return PathHelper.resolve(path, resolve) 16 | }, 17 | 18 | base: (path = "", returnRequire = false) => { 19 | if (path[0] === "/") { 20 | path = path.substring(1); 21 | } 22 | const base = Path.resolve(baseFiles + path); 23 | return returnRequire ? require(base) : base; 24 | }, 25 | 26 | backend: (path = "", returnRequire = false) => { 27 | if (path[0] === "/") { 28 | path = path.substring(1); 29 | } 30 | const backend = Path.resolve(backendFiles + "/" + path); 31 | return returnRequire ? require(backend) : backend; 32 | }, 33 | 34 | storage: (path = "") => { 35 | if (path[0] === "/") { 36 | path = path.substring(1); 37 | } 38 | return Path.resolve(paths.storage + "/" + path); 39 | }, 40 | 41 | frameworkStorageFolder: (path?: string): string => { 42 | if (path === undefined) { 43 | path = ""; 44 | } 45 | 46 | if (path[0] === "/") { 47 | path = path.substring(1); 48 | } 49 | return $.path.storage("framework/" + path); 50 | }, 51 | 52 | engine: (path: string = "", returnRequire = false, refresh = false): string | any => { 53 | const dataKey = "XpresserPath"; 54 | let EnginePath: string; 55 | 56 | if (!refresh && $.engineData.has(dataKey)) { 57 | EnginePath = $.engineData.get(dataKey); 58 | } else { 59 | if (typeof paths.engine === "string") { 60 | EnginePath = PathHelper.resolve(paths.engine) + "/"; 61 | } else { 62 | EnginePath = PathHelper.resolve([paths.npm, packageName, "src"]) + "/"; 63 | } 64 | 65 | $.engineData.set(dataKey, EnginePath); 66 | } 67 | 68 | if (path[0] === "/") { 69 | path = path.substring(1); 70 | } 71 | 72 | const engine = EnginePath + path; 73 | return returnRequire ? require(engine) : engine; 74 | }, 75 | 76 | events: (path: string = "", returnRequire: boolean = false): string | any => { 77 | if (path[0] === "/") { 78 | path = path.substring(1); 79 | } 80 | 81 | const event = PathHelper.resolve([paths.events, path]); 82 | 83 | return returnRequire ? require(event) : event; 84 | }, 85 | 86 | controllers: (path: string = "", returnRequire: boolean = false): string | any => { 87 | if (path[0] === "/") { 88 | path = path.substring(1); 89 | } 90 | 91 | const controller = PathHelper.resolve([paths.controllers, path]); 92 | 93 | return returnRequire ? require(controller) : controller; 94 | }, 95 | 96 | middlewares: (path: string = "", returnRequire: boolean = false): string | any => { 97 | if (path[0] === "/") { 98 | path = path.substring(1); 99 | } 100 | 101 | const middleware = PathHelper.resolve([paths.middlewares, path]); 102 | 103 | return returnRequire ? require(middleware) : middleware; 104 | }, 105 | 106 | models: (path: string = "", returnRequire: boolean = false): string | any => { 107 | if (path[0] === "/") { 108 | path = path.substring(1); 109 | } 110 | 111 | const model = PathHelper.resolve([paths.models, path]); 112 | 113 | return returnRequire ? require(model) : model; 114 | }, 115 | 116 | views: (path: string = "") => { 117 | if (path[0] === "/") { 118 | path = path.substring(1); 119 | } 120 | 121 | return PathHelper.resolve([paths.views, path]); 122 | }, 123 | 124 | jsonConfigs: (path: string = "") => { 125 | if (path[0] === "/") { 126 | path = path.substring(1); 127 | } 128 | return PathHelper.resolve([paths.jsonConfigs, path]); 129 | }, 130 | 131 | 132 | configs: (path: string = "", returnRequire: boolean = false): string | any => { 133 | if (path[0] === "/") { 134 | path = path.substring(1); 135 | } 136 | 137 | const config = PathHelper.resolve([paths.configs, path]); 138 | 139 | return returnRequire ? require(config) : config; 140 | }, 141 | 142 | node_modules(path: string = ""): string { 143 | let currentNodeModules: string; 144 | 145 | try { 146 | currentNodeModules = require.resolve('xpresser') 147 | .replace('xpresser\\dist\\index.js', '') 148 | .replace('xpresser/dist/index.js', ''); 149 | } catch (e) { 150 | currentNodeModules = PathHelper.resolve([paths.npm, path]) 151 | } 152 | 153 | return currentNodeModules + path; 154 | } 155 | }; 156 | -------------------------------------------------------------------------------- /src/Factory/controller.hbs: -------------------------------------------------------------------------------- 1 | const {ControllerClass} = require('xpresser'); 2 | /** 3 | * {{name}} 4 | */ 5 | class {{name}} extends ControllerClass { 6 | 7 | /** 8 | * middleware - Set Middleware 9 | * @returns {Object} 10 | */ 11 | static middleware(){ 12 | return {} 13 | } 14 | 15 | /** 16 | * Example Action 17 | * @param {Xpresser.Http} http 18 | */ 19 | action(http){ 20 | return http.send("Hello World") 21 | } 22 | 23 | } 24 | 25 | 26 | module.exports = {{name}}; 27 | -------------------------------------------------------------------------------- /src/Factory/controller.ts.hbs: -------------------------------------------------------------------------------- 1 | import {ControllerClass} from "xpresser"; 2 | import type {Http} from "xpresser/types/http"; 3 | 4 | /** 5 | * {{name}} 6 | */ 7 | class {{name}} extends ControllerClass { 8 | 9 | /** 10 | * Example controller action. 11 | * @param {Http} http 12 | */ 13 | action(http: Http) { 14 | return http.send({ 15 | route: http.route 16 | }); 17 | } 18 | 19 | } 20 | 21 | export = {{name}}; -------------------------------------------------------------------------------- /src/Factory/controller_object.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{name}} 3 | */ 4 | module.exports = { 5 | // Controller Name 6 | name: "{{name}}", 7 | 8 | // Controller Default Service Error Handler. 9 | e: (http, error) => http.status(401).json({error}), 10 | 11 | 12 | /** 13 | * Example Action. 14 | * @param {Xpresser.Http} http - Current Http Instance 15 | * @returns {*} 16 | */ 17 | action(http){ 18 | return http.send("Hello World"); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/Factory/controller_object.ts.hbs: -------------------------------------------------------------------------------- 1 | import type {Controller, Http} from "xpresser/types/http"; 2 | 3 | /** 4 | * {{name}} 5 | */ 6 | export = { 7 | // Controller Name 8 | name: "{{name}}", 9 | 10 | // Controller Default Error Handler. 11 | e: (http: Http, error: string) => http.status(401).json({ error }), 12 | 13 | 14 | /** 15 | * Example Action. 16 | * @param http - Current Http Instance 17 | */ 18 | action(http) { 19 | return http.send({ 20 | route: http.route 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/Factory/controller_service.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{name}} 3 | * @type {Xpresser.Controller.Service|*} 4 | */ 5 | const {{name}} = {}; 6 | {{name}}.namespace = null; 7 | 8 | {{name}}.exampleService = (options, {error}) => { 9 | // Logic Here or return error 10 | }; 11 | 12 | module.exports = {{name}}; 13 | -------------------------------------------------------------------------------- /src/Factory/controller_with_services.hbs: -------------------------------------------------------------------------------- 1 | const {getInstance} = require('xpresser'); 2 | const $ = getInstance(); 3 | 4 | /** 5 | * {{name}} 6 | */ 7 | const {{name}} = $.handler({ 8 | 9 | // Controller Name 10 | name: "{{name}}", 11 | 12 | // Controller Middlewares 13 | middlewares: {}, 14 | 15 | // Controller Default Service Error Handler. 16 | e: (http, error) => http.send({error}), 17 | 18 | /** 19 | * Example Method. 20 | * @returns {*} 21 | */ 22 | index: {send: "Hello World"}, 23 | }); 24 | 25 | 26 | module.exports = {{name}}; 27 | -------------------------------------------------------------------------------- /src/Factory/controller_with_services.ts.hbs: -------------------------------------------------------------------------------- 1 | import {getInstance} from "xpresser"; 2 | const $ = getInstance(); 3 | 4 | /** 5 | * {{name}} 6 | */ 7 | const {{name}} = $.handler({ 8 | // Controller Name 9 | name: "{{name}}", 10 | 11 | // Default Error Handler. 12 | e: (http: Http, error: any) => http.send({error}), 13 | 14 | /** 15 | * Action Example. 16 | */ 17 | index: {respond: "Hello World"}, 18 | }); 19 | 20 | {{name}}.services({ 21 | respond: (message: string) => ({message}), 22 | }); 23 | 24 | export = {{name}}; 25 | -------------------------------------------------------------------------------- /src/Factory/event.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{name}} 3 | */ 4 | module.exports = { 5 | namespace: "{{namespace}}", 6 | 7 | /** 8 | * The `index` method is called by default. 9 | * it also inherits namespace as name. 10 | */ 11 | index() { 12 | // Your Code 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/Factory/event.ts.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{name}} 3 | */ 4 | export = { 5 | namespace: "{{namespace}}", 6 | 7 | /** 8 | * The `index` method is called by default. 9 | * it also inherits namespace as name. 10 | */ 11 | index() { 12 | // Your Code 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/Factory/job.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Job: {{name}} 3 | */ 4 | module.exports = { 5 | // Job Handler 6 | async handler(args, job) { 7 | // Your Job Here 8 | 9 | 10 | // End current job process. 11 | return job.end(); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/Factory/job.ts.hbs: -------------------------------------------------------------------------------- 1 | import type JobHelper from "xpresser/src/Console/JobHelper"; 2 | 3 | /** 4 | * Job: {{name}} 5 | */ 6 | export = { 7 | // Job Handler 8 | async handler(args: string[], job: JobHelper): Promise { 9 | // Your Job Here 10 | 11 | 12 | // End current job process. 13 | return job.end(); 14 | } 15 | }; -------------------------------------------------------------------------------- /src/Factory/middleware.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{name}} 3 | */ 4 | module.exports = { 5 | 6 | /** 7 | * Default Middleware Action 8 | * @param {Xpresser.Http} http 9 | */ 10 | allow(http) { 11 | // run check here 12 | 13 | return http.next(); 14 | } 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /src/Factory/middleware.ts.hbs: -------------------------------------------------------------------------------- 1 | import type {Http} from "xpresser/types/http"; 2 | 3 | /** 4 | * {{name}} 5 | */ 6 | export = { 7 | 8 | /** 9 | * Default Middleware Action 10 | * @param {Xpresser.Http} http 11 | */ 12 | allow(http: Http): any { 13 | // run check here 14 | 15 | return http.next(); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /src/Factory/model.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{name}} Model 3 | */ 4 | class {{name}} { 5 | 6 | /** 7 | * Name of model database table name. 8 | * @method tableName 9 | * @returns {string} 10 | */ 11 | static get tableName() { 12 | return "{{table}}"; 13 | } 14 | 15 | } 16 | 17 | {{name}}.prototype.$hidden = []; 18 | 19 | module.exports = {{name}}; 20 | -------------------------------------------------------------------------------- /src/Factory/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Index file for your types. 3 | * Reference any declarations required by typescript compiler here. 4 | */ 5 | import "./modules"; 6 | import "./xpresser"; -------------------------------------------------------------------------------- /src/Factory/types/modules.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modules declaration file. 3 | * 4 | * Declare types for javascript modules without types. 5 | */ 6 | // For Example: 7 | // declare module "a-module-without-types"; -------------------------------------------------------------------------------- /src/Factory/types/xpresser.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Xpresser Extension Declaration file. 3 | * All Xpresser related types should be modified here. 4 | * 5 | * See: http://xpresserjs.com/typescript/initialize.html#xpresser-d-ts 6 | */ 7 | 8 | /** 9 | * Uncomment to Extend DollarSign Interface. 10 | */ 11 | /* 12 | import "xpresser/types"; 13 | 14 | declare module "xpresser/types" { 15 | interface DollarSign { 16 | key: value 17 | } 18 | }*/ 19 | -------------------------------------------------------------------------------- /src/FileEngine.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import PATH from "path"; 3 | import {getInstance} from "../index"; 4 | import fse from "fs-extra"; 5 | import {encodingType} from "../types"; 6 | import {DeleteDirOptions} from "./types"; 7 | 8 | const $ = getInstance() 9 | 10 | 11 | $.file = { 12 | /** 13 | * Return Node fs getInstance 14 | * @return {fs} 15 | */ 16 | fs() { 17 | return fs; 18 | }, 19 | 20 | /** 21 | * Return Node fs-extra getInstance 22 | * @return {fse} 23 | */ 24 | fsExtra() { 25 | return fse; 26 | }, 27 | 28 | /** 29 | * Check file size. 30 | * @param $path 31 | */ 32 | size($path: string): number { 33 | try { 34 | return fs.statSync($path).size; 35 | } catch (e) { 36 | return 0; 37 | } 38 | }, 39 | 40 | /** 41 | * IsFIle 42 | * @param $path 43 | */ 44 | isFile($path: string): boolean { 45 | try { 46 | return fs.statSync($path).isFile(); 47 | } catch (e) { 48 | return false; 49 | } 50 | }, 51 | 52 | /** 53 | * isSymbolicLink 54 | * @param $path 55 | */ 56 | isSymbolicLink($path: string): boolean { 57 | try { 58 | return fs.statSync($path).isSymbolicLink(); 59 | } catch (e) { 60 | return false; 61 | } 62 | }, 63 | 64 | /** 65 | * isDirectory 66 | * @param $path 67 | */ 68 | isDirectory($path: string): boolean { 69 | try { 70 | return fs.statSync($path).isDirectory(); 71 | } catch (e) { 72 | return false; 73 | } 74 | }, 75 | 76 | /** 77 | * 78 | * @param $path 79 | * @param $options 80 | */ 81 | get($path: string, $options?: { encoding?: encodingType, flag?: string } | null): string | Buffer | false { 82 | const fileExists = $.file.exists($path); 83 | 84 | if (!fileExists) { 85 | return false; 86 | } 87 | 88 | return fs.readFileSync($path, $options as any); 89 | }, 90 | 91 | /** 92 | * @param $path 93 | * @param $options 94 | */ 95 | read($path: string, $options?: { encoding?: encodingType, flag?: string }): string | Buffer | false { 96 | return $.file.get($path, $options); 97 | }, 98 | 99 | /** 100 | * Read Directory 101 | * @param $path 102 | * @param $options 103 | */ 104 | readDirectory($path: string, $options?: { 105 | encoding?: encodingType, 106 | writeFileTypes?: string, 107 | }): string[] | Buffer[] | false { 108 | return this.getDirectory($path, $options); 109 | }, 110 | 111 | /** 112 | * Get Directory 113 | * @param $path 114 | * @param $options 115 | */ 116 | getDirectory($path: string, $options?: { 117 | encoding?: encodingType, 118 | writeFileTypes?: string, 119 | }): string[] | Buffer[] | false { 120 | const fileExists = $.file.exists($path); 121 | 122 | if (!fileExists) { 123 | return false; 124 | } 125 | 126 | return fs.readdirSync($path, $options as any); 127 | }, 128 | 129 | /** 130 | * Check if a path or an array of paths exists. 131 | * 132 | * if $returnList is true and $path is an array, 133 | * the list of files found will be returned. 134 | * @param {string|string[]} $path - Path or Paths to find. 135 | * @param {boolean} $returnList - Return list of found files in array. 136 | */ 137 | exists($path: string | string[], $returnList = false): boolean | string[] { 138 | // If Array, loop and check if each files exists 139 | if (Array.isArray($path)) { 140 | const files = $path as string[]; 141 | // Holds files found 142 | const filesFound = [] as string[]; 143 | 144 | for (const file of files) { 145 | const fileExists = $.file.exists(file); 146 | 147 | // If we are not returning lists then we should stop once a path is not found. 148 | if (!$returnList && !fileExists) { 149 | return false; 150 | } 151 | 152 | if (fileExists) { 153 | filesFound.push(file); 154 | } 155 | } 156 | 157 | return $returnList ? filesFound : true; 158 | } else { 159 | // to check data passed. 160 | try { 161 | return fs.existsSync($path); 162 | } catch (e) { 163 | return false; 164 | } 165 | } 166 | 167 | }, 168 | 169 | delete($path: string | string[], $returnList = false) { 170 | 171 | 172 | // If Array, loop and check if each files exists 173 | if (Array.isArray($path)) { 174 | const paths = $path as string[]; 175 | // Holds files found 176 | const pathsDeleted = [] as string[]; 177 | 178 | for (const path of paths) { 179 | const pathExists = $.file.delete(path); 180 | 181 | // If we are not returning lists then we should stop once a path is not found. 182 | if (!$returnList && !pathExists) { 183 | return false; 184 | } 185 | 186 | if (pathExists) { 187 | pathsDeleted.push(path); 188 | } 189 | } 190 | 191 | return $returnList ? pathsDeleted : true; 192 | } else { 193 | // to check data passed. 194 | try { 195 | fs.unlinkSync($path); 196 | return true; 197 | } catch (e) { 198 | return false; 199 | } 200 | } 201 | }, 202 | 203 | 204 | deleteDirectory($path: string | string[], options?: DeleteDirOptions) { 205 | // If Array, loop and check if each file exists 206 | if (Array.isArray($path)) { 207 | const paths = $path as string[]; 208 | const returnList = options && options.returnList; 209 | // Holds files found 210 | const pathsDeleted = [] as string[]; 211 | 212 | for (const path of paths) { 213 | const deleted = $.file.deleteDirectory(path, options); 214 | 215 | // If we are not returning lists then we should stop once a path is not found. 216 | if (!(returnList) && !deleted) { 217 | return false; 218 | } 219 | 220 | if (deleted) pathsDeleted.push(path); 221 | } 222 | 223 | return returnList ? pathsDeleted : true; 224 | } else { 225 | // to check data passed. 226 | try { 227 | let rmDirOptions: any = undefined; 228 | 229 | if (options) { 230 | const {returnList, ...others} = options; 231 | rmDirOptions = others; 232 | } 233 | 234 | fs.rmSync($path, rmDirOptions); 235 | return true; 236 | } catch (e) { 237 | return false; 238 | } 239 | } 240 | }, 241 | 242 | readJson($path: string, fileExists = false) { 243 | /** 244 | * Check if path exists 245 | */ 246 | if (!fileExists && !fs.existsSync($path)) { 247 | throw Error(`$.file.readJson: Path (${$path}) does not exists.`); 248 | } 249 | 250 | try { 251 | const file = fs.readFileSync($path).toString(); 252 | return JSON.parse(file); 253 | } catch (e) { 254 | throw Error(`$.file.readJson: Error parsing json file (${$path})`); 255 | } 256 | }, 257 | 258 | saveToJson($path: string, $content: any, $options = {}) { 259 | $options = Object.assign({ 260 | checkIfFileExists: true, 261 | replacer: null, 262 | space: 2 263 | }, $options) 264 | 265 | // @ts-ignore 266 | if ($options.checkIfFileExists && !fs.existsSync($path)) { 267 | throw Error(`$.file.saveToJson: Path (${$path}) does not exists.`); 268 | } 269 | 270 | try { 271 | // @ts-ignore 272 | fs.writeFileSync($path, JSON.stringify($content, $options.replacer, $options.space)) 273 | return true; 274 | } catch (e) { 275 | console.log(e); 276 | throw Error(`$.file.saveToJson: Error saving data to json file (${$path})`); 277 | } 278 | }, 279 | 280 | /** 281 | * Makes a dir if it does not exist. 282 | * @param $path 283 | * @param $isFile 284 | */ 285 | makeDirIfNotExist($path: string, $isFile = false) { 286 | if ($isFile) { 287 | $path = PATH.dirname($path); 288 | } 289 | 290 | if (!fs.existsSync($path)) { 291 | try { 292 | fs.mkdirSync($path, {recursive: true}); 293 | } catch (e) { 294 | $.logErrorAndExit(e); 295 | } 296 | } 297 | 298 | return $path; 299 | } 300 | }; 301 | -------------------------------------------------------------------------------- /src/Functions/artisan.fn.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import lodash from "lodash"; 3 | import {getInstance} from "../../index"; 4 | import Handlebars from "handlebars"; 5 | import fse from "fs-extra"; 6 | import Pluralise from "pluralize"; 7 | import colors from "../Objects/consoleColors.obj"; 8 | import PathHelper from "../Helpers/Path"; 9 | 10 | const $ = getInstance(); 11 | 12 | const isTinker = typeof $.options.isTinker === "boolean" && $.options.isTinker; 13 | const FILE_EXTENSION = $.config.get("project.fileExtension", ".js"); 14 | 15 | /** 16 | * Get the string after the last forward slash. 17 | * @param str 18 | */ 19 | const afterLastSlash = (str: string) => { 20 | if (str.includes("/")) { 21 | const parts = str.split("/"); 22 | return lodash.upperFirst(parts[parts.length - 1]); 23 | } 24 | 25 | return lodash.upperFirst(str); 26 | }; 27 | 28 | export = { 29 | logThis(...args: any[]) { 30 | if (isTinker) { 31 | args.unshift(colors.fgMagenta); 32 | } else { 33 | args.unshift(colors.fgCyan); 34 | } 35 | 36 | args.push(colors.reset); 37 | 38 | return $.log(...args); 39 | }, 40 | 41 | logThisAndExit(...args: any[]) { 42 | if (isTinker) { 43 | return this.logThis(...args); 44 | } else { 45 | this.logThis(...args); 46 | $.logAndExit(); 47 | } 48 | }, 49 | 50 | 51 | factory(file: string, $data = {}) { 52 | 53 | /** 54 | * If app is Typescript then check if `.ts.hbs` of same file exists. 55 | */ 56 | if ($.isTypescript()) { 57 | const tsFile = file.replace('.hbs', '.ts.hbs'); 58 | if (fs.existsSync(tsFile)) { 59 | file = tsFile; 60 | } 61 | } 62 | 63 | const source = fs.readFileSync(file).toString(); 64 | const template = Handlebars.compile(source); 65 | return template($data); 66 | }, 67 | 68 | copyFromFactoryToApp(For: "job" | "event" | "controller" | "middleware" | "model" | string[], $name: string, $to: string, $data: Record = {}, addPrefix = true) { 69 | let $factory; 70 | let $for: string 71 | 72 | 73 | if (Array.isArray(For)) [$for, $factory] = For; 74 | else $for = For; 75 | 76 | PathHelper.makeDirIfNotExist($to); 77 | 78 | if (!$name.toLowerCase().includes($for)) { 79 | if (addPrefix && $for !== "model") { 80 | $name = $name + lodash.upperFirst($for); 81 | } 82 | } 83 | 84 | // make last path upper case 85 | if ($for !== "job") { 86 | if ($name.includes('/')) { 87 | const names = $name.split('/'); 88 | const lastPath = lodash.upperFirst(names.pop()); 89 | names.push(lastPath); 90 | $name = names.join('/'); 91 | } else { 92 | $name = lodash.upperFirst($name); 93 | } 94 | } 95 | 96 | $to = $to + "/" + $name + FILE_EXTENSION; 97 | 98 | if (fs.existsSync($to)) { 99 | return this.logThisAndExit($name + " already exists!"); 100 | } 101 | 102 | PathHelper.makeDirIfNotExist($to, true); 103 | 104 | /** 105 | * Get factory file from config or use default 106 | */ 107 | const factoryName: string = $factory ? $factory : $for; 108 | let $from = $.path.engine("Factory/" + factoryName + ".hbs"); 109 | 110 | let customFactoryFile: string = $.config.get(`artisan.factory.${factoryName}`); 111 | 112 | if (customFactoryFile) { 113 | if (!customFactoryFile.includes('.hbs')) { 114 | return this.logThisAndExit(`Custom factory file defined for ${factoryName} is not a (.hbs) file`) 115 | } 116 | 117 | customFactoryFile = PathHelper.resolve(customFactoryFile); 118 | 119 | if (!$.file.exists(customFactoryFile)) { 120 | return this.logThisAndExit(`Custom factory file defined for ${factoryName} does not exist.`) 121 | } 122 | 123 | $from = customFactoryFile; 124 | } 125 | 126 | /** 127 | * Append needed data 128 | */ 129 | $data['name'] = afterLastSlash($name); 130 | 131 | fs.writeFileSync($to, this.factory($from, $data)); 132 | 133 | this.logThis($name + " created successfully."); 134 | this.logThis("located @ " + $to.replace($.path.base(), "")); 135 | }, 136 | 137 | copyFolder($from: string, $to: string) { 138 | fse.copySync($from, $to); 139 | $.logAndExit("Views folder published successful."); 140 | }, 141 | 142 | pluralize(str = ""): string { 143 | if (!str.trim().length) { 144 | return str; 145 | } 146 | 147 | return Pluralise(str); 148 | }, 149 | 150 | singular(str: string) { 151 | return Pluralise.singular(str); 152 | }, 153 | }; 154 | -------------------------------------------------------------------------------- /src/Functions/inbuilt.fn.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import fs from "fs"; 3 | 4 | /** 5 | * Get LanIp 6 | */ 7 | export const getLocalExternalIp = (): string => { 8 | const values = Object.values(os.networkInterfaces()); 9 | return ([].concat(...values as any[]) 10 | .find((details: any) => details.family === 'IPv4' && !details.internal) as any)?.address; 11 | }; 12 | 13 | /** 14 | * Generate a randomStr 15 | * @param length 16 | */ 17 | export const randomStr = (length: number): string => { 18 | let result = ''; 19 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 20 | const charactersLength = characters.length; 21 | for (let i = 0; i < length; i++) { 22 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 23 | } 24 | return result; 25 | }; 26 | 27 | /** 28 | * Get All files in a given path. 29 | * @param path 30 | * @returns {Array} 31 | */ 32 | export const getAllFiles = (path: string): string[] => { 33 | 34 | const list: string[] = []; 35 | 36 | if (fs.existsSync(path)) { 37 | const files = fs.readdirSync(path); 38 | for (const file of files) { 39 | 40 | const fullPath: string = path + '/' + file; 41 | 42 | if (fs.lstatSync(fullPath).isDirectory()) { 43 | 44 | const folderFiles = getAllFiles(fullPath); 45 | for (const folderFile of folderFiles) { 46 | list.push(folderFile); 47 | } 48 | } else { 49 | list.push(fullPath); 50 | } 51 | } 52 | } 53 | 54 | return list; 55 | }; 56 | 57 | /** 58 | * Finds {{keys}} in string. 59 | * 60 | * @example 61 | * const mustaches = "I ate {{2}} {{cakes}} today"; 62 | * // ['2', 'cakes'] 63 | */ 64 | export function touchMyMustache(str: string) { 65 | // Stop if {{ is not found in string. 66 | if (str.indexOf('{{') < 0) return []; 67 | 68 | const match = str.match(new RegExp(/{{(.*?)}}/, 'g')); 69 | return match ? match : []; 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/Functions/internals.fn.ts: -------------------------------------------------------------------------------- 1 | import {getInstance} from "../../index"; 2 | import type {DollarSign} from "../../types"; 3 | import PathHelper from "../Helpers/Path"; 4 | import path from "path"; 5 | 6 | const $ = getInstance(); 7 | 8 | export function parseControllerString(controllerString: string) { 9 | const split = controllerString.split("@"); 10 | let startIndex = 0; 11 | if (split.length > 2) startIndex = split.length - 2; 12 | const method = split[startIndex + 1]; 13 | 14 | return { 15 | controller: controllerString.replace(`@${method}`, ''), 16 | method 17 | } 18 | } 19 | 20 | export function initializeTypescriptFn(filename: string, run?: (isNode: any) => void): DollarSign { 21 | 22 | if (!filename) throw Error(`isTypescript: requires __filename as argument.`); 23 | 24 | const isTypeScriptFile = filename.slice(-3) === '.ts'; 25 | const tsBaseFolder = 26 | isTypeScriptFile ? $.path.base() : path.resolve($.path.resolve(`base://../`)) 27 | 28 | 29 | // save tsBaseFolder to xpresser store 30 | $.engineData.set('tsBaseFolder', tsBaseFolder); 31 | 32 | // Set Project extension 33 | if (isTypeScriptFile) { 34 | $.config.set('project.fileExtension', '.ts'); 35 | 36 | // Change Default config file. 37 | if ($.config.get("paths.routesFile") === "backend://routes.js") { 38 | $.config.set('paths.routesFile', 'backend://routes.ts'); 39 | } 40 | } 41 | 42 | // Check for xpresser engine 43 | if (!isTypeScriptFile && !$.file.exists(PathHelper.resolve($.config.get('paths.npm')))) { 44 | try { 45 | $.config.set('paths.npm', $.path.node_modules()); 46 | $.path.engine('', false, true); 47 | } catch (e) { 48 | $.logError('Path to xpresser engine files maybe missing, point {config.paths.npm} to your node_modules folder.') 49 | } 50 | } 51 | 52 | 53 | if (!isTypeScriptFile) { 54 | /** 55 | * Fix route file. 56 | */ 57 | const routesFile: string = $.config.get('paths.routesFile'); 58 | 59 | if (routesFile.includes('.ts')) { 60 | $.config.set('paths.routesFile', routesFile.replace('.ts', '.js')) 61 | } 62 | 63 | /** 64 | * Fix jsonConfigs 65 | */ 66 | let jsonConfigs: string = $.config.get('paths.jsonConfigs'); 67 | jsonConfigs = $.path.resolve(jsonConfigs); 68 | jsonConfigs = jsonConfigs.replace($.path.base(), tsBaseFolder + '/'); 69 | $.config.set('paths.jsonConfigs', jsonConfigs); 70 | 71 | 72 | /** 73 | * Fix views folder 74 | */ 75 | let viewsPath: string = $.config.get('paths.views'); 76 | viewsPath = $.path.resolve(viewsPath); 77 | const tsViewsPath = viewsPath.replace($.path.base(), tsBaseFolder + '/') 78 | 79 | if ($.file.exists(tsViewsPath) && !$.file.exists(viewsPath)) { 80 | $.config.set('paths.views', tsViewsPath); 81 | } 82 | } 83 | 84 | /** 85 | * If run is a function, we pass `isNode` i.e `!isTypeScriptFile` to it. 86 | */ 87 | if (typeof run === "function") { 88 | run(isTypeScriptFile ? false : { 89 | ts: { 90 | baseFolder: tsBaseFolder 91 | } 92 | }); 93 | } 94 | 95 | return $; 96 | } -------------------------------------------------------------------------------- /src/Functions/modules.fn.ts: -------------------------------------------------------------------------------- 1 | import lodash, {LoDashStatic} from "lodash"; 2 | import moment from "moment"; 3 | import momentT from "moment-timezone"; 4 | 5 | const modules = { 6 | /** 7 | * Return lodash 8 | */ 9 | lodash(): LoDashStatic { 10 | return lodash; 11 | }, 12 | 13 | moment(): typeof moment { 14 | return moment; 15 | }, 16 | 17 | momentT(): typeof momentT { 18 | return momentT 19 | } 20 | }; 21 | 22 | export = modules; 23 | -------------------------------------------------------------------------------- /src/Functions/plugins.fn.ts: -------------------------------------------------------------------------------- 1 | import {getInstance} from "../../index"; 2 | 3 | const $ = getInstance() 4 | 5 | /** 6 | * Check if plugin file exists or throw error. 7 | * @param plugin 8 | * @param pluginPath 9 | * @param file 10 | */ 11 | export function pluginPathExistOrExit(plugin: string, pluginPath: string, file: string) { 12 | /** 13 | * ResolvedRoutePath - get file real path, 14 | * Just in any case smartPaths are used. 15 | */ 16 | const ResolvedRoutePath = $.path.resolve(file, false); 17 | 18 | if (file === ResolvedRoutePath) { 19 | // Merge plugin base path to file. 20 | file = pluginPath + "/" + file; 21 | } else { 22 | // file is ResolvedPath 23 | file = ResolvedRoutePath; 24 | } 25 | 26 | // If file or folder does not exist throw error. 27 | if (!$.file.exists(file)) { 28 | return $.logPerLine([ 29 | {error: plugin}, 30 | {error: `REQUIRED FILE or DIR MISSING: ${file}`}, 31 | {errorAndExit: ""}, 32 | ], true); 33 | } 34 | 35 | // return real path. 36 | return file; 37 | } 38 | 39 | 40 | /** 41 | * Since 0.5.0 plugins.json started using objects instead of arrays. 42 | * This function converts old arrays to new object syntax 43 | * @param plugins 44 | */ 45 | export function convertPluginArrayToObject(plugins: string[]) { 46 | if (!plugins) return {} 47 | 48 | const newPluginsObject: Record = {}; 49 | 50 | // Map plugins and set keys to newPluginsObject 51 | plugins.map(plugin => newPluginsObject[plugin] = true); 52 | 53 | return newPluginsObject; 54 | } 55 | 56 | 57 | 58 | /** 59 | * Compare version function 60 | * -1 = version1 is less than version 2 61 | * 1 = version1 is greater than version 2 62 | * 0 = Both are the same 63 | */ 64 | export function compareVersion(version1: string, version2: string) { 65 | const v1 = version1.split('.') as (string | number)[]; 66 | const v2 = version2.split('.') as (string | number)[]; 67 | const k = Math.min(v1.length, v2.length); 68 | 69 | for (let i = 0; i < k; ++i) { 70 | v1[i] = parseInt(v1[i] as string, 10); 71 | v2[i] = parseInt(v2[i] as string, 10); 72 | if (v1[i] > v2[i]) return 1; 73 | if (v1[i] < v2[i]) return -1; 74 | } 75 | 76 | return v1.length == v2.length ? 0 : (v1.length < v2.length ? -1 : 1); 77 | } 78 | -------------------------------------------------------------------------------- /src/Functions/request.fn.ts: -------------------------------------------------------------------------------- 1 | import type RequestEngine from "../RequestEngine"; 2 | 3 | export = (http: RequestEngine) => { 4 | return { 5 | currentViewIs(path: string, ifTrue = undefined, ifFalse = undefined) { 6 | const currentView = http.res.locals.ctx.$currentView; 7 | const check = currentView === path; 8 | 9 | if (check && ifTrue !== undefined) { return ifTrue; } 10 | if (!check && ifFalse !== undefined) { return ifFalse; } 11 | 12 | return check; 13 | }, 14 | 15 | old(key: string, $default = "") { 16 | const value = http.res.locals.ctx.$flash["old:" + key]; 17 | if (typeof value !== "undefined") { 18 | return value; 19 | } 20 | 21 | return $default; 22 | }, 23 | 24 | pushToScriptsStack(scriptPath: string | string[]) { 25 | if (!Array.isArray(scriptPath)) { 26 | scriptPath = [scriptPath]; 27 | } 28 | 29 | for (let i = 0; i < scriptPath.length; i++) { 30 | http.res.locals.ctx.$stackedScripts.push(scriptPath[i]); 31 | } 32 | 33 | return ""; 34 | }, 35 | 36 | showStackedScripts() { 37 | let scripts = ""; 38 | 39 | for (let i = 0; i < http.res.locals.ctx.$stackedScripts.length; i++) { 40 | scripts += ''; 41 | } 42 | 43 | return scripts; 44 | }, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/Functions/router.fn.ts: -------------------------------------------------------------------------------- 1 | import {parse} from "path-to-regexp"; 2 | 3 | 4 | export function pathToUrl(path: string | any) { 5 | if (typeof path !== "string") return path as string; 6 | 7 | try { 8 | const segments = parse(path); 9 | const newUrl: string[] = []; 10 | for (const segment of segments) { 11 | if (typeof segment === "string") { 12 | newUrl.push(segment); 13 | } else { 14 | newUrl.push(segment.prefix + "_??_"); 15 | } 16 | } 17 | 18 | return newUrl.join(""); 19 | } catch { 20 | return path; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/Functions/util.fn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Xpresser Util Functions 3 | * @type Xpresser.Helpers.Util 4 | */ 5 | export default { 6 | extArrayRegex(arr: []): RegExp | string { 7 | let regex = "\\.+("; 8 | const regexEnd = ")"; 9 | 10 | if (!arr.length) { 11 | return regex + regexEnd; 12 | } 13 | 14 | for (let i = 0; i < arr.length; i++) { 15 | regex = regex + arr[i] + "|"; 16 | } 17 | 18 | regex = regex.slice(0, regex.length - 1); 19 | regex = regex + regexEnd; 20 | 21 | return new RegExp(regex, "g"); 22 | }, 23 | 24 | findWordsInString(str: string, keywords: []): RegExpMatchArray | null { 25 | if (!keywords.length) { 26 | return null; 27 | } 28 | 29 | let regex = ""; 30 | 31 | for (let i = 0; i < keywords.length; i++) { 32 | regex = regex + "\\b" + keywords[i] + "|"; 33 | } 34 | 35 | regex = regex.slice(0, regex.length - 1); 36 | const pattern = new RegExp(regex, "g"); 37 | return str.match(pattern); 38 | }, 39 | 40 | isPromise(value: any): boolean { 41 | return value !== undefined && typeof value === "object" && typeof value.then === "function"; 42 | }, 43 | 44 | /** 45 | * Check if value is an AsyncFunction 46 | * @param fn 47 | */ 48 | isAsyncFn(fn: () => any): boolean { 49 | return typeof fn === "function" && fn.constructor.name === "AsyncFunction"; 50 | }, 51 | 52 | randomStr(length = 10) { 53 | let text = ""; 54 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 55 | let i = 0; 56 | while (i < length) { 57 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 58 | i++; 59 | } 60 | return text; 61 | }, 62 | 63 | regExpSourceOrString(str: string | RegExp) { 64 | if (str instanceof RegExp) { 65 | return str.source; 66 | } 67 | 68 | return str; 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/Helpers/Base64.ts: -------------------------------------------------------------------------------- 1 | const Base64 = { 2 | /** 3 | * Encode Str or Object 4 | * If Object, we will Json.stringify it 5 | * @param str 6 | */ 7 | encode(str: string | object): string { 8 | if (typeof str === "object") { 9 | str = JSON.stringify(str); 10 | } 11 | 12 | return Buffer.from(str).toString("base64"); 13 | }, 14 | 15 | /** 16 | * Decode encoded text. 17 | * @param str 18 | */ 19 | decode(str: string = ""): string { 20 | return Buffer.from(str, "base64").toString("ascii"); 21 | }, 22 | 23 | /** 24 | * Decode To Json Object 25 | * @param str 26 | */ 27 | decodeToObject(str: string = ""): object { 28 | str = Base64.decode(str); 29 | return JSON.parse(str); 30 | }, 31 | }; 32 | 33 | export = Base64; 34 | -------------------------------------------------------------------------------- /src/Helpers/Conditions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if a variable is null or undefined. 3 | * @param item 4 | */ 5 | export function isNullOrUndefined(item: any) { 6 | return item === null || item === undefined; 7 | } 8 | 9 | /** 10 | * Checks if variable is null or undefined. 11 | * 12 | * Returns variable value if defined else returns value of alt variable or function. 13 | * @param variable 14 | * @param alt 15 | */ 16 | export function ifNotDefined(variable: T | (() => T), alt: T | (() => T)): T { 17 | if (typeof variable === "function") { 18 | variable = (variable as Function)(); 19 | } 20 | 21 | if (isNullOrUndefined(variable) && typeof alt === "function") { 22 | alt = (alt as Function)(); 23 | } 24 | 25 | return (isNullOrUndefined(variable) ? alt : variable) as T; 26 | } 27 | 28 | 29 | /** 30 | * Checks if variable is defined. 31 | * 32 | * Returns variable value if defined else returns value of alt variable or function. 33 | * @param variable 34 | * @param alt 35 | */ 36 | export function ifOrElse(variable: T | (() => T), alt: T | (() => T)): T { 37 | if (typeof variable === "function") { 38 | variable = (variable as Function)(); 39 | } 40 | 41 | if (!variable && typeof alt === "function") { 42 | alt = (alt as Function)(); 43 | } 44 | 45 | return (variable ? variable : alt) as T; 46 | } -------------------------------------------------------------------------------- /src/Helpers/Path.ts: -------------------------------------------------------------------------------- 1 | import PATH from "path"; 2 | import {getInstance} from "../../index"; 3 | import fs from "fs"; 4 | 5 | const $ = getInstance(); 6 | 7 | const pathHelpers = { 8 | base: "base://", 9 | backend: "backend://", 10 | frontend: "frontend://", 11 | npm: "npm://", 12 | public: "public://", 13 | }; 14 | 15 | class PathHelper { 16 | static path() { 17 | return PATH; 18 | } 19 | 20 | static resolve($path: string | string[], $resolve: boolean = true): string { 21 | if (Array.isArray($path)) { 22 | for (let i = 0; i < $path.length; i++) { 23 | const $pathElement = $path[i]; 24 | if ($pathElement.slice(-1) === "/") { 25 | $path[i] = $pathElement.substring(0, $pathElement.length - 1); 26 | } 27 | } 28 | 29 | $path = $path.join("/"); 30 | } 31 | 32 | // Replace .js.js to .js 33 | if ($path.includes('.js.js')) { 34 | $path = $path.replace('.js.js', '.js'); 35 | } 36 | 37 | if ($.isTypescript()) { 38 | // Replace .js.ts to .js 39 | if ($path.includes('.js.ts')) { 40 | $path = $path.replace('.js.ts', '.js'); 41 | } 42 | 43 | // Replace .ts.js to .ts 44 | if ($path.includes('.ts.js')) { 45 | $path = $path.replace('.js.js', '.ts'); 46 | } 47 | } 48 | 49 | if ($path.indexOf("://") >= 0) { 50 | const $splitPath: string[] = $path.split("://"); 51 | 52 | if (pathHelpers.hasOwnProperty($splitPath[0])) { 53 | if (!$splitPath[1]) { 54 | $splitPath[1] = ""; 55 | } 56 | 57 | return PathHelper.helperToPath($splitPath); 58 | } 59 | 60 | } 61 | 62 | if ($path.slice(-1) === "/") { 63 | $path = $path.slice(0, -1); 64 | } 65 | 66 | return $resolve ? (p => PATH.resolve(p))($path) : $path; 67 | } 68 | 69 | static helperToPath([$helper, $path]: string[]): string { 70 | const config: any = $.config.get('paths'); 71 | 72 | if ($helper === "base") { 73 | return config.base + "/" + $path; 74 | } else if ($helper === "npm") { 75 | return PathHelper.resolve([config.npm, $path]); 76 | } else { 77 | if (config[$helper]) { 78 | return PathHelper.resolve([config[$helper], $path]); 79 | } 80 | return $.path.base(`${$helper}/${$path}`); 81 | } 82 | } 83 | 84 | /** 85 | * Get path in storage/framework folder. 86 | */ 87 | frameworkStorage(path?: string): string { 88 | if (path === undefined) { 89 | path = ""; 90 | } 91 | 92 | if (path[0] === "/") { 93 | path = path.substring(1); 94 | } 95 | return $.path.storage("framework/" + path); 96 | } 97 | 98 | /** 99 | * Makes a dir if it does not exist. 100 | * @param $path 101 | * @param $isFile 102 | */ 103 | static makeDirIfNotExist($path: string, $isFile = false) { 104 | if ($isFile === true) { 105 | $path = PATH.dirname($path); 106 | } 107 | 108 | if (!fs.existsSync($path)) { 109 | try { 110 | fs.mkdirSync($path, {recursive: true}); 111 | } catch (e) { 112 | $.logErrorAndExit(e); 113 | } 114 | } 115 | 116 | return $path; 117 | } 118 | 119 | /** 120 | * Adds project extension to a string or array of strings 121 | * @example 122 | * e.g if project extension is `.js` 123 | * ['server', 'app', 'test'] 124 | * 125 | * => ['server.js', 'app.js', 'test.js'] 126 | * @param files 127 | * @param useExtension 128 | * @param clone 129 | */ 130 | static addProjectFileExtension(files: string | string[], useExtension?: string | undefined, clone = false): string[] | string { 131 | if (Array.isArray(files)) { 132 | let array; 133 | if (clone) { 134 | array = [...files]; 135 | } else { 136 | array = files; 137 | } 138 | 139 | for (let i = 0; i < array.length; i++) { 140 | const file = array[i]; 141 | array[i] = PathHelper.addProjectFileExtension(file) as string; 142 | } 143 | 144 | return array; 145 | } else { 146 | // default file extension 147 | const jsExt = ".js"; 148 | // if custom extension is defined use it else use defined project extension. 149 | useExtension = (useExtension && useExtension.length) ? useExtension : $.config.get('project.fileExtension') 150 | // if not useExtension use jsExt 151 | const ext = useExtension ? useExtension : jsExt; 152 | // check and store if path has extension 153 | const hasExtInName = files.slice(-ext.length) === ext; 154 | 155 | if ( 156 | // ext is not same with jsExt (i.e maybe .ts) 157 | ext !== jsExt && 158 | // And ext was not found in name 159 | !hasExtInName && 160 | // And ext is .js 161 | files.slice(-jsExt.length) === jsExt 162 | ) { 163 | // return path un-modified. 164 | return files; 165 | } 166 | 167 | // Else add extension to name; 168 | return hasExtInName ? files : files + ext; 169 | } 170 | } 171 | 172 | /** 173 | * Adds project extension to a string or array of strings 174 | * @example 175 | * e.g if project extension is `.js` 176 | * ['server.js', 'app.js', 'test.js'] 177 | * 178 | * => ['server', 'app', 'test'] 179 | * @param files 180 | * @param clone 181 | */ 182 | static removeProjectFileExtension(files: string | string[], clone = false) { 183 | if (Array.isArray(files)) { 184 | let array; 185 | if (clone) { 186 | array = [...files]; 187 | } else { 188 | array = files; 189 | } 190 | for (let i = 0; i < array.length; i++) { 191 | const file = array[i]; 192 | array[i] = this.removeProjectFileExtension(file) as string; 193 | } 194 | 195 | return array; 196 | } else { 197 | return files.substring(0, files.length - $.config.get('project.fileExtension').length); 198 | } 199 | } 200 | 201 | 202 | /** 203 | * Get extension of path. 204 | */ 205 | static getExtension(path: string, withDot: boolean = true): string | undefined { 206 | const dots = path.split('.'); 207 | return dots.length > 1 ? ((withDot ? '.' : '') + dots.pop()) : undefined; 208 | } 209 | } 210 | 211 | export = PathHelper; 212 | -------------------------------------------------------------------------------- /src/Helpers/String.ts: -------------------------------------------------------------------------------- 1 | class StringHelper { 2 | static upperFirst(str: string) { 3 | return str[0].toUpperCase() + str.substring(1); 4 | } 5 | 6 | static lowerFirst(str: string) { 7 | return str[0].toLowerCase() + str.substring(1); 8 | } 9 | 10 | static hasSuffix(str: string, suffix: string) { 11 | return str.slice(-suffix.length) === suffix; 12 | } 13 | 14 | static withSuffix(str: string, suffix: string) { 15 | if (!this.hasSuffix(str, suffix)) { 16 | str += suffix; 17 | } 18 | return str; 19 | } 20 | 21 | static withoutSuffix(str: string, suffix: string) { 22 | if (this.hasSuffix(str, suffix)) { 23 | str = str.substring(0, str.length - suffix.length); 24 | } 25 | return str; 26 | } 27 | } 28 | 29 | export default StringHelper; 30 | -------------------------------------------------------------------------------- /src/MiddlewareEngine.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | import RequestEngine from "./Plugins/ExtendedRequestEngine"; 3 | import {getInstance} from "../index"; 4 | 5 | const $ = getInstance() 6 | 7 | const projectFileExtension = $.config.get('project.fileExtension', '').substring(1); 8 | /** 9 | * @param {string} middlewarePath 10 | * @param {*} action 11 | * @param route 12 | * @param processOnly 13 | */ 14 | const MiddlewareEngine = (middlewarePath: any, action?: any, route?: any, processOnly = false): any => { 15 | 16 | /** 17 | * if middleware has a dot sing we check if it is project extension file 18 | * if it is not we assume it is a method. 19 | */ 20 | if (middlewarePath.indexOf(".") > 0) { 21 | const m = middlewarePath.split("."); 22 | 23 | if (m[1] !== projectFileExtension) { 24 | middlewarePath = m[0]; 25 | action = m[1]; 26 | } 27 | } 28 | 29 | middlewarePath = lodash.upperFirst(middlewarePath); 30 | 31 | /** 32 | * Get Middleware from path 33 | */ 34 | const middleware = $.use.middleware(middlewarePath, false); 35 | 36 | if (middleware === false) { 37 | return $.logErrorAndExit("Middleware: " + middlewarePath + " not found!"); 38 | } 39 | 40 | if (typeof middleware !== "object" && typeof middleware !== "function") { 41 | return $.logErrorAndExit("No Middleware found in: " + middlewarePath); 42 | } 43 | 44 | /** 45 | * If middleware is object, check if method exists. 46 | */ 47 | if (typeof middleware === "object") { 48 | if (!action) { 49 | action = "allow"; 50 | } 51 | 52 | if (typeof middleware[action] === "undefined") { 53 | return $.logErrorAndExit("MethodWithHttp {" + action + "} not found in middleware: " + middlewarePath); 54 | } 55 | } 56 | 57 | if (processOnly) { 58 | return middleware 59 | } 60 | /** 61 | * Return Parsed Middleware 62 | */ 63 | return RequestEngine.expressify(http => { 64 | try { 65 | if (typeof middleware === "function") { 66 | return middleware(http); 67 | } else { 68 | return middleware[action](http); 69 | } 70 | } catch (e) { 71 | return http.newError(e).view({ 72 | error: { 73 | in: `Middleware: [${middlewarePath}]`, 74 | html: `Error in Middleware ${middlewarePath}`, 75 | log: (e as Error).stack, 76 | }, 77 | }); 78 | } 79 | 80 | }); 81 | }; 82 | 83 | export = MiddlewareEngine; 84 | -------------------------------------------------------------------------------- /src/Objects/consoleColors.obj.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | reset: "\x1b[0m", 3 | bright: "\x1b[1m", 4 | dim: "\x1b[2m", 5 | underscore: "\x1b[4m", 6 | blink: "\x1b[5m", 7 | reverse: "\x1b[7m", 8 | hidden: "\x1b[8m", 9 | 10 | fgBlack: "\x1b[30m", 11 | fgRed: "\x1b[31m", 12 | fgGreen: "\x1b[32m", 13 | fgYellow: "\x1b[33m", 14 | fgBlue: "\x1b[34m", 15 | fgMagenta: "\x1b[35m", 16 | fgCyan: "\x1b[36m", 17 | fgWhite: "\x1b[37m", 18 | 19 | bgBlack: "\x1b[40m", 20 | bgRed: "\x1b[41m", 21 | bgGreen: "\x1b[42m", 22 | bgYellow: "\x1b[43m", 23 | bgBlue: "\x1b[44m", 24 | bgMagenta: "\x1b[45m", 25 | bgCyan: "\x1b[46m", 26 | bgWhite: "\x1b[47m", 27 | }; 28 | -------------------------------------------------------------------------------- /src/On.ts: -------------------------------------------------------------------------------- 1 | /* On Applications Event Engine */ 2 | import {getInstance} from "../index"; 3 | const $ = getInstance(); 4 | 5 | const OnEvents: { [key: string]: any[] } = { 6 | start: [], 7 | boot: [], 8 | expressInit: [], 9 | bootServer: [], 10 | http: [], 11 | https: [], 12 | serverBooted: [] 13 | }; 14 | 15 | /** 16 | * AddToEvents - Short Hand Function 17 | * Adds an event to a given key. 18 | * @param name 19 | * @param todo 20 | * @constructor 21 | */ 22 | const AddToEvents = (name: any, todo: any) => { 23 | if (Array.isArray(todo)) { 24 | for (const key in todo) { 25 | if (todo.hasOwnProperty(key)) { 26 | $.on[name](todo[key]); 27 | } 28 | } 29 | } else { 30 | OnEvents[name].push(todo); 31 | } 32 | }; 33 | 34 | // Initialise $.on 35 | $.on = { 36 | events() { 37 | return OnEvents; 38 | }, 39 | 40 | start(todo) { 41 | return AddToEvents("start", todo); 42 | }, 43 | 44 | boot(todo) { 45 | return AddToEvents("boot", todo); 46 | }, 47 | 48 | expressInit(todo) { 49 | return AddToEvents("expressInit", todo); 50 | }, 51 | 52 | bootServer(todo) { 53 | return AddToEvents("bootServer", todo); 54 | }, 55 | 56 | http(todo) { 57 | return AddToEvents("http", todo); 58 | }, 59 | 60 | https(todo) { 61 | return AddToEvents("https", todo); 62 | }, 63 | 64 | serverBooted(todo) { 65 | return AddToEvents("serverBooted", todo); 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /src/PluginEngine.ts: -------------------------------------------------------------------------------- 1 | import PathHelper from "./Helpers/Path"; 2 | import hasPkg from "has-pkg"; 3 | import {getInstance} from "../index"; 4 | import InXpresserError from "./Errors/InXpresserError"; 5 | import {compareVersion, pluginPathExistOrExit} from "./Functions/plugins.fn"; 6 | 7 | const $ = getInstance(); 8 | 9 | /** 10 | * PluginRoutes - holds all plugins routes. 11 | */ 12 | const pluginRoutes = [] as any[]; 13 | 14 | /** 15 | * PluginNamespaceToData - Holds plugin data using namespaces as keys. 16 | */ 17 | const PluginNamespaceToData: Record = {}; 18 | 19 | 20 | /** 21 | * @class PluginEngine 22 | */ 23 | class PluginEngine { 24 | 25 | /** 26 | * Load plugins and process their use.json, 27 | */ 28 | public static async loadPlugins(PackageDotJson: Record) { 29 | // get logs.plugins config. 30 | const logPlugins = $.config.get("log.plugins", true); 31 | // Hold Plugins 32 | let plugins!: Record)>; 33 | // Get plugins.json path. 34 | const PluginsPath = $.path.jsonConfigs("plugins.json"); 35 | 36 | /** 37 | * Check if plugins.json exists 38 | * if yes, we load plugins else no plugins defined. 39 | */ 40 | if ($.file.exists(PluginsPath)) { 41 | // Try to require PluginsPath 42 | try { 43 | plugins = require(PluginsPath); 44 | } catch (e) { 45 | // Log Error and continue 46 | // Expected: Json Errors 47 | $.logError(e); 48 | } 49 | 50 | if (Array.isArray(plugins) && plugins.length) { 51 | $.logErrorAndExit('plugins.json should be an object.'); 52 | } 53 | 54 | /** 55 | * Load plugins if plugins is an object. 56 | */ 57 | if (typeof plugins === "object") { 58 | // Holds all defined plugin keys 59 | const pluginKeys = Object.keys(plugins); 60 | 61 | // Holds current app env. 62 | const env = $.config.get('env'); 63 | 64 | // Caches plugin paths. 65 | const pluginPaths: Record = {}; 66 | 67 | // Caches plugin Data 68 | const pluginData: Record = {}; 69 | 70 | 71 | /** 72 | * Loop through and process plugins. 73 | * 74 | * We want to log all plugin names before loading them. 75 | * Just in any case plugins have logs it does not interfere with the plugin names list. 76 | * 77 | * Also check if a particular plugin is meant for the environment your project is in. 78 | */ 79 | const loadedPlugins: typeof plugins = {}; 80 | for (const plugin of pluginKeys) { 81 | const pluginUseDotJson = plugins[plugin] 82 | if (typeof pluginUseDotJson === "boolean" && !pluginUseDotJson) 83 | continue; 84 | 85 | if (typeof pluginUseDotJson === "object") { 86 | 87 | if (pluginUseDotJson.hasOwnProperty('load') && 88 | pluginUseDotJson.load === false) { 89 | continue; 90 | } 91 | 92 | if (pluginUseDotJson.hasOwnProperty('env')) { 93 | if (typeof pluginUseDotJson.env === "string" && pluginUseDotJson.env !== env) 94 | continue; 95 | 96 | if (Array.isArray(pluginUseDotJson.env) && !pluginUseDotJson.env.includes(env)) 97 | continue; 98 | } 99 | } 100 | 101 | loadedPlugins[plugin] = pluginUseDotJson; 102 | } 103 | 104 | 105 | /** 106 | * Start Processing loaded plugins 107 | */ 108 | const listOfPluginNamespaces: string[] = []; 109 | for (const plugin of Object.keys(loadedPlugins)) { 110 | // get plugin real path. 111 | const $pluginPath: string = pluginPaths[plugin] = PathHelper.resolve(plugin); 112 | 113 | try { 114 | const $data = pluginData[plugin] = PluginEngine.loadPluginUseData($pluginPath, PackageDotJson); 115 | listOfPluginNamespaces.push($data.namespace); 116 | } catch (e) { 117 | // Throw any error from processing and stop xpresser. 118 | $.logPerLine([ 119 | {error: plugin}, 120 | {error: e}, 121 | {errorAndExit: ""}, 122 | ], true); 123 | } 124 | } 125 | 126 | $.ifNotConsole(() => { 127 | if (logPlugins) { 128 | $.logSuccess(`Using plugins: [${listOfPluginNamespaces.join(', ')}]`) 129 | } else { 130 | const pluginsLength = listOfPluginNamespaces.length; 131 | $.logSuccess(`Using (${pluginsLength}) ${pluginsLength === 1 ? 'plugin' : 'plugins'}`) 132 | } 133 | }) 134 | 135 | 136 | for (const plugin of Object.keys(loadedPlugins)) { 137 | if (plugin.length) { 138 | // get plugin real path. 139 | const $pluginPath: string = pluginPaths[plugin]; 140 | 141 | // Try processing plugin use.json 142 | try { 143 | const $data = pluginData[plugin]; 144 | PluginNamespaceToData[$data.namespace] = await PluginEngine.usePlugin( 145 | plugin, $pluginPath, $data 146 | ); 147 | 148 | // Save to engineData 149 | $.engineData.set("PluginEngine:namespaces", PluginNamespaceToData); 150 | } catch (e) { 151 | // Throw any error from processing and stop xpresser. 152 | $.logPerLine([ 153 | {error: plugin}, 154 | {error: e}, 155 | {errorAndExit: ""}, 156 | ], true); 157 | } 158 | } 159 | } 160 | } else { 161 | $.logWarning('Plugins not loaded! Typeof plugins is expected to be an object.') 162 | } 163 | } 164 | 165 | // return processed plugin routes. 166 | return {routes: pluginRoutes}; 167 | } 168 | 169 | /** 170 | * Read plugins use.json 171 | * @param pluginPath 172 | * @param PackageDotJson 173 | */ 174 | public static loadPluginUseData(pluginPath: string, PackageDotJson: Record) { 175 | const data = require(pluginPath + "/use.json"); 176 | if (!data.namespace) { 177 | throw new InXpresserError(`Cannot read property 'namespace'`); 178 | } 179 | 180 | /** 181 | * Version checker 182 | */ 183 | if (data.xpresser) { 184 | let version = data.xpresser; 185 | const xpresserVersion = PackageDotJson.version; 186 | 187 | const compareWith = version.substring(0, 2); 188 | version = data.xpresser.substring(2); 189 | 190 | 191 | if (compareWith === ">=" && compareVersion(xpresserVersion, version) === -1) { 192 | $.logErrorAndExit( 193 | `Plugin: [${data.namespace}] requires xpresser version [${compareWith + version}],\nUpgrade xpresser to continue.` 194 | ); 195 | } else if ( 196 | compareWith === "<=" && 197 | compareVersion(version, xpresserVersion) === -1 198 | ) { 199 | $.logErrorAndExit( 200 | `Plugin: [${data.namespace}] requires xpresser version [${compareWith + version}],\nDowngrade xpresser to continue.` 201 | ); 202 | } 203 | } 204 | 205 | return data; 206 | } 207 | 208 | /** 209 | * Process Plugin use.json 210 | * @param plugin 211 | * @param path 212 | * @param data 213 | */ 214 | public static async usePlugin(plugin: string, path: string, data: object) { 215 | const $data = $.objectCollection(data); 216 | let pluginData: any; 217 | 218 | // Set plugin Data 219 | pluginData = { 220 | namespace: $data.get("namespace"), 221 | plugin, 222 | path, 223 | paths: {}, 224 | }; 225 | 226 | // check if plugin has routesFile 227 | if ($data.has("paths.routesFile")) { 228 | let RoutePath: any = $data.get("paths.routesFile"); 229 | 230 | RoutePath = pluginPathExistOrExit(plugin, path, RoutePath); 231 | 232 | pluginRoutes.push({plugin, path: RoutePath}); 233 | } 234 | 235 | // check if plugin use.json has paths.controllers 236 | if ($data.has("paths.controllers")) { 237 | let controllerPath: any = $data.get("paths.controllers"); 238 | controllerPath = pluginPathExistOrExit(plugin, path, controllerPath); 239 | pluginData.paths.controllers = controllerPath; 240 | } 241 | 242 | 243 | $.ifNotConsole(() => { 244 | // check if plugin use.json has paths.views 245 | if ($data.has("paths.views")) { 246 | let viewsPath: any = $data.get("paths.views"); 247 | viewsPath = pluginPathExistOrExit(plugin, path, viewsPath); 248 | pluginData.paths.views = viewsPath; 249 | } 250 | 251 | // check if plugin use.json has paths.middlewares 252 | if ($data.has("paths.middlewares")) { 253 | let middlewarePath: any = $data.get("paths.middlewares"); 254 | middlewarePath = pluginPathExistOrExit(plugin, path, middlewarePath); 255 | pluginData.paths.middlewares = middlewarePath; 256 | } 257 | 258 | // check if plugin use.json has extends 259 | if ($data.has("extends")) { 260 | const extensionData: Record = {}; 261 | if ($data.has("extends.RequestEngine")) { 262 | const extenderPath = $data.get("extends.RequestEngine"); 263 | extensionData["RequestEngine"] = pluginPathExistOrExit(plugin, path, extenderPath); 264 | } 265 | 266 | pluginData.extends = extensionData; 267 | } 268 | 269 | }); 270 | 271 | $.ifIsConsole(() => { 272 | 273 | if ($data.has('publishable')) { 274 | pluginData.publishable = $data.get('publishable') 275 | } 276 | 277 | if ($data.has('importable')) { 278 | pluginData.publishable = $data.get('importable') 279 | } 280 | 281 | // check if plugin use.json has paths.Commands only if console 282 | if ($data.has("paths.commands")) { 283 | let commandPath: any = $data.get("paths.commands"); 284 | commandPath = pluginPathExistOrExit(plugin, path, commandPath); 285 | pluginData.paths.commands = commandPath; 286 | 287 | const cliCommandsPath = path + "/cli-commands.json"; 288 | 289 | if ($.file.isFile(cliCommandsPath)) { 290 | pluginData.commands = {}; 291 | 292 | const cliCommands = require(cliCommandsPath); 293 | if (cliCommands && Array.isArray(cliCommands)) { 294 | for (const command of cliCommands) { 295 | let commandAction = command['action']; 296 | 297 | if (!commandAction) { 298 | commandAction = command.command.split(" ")[0]; 299 | } 300 | 301 | pluginData.commands[commandAction] = pluginPathExistOrExit(plugin, commandPath, command.file); 302 | } 303 | } 304 | } 305 | } 306 | }); 307 | 308 | 309 | // check if plugin uses index file. 310 | const useIndex = $data.get('use_index', false); 311 | if (useIndex) { 312 | 313 | /** 314 | * Check for typescript plugins. 315 | * 316 | * If in typescript mode we check for base file. 317 | */ 318 | const isCustomFile = typeof useIndex === "string"; 319 | const pluginIndexFile = isCustomFile ? useIndex : 'index.js'; 320 | const indexFilePath: void | string = pluginPathExistOrExit(plugin, path, pluginIndexFile); 321 | 322 | if (indexFilePath) { 323 | /** 324 | * Run plugin indexFile. 325 | */ 326 | const {run, dependsOn} = require(indexFilePath); 327 | 328 | // check for packages plugin dependsOn 329 | if (dependsOn && typeof dependsOn === "function") { 330 | let pluginDependsOn: string[] | undefined = await dependsOn(pluginData, $); 331 | 332 | // Validate function return type. 333 | if (!pluginDependsOn || !Array.isArray(pluginDependsOn)) 334 | return $.logErrorAndExit(`dependsOn() function for plugin {${pluginData.namespace}} must return an array of packages.`) 335 | 336 | 337 | // Log warning for missing required packages. 338 | if (pluginDependsOn.length) { 339 | let missingPkgs = 0; 340 | 341 | // Loop through and check packages. 342 | pluginDependsOn.forEach(pkg => { 343 | 344 | // Show warning for every missing package. 345 | if (!hasPkg(pkg)) { 346 | 347 | // Intro log. 348 | if (missingPkgs === 0) 349 | $.logError(`Plugin: (${pluginData.namespace}) requires the following dependencies:`) 350 | 351 | console.log(`- ${pkg}`); 352 | 353 | missingPkgs++; 354 | } 355 | }) 356 | 357 | // Stop if missing package 358 | if (missingPkgs) 359 | return $.logErrorAndExit(`Install required ${missingPkgs > 1 ? 'dependencies' : 'dependency'} and restart server.`) 360 | } 361 | 362 | } 363 | 364 | 365 | /** 366 | * Call Run function. 367 | */ 368 | if (run && typeof run === "function") await run(pluginData, $); 369 | } 370 | } 371 | 372 | // return processed Plugin Data 373 | return pluginData; 374 | } 375 | 376 | } 377 | 378 | export = PluginEngine; 379 | -------------------------------------------------------------------------------- /src/Plugins/ExtendedRequestEngine.ts: -------------------------------------------------------------------------------- 1 | import RequestEngine from "../RequestEngine"; 2 | import {getInstance} from "../../index"; 3 | import PathHelper from "../Helpers/Path"; 4 | import type ObjectCollection from "object-collection"; 5 | 6 | 7 | const $ = getInstance(); 8 | let ExtendedRequestEngine = RequestEngine; 9 | 10 | // Set Request Engine Getter 11 | $.engineData.set("ExtendedRequestEngine", () => ExtendedRequestEngine); 12 | 13 | // Set extended request engine getter. 14 | $.extendedRequestEngine = () => { 15 | return $.engineData.get("ExtendedRequestEngine")(); 16 | } 17 | 18 | const PluginNameSpaces: Record = $.engineData.get("PluginEngine:namespaces", {}); 19 | const useDotJson: ObjectCollection = $.engineData.get("UseDotJson"); 20 | 21 | /** 22 | * Extend RequestEngine 23 | */ 24 | const ExtendRequestEngineUsing = (extender: ($class: any) => any) => { 25 | if (typeof extender === "function") { 26 | /** 27 | * Since we can't differentiate between a class and a function 28 | * we need to check if the extender has a static function called `expressify` 29 | * which exists on the default RequestEngine. 30 | */ 31 | if (typeof (extender as unknown as typeof RequestEngine).expressify === "function") { 32 | ExtendedRequestEngine = extender as unknown as typeof RequestEngine; 33 | } else { 34 | /** 35 | * The case of Extenders returning a function that returns a class 36 | * Maybe deprecated in future since the introduction of $.extendedRequestEngine() 37 | */ 38 | ExtendedRequestEngine = extender(ExtendedRequestEngine); 39 | } 40 | } else { 41 | throw new Error("Custom RequestEngine extender must be a function or an extended RequestEngine class."); 42 | } 43 | }; 44 | 45 | const RequireOrFail = ($RequestEngine: any, plugin?: any) => { 46 | try { 47 | $RequestEngine = PathHelper.resolve($RequestEngine); 48 | ExtendRequestEngineUsing(require($RequestEngine)); 49 | } catch (e) { 50 | $.logPerLine([ 51 | plugin === undefined ? {} : {error: `Error in plugin: ${plugin}`}, 52 | {error: (e as Error).stack}, 53 | {errorAndExit: ""}, 54 | ]); 55 | } 56 | }; 57 | 58 | /** 59 | * Load Plugin Request Engines 60 | */ 61 | const pluginNamespaceKeys = Object.keys(PluginNameSpaces); 62 | 63 | for (let k = 0; k < pluginNamespaceKeys.length; k++) { 64 | const pluginNamespaceKey = pluginNamespaceKeys[k]; 65 | const plugin = $.objectCollection(PluginNameSpaces[pluginNamespaceKey]); 66 | 67 | if (plugin.has("extends.RequestEngine")) { 68 | const requestEngineExtender = plugin.get("extends.RequestEngine"); 69 | RequireOrFail(requestEngineExtender, pluginNamespaceKey); 70 | } 71 | } 72 | 73 | /** 74 | * Load User defined request Engine 75 | */ 76 | const userRequestExtension = useDotJson.get("extends.RequestEngine", false); 77 | if (userRequestExtension) { 78 | if (Array.isArray(userRequestExtension)) { 79 | for (const extension of userRequestExtension) { 80 | RequireOrFail(extension); 81 | } 82 | } else { 83 | RequireOrFail(userRequestExtension); 84 | } 85 | } 86 | 87 | export = ExtendedRequestEngine; 88 | -------------------------------------------------------------------------------- /src/Plugins/Installer.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import lodash from "lodash"; 3 | import {getInstance} from "../../index"; 4 | import moment from "moment"; 5 | import PathHelper from "../Helpers/Path"; 6 | 7 | const $ = getInstance(); 8 | 9 | const PluginLockDataPath: string = $.path.jsonConfigs("plugins-lock.json"); 10 | let PluginLockData = $.objectCollection(); 11 | let UpdatePluginLockData = false; 12 | 13 | PathHelper.makeDirIfNotExist(PluginLockDataPath, true); 14 | if (fs.existsSync(PluginLockDataPath)) { 15 | const lockData = require(PluginLockDataPath); 16 | PluginLockData = $.objectCollection(lockData); 17 | } 18 | 19 | export = ($plugin: string) => { 20 | // Get all loaded plugin namespace 21 | const $pluginNamespaces: any = $.engineData.get("PluginEngine:namespaces", {}); 22 | const $pluginNamespaceKeys = Object.keys($pluginNamespaces); 23 | let $pluginData: any = null; 24 | 25 | // Loop Through all to find exact plugin to install 26 | for (let i = 0; i < $pluginNamespaceKeys.length; i++) { 27 | const $pluginNamespace = $pluginNamespaces[$pluginNamespaceKeys[i]]; 28 | if ($pluginNamespace.plugin === $plugin) { 29 | $pluginData = $pluginNamespace; 30 | break; 31 | } 32 | } 33 | 34 | if ($pluginData === null) { 35 | return $.logErrorAndExit(`Plugin: ${$plugin} not found.`); 36 | } 37 | 38 | if (!PluginLockData.has($plugin)) { 39 | PluginLockData.set($plugin, {}); 40 | } 41 | 42 | const $thisPluginLockData = PluginLockData.newInstanceFrom($plugin); 43 | if ($thisPluginLockData.get("installed", false)) { 44 | return $.logPerLine([ 45 | {info: `Plugin: ${$plugin} is already installed!`}, 46 | {errorAndExit: ""}, 47 | ]); 48 | } 49 | 50 | $.logInfo(`Installing ${$plugin}...`); 51 | if ($pluginData.hasOwnProperty("migrations")) { 52 | 53 | const $migrationLockData = $thisPluginLockData.newInstanceFrom("migrations"); 54 | const $migrationsFolder: string = $pluginData.migrations; 55 | const $migrationFiles = fs.readdirSync($migrationsFolder); 56 | 57 | if ($migrationFiles.length) { 58 | PathHelper.makeDirIfNotExist($.path.base("migrations")); 59 | } 60 | 61 | for (let i = 0; i < $migrationFiles.length; i++) { 62 | const $migrationFile = $migrationFiles[i]; 63 | const $splitMigrationFileName = $migrationFile.split("_"); 64 | let $newMigrationFilePath = $migrationFile; 65 | 66 | if (!isNaN(Number($splitMigrationFileName[0]))) { 67 | 68 | $splitMigrationFileName[0] = moment(new Date()) 69 | .format("YMMDHmmss") 70 | .toString() + "_(" + $pluginData.namespace.toLowerCase() + ")"; 71 | } 72 | 73 | const $newMigrationFile = $splitMigrationFileName.join("_"); 74 | $newMigrationFilePath = $.path.base("migrations/" + $newMigrationFile); 75 | const $migrationFileFullPath = $migrationsFolder + "/" + $migrationFile; 76 | 77 | if (!$migrationLockData.has($newMigrationFile)) { 78 | fs.copyFileSync($migrationFileFullPath, $newMigrationFilePath); 79 | $.logInfo(`Moved migration: ${$newMigrationFile}`); 80 | 81 | const $data = { 82 | migrations: { 83 | [$migrationFile]: $newMigrationFile, 84 | }, 85 | }; 86 | 87 | $thisPluginLockData.merge($data); 88 | UpdatePluginLockData = true; 89 | } 90 | } 91 | } 92 | 93 | if ($pluginData.hasOwnProperty("models")) { 94 | const $models = fs.readdirSync($pluginData.models); 95 | 96 | for (let i = 0; i < $models.length; i++) { 97 | const $modelFile = $models[i]; 98 | let $model = $models[i]; 99 | const $newModel = lodash.upperFirst($pluginData.namespace) + "/" + $model; 100 | $model = $pluginData.models + "/" + $model; 101 | 102 | const $newModelFullPath = $.path.models($newModel); 103 | 104 | PathHelper.makeDirIfNotExist($newModelFullPath, true); 105 | 106 | if (!fs.existsSync($newModelFullPath)) { 107 | fs.copyFileSync($model, $newModelFullPath); 108 | } 109 | 110 | $.logInfo(`Moved model: ${$newModel}`); 111 | 112 | const $data = { 113 | models: { 114 | [$modelFile]: $newModel, 115 | }, 116 | }; 117 | 118 | $thisPluginLockData.merge($data); 119 | UpdatePluginLockData = true; 120 | } 121 | } 122 | 123 | const $pluginIsInstalled = $thisPluginLockData.get("installed", false); 124 | if (!$pluginIsInstalled) { 125 | try { 126 | const pluginInit: any = require($pluginData.path); 127 | if (typeof pluginInit.install === "function") { 128 | pluginInit.install(); 129 | } 130 | 131 | $thisPluginLockData.set("installed", true); 132 | UpdatePluginLockData = true; 133 | } catch (e) { 134 | $.logError($plugin); 135 | $.logErrorAndExit(e); 136 | } 137 | } 138 | 139 | if (UpdatePluginLockData) { 140 | fs.writeFileSync(PluginLockDataPath, JSON.stringify(PluginLockData.all(), null, 2)); 141 | $.logInfo("Updated plugins-lock.json"); 142 | } 143 | 144 | $.logInfo("Installation Complete!"); 145 | }; 146 | -------------------------------------------------------------------------------- /src/RouterEngine.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | import XpresserRouter from "@xpresser/router"; 3 | import {getInstance} from "../index"; 4 | import {parseControllerString} from "./Functions/internals.fn"; 5 | import XpresserRoute from "@xpresser/router/src/XpresserRoute"; 6 | import XpresserPath from "@xpresser/router/src/XpresserPath"; 7 | import {RouteData, RoutePathData} from "@xpresser/router/src/custom-types"; 8 | import {pathToUrl} from "./Functions/router.fn"; 9 | import PathHelper from "./Helpers/Path"; 10 | import MiddlewareEngine from "./MiddlewareEngine"; 11 | 12 | 13 | const $ = getInstance(); 14 | 15 | // EngineDate Store Key for all processed routes. 16 | const AllRoutesKey = "RouterEngine:allRoutes"; 17 | 18 | // Name to route records memory cache. 19 | const NameToRoute: Record = {}; 20 | 21 | // Processed routes records memory cache. 22 | const ProcessedRoutes: (RouteData & { url: string })[] = []; 23 | 24 | // Resolved Controller Names records memory cache. 25 | const ControllerStringCache: Record = {}; 26 | 27 | /** 28 | * RouterEngine Class. 29 | * Handles processing of routes. 30 | */ 31 | class RouterEngine { 32 | /** 33 | * Get All Registered Routes 34 | * @returns {*} 35 | */ 36 | public static allRoutes(): (XpresserRoute | XpresserPath)[] { 37 | return $.engineData.get(AllRoutesKey); 38 | } 39 | 40 | /** 41 | * Add Routes to already set routes 42 | * @param route 43 | */ 44 | public static addToRoutes(route: XpresserRouter) { 45 | if (typeof route.routes !== "undefined" && Array.isArray(route.routes)) { 46 | const allRoutes = $.router.routes; 47 | $.router.routes = lodash.concat(allRoutes, route.routes); 48 | 49 | $.engineData.set(AllRoutesKey, $.router.routes); 50 | } 51 | } 52 | 53 | /** 54 | * Get All Processed Routes 55 | * @returns {*} 56 | */ 57 | public static allProcessedRoutes($format?: string, $key: string = "path") { 58 | if ($format === "array") { 59 | const routesArray: any[] = []; 60 | 61 | for (let i = 0; i < ProcessedRoutes.length; i++) { 62 | 63 | const processedRoute = ProcessedRoutes[i]; 64 | 65 | const routeArray = [ 66 | processedRoute.method!.toUpperCase(), 67 | processedRoute.path, 68 | processedRoute.name || null, 69 | ]; 70 | 71 | routesArray.push(routeArray); 72 | } 73 | 74 | return routesArray; 75 | } else if ($format === "key") { 76 | const routesArray: any[] = []; 77 | 78 | for (let i = 0; i < ProcessedRoutes.length; i++) { 79 | const processedRoute = ProcessedRoutes[i]; 80 | routesArray.push((processedRoute as any)[$key]); 81 | } 82 | 83 | return routesArray; 84 | } 85 | return ProcessedRoutes; 86 | } 87 | 88 | /** 89 | * @private 90 | * @param format 91 | * @returns {string} 92 | */ 93 | public static namedRoutes(format: boolean | string = false) { 94 | if (format !== false) { 95 | const names = Object.keys(NameToRoute); 96 | const newFormat: Record = {}; 97 | 98 | for (let i = 0; i < names.length; i++) { 99 | const name = names[i]; 100 | const route = NameToRoute[name]; 101 | 102 | newFormat[route.method + " " + route.path] = "{" + route.name + "} ===> " + route.controller; 103 | 104 | } 105 | 106 | // noinspection JSValidateTypes 107 | if (typeof format === "string" && format === "json") { 108 | return JSON.stringify(newFormat, null, 2); 109 | } 110 | 111 | return newFormat; 112 | } 113 | return NameToRoute; 114 | } 115 | 116 | /** 117 | * NameToPath 118 | * @param returnKey 119 | * @return {Object} 120 | */ 121 | public static nameToPath(returnKey = "path") { 122 | const localVariableName = "RouterEngine:nameToPath"; 123 | 124 | if ($.engineData.has(localVariableName)) { 125 | return $.engineData.get>(localVariableName); 126 | } 127 | 128 | const names = Object.keys(NameToRoute); 129 | const newRoutes: Record = {}; 130 | 131 | for (let i = 0; i < names.length; i++) { 132 | const name = names[i]; 133 | newRoutes[name] = NameToRoute[name][returnKey]; 134 | } 135 | 136 | if (returnKey !== "path") { 137 | return newRoutes; 138 | } 139 | 140 | $.engineData.set(localVariableName, newRoutes); 141 | 142 | return newRoutes; 143 | } 144 | 145 | /** 146 | * NameToUrl 147 | * @return {Object} 148 | */ 149 | public static nameToUrl() { 150 | const localVariableName = "RouterEngine:nameToUrl"; 151 | 152 | if ($.engineData.has(localVariableName)) { 153 | return $.engineData.get(localVariableName); 154 | } 155 | 156 | const routes = RouterEngine.nameToPath(); 157 | const names = Object.keys(routes); 158 | const newRoutes: Record = {}; 159 | 160 | for (let i = 0; i < names.length; i++) { 161 | const name = names[i]; 162 | newRoutes[name] = $.helpers.route(name, [], false); 163 | } 164 | 165 | $.engineData.set(localVariableName, newRoutes); 166 | return newRoutes; 167 | } 168 | 169 | /** 170 | * Process Routes 171 | * @param routes 172 | * @param parent 173 | */ 174 | public static processRoutes(routes: (XpresserRoute | XpresserPath)[] | null = null, parent?: RoutePathData) { 175 | const Controller = require("./ControllerEngine"); 176 | 177 | if (!Array.isArray(routes)) { 178 | routes = RouterEngine.allRoutes(); 179 | } 180 | 181 | for (let i = 0; i < routes.length; i++) { 182 | let route = routes[i].data as RouteData; 183 | let routeAsPath = Array.isArray((route as RoutePathData).children) ? route as RoutePathData : false; 184 | let nameWasGenerated = false; 185 | 186 | /* 187 | * If Route has children (meaning it is a Group/Path), 188 | * and also has a parent with children, it extends the parent. 189 | * 190 | * This means if a child of a route is a Group/Path and does not have controller set 191 | * it automatically inherits the parent controller 192 | * 193 | * e.g 194 | * Route.path('/api', () => { 195 | * // Another Path here 196 | * 197 | * Route.path('user', ()=> { 198 | * // Some Routes 199 | * }); 200 | * 201 | * // The path above i.e "/api/user" will inherit the parent 202 | * // Route controller and its other properties unless it has it's own defined. 203 | * 204 | * }).controller('Auth').as('auth'); 205 | */ 206 | if (routeAsPath && parent) { 207 | 208 | if (parent.children) { 209 | // tslint:disable-next-line:max-line-length 210 | if (typeof routeAsPath.as === "string" && typeof parent.as === "string" && routeAsPath.as.substring(0, 1) === ".") { 211 | routeAsPath.as = parent.as + routeAsPath.as; 212 | } 213 | 214 | // Mutates both route and routeAsPath 215 | lodash.defaults(routeAsPath, { 216 | as: parent.as, 217 | controller: parent.controller, 218 | useActionsAsName: parent.useActionsAsName, 219 | }) 220 | } 221 | } 222 | 223 | if (typeof route.controller === "string") { 224 | if (!routeAsPath && (parent?.useActionsAsName) && !route.name) { 225 | let nameFromController = route.controller; 226 | if (nameFromController.includes("@")) { 227 | let splitName = nameFromController.split("@"); 228 | nameFromController = splitName[splitName.length - 1]; 229 | } 230 | 231 | route.name = lodash.snakeCase(nameFromController); 232 | nameWasGenerated = true; 233 | } 234 | } 235 | 236 | if ((parent?.as) && typeof route.name === "string" && route.name.substring(0, 1) !== "/") { 237 | if (route.path === "" && nameWasGenerated) { 238 | route.name = parent.as; 239 | } else { 240 | route.name = parent.as + "." + route.name; 241 | } 242 | } 243 | 244 | if (route.name) { 245 | if (route.name.substring(0, 1) === "/") { 246 | route.name = route.name.substring(1); 247 | } 248 | route.name = route.name.toLowerCase(); 249 | } 250 | 251 | // tslint:disable-next-line:max-line-length 252 | if (!routeAsPath && (parent?.controller) && typeof route.controller === "string" && !route.controller.includes("@")) { 253 | route.controller = parent.controller + "@" + route.controller; 254 | } 255 | 256 | if (parent?.path) { 257 | const routePath = $.utils.regExpSourceOrString(route.path as string); 258 | const parentPath = $.utils.regExpSourceOrString(parent.path as string); 259 | 260 | if (route.path instanceof RegExp || parent.path instanceof RegExp) { 261 | 262 | route.path = new RegExp(`${parentPath}${parentPath !== "/" ? "/" : ""}${routePath}`); 263 | 264 | } else { 265 | 266 | if (routePath.length && parentPath.slice(-1) !== "/" && routePath.substring(0, 1) !== "/") { 267 | route.path = "/" + routePath; 268 | } 269 | 270 | route.path = parent.path as string + route.path; 271 | } 272 | 273 | } 274 | 275 | if (typeof route.path === "string" && route.path.substring(0, 2) === "//") { 276 | route.path = route.path.substring(1); 277 | } 278 | 279 | if (typeof route.name !== "undefined") { 280 | NameToRoute[route.name] = route; 281 | } 282 | 283 | if (!routeAsPath && typeof route.controller === "string" && route.controller.includes("@")) { 284 | let {method, controller} = parseControllerString(route.controller); 285 | const controllerCacheKey = controller; 286 | 287 | if (ControllerStringCache.hasOwnProperty(controllerCacheKey)) { 288 | route.controller = ControllerStringCache[controllerCacheKey] + "@" + method; 289 | } else { 290 | let isTypescriptButUsingJsExtension = false; 291 | 292 | let controllerPath = $.use.controller( 293 | PathHelper.addProjectFileExtension(controller) as string 294 | ); 295 | 296 | if (!$.file.exists(controllerPath)) { 297 | if (!controller.includes("Controller")) { 298 | 299 | controllerPath = $.use.controller( 300 | PathHelper.addProjectFileExtension(controller + "Controller") as string 301 | ); 302 | 303 | if (!$.file.exists(controllerPath)) { 304 | /** 305 | * Check If is using typescript and plugin requires a js file. 306 | */ 307 | controllerPath = $.use.controller( 308 | PathHelper.addProjectFileExtension(controller + "Controller", '.js') as string 309 | ); 310 | 311 | if ($.isTypescript() && $.file.exists(controllerPath)) { 312 | isTypescriptButUsingJsExtension = true; 313 | } 314 | 315 | if (!isTypescriptButUsingJsExtension) { 316 | $.logError("Controller: " + [controller, method].join("@") + " not found"); 317 | $.logErrorAndExit('Path:', controllerPath) 318 | } 319 | } 320 | 321 | controller = controller + "Controller"; 322 | } 323 | } 324 | 325 | if (isTypescriptButUsingJsExtension) { 326 | controller = controller + '.js'; 327 | } 328 | 329 | ControllerStringCache[controllerCacheKey] = controller; 330 | route.controller = controller + "@" + method; 331 | } 332 | } 333 | 334 | const canRegisterRoutes = ($.app && (!$.options.isTinker && !$.options.isConsole)); 335 | 336 | if (routeAsPath) { 337 | if (canRegisterRoutes) { 338 | const RegisterMiddleware = (middleware: string) => { 339 | const PathMiddleware = MiddlewareEngine(middleware); 340 | if (PathMiddleware) { 341 | $.app!.use((routeAsPath as RoutePathData).path as string, PathMiddleware); 342 | } 343 | }; 344 | 345 | if (Array.isArray(routeAsPath.middleware)) { 346 | routeAsPath.middleware.forEach((element: any) => { 347 | RegisterMiddleware(element); 348 | }); 349 | } else if (typeof routeAsPath.middleware === "string") { 350 | RegisterMiddleware(routeAsPath.middleware); 351 | } 352 | } 353 | 354 | if (routeAsPath.children!.length) { 355 | RouterEngine.processRoutes(routeAsPath.children as any, routeAsPath); 356 | } 357 | } else { 358 | // Add to all Routes 359 | ProcessedRoutes.push({ 360 | url: pathToUrl(route.path as string), 361 | ...route, 362 | }); 363 | 364 | if (canRegisterRoutes) { 365 | const controller = Controller(route); 366 | // @ts-ignore 367 | $.app[route.method](route.path, controller.middlewares, controller.method); 368 | } 369 | } 370 | } 371 | } 372 | } 373 | 374 | export = RouterEngine; 375 | -------------------------------------------------------------------------------- /src/Routes/Loader.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import {getInstance} from "../../index"; 3 | import RouterEngine from "../RouterEngine"; 4 | import Path from "../Helpers/Path"; 5 | 6 | const $ = getInstance(); 7 | 8 | $.routerEngine = RouterEngine; 9 | const RouteFile = Path.resolve($.config.get('paths.routesFile')); 10 | 11 | if (fs.existsSync(RouteFile)) { 12 | try { 13 | require(RouteFile); 14 | } catch (e) { 15 | $.logPerLine([ 16 | {error: "Router Error:"}, 17 | {errorAndExit: e}, 18 | ]); 19 | } 20 | } 21 | 22 | // Import plugin routes 23 | const PluginData = $.engineData.get("PluginEngineData"); 24 | const PluginRoutes = PluginData.routes; 25 | 26 | for (let i = 0; i < PluginRoutes.length; i++) { 27 | const pluginRoute = PluginRoutes[i]; 28 | const Routes = require(pluginRoute.path); 29 | 30 | // Add to routes if returned value is getInstance of XpresserRouter 31 | if ( 32 | typeof Routes === "object" && 33 | (Routes.constructor && Routes.constructor.name === "XpresserRouter") 34 | ) { 35 | $.routerEngine.addToRoutes(Routes); 36 | } 37 | } 38 | 39 | if (typeof $.router.routesAfterPlugins === "function") { 40 | $.router.routesAfterPlugins(); 41 | } 42 | -------------------------------------------------------------------------------- /src/StartConsole.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import {getInstance} from "../index"; 4 | import PathHelper from "./Helpers/Path"; 5 | import JobHelper from "./Console/JobHelper"; 6 | import Console from "./Console/Commands"; 7 | 8 | 9 | const {Commands, Artisan}: { Commands: Record, Artisan: Record } = Console; 10 | const DefinedCommands: Record = {}; 11 | const $ = getInstance(); 12 | 13 | // Get Command Arguments 14 | const cloneArgs = [...process.argv]; 15 | const args: any[] = cloneArgs.splice(3); 16 | 17 | // Check if command is from xjs-cli 18 | if (args[args.length - 1] === "--from-xjs-cli") { 19 | 20 | // if true, set isFromXjsCli to true, 21 | $.options.isFromXjsCli = true; 22 | 23 | // remove --from-xjs-cli from args and process.argv 24 | args.length = args.length - 1; 25 | process.argv.length = process.argv.length - 1; 26 | } 27 | 28 | // Require Artisan helper Functions 29 | let argCommand: string = args[0]; 30 | 31 | // return error if no command is defined. 32 | if (typeof argCommand === "undefined") { 33 | $.logErrorAndExit("No command provided!"); 34 | } 35 | 36 | // Trim argCommand 37 | argCommand = argCommand.trim(); 38 | 39 | let isJobCommand = argCommand.substring(0, 1) === "@"; 40 | 41 | /* 42 | If default commands does not have `argCommand` 43 | Then assume this command: 44 | 1. is a plugin command, so load plugins commands 45 | 2. is a job. 46 | */ 47 | if (isJobCommand || !Commands.hasOwnProperty(argCommand)) { 48 | 49 | /* 50 | load plugins commands first since defined jobs may call plugin commands. 51 | */ 52 | const PluginData: any = $.engineData.get("PluginEngine:namespaces", {}); 53 | const plugins = Object.keys(PluginData); 54 | 55 | for (const plugin of plugins) { 56 | const $plugin: Record = PluginData[plugin]; 57 | 58 | if ($plugin.hasOwnProperty("commands")) { 59 | const commands: Record = $plugin["commands"]; 60 | const commandKeys = Object.keys(commands); 61 | 62 | for (const command of commandKeys) { 63 | Commands[command] = commands[command]; 64 | } 65 | } 66 | } 67 | 68 | // Load Job if command has `@` sign before it. 69 | if (isJobCommand) { 70 | argCommand = argCommand.substring(1); 71 | let jobPath = $.path.backend(`jobs/${argCommand}`); 72 | // Add project extension if not exists. 73 | jobPath = PathHelper.addProjectFileExtension(jobPath); 74 | 75 | // Check if job file exists 76 | if (!fs.existsSync(jobPath)) { 77 | $.logErrorAndExit(`Job: (${argCommand}) does not exist.`); 78 | } 79 | 80 | /* 81 | * Require Job and read its configurations. 82 | */ 83 | const job = require(jobPath); 84 | if (typeof job !== "object") { 85 | $.logErrorAndExit("Job: {" + argCommand + "} did not return object!"); 86 | 87 | if (!job.hasOwnProperty("handler")) { 88 | $.logErrorAndExit("Job: {" + argCommand + "} is not structured properly!"); 89 | } 90 | } 91 | 92 | DefinedCommands[argCommand] = job; 93 | } 94 | } 95 | 96 | if (typeof Commands[argCommand] === "undefined" && typeof DefinedCommands[argCommand] === "undefined") { 97 | 98 | if ($.options.isTinker) { 99 | $.log(`Console Command not found: {${argCommand}}`); 100 | } else { 101 | $.logAndExit(`Command not found: {${argCommand}}`); 102 | } 103 | 104 | } else { 105 | const jobHelper = new JobHelper(argCommand); 106 | 107 | // Send only command args to function 108 | args.splice(0, 1); 109 | 110 | if (typeof Commands[argCommand] === "function") { 111 | // Run Command 112 | if (argCommand === 'routes') { 113 | require("./Routes/Loader"); 114 | // Register Routes 115 | $.routerEngine.processRoutes($.router.routes); 116 | } 117 | 118 | Commands[argCommand](args, jobHelper); 119 | 120 | } else if (typeof Commands[argCommand] === "string") { 121 | 122 | const command = require(Commands[argCommand]); 123 | command(args, {artisan: Artisan, helper: jobHelper}); 124 | 125 | } else if (typeof DefinedCommands[argCommand] === "object") { 126 | 127 | const command: { 128 | use?: { 129 | events: boolean, 130 | routes: boolean, 131 | }, 132 | command?: string, 133 | schedule?: string, 134 | handler: (...args: any[]) => (any | void), 135 | } = DefinedCommands[argCommand]; 136 | 137 | if (typeof command.handler !== "function") { 138 | $.logAndExit(`Command: {${argCommand}} has no handler method`); 139 | } 140 | 141 | if (command.use && typeof command.use === "object") { 142 | const {use} = command; 143 | 144 | // Load Events if use.events 145 | if (use.events) { 146 | require("./Events/Loader"); 147 | } 148 | 149 | // Load and Process Routes if use.routes 150 | if (use.routes) { 151 | require("./Routes/Loader"); 152 | // Register Routes 153 | $.routerEngine.processRoutes($.router.routes); 154 | } 155 | } 156 | 157 | // Run Command 158 | try { 159 | command.handler(args, jobHelper); 160 | } catch (e) { 161 | $.logPerLine([ 162 | {error: `Error in command: {${argCommand}}`}, 163 | {errorAndExit: e}, 164 | ]); 165 | } 166 | 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/UseEngine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * UseEngine provides methods for including 3 | * different types of files. 4 | * 5 | * UseEngine is later exposed to the framework as $.use 6 | */ 7 | import PathHelper from "./Helpers/Path"; 8 | import {JsonSettings} from "../types"; 9 | import {getInstance} from "../index"; 10 | import InXpresserError from "./Errors/InXpresserError"; 11 | import StringHelper from "./Helpers/String"; 12 | 13 | const $ = getInstance(); 14 | 15 | let Use: JsonSettings.Use; 16 | const PluginNamespaces: any = $.engineData.get("PluginEngine:namespaces", {}); 17 | const projectFileExtension = $.config.get('project.fileExtension'); 18 | 19 | /** 20 | * UseEngine requires `use.json` in frameworks backend folder. 21 | * Object returned from use.json is processed and saved in $.engineData as path to file. 22 | * @type {{}} 23 | */ 24 | const UsePath = "UseDotJson"; 25 | Use = $.engineData.get( 26 | UsePath, 27 | $.objectCollection(), 28 | ).all(); 29 | 30 | // Process Use Data 31 | if (typeof Use.middlewares === "object") { 32 | const MiddlewareSuffix = "Middleware"; 33 | const useMiddlewares = Use.middlewares; 34 | const middlewareKeys: string[] = Object.keys(useMiddlewares); 35 | 36 | for (let i = 0; i < middlewareKeys.length; i++) { 37 | const middlewareKey: string = middlewareKeys[i]; 38 | 39 | let middleware: string = useMiddlewares[middlewareKey]; 40 | 41 | if (middleware.slice(-3) === projectFileExtension) { 42 | middleware = middleware.slice(0, -3); 43 | } 44 | 45 | let middlewareRealPath = PathHelper.resolve(middleware); 46 | let hasMiddleware = false; 47 | if ($.file.exists(middlewareRealPath)) { 48 | hasMiddleware = true; 49 | } else if ($.file.exists(middlewareRealPath + projectFileExtension)) { 50 | hasMiddleware = true; 51 | } else { 52 | if ($.file.exists(middlewareRealPath + MiddlewareSuffix + projectFileExtension)) { 53 | middlewareRealPath = middlewareRealPath + MiddlewareSuffix; 54 | hasMiddleware = true; 55 | } 56 | } 57 | 58 | if (hasMiddleware) { 59 | const hasSuffix = StringHelper.hasSuffix(middlewareKey, MiddlewareSuffix); 60 | if (hasSuffix) { 61 | Use.middlewares[StringHelper.withoutSuffix(middlewareKey, MiddlewareSuffix)] = middlewareRealPath; 62 | delete Use.middlewares[middlewareKey]; 63 | } else { 64 | Use.middlewares[middlewareKey] = middlewareRealPath; 65 | } 66 | } else { 67 | $.logError(`Middleware not found:`); 68 | $.logErrorAndExit(middleware); 69 | delete Use.middlewares[middlewareKey]; 70 | } 71 | } 72 | } 73 | 74 | $.engineData.set( 75 | UsePath, 76 | $.objectCollection(Use), 77 | ); 78 | 79 | // Functions 80 | function parsePath(path: string, data: Record = {}) { 81 | const dataKeys = Object.keys(data); 82 | 83 | if (dataKeys.length) { 84 | for (let i = 0; i < dataKeys.length; i++) { 85 | const dataKey = dataKeys[i]; 86 | path = path.replace(`{${dataKey}}`, data[dataKey]); 87 | } 88 | } 89 | 90 | return path; 91 | } 92 | 93 | function fileExistsInPath(file: string, path: string, suffix = ""): [boolean, string] { 94 | 95 | if (suffix.length) { 96 | const hasSuffix = file.slice(-suffix.length) === suffix; 97 | 98 | if (!hasSuffix) { 99 | file += suffix; 100 | } 101 | } 102 | 103 | let fullPath = parsePath(path, {file}); 104 | fullPath = PathHelper.resolve(fullPath); 105 | 106 | if (!$.file.exists(fullPath)) { 107 | file = StringHelper.upperFirst(file); 108 | if (!$.file.exists(parsePath(path, {file}))) { 109 | return [false, fullPath]; 110 | } 111 | } 112 | 113 | return [true, fullPath]; 114 | } 115 | 116 | class UseEngine { 117 | /** 118 | * Use Package from npm. 119 | * @param $package 120 | * @param handleError 121 | * @return {boolean|*} 122 | */ 123 | public static package($package: string, handleError = true) { 124 | try { 125 | return require($package); 126 | } catch (e) { 127 | return !handleError ? false : $.logErrorAndExit(`Package {${[$package]}} not found in node_modules.`); 128 | } 129 | } 130 | 131 | /** 132 | * Use file from backend 133 | * @param {string} path 134 | * @return {*} 135 | */ 136 | public static file(path: string) { 137 | const fullPath = $.path.backend("{file}" + projectFileExtension); 138 | const [hasPath, realPath] = fileExistsInPath(path, fullPath); 139 | if (!hasPath) { 140 | throw Error(`File ${realPath} does not exist!`); 141 | } 142 | return require(realPath); 143 | } 144 | 145 | /** 146 | * Use Model 147 | * @param {string} model 148 | * @param {boolean} [handleError=true] 149 | * @return {*} 150 | */ 151 | public static model(model: string, handleError = true): any { 152 | let fullPath = PathHelper.resolve($.config.get('paths.models')) + "/{file}"; 153 | fullPath = PathHelper.addProjectFileExtension(fullPath) as string; 154 | 155 | const [hasPath, realPath] = fileExistsInPath(model, fullPath); 156 | 157 | if (!hasPath) { 158 | // @ts-ignore 159 | if (!handleError) { 160 | return false 161 | } else { 162 | throw new InXpresserError(`Model ${realPath} does not exists`); 163 | } 164 | } 165 | return require(realPath); 166 | } 167 | 168 | /** 169 | * Use Middleware 170 | * @param {string} middleware 171 | * @param {boolean} [handleError=true] 172 | * @param {boolean} [suffix=true] 173 | * @return {boolean|*} 174 | */ 175 | public static middleware(middleware: string, handleError = true, suffix = true) { 176 | if (typeof Use.middlewares === "object") { 177 | const useMiddlewares = Use.middlewares; 178 | const middleWithoutSuffix = StringHelper.withoutSuffix(middleware, "Middleware"); 179 | 180 | if (useMiddlewares.hasOwnProperty(middleWithoutSuffix)) { 181 | return require(useMiddlewares[middleWithoutSuffix]); 182 | } 183 | } 184 | 185 | const fullPath = $.path.backend("middlewares/{file}" + projectFileExtension); 186 | 187 | const [hasPath, realPath] = fileExistsInPath(middleware, fullPath, suffix ? "Middleware" : ""); 188 | 189 | if (!hasPath) { 190 | return !handleError ? false : $.logErrorAndExit(new Error(`Middleware ${realPath} does not exits`)); 191 | } 192 | 193 | return require(realPath); 194 | } 195 | 196 | public static controller(controller: string) { 197 | 198 | if (controller.indexOf("::") > 0) { 199 | const $splitController = controller.split("::"); 200 | const $pluginNamespace = $splitController[0]; 201 | 202 | if (PluginNamespaces.hasOwnProperty($pluginNamespace)) { 203 | 204 | const plugin = PluginNamespaces[$pluginNamespace]; 205 | 206 | if (plugin.paths && plugin.paths.hasOwnProperty("controllers")) { 207 | return plugin.paths.controllers + "/" + $splitController[1]; 208 | } 209 | } 210 | } 211 | 212 | return $.path.controllers(controller); 213 | } 214 | 215 | /** 216 | * Return Router 217 | * @return {$.router} 218 | */ 219 | public static router() { 220 | return $.router; 221 | } 222 | } 223 | 224 | export = UseEngine; 225 | -------------------------------------------------------------------------------- /src/XpresserRepl.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import {REPLServer} from "repl"; 3 | import {DollarSign} from "../types"; 4 | import fs from "fs"; 5 | 6 | 7 | type FnWithDollarSignArgument = (xpresserInstance: DollarSign) => (void | any); 8 | type FnReturnsDollarSign = () => DollarSign; 9 | 10 | class XpresserRepl { 11 | 12 | started: boolean = false; 13 | 14 | data: { 15 | commandPrefix: string, 16 | configProvider: (() => any), 17 | xpresserProvider?: FnReturnsDollarSign, 18 | beforeStart?: FnWithDollarSignArgument, 19 | historyFile?: string 20 | } = { 21 | commandPrefix: 'xpresser>', 22 | configProvider() { 23 | return {} 24 | }, 25 | } 26 | 27 | context: Record = {}; 28 | server!: REPLServer; 29 | 30 | constructor(config?: string) { 31 | if (config) this.setConfigProvider(config); 32 | } 33 | 34 | /** 35 | * Check if repl has custom xpresser provider 36 | */ 37 | hasXpresserProvider(): boolean { 38 | return typeof this.data.xpresserProvider === "function" 39 | } 40 | 41 | /** 42 | * Check if repl has custom xpresser extender 43 | */ 44 | hasBeforeStartFn() { 45 | return typeof this.data.beforeStart === "function" 46 | } 47 | 48 | /** 49 | * Set repl ConfigProvider 50 | * @example 51 | * // Provide relative path. 52 | * repl.setConfigProvider('./path/to/config'); 53 | * // Or provide function that returns config 54 | * repl.setConfigProvider(() => ({ 55 | * env: process.env.NODE_ENV, 56 | * paths: {base: __dirname} 57 | * })); 58 | * @param configProvider 59 | */ 60 | setConfigProvider(configProvider: string | (() => any)): this { 61 | 62 | // Import Required Modules 63 | const fs = require('fs'); 64 | const path = require('path') 65 | 66 | if (typeof configProvider === "string") { 67 | const configPath = path.resolve(configProvider); 68 | if (!fs.existsSync(configPath)) { 69 | throw Error(`Config path not found! {${configPath}}`) 70 | } 71 | this.data.configProvider = () => require(configPath); 72 | } else if (typeof configProvider === "function") { 73 | this.data.configProvider = configProvider; 74 | } 75 | 76 | return this; 77 | } 78 | 79 | /** 80 | * Set repl XpresserProvider 81 | * 82 | * In some cases you may have a special xpresser setup, you can pass a custom xpresser provider. 83 | * 84 | * The xpresser instance should not be booted. the repl class will boot it on 85 | * `repl.start()` 86 | * 87 | * @example 88 | * // Pass a function that returns an xpresser instance i.e DollarSign 89 | * repl.setXpresserProvider(() => { 90 | * const {init} = require('xpresser'); 91 | * 92 | * return init({ 93 | * // your xpresser config 94 | * }, { 95 | * requireOnly:true, 96 | * isConsole: true 97 | * }); 98 | * }) 99 | * @param xpresserProvider 100 | */ 101 | setXpresserProvider(xpresserProvider: FnReturnsDollarSign): this { 102 | this.data.xpresserProvider = xpresserProvider; 103 | return this; 104 | } 105 | 106 | /** 107 | * Set Command Prefix 108 | * @param commandPrefix 109 | */ 110 | setCommandPrefix(commandPrefix: string): this { 111 | this.data.commandPrefix = commandPrefix; 112 | return this; 113 | } 114 | 115 | /** 116 | * Run a function before start 117 | * 118 | * This function gives you the opportunity to extend xpresser instance used before starting repl. 119 | * 120 | * @param run 121 | * 122 | * @example 123 | * repl.extendXpresser(($) => { 124 | * $.on.boot(() => { 125 | * $.logInfo('Log before repl start.') 126 | * }) 127 | * }) 128 | * 129 | */ 130 | beforeStart(run: FnWithDollarSignArgument): this { 131 | this.data.beforeStart = run; 132 | return this; 133 | } 134 | 135 | /** 136 | * Add Data to context 137 | * @param key 138 | * @param value 139 | */ 140 | addContext(key: string | Record, value?: any): this { 141 | if (this.started) { 142 | throw Error(`addContext(): cannot be used after repl server has started, use replServer.context instead`); 143 | } 144 | 145 | if (typeof key === "string") { 146 | this.context[key] = value; 147 | } else if (typeof key === "object") { 148 | Object.assign(this.context, key) 149 | } 150 | 151 | return this; 152 | } 153 | 154 | /** 155 | * Try to Build Instance 156 | */ 157 | async buildInstance(): Promise { 158 | const {init} = require('xpresser'); 159 | const config = await this.data.configProvider(); 160 | 161 | return init(config, { 162 | requireOnly: true, 163 | isConsole: true 164 | }) as DollarSign; 165 | } 166 | 167 | /** 168 | * Start repl 169 | */ 170 | async start(onReplStart?: FnWithDollarSignArgument): Promise { 171 | const repl = require('repl'); 172 | const chalk = require('chalk'); 173 | const _ = require('lodash'); 174 | 175 | // Holds xpresser instance i.e ($) 176 | let xpr: DollarSign; 177 | 178 | /** 179 | * if this repl has a custom xpresser provider, 180 | * load it, else try to build an instance. 181 | */ 182 | if (this.hasXpresserProvider()) { 183 | xpr = await (this.data.xpresserProvider as FnReturnsDollarSign)(); 184 | 185 | // Set Required options for repl 186 | if (!xpr.options.requireOnly) 187 | xpr.options.requireOnly = true; 188 | 189 | if (!xpr.options.isConsole) 190 | xpr.options.isConsole = true; 191 | } else { 192 | xpr = await this.buildInstance(); 193 | } 194 | 195 | // Throw error if xpr is undefined for any unknown reason. 196 | if (!xpr) { 197 | throw Error('Xpresser instance is undefined!'); 198 | } 199 | 200 | xpr.on.boot(async (next) => { 201 | // Add DollarSign 202 | this.context.$ = xpr; 203 | 204 | // Log Welcome Message 205 | console.log(chalk.gray('>>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>>')) 206 | console.log(chalk.white(`Xpresser Repl Session.`)); 207 | console.log() 208 | console.log(`Env: ${chalk.yellow(xpr.config.get('env'))}`) 209 | console.log(`Name: ${chalk.yellow(xpr.config.get('name'))}`) 210 | console.log() 211 | console.log(chalk.white(`Use ${chalk.whiteBright('.end')} to end repl session.`)) 212 | console.log(chalk.gray('<<<<<<<<<< <<<<<<<<<< <<<<<<<<<< <<<<<<<<<< <<<<<<<<<<')) 213 | 214 | // If has before start, run it. 215 | if (this.hasBeforeStartFn()) { 216 | await (this.data.beforeStart as any)(xpr) 217 | } 218 | 219 | return next(); 220 | }) 221 | 222 | // Start Repl on boot 223 | xpr.on.boot(async () => { 224 | 225 | // Start Repl 226 | this.server = repl.start({ 227 | prompt: chalk.cyanBright(`${this.data.commandPrefix.trim()} `), 228 | useColors: true, 229 | terminal: true, 230 | }); 231 | 232 | // Set Repl history 233 | const replHistory = this.data.historyFile || xpr.path.storage('framework/.repl_history'); 234 | if (!fs.existsSync(replHistory)) { 235 | xpr.file.makeDirIfNotExist(replHistory, true); 236 | } 237 | 238 | this.server.setupHistory( 239 | replHistory, 240 | (err) => { 241 | if (err) throw err; 242 | } 243 | ); 244 | 245 | // Add End helper 246 | this.server.defineCommand('end', { 247 | help: 'End repl session.', 248 | action() { 249 | xpr.log("Goodbye! See you later..."); 250 | xpr.exit(); 251 | } 252 | }); 253 | 254 | this.server.defineCommand('clearHistory', { 255 | help: 'Clear repl history.', 256 | action() { 257 | try { 258 | fs.unlinkSync(replHistory); 259 | xpr.log("History cleared, restart repl!"); 260 | xpr.exit(); 261 | } catch (e) { 262 | xpr.logError(e) 263 | } 264 | } 265 | }) 266 | 267 | this.server.defineCommand('ls', { 268 | help: 'List context in current session.', 269 | action: (context) => { 270 | 271 | if (context) { 272 | if (!_.has(this.context, context)) { 273 | xpr.logError(`No context with name: ${context}`) 274 | return this.server.displayPrompt(); 275 | } 276 | 277 | const contextValue = _.get(this.context, context); 278 | 279 | if (typeof contextValue === "object") { 280 | mapAndLogContext(chalk, xpr, contextValue); 281 | return this.server.displayPrompt(); 282 | } 283 | 284 | console.log(`${chalk.yellowBright(context)}:`, typeof (contextValue)); 285 | return this.server.displayPrompt(); 286 | } 287 | 288 | // Map through context entries and log them 289 | mapAndLogContext(chalk, xpr, this.context); 290 | this.server.displayPrompt(); 291 | } 292 | }); 293 | 294 | if (typeof onReplStart === "function") { 295 | await onReplStart(xpr); 296 | } 297 | 298 | 299 | // Merge with this context 300 | Object.assign(this.server.context, this.context); 301 | 302 | // Set repl to started 303 | this.started = true; 304 | }) 305 | 306 | // Boot Xpresser 307 | xpr.boot(); 308 | 309 | return this; 310 | } 311 | 312 | /** 313 | * Requires Files and adds then to context. 314 | * @example 315 | * repl.addContextFromFiles({ 316 | * User: 'backend/models/User.js', 317 | * Mailer: 'backend/lib/Mailer.js' 318 | * }) 319 | * @param files 320 | * @param as 321 | * @param interceptor 322 | */ 323 | addContextFromFiles(files: { [contextName: string]: string }, as?: string | null, interceptor?: (context: any) => any): this { 324 | const contentNames = Object.keys(files); 325 | const context: Record = {}; 326 | 327 | for (const contextName of contentNames) { 328 | try { 329 | const file = path.resolve(files[contextName]) 330 | context[contextName] = interceptor ? interceptor(require(file)) : require(file); 331 | } catch (e) { 332 | throw e 333 | } 334 | } 335 | 336 | return as ? this.addContext(as, context) : this.addContext(context); 337 | } 338 | 339 | /** 340 | * Requires Files and adds then to context. 341 | * @example 342 | * repl.addContextFromFiles('backend/models') 343 | * @param folder 344 | * @param as 345 | * @param extensions 346 | * @param interceptor 347 | */ 348 | addContextFromFolder(folder: string, as?: string | null, extensions?: string[] | null, interceptor?: (context: any) => any): this { 349 | /** 350 | * Import get all files helper 351 | */ 352 | const {getAllFiles} = require("./Functions/inbuilt.fn"); 353 | const {camelCase} = require('lodash'); 354 | 355 | folder = path.resolve(folder); 356 | 357 | // Allowed Extensions 358 | let allowedExtensions = ['js', 'ts']; 359 | if (extensions) 360 | allowedExtensions = allowedExtensions.concat(extensions); 361 | 362 | const files = getAllFiles(folder); 363 | const context: Record = {}; 364 | 365 | for (const file of files) { 366 | const extension = file.split('.').pop(); 367 | if (allowedExtensions.includes(extension)) { 368 | let shortFilePath = file.replace(folder, ''); 369 | shortFilePath = shortFilePath.substring(0, shortFilePath.length - `.${extension}`.length) 370 | 371 | const contextName = capitalize(camelCase(shortFilePath)); 372 | 373 | try { 374 | context[contextName] = interceptor ? interceptor(require(file)) : require(file); 375 | } catch (e) { 376 | throw e 377 | } 378 | } 379 | } 380 | 381 | return as ? this.addContext(as, context) : this.addContext(context); 382 | } 383 | } 384 | 385 | /** 386 | * Capitalize Text 387 | * @param s 388 | */ 389 | const capitalize = (s: string) => { 390 | return s.charAt(0).toUpperCase() + s.slice(1) 391 | } 392 | 393 | /** 394 | * Get Type of context 395 | * @param context 396 | */ 397 | const getTypeOfContext = (context: any) => { 398 | if (context) { 399 | if (typeof context === 'object' && context.constructor && context.constructor.name) { 400 | return context.constructor.name; 401 | } else if (typeof context === 'function' && context.name) { 402 | return context.name; 403 | } 404 | } 405 | 406 | return typeof context; 407 | } 408 | 409 | /** 410 | * Map and log context. 411 | * @param chalk 412 | * @param xpr 413 | * @param entries 414 | */ 415 | const mapAndLogContext = (chalk: any, xpr: DollarSign, entries: any) => { 416 | Object.entries(entries).forEach(([entry, value]) => { 417 | if (value === xpr) { 418 | console.log(`${chalk.yellowBright(entry)}:`, 'XpresserInstance ($)'); 419 | } else { 420 | console.log(`${chalk.yellowBright(entry)}:`, getTypeOfContext(value)); 421 | } 422 | }) 423 | } 424 | 425 | export = XpresserRepl; -------------------------------------------------------------------------------- /src/backend/views/__errors/index.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | if (typeof error === "undefined") error = {}; 3 | const isProduction = ctx.config('env') === "production"; 4 | %> 5 | 6 | 7 | 8 | 9 | 10 | <%= ctx.config('name') %> 11 | <%- include('./bootstrap') %> 12 | 13 | <% if( isProduction ) { %> 14 | 15 |
16 |
17 |
18 |
19 |

<%= statusCode %> <%= statusCodes[statusCode] || "Oops!" %> 20 |

21 |
22 |
23 |
24 |
25 | 26 | <% } else { %> 27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |

<%- error.title || 'Oops!' %>

35 |
36 | <% if(error.html) { %> 37 |
38 | <%- error.html %> 39 |
40 |
41 | <% } %> 42 |
43 | 44 | <%- error.log %> 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 | <% } %> 55 | 56 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | import {getInstance} from "../index"; 2 | // Helpers 3 | import Helpers from "./Extensions/Helpers"; 4 | import Modules from "./Functions/modules.fn"; 5 | import {initializeTypescriptFn} from "./Functions/internals.fn"; 6 | // Get Lan Ip 7 | import {getLocalExternalIp} from "./Functions/inbuilt.fn"; 8 | import Base64 from "./Helpers/Base64"; 9 | import Utils from "./Functions/util.fn"; 10 | 11 | const $ = getInstance(); 12 | 13 | 14 | // Use Base64 and Object-validator-pro 15 | $.base64 = Base64; 16 | 17 | $.helpers = Helpers; 18 | $.modules = Modules; 19 | 20 | 21 | // Assign Utils to $.fn 22 | $.fn = Utils; 23 | 24 | // Assign Utils to $.fn 25 | $.utils = Utils; 26 | 27 | // Set $.typescriptInit 28 | $.initializeTypescript = initializeTypescriptFn; 29 | 30 | // Set Lan Ip 31 | $.engineData.set('lanIp', getLocalExternalIp()); 32 | 33 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | /** 4 | * Un-Categorized Types 5 | */ 6 | export interface DeleteDirOptions extends fs.RmOptions { 7 | returnList?: boolean 8 | } -------------------------------------------------------------------------------- /tests/backend/MyRequestEngine.ts: -------------------------------------------------------------------------------- 1 | import {getInstance} from "../../index"; 2 | import {HttpError} from "../../types/http"; 3 | 4 | const $ = getInstance(); 5 | 6 | class MyRequestEngine extends $.extendedRequestEngine() { 7 | name(){ 8 | return this.send("This is not mine.") 9 | } 10 | 11 | onError(e: any, formatted: HttpError.Data) { 12 | console.log(e) 13 | return this.send(formatted); 14 | } 15 | } 16 | 17 | export = MyRequestEngine; -------------------------------------------------------------------------------- /tests/backend/controllers/AppController.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Http} from "../../../types/http"; 2 | 3 | export = { 4 | name: "AppController", 5 | 6 | middlewares: { 7 | "test": "*" 8 | }, 9 | 10 | start(http: any) { 11 | // JobHelper.dispatch("play") 12 | return ("I would like to see an error!") 13 | }, 14 | 15 | url_case(http: Http){ 16 | return http.route; 17 | } 18 | } -------------------------------------------------------------------------------- /tests/backend/middlewares/TestMiddleware.ts: -------------------------------------------------------------------------------- 1 | import type {Http} from "../../../types/http"; 2 | 3 | export = { 4 | allow(http: Http){ 5 | // throw new Error("Not implemented"); 6 | 7 | return http.next() 8 | } 9 | } -------------------------------------------------------------------------------- /tests/backend/plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "./tests/test-plugin": true 3 | } -------------------------------------------------------------------------------- /tests/backend/use.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": { 3 | "RequestEngine": [ 4 | "./tests/backend/MyRequestEngine.ts" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /tests/server.ts: -------------------------------------------------------------------------------- 1 | import {init} from "../index" 2 | 3 | function main(port: number) { 4 | 5 | const $ = init({ 6 | name: "Test Xpresser", 7 | env: 'development', 8 | 9 | paths: { 10 | base: __dirname, 11 | backend: "base://backend/", 12 | }, 13 | server: {port, router: {pathCase: 'kebab'}} 14 | }) 15 | 16 | $.initializeTypescript(__filename); 17 | 18 | $.on.boot(next => { 19 | // $.router.config.pathCase = 'kebab'; 20 | 21 | $.router.get('/', "AppController@start") 22 | $.router.path('/test', () => { 23 | $.router.get("@url_case"); 24 | }).controller("App"); 25 | 26 | return next(); 27 | }); 28 | 29 | 30 | $.boot(); 31 | } 32 | 33 | main(3000) 34 | -------------------------------------------------------------------------------- /tests/specs/index.spec.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert/strict'; 2 | import test from 'node:test'; 3 | 4 | test('1 is equal to 1.', () => { 5 | assert.strictEqual(1, 1); 6 | }); -------------------------------------------------------------------------------- /tests/test-plugin/plugin-index.ts: -------------------------------------------------------------------------------- 1 | import {DollarSign, PluginData} from "../../types"; 2 | 3 | export function dependsOn() { 4 | // return ["abolish"] 5 | return []; 6 | } 7 | 8 | export function run(plugin: PluginData, $: DollarSign) { 9 | $.logWarning(`Hello from plugin: ${plugin.namespace}`); 10 | } -------------------------------------------------------------------------------- /tests/test-plugin/use.json: -------------------------------------------------------------------------------- 1 | { 2 | "xpresser": ">=0.24.0", 3 | "namespace": "Test Plugin", 4 | "use_index": "./plugin-index.ts" 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /truth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Xpresser Source of truth 3 | * Holds current instanceId 4 | */ 5 | 6 | export = { 7 | instanceId: null 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": true, 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "target": "ES2019", 7 | "moduleResolution": "node", 8 | "sourceMap": false, 9 | "outDir": "dist", 10 | "declaration": false, 11 | "downlevelIteration": true, 12 | "skipLibCheck": true, 13 | "resolveJsonModule": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": false, 17 | 18 | /* Strict Type-Checking Options */ 19 | "strict": true, 20 | /* Enable all strict type-checking options. */ 21 | 22 | "types": [ 23 | "node", 24 | "express", 25 | "fs-extra", 26 | "lodash" 27 | ] 28 | }, 29 | "lib": [ 30 | "es2016" 31 | ], 32 | "files": [ 33 | "./global.d.ts", 34 | "./types/index.d.ts", 35 | "./types/node.d.ts", 36 | "./types/helpers.d.ts", 37 | "./types/http.d.ts", 38 | "./types/modules.d.ts" 39 | ], 40 | "include": [ 41 | "./**/*.ts" 42 | ], 43 | "exclude": [ 44 | "node_modules", 45 | "dist" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /types/helpers.d.ts: -------------------------------------------------------------------------------- 1 | import {LoDashStatic} from "lodash"; 2 | import moment from "moment"; 3 | 4 | declare namespace Xpresser { 5 | namespace Helpers { 6 | interface Main { 7 | urlBuilder: any; 8 | 9 | url($path?: string, $query?: object): string; 10 | 11 | route($route: string, $keys?: any[], $query?: object | boolean, $includeUrl?: boolean): string; 12 | 13 | config($config: string, $default?: any): any; 14 | 15 | mix(file: string): string; 16 | 17 | env(key: string, $default?: any): any; 18 | 19 | /** 20 | * Random String Generator 21 | * @param [length=10] 22 | */ 23 | randomStr(length: number): string; 24 | 25 | /** 26 | * Random Number Generator 27 | * @param min 28 | * @param max 29 | */ 30 | randomInteger(min: number, max: number): number; 31 | 32 | randomArray(length: number): any[]; 33 | 34 | now(): string; 35 | 36 | today(): string; 37 | 38 | toDate(date?: any, format?: string): any; 39 | 40 | timeAgo(date: any, format?: string): string; 41 | } 42 | 43 | interface Util { 44 | extArrayRegex(arr: any[]): (RegExp | string); 45 | 46 | /** 47 | * Find words in string 48 | * @param str 49 | * @param $keywords 50 | */ 51 | findWordsInString(str: string, $keywords: string[]): RegExpMatchArray | null; 52 | 53 | /** 54 | * Check if value is a promise 55 | * @param value - value to check 56 | * @return boolean 57 | */ 58 | isPromise(value: any): boolean; 59 | 60 | /** 61 | * Check if value is an AsyncFunction 62 | * @param value - value to check 63 | * @return boolean 64 | */ 65 | isAsyncFn(value: any): boolean; 66 | 67 | /** 68 | * Generate Random string. 69 | * @param length - Length of string to generate. 70 | */ 71 | randomStr(length: number): string; 72 | 73 | /** 74 | * Get Source of RegExp or return string if not regex 75 | * @param str 76 | */ 77 | regExpSourceOrString(str: string | RegExp): string; 78 | } 79 | 80 | interface Base64 { 81 | /** 82 | * Encode Str or Object 83 | * If Object, we will Json.stringify it 84 | * @param str 85 | */ 86 | encode(str: string | object): string; 87 | 88 | /** 89 | * Decode encoded text. 90 | * @param str 91 | */ 92 | decode(str: string): string; 93 | 94 | /** 95 | * Decode To Json Object 96 | * @param str 97 | */ 98 | decodeToObject(str: string): object; 99 | } 100 | 101 | interface Modules { 102 | /** 103 | * Lodash Package 104 | */ 105 | lodash(): LoDashStatic; 106 | 107 | /** 108 | * Moment Package 109 | */ 110 | moment(): typeof moment; 111 | } 112 | } 113 | } 114 | 115 | export = Xpresser; 116 | -------------------------------------------------------------------------------- /types/http.d.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import RequestEngine from "../src/RequestEngine"; 3 | import ControllerServiceError from "../src/Controllers/ControllerServiceError"; 4 | import ControllerService from "../src/Controllers/ControllerService"; 5 | 6 | declare namespace Xpresser { 7 | 8 | // tslint:disable-next-line:no-empty-interface 9 | interface Http extends RequestEngine { 10 | } 11 | 12 | namespace Http { 13 | interface Request extends express.Request { 14 | // session: any 15 | flash?(key?: string, value?: any): any 16 | } 17 | 18 | // tslint:disable-next-line:no-empty-interface 19 | interface Response extends express.Response { 20 | } 21 | } 22 | 23 | namespace Controller { 24 | 25 | interface ServiceContext { 26 | http: Http; 27 | boot: any; 28 | services: any; 29 | error: (...args: any[]) => ControllerServiceError; 30 | } 31 | 32 | interface Services { 33 | /** 34 | * Controller Service 35 | * @param options - option passed to service in controller method. 36 | * @param context - Your current request and services context 37 | */ 38 | [name: string]: (options: any, context: ServiceContext) => (any | void); 39 | } 40 | 41 | type MethodWithHttp = (http: Http) => (any | void); 42 | type InlineServiceMethod = (context: ServiceContext) => (any | void); 43 | type MiddlewareWithHelper = (helpers: { 44 | use: MethodWithHttp, 45 | }) => Record; 46 | 47 | interface MethodWithServices { 48 | // Controller Service 49 | [name: string]: InlineServiceMethod | any; 50 | } 51 | 52 | type ErrorHandler = (...args: any[]) => any; 53 | type MethodWithBoot = (http: Http, boot: any, error: ErrorHandler) => (any | void); 54 | type ActionResponse = Http.Response | void 55 | 56 | type Object = { 57 | // Name of Controller. 58 | name: string; 59 | 60 | /** 61 | * Default controller error handler. 62 | * Available to all service functions, 63 | * 64 | * @example 65 | * const service: (option, {error}) => { 66 | * error("arg1", "arg2") 67 | * } 68 | * 69 | * Arg1 and Arg2 will be passed to your error function after 70 | * xpresser RequestEngine variable. 71 | * 72 | * @param http 73 | * @param args 74 | */ 75 | e?: (http: Http, ...args: any[]) => ActionResponse | Promise; 76 | 77 | /** 78 | * Register Middlewares 79 | * @param helpers 80 | */ 81 | middleware?: MiddlewareWithHelper; 82 | 83 | /** 84 | * Register middlewares using object 85 | */ 86 | middlewares?: Record; 87 | 88 | /** 89 | * Boot method. 90 | * Any value returned in this function is passed to your 91 | * controller method and services method 92 | * 93 | * controller(http, boot); 94 | * 95 | * service(option, {boot}); 96 | * 97 | * inlineService({boot}); 98 | */ 99 | boot?: (http: Http, error: ErrorHandler) => (any | void) | Promise; 100 | } | { 101 | [action: string]: (http: Http, boot: Boot, error: ErrorHandler) => ActionResponse | Promise; 102 | } 103 | 104 | // interface Object { 105 | // // Name of Controller. 106 | // name: string; 107 | // 108 | // 109 | // e?: (http: Http, ...args: any[]) => Http.Response | void; 110 | // 111 | // 112 | // [name: string]: MethodWithBoot | ErrorHandler | MiddlewareWithHelper | Record | string | undefined; 113 | // } 114 | 115 | // tslint:disable-next-line:no-empty-interface 116 | interface Handler extends ControllerService { 117 | 118 | } 119 | 120 | class Class { 121 | public static middleware(helpers?: { use?: MethodWithBoot }): object; 122 | 123 | public static boot(http: Http, error?: () => (any | void)): object; 124 | } 125 | } 126 | 127 | namespace HttpError { 128 | type Data = { in?: string, title?: string, html?: string, log?: string }; 129 | type onError = { onError: (e: any, data: HttpError.Data) => void } 130 | } 131 | } 132 | 133 | export = Xpresser; 134 | -------------------------------------------------------------------------------- /types/modules.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Typings for know js files. 3 | */ 4 | import JobHelper from "../src/Console/JobHelper"; 5 | 6 | // @ts-ignore 7 | declare module "xpresser/dist/src/Console/JobHelper" { 8 | export = JobHelper; 9 | } -------------------------------------------------------------------------------- /types/node.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface Global { 3 | $: any; 4 | xpresserInstance(instanceId?: string): any; 5 | InXpresserError: typeof Error; 6 | } 7 | } 8 | 9 | /** 10 | * Make Declaration public 11 | */ 12 | declare namespace xpresser { 13 | type useNamespace_Xpresser_Instead = boolean 14 | } 15 | declare namespace Xpresser { 16 | } 17 | -------------------------------------------------------------------------------- /xpresser-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpresserjs/framework/9febc7c31b293a4ed1b3d461ea82f560edb19820/xpresser-logo-black.png -------------------------------------------------------------------------------- /xpresser-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpresserjs/framework/9febc7c31b293a4ed1b3d461ea82f560edb19820/xpresser-logo-white.png --------------------------------------------------------------------------------