From afe9fa20885c809ddbce69835f30be16e8c60096 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Thu, 1 Aug 2024 23:40:17 -0700 Subject: [PATCH 1/5] feat: provider add(existing) and create(external) options --- src/cli.ts | 6 +- src/new/form.ts | 16 +-- src/providers/add/cli.ts | 29 +++++ src/providers/add/form.ts | 106 ++++++++++++++++++ src/providers/cli.ts | 46 -------- src/providers/create/cli.ts | 25 +++++ .../external.provider.ts => create/form.ts} | 43 ++++--- src/providers/index.ts | 3 +- src/utils/change-package-info.ts | 17 +++ 9 files changed, 210 insertions(+), 81 deletions(-) create mode 100644 src/providers/add/cli.ts create mode 100644 src/providers/add/form.ts delete mode 100644 src/providers/cli.ts create mode 100644 src/providers/create/cli.ts rename src/providers/{external/external.provider.ts => create/form.ts} (61%) create mode 100644 src/utils/change-package-info.ts diff --git a/src/cli.ts b/src/cli.ts index 8873c20..50fd0e5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,7 +7,8 @@ import { generateProject } from "./generate"; import { helpCommand } from "./help/cli"; import { infoProject } from "./info"; import { createProject } from "./new"; -import { generateProviders } from "./providers"; +import { createExternalProviderCMD } from "./providers/create/cli"; +import { addProviderCMD } from "./providers"; console.log(`\n[🐎 Expressots]\n`); @@ -15,7 +16,8 @@ yargs(hideBin(process.argv)) .scriptName("expressots") .command(runCommandModule) .command(createProject()) - .command(generateProviders()) + .command(createExternalProviderCMD()) + .command(addProviderCMD()) .command(generateProject()) .command(infoProject()) .command(helpCommand()) diff --git a/src/new/form.ts b/src/new/form.ts index 387039f..982c837 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -7,6 +7,7 @@ import fs from "node:fs"; import path from "node:path"; import { centerText } from "../utils/center-text"; import { printError } from "../utils/cli-ui"; +import { changePackageName } from "../utils/change-package-info"; async function packageManagerInstall({ packageManager, @@ -85,21 +86,6 @@ async function checkIfPackageManagerExists(packageManager: string) { } } -function changePackageName({ - directory, - name, -}: { - directory: string; - name: string; -}): void { - const absDirPath = path.resolve(directory); - const packageJsonPath = path.join(absDirPath, "package.json"); - const fileContents = fs.readFileSync(packageJsonPath, "utf-8"); - const packageJson = JSON.parse(fileContents); - packageJson.name = name; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); -} - function renameEnvFile(directory: string): void { try { const envExamplePath = path.join(directory, ".env.example"); diff --git a/src/providers/add/cli.ts b/src/providers/add/cli.ts new file mode 100644 index 0000000..3dc2deb --- /dev/null +++ b/src/providers/add/cli.ts @@ -0,0 +1,29 @@ +import { Argv, CommandModule } from "yargs"; +import { addExternalProvider } from "./form"; + +// eslint-disable-next-line @typescript-eslint/ban-types +type CommandModuleArgs = {}; + +export const addProviderCMD = (): CommandModule => { + return { + command: "add [version]", + describe: "Add an external provider to the project", + builder: (yargs: Argv): Argv => { + yargs + .positional("provider", { + describe: "The provider to be added to the project", + type: "string", + }) + .option("version", { + describe: "The provider version to be installed", + type: "string", + default: "latest", + alias: "v", + }); + return yargs; + }, + handler: async ({ provider, version }) => { + await addExternalProvider(provider, version); + }, + }; +}; diff --git a/src/providers/add/form.ts b/src/providers/add/form.ts new file mode 100644 index 0000000..88e99e1 --- /dev/null +++ b/src/providers/add/form.ts @@ -0,0 +1,106 @@ +import chalk from "chalk"; +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import { exit } from "node:process"; +import { printError } from "../../utils/cli-ui"; + +export const addExternalProvider = async ( + provider: string, + version: string, +): Promise => { + await installProvider(provider, version); +}; + +async function installProvider(provider: string, version: string) { + const packageManager = fs.existsSync( + "package-lock.json" || "yarn.lock" || "pnpm-lock.yaml", + ) + ? "npm" + : fs.existsSync("yarn.lock") + ? "yarn" + : fs.existsSync("pnpm-lock.yaml") + ? "pnpm" + : null; + + if (packageManager) { + console.log(`Installing ${provider} provider ...`); + const currentVersion = version === "latest" ? "" : `@${version}`; + await execProcess({ + commandArg: packageManager, + args: ["add", `${provider}${currentVersion}`, "--prefer-offline"], + directory: process.cwd(), + }); + } else { + printError( + "No package manager found in the project", + "install-provider", + ); + return; + } +} + +async function execProcess({ + commandArg, + args, + directory, +}: { + commandArg: string; + args: string[]; + directory: string; +}) { + return new Promise((resolve, reject) => { + const isWindows: boolean = process.platform === "win32"; + const command: string = isWindows ? `${commandArg}.cmd` : commandArg; + + const installProcess = spawn(command, args, { + cwd: directory, + shell: true, + }); + + console.log( + chalk.bold.blue(`Executing: ${commandArg} ${args.join(" ")}`), + ); + console.log( + chalk.yellow("-------------------------------------------------"), + ); + + installProcess.stdout.on("data", (data) => { + console.log(chalk.green(data.toString().trim())); // Display regular messages in green + }); + + installProcess.stderr.on("data", (data) => { + console.error(chalk.red(data.toString().trim())); // Display error messages in red + }); + + installProcess.on("close", (code) => { + if (code === 0) { + console.log( + chalk.bold.green( + "-------------------------------------------------", + ), + ); + console.log(chalk.bold.green("Installation Done!\n")); + resolve("Installation Done!"); + } else { + console.error( + chalk.bold.red("---------------------------------------"), + ); + console.error( + chalk.bold.red( + `Command ${command} ${args.join( + " ", + )} exited with code ${code}`, + ), + ); + reject( + new Error( + `Command ${command} ${args.join( + " ", + )} exited with code ${code}`, + ), + ); + exit(1); + } + }); + }); +} diff --git a/src/providers/cli.ts b/src/providers/cli.ts deleted file mode 100644 index 329b7f7..0000000 --- a/src/providers/cli.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Argv, CommandModule } from "yargs"; -import { externalProvider } from "./external/external.provider"; -import { prismaProvider } from "./prisma/prisma.provider"; - -// eslint-disable-next-line @typescript-eslint/ban-types -type CommandModuleArgs = {}; - -const generateProviders = (): CommandModule => { - return { - command: "add [library-version] [provider-version]", - describe: "Scaffold a new provider", - aliases: ["a"], - builder: (yargs: Argv): Argv => { - yargs - .positional("provider", { - choices: ["prisma", "provider"] as const, - describe: "The provider to add to the project", - type: "string", - alias: "p", - }) - .option("library-version", { - describe: "The library version to install", - type: "string", - default: "latest", - alias: "v", - }) - .option("provider-version", { - describe: "The version of the provider to install", - type: "string", - default: "latest", - alias: "vp", - }); - - return yargs; - }, - handler: async ({ provider, libraryVersion, providerVersion }) => { - if (provider === "prisma") { - await prismaProvider(libraryVersion, providerVersion); - } else if (provider === "provider") { - await externalProvider(); - } - }, - }; -}; - -export { generateProviders }; diff --git a/src/providers/create/cli.ts b/src/providers/create/cli.ts new file mode 100644 index 0000000..49d7b13 --- /dev/null +++ b/src/providers/create/cli.ts @@ -0,0 +1,25 @@ +import { Argv, CommandModule } from "yargs"; +import { createExternalProvider } from "./form"; + +// eslint-disable-next-line @typescript-eslint/ban-types +type CommandModuleArgs = {}; + +export const createExternalProviderCMD = (): CommandModule< + CommandModuleArgs, + any +> => { + return { + command: "create [provider]", + describe: "Scaffold a new provider", + builder: (yargs: Argv): Argv => { + yargs.option("provider", { + describe: "Provider name", + type: "string", + }); + return yargs; + }, + handler: async ({ provider }) => { + await createExternalProvider(provider); + }, + }; +}; diff --git a/src/providers/external/external.provider.ts b/src/providers/create/form.ts similarity index 61% rename from src/providers/external/external.provider.ts rename to src/providers/create/form.ts index c8af189..0554f48 100644 --- a/src/providers/external/external.provider.ts +++ b/src/providers/create/form.ts @@ -2,6 +2,7 @@ import chalk from "chalk"; import degit from "degit"; import inquirer from "inquirer"; import { centerText } from "../../utils/center-text"; +import { changePackageName } from "../../utils/change-package-info"; import { printError } from "../../utils/cli-ui"; async function printInfo(providerName: string): Promise { @@ -34,35 +35,43 @@ interface IExternalProvider { providerName: string; } -const externalProvider = async (): Promise => { +export const createExternalProvider = async ( + provider: string, +): Promise => { return new Promise(async (resolve, reject) => { - const providerInfo = await inquirer.prompt([ - { - type: "input", - name: "providerName", - message: "Type the name of your provider:", - default: "expressots-provider", - transformer: (input: string) => { - return chalk.yellow(chalk.bold(input)); + let providerInfo: IExternalProvider = {} as IExternalProvider; + providerInfo.providerName = provider; + + if (!provider) { + providerInfo = await inquirer.prompt([ + { + type: "input", + name: "providerName", + message: "Provider name", + default: "expressots-provider", + transformer: (input: string) => { + return chalk.yellow(chalk.bold(input)); + }, }, - }, - ]); + ]); + } try { const emitter = degit(`expressots/expressots-provider-template`); await emitter.clone(providerInfo.providerName); + + changePackageName({ + directory: providerInfo.providerName, + name: providerInfo.providerName, + }); + await printInfo(providerInfo.providerName); resolve(); } catch (err: any) { console.log("\n"); - printError( - "Project already exists or Folder is not empty", - "generate-external-provider", - ); + printError("Project already exists or Folder is not empty", ""); reject(err); } }); }; - -export { externalProvider }; diff --git a/src/providers/index.ts b/src/providers/index.ts index c1d55cf..1295915 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1 +1,2 @@ -export * from "./cli"; +export * from "./add/cli"; +export * from "./create/cli"; diff --git a/src/utils/change-package-info.ts b/src/utils/change-package-info.ts new file mode 100644 index 0000000..9884524 --- /dev/null +++ b/src/utils/change-package-info.ts @@ -0,0 +1,17 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function changePackageName({ + directory, + name, +}: { + directory: string; + name: string; +}): void { + const absDirPath = path.resolve(directory); + const packageJsonPath = path.join(absDirPath, "package.json"); + const fileContents = fs.readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(fileContents); + packageJson.name = name; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); +} From 5e766263976917ab2707075090956454180c39fb Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Fri, 2 Aug 2024 01:47:02 -0700 Subject: [PATCH 2/5] refactor: update ui command text --- src/cli.ts | 14 +++----------- src/commands/project.commands.ts | 2 +- src/generate/cli.ts | 2 +- src/help/cli.ts | 2 +- src/help/form.ts | 4 ++-- src/info/cli.ts | 2 +- src/new/cli.ts | 2 +- src/providers/add/cli.ts | 2 +- src/providers/create/cli.ts | 2 +- 9 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 50fd0e5..53b0e25 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,8 +9,9 @@ import { infoProject } from "./info"; import { createProject } from "./new"; import { createExternalProviderCMD } from "./providers/create/cli"; import { addProviderCMD } from "./providers"; +import chalk from "chalk"; -console.log(`\n[🐎 Expressots]\n`); +console.log(`\n${[chalk.bold.green("🐎 Expressots")]}\n`); yargs(hideBin(process.argv)) .scriptName("expressots") @@ -21,18 +22,9 @@ yargs(hideBin(process.argv)) .command(generateProject()) .command(infoProject()) .command(helpCommand()) - .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: \n" + + `${chalk.bold.green("For more information:")} \n\n` + "🌐 visit:\t https://expresso-ts.com\n" + "💖 Sponsor:\t https://github.com/sponsors/expressots", ) diff --git a/src/commands/project.commands.ts b/src/commands/project.commands.ts index 8a10486..4acdb16 100644 --- a/src/commands/project.commands.ts +++ b/src/commands/project.commands.ts @@ -86,7 +86,7 @@ const copyFiles = async () => { // eslint-disable-next-line @typescript-eslint/ban-types export const runCommandModule: CommandModule<{}, { command: string }> = { command: "run ", - describe: "Runs a specified command (dev, build, prod)", + describe: "Run command (dev, build, prod).", builder: (yargs: Argv) => { return yargs.positional("command", { describe: "The command to run", diff --git a/src/generate/cli.ts b/src/generate/cli.ts index 4bfc81c..ffc8ff8 100644 --- a/src/generate/cli.ts +++ b/src/generate/cli.ts @@ -30,7 +30,7 @@ const coerceSchematicAliases = (arg: string) => { const generateProject = (): CommandModule => { return { command: "generate [schematic] [path] [method]", - describe: "Scaffold a new resource", + describe: "Generate ExpressoTS resource.", aliases: ["g"], builder: (yargs: Argv): Argv => { yargs.positional("schematic", { diff --git a/src/help/cli.ts b/src/help/cli.ts index 489ce58..cfc2321 100644 --- a/src/help/cli.ts +++ b/src/help/cli.ts @@ -7,7 +7,7 @@ type CommandModuleArgs = {}; const helpCommand = (): CommandModule => { return { command: "resources", - describe: "Resource list", + describe: "Resource list.", aliases: ["r"], handler: async () => { await helpForm(); diff --git a/src/help/form.ts b/src/help/form.ts index 1c15272..be900e0 100644 --- a/src/help/form.ts +++ b/src/help/form.ts @@ -25,8 +25,8 @@ const helpForm = async (): Promise => { ["usecase", "g u", "Generate a usecase"], ["dto", "g d", "Generate a dto"], ["entity", "g e", "Generate an entity"], - ["provider", "g p", "Generate a provider"], - ["provider external", "a provider", "Generate an external provider"], + ["provider", "add", "Add provider to the project"], + ["provider", "create", "Create a provider"], ["module", "g mo", "Generate a module"], ["middleware", "g mi", "Generate a middleware"], ); diff --git a/src/info/cli.ts b/src/info/cli.ts index 7999541..745241a 100644 --- a/src/info/cli.ts +++ b/src/info/cli.ts @@ -7,7 +7,7 @@ type CommandModuleArgs = {}; const infoProject = (): CommandModule => { return { command: "info", - describe: "Displays project details", + describe: "Displays project info.", aliases: ["i"], handler: async () => { await infoForm(); diff --git a/src/new/cli.ts b/src/new/cli.ts index c808078..dfb10e7 100644 --- a/src/new/cli.ts +++ b/src/new/cli.ts @@ -54,7 +54,7 @@ const checkNodeVersion = (): void => { const createProject = (): CommandModule => { return { command: "new [package-manager] [template] [directory]", - describe: "Create a new project", + describe: "Create ExpressoTS application.", builder: commandOptions, handler: async ({ projectName, diff --git a/src/providers/add/cli.ts b/src/providers/add/cli.ts index 3dc2deb..7d92fe6 100644 --- a/src/providers/add/cli.ts +++ b/src/providers/add/cli.ts @@ -7,7 +7,7 @@ type CommandModuleArgs = {}; export const addProviderCMD = (): CommandModule => { return { command: "add [version]", - describe: "Add an external provider to the project", + describe: "Add provider to the project.", builder: (yargs: Argv): Argv => { yargs .positional("provider", { diff --git a/src/providers/create/cli.ts b/src/providers/create/cli.ts index 49d7b13..04e90df 100644 --- a/src/providers/create/cli.ts +++ b/src/providers/create/cli.ts @@ -10,7 +10,7 @@ export const createExternalProviderCMD = (): CommandModule< > => { return { command: "create [provider]", - describe: "Scaffold a new provider", + describe: "Create a provider.", builder: (yargs: Argv): Argv => { yargs.option("provider", { describe: "Provider name", From 9279fbf9feb1ec8fe2dbe4cf90e738f42ac82040 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Fri, 2 Aug 2024 13:48:22 -0700 Subject: [PATCH 3/5] fix: use stdout.write for optimal performance main menu --- src/cli.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index 53b0e25..c9ae889 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,8 +10,9 @@ import { createProject } from "./new"; import { createExternalProviderCMD } from "./providers/create/cli"; import { addProviderCMD } from "./providers"; import chalk from "chalk"; +import { stdout } from "process"; -console.log(`\n${[chalk.bold.green("🐎 Expressots")]}\n`); +stdout.write(`\n${[chalk.bold.green("🐎 Expressots")]}\n\n`); yargs(hideBin(process.argv)) .scriptName("expressots") From f50547898976030fd1979f12eedc148f8d2dff47 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Fri, 2 Aug 2024 20:21:22 -0700 Subject: [PATCH 4/5] feat: adjust ui and add dev, build, prod as individual cmd --- src/cli.ts | 10 ++++- src/commands/project.commands.ts | 67 +++++++++++++++++++++----------- src/utils/cli-ui.ts | 9 +++++ 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index c9ae889..78147b3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,11 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { runCommandModule } from "./commands/project.commands"; +import { + devCommand, + buildCommand, + prodCommand, +} from "./commands/project.commands"; import { generateProject } from "./generate"; import { helpCommand } from "./help/cli"; import { infoProject } from "./info"; @@ -16,8 +20,10 @@ stdout.write(`\n${[chalk.bold.green("🐎 Expressots")]}\n\n`); yargs(hideBin(process.argv)) .scriptName("expressots") - .command(runCommandModule) .command(createProject()) + .command(devCommand) + .command(buildCommand) + .command(prodCommand) .command(createExternalProviderCMD()) .command(addProviderCMD()) .command(generateProject()) diff --git a/src/commands/project.commands.ts b/src/commands/project.commands.ts index 4acdb16..7b2e1db 100644 --- a/src/commands/project.commands.ts +++ b/src/commands/project.commands.ts @@ -2,7 +2,9 @@ import { spawn } from "child_process"; import { promises as fs } from "fs"; import path from "path"; import { Argv, CommandModule } from "yargs"; +import { printError, printSuccess } from "../utils/cli-ui"; import Compiler from "../utils/compiler"; +import os from "os"; /** * Load the configuration from the compiler @@ -12,6 +14,7 @@ import Compiler from "../utils/compiler"; const opinionatedConfig: Array = [ "--transpile-only", + "--clear", "-r", "dotenv/config", "-r", @@ -21,6 +24,7 @@ const opinionatedConfig: Array = [ const nonOpinionatedConfig: Array = [ "--transpile-only", + "--clear", "-r", "dotenv/config", "./src/main.ts", @@ -58,11 +62,13 @@ function execCmd( // Helper to delete the dist directory const cleanDist = async (): Promise => { await fs.rm("./dist", { recursive: true, force: true }); + printSuccess("Deleted dist directory", "clean-dist"); }; // Helper to compile TypeScript const compileTypescript = async () => { await execCmd("npx", ["tsc", "-p", "tsconfig.build.json"]); + printSuccess("Built successfully", "compile-typescript"); }; // Helper to copy files @@ -83,31 +89,47 @@ const copyFiles = async () => { }); }; -// eslint-disable-next-line @typescript-eslint/ban-types -export const runCommandModule: CommandModule<{}, { command: string }> = { - command: "run ", - describe: "Run command (dev, build, prod).", - builder: (yargs: Argv) => { - return yargs.positional("command", { - describe: "The command to run", - type: "string", - choices: ["dev", "build", "prod"], - }); +// Helper clear screen +const clearScreen = () => { + const platform = os.platform(); + const command = platform === "win32" ? "cls" : "clear"; + spawn(command, { stdio: "inherit", shell: true }); +}; + +export const devCommand: CommandModule = { + command: "dev", + describe: "Start development server.", + handler: async () => { + await runCommand({ command: "dev" }); }, - handler: async (argv) => { - const { command } = argv; - // Now call your original runCommand function with the command - // Ensure runCommand is properly defined to handle these commands - await runCommand({ command }); +}; + +export const buildCommand: CommandModule = { + command: "build", + describe: "Build the project.", + handler: async () => { + await runCommand({ command: "build" }); }, }; -const runCommand = async ({ command }: { command: string }): Promise => { +export const prodCommand: CommandModule = { + command: "prod", + describe: "Run in production mode.", + handler: async () => { + await runCommand({ command: "prod" }); + }, +}; + +export const runCommand = async ({ + command, +}: { + command: string; +}): Promise => { const { opinionated } = await Compiler.loadConfig(); + try { switch (command) { case "dev": - // Use execSync or spawn to run ts-node-dev programmatically execCmd( "tsnd", opinionated ? opinionatedConfig : nonOpinionatedConfig, @@ -131,16 +153,15 @@ const runCommand = async ({ command }: { command: string }): Promise => { } else { config = ["-r", "dotenv/config", "./dist/main.js"]; } - // Ensure environment variables are set + clearScreen(); execCmd("node", config); break; } default: - console.log(`Unknown command: ${command}`); + printError(`Unknown command: `, command); + break; } - } catch (error) { - console.error("Error executing command:", error); + } catch (error: Error | any) { + printError("Error executing command:", error.message); } }; - -export { runCommand }; diff --git a/src/utils/cli-ui.ts b/src/utils/cli-ui.ts index 08f7755..189a6df 100644 --- a/src/utils/cli-ui.ts +++ b/src/utils/cli-ui.ts @@ -7,6 +7,15 @@ export function printError(message: string, component: string): void { ); } +export function printSuccess(message: string, component: string): void { + stdout.write( + chalk.green( + `${message}:`, + chalk.bold(chalk.white(`[${component}] ✔️\n`)), + ), + ); +} + export function printWarning(message: string, component?: string): void { if (component === undefined) { stdout.write(chalk.yellow(`${message} ⚠️\n`)); From 7e465b4169f53bcdf0f00332653428c4cdda7b1f Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Fri, 2 Aug 2024 20:34:54 -0700 Subject: [PATCH 5/5] refactor: snyk glob security update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44133e3..cc0ea0d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "cli-progress": "3.12.0", "cli-table3": "0.6.5", "degit": "2.8.4", - "glob": "10.4.1", + "glob": "10.4.5", "inquirer": "8.2.6", "mustache": "4.2.0", "semver": "7.6.2",