UNPKG

@uisap/core

Version:

A modular Fastify-based framework inspired by Laravel

945 lines (899 loc) 30 kB
#!/usr/bin/env node import dotenv from "dotenv"; import { program } from "commander"; import { Application, Config, ScheduleFacade, QueueFacade, BroadcastFacade, DatabaseProvider, RouteProvider, BroadcastProvider, QueueProvider, Command, } from "@uisap/core"; import * as path from "path"; import { pathToFileURL } from "url"; import { fileURLToPath } from "url"; import { dirname } from "path"; import fs from "fs-extra"; import chalk from "chalk"; import { spawn } from "child_process"; async function loadCommands() { const commandsDir = path.join(process.cwd(), "app/console/commands"); try { const files = await fs.readdir(commandsDir); const commandFiles = files.filter((file) => file.endsWith(".js")); for (const file of commandFiles) { const filePath = pathToFileURL(path.join(commandsDir, file)).href; try { const commandModule = await import(filePath); const CommandClass = commandModule.default; if (CommandClass && CommandClass.prototype instanceof Command) { const config = await Config.load(); const app = new Application({ ...config.app, database: config.database, // Veritabanı yapılandırmasını ekle queue: config.queue, // Kuyruk yapılandırmasını ekle routes: config.routes, }); app.fastify.config = config; app.provider(DatabaseProvider); app.provider(QueueProvider); app.provider(RouteProvider); try { console.log(`Bootstrapping for command: ${file}`); await app.bootstrap(); console.log(`Bootstrap completed for command: ${file}`); } catch (bootstrapErr) { console.error( chalk.red( `Bootstrap failed for command ${file}: ${bootstrapErr.message}` ) ); console.error(bootstrapErr.stack); await app.close(); continue; } // Komut örneği oluşturma ve kayıt let tempCommandInstance = new CommandClass(app.fastify); const signature = tempCommandInstance.signature; const description = tempCommandInstance.description; program .command(signature) .description(description) .option("--help", "Display help for this command") .action(async (options) => { if (options.help) { console.log(chalk.cyan(`Help for "${signature}":`)); console.log(` Description: ${description}`); console.log(` Usage: ${signature}`); return; } try { const config = await Config.load(); const app = new Application({ ...config.app, database: config.database, // Veritabanı yapılandırmasını ekle queue: config.queue, // Kuyruk yapılandırmasını ekle routes: config.routes }); app.fastify.config = config; app.provider(DatabaseProvider); app.provider(QueueProvider); app.provider(RouteProvider); console.log("Starting bootstrap..."); await app.bootstrap(); console.log("Bootstrap completed, running command..."); const commandInstance = new CommandClass(app.fastify); await commandInstance.handle(options); console.log("Command executed, closing app..."); await app.close(); console.log("App closed, exiting process..."); process.exit(0); } catch (err) { console.error( chalk.red(`Command execution failed: ${err.message}`) ); console.error(err.stack); process.exit(1); } }); console.log(chalk.blue(`Komut yüklendi: ${signature}`)); await app.close(); } } catch (err) { console.error( chalk.red(`Failed to load command ${file}: ${err.message}`) ); console.error(err.stack); continue; } } } catch (err) { if (err.code !== "ENOENT") { console.error(chalk.red(`Komutlar yüklenirken hata: ${err.message}`)); console.error(err.stack); } } } console.log(chalk.blue("UISAP CLI çalıştırıldı")); const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); const templates = { controller: { type: "controller", dir: "app/controllers", fileName: (name) => `${capitalize(name)}.js`, content: (name) => ` import { BaseController } from "@uisap/core"; export class ${capitalize(name)}Controller extends BaseController { constructor(fastify, container) { super(fastify, container); } async index(request, reply) { reply.send({ message: "${capitalize(name)} controller works!" }); } } `, }, model: { type: "model", dir: "app/models", fileName: (name) => `${capitalize(name)}.js`, content: (name) => ` import { Model } from "@uisap/core"; export class ${capitalize(name)} extends Model { constructor(fastify) { super(fastify); } async getData() { return await this.rawSql("SELECT 1 AS test"); } } `, }, middleware: { type: "middleware", dir: "app/middlewares", fileName: (name) => `${capitalize(name)}.js`, content: (name) => ` import { Middleware } from "@uisap/core"; export class ${capitalize(name)} extends Middleware { async handle(request, reply, next) { return reply.code(401).send({ error: "Unauthorized" }); } } `, }, job: { type: "job", dir: "app/jobs", fileName: (name) => `${capitalize(name)}.js`, content: (name) => ` export class ${capitalize(name)} { constructor(data) { this.data = data; } async handle() { console.log("${capitalize(name)} job running:", this.data); } } `, }, event: { type: "event", dir: "app/events", fileName: (name) => `${capitalize(name)}.js`, content: (name) => ` import { Event } from "@uisap/core"; export class ${capitalize(name)} extends Event { constructor(data) { super(data); } broadcastOn() { return ["${name.toLowerCase()}"]; } broadcastWith() { return this.data; } broadcastAs() { return "${capitalize(name)}"; } } `, }, listener: { type: "listener", dir: "app/listeners", fileName: (name) => `${capitalize(name)}.js`, content: (name) => ` export default class ${capitalize(name)} { constructor() { this.queueName = null; } onQueue(queueName = "default") { this.queueName = queueName; return this; } async handle(event, app) { app.fastify.logger.info("${capitalize(name)} handling event:", event); } async dispatch(event, app) { if (this.queueName) { await app.container.make("queue").addTo(this.queueName, "${capitalize( name )}", { eventData: event }); } else { await this.handle(event, app); } } } `, }, command: { type: "command", dir: "app/console/commands", fileName: (name) => `${capitalize(name)}.js`, content: (name) => ` import { Command } from "@uisap/core"; export default class ${capitalize(name)} extends Command { constructor(fastify) { super(fastify); this.signature = "${name.toLowerCase()}"; this.description = "Description of ${capitalize(name)}"; } async handle(options = {}) { this.fastify.logger.info("${capitalize(name)} command executed"); } } `, }, }; const generateFile = async (template, name) => { const { dir, fileName, content } = template; const filePath = path.join(process.cwd(), dir, fileName(name)); try { await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, content(name).trim()); console.log( chalk.green(`${capitalize(template.type)} created: ${filePath}`) ); } catch (err) { console.error(chalk.red(`Error: ${err.message}`)); } }; const updateConfig = async (type, name, filePath) => { const configPath = path.join(process.cwd(), "config", `${type}.js`); const importStatement = `import { join } from "path";\n`; const relativePath = filePath.split(process.cwd())[1].replace(/\\/g, "/"); try { let configContent; const fileExists = await fs.pathExists(configPath); if (!fileExists) { configContent = type === "schedule" ? `${importStatement}\nexport default {\n commands: {\n "${name.toLowerCase()}": join(process.cwd(), "${relativePath}")\n }\n};\n` : `${importStatement}\nexport default {\n enabled: true,\n redis: {\n host: process.env.REDIS_HOST || "localhost",\n port: Number(process.env.REDIS_PORT) || 6379\n },\n connections: {\n default: {},\n broadcast: {},\n console: {}\n },\n handlers: {\n ${capitalize( name )}: join(process.cwd(), "${relativePath}")\n }\n};\n`; } else { configContent = await fs.readFile(configPath, "utf-8"); if (!configContent.includes('import { join } from "path"')) { configContent = importStatement + configContent; } const newEntry = type === "schedule" ? `"${name.toLowerCase()}": join(process.cwd(), "${relativePath}")` : `${capitalize(name)}: join(process.cwd(), "${relativePath}")`; if (type === "schedule") { if (configContent.includes("commands:")) { configContent = configContent.replace( /commands:\s*{\s*([^}]*?)\s*}/, (match, p1) => { const entries = p1 .split(/,\s*\n/) .map((line) => line.trim()) .filter(Boolean) .concat(newEntry); return `commands: {\n ${entries.join(",\n ")}\n }`; } ); } else { configContent = configContent.replace( /export default\s*{([^}]*)}/, (match, p1) => { const trimmedP1 = p1.trim(); return `export default {\n${trimmedP1}${ trimmedP1 ? ",\n" : "" } commands: {\n ${newEntry}\n }\n}`; } ); } } else if (type === "queue") { if (configContent.includes("handlers:")) { configContent = configContent.replace( /handlers:\s*{\s*([^}]*?)\s*}/, (match, p1) => { const entries = p1 .split(/,\s*\n/) .map((line) => line.trim()) .filter(Boolean) .concat(newEntry); return `handlers: {\n ${entries.join(",\n ")}\n }`; } ); } else { configContent = configContent.replace( /export default\s*{([^}]*)}/, (match, p1) => { const trimmedP1 = p1.trim(); return `export default {\n${trimmedP1}${ trimmedP1 ? ",\n" : "" } handlers: {\n ${newEntry}\n }\n}`; } ); } } } const formattedContent = configContent .split("\n") .map((line) => line.trimEnd()) .join("\n") .replace(/,\s*\n\s*}/g, "\n }") .replace(/}\s*}\s*;/g, "}\n};"); await fs.writeFile(configPath, formattedContent); console.log(chalk.green(`${type}.js updated with ${name}`)); } catch (err) { console.error(chalk.red(`Failed to update ${type}.js: ${err.message}`)); } }; const updateAppServiceProvider = async (eventName, listenerName) => { const providerPath = path.join( process.cwd(), "app/providers/AppServiceProvider.js" ); const importCore = `import { ServiceProvider, EventFacade as Event } from '@uisap/core';\n`; const importListener = `import ${capitalize( listenerName )} from '../listeners/${capitalize(listenerName)}.js';\n`; const listenStatement = ` Event.listen('${capitalize( eventName )}', new ${capitalize(listenerName)}(), 10);\n`; try { let content; const fileExists = await fs.pathExists(providerPath); if (!fileExists) { content = `${importCore}${importListener} export class AppServiceProvider extends ServiceProvider { register() { // You can register your services here } async boot() { // You can boot your services here ${listenStatement} } } `; } else { content = await fs.readFile(providerPath, "utf-8"); if (!content.includes(`import ${capitalize(listenerName)} from`)) { if (content.includes(importCore)) { content = content.replace( /import { ServiceProvider, EventFacade as Event } from '@uisap\/core';/, `${importCore}${importListener}` ); } else { content = `${importCore}${importListener}${content}`; } } if (content.includes("async boot()")) { content = content.replace(/async boot\(\) {([^}]*)}/, (match, p1) => { const trimmedP1 = p1.trim(); const entries = trimmedP1 ? `${trimmedP1}\n${listenStatement}` : `${listenStatement}`; return `async boot() {\n${entries} }`; }); } else { content = content.replace( /export class AppServiceProvider extends ServiceProvider {([^}]*)}/, `export class AppServiceProvider extends ServiceProvider {\n$1\n async boot() {\n${listenStatement} }\n}` ); } } const formattedContent = content .split("\n") .map((line) => line.trimEnd()) .join("\n"); await fs.writeFile(providerPath, formattedContent); console.log( chalk.green( `AppServiceProvider updated with ${eventName} and ${listenerName}` ) ); } catch (err) { console.error( chalk.red(`Failed to update AppServiceProvider: ${err.message}`) ); } }; program .version("1.0.5") .description("UISAP - Fastify framework CLI tool") .usage("[command] [options]"); // Global Help Command program .command("help") .description("Display detailed help information for all commands") .action(() => { console.log(chalk.cyan.bold("\nUISAP CLI Help\n")); console.log(chalk.white("Version: 1.0.3")); console.log(chalk.white("Description: Fastify framework CLI tool")); console.log(chalk.white("Usage: uisap <command> [options]\n")); console.log(chalk.cyan("Available Commands:\n")); const commands = [ { name: "schedule:list", desc: "List all scheduled tasks", usage: "schedule:list", options: [], }, { name: "serve", desc: "Start the Fastify application with npm run dev", usage: "serve [-p, --port <port>]", options: ["-p, --port <port> Port number (default: 4115)"], }, { name: "schedule:run", desc: "Run all scheduled tasks once", usage: "schedule:run", options: [], }, { name: "queue:work", desc: "Process jobs from the queue", usage: "queue:work [--queue <queue>] [--tries <tries>]", options: [ "--queue <queue> Queue to process (default: 'default')", "--tries <tries> Number of retries (default: 3)", ], }, { name: "broadcast:test", desc: "Send a test broadcast message", usage: "broadcast:test [--channel <channel>] [--event <event>] [--data <data>]", options: [ "--channel <channel> Channel name (default: 'my-channel')", "--event <event> Event name (default: 'my-event')", "--data <data> Data to send (default: 'Test message')", ], }, { name: "make:controller <name>", desc: "Create a new controller class", usage: "make:controller <name>", options: [], }, { name: "make:model <name>", desc: "Create a new model class", usage: "make:model <name>", options: [], }, { name: "make:middleware <name>", desc: "Create a new middleware class", usage: "make:middleware <name>", options: [], }, { name: "make:job <name>", desc: "Create a new job class", usage: "make:job <name>", options: [], }, { name: "make:event <name>", desc: "Create a new event class", usage: "make:event <name> [--listener <listener>]", options: [ "--listener <listener> Add to AppServiceProvider with specified listener", ], }, { name: "make:listener <name>", desc: "Create a new listener class", usage: "make:listener <name> [--queue] [--event <event>]", options: [ "--queue Add to config/queue.js as handler", "--event <event> Add to AppServiceProvider with specified event", ], }, { name: "make:command <name>", desc: "Create a new console command", usage: "make:command <name> [--schedule]", options: ["--schedule Add to config/schedule.js"], }, { name: "help", desc: "Display this help information", usage: "help", options: [], }, ]; commands.forEach((cmd) => { console.log(chalk.green(` ${cmd.name}`)); console.log(` Description: ${cmd.desc}`); console.log(` Usage: ${cmd.usage}`); if (cmd.options.length > 0) { console.log(" Options:"); cmd.options.forEach((opt) => console.log(` ${opt}`)); } console.log(); }); console.log(chalk.cyan("Notes:")); console.log( " - Use `<name>` for required arguments and `[options]` for optional ones." ); console.log( " - Run any command with `--help` to see its specific help (if available).\n" ); }); program .command("schedule:list") .description("List all scheduled tasks") .option("--help", "Display help for this command") .action(async (options) => { if (options.help) { console.log(chalk.cyan("Help for 'schedule:list':")); console.log(" Description: List all scheduled tasks"); console.log(" Usage: schedule:list"); console.log(" No additional options available."); return; } try { const config = await Config.load(); const app = new Application({ ...config.app, routes: { console: ( await import( pathToFileURL(path.join(process.cwd(), "routes/console.js")).href ) ).default, }, }); app.fastify.config = config; app.provider(QueueProvider); app.provider(RouteProvider); await app.bootstrap(); await app.routes.console(app); const tasks = ScheduleFacade.listTasks(); if (tasks.length === 0) { console.log(chalk.yellow("No scheduled tasks found.")); } else { console.table(tasks); } } catch (err) { console.error(chalk.red(`Error listing schedules: ${err.message}`)); } }); program .command("serve") .description("Start the Fastify application with npm run dev") .option("-p, --port <port>", "Port number", "4115") .option("--help", "Display help for this command") .action((options) => { if (options.help) { console.log(chalk.cyan("Help for 'serve':")); console.log( " Description: Start the Fastify application with npm run dev" ); console.log(" Usage: serve [-p, --port <port>]"); console.log(" Options:"); console.log(" -p, --port <port> Port number (default: 4115)"); return; } try { const devProcess = spawn( "npm", ["run", "dev", "--", `--port=${options.port}`], { stdio: "inherit", shell: true, } ); devProcess.on("error", (err) => console.error(chalk.red(`Failed to start npm run dev: ${err.message}`)) ); devProcess.on("close", (code) => { if (code !== 0) { console.error(chalk.red(`npm run dev exited with code ${code}`)); } }); console.log( chalk.green(`Starting npm run dev on port ${options.port}...`) ); } catch (err) { console.error(chalk.red(`Error starting serve: ${err.message}`)); } }); program .command("schedule:run") .description("Run all scheduled tasks once") .option("--help", "Display help for this command") .action(async (options) => { if (options.help) { console.log(chalk.cyan("Help for 'schedule:run':")); console.log(" Description: Run all scheduled tasks once"); console.log(" Usage: schedule:run"); console.log(" No additional options available."); return; } try { const config = await Config.load(); const app = new Application({ ...config.app, routes: { console: ( await import( pathToFileURL(path.join(process.cwd(), "routes/console.js")).href ) ).default, }, }); app.fastify.config = config; app.provider(QueueProvider); await app.bootstrap(); await app.routes.console(app); await ScheduleFacade.run(); console.log(chalk.green("Scheduled tasks started")); } catch (err) { console.error(chalk.red(`Error running schedules: ${err.message}`)); } }); program .command("queue:work") .description("Process jobs from the queue") .option("--queue <queue>", "Queue to process", "default") .option("--tries <tries>", "Number of retries", "3") .option("--help", "Display help for this command") .action((options) => { if (options.help) { console.log(chalk.cyan("Help for 'queue:work':")); console.log(" Description: Process jobs from the queue"); console.log(" Usage: queue:work [--queue <queue>] [--tries <tries>]"); console.log(" Options:"); console.log(" --queue <queue> Queue to process (default: 'default')"); console.log(" --tries <tries> Number of retries (default: 3)"); return; } try { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const worker = spawn("node", [path.join(__dirname, "worker.js")], { stdio: "inherit", env: { ...process.env, QUEUE_NAME: options.queue, QUEUE_TRIES: options.tries, }, }); worker.on("error", (err) => console.error(chalk.red(`Worker error: ${err.message}`)) ); console.log(chalk.green(`Processing queue: ${options.queue}`)); } catch (err) { console.error(chalk.red(`Failed to start queue worker: ${err.message}`)); } }); program .command("broadcast:test") .description("Send a test broadcast message") .option("--channel <channel>", "Channel name", "my-channel") .option("--event <event>", "Event name", "my-event") .option("--data <data>", "Data to send", "Test message") .option("--help", "Display help for this command") .action(async (options) => { if (options.help) { console.log(chalk.cyan("Help for 'broadcast:test':")); console.log(" Description: Send a test broadcast message"); console.log( " Usage: broadcast:test [--channel <channel>] [--event <event>] [--data <data>]" ); console.log(" Options:"); console.log( " --channel <channel> Channel name (default: 'my-channel')" ); console.log(" --event <event> Event name (default: 'my-event')"); console.log( " --data <data> Data to send (default: 'Test message')" ); return; } try { const config = await Config.load(); const app = new Application({ ...config.app, routes: { channels: ( await import( pathToFileURL(path.join(process.cwd(), "routes/channels.js")).href ) ).default, }, }); app.fastify.config = config; app.provider(BroadcastProvider); await app.bootstrap(); await app.routes.channels(app); await BroadcastFacade.toRoom( options.channel, options.event, options.data ); console.log( chalk.green(`Broadcast sent to ${options.channel}: ${options.event}`) ); } catch (err) { console.error(chalk.red(`Broadcast failed: ${err.message}`)); } }); program .command("make:controller <name>") .description("Create a new controller class") .option("--help", "Display help for this command") .action((name, options) => { if (options.help) { console.log(chalk.cyan("Help for 'make:controller':")); console.log(" Description: Create a new controller class"); console.log(" Usage: make:controller <name>"); console.log(" Arguments:"); console.log(" <name> Name of the controller class"); return; } generateFile(templates.controller, name); }); program .command("make:model <name>") .description("Create a new model class") .option("--help", "Display help for this command") .action((name, options) => { if (options.help) { console.log(chalk.cyan("Help for 'make:model':")); console.log(" Description: Create a new model class"); console.log(" Usage: make:model <name>"); console.log(" Arguments:"); console.log(" <name> Name of the model class"); return; } generateFile(templates.model, name); }); program .command("make:middleware <name>") .description("Create a new middleware class") .option("--help", "Display help for this command") .action((name, options) => { if (options.help) { console.log(chalk.cyan("Help for 'make:middleware':")); console.log(" Description: Create a new middleware class"); console.log(" Usage: make:middleware <name>"); console.log(" Arguments:"); console.log(" <name> Name of the middleware class"); return; } generateFile(templates.middleware, name); }); program .command("make:job <name>") .description("Create a new job class") .option("--help", "Display help for this command") .action((name, options) => { if (options.help) { console.log(chalk.cyan("Help for 'make:job':")); console.log(" Description: Create a new job class"); console.log(" Usage: make:job <name>"); console.log(" Arguments:"); console.log(" <name> Name of the job class"); return; } generateFile(templates.job, name); }); program .command("make:event <name>") .description("Create a new event class") .option( "--listener <listener>", "Add to AppServiceProvider with specified listener" ) .option("--help", "Display help for this command") .action((name, options) => { if (options.help) { console.log(chalk.cyan("Help for 'make:event':")); console.log(" Description: Create a new event class"); console.log(" Usage: make:event <name> [--listener <listener>]"); console.log(" Arguments:"); console.log(" <name> Name of the event class"); console.log(" Options:"); console.log( " --listener <listener> Add to AppServiceProvider with specified listener" ); return; } generateFile(templates.event, name); if (options.listener) { updateAppServiceProvider(name, options.listener); } }); program .command("make:listener <name>") .description("Create a new listener class") .option("--queue", "Add to config/queue.js as handler") .option("--event <event>", "Add to AppServiceProvider with specified event") .option("--help", "Display help for this command") .action((name, options) => { if (options.help) { console.log(chalk.cyan("Help for 'make:listener':")); console.log(" Description: Create a new listener class"); console.log(" Usage: make:listener <name> [--queue] [--event <event>]"); console.log(" Arguments:"); console.log(" <name> Name of the listener class"); console.log(" Options:"); console.log(" --queue Add to config/queue.js as handler"); console.log( " --event <event> Add to AppServiceProvider with specified event" ); return; } generateFile(templates.listener, name); if (options.queue) { updateConfig( "queue", name, path.join(process.cwd(), "app/listeners", `${capitalize(name)}.js`) ); } if (options.event) { updateAppServiceProvider(options.event, name); } }); program .command("make:command <name>") .description("Create a new console command") .option("--schedule", "Add to config/schedule.js") .option("--help", "Display help for this command") .action((name, options) => { if (options.help) { console.log(chalk.cyan("Help for 'make:command':")); console.log(" Description: Create a new console command"); console.log(" Usage: make:command <name> [--schedule]"); console.log(" Arguments:"); console.log(" <name> Name of the command class"); console.log(" Options:"); console.log(" --schedule Add to config/schedule.js"); return; } generateFile(templates.command, name); if (options.schedule) { updateConfig( "schedule", name, path.join( process.cwd(), "app/console/commands", `${capitalize(name)}.js` ) ); } }); loadCommands().then(() => { program.parse(process.argv); // If no command is provided, show the global help if (!process.argv.slice(2).length) { program.outputHelp(); } });