diff --git a/src/commands/plugin/index.ts b/src/commands/plugin/index.ts index f7fec39..ff16195 100644 --- a/src/commands/plugin/index.ts +++ b/src/commands/plugin/index.ts @@ -8,14 +8,13 @@ import fs, { readSync, writeFileSync, } from "fs"; -import { execSync } from "child_process"; +import { execSync, spawn } from "child_process"; import inquirer from "inquirer"; import * as yaml from "js-yaml"; import { cwd } from "process"; import chalk from "chalk"; import ora from "ora"; -// Load the plugins list from JSON file const pluginsFilePath = path.resolve(__dirname, '../../../pluginsList.json'); if (!fs.existsSync(pluginsFilePath)) { console.error("Error: pluginsList.json file not found!"); @@ -24,12 +23,6 @@ import ora from "ora"; const pluginsData = fs.readFileSync(pluginsFilePath, { encoding: "utf-8" }); const availablePlugins = JSON.parse(pluginsData); - // or Search the plugins list from npm - // const command = "npm search @godspeedsystems/plugins --json"; - // const stdout = execSync(command, { encoding: "utf-8" }); - // const availablePlugins = JSON.parse(stdout.trim()); - - // Map to the format expected by the UI const pluginNames = availablePlugins.map((plugin: { value: string; name: string; description: string }) => ({ value: plugin.value, Name: plugin.name.split("plugins-")[1], @@ -40,8 +33,149 @@ const program = new Command(); type ModuleType = "DS" | "ES" | "BOTH"; +async function manuallyConfigurePlugin(pluginName: string) { + + const pluginShortName = pluginName.split('/').pop()?.replace('plugins-', '') + .replace('-as-eventsource', '') + .replace('-as-datasource', '') || ''; + + let moduleType = "ES"; + if (pluginName.includes('-as-datasource') && pluginName.includes('-as-eventsource')) { + moduleType = "BOTH"; + } else if (pluginName.includes('-as-datasource')) { + moduleType = "DS"; + } + + try { + switch (moduleType) { + case "BOTH": + { + mkdirSync( + path.join(process.cwd(), "src", "eventsources", "types"), + { + recursive: true, + } + ); + mkdirSync(path.join(process.cwd(), "src", "datasources", "types"), { + recursive: true, + }); + + // Write EventSource files + writeFileSync( + path.join( + process.cwd(), + "src", + "eventsources", + "types", + `${pluginShortName}.ts` + ), + ` +import { EventSource } from '${pluginName}'; +export default EventSource; + ` + ); + writeFileSync( + path.join( + process.cwd(), + "src", + "eventsources", + `${pluginShortName}.yaml` + ), + yaml.dump({ type: pluginShortName }) + ); + + // Write DataSource files + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + "types", + `${pluginShortName}.ts` + ), + ` +import { DataSource } from '${pluginName}'; +export default DataSource; + ` + ); + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + `${pluginShortName}.yaml` + ), + yaml.dump({ type: pluginShortName }) + ); + } + break; + case "DS": + { + mkdirSync(path.join(process.cwd(), "src", "datasources", "types"), { + recursive: true, + }); + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + "types", + `${pluginShortName}.ts` + ), + ` +import { DataSource } from '${pluginName}'; +export default DataSource; + ` + ); + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + `${pluginShortName}.yaml` + ), + yaml.dump({ type: pluginShortName }) + ); + } + break; + case "ES": + { + mkdirSync(path.join(process.cwd(), "src", "eventsources", "types"), { + recursive: true, + }); + writeFileSync( + path.join( + process.cwd(), + "src", + "eventsources", + "types", + `${pluginShortName}.ts` + ), + ` +import { EventSource } from '${pluginName}'; +export default EventSource; + ` + ); + writeFileSync( + path.join( + process.cwd(), + "src", + "eventsources", + `${pluginShortName}.yaml` + ), + yaml.dump({ type: pluginShortName }) + ); + } + break; + } + return true; + } catch (error) { + console.error(`❌ Error manually configuring plugin ${pluginName}:`, error); + return false; + } +} + const addAction = async (pluginsList: string[]) => { - // install that package const spinner = ora({ text: "Installing plugins... ", spinner: { @@ -52,179 +186,254 @@ const addAction = async (pluginsList: string[]) => { async function installPlugin(pluginsList: string[]) { try { + console.log("Starting plugin installation..."); + console.log(`Plugins to install: ${pluginsList.join(', ')}`); + + const hasPnpm = await checkCommandExists("pnpm"); + + const packageManager = hasPnpm ? "pnpm" : "npm"; + console.log(`Using package manager: ${packageManager}`); + + spinner.text = `Installing plugins with ${packageManager}...`; spinner.start(); - - // Use spawnCommand instead of spawnSync - const child = spawnSync( - "npm", - [ - "install", - ...pluginsList, - "--quiet", - "--no-warnings", - "--silent", - "--progress=false", - ], - { - stdio: "inherit", // Redirect output + + const startTime = Date.now(); + let dots = 0; + + const intervalId = setInterval(() => { + dots = (dots + 1) % 4; + const elapsed = Math.floor((Date.now() - startTime) / 1000); + spinner.text = `Installing plugins with ${packageManager}${'.'.repeat(dots)} (${elapsed}s elapsed)`; + }, 1000); + + return new Promise((resolve, reject) => { + const { exec } = require('child_process'); + + let cmd = ''; + if (packageManager === "pnpm") { + cmd = `pnpm add ${pluginsList.join(' ')} --reporter=silent`; + } else { + cmd = `npm install ${pluginsList.join(' ')} --quiet --no-warnings --silent --progress=false`; } - ); - - await new Promise((resolve) => { - child.on("close", () => { + + + const childProcess = exec(cmd, { cwd: process.cwd() }); + + let stdoutData = ''; + let stderrData = ''; + + childProcess.stdout?.on('data', (data: any) => { + stdoutData += data; + if (data.trim()) { + // console.log(`[${packageManager} output]: ${data.trim()}`); + } + }); + + childProcess.stderr?.on('data', (data: any) => { + stderrData += data; + if (data.trim()) { + console.error(`[${packageManager} error]: ${data.trim()}`); + } + }); + + childProcess.on('exit', (code: number) => { + clearInterval(intervalId); + + if (code !== 0) { + spinner.stop(); + console.error(`Installation failed with exit code: ${code}`); + console.error("Error output:", stderrData || "No error output"); + reject(new Error(`Process exited with code ${code}`)); + return; + } + + const totalTime = ((Date.now() - startTime) / 1000).toFixed(1); + spinner.stop(); + console.log(`\nPlugins installed successfully in ${totalTime}s!`); + console.log(`Installed plugins: ${pluginsList.join(', ')}`); + console.log(chalk.cyan.bold("Happy coding with Godspeed! 🚀🎉\n")); resolve(); }); }); - - spinner.stop(); // Stop the spinner when the installation is complete - console.log("\nPlugins installed successfully!"); - console.log(chalk.cyan.bold("Happy coding with Godspeed! 🚀🎉\n")); } catch (error: any) { - spinner.stop(); // Stop the spinner in case of an error + spinner.stop(); console.error("Error during installation:", error.message); + throw error; } } - - // Call the installPlugin function - await installPlugin(pluginsList); - - pluginsList.map(async (pluginName: string) => { - try { - const Module = await import( - path.join(process.cwd(), "node_modules", pluginName) - ); - - let moduleType = Module.SourceType as ModuleType; - let loaderFileName = Module.Type as string; - let yamlFileName = Module.CONFIG_FILE_NAME as string; - let defaultConfig = Module.DEFAULT_CONFIG || ({} as PlainObject); - - switch (moduleType) { - case "BOTH": - { - mkdirSync( - path.join(process.cwd(), "src", "eventsources", "types"), + + // Helper function to check if a command exists + async function checkCommandExists(command: string): Promise { + return new Promise((resolve) => { + const { exec } = require('child_process'); + const checkCmd = process.platform === 'win32' ? 'where' : 'which'; + + exec(`${checkCmd} ${command}`, (error: any) => { + resolve(!error); + }); + }); + } + try { + await installPlugin(pluginsList); + + for (const pluginName of pluginsList) { + try { + let configSuccess = false; + + try { + console.log(`Trying to import and configure ${pluginName}...`); + const Module = await import( + path.join(process.cwd(), "node_modules", pluginName) + ); + + let moduleType = Module.SourceType as ModuleType; + let loaderFileName = Module.Type as string; + let yamlFileName = Module.CONFIG_FILE_NAME as string; + let defaultConfig = Module.DEFAULT_CONFIG || ({} as PlainObject); + + switch (moduleType) { + case "BOTH": { - recursive: true, + mkdirSync( + path.join(process.cwd(), "src", "eventsources", "types"), + { + recursive: true, + } + ); + mkdirSync(path.join(process.cwd(), "src", "datasources", "types"), { + recursive: true, + }); + + writeFileSync( + path.join( + process.cwd(), + "src", + "eventsources", + "types", + `${loaderFileName}.ts` + ), + ` + import { EventSource } from '${pluginName}'; + export default EventSource; + ` + ); + writeFileSync( + path.join( + process.cwd(), + "src", + "eventsources", + `${yamlFileName}.yaml` + ), + yaml.dump({ type: loaderFileName, ...defaultConfig }) + ); + + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + "types", + `${loaderFileName}.ts` + ), + ` + import { DataSource } from '${pluginName}'; + export default DataSource; + ` + ); + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + `${yamlFileName}.yaml` + ), + yaml.dump({ type: loaderFileName, ...defaultConfig }) + ); } - ); - mkdirSync(path.join(process.cwd(), "src", "datasources", "types"), { - recursive: true, - }); - - writeFileSync( - path.join( - process.cwd(), - "src", - "eventsources", - "types", - `${loaderFileName}.ts` - ), - ` - import { EventSource } from '${pluginName}'; - export default EventSource; - ` - ); - writeFileSync( - path.join( - process.cwd(), - "src", - "eventsources", - `${yamlFileName}.yaml` - ), - yaml.dump({ type: loaderFileName, ...defaultConfig }) - ); - - writeFileSync( - path.join( - process.cwd(), - "src", - "datasources", - "types", - `${loaderFileName}.ts` - ), - ` - import { DataSource } from '${pluginName}'; - export default DataSource; - ` - ); - writeFileSync( - path.join( - process.cwd(), - "src", - "datasources", - `${yamlFileName}.yaml` - ), - yaml.dump({ type: loaderFileName, ...defaultConfig }) - ); - } - break; - case "DS": - { - mkdirSync(path.join(process.cwd(), "src", "datasources", "types"), { - recursive: true, - }); - writeFileSync( - path.join( - process.cwd(), - "src", - "datasources", - "types", - `${loaderFileName}.ts` - ), - ` - import { DataSource } from '${pluginName}'; - export default DataSource; + break; + case "DS": + { + mkdirSync(path.join(process.cwd(), "src", "datasources", "types"), { + recursive: true, + }); + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + "types", + `${loaderFileName}.ts` + ), + ` + import { DataSource } from '${pluginName}'; + export default DataSource; + ` + ); + if (Module.Type !== "prisma") { + writeFileSync( + path.join( + process.cwd(), + "src", + "datasources", + `${yamlFileName}.yaml` + ), + yaml.dump({ type: loaderFileName, ...defaultConfig }) + ); + } + } + break; + case "ES": { + mkdirSync(path.join(process.cwd(), "src", "eventsources", "types"), { + recursive: true, + }); + writeFileSync( + path.join( + process.cwd(), + "src", + "eventsources", + "types", + `${loaderFileName}.ts` + ), ` - ); - // special case for prisma for now - // @ts-ignore - if (Module.Type !== "prisma") { + import { EventSource } from '${pluginName}'; + export default EventSource; + ` + ); writeFileSync( path.join( process.cwd(), "src", - "datasources", + "eventsources", `${yamlFileName}.yaml` ), yaml.dump({ type: loaderFileName, ...defaultConfig }) ); } } - break; - case "ES": { - mkdirSync(path.join(process.cwd(), "src", "eventsources", "types"), { - recursive: true, - }); - writeFileSync( - path.join( - process.cwd(), - "src", - "eventsources", - "types", - `${loaderFileName}.ts` - ), - ` - import { EventSource } from '${pluginName}'; - export default EventSource; - ` - ); - writeFileSync( - path.join( - process.cwd(), - "src", - "eventsources", - `${yamlFileName}.yaml` - ), - yaml.dump({ type: loaderFileName, ...defaultConfig }) - ); + + configSuccess = true; + console.log(`✅ Plugin ${pluginName} dynamically configured.`); + + } catch (importError: any) { + // console.log(`Dynamic import failed for ${pluginName}: ${importError?.message}`); + // console.log("Falling back to manual configuration..."); + + configSuccess = await manuallyConfigurePlugin(pluginName); } + + if (configSuccess) { + console.log(`✅ Plugin "${pluginName}" installed and configured.`); + } else { + console.warn(`⚠️ Plugin "${pluginName}" installed but configuration may be incomplete.`); + } + + } catch (error) { + console.error("Unable to configure plugin:", error); } - } catch (error) { - console.error("unable to import the module.", error); } - }); - // create folder for eventsource or datasource respective file + } catch (error) { + console.error("Plugin installation failed:", error); + } }; const add = program @@ -277,8 +486,6 @@ const add = program async function runPrompt() { try { const answer = await inquirer.prompt([tableCheckboxPrompt]); - // console.log(answer); - // Call the add action with pluginName if (answer.gsPlugin.length !== 0) { await addAction(answer.gsPlugin); } else { @@ -289,7 +496,6 @@ const add = program } } - // Call the prompt function runPrompt(); } } else { @@ -297,7 +503,7 @@ const add = program for (const plugin of pluginNames) { if (plugin.value === givenPluginName) { chosenPluginName = plugin; - break; // Exit the loop once a match is found + break; } } if (chosenPluginName !== null) { @@ -330,7 +536,6 @@ const removeAction = async (pluginsList: string[]) => { try { spinner.start(); - // Use spawnCommand instead of spawnSync const child = spawnSync( "npm", [ @@ -342,7 +547,7 @@ const removeAction = async (pluginsList: string[]) => { "--progress=false", ], { - stdio: "inherit", // Redirect output + stdio: "inherit", } ); @@ -352,23 +557,21 @@ const removeAction = async (pluginsList: string[]) => { }); }); - spinner.stop(); // Stop the spinner when the installation is complete + spinner.stop(); console.log("\nPlugins uninstalled successfully!"); console.log(chalk.cyan.bold("Happy coding with Godspeed! 🚀🎉\n")); } catch (error: any) { - spinner.stop(); // Stop the spinner in case of an error + spinner.stop(); console.error("Error during installation:", error.message); } } pluginsList.map(async (pluginName: string) => { try { - // Import the module dynamically const Module = await import( path.join(process.cwd(), "node_modules", pluginName) ); - // Define module-specific variables let moduleType = Module.SourceType as ModuleType; let loaderFileName = Module.Type as string; let yamlFileName = Module.CONFIG_FILE_NAME as string; @@ -376,12 +579,10 @@ const removeAction = async (pluginsList: string[]) => { switch (moduleType) { case "BOTH": - // Remove both EventSource and DataSource files await removeModule("ES", pluginName, loaderFileName, yamlFileName); await removeModule("DS", pluginName, loaderFileName, yamlFileName); break; - // Remove either EventSource or DataSource files case "ES": await removeModule( @@ -406,14 +607,11 @@ const removeAction = async (pluginsList: string[]) => { break; } - // spawnSync("npm", ["uninstall", pluginName], { stdio: "inherit" }); - // Use spawnCommand instead of spawnSync } catch (error) { console.error("Unable to remove the plugin.", error); } }); - // Define a function to remove EventSource or DataSource files const removeModule = async ( moduleType: ModuleType, pluginName: string, @@ -421,7 +619,6 @@ const removeAction = async (pluginsList: string[]) => { yamlFileName: string ) => { try { - // Determine the paths to the TypeScript and YAML files const tsFilePath = path.join( process.cwd(), moduleType === "ES" @@ -435,7 +632,6 @@ const removeAction = async (pluginsList: string[]) => { `${yamlFileName}.yaml` ); - // Check if the TypeScript and YAML files exist and remove them await Promise.all([ fs.unlink(tsFilePath, (err) => {}), fs.unlink(yamlFilePath, (err) => {}), @@ -460,19 +656,16 @@ const remove = program } else { let pluginsList: any; try { - // List all the installed plugins let pkgPath = path.join(cwd(), "package.json"); pluginsList = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, { encoding: "utf-8" })) .dependencies : {}; - // console.log(pluginsList) for (const pluginName in pluginsList) { const isGSPlugin = pluginName.includes("@godspeedsystems/plugins"); !isGSPlugin && delete pluginsList[pluginName]; } - // If package.json doesn't have "dependencies" key or no valid plugins are found if (!pluginsList || Object.keys(pluginsList).length === 0) { throw new Error(); } @@ -485,7 +678,6 @@ const remove = program pluginsList = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, { encoding: "utf-8" })).dependencies : {}; - // console.log(pluginsList) for (const pluginName in pluginsList) { const isGSPlugin = pluginName.includes("@godspeedsystems/plugins"); !isGSPlugin && delete pluginsList[pluginName]; @@ -533,19 +725,17 @@ const update = program .action(async (pluginName: string) => { let pluginsList: string[]; try { - // list all the installed plugins let pkgPath = path.join(cwd(), "package.json"); pluginsList = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, { encoding: "utf-8" })).dependencies : []; for (const pluginName in pluginsList) { - // if the dependency name does not start with @godspeedsystems/plugin-, then it's not a godspeed plugin + const isGSPlugin = pluginName.includes("@godspeedsystems/plugins"); !isGSPlugin && delete pluginsList[pluginName]; } - // id package.json dont have "dependencies" key if (!pluginsList || pluginsList.length) throw new Error(); } catch (error) { console.error("There are no eventsource/datasource plugins installed."); @@ -563,7 +753,6 @@ const update = program try { spinner.start(); - // Use spawnCommand instead of spawnSync const child = spawnSync( "npm", [ @@ -575,7 +764,7 @@ const update = program "--progress=false", ], { - stdio: "inherit", // Redirect output + stdio: "inherit", } ); @@ -585,11 +774,11 @@ const update = program }); }); - spinner.stop(); // Stop the spinner when the installation is complete + spinner.stop(); console.log("\nPlugins updated successfully!"); console.log(chalk.cyan.bold("Happy coding with Godspeed! 🚀🎉\n")); } catch (error: any) { - spinner.stop(); // Stop the spinner in case of an error + spinner.stop(); console.error("Error during updation:", error.message); } } @@ -599,13 +788,11 @@ const update = program pluginsList = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, { encoding: "utf-8" })).dependencies : {}; - // console.log(pluginsList) for (const pluginName in pluginsList) { const isGSPlugin = pluginName.includes("@godspeedsystems/plugins"); !isGSPlugin && delete pluginsList[pluginName]; } - // console.log(pluginsList) const commonPlugins = pluginNames.filter( (plugin: any) => pluginsList[plugin.value] ); diff --git a/src/utils/index.ts b/src/utils/index.ts index 6278f8c..2d5d949 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,8 +11,14 @@ import chalk from "chalk"; import { spawnSync } from "child_process"; import spawnCommand from "cross-spawn"; import ora from "ora"; -import { yamlLoader as loadYaml, generateSwaggerJSON, logger , PlainObject, yamlLoader} from '@godspeedsystems/core'; -const { exec } = require('child_process'); +import { + yamlLoader as loadYaml, + generateSwaggerJSON, + logger, + PlainObject, + yamlLoader, +} from "@godspeedsystems/core"; +const { exec } = require("child_process"); const userID = (): string => { if (process.platform == "linux") { @@ -163,7 +169,7 @@ export const compileAndCopyOrJustCopy = async ( isUpdateCall = fsExtras .lstatSync(path.resolve(process.cwd(), ".godspeed")) .isFile(); - } catch (error) { } + } catch (error) {} fileList.map(async (sourceFilePath: string) => { if (fsExtras.lstatSync(sourceFilePath).isFile()) { @@ -173,17 +179,17 @@ export const compileAndCopyOrJustCopy = async ( relativeDestinationPath = !isUpdateCall ? path.relative( - path.resolve(projectDirPath, sourceFolder), - sourceFilePath - ) - : path.resolve( - projectDirPath, - destinationFolder, - path.relative( path.resolve(projectDirPath, sourceFolder), sourceFilePath ) - ); + : path.resolve( + projectDirPath, + destinationFolder, + path.relative( + path.resolve(projectDirPath, sourceFolder), + sourceFilePath + ) + ); let finalDestinationWithFileName = path.resolve( projectDirPath, @@ -242,66 +248,161 @@ export const compileAndCopyOrJustCopy = async ( } }; - - export const installDependencies = async ( projectDirPath: string, projectName: string ) => { - async function installPlugin() { - const spinner = ora({ - spinner: { - frames: ["🌍 ", "🌎 ", "🌏 ", "🌐 ", "🌑 ", "🌒 ", "🌓 ", "🌔 "], - interval: 180, - }, - }).start("installing dependencies..."); - try { - // Use spawnCommand instead of spawnSync - const child = spawnCommand( - "npm", - [ - "install", - "--quiet", - "--no-warnings", - "--silent", - "--progress=false" - ], - { - cwd: projectDirPath, - stdio: "inherit", // Redirect output + const spinner = ora({ + spinner: { + frames: ["🌍 ", "🌎 ", "🌏 ", "🌐 ", "🌑 ", "🌒 ", "🌓 ", "🌔 "], + interval: 180, + }, + }).start("checking package managers..."); + + try { + // Check if pnpm is already available + const hasPnpm = await checkCommandExists("pnpm"); + + // If pnpm is not available, try to use corepack + if (!hasPnpm) { + const hasCorepack = await checkCommandExists("corepack"); + + if (hasCorepack) { + spinner.text = "setting up pnpm via corepack..."; + await enableCorepackAndPnpm(); + } else { + spinner.text = "falling back to npm (slower)..."; + } + } + + // Choose the best available package manager + const packageManager = (await checkCommandExists("pnpm")) ? "pnpm" : "npm"; + + spinner.text = `installing dependencies with ${packageManager} (this may take a minute)...`; + + // Update spinner text periodically to show activity + const intervalId = setInterval(() => { + spinner.text = `still installing... (${new Date().toLocaleTimeString()})`; + }, 10000); + + const installArgs = + packageManager === "pnpm" + ? ["install", "--reporter=silent"] + : [ + "install", + "--prefer-offline", + "--no-audit", + "--silent", + "--progress=false", + ]; + + await new Promise((resolve, reject) => { + const child = spawnCommand(packageManager, installArgs, { + cwd: projectDirPath, + stdio: "pipe", + }); + + child.on("close", (code) => { + clearInterval(intervalId); + if (code === 0) { + resolve(); + } else { + reject(new Error(`Process exited with code ${code}`)); } - ); - child.on("close", () => { - spinner.stop(); // Stop the spinner when the installation is complete - console.log("\ndependencies installed successfully!"); + }); - console.log( - `${chalk.green("\nSuccessfully created the project")} ${chalk.yellow( - projectName - )}.` - ); + child.on("error", (err) => { + clearInterval(intervalId); + reject(err); + }); + }); - console.log( - `${chalk.green( - "Use `godspeed help` command for available commands." - )} ${chalk.green.bold( - "\n\nHappy building microservices with Godspeed! 🚀🎉\n" - )}` - ); + spinner.stop(); + console.log("\ndependencies installed successfully!"); + console.log( + `${chalk.green("\nSuccessfully created the project")} ${chalk.yellow( + projectName + )}.` + ); + console.log( + `${chalk.green( + "Use `godspeed help` command for available commands." + )} ${chalk.green.bold( + "\n\nHappy building microservices with Godspeed! 🚀🎉\n" + )}` + ); + } catch (error: any) { + spinner.stop(); + console.error("Error during installation:", error.message); + } +}; + +// Check if a command exists and is executable +async function checkCommandExists(command: string): Promise { + try { + // Use 'which' on Unix-like systems or 'where' on Windows + const checkCmd = process.platform === "win32" ? "where" : "which"; + + return new Promise((resolve) => { + exec(`${checkCmd} ${command}`, (error: any) => { + resolve(!error); }); - } catch (error: any) { - spinner.stop(); // Stop the spinner in case of an error - console.error("Error during installation:", error.message); - } + }); + } catch (error) { + return false; } +} +// Enable corepack and prepare pnpm +async function enableCorepackAndPnpm(): Promise { + try { + // Enable corepack + await new Promise((resolve, reject) => { + const child = spawnCommand("corepack", ["enable"], { stdio: "pipe" }); - // Call the installPlugin function - await installPlugin(); + child.on("close", (code) => { + if (code === 0 || code === 1) { + resolve(); + } else { + reject(new Error(`Corepack enable failed with code ${code}`)); + } + }); -}; + child.on("error", (err) => { + reject(err); + }); + }); -export const installPackage = async (projectDirPath: string, package_name: string) => { + // Prepare and activate pnpm + await new Promise((resolve, reject) => { + const child = spawnCommand( + "corepack", + ["prepare", "pnpm@latest", "--activate"], + { stdio: "pipe" } + ); + + child.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`pnpm preparation failed with code ${code}`)); + } + }); + + child.on("error", (err) => { + reject(err); + }); + }); + } catch (error) { + // Continue even if enabling corepack or preparing pnpm fails + console.log("Failed to set up pnpm, falling back to npm"); + } +} + +export const installPackage = async ( + projectDirPath: string, + package_name: string +) => { async function installprisma(): Promise { const command = `npm install ${package_name}`; @@ -311,22 +412,23 @@ export const installPackage = async (projectDirPath: string, package_name: strin stdio: "inherit", // Redirect output }); - child.on('exit', (code: any) => { + child.on("exit", (code: any) => { if (code === 0) { resolve(); } else { - reject(new Error(`Command exited with non-zero status code: ${code}`)); + reject( + new Error(`Command exited with non-zero status code: ${code}`) + ); } }); - child.on('error', (error: any) => { + child.on("error", (error: any) => { reject(error); }); }); } - await installprisma() -} - + await installprisma(); +}; export const generateProjectFromDotGodspeed = async ( projectName: string, @@ -366,7 +468,7 @@ export const generateProjectFromDotGodspeed = async ( ); // generate package.json, tsConfig.json - for (let file of ['package.json', 'tsconfig.json']) { + for (let file of ["package.json", "tsconfig.json"]) { const packageJson = await fsExtras.readJson( path.resolve(projectDirPath, `.template/${file}`) ); @@ -383,7 +485,6 @@ export const generateProjectFromDotGodspeed = async ( ); } - // generate .swcrc file const swcrc = await fsExtras.readJson( path.resolve(projectDirPath, ".template/dot-configs/.swcrc") @@ -483,23 +584,24 @@ export const generateProjectFromDotGodspeed = async ( } catch (error) { log.fatal("Error while generating files.", error); } -} +}; export const genGraphqlSchema = async () => { try { - const availableApoloeventsources = globSync( - path.join(process.cwd(), 'src/eventsources/*.yaml').replace(/\\/g, '/') + path.join(process.cwd(), "src/eventsources/*.yaml").replace(/\\/g, "/") ); // Filter files that contain 'Apollo' in their name - const apolloEventsources = availableApoloeventsources - .map((file) => path.parse(file).name); + const apolloEventsources = availableApoloeventsources.map( + (file) => path.parse(file).name + ); const questions = [ { - type: 'checkbox', - name: 'selectedOptions', - message: 'Please select the Graphql Event Sources for which you wish to generate the Graphql schema from Godspeed event defs:', + type: "checkbox", + name: "selectedOptions", + message: + "Please select the Graphql Event Sources for which you wish to generate the Graphql schema from Godspeed event defs:", choices: apolloEventsources, }, ]; @@ -508,9 +610,11 @@ export const genGraphqlSchema = async () => { try { const answers = await inquirer.prompt(questions); if (answers.selectedOptions.length == 0) { - console.log(chalk.red("Please select atleast one GraphQL eventsource")) + console.log( + chalk.red("Please select atleast one GraphQL eventsource") + ); } else { - await createSwaggerFile(answers.selectedOptions) + await createSwaggerFile(answers.selectedOptions); } } catch (error) { console.error(error); @@ -518,8 +622,8 @@ export const genGraphqlSchema = async () => { } runPrompt(); } catch (error) { - console.log(error) - }; + console.log(error); + } const createSwaggerFile = async (eventSources: string[]) => { const eventPath = path.join(process.cwd(), "/src/events"); @@ -527,57 +631,83 @@ export const genGraphqlSchema = async () => { const allEventsSchema: PlainObject = await loadYaml(eventPath, true); //all events of the project const definitions: PlainObject = await loadYaml(definitionsPath, false); eventSources.map(async (eventSourceName: string) => { - logger.info('Generating graphql schema for %s. First we will create swagger schema in %s', eventSourceName, os.tmpdir()); + logger.info( + "Generating graphql schema for %s. First we will create swagger schema in %s", + eventSourceName, + os.tmpdir() + ); // Find out the events for this eventSourceName key const eventSchemas = Object.fromEntries( Object.entries(allEventsSchema).filter(([key]) => { const eventSourceKey = key.split(".")[0]; - // eventSourceKey is the name of eventsources in this event definition. + // eventSourceKey is the name of eventsources in this event definition. // It could be one like 'http' or more like 'http & graphql' if (eventSourceKey == eventSourceName) { return true; } - const eventSources = eventSourceKey.split('&').map((s) => s.trim()); + const eventSources = eventSourceKey.split("&").map((s) => s.trim()); return eventSources.includes(eventSourceName); }) ); if (Object.keys(eventSchemas).length === 0) { - logger.fatal(chalk.red(`Did not find any events for the ${eventSourceName} eventsource. Why don't you define the first one in the events folder?`)) + logger.fatal( + chalk.red( + `Did not find any events for the ${eventSourceName} eventsource. Why don't you define the first one in the events folder?` + ) + ); process.exit(1); } // let swaggerSchema = await generateSwaggerui(eventSchemas,definitions); - let eventSourceConfig = yaml.parse(fsExtras.readFileSync(process.cwd() + `/src/eventsources/${eventSourceName}.yaml`, { encoding: 'utf-8' }) ); + let eventSourceConfig = yaml.parse( + fsExtras.readFileSync( + process.cwd() + `/src/eventsources/${eventSourceName}.yaml`, + { encoding: "utf-8" } + ) + ); //The yaml file of the eventsource - let swaggerSchema = generateSwaggerJSON(eventSchemas, definitions, eventSourceConfig); + let swaggerSchema = generateSwaggerJSON( + eventSchemas, + definitions, + eventSourceConfig + ); // For swagger-to-graphql plugin we need to save this file somewhere - const swaggerFilePath = path.join( os.tmpdir(), eventSourceName + '-swagger.json'); - + const swaggerFilePath = path.join( + os.tmpdir(), + eventSourceName + "-swagger.json" + ); + // Write the swagger.json file in the temp folder - await fsExtras.writeFileSync(swaggerFilePath, JSON.stringify(swaggerSchema, null, 2)); - - logger.info('Generated and saved swagger schema at temporary location %s. Now generating graphql schema from the same.', swaggerFilePath); - + await fsExtras.writeFileSync( + swaggerFilePath, + JSON.stringify(swaggerSchema, null, 2) + ); + + logger.info( + "Generated and saved swagger schema at temporary location %s. Now generating graphql schema from the same.", + swaggerFilePath + ); + // genereate graphql schema await generateGraphqlSchema(eventSourceName, swaggerFilePath); - }); + }; +}; - - } -} - -async function generateGraphqlSchema(eventSourceName: string, swaggerFilePath: string) { +async function generateGraphqlSchema( + eventSourceName: string, + swaggerFilePath: string +) { const command = `npx swagger-to-graphql --swagger-schema=${swaggerFilePath} > ./src/eventsources/${eventSourceName}.graphql`; exec(command, (error: any, stdout: any, stderr: any) => { if (error) { console.log( - chalk.red.bold(`Failed to generate Graphql schema for eventsource ${eventSourceName}`) - ); - console.log( - chalk.red(error.message) + chalk.red.bold( + `Failed to generate Graphql schema for eventsource ${eventSourceName}` + ) ); + console.log(chalk.red(error.message)); return; } if (stderr) { @@ -585,40 +715,45 @@ async function generateGraphqlSchema(eventSourceName: string, swaggerFilePath: s return; } console.log( - chalk.green(`Graphql schema generated successfuly for eventsource ${eventSourceName} at ./src/eventsources/${eventSourceName}.graphql`) - + chalk.green( + `Graphql schema generated successfuly for eventsource ${eventSourceName} at ./src/eventsources/${eventSourceName}.graphql` + ) ); }); } -const generateSwaggerui = async (eventsSchema: PlainObject, definitions: PlainObject) => { +const generateSwaggerui = async ( + eventsSchema: PlainObject, + definitions: PlainObject +) => { let finalSpec: PlainObject = {}; const swaggerCommonPart = { - "openapi": "3.0.0", - "info": { - "version": "0.0.1", - "title": "Godspeed: Sample Microservice", - "description": "Sample API calls demonstrating the functionality of Godspeed framework", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "name": "Mindgrep Technologies Pvt Ltd", - "email": "talktous@mindgrep.com", - "url": "https://docs.mindgrep.com/docs/microservices/intro" + openapi: "3.0.0", + info: { + version: "0.0.1", + title: "Godspeed: Sample Microservice", + description: + "Sample API calls demonstrating the functionality of Godspeed framework", + termsOfService: "http://swagger.io/terms/", + contact: { + name: "Mindgrep Technologies Pvt Ltd", + email: "talktous@mindgrep.com", + url: "https://docs.mindgrep.com/docs/microservices/intro", + }, + license: { + name: "Apache 2.0", + url: "https://www.apache.org/licenses/LICENSE-2.0.html", }, - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html" - } }, - "paths": {} + paths: {}, }; let swaggerSpecBase = JSON.parse(JSON.stringify(swaggerCommonPart)); finalSpec = swaggerSpecBase; Object.keys(eventsSchema).forEach((event: any) => { - let apiEndPoint = event.split('.')[2]; - apiEndPoint = apiEndPoint.replace(/:([^\/]+)/g, '{$1}'); //We take :path_param. OAS3 takes {path_param} - const method = event.split('.')[1]; + let apiEndPoint = event.split(".")[2]; + apiEndPoint = apiEndPoint.replace(/:([^\/]+)/g, "{$1}"); //We take :path_param. OAS3 takes {path_param} + const method = event.split(".")[1]; const eventSchema = eventsSchema[event]; //Initialize the schema for this method, for given event @@ -644,4 +779,3 @@ const generateSwaggerui = async (eventsSchema: PlainObject, definitions: PlainOb return finalSpec; }; -