diff --git a/.gitignore b/.gitignore index 8044ca9..1d814f4 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ package-lock.json *.log expressots.config.ts + +*.tgz diff --git a/package.json b/package.json index fb452fa..ec6088a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expressots/cli", - "version": "1.2.2", + "version": "1.3.0-dev", "description": "Expressots CLI - modern, fast, lightweight nodejs web framework (@cli)", "author": "Richard Zampieri", "license": "MIT", diff --git a/src/cli.ts b/src/cli.ts index a6b1e44..4aacb3b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,13 +3,30 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { generateProject } from "./generate"; +import { infoProject } from "./info"; import { createProject } from "./new"; +export const CLI_VERSION = "1.2.2"; + +console.log(`\n[šŸŽ Expressots]\n`); + yargs(hideBin(process.argv)) - .example("$0 new ", "Create a new Expresso TS project") + .scriptName("expressots") .command(createProject()) .command(generateProject()) + .command(infoProject()) + .example("$0 new expressots-demo", "Create interactively") + .example("$0 new expressots-demo -d ./", "Create interactively with path") + .example("$0 new expressots-demo -p yarn -t opinionated", "Create silently") + .example("$0 new expressots-demo -p yarn -t opinionated -d ./", "Create silently with path") + .example("$0 generate service user-create", "Scaffold a service") + .example("$0 info", "Show CLI details") .demandCommand(1, "You need at least one command before moving on") - .epilog("For more information, visit https://expresso-ts.com") - .help() - .parse(); + .epilog("For more information: \n" + + "šŸŒ visit:\t https://expresso-ts.com\n" + + "šŸ’– Sponsor:\t https://github.com/sponsors/expressots") + .help("help", "Show command help") + .alias("h", "help") + .version(false) + .wrap(140) + .parse(); \ No newline at end of file diff --git a/src/generate/cli.ts b/src/generate/cli.ts index dbe6bc1..e0ba785 100644 --- a/src/generate/cli.ts +++ b/src/generate/cli.ts @@ -7,7 +7,7 @@ type CommandModuleArgs = {}; const generateProject = (): CommandModule => { return { command: "generate [schematic] [path]", - describe: "Generate a schematic", + describe: "Scaffold a new resource", aliases: ["g"], builder: (yargs: Argv): Argv => { yargs.positional("schematic", { diff --git a/src/info/cli.ts b/src/info/cli.ts new file mode 100644 index 0000000..16b9267 --- /dev/null +++ b/src/info/cli.ts @@ -0,0 +1,18 @@ +import { CommandModule } from "yargs"; +import { infoForm } from "./form"; + +// eslint-disable-next-line @typescript-eslint/ban-types +type CommandModuleArgs = {}; + +const infoProject = (): CommandModule => { + return { + command: "info", + describe: "Displays project details", + aliases: ["i"], + handler: async () => { + await infoForm(); + }, + }; +}; + +export { infoProject }; diff --git a/src/info/form.ts b/src/info/form.ts new file mode 100644 index 0000000..210dadc --- /dev/null +++ b/src/info/form.ts @@ -0,0 +1,39 @@ +import chalk from "chalk"; +import path from "path"; +import fs from "fs"; +import os from "os"; +import { CLI_VERSION } from "../cli"; +import { printError } from "../utils/cli-ui"; + +function getInfosFromPackage() { + try { + // Get the absolute path of the input directory parameter + const absDirPath = path.resolve(); + // Load the package.json file + const packageJsonPath = path.join(absDirPath, "package.json"); + + const fileContents = fs.readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(fileContents); + + console.log(chalk.green("ExpressoTS Project:")); + console.log(chalk.bold(`\tName: ${packageJson.name}`)); + console.log(chalk.bold(`\tDescription: ${packageJson.description}`)); + console.log(chalk.bold(`\tVersion: ${packageJson.version}`)); + console.log(chalk.bold(`\tAuthor: ${packageJson.author}`)); + } catch (error) { + printError("No project information available.", "package.json not found!") + } +} + +const infoForm = async (): Promise => { + console.log(chalk.green("System informations:")); + console.log(chalk.bold(`\tOS Version: ${os.version()}`)); + console.log(chalk.bold(`\tNodeJS version: ${process.version}`)); + + console.log(chalk.green("CLI Version:")); + console.log(chalk.bold(`\tCurrent version: v${CLI_VERSION}`)); + + getInfosFromPackage(); +}; + +export { infoForm }; diff --git a/src/info/index.ts b/src/info/index.ts new file mode 100644 index 0000000..5be3e1f --- /dev/null +++ b/src/info/index.ts @@ -0,0 +1 @@ +export * from "./cli" diff --git a/src/new/cli.ts b/src/new/cli.ts index fd07be4..7b55ef6 100644 --- a/src/new/cli.ts +++ b/src/new/cli.ts @@ -1,12 +1,12 @@ -import { CommandModule, Argv } from "yargs"; +import { Argv, CommandModule } from "yargs"; import { projectForm } from "./form"; type CommandModuleArgs = {}; - + const createProject = (): CommandModule => { return { - command: "new [package-manager] [template]", - describe: "Create a new Expresso TS project", + command: "new [package-manager] [template] [directory]", + describe: "Create a new project", builder: (yargs: Argv): Argv => { yargs .positional("project-name", { @@ -24,14 +24,22 @@ const createProject = (): CommandModule => { type: "string", choices: ["npm", "yarn", "pnpm"], alias: "p", - }); + }) + .option("directory", { + describe: "The directory for new project", + type: "string", + alias: "d", + }) + .implies("package-manager", "template") + .implies("template", "package-manager") return yargs; }, - handler: async ({projectName, packageManager, template}) => { - return await projectForm(projectName, packageManager, template); + handler: async ({projectName, packageManager , template, directory}) => { + return await projectForm(projectName, [packageManager, template, directory]); }, }; }; export { createProject }; + \ No newline at end of file diff --git a/src/new/form.ts b/src/new/form.ts index 9c72592..d3dd09e 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -1,10 +1,12 @@ -import inquirer from "inquirer"; import chalk from "chalk"; -import degit from "degit"; -import { spawn, execSync } from "child_process"; +import { execSync, spawn } from "child_process"; import { Presets, SingleBar } from "cli-progress"; +import degit from "degit"; +import inquirer from "inquirer"; import fs from "node:fs"; import path from "node:path"; +import { centerText } from "../utils/center-text"; +import { printError } from "../utils/cli-ui"; async function packageManagerInstall({ packageManager, @@ -35,7 +37,7 @@ async function packageManagerInstall({ if (code === 0) { resolve("Installation Done!"); } else { - reject(new Error(`npm install exited with code ${code}`)); + reject(new Error(`${packageManager} install exited with code ${code}`)); } }); }); @@ -45,12 +47,12 @@ async function checkIfPackageManagerExists(packageManager: string) { try { execSync(`${packageManager} --version`); return true; - } catch (_) { - throw new Error(`Package manager ${packageManager} is not installed`); + } catch (error) { + printError("Package manager not found!", packageManager); + process.exit(1); } } -// Change the package.json name to the user's project name function changePackageName({ directory, name, @@ -79,13 +81,31 @@ enum Template { } const enum PackageManager { - npm, - yarn, - pnpm, + npm = "npm", + yarn = "yarn", + pnpm = "pnpm" } -const projectForm = async (projectName: string, packageManager: PackageManager, template: keyof typeof Template): Promise => { +const projectForm = async (projectName: string, args: any[]): Promise => { let answer: any; + const projName: string = projectName; + let packageManager: PackageManager | undefined; + let template: keyof typeof Template | undefined; + let directory: string | undefined; + + // Resolving the argument order problem + for (const arg of args) { + if (args.length >= 3) { + if (arg === "npm" || arg === "yarn" || arg === "pnpm") { + packageManager = arg as PackageManager; + } else if (arg === "non-opinionated" || arg === "opinionated") { + template = arg as keyof typeof Template; + } else { + directory = arg; + } + } + } + if (packageManager && template) { answer = { name: projectName, @@ -94,7 +114,6 @@ const projectForm = async (projectName: string, packageManager: PackageManager, confirm: true, }; } else { - answer = await inquirer.prompt([ { type: "input", @@ -128,6 +147,16 @@ const projectForm = async (projectName: string, packageManager: PackageManager, }, ]); } + + if (directory) { + if(!fs.existsSync(path.join(directory, answer.name))) { + answer.name = path.join(directory, answer.name); + } else { + printError("Directory already exists", directory); + process.exit(1); + } + } + // Hashmap of templates and their directories const templates: Record = { "Non-Opinionated": "non_opinionated", @@ -135,12 +164,8 @@ const projectForm = async (projectName: string, packageManager: PackageManager, }; if (answer.confirm) { - // Check if the package manager exists - await checkIfPackageManagerExists(answer.packageManager).catch((err) => { - console.log(chalk.red(err.message)); - process.exit(1); - }); - + await checkIfPackageManagerExists(answer.packageManager); + console.log("\n"); const progressBar = new SingleBar( { format: @@ -156,38 +181,60 @@ const projectForm = async (projectName: string, packageManager: PackageManager, const [_, template] = answer.template.match(/(.*) ::/) as Array; - const emitter = degit( - `expressots/expressots/templates/${templates[template]}`, - ); - - await emitter.clone(answer.name); - + try { + const emitter = degit( + `expressots/expressots/templates/${templates[template]}`, + ); + + await emitter.clone(answer.name); + } catch (err: any) { + printError("Project already exists or Folder is not empty", answer.name); + process.exit(1); + } + progressBar.update(50, { doing: "Installing dependencies", }); - // Run the package manager install in the directory await packageManagerInstall({ packageManager: answer.packageManager, directory: answer.name, progressBar, }); + progressBar.update(90); changePackageName({ directory: answer.name, - name: answer.name, + name: projName, }); progressBar.update(100); progressBar.stop(); - console.log(chalk.green("Project created successfully!")); - console.log("Run the following commands to start the project:"); - console.log(chalk.bold(`cd ${answer.name}`)); - console.log(chalk.bold(`${answer.packageManager} start`)); + console.log("\n"); + console.log("šŸŽ Project ",chalk.green(projName), "created successfully!"); + console.log("šŸ¤™ Run the following commands to start the project:\n"); + + console.log(chalk.bold.gray(`$ cd ${answer.name}`)); + switch (answer.packageManager) { + case "npm": + console.log(chalk.bold.gray("$ npm run dev")); + break; + case "yarn": + console.log(chalk.bold.gray("$ yarn dev")); + break; + case "pnpm": + console.log(chalk.bold.gray("$ pnpm run dev")); + break; + } + + console.log("\n"); + console.log(chalk.bold.green(centerText("Happy coding!"))); + console.log(chalk.bold.gray(centerText("Please consider donating to support the project.\n"))); + console.log(chalk.bold.white(centerText("šŸ’– Sponsor: https://github.com/sponsors/expressots"))); } }; diff --git a/src/utils/center-text.ts b/src/utils/center-text.ts new file mode 100644 index 0000000..937bc61 --- /dev/null +++ b/src/utils/center-text.ts @@ -0,0 +1,9 @@ +function centerText(text: string): string { + const terminalWidth = process.stdout.columns; + const padding = Math.floor((terminalWidth - text.length) / 2); + const centeredText = ' '.repeat(padding) + text; + + return centeredText; +} + +export { centerText }; \ No newline at end of file diff --git a/src/utils/cli-ui.ts b/src/utils/cli-ui.ts new file mode 100644 index 0000000..44db51e --- /dev/null +++ b/src/utils/cli-ui.ts @@ -0,0 +1,5 @@ +import chalk from "chalk"; + +export function printError(message: string, component: string): void { + console.error(chalk.red(`\n\nšŸ˜ž ${message}:`,chalk.white(`[${component}]`))); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 41a4bf8..7cc6e3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { - "moduleDetection": "force", - "module": "commonjs", - "declaration": true, - "esModuleInterop": true, + "moduleDetection": "force", + "module": "commonjs", + "declaration": true, + "esModuleInterop": true, "moduleResolution": "node", - "resolveJsonModule": false, - "target": "ES2017", - "lib": ["ES2017"], - "outDir": "bin", + "resolveJsonModule": false, + "target": "ES2017", + "lib": ["ES2017"], + "outDir": "bin", "sourceMap": false, "strictNullChecks": false, "strict": true,